最近写SpringBoot+redis时,发现当某方法A调用同类方法B,与此同时B方法存在缓存操作,当你调用方法A时你会发现方法B缓存无效。
import org.springframework.cache.annotation.CacheConfig;
import org.springframework.cache.annotation.Cacheable;
@CacheConfig(cacheNames="test")
public class RedisTest {
public void A() {
B();
}
@Cacheable
public Object B() {
return new Object();
}
}
此时当你调用方法A,方法B的缓存是无效的
在springboot中,当你调用方法A(非本类中调用)spring会通过RedisTest的代理对象调用A,如果在A中调用了B,调用B的对象不再是代理对象,而是RedisTest的一个实例,因为本质上是通过this调用方法B。
将方法B移到另外一个类OtherClass,在方法A中通过new OtherClass().B()调用B方法,此时B的缓存会生效
不建议,麻烦,冗余
通过动态代理得到RedisTest代理对象,利用代理对象调用B
建议
2.1引入aop
org.springframework.boot
spring-boot-starter-aop
2.2配置动态代理
在springboot的启动类上加下列注解
@EnableAspectJAutoProxy(proxyTargetClass = true, exposeProxy = true)
在JDK动态代理中,目标类需要实现某一个接口,但是我们的RedisTest没有实现接口怎么办?这时我们就需要使用Cglib代理,Cglib代理可以对任何类生成代理,代理的原理是可以对目标对象接口实现代理,也可以进行继承代理。
设置proxyTargetClass为true,实际上是开启Cglib代理。
2.3获取代理对象
import org.springframework.aop.framework.AopContext;
import org.springframework.cache.annotation.CacheConfig;
import org.springframework.cache.annotation.Cacheable;
@CacheConfig(cacheNames="test")
public class RedisTest {
public void A() {
((RedisTest)AopContext.currentProxy()).B();
}
@Cacheable
public Object B() {
return new Object();
}
}
此时方法B的缓存生效
springboot中利用cglib代理获取代理对象时,代理对象调用的方法B权限必须非私有(可继承),因为cglib代理利用继承来实现代理,如果代理类无法继承你的方法,拿本例而言,结果就是B缓存无效。
import org.springframework.aop.framework.AopContext;
import org.springframework.cache.annotation.CacheConfig;
import org.springframework.cache.annotation.Cacheable;
@CacheConfig(cacheNames="test")
public class RedisTest {
public void A() {
((RedisTest)AopContext.currentProxy()).B();
}
@Cacheable
private Object B() {
return new Object();
}
}
B方法权限变为private私有,假设此时某个类调用A,A再调用B,B的缓存仍失效。
其实只要理解代理模式以及静态代理,动态代理,你会发现很简单
如果不太明白的话,利用JDK代理模拟一下这个问题。
jdb动态代理是通过实现接口来实现,因此我们首先要有一个Target接口;
public interface Target {
public void run();
public void eat();
public void sleep();
}
同时需要一个接口的实现类TargetImpl
public class TargetImpl implements Target {
@Override
public void run() {
// TODO Auto-generated method stub
System.out.println("run");
}
@Override
public void eat() {
// TODO Auto-generated method stub
System.out.println("eat");
}
@Override
public void sleep() {
// TODO Auto-generated method stub
System.out.println("sleep");
}
}
接口有了,目标实现类有了,现在我们唯一缺的就是代理类ProxyFactory,在这里我们看到JDK代理是需要代理类实现某一个接口。
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;
public class ProxyFactory {
public static Object getProxy(T t){
Object object = Proxy.newProxyInstance(
t.getClass().getClassLoader(),
// 得到参数t的接口对象
t.getClass().getInterfaces(),
new InvocationHandler() {
@Override
// proxy为目标类,menthod为调用的方法,args为参数
public Object invoke(Object proxy, Method method, Object[] args) throws IllegalAccessException, IllegalArgumentException, InvocationTargetException {
System.out.println("执行前");
Object object = method.invoke(t, args);
System.out.println("执行后");
return object;
}
});
return object;
}
}
TargetImpl是我们需要去调用的类,例如我们要调用该类中的eat,run,sleep方法,因为jdk代理是基于接口实现因此我们需要一个接口Target,此时我们需要代理类ProxyFactory去获取代理对象,通过代理对象去调用TargetImpl中的eat,run,sleep方法,而不再是我们利用TargetImpl的对象调用这些方法。
直接看结果
public class Me {
public static void main(String[] args) {
Target target = (Target) ProxyFactory.getProxy(new TargetImpl());
target.run();
}
}
>>>>>>>结果<<<<<<<<
执行前
run
执行后
这时回到我们最初的问题,在A中调用B不会触发缓存,那我们在run中调用eat会触发输出语句吗?
那如果此时我在run方法中调用eat方法会怎么样!!!!
@Override
public void run() {
// TODO Auto-generated method stub
System.out.println("run");
eat();
}
@Override
public void eat() {
// TODO Auto-generated method stub
System.out.println("eat");
}
public class Me {
public static void main(String[] args) {
Target target = (Target) ProxyFactory.getProxy(new TargetImpl());
target.run();
}
}
>>>>>>>结果<<<<<<<<
执行前
run
eat
执行后
在TargetImpl的run中对eat的调用是通过this,即TargetImpl的一个实例,不再是代理对象。
在run中我们利用代理对象调用eat方法?
@Override
public void run() {
// TODO Auto-generated method stub
System.out.println("run");
((Target) ProxyFactory.getProxy(new TargetImpl())).eat();
}
@Override
public void eat() {
// TODO Auto-generated method stub
System.out.println("eat");
}
public class Me {
public static void main(String[] args) {
Target target = (Target) ProxyFactory.getProxy(new TargetImpl());
target.run();
}
}
>>>>>>结果<<<<<<<
执行前
run
执行前
eat
执行后
执行后
缓存就好像是该例中的执行前执行后等操作,在使用非注解的方式去使用缓存的话,我们需要利用redis的数据库操作对象去决定什么时候加入缓存,在哪加入缓存等等,代理模式帮助我们将这些缓存操作正确的插入至方法的前后。
回到最初的问题,在spring中在某个方法体中直接调用同类的方法,如果该方法存在缓存,事务等操作时,都是无效的,此时需要人工的利用代理对象调用,因为缓存或者事务都是通过代理模式切入到方法执行的前后,面向切面编程也是spring的核心。