【Spring的AOP】Spring的简介、案例与工作流程

news2024/10/5 16:31:04

文章目录

  • 1. 什么是AOP
  • 2. AOP的核心概念
  • 3. AOP的入门案例
    • 原始代码
    • 思路分析
    • 第一步:导入坐标
    • 第二步:制作连接点(原始操作,Dao接口与实现类)
    • 第三步:制作共性功能(通知类与通知)
    • 第四步:定义切入点
    • 第五步:绑定切入点与通知关系(切面)
    • 第六步:让Spring“看到”这个切面
    • 第七步:在Spring配置类中加上注解
    • 运行主方法
  • 4. AOP工作流程
    • 动态代理
      • 为什么需要代理?代理长什么样?Java通过什么来保证代理的样子?
      • 动态代理3问(根据以上内容给出答案)
        • 1. Java提供了什么API帮我们创建代理?
        • 2. newProxyInstance方法在创建时,需要接几个参数?每个参数的含义是什么?
        • 3. 通过invokehandler的invoke方法指定代理干的事时,这个invoke会被谁调用?需要接哪几个参数?
      • 动态代理实例
    • AOP工作流程
  • 5. AOP切入点表达式
    • 语法格式
    • 通配符
    • 书写技巧
  • 6. AOP通知类型
    • 前置通知和后置通知
    • 环绕通知
    • 返回后通知
    • 抛出异常后通知
  • 7. AOP案例:测量业务接口执行效率
    • 案例代码
      • 整体架构
      • pom.xml
      • config下的包:SpringConfig、JdbcConfig、MyBatisConfig
      • 数据源
      • 实体类
      • 数据层
      • 业务层
      • 测试类
    • 进行切面编程
  • 8. AOP通知获取数据
    • 获取参数
    • 获取返回值
    • 获取异常
    • 9. AOP总结

1. 什么是AOP

Spring的两大理念:IOC(Inversion of Control)和AOP
AOP是Aspect Oriented Programming,面向切面编程,是一种编程范式,指导开发者如何组织程序结构
(PS:原本的OOP,Object Oriented Programming,面向对象编程,指导我们如何根据对象属性来进行类的开发)
作用: 在不惊动原始设计的基础上为其进行功能增强
举个例子:
在一个类中有四个方法:
在这里插入图片描述
通过测试类分别调用这个四个方法,save会打印出10000次book dao save,若updatedelete在代码里没有显示定义这些内容的情况下也分别打印了10000次book dao updatebook dao delete,则说明在不惊动原始设计的基础上为其进行功能增强(不对原始方法做修改的情况下加上了打印10000次的功能)
这也符合Spring的理念:无入侵式/无侵入式编程

2. AOP的核心概念

在这里插入图片描述
首先将我们想要实现的共通的功能从代码中抽取出来,写成一个通知类
通知类中定义一个方法,称为通知,就是把共同的功能抽出来写在其中
不是所有的方法都要执行这个通知,所以要把执行对应通知的方法找出来,定义成切入点(匹配某些方法)
有了切入点通知,我们将他们绑定起来形成切面切面就是在哪些切入点上执行哪些通知
在这里插入图片描述

3. AOP的入门案例

案例目标:在接口执行前输出当前系统的时间

原始代码

pom.xml

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>
    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>3.2.0</version>
        <relativePath/> <!-- lookup parent from repository -->
    </parent>
    <groupId>com.example</groupId>
    <artifactId>project3</artifactId>
    <version>0.0.1-SNAPSHOT</version>
    <name>project3</name>
    <description>project3</description>
    <properties>
        <java.version>17</java.version>
    </properties>
    <dependencies>
        <dependency>
            <groupId>org.springframework</groupId>
            <artifactId>spring-context</artifactId>
            <version>6.0.3</version>
        </dependency>
        <dependency>
            <groupId>junit</groupId>
            <artifactId>junit</artifactId>
            <version>4.13.2</version>
            <scope>test</scope>
        </dependency>
        <dependency>
            <groupId>org.springframework</groupId>
            <artifactId>spring-test</artifactId>
            <version>5.3.25</version>
        </dependency>
    </dependencies>

</project>

代码结构:
在这里插入图片描述
SpringConfig.java

package com.example.project3.config;

import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;

@Configuration
@ComponentScan("com.example.project3")
public class SpringConfig {

}

BookDao.java

package com.example.project3.dao;

import org.springframework.stereotype.Repository;

public interface BookDao {
    void save();
    void update();
}

BookDaoImpl.java

package com.example.project3.dao.impl;

import com.example.project3.dao.BookDao;
import org.springframework.stereotype.Repository;

@Repository
public class BookDaoImpl implements BookDao {
    @Override
    public void save() {
        System.out.println(System.currentTimeMillis());
        System.out.println("book dao save...");
    }

    @Override
    public void update() {
        System.out.println("book dao update...");
    }
}

Project3Application.java(主方法)

package com.example.project3;

import com.example.project3.config.SpringConfig;
import com.example.project3.dao.BookDao;
import org.springframework.context.ApplicationContext;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;

public class Project3Application {

    public static void main(String[] args) {
        ApplicationContext ctx = new AnnotationConfigApplicationContext(SpringConfig.class);
        BookDao bookDao = ctx.getBean(BookDao.class);
        bookDao.update();
    }

}

此时执行主方法:
在这里插入图片描述
若将bookDao.update()换成bookDao.save()
在这里插入图片描述

思路分析

  1. 导入坐标
  2. 制作连接点(原始操作,Dao接口与实现类)
  3. 制作共性功能(通知类通知
  4. 定义切入点
  5. 绑定切入点通知关系(切面

第一步:导入坐标

实际上,在导入spring-context的时候,相关的aop包已经导入进来了:
在这里插入图片描述

但还需要在pom.xml中进一步导入:

<dependency>
    <groupId>org.aspectj</groupId>
    <artifactId>aspectjweaver</artifactId>
    <version>1.9.5</version>
</dependency>

第二步:制作连接点(原始操作,Dao接口与实现类)

这个其实已经做好了,就是我们的Dao接口及实现类

第三步:制作共性功能(通知类与通知)

在项目下新建一个aop包,在下面新建一个MyAdvice.java,这个MyAdvice就是通知类,里面写抽取出来的共性方法method就是通知

package com.example.project3.aop;

import org.aspectj.lang.annotation.Pointcut;

public class MyAdvice {

    public void method(){
        System.out.println(System.currentTimeMillis());
    }
}

第四步:定义切入点

通过@Pointcut定义切入点,里边的内容意思为:执行()内的方法时为切入点
切入点依托一个不具有实际意义的方法进行,即无参数、无返回值,方法体无实际逻辑

package com.example.project3.aop;

import org.aspectj.lang.annotation.Pointcut;

public class MyAdvice {

    @Pointcut("execution(void com.example.project3.dao.BookDao.update())")
    private void pt(){}

    public void method(){
        System.out.println(System.currentTimeMillis());
    }
}

第五步:绑定切入点与通知关系(切面)

通过@Before绑定关系,意为在pt()开始之前执行该通知(方法)

package com.example.project3.aop;

import org.aspectj.lang.annotation.Before;
import org.aspectj.lang.annotation.Pointcut;

public class MyAdvice {

    @Pointcut("execution(void com.example.project3.dao.BookDao.update())")
    private void pt(){}

    @Before("pt()")
    public void method(){
        System.out.println(System.currentTimeMillis());
    }
}

第六步:让Spring“看到”这个切面

在该类上加上两个注解,一个是@Component,将其作为一个bean,并告诉Spring,将其作为切面处理,所以加上@Aspect

package com.example.project3.aop;

import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import org.aspectj.lang.annotation.Pointcut;
import org.springframework.stereotype.Component;

@Component
@Aspect
public class MyAdvice {

    @Pointcut("execution(void com.example.project3.dao.BookDao.update())")
    private void pt(){}

    @Before("pt()")
    public void method(){
        System.out.println(System.currentTimeMillis());
    }
}

第七步:在Spring配置类中加上注解

在配置类中加上注解@EnableAspectJAutoProxy,此注解主要用来导入 Spring 切面功能类

package com.example.project3.config;

import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.EnableAspectJAutoProxy;

@Configuration
@ComponentScan("com.example.project3")
@EnableAspectJAutoProxy
public class SpringConfig {

}

运行主方法

package com.example.project3;

import com.example.project3.config.SpringConfig;
import com.example.project3.dao.BookDao;
import org.springframework.context.ApplicationContext;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;

public class Project3Application {

    public static void main(String[] args) {
        ApplicationContext ctx = new AnnotationConfigApplicationContext(SpringConfig.class);
        BookDao bookDao = ctx.getBean(BookDao.class);
        bookDao.update();
    }

}

结果如下:
在这里插入图片描述

4. AOP工作流程

动态代理

视频:【黑马磊哥】Java动态代理深入剖析,真正搞懂Java核心设计模式:代理设计模式
动态代理(Spring、MyBatis、SpringMVC、Spring Boot)几乎是所有框架的核心原理之一

为什么需要代理?代理长什么样?Java通过什么来保证代理的样子?

举一个最普通的例子, 有一位明星,她能够唱歌和跳舞(具备sing()dance()方法):

class 明星{
	void 唱歌(){
		准备话筒、收钱
		开始唱歌
	}
	void 跳舞(){
		准备场地、收钱
		开始跳舞
	}
}

但是作为一个大明星,她不会自己去做准备话筒、收钱, 准备场地、收钱这件事,所以请了一个中介公司来进行代理(对象如果嫌身上干的事太多的话,可以通过代理来转移职责
中介公司会派一位代理人,来管理准备场地、话筒, 准备场地、收钱这些事。作为一个代理,它也同样拥有sing()dance()方法,对象有什么方法想被代理,代理就一定要有对应的方法

class 代理{
	void 唱歌(){
		准备话筒、收钱
		找明星唱歌
	}
	void 跳舞(){
		准备场地、收钱
		找明星跳舞
	}
}

同时这个 “中介” 是通过 “接口” 了解具体需要代理的方法的
明星:

package com.example.demo.agent;

public class star implements Istar{

    String starName;

    public star(String starName) {
        this.starName = starName;
    }

    @Override
    public String sing(String name) {
        System.out.println(this.starName + "正在唱" + name);
        return "谢谢!谢谢!谢谢!";
    }

    @Override
    public void dance() {
        System.out.println(this.starName + "正在跳舞");
    }
}

接口:

package com.example.demo.agent;

public interface Istar {
    String sing(String name);
    void dance();
}

代理:
首先代理类返回的就是接口的一个对象,其中参数为Star类(代理为明星做代理,需要传入明星类,以获知明星能够做的事情)
通过Proxy.newProxyInstance()获得一个新的代理实例,返回的是一个(Object)类型的对象,将其强转成IStar类型,在该方法中需要传入3个参数:

  1. 类加载器,这个一般默认使用当前代理类的类加载器:ProxyUtil.class.getClassLoader()
  2. 了解明星能够实现哪一些方法,也就是要知道代理类长什么样子,这里是给了一个数组接口:new Class[]{IStar.class}
  3. InvocationHandler类,定义这个匿名类的方法类写清楚代理对象需要做什么事。其中proxy代表的是代理本身,把代理本身作为一个对象传给invoke方法,method表示当前star调用了哪个方法,args表示调用方法时传入的参数。
package com.example.demo.agent;

import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;

public class ProxyUtil {

    public static IStar createProxy(Star star){
        /*
        * newProxyInstance(ClassLoader loader,
        * Class<?>[] interfaces,
        * InvocationHandler h)
        * 参数1:用于指定一个类加载器
        * 参数2:指定生成的代理长什么样子,也就是有哪些方法
        * 参数3:用来指定生成的代理对象要干什么事
        * */
//        代理干什么事,由invoke决定
//        Star star = new Star("xxx")
//        IStar starProxy = ProxyUtil.createProxy(s)
//        starProxy.sing("xxx"), starProxy.dance()
        IStar iStar = (IStar) Proxy.newProxyInstance(ProxyUtil.class.getClassLoader(), new Class[]{IStar.class},
                new InvocationHandler() {
                    @Override // 回调方法
                    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
//                        代理对象要做的事,在这里写代码
                        if (method.getName().equals("sing")){
                            System.out.println("准备话筒,收钱");
                        } else if (method.getName().equals("dance")) {
                            System.out.println("准备场地,收钱");
                        }
                        return method.invoke(star, args);
                    }
                });
        return iStar;
    }
    
}

测试:

package com.example.demo.agent;

public class Test {
    public static void main(String[] args) {
        Star star = new Star("大明星");
        IStar starProxy = ProxyUtil.createProxy(star);

        String rs = starProxy.sing("一首歌");
        System.out.println(rs);

        starProxy.dance();
    }
}

结果:
在这里插入图片描述
方法调用顺序:
在这里插入图片描述

动态代理3问(根据以上内容给出答案)

1. Java提供了什么API帮我们创建代理?

创建代理的API:

Proxy.newProxyInstance(xxxxx)

使用该API并使用强转实现类型转换

2. newProxyInstance方法在创建时,需要接几个参数?每个参数的含义是什么?

3个参数
第一个参数:类加载器,默认使用当前代理类的类加载器:NowClass.class.geetClassLoader()
第二个参数:给出代理类的“模样”,即代理类应该参照的接口,这是一个数组,可以有多个模样:new Class[]{xxx.class}
第三个参数:通过InvocationHandler类写清楚代理对象需要做的事

3. 通过invokehandler的invoke方法指定代理干的事时,这个invoke会被谁调用?需要接哪几个参数?

执行方法之前会被调用,有参数proxy表示代理本身,method表示即将调用的方法,args表示调用方法时传入的参数

动态代理实例

在这里插入图片描述
代码如下:
结构目录如下:
在这里插入图片描述
其中,UserService.java

package com.example.project3.aoppractice;
// 用户业务接口
public interface UserService {
//    登录功能
    void login(String loginName, String password) throws Exception;
//    删除用户
    void deleteUsers() throws Exception;
//    查询用户,返回数组形式
    String[] selectUsers() throws Exception;
}

UserServiceImpl.java

package com.example.project3.aoppractice;

public class UserServiceImpl implements UserService{
    @Override
    public void login(String loginName, String password) throws Exception {
        long starTime = System.currentTimeMillis();

        if("admin".equals(loginName) && "123456".equals(password)){
            System.out.println("登陆成功,欢迎光临");
        } else {
            System.out.println("登录失败,用户名/密码错误");
        }
        Thread.sleep(1000);

        long endTime = System.currentTimeMillis();
        System.out.println("login方法执行耗时:" + (endTime - starTime)/ 1000.0 + "s");
    }

    @Override
    public void deleteUsers() throws Exception {
        long startTime = System.currentTimeMillis();

        System.out.println("删除了1万个用户");
        Thread.sleep(1000);

        long endTime = System.currentTimeMillis();
        System.out.println("deleteUsers方法耗时:" + (endTime - startTime)/1000.0 + "s");
    }

    @Override
    public String[] selectUsers() throws Exception {
        long startTime = System.currentTimeMillis();

        System.out.println("查询了3个用户");
        String[] names = {"用户1", "用户2", "用户3"};
        Thread.sleep(1000);

        long endTime = System.currentTimeMillis();
        System.out.println("deleteUsers方法耗时:" + (endTime - startTime)/1000.0 + "s");
        return names;
    }
}

这样的程序中存在的问题是:在方法中存在了大量重复的求程序开始时间和程序结束时间的操作,为此,我们可以通过动态代理来简化这些方法

  1. 定义动态代理类
package com.example.project3.aoppractice;

import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;

public class ProxyUtil{
    public static UserService createProxy(UserServiceImpl impl){
        UserService userService = (UserService) Proxy.newProxyInstance(ProxyUtil.class.getClassLoader(), new Class[]{UserService.class}, new InvocationHandler() {
            @Override
            public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {

                if (method.getName().equals("login") || method.getName().equals("deleteUsers") || method.getName().equals("selectUsers")){
                    long startTime = System.currentTimeMillis();
                    Object rs = method.invoke(impl, args);
                    long endTime = System.currentTimeMillis();
                    System.out.println(method.getName() + "方法执行耗时:" + (endTime - startTime) / 1000.0 + "s");
                    return rs;
                } else {
                    Object rs = method.invoke(impl, args);
                    return rs;
                }
            }
        });
        return userService;
    }
}

这里就是将公共的获取时间的方法从中抽出来,然后再invoke中执行该方法,并返回方法的执行结果

  1. 测试
package com.example.project3.aoppractice;

public class Test {
    public static void main(String[] args) throws Exception {
        UserService userServiceProxy = ProxyUtil.createProxy(new UserServiceImpl());

        userServiceProxy.login("admin", "12345");
        System.out.println("--------------------------------");
        userServiceProxy.deleteUsers();
        System.out.println("--------------------------------");
        String[] names = userServiceProxy.selectUsers();
        System.out.println("查询到的用户是:" + Arrays.toString(names));
        System.out.println("--------------------------------");
    }
}
  1. 运行结果
    在这里插入图片描述

AOP工作流程

  1. Spring容器启动
  2. 读取所有切面配置中的切入点,如下图,因为只绑定了通知和切入点pt(),所以这里只读取切入点pt()而不读取ptx()
    在这里插入图片描述
  3. 初始化bean,判定bean对应的类中的方法是否匹配到任意切入点
    匹配失败,创建对象
    匹配成功,创建原始对象(目标对象)的代理对象
  4. 获取bean执行方法
    获取bean,调用方法并执行,完成操作
    获取的是bean的代理对象时,根据代理对象的运行模式运行原始方法与增强的内容,完成操作

这里第3、第4点可能讲的有点抽象。我理解这几句话的意思如下:
“判定bean对应的类中的方法是否匹配到任意切入点”,这句话的意思判断我们测试时候new的对象:BookDao bookDao = ctx.getBean(BookDao.class);中的方法:
在这里插入图片描述
是否与我们定义的切入点(@Pointcut中指定的具体的方法)匹配上了。
在这里update()方法的实现如下:
在这里插入图片描述

现在我们BookDao定义了方法update()、实现类中实现了方法update()
然后在@Pointcut中又指定了:execution(void com.example.project3.dao.BookDao.update()),也就是指定到了BookDao中的update()方法
这时候说明已经形成了切面,即通知和切入点已经绑定上了,这时候初始化的对象就是代理对象而非原始对象本身,则通过对象名.方法调用就会走代理对象中的内容,先打印当前的时间,再执行对象方法中的内容

1702475078485
book dao update...

如果我们将@Pointcut中的内容改成execution(void com.example.project3.dao.BookDao.update1()),则此时没有办法匹配上,因为在BookDao中没有方法update1(),那么这时候就会初始化原始对象,通过对象名.方法调用不会走代理对象中的内容,只会执行对象本身的内容:

book dao update...

在初始化一个对象时候,我们可以通过对象名.getClass()方法来判断具体产生的是哪个对象,代理对象打印的结果如下:
在这里插入图片描述

5. AOP切入点表达式

切入点:要进行增强的方法
切入点表达式:要进行增强的方法的描述方式
在这里插入图片描述
描述方式一:execution(void com.itheima.dao.BookDao.update())
描述方式二:execution(void com.itheima.dao.impl.BookDaoImpl.update())
描述接口类及其实现皆可

语法格式

在这里插入图片描述

通配符

可以使用通配符描述切入点,快速描述

注意:假如我们要求参数为(*),则方法中必须有1个或多个参数,若方法没有参数则无法匹配到

书写技巧

在这里插入图片描述

6. AOP通知类型

在这里插入图片描述
代码如下,针对五种类型集中/分别进行说明:
BookDao.java

package com.example.project3.dao;

import org.springframework.stereotype.Repository;

public interface BookDao {
    void update();
    int select();
}

BookDaoImpl.java

package com.example.project3.dao.impl;

import com.example.project3.dao.BookDao;
import org.springframework.stereotype.Repository;

@Repository
public class BookDaoImpl implements BookDao {

    @Override
    public void update() {
        System.out.println("book dao update is running ...");
    }

    @Override
    public int select() {
        System.out.println("book dao select is running ...");
        return 100;
    }
}

MyAdvice.java

package com.example.project3.aop;

import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import org.aspectj.lang.annotation.Pointcut;
import org.springframework.stereotype.Component;

@Component
@Aspect
public class MyAdvice {

    @Pointcut("execution(void com.example.project3.dao.BookDao.update())")
    private void pt(){}

    public void before(){
        System.out.println("Before advice");
    }

    public void after(){
        System.out.println("After advice");
    }

    public void around(){
        System.out.println("around before advice ... ");
        System.out.println("around after advice ... ");
    }

    public void afterReturning(){
        System.out.println("afterReturning advice ... ");
    }

    public void afterThrowing(){
        System.out.println("afterThrowing advice ... ");
    }
}

测试方法:

package com.example.project3;

import com.example.project3.config.SpringConfig;
import com.example.project3.dao.BookDao;
import org.springframework.context.ApplicationContext;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;

public class Project3Application {

    public static void main(String[] args) {
        ApplicationContext ctx = new AnnotationConfigApplicationContext(SpringConfig.class);
        BookDao bookDao = ctx.getBean(BookDao.class);
        bookDao.update();
    }
}

前置通知和后置通知

两种最简单的通知方式:@Before@After

	@Before("pt()")
    public void before(){
        System.out.println("Before advice");
    }

    @After("pt()")
    public void after(){
        System.out.println("After advice");
    }

结果为:
在这里插入图片描述

环绕通知

环绕通知注解:

	@Around("pt()")
    public void around(){
        System.out.println("around before advice ... ");
        System.out.println("around after advice ... ");
    }

结果为:
在这里插入图片描述
这样会发现对象的方法没有被调用,问题在于当我们使用环绕通知的时候,要在某一个地方进行方法的调用,格式如下:

    @Around("pt()")
    public void around(ProceedingJoinPoint pjp) throws Throwable{
        System.out.println("around before advice ... ");
//        表示对原始操作的调用
        pjp.proceed();
        System.out.println("around after advice ... ");
    }

在这里强制地抛出了一个异常,为什么要抛出异常呢?因为它不知道pjp调用的过程中是否有异常抛出,所以它这里就提前抛出,如果有异常要抛出不由此处处理而由pjp本身去处理
运行结果如下:
在这里插入图片描述

如果我们加上一个切入点:

    @Pointcut("execution(int com.example.project3.dao.BookDao.select())")
    private void pt2(){}

写一个新方法并加上环绕通知:

    @Around("pt2()")
    public void aroundSelect(ProceedingJoinPoint pjp) throws Throwable{
        System.out.println("around before advice ... ");
//        表示对原始操作的调用
        pjp.proceed();
        System.out.println("around after advice ... ");
    }

在测试方法中调用bookDao.select(),此时运行结果为:
在这里插入图片描述
这是因为select()方法本身是有返回值的,而我们没有把这个返回值给丢出去。
正确的写法如下:

    @Around("pt2()")
    public Object aroundSelect(ProceedingJoinPoint pjp) throws Throwable{
        System.out.println("around before advice ... ");
//        表示对原始操作的调用
        Object result = pjp.proceed();
        System.out.println("around after advice ... ");
        return result;
    }

在这其中将类的返回值改成了Object,这才是标准的写法。如果pjp.proceed()方法本身没有返回值,则返回结果为null

运行结果为:
在这里插入图片描述
在这里插入图片描述
通过这个@Around可以对原始方法做隔离,比如只有经理/会员才能执行该方法,就可以通过@Around在这其中进行处理

返回后通知

这是在方法正常执行完毕后返回的通知
假如目前在这两个方法上加了注解:

    @After("pt2()")
    public void after(){
        System.out.println("After advice");
    }
    @AfterReturning("pt2()")
    public void afterReturning(){
        System.out.println("afterReturning advice ... ");
    }

测试类改为:

package com.example.project3;

import com.example.project3.config.SpringConfig;
import com.example.project3.dao.BookDao;
import org.springframework.context.ApplicationContext;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;

public class Project3Application {

    public static void main(String[] args) {
        ApplicationContext ctx = new AnnotationConfigApplicationContext(SpringConfig.class);
        BookDao bookDao = ctx.getBean(BookDao.class);
        int result = bookDao.select();
        System.out.println(result);
    }

}

运行测试类结果为:
在这里插入图片描述
如果在select()方法中加上:
在这里插入图片描述
则此时运行会在int a = 1/0处报错,此时返回结果为:
在这里插入图片描述
可以看出来只执行了After advice,而没有执行afterReturning advice
这说明 afterReturning advice 是在返回值返回之后才执行的,可以将其视为“最后的通知”

抛出异常后通知

这是在方法抛出异常后返回的通知
保持上面的select()方法不变,测试类不变,只保持以下方法的注解:

    @AfterThrowing("pt2()")
    public void afterThrowing(){
        System.out.println("afterThrowing advice ... ");
    }

此时运行结果为:
在这里插入图片描述
可以发现有异常抛出时候回执行异常抛出的通知类里的代码
若我们将异常注释掉,再次运行结果为:
在这里插入图片描述
此时不会执行AfterThrowing内的通知

7. AOP案例:测量业务接口执行效率

需求:任意业务层接口执行均可显示其执行效率(执行时长)
分析:

  • 业务功能:业务层执行接口执行前后分别记录时间,求差值得到执行效率
  • 通知类型:根据业务功能选择环绕通知

PS: 在完成这部分内容的时候,我是重新建立了项目并写了代码,遇到了之前可能没有注意的一些坑,会使用红字进行标注。

案例代码

整体架构

在这里插入图片描述

pom.xml

pom.xml,其中导入的包包含spring-context,以及一些mybatis,JUnit,和AOP相关的aspectj包。
注意,这里Spring版本和MyBatis版本的匹配尤为重要!如果出现版本没办法匹配上,在下面写代码时候会报一些莫名其妙的错误,比如我就是报了:Invalid value type for attribute ‘factoryBeanObjectType’: java.lang.String,一开始觉得莫名其妙,后来发现我是<parent>处的spring版本不对,修改以后就可以运行了

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>
    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>3.1.5</version>
        <relativePath/> <!-- lookup parent from repository -->
    </parent>
    <groupId>com.example</groupId>
    <artifactId>project4</artifactId>
    <version>0.0.1-SNAPSHOT</version>
    <name>project4</name>
    <description>project4</description>
    <properties>
        <java.version>17</java.version>
    </properties>
    <dependencies>
        <dependency>
            <groupId>org.springframework</groupId>
            <artifactId>spring-context</artifactId>
            <version>6.0.3</version>
        </dependency>
        <dependency>
            <groupId>org.springframework</groupId>
            <artifactId>spring-jdbc</artifactId>
            <version>6.0.3</version>
        </dependency>
        <dependency>
            <groupId>com.mysql</groupId>
            <artifactId>mysql-connector-j</artifactId>
            <version>8.0.33</version>
        </dependency>
        <dependency>
            <groupId>org.mybatis</groupId>
            <artifactId>mybatis</artifactId>
            <version>3.5.11</version>
        </dependency>
        <dependency>
            <groupId>org.mybatis</groupId>
            <artifactId>mybatis-spring</artifactId>
            <version>3.0.1</version>
        </dependency>
        <dependency>
            <groupId>com.alibaba</groupId>
            <artifactId>druid</artifactId>
            <version>1.2.13</version>
        </dependency>
        <dependency>
            <groupId>junit</groupId>
            <artifactId>junit</artifactId>
            <version>4.13.2</version>
            <scope>test</scope>
        </dependency>
        <dependency>
            <groupId>org.springframework</groupId>
            <artifactId>spring-test</artifactId>
            <version>5.3.25</version>
        </dependency>
        <dependency>
            <groupId>org.aspectj</groupId>
            <artifactId>aspectjweaver</artifactId>
            <version>1.9.5</version>
        </dependency>
    </dependencies>

</project>

config下的包:SpringConfig、JdbcConfig、MyBatisConfig

SpringConfig.java,这其中包含的一些配置之前都有介绍过,@Configuration指明这是一个配置类,@ComponentScan指示扫描的范围,@EnableAspectJAutoProxy允许切面编程!我后面就是由于少了这个注解,导致AOP失效@PropertySource引入外部数据源,@Import表示导入的其他配置类

package com.example.project4.config;

import org.springframework.context.annotation.*;

@Configuration
@ComponentScan("com.example.project4")
@EnableAspectJAutoProxy
@PropertySource("classpath:jdbc.properties")
@Import({JdbcConfig.class, MyBatisConfig.class})
public class SpringConfig {
}

JdbcConfig.java,配置数据源

package com.example.project4.config;

import com.alibaba.druid.pool.DruidDataSource;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;

import javax.sql.DataSource;

public class JdbcConfig {

    @Value("${jdbc.driver}")
    String driverClassName;
    @Value("${jdbc.url}")
    String url;
    @Value("${jdbc.password}")
    String password;
    @Value("${jdbc.username}")
    String username;

    @Bean
    public DataSource dataSource(){
        DruidDataSource dataSource = new DruidDataSource();
        dataSource.setDriverClassName(driverClassName);
        dataSource.setUrl(url);
        dataSource.setPassword(password);
        dataSource.setUsername(username);
        return dataSource;
    }
}

MyBatis.java,配置工厂方法,及配置MyBatis的扫描范围:

package com.example.project4.config;

import org.apache.ibatis.session.SqlSessionFactory;
import org.mybatis.spring.SqlSessionFactoryBean;
import org.mybatis.spring.mapper.MapperScannerConfigurer;
import org.springframework.context.annotation.Bean;

import javax.sql.DataSource;

public class MyBatisConfig {

    @Bean
    public SqlSessionFactoryBean sqlSessionFactory(DataSource dataSource){
        SqlSessionFactoryBean ssfb = new SqlSessionFactoryBean();
        ssfb.setTypeAliasesPackage("com.example.project4.domain");
        ssfb.setDataSource(dataSource);
        return ssfb;
    }

    @Bean
    public MapperScannerConfigurer mapperScannerConfigurer(){
        MapperScannerConfigurer msc = new MapperScannerConfigurer();
        msc.setBasePackage("com.example.project4.dao");
        return msc;
    }
}

数据源

jdbc.properties指定数据库的配置

jdbc.driver=com.mysql.cj.jdbc.Driver
jdbc.url=jdbc:mysql://localhost:3306/test
jdbc.username=root
jdbc.password=123456

实体类

Account.java,指定实体的属性

package com.example.project4.domain;

import org.springframework.context.annotation.Bean;

public class Account {
    int id;
    String username;
    double money;

    @Override
    public String toString() {
        return "Account{" +
                "id=" + id +
                ", username='" + username + '\'' +
                ", money=" + money +
                '}';
    }

    public Account() {
    }

    public Account(int id, String username, double money) {
        this.id = id;
        this.username = username;
        this.money = money;
    }

    public int getId() {
        return id;
    }

    public void setId(int id) {
        this.id = id;
    }

    public String getUsername() {
        return username;
    }

    public void setUsername(String username) {
        this.username = username;
    }

    public double getMoney() {
        return money;
    }

    public void setMoney(double money) {
        this.money = money;
    }
}

数据层

AccountDao.java,使用MyBatis注解进行开发

package com.example.project4.dao;


import com.example.project4.domain.Account;
import org.apache.ibatis.annotations.Delete;
import org.apache.ibatis.annotations.Insert;
import org.apache.ibatis.annotations.Select;
import org.apache.ibatis.annotations.Update;
import org.springframework.stereotype.Repository;

import java.util.List;

@Repository
public interface AccountDao {

    @Insert("INSERT INTO account(username, money) VALUES (#{username}, #{money})")
    void save(Account account);

    @Delete("DELETE FROM account WHERE id=#{id}")
    void delete(Integer id);

    @Update("UPDATE account SET username=#{username}, money=#{money} where id=#{id}")
    void update(Account account);

    @Select("SELECT * FROM account")
    List<Account> findAll();

    @Select("SELECT * FROM account where id=#{id}")
    Account findById(Integer id);

}

业务层

AccountService.java,业务层接口

package com.example.project4.service;

import com.example.project4.domain.Account;

import java.util.List;

public interface AccountService {
    void save(Account account);

    void delete(Integer id);

    void update(Account account);

    List<Account> findAll();

    Account findById(Integer id);
}

AccountServiceImpl.java,业务层实现类,在里面使用自动注解将AccountDao注入进来,并调用具体的方法。

package com.example.project4.service.impl;

import com.example.project4.dao.AccountDao;
import com.example.project4.service.AccountService;
import com.example.project4.domain.Account;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;

import java.util.List;

@Service
public class AccountServiceImpl implements AccountService {

    @Autowired
    private AccountDao accountDao;

    @Override
    public void save(Account account) {
        accountDao.save(account);
    }

    @Override
    public void delete(Integer id) {
        accountDao.delete(id);
    }

    @Override
    public void update(Account account) {
        accountDao.update(account);
    }

    @Override
    public List<Account> findAll() {
        return accountDao.findAll();
    }

    @Override
    public Account findById(Integer id) {
        return accountDao.findById(id);
    }
}

测试类

Project4ApplicationTest.java

package com.example.project4;

import com.example.project4.config.SpringConfig;
import com.example.project4.domain.Account;
import com.example.project4.service.AccountService;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;

import java.util.List;

@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(classes = SpringConfig.class)
public class Project4ApplicationTests {

    @Autowired
    private AccountService accountService;

    @Test
    public void testFindById(){
        Account ac = accountService.findById(2);
        System.out.println(ac);
    }

    @Test
    public void testFindAll(){
        System.out.println(accountService.getClass());
        List<Account> accountList = accountService.findAll();
        System.out.println(accountList);
    }

}

进行切面编程

结合4和6进行开发即可。由于这里我们根据需求使用环绕通知,所以注解会使用@Around
在项目下建立一个包aop,在下边建立一个ProjectAdvice.java
第一步:使用@Pointcut定义切入点,运用了通配符进行描述,这里指的是任意返回类型 com.example.project4.service下所有以Service结尾的类下的任意方法

    @Pointcut("execution(* com.example.project4.service.*Service.*(..))")
    private void servicePt(){}

第二步:定义具体的“通知”,并在通知上加上注解,说明在哪些切入点执行该方法中的内容。方法中的参数ProceedingJointPoint用来进行方法的调用:pjp.proceed(),同时通过pjp.getSignature()可以获得signature参数,并获得执行方法所在的类名getDeclaringTypeName和执行方法的方法名getName

    @Around("servicePt()")
    public void runSpeed(ProceedingJoinPoint pjp) throws Throwable{
        Signature signature = pjp.getSignature();
        String className = signature.getDeclaringTypeName();
        String methodName = signature.getName();

        long startTime = System.currentTimeMillis();

        for (int i = 0; i < 10000; i++){
            pjp.proceed();
        }

        long endTime = System.currentTimeMillis();
        System.out.println("万次执行:" + className + "." + methodName + "--->" + (endTime-startTime) + "ms");
    }

使用JUnit得到结果为(由于这里没有返回参数,所以返回值为null):
在这里插入图片描述

8. AOP通知获取数据

在这里插入图片描述
此处案例使用的是AOP工作流程中的案例,只有以下代码稍有差别:
BookDao.java

package com.example.project4.dao;

public interface BookDao {
    public String findName(int id);
}

BookDaoImpl.java

package com.example.project4.dao.impl;

import com.example.project4.dao.BookDao;
import org.springframework.stereotype.Repository;

@Repository
public class BookDaoImpl implements BookDao {
    @Override
    public String findName(int id) {
        System.out.println("id:" + id);
        return "itcast";
    }
}

MyAdvice.java(这个是切面编程的类)

package com.example.project4.aop;

import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.*;
import org.springframework.stereotype.Component;

@Component
@Aspect
public class MyAdvice {
    @Pointcut("execution(* com.example.project4.dao.BookDao.findName(..))")
    private void pt(){}

    @Before("pt()")
    public void before(){
        System.out.println("before advice ...");
    }

    @After("pt()")
    public void after(){
        System.out.println("after advice ...");
    }

    @Around("pt()")
    public Object around(ProceedingJoinPoint pjp) throws Throwable{
        Object ret = pjp.proceed();
        return ret;
    }

    @AfterReturning("pt()")
    public void afterReturning(){
        System.out.println("afterReturning advice ...");
    }

    @AfterThrowing("pt()")
    public void afterThrowing(){
        System.out.println("afterThrowing advice ...");
    }
}

测试类

package com.example.project4;

import com.example.project4.config.SpringConfig;
import com.example.project4.dao.BookDao;
import com.example.project4.dao.impl.BookDaoImpl;
import org.springframework.context.ApplicationContext;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;

public class Project4Application {

    public static void main(String[] args) {
        ApplicationContext ctx = new AnnotationConfigApplicationContext(SpringConfig.class);
        BookDao bookDao = (BookDao) ctx.getBean(BookDao.class);
        String result = bookDao.findName(100);
        System.out.println(result);
    }

}

AOP相关的类:

package com.example.project4.aop;

import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.*;
import org.springframework.stereotype.Component;

@Component
@Aspect
public class MyAdvice {
    @Pointcut("execution(* com.example.project4.dao.BookDao.findName(..))")
    private void pt(){}

    @Before("pt()")
    public void before(){
        System.out.println("before advice ...");
    }

//    @After("pt()")
    public void after(){
        System.out.println("after advice ...");
    }

//    @Around("pt()")
    public Object around(ProceedingJoinPoint pjp) throws Throwable{
        Object ret = pjp.proceed();
        return ret;
    }

//    @AfterReturning("pt()")
    public void afterReturning(){
        System.out.println("afterReturning advice ...");
    }

//    @AfterThrowing("pt()")
    public void afterThrowing(){
        System.out.println("afterThrowing advice ...");
    }
}

获取参数

@Before,@After,@AfterReturning,@AfterThrowing中都可以加入这个参数JoinPoint jp

	@Before("pt()")
    public void before(JoinPoint jp){
        Object[] args = jp.getArgs();
        System.out.println(Arrays.toString(args));
        System.out.println("before advice ...");
    }

通过这个jp可以获取参数,返回值为Object类型的数组。执行一下测试方法:
在这里插入图片描述
对于参数ProceedingJoinPoint pjpProceedingJoinPoint作为JoinPoint的子类,同样使用pjp.getArgs()可以获得具体的参数。
有意思的是,我们可以修改参数,并在调用方法时传入我们修改好的参数(注意,这里方法不传入参数也是可以执行的)

    @Around("pt()")
    public Object around(ProceedingJoinPoint pjp) throws Throwable{
        Object[] args = pjp.getArgs();
        System.out.println(Arrays.toString(args));
        args[0] = 666;
        Object ret = pjp.proceed(args);
        return ret;
    }

结果会变成:
在这里插入图片描述

获取返回值

想要获取方法的返回值,方法如下:

	@AfterReturning(value = "pt()", returning = "ret")
    public void afterReturning(Object ret){
        System.out.println("afterReturning advice ..." + ret);
    }

也就是在注解@AfterReturning中,除了定义value以外,还定义returning,里面的参数写的事括号里的参数,也就是告诉这个注解,将返回值放到参数ret
执行测试方法,结果:
在这里插入图片描述
注意:如果JointPoint和Object同时作为参数时,必须将JointPoint写在第一个!
在这里插入图片描述

获取异常

和获取返回值类似,获取异常的方法:

    @AfterThrowing(value = "pt()", throwing = "t")
    public void afterThrowing(Throwable t){
        System.out.println("afterThrowing advice ..." + t);
    }

修改一下方法并测试:
在这里插入图片描述
在这里插入图片描述

9. AOP总结

概念: AOP(Aspect Oriented Programming)面向切面编程,是一种编程范式
作用:在不惊动原始设计的基础上为方法功能进行增强
核心概念:
在这里插入图片描述
切入点表达式:
在这里插入图片描述
在这里插入图片描述

通知类型:
在这里插入图片描述

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

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

用Excel绘制柱形图

在需要将数据用柱状图表示的时候&#xff0c;可以用Excel进行绘制。不单绘制柱形图&#xff0c;其他数据图也可以用Excel绘制。 接下来用绘制一个销售表的示例演示。 1.将数据输入Excel 数学书 语文书 英语书 一月 80 94 77 二月 95 86 84 三月 130 93 79 四月 …

深度学习环境配置

一、Anaconda安装 下载&#xff1a;从清华大学开源软件镜像下载 镜像网址 出现base即为安装成功&#xff1a; 检查显卡的驱动是否正确安装&#xff1a; &#xff08;GPU可以显示出名称&#xff09; GPU0是集显集成显卡是主板自带的显卡。 GPU1是独显即独立显卡&#xff0c…

C# 基本桌面编程(一)

前言 学习心得&#xff1a;C# 入门经典第8版书中的第14章《基本桌面编程》&#xff0c;文章的章节和部分介绍是引入书籍上的描述。如想多了解建议大家去购买书籍&#xff0c;进行阅读。 XAML XAML是一门使用XAMl语法的语言。XAML允许通过DirectX来使用这些显卡提供所有高级功能…

HPM6750系列--总章

本栏目介绍先楫半导体出品的HPM6750芯片&#xff08;基于HPM6750evkmini开发板&#xff09; ​​​​​​​ 内容概述 HPM6750系列--第一篇 初识HPM6750 介绍HPM6750芯片信息&#xff0c;包括主频、内存、外设配置&#xff0c;并列举了各种开发工具和开发资源。 HPM6750系列--…

101基于matlab的极限学习机ELM算法进行遥感图像分类

基于matlab的极限学习机ELM算法进行遥感图像分类&#xff0c;对所获取的遥感图片进行初步分类和最终分类。数据可更换自己的&#xff0c;程序已调通&#xff0c;可直接运行。 101matlab遥感图像分类模式识别 (xiaohongshu.com)

大数据生态圈kafka在物联网中的应用测试

背景 由物联网项目中使用到了Tbox应用管理车辆&#xff0c;在上报数据的过程中&#xff0c;需要将终端产生的数据通过kafka的produce topic customer对数据进行处理后&#xff0c;放置到mysql中。完成数据二进制到json转换工作。 Kafka的使用 查看kafka的topic ./kafka-topi…

【SpringBoot】从入门到精通的快速开发指南

&#x1f389;&#x1f389;欢迎来到我的CSDN主页&#xff01;&#x1f389;&#x1f389; &#x1f3c5;我是Java方文山&#xff0c;一个在CSDN分享笔记的博主。&#x1f4da;&#x1f4da; &#x1f31f;推荐给大家我的专栏《SpringBoot》。&#x1f3af;&#x1f3af; &…

【开源软件】最好的开源软件-2023-第14名 Appsmith

自我介绍 做一个简单介绍&#xff0c;酒架年近48 &#xff0c;有20多年IT工作经历&#xff0c;目前在一家500强做企业架构&#xff0e;因为工作需要&#xff0c;另外也因为兴趣涉猎比较广&#xff0c;为了自己学习建立了三个博客&#xff0c;分别是【全球IT瞭望】&#xff0c;【…

解决下载huggingface模型权重无法下载的问题

文章目录 方法一(推荐)方法二方法三依然存在的问题 由于某些原因&#xff0c;huggingface的访问速度奇慢无比&#xff0c;对于一些模型(比如大语言模型LLM)的权重文件动辄几十上百G&#xff0c;如果用默认下载方式&#xff0c;很可能中断&#xff0c;这里推荐几种方式。 方法一…

Qt容器QScrollArea小部件的滚动视图

​# QScrollArea 平台:linux、windows、mac皆可,Qt版本:Qt5.14.2 QScrollArea是Qt框架中用于提供可滚动视图区域的小部件。它通常被用来包含一个较大的内容区域,并且可以在其中嵌入其他小部件。下面是一些常用的QScrollArea函数: 1. `setWidget(QWidget *widget)`: 设置在…

八大排序(插入排序 | 选择排序 | 冒泡排序)

在我们内存中我们一般会有一些没有顺序的数据&#xff0c;我们成为内排序&#xff0c;而今天分享八大排序的是时间复杂度为O&#xff08;N^2&#xff09;的插入排序&#xff0c;选择排序和教学意义比较强的冒泡排序。 插入排序 这是插入排序的动图&#xff0c;通过动图我们也…

pom打包跳过不含main方法的pom工程

Unable to find main class 总pom包已经有打包插件 不包含main方法的工程中的pom配置如下配置 <build><plugins><plugin><groupId>org.springframework.boot</groupId><artifactId>spring-boot-maven-plugin</artifactId><con…

KaiwuDB 获评信通院 2023 大数据“星河”标杆案例

12月6日&#xff0c;由中国信息通信研究院、中国通信标准化协会大数据技术标准推进委员会(CCSA TC601) 共同组织的 2023 大数据“星河(Galaxy)”案例评选结果正式公示&#xff0c;“基于 KaiwuDB 的台区云储能示范项目”历经多环节严苛评审&#xff0c;从累计 706 份申报项目中…

JVM虚拟机系统性学习-JVM调优实战之内存溢出、高并发场景调优

调优实战-内存溢出的定位与分析 首先&#xff0c;对于以下代码如果造成内存溢出该如何进行定位呢&#xff1f;通过 jmap 与 MAT 工具进行定位分析 代码如下&#xff1a; public class TestJvmOutOfMemory {public static void main(String[] args) {List<Object> list…

如何快速制作一个属于自己的网站

在现在这个数字化的时代&#xff0c;有一个属于自己的网站逐渐的成为了展示自己或企业形象、推广产品和服务的重要手段之一。对于小白来说制作一个网站可能听起来很复杂和困难&#xff0c;但其实实际上随着技术的发展和各种网站建设工具的出现&#xff0c;制作一个属于自己的网…

C语言——模拟strcpy函数

代码实现&#xff1a; #include<stdio.h>void mystrcpy(char *des,char *src) {int i0;while(src[i]!\0){des[i]src[i];i;}des[i]\0;//也可以用下面的方法//while(*src!\0)//{// *des*src;// des;// src;//}//*des\0;//或者下面方法//while(*des *src)//{// …

linux应用层编程问题--沙雕问题

1.调用沁恒 USB读取接口 读不到数据 static bool CH37XASyncReadData(int iIndex, uint32_t epindex, void *oBuffer, uint32_t *ioLength) {struct _bulkUp {uint32_t len;uint8_t epindex;uint8_t data[0];} __attribute__((packed));struct _bulkUp *bulkUp;int retval;bul…

计算机毕业设计 基于SpringBoot的日常办公用品直售推荐系统的设计与实现 Java实战项目 附源码+文档+视频讲解

博主介绍&#xff1a;✌从事软件开发10年之余&#xff0c;专注于Java技术领域、Python人工智能及数据挖掘、小程序项目开发和Android项目开发等。CSDN、掘金、华为云、InfoQ、阿里云等平台优质作者✌ &#x1f345;文末获取源码联系&#x1f345; &#x1f447;&#x1f3fb; 精…

MES管理系统执行过程中的动态批次管理

MES生产管理系统是现代制造业中非常重要的生产管理工具&#xff0c;它能够有效地协调和监控生产过程。在MES管理系统中&#xff0c;动态批次管理技术发挥着关键作用&#xff0c;对于提高生产效率和质量具有重要意义。本文将详细介绍MES管理系统中的动态批次管理技术及其在生产过…

【教3妹学编程-算法题】反转二叉树的奇数层

插&#xff1a; 前些天发现了一个巨牛的人工智能学习网站&#xff0c;通俗易懂&#xff0c;风趣幽默&#xff0c;忍不住分享一下给大家。点击跳转到网站。 坚持不懈&#xff0c;越努力越幸运&#xff0c;大家一起学习鸭~~~ 3妹&#xff1a;“你不是真正的快乐&#xff0c; 你的…