spring boot log4j2 自定義級別日誌並存儲,超詳細
由於需要一些業務日誌,本來是用的註解,然後用spring aop獲取註解的形式來記錄,但是由於最開始的時候沒有統一controller 方法的引數,引數資料,細緻到id不太好記錄。於是想到了log4j的形式儲存資料庫,但log4j的形式記錄會記錄所有級別的日誌,即使指定日誌級別,其他框架裡面的同級別日誌也會記錄,很混亂。於是想到了自定義級別來記錄儲存,這樣就解決了其他框架同級別的日誌不會同時儲存,前臺要確定其他的框架沒有相同級別的日誌。比如我自定義的級別是OPERATING 級別是350
controller方法程式碼:
@RequestMapping("/exportPage/") @ResponseBody @MethodLog public void exportPage(HttpServletRequest request,HttpServletResponse response,@RequestParam("page")Integer page,@RequestParam("startDate")String startDate,@RequestParam("endDate")String endDate) throws Exception{ Page<FinanceIncomeEntity> p=new Page<FinanceIncomeEntity>(); p.setCurrentPage(page); ExcelUtils.setResponse(response, service.exportPage(p,startDate,endDate)); logger.log(OPERATING, "匯出財務報表-收益表"); }
@MethodLog是我自定義的註解,用於設定一個公共的資料來進行log4j2儲存,比如沒個業務操作都用登入使用者資訊,不用每次都在logger.log()方法裡面寫
程式碼:
package com.pinyu.system.global.ann; 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.METHOD, ElementType.TYPE }) @Retention(RetentionPolicy.RUNTIME) @Documented public @interface MethodLog { /** * 該註解作用於方法上時需要備註資訊 */ String remark() default ""; String operType() default ""; }
獲取@MethodLog註解是用的spring aop的動態代理 Aspect切面
程式碼:
package com.pinyu.system.global.aspect; import java.lang.reflect.Method; import javax.servlet.http.HttpServletRequest; import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; import org.apache.logging.log4j.ThreadContext; import org.aspectj.lang.ProceedingJoinPoint; import org.aspectj.lang.annotation.Around; import org.aspectj.lang.annotation.Aspect; import org.aspectj.lang.annotation.Pointcut; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Component; import org.springframework.web.context.request.RequestContextHolder; import org.springframework.web.context.request.ServletRequestAttributes; import com.pinyu.system.dao.OperationLogDao; import com.pinyu.system.entity.UserEntity; import com.pinyu.system.global.GlobalConstants; import com.pinyu.system.global.ann.MethodLog; import com.pinyu.system.utils.SessionUtils; /** * @author ypp 建立時間:2018年10月9日 上午10:37:34 * @Description: TODO(日誌切面) */ @Component @Aspect public class OperationLogAspect { protected Logger logger = LogManager.getLogger(OperationLogAspect.class); @Autowired private OperationLogDao logDao; public OperationLogAspect() { logger.info("使用者操作日誌"); } /** * 切點 */ @Pointcut("@annotation(com.pinyu.system.global.ann.MethodLog)") public void methodCachePointcut() { } /** * 切面 * * @param point * @return * @throws Throwable */ @Around("methodCachePointcut()") public Object around(ProceedingJoinPoint point) throws Throwable { HttpServletRequest request = ((ServletRequestAttributes) RequestContextHolder.getRequestAttributes()) .getRequest(); String ip = getIp(request); UserEntity loginUser = SessionUtils.getLoginUser(request); ThreadContext.put("userId",String.valueOf(loginUser.getId())); ThreadContext.put("creater", loginUser.getRealName()); ThreadContext.put("createUserName", loginUser.getUserName()); ThreadContext.put("ip", ip); ThreadContext.put("type", GlobalConstants.LogType.OPERATING.getType()); Object object = point.proceed(); return object; } /** * 獲取請求ip * * @param request * @return */ public static 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; } /** * 獲取方法中的中文備註 * * @param joinPoint * @return * @throws Exception */ public static String getMthodRemark(ProceedingJoinPoint joinPoint) throws Exception { String targetName = joinPoint.getTarget().getClass().getName(); String methodName = joinPoint.getSignature().getName(); Object[] arguments = joinPoint.getArgs(); Class targetClass = Class.forName(targetName); Method[] method = targetClass.getMethods(); String methode = ""; for (Method m : method) { if (m.getName().equals(methodName)) { Class[] tmpCs = m.getParameterTypes(); if (tmpCs.length == arguments.length) { MethodLog methodCache = m.getAnnotation(MethodLog.class); if (methodCache != null) { methode = methodCache.remark(); } break; } } } return methode; } }
ThreadContext.put("userId",String.valueOf(loginUser.getId())); ThreadContext.put("creater", loginUser.getRealName()); ThreadContext.put("createUserName", loginUser.getUserName()); ThreadContext.put("ip", ip); ThreadContext.put("type", GlobalConstants.LogType.OPERATING.getType());
這幾行程式碼是設定公共資訊,使用者的資訊,type是日誌的一個分類型別,自己根據自己業務需要決定就行
比如:ThreadContext.put("userId",String.valueOf(loginUser.getId()));
在配置檔案取出來就是:<Column name="user_id" pattern="%X{userId}" />
log4j2.xml配置:
<?xml version="1.0" encoding="UTF-8"?>
<!--日誌級別以及優先順序排序: OFF > FATAL > ERROR > WARN > INFO > DEBUG > TRACE > ALL -->
<!--Configuration後面的status,這個用於設定log4j2自身內部的資訊輸出,可以不設定,當設定成trace時,你會看到log4j2內部各種詳細輸出 -->
<!--monitorInterval:Log4j能夠自動檢測修改配置 檔案和重新配置本身,設定間隔秒數 -->
<Configuration status="WARN" monitorInterval="30">
<!--全域性屬性 -->
<Properties>
<Property name="LOG_FILE_PATH">D:/apache-tomcat-8.5.33/logs</Property>
<!-- linux日誌存放路徑 <Property name="LOG_FILE_PATH">/usr/logs</Property> -->
<Property name="PATTERN_FORMAT">%d{yyyy-MM-dd HH:mm:ss} %-5level %class{36} %L
%M - %msg%xEx%n</Property>
</Properties>
<CustomLevels>
<CustomLevel name="OPERATING" intLevel="350" />
</CustomLevels>
<Appenders>
<!--輸出到控制檯 -->
<Console name="Console" target="SYSTEM_OUT">
<PatternLayout pattern="${PATTERN_FORMAT}" />
</Console>
<!--輸出到檔案 用來定義超過指定大小自動刪除舊的建立新的的Appender. -->
<RollingFile name="RollingInfoFile" fileName="${LOG_FILE_PATH}/pinyu.log"
filePattern="${LOG_FILE_PATH}/$${date:yyyyMM}/info-%d{yyyyMMdd}-%i.log.gz">
<!--控制檯只輸出level及以上級別的資訊(onMatch),其他的直接拒絕(onMismatch) -->
<Filters>
<ThresholdFilter level="warn" onMatch="DENY"
onMismatch="NEUTRAL" />
<ThresholdFilter level="debug" onMatch="ACCEPT"
onMismatch="DENY" />
</Filters>
<PatternLayout>
<pattern>${PATTERN_FORMAT}</pattern>
</PatternLayout>
<Policies>
<!-- rollover on startup, daily and when the file reaches 10 MegaBytes -->
<OnStartupTriggeringPolicy />
<SizeBasedTriggeringPolicy size="100 MB" />
<TimeBasedTriggeringPolicy />
</Policies>
</RollingFile>
<!--儲存到資料庫配置檔案 -->
<JDBC name="DatabaseAppender" tableName="sys_logs">
<ConnectionFactory
class="com.pinyu.system.global.config.log4j2.ConnectionFactory"
method="getDatabaseConnection" />
<!-- 方法名 -->
<Column name="function" pattern="%M" />
<!-- 日誌級別 -->
<Column name="level" pattern="%level" />
<Column name="logger" pattern="%logger" />
<!-- 類 -->
<Column name="class" pattern="%C" />
<!-- 時間 -->
<Column name="create_date" pattern="%d{yyyy-MM-dd hh:mm:ss}" />
<!-- 日誌內容 -->
<Column name="message" pattern="%message" />
<Column name="user_id" pattern="%X{userId}" />
<Column name="creater" pattern="%X{creater}" />
<Column name="ip" pattern="%X{ip}" />
<Column name="create_user_name" pattern="%X{createUserName}" />
<Column name="type" pattern="%X{type}" />
</JDBC>
</Appenders>
<Loggers>
<!--過濾掉spring和mybatis的一些無用的DEBUG資訊 -->
<Logger name="org.springframework" level="INFO" />
<Logger name="org.mybatis" level="INFO" />
<!-- LOG "com.luis*" at TRACE level -->
<Logger name="com.luis" level="INFO" />
<!-- LOG everything at INFO level -->
<Root level="ALL">
<AppenderRef ref="Console" />
<AppenderRef ref="RollingInfoFile" />
<AppenderRef ref="RollingWarnFile" />
<AppenderRef ref="RollingErrorFile" />
<AppenderRef ref="DatabaseAppender" level="operating"/>
</Root>
<!-- <Root level="OPERATION"> -->
<!-- </Root> -->
</Loggers>
</Configuration>
<CustomLevels> <CustomLevel name="OPERATING" intLevel="350" /> </CustomLevels>
這幾行程式碼是自定義級別
<JDBC name="DatabaseAppender" tableName="sys_logs"> <ConnectionFactory class="com.pinyu.system.global.config.log4j2.ConnectionFactory" method="getDatabaseConnection" />
</JDBC>
這裡面的資訊解釋:
name就是一個名字,在下面ROOT標籤裡面的<AppenderRef ref="DatabaseAppender" level="operating"/>會用到它
tableName就是資料庫表名
class是一系列的資料庫連線這些
method是獲取資料庫連線、連線池等
class程式碼 ConnectionFactory: 用的dbcp的連線池
package com.pinyu.system.global.config.log4j2;
import java.sql.Connection;
import java.sql.SQLException;
import java.util.Properties;
import javax.sql.DataSource;
import org.apache.commons.dbcp.DriverManagerConnectionFactory;
import org.apache.commons.dbcp.PoolableConnection;
import org.apache.commons.dbcp.PoolableConnectionFactory;
import org.apache.commons.dbcp.PoolingDataSource;
import org.apache.commons.pool.impl.GenericObjectPool;
import org.springframework.stereotype.Component;
import com.pinyu.system.utils.SystemPropertiesUtils;
/**
* @author ypp
* 建立時間:2018年11月1日 下午2:33:44
* @Description: TODO(log4j2日誌儲存到資料庫)
*/
@Component
public class ConnectionFactory {
private static interface Singleton {
final ConnectionFactory INSTANCE = new ConnectionFactory();
}
private final DataSource dataSource;
private ConnectionFactory() {
try {
Class.forName(SystemPropertiesUtils.getDataSourceDriverClassName());
} catch (ClassNotFoundException e) {
e.printStackTrace();
System.exit(0);
}
Properties properties = new Properties();
properties.setProperty("user", SystemPropertiesUtils.getDataSourceUserName());
properties.setProperty("password", SystemPropertiesUtils.getDataSourcePassword());
GenericObjectPool<PoolableConnection>pool = new GenericObjectPool<PoolableConnection>();
DriverManagerConnectionFactory connectionFactory = new DriverManagerConnectionFactory(
SystemPropertiesUtils.getDataSourceUrl(),properties
);
new PoolableConnectionFactory(connectionFactory, pool, null, null, false, true);
this.dataSource = new PoolingDataSource(pool);
}
public static Connection getDatabaseConnection() throws SQLException {
return Singleton.INSTANCE.dataSource.getConnection();
}
}
最後程式碼中記錄自定義級別的日誌:
package com.pinyu.system.web.controller.finance;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.ResponseBody;
import com.pinyu.system.entity.finance.FinanceIncomeEntity;
import com.pinyu.system.global.ann.MethodLog;
import com.pinyu.system.service.finance.FinanceIncomeService;
import com.pinyu.system.utils.excel.ExcelUtils;
import com.pinyu.system.web.controller.common.BaseController;
import com.pinyu.system.web.page.Page;
/**
* @author ypp 建立時間:2018年10月29日 下午1:28:57
* @Description: TODO(財務報表-收益)
*/
@Controller
@RequestMapping("/financeIncome")
public class FinanceIncomeController extends BaseController{
protected Logger logger = LogManager.getLogger(FinanceIncomeController.class);
/**
* 匯出本頁
* @param page
* @return
* @throws Exception
*/
@RequestMapping("/exportPage/")
@ResponseBody
@MethodLog
public void exportPage(HttpServletRequest request,HttpServletResponse response,@RequestParam("page")Integer page,@RequestParam("startDate")String startDate,@RequestParam("endDate")String endDate) throws Exception{
Page<FinanceIncomeEntity> p=new Page<FinanceIncomeEntity>();
p.setCurrentPage(page);
ExcelUtils.setResponse(response, service.exportPage(p,startDate,endDate));
logger.log(OPERATING, "匯出財務報表-收益表");
}
}
OPERATING也就是我自己定義的級別,可以看到log4j2.xml配置檔案裡面資料庫儲存的也是儲存的這個級別的,這個級別我是抽到BaseController裡面的,因為基本很多controller都會用到這個
如果需要BaseController裡面程式碼,可以直接用就行,不需要其他的只看Level那一行程式碼就可以了
BaseController:
package com.pinyu.system.web.controller.common;
import java.io.UnsupportedEncodingException;
import java.util.Enumeration;
import java.util.HashMap;
import java.util.Map;
import java.util.UUID;
import javax.servlet.http.Cookie;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.apache.logging.log4j.Level;
import org.springframework.web.servlet.ModelAndView;
import org.springframework.web.servlet.support.RequestContext;
import com.pinyu.system.entity.UserEntity;
import com.pinyu.system.utils.SessionUtils;
/**
* @author ypp
* 建立時間:2018年11月1日 下午1:51:59
* @Description: TODO(用一句話描述該檔案做什麼)
*/
public abstract class BaseController {
protected static final Level OPERATING = Level.forName("OPERATION", 350);
/**
* 要跳轉的頁面
*/
protected ModelAndView go(String path) {
return new ModelAndView(path);
}
/**
* 獲取登入使用者資訊
* @return
*/
public UserEntity getUser(HttpServletRequest request) {
return SessionUtils.getLoginUser(request);
}
/**
* 獲取登入使用者資訊
* @return
*/
public String logInfo(HttpServletRequest request) {
UserEntity user = getUser(request);
return "使用者id:"+user.getId()+",姓名:"+user.getRealName()+",";
}
/**
* 獲取國際化資訊
*
* @param key
* @return
*/
public String getI18nMsg(HttpServletRequest request,String key) throws Exception {
// 從後臺程式碼獲取國際化資訊
String value = new RequestContext(request).getMessage(key);
return value != null ? value : "";
}
/**
* 請求方式判斷
*
* @param request
* @return
*/
public boolean isAjaxRequest(HttpServletRequest request) {
if (!(request.getHeader("accept").indexOf("application/json") > -1
|| (request.getHeader("X-Requested-With") != null
&& request.getHeader("X-Requested-With").indexOf("XMLHttpRequest") > -1)
|| "XMLHttpRequest".equalsIgnoreCase(request.getParameter("X_REQUESTED_WITH")))) {
return false;
}
return true;
}
/**
* 設定 cookie
*
* @param cookieName
* @param value
* @param age
*/
protected void setCookie(HttpServletResponse response,String cookieName, String value, int age) {
Cookie cookie = new Cookie(cookieName, value);
cookie.setMaxAge(age);
// cookie.setHttpOnly(true);
response.addCookie(cookie);
}
/**
* 方法名稱: getUUID<br>
* 描述:獲得唯一標識(目前用於驗證表單提交唯一性)<br>
* 作者: <br>
* 修改日期:2014年10月13日上午3:15:24<br>
*/
protected String getUUID() {
return UUID.randomUUID().toString();
}
/**
* 獲取請求完整路徑
* @param request
* @return
*/
public String getUrl(HttpServletRequest request){
String url = request.getRequestURI();
String params = "";
if(request.getQueryString()!=null){
params = request.getQueryString().toString();
}
if(!"".equals(params)){
url = url+"?"+params;
}
return url;
}
/**
* 把瀏覽器引數轉化放到Map集合中
* @param request
* @return
* @throws UnsupportedEncodingException
*/
protected Map<String, Object> getParam(HttpServletRequest request) {
Map<String, Object> paramMap = new HashMap<String, Object>();
String method = request.getMethod();
Enumeration<?> keys = request.getParameterNames();
while (keys.hasMoreElements()) {
Object key = keys.nextElement();
if(key!=null){
if (key instanceof String) {
String value = request.getParameter(key.toString());
if("GET".equals(method)){//前臺encodeURIComponent('我們');轉碼後到後臺還是ISO-8859-1,所以還需要轉碼
try {
value =new String(value.getBytes("ISO-8859-1"),"UTF-8");
} catch (UnsupportedEncodingException e) {
e.printStackTrace();
}
}
paramMap.put(key.toString(), value);
}
}
}
return paramMap;
}
}
Level.forName("OPERATION", 350);會從log4j2裡面的所有級別裡面去取,如果沒有這個級別,會給你建立這個級別
原始碼:
/**
* Retrieves an existing Level or creates on if it didn't previously exist.
*
* @param name The name of the level.
* @param intValue The integer value for the Level. If the level was previously created this value is ignored.
* @return The Level.
* @throws java.lang.IllegalArgumentException if the name is null or intValue is less than zero.
*/
public static Level forName(final String name, final int intValue) {
final Level level = LEVELS.get(name);
if (level != null) {
return level;
}
try {
return new Level(name, intValue);
} catch (final IllegalStateException ex) {
// The level was added by something else so just return that one.
return LEVELS.get(name);
}
}
LEVELS這個是一個key-value資料結構的,其實就是map,我認為就是一個級別池
實際開發中有很多場景可以用它來記錄不同型別的日誌,自定義一個級別
程式碼正在優化中,有些程式碼根據實際業務可以取捨進行優化的。
如果想自定義級別的方法,比如logger.operating("xxxxxx")這種。需要用到log4j2的記錄器包裝器。這裡不再講解
有興趣的朋友可以參考官方示例等