JetCache中使用Cache注解缓存到远程redis以及踩过的坑

目前网上关于jetcache的使用大多是基于官网的解释,给初学者造成很大的困扰,这里就将我使用的过程中遇到的坑总结一下。

项目是一个springboot项目,目前需要在一个接口方法上加@cache注解,希望将方法返回的结果连同自定义的key一起存到远程redis中。在实现的过程中遇到了如下的问题:

1. 希望将方法中传入的参数经过处理后做为缓存的key,但是不知道jetcache中spel表达式如何调用方法。

2. 运行过程报错:org.springframework.expression.spel.SpelEvaluationException: EL1072E: An exception occurred whilst evaluating a compiled expression

最后运行成功的项目代码

1. pom.xml

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>

    <groupId>com.example</groupId>
    <artifactId>demo</artifactId>
    <version>0.0.1-SNAPSHOT</version>
    <packaging>jar</packaging>

    <name>springboot-demo</name>
    <description>Demo project for Spring Boot</description>

    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>2.0.6.RELEASE</version>
        <relativePath/> <!-- lookup parent from repository -->
    </parent>
    <profiles>
        <profile>
            <id>dev</id>
            <activation>
                <activeByDefault>true</activeByDefault>
            </activation>
            <properties>
                <db.driver>com.mysql.jdbc.Driver</db.driver>
                <db.url>jdbc:mysql://localhost:3306/test?allowMultiQueries=true</db.url>
                <db.username>root</db.username>
                <db.password>root</db.password>
            </properties>
        </profile>
    </profiles>

    <properties>
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
        <project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
        <java.version>1.8</java.version>
        <spring-cloud.version>Finchley.SR1</spring-cloud.version>
    </properties>

    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-data-redis</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
        <dependency>
            <groupId>org.mybatis.spring.boot</groupId>
            <artifactId>mybatis-spring-boot-starter</artifactId>
            <version>1.3.2</version>
        </dependency>
        <dependency>
            <groupId>com.alibaba</groupId>
            <artifactId>druid-spring-boot-starter</artifactId>
            <version>1.1.10</version>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-devtools</artifactId>
            <optional>true</optional>
        </dependency>
        <dependency>
            <groupId>com.alicp.jetcache</groupId>
            <artifactId>jetcache-starter-redis</artifactId>
            <version>2.5.9</version>
        </dependency>
        <dependency>
            <groupId>org.apache.commons</groupId>
            <artifactId>commons-lang3</artifactId>
            <version>3.6</version>
        </dependency>
        <dependency>
            <groupId>org.apache.commons</groupId>
            <artifactId>commons-collections4</artifactId>
            <version>4.1</version>
        </dependency>
        <dependency>
            <groupId>mysql</groupId>
            <artifactId>mysql-connector-java</artifactId>
            <scope>runtime</scope>
        </dependency>
        <dependency>
            <groupId>org.apache.commons</groupId>
            <artifactId>commons-lang3</artifactId>
            <version>3.6</version>
        </dependency>
        <dependency>
            <groupId>commons-codec</groupId>
            <artifactId>commons-codec</artifactId>
            <version>1.10</version>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
        </dependency>
    </dependencies>

    <build>
        <plugins>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
            </plugin>            
            <plugin>
                <artifactId>maven-resources-plugin</artifactId>
                <version>3.0.2</version>
                <configuration>
                    <delimiters>
                        <delimiter>$</delimiter>
                    </delimiters>
                    <useDefaultDelimiters>false</useDefaultDelimiters>
                </configuration>
            </plugin>
            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-compiler-plugin</artifactId>
                <configuration>
                    <source>1.8</source>
                    <target>1.8</target>
                    <encoding>${project.build.sourceEncoding}</encoding>
                    <compilerArgs>
                    <!-- 编译加上参数 -parameters-->
                        <arg>-parameters</arg>
                    </compilerArgs>
                </configuration>
            </plugin>
        </plugins>
    </build>
</project>

2. application.yml

spring:
  datasource:
    druid:
      url: $db.url$
      username: $db.username$
      password: $db.password$
      driver-class-name: $db.driver$
# jetcache使用
jetcache: 
  statIntervalMinutes: 60
  areaInCacheName: false
  local:
    default:
      type: linkedhashmap
      keyConvertor: fastjson
  remote:
    default:
      type: redis
      keyConvertor: fastjson
      valueEncoder: java
      valueDecoder: java
      timeout: 5000
      poolConfig:
        minIdle: 5
        maxIdle: 10
        maxTotal: 20
        testOnBorrow: false
        testOnReturn: true
      host: 47.93.**.**
      port: 6378
mybatis:
  mapper-locations:
  - classpath:conf/mapper/*.xml
  config-location: classpath:conf/mybatis/mybatis.xml

2. application启动类

@SpringBootApplication
@EnableCreateCacheAnnotation
@MapperScan("com.example.demo.dao")
@EnableMethodCache(basePackages = "com.example.demo")
@EnableAspectJAutoProxy(exposeProxy = true, proxyTargetClass = true)
public class SpringbootDemoApplication {

    public static void main(String[] args) {
        SpringApplication.run(SpringbootDemoApplication.class, args);
    }
}

@EnableCreateCacheAnnotation注解用于开启jetcache中@CreateCache注解,
@EnableMethodCache(basePackages = “com.example.demo”)注解用于开启@cache注解

3. 接口及实现类

public interface IUserService {

    String addRecord(String mobile);
    
    String addIntRecord(String mobile);
    
    String addUser(User user);
    
    String processMobile(User user);
    
}
@Service
public class UserServiceImpl implements IUserService{

    @Override
    public String addRecord(String mobile) {
        
        User user = new User();
        user.setAge("22");
        user.setCity("上海");
        user.setMobile(mobile);
        
        IUserService userService = (IUserService) AopContext.currentProxy();
        userService.processMobile(user);
        
        return "test liu";
    }

    @Override
    @Cached(name="ls:dlc:int",key="targetObject.processMobile(#mobile)",cacheType=CacheType.REMOTE)
    public String addIntRecord(String mobile) {
        return "processMobile";
    }

    @Cached(name="ls:dlc:scoring:",key="#user.name",cacheType=CacheType.REMOTE)
    @Override
    public String addUser(User user) {

        return user.getAge();
    }
    
    @Cached(name="ls:dlc:scoring:",key="#user.name",cacheType=CacheType.REMOTE)
    @Override
    public String processMobile(User user) {
        
        System.out.println("调用processMobile方法。。。");
        
        return "user:"+user.getMobile();
    }
    
    public String processMobile(String mobile){
        
        System.out.println(mobile);
        
        return "processMobile"+mobile;
    }

}

这里注意两个processMobile方法的不同,一个是string字符作为参数,一个是对象作为参数;一个是实现接口方法,一个是实现类中自己的方法。

4. controller测试代码

@RestController
public class JetcacheController {

    @Autowired
    private IUserService userService;
    
    @RequestMapping("jet_spel_test")
    public String JetSpelTest(){
        
        String addIntRecord = userService.addIntRecord("22222");
        
        return addIntRecord;
    }
    
    @RequestMapping("jet_mobile_test")
    public String JetMobileTest(String mobile){
        
        String addRecord = userService.addRecord("123456789");
        
        return addRecord;
    }
    
    @RequestMapping("jet_object_test")
    public String JetObjectTest(){
        
        User user = new User();
        user.setAge("23");
        user.setCity("南京");
        user.setName("zhaosi");
        
        String result = userService.addUser(user);
        
        System.out.println("result:"+result);
        
        return result;
    }
    
}

遇到的问题

1. spEL表达式的使用

最初的需求是当调用userservice.addRecord(string mobile),希望对mobile进行特殊处理,比如md5加密后作为缓存key,所以在实现类中还定义了一个processMobile方法,用于处理手机号。不要说什么可以加密之后再传入方法,这里只是举例说明如果有这种需要。

但是试了很多,spEL的表达式都找不到当前类中的processMobile方法,在spring cache中这种情况是使用:

SpEL表达式可基于上下文并通过使用缓存抽象,提供与root独享相关联的缓存特定的内置参数。

名称位置描述示例
methodNameroot对象当前被调用的方法名#root.methodname
methodroot对象当前被调用的方法#root.method.name
targetroot对象当前被调用的目标对象实例#root.target
targetClassroot对象当前被调用的目标对象的类#root.targetClass
argsroot对象当前被调用的方法的参数列表#root.args[0]
cachesroot对象当前方法调用使用的缓存列表#root.caches[0].name
Argument Name执行上下文当前被调用的方法的参数,如findArtisan(Artisan artisan),可以通过#artsian.id获得参数#artsian.id
result执行上下文方法执行后的返回值(仅当方法执行后的判断有效,如 unless cacheEvict的beforeInvocation=false)#result

但是这种方式在jetcache中并不好用,最后还是在github的issues中找到了解决方案:

在这里插入图片描述

正如上面接口实现类中的addIntRecord所示,使用targetObject获取当前类中的方法。这里需要注意的是调用的processMobile方法需要是public修饰,需要传参使用#+参数的方式传递。

2. spEL表达式不可用时候使用的替代方法

这一步是在没有找到spEL表达式的时候,所想到的替代方法,因为官网给的教程是通过对象获取参数,或者直接传递参数。

这里如addRecord方法所示,addRecord方法我并没有加@cache注解,而是使用user对象存放mobile属性,通过调用@cache注解修饰的processMobile(User user)方法去实现将结果加入远程缓存中,即将需要进行的处理逻辑和最终的返回结果放入processMobile(User user)方法中,这时候通过#user.mobile就可以获取到手机号了。

这里遇到的问题是通过一个方法调用当前类中的另一个方法的时候,不能保持事务的一致性,即所调用的方法不是spring所管理的,最终也会报错。

解决方式有两种:

1) 通过方法类调用自己方法类的方式实现

即在方法上通过@autowired或者@resource注解引入userservice对象,在此中使用此对象调用service中的方法。

2) 通过代理获取spring中的对象

IUserService userService = (IUserService) AopContext.currentProxy();
userService.processMobile(user);

使用aopContext获取当前代理对象,需要在启动类上加上如下注解,即需要手动暴露代理

@EnableAspectJAutoProxy(exposeProxy = true, proxyTargetClass = true)

3. devtools与jetcache冲突 EL1072E: An exception occurred whilst evaluating a compiled expression

在使用的过程中,最初存到远程redis缓存成功,但是从缓存中取的时候就会报错的问题。错误信息大致如下:

org.springframework.expression.spel.SpelEvaluationException: EL1072E: An exception occurred whilst evaluating a compiled expression

Caused by: java.lang.ClassCastException: com.example.entity.User cannot be cast to com.example.entity.User
at spel.Ex3.getValue(Unknown Source)
at org.springframework.expression.spel.standard.SpelExpression.getValue(SpelExpression.java:254)
... 42 common frames omitted

最开始看到这个错误是蒙蔽的,怎么user不能转成user呢,网上还是查不到任何有用的信息,最终还是在github的issues中找到了解释:

在这里插入图片描述

测试结果的时候,我将pom文件中关于devtools的依赖取消了,果然运行没有报错,至于-parameters参数的设置,我也修改了,但是并没有解决实质性的问题。

这里还是附一下-parameter参数的设置方式:

  1. 最简单的方式,pom文件中修改
<plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-compiler-plugin</artifactId>
                <configuration>
                    <source>1.8</source>
                    <target>1.8</target>
                    <encoding>${project.build.sourceEncoding}</encoding>
                    <compilerArgs>
                    <!-- 编译加上参数 -parameters-->
                        <arg>-parameters</arg>
                    </compilerArgs>
                </configuration>
            </plugin>
  1. eclipse中默认设置是关闭的,需要手动开启
    在这里插入图片描述

总结

jetcache的文档还是太少,网上有的教程一般都是网上能够找到的,但是一些特别的坑还是要取issues中才能找到解决方法,这也算是给自己增加了使用开源框架的经验。


版权声明:本文为nb7474原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接和本声明。