- Spring核心技术和案例实战
- 郑天民
- 3031字
- 2023-06-21 18:56:28
1.1 Spring容器
本节讨论Spring容器,并给出容器所具备的非常重要的两个功能特性,即依赖注入和面向切面编程。
1.1.1 IoC
在介绍Spring容器之前,我们先来介绍一个概念,即控制反转(Inversion of Control,IoC)。试想,如果想有效管理一个对象,就需要知道创建、使用以及销毁这个对象的方法。这个过程显然是繁杂而重复的。而通过控制反转,就可以把这部分工作交给一个容器,由容器负责控制对象的生命周期和对象之间的关联关系。这样,与一个对象控制其他对象的处理方式相比,现在所有对象都被容器控制,控制的方向做了一次反转,这就是“控制反转”这一名称的由来。而Spring扮演的角色就是这里的容器。
可以看到控制反转的重点是在系统运行中,按照某个对象的需要,动态提供它所依赖的其他对象,而这一点可以通过依赖注入(Dependency Injection,DI)实现。Spring会在适当的时候创建一个Bean,然后像使用注射器一样把它注入目标对象中,这样就完成了对各个对象之间关系的控制。
可以说,依赖注入是开发人员使用Spring框架的基本手段,我们可以通过依赖注入获取所需的各种Bean。Spring为开发人员提供了3种不同的依赖注入方式,分别是字段注入、构造器注入和Setter方法注入。
现在,假设我们有如下所示的HealthRecordService接口以及它的实现类:
public interface HealthRecordService {
public void recordUserHealthData();
}
public class HealthRecordServiceImpl implements HealthRecordService {
@Override
public void recordUserHealthData () {
System.out.println("HealthRecordService has been called.");
}
}
下面我们来讨论具体如何在Spring中完成对HealthRecordServiceImpl实现类的注入,并分析各种注入类型的优缺点。
1.依赖注入的3种方式
首先,我们来看看字段注入,即在一个类中通过字段的方式注入某个对象,如下所示:
public class ClientService {
@Autowired
private HealthRecordService healthRecordService;
public void recordUserHealthData() {
healthRecordService.recordUserHealthData();
}
}
可以看到,通过@Autowired注解,字段注入的实现方式非常简单而直接,代码的可读性也很高。事实上,字段注入是3种依赖注入方式中最常用、最容易使用的一种。但是,它也是3种注入方式中最应该避免使用的一种。如果使用过IDEA,你可能遇到过这个提示—Field injection is not recommended,告诉你不建议使用字段注入。字段注入的最大问题是对象在外部是不可见的。正如在上面的ClientService类中,我们定义了一个私有变量HealthRecordService来注入该接口的实例。显然,这个实例只能在ClientService类中被访问,脱离了容器环境就无法访问这个实例。
基于以上分析,Spring官方推荐的注入方式实际上是构造器注入。这种注入方式也很简单,就是通过类的构造函数来完成对象的注入,如下所示:
public class ClientService {
private HealthRecordService healthRecordService;
@Autowired
public ClientService(HealthRecordService healthRecordService) {
this.healthRecordService = healthRecordService;
}
public void recordUserHealthData() {
healthRecordService.recordUserHealthData();
}
}
可以看到构造器注入能解决对象外部可见性的问题,因为HealthRecordService是通过ClientService构造函数进行注入的,所以势必可以脱离ClientService而独立存在。构造器注入的显著问题就是当构造函数中存在较多依赖对象时,大量的构造器参数会让代码显得比较冗长。这时就可以使用Setter方法注入。我们同样先来看一下Setter方法注入的实现代码,如下所示:
public class ClientService {
private HealthRecordService healthRecordService;
@Autowired
public void setHealthRecordService(HealthRecordService healthRecordService) {
this.healthRecordService = healthRecordService;
}
public void recordUserHealthData() {
healthRecordService.recordUserHealthData();
}
}
Setter方法注入和构造器注入看上去有些类似,但Setter方法比构造函数更具可读性,因为我们可以把多个依赖对象分别通过Setter方法逐一进行注入。而且,Setter方法注入对于非强制依赖注入很有用,我们可以有选择地注入一部分想要注入的依赖对象。换句话说,可以实现按需注入,帮助开发人员只在需要时注入依赖关系。
作为总结,我们用一句话来概括Spring中所提供的3种依赖注入方式:构造器注入适用于强制对象注入;Setter方法注入适用于可选对象注入;而字段注入是应该避免的,因为对象无法脱离容器而独立运行。
2.Bean的作用域
所谓Bean的作用域,描述了Bean在Spring容器上下文中的生命周期和可见性。在这里,我们将讨论Spring框架中不同类型的Bean的作用域以及使用上的指导规则。
如果想要通过注解来设置Bean的作用域,可以使用如下所示的代码:
@Configuration
public class AppConfig {
@Bean
@Scope("singleton")
public HealthRecordService createHealthRecordService() {
return new HealthRecordServiceImpl();
}
}
可以看到这里使用了一个@Scope注解来指定Bean的作用域为单例的“singleton”。在Spring中,除了单例作用域之外,还有一个“prototype”,即原型作用域,也可以称为多例作用域来与单例作用域进行区别。在使用方式上,我们同样可以使用如下所示的枚举值来对它们进行设置:
@Scope(value = ConfigurableBeanFactory.SCOPE_SINGLETON)
@Scope(value = ConfigurableBeanFactory.SCOPE_PROTOTYPE)
在Spring IoC容器中,Bean的默认作用域是单例作用域,也就是说不管对Bean的引用有多少个,容器只会创建一个实例。而原型作用域则不同,每次请求Bean时,Spring IoC容器都会创建一个新的对象实例。
从两种作用域的效果而言,我们总结一条开发上的结论,即对于无状态的Bean,我们应该使用单例作用域,反之则应该使用原型作用域。
那么,什么样的Bean属于有状态的呢?结合Web应用程序,我们可以明确,对每次HTTP请求而言,都应该创建一个Bean来代表这一次的请求对象。同样,对会话而言,我们也需要针对每个会话创建一个会话状态对象。这些都是常见的有状态的Bean。为了更好地管理这些Bean的生命周期,Spring还专门针对Web开发场景提供了对应的“request”和“session”作用域。
1.1.2 AOP
在本小节中,我们将讨论Spring容器的另一项核心功能,即面向切面编程(Aspect Oriented Programming,AOP)。我们将介绍AOP的概念以及实现这些概念的方法。
所谓切面,本质上解决的是关注点分离的问题。在面向对象编程的世界中,我们把一个应用程序按照职责和定位拆分成多个对象,这些对象构成了不同的层次。而AOP可以说是面向对象编程的一种补充,目标是将一个应用程序抽象成各个切面。
举个例子,假设一个Web应用中存在ServiceA、ServiceB和ServiceC这3个服务,而每个服务都需要考虑安全校验、日志记录、事务处理等非功能性需求。这时,就可以引入AOP的思想把这些非功能性需求从业务需求中拆分出来,构成独立的关注点,如图1-1所示。
图1-1 AOP的思想示意
从图1-1可以很形象地看出,所谓切面相当于应用对象间的横切面,我们可以将其抽象为单独的模块进行开发和维护。
为了理解AOP的具体实现过程,我们需要引入一组特定的术语,具体如下。
● 连接点(Join Point):连接点表示应用执行过程中能够插入切面的一个点。这种连接点可以是方法调用、异常处理、类初始化或对象实例化。在Spring框架中,连接点只支持方法的调用。
● 通知(Advice):通知描述了切面何时执行以及如何执行对应的业务逻辑。通知有很多种类型,在Spring中提供了一组注解用来表示通知,包括@Before、@After、@Around、@AfterThrowing和@AfterReturning等。我们会在后续代码示例中看到这些注解的使用方法。
● 切点(Point Cut):切点是连接点的集合,用于定义必须执行的通知。通知不一定应用于所有连接点,因此切点提供了在应用程序中的组件上执行通知的细粒度控制。在Spring中,可以通过表达式来定义切点。
● 切面(Aspect):切面是通知和切点的组合,用于定义应用程序中的业务逻辑及其应执行的位置。Spring提供了@Aspect注解来定义切面。
现在,假设有这样一个代表转账操作的TransferService接口:
public interface TransferService {
boolean transfer(Account source, Account dest, int amount) throws MinimumAmountException;
}
然后我们提供它的实现类:
package com.demo;
public class TransferServiceImpl implements TransferService {
private static final Logger LOGGER = Logger.getLogger(TransferServiceImpl.class);
@Override
public boolean transfer(Account source, Account dest, int amount) throws MinimumAmountException {
LOGGER.info("Tranfering " + amount + " from " + source.getAccountName() + " to " + dest.getAccountName());
if (amount < 10) {
throw new MinimumAmountException("转账金额必须大于10");
}
return true;
}
}
针对转账操作,我们希望在该操作之前、之后以及执行过程进行切入,并添加对应的日志记录,那么可以实现如下所示的TransferServiceAspect类:
@Aspect
public class TransferServiceAspect {
private static final Logger LOGGER = Logger.getLogger(TransferServiceAspect.class);
@Pointcut("execution(* com.demo.TransferService.transfer(..))")
public void transfer() {}
@Before("transfer()")
public void beforeTransfer(JoinPoint joinPoint) {
LOGGER.info("在转账之前执行");
}
@After("transfer()")
public void afterTransfer(JoinPoint joinPoint) {
LOGGER.info("在转账之后执行");
}
@AfterReturning(pointcut = "transfer() and args(source, dest, amount)", returning = "isTransferSucessful")
public void afterTransferReturns(JoinPoint joinPoint, Account source, Account dest, Double amount, boolean isTransferSucessful) {
if (isTransferSucessful) {
LOGGER.info("转账成功了");
}
}
@AfterThrowing(pointcut = "transfer()", throwing = "minimumAmountException")
public void exceptionFromTransfer(JoinPoint joinPoint, MinimumAmountException minimumAmountException) {
LOGGER.info("转账失败了:" + minimumAmountException.getMessage());
}
@Around("transfer()")
public boolean aroundTransfer(ProceedingJoinPoint proceedingJoinPoint){
LOGGER.info("方法执行之前调用");
boolean isTransferSuccessful = false;
try {
isTransferSuccessful = (Boolean)proceedingJoinPoint.proceed();
} catch (Throwable e) {
LOGGER.error(e.getMessage(), e);
}
LOGGER.info("方法执行之后调用");
return isTransferSuccessful;
}
}
上述代码代表了Spring AOP机制的典型使用方法。使用@Pointcut注解定义了一个切入点,并通过“execution”指示器限定该切入点匹配的包结构为“com.demo”,匹配的方法是TransferService类的transfer()方法。
请注意,在TransferServiceAspect中综合使用了@Before、@After、@Around、@AfterThrowing和@AfterReturning注解用来设置5种不同类型的通知。其中@Around注解会将目标方法封装起来,并执行动态添加返回值、异常信息等操作。这样@AfterThrowing和@AfterReturning注解就能获取这些返回值或异常信息并做出响应,而@Before和@After注解可以在方法调用的前后分别添加自定义的处理逻辑。