Android端RSA加密数据送Java服务端解密时出现BadPaddingException


原因:Android系统使用的虚拟机(dalvik)跟SUN标准JDK是有所区别的,其中他们默认的RSA实现就不同。即Android端用Cipher.getInstance(“RSA”)方法进行加密时,使用的provider是Bouncycastle Security provider,Bouncycastle Security provider默认实现的是“RSA/None/NoPadding”算法,而服务器(PC)端用Cipher.getInstance(“RSA”)进行解密时,使用的是Sun的security provider,实现的是“RSA/None/PKCS1Padding”算法,所以,解密时会失败。

正确的设置方法:Java服务端代码

Cipher cipher = Cipher.getInstance(“RSA”);

Android端代码

Cipher cipher = Cipher.getInstance(“RSA/None/PKCS1Padding”);

Advertisements

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无太大区别,缺乏灵活性。

 

使用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() 方法返回的值。