阿里云-云小站(无限量代金券发放中)
【腾讯云】云服务器、云数据库、COS、CDN、短信等热卖云产品特惠抢购

Spring+Log4j+ActiveMQ实现远程记录日志——实战+分析

174次阅读
没有评论

共计 14598 个字符,预计需要花费 37 分钟才能阅读完成。

这几天由于工作需要,研究了一下远程打印日志的方式,于是将研究结果记录在此,分享给大家,希望对大家有所帮助。

应用场景

随着项目的逐渐扩大,日志的增加也变得更快。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 下对应的系统架构文件夹

Spring+Log4j+ActiveMQ 实现远程记录日志——实战 + 分析

3. 双击 activemq.bat 启动,如果看到类似下面的页面,就代表 activemq 启动好了:

Spring+Log4j+ActiveMQ 实现远程记录日志——实战 + 分析

然后打开浏览器,输入地址:http://localhost:8161 进入管理页面,用户名 admin,密码 admin:

Spring+Log4j+ActiveMQ 实现远程记录日志——实战 + 分析

可以点击 Manage ActiveMQ broker 进入 Queue 的查看界面。

实战

我用 Maven 来管理项目,方便维护各种依赖的 jar 包。先看下项目结构:

Spring+Log4j+ActiveMQ 实现远程记录日志——实战 + 分析

项目不复杂,主要是 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)
 

为什么会报错呢?来看看 JMSAppender 的 javadoc 文档,它是这么描述的:

Spring+Log4j+ActiveMQ 实现远程记录日志——实战 + 分析

大意是说,JMSAppender 需要一个 jndi 配置来初始化一个 JNDI 上下文(Context)。因为有了这个上下文才能管理 JMS Topic 和 topic 的连接。于是为项目配置一个叫 jndi.properties 的文件,其内容为:

topic.logTopic=logTopic

然后再运行就不会报错了。我们先来看看 ActiveMQ(注意切换到 Topic 标签页下):

Spring+Log4j+ActiveMQ 实现远程记录日志——实战 + 分析

可以看到,主题为 logTopic 的消息,有 3 条进 Queue,这 3 条也出 Queue 了。而出 Queue 的消息,已经被我们的监听器收到并打印出来了:

Spring+Log4j+ActiveMQ 实现远程记录日志——实战 + 分析

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 项目

来看看项目结构图:

Spring+Log4j+ActiveMQ 实现远程记录日志——实战 + 分析

为了让监听器一直活着,我把 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 spring.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 spring.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 类打印到控制台的日志:

Spring+Log4j+ActiveMQ 实现远程记录日志——实战 + 分析

接下来去看看 Queue 中的情况:

Spring+Log4j+ActiveMQ 实现远程记录日志——实战 + 分析

可以看到有个叫 logTopic 的主题的消息,进了 3 条,出了 3 条。不用想,出 Queue 的 3 条日志已经被 Logging 项目的 Listener 接收并打印出来了,现在去看看 Tomcat 的控制台:

Spring+Log4j+ActiveMQ 实现远程记录日志——实战 + 分析

还要注意 Queue 中的 logTopic 的 Consumer 数量为 1 而不是 0,这与开始的截图不同。我们都知道这个 Consumer 是 Logging 项目中的 LogMessageListener 对象,它一直活着,是因为 Tomcat 一直活着;之前的 Consumer 数量为 0,是因为在 main 函数执行完后,Queue 的监听器(也是写日志的对象)就退出了。

通过把 Product 和 Logging 项目分别放在不同的机器上执行,在第三台机器上部署 ActiveMQ(当然你可以把 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,比如:

Spring+Log4j+ActiveMQ 实现远程记录日志——实战 + 分析

这些 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。咱们再来看看这个 Appender 的 append 方法:

/**
 * 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://linux.linuxidc.com/

用户名与密码都是 www.linuxidc.com

具体下载目录在 /2015 年资料 /12 月 /13 日 /Spring+Log4j+ActiveMQ 实现远程记录日志——实战 + 分析 /

下载方法见 http://www.linuxidc.com/Linux/2013-07/87684.htm

—————————————— 分割线 ——————————————

推荐阅读:

Spring 下 ActiveMQ 实战  http://www.linuxidc.com/Linux/2015-11/124854.htm

Linux 系统下 ActiveMQ 安装 http://www.linuxidc.com/Linux/2012-03/55623.htm

Ubuntu 下的 ACTIVEMQ 服务器 http://www.linuxidc.com/Linux/2008-07/14587.htm

CentOS 6.5 启动 ActiveMQ 报错解决 http://www.linuxidc.com/Linux/2015-08/120898.htm

Spring+JMS+ActiveMQ+Tomcat 实现消息服务 http://www.linuxidc.com/Linux/2011-10/44632.htm

Linux 环境下面 ActiveMQ 端口号设置和 WEB 端口号设置 http://www.linuxidc.com/Linux/2012-01/51100.htm

ActiveMQ 的详细介绍 :请点这里
ActiveMQ 的下载地址 :请点这里

参考:

http://activemq.apache.org/how-do-i-use-log4j-jms-appender-with-activemq.html

本文永久更新链接地址 :http://www.linuxidc.com/Linux/2015-12/126163.htm

正文完
星哥说事-微信公众号
post-qrcode
 
星锅
版权声明:本站原创文章,由 星锅 2022-01-21发表,共计14598字。
转载说明:除特殊说明外本站文章皆由CC-4.0协议发布,转载请注明出处。
【腾讯云】推广者专属福利,新客户无门槛领取总价值高达2860元代金券,每种代金券限量500张,先到先得。
阿里云-最新活动爆款每日限量供应
评论(没有评论)
验证码
【腾讯云】云服务器、云数据库、COS、CDN、短信等云产品特惠热卖中