简单日志集成

文章出现在个人博客首页时,博客园Markdown支持有问题,请点击标题再阅读,避免展示错误

我们在开发应用时,为了便于调试和业务需要,在业务逻辑中加入了许多日志,通常这样会给人一种感觉:业务和日志耦合了,因此我们很自然的剔除掉了许多日志,并采用AOP实现日志切面。
然而在许多情况下,我们并不能保证每个程序开发人员的代码都能做到整齐划一,逻辑复杂程度类似,某些和业务逻辑代码紧密关联的日志是无可避免的,毕竟太理想化的场景对业务和程序开发本身要求都比较高。

本文对这两种情形的极端情况都提供了处理策略.

1,完全的AOP切面提供日志,业务逻辑不存在任何日志代码
2,扩展log4j Appender,所有日志都分布在业务逻辑代码当中

为了方便日志接入到第三方系统,本文采用Activemq消息服务器接收日志.

AOP日志集成#

AOP切面方式实在是太热门,这种方式的优点是无侵入,下文将通过简单的业务场景,展示这种实践过程。

温馨提示:请先行启动外置的Activemq(从官网下载安装包,默认条件启动即可),如果未启动,请打开Spring配置文件中内置broker服务

业务流##

执行业务方法getCustomer ---> 被日志切面拦截:执行正常业务处理pjp.proceed(),然后发送日志给目标队列(demo.business.log) ---> 监听器(demo.business.log)收到消息

样例代码##

业务服务

package org.wit.ff.business;

import org.springframework.stereotype.Service;
import org.wit.ff.model.Customer;

/**
 * Created by F.Fang on 2015/11/10.
 * Version :2015/11/10
 */

@Service
public class CustomerBusiness {

    public Customer getCustomer(int appId, int customerId){
        Customer customer = new Customer();
        customer.setCompanyId(10010);
        customer.setId(customerId);
        customer.setTitle("hnb");
        customer.setName("cxb");
        customer.setLevel(Integer.MAX_VALUE);
        return new Customer();
    }

    public void saveCustomer(int appId, Customer customer){
        System.out.println("appId is:"+appId);
        System.out.println("customer is:"+customer);
    }

}

备注:无任何Log4j日志代码

模型

package org.wit.ff.model;

/**
 * Created by F.Fang on 2015/11/10.
 * Version :2015/11/10
 */
public class Customer {

    private int id;

    private int companyId;

    private String name;

    private int level;

    private String title;

    public int getCompanyId() {
        return companyId;
    }

    public void setCompanyId(int companyId) {
        this.companyId = companyId;
    }

    public String getTitle() {
        return title;
    }

    public void setTitle(String title) {
        this.title = title;
    }

    public int getLevel() {
        return level;
    }

    public void setLevel(int level) {
        this.level = level;
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public int getId() {
        return id;
    }

    public void setId(int id) {
        this.id = id;
    }
}

日志切面

package org.wit.ff.log;

import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.jms.core.JmsTemplate;
import org.springframework.jms.core.MessageCreator;
import org.wit.ff.util.JsonUtil;

import javax.jms.Destination;
import javax.jms.JMSException;
import javax.jms.Message;
import javax.jms.Session;
import java.io.PrintWriter;
import java.io.StringWriter;

/**
 * Created by F.Fang on 2015/11/10.
 * Version :2015/11/10
 */
@Aspect
public class BusinessLogAspect {

    @Autowired
    private JmsTemplate jmsTemplate;

    @Autowired
    private Destination destination;

    @Around("execution(* org.wit.ff.business.*.*(..))")
    public Object record(ProceedingJoinPoint pjp) throws Throwable {
        try {
            Object result = pjp.proceed();
            // 添加正常处理的日志.
            sendMsg(buildLog(pjp, null));
            return result;
        } catch (Throwable e) {
            // 增加异常处理的日志.
            sendMsg(buildLog(pjp, e));
            throw e;
        }
    }

    private TraceLog buildLog(ProceedingJoinPoint pjp, Throwable e) {
        TraceLog log = new TraceLog();
        // 要保证所有的逻辑方法在调用参数上做限定,必须保证第一个参数是appId.
        if (pjp.getArgs() != null && pjp.getArgs().length >= 1) {
            log.setAppId((int) pjp.getArgs()[0]);
        }
        log.setOperation(pjp.getSignature().getName());
        if (null != e) {
            String msg = getStackTrace(e);
            if(msg.length()>256){
                log.setDetails(msg.substring(0,256));
            } else{
                log.setDetails(msg);
            }
        }
        return log;
    }

    private void sendMsg(final TraceLog log) {
        jmsTemplate.send(destination, new MessageCreator() {
            @Override
            public Message createMessage(Session paramSession) throws JMSException {
                return paramSession.createTextMessage(JsonUtil.objectToJson(log));
            }
        });
    }

    /**
     * 获取目标异常栈信息.
     * 由于异常栈信息可能过长,如果考虑将数据入库或其它介质,最好考虑最大长度不超过一个阀值.
     *
     * @param throwable 目标异常.
     * @return
     */
    private String getStackTrace(Throwable throwable) {
        StringWriter sw = new StringWriter();
        PrintWriter pw = new PrintWriter(sw);
        try {
            throwable.printStackTrace(pw);
            return sw.toString();
        } finally {
            pw.close();
        }
    }

}

日志模型

package org.wit.ff.log;

import org.apache.commons.lang3.builder.ReflectionToStringBuilder;
import org.apache.commons.lang3.builder.ToStringStyle;

/**
 * Created by F.Fang on 2015/11/10.
 * Version :2015/11/10
 */
public class TraceLog {

    private String operation;

    /**
     * 任何一个操作都需要一个应用,此属性用于标识不同的应用数据接入.
     */
    private int appId;

    /**
     * 详细信息.
     */
    private String details;

    public String getOperation() {
        return operation;
    }

    public void setOperation(String operation) {
        this.operation = operation;
    }

    public int getAppId() {
        return appId;
    }

    public void setAppId(int appId) {
        this.appId = appId;
    }

    public String getDetails() {
        return details;
    }

    public void setDetails(String details) {
        this.details = details;
    }

    @Override
    public String toString() {
        return ReflectionToStringBuilder.toString(this, ToStringStyle.DEFAULT_STYLE);
    }
}

消息监听

package org.wit.ff.log;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.wit.ff.util.JsonUtil;

import javax.jms.JMSException;
import javax.jms.Message;
import javax.jms.MessageListener;
import javax.jms.TextMessage;

/**
 * Created by F.Fang on 2015/11/10.
 * Version :2015/11/10
 */
public class BusinessLogMessageListener implements MessageListener{

    private static final Logger LOGGER = LoggerFactory.getLogger(BusinessLogAspect.class);

    @Override
    public void onMessage(Message message) {
        // 处理消息.
        TextMessage txtMsg = (TextMessage) message;
        try {
            TraceLog log = JsonUtil.jsonToObject(txtMsg.getText(), TraceLog.class);
            LOGGER.info("business log:"+log.toString());
        } catch (JMSException e) {
            LOGGER.error("处理业务日志发生异常!", e);
        }
    }
}

日志配置

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE log4j:configuration SYSTEM "log4j.dtd">
<log4j:configuration xmlns:log4j="http://jakarta.apache.org/log4j/">
    <!-- ===================================================================== -->
    <!--  以下是appender的定义                                                 -->
    <!-- ===================================================================== -->

    <!-- org.apache.log4j.ConsoleAppender -->
    <appender name="PROJECT-CONSOLE" class="org.apache.log4j.ConsoleAppender">
        <layout class="org.apache.log4j.PatternLayout">
            <param name="ConversionPattern" value="%d %-5p %c{2} - %m%n"/>
        </layout>
    </appender>
   
    <appender name="businessAppender" class="org.apache.log4j.DailyRollingFileAppender">
        <param name="file" value="logs/business.log"/>
        <!-- 若配置为true,表示在原有日志上继续append -->
        <param name="append" value="true"/>
        <!-- 若配置为false,表示清空原有日志 -->
        <!-- <param name="append" value="false"/> -->
        <param name="encoding" value="UTF-8"/>
        <layout class="org.apache.log4j.PatternLayout">
            <param name="ConversionPattern" value="%d %-5p %c{2} - %m%n"/>
        </layout>
    </appender>
    
    <!-- 定义logger,链接多个Appender表示信息将输出到多个目标(可以是文件,也可以是控制台或其它) -->
    <logger name="org.wit.ff.business" additivity="false">
        <level value="INFO"/>
        <appender-ref ref="businessAppender"/>
        <appender-ref ref="PROJECT-CONSOLE"/>
    </logger>
    
    <!-- ===================================================================== -->
    <!--  Root logger的定义                                                    -->
    <!-- ===================================================================== -->
    <root>
    <!--  DEBUG < INFO < WARN < ERROR < FATAL -->
        <level value="INFO"></level>
        <!-- <level value="WARN"/> -->
        <appender-ref ref="PROJECT-CONSOLE"/>
    </root>
</log4j:configuration>

Spring配置文件(spring-log-aop.xml)

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
	xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
	xmlns:context="http://www.springframework.org/schema/context"  xmlns:aop="http://www.springframework.org/schema/aop"
	xsi:schemaLocation="
     http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-3.0.xsd
     http://www.springframework.org/schema/aop http://www.springframework.org/schema/aop/spring-aop-4.0.xsd
     http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context-3.0.xsd">


    <!-- 本地内置的代理服务, 如果外置的Activemq已启动,请注释 -->
    <!--
    <bean id="localBroker" class="org.apache.activemq.broker.BrokerService"
          init-method="start" destroy-method="stop">
        <property name="brokerName" value="mainBroker" />
        <property name="persistent" value="false" />
        <property name="transportConnectorURIs">
            <list>
                <value>tcp://localhost:61616</value>
            </list>
        </property>
    </bean>
    -->

    <!-- 客户端连接工厂 -->
    <bean id="connectionFactory" class="org.apache.activemq.ActiveMQConnectionFactory">
        <property name="brokerURL">
            <value>tcp://localhost:61616</value>
        </property>
    </bean>

    <!-- Jms模版 -->
    <bean id="jmsTemplate" class="org.springframework.jms.core.JmsTemplate">
        <property name="connectionFactory" ref="connectionFactory" />
    </bean>

    <!-- 目标队列 -->
    <bean id="destination" class="org.apache.activemq.command.ActiveMQQueue">
        <constructor-arg value="demo.business.log" />
    </bean>

    <!-- 监听器. -->
    <bean id="businessLogListenerContainer"
          class="org.springframework.jms.listener.DefaultMessageListenerContainer">
        <property name="connectionFactory" ref="connectionFactory" />
        <property name="destinationName" value="demo.business.log" />
        <property name="messageListener" ref="messageListener" />
    </bean>

    <bean id="messageListener" class="org.wit.ff.log.BusinessLogMessageListener" />

    <!-- 日志Aspect扫描 -->
    <bean id="logAspect" class="org.wit.ff.log.BusinessLogAspect" />


    <aop:aspectj-autoproxy proxy-target-class="true"/>

    <!-- 启动service扫描 -->
	<context:component-scan base-package="org.wit.ff.business"/>
</beans>

测试

package org.wit.ff.business;

import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.junit4.AbstractJUnit4SpringContextTests;
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
import org.wit.ff.model.Customer;

/**
 * Created by F.Fang on 2015/11/10.
 * Version :2015/11/10
 */

@ContextConfiguration(locations = "classpath:spring-log-aop.xml")
@RunWith(SpringJUnit4ClassRunner.class)
public class BusinessLogTest extends AbstractJUnit4SpringContextTests{

    @Autowired
    private CustomerBusiness customerBusiness;

    @Test
    public void demo() throws Exception {
        customerBusiness.getCustomer(1,1);
        Thread.sleep(10000);
    }

}

控制台日志

2015-11-11 00:58:19,672 INFO  log.BusinessLogAspect - business log:org.wit.ff.log.TraceLog@191a9961[operation=getCustomer,appId=1,details=<null>]

扩展log4j appender#

扩展log4j appender是非常廉价的,自定义一个Appender即可,log4j的体系结构中,appender对应了一个目标输出介质,可以是文件、控制台、数据库。

业务流##

每一条日志都导向了CommonLogAppender ---> CommonBusiness执行getCustomer()方法,内部执行LOGGER.info(xxx)方法,实际日志内容是getCutomer,appId=1 --> CommonLogAppender执行append方法,并发送日志到队列(demo.common.log) ---> 监听器接收日志并打印到控制台。

样例代码##

自定义Appender

package org.wit.ff.log;

import org.apache.activemq.ActiveMQConnectionFactory;
import org.apache.activemq.pool.PooledConnectionFactory;
import org.apache.log4j.AppenderSkeleton;
import org.apache.log4j.spi.LoggingEvent;

import javax.jms.*;

/**
 * Created by F.Fang on 2015/11/10.
 * Version :2015/11/10
 */
public class CommonLogAppender extends AppenderSkeleton {

    private static final String COMMON_LOG_QUEUE = "demo.common.log";
    private PooledConnectionFactory pooledConnectionFactory;

    @Override
    public void activateOptions() {
        ActiveMQConnectionFactory connectionFactory = new ActiveMQConnectionFactory();
        connectionFactory.setBrokerURL("tcp://localhost:61616");
        pooledConnectionFactory = new PooledConnectionFactory(connectionFactory);
        pooledConnectionFactory.setMaxConnections(1);
        pooledConnectionFactory.setMaximumActiveSessionPerConnection(2);
    }

    @Override
    protected void append(LoggingEvent event) {
        Connection connection = null;
        Session session = null;
        try {
            connection = pooledConnectionFactory.createConnection();
            session = connection.createSession(false, Session.AUTO_ACKNOWLEDGE);
            MessageProducer producer = session.createProducer(session.createQueue(COMMON_LOG_QUEUE));
            if(event.getMessage()!=null){
                TextMessage txtMsg = session.createTextMessage(event.getMessage().toString());
                producer.send(txtMsg);
            }
        } catch (JMSException e) {
            e.printStackTrace();
        } finally {
            if (session != null) {
                try {
                    session.close();
                } catch (JMSException e) {
                    e.printStackTrace();
                }
            }
            if (connection != null) {
                try {
                    connection.close();
                } catch (JMSException e) {
                    e.printStackTrace();
                }
            }
        }
    }

    @Override
    public void close() {
        System.out.println("close!!!");
        pooledConnectionFactory.stop();
    }

    @Override
    public boolean requiresLayout() {
        return true;
    }
}

业务服务

package org.wit.ff.business;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Service;
import org.wit.ff.model.Customer;

/**
 * Created by F.Fang on 2015/11/10.
 * Version :2015/11/10
 */
@Service
public class CommonBusiness {

    private static final Logger LOGGER = LoggerFactory.getLogger(CommonBusiness.class);

    public Customer getCustomer(int appId, int customerId){
        LOGGER.info("getCustomer, appId="+appId);
        Customer customer = new Customer();
        customer.setCompanyId(10010);
        customer.setId(customerId);
        customer.setTitle("hnb");
        customer.setName("cxb");
        customer.setLevel(Integer.MAX_VALUE);
        return new Customer();
    }

    public void saveCustomer(int appId, Customer customer){
        LOGGER.info("saveCustomer, appId="+appId);
        System.out.println("appId is:"+appId);
        System.out.println("customer is:"+customer);
    }

}

log4j配置

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE log4j:configuration SYSTEM "log4j.dtd">
<log4j:configuration xmlns:log4j="http://jakarta.apache.org/log4j/">
    <!-- ===================================================================== -->
    <!--  以下是appender的定义                                                 -->
    <!-- ===================================================================== -->

    <!-- org.apache.log4j.ConsoleAppender -->
    <appender name="PROJECT-CONSOLE" class="org.wit.ff.log.CommonLogAppender">
        <layout class="org.apache.log4j.PatternLayout">
            <param name="ConversionPattern" value="%d %-5p %c{2} - %m%n"/>
        </layout>
    </appender>
   
    <appender name="businessAppender" class="org.apache.log4j.DailyRollingFileAppender">
        <param name="file" value="logs/business.log"/>
        <!-- 若配置为true,表示在原有日志上继续append -->
        <param name="append" value="true"/>
        <!-- 若配置为false,表示清空原有日志 -->
        <!-- <param name="append" value="false"/> -->
        <param name="encoding" value="UTF-8"/>
        <layout class="org.apache.log4j.PatternLayout">
            <param name="ConversionPattern" value="%d %-5p %c{2} - %m%n"/>
        </layout>
    </appender>
    
    <!-- 定义logger,链接多个Appender表示信息将输出到多个目标(可以是文件,也可以是控制台或其它) -->
    <logger name="org.wit.ff.business" additivity="false">
        <level value="INFO"/>
        <appender-ref ref="businessAppender"/>
        <appender-ref ref="PROJECT-CONSOLE"/>
    </logger>
    
    <!-- ===================================================================== -->
    <!--  Root logger的定义                                                    -->
    <!-- ===================================================================== -->
    <root>
    <!--  DEBUG < INFO < WARN < ERROR < FATAL -->
        <level value="INFO"></level>
        <!-- <level value="WARN"/> -->
        <appender-ref ref="PROJECT-CONSOLE"/>
    </root>
</log4j:configuration>

Spring配置

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
	xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
	xmlns:context="http://www.springframework.org/schema/context"  xmlns:aop="http://www.springframework.org/schema/aop"
	xsi:schemaLocation="
     http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-3.0.xsd
     http://www.springframework.org/schema/aop http://www.springframework.org/schema/aop/spring-aop-4.0.xsd
     http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context-3.0.xsd">


    <!-- 本地内置的代理服务 -->
    <!--
    <bean id="localBroker" class="org.apache.activemq.broker.BrokerService"
          init-method="start" destroy-method="stop">
        <property name="brokerName" value="mainBroker" />
        <property name="persistent" value="false" />
        <property name="transportConnectorURIs">
            <list>
                <value>tcp://localhost:61616</value>
            </list>
        </property>
    </bean>
    -->

    <!-- 客户端连接工厂 -->
    <bean id="connectionFactory" class="org.apache.activemq.ActiveMQConnectionFactory">
        <property name="brokerURL">
            <value>tcp://localhost:61616</value>
        </property>
    </bean>

    <!-- 监听器. -->
    <bean id="businessLogListenerContainer"
          class="org.springframework.jms.listener.DefaultMessageListenerContainer">
        <property name="connectionFactory" ref="connectionFactory" />
        <property name="destinationName" value="demo.common.log" />
        <property name="messageListener" ref="messageListener" />
    </bean>

    <bean id="messageListener" class="org.wit.ff.log.CommonLogMessageListener" />

    <!-- 启动service扫描 -->
	<context:component-scan base-package="org.wit.ff.business"/>
</beans>

测试

package org.wit.ff.business;

import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.junit4.AbstractJUnit4SpringContextTests;
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;

/**
 * Created by F.Fang on 2015/11/10.
 * Version :2015/11/10
 */
@ContextConfiguration(locations = "classpath:spring-log-expand-log4j.xml")
@RunWith(SpringJUnit4ClassRunner.class)
public class CommonLogTest extends AbstractJUnit4SpringContextTests {

    @Autowired
    private CommonBusiness commonBusiness;

    @Test
    public void demo() throws Exception {
        commonBusiness.getCustomer(1, 1);
        Thread.sleep(10000);
    }
}

日志记录

getCustomer, appId=1

QA#

posted @ 2015-11-11 12:06  坚持很贵  阅读(584)  评论(0编辑  收藏  举报