项目上需要对一些重要的接口记录操作日志,便于历史问题追踪、排查。主要记录的字段有操作人、请求ip、操作时间、模块、功能、请求参数、请求结果等。
记录操作日志基本上都是用AOP,当然我也不例外,需要记录的字段,大部分都很容易获取到,比较难获取的一个字段是请求参数,因为不同的接口参数请求方式不同,有的接口使用@RequestBody传json字符串,有的接口使用@RequestParam传的form参数,有的接口有文件上传@RequestParam("file") MultipartFile file。我们项目中大部分是使用@RequestBody传json字符串,少部分文件上传接口使用@RequestParam("file") MultipartFile file。
使用httpRequest.getInputStream()的方式提取@RequestBody中的json参数,大致代码如下:
ServletInputStream inputStream = httpRequest.getInputStream();
InputStreamReader reader = new InputStreamReader(inputStream,StandardCharsets.UTF_8);
BufferedReader bfReader = new BufferedReader(reader);
StringBuilder sb = new StringBuilder();
String line;
while ((line = bfReader.readLine()) != null){
sb.append(line);
}
System.out.println(sb.toString());
问题:发现AOP拦截后,controller那里获取到的参数为空。在网上查到问题原因是,“在拦截器中读取请求的JSON数据需要,获取请求中的输入流InputStream is = request.getInputStream();
当我们拦截器执行完成后,进入其他拦截器或者控制层参数解析时,也需要获取,当因为我们之前的拦截器已经获取过一次,之后的都获取不到内容,因此报出此错误!”。解决思路就是“通过过滤器,将原始的 HttpServletRequest
替换成我们自己的请求包装类,在其中重写 getInputStream()
方法”
public class BodyReaderRequestWrapper extends HttpServletRequestWrapper {
// 将流中的内容保存
private final byte[] buff;
public BodyReaderRequestWrapper(HttpServletRequest request) throws IOException {
super(request);
InputStream is = request.getInputStream();
ByteArrayOutputStream baos = new ByteArrayOutputStream();
byte[] b = new byte[1024];
int len;
while ((len = is.read(b)) != -1) {
baos.write(b, 0, len);
}
buff = baos.toByteArray();
}
@Override
public ServletInputStream getInputStream() throws IOException {
final ByteArrayInputStream bais = new ByteArrayInputStream(buff);
return new ServletInputStream() {
@Override
public boolean isFinished() {
return false;
}
@Override
public boolean isReady() {
return false;
}
@Override
public void setReadListener(ReadListener listener) {
}
@Override
public int read() throws IOException {
return bais.read();
}
};
}
@Override
public BufferedReader getReader() throws IOException {
return new BufferedReader(new InputStreamReader(getInputStream()));
}
public String getRequestBody() {
return new String(buff);
}
}
public class AuthFilter implements Filter {
@Override
public void init(FilterConfig filterConfig) throws ServletException {
}
@Override
public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
// 防止流读取一次后就没有了, 所以需要将流继续写出去
HttpServletRequest httpServletRequest = (HttpServletRequest) servletRequest;
// 这里将原始request传入,读出流并存储
ServletRequest requestWrapper = new BodyReaderRequestWrapper(httpServletRequest);
// 这里将原始request替换为包装后的request,此后所有进入controller的request均为包装后的
filterChain.doFilter(requestWrapper, servletResponse);//
}
@Override
public void destroy() {
}
}
@Configuration
public class FilterOrderConfig {
@Bean
public FilterRegistrationBean filterRegistrationBean1(){
FilterRegistrationBean filterRegistrationBean=new FilterRegistrationBean();
filterRegistrationBean.setFilter(new AuthFilter());
filterRegistrationBean.addUrlPatterns("/*");
//order的数值越小 则优先级越高,这里直接使用的最高优先级
filterRegistrationBean.setOrder(Ordered.HIGHEST_PRECEDENCE);
return filterRegistrationBean;
}
}
这种方式确实解决了第一次遇到的问题,测了几个普通接口也没问题,后来发现包含@RequestParam("file") MultipartFile file文件上传的接口提示Required request part 'file' is not present
在网上查了半天也没有查到合适的解决方法。于是准备参考一下一些成熟的springboot项目脚手架怎么处理的,发现若依项目没有出现这个问题,于是参考若依项目,结合自己项目特点,完成了这记录操作日志的功能,目前没发现问题。
直接贴上代码
操作日志实体类,存储在mongo
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
@Document(collection = "op_log")
public class OpLog {
@Id
private String id;
/**
* 用户名
*/
private String username;
/**
* 方法名
*/
private String method;
/**
* 参数
*/
private String params;
/**
* ip地址
*/
private String ip;
/**
* 请求url
*/
private String url;
/**
* //操作类型 :新增、删除等等
*/
private String type;
/**
* 模块
*/
private String model;
/**
* 操作时间
*/
private Long createTime;
/**
* 操作结果
*/
private String result;
/**
* 描述
*/
private String description;
}
注解定义
import java.lang.annotation.Documented;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
/**
* 自定义操作日志记录注解
*/
@Target({ ElementType.PARAMETER, ElementType.METHOD })
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface OperationLog
{
/**
* 模块
*/
String module() default "";
/**
* 功能
* @return
*/
String function() default "";
/**
* 操作类型
*/
BusinessType type() default BusinessType.QUERY;
}
操作日志处理
import cn.hutool.core.date.DateUtil;
import cn.hutool.json.JSONUtil;
import com.alibaba.fastjson.JSON;
import lombok.extern.slf4j.Slf4j;
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.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.stereotype.Component;
import org.springframework.validation.BindingResult;
import org.springframework.web.multipart.MultipartFile;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.lang.reflect.Method;
import java.util.Collection;
import java.util.Iterator;
import java.util.Map;
/**
* 操作日志记录处理
*/
@Aspect
@Component
@Slf4j
public class LogAspect {
@Autowired
private OpLogService opLogService;
// 配置织入点
@Pointcut("@annotation(com.cq.mysmsmanagerback.annotation.OperationLog)")
public void logPointCut() {
}
/**
* 处理完请求后执行
*
* @param joinPoint 切点
*/
@AfterReturning(pointcut = "logPointCut()", returning = "jsonResult")
public void doAfterReturning(JoinPoint joinPoint, Object jsonResult) {
handleLog(joinPoint, null, jsonResult);
}
/**
* 拦截异常操作
*
* @param joinPoint 切点
* @param e 异常
*/
@AfterThrowing(value = "logPointCut()", throwing = "e")
public void doAfterThrowing(JoinPoint joinPoint, Exception e) {
handleLog(joinPoint, e, null);
}
protected void handleLog(final JoinPoint joinPoint, final Exception e, Object result) {
try {
// 获得注解
OperationLog controllerOperationLog = getAnnotationLog(joinPoint);
if (controllerOperationLog == null) {
return;
}
OpLog opLog = new OpLog();
// 从切面织入点处通过反射机制获取织入点处的方法
MethodSignature signature = (MethodSignature) joinPoint.getSignature();
//获取切入点所在的方法
Method method = signature.getMethod();
//获取操作
OperationLog annotation = method.getAnnotation(OperationLog.class);
if (annotation != null) {
opLog.setModel(annotation.module());
opLog.setDescription(annotation.function());
opLog.setType(annotation.type().name());
}
// 获取请求的类名
String className = joinPoint.getTarget().getClass().getName();
// 获取请求的方法名
String methodName = method.getName();
methodName = className + "." + methodName;
opLog.setMethod(methodName);
opLog.setCreateTime(DateUtil.date().getTime());
//操作用户 --登录时有把用户的信息保存在session中,可以直接取出
String userName = SecurityContextHolder.getContext().getAuthentication().getName();
opLog.setUsername(userName);
String ip = IpUtils.getIpAddr(ServletUtils.getRequest());
opLog.setIp(ip);
opLog.setUrl(ServletUtils.getRequest().getRequestURI());
// 请求参数
String params = argsArrayToString(joinPoint.getArgs());
opLog.setParams(params.length() > 2000 ? params.substring(0, 2000) : params);
opLog.setResult(JSONUtil.toJsonStr(result));
// 插入到mongo
log.info("opLog: {}", JSONUtil.toJsonStr(opLog));
opLogService.insertOpLog(opLog);
// 保存数据库
// AsyncManager.me().execute(AsyncFactory.recordOper(operLog));
} catch (Exception exp) {
// 记录本地异常日志
log.error("==前置通知异常==");
log.error("异常信息:{}", exp.getMessage());
exp.printStackTrace();
}
}
/**
* 是否存在注解,如果存在就获取
*/
private OperationLog getAnnotationLog(JoinPoint joinPoint) throws Exception {
Signature signature = joinPoint.getSignature();
MethodSignature methodSignature = (MethodSignature) signature;
Method method = methodSignature.getMethod();
if (method != null) {
return method.getAnnotation(OperationLog.class);
}
return null;
}
/**
* 参数拼装
*/
private String argsArrayToString(Object[] paramsArray) {
String params = "";
if (paramsArray != null && paramsArray.length > 0) {
for (int i = 0; i < paramsArray.length; i++) {
if (paramsArray[i] != null && !isFilterObject(paramsArray[i])) {
Object jsonObj = JSON.toJSON(paramsArray[i]);
params += jsonObj.toString() + " ";
}
}
}
return params.trim();
}
/**
* 判断是否需要过滤的对象。
*
* @param o 对象信息。
* @return 如果是需要过滤的对象,则返回true;否则返回false。
*/
@SuppressWarnings("rawtypes")
public boolean isFilterObject(final Object o) {
Class> clazz = o.getClass();
if (clazz.isArray()) {
return clazz.getComponentType().isAssignableFrom(MultipartFile.class);
} else if (Collection.class.isAssignableFrom(clazz)) {
Collection collection = (Collection) o;
for (Iterator iter = collection.iterator(); iter.hasNext(); ) {
return iter.next() instanceof MultipartFile;
}
} else if (Map.class.isAssignableFrom(clazz)) {
Map map = (Map) o;
for (Iterator iter = map.entrySet().iterator(); iter.hasNext(); ) {
Map.Entry entry = (Map.Entry) iter.next();
return entry.getValue() instanceof MultipartFile;
}
}
return o instanceof MultipartFile || o instanceof HttpServletRequest || o instanceof HttpServletResponse
|| o instanceof BindingResult;
}
}
业务操作类型
public enum BusinessType
{
/**
* 新增
*/
ADD,
/**
* 修改
*/
UPDATE,
/**
* 删除
*/
DELETE,
/**
* 删除查询
*/
QUERY,
}
操作日志存储
@Service
@Slf4j
public class OpLogServiceImpl implements OpLogService {
@Resource
private MongoTemplate mongoTemplate;
/**
* 插入操作日志到mongo
*
* @param opLog
* @return
*/
@Override
public Result insertOpLog(OpLog opLog) {
mongoTemplate.insert(opLog);
return Result.buildSucc();
}
}
使用方法,在controller方法上加上下面类似注解就好
@OperationLog(module = "客户管理", function = "创建客户", type = BusinessType.ADD)
一些用得比较多的脚手架,里面还是有很多值得我们学习的地方,一些常用功能,可以多参考参考脚手架中怎么实现的,包括功能界面设计和代码实现。