4_Day17

102. 计划任务(续)

在根包下创建schedule.CacheSchedule类,在类上添加@Component注解,并在类中自定义计划任务方法:

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;

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

    // 关于@Schedule注解的参数配置
    // fixedRate:执行频率,将按照上一次开始执行的时间来计算下一次的执行时间,以毫秒值为单位
    // fixedDelay:执行间隔时间,即上次执行结束后再过多久执行下一次,以毫秒值为单位
    // cron:使用1个字符串,其中包括6~7个值,各值之间使用1个空格进行分隔
    // >> 在cron的字符串中各值依次表示:秒 分 时 日 月 周(星期) [年]
    // >> 以上各值都可以使用通配符
    // >> 使用星号(*)表示任意值
    // >> 使用问号(?)表示不关心具体值,问号只能用于“日”和“周(星期)”
    // >> 例如:"56 34 12 15 11 ? 2022"表示“2022年11月15日12:34:56,无视当天星期几”
    // >> 以上各值,可以使用“x/x”格式的值,例如:在分钟对应的位置设置“1/5”,则表示当分钟值为1时执行,且每间隔5分钟执行1次
    @Scheduled(fixedRate = 1 * 60 * 1000)
    public void rebuildBrandCache() {
        log.debug("开始执行【重建品牌缓存】计划任务…………");
        brandService.rebuildCache();
        log.debug("本次【重建品牌缓存】计划任务执行完成!");
    }

}

提示:以上计划任务需要在业务逻辑层补充“重建品牌缓存”的功能,在IBrandService中添加:

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

并在BrandServiceImpl中实现:

@Override
public void rebuildCache() {
    log.debug("开始处理【重建品牌缓存】的业务,无参数");
    log.debug("准备删除Redis缓存中的品牌数据……");
    brandRedisRepository.deleteAll();
    log.debug("删除Redis缓存中的品牌数据,完成!");

    log.debug("准备从数据库中读取品牌列表……");
    List<BrandListItemVO> list = brandMapper.list();
    log.debug("从数据库中读取品牌列表,完成!");

    log.debug("准备将品牌列表写入到Redis缓存……");
    brandRedisRepository.save(list);
    log.debug("将品牌列表写入到Redis缓存,完成!");

    log.debug("准备将各品牌详情写入到Redis缓存……");
    for (BrandListItemVO brandListItemVO : list) {
        Long id = brandListItemVO.getId();
        BrandStandardVO brandStandardVO = brandMapper.getStandardById(id);
        brandRedisRepository.save(brandStandardVO);
    }
    log.debug("将各品牌详情写入到Redis缓存,完成!");
}

103. 手动更新缓存

由于在业务逻辑层已经实现“重建品牌缓存”的功能,在控制器中添加处理请求的方法,即可实现手动更新缓存:

// http://localhost:9080/brands/cache/rebuild
@ApiOperation("重建品牌缓存")
@ApiOperationSupport(order = 600)
@PostMapping("/cache/rebuild")
public JsonResult<Void> rebuildCache() {
    log.debug("开始处理【重建品牌缓存】的请求,无参数");
    brandService.rebuildCache();
    return JsonResult.ok();
}

后续,客户端只需要提交请求,即可实现“重建品牌缓存”。

104. 按需加载缓存数据

假设当根据id获取品牌详情时,需要通过“按需加载缓存数据”的机制来实现缓存,可以将原业务调整为:

@Override
public BrandStandardVO getStandardById(Long id) {
    log.debug("开始处理【根据id查询品牌详情】的业务,参数:{}", id);
    // 根据id从缓存中获取数据
    log.debug("将从Redis中获取相关数据");
    BrandStandardVO brand = brandRedisRepository.get(id);
    // 判断获取到的结果是否不为null
    if (brand != null) {
        // 是:直接返回
        log.debug("命中缓存,即将返回:{}", brand);
        return brand;
    }

    // 无缓存数据,从数据库中查找数据
    log.debug("未命中缓存,即将从数据库中查找数据");
    brand = brandMapper.getStandardById(id);
    // 判断查询到的结果是否为null
    if (brand == null) {
        // 是:抛出异常
        String message = "获取品牌详情失败,尝试访问的数据不存在!";
        log.warn(message);
        throw new ServiceException(ServiceCode.ERR_NOT_FOUND, message);
    }

    // 将查询结果写入到缓存,并返回
    log.debug("从数据库查询到有效结果,将查询结果存入到Redis:{}", brand);
    brandRedisRepository.save(brand);
    log.debug("返回结果:{}", brand);
    return brand;
}

105. Spring AOP

AOP:面向切面的编程。

注意:AOP并不是Spring框架特有的技术,只是Spring框架很好的支持了AOP。

AOP主要用于:日志、安全、事务管理、自定义的业务规则

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

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

添加类别:请求 ------> Controller ------> Service ------> Mapper ------> DB

删除品牌:请求 ------> Controller ------> Service ------> Mapper ------> DB

其实,各请求提交到服务器端后,数据的处理流程是相对固定的!

假设存在某个需求,无论是添加品牌、添加类别、删除品牌,或其它请求的处理,都需要在Service组件中执行相同的任务,应该如何处理?

例如,假设需要实现“统计所有Service中的业务方法的执行耗时”,首先,需要添加依赖项:

<!-- Spring Boot AOP的依赖项 -->
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-aop</artifactId>
</dependency>

然后,在根包下创建aop.TimerAspect类,作为切面类,必须添加@Aspect注解,由于是通过Spring来实现AOP,所以,此类还应该交由Spring管理,它应该是个组件类,则再补充添加@Component注解,并在类中自定义方法,且通过注解来配置方法何时执行:

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 {

    public TimerAspect() {
        log.debug("创建切面对象:TimerAspect");
    }

    // 【AOP的核心概念】
    // 连接点(JoinPoint):数据处理过程中的某个时间节点,可能是调用了方法,或抛出了异常
    // 切入点(PointCut):选择一个或多个连接点的表达式
    // 通知(Advice):在选择到的连接点执行的代码
    // 切面(Aspect):是包含了切入点和通知的模块
    // ----------------------------------------------------------
    // 【通知注解】
    // @Before注解:表示“在……之前”,且方法应该是无参数的
    // @After注解:表示“在……之后”,无论是否抛出异常,或是否返回结果,都会执行,且方法应该是无参数的
    // @AfterReturning注解:表示“在返回结果之后”,且方法的参数是JoinPoint和返回值
    // @AfterThrowing注解:表示“在抛出异常之后”,且方法的参数是JoinPoint和异常对象
    // @Around注解:表示“包裹”,通常也称之为“环绕”,且方法的参数是ProceedingJoinPoint
    // ----------------------------------------------------------
    // @Around开始
    // try {
    //     @Before
    //     表达式匹配的方法
    //     @AfterReturning
    // } catch (Throwable e) {
    //     @AfterThrowing
    // } finally {
    //     @After
    // }
    // @Around结束
    // ----------------------------------------------------------
    // 注解中的execution内部配置表达式,以匹配上需要哪里执行切面代码
    // 表达式中,星号(*)是通配符,可匹配1次任意内容
    // 表达式中,2个连接的小数点(..)也是通配符,可匹配0~n次,只能用于包名和参数列表
    @Around("execution(* cn.tedu.csmall.product.service.*.*(..))")
    //                 ↑ 此星号表示需要匹配的方法的返回值类型
    //                   ↑ ---------- 根包 ----------- ↑
    //                                                  ↑ 类名
    //                                                    ↑ 方法名
    //                                                      ↑↑ 参数列表
    public Object timer(ProceedingJoinPoint pjp) throws Throwable {
        log.debug("执行了TimeAspect中的方法……");

        log.debug("【{}】类型的对象调用了【{}】方法,参数值为【{}】",
                pjp.getTarget().getClass().getName(),
                pjp.getSignature().getName(),
                pjp.getArgs());

        long start = System.currentTimeMillis();
        // 注意:必须获取调用此方法的返回值,作为当前切面方法的返回值
        // 注意:必须抛出调用此方法的异常,不可以使用try...catch捕获并处理
        Object result = pjp.proceed(); // 相当于执行了匹配的方法,即业务方法
        long end = System.currentTimeMillis();

        log.debug("执行耗时:{}毫秒", end - start);

        return result;
    }

}
posted @ 2023-03-15 17:05  富兰克林_YY  阅读(168)  评论(0编辑  收藏  举报