Spring 实践 -AOP

简介: Spring 实践标签: Java与设计模式AOP引介AOP(Aspect Oriented Programing)面向切面编程采用横向抽取机制,以取代传统的纵向继承体系的重复性代码(如性能监控/事务管理/安全检查/缓存实现等).

Spring 实践

标签: Java与设计模式


AOP引介

AOP(Aspect Oriented Programing)面向切面编程采用横向抽取机制,以取代传统的纵向继承体系的重复性代码(如性能监控/事务管理/安全检查/缓存实现等).

横向抽取代码复用: 基于代理技术,在不修改原来代码的前提下,对原有方法进行增强.


Spring AOP 历史

  • 1.2开始, Spring开始支持AOP技术(Spring AOP)
    Spring AOP使用纯Java实现,不需要专门的编译过程和类加载器,在运行期通过代理方式向目标类织入增强代码.
  • 2.0之后, 为了简化AOP开发, Spring开始支持AspectJ(一个基于Java的AOP框架)框架.

AOP相关术语

术语 中文 描述
Joinpoint 连接点 指那些被拦截到的点.在Spring中,这些点指方法(因为Spring只支持方法类型的连接点).
Pointcut 切入点 指需要(配置)被增强的Joinpoint.
Advice 通知/增强 指拦截到Joinpoint后要做的操作.通知分为前置通知/后置通知/异常通知/最终通知/环绕通知等.
Aspect 切面 切入点和通知的结合.
Target 目标对象 需要被代理(增强)的对象.
Proxy 代理对象 目标对象被AOP 织入 增强/通知后,产生的对象.
Weaving 织入 指把增强/通知应用到目标对象来创建代理对象过程(Spring采用动态代理织入,AspectJ采用编译期织入和类装载期织入).
Introduction 引介 一种特殊通知,在不修改类代码的前提下,可以在运行期为类动态地添加一些Method/Field(不常用).

其他关于AOP理论知识可参考AOP技术研究.


AOP实现

Spring AOP代理实现有两种:JDK动态代理Cglib框架动态代理, JDK动态代理可以参考博客代理模式的动态代理部分, 在这里仅介绍CGLib框架实现.

cglib 动态代理

cglib(Code Generation Library)是一个开源/高性能/高质量的Code生成类库,可以在运行期动态扩展Java类与实现Java接口.
cglib比java.lang.reflect.Proxy更强的在于它不仅可以接管接口类的方法,还可以接管普通类的方法(cglib项目).从3.2开始, spring-core包中内置cglib类,因此可以不用添加额外依赖.

  • UserDAO(并没有实现接口)
/**
 * @author jifang
 * @since 16/3/3 上午11:16.
 */
public class UserDAO {

    public void add(Object o) {
        System.out.println("UserDAO -> Add: " + o.toString());
    }

    public void get(Object o) {
        System.out.println("UserDAO -> Get: " + o.toString());
    }
}
  • CGLibProxyFactory
public class CGLibProxyFactory {

    private Object target;

    public CGLibProxyFactory(Object target) {
        this.target = target;
    }


    private Callback callback = new MethodInterceptor() {

        /**
         *
         * @param obj   代理对象
         * @param method    当期调用方法
         * @param args  方法参数
         * @param proxy 被调用方法的代理对象(用于执行父类的方法)
         * @return
         * @throws Throwable
         */
        @Override
        public Object intercept(Object obj, Method method, Object[] args, MethodProxy proxy) throws Throwable {

            // 前置增强
            System.out.println("+ Before Advice ...");

            // 执行目标方法
            //Object result = method.invoke(target, args);
            Object result = proxy.invoke(target, args);

            // 后置增强
            System.out.println("+ After Advice ...");

            return result;
        }
    };

    public Object createProxy() {

        // 1. 创建Enhancer对象
        Enhancer enhancer = new Enhancer();

        // 2. cglib创建代理, 对目标对象创建子对象
        enhancer.setSuperclass(target.getClass());

        // 3. 传入回调接口, 对目标增强
        enhancer.setCallback(callback);

        return enhancer.create();
    }

    public static void main(String[] args) {
        UserDAO proxy = (UserDAO) new CGLibProxyFactory(new UserDAO()).createProxy();
        proxy.get("hello");
        proxy.add("world");
    }
}

AOP小结

  • Spring AOP的底层通过JDK/cglib动态代理为目标对象进行横向织入:
    1) 若目标对象实现了接口,则Spring使用JDK的java.lang.reflect.Proxy代理.
    2) 若目标对象没有实现接口,则Spring使用cglib库生成目标对象的子类.
  • Spring只支持方法连接点,不提供属性连接.
  • 标记为final的方法不能被代理,因为无法进行覆盖.
  • 程序应优先对针对接口代理,这样便于程序解耦/维护.

Spring AOP

AOP联盟为通知Advice定义了org.aopalliance.aop.Advice接口, Spring在Advice的基础上,根据通知在目标方法的连接点位置,扩充为以下五类:

通知 接口 描述
前置通知 MethodBeforeAdvice 在目标方法执行实施增强
后置通知 AfterReturningAdvice …执行实施增强
环绕通知 MethodInterceptor ..执行前后实施增强
异常抛出通知 ThrowsAdvice …抛出异常后实施增强
引介通知 IntroductionInterceptor 在目标类中添加新的方法和属性(少用)
  • 添加Spring的AOP依赖
    使用Spring的AOP和AspectJ需要在pom.xml中添加如下依赖:
<dependency>
    <groupId>org.springframework</groupId>
    <artifactId>spring-aop</artifactId>
    <version>${spring.version}</version>
</dependency>
<dependency>
    <groupId>org.springframework</groupId>
    <artifactId>spring-aspects</artifactId>
    <version>${spring.version}</version>
</dependency>
  • 定义Target
/**
 * @author jifang
 * @since 16/3/3 下午2:50.
 */
public interface OrderService {

    void save();

    Integer delete(Integer param);
}
public class OrderServiceImpl implements OrderService {

    @Override
    public void save() {
        System.out.println("添加...");
    }

    @Override
    public Integer delete(Integer param) {
        System.out.println("删除...");
        return param;
    }
}
  • 定义Advice
/**
 * 实现MethodInterceptor接口定义环绕通知
 *
 * @author jifang
 * @since 16/3/6 下午2:54.
 */
public class ConcreteInterceptor implements MethodInterceptor {

    @Override
    public Object invoke(MethodInvocation invocation) throws Throwable {
        System.out.println("前置通知 -> ");

        Object result = invocation.proceed();

        System.out.println("<- 后置通知");

        return result;
    }
}

Spring手动代理

  • 配置代理
    Spring最原始的AOP支持, 手动指定目标对象与通知(没有使用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"
       xsi:schemaLocation="http://www.springframework.org/schema/beans
       http://www.springframework.org/schema/beans/spring-beans.xsd">

    <!-- target -->
    <bean id="service" class="com.fq.service.impl.OrderServiceImpl"/>
    <!-- advice -->
    <bean id="advice" class="com.fq.advice.ConcreteInterceptor"/>

    <bean id="serviceProxy" class="org.springframework.aop.framework.ProxyFactoryBean">
        <property name="target" ref="service"/>
        <property name="interceptorNames" value="advice"/>
        <property name="proxyTargetClass" value="false"/>
    </bean>
</beans>
  • Client
@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(locations = "classpath:spring/applicationContext.xml")
public class AOPClient {

    @Autowired
    // 必须指定使用代理对象名称, 否则不予代理
    @Qualifier("serviceProxy")
    private OrderService service;

    @Test
    public void client() {
        service.save();
        service.delete(88);
    }
}

这种方式的缺陷在于每个Target都必须手动指定ProxyFactoryBean对其代理(不能批量指定),而且这种方式会在Spring容器中存在两份Target对象(代理前/代理后),浪费资源,且容易出错(比如没有指定@Qualifier).


Spring自动代理 - 引入AspectJ

通过AspectJ引入Pointcut切点定义

  • Target/Advice同前
  • 定义切面表达式
    通过execution函数定义切点表达式(定义切点的方法切入)
    execution(<访问修饰符> <返回类型><方法名>(<参数>)<异常>)
    如:
    1) execution(public * *(..)) # 匹配所有 public方法.
    2) execution(* com.fq.dao.*(..)) # 匹配指定包下所有类方法(不包含子包)
    3) execution(* com.fq.dao..*(..)) # 匹配指定包下所有类方法(包含子包)
    4) execution(* com.fq.service.impl.OrderServiceImple.*(..)) # 匹配指定类所有方法
    5) execution(* com.fq.service.OrderService+.*(..)) # 匹配实现特定接口所有类方法
    6) execution(* save*(..)) # 匹配所有save开头的方法
<?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.xsd
       http://www.springframework.org/schema/aop
       http://www.springframework.org/schema/aop/spring-aop.xsd">

    <!-- target -->
    <bean id="service" class="com.fq.service.impl.OrderServiceImpl"/>
    <!-- advice -->
    <bean id="advice" class="com.fq.advice.ConcreteInterceptor"/>

    <!-- 配置切面 : proxy-target-class确定是否使用CGLIB -->
    <aop:config proxy-target-class="true">
        <!--
            aop:pointcut : 切点定义
            aop:advisor: 定义Spring传统AOP的切面,只支持一个pointcut/一个advice
            aop:aspect : 定义AspectJ切面的,可以包含多个pointcut/多个advice
        -->
        <aop:pointcut id="pointcut" expression="execution(* com.fq.service.impl.OrderServiceImpl.*(..))"/>
        <aop:advisor advice-ref="advice" pointcut-ref="pointcut"/>
    </aop:config>

</beans>
  • Client同前

AspectJ AOP

AspectJ是一个基于Java的AOP框架,提供了强大的AOP功能,其他很多AOP框架都借鉴或采纳了AspectJ的一些思想,Spring2.0以后增加了对AspectJ切点表达式支持(如上),并在Spring3.0之后与AspectJ进行了很好的集成.
在Java领域,AspectJ中的很多语法结构基本上已成为AOP领域的标准, 他定义了如下几类通知类型:

通知 接口 描述
前置通知 @Before 相当于BeforeAdvice
后置通知 @AfterReturning 相当于AfterReturningAdvice
环绕通知 @Around 相当于MethodInterceptor
抛出通知 @AfterThrowing 相当于ThrowAdvice
引介通知 @DeclareParents 相当于IntroductionInterceptor
最终final通知 @After 不管是否异常,该通知都会执行

新版本Spring,建议使用AspectJ方式开发以简化AOP配置.


AspectJ-XML-AOP

使用AspectJ编写Advice无需实现任何接口,而且可以将多个通知写入一个切面类.

前置通知

  • 定义通知
/**
 * @author jifang
 * @since 16/3/3 下午5:38.
 */
public class Aspect {

    /**
     * 无返回值
     */
    public void before1() {
        System.out.println("前置增强before1");
    }

    /**
     * 还可以传入连接点参数 JoinPoint
     *
     * @param point
     */
    public void before2(JoinPoint point) {
        System.out.printf("前置增强before2 %s%n", point.getKind());
    }
}
  • 装配
<?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.fq.service"/>

    <!-- 配置切面通知 -->
    <bean id="advice" class="com.fq.advice.Aspect"/>

    <!-- AOP切面配置 -->
    <aop:config>
        <aop:aspect ref="advice">
            <aop:pointcut id="pointcut" expression="execution(* com.fq.service.impl.OrderServiceImpl.*(..))"/>
            <aop:before method="before1" pointcut-ref="pointcut"/>
            <aop:before method="before2" pointcut-ref="pointcut"/>
        </aop:aspect>
    </aop:config>

</beans>
  • 前置通知小结
    • 前置通知会保证在目标方法执行前执行;
    • 前置通知默认不能阻止目标方法执行(但如果通知抛出异常,则目标方法无法执行);
    • 可以通过JoinPoint参数获得当前拦截对象和方法等信息.

后置通知

  • 定义通知
public void afterReturning(JoinPoint point, Object result) {
    System.out.printf("后置增强, 结果为 %s%n", result);
}
  • 装配
<aop:after-returning method="afterReturning" returning="result" pointcut-ref="pointcut"/>

后置通知可以获得方法返回值,但在配置文件定义返回值参数名必须与后置通知方法参数名一致(如result).


环绕通知

  • 定义通知
public Object around(ProceedingJoinPoint point) throws Throwable {
    System.out.printf("环绕前置增强 method: %s, args: %s%n", point.toShortString(), Arrays.toString(point.getArgs()));

    Object result = point.proceed(point.getArgs());

    System.out.printf("环绕后置增强 result: %s%n", result);

    return result;
}
  • 装配
<aop:around method="around" arg-names="point" pointcut-ref="pointcut"/>

环绕通知可以实现任何通知的效果, 甚至可以阻止目标方法的执行.


抛出通知

  • 定义通知
private static final Logger LOGGER = LoggerFactory.getLogger(Aspect.class);

public void afterThrowing(JoinPoint point, Throwable ex) {
    String message = new StringBuilder("method ").append(point.getSignature().getName()).append(" error").toString();
    System.out.println(message);

    LOGGER.error("{},", message, ex);
}
  • 装配
<aop:after-throwing method="afterThrowing" throwing="ex" pointcut-ref="pointcut"/>

throwing属性指定异常对象名, 该名称应和方法定义参数名一致.


最终通知

  • 定义通知
public void after(JoinPoint point) {
    System.out.println("最终通知, 释放资源");
}
  • 装配
<aop:after method="after" pointcut-ref="pointcut"/>

无论目标方法是否出现异常,该通知都会执行(类似finally代码块, 应用场景为释放资源).


AspectJ-Annotation-AOP

@AspectJ是AspectJ 1.5新增功能,可以通过JDK注解技术,直接在Bean类中定义切面.
AspectJ预定义的注解有:@Before/@AfterReturning/@Around/@AfterThrowing/@DeclareParents/@After.描述同前.
使用AspectJ注解AOP需要在applicationContext.xml文件中开启注解自动代理功能:

<?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">

    <!-- 批量扫描@Component -->
    <context:component-scan base-package="com.fq"/>
    <!-- 启用注解自动代理@Aspect-->
    <aop:aspectj-autoproxy/>
</beans>
  • OrderService/Client同前

@Before

  • Aspect
/**
 * @Aspect: 指定是一个切面
 * @Component: 指定可以被Spring容器扫描到
 */
@Aspect
@Component
public class CustomAspect {

    @Before("execution(* com.fq.service.impl.OrderServiceImpl.*(..))")
    public void before(JoinPoint point) {
        System.out.printf("前置增强before2 %s%n", point.getKind());
    }
}

@AfterReturning

@AfterReturning(value = "execution(* com.fq.service.impl.OrderServiceImpl.d*(..))", returning = "result")
public void afterReturning(JoinPoint point, Object result) {
    System.out.printf("后置增强, 结果为 %s%n", result);
}

@Around

@Around("execution(* com.fq.service.impl.OrderServiceImpl.*(..))")
public Object around(ProceedingJoinPoint point) throws Throwable {
    long start = System.currentTimeMillis();
    Object result = point.proceed(point.getArgs());
    long time = System.currentTimeMillis() - start;

    System.out.printf("method %s invoke consuming %d ms%n", point.toLongString(), time);

    return result;
}

如果不调用ProceedingJoinPointproceed方法,那么目标方法就不执行了.


@AfterThrowing

@AfterThrowing(value = "execution(* com.fq.service.impl.OrderServiceImpl.*(..))", throwing = "ex")
public void afterThrowing(JoinPoint point, Throwable ex) {
    String message = new StringBuilder("method ").append(point.getSignature().getName()).append(" error").toString();
    System.out.println(message);

    LOGGER.error("{},", message, ex);
}

@After

@After("execution(* com.fq.service.impl.OrderServiceImpl.*(..))")
public void after(JoinPoint point) {
    System.out.println("最终通知, 释放资源");
}

@Pointcut定义切点

对于重复的切点,可以使用@Pointcut进行定义, 然后在通知注解内引用.

  • 定义切点方法
    无参/无返回值/方法名为切点名:
/**
 * @author jifang
 * @since 16/3/4 上午11:47.
 */
public class OrderServicePointcut {

    @Pointcut("execution(* com.fq.service.impl.OrderServiceImpl.*(..))")
    public void pointcut() {
    }
}
  • 引用切点
    在Advice上像调用方法一样引用切点:
@After("OrderServicePointcut.pointcut()")
public void after(JoinPoint point) {
    System.out.println("最终通知, 释放资源");
}

1) 如果切点与切面在同一个类内, 可省去类名前缀;
2) 当需要通知多个切点时,可以使用||/&&进行连接.


小结

通知 描述
前置通知 权限控制(少用)
后置通知 少用
环绕通知 权限控制/性能监控/缓存实现/事务管理
异常通知 发生异常后,记录错误日志
最终通知 释放资源

目录
相关文章
|
4月前
|
监控 安全 Java
Spring AOP实现原理
本内容主要介绍了Spring AOP的核心概念、实现机制及代理生成流程。涵盖切面(Aspect)、连接点(Join Point)、通知(Advice)、切点(Pointcut)等关键概念,解析了JDK动态代理与CGLIB代理的原理及对比,并深入探讨了通知执行链路和责任链模式的应用。同时,详细分析了AspectJ注解驱动的AOP解析过程,包括切面识别、切点表达式匹配及通知适配为Advice的机制,帮助理解Spring AOP的工作原理与实现细节。
|
17天前
|
人工智能 监控 安全
如何快速上手【Spring AOP】?核心应用实战(上篇)
哈喽大家好吖~欢迎来到Spring AOP系列教程的上篇 - 应用篇。在本篇,我们将专注于Spring AOP的实际应用,通过具体的代码示例和场景分析,帮助大家掌握AOP的使用方法和技巧。而在后续的下篇中,我们将深入探讨Spring AOP的实现原理和底层机制。 AOP(Aspect-Oriented Programming,面向切面编程)是Spring框架中的核心特性之一,它能够帮助我们解决横切关注点(如日志记录、性能统计、安全控制、事务管理等)的问题,提高代码的模块化程度和复用性。
|
16天前
|
设计模式 Java 开发者
如何快速上手【Spring AOP】?从动态代理到源码剖析(下篇)
Spring AOP的实现本质上依赖于代理模式这一经典设计模式。代理模式通过引入代理对象作为目标对象的中间层,实现了对目标对象访问的控制与增强,其核心价值在于解耦核心业务逻辑与横切关注点。在框架设计中,这种模式广泛用于实现功能扩展(如远程调用、延迟加载)、行为拦截(如权限校验、异常处理)等场景,为系统提供了更高的灵活性和可维护性。
|
5月前
|
JSON 前端开发 Java
深入理解 Spring Boot 中日期时间格式化:@DateTimeFormat 与 @JsonFormat 完整实践
在 Spring Boot 开发中,日期时间格式化是前后端交互的常见痛点。本文详细解析了 **@DateTimeFormat** 和 **@JsonFormat** 两个注解的用法,分别用于将前端传入的字符串解析为 Java 时间对象,以及将时间对象序列化为指定格式返回给前端。通过完整示例代码,展示了从数据接收、业务处理到结果返回的全流程,并总结了解决时区问题和全局配置的最佳实践,助你高效处理日期时间需求。
592 0
|
5月前
|
存储 Java 数据库
Spring Boot 注册登录系统:问题总结与优化实践
在Spring Boot开发中,注册登录模块常面临数据库设计、密码加密、权限配置及用户体验等问题。本文以便利店销售系统为例,详细解析四大类问题:数据库字段约束(如默认值缺失)、密码加密(明文存储风险)、Spring Security配置(路径权限不当)以及表单交互(数据丢失与提示不足)。通过优化数据库结构、引入BCrypt加密、完善安全配置和改进用户交互,提供了一套全面的解决方案,助力开发者构建更 robust 的系统。
156 0
|
2月前
|
缓存 安全 Java
Spring 框架核心原理与实践解析
本文详解 Spring 框架核心知识,包括 IOC(容器管理对象)与 DI(容器注入依赖),以及通过注解(如 @Service、@Autowired)声明 Bean 和注入依赖的方式。阐述了 Bean 的线程安全(默认单例可能有安全问题,需业务避免共享状态或设为 prototype)、作用域(@Scope 注解,常用 singleton、prototype 等)及完整生命周期(实例化、依赖注入、初始化、销毁等步骤)。 解析了循环依赖的解决机制(三级缓存)、AOP 的概念(公共逻辑抽为切面)、底层动态代理(JDK 与 Cglib 的区别)及项目应用(如日志记录)。介绍了事务的实现(基于 AOP
|
8月前
|
XML Java 开发者
Spring Boot中的AOP实现
Spring AOP(面向切面编程)允许开发者在不修改原有业务逻辑的情况下增强功能,基于代理模式拦截和增强方法调用。Spring Boot通过集成Spring AOP和AspectJ简化了AOP的使用,只需添加依赖并定义切面类。关键概念包括切面、通知和切点。切面类使用`@Aspect`和`@Component`注解标注,通知定义切面行为,切点定义应用位置。Spring Boot自动检测并创建代理对象,支持JDK动态代理和CGLIB代理。通过源码分析可深入了解其实现细节,优化应用功能。
368 6
|
5月前
|
Java 开发者 微服务
Spring Cloud OpenFeign详解与实践
总结起来说,Spring Cloud OpenFeign提供了一种简单易懂且高效的方式去实现微服务之间通信.它隐藏了许多复杂性,并且允许开发者以声明式方式编写HTTP客户端代码.如果你正在开发基于Spring Cloud 的微服务架构系统,Spring Cloud Open Feign是一个非常好用且强大工具.
342 33
|
5月前
|
JSON 前端开发 Java
深入理解 Spring Boot 中日期时间格式化:@DateTimeFormat 与 @JsonFormat 完整实践
在 Spring Boot 开发中,处理前后端日期交互是一个常见问题。本文通过 **@DateTimeFormat** 和 **@JsonFormat** 两个注解,详细讲解了如何解析前端传来的日期字符串以及以指定格式返回日期数据。文章从实际案例出发,结合代码演示两者的使用场景与注意事项,解决解析失败、时区偏差等问题,并提供全局配置与局部注解的实践经验。帮助开发者高效应对日期时间格式化需求,提升开发效率。
1234 2

热门文章

最新文章