springboot 實現記錄業務日志和異常業務日志的操作
日志記錄到redis展現形式
1.基於註解的方式實現日志記錄,掃描對應的方法實現日志記錄
@Inherited @Retention(RetentionPolicy.RUNTIME) @Target({ElementType.METHOD}) public @interface BussinessLog { /** * 業務的名稱,例如:"修改菜單" */ String value() default ""; /** * 被修改的實體的唯一標識,例如:菜單實體的唯一標識為"id" */ String key() default "id"; /** * 業務類型 */ String type() default "0"; /** * 字典(用於查找key的中文名稱和字段的中文名稱) */ Class<? extends AbstractDictMap> dict() default SystemDict.class; }
2.掃描的方法,基於註解實現方法掃描並且記錄日志
3.基於@Aspect註解,實現日志掃描,並且記錄日志
import org.aspectj.lang.ProceedingJoinPoint; import org.aspectj.lang.Signature; import org.aspectj.lang.annotation.Around; import org.aspectj.lang.annotation.Aspect; import org.aspectj.lang.annotation.Pointcut; import org.aspectj.lang.reflect.MethodSignature; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.stereotype.Component; import java.lang.reflect.Method; import java.util.Map; /** * 日志記錄 * */ @Aspect @Component public class LogAop { private Logger log = LoggerFactory.getLogger(this.getClass()); @Pointcut(value = "@annotation(com.stylefeng.guns.core.common.annotion.BussinessLog)") public void cutService() { } @Around("cutService()") public Object recordSysLog(ProceedingJoinPoint point) throws Throwable { //先執行業務 Object result = point.proceed(); try { handle(point); } catch (Exception e) { log.error("日志記錄出錯!", e); } return result; } private void handle(ProceedingJoinPoint point) throws Exception { //獲取攔截的方法名 Signature sig = point.getSignature(); MethodSignature msig = null; if (!(sig instanceof MethodSignature)) { throw new IllegalArgumentException("該註解隻能用於方法"); } msig = (MethodSignature) sig; Object target = point.getTarget(); Method currentMethod = target.getClass().getMethod(msig.getName(), msig.getParameterTypes()); String methodName = currentMethod.getName(); //如果當前用戶未登錄,不做日志 ShiroUser user = ShiroKit.getUser(); if (null == user) { return; } //獲取攔截方法的參數 String className = point.getTarget().getClass().getName(); Object[] params = point.getArgs(); //獲取操作名稱 BussinessLog annotation = currentMethod.getAnnotation(BussinessLog.class); String bussinessName = annotation.value(); String key = annotation.key(); Class dictClass = annotation.dict(); StringBuilder sb = new StringBuilder(); for (Object param : params) { sb.append(param); sb.append(" & "); } //如果涉及到修改,比對變化 String msg; if (bussinessName.contains("修改") || bussinessName.contains("編輯")) { Object obj1 = LogObjectHolder.me().get(); Map<String, String> obj2 = HttpContext.getRequestParameters(); msg = Contrast.contrastObj(dictClass, key, obj1, obj2); } else { Map<String, String> parameters = HttpContext.getRequestParameters(); AbstractDictMap dictMap = (AbstractDictMap) dictClass.newInstance(); msg = Contrast.parseMutiKey(dictMap, key, parameters); } log.info("[記錄日志][RESULT:{}]",user.getId()+bussinessName+className+methodName+msg.toString()); LogManager.me().executeLog(LogTaskFactory.bussinessLog(user.getId(), bussinessName, className, methodName, msg)); } }
4.比較兩個對象的工具類
import java.beans.PropertyDescriptor; import java.lang.reflect.Field; import java.lang.reflect.Method; import java.util.Date; import java.util.Map; /** * 對比兩個對象的變化的工具類 * * @author ... * @Date 2017/3/31 10:36 */ public class Contrast { //記錄每個修改字段的分隔符 public static final String separator = ";;;"; /** * 比較兩個對象,並返回不一致的信息 * * @author ... * @Date 2017/5/9 19:34 */ public static String contrastObj(Object pojo1, Object pojo2) { String str = ""; try { Class clazz = pojo1.getClass(); Field[] fields = pojo1.getClass().getDeclaredFields(); int i = 1; for (Field field : fields) { if ("serialVersionUID".equals(field.getName())) { continue; } PropertyDescriptor pd = new PropertyDescriptor(field.getName(), clazz); Method getMethod = pd.getReadMethod(); Object o1 = getMethod.invoke(pojo1); Object o2 = getMethod.invoke(pojo2); if (o1 == null || o2 == null) { continue; } if (o1 instanceof Date) { o1 = DateUtil.getDay((Date) o1); } if (!o1.toString().equals(o2.toString())) { if (i != 1) { str += separator; } str += "字段名稱" + field.getName() + ",舊值:" + o1 + ",新值:" + o2; i++; } } } catch (Exception e) { e.printStackTrace(); } return str; } /** * 比較兩個對象pojo1和pojo2,並輸出不一致信息 * * @author ... * @Date 2017/5/9 19:34 */ public static String contrastObj(Class dictClass, String key, Object pojo1, Map<String, String> pojo2) throws IllegalAccessException, InstantiationException { AbstractDictMap dictMap = (AbstractDictMap) dictClass.newInstance(); String str = parseMutiKey(dictMap, key, pojo2) + separator; try { Class clazz = pojo1.getClass(); Field[] fields = pojo1.getClass().getDeclaredFields(); int i = 1; for (Field field : fields) { if ("serialVersionUID".equals(field.getName())) { continue; } PropertyDescriptor pd = new PropertyDescriptor(field.getName(), clazz); Method getMethod = pd.getReadMethod(); Object o1 = getMethod.invoke(pojo1); Object o2 = pojo2.get(StrKit.firstCharToLowerCase(getMethod.getName().substring(3))); if (o1 == null || o2 == null) { continue; } if (o1 instanceof Date) { o1 = DateUtil.getDay((Date) o1); } else if (o1 instanceof Integer) { o2 = Integer.parseInt(o2.toString()); } if (!o1.toString().equals(o2.toString())) { if (i != 1) { str += separator; } String fieldName = dictMap.get(field.getName()); String fieldWarpperMethodName = dictMap.getFieldWarpperMethodName(field.getName()); if (fieldWarpperMethodName != null) { Object o1Warpper = DictFieldWarpperFactory.createFieldWarpper(o1, fieldWarpperMethodName); Object o2Warpper = DictFieldWarpperFactory.createFieldWarpper(o2, fieldWarpperMethodName); str += "字段名稱:" + fieldName + ",舊值:" + o1Warpper + ",新值:" + o2Warpper; } else { str += "字段名稱:" + fieldName + ",舊值:" + o1 + ",新值:" + o2; } i++; } } } catch (Exception e) { e.printStackTrace(); } return str; } /** * 比較兩個對象pojo1和pojo2,並輸出不一致信息 * * @author ... * @Date 2017/5/9 19:34 */ public static String contrastObjByName(Class dictClass, String key, Object pojo1, Map<String, String> pojo2) throws IllegalAccessException, InstantiationException { AbstractDictMap dictMap = (AbstractDictMap) dictClass.newInstance(); String str = parseMutiKey(dictMap, key, pojo2) + separator; try { Class clazz = pojo1.getClass(); Field[] fields = pojo1.getClass().getDeclaredFields(); int i = 1; for (Field field : fields) { if ("serialVersionUID".equals(field.getName())) { continue; } String prefix = "get"; int prefixLength = 3; if (field.getType().getName().equals("java.lang.Boolean")) { prefix = "is"; prefixLength = 2; } Method getMethod = null; try { getMethod = clazz.getDeclaredMethod(prefix + StrKit.firstCharToUpperCase(field.getName())); } catch (NoSuchMethodException e) { System.err.println("this className:" + clazz.getName() + " is not methodName: " + e.getMessage()); continue; } Object o1 = getMethod.invoke(pojo1); Object o2 = pojo2.get(StrKit.firstCharToLowerCase(getMethod.getName().substring(prefixLength))); if (o1 == null || o2 == null) { continue; } if (o1 instanceof Date) { o1 = DateUtil.getDay((Date) o1); } else if (o1 instanceof Integer) { o2 = Integer.parseInt(o2.toString()); } if (!o1.toString().equals(o2.toString())) { if (i != 1) { str += separator; } String fieldName = dictMap.get(field.getName()); String fieldWarpperMethodName = dictMap.getFieldWarpperMethodName(field.getName()); if (fieldWarpperMethodName != null) { Object o1Warpper = DictFieldWarpperFactory.createFieldWarpper(o1, fieldWarpperMethodName); Object o2Warpper = DictFieldWarpperFactory.createFieldWarpper(o2, fieldWarpperMethodName); str += "字段名稱:" + fieldName + ",舊值:" + o1Warpper + ",新值:" + o2Warpper; } else { str += "字段名稱:" + fieldName + ",舊值:" + o1 + ",新值:" + o2; } i++; } } } catch (Exception e) { e.printStackTrace(); } return str; } /** * 解析多個key(逗號隔開的) * * @author ... * @Date 2017/5/16 22:19 */ public static String parseMutiKey(AbstractDictMap dictMap, String key, Map<String, String> requests) { StringBuilder sb = new StringBuilder(); if (key.indexOf(",") != -1) { String[] keys = key.split(","); for (String item : keys) { String fieldWarpperMethodName = dictMap.getFieldWarpperMethodName(item); String value = requests.get(item); if (fieldWarpperMethodName != null) { Object valueWarpper = DictFieldWarpperFactory.createFieldWarpper(value, fieldWarpperMethodName); sb.append(dictMap.get(item) + "=" + valueWarpper + ","); } else { sb.append(dictMap.get(item) + "=" + value + ","); } } return StrKit.removeSuffix(sb.toString(), ","); } else { String fieldWarpperMethodName = dictMap.getFieldWarpperMethodName(key); String value = requests.get(key); if (fieldWarpperMethodName != null) { Object valueWarpper = DictFieldWarpperFactory.createFieldWarpper(value, fieldWarpperMethodName); sb.append(dictMap.get(key) + "=" + valueWarpper); } else { sb.append(dictMap.get(key) + "=" + value); } return sb.toString(); } } }
5.根據輸入方法獲取數據字典的數據
import java.lang.reflect.Method; public class DictFieldWarpperFactory { public static Object createFieldWarpper(Object parameter, String methodName) { IConstantFactory constantFactory = ConstantFactory.me(); try { Method method = IConstantFactory.class.getMethod(methodName, parameter.getClass()); return method.invoke(constantFactory, parameter); } catch (Exception e) { try { Method method = IConstantFactory.class.getMethod(methodName, Integer.class); return method.invoke(constantFactory, Integer.parseInt(parameter.toString())); } catch (Exception e1) { throw new RuntimeException("BizExceptionEnum.ERROR_WRAPPER_FIELD"); } } } }
6.對應獲取數據字典的方法
public interface IConstantFactory { /** * 獲取狀態 */ String getWordStatus(Integer DATA_STATUS); }
import com.qihoinfo.dev.log.util.SpringContextHolder; import org.anyline.service.AnylineService; import org.springframework.context.annotation.DependsOn; import org.springframework.stereotype.Component; @Component @DependsOn("springContextHolder") public class ConstantFactory implements IConstantFactory { private AnylineService anylineService = SpringContextHolder.getBean(AnylineService.class); public static IConstantFactory me() { return SpringContextHolder.getBean("constantFactory"); } @Override public String getWordStatus(Integer DATA_STATUS) { if ("1".equals(DATA_STATUS.toString())) { return "啟用"; } else { return "停用"; } } }
7.spring根據方法名獲取對應容器中的對象
import org.springframework.beans.BeansException; import org.springframework.context.ApplicationContext; import org.springframework.context.ApplicationContextAware; import org.springframework.stereotype.Component; /** * Spring的ApplicationContext的持有者,可以用靜態方法的方式獲取spring容器中的bean */ @Component public class SpringContextHolder implements ApplicationContextAware { private static ApplicationContext applicationContext; @Override public void setApplicationContext(ApplicationContext applicationContext) throws BeansException { SpringContextHolder.applicationContext = applicationContext; } public static ApplicationContext getApplicationContext() { assertApplicationContext(); return applicationContext; } @SuppressWarnings("unchecked") public static <T> T getBean(String beanName) { assertApplicationContext(); return (T) applicationContext.getBean(beanName); } public static <T> T getBean(Class<T> requiredType) { assertApplicationContext(); return applicationContext.getBean(requiredType); } private static void assertApplicationContext() { if (SpringContextHolder.applicationContext == null) { throw new RuntimeException("applicaitonContext屬性為null,請檢查是否註入瞭SpringContextHolder!"); } } }
8.字符串工具類
/** * 字符串工具類 */ public class StrKit { /** * 首字母變小寫 */ public static String firstCharToLowerCase(String str) { char firstChar = str.charAt(0); if (firstChar >= 'A' && firstChar <= 'Z') { char[] arr = str.toCharArray(); arr[0] += ('a' - 'A'); return new String(arr); } return str; } /** * 首字母變大寫 */ public static String firstCharToUpperCase(String str) { char firstChar = str.charAt(0); if (firstChar >= 'a' && firstChar <= 'z') { char[] arr = str.toCharArray(); arr[0] -= ('a' - 'A'); return new String(arr); } return str; } /** * 去掉指定後綴 */ public static String removeSuffix(String str, String suffix) { if (isEmpty(str) || isEmpty(suffix)) { return str; } if (str.endsWith(suffix)) { return str.substring(0, str.length() - suffix.length()); } return str; } /** * 字符串是否為空,空的定義如下 1、為null <br> * 2、為""<br> */ public static boolean isEmpty(String str) { return str == null || str.length() == 0; } }
import java.io.IOException; import java.io.PrintWriter; import java.io.StringWriter; public class ToolUtil { public static final int SALT_LENGTH = 6; public ToolUtil() { } public static String getExceptionMsg(Throwable e) { StringWriter sw = new StringWriter(); try { e.printStackTrace(new PrintWriter(sw)); } finally { try { sw.close(); } catch (IOException var8) { var8.printStackTrace(); } } return sw.getBuffer().toString().replaceAll("\\$", "T"); } }
9.獲取數據字典的類
import java.util.HashMap; public abstract class AbstractDictMap { protected HashMap<String, String> dictory = new HashMap<>(); protected HashMap<String, String> fieldWarpperDictory = new HashMap<>(); public AbstractDictMap() { put("ID", "主鍵ID"); init(); initBeWrapped(); } public abstract void init(); protected abstract void initBeWrapped(); public String get(String key) { return this.dictory.get(key); } public void put(String key, String value) { this.dictory.put(key, value); } public String getFieldWarpperMethodName(String key) { return this.fieldWarpperDictory.get(key); } public void putFieldWrapperMethodName(String key, String methodName) { this.fieldWarpperDictory.put(key, methodName); } }
public class SystemDict extends AbstractDictMap { @Override public void init() { } @Override protected void initBeWrapped() { } }
public class WordMap extends AbstractDictMap { @Override public void init() { put("EN", "英文"); put("CN", "中文"); put("SHORT", "簡稱"); put("REMARK", "備註"); put("DATA_STATUS", "狀態"); } @Override protected void initBeWrapped() { putFieldWrapperMethodName("DATA_STATUS","getWordStatus"); } }
10.獲取緩存對象的bean
import org.springframework.context.annotation.Scope; import org.springframework.stereotype.Component; import org.springframework.web.context.WebApplicationContext; import java.io.Serializable; @Component @Scope(scopeName = WebApplicationContext.SCOPE_SESSION) public class LogObjectHolder implements Serializable{ private Object object = null; public void set(Object obj) { this.object = obj; } public Object get() { return object; } public static LogObjectHolder me(){ LogObjectHolder bean = SpringContextHolder.getBean(LogObjectHolder.class); return bean; } }
11.運行時異常的獲取
@ControllerAdvice public class GlobalExceptionHandler extends BasicMemberJSONController { private Logger log = LoggerFactory.getLogger(this.getClass()); @ExceptionHandler(RuntimeException.class) @ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR) @ResponseBody public String notFount(RuntimeException e) { String userName = curManage().get("USERNAME").toString(); LogManager.me().executeLog(LogTaskFactory.exceptionLog(userName, e)); log.error("運行時異常:", e); return fail(); } }
12.使用線程池創建操作日志
import java.util.Date; public class LogFactory { /** * 創建操作日志 */ public static DataRow createOperationLog(LogType logType, String userName, String bussinessName, String clazzName, String methodName, String msg, LogSucceed succeed) { DataRow operationLog = new DataRow(); operationLog.put("log_type", logType.getMessage()); operationLog.put("USER_NAME", userName); operationLog.put("log_name", bussinessName); operationLog.put("CLASS_NAME", clazzName); operationLog.put("METHOD", methodName); operationLog.put("CREATE_TIME", new Date()); operationLog.put("SUCCEED", succeed.getMessage()); if (msg.length() > 800) { msg = msg.substring(0, 800); operationLog.put("MESSAGE", msg); } else { operationLog.put("MESSAGE", msg); } return operationLog; } }
import java.util.TimerTask; import java.util.concurrent.ScheduledThreadPoolExecutor; import java.util.concurrent.TimeUnit; public class LogManager { //日志記錄操作延時 private final int OPERATE_DELAY_TIME = 10; //異步操作記錄日志的線程池 private ScheduledThreadPoolExecutor executor = new ScheduledThreadPoolExecutor(10); private LogManager() { } public static LogManager logManager = new LogManager(); public static LogManager me() { return logManager; } public void executeLog(TimerTask task) { executor.schedule(task, OPERATE_DELAY_TIME, TimeUnit.MILLISECONDS); } }
public enum LogSucceed { SUCCESS("成功"), FAIL("失敗"); String message; LogSucceed(String message) { this.message = message; } public String getMessage() { return message; } public void setMessage(String message) { this.message = message; } }
import com.qihoinfo.dev.log.annotation.RedisDb; import com.qihoinfo.dev.log.util.ToolUtil; import org.anyline.entity.DataRow; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.context.annotation.DependsOn; import org.springframework.data.redis.core.StringRedisTemplate; import org.springframework.stereotype.Component; import java.util.TimerTask; @Component @DependsOn("springContextHolder") public class LogTaskFactory { private static Logger logger = LoggerFactory.getLogger(LogManager.class); private static StringRedisTemplate redisTemplate = RedisDb.getMapper(StringRedisTemplate.class); public static TimerTask bussinessLog(final String userName, final String bussinessName, final String clazzName, final String methodName, final String msg) { return new TimerTask() { @Override public void run() { DataRow operationLog = LogFactory.createOperationLog( LogType.BUSSINESS, userName, bussinessName, clazzName, methodName, msg, LogSucceed.SUCCESS); try { redisTemplate.opsForList().rightPush("sys_operation_log", operationLog.getJson()); } catch (Exception e) { logger.error("創建業務日志異常!", e); } } }; } public static TimerTask exceptionLog(final String userName, final Exception exception) { return new TimerTask() { @Override public void run() { String msg = ToolUtil.getExceptionMsg(exception); DataRow operationLog = LogFactory.createOperationLog( LogType.EXCEPTION, userName, "", null, null, msg, LogSucceed.FAIL); try { redisTemplate.opsForList().rightPush("sys_operation_log", operationLog.getJson()); } catch (Exception e) { logger.error("創建異常日志異常!", e); } } }; } }
public enum LogType { EXCEPTION("異常日志"), BUSSINESS("業務日志"); String message; LogType(String message) { this.message = message; } public String getMessage() { return message; } public void setMessage(String message) { this.message = message; } }
13.將日志記錄到redis數據庫
package com.qihoinfo.dev.log.annotation; import com.qihoinfo.dev.log.util.SpringContextHolder; import org.springframework.data.redis.core.StringRedisTemplate; public class RedisDb<T> { private Class<T> clazz; private StringRedisTemplate baseMapper; private RedisDb(Class clazz) { this.clazz = clazz; this.baseMapper = (StringRedisTemplate) SpringContextHolder.getBean(clazz); } public static <T> RedisDb<T> create(Class<T> clazz) { return new RedisDb<T>(clazz); } public StringRedisTemplate getMapper() { return this.baseMapper; } public static <T> T getMapper(Class<T> clazz) { return SpringContextHolder.getBean(clazz); } }
import org.springframework.web.context.request.RequestContextHolder; import org.springframework.web.context.request.ServletRequestAttributes; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import java.util.Enumeration; import java.util.HashMap; import java.util.Map; /** * @Description: * @Auther: wj * @Date: 2019/5/28 13:56 */ public class HttpContext { public HttpContext() { } public static String getIp() { HttpServletRequest request = getRequest(); return request == null ? "127.0.0.1" : request.getRemoteHost(); } public static HttpServletRequest getRequest() { ServletRequestAttributes requestAttributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes(); return requestAttributes == null ? null : requestAttributes.getRequest(); } public static HttpServletResponse getResponse() { ServletRequestAttributes requestAttributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes(); return requestAttributes == null ? null : requestAttributes.getResponse(); } public static Map<String, String> getRequestParameters() { HashMap<String, String> values = new HashMap(); HttpServletRequest request = getRequest(); if (request == null) { return values; } else { Enumeration enums = request.getParameterNames(); while (enums.hasMoreElements()) { String paramName = (String) enums.nextElement(); String paramValue = request.getParameter(paramName); values.put(paramName, paramValue); } return values; } } }
以上為個人經驗,希望能給大傢一個參考,也希望大傢多多支持WalkonNet。