Skip to content

spring boot

spring boot quick start

在spring boot 里,很吸引人的一个特性是可以直接把应用打包成为一个 jar/war,然后这个 jar/war 是可以直接启动的,不需要另外配置一个 Web Server。 如果之前没有使用过 spring boot 可以通过下面的 demo 来感受下。 下面以这个工程为例,演示如何启动 Spring boot 项目:

sh
$ git clone git@github.com:hengyunabc/spring-boot-demo.git
$ mvn spring-boot-demo
$ java -jar target/demo-0.0.1-SNAPSHOT.jar

对spring boot 的两个疑问

  • spring boot 如何启动的?
  • spring boot embed tomcat 是如何工作的? 静态文件,jsp,网页模板这些是如 何加载到的?

打包为单个 jar 时,spring boot 的 启动方式

maven 打包之后,会生成两个 jar 文件: demo-0.0.1-SNAPSHOT.jar demo-0.0.1-SNAPSHOT.jar.original` 其中 demo-0.0.1-SNAPSHOT.jar.original 是默认的 maven-jar-plugin 生成的包。 demo-0.0.1-SNAPSHOT.jar 是 spring boot maven 插件生成的 jar 包,里面包含了应用的依赖,以及 spring boot 相关的类。下面称之为 fat jar。 先来查看 spring boot 打好的包的目录结构(不重要的省略掉):

├── META-INF 
│   └── MANIFEST.MF 
├── com 
│   └── example 
│       └── SpringBootDemoApplication.class 
├── lib 
│   ├── aopalliance-1.0.jar 
│   ├── spring-beans-4.2.3.RELEASE.jar 
│   ├── ... 
└── org 
    └── springframework 
    └── boot 
        └── loader 
            ├── ExecutableArchiveLauncher.class 
            ├── JarLauncher.class 
            ├── JavaAgentDetector.class 
            ├── LaunchedURLClassLoader.class 
            ├── Launcher.class 
            ├── MainMethodRunner.class 
            ├── ...

依次来看下这些内容。

MANIFEST.MF

Manifest-Version: 1.0 
Start-Class: com.example.SpringBootDemoApplication 
Implementation-Vendor-Id: com.example 
Spring-Boot-Version: 1.3.0.RELEASE 
Created-By: Apache Maven 3.3.3 
Build-Jdk: 1.8.0_60 
Implementation-Vendor: Pivotal Software, Inc. 
Main-Class: org.springframework.boot.loader.JarLauncher

可以看到有 Main-Class 是 org.springframework.boot.loader.JarLauncher,这个是 jar 启动的 Main 函数。 还有一个 Start-Class 是 com.example.SpringBootDemoApplication,这个是我们应用自己的 Main函数。

java
@SpringBootApplication 
public class SpringBootDemoApplication { 
    public static void main(String[] args) { 
        SpringApplication.run(SpringBootDemoApplication.class, args); 
    } 
}

com/example 目录 这下面放的是应用的 .class 文件。

lib 目录 这里存放的是应用的Maven依赖的 jar 包文件。比如 spring-beans,spring-mvc 等 jar。

org/springframework/boot/loader 目录 这下面存放的是 Spring boot loader 的 .class 文件。

Archive 的概念 archive 即归档文件,这个概念在 linux 下比较常见。 通常就是一个 tar/zip 格式的压缩包 jar 是 zip 格式。 在 spring boot 里,抽象出了Archive的概念。 一个 archive 可以是一个jar(JarFileArchive),也可以是一个文件目录(ExplodedArchive)。可以理解为 Spring boot 抽象出来的统一访问资源的层。 上面的 demo-0.0.1-SNAPSHOT.jar 是一个Archive,然后 demo-0.0.1-SNAPSHOT.jar 里的 /lib 目录下面的每一个Jar 包,也是一个 Archive。

java
public abstract class Archive { 
    public abstract URL getUrl(); 
    public String getMainClass(); 
    public abstract Collection<Entry> getEntries(); 
    public abstract List<Archive> getNestedArchives(EntryFilter filter);
}

可以看到 Archive 有一个自己的 URL,比如:jar:file:/tmp/target/demo-0.0.1-SNAPSHOT.jar!/ 还有一个getNestedArchives 函数,这个实际返回的是 demo-0.0.1-SNAPSHOT.jar/lib 下面的 jar 的 Archive 列表。它们的URL是: jar:file:/tmp/target/demo-0.0.1-SNAPSHOT.jar!/lib/aopalliance-1.0.jar jar:file:/tmp/target/demo-0.0.1-SNAPSHOT.jar!/lib/spring-beans-4.2.3.RELEASE.jar

JarLauncher 从 MANIFEST.MF 可以看到 Main 函数是 JarLauncher,下面来分析它的工作流程。 JarLauncher 类的继承结构是:

java
class JarLauncher extends ExecutableArchiveLauncher
class ExecutableArchiveLauncher extends Launcher

以 demo-0.0.1-SNAPSHOT.jar 创建一个 Archive: JarLauncher 先找到自己所在的 jar,即 demo-0.0.1-SNAPSHOT.jar 的路径,然后创建了一个Archive。 下面的代码展示了如何从一个类找到它的加载的位置的技巧:

java
protected final Archive createArchive() throws Exception { 
    ProtectionDomain protectionDomain = getClass().getProtectionDomain(); 
    CodeSource codeSource = protectionDomain.getCodeSource(); 
    URI location = (codeSource == null ? null : codeSource.getLocation().toURI()); 
    String path = (location == null ? null : location.getSchemeSpecificPart()); 
    if (path == null) { 
        throw new IllegalStateException("Unable to determine code source archive"); 
    } 
    File root = new File(path); 
    if (!root.exists()) { 
        throw new IllegalStateException("Unable to determine code source archive from " + root); 
    } 
    return (root.isDirectory() ? new ExplodedArchive(root) : new JarFileArchive(root)); 
}

获取 lib/ 下面的 jar , 并创建一 个 LaunchedURLClassLoader JarLauncher 创建好 Archive 之后,通过 getNestedArchives 函数来获取到 demo-0.0.1-SNAPSHOT.jar/lib 下面的所有 jar 文件,并创建为 List。 注意上面提到,Archive 都是有自己的 URL 的。 获取到这些 Archive 的 URL 之后,也就获得了一个 URL[] 数组,用这个来构造一个自定义的 ClassLoader:LaunchedURLClassLoader。 创建好 ClassLoader 之后,再从 MANIFEST.MF 里读取到 Start-Class,即 com.example.SpringBootDemoApplication,然后创建一个新的线程来启动应用的 Main 函数。

java
** 
* Launch the application given the archive file and a fully configured classloader. 
*/ 
protected void launch(String[] args, String mainClass, ClassLoader classLoader) throws Exception { 
    Runnable runner = createMainMethodRunner(mainClass, args, classLoader); 
    Thread runnerThread = new Thread(runner); 
    runnerThread.setContextClassLoader(classLoader); 
    runnerThread.setName(Thread.currentThread().getName()); 
    runnerThread.start(); 
} 

/** 
* Create the {@code MainMethodRunner} used to launch the application. 
*/ 
protected Runnable createMainMethodRunner(String mainClass, String [] args, ClassLoader classLoader) throws Exception { 
    Class<?> runnerClass = classLoader.loadClass(RUNNER_CLASS); 
    Constructor<?> constructor = runnerClass.getConstructor(String.class, String[].class); 
    return (Runnable) constructor.newInstance(mainClass, args);
}

LaunchedURLClassLoader LaunchedURLClassLoader 和普通的 URLClassLoader 的不同之处是,它提供了从 Archive 里加载 .class 的能力。 结合 Archive 提供的 getEntries 函数,就可以获取到 Archive 里的 Resource。当然里面的细节还是很多的,下面再描述。

spring boot 应用启动流程总结 看到这里,可以总结下Spring Boot应用的启动流程: 1.spring boot 应用打包之后,生成一个fat jar,里面包含了应用依赖的jar包,还有 Spring boot loader 相关的类 2.Fat jar 的启动Main函数是JarLauncher,它负责创建一个 LaunchedURLClassLoader 来加载 /lib 下面的 jar,并以一个新线程启动应用的 Main 函数。

spring boot loader 里的细节 代码地址: https://github.com/spring-projects/spring-boot/tree/main/spring-boot-project/spring-boot-tools/spring-boot-loader

JarFile URL 的扩展 Spring boot 能做到以一个 fat jar 来启动,最重要的一点是它实现了 jar in jar 的加载方式。 JDK 原始的JarFile URL 的定义可以参考这里:https://docs.oracle.com/javase/7/docs/api/java/net/JarURLConnection.html

原始的JarFile URL 是这样子的: jar:file:/tmp/target/demo-0.0.1-SNAPSHOT.jar!/ jar 包里的资源的URL: jar:file:/tmp/target/demo-0.0.1-SNAPSHOT.jar!/com/example/SpringBootDemoApplication.class 可以看到对于Jar里的资源,定义以’!/’来分隔。原始的JarFile URL只支持一个’!/’。Spring boot 扩展了这个协议,让它支持多个’!/’,就可以表示 jar in jar,jar in directory 的资源了。

比如下面的URL表示 demo-0.0.1-SNAPSHOT.jar 这个 jar 里 lib 目录下面的 spring-beans-4.2.3.RELEASE.jar 里面的 MANIFEST.MF:jar:file:/tmp/target/demo-0.0.1-SNAPSHOT.jar!/lib/spring-beans-4.2.3.RELEASE.jar!/META-INF/MANIFEST.MF

自定义URLStreamHandler ,扩展 JarFile 和 JarURLConnection 在构造一个 URL 时,可以传递一个 Handler,而 JDK 自带有默认的 Handler类,应用可以自己注册 Handler 来处理自定义的 URL。

java
public URL(String protocol, String host, int port, String file, URLStreamHandler handler) throws MalformedURLException

参考: https://docs.oracle.com/javase/8/docs/api/java/net/URL.html#URL-java.lang.String-java.lang.String-int-java.lang.String-

Spring boot 通过注册了一个自定义的 Handler 类来处理多重 jar in jar 的逻辑。 这个 Handler 内部会用 SoftReference 来缓存所有打开过的 JarFile。 在处理像下面这样的 URL 时,会循环处理’!/’分隔符,从最上层出发,先构造出 demo-0.0.1-SNAPSHOT.jar 这个 JarFile,再构造出 spring-beans-4.2.3.RELEASE.jar 这个 JarFile,然后再构造出指向 MANIFEST.MF 的 JarURLConnection。 jar:file:/tmp/target/demo-0.0.1-SNAPSHOT.jar!/lib/spring-beans-4.2.3.RELEASE.jar!/META-INF/MANIFEST.MF

java
//org.springframework.boot.loader.jar.Handler 
public class Handler extends URLStreamHandler { 
    private static final String SEPARATOR = "!/"; 
    private static SoftReference<Map<File, JarFile>> rootFileCache;
    private final JarFile jarFile;
    
    @Override 
    protected URLConnection openConnection(URL url) throws IOException {
        if (this.jarFile != null && this.isUrlInJarFile(url, this.jarFile)) {
            return JarURLConnection.get(url, this.jarFile);
        } else {
            try {
                return JarURLConnection.get(url, this.getRootJarFileFromUrl(url));
            } catch (Exception var3) {
                return this.openFallbackConnection(url, var3);
            }
        }
    }

    public JarFile getRootJarFileFromUrl(URL url) throws IOException {
        String spec = url.getFile();
        int separatorIndex = spec.indexOf("!/");
        if (separatorIndex == -1) {
            throw new MalformedURLException("Jar URL does not contain !/ separator");
        } else {
            String name = spec.substring(0, separatorIndex);
            return this.getRootJarFile(name);
        }
    }
}

final class JarURLConnection extends java.net.JarURLConnection {
    static JarURLConnection get(URL url, JarFile jarFile) throws IOException {
        StringSequence spec = new StringSequence(url.getFile());
        int index = indexOfRootSpec(spec, jarFile.getPathFromRoot());
        if (index == -1) {
            return Boolean.TRUE.equals(useFastExceptions.get()) ? NOT_FOUND_CONNECTION : new JarURLConnection(url, (JarFile)null, EMPTY_JAR_ENTRY_NAME);
        } else {
            int separator;
            JarEntryName jarEntryName;
            while((separator = spec.indexOf("!/", index)) > 0) {
                jarEntryName = JarURLConnection.JarEntryName.get(spec.subSequence(index, separator));
                JarEntry jarEntry = jarFile.getJarEntry(jarEntryName.toCharSequence());
                if (jarEntry == null) {
                    return notFound(jarFile, jarEntryName);
                }

                jarFile = jarFile.getNestedJarFile(jarEntry);
                index = separator + "!/".length();
            }

            jarEntryName = JarURLConnection.JarEntryName.get(spec, index);
            if (Boolean.TRUE.equals(useFastExceptions.get()) && !jarEntryName.isEmpty() && !jarFile.containsEntry(jarEntryName.toString())) {
                return NOT_FOUND_CONNECTION;
            } else {
                return new JarURLConnection(url, jarFile, jarEntryName);
            }
        }
    }
    
    public InputStream getInputStream() throws IOException {
        if (this.jarFile == null) {
            throw FILE_NOT_FOUND_EXCEPTION;
        } else if (this.jarEntryName.isEmpty() && this.jarFile.getType() == JarFileType.DIRECT) {
            throw new IOException("no entry name specified");
        } else {
            this.connect();
            InputStream inputStream = this.jarEntryName.isEmpty() ? this.jarFile.getData().getInputStream() : this.jarFile.getInputStream(this.jarEntry);
            if (inputStream == null) {
                this.throwFileNotFound(this.jarEntryName, this.jarFile);
            }

            return inputStream;
        }
    }    
}

ClassLoader 如何读取到 Resource 对于一个ClassLoader,它需要哪些能力?

  • 查找资源
  • 读取资源 对应的 API 是:
java
public URL findResource(String name) 
public InputStream getResourceAsStream(String name)

上面提到,Spring boot 构造 LaunchedURLClassLoader 时,传递了一个 URL[] 数组。数组里是 lib 目录下面的 jar 的 URL。 对于一个 URL,JDK 或者 ClassLoader 如何知道怎么读取到里面的内容的?

java
public class LaunchedURLClassLoader extends URLClassLoader {  
    public LaunchedURLClassLoader(URL[] urls, ClassLoader parent) {  
        super(urls, parent);  
    }
}

实际上流程是这样子的: 1.LaunchedURLClassLoader.loadClass() 2.JarURLConnection.getContent() 3.JarURLConnection.openConnection() 4.Handler.openConnection(URL) 5.JarURLConnection.getInputStream()

从一个 URL,到最终读取到URL里的内容,整个过程是比较复杂的,总结下:

  • spring boot 注册了一个 Handler 来处理“jar:”这种协议的 URL
  • spring boot 扩展了 JarFile 和 JarURLConnection,内部处理 jar in jar 的情况
  • 在处理多重 jar in jar 的 URL 时,spring boot会循环处理,并缓存已经加载到的 JarFile
  • 对于多重 jar in jar,实际上是解压到了临时目录来处理,可以参考 JarFileArchive 里的代码
  • 在获取 URL 的 InputStream 时,最终获取到的是 JarFile 里的 JarEntryData

这里面的细节很多,只列出比较重要的一些点。 然后,URLClassLoader 是如何 getResource 的呢? URLClassLoader 在构造时,有 URL[] 数组参数,它内部会用这个数组来构造一个 URLClassPath:

java
URLClassPath ucp = new URLClassPath(urls);

在 URLClassPath 内部会为这些 URLS 都构造一个 Loader,然后在 getResource 时,会从这些 Loader 里一个个去尝试获取。如果获取成功的话,就像下面那样包装为一个 Resource。

java
package sun.misc;
public class URLClassPath {
    public URLClassPath(URL[] urls) {
        this(urls, null, null);
    }
    
    public Resource getResource(String name) {
        return getResource(name, true);
    }

    /**
     * Finds the first Resource on the URL search path which has the specified
     * name. Returns null if no Resource could be found.
     *
     * @param name the name of the Resource
     * @param check     whether to perform a security check
     * @return the Resource, or null if not found
     */
    public Resource getResource(String name, boolean check) {
        if (DEBUG) {
            System.err.println("URLClassPath.getResource(\"" + name + "\")");
        }

        Loader loader;
        int[] cache = getLookupCache(name);
        for (int i = 0; (loader = getNextLoader(cache, i)) != null; i++) {
            Resource res = loader.getResource(name, check);
            if (res != null) {
                return res;
            }
        }
        return null;
    }
    
    private static class Loader implements Closeable {
        Resource getResource(final String name, boolean check) {
            final URL url;
            try {
                url = new URL(base, ParseUtil.encodePath(name, false));
            } catch (MalformedURLException e) {
                throw new IllegalArgumentException("name");
            }
            final URLConnection uc;
            try {
                if (check) {
                    URLClassPath.check(url);
                }
                uc = url.openConnection();
                InputStream in = uc.getInputStream();
                if (uc instanceof JarURLConnection) {
                    /* Need to remember the jar file so it can be closed
                     * in a hurry.
                     */
                    JarURLConnection juc = (JarURLConnection)uc;
                    jarfile = JarLoader.checkJar(juc.getJarFile());
                }
            } catch (Exception e) {
                return null;
            }
            return new Resource() {
                public String getName() { return name; }
                public URL getURL() { return url; }
                public URL getCodeSourceURL() { return base; }
                public InputStream getInputStream() throws IOException {
                    return uc.getInputStream();
                }
                public int getContentLength() throws IOException {
                    return uc.getContentLength();
                }
            };
        }
    }
}

从代码里可以看到,实际上是调用了 url.openConnection()。这样完整的链条就可以连接起来了。

在IDE/开放目录启动 Spring boot 应用 在上面只提到在一个 fat jar 里启动 Spring boot 应用的过程,下面分析 IDE 里 Spring boot 是如何启动的。 在 IDE 里,直接运行的 Main 函数是应用自己的 Main 函数:

java
@SpringBootApplication 
public class SpringBootDemoApplication { 
    public static void main(String[] args) { 
        SpringApplication.run(SpringBootDemoApplication.class, args); 
    } 
}

其实在 IDE 里启动 Spring boot 应用是最简单的一种情况,因为依赖的 Jar 都让 IDE 放到 classpath 里了,所以 Spring boot 直接启动就完事了。

还有一种情况是在一个开放目录下启动 Spring boot 启动。所谓的开放目录就是把 fat jar 解压,然后直接启动应用。

java
java org.springframework.boot.loader.JarLauncher

这时,Spring boot 会判断当前是否在一个目录里,如果是的,则构造一个 ExplodedArchive(前面在 jar 里时是 JarFileArchive),后面的启动流程类似 fat jar 的。

Embead Tomcat 的启动流程

判断是否在 web 环境 spring boot 在启动时,先通过一个简单的查找Servlet类的方式来判断是不是在web环境:

java
private static final String[] WEB_ENVIRONMENT_CLASSES = { "javax.servlet.Servlet", "org.springframework.web.context.ConfigurableWebApplicationContext" }; 
private boolean deduceWebEnvironment() { 
    for (String className : WEB_ENVIRONMENT_CLASSES) { 
        if (!ClassUtils.isPresent(className, null)) { 
            return false; 
        } 
    } 
    return true; 
}

如果是的话,则会创建 AnnotationConfigEmbeddedWebApplicationContext,否则 Spring context 就是AnnotationConfigApplicationContext:

java
//org.springframework.boot.SpringApplication
protected ConfigurableApplicationContext createApplicationContext(){
    Class<?> contextClass = this.applicationContextClass; 
    if (contextClass == null) {
        try { 
            contextClass = Class.forName(this.webEnvironment ? DEFAULT_WEB_CONTEXT_CLASS : DEFAULT_CONTEXT_CLASS);
        } catch (ClassNotFoundException ex) { 
            throw new IllegalStateException( "Unable create a default ApplicationContext, please specify an ApplicationContextClass",ex); 
        }
    }
    return (ConfigurableApplicationContext) BeanUtils.instantiate (contextClass);
}

获取 EmbeddedServletContainerFactory 的实现类 spring boot 通过获取 EmbeddedServletContainerFactory 来启动对应的 spring boot 通过获取 EmbeddedServletContainerFactory 来启动对应的 web 服务器。 常用的两个实现类是 TomcatEmbeddedServletContainerFactory 和 JettyEmbeddedServletContainerFactory。 启动 Tomcat 的代码:

java
//TomcatEmbeddedServletContainerFactory 
@Override 
public EmbeddedServletContainer getEmbeddedServletContainer(ServletContextInitializer... initializers) { 
    Tomcat tomcat = new Tomcat(); 
    File baseDir = (this.baseDirectory != null ? this.baseDirectory : createTempDir("tomcat")); 
    tomcat.setBaseDir(baseDir.getAbsolutePath()); 
    Connector connector = new Connector(this.protocol); 
    tomcat.getService().addConnector(connector); 
    customizeConnector(connector); 
    tomcat.setConnector(connector); 
    tomcat.getHost().setAutoDeploy(false); 
    tomcat.getEngine().setBackgroundProcessorDelay(-1); 
    for (Connector additionalConnector : this.additionalTomcatConnectors) { 
        tomcat.getService().addConnector(additionalConnector); 
    } 
    prepareContext(tomcat.getHost(), initializers); 
    return getTomcatEmbeddedServletContainer(tomcat); 
}

会为 tomca t创建一个临时文件目录,如:/tmp/tomcat.2233614112516545210.8080,做为tomcat的basedir。里面会放 tomcat 的临时文件,比如 work 目录。 还会初始化 Tomcat 的一些 Servlet,比如比较重要的 default/jsp servlet:

java
private void addDefaultServlet(Context context) { 
    Wrapper defaultServlet = context.createWrapper(); 
    defaultServlet.setName("default"); 
    defaultServlet.setServletClass("org.apache.catalina.servlets.DefaultServlet"); 
    defaultServlet.addInitParameter("debug", "0"); 
    defaultServlet.addInitParameter("listings", "false"); 
    defaultServlet.setLoadOnStartup(1); 
    // Otherwise the default location of a Spring DispatcherServlet can not be set 
    defaultServlet.setOverridable(true); 
    context.addChild(defaultServlet); 
    context.addServletMapping("/", "default"); 
} 
private void addJspServlet(Context context) { 
    Wrapper jspServlet = context.createWrapper(); 
    jspServlet.setName("jsp"); 
    jspServlet.setServletClass(getJspServletClassName()); 
    jspServlet.addInitParameter("fork", "false"); 
    jspServlet.setLoadOnStartup(3); 
    context.addChild(jspServlet); 
    context.addServletMapping("*.jsp", "jsp"); 
    context.addServletMapping("*.jspx", "jsp"); 
}

spring boot 的 web应用如何访问Resource 当 spring boot 应用被打包为一个 fat jar 时,是如何访问到 web resource 的? 实际上是通过 Archive 提供的 URL,然后通过 Classloader 提供的访问 classpath resource 的能力来实现的。

index.html 比如需要配置一个 index.html,这个可以直接放在代码里的 src/main/resources/static 目录下。 对于 index.html 欢迎页,spring boot 在初始化时,就会创建一个 ViewController 来处理:

java
//ResourceProperties 
public class ResourceProperties implements ResourceLoaderAware { 
    private static final String[] SERVLET_RESOURCE_LOCATIONS = { "/" }; 
    private static final String[] CLASSPATH_RESOURCE_LOCATIONS = { "classpath:/META-INF/resources/", "classpath:/resources/", "classpath:/static/", "classpath:/public/" }; 
}
java
//WebMvcAutoConfigurationAdapter 
@Override 
public void addViewControllers(ViewControllerRegistry registry){ 
    Resource page = this.resourceProperties.getWelcomePage(); 
    if (page != null) { 
        logger.info("Adding welcome page: " + page); 
        registry.addViewController("/").setViewName("forward:index.html"); 
    } 
}

template 像页面模板文件可以放在 src/main/resources/template 目录下。但这个实际上是模板的实现类自己处理的。比如 ThymeleafProperties 类里的: public static final String DEFAULT_PREFIX = "classpath:/templates/";

jsp jsp 页面和 template 类似。实际上是通过 spring mvc 内置的 JstlView 来处理的。 可以通过配置 spring.view.prefix 来设定 jsp 页面的目录:spring.view.prefix: /WEB-INF/jsp/

spring boot 里统一的错误页面的处理 对于错误页面,Spring boot 也是通过创建一个 BasicErrorController 来统一处理的。

java
@Controller 
@RequestMapping("${server.error.path:${error.path:/error}}") 
public class BasicErrorController extends AbstractErrorController{

    // 对应的View是一个简单的HTML提醒:
    @Configuration 
    @ConditionalOnProperty(prefix = "server.error.whitelabel", name = "enabled", matchIfMissing = true) 
    @Conditional(ErrorTemplateMissingCondition.class) 
    protected static class WhitelabelErrorViewConfiguration { 
    
        private final SpelView defaultErrorView = new SpelView( 
            "<html><body><h1>Whitelabel Error Page</h1>" 
            + "<p>This application has no explicit mapping for/error, so you are seeing this as a fallback.</p>" 
            + "<div id='created'>${timestamp}</div>" 
            + "<div>There was an unexpected error (type=${error}, status=${status}).</div>" 
            + "<div>${message}</div></body></html>"); 

        @Bean(name = "error") 
        @ConditionalOnMissingBean(name = "error") 
        public View defaultErrorView() { 
            return this.defaultErrorView; 
        } 
}

spring boot 的这个做法很好,避免了传统的 web 应用来出错时,默认抛出异常,容易泄密。

spring boot 应用的 maven 打包过程 先通过 maven-shade-plugin 生成一个包含依赖的 jar,再通过 spring-boot-maven-plugin 插件把 spring boot loader 相关的类,还有 MANIFEST.MF 打包到 jar 里。

spring boot 里有颜色日志的实现 当在 shell 里启动 spring boot 应用时,会发现它的logger输出是有颜色的,这个特性很有意思。 可以通过这个设置来关闭:spring.output.ansi.enabled=false 原理是通过 AnsiOutputApplicationListener,这个来获取这个配置,然后设置logback 在输出时,加了一个 ColorConverter,通过 org.springframework.boot.ansi.AnsiOutput ,对一些字段进行了渲染。

一些代码小技巧 实现 ClassLoader 时,支持 JDK7 并行加载 可以参考 LaunchedURLClassLoader 里的 LockProvider

java
public class LaunchedURLClassLoader extends URLClassLoader { 
    private static LockProvider LOCK_PROVIDER = setupLockProvider(); 
    private static LockProvider setupLockProvider() { 
    try { 
        ClassLoader.registerAsParallelCapable(); 
        return new Java7LockProvider(); 
    } catch (NoSuchMethodError ex) { 
        return new LockProvider(); 
    } 
 
    @Override 
    protected Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException { 
        synchronized (LaunchedURLClassLoader.LOCK_PROVIDER.getLock(this, name)) { 
            Class<?> loadedClass = findLoadedClass(name); 
            if (loadedClass == null) { 
                Handler.setUseFastConnectionExceptions(true); 
                try { 
                    loadedClass = doLoadClass(name); 
                } finally { 
                    Handler.setUseFastConnectionExceptions(false); 
                } 
            } 
            if (resolve) { 
                resolveClass(loadedClass); 
            } 
            return loadedClass; 
        } 
}

检测jar包是否通过agent加载的 InputArgumentsJavaAgentDetector,原理是检测 jar 的 URL 是否有”-javaagent:”的前缀。

java
private static final String JAVA_AGENT_PREFIX = "-javaagent:";

获取进程的PID ApplicationPid,可以获取 PID。

java
private String getPid() { 
    try { 
        String jvmName = ManagementFactory.getRuntimeMXBean().getName(); 
        return jvmName.split("@")[0]; 
    } catch (Throwable ex) { 
        return null; 
    } 
}

包装Logger类

spring boot 里自己包装了一套 logger,支持 java, log4j, log4j2, logback,以后有需要自己包装 logger 时,可以参考这个。 在 org.springframework.boot.logging 包下面。

获取原始启动的main函数 通过堆栈里获取的方式,判断 main 函数,找到原始启动的 main 函数。

java
private Class<?> deduceMainApplicationClass() { 
    try { 
        StackTraceElement[] stackTrace = new RuntimeException().getStackTrace(); 
        for (StackTraceElement stackTraceElement : stackTrace) { 
            if ("main".equals(stackTraceElement.getMethodName())) { 
                return Class.forName(stackTraceElement.getClassName()); 
            } 
        } 
    } catch (ClassNotFoundException ex) { 
        // Swallow and continue 
    } 
    return null; 
}

spirng boot 的一些缺点:

当spring boot 应用以一个 fat jar 方式运行时,会遇到一些问题。以下是个人看法:

  • 日志不知道放哪,默认是输出到 stdout 的
  • 数据目录不知道放哪 jenkins 的做法是放到 ${user.home}/.jenkins 下面
  • 相对目录API不能使用,servletContext.getRealPath(“/”) 返回的是 NULL
  • spring boot 应用喜欢把配置都写到代码里,有时会带来混乱。一些简单可以用 xml 来表达的配置可能会变得难读,而且凌乱。

总结 spring boot 通过扩展了jar协议,抽象出 Archive 概念,和配套的 JarFile,JarUrlConnection,LaunchedURLClassLoader,从而实现了上层应用无感知的 all in one 的开发体验。尽管Executable war 并不是 spring 提出的概念,但 spring boot 让它发扬光大。 spring boot 是一个惊人的项目,可以说是spring的第二春,spring-cloud-config, spring-session, metrics, remote shell 等都是深爱开发者喜爱的项目、特性。几乎可以肯定设计者是有丰富的一线开发经验,深知开发人员的痛点。

org.springframework.boot.spring-boot-starter-parent

xml
<parent>
	<groupId>org.springframework.boot</groupId>
	<artifactId>spring-boot-starter-parent</artifactId>
	<version></version>
	<relativePath/> <!-- lookup parent from repository -->
</parent>