IOC
在 Java 基础中,我们往往使用 new 关键字来完成服务对象的创建。举个例子,我们有很多的 U 盘,它们都能够存储计算机的数据,但是它们可能来自不同的品牌,有金士顿(KingstonUSBDisk)的、闪迪(SanUSBDisk)的,或者其他满足 U 盘接口(USBDisk)规范的。如果我们用 new 方法,那么就意味着我们的接口只能用于某种特定品牌的 U 盘。
USBDisk usbDisk = new KingstonUSBDisk();
通过上面的操作,USBDisk 和 KingstonUSBDisk 就形成了耦合。换句话说,如果想用闪迪的 U 盘我们需要修改源码才行。如果未来有更先进的 U 盘,那就要修改源码了,大型系统的资源多达成百上千,如果都采用这样的方式,系统会造成严重的耦合,不利于维护和扩展。
这个时候 IOC 理念来了,首先它不是一种技术,而是一种理念。假设我们不采用 new 方法,而是采用一种描述的方式,每一个 U 盘都有一段自己的描述,通过接口我们可以读入这些信息,根据这些信息注入对应的 U 盘,这样我们在维护源码的时候只需要去描述这些信息并且提供对应的服务即可,不需要去改动源码了。
仍以 U 盘为例,如果用的是闪迪 U 盘,那么在信息描述段给出的是闪迪 U 盘,系统就会根据这个信息去匹配对应的 实现类,而无需用 new 方法去生成实现类。同样,如果用的是金士顿 U 盘,那么在信息描述段给出的就是金士顿,系统也会自动生成对应的服务注入到我们的系统中,而我们只需要通过描述就能获得对应资源,无需自己用 new 方法去创建资源和服务。
从上面的描述可以知道,我们往 Spring 中注入资源往往是通过描述来实现的,在 Spring 中往往是注解或者 XML 描述。Spring 中的 IOC 注入方法分为下面这 3 种:
1.构造方法注入。
2.setter 方法注入。
3.接口注入。
构造方法注入是依靠类的构造去实现的,对于一些参数较少的对象可以使用这个方式注入。比如角色类(Role),它的构造方法中包含三个属性:编号(id)、角色名称(roleName)和备注(note)。我们需要进行如下代码清单来构建它:
<bean id="role" class="com.learn.mybatis.chapter8.pojo.Role">
<constructor-arg index="0" value="1"/>
<constructor-arg index="1" value="CEO"/>
<constructor-arg index="2" value="公司老大"/>
</bean>
这样我们就描述了一个 Role,它可以注入到其他的资源中。但是如果构造方法多,显然构造注入不是一个很好的方法,而 Spring 更加推进使用 setter 注入。假设上例角色类还有一个没有参数的构造方法,它的三个属性,编号(id)、角色名称(roleName)和备注(note)都有 setter 方法,那么我们可以使用 setter 注入,如下代码清单所示:
<bean id="role" class="com.learn.mybatis.chapter8.pojo.Role">
<property name="id" value="1"/>
<property name="roleName" value="CEO"/>
<property name="note" value="公司老大"/>
</bean>
使用 setter 注入更加灵活,因为使用构造方法,会受到构造方法的参数个数、顺序这些因素干扰。侵入更加少,所以这是 Spring 首选的注入方式。
Spring 的接口注入方式。它是一种注入其他服务的接口,比如 JNDI 数据源的注入,在 Tomcat 或者其他的服务器中往往配置了 JNDI 数据源,那么就可以使用接口注入我们需要的资源,如下代码清单所示:
<bean id="dataSource" class="org.springframework.jndi.JndiObjectFactoryBean">
<property name="jndiName">
<value>java:comp/env/jdbc/mybatis</value>
</property>
</bean>
它允许你从一个远程服务中注入一些服务到本地调用。在大型系统中,我们往往还会使用注解注入的方式来描述系统服务之间的关系,这也是 Spring 所推荐的方式。
AOP
Spring AOP 是通过动态代理来实现的。首先在传统的 MVC 架构中,业务层一般都夹带着数据库的事务管理,例如,插入一个角色,它是使用 RoleService 接口的实现类 RoleServiceImpl 去实现的,如代码清单所示:
@Service
public class RoleServiceImpl implements RoleService {
@Autowired
private RoleDao roleDao;
@Override
@Transactional(isolation=Isolation.READ_COMMITED, propagation=Propagation.REQUIRED)
public int insertRole(Role role) {
return roleDao.insertRole(role);
}
}
当程序进入到 insertRole 方法的时候,Spring 就会读取配置的传播行为进行设置,这里的配置为 Propagation.REQUIRED,它的意思是当前方法如果有事务则加入当前事务,否则就创建新的事务。这样这个 insertRole 方法就在事务内调用了。
Spring AOP 实际上就是一个动态代理的典范。现在以角色服务类(RoleServiceImpl)为例。
首先 Spring 可以生成代理对象,这样调度 insertRole 方法的时候就进入了一个 invoke 方法里面。Spring 会判断到底要不要拦截这个方法,这是一个切入点的配置问题,它是通过正则式匹配的,比如我们在正则式配置 insert * 这样的统配,那么 Spring 就会拦截这个 insertRole()方法,否则就不拦截,直接通过 invoke 方法反射调用这个方法,就结束了。这便是切入点的概念。
其次就是切面。切面是干什么的?它是插入角色的,里面包含事务,而事务就是整个方法的一个切面,可能你的方法会很复杂,包含业务、财务和日志等多方面,而它们都受到同一个事务管辖,那么事务就是这方法的一个切面。这个时候 Spring 就会根据我们配置的信息,知道这个方法需要事务,采用传播行为 Propagation.REQUIRED 运行方法,这就是切面。
再次就是连接点。连接点是在程序运行中根据不同的通知来实现的程序段。由于 Spring 使用动态代理,我们在反射原始的方法之前可以做一些事情,于是有了前置通知(Before advice),也可以在反射之后做一些事情,那便是后置通知(After advice),反射原来的方法可能正确返回,也可能因此抛出异常,所以还有正常返回后通知(After return advice)和产生异常的抛出异常后通知(After throwing advice)。也有可能需要用自定义方法取代原有的方法,就如 MyBatis 的插件一样,不采用原有的 invoke 方法而是使用自定义的方法,所以还有环绕通知(Around advice)。
AOP 代理(AOP Proxy)就是指采用何种方式进行代理,JDK 的代理需要使用接口,而 CGLIB 则不需要,因此在默认的情况下 Spring 采用这样的规则。当 Spring 的服务包含接口描述时采用 JDK 动态代理,否则采用 CGLIB 代理。可以通过配置修改它们。
Spring AOP 在动态代理下运行的流程:
相关术语释义
Aspect:切面,切面一般定义为一个 Java 类,每个切面侧重于特定的跨领域功能,比如,事务管理或者日志打印等。
JoinPoint:连接点,程序执行的某个点,比如方法执行。构造函数调用或者字段赋值等。在 Spring AOP 中,连接点只会有方法调用(Method execution)。
Pointcut:切入点,一个匹配连接点的正则表达式。当一个连接点匹配到切点时,一个关联到这个切点的特定的通知 (Advice) 会被执行。
Advice:通知,在连接点要执行的代码。
Weaving:编织,负责将切面和目标对象链接,以创建统计对象,在 Spring AOP 中不存在这个东西。
1. 什么是 Spring AOP?
Spring AOP 支持在 Spring 应用程序中进行面向切面的编程。在 AOP 中,各切面可以实现关注点的模块化,例如事务管理、日志记录和跨多种类型和对象的安全性(通常称为横切关注点)。
AOP 提供了一种使用简单的可插拔配置在实际逻辑之前、之后或周围动态添加横切关注点的方式。这使得维护代码变得更加容易。通过更改配置文件来添加 / 删除关注点,而无需重新编译完整的源代码(要求使用 XML 配置的方法)。
2. 什么是通知,连接点,切入点?
- AOP 中一个重要术语是通知。它是切面在特定的连接点处采取的操作。
- 连接点是程序的执行点,例如方法的执行或异常的处理。在 Spring AOP 中,连接点始终代表方法的执行。
- 切入点是匹配连接点的谓词或表达式。
- 通知与切入点表达式关联,并在该切入点匹配的任何连接点处运行。
- Spring 默认使用 AspectJ 切入点表达式。
3. AOP 通知类型
- 前置通知(Before advice):在连接点之前执行通知,前置通知不会影响连接点的执行,除非此处抛出异常。
- 正常返回通知(After returning advice):在连接点正常执行后执行,如果连接点抛出异常,则不会执行。
- 异常返回通知(After throwing advie):在连接点抛出异常后执行。
- 返回通知(After(finally) advice):在连接点执行完成后执行,不管是正常执行完成,还是抛出异常,都会执行返回通知中的内容。
- 环绕通知(Around advice):环绕通知在连接点前后,比如一个方法调用的前后。这是强大的通知类型,能在方法调用前后自定义一些操作。环绕通知还需要负责决定是继续处理 join point(调用 ProceedingJoinPoint 的 proceed 方法)还是中断执行。
事务
事务,就是一组操作数据库的动作集合。事务是现代数据库理论中的核心概念之一。如果一组处理步骤或者全部发生或者一步也不执行,我们称该组处理步骤为一个事务。当所有的步骤像一个操作一样被完整地执行,我们称该事务被提交。由于其中的一部分或多步执行失败,导致没有步骤被提交,则事务必须回滚到最初的系统状态。
Spring AOP 动态代理下消息执行过程
事务传播行为
传播行为,是指方法之间的调用问题。在大部分的情况下,我们认为事务都应该是一次性全部成功或者全部失败的。例如,业务做成功了,但是财务没有合乎规范,被财务部否决了,这个时候就需要回滚所有的事务。但是也会有特殊的场景,比如信用卡还款,在还款过程中,我们有一个总的程序代码,循环调用一个 repayCreditCard 的还款方法,进行还款处理,但是我们发现其中的一张卡发送了异常,这时我们不能把所有执行过的信用卡数据回滚,而只能回滚出现异常的这张卡。如果将所有执行过还款操作的信用卡回滚,那么就意味着之前按时还款的用户也被认为是不按时还款的,这显然不合理。换句话说,我们在做每一张卡操作的时候都希望有一个独立的事务管理它,使得每一张卡的还款互不干扰。
在 Spring 中定义了 7 中传播行为:
支持当前事务的情况:
PROPAGATION_REQUIRED: 如果当前存在事务,则加入该事务;如果当前没有事务,则创建一个新的事务。(这是 Spring 默认的传播行为)
PROPAGATION_SUPPORTS:如果当前存在事务,则加入该事务;如果当前没有事务,则以非事务的方式继续运行。
PROPAGATION_MANDATORY:如果当前存在事务,则加入该事务;如果当前没有事务,则抛出异常。(mandatory:强制性)
不支持当前事务的情况:
PROPAGATION_REQUIRES_NEW:总是开启一个新的事务。如果当前存在事务,则把当前事务挂起。(在信用卡场景中,我们往往需要这个传播行为为每一个卡创建独立的事务)
PROPAGATION_NOT_SUPPORTED:总是以非事务方式运行,如果当前存在事务,则把当前事务挂起。
PROPAGATION_NEVER:总是以非事务方式运行,如果当前存在事务,则抛出异常。
其他情况:
PROPAGATION_NESTED:如果当前存在事务,则创建一个事务作为当前事务的嵌套事务来运行;如果当前没有事务,则按照 TransactionDefinition.PROPAGATION_REQUIRED 属性执行。
@Transactional(rollbackFor = Exception.class)注解
Exception 分为运行时异常 RuntimeException 和非运行时异常。事务管理对于企业应用来说是至关重要的,即使出现异常情况,它也可以保证数据的一致性。
当 @Transactional
注解作用于类上时,该类的所有 public 方法将都具有该类型的事务属性,同时,我们也可以在方法级别使用该标注来覆盖类级别的定义。如果类或者方法加了这个注解,那么这个类里面的方法抛出异常,就会回滚,数据库里面的数据也会回滚。
在 @Transactional
注解中如果不配置 rollbackFor
属性, 那么事物只会在遇到 RuntimeException
的时候才会回滚, 加上 rollbackFor=Exception.class
, 可以让事物在遇到非运行时异常时也回滚。
我们应该注意自调用问题,什么是自调用呢?比如说我们的角色服务类有两个方法,分别是 insertRoleList 方法和 insertRole 方法,而 insertRole 方法注解为 PROPAGATION_REQUIRES_NEW,如下代码清单所示:
@Service
public class RoleServiceImpl {
@Transactional(isolation=Isolation.READ_COMMITED, propagation=Propagation.REQUIRED)
public int insertRoleList(List<Role> roleList) {
for (Role role : roleList) {
this.insertRole(role); //insertRole 的注解失效
}
}
@Transactional(isolation=Isolation.READ_COMMITED, propagation=Propagation.REQUIRED_NEW)
public int insertRole(Role role) {
try {
return roleDAO.insertRole(role);
} catch(Exception e) {
e.printStackTrace();
}
return 0;
}
}
无效的传播行为
所谓自调用就是自己的类方法调用其他方法的过程。如 insertRoleList 调用了 insertRole 方法,而这里注解 insertRole 为 REQUIRES_NEW,每次调用方法的时候,会生成独立事务。但实际上是不生效的,为什么呢?
Spring 的数据库事务是在动态代理进入到一个 invoke 方法里面的,然后判断是否需要拦截方法,需要的时候才根据注解和配置生成数据库事务切面上下文,而这里的自调用是没有代理对象的,是原始对象的调用,所以根本就没有 invoke 方法去解析注解和配置生成数据库切面的上下文,独立事务也无从谈起,Spring 只会延续使用 insertRoleList 的上下文信息,所以这个注解是无效的。如果需要这样的功能,只能独立写一个类,再去调用 insertRole 方法,因为在另外一个类里面,你得到的是 RoleServiceImpl 的代理类,进入它的 invoke 方法的时候它会去解析注解,知道你需要一个独立事务。
举例说明:
ServiceA {
void methodA() {
ServiceB.methodB();
}
}
ServiceB {
void methodB() {
}
}
如果当前执行的事务不在另外一个事务里,就新起一个事务;ServiceB 和 ServiceA 在同一个事务里面,ServiceB 如果异常,则整个事务认为是执行失败的,即便是在 A 里面 try catch 了异常也会导致 A 和 B 都回滚;同样,即便 B 执行成功,A 执行报错产生异常,那么 A 和 B 都会回滚的;
新建事务, 假设当前存在事务, 把当前事务挂起; 比如服务 A 的事务级别是 PROPAGATION_REQUIRED,那么服务 B 的级别是 PROPAGATION_REQUIRES_NEW; 那么当运行到 ServiceB.methodB 的时候, ServiceA.methodA 所在的事务就会挂起。ServiceB.methodB 会起一个新的事务, 等待 ServiceB.methodB 的事务完毕以后,他才继续运行;跟 PROPAGATION_REQUIRED 的区别是会新起一个事务,而不是使用父事务,所以是两个截然不同的事务,ServiceB 的执行报错,如果被 ServiceA 捕获了,不会影响到 ServiceA 的回滚;