基础
1.Spring 是什么?特性?有哪些模块?
一句话概括:Spring 是一个轻量级、非入侵式的控制反转 (IoC) 和面向切面 (AOP) 的框架。
2003 年,一个音乐家 Rod Johnson 决定发展一个轻量级的 Java 开发框架,
Spring
作为 Java 战场的龙骑兵渐渐崛起,并淘汰了
EJB
这个传统的重装骑兵。
到了现在,企业级开发的标配基本就是 Spring5 + Spring Boot 2 + JDK 8
Spring 有哪些特性呢?
Spring 有很多优点:
- IOC 和 DI 的支持
Spring 的核心就是一个大的工厂容器,可以维护所有对象的创建和依赖关系,Spring 工厂用于生成 Bean,并且管理 Bean 的生命周期,实现高内聚低耦合的设计理念。
- AOP 编程的支持
Spring 提供了面向切面编程,可以方便的实现对程序进行权限拦截、运行监控等切面功能。
- 声明式事务的支持
支持通过配置就来完成对事务的管理,而不需要通过硬编码的方式,以前重复的一些事务提交、回滚的 JDBC 代码,都可以不用自己写了。
- 快捷测试的支持
Spring 对 Junit 提供支持,可以通过注解快捷地测试 Spring 程序。
- 快速集成功能
方便集成各种优秀框架,Spring 不排斥各种优秀的开源框架,其内部提供了对各种优秀框架(如:Struts、Hibernate、MyBatis、Quartz 等)的直接支持。
- 复杂 API 模板封装
Spring 对 JavaEE 开发中非常难用的一些 API(JDBC、JavaMail、远程调用等)都提供了模板化的封装,这些封装 API 的提供使得应用难度大大降低。
2.Spring 有哪些模块呢?
Spring 框架是分模块存在,除了最核心的
Spring Core Container
是必要模块之外,其他模块都是
可选
,大约有 20 多个模块。
最主要的七大模块:
- Spring Core:Spring 核心,它是框架最基础的部分,提供 IOC 和依赖注入 DI 特性。
- Spring Context:Spring 上下文容器,它是 BeanFactory 功能加强的一个子接口。
- Spring Web:它提供 Web 应用开发的支持。
- Spring MVC:它针对 Web 应用中 MVC 思想的实现。
- Spring DAO:提供对 JDBC 抽象层,简化了 JDBC 编码,同时,编码更具有健壮性。
- Spring ORM:它支持用于流行的 ORM 框架的整合,比如:Spring + Hibernate、Spring + iBatis、Spring + JDO 的整合等。
- Spring AOP:即面向切面编程,它提供了与 AOP 联盟兼容的编程实现。
3.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 的接口实现方法。 - @RestController:该注解为一个组合注解,相当于@Controller 和@ResponseBody 的组合,注解在类上,意味着,该 Controller 的所有方法都默认加上了@ResponseBody。
容器:
- @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 注解,即可声明式开启事务。
总结
关于web相关注解,发送请求@RequestMapping指定请求方式method,请求路径value;另外请求参数相关@requestparam,@requestbody,@pathvariable
@requestbody:一般用于接收前端发送来的 json、xml格式的数据,也就是 application/json、application/xml等类型的数据。
@PathVariable
映射 URL 绑定的占位符(只能占一个参数)
@RequestParam
是 获取请求参数的值,也就是 **
?
** 后面的那些值 。
返回数据json格式@responsebody,@restcontroller=@controller+@responsebody
关于面向切面编程,@aspect表示是一个切面类, 使用@After、@Before、@Around 定义建言(advice),可直接将拦截规则(切点)作为参数
关于容器相关的@bean,@service,@repository,@controller,@value,@configuration
4.Spring 中应用了哪些设计模式呢?
Spring 框架中广泛使用了不同类型的设计模式,下面我们来看看到底有哪些设计模式?
- 工厂模式 : Spring 容器本质是一个大工厂,使用工厂模式通过 BeanFactory、ApplicationContext 创建 bean 对象。
- 代理模式 : Spring AOP 功能功能就是通过代理模式来实现的,分为动态代理和静态代理。
- 单例模式 : Spring 中的 Bean 默认都是单例的,这样有利于容器对 Bean 的管理。
- 模板模式 : Spring 中 JdbcTemplate、RestTemplate 等以 Template 结尾的对数据库、网络等等进行操作的模板类,就使用到了模板模式。
- 观察者模式: Spring 事件驱动模型就是观察者模式很经典的一个应用。
- 适配器模式 :Spring AOP 的增强或通知 (Advice) 使用到了适配器模式、Spring MVC 中也是用到了适配器模式适配 Controller。
- 策略模式:Spring 中有一个 Resource 接口,它的不同实现类,会根据不同的策略去访问资源。
IOC
5.说一说什么是 IOC?什么是 DI?
Java 是面向对象的编程语言,一个个实例对象相互合作组成了业务逻辑,原来,我们都是在代码里创建对象和对象的依赖。
所谓的IOC(控制反转):就是由容器来负责控制对象的生命周期和对象间的关系。以前是我们想要什么,就自己创建什么,现在是我们需要什么,容器就给我们送来什么。
也就是说,控制对象生命周期的不再是引用它的对象,而是容器。对具体对象,以前是它控制其它对象,现在所有对象都被容器控制,所以这就叫控制反转。
DI(依赖注入):指的是容器在实例化对象的时候把它依赖的类注入给它。有的说法是 IOC 是思想,DI 是 IOC 的实现。
为什么要使用 IOC 呢?
最主要的是两个字解耦,硬编码会造成对象间的过度耦合,使用 IOC 之后,我们可以不用关心对象间的依赖,专心开发应用就行。
谁控制谁:在之前开发中,我们需要什么对象就去创建对象,自己控制对象,有了IOC容器之后,就会编程ioc容器控制对象
控制什么:在实现过程中所需要的对象以及需要依赖的对象
什么是反转:在没有IOC容器之前我们都是在对象中主动创建依赖的对象,正转,有了IOC容器之后,依赖的对象直接由ioc容器创建后注入对象中,由主动创建变成了被动接受,这是反转
哪些方面被反转:依赖的对象
总结
1 在传统的 Java 程序开发中,我们只能通过 new 关键字来创建对象,这种导致程序中对象的依赖关系比较复杂,耦合度较高
而 IOC 的主要作用是实现了对象的管理,也就是我们把设计好的对象交给了 IOC容器控制,然后在需要用到目标对象的时候,直接从容器中去获取
有了 IOC 容器来管理 Bean 以后,相当于把对象的创建和查找依赖对象的控制权交给了容器,这种设计理念使得对象与对象之间是一种松耦合状态,极大提升了程序的灵活性以及功能的复用性。
2 然后,DI 表示依赖注入,也就是对于 IOC 容器中管理的 Bean,如果 Bean 之间存在依赖关系,那么 IOC 容器需要自动实现依赖对象的实例注入,通常有三种方法来描述 Bean 之间的依赖关系,接口注入,setter 注入,构造器注入
3 另外,为了更加灵活的实现 Bean 实例的依赖注入,Spring 还提供了@Resource和@Autowired 这两个注解。分别是根据 bean 的 id 和 bean 的类型来实现依赖注入。
6.能简单说一下 Spring IOC 的实现机制吗?
IOC 是什么
Bean 的声明方式
IOC 的工作流程
IOC 的全称是 Inversion Of Control,也就是控制反转,它的核心思想是把对象的管理权限交给容器。
应用程序如果需要使用到某个对象实例,直接从 IOC 容器中去获取就行,这样设计的好处是降低了程序里面对象与对象之间的耦合性。
使得程序的整个体系结构变得更加灵活
声明方式:xml,注解,实现接口
工作流程
第一个阶段,就是 IOC 容器的初始化
这个阶段主要是根据程序中定义的 XML 或者注解等 Bean 的声明方式通过解析和加载后生成 BeanDefinition,然后把 BeanDefinition 注册到 IOC 容
器。
通过注解或者 xml 声明的 bean 都会解析得到一个 BeanDefinition 实体,实体中包含这个 bean 中定义的基本属性。
最后把这个 BeanDefinition 保存到一个 Map 集合里面,从而完成了 IOC 的初始化。
IoC 容器的作用就是对这些注册的 Bean 的定义信息进行处理和维护,它 IoC 容器控制反转的核心
第二个阶段,完成 Bean 初始化及依赖注入然后进入到第二个阶段,这个阶段会做两个事情
通过反射针对没有设置 lazy-init 属性的单例 bean 进行初始化。完成 Bean 的依赖注入。
初始化ioc-》初始化bean,非懒加载->创建bean实例->注入属性
第三个阶段,Bean 的使用
通常我们会通过@Autowired或者BeanFactory.getBean()从IOC容器中获取指定的 bean 实例。
另外,针对设置 layy-init 属性以及非单例 bean 的实例化,是在每次获取 bean对象的时候,调用 bean 的初始化方法来完成实例化的,并且 Spring IOC 容器不会去管理这些 Bean。
7.说说 BeanFactory 和 ApplicantContext?
可以这么形容,BeanFactory是Spring的“心脏”,ApplicantContext是完整的“身躯”。
BeanFactory和ApplicantContext的比喻
BeanFactory(Bean工厂)是Spring框架的基础设施,面向Spring本身。
ApplicantContext(应用上下文)建立在BeanFactoty基础上,面向使用Spring框架的开发者。
BeanFactory 接口
BeanFactory是类的通用工厂,可以创建并管理各种类的对象。
Spring为BeanFactory提供了很多种实现,最常用的是XmlBeanFactory,但在Spring 3.2中已被废弃,建议使用XmlBeanDefinitionReader、DefaultListableBeanFactory。
BeanFactory接口位于类结构树的顶端,它最主要的方法就是getBean(String var1),这个方法从容器中返回特定名称的Bean。
BeanFactory的功能通过其它的接口得到了不断的扩展,比如AbstractAutowireCapableBeanFactory定义了将容器中的Bean按照某种规则(比如按名字匹配、按类型匹配等)进行自动装配的方法。
总结
beanfactory
(1)、是Spring里面最底层的接口(最原始的接口),包含了各种Bean的定义,读取bean配置文档,管理bean的加载、实例化,控制bean的生命周期,维护bean之间的依赖关系。
(2)、采用的是延迟加载形式来注入Bean的,即只有在使用到某个Bean时(调用getBean()),才对该Bean进行加载实例化。这样,我们就不能发现一些存在的Spring的配置问题。如果Bean的某一个属性没有注入,BeanFacotry加载后,直至第一次使用调用getBean方法才会抛出异常。
(3)BeanFactory通常以编程的方式被创建。
ApplicationContext
ApplicationContext接口作为BeanFactory的派生,除了提供BeanFactory所具有的功能外,还提供了更完整的框架功能
它是在容器启动时,一次性创建了所有的Bean。这样,在容器启动时,我们就可以发现Spring中存在的配置错误,这样有利于检查所依赖属性是否注入。 ApplicationContext启动后预载入所有的单实例Bean,通过预载入单实例bean ,确保当你需要的时候,你就不用等待,因为它们已经创建好了。
编程式方式创建,还能能以声明的方式创建,如使用ContextLoader。
8.你知道 Spring 容器启动阶段会干什么吗?
Spring 的 IOC 容器工作的过程,其实可以划分为两个阶段:容器启动阶段和Bean 实例化阶段。
其中容器启动阶段主要做的工作是加载和解析配置文件,保存到对应的 Bean 定义中。
容器启动开始,首先会通过某种途径加载 Congiguration MetaData,在大部分情况下,容器需要依赖某些工具类(BeanDefinitionReader)对加载的 Congiguration MetaData 进行解析和分析,并将分析后的信息组为相应的 BeanDefinition。
最后把这些保存了 Bean 定义必要信息的 BeanDefinition,注册到相应的 BeanDefinitionRegistry,这样容器启动就完成了。
9.能说一下 Spring Bean 生命周期吗?
1 创建前准备
2 创建实例化
3 依赖注入
4 容器缓存
5 销毁实例
第一个阶段:创建前准备
这个阶段主要作用,bean在开始加载之前,要从上下文和一些配置中去解析并且查找bean有关的扩展实现,比如说像"init-method" 容器在初始化bean时候会调用方法,destory-method 容器在销毁时候会调用方法,以及beanfactoryprocessor这类bean加载过程中,前置和后置一些处理扩展实现,这些类或者配置其实是spring 提供给开发者实现bean加载过程中的一些扩展,在很多spring集成的中间件比较常见,比如说像dubbo
第二个阶段:创建实例
这个阶段主要作用,通过反射去创建bean的实例对象,并且会扫描和解析bean定义声明的一些属性
第三个阶段: 依赖注入
如果被实例化的bean, 存在依赖其他bean对象一些情况,则需要对依赖的bean进行对象注入,比如常见的@autowired以及setter注入等,这样一些配置形式;同时在这个阶段会触发一些扩展的调用,比如说常见的扩展类beanpostProcessors用来实现bean初始化前后的扩展回调,比如beanfactoryaware
第四个阶段:容器缓存阶段
主要作用是把bean保存到容器中,以及spring的缓存中;到了这个阶段的bean,bean就可以被开发者使用了,这个阶段常用操作init-method,这个属性的一些方法,或者会被调用以及beanpostprocessors后置处理器方法,也会在这个阶段触发
第五个阶段:销毁实例阶段
当spring的应用上下文被关闭时候, 那么这个上下文所有的bean会被销毁,如果存在bean,实现了DisposableBean接口,或者配置了destory-method属性的方法会在这个阶段会被调用
10.Bean 定义和依赖定义有哪些方式?
有三种方式:直接编码方式、配置文件方式、注解方式。
- 直接编码方式:我们一般接触不到直接编码的方式,但其实其它的方式最终都要通过直接编码来实现。
- 配置文件方式:通过 xml、propreties 类型的配置文件,配置相应的依赖关系,Spring 读取配置文件,完成依赖关系的注入。
- 注解方式:注解方式应该是我们用的最多的一种方式了,在相应的地方使用注解修饰,Spring 会扫描注解,完成依赖关系的注入。
xml配置bean
构造器
如果你之前有在bean.xml文件中配置过bean的经历,那么对如下的配置肯定不会陌生:
<beanid="personService"class="com.sue.cache.service.test7.PersonService"></bean>
这种方式是以前使用最多的方式,它默认使用了无参构造器创建bean。
当然我们还可以使用有参的构造器,通过
<constructor-arg>
标签来完成配置。
<beanid="personService"class="com.sue.cache.service.test7.PersonService"><constructor-argindex="0"value="susan"></constructor-arg><constructor-argindex="1"ref="baseInfo"></constructor-arg></bean>
其中:
index
表示下标,从0开始。value
表示常量值ref
表示引用另一个bean
setter方法
除此之外,spring还提供了另外一种思路:通过setter方法设置bean所需参数,这种方式耦合性相对较低,比有参构造器使用更为广泛。
先定义Person实体:
@DatapublicclassPerson{privateString name;privateint age;}
它里面包含:成员变量name和age,getter/setter方法。
然后在bean.xml文件中配置bean时,加上
<property>
标签设置bean所需参数。
<beanid="person"class="com.sue.cache.service.test7.Person"><propertyname="name"value="susan"></constructor-arg><propertyname="age"value="18"></constructor-arg></bean>
静态工厂
这种方式的关键是需要定义一个工厂类,它里面包含一个创建bean的静态方法。例如:
publicclassSusanBeanFactory{publicstaticPersoncreatePerson(String name,int age)returnnewPerson(name, age);}}
接下来定义Person类如下:
@AllArgsConstructor@NoArgsConstructor@DatapublicclassPerson{privateString name;privateint age;}
它里面包含:成员变量name和age,getter/setter方法,无参构造器和全参构造器。
然后在bean.xml文件中配置bean时,通过
factory-method
参数指定静态工厂方法,同时通过
<constructor-arg>
设置相关参数。
<beanclass="com.sue.cache.service.test7.SusanBeanFactory"factory-method="createPerson"><constructor-argindex="0"value="susan"></constructor-arg><constructor-argindex="1"value="18"></constructor-arg></bean>
实例工厂
这种方式也需要定义一个工厂类,但里面包含非静态的创建bean的方法。
public class SusanBeanFactory {
public Person createPerson(String name,int age){returnnewPerson(name, age);}}
Person类跟上面一样,就不多说了。
然后bean.xml文件中配置bean时,需要先配置工厂bean。然后在配置实例bean时,通过
factory-bean
参数指定该工厂bean的引用。
<beanid="susanBeanFactory"class="com.sue.cache.service.test7.SusanBeanFactory"></bean><beanfactory-bean="susanBeanFactory"factory-method="createPerson"><constructor-argindex="0"value="susan"></constructor-arg><constructor-argindex="1"value="18"></constructor-arg></bean>
factorybean
不知道大家有没有发现,上面的实例工厂方法每次都需要创建一个工厂类,不方面统一管理。
这时我们可以使用
FactoryBean
接口。
publicclassUserFactoryBeanimplementsFactoryBean<User>{@OverridepublicUsergetObject()throwsException{returnnewUser();}@OverridepublicClass<?>getObjectType(){returnUser.class;}}
在它的
getObject
方法中可以实现我们自己的逻辑创建对象,并且在
getObjectType
方法中我们可以定义对象的类型。
然后在bean.xml文件中配置bean时,只需像普通的bean一样配置即可。
<beanid="userFactoryBean"class="com.sue.async.service.UserFactoryBean"></bean>
轻松搞定,so easy。
注意:getBean(“userFactoryBean”);获取的是getObject方法中返回的对象。而getBean(“&userFactoryBean”);获取的才是真正的UserFactoryBean对象。
我们通过上面五种方式,在bean.xml文件中把bean配置好之后,spring就会自动扫描和解析相应的标签,并且帮我们创建和实例化bean,然后放入spring容器中。
虽说基于xml文件的方式配置bean,简单而且非常灵活,比较适合一些小项目。但如果遇到比较复杂的项目,则需要配置大量的bean,而且bean之间的关系错综复杂,这样久而久之会导致xml文件迅速膨胀,非常不利于bean的管理。
@Component注解配置bean
为了解决bean太多时,xml文件过大,从而导致膨胀不好维护的问题。在spring2.5中开始支持:
@Component
、
@Repository
、
@Service
、
@Controller
等注解定义bean。
不过,需要特别注意的是,通过这种
@Component
扫描注解的方式定义bean的前提是:需要先配置扫描路径。
在applicationContext.xml文件中使用
<context:component-scan>
标签
<context:component-scanbase-package="com.sue.cache"/>
在springboot的启动类上加上
@ComponentScan
注解
@ComponentScan(basePackages ="com.sue.cache")@SpringBootApplicationpublicclassApplication{publicstaticvoidmain(String[] args){newSpringApplicationBuilder(Application.class).web(WebApplicationType.SERVLET).run(args);}}
当然,如果你需要扫描的类跟springboot的入口类,在同一级或者子级的包下面,无需指定
scanBasePackages
参数,spring默认会从入口类的同一级或者子级的包去找。
直接编码形式
@Component
系列注解虽说使用起来非常方便,但是bean的创建过程完全交给spring容器来完成,我们没办法自己控制。
spring从3.0以后,开始支持JavaConfig的方式定义bean。它可以看做spring的配置文件,但并非真正的配置文件,我们需要通过编码java代码的方式创建bean。例如:
@ConfigurationpublicclassMyConfiguration{@BeanpublicPersonperson(){returnnewPerson();}}
在JavaConfig类上加
@Configuration
注解,相当于配置了
<beans>
标签。而在方法上加
@Bean
注解,相当于配置了
<bean>
标签。
此外,springboot还引入了一些列的
@Conditional
注解,用来控制bean的创建。
@ConfigurationpublicclassMyConfiguration{@ConditionalOnClass(Country.class)@BeanpublicPersonperson(){returnnewPerson();}}
@ConditionalOnClass
注解的功能是当项目中存在Country类时,才实例化Person类。换句话说就是,如果项目中不存在Country类,就不实例化Person类。
这个功能非常有用,相当于一个开关控制着Person类,只有满足一定条件才能实例化。
@Import
通过前面介绍的@Configuration和@Bean相结合的方式,我们可以通过代码定义bean。但这种方式有一定的局限性,它只能创建该类中定义的bean实例,不能创建其他类的bean实例,如果我们想创建其他类的bean实例该怎么办呢?
这时可以使用
@Import
注解导入。
普通类
spring4.2之后
@Import
注解可以实例化普通类的bean实例。例如:
先定义了Role类:
@DatapublicclassRole{privateLong id;privateString name;}
接下来使用@Import注解导入Role类:
@Import(Role.class)@ConfigurationpublicclassMyConfig{}
然后在调用的地方通过
@Autowired
注解注入所需的bean。
@RequestMapping("/")@RestControllerpublicclassTestController{@AutowiredprivateRole role;@GetMapping("/test")publicStringtest(){System.out.println(role);return"test";}}
聪明的你可能会发现,我没有在任何地方定义过Role的bean,但spring却能自动创建该类的bean实例,这是为什么呢?
这也许正是
@Import
注解的强大之处。
此时,有些朋友可能会问:
@Import
注解能定义单个类的bean,但如果有多个类需要定义bean该怎么办呢?
恭喜你,这是个好问题,因为
@Import
注解也支持。
@Import({Role.class,User.class})@ConfigurationpublicclassMyConfig{}
甚至,如果你想偷懒,不想写这种
MyConfig
类,springboot也欢迎。
@Import({Role.class,User.class})@SpringBootApplication(exclude ={DataSourceAutoConfiguration.class,DataSourceTransactionManagerAutoConfiguration.class})publicclassApplication{publicstaticvoidmain(String[] args){newSpringApplicationBuilder(Application.class).web(WebApplicationType.SERVLET).run(args);}}
可以将@Import加到springboot的启动类上。
这样也能生效?
springboot的启动类一般都会加@SpringBootApplication注解,该注解上加了@SpringBootConfiguration注解。
而@SpringBootConfiguration注解,上面又加了@Configuration注解所以,springboot启动类本身带有@Configuration注解的功能。意不意外?惊不惊喜?
Configuration类
上面介绍了@Import注解导入普通类的方法,它同时也支持导入Configuration类。
先定义一个Configuration类:
@ConfigurationpublicclassMyConfig2{@BeanpublicUseruser(){returnnewUser();}@BeanpublicRolerole(){returnnewRole();}}
然后在另外一个Configuration类中引入前面的Configuration类:
@Import({MyConfig2.class})@ConfigurationpublicclassMyConfig{}
这种方式,如果MyConfig2类已经在spring指定的扫描目录或者子目录下,则MyConfig类会显得有点多余。因为MyConfig2类本身就是一个配置类,它里面就能定义bean。
但如果MyConfig2类不在指定的spring扫描目录或者子目录下,则通过MyConfig类的导入功能,也能把MyConfig2类识别成配置类。这就有点厉害了喔。
总结
1 使用 xml 的方式来声明 Bean 的定义,Spring 容器在启动的时候会加载并解析这个 xml,把 bean 装载到 IOC 容器中。
2 使用@CompontScan 注解来扫描声明了@Controller、@Service、@Repository、@Component 注解的类。
3 使用@Configuration 注解声明配置类,并使用@Bean 注解实现 Bean 的定义,这种方式其实是 xml 配置方式的一种演变,是 Spring 迈入到无配置化时代的里程碑。
4 使用@Import 注解,导入配置类或者普通的 Bean使 用 FactoryBean 工 厂 bean , 动 态 构 建 一 个 Bean 实 例 , Spring Cloud
OpenFeign 里面的动态代理实例就是使用 FactoryBean 来实现的。
5 实现 ImportBeanDefinitionRegistrar 接口,可以动态注入 Bean 实例。这个在Spring Boot 里面的启动注解有用到。
6 实现 ImportSelector 接口,动态批量注入配置类或者 Bean 对象,这个在 SpringBoot 里面的自动装配机制里面有用到。
11.有哪些依赖注入的方法?
Spring 支持构造方法注入、属性注入、工厂方法注入,其中工厂方法注入,又可以分为静态工厂方法注入和非静态工厂方法注入。
- 构造方法注入通过调用类的构造方法,将接口实现类通过构造方法变量传入
publicCatDaoImpl(String message){this. message = message;}``````<bean id="CatDaoImpl"class="com.CatDaoImpl"><constructor-arg value=" message "></constructor-arg></bean>
- 属性注入通过 Setter 方法完成调用类所需依赖的注入
publicclassId{privateint id;publicintgetId(){return id;}publicvoidsetId(int id){this.id = id;}}``````<bean id="id"class="com.id "><property name="id" value="123"></property></bean>
- 工厂方法注入- 静态工厂注入静态工厂顾名思义,就是通过调用静态工厂的方法来获取自己需要的对象,为了让 Spring 管理所有对象,我们不能直接通过"工程类.静态方法()"来获取对象,而是依然通过 Spring 注入的形式获取:
publicclassDaoFactory{//静态工厂publicstaticfinalFactoryDaogetStaticFactoryDaoImpl(){returnnewStaticFacotryDaoImpl();}}publicclassSpringAction{//注入对象privateFactoryDao staticFactoryDao;//注入对象的 set 方法publicvoidsetStaticFactoryDao(FactoryDao staticFactoryDao){this.staticFactoryDao = staticFactoryDao;}}``````//factory-method="getStaticFactoryDaoImpl"指定调用哪个工厂方法<bean name="springAction"class=" SpringAction"><!--使用静态工厂的方法注入对象,对应下面的配置文件--><property name="staticFactoryDao" ref="staticFactoryDao"></property></bean><!--此处获取对象的方式是从工厂类中获取静态方法--><bean name="staticFactoryDao"class="DaoFactory" factory-method="getStaticFactoryDaoImpl"></bean>
- 非静态工厂注入非静态工厂,也叫实例工厂,意思是工厂方法不是静态的,所以我们需要首先 new 一个工厂实例,再调用普通的实例方法。//非静态工厂publicclassDaoFactory{publicFactoryDaogetFactoryDaoImpl(){returnnewFactoryDaoImpl();}}publicclassSpringAction{//注入对象privateFactoryDao factoryDao;publicvoidsetFactoryDao(FactoryDao factoryDao){this.factoryDao = factoryDao;}}``````<bean name="springAction"class="SpringAction"><!--使用非静态工厂的方法注入对象,对应下面的配置文件--><property name="factoryDao" ref="factoryDao"></property></bean><!--此处获取对象的方式是从工厂类中获取实例方法--><bean name="daoFactory"class="com.DaoFactory"></bean><bean name="factoryDao" factory-bean="daoFactory" factory-method="getFactoryDaoImpl"></bean>
12.Spring 有哪些自动装配的方式?
什么是自动装配?
Spring IOC 容器知道所有 Bean 的配置信息,此外,通过 Java 反射机制还可以获知实现类的结构信息,如构造方法的结构、属性等信息。掌握所有 Bean 的这些信息后,Spring IOC 容器就可以按照某种规则对容器中的 Bean 进行自动装配,而无须通过显式的方式进行依赖配置。
Spring 提供的这种方式,可以按照某些规则进行 Bean 的自动装配,
<bean>
元素提供了一个指定自动装配类型的属性:
autowire="<自动装配类型>"
Spring 提供了哪几种自动装配类型?
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。
13.Spring 中的 Bean 的作用域有哪些?
Spring 的 Bean 主要支持五种作用域:
- singleton : 在 Spring 容器仅存在一个 Bean 实例,Bean 以单实例的方式存在,是 Bean 默认的作用域。
- prototype : 每次从容器重调用 Bean 时,都会返回一个新的实例。
以下三个作用域于只在 Web 应用中适用:
- request : 每一次 HTTP 请求都会产生一个新的 Bean,该 Bean 仅在当前 HTTP Request 内有效。
- session : 同一个 HTTP Session 共享一个 Bean,不同的 HTTP Session 使用不同的 Bean。
- globalSession:同一个全局 Session 共享一个 Bean,只用于基于 Protlet 的 Web 应用,Spring5 中已经不存在了。
总结
首先呢,Spring 框架里面的 IOC 容器,可以非常方便的去帮助我们管理应用里面的 Bean 对象实例。我们只需要按照 Spring 里面提供的 xml 或者注解等方式去告诉 IOC 容器,哪些Bean 需要被 IOC 容器管理就行了。其次呢,既然是 Bean 对象实例的管理,那意味着这些实例,是存在生命周期,
也就是所谓的作用域。
理论上来说,常规的生命周期只有两种:
singleton,也就是单例,意味着在整个 Spring 容器中只会存在一个 Bean 实例。
prototype,翻译成原型,意味着每次从 IOC 容器去获取指定 Bean 的时候,都会返回一个新的实例对象。
但是在基于 Spring 框架下的 Web 应用里面,增加了一个会话纬度来控制 Bean的生命周期,主要有三个选择
request,针对每一次 http 请求,都会创建一个新的 Bean
session,以 sesssion 会话为纬度,同一个 session 共享同一个 Bean 实例,不同的 session 产生不同的 Bean 实例
globalSession,针对全局 session 纬度,共享同一个 Bean 实例
14.Spring 中的单例 Bean 会存在线程安全问题吗?
首先结论在这:Spring 中的单例 Bean不是线程安全的。
因为单例 Bean,是全局只有一个 Bean,所有线程共享。如果说单例 Bean,是一个无状态的,也就是线程中的操作不会对 Bean 中的成员变量执行查询以外的操作,那么这个单例 Bean 是线程安全的。比如 Spring mvc 的 Controller、Service、Dao 等,这些 Bean 大多是无状态的,只关注于方法本身。
假如这个 Bean 是有状态的,也就是会对 Bean 中的成员变量进行写操作,那么可能就存在线程安全的问题。
单例 Bean 线程安全问题怎么解决呢?
常见的有这么些解决办法:
- 将 Bean 定义为多例这样每一个线程请求过来都会创建一个新的 Bean,但是这样容器就不好管理 Bean,不能这么办。
- 在 Bean 对象中尽量避免定义可变的成员变量削足适履了属于是,也不能这么干。
- 将 Bean 中的成员变量保存在 ThreadLocal 中 ⭐我们知道 ThredLoca 能保证多线程下变量的隔离,可以在类中定义一个 ThreadLocal 成员变量,将需要的可变成员变量保存在 ThreadLocal 里,这是推荐的一种方式。
15.说说循环依赖?
什么是循环依赖?
成品对象:完成实例化和初始化
半成品对象:完成实例化但是没有初始化
Spring 循环依赖:简单说就是自己依赖自己,或者和别的 Bean 相互依赖。
只有单例的 Bean 才存在循环依赖的情况,原型(Prototype)情况下,Spring 会直接抛出异常。原因很简单,AB 循环依赖,A 实例化的时候,发现依赖 B,创建 B 实例,创建 B 的时候发现需要 A,创建 A1 实例……无限套娃,直接把系统干垮。
Spring 可以解决哪些情况的循环依赖?
Spring 不支持基于构造器注入的循环依赖,但是假如 AB 循环依赖,如果一个是构造器注入,一个是 setter 注入呢?
看看几种情形:
第四种可以而第五种不可以的原因是 Spring 在创建 Bean 时默认会根据自然排序进行创建,所以 A 会先于 B 进行创建。
所以简单总结,当循环依赖的实例都采用 setter 方法注入的时候,Spring 可以支持,都采用构造器注入的时候,不支持,构造器注入和 setter 注入同时存在的时候,看天。
/** Cache of singleton objects: bean name to bean instance. */privatefinalMap<String,Object> singletonObjects =newConcurrentHashMap<>(256);/** 三级缓存,用于保存beanName和创建bean的工厂之间关系 */privatefinalMap<String,ObjectFactory<?>> singletonFactories =newHashMap<>(16);/** 二级缓存,保存beanName和bean实例之间关系,与三级缓存不同是,当一个单例bean放到这里之后,
* 那么就可以通过getBean获取到,方便循环依赖检测
*/privatefinalMap<String,Object> earlySingletonObjects =newConcurrentHashMap<>(16);
16.那 Spring 怎么解决循环依赖的呢?
PS:其实正确答案是开发人员做好设计,别让 Bean 循环依赖,但是没办法,面试官不想听这个。
我们都知道,单例 Bean 初始化完成,要经历三步:
注入就发生在第二步,属性赋值,结合这个过程,Spring 通过三级缓存解决了循环依赖:
- 一级缓存 :
Map<String,Object>
singletonObjects,单例池,用于保存实例化、属性赋值(注入)、初始化完成的 bean 实例 - 二级缓存 :
Map<String,Object>
earlySingletonObjects,早期曝光对象,用于保存实例化完成的 bean 实例 - 三级缓存 :
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 注入的循环依赖了,因为实例化和属性赋值是分开的,所以里面有操作的空间。如果都是构造器注入的化,那么都得在实例化这一步完成注入,所以自然是无法支持了。
源码分析
getBean->doGetBean->createBean->doCreateBean->createBeanInstance->populateBean
三个缓存对象,获取数据,按照什么顺序获取
从一级到三级依次获取,当前面缓存中存在需要的对象,那么后面需要将缓存对象清空。
如果只有一级缓存可以解决循环依赖问题吗
不能,如果只有一级缓存,那么成品对象和半成品对象放在一起,是没有办法区分的,所以需要两个缓存分别存放不同状态的对象,一级缓存存放成品对象,二级缓存存放半成品对象。
如果只有二级缓存,是否可以解决循环依赖问题
可以,只不过有限制条件,如果对象创过程不包含aop操作,那么二级缓存可以解决循环依赖问题
17.为什么要三级缓存?⼆级不⾏吗?
不行,主要是为了⽣成代理对象。如果是没有代理的情况下,使用二级缓存解决循环依赖也是 OK 的。但是如果存在代理,三级没有问题,二级就不行了。
因为三级缓存中放的是⽣成具体对象的匿名内部类,获取 Object 的时候,它可以⽣成代理对象,也可以返回普通对象。使⽤三级缓存主要是为了保证不管什么时候使⽤的都是⼀个对象。
假设只有⼆级缓存的情况,往⼆级缓存中放的显示⼀个普通的 Bean 对象,Bean 初始化过程中,通过 BeanPostProcessor 去⽣成代理对象之后,覆盖掉⼆级缓存中的普通 Bean 对象,那么可能就导致取到的 Bean 对象不一致了。
18.@Autowired 的实现原理?
实现@Autowired 的关键是:AutowiredAnnotationBeanPostProcessor
在 Bean 的初始化阶段,会通过 Bean 后置处理器来进行一些前置和后置的处理。
实现@Autowired 的功能,也是通过后置处理器来完成的。这个后置处理器就是 AutowiredAnnotationBeanPostProcessor。
- Spring 在创建 bean 的过程中,最终会调用到 doCreateBean()方法,在 doCreateBean()方法中会调用 populateBean()方法,来为 bean 进行属性填充,完成自动装配等工作。
- 在 populateBean()方法中一共调用了两次后置处理器,第一次是为了判断是否需要属性填充,如果不需要进行属性填充,那么就会直接进行 return,如果需要进行属性填充,那么方法就会继续向下执行,后面会进行第二次后置处理器的调用,这个时候,就会调用到 AutowiredAnnotationBeanPostProcessor 的 postProcessPropertyValues()方法,在该方法中就会进行@Autowired 注解的解析,然后实现自动装配。
/**
* 属性赋值
**/protectedvoidpopulateBean(String beanName,RootBeanDefinition mbd,@NullableBeanWrapper bw){//…………if(hasInstAwareBpps){if(pvs ==null){
pvs = mbd.getPropertyValues();}PropertyValues pvsToUse;for(Iterator var9 =this.getBeanPostProcessorCache().instantiationAware.iterator(); var9.hasNext(); pvs = pvsToUse){InstantiationAwareBeanPostProcessor bp =(InstantiationAwareBeanPostProcessor)var9.next();
pvsToUse = bp.postProcessProperties((PropertyValues)pvs, bw.getWrappedInstance(), beanName);if(pvsToUse ==null){if(filteredPds ==null){
filteredPds =this.filterPropertyDescriptorsForDependencyCheck(bw, mbd.allowCaching);}//执行后处理器,填充属性,完成自动装配//调用InstantiationAwareBeanPostProcessor的postProcessPropertyValues()方法
pvsToUse = bp.postProcessPropertyValues((PropertyValues)pvs, filteredPds, bw.getWrappedInstance(), beanName);if(pvsToUse ==null){return;}}}}//…………}
- postProcessorPropertyValues()方法的源码如下,在该方法中,会先调用 findAutowiringMetadata()方法解析出 bean 中带有@Autowired 注解、@Inject 和@Value 注解的属性和方法。然后调用 metadata.inject()方法,进行属性填充。
publicPropertyValuespostProcessProperties(PropertyValues pvs,Object bean,String beanName){//@Autowired注解、@Inject和@Value注解的属性和方法InjectionMetadata metadata =this.findAutowiringMetadata(beanName, bean.getClass(), pvs);try{//属性填充
metadata.inject(bean, beanName, pvs);return pvs;}catch(BeanCreationException var6){throw var6;}catch(Throwable var7){thrownewBeanCreationException(beanName,"Injection of autowired dependencies failed", var7);}}
19.FactoryBean和BeanFactory区别
首先,Spring 里面的核心功能是 IOC 容器,所谓 IOC 容器呢,本质上就是一个Bean 的容器或者是一个 Bean 的工厂。
它能够根据 xml 里面声明的 Bean 配置进行 bean 的加载和初始化,然后BeanFactory 来生产我们需要的各种各样的 Bean。
所以我对 BeanFactory 的理解了有两个。
BeanFactory 是所有 Spring Bean 容器的顶级接口,它为 Spring 的容器定义了一套规范,并提供像 getBean 这样的方法从容器中获取指定的 Bean 实例。
BeanFactory 在产生 Bean 的同时,还提供了解决 Bean 之间的依赖注入的能力,也就是所谓的 DI。
FactoryBean 是一个工厂 Bean,它是一个接口,主要的功能是动态生成某一个类型的 Bean 的实例,也就是说,我们可以自定义一个 Bean 并且加载到 IOC 容器里面。它里面有一个重要的方法叫 getObject(),这个方法里面就是用来实现动态构建Bean 的过程。
Spring Cloud 里 面 的 OpenFeign 组 件 , 客 户 端 的 代 理 类 , 就 是 使 用 了FactoryBean 来实现的。
20.Spring 中,有两个 id 相同的 bean会报错吗,如果会报错,在哪个阶段报错
首先,在同一个 XML 配置文件里面,不能存在 id 相同的两个 bean,否则 spring容器启动的时候会报错
因为 id 这个属性表示一个 Bean 的唯一标志符号,所以 Spring 在启动的时候会去验证 id 的唯一性,一旦发现重复就会报错,
这个错误发生 Spring 对 XML 文件进行解析转化为 BeanDefinition 的阶段。但是在两个不同的 Spring 配置文件里面,可以存在 id 相同的两个 bean。 IOC容器在加载 Bean 的时候,默认会多个相同 id 的 bean 进行覆盖。
在 Spring3.x 版本以后,这个问题发生了变化
我们知道 Spring3.x 里面提供@Configuration 注解去声明一个配置类,然后使用@Bean 注解实现 Bean 的声明,这种方式完全取代了 XMl。
在这种情况下,如果我们在同一个配置类里面声明多个相同名字的 bean,在Spring IOC 容器中只会注册第一个声明的 Bean 的实例。
后续重复名字的 Bean 就不会再注册了。
@ConfigurationpublicclassSpringConfiguration{@Bean(name ="userService")publicUserService01userService01(){returnnewUserService01();}@Bean(name ="userService")public userService userService02(){returnnewUserService02();}}
像这样一段代码,在 Spring IOC 容器里面,只会保存 UserService01 这个实例,后续相同名字的实例不会再加载。
如果使用@Autowired 注解根据类型实现依赖注入,因为 IOC 容器只有UserService01的实例,所以启动的时候会提示找不到UserService02这个实例。
@AutowiredprivateUserService01 userService01;@AutowiredprivateUserService02 userService02;
如果使用@Resource 注解根据名词实现依赖注入,在 IOC 容器里面得到的实例对象是 UserService01,
于是 Spring 把 UserService01 这个实例赋值给 UserService02,就会提示类型不匹配错误。
@Resource(name="userService")privateUserService01 userService;@Resource(name="userService")privateUserService02 userService02;
这个错误,是在 Spring IOC 容器里面的 Bean 初始化之后的依赖注入阶段发生的。
21.
@Autowired
和@
Resource
区别理解
@Autowired
@Autowired 注解里面有一个 required 属性默认值是 true,表示强制要求 bean实例的注入,
在应用启动的时候,如果 IOC 容器里面不存在对应类型的 Bean,就会报错。
当然,如果不希望自动注入,可以把这个属性设置成 false
@Resource
@Resource 是 JDK 提供的注解,只是 Spring 在实现上提供了这个注解的功能支持。
它的使用方式和@Autowired 完全相同,最大的差异于@Resource 可以支持ByName 和 ByType 两种注入方式。
1、
@Autowired
是根据
type
来匹配,
@Resource
可以根据
name
和
type
来匹配,默认是
name
匹配。
2、
@Autowired
如果需要支持
name
匹配,就需要配合
@Primary
或者
@Qualifier
来实现。
3、一个接口对应多个实现类时候,使用@Autowired会出现错误,
required single bean but found 2 were found
3.1 按照类型的话,控制层注入的时候,直接注入多个实现类的小驼峰形式即可;当然也可以搭配@Qualifier指定名字,@Service声明一下,需要一一对应
3.2 按照名称的话,直接使用
@Resource
案例说明
默认情况下,
@Autowired
是按照
bean
类型注入
业务层接口
publicinterfaceIHumanService{StringisMan();}
业务层实现类
// 男人实现类publicclassManServiceImplextendsBaseServiceimplementsIHumanService{@OverridepublicStringisMan(){return"男人";}}
控制层
@RestControllerpublicclassHumanController{@AutowiredprivateIHumanService humanService;// 问题1:此处humanService可以修改为abc吗// 解答:可以,由于此时IHumanService只有一个实现类,也就是只有类型HumanServiceImpl,声明的属性名称abc无关紧要@RequestMapping(value="/isMan")publicStringisMan(){return humanService.isMan();}}
接口对应多个实现类,按照类型注入,解决问题
required 1 bean, but 2 were bean
publicinterfaceIHumanService{StringisMan();}// 男人实现类@Service("man")publicclassManServiceImplextendsBaseServiceimplementsIHumanService{@OverridepublicStringisMan(){return"男人";}}// 女人实现类@Service("woman")publicclassWomanServiceImplextendsBaseServiceimplementsIHumanService{@OverridepublicStringisMan(){return"女人";}}// 控制层@RestControllerpublicclassHumanController{@Autowired@Qualifier(value="man")privateIHumanService manService;@Autowired@Qualifier(value="woman")privateIHumanService womanService;@RequestMapping(value="/isMan")publicStringisMan(){return manService.isMan();}}
接口对应多个实现类,按照类型注入,解决问题
required 1 bean, but 2 were bean
publicinterfaceIHumanService{StringisMan();}// 男人实现类@ServicepublicclassManServiceImplextendsBaseServiceimplementsIHumanService{@OverridepublicStringisMan(){return"男人";}}// 女人实现类@ServicepublicclassWomanServiceImplextendsBaseServiceimplementsIHumanService{@OverridepublicStringisMan(){return"女人";}}// 控制层@RestControllerpublicclassHumanController{@AutowiredprivateIHumanService manServiceImpl;@AutowiredprivateIHumanService womanServiceImpl;@RequestMapping(value="/isMan")publicStringisMan(){return manServiceImpl.isMan();}}
接口对应多个实现类,按照名称注入,解决问题
required 1 bean, but 2 were bean
publicinterfaceIHumanService{StringisMan();}// 男人实现类@Service("man")publicclassManServiceImplextendsBaseServiceimplementsIHumanService{@OverridepublicStringisMan(){return"男人";}}// 女人实现类@Service("woman")publicclassWomanServiceImplextendsBaseServiceimplementsIHumanService{@OverridepublicStringisMan(){return"女人";}}// 控制层@RestControllerpublicclassHumanController{@Resource(name="man")privateIHumanService manServiceImpl;@Resource(name="woman")privateIHumanService womanServiceImpl;@RequestMapping(value="/isMan")publicStringisMan(){return manServiceImpl.isMan();}}
22.spring中扩展点
扩展接口作用BeanFactoryPostProcessor处理所有bean之前,对bean factory进行预处理BeanDefinitionRegistryPostProcessor可以添加自定义beanBeanPostProcessor支持bean初始化之前,之后对bean进行处理ApplicationContextAware可以获得ApplicationContext及其beanInitializingBeanbean创建完成,所有属性注入完成后执行DisposableBeanbean销毁前执行ApplicationListener用来监听bean声明事件
BeanPostProcessor
提供对bean实例的操作扩展,在spring容器对bean实例化和设置依赖之后,其回调开始执行
接口定义的两个方法,分别在bean的初始化方法(InitializingBean接口,或者init-method定义)执行的前后执行
如果你想在Spring容器完成实例化,配置和初始化bean之后实现一些自定义逻辑,则可以插入一个或多个自定义BeanPostProcessor实现。这些实现成为后置处理器。
BeanPostProcessor接口包含两个回调方法。
当实现此接口类通过容器注册为后处理器时,由Spring容器实例的Bean,Spring容器会在bean 的init方法执行前回调postProcessBeforeInitialization方法,
然后会在bean初始化之后回调postProcessAfterInitialization方法。
后置处理器可以对这些Bean做任何自定义操作。一些Spring Aop 的基础实现类就是通过实现BeanPostProcessor从而提供代理包装逻辑 。
Spring容器能够自动检测任何实现了BeanPostProcessor接口的Bean。容器会自动将这些bean注册成后置处理器以便后续调用。
另外我们可以定义多个BeanPostProcessor,他们执行的顺序可以通过实现PriorityOrdered、Ordered接口来控制。
自定义拦截器
假如有权限认证、日志、统计的场景,可以使用该拦截器
继承 HandlerInterceptorAdapter
packagecom.geekmice.springbootxml.plugin.interceptor;importorg.slf4j.Logger;importorg.slf4j.LoggerFactory;importorg.springframework.web.servlet.handler.HandlerInterceptorAdapter;importjavax.servlet.http.HttpServletRequest;importjavax.servlet.http.HttpServletResponse;publicclassAuthInterceptorextendsHandlerInterceptorAdapter{privateLogger logger =LoggerFactory.getLogger(AuthInterceptor.class);@OverridepublicbooleanpreHandle(HttpServletRequest request,HttpServletResponse response,Object handler)throwsException{String requestURI = request.getRequestURI();
logger.info("{}", request);
logger.info("{}", handler);
logger.info("{}", response);if(checkAuth(requestURI)){
logger.info("校验通过");returntrue;}
logger.info("校验失败");returnfalse;}publicbooleancheckAuth(String requestUrl){if(requestUrl.equals("/get")){
logger.info("权限校验");returntrue;}returnfalse;}}
定义配置类交给spring容器管理
@ConfigurationpublicclassWebAuthConfigextendsWebMvcConfigurerAdapter{@BeanpublicAuthInterceptorgetAuth(){returnnewAuthInterceptor();}@OverridepublicvoidaddInterceptors(InterceptorRegistry registry){
registry.addInterceptor(newAuthInterceptor());}}
获取spring容器对象
全局异常处理
@RestControllerAdvicepublicclassGlobalExceptionHandler{@ExceptionHandler(Exception.class)publicStringhandleException(Exception e){if(e instanceofArithmeticException){return"数据异常";}if(e instanceofException){return"服务器内部异常";}
retur nnull;}}
只需在handleException方法中处理异常情况,业务接口中可以放心使用,不再需要捕获异常
导入相关配置
普通类
publicclassDemo{}@Import(Demo.class)@ConfigurationpublicclassTestConfiguration{}
通过@Import注解引入Demo类,spring就可以自动实例化Demo对象,然后需要的地方通过@autowired注解注入即可
@AutowiredprivateDemo demo;
配置类
这种方式引入比较复杂,因为@Configuration支持多种组合注解,如下操作
publicclassA{}publicclassB{}@Import(B.class)@ConfigurationpublicAConfiguration{@BeanpublicAa(){returnnewA();}}@Import(AConfiguration.class)@ConfigurationpublicclassTestConfiguration{}
通过@Import注解引入@Configuration注解的配置类,会把该配置类相关@Import、@ImportResource、@PropertySource等注解引入的类进行递归,一次性全部引入。
ImportSelector
publicclassAImportSelectorimplementsImportSelector{privatestaticfinalStringCLASS_NAME="com.sue.cache.service.test13.A";publicString[]selectImports(AnnotationMetadata importingClassMetadata){returnnewString[]{CLASS_NAME};}}@Import(AImportSelector.class)@ConfigurationpublicclassTestConfiguration{}
这种方式的好处是selectImports方法返回的是数组,意味着可以同时引入多个类,还是非常方便
ImportBeanDefinitionRegistrar
publicclassAImportBeanDefinitionRegistrarimplementsImportBeanDefinitionRegistrar{@OverridepublicvoidregisterBeanDefinitions(AnnotationMetadata importingClassMetadata,BeanDefinitionRegistry registry){RootBeanDefinition rootBeanDefinition =newRootBeanDefinition(A.class);
registry.registerBeanDefinition("a", rootBeanDefinition);}}@Import(AImportBeanDefinitionRegistrar.class)@ConfigurationpublicclassTestConfiguration{}
这种方式是最灵活的,能在registerBeanDefinitions方法中获取到BeanDefinitionRegistry容器注册对象,可以手动控制BeanDefinition的创建和注册。
项目启动扩展
有时候我们需要在项目启动时定制化一些附加功能,比如:加载一些系统参数、完成初始化、预热本地缓存等,该怎么办呢?
CommandLineRunner 预先数据加载
ApplicationRunner
packagecom.geekmice.springbootxml.plugin.runner;importcom.geekmice.springbootxml.service.LogService;importorg.springframework.beans.factory.annotation.Autowired;importorg.springframework.boot.ApplicationArguments;importorg.springframework.boot.ApplicationRunner;importorg.springframework.stereotype.Component;importjavax.annotation.Priority;@Component@Priority(1)publicclassLogRunnerimplementsApplicationRunner{@AutowiredprivateLogService logService;@Overridepublicvoidrun(ApplicationArguments args)throwsException{
logService.loadData();System.out.println("@Priority(1)");}}
packagecom.geekmice.springbootxml.plugin.runner;importorg.springframework.boot.ApplicationArguments;importorg.springframework.boot.ApplicationRunner;importorg.springframework.stereotype.Component;importjavax.annotation.Priority;@Component@Priority(2)publicclassPriorityRunnerimplementsApplicationRunner{@Overridepublicvoidrun(ApplicationArguments args)throwsException{System.out.println("@Priority(2)");System.out.println(args);}}
2022-11-05 14:53:36.981 INFO 3840 — [ main] c.g.springbootxml.service.LogService : 容器启动前信息
@Priority(1)
@Priority(2)
org.springframework.boot.DefaultApplicationArguments@16a9eb2e
如果项目中有多个类实现了ApplicationRunner接口,他们的执行顺序要怎么指定呢?
答案是使用@Order(n)注解,n的值越小越先执行。当然也可以通过@Priority注解指定顺序。
初始化bean前后
有时,你想在初始化Bean前后,实现一些自己的逻辑。
这时可以实现:BeanPostProcessor接口。
该接口目前有两个方法:
postProcessBeforeInitialization 该在初始化方法之前调用。
postProcessAfterInitialization 该方法再初始化方法之后调用。
@ComponentpublicclassMyBeanPostProcessorimplementsBeanPostProcessor{@OverridepublicObjectpostProcessAfterInitialization(Object bean,String beanName)throwsBeansException{if(bean instanceofUser){((User) bean).setUserName("苏三说技术");}return bean;}}
自定义作用域
我们都知道spring默认支持的Scope只有两种:
singleton 单例,每次从spring容器中获取到的bean都是同一个对象。
prototype 多例,每次从spring容器中获取到的bean都是不同的对象。
spring web又对Scope进行了扩展,增加了:
RequestScope 同一次请求从spring容器中获取到的bean都是同一个对象。
SessionScope 同一个会话从spring容器中获取到的bean都是同一个对象。
即便如此,有些场景还是无法满足我们的要求。
比如,我们想在同一个线程中从spring容器获取到的bean都是同一个对象,该怎么办?
这就需要自定义Scope了。
第一步实现Scope接口:
publicclassThreadLocalScopeimplementsScope{privatestaticfinalThreadLocalTHREAD_LOCAL_SCOPE=newThreadLocal();@OverridepublicObjectget(String name,ObjectFactory<?> objectFactory){Object value =THREAD_LOCAL_SCOPE.get();if(value !=null){return value;}Object object = objectFactory.getObject();THREAD_LOCAL_SCOPE.set(object);return object;}@OverridepublicObjectremove(String name){THREAD_LOCAL_SCOPE.remove();returnnull;}@OverridepublicvoidregisterDestructionCallback(String name,Runnable callback){}@OverridepublicObjectresolveContextualObject(String key){returnnull;}@OverridepublicStringgetConversationId(){returnnull;}}
第二步将新定义的Scope注入到spring容器中:
@ComponentpublicclassThreadLocalBeanFactoryPostProcessorimplementsBeanFactoryPostProcessor{@OverridepublicvoidpostProcessBeanFactory(ConfigurableListableBeanFactory beanFactory)throwsBeansException{
beanFactory.registerScope("threadLocalScope",newThreadLocalScope());}}
第三步使用新定义的Scope:
@Scope("threadLocalScope")@ServicepublicclassCService{publicvoidadd(){}}
AOP
19.说说什么是 AOP?
任何一个系统都是由不同模块组成的,每个模块负责特定功能,当然存在很多模块与业务是无关的,比如日志,权限,事务等核心场景,
这个核心模块经常融入具体业务场景中,如果我们为每个具体业务逻辑操作都添加这样的代码,显然代码冗余太多,因此我们需要将总共的代码逻辑抽象出一个切面,然后注入目标对象(业务对象)中去,aop正是基于这样一个思路实现的,通过动态代理方式,将需要注入切面的对象进行代理,进行调用时候,将公共逻辑添加进去,不需要修改原有的逻辑代码,只需要原有业务场景做一些增强功能即可。
AOP 有哪些核心概念?
AOP全称叫做Aspect Oriented Programming 面向切面编程,它是为解耦而生的,解耦是程序员编码开发过程中一直追求的境界,aop在业务类上的隔离,绝对是做到了解耦
- 切面(Aspect):关注点模块化,这个关注点可能会横切多个对象,事务管理就是有关横切关注点的例子,在spring aop中可以使用@Aspect注解实现
- 连接点(Joinpoint):程序运行过程中某个特定的点,例如某个方法调用的时间点或者处理异常的时间点,spring aop中,一个连接点总是代表一个方法执行
- 切点(Pointcut):对连接点进行拦截的定位,execution表达式针对某个连接点
- 通知(Advice):所谓通知指的就是指拦截到连接点之后要执行的代码,也可以称作增强
- 目标对象 (Target):代理的目标对象
- 织入(Weabing):织入是将增强添加到目标类的具体连接点上的过程。- 编译期织入:切面在目标类编译时被织入- 类加载期织入:切面在目标类加载到 JVM 时被织入。需要特殊的类加载器,它可以在目标类被引入应用之前增强该目标类的字节码。- 运行期织入:切面在应用运行的某个时刻被织入。一般情况下,在织入切面时,AOP 容器会为目标对象动态地创建一个代理对象。SpringAOP 就是以这种方式织入切面。Spring 采用运行期织入,而 AspectJ 采用编译期织入和类加载器织入。
- 引介(introduction):引介是一种特殊的增强,可以动态地为类添加一些属性和方法
AOP 有哪些环绕方式?
AOP 一般有 5 种环绕方式:
- 前置通知 (@Before)
- 返回通知 (@AfterReturning)
- 异常通知 (@AfterThrowing)
- 后置通知 (@After)
- 环绕通知 (@Around)
多个切面的情况下,可以通过 @Order 指定先后顺序,数字越小,优先级越高。
总结
声明式事务处理流程了解一下
20.说说你平时有用到 AOP 吗?
PS:这道题老三的同事面试候选人的时候问到了,候选人说了一堆 AOP 原理,同事就势来一句,你能现场写一下 AOP 的应用吗?结果——场面一度很尴尬。虽然我对面试写这种百度就能出来的东西持保留意见,但是还是加上了这一问,毕竟招人最后都是要撸代码的。
这里给出一个小例子,SpringBoot 项目中,利用 AOP 打印接口的入参和出参日志,以及执行时间,还是比较快捷的。
引入依赖:引入 AOP 依赖
<dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-aop</artifactId></dependency>
自定义注解:自定义一个注解作为切点
@Retention(RetentionPolicy.RUNTIME)@Target({ElementType.METHOD})@Documentedpublic@interfaceWebLog{}
配置 AOP 切面:- @Aspect:标识切面- @Pointcut:设置切点,这里以自定义注解为切点,定义切点有很多其它种方式,自定义注解是比较常用的一种。- @Before:在切点之前织入,打印了一些入参信息- @Around:环绕切点,打印返回参数和接口执行时间
@Aspect@ComponentpublicclassWebLogAspect{privatefinalstaticLogger logger =LoggerFactory.getLogger(WebLogAspect.class);/** * 以自定义 @WebLog 注解为切点 **/@Pointcut("@annotation(cn.fighter3.spring.aop_demo.WebLog)")publicvoidwebLog(){}/** * 在切点之前织入 */@Before("webLog()")publicvoiddoBefore(JoinPoint joinPoint)throwsThrowable{// 开始打印请求日志ServletRequestAttributes attributes =(ServletRequestAttributes)RequestContextHolder.getRequestAttributes();HttpServletRequest request = attributes.getRequest();// 打印请求相关参数 logger.info("========================================== Start ==========================================");// 打印请求 url logger.info("URL : {}", request.getRequestURL().toString());// 打印 Http method logger.info("HTTP Method : {}", request.getMethod());// 打印调用 controller 的全路径以及执行方法 logger.info("Class Method : {}.{}", joinPoint.getSignature().getDeclaringTypeName(), joinPoint.getSignature().getName());// 打印请求的 IP logger.info("IP : {}", request.getRemoteAddr());// 打印请求入参 logger.info("Request Args : {}",newObjectMapper().writeValueAsString(joinPoint.getArgs()));}/** * 在切点之后织入 * @throws Throwable */@After("webLog()")publicvoiddoAfter()throwsThrowable{// 结束后打个分隔线,方便查看 logger.info("=========================================== End ===========================================");}/** * 环绕 */@Around("webLog()")publicObjectdoAround(ProceedingJoinPoint proceedingJoinPoint)throwsThrowable{//开始时间long startTime =System.currentTimeMillis();Object result = proceedingJoinPoint.proceed();// 打印出参 logger.info("Response Args : {}",newObjectMapper().writeValueAsString(result));// 执行耗时 logger.info("Time-Consuming : {} ms",System.currentTimeMillis()- startTime);return result;}}
使用:只需要在接口上加上自定义注解
@GetMapping("/hello")@WebLog(desc ="这是一个欢迎接口")publicStringhello(String name){return"Hello "+name;}
执行结果:可以看到日志打印了入参、出参和执行时间
21.说说 JDK 动态代理和 CGLIB 代理 ?
Spring 的 AOP 是通过动态代理来实现的,动态代理主要有两种方式 JDK 动态代理和 Cglib 动态代理,这两种动态代理的使用和原理有些不同。
JDK 动态代理
- Interface:对于 JDK 动态代理,目标类需要实现一个 Interface。
- InvocationHandler:InvocationHandler 是一个接口,可以通过实现这个接口,定义横切逻辑,再通过反射机制(invoke)调用目标类的代码,在次过程,可能包装逻辑,对目标方法进行前置后置处理。
- Proxy:Proxy 利用 InvocationHandler 动态创建一个符合目标类实现的接口的实例,生成目标类的代理对象。
CgLib 动态代理
- 使用 JDK 创建代理有一大限制,它只能为接口创建代理实例,而 CgLib 动态代理就没有这个限制。
- CgLib 动态代理是使用字节码处理框架 ASM,其原理是通过字节码技术为一个类创建子类,并在子类中采用方法拦截的技术拦截所有父类方法的调用,顺势织入横切逻辑。
- CgLib 创建的动态代理对象性能比 JDK 创建的动态代理对象的性能高不少,但是 CGLib 在创建代理对象时所花费的时间却比 JDK 多得多,所以对于单例的对象,因为无需频繁创建对象,用 CGLib 合适,反之,使用 JDK 方式要更为合适一些。同时,由于 CGLib 由于是采用动态创建子类的方法,对于 final 方法,无法进行代理。
我们来看一个常见的小场景,客服中转,解决用户问题:
JDK 动态代理实现:
- 接口
publicinterfaceISolver{voidsolve();}
- 目标类:需要实现对应接口
publicclassSolverimplementsISolver{@Overridepublicvoidsolve(){System.out.println("疯狂掉头发解决问题……");}}
- 态代理工厂:ProxyFactory,直接用反射方式生成一个目标对象的代理对象,这里用了一个匿名内部类方式重写 InvocationHandler 方法,实现接口重写也差不多
publicclassProxyFactory{// 维护一个目标对象privateObject target;publicProxyFactory(Object target){this.target = target;}// 为目标对象生成代理对象publicObjectgetProxyInstance(){returnProxy.newProxyInstance(target.getClass().getClassLoader(), target.getClass().getInterfaces(),newInvocationHandler(){@OverridepublicObjectinvoke(Object proxy,Method method,Object[] args)throwsThrowable{System.out.println("请问有什么可以帮到您?");// 调用目标对象方法Object returnValue = method.invoke(target, args);System.out.println("问题已经解决啦!");returnnull;}});}}
- 客户端:Client,生成一个代理对象实例,通过代理对象调用目标对象方法
publicclassClient{publicstaticvoidmain(String[] args){//目标对象:程序员ISolver developer =newSolver();//代理:客服小姐姐ISolver csProxy =(ISolver)newProxyFactory(developer).getProxyInstance();//目标方法:解决问题 csProxy.solve();}}
Cglib 动态代理实现:
- 目标类:Solver,这里目标类不用再实现接口。
publicclassSolver{publicvoidsolve(){System.out.println("疯狂掉头发解决问题……");}}
- 动态代理工厂:
publicclassProxyFactoryimplementsMethodInterceptor{//维护一个目标对象privateObject target;publicProxyFactory(Object target){this.target = target;}//为目标对象生成代理对象publicObjectgetProxyInstance(){//工具类Enhancer en =newEnhancer();//设置父类 en.setSuperclass(target.getClass());//设置回调函数 en.setCallback(this);//创建子类对象代理return en.create();}@OverridepublicObjectintercept(Object obj,Method method,Object[] args,MethodProxy proxy)throwsThrowable{System.out.println("请问有什么可以帮到您?");// 执行目标对象的方法Object returnValue = method.invoke(target, args);System.out.println("问题已经解决啦!");returnnull;}}
- 客户端:Client
publicclassClient{publicstaticvoidmain(String[] args){//目标对象:程序员Solver developer =newSolver();//代理:客服小姐姐Solver csProxy =(Solver)newProxyFactory(developer).getProxyInstance();//目标方法:解决问题 csProxy.solve();}}
22.说说 Spring AOP 和 AspectJ AOP 区别?
Spring AOP
Spring AOP 属于
运行时增强
,主要具有如下特点:
- 基于动态代理来实现,默认如果使用接口的,用 JDK 提供的动态代理实现,如果是方法则使用 CGLIB 实现
- Spring AOP 需要依赖 IOC 容器来管理,并且只能作用于 Spring 容器,使用纯 Java 代码实现
- 在性能上,由于 Spring AOP 是基于动态代理来实现的,在容器启动时需要生成代理实例,在方法调用上也会增加栈的深度,使得 Spring AOP 的性能不如 AspectJ 的那么好。
- Spring AOP 致力于解决企业级开发中最普遍的 AOP(方法织入)。
AspectJ
AspectJ 是一个易用的功能强大的 AOP 框架,属于
编译时增强
, 可以单独使用,也可以整合到其它框架中,是 AOP 编程的完全解决方案。AspectJ 需要用到单独的编译器 ajc。
AspectJ 属于静态织入,通过修改代码来实现,在实际运行之前就完成了织入,所以说它生成的类是没有额外运行时开销的,一般有如下几个织入的时机:
- 编译期织入(Compile-time weaving):如类 A 使用 AspectJ 添加了一个属性,类 B 引用了它,这个场景就需要编译期的时候就进行织入,否则没法编译类 B。
- 编译后织入(Post-compile weaving):也就是已经生成了 .class 文件,或已经打成 jar 包了,这种情况我们需要增强处理的话,就要用到编译后织入。
- 类加载后织入(Load-time weaving):指的是在加载类的时候进行织入,要实现这个时期的织入,有几种常见的方法
整体对比如下:
事务
Spring 事务的本质其实就是数据库对事务的支持,没有数据库的事务支持,Spring 是无法提供事务功能的。Spring 只提供统一事务管理接口,具体实现都是由各数据库自己实现,数据库事务的提交和回滚是通过数据库自己的事务机制实现。
23.Spring 事务的种类?
Spring 支持
编程式事务
管理和
声明式
事务管理两种方式:
- 编程式事务
编程式事务管理使用 TransactionTemplate,需要显式执行事务。
- 声明式事务
- 声明式事务管理建立在 AOP 之上的。其本质是通过 AOP 功能,对方法前后进行拦截,将事务处理的功能编织到拦截的方法中,也就是在目标方法开始之前启动一个事务,在执行完目标方法之后根据执行情况提交或者回滚事务
- 优点是不需要在业务逻辑代码中掺杂事务管理的代码,只需在配置文件中做相关的事务规则声明或通过 @Transactional 注解的方式,便可以将事务规则应用到业务逻辑中,减少业务代码的污染。唯一不足地方是,最细粒度只能作用到方法级别,无法做到像编程式事务那样可以作用到代码块级别。
24.Spring 的事务隔离级别?
Spring 的接口 TransactionDefinition 中定义了表示隔离级别的常量,当然其实主要还是对应数据库的事务隔离级别:
- ISOLATION_DEFAULT:使用后端数据库默认的隔离界别,MySQL 默认可重复读,Oracle 默认读已提交。
- ISOLATION_READ_UNCOMMITTED:读未提交
- ISOLATION_READ_COMMITTED:读已提交
- ISOLATION_REPEATABLE_READ:可重复读
- ISOLATION_SERIALIZABLE:串行化
25.Spring 的事务传播机制?
Spring 事务的传播机制说的是,当多个事务同时存在的时候——一般指的是多个事务方法相互调用时,Spring 如何处理这些事务的行为。
事务传播机制是使用简单的 ThreadLocal 实现的,所以,如果调用的方法是在新线程调用的,事务传播实际上是会失效的。
Spring 默认的事务传播行为是 PROPAFATION_REQUIRED,它适合绝大多数情况,如果多个 ServiceX#methodX()都工作在事务环境下(均被 Spring 事务增强),且程序中存在调用链
Service1#method1()->Service2#method2()->Service3#method3()
,那么这 3 个服务类的三个方法通过 Spring 的事务传播机制都工作在同一个事务中。
对于这个问题,所谓的事务传播行为,就是说多个声明的事务的方法,相互调用时候,这个事务应该如何传递,比如说methodA方法调用methodB方法,那么这两个方法,显示开启事务,methodB是开启了新的事务,还是继续在methodA方法这个事务里面执行 就取决于事务传播的一个行为
26.声明式事务实现原理了解吗?
就是通过 AOP/动态代理。
- 在 Bean 初始化阶段创建代理对象:Spring 容器在初始化每个单例 bean 的时候,会遍历容器中的所有 BeanPostProcessor 实现类,并执行其 postProcessAfterInitialization 方法,在执行 AbstractAutoProxyCreator 类的 postProcessAfterInitialization 方法时会遍历容器中所有的切面,查找与当前实例化 bean 匹配的切面,这里会获取事务属性切面,查找@Transactional 注解及其属性值,然后根据得到的切面创建一个代理对象,默认是使用 JDK 动态代理创建代理,如果目标类是接口,则使用 JDK 动态代理,否则使用 Cglib。
- 在执行目标方法时进行事务增强操作:当通过代理对象调用 Bean 方法的时候,会触发对应的 AOP 增强拦截器,声明式事务是一种环绕增强,对应接口为
MethodInterceptor
,事务增强对该接口的实现为TransactionInterceptor
,类图如下:
事务拦截器
TransactionInterceptor
在
invoke
方法中,通过调用父类
TransactionAspectSupport
的
invokeWithinTransaction
方法进行事务处理,包括开启事务、事务提交、异常回滚。
27.声明式事务在哪些情况下会失效?
1、@Transactional 应用在非 public 修饰的方法上
如果 Transactional 注解应用在非 public 修饰的方法上,Transactional 将会失效。
是因为在 Spring AOP 代理时,TransactionInterceptor (事务拦截器)在目标方法执行前后进行拦截,DynamicAdvisedInterceptor(CglibAopProxy 的内部类)的 intercept 方法 或 JdkDynamicAopProxy 的 invoke 方法会间接调用 AbstractFallbackTransactionAttributeSource 的 computeTransactionAttribute方法,获取 Transactional 注解的事务配置信息。
protectedTransactionAttributecomputeTransactionAttribute(Method method,Class<?> targetClass){// Don't allow no-public methods as required.if(allowPublicMethodsOnly()&&!Modifier.isPublic(method.getModifiers())){returnnull;}
此方法会检查目标方法的修饰符是否为 public,不是 public 则不会获取@Transactional 的属性配置信息。
2、@Transactional 注解属性 propagation 设置错误
- TransactionDefinition.PROPAGATION_SUPPORTS:如果当前存在事务,则加入该事务;如果当前没有事务,则以非事务的方式继续运行。
- TransactionDefinition.PROPAGATION_NOT_SUPPORTED:以非事务方式运行,如果当前存在事务,则把当前事务挂起。
- TransactionDefinition.PROPAGATION_NEVER:以非事务方式运行,如果当前存在事务,则抛出异常。
3、@Transactional 注解属性 rollbackFor 设置错误
rollbackFor 可以指定能够触发事务回滚的异常类型。Spring 默认抛出了未检查 unchecked 异常(继承自 RuntimeException 的异常)或者 Error 才回滚事务,其他异常不会触发回滚事务。
// 希望自定义的异常可以进行回滚@Transactional(propagation=Propagation.REQUIRED,rollbackFor=MyException.class
若在目标方法中抛出的异常是 rollbackFor 指定的异常的子类,事务同样会回滚。
4、同一个类中方法调用,导致@Transactional 失效
开发中避免不了会对同一个类里面的方法调用,比如有一个类 Test,它的一个方法 A,A 再调用本类的方法 B(不论方法 B 是用 public 还是 private 修饰),但方法 A 没有声明注解事务,而 B 方法有。则外部调用方法 A 之后,方法 B 的事务是不会起作用的。这也是经常犯错误的一个地方。
那为啥会出现这种情况?其实这还是由于使用 Spring AOP 代理造成的,因为只有当事务方法被当前类以外的代码调用时,才会由 Spring 生成的代理对象来管理。
//@Transactional@GetMapping("/test")privateIntegerA()throwsException{CityInfoDict cityInfoDict =newCityInfoDict();
cityInfoDict.setCityName("2");/**
* B 插入字段为 3的数据
*/this.insertB();/**
* A 插入字段为 2的数据
*/int insert = cityInfoDictMapper.insert(cityInfoDict);return insert;}@Transactional()publicIntegerinsertB()throwsException{CityInfoDict cityInfoDict =newCityInfoDict();
cityInfoDict.setCityName("3");
cityInfoDict.setParentCityId(3);return cityInfoDictMapper.insert(cityInfoDict);}
这种情况是最常见的一种@Transactional 注解失效场景
@TransactionalprivateIntegerA()throwsException{int insert =0;try{CityInfoDict cityInfoDict =newCityInfoDict();
cityInfoDict.setCityName("2");
cityInfoDict.setParentCityId(2);/**
* A 插入字段为 2的数据
*/
insert = cityInfoDictMapper.insert(cityInfoDict);/**
* B 插入字段为 3的数据
*/
b.insertB();}catch(Exception e){
e.printStackTrace();}}
如果 B 方法内部抛了异常,而 A 方法此时 try catch 了 B 方法的异常,那这个事务就不能正常回滚了,会抛出异常:
org.springframework.transaction.UnexpectedRollbackException:Transaction rolled back because it has been marked as rollback-only
Spring 里面的事务和分布式事务的使用如何区分,以及这两个事务之间有什么关联?
1 首先,在 Spring 里面并没有提供事务,它只是提供了对数据库事务管理的封装。通过声明式的事务配置,使得开发人员可以从一些复杂的事务处理中得到解脱,我们不再需要关心连接的获取、连接的关闭、事务提交、事务回滚这些操作。更加聚焦在业务开发层面。
所以,Spring 里面的事务,本质上就是数据库层面的事务,这种事务的管理,主要是针对单个数据库里面多个数据表操作的,去满足事务的 ACID 特性。
2 分布式事务,是解决多个数据库的事务操作的数据一致性问题,传统的关系型数据库不支持跨库事务的操作,所以需要引入分布式事务的解决方案
而 Spring 并没有提供分布式事务场景的支持,所以 Spring 事务和分布式事务在使用上并没有直接的关联性。
但是我们可以使用一些主流的事务解决框架,比如 Seata,集成到 Spring 生态里面,去解决分布式事务的问题。
MVC
28.Spring MVC 的核心组件?
- DispatcherServlet:前置控制器,是整个流程控制的核心,控制其他组件的执行,进行统一调度,降低组件之间的耦合性,相当于总指挥。
- Handler:处理器,完成具体的业务逻辑,相当于 Servlet 或 Action。
- HandlerMapping:DispatcherServlet 接收到请求之后,通过 HandlerMapping 将不同的请求映射到不同的 Handler。
- HandlerInterceptor:处理器拦截器,是一个接口,如果需要完成一些拦截处理,可以实现该接口。
- HandlerExecutionChain:处理器执行链,包括两部分内容:Handler 和 HandlerInterceptor(系统会有一个默认的 HandlerInterceptor,如果需要额外设置拦截,可以添加拦截器)。
- HandlerAdapter:处理器适配器,Handler 执行业务方法之前,需要进行一系列的操作,包括表单数据的验证、数据类型的转换、将表单数据封装到 JavaBean 等,这些操作都是由 HandlerApater 来完成,开发者只需将注意力集中业务逻辑的处理上,DispatcherServlet 通过 HandlerAdapter 执行不同的 Handler。
- ModelAndView:装载了模型数据和视图信息,作为 Handler 的处理结果,返回给 DispatcherServlet。
- ViewResolver:视图解析器,DispatcheServlet 通过它将逻辑视图解析为物理视图,最终将渲染结果响应给客户端。
29.Spring MVC 的工作流程?
- 客户端向服务端发送一次请求,这个请求会先到前端控制器 DispatcherServlet(也叫中央控制器)。
- DispatcherServlet 接收到请求后会调用 HandlerMapping 处理器映射器。由此得知,该请求该由哪个 Controller 来处理(并未调用 Controller,只是得知)
- DispatcherServlet 调用 HandlerAdapter 处理器适配器,告诉处理器适配器应该要去执行哪个 Controller
- HandlerAdapter 处理器适配器去执行 Controller 并得到 ModelAndView(数据和视图),并层层返回给 DispatcherServlet
- DispatcherServlet 将 ModelAndView 交给 ViewReslover 视图解析器解析,然后返回真正的视图。
- DispatcherServlet 将模型数据填充到视图中
- DispatcherServlet 将结果响应给客户端
Spring MVC 虽然整体流程复杂,但是实际开发中很简单,大部分的组件不需要开发人员创建和管理,只需要通过配置文件的方式完成配置即可,真正需要开发人员进行处理的只有 Handler(Controller) 、View 、Model。
当然我们现在大部分的开发都是前后端分离,Restful 风格接口,后端只需要返回 Json 数据就行了。
30.SpringMVC Restful 风格的接口的流程是什么样的呢?
PS:这是一道全新的八股,毕竟 ModelAndView 这种方式应该没人用了吧?现在都是前后端分离接口,八股也该更新换代了。
我们都知道 Restful 接口,响应格式是 json,这就用到了一个常用注解:**@ResponseBody**
@GetMapping("/user")@ResponseBodypublicUseruser(){returnnewUser(1,"张三");}
加入了这个注解后,整体的流程上和使用 ModelAndView 大体上相同,但是细节上有一些不同:
- 客户端向服务端发送一次请求,这个请求会先到前端控制器 DispatcherServlet
- DispatcherServlet 接收到请求后会调用 HandlerMapping 处理器映射器。由此得知,该请求该由哪个 Controller 来处理
- DispatcherServlet 调用 HandlerAdapter 处理器适配器,告诉处理器适配器应该要去执行哪个 Controller
- Controller 被封装成了 ServletInvocableHandlerMethod,HandlerAdapter 处理器适配器去执行 invokeAndHandle 方法,完成对 Controller 的请求处理
- HandlerAdapter 执行完对 Controller 的请求,会调用 HandlerMethodReturnValueHandler 去处理返回值,主要的过程:5.1. 调用 RequestResponseBodyMethodProcessor,创建 ServletServerHttpResponse(Spring 对原生 ServerHttpResponse 的封装)实例5.2.使用 HttpMessageConverter 的 write 方法,将返回值写入 ServletServerHttpResponse 的 OutputStream 输出流中5.3.在写入的过程中,会使用 JsonGenerator(默认使用 Jackson 框架)对返回值进行 Json 序列化
- 执行完请求后,返回的 ModealAndView 为 null,ServletServerHttpResponse 里也已经写入了响应,所以不用关心 View 的处理
Spring Boot
31.介绍一下 SpringBoot,有哪些优点?
Spring Boot 基于 Spring 开发,Spirng Boot 本身并不提供 Spring 框架的核心特性以及扩展功能,只是用于快速、敏捷地开发新一代基于 Spring 框架的应用程序。它并不是用来替代 Spring 的解决方案,而是和 Spring 框架紧密结合用于提升 Spring 开发者体验的工具。
Spring Boot 以
约定大于配置
核心思想开展工作,相比 Spring 具有如下优势:
- Spring Boot 可以快速创建独立的 Spring 应用程序。
- Spring Boot 内嵌了如 Tomcat,Jetty 和 Undertow 这样的容器,也就是说可以直接跑起来,用不着再做部署工作了。
- Spring Boot 无需再像 Spring 一样使用一堆繁琐的 xml 文件配置。
- Spring Boot 可以自动配置(核心)Spring。SpringBoot 将原有的 XML 配置改为 Java 配置,将 bean 注入改为使用注解注入的方式(@Autowire),并将多个 xml、properties 配置浓缩在一个 appliaction.yml 配置文件中。
- Spring Boot 提供了一些现有的功能,如量度工具,表单数据验证以及一些外部配置这样的一些第三方功能。
- Spring Boot 可以快速整合常用依赖(开发库,例如 spring-webmvc、jackson-json、validation-api 和 tomcat 等),提供的 POM 可以简化 Maven 的配置。当我们引入核心依赖时,SpringBoot 会自引入其他依赖。
32.SpringBoot 自动配置原理了解吗?
SpringBoot 开启自动配置的注解是
@EnableAutoConfiguration
,启动类上的注解
@SpringBootApplication
是一个复合注解,包含了@EnableAutoConfiguration:
EnableAutoConfiguration
只是一个简单的注解,自动装配核心功能的实现实际是通过AutoConfigurationImportSelector
类@AutoConfigurationPackage//将main同级的包下的所有组件注册到容器中@Import({AutoConfigurationImportSelector.class})//加载自动装配类 xxxAutoconfigurationpublic@interfaceEnableAutoConfiguration{StringENABLED_OVERRIDE_PROPERTY="spring.boot.enableautoconfiguration";Class<?>[]exclude()default{};String[]excludeName()default{};}
AutoConfigurationImportSelector
实现了ImportSelector
接口,这个接口的作用就是收集需要导入的配置类,配合@Import()
就可以将相应的类导入到 Spring 容器中- 获取注入类的方法是 selectImports(),它实际调用的是
getAutoConfigurationEntry
,这个方法是获取自动装配类的关键,主要流程可以分为这么几步:1. 获取注解的属性,用于后面的排除2. 获取所有需要自动装配的配置类的路径:这一步是最关键的,从 META-INF/spring.factories 获取自动配置类的路径3. 去掉重复的配置类和需要排除的重复类,把需要自动加载的配置类的路径存储起来
protectedAutoConfigurationImportSelector.AutoConfigurationEntrygetAutoConfigurationEntry(AnnotationMetadata annotationMetadata){if(!this.isEnabled(annotationMetadata)){returnEMPTY_ENTRY;}else{//1.获取到注解的属性AnnotationAttributes attributes =this.getAttributes(annotationMetadata);//2.获取需要自动装配的所有配置类,读取META-INF/spring.factories,获取自动配置类路径List<String> configurations =this.getCandidateConfigurations(annotationMetadata, attributes);//3.1.移除重复的配置
configurations =this.removeDuplicates(configurations);//3.2.处理需要排除的配置Set<String> exclusions =this.getExclusions(annotationMetadata, attributes);this.checkExcludedClasses(configurations, exclusions);
configurations.removeAll(exclusions);
configurations =this.getConfigurationClassFilter().filter(configurations);this.fireAutoConfigurationImportEvents(configurations, exclusions);returnnewAutoConfigurationImportSelector.AutoConfigurationEntry(configurations, exclusions);}}
总结
自动装配,简单来说就是自动把第三方组件的 Bean 装载到 Spring IOC 器里面,不需要开发人员再去写 Bean 的装配配置。
在 Spring Boot 应用里面,只需要在启动类加上@SpringBootApplication 注解就可以实现自动装配。
@SpringBootApplication 是 一 个 复 合 注 解 , 真 正 实 现 自 动 装 配 的 注 解 是@EnableAutoConfiguration。
自动装配的实现主要依靠三个核心关键技术。引入 Starter 启动依赖组件的时候,这个组件里面必须要包含@Configuration 配
置类,在这个配置类里面通过@Bean 注解声明需要装配到 IOC 容器的 Bean 对象。这个配置类是放在第三方的 jar 包里面,然后通过 SpringBoot 中的约定优于配置思想,把这个配置类的全路径放在 classpath:/META-INF/spring.factories 文件中。
这样 SpringBoot 就可以知道第三方 jar 包里面的配置类的位置,这个步骤主要是用到了 Spring 里面的 SpringFactoriesLoader 来完成的。
SpringBoot 拿到所第三方 jar 包里面声明的配置类以后,再通过 Spring 提供的ImportSelector 接口,实现对这些配置类的动态加载。
在我看来,SpringBoot 是约定优于配置这一理念下的产物,所以在很多的地方,都会看到这类的思想。它的出现,让开发人员更加聚焦在了业务代码的编写上,而不需要去关心和业务无关的配置。
其实,自动装配的思想,在 SpringFramework3.x 版本里面的@Enable 注解,就有了实现的雏形。@Enable 注解是模块驱动的意思,我们只需要增加某个@Enable 注解,就自动打开某个功能,而不需要针对这个功能去做 Bean 的配置,@Enable 底层也是帮我们去自动完成这个模块相关 Bean 的注入。
33.如何自定义一个 SpringBoot Srarter,如何理解
理解
Starter 是 Spring Boot 的四大核心功能特性之一,除此之外,Spring Boot 还有自动装配、Actuator 监控等特性。
Spring Boot 里面的这些特性,都是为了让开发者在开发基于 Spring 生态下的企业级应用时,只需要关心业务逻辑,
减少对配置和外部环境的依赖。
其中,Starter 是启动依赖,它的主要作用有几个。
1 Starter 组件以功能为纬度,来维护对应的 jar 包的版本依赖,使得开发者可以不需要去关心这些版本冲突这种容易出错的细节。
2 Starter 组件会把对应功能的所有 jar 包依赖全部导入进来,避免了开发者自己去引入依赖带来的麻烦。
3 Starter 内部集成了自动装配的机制,也就说在程序中依赖对应的 starter 组件以后,这个组件自动会集成到 Spring 生态下,并且对于相关 Bean 的管理,也是基于自动装配机制来完成。
4 依赖 Starter 组件后,这个组件对应的功能所需要维护的外部化配置,会自动集成到 Spring Boot 里面,
我们只需要在 application.properties 文件里面进行维护就行了,比如 Redis 这个starter,只需要在 application.properties
文件里面添加 redis 的连接信息就可以直接使用了。
在我看来,Starter组件几乎完美的体现了Spring Boot里面约定优于配置的理念。另外,Spring Boot 官方提供了很多的 Starter 组件,比如 Redis、JPA、MongoDB等等。
但是官方并不一定维护了所有中间件的 Starter,所以对于不存在的 Starter,第三方组件一般会自己去维护一个。
官方的 starter 和第三方的 starter 组件,最大的区别在于命名上。官方维护的 starter 的以 spring-boot-starter 开头的前缀。
第三方维护的 starter 是以 spring-boot-starter 结尾的后缀这也是一种约定优于配置的体现
自定义使用
知道了自动配置原理,创建一个自定义 SpringBoot Starter 也很简单。
- 创建一个项目,命名为 demo-spring-boot-starter,引入 SpringBoot 相关依赖
<dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter</artifactId></dependency><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-configuration-processor</artifactId><optional>true</optional></dependency>
- 编写配置文件这里定义了属性配置的前缀
@ConfigurationProperties(prefix ="hello")publicclassHelloProperties{privateString name;//省略getter、setter}
- 自动装配创建自动配置类 HelloPropertiesConfigure
@Configuration@EnableConfigurationProperties(HelloProperties.class)publicclassHelloPropertiesConfigure{}
- 配置自动类在
/resources/META-INF/spring.factories
文件中添加自动配置类路径org.springframework.boot.autoconfigure.EnableAutoConfiguration=\ cn.fighter3.demo.starter.configure.HelloPropertiesConfigure
- 测试- 创建一个工程,引入自定义 starter 依赖
<dependency><groupId>cn.fighter3</groupId><artifactId>demo-spring-boot-starter</artifactId><version>0.0.1-SNAPSHOT</version></dependency>
- 在配置文件里添加配置hello.name=张三
- 测试类@RunWith(SpringRunner.class)@SpringBootTestpublicclassHelloTest{@AutowiredHelloProperties helloProperties;@Testpublicvoidhello(){System.out.println("你好,"+helloProperties.getName());}}
- 运行结果
至此,随手写的一个自定义 SpringBoot-Starter 就完成了,虽然比较简单,但是完成了主要的自动装配的能力。
34.Springboot 启动原理?
SpringApplication 这个类主要做了以下四件事情:
- 推断应用的类型是普通的项目还是 Web 项目
- 查找并加载所有可用初始化器 , 设置到 initializers 属性中
- 找出所有的应用程序监听器,设置到 listeners 属性中
- 推断并设置 main 方法的定义类,找到运行的主类
SpringBoot 启动大致流程如下 :
Spring Cloud
35.对 SpringCloud 了解多少?
SpringCloud 是 Spring 官方推出的微服务治理框架。
什么是微服务?
- 2014 年 Martin Fowler 提出的一种新的架构形式。微服务架构是一种架构模式,提倡将单一应用程序划分成一组小的服务,服务之间相互协调,互相配合,为用户提供最终价值。每个服务运行在其独立的进程中,服务与服务之间采用轻量级的通信机制(如 HTTP 或 Dubbo)互相协作,每个服务都围绕着具体的业务进行构建,并且能够被独立的部署到生产环境中,另外,应尽量避免统一的,集中式的服务管理机制,对具体的一个服务而言,应根据业务上下文,选择合适的语言、工具(如 Maven)对其进行构建。
- 微服务化的核心就是将传统的一站式应用,根据业务拆分成一个一个的服务,彻底地去耦合,每一个微服务提供单个业务功能的服务,一个服务做一件事情,从技术角度看就是一种小而独立的处理过程,类似进程的概念,能够自行单独启动或销毁,拥有自己独立的数据库。
微服务架构主要要解决哪些问题?
- 服务很多,客户端怎么访问,如何提供对外网关?
- 这么多服务,服务之间如何通信? HTTP 还是 RPC?
- 这么多服务,如何治理? 服务的注册和发现。
- 服务挂了怎么办?熔断机制。
有哪些主流微服务框架?
- Spring Cloud Netflix
- Spring Cloud Alibaba
- SpringBoot + Dubbo + ZooKeeper
SpringCloud 有哪些核心组件?
总结
spring
官方推荐出来的一套微服务解决方案,准确来说,我任务
spring cloud
其实是微服务架构里面的出现各种技术场景的定义的一套标准规范,然后再这个规范里面,
spring
集成了
Netflix
公司里面的
oss
开源组件,比如说
zuul
实现应用网关,
eureka
实现服务注册和发现,
ribbon
实现负载均衡,
hytrix
熔断器实现服务的熔断,我们可以去使用
spring cloud netflix
这样一套组件,去快速落地微服务架构以及去解决微服务治理一系列问题,但是啊,随着
netflix oss
相关一些组件不再升级和停止维护,
spring官方
也自研一些组件,
gateway
实现网关,
loadbalancer
实现负载均衡,另外阿里巴巴里面开源组件也实现了
spring cloud
这样一组规范,成为
spring cloud
里面另外一套微服务解决方案包括
dubbo
实现
rpc
通信,
nacos
实现服务注册和发现,以及动态配置中心,
sentinel
实现服务限流和降级。
36 谈一下你对微服务和分布式理解
首先,我先解释一下分布式系统,分布式是通过网络进行通信,并且为了完成共同的计算任务的计算机节点组成系统,分布式系统的设计理念,其实是来自于小型机或者大型机的计算能力的瓶颈和成本的增加, 在集中式系统里面,我们想要提升程序的处理性能,只能不断提升CPU或者增加内存,但是硬件提升本身是有也是有瓶颈的,所以当企业对计算能力越来越高的时候,集群的架构已经无法去满足需求了,所以在这个背景里面,就会产生了分布式计算,也就是说把一个计算任务分配各多个计算机节点进行运算,但是这种计算方式对于用户或者客户端是无感知的,就像访问单个计算机一样,它看到仍然是一个整体,在分布式系统里面,软件架构也要作出响应的调整,也就是说,需要把原本单机应用拆分,部署到多个计算机节点,进行分布式运算,然后呢在每个服务之间,使用远程通信协议,去实现计算结果的数据交互,之后对结果进行汇总返回给客户端;针对这样分布式系统部署的应用架构,我们成为soa,也叫作面向服务的一个架构
其次,我来解释一个微服务架构,微服务架构就是一种分布式架构,只是说微服务架构强调的是对部署在各个计算机节点应用服务的一个粒度的控制,核心思想是,我们针对于soa架构拆分的服务节点,做更进一步的粒度解耦,而每个拆分出来的更小粒度的服务,我们称之为微服务,而每个独立拆分出来微服务由小团队负责,一般是3到5个人,这样拆分好处就是使得我们程序扩展性变得更强,开发迭代效率更高,对于一些大型互联网项目来说,微服务架构能够在不影响用户使用情况下,非常方便实现产品功能更新和迭代。
spring中扩展点
Spring中最常用的11个扩展点
版权归原作者 geekmice 所有, 如有侵权,请联系我们删除。