[Spring] Spring AOP

🌸个人主页:https://blog.csdn.net/2301_80050796?spm=1000.2115.3001.5343

🏵️热门专栏:

🧊 Java基本语法(97平均质量分)https://blog.csdn.net/2301_80050796/category_12615970.html?spm=1001.2014.3001.5482

🍕 Collection与数据结构 (92平均质量分)https://blog.csdn.net/2301_80050796/category_12621348.html?spm=1001.2014.3001.5482

🧀线程与网络(96平均质量分) https://blog.csdn.net/2301_80050796/category_12643370.html?spm=1001.2014.3001.5482

🍭MySql数据库(93平均质量分)https://blog.csdn.net/2301_80050796/category_12629890.html?spm=1001.2014.3001.5482

🍬算法(97平均质量分)https://blog.csdn.net/2301_80050796/category_12676091.html?spm=1001.2014.3001.5482

🍃 Spring(97平均质量分)https://blog.csdn.net/2301_80050796/category_12724152.html?spm=1001.2014.3001.5482

感谢点赞与关注~~~

目录

  • [1. AOP概述](#1. AOP概述)
  • [2. Spring AOP快速入门](#2. Spring AOP快速入门)
    • [2.1 引入AOP依赖](#2.1 引入AOP依赖)
    • [2.2 编写AOP程序](#2.2 编写AOP程序)
  • [3. Spring AOP详解](#3. Spring AOP详解)
    • [3.1 核心概念](#3.1 核心概念)
      • [3.1.1 切点(PointCut)](#3.1.1 切点(PointCut))
      • [3.1.2 连接点(Join Point)](#3.1.2 连接点(Join Point))
      • [3.1.3 通知(Advice)](#3.1.3 通知(Advice))
      • [3.1.4 切面(Aspect)](#3.1.4 切面(Aspect))
    • [3.2 通知类型](#3.2 通知类型)
    • [3.3 @Pointcut](#3.3 @Pointcut)
    • [3.4 切面优先级@Order](#3.4 切面优先级@Order)
    • [3.5 切点表达式](#3.5 切点表达式)
      • [3.5.1 execution表达式](#3.5.1 execution表达式)
      • [3.5.2 @annotation(翻译:注解)](#3.5.2 @annotation(翻译:注解))
  • [4. Spring AOP原理](#4. Spring AOP原理)
    • [4.1 代理模式](#4.1 代理模式)
      • [4.1.1 静态代理](#4.1.1 静态代理)
      • [4.1.2 动态代理](#4.1.2 动态代理)

1. AOP概述

  • 什么是AOP?
    所谓AOP,就是面相切面的编程.什么是面向切面的编程呢,切面就是指定某一类特定的问题,所以,面向切面的编程就是正对于同一类问题进行编程.
    简单来说,就是针对某一类事情的集中处理.
  • 什么是Spring AOP?
    AOP是一种思想,实现AOP的方法有很多,有Spring AOP,有AspectJ,有CGLIB等.Spring AOP是其中的一种实现方式.

某种程度上,他和我们前面提到的统一功能处理的效果差不多,但是,统一功能处理并不等同于SpringAOP,拦截器的作用维度是URL,即一次请求响应,但是AOP的作用维度更加细致,可以对包括包,类,方法,参数等进行统一功能的处理,可以实现更加复杂的业务逻辑.

举例:

现在有一些业务执行效率比较低,我们需要对接口进行优化,第一步需要定位出耗时较长的业务方法,在针对业务额来进行优化.我们就可以在每一个方法的结束和开始的地方加上时间戳,之后作差展示出来.但是在一个项目中,我们有好多接口,此时在接口中一个个低添加时间戳又不是很现实,此时我们就需要用到AOP.

接下来,我们就来看看AOP如何使用.

2. Spring AOP快速入门

需求: 统计图书管理系统各个接口和方法的执行时间.

2.1 引入AOP依赖

在pom文件中添加依赖:

xml 复制代码
<dependency>
	<groupId>org.springframework.boot</groupId>
	<artifactId>spring-boot-starter-aop</artifactId>
</dependency>

2.2 编写AOP程序

记录Controller中每个方法的执行时间.

java 复制代码
package com.jrj.books.component;

import lombok.extern.slf4j.Slf4j;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.springframework.stereotype.Component;

@Aspect//表示切面
@Component
@Slf4j
public class TimeAspect {
    @Around("execution(* com.jrj.books.controller.*.*(..)))")//切点表达式
    public Object recordTime(ProceedingJoinPoint proceedingJoinPoint) throws Throwable {//参数是连接点
        long begin = System.currentTimeMillis();
        Object ret = proceedingJoinPoint.proceed();//执行连接点方法
        long end = System.currentTimeMillis();
        log.info("执行耗时:" + (end-begin));
        return ret;
    }
}

上述代码的测试结果如下:

我们看到了接口返回了执行耗时.

下面我们对上面的代码进行详细的解释:

  1. @Aspect : 表示的是切面.表示这个类是一个切面类.

  2. @Around: 环绕通知,在目标方法前后都会有代码执行 .

  3. proceedingJoinPoint.proceed(): 执行加入切面的连接点.

  4. execution(* com.jrj.books.controller.*.*(..))):切点表达式,表示这个切面对该项目下的那个方法生效.

  5. ProceedingJoinPoint proceedingJoinPoint: 加入切面的连接点.

3. Spring AOP详解

3.1 核心概念

3.1.1 切点(PointCut)

切点,也就是切入点,我们一般在通知类型注解的后面使用execution切点表达式 来描述切点.切点就是告诉程序对那些方法的功能进行加强.

3.1.2 连接点(Join Point)

满足切点表达式规则的方法,就是连接点,也就是可以被该切面所作用的方法.连接点的数据类型是:ProceedingJoinPoint.也就是com.jrj.books.controller.包下的所有方法都是该切面的连接点.

切点与连接点之间的关系

连接点事满足切点表达式的元素,切点可以看做是一个保存了众多连接点的集合.

3.1.3 通知(Advice)

通知就是具体要做的工作,指在指定的方法中重复那些逻辑 ,也就是共性功能.

比如上面实现计算运行前后的时间差的业务逻辑就叫做通知.

3.1.4 切面(Aspect)

切面=切点+通知

切面既包含了逻辑的定义,也包含连接点的定义.

切面说在的类,我们一般称为切面类.

3.2 通知类型

SpringAOP中的通知类型有以下几种:

  • @Around:环绕通知,表示该方法在目标方法前后都会被执行.
  • @Before:前置通知,表示该方法只在目标方法之前被执行.
  • @After:后置通知,表示该方法只在目标方法之后被执行,无论是否有异常发生.
  • @AfterReturning:返回后通知,表示该方法只在目标方法之后被执行,方法返回了之后才会执行,有异常发生不会执行.
  • @AfterThrowing:异常后通知.表示方法在发生异常之后执行.

代码演示:

java 复制代码
@Slf4j
@Component
@Aspect
public class AspectDemo {
    @Before("execution(* com.jrj.aspect.*.*(..))")
    public void before(){
        log.info("before method");
    }
    @After("execution(* com.jrj.aspect.*.*(..))")
    public void after(){
        log.info("after method");
    }
    @Around("execution(* com.jrj.aspect.*.*(..))")
    public Object around(ProceedingJoinPoint proceedingJoinPoint) throws Throwable {
        log.info("around before");
        Object o = proceedingJoinPoint.proceed();
        log.info("around after");
        return o;
    }
    @AfterReturning("execution(* com.jrj.aspect.*.*(..))")
    public void afterReturning(){
        log.info("afterReturning method");
    }
    @AfterThrowing("execution(* com.jrj.aspect.*.*(..))")
    public void afterThrowing(){
        log.info("afterThrowing method");
    }
}

注意\] 被`@Around`标志的方法必须有形式和返回值.其他注解标志的方法可以加上`JoinPoint`类型的形式参数,\*\*用来获取原始方法的数据. 下面我们准备测试代码:其中一个正常执行,另外一个制造一些异常出来. ```java @RestController @RequestMapping("/test") public class TestController { @RequestMapping("/t1") public String t1(){ return "t1"; } @RequestMapping("t2") public String t2(){ int a = 10/0; return ""; } } ``` 下面我们来执行代码,观察后端日志: 1. 正常执行的代码 ![在这里插入图片描述](https://i-blog.csdnimg.cn/direct/cdca206c983746eba0e5976734d0802c.png) 在程序运行正常的情况下,`@AfterThrowing`注解的方法不会执行. 从上面的执行结果中,我们可以看出:`@Around`有前置逻辑和后置逻辑两部分,这两个逻辑再内层就是`@Before`和`@After`标识的方法,再往内层就是`@AfterReturning`标注的方法. ![在这里插入图片描述](https://i-blog.csdnimg.cn/direct/37c21abcc1c94161971af32f4ef6b913.png) 2. 异常的情况 ![在这里插入图片描述](https://i-blog.csdnimg.cn/direct/0c191a87ab7d42c18df9f363da416d23.png) 在异常发生的情况下,`@Around`标识的后置逻辑不会被执行.`@AfterReturning`标识的方法不会被执行.`@AfterThrowing`表示的方法会被执行. ![在这里插入图片描述](https://i-blog.csdnimg.cn/direct/aab218d324ec4b8f9020ae39af2eda31.png) \[注意事项

• @Around 环绕通知需要调用ProceedingJoinPoint.proceed() 来让原始方法执行 ,其他通知不需要考虑目标方法执行.
• @Around 环绕通知方法的返回值,必须指定为Object,来接收原始方法的返回值,否则原始方法执行完毕,是获取不到返回值的.

3.3 @Pointcut

上面的代码中存在一个问题,就是切点表达式大量重复,这时候,Spring提供了@Pointcut注解用来把公共的切点表达式提取出来.到时候直接使用方法名把它提取出来即可.

java 复制代码
@Slf4j
@Component
@Aspect
public class AspectDemo {
    @Pointcut("execution(* com.jrj.aspect.*.*(..))")
    public void pointCut(){}
    @Before("pointCut()")
    public void before(){
        log.info("before method");
    }
    @After("pointCut()")
    public void after(){
        log.info("after method");
    }
    @Around("pointCut()")
    public Object around(ProceedingJoinPoint proceedingJoinPoint) throws Throwable {
        log.info("around before");
        Object o = proceedingJoinPoint.proceed();
        log.info("around after");
        return o;
    }
    @AfterReturning("pointCut()")
    public void afterReturning(){
        log.info("afterReturning method");
    }
    @AfterThrowing("pointCut()")
    public void afterThrowing(){
        log.info("afterThrowing method");
    }
}

注意\] 当切点使用private修饰的时候,仅可以在该类中使用.当其他切面类也需要使用到该切点的时候,就需要把private改成public,并使用**全限定方法名**. ### 3.4 切面优先级@Order 当我们在一个项目中,定义了多个切面类,**并且这些切面类的多个切点都匹配到了同一个目标方法**,当目标方法运行的时候,这些切面类中的方法都会执行,那么这些方法执行的顺序是什么样的呢?我们通过代码来验证. ```java @RequestMapping("/test2") public class TestController { @RequestMapping("/t1") public String t1(){ return "t1"; } } @Slf4j @Component @Aspect public class AspectDemo1 { @Pointcut("execution(* com.jrj.aspect.order.*.*(..))") public void pt(){} @Before("pt()") public void before(){ log.info("AspectDemo1 before"); } @After("pt()") public void after(){ log.info("AspectDemo1 after"); } } @Slf4j @Component @Aspect public class AspectDemo2 { @Before("com.jrj.aspect.order.AspectDemo1.pt()") public void before(){ log.info("AspectDemo2 before"); } @After("com.jrj.aspect.order.AspectDemo1.pt()") public void after(){ log.info("AspectDemo2 after"); } } @Slf4j @Component @Aspect public class AspectDemo3 { @Before("com.jrj.aspect.order.AspectDemo1.pt()") public void before(){ log.info("AspectDemo3 before"); } @After("com.jrj.aspect.order.AspectDemo1.pt()") public void after(){ log.info("AspectDemo3 after"); } } ``` 观察日志: ![在这里插入图片描述](https://i-blog.csdnimg.cn/direct/b85c742371f943edbeb026f42d2af42d.png) 通过上述程序的运行结果,可以看出: 存在多个切面类对应一个方法的时候,默认按照切面类的字母序排序.**Before和After成对称式分布**. * @Before:字母序靠前的先通知. * @After:字母序靠后的先通知. 此外,我们还可以通过Spring的注解,来控制切面的执行顺序:`@Order`. 使用方式如下: ```java @Slf4j @Component @Aspect @Order(1) public class AspectDemo1 { ... } @Slf4j @Component @Aspect @Order(3) public class AspectDemo2 { ... } @Slf4j @Component @Aspect @Order(2) public class AspectDemo3 { ... } ``` 观察日志: ![在这里插入图片描述](https://i-blog.csdnimg.cn/direct/64afda7d6e544165835bbfad85c3314a.png) 我们可以得出以下结论: * @Before:数字小的先执行 * @After:数字大的先执行 和上面一样,Before和After也是呈对称式分布. ![在这里插入图片描述](https://i-blog.csdnimg.cn/direct/8a1ecbedf8de46b8a92c14ac5bd46d78.png) ### 3.5 切点表达式 上面的代码中,我们一直在使用切点表达式来描述切点,下面我们来介绍一下切点表达式. 切点表达式最常见的有以下两种方式: 1. `execution(...)`: 根据**方法的签名来匹配**. 2. `@annotation`:根据**注解匹配**. #### 3.5.1 execution表达式 `execution(<访问限定符> <返回类型> <包名.类名.方法(方法参数)><异常>)` 其中访问**限定符可以省略** . ![在这里插入图片描述](https://i-blog.csdnimg.cn/direct/723f4674fd0a4c88a49fd036fe28044a.png) 切点表达式支持通配符: 1. `*`:匹配任何字符,只可以匹配一个元素,可以匹配返回类型,包名,类名,方法名,方法参数. a. 包名使用`* `表示任意包(⼀层包使用⼀个`*`) b. 类名使用`* `表示任意类 c. 返回值使用`*` 表示任意返回值类型 d. 方法名使用`*` 表示任意方法 e. 参数使用`*` 表示⼀个任意类型的参数 2. `..`:匹配多个连续的任意符号,可以通配任意层级的包,或任意类型,任意个数的参数. a. 使用 `..` 配置包名,标识此包以及此包下的所有子包 b. 可以使用 `..` 配置参数,任意个任意类型的参数 \[注意\] `*`只可以匹配一个元素,`..`可以匹配多个元素. 举例: * 匹配TestController下的所有无参方法. execution(* com.example.demo.controller.TestController.*()) * 匹配TestController下的所有方法 execution(* com.example.demo.controller.TestController.*(..)) * 匹配com.example.demo包下,子孙包下的所有类的所有方法 execution(* com.example.demo..*(..)) * 匹配所有包下面的TestController类的所有方法. execution(* com..TestController.*(..)) #### 3.5.2 @annotation(翻译:注解) 准备接口测试代码: ```java @RestController @RequestMapping("/user") public class UserController { @RequestMapping("/u1") public String user1(){ return "u1"; } @RequestMapping("/u2") public String user2(){ return "u2"; } } @RestController @RequestMapping("/test") public class TestController { @RequestMapping("/t1") public String t1(){ return "t1"; } @RequestMapping("t2") public String t2(){ int a = 10/0; return ""; } } ``` 如果我们要匹配多个无规则的方法呢?比如UserController中的user1方法和TestController中的t1方法.这时候使用execution显得有些麻烦.这时候我们就可以借助切点表达式的另一种方式.`@annotation`注解来描述一类切点.具体实现步骤如下: 1. 编写自定义注解 2. 使用`@annotation`表达式来描述切点. 3. 在连接点的方法上**添加自定义注解**. * 自定义注解 创建一个自定义注解类,创建的方式和创建类是一样的. ![在这里插入图片描述](https://i-blog.csdnimg.cn/direct/223eec0dbfbc46b7a33dd76150e6b501.png) ```java @Target(ElementType.METHOD) @Retention(RetentionPolicy.RUNTIME) public @interface MyAspect {} ``` 1. `@Target`注解,代表的是注解可以修饰的对象,常用的有以下四种取值: `ElementType.TYPE`:用于描述类、接口(包括注解类型)或enum声明 `ElementType.METHOD`:描述方法 `ElementType.PARAMETER`:描述参数 `ElementType.TYPE_USE`:可以标注任意类型 2. `@Retention`注解,代表的是`Annotation`被保留的时间.表示的是该注解的生命周期. `RetentionPolicy.SOURCE`:表示注解仅存在于源代码中,编译成字节码后会被丢弃. `RetentionPolicy.CLASS`:编译时注解.表示注解存在于源代码和字节码中,但在运行时会被丢弃. `RetentionPolicy.RUNTIME`:运行时注解,表示注解存在于源代码,字节码和运行时中. * 切面类 使用`@annotation`切点表达式定义切点,只对@MyAspect生效. ```java @Aspect @Slf4j @Component public class AspectDemo4 { @Before("@annotation(com.jrj.aspect.MyAspect)") public void before(){ log.info("before method"); } @After("@annotation(com.jrj.aspect.MyAspect))") public void after(){ log.info("after method"); } } ``` * 为指定方法添加自定义注解,在加上自定义注解之后,在调用接口的时候就会执行切面中的方法. 在测试代码的方法下面加上自定义注解: ```java @RestController @RequestMapping("/test") public class TestController { @MyAspect @RequestMapping("/t1") public String t1(){ ... } @MyAspect @RequestMapping("t2") public String t2(){ ... } } @RestController @RequestMapping("/user") public class UserController { @MyAspect @RequestMapping("/u1") public String user1(){ ... } @MyAspect @RequestMapping("/u2") public String user2(){ ... } } ``` 观察日志: ![在这里插入图片描述](https://i-blog.csdnimg.cn/direct/fa6c44e0fde04b058560910b2fc774ce.png) 我们发现测试的接口打印出了制定的日志. ## 4. Spring AOP原理 SpringAOP使用的是**动态代理**模式来实现的.他所涉及的设计模式是代理模式. ### 4.1 代理模式 代理模式,也叫委托模式 为其他对象提供⼀种代理以控制对这个对象的访问.它的作用就是通过提供⼀个代理类,让我们在调用目标方法的时候,不再是直接对目标方法进行调用,而是通过代理类间接调用. ![在这里插入图片描述](https://i-blog.csdnimg.cn/direct/08ae4f9f277d4ee1a3592a9d8aea5ab4.png)![在这里插入图片描述](https://i-blog.csdnimg.cn/direct/5ec6a31c9f3d4cf2861de8ab0640e8cf.png) > 举例说明代理模式: > > 房屋中介与房东,艺人与经纪人,老板与秘书. 代理模式的主要角色: 1. Subject:业务接口类.这里定义的是代理和被代理对象要做的事情.可以抽象类或者接口. 2. RealSubject:业务实现类.具体的业务执行,也就是被代理的对象. 3. Proxy:代理类.RealSubject的代理. 根据代理创建的时期,可以把代理模式分为**动态代理** 和**静态代理**. #### 4.1.1 静态代理 静态代理:在程序运行前,代理类的.class文件就已经存在了.(在出租房子之前,中介已经做好了相关的工作就等租户来租房子了) 1. 接口定义(定义房东和中介要做的事情) ```java public interface HouseSubject { void rentHouse(); } ``` 2. 实现接口(房东出租房子) ```java public class RealHouseProxy implements HouseSubject{ @Override public void rentHouse() { System.out.println("我要出租房子"); } } ``` 3. 代理(中介,帮房东出租房子) ```java public class Proxy implements HouseSubject{ private RealHouseProxy realHouseProxy; public Proxy(RealHouseProxy realHouseProxy) { this.realHouseProxy = realHouseProxy; } @Override public void rentHouse() { System.out.println("我要代理出租房子"); realHouseProxy.rentHouse(); System.out.println("代理完成"); } } ``` 4. 使用 ```java public static void main(String[] args) { RealHouseProxy realHouseProxy1 = new RealHouseProxy(); Proxy proxy = new Proxy(realHouseProxy1); proxy.rentHouse(); } ``` 运行结果: ![在这里插入图片描述](https://i-blog.csdnimg.cn/direct/fbc66206eddf42faa29f0d4009156128.png) 虽然静态代理也完成了代理,但是由于代码是写死的,对目标方法的增强都是手动来完成的,非常不灵活,所以我们有了动态代理. #### 4.1.2 动态代理 我们不需要针对每个目标对象都单独创建一个代理对象,而是把这个代理对象的工作推迟到程序运行的时候由JVM来实现,也就是在程序运行的时候,根据需要动态创建代理. 常见的动态代理实现模式有两种方式: 1. jdk动态代理 实现步骤如下: * 首先定义一个接口及其实现类(相当于房东). * 之后自定义一个类,实现`InvocationHandler`接口并重写`invoke`方法,在invoke方法中**调用目标方法(被代理的方法)并自定义一些处理逻辑**. * 这里调用方法的时候使用的是反射的方式,使用invoke()方法来调用方法(**反射方式**),并传入被代理对象和args(数组中存放的是方法的参数)参数. * 通过`Proxy.newProxyInstance(ClassLoader loader,Class[] interfaces,InvocationHandler h`方法创建代理对象. ```java public interface HouseSubject { void rentHouse(); } public class RealHouseProxy implements HouseSubject{ @Override public void rentHouse() { System.out.println("我要出租房子"); } } public class JDKInvocationHandler implements InvocationHandler { private Object object; public JDKInvocationHandler(Object o) { this.object = o; } @Override public Object invoke(Object proxy, Method method, Object[] args) throws Throwable { System.out.println("我是代理,开始代理"); Object object = method.invoke(this.object,args); System.out.println("代理结束"); return object; } public static void main(String[] args) { HouseSubject houseSubject = new RealHouseProxy(); HouseSubject proxy = (HouseSubject) Proxy.newProxyInstance(houseSubject.getClass().getClassLoader(), new Class[]{HouseSubject.class},new JDKInvocationHandler(houseSubject)); proxy.rentHouse(); } } ``` [注意事项] 1. 在动态代理类中,由于代理的对象可以是任何类型的,所以被代理对象应该是Object类型的. 2. 在使用newInstance方法创建代理对象的时候,第一个传入的是被代理对象的加载器,第二个传入的是一个class类的数组,数组中放着被代理对象接口的class,最后一个放的是我们提前设定好的代理类的对象. 2. CGLIB动态代理 jdk代理致命的问题就是他只可以代理接口(实现一个接口的类),但是CGLIB动态代理可以既可以实现接口,又可以实现类. CGLIB动态代理类实现步骤: * 引入CGLIB依赖 * 定义一个被代理类 * 自定义一个代理类,并实现`MethodInterceptor `接口,实现`intercept`方法,`Intercept`方法用于调用被代理类的方法,方法和jdk代理中的`invoke`类似. * 通过`Enhancer`类的`create()`方法创建代理对象.在其中传入接口的class和之前写好的自定义CGLIB代理类的对象. ```xml cglib cglib 3.3.0 ``` ```java public interface HouseSubject { void rentHouse(); } public class RealHouseSubject implements HouseSubject{ @Override public void rentHouse() { System.out.println("我要出租房子"); } } public class CGLIBInterceptor implements MethodInterceptor { private Object object; public CGLIBInterceptor(Object object) { this.object = object; } @Override public Object intercept(Object o, Method method, Object[] objects, MethodProxy methodProxy) throws Throwable { System.out.println("我是代理,开始代理"); Object o1 = methodProxy.invoke(object,objects); System.out.println("代理结束"); return o1; } } public class Main { public static void main(String[] args) { HouseSubject houseSubject = new RealHouseSubject(); HouseSubject proxy = (HouseSubject) Enhancer.create(HouseSubject.class,new CGLIBInterceptor(houseSubject)); proxy.rentHouse(); } } ``` CGLB代理模式和jdk代理模式非常类似.

相关推荐
mghio8 小时前
Dubbo 中的集群容错
java·微服务·dubbo
Asthenia04128 小时前
Spring AOP 和 Aware:在Bean实例化后-调用BeanPostProcessor开始工作!在初始化方法执行之前!
后端
Asthenia04129 小时前
什么是消除直接左递归 - 编译原理解析
后端
Asthenia04129 小时前
什么是自上而下分析 - 编译原理剖析
后端
Asthenia04129 小时前
什么是语法分析 - 编译原理基础
后端
Asthenia041210 小时前
理解词法分析与LEX:编译器的守门人
后端
uhakadotcom10 小时前
视频直播与视频点播:基础知识与应用场景
后端·面试·架构
Asthenia041211 小时前
Spring扩展点与工具类获取容器Bean-基于ApplicationContextAware实现非IOC容器中调用IOC的Bean
后端
bobz96511 小时前
ovs patch port 对比 veth pair
后端