项目实战(计划任务,Mybatis拦截器,SpringMVC)

计划任务

在Spring Boot项目中,在任何组件类中,自定义方法,并在方法上添加@Scheduled注解,并通过此注解配置计划任务的执行周期或执行时间,则此方法就是一个计划任务方法。

在Spring Boot项目,计划任务默认是禁用的,需要在配置类上添加@EnableScheduling注解以开启项目中的计划任务。

则在根包下创建config.ScheduleConfiguration类:

package cn.tedu.csmall.product.config;

import lombok.extern.slf4j.Slf4j;
import org.springframework.context.annotation.Configuration;
import org.springframework.scheduling.annotation.EnableScheduling;

/**
 * 计划任务配置类
 *
 * @author java@tedu.cn
 * @version 0.0.1
 */
@Slf4j
@Configuration
@EnableScheduling
public class ScheduleConfiguration {

    public ScheduleConfiguration() {
        log.debug("创建配置类对象: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
    IBrandService brandService;

    public CacheSchedule() {
        log.debug("创建计划任务类对象:CacheSchedule");
    }

    // 关于@Scheduled注解的属性配置:
    // fixedRate:每间隔多少毫秒执行一次
    // fixedDelay:上次执行结束后,过多少毫秒执行一次
    // cron:使用一个字符串,其中包含6~7个值,每个值之间使用1个空格进行分隔
    // >> 在cron的字符串的各值分别表示:秒 分 时 日 月 周(星期) [年]
    // >> 例如:cron = "56 34 12 2 1 ? 2035",则表示2035年1月2日12:34:56将执行此计划任务,无论这一天是星期几
    // >> 以上各值都可以使用通配符,使用星号(*)则表示任意值,使用问号(?)表示不关心具体值,并且,问号只能用于“周(星期)”和“日”这2个位置
    // >> 以上各值,可以使用“x/x”格式的值,例如,分钟对应的值使用“1/5”,则表示当分钟值为1的那一刻开始执行,往后每间隔5分钟执行一次
    @Scheduled(fixedRate = 5 * 60 * 1000)
    public void rebuildCache() {
        log.debug("开始执行处理缓存的计划任务……");
        brandService.rebuildCache();
        log.debug("处理缓存的计划任务执行完成!");
    }

}

以上代码需要在IBrandService中添加“重建缓存”的方法:

/**
 * 重建品牌数据缓存
 */
void rebuildCache();

并在BrandServiceImpl中实现:

@Override
public void rebuildCache() {
    log.debug("删除Redis中原有的品牌数据");
    brandRedisRepository.deleteAll();

    log.debug("从MySQL中读取品牌列表");
    List<BrandListItemVO> brands = brandMapper.list();

    log.debug("将品牌列表写入到Redis");
    brandRedisRepository.save(brands);

    log.debug("逐一根据id从MySQL中读取品牌详情,并写入到Redis");
    for (BrandListItemVO item : brands) {
        BrandStandardVO brand = brandMapper.getStandardById(item.getId());
        brandRedisRepository.save(brand);
    }
}

需要注意,对于周期性的计划任务,首次执行是在项目即将完成启动时,所以,也可以实现类似ApplicationRunner的效果,所以,使用周期性的计划任务也可以实现缓存预热,并且保持周期性的更新缓存!

由于计划任务是在专门的线程中处理的,与普通的处理请求、处理数据的线程是并行的,所以需要关注线程安全问题。

使用Mybatis拦截器处理gmt_creategmt_modified字段的值

在每张数据表中,都有gmt_creategmt_modified这2个字段(是在阿里的开发规范上明确要求的),这2个字段的值是有固定规则的,例如gmt_create的值就是INSERT这条数据时的时间,而gmt_modified的值就是每次执行UPDATE时更新的时间,由于这是固定的做法,可以使用Mybatis拦截器进行处理,即每次执行SQL语句之前,先判断是否为INSERT / UPDATE类型的SQL语句,如果是,再判断SQL语句是否处理了相关时间,如果没有,则修改原SQL语句,得到处理了相关时间的新SQL语句,并放行,使之最终执行的是修改后的SQL语句。

关于此拦截器的示例:

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;

/**
 * <p>基于MyBatis的自动更新"最后修改时间"的拦截器</p>
 *
 * <p>需要SQL语法预编译之前进行拦截,则拦截类型为StatementHandler,拦截方法是prepare</p>
 *
 * <p>具体的拦截处理由内部的intercept()方法实现</p>
 *
 * <p>注意:由于仅适用于当前项目,并不具备范用性,所以:</p>
 *
 * <ul>
 * <li>拦截所有的update方法(根据SQL语句以update前缀进行判定),无法不拦截某些update方法</li>
 * <li>所有数据表中"最后修改时间"的字段名必须一致,由本拦截器的FIELD_MODIFIED属性进行设置</li>
 * </ul>
 *
 * @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) {
        // 无须执行操作
    }

    /**
     * <p>获取BoundSql对象,此部分代码相对固定</p>
     *
     * <p>注意:根据拦截类型不同,获取BoundSql的步骤并不相同,此处并未穷举所有方式!</p>
     *
     * @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();
    }

    /**
     * <p>通过反射,设置某个对象的某个属性的值</p>
     *
     * @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);
    }
}

关于Spring MVC框架

Spring MVC框架主要解决了接收请求、响应结果及相关的问题。

相关的问题:接收请求参数、转换响应结果、统一处理异常等。

关于Spring MVC框架的核心执行流程,如下图所示:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-V9wr3Lw7-1667125528172)(file://C:\Users\lenovo\Desktop\第四阶段\doc\note\images\DAY19\image-20221019114229471.png)]

注意:以上示例图描述是“非响应正文”的处理流程!

Spring MVC框架的5个核心组件:

  • DispatcherServlet:也称之为“前端控制器”,用于在Spring MVC框架接收所有来自客户端的请求,并进行分发、组织整个处理流程,此组件将由配置文件进行处理,在Spring Boot项目中,是自动配置的
  • HandlerMapping:记录了请求路径与处理请求的控制器(方法)的对应关系,在开发实践时,使用@RequestMapping系列注解配置的请求路径,本质就是在向HandlerMapping中添加映射关系
  • Controller:实际处理请求的组件,是由开发人员自行定义的
    • 注意:如果设计的控制器处理请求的方法是响应正文的,当Controller组件执行结束后,就会开始向客户端响应数据,不会执行以上示例图中剩余的流程
  • ModelAndView:是Controller处理请求后返回的对象,此对象封装了Controller处理请求后的数据和显示这些数据所使用到的视图组件的名称
  • ViewResolver:也称之为“视图解析器”,可以根据“视图组件的名称”来决定具体使用的视图组件

Spring AOP

AOP:面向切面的编程,实现了横切关注的相关问题,通常是许多不同的数据处理流程中都需要解决的共同的问题。

目前,项目中处理数据的流程大致是:

添加品牌:请求 -----> Filter -----> Controller -----> Service -----> Mapper -----> DB

添加相册:请求 -----> Filter -----> Controller -----> Service -----> Mapper -----> DB

删除类别:请求 -----> Filter -----> Controller -----> Service -----> Mapper -----> DB

编辑商品:请求 -----> Filter -----> Controller -----> Service -----> Mapper -----> DB

可能在处理过程中,虽然处理请求不同,但是需要执行一些高度相似甚至完全相同的代码,某些特定的执行时间节点可以通过一些特殊的组件来完成,例如Java EE中的Filter、Spring MVC的Interceptor、Mybatis的Interceptor,但是,这些特殊的组件只能在特定的时间节点执行,例如Filter是在服务器接收到请求的第一时间就已经执行,Spring MVC的Interceprot是在Controller的前后执行,Mybatis的Intercepror是在处理SQL语句时执行,如果需要在其它执行时间节点处理相同的任务,这些组件都是不可用的!

使用AOP通常解决以下类型的问题:安全、事务管理、日志等等。

AOP技术本身并不是Spring特有的技术,只是Spring很好的支持了AOP。

假设需要实现:统计每个业务方法的执行耗时。

首先,需要添加依赖项:

<!-- Spring AOP,用于实现AOP编程 -->
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-aop</artifactId>
</dependency>

然后,创建用于统计业务执行耗时的切面类,这种切面类本身是个组件类,并且需要添加@Aspect注解,则在根包下创建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;

/**
 * 统计业务方法执行耗时的切面类
 * 
 * @author java@tedu.cn
 * @version 0.0.1
 */
@Slf4j
@Aspect
@Component
public class TimerAspect {

    // 在AOP中,有多种Advice(通知)
    // @Around:包裹,可以实现在连接点之前和之后均自定义代码
    // @Before:在连接点之前执行
    // @After:在连接点之后执行,无论是正常返回还是抛出异常都会执行
    // @AfterReturning:在连接点返回之后执行
    // @AfterThrowing:在连接点抛出异常之后执行
    // 仅当使用@Around时,方法才可以自行处理ProceedingJoinPointer
    // 各Advice的执行大概是:
    // @Around
    // try {
    //   @Before
    //   连接点方法
    //   @AfterReturning
    // } catch (Throwable e) {
    //   @AfterThrowing
    // } finally {
    //   @After
    // }
    // @Around
    // ---------------------------------------------------
    // 关于ProceedingJoinPoint
    // 必须调用proceed()方法,表示执行表达式匹配到的方法
    // 调用proceed()方法必须获取返回值,且作为当前方法的返回值,表示返回表达式匹配的方法的返回值
    // 调用proceed()方法时的异常必须抛出,不可以使用try...catch进行捕获并处理
    // ---------------------------------------------------
    // 关于execution表达式:用于匹配在何时执行AOP相关代码
    // 表达式中的星号:匹配任意内容,只能匹配1次
    // 表达式中的2个连续的小数点:匹配任意内容,可以匹配0~n次,只能用于包名和参数列表部分
    // 表达式中的包是根包,会自动包含其子孙包中的匹配项
    @Around("execution(* cn.tedu.csmall.product.service.*.*(..))")
    //                 ↑ 无论方法的返回值类型是什么
    //                                                  ↑ 无论是哪个类
    //                                                     ↑ 无论是哪个方法
    //                                                       ↑ 2个小数点表示任何参数列表
    public Object timer(ProceedingJoinPoint pjp) throws Throwable {
        log.debug("执行了TimerAspect中的方法……");

        long start = System.currentTimeMillis();
        Object result = pjp.proceed(); // 执行连接点方法,获取返回结果
        long end = System.currentTimeMillis();

        log.debug("【{}】类型的对象调用了【{}】方法,方法的参数值为【{}】",
                pjp.getTarget().getClass().getName(),
                pjp.getSignature().getName(),
                pjp.getArgs());
        log.debug("执行耗时:{}毫秒", end - start);

        return result; // 返回调用pjp.proceed()时的结果
    }

}

切面是无侵入性的,在不修改任何其它类的任何代码的情况下,就可以作用于整个数据处理过程!

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

专注摸鱼的汪

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值