美文网首页
Sping中自定义注解的两种方式【AOP、Cglib】

Sping中自定义注解的两种方式【AOP、Cglib】

作者: rpf_siwash | 来源:发表于2019-03-06 15:16 被阅读0次

    Spring中经常会用到各种各样的注解@service、@compont等等,注解本身并没有什么神奇的,最初只是用来做文档标注,到后面用注解来标记类,通过反射去扫描注解中的信息并去完成自己的业务,而不是在方法体中嵌入业务代码,极大的提高了逼格和效率。本文将通过AOP和Cglib分别实现自定义注解类,以达到模拟redis的@CacheEvict类似作用,@CacheEvict注解可以在方法运行前,根据注解中的value值作为key,去redis中判断是否存在。若存在,拦截方法的运行,并直接返回redis中的value值,若不存在让方法执行,并同时将返回的value保存的redis中,已达到自动缓存的功能。而本文主要是实现通过class.method作为key,一个增强版缓存注解。

    java中常用的实现自定义注解的方式

    1.通过反射机制,这类注解只能动态的将bean的field动态赋值,不能拦截方法,也获取不到方法的参数值。比如:Autowired和Resource
    2.通过AOP:AOP切面编程,通过指定切面类中的切点(通常是注解类Class),将逻辑代码写在万金油——环绕@Around注解中。底层也用到了动态代理。
    3.通过动态代理:这类实际上是由代理类生成的bean在执行被代理类的方法,用于拦截Method的注解,本例使用cglib,一是性能更好,二是不用指定接口类。

    一、通过AOP实现

    1.首先创建一个普通的spring项目:


    在这里插入图片描述

    2.在pom中加入AOP相关依赖:

    <!-- https://mvnrepository.com/artifact/org.springframework/spring-aop -->
        <dependency>
            <groupId>org.springframework</groupId>
            <artifactId>spring-aop</artifactId>
            <version>4.3.18.RELEASE</version>
        </dependency>
        <!-- https://mvnrepository.com/artifact/org.aspectj/aspectjweaver -->
        <dependency>
            <groupId>org.aspectj</groupId>
            <artifactId>aspectjweaver</artifactId>
            <version>1.8.13</version>
        </dependency>
    

    3.创建配置文件

    <?xml version="1.0" encoding="UTF-8"?>
    <beans xmlns="http://www.springframework.org/schema/beans"
           xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
           xmlns:context="http://www.springframework.org/schema/context"
           xmlns:aop="http://www.springframework.org/schema/aop"
           xsi:schemaLocation="http://www.springframework.org/schema/beans
            http://www.springframework.org/schema/beans/spring-beans.xsd
            http://www.springframework.org/schema/context
            http://www.springframework.org/schema/context/spring-context.xsd
            http://www.springframework.org/schema/aop
            http://www.springframework.org/schema/aop/spring-aop.xsd">
    
        <!-- 开启注解扫描 -->
        <context:component-scan base-package="rpf.study.annotation"></context:component-scan>
        <!-- 开启AOP扫描 -->
        <aop:aspectj-autoproxy></aop:aspectj-autoproxy>
    </beans>
    

    关键的只需要包扫描和AOP扫描即可
    4.创建注解类

    @Target({ElementType.METHOD})
    @Retention(RetentionPolicy.RUNTIME)
    public @interface AutoCache {
        String value() default "";
    }
    

    注意作用域是:ElementType.METHOD
    5.创建模拟的业务类:
    userService:

    @Service
    public class UserService {
    
        @AutoCache
        public String shit(){
            System.out.println("shit^shit^shit");
            return "200";
        }
    }
    

    模拟的redis工具包:

    @Component
    public class SimulateCacheUtils
    {
    
        private Map<String,Object> redis;
    
        public SimulateCacheUtils() {
            redis=new HashMap<>();
        }
        /***
         * 读取缓存
         * */
        public  Object getCache(String key){
            if (isCached(key)){
                Object bean=redis.get(key);
                System.out.println("【redis】读取缓存成功 key="+key+"\t value="+bean);
                return bean;
            }else {
                System.out.println("【redis】--key 不存在");
                return  null;
            }
        }
        /**
         * 写入缓存
         * */
        public boolean writeCache(String key,Object bean){
            if (!isCached(key)){
                redis.put(key,bean);
                System.out.println("【redis】写入缓存成功:key="+key+"\t value="+bean);
                return true;
            }else {
                System.out.println("【redis】--key 已存在");
                return  false;
            }
        }
        /**
         * 判断是否存在缓存
         * */
        public  boolean isCached(String key){
            return redis.containsKey(key);
        }
        /***
         * 修改缓存
         * */
        public boolean setCache(String key,Object bean){
            if (isCached(key)){
                redis.put(key,bean);
                return true;
            }else {
                System.out.println("key 不存在");
                return  false;
            }
        }
    }
    
    

    创建核心的AOP类:

    @Component
    @Aspect
    public class RedisCacheAspect {
    
    
        @Resource
        SimulateCacheUtils simulateCacheUtils;
    
        @Pointcut("@annotation(rpf.study.annotation.DefineAnnotation.AutoCache)")
        public  void setJoinPoint(){}
        @Around(value = "setJoinPoint()")
        public Object aroundMethod(ProceedingJoinPoint joinPoint) throws Throwable {
            System.out.println("【AOP】拦截到带@AutoCache注解的方法:"+joinPoint.getSignature().getName());
            String key=joinPoint.getTarget().getClass().toString().concat(".").concat(joinPoint.getSignature().getName());
            if (simulateCacheUtils.isCached(key)){
                System.out.println("【AOP】直接从缓存中读取数据");
                return simulateCacheUtils.getCache(key);
            }else {
                System.out.println("【AOP】缓存里面没有数据,运行方法:"+joinPoint.getSignature().getName());
                Object result=joinPoint.proceed(joinPoint.getArgs());
                simulateCacheUtils.writeCache(key,result);
                return  result;
            }
        }
    }
    

    由于这里只需要用到切点和环绕,就只写了两个方法setJoinPointaroundMethod,注意setJoinPoint的写法是:@annotation(rpf.study.annotation.DefineAnnotation.AutoCache)",指定类型是注解annotation和注解的全类名。Around注解中的方法指向切入点的方法名字setJoinPoint
    6.编写main入口:

    public class App {
        public static void main(String[] args) {
            BeanFactory beanFactory=new ClassPathXmlApplicationContext("application.xml");
            UserService userService= (UserService) beanFactory.getBean("userService");
            for (int i = 0; i < 5; i++) {
                String var1= userService.shit();
                System.out.println("【UserService】[shit()]方法运行结果:"+var1);
            }
    
            System.out.println("【APP】最终的UserService= "+userService.getClass().getName());
        }
    }
    

    这里执行了五次,查看打印结果:

    【AOP】拦截到带@AutoCache注解的方法:shit
    【AOP】缓存里面没有数据,运行方法:shit
    shit^shit^shit
    【redis】写入缓存成功:key=class rpf.study.annotation.service.UserService.shit    value=200
    【UserService】[shit()]方法运行结果:200
    【AOP】拦截到带@AutoCache注解的方法:shit
    【AOP】直接从缓存中读取数据
    【redis】读取缓存成功 key=class rpf.study.annotation.service.UserService.shit    value=200
    【UserService】[shit()]方法运行结果:200
    【AOP】拦截到带@AutoCache注解的方法:shit
    【AOP】直接从缓存中读取数据
    【redis】读取缓存成功 key=class rpf.study.annotation.service.UserService.shit    value=200
    【UserService】[shit()]方法运行结果:200
    【AOP】拦截到带@AutoCache注解的方法:shit
    【AOP】直接从缓存中读取数据
    【redis】读取缓存成功 key=class rpf.study.annotation.service.UserService.shit    value=200
    【UserService】[shit()]方法运行结果:200
    【AOP】拦截到带@AutoCache注解的方法:shit
    【AOP】直接从缓存中读取数据
    【redis】读取缓存成功 key=class rpf.study.annotation.service.UserService.shit    value=200
    【UserService】[shit()]方法运行结果:200
    【APP】最终的UserService= rpf.study.annotation.service.UserService$$EnhancerBySpringCGLIB$$3e764cdd
    

    可以看到除了第一次打印出了:shit^ shit^shit,后面都是直接取缓存的数据,方法中的打印代码并没有执行,而最后UserService不再是我们前面创建的那个,而是在在切面后生成的代理类,并且还是用的Cglib,这也就说明后面用Cglib无须加入jar包依赖。

    二、通过cglib实现自定义注解

    分析一波:在AOP中实际上是:结合RedisCacheAspect中的切点和环绕代码,使用cglib动态生成一个代理类替换了我们手写的UserService。如果不用AOP手动实现,则需要解决的问题就是:这么把生成的代理类替换到IOC中。

    Spring中确实提供了一个接口,让用户包装自己注入到IOC中的bean:BeanPostProcessor,他有一个前置方法,一个后置方法。官方api介绍:

    https://docs.spring.io/spring/docs/5.1.3.RELEASE/javadoc-api/org/springframework/beans/factory/config/BeanPostProcessor.html

    代码:

    @Component
    public class MyListenerProcessor implements BeanPostProcessor {
    
        @Resource
        SimulateCacheUtils simulateCacheUtils;
        public Object postProcessBeforeInitialization(Object bean, String s) throws BeansException {
            return bean;
        }
    /**
     * 若bean为代理后的对象(结尾含$符号),直接用反射获取不到注解,解决办法:
     * 1.使用ReflectionUtil获得的bean注解不会丢失。这里返回的直接是没代理前的bean的Method。
     * 2.使用AnnotationUtils.findAnnotation可以读取到已代被理类的注解,实际上是扫描代理类原来的class的注解。
     * **/
        public Object postProcessAfterInitialization(Object bean, String s) throws BeansException {
            Method [] methods= ReflectionUtils.getAllDeclaredMethods(bean.getClass());
            System.out.println("【BeanPostProcessor】:正在初始化:"+s);
            if (methods!=null){
                for (Method method : methods) {
                    if (method.isAnnotationPresent(Reflect.class)){
                        Reflect autoCache=method.getAnnotation(Reflect.class);
                        if (autoCache!=null){
                            return new MyCglibPoxy(simulateCacheUtils).getInstance(bean);
                        }
                    }
    
                }
            }
            return bean;
        }
    }
    

    此方法会将用户注入的bean,作为postProcessAfterInitialization/postProcessBeforeInitialization的bean参数传入进来,我只需要在将返回的bean替换成动态代理类就可以了。这里推荐使用spring的工具类:ReflectionUtil和AnnotationUtils。他们的作用如下:
    ReflectionUtil:直接获取bean原类中的Method集合。因为在动态代理后生成类会丢失注解,使用此方法后可以正常获取注解。
    AnnotationUtils:传入method和对应class,返回注解,也是在未代理前的class上取获取Method集合。
    2.自定义的注解和service方法:

    @Target({ElementType.METHOD})
    @Retention(RetentionPolicy.RUNTIME)
    public @interface Reflect {
        String value() default "";
    }
    

    userService新增的方法:

    @Service
    public class UserService {
    
        @AutoCache
        public String shit(){
            System.out.println("shit^shit^shit");
            return "200";
        }
        @Reflect
        public String eat(){
            System.out.println("chi^chi^chi");
            return "300";
        }
    
    }
    

    2.自定义的动态代理类

    @Component
    public class MyCglibPoxy implements MethodInterceptor {
        //@Autowired 注解不会起作用
        SimulateCacheUtils simulateCacheUtils;
    
        private  Object target;
    
        public MyCglibPoxy(SimulateCacheUtils simulateCacheUtils) {
            this.simulateCacheUtils = simulateCacheUtils;
        }
    
    
        public  Object getInstance(Object target){
            this.target=target;
            Enhancer enhancer=new Enhancer();
            System.out.println("【cglib】--【oldClassName】:"+target.getClass().getName());
            int proxyClassIndex=target.getClass().getName().indexOf("$");
            String realClassName=proxyClassIndex==-1? target.getClass().getName():target.getClass().getName().substring(0,proxyClassIndex);
            System.out.println("【cglib】--【realClassName】:"+realClassName);
            try {
                enhancer.setSuperclass(Class.forName(realClassName));
                enhancer.setCallback(this);
                return enhancer.create();
            } catch (ClassNotFoundException e) {
                e.printStackTrace();
            }
            return null;
        }
        @Override
         public Object intercept(Object bean, Method method, Object[] objects, MethodProxy methodProxy) throws Throwable {
            if (method.isAnnotationPresent(Reflect.class)){
                Class type = target.getClass();
                int proxyClassIndex=target.getClass().getName().indexOf("$");
                String realClassName=proxyClassIndex==-1? target.getClass().getName():target.getClass().getName().substring(0,proxyClassIndex);
                String key=realClassName.concat(".").concat(method.getName());
                System.out.println("【cglib】拦截到带@Reflect注解的方法:"+method.getName());
                if (method.isAnnotationPresent(Reflect.class)){
                    if (simulateCacheUtils.isCached(key)){
                        System.out.println("【cglib】直接从缓存中读取数据");
                        return simulateCacheUtils.getCache(key);
                    }else {
                        System.out.println("【cglib】缓存里面没有数据,运行方法:"+method.getName());
                        Object result=method.invoke(target,objects);
                        simulateCacheUtils.writeCache(key,result);
                        return  result;
                    }
                }
            }
    
    
    
            return method.invoke(target,objects);
        }
    }
    

    这里要注意动态代理类花括号{}里面的注解不会生效。而我们只需要去实现intercept方法即可。这里的bean是动态代理的类,而不是要被代理的类,被代理类一般要通过构造器传进来,method为被代理类中的所有方法中的一个,objects为方法的参数,MethodProxy为父类代理方法,这里用不到。
    注意:用代理类的class去生成代理会报错。所以这里用$符号做了截取操作,因为代理类一般是以这个结尾的。
    3.main入口方法:

    public class App {
        public static void main(String[] args) {
            BeanFactory beanFactory=new ClassPathXmlApplicationContext("application.xml");
            UserService userService= (UserService) beanFactory.getBean("userService");
            for (int i = 0; i < 5; i++) {
                String var1= userService.shit();
                System.out.println("【UserService】[shit()]方法运行结果:"+var1);
            }
            System.out.println("--------------------------------");
            for (int i = 0; i < 5; i++) {
            System.out.println("【UserService】[eat()]方法运行结果:"+userService.eat());
            }
    
            System.out.println("【APP】最终的UserService= "+userService.getClass().getName());
        }
    }
    

    4.运行结果:

    【BeanPostProcessor】:正在初始化:myCglibPoxy
    【BeanPostProcessor】:正在初始化:redisCacheAspect
    【BeanPostProcessor】:正在初始化:userService
    【BeanPostProcessor】当前class:rpf.study.annotation.service.UserService$$EnhancerBySpringCGLIB$$152d378d
    【cglib】--【oldClassName】:rpf.study.annotation.service.UserService$$EnhancerBySpringCGLIB$$152d378d
    【cglib】--【realClassName】:rpf.study.annotation.service.UserService
    【BeanPostProcessor】:正在初始化:org.springframework.context.event.internalEventListenerProcessor
    【BeanPostProcessor】:正在初始化:org.springframework.context.event.internalEventListenerFactory
    【AOP】拦截到带@AutoCache注解的方法:shit
    【AOP】缓存里面没有数据,运行方法:shit
    shit^shit^shit
    【redis】写入缓存成功:key=class rpf.study.annotation.service.UserService.shit    value=200
    【UserService】[shit()]方法运行结果:200
    【AOP】拦截到带@AutoCache注解的方法:shit
    【AOP】直接从缓存中读取数据
    【redis】读取缓存成功 key=class rpf.study.annotation.service.UserService.shit    value=200
    【UserService】[shit()]方法运行结果:200
    【AOP】拦截到带@AutoCache注解的方法:shit
    【AOP】直接从缓存中读取数据
    【redis】读取缓存成功 key=class rpf.study.annotation.service.UserService.shit    value=200
    【UserService】[shit()]方法运行结果:200
    【AOP】拦截到带@AutoCache注解的方法:shit
    【AOP】直接从缓存中读取数据
    【redis】读取缓存成功 key=class rpf.study.annotation.service.UserService.shit    value=200
    【UserService】[shit()]方法运行结果:200
    【AOP】拦截到带@AutoCache注解的方法:shit
    【AOP】直接从缓存中读取数据
    【redis】读取缓存成功 key=class rpf.study.annotation.service.UserService.shit    value=200
    【UserService】[shit()]方法运行结果:200
    --------------------------------
    【cglib】拦截到带@Reflect注解的方法:eat
    【cglib】缓存里面没有数据,运行方法:eat
    chi^chi^chi
    【redis】写入缓存成功:key=rpf.study.annotation.service.UserService.eat   value=300
    【UserService】[eat()]方法运行结果:300
    【cglib】拦截到带@Reflect注解的方法:eat
    【cglib】直接从缓存中读取数据
    【redis】读取缓存成功 key=rpf.study.annotation.service.UserService.eat   value=300
    【UserService】[eat()]方法运行结果:300
    【cglib】拦截到带@Reflect注解的方法:eat
    【cglib】直接从缓存中读取数据
    【redis】读取缓存成功 key=rpf.study.annotation.service.UserService.eat   value=300
    【UserService】[eat()]方法运行结果:300
    【cglib】拦截到带@Reflect注解的方法:eat
    【cglib】直接从缓存中读取数据
    【redis】读取缓存成功 key=rpf.study.annotation.service.UserService.eat   value=300
    【UserService】[eat()]方法运行结果:300
    【cglib】拦截到带@Reflect注解的方法:eat
    【cglib】直接从缓存中读取数据
    【redis】读取缓存成功 key=rpf.study.annotation.service.UserService.eat   value=300
    【UserService】[eat()]方法运行结果:300
    【APP】最终的UserService= rpf.study.annotation.service.UserService$$EnhancerByCGLIB$$9c2098aa
    

    可以看到整个的运行过程,
    1.首先是执行了BeanPostProcessor,这里面把用户注入的bean都放进去执行了一次。
    2.在执行UseService这个bean时扫描打它的一个Method带有注解@Reflect,此时打印出了,当前bean的class:

    【BeanPostProcessor】当前class:rpf.study.annotation.service.UserService$$EnhancerBySpringCGLIB$$152d378d
    

    然后进入代理类,通过$符号截取到真实的class为:rpf.study.annotation.service.UserService,同时自动生成了key:

    rpf.study.annotation.service.UserService.eat
    

    并在模拟的simulateCacheUtils中判断是否有这个key,没有就执行方法,并写入缓存:

    else {
                        System.out.println("【cglib】缓存里面没有数据,运行方法:"+method.getName());
                        Object result=method.invoke(target,objects);
                        simulateCacheUtils.writeCache(key,result);
                        return  result;
                    }
              
    

    对应控制台:

    【cglib】拦截到带@Reflect注解的方法:eat
    【cglib】缓存里面没有数据,运行方法:eat
    chi^chi^chi
    【redis】写入缓存成功:key=rpf.study.annotation.service.UserService.eat   value=300
    

    3.下一次执行后,由于缓存有这个key,则直接读取缓存,并返回出去:

    if (simulateCacheUtils.isCached(key)){
                        System.out.println("【cglib】直接从缓存中读取数据");
                        return simulateCacheUtils.getCache(key);
                    }
    

    对应控制台:

    【cglib】拦截到带@Reflect注解的方法:eat
    【cglib】直接从缓存中读取数据
    【redis】读取缓存成功 key=rpf.study.annotation.service.UserService.eat   value=300
    

    4.方法执行完成后,发现代理类被再一次代理:

    【APP】最终的UserService= rpf.study.annotation.service.UserService$$EnhancerByCGLIB$$9c2098aa
    

    这里和之前的不一样了:

    【cglib】--【oldClassName】:rpf.study.annotation.service.UserService$$EnhancerBySpringCGLIB$$152d378d
    

    总结:

    在用cglib注意,不能用已经是代理类的bean,作为enhancer.setSuperclass()的参数,这里原本是让写父类,没有父类写自己也是可以的;代理类{}花括号里面的注解不起作用,可以通过构造器将bean传进来;省事的话,还是AOP更加简单好用。

    代码下载地址:https://github.com/Siwash/siwash_annotation

    相关文章

      网友评论

          本文标题:Sping中自定义注解的两种方式【AOP、Cglib】

          本文链接:https://www.haomeiwen.com/subject/zzatpqtx.html