問題の0、従来の開発 - AOPの理由をご紹介
AOPを学習する前に、我々は問題を見て、問題は、データベースのトランザクションを制御することです:
**転送事業で、原因が生じると考えるべきさまざまな問題に、制御トランザクション、およびサービスのさまざまなメソッドを追加する必要がありますリピートコードは、それを私たちは、コード抽出を複製することができるかどうかを検討し、冗長なコードで、その結果、追加する必要があり、かつ適切なタイミングで実行ビジネスコードに挿入しますか?上記の説明は、以下のように抽象化することができます。
1. AOPとは何ですか
AOPは、アスペクト指向Programmming、すなわちアスペクト指向プログラミング(とも呼ばれるアスペクト指向プログラミング)の完全である、オブジェクト指向プログラミング(OOP)を補足するものです、今より成熟したプログラミングになっています。
伝統的なビジネスプロセスのコードでは、通常、トランザクション・ログ・レコードを運営しています。OOPの使用は、組み合わせや相続コード、それを達成するための再利用方法を、しかし、あなたは(ロギングなど)、特定の機能を実現したい場合は、同じコードは依然として各プロセスに配布されることができますが。この機能をオフにする、またはそれを変更したい場合は、この方法では、あなたはすべての関連する方法を変更する必要があります。これにより、開発者の作業負荷を増加させるだけでなく、符号誤り率を向上させるだけでなく、
この問題を解決するために、AOPのアイデアが続きます。コード中に分散された横方向に引か取らAOP機構は、横の方法により、この抽出機構、再コンパイルまたは実行時に実行される抽出アプリケーションコードを繰り返し、その後、それぞれの方法が抽出繰り返される、従来のOOPだけ垂直再利用親子関係を達成することができますので、OOPの考え方は明確に、実行することはできません。AOPは、新しいプログラミングアイデアですが、それはOOPに代わるものではありませんが、それはOOPの単なる延長とサプリメントです。
AOPに別々アスペクト(セクション)によって異なるクラスのメソッドでトランザクションログ、アクセス許可および異常な機能に追加していると考え。
AOPは、**の書き込みビジネスロジックだけでなく、開発の効率を向上させ、他のビジネスロジックのためあまり気にせず、自社のコアビジネスに集中するだけでなく、コードの保守性を向上することができたときに、開発者が使用することができます
目前流行的AOP框架有两个,分别是Spring AOP和AspectJ.Spring AOP使用纯Java实现,不需要专门的编译过程和类加载器,在运行期间通过代理方式向目标类植入增强的代码。AspectJ是一个基于Java语言的AOP框架。
1.1、AOP的作用和优势
DI能够让相互协作的软件组件保持松散耦合,而面向切面编程(aspect-oriented programming,AOP)允许你把遍布应用各处的功能分离出来形成可重用的组件。
面向切面编程往往被定义为促使软件系统实现关注点的分离的一项技术。系统由许多不同的组件组成,每一个组件各负责一块特定的功能。除了实现自身核心的功能之外,这些组件还经常承担着额外的职责。诸如日志,事务管理和安全这样的系统服务经常融入到自身具有核心业务逻辑组件中去,这些系统服务通常称为横切关注点,因为它们会跨越系统的多个组件。
如果将这些关注点分散到多个组件中去,你的代码将会带来多重的复杂性:
- 实现系统关注点功能的代码将会重复出现在多个组件中。这意味着如果要修改这些关注点的逻辑,必须修改各个模块中的相关实现。即使把这些关注点抽象为一个独立的模块,其他模块只是调用它的方法,但方法调用还是会重复出现在各个模块中。
- 组件会因为那些与自身核心业务无关的代码而变得混乱。一个向地址簿增加地址条目的方法应该只关注如歌添加地址,而不应该关注它是不是安全的或者是否需要支持事务。
AOP能够使这些服务模块化,并以声明的方式将他们应用到它们需要影响的组件中去。所造成的结果就是这些组件会具有更高的内聚性并且会更加关注自身的业务,完全不需要了解系统服务所带来的复杂性。总之,AOP能够确保POJO的简单性,
2、AOP原理:基于动态代理
在了解AOP是怎么来的,以及AOP的作用和优势之后我们再来看看AOP是怎么实现的,也就是AOP的底层原理是什么么?
查看官方的文档,我们看看AOP是怎么实现的?
通过以上分析,可以看出AOP的底层原理就是基于动态代理的,那么结合前面的IOC容器,可以看出 IOC容器使用了一种工厂模式,而AOP使用了代理模式。
AOP的代理模式分为两种,第一种是基于JDK动态代理,如果代理接口实现类使用这种,而如果是没有实现接口的类要实现代理采用CGLIB代理的方式。
2.1JDK代理
public class Client {
public static void main(String[] args) {
//被代理对象
final Producer producer = new Producer();
/**
* 动态代理特点:
* 特点:随用随创建,随用随加载
* 作用:不修该源码基础上对方法增强
* 分类:
* 基于接口动态代理
* 基于子类动态代理
* 基于接口动态代理:
* 涉及的类:Proxy
* 提供者:JDK官方
*
* 如何创建代理对象:
* 使用Proxy类中的newProxyInstance方法
*
* 创建代理对象的要求:
* 被代理类至少实现一个接口,如果没有则不能调用
*
*
*
* public static Object newProxyInstance(ClassLoader loader,
Class<?>[] interfaces,
InvocationHandler h)
* newProxyInstance 方法的参数:
* classLoader:类加载器,它是用于加载代理对象字节码的,和被代理对象使用相同的类加载器:固定写法
* Class[]:字节码数组,它是用于让代理对象和被代理对象有相同的方法(有相同的接口即可):固定写法
* InvocationHandler:用于提供增强的代码;它是让我们写如何代理。
* 我们一般都是写一个该接口的实现类,通常情况下都是匿名内部类,但不是必须的。
* 此接口的实现类,一般谁用谁写
*
*/
//被代理对象
Iproducer iproducer =
(Iproducer) Proxy.newProxyInstance(producer.getClass().getClassLoader(), producer.getClass().getInterfaces(), new InvocationHandler() {
/**
* 作用执行被代理对象的任何接口方法都会经过该方法,该方法有拦截的功能
* @param proxy 代理对象的引用 (一般不用)
* @param method 当前执行的方法
* @param args 当前执行方法所需要的参数
* @return 和被代理对象有相同的返回值
* @throws Throwable
*/
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
//提供增强的代码
Object returnValue = null;
//1、获取方法执行的参数
Float money = (Float)args[0];
//2、判断当前方法是不是销售
if("saleProduct".equals(method.getName()))
returnValue = method.invoke(producer,money*0.8f);
return returnValue;
}
});
iproducer.saleProduct(10000f);
}
2.2CGLIB代理
import com.iyheima.Iproducer;
import net.sf.cglib.proxy.Enhancer;
import net.sf.cglib.proxy.MethodInterceptor;
import net.sf.cglib.proxy.MethodProxy;
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;
/**
* @Author Zhou jian
* @Date 2019 ${month} 2019/11/21 0021 16:30
* 模拟一个消费者
*/
public class Client {
public static void main(String[] args) {
final Producer producer = new Producer();
/**
* 动态代理特点:
* 特点:随用随创建,随用随加载
* 作用:不修该源码基础上对方法增强
* 分类:
* 基于接口动态代理
* 基于子类动态代理
* 基于子类动态代理:
* 涉及的类:Enhancer
* 提供者:第三方 Cglib
*
* 如何创建代理对象:
* 使用Enhancer类中的create方法
*
* 创建代理对象的要求:
* 被代理类不能是最终类
*
* create方法的参数:
* class:字节码
* 它是指被代理对象的字节码
*
* callBack:用于提供增强的方法,我们一般写的都是该接口的子接口的实现类:MethodInterCeptor
* 特点:执行被代理对象的任何方法都会经过该方法
*
*/
Producer cglibProducer =(Producer) Enhancer.create(producer.getClass(), new MethodInterceptor() {
/**
*特点:执行被代理对象的任何方法都会经过该方法
* @param proxy:代理对象的引用 (一般不用)
* @param method 当前执行的方法
* @param args 当前执行方法所需要的参数
* @param methodProxy 当前执行方法的代理对象 (用不上)
* @return
* @throws Throwable
*/
public Object intercept(Object proxy, Method method, Object[] args, MethodProxy methodProxy) throws Throwable {
//提供增强的代码
Object returnValue = null;
//1、获取方法执行的参数
Float money = (Float)args[0];
//2、判断当前方法是不是销售
if("saleProduct".equals(method.getName()))
returnValue = method.invoke(producer,money*0.8f);
return returnValue;
}
});
cglibProducer.saleProduct(10000f);
}
3、AOP使用
在使用AOP之前声明一些术语:
使用AspectJ实现AOP有两种方式:一种是基于XML的生明式;另一种是基于注解式的声明。
在使用前导入相关的jar包:
<dependencies>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-context</artifactId>
<version>5.0.2.RELEASE</version>
</dependency>
<!-- 解析切入点表达式-->
<dependency>
<groupId>org.aspectj</groupId>
<artifactId>aspectjweaver</artifactId>
<version>1.8.7</version>
</dependency>
</dependencies>
在配置文件中,引入配置声明头文件:
<?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:aop="http://www.springframework.org/schema/aop"
xsi:schemaLocation="http://www.springframework.org/schema/beans
https://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/aop
https://www.springframework.org/schema/aop/spring-aop.xsd">
3.1、基于XML的AOP使用
配置步骤总结:
1、把通知Bean也交给spring来管理
2、使用 aop:config标签 表明开始aop的配置
3、使用 aop:aspect表明开始配置切面
id:是给切面提供唯一标志
ref:指定通知类的bean的id
4、在 aop:aspect标签的内部使用对应的标签来配置通知的类型
现在的示例是让logger方法printLogg在切入点之前执行,所以前置通知aop:before
aop:before:表示配置前置通知
method属性:用于指定logger类中哪个方法时前置通知
pointcut:用于指定切入点表达式,该表达式的含义是对业务层哪些方法增强
基于XML的声明式AspectJ是指通过XML文件来定义切面、切入点及通知,所有的切面、切入点和通知都必须定义在aop:config元素内。Spring配置文件中的元素下可以包含多个aop:config元素,一个aop:config元素中又可以包含属性和子元素,其子元素包括aop:pointcut、aop:advisor和aop:aspect.
(1)配置切面
在Spring的配置文件中,配置切面使用的是aop:aspect元素,该元素会将一个已定义好的Spring Bean转换成切面Bean,所以要在配置文件中先定义一个普通的SpringBean,定义完成之后,通过aop:aspecct元素的ref属性即可饮用该Bean.
(2)配置切入点
在Spring的配置文件中,切入点是通过aop:pointcut元素来定义的。当aop:pointcut元素作为aop:config元素的子元素定义时,表示该切入点是全局切入点,可以被多个切面所共享;当aop:pointcut元素作为aop:aspect元素的子元素时,表示该切入点只能对当前切面有效。
属性名称 | 描述 |
---|---|
id | 用于定义切入点的唯一标志名称 |
expression | 用于指定切入点关联的切入点表达式 |
切入点表达式的写法:
关键字: execution(表达式)
表达式:
访问修饰符 返回值 包名。包名.类名.方法名.(参数列表)
public void com.itheima.service.Impl.AccountServiceImpl. saveAccount()
①访问修饰符可以省略
void com.itheima.service.Impl.AccountServiceImpl. saveAccount()
②返回值可以使用通配符表示任意返回值
* com.itheima.service.Impl.AccountServiceImpl. saveAccount()
③包名可以使用通配符表示任意包,但是有几级包就需要些几个*
* *.*.*.*.AccountServiceImpl.saveAccount()
③可以使用..表示当前包及其自爆
* *..AccountServiceImpl.SaveAccount()
⑤类名和方法名都可以使用*实现通配
* *..* *()
⑥参数列表可以直接书序类型:
基本类性可以直接写名称
引用类型可以写包名.类名的方式
类型可以使用通配符,可以使用任意类型但必须有参数:*
可以使用 ..表示有误参数均可
全通配写法:
* *..*.*(..)
实际开发中切入点表达式,切到业务层实现类下所有方法
* com.itheima.Service.Impl.*.*(..)
(3)配置通知
在配置代码中,分别使用aop:aspect的子元素配置了5种常用通知,这些子元素不支持再使用子元素。但在使用时,可以指定一些属性:
属性名称 | 描述 |
---|---|
pointcut | 用于指定一个切入点表达式,Spring将在匹配该表达式的连接点时植入该通知 |
pointcut-ref | 指定一个已经存在的切入点名称,通常poincut和point-ref两个属性只需要使用其中之一 |
method | 指定一个方法名,指将切面Bean中的该方法转换为增强处理 |
throwing |
通知的种类:
配置环绕通知的注意事项:
/**
* 环绕通知
*
* 问题:当我们配置了环绕通知之后,切入点方法没有执行,而通知方法执行了。
* 分析:
* 通过对比动态代理中的环绕通知代码,发现动态代理放入环绕通知有明确的切入点方法调用,而我们的代码中没有
*
* Spring框架提供了一个接口: ProceedingJoinPoint,该接口有一个方法proceed(),此方法就相当于明确调用切入点方法。
* 该接口可以作为环绕通知的方法参数,在程序执行时,Spring框架为我们提供改接口的实现类,为我们使用。
Spring中的环绕通知:
它是Spring框架为我们提供的一种方式:可以在代码中手动控制增强方法何时执行的方式。
对应配置的方式;
*/
public Object aroundPrintLog(ProceedingJoinPoint pjp){
Object rtValue = null;
try {
Object[] args = pjp.getArgs();//得到方法执行所需要的参数
System.out.println("Logger类中的aroundPrintLog方法开始记录日志...前置");
rtValue = pjp.proceed(args);//明确调用业务层方法(切入点方法)
System.out.println("Logger类中的aroundPrintLog方法开始记录日志..后置");
return rtValue;
} catch (Throwable throwable) {
throwable.printStackTrace();
System.out.println("Logger类中的aroundPrintLog方法开始记录日志..异常");
throw new RuntimeException("");
}finally {
System.out.println("Logger类中的aroundPrintLog方法开始记录日志..最终");
}
}
3.2、注釈ベースのAOPの使用
XMLベースの宣言型プログラミングは、AspectJのは便利なものの、AOPを実現するが、いくつかの欠点は、あなたがこの問題を解決するために、ファイルの春のコード多くの情報を設定することがあり、AspectJのフレームワークは、春を置き換えるためにAOPを実現するためのアノテーションのセットを提供します肥大化したコードの構成されたAOP機能を実現するための設定ファイル:
ノート名 | 説明 |
---|---|
@側面 | セクションを定義するために使用されます |
@Pointcut | この方法を使用する場合、ポイントを定義するために使用される式は、エントリポイント名を表すために、名前および任意のパラメータ署名を定義する必要が挙げられます。実際には、このメソッドのシグネチャは、ボイドの戻り値であり、メソッド本体は、一般的な方法の空であります |
@前 | 事前通知を定義するために使用される、BeforeAdviceの対応は、使用時に、一般的に既存のエントリ・ポイントであってもよい(value属性値は、属性値を指定ポイントカット式を開発する必要があり、エントリポイントを直接発現定義することができます。スタイル) |
@AfterReturing | 通知後AfterReturingAdviceに対応する、対向定義するために使用されます。ポイントカットを指定するために使用する場合。 |
@Around | Value属性を指定するために使用される場合には、エントリポイント通知が移植された指定、アドバイスMethodInterceptorのと同等の周りに定義するために使用されます |
@AfterThrowing | 例外処理は、未処理の西に異常通知を定義するために使用され |
@After | かかわらず、例外があるかどうか、通知が実行されるの、最終的な最終的な通知を定義するために使用されます。 |
使用する前に、XML設定ファイルに次のように開く必要があります。
<!--配置Spring创建容器时要扫描的包-->
<context:component-scan base-package="com.itheima"></context:component-scan>
<!--配置spring开启注解Aop的支持-->
<aop:aspectj-autoproxy></aop:aspectj-autoproxy>
package com.itheima.utils;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.*;
import org.springframework.stereotype.Component;
/**
* @Author Zhou jian
* @Date 2019 ${month} 2019/11/21 0021 19:45
用于记录日志的工具类,它里面提供了公共的代码
*/
@Component("logger")
@Aspect //表述当前类是一个切面类
public class Logger {
@Pointcut("execution(* com.itheima.service.Impl.*.*(..))")
private void pt1(){}
/**
* 前置通知用于打印日志,计划让其在切入点方法之前执行(切入点方法就是业务层方法
*/
@Before("pt1()")
public void beforeprintLog(){
System.out.println("前置通知Logger类中的printLog方法开始记录日志");
}
/**
* 后置通知
*/
@AfterReturning("pt1()")
public void afterReturingprintLog(){
System.out.println("后置通知Logger类中的afterprintLog方法开始记录日志");
}
/**
* 异常通知
*/
@AfterThrowing("pt1()")
public void afterThrowingprintLog(){
System.out.println("异常通知Logger类中的afterReturingprintLog方法开始记录日志");
}
/**
* 最终通知
*/
@After("pt1()")
public void afterprintLog(){
System.out.println("最终通知Logger类中的afterprintLog方法开始记录日志");
}
/**
* 环绕通知
*
* 问题:当我们配置了环绕通知之后,切入点方法没有执行,而通知方法执行了。
* 分析:
* 通过对比动态代理中的环绕通知代码,发现动态代理放入环绕通知有明确的切入点方法调用,而我们的代码中没有
*
* Spring框架提供了一个接口: ProceedingJoinPoint,该接口有一个方法proceed(),此方法就相当于明确调用切入点方法。
* 该接口可以作为环绕通知的方法参数,在程序执行时,Spring框架为我们提供改接口的实现类,为我们使用。
Spring中的环绕通知:
它是Spring框架为我们提供的一种方式:可以在代码中手动控制增强方法何时执行的方式。
对应配置的方式;
*/
// @Around("pt1()")
public Object aroundPrintLog(ProceedingJoinPoint pjp){
Object rtValue = null;
try {
Object[] args = pjp.getArgs();//得到方法执行所需要的参数
System.out.println("Logger类中的aroundPrintLog方法开始记录日志...前置");
rtValue = pjp.proceed(args);//明确调用业务层方法(切入点方法)
System.out.println("Logger类中的aroundPrintLog方法开始记录日志..后置");
return rtValue;
} catch (Throwable throwable) {
throwable.printStackTrace();
System.out.println("Logger类中的aroundPrintLog方法开始记录日志..异常");
throw new RuntimeException("");
}finally {
System.out.println("Logger类中的aroundPrintLog方法开始记录日志..最终");
}
}