Spring
注解:
核心注解
@Component:通用组件注解,用于标记一个类为Spring管理的组件(Bean)。它可以被
@Service
,@Repository
, 和@Controller
等更特定的注解所替代。- 使用
@Component
注解时,在解析@Component注册为BeanDefinition时会调用AnnotationBeanNameGenerator.buildDefaultBeanName,此时正常情况下会将第一个字符从大写转为小写,但如果第一个和第二个都是大写,则不变。【FooBah->fooBah; URL->URL】
- 使用
- @Service:标记服务层组件,通常用于业务逻辑层。它是一个特化的
@Component
,有助于分层架构的理解和维护。 - @Repository:标记数据访问层(DAO层)组件,用于持久层操作。同样也是
@Component
的一种具体形式,主要用于数据库交互。 - @Controller:用于标记Web控制器组件,通常与Spring MVC一起使用处理HTTP请求。
- @RestController:是
@Controller
和@ResponseBody
的组合注解,专门用于创建RESTful Web服务。
依赖注入相关注解
- @Autowired:自动装配依赖项,可以用于构造函数、字段或setter方法上。默认情况下,它会根据类型进行匹配。
- @Qualifier:与
@Autowired
结合使用,当有多个相同类型的Bean时,用来指定具体的Bean。 - @Value:用于注入外部配置文件中的属性值,或者直接指定字符串值。
- @Resource:类似于
@Autowired
,但它是基于名字而非类型的匹配,默认情况下。 - @Inject:JSR-330标准中定义的注解,功能类似
@Autowired
,但不支持一些高级特性如required=false。
配置相关注解
- @Configuration:标识一个类为配置类,包含Bean定义的方法。
- @Bean:在
@Configuration
类中使用,标明一个方法返回的对象将被注册为Spring容器中的Bean。 - @ComponentScan:指示Spring扫描指定包下的组件(包括
@Component
,@Service
,@Repository
,@Controller
等),以便将其注册为Spring Bean。 - @PropertySource:加载properties文件到Spring环境中。
- @Import:允许从另一个配置类导入Bean定义。
- @EnableAutoConfiguration:启用自动配置,内部实际上就去加载META-INF/spring.factories文件的信息,然后筛选出以EnableAutoConfiguration为key的数据,加载到IOC容器中,实现自动配置功能!
AOP相关注解
- @Aspect:定义一个切面类。
- @Before, @After, @Around 等:这些注解用于定义切点处的增强逻辑。
其他注解
- @Transactional:声明事务边界,通常用在服务层。
- @RequestMapping:用于映射web请求到处理器方法,包括GET, POST等请求方式。其变体有
@GetMapping
,@PostMapping
等。
@Transactional:
@Transactional(readOnly = true)
实际上就是利用数据库的SET TRANSACTION READ ONLY, 开启只读事务,这意味着在此事务内部,任何修改数据的操作(如 INSERT、UPDATE、DELETE)都将被禁止,只能执行读取操作(如 SELECT);只读事务依然会运用上隔离级别(MVCC),需要事务隔离级别需要一定性能开销。
当执行查询操作时,如果不需要事务的隔离级别和一致性保证,并且不需要使用事务管理的功能,那么不开启事务可能是更为合适的选择。这种情况下,查询操作将立即执行并返回结果,不会受到事务管理和隔离级别的开销影响。
然而,需要注意的是,如果应用需要保证数据的一致性和隔离性,并且希望查询操作与其他事务的修改行为相互独立,那么开启只读事务是必要的。在这种情况下,虽然可能会有一些额外的性能开销,但可以保证数据的一致性和隔离性,避免了并发操作可能引起的数据不一致问题。
Spring事务:
- Spring事务底层是基于数据库事务和AOP机制的。
- 首先对于使用了@Transactional注解的Bean,Spring会创建一个代理对象作为Bean。
- 当调用代理对象的方法时,会先判断该方法上是否加了@Transactional注解。
- 如果加了,那么则利用事务管理器创建一个数据库连接。
- 并且修改数据库连接的autocommit属性为false,禁止此连接的自动提交。
- 然后执行当前方法,方法中会执行sql。
- 执行完当前方法后,如果没有出现异常就直接提交事务。
- 如果出现了异常,并且这个异常是需要回滚的就会回滚事务,否则仍然提交事务。
- Spring事务的隔离级别对应的就是数据库的隔离级别。
- Spring事务的传播机制是Spring事务自己实现的,也是Spring事务中最复杂的,Spring事务的传播机制是基于数据库连接来做的,一个数据库连接一个事务,如果传播机制配置为需要新开一个事务,那么实际上就是先建立一个数据库连接,在此新数据库连接上执行sql。
Spring事务传播:
- REQUIRED(Spring默认的事务传播类型):如果当前没有事务,则自己新建一个事务,如果当前存在事务,则加入这个事务
- SUPPORTS:当前存在事务,则加入当前事务,如果当前没有事务,就以非事务方法执行
- MANDATORY:当前存在事务,则加入当前事务,如果当前事务不存在,则抛出异常。
- REQUIRES_NEW:创建一个新事务,如果存在当前事务,则挂起该事务。
- NOT_SUPPORTED:以非事务方式执行,如果当前存在事务,则挂起当前事务
- NEVER:不使用事务,如果当前事务存在,则抛出异常
- NESTED:如果当前事务存在,则在嵌套事务中执行,否则和REQUIRED的操作一样(开启一个事务)
Spring事务失效:
方法内自调用:Spring事务是基于AOP的,只要使用代理对象调用某个方法时,Spring事务才能生效,而在一个方法中调用使用this.xxx()调用方法时,this并不是代理对象,所以会导致事务失效。
- 解决方法1:把调用方法拆分到另外一个Bean中。
- 解决方法2:自己注入自己。
- 解决方法3:AopContext.currentProxy()+@EnableAspectJAutoProxy(exposeProxy = true)
- 方法是private或final:Spring事务会基于CGLIB来进行AOP,而CGLIB会基于父子类来实现,子类是代理类,父类是被代理类,如果父类中的某个方法是private的,那么子类就没有办法重写它,也就没有办法额外增加Spring事务的逻辑。
- 单独的线程调用方法:当Mybatis或JdbcTemplate执行SQL时,会从ThreadLocal中去获取数据库连接对象,如果开启事务的线程和执行SQL的线程是同一个,那么就能拿到数据库连接对象,如果不是同一个线程,那就拿到不到数据库连接对象,这样,Mybatis或JdbcTemplate就会自己去新建一个数据库连接用来执行SQL,此数据库连接的autocommit为true,那么执行完SQL就会提交,后续再抛异常也就不能再回滚之前已经提交了的SQL了。
- 异常被吃掉:如果Spring事务没有捕获到异常,那么也就不会回滚了,默认情况下Spring会捕获RuntimeException和Error。
- 没加@Configuration注解:如果用SpringBoot基本没有这个问题,但是如果用的Spring,那么可能会有这个问题,这个问题的原因其实也是由于Mybatis或JdbcTemplate会从ThreadLocal中去获取数据库连接,但是ThreadLocal中存储的是一个MAP,MAP的key为DataSource对象,value为连接对象,而如果我们没有在AppConfig上添加@Configuration注解的话,会导致MAP中存的DataSource对象和Mybatis和JdbcTemplate中的DataSource对象不相等,从而也拿不到数据库连接,导致自己去创建数据库连接了。
- 其他:类没有被Spring管理;数据库不支持事务。
自动装配:
自动装配方式:
自动装配需要满足一定的条件,如Bean的定义必须在Spring容器中,且只能有一个匹配的依赖项。如果存在多个匹配的依赖项,可以使用@Qualifier注解或@Primary注解来指定具体的依赖项。
ByName
:在XML配置中,可以使用autowire="byName"来启用byName自动装配。ByType
:在XML配置中,可以使用autowire="byType"来启用byType自动装配。- 构造函数:Spring容器会根据构造函数的参数类型自动将相应的依赖注入到构造函数中。
@Autowired
:使用@Autowired注解,Spring容器会自动将相应的依赖注入到标注了@Autowired的位置。
@Autowired 和 @Resource区别:
- 来源不同:
@Resource
是 Java 定义的注解,它来自于 JSR-250(Java 250 规范提案);@Autowired
是 Spring2.5 定义的注解。 依赖查找顺序不同:依赖注入的功能,是通过先在 Spring IoC 容器中查找对象,再将对象注入引入到当前类中。
@Autowired
注解在查找要注入的 bean 时,首先会按照类型进行匹配。如果有多个匹配的 bean,就会根据名称进行匹配。@Autowired
注解中名称匹配使用 @Qualifier 注解来指定要注入的bean的名称,如果不使用 @Qualifier 注解就会使用属性名。@Resource
注解既没有指定name属性,也没有指定type属性,那么它会默认按照名称来查找对应的bean。- 否则
@Resource
会按配置的name或type去对应查找,并进行bean注入。
- 参数不同:
@Autowired
只支持设置required参数;@Resource
支持name、lookup、type、mappedNames、description、authenticationType、shareable 7个参数。 - 注入方式不同:
@Autowired
支持属性注入、构造方法注入和 Setter 注入,而@Resource
只支持属性注入和 Setter 注入
其他:
AOP:
- 动态代理:Spring AOP是利用的动态代理机制,如果一个Bean实现了接口,那么就会采用JDK动态代理来生成该接口的代理对象,如果一个Bean没有实现接口,那么就会采用CGLIB来生成当前类的一个代理对象。代理对象的作用就是代理原本的Bean对象,代理对象在执行某个方法时,会在该方法的基础上增加一些切面逻辑,使得我们可以利用AOP来实现一些诸如登录校验、权限控制、日志记录等统一功能。
单例模式:
单例Bean:
- 在Spring框架中,在整个应用程序中只存在一个实例的Bean对象。单例Bean的作用是共享和复用对象实例,以提高性能和减少资源消耗。
- Spring容器在启动时会创建单例Bean的实例,并在整个应用程序的生命周期中共享该实例
- 单例Bean适用于需要共享和复用对象实例的场景,例如数据库连接池、线程池等。
- 【同一个class可以在容器中存在2个不同name的实例, 这一点不符合单例设计模式的原则】
单例模式:
- 单例模式是一种设计模式,用于确保一个类只有一个实例,并提供一个全局访问点来获取该实例。单例模式的目的是限制类的实例化次数,以保证全局唯一性和避免资源浪费。
- 常见的方式包括饿汉式(在类加载时就创建实例)、懒汉式(在第一次使用时创建实例)。
- 单例模式适用于需要确保全局唯一性和避免资源浪费的场景,例如配置信息管理、日志记录器等。
Bean的实例化和初始化:
- 实例化:Spring在创建一个Bean对象时,会先创建出来一个Java对象,会通过反射来执行类的构造方法从而得到一个Java对象,而这个过程就是Bean的实例化。
- 初始化:得到Java对象后,会进行依赖注入,依赖注入之后就会进行初始化了,而Bean的初始化就是调用前面创建出来的Java对象中特定的方法,比如Java对象实现了InitializingBean接口,那么初始化的时候就会执行Java对象的afterPropertiesSet(),Spring只会执行这个方法,并不关心方法做了什么。
- 创建bean:推断构造方法-> 实例化-> 填充属性,也就是依赖注入-> 处理Aware回调-> 初始化前,处理@PostConstruct注解-> 初始化,处理InitializingBean接口-> 初始化后,进行AOP
Spring SPI:
spring.factories是SpringBoot SPI机制实现的核心,SPI机制表示扩展机制,所以spring.factories文件的作用就是用来对SpringBoot进行扩展的,SpringBoot在启动的过程中,会找出项目中所有的spring.factories文件,包括jar中spring.factories,从而向Spring容器中添加各个spring.factories文件中指定的ApplicationListener、ApplicationContextInitializer、配置类等组件,使得对SpringBoot做扩展非常容易了,只要引入一个jar,这个jar中有spring.factories文件,就可以把ApplicationListener等添加到Spring容器中。
Spring 自动配置:
在Spring中,我们通常需要去配置很多的Bean:
- 比如用Mybatis,我们要配置SqlSessionFactory的Bean对象
- 用AOP,我们需要配置@EnableAspectJAutoProxy注解
- 用Spring事务,我们需要配置DataSourceTransactionManager的Bean对象
- 用RabbitMQ,我们要配置RabbitTemplate的Bean对象
用SpringBoot时,SpringBoot帮我们配置好了,我们在依赖了spring-boot-starter-web后,会间接的依赖到spring-boot-autoconfigure这个jar,这个jar中都包含了很多的自动配置类:
- RabbitAutoConfiguration
- AopAutoConfiguration
- ElasticsearchDataAutoConfiguration
- DataSourceTransactionManagerAutoConfiguration等等。
Spring循环依赖:
- 构造器注入导致的循环依赖无法解决:如果Bean之间的依赖是通过构造器注入的方式定义的,那么Spring将无法解决这种循环依赖的问题,通常会抛出异常。
字段注入或Setter方法注入:对于使用字段注入或者Setter方法注入造成的循环依赖,Spring可以有效地进行处理。Spring使用三级缓存(singletonObjects、earlySingletonObjects、singletonFactories)来解决单例Bean之间的循环依赖问题。
- 当Spring正在创建一个单例Bean A时,它首先会创建这个Bean的一个原始实例,并将其放入“singletonFactories”缓存中。此时,Bean A还未完全初始化。
- 如果在创建过程中发现需要依赖另一个未完全初始化的Bean B(可能是A依赖B,而B也依赖A),Spring会从缓存中获取Bean A的早期引用,并将其提前暴露给其他Bean(如Bean B)使用。
- 一旦Bean B完成初始化并返回到Bean A的创建过程中,Spring会继续完成Bean A的初始化过程。
- 原型(Prototype)范围的Bean:对于原型范围的Bean,Spring不会尝试去解决循环依赖的问题,因为每次请求原型Bean时都会创建一个新的实例,这使得循环依赖在这种情况下难以管理且没有实际意义。
Spring MVC:
创建
- 在启动Tomcat过程中,会创建DispatcherServlet对象,并执行它的初始化逻辑。
- DispatcherServlet初始化过程中会创建Spring容器(根据用户的Spring配置)。
- 然后初始化过程中还是初始化HandlerMapping、HandlerAdapter等等。
- SpringMVC中默认提供了好几个HandlerMapping,其中有一个为RequestMappingHandlerMapping。
- RequestMappingHandlerMapping的作用是去寻找Spring容器中有哪些加了@RequestMapping的方法。
- 找到这些方法后,就会解析该注解上的信息,包含了指定的path,然后就把path作为key,Method作为value存到一个map中。
执行
- 请求到达DispatcherServlet:当一个HTTP请求到达时,它首先被Spring MVC的核心前端控制器
DispatcherServlet
接收。这个Servlet充当着请求的分发中心。 - HandlerMapping的作用:
DispatcherServlet
并不直接处理请求,而是查询一个或多个HandlerMapping
实例来决定哪个控制器应该处理请求。HandlerMapping
会根据请求的URL或者其他参数来确定合适的控制器。 - 处理器执行:一旦确定了适当的控制器,
DispatcherServlet
将请求转发给该控制器。控制器(也称为处理器)负责处理请求,并返回一个包含视图名称和/或模型数据的ModelAndView
对象。 - 视图解析:得到
ModelAndView
后,DispatcherServlet
接着查询ViewResolver
来找到实际的视图对象。ViewResolver
通过名字找到具体的视图实现,比如JSP页面或者Thymeleaf模板等。 - 视图渲染:一旦找到了正确的视图,
DispatcherServlet
将模型数据传递给视图,然后由视图负责渲染结果并返回给客户端。 - 响应返回客户端:最后,生成的视图结果会被作为HTTP响应返回给客户端。
- 请求到达DispatcherServlet:当一个HTTP请求到达时,它首先被Spring MVC的核心前端控制器
Spring拦截器和过滤器:
区别
概念与作用范围
- 过滤器(Filter):是Servlet规范的一部分,可以用来过滤所有到达Servlet容器的请求。它不仅适用于Spring应用,也适用于任何基于Servlet的应用程序。过滤器可以在请求到达Servlet之前或响应返回客户端之前进行处理,常用于日志记录、权限验证等。
- 拦截器(Interceptor):是Spring MVC框架特有的组件,主要用于拦截处理器(Handler)的执行,也就是控制层的方法调用前后的处理。它更适合于需要与Spring上下文交互的场景,如权限检查、参数校验等。
配置方式
- 过滤器通常通过web.xml文件或者Servlet 3.0+的注解方式进行配置。
- 拦截器则是在Spring配置类中通过实现
HandlerInterceptor
接口并注册到InterceptorRegistry
来配置。
灵活性
- 过滤器的作用范围更广,可以应用于任何类型的请求,而不仅仅是MVC请求。
- 拦截器更加灵活,可以访问Spring MVC框架内部的数据结构,比如可以直接操作ModelAndView对象。
执行顺序
在一个典型的Spring MVC应用中,当一个HTTP请求到来时,其处理流程和执行顺序如下:
- 过滤器链(Filter Chain):首先,请求会经过一系列的过滤器(如果有的话)。这些过滤器按照它们在web.xml中定义的顺序或者通过注解声明的顺序依次执行。每个过滤器可以选择继续向下传递请求给下一个过滤器或者直接向客户端发送响应。
- DispatcherServlet:一旦所有前置过滤器都执行完毕且未终止请求,则请求会被传递给Spring MVC的核心前端控制器
DispatcherServlet
。 - 拦截器链(Interceptor Chain):
DispatcherServlet
接着会将请求分发给合适的处理器(Controller),但在实际调用处理器方法之前,会先执行与该请求匹配的所有拦截器的preHandle
方法。如果所有的preHandle
方法都返回true
,则继续执行处理器方法;否则,跳过后续的拦截器和处理器方法,并可能直接返回响应。 - 处理器方法:处理器(Controller中的方法)被执行。
- 拦截器链(再次):在处理器方法执行之后,拦截器链中的
postHandle
方法被调用,这是按拦截器添加顺序的逆序执行的。此时可以修改模型数据或视图对象。 - 视图渲染:视图被渲染。
- 完成阶段:最后,无论请求处理是否成功,拦截器的
afterCompletion
方法都会被调用,这也是按拦截器添加顺序的逆序执行的。这一步可以用来清理资源等。
Spring注入:
字段注入可能失效的情况
- 代理问题:当使用CGLIB代理而非JDK动态代理时,由于CGLIB是通过继承的方式创建代理对象,对于私有字段的访问可能会遇到问题,尤其是在使用
@Autowired
进行字段注入时,如果代理类尝试直接访问目标类的私有成员变量而不是通过getter/setter方法,这可能导致依赖注入失败。 - 循环依赖:虽然Spring能够处理大多数情况下基于setter或构造器注入的循环依赖问题,但字段注入在这种情况下通常会导致应用启动失败。这是因为字段注入是在对象实例化之后进行的,而Spring无法解决这种情况下产生的循环依赖。
- 非单例作用域:对于原型(prototype)作用域的Bean,字段注入不会自动工作,因为Spring只在单例Bean初始化时执行依赖注入。
- 代理问题:当使用CGLIB代理而非JDK动态代理时,由于CGLIB是通过继承的方式创建代理对象,对于私有字段的访问可能会遇到问题,尤其是在使用
Setter方法注入可能失效的情况:Setter方法注入相对稳定,但在某些特定情况下也可能出现问题:
- 未调用Setter方法:理论上,只要定义了正确的setter方法并正确使用
@Autowired
注解,Spring会自动调用这些方法进行依赖注入。但是,如果你手动创建了一个Bean(即不通过Spring容器),那么Spring将没有机会调用这些setter方法,导致依赖注入失效。 - 循环依赖:如前所述,在涉及循环依赖的情况下,如果两个或多个Bean相互依赖,且都使用setter方法注入,则Spring可以处理这种情况。但如果配置不当,比如忽略了必要的
@Lazy
注解,仍可能导致问题。
- 未调用Setter方法:理论上,只要定义了正确的setter方法并正确使用
构造器注入可能失效的情况:构造器注入被认为是最安全的依赖注入方式,因为它确保了所有必需的依赖都在对象创建时被提供,减少了空指针异常的风险。然而,它也有其局限性:
- 循环依赖:构造器注入不支持直接解决循环依赖的问题。如果两个Bean相互依赖对方,并试图通过构造器注入对方,则会导致应用启动失败。不过,这个问题可以通过使用
@Lazy
注解延迟其中一个依赖的加载来缓解。 - 不可变参数的变化:一旦对象被创建,通过构造器注入的依赖通常是不可变的(final修饰)。这意味着如果需要在对象生命周期内更改某个依赖,构造器注入就不适用了,这时候应该考虑使用setter注入。
- 循环依赖:构造器注入不支持直接解决循环依赖的问题。如果两个Bean相互依赖对方,并试图通过构造器注入对方,则会导致应用启动失败。不过,这个问题可以通过使用