springboot—aop 实现系统操作日志记录到数据库
最近有个需求,需要记录项目中各个接口的操作情况到数据库,采用spring 的 aop 技术定位到自定义注解上,针对不同注解标志进行参数解析,记录日志。
缺点:要针对每个不同的注解标志取注解标志,获取参数进行日志记录输出
1. 需要引用的依赖
<!--spring切面aop依赖-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-aop</artifactId>
</dependency>
2. 创建实体类
public class Operation {
private Long id;
private String identity ; // 操作人账号
private String clientIp ; // 客户端ip
private String username ; // 操作人姓名
private Long operType ; // 日志类型
private String operUrl ; // 操作的url
private String operEvent ; // 操作事件
private String reqParam ; // 请求参数信息
private String reqType ; // 请求方式:POST或者GET
private Date operTime ; // 操作时间
}
3. 创建一个自定义注解类,使用spring 的 aop 技术定位到自定义注解上
import java.lang.annotation.*;
/**
* 自定义注解类
*/
@Target(ElementType.METHOD) //注解放置的目标位置,METHOD是可注解在方法级别上
@Retention(RetentionPolicy.RUNTIME) //注解在哪个阶段执行
@Documented //生成文档
public @interface ViLog {
/** 操作事件 */
String operEvent () default "";
/** 日志类型 */
int operType ();
}
4. 创建aop切面实现类
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import org.aspectj.lang.annotation.Pointcut;
import org.aspectj.lang.reflect.MethodSignature;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;
import org.springframework.web.multipart.MultipartFile;
import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import javax.servlet.http.HttpServletRequest;
import java.io.*;
import java.lang.reflect.Method;
import java.util.Date;
/** 系统日志:切面处理类 */
@Aspect
@Component
public class SysLogAspect {
private static final Logger log = LoggerFactory.getLogger(SysLogAspect.class);
@Autowired
private IOperationService operationService;
//定义切点 @Pointcut
//在注解的位置切入代码
@Pointcut("@annotation( com.hz.vi.log.ViLog)")
public void logPoinCut() {
}
//切面 配置通知
@Before("logPoinCut()") //AfterReturning
public void saveOperation(JoinPoint joinPoint) {
log.info("---------------接口日志记录---------------");
//保存日志
Operation operation = new Operation();
//从切面织入点处通过反射机制获取织入点处的方法
MethodSignature signature = (MethodSignature) joinPoint.getSignature();
// System.out.println("signature="+signature);
//获取切入点所在的方法
Method method = signature.getMethod();
// System.out.println("method="+method);
//获取操作--方法上的ViLog的值
ViLog viLog = method.getAnnotation(ViLog.class);
if (viLog != null) {
//保存操作事件
String operEvent = viLog.operEvent();
operation.setOperEvent(operEvent);
//保存日志类型
long operType = viLog.operType();
operation.setOperType(operType);
log.info("operEvent="+operEvent+",operType="+operType);
}
HttpServletRequest request = ((ServletRequestAttributes) RequestContextHolder.getRequestAttributes()).getRequest();
// 请求方式:POST/GET/other
String httpMethod = request.getMethod();
operation.setReqType(httpMethod);
//操作的url
String requestURI = request.getRequestURI();
String requestURL = request.getRequestURL().toString();
operation.setOperUrl(requestURL);
// 客户端ip
String ip = request.getRemoteAddr();
operation.setClientIp(ip);
// 操作人账号、姓名
User user = (User) request.getSession().getAttribute(SysUser.SESSION_KEY);
if(user != null) {
String account = user.getAccount();
String username = user.getUsername();
operation.setIdentity(account);
operation.setUsername(username);
// System.out.println("=================account:"+account+"--"+username);
}
log.info("httpMethod="+httpMethod+",URL="+requestURL);
//请求参数信息
String paramter = "";
//get的参数
if(httpMethod.equalsIgnoreCase("GET")){
Object[] args = joinPoint.getArgs();
Object[] arguments = new Object[args.length];
for (int i = 0; i < args.length; i++) {
if (args[i] instanceof ServletRequest || args[i] instanceof ServletResponse || args[i] instanceof MultipartFile) {
//ServletRequest不能序列化,从入参里排除,否则报异常:java.lang.IllegalStateException: It is illegal to call this method if the current request is not in asynchronous mode (i.e. isAsyncStarted() returns false)
//ServletResponse不能序列化 从入参里排除,否则报异常:java.lang.IllegalStateException: getOutputStream() has already been called for this response
continue;
}
arguments[i] = args[i];
}
if (arguments != null) {
try {
paramter = JSONObject.toJSONString(arguments);
} catch (Exception e) {
paramter = arguments.toString();
}
}
operation.setReqParam(paramter);
//其他方法的参数
}else {
try {
request.setCharacterEncoding("UTF-8");
paramter = PostUtil.getRequestPostStr(request);
String trim = paramter.trim();
if("".equals(trim)) {
for (int i = 0; i < args.length; i++) {
if (args[i] instanceof ServletRequest || args[i] instanceof ServletResponse || args[i] instanceof MultipartFile) {
//ServletRequest不能序列化,从入参里排除,否则报异常:java.lang.IllegalStateException: It is illegal to call this method if the current request is not in asynchronous mode (i.e. isAsyncStarted() returns false)
//ServletResponse不能序列化 从入参里排除,否则报异常:java.lang.IllegalStateException: getOutputStream() has already been called for this response
continue;
}
arguments[i] = args[i];
}
if (arguments != null) {
try {
paramter = JSONObject.toJSONString(arguments);
} catch (Exception e) {
paramter = arguments.toString();
}
}
}
} catch (IOException e) {
log.error("error:"+e);
}
operation.setReqParam(paramter);
}
/* //获取请求的类名
String className = joinPoint.getTarget().getClass().getName();
//获取请求的方法名
String methodName = method.getName();
System.out.println("method name="+className + "." + methodName);*/
//操作时间
operation.setOperTime(new Date());
//获取用户名
// operation.setUsername(ShiroUtils.getUserEntity().getUsername());
//获取用户ip地址
// HttpServletRequest request = HttpContextUtils.getHttpServletRequest();
// operation.setClientIp(IPUtils.getIpAddr(request));
//调用service保存Operation实体类到数据库
operationService.insert(operation);
}
}
5. 接下来就可以在需要监控的方法上添加 aop的自定义注解
格式为 @+自定义注解的类名 @MyLog
//例如在contoller类的方法上加注解
@RestController
@RequestMapping("/sys/menu")
public class SysMenuController extends AbstractController {
@Autowired
private SysMenuService sysMenuService;
@ViidLog(operEvent = "删除菜单记录", operType =1) //接口日志记录的AOP自定义注解-operEvent:操作事件;operType=日志类型
@PostMapping("/del")
public R deleteBatch(@RequestBody Long[] menuIds) {
for (Long menuId : menuIds) {
if (menuId <= 31) {
return R.error("系统菜单,不能删除");
}
}
sysMenuService.deleteBatch(menuIds);
return R.ok("删除成功");
}
}
注意
在获取post请求的参数的时候会问题--在拦截器获取了一遍参数后再传到controller里就不能在获取到post请求的参数。
经过拦截器后,参数经过@RequestBody注解赋值给controller中的方法的时候,却抛出了一个这样的异常:
org.springframework.http.converter.HttpMessageNotReadableException: Required request body is missing
因为流对应的是数据,数据放在内存中,有的是部分放在内存中。read 一次标记一次当前位置(mark position),第二次read就从标记位置继续读(从内存中copy)数据。 所以这就是为什么读了一次第二次是空了。 怎么让它不为空呢?只要inputstream 中的pos 变成0就可以重写读取当前内存中的数据。javaAPI中有一个方法public void reset() 这个方法就是可以重置pos为起始位置,但是不是所有的IO读取流都可以调用该方法!ServletInputStream是不能调用reset方法,这就导致了只能调用一次getInputStream()。
解决方案
重写HttpServletRequestWrapper把request保存下来,然后通过过滤器把保存下来的request再填充进去,这样就可以多次读取request了。
1.写一个类,继承HttpServletRequestWrapper
import javax.servlet.ReadListener;
import javax.servlet.ServletInputStream;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletRequestWrapper;
import java.io.*;
public class RequestWrapper extends HttpServletRequestWrapper {
private final String body;
public RequestWrapper(HttpServletRequest request) {
super(request);
StringBuilder stringBuilder = new StringBuilder();
BufferedReader bufferedReader = null;
InputStream inputStream = null;
try {
inputStream = request.getInputStream();
if (inputStream != null) {
bufferedReader = new BufferedReader(new InputStreamReader(inputStream));
char[] charBuffer = new char[128];
int bytesRead = -1;
while ((bytesRead = bufferedReader.read(charBuffer)) > 0) {
stringBuilder.append(charBuffer, 0, bytesRead);
}
} else {
stringBuilder.append("");
}
} catch (IOException ex) {
} finally {
if (inputStream != null) {
try {
inputStream.close();
}
catch (IOException e) {
e.printStackTrace();
}
}
if (bufferedReader != null) {
try {
bufferedReader.close();
}
catch (IOException e) {
e.printStackTrace();
}
}
}
body = stringBuilder.toString();
}
@Override
public ServletInputStream getInputStream() throws IOException {
final ByteArrayInputStream byteArrayInputStream = new ByteArrayInputStream(body.getBytes());
ServletInputStream servletInputStream = new ServletInputStream() {
@Override
public boolean isFinished() {
return false;
}
@Override
public boolean isReady() {
return false;
}
@Override
public void setReadListener(ReadListener readListener) {
}
@Override
public int read() throws IOException {
return byteArrayInputStream.read();
}
};
return servletInputStream;
}
@Override
public BufferedReader getReader() throws IOException {
return new BufferedReader(new InputStreamReader(this.getInputStream()));
}
public String getBody() {
return this.body;
}
}
2.过滤器Filter,用来把request传递下去
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import javax.servlet.*;
import javax.servlet.annotation.WebFilter;
import javax.servlet.http.HttpServletRequest;
import java.io.IOException;
@WebFilter(urlPatterns = "/*",filterName = "channelFilter")
public class ChannelFilter implements Filter{
private static Logger LOGGER = LoggerFactory.getLogger(ChannelFilter .class);
@Override
public void init(FilterConfig filterConfig) throws ServletException {
}
@Override
public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
ServletRequest requestWrapper = null;
if(servletRequest instanceof HttpServletRequest) {
requestWrapper = new RequestWrapper((HttpServletRequest) servletRequest);
}
if(requestWrapper == null) {
filterChain.doFilter(servletRequest, servletResponse);
} else {
filterChain.doFilter(requestWrapper, servletResponse);
}
}
@Override
public void destroy() {
}
}
3.在启动类中注册拦截器
@SpringBootApplication
@ServletComponentScan //注册过滤器注解
@Configuration
public class WebApplication {
public static void main(String[] args) {
SpringApplication.run(WebApplication.class, args);
}
}