前言
- 当引入登录模块后我们需要做菜单。而菜单自然需要权限的参与,我们在springboot中设计的权限细粒度还算是比较细的。当我们查询菜单是需要根据权限查找对应的菜单。但是在springboot中我设计了一个底层超级管理员
- 先来看看我一开始实现这个超级管理员菜单获取的部分代码
?if (SecurityUtils.getSubject().hasRole(RoleList.SUPERADMIN)) {
? ? ?listemp = customMapper.selectRootMenusByRoleIdList(null,null, null, null);
?} else {
? ? ?listemp = customMapper.selectRootMenusByRoleIdList(roleList, oauthClientId,null, moduleCodes);
?}
- 这样实现是很正常的思路,通过判断角色是否是超级管理员来做分支执行思路,但是超级管理员可能涉及到多个地方如果在每个地方都这样if else执行的,我觉得有点low, 所以我决定改造一下。不够最终执行的思路依然是if else判断 。 只不过让我们在代码层面上功能间不在那么杂糅在一起
自定义注解
- 首先我需要两个注解,
SuperDirection 和SuperDirectionHandler 分别表示需要判断超级管理员分支和具体管理员分支的目标函数 。 这句话说的还是有点抽象的,容我慢慢道来!
SuperDirection
?@Target({ElementType.TYPE, ElementType.METHOD})
?@Retention(RetentionPolicy.RUNTIME)
?@Documented
?public @interface SuperDirection {
??
? ? ?String value() default StringUtils.EMPTY;
??
?}
SuperDirectionHandler
?@Target({ElementType.TYPE, ElementType.METHOD})
?@Retention(RetentionPolicy.RUNTIME)
?@Documented
?@Component
?public @interface SuperDirectionHandler {
?}
作用
SuperDirection 是用于表明该方法需要进行判断超级管理员,而value值存储的就是判断的表达式。关于这个表达式我们后面介绍SuperDirectionHandler 我们不难发现他没有实际属性但是多了一个@Component 注解。目的是方便Spring管理该注解;这样我们就可以通过Spring来获取被该注解标注的类了。
位置
- 该注解释放给全局使用的,在maltcloud结构介绍中我们知道org.framework.core模块是所有模块的基石,所以这两个注解我选择在
org.framework.core 模块中
切面
- 我们想在方法执行前进行条件判断,可选方案有很多我们可以在filter中拦截方法进行判断选择执行哪一个,但是过滤器中我们无法直接获取到方法的相关信息,注意这里说的是无法直接获取,如果你想在filter中实现也不是不行,这种方案感兴趣的可以试试
- spring的另外一个特性切面正好符合我们的需求,我们只需要在aroud环绕方法中实现我们的需要。
- 首先我们定义一个切点,切点拦截所有被
SuperDirection 注解标注的类或者方法。
?/**定义一个切点; 拦截所有带有SuperDirection注解的类和方法*/
?@Pointcut("@annotation(com.github.zxhtom.core.annotaion.SuperDirection) || @within(com.github.zxhtom.core.annotaion.SuperDirection)")
?public void direction() {
??
?}
- 这里稍作一下解释
@annotation 用于标识方法上的SuperDirection , @within 用于标识在类上的SuperDirection 。 - 正常我们的一个业务处理都是放在service层的,spring中的三层架构的service正常是实现一个接口然后实现。所以我们这里在切面中先获取被拦截对象实现的接口。获取到接口信息我们通过接口信息获取该接口在spring中的其它实现类
- 在spring容器中提供了获取bean集合的方法,加上我们maltcloud中实现了获取
ApplicationContext 的工具类,所以我们通过如下来获取bean集合
?Map<String, ?> beansOfType = ApplicationContextUtil.getApplicationContext().getBeansOfType(接口class);
- 获取到集合了,此时我们还是无法确定哪一个实现类使我们替补执行超级管理员的bean 。 这就需要我们
SuperDirectionHandler 注解了。 - 这个时候我们在通过Spring获取被该注解标识的类。这个时候获取到很多不想关类,我们在和上面的
beansOfType 进行比对。就可以确定哪一个实现bean是我们需要的。
?@Around("direction()")
?public Object aroud(ProceedingJoinPoint pjp) throws Throwable {
? ? ?Class<?>[] inters = pjp.getTarget().getClass().getInterfaces();
? ? ?for (Class<?> inter : inters) {
? ? ? ? ?Map<String, ?> beansOfType = ApplicationContextUtil.getApplicationContext().getBeansOfType(inter);
? ? ? ? ?Map<String, Object> beansWithAnnotation = ApplicationContextUtil.getApplicationContext().getBeansWithAnnotation(SuperDirectionHandler.class);
? ? ? ? ?for (Map.Entry<String, ?> entry : beansOfType.entrySet()) {
? ? ? ? ? ? ?if (beansWithAnnotation.containsKey(entry.getKey())) {
? ? ? ? ? ? ? ? ?try {
? ? ? ? ? ? ? ? ? ? ?return doOthersHandler(entry.getValue(), pjp);
? ? ? ? ? ? ? ? } catch (Exception e) {
? ? ? ? ? ? ? ? ? ? ?log.error("分支执行失败,系统判定执行原有分支....");
? ? ? ? ? ? ? ? }
? ? ? ? ? ? }
? ? ? ? }
? ? }
? ? ?return pjp.proceed();
?}
- 当确定执行类之后,我们只需要携带着该bean ,在根据
SuperDirection 上的表达式进行判断是执行超级管理员实现类还是原有实现类的方法了。
条件判断
- 在Aspect执行中原有方法的执行很简单,只需要
pjp.proceed() 就可以了。所以这里我们先获取一下上面获取到的超级管理员实现bean的对应方法吧。
?MethodSignature msig = (MethodSignature) pjp.getSignature();
?Method targetMethod = value.getClass().getDeclaredMethod(msig.getName(),msig.getParameterTypes());
- 然后我们在获取
SuperDirection 注解信息,因为该注解可能在类上,也可能在方法上,所以这里我们需要处理下
?SuperDirection superDirection = null;
?superDirection = targetMethod.getAnnotation(SuperDirection.class);
?if (superDirection == null) {
? ? ?superDirection = pjp.getTarget().getClass().getAnnotation(SuperDirection.class);
?}
?if(selectAnnotationChoiceDo(superDirection)){
? ? ?//如果表达式验证通过,则执行替补bean实现类
? ? ?return targetMethod.invoke(value,pjp.getArgs());
?}
?//否则执行原有bean实现类
?return pjp.proceed();
表达式解析
- 表达式解析涉及两个模块,一个是登录模块中获取当前登录用户的角色,另外一个是我们上面提到的表达式解析
- 获取当前登录用户信息类我在
org.framework.web 中提供了bean 。 具体的实现由各个登录子模块负责去实现,这里我们只需要引入spirng bean使用就要可以了。这里充分体现了模块拆分的好处了。 - 至于表达式解析,我选择放在本模块中
org.framework.commons 。 这里目前简单提供了几个表达式解析。
?public interface RootChoiceExpression {
? ? ?public boolean haslogined();
? ? ?public boolean hasRole(String role);
? ? ?public boolean hasAnyRole(String... roles);
?}
- 他们分别是验证是否登录、是否拥有角色和角色组。关于他的视线最终也还是依赖上面提到的
org.framework.web 模块中的登录用户的信息类
?@Service
?public class DefaultChoiceExpression implements RootChoiceExpression {
? ? ?@Autowired
? ? ?OnlineSecurity onlineSecurity;
??
? ? ?@Override
? ? ?public boolean haslogined() {
? ? ? ? ?return onlineSecurity.getOnlinePrincipal()!=null;
? ? }
??
? ? ?@Override
? ? ?public boolean hasRole(String role) {
? ? ? ? ?return onlineSecurity.hasAnyRole(role);
? ? }
??
? ? ?@Override
? ? ?public boolean hasAnyRole(String... roles) {
? ? ? ? ?return onlineSecurity.hasAnyRole(roles);
? ? }
?}
- 这里也算是流出扩展吧,后面根据项目需求我们可以重写该表达式解析,在根据自己的业务进行表达式新增。这里仅作为基础功能
?private boolean selectAnnotationChoiceDo(SuperDirection superDirection) throws NoSuchMethodException, InvocationTargetException, IllegalAccessException {
? ? ?String value = superDirection.value();
? ? ?if (StringUtils.isEmpty(value)) {
? ? ? ? ?return onlineSecurity.getRoleNames().contains(MaltcloudConstant.SUPERADMIN);
? ? }
? ? ?MethodInfo info = selectInfoFromExpression(value);
? ? ?Method declaredMethod = expression.getClass().getDeclaredMethod(info.getMethodName(), String.class);
? ? ?Object invoke = declaredMethod.invoke(expression, info.getArgs());
? ? ?if (invoke != null && invoke.toString().equals("true")) {
? ? ? ? ?return true;
? ? }
? ? ?return false;
?}
- 首先根据正则解析出方法名和参数。然后根据反射调用我们spring中表达式bean去执行我们在
SuperDirection 配置的表达式。通过上面我们又能发现目前表达式仅支持String传参。因为在SuperDireciton 传递过来的已经是String了,所以在这里目前我还没想到如何支持更多类型的数据。先埋坑吧! - 该方法最终决定执行原生方法还是替补方法。
演示使用
- 上面说的那么枯燥主要是因为是我的一个设计思路,下面我们来实操感受一下吧。
controller
- 首先我在controller中开发一个接口 。这里需要注意下因为我们上面会出现多个实现bean在spring中,所以我们在使用这些接口的时候就不能单纯的使用
@Autowired 了, 而需要通过beanName来使用了。这里名叫commonTestServiceImpl 的CommonTestService 接口的普通实现类,用于实现我们正常的操作。
?@RestController
?@RequestMapping(value = "/demo/common")
?public class CommonController {
? ?
? ? ?@Qualifier(value = "commonTestServiceImpl")
? ? ?@Autowired
? ? ?CommonTestService commonTestService;
??
? ? ?@RequestMapping(value = "/test",method = RequestMethod.GET)
? ? ?public void test() {
? ? ? ? ?commonTestService.test();
? ? }
?}
service
- 这里有两个实现类分别是
CommonTestServiceImpl 、CommonTest2ServiceImpl
?@Service
?@SuperDirectionHandler
?public class CommonTest2ServiceImpl implements CommonTestService {
? ? ?@Override
? ? ?public void test() {
? ? ? ? ?System.out.println("hello test 2");
? ? }
?}
?@Service
?@SuperDirection(value = "")
?public class CommonTestServiceImpl implements CommonTestService {
? ? ?@Override
? ? ?public void test() {
? ? ? ? ?System.out.println("hello i am test ing ...");
? ? }
?}
- 在controller层我们使用的是
CommonTestServiceImpl 用来实现正常的逻辑。而CommonTest2ServiceImpl 是针对超级管理员做的操作。我们就可以进行如上的配置。在SuperDirection 中配置空值标识判断超级管理员进行分支执行。你也可以配置目前支持的表达式,我这里简单点了。 - 然后通过
SuperDirectionHandler 标识ConmmonTest2ServiceImpl 是替补执行。
测试
- 由于为了简单测试,我在org.framework.demo.common模块中还没有引入login模块,所以此时登录用户获取类还是我们默认的
OnlineSecurityImpl
- 通过上面代码我们可以看出来我们当前是没有角色的,所以我们可以理解成调用接口是没有超级管理员接口。那么就会执行我们
CommonTestServiceImpl 中的方法。然后我们在将这里的hasAnyRole改成true , 会发现就会执行CommonTest2ServiceImpl 里的方法。
总结
- 经过上面这么折腾,我们就可以在涉及到超级管理员的地方重新实现一下,然后再原有的实现类中只需要专注我们权限架构中的规则进行数据库查询等操作了,而不需要向我一开始那样为超级管理员进行特殊操作。如果后续我们需要为特殊用户进行特殊开发。我们就可以扩展我们的表达式解析然后再开发我们备用接口就可以了。
- 这个思路主要来自于Spring Cloud 中的OpenFeign 的容灾降级的思路。
我正在参与掘金技术社区创作者签约计划招募活动,点击链接报名投稿。
?
如果本文对你有帮助,别忘记给我个3连 ,点赞,转发,评论,
学习更多JAVA知识与技巧,关注博主学习JAVA 课件,源码,安装包,还有最新大厂面试资料等等等
咱们下期见。
收藏 等于白嫖,点赞才是真情。
?
|