July 02
今天在项目里碰到了非常纠结的问题——中文乱码问题。
原因是这样的,我打算通过JSF的一个FRAME控件LOAD一个jsp页面,该jsp页面根据GET参数得到文件的路径和文件名,从而可以将文件用二进制流输出给浏览器,以便下载。
但是由于我本地的文件名是中文的,所以出现了乱码问题。
简单点说,主要有以下几点:
1. 用java创建本地文件的中文文件名问题:
为了保证你的文件名不会乱码,在用java.io.File对象创建文件的时候,构造函数里的filename千万不要随意的转换成其他字符集的。就用默认的就好。也就是说
File f= new File(“中文文件.txt”);
足矣。这样,无论Windows用GBK编码文件名还是LINUX用UTF-8编码文件名,都可以在当前的系统中正常的查看。
2.向JSP传递中文参数的时候,如果你没有设定Tomcat的全局URIEncoding,一定要把中文参数进行URLEncoding
URLEncoder.encode(requestUrl,"UTF-8");
在被请求的页面,执行URLDecoder.decode(request.getParameter(“param”),"UTF-8");从而得到正确的中文。如果不能,可以尝试“new String(request.getParameter(“param”).getBytes("iso8859_1"),”UTF-8”)”
3.如果你在POST传参的时候发现出了问题,
可以在web.xml里面配置一个CharacterEncodingFillter。这个东西网上有很多代码,自己找吧。如果你用的是Spring,可以用它自带的org.springframework.web.filter.CharacterEncodingFilter
4.另一种解决参数乱码的办法就是修改TOMCAT的配置文件。
找到server.xml。把里面HTTP端口和HTTPS端口(如果你放开了)的Connector元素后面加上URIEncoding=”UTF-8”字样。
June 26
做的项目中要用到日志功能,记录重要数据增删改,以提供后台动态数据恢复功能,在数据库中建立一个表四个字段:
id:标识(long)、action:增删改类别(String 或 int)、olddata与newdata分别记录增删改前后的数据类型为blob、optime记录操作时间
项目持久层用了Hibernate所以数据库中所有条目都是以JavaBean形式出现,JavaBean扩展了Serializable可以实现对象的序列化,现在问题就是怎样保存JavaBean序列化的结果到数据库,并且可以逆向反序列化为实例。
因为刚接触Java对列的概念不是很清楚,所以在序列化上遇到了问题,首先是如何不通过临时文件取得对象序列化的结果,网上的例子大多是对文件流的操作,用来保存图片
综合网上的多个例子以及从JDK中查询的结果,总结出以下过程:
1、还是对流的操作,不过不是文件流而是字节流,利用ByteArrayOutputStream创建
2、通过new出来ByteArrayOutputStream作为参数创建ObjectOutputStream
3、调用ObjectOutputStream的writeObject将任意JavaBean序列化为字节流
//以上是序列化过程,实际上使用不同的XStream就可以把JavaBean序列化到不同的流中
4、通过调用ByteArrayOutputStream的toByteArray可以获得byte数组
//这是取中间值,相当于文件流操作时利用文件名打开一个文件流,文件名也是一个中间值
5、将得到的byte数组作为参数用ByteArrayInputStream打开一个输入流
6、调用静态方法Hibernate.createBlob(),以输入流为参数获取Blob
7、此后可将该Blob设置为接收Bean的属性保存到数据库中
//以上完成将序列化的结果存储到数据库
8、利用Hibernate的API的到数据库中的Blob很容易,然后调用Blob的getBinaryStream可获取输入流,将此流作为ObjectInputStream,调用readObject可得到序列化前的实例
代码:
//序列化
ByteArrayOutputStream bos=new ByteArrayOutputStream();
ObjectOutputStream oos=new ObjectOutputStream(bos);
oos.writeObject(new User("cfgxy"));
//保存到数据库,sessionFactory是Hibernate中SessionFactory的一个实例
Session session=sessionFactory.createSession();
Transaction tx =session.openTransaction();
ByteArrayInputStream bis=new ByteArrayInputStream(bos.getByteArray());
session.save(new Logs(null,"INSERT",null,Hibernate.createBlob(bis)));
tx.commit();
session.close();
//从数据库读取,假设传来的参数id为数据库主键的值
Session session=sessionFactory.createSession();
Logs log=(Logs)session.load(Logs.class,id);
ObjectInputStream ois=new ObjectInputStream(log.getNewData().getBinaryStream());
return (User)ois.readObject();
//代码中均未对异常进行捕捉,实际运用中要捕捉异常
June 25
"java深度历险"一书在讲解“类装载”的一章中,举了以下的例子:
引用
Java代码
- public interface Assembly{
- public void start();;
- }
-
- public class Word implements Assembly{
- static{
- System.out.println("Word static initialization!");;
- }
-
- public void start();{
- System.out.prinlnt("Word starts");;
- }
- }
-
- public class Office{
- public static void main(String args[]); throws Exception{
- Office off = new Office();;
- System.out.println("类别准备载入");;
- Class c = Class.forName(args[0],true,off.getClass();.getClassLoader(););;
- System.out.println("类别准备实例化");;
- Object o = c.newInstance();;
- Object o2= c.newInstance();;
- }
- }
执行java Office Word,运行结果如下:
“Loaded Office”
“类别准备载入”
“Loaded Accembly”
“Loaded Word””
“Word static initialization”
“类别准备实体化”。
但是如果将Office.java中Class.forName(args[0],true,off.getClass().getClassLoader())中的true变为false,再执行java Office Word结果显示为:
“Loaded Office”
“类别准备载入”
“Loaded Accembly”
“Loaded Word””
“类别准备实体化”
“Word static initialization”。
显然两次红字部分顺序相反,及static块执行的顺序不同。此书作者提出了原因,原文:
引用
“过去很多java书上提到静态初始化(static initializion block)时,都会说静态初始化区块只是在类第一次载入的时候才会被调用仅仅一次。可是上面输出却发现即使类被载入了,其静态初始化区块也没有被调用,而是在第一次调用newInstance方法时,静态初始化块才被真正调用,应该改成-静态初始化块只是在类被第一次实体化的时候才会被仅仅调用一次。”
其实,该书作者的上述描述有误。通过一个试验,就可以看出谬误所在。
Java代码
- public class TestA{
- static{
- System.out.println("Static block executed!");;
- }
- }
-
- public class Test{
- public static void main(String args[]);{
- Test test = new Test();;
- Class.forName("TestA",true,test.getClass();.getClassLoader(););;
- }
- }
运行一下,相信大家一定可以看到,“Static block executed!”的输出。这与
引用
而是在第一次调用newInstance方法时,静态初始化块才被真正调用
的说法矛盾。
其实我想事实是这样的:
一个类的运行,JVM做会以下几件事情 1、类装载 2、链接 3、初始化 4、实例化;而初始化阶段做的事情是初始化静态变量和执行静态方法等的工作。所以,当Class.forName(args[0],true,off.getClass().getClassLoader());中的true变为false的时候,就是告诉JVM不需再load class之后进行initial的工作。这样,将initial的工作推迟到了newInstance的时候进行。所以,static块的绝对不是什么“只是在类被第一次实体化的时候才会被仅仅调用一次”,而应该是在类被初始化的时候,仅仅调用一次。
May 05
一、性能优化的一般概念
人们普遍认为Java程序总是比C程序慢,对于这种意见,大多数人或许已经听得太多了。实际上,情况远比那些陈旧的主张要复杂。许多 Java程序确实很慢,但速度慢不是所有Java程序的固有特征。许多Java程序可以达到C或C++中类似程序的效率,但只有当设计者和程序员在整个开发过程中密切注意性能问题时,这才有可能。
本文的主要目的是讨论如何优化Java IO操作的性能。许多应用把大量的运行时间花在网络或文件IO操作上,设计拙劣的IO代码可能要比经过精心调整的IO代码慢上几倍。
说到Java程序的性能优化,有些概念总是一次又一次地被提起。本文的示例围绕IO应用的优化展开,但基本原则同样适用于其他性能情况。
对于性能优化来说,最重要的原则也许就是:尽早测试,经常测试。不知道性能问题的根源就无法有效地调整性能,许多程序员因为毫无根据地猜测性能问题的所在而徒劳无功。在一个只占程序总运行时间百分之一的模块上花费数天时间,应用性能的改进程度不可能超过百分之一。所以,应当避免猜测,而是采用性能测试工具,比如一些代码分析工具或带有时间信息的日志,找出应用中耗时最多的地方,然后集中精力优化这些程序的热点。性能调整完成后,应当再次进行测试。测试不仅有助于程序员把精力集中在那些最重要的代码上,而且还能够显示出性能调整是否真地取得了成功。
在调整程序性能的过程中,需要测试的数据可能有很多,例如运行总时间、内存占用平均值、内存占用峰值、程序的吞吐能力、请求延迟时间以及对象创建情况等。到底应该关注哪些因素,这与具体的情况和对性能的要求有关。大部分上述数据都可以通过一些优秀的商品化分析工具测试得到,然而,并非一定要有昂贵的代码分析工具才能收集得到有用的性能数据。
本文收集的性能数据只针对运行时间,测试所用的工具类似于下面的Timer类(可以方便地对它进行扩展,使它支持pause()和 restart()之类的操作)。带有时间信息的日志输出语句会影响测试结果,因为这些语句也要创建对象和执行IO操作,Timer允许我们在不用这类语句的情况下收集时间信息。
|
public class Timer { // 一个简单的“秒表”类,精度为毫秒。 private long startTime, endTime; public void start() { startTime = System.currentTimeMillis(); } public void stop() { endTime = System.currentTimeMillis(); } public long getTime() { return endTime - startTime; } } |
引起Java性能问题的常见原因之一是过多地创建临时对象。虽然新的Java虚拟机在创建许多小型对象时有效地降低了性能影响,但对象创建属于昂贵操作这一事实仍旧没有改变。由于字符串对象不可变的特点,String类常常是性能问题最大的罪魁祸首,因为每次修改一个String对象,就要创建一个或者多个新的对象。由此可以看出,提高性能的第二个原则是:避免过多的对象创建操作。
二、IO性能优化
许多应用要进行大规模的数据处理,而IO操作正属于那种细微的改动会导致巨大性能差异的地方。本文的例子来自对一个文字处理应用的性能优化,这个文字处理应用要对大量的文本进行分析和处理。在文字处理应用中,读取和处理输入文本的时间很关键,优化该应用所采用的措施为上面指出的性能优化原则提供了很好的例子。
影响Java IO性能最主要的原因之一在于大量地使用单字符IO操作,即用InputStream.read()和Reader.read()方法每次读取一个字符。 Java的单字符IO操作继承自C语言。在C语言中,单字符IO操作是一种常见的操作,比如重复地调用getc()读取一个文件。C语言单字符IO操作的效率很高,因为getc()和putc()函数以宏的形式实现,且支持带缓冲的文件访问,因此这两个函数只需要几个时钟周期就可以执行完毕。在Java 中,情况完全不同:对于每一个字符,不仅要有一次或者多次方法调用,而且更重要的是,如果不使用任何类型的缓冲,要获得一个字符就要有一次系统调用。虽然一个依赖read()的Java程序可能在表现、功能上和C程序一样,但两者在性能上不能相提并论。幸而,Java提供了几种简单的办法帮助我们获得更好的IO性能。
缓冲可以用以下两种方式之一实现:使用标准的BufferedReader和BufferedInputStream类,或者使用块读取方法一次读取一大块数据。前者快速简单,能够有效地改进性能,且只需少量地增加代码,出错的机会也较少。后者也即自己编写代码,复杂性略有提高——当然也说不上困难,但它能够获得更好的效果。
为测试不同IO操作方式的效率,本文用到了六个小程序,这六个小程序读取几百个文件并分析每一个字符。表一显示了这六个程序的运行时间,测试用到了五个常见的Linux Java虚拟机:Sun 1.1.7、1.2.2和1.3 Java虚拟机,IBM 1.1.8和1.3 Java虚拟机。
这六个程序是:
- RawBytes:用FileInputStream.read()每次读取一个字节。
- RawChars:用FileReader.read()每次读取一个字符。
- BufferedIS:用BufferedInputStream封装FileInputStream,用read()每次读取一个字节的数据。
- BufferedR:用BufferedReader封装FileReader,用read()每次读取一个字符。
- SelfBufferedIS:用FileInputStream.read(byte[])每次读取1 K数据,从缓冲区访问数据。
- SelfBufferedR:用FileReader.read(char[])每次读取1 K数据,从缓冲区访问数据。
| 表一 |
| |
Sun 1.1.7 |
IBM 1.1.8 |
Sun 1.2.2 |
Sun 1.3 |
IBM 1.3 |
| RawBytes |
20.6 |
18.0 |
26.1 |
20.70 |
62.70 |
| RawChars |
100.0 |
235.0 |
174.0 |
438.00 |
148.00 |
| BufferedIS |
9.2 |
1.8 |
8.6 |
2.28 |
2.65 |
| BufferedR |
16.7 |
2.4 |
10.0 |
2.84 |
3.10 |
| SelfBufferedIS |
2.1 |
0.4 |
2.0 |
0.61 |
0.53 |
| SelfBufferedR |
8.2 |
0.9 |
2.7 |
1.12 |
1.17 |
表一是调整Java VM和程序启动配置之后,处理几百个文件的总计时间。从表一我们可以得到几个显而易见的结论:
- InputStream比Reader高效。一个char用两个字节保存字符,而byte只需要一个,因此用byte保存字符消耗的内存和需要执行的机器指令更少。更重要的是,用byte避免了进行Unicode转换。因此,如果可能的话,应尽量使用byte替代char。例如,如果应用必须支持国际化,则必须使用char;如果从一个ASCII数据源读取(比如HTTP或MIME头),或者能够确定输入文字总是英文,则程序可以使用byte。
- 无缓冲的字符IO实在很慢。字符IO本来就效率不高,如果没有缓冲,情形就更糟了。因此,在编程实践中,至少应该为流加上缓冲,它可以让IO性能提高10倍以上。
- 带有缓冲的块操作IO要比缓冲的流字符IO快。对于字符IO,虽然缓冲流避免了每次读取字符时的系统调用开销,但仍需要一次或多次方法调用。带缓冲的块IO比缓冲流IO快2到4倍,比无缓冲的IO快4到40倍。
从表一不易看出的一点是,字符IO可能抵消速度较快的Java VM带来的优势。在大多数性能测试中,IBM 1.1.8 Linux Java VM大约有Sun 1.1.7 Linux Java VM两倍那么快,然而在RawBytes和RawChars的测试中,结果显示出两者差不多慢,它们花在系统调用上的额外时间开销掩盖了较快Java VM带来的速度优势。
块IO还有另一个不那么明显的优点。缓冲的字符IO有时对组件之间的协调有更多的要求,带来更多的出错机会。很多时候,应用中的IO操作由一个组件完成,应用把一个Reader或InputStream传递给组件,然后,IO组件处理流的内容。一些IO组件可能错误地假设它所操作的流是一个带缓冲的流,但又不在文档中说明这方面的需求,或者虽然IO组件在文档中说明了这方面的要求,但应用的开发者却未能留意到这一点。在这些情况下,IO 操作将不按意料之中地那样带有缓冲,从而带来严重的性能问题。如果改用块IO,这类情形就不可能出现(因此,设计软件组件时,最好能够做到组件不可能被误用,而不要依赖于文档来保证组件的正确使用)。
从上述简单的测试可以看出,用最直接的方法完成一个简单任务,比如读取文本,可能比细心选择的方法慢40到60倍。在这些测试中,程序在提取和分析每一个字符时进行了一些计算。如果程序只是把数据从一个流复制到另一个流,则非缓冲的字符IO和块IO之间的性能差异将更加明显,块IO的性能将达到非缓冲字符IO的300到500倍。
三、再次测试
性能调整必须反复地进行,因为在主要性能问题解决之前,次要性能问题往往不能显露出来。在文字处理应用的例子中,最初的分析显示出程序把绝大部分的时间花费在读取字符上,加上缓冲功能后性能有了戏剧性的提高。只有在程序解决了主要的性能瓶颈(字符IO)之后,剩余的性能热点才显现出来。对程序的第二次分析显示出,程序在创建String对象上花费了大量的时间,而且看起来它为输入文本中的每一个单词创建了一个以上的String对象。
本文例子中的文本分析应用采用了模块化的设计,用户可以结合多个文本处理操作达到预期的目标。例如,用户可以结合运用单词标识器部件(读取输入字符并把它们组织成单词)和小写字母转换器部件(把单词转换成小写字母),以及一个还原器部件(把单词转换成它们的基本形式,例如,把 jumper和jumped转换成jump)。
虽然模块化构造具有很明显的优点,但这种处理方式会对性能产生负面影响。由于部件之间的接口是固定的(每一个部件都以一个String 作为输入,并输出另一个String),部件之间也许存在一些重复的操作。如果有几个部件经常组合在一起使用,对这些情形进行优化是值得的。
在这个文字处理系统中,从实际使用情况可以看出,用户几乎总是在使用单词标识器部件之后,紧接着使用小写字母转换器部件。单词标识器分析每一个字符,寻找单词边界,同时填充一个单词缓冲区。标识出一个完整的单词之后,单词标识器部件将为它创建一个String对象。调用链中的下一个部件是小写字母转换器部件,这个部件将在前面得到的String上调用String.toLowerCase(),从而创建了另一个String对象。对于输入文本中的每一个单词,顺序使用这两个部件将生成两个String对象。由于单词标识器部件和小写字母转换器部件频繁地一起使用,因此可以添加一个经过优化的小写字母单词标识器,这个标识器具有原来两个部件的功能,但只为每一个单词创建一个String对象,从而有利于提高性能。表二显示了测试结果:
| 表二 |
| |
|
Sun 1.1.7 |
IBM 1.1.8 |
Sun 1.2.2 |
Sun 1.3 |
IBM 1.3 |
| A |
单词标识 |
23.0 |
3.6 |
10.7 |
2.6 |
2.9 |
| B |
单词标识 + 小写字母转换 |
39.6 |
6.7 |
13.9 |
3.9 |
3.9 |
| C |
结合单词标识和小写字母转换 |
29.0 |
3.8 |
12.9 |
3.1 |
3.1 |
| |
临时字符串创建时间 (B-C) |
10.6 |
2.9 |
1.0 |
0.8 |
0.8 |
从表二我们可以得到几个有用的发现:
- 对于Java VM 1.1,简单的优化引人注目地提高了性能:大约在百分之二十五到百分之四十五之间。最后一行显示出,创建临时String对象占用了程序A和程序B之间百分之六十到九十的性能增加值。另外,正如其他几个测试项目显示出的,IBM Java VM 1.1运行速度要比Sun Java VM 1.1快。
- 对于1.2和1.3的Java VM,两个版本之间的性能差异不再那么大,大约只有百分之十到百分之二十五之间,相当于创建临时String对象所耗时间的百分比。这个结果表明,在创建对象实例方面,版本较高的Java VM确实提高了效率,但过多的对象创建操作对性能的影响仍旧值得注意。
- 对于这类创建大量小型对象的操作,1.3版本的Java VM要比1.1和1.2版本的Java VM快得多。
性能优化是一种需要反复进行的工作。在开发工作的早期阶段开始收集性能数据是值得的,因为这样可以尽早地找出和调整性能热点。通过一些比较简单的改进,比如为IO操作增加缓冲,或在适当的时候用byte替代char,常常可以戏剧性地提高应用的性能。另外,不同的VM之间也有着很大的性能差异,简单地换上一个速度较快的Java VM,可能就让程序的性能向预期的目标跨出了一大步。
April 29
Java类如下
public static void downloadFile(String path,String fileName) {
try {
// 获得JSF上下文环境
FacesContext context = FacesContext.getCurrentInstance();
// 获得ServletContext对象
ServletContext servletContext = (ServletContext) context
.getExternalContext().getContext();
// 取得文件的绝对路径
String realName = servletContext.getRealPath(path) + "/"
+ fileName;
HttpServletResponse httpServletResponse =
(HttpServletResponse) FacesContext
.getCurrentInstance().getExternalContext().getResponse();
downloadFile(httpServletResponse,realName,fileName);
} catch (IOException e) {
e.printStackTrace();
}
FacesContext.getCurrentInstance().responseComplete();
}
public static void downloadFile(HttpServletResponse response,String realName,String fileName)
throws IOException
{
response.setHeader("Content-disposition",
"attachment; filename=" + fileName);
response.setContentType("application/x-download");
//File exportFile = new File(realName);
//response.setContentLength((int) exportFile.length());
ServletOutputStream servletOutputStream = response.getOutputStream();
byte[] b = new byte[1024];
int i = 0;
FileInputStream fis = new java.io.FileInputStream(realName);
while ((i = fis.read(b)) > 0) {
servletOutputStream.write(b, 0, i);
}
}
使用方法
1、在backing bean的方法中调用函数1即可。如Abean中download方法调用了该方法,前台可以这样调用:
<h:commandButton value="download" action="#{aBean.download}"></h:commandButton>
或者
<h:commandLink value="download" action="#{fileUploadForm.download}"></h:commandLink>
2、jsp页面可以这样调用:
<%@ page contentType="text/html; charset=gb2312"%><%@page import="java.io.*"%><%
String filename = "";
if (request.getParameter("filename") != null) {
filename = request.getParameter("filename");
}
try {
framework.util.FileUtils.downloadFile(response,getServletContext().getRealPath(filename),filename);
} catch(final IOException e) {
System.out.println ( "出现IOException." + e );
} catch(final IllegalStateException e) {
System.out.println ( "出现IllegalStateException." + e );
}
%>
于是jsf页面我们可以借助outputlink来调用该页面
<h:outputLink id="downloadfile" value="#{page/FileDownload.jsp?filename=}">
<t:outputText value="下载文件" />
</h:outputLink>