由于需要一些业务日志,本来是用的注解,然后用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 p=new Page();
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()));
在配置文件取出来就是:
log4j2.xml配置:
D:/apache-tomcat-8.5.33/logs
%d{yyyy-MM-dd HH:mm:ss} %-5level %class{36} %L
%M - %msg%xEx%n
${PATTERN_FORMAT}
这几行代码是自定义级别
method="getDatabaseConnection" />
这里面的信息解释:
name就是一个名字,在下面ROOT标签里面的
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());
GenericObjectPoolpool = new GenericObjectPool();
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 p=new Page();
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
* 描述:获得唯一标识(目前用于验证表单提交唯一性)
* 作者:
* 修改日期:2014年10月13日上午3:15:24
*/
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 getParam(HttpServletRequest request) {
Map paramMap = new HashMap();
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的记录器包装器。这里不再讲解
有兴趣的朋友可以参考官方示例等
http://logging.apache.org/log4j/2.x/manual/customloglevels.html