1. IOC和AOP
IoC & AOP 不是 Spring 提出来的,它们在 Spring 之前其实已经存在了,只不过当时更加偏向于理论。Spring 在技术层次将这两个思想进行了很好的实现。
1.1 IOC 控制反转
IoC (Inversion of control )控制反转/反转控制。它是一种思想不是一个技术实现。描述的是:Java 开发领域对象的创建以及管理的问题。
例如:现有类 A 依赖于类 B
- 传统的开发方式 :往往是在类 A 中手动通过 new 关键字来 new 一个 B 的对象出来
- 使用 IoC 思想的开发方式 :不通过 new 关键字来创建对象,而是通过 IoC 容器(Spring 框架) 来帮助我们实例化对象。我们需要哪个对象,直接从 IoC 容器里面过去即可。
从以上两种开发方式的对比来看:我们 “丧失了一个权力” (创建、管理对象的权力),从而也得到了一个好处(不用再考虑对象的创建、管理等一系列的事情)
- 为什么叫控制反转?
控制:指的是对象创建(实例化、管理)的权力,反转:控制权交给外部环境(Spring 框架、IoC 容器) - 解决了什么问题?
IoC 的思想就是两方之间不互相依赖,由第三方容器来管理相关资源。这样有什么好处呢?首先对象之间的耦合度或者说依赖程度降低;然后资源变的容易管理;比如你用 Spring 容器提供的话很容易就可以实现一个单例。
1.2 AOP 面向切面编程
AOP:Aspect oriented programming 面向切面编程,AOP 是 OOP(面向对象编程)的一种延续。
- 切 :指的是横切逻辑,原有业务逻辑代码不动,只能操作横切逻辑代码,所以面向横切逻辑;
- 面 :横切逻辑代码往往要影响的是很多个方法,每个方法如同一个点,多个点构成一个面。
AOP是一种编程思想,是通过预编译方式和运行期动态代理的方式实现不修改源代码的情况下给程序动态统一添加功能的技术。面向对象编程将程序抽象成各个层次的对象,而面向切面编程是将程序抽象成各个切面。所谓切面,相当于应用对象间的横切点,我们可以将其单独抽象为单独的模块。 AOP技术利用一种称为“横切”的技术,剖解开封装对象的内部,将影响多个类的公共行为封装到一个可重用的模块中,并将其命名为切面。所谓的切面,简单来说就是与业务无关,却为业务模块所共同调用的逻辑,将其封装起来便于减少系统的重复代码,降低模块的耦合度,有利用未来的可操作性和可维护性。 利用AOP可以对业务逻辑各个部分进行隔离,从而使业务逻辑各部分之间的耦合度降低,提高程序的可重用性,同时提高开发效率。 AOP可以有多种实现方式,而Spring AOP支持如下两种实现方式。
- JDK动态代理:这是Java提供的动态代理技术,可以在运行时创建接口的代理实例。Spring AOP默认采用这种方式,在接口的代理实例中织入代码。 -
- CGLib动态代理:采用底层的字节码技术,在运行时创建子类代理的实例。当目标对象不存在接口时,Spring AOP就会采用这种方式,在子类实例中织入代码。
在应用场景方面,Spring AOP为IoC的使用提供了更多的便利,一方面,应用可以直接使用AOP的功能,设计应用的横切关注点,把跨越应用程序多个模块的功能抽象出来,并通过简单的AOP的使用,灵活地编制到模块中,比如可以通过AOP实现应用程序中的日志功能。另一方面,在Spring内部,例如事务处理之类的一些支持模块也是通过Spring AOP来实现的。 AOP不能增强的类: 1. Spring AOP只能对IoC容器中的Bean进行增强,对于不受容器管理的对象不能增强。 2. 由于CGLib采用动态创建子类的方式生成代理对象,所以不能对final修饰的类进行代理。
1.2.1 AOP解决的问题
通过上面的分析可以发现,AOP 主要用来解决:在不改变原有业务逻辑的情况下,增强横切逻辑代码,根本上解耦合,避免横切逻辑代码重复。
1.2.2 AOP实现原理
AOP实现:JDK动态代理,CGLIB
在Spring框架中,JDK动态代理和CGLIB是两种常用的代理技术,它们各自有自己的优势和适用场景。下面是对这两种代理技术的比较:
1.2.2.1 代理对象的生成方式
- JDK动态代理:JDK动态代理是Java内建的一种代理机制,它依赖于接口。当你为一个接口创建代理对象时,JDK动态代理会为你生成一个代理类,这个代理类实现了指定的接口,并在运行时动态地将方法调用转发给指定的处理器(InvocationHandler)。
- CGLIB:CGLIB(Code Generation Library)是一个第三方库,它通过继承目标类来创建代理对象。因此,CGLIB代理的对象不需要实现接口。CGLIB在运行时生成目标类的子类,并在子类中拦截对父类方法的调用。
1.2.2.2 所需条件
- JDK动态代理:只能为接口创建代理实例,目标对象必须实现一个或多个接口。
- CGLIB:可以为类创建代理实例,无需接口。但是,由于CGLIB是通过继承实现的,所以目标类不能被final修饰,且目标类中的方法也不能被final修饰,否则无法创建代理。
1.2.2.3 性能
- 在大多数情况下,JDK动态代理的性能略优于CGLIB,因为JDK动态代理直接利用了Java的内建机制,而CGLIB需要额外生成字节码并加载类。
- 但是,如果目标类有大量的方法,并且大部分方法都不需要被代理,那么CGLIB的性能可能会更好,因为它可以只拦截需要代理的方法。
1.2.2.4 使用场景
- JDK动态代理:当目标对象实现了接口,并且希望基于接口进行代理时,应该使用JDK动态代理。在Spring AOP中,如果目标对象实现了接口,则默认使用JDK动态代理。
- CGLIB:当目标对象没有实现接口,或者需要基于类进行代理时,应该使用CGLIB。在Spring AOP中,如果目标对象没有实现接口,Spring会自动切换到使用CGLIB进行代理。
1.2.2.5 整合性
- Spring框架对这两种代理技术都有很好的整合,可以根据目标对象的特点自动选择使用哪种代理方式。
综上所述,JDK动态代理和CGLIB各有其优势和适用场景。在选择使用哪种代理技术时,需要根据目标对象的特性和需求进行权衡。在大多数情况下,Spring框架会自动为你做出最佳选择。
2. Spring Bean的生命周期
Spring Bean生命周期的四大部分以及详细步骤 标准回答 Bean 生命周期大致分为:
- Bean 定义
- Bean 的初始化
- Bean的生存期
- Bean 的销毁
2.1 生命周期
具体步骤如下:
1. Spring启动,查找并加载需要被Spring管理的bean,进行Bean的实例化
2. Bean实例化后对将Bean的引入和值注入到Bean的属性中
3. 如果Bean实现了BeanNameAware接口的话,Spring将Bean的Id传递给setBeanName()方法
4. 如果Bean实现了BeanFactoryAware接口的话,Spring将调用setBeanFactory()方法,将BeanFactory容器实例传入
5. 如果Bean实现了ApplicationContextAware接口的话,Spring将调用Bean的setApplicationContext()方法,将bean所在应用上下文引用传入进来。
6. 如果Bean实现了BeanPostProcessor接口,Spring就将调用他们的postProcessBeforeInitialization()方法。
7. 如果Bean 实现了InitializingBean接口,Spring将调用他们的afterPropertiesSet()方法。类似的,如果bean使用init-method声明了初始化方法,该方法也会被调用
8. 如果Bean 实现了BeanPostProcessor接口,Spring就将调用他们的postProcessAfterInitialization()方法。
9. 此时,Bean已经准备就绪,可以被应用程序使用了。他们将一直驻留在应用上下文中,直到应用上下文被销毁。
10. 如果bean实现了DisposableBean接口,Spring将调用它的destory()接口方法,同样,如果bean使用了destory-method 声明销毁方法,该方法也会被调用。 加分回答 这个过程是由Spring容器自动管理的,其中有两个环节我们可以进行干预。 1. 我们可以自定义初始化方法,并在该方法前增加@PostConstruct注解,届时Spring容器将在调用SetBeanFactory方法之后调用该方法。 2. 我们可以自定义销毁方法,并在该方法前增加@PreDestroy注解,届时Spring容器将在自身销毁前,调用这个方法。
ps:ApplicationContext中的Bean的生命周期
2.2 基于生命周期可以做什么?
Spring Bean的生命周期是指在Spring容器中,一个Bean从创建到销毁的整个过程。这个过程中,Spring框架提供了丰富的生命周期回调方法,允许开发者在Bean的不同生命周期阶段执行特定的操作。基于Spring Bean的生命周期,我们可以做很多事情,以下是一些常见的应用场景:
- 初始化操作:在Bean的初始化阶段,我们可以执行一些必要的设置或启动任务。例如,建立数据库连接、加载配置文件、初始化第三方服务客户端等。这些操作通常通过实现
InitializingBean
接口的afterPropertiesSet
方法或配置XML中的init-method
属性来完成。 - 资源准备:如果Bean需要一些外部资源(如文件、网络连接等),可以在初始化阶段进行资源的准备和检查。确保Bean在运行时能够访问到所需的资源。
- 依赖注入后的处理:在Bean的属性被填充(依赖注入)之后,我们可以执行一些基于这些属性的操作。例如,根据注入的配置值来调整Bean的行为或状态。
- AOP代理的创建:在Bean初始化完成后,Spring会执行
BeanPostProcessor
的后置处理方法postProcessAfterInitialization
。在这个方法中,可以完成AOP代理的创建,从而实现对Bean的切面编程,如日志记录、事务管理等。 - 销毁前的清理:当Bean不再需要时,会进行销毁操作。在销毁前,我们可以执行一些清理工作,如关闭数据库连接、释放占用的资源等。这些操作通常通过实现
DisposableBean
接口的destroy
方法或配置XML中的destroy-method
属性来完成。 - 工厂后处理器的应用:在Spring容器创建Bean的过程中,还可以利用工厂后处理器(如
BeanFactoryPostProcessor
)来对Bean的定义进行修改或增强。例如,可以添加额外的Bean定义、修改Bean的属性值等。
3. Spring的启动过程
3.1 启动
3.2 Spring循环依赖
循环依赖:一个或多个对象实例之间存在直接或间接的依赖关系,这种依赖关系构成了构成一个环形调用。
在Spring框架中,循环依赖是一个常见的问题,它发生在两个或多个bean相互依赖对方的情况,从而形成一个闭环。这种依赖关系在创建bean的过程中可能会导致问题,因为Spring需要确保每个bean在其依赖项被注入之前都是完全可用的。
循环依赖主要分为两种类型:构造器循环依赖和setter循环依赖。
3.2.1 构造器循环依赖
当两个或多个bean通过构造器相互注入时,可能会发生构造器循环依赖。这种情况下,Spring无法解决循环依赖,因为在创建bean的过程中,如果检测到存在循环依赖,Spring会抛出BeanCurrentlyInCreationException异常。这是因为构造器注入需要在bean实例化时立即完成,所以无法等待其他bean的创建完成。
3.2.2 Setter循环依赖
与构造器注入不同,setter注入允许在bean实例化后,通过setter方法注入依赖项。这种方式的灵活性更高,因此Spring能够解决setter循环依赖。Spring使用三级缓存机制来解决setter循环依赖问题:
- 一级缓存(singletonObjects):存放完全初始化且可用的Bean实例,即成品Bean池。
- 二级缓存(earlySingletonObjects):存放尚未完全初始化的Bean实例,即半成品Bean池。当Bean的实例化过程完成后,但属性尚未填充时,会被放入这个缓存中。
- 三级缓存(singletonFactories):存放能够生成Bean实例的ObjectFactory,当Bean的实例化过程还未开始,但创建Bean的工厂已经可用时,会被放入这个缓存中。
当Spring在创建bean时遇到循环依赖,它会尝试从缓存中查找已经创建的bean实例或ObjectFactory。如果找到了,就可以解决循环依赖问题。否则,Spring会继续创建bean,并将其放入相应的缓存中,等待后续的依赖注入。
需要注意的是,虽然Spring可以解决setter循环依赖问题,但它只能解决单例模式下的循环依赖。对于非单例模式(如原型模式),由于每次请求都会创建一个新的实例,所以无法通过缓存来解决循环依赖问题。
总的来说,解决Spring中的循环依赖问题需要谨慎设计系统的依赖关系,尽量避免出现循环依赖的情况。如果确实需要依赖其他bean,可以优先考虑使用setter注入而不是构造器注入,以利用Spring的缓存机制来解决潜在的循环依赖问题。同时,也要注意在设计系统时遵循良好的面向对象设计原则,如单一职责原则、依赖倒置原则等,以降低循环依赖发生的可能性。
4. MyBatis
4.1 MyBatis是什么?
MyBatis 是一个流行的持久层框架,它提供了一种半自动化的数据库操作方式。MyBatis 帮助开发者通过 XML 或注解的方式将 SQL 语句和 Java 对象映射起来,从而简化了数据库访问和数据操作的过程。MyBatis 支持定制化 SQL、存储过程以及高级映射,使得开发者能够更加灵活地处理数据库操作。核心特性如下:
- SQL 和 Java 对象映射:MyBatis 允许开发者通过 XML 文件或注解定义 SQL 语句和 Java 对象之间的映射关系。这样,开发者可以编写具体的 SQL 语句,而不是依赖于框架生成的动态 SQL,从而获得更好的性能和更精确的控制。
- 动态 SQL:MyBatis 支持动态 SQL,这意味着可以根据不同的条件构建复杂的 SQL 语句。MyBatis 使用 XML 文件中的
<if>
、<choose>
、<foreach>
等标签来构建动态 SQL。 - 结果集映射:MyBatis 可以将数据库查询的结果集映射到 Java 对象中。开发者可以定义结果集的映射规则,MyBatis 会负责将每一行数据映射到相应的 Java 对象属性上。
- 缓存机制:MyBatis 提供了一级和二级缓存,以减少对数据库的重复查询。这可以显著提高应用程序的性能,尤其是在读取操作频繁的场景下。
- 事务管理:MyBatis 支持事务管理,可以与 Spring 等框架集成,使用声明式事务管理或编程式事务管理来控制事务的边界和行为。
- 连接池管理:MyBatis 可以与多种连接池(如 Apache Commons DBCP、C3P0 等)集成,提供数据库连接的快速获取和释放。
4.2 常见问题
4.2.1 使用方式
4.2.2 #{}和${}的区别
${}是字符串替换,#{}是预处理;使用#{}可以有效的防止SQL注入,提高系统安全性。
Mybatis在处理${}时,就是把${}直接替换成变量的值。而Mybatis在处理#{}时,会对sql语句进行预处理,将sql中的#{}替换为?号,调用PreparedStatement的set方法来赋值;
4.2.3 XML文件和DAO接口的关系
Dao接口,就是人们常说的Mapper接口;接口的全限名,就是映射文件中的namespace的值;接口的方法名,就是映射文件中MappedStatement的id值;接口方法内的参数,就是传递给sql的参数。Mapper接口是没有实现类的,当调用接口方法时,接口全限名+方法名拼接字符串作为key值,可唯一定位一个MappedStatement,举例:com.mybatis3.mappers.StudentDao.findStudentById,可以唯一找到namespace为com.mybatis3.mappers.StudentDao下面id = findStudentById的MappedStatement。在Mybatis中,每一个<select>、<insert>、<update>、<delete>标签,都会被解析为一个MappedStatement对象。
Dao接口里的方法,是不能重载的,因为是 全限名+方法名 的保存和寻找策略。Dao接口的工作原理是JDK动态代理;Mybatis运行时会使用JDK动态代理为Dao接口生成代理proxy对象,代理对象proxy会拦截接口方法,转而执行MappedStatement所代表的sql,然后将sql执行结果返回。
4.2.4 一级、二级缓存
MyBatis 提供了一级缓存和二级缓存机制,这些缓存机制旨在减少对数据库的查询次数,从而提高应用程序的性能。下面是对一级缓存和二级缓存的简要说明以及它们如何工作:
4.2.4.1 一级缓存(本地会话缓存)
一级缓存是 MyBatis 默认开启的缓存,它是每个 SqlSession 的本地缓存。当同一个 SqlSession 进行数据库操作时,它会首先查询一级缓存,如果缓存中有相应的数据,则直接返回缓存中的数据,而不需要查询数据库。
一级缓存的生命周期与 SqlSession 相同,当 SqlSession 被关闭时,其一级缓存也会被清空。
4.2.4.2 二级缓存(全局会话缓存)
二级缓存是一个全局的缓存,可以被同一个 MyBatis 映射器配置中的所有 SqlSession 共享。这意味着,如果多个 SqlSession 操作相同的数据,它们可以共享二级缓存中的数据,从而减少对数据库的查询。
二级缓存是可配置的,需要在 MyBatis 配置文件中显式开启。此外,二级缓存依赖于具体的实现,MyBatis 本身只定义了二级缓存的接口,实际的缓存实现(如 Ehcache)需要单独引入。
4.2.4.3 缓存失效
尽管 MyBatis 的缓存机制可以显著减少数据库查询,但它们并不是永久有效的。缓存可能会因为以下原因而失效:
- **一级缓存**:当 SqlSession 被提交(commit)或回滚(rollback)时,一级缓存会被清空。
- **二级缓存**:可以通过配置设置缓存的超时时间,超过这个时间后缓存会自动失效。此外,显式的调用 `clearCache` 方法也会清空二级缓存。
4.2.4.4 注意事项
- 缓存主要用于读取操作。对于写入操作(插入、更新、删除),MyBatis 会自动使缓存中的数据失效,以确保数据的一致性。
- 并不是所有的查询都能从缓存中受益。例如,使用 `distinct`、`groupBy`、`having`、`orderBy` 等子句的查询可能不会使用缓存。
- 缓存配置需要谨慎处理,以避免数据不一致的问题。
总的来说,MyBatis 的一级缓存和二级缓存可以在很多情况下避免数据库查询,但它们并不是万能的。开发者需要根据实际的应用场景和需求来决定是否使用缓存,以及如何配置和管理缓存。
4.2 Spring中的事务
4.2.1 事务实现的方式
-
声明式事务管理(Declarative Transaction Management)
声明式事务管理是Spring中推荐使用的事务管理方式,它基于AOP(面向切面编程)原理,通过注解或XML配置来声明事务的边界和属性,无需手动编写事务管理代码,简化了开发过程。如果你希望事务管理尽可能简单,并且大多数事务场景都相似,那么声明式事务管理是最佳选择。它通过注解或XML配置减少了编码工作量,并且易于理解和维护。
注解方式:@Transactional
这是最常用的注解,可以将其添加到类声明或方法上。通过这个注解,你可以指定事务的传播行为、隔离级别、超时时间等属性。
@Service
public class MyService {
@Transactional(readOnly = true)
public MyEntity findEntityById(Long id) {
// ...
}
}
XML配置方式:在Spring的XML配置文件中,你可以使用<tx:advice>
和<aop:config>
标签来声明事务属性和通知。
<tx:advice transaction-manager="transactionManager" id="txAdvice">
<tx:attributes>
<tx:method name="find*" read-only="true"/>
</tx:attributes>
</tx:advice>
<aop:config>
<aop:pointcut id="pc1" expression="execution(* com.example.*.*(..))"/>
<aop:advisor advice-ref="txAdvice" pointcut-ref="pc1"/>
</aop:config>
-
编程式事务管理(Programmatic Transaction Management)
编程式事务管理提供了更高的灵活性,但需要手动编写代码来管理事务的开启、提交和回滚。这种方式不依赖于AOP,而是通过使用PlatformTransactionManager
接口及其实现类来编程式地管理事务。在Spring中,你可以通过注入PlatformTransactionManager
实例来编程式地管理事务。如果你需要对事务进行细粒度的控制,或者你的事务管理需求非常复杂,那么编程式事务管理提供了更多的灵活性。通过编程方式,你可以根据业务逻辑的需要动态地管理事务。
@Service
public class MyService {
@Autowired
private PlatformTransactionManager transactionManager;
public void performTransaction() {
TransactionDefinition def = new DefaultTransactionDefinition();
TransactionStatus status = transactionManager.getTransaction(def);
try {
// 执行业务逻辑
transactionManager.commit(status);
} catch (Exception e) {
transactionManager.rollback(status);
throw e;
}
}
}
4.2.2 事务不生效的场景
场景 | 详情 |
方法自调用 | 对于声明式事务,Spring使用AOP(面向切面编程)来管理事务。如果你在一个@Transactional 注解的方法中直接调用自身,那么这个调用不会通过代理对象,因此事务不会生效。 |
异常被捕获 | 如果@Transactional 方法中的异常被try-catch块捕获并且没有重新抛出,那么Spring事务管理器将不会感知到异常的发生,因此不会触发回滚。 |
方法非public | 由于Spring AOP代理是基于接口的,只有public方法才能被代理。如果你在一个类中使用了@Transactional 注解了一个非public方法,那么这个注解将不会生效。 |
非运行时异常 | 默认情况下,@Transactional 只回滚RuntimeException 及其子类的异常。如果你的方法抛出了其他类型的异常(例如Exception ),并且没有通过rollbackFor 属性指定,那么事务不会回滚。 |
异步事务 | 使用 |
错误的事务传播行为 | Spring定义了多种事务传播行为,如REQUIRED 、REQUIRES_NEW 等。如果选择了错误的传播行为,可能会导致事务不按预期执行。例如,如果你在一个已经存在事务的方法中使用REQUIRES_NEW ,那么它会创建一个新的事务,而不是加入当前事务。 |
方法被private 或final 修饰 | private 方法不会被代理,因此不会应用事务逻辑。final 方法也不能被子类重写,如果你使用了CGLIB代理,那么final 方法上的@Transactional 注解也不会生效。 |
当前类没有被Spring容器托管 | 如果你的类没有通过@Service 、@Repository 等注解标记为Spring管理的Bean,那么Spring事务管理器将无法管理这个类的事务。 |
数据库不支持事务 | 如果你使用的数据库存储引擎不支持事务(例如MySQL的MyISAM),那么即使在Spring中配置了事务管理,也无法实现事务功能。 |
4.2.3 事务的传播属性
事务的传播属性是Spring框架中的概念,它定义了事务如何在多个方法之间传播和嵌套。事务传播属性的主要作用是在方法调用时,确定事务的创建和加入行为。这有助于开发者在复杂的业务逻辑中,精确控制事务的边界和行为,确保数据的一致性和完整性。
通过一个具体的例子来说明Spring事务传播属性的作用。假设我们有一个银行转账的场景,其中包含两个关键步骤:从一个账户扣除金额(debitAccount
),然后将金额添加到另一个账户(creditAccount
)。为了保证数据的一致性,这两个步骤必须在同一个事务中执行。
首先,我们定义一个服务类AccountService
,其中包含两个方法:transferFunds
用于执行转账操作,debitAccount
和creditAccount
分别用于扣款和存款。
@Service
public class AccountService {
@Autowired
private AccountRepository accountRepository;
@Transactional
public void transferFunds(String fromAccount, String toAccount, BigDecimal amount) {
debitAccount(fromAccount, amount);
creditAccount(toAccount, amount);
}
public void debitAccount(String account, BigDecimal amount) {
// 扣款逻辑
accountRepository.debit(account, amount);
}
public void creditAccount(String account, BigDecimal amount) {
// 存款逻辑
accountRepository.credit(account, amount);
}
}
传播属性 | 详情 | 实例 |
PROPAGATION_REQUIRED | 如果当前没有事务,就新建一个事务。如果已存在事务,就加入该事务,这是默认的传播行为。 | 如果transferFunds 方法被另一个@Transactional 注解的方法调用,那么这两个方法将共享同一个事务。如果transferFunds 是第一个被调用的方法,它将创建一个新的事务。 |
PROPAGATION_SUPPORTS | 支持当前事务,如果当前存在事务则加入,不存在则按非事务方式执行。 | 如果transferFunds 在一个事务环境中被调用(外面调用的方法申明是事务),它将加入这个事务。如果没有事务环境,debitAccount 和creditAccount 将不在一个事务中执行。这可能导致数据不一致,例如,如果debitAccount 成功执行,但creditAccount 失败,那么资金将被扣除但不会添加到目标账户。 |
PROPAGATION_MANDATORY | 强制加入当前事务,如果不存在则抛出异常。 | |
PROPAGATION_REQUIRES_NEW | 创建一个新的事务,如果存在当前事务,将当前事务挂起。 | 每次调用transferFunds 时,它都会创建一个新的事务,不管外部是否有事务。这意味着debitAccount 和creditAccount 将在独立的事务中执行。如果transferFunds 被另一个事务方法调用,外部事务将被挂起,直到transferFunds 完成其事务。 |
PROPAGATION_NOT_SUPPORTED | 以非事务方式运行,如果存在当前事务,将当前事务挂起。 | |
PROPAGATION_NEVER | 以非事务方式运行,如果存在当前事务,则抛出异常。 | |
PROPAGATION_NESTED | 如果当前事务存在,则在一个嵌套事务内执行;如果当前事务不存在,则表现与PROPAGATION_REQUIRED相同。 | transferFunds 将创建一个嵌套事务。如果外部存在事务,那么debitAccount 和creditAccount 将在嵌套事务中执行,允许它们独立回滚而不影响外部事务。如果没有外部事务,这将表现得像PROPAGATION_REQUIRED 。 |
通过一个具体的例子来说明Spring事务传播属性的作用。假设我们有一个银行转账的场景,其中包含两个关键步骤:从一个账户扣除金额(debitAccount
),然后将金额添加到另一个账户(creditAccount
)。为了保证数据的一致性,这两个步骤必须在同一个事务中执行。
首先,我们定义一个服务类AccountService
,其中包含两个方法:transferFunds
用于执行转账操作,debitAccount
和creditAccount
分别用于扣款和存款。
@Service
public class AccountService {
@Autowired
private AccountRepository accountRepository;
@Transactional
public void transferFunds(String fromAccount, String toAccount, BigDecimal amount) {
debitAccount(fromAccount, amount);
creditAccount(toAccount, amount);
}
public void debitAccount(String account, BigDecimal amount) {
// 扣款逻辑
accountRepository.debit(account, amount);
}
public void creditAccount(String account, BigDecimal amount) {
// 存款逻辑
accountRepository.credit(account, amount);
}
}
5. SpringBoot
5.1 启动过程
5.1.1 启动完成后立即启动
@Slf4j
@Configuration
@Order(Ordered.HIGHEST_PRECEDENCE)
@AllArgsConstructor
public class MailSenderListener implements ApplicationListener<ApplicationStartedEvent> {
private MailSenderConfig mailSenderConfig;
@SneakyThrows
@Override
public void onApplicationEvent(ApplicationStartedEvent event) {
this.mailSenderConfig.buildMailSender();
}
}
5.1.2 @PostConstruct
@PostConstruct是JDK所提供的注解,使用该注解的方法会在服务器加载Servlet的时候运行。也可以在一个类中写多个方法并添加这个注解。
import org.springframework.stereotype.Component;
import javax.annotation.PostConstruct;
@Component
public class PostConstructTest {
@PostConstruct
public void start() {
System.out.println("@PostConstruct方法执行");
}
@PostConstruct
public void start01() {
System.out.println("@PostConstruct1111方法执行");
}
}
5.2 常用注解
5.3 自定义注解
自定义注解步骤如下:
5.3.1 引入依赖
首先,确保你的Spring Boot项目中已经引入了必要的依赖,比如spring-boot-starter-aop
,因为自定义注解经常与AOP(面向切面编程)一起使用。
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-aop</artifactId>
</dependency>
5.3.2 定义注解
使用@interface
关键字来定义一个Java注解。你可以添加元注解来指定注解的适用范围、生命周期等信息。
@Target({ElementType.METHOD, ElementType.PARAMETER}) // 指定注解可以应用的地方
@Retention(RetentionPolicy.RUNTIME) // 指定注解的生命周期
@Documented // 指示该注解是否包含在JavaDoc中
public @interface MyCustomAnnotation {
// 定义注解的属性
String value() default "";
// 可以定义更多的属性...
}
在这个例子中,我们定义了一个名为MyCustomAnnotation
的注解,它有一个名为value
的属性,该属性有一个默认值。
5.3.3 使用注解
在你的代码中使用自定义的注解。例如,你可以将其应用于方法或参数。
@MyCustomAnnotation(value = "example")
public void myMethod() {
// 方法体
}
5.3.4 处理注解
为了让注解起作用,你通常需要编写一个切面(Aspect)或其他类型的处理器来读取和处理注解。在Spring AOP中,你可以创建一个切面,并在其中使用@annotation
切点表达式来匹配带有特定注解的方法。
@Aspect
@Component
public class MyCustomAnnotationAspect {
@Around("@annotation(com.example.MyCustomAnnotation)")
public Object handleCustomAnnotation(ProceedingJoinPoint joinPoint) throws Throwable {
// 在方法执行前后添加逻辑
Object result = joinPoint.proceed();
return result;
}
}
在这个切面中,@Around
注解表示这是一个环绕通知,它会在带有MyCustomAnnotation
注解的方法执行前后执行特定的逻辑。
5.3.5 启用AOP
确保Spring Boot应用启用了AOP支持。如果你使用了@EnableAspectJAutoProxy
注解或者spring-boot-starter-aop
依赖,那么AOP应该已经自动配置了。
5.4 自定义spring-boot-starter
步骤 | 详情 |
创建Maven项目 | 创建一个Maven工程,这是自定义starter的基础。 |
定义业务逻辑 | 在Maven项目中,定义自己的业务逻辑。这通常包括一些服务类、工具类、配置类等。这些类将作为starter的核心功能。 |
创建自动配置类 | 为了实现starter的自动配置功能,需要创建一个自动配置类。这个类通常使用@Configuration 注解进行标记,并使用@Bean 注解来定义需要创建的Bean。这些Bean将自动被Spring容器管理,并在需要时注入到其他类中。 |
添加配置属性 | 如果starter需要一些配置属性,你可以在application.properties 或application.yml 文件中添加它们,并使用@ConfigurationProperties 注解将属性绑定到相应的配置类中。 |
创建META-INF/spring.factories文件 | 在Maven项目的src/main/resources/META-INF 目录下,创建一个名为spring.factories 的文件。这个文件用于指定自动配置类的位置,让Spring Boot在启动时能够找到并加载这些配置。 |
构建和安装starter | 使用Maven构建你的starter,并安装到本地仓库或远程仓库中。这样,其他项目就可以通过Maven依赖的方式引入你的starter了。 |
在其他项目中使用starter | 在其他Spring Boot项目中,只需要在pom.xml 文件中添加对你自定义starter的依赖,然后在需要的地方使用starter提供的功能即可。 |
6. Spring中的设计模式
6.1 单例模式
Spring 中的 Bean 默认是单例的。
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@Configuration
public class AppConfig {
@Bean
public MyService myService() {
return new MyServiceImpl(); // 默认是单例
}
}
6.2 工厂模式
Spring 使用工厂模式来创建和管理对象,例如通过 ApplicationContext
获取 Bean。
import org.springframework.context.ApplicationContext;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
public class FactoryPatternExample {
public static void main(String[] args) {
ApplicationContext context = new AnnotationConfigApplicationContext("com.example.config");
MyService myService = context.getBean(MyService.class);
myService.doSomething();
}
}
6.3 代理模式
Spring AOP 使用代理模式来实现面向切面编程。
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import org.springframework.stereotype.Component;
@Aspect
@Component
public class LoggingAspect {
@Before("execution(* com.example.service.*.*(..))")
public void logBefore() {
System.out.println("Logging before method execution");
}
}
6.4 观察者模式
Spring 的事件机制基于观察者模式。
import org.springframework.context.ApplicationEvent;
import org.springframework.context.ApplicationListener;
import org.springframework.context.support.AbstractApplicationContext;
import org.springframework.context.support.ClassPathXmlApplicationContext;
public class ObserverPatternExample {
public static void main(String[] args) {
AbstractApplicationContext context = new ClassPathXmlApplicationContext("applicationContext.xml");
context.publishEvent(new CustomEvent("Custom event occurred"));
context.registerListener(new CustomEventListener());
context.close();
}
}
class CustomEvent extends ApplicationEvent {
public CustomEvent(Object source) {
super(source);
}
}
class CustomEventListener implements ApplicationListener<CustomEvent> {
@Override
public void onApplicationEvent(CustomEvent event) {
System.out.println("Received custom event: " + event.getSource());
}
}
6.5 策略模式
Spring 的资源加载机制(例如不同的数据源配置)可以使用策略模式来实现。
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.jdbc.datasource.DriverManagerDataSource;
@Configuration
public class DataSourceConfig {
@Bean
public DataSource dataSource() {
DriverManagerDataSource dataSource = new DriverManagerDataSource();
dataSource.setDriverClassName("com.mysql.jdbc.Driver");
dataSource.setUrl("jdbc:mysql://localhost:3306/mydb");
dataSource.setUsername("user");
dataSource.setPassword("pass");
return dataSource;
}
}