效果图

建好数据库即可,表不存在会自动创建,别说什么高并发会怎么怎么样了,高并发不用搜索引擎,还想用数据库记录日志就是疯了

java异步查询设计 java异步记录操作日志_java


下面第一条是异常信息,第二条是正常的数据返回。

java异步查询设计 java异步记录操作日志_java_02

LogAspect.java

日志切面

import com.fasterxml.jackson.databind.ObjectMapper;
import com.fu.work.entity.Log;
import com.fu.work.service.LogService;
import lombok.extern.slf4j.Slf4j;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.*;
import org.springframework.stereotype.Component;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;

import javax.annotation.Resource;
import javax.servlet.http.HttpServletRequest;
import java.util.Date;
import java.util.HashMap;
import java.util.Map;

/**
 * 日志切面
 */
@Slf4j
@Aspect
@Component
public class LogAspect {
    @Resource
    private LogService logService;
	
	//把fu替换成你自己的包即可
    @Pointcut("execution(* com.fu.*.controller.*Controller.*(..))||execution(* com.fu.*.*.controller.*Controller.*(..))||execution(* com.fu.*.*.*.controller.*Controller.*(..))")
    public void logOperation() {
    }

    /**
     * 抛出异常时执行(Around和AfterThrowing互斥,只会有一个执行)
     */
    @AfterThrowing(pointcut = "logOperation()", throwing = "e")
    public void afterThrowing(Exception e){
        try {
            ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
            assert attributes != null;
            HttpServletRequest request = attributes.getRequest();
            Map<String, String[]> parametersMap = request.getParameterMap();
            Map<String, Object> parameters = new HashMap<>();
            if (parametersMap != null) {
                parametersMap.forEach((key, value) -> {
                    parameters.put(key, value[0]);
                });
            }
            ObjectMapper om = new ObjectMapper();

            Log logObj = new Log();
            logObj.setUserId("userId");//通过token获取
            logObj.setUsername("Meta");//通过token获取
            logObj.setApplicationName("应用名称");
            logObj.setCode(500);//状态码
            logObj.setMethod(request.getMethod());//请求方法
            logObj.setRequestURI(request.getRequestURI());//请求URI
            logObj.setRequestData(String.valueOf(parameters));//请求数据
            logObj.setMsg(r.getMsg());//返回信息
            logObj.setErrorMsg(om.writeValueAsString(e));//错误信息
            logObj.setCreateTime(new Date());//创建时间
            //异步入库
            logService.insert(logObj);
        } catch (Exception ex) {
            log.error("日志入库异常:", ex);
        }
    }

    /**
     * 环绕是最后执行的(Around和AfterThrowing互斥,只会有一个执行)
     */
    @Around("logOperation()")
    public Object doAround(ProceedingJoinPoint point) throws Throwable {
        Object result = point.proceed();//执行切点
        try {
            ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
            assert attributes != null;
            HttpServletRequest request = attributes.getRequest();
            Map<String, String[]> parametersMap = request.getParameterMap();
            Map<String, Object> parameters = new HashMap<>();
            if (parametersMap != null) {
                parametersMap.forEach((key, value) -> {
                    parameters.put(key, value[0]);
                });
            }

            Log logObj = new Log();
            logObj.setUserId("userId");//通过token获取
            logObj.setUsername("Meta");//通过token获取
            logObj.setApplicationName("应用名称");
            logObj.setCode(200);//状态码
            logObj.setMethod(request.getMethod());//请求方法
            logObj.setRequestURI(request.getRequestURI());//请求URI
            logObj.setRequestData(String.valueOf(parameters));//请求数据
            logObj.setRespondData(String.valueOf(result));//返回数据
            logObj.setMsg(r.getMsg());//返回信息
            logObj.setCreateTime(new Date());//创建时间
            //异步入库
            log.info("打印当前A线程名称:{}", Thread.currentThread().getName());
            logService.insert(logObj);
        } catch (Exception ex) {
            log.error("日志入库异常:", ex);
        }
        return result;
    }

}

AsyncConfig.java

异步方法配置需要在@SpringBootApplication注解上方加上@EnableAsync注解开启异步线程

import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor;

import java.util.concurrent.Executor;
import java.util.concurrent.ThreadPoolExecutor;

/**
 * 异步线程配置:确保执行方法和异步执行的方法不在同一个类
 */
@Configuration
public class AsyncConfig {
    @Value("${spring.task.execution.pool.core-size}")
    private int corePoolSize;
    @Value("${spring.task.execution.pool.max-size}")
    private int maxPoolSize;
    @Value("${spring.task.execution.pool.queue-capacity}")
    private int queueCapacity;
    @Value("${spring.task.execution.thread-name-prefix}")
    private String threadNamePrefix;
    @Value("${spring.task.execution.pool.keep-alive}")
    private int keepAliveSeconds;

    @Bean("logAsync")
    public Executor logAsync() {
        ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
        executor.setCorePoolSize(corePoolSize);
        executor.setMaxPoolSize(maxPoolSize);
        executor.setQueueCapacity(queueCapacity);
        executor.setThreadNamePrefix(threadNamePrefix);
        executor.setKeepAliveSeconds(keepAliveSeconds);
        /*
          拒绝处理策略
          CallerRunsPolicy():交由调用方线程运行,比如 main 线程。
          AbortPolicy():直接抛出异常。
          DiscardPolicy():直接丢弃。
          DiscardOldestPolicy():丢弃队列中最老的任务。
         */
        executor.setRejectedExecutionHandler(new ThreadPoolExecutor.CallerRunsPolicy());
        executor.initialize();
        return executor;
    }
}

LogService.java

@Slf4j
@Service
public class LogService {

    @Resource
    private LogDao logDao;
	/**
     * 异步日志入库
     */
    @Async("logAsync")
    public void insert(Log logObj) {
        log.info("打印当前B线程名称:{}",Thread.currentThread().getName());
        //获取当前年月
        String tableName = getYearMonth(0);
        try {
            logDao.insert(tableName, logObj);
        } catch (BadSqlGrammarException e) {
            log.error("入库异常:", e);
            //如果是不存在表,则创建表
            if (1146 == e.getSQLException().getErrorCode()) {
                //判断下个月的日志表是否存在,如果不存在则创建
                logDao.crateTable(getYearMonth(0));
                //再次入库
                logDao.insert(tableName, logObj);
            }
        } catch (Exception e) {
            log.error("未知异常:", e);
        }
    }
    //自定义方法==============================================================================================================
    /**
     * 获取年月
     *
     * @param addOrReduceMonth 正数表示后几个月,负数表示前几个月,默认0,表示当前月
     */
    public static String getYearMonth(int addOrReduceMonth) {
        //获取当前月份的表
        Calendar cal = Calendar.getInstance();
        cal.add(Calendar.MONTH, addOrReduceMonth);
        SimpleDateFormat dft = new SimpleDateFormat("yyyyMM");
        return dft.format(cal.getTime());
    }
}

LogDao.java

@Mapper
public interface LogDao{
	/**
     * 创建日志表
     */
    int crateTable(@Param("tableName") String tableName);
    /**
     * 写入日志
     */
    int insert(@Param("tableName") String tableName,@Param("log") Log log);
}

LogMapper.xml

<resultMap type="com.fu.work.entity.Log" id="BaseResultMap">
        <result property="id" column="id" jdbcType="BIGINT"/>
        <result property="userId" column="user_id" jdbcType="VARCHAR"/>
        <result property="username" column="username" jdbcType="VARCHAR"/>
        <result property="applicationName" column="application_name" jdbcType="VARCHAR"/>
        <result property="code" column="code" jdbcType="SMALLINT"/>
        <result property="method" column="method" jdbcType="VARCHAR"/>
        <result property="requestURI" column="request_uri" jdbcType="VARCHAR"/>
        <result property="requestData" column="request_data" jdbcType="VARCHAR"/>
        <result property="respondData" column="respond_data" jdbcType="VARCHAR"/>
        <result property="msg" column="msg" jdbcType="VARCHAR"/>
        <result property="errorMsg" column="error_msg" jdbcType="VARCHAR"/>
        <result property="createTime" column="create_time" jdbcType="TIMESTAMP"/>
    </resultMap>

    <sql id="Base_Column_List">id, user_id, username, application_name, code, `method`, request_uri, request_data, respond_data, msg, error_msg, create_time</sql>

    <!-- 创建日志表 -->
    <update id="crateTable" parameterType="String">
        CREATE TABLE IF NOT EXISTS log_${tableName} (
            `id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT '主键ID',
            `user_id` char(36) DEFAULT NULL COMMENT '用户ID',
            `username` varchar(64) DEFAULT NULL COMMENT '用户名',
            `application_name` varchar(255) DEFAULT NULL COMMENT '应用名称',
            `code` smallint(6) DEFAULT NULL COMMENT '状态码',
            `method` varchar(16) DEFAULT NULL COMMENT '请求方法',
            `request_uri` varchar(2048) DEFAULT NULL COMMENT '请求URI',
            `request_data` longtext DEFAULT NULL COMMENT '请求数据',
            `respond_data` longtext DEFAULT NULL COMMENT '返回数据',
            `msg` text DEFAULT NULL COMMENT '返回信息',
            `error_msg` longtext DEFAULT NULL COMMENT '详细错误信息',
            `create_time` datetime DEFAULT NULL COMMENT '创建时间',
            PRIMARY KEY (`id`),
            KEY `create_time` (`create_time`) USING BTREE COMMENT '创建时间索引',
            KEY `user_id` (`user_id`) COMMENT '用户ID索引',
            KEY `application_name` (`application_name`) COMMENT '应用名称索引'
            ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
    </update>

	<!-- 新增 -->
    <insert id="insert">
        insert into log_${tableName}
        <trim prefix="(" suffix=")" suffixOverrides=",">
            <if test="log.id != null">
                id,
            </if>
            <if test="log.userId != null">
                user_id,
            </if>
            <if test="log.username != null">
                username,
            </if>
            <if test="log.applicationName != null">
                application_name,
            </if>
            <if test="log.code != null">
                code,
            </if>
            <if test="log.method != null">
                `method`,
            </if>
            <if test="log.requestURI != null">
                request_uri,
            </if>
            <if test="log.requestData != null">
                request_data,
            </if>
            <if test="log.respondData != null">
                respond_data,
            </if>
            <if test="log.msg != null">
                msg,
            </if>
            <if test="log.errorMsg != null">
                error_msg,
            </if>
            <if test="log.createTime != null">
                create_time,
            </if>
        </trim>
        <trim prefix="values (" suffix=")" suffixOverrides=",">
            <if test="log.id != null">
                #{log.id},
            </if>
            <if test="log.userId != null">
                #{log.userId},
            </if>
            <if test="log.username != null">
                #{log.username},
            </if>
            <if test="log.applicationName != null">
                #{log.applicationName},
            </if>
            <if test="log.code != null">
                #{log.code},
            </if>
            <if test="log.method != null">
                #{log.method},
            </if>
            <if test="log.requestURI != null">
                #{log.requestURI},
            </if>
            <if test="log.requestData != null">
                #{log.requestData},
            </if>
            <if test="log.respondData != null">
                #{log.respondData},
            </if>
            <if test="log.msg != null">
                #{log.msg},
            </if>
            <if test="log.errorMsg != null">
                #{log.errorMsg},
            </if>
            <if test="log.createTime != null">
                #{log.createTime},
            </if>
        </trim>
    </insert>

Log.java

import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;

import java.util.Date;

@Data
@NoArgsConstructor
@AllArgsConstructor
public class Log {
    private Long id;
    private String userId;
    private String username;
    private String applicationName;
    private Integer code;
    private String method;
    private String requestURI;
    private String requestData;
    private String respondData;
    private String msg;
    private String errorMsg;
    private Date createTime;
}

application.yml

#忽略写入日志的URI
spring:
  jackson:
    date-format: yyyy-MM-dd hh:mm:ss
    time-zone: GMT+8
  task:
    execution:
      #异步线程名称
      thread-name-prefix: log-async
      pool:
        core-size: 8
        max-size: 16
        queue-capacity: 500
        keep-alive: 60

  datasource:
    #可以换成其它数据库驱动
    driver-class-name: org.mariadb.jdbc.Driver
    url: jdbc:mariadb://localhost:3306/db_log?serverTimezone=Asia/Shanghai&useUnicode=true&characterEncoding=utf-8&useSSL=false
    username: root
    password: 123456

mybatis:
  mapper-locations: classpath:mapper/*.xml

build.gradle(和maven的pom.xml差不多)

dependencies {//类似于maven的dependencies
    implementation 'org.springframework.boot:spring-boot-starter-web'//格式为groupId:artifactId:version
    compileOnly 'org.projectlombok:lombok'//仅编译时使用,不参与打包
    annotationProcessor 'org.projectlombok:lombok'
    implementation 'org.springframework.boot:spring-boot-starter-aop'
    annotationProcessor "org.springframework.boot:spring-boot-configuration-processor"
    runtimeClasspath 'org.mariadb.jdbc:mariadb-java-client'
    implementation 'org.mybatis.spring.boot:mybatis-spring-boot-starter:2.2.2'
}

注意事项

@Async异步不生效
1、标注@Async注解的方法必须是public void的(也就是公有的、无返回值的)
2、调用@Async注解的方法不建议在同一个方法内(即:A类调用B类的异步方法,AB不建议在同一个方法中,如需要在同一个类,需要手动获取代理)

调用方法和异步方法都在同一个类(手动获取代理)

import cn.hutool.extra.spring.SpringUtil;
import org.springframework.stereotype.Component;

@Component
public class myClass {
	//同类方法a调用异步方法b
	public String a(){
	System.out.println("主线程开始执行......");
	//手动获取代理,调用异步方法b
	SpringUtil.getApplicationContext().getBean(myClass.class).a(String str);
	System.out.println("主线程执行结束......");
	}
	
	@Async
	public void b(String str){
		Thread.sleep(3000);//休眠3秒,判断是否会阻塞主线程执行。
		System.out.println("因为我是异步的,所以与主线程无关"+str);
	}
}