先看一个例子:
声明一个接口:
java
// + - * / 运算的标准接口!
public interface Calculator {
int add(int i, int j);
int sub(int i, int j);
int mul(int i, int j);
int div(int i, int j);
}
实现该接口:
java
package com.sunsplanter.proxy;
/**
* 在每个方法中,输出传入的参数和计算后的返回结果!
*/
public class CalculatorLogImpl implements Calculator {
@Override
public int add(int i, int j) {
System.out.println("参数是:" + i + "," + j);
int result = i + j;
System.out.println("方法内部 result = " + result);
return result;
}
@Override
public int sub(int i, int j) {
System.out.println("参数是:" + i + "," + j);
int result = i - j;
System.out.println("方法内部 result = " + result);
return result;
}
@Override
public int mul(int i, int j) {
System.out.println("参数是:" + i + "," + j);
int result = i * j;
System.out.println("方法内部 result = " + result);
return result;
}
@Override
public int div(int i, int j) {
System.out.println("参数是:" + i + "," + j);
int result = i / j;
System.out.println("方法内部 result = " + result);
return result;
}
}
存在的问题:
- 核心功能与边缘功能掺杂(即计算的过程和sout掺杂)
- 边缘功能尽管重复,但却大量分散,不利于管理
目标: 将重复的代码统一提取,并且[[动态插入]]到每个业务方法!
用代理模式解决:
相关术语:
- 代理:将非核心逻辑剥离出来以后,封装这些非核心逻辑的类、对象、方法。(中介)
- 动词:指做代理这个动作,或这项工作
- 名词:扮演代理这个角色的类、对象、方法
- 目标:被代理"套用"了核心逻辑代码的类、对象、方法。(房东)
解决问题的思维:AOP
解决问题技术:代理技术
代理技术太麻烦,因此使用框架
Spring AOP框架(底层是代理技术:jdk动态daili,cglib)
代理在开发中实现的方式具体有两种:静态代理,[动态代理技术]
1. 静态代理解决了功能掺杂
缺点:静态代理确实实现了解耦,但是由于代码都写死了,完全不具备任何的灵活性。就拿sout来说,将来其他地方也需要sout,那还得再声明更多个静态代理类,那就产生了大量重复的代码,sout还是分散的,没有统一管理。
项目结构:
原有代码不变,
使用静态代理即主动创建一个代理类CalculatorStaticProxy实现接口,:
java
public class CalculatorStaticProxy implements Calculator {
// 将被代理的目标对象声明为成员变量(令代理类与被代理对象建立关系)
private Calculator target;
//将代理对象传入构造方法,"注入"
public CalculatorStaticProxy(Calculator target) {
this.target = target;
}
@Override
public int add(int i, int j) {
// 附加功能由代理类中的代理方法来实现
System.out.println("参数是:" + i + "," + j);
// 通过目标对象来实现核心业务逻辑
int addResult = target.add(i, j);
System.out.println("方法内部 result = " + result);
return addResult;
}
@Override
public int sub(int i, int j) {
// 附加功能由代理类中的代理方法来实现
System.out.println("参数是:" + i + "," + j);
// 通过目标对象来实现核心业务逻辑
int subResult = target.sub(i, j);
System.out.println("方法内部 result = " + result);
return subResult;
}
......
提出进一步的需求:将sout集中到一个代理类中,将来有任何日志需求,都通过这一个代理类来实现。这就需要使用动态代理技术了。
2. 动态代理进一步解决了集中管理边缘功能
动态代理技术分类
- JDK动态代理:JDK原生的实现方式,需要被代理的目标类必须实现接口(也即 必须有接口)!他会根据目标类的接口动态生成一个代理对象!代理对象和目标对象有相同的接口!(接口作为父亲,代理对象和目标对象拜把子)
- cglib:通过继承被代理的目标类实现代理,可以代理没有实现接口的类!(也即不需要有接口,代理对象认目标对象为干爹)
JDK动态代理技术实现(了解)
代理工程:基于jdk代理技术,生成代理对象
在这里我们不给出代理的具体实现代码,后续会学习Spring AOP框架,该框架封装动态代理技术,们只需写少量的配置,指定生效范围即可,即可完成面向切面思维编程的实现!
3. AOP思维
-
OOP引入封装、继承、多态等概念来建立一种对象层次结构,用于模拟公共行为的一个集合。OOP允许开发者定义纵向的关系。但在OOP设计中,它导致了大量公共代码的重复,而不利于各个模块的重用。
-
AOP技术恰恰相反,它利用一种称为"横切"的技术,剖解开封装的对象内部,并将那些影响了多个类的公共行为封装到一个可重用模块,并将其命名为"Aspect",即切面。所谓"切面",简单说就是那些与业务无关,却为业务模块所共同调用的逻辑或责任封装起来,便于减少系统的重复代码,降低模块之间的耦合度,并有利于未来的可操作性和可维护性。
-
使用AOP,可以在不修改原来代码的基础上添加新功能。
-
AOP思想主要的应用场景
- 日志记录:在系统中记录日志是非常重要的,可以使用AOP来实现日志记录的功能,可以在方法执行前、执行后或异常抛出时记录日志。
- 事务处理:在数据库操作中使用事务可以保证数据的一致性,可以使用AOP来实现事务处理的功能,可以在方法开始前开启事务,在方法执行完毕后提交或回滚事务。
- 安全控制:在系统中包含某些需要安全控制的操作,如登录、修改密码、授权等,可以使用AOP来实现安全控制的功能。可以在方法执行前进行权限判断,如果用户没有权限,则抛出异常或转向到错误页面,以防止未经授权的访问。
- 性能监控:在系统运行过程中,有时需要对某些方法的性能进行监控,以找到系统的瓶颈并进行优化。可以使用AOP来实现性能监控的功能,可以在方法执行前记录时间戳,在方法执行完毕后计算方法执行时间并输出到日志中。
- 异常处理:系统中可能出现各种异常情况,如空指针异常、数据库连接异常等,可以使用AOP来实现异常处理的功能,在方法执行过程中,如果出现异常,则进行异常处理(如记录日志、发送邮件等)。
- 缓存控制:在系统中有些数据可以缓存起来以提高访问速度,可以使用AOP来实现缓存控制的功能,可以在方法执行前查询缓存中是否有数据,如果有则返回,否则执行方法并将方法返回值存入缓存中。
- 动态代理:AOP的实现方式之一是通过动态代理,可以代理某个类的所有方法,用于实现各种功能。
-
AOP术语名词介绍
1-横切关注点
从每个方法中抽取出来的同一类非核心业务。在同一个项目中,我们可以使用多个横切关注点对相关方法进行多个不同方面的增强。
有十个附加功能,就有十个横切关注点。
AOP把软件系统分为两个部分:核心关注点和横切关注点。业务处理的主要流程是核心关注点,与之关系不大的部分是横切关注点。横切关注点的一个特点是,他们经常发生在核心关注点的多处,而各处基本相似,比如权限认证、日志、事务、异常等。AOP的作用在于分离系统中的各种关注点,将核心关注点和横切关注点分离开来。
2-通知(增强)
每一个横切关注点上要做的事情都需要写一个方法来实现,这样的方法就叫通知方法。就是具体你要织入的代码。
- 前置通知:在被代理的目标方法前执行
- 返回通知:在被代理的目标方法成功结束后执行(寿终正寝)
- 异常通知:在被代理的目标方法异常结束后执行(死于非命)
- 后置通知:在被代理的目标方法最终结束后执行(盖棺定论)
- 环绕通知(最常用):使用try...catch...finally结构围绕整个被代理的目标方法,包括上面四种通知对应的所有位置。
3-切入点 pointcut
定位连接点的方式,或者可以理解成被选中的连接点!
是一个表达式,比如execution(* com.spring.service.impl..(...))。符合条件的每个方法都是一个具体的连接点。
4-切面 aspect
切入点和通知的结合。是一个类。
3-连接点 joinpoint
这也是一个纯逻辑概念,不是语法定义的。
指那些被拦截到的点。在 Spring 中,可以被动态代理拦截目标类的方法
6-目标 target
被代理的目标对象。
7-代理 proxy
向目标对象应用通知之后创建的代理对象。
8-织入 weave
指把通知应用到目标上,生成代理对象的过程。可以在编译期织入,也可以在运行期织入,Spring采用后者。
3.1 AOP封装层次
3.2 注解实现AOP
3.2.1 切点表达式:定义通知(Advice)往哪些方法上切入。
切入点表达式语法格式:
execution([访问控制权限修饰符] 返回值类型 [全限定类名]方法名(形式参数列表) [异常])
访问控制权限修饰符:
- 可选项。没写,就是4个权限都包括。
- 写public就表示只包括公开的方法。
返回值类型:
- 必填项。
- *表示返回值类型任意。
全限定类名:
- 可选项。
- 两个点"..."代表当前包以及子包下的所有类。
- 省略时表示所有的类。
方法名:
-
必填项。
-
*表示所有方法。
-
set*表示所有的set方法。
r
形式参数列表: -
必填项
-
() 表示没有参数的方法
-
(...) 参数类型和个数随意的方法
-
(*) 只有一个参数的方法
-
(*, String) 第一个参数类型随意,第二个参数是String的。
异常:
- 可选项。
- 省略时表示任意异常类型。
3.2.2 简单演示AOP的实现
- 首先是导入依赖,实现AOP需要导入Spring-aspect,但Spring-aop包括了Spring-aspect,Spring-context又包括了Spring-aop,因此之前导入的Spring-context就够了。
- 在spring配置文件中增加命名空间; 启用自动代理
xml
<?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="com.powernode.spring6.service"/>
<!--开启自动代理后,凡是带有@Aspect注解的bean都会生成代理对象。
proxy-target-class是可选属性,默认为false时采用JDK动态代理,
但即便是最坏的情况,即采用JDK动态代理,而类又没有实现接口,Spring仍会自动转向cglib动态代理-->
<aop:aspectj-autoproxy proxy-target-class="true"/>
</beans>
</beans>
- 定义目标类以及目标方法, 纳入Spring管理
java
package com.sunsplanter.spring6.service;
// 目标类
@Component
public class OrderService {
// 目标方法
public void generate(){
System.out.println("订单已生成!");
}
}
第二步:定义切面类, 纳入Spring管理
java
package com.sunsplanter.spring6.service;
import org.aspectj.lang.annotation.Aspect;
// 切面类的注解
@Aspect
@Component
public class MyAspect {
}
第三步:在Spring配置文件中添加组建扫描
cpp
<?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="com.powernode.spring6.service"/>
</beans>
第四步: 在切面类中定义一个切面
java
package com.sunsplanter.spring6.service;
import org.aspectj.lang.annotation.Before;
import org.springframework.stereotype.Component;
import org.aspectj.lang.annotation.Aspect;
// 切面类
@Aspect
@Component
public class MyAspect {
//通知+切点=切面
// 切点表达式,@Before即前置通知
@Before("execution(* com.powernode.spring6.service.OrderService.*(..))")
// 这就是需要增强的代码(通知)
public void advice(){
System.out.println("我是一个通知");
}
}
第五步:测试程序:
java
package com.sunsplanter.spring6.test;
import com.sunsplanter.spring6.service.OrderService;
import org.junit.Test;
import org.springframework.context.ApplicationContext;
import org.springframework.context.support.ClassPathXmlApplicationContext;
public class AOPTest {
@Test
public void testAOP(){
ApplicationContext applicationContext = new ClassPathXmlApplicationContext("spring-aspectj-aop-annotation.xml");
OrderService orderService = applicationContext.getBean("orderService", OrderService.class);
orderService.generate();
}
}
目标:横向插入增强代码(日志输出/)