簡單日誌集成

文章出如今我的博客首頁時,博客園Markdown支持有問題,請點擊標題再閱讀,避免展現錯誤java

咱們在開發應用時,爲了便於調試和業務須要,在業務邏輯中加入了許多日誌,一般這樣會給人一種感受:業務和日誌耦合了,所以咱們很天然的剔除掉了許多日誌,並採用AOP實現日誌切面。
然而在許多狀況下,咱們並不能保證每一個程序開發人員的代碼都能作到整齊劃一,邏輯複雜程度相似,某些和業務邏輯代碼緊密關聯的日誌是無可避免的,畢竟太理想化的場景對業務和程序開發自己要求都比較高。spring

本文對這兩種情形的極端狀況都提供了處理策略.數據庫

1,徹底的AOP切面提供日誌,業務邏輯不存在任何日誌代碼
2,擴展log4j Appender,全部日誌都分佈在業務邏輯代碼當中apache

爲了方便日誌接入到第三方系統,本文采用Activemq消息服務器接收日誌.json

AOP日誌集成

AOP切面方式實在是太熱門,這種方式的優勢是無侵入,下文將經過簡單的業務場景,展現這種實踐過程。bash

舒適提示:請先行啓動外置的Activemq(從官網下載安裝包,默認條件啓動便可),若是未啓動,請打開Spring配置文件中內置broker服務服務器

業務流

執行業務方法getCustomer ---> 被日誌切面攔截:執行正常業務處理pjp.proceed(),而後發送日誌給目標隊列(demo.business.log) ---> 監聽器(demo.business.log)收到消息session

樣例代碼

業務服務併發

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日誌代碼app

模型

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

相關文章
相關標籤/搜索