AOP
- 定义
面向切面编程,通过预编译方式和运行期动态代理实现程序功能的统一维护的一种技术.
- AOP是OOP的延续是软件开发中的一个热点,也是Spring框架中的一个重要内容,是函数式编程的一种衍生范型.
- 利用AOP可以对业务逻辑的各个部分进行隔离,从而使得业务逻辑各个部分之间的耦合度降低,提高程序的可重用性,同时提高了开发的效率
- 具体内容
AOP:面向切面编程
将业务逻辑中的横切关注点
通过切面的方式分离出来,在指定位置执行的过程
- 横切关注是什么?
通用功能实在你的应用程序中许多地方都需要的
实例
日志与跟踪
事务管理
安全
缓存
错误处理
性能监测
自定义服务规则
核心关注点:与核心业务息息相关的,缺少的话无法完成核心功能的逻辑
横切关注点:与核心业务无关,在多处业务中通用,缺少的话仍保证核心功能正常使用的逻辑
- 面试 - 什么是AOP?
面向切面编程
为什么有这个呢? 这个是什么思想呢?
传统的方式需要把一些通用逻辑在源代码里写, 很麻烦
有了OOP, 我们可以封装一个方法显式的调用, 但是调用代码的侵入性(耦合性)太强, 所以我们通过第三方框架Spring来注入实现
我们只需要写上一些配置, 配置它在哪执行就可以了, 剩下的由Spring来帮我们注入
- 应用
@Aspect //标明为切面类
@Component //表明为可以扫描到的
public class SysLogAspect {
/**
* 'execution()'为表达式的主体
* 第一个"*"号表示返回指的类型任意
* 'cn.tedu.db.sys.service.impl'表示AOP所切的服务的包名
* 第二个"*"号表示类名, *即所有类
* '.*(..)'表示任何方法名, 括号表示参数, 两个点表示任何参数类型
*/
@Around("execution(* cn.tedu.db.sys.service.impl.*.*(..))")
public Object advice(ProceedingJoinPoint pjp) throws Throwable {
//执行目标方法, 获得result
Object result = pjp.proceed();
//记录日志信息的具体逻辑
......
//返回结果
return result;
}
}
领先的AOP技术
- AspectJ
最早的AOP技术(1995年推出第1个版本)
一个完整的面向切面编程语言
- 使用字节码编程进行切面编织
- Spring AOP
基于Java的AOP框架, 集成AspectJ
- 使用动态代理进行切面编织
专注于使用AOP解决企业问题
核心AOP概念
- 连接点
程序执行过程中的一个点, 例如方法的调用, 或抛出异常
- 切入点
选择一个或多个连接点的表达式
- Advice(通知)
在选择的每个连接点执行的代码
- 切面
一个囊括了切入点和Advice的模块
- 编织
将切面与主要代码进行结合的技术
定义切入点
- 前置Advice
- 使用@Before注解
@Aspect
@Component
public class ProportyChangeTracker{
private Logger logger = Logger.getLogger(getClass());
@Before("execution(void set*(*))")
public void trackChange(){
logger.info("属性值即将改变...");
}
}
注意:如果Advice抛出异常,则目标不会被调用-这是前置Advice的有效用法
- 返回之后Advice
- 带有returning属性的@AfterReturning注解
@AfterReturning(value="execution(* service..*.*(..))",
returning="reward")
public void audit(JoinPoint point, Reward reward){
auditService.logEvent(point.getSignature()+
"返回这个Reward对象:"+reward.toString());
}
- 抛出异常之后Advice
- 使用带有throwing属性的@AfterThrowing注解
- 仅当抛出正确类型的异常时才会调用Advice
@AfterThrowing(value="execution(* *..Repository.*(..))", throwing="e")
public void report(JoinPoint point, DataAccessException e){
mailService.emailFailure("Repository出现异常",point,e);
}
- 抛出异常之后Advice-传播
- @AfterThrowing的Advice不会停止异常的传播
- 但是他可以抛出一个类型不同的异常
@AfterThrowing(value="execution(* *..Repository.*(..))", throwing="e")
public void report(JoinPoint point, DataAccessException e){
mailService.emailFailure("Repository出现异常",point,e);
throw newRewardsException(e);
}
如果你想停止异常的进一步传播,你可以使用@Around的Advice
- 后置Advice
- 使用@After注解
- 无论目标是否抛出异常,都会被调用
@Aspect
@Component
public class ProportyChangeTracker{
private Logger logger = Logger.getLogger(getClass());
@After("execution(void update*(*))")
public void trackChange(){
logger.info("已尝试进行更新");
}
}
- 环绕Advice
- 使用@Around注解和一个ProceedingJoinPoint
- 继承JionPoint并增加了proceed()方法
@Around(value="execution(@example.Cacheable * rewards.service..*.*(..))")
public void cache(ProcceedingJoinPoint point) throws Throwable{
Object value = cacheStore.get(CacheUtils.toKey(Point));
if(value!=null) return value; //有没有值?
value = point.procceed();
cacheStore.put(CacheUtils.toKey(point).value); //仅当尚未缓存时经行处理
return value;
}
标识符,表达式
- 常见的插入点标识符
execution(<方法模式>)
该方法必须匹配模式
可以串联起来创建复合切入点
- &&(与), ||(或), !(非)
- execution(<方法模式1>) || execution(<方法模式2>)
方法模式
- [修饰符] 返回类型 [类的类型]方法名称(参数列表) [throws异常类型]
注:带中括号的可以省略
- 示例表达式
execution(* reward.restaurant.*Service.find*(..))
execution( * reward.restaurant. *Service. find*( ..))
标识符 返回值类型 包 类 方法 参数
通配符:
- 匹配1次(返回值类型, 包, 类, 方法名, 参数)
- 匹配0次活多次(参数或包)
Execution表达式示例
万能表达式 - 所有的方法都能选中
- 任意类或包
execution(void send*(rewards.Dining))
- 任何以send开头的方法,仅接受1个Dining类型的参数,且返回值类型声明为void
- 注意使用类的全名
execution(* send(*))
- 任何名为send的方法,接受1个参数
execution(* send(int, ..))
- 任何名为send的方法,且第1个参数是int类型的
(".."代表接下来的参数可以使任意数量)
- 实现类VS接口
按类限制
execution(void example.MessageServiceImpl.*(..))
- 在MessageServiceImpl类中的任何void方法
- 包含任何子类
- 但如果使用了不同的实现,将会被忽略
按接口限制
execution(void example.MessageService.send(*))
- 在实现MessageService的对象中,任何接收1个参数的、返回值类型为void且名为send的方法
- 更加灵活的选择-当实现发生变化时,依然是可用的
- 使用
execution(@javax.annotation.security.RolesAllowed void send*(..))
任何名称使用send 开头的void方法,且添加了@RolesAllowed注解
public interface Mailer{
@RolesAllowed("USER")
public void sendMessage(String text);
}
对自己的类使用再记得注解的理想技术
- 如果存在注解进行匹配
- 面试 - AOP
AOP它是面向切面编程,意义在于将业务当中的横切关注(所谓横切关注就是通用的一些逻辑,缺少了核心功能一样能完成的逻辑)我们把它分离出来,通过Spring注入的方式,在指定位置执行的这么一个过程
AOP一般会出现的场景
在我们编程的时候,比如说做一个日志功能,向记录用户的操作信息,那么我就需要在登录功能里面去记录日志,需要早我的删除订单里面记录日志,需要在地址管理记录日志.这样式非常麻烦的,一个代码要写很多遍,改也不好改,那么我们可以引用面向对象的编程思想.
面向对象的编程思想它可以进行封装,我们可以将业务逻辑(就是我们所谓的这些代码记录日志功能)封装起来进行调用.但是调用的这个操作也很麻烦(比如我想增加一个还要去调用一遍,我想改一个名字都很麻烦),所以才要使用面向切面编程
它的目标就是,我在原业务逻辑不再添加新的代码,减少代码的侵入性(耦合),通过切入点来直接标明在哪个位置执行的这么一个过程
那么我们管整个写的这一个类加切入点我们叫做切面,包含了Advice通知.有五种Advice方式,分别是Before我们叫做前置通知,我们还有After后置通知,我们还有所谓的异常通知和返回通知:
Before的特点就是目标方法执行之前会执行一次,如果它停止了、出异常了那么目标方法就不执行了
AfterThrowing是出现了指定异常,它才会执行到这个位置,此处如果出现了异常传播它不会终止异常传播,因为没有终止异常传播的功能,调用者依旧会出现异常
AfterReturning它是在目标方法获取返回值的时候,正常执行完了我在去执行我这段业务逻辑
最后After不管你目标方法是出现错误了还是出现异常了还是正常执行了,我都执行
不过这四种我们用的比较少,我们常用的叫做@Around.@Around它是环绕,它可以通过一个ProceedingJoinPoint,然后调用proceed()方法获取目标方法,有了目标方法,我就可以在目标方法之前,目标方法之后,在出现异常和返回值都可以的时候,以及我可以终止异常传播,所以我们常用的就是这个Around
对于整个的一个AOP来说,这就是关于一个切入点位置的问题,那么业务逻辑有了那么我们需要在类上标记@Aspect、@Component,选择五种通知的一个,然后通过切入点表达式(也就是execution表达式),这个表达式有被称作万能表达式,我们可以在这里面用逻辑运算符,然后我们进行连接,也可以通过匹配符(包括 * .. 在内的)匹配一个或多个
*我们一般用在类上、返回值上、参数上、方法上、包上
..我们可以用在包上和参数上,来标明可以匹配0个或多个
另外我们还可以匹配某个注解(比如说我们常用的@Autowired标注的方法),那我需要在整个切入点表达式最前面写上该注解的全称,那么这就是一个完整的切面
然后再通过Spring注入的方式,在指定的位置执行.这样的话我就在不影响原代码逻辑的前提下,通过面向切面的方式解决了耦合度过高以及侵入性太强的问题
这就是面向切面编程
命名切入点注解
- 示例
@Aspect
@Component
public class PropertyChangeTracker{
private Logger logger = lLogger.getLogger(getClass());
@Before("serviceMethod() || repositoryMethod()")
public void monitior(){
logger.info("已访问某个业务逻辑方法");
}
@Pointcut("execution(* rewards.service..*Service.*(..))")
public void serviceMethod(){}
@Pointcut("execution(* rewards.repository..*Repository.*(..))")
public void repositoryMethod(){}
}
表达式可以外部化
@Aspect
@Component
public class ServiceMethodInvocationMonitor{
private Logger logger=Logger.getLogger(getClass());
@Before("com.acme.Pointcuts..serviceMethods()") -----+
public void monitor(){ |
logger.info("已访问某个业务方法"); |
} |
} |
↓
public class Pointcuts{
@Pointcut("execution(* rewards.service..*Service.*(..))")
public void serviceMethod(){}
}
- 小结
- 可以将一个复杂的表达式分解为多个子表达式
- 允许切点表达式的复用性
- 最佳实践:考虑将表达式外部化为一个专门的类
- 当使用多个切入点时
- 当编写复杂的表达式时