业务描述

  • 用户行为日志系统主要是实现对用户行为日志(例如谁在什么时间点执行了什么操作, 访问了哪些方法,传递的什么参数,执行时长等)进行记录、查询、删除等操作,其表设计语 句如下:
  • log.sql

    1. CREATE TABLE `sys_logs` (
    2. `id` bigint(20) NOT NULL AUTO_INCREMENT,
    3. `username` varchar(50) DEFAULT NULL COMMENT '用户名',
    4. `operation` varchar(50) DEFAULT NULL COMMENT '业务操作',
    5. `method` varchar(200) DEFAULT NULL COMMENT '请求方法',
    6. `params` varchar(5000) DEFAULT NULL COMMENT '请求参数',
    7. `time` bigint(20) NOT NULL COMMENT '执行时长(毫秒)',
    8. `ip` varchar(64) DEFAULT NULL COMMENT 'IP 地址',
    9. `status` Integer(1) DEFAULT 1 COMMENT '1 正常,0 异常',
    10. `error` varchar(2000) DEFAULT NULL COMMENT '错误信息',
    11. `createdTime` datetime DEFAULT NULL COMMENT '创建时间',
    12. PRIMARY KEY (`id`)
    13. ) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8 COMMENT='系统
    14. 日志';
  • 导入数据库

image.png

系统原型设计

image.png

  • 业务

    • 添加日志到数据
    • 删除多条数据
    • 基于ID查询数据
    • 基于条件的查询操作

      AOP

      简介

  • AOP(Aspect Orient Programming)是一种设计思想,是软件设计领域中的面向切 面编程,它是面向对象编程(OOP)的一种补充和完善。实际项目中我们通常将面向对象理解 为一个静态过程(例如一个系统有多少个模块,一个模块有哪些对象,对象有哪些属性),面向切面理解为一个动态过程(在对象运行时动态织入一些扩展功能或控制对象执行)

    常见应用

  • 在程序设计中,遵循开放封闭原则,即对修改关闭,不能对已经上线使用的部分进行修改,对扩展开放,可以对程序功能进行扩展。而AOP就是对程序功能扩展的主要方式

实现原理

  • 在对业务模块进行扩展时,考虑到OCP开放封闭原则,以下两种方式
    • 采用为接口创建实现类,而实现类中采用has a的关系耦合了XxxServiceImpl,实现了业务的扩展
      • 借助 JDK 官方 API 为目标对象类型(实现类)创建其兄弟类型对象(代理对象),但是目标对象类型需要实现相应接口
    • 采用继承XxxServiceImpl的方式,重写了业务方法进行扩展
      • 借助 CGLIB 库为目标对象类型创建其子类类型对象(代理对象),但是目标对象类型不能使用 final 修饰
    • 均由Spring框架提供

image.png

相关术语分析

  • 切面(aspect): 横切面对象
  • 切入点(pointcut):定义了切入扩展业务逻辑的位置
    • 定义了标记目标方法的注解—切入点,被添加了该注解(切入点)的方法就是切入点方法
  • 通知(Advice): 内部封装扩展业务逻辑的具体方法,在切面的某个特定位置上执行的动作(扩展功能),一个切面中可以有多个通知
    • 当切入点方法执行时,这些通知在它周围执行
  • 连接点(joinpoint):程序执行过程中,封装了某个正在执行的目标方法信息的对象 ,可以通过此对象获取具体的目标方法信息,甚至去调用目标方法
    • 封装了切入点方法信息的对象

Spring AOP 快速入门

  • 添加依赖
  • 基于此依赖 spring 可以整合 AspectJ 框架快速完成 AOP 的基本实现。AspectJ 是一个面向切面的框架,他定义了 AOP 的一些语法,有一个专门的字节码生成器来生成遵 守 java 规范的 class 文件

    <dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-aop</artifactId>
    </dependency>
    
  • 业务切面对象设计

    • 创建注解类型,应用于切入点表达式的定义
    • 创建切面对象,用于做日志业务增强

image.png

  • 图中@RequiredLog为切入点注解
  • 控制层里的handler耦合了AOP提供的代理对象,它会去执行切点对象里的通知,多个切入点按照优先级执行,多个实现类里的切入点方法按照调用顺序执行,每个实现类里的切入点方法只执行一次

  • 代理方式(CGlig代理和JDK代理)

    • 配置文件中修改
    • 优先选择CGlig代理,CGlig更加灵活且容易理解
      #CGLIB代理(AOP 默认true)
      #spring.aop.proxy-target-class=true
      #配置JDK代理(默认true为cglib代理)
      spring.aop.proxy-target-class=false
      
  • 定义注解 ```java package com.cy.hu.ruoyi.common.annotation;

@Retention(RetentionPolicy.RUNTIME) @Target(ElementType.METHOD) public @interface RequiredLog{ String operate(); }


- 定义切面对象
   - 切入点
   - 通知
```java
package com.cy.hu.ruoyi.sys.service.aspect;

import lombok.extern.slf4j.Slf4j;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Pointcut;
import org.springframework.stereotype.Component;

/**
 * Spring框架中由@Aspect注解描述的类型为切面类型,此切面类型中要有
 * 1)切入点的定义 (用于约束在哪些目标方法执行时,进行功能拓展)
 * 2)通知方法的定义(这样的方法中会封装要执行的扩展业务逻辑,例如日志记录)
 *
 * @Component 注解表示此对象交给Spring管理
 */
@Aspect
@Component
@Slf4j
public class SysLogAspect {
    /**
     * @Pointcut 注解用于定义切入点
     * @annotation(注解类全名) 为定义切入点的一种表达式,
     * 此表达式中 被注解 描述的方法为切入点方法,可以在目标业务执行时通过指定的通知方法进行功能增强
     */
    @Pointcut("@annotation(com.cy.hu.ruoyi.common.annotation.RequiredLog)")
    public void doLog(){}  //此方法只负责承载切入点,不需要写方法实现

    /**
     * @Around 注解描述的方法为一个用于执行拓展业务逻辑的方法,此方法中
     * 可以通过连接点对象(joinPoint)调用目标方法。
     * @param joinPoint 表示一个连接点对象,此对象封装了切入点方法信息(目标方法信息),
     *                  可以通过连接点对象调用切入点方法(目标方法)
     * @return 目标方法的返回结果。
     */
    @Around("doLog()")  //@Around("@annotation(com.cy.hu.ruoyi.common.annotation.RequiredLog)")
    public Object doLogAround(ProceedingJoinPoint joinPoint) throws Throwable {
        long t1 = System.currentTimeMillis();
        try {
            Object result = joinPoint.proceed();  //调用切入点方法
            long t2 = System.currentTimeMillis();
            log.info("此处添加扩展业务");  //其它通知也可以增加,但只有@Around通知才有返回值
            log.info("opertime {}",t2-t1);
            return result;  //此返回值会交给代理对象,代理对象传给handler里的调用方法
        }catch (Throwable e) {
            e.printStackTrace();
            long t3 = System.currentTimeMillis();
            log.info("exception {}",e.getMessage());
            log.info("operatime {}",t3-t1);
            throw e;
        }
    }
}
  • SysNoticeServiceImpl
    • 目标对象切入点方法

image.png

多个切面执行顺序与通知

  • 客户端调用controller层的方法
  • AOP在项目启动时会产生业务类的代理对象,业务的代理对象察觉到方法上的切入点注解时就会执行切面对象的通知方法,其中@Around注解的方法优先执行,@Before注解在目标方法前执行,然后是目标方法的执行,结果返回时,正常结束执行@AfterReturning注解的方法,异常结束执行@AfterThrowing注解的方法。@After不管正常结束还是出现了错误,都会执行,结果对象最后返回给前端控制器处理(并不能确定@After相比@AfterReturning或者@AfterThrowing执行的先后顺序)
  • 当多个切面作用于同一个目标对象方法时,这些切面会构建成一个切面链,类似过滤 器链、拦截器链,多个切面对象和单个执行顺序也是一样的,切入点方法(目标方法)在中间执行,且只是执行一次

image.png

  • Spring 框架 AOP 模块定义通知类型,有如下几种: ▪
    • @Around (优先级最高的通知,可以在目标方法执行之前,之后灵活进行业务拓展.)
    • @Before (目标方法执行之前调用)
    • @AfterReturning (目标方法正常结束时执行)
    • @AfterThrowing (目标方法异常结束时执行)
    • @After (目标方法结束时执行,正常结束和异常结束它都会执行)
  • 定义注解 ```java package com.cy.hu.ruoyi.common.annotation;

@Retention(RetentionPolicy.RUNTIME) @Target(ElementType.METHOD) public @interface RequiredTime { }


- 切面类
   - @Order(n),定义切面执行优先级,n越小,优先级越高
```java
package com.cy.hu.ruoyi.sys.service.aspect;

import lombok.extern.slf4j.Slf4j;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.*;
import org.springframework.core.annotation.Order;
import org.springframework.stereotype.Component;

/**
 * 通过此切面演示各种通知(Before,After,Around,AfterReturning,AfterThrowing)
 * 的执行时间点
 */

@Order(1) //定义切面执行优先级数字越小优先级越高
@Aspect
@Component
@Slf4j
public class SysTimeAspect {
    @Pointcut("@annotation(com.cy.hu.ruoyi.common.annotation.RequiredTime)")
    public void doTime(){}

    @Before("doTime()")
    public void doBefore(){
        log.info("before");
    }
    @After("doTime()")
    public void doAfter(){
        log.info("After");
    }
    @AfterReturning("doTime()")
    public void doAfterReturning(){
        log.info("AfterReturning");
    }
    @AfterThrowing("doTime()")
    public void doAfterThrowing(){
        log.info("AfterThrowing");
    }

    @Around("doTime()")
    public Object doAround(ProceedingJoinPoint joinPoint) throws Throwable {
        log.info("Around.Before");
        try {
            Object result = joinPoint.proceed(); //调用本类其它通知方法,后续其它切面的通知方法,最后目标业务方法
            log.info("Around.AfterReturning");
            return result;
        }catch (Throwable e){
            log.info("Around.AfterThrowing");
            throw e;
        }finally {
            log.info("Around.After");
        }
    }
}
  • SysNoticeServiceImpl
    • 目标对象切入点方法

image.png

  • 结果

image.png

操作日志的实现

日志业务 API 设计

image.png

操作原理

  • 通过AOP的连接点proceedingjoinpoint、反射等技术操作日志

    • 相当于截取了用户的操作

      操作步骤

  • 日志实体类 ```java package com.cy.hu.ruoyi.sys.pojo; import lombok.Data; import java.util.Date;

@Data public class SysLog { private Integer id; private String ip; private String username; private String operation; private String method; private String params; private Long time; private Integer status=1; private String error; private Date createdTime; }


- Dao数据层
   - 接口
```java
package com.cy.hu.ruoyi.sys.dao;
import com.cy.hu.ruoyi.sys.pojo.SysLog;
import org.apache.ibatis.annotations.Mapper;
import java.util.List;

@Mapper
public interface SysLogDao {

    int insertLog(SysLog entity);
    int deleteById(Long... ids);
    SysLog selectById(Long id);
    List<SysLog> selectSysLogs(SysLog sysLog);
}
  • mapper映射文件

    <?xml version="1.0" encoding="UTF-8" ?>
    <!DOCTYPE mapper
       PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
       "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
    <mapper namespace="com.cy.hu.ruoyi.sys.dao.SysLogDao">
    <!--sql标签提取共性代码-->
    <sql id="selectColumns">
       select id,username,ip,operation,method,params,status,error,time,createdTime
       from sys_logs
    </sql>
    <insert id="insertLog">
       insert into sys_logs
       (username,ip,operation,method,params,
        time,status,error,createdTime)
       values
       (#{username},#{ip},#{operation},#{method},#{params},
       #{time},#{status},#{error},#{createdTime})
    </insert>
    
    <delete id="deleteById">
       delete from sys_logs
       <where>
           <!--另一种写法,if else-->
           <choose>
               <when test="ids!=null and ids.length>0">
                   id in
                   <foreach collection="ids" open="(" close=")" separator="," item="id">
                       #{id}
                   </foreach>
               </when>
               <otherwise>
                   1=2
               </otherwise>
           </choose>
       </where>
    </delete>
    
    <select id="selectById" resultType="com.cy.hu.ruoyi.sys.pojo.SysLog">
       <!--引用共性代码-->
       <include refid="selectColumns"/>
       <where>
           id=#{id}
       </where>
    </select>
    
    <select id="selectSysLogs" resultType="com.cy.hu.ruoyi.sys.pojo.SysLog">
       <!--引用共性代码-->
       <include refid="selectColumns"/>
       <where>
           <if test="status!=null">
               status=#{status}
           </if>
           <if test="operation!=null and operation!=''">
               and operation like concat("%",#{operation},"%")
           </if>
           <if test="username!=null and username!=''">
               and username like concat("%",#{username},"%")
           </if>
       </where>
       order by createdTime desc
    </select>
    </mapper>
    
  • 切面截取用户操作生成日志(关键)

image.png

package com.cy.hu.ruoyi.sys.service.aspect;

/**
 * Spring框架中由@Aspect注解描述的类型为切面类型,此切面类型中要有
 * 1)切入点的定义 (用于约束在哪些目标方法执行时,进行功能拓展)
 * 2)通知方法的定义(这样的方法中会封装要执行的扩展业务逻辑,例如日志记录)
 *
 * @Component 注解表示此对象交给Spring管理
 */
@Order(2)
@Aspect
@Component
@Slf4j
public class SysLogAspect {
    /**
     * @Pointcut 注解用于定义切入点
     * @annotation(注解类全名) 为定义切入点的一种表达式,
     * 此表达式中 被注解 描述的方法为切入点方法,可以在目标业务执行时通过指定的通知方法进行功能增强
     */
    @Pointcut("@annotation(com.cy.hu.ruoyi.common.annotation.RequiredLog)")
    public void doLog(){}  //此方法只负责承载切入点,不需要写方法实现

    /**
     * @Around 注解描述的方法为一个用于执行拓展业务逻辑的方法,此方法中
     * 可以通过连接点对象(joinPoint)调用目标方法。
     * @param joinPoint 表示一个连接点对象,此对象封装了切入点方法信息(目标方法信息),
     *                  可以通过连接点对象调用切入点方法(目标方法)
     * @return 目标方法的返回结果。
     */
    @Around("doLog()")  //合并切入点定义,也可以直接写@Around("@annotation(com.cy.hu.ruoyi.common.annotation.RequiredLog)")
    public Object doLogAround(ProceedingJoinPoint joinPoint) throws Throwable {
        long t1 = System.currentTimeMillis();
        try {
            Object result = joinPoint.proceed();
            long t2 = System.currentTimeMillis();
            //记录详细的正常行为日志
            doLogInfo(joinPoint,t2-t1,null);
            return result; //此返回值会交给代理对象,代理对象传给调用方法
        }catch (Throwable e) {
            e.printStackTrace();
            long t3 = System.currentTimeMillis();
            //记录详细的异常行为日志
            doLogInfo(joinPoint,t3-t1,e);
            throw e;
        }
    }
    //记录详细的用户操作日志
    private void doLogInfo(ProceedingJoinPoint joinPoint,long time,Throwable e) throws Exception {
        //1.获取日志
        //1.1获取登录用户名 (还没做登录时,先给假数据)
        String username = "iu";
        //1.2获取ip(目前不会获取ip地址时先给假数据)
        String ip = "202.106.29.20";
        //1.3获取操作信息(切入点方法上RequiredLog注解中operation属性值)
        //1.3.1获取目标对象对应的字节码对象
        Class<?> targetCls = joinPoint.getTarget().getClass();
        //1.3.2获取目标方法对象
        //1.3.2.1 获取方法签名信息
        Signature signature=joinPoint.getSignature();
        MethodSignature ms= (MethodSignature)signature;
        //1.3.2.2通过字节码对象以及方法信息获取目标方法对象
        Method method = targetCls.getMethod(ms.getName(), ms.getParameterTypes());
        //1.3.3 获取方法上的requiredLog注解
        RequiredLog requiredLog = method.getAnnotation(RequiredLog.class);
        //1.3.4 获取注解中operation属性的值
        String operate =  requiredLog.operate();
        //log.info("operation {}",operate);
        //1.4获取目标方法信息(切入点方法的信息(方法所在的类+方法名))
        String targetClsMethod = targetCls.getName()+"."+method.getName();
        //1.5获取方法参数(执行方法时传入实际参数)
        //1.5.1获取实际参数值
        Object[] args = joinPoint.getArgs();
        //1.5.2将参数值转换为json格式字符串(一种常用的数据格式)
        String jsonParamStr = new ObjectMapper().writeValueAsString(args);
        //2.封装日志
        SysLog userLog = new SysLog();
        userLog.setUsername(username);
        userLog.setIp(ip);
        userLog.setOperation(operate);
        userLog.setMethod(targetClsMethod);
        userLog.setParams(jsonParamStr);
        if (e!=null){
            userLog.setStatus(0); //操作状态
            userLog.setError(e.getMessage()); //错误信息
        }
        userLog.setTime(time);
        userLog.setCreatedTime(new Date());
        log.info("userLog {}",new ObjectMapper().writeValueAsString(userLog));
        //此处的用户操作数据库的线程和执行写入日志业务的线程是同一个
        log.info("ThreadName {}",Thread.currentThread().getName());
        //3.记录日志
        sysLogService.saveLog(userLog);
    }
    @Autowired
    private SysLogService sysLogService;
}
  • service
    • 接口 ```java package com.cy.hu.ruoyi.sys.service;

public interface SysLogService { void saveLog(SysLog sysLog); int deleteById(Long…ids); SysLog findById(Long id); List findLogs(SysLog sysLog); }


   - 实现类
```java
package com.cy.hu.ruoyi.sys.service.impl;

@Service
public class SysLogServiceImpl implements SysLogService {
    @Autowired
    private SysLogDao sysLogDao;

    @Override
    public void saveLog(SysLog sysLog) {
        sysLogDao.insertLog(sysLog);
    }

    @Override
    public int deleteById(Long... ids) {
        return sysLogDao.deleteById(ids);
    }

    @Override
    public SysLog findById(Long id) {
        return sysLogDao.selectById(id);
    }

    @Override
    public List<SysLog> findLogs(SysLog sysLog) {
        return sysLogDao.selectSysLogs(sysLog);
    }
}
  • controller层 ```java package com.cy.hu.ruoyi.sys.web.controller;

//restful 风格url(一种编码风格) //@GetMapping 所有—>/log/ //@GetMapping 某个—>/log/{id} //@DeleteMapping 某些—>/log/{id} //这种rest方式可以简化,直接根据请求方式和部分参数决定使用哪种方法,地址栏只需要写 请求主地址+参数值

@RestController @RequestMapping(“/log/“) public class SysLogController { @Autowired private SysLogService sysLogService;

@DeleteMapping("{ids}")
public JsonResult deleteById(@PathVariable Long... ids) {
    sysLogService.deleteById(ids);
    return new JsonResult("delete ok");
}

@GetMapping("{id}")
public JsonResult findById(@PathVariable Long id) {
    return new JsonResult(sysLogService.findById(id));
}

@GetMapping
public JsonResult findLogs(SysLog sysLog) {
    return new JsonResult(sysLogService.findLogs(sysLog));
}

}

<a name="8Lyz5"></a>
### 异步实现(线程池)

- 为什么要异步,用户请求对数据库数据操作的线程和写入日志的线程是同一个线程,导致主要任务(对数据库数据操作)的响应时间过长,用户体验下降
   - 并发小量时直接new一个线程
   - 并发大量时,建立和销毁线程耗时,所以采用线程池技术
      - 线程池构建参数解释
      - 使用框架内置线程池

- 并发量小时
   - 对切面类调用写日志方法使用新线程
```java
//3.记录日志
//并发比较小时,可以自己new 线程执行写日志这个耗时操作
new Thread(new Runnable() {
    @Override
    public void run() {
        sysLogService.saveLog(userLog);
    }
}).start();
//new Thread(()->sysLogService.saveLog(userLog)).start(); //lambda表达式也可以
  • 并发量大:线程池
    • jdk内置线程池,springboot里使用了它来构建自己的线程池

image.png

  • corePoolSize
    • 池中核心线程数
      • 当业务来了没达到核心线程数时,会新建线程来处理这个业务(即使有空闲线程也会去新建)
  • maximumPoolSize
    • 池中最大线程数
      • 见名知意
  • keepAliveTime
    • 线程的存活时间
  • unit
    • 线程存活时间单位
  • workQueue
    • 任务队列,存储要执行的任务
  • threadFactory
    • 创建线程的工厂
  • handler
    • 达到自己能处理的最大限度后还有任务过来的拒绝策略
      • 有抛出异常的拒绝策略
      • 有让调用者自己处理的拒绝策略
      • 有丢弃开头任务的拒绝策略
      • ……

image.png

package com.cy.java.api.thread;

import java.util.concurrent.*;
import java.util.concurrent.atomic.AtomicInteger;
//JUC

public class ThreadPoolTests {
    public static void main(String[] args) {
        int corePoolSize=2;
        int maximumPoolSize=3;
        int keepAliveTime=60;
        TimeUnit unit=TimeUnit.SECONDS;
        //任务队列,存储要执行的任务
        BlockingQueue<Runnable> workQueue=
                new ArrayBlockingQueue<>(1);
        //创建线程的工厂
        ThreadFactory threadFactory=new ThreadFactory() {
            private String prefix="db-service-thread-";
            private AtomicInteger atomicInteger=new AtomicInteger(0);
            @Override
            public Thread newThread(Runnable task) {
                return new Thread(task,prefix+atomicInteger.incrementAndGet());
            }
        };
        RejectedExecutionHandler handler=
          //new ThreadPoolExecutor.AbortPolicy();//不能执行任务时抛出异常
          new ThreadPoolExecutor.CallerRunsPolicy();//由调用者去执行
        //构建线程池对象
        ThreadPoolExecutor threadPool= new ThreadPoolExecutor(
                        corePoolSize,
                        maximumPoolSize,
                        keepAliveTime,
                        unit,
                        workQueue,
                        threadFactory,
                        handler);
        threadPool.execute(new Runnable() {//task
            @Override
            public void run() {
                String tName=Thread.currentThread().getName();
                System.out.println(tName+"->execute->task1");
                try{Thread.sleep(5000);}catch (Exception e){}
            }
        });
        threadPool.execute(new Runnable() {//task
            @Override
            public void run() {
                String tName=Thread.currentThread().getName();
                System.out.println(tName+"->execute->task2");
                try{Thread.sleep(5000);}catch (Exception e){}
            }
        });
        threadPool.execute(new Runnable() {//task
            @Override
            public void run() {
                String tName=Thread.currentThread().getName();
                System.out.println(tName+"->execute->task3");
            }
        });
        threadPool.execute(new Runnable() {//task
            @Override
            public void run() {
                String tName=Thread.currentThread().getName();
                System.out.println(tName+"->execute->task4");
                try{Thread.sleep(5000);}catch (Exception e){}
            }
        });
        threadPool.execute(new Runnable() {//task
            @Override
            public void run() {
                String tName=Thread.currentThread().getName();
                System.out.println(tName+"->execute->task5");
                try{Thread.sleep(5000);}catch (Exception e){}
            }
        });
    }
}
  • 使用springboot内置线程池
  • 添加线程池配置

    #核心线程数(当池中核心线程数小于这个值时,
    #每来一个任务都会创建一个新的线程,直到线程数达到core-size设置的值)
    spring.task.execution.pool.core-size=2
    #阻塞式队列容量(当所有核心线程都在忙,此时又来新的任务,任务会存储到队列)
    spring.task.execution.pool.queue-capacity=1
    #池中允许的最大线程数(当核心线程都在忙,任务队列也满了,此时再来新的任务会创建新的线程)
    spring.task.execution.pool.max-size=3
    #当并发高峰期过后,池中的线程空闲下来,假如一直没有新的任务,
    #则超出keep-alive属性设置的值,则释放线程
    spring.task.execution.pool.keep-alive=60000
    #配置线程名的前缀,设置的目的是了更好识别这些线程,出现问题便于调试
    spring.task.execution.thread-name-prefix=db-service-task-
    #是否允许核心线程被释放
    spring.task.execution.pool.allow-core-thread-timeout=false
    
  • 主程序类上添加注解@EnableAsync ```java //此注解用于告诉springboot,启动时初始化一个线程池(ThreadPoolExecutor) @EnableAsync @SpringBootApplication public class Application {

    public static void main(String[] args) {

      SpringApplication.run(Application.class, args);
    

    }

}


- @Async注解修饰的方法采用线程池提供的线程调用执行
```java
/**
     * @Async 注解描述的方法为一个异步切入点方法,
     * 这个方法会在切面通知方法中通过一个新的线程调用执行,
     * 这里的线程由springboot内置的线程池提供.
     * @param sysLog
     */
@Override
@Async
public void saveLog(SysLog sysLog) {
    System.out.println("Thread"+Thread.currentThread().getName());
    sysLogDao.insertLog(sysLog);
}

分页

  • 采用PageHelper实现
  • PageHelper是基于MyBatis框架实现的一个分页插件

  • PageHelper应用原理

    • PageHelper底层基于mybatis框架中的拦截器规范,做了一个分页拦截器的具体实现,假如我们调用了 PageHelper.startPage()方法,底层会在mybatis层面启动一拦截器,在拦截器中对sql查询进行拦截,拦截到 sql以后,首先查询表中记录条数,然后才根据页数和页面记录大小对sql语句进行limit分页处理,当页数为1时,则变成了select * from table limit ?

image.png

  • 操作步骤
  • 添加PageHelper依赖

    <dependency>
    <groupId>com.github.pagehelper</groupId>
    <artifactId>pagehelper-spring-boot-starter</artifactId>
    <version>1.3.0</version>
    </dependency>
    
  • 定义分页工具类

    package com.cy.hu.ruoyi.common.util;
    //StringUtil提供了对字符串是否为空串的逻辑判断
    public class StringUtil {
      public static boolean isEmpty(String str){
          return str==null || str.equals("");
      }
    }
    
    package com.cy.hu.ruoyi.common.util;
    import org.springframework.web.context.request.RequestContextHolder;
    import org.springframework.web.context.request.ServletRequestAttributes;
    import javax.servlet.http.HttpServletRequest;
    //,ServletUtil为获取Request对象,提供了遍历
    public class ServletUitl {
      /**通过RequestContextHolder类型获取请求属性*/
      public static ServletRequestAttributes getServletRequestAttributes(){
          return (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
      }
    
      /**获取请求对象*/
      public static HttpServletRequest getRequest(){
          return getServletRequestAttributes().getRequest();
      }
    }
    

    ```java package com.cy.hu.ruoyi.common.util; import com.github.pagehelper.Page; import com.github.pagehelper.PageHelper; import javax.servlet.http.HttpServletRequest; /PageUtil使我们自己写的一个工具类,在工具类可以获取请求对象,基于请求对象,获取请求中分页参 数,然后启动分页查询/ public class PageUtil { /* 通过此方法启动分页查询 @param 这里的T为泛型,返回值类型左侧有这种符号的表示方法为泛型方法 @return / public static Page startPage(){

      HttpServletRequest request = ServletUitl.getRequest();
      //页面大小(每页最多显示多少条记录)
      String pageSizeStr = request.getParameter("pageSize");
      //当前页码值(要查第几页的数据)
      String pageCurrentStr = request.getParameter("pageCurrent");
      Integer pageSize = StringUtil.isEmpty(pageSizeStr)?10:Integer.parseInt(pageSizeStr);
      Integer pageCurrent = StringUtil.isEmpty(pageCurrentStr)?1:Integer.parseInt(pageCurrentStr);
      //调用PageHelper中的startPage方法启动分页
      return PageHelper.startPage(pageCurrent,pageSize);
    

    }

}


- 在程序中可启动分页查询,例如控制逻辑层启动分页查询
```java
@GetMapping
public JsonResult findLogs(SysLog sysLog) { //参数需要new ISelect()对象
    return new JsonResult(PageUtil.startPage().doSelectPageInfo(new ISelect() {
        @Override
        public void doSelect() {
            sysLogService.findLogs(sysLog);
        }
    }));
}

image.png

  • 除了查到的分页后的数据以外,还有分页信息

image.png