面试-Spring
Spring 基础
什么是 Spring 框架?
Spring 是一款开源的轻量级 Java 开发框架,旨在提高开发人员的开发效率以及系统的可维护性。
Spring 翻译过来就是春天的意思,可见其目标和使命就是为 Java 程序员带来春天啊!感动!
题外话 : 语言的流行通常需要一个杀手级的应用,Spring 就是 Java 生态的一个杀手级的应用框架。
我们一般说 Spring 框架指的都是 Spring Framework,它是很多模块的集合,使用这些模块可以很方便地协助我们进行开发。
比如说 Spring 自带 IoC(Inverse of Control:控制反转) 和 AOP(Aspect-Oriented Programming:面向切面编程)、可以很方便地对数据库进行访问、可以很方便地集成第三方组件(电子邮件,任务,调度,缓存等等)、对单元测试支持比较好、支持 RESTful Java 应用程序的开发。
Spring 最核心的思想就是不重新造轮子,开箱即用!
Spring 提供的核心功能主要是 IoC 和 AOP。学习 Spring ,一定要把 IoC 和 AOP 的核心思想搞懂!
Spring 官网:https://spring.io/open in new window
Github 地址: https://github.com/spring-projects/spring-framework
Spring的特性
Spring有很多优点:
1.IOC 和 DI 的支持
Spring 的核心就是一个大的工厂容器,可以维护所有对象的创建和依赖关系,Spring 工厂用
于生成 Bean,并且管理 Bean 的生命周期,实现 高内聚低耦合 的设计理念。
2.AOP 编程的支持
Spring 提供了 面向切面编程 ,可以方便的实现对程序进行权限拦截、运行监控等切面功能。
3. 声明式事务的支持
支持通过配置就来完成对事务的管理,而不需要通过硬编码的方式,以前重复的一些事务提
交、回滚的JDBC代码,都可以不用自己写了。
4. 快捷测试的支持
Spring 对 Junit 提供支持,可以通过 注解 快捷地测试 Spring 程序。
5. 快速集成功能
方便集成各种优秀框架,Spring 不排斥各种优秀的开源框架,其内部提供了对各种优秀框架
(如:Struts、Hibernate、MyBatis、Quartz 等)的直接支持。
6. 复杂API模板封装
Spring 对 JavaEE 开发中非常难用的一些 API(JDBC、JavaMail、远程调用等)都提供了模板
化的封装,这些封装 API 的提供使得应用难度大大降低。
列举一些重要的 Spring 模块?
下图对应的是 Spring4.x 版本。目前最新的 5.x 版本中 Web 模块的 Portlet 组件已经被废弃掉,同时增加了用于异步响应式处理的 WebFlux 组件。
Spring Core
核心模块, Spring 其他所有的功能基本都需要依赖于该模块,主要提供 IoC 依赖注入功能的支持。
Spring Aspects
该模块为与 AspectJ 的集成提供支持。
Spring AOP
提供了面向切面的编程实现。
Spring Data Access/Integration :
Spring Data Access/Integration 由 5 个模块组成:
spring-jdbc : 提供了对数据库访问的抽象 JDBC。不同的数据库都有自己独立的 API 用于操作数据库,而 Java 程序只需要和 JDBC API 交互,这样就屏蔽了数据库的影响。
spring-tx : 提供对事务的支持。
spring-orm : 提供对 Hibernate 等 ORM 框架的支持。
spring-oxm : 提供对 Castor 等 OXM 框架的支持。
spring-jms : Java 消息服务。
Spring Web
Spring Web 由 4 个模块组成:
spring-web :对 Web 功能的实现提供一些最基础的支持。
spring-webmvc : 提供对 Spring MVC 的实现。
spring-websocket : 提供了对 WebSocket 的支持,WebSocket 可以让客户端和服务端进行双向通信。
spring-webflux :提供对 WebFlux 的支持。WebFlux 是 Spring Framework 5.0 中引入的新的响应式框架。与 Spring MVC 不同,它不需要 Servlet API,是完全异步.
Spring Test
Spring 团队提倡测试驱动开发(TDD)。有了控制反转 (IoC)的帮助,单元测试和集成测试变得更简单。
Spring 的测试模块对 JUnit(单元测试框架)、TestNG(类似 JUnit)、Mockito(主要用来 Mock 对象)、PowerMock(解决 Mockito 的问题比如无法模拟 final, static, private 方法)等等常用的测试框架支持的都比较好。
Spring有哪些常用注解呢?
Spring有很多模块,甚至广义的SpringBoot、SpringCloud也算是Spring的一部分,我们来分模块,按功能来看一下一些常用的注解:
Web :
@Controller:组合注解(组合了@Component注解),应用在MVC层(控制层)。
@RestController:该注解为一个组合注解,相当于@Controller和@ResponseBody的组合,注解在类上,意味着,该Controller的所有方法都默认加上了@ResponseBody。
@RequestMapping:用于映射Web请求,包括访问路径和参数。如果是Restful风格接又,还可以根据请求类型使用不同的注解:
@GetMapping
@PostMapping
@PutMapping
@DeleteMapping
@ResponseBody:支持将返回值放在response内,而不是一个页面,通常用户返回json数据。
@RequestBody:允许request的参数在request体中,而不是在直接连接在地址后面。
@PathVariable:用于接收路径参数,比如@RequestMapping(“/hello/{name}”)申明的路径,将注解放在参数中前,即可获取该值,通常作为Restful的接口实现方法。
容器 :
@Component:表示一个带注释的类是一个“组件”,成为Spring管理的Bean。当使用基于注解的配置和类路径扫描时,这些类被视为自动检测的候选对象。同时@Component还是一个元注解。
@Service:组合注解(组合了@Component注解),应用在service层(业务逻辑层)。
@Repository:组合注解(组合了@Component注解),应用在dao层(数据访问层)。
@Autowired:Spring提供的工具(由Spring的依赖注入工具(BeanPostProcessor、BeanFactoryPostProcessor)自动注入)。
@Qualifier:该注解通常跟 @Autowired 一起使用,当想对注入的过程做更多的控制,@Qualifier 可帮助配置,比如两个以上相同类型的 Bean 时 Spring 无法抉择,用到此注解
@Configuration:声明当前类是一个配置类(相当于一个Spring配置的xml文件)
@Value:可用在字段,构造器参数跟方法参数,指定一个默认值,支持 #{} 跟 ${} 两个方式。一般将 SpringbBoot 中的 application.properties 配置的属性值赋值给变量。
@Bean:注解在方法上,声明当前方法的返回值为一个Bean。返回的Bean对应的类中可以定义init()方法和destroy()方法,然后在@Bean(initMethod=”init”,destroyMethod=”destroy”)定义,在构造之后执行init,在销毁之前执行destroy。
@Scope:定义我们采用什么模式去创建Bean(方法上,得有@Bean) 其设置类型包括:Singleton 、Prototype、Request 、 Session、GlobalSession。
AOP :
@Aspect:声明一个切面(类上) 使用@After、@Before、@Around定义建言(advice),
可直接将拦截规则(切点)作为参数。
@After :在方法执行之后执行(方法上)。
@Before : 在方法执行之前执行(方法上)。
@Around : 在方法执行之前与之后执行(方法上)。
@PointCut : 声明切点 在java配置类中使用@EnableAspectJAutoProxy注解开启Spring对AspectJ代理的支持(类上)。
事务:
@Transactional:在要开启事务的方法上使用@Transactional注解,即可声明式开启事务。
Spring,Spring MVC,Spring Boot 之间什么关系?
很多人对 Spring,Spring MVC,Spring Boot 这三者傻傻分不清楚!这里简单介绍一下这三者,其实很简单,没有什么高深的东西。
Spring 包含了多个功能模块(上面刚刚提高过),其中最重要的是 Spring-Core(主要提供 IoC 依赖注入功能的支持) 模块, Spring 中的其他模块(比如 Spring MVC)的功能实现基本都需要依赖于该模块。
下图对应的是 Spring4.x 版本。目前最新的 5.x 版本中 Web 模块的 Portlet 组件已经被废弃掉,同时增加了用于异步响应式处理的 WebFlux 组件。
Spring MVC 是 Spring 中的一个很重要的模块,主要赋予 **Spring 快速构建 MVC 架构的 Web 程序的能力。MVC 是模型(Model)、视图(View)、控制器(Controller)**的简写,其核心思想是通过将业务逻辑、数据、显示分离来组织代码。
使用 Spring 进行开发各种配置过于麻烦比如开启某些 Spring 特性时,需要用 XML 或 Java 进行显式配置。于是,Spring Boot 诞生了!
Spring 旨在简化 J2EE 企业应用程序开发。Spring Boot 旨在简化 Spring 开发(减少配置文件,开箱即用!)。
Spring Boot 只是简化了配置,如果你需要构建 MVC 架构的 Web 程序,你还是需要使用 Spring MVC 作为 MVC 框架,只是说 Spring Boot 帮你简化了 Spring MVC 的很多配置,真正做到开箱即用!
Spring IOC & AOP
谈谈自己对于 Spring IoC 的了解
IoC(Inverse of Control:控制反转) 是一种设计思想,而不是一个具体的技术实现。IoC 的思想就是将原本在程序中手动创建对象的控制权,交由 Spring 框架来管理。不过, IoC 并非 Spring 特有,在其他语言中也有应用。
为什么叫控制反转?
控制 :指的是对象创建(实例化、管理)的权力
反转 :控制权交给外部环境(Spring 框架、IoC 容器)
将对象之间的相互依赖关系交给 IoC 容器来管理,并由 IoC 容器完成对象的注入。这样可以很大程度上简化应用的开发,把应用从复杂的依赖关系中解放出来。 IoC 容器就像是一个工厂一样,当我们需要创建一个对象的时候,只需要配置好配置文件/注解即可,完全不用考虑对象是如何被创建出来的。
在实际项目中一个 Service 类可能依赖了很多其他的类,假如我们需要实例化这个 Service,你可能要每次都要搞清这个 Service 所有底层类的构造函数,这可能会把人逼疯。如果利用 IoC 的话,你只需要配置好,然后在需要的地方引用就行了,这大大增加了项目的可维护性且降低了开发难度。
在 Spring 中, IoC 容器是 Spring 用来实现 IoC 的载体, IoC 容器实际上就是个 Map(key,value),Map 中存放的是各种对象。
Spring 时代我们一般通过 XML 文件来配置 Bean,后来开发人员觉得 XML 文件来配置不太好,于是 SpringBoot 注解配置就慢慢开始流行起来。
为什么要使⽤IOC呢?
最主要的是两个字 解耦 ,硬编码会造成对象间的过度耦合,使用IOC之后,我们可以不用关心对象间的依赖,专心开发应用就行。
DI(依赖注入) :指的是容器在实例化对象的时候把它依赖的类注入给它。有的说法IOC和DI是一回事,有的说法是IOC是思想,DI是IOC的实现。
能简单说⼀下Spring IOC的实现机制吗?
Spring的IOC本质就是一个大工厂,我们想想一个工厂是怎么运行的呢?
生产产品 :
一个工厂最核心的功能就是生产产品。在Spring里,不用Bean自己来实例化,而是交给Spring,应该怎么实现呢?——答案毫无疑问, 反射 。那么这个厂子的生产管理是怎么做的?你应该也知道—— 工厂模式 。
库存产品 :
工厂一般都是有库房的,用来库存产品,毕竟生产的产品不能立马就拉走。Spring我们都知道是一个容器,这个容器里存的就是对象,不能每次来取对象,都得现场来反射创建对象,得把创建出的对象存起来。
订单处理 :
还有最重要的一点,工厂根据什么来提供产品呢?订单。这些订单可能五花八门,有线上签签的、有到工厂签的、还有工厂销售上门签的……最后经过处理,指导工厂的出货。在Spring里,也有这样的订单,它就是我们bean的定义和依赖关系,可以是xml形式,也可以是我们最熟悉的注解形式。
我们简单地实现一个mini版的Spring IOC:
Bean定义:
Bean通过一个配置文件定义,把它解析成一个类型。
beans.properties
这里直接用了最方便解析的properties,这里直接用一个<key,value>类型的配置来代表Bean的定义,其中key是beanName,value是class
1 | userDao:cn.fighter3.bean.UserDao |
BeanDefinition.java
bean定义类,配置文件中bean定义对应的实体
1 | public class BeanDefinition { |
ResourceLoader.java
资源加载器,用来完成配置文件中配置的加载
1 | public class ResourceLoader { |
BeanRegister.java
对象注册器,这里用于单例bean的缓存,我们大幅简化,默认所有bean都是单例的。可以看到所谓单例注册,也很简单,不过是往HashMap里存对象。
1 | public class BeanRegister { |
- BeanFactory.java
对象工厂,我们最 核心 的一个类,在它初始化的时候,创建了bean注册器,完成了资源的加载。
获取bean的时候,先从单例缓存中取,如果没有取到,就创建并注册一个bean
1 | public class BeanFactory { |
- 测试
- UserDao.java
我们的Bean类,很简单
- UserDao.java
1 | public class UserDao { |
- 单元测试
1 | public class ApiTest { |
- 运行结果
1 | A good man. |
谈谈自己对于 AOP 的了解
AOP(Aspect-Oriented Programming:面向切面编程)能够将那些与业务无关,却为业务模块所共同调用的逻辑或责任(例如事务处理、日志管理、权限控制等)封装起来,便于减少系统的重复代码,降低模块间的耦合度,并有利于未来的可拓展性和可维护性。
具体来说,假如我现在要crud写一堆业务,可是如何业务代码前后前后进行打印日志和参数的校验呢?
我们可以把日志记录和数据校验可重用的功能模块分离出来,然后在程序的执行的合适的地方动态地植入这些代码并执行。这样就简化了代码的书写。
业务逻辑代码中没有参和通用逻辑的代码,业务模块更简洁,只包含核心业务代码。实现了业务逻辑和通用逻辑的代码分离,便于维护和升级,降低了业务逻辑和通用逻辑的耦合性。
AOP 可以将遍布应用各处的功能分离出来形成可重用的组件。在编译期间、装载期间或运行期间实现在不修改源代码的情况下给程序动态添加功能。从而实现对业务逻辑的隔离,提高代码的模块化能力。
AOP 的核心其实就是 动态代理 ,如果是实现了接又的话就会使用 JDK 动态代理,否则使用CGLIB 代理,主要应用于处理一些具有横切性质的系统级服务,如日志收集、事务管理、安全检查、缓存、对象池管理等。
Spring AOP 就是基于动态代理的,如果要代理的对象,实现了某个接口,那么 Spring AOP 会使用 JDK Proxy,去创建代理对象,而对于没有实现接口的对象,就无法使用 JDK Proxy 去进行代理了,这时候 Spring AOP 会使用 Cglib 生成一个被代理对象的子类来作为代理,如下图所示:
当然你也可以使用 AspectJ !Spring AOP 已经集成了 AspectJ ,AspectJ 应该算的上是 Java 生态系统中最完整的 AOP 框架了。
AOP有哪些核心概念?
切面 (Aspect):类是对物体特征的抽象,切面就是对横切关注点的抽象
连接点 (Joinpoint):被拦截到的点,因为 Spring 只支持方法类型的连接点,所以在Spring中连接点指的就是被拦截到的方法,实际上连接点还可以是字段或者构造器
切点 (Pointcut):对连接点进行拦截的定位
通知 (Advice):所谓通知指的就是指拦截到连接点之后要执行的代码,也可以称作增强
目标对象 (Target):代理的目标对象
织入 (Weabing):织入是将增强添加到目标类的具体连接点上的过程。
编译期织入:切面在目标类编译时被织入
类加载期织入:切面在目标类加载到JVM时被织入。需要特殊的类加载器,它可以在目标类被引入应用之前增强该目标类的字节码。
运行期织入:切面在应用运行的某个时刻被织入。一般情况下,在织入切面时,AOP容器会为目标对象动态地创建一个代理对象。SpringAOP就是以这种方式织入切面。Spring采用运行期织入,而AspectJ采用编译期织入和类加载器织入。
引介 (introduction):引介是一种特殊的增强,可以动态地为类添加一些属性和方法
AOP有哪些环绕方式?
AOP 一般有 5 种 环绕方式:
- 前置通知 (@Before)
- 返回通知 (@AfterReturning)
- 异常通知 (@AfterThrowing)
- 后置通知 (@After)
- 环绕通知 (@Around)
多个切面的情况下,可以通过 @Order 指定先后顺序,数字越小,优先级越高。
说说你平时有用到AOP吗?
SpringBoot项目中,利用AOP打印接又的入参和出参日志,以及执行时间,还是比较快捷的。
引入依赖:引入AOP依赖
1 | <dependency> |
自定义注解:自定义一个注解作为切点
1 |
|
配置AOP切面:
@Aspect:标识切面
@Pointcut:设置切点,这里以自定义注解为切点,定义切点有很多其它种方式,自定
义注解是比较常用的一种。
@Before:在切点之前织入,打印了一些入参信息
@Around:环绕切点,打印返回参数和接又执行时间
1 |
|
使用:只需要在接又上加上自定义注解
1 |
|
执行结果:可以看到日志打印了入参、出参和执行时间
说说JDK 动态代理和 CGLIB 代理 ?
Spring的AOP是通过动态代理来实现的,动态代理主要有两种方式JDK动态代理和Cglib动态代理,这两种动态代理的使用和原理有些不同。
JDK 动态代理
1. Interface:对于 JDK 动态代理,目标类需要实现一个Interface。
2. InvocationHandler:InvocationHandler是一个接口,可以通过实现这个接口,定义横切逻辑,再通过反射机制(invoke)调用目标类的代码,在次过程,可能包装逻辑,对目标方法进行前置后置处理。
3. Proxy:Proxy利用InvocationHandler动态创建一个符合目标类实现的接口的实例,生成目标类的代理对象。
CgLib 动态代理
1. 使用JDK创建代理有一大限制,它只能为接又创建代理实例,而CgLib 动态代理就没有这个限制。
2. CgLib 动态代理是使用字节码处理框架 ASM,其原理是通过字节码技术为一个类创建子类,并在子类中采用方法拦截的技术拦截所有父类方法的调用,顺势织入横切逻辑。
3. CgLib 创建的动态代理对象性能比 JDK 创建的动态代理对象的性能高不少,但是 CGLib在创建代理对象时所花费的时间却比 JDK 多得多,所以对于单例的对象,因为无需频繁创建对象,用 CGLib 合适,反之,使用 JDK 方式要更为合适一些。同时,由于 CGLib 由于是采用动态创建子类的方法,对于 final 方法,无法进行代理。
我们来看一个常见的小场景,客服中转,解决用户问题:
JDK动态代理实现:
- 接口
1 | public interface ISolver { |
- 目标类:需要实现对应接口
1 | public class Solver implements ISolver { |
动态代理工厂:ProxyFactory,直接用反射方式生成一个目标对象的代理对象,这里用了一个匿名内部类方式重写InvocationHandler方法,实现接口重写也差不多
1 | public class ProxyFactory { |
客户端:Client,生成一个代理对象实例,通过代理对象调用目标对象方法
1 | public class Client { |
Cglib动态代理实现:
目标类:Solver,这里目标类不用再实现接口。
1 | public class Solver { |
动态代理工厂:
1 | public class ProxyFactory implements MethodInterceptor { |
客户端:Client
1 | public class Client { |
Spring AOP 和 AspectJ AOP 有什么区别?
Spring AOP 属于运行时增强,而 AspectJ 是编译时增强。 Spring AOP 基于代理(Proxying),而 AspectJ 基于字节码操作(Bytecode Manipulation)。
Spring AOP 已经集成了 AspectJ ,AspectJ 应该算的上是 Java 生态系统中最完整的 AOP 框架了。AspectJ 相比于 Spring AOP 功能更加强大,但是 Spring AOP 相对来说更简单,
如果我们的切面比较少,那么两者性能差异不大。但是,当切面太多的话,最好选择 AspectJ ,它比 Spring AOP 快很多。
Spring AOP 属于运行时增强,主要具有如下特点:
1. 基于动态代理来实现,默认如果使用接又的,用 JDK 提供的动态代理实现,如果是方法则使用 CGLIB 实现
2. Spring AOP 需要依赖 IOC 容器来管理,并且只能作用于 Spring 容器,使用纯 Java 代码实现
3. 在性能上,由于 Spring AOP 是基于动态代理来实现的,在容器启动时需要生成代理实例,在方法调用上也会增加栈的深度,使得 Spring AOP 的性能不如 AspectJ 的那么好。
4. Spring AOP 致力于解决企业级开发中最普遍的 AOP(方法织入)。
AspectJ
AspectJ 是一个易用的功能强大的 AOP 框架,属于编译时增强, 可以单独使用,也可以整合到其它框架中,是 AOP 编程的完全解决方案。AspectJ 需要用到单独的编译器 ajc。
AspectJ 属于 静态织入 ,通过修改代码来实现,在实际运行之前就完成了织入,所以说它生成的类是没有额外运行时开销的,一般有如下几个织入的时机:
1. 编译期织入(Compile-time weaving):如类 A 使用 AspectJ 添加了一个属性,类 B 引用了它,这个场景就需要编译期的时候就进行织入,否则没法编译类 B。
2. 编译后织入(Post-compile weaving):也就是已经生成了 .class 文件,或已经打成 jar 包了,这种情况我们需要增强处理的话,就要用到编译后织入。
3. 类加载后织入(Load-time weaving):指的是在加载类的时候进行织入,要实现这个时期的织入,有几种常见的方法
整体对比如下:
Spring Bean
什么是 Spring Bean?
简单来说,Bean 代指的就是那些被 IoC 容器所管理的对象。
我们需要告诉 IoC 容器帮助我们管理哪些对象,这个是通过配置元数据来定义的。配置元数据可以是 XML 文件、注解或者 Java 配置类。
1 | <!-- Constructor-arg with 'value' attribute --> |
下图简单地展示了 IoC 容器如何使用配置元数据来管理对象。
org.springframework.beans和 org.springframework.context 这两个包是 IoC 实现的基础,如果想要研究 IoC 相关的源码的话,可以去看看
Bean定义和依赖定义有哪些方式?
有三种方式: 直接编码方式 、 配置文件方式 、 注解方式 。
- 直接编码方式:我们一般接触不到直接编码的方式,但其实其它的方式最终都要通过直接编码来实现。
- 配置文件方式:通过xml、propreties类型的配置文件,配置相应的依赖关系,Spring读取配置文件,完成依赖关系的注入。
- 注解方式:注解方式应该是我们用的最多的一种方式了,在相应的地方使用注解修饰,Spring会扫描注解,完成依赖关系的注入。
有哪些依赖注入的方法?
Spring支持 构造方法注入 、 属性注入 、 工厂方法注入 ,其中工厂方法注入,又可以分为 静态工厂方法注入 和 非静态工厂方法注入 。
1 | public CatDaoImpl(String message){ |
1 | <bean id="CatDaoImpl" class="com.CatDaoImpl"> |
属性注入
通过Setter方法完成调用类所需依赖的注入
1 | public class Id { |
1 | <bean id="id" class="com.id "> |
- 工厂方法注入
- 静态工厂注入
静态工厂顾名思义,就是通过调用静态工厂的方法来获取自己需要的对象,为了让Spring 管理所有对象,我们不能直接通过”工程类.静态方法()”来获取对象,而是依然通过 Spring 注入的形式获取:
1 | public class DaoFactory { //静态工厂 |
1 | //factory-method="getStaticFactoryDaoImpl"指定调用哪个工厂方法 |
- 非静态工厂注入
非静态工厂,也叫实例工厂,意思是工厂方法不是静态的,所以我们需要首先 new 一个工厂实例,再调用普通的实例方法。
1 | public class DaoFactory { |
1 | <bean name="springAction" class="SpringAction"> |
什么是自动装配?
Spring IOC容器知道所有Bean的配置信息,此外,通过Java反射机制还可以获知实现类的结构信息,如构造方法的结构、属性等信息。掌握所有Bean的这些信息后,Spring IOC容器就可以按照某种规则对容器中的Bean进行自动装配,而无须通过显式的方式进行依赖配置。
Spring提供的这种方式,可以按照某些规则进行Bean的自动装配,元素提供了一个指定自动装配类型的属性:
autowire=”<自动装配类型>”
Spring提供了 4 种自动装配类型:
- byName:根据名称进行自动匹配,假设Boss又一个名为car的属性,如果容器中刚好有一个名为car的bean,Spring就会自动将其装配给Boss的car属性
- byType:根据类型进行自动匹配,假设Boss有一个Car类型的属性,如果容器中刚好有一个Car类型的Bean,Spring就会自动将其装配给Boss这个属性
- constructor:与 byType类似, 只不过它是针对构造函数注入而言的。如果Boss有一个构造函数,构造函数包含一个Car类型的入参,如果容器中有一个Car类型的Bean,则Spring将自动把这个Bean作为Boss构造函数的入参;如果容器中没有找到和构造函数入参匹配类型的Bean,则Spring将抛出异常。
- autodetect:根据Bean的自省机制决定采用byType还是constructor进行自动装配,如果Bean提供了默认的构造函数,则采用byType,否则采用constructor。
将一个类声明为 Bean 的注解有哪些?
@Component :通用的注解,可标注任意类为 Spring 组件。如果一个 Bean 不知道属于哪个层,可以使用@Component 注解标注。
@Repository : 对应持久层即 Dao 层,主要用于数据库相关操作。
@Service : 对应服务层,主要涉及一些复杂的逻辑,需要用到 Dao 层。
@Controller : 对应 Spring MVC 控制层,主要用户接受用户请求并调用 Service 层返回数据给前端页面。
@Component 和 @Bean 的区别是什么?
@Component 注解作用于类,而@Bean注解作用于方法。
@Component通常是通过类路径扫描来自动侦测以及自动装配到 Spring 容器中(我们可以使用 @ComponentScan 注解定义要扫描的路径从中找出标识了需要装配的类自动装配到 Spring 的 bean 容器中)。@Bean 注解通常是我们在标有该注解的方法中定义产生这个 bean,@Bean告诉了 Spring 这是某个类的实例,当我需要用它的时候还给我。
@Bean 注解比 @Component 注解的自定义性更强,而且很多地方我们只能通过 @Bean 注解来注册 bean。比如当我们引用第三方库中的类需要装配到 Spring容器时,则只能通过 @Bean来实现。
@Bean注解使用示例:
1 |
|
上面的代码相当于下面的 xml 配置
1 | <beans> |
下面这个例子是通过 @Component 无法实现的。
1 |
|
@Autowired 和 @Resource 的区别是什么?
Autowired 属于 Spring 内置的注解,默认的注入方式为byType(根据类型进行匹配),也就是说会优先根据接口类型去匹配并注入 Bean (接口的实现类)。
这会有什么问题呢? 当一个接口存在多个实现类的话,byType这种方式就无法正确注入对象了,因为这个时候 Spring 会同时找到多个满足条件的选择,默认情况下它自己不知道选择哪一个。
这种情况下,注入方式会变为 byName(根据名称进行匹配),这个名称通常就是类名(首字母小写)。就比如说下面代码中的 smsService 就是我这里所说的名称,这样应该比较好理解了吧。
1 | // smsService 就是我们上面所说的名称 |
举个例子,SmsService 接口有两个实现类: SmsServiceImpl1和 SmsServiceImpl2,且它们都已经被 Spring 容器所管理。
1 | // 报错,byName 和 byType 都无法匹配到 bean |
@Autowired的实现原理?
实现@Autowired的关键是: AutowiredAnnotationBeanPostProcessor
在Bean的初始化阶段,会通过Bean后置处理器来进行一些前置和后置的处理。
实现@Autowired的功能,也是通过后置处理器来完成的。这个后置处理器就是AutowiredAnnotationBeanPostProcessor。
Spring在创建bean的过程中,最终会调用到doCreateBean()**方法,在doCreateBean()方法中会调用populateBean()方法,来为bean进行属性填充,完成自动装配等工作。
在populateBean()方法中一共调用了两次后置处理器,第一次是为了判断是否需要属性填充,如果不需要进行属性填充,那么就会直接进行return,如果需要进行属性填充,那么方法就会继续向下执行,后面会进行第二次后置处理器的调用,这个时候,就会调用到
**AutowiredAnnotationBeanPostProcessor的postProcessPropertyValues()**方法,在该方法中就会进行@Autowired注解**的解析,然后实现自动装配。
1 | /** |
- postProcessorPropertyValues()**方法的源码如下,在该方法中,会先调用findAutowiringMetadata()方法解析出bean中带有@Autowired注解、@Inject和@Value**注解的属性和方法。然后调用metadata.inject()方法,进行属性填充。
Bean 的作用域有哪些?
Spring 中 Bean 的作用域通常有下面几种:
singleton : 唯一 bean 实例,Spring 中的 bean 默认都是单例的,对单例设计模式的应用。
prototype : 每次请求都会创建一个新的 bean 实例。
request : 每一次 HTTP 请求都会产生一个新的 bean,该 bean 仅在当前 HTTP request 内有效。
session : 每一次来自新 session 的 HTTP 请求都会产生一个新的 bean,该 bean 仅在当前 HTTP session 内有效。
global-session : 全局 session 作用域,仅仅在基于 portlet 的 web 应用中才有意义,Spring5 已经没有了。Portlet 是能够生成语义代码(例如:HTML)片段的小型 Java Web 插件。它们基于 portlet 容器,可以像 servlet 一样处理 HTTP 请求。但是,与 servlet 不同,每个 portlet 都有不同的会话。
如何配置 bean 的作用域呢?
xml 方式:
1 | <bean id="..." class="..." scope="singleton"></bean> |
注解方式:
1 |
|
单例 Bean 的线程安全问题了解吗?
首先结论在这:Spring中的单例Bean 不是线程安全的 。
因为单例Bean,是全局只有一个Bean,所有线程共享。如果说单例Bean,是一个无状态的,也就是线程中的操作不会对Bean中的成员变量执行 查询 以外的操作,那么这个单例Bean是线程安全的。比如Spring mvc 的 Controller、Service、Dao等,这些Bean大多是无状态的,只关注于方法本身。
假如这个Bean是有状态的,也就是会对Bean中的成员变量进行写操作,那么可能就存在线程安全的问题。
大部分时候我们并没有在项目中使用多线程,所以很少有人会关注这个问题。单例 Bean 存在线程问题,主要是因为当多个线程操作同一个对象的时候是存在资源竞争的。
单例Bean线程安全问题怎么解决呢?
常见的有这么些解决办法:
1. 将Bean定义为多例这样每一个线程请求过来都会创建一个新的Bean,但是这样容器就不好管理Bean,不能这么办。
2. 在Bean对象中尽量避免定义可变的成员变量削足适履了属于是,也不能这么干。
3. 将Bean中的成员变量保存在ThreadLocal中⭐我们知道ThredLoca能保证多线程下变量的隔离,可以在类中定义一个ThreadLocal成员变量,将需要的可变成员变量保存在ThreadLocal里,这是推荐的一种方式。
Bean 的生命周期了解么?
在Spring中,基本容器BeanFactory和扩展容器ApplicationContext的实例化时机不太一样,BeanFactory采用的是延迟初始化的方式,也就是只有在第一次getBean()的时候,才会实例化Bean;ApplicationContext启动之后会实例化所有的Bean定义。
Spring IOC 中Bean的生命周期大致分为四个阶段: 实例化 (Instantiation)、 属性赋值(Populate)、 初始化 (Initialization)、 销毁 (Destruction)。
实例化:第 1 步,实例化一个 Bean 对象
属性赋值:第 2 步,为 Bean 设置相关属性和依赖
初始化:初始化的阶段的步骤比较多, 5 、 6 步是真正的初始化,第 3 、 4 步为在初始化前执行,第 7 步在初始化后执行,初始化完成之后,Bean就可以被使用了
销毁:第 8~10步,第 8 步其实也可以算到销毁阶段,但不是真正意义上的销毁,而是先在使用前注册了销毁的相关调用接又,为了后面第 9 、 10 步真正销毁 Bean 时再执行相应的方法
简单总结⼀下,Bean⽣命周期⾥初始化的过程相对步骤会多⼀些,⽐如前置、后置的处理。 最后通过⼀个实例来看⼀下具体的细节
定义一个PersonBean 类,实现DisposableBean ,InitializingBean ,BeanFactoryAware , BeanNameAware 这 4 个接又,同时还有自定义的init-method和destroy-method 。
1 | public class PersonBean implements InitializingBean, BeanFactoryAware,BeanNameAware, DisposableBean { |
配置文件,指定init-method 和destroy-method 属性
1 |
|
- 测试
1 | public class Main { |
1 | 1.调用构造方法:我出生了! |
说说循环依赖
Spring 循环依赖:简单说就是自己依赖自己,或者和别的Bean相互依赖。
只有单例的Bean才存在循环依赖的情况, 原型 (Prototype)情况下,Spring会直接抛出异常。原因很简单,AB循环依赖,A实例化的时候,发现依赖B,创建B实例,创建B的时候发现需要A,创建A1实例……无限套娃,直接把系统干垮。
Spring可以解决哪些情况的循环依赖?
Spring不支持基于构造器注入的循环依赖,但是假如AB循环依赖,如果一个是构造器注入,一个是setter注入呢?
看看几种情形:
第四种可以而第五种不可以的原因是 Spring 在创建 Bean 时默认会根据自然排序进行创建,所以 A 会先于 B 进行创建。
所以简单总结,当循环依赖的实例都采用setter方法注入的时候,Spring可以支持,都采用构造器注入的时候,不支持,构造器注入和setter注入同时存在的时候,看天。
那Spring怎么解决循环依赖的呢?
我们都知道,单例Bean初始化完成,要经历三步:
注入就发生在第二步, 属性赋值 ,结合这个过程,Spring 通过 三级缓存 解决了循环依赖:
1. 一级缓存 : Map<String,Object> singletonObjects,单例池,用于保存实例化、属性赋值(注入)、初始化完成的 bean 实例
2. 二级缓存 : Map<String,Object> earlySingletonObjects,早期曝光对象,用于保存实例化完成的 bean 实例
3. 三级缓存 : Map<String,ObjectFactory<?>> singletonFactories,早期曝光对象工厂,用于保存 bean 创建工厂,以便于后面扩展有机会创建代理对象。
我们来看一下三级缓存解决循环依赖的过程:
当 A、B 两个类发生循环依赖时:
A实例的初始化过程:
- 创建A实例,实例化的时候把A对象工厂放入三级缓存,表示A开始实例化了,虽然我这个对象还不完整,但是先曝光出来让大家知道
A注入属性时,发现依赖B,此时B还没有被创建出来,所以去实例化B
同样,B注入属性时发现依赖A,它就会从缓存里找A对象。依次从一级到三级缓存查询A,从三级缓存通过对象工厂拿到A,发现A虽然不太完善,但是存在,把A放入二级缓存,同时删除三级缓存中的A,此时,B已经实例化并且初始化完成,把B放入一级缓存。
接着A继续属性赋值,顺利从一级缓存拿到实例化且初始化完成的B对象,A对象创建也完成,删除二级缓存中的A,同时把A放入一级缓存
最后,一级缓存中保存着实例化、初始化都完成的A、B对象
所以,我们就知道为什么Spring能解决setter注入的循环依赖了,因为实例化和属性赋值是分开的,所以里面有操作的空间。如果都是构造器注入的化,那么都得在实例化这一步完成注入,所以自然是无法支持了。
为什么要三级缓存?二级不行吗?
不行,主要是为了生成代理对象 。如果是没有代理的情况下,使用二级缓存解决循环依赖也是OK的。但是如果存在代理,三级没有问题,二级就不行了。
因为三级缓存中放的是生成具体对象的匿名内部类,获取Object的时候,它可以生成代理对象,也可以返回普通对象。使用三级缓存主要是为了保证不管什么时候使用的都是一个对象。
假设只有二级缓存的情况,往二级缓存中放的显示一个普通的Bean对象,Bean初始化过程中,通过 BeanPostProcessor 去生成代理对象之后,覆盖掉二级缓存中的普通Bean对象,那么可能就导致取到的Bean对象不一致了。
Spring 事务
Spring 事务的本质其实就是数据库对事务的支持,没有数据库的事务支持,Spring 是无法提供事务功能的。Spring 只提供统一事务管理接口,具体实现都是由各数据库自己实现,数据库事务的提交和回滚是通过数据库自己的事务机制实现。
Spring 事务的种类?
Spring 支持编程式事务管理和声明式事务管理两种方式:
1.**编程式事务
编程式事务管理使用 **TransactionTemplate,需要显式执行事务。
2. 声明式事务
3. 声明式事务管理建立在 AOP 之上的。其本质是通过 AOP 功能,对方法前后进行拦截,将事务处理的功能编织到拦截的方法中,也就是在目标方法开始之前启动一个事务,在执行完目标方法之后根据执行情况提交或者回滚事务
4. 优点是不需要在业务逻辑代码中掺杂事务管理的代码,只需在配置文件中做相关的事务规则声明或通过 @Transactional 注解的方式,便可以将事务规则应用到业务逻辑中,减少业务代码的污染。唯一不足地方是,最细粒度只能作用到方法级别,无法做到像编程式事务
那样可以作用到代码块级别。
Spring 的事务传播机制?
Spring 事务的传播机制说的是,当多个事务同时存在的时候——一般指的是多个事务方法相互调用时,Spring 如何处理这些事务的行为。
事务传播机制是使用简单的 ThreadLocal 实现的,所以,如果调用的方法是在新线程调用的,事务传播实际上是会失效的。
Spring默认的事务传播行为是PROPAFATION_REQUIRED,它适合绝大多数情况,如果多个ServiceX#methodX()都工作在事务环境下(均被Spring事务增强),且程序中存在调用链Service1#method1()->Service2#method2()->Service3#method3(),那么这 3 个服务类的三个方法通过Spring的事务传播机制都工作在同一个事务中。
七种事务传播行为详解与示例
在介绍七种事务传播行为前,我们先设计一个场景,帮助大家理解,场景描述如下
现有两个方法A和B,方法A执行会在数据库ATable插入一条数据,方法B执行会在数据库BTable插入一条数据,伪代码如下:
1 | //将传入参数a存入ATable |
接下来,我们看看在如下场景下,没有事务,情况会怎样
1 | public void testMain(){ |
在这里要做一个重要提示:Spring中事务的默认实现使用的是AOP,也就是代理的方式,如果大家在使用代码测试时,同一个Service类中的方法相互调用需要使用注入的对象来调用,不要直接使用this.方法名来调用,this.方法名调用是对象内部方法调用,不会通过Spring代理,也就是事务不会起作用
以上伪代码描述的一个场景,方法testMain和testB都没有事务,执行testMain方法,那么结果会怎么样呢?
相信大家都知道了,就是a1数据成功存入ATable表,b1数据成功存入BTable表,而在抛出异常后b2数据存储就不会执行,也就是b2数据不会存入数据库,这就是没有事务的场景。
可想而知,在上一篇文章(认识事务)中举例的转账操作,如果在某一步发生异常,且没有事务,那么钱是不是就凭空消失了,所以事务在数据库操作中的重要性可想而知。接下我们就开始理解七种不同事务传播类型的含义
REQUIRED(Spring默认的事务传播类型)
如果当前没有事务,则自己新建一个事务,如果当前存在事务,则加入这个事务
*(示例1)*根据场景举栗子,我们在testMain和testB上声明事务,设置传播行为REQUIRED,伪代码如下:
1 |
|
该场景下执行testMain方法结果如何呢?
数据库没有插入新的数据,数据库还是保持着执行testMain方法之前的状态,没有发生改变。testMain上声明了事务,在执行testB方法时就加入了testMain的事务(当前存在事务,则加入这个事务),在执行testB方法抛出异常后事务会发生回滚,又testMain和testB使用的同一个事务,所以事务回滚后testMain和testB中的操作都会回滚,也就使得数据库仍然保持初始状态。
*(示例2)*根据场景再举一个栗子,我们只在testB上声明事务,设置传播行为REQUIRED,伪代码如下:
1 | public void testMain(){ |
这时的执行结果又如何呢?
数据a1存储成功,数据b1和b2没有存储。由于testMain没有声明事务,testB有声明事务且传播行为是REQUIRED,所以在执行testB时会自己新建一个事务(如果当前没有事务,则自己新建一个事务),testB抛出异常则只有testB中的操作发生了回滚,也就是b1的存储会发生回滚,但a1数据不会回滚,所以最终a1数据存储成功,b1和b2数据没有存储。
SUPPORTS
当前存在事务,则加入当前事务,如果当前没有事务,就以非事务方法执行
*(示例3)*根据场景举栗子,我们只在testB上声明事务,设置传播行为SUPPORTS,伪代码如下:
1 | public void testMain(){ |
这种情况下,执行testMain的最终结果就是,a1,b1存入数据库,b2没有存入数据库。由于testMain没有声明事务,且testB的事务传播行为是SUPPORTS,所以执行testB时就是没有事务的(如果当前没有事务,就以非事务方法执行),则在testB抛出异常时也不会发生回滚,所以最终结果就是a1和b1存储成功,b2没有存储。
那么当我们在testMain上声明事务且使用REQUIRED传播方式的时候,这个时候执行testB就满足当前存在事务,则加入当前事务,在testB抛出异常时事务就会回滚,最终结果就是a1,b1和b2都不会存储到数据库。
MANDATORY
当前存在事务,则加入当前事务,如果当前事务不存在,则抛出异常。
*(示例4)*场景举栗子,我们只在testB上声明事务,设置传播行为MANDATORY,伪代码如下:
1 | public void testMain(){ |
这种情形的执行结果就是a1存储成功,而b1和b2没有存储。b1和b2没有存储,并不是事务回滚的原因,而是因为testMain方法没有声明事务,在去执行testB方法时就直接抛出事务要求的异常(如果当前事务不存在,则抛出异常),所以testB方法里的内容就没有执行。
那么如果在testMain方法进行事务声明,并且设置为REQUIRED,则执行testB时就会使用testMain已经开启的事务,遇到异常就正常的回滚了。
REQUIRES_NEW
创建一个新事务,如果存在当前事务,则挂起该事务。
可以理解为设置事务传播类型为REQUIRES_NEW的方法,在执行时,不论当前是否存在事务,总是会新建一个事务。
*(示例5)*场景举栗子,为了说明设置REQUIRES_NEW的方法会开启新事务,我们把异常发生的位置换到了testMain,然后给testMain声明事务,传播类型设置为REQUIRED,testB也声明事务,设置传播类型为REQUIRES_NEW,伪代码如下
1 |
|
这种情形的执行结果就是a1没有存储,而b1和b2存储成功,因为testB的事务传播设置为REQUIRES_NEW,所以在执行testB时会开启一个新的事务,testMain中发生的异常时在testMain所开启的事务中,所以这个异常不会影响testB的事务提交,testMain中的事务会发生回滚,所以最终a1就没有存储,而b1和b2就存储成功了。
与这个场景对比的一个场景就是testMain和testB都设置为REQUIRED,那么上面的代码执行结果就是所有数据都不会存储,因为testMain和testMain是在同一个事务下的,所以事务发生回滚时,所有的数据都会回滚。
NOT_SUPPORTED
始终以非事务方式执行,如果当前存在事务,则挂起当前事务
可以理解为设置事务传播类型为NOT_SUPPORTED的方法,在执行时,不论当前是否存在事务,都会以非事务的方式运行。
*(示例6)*场景举栗子,testMain传播类型设置为REQUIRED,testB传播类型设置为NOT_SUPPORTED,且异常抛出位置在testB中,伪代码如下
1 |
|
该场景的执行结果就是a1和b2没有存储,而b1存储成功。testMain有事务,而testB不使用事务,所以执行中testB的存储b1成功,然后抛出异常,此时testMain检测到异常事务发生回滚,但是由于testB不在事务中,所以只有testMain的存储a1发生了回滚,最终只有b1存储成功,而a1和b1都没有存储。
NEVER
不使用事务,如果当前事务存在,则抛出异常
很容易理解,就是我这个方法不使用事务,并且调用我的方法也不允许有事务,如果调用我的方法有事务则我直接抛出异常。
*示例7)*场景举栗子,testMain设置传播类型为REQUIRED,testB传播类型设置为NEVER,并且把testB中的抛出异常代码去掉,则伪代码如下
1 |
|
该场景执行,直接抛出事务异常,且不会有数据存储到数据库。由于testMain事务传播类型为REQUIRED,所以testMain是运行在事务中,而testB事务传播类型为NEVER,所以testB不会执行而是直接抛出事务异常,此时testMain检测到异常就发生了回滚,所以最终数据库不会有数据存入。
NESTED
如果当前事务存在,则在嵌套事务中执行,否则REQUIRED的操作一样(开启一个事务)
这里需要注意两点:
- 和REQUIRES_NEW的区别
REQUIRES_NEW是新建一个事务并且新开启的这个事务与原有事务无关,而NESTED则是当前存在事务时(我们把当前事务称之为父事务)会开启一个嵌套事务(称之为一个子事务)。
在NESTED情况下父事务回滚时,子事务也会回滚,而在REQUIRES_NEW情况下,原有事务回滚,不会影响新开启的事务。
- 和REQUIRED的区别
REQUIRED情况下,调用方存在事务时,则被调用方和调用方使用同一事务,那么被调用方出现异常时,由于共用一个事务,所以无论调用方是否catch其异常,事务都会回滚
而在NESTED情况下,被调用方发生异常时,调用方可以catch其异常,这样只有子事务回滚,父事务不受影响
*(示例8)*场景举栗子,testMain设置为REQUIRED,testB设置为NESTED,且异常发生在testMain中,伪代码如下
1 |
|
该场景下,所有数据都不会存入数据库,因为在testMain发生异常时,父事务回滚则子事务也跟着回滚了,可以与*(示例5)*比较看一下,就找出了与REQUIRES_NEW的不同
*(示例9)*场景举栗子,testMain设置为REQUIRED,testB设置为NESTED,且异常发生在testB中,伪代码如下
1 |
|
这种场景下,结果是a1,a2存储成功,b1和b2存储失败,因为调用方catch了被调方的异常,所以只有子事务回滚了。
同样的代码,如果我们把testB的传播类型改为REQUIRED,结果也就变成了:没有数据存储成功。就算在调用方catch了异常,整个事务还是会回滚,因为,调用方和被调方共用的同一个事务。
Spring 的事务隔离级别
Spring的接又TransactionDefinition中定义了表示隔离级别的常量,当然其实主要还是对应数据
库的事务隔离级别:
1. ISOLATION_DEFAULT:使用后端数据库默认的隔离界别,MySQL 默认可重复读,Oracle 默认读已提交。
2. ISOLATION_READ_UNCOMMITTED:读未提交
3. ISOLATION_READ_COMMITTED:读已提交
4. ISOLATION_REPEATABLE_READ:可重复读
5. ISOLATION_SERIALIZABLE:串行化
@Transactional(rollbackFor = Exception.class)注解了解吗?
Exception 分为运行时异常 RuntimeException 和非运行时异常。事务管理对于企业应用来说是至关重要的,即使出现异常情况,它也可以保证数据的一致性。
当 @Transactional 注解作用于类上时,该类的所有 public 方法将都具有该类型的事务属性,同时,我们也可以在方法级别使用该标注来覆盖类级别的定义。如果类或者方法加了这个注解,那么这个类里面的方法抛出异常,就会回滚,数据库里面的数据也会回滚。
在 @Transactional 注解中如果不配置rollbackFor属性,那么事务只会在遇到RuntimeException的时候才会回滚,加上 rollbackFor=Exception.class,可以让事务在遇到非运行时异常时也回滚。
Spring MVC
说说自己对于 Spring MVC 了解?
MVC 是模型(Model)、视图(View)、控制器(Controller)的简写,其核心思想是通过将业务逻辑、数据、显示分离来组织代码。
Model 1 时代
很多学 Java 后端比较晚的朋友可能并没有接触过 Model 1 时代下的 JavaWeb 应用开发。在 Model1 模式下,整个 Web 应用几乎全部用 JSP 页面组成,只用少量的 JavaBean 来处理数据库连接、访问等操作。
这个模式下 JSP 即是控制层(Controller)又是表现层(View)。显而易见,这种模式存在很多问题。比如控制逻辑和表现逻辑混杂在一起,导致代码重用率极低;再比如前端和后端相互依赖,难以进行测试维护并且开发效率极低。
Model 2 时代
学过 Servlet 并做过相关 Demo 的朋友应该了解“Java Bean(Model)+ JSP(View)+Servlet(Controller) ”这种开发模式,这就是早期的 JavaWeb MVC 开发模式。
Model:系统涉及的数据,也就是 dao 和 bean。
View:展示模型中的数据,只是用来展示。
Controller:处理用户请求都发送给 ,返回数据给 JSP 并展示给用户。
Model2 模式下还存在很多问题,Model2 的抽象和封装程度还远远不够,使用 Model2 进行开发时不可避免地会重复造轮子,这就大大降低了程序的可维护性和复用性。
于是,很多 JavaWeb 开发相关的 MVC 框架应运而生比如 Struts2,但是 Struts2 比较笨重。
Spring MVC 时代
随着 Spring 轻量级开发框架的流行,Spring 生态圈出现了 Spring MVC 框架, Spring MVC 是当前最优秀的 MVC 框架。相比于 Struts2 , Spring MVC 使用更加简单和方便,开发效率更高,并且 Spring MVC 运行速度更快。
MVC 是一种设计模式,Spring MVC 是一款很优秀的 MVC 框架。Spring MVC 可以帮助我们进行更简洁的 Web 层的开发,并且它天生与 Spring 框架集成。Spring MVC 下我们一般把后端项目分为 Service 层(处理业务)、Dao 层(数据库操作)、Entity 层(实体类)、Controller 层(控制层,返回数据给前台页面)。
Spring MVC 的核心组件?
1. DispatcherServlet:前置控制器,是整个流程控制的核心,控制其他组件的执行,进行统一调度,降低组件之间的耦合性,相当于总指挥。
2. Handler:处理器,完成具体的业务逻辑,相当于 Servlet 或 Action。
3. HandlerMapping:DispatcherServlet 接收到请求之后,通过 HandlerMapping 将不同的请求映射到不同的 Handler。
4. HandlerInterceptor:处理器拦截器,是一个接口,如果需要完成一些拦截处理,可以实现该接口。
5. HandlerExecutionChain:处理器执行链,包括两部分内容:Handler 和 HandlerInterceptor(系统会有一个默认的 HandlerInterceptor,如果需要额外设置拦截,可以添加拦截器)。
6. HandlerAdapter:处理器适配器,Handler 执行业务方法之前,需要进行一系列的操作,包括表单数据的验证、数据类型的转换、将表单数据封装到 JavaBean 等,这些操作都是由 HandlerApater 来完成,开发者只需将注意力集中业务逻辑的处理上,DispatcherServlet
通过 HandlerAdapter 执行不同的 Handler。
7. ModelAndView:装载了模型数据和视图信息,作为 Handler 的处理结果,返回给DispatcherServlet。
8. ViewResolver:视图解析器,DispatcheServlet 通过它将逻辑视图解析为物理视图,最终将渲染结果响应给客户端。
Spring MVC 的工作流程?
1. 客户端向服务端发送一次请求,这个请求会先到前端控制器DispatcherServlet(也叫中央控制器)。
2. DispatcherServlet接收到请求后会调用HandlerMapping处理器映射器。由此得知,该请求该由哪个Controller来处理(并未调用Controller,只是得知)
3. DispatcherServlet调用HandlerAdapter处理器适配器,告诉处理器适配器应该要去执行哪个Controller
4. HandlerAdapter处理器适配器去执行Controller并得到ModelAndView(数据和视图)**,并层层返回给DispatcherServlet
**5. DispatcherServlet将ModelAndView交给ViewReslover视图解析器解析,然后返回真正的视图
6. DispatcherServlet将模型数据填充到视图中
7. DispatcherServlet将结果响应给客户端
Spring MVC 虽然整体流程复杂,但是实际开发中很简单,大部分的组件不需要开发人员创建和管理,只需要通过配置文件的方式完成配置即可,真正需要开发人员进行处理的只有Handler(Controller) 、 View 、 Model 。
当然我们现在大部分的开发都是前后端分离,Restful风格接口,后端只需要返回Json数据就行了。
SpringMVC Restful风格的接又的流程是什么样的呢?
我们都知道Restful接又,响应格式是json,这就用到了一个常用注解: @ResponseBody
加入了这个注解后,整体的流程上和使用ModelAndView大体上相同,但是细节上有一些不同:
1 |
|
1. 客户端向服务端发送一次请求,这个请求会先到前端控制器DispatcherServlet
2. DispatcherServlet接收到请求后会调用HandlerMapping处理器映射器。由此得知,该请求该由哪个Controller来处理
3. DispatcherServlet调用HandlerAdapter处理器适配器,告诉处理器适配器应该要去执行哪个Controller
4. Controller被封装成了ServletInvocableHandlerMethod,HandlerAdapter处理器适配器去执行invokeAndHandle方法,完成对Controller的请求处理
5. HandlerAdapter执行完对Controller的请求,会调用HandlerMethodReturnValueHandler去处理返回值,主要的过程:
5.1. 调用RequestResponseBodyMethodProcessor,创建ServletServerHttpResponse(Spring对原生ServerHttpResponse的封装)实例
5.2.**使用HttpMessageConverter的write方法,将返回值写入ServletServerHttpResponse的OutputStream输出流中
**5.3.**在写入的过程中,会使用JsonGenerator(默认使用Jackson框架)对返回值进行Json序列化
**6. 执行完请求后,返回的ModealAndView为null,ServletServerHttpResponse里也已经写入了响应,所以不用关心View的处理
Spring Boot
介绍一下SpringBoot,有哪些优点?
Spring Boot 基于 Spring 开发,Spirng Boot 本身并不提供 Spring 框架的核心特性以及扩展功能,只是用于快速、敏捷地开发新一代基于 Spring 框架的应用程序。它并不是用来替代Spring 的解决方案,而是和 Spring 框架紧密结合用于提升 Spring 开发者体验的工具。
Spring Boot 以约定大于配置核心思想开展工作,相比Spring具有如下优势:
1. Spring Boot 可以快速创建独立的Spring应用程序。
2. Spring Boot 内嵌了如Tomcat,Jetty和Undertow这样的容器,也就是说可以直接跑起来,用不着再做部署工作了。
3. Spring Boot 无需再像Spring一样使用一堆繁琐的xml文件配置。
4. Spring Boot 可以自动配置(核心)Spring。SpringBoot将原有的XML配置改为Java配置,将bean注入改为使用注解注入的方式(@Autowire),并将多个xml、properties配置浓缩在一个appliaction.yml配置文件中。
5. Spring Boot 提供了一些现有的功能,如量度工具,表单数据验证以及一些外部配置这样的一些第三方功能。
6. Spring Boot 可以快速整合常用依赖(开发库,例如spring-webmvc、jackson-json、validation-api和tomcat等),提供的POM可以简化Maven的配置。当我们引入核心依赖时,SpringBoot会自引入其他依赖。
SpringBoot自动配置原理了解吗?
SpringBoot开启自动配置的注解是**@EnableAutoConfiguration** ,启动类上的注解**@SpringBootApplication是一个复合注解,包含了@EnableAutoConfiguration**:
EnableAutoConfiguration 只是一个简单的注解,自动装配核心功能的实现实际是通过 AutoConfigurationImportSelector类
1 | //将main同级的包下的所有组件注册到容器中 |
AutoConfigurationImportSelector实现了ImportSelector接口,这个接口的作用就是收集需要导入的配置类,配合**@Import()就可以将相应的类导入到Spring容器中获取注入类的方法是selectImports(),它实际调用的是getAutoConfigurationEntry**,这个方法是获取自动装配类的关键,主要流程可以分为这么几步:
1. 获取注解的属性,用于后面的排除
2. 获取所有需要自动装配的配置类的路径:这一步是最关键的,从META-INF/spring.factories获取自动配置类的路径
3. 去掉重复的配置类和需要排除的重复类,把需要自动加载的配置类的路径存储起来
1 | rotected AutoConfigurationImportSelector.AutoConfigurationEntry |
如何自定义一个SpringBoot Srarter?
1. 创建一个项目,命名为demo-spring-boot-starter,引入SpringBoot相关依赖
1 | <dependency> |
2. 编写配置文件
这里定义了属性配置的前缀
1 |
|
3.自动装配
创建自动配置类HelloPropertiesConfigure
1 |
|
4.配置自动类
在/resources/META-INF/spring.factories文件中添加自动配置类路径
1 | org.springframework.boot.autoconfigure.EnableAutoConfiguration=\ |
5.测试
- 创建一个工程,引入自定义starter依赖
1 | <dependency> |
- 在配置文件里添加配置
1 | hello.name=张三 |
- 测试类
1 |
|
运行结果
Springboot 启动原理?
SpringApplication 这个类主要做了以下四件事情:
1. 推断应用的类型是普通的项目还是 Web 项目
2. 查找并加载所有可用初始化器 , 设置到 initializers 属性中
3. 找出所有的应用程序监听器,设置到 listeners 属性中
4. 推断并设置 main 方法的定义类,找到运行的主类
SpringBoot 启动大致流程如下 :