Servlet, Struts2 Action, SpringMVC Control是否单例测试结果


同事咨询Servlet, Struts2 Action, SpringMVC Control的应用情况,针对是否单例,运行工程实际测试了一下。

测试方法:

  • 在Servlet.service方法中打印对象的hashcode值
  • 在Action.excute方法中打印对象的hashcode值
  • 在Control.XXX方法中打印对象的hashcode值

测试结果:

  • Servlet是单例的,多个请求共用一个对象。
  • Struts2 Action是多例的,每次请求都新生成一个实例对象。
  • SpringMVC Control是单例的,多个请求共用一个对象。

结论:

对于高并发的Web应用,建议不使用Struts,每个请求新生成一个对象,是个比较大的开销,同时Struts在开发上限制太多和直接写Servlet无太大区别,缺乏灵活性。

 

Advertisements

使用POI输出超过65536行大Excel(SXSSF技术)


一直以来都用POI做Excel导出,之前导出的Excel格式都是2003版的——XLS,最近客户要求导出超过65536条数据,超出了XLS的上限,所以研究起XSSFWorkbook的用法。

原本以为只是把HSSFWorkbook替换成XSSFWorkbook就能导出数据,无奈总是报告内存溢出。放狗查找资料,最终锁定SXSSFWorkbook类。

首先参考Upgrading to POI 3.5, including converting existing HSSF Usermodel code to SS Usermodel (for XSSF and HSSF)完成HSSFWorkbook转换为XSSFWorkkbook的转换,注意使用CreationHelper类。

再参考SXSSF (Streaming Usermodel API)完成大文件的输出

import junit.framework.Assert;
import org.apache.poi.ss.usermodel.Cell;
import org.apache.poi.ss.usermodel.Row;
import org.apache.poi.ss.usermodel.Sheet;
import org.apache.poi.ss.usermodel.Workbook;
import org.apache.poi.ss.util.CellReference;
import org.apache.poi.xssf.streaming.SXSSFWorkbook;

public static void main(String[] args) throws Throwable {
    SXSSFWorkbook wb = new SXSSFWorkbook(100); // keep 100 rows in memory, exceeding rows will be flushed to disk
    Sheet sh = wb.createSheet();
    for(int rownum = 0; rownum < 1000; rownum++){
        Row row = sh.createRow(rownum);
        for(int cellnum = 0; cellnum < 10; cellnum++){
            Cell cell = row.createCell(cellnum);
            String address = new CellReference(cell).formatAsString();
            cell.setCellValue(address);
        }

    }

    // Rows with rownum < 900 are flushed and not accessible
    for(int rownum = 0; rownum < 900; rownum++){
      Assert.assertNull(sh.getRow(rownum));
    }

    // ther last 100 rows are still in memory
    for(int rownum = 900; rownum < 1000; rownum++){
        Assert.assertNotNull(sh.getRow(rownum));
    }

    FileOutputStream out = new FileOutputStream("/temp/sxssf.xlsx");
    wb.write(out);
    out.close();    // dispose of temporary files backing this workbook on disk
    wb.dispose();
}

写在最后

  • SXSSF通过把数据缓存到磁盘临时文件,完成超大Excel的输出。
  • 一个Sheet对应一个临时文件
  • 通过SXSSFWorkbook构造函数指定多少条写入临时文件,实际测试证明,维持默认100条能够获得较好的性能。
  • 最后不要忘记使用SXSSFWorkbook.dispose()方法清除临时文件,该方法返回true,则表示所有的临时文件都已经清除。
    该方法实际最终执行SheetDataWriter.dispose(),此方法内部执行java.io.Writer.close()方法释放资源,同时执行java.io.File.delete()方法删除临时文件。
  • 如果碰到临时文件空间的问题,可以尝试启动GZIP,用CPU来换空间了。

GZIP启用方法:

SXSSFWorkbook wb = new SXSSFWorkbook(); 
  wb.setCompressTempFiles(true); // temp files will be gzipped

XML创建/解析/验证


最近用到XML结合JMS TextMessage传输数据的技术。采用TextMessage+XML格式传输对象是考虑到这个平台不仅仅使用Java语言开发,同时还需要能够支持其他语言接入:比如C, Ruby, Perl, Python, PHP, ActionScript/Flash, Smalltalk,借助于ActiveMQ提供的Stomp,这些非Java语言能够很方便的就接入平台。

总结下来整个传输过程如下(Java to Java):

DTO对象->XML对象->XML文本->JMS TextMessage->XML文本->XML对象->DTO

对象

涉及技术点:

  1. JavaDTO对象如何转变为XML对象
  2. 如何从XML对象中获得XML文本
  3. 如何通过XML文本构造XML对象
  4. 遍历XML对象创建JavaDTO对象

先说第一点,JavaDTO对象如何转变为XML对象

// 创建DocumentBuilderFactory,创建和解析XML都需要
DocumentBuilderFactory domfac = DocumentBuilderFactory.newInstance();
// 创建DocumentBuilder
DocumentBuilder builder = domfac.newDocumentBuilder();
// 新建文档
Document doc = builder.newDocument();
doc.setXmlVersion("1.0");
// 创建文档根节点
Element root = doc.createElement("root");
// 循环创建叶子节点
Element leaf = doc.createElement("leaf");
// 添加叶子节点属性
leaf.setAttribute("name", "从Java对象中获取");
// 增加叶子节点至根节点
root.appendChild(leaf);

至此,XML对象已经创建完成,现在需要从XML对象中获取XML文本。

Continue reading “XML创建/解析/验证”

Log4j中添加自定义的日志级别


Log4j自带了trace,debug,info.warn,error,fatal几个日志级别。

由于项目需要拆分业务和系统日志,而我又想偷懒,所以决定在log4j日志框架上增加自定义的日志级别专门用于业务日志输出。需要增加的日志级别定为audit,在log4j所有自带日志级别之上。

Log4j官方推荐的增加自定义的日志级别的方法是“继承org.apache.log4j.Level类”,具体的可以参考log4j中的示例XLevel。由于系统开发时不直接使用Log4j,而是通过slf4j调用log4j,官方推荐的方法行不通,所以决定修改log4j和slf4j相关源代码。

Log4j源代码修改点如下:

org.apache.log4j.Logger添加audit方法

public void audit(Object message) {
	if (repository.isDisabled(Level.AUDIT_INT)) {
		return;
	}

	if (Level.AUDIT.isGreaterOrEqual(this.getEffectiveLevel())) {
		forcedLog(FQCN, Level.AUDIT, message, null);
	}
}

org.apache.log4j.Priority类添加AUDIT_INT属性

public final static int AUDIT_INT = 60000; //日志级别大于log4j包含的所有日志级别,便于后期通过Threshold属性过滤日志

org.apache.log4j.Level类添加AUDIT相关属性和方法

public static final Level AUDIT = new Level(AUDIT_INT, "AUDIT", 0);public static Level toLevel(int val, Level defaultLevel) {
	switch(val) {
		case ALL_INT: return ALL;
		case AUDIT_INT: return Level.AUDIT;
		case DEBUG_INT: return Level.DEBUG;
		case INFO_INT: return Level.INFO;
		case WARN_INT: return Level.WARN;
		case ERROR_INT: return Level.ERROR;
		case FATAL_INT: return Level.FATAL;
		case OFF_INT: return OFF;
		case TRACE_INT: return Level.TRACE;
		default: return defaultLevel;
	}
}

至此Log4j修改完成,下面修改slf4j相关源代码。slf4j需要修改slf4j-api和slf4j-log4j12两个包 Continue reading “Log4j中添加自定义的日志级别”

apache-derby-数据库使用注意事项


Apache Derby数据库是一个小巧的、支持内嵌模式的数据库,他实现了SQL-99 和 SQL-2003 标准。最近由于项目的原因,开始使用Derby,以下是一些使用心得:

  1. SQL语句不支持rownum关键字

    虽然不直接支持rownum关键字,但是可以通过ROW_NUMBER函数实现。用法如下:

    SELECT * FROM (
      SELECT
        ROW_NUMBER() OVER () AS rownum,
        columns
      FROM tablename
    ) AS foo
    WHERE rownum

    参考:OLAPRowNumber

  2. 修改derby.log文件存储目录

    derby.log记录了数据库启动、运行、结束的日志信息,这个日志文件默认并不会跟随着数据库存储,而是存放在了工程根目录下。通过设置derby.system.home属性,我们可以解决derby.log日志文件存储路径的问题。在Java应用启动的时候增加一下参数:

    -Dderby.system.home=/home/leo/derby

    系统启动之后,derby.log文件就会存放在/home/leo/derby目录下

  3. 关闭Derby数据库

    Apache Derby在作为内嵌数据库运行的时候, 我们需要在系统退出之前停止Derby数据库。方法如下:

    DriverManager.getConnection("jdbc:derby:;shutdown=true");

    可以直接用以上语句停止数据库。执行停止命令时,数据库名称不是必须的。Apache Derby数据库在停止成功后,会抛出java.sql.SQLException,这是正常的。 更详细的描述请参考:Shutting down the system

  4. 同一个JVM实例中如何重启Derby

    首先按照第三步关闭数据库,然后执行下面的代码,需要特别注意的是newInstance()方法。重新注册驱动,必须增加newInstance()方法。

    Class.forName(org.apache.derby.jdbc.EmbeddedDriver).newInstance();

    或者增加 deregister=false属性,然后关闭数据库。通过设置deregister=false属性后,可以避免重新注册驱动。

    DriverManager.getConnection("jdbc:derby:;shutdown=true;deregister=false");

FWK005 parse may not be called while parsing.


最近在使用javax.xml.parsers.DocumentBuilder解析xml文件的时候偶尔会出错:

org.xml.sax.SAXException: FWK005 parse may not be called while parsing.
        at com.sun.org.apache.xerces.internal.parsers.DOMParser.parse(DOMParser.java:263)
        at com.sun.org.apache.xerces.internal.jaxp.DocumentBuilderImpl.parse(DocumentBuilderImpl.java:284)
        at javax.xml.parsers.DocumentBuilder.parse(DocumentBuilder.java:208)
...

跟踪了一下代码,发现这个异常是在com.sun.org.apache.xerces.internal.parsers.DTDConfiguration.parse(DTDConfiguration.java:546)抛出来的。该段代码结构如下:

if(fParseInProgress) {
    throw new XNIException("FWK005 parse may not be called while parsing.");
}

fParseInProgress = true;

// 解析xml文件

finally {
    fParseInProgress = false;
}

从程序逻辑来看,如果当前DocumentBuilder对象正在转换文档,此时再次请求转换文档,那么直接抛出XNIException(“FWK005 parse may not be called while parsing.”);异常。

这个问题也比较好解决,一种是对转换xml文档的方法,增加synchronized关键字,这样子不会有两个线程同时访问方法。

还有一种方法是创建一个DocumentBuilder类型的ThreadLocal变量,这样子每个线程都拥有自己的DocumentBuilder对象,能够同时转换多个xml文件。代码如下:

private static ThreadLocal docBuildeIns = new ThreadLocal() {
    protected DocumentBuilder initialValue() {
        try {
            return DocumentBuilderFactory.newInstance().newDocumentBuilder();
        } catch (ParserConfigurationException e) {
            String msg = "DocumentBuilder 对象初始化失败!";
            log.error(msg, e);
            throw new IllegalStateException(msg, e);
        }
    }
};

解析xml文件时的调用方法:

docBuildIns.get().parse(File);

get()方法返回此线程局部变量的当前线程副本中的值。如果变量没有用于当前线程的值,则先将其初始化为调用 initialValue() 方法返回的值。

跟踪 C3p0 连接池连接泄漏之参数优化与日志分析


上一篇:跟踪 C3p0 连接池连接泄漏 我们有谈到c3p0的debugUnreturnedConnectionStackTraces、unreturnedConnectionTimeout参数的说明,此篇日志进一步介绍应用这两个参数后的一些经验。

首先,为了跟踪问题,debugUnreturnedConnectionStackTraces参数肯定是设置为true的。下面主要说明unreturnedConnectionTimeout参数的设置,unreturnedConnectionTimeout参数是在连接被应用程序checkout后指定时间内未checkin则由连接缓冲池执行kill操作,同时打印堆栈跟踪信息。在我的应用里,maxIdleTime的设置是120秒,所以,我把unreturnedConnectionTimeout设置成150秒,如果达到最大存活时间后,连接还是不能被连接缓冲池正常关闭,那么肯定出现了连接泄漏,此时,再过30秒后,由连接缓冲池主动执行kill。

通过以上设置后,确实收获了一些成果,通过分析日志,找到了连接泄漏问题代码。异常信息如下:

2011-04-06 15:49:42,599 INFO : com.mchange.v2.resourcepool.BasicResourcePool.removeResource(BasicResourcePool.java:1395) – Logging the stack trace by which the overdue resource was checked-out.
java.lang.Exception: DEBUG ONLY: Overdue resource check-out stack trace.
at com.mchange.v2.resourcepool.BasicResourcePool.checkoutResource(BasicResourcePool.java:506)
at com.mchange.v2.c3p0.impl.C3P0PooledConnectionPool.checkoutPooledConnection(C3P0PooledConnectionPool.java:525)
at com.mchange.v2.c3p0.impl.AbstractPoolBackedDataSource.getConnection(AbstractPoolBackedDataSource.java:128)
at org.springframework.orm.hibernate3.LocalDataSourceConnectionProvider.getConnection(LocalDataSourceConnectionProvider.java:82)
at org.hibernate.jdbc.ConnectionManager.openConnection(ConnectionManager.java:423)
at org.hibernate.jdbc.ConnectionManager.getConnection(ConnectionManager.java:144)
at org.hibernate.jdbc.AbstractBatcher.prepareQueryStatement(AbstractBatcher.java:139)
at org.hibernate.loader.Loader.prepareQueryStatement(Loader.java:1547)
at org.hibernate.loader.Loader.doQuery(Loader.java:673)
at org.hibernate.loader.Loader.doQueryAndInitializeNonLazyCollections(Loader.java:236)
at org.hibernate.loader.Loader.doList(Loader.java:2220)
at org.hibernate.loader.Loader.listIgnoreQueryCache(Loader.java:2104)
at org.hibernate.loader.Loader.list(Loader.java:2099)
at org.hibernate.loader.custom.CustomLoader.list(CustomLoader.java:289)
at org.hibernate.impl.SessionImpl.listCustomQuery(SessionImpl.java:1695)
at org.hibernate.impl.AbstractSessionImpl.list(AbstractSessionImpl.java:142)
at org.hibernate.impl.SQLQueryImpl.list(SQLQueryImpl.java:152)
at com.leo.dao.XXXDAO.queryXXX(XXXDAO.java:20)
at jsp_servlet._keyareas._country.__taskcdb_add._jspService(__taskcdb_add.java:195)
at weblogic.servlet.jsp.JspBase.service(JspBase.java:34)
at weblogic.servlet.internal.StubSecurityHelper$ServletServiceAction.run(StubSecurityHelper.java:227)
at weblogic.servlet.internal.StubSecurityHelper.invokeServlet(StubSecurityHelper.java:125)
at weblogic.servlet.internal.ServletStubImpl.execute(ServletStubImpl.java:300)
at weblogic.servlet.internal.ServletStubImpl.onAddToMapException(ServletStubImpl.java:416)
at weblogic.servlet.internal.ServletStubImpl.execute(ServletStubImpl.java:326)
at weblogic.servlet.internal.ServletStubImpl.execute(ServletStubImpl.java:183)
at weblogic.servlet.internal.RequestDispatcherImpl.invokeServlet(RequestDispatcherImpl.java:526)
at weblogic.servlet.internal.RequestDispatcherImpl.forward(RequestDispatcherImpl.java:253)

通过堆栈信息,很容易就确认了问题点。这个问题点是由于DAO使用不规范导致的,XXXDAO继承了HibernateDaoSupport,在方法中使用了getHibernateTemplate().getSessionFactory().openSession()获得session,然后通过session.createSQLQuery(),随后却并没有执行session.close()。

关于日志大小:在目前maxIdleTime=120,maxPoolSize=18,minPoolSize=5,idleConnectionTestPeriod=30的情况下,日志PatternLayout %d %-5p: %l – %m%n,一天的日志记录情况大约是12M,在硬盘空间允许的情况下建议按天存储日志。