文章出如今我的博客首頁時,博客園Markdown支持有問題,請點擊標題再閱讀,避免展現錯誤java
咱們在開發應用時,爲了便於調試和業務須要,在業務邏輯中加入了許多日誌,一般這樣會給人一種感受:業務和日誌耦合了,所以咱們很天然的剔除掉了許多日誌,並採用AOP實現日誌切面。
然而在許多狀況下,咱們並不能保證每一個程序開發人員的代碼都能作到整齊劃一,邏輯複雜程度相似,某些和業務邏輯代碼緊密關聯的日誌是無可避免的,畢竟太理想化的場景對業務和程序開發自己要求都比較高。spring
本文對這兩種情形的極端狀況都提供了處理策略.數據庫
1,徹底的AOP切面提供日誌,業務邏輯不存在任何日誌代碼。
2,擴展log4j Appender,全部日誌都分佈在業務邏輯代碼當中。apache
爲了方便日誌接入到第三方系統,本文采用Activemq消息服務器接收日誌.json
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是很是廉價的,自定義一個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