R是统一泛型返回对象,Log是日志对象,LogHttpServletResponseWrapper是防止请求数据获取后丢失
建好数据库即可,表不存在会自动创建,别说什么高并发会怎么怎么样了,高并发不用搜索引擎,还想用数据库记录日志就是疯了
异步入库结果图
A线程获取完数据,以后入库由B线程去执行,不影响主线程,即是入库失败也不会影响主线程的功能
LogFilter.java
过滤请求头和返回内容
import com.fasterxml.jackson.databind.ObjectMapper;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;
import javax.annotation.Resource;
import javax.servlet.*;
import javax.servlet.annotation.WebFilter;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.util.*;
@Slf4j
@Component
@WebFilter(urlPatterns = "/*", filterName = "logFilter")
public class LogFilter implements Filter {
@Value("${spring.application.name}")
private String applicationName;
@Value("${log-application.ignore-uri}")
private String ignoreURIs;
@Resource
private LogService logService;
@Override
public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
HttpServletRequest request = (HttpServletRequest) servletRequest;
HttpServletResponse response = (HttpServletResponse) servletResponse;
Map<String, String[]> parameterMap = request.getParameterMap();
Map<String, Object> map = new HashMap<>();
if (parameterMap != null) {
parameterMap.forEach((key, value) -> {
map.put(key, value[0]);
});
}
LogHttpServletResponseWrapper logHttpServletResponseWrapper = new LogHttpServletResponseWrapper(response);
filterChain.doFilter(request, logHttpServletResponseWrapper);
try {
String content = new String(logHttpServletResponseWrapper.getContent(), StandardCharsets.UTF_8);
ObjectMapper om = new ObjectMapper();
R r = om.readValue(content, R.class);
boolean haveIgnoreURI = Arrays.stream(ignoreURIs.split(",")).anyMatch(ignoreURI -> request.getRequestURI().contains(ignoreURI));
if (!haveIgnoreURI) {
Log logObj = new Log();
logObj.setUserId("1");
logObj.setUsername("Meta");
logObj.setApplicationName(applicationName);
logObj.setMethod(request.getMethod());
logObj.setRequestURI(request.getRequestURI());
logObj.setRequestData(String.valueOf(map));
logObj.setCode(r.getCode());
logObj.setRespondData(om.writeValueAsString(r.getData()));
logObj.setErrorMsg(om.writeValueAsString(r.getDetailErrorMsg()));
logObj.setCreateTime(new Date());
log.info("打印当前A线程名称:{}",Thread.currentThread().getName());
logService.insert(logObj);
r.setDetailErrorMsg(null);
}
String newContent = om.writeValueAsString(r);
response.setContentLength(newContent.getBytes(StandardCharsets.UTF_8).length);
ServletOutputStream out = response.getOutputStream();
out.write(newContent.getBytes(StandardCharsets.UTF_8));
out.flush();
out.close();
} catch (Exception e) {
log.error("日志入库异常:", e);
}
}
}
LogHttpServletResponseWrapper.java
防止请求数据丢失,回写数据
import javax.servlet.ServletOutputStream;
import javax.servlet.WriteListener;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpServletResponseWrapper;
import java.io.*;
public class LogHttpServletResponseWrapper extends HttpServletResponseWrapper {
private final ByteArrayOutputStream buffer;
private final ServletOutputStream out;
public LogHttpServletResponseWrapper(HttpServletResponse httpServletResponse) {
super(httpServletResponse);
buffer = new ByteArrayOutputStream();
out = new WrapperOutputStream(buffer);
}
@Override
public ServletOutputStream getOutputStream() {
return out;
}
@Override
public void flushBuffer() throws IOException {
if (out != null) {
out.flush();
}
}
public byte[] getContent() throws IOException {
flushBuffer();
return buffer.toByteArray();
}
static class WrapperOutputStream extends ServletOutputStream {
private final ByteArrayOutputStream bos;
public WrapperOutputStream(ByteArrayOutputStream bos) {
this.bos = bos;
}
@Override
public void write(int b) {
bos.write(b);
}
@Override
public boolean isReady() {
return false;
}
@Override
public void setWriteListener(WriteListener arg0) {
}
}
}
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 errorMsg;
private Date createTime;
}
LogDTO.java
日志请求参数
import lombok.Data;
@Data
public class LogDTO {
private String yyyyMM;
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 errorMsg;
private String startTime;
private String endTime;
}
LogController.java
日志控制层
import javax.annotation.Resource;
import com.fu.work.entity.LogDTO;
import com.fu.work.entity.R;
import com.fu.work.service.LogService;
import org.springframework.web.bind.annotation.*;
@RestController
@RequestMapping("log")
public class LogController {
@Resource
private LogService logService;
@GetMapping("queryById")
public R queryById(@RequestParam(required = false) String yyyyMM, @RequestParam Long id) {
return R.ok(logService.queryById(yyyyMM,id));
}
@PostMapping("findAll")
public R findAll(@RequestBody LogDTO logDTO) {
return R.ok(logService.findAll(logDTO));
}
}
LogService.java
异步线程入库
import javax.annotation.Resource;
import lombok.extern.slf4j.Slf4j;
import org.springframework.jdbc.BadSqlGrammarException;
import org.springframework.scheduling.annotation.Async;
import org.springframework.stereotype.Service;
import org.springframework.util.StringUtils;
import java.text.SimpleDateFormat;
import java.util.Calendar;
import java.util.List;
@Slf4j
@Service
public class LogService {
@Resource
private LogDao logDao;
public Log queryById(String tableName, Long id) {
if (!StringUtils.hasLength(tableName)) {
tableName = getYearMonth(0);
}
return logDao.queryById(tableName, id);
}
public List<Log> findAll(LogDTO logDTO) {
String tableName = logDTO.getYyyyMM();
if (!StringUtils.hasLength(tableName)) {
tableName = getYearMonth(0);
}
return logDao.findAll(tableName, logDTO);
}
@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);
}
}
}
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());
}
}
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);
executor.setRejectedExecutionHandler(new ThreadPoolExecutor.AbortPolicy());
executor.initialize();
return executor;
}
}
LogDao.java
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Param;
import java.util.List;
@Mapper
public interface LogDao{
int crateTable(@Param("tableName") String tableName);
Log queryById(@Param("tableName") String tableName,@Param("id") Long id);
List<Log> findAll(@Param("tableName") String tableName,@Param("logDTO") LogDTO logDTO);
int insert(@Param("tableName") String tableName,@Param("log") Log log);
}
LogMapper.xml
sql创建按月分表日志表和查询日志数据
<?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.fu.work.dao.LogDao">
<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="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, 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 '返回数据',
`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>
<select id="queryById" resultMap="BaseResultMap">
select
<include refid="Base_Column_List"/>
from log_${tableName} where id = #{id}
</select>
<select id="findAll" resultMap="BaseResultMap">
select
<include refid="Base_Column_List"/>
from log_${tableName}
<where>
<if test="logDTO.userId != null and logDTO.userId != '' ">
AND user_id = #{logDTO.userId}
</if>
<if test="logDTO.code != null">
AND code = #{logDTO.code}
</if>
<if test="logDTO.method != null and logDTO.method != ''">
AND `method` = #{logDTO.method}
</if>
<if test="logDTO.applicationName != null and logDTO.applicationName != ''">
AND application_name = #{logDTO.applicationName}
</if>
<if test="logDTO.username != null and logDTO.username != ''">
AND username LIKE CONCAT('%',#{logDTO.username,jdbcType=VARCHAR},'%')
</if>
<if test="logDTO.requestURI != null and logDTO.requestURI != ''">
AND request_uri LIKE CONCAT('%',#{logDTO.requestURI},'%')
</if>
<if test="logDTO.requestData != null and logDTO.requestData != ''">
AND request_data LIKE CONCAT('%',#{logDTO.requestData,jdbcType=VARCHAR},'%')
</if>
<if test="logDTO.respondData != null and logDTO.respondData != ''">
AND respond_data LIKE CONCAT('%',#{logDTO.respondData,jdbcType=VARCHAR},'%')
</if>
<if test="logDTO.errorMsg != null and logDTO.errorMsg != ''">
AND error_msg LIKE CONCAT('%',#{logDTO.errorMsg,jdbcType=VARCHAR},'%')
</if>
<if test="logDTO.startTime != null">
AND create_time <![CDATA[>=]]> #{logDTO.startTime}
</if>
<if test="logDTO.endTime != null">
AND create_time <![CDATA[<=]]> #{logDTO.endTime}
</if>
</where>
ORDER BY create_time DESC
</select>
<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.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.errorMsg != null">
#{log.errorMsg},
</if>
<if test="log.createTime != null">
#{log.createTime},
</if>
</trim>
</insert>
</mapper>
R.java
统一泛型返回对象
import lombok.*;
import lombok.experimental.Accessors;
import java.io.Serializable;
@Builder
@ToString
@NoArgsConstructor
@AllArgsConstructor
@Accessors(chain = true)
public class R<T> implements Serializable {
private static final long serialVersionUID = 1L;
@Getter
@Setter
private int code;
@Setter
private String msg;
public String getMsg(){
if (msg==null){
msg=ResultCode.getMsg(this.code);
}
return msg;
}
@Getter
@Setter
private T data;
@Getter
@Setter
private Exception detailErrorMsg;
public static <T> R<T> ok() {
return restResult(null, 0, null);
}
public static <T> R<T> ok(T data) {
return restResult(data, 0, null);
}
public static <T> R<T> ok(T data, String msg) {
return restResult(data, 0, msg);
}
public static <T> R<T> failed() {
return restResult(null, 1, null);
}
public static <T> R<T> failed(int code) {
return restResult(null, code, null);
}
public static <T> R<T> failed(String msg) {
return restResult(null, 1, msg);
}
public static <T> R<T> failed(T data) {
return restResult(data, 1, null);
}
public static <T> R<T> failed(T data, String msg) {
return restResult(data, 1, msg);
}
public static <T> R<T> failed(T data, int code, String msg) {
return restResult(data, code, msg);
}
public static <T> R<T> failed(int code, String msg) {
return restResult(null, code, msg);
}
private static <T> R<T> restResult(T data, int code, String msg) {
R<T> apiResult = new R<>();
apiResult.setCode(code);
apiResult.setData(data);
apiResult.setMsg(msg);
return apiResult;
}
public static <T> R<T> failed(String msg,Exception detailErrorMsg) {
return restResult(null, 1, msg,detailErrorMsg);
}
private static <T> R<T> restResult(T data, int code, String msg,Exception detailErrorMsg) {
R<T> apiResult = new R<>();
apiResult.setCode(code);
apiResult.setData(data);
apiResult.setMsg(msg);
apiResult.setDetailErrorMsg(detailErrorMsg);
return apiResult;
}
}
GlobalExceptionHandler.java
全局异常处理类
@Slf4j
@RestControllerAdvice
public class GlobalExceptionHandler {
@ExceptionHandler(Exception.class)
public R Exception(Exception e) {
log.error("全局异常信息:", e);
return R.failed(e.Message(), e);
}
}
ReturnRes.java
拦截返回内容,把不符合统一泛型返回对象的转成统一泛型返回对象
import com.fasterxml.jackson.databind.ObjectMapper;
import lombok.SneakyThrows;
import org.springframework.core.MethodParameter;
import org.springframework.http.MediaType;
import org.springframework.http.converter.HttpMessageConverter;
import org.springframework.http.server.ServerHttpRequest;
import org.springframework.http.server.ServerHttpResponse;
import org.springframework.web.bind.annotation.RestControllerAdvice;
import org.springframework.web.servlet.mvc.method.annotation.ResponseBodyAdvice;
@RestControllerAdvice
public class ReturnRes implements ResponseBodyAdvice<Object> {
@Override
public boolean supports(MethodParameter methodParameter, Class<? extends HttpMessageConverter<?>> aClass) {
return true;
}
@SneakyThrows
@Override
public Object beforeBodyWrite(Object o, MethodParameter methodParameter, MediaType mediaType, Class<? extends HttpMessageConverter<?>> aClass, ServerHttpRequest serverHttpRequest, ServerHttpResponse serverHttpResponse) {
if (o instanceof String) {
return new ObjectMapper().writeValueAsString(R.ok(o));
} else if (o instanceof R) {
return o;
}
return R.ok(o);
}
}
application.yml
配置文件
log-application:
ignore-uri: "/log/findAll,/log/queryById"
server:
port: 88
spring:
application:
name: log
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
logging:
level:
com.fu.work.dao: info
build.gradle(和maven的pom.xml差不多)
依赖
dependencies {
implementation 'org.springframework.boot:spring-boot-starter-web'
compileOnly 'org.projectlombok:lombok'
annotationProcessor 'org.projectlombok:lombok'
runtimeClasspath 'org.mariadb.jdbc:mariadb-java-client'
implementation 'org.mybatis.spring.boot:mybatis-spring-boot-starter:2.2.2'
}
|