用户工具

站点工具


about_jetty_1

Jetty的那些事儿 第一章:启动过程和配置的控制反转

作者:陈科

联系方式:chenke1818@gmail.com

转载请说明出处:http://www.dumpcache.com/wiki/doku.php?id=about_jetty_1

Jetty 的前身今世

列宁同志说的好:忘记历史就意味着背叛。了解一个软件还是先要知道它的历 史。

曾几何时,Java 世界的容器都被商业软件所统治,众所周知的 Weblogic, Websphere 等,都是收费界的大拿。当时 JEE 世界的技术也被笨重的 EJB 标准 统治者。

随着开源运动的兴起,Jboss 横空出世,打破了商业软件对容器的垄断,而 Jboss 内置 Servlet 容器其实就是 Tomcat,Jboss 通过另类的 JMX 微内核实现,将各种 JEE 规范实现在了一起,打造成了一个 EJB 容器。所以 Tomcat 慢慢成为了很多 Java 程序员的 Servlet 容器选项。在这之前 Jetty 一直默默无闻,知道 Jetty 的程 序员也只是把它作为一个玩具来把玩,很少有人敢把它放到测试环境。 自从 JDK1.4 引入 NIO 机制之后,Java 世界在服务器领域就开始崭露头角,之 前由于只能基于坑爹 BIO 模型来编写代码,Java 的服务器一直都无法与 C 的服 务器实现来匹敌。

Jetty 真正让我眼前一亮是在大概 2010 年的时候,当时正好我工作的团队将要 对我们维护的线上产品进行一次彻底的性能优化,在这之前我们已经对基于 NIO 的 Java 容器进行了多番考察,例如 Tomcat 6.x,Jetty 7.x 等等。经过多番考 虑和抉择,我们选择了 Jetty 7.x,而 Jetty 在 NIO 方面良好的表现也让我们服务 器的吞吐量大为提升。

时至今日,Jetty 项目已经挂在了 eclipse 的基金会下。很多开源的软件,互联网 公司在 Java 容器选型上,也不得不正视一下 Jetty 的存在,一个屌丝逆袭高富 帅的故事就这样由此诞生了。

由于工作的需要,本人对 Jetty 曾经经过一番彻底的研究,从今天开始,我从几 个方面跟大家一起来剖析 Jetty,共同切磋,提高技艺。

下面我们进入正题。

第一章:启动过程和配置的控制反转

透过现象看本质,对于一个软件,我们总是从第一次启动和使用开始的,所以, 我们就从启动篇开始。

在 Jetty 的根目录下,有一个 start.jar,这个文件就是 Jetty 提供给我们的启动工 具包,当然我们也可以不使用它来启动 Jetty,不过既然人家已经提供了,你何 苦自己再手工写一个呢?KISS(Keep It Simple)原则一直是开源界的一个思想, 既然有了,那么我们就用吧。

我们可以先来看下 start.jar 的使用方法:

java –jar start.jar –help可以查看启动参数,要注意的是这里列出来的不是全部,很多高级参数需要自己查看代码摸索。

localhost:jetty-distribution-8.1.15.v20140411 chenke$ java -jar start.jar  --help
Usage: java -jar start.jar [options...] [properties...] [configs...]

  The start.jar builds a classpath and executes a main java class with
  a classloader built from that classpath.  By default the start.jar
  mechanism is configured to start the jetty server, but it can be 
  configured to start any java main class.

Command Line Options:
  --help           This help / usage information.
  
  --version        Print the version information for Jetty and
                   dependent jars, then exit.
                   
  --list-options   List the details of each classpath OPTION
  
  --list-config    List the start.config file.
                                    
  --dry-run        Print the command line that the start.jar generates,
                   then exit. This may be used to generate command lines
                   when the start.ini includes -X or -D arguments.
                   
  --exec           Run the generated command line (see --dry-run) in 
                   a sub process. This can be used when start.ini
                   contains -X or -D arguments, but creates an extra
                   JVM instance.
                     
  --stop           Send a stop signal to the running Jetty instance.
  
  --daemon         Start in daemon mode with stderr and stdout 
                   redirected to ${jetty.log}/start.log
  
  --config=<file>  Specify an alternate start.config file.  
                   The default is the start.config file inside
                   the start.jar. The default can also be specified
                   with the START system property.
  
  --ini=<file>     Load command line arguments from a file. If 
                   no --ini options are specified, then the 
                   start.ini file will be read if it exists in 
                   jetty.home. If specified jetty.home/start.ini
                   and additional .ini files in jetty.home/start.d/
                   will NOT be read. A --ini option with no file indicates that
                   start.ini should not be read.
                   
  --pre=<file>     Specify a configuration file that is to be processed
                   before any configuration files listed in start.ini

我们先通过默认的方式启动来进入 Jetty 的世界。 java –jar start.jar

1.参数解析

start.jar 中的 Main 方法在 org.eclipse.jetty.start.Main 中

public static void main(String[] args)
{
try
{
Main main = new Main();
List<String> arguments = main.expandCommandLine(args); List<String> xmls = main.processCommandLine(arguments); if (xmls != null)
main.start(xmls);
}
catch (Throwable e) {
usageExit(e,ERR_UNKNOWN); }
}

Main 的构造函数首先初始化 jettyHome 环境变量。

Main() throws IOException
{
_jettyHome = System.getProperty("jetty.home","."); _jettyHome = new File(_jettyHome).getCanonicalPath();
}

可以看到 jettyHome 是从启动参数中的-Djetty.home 来读取的。

接着进行processCommandLine:

public List<String>  (String[] args) throws Exception
{
List<String> arguments = new ArrayList<String>();
// add the command line args and look for start.ini args
boolean ini = false; for (String arg : args) {
if (arg.startsWith("--ini=") || arg.equals("--ini")) {
ini = true;
if (arg.length() > 6) {
arguments.addAll(loadStartIni(new File(arg.substring(6))));
} }
else if (arg.startsWith("--config=")) {
_startConfig = arg.substring(9); }
else
{
arguments.add(arg);
} }
// if no non-option inis, add the start.ini and start.d
if (!ini)
expandCommandLine
expandCommandLine
{
arguments.addAll(0,parseStartIniFiles());
}
return arguments; }

可以看到假如启动参数不带—ini 指定 start.ini,那么 parseStartIniFiles 函数将会 从 jettyHome 的根目录下寻找 start.ini 进行加载。假如 jettyHome 目录下不存在 start.ini,那么会从 jettyHome 目录下的 start.d 目录来加载 ini 配置文件。

另外 starit.ini 的格式为每一行为一个命令行中的 argument 参数.

接着进行processCommandLine:

public List<String> processCommandLine(List<String> arguments) throws Exception
{
// The XML Configuration Files to initialize with
List<String> xmls = new ArrayList<String>();
// Process the arguments
int startup = 0;
for (String arg : arguments) {
if ("--help".equals(arg) || "-?".equals(arg)) {
_showUsage = true;
continue; }
if ("--stop".equals(arg)) {
int port = Integer.parseInt(Config.getProperty("STOP.PORT","-1"));
String key = Config.getProperty("STOP.KEY",null);
int timeout = Integer.parseInt(Config.getProperty("STOP.WAIT","0"));
stop(port,key,timeout);
return null; }
if ("--version".equals(arg) || "-v".equals(arg) || "-- info".equals(arg))
{
_dumpVersions = true; continue;
}
if ("--list-modes".equals(arg) || "--list-options".equals(arg)) {
_listOptions = true; continue;
}
if ("--list-config".equals(arg)) {
_listConfig = true;
continue; }
if ("--exec-print".equals(arg) || "--dry-run".equals(arg)) {
_dryRun = true;
continue; }
if ("--exec".equals(arg)) {
_exec = true;
continue; }
// Special internal indicator that jetty was started by the jetty.sh Daemon
if ("--daemon".equals(arg)) {
File startDir = new File(System.getProperty("jetty.logs","logs"));
if (!startDir.exists() || !startDir.canWrite()) startDir = new File(".");
File startLog = new File(startDir,START_LOG_ROLLOVER_DATEFORMAT.format(new Date()));
if (!startLog.exists() && !startLog.createNewFile()) {
// Output about error is lost in majority of cases.
System.err.println("Unable to create: " + startLog.getAbsolutePath());
// Toss a unique exit code indicating this failure.
usageExit(ERR_LOGGING); }
if (!startLog.canWrite()) {
// Output about error is lost in majority of cases.
System.err.println("Unable to write to: " + startLog.getAbsolutePath());
// Toss a unique exit code indicating this failure.
usageExit(ERR_LOGGING); }
PrintStream logger = new PrintStream(new FileOutputStream(startLog,false));
System.setOut(logger);
System.setErr(logger);
System.out.println("Establishing " + START_LOG_FILENAME + "
on " + new Date()); continue;
}
if (arg.startsWith("--pre=")) {
xmls.add(startup++,arg.substring(6));
continue; }
if (arg.startsWith("-D")) {
String[] assign = arg.substring(2).split("=",2); _sysProps.add(assign[0]);
switch (assign.length)
{
case 2: System.setProperty(assign[0],assign[1]); break;
case 1: System.setProperty(assign[0],""); break;
default: break;
}
continue; }
if (arg.startsWith("-")) {
_jvmArgs.add(arg);
continue; }
// Is this a Property?
if (arg.indexOf('=') >= 0) {
String[] assign = arg.split("=",2);
switch (assign.length) {
case 2:
if ("OPTIONS".equals(assign[0])) {
String opts[] = assign[1].split(","); for (String opt : opts)
_config.addActiveOption(opt.trim());
}
else
{
this._config.setProperty(assign[0],assign[1]);
}
break; case 1:
this._config.setProperty(assign[0],null);
break; default:
break; }
continue; }
// Anything else is considered an XML file.
if (xmls.contains(arg)) {
System.out.println("WARN: Argument '" + arg + "' specified multiple times. Check start.ini?");
System.out.println("Use \"java -jar start.jar --help\" for more information.");
}
xmls.add(arg); }
return xmls; }

这段代码很长,我们需要细细来品: 首先,第一个大循环进行 argement 参数的遍历

a)如果参数为–help 或者-?则跳出循环,设置_showUsage 为 true b)如果参数为–stop 则调用 stop 函数,停止 jetty 进程,stop 必须在 Java 启动参 数上带上-DSTOP.PORT(管理端口)和-DSTOP.KEY(管理密码) c)如果参数为为–deamon 则 Jetty 启动为后台进程,并且需要指定 jetty.logs 目录。

然后 jetty 会将输出流和错误流重定向到日志文件: System.setOut(logger);

System.setErr(logger);

d)如果参数以–pre=开头,则会将等号后面的文件解析为 xml 配置。 e)如果参数以-D 开头,则解析为系统参数 f)如果参数以-开头,则添加到_jvmArgs 中.

g)如果参数中包􏰀=号,并且参数的 key 为 OPTIONS 则将后面的内容以,分割存 入_config.addActiveOption,其他 key 类型直接存到_config.setProperty e)如果参数什么都不带,那么就会为认为是 xml 文件存入 xmls 数组,所以命 令行中的 xml 是会替换 start.ini 中的同名 XML 的,注意啊。

2.启动

我们来看下启动的start函数:

public void start(List<String> xmls) throws IOException, InterruptedException
    {
        // Load potential Config (start.config)
        List<String> configuredXmls = loadConfig(xmls);

        // No XML defined in start.config or command line. Can't execute.
        if (configuredXmls.isEmpty())
        {
            throw new FileNotFoundException("No XML configuration files specified in start.config or command line.");
        }

        // Normalize the XML config options passed on the command line.
        configuredXmls = resolveXmlConfigs(configuredXmls);

        // Get Desired Classpath based on user provided Active Options.
        Classpath classpath = _config.getActiveClasspath();

        System.setProperty("java.class.path",classpath.toString());
        ClassLoader cl = classpath.getClassLoader();
        if (Config.isDebug())
        {
            System.err.println("java.class.path=" + System.getProperty("java.class.path"));
            System.err.println("jetty.home=" + System.getProperty("jetty.home"));
            System.err.println("java.home=" + System.getProperty("java.home"));
            System.err.println("java.io.tmpdir=" + System.getProperty("java.io.tmpdir"));
            System.err.println("java.class.path=" + classpath);
            System.err.println("classloader=" + cl);
            System.err.println("classloader.parent=" + cl.getParent());
            System.err.println("properties=" + Config.getProperties());
        }

        // Show the usage information and return
        if (_showUsage)
        {
            usage();
            return;
        }

        // Show the version information and return
        if (_dumpVersions)
        {
            showClasspathWithVersions(classpath);
            return;
        }

        // Show all options with version information
        if (_listOptions)
        {
            showAllOptionsWithVersions();
            return;
        }

        if (_listConfig)
        {
            listConfig();
            return;
        }

        // Show Command Line to execute Jetty
        if (_dryRun)
        {
            CommandLineBuilder cmd = buildCommandLine(classpath,configuredXmls);
            System.out.println(cmd.toString());
            return;
        }

        // execute Jetty in another JVM
        if (_exec)
        {
            CommandLineBuilder cmd = buildCommandLine(classpath,configuredXmls);

            ProcessBuilder pbuilder = new ProcessBuilder(cmd.getArgs());
            final Process process = pbuilder.start();
            Runtime.getRuntime().addShutdownHook(new Thread()
            {
                @Override
                public void run()
                {
                    Config.debug("Destroying " + process);
                    process.destroy();
                }
            });

            copyInThread(process.getErrorStream(),System.err);
            copyInThread(process.getInputStream(),System.out);
            copyInThread(System.in,process.getOutputStream());
            process.waitFor();
            System.exit(0); // exit JVM when child process ends.
            return;
        }

        if (_jvmArgs.size() > 0 || _sysProps.size() > 0)
        {
            System.err.println("WARNING: System properties and/or JVM args set.  Consider using --dry-run or --exec");
        }

        // Set current context class loader to what is selected.
        Thread.currentThread().setContextClassLoader(cl);

        // Invoke the Main Class
        try
        {
            // Get main class as defined in start.config
            String classname = _config.getMainClassname();

            // Check for override of start class (via "jetty.server" property)
            String mainClass = System.getProperty("jetty.server");
            if (mainClass != null)
            {
                classname = mainClass;
            }

            // Check for override of start class (via "main.class" property)
            mainClass = System.getProperty("main.class");
            if (mainClass != null)
            {
                classname = mainClass;
            }

            Config.debug("main.class=" + classname);

            invokeMain(cl,classname,configuredXmls);
        }
        catch (Exception e)
        {
            usageExit(e,ERR_INVOKE_MAIN);
        }
    }

2.1配置解析

start函数首先进行可能的xml文件解析(config.start), 调用loadConfig函数:

private List<String> loadConfig(List<String> xmls)
    {
        InputStream cfgstream = null;
        try
        {
            // Pass in xmls.size into Config so that conditions based on "nargs" work.
            _config.setArgCount(xmls.size());

            cfgstream = getConfigStream();

            // parse the config
            _config.parse(cfgstream);

            _jettyHome = Config.getProperty("jetty.home",_jettyHome);
            if (_jettyHome != null)
            {
                _jettyHome = new File(_jettyHome).getCanonicalPath();
                System.setProperty("jetty.home",_jettyHome);
            }

            // Collect the configured xml configurations.
            List<String> ret = new ArrayList<String>();
            ret.addAll(xmls); // add command line provided xmls first.
            for (String xmlconfig : _config.getXmlConfigs())
            {
                // add xmlconfigs arriving via start.config
                if (!ret.contains(xmlconfig))
                {
                    ret.add(xmlconfig);
                }
            }

            return ret;
        }
        catch (Exception e)
        {
            usageExit(e,ERR_UNKNOWN);
            return null; // never executed (just here to satisfy javac compiler)
        }
        finally
        {
            close(cfgstream);
        }
    }

这里对可能存在的start.config(默认在jar包的org/eclipse/jetty/start/start.config)文件进行了parse。

我们来看下start.config的格式:

# This file controls what file are to be put on classpath or command line.
  2 #
  3 # Format is as follows:
  4 #
  5 # Each line contains entry in the format:
  6 #
  7 #  SUBJECT [ [!] CONDITION [AND|OR] ]*
  8 # 
  9 # where SUBJECT: 
 10 #   ends with ".class" is the Main class to run.
 11 #   ends with ".xml" is a configuration file for the command line
 12 #   ends with "/" is a directory from which to add all jar and zip files. 
 13 #   ends with "/*" is a directory from which to add all unconsidered jar and zip files.
 14 #   ends with "/**" is a directory from which to recursively add all unconsidered jar and zip files.
 15 #   Containing = are used to assign system properties.
 16 #   Containing ~= are used to assign start properties.
 17 #   Containing /= are used to assign a canonical path.
 18 #   all other subjects are treated as files to be added to the classpath.
 19 #
 20 # ${name} is expanded to a start property
 21 # $(name) is expanded to either a start property or a system property. 
 22 # The start property ${version} is defined as the version of the start.jar
 23 #
 24 # Files starting with "/" are considered absolute, all others are relative to
 25 # the home directory.
 26 #
 27 # CONDITION is one of:
 28 #   always
 29 #   never
 30 #   available classname        # true if class on classpath
 31 #   property name              # true if set as start property
 32 #   system   name              # true if set as system property
 33 #   exists file                # true if file/dir exists
 34 #   java OPERATOR version      # java version compared to literal
 35 #   nargs OPERATOR number      # number of command line args compared to literal
 36 #   OPERATOR := one of "<",">","<=",">=","==","!="
 37 #
 38 # CONTITIONS can be combined with AND OR or !, with AND being the assume
 39 # operator for a list of CONDITIONS.
 40 #
 41 # Classpath operations are evaluated on the fly, so once a class or jar is
 42 # added to the classpath, subsequent available conditions will see that class.
 43 #
 44 # The configuration file may be divided into sections with option names like:
 45 # [ssl,default]
 46 #
 47 # Clauses after a section header will only be included if they match one of the tags in the 
 48 # options property.  By default options are set to "default,*" or the OPTIONS property may
 49 # be used to pass in a list of tags, eg. :
 50 #
 51 #    java -jar start.jar OPTIONS=jetty,jsp,ssl
 52 #
 53 # The tag '*' is always appended to the options, so any section with the * tag is always 
 54 # applied.
 55 #

解析的规则其实很简单,每行的格式如下:

SUBJECT [ [!] CONDITION [AND|OR] ]*

SUBJECT表示启动参数或者系统参数

[]包起来的字符串概念为:options,每个option用逗号分隔。

然后下面几行的jar或者目录假如存在的话,就会构造相应的classpath.

config和他们之间的关系为:

config中持有的Map维护了option→Classpath的关系。

Classpath持有一个File的Vector,是通过addComponent方法添加的

2.2resolveConfig

在start函数中,接着调用了resolveConfig函数,该函数主要对除start.config外的xml文件进行了绝对路径的查找,优先在jettyHome中找,找不到则在jettyHome的etc目录下找。

2.3初始化classpath

Classpath classpath = _config.getActiveClasspath();

最终把config持有的所有option的Classpath合并成一个.

public Classpath getCombinedClasspath(Collection<String> optionIds)
    {
        Classpath cp = new Classpath();

        cp.overlay(_classpaths.get(DEFAULT_SECTION));
        for (String optionId : optionIds)
        {
            Classpath otherCp = _classpaths.get(optionId);
            if (otherCp == null)
            {
                throw new IllegalArgumentException("No such OPTIONS: " + optionId);
            }
            cp.overlay(otherCp);
        }
        cp.overlay(_classpaths.get("*"));
        return cp;
    }

2.4初始化classloader

ClassLoader cl = classpath.getClassLoader();


public ClassLoader getClassLoader()
    {
        int cnt = _elements.size();
        URL[] urls = new URL[cnt];
        for (int i = 0; i < cnt; i++)
        {
            try
            {
                urls[i] = _elements.elementAt(i).toURI().toURL();
            }
            catch (MalformedURLException e)
            {
            }
        }

        ClassLoader parent = Thread.currentThread().getContextClassLoader();
        if (parent == null)
        {
            parent = Classpath.class.getClassLoader();
        }
        if (parent == null)
        {
            parent = ClassLoader.getSystemClassLoader();
        }
        return new Loader(urls, parent);
    }

这里的Loader其实就是一个URLClassloader

private static class Loader extends URLClassLoader
    {
        Loader(URL[] urls, ClassLoader parent)
        {
            super(urls, parent);
        }

        @Override
        public String toString()
        {
            return "startJarLoader@" + Long.toHexString(hashCode());
        }
    }

2.5启动Jetty框架

jetty的启动方式分为两种,–exec方式和–exec方式

2.5.1--exec启动

a)首先进行命令行构建工作buildCommandLine:

CommandLineBuilder buildCommandLine(Classpath classpath, List<String> xmls) throws IOException
    {
        CommandLineBuilder cmd = new CommandLineBuilder(findJavaBin());

        for (String x : _jvmArgs)
        {
            cmd.addArg(x);
        }
        cmd.addRawArg("-Djetty.home=" + _jettyHome);

        // Special Stop/Shutdown properties
        ensureSystemPropertySet("STOP.PORT");
        ensureSystemPropertySet("STOP.KEY");

        // System Properties
        for (String p : _sysProps)
        {
            String v = System.getProperty(p);
            cmd.addEqualsArg("-D" + p,v);
        }

        cmd.addArg("-cp");
        cmd.addRawArg(classpath.toString());
        cmd.addRawArg(_config.getMainClassname());

        // Check if we need to pass properties as a file
        Properties properties = Config.getProperties();
        if (properties.size() > 0)
        {
            File prop_file = File.createTempFile("start",".properties");
            if (!_dryRun)
                prop_file.deleteOnExit();
            properties.store(new FileOutputStream(prop_file),"start.jar properties");
            cmd.addArg(prop_file.getAbsolutePath());
        }

        for (String xml : xmls)
        {
            cmd.addRawArg(xml);
        }
        return cmd;
    }

把所有的启动参数和系统参数,jvm启动参数等构建成了CommandLineBuilder中的args。

b)fork新的jetty进程,并且启动框架

ProcessBuilder pbuilder = new ProcessBuilder(cmd.getArgs());
final Process process = pbuilder.start();
Runtime.getRuntime().addShutdownHook(new Thread()
            {
                @Override
                public void run()
                {
                    Config.debug("Destroying " + process);
                    process.destroy();
                }
            });

2.5.2 非--exec方式启动

  // Set current context class loader to what is selected.
        Thread.currentThread().setContextClassLoader(cl);

        // Invoke the Main Class
        try
        {
            // Get main class as defined in start.config
            String classname = _config.getMainClassname();

            // Check for override of start class (via "jetty.server" property)
            String mainClass = System.getProperty("jetty.server");
            if (mainClass != null)
            {
                classname = mainClass;
            }

            // Check for override of start class (via "main.class" property)
            mainClass = System.getProperty("main.class");
            if (mainClass != null)
            {
                classname = mainClass;
            }

            Config.debug("main.class=" + classname);

            invokeMain(cl,classname,configuredXmls);
        }
        catch (Exception e)
        {
            usageExit(e,ERR_INVOKE_MAIN);
        }

最后找到mainClass并且启动,假如未设置jetty.server启动参数,那么将会从start.config中找。 设置的值为:org.eclipse.jetty.xml.XmlConfiguration.class

最后invokeMain方法通过classloader加载该类,并调用main函数执行。至此框架启动完毕!

3.配置控制反转,前面讲了那么多,我们这边来总结下,如何实现配置的控制翻转,将启动的主动权掌握在我们自己手里。

Jetty的配置无非有三类: 1. java启动参数 2.etc下的xml配置 3.start.jar的启动参数 4.start.config留给我们的后门,例如-Dlib

下面我提出我目前的翻转控制需求: 1.自定义监听端口 2.自定义jetty.xml配置 3.自定义jetty-webapp.xml 4.替换掉指定的jetty框架类 5.以deamon方式启动 6.自定义start.ini文件 7.自定义jettyHome

那么,我们的方式可以如下:

java –jar start.jar jetty.port=7070 –ini=start.ini –Dlib=test.jar -Djetty.home=${JETTY_HOME} jetty.extractwars=true –deamon jetty.xml jetty-webapp.xml

到此,本文暂告一个段落,下一篇将会讲解jetty核心框架架构,敬请期待。

about_jetty_1.txt · 最后更改: 2018/10/14 15:31 (外部编辑)