Java 企业级应用开发 part 4 - J2EE part 4

AOP 编程

  • Spring AOP
  • JDK 动态代理
  • CGLib 动态代理

Spring AOP 相关概念

切面(Aspect)

抽离出来的通用功能模块(包含逻辑 + 植入规则),通常是指封装的、用于横向插入系统的功能类

连接点(Joinpoint)

程序执行的某个特定点,如方法调用、异常抛出等

切入点(Pointcut)

具体插入切面的位置(筛选后的连接点)

通知(Advice)

在连接点处执行的代码,包括前置通知、后置通知、环绕通知等,即切面的具体执行逻辑(何时、如何执行)

目标对象(Target)

被切面所织入的对象,即业务逻辑所在的对象

织入(Weaving)

将切面代码插入到目标对象上,从而生成代理对象的过程

代理(Proxy)

将通知应用到目标对象后,动态生成的通知对象,就称为代理

引介 (Introduction)

是一种特殊的通知,可以在不修改目标类的情况下为目标类添加新的方法或属性

JDK 动态代理

创建 UserDao 的接口和实现类

UserDao.java
1
2
3
4
5
6
7
package com.itheima.demo01;

public interface UserDao {
public void addUser();

public void deleteUser();
}
UserDaoImpl.java
1
2
3
4
5
6
7
8
9
10
11
12
package com.itheima.demo01;

// 目标类
public class UserDaoImpl implements UserDao {
public void addUser() {
System.out.println("添加用户");
}

public void deleteUser() {
System.out.println("删除用户");
}
}

创建切面类

MyAspect.java
1
2
3
4
5
6
7
8
9
10
11
12
package com.itheima.demo01;

//切面类:存在多个通知Advice(增强的方法)
public class MyAspect {
public void check_Permissions() {
System.out.println("模拟检查权限...");
}

public void log() {
System.out.println("模拟记录日志...");
}
}

创建代理类

MyProxy.java
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
package com.itheima.demo01;

import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;

/**
* JDK代理类
*/
public class MyProxy implements InvocationHandler {
//声明目标类接口
private UserDao userDao;

//创建代理方法
public Object createProxy(UserDao userDao) {
this.userDao = userDao;
// 1.类加载器
ClassLoader classLoader = MyProxy.class.getClassLoader();
// 2.被代理对象实现的所有接口
Class[] classes = userDao.getClass().getInterfaces();
// 3.使用代理类,进行增强,返回的是代理对象
return Proxy.newProxyInstance(classLoader, classes, this);
}

/*
* 所有动态代理类的方法调用,都会交由invoke()方法去处理
* proxy 被代理的对象
* method 将要被执行的方法信息(反射)
* args 执行方法时需要的参数
*/
public Object invoke(Object proxy, Method method, Object[] args)
throws Throwable {
// 创建切面对象
MyAspect myAspect = new MyAspect();
// 前增强
myAspect.check_Permissions();
// 在目标类上调用方法,并传入参数
Object obj = method.invoke(userDao, args);
// 后增强
myAspect.log();
return obj;
}
}

这里 MyProxy 类实现了 InvocationHandler 接口,重写了 invoke 方法,在方法中实现了前增强和后增强的逻辑。通过 createProxy 方法创建代理对象,其中 Proxy.newProxyInstance 接收 3 个参数,分别为类加载器、被代理对象实现的接口和 InvocationHandler 实例。

创建测试类

JDKTest.java
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
package com.itheima.demo01;

public class JDKTest {
public static void main(String[] args) {
// 创建代理对象
MyProxy jdkProxy = new MyProxy();
// 创建目标对象
UserDao userDao = new UserDaoImpl();
// 从代理对象中获取增强后的目标对象
UserDao userDao1 = (UserDao) jdkProxy.createProxy(userDao);
// 执行方法
userDao1.addUser();
userDao1.deleteUser();
}
}

CGLib(Code Generation Library)动态代理

JDK 动态代理只能对接口进行代理,而 CGLib 动态代理可以对类进行代理。CGLib 通过继承目标类并重写方法来实现代理功能。

编写类

UserDao.java
1
2
3
4
5
6
7
8
9
10
11
12
package com.itheima.demo02;

//目标类
public class UserDao {
public void addUser() {
System.out.println("添加用户");
}

public void deleteUser() {
System.out.println("删除用户");
}
}

编写 CGLib 代理类

CglibProxy.java
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
package com.itheima.demo02;

import com.itheima.demo01.MyAspect;
import org.springframework.cglib.proxy.Enhancer;
import org.springframework.cglib.proxy.MethodInterceptor;
import org.springframework.cglib.proxy.MethodProxy;

import java.lang.reflect.Method;

// 代理类
public class CglibProxy implements MethodInterceptor {
// 代理方法
public Object createProxy(Object target) {
// 创建一个动态类对象
Enhancer enhancer = new Enhancer();
// 确定需要增强的类,设置其父类
enhancer.setSuperclass(target.getClass());
// 添加回调函数
enhancer.setCallback(this);
// 返回创建的代理类
return enhancer.create();
}

/**
* proxy CGlib根据指定父类生成的代理对象
* method 拦截的方法
* args 拦截方法的参数数组
* methodProxy 方法的代理对象,用于执行父类的方法
*/
public Object intercept(Object proxy, Method method, Object[] args,
MethodProxy methodProxy) throws Throwable {
// 创建切面类对象
MyAspect myAspect = new MyAspect();
// 前增强
myAspect.check_Permissions();
// 目标方法执行
Object obj = methodProxy.invokeSuper(proxy, args);
// 后增强
myAspect.log();
return obj;
}
}

这里用 methodProxy.invokeSuper(proxy, args) 来调用父类的方法,而不是使用反射的 method.invoke,因为 CGLib 生成的代理类是目标类的子类,如果使用反射调用方法,会导致递归调用。

创建测试类

CglibTest.java
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
package com.itheima.demo02;

// 测试类
public class CglibTest {
public static void main(String[] args) {
// 创建代理对象
CglibProxy cglibProxy = new CglibProxy();
// 创建目标对象
UserDao userDao = new UserDao();
// 获取增强后的目标对象
UserDao userDao1 = (UserDao) cglibProxy.createProxy(userDao);
// 执行方法
userDao1.addUser();
userDao1.deleteUser();
}
}

基于 XML 的 Spring AOP 配置

没错,上面两节和用 Spring 没什么关系。。。

需要引入 aspectjrt 包和 aspectjweaver 包的依赖:

1
2
3
4
5
6
7
8
9
10
11
12
<!-- aspectjrt包的依赖 -->
<dependency>
<groupId>org.aspectj</groupId>
<artifactId>aspectjrt</artifactId>
<version>1.9.1</version>
</dependency>
<!-- aspectjweaver包的依赖 -->
<dependency>
<groupId>org.aspectj</groupId>
<artifactId>aspectjweaver</artifactId>
<version>1.9.6</version>
</dependency>

Spring AOP 中的代理对象是基于 IoC 容器自动生成,无需开发者关注

创建接口和实现类

UserDao.java
1
2
3
4
5
6
7
8
9
10
11
package com.itheima.demo03;

public interface UserDao {
public void insert();

public void delete();

public void update();

public void select();
}
UserDaoImpl.java
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
package com.itheima.demo03;

public class UserDaoImpl implements UserDao {
public void insert() {
System.out.println("添加用户信息");
}

public void delete() {
System.out.println("删除用户信息");
}

public void update() {
System.out.println("更新用户信息");
}

public void select() {
System.out.println("查询用户信息");
}
}

创建切面类

XmlAdvice.java
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
package com.itheima.demo03;

import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.ProceedingJoinPoint;

public class XmlAdvice {
//前置通知
public void before(JoinPoint joinPoint) {
System.out.print("这是前置通知!");
System.out.print("目标类是:" + joinPoint.getTarget());
System.out.println(",被织入增强处理的目标方法为:" +
joinPoint.getSignature().getName());
}

//返回通知
public void afterReturning(JoinPoint joinPoint) {
System.out.print("这是返回通知(方法不出现异常时调用)!");
System.out.println("被织入增强处理的目标方法为:" +
joinPoint.getSignature().getName());
}

/**
* 环绕通知
* ProceedingJoinPoint 是JoinPoint子接口,表示可以执行目标方法
* 1.必须是Object类型的返回值
* 2.必须接收一个参数,类型为ProceedingJoinPoint
* 3.必须throws Throwable
*/
public Object around(ProceedingJoinPoint point) throws Throwable {
System.out.println("这是环绕通知之前的部分!");
//调用目标方法
Object object = point.proceed();
System.out.println("这是环绕通知之后的部分!");
return object;
}

//异常通知
public void afterException() {
System.out.println("异常通知!");
}

//后置通知
public void after() {
System.out.println("这是后置通知!");
}
}

advice 本质是这个 aspect 类当中具体的一个方法,当然要用 Spring 管理,所以这些都是 Java Bean

编写配置文件

applicatioContext.xml
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:aop="http://www.springframework.org/schema/aop"
xmlns="http://www.springframework.org/schema/beans"
xsi:schemaLocation="http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/aop
http://www.springframework.org/schema/aop/spring-aop.xsd">
<!-- 注册Bean -->
<bean name="userDao" class="com.itheima.demo03.UserDaoImpl"/>
<bean name="xmlAdvice" class="com.itheima.demo03.XmlAdvice"/>
<!-- 配置Spring AOP-->
<aop:config>
<!-- 指定切点 -->
<aop:pointcut id="pointcut" expression="execution(*
com.itheima.demo03.UserDaoImpl.*(..))"/>
<!-- 指定切面 -->
<aop:aspect ref="xmlAdvice">
<!-- 指定前置通知 -->
<aop:before method="before" pointcut-ref="pointcut"/>
<!-- 指定返回通知 -->
<aop:after-returning method="afterReturning"
pointcut-ref="pointcut"/>
<!-- 指定环绕方式 -->
<aop:around method="around" pointcut-ref="pointcut"/>
<!-- 指定异常通知 -->
<aop:after-throwing method="afterException"
pointcut-ref="pointcut"/>
<!-- 指定后置通知 -->
<aop:after method="after" pointcut-ref="pointcut"/>
</aop:aspect>
</aop:config>
</beans>

这里通过 <aop:config> 标签配置 Spring AOP,定义了一个切点 pointcut,表达式 execution(* com.itheima.demo03.UserDaoImpl.*(..)) 表示匹配 UserDaoImpl 类中所有方法的执行。然后通过 <aop:aspect> 标签指定切面,ref 属性指定切面所引用的 Bean,并通过 <aop:before><aop:after-returning><aop:around><aop:after-throwing><aop:after> 标签分别指定前置通知、返回通知、环绕通知、异常通知和后置通知,method 属性指定通知方法的名称,pointcut-ref 属性指定通知所应用的切点。

其中,切点表达式的一般格式为 execution([modifiers-pattern]? [return-type-pattern] [declaring-type-pattern]?[method-name-pattern]([parameters-pattern]) [throws-pattern]?),其中 ? 表示可选项
在这里的表达式 execution(* com.itheima.demo03.UserDaoImpl.*(..)) 中,* 表示匹配任意返回类型,com.itheima.demo03.UserDaoImpl 表示匹配 UserDaoImpl 类,.*(..) 表示匹配该类中所有方法的执行,允许任意数量/类型的参数

  • <aop:before> 表示前置通知,在目标方法执行之前执行
  • <aop:after-returning> 表示返回通知,在目标方法正常返回后执行
  • <aop:around> 表示环绕通知,可以在目标方法执行前后执行,并且可以控制目标方法是否执行,实际上是对目标方法的一个包装,控制目标方法的执行。
  • <aop:after-throwing> 表示异常通知,在目标方法抛出异常后执行
  • <aop:after> 表示后置通知,在目标方法执行后(无论是否抛出异常)执行

创建测试类

TestXml.java
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
package com.itheima.demo03;

import org.springframework.context.ApplicationContext;
import org.springframework.context.support.ClassPathXmlApplicationContext;

public class TestXml {
public static void main(String[] args) {
ApplicationContext context = new
ClassPathXmlApplicationContext("applicationContext.xml");
UserDao userDao = context.getBean("userDao", UserDao.class);
userDao.delete();
System.out.println();
userDao.insert();
System.out.println();
userDao.select();
System.out.println();
userDao.update();
}
}

基于注解的 Spring AOP 配置

编写配置文件

applicationContext-Anno.xml
1
2
3
4
5
6
7
8
9
10
11
12
13
14
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:aop="http://www.springframework.org/schema/aop"
xmlns="http://www.springframework.org/schema/beans"
xsi:schemaLocation="http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/aop
http://www.springframework.org/schema/aop/spring-aop.xsd">
<!-- 注册Bean -->
<bean name="userDao" class="com.itheima.demo03.UserDaoImpl"/>
<bean name="AnnoAdvice" class="com.itheima.demo04.AnnoAdvice"/>
<!-- 开启@aspectj的自动代理支持 -->
<aop:aspectj-autoproxy/>
</beans>

这里的 <aop:aspectj-autoproxy/> 标签用于开启基于注解的 Spring AOP 配置,Spring 会自动扫描带有 @Aspect 注解的类,并将其作为切面进行处理。

创建切面类

AnnoAdvice.java
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
package com.itheima.demo04;

import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.*;

@Aspect
public class AnnoAdvice {
//切点
@Pointcut("execution( * com.itheima.demo03.UserDaoImpl.*(..))")
public void poincut() {
}

//前置通知
@Before("poincut()")
public void before(JoinPoint joinPoint) {
System.out.print("这是前置通知!");
System.out.print("目标类是:" + joinPoint.getTarget());
System.out.println(",被织入增强处理的目标方法为:" +
joinPoint.getSignature().getName());
}

//返回通知
@AfterReturning("poincut()")
public void afterReturning(JoinPoint joinPoint) {
System.out.print("这是返回通知!");
System.out.println("被织入增强处理的目标方法为:" +
joinPoint.getSignature().getName());
}

//环绕通知
@Around("poincut()")
public Object around(ProceedingJoinPoint point) throws Throwable {
System.out.println("这是环绕通知之前的部分!");
//调用目标方法
Object object = point.proceed();
System.out.println("这是环绕通知之后的部分!");
return object;
}

//异常通知
@AfterThrowing("poincut()")
public void afterException() {
System.out.println("异常通知");
}

//后置通知
@After("poincut()")
public void after() {
System.out.println("这是后置通知!");
}
}

这里的 pointcut() 是 dummy 方法,用于定义切点,仅用于提供 id,并不会被执行,@Pointcut 注解指定切点表达式,其他通知注解(如 @Before@AfterReturning@Around@AfterThrowing@After)通过引用切点方法来指定通知的应用范围,而无需重复编写具体的切点表达式。

编写测试类

TestAnnotation.java
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
package com.itheima.demo04;

import com.itheima.demo03.UserDao;
import org.springframework.context.ApplicationContext;
import org.springframework.context.support.ClassPathXmlApplicationContext;

public class TestAnnotation {
public static void main(String[] args) {
ApplicationContext context = new
ClassPathXmlApplicationContext("applicationContext-Anno.xml");
UserDao userDao = context.getBean("userDao", UserDao.class);
userDao.delete();
System.out.println();
userDao.insert();
System.out.println();
userDao.select();
System.out.println();
userDao.update();
}
}