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

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项目:


Sping中自定义注解的两种方式【AOP、Cglib】_第1张图片
在这里插入图片描述

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


    
        org.springframework
        spring-aop
        4.3.18.RELEASE
    
    
    
        org.aspectj
        aspectjweaver
        1.8.13
    

3.创建配置文件




    
    
    
    

关键的只需要包扫描和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 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】)