老大喊我記錄下API的操做日誌,省得前端甩鍋,主要記錄新增,修改,刪除等操做。我想了下就決定用AOP來實現這個功能。前端
因爲使用的是SpringBoot,因此首先應該在依賴中引入AOP包。java
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-aop</artifactId>
</dependency>
複製代碼
通常引入了AOP以後,通常不用作其餘特殊配置,也不用加上@EnableAspectJAutoProxy註解。可是它仍有兩個屬性須要咱們注意web
# 至關於@EnableAspectJAutoProxy
spring.aop.auto=true
# 默認使用jdk來實現AOP,若是想要使用CGLIB的話,這裏要改爲true
spring.aop.proxy-target-class=false
複製代碼
我想要記錄某個API對模塊進行了什麼操做,操做的key,對於修改,刪除來講咱們記錄id,對於新增來講,最開始沒有id,咱們記錄name便可(也能夠是其餘屬性),因此OpLog註解是這樣的spring
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.METHOD})
@Documented
public @interface OpLog {
// 模塊
String module();
// 類型
String opType();
// 級別,默認通常
String level() default OpLevel.COMMON;
// 須要記錄的key
String key();
}
複製代碼
爲了讓這個功能更加易用,因此我要求其餘開發者必須在Controller上加上註解@OpLog,爲何是Controller呢?由於API是在Controller上,我並不關心具體業務,我只要記錄對應的操做便可。在Controller層你只須要這要作就好了springboot
@RestController
@RequestMapping("/user")
public class CpGroupController {
// 注意這裏的userParam和@RequestBody的userParam名字必須要要一致
@OpLog(module = "用戶管理", opType = "新增", key = "userParam.name")
@PostMapping("/add")
public BaseResponse<Boolean> add(@RequestBody UserParam userParam) {
return ResponseUtil.success(userService.add(userParam));
}
}
複製代碼
接下來就是切面代碼的編寫了,其中咱們要記錄訪問的url以及必要的操做信息。bash
@Component
@Aspect
@Slf4j
public class OpLogAspect {
@Autowired
private OperateLogService operateLogService;
int paramNameIndex = 0;
int propertyIndexFrom = 1;
@Pointcut("execution(public * com.generalthink.springboot.web.controller..*.*(..))")
public void webLogPointcut() {
}
@Around(value = "webLogPointcut() && @annotation(opLog)", argNames = "joinPoint,opLog")
public Object around(ProceedingJoinPoint joinPoint, OpLog opLog) throws Throwable {
Object objReturn = joinPoint.proceed();
try {
OperateLog operationLog = generateOperateLog(joinPoint, opLog);
operateLogService.save(operationLog);
} catch (Exception e) {
log.error("operateLog record error", e);
}
return objReturn;
}
private OperateLog generateOperateLog(ProceedingJoinPoint joinPoint, OpLog opLog) {
String requestUrl = extractRequestUrl();
Object recordKey = getOpLogRecordKey(joinPoint, opLog);
return OperateLog.builder()
.module(opLog.module())
.opType(opLog.opType())
.level(opLog.level())
.operateTimeUnix(CommonUtils.getNowTimeUnix())
.recordKey(recordKey != null ? recordKey.toString() : null)
.url(requestUrl)
.operator(getCurrentUser())
.build();
}
// 獲取當前用戶
private String getCurrentUser() {
Subject subject = SecurityUtils.getSubject();
return (String) subject.getPrincipal();
}
// 獲取想要記錄的key
private Object getOpLogRecordKey(ProceedingJoinPoint joinPoint, OpLog opLog) {
String key = opLog.key();
if(StringUtils.isEmpty(key)) {
return null;
}
String[] keys = key.split("\\.");
//入參 value
Object[] args = joinPoint.getArgs();
CodeSignature codeSignature = (CodeSignature) joinPoint.getSignature();
// 獲取Controller方法上的參數名稱
String[] paramNames = codeSignature.getParameterNames();
Object paramArg = null;
for (int i = 0; i < paramNames.length; i++) {
String paramName = paramNames[i];
if (paramName.equals(keys[paramNameIndex])) {
paramArg = args[i];
break;
}
}
if(keys.length == 1) {
return paramArg;
}
// 獲取參數的值
Object paramValue = null;
if(null != paramArg) {
try {
paramValue = getParamValue(paramArg, keys, propertyIndexFrom);
} catch (IllegalAccessException e) {
log.error("parse field error", e);
}
}
return paramValue;
}
public Object getParamValue(Object param, String[] keys, int idx) throws IllegalAccessException {
Optional<Field> fieldOptional = getAllFields(new ArrayList<>(), param.getClass())
.stream()
.filter(field -> field.getName().equalsIgnoreCase(keys[idx]))
.findFirst();
if(!fieldOptional.isPresent()) {
return null;
}
// 設置屬性可訪問,由於bean當中屬性通常都是private
Field field = fieldOptional.get();
field.setAccessible(true);
if(idx + 1 == keys.length) {
return field.get(param);
}
return getParamValue(field.get(param), keys, idx + 1);
}
// 遞歸獲取全部Field
public List<Field> getAllFields(List<Field> fieldList, Class<?> type) {
// 獲取當前類的全部Field
fieldList.addAll(Arrays.asList(type.getDeclaredFields()));
// 獲取父類的全部Field
Class<?> superClass = type.getSuperclass();
if(superClass != null && superClass != Object.class) {
getAllFields(fieldList,superClass);
}
return fieldList;
}
// 獲取訪問URL
private String extractRequestUrl() {
ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder
.getRequestAttributes();
HttpServletRequest request = attributes.getRequest();
return request.getRequestURL().toString();
}
}
複製代碼
主要就是記錄了操做了什麼模塊,操做內容等等,對於修改和刪除操做咱們能夠記錄id,可是對於save操做,咱們沒有辦法記錄id,只能記錄其餘屬性,好比說name,就能夠記錄保存的數據了app
{
name: "zhangsan",
age: 20
}
複製代碼
對於上面的數據就會把"zhangsan"這個值保存下來,後面就能夠經過查詢日誌表知道是誰操做的,修改和刪除也是一樣的.spring-boot