1. 需求描述
最近开发的一个项目,涉及到这样一个需求:随着项目的不断推进,后台管理功能逐渐增多,与此同时,静态路由表也逐渐扩大,需要把静态路由方式转换为动态路由方式。要完成这样一个转换,有几个技术要点需要解决,其中一个就是后端的实现方式。那么,后端如何实现,对后端的侵入性最小呢?基于AOP的思想,想到了基于注解的方式。
2. 实现思路
后台项目,最开始的时候,是实现简单的权限认证,即在前端的登录请求login中,传入请求参数(String json)json中封装用户名和密码。后台通过json对象查找数据库,比对用户名和密码。认证成功后,生成一个用户对象user返回前端,在user对象中,封装token,供前端使用,这是在静态路由的情况下,典型的登录返回方式。
那么,实现动态路由后,后端有什么改变呢?
后端的改变比较明显的就是,之前在前端存放的路由数据,现在放在数据库中,需要登录的时候,随user对象一起返回前端。 也就是,在返回前端的user对象中,需要增加一个treeMenu对象。该对象需要:通过login方法的参数获取用户名,通过用户名在用户权限表中查找该用户对应的权限,通过权限菜单表查询到对应的菜单ids,在菜单表中,查找到该用户的可用菜单信息。概括起来,即该对象需要通过login的参数,从数据库中查找到对应的菜单信息,然后,把该菜单信息添加到login返回的对象中,返回前端。
最简单的方式,就是在login方法返回之前,在里面插入一个调用菜单的方法,返回菜单列表,然后把该菜单列表添加进user对象中,返回前端。 但是,该方式对login方法内部具有侵入性,即当需要从动态路由,再次切换到静态路由的时候,需要再次修改login方法里面的代码。
如果在不改变login方法内部实现的情况下,仅通过添加一个注解就实现了想要的功能,那不是更好吗?
下面要说的,就是基于注解的方式,实现后的效果如下图所示:
3. 具体实现
下图中,为login登录方法的实现效果。
即login登录方法内部不做改变,仅在该方法上,添加一个注解@AddTreeMenu。
对应的注解代码如下所示:
/**
* 在Controller方法上加入此注解,利用该方法的参数给该方法的返回值添加 菜单树
*/
@Target( { ElementType.METHOD } )
@Retention( RetentionPolicy.RUNTIME )
@Documented
public @interface AddTreeMenu {
}
有了注解后,当然还需要编写注解解析器,通过Component的方式把Aspect对象注入容器。当前端发送登录请求,后台收到请求后,会进入该切面中。在该切面的环绕通知中,获取login的参数,从Service层请求菜单列表,然后封装到返回结果中,返回供前端Vue动态路由使用。
核心代码如下:
@Around("pointCut()")
public Object around(ProceedingJoinPoint proceedingJoinPoint) throws Throwable {
MethodSignature signature = (MethodSignature) proceedingJoinPoint.getSignature();
Method method = signature.getMethod();
Object proceed = proceedingJoinPoint.proceed();
if (!method.isAnnotationPresent(AddTreeMenu.class)) {
return proceed;
}
ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
if (attributes != null) {
// 获取参数
String json = attributes.getRequest().getParameter("json");
// 数据库查询菜单列表
List<Map<String, Object>> treeMenu = service.getTreeMenuByUser(json);
// 把菜单列表写入返回值里面。
((RespEntity<Map<String, Object>>)proceed).getData().put("treeMenu",treeMenu);
}
return proceed;
}
完整的实现如下:
@Aspect
@Component
public class AddTreeMenuAspect {
@Autowired
private BaseService service;
private static final Logger logger = LoggerFactory.getLogger(AddTreeMenuAspect.class);
@Pointcut(AnnotationConst.ANNOTATION_ADD_TREE_MENU)
public void pointCut(){
}
@Before("pointCut()")
public void before(JoinPoint joinPoint){}
@Around("pointCut()")
public Object around(ProceedingJoinPoint proceedingJoinPoint) throws Throwable {
MethodSignature signature = (MethodSignature) proceedingJoinPoint.getSignature();
Method method = signature.getMethod();
Object proceed = proceedingJoinPoint.proceed();
if (!method.isAnnotationPresent(AddTreeMenu.class)) {
return proceed;
}
ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
if (attributes != null) {
// 获取参数
String json = attributes.getRequest().getParameter("json");
// 数据库查询菜单列表
List<Map<String, Object>> treeMenu = service.getTreeMenuByUser(json);
// 把菜单列表写入返回值里面。
((RespEntity<Map<String, Object>>)proceed).getData().put("treeMenu",treeMenu);
}
return proceed;
}
/**
* 返回通知
* */
@AfterReturning(pointcut = "pointCut()", returning = "result")
public void afterReturning(JoinPoint joinPoint, Object result) {
}
/**
* 异常通知
* */
@AfterThrowing(pointcut = "pointCut()", throwing = "throwable")
public void afterThrowing(JoinPoint joinPoint, Throwable throwable){
}
@After("pointCut()")
public void after(JoinPoint joinPoint){}
}
4. 结语
本文主要介绍了Vue静态路由切换为Vue动态路由的过程中,后端的可实现方式和侵入性很小的基于注解的实现方式及对应源码的实现。
在后端开发中,经常会出现想添加一个功能,但又不想对原来的功能代码进行修改,修改可能会造成代码可维护性降低,这个时候,可以考虑AOP的思路。与之类似的典型案例,包括登录日志的记录、操作日志的记录、后端异常的统一处理,后端事务的处理,等等。