最新最全面的Spring详解(四)——面向切面编程(下)

简介: 最新最全面的Spring详解(四)——面向切面编程(下)

🍀切入点表达式运算


可以使用’ &&’ || ‘和’ ! '组合切入点表达式。 您还可以通过名称引用切入点表达式。 下面的例子展示了三个切入点表达式:

@Pointcut("execution(public * *(..))")
private void anyPublicOperation() {} 
@Pointcut("within(com.xyz.myapp.trading..*)")
private void inTrading() {} 
@Pointcut("anyPublicOperation() && inTrading()")
private void tradingOperation() {} 

🍀共享公共切入点定义


在使用企业应用程序时,开发人员经常希望从几个切面引用应用程序的模块和特定的操作集。 我们建议定义一个【CommonPointcut】切面来捕获通用的切入点表达式。 这样一个方面典型地类似于以下示例:

package com.xyz.myapp;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Pointcut;
@Aspect
public class CommonPointcuts {
    /**
     * A join point is in the web layer if the method is defined
     * in a type in the com.xyz.myapp.web package or any sub-package
     * under that.
     */
    @Pointcut("within(com.xyz.myapp.web..*)")
    public void inWebLayer() {}
    /**
     * A join point is in the service layer if the method is defined
     * in a type in the com.xyz.myapp.service package or any sub-package
     * under that.
     */
    @Pointcut("within(com.xyz.myapp.service..*)")
    public void inServiceLayer() {}
    /**
     * A join point is in the data access layer if the method is defined
     * in a type in the com.xyz.myapp.dao package or any sub-package
     * under that.
     */
    @Pointcut("within(com.xyz.myapp.dao..*)")
    public void inDataAccessLayer() {}
    /**
     * A business service is the execution of any method defined on a service
     * interface. This definition assumes that interfaces are placed in the
     * "service" package, and that implementation types are in sub-packages.
     *
     * If you group service interfaces by functional area (for example,
     * in packages com.xyz.myapp.abc.service and com.xyz.myapp.def.service) then
     * the pointcut expression "execution(* com.xyz.myapp..service.*.*(..))"
     * could be used instead.
     *
     * Alternatively, you can write the expression using the 'bean'
     * PCD, like so "bean(*Service)". (This assumes that you have
     * named your Spring service beans in a consistent fashion.)
     */
    @Pointcut("execution(* com.xyz.myapp..service.*.*(..))")
    public void businessService() {}
    /**
     * A data access operation is the execution of any method defined on a
     * dao interface. This definition assumes that interfaces are placed in the
     * "dao" package, and that implementation types are in sub-packages.
     */
    @Pointcut("execution(* com.xyz.myapp.dao.*.*(..))")
    public void dataAccessOperation() {}
}

您可以在任何需要切入点表达式的地方引用在这样一个切面中定义的切入点。 例如,要使服务层成为事务性的,可以这样写:

<aop:config>
    <aop:advisor
        pointcut="com.xyz.myapp.CommonPointcuts.businessService()"
        advice-ref="tx-advice"/>
</aop:config>
<tx:advice id="tx-advice">
    <tx:attributes>
        <tx:method name="*" propagation="REQUIRED"/>
    </tx:attributes>
</tx:advice>

4️⃣声明通知


通知与切入点表达式相关联,并在切入点匹配的方法执行之前、之后或前后运行。 切入点表达式可以是对指定切入点的【简单引用】,也可以是适当声明的切入点表达式。


🍀(1)(Before advice)前置通知


你可以使用【@Before】注解在方面中声明before通知:

import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
@Aspect
public class BeforeExample {
    @Before("com.xyz.myapp.CommonPointcuts.dataAccessOperation()")
    public void doAccessCheck() {
        // ...
    }
}

如果使用切入点表达式,可以将前面的示例重写为以下示例:

import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
@Aspect
public class BeforeExample {
    @Before("execution(* com.xyz.myapp.dao.*.*(..))")
    public void doAccessCheck() {
        // ...
    }
}

🍀(2)(After returning advice)返回通知


当匹配的方法执行正常返回时,返回通知运行。 你可以通过使用【@AfterReturning】注解声明它:

import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.AfterReturning;
@Aspect
public class AfterReturningExample {
    @AfterReturning("com.xyz.myapp.CommonPointcuts.dataAccessOperation()")
    public void doAccessCheck() {
        // ...
    }
}

有时,您需要在通知主体中访问返回的实际值。 你可以使用’ @afterreturn '的形式绑定返回值以获得访问,如下例所示:

import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.AfterReturning;
@Aspect
public class AfterReturningExample {
    @AfterReturning(
        pointcut="com.xyz.myapp.CommonPointcuts.dataAccessOperation()",
        returning="retVal")
    public void doAccessCheck(Object retVal) {
        // ...
    }
}

🍀(3)(After throwing advice)抛出异常后通知


抛出通知后,当匹配的方法执行通过抛出异常退出时运行。 你可以通过使用【 @AfterThrowing】注解来声明它,如下面的例子所示:

import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.AfterThrowing;
@Aspect
public class AfterThrowingExample {
    @AfterThrowing("com.xyz.myapp.CommonPointcuts.dataAccessOperation()")
    public void doRecoveryActions() {
        // ...
    }
}

通常,您如果希望通知仅在【抛出给定类型】的异常时运行,而且您还经常需要访问通知主体中抛出的异常。 你可以使用’ thrown ‘属性来限制匹配(如果需要,则使用’ Throwable '作为异常类型),并将抛出的异常绑定到一个advice参数。 下面的例子展示了如何做到这一点:

import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.AfterThrowing;
@Aspect
public class AfterThrowingExample {
    @AfterThrowing(
        pointcut="com.xyz.myapp.CommonPointcuts.dataAccessOperation()",
        throwing="ex")
    public void doRecoveryActions(DataAccessException ex) {
        // ...
    }
}

【throwing】属性中使用的【名称必须与通知方法中的参数名称】相对应。 当一个方法执行通过抛出异常而退出时,异常将作为相应的参数值传递给advice方法。


🍀(4)After (Finally) 最终通知


After (finally)通知在匹配的方法执行退出时运行。 它是通过使用【@After 】注解声明的。 After advice必须准备好处理正常和异常返回条件,它通常用于释放资源以及类似的目的。 下面的例子展示了如何使用after finally通知:

import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.After;
@Aspect
public class AfterFinallyExample {
    @After("com.xyz.myapp.CommonPointcuts.dataAccessOperation()")
    public void doReleaseLock() {
        // ...
    }
}


更多值得注意的地方,AspectJ中的’ @After '通知被定义为【after finally】,类似于try-catch语句中的finally块。 它将对任何结果,其中包括【正常返回】或【从连接点抛出异常】都会进行调用,而【 @ afterreturn】只适用于成功的正常返回。


🍀(5)Around通知


【Around advice】环绕匹配的方法执行。 它有机会在方法运行之前和之后进行工作,并确定方法何时、如何运行,甚至是否真正运行。 如果您需要在方法执行之前和之后以线程安全的方式共享状态(例如,启动和停止计时器),经常使用Around通知。 我们推荐,总是使用最弱的通知形式,以满足你的要求(也就是说,不要使用环绕通知,如果前置通知也可以完成需求)。


Around通知是通过使用【@Around】注解声明的。 advice方法的第一个参数必须是【ProceedingJoinPoint】类型。 在通知体中,在【ProceedingJoinPoint】上调用【proceed()】会导致底层方法运行。 【proceed】方法也可以传入【Object[] 】。 当方法执行时,数组中的值被用作方法执行的参数。


下面的例子展示了如何使用around advice:

import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.ProceedingJoinPoint;
@Aspect
public class AroundExample {
    @Around("com.xyz.myapp.CommonPointcuts.businessService()")
    public Object doBasicProfiling(ProceedingJoinPoint pjp) throws Throwable {
        // 我们可以在前边做一些工作,比如启动计时器
        // 这里是真正的方法调用的地方
        Object retVal = pjp.proceed();
        // 我们可以在后边做一些工作,比如停止计时器,搜集方法的执行时间
        return retVal;
    }
}

注意: around通知返回的值是【方法调用者看到的返回值】。 例如,一个简单的缓存切面可以从缓存返回一个值(如果它有一个值),如果没有,则调用’ proceed() '。 注意,【proceed方法】你可以只调用一次,也可以调用多次,也可以根本不去调用, 这都是可以的。


🍀(6)通知的参数


Spring提供了完整类型的通知,这意味着您可以在【通知签名】中声明【所需的参数】(就像我们前面在返回和抛出示例中看到的那样)。


访问当前 JoinPoint


任何通知方法都可以声明一个类型为【org.aspectj.lang.JoinPoint】的参数作为它的【第一个参数】(注意,around通知需要声明类型为’ ProceedingJoinPoint )的第一个参数,它是【oinPoint】的一个子类。 【JoinPoint】接口提供了许多有用的方法:


getArgs(): 返回方法参数。

getThis(): 返回代理对象。

getTarget(): 返回目标对象。

getSignature(): 返回被通知的方法的签名。

toString(): 打印被建议的方法的有用描述。

@Before("beforePointcut()")
private void beforeAdvice(JoinPoint jp) throws InvocationTargetException, IllegalAccessException {
    MethodSignature signature = (MethodSignature)jp.getSignature();
    // 能拿到方法,能不能拿到方法的注解
    Method method = signature.getMethod();
    // 调用方法的过程
    method.invoke(jp.getTarget(), jp.getArgs());
    System.out.println("this is before advice");
}

bb4a2f9156ef44ad95f99eff86b8581d.png


将参数传递给Advice


我们已经看到了如何绑定【返回值或异常值】。 要使参数值对通知主体可用,可以使用【args 】的绑定形式。 如果在args表达式中使用【参数名】代替类型名,则在【调用通知】时将传递相应值作为参数值。


举个例子应该能更清楚地说明这一点:

@Override
public String order(Integer money) {
    try {
        logger.info("这是order的方法");
        return "inner try";
    } finally {
        logger.info("finally");
        //return "finally";
    }
}
@Before("execution(* com.ydlclass.service.impl.OrderService.*(..)) && args(money,..)")
public void validateAccount(Integer money) {
    System.out.println("before--------" + money);
}

切入点表达式的’ args(account,…) '部分有两个目的


首先,它限制只匹配哪些方法执行,其中方法接受至少一个参数,并且传递给该参数的参数是’ Account '的一个实例。


其次,它通过’ Account ‘参数使通知可以使用实际的’ Account '对象。

另一种方式是【编写方法】声明一个切入点,该切入点在匹配连接点时“提供”‘Account’对象值,然后从通知中引用指定的切入点。 这看起来如下:

@Pointcut("com.xyz.myapp.CommonPointcuts.dataAccessOperation() && args(account,..)")
private void accountDataAccessOperation(Account account) {}
@Before("accountDataAccessOperation(account)")
public void validateAccount(Account account) {
    // ...
}

5️⃣引入Introduction


引入使切面能够声明被通知的对象【实现给定的接口】,也就是让代理对象实现新的接口。

@DeclareParents(value="com.ydlclass.service.impl.OrderService",defaultImpl= ActivityService.class)
public static IActivityService activityService;

要实现的接口由注解字段的类型决定。 @DeclareParents注解的【value】属性是一个AspectJ类型类。 任何与之匹配的类型的bean都将实现【UsageTracked】接口。 注意,在前面示例的before通知中,服务bean可以直接用作【UsageTracked】接口的实现。 如果以编程方式访问bean,您将编写以下代码:

IActivityService bean = ctx.getBean(IActivityService.class);
bean.sendGif();

搞过debug看到了,生成的代理实现了两个接口:


1c359fcddcd848b7971d7a0202077ded.png


6️⃣Advice Ordering


当多个通知都想在同一个连接点上运行时,Spring AOP遵循与AspectJ相同的优先规则来确定通知执行的顺序。优先级最高的通知在【进入时】首先运行【因此,给定两个before通知,优先级最高的将首先运行】。从连接点【退出】时,优先级最高的通知最后运行【因此,给定两个after通知,优先级最高的通知将第二运行】。

当在不同切面定义的两个通知都需要在同一个连接点上运行时,除非另行指定,否则执行顺序是未定义的。 您可以通过指定优先级来控制执行顺序。在切面类中使用【Ordered】接口,或者用【@Order】注释它。 对于两个切面,从’Ordered.getOrder() '返回较低值的切面(或注释值)具有较高的优先级。


7️⃣AOP 的例子


业务代码的执行有时会由于【并发性问题】而失败(例如,死锁而导致的失败)。 如果再次尝试该操作,很可能在下一次尝试时成功。 对于适合在这种条件下重试的业务服务,我们希望进行透明地重试操作。 这是一个明显跨越service层中的多个服务的需求,因此是通过切面实现的理想需求。


因为我们想要重试操作,所以我们需要使用around通知,以便我们可以多次调用’ proceed '。 下面的例子显示了基本方面的实现:

@Aspect
public class ConcurrentOperationExecutor implements Ordered {
    private static final int DEFAULT_MAX_RETRIES = 2;
    private int maxRetries = DEFAULT_MAX_RETRIES;
    private int order = 1;
    public void setMaxRetries(int maxRetries) {
        this.maxRetries = maxRetries;
    }
    public int getOrder() {
        return this.order;
    }
    public void setOrder(int order) {
        this.order = order;
    }
    @Around("com.xyz.myapp.CommonPointcuts.businessService()")
    public Object doConcurrentOperation(ProceedingJoinPoint pjp) throws Throwable {
        int numAttempts = 0;
        PessimisticLockingFailureException lockFailureException;
        do {
            numAttempts++;
            try {
                return pjp.proceed();
            }
            catch(PessimisticLockingFailureException ex) {
                lockFailureException = ex;
            }
        } while(numAttempts <= this.maxRetries);
        throw lockFailureException;
    }
}

注意,切面实现了’ Ordered '接口,因此我们可以将【该切面的优先级】设置得高于【事务通知】,我们希望每次重试时都有一个新的事务。 ’ maxRetries ‘和’ order '属性都是可以由Spring配置注入的。

对应的Spring配置如下:

<aop:aspectj-autoproxy/>
<bean id="concurrentOperationExecutor" class="com.xyz.myapp.service.impl.ConcurrentOperationExecutor">
    <property name="maxRetries" value="3"/>
    <property name="order" value="100"/>
</bean>

五、基于schema的AOP支持


如果您喜欢基于xml的格式,Spring还提供了使用【aop命名空间】标记定义切面的支持。 它支持与使用@AspectJ样式时完全相同的切入点表达式和通知类型。


要使用本节中描述的aop命名空间标记,您需要导入’ spring-aop '模块。


在Spring配置中,所有【切面和通知】元素都必须放在一个<aop:config> 元素中(在应用程序上下文配置中可以有多个<aop:config> 元素)。 一个<aop:config> 元素可以包含切入点、通知和切面元素(注意这些元素必须按照这个顺序声明)。


🍀配置切面,切点表达式,通知的方法如下

<?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 https://www.springframework.org/schema/beans/spring-beans.xsd
        http://www.springframework.org/schema/aop https://www.springframework.org/schema/aop/spring-aop.xsd">
</beans>
<?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 https://www.springframework.org/schema/beans/spring-beans.xsd
        http://www.springframework.org/schema/aop https://www.springframework.org/schema/aop/spring-aop.xsd">
<!--    <aop:aspectj-autoproxy/>-->
    <aop:config>
        <aop:aspect ref="aop">
            <aop:pointcut id="point" expression="execution(* com.ydlclass..*(..))"/>
            <aop:before method="beforeAdvice" pointcut="execution(* com.ydlclass..*(..)) and args(money,..)"/>
            <aop:after method="afterAdvice" pointcut-ref="point"/>
            <aop:after-returning method="afterReturningAdvice" pointcut-ref="point"/>
            <aop:after-throwing throwing="ex" method="afterThrowing" pointcut-ref="point"/>
        </aop:aspect>
    </aop:config>
    <bean id="aop" class="com.ydlclass.aspecj.MyAop"/>
    <bean id="orderService" class="com.ydlclass.service.impl.OrderService"/>
    <bean id="userService" class="com.ydlclass.service.impl.UserService"/>
</beans>

🍀Introduction

<aop:aspect id="usageTrackerAspect" ref="usageTracking">
    <aop:declare-parents
        types-matching="com.xzy.myapp.service.*+"
        implement-interface="com.xyz.myapp.service.tracking.UsageTracked"
        default-impl="com.xyz.myapp.service.tracking.DefaultUsageTracked"/>
</aop:aspect>

🍀AOP示例

public class ConcurrentOperationExecutor implements Ordered {
    private static final int DEFAULT_MAX_RETRIES = 2;
    private int maxRetries = DEFAULT_MAX_RETRIES;
    private int order = 1;
    public void setMaxRetries(int maxRetries) {
        this.maxRetries = maxRetries;
    }
    public int getOrder() {
        return this.order;
    }
    public void setOrder(int order) {
        this.order = order;
    }
    public Object doConcurrentOperation(ProceedingJoinPoint pjp) throws Throwable {
        int numAttempts = 0;
        PessimisticLockingFailureException lockFailureException;
        do {
            numAttempts++;
            try {
                return pjp.proceed();
            }
            catch(PessimisticLockingFailureException ex) {
                lockFailureException = ex;
            }
        } while(numAttempts <= this.maxRetries);
        throw lockFailureException;
    }
}

对应的Spring配置如下:

<aop:config>
    <aop:aspect id="concurrentOperationRetry" ref="concurrentOperationExecutor">
        <aop:pointcut id="idempotentOperation"
            expression="execution(* com.xyz.myapp.service.*.*(..))"/>
        <aop:around
            pointcut-ref="idempotentOperation"
            method="doConcurrentOperation"/>
    </aop:aspect>
</aop:config>
<bean id="concurrentOperationExecutor"
    class="com.xyz.myapp.service.impl.ConcurrentOperationExecutor">
        <property name="maxRetries" value="3"/>
        <property name="order" value="100"/>
</bean>

六、AOP声明风格


一旦您确定使用aop是实现给定需求的最佳方法,您如何决定是使用Spring AOP还是Aspect?是使用@AspectJ注解风格还是Spring XML风格?


如果您选择使用Spring AOP,那么您可以选择【@AspectJ或XML】样式。


XML样式可能是现有Spring用户最熟悉的,并且它是由真正的【pojo支持】(侵入性很低)的。 当使用AOP作为配置企业服务的工具时,XML可能是一个很好的选择(一个很好的理由是您【是否认为切入点表达式】 是需要【独立更改】的一部分配置)。使用XML样式,可以从配置中更清楚地看出系统中存在哪些切面。


XML样式有两个缺点。 首先,它没有将它所处理的需求的实现完全封装在一个地方。 其次,与@AspectJ风格相比,XML风格在它能表达的内容上稍微受到一些限制,不可能在XML中声明的命名切入点进行组合。 例如,在@AspectJ风格中,你可以写如下内容:

@Pointcut("execution(* get*())")
public void propertyAccess() {}
@Pointcut("execution(org.xyz.Account+ *(..))")
public void operationReturningAnAccount() {}
@Pointcut("propertyAccess() && operationReturningAnAccount()")
public void accountPropertyAccess() {}

在XML样式中,可以声明前两个切入点:

<aop:pointcut id="propertyAccess"
        expression="execution(* get*())"/>
<aop:pointcut id="operationReturningAnAccount"
        expression="execution(org.xyz.Account+ *(..))"/>

XML方法的缺点是不能通过组合这些定义来定义“accountPropertyAccess”切入点。


@AspectJ还有一个优点,即@AspectJ切面可以被Spring AOP和AspectJ理解(从而被使用)。 因此,如果您以后决定需要AspectJ的功能来实现额外的需求,您可以轻松地迁移到经典的AspectJ当中。


总的来说,Spring团队更喜欢自定义切面的@AspectJ风格,而不是简单的企业服务配置。


七、以编程方式创建@AspectJ代理


除了通过使用<aop:config> 或<aop:aspectj-autoproxy>在配置中声明方面之外,还可以通过编程方式创建通知目标对象的代理。


代码如下,这只是一个小例子,用来看一下spring是怎么封装代理的:


public static void main(String[] args) {
    AspectJProxyFactory aspectJProxyFactory = new AspectJProxyFactory(new OrderService());
    aspectJProxyFactory.addAspect(MyAspect.class);
    IOrderService proxy = (IOrderService)aspectJProxyFactory.getProxy();
    proxy.order(111);
}


相关文章
|
2月前
|
Java Spring
【编程笔记】在 Spring 项目中使用 RestTemplate 发送网络请求
【编程笔记】在 Spring 项目中使用 RestTemplate 发送网络请求
94 0
|
3月前
|
Java 数据库连接 应用服务中间件
Spring5源码(39)-Aop事物管理简介及编程式事物实现
Spring5源码(39)-Aop事物管理简介及编程式事物实现
26 0
|
4月前
|
Java 程序员 Maven
Spring AOP入门指南:轻松掌握面向切面编程的基础知识
Spring AOP入门指南:轻松掌握面向切面编程的基础知识
|
4月前
|
XML 设计模式 SQL
Spring6 面向切面(AOP)
Spring6 面向切面(AOP)
|
9天前
|
XML 监控 安全
18:面向切面编程-Java Spring
18:面向切面编程-Java Spring
26 5
|
16天前
|
安全 Java Maven
[AIGC] Spring Boot中的切面编程和实例演示
[AIGC] Spring Boot中的切面编程和实例演示
|
28天前
|
Java Spring
代码优雅的转变:基于注解的AOP编程在Spring中的实践
代码优雅的转变:基于注解的AOP编程在Spring中的实践
17 0
|
28天前
|
Java Spring
切面编程的锋芒:Spring切入点的玩法与技巧
切面编程的锋芒:Spring切入点的玩法与技巧
16 0
切面编程的锋芒:Spring切入点的玩法与技巧
|
28天前
|
Java 数据库 Spring
切面编程的艺术:Spring动态代理解析与实战
切面编程的艺术:Spring动态代理解析与实战
29 0
切面编程的艺术:Spring动态代理解析与实战
|
1月前
|
XML 安全 Java
spring面向切面编程AOP
spring面向切面编程AOP