一、代理模式
在说AOP之前,首先绕个弯路去谈一谈代理模式。代理模式分为静态代理和动态代理,网上经常用租房子这件事来形象化代理模式这件事,这里也画一个简单的关系图。
角色分析:
- 抽象角色:共同完成的一件事,比如租房这件事,一般使用接口和抽象类来解决
- 真实角色:被代理的角色,一般是个具体的类
- 代理角色:代理真实角色,但是一般代理角色代理真实角色后,会增加一些附属操作
- 客户(租客):访问代理角色的人
1、静态代理
- 代码实例:
// 抽象角色 ==> 租房的行为
public interface Rent {
void rent();
}
// 真实角色 ==> 房东,需要被中介代理,常见的场景是原有的,或者纯粹的业务代码
public class Host implements Rent {
@Override
public void rent() {
System.out.println("房东要租房子了");
}
}
// 代理角色 ==> 代理真实角色,代理了真实角色之后,一般会有附属的操作(看房子,收中介费)
// 比如在业务代码之前增加操作日志
public class HostProxy implements Rent {
private Host host;
public HostProxy() {
}
public HostProxy(Host host) {
this.host = host;
}
@Override
public void rent() {
// 代理角色的附属操作1
this.beforeRent();
this.host.rent();
// 代理角色的附属操作2
this.afterRent();
}
private void beforeRent() {
System.out.println("带租客看房子");
}
private void afterRent() {
System.out.println("收房租");
}
}
// 租客 ==> 访问代理角色的人
// 实际的业务场景:对直接的老代码进行封装,在不改变直接代码的前提下,增加一些新的代码逻辑
public class Client {
public static void main(String[] args) {
Host host = new Host();
// 代理
HostProxy hostProxy = new HostProxy(host);
// 租客不需要直接面对房东,直接找中介租房即可
hostProxy.rent();
}
}
- 代理模式的好处:
- 可以使真实角色的操作更加纯粹,不用去关注一些公共的业务
- 公共的操作交给了代理角色,实现了业务分工
- 公共业务在扩展的时候,方便集中管理
- 缺点
- 一个代理角色只能代理一个真实角色
2、动态代理
- 在静态代理中一个代理角色只能代理一个真实角色,这其实就大大增加了代理的成本。而动态代理的出现就是为了解决上述这个问题,动态代理的代理对象是动态生成的,而不是一个具体的对象。
动态代理分为两大类:
- 基于接口的动态代理 –
JDK
的动态代理 - 基于类的动态代理 –
cglib
-
动态代理中两个关键:
Proxy
类 :代理/InvocationHandler
接口:调用处理程序InvocationHandler
--> 调用处理程度并返回一个结果Proxy
--> 生成动态代理实例
不管代理被调用的是何种方法,处理器被调用的一定是invoke()
方法,具体的工作方式如下:
(1)利用Proxy
生产代理对象
(2)假如proxy
的rent()
方法被调用,假如rent()
方法有一个参数
(3)proxy
紧接着会调用InvocationHandler
的invoke()
方法
(4)handler
会根据反射,去调用RealSubject
中的具体方法
-
上具体的代码
// 代理接口 public interface Rent { void rent(String s); }
// 真实对象,需要被代理的具体对象
public class Host implements Rent {
@Override
public void rent(String s) {
System.out.println("房东要租房子了");
System.out.println(s);
}
}
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;
// 动态代理的生产类
// 这段代码基本上固定的动态代码生成模板,是不用改造的
public class ProxyInvocationHandler implements InvocationHandler {
// 要代理的对象
private Object target;
// 利用Proxy的静态方法产生代理对象
public Object getProxy() {
return Proxy.newProxyInstance(target.getClass().getClassLoader(), target.getClass().getInterfaces(), this);
}
public void setRent(Rent rent){
this.target = rent;
}
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
log(method.getName());
Object result = method.invoke(target, args);
return result;
}
private void log(String msg) {
System.out.println("执行了" + msg + "方法");
}
}
// 实际要调用的对象
public class Client {
public static void main(String[] args) {
// 真实角色
Host host = new Host();
// 代理角色
ProxyInvocationHandler pih = new ProxyInvocationHandler();
// 通过调用程序处理角色来处理我们要调用的接口对象
pih.setRent(host);
Rent proxy = (Rent)pih.getProxy();
proxy.rent("租金为3k");
}
}
二、进入AOP
讲完了代理模式之后,就可以更好的来看看AOP是怎么回事,AOP的本质就是利用动态代理来实现对整个spring
代码的无侵入式的修改增强,把和主业务无关的代码逻辑完全剥离出去了。
1.AOP在spring
中的作用
提供声明式事务,允许用户自定义切面
下面列出一些关键词,帮助理解,记不住这个术语也无关紧要
- 横切关注点:跨越程序多个模块的方法或功能。也就是和业务无关的,但是需要关注的部分。比如操作日志,参数校验
- 切面:横切关注点被模块化的特殊对象,是一个类。比如LOG
- 通知:切面要完成的工作,就是要执行的方法
- 目标:要通知的对象
- 切入点:切面通知要执行的位置
在SpringAOP
中,通过Advice
定义横切逻辑,Spring
中支持5种类型的Advice
- 前置通知【
MethodBeforeAdvice
】:before advice
在方法执行前执行 - 后置通知【
AfterReturningAdvice
】:after returning advice
在方法执行后返回一个结果后执行。 - 异常抛出通知【
AfterThrowing
】:after throwing advice
在方法执行过程中抛出异常的时候执行。 - 环绕通知【
AspectJAroundAdvice
】:around advice
在方法执行前后和抛出异常时执行,相当于综合了以上三种通知。
2.使用spring
实现AOP
使用AOP
织入,需要导入的依赖包:
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-aop</artifactId>
</dependency>
<dependency>
<groupId>org.aspectj</groupId>
<artifactId>aspectjweaver</artifactId>
<version>1.9.4</version>
</dependency>
2.1方式一: 使用spring
的API
接口
API
定义了每种类型的Advice
接口,通过实现不同的Advice
接口来实现不同的通知方式,下面的例子中使用了前置通知和后置通知
-
配置
AOP
,需要在spring
启动的时候扫描导入AOP
的配置文件(1)方式一的
AOP
配置文件<?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 http://www.springframework.org/schema/beans/spring-beans-2.5.xsd http://www.springframework.org/schema/aop http://www.springframework.org/schema/aop/spring-aop-2.5.xsd"> <!--方式一:使用spring的API接口--> <!-- 配置AOP: 需要导入AOP约束--> <aop:config> <!--切入点--> <!--id:在下面的配置中使用定位切入点--> <!--expression: 表达式,指定切入点作为的具体位置,具体表达式语法可google--> <aop:pointcut id="pointcut" expression="execution(* com.example.demo.service.aopservice..*(..))"/> <!--指定切入方式--> <!--advice-ref 具体切入的方法,取值必须是spring管理的bean. 比如这里的 beforeLog, afterLog--> <!--pointcut-ref: 哪个切入点,对应aop:pointcut 的id--> <aop:advisor advice-ref="beforeLog" pointcut-ref="pointcut"/> <aop:advisor advice-ref="afterLog" pointcut-ref="pointcut"/> </aop:config> </beans>
(2)
spring
启动时导入配置文件
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.ImportResource;
@Configuration
@ComponentScan("com.example")
@ImportResource(locations = {"classpath:aopLogAspect.xml"})
public class AppConfig {
}
-
定义具体的处理器(这里以前置处理器和后置处理器为例),需要注意的是,这里定义的处理器是要交给
spring
管理的bean
import org.springframework.aop.MethodBeforeAdvice; import org.springframework.stereotype.Component; import java.lang.reflect.Method; @Component public class BeforeLog implements MethodBeforeAdvice { /** * before 前置执行 * * @param method 要执行的目标对象的方法 * @param args 方法的参数 * @param target 目标对象 */ @Override public void before(Method method, Object[] args, Object target) throws Throwable { System.out.println(target.getClass().getName() + "的" + method.getName() + "被执行了"); } }
import org.springframework.aop.AfterReturningAdvice;
import org.springframework.stereotype.Component;
import java.lang.reflect.Method;
@Component
public class AfterLog implements AfterReturningAdvice {
/**
* afterReturning 后置通知
*
* @param returnValue 方法执行的放回值
* @param method 要执行的目标对象的方法
* @param args 方法的参数
* @param target 目标对象
*/
@Override
public void afterReturning(Object returnValue, Method method, Object[] args, Object target) throws Throwable {
System.out.println("执行了" + method.getName() + "返回结果为:" + returnValue);
}
}
- 具体的业务代码
import org.springframework.stereotype.Component;
@Component
public class UserService {
public void create() {System.out.println("创建一个用户");}
public void delete() {System.out.println("删除一个用户");}
public void update() {System.out.println("更新一个用户");}
public void read() {System.out.println("查询一个用户");}
}
-
调用具体的业务代码以及返回的结果
import com.example.demo.config.AppConfig; import com.example.demo.service.aopservice.UserService; import org.springframework.boot.autoconfigure.SpringBootApplication; import org.springframework.context.annotation.AnnotationConfigApplicationContext; @SpringBootApplication public class DemoApplication { public static void main(String[] args) { AnnotationConfigApplicationContext ac = new AnnotationConfigApplicationContext(AppConfig.class); UserService userService = ac.getBean(UserService.class); userService.create(); } }
2.2方式二:自定义类实现AOP
自定义类作为切面,在配置文件中引入切面,aop:aspect
,引入切面后,相关配置会有具体的通知方式来选择
- 方式二的
AOP
配置文件
<?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
http://www.springframework.org/schema/beans/spring-beans-2.5.xsd
http://www.springframework.org/schema/aop
http://www.springframework.org/schema/aop/spring-aop-2.5.xsd">
<!--方式二:自定义类实现`AOP`-->
<!-- 配置AOP: 需要导入AOP约束-->
<aop:config>
<!--自定义切面-->
<!--ref 引入一个切面(spring管理的bean)-->
<aop:aspect ref="diyPointAspect">
<!--切入点-->
<!--id:在下面的配置中使用定位切入点-->
<!--expression: 表达式,指定切入点作为的具体位置,具体表达式语法可google-->
<aop:pointcut id="pointcut" expression="execution(* com.example.demo.service.aopservice..*(..))"/>
<!--通知-->
<!--在aop:aspect标签下,会出现子标签,来定义具体的通知方式,比如:aop:before/aop:after -->
<aop:before method="before" pointcut-ref="pointcut"/>
<aop:after method="after" pointcut-ref="pointcut"/>
</aop:aspect>
</aop:config>
</beans>
- 自定义切面
import org.springframework.stereotype.Component;
@Component
public class DiyPointAspect {
public void before() { System.out.println("==========方法执行前===========");}
public void after() { System.out.println("==========方法执行后===========");}
}
-
具体业务代码(和方式一相同,略)
-
调用具体的业务代码以及返回的结果(略)
-
小彩蛋(嘻嘻嘻嘻嘻)
(1)
AOP-execution
表达式后面支持注解的形式,比如aop
的配置文件中的aop:pointcut
可以修改为:<aop:pointcut id="pointcut" expression="@annotation(com.example.demo.service.aopservice.LogPointCut)"/>
(2)定义一个注解
import java.lang.annotation.Documented; import java.lang.annotation.ElementType; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.lang.annotation.Target; @Documented @Target(ElementType.METHOD) @Retention(RetentionPolicy.RUNTIME) public @interface LogPointCut { }
(3)将注解作用到对应的方法上
@Component public class UserService { @LogPointCut public void create() { System.out.println("创建一个用户"); } @LogPointCut public void delete() { System.out.println("删除一个用户");} @LogPointCut public void update() { System.out.println("更新一个用户");} @LogPointCut public void read() { System.out.println("查询一个用户");} }
2.3方式三:使用注解方式实现AOP
这种方式和方式二基本上是一致的,只不过是用注解来代替了配置文件
(1)注解配置的切面,通过@Aspect
来告诉spring
这是一个切面。通过@Before
/@After
来告诉spring
具体的通知方式
注意:这里如果不扫描配置文件(因为不用写配置文件,所以肯定没的扫描了),就需要用注解的方式告诉spring
这是配置类,可以通过@Configuration
注解,当然你也可以通过配置文件的方式
import org.aspectj.lang.annotation.After;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import org.springframework.context.annotation.Configuration;
@Configuration // 告诉spring这是配置类
@Aspect // 告诉spring这是一个切面
public class AnnotationPointAspect {
@Before("execution(* com.example.demo.service.aopservice..*(..))")
public void before() { System.out.println("==========方法执行前===========");}
@After("execution(* com.example.demo.service.aopservice..*(..))")
public void after() {System.out.println("==========方法执行后===========");}
}
-
具体业务代码(和方式一相同,略)
-
调用具体的业务代码以及返回的结果(略)