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;
}
}
}
由于这里只需要用到切点和环绕,就只写了两个方法setJoinPoint
和aroundMethod
,注意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介绍:
代码:
@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更加简单好用。