写项目的时候经常会通过日志来排查原因,一种是通过log4j将日志收集出来,然后使用elk进行数据的分类与统计,这种是针对大量的请求日志进行记录。另一种就是通过自定义注解的方式写入数据库,这种是针对特定接口来处理,将重要的操作信息记录到数据库中。这篇博客就是说的第二种方式。
2、思路首先,应该使用自定义注解来标记哪些接口需要记录操作日志。
然后, 使用切面来统一拦截参数和返回的结果,并将信息返回到数据库。
3、实现过程 1、Log 自定义注解package com.example.demo.common.annotation;
import com.example.demo.enums.BusinessType;
import com.example.demo.enums.OperatorType;
import java.lang.annotation.*;
@Target({ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface Log {
String title() default "";
BusinessType businessType() default BusinessType.INSERT;
OperatorType operatorType() default OperatorType.OTHER;
boolean isSaveRequestData() default true;
}
2、 进行跨域配置
package com.example.demo.config;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.CorsRegistry;
import org.springframework.web.servlet.config.annotation.ResourceHandlerRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
@Configuration
public class CorsConfiguration implements WebMvcConfigurer {
@Override
public void addCorsMappings(CorsRegistry registry) {
//本应用的所有方法都会去处理跨域请求
registry.addMapping("
@RequestMapping("/user")
@RestController
public class UserController {
@GetMapping("/selectAll")
@Log(title = "用户查询",businessType = BusinessType.SELECT)
public List selectAllUser() {
return new ArrayList<>();
}
}
4、定义切面实现请求拦截
package com.example.demo.aspectj;
import com.alibaba.fastjson.JSON;
import com.example.demo.common.annotation.Log;
import com.example.demo.entity.SysLog;
import com.example.demo.enums.BusinessStatus;
import com.example.demo.service.ISysLogService;
import com.example.demo.util.IpUtils;
import com.example.demo.util.SpringUtils;
import org.apache.commons.lang3.StringUtils;
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.Signature;
import org.aspectj.lang.annotation.AfterReturning;
import org.aspectj.lang.annotation.AfterThrowing;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Pointcut;
import org.aspectj.lang.reflect.MethodSignature;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.http.HttpMethod;
import org.springframework.stereotype.Component;
import org.springframework.web.context.request.RequestAttributes;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;
import org.springframework.web.servlet.HandlerMapping;
import java.lang.reflect.Method;
import java.util.Map;
@Aspect
@Component
public class LogAspect {
private static final Logger log = LoggerFactory.getLogger(LogAspect.class);
//配置织入点
@Pointcut("@annotation(com.example.demo.common.annotation.Log)")
public void logPointCut() {
}
@AfterReturning(pointcut = "logPointCut()", returning = "jsonResult")
public void doAfterRetuning(JoinPoint joinPoint, Object jsonResult) {
handleLog(joinPoint, null, jsonResult);
}
@AfterThrowing(value = "logPointCut()", throwing = "ex")
public void doAfterThrowing(JoinPoint joinPoint, Exception ex) {
handleLog(joinPoint, ex, null);
}
protected void handleLog(final JoinPoint joinPoint, final Exception e, Object jsonResult) {
Log controllerLog = getAnnotationLog(joinPoint);
if (controllerLog == null) {
return;
}
SysLog sysLog = new SysLog();
sysLog.setStatus(BusinessStatus.SUCCESS.ordinal());
// 请求的地址
RequestAttributes attributes = RequestContextHolder.getRequestAttributes();
ServletRequestAttributes requestAttributes = (ServletRequestAttributes) attributes;
String ip = IpUtils.getIpAddr(requestAttributes.getRequest());
sysLog.setOperIp(ip);
// 返回参数
sysLog.setOperUrl(requestAttributes.getRequest().getRequestURI());
sysLog.setOperName("zhangyang");
if (e != null) {
sysLog.setStatus(BusinessStatus.FAIL.ordinal());
sysLog.setErrorMsg(StringUtils.substring(e.getMessage(), 0, 2000));
}
// 设置方法名称
String className = joinPoint.getTarget().getClass().getName();
String methodName = joinPoint.getSignature().getName();
sysLog.setMethod(className + "." + methodName + "()");
sysLog.setOperLocation(sysLog.getOperIp());
// 设置请求方式
sysLog.setRequestMethod(requestAttributes.getRequest().getMethod());
getControllerMethodDescription(joinPoint, controllerLog, sysLog);
// 保存到数据库
SpringUtils.getBean(ISysLogService.class).insertLog(sysLog);
}
public void getControllerMethodDescription(JoinPoint joinPoint, Log log, SysLog sysLog) {
// 设置action动作
sysLog.setBusinessType(log.businessType().ordinal());
// 设置标题
sysLog.setTitle(log.title());
// 设置操作人的类别
sysLog.setOperatorType(log.operatorType().ordinal());
// 是否需要保存request 参数和 值
if (log.isSaveRequestData()) {
setRequestValue(joinPoint, sysLog);
}
}
private void setRequestValue(JoinPoint joinPoint,SysLog sysLog) {
String requestMethod = sysLog.getRequestMethod();
if(HttpMethod.PUT.name().equals(requestMethod) || HttpMethod.POST.name().equals(requestMethod)) {
String params = argsArrayString(joinPoint.getArgs());
sysLog.setOperParam(StringUtils.substring(params,0,2000));
} else {
// 请求的地址
RequestAttributes attributes = RequestContextHolder.getRequestAttributes();
ServletRequestAttributes requestAttributes = (ServletRequestAttributes) attributes;
Map,?> paramsMap = (Map,?>) requestAttributes.getRequest().getAttribute(HandlerMapping.URI_TEMPLATE_VARIABLES_ATTRIBUTE);
sysLog.setOperParam(StringUtils.substring(paramsMap.toString(),0,2000));
}
}
private String argsArrayString(Object[] paramsArray) {
String param = "";
if(paramsArray != null && paramsArray.length > 0) {
for(int i=0; i
5、相关实体类
1、操作日志实体类SysLog
package com.example.demo.entity;
import lombok.Data;
@Data
public class SysLog {
private Long id;
private String title;
private Integer businessType;
private String method;
private String requestMethod;
private Integer operatorType;
private String operName;
private String deptName;
private String operUrl;
private String operIp;
private String operLocation;
private String operParam;
private String jsonResult;
private Integer status;
private String errorMsg;
}
2、User
package com.example.demo.entity;
import com.baomidou.mybatisplus.annotation.TableId;
import lombok.Data;
@Data
public class User {
@TableId
Integer id;
String name;
String address;
}
6、枚举类
package com.example.demo.enums;
public enum BusinessStatus {
SUCCESS,
FAIL,
}
package com.example.demo.enums;
public enum BusinessType {
INSERT,
UPDATE,
DELETE,
CLEAN,
SELECT,
}
package com.example.demo.enums;
public enum OperatorType {
OTHER,
MANAGE,
MOBILE
}
7、Mapper
package com.example.demo.mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.example.demo.entity.SysLog;
import org.apache.ibatis.annotations.Mapper;
@Mapper
public interface SysLogMapper extends BaseMapper {
}
8、service以及Impl
package com.example.demo.service;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.example.demo.entity.SysLog;
import com.example.demo.vo.SysLogBasePageVo;
public interface ISysLogService {
void insertLog(SysLog sysLog);
Page selectSysLogPage(SysLogBasePageVo sysLogBasePageVo);
}
package com.example.demo.service.impl;
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.example.demo.entity.SysLog;
import com.example.demo.mapper.SysLogMapper;
import com.example.demo.service.ISysLogService;
import com.example.demo.vo.SysLogBasePageVo;
import org.apache.commons.lang3.StringUtils;
import org.springframework.stereotype.Service;
import javax.annotation.Resource;
@Service
public class SysLogServiceImpl implements ISysLogService {
@Resource
private SysLogMapper sysLogMapper;
@Override
public void insertLog(SysLog sysLog) {
sysLogMapper.insert(sysLog);
}
@Override
public Page selectSysLogPage(SysLogBasePageVo sysLogBasePageVo) {
QueryWrapper sysLogQueryWrapper = new QueryWrapper<>();
if(StringUtils.isNotEmpty(sysLogBasePageVo.getSearchKey())) {
sysLogQueryWrapper.eq("title",sysLogBasePageVo.getSearchKey());
}
return sysLogMapper.selectPage(new Page<>(sysLogBasePageVo.getCurrentPage(),sysLogBasePageVo.getPageSize()),sysLogQueryWrapper);
}
}
9、Util工具类
package com.example.demo.util;
import javax.servlet.http.HttpServletRequest;
public class IpUtils
{
public static String getIpAddr(HttpServletRequest request)
{
if (request == null)
{
return "unknown";
}
String ip = request.getHeader("x-forwarded-for");
if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip))
{
ip = request.getHeader("Proxy-Client-IP");
}
if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip))
{
ip = request.getHeader("X-Forwarded-For");
}
if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip))
{
ip = request.getHeader("WL-Proxy-Client-IP");
}
if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip))
{
ip = request.getHeader("X-Real-IP");
}
if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip))
{
ip = request.getRemoteAddr();
}
return "0:0:0:0:0:0:0:1".equals(ip) ? "127.0.0.1" : "";
}
}
package com.example.demo.util;
import org.springframework.beans.BeansException;
import org.springframework.beans.factory.config.BeanFactoryPostProcessor;
import org.springframework.beans.factory.config.ConfigurableListableBeanFactory;
import org.springframework.context.ApplicationContext;
import org.springframework.context.ApplicationContextAware;
import org.springframework.stereotype.Component;
@Component
public class SpringUtils implements BeanFactoryPostProcessor, ApplicationContextAware {
private static ConfigurableListableBeanFactory beanFactory;
private static ApplicationContext applicationContext;
@Override
public void postProcessBeanFactory(ConfigurableListableBeanFactory configurableListableBeanFactory) throws BeansException {
SpringUtils.beanFactory = configurableListableBeanFactory;
}
@Override
public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
SpringUtils.applicationContext = applicationContext;
}
public static T getBean(String name) {
return (T) beanFactory.getBean(name);
}
public static T getBean(Class clz) {
T result = (T) beanFactory.getBean(clz);
return result;
}
}
10、vo 对象
package com.example.demo.vo;
import lombok.Data;
@Data
public class BasePageVo {
Integer pageSize;
Integer currentPage;
Integer pages;
}
package com.example.demo.vo;
import lombok.Data;
@Data
public class SysLogBasePageVo extends BasePageVo {
String searchKey;
}
11、application.yml工具类
# 应用名称
spring.application.name=demo
# 应用服务 WEB 访问端口
server.port=8080
# 数据库驱动:
spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver
# 数据源名称
spring.datasource.name=defaultDataSource
# 数据库连接地址
spring.datasource.url=jdbc:mysql://localhost:3306/springboot?useUnicode=true&characterEncoding=utf8&zeroDateTimeBehavior=convertToNull&useSSL=true&serverTimezone=GMT%2B8
# 数据库用户名&密码:
spring.datasource.username=root
spring.datasource.password=root
SysLog 表结构
User 表结构
4、代码分析
首先,定义了一个自定义注解Log,其目标范围为Method,只有在运行的时候才会保留,同时可以被javadoc工具类文档化。其代码中定义了 属性类型为String的属性名称为title ,
属性类型为BusinessType 属性名称为busniessType 默认值为INSERT , 属性类型为OperatorType 属性名称operatorTpye 默认值为 OperatorType.OTHER, 属性类型为boolean
属性方法为isSaveRequestData 默认值为true。
然后controller 使用了这个注解
@GetMapping("/selectAll")
@Log(title = "用户查询",busninessType = BusinessType.SELECT)
最后,定义了LogAspect 切面类,
@Pointcut("@annotation(com.example.demo.common.annnotaion.Log)")
public void logPointCut() {
}
拦截方法处理完后的请求
@AfterReturning(poincut = "logPointCut()", returning="jsonResult")
public void doAfterReturning(JoinPoint joinPoint, Object jsonResult) {
handleLog(joinPoint, null, jsonResult);
}
拦截方法被抛出异常的请求
public void doAfterThrowing(JoinPoint joinPoint,Exception ex) {
handleLog(joinPoint,ex,null);
}
封装方法并保存到数据库中
protected void handleLog(final JoinPoint joinPoint, final Exception e, Object jsonResult) {
Log controllerLog = getAnnotationLog(joinPoint);
if (controllerLog == null) {
return;
}
SysLog sysLog = new SysLog();
sysLog.setStatus(BusinessStatus.SUCCESS.ordinal());
// 请求的地址
RequestAttributes attributes = RequestContextHolder.getRequestAttributes();
ServletRequestAttributes requestAttributes = (ServletRequestAttributes) attributes;
String ip = IpUtils.getIpAddr(requestAttributes.getRequest());
sysLog.setOperIp(ip);
// 返回参数
sysLog.setOperUrl(requestAttributes.getRequest().getRequestURI());
sysLog.setOperName("zhangyang");
if (e != null) {
sysLog.setStatus(BusinessStatus.FAIL.ordinal());
sysLog.setErrorMsg(StringUtils.substring(e.getMessage(), 0, 2000));
}
// 设置方法名称
String className = joinPoint.getTarget().getClass().getName();
String methodName = joinPoint.getSignature().getName();
sysLog.setMethod(className + "." + methodName + "()");
sysLog.setOperLocation(sysLog.getOperIp());
// 设置请求方式
sysLog.setRequestMethod(requestAttributes.getRequest().getMethod());
getControllerMethodDescription(joinPoint, controllerLog, sysLog);
// 保存到数据库
SpringUtils.getBean(ISysLogService.class).insertLog(sysLog);
}
public void getControllerMethodDescription(JoinPoint joinPoint, Log log, SysLog sysLog) {
// 设置action动作
sysLog.setBusinessType(log.businessType().ordinal());
// 设置标题
sysLog.setTitle(log.title());
// 设置操作人的类别
sysLog.setOperatorType(log.operatorType().ordinal());
// 是否需要保存request 参数和 值
if (log.isSaveRequestData()) {
setRequestValue(joinPoint, sysLog);
}
}
读取请求中的参数并放到log中
private void setRequestValue(JoinPoint joinPoint,SysLog sysLog) {
String requestMethod = sysLog.getRequestMethod();
if(HttpMethod.PUT.name().equals(requestMethod) || HttpMethod.POST.name().equals(requestMethod)) {
String params = argsArrayString(joinPoint.getArgs());
sysLog.setOperParam(StringUtils.substring(params,0,2000));
} else {
// 请求的地址
RequestAttributes attributes = RequestContextHolder.getRequestAttributes();
ServletRequestAttributes requestAttributes = (ServletRequestAttributes) attributes;
Map,?> paramsMap = (Map,?>) requestAttributes.getRequest().getAttribute(HandlerMapping.URI_TEMPLATE_VARIABLES_ATTRIBUTE);
sysLog.setOperParam(StringUtils.substring(paramsMap.toString(),0,2000));
}
}
将参数转化为json
private String argsArrayString(Object[] paramsArray) {
String param = "";
if(paramsArray != null && paramsArray.length > 0) {
for(int i=0; i
private Log getAnnotationLog(JoinPoint joinPoint) {
Signature signature = joinPoint.getSignature();
MethodSignature methodSignature = (MethodSignature) signature;
Method method = methodSignature.getMethod();
if (method != null) {
return method.getAnnotation(Log.class);
}
return null;
}
5、实现结果
运行起项目,地址栏访问 http://localhost:8080/user/selectAll
这时候就会发现数据库表sys_log 就会新增一条记录
6、总结
记录请求日志功能主要是用切面知识,
切面主要支持5种类型的通知注解:
@Before 前置通知,在方法执行之前执行
@After 后置通知,在方法执行之后执行
@AfterRunning 返回通知,在方法返回结果之后执行
@AfterThrowing 异常通知,在方法抛出异常之后
@Around: 环绕通知
也就是,需要在核心业务前执行该辅助业务、在核心业务执行之后执行该辅助业务、在报错时候执行该辅助业务、在方法执行之前之后异常时执行该辅助业务
@PointCut(value ="execution(* com.cn.spring.aspectj.NotVeryUsefulAspectService.*(...))")
其中表达式标签
execution 用于匹配方法执行的连接点
args(): 用于匹配当前执行的方法传入的参数为指定类型的执行方法
this(): 用于匹配当前AOP代理对象类型的执行方法,注意是AOP代理对象类型的配置,这样就可能包括引入接口也类型匹配;
target(): 用于匹配当前目标对象类型的执行方法,注意是目标对象的类型匹配,这样就不包括引入接口也类型匹配;
within(): 用于匹配指定类型内的方法执行;
@args(): 用于匹配当前执行的参数
如 this(com.test.spring.aop.pointcutexp.Intf) 实现Intf 接口的所有类,如果Intf不是接口,限定Intf单个类
@target(org.springframework.transaction.annotation.Transactional) 带有@Transactional 标注的所有类的任意方法
@args(org.springframework.transation.annotation.Transational) 参数带有@Transational 标注的方法
args(String) 参数为String 类型的方法,
args(java,io.serializable,...) 任何一个以接受“传入参数类型为java.io.serializable”开头,且其后可跟任意个任意类型的参数的方法执行,args 指定的参数类型是在运行时动态匹配的
第二个就是使用手动获取Spring的bean对象
package com.example.demo.util;
import org.springframework.beans.BeansException;
import org.springframework.beans.factory.config.BeanFactoryPostProcessor;
import org.springframework.beans.factory.config.ConfigurableListableBeanFactory;
import org.springframework.context.ApplicationContext;
import org.springframework.context.ApplicationContextAware;
import org.springframework.stereotype.Component;
@Component
public class SpringUtils implements BeanFactoryPostProcessor, ApplicationContextAware {
private static ConfigurableListableBeanFactory beanFactory;
private static ApplicationContext applicationContext;
@Override
public void postProcessBeanFactory(ConfigurableListableBeanFactory configurableListableBeanFactory) throws BeansException {
SpringUtils.beanFactory = configurableListableBeanFactory;
}
@Override
public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
SpringUtils.applicationContext = applicationContext;
}
public static T getBean(String name) {
return (T) beanFactory.getBean(name);
}
public static T getBean(Class clz) {
T result = (T) beanFactory.getBean(clz);
return result;
}
}



