目录
- 基于注解的 AOP 开发
- 1 快速入门
- 2 注解通知的类型
- 3 企业开发经验
- 4 综合案例
- 5 代理设计模式介绍
- 静态代理
- 动态代理技术
- 代理模式的选择
- 面试
- AOP是什么
- 动态代理的分类
- 在开发中经常的通知有哪些
- 要想对某个方法进行增强有几种方式
基于注解的 AOP 开发
1 快速入门
1.1 注解开发AOP制作步骤
在XML格式基础上
- 导入坐标(伴随spring-context坐标导入已经依赖导入完成)
- 开启AOP注解支持
- 配置切面@Aspect
- 定义专用的切入点方法,并配置切入点@Pointcut
- 为通知方法配置通知类型及对应切入点@Before
1.2 注解开发AOP注意事项
1.切入点最终体现为一个方法,无参无返回值,无实际方法体内容,但不能是抽象方法
2.引用切入点时必须使用方法调用名称,方法后面的()不能省略
3.切面类中定义的切入点只能在当前类中使用,如果想引用其他类中定义的切入点使用“类名.方法名()”引用
4.可以在通知类型注解后添加参数,实现XML配置中的属性,例如after-returning后的returning属性
1.3 创建目标接口和目标类(内部有切点)
public interface TargetInterface {
public void method();
}
public class Target implements TargetInterface {
@Override
public void method() {
System.out.println("Target running....");
}
}
1.4 创建切面类
public class MyAspect {
//前置增强方法
public void before(){
System.out.println("前置代码增强.....");
}
}
1.5 将目标类和切面类的对象创建权交给 spring
@Component("target")
public class Target implements TargetInterface {
@Override
public void method() {
System.out.println("Target running....");
}
}
@Component("myAspect")
public class MyAspect {
public void before(){
System.out.println("前置代码增强.....");
}
}
1.6 在切面类中使用注解配置织入关系
@Component("myAspect")
@Aspect
public class MyAspect {
@Before("execution(* com.itheima.aop.*.*(..))")
public void before(){
System.out.println("前置代码增强.....");
}
}
或者把切点表达式抽取出来
@Component("myAspect")
@Aspect
public class MyAspect {
@Pointcut(""execution(* com.itheima.aop.*.*(..))"")
public void pt(){}
@Before("pt()")
public void before(){
System.out.println("前置代码增强.....");
}
}
1.7 在配置文类中开启组件扫描和 AOP 的自动代理
@Configuration
@ComponentScan("com.itheima")
@EnableAspectJAutoProxy
public class SpringConfig {
}
1.8 测试代码
@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(classes=SpringConfig.class)
public class AopTest {
@Autowired
private TargetInterface target;
@Test
public void test1(){
target.method();
}
}
1.9 测试结果
2 注解通知的类型
通知的配置语法:@通知注解(“切点表达式”)
2.1 AOP注解详解
2.1.1 @Aspect
- 名称:@Aspect
- 类型:注解
- 位置:类定义上方
- 作用:设置当前类为切面类
- 格式:
@Aspect
public class AopAdvice {
} - 说明:一个beans标签中可以配置多个aop:config标签
2.2.2 @Pointcut
- 名称:@Pointcut
- 类型:注解
- 位置:方法定义上方
- 作用:使用当前方法名作为切入点引用名称
- 格式:
@Pointcut(“execution(* *(…))”)
public void pt() {
} - 说明:被修饰的方法忽略其业务功能,格式设定为无参无返回值的方法,方法体内空实现(非抽象)
2.2.3 @Before
- 名称:@Before
- 类型:注解
- 位置:方法定义上方
- 作用:标注当前方法作为前置通知
- 格式:
@Before(“pt()”)
public void before(){
} - 特殊参数:
- 无
2.2.4 @After
- 名称:@After
- 类型:注解
- 位置:方法定义上方
- 作用:标注当前方法作为后置通知
- 格式:
@After(“pt()”)
public void after(){
} - 特殊参数:
- 无
2.2.5 @AfterReturning
- 名称:@AfterReturning
- 类型:注解
- 位置:方法定义上方
- 作用:标注当前方法作为返回后通知
- 格式:
@AfterReturning(value=“pt()”,returning = “ret”)
public void afterReturning(Object ret) {
} - 特殊参数:
- returning :设定使用通知方法参数接收返回值的变量名
2.2.6 @AfterThrowing
- 名称:@AfterThrowing
- 类型:注解
- 位置:方法定义上方
- 作用:标注当前方法作为异常后通知
- 格式:
@AfterThrowing(value=“pt()”,throwing = “t”)
public void afterThrowing(Throwable t){
} - 特殊参数:
- throwing :设定使用通知方法参数接收原始方法中抛出的异常对象名
2.2.7 @Around
- 名称:@Around
- 类型:注解
- 位置:方法定义上方
- 作用:标注当前方法作为环绕通知
- 格式:
@Around(“pt()”)
public Object around(ProceedingJoinPoint pjp) throws Throwable {
Object ret = pjp.proceed();
return ret;
} - 特殊参数:
- 无
AOP使用XML配置情况下,通知的执行顺序由配置顺序决定,在注解情况下由于不存在配置顺序的概念的概念,参照通知所配置的方法名字符串对应的编码值顺序,可以简单理解为字母排序
- 同一个通知类中,相同通知类型以方法名排序为准
- 不同通知类中,以类名排序为准
- 使用@Order注解通过变更bean的加载顺序改变通知的加载顺序
3 企业开发经验
- 通知方法名由3部分组成,分别是前缀、顺序编码、功能描述
- 前缀为固定字符串,例如baidu、itheima等,无实际意义
- 顺序编码为6位以内的整数,通常3位即可,不足位补0
- 功能描述为该方法对应的实际通知功能,例如exception、strLenCheck
3.1 AOP注解驱动
- 名称:@EnableAspectJAutoProxy
- 类型:注解
- 位置:Spring注解配置类定义上方
- 作用:设置当前类开启AOP注解驱动的支持,加载AOP注解
- 格式:
@Configuration @ComponentScan("com.itheima") @EnableAspectJAutoProxy public class SpringConfig { }
4 综合案例
4.1 案例介绍
对项目进行业务层接口执行监控,测量业务层接口的执行效率
public interface AccountService {
void save(Account account);
void delete(Integer id);
void update(Account account);
List<Account> findAll();
Account findById(Integer id);
}
4.2 案例分析
- 测量接口执行效率:接口方法执行前后获取执行时间,求出执行时长
- System.currentTimeMillis( )
- 对项目进行监控:项目中所有接口方法,AOP思想,执行期动态织入代码
- 环绕通知
- proceed()方法执行前后获取系统时间
4.3 案例制作步骤
- 定义切入点(务必要绑定到接口上,而不是接口实现类上)
- 制作AOP环绕通知,完成测量功能
- 注解配置AOP
- 开启注解驱动支持
4.4 案例制作核心代码
public class RunTimeMonitorAdvice {
//拦截所有的业务层接口中查询操作的执行
@Pointcut("execution(* com.itheima.service.*Service.find*(..))")
public void pt(){}
@Around("pt()")
public Object runtimeMonitor(ProceedingJoinPoint pjp) throws Throwable {
//获取执行签名信息
Signature signature = pjp.getSignature();
//通过签名获取执行类型(接口名)
String targetClass = signature.getDeclaringTypeName();
//通过签名获取执行操作名称(方法名)
String targetMethod = signature.getName();
//获取操作前系统时间beginTime
long beginTime = System.currentTimeMillis();
Object ret = pjp.proceed(pjp.getArgs());
//获取操作后系统时间endTime
long endTime = System.currentTimeMillis();
System.out.println(targetClass+" 中 "+targetMethod+" 运行时长 "+(endTime-beginTime)+"ms");
return ret;
}
}
4.5 案例后续思考与设计
- 测量真实性
- 开发测量是隔离性反复执行某个操作,是理想情况,上线测量差异过大
- 上线测量服务器性能略低于单机开发测量
- 上线测量基于缓存的性能查询要优于数据库查询测量
- 上线测量接口的性能与最终对外提供的服务性能差异过大
- 当外部条件发生变化(硬件),需要进行回归测试,例如数据库迁移
- 测量结果展示
- 测量结果无需每一个都展示,需要设定检测阈值
- 阈值设定要根据业务进行区分,一个复杂的查询与简单的查询差异化很大
- 阈值设定需要做独立的配置文件或通过图形工具配置(工具级别的开发)
- 配合图形界面展示测量结果
5 代理设计模式介绍
5.1 静态代理和装饰者设计模式的区别
5.1.1 相同点
1)都要实现与目标类相同的业务接口
2)在俩个类中都要声明目标对象
3)都可以在不修改目标类的前提下增强目标方法
不同点
1)目的不同,装饰者,简单说,就是为了增强目标对象
静态代理的使用目的是为了保护和隐藏目标对象
2)对于目标对象的获取方式不同
装饰者中目标对象的获取,通过代参构造器传入,静态代理类中,是在无参构造器中直接创建。
静态代理
静态代理的思想:将被代理类作为代理类的成员,通过代理类调用被代理类的函数,并添加新的控制。包装类与被包装类实现同一接口,使得使用时的代码一致。
应用:已经有一个日志记录器LoggerSubject,需要对writeLog()函数的前后进行某些操作(如初始化、异常处理等),使用Proxy类间接调用LoggerSubject.writeLog()实现新控制操作的添加。
interface Logger {
void writeLog();
}
// 被代理类
class LoggerSubject implements Logger{
@Override
public void writeLog(){
System.out.println("writeLog by LoggerSubject");
}
}
// 代理类
class Proxy implements Logger{
Logger logger;
// 与装饰者模式的主要区别位置
// 代理模式一般要求和原来的类行为一致,因此构造函数不传入对象
Proxy(){
this.logger = new LoggerSubject();
}
@Override
public void writeLog(){
System.out.println("logger write before");
logger.writeLog();
System.out.println("logger write after");
}
}
public class StaticProxy {
public static void main(String []argvs){
Logger logger = new Proxy();
logger.writeLog();
}
}
动态代理技术
常用的动态代理技术
JDK 代理 : 基于接口的动态代理技术
cglib 代理:基于父类的动态代理技术
JDK 的动态代理
①目标类接口
public interface TargetInterface {
public void method();
}
②目标类
public class Target implements TargetInterface {
@Override
public void method() {
System.out.println("Target running....");
}
}
③动态代理代码
Target target = new Target(); //创建目标对象
//创建代理对象
TargetInterface proxy = (TargetInterface) 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("前置增强代码...");
Object invoke = method.invoke(target, args);
System.out.println("后置增强代码...");
return invoke;
}
}
);
④ 调用代理对象的方法测试
// 测试,当调用接口的任何方法时,代理对象的代码都无序修改
proxy.method();
cglib 的动态代理
①目标类
public class Target {
public void method() {
System.out.println("Target running....");
}
}
②动态代理代码
Target target = new Target(); //创建目标对象
Enhancer enhancer = new Enhancer(); //创建增强器
enhancer.setSuperclass(Target.class); //设置父类
enhancer.setCallback(new MethodInterceptor() { //设置回调
@Override
public Object intercept(Object o, Method method, Object[] objects, MethodProxy methodProxy) throws Throwable {
System.out.println("前置代码增强....");
Object invoke = method.invoke(target, objects);
System.out.println("后置代码增强....");
return invoke;
}
});
Target proxy = (Target) enhancer.create(); //创建代理对象
③调用代理对象的方法测试
//测试,当调用接口的任何方法时,代理对象的代码都无序修改
proxy.method();
代理模式的选择
Spirng可以通过配置的形式控制使用的代理形式,默认使用jdkproxy,通过配置可以修改为使用cglib
false表示使用默认JDK动态代理,true表示使用cglib动态代理
XML配置
<!--XMP配置AOP-->
<aop:config proxy-target-class="false"></aop:config>
注解驱动
//注解驱动
@EnableAspectJAutoProxy(proxyTargetClass = true)
面试
AOP是什么
面向切面编程,在不修改源代码的情况下对原有功能进行增强,底层是动态代理
动态代理的分类
分为jdk和cglib,jdk动态代理只能增强某个接口的子类对象,cglib接口和类的子类对象都可以增强,spring默认是jdk代理而spring boot是cglib
在开发中经常的通知有哪些
前置通知Before,后置通知After,环绕通知Around 这三个用的比较多
要想对某个方法进行增强有几种方式
1.继承
2.装饰者
3.动态代理