介绍
AOP,面向切面编程,作为面向对象的一种增补,将公共逻辑(事件管理、日志、缓存、权限控制、限流等)封装成切面,跟业务代码进行分离,可以减少系统的重复代码和降低模块之间的耦合度。切面就是那些与业务无关,但全部业务模块都会调用的公共逻辑。
先看一个例子:怎样给如下UserServiceImpl中全部方法添加进入方法的日志,- public class UserServiceImpl implements IUserService {
- @Override
- public List<User> findUserList() {
- System.out.println("execute method: findUserList");
- return Collections.singletonList(new User("seven", 18));
- }
- @Override
- public void addUser() {
- System.out.println("execute method: addUser");
- // do something
- }
- }
复制代码 将记录日志功能解耦为日志切面,它的目标是解耦。进而引出AOP的理念:就是将分散在各个业务逻辑代码中雷同的代码通过横向切割的方式抽取到一个独立的模块中!
OOP面向对象编程,针对业务处置惩罚过程的实体及其属性和行为进行抽象封装,以获得更加清晰高效的逻辑单元划分。
AOP则是针对业务处置惩罚过程中的切面进行提取,它所面对的是处置惩罚过程的某个步调或阶段,以获得逻辑过程的中各部分之间低耦合的隔离效果。这两种设计思想在目标上有着本质的差异。
AOP相干术语
起重要知道,aop不是spring所特有的,同样的,这些术语也不是spring所特有的。是由AOP联盟定义的
- 切面(Aspect):切面是增强和切点的结合,增强和切点共同定义了切面的全部内容。
多个切面之间的实行顺序怎样控制?起重要明确,在“进入”连接点的情况下,最高优先级的增强会先实行;在“退出”连接点的情况下,最高优先级的增强会末了实行。
- 通常使用@Order 注解直接定义切面顺序
- 实现Ordered 接口重写 getOrder 方法。Ordered.getValue()方法返回值(或者注解值)较低的那个有更高的优先级。
- 连接点(Join point):一般指方法,在Spring AOP中,一个连接点总是代表一个方法的实行。连接点是在应用实行过程中可以或许插入切面的一个点。这个点可以是调用方法时、抛出异常时、甚至修改一个字段时。切面代码可以使用这些点插入到应用的正常流程之中,并添加新的行为。固然,连接点也可能是类初始化、方法实行、方法调用、字段调用或处置惩罚异常等
- 增强(或称为关照)(Advice):在AOP术语中,切面的工作被称为增强。知现实上是程序运行时要通过Spring AOP框架来触发的代码段。
- 前置增强(Before):在目标方法被调用之前调用增强功能;
- 后置增强(After):在目标方法完成之后调用增强,此时不会关心方法的输出是什么;
- 返回增强(After-returning ):在目标方法乐成实行之后调用增强;
- 异常增强(After-throwing):在目标方法抛出异常后调用增强;
- 环绕增强(Around):增强包裹了被增强的方法,在被增强的方法调用之前和调用之后实行自定义的逻辑
- 切点(Pointcut):切点的定义会匹配增强所要织入的一个或多个连接点。通常使用明确的类和方法名称,或是利用正则表达式定义所匹配的类和方法名称来指定这些切点。以AspectJ举例,说白了就可以理解为是execution表达式
- 引入(Introduction):引入允许我们向现有类添加新方法或属性。 在AOP中表示为干什么(引入什么);
- 目标对象(Target Object): 被一个或者多个切面(aspect)所增强(advise)的对象。它通常是一个代理对象。
- 织入(Weaving):织入是把切面应用到目标对象并创建新的代理对象的过程。在AOP中表示为怎么实现的;织入分为编译期织入、类加载期织入、运行期织入;SpringAOP是在运行期织入
execution表达式格式:- execution(modifiers-pattern? ret-type-pattern declaring-type-pattern? name-pattern(param-pattern) throws-pattern?)
复制代码
- ret-type-pattern 返回类型模式, name-pattern名字模式和param-pattern参数模式是必选的, 别的部分都是可选的。返回类型模式决定了方法的返回类型必须依次匹配一个连接点。 使用的最频繁的返回类型模式是*,它代表了匹配任意的返回类型。
- declaring-type-pattern, 一个全限定的类型名将只会匹配返回给定类型的方法。
- name-pattern 名字模式匹配的是方法名。 可以使用*通配符作为全部或者部分定名模式。
- param-pattern 参数模式稍微有点复杂:()匹配了一个不担当任何参数的方法, 而(..)匹配了一个担当任意数量参数的方法(零或者更多)。 模式(*)匹配了一个担当一个任何类型的参数的方法。 模式(*,String)匹配了一个担当两个参数的方法,第一个可以是任意类型, 第二个则必须是String类型。
例如:- execution(* com.seven.springframeworkaopannojdk.service.*.*(..))
复制代码 Spring AOP和AspectJ的关系
AspectJ是一个java实现的AOP框架,它可以或许对java代码进行AOP编译(一般在编译期进行),让java代码具有AspectJ的AOP功能(固然必要特殊的编译器)。可以如许说AspectJ是目前实现AOP框架中最成熟,功能最丰富的语言,更幸运的是,AspectJ与java程序完全兼容,险些是无缝关联,因此对于有java编程基础的工程师,上手和使用都非常容易。
- AspectJ是更强的AOP框架,是现实意义的AOP标准;
- Spring为何不写类似AspectJ的框架? Spring AOP使用纯Java实现, 它不必要专门的编译过程, 它一个重要的原则就是无侵入性(non-invasiveness); Spring 小组完全有本领写类似的框架,只是Spring AOP从来没有打算通过提供一种全面的AOP解决方案来与AspectJ竞争。Spring的开辟小组相信无论是基于代理(proxy-based)的框架如Spring AOP或者是成熟的框架如AspectJ都是很有价值的,他们之间应该是互补的而不是竞争的关系。
- Spring小组喜好@AspectJ注解风格更胜于Spring XML设置; 所以在Spring 2.0使用了和AspectJ 5一样的注解,并使用AspectJ来做切入点解析和匹配。但是,AOP在运行时仍旧是纯的Spring AOP,并不依赖于AspectJ的编译器或者织入器(weaver)。
- Spring 2.5对AspectJ的支持:在一些情况下,增加了对AspectJ的装载时编织支持,同时提供了一个新的bean切入点。
下表总结了 Spring AOP 和 AspectJ 之间的关键区别:
Spring AOPAspectJ在纯 Java 中实现使用 Java 编程语言的扩展实现不必要单独的编译过程除非设置 LTW,否则必要 AspectJ 编译器 (ajc)只能使用运行时织入运行时织入不可用。支持编译时、编译后和加载时织入功能不强 - 仅支持方法级编织更强大 - 可以编织字段、方法、构造函数、静态初始值设定项、终极类/方法等......。只能在由 Spring 容器管理的 bean 上实现可以在全部域对象上实现仅支持方法实行切入点支持全部切入点代理是由目标对象创建的, 而且切面应用在这些代理上在实行应用程序之前 (在运行时) 前, 各方面直接在代码中进行织入比 AspectJ 慢多了更好的性能易于学习和应用相对于 Spring AOP 来说更复杂AOP的实现原理
AOP有两种实现方式:静态代理和动态代理。
静态代理
静态代理分为:编译时织入(特殊编译器实现)、类加载时织入(特殊的类加载器实现)。
代理类在编译阶段生成,在编译阶段将增强织入Java字节码中,也称编译时增强。AspectJ使用的是静态代理。
缺点:代理对象必要与目标对象实现一样的接口,而且实现接口的方法,会有冗余代码。同时,一旦接口增加方法,目标对象与代理对象都要维护。
动态代理
动态代理:代理类在程序运行时创建,AOP框架不会去修改字节码,而是在内存中临时生成一个代理对象,在运行期间对业务方法进行增强,不会生成新类
Spring的AOP实现原理
而Spring的AOP的实现就是通过动态代理实现的。
如果为Spring的某个bean设置了切面,那么Spring在创建这个bean的时候,现实上创建的是这个bean的一个代理对象,后续对bean中方法的调用,现实上调用的是代理类重写的代理方法。而Spring的AOP使用了两种动态代理,分别是JDK的动态代理,以及CGLib的动态代理。
- 如果目标类实现了接口,Spring AOP会选择使用JDK动态代理目标类。代理类根据目标类实现的接口动态生成,不必要自己编写,生成的动态代理类和目标类都实现雷同的接口。JDK动态代理的核心是InvocationHandler接口和Proxy类。
- 如果目标类没有实现接口,那么Spring AOP会选择使用CGLIB来动态代理目标类。CGLIB(Code Generation Library)可以在运行时动态生成类的字节码,动态创建目标类的子类对象,在子类对象中增强目标类。CGLIB是通过继续的方式做的动态代理,因此CGLIB存在的束:类是final的,或是方法是final的,或是方法是private,或是静态方法,也就是无法被子类实现的方法都无法使用CGLIB实现代理。
那么什么时候采用哪种动态代理呢?
- 如果目标对象实现了接口,默认情况下会采用JDK的动态代理实现AOP
- 如果目标对象实现了接口,可以强制使用CGLIB实现AOP
- 如果目标对象没有实现了接口,必须采用CGLIB库
AOP的设置方式
基于XML
Spring提供了使用"aop"定名空间来定义一个切面,我们来看个例子
- public class AopDemoServiceImpl {
- public void doMethod1() {
- System.out.println("AopDemoServiceImpl.doMethod1()");
- }
- public String doMethod2() {
- System.out.println("AopDemoServiceImpl.doMethod2()");
- return "hello world";
- }
- public String doMethod3() throws Exception {
- System.out.println("AopDemoServiceImpl.doMethod3()");
- throw new Exception("some exception");
- }
- }
复制代码- public class LogAspect {
- public Object doAround(ProceedingJoinPoint pjp) throws Throwable {
- System.out.println("-----------------------");
- System.out.println("环绕通知: 进入方法");
- Object o = pjp.proceed();
- System.out.println("环绕通知: 退出方法");
- return o;
- }
- public void doBefore() {
- System.out.println("前置通知");
- }
- public void doAfterReturning(String result) {
- System.out.println("后置通知, 返回值: " + result);
- }
- public void doAfterThrowing(Exception e) {
- System.out.println("异常通知, 异常: " + e.getMessage());
- }
- public void doAfter() {
- System.out.println("最终通知");
- }
- }
复制代码- <?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"
- xmlns:context="http://www.springframework.org/schema/context"
- xsi:schemaLocation="http://www.springframework.org/schema/beans
- http://www.springframework.org/schema/beans/spring-beans.xsd
- http://www.springframework.org/schema/aop
- http://www.springframework.org/schema/aop/spring-aop.xsd
- http://www.springframework.org/schema/context
- http://www.springframework.org/schema/context/spring-context.xsd
- ">
- <context:component-scan base-package="com.seven.springframeworkaopxml" />
- <aop:aspectj-autoproxy/>
-
- <bean id="demoService" >
-
- </bean>
-
- <bean id="logAspect" >
-
- </bean>
- <aop:config>
-
- <aop:aspect ref="logAspect">
-
- <aop:pointcut id="pointCutMethod" expression="execution(* com.seven.springframeworkaopxml.service.*.*(..))"/>
-
- <aop:around method="doAround" pointcut-ref="pointCutMethod"/>
-
- <aop:before method="doBefore" pointcut-ref="pointCutMethod"/>
-
- <aop:after-returning method="doAfterReturning" pointcut-ref="pointCutMethod" returning="result"/>
-
- <aop:after-throwing method="doAfterThrowing" pointcut-ref="pointCutMethod" throwing="e"/>
-
- <aop:after method="doAfter" pointcut-ref="pointCutMethod"/>
- </aop:aspect>
- </aop:config>
- </beans>
复制代码- public static void main(String[] args) {
- // create and configure beans
- ApplicationContext context = new ClassPathXmlApplicationContext("aspects.xml");
- // retrieve configured instance
- AopDemoServiceImpl service = context.getBean("demoService", AopDemoServiceImpl.class);
- // use configured instance
- service.doMethod1();
- service.doMethod2();
- try {
- service.doMethod3();
- } catch (Exception e) {
- // e.printStackTrace();
- }
- }
复制代码 基于AspectJ注解(直接写表达式)
基于XML的声明式AspectJ存在一些不足,必要在Spring设置文件设置大量的代码信息,为相识决这个问题,Spring 使用了@AspectJ框架为AOP的实现提供了一套注解。
注解名称解释@Aspect用来定义一个切面。@pointcut用于定义切入点表达式。在使用时还必要定义一个包含名字和任意参数的方法签名来表示切入点名称,这个方法签名就是一个返回值为void,且方法体为空的普通方法。@Before用于定义前置关照,相当于BeforeAdvice。在使用时,通常必要指定一个value属性值,该属性值用于指定一个切入点表达式(可以是已有的切入点,也可以直接定义切入点表达式)。@AfterReturning用于定义后置关照,相当于AfterReturningAdvice。在使用时可以指定pointcut / value和returning属性,此中pointcut / value这两个属性的作用一样,都用于指定切入点表达式。@Around用于定义环绕关照,相当于MethodInterceptor。在使用时必要指定一个value属性,该属性用于指定该关照被植入的切入点。@After-Throwing用于定义异常关照来处置惩罚程序中未处置惩罚的异常,相当于ThrowAdvice。在使用时可指定pointcut / value和throwing属性。此中pointcut/value用于指定切入点表达式,而throwing属性值用于指定-一个形参名来表示Advice方法中可定义与此同名的形参,该形参可用于访问目标方法抛出的异常。@After用于定义终极final 关照,不管是否异常,该关照都会实行。使用时必要指定一个value属性,该属性用于指定该关照被植入的切入点。@DeclareParents用于定义引介关照,相当于IntroductionInterceptor (不要求掌握)。基于JDK动态代理
基于JDK动态代理例子源码点这里
- public interface IJdkProxyService {
- void doMethod1();
- String doMethod2();
- String doMethod3() throws Exception;
- }
复制代码- @Service
- public class JdkProxyDemoServiceImpl implements IJdkProxyService {
- @Override
- public void doMethod1() {
- System.out.println("JdkProxyServiceImpl.doMethod1()");
- }
- @Override
- public String doMethod2() {
- System.out.println("JdkProxyServiceImpl.doMethod2()");
- return "hello world";
- }
- @Override
- public String doMethod3() throws Exception {
- System.out.println("JdkProxyServiceImpl.doMethod3()");
- throw new Exception("some exception");
- }
- }
复制代码- @EnableAspectJAutoProxy
- @Component
- @Aspect
- public class LogAspect {
- /**
- * define point cut.
- */
- @Pointcut("execution(* com.seven.springframeworkaopannojdk.service.*.*(..))")
- private void pointCutMethod() {
- }
- /**
- * 环绕通知.
- *
- * @param pjp pjp
- * @return obj
- * @throws Throwable exception
- */
- @Around("pointCutMethod()")
- public Object doAround(ProceedingJoinPoint pjp) throws Throwable {
- System.out.println("-----------------------");
- System.out.println("环绕通知: 进入方法");
- Object o = pjp.proceed();
- System.out.println("环绕通知: 退出方法");
- return o;
- }
- /**
- * 前置通知.
- */
- @Before("pointCutMethod()")
- public void doBefore() {
- System.out.println("前置通知");
- }
- /**
- * 后置通知.
- *
- * @param result return val
- */
- @AfterReturning(pointcut = "pointCutMethod()", returning = "result")
- public void doAfterReturning(String result) {
- System.out.println("后置通知, 返回值: " + result);
- }
- /**
- * 异常通知.
- *
- * @param e exception
- */
- @AfterThrowing(pointcut = "pointCutMethod()", throwing = "e")
- public void doAfterThrowing(Exception e) {
- System.out.println("异常通知, 异常: " + e.getMessage());
- }
- /**
- * 最终通知.
- */
- @After("pointCutMethod()")
- public void doAfter() {
- System.out.println("最终通知");
- }
- }
复制代码- public class App {
- public static void main(String[] args) {
- // create and configure beans
- ApplicationContext context = new AnnotationConfigApplicationContext("com.seven.springframeworkaopannojdk");
- // retrieve configured instance
- IJdkProxyService service = context.getBean(IJdkProxyService.class);
- // use configured instance
- service.doMethod1();
- service.doMethod2();
- try {
- service.doMethod3();
- } catch (Exception e) {
- // e.printStackTrace();
- }
- }
- }
复制代码 非接口使用Cglib代理
基于Cglib代理例子源码点这里
- @Service
- public class CglibProxyDemoServiceImpl {
- public void doMethod1() {
- System.out.println("CglibProxyDemoServiceImpl.doMethod1()");
- }
- public String doMethod2() {
- System.out.println("CglibProxyDemoServiceImpl.doMethod2()");
- return "hello world";
- }
- public String doMethod3() throws Exception {
- System.out.println("CglibProxyDemoServiceImpl.doMethod3()");
- throw new Exception("some exception");
- }
- }
复制代码 和上面雷同
- public class App {
- public static void main(String[] args) {
- // create and configure beans
- ApplicationContext context = new AnnotationConfigApplicationContext("com.seven.springframeworkaopannocglib");
- // cglib proxy demo
- CglibProxyDemoServiceImpl service = context.getBean(CglibProxyDemoServiceImpl.class);
- service.doMethod1();
- service.doMethod2();
- try {
- service.doMethod3();
- } catch (Exception e) {
- // e.printStackTrace();
- }
- }
- }
复制代码 使用注解装配AOP
上面使用AspectJ的注解,并共同一个复杂的execution(* com.seven.springframeworkaopannojdk.service.*.*(..)) 语法来定义应该怎样装配AOP。另有另一种方式,则是使用注解来装配AOP,这两者一般存在与不同的应用场景中:
- 对于业务开辟来说,一般使用 注解的方式来装配AOP,因为如果要使用AOP进行增强,业务开辟就必要设置注解,业务可以或许很好的感知到这个方法(这个类)进行了增强。如果使用 表达式来装配AOP,当后续新增Bean,如果不清晰现有的AOP装配规则,容易被逼迫装配,而在开辟时未感知到,导致出现线上故障。例如,Spring提供的@Transactional就是一个非常好的例子。如果自己写的Bean渴望在一个数据库事件中被调用,就标注上@Transactional。
- 对于基础架构开辟来说,无需业务感知到增强了什么方法,则可以使用表达式的方式来装配AOP。必要记录全部接口的耗时时长,直接写表达式,对业务无侵入
- 定义注解
- @Target(ElementType.METHOD)
- @Retention(RetentionPolicy.RUNTIME)
- public @interface LogAspectAnno {
- }
复制代码- @EnableAspectJAutoProxy
- @Component
- @Aspect
- public class LogAspect {
- @Around("@annotation(logaspectanno)") //注意,括号里为logaspectanno,而不是LogAspectAnno
- public Object doAround(ProceedingJoinPoint pjp, LogAspectAnno logaspectanno) throws Throwable {
- System.out.println("-----------------------");
- System.out.println("环绕通知: 进入方法");
- Object o = pjp.proceed();
- System.out.println("环绕通知: 退出方法");
- return o;
- }
-
- }
复制代码
- 修改实现类,这里只对 doMethod1 方法装配AOP
- @Service
- public class CglibProxyDemoServiceImpl {
- @LogAspectAnno()
- public void doMethod1() {
- System.out.println("CglibProxyDemoServiceImpl.doMethod1()");
- }
- public String doMethod2() {
- System.out.println("CglibProxyDemoServiceImpl.doMethod2()");
- return "hello world";
- }
- }
- @Service
- public class JdkProxyDemoServiceImpl implements IJdkProxyService {
- @LogAspectAnno
- @Override
- public void doMethod1() {
- System.out.println("JdkProxyServiceImpl.doMethod1()");
- }
- @Override
- public String doMethod2() {
- System.out.println("JdkProxyServiceImpl.doMethod2()");
- return "hello world";
- }
- }
复制代码- // create and configure beans
- ApplicationContext context = new AnnotationConfigApplicationContext("com.seven.springframeworkaopannotation");
- // cglib proxy demo
- CglibProxyDemoServiceImpl service1 = context.getBean(CglibProxyDemoServiceImpl.class);
- service1.doMethod1();
- service1.doMethod2();
- IJdkProxyService service2 = context.getBean(IJdkProxyService.class);
- service2.doMethod1();
- service2.doMethod2();
复制代码- -----------------------
- 环绕通知: 进入方法
- CglibProxyDemoServiceImpl.doMethod1()
- 环绕通知: 退出方法
- CglibProxyDemoServiceImpl.doMethod2()
- -----------------------
- 环绕通知: 进入方法
- JdkProxyServiceImpl.doMethod1()
- 环绕通知: 退出方法
- JdkProxyServiceImpl.doMethod2()
复制代码 可以看到,只有doMethod1方法被增强了,doMethod2没有被增强,就是因为@LogAspectAnno 只注解了 doMethod1() 方法,从而实现更精细化的控制,是业务感知到这个方法是被增强了。
应用场景
我们知道AO可以或许将那些与业务无关,却为业务模块所共同调用的逻辑或责任(例如事件处置惩罚、日志管理、权限控制等)封装起来,便于减少系统的重复代码,降低模块间的耦合度,提高系统可拓展性和可维护性。
- 基于 AOP 实现同一的日志管理。
- 基于 Redisson + AOP 实现了接口防刷,一个注解即可限制接口指定时间内单个用户可以请求的次数。
- 基于 Spring Security 提供的 @PreAuthorize 实现权限控制,其底层也是基于 AOP。
日志记录
使用 AOP 方式记录日志,只必要在 Controller 的方法上使用自定义 @Log 日志注解,就可以将用户操纵记录到数据库。- @Log(description = "新增用户")
- @PostMapping(value = "/users")
- public ResponseEntity create(@Validated @RequestBody User resources){
- checkLevel(resources);
- return new ResponseEntity(userService.create(resources),HttpStatus.CREATED);
- }
复制代码 AOP 切面类 LogAspect用来拦截带有 @Log 注解的方法并处置惩罚:- @Aspect
- @Component
- public class LogAspect {
- private static final Logger logger = LoggerFactory.getLogger(LogAspect.class);
- // 定义切点,拦截带有 @Log 注解的方法
- @Pointcut("@annotation(com.example.annotation.Log)") // 这里需要根据你的实际包名修改
- public void logPointcut() {
- }
- // 环绕通知,用于记录日志
- @Around("logPointcut()")
- public Object around(ProceedingJoinPoint joinPoint) throws Throwable {
- //...
- }
- }
复制代码 限流
使用 AOP 方式对接口进行限流,只必要在 Controller 的方法上使用自定义的 @RateLimit 限流注解即可。- /**
- * 该接口 60 秒内最多只能访问 10 次,保存到 redis 的键名为 limit_test,
- */
- @RateLimit(key = "test", period = 60, count = 10, name = "testLimit", prefix = "limit")
- public int test() {
- return ATOMIC_INTEGER.incrementAndGet();
- }
复制代码 AOP 切面类 RateLimitAspect用来拦截带有 @RateLimit 注解的方法并处置惩罚:- @Slf4j
- @Aspect
- public class RateLimitAspect {
- // 拦截所有带有 @RateLimit 注解的方法
- @Around("@annotation(rateLimit)")
- public Object around(ProceedingJoinPoint joinPoint, RateLimit rateLimit) throws Throwable {
- //...
- }
- }
复制代码 关于限流实现这里多说一句,这里并没有自己写 Redis Lua 限流脚本,而是使用 Redisson 中的 RRateLimiter 来实现分布式限流,其底层实现就是基于 Lua 代码+令牌桶算法。
权限控制
Spring Security 使用 AOP 进行方法拦截。在现实调用 update 方法之前,Spring 会检查当前用户的权限,只有用户权限满意对应的条件才华实行。- @Log(description = "修改菜单")
- @PutMapping(value = "/menus")
- // 用户拥有 `admin`、`menu:edit` 权限中的任意一个就能能访问`update`方法
- @PreAuthorize("hasAnyRole('admin','menu:edit')")
- public ResponseEntity update(@Validated @RequestBody Menu resources){
- //...
- }
复制代码 面试题专栏
Java面试题专栏已上线,欢迎访问。
- 如果你不知道简历怎么写,简历项目不知道怎么包装;
- 如果简历中有些内容你不知道该不应写上去;
- 如果有些综合性问题你不知道怎么答;
那么可以私信我,我会尽我所能资助你。
免责声明:如果侵犯了您的权益,请联系站长,我们会及时删除侵权内容,谢谢合作!更多信息从访问主页:qidao123.com:ToB企服之家,中国第一个企服评测及商务社交产业平台。 |