SpringAop實(shí)現(xiàn)操作日志記錄
前言
大家好,這里是經(jīng)典雞翅,今天給大家?guī)?lái)一篇基于SpringAop實(shí)現(xiàn)的操作日志記錄的解決的方案。大家可能會(huì)說(shuō),切,操作日志記錄這么簡(jiǎn)單的東西,老生常談了。不!
網(wǎng)上的操作日志一般就是記錄操作人,操作的描述,ip等。好一點(diǎn)的增加了修改的數(shù)據(jù)和執(zhí)行時(shí)間。那么!我這篇有什么不同呢!今天這種不僅可以記錄上方所說(shuō)的一切,還增加記錄了操作前的數(shù)據(jù),錯(cuò)誤的信息,堆棧信息等。正文開(kāi)始~~~~~
思路介紹
記錄操作日志的操作前數(shù)據(jù)是需要思考的重點(diǎn)。我們以修改場(chǎng)景來(lái)作為探討。當(dāng)我們要完全記錄數(shù)據(jù)的流向的時(shí)候,我們必然要記錄修改前的數(shù)據(jù),而前臺(tái)進(jìn)行提交的時(shí)候,只有修改的數(shù)據(jù),那么如何找到修改前的數(shù)據(jù)呢。有三個(gè)大的要素,我們需要知道修改前數(shù)據(jù)的表名,表的字段主鍵,表主鍵的值。這樣通過(guò)這三個(gè)屬性,我們可以很容易的拼出 select * from 表名 where 主鍵字段 = 主鍵值。我們就獲得了修改前的數(shù)據(jù),轉(zhuǎn)換為json之后就可以存入到數(shù)據(jù)庫(kù)中了。如何獲取三個(gè)屬性就是重中之重了。我們采取的方案是通過(guò)提交的映射實(shí)體,在實(shí)體上打上注解,根據(jù) Java 的反射取到值。再進(jìn)一步拼裝獲得對(duì)象數(shù)據(jù)。那么AOP是在哪里用的呢,我們需要在記錄操作日志的方法上,打上注解,再通過(guò)切面獲取到切點(diǎn),一切的數(shù)據(jù)都通過(guò)反射來(lái)進(jìn)行獲得。
定義操作日志注解
既然是基于spinrg的aop實(shí)現(xiàn)切面。那么必然是需要一個(gè)自定義注解的。用來(lái)作為切點(diǎn)。我們定義的注解,可以帶一些必要的屬性,例如操作的描述,操作的類型。操作的類型需要說(shuō)一下,我們分為新增、修改、刪除、查詢。那么只有修改和刪除的時(shí)候,我們需要查詢一下修改前的數(shù)據(jù)。其他兩種是不需要的,這個(gè)也可以用來(lái)作為判斷。
@Target({ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface OperateLog {
String operation() default "";
String operateType() default "";
}
定義用于找到表和表主鍵的注解
表和表主鍵的注解打在實(shí)體上,內(nèi)部有兩個(gè)屬性 tableName 和 idName。這兩個(gè)屬性的值獲得后,可以進(jìn)行拼接 select * from 表名 where 主鍵字段。
@Target({ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface SelectTable {
String tableName() default "";
String idName() default "";
}
定義獲取主鍵值的注解
根據(jù)上面所說(shuō)的三個(gè)元素,我們還缺最后一個(gè)元素主鍵值的獲取,用于告訴我們,我們應(yīng)該從提交的請(qǐng)求的那個(gè)字段,拿到其中的值。
@Target({ElementType.FIELD})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface SelectPrimaryKey {
}
注解的總結(jié)
有了上面的三個(gè)注解,注解的準(zhǔn)備工作已經(jīng)進(jìn)行完畢。我們通過(guò)反射取到數(shù)據(jù),可以獲得一切。接下來(lái)開(kāi)始實(shí)現(xiàn)切面,對(duì)于注解的值進(jìn)行拼接處理,最終存入到我們的數(shù)據(jù)庫(kù)操作日志表中。
切面的實(shí)現(xiàn)
對(duì)于切面來(lái)說(shuō),我們需要實(shí)現(xiàn)切點(diǎn)、數(shù)據(jù)庫(kù)的插入、反射的數(shù)據(jù)獲取。我們先分開(kāi)進(jìn)行解釋,最后給出全面的實(shí)現(xiàn)代碼。方便大家的理解和學(xué)習(xí)。
切面的定義
基于spring的aspect進(jìn)行聲明這是一個(gè)切面。
@Aspect
@Component
public class OperateLogAspect {
}
切點(diǎn)的定義
切點(diǎn)就是對(duì)所有的打上OperateLog的注解的請(qǐng)求進(jìn)行攔截和加強(qiáng)。我們使用annotation進(jìn)行攔截。
@Pointcut("@annotation(com.jichi.aop.operateLog.OperateLog)")
private void operateLogPointCut(){
}
獲取請(qǐng)求ip的共用方法
private String getIp(HttpServletRequest request){
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("WL-Proxy-Client-IP");
}
if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) {
ip = request.getHeader("HTTP_CLIENT_IP");
}
if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) {
ip = request.getHeader("HTTP_X_FORWARDED_FOR");
}
if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) {
ip = request.getRemoteAddr();
}
return ip;
}
數(shù)據(jù)庫(kù)的日志插入操作
我們將插入數(shù)據(jù)庫(kù)的日志操作進(jìn)行單獨(dú)的抽取。
private void insertIntoLogTable(OperateLogInfo operateLogInfo){
operateLogInfo.setId(UUID.randomUUID().toString().replace("-",""));
String sql="insert into log values(?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)";
jdbcTemplate.update(sql,operateLogInfo.getId(),operateLogInfo.getUserId(),
operateLogInfo.getUserName(),operateLogInfo.getOperation(),operateLogInfo.getMethod(),
operateLogInfo.getModifiedData(),operateLogInfo.getPreModifiedData(),
operateLogInfo.getResult(),operateLogInfo.getErrorMessage(),operateLogInfo.getErrorStackTrace(),
operateLogInfo.getExecuteTime(),operateLogInfo.getDuration(),operateLogInfo.getIp(),
operateLogInfo.getModule(),operateLogInfo.getOperateType());
}
環(huán)繞通知的實(shí)現(xiàn)
日志的實(shí)體類實(shí)現(xiàn)
@TableName("operate_log")
@Data
public class OperateLogInfo {
//主鍵id
@TableId
private String id;
//操作人id
private String userId;
//操作人名稱
private String userName;
//操作內(nèi)容
private String operation;
//操作方法名稱
private String method;
//操作后的數(shù)據(jù)
private String modifiedData;
//操作前數(shù)據(jù)
private String preModifiedData;
//操作是否成功
private String result;
//報(bào)錯(cuò)信息
private String errorMessage;
//報(bào)錯(cuò)堆棧信息
private String errorStackTrace;
//開(kāi)始執(zhí)行時(shí)間
private Date executeTime;
//執(zhí)行持續(xù)時(shí)間
private Long duration;
//ip
private String ip;
//操作類型
private String operateType;
}
準(zhǔn)備工作全部完成。接下來(lái)的重點(diǎn)是對(duì)環(huán)繞通知的實(shí)現(xiàn)。思路分為數(shù)據(jù)處理、異常捕獲、finally執(zhí)行數(shù)據(jù)庫(kù)插入操作。環(huán)繞通知的重點(diǎn)類就是ProceedingJoinPoint ,我們通過(guò)它的getSignature方法可以獲取到打在方法上注解的值。例如下方。
MethodSignature signature = (MethodSignature) pjp.getSignature(); OperateLog declaredAnnotation = signature.getMethod().getDeclaredAnnotation(OperateLog.class); operateLogInfo.setOperation(declaredAnnotation.operation()); operateLogInfo.setModule(declaredAnnotation.module()); operateLogInfo.setOperateType(declaredAnnotation.operateType()); //獲取執(zhí)行的方法 String method = signature.getDeclaringType().getName() + "." + signature.getName(); operateLogInfo.setMethod(method); String operateType = declaredAnnotation.operateType();
獲取請(qǐng)求的數(shù)據(jù),也是通過(guò)這個(gè)類來(lái)實(shí)現(xiàn),這里有一點(diǎn)是需要注意的,就是我們要約定參數(shù)的傳遞必須是第一個(gè)參數(shù)。這樣才能保證我們?nèi)〉降臄?shù)據(jù)是提交的數(shù)據(jù)。
if(pjp.getArgs().length>0){
Object args = pjp.getArgs()[0];
operateLogInfo.setModifiedData(new Gson().toJson(args));
}
接下來(lái)的一步就是對(duì)修改前的數(shù)據(jù)進(jìn)行拼接。之前我們提到過(guò)如果是修改和刪除,我們才會(huì)進(jìn)行數(shù)據(jù)的拼接獲取,主要是通過(guò)類來(lái)判斷書(shū)否存在注解,如果存在注解,那么就要判斷注解上的值是否是控制或者,非空才能正確的進(jìn)行拼接。取field的值的時(shí)候,要注意私有的變量需要通過(guò)setAccessible(true)才可以進(jìn)行訪問(wèn)。
if(GlobalStaticParas.OPERATE_MOD.equals(operateType) ||
GlobalStaticParas.OPERATE_DELETE.equals(operateType)){
String tableName = "";
String idName = "";
String selectPrimaryKey = "";
if(pjp.getArgs().length>0){
Object args = pjp.getArgs()[0];
//獲取操作前的數(shù)據(jù)
boolean selectTableFlag = args.getClass().isAnnotationPresent(SelectTable.class);
if(selectTableFlag){
tableName = args.getClass().getAnnotation(SelectTable.class).tableName();
idName = args.getClass().getAnnotation(SelectTable.class).idName();
}else {
throw new RuntimeException("操作日志類型為修改或刪除,實(shí)體類必須指定表面和主鍵注解!");
}
Field[] fields = args.getClass().getDeclaredFields();
Field[] fieldsCopy = fields;
boolean isFindField = false;
int fieldLength = fields.length;
for(int i = 0; i < fieldLength; ++i) {
Field field = fieldsCopy[i];
boolean hasPrimaryField = field.isAnnotationPresent(SelectPrimaryKey.class);
if (hasPrimaryField) {
isFindField = true;
field.setAccessible(true);
selectPrimaryKey = (String)field.get(args);
}
}
if(!isFindField){
throw new RuntimeException("實(shí)體類必須指定主鍵屬性!");
}
}
if(StringUtils.isNotEmpty(tableName) &&
StringUtils.isNotEmpty(idName)&&
StringUtils.isNotEmpty(selectPrimaryKey)){
StringBuffer sb = new StringBuffer();
sb.append(" select * from ");
sb.append(tableName);
sb.append(" where ");
sb.append(idName);
sb.append(" = ? ");
String sql = sb.toString();
try{
List<Map<String, Object>> maps = jdbcTemplate.queryForList(sql, selectPrimaryKey);
if(maps!=null){
operateLogInfo.setPreModifiedData(new Gson().toJson(maps));
}
}catch (Exception e){
e.printStackTrace();
throw new RuntimeException("查詢操作前數(shù)據(jù)出錯(cuò)!");
}
}else {
throw new RuntimeException("表名、主鍵名或主鍵值 存在空值情況,請(qǐng)核實(shí)!");
}
}else{
operateLogInfo.setPreModifiedData("");
}
切面的完整實(shí)現(xiàn)代碼
@Aspect
@Component
public class OperateLogAspect {
@Autowired
private JdbcTemplate jdbcTemplate;
@Pointcut("@annotation(com.jichi.aop.operateLog.OperateLog)")
private void operateLogPointCut(){
}
@Around("operateLogPointCut()")
public Object around(ProceedingJoinPoint pjp) throws Throwable {
Object responseObj = null;
OperateLogInfo operateLogInfo = new OperateLogInfo();
String flag = "success";
try{
HttpServletRequest request = SpringContextUtil.getHttpServletRequest();
DomainUserDetails currentUser = SecurityUtils.getCurrentUser();
if(currentUser!=null){
operateLogInfo.setUserId(currentUser.getId());
operateLogInfo.setUserName(currentUser.getUsername());
}
MethodSignature signature = (MethodSignature) pjp.getSignature();
OperateLog declaredAnnotation = signature.getMethod().getDeclaredAnnotation(OperateLog.class);
operateLogInfo.setOperation(declaredAnnotation.operation());
operateLogInfo.setModule(declaredAnnotation.module());
operateLogInfo.setOperateType(declaredAnnotation.operateType());
//獲取執(zhí)行的方法
String method = signature.getDeclaringType().getName() + "." + signature.getName();
operateLogInfo.setMethod(method);
String operateType = declaredAnnotation.operateType();
if(pjp.getArgs().length>0){
Object args = pjp.getArgs()[0];
operateLogInfo.setModifiedData(new Gson().toJson(args));
}
if(GlobalStaticParas.OPERATE_MOD.equals(operateType) ||
GlobalStaticParas.OPERATE_DELETE.equals(operateType)){
String tableName = "";
String idName = "";
String selectPrimaryKey = "";
if(pjp.getArgs().length>0){
Object args = pjp.getArgs()[0];
//獲取操作前的數(shù)據(jù)
boolean selectTableFlag = args.getClass().isAnnotationPresent(SelectTable.class);
if(selectTableFlag){
tableName = args.getClass().getAnnotation(SelectTable.class).tableName();
idName = args.getClass().getAnnotation(SelectTable.class).idName();
}else {
throw new RuntimeException("操作日志類型為修改或刪除,實(shí)體類必須指定表面和主鍵注解!");
}
Field[] fields = args.getClass().getDeclaredFields();
Field[] fieldsCopy = fields;
boolean isFindField = false;
int fieldLength = fields.length;
for(int i = 0; i < fieldLength; ++i) {
Field field = fieldsCopy[i];
boolean hasPrimaryField = field.isAnnotationPresent(SelectPrimaryKey.class);
if (hasPrimaryField) {
isFindField = true;
field.setAccessible(true);
selectPrimaryKey = (String)field.get(args);
}
}
if(!isFindField){
throw new RuntimeException("實(shí)體類必須指定主鍵屬性!");
}
}
if(StringUtils.isNotEmpty(tableName) &&
StringUtils.isNotEmpty(idName)&&
StringUtils.isNotEmpty(selectPrimaryKey)){
StringBuffer sb = new StringBuffer();
sb.append(" select * from ");
sb.append(tableName);
sb.append(" where ");
sb.append(idName);
sb.append(" = ? ");
String sql = sb.toString();
try{
List<Map<String, Object>> maps = jdbcTemplate.queryForList(sql, selectPrimaryKey);
if(maps!=null){
operateLogInfo.setPreModifiedData(new Gson().toJson(maps));
}
}catch (Exception e){
e.printStackTrace();
throw new RuntimeException("查詢操作前數(shù)據(jù)出錯(cuò)!");
}
}else {
throw new RuntimeException("表名、主鍵名或主鍵值 存在空值情況,請(qǐng)核實(shí)!");
}
}else{
operateLogInfo.setPreModifiedData("");
}
//操作時(shí)間
Date beforeDate = new Date();
Long startTime = beforeDate.getTime();
operateLogInfo.setExecuteTime(beforeDate);
responseObj = pjp.proceed();
Date afterDate = new Date();
Long endTime = afterDate.getTime();
Long duration = endTime - startTime;
operateLogInfo.setDuration(duration);
operateLogInfo.setIp(getIp(request));
operateLogInfo.setResult(flag);
}catch (RuntimeException e){
throw new RuntimeException(e);
}catch (Exception e){
flag = "fail";
operateLogInfo.setResult(flag);
operateLogInfo.setErrorMessage(e.getMessage());
operateLogInfo.setErrorStackTrace(e.getStackTrace().toString());
e.printStackTrace();
}finally {
insertIntoLogTable(operateLogInfo);
}
return responseObj;
}
private void insertIntoLogTable(OperateLogInfo operateLogInfo){
operateLogInfo.setId(UUID.randomUUID().toString().replace("-",""));
String sql="insert into energy_log values(?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)";
jdbcTemplate.update(sql,operateLogInfo.getId(),operateLogInfo.getUserId(),
operateLogInfo.getUserName(),operateLogInfo.getOperation(),operateLogInfo.getMethod(),
operateLogInfo.getModifiedData(),operateLogInfo.getPreModifiedData(),
operateLogInfo.getResult(),operateLogInfo.getErrorMessage(),operateLogInfo.getErrorStackTrace(),
operateLogInfo.getExecuteTime(),operateLogInfo.getDuration(),operateLogInfo.getIp(),
operateLogInfo.getModule(),operateLogInfo.getOperateType());
}
private String getIp(HttpServletRequest request){
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("WL-Proxy-Client-IP");
}
if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) {
ip = request.getHeader("HTTP_CLIENT_IP");
}
if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) {
ip = request.getHeader("HTTP_X_FORWARDED_FOR");
}
if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) {
ip = request.getRemoteAddr();
}
return ip;
}
}
示例的使用方式
針對(duì)于示例來(lái)說(shuō)我們要在controller上面打上操作日志的注解。
@PostMapping("/updateInfo")
@OperateLog(operation = "修改信息",operateType = GlobalStaticParas.OPERATE_MOD)
public void updateInfo(@RequestBody Info info) {
service.updateInfo(info);
}
針對(duì)于Info的實(shí)體類,我們則要對(duì)其中的字段和表名進(jìn)行標(biāo)識(shí)。
@Data
@SelectTable(tableName = "info",idName = "id")
public class Info {
@SelectPrimaryKey
private String id;
private String name;
}
總結(jié)
文章寫(xiě)到這,也就結(jié)束了,文中難免有不足,歡迎大家批評(píng)指正
以上就是SpringAop實(shí)現(xiàn)操作日志記錄的詳細(xì)內(nèi)容,更多關(guān)于SpringAop 操作日志記錄的資料請(qǐng)關(guān)注腳本之家其它相關(guān)文章!
- SpringBoot使用AOP實(shí)現(xiàn)統(tǒng)一角色權(quán)限校驗(yàn)
- Spring AOP實(shí)現(xiàn)功能權(quán)限校驗(yàn)功能的示例代碼
- SpringBoot中使用AOP實(shí)現(xiàn)日志記錄功能
- SpringBoot使用AOP實(shí)現(xiàn)日志記錄功能詳解
- Spring AOP如何自定義注解實(shí)現(xiàn)審計(jì)或日志記錄(完整代碼)
- 在springboot中使用AOP進(jìn)行全局日志記錄
- Spring AOP實(shí)現(xiàn)復(fù)雜的日志記錄操作(自定義注解)
- springMVC自定義注解,用AOP來(lái)實(shí)現(xiàn)日志記錄的方法
- 使用Spring AOP做接口權(quán)限校驗(yàn)和日志記錄
相關(guān)文章
java執(zhí)行SQL語(yǔ)句實(shí)現(xiàn)查詢的通用方法詳解
這篇文章主要介紹了java執(zhí)行SQL語(yǔ)句實(shí)現(xiàn)查詢的通用方法詳解,具有一定借鑒價(jià)值,需要的朋友可以參考下。2017-12-12
IntelliJ IDEA中ajax開(kāi)發(fā)實(shí)現(xiàn)分頁(yè)查詢示例
這篇文章主要介紹了IntelliJ IDEA中ajax開(kāi)發(fā)實(shí)現(xiàn)分頁(yè)查詢,小編覺(jué)得挺不錯(cuò)的,現(xiàn)在分享給大家,也給大家做個(gè)參考。一起跟隨小編過(guò)來(lái)看看吧2018-03-03
淺談Java序列化和反序列化為何要實(shí)現(xiàn)Serializable接口
這篇文章主要介紹了淺談Java序列化和反序列化為何要實(shí)現(xiàn)Serializable接口,序列化最重要的作用是在傳遞和保存對(duì)象時(shí).保證對(duì)象的完整性和可傳遞性,對(duì)象轉(zhuǎn)換為有序字節(jié)流,以便在網(wǎng)絡(luò)上傳輸或者保存在本地文件中,需要的朋友可以參考下2023-12-12

