对于发布后的jar或class,我们经常会写一些bat脚本来方便地使之运行,比如:
java -jar XXX.jar
一旦双击这个bat脚本,会启动一个黑色的console界面,如果不需要这个,可以修改为:
@echo off
start javaw -jar xxx.jar
某些时候,我们是需要这个console界面的,因为日志(system.out.print或logger.debug等)都可以很好地实时显示。但问题是:当需要关闭程序时,你指望别人会去按ctrl+C来终止,从而调用你的shutdownHook?呵呵,一般不会。那么,怎么实时看到日志,又可以优雅地关闭程序?我们有两个思路:
- 双击bat后,执行自己的Java桌面应用,不显示console界面,把日志包括系统的system.out.print和log4j的日志都显示在自己的界面里。此时要关闭事件可以很容易捕捉。
- 双击bat后,显示console界面,当用户关闭窗口时,捕获事件,回调java的善后处理方法。
第一个方法经过测试是可以实现的,但貌似很暴力,而且大多时候我只是想运行的时候看看日志,为什么还要拿个swing程序来封装?第二个办法还是更人性化一些。下面我们就看看如何实现。
实现依据:当用户关闭console窗口时,触发的是ctrl+close事件, 详见这里。所以需要依靠JNI或是JNA来捕获底层事件。关于JNI的实现可以 参考这里,下面我再整理一下:
JNI实现:
import java.io.File;
import java.io.IOException;
public class TestConsoleHandler {
private static Thread hook;
public static void main(String[] args) {
System.out.println("Start");
hook = new ShutdownHook();
Runtime.getRuntime().addShutdownHook(hook);
replaceConsoleHandler(); // actually not "replace" but "add"
try {
Thread.sleep(10000); // You have 10 seconds to close console
} catch (InterruptedException e) {
}
}
public static void shutdown() {
hook.run();
}
private static native void replaceConsoleHandler();
static {
System.loadLibrary("TestConsoleHandler");
}
}
class ShutdownHook extends Thread {
public void run() {
try {
// do some visible work
new File("d:/shutdown.mark").createNewFile();
} catch (IOException e) {
e.printStackTrace();
}
System.out.println("Shutdown");
}
}
执行javah TestConsoleHandler来获取TestConsoleHandler.h。
在visual studio里新建c++项目 -> win32(dll)项目TestConsoleHandler,将TestConsoleHandler.h导入头文件,编写TestConsoleHandler.cpp如下:
#include "stdafx.h"
#include "TestConsoleHandler.h"
JavaVM *jvm;
BOOL WINAPI HandlerRoutine(__in DWORD dwCtrlType) {
if (dwCtrlType == CTRL_CLOSE_EVENT) {
JNIEnv *env;
jint res = jvm->AttachCurrentThread((void **)(&env), &env);
jclass cls = env->FindClass("TestConsoleHandler");
jmethodID mid = env->GetStaticMethodID(cls, "shutdown", "()V");
env->CallStaticVoidMethod(cls, mid);
jvm->DetachCurrentThread();
return TRUE;
}
return FALSE;
}
JNIEXPORT void JNICALL Java_TestConsoleHandler_replaceConsoleHandler(JNIEnv *env, jclass clazz) {
env->GetJavaVM(&jvm);
SetConsoleCtrlHandler(&HandlerRoutine, TRUE);
}
编译成dll拷贝到java项目根目录即可。
JNA实现:
import java.io.File;
import java.io.IOException;
import com.sun.jna.Callback;
import com.sun.jna.Library;
import com.sun.jna.Native;
public class TestJnaConsoleHandler {
private static Thread hook;
/**
* @param args
*/
public static void main(String[] args) {
System.out.println("Start");
hook = new ShutdownHook2();
Runtime.getRuntime().addShutdownHook(hook);
CLib lib = (CLib) Native.loadLibrary("TestJnaConsoleHandler", CLib.class);
lib.init(new CLib.OpenFunc() {
public void invoke(String name, int i) {
hook.run();
}
});
lib.replaceConsoleHandler();
try {
Thread.sleep(10000); // You have 10 seconds to close console
} catch (InterruptedException e) {
}
}
public interface CLib extends Library{
public interface OpenFunc extends Callback {
void invoke(String name, int i);
}
void init(OpenFunc openfunc);
void replaceConsoleHandler();
}
}
class ShutdownHook2 extends Thread {
public void run() {
try {
// do some visible work
new File("d:/shutdown.mark").createNewFile();
} catch (IOException e) {
e.printStackTrace();
}
System.out.println("Shutdown");
}
}
c++实现:
#include "stdafx.h"
#include <iostream>
using namespace std;
#define MYLIBAPI extern "C" __declspec( dllexport )
typedef void (*OpenFunc)(const char*,int);
OpenFunc openfunc = NULL;
MYLIBAPI void init(OpenFunc func) {
cout << "init called" <<endl;
openfunc = func;
}
BOOL WINAPI HandlerRoutine(__in DWORD dwCtrlType) {
if (dwCtrlType == CTRL_CLOSE_EVENT) {
(*openfunc)("test", 0);
return TRUE;
}
return FALSE;
}
MYLIBAPI void replaceConsoleHandler() {
SetConsoleCtrlHandler(&HandlerRoutine, TRUE);
}
至此,当用户关闭console窗口或是按下ctrl+C时,都会调用预定义的方法来优雅地处理。