springboot通用分支处理---还在硬编码特殊处理逻辑?超级管理员不应该被区别对待

Posted 十一技术斩

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了springboot通用分支处理---还在硬编码特殊处理逻辑?超级管理员不应该被区别对待相关的知识,希望对你有一定的参考价值。

前言

  • 当引入登录模块后我们需要做菜单。而菜单自然需要权限的参与,我们在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判断 。 只不过让我们在代码层面上功能间不在那么杂糅在一起

自定义注解

  • 首先我需要两个注解,SuperDirectionSuperDirectionHandler分别表示需要判断超级管理员分支和具体管理员分支的目标函数 。 这句话说的还是有点抽象的,容我慢慢道来!

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来使用了。这里名叫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

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

总结

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

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

 

如果本文对你有帮助,别忘记给我个3连 ,点赞,转发,评论,

学习更多JAVA知识与技巧,关注博主学习JAVA 课件,源码,安装包,还有最新大厂面试资料等等等

咱们下期见。

收藏 等于白嫖,点赞才是真情。


 

Java:处理大数据的通用 BaseN 编码器/解码器

【中文标题】Java:处理大数据的通用 BaseN 编码器/解码器【英文标题】:Java: Universal BaseN encoder/decoder working with large data sizes 【发布时间】:2017-03-23 06:49:51 【问题描述】:

我正在寻找一个不错的 Java BaseN 编码器(带有自定义字符集),它不受输入数据大小(字节数组)的限制。

类似这样的:

https://github.com/mklemm/base-n-codec-java

但是对于“无限”数据长度,没有任何不必要的内存/性能损失和“BigInteger 滥用魔法”。只是作为标准 BASE64 编码器工作的东西,但普遍适用于任何基本/字符集。欢迎任何解决方案或想法如何实现。

也许,如果有人有使用 apache BaseNCodec 的经验:

https://commons.apache.org/proper/commons-codec/apidocs/org/apache/commons/codec/binary/BaseNCodec.html

它看起来很有希望,但是它是一个抽象类,并且可用的实现看起来比从头开始更难。


我需要将二进制数据用于自定义字符集编码器(其中字符集中的字符数是可变的,"ABCDE" = Base5"ABCDE-+*/." = Base10,...)。
更新: 来自 GitHub(上图)的“Base N Codec”似乎有问题,所以我最后使用了以下代码:

https://dzone.com/articles/base-x-encoding

【问题讨论】:

如果您的基数不是 2 的幂,您如何定义一个不将数据视为潜在(非常)大整数并因此需要 BigInteger 或等价物的编码? @dave_thompson_085 是的,但应该“虚拟地”逐个序列地完成,如有必要(不直接使用 BigInteger),会溢出到另一个序列。否则,它将“针对相对较短的字节序列(最多约 1000 个字节)”(如github.com/mklemm/base-n-codec-java 中的状态)。 几乎不可能,如果“通用”是指解码/编码现有标准,如 Base85、Base64 等(不同的填充策略和分组方法)。如果你的意思是像 Integer.toString(i, radix) 那么这个用例是什么? @Durandal 用例:我需要将它用于自定义字符集编码器的二进制数据(其中字符集中的字符数是可变的,"ABCDE" = Base5"ABCDE-+*/." = Base10,...)。 也许我的问题不够精确。决定性的一点是,您是否绝对需要以尽可能少的浪费位对二进制数据进行编码(将整个流视为一个数字),或者一些浪费不是问题(例如 Base85 编码在 5 个符号中编码 4 个字节,仅产生 85^5 种可能性中的 2^32 个)。如果您只是寻求使用有限的符号集对传输进行编码,接受一些浪费的位,它会将问题简化为只需选择大量符号来编码相当少的字节数。 【参考方案1】:

一般答案:否。特殊情况:是,对于底数是 2 的幂。

为什么?因为 Q 中的思想处于“激烈竞争”中(实际上可能是“矛盾”)。

    作为输入,您希望在某个基数 N 中支持无限整数(将其视为 BigIntegerBaseN)。作为输出,您需要支持某个基数 M 中的无限整数(将其视为 BigIntegerBaseM)。 您想要执行基本转换 - 在数学上定义为一系列(乘法和加法)和除法。见http://www.cut-the-knot.org/recurrence/conversion.shtml 和https://math.stackexchange.com/questions/48968/how-to-change-from-base-n-to-m。 您希望找到一种无需在 BigIntegers 上(在任何实现基础中)进行乘法和除法即可计算此类结果的方法。

你能不进行乘除运算就确定乘除运算的结果吗?不。这是一个矛盾。当你得到结果时,根据定义,你已经进行了计算。

因此,这不是您能否避免计算的问题,而是如何简化计算的问题。

如果 N 和/或 M 的底数是 2 的幂,则乘法/除法可以通过简单的位移计算 = 与主要流线型相同的计算。这可以通过避免 BigInteger 计算来完成。 否则,您可以缓存某些重复计算,将中间结果存储在数组或 HashMap 中,然后通过流线型获得相同的计算。但是仍然需要 BigInteger 计算(但要避免冗余重复)。

希望对您的方法有所帮助。 :)

【讨论】:

【参考方案2】:

如果 N 是 2 的幂,则基数 N 编码非常有效,因为这样可以在固定大小的数字组和固定大小的字节之间进行转换。

Base64:26 - 每位 6 位,因此 4 位 = 24 位 = 3 个字节。

否则学校乘法必须在整个长度上发生,导致大量“BigInteger”计算。

比重复地乘以/除以基数 N 更快一点,是拥有一个 N 的幂数组。

对于将字节数组编码为数字,您可以使用 N0、N1、N2 , N3, ... 作为长度更小或相等的字节数组,并进行重复减法。

byte 已签名,short 可能更适合。假设数字的最高字节是 98,较小的 N 次方是 12,那么大约 7 就是那个数字。

对于将数字解码为字节数组,可能会使用相同的幂。

玩得开心。

【讨论】:

如果我能很好地理解第二个选项,那么对于大数据量来说,它不会对资源非常友好。 是的:N 的所有幂直到数字。可以懒惰和静态地完成。对于编码/解码一次相当开销。另一方面,空间需求小于 N-log(n).(n/256) 字节(n 是数字),并且您已经需要 (n/256) 字节作为数字.适合大 N。【参考方案3】:

您提到了两种截然不同的方法。 Github implementation 中使用的 BaseN 算法使用在基数之间转换整数的数学符号。这相当于说 10 与 base-8 算术中的 12 或 base-2 算术中的 1010 相同。该算法将字节流解释为一个大数并转换为分配的基数。

Base64 是一种非常不同的方法,您可以在Wikipedia Base64 page 中看到一个示例。该算法基本上将输入流拆分为每个元素的 6 位数组。 2^6 = 64,因此命名为 Base64。它有一个包含 64 个不同字符的表,并将数组(6 位)中的每个元素显示到相应的转换表中。

我认为您需要选择两种方法之一,因为它们非常不同并且彼此不兼容。至于实现细节,如果选择第二种方法,我认为这更容易实现,因为您基本上将流分成固定大小的部分并根据您自己的表格对其进行编码。

第一种方法可能会变得相当复杂,因为任意算术运算都依赖于相当复杂的结构。你可以看看现有的软件,再次@Wikipedia' s list of arbitrary-precision arithmetic software。

实际上,我认为在某些时候您会发现很难为您的转换获取字符(随着基数增加或位数增加),除非您将使用整个 Unicode 字母表:)。

希望对我有所帮助

【讨论】:

以上是关于springboot通用分支处理---还在硬编码特殊处理逻辑?超级管理员不应该被区别对待的主要内容,如果未能解决你的问题,请参考以下文章

initialValues仅在硬编码时预填充数据,但不在动态数据中预填充数据

dart系列之:还在为编码解码而烦恼吗?用dart试试

还在滥用try-catch处理异常?看看springboot的优雅实现吧

springboot2.0处理任何异常返回通用数据格式

减少通用树遍历中的分支

Python基础知识进阶(五---2)----程序基本结构简单分支异常处理三大实例分析基本循环结构通用循环构造方法死循环嵌套循环布尔表达式