Archive for the 'Java' Category

How to exit a Java app gracefully

标题:如何优雅的退出Java程序
本文中信号拦截代码来自下面的博客:
http://twit88.com/blog/2007/09/27/do-a-graceful-shutdown-of-your-java-application-when-ctr-c-kill/

在UNIX下,通常我们结束一个进程,最常用的方式就是使用kill –9 <PID>,这个方法最简单,但是会带来一个问题,程序可能正在执行某些操作,而这些操作不能被突然中断,否则会造成一些资源的泄露,或者数据完整性的问题。

在UNIX下,信号可以被用来解决这个问题,我们应该比较熟悉kill这个命令,通常使用的命令kill –9 中的kill其实不是‘杀死’的意思,而是‘发送’。kill –9 就是把 SIGKILL(数字代码为9)的信号发送到目标进程。在Java中,可以捕获这些信号:

Runtime.getRuntime().addShutdownHook(…);

当我们执行Ctrl+C或者kill命令的时候,信号Handler会被通知到,这个Handler在一个新的线程中(并非主线程),我们可以在这个Handler里面处理资源释放等问题,注意这里的kill没有带参数,其实等同于kill -15,如果使用kill -9就会直接干掉目标进程,资源得不到回收。具体做法可参考上面的链接。

当Java捕捉到SIGTERM信号的时候,它会执行子线程中的shutdown(); 代码,理论上我们可以把资源清理工作放在这里,我们知道,这个信号监听的线程和主线程,分别在处在两个线程中,这里的代码执行完毕后,它并不管主线程里面的代码是否执行完毕,直接就把整个进程结束了。假设我们有一个长循环在主线程中,它可能会在循环没有执行完毕就殒命了。这个不是我们想要的,解决也很简单:在两个线程中增加同步机制就可以了。

尝试一:
设置一个标志位(notifiedMainThreadToExit = false),当收到SIGTERM信号后,在子线程中设置notifiedMainThreadToExit为true,然后sleep(Long.MAX_VALUE),主线程轮询这个标识位,一旦发现该变量为true,就退出循环,并且释放资源。

问题出现了:主线程的确退出了,可是子线程一直长眠,这个会导致整个进程都不会退出。后来我在主线程退出前,执行System.exit,问题依旧。理论上:System.exit执行后,它会结束该虚拟机中的所有线程,不知道为什么在这里行不通。

尝试二:
设置两个标识位(notifiedMainThreadToExit = false, notifiedSignalHandlerToExit = false),当子线程收到信号后,它通知主线程(set notifiedMainThreadToExit = true):“我收到SIGTERM信号了,你可以准备收工了”,然后,子线程并不马上退出,也不永久休眠,而是轮训等待主线程通知自己。当主线程接受到子线程的通知后,退出循环,然后通知子线程(set notifiedSignalHandlerToExit = true):“我已经准备好了,你也可以收工了”,这样两个线程都可以正常的退出。

结论:
尝试二是可行的,且两个线程间不会产生Race Condition,因为没有两个线程同时写的问题,所以很安全。这里可以下载例子。最后提醒,文章提到的kill,是不带参数的,不要用kill -9。

[ad]

基于文件系统的生产者和消费者问题

周末的时候和team讨论了下如何用最简单的方式,提高数据文件的单位时间传输吞吐量。下面是一个简单的应用场景:

一个目录(DIR1),有很多Producer向这个目录里面放文件,同时有很多的Consumer负责从这个目录里面消费这些文件,插入数据库或者做其他的操作,然后删除或者移走这些文件。

假设条件:

  • 一个文件中转目录DIR1,这个目录位于一个网络存储上
  • 一个生产者,每一秒钟向DIR1里面放一个文件
  • 若干个消费者,假设有8个,其实是8个不同的服务器,都可以访问DIR1,多台服务器可以起负载均衡的作用,任何一台或者几台出问题,整个数据流不会中断
  • 解析一个文件大约需要2-14秒
  • 最后一点:位于网络存储上的目录DIR1,我们认为它是不会出问题的,它不是这里的问题核心

这个场景很普遍,很多公司大概都会用到,尤其是那么比较老的系统(Legacy System),下面是两种方案:

方案一

Consumer循环扫描DIR1,一旦发现有文件,循环解析这些文件,这里有8台服务器,也就是说有8个Consumer一起这样做。代码如下:

	public void run() {
		System.out.println("Created consumer:" + threadName);

		while (true) {
			File file = new File(Constant.STAGING_FOLDER);
			File files[] = file.listFiles();
			for (int i = 0; i < files.length; ++i) {
				File f = files[i];
				parse(Constant.STAGING_FOLDER + "/" + f.getName());
			}

			Commons.sleep();
		}
	}

看起来很简单,可是上面的代码效率非常的差,多个Consumer有很大的几率拿到相同的文件,当某个Consumer尝试去解析一个文件时,却发现这个文件已经被别的Consumer解析过了,并且文件也都删除或者移走了。这样浪费的很多的CPU时间。

可以用下面的方案来替代:

方案二

	public void run() {
		System.out.println("Created consumer:" + threadName);

		while (true) {
			File file = new File(Constant.STAGING_FOLDER);
			File files[] = file.listFiles();

			int nCapacity = files.length > Constant.CAPACITY ? Constant.CAPACITY
					: files.length;
			System.out.println(this.threadName + " found " + nCapacity
					+ " files");

			for (int i = 0; i < nCapacity; ++i) {
				File f = files[i];
				f.renameTo(new File(Constant.TMP_FOLDER + "/" + f.getName()));
			}

			for (int i = 0; i < nCapacity; ++i) {
				parse(Constant.TMP_FOLDER + "/" + files[i].getName());
			}

			Commons.sleep();
		}
	}

它和方案一的不同之处在于:它每次扫描完目录后,最多只取前若干个文件,这里是10个。并且,它不急于去处理文件,而是把文件马上移动到一个临时工作目录,其他的的操作都是相同的。

对于这个方案,有个附加条件:这个临时工作目录tmp,一定要和staging目录在同一个文件系统(filesystem),这样的话,mv操作就只是修改一下inode,几乎瞬间完成。

比较(Benchmarking)

为了测试两中方案的效率差别,我写了一个模拟程序(http://googlestop.com/download/SimConsumer.7z),它有7个class:

  1. App.java - 程序入口
  2. Commons.java - 共享的函数
  3. Constant.java – 配置参数
  4. Producer.java - 生产者,每隔一秒向目录staging里丢一个文件
  5. AbstractConsumer.java – 抽象消费者,定义消费者的一些基本属性和行为
  6. Consumer1.java - 具体消费者,实现方案一
  7. Consumer2.java - 具体消费者,实现方案二

在App.java中,你可以指定调用Consumer1还是Consumer2。

对于前者(Consumer1),staging目录下的文件数目不停的增长,并且如log显示,有很多冲突:一个Consumer准备处理的文件已经被其他的Consumer处理完了,造成了很多无效的操作,由于消费速度更不上生产速度,DIR1被撑爆只是时间的问题。

对于后者(Consumer2),staging目录下的文件几乎马上就会被移动到tmp目录下,大部分时间,文件数都为0。而tmp目录下,在程序稳定后大概保存在20多个文件左右,保持一个动态的平衡。用这种方式,你也会看到很多冲突,但是只会发生在程序刚开始,原因是,刚开始的时候,8个线程几乎是同时去访问staging目录,势必拿到很多相同的文件,待到稳定后,就很少有冲突发生了。

这两种方案都是最基本的,没有借助于第三方工具完成的,成本是最低的,其实还有一些其他的方案,可能会借助一些服务来实现,比如消息分发、数据库等。有时间的话,我继续补充。

[ad]

NoSuchMethodError of BeanUtils.copyProperty(due to wrong access level)

为了节省开发时间,今天打算做一个数据集合类,可以直接将java中的ResultSet,或者其他Collection派生类的内容copy到该集合中,然后加入自定义的一些方法,比如支持直接导出Excel、CSV、KDF、Image、HTML等。这里借助Apache Commons BeanUtils的和反射(Reflection),将数据库中的一行记录保存为一个对象,然后插入数据集中,结果老是报错如下:

USING CONVERTER org.apache.commons.beanutils.converters.IntegerConverter@1befab0
java.lang.reflect.InvocationTargetException: Cannot set id
at org.apache.commons.beanutils.BeanUtilsBean.copyProperty(BeanUtilsBean.java:449)
at org.apache.commons.beanutils.BeanUtils.copyProperty(BeanUtils.java:129)

… …
Caused by: java.lang.NoSuchMethodException: Property ‘id’ has no setter method
at org.apache.commons.beanutils.PropertyUtilsBean.setSimpleProperty(PropertyUtilsBean.java:1746)
at org.apache.commons.beanutils.BeanUtilsBean.copyProperty(BeanUtilsBean.java:447)

代码段如下:

private void dumpResultSet(ResultSet rs, Class clazz) throws Exception {
	ResultSetMetaData metaData = (ResultSetMetaData) rs.getMetaData();
	int colCnt = metaData.getColumnCount();
	Field[] fields = clazz.getDeclaredFields();

	while (rs.next()) {
		Object newInstance = clazz.newInstance();

		for (int i = 1; i <= colCnt; i++) {
			try {
				Object value = rs.getObject(i);
				for (int j = 0; j < fields.length; j++) {
					Field f = fields[j];
					if (f.getName().equalsIgnoreCase(
							metaData.getColumnName(i).replaceAll("_", ""))) {

						log.info("f.getName:" + f.getName());

						BeanUtils.copyProperty(newInstance, f.getName(),
								value);
					}
				}
			} catch (Exception e) {
				e.printStackTrace();
			}
		}

		list.add(newInstance);
	}
}

The root cause: public is required for class UserInfo, or else you’ll get the ‘NoSuchMethodError‘ exception, DO NOT ignore the access level

public class UserInfo {
	private int id;
	private String user;
	private String password;

	public UserInfo() {
	}

	public String getUser() {
		return user;
	}

	public void setUser(String user) {
		this.user = user;
	}

	public String getPassword() {
		return password;
	}

	public void setPassword(String password) {
		this.password = password;
	}

	public int getId() {
		return id;
	}

	public void setId(int id) {
		this.id = id;
	}
}

Eclipse一直提示:NoSuchMethodError,可是UserInfo里面明明有对应的setter方法,最后,才鬼使神差的发现,只要将UserInfo整个类声明为public就可以了。我一直都把注意力放到是否把setter和getter设置为public,却忽略了Bean的访问级别,希望碰到类似问题的朋友可以注意一下。

[ad]

A XFire error and solution

when i tried to deploy my web service with XFire, i got the following error message:

Exception in thread "main" org.codehaus.xfire.annotations.AnnotationException: Service class cannot be abstract: com.webserviceproject.xifre…

the root cause is mis-matched dependency libs, for my case, i’ve imported xfire-annotation-1.2.6.jar and xfire-annotation-1.1.1.jar at the same time. coz i used the wrong pom.xml as below:

        <dependency>
            <groupId>org.codehaus.xfire</groupId>
            <artifactId>xfire-jaxb2</artifactId>
            <version>1.1.1</version>
        </dependency>
        <dependency>
            <groupId>org.codehaus.xfire</groupId>
            <artifactId>xfire-spring</artifactId>
            <version>1.1.1</version>
        </dependency>
        <dependency>
            <groupId>org.codehaus.xfire</groupId>
            <artifactId>xfire-java5</artifactId>
            <version>1.1.1</version>
        </dependency>
       
      <dependency>
            <groupId>org.codehaus.xfire</groupId>
            <artifactId>xfire-all</artifactId>
            <version>1.2.6</version>
        </dependency>

see, actually i’ve imported XFire lib twice(1.1.1 & 1.2.6), that’s the reason why the Java complains, we just need the dependency in green.

good luck.

Problem with JFreechart

一个基于Tomcat的程序里面用到了JFreeChart,偶尔用浏览器访问时,PC端安装的XManager的XServer会自动的打开,然后Tomcat就莫名其妙的挂掉。在没有安装XManager的PC上通常就不会出这种情况。查看一下log,如下:

Can’t connect to X11 window server using ‘:0.0′ as the value of the DISPLAY variable.

搜之,此乃awt的bug。加入以下参数启动即可:

-Djava.awt.handless=true

如果是Tomcat,把上面的参数加到环境变量:CATALINA_OTPS里即可。

[ad]

Upload the 3rd party artifact to local Archiva

Maven is a powerful build tool, more and more developers migrate to it, so do I. Below is the command for deploy the 3rd artifact to your local Archiva:

mvn deploy:deploy-file -DrepositoryId=internal -Durl=http://ssuzsws02:8080/archiva/repository/internal -DgroupId=com.amd.sws -DartifactId=mysql-connector-java -Dversion=3.0.17 -Dpackaging=jar -Dfile=test.jar

but before you issue this command, you should add the following lines to your local settings.xml

<servers>
  <server>
   <id>internal</id>
   <username>admin</username>
   <password>mypassword</password>
  </server>
</servers>

FYI.: when you upload your jar file to Archiva, you can’t see it immediately, please be patient and wait several minutes, the artifact list will be synced soon.

Hibernate, SocketTimeOutException错误

项目中用到Hibernate,部署的时候发现,过了一段时间后,Hibernate就不能正常工作了,时间很有规律,通常是在部署后的若干个小时。错误日志如下:

** BEGIN NESTED EXCEPTION **

java.net.SocketTimeoutException
MESSAGE: Read timed out

STACKTRACE:

java.net.SocketTimeoutException: Read timed out
    at java.net.SocketInputStream.socketRead0(Native Method)
    at java.net.SocketInputStream.read(Unknown Source)
    at com.mysql.jdbc.MysqlIO.readFully(MysqlIO.java:1392)
    at com.mysql.jdbc.MysqlIO.reuseAndReadPacket(MysqlIO.java:1539)
    at com.mysql.jdbc.MysqlIO.checkErrorPacket(MysqlIO.java:1930)
    at com.mysql.jdbc.MysqlIO.sendCommand(MysqlIO.java:1168)
    at com.mysql.jdbc.MysqlIO.sqlQueryDirect(MysqlIO.java:1279)
    at com.mysql.jdbc.MysqlIO.sqlQuery(MysqlIO.java:1225)
    at com.mysql.jdbc.Connection.execSQL(Connection.java:2278)
    at com.mysql.jdbc.Connection.execSQL(Connection.java:2237)
    at com.mysql.jdbc.Connection.execSQL(Connection.java:2218)
    at com.mysql.jdbc.Connection.commit(Connection.java:1155)
    at org.hibernate.transaction.JDBCTransaction.commitAndResetAutoCommit(JDBCTransaction.java:139)
    at org.hibernate.transaction.JDBCTransaction.commit(JDBCTransaction.java:115)
    at com.amd.BizB.test(BizB.java:27)
    at org.apache.jsp.index_jsp._jspService(index_jsp.java:109)
    at org.apache.jasper.runtime.HttpJspBase.service(HttpJspBase.java:70)
    at javax.servlet.http.HttpServlet.service(HttpServlet.java:803)
    at org.apache.jasper.servlet.JspServletWrapper.service(JspServletWrapper.java:393)
    at org.apache.jasper.servlet.JspServlet.serviceJspFile(JspServlet.java:320)
    at org.apache.jasper.servlet.JspServlet.service(JspServlet.java:266)
    at javax.servlet.http.HttpServlet.service(HttpServlet.java:803)
    at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:290)
    at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:206)
    at org.apache.catalina.core.StandardWrapperValve.invoke(StandardWrapperValve.java:230)
    at org.apache.catalina.core.StandardContextValve.invoke(StandardContextValve.java:175)
    at org.apache.catalina.core.StandardHostValve.invoke(StandardHostValve.java:128)
    at org.apache.catalina.valves.ErrorReportValve.invoke(ErrorReportValve.java:104)
    at org.apache.catalina.core.StandardEngineValve.invoke(StandardEngineValve.java:109)
    at org.apache.catalina.connector.CoyoteAdapter.service(CoyoteAdapter.java:261)
    at org.apache.coyote.http11.Http11Processor.process(Http11Processor.java:844)
    at org.apache.coyote.http11.Http11Protocol$Http11ConnectionHandler.process(Http11Protocol.java:581)
    at org.apache.tomcat.util.net.JIoEndpoint$Worker.run(JIoEndpoint.java:447)
    at java.lang.Thread.run(Unknown Source)

最后发现是由于配置文件Hibernate.cfg.xml没有写好,少加了若干的项(注意黑体部分)

<property name=”connection.autocommit”>true</property>

<property name=”connection.url”>jdbc:mysql://ssuzsws01:3306/foo?autoReconnect=true</property>

加了这些选项后,就正常了,希望对遇到类似问题的朋友有帮助。

Java中发送结构体

最近给个朋友做个网站的客户端,使用C/S模式,Client为VC6开发,Server为Java,通过Socket通信。由于Client这边为C++,所以,在接受Java发过来的数据包时,需要知道发来的包的长度,所以,就要引入变长包的机制。

方法是:首先Server发送一个包头,如下:

// packet head
typedef struct tagPacketHead{
long PacketID;
long PacketLen;
}PacketHead;

Read the rest of this entry »

Switch to our mobile site