在48. 面向对象的LotusScript(十四)之Log4Dom上和49. 面向对象的LotusScript(十五)之Log4Dom下笔者介绍了一个简单实用的日志记录模块,代码用LotusScript写成,在Lotus Notes传统的客户端和web应用程序里都可以使用。在XPages开发里,需要可在SSJS或Java里使用的日志功能。用Java开发时,有很多现成的日志框架,比如Log4J和JDK自带的java.util.logging包。但是这些框架包含日志器层次(logger hierarchy)、过滤器(filter)、记录器(handler)和格式器(formatter)等特性,对于Lotus Notes平台上的开发来说过于复杂。Lotus Notes的环境有特殊性,现成和方便的记录日志的载体就是Notes文档,所以笔者在借鉴了OpenNTF上的Log4Dom项目后,编写了满足典型的Lotus Notes里日志需求的Java类NotesLogger。(与Log4Dom的Java版本比较,NotesLogger放弃了Log4J的可配制记录器的架构,直接将日志写入唯一的载体——指定Notes数据库的文档;更正了bug;简化了使用方法;减少了类的数量;调整了方法的名称和签名。)记录日志的文档使用的表单和视图与LotusScript的Log4Dom一样。下面就是在一个XPage的按钮里分别用xp:eventHandler的action和actionListener属性触发managed bean里的两个测试方法,分成两条日志文档。
<xp:button value="Test eventHandler action" id="button1">
<xp:eventHandler event="onclick" submit="true"
refreshMode="complete" action="#{bean.testAction}">
</xp:eventHandler>
</xp:button>
<xp:button value="Test eventHandler actionListener" id="button2">
<xp:eventHandler event="onclick" submit="true"
refreshMode="complete" actionListener="#{bean.testActionListener}">
</xp:eventHandler>
</xp:button>
注意两处绑定使用的都是表达式语言,并且XPages继承自JSF,分别可以使用action和actionListener/actionListeners绑定两种签名不同的方法。action属性绑定的方法须为managed bean的公开方法,无参数且返回一个字符串;actionListener/actionListeners绑定的方法同样须为公开方法,但接受一个类型为javax.faces.event.ActionEvent的参数且返回类型为void。
public void testActionListener(ActionEvent ae) throws NotesException{
NotesLogger logger=new NotesLogger(XSPUtil.getSession());
logger.setLogName("test actionListener from bean");
logger.info(ae);
logger.close();
}
public void testAction() throws NotesException{
NotesLogger logger=new NotesLogger(XSPUtil.getSession());
logger.setLogName("test action from bean");
logger.info("Print message from NotesLogger");
logger.close();
}
产生的日志如下:
我们还可以稍微再简化一下NotesLogger的调用步骤。在一个managed bean里,很多方法都可能会写日志,我们可以在bean里声明一个logger字段作为共享的日志器,在bean的构造方法里或者利用managed bean的managed property初始化logger变量,如此一来在每个方法写日志时,就可省去创建日志器的环节。
下面是为此写的faces-config.xml:
<?xml version="1.0" encoding="UTF-8"?>
<faces-config>
<managed-bean>
<managed-bean-name>logger</managed-bean-name>
<managed-bean-class>starrow.BeanLogger</managed-bean-class>
<managed-bean-scope>application</managed-bean-scope>
</managed-bean>
<managed-bean>
<managed-bean-name>bean</managed-bean-name>
<managed-bean-class>starrow.Test</managed-bean-class>
<managed-bean-scope>session</managed-bean-scope>
<managed-property>
<property-name>logger</property-name>
<value>#{logger}</value>
</managed-property>
</managed-bean>
<!--AUTOGEN-START-BUILDER: Automatically generated by IBM Lotus Domino Designer. Do not modify.-->
<!--AUTOGEN-END-BUILDER: End of automatically generated section-->
</faces-config>
和相应的bean的片段:
private BeanLogger logger;
public void setLogger(BeanLogger logger) {
this.logger = logger;
}
public void test() throws NotesException{
logger.setLogName("from bean logger");
logger.debug(1);
logger.info("text");
logger.warn(true);
logger.error(new Date());
String user=XSPUtil.getSession().getEffectiveUserName();
logger.info("user: " + user);
logger.flush();
logger.close();
}
产生的日志如下:
注意上面使用的日志类是BeanLogger,这是为了能将NotesLogger作为managed bean使用而继承自它的一个扩展类。
最后,我们来看看NotesLogger和BeanLogger的代码。
package starrow;
import lotus.domino.*;
import java.util.*;
import java.text.SimpleDateFormat;
/**
* Log to Notes documents
*/
public class NotesLogger {
// built in logging levels
public static final int LEVEL_DEBUG=5;
public static final int LEVEL_INFO=4;
public static final int LEVEL_WARN = 3;
public static final int LEVEL_ERROR=2;
public static final int LEVEL_FATAL=1;
public static final int LEVEL_NONE=0;
protected int logLevel;
private String logName;
protected String dbPath;
protected Database logDb;
private Document logDoc;
private RichTextItem logItem;
private boolean written = false;
/**
* @param db A NotesDatabase used for logging
* @throws NotesException
*/
public NotesLogger(Database db) throws NotesException{
logLevel = LEVEL_DEBUG;
logDb=db;
if (!logDb.isOpen()){
if (!logDb.open()){
throw new NotesException(NotesError.NOTES_ERR_DATABASE_NOTOPEN, "Cannot open the log database.");
}
}
}
/**
* @param s notes session
* @param dbPath path of the log database
* @throws NotesException
*/
public NotesLogger(Session s, String dbPath) throws NotesException {
//Database db=s.getDatabase(null, dbPath);
this(s.getDatabase(null, dbPath));
}
/**
* @param s notes session
* @throws NotesException
*/
public NotesLogger(Session s) throws NotesException{
this(s.getCurrentDatabase());
}
public void setLogLevel(int level) {
logLevel = level;
}
public void setLogName(String name) {
logName = name;
}
public void info(Object message) throws NotesException {
log(LEVEL_INFO, message);
}
public void warn(Object message) throws NotesException {
log(LEVEL_WARN, message);
}
public void debug(Object message) throws NotesException {
log(LEVEL_DEBUG, message);
}
public void error(Object message) throws NotesException {
log(LEVEL_ERROR, message);
}
public void fatal(Object message) throws NotesException {
log(LEVEL_FATAL, message);
}
public void log (int level, Object message) throws NotesException {
if (level>logLevel) return;
SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy/MM/dd hh:mm:ss aa", Locale.US);
String text = new String();
Date theCurrentDate = new Date();
String theDate = dateFormat.format(theCurrentDate);
String theLevel = new String();
theLevel = theLevel+getLevelString(level);
//System.out.println("Level :"+level+" - logLevel "+logLevel);
text = theDate+" ["+theLevel+"]: "+message;
if (logDoc == null) {
// create document and a RTF for the log
logDoc = logDb.createDocument();
logDoc.replaceItemValue("Form", "log");
logDoc.replaceItemValue("LogName", logName);
if (logDb.getParent().isOnServer()){
logDoc.replaceItemValue("ScriptRunOn", "Server");
}else{
logDoc.replaceItemValue("ScriptRunOn", "Local");
}
logItem = logDoc.createRichTextItem("logBody");
}
// write log entry
logItem.appendText(text);
logItem.addNewLine();
written = true;
}
//convert from level numbers into strings
private String getLevelString(int level) {
String levelString="";
switch (level) {
case LEVEL_DEBUG: levelString="DEBUG" ; break;
case LEVEL_INFO: levelString="INFO" ; break;
case LEVEL_WARN: levelString="WARN" ; break;
case LEVEL_ERROR: levelString="ERROR" ; break;
case LEVEL_FATAL: levelString="FATAL" ; break;
default : levelString=levelString+"LEVEL "+level; break;
} // end switch
return levelString;
}
/**
* save the log document
* @throws NotesException
*/
public void flush() throws NotesException{
if (written) {
logDoc.save();
written=false;
}
}
/**
* close the log via recycling the underlying resource.
* @throws NotesException
*/
public void close() throws NotesException {
flush();
logDoc.recycle();
logDoc=null;
if (this.dbPath!=""){
//If the log db is not the current one, recycle it.
//Even recycling the current db doesn't cause any error.
this.logDb.recycle();
}
}
}
package starrow;
import starrow.xsp.XSPUtil;
import lotus.domino.NotesException;
public class BeanLogger extends NotesLogger implements java.io.Serializable{
private static final long serialVersionUID = 1L;
public BeanLogger(String dbPath) throws NotesException {
super(XSPUtil.getSession());
this.dbPath=dbPath;
logLevel = LEVEL_DEBUG;
}
/**
* Add a constructor with no parameter for bean creation
* @throws NotesException
*/
public BeanLogger() throws NotesException{
this("");
}
public void log(int level, Object message) throws NotesException{
//get the current db freshly
if (this.dbPath==""){
logDb=XSPUtil.getDatabase();
}else{
logDb=XSPUtil.getDatabase(this.dbPath);
}
super.log(level, message);
}
}
BeanLogger里不寻常的一点是在重载的log方法里,获取全新的日志数据库实例,这是因为XPages引擎会频繁地自动清理Java里Lotus Domino对象用到的后端对象(参看42. Lotus Notes中的垃圾回收之Java),BeanLogger用到的日志数据库和文档很快会变成无效状态,调用它会引发以下异常:
NotesException: Object has been removed or recycled
at lotus.domino.local.NotesBase.CheckObject(Unknown Source)
at lotus.domino.local.Document.save(Unknown Source)
at lotus.domino.local.Document.save(Unknown Source)
…