springboot general branch processing --- still hardcoding special processing logic? Super administrators should not be treated differently

foreword

  • When the login module is introduced, we need to make the menu. The menu naturally requires the participation of permissions, and the fine-grained permissions we designed in springboot are relatively fine. When we query the menu, we need to find the corresponding menu according to the permissions. But in springboot I designed a bottom super administrator
  • Let's take a look at some of the code I used to implement this super administrator menu at the beginning
 if (SecurityUtils.getSubject().hasRole(RoleList.SUPERADMIN)) {
     listemp = customMapper.selectRootMenusByRoleIdList(null,null, null, null);
 } else {
     listemp = customMapper.selectRootMenusByRoleIdList(roleList, oauthClientId,null, moduleCodes);
 }
  • This is a very normal idea to implement the branch execution idea by judging whether the role is a super administrator, but the super administrator may involve multiple places. If this if else is executed in every place, I think it is a bit low, so I decided to remodel it. The idea of ​​insufficient final execution is still the if else judgment. It's just that we don't have such a mix of functions at the code level.

custom annotation

  • First of all, I need two annotations, SuperDirectionand SuperDirectionHandlerthe target function that respectively indicates the need to judge the super administrator branch and the specific administrator branch. This sentence is still a bit abstract, let me take it slow!

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 {
 }

effect

  • SuperDirectionIt is used to indicate that the method needs to judge the super administrator, and the value stored is the judgment expression. We will introduce this expression later
  • SuperDirectionHandlerIt is not difficult for us to find that he has no actual properties but an additional @Componentannotation. The purpose is to facilitate Spring to manage the annotation; so that we can get the class marked by the annotation through Spring.

Location

  • This annotation is for global use. In the introduction of maltcloud structure, we know that the org.framework.core module is the cornerstone of all modules, so I choose these two annotations in the org.framework.coremodule

image-20211013155948064.png

section

  • We want to make a conditional judgment before the method is executed. There are many options. We can intercept the method in the filter to judge which one to execute, but we cannot directly obtain the relevant information of the method in the filter. Note that it is impossible to directly Get, if you want to implement it in the filter, it is not impossible, if you are interested in this scheme, you can try it
  • 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来使用了。这里名叫commonTestServiceImplCommonTestService接口的普通实现类,用于实现我们正常的操作。
 @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

  • 这里有两个实现类分别是CommonTestServiceImplCommonTest2ServiceImpl
 @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

image-20211013162826929.png

  • 通过上面代码我们可以看出来我们当前是没有角色的,所以我们可以理解成调用接口是没有超级管理员接口。那么就会执行我们CommonTestServiceImpl中的方法。然后我们在将这里的hasAnyRole改成true , 会发现就会执行CommonTest2ServiceImpl里的方法。

总结

  • 经过上面这么折腾,我们就可以在涉及到超级管理员的地方重新实现一下,然后再原有的实现类中只需要专注我们权限架构中的规则进行数据库查询等操作了,而不需要向我一开始那样为超级管理员进行特殊操作。如果后续我们需要为特殊用户进行特殊开发。我们就可以扩展我们的表达式解析然后再开发我们备用接口就可以了。
  • 这个思路主要来自于Spring Cloud 中的OpenFeign 的容灾降级的思路。

我正在参与掘金技术社区创作者签约计划招募活动,点击链接报名投稿。

Guess you like

Origin juejin.im/post/7121903722099114015