[不看会后悔]Log4J记录系统业务日志到数据库

/ Log4j高级应用 / 5 条评论 / 9790浏览

日志记录现状:

​ 日志已经成为绝大多数系统都必备的基础功能了。我一直在思考怎么让日志满足用户业务需求的同时能够尽可能的让开发人员使用起来更简单;用最少的代码完成日志记录;在技术层面上要优雅、低侵入性、灵活等。

沿着这个思路结合目前主流的技术;大体上有三种方法来实现日志打印的功能:

  1. 传统粗暴式的硬编码:

    实现思路如下:

   
   /**传入request方便获取IP、用户信息等,日志结果、操作类型,日志描述等信息用参数传递。*/
    LogUtil.addLog(request,Log.OPER_TYPE_LOGIN,Log.RESULT_SUCCESS, "系统登录", ""); 
    

优缺点:这种打日志的方法简单粗暴!侵入性强;无可配置之言。

  1. 使用Struts2或者Spring的拦截器:

    这种方式应该是用的较多的一种;在applicationContext中或者使用注释声明拦截器和需要拦截的业务方法;在方法调用前后获取用户操作信息记录到数据库中;这其实可以视为对方法1的一种改进;但是这种方式对于复杂的业务日志要求就显得无能为力或者非常费劲了。举个例子:

    我在某省联通的某系统中用户要求,记录和用户操作相关的详细日志信息,用户修改信息要记录修改前、修改后;用户删除信息要记录删除信息的内容等。这种使用拦截器来实现就比较尴尬;获取方法运行时变量参数;还有区分不同的操作记录不同的类型业务关联性很高;全部放在一个拦截器里面显得不太合适。通用性高、灵活性下降了。

    对于这种主流的记录日志方法;为了缩减篇幅;代码我就不贴了;大家可以自行Google研究一下。

    优缺点:侵入性低、和业务代码解耦、可配置稍低。应对负责的业务日志记录场景可能满足不了或者产生大量的硬编码。

    3、Log4J框架打业务日志:

​ 前面做了那么多铺垫;终于引出了今天的主角;先来说说Log4J记录业务日志的优点吧:

基于Log4j的解决方案:

下面让我们沐浴在代码里来深深的体味这一切的神奇和美好吧。

Log4j.properties 核心关键配置

#定义项目中包下面的类的日志级别;我占用了Log4j的info级别做业务日志输出;你可要自定义日志级别。
log4j.logger.cn.promore.bf=info,dblog
#output in root logger  是否把项目日志输出到主日志中
log4j.additivity.cn.promore.bf=false 
#覆盖了log4j的默认appender来实现自动获取入库操作的数据源;
#这样免去了log4j本身的几个配置。也免去了改数据库连接方式要修改多处还容易遗忘的烦恼。
log4j.appender.dblog=info.huzd.bf.log4j.extend.IportalJDBCAppender
#定义字符集防止中文乱码
log4j.appender.dblog.encoding=UTF-8
#the lowest log level of the appender
#记录的最低日志级别。高于这个的就会全部会被记录;所以你知道我为什么占用info来打业务日志了。
log4j.appender.dblog.Threshold=info
log4j.appender.dblog.BufferSize=10 #日志缓存;这个配置可以提交性能;累计操作N(10)个记录一次性写入数据库
#打日志的SQL语句;提前建好日志表。请注意 %X{变量名} 这里使用的就是MDC;值注入!其他的%M这些都是Log4j的内置变量。
log4j.appender.dblog.sql= log4j.appender.dblog.sql= INSERT INTO sys_log\ (username,clientIp,operateModule,operateModuleName,operateType,operateTime,operateContent,operateResult)\ 
VALUES \
('%X{username}','%X{clientIp}','%C{1}','%X{operateModuleName}','%M','%d{yyyy-MM-dd HH🇲🇲ss}','%m','%X{operateResult}')
#同步输出到控制台的配置。
log4j.appender.dblog.layout=org.apache.log4j.PatternLayout  
log4j.appender.dblog.layout.ConversionPattern=-[%d{yyyy-MM-dd HH🇲🇲ss}]-[%C{1}]-[%M]-%X{clientIp}-%X{operateModuleName}-%X{username}-%X{operateContent}-%m-%n

IportalJDBCAppender

public class IportalJDBCAppender extends JDBCAppender {
  //从Spring容器中直接获取数据源
	@Override
	protected Connection getConnection() throws SQLException {
		WebApplicationContext wac = ContextLoader.getCurrentWebApplicationContext();
		DruidDataSource dataSource = wac.getBean("dataSource", DruidDataSource.class);
//		BasicDataSource dataSource = wac.getBean("dataSource", BasicDataSource.class);
		return dataSource.getConnection();
	}
 //限定我们只记录log.info的日志;默认的log4j记录日志等级比info低的其他日志例如debug、ALL
	@Override
	public boolean isAsSevereAsThreshold(Priority priority) {
		return (null!=getThreshold()&&null!=priority&&priority.isGreaterOrEqual(getThreshold())&&getThreshold().isGreaterOrEqual(priority));
	}
	
}

BaseAction 变量统一设置

抽离出一个BaseAction目的就是把一些共性的日志参数做统一值注入;这样在打日志时只需要再注入关键信息就可以了!

public class BaseAction extends ActionSupport {
	
	private static final long serialVersionUID = 1L;
	
	protected HttpServletRequest request = ServletActionContext.getRequest();
	protected HttpServletResponse response = ServletActionContext.getResponse();
	protected HttpSession session = ServletActionContext.getRequest().getSession();
	
	public BaseAction(){
    
    /** 操作用户、IP地址、操作结果等这些都是每次打日志都必须的;抽离出来只要写一次就OK了。当然BaseAction还会封装其他的一些共性方法 ***/
		User user = (User)request.getSession().getAttribute("user");
		MDC.put("username",null!=user?user.getUsername():"anonymous");  
		MDC.put("clientIp",NetUtil.getIpAddr(request));  
		MDC.put("operateResult", "success"); 
	}
	
	// ... 其他代码省略 ...
}

具体的业务Action实例:

@Controller
@Action(value="roleAction")
@ParentPackage("huzdDefault")
@Results({@Result(name="objectResult",type="json",params={"includeProperties","flag,message,role\\.(id|name|desc|canInherit|isBuiltIn|region)","excludeNullProperties","true"}),
		 @Result(name="listResult",type="json",params={"includeProperties","roles\\[\\d+\\]\\.(id|name|desc|canInherit|isBuiltIn|region),page\\.(\\w+),flag,message","excludeNullProperties","true"}),
		 @Result(name="result",type="json",params={"includeProperties","flag,message"}),
		 @Result(name="messageResult",type="json",params={"root","message"}),
		 @Result(name="config",type="json",params={"root","flag"})
		 })
public class RoleAction extends BaseAction{
	public static Logger LOG = LoggerFactory.getLogger(RoleAction.class);
	private static final long serialVersionUID = -8613055080615406396L;
	
	@Resource 
	RoleService roleService;
	@Resource
	ResourceService resourceService;
	private Role role;
	private boolean flag = true;
	private String message;
	private Page page;
	private List<Role> roles;
	private List<cn.promore.bf.bean.Resource> resources;
	private String dataIds;
	public RoleAction() {
            //这里注入模块的中文名称,每个模块都不一样;所以在构造方法里面进行注入;这么做用户体验好点;用户可以按到模块中文名称而不是RoleAction这样。
		MDC.put("operateModuleName","角色管理");
	}
	
	public String update(){
			SecurityUtils.getSubject().checkPermission("role:update");
			Role temp = roleService.findById(role.getId());
                        String beforeUpdate = temp.toString();
			try {
				SecurityUtils.getSubject().checkPermission("role>update>"+temp.getRegion());
				if(null!=temp){
					// ... 其他代码省略 ...
					roleService.update(temp);
					flag = true;
				}
        String afterUpdate = temp.toString(); 
        //打业务日志唯一需要写的两行代码。 这样就可以记录角色更新前信息和更新后的信息;日志记录到这个份上基本上就到位了。
				MDC.put("operateContent","角色更新:更新前:"+ beforeUpdate+",更新后:"+ afterUpdate);   
				LOG.info("");
			} catch (AuthorizationException e) {
				flag = false;
				message = "不具备更新角色【"+temp.getName()+"】的权限";
			} catch (Exception e) {
				flag = false;
				message = "更新失败!请重试。";
			}
		return "result";
	}
	
    …… get set 方法 ……
}

简单的三步就完成了日志记录;是不是很简单???!

程序员需要做的事情就是 LOG.info("");

我的理念就是能不让程序员做的尽量不让其做,在框架搭建的时候就做好共性的东西;只做无路如何无法回避的步骤。让开发人员简单到极致

效果截图

下面是我们系统的截图日志监控界面:

img

更多可能

扩展话题:利用Log4j的特性;我们就可以做很多文章了;比如:在线改变日志级别!

有时候我们会有一些奇葩的需求和场景:

A.系统包含100个模块,某某模块下的日志我突然不想记录了;想关闭!

B.当初记录日志的时候包含调试日志、业务日志;系统出BUG的时候我想打开调试日志;分析原因后发现数据库数据有问题;这个时候我想关闭调试日志。

这些都非常简单!只需要在系统设置里面点点就可以了;关键点就是Log4j可以在运行时改变日志界别!核心代码如下:

//获取所有Log4j下的日志仓库
LoggerRepository loggerRepository = org.apache.log4j.LogManager.getLoggerRepository();
//获取日志类别 打印的结果就是包名+类名
//我们可以把这个输出到HTML页面中;然后在后面加上关闭按钮;在调用Log4j的接口来调整日志级别!
//LogManager.getLogger(logger.getName()).setLevel(Level.OFF);//可以关闭、开启、改变日志级别!
Enumeration allCategories = loggerRepository.getCurrentCategories();
while(allCategories.hasMoreElements()){
org.apache.log4j.Logger logger = (org.apache.log4j.Logger)allCategories.nextElement();
if(logger.getName().indexOf("cn.promore.bf")!=-1)System.out.println("------------"+logger.getName());
}

结束语

吹牛的话写在后面!

使用Log4j打业务日志在几年前我开始使用的时候;我个人感觉是一种综合的创新吧;因为之前我没有见别人这么用过;即使现在你可以在Google\Baidu上也找不到这么体系完整的教你使用的例子!很多零散的知识点都是可以查到的;当然基础的东西都是LOG4J提供的不是我创造的;我只是综合一下玩出了新花样!希望这篇原创的博文能给JAVA工程师们一些帮助!

  1. 打错了,是有log4j2版本么

    回复
    1. @18TooShort

      不是的;不过最近在看Log4j 2 改造也很简单。

      回复
  2. 用log4j2版本么,好像有很多不一样了

    回复
  3. 给力,给力

    回复
  4. 东哥,威武

    回复