1.1 增强类型扩展
在第5章中学习Spring AOP时学习了前置增强和后置增强两种增强处理。本节将学习更多的增强方式。
1.1.1 异常抛出增强
异常抛出增强是指当目标对象方法抛出异常时进行织入操作的一种增强方式。通常,异常抛出增强用来为项目提供统一的异常处理功能,具有可灵活插拔的特点。使用异常抛出增强时,需要在配置文件中使用<aop:after-throwing>标签进行配置,语法如下。
语法:
<aop:config>
<aop:aspect ref="增强方法所在的Bean">
<aop:after-throwing method="增强处理方法"
pointcut-ref="切入点id" throwing="e" />
</aop:aspect>
</aop:config>
上述语法中,<aop:after-throwing>标签表示增强处理类型为异常抛出增强。method属性表示增强处理最终调用的方法,用以捕获异常。如果需要获取抛出的异常信息,可以为增强方法声明相关类型的参数,并通过throwing属性指定该参数名称。Spring框架会为其注入从目标方法抛出的异常实例。根据语法编写配置文件,代码如示例1所示。
示例1
<!-- 声明增强方法所在的Bean -->
<bean id="theLogger" class="aop.ErrorLogger">
<!-- 配置切面 -->
<aop:config>
<!-- 定义切入点 -->
<aop:pointcut id="pointcut" expression="execution(* service.UserService.*(..))" />
<!-- 引用包含增强方法的Bean -->
<aop:aspect ref="theLogger">
<!-- 将afterThrowing()方法定义为异常抛出增强并引用pointcut切入点 -->
<!-- 通过throwing属性指定为名为e的参数注入异常实例 -->
<aop:after-throwing method="afterThrowing"
pointcut-ref="pointcut" throwing="e" />
</aop:aspect>
</aop:config>
定义异常处理类ErrorLogger,并在类中创建处理异常的方法afterThrowing(),如示例2所示。
示例2
/**
* 定义包含增强方法的JavaBean
*/
public class ErrorLogger {
private static final Loggerlog = Logger.getLogger(ErrorLogger.class);
public void afterThrowing(JoinPoint jp, RuntimeException e) {
log.error(jp.getSignature().getName() +" 方法发生异常:" + e);
}
}
1.1.2 最终增强
最终增强是指无论目标对象的方法正常运行还是抛出异常,该增强处理都会被执行的一种增强方式。其与Java中finally代码块的作用相似,通常用于释放资源等操作,具有可灵活插拔的特点。使用最终增强,需要在配置文件中使用aop:after标签进行配置,语法如下。
语法:
<aop:config>
<aop:aspect ref="增强方法所在的Bean">
<aop:after method="增强处理方法" pointcut-ref="切入点id" />
</aop:aspect>
</aop:config>
上述语法中,<aop:after>标签表示增强处理类型为最终增强。method属性表示增强处理最终调用的方法。根据语法编写配置文件,代码如示例3所示。
示例3
<!-- 声明增强方法所在的Bean -->
<bean id="theLogger" class="aop.AfterLogger">
<!-- 配置切面 -->
<aop:config>
<!-- 定义切入点 -->
<aop:pointcut id="pointcut"
expression="execution(* service.UserService.*(..))" />
<!-- 引用包含增强方法的Bean -->
<aop:aspect ref="theLogger">
<!-- 将afterLogger()方法定义为最终增强并引用pointcut切入点 -->
<aop:after method="afterLogger" pointcut-ref="pointcut"/>
</aop:aspect>
</aop:config>
定义最终增强处理类AfterLogger,并在类中创建处理异常的方法afterLogger(),如示例4所示。
示例4
/**
* 定义包含增强方法的JavaBean
*/
public class AfterLogger {
private static final Loggerlog = Logger.getLogger(AfterLogger.class);
public void afterLogger(JoinPoint jp) {
log.info(jp.getSignature().getName() +" 方法结束执行。");
}
}
1.1.3 环绕增强
环绕增强是指在目标对象方法前后都可以进行织入的一种增强处理方式。在环绕增强处理中,可以获取或修改目标方法的参数、返回值,可以对它进行异常处理,甚至可以决定目标方法是否被执行。使用环绕增强,需要在配置文件中使用<aop:around>标签进行配置,语法如下。
语法:
<aop:config>
<aop:aspect ref="增强方法所在的Bean">
<aop:around method="增强处理方法" pointcut-ref="切入点id" />
</aop:aspect>
</aop:config>
上述语法中,<aop:around>标签表示增强处理类型为环绕增强。method属性表示增强处理最终调用的方法。根据语法编写配置文件,代码如示例5所示。
示例5
<!-- 声明增强方法所在的Bean -->
<bean id="theLogger" class="aop.AroundLogger">
<!-- 配置切面 -->
<aop:config>
<!-- 定义切入点 -->
<aop:pointcut id="pointcut"
expression="execution(* service.UserService.*(..))" />
<!-- 引用包含增强方法的Bean -->
<aop:aspect ref="theLogger">
<!-- 将aroundLogger()方法定义为环绕增强并引用pointcut切入点 -->
<aop:after method="aroundLogger" pointcut-ref="pointcut"/>
</aop:aspect>
</aop:config>
定义环绕增强处理类AroundLogger,并在类中创建处理异常的方法aroundLogger(),如示例6所示。
示例6
/**
* 定义包含增强方法的JavaBean
*/
public class AroundLogger {
private static final Loggerlog = Logger.getLogger(AroundLogger.class);
public ObjectaroundLogger(ProceedingJoinPoint jp)throws Throwable {
log.info("调用 " + jp.getTarget() +" 的 " + jp.getSignature().
getName() +" 方法。方法入参: " + Arrays.toString(jp.getArgs()));
try {
Object result = jp.proceed(); //执行目标方法并获得其返回值
log.info("调用 " + jp.getTarget() +" 的 " + jp.getSignature().
getName() +" 方法。方法返回值: " + result);
return result;
}catch (Throwable e) {
log.error(jp.getSignature().getName() +" 方法发生异常: " + e);
throw e;
}finally {
log.info(jp.getSignature().getName() +" 方法结束执行。");
}
}
}
通过为增强方法声明ProceedingJoinPoint类型的参数,可以获得连接点信息,方法与JoinPoint相同。ProceedingJoinPoint是JoinPoint的子接口,其不仅封装目标方法及其入参数组,还封装了被代理的目标对象,通过它的proceed()方法可以调用真正的目标方法,从而达到对连接点的完全控制。
1.2 依赖注入方式扩展
在第5章中同学们学习了Spring IoC通过setter方法实现对属性的赋值功能,这种方式被称为设值注入。除此之外,Spring框架还提供了多种属性注入方式,下面逐一进行学习。
1.2.1 构造注入
构造注入是指Spring框架通过构造方法为属性赋值的一种注入方式。构造注入可以在对象初始化时为属性赋值,具有良好的时效性。使用构造注入,需要在配置文件中使用<constructor-arg>标签进行配置,语法如下。
语法:
<bean id="唯一标识" class="类的全路径">
<constructor-arg name="参数名称" ref="依赖对象"/ >
</bean>
上述语法中,<constructor-arg>标签表示使用构造器注入的方式向当前Bean注入依赖,其内部的ref标签用来配置被注入的Bean名称。以保存用户功能为例,根据上述语法编写配置文件,代码如示例7所示。
示例7
<!-- 定义UserDaoImpl对象,并指定id为userDao -->
<bean id="userDao" class="dao.impl.UserDaoImpl" />
<!-- 定义UserServiceImpl对象,并指定id为userService -->
<bean id="userService" class="service.impl.UserServiceImpl">
<!-- 通过参数名称为构造方法注入值 -->
<constructor-arg name="userDao" ref="userDao"/>
</bean>
知识扩展:
(1)如果有多个参数,可以在当前bean标签下配置多个<constructor-arg>标签。
(2)除使用name属性区别多个参数外,还可以使用<constructor-arg>标签中的index属性指定参数的位置索引,位置从0开始。
(3)<constructor-arg>标签提供了type属性,用以标识参数的类型。
在示例7中,把userDao以构造注入的方式注入userService中,所以在userService中应该提供一个以userDao为参数的构造方法,如示例8所示。
示例8
/**
* 用户功能业务层实现
*/
public class UserServiceImplimplements UserService {
//只声明接口类型引用
private UserDaodao;
//无参构造
public UserServiceImpl() { }
//用于为dao属性赋值的构造方法
public UserServiceImpl(UserDao dao) {
this.dao = dao;
}
/**
* 保存用户
* @param user
*/
public void save(User user) {
//保存用户信息
dao.saveUser(user);
}
}
构造注入方式具有良好的时效性,在对象实例化时就能得到所依赖的对象,便于在对象初始化方法中使用所依赖的对象。但是,其受限于方法重载的形式,使用灵活性不足。相比而言,设值注入使用灵活,但时效性不足,并且大量的set()方法增加了类的复杂性。Spring框架并不倾向于某种注入方式,用户应该根据实际情况进行合理的选择。
经验:
Spring框架提供的注入方式不止这两种,只是这两种方式用得较普遍,有兴趣的同学可以通过Spring框架的开发手册了解其他注入方式。
1.2.2 p命名空间注入
在第5章学习Spring AOP时,同学们学习了使用Spring配置文件中aop命名空间下的相关标签实现织入切面的功能,除aop外,Spring提供的命名空间还有很多。Spring配置文件从2.0版本开始采用schema形式,schema的配置方案为许多领域提供了简化的配置方法,极大地简化了配置文件的复杂度和工作量。本节将学习和使用p命名空间,体验它带来的便捷之处。
使用Spring配置文件做属性注入时,需要用到property标签,如示例9所示。
示例9
<bean id="user" class="entity.User">
<property name="userName">
<value>诸葛亮
<property name="age">
<value>27
<property name="evaluate">
<value>鞠躬尽瘁,死而后已
<bean id="userDao" class="dao.impl.UserDaoImpl" />
<bean id="userService" class="service.impl.UserServiceImpl">
<property name="dao">
<ref bean="userDao" />
</bean>
p命名空间的特点是使用属性而不是子元素的形式配置Bean的属性,从而简化了Bean的配置。p标签的语法非常简单,如下所示。
语法:
<bean id="唯一标识" class="类的全路径"
p: "属性 1" ="注入的值" p: "属性 2" ="注入的值"/>
<bean id="唯一标识" class="类的全路径"
p:dao-ref="注入的Bean" />
使用p标签注入属性时,如果有多个属性,写多个p标签即可。
1.2.3 不同数据类型的注入
Java方法中的参数类型包括基本类型、集合数组类型、引用类型等,本节将针对以上类型参数在Spring配置文件中的配置方式进行扩展学习。创建一个包含这些参数的实体类用于测试,关键代码如示例10所示。
示例10
public class TestEntity {
private StringspecialCharacter1; //特殊字符值1
private StringspecialCharacter2; //特殊字符值2
private Useruser; //引用外部对象
private UserinnerBean; //JavaBean类型
private Listlist; //List类型
private String[]array; //数组类型
private Setset; //Set类型
private Mapmap; //Map类型
private Propertiesprops; //Properties类型
private StringemptyValue; //注入空字符串值
private StringnullValue ="init value"; //注入null值
······ //省略getter、setter方法
}
示例11中省略标准的getter、setter方法。
1.3 使用注解实现Spring IoC
第5章中已经学习了使用XML配置文件的方式实现Spring IoC相关的内容,但是当项目越来越庞大时,使用这种方式需要在配置文件中编写大量的代码。Spring从2.0版本开始引入注解的配置方式来解决这个问题。
1.3.1 使用注解方式实现用户保存功能
高版本的Spring在使用注解实现IoC时,需要依赖spring-aop.jar。所以,在项目lib文件夹下添加spring-aop-5.2.2.RELEASE.jar。
1.3.2 使用Java标准注解完成装配
Spring框架是一个超级黏合平台,它除本身提供@Autowired注解实现Bean的装配功能外,还支持同样可以实现该功能的@Resource注解。@Resource注解是在Java规范提案——JSR-250中定义的。
说明:JSR全称是Java Specification Requests,即Java规范提案。Java的版本和功能在不断地更新和扩展,JSR就是用来规范这些功能和接口的标准,其已经成为Java业界的一个重要标准。
@Resource注解的使用方式如示例11所示。
示例11
/**
* 用户模块业务层实现
*/
@Service("userService")
public class UserServiceImplimplements UserService {
@Resource
private UserDaodao;
······ //省略业务方法
}
1.4 使用注解实现Spring AOP
使用注解实现Spring AOP需要用到AspectJ框架。AspectJ本身就是AOP的框架,它扩展了Java语言,定义了AOP语法,且自带编译器。此编译器可以将代码编译成遵守字节编码规范的Class文件,在编译期间即可实现代码的织入功能。AspectJ5中提供了一个非常重要的注解——@AspectJ。该注解需要JDK5.0注解技术的支持和AspectJ切入点表达式语言描述切面,因此,使用@AspectJ时,需要保证JDK是5.0或其以上版本。此外,因为Java的反射机制无法获取方法参数名,Spring还需要利用轻量级的字节码处理框架asm(已集成在Spring Core模块中)处理@AspectJ中所描述的方法参数名。
1.4.1 使用注解方式标注切面
Spring框架通过集成AspectJ框架实现了以注解的方式定义切面,这使配置文件中的代码大大减少。相对于使用配置文件实现AOP而言,注解的方式更加清晰简洁。下面通过使用注解方式实现对业务方法前后增加日志的功能学习AspectJ框架。以用户保存功能为例,业务方法的关键代码如示例12所示。
示例12
/**
* 用户业务类,实现对User功能的业务管理
*/
@Service("userService")
public class UserServiceImplimplements UserService {
@Autowired
private UserDao dao;
/**
* 保存用户
* @param user
*/
public void save(User user) {
//调用用户DAO的方法保存用户信息
dao.saveUser(user);
}
}
创建切面类UserServiceLogger,并创建前后增强的方法before()及afterReturning()方法。关键代码如示例13所示。
示例13
import java.util.Arrays;
import org.apache.log4j.Logger;
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import org.aspectj.lang.annotation.AfterReturning;
/**
* 使用注解定义切面
*/
@Aspect
public class UserServiceLogger {
private static final Loggerlog = Logger.getLogger(UserServiceLogger.class);
/**
* 前置增强
* @param jp
*/
@Before("execution(* service.UserService.*(..))")
public void before(JoinPoint jp) {
log.info("调用 " + jp.getTarget() +" 的 " + jp.getSignature()
.getName() +" 方法。方法入参: " + Arrays.toString(jp.getArgs()));
}
/**
* 后置增强
* @param jp
* @param returnValue
*/
@AfterReturning(pointcut ="execution(* service.UserService.*(..))",
returning ="returnValue")
public void afterReturning(JoinPoint jp, Object returnValue) {
log.info("调用 " + jp.getTarget() +" 的 " + jp.getSignature()
.getName() +" 方法。方法返回值: " + returnValue);
}
}
在示例13中,分别使用了@Aspect、@Before和@AfterReturning注解。其中,@Aspect注解的作用是将UserServiceLogger类定义为切面;@Before注解是作用是将before()方法定义为前置增强;@AfterReturning注解的作用是将afterReturning()方法定义为后置增强。增强方法中使用了JoinPoint类型的参数,以获得当前连接点的信息。对于后置增强,还可以定义一个参数用于接收目标方法的返回值。需要注意的是,必须在@AfterReturning注解中通过returning属性指定该参数的名称,Spring框架才会将目标方法发返回值赋值给指定名称的参数。定义好切面之后,还需要对配置文件做一些修改才能完成织入工作。配置文件信息如示例14所示。
示例14
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:context="http://www.springframework.org/schema/context"
xmlns:aop="http://www.springframework.org/schema/aop"
xsi:schemaLocation="http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/context
http://www.springframework.org/schema/context/spring-context.xsd
http://www.springframework.org/schema/aop
http://www.springframework.org/schema/aop/spring-aop.xsd">
<!-- 注解扫描 -->
<context:component-scan base-package="service,dao" />
<bean class="aop.UserServiceLogger">
<aop:aspectj-autoproxy />
</beans>
配置文件中的内容非简单,首先需要导入aop命名空间,然后将定义好的切面注册到Spring容器中,最后只需在配置文件中添加<aop:aspectj-autoproxy />标签,这些步骤完成之后就可以启用对于@AspectJ注解的支持了,Spring框架将自动为匹配的Bean创建代理。
注意:
如果不需要被其他Bean引用,可以不指定id属性。
1.4.2 使用注解定义其他类型的增强
依然以保存用户功能为例,分别学习通过注解方式实现异常抛出类增强、最终增强和环绕增强功能。对于异常抛出增强的注解是@AfterThrowing,增强代码如示例15所示。
示例15
import org.apache.log4j.Logger;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.annotation.AfterThrowing;
/**
* 使用注解实现异常抛出增强
*/
@Aspect
public class ErrorLogger {
private static final Loggerlog = Logger.getLogger(ErrorLogger.class);
@AfterThrowing(pointcut ="execution(* service.UserService.*(..))"
, throwing ="e")
public void afterThrowing(JoinPoint jp, RuntimeException e) {
log.error(jp.getSignature().getName() +" 方法抛出异常: " + e);
}
}
示例15中,使用@AfterThrowing注解可以定义异常抛出增强。如果需要获取抛出的异常信息,可以为增强方法声明相关类型的参数,并通过@AfterThrowing注解的throwing属性指定该参数名称,Spring框架会为其注入业务方法抛出的异常实例。
总结
◎ Spring框架提供了设值注入、构造注入等依赖注入方式。
◎ 在Spring配置文件中使用<context:component-scan>扫描包含注解的类,完成初始化。
◎ 用来定义Bean组件的注解包括@Component、@Repository、@Service、@Controller。
◎ Bean组件的装配可以通过@Autowired、@Qualifier或@Resource实现。
◎ 使用p命名空间可以简化属性注入的配置。
◎ 通过在配置文件中添加<aop:aspectj-autoproxy>,就可以启用对@AspectJ注解的支持。
◎ Spring框架提供的增强处理方式主要包括前置增强、后置增强、异常抛出增强、环绕增强、最终增强等。