JPA(六)-JPA中其他功能特性
参考文献
- 拉钩教育–Spring Data JPA原理与实战
JPA中的乐观锁
-
乐观锁在实际开发过程中很常用,它没有加锁、没有阻塞,在多线程环境以及高并发的情况下 CPU 的利用率是最高的,吞吐量也是最大的。
-
而 Java Persistence API 协议也对乐观锁的操作做了规定:通过指定 @Version 字段对数据增加版本号控制,进而在更新的时候判断版本号是否有变化。如果没有变化就直接更新;如果有变化,就会更新失败并抛出“OptimisticLockException”异常。我们用 SQL 表示一下乐观锁的做法,代码如下:
1
2select uid,name,version from user where id=1;
update user set name='jack', version=version+1 where id=1 and version=1
乐观锁的实现方法
- JPA 协议规定,想要实现乐观锁可以通过 @Version 注解标注在某个字段上面,并且可以持久化到 DB 即可。其支持的类型有如下四种:
int
orInteger
short
orShort
long
orLong
java.sql.Timestamp
- 注意:Spring Data JPA 里面有两个 @Version 注解,请使用
@javax.persistence.Version
,而不是@org.springframework.data.annotation.Version
。
- 注意:乐观锁异常不仅仅是同一个方法多线程才会出现的问题,我们只是为了方便测试而采用同一个方法;不同的方法、不同的项目,都有可能导致乐观锁异常。乐观锁的本质是 SQL 层面发生的,和使用的框架、技术没有关系。
Spring 支持的重试机制
-
Spring 全家桶里面提供了@Retryable 的注解,会帮我们进行重试。下面看一个 @Retryable 的例子。
1
org.springframework.retry:spring-retry
-
第二步:在 UserInfoserviceImpl 的方法中添加 @Retryable 注解,就可以实现重试的机制了
-
第三步:新增一个RetryConfiguration并添加@EnableRetry 注解,是为了开启重试机制,使 @Retryable 生效。
1
2
3
4@EnableRetry
@Configuration
public class RetryConfiguration {
}-
maxAttempts:最大重试次数,默认为 3,如果要设置的重试次数为 3,可以不写;
-
value:抛出指定异常才会重试;
-
include:和 value 一样,默认为空,当 exclude 也为空时,默认异常;
-
exclude:指定不处理的异常;
-
backoff:重试等待策略,默认使用 @Backoff@Backoff 的 value,默认为 1s
- value=delay:隔多少毫秒后重试,默认为 1000L,单位是毫秒;
- multiplier(指定延迟倍数)默认为 0,表示固定暂停 1 秒后进行重试,如果把 multiplier 设置为 1.5,则第一次重试为 2 秒,第二次为 3 秒,第三次为 4.5 秒。
1
2
3
4
5
6
7
public interface MyService {
void retryServiceWithExternalizedConfiguration(String sql) throws SQLException;
}
// @Retryable(value = ObjectOptimisticLockingFailureException.class,backoff = @Backoff(multiplier = 1.5,random = true))1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24public enum LockModeType
{
//等同于OPTIMISTIC,默认,用来兼容2.0之前的协议
READ,
//等同于OPTIMISTIC_FORCE_INCREMENT,用来兼容2.0之前的协议
WRITE,
//乐观锁,默认,2.0协议新增
OPTIMISTIC,
//乐观写锁,强制version加1,2.0协议新增
OPTIMISTIC_FORCE_INCREMENT,
//悲观读锁 2.0协议新增
PESSIMISTIC_READ,
//悲观写锁,version不变,2.0协议新增
PESSIMISTIC_WRITE,
//悲观写锁,version会新增,2.0协议新增
PESSIMISTIC_FORCE_INCREMENT,
//2.0协议新增无锁状态
NONE
}
public interface UserInfoRepository extends JpaRepository<UserInfo, Long> {
Optional<UserInfo> findById(Long userId);
} -
JPA 对 Web MVC
- 支持在 Controller 层直接返回实体,而不使用其显式的调用方法;
- 对 MVC 层支持标准的分页和排序功能;
- 扩展的插件支持 Querydsl,可以实现一些通用的查询逻辑。
开启支持
1 |
|
组件
-
DomainClassConverter 组件
- 这个组件的主要作用是帮我们把 Path 中 ID 的变量,或 Request 参数中的变量 ID 的参数值,直接转化成实体对象注册到 Controller 方法的参数里面
-
@DynamicUpdate & @DynamicInsert 详解
-
@DynamicInsert:这个注解表示 insert 的时候,会动态生产 insert SQL 语句,其生成 SQL 的规则是:只有非空的字段才能生成 SQL。
- 这个注解主要是用在 @Entity 的实体中,如果加上这个注解,就表示生成的 insert SQL 的 Columns 只包含非空的字段;如果实体中不加这个注解,默认的情况是空的,字段也会作为 insert 语句里面的 Columns。
1
2
3
4
5
6
public DynamicInsert {
//默认是true,如果设置成false,就表示空的字段也会生成sql语句;
boolean value() default true;
} -
@DynamicUpdate:和 insert 是一个意思,只不过这个注解指的是在 update 的时候,会动态产生 update SQL 语句,生成 SQL 的规则是:只有非空的字段才会生成到 update SQL 的 Columns 里面.
- 和上一个注解的原理类似,这个注解也是用在 @Entity 的实体中,如果加上这个注解,就表示生成的 update SQL 的 Columns 只包含非空的字段;如果不加这个注解,默认的情况是空的字段也会作为 update 语句里面的 Columns。
1
2
3
4
5
6
public DynamicUpdate {
//和insert里面一个意思,默认true;
boolean value() default true;
}
-
-
HandlerMethodArgumentResolvers 详解
-
HandlerMethodArgumentResolvers 在 Spring MVC 中的主要作用是对 Controller 里面的方法参数做解析,即可以把 Request 里面的值映射到方法的参数中。
1
2
3
4
5
6
7public interface HandlerMethodArgumentResolver {
//检查方法的参数是否支持处理和转化
boolean supportsParameter(MethodParameter parameter);
//根据reqest上下文,解析方法的参数
Object resolveArgument(MethodParameter parameter, ModelAndViewContainer mavContainer,
NativeWebRequest webRequest, WebDataBinderFactory binderFactory)throws Exception;
} -
使用步骤
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97第一步:新建 MyPageableHandlerMethodArgumentResolver。
这个类的作用有两个:
用来兼容 ?page[size]=2&page[number]=0 的参数情况;
支持 JPA 新的参数形式 ?size=2&page=0。
/**
* 通过@Component把此类加载到Spring的容器里面去
*/
public class MyPageableHandlerMethodArgumentResolver extends PageableHandlerMethodArgumentResolver implements HandlerMethodArgumentResolver {
//我们假设sort的参数没有发生变化,采用PageableHandlerMethodArgumentResolver里面的写法
private static final SortHandlerMethodArgumentResolver DEFAULT_SORT_RESOLVER = new SortHandlerMethodArgumentResolver();
//给定两个默认值
private static final Integer DEFAULT_PAGE = 0;
private static final Integer DEFAULT_SIZE = 10;
//兼容新版,引入JPA的分页参数
private static final String JPA_PAGE_PARAMETER = "page";
private static final String JPA_SIZE_PARAMETER = "size";
//兼容原来老的分页参数
private static final String DEFAULT_PAGE_PARAMETER = "page[number]";
private static final String DEFAULT_SIZE_PARAMETER = "page[size]";
private SortArgumentResolver sortResolver;
//模仿PageableHandlerMethodArgumentResolver里面的构造方法
public MyPageableHandlerMethodArgumentResolver( { SortArgumentResolver sortResolver)
this.sortResolver = sortResolver == null ? DEFAULT_SORT_RESOLVER : sortResolver;
}
public boolean supportsParameter(MethodParameter parameter) {
// 假设用我们自己的类MyPageRequest接收参数
return MyPageRequest.class.equals(parameter.getParameterType());
//同时我们也可以支持通过Spring Data JPA里面的Pageable参数进行接收,两种效果是一样的
// return Pageable.class.equals(parameter.getParameterType());
}
/**
* 参数封装逻辑page和sort,JPA参数的优先级高于page[number]和page[size]参数
*/
//public Pageable resolveArgument(MethodParameter parameter, ModelAndViewContainer mavContainer, NativeWebRequest webRequest, WebDataBinderFactory binderFactory) { //这种是Pageable的方式
public MyPageRequest resolveArgument(MethodParameter parameter, ModelAndViewContainer mavContainer, NativeWebRequest webRequest, WebDataBinderFactory binderFactory) {
String jpaPageString = webRequest.getParameter(JPA_PAGE_PARAMETER);
String jpaSizeString = webRequest.getParameter(JPA_SIZE_PARAMETER);
//我们分别取参数里面page、sort和 page[number]、page[size]的值
String pageString = webRequest.getParameter(DEFAULT_PAGE_PARAMETER);
String sizeString = webRequest.getParameter(DEFAULT_SIZE_PARAMETER);
//当两个都有值时候的优先级,及其默认值的逻辑
Integer page = jpaPageString != null ? Integer.valueOf(jpaPageString) : pageString != null ? Integer.valueOf(pageString) : DEFAULT_PAGE;
//在这里同时可以计算 page+1的逻辑;如:page=page+1;
Integer size = jpaSizeString != null ? Integer.valueOf(jpaSizeString) : sizeString != null ? Integer.valueOf(sizeString) : DEFAULT_SIZE;
//我们假设,sort排序的取值方法先不发生改变
Sort sort = sortResolver.resolveArgument(parameter, mavContainer, webRequest, binderFactory);
// 如果使用Pageable参数接收值,我们也可以不用自定义MyPageRequest对象,直接返回PageRequest;
// return PageRequest.of(page,size,sort);
//将page和size计算出来的记过封装到我们自定义的MyPageRequest类里面去
MyPageRequest myPageRequest = new MyPageRequest(page, size,sort);
//返回controller里面的参数需要的对象;
return myPageRequest;
}
}
/**
* 继承父类,可以省掉很多计算page和index的逻辑
*/
public class MyPageRequest extends PageRequest {
protected MyPageRequest(int page, int size, Sort sort) {
super(page, size, sort);
}
}
第三步:implements WebMvcConfigurer 加载 myPageableHandlerMethodArgumentResolver。
/**
* 实现WebMvcConfigurer
*/
public class MyWebMvcConfigurer implements WebMvcConfigurer {
private MyPageableHandlerMethodArgumentResolver myPageableHandlerMethodArgumentResolver;
/**
* 覆盖这个方法,把我们自定义的myPageableHandlerMethodArgumentResolver加载到原始的mvc的resolvers里面去
* @param resolvers
*/
public void addArgumentResolvers(List<HandlerMethodArgumentResolver> resolvers) {
resolvers.add(myPageableHandlerMethodArgumentResolver);
}
}
第四步:我们看下 Controller 里面的写法。
//用Pageable这种方式也是可以的
public Page<UserInfo> queryByPage(Pageable pageable, UserInfo userInfo) {
return userInfoRepository.findAll(Example.of(userInfo),pageable);
}
//用MyPageRequest进行接收
public Page<UserInfo> queryByMyPage(MyPageRequest pageable, UserInfo userInfo) {
return userInfoRepository.findAll(Example.of(userInfo),pageable);
} -
实操
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61在实际的工作中,还经常会遇到“取当前用户”的应用场景。此时,普通做法是,当使用到当前用户的 UserInfo 时,每次都需要根据请求 header 的 token 取到用户信息,伪代码如下所示:
public UserInfo getUserInfo( { String token)
// 伪代码
Long userId = redisTemplate.get(token);
UserInfo useInfo = userInfoRepository.getById(userId);
return userInfo;
}
如果我们使用HandlerMethodArgumentResolver接口来实现,代码就会变得优雅许多。伪代码如下:
// 1. 实现HandlerMethodArgumentResolver接口
public class UserInfoArgumentResolver implements HandlerMethodArgumentResolver {
private final RedisTemplate redisTemplate;//伪代码,假设我们token是放在redis里面的
private final UserInfoRepository userInfoRepository;
public UserInfoArgumentResolver(RedisTemplate redisTemplate, UserInfoRepository userInfoRepository) {
this.redisTemplate = redisTemplate;//伪代码,假设我们token是放在redis里面的
this.userInfoRepository = userInfoRepository;
}
public boolean supportsParameter(MethodParameter parameter) {
return UserInfo.class.isAssignableFrom(parameter.getParameterType());
}
public Object resolveArgument(MethodParameter parameter, ModelAndViewContainer mavContainer,
NativeWebRequest webRequest, WebDataBinderFactory binderFactory) throws Exception {
HttpServletRequest nativeRequest = (HttpServletRequest) webRequest.getNativeRequest();
String token = nativeRequest.getHeader("token");
Long userId = (Long) redisTemplate.opsForValue().get(token);//伪代码,假设我们token是放在redis里面的
UserInfo useInfo = userInfoRepository.getOne(userId);
return useInfo;
}
}
//2. 我们只需要在MyWebMvcConfigurer里面把userInfoArgumentResolver添加进去即可,关键代码如下:
public class MyWebMvcConfigurer implements WebMvcConfigurer {
private MyPageableHandlerMethodArgumentResolver myPageableHandlerMethodArgumentResolver;
private UserInfoArgumentResolver userInfoArgumentResolver;
public void addArgumentResolvers(List<HandlerMethodArgumentResolver> resolvers) {
resolvers.add(myPageableHandlerMethodArgumentResolver);
//我们只需要把userInfoArgumentResolver加入resolvers中即可
resolvers.add(userInfoArgumentResolver);
}
}
// 3. 在Controller中使用
public class UserInfoController {
//获得当前用户的信息
public UserInfo getUserInfo(UserInfo userInfo) {
return userInfo;
}
//给当前用户 say hello
public String sayHello(UserInfo userInfo) {
return "hello " + userInfo.getTelephone();
}
}
-
-
WebMvcConfigurer 介绍
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20/* 拦截器配置 */
void addInterceptors(InterceptorRegistry var1);
/* 视图跳转控制器 */
void addViewControllers(ViewControllerRegistry registry);
/**
*静态资源处理
**/
void addResourceHandlers(ResourceHandlerRegistry registry);
/* 默认静态资源处理器 */
void configureDefaultServletHandling(DefaultServletHandlerConfigurer configurer);
/**
*这里配置视图解析器
**/
void configureViewResolvers(ViewResolverRegistry registry);
/* 配置内容裁决的一些选项*/
void configureContentNegotiation(ContentNegotiationConfigurer configurer);
/** 解决跨域问题 **/
void addCorsMappings(CorsRegistry registry) ;
/** 添加都会contoller的Return的结果的处理 **/
void addReturnValueHandlers(List<HandlerMethodReturnValueHandler> handlers);-
用 Result 对 JSON 的返回结果进行统一封装
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55第一步:我们自定义一个注解 ,表示此注解包装的返回结果用 Data 进行包装,代码如下:
/**
* 自定义一个注解对返回结果进行包装
*/
public WarpWithData {
}
第二步:自定义 MyWarpWithDataHandlerMethodReturnValueHandler,并继承 RequestResponseBodyMethodProcessor 来实现 HandlerMethodReturnValueHandler 接口,用来处理 Data 包装的结果,代码如下:
//自定义自己的return的处理类,我们直接继承RequestResponseBodyMethodProcessor,这样父类里面的方法我们直接使用就可以了
public class MyWarpWithDataHandlerMethodReturnValueHandler extends RequestResponseBodyMethodProcessor implements HandlerMethodReturnValueHandler {
//参考父类RequestResponseBodyMethodProcessor的做法
public MyWarpWithDataHandlerMethodReturnValueHandler(List<HttpMessageConverter<?>> converters) {
super(converters);
}
//只处理需要包装的注解的方法
public boolean supportsReturnType(MethodParameter returnType) {
return returnType.hasMethodAnnotation(WarpWithData.class);
}
//将返回结果包装一层Data
public void handleReturnValue(Object returnValue, MethodParameter methodParameter, ModelAndViewContainer modelAndViewContainer, NativeWebRequest nativeWebRequest) throws IOException, HttpMediaTypeNotAcceptableException {
Map<String,Object> res = new HashMap<>();
res.put("data",returnValue);
super.handleReturnValue(res,methodParameter,modelAndViewContainer,nativeWebRequest);
}
}
第三步:在 MyWebMvcConfigurer 里面直接把 myWarpWithDataHandlerMethodReturnValueHandler 加入 handlers 里面即可,也是通过覆盖父类 WebMvcConfigurer 里面的 addReturnValueHandlers 方法完成的,关键代码如下:
public class MyWebMvcConfigurer implements WebMvcConfigurer {
private MyWarpWithDataHandlerMethodReturnValueHandler myWarpWithDataHandlerMethodReturnValueHandler;
//把我们自定义的myWarpWithDataHandlerMethodReturnValueHandler加入handlers里面即可
public void addReturnValueHandlers(List<HandlerMethodReturnValueHandler> handlers) {
handlers.add(myWarpWithDataHandlerMethodReturnValueHandler);
}
private RequestMappingHandlerAdapter requestMappingHandlerAdapter;
//由于HandlerMethodReturnValueHandler处理的优先级问题,我们通过如下方法,把我们自定义的myWarpWithDataHandlerMethodReturnValueHandler放到第一个;
public void init() {
List<HandlerMethodReturnValueHandler> returnValueHandlers = Lists.newArrayList(myWarpWithDataHandlerMethodReturnValueHandler);
//取出原始列表,重新覆盖进去;
returnValueHandlers.addAll(requestMappingHandlerAdapter.getReturnValueHandlers());
requestMappingHandlerAdapter.setReturnValueHandlers(returnValueHandlers);
}
}
-
微服务下的实战建议
- 微服务的大环境下,服务越小,内聚越高,低耦合服务越健壮,所以一般跨库之间一定是是通过 REST 的 API 协议,进行内部服务之间的调用,这是最稳妥的方式,原因有如下几点:
- REST 的 API 协议更容易监控,更容易实现事务的原子性;
- db 之间解耦,使业务领域代码职责更清晰,更容易各自处理各种问题;
- 只读和读写的 API 更容易分离和管理。
Spring里面事务的配置方法
-
Spring Boot会通过 TransactionAutoConfiguration.java 加载 @EnableTransactionManagement 注解帮我们默认开启事务;
-
默认 @Transactional 注解式事务
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
public Transactional {
String value() default "";
String transactionManager() default "";
Propagation propagation() default Propagation.REQUIRED;
Isolation isolation() default Isolation.DEFAULT;
int timeout() default TransactionDefinition.TIMEOUT_DEFAULT;
boolean readOnly() default false;
Class<? extends Throwable>[] rollbackFor() default {};
String[] rollbackForClassName() default {};
Class<? extends Throwable>[] noRollbackFor() default {};
String[] noRollbackForClassName() default {};
}
隔离级别
-
propagation:代表的是事务的传播机制,这个是 Spring 事务的核心业务逻辑,是 Spring 框架独有的,它和 MySQL 数据库没有一点关系。所谓事务的传播行为是指在同一线程中,在开始当前事务之前,需要判断一下当前线程中是否有另外一个事务存在,如果存在,提供了七个选项来指定当前事务的发生行为。我们可以看 org.springframework.transaction.annotation.Propagation 这类的枚举值来确定有哪些传播行为。7 个表示传播行为的枚举值如下所示。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16public enum Propagation {
// REQUIRED:如果当前存在事务,则加入该事务;如果当前没有事务,则创建一个新的事务。这个值是默认的。
REQUIRED(0),
// SUPPORTS:如果当前存在事务,则加入该事务;如果当前没有事务,则以非事务的方式继续运行。
SUPPORTS(1),
// MANDATORY:如果当前存在事务,则加入该事务;如果当前没有事务,则抛出异常。
MANDATORY(2),
// REQUIRES_NEW:创建一个新的事务,如果当前存在事务,则把当前事务挂起。
REQUIRES_NEW(3),
// NOT_SUPPORTED:以非事务方式运行,如果当前存在事务,则把当前事务挂起。
NOT_SUPPORTED(4),
// NEVER:以非事务方式运行,如果当前存在事务,则抛出异常。
NEVER(5),
// NESTED:如果当前存在事务,则创建一个事务作为当前事务的嵌套事务来运行;如果当前没有事务,则该取值等价于 REQUIRED。
NESTED(6);
}
@Transactional 的局限性
-
一个当前对象调用对象自己里面的方法不起作用的场景
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
public class UserInfoServiceImpl implements UserInfoService {
private UserInfoRepository userInfoRepository;
/**
* 根据UserId产生的一些业务计算逻辑
*/
public UserInfo calculate(Long userId) {
UserInfo userInfo = userInfoRepository.findById(userId).get();
userInfo.setAges(userInfo.getAges()+1);
//.....等等一些复杂事务内的操作
userInfo.setTelephone(Instant.now().toString());
return userInfoRepository.saveAndFlush(userInfo);
}
/**
* 此方法调用自身对象的方法,就会发现calculate方法上面的事务是失效的
*/
public UserInfo save(Long userId) {
return this.calculate(userId);
}
} -
解决方法:
-
方法一: 可以引入一个类
TransactionTemplate
1
2
3public UserInfo save(Long userId) {
return transactionTemplate.execute(status -> this.calculate(userId));
} -
方法二; 自定义
TransactionHelper
-
第一步:新建一个
TransactionHelper
类,进行事务管理,代码如下1
2
3
4
5
6
7
8
9
10
11
12
13/**
* 利用spring进行管理
*/
public class TransactionHelper {
/**
* 利用spring 的机制和jdk8的function机制实现事务
*/
//可以根据实际业务情况,指定明确的回滚异常
public <T, R> R transactional(Function<T, R> function, T t) {
return function.apply(t);
}
} -
第二步:直接在 service 中就可以使用了,代码如下
1
2
3
4
5
6
7
8
private TransactionHelper transactionHelper;
/**
* 调用外部的transactionHelper类,利用transactionHelper方法上面的@Transaction注解使事务生效
*/
public UserInfo save(Long userId) {
return transactionHelper.transactional((uid)->this.calculate(uid),userId);
}
-
-
隐式事务 / AspectJ 事务配置
1 |
|
通过日志分析配置方法的过程
-
第一步,在数据连接中加上 logger=Slf4JLogger&profileSQL=true,用来显示 MySQL 执行的 SQL 日志
-
第二步,打开 Spring 的事务处理日志,用来观察事务的执行过程.
1
2
3
4
5
6
7# Log Transactions Details
logging.level.org.springframework.orm.jpa=DEBUG
logging.level.org.springframework.transaction=TRACE
logging.level.org.hibernate.engine.transaction.internal.TransactionImpl=DEBUG
# 监控连接的情况
logging.level.org.hibernate.resource.jdbc=trace
logging.level.com.zaxxer.hikari=DEBUG -
第三步,执行一个 saveOrUpdate 的操作,详细的执行日志
Spring Cache 结合 Redis 使用的最佳实践
-
不同 cache 的 name 在 redis 里面配置不同的过期时间
-
默认情况下所有 redis 的 cache 过期时间是一样的,实际工作中一般需要自定义不同 cache 的 name 的过期时间,我们这里 cache 的 name 就是指 @Cacheable 里面 value 属性对应的值。主要步骤如下
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74第一步:自定义一个配置文件,用来指定不同的 cacheName 对应的过期时间不一样。代码如下所示。
/**
* 改善一下cacheName的最佳实践方法,目前主要用不同的cache name不同的过期时间,可以扩展
*/
public class MyCacheProperties {
private HashMap<String, Duration> cacheNameConfig;
}
第二步:通过自定义类 MyRedisCacheManagerBuilderCustomizer 实现 RedisCacheManagerBuilderCustomizer 里面的 customize 方法,用来指定不同的 name 采用不同的 RedisCacheConfiguration,从而达到设置不同的过期时间的效果。代码如下所示。
/**
* 这个依赖spring boot 2.2 以上版本才有效
*/
public class MyRedisCacheManagerBuilderCustomizer implements RedisCacheManagerBuilderCustomizer {
private MyCacheProperties myCacheProperties;
private RedisCacheConfiguration redisCacheConfiguration;
public MyRedisCacheManagerBuilderCustomizer(MyCacheProperties myCacheProperties, RedisCacheConfiguration redisCacheConfiguration) {
this.myCacheProperties = myCacheProperties;
this.redisCacheConfiguration = redisCacheConfiguration;
}
/**
* 利用默认配置的只需要在这里加就可以了
* spring.cache.cache-names=abc,def,userlist2,user3
* 下面是不同的cache-name可以配置不同的过期时间,yaml也支持,如果以后还有其他属性扩展可以改这里
* spring.cache.redis.cache-name-config.user2=2h
* spring.cache.redis.cache-name-config.def=2m
* @param builder
*/
public void customize(RedisCacheManager.RedisCacheManagerBuilder builder) {
if (ObjectUtils.isEmpty(myCacheProperties.getCacheNameConfig())) {
return;
}
Map<String, RedisCacheConfiguration> cacheConfigurations = myCacheProperties.getCacheNameConfig().entrySet().stream()
.collect(Collectors
.toMap(e->e.getKey(),v->builder
.getCacheConfigurationFor(v.getKey())
.orElse(RedisCacheConfiguration.defaultCacheConfig().serializeValuesWith(redisCacheConfiguration.getValueSerializationPair()))
.entryTtl(v.getValue())));
builder.withInitialCacheConfigurations(cacheConfigurations);
}
}
第三步:在 CacheConfiguation 里面把我们自定义的 CacheManagerCustomize 加载进去即可,代码如下。
public class CacheConfiguration {
/**
* 支持不同的cache name有不同的缓存时间的配置
*
* @param myCacheProperties
* @param redisCacheConfiguration
* @return
*/
public MyRedisCacheManagerBuilderCustomizer myRedisCacheManagerBuilderCustomizer(MyCacheProperties myCacheProperties, RedisCacheConfiguration redisCacheConfiguration) {
return new MyRedisCacheManagerBuilderCustomizer(myCacheProperties,redisCacheConfiguration);
}
}
第四步:使用的时候非常简单,只需要在 application.properties 里面做如下配置即可。
# 设置默认的过期时间是20分钟
spring.cache.redis.time-to-live=20m
# 设置 5分钟过期
spring.cache.redis.cache-name-config.userInfo=5m
# 设置 room的cache1小时过期
spring.cache.redis.cache-name-config.room=1h -
自定义 KeyGenerator 实现,redis 的 key 自定义拼接规则
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
public class MyRedisCachingConfigurerSupport extends CachingConfigurerSupport {
public KeyGenerator keyGenerator() {
return getKeyGenerator();
}
/**
* 覆盖默认的redis key的生成规则,变成"方法名:参数:参数"
* @return
*/
public static KeyGenerator getKeyGenerator() {
return (target, method, params) -> {
StringBuilder key = new StringBuilder();
key.append(ClassUtils.getQualifiedMethodName(method));
for (Object obc : params) {
key.append(":").append(obc);
}
return key.toString();
};
}
/**
* 覆盖默认异常处理方法,不抛异常,改打印error日志
*
* @return
*/
public CacheErrorHandler errorHandler() {
return new CacheErrorHandler() {
public void handleCacheGetError(RuntimeException exception, Cache cache, Object key) {
log.error(String.format("Spring cache GET error:cache=%s,key=%s", cache, key), exception);
}
public void handleCachePutError(RuntimeException exception, Cache cache, Object key, Object value) {
log.error(String.format("Spring cache PUT error:cache=%s,key=%s", cache, key), exception);
}
public void handleCacheEvictError(RuntimeException exception, Cache cache, Object key) {
log.error(String.format("Spring cache EVICT error:cache=%s,key=%s", cache, key), exception);
}
public void handleCacheClearError(RuntimeException exception, Cache cache) {
log.error(String.format("Spring cache CLEAR error:cache=%s", cache), exception);
}
};
}
} -
改变默认的 cache 里面 redis 的 value 序列化方式
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
public class CacheConfiguration {
/**
* 支持不同的cache name有不同的缓存时间的配置
*
* @param myCacheProperties
* @param redisCacheConfiguration
* @return
*/
public MyRedisCacheManagerBuilderCustomizer myRedisCacheManagerBuilderCustomizer(MyCacheProperties myCacheProperties, RedisCacheConfiguration redisCacheConfiguration) {
return new MyRedisCacheManagerBuilderCustomizer(myCacheProperties,redisCacheConfiguration);
}
/**
* cache异常不抛异常,只打印error日志
*
* @return
*/
public MyRedisCachingConfigurerSupport myRedisCachingConfigurerSupport() {
return new MyRedisCachingConfigurerSupport();
}
/**
* 依赖默认的ObjectMapper,实现普通的json序列化
* @param defaultObjectMapper
* @return
*/
public GenericJackson2JsonRedisSerializer genericJackson2JsonRedisSerializer(ObjectMapper defaultObjectMapper) {
ObjectMapper objectMapper = defaultObjectMapper.copy();
objectMapper.registerModule(new Hibernate5Module().enable(REPLACE_PERSISTENT_COLLECTIONS)); //支持JPA的实体的json的序列化
objectMapper.configure(MapperFeature.SORT_PROPERTIES_ALPHABETICALLY, true);//培训
objectMapper.deactivateDefaultTyping(); //关闭 defaultType,不需要关心reids里面是否为对象的类型
return new GenericJackson2JsonRedisSerializer(objectMapper);
}
/**
* 覆盖 RedisCacheConfiguration,只是修改serializeValues with jackson
*
* @param cacheProperties
* @return
*/
public RedisCacheConfiguration jacksonRedisCacheConfiguration(CacheProperties cacheProperties,
GenericJackson2JsonRedisSerializer genericJackson2JsonRedisSerializer) {
CacheProperties.Redis redisProperties = cacheProperties.getRedis();
RedisCacheConfiguration config = RedisCacheConfiguration
.defaultCacheConfig();
config = config.serializeValuesWith(RedisSerializationContext.SerializationPair.fromSerializer(genericJackson2JsonRedisSerializer));//修改的关键所在,指定Jackson2JsonRedisSerializer的方式
if (redisProperties.getTimeToLive() != null) {
config = config.entryTtl(redisProperties.getTimeToLive());
}
if (redisProperties.getKeyPrefix() != null) {
config = config.prefixCacheNameWith(redisProperties.getKeyPrefix());
}
if (!redisProperties.isCacheNullValues()) {
config = config.disableCachingNullValues();
}
if (!redisProperties.isUseKeyPrefix()) {
config = config.disableKeyPrefix();
}
return config;
}
}
Spring Data JPA 单元测试的最佳实践
1 | 第一步:引入 test 的依赖 org.springframework.boot:spring-boot-starter-test |