目前网上关于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独享相关联的缓存特定的内置参数。
名称 | 位置 | 描述 | 示例 |
---|---|---|---|
methodName | root对象 | 当前被调用的方法名 | #root.methodname |
method | root对象 | 当前被调用的方法 | #root.method.name |
target | root对象 | 当前被调用的目标对象实例 | #root.target |
targetClass | root对象 | 当前被调用的目标对象的类 | #root.targetClass |
args | root对象 | 当前被调用的方法的参数列表 | #root.args[0] |
caches | root对象 | 当前方法调用使用的缓存列表 | #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参数的设置方式:
- 最简单的方式,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>
- eclipse中默认设置是关闭的,需要手动开启
总结
jetcache的文档还是太少,网上有的教程一般都是网上能够找到的,但是一些特别的坑还是要取issues中才能找到解决方法,这也算是给自己增加了使用开源框架的经验。