目录
22. Spring 的单例 Bean 是否有并发安全问题?
26.2. @SpringBootConfiguration
26.3. @EnableAutoConfiguration
26.3.1. @AutoConfigurationPackage
26.3.1.1. @Import(AutoConfigurationPackages.Registrar.class)
26.4. @Import(AutoConfigurationImportSelector.class)
1. 为什么要使用 spring?
- 通过IOC控制反转和DI依赖注入实现松耦合。
- 支持AOP面向切面的编程,并且把应用业务逻辑和系统服务分开。
- 通过切面和模板减少样板式代码。
- 声明式事务的支持。可以从单调繁冗的事务管理代码中解脱出来,通过声明式方式灵活地进行事务的管理,提高开发效率和质量。
- 方便集成各种优秀框架。内部提供了对各种优秀框架的直接支持(如:Hessian、Quartz、MyBatis等)。
- 方便程序的测试。Spring支持Junit4,添加注解便可以测试Spring程序。
2. 解释一下什么是 Aop?
AOP(Aspect-Oriented Programming,面向切面编程)是一种编程范式,AOP的核心思想是通过将这些横切关注点从业务逻辑中分离出来,并将其定义为独立的模块(称为切面)。切面可以在整个应用程序中重用,他以提供特定的横切关注点的功能,如日志记录、事务管理等。AOP通过定义和应用横切关注点来解决应用程序中的横切关注点的分离和抽象问题。
在传统的面向对象编程中,业务逻辑通常以类的形式组织,每个类负责实现特定的功能。然而,某些关注点(例如日志记录、事务管理、安全性检查)可能存在于整个应用程序中,并且会横跨多个类和模块。将这些关注点耦合到每个类中会导致代码的重复和难以维护。
AOP使用通知(Advice)来表示切面的具体行为,通知指定在连接点处插入切面的时机(例如方法执行前、后等)。通知的类型包括前置通知(在连接点之前执行)、后置通知(在连接点之后执行)、返回通知(在连接点成功返回后执行)、异常通知(在连接点抛出异常时执行)和环绕通知(在连接点前后执行)。
织入(Weaving)是将切面应用到目标对象中的过程。织入可以在编译时、运行时或者在系统启动时进行。织入可以通过编译器、特定的AOP框架或者动态代理实现。
AOP的优势在于解决了关注点分离和重用性的问题,提高了代码的模块化程度和可维护性。它能够降低代码的复杂性,减少了重复代码的编写,并实现了关注点的集中管理。AOP在众多框架和技术中被广泛应用,最著名的是Spring框架,它提供了强大的AOP支持
横切关注点:横切关注点(Cross-cutting Concerns)指的是那些在应用程序中存在于多个模块或组件中,与核心业务逻辑无关但却需要在不同的地方进行处理的功能或特性。
横切关注点不属于应用程序核心业务逻辑,但是它们对于应用程序的正确性、可靠性、安全性和性能等方面至关重要
3. AOP有哪些实现方式?
AOP有两种实现方式:静态代理和动态代理。
静态代理
静态代理:代理类在编译阶段生成,在编译阶段将通知织入Java字节码中,也称编译时增强。AspectJ使用的是静态代理。
缺点:代理对象需要与目标对象实现一样的接口,并且实现接口的方法,会有冗余代码。同时,一旦接口增加方法,目标对象与代理对象都要维护。
动态代理
动态代理:代理类在程序运行时创建,AOP框架不会去修改字节码,而是在内存中临时生成一个代理对象,在运行期间对业务方法进行增强,不会生成新类。
4. Spring AOP的实现原理
Spring的AOP实现原理其实很简单,就是通过动态代理实现的。如果我们为Spring的某个bean配置了切面,那么Spring在创建这个bean的时候,实际上创建的是这个bean的一个代理对象,我们后续对bean中方法的调用,实际上调用的是代理类重写的代理方法。而Spring的AOP使用了两种动态代理,分别是JDK的动态代理,以及CGLib的动态代理
5. JDK动态代理和CGLIB动态代理的区别?
Spring AOP中的动态代理主要有两种方式:JDK动态代理和CGLIB动态代理。
JDK动态代理
如果目标类实现了接口,Spring AOP会选择使用JDK动态代理目标类。代理类根据目标类实现的接口动态生成,不需要自己编写,生成的动态代理类和目标类都实现相同的接口。JDK动态代理的核心是InvocationHandler接口和Proxy类。
缺点:目标类必须有实现的接口。如果某个类没有实现接口,那么这个类就不能用JDK动态代理。
GLIB动态代理
通过继承实现。如果目标类没有实现接口,那么Spring AOP会选择使用CGLIB来动态代理目标类。CGLIB(Code Generation Library)可以在运行时动态生成类的字节码,动态创建目标类的子类对象,在子类对象中增强目标类。
CGLIB是通过继承的方式做的动态代理,因此如果某个类被标记为final,那么它是无法使用CGLIB做动态代理的。
优点:目标类不需要实现特定的接口,更加灵活。
什么时候采用哪种动态代理?
- 如果目标对象实现了接口,默认情况下会采用JDK的动态代理实现AOP
- 如果目标对象实现了接口,可以强制使用CGLIB实现AOP
- 如果目标对象没有实现了接口,必须采用CGLIB库
两者的区别:
- jdk动态代理使用jdk中的类Proxy来创建代理对象,它使用反射技术来实现,不需要导入其他依赖。cglib需要引入相关依赖:asm.jar,它使用字节码增强技术来实现。
- 当目标类实现了接口的时候Spring Aop默认使用jdk动态代理方式来增强方法,没有实现接口的时候使用cglib动态代理方式增强方法。
6. 解释一下什么是 ioc?
IOC(Inversion of Control,控制反转),它将程序中对象之间的依赖关系的创建和绑定由应用程序自身转移到了外部容器来处理。这个外部容器通常是一个框架
在传统的编程模型中,应用程序负责创建和管理对象之间的依赖关系。这种方式把对象的控制权交给了应用程序,使得对象之间的依赖关系紧密耦合,难以管理和扩展。
而IOC就是将这种控制权反转过来,将对象创建和依赖关系的处理交由外部容器负责。应用程序只需定义对象的依赖关系,而不用负责对象的创建和装配。容器根据配置信息自动创建和管理对象的实例,并将依赖关系注入到相应的对象中。这样就实现了控制的反转。
IOC的核心概念是依赖注入(Dependency Injection,DI)。依赖注入是指容器动态地将依赖关系注入到对象中,而不是通过对象自身去创建或查找依赖对象。通过依赖注入,对象可以通过接口或者特定的注解来声明它所依赖的其他对象,容器根据这些声明来自动装配所需的依赖。
IOC的优势在于解耦对象之间的依赖关系,减少了对象间的紧密耦合,提高了代码的可扩展性和可维护性。它简化了对象的创建和管理过程,减少了重复的代码,并提高了代码的可测试性。
Spring框架是IOC的一种重要实现,它提供了强大的依赖注入功能,让开发者能够充分利用IOC原则来构建松耦合、可扩展和易于测试的应用程序。
7. spring 有哪些主要模块?
Spring框架包含了很多不同的模块,每个模块都提供了独特的功能和特性,可以根据需要进行选择和使用。以下是Spring框架的一些主要模块:
- Spring Core:Spring核心模块提供了IoC容器和依赖注入的基本功能。它包括Bean容器、Bean的生命周期管理、依赖注入等核心特性。
- Spring MVC:Spring MVC是一个基于模型-视图-控制器(Model-View-Controller)的Web框架。它提供了处理Web请求和生成响应的功能,帮助构建灵活且可扩展的Web应用程序。
- Spring Data:Spring Data模块提供了对数据访问的支持。它简化了与各种数据存储技术(如关系型数据库、NoSQL数据库、键值对存储等)进行交互的过程,提供了统一的数据访问接口和便捷的数据操作功能。
- Spring Security:Spring Security是一个功能强大的安全框架,用于保护应用程序的安全性。它提供了身份验证、授权、密码加密、会话管理等功能,可以轻松集成到Spring应用程序中。
- Spring AOP:Spring AOP(Aspect-Oriented Programming)模块提供了面向切面编程的支持。它通过将横切关注点(例如日志记录、性能监控、事务管理等)从核心业务逻辑中分离出来,实现了更好的模块化和可重用性。
- Spring Batch:Spring Batch是一个处理大量数据和批量作业的框架。它提供了可重用的组件和模式,用于构建和执行批处理任务,例如数据导入/导出、报表生成、批量处理任务等。
- Spring Integration:Spring Integration是一个用于构建企业集成系统的框架。它通过提供丰富的消息处理机制和适配器,使得不同系统之间的通信变得更加简单和可靠。
8. spring 常用的注入方式有哪些?
在Spring框架中,有多种常用的依赖注入(Dependency Injection, DI)方式。以下是一些常见的注入方式:
- 构造函数注入(Constructor Injection):通过构造函数将依赖项注入到目标对象中。可以通过构造函数参数来明确指定依赖项,从而在创建对象时自动完成注入。
- Setter方法注入(Setter Injection):使用Setter方法来设置依赖项。通过定义公开的Setter方法,并在容器配置中使用属性注入(property injection)来注入依赖项。
- 字段注入(Field Injection):使用注解直接在字段上进行注入。通过在目标类的成员变量上标记注解(如@Autowired)来实现自动注入。
- 方法注入(Method Injection):使用特定注解标记的方法来完成依赖项的注入。可以在方法的参数上使用注解或通过配置方式来指定依赖项。
- 接口注入(Interface Injection):通过实现特定接口,在接口方法中进行依赖项的注入。但这种方式相对较少使用,因为它引入了对特定接口的依赖。
9. spring 中的 bean 是线程安全的吗?
在Spring框架中,默认情况下,每个bean实例都是单例的,这意味着默认情况下Spring的bean不是线程安全的。当多个线程同时访问同一个bean实例时,存在并发访问的风险。
线程安全这个问题,要从单例与原型Bean分别进行说明
- 单例模式(Singleton):默认情况下,Spring的bean是单例的,由Spring容器负责创建和管理。对于单例Bean,所有线程都共享一个单例实例Bean,因此是存在资源的竞争。
- 原型模式(Prototype):通过配置bean的作用域为prototype,可以每次请求时都创建一个新的bean实例。对于原型Bean,每次创建一个新对象,也就是线程之间并不存在Bean共享,自然是不会有线程安全的问题。
- 线程作用域(Thread Scope):Spring还提供了线程作用域(Thread Scope)的支持,可以使用ThreadLocal实现每个线程拥有独立的bean实例。这样,每个线程都可以独立地操作自己的bean实例,实现了线程间的隔离和线程安全。
如果需要保证Spring的bean的线程安全性,可以在bean的实现中采取一些线程安全的措施,比如使用同步(synchronized)关键字、使用线程安全的集合或使用线程安全的设计模式,根据具体的业务需求进行选择。
总之,Spring的bean是否线程安全取决于其实现方式和具体的配置,开发人员需要在设计和实现bean时注意线程安全的问题,并根据实际情况选择合适的解决方案。
10. spring 支持几种 bean 的作用域?
Spring框架支持以下常见的Bean作用域:
- Singleton(默认):每个Spring容器中只会存在一个Bean实例。在整个应用程序中,无论多少次请求获取该Bean,都将返回同一个实例。
- Prototype:每次通过容器的getBean()方法获取Bean时,都会创建一个新的Bean实例。每次注入或请求该Bean时,都会得到一个独立的实例。
- Request:在一次HTTP请求过程中,创建一个Bean实例,该实例为当前请求提供服务。不同的请求将获得不同的Bean实例。
- Session:在一个HTTP会话期间,创建一个Bean实例,该实例与该会话相关联。在会话期间,不同的请求将共享同一个会话Bean实例。
- Global Session:在基于Portlet的Web应用程序中,该作用域与Session作用域类似,但全局会话是面向所有Portlet请求的共享实例。
- Application:在整个Web应用程序的生命周期内,创建一个Bean实例。多个请求将共享同一个应用程序作用域的Bean实例。
- WebSocket:在WebSocket会话期间,创建一个Bean实例,该实例与WebSocket会话相关联。WebSocket会话期间,不同的请求将共享同一个WebSocket Bean实例。
以上是Spring框架中常用的Bean作用域。可以根据应用程序的不同需求选择合适的作用域,在配置Bean时使用@Scope注解或XML配置文件中的<bean>元素设定作用域。
11. spring 自动装配 bean 有哪些方式?
- 默认方式:使用注解@Autowired进行自动装配。Spring会根据类型进行自动装配,当有多个同类型的Bean时,可以使用@Qualifier指定具体的Bean名称。
@Autowired
private MyBean myBean;
- 通过构造函数进行自动装配:使用@Autowired注解可以直接将依赖注入到构造函数中。
@Autowired
public MyClass(MyBean myBean) {
this.myBean = myBean;
}
- 通过setter方法进行自动装配:使用@Autowired注解可以将依赖注入到setter方法中。
@Autowired
public void setMyBean(MyBean myBean) {
this.myBean = myBean;
}
- 使用@Resource注解进行自动装配:@Resource注解是JavaEE提供的,Spring也支持它。它可以根据Bean的名称自动注入依赖。
@Resource
private MyBean myBean;
- 通过@Inject注解进行自动装配:与@Autowired类似,@Inject也可以实现自动装配。
@Inject
private MyBean myBean;
所有这些方式都可以与@Qualifier一起使用,用于指定精确的Bean名称进行装配。
除了上述方式,Spring还提供了基于XML配置文件的自动装配方式。在XML文件中,可以使用<context:component-scan>元素激活自动扫描,并使用<bean>元素声明Bean,Spring会根据规则自动装配这些Bean。
总结:Spring提供了多种方式来实现自动装配Bean,包括注解(@Autowired,@Qualifier,@Resource,@Inject)和XML配置文件的方式。这些方式都可以根据类型或名称进行自动装配,提高了开发效率和代码的可读性
12. spring 事务实现方式有哪些?
事务就是一系列的操作原子执行。Spring事务机制主要包括声明式事务和编程式事务。
- 基于注解的声明式事务管理:使用@Transactional注解来标识需要进行事务管理的方法或类。通过在方法或类上添加该注解,Spring会自动处理事务的开始、提交、回滚等操作。
@Service
@Transactional
public class UserServiceImpl implements UserService {
// ...
}
- 基于编程式事务管理:使用编程的方式在代码中进行事务管理,通过编写事务管理的代码块来控制事务的开始、提交、回滚等操作。
@Autowired
private PlatformTransactionManager transactionManager;
@Transactional
public void performOperation() {
TransactionStatus status = transactionManager.getTransaction(new DefaultTransactionDefinition());
try {
// 执行业务逻辑操作
// ...
transactionManager.commit(status);
} catch (Exception e) {
transactionManager.rollback(status);
throw e;
}
}
- 基于XML配置的声明式事务管理:通过在XML配置文件中进行事务管理的配置。需要配置事务管理器、事务通知等相关元素。
<bean id="transactionManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
<property name="dataSource" ref="dataSource" />
</bean>
<tx:advice id="txAdvice" transaction-manager="transactionManager">
<tx:attributes>
<tx:method name="*" propagation="REQUIRED" />
</tx:attributes>
</tx:advice>
<aop:config>
<aop:pointcut id="transactionPointcut" expression="execution(* com.example.service.*.*(..))" />
<aop:advisor advice-ref="txAdvice" pointcut-ref="transactionPointcut" />
</aop:config>
13. 说一下 spring 的事务隔离?
Spring框架提供了对事务隔离级别的支持,事务隔离级别定义了事务之间的隔离程度。以下是Spring框架支持的事务隔离级别:
- DEFAULT:使用数据库默认的隔离级别。
- READ_UNCOMMITTED(读未提交):事务可以读取其他事务尚未提交的数据,可能导致脏读、不可重复读和幻读问题。
- READ_COMMITTED(读已提交):事务只能读取其他事务已经提交的数据,可以解决脏读问题,但可能产生不可重复读和幻读问题。
- REPEATABLE_READ(可重复读):事务在整个过程中可以多次读取相同的数据,保证了一致性读取,可以解决脏读和不可重复读问题,但可能产生幻读问题。
- SERIALIZABLE(串行化):事务按照顺序逐个执行,事务之间是串行化的,可以避免脏读、不可重复读和幻读问题,但性能较差。
在Spring中,通过@Transactional注解的isolation属性来设置事务隔离级别。例如:
@Transactional(isolation = Isolation.READ_COMMITTED)
public void performOperation() {
// ...
}
需要注意的是,事务隔离级别的选择应根据具体的业务需求和并发情况来进行合理的评估。较高的隔离级别可以提供更高的数据一致性,但会带来一定的性能开销。开发人员应根据具体情况选择适当的隔离级别。
注
脏读(Dirty Read):脏读指的是一个事务读取了另一个事务未提交的数据。当一个事务读取到了另一个事务修改过但未提交的数据时,如果该修改操作回滚,那么读取到的数据就是无效的,称为脏读。(读到了另一个事务提交之前的修改过的数据)
不可重复读(Non-repeatable Read):不可重复读指的是在同一个事务内,多次读取同一数据时,得到的结果不一致。例如,事务A在一个数据记录上执行了读取操作,然后事务B对该数据记录执行了更新操作并提交,接着事务A再次读取该数据记录时,得到的结果与前一次读取的结果不一样。(读取同一个数据的结果不一样)
幻读(Phantom Read):幻读指的是在同一个事务内,多次执行同样的查询操作得到了不同数量的结果。例如,事务A在一个数据表上执行了一个范围查询(例如SELECT * FROM table WHERE condition),接着事务B在该表中插入了一行符合事务A的查询条件的数据并提交,接着事务A再次执行同样的查询时,得到的结果集数量发生了变化。
(同样的操作读到数据不一样)
这些问题都是由于事务并发和隔离级别低导致的。为了解决这些问题,可以使用数据库的事务隔离级别:
- 读未提交(Read Uncommitted):最低的事务隔离级别,允许脏读、不可重复读和幻读。
- 读已提交(Read Committed):允许不可重复读和幻读,但不允许脏读。
- 可重复读(Repeatable Read):允许幻读,但不允许脏读和不可重复读。MySQL的默认隔离级别。
- 串行化(Serializable):最高的事务隔离级别,禁止脏读、不可重复读和幻读。
需要根据实际情况选择适当的事务隔离级别来解决并发问题。
14. 说一下 spring mvc 运行流程?
- 客户端发送请求到DispatcherServlet(前端控制器)。
- DispatcherServlet收到请求后,根据请求URL找到对应的HandlerMapping(处理器映射器),通过HandlerMapping确定处理请求的Handler(处理器)。
- HandlerAdapter(处理器适配器)将请求转发给Handler执行。
- Handler执行业务逻辑,处理请求,并返回一个ModelAndView对象。
- Handler将处理结果封装在ModelAndView对象中,包括数据模型和视图信息。
- Handler返回的ModelAndView交给DispatcherServlet。
- DispatcherServlet调用ViewResolver(视图解析器)根据视图信息再次转发到具体的View(视图)。
- View(视图)负责渲染数据模型,生成最终的HTML、JSON等响应。
- View将渲染结果返回给DispatcherServlet。
- DispatcherServlet将最终的响应返回给客户端。
15. spring mvc 有哪些组件?
- DispatcherServlet(前端控制器):它是整个Spring MVC框架的入口点,负责接收客户端的请求并将请求分发给相应的处理器(Controller)。
- HandlerMapping(处理器映射器):它负责根据请求的URL找到相应的处理器(Controller),将请求映射到具体的处理器。
- HandlerAdapter(处理器适配器):它负责调用处理器(Controller)来处理请求,并将处理结果封装为ModelAndView对象。
- ViewResolver(视图解析器):它负责解析视图的逻辑名称,并根据名称获取实际的视图对象。
- View(视图):它负责渲染模型数据并生成最终的响应结果,可以是JSP页面、HTML页面、JSON数据等。
- HandlerInterceptor(处理器拦截器):它提供了在请求处理过程中的拦截机制,可以在请求前、请求后或者渲染视图之前进行特定的处理。
- ModelAndView(模型和视图对象):它封装了处理器(Controller)处理请求后的模型数据和视图名称。
- DataBinder(数据绑定器):它负责将请求参数绑定到处理器(Controller)的方法参数上,实现参数的自动绑定。
16. @RequestMapping 的作用是什么?
@RequestMapping是Spring框架中的一个注解,用于将HTTP请求映射到特定的处理方法(Controller的方法)上。它的作用是将特定的URL路径或请求方法与相应的处理方法进行绑定,以定义请求的处理逻辑。
使用@RequestMapping注解可以在控制器类的方法上或整个控制器类上进行声明。在方法级别上使用@RequestMapping,可以针对具体的URL路径和请求方法定义不同的处理方法。在类级别上使用@RequestMapping,可以指定共享的基本路径,从而简化具体处理方法的URL映射。
@RequestMapping注解提供了细粒度的配置选项,包括以下常用属性:
- value:定义映射的URL路径,可以是单个字符串或字符串数组。
- method:指定HTTP请求方法,可以是GET、POST、PUT、DELETE等。
- params:定义请求参数的条件,可指定参数名和值。
- headers:定义HTTP请求头的条件,可指定头部参数名和值。
- consumes:定义请求的Content-Type,用于限制请求的媒体类型。
- produces:定义返回的媒体类型,用于指定响应的Content-Type。
通过使用不同的属性和属性值,可以根据具体需求精确地匹配请求,并将其路由到相应的处理方法上进行处理。@RequestMapping注解使得控制器能够根据URL和请求方法灵活地处理不同的HTTP请求,并将其映射到相应的业务逻辑上。
需要注意的是,从Spring 4.3版本开始,@RequestMapping注解被@GetMapping、@PostMapping、@PutMapping、@DeleteMapping等更具体的注解所替代,以提供更清晰和简洁的语义。这些具体的注解可以直接指定HTTP请求方法,避免了使用method属性。
17. @Autowired 的作用是什么?
@Autowired 是Spring框架中的一个注解,用于实现自动装配(dependency injection),即将相应类型的Bean注入到需要它的地方。它的作用是完成依赖关系的自动建立,消除了在代码中显式进行依赖对象的创建和查找的繁琐过程。
使用 @Autowired 注解可以标记在需要依赖的地方,比如构造函数、成员变量、方法参数或者 Setter 方法上。当 Spring 容器创建 Bean 的时候,会自动解析标记了 @Autowired 的依赖,然后将匹配的 Bean 实例注入到相应的位置。
作用的主要方面包括:
- 自动装配 Bean:当一个类需要依赖其他对象时,可以使用 @Autowired 注解将相关的 Bean 自动注入到该类中。
- 类型匹配:@Autowired 注解可以根据类型进行 Bean 的匹配。当容器中存在多个符合要求的 Bean 时,Spring 会尝试通过类型进行匹配。
- 依赖注入:通过 @Autowired 注解,可以将相关的依赖对象注入到目标对象中,实现对依赖的注入管理。
不允许注null,(require = false)
需要注意的是,@Autowired 注解默认是按照类型进行装配,如果有多个匹配的 Bean,可以结合使用 @Qualifier 注解指定具体的 Bean 名称。另外,@Autowired 注解也可以和 @Primary 注解一起使用,表示优先选择被标注为 @Primary 的 Bean 进行装配。
总之,@Autowired 注解简化了依赖注入的配置,减少了手动装配的代码量,提高了代码的可读性和可维护性。同时,它使得代码与特定的实现解耦,提高了应用程序的灵活性和可测试性。
18. Spring有哪些事务传播行为?
- REQUIRED(默认):如果当前存在事务,则加入到当前事务中进行执行;如果当前没有事务,则创建一个新事务进行执行。
- REQUIRES_NEW:无论当前是否存在事务,都创建一个新的事务进行执行。如果当前存在事务,则将当前事务挂起。
- SUPPORTS:如果当前存在事务,则加入到当前事务中进行执行;如果当前没有事务,则以非事务的方式执行。
- NOT_SUPPORTED:以非事务的方式执行操作,如果当前存在事务,则将当前事务挂起。
- MANDATORY:如果当前存在事务,则加入到当前事务中进行执行;如果当前没有事务,则抛出异常。
- NEVER:以非事务的方式执行操作,如果当前存在事务,则抛出异常。
- NESTED:如果当前存在事务,则创建一个嵌套事务并进行执行;如果当前没有事务,则创建一个新事务进行执行。嵌套事务可以独立提交或回滚,但是必须在外部事务的边界内进行。
propagation
PROPAGATION_NESTED 与PROPAGATION_REQUIRES_NEW的区别:
使用PROPAGATION_REQUIRES_NEW时,内层事务与外层事务是两个独立的事务。一旦内层事务进行了提交后,外层事务不能对其进行回滚。两个事务互不影响。
使用PROPAGATION_NESTED时,外层事务的回滚可以引起内层事务的回滚。而内层事务的异常并不会导致外层事务的回滚,它是一个真正的嵌套事务。
19. Spring事务在什么情况下会失效?
1.访问权限问题
java的访问权限主要有四种:private、default、protected、public,它们的权限从左到右,依次变大。
如果事务方法的访问权限不是定义成public,这样会导致事务失效,因为spring要求被代理方法必须是public的。
翻开源码,可以看到,在AbstractFallbackTransactionAttributeSource类的computeTransactionAttribute方法中有个判断,如果目标方法不是public,则返回null,即不支持事务。
protected TransactionAttribute computeTransactionAttribute(Method method, @Nullable Class<?> targetClass) {
// Don't allow no-public methods as required.
if (allowPublicMethodsOnly() && !Modifier.isPublic(method.getModifiers())) {
return null;
}
...
}
2. 方法用final修饰
如果事务方法用final修饰,将会导致事务失效。因为spring事务底层使用了aop,也就是通过jdk动态代理或者cglib,帮我们生成了代理类,在代理类中实现的事务功能。
但如果某个方法用final修饰了,那么在它的代理类中,就无法重写该方法,而添加事务功能。
同理,如果某个方法是static的,同样无法通过动态代理,变成事务方法。
3.对象没有被spring管理
使用spring事务的前提是:对象要被spring管理,需要创建bean实例。如果类没有加@Controller、@Service、@Component、@Repository等注解,即该类没有交给spring去管理,那么它的方法也不会生成事务。
4.表不支持事务
如果MySQL使用的存储引擎是myisam,这样的话是不支持事务的。因为myisam存储引擎不支持事务。
5.方法内部调用
如下代码所示,update方法上面没有加 @Transactional 注解,调用有 @Transactional 注解的 updateOrder 方法,updateOrder 方法上的事务会失效。
因为发生了自身调用,调用该类自己的方法,而没有经过 Spring 的代理类,只有在外部调用事务才会生效。
@Service
public class OrderServiceImpl implements OrderService {
public void update(Order order) {
this.updateOrder(order);
}
@Transactional
public void updateOrder(Order order) {
// update order
}
}
解决方法:
1、再声明一个service,将内部调用改为外部调用
2、使用编程式事务
3、使用AopContext.currentProxy()获取代理对象
@Servcie
public class OrderServiceImpl implements OrderService {
public void update(Order order) {
((OrderService)AopContext.currentProxy()).updateOrder(order);
}
@Transactional
public void updateOrder(Order order) {
// update order
}
}
6.未开启事务
如果是spring项目,则需要在配置文件中手动配置事务相关参数。如果忘了配置,事务肯定是不会生效的。
如果是springboot项目,那么不需要手动配置。因为springboot已经在DataSourceTransactionManagerAutoConfiguration类中帮我们开启了事务。
7.吞了异常
有时候事务不会回滚,有可能是在代码中手动catch了异常。因为开发者自己捕获了异常,又没有手动抛出,把异常吞掉了,这种情况下spring事务不会回滚。
如果想要spring事务能够正常回滚,必须抛出它能够处理的异常。如果没有抛异常,则spring认为程序是正常的。
20. Spring怎么解决循环依赖的问题?
首先,有两种Bean注入的方式。
构造器注入和属性注入。
对于构造器注入的循环依赖,Spring处理不了,会直接抛出BeanCurrentlylnCreationException异常。
对于属性注入的循环依赖(单例模式下),是通过三级缓存处理来循环依赖的。
而非单例对象的循环依赖,则无法处理。
下面分析单例模式下属性注入的循环依赖是怎么处理的:
首先,Spring单例对象的初始化大略分为三步:
- createBeanInstance:实例化bean,使用构造方法创建对象,为对象分配内存。
- populateBean:进行依赖注入。
- initializeBean:初始化bean。
Spring为了解决单例的循环依赖问题,使用了三级缓存:
singletonObjects:完成了初始化的单例对象map,bean name --> bean instance
earlySingletonObjects :完成实例化未初始化的单例对象map,bean name --> bean instance
singletonFactories : 单例对象工厂map,bean name --> ObjectFactory,单例对象实例化完成之后会加入singletonFactories。
在调用createBeanInstance进行实例化之后,会调用addSingletonFactory,将单例对象放到singletonFactories中。
protected void addSingletonFactory(String beanName, ObjectFactory<?> singletonFactory) {
Assert.notNull(singletonFactory, "Singleton factory must not be null");
synchronized (this.singletonObjects) {
if (!this.singletonObjects.containsKey(beanName)) {
this.singletonFactories.put(beanName, singletonFactory);
this.earlySingletonObjects.remove(beanName);
this.registeredSingletons.add(beanName);
}
}
}
假如A依赖了B的实例对象,同时B也依赖A的实例对象。
- A首先完成了实例化,并且将自己添加到singletonFactories中
- 接着进行依赖注入,发现自己依赖对象B,此时就尝试去get(B)
- 发现B还没有被实例化,对B进行实例化
- 然后B在初始化的时候发现自己依赖了对象A,于是尝试get(A),尝试一级缓存singletonObjects和二级缓存earlySingletonObjects没找到,尝试三级缓存singletonFactories,由于A初始化时将自己添加到了singletonFactories,所以B可以拿到A对象,然后将A从三级缓存中移到二级缓存中
- B拿到A对象后顺利完成了初始化,然后将自己放入到一级缓存singletonObjects中
- 此时返回A中,A此时能拿到B的对象顺利完成自己的初始化
由此看出,属性注入的循环依赖主要是通过将实例化完成的bean添加到singletonFactories来实现的。而使用构造器依赖注入的bean在实例化的时候会进行依赖注入,不会被添加到singletonFactories中。比如A和B都是通过构造器依赖注入,A在调用构造器进行实例化的时候,发现自己依赖B,B没有被实例化,就会对B进行实例化,此时A未实例化完成,不会被添加到singtonFactories。而B依赖于A,B会去三级缓存寻找A对象,发现不存在,于是又会实例化A,A实例化了两次,从而导致抛异常。
总结:1、利用缓存识别已经遍历过的节点; 2、利用Java引用,先提前设置对象地址,后完善对象。
21. Spring启动过程
- 读取web.xml文件。
- 创建 ServletContext,为 ioc 容器提供宿主环境。
- 触发容器初始化事件,调用 contextLoaderListener.contextInitialized()方法,在这个方法会初始化一个应用上下文WebApplicationContext,即 Spring 的 ioc 容器。ioc 容器初始化完成之后,会被存储到 ServletContext 中。
- 初始化web.xml中配置的Servlet。如DispatcherServlet,用于匹配、处理每个servlet请求。
22. Spring 的单例 Bean 是否有并发安全问题?
当多个用户同时请求一个服务时,容器会给每一个请求分配一个线程,这时多个线程会并发执行该请求对应的业务逻辑,如果业务逻辑有对单例状态的修改(体现为此单例的成员属性),则必须考虑线程安全问题。
无状态bean和有状态bean
- 有实例变量的bean,可以保存数据,是非线程安全的。
- 没有实例变量的bean,不能保存数据,是线程安全的。
在Spring中无状态的Bean适合用单例模式,这样可以共享实例提高性能。有状态的Bean在多线程环境下不安全,一般用Prototype模式或者使用ThreadLocal解决线程安全问题。
23. Spring Bean如何保证并发安全?
Spring的Bean默认都是单例的,某些情况下,单例是并发不安全的。
以 Controller 举例,假如我们在 Controller 中定义了成员变量。当多个请求来临,进入的都是同一个单例的 Controller 对象,并对此成员变量的值进行修改操作,因此会互相影响,会有并发安全的问题。
应该怎么解决呢?
为了让多个HTTP请求之间不互相影响,可以采取以下措施:
1、单例变原型
对 web 项目,可以 Controller 类上加注解 @Scope("prototype") 或 @Scope("request"),对非 web 项目,在 Component 类上添加注解 @Scope("prototype") 。
这种方式实现起来非常简单,但是很大程度上增大了 Bean 创建实例化销毁的服务器资源开销。
2、尽量避免使用成员变量
在业务允许的条件下,可以将成员变量替换为方法中的局部变量。这种方式个人认为是最恰当的。
3、使用并发安全的类
如果非要在单例Bean中使用成员变量,可以考虑使用并发安全的容器,如 ConcurrentHashMap、ConcurrentHashSet 等等,将我们的成员变量包装到这些并发安全的容器中进行管理即可。
4、分布式或微服务的并发安全
如果还要进一步考虑到微服务或分布式服务的影响,方式3便不合适了。这种情况下可以借助于可以共享某些信息的分布式缓存中间件,如Redis等。这样即可保证同一种服务的不同服务实例都拥有同一份共享信息了。
24. springbean的生命周期
- 实例化(Instantiation):当 Spring 容器加载配置文件时,会根据配置的 bean 定义,实例化相应的 bean 对象。
- 属性注入(Property Injection):在实例化后,Spring 容器会根据配置文件中的依赖关系,自动将相应的属性注入给对应的 bean。
- 初始化(Initialization):在完成属性注入后,Spring 容器会调用 bean 的初始化方法(如果有定义的话),进行一些初始化操作。
- 使用(In Use):初始化完成后,bean 就处于可用状态,可以被其他对象引用和使用。
- 销毁(Destruction):当 Spring 容器关闭时,会调用 bean 的销毁方法(如果有定义的话),进行一些清理工作,释放资源。
在 Spring 中,可以通过配置文件或注解来定义 bean 的生命周期相关的操作,例如在配置文件中可以使用 <bean> 标签的 init-method 和 destroy-method 属性来指定初始化和销毁方法,或者使用 @PostConstruct 和 @PreDestroy 注解来标注初始化和销毁方法。
25. springboot注解
@Conditional是Spring框架中的一个注解,可以用于根据条件来决定是否创建某个Bean或者启用某个配置。它的作用是根据条件进行条件化的Bean注册或者配置。
@Conditional注解可以用在以下几个地方:
- 在@Configuration类中,可以用于配置类的条件判断,根据条件选择性地加载或者注册某个配置。
- 在@Bean方法上,可以使用@Conditional来决定是否创建某个Bean。当满足条件时,该Bean会被创建并注册到ApplicationContext中;当条件不满足时,该Bean不会被创建。
@Conditional注解的使用方式多种多样,可以使用Spring提供的一些预定义的条件注解,比如@ConditionalOnProperty、@ConditionalOnClass、@ConditionalOnBean等等。此外,你还可以实现自定义的条件注解,通过实现Condition接口来定义自己的条件逻辑。
通过使用@Conditional注解,可以在Spring Boot应用中根据不同的条件来实现Bean的动态注册与配置,提高了应用的灵活性和自定义能力。
@ConditionalOnProperty是Spring框架中的一个条件注解,用于根据配置文件中的属性值来决定是否创建某个Bean或者启用某个配置。通过指定属性的名称和值,可以根据配置文件中的属性值进行条件判断。
例如,我们可以使用@ConditionalOnProperty注解来判断是否加载某个Bean:
@Configuration
@ConditionalOnProperty(name = "myapp.feature.enabled", havingValue = "true")
public class MyFeatureAutoConfiguration {
// Bean definitions
}
在上述示例中,@ConditionalOnProperty注解会根据配置文件中名为myapp.feature.enabled的属性值进行判断。只有当该属性值为true时,MyFeatureAutoConfiguration类中的Bean定义才会被加载。
@ConditionalOnClass是Spring框架中的另一个条件注解,用于根据类的存在与否来决定是否创建某个Bean或者启用某个配置。当指定的类在类路径中存在时,才会满足条件。
例如,我们可以使用@ConditionalOnClass注解来判断是否加载某个Bean:
@Configuration
@ConditionalOnClass(UserService.class)
public class UserServiceAutoConfiguration {
// Bean definitions
}
在上述示例中,只有当类路径中存在UserService类时,UserServiceAutoConfiguration类中的Bean定义才会被加载。
@ConditionalOnBean是Spring框架中的另一个条件注解,用于根据特定的Bean是否存在来决定是否创建其他的Bean或者启用某个配置。当指定的Bean存在于ApplicationContext中时,才会满足条件。
例如,我们可以使用@ConditionalOnBean注解来判断是否加载某个Bean:
@Configuration
@ConditionalOnBean(UserService.class)
public class MyFeatureAutoConfiguration {
// Bean definitions
}
在上述示例中,只有当ApplicationContext中存在UserService类型的Bean时,MyFeatureAutoConfiguration类中的Bean定义才会被加载。
通过使用这些条件注解,可以根据不同的条件来控制Bean的加载与配置,从而实现灵活的应用配置和自定义能力。
@ConditionalOnMissingBean
ConditionalOnMissingBean是Spring框架中的一个条件注解,用于根据是否存在某个Bean来决定是否创建另一个Bean。当指定的Bean在ApplicationContext中不存在时,才会满足条件。
例如,我们可以使用@ConditionalOnMissingBean注解来判断是否创建某个Bean:
@Configuration
public class MyBeanConfiguration {
@Bean
@ConditionalOnMissingBean
public MyBean myBean() {
// Bean definition
return new MyBean();
}
}
在上述示例中,@ConditionalOnMissingBean注解用于myBean()方法上。它的作用是判断在ApplicationContext中是否已经存在MyBean类型的Bean,如果不存在才会创建并注册该Bean。
这样,当ApplicationContext中不存在MyBean类型的Bean时,myBean()方法返回的MyBean对象就会被创建并注册到ApplicationContext中;但当已经存在MyBean类型的Bean时,myBean()方法不会被执行。
通过使用@ConditionalOnMissingBean注解,可以避免重复创建某个Bean,同时也提供了一种在Bean不存在时才创建的方式,增加了配置的灵活性。
26. SpringBootApplication注解
除了普通修饰注解类的原信息,还有@SpringBootConfiguration、@EnableAutoConfiguration、@ComponentScan 3个注解。
26.1. @ComponentScan
@ComponentScan的功能是自动扫描并加载符合条件的组件(如@controller、@Component等),最终将这些Bean的定义加载到Ioc容器中。
我们可以通过basePackages等属性来细粒度的定制@ComponentScan自动扫描的范围,如果不指定,则默认Spring框架实现会从声明@ComponentScan所在类的package进行扫描。所以通常我们在定义SpringBoot启动类的时候,会把它放到root package下,这样就能扫描到所有需要定义的类。
26.2. @SpringBootConfiguration
这个注解底层是有Configuration注解的
SpringBootConfiguration与Spring中的@Configuation的作用基本一致,只不过@SpringBootConfiguration是springboot的注解,而@Configuration是spring的注解。
Configuration注解就是把一个类设置为配置类交给Spring容器管理,取代了原有的beans.xml配置文件
26.3. @EnableAutoConfiguration
自动装配的关键
EnableAutoConfiguration的作用是从classpath中搜寻所有的META-INF/spring.factories配置文件,并将其中org.springframework.boot.autoconfigure.EnableutoConfiguration对应的配置项通过反射(Java Refletion)实例化为对应的标注了@Configuration的JavaConfig形式的IoC容器配置类,然后汇总为一个并加载到IoC容器。
@EnableAutoConfiguration 其实也没啥“创意”,Spring 框架提供的各种名字为 @Enable 开头的 Annotation 定义?
比如 @EnableScheduling、@EnableCaching、@EnableMBeanExport 等,@EnableAutoConfiguration 的理念和“做事方式”其实一脉相承,简单概括一下就是,借助 @Import 的支持,收集和注册特定场景相关的 bean 定义:
@EnableScheduling 是通过 @Import 将 Spring 调度框架相关的 bean 定义都加载到 IoC 容器。
@EnableMBeanExport 是通过 @Import 将 JMX 相关的 bean 定义加载到 IoC 容器。
而 @EnableAutoConfiguration 也是借助 @Import 的帮助,将所有符合自动配置条件的 bean 定义加载到 IoC 容器,仅此而已
其中,最关键的要属 @Import(EnableAutoConfigurationImportSelector.class),借助 EnableAutoConfigurationImportSelector,@EnableAutoConfiguration 可以帮助 SpringBoot 应用将所有符合条件的 @Configuration 配置都加载到当前 SpringBoot 创建并使用的 IoC 容器
26.3.1. @AutoConfigurationPackage
可以自动配置扫描到的包
26.3.1.1. @Import(AutoConfigurationPackages.Registrar.class)
@Import用于导入其他配置类或者组件类。它可以被用来在一个配置类中引入其他配置类,以便将它们的配置信息合并到当前的配置中。它的作用,就是注册启动类所在的包和其子包下的所有组件
用于注册Bean定义到Spring容器的 中。在这里,registerBeanDefinitions方法通过PackageImports类获取到注解元数据中的包名,并将这些包名作为参数调用register方法进行注册
PackageImports
register
26.4. @Import(AutoConfigurationImportSelector.class)
getCandidateConfigurations: 调用 getCandidateConfigurations 方法获取候选的配置类列表。
protected List<String> getCandidateConfigurations(AnnotationMetadata metadata, AnnotationAttributes attributes) {
List<String> configurations = new ArrayList<>(
SpringFactoriesLoader.loadFactoryNames(getSpringFactoriesLoaderFactoryClass(), getBeanClassLoader()));
ImportCandidates.load(AutoConfiguration.class, getBeanClassLoader()).forEach(configurations::add);
Assert.notEmpty(configurations,
"No auto configuration classes found in META-INF/spring.factories nor in META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports. If you "
+ "are using a custom packaging, make sure that file is correct.");
return configurations;
}
该方法的作用是获取候选的自动配置类列表,
SpringFactoriesLoader.loadFactoryNames 方法会加载外部配置文件,通过加载META-INF/spring.factories
ImportCandidates.load会加载
META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports文件中的配置类信息,以及通过ImportCandidates加载的自动配置类,构建一个配置类的列表。这些候选的配置类将被后续的自动配置过程使用。
ImportCandidates.load
ImportCandidates.load(AutoConfiguration.class, getBeanClassLoader()).forEach(configurations::add): 使用ImportCandidates工具类从META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports文件中加载自动配置类,并将它们添加到配置类列表中。AutoConfiguration.class是一个标记接口,用于指示需要加载的自动配置类。
String location = String.format(LOCATION, annotation.getName()): 根据指定注解的名称构建位置字符串。LOCATION是一个字符串常量,用于定义位置的格式。
load方法的作用是根据指定的注解,通过查找类路径中匹配的URL资源,读取候选自动配置类,并将它们存储在ImportCandidates对象中返回。ImportCandidates是一个简单的封装类,用于存储候选自动配置类的列表。
loadFactoryNames
SpringFactoriesLoader.loadFactoryNames(getSpringFactoriesLoaderFactoryClass(), getBeanClassLoader()): 使用Spring框架提供的SpringFactoriesLoader从META-INF/spring.factories文件中加载自动配置类的工厂名称,并将其存储在一个新的ArrayList中。getSpringFactoriesLoaderFactoryClass()是一个方法,用于获取SpringFactoriesLoader的工厂类。
private static Map<String, List<String>> loadSpringFactories(ClassLoader classLoader) {
Map<String, List<String>> result = cache.get(classLoader);
if (result != null) {
return result;
}
result = new HashMap<>();
try {
Enumeration<URL> urls = classLoader.getResources(FACTORIES_RESOURCE_LOCATION);
while (urls.hasMoreElements()) {
URL url = urls.nextElement();
UrlResource resource = new UrlResource(url);
Properties properties = PropertiesLoaderUtils.loadProperties(resource);
for (Map.Entry<?, ?> entry : properties.entrySet()) {
String factoryTypeName = ((String) entry.getKey()).trim();
String[] factoryImplementationNames =
StringUtils.commaDelimitedListToStringArray((String) entry.getValue());
for (String factoryImplementationName : factoryImplementationNames) {
result.computeIfAbsent(factoryTypeName, key -> new ArrayList<>())
.add(factoryImplementationName.trim());
}
}
}
// Replace all lists with unmodifiable lists containing unique elements
result.replaceAll((factoryType, implementations) -> implementations.stream().distinct()
.collect(Collectors.collectingAndThen(Collectors.toList(), Collections::unmodifiableList)));
cache.put(classLoader, result);
}
catch (IOException ex) {
throw new IllegalArgumentException("Unable to load factories from location [" +
FACTORIES_RESOURCE_LOCATION + "]", ex);
}
return result;
}