AOP : 在不惊动原始编程设计的思想上,增强文件的功能。
通知:一般我们把一个需要多个方法执行的功能(公共的)抽出来,封装到一个方法里,封装成一个类,成为通知类;
切入点:
另外上面说了一大堆就是,将公共的功能抽出来,挂载需要丰富功能的功能身上,增强功能。(功能 == 方法)
最好单独设一个包。
Mydvice.java
//通知类必须配置成Spring管理的bean
// 这2没区别
@Component
@ComponentScan
//设置当前类为切面类类
@Aspect
public class MyAdvice {
//设置切入点,要求配置在方法上方 返回值 全类名.方法名()
@Pointcut("execution(void com.itheima.dao.BookDao.save())")
private void pt() {
}
//绑定切入点和通知,这个在配置下方运行
@After("pt()")
public void method() {
System.out.println(System.currentTimeMillis());
}
}
// 我们公共功能抽出来, 设置切入点,你要切入的方法,看的出这个功能和原方法功能是独立的,
SpringConfig.java
@Configuration
@ComponentScan("com.itheima")
//开启注解开发功能
@EnableAspectJAutoProxy
public class SpringConfig {
}
AOP工作流程
先理解2个概念:如果我们的切入点找到连接点,最后执行的对象是代理对象,已经加了公共的功能了。
目标对象:原始功能去掉共性功能对应产生的类产生的对象,也被称为原始对象。
代理:目标对象无法直接完成代理工作,需要代理对象完成。
我们的共性功能,没有成功绑定,假如成功绑定上,打印出的getClass(),应该会被重写。
AOP切入点表达式
切入点:要进行增强的方法。
切入掉表达式:要进行增强方法的描述方法。
execution(public void com.qhx.dao.BookDao.update() 异常)
其中 public 和异常能省略,看来大家能看出来,访问修饰符、... 包名、类名、方法名等...
当我们要绑定多个方法时,万一数量大,需要写多个切入点,那不是很麻烦?
所以
注意,* 必须至少有1个,.. 是0个以上。这个通配符设置在* 里面能任意被替换。
- 总结一下:就是访问修饰符一般省略,因为一般是public,返回值类型,
- 包名,通常是* 代替单个包名;
- 接口名,如UserService 用 *Service代替。实现类,尽量不采用。
- 方法名:名词采用*匹配,动词采用精准匹配。如:getById 书写成 getBy*,看情况,selectAll 就不用替代。
- 异常一般省略,
AOP通知类型。
执行共性功能的位置
// 环绕通知:比较特殊,会把原方法覆盖掉,因此必须要设置调用原方法。理论上,环绕能把Before和After等能替代掉。
@Around("pt()")
public Object method(ProceedingJoinPoint point) throws Throwable {
System.out.println("before...");
System.out.println("after...");
point.proceed();
// 假如原方法有返回值,这个就是返回值,并返回出去,才算代理结束
// Integer proceed = (Integer) point.proceed();
// return proceed;
}
}
// 了解这2
@AfterReturning("pt()") // 原方法正常结束,才执行
@AfterThrowing("pt()") // 原方法异常抛出,才执行
@After
@Before
案例:为service包中每个*Service接口中每个方法,绑定一个测量万次执行时间的功能
1.我们这是个测试所以要引入测试类.
2.丰富方法的方法,所以要引入AOP
test包下的测试文件,配置Spring容器环境
@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(classes = SpringConfig.class)
public class AccountServiceTestCase {
@Autowired
private AccountService accountService;
@Test
public void testFindById() {
Account ac = accountService.findById(2);
}
aop包下的PrpjectAdvice.java advice 一般用来充当aop的通知类
@Component
@Aspect
public class ProjectAdvice {
// 默认监视业务层所有的方法,
// 1.返回值类型不确定,所以用* 替代;
// 2.方法名也不一致,所以 用* 替代
@Pointcut("execution(* com.itheima.service.*Service.*())")
public void servicePt() {
}
@Around("ProjectAdvice.servicePt()")
// 这是类名+方法名绑定,无所谓
public void service(ProceedingJoinPoint point) throws Throwable {
Signature signature = point.getSignature(); // 拿到你绑定方法的签名
String typeName = signature.getDeclaringTypeName(); // 绑定的类
String name = signature.getName(); // 绑定的方法
long start = System.currentTimeMillis();
for (int i = 0; i < 10000; i++) {
point.proceed();
}
long end = System.currentTimeMillis();
System.out.println("万次执行" + typeName + name + (start - end) + "ms");
}
}
好家伙,假如谁,你要为不同的方法装不同的逻辑呢?可以限制上述同一个共性功能。有不同的方法调用
环绕通知获取参数并调用
@Around("pt()")
public Object around(ProceedingJoinPoint pjp) {
Object[] args = pjp.getArgs(); // 获取调用参数
args[0] = 1212; // 修改原参数
Object proceed = null;
// 卧槽,我们可以获取调用方法的参数,但是在环绕通知中,本质上不就是要我们自己调用才能执行原方法吗?
// 我们可以对传入的参数进行矫健,如果不满足条件,就报错
try {
proceed = pjp.proceed(args); // 调用原方法传入修改
} catch (Throwable e) {
e.printStackTrace();
}
return proceed;
}
(了解)返回通知获取返回值,不过没啥用 。同样,异常通知获取异常对象,就把returning 换成throwing即可。
@AfterReturning(value = "pt()", returning = "ret")
public void returning(JoinPoint joinPoint, Object ret) {
System.out.println(ret);
}
案例:百度网盘分享衔接输入密码,自动删除空格
1.我们输入的密码,(有时候会莫名其秒多打一个空格),放到数据库里,进行比对,假如有空格会报错。
因此,我们可以用环绕通知对前端获取的参数进行删除空格操作,其实前端也有,这样的操作。
@Pointcut("execution(boolean com.itheima.service.*Service.*(*,*))") //
private void servicePt() {
}
@Around("DataAdvice.servicePt()")
public Object trimStr(ProceedingJoinPoint pjp) throws Throwable {
Object[] args = pjp.getArgs(); // 获取参数
// 单纯看类型是看不出的,
for (int i = 0; i < args.length; i++) {
//判断参数是不是字符串
if (args[i].getClass().equals(String.class)) {
args[i] = args[i].toString().trim(); // 如果是,将空格删除赋值给原先参数
}
}
Object ret = pjp.proceed(args); // 利用新参数调用
return ret;
}
看清楚,这个就是通过Class来对比,因为这个是类的反射是唯一的。
总结:
AOP本质上通过代理模式。