基于Spring AOP和Groovy日志模板配置的日志记录框架的二次实现与使用案例

一、项目地址

https://git.oschina.net/xuliugen/ufind-businesslog.git

说明

:本框架是基于koala-project(项目地址:http://git.oschina.net/openkoala/koala)中的koala-businesslog二次开发,因为koala-project已经很久没有维护,对于一些Maven仓库已经无法使用,Koala在Eclipse的插件也基本无法使用,最近项目开发的时候使用到了这个,决心将他改一下可以正常使用,本着来与开源回报开源的思想将这个项目分享出来,希望大家一起学习


二、改进内容


1、抽离项目依赖,去掉对org.openkoala和org.dayatang.dddlib原有框架内容的依赖,直接编译即可使用;

2、更改原有项目获取Bean的方式,这也是去掉对openkoala和dddlib框架依赖之后问题解决;

3、调整项目结构,使之更加明了和简洁,并添加相应的注释;

4、让用户自己实现日志导出器接口,方便用户选择合适的方式对日志信息进行保存;

5、添加类似后台管理的系统admin,可以对日志进行查看和搜索,对于修改、删除也提供了相应的方法;

6、提供一个完整的使用案例,是对用户注册的时候日志的记录;

7、更改原来JPA的使用为MyBatis的方式;

8、还有一些其他细节问题;


三、日志系统项目介绍


1、简介

现实场景,我们对于 业务的记录(也叫业务日志)的操作,很多时候是这样编码的:


//创建一家公司
public Organization createCompany(CompanyDto dto){
    // 执行业务方法
    companyDAO.save(dto);

    // 记录日志
    LogDAO.save(new BusinessLog(userDAO.getSubjectName() + ",创建子公司:" + dto.getCompanyName()));

} 

最后结果就是 :

  1. 新公司的创建
  2. 业务日志:张三,创建子公司:广州子公司

咋一看这样写没有什么问题,但是其中有一个最大的问题:业务逻辑和日志逻辑的混在一起了。如果业务逻辑和日志逻辑足够复杂的时候,你可以想像得到你的代码就如同意大利面一样。以后维护的时候,就会变成人间地狱!

Koala业务日志系统就是为解决此问题而设计:业务逻辑和日志逻辑分离!

2、Koala业务日志系统的目标

  1. 日志的记录对业务方法尽量无侵入
  2. 尽最大可能不影响业务方法的性能(异步实现)
  3. 系统及日志模板配置简单(基于 groovy)
  4. 日志持久化(也称为导出日志)方式灵活(面向接口设计,可扩展文件、NoSQL 存储)
  5. 修改日志模板而不需要重启应用

事实上,要达到真正的无侵入是不可能的,Koala业务日志系统对业务方法的侵入只不过是要在业务方法上加上一个注解。

3、现有模块划分

  1. ufind-businesslog-api 业务日志系统的核心api
  2. ufind-businesslog-admin 业务日志后台管理系统
  3. ufind-businesslog-demo 业务日志系统案例系统,实际使用时,可以参考此模块

4、目前的缺陷

  1. 依赖Spring 的AOP
  2. 只有受Spring IOC容器托管的bean才能被日志

5、如何使用Koala默认实现的业务日志系统

大纲


1. 在类路径下加入`businesslog.properties`文件
1. 为业务方法加上别名,具体做法:在业务方法上加入`@BusinessLogAlias`注解,并设置别名
1. 在类路径下加入日志模板配置文件 

5.1、详细操作

(1) 在类路径下加入businesslog.properties文件


    #指定拦截的业务方法,使用Spring的切入点写法
    pointcut=execution(* business.*Application.*(..))

    #日志开关
    kaola.businesslog.enable=true

    #指定日志导出器BusinessLogExporter接口的实现。默认有:BusinessLogConsoleExporter和BusinessLogExporterImpl,这个实在用户具体使用的时候进行自定义的
    businessLogExporter=com.ufind.businesslog.demo.exportImpl.BusinessLogExporterImpl

    #线程池配置。因为业务日志的导出借助线程池实现异步
    #核心线程数
    log.threadPool.corePoolSize=10
    #最大线程数
    log.threadPool.maxPoolSize=50
    #队列最大长度
    log.threadPool.queueCapacity=1000
    #线程池维护线程所允许的空闲时间
    log.threadPool.keepAliveSeconds=300
    #线程池对拒绝任务(无线程可用)的处理策略
    log.threadPool.rejectedExecutionHandler=java.util.concurrent.ThreadPoolExecutor$CallerRunsPolicy 

(2)如果使用Koala的默认日志导出器,需要配置数据库参数,数据库设置database.properties

 db.jdbc.driver=com.mysql.jdbc.Driver db.jdbc.connection.url=jdbc:mysql://127.0.0.1:3306/ufind_log?useUnicode=true&characterEncoding=UTF-8&allowMultiQueries=true db.jdbc.username=root db.jdbc.password=root db.jdbc.dialect=org.hibernate.dialect.MySQL5Dialect db.jdbc.testsql=select 1 hibernate.hbm2ddl.auto=update db.jdbc.show_sql=true db.jdbc.database.Type=MYSQL db.jdbc.generateDdl=true db.jdbc.maximumConnectionCount=200 db.jdbc.minimumConnectionCount=20

(3)为业务方法加上别名。这个别名必须符合Java方法名的命名规则。给业务方法加别名的目的是为了方便业务方法与日志模板之间的映射。

    @BusinessLogAlias("业务方法别名")
    业务方法

例如:    
  @BusinessLogAlias("UserInfoApplicationImpl_addUser")    public boolean addUser(UserInfo userInfo) {        return this.userInfo.addUser(userInfo);

}

(4)在类路径下加入日志模板配置文件


日志模板实际上是groovy文件。在这个groovy文件中,你可以写Java代码,也可以写groovy代码。这样,就可以达到最大的灵活。同时,配置起来又不复杂。

目前我们支持两种配置方式:单文件配置方式和多文件配置方式。 

例如:

package businessLogConfigclass UserInfoApplicationImpl { def context

 def UserInfoApplicationImpl_addUser() {

 "${getPreTemplate()}:创建一个新用户:${context._param0.userAccount}" }

 def getPreTemplate() {

 "${context._user}-" } }

5.2、日志模板配置

  • 单文件配置
  1. 在类路径下加入BusinessLogConfig.groovy
  2. 文件模板为:

class BusinesslogConfig {
    //必须
    def context

    //InvoiceApplicationImpl_addInvoice为业务方法别名
    def InvoiceApplicationImpl_addInvoice() {
        "日志内容"
    }

    def ProjectApplicationImpl_findSomeProjects() {
        [category:"项目操作", logs:"查找项目"]
    }
} 
  1. 配置模板说明
    配置模板实际上是一个Groovy类。你可以在类中定义任何方法。如果方法为某个业务方法的别名(使用@MethodAlias注解)
    那么,我们就认为它是一个业务日志方法。它的返回值(return或者放在方法最后一行的变量)将会被Set到org.openkoala.businesslog.BusinessLog的实例中。

    日志方法返回值有两种情况:1. 只返回一个String类型的日志文本;2. 返回一个Map,这个Map包括Key为category的日志分类及日志文本。

    在类中,还会使用Groovy定义变量的方法:def context定义一个变量。这个变量实际上是一个Map。
    Map中存储的是业务方法的返回值参数。如果需要,你可以存储任何你需要的数据。你可以从这个context中取
    出你需要的内容,填充到你的日志中。至于如何取context中的内容,请看附录

  2. 多文件配置
    当业务系统非常复杂的时候,一个日志配置文件是不足够的。我们提供多文件的配置方式

  3. 在类路径中加入businessLogConfig文件夹。

  4. 在该文件夹中加入日志配置文件,文件名任意,只要符合Groovy类文件的命名规范即可。

注: 多文件配置方式与单文件配置方式不兼容。在此业务日志系统中,单文件配置方式优先。
businessLogConfig文件夹中的所有以.groovy结尾的文件都将被作为日志配置文件。

6、实现自己的日志持久化方式

  1. 新建一个实现com.ufind.businesslog.api.BusinessLogExporter接口的类,
    比如:com.ufind.businesslog.demo.exportImpl.MyBusinesslogExporter
  2. businesslog.properties中设置businessLogExporter=com.mycom.busineslog.MyBusinesslogExporter

附录

在日志模板中取context的内容


 key                     value
_methodReturn           业务方法返回值
_param                  业务方法的参数, _param0代表第一个参数 _param1代表第二个参数,依此类推
_executeError           业务方法执行失败的异常信息
_businessMethod         业务方法
_user                   业务方法操作人
_time                   业务方法操作时间
_ip                     ip地址 

四、ufind-businesslog项目介绍


1、ufind-businesslog-api项目的核心API


BusinessLogAlias是业务日志的注解类;

BusinessLogExporter日志导出器,定义的接口让用户实现,实现的过程就是日志信息的存储过程;

BusinessLogInterceptor业务日志的拦截器,当业务方法执行之后获得执行的结果,根据groovy中配置的日志模板得到具体的日志信息,并调用异步执行任务存储日志信息;

BusinessLogThread日志的处理线程类,run()方法主要得到日志模板中的配置信息并将信息根据用户实现的日志导出器将日志信息保存到数据库中;

BusinessLogServletFilter业务日志的过滤器,在方法请求调用之前获得容器中的上下文环境,以便构造日志信息;

2、ufind-businesslog-demo业务日志项目使用案例

这里使用到日志框架API的地方就是自己去实现日志导出器,实现com.ufind.businesslog.api.BusinessLogExporter接口

exportImpl包下的是用户自己实现的日志导出器,其中指定了如何保存日志信息;

项目AOP配置

<bean id="logInterceptor" class="com.ufind.businesslog.api.BusinessLogInterceptor"/> <bean id="businessLogExporter" class="com.ufind.businesslog.demo.exportImpl.BusinessLogExporterImpl"/> <!-- 加了 proxy-target-class="true" 使spring集中制使用cglib的代理 --> <aop:config proxy-target-class="true"> <aop:pointcut id="businessBehavior" expression="execution(* com.ufind.businesslog.demo.application.impl.*.*(..))"/> <aop:aspect id="logAspect" ref="logInterceptor"> <aop:after-returning returning="result" method="logAfter" pointcut-ref="businessBehavior"/> <aop:after-throwing method="afterThrowing" pointcut-ref="businessBehavior" throwing="error"/> </aop:aspect> </aop:config>

异步线程池配置

 <bean id="threadPoolTaskExecutor" class="org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor"> <!-- 核心线程数 --> <property name="corePoolSize" value="${log.threadPool.corePoolSize}"/> <!-- 最大线程数 --> <property name="maxPoolSize" value="${log.threadPool.maxPoolSize}"/> <!-- 队列最大长度 >=mainExecutor.maxSize --> <property name="queueCapacity" value="${log.threadPool.queueCapacity}"/> <!-- 线程池维护线程所允许的空闲时间 --> <property name="keepAliveSeconds" value="${log.threadPool.keepAliveSeconds}"/> <!-- 线程池对拒绝任务(无线程可用)的处理策略 --> <property name="rejectedExecutionHandler"> <bean class="${log.threadPool.rejectedExecutionHandler}"/> </property> </bean>

自定义LogFilter过滤器继承

com.ufind.businesslog.api.BusinessLogServletFilter

package com.ufind.businesslog.demo.web.filters;import com.ufind.businesslog.api.BusinessLogServletFilter;import com.ufind.businesslog.demo.domain.UserInfo;import javax.servlet.*;import javax.servlet.http.HttpServletRequest;public class LogFilter extends BusinessLogServletFilter { /** * 将需要用到的信息放入日志上下文 */ @Override public void beforeFilter(ServletRequest req, ServletResponse resp, FilterChain chain) { addIpContext(getIp(req)); //添加IP HttpServletRequest request = (HttpServletRequest) req; UserInfo userInfo = (UserInfo) request.getSession().getAttribute("USER"); String userAccount = userInfo == null ? "未知" : userInfo.getUserAccount(); addUserContext(userAccount); }

 @Override public void init(FilterConfig filterConfig) throws ServletException {

 //To change body of implemented methods use File | Settings | File Templates. }

 @Override public void destroy() {

 //To change body of implemented methods use File | Settings | File Templates. } }

3、ufind-businesslog-admin业务日志项目admin


使用自己Spring MVC 、MyBastis对MySQL数据库的日志信息进行查询、搜索操作

这里主要是提供一种思路,具体如何对日志信息进行可视化的管理方式很多。

项目的主要思路就是

1、业务请求首先通过LogFilter, 将容器中的上下文环境加到ThreadLocalBusinessLogContext对象中,例如:用户的信息、IP地址等信息;

2、执行到加了@BusinessLogAlias注解的业务方法,执行完毕之后被BusinessLogInterceptor拦截器进行拦截;

3、BusinessLogInterceptor拦截器根据切点信息得到符合BusinessLogAlias注解的value值,就是得到grooy中要执行的方法名;

4、紧接着创建BusinessLogThread对象,为了异步执行操作,该对象包含ThreadLocalBusinessLogContext对象信息,要执行的日志模板配置文件中的哪一个方法,用户自己实现的日志导出器;

5、然后执行异步任务,再执行异步任务的时候,根据注解的value值确定执行Groovy中的def的方法并得到返回值,该返回值就是日志信息通过contex添加值之后的字符串信息;

6、然后调用日志导出器的实现类中的export方法进行具体日志信息的保存,将日志信息保存到指定位置;

最后,文章在手机上显示由于代码的原因,可能不是很清楚,建议下载项目看一下,会更清楚。