Spring Boot 项目构建出的 jar 运行时为什么也可以受理 HTTP 请求呢?它是如何做到的呢?
Spring Boot Loader抽象的一些类JarLauncher的执行过程关于自定义的类加载器LaunchedURLClassLoaderSpring Boot Loader的作用
Springboot项目中的是内嵌Tomcat
、Jetty
、Undertow
或Netty
创建一个HTTP
服务器。同时springboot中的采用的SPI的机制,在系统的Springboot项目的时候就会启动的http服务器。SpringBoot在可执行jar包中定义了自己的一套规则,比如第三方依赖jar包在/lib目录下,jar包的URL路径使用自定义的规则并且这个规则需要使用org.springframework.boot.loader.jar.Handler处理器处理。它的Main-Class使用JarLauncher,如果是war包,使用WarLauncher执行。这些Launcher内部都会另起一个线程启动自定义的SpringApplication类。
SpringBoot提供了一个插件spring-boot-maven-plugin用于把程序打包成一个可执行的jar包。在pom文件里加入这个插件即可:
打包完生成的executable-jar-1.0-SNAPSHOT.jar内部的结构如下:
然后可以直接执行jar包就能启动程序了:java -jar executable-jar-1.0-SNAPSHOT.jar
打包出来fat jar内部有4种文件类型:
META-INF文件夹:程序入口,其中MANIFEST.MF用于描述jar包的信息
lib目录:放置第三方依赖的jar包,比如springboot的一些jar包
spring boot loader相关的代码
模块自身的代码
MANIFEST.MF文件的内容:
我们看到,它的Main-Class是org.springframework.boot.loader.JarLauncher,当我们使用java -jar执行jar包的时候会调用JarLauncher的main方法,而不是我们编写的SpringApplication。
那么JarLauncher这个类是的作用是什么的?
它是SpringBoot内部提供的工具Spring Boot Loader提供的一个用于执行Application类的工具类(fat jar内部有spring loader相关的代码就是因为这里用到了)。相当于Spring Boot Loader提供了一套标准用于执行SpringBoot打包出来的jar
Spring Boot Loader抽象的一些类?
抽象类Launcher:各种Launcher的基础抽象类,用于启动应用程序;跟Archive配合使用;目前有3种实现,分别是JarLauncher、WarLauncher以及PropertiesLauncher。
Archive:归档文件的基础抽象类。JarFileArchive就是jar包文件的抽象。它提供了一些方法比如getUrl会返回这个Archive对应的URL;getManifest方法会获得Manifest数据等。ExplodedArchive是文件目录的抽象
JarFile:对jar包的封装,每个JarFileArchive都会对应一个JarFile。JarFile被构造的时候会解析内部结构,去获取jar包里的各个文件或文件夹,这些文件或文件夹会被封装到Entry中,也存储在JarFileArchive中。如果Entry是个jar,会解析成JarFileArchive。
比如一个JarFileArchive对应的URL为:
jar:file:/Users/format/Develop/gitrepository/springboot-analysis/springboot-executable-jar/target/executable-jar-1.0-SNAPSHOT.jar!/
它对应的JarFile为:
Users/format/Develop/gitrepository/springboot-analysis/springboot-executable-jar/target/executable-jar-1.0-SNAPSHOT.jar
JarFileArchive内部的一些依赖jar对应的URL(SpringBoot使用org.springframework.boot.loader.jar.Handler处理器来处理这些URL):
jar:file:/Users/Format/Develop/gitrepository/springboot-analysis/springboot-executable-jar/target/executable-jar-1.0-SNAPSHOT.jar!/lib/spring-boot-starter-web-1.3.5.RELEASE.jar!/jar:file:/Users/Format/Develop/gitrepository/springboot-analysis/springboot-executable-jar/target/executable-jar-1.0-SNAPSHOT.jar!/lib/spring-boot-loader-1.3.5.RELEASE.jar!/org/springframework/boot/loader/JarLauncher.class
我们看到如果有jar包中包含jar,或者jar包中包含jar包里面的class文件,那么会使用 !/ 分隔开,这种方式只有org.springframework.boot.loader.jar.Handler能处理,它是SpringBoot内部扩展出来的一种URL协议。
JarLauncher的执行过程
JarLauncher被构造的时候会调用父类ExecutableArchiveLauncher的构造方法。ExecutableArchiveLauncher的构造方法内部会去构造Archive,这里构造了JarFileArchive。构造JarFileArchive的过程中还会构造很多东西,比如JarFile,Entry …JarLauncher的launch方法:
protected void launch(String[] args) { try { // 在系统属性中设置注册了自定义的URL处理器:org.springframework.boot.loader.jar.Handler。如果URL中没有指定处理器,会去系统属性中查询 JarFile.registerUrlProtocolHandler(); // getClassPathArchives方法在会去找lib目录下对应的第三方依赖JarFileArchive,同时也会项目自身的JarFileArchive // 根据getClassPathArchives得到的JarFileArchive集合去创建类加载器ClassLoader。这里会构造一个LaunchedURLClassLoader类加载器,这个类加载器继承URLClassLoader,并使用这些JarFileArchive集合的URL构造成URLClassPath // LaunchedURLClassLoader类加载器的父类加载器是当前执行类JarLauncher的类加载器 ClassLoader classLoader = createClassLoader(getClassPathArchives()); // getMainClass方法会去项目自身的Archive中的Manifest中找出key为Start-Class的类 // 调用重载方法launch launch(args, getMainClass(), classLoader); } catch (Exception ex) { ex.printStackTrace(); System.exit(1); }}// Archive的getMainClass方法// 这里会找出spring.study.executablejar.ExecutableJarApplication这个类public String getMainClass() throws Exception { Manifest manifest = getManifest(); String mainClass = null; if (manifest != null) { mainClass = manifest.getMainAttributes().getValue("Start-Class"); } if (mainClass == null) { throw new IllegalStateException( "No 'Start-Class' manifest entry specified in " + this); } return mainClass;}// launch重载方法protected void launch(String[] args, String mainClass, ClassLoader classLoader) throws Exception { // 创建一个MainMethodRunner,并把args和Start-Class传递给它 Runnable runner = createMainMethodRunner(mainClass, args, classLoader); // 构造新线程 Thread runnerThread = new Thread(runner); // 线程设置类加载器以及名字,然后启动 runnerThread.setContextClassLoader(classLoader); runnerThread.setName(Thread.currentThread().getName()); runnerThread.start();}
MainMethodRunner的run方法:
Start-Class的main方法调用之后,内部会构造Spring容器,启动内置Servlet容器等过程。这些过程我们都已经分析过了。
关于自定义的类加载器LaunchedURLClassLoader
LaunchedURLClassLoader重写了loadClass方法,也就是说它修改了默认的类加载方式(先看该类是否已加载这部分不变,后面真正去加载类的规则改变了,不再是直接从父类加载器中去加载)。LaunchedURLClassLoader定义了自己的类加载规则:
加载规则:
- 如果根类加载器存在,调用它的加载方法。这里是根类加载是ExtClassLoader
- 调用LaunchedURLClassLoader自身的findClass方法,也就是URLClassLoader的findClass方法
- 调用父类的loadClass方法,也就是执行默认的类加载顺序(从BootstrapClassLoader开始从下往下寻找)
LaunchedURLClassLoader自身的findClass方法:
下面是LaunchedURLClassLoader的一个测试:
Spring Boot Loader的作用
SpringBoot在可执行jar包中定义了自己的一套规则,比如第三方依赖jar包在/lib目录下,jar包的URL路径使用自定义的规则并且这个规则需要使用org.springframework.boot.loader.jar.Handler处理器处理。它的Main-Class使用JarLauncher,如果是war包,使用WarLauncher执行。这些Launcher内部都会另起一个线程启动自定义的SpringApplication类。
这些特性通过spring-boot-maven-plugin插件打包完成。
主要是介绍在面试过程中的一些Springboot的面试问题。SpringBoot
基本上是 Spring
框架的扩展,它消除了设置 Spring
应用程序所需的 XML配置
,为更快,更高效的开发生态系统铺平了道路。
Spring
框架为开发Java
应用程序提供了全面的基础架构支持。它包含一些很好的功能,如依赖注入和开箱即用的模块,如:SpringJDBC、SpringMVC、SpringSecurity、SpringAOP、SpringORM、SpringTest
,这些模块缩短应用程序的开发时间,提高了应用开发的效率例如,在JavaWeb
开发的早期阶段,我们需要编写大量的代码来将记录插入到数据库中。但是通过使用SpringJDBC
模块的JDBCTemplate
,我们可以将操作简化为几行代码。SpringBoot
基本上是Spring
框架的扩展,它消除了设置Spring
应用程序所需的XML配置
,为更快,更高效的开发生态系统铺平了道路
SpringBoot
中的一些特征:
- 1、创建独立的
Spring
应用。 - 2、嵌入式
Tomcat
、Jetty
、Undertow
容器(无需部署war文件)。 - 3、提供的
starters
简化构建配置 - 4、尽可能自动配置
spring
应用。(自动装配) - 5、提供生产指标,例如指标、健壮检查和外部化配置
- 6、完全没有代码生成和
XML
配置要求
首先,让我们看一下使用Spring创建Web应用程序所需的最小依赖项
org.springframework
spring-web
5.1.0.RELEASE
org.springframework
spring-webmvc
5.1.0.RELEASE
与Spring不同,Spring Boot只需要一个依赖项来启动和运行Web应用程序:
org.springframework.boot
spring-boot-starter-web
2.0.6.RELEASE
应用程序启动引导配置
Spring
和Spring Boot
中应用程序引导的基本区别在于servlet
。Spring
使用web.xml
或SpringServletContainerInitializer
作为其引导入口点。Spring Boot
仅使用Servlet 3
功能来引导应用程序。
Spring
支持传统的web.xml
引导方式以及最新的Servlet 3+
方法。
配置web.xml
方法启动的步骤
Servlet
容器(服务器)读取web.xml
web.xml
中定义的DispatcherServlet
由容器实例化DispatcherServlet
通过读取WEB-INF / {servletName} -servlet.xml
来创建WebApplicationContext
。最后,DispatcherServlet
注册在应用程序上下文中定义的bean
使用Servlet 3+
方法的Spring
启动步骤
容器搜索实现ServletContainerInitializer
的类并执行SpringServletContainerInitializer
找到实现所有类WebApplicationInitializer``WebApplicationInitializer
创建具有XML或上下文@Configuration
类WebApplicationInitializer
创建DispatcherServlet
与先前创建的上下文。
Spring Boot应用程序的入口点是使用@SpringBootApplication注释的类
@SpringBootApplication
public class Application {
public static void main(String[] args) {
SpringApplication.run(Application.class, args);
}
}
默认情况下,Spring Boot
使用嵌入式容器来运行应用程序。在这种情况下,Spring Boot
使用public static void main
入口点来启动嵌入式Web
服务器。此外,它还负责将Servlet
,Filter
和ServletContextInitializer bean
从应用程序上下文绑定到嵌入式servlet
容器。Spring Boot
的另一个特性是它会自动扫描同一个包中的所有类或Main
类的子包中的组件。Spring Boot
提供了将其部署到外部容器的方式。我们只需要扩展SpringBootServletInitializer
即可:这里外部servlet
容器查找在war包下的META-INF
文件夹下MANIFEST.MF文件中定义的Main-class
,SpringBootServletInitializer
将负责绑定Servlet
,Filter
和ServletContextInitializer
。
/**
* War部署
*
* @author SanLi
* Created by 2689170096@qq.com on 2018/4/15
*/
public class ServletInitializer extends SpringBootServletInitializer {
@Override
protected SpringApplicationBuilder configure(SpringApplicationBuilder application) {
return application.sources(Application.class);
}
@Override
public void onStartup(ServletContext servletContext) throws ServletException {
super.onStartup(servletContext);
servletContext.addListener(new HttpSessionEventPublisher());
}
}
打包和部署
最后,让我们看看如何打包和部署应用程序。这两个框架都支持Maven
和Gradle
等通用包管理技术。但是在部署方面,这些框架差异很大。例如,Spring Boot Maven插件在Maven
中提供Spring Boot
支持。它还允许打包可执行jar
或war
包并就地
运行应用程序。
在部署环境中Spring Boot
对比Spring
的一些优点包括:
- 提供嵌入式容器支持
- 使用命令java -jar独立运行jar
- 在外部容器中部署时,可以选择排除依赖关系以避免潜在的jar冲突
- 部署时灵活指定配置文件的选项
- 用于集成测试的随机端口生成
-
new springApplication对象,利用spi机制加载applicationContextInitializer, applicationLister接口实例(META-INF/spring.factories);
-
调run方法准备Environment,加载应用上下文(applicationContext),发布事件 很多通过lister实现
-
创建spring容器, refreshContext() ,实现starter自动化配置,spring.factories文件加载, bean实例化
- @EnableAutoConfiguration找到META-INF/spring.factories(需要创建的bean在里面)配置文件
- 读取每个starter中的spring.factories文件
核心注解是@SpringBootApplication 由以下三种组成
- @SpringBootConfiguration:继承自Configuration,支持JavaConfig的方式进行配置。加载相关的bean对象
- @EnableAutoConfiguration:@Import就是加载的INF下面的Spring.Factory的相关的配置类到beandefinitionMap的加载气的配置类。这里里面涉及到SPI技术。
- @ComponentScan:加载相关的类中添加了@CompenScan+、@Repository、@Service、@Compent、@Controller类到beandefinitionMap中
由于 Spring Boot 官方提供了大量的非常方便的开箱即用的 Starter ,包括 Spring Security 的 Starter ,使得在 Spring Boot 中使用 Spring Security 变得更加容易,甚至只需要添加一个依赖就可以保护所有的接口,所以,如果是 Spring Boot 项目,一般选择 Spring Security 。当然这只是一个建议的组合,单纯从技术上来说,无论怎么组合,都是没有问题的。Shiro 和 Spring Security 相比,主要有如下一些特点:
- (一)、Spring Security 是一个重量级的安全管理框架;Shiro 则是一个轻量级的安全管理框架
- (二)、Spring Security 概念复杂,配置繁琐;Shiro 概念简单、配置简单
- (三)、Spring Security 功能强大;Shiro 功能简单
在微服务中,一个完整的项目被拆分成多个不相同的独立的服务,各个服务独立部署在不同的服务器上,各自的 session 被从物理空间上隔离开了,但是经常,我们需要在不同微服务之间共享 session ,常见的方案就是 Spring Session + Redis 来实现 session 共享。将所有微服务的 session 统一保存在 Redis 上,当各个微服务对 session 有相关的读写操作时,都去操作 Redis 上的 session 。这样就实现了 session 共享,Spring Session 基于 Spring 中的代理过滤器实现,使得 session 的同步操作对开发人员而言是透明的,非常简便。 session 共享大家可以参考:Spring Boot 一个依赖搞定 session 共享,没有比这更简单的方案了!
Spring Boot 中如何实现定时任务 ?定时任务也是一个常见的需求,Spring Boot 中对于定时任务的支持主要还是来自 Spring 框架。在 Spring Boot 中使用定时任务主要有两种不同的方式,一个就是使用 Spring 中的 @Scheduled 注解,另一个则是使用第三方框架 Quartz。
- 使用 Spring 中的 @Scheduled 的方式主要通过 @Scheduled 注解来实现。
- 使用 Quartz ,则按照 Quartz 的方式,定义 Job 和 Trigger 即可。
Spring Data 是 Spring 的一个子项目。用于简化数据库访问,支持NoSQL 和 关系数据存储。其主要目标是使数据库的访问变得方便快捷。
Spring Data 具有如下特点:
(一)、SpringData 项目支持 NoSQL 存储:
(二)、MongoDB (文档数据库)
(三)、Neo4j(图形数据库)
(四)、Redis(键/值存储)
(五)、Hbase(列族数据库)
SpringData 项目所支持的关系数据存储技术:
(一)、JDBC
(二)、JPA
Spring Data Jpa 致力于减少数据访问层 (DAO) 的开发量. 开发者唯一要做的,就是声明持久层的接口,其他都交给 Spring Data JPA 来帮你完成!Spring Data JPA 通过规范方法的名字,根据符合规范的名字来确定方法需要实现什么样的逻辑。
Spring Boot 打成的 jar 和普通的 jar 有什么区别 ?Spring Boot 项目最终打包成的 jar 是可执行 jar ,这种 jar 可以直接通过 java -jar xxx.jar 命令来运行,这种 jar 不可以作为普通的 jar 被其他项目依赖,即使依赖了也无法使用其中的类。
Spring Boot 的 jar 无法被其他项目依赖,主要还是他和普通 jar 的结构不同。普通的 jar 包,解压后直接就是包名,包里就是我们的代码,而 Spring Boot 打包成的可执行 jar 解压后,在 \BOOT-INF\classes 目录下才是我们的代码,因此无法被直接引用。如果非要引用,可以在 pom.xml 文件中增加配置,将 Spring Boot 项目打包成两个 jar ,一个可执行,一个可引用。
bootstrap.properties 和 application.properties 有何区别 ?单纯做 Spring Boot 开发,可能不太容易遇到 bootstrap.properties 配置文件,但是在结合 Spring Cloud 时,这个配置就会经常遇到了,特别是在需要加载一些远程配置文件的时侯。
bootstrap.properties 在 application.properties 之前加载,配置在应用程序上下文的引导阶段生效。一般来说我们在 Spring Cloud Config 或者 Nacos 中会用到它。bootstrap.properties 被 Spring ApplicationContext 的父类加载,这个类先于加载 application.properties 的 ApplicatonContext 启动。
如何禁用一个特定自动配置类@SpringBootApplication(exclude= {Order.class})
@EnableAutoConfiguration(exclude={DataSourceAutoConfiguration.class})
@EnableAutoConfiguration(excludeName={Foo.class})
Spring Boot Starter 的工作原理是什么?(@SpringBootApplication注解的原理)
Spring Boot 在启动的时候会干这几件事情:
- ① Spring Boot 在启动时会去依赖的 Starter 包中寻找 resources/META-INF/spring.factories 文件,然后根据文件中配置的 Jar 包去扫描项目所依赖的 Jar 包。
- ② 根据 spring.factories 配置加载 AutoConfigure 类
- ③ 根据 @Conditional 注解的条件,进行自动配置并将 Bean 注入 Spring Context
其实就是 Spring Boot 在启动的时候,按照约定去读取 Spring Boot Starter 的配置信息,再根据配置信息对资源进行初始化,并注入到 Spring 容器中。这样 Spring Boot 启动完毕后,就已经准备好了一切资源,使用过程中直接注入对应 Bean 资源即可。
微服务同时调用多个接口,怎么支持事务的啊?支持分布式事务,可以使用Spring Boot集成 Aatomikos来解决,但是我一般不建议这样使用,因为使用分布式事务会增加请求的响应时间,影响系统的TPS。一般在实际工作中,会利用消息的补偿机制来处理分布式的事务。
shiro和oauth还有cas他们之间的关系是什么?问下您公司权限是如何设计,还有就是这几个概念的区别。- cas和oauth是一个解决单点登录的组件,shiro主要是负责权限安全方面的工作,所以功能点不一致。但往往需要单点登陆和权限控制一起来使用,所以就有 cas+shiro或者oauth+shiro这样的组合。
- token一般是客户端登录后服务端生成的令牌,每次访问服务端会进行校验,一般保存到内存即可,也可以放到其他介质;redis可以做Session共享,如果前端web服务器有几台负载,但是需要保持用户登录的状态,这场景使用比较常见。
- 我们公司使用oauth+shiro这样的方式来做后台权限的管理,oauth负责多后台统一登录认证,shiro负责给登录用户赋予不同的访问权限。
对于无状态服务,首先说一下什么是状态:如果一个数据需要被多个服务共享,才能完成一笔交易,那么这个数据被称为状态。进而依赖这个“状态”数据的服务被称为有状态服务,反之称为无状态服务。那么这个无状态服务原则并不是说在微服务架构里就不允许存在状态,表达的真实意思是要把有状态的业务服务改变为无状态的计算类服务,那么状态数据也就相应的迁移到对应的“有状态数据服务”中。
场景说明:例如我们以前在本地内存中建立的数据缓存、Session缓存,到现在的微服务架构中就应该把这些数据迁移到分布式缓存中存储,让业务服务变成一个无状态的计算节点。迁移后,就可以做到按需动态伸缩,微服务应用在运行时动态增删节点,就不再需要考虑缓存数据如何同步的问题。
Spring Cache 三种常用的缓存注解和意义?- @Cacheable ,用来声明方法是可缓存,将结果存储到缓存中以便后续使用相同参数调用时不需执行实际的方法,直接从缓存中取值。
- @CachePut,使用 @CachePut 标注的方法在执行前,不会去检查缓存中是否存在之前执行过的结果,而是每次都会执行该方法,并将执行结果以键值对的形式存入指定的缓存中。
- @CacheEvict,是用来标注在需要清除缓存元素的方法或类上的,当标记在一个类上时表示其中所有的方法的执行都会触发缓存的清除操作。
现代浏览器出于安全的考虑, HTTP 请求时必须遵守同源策略,否则就是跨域的 HTTP 请求,默认情况下是被禁止的,IP(域名)不同、或者端口不同、协议不同(比如 HTTP、HTTPS)都会造成跨域问题。
一般前端的解决方案有:
- ① 使用 JSONP 来支持跨域的请求,JSONP 实现跨域请求的原理简单的说,就是动态创建 标签,然后利用 的 SRC 不受同源策略约束来跨域获取数据。缺点是需要后端配合输出特定的返回信息。
- ② 利用反应代理的机制来解决跨域的问题,前端请求的时候先将请求发送到同源地址的后端,通过后端请求转发来避免跨域的访问。
后来 HTML5 支持了 CORS 协议。CORS 是一个 W3C 标准,全称是”跨域资源共享”(Cross-origin resource sharing),允许浏览器向跨源服务器,发出 XMLHttpRequest 请求,从而克服了 AJAX 只能同源使用的限制。它通过服务器增加一个特殊的 Header[Access-Control-Allow-Origin]来告诉客户端跨域的限制,如果浏览器支持 CORS、并且判断 Origin 通过的话,就会允许 XMLHttpRequest 发起跨域请求。前端使用了 CORS 协议,就需要后端设置支持非同源的请求,Spring Boot 设置支持非同源的请求有两种方式。
第一,配置 CorsFilter。
@Configuration
public class GlobalCorsConfig {
@Bean
public CorsFilter corsFilter() {
CorsConfiguration config = new CorsConfiguration();
config.addAllowedOrigin("*");
config.setAllowCredentials(true);
config.addAllowedMethod("*");
config.addAllowedHeader("*");
config.addExposedHeader("*");
UrlBasedCorsConfigurationSource configSource = new UrlBasedCorsConfigurationSource();
configSource.registerCorsConfiguration("/**", config);
return new CorsFilter(configSource);
}
}
第二,在启动类上添加:(这里的代码有问题)
public class Application extends WebMvcConfigurerAdapter {
@Override
public void addCorsMappings(CorsRegistry registry) {
registry.addMapping("/**")
.allowCredentials(true)
.allowedHeaders("*")
.allowedOrigins("*")
.allowedMethods("*");
}
}
博文参考
springboot jar包运行_面试官:为什么 Spring Boot 的 jar 可以直接运行?_weixin_39620679的博客-CSDN博客