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


在这里插入图片描述

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

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 159,458评论 4 363
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,454评论 1 294
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 109,171评论 0 243
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 44,062评论 0 207
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,440评论 3 287
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,661评论 1 219
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,906评论 2 313
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,609评论 0 200
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,379评论 1 246
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,600评论 2 246
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,085评论 1 261
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,409评论 2 254
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,072评论 3 237
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,088评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,860评论 0 195
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,704评论 2 276
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,608评论 2 270

推荐阅读更多精彩内容