springboot 扫描jar包中bean_最简单的一个 Spring Boot 项目抽丝剥茧

点击上方☝SpringForAll社区 轻松关注!

及时获取有趣有料的技术文章

本文来源:http://rrd.me/g8Bce

最近在项目中使用 Spring Boot,对它的简单易用印象很深刻。Spring Boot 最大的特点是它大大简化了传统 Spring 项目的配置,使用 Spring Boot 开发 Web 项目,几乎没有任何的 xml 配置。而且它最方便的地方在于它内嵌了 Servlet 容器(可以自己选择 Tomcat、Jetty 或者 Undertow),这样我们就不需要以 war 包来部署项目,直接使用 java -jar hello.jar 就可以运行一个 Web 项目。

我们以 Maven 项目为例,Spring Boot 除了支持 Maven,还支持 Gradle 项目。一个最简单的 Spring Boot Web 项目只有 3 个文件(其实如果想要更简单一点,入口和控制器类甚至可以写在同一个文件中)。首先是一个入口文件:

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

然后再写一个控制器类:

@RestController
public class HelloController {
@RequestMapping("/")
public String index() {
return "Hello World!";
}
}

最后是这个项目的 POM(Project Object Model,项目对象模型) 文件:

<?xml version="1.0" encoding="UTF-8"?>
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
4.0.0

com.stonie
spring-boot-sample
1.0-SNAPSHOT

org.springframework.bootspring-boot-starter-parent2.0.2.RELEASE

org.springframework.bootspring-boot-starter-web

1.8

org.springframework.bootspring-boot-maven-plugin

在新建 Spring Boot 项目的时候要注意一点,入口类必须放在某个包下面,而不能放在默认包(也就是说不能直接放在 srcmainjava 目录下),否则会导致项目启动失败(其实原因很简单,因为 Spring Boot 通过 @ComponentScan 来扫描 Bean,如果入口类放在默认包下,也就意味着 Spring Boot 要扫描所有 jar 包中的所有的类):

** WARNING ** : Your ApplicationContext is unlikely to start due to a @ComponentScan of the default package.

至此,我们就写好了一个 Spring Boot 项目,完整的源码可以参考 这里。从代码上看项目非常简单,但是这里有很多值得我们学习的地方。

一、从 SpringBootApplication 注解看 Spring Boot 自动配置原理

Spring Boot 项目通常都有一个入口类,入口类中的 main 方法和标准的 Java 应用入口方法是一样的,在上面的例子中,这个 main 方法中只有一行代码:SpringApplication.run(),这是一个静态方法,用于启动整个 Spring Boot 项目。和其他 Java 程序不一样的是,入口类上多了一个 @SpringBootApplication 注解,这是非常重要的一个注解,它由多个注解组合而成,包括了:@SpringBootConfiguration@EnableAutoConfiguration@ComponentScan 和其他一些注解。Spring Boot 是如何做到不需要任何配置文件的,看名字也可以猜出来,其秘密就在于 @EnableAutoConfiguration 这个注解实现了自动配置。这个注解的实现如下:

@Target({ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Inherited
@AutoConfigurationPackage
@Import({AutoConfigurationImportSelector.class})
public @interface EnableAutoConfiguration {
String ENABLED_OVERRIDE_PROPERTY = "spring.boot.enableautoconfiguration";
Class>[] exclude() default {};
String[] excludeName() default {};
}

其中最为重要的一行代码为:@Import({AutoConfigurationImportSelector.class}),其中 @Import 是 Spring 提供的一个注解,可以导入配置类或者 Bean 到当前类中。AutoConfigurationImportSelector 类的实现比较复杂,简单来说就是扫描所有 jar 包中的 META-INF/spring-factories 文件,这个文件中声明了有哪些自动配置。我们可以打开 spring-boot-autoconfigure.jar 文件,这里就有这个文件,其中定义了一个属性 org.springframework.boot.autoconfigure.EnableAutoConfiguration 如下所示(有删减):

# Auto Configure
org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
org.springframework.boot.autoconfigure.admin.SpringApplicationAdminJmxAutoConfiguration,\
org.springframework.boot.autoconfigure.aop.AopAutoConfiguration,\
org.springframework.boot.autoconfigure.amqp.RabbitAutoConfiguration,\
...
org.springframework.boot.autoconfigure.data.ldap.LdapRepositoriesAutoConfiguration,\
org.springframework.boot.autoconfigure.data.mongo.MongoDataAutoConfiguration,\
org.springframework.boot.autoconfigure.data.mongo.MongoReactiveDataAutoConfiguration,\
org.springframework.boot.autoconfigure.data.mongo.MongoReactiveRepositoriesAutoConfiguration,\
org.springframework.boot.autoconfigure.data.mongo.MongoRepositoriesAutoConfiguration,\
org.springframework.boot.autoconfigure.data.neo4j.Neo4jDataAutoConfiguration,\
org.springframework.boot.autoconfigure.data.neo4j.Neo4jRepositoriesAutoConfiguration,\
org.springframework.boot.autoconfigure.data.solr.SolrRepositoriesAutoConfiguration,\
org.springframework.boot.autoconfigure.data.redis.RedisAutoConfiguration,\
org.springframework.boot.autoconfigure.data.redis.RedisReactiveAutoConfiguration,\
org.springframework.boot.autoconfigure.data.redis.RedisRepositoriesAutoConfiguration,\
org.springframework.boot.autoconfigure.data.rest.RepositoryRestMvcAutoConfiguration,\
org.springframework.boot.autoconfigure.data.web.SpringDataWebAutoConfiguration,\
...
org.springframework.boot.autoconfigure.ldap.embedded.EmbeddedLdapAutoConfiguration,\
org.springframework.boot.autoconfigure.ldap.LdapAutoConfiguration,\
org.springframework.boot.autoconfigure.liquibase.LiquibaseAutoConfiguration,\
org.springframework.boot.autoconfigure.mail.MailSenderAutoConfiguration,\
org.springframework.boot.autoconfigure.mail.MailSenderValidatorAutoConfiguration,\
org.springframework.boot.autoconfigure.mongo.embedded.EmbeddedMongoAutoConfiguration,\
org.springframework.boot.autoconfigure.mongo.MongoAutoConfiguration,\
org.springframework.boot.autoconfigure.mongo.MongoReactiveAutoConfiguration,\
org.springframework.boot.autoconfigure.mustache.MustacheAutoConfiguration,\
org.springframework.boot.autoconfigure.orm.jpa.HibernateJpaAutoConfiguration,\
org.springframework.boot.autoconfigure.quartz.QuartzAutoConfiguration,\
...
org.springframework.boot.autoconfigure.web.client.RestTemplateAutoConfiguration,\
org.springframework.boot.autoconfigure.web.embedded.EmbeddedWebServerFactoryCustomizerAutoConfiguration,\
org.springframework.boot.autoconfigure.web.reactive.HttpHandlerAutoConfiguration,\
org.springframework.boot.autoconfigure.web.reactive.ReactiveWebServerFactoryAutoConfiguration,\
org.springframework.boot.autoconfigure.web.reactive.WebFluxAutoConfiguration,\
org.springframework.boot.autoconfigure.web.reactive.error.ErrorWebFluxAutoConfiguration,\
org.springframework.boot.autoconfigure.web.reactive.function.client.WebClientAutoConfiguration,\
org.springframework.boot.autoconfigure.web.servlet.DispatcherServletAutoConfiguration,\
org.springframework.boot.autoconfigure.web.servlet.ServletWebServerFactoryAutoConfiguration,\
org.springframework.boot.autoconfigure.web.servlet.error.ErrorMvcAutoConfiguration,\
org.springframework.boot.autoconfigure.web.servlet.HttpEncodingAutoConfiguration,\
org.springframework.boot.autoconfigure.web.servlet.MultipartAutoConfiguration,\
org.springframework.boot.autoconfigure.web.servlet.WebMvcAutoConfiguration,\
org.springframework.boot.autoconfigure.websocket.reactive.WebSocketReactiveAutoConfiguration,\
org.springframework.boot.autoconfigure.websocket.servlet.WebSocketServletAutoConfiguration,\
org.springframework.boot.autoconfigure.websocket.servlet.WebSocketMessagingAutoConfiguration,\
org.springframework.boot.autoconfigure.webservices.WebServicesAutoConfiguration

可以看到 Spring Boot 已经内置了大量的自动配置,我们查看我们这个项目的依赖关系,如下图:

01db32903326e49675ac4f2f4511c206.png
spring-boot-dependency.png

我们这个项目中使用了 spring-boot-starter-web,可以看出它依赖于 spring-boot-starter-tomcat 和 spring-webmvc,所以这里会自动对 Tomcat 和 Spring MVC 进行配置。但是这里有一个问题,这里列出来的自动配置有那么多,难道 Spring Boot 都要一个个的去加载配置吗?当然不是,Spring Boot 也没那么傻,所以这里就要重点介绍一下从 Spring 4.x 开始引入的一个新特性:@Conditional(也叫 条件注解)。

@Conditional 可以根据条件来创建 Bean,譬如随便拿上面一个自动配置类 RedisAutoConfiguration 来看,其中用到的条件注解为 @ConditionalOnClass({RedisOperations.class}) 说明只有在 RedisOperations 类存在时才会自动配置,而我们这个项目并没有引入 redis,所以并不会加载 redis 的配置。

@Configuration
@ConditionalOnClass({RedisOperations.class})
@EnableConfigurationProperties({RedisProperties.class})
@Import({LettuceConnectionConfiguration.class, JedisConnectionConfiguration.class})
public class RedisAutoConfiguration {
}

那么我们的程序在启动的时候都自动加载了哪些配置呢?我们可以通过命令行参数 --debug 来启动 Spring Boot 应用:

$ java -jar hello.jar --debug

启动时控制台会打印出详情的信息,类似于下面这样(实际打印的日志会非常多,有兴趣的同学可以自行挖掘):

============================
CONDITIONS EVALUATION REPORT
============================

Positive matches:
-----------------

EmbeddedWebServerFactoryCustomizerAutoConfiguration.TomcatWebServerFactoryCustomizerConfiguration matched:
- @ConditionalOnClass found required classes 'org.apache.catalina.startup.Tomcat', 'org.apache.coyote.UpgradeProtocol'; @ConditionalOnMissingClass did not find unwanted class (OnClassCondition)

ServletWebServerFactoryAutoConfiguration matched:
- @ConditionalOnClass found required class 'javax.servlet.ServletRequest'; @ConditionalOnMissingClass did not find unwanted class (OnClassCondition)
- found ConfigurableWebEnvironment (OnWebApplicationCondition)

ServletWebServerFactoryAutoConfiguration#tomcatServletWebServerFactoryCustomizer matched:
- @ConditionalOnClass found required class 'org.apache.catalina.startup.Tomcat'; @ConditionalOnMissingClass did not find unwanted class (OnClassCondition)

ServletWebServerFactoryConfiguration.EmbeddedTomcat matched:
- @ConditionalOnClass found required classes 'javax.servlet.Servlet', 'org.apache.catalina.startup.Tomcat', 'org.apache.coyote.UpgradeProtocol'; @ConditionalOnMissingClass did not find unwanted class (OnClassCondition)
- @ConditionalOnMissingBean (types: org.springframework.boot.web.servlet.server.ServletWebServerFactory; SearchStrategy: current) did not find any beans (OnBeanCondition)

WebMvcAutoConfiguration matched:
- @ConditionalOnClass found required classes 'javax.servlet.Servlet', 'org.springframework.web.servlet.DispatcherServlet', 'org.springframework.web.servlet.config.annotation.WebMvcConfigurer'; @ConditionalOnMissingClass did not find unwanted class (OnClassCondition)
- found ConfigurableWebEnvironment (OnWebApplicationCondition)
- @ConditionalOnMissingBean (types: org.springframework.web.servlet.config.annotation.WebMvcConfigurationSupport; SearchStrategy: all) did not find any beans (OnBeanCondition)

Negative matches:
-----------------

ActiveMQAutoConfiguration:
Did not match:
- @ConditionalOnClass did not find required classes 'javax.jms.ConnectionFactory', 'org.apache.activemq.ActiveMQConnectionFactory' (OnClassCondition)

我从日志中挑选中我们这里比较感兴趣的 EmbeddedWebServerFactoryCustomizerAutoConfiguration,我们看看它的实现:

131ee9f507b819942e3616e946ddfb9d.png
embedded-webserver-auto-config.png

从这里就可以看出 Spring Boot 支持三种嵌入的 Web Server:Undertow、Jetty 和 Tomcat。根据上面的依赖关系 spring-boot-starter-web 默认是加载 spring-boot-starter-tomcat 的,所以这里会自动加载 Tomcat 的配置。

如果我们想改变默认的 Web Server,譬如改成轻量级的 Undertow,可以在 POM 文件中使用 exclusion 移除对 spring-boot-starter-tomcat 的引用,并加上对 spring-boot-starter-undertow 的引用,如下:

org.springframework.bootspring-boot-starter-weborg.springframework.bootspring-boot-starter-tomcatorg.springframework.bootspring-boot-starter-undertow

二、探究 Spring MVC 如何映射请求?

第二个文件是控制器类,粗看上去就是一个普通的类,外加上一个方法,只不过加上了两个注解 @RestController@RequestMapping("/") 这个方法竟然就可以处理 Web 请求了。是不是觉得这有点神奇?为什么在浏览器里访问 http://localhost:8080 时页面会显示出这里返回的 Hello World!

其实,这一切都是 Spring MVC 的功劳。只不过在 Spring Boot 项目里,Spring MVC 的配置被简化了。我们先回忆一下在传统的 Spring MVC 里如何实现一个控制器类,首先,我们要先在 web.xml 里定义 DispatcherServlet,并为这个 Servlet 配置相应的 servlet-mapping,类似于下面这样:

appNamecontextConfigLocation
classpath:/applicationContext.xml
org.springframework.web.context.ContextLoaderListenerdispatcher
org.springframework.web.servlet.DispatcherServlet1dispatcher/*

可以说 DispatcherServlet 是 Spring MVC 的核心,通过上面这个配置,它就可以截获 Web 应用的所有请求并将其分派给相应的处理器进行处理。在 Servlet 3.0 之后,还可以通过编程的方式来配置 Servlet 容器,Spring MVC 提供了一个接口 WebApplicationInitializer,通过实现这个接口也可以达到 web.xml 配置文件的目的,如下所示:

public class AppInitializer implements WebApplicationInitializer {
@Override
public void onStartup(ServletContext container) {
XmlWebApplicationContext appContext = new XmlWebApplicationContext();
appContext.setConfigLocation("/WEB-INF/spring/dispatcher-config.xml");
ServletRegistration.Dynamic dispatcher =
container.addServlet("dispatcher", new DispatcherServlet(appContext));
dispatcher.setLoadOnStartup(1);
dispatcher.addMapping("/");
}
}

那么 DispatcherServlet 是如何把 HTTP 请求映射到控制器的某个方法的呢?感兴趣的可以看看 DispatcherServlet 的源码,其实在 DispatcherServlet 初始化的时候,会扫描当前容器所有的 Bean,将包含 @Controller@RequestMapping 注解的类和方法,映射到 HandleMappering,为了实现这一点,Spring MVC 一般都有一个 dispatch-servlet.xml 配置文件:


其中,component-scan 用于开启注解扫描,RequestMappingHandlerMapping 叫做 处理器映射RequestMappingHandlerAdapter 叫做 处理器适配器(在老版本的 Spring MVC 中你可能看到的是 DefaultAnnotationHandlerMappingAnnotationMethodHandlerAdapter)。这两个类负责将 HTTP 方法,HTTP 路径,HTTP 参数匹配到具体的 @RequestMapping 注解的类和方法上。

@RequestMapping 注解的方法所支持的参数类型和返回类型非常丰富和灵活,从这里也可以看出 Spring MVC 的强大之处。这样虽然让开发人员可以根据需要任意选择,但是也会给开发人员带来困惑,可以参考这篇博客的总结:Spring MVC @RequestMapping 方法所支持的参数类型和返回类型详解。

上面就是 Spring MVC 实现请求映射的原理,在传统的 Spring MVC 项目中,这样的配置文件是很常见的,但是在 Spring Boot 项目中,这些配置都自动实现了,可以再深入研究下 DispatcherServletAutoConfigurationWebMvcAutoConfiguration 这两个类。

三、解读 POM 文件

POM 的 全称叫做 Project Object Model,翻译过来就是项目对象模型,它用来定义项目的基本信息,构建步骤,依赖信息等等。pom.xml 文件作为 Maven 项目的核心,和 Make 的 Makefile、Ant 的 build.xml 文件一样。

在这篇博客的最后,让我们来看看这个项目的 pom.xml 文件。首先我们定义了三个元素:groupIdartifactIdversion,这被称为 Maven 坐标,Maven 坐标保证了每个项目都有一个唯一的坐标值,当我们需要在其他项目中引用这个项目时,通过坐标就可以很方便的定位到该项目。

然后下面定义了一个依赖 spring-boot-starter-web,并声明这个 POM 继承自 spring-boot-starter-parent,别小看这一句继承,里面可是另有乾坤。你可以打开 spring-boot-starter-parent 的 POM 文件,可以发现它又继承自 spring-boot-dependencies。在 spring-boot-starter-parent 中定义了一堆的插件,这些插件让 Maven 也能构建 Spring Boot 项目,其中最重要的一个插件是 spring-boot-maven-plugin,这就是我们项目后面要用到的插件。另外,在 spring-boot-dependencies 中定义了一堆的依赖,足足有 3000+ 行,我们前面介绍 Spring Boot 的自动配置原理时就说过,它定义了很多自动配置类,几乎能用到的依赖它都依赖了。

在 pom.xml 文件的 元素中定义了 spring-boot-maven-plugin 插件之后,就可以运行下面的命令和平常的 jar 包一样进行打包了:

$ mvn clean package

而如果在这里没有定义 `` 元素,也可以通过下面的命令来打包:

$ mvn clean package spring-boot:repackage

如果不用这个命令,打出来的包里只有我们写的两个类文件,所有依赖的 jar 包都没有包含进去,这样的 jar 包是无法运行的。而 spring-boot:repackage 插件会在执行完 mvn package 之后再次进行打包为可执行的软件包,并且将 mvn package 打的原始的包命名为 *.jar.original。

我们可以打开 *.jar.original 里的 META-INFMANIFEST.MF 文件:

Manifest-Version: 1.0
Implementation-Title: spring-boot-sample
Implementation-Version: 1.0-SNAPSHOT
Built-By: aneasystone
Implementation-Vendor-Id: com.stonie
Created-By: Apache Maven 3.3.9
Build-Jdk: 1.8.0_111
Implementation-URL: https://projects.spring.io/spring-boot/#/spring-bo
ot-starter-parent/spring-boot-sample

然后再打开 *.jar 里的 META-INFMANIFEST.MF 文件:

Manifest-Version: 1.0
Implementation-Title: spring-boot-sample
Implementation-Version: 1.0-SNAPSHOT
Built-By: aneasystone
Implementation-Vendor-Id: com.stonie
Spring-Boot-Version: 2.0.2.RELEASE
Main-Class: org.springframework.boot.loader.JarLauncher
Start-Class: com.stonie.Application
Spring-Boot-Classes: BOOT-INF/classes/
Spring-Boot-Lib: BOOT-INF/lib/
Created-By: Apache Maven 3.3.9
Build-Jdk: 1.8.0_111
Implementation-URL: https://projects.spring.io/spring-boot/#/spring-bo
ot-starter-parent/spring-boot-sample

可以发现新打的包里多了五行代码:

Spring-Boot-Version: 2.0.2.RELEASE
Main-Class: org.springframework.boot.loader.JarLauncher
Start-Class: com.stonie.Application
Spring-Boot-Classes: BOOT-INF/classes/
Spring-Boot-Lib: BOOT-INF/lib/

并且我们可以在 BOOT-INF/lib/ 目录下找到项目依赖的所有 jar 包,说明 spring-boot-maven-plugin 插件已经自动帮我们把 jar 包转换成了一个可运行的 Spring Boot 应用。

要理解 Spring Boot 是如何通过 Maven 打包的,这里有两个非常重要的概念:生命周期插件目标。Maven 定义了三套生命周期:clean、default 和 site,其中 clean 用于清理项目,default 用于执行构建项目需要的具体步骤,site 用于发布项目站点。其中 clean 和 default 是最常使用的。譬如我们平常执行 mvn clean compile 来清理并编译项目时就用到了 clean 和 default 生命周期,其中,mvn clean 调用的是 clean 生命周期的 clean 阶段,mvn compile 调用的是 default 生命周期的 compile 阶段。

通过 mvn 命令不仅可以直接调用生命周期的某个阶段,还可以调用某个插件目标,譬如上面的 mvn spring-boot:repackage 就是调用 spring-boot 插件的 repackage 目标。实际上,Maven 的核心就是插件,它是一款基于插件的框架,所有的工作其实都是交给插件完成的,包括上面说的 clean 和 compile 实际上就是通过 clean:clean 和 compiler:compile 这两个插件完成的。

不过上面的命令中还有一个问题,执行 mvn spring-boot:repackage 时,Maven 为什么可以根据 spring-boot 这个名字定位到 spring-boot-maven-plugin 这个插件的?这是因为 spring-boot 就是 spring-boot-maven-plugin 插件,这被称为 插件前缀,为了方便书写 mvn 命令,可以给每个插件都定义一个插件前缀,这样就不用在命令行中写那么长的插件名称了。

总结

越是看似简单的东西,背后越是蕴含着无限玄机,从平时的开发工作中,要善于从细节中发现问题。虽然这个 Spring Boot 项目只有三个非常简单的文件,但是想彻底弄懂每个文件,绝对不是那么容易。

参考

  1. Building an Application with Spring Boot
  2. Spring application does not start outside of a package
  3. Spring Boot 的自动配置
  4. Spring Boot 学习笔记 02 -- 深入了解自动配置
  5. Difference between spring @Controller and @RestController annotation
  6. Spring6:基于注解的 Spring MVC(上篇)
  7. Spring MVC @RequestMapping 方法所支持的参数类型和返回类型详解
  8. Spring Boot 的 Maven 插件 Spring Boot Maven plugin 详解
  9. Spring Boot Maven Plugin

edda8c343d7c5b9c323fd18794c7ae64.gif

● ES数十亿数据量级的场景下如何优化查询性能?

● 什么?知乎又挂了?我们来聊聊微服务的高可用如何保障

● Spring Cloud Gateway 原生的接口限流该怎么玩

● 那些年非常火的MyCAT是什么?

● Java14带来了许多新功能

d2dc36bcecd4310e4df1a99d96810ece.png

如果资源对你有帮助的话

❤️给个「在看」,是最大的支持❤️
  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值