spring+redis方法内部调用同类方法缓存无效

1.问题描述

最近写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的缓存是无效的

2.为什么?

在springboot中,当你调用方法A(非本类中调用)spring会通过RedisTest的代理对象调用A,如果在A中调用了B,调用B的对象不再是代理对象,而是RedisTest的一个实例,因为本质上是通过this调用方法B。

spring+redis方法内部调用同类方法缓存无效_第1张图片

3.解决

方法1

将方法B移到另外一个类OtherClass,在方法A中通过new OtherClass().B()调用B方法,此时B的缓存会生效

不建议,麻烦,冗余

方法2

通过动态代理得到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的缓存仍失效。

其实只要理解代理模式以及静态代理,动态代理,你会发现很简单

4.JDK动态代理模拟

如果不太明白的话,利用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的核心。

你可能感兴趣的:(springboot,springboot,redis)