当已经安装Redis,并确保环境变量可用后,可以在命令提示符窗口(CMD)或终端(IDEA的Terminal,或MacOS/Linux的命令窗口)中执行相关命令。
在终端下,可以通过redis-cli登录Redis客户端:
redis-cli
在Redis客户端中,可以通过ping检测Redis是否正常工作,将得到PONG的反馈:
ping
在Redis客户端中,可以通过set命令向Redis中存入或修改简单类型的数据:
set name jack
在Redis客户端中,可以通过get命令从Redis中取出简单类型的数据:
get name
如果使用的Key并不存在,使用
get命令时,得到的结果将是(nil),等效于Java中的null
在Redis客户端中,可以通过keys命令检索Key:
keys *
keys a*
注意:默认情况下,Redis是单线程的,keys命令会执行整个Redis的检索,所以,执行时间可能较长,可能导致阻塞!
需要添加spring-boot-starter-data-redis依赖项:
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-data-redisartifactId>
dependency>
以上依赖项默认会连接localhost:6379,并且无用户名、无密码,所以,当你的Redis符合此配置,则不需要在application.properties 中添加任何配置就可以直接编程。
如果需要显式的配置,各配置项的属性名分别为:
spring.redis.hostspring.redis.portspring.redis.usernamespring.redis.passwordRedisTemplate在使用以上依赖项实现Redis编程时,需要使用到的工具类型为RedisTemplate,调用此类的对象的方法,即可实现读写Redis中的数据。
在使用之前,应该先在配置类中使用@Bean方法创建RedisTemplate,并实现对RedisTemplate的基础配置,则在项目的根包下创建config.RedisConfiguration类:
package cn.tedu.csmall.product.config;
import lombok.extern.slf4j.Slf4j;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.serializer.RedisSerializer;
import java.io.Serializable;
/**
* Redis的配置类
*
* @author java@tedu.cn
* @version 0.0.1
*/
@Slf4j
@Configuration
public class RedisConfiguration {
public RedisConfiguration() {
log.info("加载配置类:RedisConfiguration");
}
@Bean
public RedisTemplate<String, Serializable> redisTemplate(
RedisConnectionFactory redisConnectionFactory) {
RedisTemplate<String, Serializable> redisTemplate
= new RedisTemplate<>();
redisTemplate.setConnectionFactory(redisConnectionFactory);
redisTemplate.setKeySerializer(RedisSerializer.string());
redisTemplate.setValueSerializer(RedisSerializer.json());
return redisTemplate;
}
}
ValueOperations读写一般值数据使用RedisTemplate访问一般值(字符串、数值等)数据时,需要先获取ValueOperations对象,再调用此对象的API进行数据操作。
例如:测试向Redis中写入一个字符串:
@Autowired
RedisTemplate<String, Serializable> redisTemplate;
@Test
void testValueOpsSet() {
ValueOperations<String, Serializable> ops = redisTemplate.opsForValue();
String key = "username";
String value = "admin";
ops.set(key, value);
log.debug("已经向Redis中写入Key={}且Value={}的数据!", key, value);
}
由于声明的RedisTemplate的值的泛型是Serializable,所以,从Redis中读取到的值的类型会是Serializable接口类型。
例如:测试从Redis中读取此前写入的字符串:
@Test
void testValueOpsGet() {
ValueOperations<String, Serializable> ops = redisTemplate.opsForValue();
// 从Redis中读取数据
String key = "username";
Serializable value = ops.get(key);
log.debug("已经从Redis中读取Key={}的数据,Value={}", key, value);
}
由于配置RedisTemplate时,使用的值序列化器是JSON(redisTemplate.setValueSerializer(RedisSerializer.json());),所以,可以直接写入对象,会被自动处理为JSON格式的字符串。
另外,由于声明的RedisTemplate的值的泛型是Serializable,所以,写入的值的类型必须实现了Serializable接口。
例如:测试向Redis中写入一个对象:
@Test
void testValueOpsSetObject() {
ValueOperations<String, Serializable> ops = redisTemplate.opsForValue();
String key = "brand1";
Brand brand = new Brand();
brand.setId(1L);
brand.setName("大白象");
brand.setEnable(1);
ops.set(key, brand);
log.debug("已经向Redis中写入Key={}且Value={}的数据!", key, brand);
}
例如:测试从Redis中读取此前写入的对象:
@Test
void testValueOpsGetObject() {
ValueOperations<String, Serializable> ops = redisTemplate.opsForValue();
String key = "brand1";
Serializable value = ops.get(key);
log.debug("已经从Redis中读取Key={}的数据,Value={}", key, value);
log.debug("读取到的值类型是:{}", value.getClass().getName());
if (value instanceof Brand) {
Brand brand = (Brand) value;
log.debug("将读取到的值类型转换为Brand类型,成功:{}", brand);
} else {
log.debug("读取到的值类型不是Brand类型,无法实现类型转换!");
}
}
直接调用RedisTemplate的keys()方法,即可查询当前Redis中有哪些Key。
例如:测试查询Redis中所有的Key:
@Test
void testKeys() {
Set<String> keys = redisTemplate.keys("*");
for (String key : keys) {
log.debug("{}", key);
}
}
删除数据时,不关心值的类型,只需要知道Key即可,所以,删除数据时直接调用RedisTemplate的delete()方法即可。
例如:测试删除Redis中的某个数据:
@Test
void testDelete() {
String key = "name";
Boolean result = redisTemplate.delete(key);
log.debug("尝试删除Redis中Key={}的数据,操作结果为:{}", key, result);
}
提示:RedisTemplate的API中,还有批量删除的操作,例如(以下是RedisTemplate的部分源代码):
public Long delete(Collection<K> keys) {
if (CollectionUtils.isEmpty(keys)) {
return 0L;
} else {
byte[][] rawKeys = this.rawKeys(keys);
return (Long)this.execute((connection) -> {
return connection.del(rawKeys);
}, true);
}
}
在操作List列表数据之前,需要先调用RedisTemplate对象的opsForList()方法,得到ListOperations对象,再进行列表数据的操作。
在存入列表数据时,ListOperations支持从左侧压栈来存入数据,或从右侧压栈来存入数据,这2者的区别如下图所示:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Jd9WH0eF-1663683410687)(images/image-20220919113454939.png)]
通常,从右侧压栈存入数据比较符合大多情况下的需求。
例如:测试写入列表数据:
@Test
void testRightPushList() {
// push:压栈(存入数据)
// pop:弹栈(拿走数据)
// 使用RedisTemplate向Redis存入List数据:
// 1. 需要调用 opsForList() 得到 ListOperations 对象
// 2. ListOperations每次只能存入1个列表项数据
List<Brand> brands = new ArrayList<>();
for (int i = 1; i <= 8; i++) {
Brand brand = new Brand();
brand.setId(i + 0L);
brand.setName("测试品牌" + i);
brands.add(brand);
}
ListOperations<String, Serializable> opsForList = redisTemplate.opsForList();
String key = "brandList";
for (Brand brand : brands) {
opsForList.rightPush(key, brand);
}
}
调用ListOperations对象的size()方法即可获取列表的长度。
例如:获取列表的长度:
@Test
void testListSize() {
ListOperations<String, Serializable> opsForList = redisTemplate.opsForList();
String key = "brandList";
Long size = opsForList.size(key);
log.debug("列表 key={} 的长度(元素数量)为:{}", key, size);
}
如果需要读取数据,首先必须了解,Redis中的列表数据项即有正数的索引(下标),也有负数的索引(下标),正数的是从左侧第1位使用0开始向右顺序编号,而负数的是从右侧第1位使用-1并向左侧递减的编号,如下图所示:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-b2Po68xp-1663683410689)(images/image-20220919115445121.png)]
使用ListOperations的range()方法可以获取列表的区间段子列表。
例如:测试获取列表数据:
@Test
void testListRange() {
ListOperations<String, Serializable> opsForList = redisTemplate.opsForList();
String key = "brandList";
long start = 0L;
long end = -1L;
List<Serializable> list = opsForList.range(key, start, end);
for (Serializable serializable : list) {
log.debug("列表项:{}", serializable);
}
}
在当前项目中,品牌、类别这些数据应该是适合使用Redis的!
以缓存品牌数据为例,可以先在根包下创建repository.IBrandCacheRepository接口:
package cn.tedu.csmall.product.repository;
import cn.tedu.csmall.product.pojo.vo.BrandListItemVO;
import cn.tedu.csmall.product.pojo.vo.BrandStandardVO;
import java.util.List;
import java.util.Set;
/**
* 处理品牌缓存数据的存储接口
*
* @author java@tedu.cn
* @version 0.0.1
*/
public interface IBrandCacheRepository {
/**
* 将某个品牌数据保存到Redis中
*
* @param brandStandardVO 品牌数据
*/
void save(BrandStandardVO brandStandardVO);
/**
* 从Redis中取出品牌数据
*
* @param id 品牌id
* @return 匹配的品牌数据,如果Redis中没有匹配的数据,将返回null
*/
BrandStandardVO get(Long id);
/**
* 将品牌列表数据保存到Redis中
*
* @param brandList 品牌列表数据
*/
void saveList(List<BrandListItemVO> brandList);
/**
* 从Redis中取出品牌列表数据
*
* @return 品牌列表数据
*/
List<BrandListItemVO> getList();
/**
* 获取所有品牌缓存数据的Key
*
* @return 所有品牌缓存数据的Key
*/
Set<String> getAllKeys();
/**
* 删除所有缓存的品牌数据
*
* @param keys 所有缓存的品牌数据的Key的集合
* @return 删除的数据的数量
*/
Long deleteAll(Set<String> keys);
}
然后,在根包下创建repository.impl.BrandCacheRepositoryImpl类,实现以上接口:
package cn.tedu.csmall.product.repository.impl;
import cn.tedu.csmall.product.pojo.entity.Brand;
import cn.tedu.csmall.product.pojo.vo.BrandListItemVO;
import cn.tedu.csmall.product.pojo.vo.BrandStandardVO;
import cn.tedu.csmall.product.repository.IBrandCacheRepository;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.ListOperations;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.core.ValueOperations;
import org.springframework.stereotype.Repository;
import java.io.Serializable;
import java.util.ArrayList;
import java.util.List;
import java.util.Set;
/**
* 处理品牌缓存数据的存储实现类
*
* @author java@tedu.cn
* @version 0.0.1
*/
@Slf4j
@Repository
public class BrandCacheRepositoryImpl implements IBrandCacheRepository {
@Autowired
private RedisTemplate<String, Serializable> redisTemplate;
public static final String KEY_ITEM_PREFIX = "brand:item:";
public static final String KEY_LIST = "brand:list";
public BrandCacheRepositoryImpl() {
log.debug("创建处理缓存的对象:BrandCacheRepositoryImpl");
}
@Override
public void save(BrandStandardVO brandStandardVO) {
ValueOperations<String, Serializable> opsForValue = redisTemplate.opsForValue();
String key = getItemKey(brandStandardVO.getId());
opsForValue.set(key, brandStandardVO);
}
@Override
public BrandStandardVO get(Long id) {
ValueOperations<String, Serializable> opsForValue = redisTemplate.opsForValue();
String key = getItemKey(id);
Serializable value = opsForValue.get(key);
if (value != null) {
if (value instanceof BrandStandardVO) {
return (BrandStandardVO) value;
}
}
return null;
}
@Override
public void saveList(List<BrandListItemVO> brandList) {
ListOperations<String, Serializable> opsForList = redisTemplate.opsForList();
for (BrandListItemVO brand : brandList) {
opsForList.rightPush(KEY_LIST, brand);
}
}
@Override
public List<BrandListItemVO> getList() {
ListOperations<String, Serializable> opsForList = redisTemplate.opsForList();
long start = 0L;
long end = -1L;
List<Serializable> list = opsForList.range(KEY_LIST, start, end);
List<BrandListItemVO> brands = new ArrayList<>();
for (Serializable serializable : list) {
brands.add((BrandListItemVO) serializable);
}
return brands;
}
@Override
public Set<String> getAllKeys() {
String allKeysPattern = "brand:*";
return redisTemplate.keys(allKeysPattern);
}
@Override
public Long deleteAll(Set<String> keys) {
return redisTemplate.delete(keys);
}
private String getItemKey(Long id) {
return KEY_ITEM_PREFIX + id;
}
}
在src/test/java的根包下创建repository.BrandCacheRepositoryTests测试类,测试以上方法:
package cn.tedu.csmall.product.repository;
import cn.tedu.csmall.product.pojo.vo.BrandStandardVO;
import lombok.extern.slf4j.Slf4j;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
@Slf4j
@SpringBootTest
public class BrandCacheRepositoryTests {
@Autowired
IBrandCacheRepository repository;
@Test
void testSave() {
BrandStandardVO brandStandardVO = new BrandStandardVO();
brandStandardVO.setId(1L);
brandStandardVO.setName("华为");
repository.save(brandStandardVO);
log.debug("存入数据完成!");
}
@Test
void testGet() {
Long id = 1L;
BrandStandardVO brandStandardVO = repository.get(id);
log.debug("获取数据完成:{}", brandStandardVO);
}
}
缓存预热:当服务启动时,就将数据加载到缓存!
在Spring Boot项目中,可以使用组件类(添加了@Component等注解的类)实现ApplicationRunner接口,重写其中的run()方法,此方法会在服务刚刚启动完成时自动执行!
package cn.tedu.csmall.product.preload;
import cn.tedu.csmall.product.service.IBrandService;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.ApplicationArguments;
import org.springframework.boot.ApplicationRunner;
import org.springframework.stereotype.Component;
@Slf4j
@Component
public class CachePreload implements ApplicationRunner {
@Autowired
private IBrandService brandService;
public CachePreload() {
log.debug("创建服务启动后自动执行任务的对象:CachePreload");
}
@Override
public void run(ApplicationArguments args) throws Exception {
log.debug("CachePreload.run()");
brandService.loadBrandsToCache();
}
}
计划任务:每间隔一段时间,或到了特定的时间点,就会自动执行的任务!
在Spring Boot项目中,组件类中添加了@Scheduled注解的方法,就是计划任务的方法。
注意:在Spring Boot中,计划任务默认是禁用的,需要在配置类上添加@EnableScheudling注解才可以启用!
在根包下创建config.ScheduleConfiguration配置类,启用计划任务:
@Configuration
@EnableScheduling
public class ScheduleConfiguration {
}
在根包下创建schedule.CacheSchedule类:
package cn.tedu.csmall.product.schedule;
import cn.tedu.csmall.product.service.IBrandService;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Component;
/**
* 处理缓存的计划任务类
*
* @author java@tedu.cn
* @version 0.0.1
*/
@Slf4j
@Component
public class CacheSchedule {
@Autowired
private IBrandService brandService;
// 关于@Scheduled注解的配置
// 以下的各属性不可以同时配置
// >> fixedRate:每间隔多少毫秒执行1次
// >> fixedDelay:每执行结束后,过多少毫秒执行1次
// >> cron:使用1个字符串,字符串中包含6~7个值,各值之间使用空格分隔
// >> >> 各值分别表示:秒 分 时 日 月 周(星期) [年]
// >> >> 例如:cron = "56 34 12 20 1 ? 2023",表示"2023年1月20日12:34:56秒将执行,无论这一天是星期几"
// >> >> 以上各个值,均可使用星号(*)作为通配符,表示任意值
// >> >> 在“日”和“周”位置,还可以使用问号(?),表示不关心具体值
// >> >> 以上各个值,还可以使用“x/x”格式的值,例如在分钟位置使用 1/5,表示分钟值为1时执行,且每间隔5个单位(分钟)执行1次
@Scheduled(fixedRate = 1 * 60 * 60 * 1000)
public void loadBrandsToCache() {
log.debug("开始执行计划任务……");
brandService.loadBrandsToCache();
log.debug("本次计划任务执行完成!");
}
}
AOP:面向切面的编程。
AOP可以用于解决“在处理多种不同的业务时都需要执行相同的任务”的相关问题。
例如:统计每个业务方法的执行耗时。
首先,需要添加依赖项:
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-aopartifactId>
dependency>
在项目的根包下创建aop.TimerAspect类:
package cn.tedu.csmall.product.aop;
import lombok.extern.slf4j.Slf4j;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.springframework.stereotype.Component;
@Slf4j
@Aspect
@Component
public class TimerAspect {
// @Around(环绕):在匹配到的方法之前和之后都执行
@Around("execution(* cn.tedu.csmall.product.service.impl.*.*(..))")
// 任何返回值类型
// 任何类
// 任何方法
// 无论参数数量多少
// 除了@Around以外,其实还有:@Before / @After / @AfterReturning / @AfterThrowing
public Object a(ProceedingJoinPoint pjp) throws Throwable {
log.debug("TimerAspect执行了切面方法……");
long start = System.currentTimeMillis();
// 执行以上@Around注解匹配到的方法
// 注意:不要try...catch异常
// 注意:必须获取返回值,并返回
Object result = pjp.proceed();
long end = System.currentTimeMillis();
log.debug("当前业务方法执行耗时:{}毫秒", end - start);
return result;
}
}
生成gmt_create、gmt_modified字段值的Mybatis拦截器的代码:
package cn.tedu.csmall.product.interceptor.mybatis;
import lombok.extern.slf4j.Slf4j;
import org.apache.ibatis.executor.statement.StatementHandler;
import org.apache.ibatis.mapping.BoundSql;
import org.apache.ibatis.plugin.*;
import java.lang.reflect.Field;
import java.sql.Connection;
import java.time.LocalDateTime;
import java.util.Properties;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
/**
* 基于MyBatis的自动更新"最后修改时间"的拦截器
*
* 需要SQL语法预编译之前进行拦截,则拦截类型为StatementHandler,拦截方法是prepare
*
* 具体的拦截处理由内部的intercept()方法实现
*
* 注意:由于仅适用于当前项目,并不具备范用性,所以:
*
*
* - 拦截所有的update方法(根据SQL语句以update前缀进行判定),无法不拦截某些update方法
* - 所有数据表中"最后修改时间"的字段名必须一致,由本拦截器的FIELD_MODIFIED属性进行设置
*
*
* @author java@tedu.cn
* @version 0.0.1
*/
@Slf4j
@Intercepts({@Signature(
type = StatementHandler.class,
method = "prepare",
args = {Connection.class, Integer.class}
)})
public class InsertUpdateTimeInterceptor implements Interceptor {
/**
* 自动添加的创建时间字段
*/
private static final String FIELD_CREATE = "gmt_create";
/**
* 自动更新时间的字段
*/
private static final String FIELD_MODIFIED = "gmt_modified";
/**
* SQL语句类型:其它(暂无实际用途)
*/
private static final int SQL_TYPE_OTHER = 0;
/**
* SQL语句类型:INSERT
*/
private static final int SQL_TYPE_INSERT = 1;
/**
* SQL语句类型:UPDATE
*/
private static final int SQL_TYPE_UPDATE = 2;
/**
* 查找SQL类型的正则表达式:INSERT
*/
private static final String SQL_TYPE_PATTERN_INSERT = "^insert\\s";
/**
* 查找SQL类型的正则表达式:UPDATE
*/
private static final String SQL_TYPE_PATTERN_UPDATE = "^update\\s";
/**
* 查询SQL语句片段的正则表达式:gmt_modified片段
*/
private static final String SQL_STATEMENT_PATTERN_MODIFIED = ",\\s*" + FIELD_MODIFIED + "\\s*=";
/**
* 查询SQL语句片段的正则表达式:gmt_create片段
*/
private static final String SQL_STATEMENT_PATTERN_CREATE = ",\\s*" + FIELD_CREATE + "\\s*[,)]?";
/**
* 查询SQL语句片段的正则表达式:WHERE子句
*/
private static final String SQL_STATEMENT_PATTERN_WHERE = "\\s+where\\s+";
/**
* 查询SQL语句片段的正则表达式:VALUES子句
*/
private static final String SQL_STATEMENT_PATTERN_VALUES = "\\)\\s*values?\\s*\\(";
@Override
public Object intercept(Invocation invocation) throws Throwable {
// 日志
log.debug("准备拦截SQL语句……");
// 获取boundSql,即:封装了即将执行的SQL语句及相关数据的对象
BoundSql boundSql = getBoundSql(invocation);
// 从boundSql中获取SQL语句
String sql = getSql(boundSql);
// 日志
log.debug("原SQL语句:{}", sql);
// 准备新SQL语句
String newSql = null;
// 判断原SQL类型
switch (getOriginalSqlType(sql)) {
case SQL_TYPE_INSERT:
// 日志
log.debug("原SQL语句是【INSERT】语句,准备补充更新时间……");
// 准备新SQL语句
newSql = appendCreateTimeField(sql, LocalDateTime.now());
break;
case SQL_TYPE_UPDATE:
// 日志
log.debug("原SQL语句是【UPDATE】语句,准备补充更新时间……");
// 准备新SQL语句
newSql = appendModifiedTimeField(sql, LocalDateTime.now());
break;
}
// 应用新SQL
if (newSql != null) {
// 日志
log.debug("新SQL语句:{}", newSql);
reflectAttributeValue(boundSql, "sql", newSql);
}
// 执行调用,即拦截器放行,执行后续部分
return invocation.proceed();
}
public String appendModifiedTimeField(String sqlStatement, LocalDateTime dateTime) {
Pattern gmtPattern = Pattern.compile(SQL_STATEMENT_PATTERN_MODIFIED, Pattern.CASE_INSENSITIVE);
if (gmtPattern.matcher(sqlStatement).find()) {
log.debug("原SQL语句中已经包含gmt_modified,将不补充添加时间字段");
return null;
}
StringBuilder sql = new StringBuilder(sqlStatement);
Pattern whereClausePattern = Pattern.compile(SQL_STATEMENT_PATTERN_WHERE, Pattern.CASE_INSENSITIVE);
Matcher whereClauseMatcher = whereClausePattern.matcher(sql);
// 查找 where 子句的位置
if (whereClauseMatcher.find()) {
int start = whereClauseMatcher.start();
int end = whereClauseMatcher.end();
String clause = whereClauseMatcher.group();
log.debug("在原SQL语句 {} 到 {} 找到 {}", start, end, clause);
String newSetClause = ", " + FIELD_MODIFIED + "='" + dateTime + "'";
sql.insert(start, newSetClause);
log.debug("在原SQL语句 {} 插入 {}", start, newSetClause);
log.debug("生成SQL: {}", sql);
return sql.toString();
}
return null;
}
public String appendCreateTimeField(String sqlStatement, LocalDateTime dateTime) {
// 如果 SQL 中已经包含 gmt_create 就不在添加这两个字段了
Pattern gmtPattern = Pattern.compile(SQL_STATEMENT_PATTERN_CREATE, Pattern.CASE_INSENSITIVE);
if (gmtPattern.matcher(sqlStatement).find()) {
log.debug("已经包含 gmt_create 不再添加 时间字段");
return null;
}
// INSERT into table (xx, xx, xx ) values (?,?,?)
// 查找 ) values ( 的位置
StringBuilder sql = new StringBuilder(sqlStatement);
Pattern valuesClausePattern = Pattern.compile(SQL_STATEMENT_PATTERN_VALUES, Pattern.CASE_INSENSITIVE);
Matcher valuesClauseMatcher = valuesClausePattern.matcher(sql);
// 查找 ") values " 的位置
if (valuesClauseMatcher.find()) {
int start = valuesClauseMatcher.start();
int end = valuesClauseMatcher.end();
String str = valuesClauseMatcher.group();
log.debug("找到value字符串:{} 的位置 {}, {}", str, start, end);
// 插入字段列表
String fieldNames = ", " + FIELD_CREATE + ", " + FIELD_MODIFIED;
sql.insert(start, fieldNames);
log.debug("插入字段列表{}", fieldNames);
// 定义查找参数值位置的 正则表达 “)”
Pattern paramPositionPattern = Pattern.compile("\\)");
Matcher paramPositionMatcher = paramPositionPattern.matcher(sql);
// 从 ) values ( 的后面位置 end 开始查找 结束括号的位置
String param = ", '" + dateTime + "', '" + dateTime + "'";
int position = end + fieldNames.length();
while (paramPositionMatcher.find(position)) {
start = paramPositionMatcher.start();
end = paramPositionMatcher.end();
str = paramPositionMatcher.group();
log.debug("找到参数值插入位置 {}, {}, {}", str, start, end);
sql.insert(start, param);
log.debug("在 {} 插入参数值 {}", start, param);
position = end + param.length();
}
if (position == end) {
log.warn("没有找到插入数据的位置!");
return null;
}
} else {
log.warn("没有找到 ) values (");
return null;
}
log.debug("生成SQL: {}", sql);
return sql.toString();
}
@Override
public Object plugin(Object target) {
// 本方法的代码是相对固定的
if (target instanceof StatementHandler) {
return Plugin.wrap(target, this);
} else {
return target;
}
}
@Override
public void setProperties(Properties properties) {
// 无须执行操作
}
/**
* 获取BoundSql对象,此部分代码相对固定
*
* 注意:根据拦截类型不同,获取BoundSql的步骤并不相同,此处并未穷举所有方式!
*
* @param invocation 调用对象
* @return 绑定SQL的对象
*/
private BoundSql getBoundSql(Invocation invocation) {
Object invocationTarget = invocation.getTarget();
if (invocationTarget instanceof StatementHandler) {
StatementHandler statementHandler = (StatementHandler) invocationTarget;
return statementHandler.getBoundSql();
} else {
throw new RuntimeException("获取StatementHandler失败!请检查拦截器配置!");
}
}
/**
* 从BoundSql对象中获取SQL语句
*
* @param boundSql BoundSql对象
* @return 将BoundSql对象中封装的SQL语句进行转换小写、去除多余空白后的SQL语句
*/
private String getSql(BoundSql boundSql) {
return boundSql.getSql().toLowerCase().replaceAll("\\s+", " ").trim();
}
/**
* 通过反射,设置某个对象的某个属性的值
*
* @param object 需要设置值的对象
* @param attributeName 需要设置值的属性名称
* @param attributeValue 新的值
* @throws NoSuchFieldException 无此字段异常
* @throws IllegalAccessException 非法访问异常
*/
private void reflectAttributeValue(Object object, String attributeName, String attributeValue) throws NoSuchFieldException, IllegalAccessException {
Field field = object.getClass().getDeclaredField(attributeName);
field.setAccessible(true);
field.set(object, attributeValue);
}
/**
* 获取原SQL语句类型
*
* @param sql 原SQL语句
* @return SQL语句类型
*/
private int getOriginalSqlType(String sql) {
Pattern pattern;
pattern = Pattern.compile(SQL_TYPE_PATTERN_INSERT, Pattern.CASE_INSENSITIVE);
if (pattern.matcher(sql).find()) {
return SQL_TYPE_INSERT;
}
pattern = Pattern.compile(SQL_TYPE_PATTERN_UPDATE, Pattern.CASE_INSENSITIVE);
if (pattern.matcher(sql).find()) {
return SQL_TYPE_UPDATE;
}
return SQL_TYPE_OTHER;
}
}
注册Mybatis拦截器的代码(需定义在配置类中):
@Autowired
private List<SqlSessionFactory> sqlSessionFactoryList;
@PostConstruct
public void addInterceptor() {
InsertUpdateTimeInterceptor interceptor = new InsertUpdateTimeInterceptor();
for (SqlSessionFactory sqlSessionFactory : sqlSessionFactoryList) {
sqlSessionFactory.getConfiguration().addInterceptor(interceptor);
}
}