需求:为系统中所有的提交,修改,删除等等操作(除查询以外的所有操作)做日志记录,记录的内容包括:请求参数,返回参数,如果报错就存储报错信息。日志要添加一个日志类型。
方案:最好的选择就是用注解标记切点,用AOP实现此需求。
版本:
日志按功能分类,定义一个枚举类,用于在切点注解上标记日志类型。存储日志信息的时候存储到type字段上。
日志类型枚举类:LogType.java
标记此操作是什么类型,比如,增加用户,删除用户、、、、
/**
* Description:
* User: RoronoraZoro丶WangRui
* Date: 2018-09-03
* Time: 下午4:20
*/
public enum LogType {
/**
* 应用-增加
*/
APP_ADD(30001),
/**
* 部署-部署应用
*/
DEP_ADD(40001),
/**
* 部署-回滚应用
*/
DEP_ROLLBACK(40002),
private int value;
LogType(int value) {
this.value = value;
}
LogType(String value) {
for (LogType item : values()) {
if (item.name().equals(value)) {
this.value = item.value;
}
}
throw new IllegalArgumentException("Invalid type value");
}
public int value() {
return value;
}
public static LogType valueOf(int value) {
for (LogType item : values()) {
if (item.value() == value) {
return item;
}
}
throw new IllegalArgumentException("Invalid type value");
}
}
日志切点注解:LogInfo.java
用来标记某个方法需要添加日志。
/**
* Description:
* User: RoronoraZoro丶WangRui
* Date: 2018-09-03
* Time: 下午2:19
*/
@Target({ElementType.TYPE,ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface LogInfo {
/**
* 描述
* @return
*/
public String description() default "";
/**
* 日志类型
* @return
*/
public LogType logType();
}
需要被记录日志的某个Controller方法:
@ApiOperation("添加应用")
@Auth(allowRoles = {RoleType.ADMIN, RoleType.MASTER})
@PostMapping("/apps")
@LogInfo(logType = LogType.APP_ADD)
public GenericResult<AppDTO> add(@RequestBody @Valid AddAppRequest request) {
return appService.add(request);
}
为其添加了@LogInfo(logType = LogType.APP_ADD)注解,标记为一个切点,表明了方法类型:LogType.APP_ADD 添加应用。
Log实体类:Log.java
/**
* Description:
* User: RoronoraZoro丶WangRui
* Date: 2018-09-03
* Time: 下午3:27
*/
@Data
public class Log extends BaseEntity{
private int id;
private int type;
private String request = "{}";
private String response = "{}";
private String error = "{}";
}
LogService(及Impl),LogMapper(及xml)这些的代码就不贴了,直接看最关键的切面类:LogAop.java
import cn.hutool.core.util.ReflectUtil;
import com.mistra.api.aspect.annotaion.LogInfo;
import com.mistra.core.service.LogService;
import com.mistra.domain.entity.Log;
import lombok.extern.slf4j.Slf4j;
import net.sf.json.JSONObject;
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.annotation.*;
import org.aspectj.lang.reflect.MethodSignature;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.core.LocalVariableTableParameterNameDiscoverer;
import org.springframework.core.annotation.Order;
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 javax.servlet.http.HttpServletRequest;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.PrintStream;
import java.lang.reflect.Method;
import java.util.HashMap;
import java.util.Map;
/**
* Description: 采集日志
* User: RoronoraZoro丶WangRui
* Date: 2018-09-03
* Time: 下午2:22
*/
@Aspect
@Component
@Slf4j
@Order(Integer.MIN_VALUE)
public class LogAop {
@Autowired
private LogService logService;
private ThreadLocal<Log> threadLocal = new ThreadLocal<>();
@Pointcut("@annotation(com.t4f.eunomia.api.aspect.annotaion.LogInfo)")
public void controllerMethodPointcut() {
}
/**
* 前置advice
* @param point
*/
@Before("controllerMethodPointcut()")
public void before(JoinPoint point) {
Log logEntity = new Log();
//将当前实体保存到threadLocal
threadLocal.set(logEntity);
//获取连接点的方法签名对象,在该对象中可以获取到目标方法名,所属类的Class等信息
MethodSignature signature = (MethodSignature) point.getSignature();
//获取到该方法@LogInfo注解中的日志类型:枚举类LogType的值,保存到log实体中
logEntity.setType(signature.getMethod().getAnnotation(LogInfo.class).logType().value());
//RequestContextHolder:持有上下文的Request容器,获取到当前请求的request
RequestAttributes ra = RequestContextHolder.getRequestAttributes();
ServletRequestAttributes sra = (ServletRequestAttributes) ra;
HttpServletRequest httpServletRequest = sra.getRequest();
JSONObject jsonObject = new JSONObject();
//存储uri到json中
jsonObject.accumulate("uri", httpServletRequest.getRequestURI().toString());
//这一步获取到的方法有可能是代理方法也有可能是真实方法
Method m = ((MethodSignature) point.getSignature()).getMethod();
//判断代理对象本身是否是连接点所在的目标对象,不是的话就要通过反射重新获取真实方法
if (point.getThis().getClass() != point.getTarget().getClass()) {
m = ReflectUtil.getMethod(point.getTarget().getClass(), m.getName(), m.getParameterTypes());
}
//通过真实方法获取该方法的参数名称
LocalVariableTableParameterNameDiscoverer paramNames = new LocalVariableTableParameterNameDiscoverer();
String[] parameterNames = paramNames.getParameterNames(m);
//获取连接点方法运行时的入参列表
Object[] args = point.getArgs();
//将参数名称与入参值一一对应起来
Map<String, Object> params = new HashMap<>();
for (int i = 0; i < parameterNames.length; i++) {
params.put(parameterNames[i], args[i]);
}
jsonObject.accumulate("params", params);
//为log实体类的request字段赋值
logEntity.setRequest(jsonObject.toString());
System.out.println("============================ 》Before : " + logEntity.toString());
}
/**
* 方法成功return之后的advice
* @param point
* @param rtv
*/
@AfterReturning(value = "controllerMethodPointcut()", returning = "rtv")
public void after(JoinPoint point, Object rtv) {
//得到当前线程的log对象
Log log = threadLocal.get();
//rtv为controller方法返回数据
JSONObject jsonObject = JSONObject.fromObject(rtv);
//为log实体的response字段赋值
log.setResponse(jsonObject.toString());
//插入一条log信息
logService.add(threadLocal.get());
//移除当前log实体
threadLocal.remove();
System.out.println("============================ 》AfterReturning : " + log.toString());
}
/**
* 报错之后的advice
* @param throwing
*/
@AfterThrowing(value = "controllerMethodPointcut()", throwing = "throwing")
public void error(Throwable throwing) {
Log log = threadLocal.get();
try (ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream()) {
//将报错信息写入error字段
throwing.printStackTrace(new PrintStream(byteArrayOutputStream));
log.setError(byteArrayOutputStream.toString());
} catch (IOException e) {
e.printStackTrace();
}
logService.add(threadLocal.get());
threadLocal.remove();
System.out.println("============================ 》AfterThrowing : " + log.toString());
}
}
AspectJ中的切入点匹配的执行点称作连接的(Join Point),在通知方法中可以声明一个JoinPoint类型的参数。通过JoinPoint可以访问连接点的细节。下面简要介绍JponPoint的方法:
1.java.lang.Object[] getArgs():获取连接点方法运行时的入参列表;
2.Signature getSignature() :获取连接点的方法签名对象在该对象中可以获取到目标方法名,所属类的Class等信息;
3.java.lang.Object getTarget() :获取连接点所在的目标对象;
4.java.lang.Object getThis() :获取代理对象本身;
持有上下文的Request容器,RequestContextHolder里面有两个ThreadLocal保存当前线程下的request。getRequestAttributes()方法,相当于直接获取ThreadLocal里面的值,这样就保证了每一次获取到的Request是该请求的request。
public Object put (Object key, Object value) 将value映射到key下。如果此JSONObject对象之前存在一个value在这个key下,当前的value会替换掉之前的value。
public JSONObject accumulate (String key, Object value) 追加value到这个key下。这个方法同element()方法类似,特殊的是,如果当前已经存在一个value在这个key下,那么一个JSONArray将会存储在这个key下,来保存所有追加的value。如果已经存在一个JSONArray,那么当前的value就会添加到这个JSONArray中。
public JSONObject element (String key, Object value) 将键值对放到这个JSONObject对象里面。如果当前value为空(null),那么如果这个key存在的话,这个key就会移除掉。如果这个key之前有value值,那么此方法会调用accumulate()方法。
我的: