你遇到过Spring Aop失效的场景吗-如何解决-有替换方案吗

背景

最近以前的同事遇到这样一个问题:

需求:对于系统提供的接口有些需要登陆验证,有些则不需要验证,通过代码实现此功能。

说明:系统使用的是springboot框架,采用Java+kotlin混合编码。

方案一:利用Spring AOP实现

1. 定义自定义登录验证注解

定义自定义登录注解LoginCheck,只能添加到方法上。

@Target(ElementType.METHOD)
@Documented
@Retention(RetentionPolicy.RUNTIME)
public @interface LoginCheck {
}
2. 编写登录验证AOP处理器

使用spring aop对添加了LoginCheck注解的方法做切面,采用around方式对指定方法做登录验证。

@Aspect
@Component
@Slf4j
public class LoginAop {

    @Pointcut("@annotation(LoginCheck)")
    public void pointcut(){}

    @Around("pointcut()")
    public Object aop(ProceedingJoinPoint point){
        try {
            // 登录校验逻辑(省略)
            return point.proceed();
        } catch (Throwable throwable) {
            log.error("error:",throwable);
        }
        return null;
    }
}
3. 对需要添加登录验证的接口做改造
@RestController
@RequestMapping("/test")
class TestController (
     val goodsService: GoodsService
){


    @GetMapping("demo01")
    @LoginCheck
    fun demo01(){
        goodsService.getOne();
    }
}
4. 启动验证,程序报错了

你遇到过Spring Aop失效的场景吗-如何解决-有替换方案吗_第1张图片

可以看到原来kotlin的类默认是final类型的,而Spring对于类做Aop,采用的方案是使用Cglib动态生成代理对象即源类的子类对象实现的,我们知道final关键字修饰的类是不能被之类继承的,这就导致了上面的报错。

5. 解决报错,新的问题出现了

修改Kotlin类为open类型的即可,其中kotlin类修饰符如下:

修饰符 相应类的成员 注解
final 不能被覆写 在kotlin中默认所有的方法和类都是final属性
open 可以被覆写 需要被明确指出
abstract 必须要覆写 不能被实例化,默认具有open属性。
override 覆写超类的方法 如果没有被指定为final,则默认具有open属性

启动程序一切正常,当我们调用接口时,程序NPE

你遇到过Spring Aop失效的场景吗-如何解决-有替换方案吗_第2张图片

可以看到是goodsService注入失败,我们修改goodsService的注入方式为注解注入@Autowired,程序启动成功,接口调用成功。

方案二:使用filter方式实现

通过方案一可以看到kotlin下使用spring 的AOP功能要特别小心,虽然我们再最后解决了所有的问题,但这这种做法打破了kotlin的默认规则很不友好,比如新上手项目的同学就不会想到在类上添加open修饰符,使用bean的注入也势必想象到用构造函数的方式。

方案二采用新的方式,不用修改kotlin任何默认规则即可实现类似aop功能。具体操作如下:

1. 定义自定义注解同方案一
2. 项目启动时动态获取需要登录验证的接口方法

其中主要逻辑为:

  1. 项目启动完成后获取RequestMappingHandlerMapping,该对象存储所有接口地址及对应的方法的映射
  2. 遍历所有映射关系,将添加了LoginCheck注解的接口加入urlSet集合中备用
@Slf4j
@SpringBootApplication
public class App {

    public static ConfigurableApplicationContext application = null;

    public static final Set urlSet = new HashSet<>();

    public static void main(String[] args) {
        log.info("Application:recharge-center 启动开始");
        application = SpringApplication.run(App.class, args);
        Environment env = application.getEnvironment();
        final String contextPath = env.getProperty("server.context-path", "/");
        RequestMappingHandlerMapping handlerMapping = application.getBean(RequestMappingHandlerMapping.class);
        Map handlerMethods = handlerMapping.getHandlerMethods();
        for (Map.Entry entry : handlerMethods.entrySet()) {
            final HandlerMethod handlerMethod = entry.getValue();
            final Method method = handlerMethod.getMethod();
            LoginCheck annotation = method.getAnnotation(LoginCheck.class);
            if(annotation == null){
                continue;
            }
            String code = annotation.code();
            String[] fullPaths = new String[]{""};
            final RequestMapping classReq = handlerMethod.getBeanType().getAnnotation(RequestMapping.class);
            if(classReq != null){
                fullPaths = classReq.value();
            }
            Class[] clazzs = new Class[]{RequestMapping.class,PostMapping.class,GetMapping.class};
            for (Class clazz : clazzs) {
                final Annotation methodAnnotation = method.getAnnotation(clazz);
                if(methodAnnotation != null){
                    try {
                        final Method valueMethod = clazz.getDeclaredMethod("value");
                        for (String s : (String[]) valueMethod.invoke(methodAnnotation)) {
                            for (String fullPath : fullPaths) {
                                urlSet.add((contextPath + "/" + fullPath + "/" +s).replaceAll("/+","/"));
                            }
                        }
                    } catch (Exception e) {
                        log.error("初始化APP异常:",e);
                    }
                    break;
                }
            }
        }
    }
}
3. 配置过滤器拦截所有请求

过滤所有请求通过判断请求地址是否在第3步urlSet集合中来决定是否需要登录验证

@Bean
public FilterRegistrationBean registerDefaultFilter() {
    FilterRegistrationBean registration = new FilterRegistrationBean();
    registration.setFilter(new Filter() {

        @Override
        public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
            HttpServletRequest request = (HttpServletRequest) servletRequest;
            HttpServletResponse response = (HttpServletResponse) servletResponse;
           
            String requestURI = request.getRequestURI().replaceAll("/+","/");
            if(requestURI.contains("?")){
                requestURI = requestURI.substring(0,requestURI.indexOf("?"));
            }
            final String url = matchUrl(requestURI);
            if(StringUtils.isNotBlank(url)){
                //登录验证具体逻辑(省略)
                filterChain.doFilter(requestWrapper, servletResponse);
            }else{
                filterChain.doFilter(requestWrapper, servletResponse);
            }
        }
    
        private String matchUrl(String uri){
            if(App.urlSet.containsKey(uri)){
                return uri;
            }else{
                for (String str : App.urlSet) {
                    final String[] sysStrs = str.split("/");
                    final String[] webStrs = uri.split("/");
                    if(sysStrs.length != webStrs.length){
                        return null;
                    }
                    boolean match = true;
                    for (int i = 0; i < sysStrs.length; i++) {
                        match &= sysStrs[i].matches("^\\{.+\\}$") | StringUtils.equals(sysStrs[i],webStrs[i]);
                    }
                    if(match){
                        return uri;
                    }
                }
            }
            return null;
        }

    });
    registration.addUrlPatterns("/*");
    registration.setName("defaultFilter");
    registration.setOrder(10);  //值越小,Filter越靠前。
    return registration;
}
4. 启动验证,完美通过

你可能感兴趣的:(技术文章,java,spring,aop,filter,kotlin)