【Spring】之AOP详解

AOP

什么是AOP?

AOP:Aspect Oriented Programming,面向切面编程。

切面指的是某一类特定问题,因此面向切面编程也可以理解为面向特定方法编程。例如,在任何一个系统中,总有一些页面不是用户可以随便访问的,这就要对客户端发过来的请求进行检验,检验用户是否登录,这个检验用户登录的方法就是一个特定方法。此时,就可以用户AOP的思想来解决问题。

AOP是一种思想,表示对某一类事物的集中处理。

实现一个AOP 

引入依赖
<dependency>
   <groupId>org.springframework.boot</groupId>
   <artifactId>spring-boot-starter-aop</artifactId>
</dependency>

实现AOP

下述代码实现的AOP解决的问题是:对每一个接口的耗时时间使用日志来打印。这个方法的作用是对每个接口进行测试,判断哪个接口需要进行优化,因此相对来说在后台还是比较重要。

@Aspect // 表示这是一个切面类
@Slf4j // 打印日志
@Component // 将此Bean对象装配到IoC容器中
public class TimeAspect {

    /**
     * 打印每个接口耗时的日志
     * @param joinPoint 表示作用的目标方法
     * @return
     */
    @Around("execution(* com.example.demo.controller.*.*(..))")
    // 表示作用方式和作用域,即AOP在哪个环节起作用并且对哪些方式起作用
    public Object timeCost(ProceedingJoinPoint joinPoint) throws Throwable {

        long start = System.currentTimeMillis();
        Object result = joinPoint.proceed(); // 执行目标方法
        long end = System.currentTimeMillis();

        log.info(joinPoint.getSignature() + "消耗时间" + (end - start) + "ms");

        return result;

    }

}

 AOP核心概念

切点

PointCut,切点,也称之为切入点。

切点的任务就是提供一组规则,告诉程序哪些方法需要被用来进行功能的增强。

例如在上述程序中的下述代码,就是一个切点:

定义切点

在上述AOP的小demo中,切点是和通知在一起进行书写的。如果在一个程序中,有很多的AOP,这些AOP作用的方法都相同,我们就可以把切点单独定义出来,这样可以减少代码的冗余。

@Pointcut("execution(* controller.*.*())")
public void pointCut() {
    
}

 对于定义好的切点来说,如果通知也是在本类中的话,那么可以直接写成 @Before("切点()"),例如上述切点,通知可以写成@Before("pointCut()")。如果定义的切点并不是本类的话,就需要写成@Before("类的全限定名称 + 切点()")。

如果一个方法有多个被切面时,默认是按照类名来进行排序,不过有@Order注解可以用来进行排序。@Order中的数字越小,前置通知的时间越早,后置通知的时间越迟。

 切点表达式

1. execution表达式:根据方法的签名来匹配

execution(<访问修饰符> <返回类型> <包名.类名.方法.方法参数> <异常>)

 可以看到。访问修饰符和异常是可以不写的。

2. @annotation表达式:根据自定义注解的方式进行匹配。

execution表达式的方式适合匹配一些有规则的方法,如果我们需要进行匹配规则的点在某几个包下的某几个类的某几个方法,那么这种规则就不再使用。相对来说,这种使用自定义注解的方式更为适用,也就是对于每个需要匹配的方法,我们在其上面加一个注解即可。

实现步骤:①自定义注解②使用@annotation表达式描述切点③在连接点的方法上添加自定义注解。

自定义注解的实现:

/**
 * 自定义注解
 */

@Target(ElementType.METHOD) // 元注解,表示注解作用的方法
@Retention(RetentionPolicy.RUNTIME) // 元注解,表示注解的声明周期
public @interface MyAnnotation {

}

使用@annotation表达式描述切点,同时为了测试@Order注解的作用,定义了顺序。

@Component // 表示要注入IoC容器中
@Slf4j // 表示打印日志
@Aspect // 表示这是一个切面类
@Order(1) // 定义顺序
public class MyAspect {

    /**
     * 定义切点
     */
    @Pointcut("@annotation(com.example.demo.aop.MyAnnotation)")
    public void pointcut() {

    }

    /**
     * 定义前置通知
     */
    @Before("pointcut()")
    public void before() {
        log.info("执行前置通知");
    }

    /**
     * 定义后置通知
     */
    @After("pointcut()")
    public void after() {
        log.info("执行后置通知");
    }

    /**
     * 定义环绕通知
     * @param joinPoint 表示目标方法
     * @return
     * @throws Throwable
     */
    @Around("pointcut()")
    public Object around(ProceedingJoinPoint joinPoint) throws Throwable {
        log.info("执行环绕前通知");
        Object result = joinPoint.proceed();
        log.info("执行环绕后通知");
        return result;
    }

}

使用@annotation表达式描述切点,同时为了测试@Order注解的作用,定义了顺序。

@Component // 表示要注入IoC容器中
@Slf4j // 表示打印日志
@Aspect // 表示这是一个切面类
@Order(2) // 定义顺序
public class HisAspect {

    /**
     * 定义前置通知
     * 由于使用的是其他类定义的切点,因此要用全限定名称 + 类型
     */
    @Before("com.example.demo.aop.MyAspect.pointcut()")
    public void before() {
        log.info("执行前置通知");
    }

    /**
     * 定义后置通知
     * 由于使用的是其他类定义的切点,因此要用全限定名称 + 类型
     */
    @After("com.example.demo.aop.MyAspect.pointcut()")
    public void after() {
        log.info("执行后置通知");
    }

    /**
     * 定义环绕通知
     * 由于使用的是其他类定义的切点,因此要用全限定名称 + 类型
     * @param joinPoint 表示目标方法
     * @return
     * @throws Throwable
     */
    @Around("com.example.demo.aop.MyAspect.pointcut()")
    public Object around(ProceedingJoinPoint joinPoint) throws Throwable {
        log.info("执行环绕前通知");
        Object result = joinPoint.proceed();
        log.info("执行环绕后通知");
        return result;
    }

}

启动类,用来测试结果 

@Controller
@Slf4j
public class TestController {

    @RequestMapping("/hi")
    @MyAnnotation
    public void hi() {
        log.info("目标方法");
    }

}

连接点

满足切点规则的点,就是连接点。

例如在上述切面中,三层架构中controller层的所有方法都会被切面切入。

通知

通知就是在AOP中具体执行的业务逻辑,然后程序猿将其抽象成一个方法。

在上述的AOP中,这个方法就是一个通知。

public Object timeCost(ProceedingJoinPoint joinPoint) throws Throwable {

        long start = System.currentTimeMillis();
        Object result = joinPoint.proceed(); // 执行目标方法
        long end = System.currentTimeMillis();

        log.info(joinPoint.getSignature() + "消耗时间" + (end - start) + "ms");

        return result;

    }

前置通知(@Before):表示通知在目标方法执行前被执行。

后置通知(@After):表示通知在目标方法执行后被执行。

环绕通知(@Around):表示通知再目标方法执行前后被执行。

返回后通知(@AfterReturning):表示通知在目标方法执行后被执行,如果有异常就不会执行。

异常后通知(@AfterThrowing): 表示通知在目标方法异常后被执行。

如果一个方法有多个通知,那么通知的执行顺序就是:

正常情况下:环绕前通知 → 前置通知 → 返回后通知 → 后置通知 → 环绕后通知;

异常情况下:环绕前通知 → 前置通知 → 异常后通知 → 后置通知。

切面

切面 = 切点 + 通知。

通过切面我们可以知道,AOP在哪些方法中执行什么样的业务逻辑。

代理模式

SpringAOP是基于动态代理实现的。

代理的目的,其实就是对目标方法进行功能增强。

定义

代理,就是为目标对象提供一种代理以控制对这个对象的访问。它的作用就是通过一个代理类,让我们在调用目标方法的时候,不是直接对目标方法进行调用,而是通过代理类间接调用。

在某些情况下,一个对象不适合或者不能直接引用另一个对象,而代理对象可以在客户端和目标对象之间起到中介的作用。

使用代理前:

使用代理后:

 主要角色

业务接口类(Subject):表示目标对象要做什么事,可以是抽象类或接口。

业务实现类(RealSubject):表示目标对象具体做的业务逻辑,也就是被代理对象要做的业务。

代理类(Proxy):当来调用目标对象时,通过访问代理类来实现。

静态代理

由程序员创建代理类或使用特定工具自动生成源代码再对其进行编译,程序运行前代理类的.class文件就以及存在,并且已经确定好要代理的对象。

以房东和中介为例。房东属于目标对象,中介属于代理对象。房东有租房子和卖房子两项业务,但是对于一个中介来说,他在开启业务之前,只跟房东确定好了租房子的业务,而卖房子的并没有确定好。因此,当有客户时,客户想买,但是中介代理的只有租,不能卖。这就类似于静态代理,当代理对象在程序运行前,就确定好要代理什么内容,程序开始后,代理对象只能代理程序运行前确定好的,对于没有确定的,它就不能代理。

HouseSubject接口
// 业务接口类

public interface HouseSubject {

    /**
     * 出租房屋
     */
    void rentHouse();

    /**
     * 出售房子
     */
    void saleHouse();

}

 RealHouseSubject类

// 目标对象、被代理对象

public class RealHouseSubject implements HouseSubject{

    /**
     * 出租房屋
     */
    @Override
    public void rentHouse() {
        System.out.println("我是房东,我要出租房子");
    }

    /**
     * 出售房子
     */
    @Override
    public void saleHouse() {
        System.out.println("我是房东,我要出售房子");
    }

}

 HouseProxy类

// 代理类

public class HouseProxy implements HouseSubject{

    // 目标对象
    private HouseSubject houseSubject;

    public HouseProxy(HouseSubject houseSubject) {
        this.houseSubject = houseSubject;
    }

    @Override
    public void rentHouse() {
        System.out.println("开始进行代理");
        houseSubject.rentHouse();
        System.out.println("结束代理");
    }

}

 客户端来访,使用代理对象

public class Main {

    public static void main(String[] args) {

        HouseSubject subject = new RealHouseSubject();
        HouseProxy houseProxy = new HouseProxy(subject);
        houseProxy.rentHouse();
        
    }

}

在上述代码中不难看出, 代理类在程序运行前就确定好代理什么目标对象,代理目标对象的什么业务。当程序开始后,客户端来进行交易,代理类就只能代理确定好的目标对象的业务,其他一概不能操作。

动态代理

相比于静态代理来说,动态代理较为灵活。

在动态代理中,不需要为每一个目标对象都单独创建一个代理对象,而是把这个创建代理对象的任务放到程序运行时有JVM来实现,也就是说动态代理在程序运行时,根据需要动态创建。

仍然以房东和中介举例。房东属于目标对象,中介属于代理对象。房东只需要把要干啥给出,而此时中介并不需要是哪个房东,要干哪个事。当客户来了之后,诉说自己的想要什么,中介再根据具体要干的活去找哪个房东有这个东西。假如说,客户要买房,那中介就找哪个房东要卖房;客户要长租,中介就找哪个房子会长租。这就类似于动态代理,代理对象并不知道会来什么业务,也不知道目标对象有什么业务,只要客户来了,根据需要去创建一个具体的代理,然后执行业务。

动态代理分为:JDK动态代理和CGLIB动态代理。

JDK动态代理

1. 定义一个接口以及实现类,也就是静态代理中的HouseSubject、RealHouseSubject。

2. 定义一个代理类,实现InvocationHandler接口,并重写invoke方法,在invoke方法中我们会调用目标方法并实现一些业务逻辑。

3. 通过Proxy.newProxyInstance(ClassLoader loader, Class<?>[ ] interfaces, InvocationHandler h)方法创建代理对象。

HouseSubject接口
// 业务接口类

public interface HouseSubject {

    /**
     * 出租房屋
     */
    void rentHouse();

    /**
     * 出售房子
     */
    void saleHouse();

}
RealHouseSubject类
// 目标对象、被代理对象

public class RealHouseSubject implements HouseSubject{

    /**
     * 出租房屋
     */
    @Override
    public void rentHouse() {
        System.out.println("我是房东,我要出租房子");
    }

    /**
     * 出售房子
     */
    @Override
    public void saleHouse() {
        System.out.println("我是房东,我要出售房子");
    }

}
JDKInvocation类,即Proxy类
/**
 * JDK动态代理的实现
 */

public class JDKInvocation implements InvocationHandler {

    // 目标对象
    private Object target;

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

    /**
     *
     * @param proxy 代理对象
     *
     * @param method 代理对象需要实现的方法,即其中需要重写的方法
     *
     * @param args method所对应方法的参数
     *
     * @return
     *
     * @throws Throwable
     */
    @Override
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
        System.out.println("开始代理");
        Object result = method.invoke(target, args); // 通过反射来实现的
        System.out.println("结束代理");
        return result;
    }

}
客户端来访,创建代理对象
public class Main {

    public static void main(String[] args) {
        HouseSubject subject = new RealHouseSubject();
        HouseSubject proxy = (HouseSubject) Proxy.newProxyInstance(subject.getClass().getClassLoader(),
                // 类加载器,用于加载代理对象
                new Class[]{HouseSubject.class},
                // 被代理类实现的一些接口,同时也决定了JDK只能实现接口
                new JDKInvocation(subject));
                // 实现了InvocationHandler接口的对象
        proxy.saleHouse();

    }

}

 CGLIB动态代理

1. 定义一个接口以及实现类,也就是静态代理中的HouseSubject、RealHouseSubject。

2. 自定义MethodInterceptor并重写intercept方法,interceptor方法就相当于中介干活,即代理对象,用来增强目标方法,和JDK动态代理中的invoke方法类似。

3. 当有客户端来访时,通过Enhancer类的create()创建真正代理对象。

引入依赖
<dependency>
   <groupId>cglib</groupId>
   <artifactId>cglib</artifactId>
   <version>3.3.0</version>
</dependency>
HouseSubject接口
// 业务接口类

public interface HouseSubject {

    /**
     * 出租房屋
     */
    void rentHouse();

    /**
     * 出售房子
     */
    void saleHouse();

}
RealHouseSubject类
// 目标对象、被代理对象

public class RealHouseSubject implements HouseSubject{

    /**
     * 出租房屋
     */
    @Override
    public void rentHouse() {
        System.out.println("我是房东,我要出租房子");
    }

    /**
     * 出售房子
     */
    @Override
    public void saleHouse() {
        System.out.println("我是房东,我要出售房子");
    }

}
CGLibInterceptor类,即Proxy类
/**
 * CGLib动态代理的实现
 * 类似于JDK的invoke方法
 */

public class CGLibInterceptor implements MethodInterceptor {

    // 目标对象
    private Object target;

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

    @Override
    public Object intercept(Object o, Method method, Object[] objects, MethodProxy methodProxy) throws Throwable {
        System.out.println("开始代理");
        // 调用目标对象
        Object result = method.invoke(target, objects);
        System.out.println("结束代理");
        return result;
    }

}
客户端来访,创建代理对象
public class Main {

    public static void main(String[] args) {
        HouseSubject subject = new RealHouseSubject();
        HouseSubject proxy = (HouseSubject) Enhancer.create(subject.getClass(),
                    // 被代理的类或接口的类型
                        new CGLibInterceptor(subject));
                    // 自定义方法拦截器MethodInterceptor
                    proxy.saleHouse();
    }

}

对于JDK动态代理和CGLIB动态代理两者,JDK动态代理只能代理接口,而CGLIB动态代理可以代理类和接口。 

不管是JDK动态代理还是CGLIB动态代理,都可以明显的发现,对于代理对象来说,都不知道要代理的内容是什么,当运行时,来了什么内容,要对哪个目标对象做业务,那么代理对象就代理哪个目标对象。

静态代理和动态代理进行对比之后,就可以发现动态代理的高效性,不用针对每一个目标对象都去构建一个代理对象。

源码分析

SpringAOP是基于动态代理实现的,而动态代理又有JDK和CGLIB两种方法进行实现。SpringAOP则是两种方法结合使用。

如果是代理接口,JDK和CGLIB都可以使用;如果是代理类,那么只有JDK可以使用。在SpringBoot2.x之后,默认使用CGLIB代理。当然,如果在配置文件中设置spring.aop.proxy-target-class=false,那就变成了JDK代理,但是如果目标对象是类的话,那么还是CGLIB动态代理。

 对于AOP的介绍就到这里了,AOP是Spring两大主要内容之一了(另一个是IoC),因此非常重要,不论是日常工作中,还是八股文中,都是一个比较重要的内容。在文章中,简单介绍了一下AOP的内容,及背后的设计模式,还有SpringAOP的源码,接下来会对Spring实现的一些功能进行简单介绍,例如统一格式返回,拦截器,统一异常以及权限管理中后端利用AOP思想来实现拦截URL的过程。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:/a/520866.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

设置你的第一个React应用

目录 一、React入门 1.1 你好React 1.2 创建React 1.3 应用结构 二、总结 2.1 定义组件 2.2 组件源码 三、组件详解 注意事项 3.1 组件三部曲 3.2 组件通信 —— props 3.3 对象数组迭代 —— map() 3.4 事件处理 3.5 钩子函数 —— useState() 初次学习最终效果…

Cortex-M7 内存映射模型

1 前言 如图1所示&#xff0c; Cortex-M7最大支持4GB的内存寻址&#xff0c;并对内存映射(memory map)做了初步的规定&#xff0c;将整个内存空间划分为了多个内存区域(region)。每个内存区域有着既定的内存类型(memory type)和内存属性(memory attribute)&#xff0c;这两者决…

AI - ComfyUI过程图(3)

ComfyUI 比 Stable Diffusion WebUI更灵活&#xff0c;而且可以看到处理过程&#xff0c;能增加节点进行后续处理&#xff0c;因而更强大。 看看下面一张图的变化&#xff0c;一开始惨不忍睹。 使用 Ultimate SD Upscale 提升分辨率 超精后脸部有改善&#xff1a; 脸部比较…

递归实现指数型枚举(acwing)

题目描述&#xff1a; 从 1∼n 这 n 个整数中随机选取任意多个&#xff0c;输出所有可能的选择方案。 输入格式&#xff1a; 输入一个整数 n。 输出格式&#xff1a; 每行输出一种方案。 同一行内的数必须升序排列&#xff0c;相邻两个数用恰好 1 个空格隔开。 对于没有…

一周年纪念

文章目录 机缘&#xff1a;命运之门收获---知识之心日常---灵魂之窗成就 — 自我之光憧憬 — 未来之路 机缘&#xff1a;命运之门 “人生是由一连串的选择组成&#xff0c;而真正的成长&#xff0c;往往始于最具挑战性的决定。” —— 这句话恰如其分地概括了我选择跨考计算机的…

自动驾驶执行层 - 线控底盘基础原理(非常详细)

自动驾驶执行层 - 线控底盘基础原理(非常详细) 附赠自动驾驶学习资料和量产经验&#xff1a;链接 1. 前言 1.1 线控的对象 在自动驾驶行业所谓的“感知-定位-决策-执行”的过程中&#xff0c;在末端的执行层&#xff0c;车辆需要自主执行决策层所给出的指令&#xff0c;具体…

2024最全ChatGPT支持GPTs使用教程+Prompt应用预设词教程

使用指南 直接复制使用 可以前往已经添加好Prompt预设的AI系统测试使用&#xff08;可自定义添加使用&#xff09; https://ai.sparkaigf.com 现已支持GPTs 雅思写作考官 我希望你假定自己是雅思写作考官&#xff0c;根据雅思评判标准&#xff0c;按我给你的雅思考题和对应…

【多模态融合】MetaBEV 解决传感器故障 3D检测、BEV分割任务

前言 本文介绍多模态融合中&#xff0c;如何解决传感器故障问题&#xff1b;基于激光雷达和相机&#xff0c;融合为BEV特征&#xff0c;实现3D检测和BEV分割&#xff0c;提高系统容错性和稳定性。 会讲解论文整体思路、模型框架、论文核心点、损失函数、实验与测试效果等。 …

Python 基于列表实现的通讯录管理系统(有完整源码)

目录 通讯录管理系统 PersonInformation类 ContactList类 menu函数 main函数 程序的运行流程 完整代码 运行示例 通讯录管理系统 这是一个基于文本的界面程序&#xff0c;用户可以通过命令行与之交互&#xff0c;它使用了CSV文件来存储和读取联系人信息&#xff0c;这…

浅谈Redis和一些指令

浅浅谈一谈Redis的客户端 Redis客户端 Redis也是一个客户端/服务端结构的程序。 MySQL也是一个客户端/服务端结构的程序。 Redis的客户端也有多种形态 1.自带命令行客户端 redis-cli 2.图形化界面的客户端&#xff08;桌面程序&#xff0c;web程序&#xff09; 像这样的图形…

3d代理模型怎么转换成标准模型---模大狮模型网

在当今的虚拟世界中&#xff0c;3D建模技术被广泛运用于游戏开发、电影制作、工业设计等领域。在3D建模过程中&#xff0c;有时会遇到需要将代理模型转换成标准模型的情况。模大狮将从理论和实践两方面&#xff0c;介绍如何将3D代理模型转换成标准模型&#xff0c;以帮助读者更…

推荐一款免费开源引擎:批量识别PDF及图片表格及文字(可本地化部署)

在数字化时代&#xff0c;信息的快速处理和高效管理成为企业和个人的重要需求。表格文字识别技术作为一项关键的技术&#xff0c;能够将纸质或图片中的表格数据快速转换为结构化的电子数据&#xff0c;极大地提高了数据处理的效率和准确性。本文将对思通数科的表格文字识别技术…

MySQL复制拓扑2

文章目录 主要内容一.配置基本复制结构1.分别在三台主机上停止mysqld服务&#xff0c;并对状态进行确认&#xff1a;代码如下&#xff08;示例&#xff09;: 2.对三个MySQL服务器的配置文件分别进行编辑&#xff0c;在[mysqld] 选项组中添加以下红色条目&#xff1a;3.在数据目…

淘宝优惠券领取软件叫什么?

草柴返利APP是一款淘宝优惠券领取软件。用户可以通过草柴淘宝优惠券领取软件轻松查找领取淘宝大额隐藏优惠券&#xff0c;领取成功后再购物可享受券后价优惠。同时&#xff0c;通过草柴APP领券购买成功&#xff0c;确认收货后再回到草柴APP提取购物返利&#xff0c;享受双重省钱…

【自用笔记】【大数据】

1 mapreduce &#xff08;1&#xff09;Map任务的数量&#xff1a;由输入数据的大小决定的&#xff0c;如文件数量和大小、HDFS块大小以及FileInputFormat的设置等。每个MapSlot可以运行一个Map任务 &#xff08;2&#xff09;Reduce任务的数量&#xff08;分区数&#xff09;&…

DHCP-PXE

Dynamic Host Configuration Protocol 动态主机配置协议 1.Selinux 调试为Permission 防火墙配置 搭建DHCP的主机必须有一个静态地址&#xff0c;提前配置好 安装DHCP软件 服务名为dhcpd DHCP地址分配四次会话&#xff0c; DISCOVERY发现 OFFER 提供 REQUEST 回应 A…

vue使用iview导航栏Menu activeName不生效

activeName不生效 一、问题一、解决方案&#xff0c; 一、问题 根据ivew官网的提示&#xff0c;设置了active-name和open-names以后&#xff0c;发现不管是设置静态是数据还是设置动态的数据&#xff0c;都不生效 一、解决方案&#xff0c; 在设置动态名称的时候&#xff0c…

修复打印机显示为脱机的几种方法,总有一种适合你

打印机显示为脱机有几个可能的原因。在大多数情况下,只要对症下药,问题就很容易解决。下面解释了打印机脱机的原因,以及如何使其联机并再次打印。 “打印机脱机”是什么意思 当打印机显示为脱机时,这意味着它当前未通过电缆或Wi-Fi网络连接到计算机。它无法与你的计算机通…

Feign(黑马程序员)

Feign是代替RestTemplate进行http请求的。 定义和使用 Feign 客户端&#xff1a; 1 引入依赖&#xff1a; <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-openfeign</artifactId> </depe…

Autosar BswM 模式管理

EcuMs管理ECU上下电状态,BswM管理模式,协同工作。当使用EcuM - Fixed时,它将向BswM指示当前ECU状态 有了BswM,从图可以更加直观看出,BswM管理各个模块,每个模块独立,降低耦合。 BswM 的主要功能包括: 模式管理:BswM 可以管理和控制 ECU 的不同模式,例如正常模式、备…