想必我们第一次接触这个概念一定会进行百度百科,那么我们先来看一下比较官方的说法
AOP就是Aspect Oriented Programming的缩写,也可以称为面向方面编程,它通过预编译和动态代理来实现,目的是对程序功能进行统一的维护。我们知道一个大型项目的成本不仅仅在于代码的编写,更是代码的维护,庞大的代码对维护很不友好。试想如果添加一个新的功能要在原有代码的基础上修改,而这部分代码又与很多地方的代码耦合,那么我们就要首先读懂相关部分的代码,并且在多个地方的源码进行修改,这是一件繁琐的事情。
那么开闭原则就是为解决这一问题
我们的代码要对拓展开放而对修改封闭,这样就会大大解耦,并且易于维护,而AOP可以实现这个设想。
举个例子: 比如游戏的登录过程
用户名密码 ——> 数据库查询 ——> 一系列校验 ——> 是否登录成功
需求: 由于有关部门对未成年游戏时间的有关规定,我要在在校验部分增加对目前时间和用户是否是未成年的相关校验
不好的做法: 直接在一系列校验的部分进行修改,这违反开闭原则
AOP做法: 扩展一个权限判断模块,配置到原来的流程中去
我们可以很轻松的在Spring中实现AOP,而Spring底层则是通过动态代理的方式实现AOP,我们实际中并不需要去自己完成动态代理的代码,但我们有必要去了解它的原理。所以这部分只是介绍一下底层原理。
对某个对象提供一种代理以控制这个对象的访问的模式就是代理模式。
比如原告由于某些原因不能或不想出庭那么可以由代理律师来代替原告出庭。这时律师就是原告的代理,律师代替原告执行原告和被告以及法官交涉。在一般情况下,代理律师更有经验更有专业知识,所以我们可以认为代理律师对原告的功能进行了增强,但是其实原告本身的能力并没有增强。回到代理模式,代理对象其实本身就隐含了一种对被代理对象功能增强的含义,也就是说代理对象并不改变被代理对象的真正的功能,但是从外部来看似乎是被代理对象功能有了增强。
再回到开闭原则,对修改封闭而对扩展开放,如何在不修改原来代码的基础增强原来代码的功能呢?如何在不增强原告本身法律知识等能力的情况下增大原告胜诉的几率呢?答案显而易见,为原告请代理律师,对原有代码进行代理!
代理模式的角色一般分为:抽象角色,真实角色,代理角色
抽象角色是真实角色和代理角色的共同接口
RealClass和ProxyClass都实现AbsInterface接口,而ProxyClass又聚合了RealClass,ProxyClass中的doAnotherthing相当于对被代理对象的增强。
//抽象接口
interface AbsInterface{
void doSomething();
}
//被代理类
class RealClass implements AbsInterface{
void doSomething(){
System.out.println("真实对象做一些事情!");
}
}
//代理类
class ProxyClass implements AbsInterface{
//持有被代理类的对象
private RealClass realClassObj;
void doSomething(){
if(realClassObj==null){
realClassObj = new RealClass();
realClassObj.doSomething();
}else {
realClassObj.doSomething();
}
//对被代理类的增强
doAnotherthing();
}
//增强方法
public void doAnotherthing(){
System.out.println("做一些其他事情!");
}
}
//客户端
class Client{
public static void main(String[] args) {
//创建代理类的对象
AbsInterface obj = new ProxyClass();
obj.doSomething();
}
}
想一想: 上述写法中,一个代理类可以为一个接口提供代理服务,但是如果有许多要被代理的类,那么我们就要写许多代理类,显然上述的方式不是很好的,因为上述方式中我们是已知被代理类是什么然后再去写代理类,这样必然是一对一的。这种写法也被称为静态代理模式。
那么有没有可能我们只写一个代理类,让它根据需要有时去对A进行代理,有时对B进行代理呢?那么这种写法我们称为动态代理,因为被代理类是动态的不确定的,所以称动态代理!
动态代理分为JDK代理和cglib代理,JDK代理应用于接口实现类的代理,它的特征就是存在接口,而cglib代理没有接口。JDK代理的实现方式是基于反射的,也就是需要使用java.lang.reflect包。
需要注意,ProxyFactory根本不需要实现ITeacherDao接口!!
//接口
interface ITeacherDao {
void teach();
int sayHello(String name);
}
//接口实现类
class TeacherDao implements ITeacherDao {
@Override
public void teach() {
System.out.println("老师上课...");
}
@Override
public int sayHello(String name) {
System.out.println("hello " + name);
return 111;
}
}
//动态代理类
class ProxyFactory {
//持有被代理对象
private Object target;
public ProxyFactory(Object target) {
this.target = target;
}
//给目标对象生成代理对象
public Object getProxyInstacne() {
/*1.loader->ClassLoader(类加载器)->指定当前目标对象使用的类加载器
2.interfaces->目标对象使用的接口类型->使用泛型方式确认类型
3.InvocationHandler-事件处理,执行目标对象的方法,出发事件处理器的方法
*/
return Proxy.newProxyInstance(target.getClass().getClassLoader(), target.getClass().getInterfaces(), new InvocationHandler() {
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
System.out.println("JDK代理开始");
Object returnValue = method.invoke(target,args);
System.out.println("JDK代理提交");
return returnValue;
}
});
}
}
//客户端调用
class Client {
public static void main(String[] args) {
//创建目标对象
ITeacherDao target = new TeacherDao();
//给目标对象创建代理对象
ITeacherDao proxyInstacne = (ITeacherDao) new ProxyFactory(target).getProxyInstacne();
proxyInstacne.sayHello("兔兔");
}
}
我们重点关注Proxy.newProxyInstance()方法,这个方法是java.lang.reflect.Proxy提供给我们的
Proxy.newProxyInstance(target.getClass().getClassLoader(), target.getClass().getInterfaces(), new InvocationHandler() {
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
System.out.println("JDK代理开始");
Object returnValue = method.invoke(target, args);
System.out.println("JDK代理提交");
return returnValue;
}
});
第一个参数: 传入一个类加载器,我这里用的是target对象的类加载器,用其他的都可以的(target.getClass().getClassLoader())
第二个参数: 传入被代理类实现的接口,被代理类可以实现多个接口,这里的参数本身就是一个接口数组(target.getClass().getInterfaces())
第三个参数: 传入InvocationHandler接口的匿名实现类的对象并实现其中invoke方法
我们继续关注这个invoke方法
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
System.out.println("JDK代理开始");
Object returnValue = method.invoke(target, args);
System.out.println("JDK代理提交");
return returnValue;
}
method.invoke(target,args)就是调用了被代理类对应的方法,target是被代理类对象,args是参数
而前后两个println可以理解为对被代理类的增强。
最后我们来看客户端调用
//创建目标对象
ITeacherDao target = new TeacherDao
//给目标对象创建代理对象
ITeacherDao proxyInstacne = (ITeacherDao) new ProxyFactory(target).getProxyInstacne();
proxyInstacne.sayHello("兔兔");
注意转型,因为返回值是Object类型
上述是JDk代理,用来动态代理实现了接口的类,对于没有实现接口的类我们使用cglib代理
注意点
cglib代理
1>目标对象不需要实现接口
2>需要使用jar包
import net.sf.cglib.proxy.*;
3>被代理的类不能是final
先看类图
被代理类TeacherDao没有实现接口
代理类ProxyFactory需要实现MethodInterceptor接口
//被代理类
class TeacherDao {
public void teach(){
System.out.println("老师讲课....");
}
}
//代理类
public class ProxyFactory implements MethodInterceptor {
//维护一个目标对象
private Object target;
//构造器用来传入被代理对象
public ProxyFactory(Object target) {
this.target = target;
}
//返回代理对象
public Object getProxyInstance() {
//创建工具类
Enhancer enhancer = new Enhancer();
//设置父类
enhancer.setSuperclass(target.getClass());
//设置回调函数
enhancer.setCallback(this);
//创建子类,即代理对象
return enhancer.create();
}
@Override
public Object intercept(Object o, Method method, Object[] args, MethodProxy methodProxy) throws Throwable {
System.out.println("Cglib代理模式开始");
Object invoke = method.invoke(target, args);
System.out.println("Cglib代理提交");
return invoke;
}
}
//客户端调用
class Client {
public static void main(String[] args) {
//创建目标对象
TeacherDao target = new TeacherDao();
//代理
TeacherDao proxyInstance = (TeacherDao) new ProxyFactory(target).getProxyInstance();
proxyInstance.teach();
}
}
这里的intercept类似JDK代理的invoke方法
我们一直说AOP是为了在方法功能进行扩展的时候不去修改源代码而去实现的一种方式,所以引出增强这一概念,就是表示方法功能的扩展。
1>连接点
类中可以被增强的方法称为连接点,连接点描述了一种潜能
2>切入点
类中实际被增强的方法称为切入点。切入点描述了一种状态
3>通知(增强)
实际增强的逻辑部分成为通知或增强
前置通知:在被增强的方法之前执行
后置通知(返回通知):在被增强的方法返回后执行,出异常则不执行
环绕通知:在连接点前后执行
异常通知:在被增强的方法出现异常后执行
最终通知:无论连接点是正确执行还是抛异常,都会执行
4>切面
把通知应用到切入点的过程成为切面,切面描述了一种动作
5>切入点表达式
execution([权限修饰符] [返回类型] [类全路径] [方法名称]([参数列表]) )
例如:对com.tong.dao.UserDao类中的update方法进行增强
class UserDao{
public void update(int a,int b){
return;
}
}
那么切入点表达式为
execution(public void com.tong.dao.UserDao.update(int,int))
注意:
全类名和方法名之间用.连接
权限修饰符可以省略
例如:例如:对com.tong.dao.UserDao类中的所有方法进行增强
那么切入点表达式为
execution(public void com.tong.dao.UserDao.*(...))
注意:
参数列表写为...表示和任意类型数量的参数匹配
*表示所有方法
AOP是基于AspectJ实现的,AspectJ不是Spring的一部分,是独立的实现AOP的方式,spring使用AspectJ实现AOP,所以需要导入相关jar包
com.springsource.net.sf.cglib-2.2.0.jar
com.springsource.org.aopalliance-1.0.0.jar
com.springsource.org.aspectj.weaver-1.6.8.RELEASE.jar
1>创建一个类
class Judge{
public void judge(String name){
System.out.println("判断"+name+"是否可用");
}
}
2>创建增强类并编写增强逻辑
class JudgeProxy{
//前置通知
public void before(){
System.out.println("前置通知");
}
}
3>进行通知的配置
在 spring 配置文件中,引入context和aop空间,开启注解扫描
<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 basepackage="com.xxx.xxx">context:component-scan>
4>使用注解创建Judge和JudgeProxy的对象
也就是在两个类的上面使用 @Component 注解
@Component
class Judge{
public void judge(String name){
System.out.println("判断"+name+"是否可用");
}
}
@Component
class JudgeProxy{
//前置通知
public void before(){
System.out.println("前置通知");
}
}
5>在增强类上添加注解生成代理对象
也就是在JudgeProxy类上添加 @Aspect注解
@Component
@Aspect
class JudgeProxy{
//前置通知
public void before(){
System.out.println("前置通知");
}
}
6>在 spring 配置文件中开启生成代理对象
<aop:aspectj-autoproxy>aop:aspectj-autoproxy>
7>配置通知的类型
在增强类中用作增强的方法上添加通知类型注解,并且结合切入点表达式
@Component
@Aspect
class JudgeProxy{
//前置通知
@Before(value = "execution(public void com.xxx.xxx.Judge.judge(..))")
public void before(){
System.out.println("前置通知");
}
}
//相应的
后置通知 @AfterReturning
最终通知 @After
环绕通知 @Around
异常通知 @AfterThrowing
//环绕通知比较特殊
@Around(value = "execution(public void com.xxx.xxx.Judge.judge(..))")
public void around(ProceedingJoinPoint proceedingJoinPoint) throws Throwable {
System.out.println("环绕之前.........");
//被增强的方法执行
proceedingJoinPoint.proceed();
System.out.println("环绕之后.........");
}
}
可以看到环绕通知必须传入参数proceedingJoinPoiont决定在那里作为环绕前和环绕后的分界点
关于公共切入点的提取
可以注意到上面两个方法的切入点都是
execution(* com.atguigu.spring5.aopanno.User.add(..))
那么我们可以提取公共切入点便于代码重用
@Pointcut(value = "execution(public void com.xxx.xxx.Judge.judge(..))")
public void pointcut(){}
在增强类中任意定义一个方法pointcut,在上面加上@Pointcut注解
这样之后,在增强方法上注解简化为
@Before(value ="pointcut()")//上个方法名
public void before(){
System.out.println("前置通知");
}
}
关于增强类的优先级问题
多个增强类对一个方法进行增强时可以设置优先级
使用@Order注解
@Component
@Aspect
@Order(1)
class JudgeProxy{
//前置通知
public void before(){
System.out.println("前置通知");
}
}
@Order值越小优先级越高
关于如何彻底不用配置文件
上述虽然是基于注解进行AOP操作,但依然使用了配置文件,那么如何彻底放弃配置文件呢?
创建配置类,不需要创建 xml 配置文件
@Configuration
@ComponentScan(basePackages = {"com.xxx"})//扫描包
@EnableAspectJAutoProxy(proxyTargetClass = true)//设为true
public class ConfigAop {
}
实际中用注解实现AOP,但配置文件也可以实现AOP,这里做一个简单了解
1>创建增强类和被增强类
Judge和JudgeProxy
class Judge{
public void judge(String name){
System.out.println("判断"+name+"是否可用");
}
}
class JudgeProxy{
public void before(){
System.out.println("前置增强");
}
}
2>在 spring 配置文件中创建两个类对象
<bean id="judge" class="com.xxx.Judge">bean>
<bean id="judgeProxy" class="com.xxx.JudgeProxy">bean>
3>在 spring 配置文件中配置切入点
<aop:config>
<aop:pointcut id="p" expression="execution(public void com.xxx.Judge.judge(...)"/>
<aop:aspect ref="judgeProxy">
<aop:before method="before" pointcut-ref="p"/>
aop:aspect>
aop:config>