站内搜索: 请输入搜索关键词

当前页面: 开发资料首页J2EE 专题使用 Web Service Appender for Log4j 管理日志纪录

使用 Web Service Appender for Log4j 管理日志纪录

摘要: 深入理解 WSDL 如何映射到 SOAP。在一般环境下,无须考虑 SOAP 消息中的命名空间。然而,在某些情况下,就必须要考虑这个问题。您可能需要手工创建 SOAP 消息,并在没有其它工具辅助下处理命名空间的问题。本文将专门针对这些情况来解决相关问题。
引言
你可以使用 Web Service Appender 将日志集中到某一位置,同时,Web Service Appender 允许管理者监控、开发者调试面向服务架构(SOA)环境里可能存在的任何问题。Web Service Appender 是一种扩展 JAVA 类,它由 Log4j 的 Appender 类扩展而来。

从定义上看,SOA 是一种彼此可以互相通信的服务集合,但这些服务的内容是各自独立的,每一类服务均不受其它服务内容或服务状态的影响,并且这些服务都工作在分布式的系统架构里。在 SOA 中,Web 服务通常被用来在给定事务中处理请求,这些请求可以是遗留代码、企业级 Java Beans(EJBs) 的封装,也可以是 Java 类的封装,使用一种可以将日志信息聚集在中心位置里的日志纪录方法,能帮助您隔离缺陷和问题,并能让你更好的理解逻辑流的处理。

将特定模块或服务的日志消息纪录到一个中心位置的机制,可以把可能潜在的问题和缺陷降低到最小。

本文对 Log4j 的功能进行了大体的概述,并介绍了如何编写自定义的 Log4j Appender,这类特殊的 Appender 将日志消息编到一种特定的 Web 服务。

Log4j 快速入门
Log4j 是一种开放源代码的日志库,它已被发展为 Apache Software Foundation 日志服务项目的子项目。该库是以 IBM 在 90 年代末开发的日志库为基础的,第一版发布于 1999 年。现在它在开放源代码团体得到了广泛使用,它的体系是围绕以下三个主要概念构建起来的:

Logger
Appender
Layout
这些概念可以让您根据消息类型、消息优先级来纪录消息,您可以控制消息在何处结束及消息如何格式化。 Logger 是应用程序首先调用以初始化消息纪录的对象。当把某一消息传递给日志时,logger 会生成 LoggingEvent,对消息进行封装。之后,Logger 对象将 LoggingEvent 传递给与之关联的 Appender。

Appender 将 LoggingEvent 所包含的消息发送给指定的目标输出文件。所谓指定的文件,大多数情况下,是 Log4 属性文件。一些 Appender 存在于 Log4j 中。您也可以扩展 Appender,使之支持其它的目标文件,比如 XML 文件、控制台等等。

在 Log4j 里, LoggingEvent 被赋予某一级别,以表明它们的优先级。缺省的级别包括如下几种:

OFF:可能是最高的级别,它是用来关闭日志纪录的
FATAL:指出现了非常严重的错误事件,这些错误可能会导致应用程序异常中止
ERROR:指虽有错误,但仍允许应用程序继续运行
WARN:指运行环境潜藏着危害
INFO:指报告信息,这些信息在粗粒度级别上突出显示应用程序的进程
DEBUG:指细粒度信息事件,细粒度信息事件对于应用程序的调试是最有用的
ALL:可能是最低的级别,其目的是打开所有日志记录
Logger 和 Appender 也被赋予上述的某一级别,并且仅执行等于或高于它们自身的级别的日志请求。比如,如果一个 Appender 属于 INFO 级别,而日志请求属于 DEBUG,那么 Appender 将不会为给定的日志事件写消息。

客户端组件


客户端 log4j.properties 文件
客户端 log4j.properties 文件是一种标准文件,它包含服务或模块使用的所有 Appender。Web Service Appender 要求有一个端点(endpoint) 属性以指定所使用的日志服务。

清单 1 描述了使用 WebServiceAppender 所必需的 Web 服务客户端 Log4j 属性。 黑体显示的文本指明了将访问 WebServiceAppender 服务器端的 Appender。属性文件是使用 Log4j 的基本需求,它可以让您配置应用程序以使用多个 Appender 以及 logging severity。一旦应用程序进入运行状态或潜在的问题得到解决,您就可以轻松地修改属性文件。

清单 1:客户端 Log4j 的属性文件

#set the level of the root logger 
log4j.rootLogger = INFO, CONSOLE

#set own logger
log4j.logger.com.carmelouria.logging.test=CONSOLE

log4j.appender.CONSOLE=com.carmelouria.logging.WebServiceAppender
log4j.appender.CONSOLE.endpoint=
http://localhost:9080/log4j/services/LogAppenderService

log4j.appender.CONSOLE.layout=org.apache.log4j.PatternLayout
log4j.appender.CONSOLE.layout.ConversionPattern=%p [%t] %c{2} (%M:%L) :: %m%n





服务器的 Log4j.properties 文件
服务器 Log4j.properties 文件被用来关联客户端 Log4j 属性文件,它指定了日志的级别及服务器将如何输出消息。对于支持 Log4j 的应用程序,您可以定义多个 appender。当然,这些 appender 既可以用于客户端服务,也可以用于服务模块。

清单 2 描述了一份典型的 Log4j 属性文件,服务器端的 WebServiceAppender 使用缺省的 Log4j Appenders。服务器端的 Appender 可以潜在的调用另一个 WebServiceAppender,并将日志信息链接起来:

清单 2:服务器端的 Log4j 属性文件

#set the level of the root logger 
log4j.rootLogger = INFO, FILE

#set own logger
log4j.appender.FILE=org.apache.log4j.RollingFileAppender
log4j.appender.FILE.file=c:/temp/log4j/server/server.log
log4j.appender.FILE.layout=org.apache.log4j.PatternLayout
log4j.appender.FILE.layout.ConversionPattern=%p [%t] %c{2} (%M:%L) :: %m%n




客户端程序测试示例:
这个客户端程序示例是无格式普通 Java 对象(POJO),它记录了一条消息,并被配置为使用 Web Service Appender 来处理消息。清单 3 显示了这个示例:

清单 3:客户端应用程序使用 WebServiceAppender 的示例

package com.carmelouria.logging.test;

import org.apache.log4j.Level;
import org.apache.log4j.Logger;
import org.apache.log4j.PropertyConfigurator;

/**
* @author Carmelo Uria
*
*/
public class LoggingSample
{
private static Logger logger = Logger.getLogger(LoggingSample.class.getName());

/**
*
*/
public LoggingSample()
{
super();
PropertyConfigurator.configure("c:/temp/log4j.properties");
logger.log(Level.INFO, "LoggingSample instantiation...");
System.out.println("finished...");
}

public static void main(String[] args)
{
LoggingSample sample = new LoggingSample();
}
}




WebServiceAppender
WebServiceAppender 是必需的,它可以将消息发送到指定的 Web 服务。WebServiceAppender 继承了 org.log4j.Appender,它允许使用 log4.properties,并成为有效的 Log4j Appender。

WebServiceAppender 使用基于 XML 的远程过程调用 (JAX-RPC) 的 Java API,来将消息发送到服务器。JAX-RPC 是一种规范,它描述使用 RPC 和 XML 构建 Web 服务和 Web 服务客户端的应用编程接口 (API) 和约定。JAX-RPC 又被称为 JSR 101。

LoggingEvent 通过 SOAPElement 被分割并表示为 XML。javax.xml.soap.SOAPElement 接口意味着服务端点接口将包含一个参数,或返回 javax.xml.soap.SOAPElement 类型的值,以对应于 schema 中每个使用的地方。从本质上看,它是 XML 参数的封装,且没有相应的序列化/反序列化 JAVA 类。例如,一旦客户请求记录一个消息,就会创建一个 LoggEvent 对象,然后传送给 Appender。在这种情况下,Appender 就是 WebServiceAppender。Appender 检索事件,并在解析事件中的信息。一些额外的信息会被加入,如主机名称,这样您就知道这些消息来自哪个系统。同时,append 方法也将消息转换为 SOAPElement,这样就可以通过 executeWebService 方法将消息传递给 Web 服务。使用 SOAPElement 充分考虑了 WebServiceAppender 未来版本的可扩展性问题。

清单4:执行 WebServiceAppender 服务的 Append 方法

protected void append(LoggingEvent event)
{
// create Web Service client using endpoint
if (endpoint == null)
{
System.out.println("no endpoint set. Check configuration file");
System.out.println("[" + hostname + "] " + this.layout.format(event));
return;
}
executeWebService(event);
}
private void executeWebService(LoggingEvent event)
{
SoapClient client = new SoapClient();
URL endPoint = null;
try
{
endPoint = new URL(getendpoint());
}
catch (MalformedURLException e1)
{
e1.printStackTrace();
}
String nameSpace = "http://ejb.logging.carmelouria.com";
QName serviceName = new QName(nameSpace, "LogAppenderServiceService");
QName operation = new QName(nameSpace, "log");
QName port = new QName(nameSpace, "LogAppenderService");
Parameter message =
new Parameter("log", Constants.XSD_ANY, SOAPElement.class, ParameterMode.IN);
try
{
/**
*create SOAPElement from LoggingEvent need hostname
*/

Level level = event.getLevel();
String sysLog = "" + new Integer(level.getSyslogEquivalent()).toString()
+ "
";
String startTime = new Long(LoggingEvent.getStartTime()).toString();
String timeTag = "" + startTime + "";
String hostName = "" + InetAddress.getLocalHost() +
"
";
String threadName = "<thread_name>" + event.getThreadName()
+"</thread_name>";
String logger = "" + event.getLoggerName() + "";
String eventMessage = "" + event.getRenderedMessage() +
"
";
String log = hostName + threadName + logger + timeTag + sysLog +
eventMessage;
String throwableInformation[] = event.getThrowableStrRep();
if (throwableInformation != null)
{
for (int i = 0; i < throwableInformation.length; i++)
{
String throwable = "<throwable_information>" + throwableInformation +
"</throwable_information>";
log += throwable;
}
}

String ndcString = event.getNDC();
if (throwableInformation != null)
{
String throwable = " + ndcString + ";
log += throwable;
}

message.setValue(SOAPElementFactory.create(" + log + "));
}
catch (UnknownHostException unknownHostException)
{
unknownHostException.printStackTrace();
}
catch (SOAPException e2)
{
e2.printStackTrace();
}

Parameter resultType = newParameter("logResponse",
Constants.WEBSERVICES_VOID,
Object.class,
ParameterMode.OUT);

Parameter[] parameters = { message };

try
{
// execute client
Object result =
client.execute(endPoint, serviceName, operation, "wrapped", null,
port, resultType, parameters);
if ((result != null) && (result instanceof String))
System.out.println((String) result);
}
catch (ClientException e)
{
e.printStackTrace();
}
}




[i]Hostname

不幸的是,Log4j 的 LoggingEvent 没有包含 Hostname,而 Hostname 是 Web Service Appender 众多需求之一。在创建 SOAPElement 以前,您可以用下面的语句将 Hostname 添加到 XML 文件里:

String hostName = "" + InetAddress.getLocalHost() + "";

SoapElementFactory
SoapElementFactory 是主要用于创建 SOAPElement 的类。它同时支持创建 IBM 和 Java 的 SOAPElement 实现,如清单 5 所示:

清单 5:使用 SoapElementFactory 类的创建方法

public static javax.xml.soap.SOAPElement create(String xml) throws SOAPException
{
com.ibm.ws.webservices.engine.xmlsoap.SOAPFactory factory =
(com.ibm.ws.webservices.engine.xmlsoap.SOAPFactory)
com.ibm.ws.webservices.engine.xmlsoap.SOAPFactory
.newInstance();

SOAPElement element =
(javax.xml.soap.SOAPElement)factory.createElementFromXMLString(xml);
return(element);
}
public static SOAPElement create(String arg0, String arg1, String arg2,
boolean ibmSoapElement) throws
SOAPException
{
if (ibmSoapElement)
{
SOAPFactory soapFactory =
(com.ibm.ws.webservices.engine.xmlsoap.SOAPFactory)
com.ibm.ws.webservices.engine.xmlsoap.SOAPFactory.newInstance();
return (soapFactory.createSOAPElement(arg0, arg1));
}

javax.xml.soap.SOAPFactory soapFactory =
javax.xml.soap.SOAPFactory.newInstance();
return (soapFactory.createElement(arg0, arg1, arg2));
}




SoapClient
SoapClient 类封装了 Call 接口的 JAX-RPC 实现,javax.xml.rpc.Call 接口提供了对服务端点动态调用的支持。javax.xml.rpc.Service 接口就好象是创建 Call 实例的工厂。

清单 6 说明了客户端如何动态调用服务。这允许对服务进行变更,而无需生成客户端代理来访问远程服务。

清单 6:使用 SoapClient 类的调用方法

 private Object call(SoapService service, QName operation, QName portType, 
String operationStyleProperty,
String encodingURIProperty, Parameter returnType,
Parameter[] parameters) throws ClientException
{
QName portName;
String response = null;
Object results = null;
Call call = null;

try
{
// check to see if Service object exists
if (service == null)
throw new ClientException("Invalid Service object. It maybe null.");

// retrieve call from Service object
call = service.createCall();
call.setOperationName(operation);
call.setPortTypeName(portType);

// check call object
if (call == null)
throw new ClientException("invalid operation. Call object is null.");

// set default values
if (operationStyleProperty == null)
call.setProperty(Call.OPERATION_STYLE_PROPERTY,
OPERATION_STYLE_DOCUMENT_TYPE);
else
call.setProperty(Call.OPERATION_STYLE_PROPERTY,
operationStyleProperty);

if (encodingURIProperty == null)
call.setProperty(Call.ENCODINGSTYLE_URI_PROPERTY,
ENCODING_LITERAL);
else
call.setProperty(Call.ENCODINGSTYLE_URI_PROPERTY,
encodingURIProperty);

call.setTargetEndpointAddress(service.getServiceEndPoint());

//create Parameter class for SoapClient
for (int i = 0; i < parameters.length; i++)
{
Class classObject = parameters.getClassObject();

if (classObject != null)
call.addParameter(parameters[i].getName(), parameters[i].getXmlType(),
parameters[i].getClassObject(), parameters[i].getMode());
else
call.addParameter(parameters[i].getName(), parameters[i].getXmlType(),
parameters[i].getMode());
}

// pass parameter as ReturnType
if (returnType != null)
{
if (returnType.getClassObject() != null)
call.setReturnType(returnType.getXmlType(), returnType.getClassObject());
else
call.setReturnType(returnType.getXmlType());
}

Object[] request = new Object[parameters.length];

// add parameter values
for (int i = 0; i < request.length; i++)
{
request[i] = parameters[i].getValue();
}

results = call.invoke(request);
}
catch (SOAPFaultException e)
{
System.out.println(e.getFaultString());
e.getStackTrace();
throw new ClientException(e.getLocalizedMessage(), e);
}
catch (ServiceException serviceException)
{
serviceException.getStackTrace();
throw new ClientException(serviceException.getLocalizedMessage(),
serviceException);
}
catch (RemoteException exception)
{
exception.printStackTrace();
throw new ClientException(exception.getLocalizedMessage(), exception);
}

return (results); }




服务组件


Log4j.server.properties
Log4j.server.properties 文件包含了一个基本的 Log4j 配置文件,该文件可以让您指定把哪些日志发送给 Web 服务系统。

[i]清单 7:Log4j.server.properties 文件


#set the level of the root logger 
log4j.rootLogger = INFO, FILE

#set own logger
log4j.appender.FILE=org.apache.log4j.RollingFileAppender
log4j.appender.FILE.file=c:/temp/log4j/server/server.log
log4j.appender.FILE.layout=org.apache.log4j.PatternLayout
log4j.appender.FILE.layout.ConversionPattern=%p [%t] %c{2} (%M:%L) :: %m%n




LogAppenderBean.java
LogAppenderBean.java 是 Web Service Appender 服务所要使用的 EJB。该服务启动 LogAppenderBean 以处理来自每个 Web Service Appender 客户端的每一个请求。

清单 8 显示了来自 WebServiceAppender EJB 的 log 方法,该方法解析来自客户端的消息,并将客户端信息纪录到服务的服务器端。

清单 8:LogAppenderBean 的 log 方法

public void log(SOAPElement message)
{
try
{
InputSource source = ((IBMSOAPElement)
message).toInputSource(false);
Document document = Parser.parse(source);
String log = null;
String hostname =
document.selectSingleNode("//hostname").getText();
String threadName =
document.selectSingleNode("//thread_name").getText();
String syslog =
document.selectSingleNode("//syslog").getText();
String startTime = new Long(
document.selectSingleNode("//start_time").
getText()).toString();
log = '[' + startTime + ':' + hostname + ':' + threadName +
"] " + document.selectSingleNode(
"//message").getText();
// retrieve any throwable messages
List throwableList = document.selectNodes(
"//throwable_information");
if(throwableList != null)
{
Iterator throwables = throwableList.iterator();
while(throwables.hasNext())
{
log += '\n' + ((Node)throwables.next()).getText();
}

log += '\n';
}

logger.log(Level.toLevel(new Integer(syslog).intValue()),
log);
logger.log(Level.INFO,log);
}
catch(ParserException parseException)
{
parseException.printStackTrace();
}
catch (SAXException e)
{
e.printStackTrace();
}
}




通过 IBM SOAPElement 的 InputSource,每一个 SOAPElement 的内容都会被检索。目前,只有 IBM WebSphere&reg; Application Server (Application Server) 支持这些代码(请参阅参考资料)。 然而,如果您移除 IBM SOAPElement,那么您就可以在任何应用服务器上使用这些代码。IBM SOAPElement 内置的性能优化也适用于 Application Server。

每一个 SOAPElement 都使用 Dom4j 来读取、解析和转换。Dom4j 是一种在内存中表示 XML 树的对象模型。Dom4j 提供了一组易于使用的 API,从而为我们提供了一整套强大的功能来处理、操作或定位 XML,使用 XPath 和 XSLT 进行工作,以及与 SAX、 JAXP、DOM 集成。

除了可以使用任意的 XML 解析器外,DOM4J 还允许使用任意的 SAX 解析器,为实现更好的性能,还允许使用所有标准的 XSLT 转换器。转换被用来析取发送给 Web Service Appender 的客户端 LoggingEvent 的元素。

如果您允许使用 SOAPElement,那么就需要在代码中维持最大限度的灵活性。Web Service Appender 服务可以被修改,以支持所有发送给服务的 XML。

输出
下面的示例展示了 Web Service Appender 的可能的输出:

INFO [WebContainer : 0] ejb.LogAppenderBean (log:?) :: [1111513482641:OO7-64BIT/9.48.114.183:main]LoggingSample instantiation...

OO7-64BIT/9.48.114.183 是机器名和 IP 地址,而 main 是日志所在处的方法名。

结束语
Web Service Appender 是将日志集中到某一位置的基本工具。由于 Web Service Appender 是 Log4j 的 Appender 类的子集,因而配置和使用 Appender 都非常简单易懂。您可以修改 Log4j 的属性文件,这样,使用 Log4j 的现有应用程序和服务就可以马上使用 Web Service Appender。

参考资料

您可以参阅本文在 developerWorks 全球站点上的 英文原文。


在 Apache 网站上学习更多关于 Log4j 的知识。


使用 Dom4j 读、写、定位、创建和修改 XML 文档。


查询更多关于 SOAPElement 接口 和 Call 接口的信息。


想要得到来自 DB2&reg;、Lotus&reg;、Rational&reg;、Tivoli&reg;和 WebSphere&reg; 的开发工具和中间件产品? 您可以免费下载这些产品的 评测版,也可以选择 Linux 或 Windows 版本的免费 Software Evaluation Kit。


访问 Developer Bookstore 以获得完整的技术书籍清单,其中有数百本 Web 服务主题的书籍。


加入 developerWorks 社区,一起参加 developerWorks blogs。


IBM developerWorks 小组在全世界有大量的技术讲座,您可以免费参加。


想获取更多信息吗?developerWorks SOA 和 Web 服务专区有大量的信息文章,并有关于如何开发 Web 服务应用程序的初级、中级、高级教程。
↑返回目录
前一篇: 手工创建的 SOAP 消息中命名空间的处理
后一篇: 大道至简-Java之23种模式一点就通