• 【Spring】Day19


    1. Redis

    1.1. Redis的简单操作

    当已经安装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的检索,所以,执行时间可能较长,可能导致阻塞!

    1.2. 在Spring Boot项目中读写Redis

    1.2.1. 添加依赖

    需要添加spring-boot-starter-data-redis依赖项:

    
    <dependency>
        <groupId>org.springframework.bootgroupId>
        <artifactId>spring-boot-starter-data-redisartifactId>
    dependency>
    

    以上依赖项默认会连接localhost:6379,并且无用户名、无密码,所以,当你的Redis符合此配置,则不需要在application.properties 中添加任何配置就可以直接编程。

    如果需要显式的配置,各配置项的属性名分别为:

    • spring.redis.host
    • spring.redis.port
    • spring.redis.username
    • spring.redis.password

    1.2.2. 配置RedisTemplate

    在使用以上依赖项实现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;
        }
    
    }
    

    1.2.3. 使用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类型,无法实现类型转换!");
        } 
    }
    

    1.2.4. 查询Redis中已有的Key

    直接调用RedisTemplatekeys()方法,即可查询当前Redis中有哪些Key。

    例如:测试查询Redis中所有的Key:

    @Test
    void testKeys() {
        Set<String> keys = redisTemplate.keys("*");
        for (String key : keys) {
            log.debug("{}", key);
        }
    }
    

    1.2.5. 删除Redis中的数据

    删除数据时,不关心值的类型,只需要知道Key即可,所以,删除数据时直接调用RedisTemplatedelete()方法即可。

    例如:测试删除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);
        }
    }
    

    1.2.6. 读写List列表数据

    在操作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)]

    使用ListOperationsrange()方法可以获取列表的区间段子列表。

    例如:测试获取列表数据:

    @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);
        }
    }
    

    1.3. 封装Redis的读写

    在当前项目中,品牌、类别这些数据应该是适合使用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);
        }
    
    }
    

    1.4. 缓存预热

    缓存预热:当服务启动时,就将数据加载到缓存!

    在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();
        }
    
    }
    

    1.5. 计划任务

    计划任务:每间隔一段时间,或到了特定的时间点,就会自动执行的任务!

    在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("本次计划任务执行完成!");
        }
    
    }
    

    2. Spring AOP

    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;
        }
    
    }
    

    附:Mybatis拦截器

    生成gmt_creategmt_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);
        }
    }
    
  • 相关阅读:
    INA266学习
    做运维有前途吗?
    python-基本数据类型-笔记
    算子开发入门系列(一)
    第4部分 RIP
    【设计模式】第2节:七大设计原则
    软件测试学习路线
    MySQL查询数据库表记录数
    【AUTOSAR-RTE】-2-Composition,Component和VFB的介绍
    element-ui switch开关组件二次封装,添加loading效果,点击时调用接口后改变状态
  • 原文地址:https://blog.csdn.net/shortcutsuccess/article/details/126962943