源:http://srcode.org/2014/05/11/write-your-own-java-server/
评:
Classloaders are on the dark side of Java. If you want to implement your own Java server to manage the lifecycle of your services and modules, you probably have to deal with these monsters. Normally you start up a Java program like this: $ java -classpath my_lib.jar MyMainClass, where MyMainClass contains the main function. The program will have access to the my_lib.jar defined by -classpath or -cp. However, the reality is you have tons of jars to load, and more jars are added as you programs grows. Now you might start to think about writing a script to maintain the list of jars. If you have only a couple of jars, say, less than 10, this probably is not a problem, while if you have 50 or more jars to load, you quickly blow your script up. So we needs a smarter way to manage our jars. Think about when you deploy a web application with Tomcat, Tomcat automatically recognizes the jars and classes you dropped in the WEB-INF/lib and WEB-INF/classes folder. So what’s the magic? Before running into code, let’s take a look at how Java’s classloaders work together to load all these jars and classes.
Java classloadersJava classloaders
From top to bottom, the bootstrap classloader is managed by the JDK or JRE, which loads all the core java classes shipped with the JRE, like the java.io and javax.swing. Extension classloader loads up the classes you put in the ext folder in your JRE. If your program needs to access the serial ports, sometimes you put those com port access api libraries there. The system classloader loads classes from your classpath where you defined in the java startup script. So the real magic is here in the blue boxes. We can futher make our own classloader to load jars and classes wherever we want.
First, let’s create a folder called our_app as the root of our Java server application. Inside the root folder, we have a lib folder to hold all the jars and a classes folder to hold all the calsses. This is going to be the standard layout of our app. Then we create the Bootstrap class to load the jars and classes in the lib and classes folder, respectively.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
package boot;
import java.io.File;
import java.io.FilenameFilter;
import java.io.IOException;
import java.lang.reflect.Method;
import java.net.URL;
import java.net.URLClassLoader;
import java.util.ArrayList;
import java.util.List;
public class Bootstrap {
private static final String CP_CLASSES = "classes";
private static final String CP_LIB = "lib";
public static void main(String[] args) throws Exception {
List<File> jarList = new ArrayList<File>();
getClassPath(jarList, new File(getServerHome(), CP_LIB));
URL[] urlList = toURL(jarList);
URLClassLoader loader = new URLClassLoader(urlList);
Class c = loader.loadClass(args[0]);
Method m = c.getMethod("main", new Class[] { args.getClass() });
String[] appArgs = new String[args.length - 1];
System.arraycopy(args, 1, appArgs, 0, args.length - 1);
m.invoke(null, new Object[] { appArgs });
}
private static URL[] toURL(List<File> list) throws IOException {
List<URL> urlList = new ArrayList<URL>();
urlList.add(new URL("file:///" + getServerHome() + "/" + CP_CLASSES + "/"));
for (int i = 0; i < list.size(); ++i) {
urlList.add(new URL("file:///" + ((File) list.get(i)).getCanonicalPath()));
}
return (URL[]) urlList.toArray(new URL[0]);
}
private static Filter filter = new Filter();
private static void getClassPath(List<File> list, File f) {
if (f.exists() && f.isDirectory()) {
File[] ss = f.listFiles(filter);
for (int i = 0; i < ss.length; ++i) {
if (ss[i].isFile()) {
list.add(ss[i]);
} else if (ss[i].isDirectory()) {
getClassPath(list, ss[i]);
}
}
}
}
private static class Filter implements FilenameFilter {
public boolean accept(File dir, String name) {
File f = new File(dir, name);
boolean isDir = f.isDirectory();
boolean isFile = f.isFile();
boolean isJar = name.toLowerCase().endsWith(".jar");
boolean isZip = name.toLowerCase().endsWith(".zip");
return (isFile && (isJar || isZip)) || isDir;
}
}
private static String getServerHome() {
String serverHome = System.getProperty("server.home");
if (serverHome == null) {
serverHome = new File(System.getProperty("user.dir")).getParent();
System.setProperty("server.home", serverHome);
}
return serverHome;
}
}
The Bootstrap class uses a URLClassLoader classloader to search for all jars and classes from the lib and classes folders. When the search is done, it’s ready to invoke the real application’s main method using this instance of the URLClassLoader.
Then let’s compile and package the Bootstrap class to loader.jar. The loader.jar is a tiny utility jar that we use to further load and manage our real java application.
1
2
3
4
5
6
7
8
9
$ javac boot/Bootstrap.java
Note: boot/Bootstrap.java uses unchecked or unsafe operations.
Note: Recompile with -Xlint:unchecked for details.
$
$ jar cvf loader.jar boot/*.class
added manifest
adding: boot/Bootstrap$1.class(in = 188) (out= 151)(deflated 19%)
adding: boot/Bootstrap$Filter.class(in = 883) (out= 535)(deflated 39%)
adding: boot/Bootstrap.class(in = 3046) (out= 1565)(deflated 48%)
Now let’s create a bin folder under the application root, and move the newly created loader.jar to the bin folder. Next create a script called startup.sh and place it in bin. The startup.sh will try its best to detect where your java is. Once java executable is found, it will launch the loader we just created, and invoke our real application.
startup.sh
1
2
3
4
5
6
7
8
9
10
11
12
#!/bin/bash
if [[ $(which ${JAVA_HOME}/bin/java) ]]; then
exe="${JAVA_HOME}/bin/java"
elif [[ $(which java) ]]; then
exe="java"
else
echo "Java environment is not detected."
exit 1
fi
${exe} -cp loader.jar boot.Bootstrap com.my.Main "Hello world"
Finally, let’s create our real, and really simple demo application, and package it into com.my.Main class. The source code is really simple as below. It prints the first parameter passed in if there is, otherwise, it prints “Hello.”.
1
2
3
4
5
6
7
8
9
10
11
package com.my;
public class Main {
public static void main(String[] args) {
if (args.length > 0) {
System.out.println(args[0]);
} else {
System.out.println("Hello.");
}
}
}
Compile it, package it into our_app.jar, drop it into the lib folder, and we are ready to go.
1
2
3
4
5
6
7
8
9
10
$ javac com/my/Main.java
$
$ java com/my/Main "Hello world"
Hello world
$ java com/my/Main
Hello.
$
$ jar cvf our_app.jar com/**/*.class
added manifest
adding: com/my/Main.class(in = 466) (out= 324)(deflated 30%)
Now the layout of our_app should look something like this:
1
2
3
4
5
6
|____bin
| |____loader.jar
| |____startup.sh
|____classes
|____lib
| |____our_app.jar
Go into the bin folder, and run ./start.sh. Boom, it says Hello world as expected.
1
2
3
$ cd bin
$ ./startup.sh
Hello world
Here is the link to download the sample code and script.
评:
Classloaders are on the dark side of Java. If you want to implement your own Java server to manage the lifecycle of your services and modules, you probably have to deal with these monsters. Normally you start up a Java program like this: $ java -classpath my_lib.jar MyMainClass, where MyMainClass contains the main function. The program will have access to the my_lib.jar defined by -classpath or -cp. However, the reality is you have tons of jars to load, and more jars are added as you programs grows. Now you might start to think about writing a script to maintain the list of jars. If you have only a couple of jars, say, less than 10, this probably is not a problem, while if you have 50 or more jars to load, you quickly blow your script up. So we needs a smarter way to manage our jars. Think about when you deploy a web application with Tomcat, Tomcat automatically recognizes the jars and classes you dropped in the WEB-INF/lib and WEB-INF/classes folder. So what’s the magic? Before running into code, let’s take a look at how Java’s classloaders work together to load all these jars and classes.
Java classloadersJava classloaders
From top to bottom, the bootstrap classloader is managed by the JDK or JRE, which loads all the core java classes shipped with the JRE, like the java.io and javax.swing. Extension classloader loads up the classes you put in the ext folder in your JRE. If your program needs to access the serial ports, sometimes you put those com port access api libraries there. The system classloader loads classes from your classpath where you defined in the java startup script. So the real magic is here in the blue boxes. We can futher make our own classloader to load jars and classes wherever we want.
First, let’s create a folder called our_app as the root of our Java server application. Inside the root folder, we have a lib folder to hold all the jars and a classes folder to hold all the calsses. This is going to be the standard layout of our app. Then we create the Bootstrap class to load the jars and classes in the lib and classes folder, respectively.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
package boot;
import java.io.File;
import java.io.FilenameFilter;
import java.io.IOException;
import java.lang.reflect.Method;
import java.net.URL;
import java.net.URLClassLoader;
import java.util.ArrayList;
import java.util.List;
public class Bootstrap {
private static final String CP_CLASSES = "classes";
private static final String CP_LIB = "lib";
public static void main(String[] args) throws Exception {
List<File> jarList = new ArrayList<File>();
getClassPath(jarList, new File(getServerHome(), CP_LIB));
URL[] urlList = toURL(jarList);
URLClassLoader loader = new URLClassLoader(urlList);
Class c = loader.loadClass(args[0]);
Method m = c.getMethod("main", new Class[] { args.getClass() });
String[] appArgs = new String[args.length - 1];
System.arraycopy(args, 1, appArgs, 0, args.length - 1);
m.invoke(null, new Object[] { appArgs });
}
private static URL[] toURL(List<File> list) throws IOException {
List<URL> urlList = new ArrayList<URL>();
urlList.add(new URL("file:///" + getServerHome() + "/" + CP_CLASSES + "/"));
for (int i = 0; i < list.size(); ++i) {
urlList.add(new URL("file:///" + ((File) list.get(i)).getCanonicalPath()));
}
return (URL[]) urlList.toArray(new URL[0]);
}
private static Filter filter = new Filter();
private static void getClassPath(List<File> list, File f) {
if (f.exists() && f.isDirectory()) {
File[] ss = f.listFiles(filter);
for (int i = 0; i < ss.length; ++i) {
if (ss[i].isFile()) {
list.add(ss[i]);
} else if (ss[i].isDirectory()) {
getClassPath(list, ss[i]);
}
}
}
}
private static class Filter implements FilenameFilter {
public boolean accept(File dir, String name) {
File f = new File(dir, name);
boolean isDir = f.isDirectory();
boolean isFile = f.isFile();
boolean isJar = name.toLowerCase().endsWith(".jar");
boolean isZip = name.toLowerCase().endsWith(".zip");
return (isFile && (isJar || isZip)) || isDir;
}
}
private static String getServerHome() {
String serverHome = System.getProperty("server.home");
if (serverHome == null) {
serverHome = new File(System.getProperty("user.dir")).getParent();
System.setProperty("server.home", serverHome);
}
return serverHome;
}
}
The Bootstrap class uses a URLClassLoader classloader to search for all jars and classes from the lib and classes folders. When the search is done, it’s ready to invoke the real application’s main method using this instance of the URLClassLoader.
Then let’s compile and package the Bootstrap class to loader.jar. The loader.jar is a tiny utility jar that we use to further load and manage our real java application.
1
2
3
4
5
6
7
8
9
$ javac boot/Bootstrap.java
Note: boot/Bootstrap.java uses unchecked or unsafe operations.
Note: Recompile with -Xlint:unchecked for details.
$
$ jar cvf loader.jar boot/*.class
added manifest
adding: boot/Bootstrap$1.class(in = 188) (out= 151)(deflated 19%)
adding: boot/Bootstrap$Filter.class(in = 883) (out= 535)(deflated 39%)
adding: boot/Bootstrap.class(in = 3046) (out= 1565)(deflated 48%)
Now let’s create a bin folder under the application root, and move the newly created loader.jar to the bin folder. Next create a script called startup.sh and place it in bin. The startup.sh will try its best to detect where your java is. Once java executable is found, it will launch the loader we just created, and invoke our real application.
startup.sh
1
2
3
4
5
6
7
8
9
10
11
12
#!/bin/bash
if [[ $(which ${JAVA_HOME}/bin/java) ]]; then
exe="${JAVA_HOME}/bin/java"
elif [[ $(which java) ]]; then
exe="java"
else
echo "Java environment is not detected."
exit 1
fi
${exe} -cp loader.jar boot.Bootstrap com.my.Main "Hello world"
Finally, let’s create our real, and really simple demo application, and package it into com.my.Main class. The source code is really simple as below. It prints the first parameter passed in if there is, otherwise, it prints “Hello.”.
1
2
3
4
5
6
7
8
9
10
11
package com.my;
public class Main {
public static void main(String[] args) {
if (args.length > 0) {
System.out.println(args[0]);
} else {
System.out.println("Hello.");
}
}
}
Compile it, package it into our_app.jar, drop it into the lib folder, and we are ready to go.
1
2
3
4
5
6
7
8
9
10
$ javac com/my/Main.java
$
$ java com/my/Main "Hello world"
Hello world
$ java com/my/Main
Hello.
$
$ jar cvf our_app.jar com/**/*.class
added manifest
adding: com/my/Main.class(in = 466) (out= 324)(deflated 30%)
Now the layout of our_app should look something like this:
1
2
3
4
5
6
|____bin
| |____loader.jar
| |____startup.sh
|____classes
|____lib
| |____our_app.jar
Go into the bin folder, and run ./start.sh. Boom, it says Hello world as expected.
1
2
3
$ cd bin
$ ./startup.sh
Hello world
Here is the link to download the sample code and script.