實戰+分析Spring+Log4j+ActiveMQ實現遠程記錄日志
?應用場景?
隨著項目的逐漸擴大,日志的增加也變得更快。Log4j是常用的日志記錄工具,在有些時候,我們可能需要將Log4j的日志發送到專門用于記錄日志的遠程服務器,特別是對于稍微大一點的應用。這么做的優點有:
-
可以集中管理日志:可以把多臺服務器上的日志都發送到一臺日志服務器上,方便管理、查看和分析
-
可以減輕服務器的開銷:日志不在服務器上了,因此服務器有更多可用的磁盤空間
-
可以提高服務器的性能:通過異步方式,記錄日志時服務器只負責發送消息,不關心日志記錄的時間和位置,服務器甚至不關心日志到底有沒有記錄成功
遠程打印日志的原理:項目A需要打印日志,而A調用Log4j來打印日志,Log4j的JMSAppender又給配置的地址(ActiveMQ地址)發送一條JMS消息,此時綁定在Queue上的項目B的監聽器發現有消息到來,于是立即喚醒監聽器的方法開始輸出日志。
本文將使用兩個Java項目Product和Logging,其中Product項目就是模擬線上的項目,而Logging項目模擬運行在專用的日志服務器上的項目。說明:本文的例子是在Windows平臺下。
?安裝ActiveMQ?
1. 下載:http://activemq.apache.org/download.html
2. 解壓后不需要任何配置,進入到bin下對應的系統架構文件夾
3. 雙擊activemq.bat啟動,如果看到類似下面的頁面,就代表activemq啟動好了:
然后打開瀏覽器,輸入地址:http://localhost:8161進入管理頁面,用戶名admin,密碼admin:
可以點擊Manage ActiveMQ broker進入Queue的查看界面。
實戰
我用Maven來管理項目,方便維護各種依賴的jar包。先看下項目結構:
項目不復雜,主要是4個文件:pom.xml,Main.java,log4j.properties和jndi.properties
pom.xml中主要是聲明項目的依賴包,其余沒有什么東西了:
<!-- Use to call write log methods --> <dependency> <groupId>log4j</groupId> <artifactId>log4j</artifactId> <version>1.2.17</version> </dependency> <!-- Log4j uses this lib --> <dependency> <groupId>org.slf4j</groupId> <artifactId>slf4j-log4j12</artifactId> <version>1.7.13</version> </dependency> <!-- Spring jms lib --> <dependency> <groupId>org.springframework</groupId> <artifactId>spring-jms</artifactId> <version>4.0.0.RELEASE</version> </dependency> <!-- ActiveMQ lib --> <dependency> <groupId>org.apache.activemq</groupId> <artifactId>activemq-core</artifactId> <version>5.7.0</version> </dependency>
Main.java:
package com.demo.product; import javax.jms.Connection; import javax.jms.Destination; import javax.jms.Message; import javax.jms.MessageConsumer; import javax.jms.MessageListener; import javax.jms.Session; import org.apache.activemq.ActiveMQConnectionFactory; import org.apache.activemq.command.ActiveMQObjectMessage; import org.apache.log4j.Logger; import org.apache.log4j.spi.LoggingEvent; public class Main implements MessageListener { public Main() throws Exception { // create consumer and listen queue ActiveMQConnectionFactory factory = new ActiveMQConnectionFactory("tcp://localhost:61616"); Connection connection = factory.createConnection(); Session session = connection.createSession(false, Session.AUTO_ACKNOWLEDGE); connection.start(); //////////////注意這里JMSAppender只支持TopicDestination,下面會說到//////////////// Destination topicDestination = session.createTopic("logTopic"); MessageConsumer consumer = session.createConsumer(topicDestination); consumer.setMessageListener(this); // log a message Logger logger = Logger.getLogger(Main.class); logger.info("Info Log."); logger.warn("Warn Log"); logger.error("Error Log."); // clean up Thread.sleep(1000); consumer.close(); session.close(); connection.close(); System.exit(1); } public static void main(String[] args) throws Exception { new Main(); } public void onMessage(Message message) { try { // receive log event in your consumer LoggingEvent event = (LoggingEvent)((ActiveMQObjectMessage)message).getObject(); System.out.println("Received log [" + event.getLevel() + "]: "+ event.getMessage()); } catch (Exception e) { e.printStackTrace(); } } }
說明:然后是log4j.properties:
log4j.rootLogger=INFO, stdout, jms ## Be sure that ActiveMQ messages are not logged to 'jms' appender log4j.logger.org.apache.activemq=INFO, stdout log4j.appender.stdout=org.apache.log4j.ConsoleAppender log4j.appender.stdout.layout=org.apache.log4j.PatternLayout log4j.appender.stdout.layout.ConversionPattern=%d %-5p %c - %m%n ## Configure 'jms' appender. You'll also need jndi.properties file in order to make it work log4j.appender.jms=org.apache.log4j.net.JMSAppender log4j.appender.jms.InitialContextFactoryName=org.apache.activemq.jndi.ActiveMQInitialContextFactory log4j.appender.jms.ProviderURL=tcp://localhost:61616 log4j.appender.jms.TopicBindingName=logTopic log4j.appender.jms.TopicConnectionFactoryBindingName=ConnectionFactory
其實按理說只需要這么三個文件就可以了,但是這時候執行會報錯:
javax.naming.NameNotFoundException: logTopic at org.apache.activemq.jndi.ReadOnlyContext.lookup(ReadOnlyContext.java:235) at javax.naming.InitialContext.lookup(Unknown Source) at org.apache.log4j.net.JMSAppender.lookup(JMSAppender.java:245) at org.apache.log4j.net.JMSAppender.activateOptions(JMSAppender.java:222) at org.apache.log4j.config.PropertySetter.activate(PropertySetter.java:307) ... at org.apache.activemq.ActiveMQPrefetchPolicy.<clinit>(ActiveMQPrefetchPolicy.java:39) at org.apache.activemq.ActiveMQConnectionFactory.<init>(ActiveMQConnectionFactory.java:84) at org.apache.activemq.ActiveMQConnectionFactory.<init>(ActiveMQConnectionFactory.java:137) at com.demo.product.Main.<init>(Main.java:20) at com.demo.product.Main.main(Main.java:43)
找了找官網,它是這么描述的:
The important thing is not to send ActiveMQ logs to JMS appender, as it can cause errors since the broker will want to log before the connection is established. You will also need a JNDI configuration, so that appender can find appropriate topic to send log messages to.
于是配置一個叫jndi.properties的文件,其內容為:
topic.logTopic=logTopic
然后再運行就不會報錯了。我們先來看看ActiveMQ(注意切換到Topic標簽頁下):
可以看到,主題為logTopic的消息,有3條進Queue,這3條也出Queue了。而出Queue的消息,已經被我們的監聽器收到并打印出來了:
Spring整合
需要注意的是,本例只是一個很簡單的例子,目的是闡明遠程打印日志的原理。實際項目中,一般日志服務器上運行著的,不是項目,而是專用的日志記錄器。下面,我們就把這個項目拆分成兩個項目,并用Spring來管理這些用到的Bean
修改Product項目
修改后的Product的項目結構并沒有改變,改變的只是Main類:
package com.demo.product; import org.apache.log4j.Logger; public class Main{ private static final Logger logger = Logger.getLogger(Main.class); public static void main(String[] args) throws Exception { // just log a message logger.info("Info Log."); logger.warn("Warn Log"); logger.error("Error Log."); System.exit(0); } }
這個Main類和普通的logger調用一樣,僅僅負責打印日志。有沒有覺得太簡單了呢?
Logging項目
來看看項目結構圖:
為了讓監聽器一直活著,我把Logging寫成了一個Web項目,跑在Tomcat上。index.jsp就是個Hello World字符串而已,用來驗證Logging活著。注意,在Logging項目中,已沒有Product項目中的log4j.properties和jndi.properties兩個文件。
來看看另外幾個文件:
pom.xml(每個包的目的都寫在注釋里了):
<!-- Use to cast object to LogEvent when received a log --> <dependency> <groupId>log4j</groupId> <artifactId>log4j</artifactId> <version>1.2.17</version> </dependency> <!-- Use to receive jms message --> <dependency> <groupId>org.springframework</groupId> <artifactId>spring-jms</artifactId> <version>4.0.0.RELEASE</version> </dependency> <!-- Use to load springmvc.xml --> <dependency> <groupId>org.springframework</groupId> <artifactId>spring-web</artifactId> <version>4.0.0.RELEASE</version> </dependency> <!-- ActiveMQ lib --> <dependency> <groupId>org.apache.activemq</groupId> <artifactId>activemq-core</artifactId> <version>5.7.0</version> </dependency>
web.xml
<!DOCTYPE web-app PUBLIC "-//Sun Microsystems, Inc.//DTD Web Application 2.3//EN" "http://java.sun.com/dtd/web-app_2_3.dtd" > <web-app> <context-param> <param-name>contextConfigLocation</param-name> <param-value>classpath:spring.xml</param-value> </context-param> <!-- Use to load springmvc.xml --> <listener> <listener-class> org.springframework.web.context.ContextLoaderListener </listener-class> </listener> <welcome-file-list> <welcome-file>index.jsp</welcome-file> </welcome-file-list> </web-app>
spring.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" xsi:schemaLocation=" http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-4.0.xsd"> <bean id="jmsTemplate" class="org.springframework.jms.core.JmsTemplate"> <property name="connectionFactory" ref="connectionFactory"/> </bean> <bean id="connectionFactory" class="org.springframework.jms.connection.SingleConnectionFactory"> <property name="targetConnectionFactory" ref="targetConnectionFactory"/> </bean> <bean id="targetConnectionFactory" class="org.apache.activemq.ActiveMQConnectionFactory"> <property name="brokerURL" value="tcp://localhost:61616"/> </bean> <!-- As JMSAppender only support the topic way to send messages, thus queueDestination here is useless. <bean id="queueDestination" class="org.apache.activemq.command.ActiveMQQueue"> <constructor-arg name="name" value="queue" /> </bean> --> <bean id="topicDestination" class="org.apache.activemq.command.ActiveMQTopic"> <constructor-arg name="name" value="logTopic" /> </bean> <bean id="jmsContainer" class="org.springframework.jms.listener.DefaultMessageListenerContainer"> <property name="connectionFactory" ref="connectionFactory" /> <!-- <property name="destination" ref="queueDestination" /> --> <property name="destination" ref="topicDestination" /> <property name="messageListener" ref="logMessageListener" /> </bean> <bean id="logMessageListener" class="com.demo.logging.LogMessageListener"/> </beans>
logMessageListener指向我們自己實現的日志消息處理邏輯類,topicDestination則關注topic為“logTopic”的消息,而jmsContainer把這兩個對象綁在一起,這樣就能接收并處理消息了。
最后就是偉大的監聽器了LogMessageListener了:
package com.demo.logging; import javax.jms.Message; import javax.jms.MessageListener; import org.apache.activemq.command.ActiveMQObjectMessage; import org.apache.log4j.spi.LoggingEvent; public class LogMessageListener implements MessageListener { public void onMessage(Message message) { try { // receive log event in your consumer LoggingEvent event = (LoggingEvent)((ActiveMQObjectMessage)message).getObject(); System.out.println("Logging project: [" + event.getLevel() + "]: "+ event.getMessage()); } catch (Exception e) { e.printStackTrace(); } } }
哈哈,說偉大,其實太簡單了。但是可以看到,監聽器里面就是之前Product項目中Main類里面移除的實現了MessageListener接口中的代碼。
?測試?
在執行測試前,刪掉ActiveMQ中所有的Queue,確保測試效果。
先運行Logging項目,開始Queue的監聽。再運行Product的Main類的main函數,可以先看到Main類打印到控制臺的日志:
接下來去看看Queue中的情況:
可以看到有個叫logTopic的主題的消息,進了3條,出了3條。不用想,出Queue的3條日志已經被Logging項目的Listener接收并打印出來了,現在去看看Tomcat的控制臺:
還要注意Queue中的logTopic的Consumer數量為1而不是0,這與開始的截圖不同。我們都知道這個Consumer是Logging項目中的LogMessageListener對象,它一直活著,是因為Tomcat一直活著;之前的Consumer數量為0,是因為在main函數執行完后,Queue的監聽器(也是寫日志的對象)就退出了。
通過把Product和Logging項目分別放在不同的機器上執行,在第三臺機器上部署ActiveMQ,再配置一下Product項目的log4j.properties文件和Logging項目的spring.xml文件就能用于生產環境啦。
JMSAppender類的分析
JMSAppender類將LoggingEvent實例序列化成ObjectMessage,并將其發送到JMS Server的一個指定Topic中,因此,使用此種將日志發送到遠程的方式只支持Topic方式發送,不支持Queue方式發送。我們再log4j.properties中配置了這一句:
log4j.appender.jms=org.apache.log4j.net.JMSAppender
這一句指定了使用的Appender,打開這個Appender,在里面可以看到很多setter,比如:
這些setter不是巧合,而正是對應了我們在log4j.properties中設置的其他幾個選項:
log4j.appender.jms.InitialContextFactoryName=org.apache.activemq.jndi.ActiveMQInitialContextFactory log4j.appender.jms.ProviderURL=tcp://localhost:61616 log4j.appender.jms.TopicBindingName=logTopic log4j.appender.jms.TopicConnectionFactoryBindingName=ConnectionFactory
來看看JMSAppender的activeOptions方法,這個方法是用于使我們在log4j.properties中的配置生效的:
/** * Options are activated and become effective only after calling this method. */ public void activateOptions() { TopicConnectionFactory topicConnectionFactory; try { Context jndi; LogLog.debug("Getting initial context."); if (initialContextFactoryName != null) { Properties env = new Properties(); env.put(Context.INITIAL_CONTEXT_FACTORY, initialContextFactoryName); if (providerURL != null) { env.put(Context.PROVIDER_URL, providerURL); } else { LogLog.warn("You have set InitialContextFactoryName option but not the " + "ProviderURL. This is likely to cause problems."); } if (urlPkgPrefixes != null) { env.put(Context.URL_PKG_PREFIXES, urlPkgPrefixes); } if (securityPrincipalName != null) { env.put(Context.SECURITY_PRINCIPAL, securityPrincipalName); if (securityCredentials != null) { env.put(Context.SECURITY_CREDENTIALS, securityCredentials); } else { LogLog.warn("You have set SecurityPrincipalName option but not the " + "SecurityCredentials. This is likely to cause problems."); } } jndi = new InitialContext(env); } else { jndi = new InitialContext(); } LogLog.debug("Looking up [" + tcfBindingName + "]"); topicConnectionFactory = (TopicConnectionFactory) lookup(jndi, tcfBindingName); LogLog.debug("About to create TopicConnection."); ///////////////////////////////注意這里只會創建TopicConnection//////////////////////////// if (userName != null) { topicConnection = topicConnectionFactory.createTopicConnection(userName, password); } else { topicConnection = topicConnectionFactory.createTopicConnection(); } LogLog.debug("Creating TopicSession, non-transactional, " + "in AUTO_ACKNOWLEDGE mode."); topicSession = topicConnection.createTopicSession(false, Session.AUTO_ACKNOWLEDGE); LogLog.debug("Looking up topic name [" + topicBindingName + "]."); Topic topic = (Topic) lookup(jndi, topicBindingName); LogLog.debug("Creating TopicPublisher."); topicPublisher = topicSession.createPublisher(topic); LogLog.debug("Starting TopicConnection."); topicConnection.start(); jndi.close(); } catch (JMSException e) { errorHandler.error("Error while activating options for appender named [" + name + "].", e, ErrorCode.GENERIC_FAILURE); } catch (NamingException e) { errorHandler.error("Error while activating options for appender named [" + name + "].", e, ErrorCode.GENERIC_FAILURE); } catch (RuntimeException e) { errorHandler.error("Error while activating options for appender named [" + name + "].", e, ErrorCode.GENERIC_FAILURE); } }
上面初始化了一個TopicConnection,一個TopicSession,一個TopicPublisher。咱們再來看看
/** * This method called by {@link AppenderSkeleton#doAppend} method to do most * of the real appending work. */ public void append(LoggingEvent event) { if (!checkEntryConditions()) { return; } try { ObjectMessage msg = topicSession.createObjectMessage(); if (locationInfo) { event.getLocationInformation(); } msg.setObject(event); topicPublisher.publish(msg); } catch (JMSException e) { errorHandler.error("Could not publish message in JMSAppender [" + name + "].", e, ErrorCode.GENERIC_FAILURE); } catch (RuntimeException e) { errorHandler.error("Could not publish message in JMSAppender [" + name + "].", e, ErrorCode.GENERIC_FAILURE); } }
這里使用TopicPublisher.publish()方法,把序列化的消息發布出去。可見這也證明了JMSAppender只支持以Topic方式發送消息。
樣例下載:百度網盤
鏈接: http://pan.baidu.com/s/1pJF1ybx 密碼: x5r6
參考:
http://activemq.apache.org/how-do-i-use-log4j-jms-appender-with-activemq.html
來自:http://my.oschina.net/itblog/blog/533730