通知(Advice),切面的一种实现,可以完成简单织入功能(织入功能就是在这里完成的)。 常用通知有:前置通知、后置通知、环绕通知、异常处理通知。
通知详解
通知(Advice)是 Spring 提供的一种切面(Aspect)。但其功能过于简单:只能将切面织入到目标类的所有目标方法中, 无法完成将切面织入到指定目标方法中。
前置通知
定义前置通知,需要实现 MethodBeforeAdvice 接口。该接口中有一个方法 before(),会在目标方法执行之前执行。 前置通知的特点:
- 在目标方法执行之前先执行。
- 不改变目标方法的执行流程,前置通知代码不能阻止目标方法执行。
- 不改变目标方法执行的结果。
(1) 定义业务接口与目标类
public interface ISomeService {
//主业务方法
void doFirst();
//主业务方法
void doSecond();
}
//目标类
public class SomeServiceImpl implements ISomeService {
//目标方法
@Override
public void doFirst() {
System.out.println("执行doFirst()方法");
}
//目标方法
@Override
public void doSecond() {
System.out.println("执行doSecond()方法");
}
}
(2) 定义前置通知
//前置通知
public class MyMethodBeforeAdvice implements MethodBeforeAdvice {
/**
*当前方法在目标方法执行之前执行
*method:目标方法
*args:目标方法的参数列表
*target:目标对象
*/
@Override
public void before(Method method, Object[] args, Object target) throws Throwable {
System.out.println("执行前置通知方法");
}
}
(3) 定义 Spring 配置文件
<!-- 注册目标对象 -->
<bean id="someService" class="com.huang.aop01.SomeServiceImpl"/>
<!-- 注册切面:通知 -->
<bean id="myAdvice" class="com.huang.aop01.MyMethodBeforeAdvice"/>
<!-- 生成代理对象 -->
<bean id="serviceProxy" class="org.springframework.aop.framework.ProxyFactoryBean">
<!-- <property name="targetName" value="someService"/> -->
<!-- 指定目标对象 -->
<property name="target" ref="someService"/>
<!-- 指定切面 -->
<property name="interceptorNames" value="myAdvice"/>
</bean>
(4) 定义测试类
public class MyTest {
@Test
public void test01() {
String resource = "com/huang/aop01/applicationContext.xml";
ApplicationContext ac = new ClassPathXmlApplicationContext(resource);
//serviceProxy为代理对象,而非目标对象
ISomeService service = (ISomeService) ac.getBean("serviceProxy");
service.doFirst();
System.out.println("----------------------");
service.doSecond();
}
}
(5) 运行结果
使用 Debug 查看后台运行情况,可以看到代理生成使用的是 JDK 代理机制。
后置通知
定义后置通知,需要实现接口 AfterReturningAdvice。该接口中有一个方法 afterReturning(),会在目标方法执行之后执行。后置通知的特点:
- 在目标方法执行之后执行。
- 不改变目标方法的执行流程,后置通知代码不能阻止目标方法执行。
- 不改变目标方法执行的结果。
(1) 修改业务接口与目标类
String doSecond();
//目标方法
@Override
public String doSecond() {
System.out.println("执行doSecond()方法");
return "abcde";
}
(2) 定义后置通知
public class MyAfterReturningAdvice implements AfterReturningAdvice {
@Override
public void afterReturning(Object returnValue, Method method, Object[] args, Object target)
throws Throwable {
System.out.println("执行后置通知方法 returnValue= " + returnValue);
if (returnValue != null) {
returnValue = ((String) returnValue).toUpperCase();
System.out.println("修改过的结果 returnValue=" + returnValue);
}
}
}
(3) 修改 Spring 配置文件
<!-- 注册切面:通知 -->
<bean id="myAdvice" class="com.huang.aop02.MyAfterReturningAdvice"/>
(4) 修改测试类
@Test
public void test01() {
String resource = "com/huang/aop02/applicationContext.xml";
ApplicationContext ac = new ClassPathXmlApplicationContext(resource);
//serviceProxy为代理对象,而非目标对象
ISomeService service = (ISomeService) ac.getBean("serviceProxy");
service.doFirst();
System.out.println("----------------------");
String result = service.doSecond();
System.out.println(result);
}
环绕通知
定义环绕通知,需要实现 MethodInterceptor 接口。环绕通知,也叫方法拦截器,可以在目标方法调用之前及之后做处理,可以改变目标方法的返回值,也可以改变程序执行流程。
(1) 定义后置通知
public class MyMethodInterceptor implements MethodInterceptor {
@Override
public Object invoke(MethodInvocation invocation) throws Throwable {
System.out.println("执行环绕通知:目标方法执行之前");
Object result = invocation.proceed();
System.out.println("执行环绕通知:目标方法执行之后");
if (result != null) {
result = ((String)result).toUpperCase();
}
return result;
}
}
(2) 修改 Spring 配置文件
<!-- 注册切面:通知 -->
<bean id="myAdvice" class="com.huang.aop03.MyMethodInterceptor"/>
异常通知
定义异常通知,需要实现 ThrowsAdvice 接口。该接口的主要作用是,在目标方法抛出异常后,根据异常的不同做出相应的处理。当该接口处理完异常后,会简单地将异常再次抛出给目标方法。
不过,这个接口较为特殊,从形式上看,该接口中没有必须要实现的方法。但,这个接口却确实有必须要实现的方法 afterThrowing()。这个方法重载了四种形式。由于使用时,一般只使用其中一种,该接口定义为标识接口(没有方法的接口)。
这四种方法中,常用的形式如下:
public void afterThrowing(自定义的异常类 e)
这里的参数 e 为,与具体业务相关的用户自定义的异常类对象。容器会根据异常类型的不同,自动选择不同的该方法执行。这些方法的执行是在目标方法执行结束后执行的。
(1) 定义异常类的父类
public class UserException extends Exception {
public UserException() {
super();
}
public UserException(String message) {
super(message);
}
}
(2) 定义两个异常类的子类
public class UsernameException extends UserException {
public UsernameException() {
super();
}
public UsernameException(String message) {
super(message);
}
}
public class PasswordException extends UserException {
public PasswordException() {
super();
}
public PasswordException(String message) {
super(message);
}
}
(3) 定义业务接口。要抛出异常父类
//主业务借口
public interface ISomeService {
//目标方法
boolean login(String username,String password)
throws UserException;
}
(4) 定义目标类
//目标类
public class SomeServiceImpl implements ISomeService {
@Override
public boolean login(String username, String password) throws UserException {
if (!"huang".equals(username)) {
throw new UsernameException("用户名输错了");
}
if (!"111".equals(password)) {
throw new PasswordException("密码输错了");
}
// double a = 3 / 0;
return true;
}
}
(5) 定义异常通知
public class MyThrowsAdvice implements ThrowsAdvice {
// 当目标方法抛出UsernameException异常时,执行当前方法
public void afterThrowing(UserException ex) {
System.out.println("发生用户名异常 ex = " + ex.getMessage());
}
// 当目标方法抛出PasswordException异常时,执行当前方法
public void afterThrowing(PasswordException ex) {
System.out.println("发生密码异常 ex = " + ex.getMessage());
}
// 当目标方法抛出其它异常时,执行当前方法
public void afterThrowing(Exception ex) {
System.out.println("发生异常 ex = " + ex.getMessage());
}
}
(6) 定义测试类
public class MyTest {
@Test
public void test01() throws UserException {
String resource = "com/huang/aop05/applicationContext.xml";
ApplicationContext ac = new ClassPathXmlApplicationContext(resource);
//serviceProxy为代理对象,而非目标对象
ISomeService service = (ISomeService) ac.getBean("serviceProxy");
service.login("huang", "111");
}
}
通知的其它用法
给目标方法织入多个切面
若要给目标方法织入多个切面,则需要在配置代理对象的切面属性时,设定为 array。
<property name="interceptorNames">
<array>
<value>myBeforeAdvice</value>
<value>myAfterAdvice</value>
</array>
</property>
无接口的 CGLIB 代理生成
若不存在接口,则 ProxyFactoryBean 会自动采用 CGLIB 方式生成动态代理(有接口使用 JDK 的 Proxy 动态代理)。
<bean id="serviceProxy" class="org.springframework.aop.framework.ProxyFactoryBean">
<property name="target" ref="someService"/>
<property name="interceptorNames" value="myAdvice"/>
</bean>
查看后台运行情况,可以看到代理生成使用的是 CGLIB 代理机制。
有接口的 CGLIB 代理生成
若存在接口,但又需要使用 CGLIB 生成代理对象,此时,只需要在配置文件中增加一个 proxyTargetClass 属性设置,用于指定强制使用 CGLIB 代理机制。
<property name="proxyTargetClass" value="true"/>
也可指定 optimize(优化)的值为 true,强制使用 CGLIB 代理机制。
<property name="optimize" value="true"/>
查看后台运行情况,可以看到代理生成使用的是 CGLIB 代理机制。