• 项目实战(计划任务,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");
        }
    
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22

    另外,在根包下创建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("处理缓存的计划任务执行完成!");
        }
    
    }
    
    • 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

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

    /**
     * 重建品牌数据缓存
     */
    void rebuildCache();
    
    • 1
    • 2
    • 3
    • 4

    并在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);
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17

    需要注意,对于周期性的计划任务,首次执行是在项目即将完成启动时,所以,也可以实现类似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;
    
    /**
     * 

    基于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; } }
    • 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
    • 98
    • 99
    • 100
    • 101
    • 102
    • 103
    • 104
    • 105
    • 106
    • 107
    • 108
    • 109
    • 110
    • 111
    • 112
    • 113
    • 114
    • 115
    • 116
    • 117
    • 118
    • 119
    • 120
    • 121
    • 122
    • 123
    • 124
    • 125
    • 126
    • 127
    • 128
    • 129
    • 130
    • 131
    • 132
    • 133
    • 134
    • 135
    • 136
    • 137
    • 138
    • 139
    • 140
    • 141
    • 142
    • 143
    • 144
    • 145
    • 146
    • 147
    • 148
    • 149
    • 150
    • 151
    • 152
    • 153
    • 154
    • 155
    • 156
    • 157
    • 158
    • 159
    • 160
    • 161
    • 162
    • 163
    • 164
    • 165
    • 166
    • 167
    • 168
    • 169
    • 170
    • 171
    • 172
    • 173
    • 174
    • 175
    • 176
    • 177
    • 178
    • 179
    • 180
    • 181
    • 182
    • 183
    • 184
    • 185
    • 186
    • 187
    • 188
    • 189
    • 190
    • 191
    • 192
    • 193
    • 194
    • 195
    • 196
    • 197
    • 198
    • 199
    • 200
    • 201
    • 202
    • 203
    • 204
    • 205
    • 206
    • 207
    • 208
    • 209
    • 210
    • 211
    • 212
    • 213
    • 214
    • 215
    • 216
    • 217
    • 218
    • 219
    • 220
    • 221
    • 222
    • 223
    • 224
    • 225
    • 226
    • 227
    • 228
    • 229
    • 230
    • 231
    • 232
    • 233
    • 234
    • 235
    • 236
    • 237
    • 238
    • 239
    • 240
    • 241
    • 242
    • 243
    • 244
    • 245
    • 246
    • 247
    • 248
    • 249
    • 250
    • 251
    • 252
    • 253
    • 254
    • 255
    • 256
    • 257
    • 258
    • 259
    • 260
    • 261
    • 262
    • 263
    • 264
    • 265
    • 266
    • 267
    • 268
    • 269
    • 270
    • 271
    • 272
    • 273

    Mybatis拦截器必须注册后才能生效!可以在配置类(或任何组件类)中:

    @Autowired
    private List<SqlSessionFactory> sqlSessionFactoryList;
    
    @PostConstruct // 使得此方法在调用了构造方法、完成了属性注入之后自动执行
    public void addInterceptor() {
        InsertUpdateTimeInterceptor interceptor = new InsertUpdateTimeInterceptor();
        for (SqlSessionFactory sqlSessionFactory : sqlSessionFactoryList) {
            sqlSessionFactory.getConfiguration().addInterceptor(interceptor);
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10

    关于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
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7

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

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

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

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

    首先,需要添加依赖项:

    
    <dependency>
        <groupId>org.springframework.bootgroupId>
        <artifactId>spring-boot-starter-aopartifactId>
    dependency>
    
    • 1
    • 2
    • 3
    • 4
    • 5

    然后,创建用于统计业务执行耗时的切面类,这种切面类本身是个组件类,并且需要添加@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()时的结果
        }
    
    }
    
    • 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

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

  • 相关阅读:
    Python读写文件代码
    DSPE-PEG-iRGD,iRGD-PEG-DSPE,磷脂-聚乙二醇-靶向肽iRGD,一种磷脂PEG肽
    java教师科研成果管理系统
    Selling Partner API Document
    记一次pdjs时安装glob出现,npm ERR! code ETARGET和npm ERR! code ELIFECYCLE
    【矩阵论】正规方程——生成子空间
    《牛客刷verilog》Part II Verilog进阶挑战
    epoll实现多路IO转接
    3ds MAX 2024版资源包下载分享 3ds Max三维建模软件资源包下载安装
    说说对Redux中间件的理解?常用的中间件有哪些?实现原理?
  • 原文地址:https://blog.csdn.net/weixin_43121885/article/details/127602845