当前位置: 首页 > news >正文

Spring(3)Spring从零到入门 - Spring整合技术及AOP事务管理

Spring(3)Spring从零到入门 - Spring整合技术及AOP事务管理

文章目录

  • Spring(3)Spring从零到入门 - Spring整合技术及AOP事务管理
    • 4 Spring整合技术示例
      • 4.1 Spring整合Mybatis
        • 4.1.1 Mybatis开发回顾
        • 4.1.2 整合Spring分析
        • 4.1.3 Spring整合Mybatis
      • 4.2 Spring整合Junit
        • 4.2.1 环境准备
        • 4.2.2 整合Junit步骤
    • 5 AOP
      • 5.1 AOP简介
      • 5.2 AOP入门案例
        • 5.2.1 环境准备
        • 5.2.2 实现步骤
      • 5.3 AOP工作流程
        • 5.3.1 AOP工作流程
        • 5.3.2 验证容器中是否为代理对象
          • 5.3.2.1 不被增强
          • 5.3.2.2 被增强
        • 5.3.3 AOP核心概念
      • 5.4 AOP配置管理
        • 5.4.1 AOP切入点表达式
          • 5.4.1.1 语法格式
          • 5.4.1.2 通配符
          • 5.4.1.3 书写技巧
        • 5.4.2 AOP通知(增强)类型
          • 5.4.2.1 类型介绍
            • **前置通知**
            • **后置通知**
            • **环绕通知**
            • **返回后通知**
            • **异常后通知**
            • **环绕通知扩展**
          • 通知类型总结
        • 5.4.3 AOP获取数据
          • 5.4.3.1 获取参数
          • 5.4.3.2 获取返回值
          • 5.4.3.3 获取异常
      • 5.5 AOP总结
        • 5.5.1 AOP的核心概念
        • 5.5.2 切入点表达式
        • 5.5.3 五种通知类型
        • 5.5.4 通知中获取参数
      • 5.6 AOP事务管理
        • 5.6.1 Spring事务简介
          • 5.6.1.1 转账案例
        • 5.6.2 Spring事务角色
          • 5.6.2.1 未开启Spring事务之前
          • 5.6.2.2 开启了Spring事务之后
        • 5.6.3 Spring事务属性
          • 5.6.3.1 事务配置
          • 5.6.3.2 事务传播行为
            • **5.6.3.2.1 事务传播行为**
            • 5.6.3.2.2 事务传播行为属性说明
            • **5.6.3.2.1 事务传播行为**
            • 5.6.3.2.2 事务传播行为属性说明

4 Spring整合技术示例

4.1 Spring整合Mybatis

4.1.1 Mybatis开发回顾

步骤1:准备数据库表

Mybatis是来操作数据库表,所以先创建一个数据库及表

create database spring_db character set utf8;
use spring_db;
create table tbl_account(id int primary key auto_increment,name varchar(35),money double
);

步骤2:创建项目导入jar包

项目的pom.xml添加相关依赖

<dependencies><dependency><groupId>org.springframework</groupId><artifactId>spring-context</artifactId><version>5.2.10.RELEASE</version></dependency><dependency><groupId>com.alibaba</groupId><artifactId>druid</artifactId><version>1.1.16</version></dependency><dependency><groupId>org.mybatis</groupId><artifactId>mybatis</artifactId><version>3.5.6</version></dependency><dependency><groupId>mysql</groupId><artifactId>mysql-connector-java</artifactId><version>5.1.47</version></dependency>
</dependencies>

步骤3:根据表创建模型类

public class Account implements Serializable {private Integer id;private String name;private Double money;//setter...getter...toString...方法略    
}

回顾**Serializable**

Serializable 是一个标记接口,继承了这个接口,相关对象可实现序列化

什么场景中会涉及到将对象进行**序列化(Serializable)**

  1. 分布式系统: 在分布式系统中,不同的服务可能运行在不同的物理机器上。为了在这些服务之间传递对象,需要将对象转换成字节序列,以便在网络上传输。这是一种常见的序列化应用场景。
  2. 缓存: 在将对象存储到缓存中时,有些缓存系统要求存储的对象是可序列化的。这样可以在需要时将对象保存到缓存中,以提高数据访问性能。
  3. 消息队列: 在消息队列系统中,消息通常需要被序列化以进行传输。生产者将对象序列化为消息,而消费者则负责将消息反序列化为对象。

步骤4:创建Dao接口

public interface AccountDao {@Insert("insert into tbl_account(name,money)values(#{name},#{money})")void save(Account account);@Delete("delete from tbl_account where id = #{id} ")void delete(Integer id);@Update("update tbl_account set name = #{name} , money = #{money} where id = #{id} ")void update(Account account);@Select("select * from tbl_account")List<Account> findAll();@Select("select * from tbl_account where id = #{id} ")Account findById(Integer id);
}

步骤5:创建Service接口和实现类

public interface AccountService {void save(Account account);void delete(Integer id);void update(Account account);List<Account> findAll();Account findById(Integer id);}@Service
public class AccountServiceImpl implements AccountService {@Autowiredprivate AccountDao accountDao;public void save(Account account) {accountDao.save(account);}public void update(Account account){accountDao.update(account);}public void delete(Integer id) {accountDao.delete(id);}public Account findById(Integer id) {return accountDao.findById(id);}public List<Account> findAll() {return accountDao.findAll();}
}

步骤6:添加jdbc.properties文件

resources目录下添加,用于配置数据库连接四要素

jdbc.driver=com.mysql.jdbc.Driver
jdbc.url=jdbc:mysql://localhost:3306/spring_db?useSSL=false
jdbc.username=root
jdbc.password=1234

useSSL:关闭MySQL的SSL连接

在MySQL中,**SSL(Secure Sockets Layer)连接**是通过使用SSL协议来加密和保护数据库连接的一种方式。SSL是一种用于在计算机网络上进行安全通信的协议,它使用加密算法来确保数据在传输过程中的机密性和完整性。对于数据库连接,使用SSL可以有效地保护敏感信息,防止在传输过程中被窃听或篡改。

使用SSL连接MySQL的过程包括以下步骤:

  1. 生成SSL证书和私钥
  2. 配置MySQL服务器
  3. 配置MySQL客户端

步骤7:添加Mybatis核心配置文件

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE configurationPUBLIC "-//mybatis.org//DTD Config 3.0//EN""http://mybatis.org/dtd/mybatis-3-config.dtd">
<configuration><!--读取外部properties配置文件--><properties resource="jdbc.properties"></properties><!--别名扫描的包路径--><typeAliases><package name="com.ibaidu.domain"/></typeAliases><!--数据源--><environments default="mysql"><environment id="mysql"><transactionManager type="JDBC"></transactionManager><dataSource type="POOLED"><property name="driver" value="${jdbc.driver}"></property><property name="url" value="${jdbc.url}"></property><property name="username" value="${jdbc.username}"></property><property name="password" value="${jdbc.password}"></property></dataSource></environment></environments><!--映射文件扫描包路径--><mappers><package name="com.ibaidu.dao"></package></mappers>
</configuration>
  • 第一行读取外部properties配置文件,Spring有提供具体的解决方案@PropertySource,需要交给Spring

  • 第二行起别名,包扫描,为SqlSessionFactory服务的,需要交给Spring

    别名配置允许你给Java类设置一个别名

    <typeAliases><typeAlias alias="User" type="com.example.User"/>
    </typeAliases>
    

    上述配置将com.example.User类设置别名为User,之后在映射文件中可以使用<resultMap><parameterMap>等元素时,可以使用User代替完整的类名

    包扫描(Type Aliases Package)

    <typeAliases><package name="com.example.domain"/>
    </typeAliases>
    

    包扫描配置允许你指定一个包名,MyBatis将会自动扫描该包下的所有类,并将这些类设置为别名。这样,你在映射文件中引用类时,只需使用类名,而不必配置每个类的别名

  • 第三行主要用于做连接池,Spring之前我们已经整合了Druid连接池,这块也需要交给Spring

  • 前面三行一起都是为了创建SqlSession对象用的,SqlSession是由SqlSessionFactory创建出来的,所以只需要将SqlSessionFactory交给Spring管理即可。

  • 第四行是Mapper接口和映射文件[如果使用注解就没有该映射文件],这个是在获取到SqlSession以后执行具体操作的时候用,所以它和SqlSessionFactory创建的时机都不在同一个时间可能需要单独管理

    这段XML配置是MyBatis中用于配置映射器(mappers)的一部分。具体来说,它通过package元素指定了映射器接口所在的包名,MyBatis将会扫描该包下的所有映射器接口,并自动加载它们

步骤8:编写应用程序

public class App {public static void main(String[] args) throws IOException {// 1. 创建SqlSessionFactoryBuilder对象SqlSessionFactoryBuilder sqlSessionFactoryBuilder = new SqlSessionFactoryBuilder();// 2. 加载SqlMapConfig.xml配置文件InputStream inputStream = Resources.getResourceAsStream("SqlMapConfig.xml");// 3. 创建SqlSessionFactory对象SqlSessionFactory sqlSessionFactory = sqlSessionFactoryBuilder.build(inputStream);// 4. 获取SqlSessionSqlSession sqlSession = sqlSessionFactory.openSession();// 5. 执行SqlSession对象执行查询,获取结果UserAccountDao accountDao = sqlSession.getMapper(AccountDao.class);Account ac = accountDao.findById(1);System.out.println(ac);// 6. 释放资源sqlSession.close();}
}

SqlSessionFactoryBuilder的主要作用是读取MyBatis的配置信息并构建出一个SqlSessionFactory对象

SqlSessionFactory:是“生产”SqlSession的“工厂”

SqlSession:代表Java程序和数据库之间的会话。(HttpSession是Java程序和浏览器之间的会话)

工厂模式:如果创建某一个对象,使用的过程基本固定,那么我们就可以把创建这个对象的相关代码封装到一个“工厂类”中,以后都使用这个工厂类来“生产”我们需要的对象。

步骤9:运行程序

image-20231221142106523

4.1.2 整合Spring分析

Mybatis的基础环境我们已经准备好了,接下来就得分析下在上述的内容中,哪些对象可以交给Spring来管理?

image-20231221143149306

真正需要交 给Spring管理的是**SqlSessionFactory**

4.1.3 Spring整合Mybatis

前面我们已经分析了Spring与Mybatis的整合,大体需要做两件事,

第一件事是:Spring要管理MyBatis中的SqlSessionFactory

第二件事是:Spring要管理Mapper接口的扫描

步骤1:项目中导入整合需要的jar包

<dependency><!--Spring操作数据库需要该jar包--><groupId>org.springframework</groupId><artifactId>spring-jdbc</artifactId><version>5.2.10.RELEASE</version>
</dependency>
<dependency><!--Spring与Mybatis整合的jar包这个jar包mybatis在前面,是Mybatis提供的--><groupId>org.mybatis</groupId><artifactId>mybatis-spring</artifactId><version>1.3.0</version>
</dependency>

步骤2:创建Spring的主配置类

//配置类注解
@Configuration
//包扫描,主要扫描的是项目中的AccountServiceImpl类
@ComponentScan("com.ibaidu")
public class SpringConfig {
}

步骤3:创建数据源的配置类

在配置类中完成数据源的创建

public class JdbcConfig {@Value("${jdbc.driver}")private String driver;@Value("${jdbc.url}")private String url;@Value("${jdbc.username}")private String userName;@Value("${jdbc.password}")private String password;@Beanpublic DataSource dataSource(){DruidDataSource ds = new DruidDataSource();ds.setDriverClassName(driver);ds.setUrl(url);ds.setUsername(userName);ds.setPassword(password);return ds;}
}

步骤4:主配置类中读properties并引入数据源配置类

@Configuration
@ComponentScan("com.ibaidu")
@PropertySource("classpath:jdbc.properties")
@Import(JdbcConfig.class)
public class SpringConfig {
}

步骤5:创建Mybatis配置类并配置SqlSessionFactory

public class MybatisConfig {//定义bean,SqlSessionFactoryBean,用于产生SqlSessionFactory对象@Beanpublic SqlSessionFactoryBean sqlSessionFactory(DataSource dataSource){SqlSessionFactoryBean ssfb = new SqlSessionFactoryBean();//设置模型类的别名扫描ssfb.setTypeAliasesPackage("com.ibaidu.domain");//设置数据源ssfb.setDataSource(dataSource);return ssfb;}//定义bean,返回MapperScannerConfigurer对象@Beanpublic MapperScannerConfigurer mapperScannerConfigurer(){MapperScannerConfigurer msc = new MapperScannerConfigurer();msc.setBasePackage("com.ibaidu.dao");return msc;}
}

说明:

  • 使用SqlSessionFactoryBean封装SqlSessionFactory需要的环境信息

    1630138835057

    • SqlSessionFactoryBean是前面我们讲解FactoryBean的一个子类,在该类中将SqlSessionFactory的创建进行了封装,简化对象的创建,我们只需要将其需要的内容设置即可。
  • 使用MapperScannerConfigurer加载Dao接口,创建代理对象保存到IOC容器中

    1630138916939

    • 这个**MapperScannerConfigurer对象也是MyBatis提供的专用于整合的jar包中的类**,用来处理原始配置文件中的mappers相关配置,加载数据层的Mapper接口类
    • MapperScannerConfigurer有一个核心属性basePackage,就是用来设置所扫描的包路径

步骤6:主配置类中引入Mybatis配置类

@Configuration
@ComponentScan("com.ibaidu")
@PropertySource("classpath:jdbc.properties")
@Import({JdbcConfig.class,MybatisConfig.class})
public class SpringConfig {
}

Import中的加载顺序

JdbcConfig.classMybatisConfig.class的代码顺序并不会影响它们的加载顺序;Spring容器会根据配置类的依赖关系和其他条件,以一种合适的顺序加载它们。

步骤7:编写运行类

在运行类中,从IOC容器中获取Service对象,调用方法获取结果

public class App2 {public static void main(String[] args) {ApplicationContext ctx = new AnnotationConfigApplicationContext(SpringConfig.class);AccountService accountService = ctx.getBean(AccountService.class);Account ac = accountService.findById(1);System.out.println(ac);}
}

步骤8:运行程序

image-20231221150725698

支持Spring与Mybatis的整合就已经完成了,其中主要用到的两个类分别是:

  • SqlSessionFactoryBean
  • MapperScannerConfigurer

4.2 Spring整合Junit

Junit是一个搞单元测试用的工具,它不是我们程序的主体,也不会参加最终程序的运行;从作用上来说就和之前的东西不同,它不是做功能的,更像是一个辅助工具

4.2.1 环境准备

步骤1:引入依赖

pom.xml

<dependency><groupId>junit</groupId><artifactId>junit</artifactId><version>4.12</version><scope>test</scope>
</dependency><dependency><groupId>org.springframework</groupId><artifactId>spring-test</artifactId><version>5.2.10.RELEASE</version>
</dependency>
4.2.2 整合Junit步骤

在test\java下创建一个AccountServiceTest,这个名字任意

//设置类运行器
@RunWith(SpringJUnit4ClassRunner.class)
//设置Spring环境对应的配置类
@ContextConfiguration(classes = {SpringConfiguration.class}) //加载配置类
//@ContextConfiguration(locations={"classpath:applicationContext.xml"})//加载配置文件
public class AccountServiceTest {//支持自动装配注入bean@Autowiredprivate AccountService accountService;@Testpublic void testFindById(){System.out.println(accountService.findById(1));}@Testpublic void testFindAll(){System.out.println(accountService.findAll());}
}

注意:

  • 单元测试,如果测试的是注解配置类,则使用@ContextConfiguration(classes = 配置类.class)

  • 单元测试,如果测试的是配置文件,则使用@ContextConfiguration(locations={配置文件名,...})

    虽然配置文件本身不包含业务逻辑,但在测试中仍然有一些可以考虑的方面:

    配置项的正确性测试配置文件的存在性检查环境变量和配置文件的协同测试

  • Junit运行后是基于Spring环境运行的,所以Spring提供了一个专用的类运行器,这个务必要设置,这个类运行器就在Spring的测试专用包中提供的,导入的坐标就是这个东西SpringJUnit4ClassRunner

  • 上面两个配置都是固定格式,当需要测试哪个bean时,使用自动装配加载对应的对象,下面的工作就和以前做Junit单元测试完全一样了

知识点1:@RunWith

名称@RunWith
类型测试类注解
位置测试类定义上方
作用设置JUnit运行器
属性value(默认):运行所使用的运行期

知识点2:@ContextConfiguration

名称@ContextConfiguration
类型测试类注解
位置测试类定义上方
作用设置JUnit加载的Spring核心配置
属性classes:核心配置类,可以使用数组的格式设定加载多个配置类
locations:配置文件,可以使用数组的格式设定加载多个配置文件名称

关于测试类的文件位置

在Java项目中,通常会有一些约定俗成的目录结构,用于存放源代码和测试代码。这种目录结构有助于开发者更容易地组织和管理代码。一般而言,测试类(Test Classes)通常存放在与源代码相对应的测试目录

src
├── main
│   └── java
│       └── com
│           └── example
│               └── MyClass.java
└── test└── java└── com└── example└── MyClassTest.java

按照这个结构存放,确实一下就可以识别到相关的测试类,和相关的注解:

image-20231221160124360

运行后报错:Class not found: “com.baidu.test.AccountServiceTest”

image-20231221160152671

原因:pom.xml配置中配置了其他的测试类的地址,注释掉之后就不会出错了

image-20231221160758441

运行结果:

image-20231221160838109

如果想要自定义测试类,那么

① 在 pom.xml 文件中,将 <testSourceDirectory> 标签设置为新的测试目录路径。

<build><testSourceDirectory>src/test2/java</testSourceDirectory><!-- 其他构建配置 -->
</build>

② 确保在你的测试类上使用了适当的测试框架(如 JUnit),并在类上添加 @RunWith 注解。

@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(classes = {SpringConfig.class})
public class AccountServiceTest {@Autowiredprivate AccountService accountService;@Testpublic void test01(){System.out.println(this.accountService.findById(1));}
}

**但是!**识别不到这个文件,无法测试:

image-20231221161532105

建议还是将测试类及其代码放在约定俗成相应的文件夹中

对于接口的注入报错

接口AccountDao采用注解的方式实现了具体的方法,所以没有AccountDaoImpl这样的实现类:

image-20231223211718155

所以,相关注入会报错:

image-20231223211810593

但这种情况下,不用管这个报错,程序能够正常执行,因为MybatisConfig中扫描了相关文件,加载Dao接口,创建代理对象保存到IOC容器中

public class MybatisConfig {@Beanpublic SqlSessionFactoryBean sqlSessionFactory(DataSource dataSource){SqlSessionFactoryBean ssfb = new SqlSessionFactoryBean();ssfb.setTypeAliasesPackage("com.baidu.domain");ssfb.setDataSource(dataSource);return ssfb;}@Beanpublic MapperScannerConfigurer mapperScannerConfigurer(){MapperScannerConfigurer msc = new MapperScannerConfigurer();msc.setBasePackage("com.baidu.dao");return msc;}
}

5 AOP

Spring有两个核心的概念,一个是IOC/DI,一个是AOP

AOP是在不改原有代码的前提下对其进行增强

5.1 AOP简介

  • AOP(Aspect Oriented Programming)面向切面编程,一种编程范式,指导开发者如何组织程序结构

    OOP(Object Oriented Programming)面向对象编程

  • OOP是一种编程思想,那么AOP也是一种编程思想,编程思想主要的内容就是指导程序员该如何编写程序,所以它们两个是不同的编程范式(Programming paradigm)

  • 作用: 在不惊动原始设计的基础上为其进行功能增强,前面咱们有技术就可以实现这样的功能即代理模式

    如何理解这里的**功能增强**呢?

    @Repository
    public class BookDaoImpl implements BookDao {public void save() {//记录程序当前执行执行(开始时间)Long startTime = System.currentTimeMillis();//业务执行万次for (int i = 0;i<10000;i++) {System.out.println("book dao save ...");}//记录程序当前执行时间(结束时间)Long endTime = System.currentTimeMillis();//计算时间差Long totalTime = endTime-startTime;//输出信息System.out.println("执行万次消耗时间:" + totalTime + "ms");}public void update(){System.out.println("book dao update ...");}public void delete(){System.out.println("book dao delete ...");}public void select(){System.out.println("book dao select ...");}
    }
    

    于计算万次执行消耗的时间,只有save方法有,可不可以让delete和update方法也有呢?可不可以让select方法保持原来的效果即没有呢

    用Spring的AOP就可以实现。

    image-20231222145926287

    不惊动(改动)原有设计(代码)的前提下,想给谁添加功能就给谁添加。

    这个也就是Spring的理念:无入侵式/无侵入式。前面的注入也具有这样的思想,提供了一个变量,相关的对象就有值了

  • 背后的原理是什么样的呢?(核心概念)

    image-20231222150610654

    连接点:类里面哪些方法可以被增强,这些方法称为连接点

    连接点(JoinPoint):程序执行过程中的任意位置,粒度为执行方法、抛出异常、设置变量等

    • 在SpringAOP中,理解为方法的执行

    切入点:实际被真正增强的方法,称为切入点

    切入点(Pointcut): 匹配连接点的式子

    • 在SpringAOP中,一个切入点可以描述一个具体方法,也可也匹配多个方法
      • 一个具体的方法:如com.baidu.dao包下的BookDao接口中的无形参无返回值的save方法
      • 匹配多个方法:所有的save方法,所有的get开头的方法,所有以Dao结尾的接口中的任意方法,所有带有一个参数的方法
    • 连接点范围要比切入点范围大,是切入点的方法也一定是连接点,但是是连接点的方法就不一定要被增强,所以可能不是切入点。

    通知(增强):实际增强的逻辑部分称为通知(增强);通知有多种类型:前置通知,后置通知,环绕通知,异常通知,最终通知

    通知(Advice): 在切入点处执行的操作,也就是共性功能

    • 在SpringAOP中,功能最终以方法的形式呈现

    通知类:通知是一个方法,方法不能独立存在需要被写在一个类中,即通知类

    切面:是动作,把通知应用到切入点的过程,个切入点需要添加哪个通知,就需要提前将它们之间的关系描述清楚

5.2 AOP入门案例

使用SpringAOP的注解方式完成在方法执行前添加打印出当前系统时间的功能

5.2.1 环境准备
  • pom.xml添加Spring依赖

    <dependencies><dependency><groupId>org.springframework</groupId><artifactId>spring-context</artifactId><version>5.2.10.RELEASE</version></dependency>
    </dependencies>
    
  • 添加BookDao和BookDaoImpl类

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

    目前打印save方法的时候,因为方法中有打印系统时间,所以运行的时候是可以看到系统时间

    对于update方法来说,就没有该功能,我们要使用SpringAOP的方式在不改变update方法的前提下让其具有打印系统时间的功能。

  • 创建Spring的配置类

    @Configuration
    @ComponentScan("com.baidu")
    public class SpringConfig {
    }
    
  • 编写App运行类

    public class App {public static void main(String[] args) {ApplicationContext ctx = new AnnotationConfigApplicationContext(SpringConfig.class);BookDao bookDao = ctx.getBean(BookDao.class);bookDao.save();}
    }
    
5.2.2 实现步骤

步骤1:添加依赖

pom.xml

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

1630146885493

  • 因为spring-context中已经导入了spring-aop,所以不需要再单独导入spring-aop
  • 导入AspectJ的jar包;AspectJ是AOP思想的一个具体实现,Spring有自己的AOP实现,但是相比于AspectJ来说比较麻烦,所以我们直接采用Spring整合ApsectJ的方式进行AOP开发。

步骤2:定义接口与实现类

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

步骤3:定义通知类和通知

通知就是将共性功能抽取出来后形成的方法,共性功能指的就是当前系统时间的打印。

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

类名和方法名没有要求,可以任意。

步骤4:定义切入点

BookDaoImpl中有两个方法,分别是save和update,我们要增强的是update方法,该如何定义呢?

public class MyAdvice {@Pointcut("execution(void com.baidu.dao.BookDao.update())")private void pt(){}public void method(){System.out.println(System.currentTimeMillis());}
}

说明:

  • 切入点定义依托一个不具有实际意义的方法进行,即无参数、无返回值、方法体无实际逻辑
  • execution及后面编写的内容,指定了一个方法的签名,规定了哪些方法会被选定作为切入点。(后面会详细讲解)

步骤5:制作切面

切面是用来描述通知和切入点之间的关系,如何进行关系的绑定?

public class MyAdvice {@Pointcut("execution(void com.baidu.dao.BookDao.update())")private void pt(){}@Before("pt()")public void method(){System.out.println(System.currentTimeMillis());}
}

绑定切入点与通知关系,并指定通知添加到原始连接点的具体执行位置

1630148447689

说明:@Before翻译过来是之前,也就是说通知会在切入点方法执行之前执行,除此之前还有其他四种类型

切入点的定义切面的制作,都是在通知类中进行的。(因为不能改动原有设计代码

步骤6:将通知类配给容器并标识其为切面类

@Component
@Aspect
public class MyAdvice {@Pointcut("execution(void com.baidu.dao.BookDao.update())")private void pt(){}@Before("pt()")public void method(){System.out.println(System.currentTimeMillis());}
}

步骤7:开启注解格式AOP功能

@Configuration
@ComponentScan("com.baidu")
@EnableAspectJAutoProxy
public class SpringConfig {
}

步骤8:运行程序

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

执行结果:

image-20231222153732825

知识点1:@EnableAspectJAutoProxy

名称@EnableAspectJAutoProxy
类型配置类注解
位置配置类定义上方
作用开启注解格式AOP功能

知识点2:@Aspect

名称@Aspect
类型类注解
位置切面类定义上方
作用设置当前类为AOP切面类

知识点3:@Pointcut

名称@Pointcut
类型方法注解
位置切入点方法定义上方
作用设置切入点方法
属性value(默认):切入点表达式
定义切入点定义依托一个不具有实际意义的方法进行,即无参数、无返回值、方法体无实际逻辑

知识点4:@Before

名称@Before
类型方法注解
位置通知方法定义上方
作用设置当前通知方法与切入点之间的绑定关系,当前通知方法在原始切入点方法前运行

5.3 AOP工作流程

5.3.1 AOP工作流程

核心:代理模式

这得从Spring加载bean说起…

流程1: Spring容器启动

  • 容器启动就需要去加载bean,哪些类会被加载呢?
  • 需要被增强的类,如:BookServiceImpl
  • 通知类,如:MyAdvice
  • 注意此时bean对象还没有创建成功

流程2: 读取所有切面配置中的切入点

image-20231222163925610

  • 上面这个例子中有两个切入点的配置,但是第一个切入点定义时依托的方法ptx()并没有被使用,所以不会被读取。
  • 如何看有没有被使用,就看切面注解上有没有这个方法即可

流程3:初始化bean

判定bean对应的类中的方法是否匹配到任意切入点(流程2中已经读取了切面中所有的切入点)

  • 注意第1步在容器启动的时候,bean对象还没有被创建成功

  • 要对实例化bean对象的类中的方法和切入点进行匹配

    1630152538083

    • 匹配失败,创建原始对象, 如UserDao
      • 匹配失败说明不需要增强,直接调用原始对象的方法即可。
    • 匹配成功,创建原始对象(目标对象)的代理对象,如:BookDao
      • 匹配成功说明需要对其进行增强
      • 对哪个类做增强,这个类对应的对象就叫做目标对象
      • 因为要对目标对象进行功能增强,而采用的技术是动态代理,所以会为其创建一个代理对象
      • 最终运行的是代理对象的方法,在该方法中会对原始方法进行功能增强

流程4:获取bean执行方法

  • 获取的bean是原始对象时,调用方法并执行,完成操作
  • 获取的bean是代理对象时,根据代理对象的运行模式运行原始方法与增强的内容,完成操作
5.3.2 验证容器中是否为代理对象
  • 如果目标对象中的方法不被增强,那么容器中将存入的是目标对象本身
  • 如果目标对象中的方法会被增强,那么容器中将存入的是目标对象的代理对象
5.3.2.1 不被增强
@Component
@Aspect
public class MyAdvice {@Pointcut("execution(void com.baidu.dao.BookDao.update1())")private void pt(){}@Before("pt()")public void method(){System.out.println(System.currentTimeMillis());}
}

切入点中update1这个方法是不存在的

运行代码:

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

运行结果:

image-20231222170324805

5.3.2.2 被增强
@Component
@Aspect
public class MyAdvice {@Pointcut("execution(void com.baidu.dao.BookDao.update1())")private void pt(){}@Before("pt()")public void method(){System.out.println(System.currentTimeMillis());}
}

同样的运行代码运行出的结果:

image-20231222170438494

类型是代理类。

为什么打印出来的对象仍是BookDaoImpl?

因为Spring的AOP对其toString方法进行了重写,所以打印出来的对象会感觉是BookDaoImpl类型

5.3.3 AOP核心概念

在上面介绍AOP的工作流程中,我们提到了两个核心概念,分别是:

  • 目标对象(Target):原始功能去掉共性功能对应的类产生的对象,这种对象是无法直接完成最终工作的
  • 代理(Proxy):目标对象无法直接完成工作,需要对其进行功能回填,通过原始对象的代理对象实现

简单来说,

目标对象就是要增强的类[如:BookServiceImpl类]对应的对象,也叫原始对象,不能说它不能运行,只能说它在运行的过程中对于要增强的内容是缺失的。

SpringAOP是在不改变原有设计(代码)的前提下对其进行增强的,它的底层采用的是代理模式实现的,所以要对原始对象进行增强,就需要对原始对象创建代理对象,在代理对象中的方法把通知[如:MyAdvice中的method方法]内容加进去,就实现了增强,这就是我们所说的代理(Proxy)

5.4 AOP配置管理

5.4.1 AOP切入点表达式

image-20231222193450619

形如execution(void com.baidu.dao.BookDao.update())即为切入点表达式

对于切入点表达式,重点关注其语法格式通配符书写技巧

5.4.1.1 语法格式
  • 切入点: 要进行增强的方法

    因为调用接口方法的时候最终运行的还是其实现类的方法,所以有两种描述方式

    image-20231222194937691

    描述方式一:执行com.baidu.dao包下的BookDao接口中的无参数update方法

    execution(void com.baidu.dao.BookDao.update())
    

    描述方式二:执行com.baidu.dao.impl包下的BookDaoImpl类中的无参数update方法

    execution(void com.baidu.dao.impl.BookDaoImpl.update())
    
  • 切入点表达式:要进行增强的方法的描述方式

    对于**切入点表达式的语法**为:

    • 切入点表达式标准格式:动作关键字(访问修饰符 返回值 包名.类/接口名.方法名(参数) 异常名)

    对于这个格式,我们不需要硬记,通过一个例子,理解它:

    execution(public User com.baidu.service.UserService.findById(int))
    
    • execution:动作关键字,描述切入点的行为动作,例如execution表示执行到指定切入点
    • public: 访问修饰符,还可以是public,private等,可以省略
    • User:返回值,写返回值类型
    • com.baidu.service:包名,多级包使用点连接
    • UserService: 类/接口名称
    • findById:方法名
    • int: 参数,直接写参数的类型,多个类型用逗号隔开
    • 异常名:方法定义中抛出指定异常,可以省略

    但如果每一个方法对应一个切入点表达式,编写起来会比较麻烦,有没有更简单的方式呢?

    就需要用到下面的通配符

5.4.1.2 通配符

我们使用通配符描述切入点,主要的目的就是简化之前的配置

  • *:单个独立的任意符号,可以独立出现,也可以作为前缀或者后缀的匹配符出现

    execution(public * com.baidu.*.UserService.find*(*))
    

    匹配com.baidu包下的任意包中的UserService类或接口中所有find开头的带有一个参数的方法

  • ..:多个连续的任意符号,可以独立出现,常用于简化包名与参数的书写

    execution(public User com..UserService.findById(..))
    

    匹配com包下的任意包中的UserService类或接口中所有名称为findById的方法

  • +:专用于匹配子类类型

    execution(* *..*Service+.*(..))
    

    这个使用率较低,描述子类的,咱们做JavaEE开发,继承机会就一次,使用都很慎重,所以很少用它。*Service+,表示所有以Service结尾的接口的子类。

  • 实际案例分析

    image-20231222195748944

    execution(void com.baidu.dao.BookDao.update())
    匹配接口,能匹配到
    execution(void com.baidu.dao.impl.BookDaoImpl.update())
    匹配实现类,能匹配到
    execution(* com.baidu.dao.impl.BookDaoImpl.update())
    返回值任意,能匹配到
    execution(* com.baidu.dao.impl.BookDaoImpl.update(*))
    返回值任意,但是update方法必须要有一个参数,无法匹配,要想匹配需要在update接口和实现类添加参数
    execution(void com.*.*.*.*.update())
    返回值为void,com包下的任意包三层包下的任意类的update方法,匹配到的是实现类,能匹配
    execution(void com.*.*.*.update())
    返回值为void,com包下的任意两层包下的任意类的update方法,匹配到的是接口,能匹配
    execution(void *..update())
    返回值为void,方法名是update的任意包下的任意类,能匹配
    execution(* *..*(..))
    匹配项目中任意类的任意方法,能匹配,但是不建议使用这种方式,影响范围广
    execution(* *..u*(..))
    匹配项目中任意包任意类下只要以u开头的方法,update方法能满足,能匹配
    execution(* *..*e(..))
    匹配项目中任意包任意类下只要以e结尾的方法,update和save方法能满足,能匹配
    execution(void com..*())
    返回值为void,com包下的任意包任意类任意方法,能匹配,*代表的是方法
    execution(* com.baidu.*.*Service.find*(..))
    将项目中所有业务层方法的以find开头的方法匹配
    execution(* com.baidu.*.*Service.save*(..))
    将项目中所有业务层方法的以save开头的方法匹配
    

    后面两种更符合我们平常切入点表达式的编写规则

5.4.1.3 书写技巧

切入点表达式的编写其实是很灵活的,常用的书写技巧:(所有代码按照标准规范开发,否则以下技巧全部失效)

  • 描述切入点通常描述接口,而不描述实现类, 如果描述到实现类,就出现紧耦合了

  • 访问控制修饰符针对接口开发均采用public描述可省略访问控制修饰符描述

  • 返回值类型对于**增删改类使用精准类型加速匹配**,对于**查询类使用*通配快速描述**

  • 包名书写尽量不使用…匹配,效率过低,常用*做单个包描述匹配,或精准匹配

  • 接口名/类名书写名称与模块相关的采用*匹配,例如UserService书写成*Service,绑定业务层接口名

  • 方法名书写以动词进行精准匹配,名词采用匹配,例如getById书写成getBy,selectAll书写成selectAll

  • 参数规则较为复杂,根据业务方法灵活调整

  • 通常不使用异常作为匹配规则

    指定在目标方法抛出异常时执行的通知(advice)

    @Aspect
    @Component
    public class ExceptionAspect {@AfterThrowing(pointcut = "execution(* com.example.service.*.*(..))", throwing = "exception")public void handleException(Exception exception) {// 在方法抛出异常时执行此通知System.out.println("Exception caught: " + exception.getMessage());// 可以在这里执行异常处理逻辑,例如记录日志、发送通知等}
    }
    
5.4.2 AOP通知(增强)类型

回顾:

image-20231222201032116

它所代表的含义是将通知添加到切入点方法执行的前面

除了加在前面的类型,还有没有加在其他地方的类型?

5.4.2.1 类型介绍
  • 前置通知
  • 后置通知
  • 环绕通知(重点)
  • 返回后通知 (了解)
  • 抛出异常后通知 (了解)
image-20231222201320598

(1) 前置通知,追加功能到方法执行前,类似于在代码1或者代码2添加内容

(2) 后置通知, 追加功能到方法执行后,不管方法执行的过程中有没有抛出异常都会执行,类似于在代码5添加内容

(3) 返回后通知, 追加功能到方法执行后,只有方法正常执行结束后才进行,类似于在代码3添加内容,如果方法执行抛出异常,返回后通知将不会被添加

(4) 抛出异常后通知,追加功能到方法抛出异常后,只有方法执行出异常才进行,类似于在代码4添加内容,只有方法抛出异常后才会被添加

(5) 环绕通知, 环绕通知功能比较强大,它可以追加功能到方法执行的前后,这也是比较常用的方式它可以实现其他四种通知类型的功能

环境准备

<dependencies><dependency><groupId>org.springframework</groupId><artifactId>spring-context</artifactId><version>5.2.10.RELEASE</version></dependency><dependency><groupId>org.aspectj</groupId><artifactId>aspectjweaver</artifactId><version>1.9.4</version></dependency>
</dependencies>
public interface BookDao {public void update();public int select();
}@Repository
public class BookDaoImpl implements BookDao {public void update(){System.out.println("book dao update ...");}public int select() {System.out.println("book dao select is running ...");return 100;}
}
@Configuration
@ComponentScan("com.baidu")
@EnableAspectJAutoProxy
public class SpringConfig {
}
@Component
@Aspect
public class MyAdvice {@Pointcut("execution(void com.baidu.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 ...");}
}
public class App {public static void main(String[] args) {ApplicationContext ctx = new AnnotationConfigApplicationContext(SpringConfig.class);BookDao bookDao = ctx.getBean(BookDao.class);bookDao.update();}
}

通知类型的使用

前置通知

修改MyAdvice,在before方法上添加@Before注解

@Component
@Aspect
public class MyAdvice {@Pointcut("execution(void com.baidu.dao.BookDao.update())")private void pt(){}@Before("pt()")//此处也可以写成 @Before("MyAdvice.pt()"),不建议public void before() {System.out.println("before advice ...");}
}
后置通知
@Component
@Aspect
public class MyAdvice {@Pointcut("execution(void com.baidu.dao.BookDao.update())")private void pt(){}@Before("pt()")public void before() {System.out.println("before advice ...");}@After("pt()")public void after() {System.out.println("after advice ...");}
}

运行结果:

image-20231222203222198

环绕通知

试着编写这样的通知并调用:

@Component
@Aspect
public class MyAdvice {@Pointcut("execution(void com.baidu.dao.BookDao.update())")private void pt(){}@Around("pt()")public void around(){System.out.println("around before advice ...");System.out.println("around after advice ...");}
}

运行结果:

image-20231222203451723

结果:通知的内容打印出来,但是原始方法的内容却没有被执行,并且之前的前置,后置通知也没显示结果。

因为环绕通知需要在原始方法的前后进行增强,所以环绕通知就必须要能对原始操作进行调用

环绕通知示例如下:

@Component
@Aspect
public class MyAdvice {@Pointcut("execution(void com.baidu.dao.BookDao.update())")private void pt(){}@Around("pt()")public void around(ProceedingJoinPoint pjp) throws Throwable{System.out.println("around before advice ...");//表示对原始操作的调用pjp.proceed();System.out.println("around after advice ...");}
}

提示:proceed()为什么要抛出异常?

原因:查看源码

image-20231222203803058

运行结果:

image-20231222203829712

注意事项:

原始方法有返回值的处理

select方法带有一个int的返回值

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

对select方法添加环绕通知

@Component
@Aspect
public class MyAdvice {@Pointcut("execution(void com.baidu.dao.BookDao.update())")private void pt(){}@Pointcut("execution(int com.baidu.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 ...");}
}

修改App类,调用select方法

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

会报错:

image-20231222204501715

错误大概的意思是:空的返回不匹配原始方法的int返回

  • void就是返回Null
  • 原始方法就是BookDao下的select方法

所以如果我们使用环绕通知的话,要根据原始方法的返回值来设置环绕通知的返回值,具体解决方案为:

@Component
@Aspect
public class MyAdvice {@Pointcut("execution(void com.baidu.dao.BookDao.update())")private void pt(){}@Pointcut("execution(int com.baidu.dao.BookDao.select())")private void pt2(){}@Around("pt2()")public Object aroundSelect(ProceedingJoinPoint pjp) throws Throwable {System.out.println("around before advice ...");//表示对原始操作的调用Object ret = pjp.proceed();System.out.println("around after advice ...");return ret;}
}

说明:

  • 为什么返回的是Object而不是int的主要原因是Object类型更通用。

  • 在环绕通知中是可以对原始方法返回值就行修改的。

    如何修改?

    @Aspect
    @Component
    public class ModifyIntReturnValueAspect {@Around("execution(* com.example.service.*.*(..))")public Object modifyIntReturnValue(ProceedingJoinPoint joinPoint) throws Throwable {// 调用目标方法,并获取原始的返回值int originalReturnValue = (int) joinPoint.proceed();// 修改返回值,这里演示简单地加上一个固定的值int modifiedReturnValue = originalReturnValue + 10;// 返回修改后的返回值return modifiedReturnValue;}
    }
    

    Object是所有类的根类,因此它可以接受任何非基本数据类型(比如int)的对象。这是由于自动装箱(Autoboxing)的特性

返回后通知
@Component
@Aspect
public class MyAdvice {@Pointcut("execution(void com.baidu.dao.BookDao.update())")private void pt(){}@Pointcut("execution(int com.baidu.dao.BookDao.select())")private void pt2(){}@AfterReturning("pt2()")public void afterReturning() {System.out.println("afterReturning advice ...");}
}

运行结果:(这里注释掉了around,但保留了Before和After)

image-20231222205343169

注意:返回后通知是需要在原始方法select正常执行后才会被执行,如果select()方法执行的过程中出现了异常,那么返回后通知是不会被执行。后置通知是不管原始方法有没有抛出异常都会被执行。

异常后通知
@Component
@Aspect
public class MyAdvice {@Pointcut("execution(void com.baidu.dao.BookDao.update())")private void pt(){}@Pointcut("execution(int com.baidu.dao.BookDao.select())")private void pt2(){}@AfterThrowing("pt2()")public void afterThrowing() {System.out.println("afterThrowing advice ...");}
}
//可以在`select()`方法中添加一行代码`int i = 1/0`来产生异常

运行结果:

image-20231222205630860

环绕通知扩展

思考下环绕通知是如何实现其他通知类型的功能的:

因为环绕通知是**可以控制原始方法执行( pjp.proceed();)**的,所以我们把增强的代码写在调用原始方法的不同位置就可以实现不同的通知类型的功能

image-20231222210336665

通知类型总结

知识点1:@After

名称@After
类型方法注解
位置通知方法定义上方
作用设置当前通知方法与切入点之间的绑定关系,当前通知方法在原始切入点方法后运行

知识点2:@AfterReturning

名称@AfterReturning
类型方法注解
位置通知方法定义上方
作用设置当前通知方法与切入点之间绑定关系,当前通知方法在原始切入点方法正常执行完毕后执行

知识点3:@AfterThrowing

名称@AfterThrowing
类型方法注解
位置通知方法定义上方
作用设置当前通知方法与切入点之间绑定关系,当前通知方法在原始切入点方法运行抛出异常后执行

知识点4:@Around

名称@Around
类型方法注解
位置通知方法定义上方
作用设置当前通知方法与切入点之间的绑定关系,当前通知方法在原始切入点方法前后运行

环绕通知注意事项

  1. 环绕通知必须依赖形参ProceedingJoinPoint才能实现对原始方法的调用,进而实现原始方法调用前后同时添加通知
  2. 通知中如果未使用ProceedingJoinPoint对原始方法进行调用的话,将跳过原始方法的执行
  3. 对原始方法的调用可以不接收返回值,通知方法设置成void即可,如果接收返回值,最好设定为Object类型
  4. 原始方法的返回值如果是void类型,通知方法的返回值类型可以设置成void,也可以设置成Object
  5. 由于无法预知原始方法运行后是否会抛出异常,因此环绕通知方法必须要处理Throwable异常

介绍完这么多种通知类型,具体该选哪一种呢?

我们可以通过一些案例加深下对通知类型的学习。

5.4.3 AOP获取数据

通知(增强)中获取切入点的数据,如获取参数,获取返回值,获取异常

  • 获取切入点方法的参数,所有的通知类型都可以获取参数

    • JoinPoint类ProceedingJoinPoint类都是通知中函数参数的数据类型

      如:

      image-20231223133419121

    • JoinPoint:适用于前置、后置、返回后、抛出异常后通知

    • ProceedingJoinPoint:适用于环绕通知

  • 获取切入点方法返回值,前置和抛出异常后通知是没有返回值后置通知可有可无,所以不做研究

    • 回顾:为什么要获取返回值

      如果使用**返回后通知或者环绕通知,要根据原始方法的返回值来设置通知的返回值**,否则会报错

    • 返回后通知

    • 环绕通知

  • 获取切入点方法运行异常信息,前置和返回后通知是不会有,后置通知可有可无,所以不做研究

    • 抛出异常后通知
    • 环绕通知
5.4.3.1 获取参数

非环绕通知获取方式

在方法上添加JoinPoint,通过JoinPoint来获取参数

@Component
@Aspect
public class MyAdvice {@Pointcut("execution(* com.baidu.dao.BookDao.findName(..))")private void pt(){}@Before("pt()")public void before(JoinPoint jp) Object[] args = jp.getArgs();System.out.println(Arrays.toString(args));System.out.println("before advice ..." );}//...其他的略
}

运行方法:

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

运行结果:

image-20231223135619147

说明:

使用JoinPoint的方式获取参数适用于前置后置返回后抛出异常后通知

环绕通知获取方式

环绕通知使用的是ProceedingJoinPoint,因为ProceedingJoinPoint是JoinPoint类的子类,所以对于ProceedingJoinPoint类中应该也会有对应的getArgs()方法

@Component
@Aspect
public class MyAdvice {@Pointcut("execution(* com.baidu.dao.BookDao.findName(..))")private void pt(){}@Around("pt()")public Object around(ProceedingJoinPoint pjp)throws Throwable {Object[] args = pjp.getArgs();System.out.println(Arrays.toString(args));Object ret = pjp.proceed();return ret;}//其他的略
}

运行结果:

image-20231223135811314

注意:

  • pjp.proceed()方法是有两个构造方法,分别是:

    image-20231223140047684
    • 调用无参数的proceed,当原始方法有参数,会在调用的过程中自动传入参数

    • 所以调用这两个方法的任意一个都可以完成功能

    • 但是当需要修改原始方法的参数时,就只能采用带有参数的方法, 如下:

      @Component
      @Aspect
      public class MyAdvice {@Pointcut("execution(* com.baidu.dao.BookDao.findName(..))")private void pt(){}@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;}//其他的略
      }
      

      有了这个特性后,我们就可以在环绕通知中对原始方法的参数进行拦截过滤,避免由于参数的问题导致程序无法正确运行,保证代码的健壮性。

      例如,百度网盘在获取提取码时,会将传入的参数去掉一些空格之后再将其传入

5.4.3.2 获取返回值

对于返回值,只有返回后AfterReturing环绕Around这两个通知类型可以获取

返回后通知获取返回值

@Component
@Aspect
public class MyAdvice {@Pointcut("execution(* com.baidu.dao.BookDao.findName(..))")private void pt(){}@AfterReturning(value = "pt()",returning = "ret")public void afterReturning(Object ret) {System.out.println("afterReturning advice ..."+ret);}//其他的略
}

注意 :

(1) 参数名的问题

image-20231223140953860

(2) afterReturning方法参数类型的问题

参数类型可以写成String,但是为了能匹配更多的参数类型,建议写成Object类型

(3) afterReturning方法参数的顺序问题

image-20231223141114180

运行App后查看运行结果,说明返回值已经被获取到

image-20231223141343313

环绕通知获取返回值

@Component
@Aspect
public class MyAdvice {@Pointcut("execution(* com.baidu.dao.BookDao.findName(..))")private void pt(){}@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;}//其他的略
}

上述代码中,ret就是方法的返回值,我们是可以直接获取不但可以获取,如果需要还可以进行修改

5.4.3.3 获取异常

对于获取抛出的异常,只有抛出异常后AfterThrowing环绕Around这两个通知类型可以获取

环绕通知获取异常

catch方法中就可以获取到异常,至于获取到异常以后该如何处理,和业务需求有关

@Component
@Aspect
public class MyAdvice {@Pointcut("execution(* com.baidu.dao.BookDao.findName(..))")private void pt(){}@Around("pt()")public Object around(ProceedingJoinPoint pjp){Object[] args = pjp.getArgs();System.out.println(Arrays.toString(args));args[0] = 666;Object ret = null;try{ret = pjp.proceed(args);}catch(Throwable throwable){t.printStackTrace();}return ret;}//其他的略
}

抛出异常后通知获取异常

@Component
@Aspect
public class MyAdvice {@Pointcut("execution(* com.baidu.dao.BookDao.findName(..))")private void pt(){}@AfterThrowing(value = "pt()",throwing = "t")public void afterThrowing(Throwable t) {System.out.println("afterThrowing advice ..."+t);}//其他的略
}

注意:

image-20231223141834429

运行结果:

image-20231223141936247

5.5 AOP总结

AOP的知识就已经讲解完了,接下来对于AOP的知识进行一个总结:

5.5.1 AOP的核心概念
  • 概念:AOP(Aspect Oriented Programming)面向切面编程,一种编程范式
  • 作用:在不惊动原始设计的基础上为方法进行功能增强
  • 核心概念
    • 代理(Proxy):SpringAOP的核心本质是采用代理模式实现的
    • 连接点(JoinPoint):在SpringAOP中,理解为任意方法的执行
    • 切入点(Pointcut):匹配连接点的式子,切入点一定是连接点
    • 通知(Advice):若干个方法的共性功能,在切入点处执行,最终体现为一个方法
    • 切面(Aspect):描述通知与切入点的对应关系
    • 目标对象(Target):被代理的原始对象成为目标对象
5.5.2 切入点表达式
  • 切入点表达式标准格式:动作关键字(访问修饰符 返回值 包名.类/接口名.方法名(参数)异常名)

    execution(* com.baidu.service.*Service.*(..))
    
  • 切入点表达式描述通配符:

    • 作用:用于快速描述,范围描述
    • *:匹配任意符号(常用)
    • .. :匹配多个连续的任意符号(常用)
    • +:匹配子类类型
  • 切入点表达式书写技巧

    1. 标准规范开发

    2. 查询操作的返回值建议使用*匹配

    3. 减少使用…的形式描述包

    4. 对接口进行描述,使用*表示模块名,例如UserService的匹配描述为*Service

    5. 方法名书写保留动词,例如get,使用*表示名词,例如getById匹配描述为getBy*

    6. 参数根据实际情况灵活调整

5.5.3 五种通知类型
  • 前置通知
  • 后置通知
  • 环绕通知(重点)
    • 环绕通知依赖形参ProceedingJoinPoint才能实现对原始方法的调用
    • 环绕通知可以隔离原始方法的调用执行(即不使用ProceedingJoinPoint进行对原始方法的调用)
    • 环绕通知返回值设置为Object类型
    • 环绕通知中可以对原始方法调用过程中出现的异常进行处理
  • 返回后通知
  • 抛出异常后通知
5.5.4 通知中获取参数
  • 获取切入点方法的参数,所有的通知类型都可以获取参数
    • JoinPoint:适用于前置、后置、返回后、抛出异常后通知
    • ProceedingJoinPoint:适用于环绕通知
  • 获取切入点方法返回值,前置和抛出异常后通知是没有返回值,后置通知可有可无,所以不做研究
    • 返回后通知
    • 环绕通知
  • 获取切入点方法运行异常信息,前置和返回后通知是不会有,后置通知可有可无,所以不做研究
    • 抛出异常后通知
    • 环绕通知

5.6 AOP事务管理

5.6.1 Spring事务简介
  • 事务作用:在数据层保障一系列的数据库操作同成功同失败

  • Spring事务作用:在**数据层业务层保障一系列的数据库操作同成功同失败**

    数据层有事务我们可以理解,为什么业务层也需要处理事务呢?

    因为会出现这种情况**:业务层中有事务,事务是在数据层中,不同事务对应着各自的数据操作,要保障一系列的数据库操作同成功同失败,最终也就是要保障业务层一系列的数据库操作同成功同失败**

  • Spring为了管理事务,提供了一个平台事务管理器PlatformTransactionManager

    image-20231223204510766

    commit是用来提交事务,rollback是用来回滚事务。

    PlatformTransactionManager只是一个接口,Spring还为其提供了一个具体的实现:

    image-20231223204544532

    从名称上可以看出,我们只需要给它一个DataSource对象,它就可以帮你去在业务层管理事务。

    其内部采用的是JDBC的事务

    所以说如果你持久层采用的是JDBC相关的技术就可以采用这个事务管理器来管理你的事务

    Mybatis内部采用的就是JDBC的事务,所以后期我们Spring整合Mybatis就采用的这个DataSourceTransactionManager事务管理器。

    持久层(Persistence Layer)是指应用程序中负责处理数据持久化(即数据在应用程序数据库之间的存储和检索)的部分

    即常见的Dao层

5.6.1.1 转账案例

需求: 实现任意两个账户间转账操作

需求微缩: A账户减钱,B账户加钱

实现步骤:

①:数据层提供基础操作,指定账户减钱(outMoney),指定账户加钱(inMoney)

②:业务层提供转账操作(transfer),调用减钱与加钱的操作

③:提供2个账号和操作金额执行转账操作

④:基于Spring整合MyBatis环境搭建上述操作

image-20231223205707827

public interface AccountDao {@Update("update tbl_account set money = money + #{money} where name = #{name}")void inMoney(@Param("name") String name, @Param("money") Double money);@Update("update tbl_account set money = money - #{money} where name = #{name}")void outMoney(@Param("name") String name, @Param("money") Double money);
}

正常情况:程序正常执行时,账户金额A减B加。

引入:如果在转账的过程中出现了异常,如:

@Service
public class AccountServiceImpl implements AccountService {@Autowiredprivate AccountDao accountDao;public void transfer(String out,String in ,Double money) {accountDao.outMoney(out,money);int i = 1/0;accountDao.inMoney(in,money);}}

出现的问题金额减少之后并没有相关账户金额的增加,(如果交换执行顺序,相关账户金额的增加但没有账户金额减少)

程序出现异常后,转账失败,但是异常之前操作成功,异常之后操作失败,整体业务失败

当程序出问题后,我们需要让事务进行回滚,而且这个事务应该是加在业务层Spring的事务管理就是用来解决这类问题的

Spring事务管理具体的实现步骤为:

步骤1:在需要被事务管理的方法上添加注解 @Transactional

public interface AccountService {/*** 转账操作* @param out 传出方* @param in 转入方* @param money 金额*///配置当前接口方法具有事务public void transfer(String out,String in ,Double money) ;
}@Service
public class AccountServiceImpl implements AccountService {@Autowiredprivate AccountDao accountDao;@Transactionalpublic void transfer(String out,String in ,Double money) {accountDao.outMoney(out,money);int i = 1/0;accountDao.inMoney(in,money);}}

注意:

@Transactional可以写在接口类上、接口方法上、实现类上和实现类方法

  • 写在接口类上,该接口的**所有实现类的所有方法**都会有事务
  • 写在接口方法上,该接口的**所有实现类的该方法**都会有事务
  • 写在实现类上,该**类中的所有方法**都会有事务
  • 写在实现类方法上,该方法上有事务
  • 建议写在实现类或实现类的方法上

步骤2:在JdbcConfig类中配置事务管理器 (并注入Spring容器中@Bean)

public class JdbcConfig {@Value("${jdbc.driver}")private String driver;@Value("${jdbc.url}")private String url;@Value("${jdbc.username}")private String userName;@Value("${jdbc.password}")private String password;@Beanpublic DataSource dataSource(){DruidDataSource ds = new DruidDataSource();ds.setDriverClassName(driver);ds.setUrl(url);ds.setUsername(userName);ds.setPassword(password);return ds;}//配置事务管理器,mybatis使用的是jdbc事务@Beanpublic PlatformTransactionManager transactionManager(DataSource dataSource){DataSourceTransactionManager transactionManager = new DataSourceTransactionManager();transactionManager.setDataSource(dataSource);return transactionManager;}
}

注意:事务管理器要根据使用技术进行选择,Mybatis框架使用的是JDBC事务,可以直接使用DataSourceTransactionManager

步骤3:开启事务注解 @EnableTransactionManagement

在SpringConfig的配置类中开启

@Configuration
@ComponentScan("com.baidu")
@PropertySource("classpath:jdbc.properties")
@Import({JdbcConfig.class,MybatisConfig.class
//开启注解式事务驱动
@EnableTransactionManagement
public class SpringConfig {
}

步骤4:运行测试类

在测试类中测试:

@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(classes = SpringConfig.class)
public class AccountServiceTest {@Autowiredprivate AccountService accountService;@Testpublic void testTransfer() throws IOException {accountService.transfer("Tom","Jerry",100D);}}

会发现在转换的业务出现错误后,事务就可以控制回滚,保证数据的正确性

知识点1:@EnableTransactionManagement

名称@EnableTransactionManagement
类型配置类注解
位置配置类定义上方
作用设置当前Spring环境中开启注解式事务支持

知识点2:@Transactional

名称@Transactional
类型接口注解 类注解 方法注解
位置业务层接口上方 业务层实现类上方 业务方法上方
作用为当前业务层方法添加事务(如果设置在类或接口上方则类或接口中所有方法均添加事务)
5.6.2 Spring事务角色

两个角色:分别是事务管理员事务协调员

  • 事务管理员:发起事务方,在Spring中通常指代业务层开启事务的方法
  • 事务协调员:加入事务方,在Spring中通常指代数据层方法,也可以是业务层方法
5.6.2.1 未开启Spring事务之前

未开启Spring事务之前(也就是如果在转账的过程中出现了异常,会导致数据不一致时):

image-20231223214718930

  • AccountDao的outMoney因为是修改操作,会开启一个事务T1
  • AccountDao的inMoney因为是修改操作,会开启一个事务T2
  • AccountService的transfer没有事务
    • 运行过程中如果没有抛出异常,则T1和T2都正常提交,数据正确
    • 如果在两个方法中间抛出异常,T1因为执行成功提交事务,T2因为抛异常不会被执行
    • 就会导致数据出现错误
5.6.2.2 开启了Spring事务之后

image-20231223214921431

  • transfer上添加了@Transactional注解,在该方法上就会有一个事务T
  • AccountDao的outMoney方法的**事务T1加入到transfer的事务T中**
  • AccountDao的inMoney方法的**事务T2加入到transfer的事务T中**
  • 这样就保证他们在同一个事务中,当业务层中出现异常,整个事务就会回滚,保证数据的准确性。

注意:

目前的事务管理是基于DataSourceTransactionManager(用于管理数据库事务)SqlSessionFactoryBean(配置和创建 MyBatis 的 SqlSessionFactory 的工厂Bean)使用的是同一个数据源

回顾,数据源在Spring中的创建过程

@Bean
public DataSource dataSource(){DruidDataSource ds = new DruidDataSource();ds.setDriverClassName(driver);ds.setUrl(url);ds.setUsername(userName);ds.setPassword(password);return ds;
}
5.6.3 Spring事务属性
  • 事务的管理员和事务的协同员,这两个概念具体做什么?
  • 除了这两个概念,事务的其他相关配置都有哪些?
5.6.3.1 事务配置

image-20231224140916112

上面这些属性都可以在@Transactional注解的参数上进行设置。

  • readOnly:true只读事务,false读写事务增删改要设为false, 查询设为true。

  • timeout: 设置超时时间单位秒,在多长时间之内事务没有提交成功就自动回滚,-1表示不设置超时时间。

  • rollbackFor: 当出现指定异常进行事务回滚(即也侧面说明了并不是所有的异常都会回滚事务)

    Spring的事务只会对Error异常和RuntimeException异常及其子类进行事务回顾,其他的异常类型是不会回滚的;如IOException不符合条件,不会回滚

    @Service
    public class AccountServiceImpl implements AccountService {@Autowiredprivate AccountDao accountDao;@Transactionalpublic void transfer(String out,String in ,Double money) throws IOException{accountDao.outMoney(out,money);//int i = 1/0; //这个异常事务会回滚if(true){throw new IOException(); //这个异常事务就不会回滚}accountDao.inMoney(in,money);}
    }
    

    运行上面的事务,会发现,虽然抛出了错误,但是数据库的数据仍然发生了变化

    此时就可以使用rollbackFor属性来设置出现IOException异常产生回滚操作

    @Service
    public class AccountServiceImpl implements AccountService {@Autowiredprivate AccountDao accountDao;@Transactional(rollbackFor = {IOException.class})public void transfer(String out,String in ,Double money) throws IOException{accountDao.outMoney(out,money);//int i = 1/0; //这个异常事务会回滚if(true){throw new IOException(); //这个异常事务就不会回滚}accountDao.inMoney(in,money);}
    }
    

    结果为:抛出了IOException的异常,并且数据库的数据没有发生变化

  • noRollbackFor:当出现指定异常进行事务回滚

  • rollbackForClassName等同于rollbackFor,只不过属性为异常的类全名字符串

  • noRollbackForClassName等同于noRollbackFor,只不过属性为异常的类全名字符串

  • isolation设置事务的隔离级别(一个事务的执行不受其他事务的影响程度)

    • DEFAULT: 默认隔离级别, 会采用数据库的隔离级别

    • READ_UNCOMMITTED : 读未提交(允许一个事务读取另一个事务未提交的数据)

    • READ_COMMITTED : 读已提交(保证一个事务不会读取到另一个事务未提交的数据)

    • REPEATABLE_READ : 重复读取(保证一个事务在执行期间多次读取相同的数据时,会得到相同的结果)

    • SERIALIZABLE: 串行化

      串行化:

      • 最高的隔离级别,确保事务之间的完全隔离
      • 避免了脏读、不可重复读和幻读,但可能导致性能下降,因为事务需要等待锁的释放。
5.6.3.2 事务传播行为

需求引入:

在前面的转案例的基础上添加新的需求,完成转账后记录日志。

  • 需求:实现任意两个账户间转账操作,并对每次转账操作在数据库进行留痕
  • 需求微缩:A账户减钱,B账户加钱,数据库记录日志,无论转账操作是否成功,均进行转账操作的日志留痕

分析:

在业务层转账操作(transfer),调用减钱、加钱与记录日志功能

环境准备:

步骤1: 创建日志表

create table tbl_log(id int primary key auto_increment,info varchar(255),createDate datetime
)

步骤2: 添加LogDao接口

now() 是一个数据库函数,用于获取当前的日期和时间

public interface LogDao {@Insert("insert into tbl_log (info,createDate) values(#{info},now())")void log(String info);
}

步骤3: 添加LogService接口与实现类

public interface LogService {void log(String out, String in, Double money);
}
@Service
public class LogServiceImpl implements LogService {@Autowiredprivate LogDao logDao;@Transactionalpublic void log(String out,String in,Double money ) {logDao.log("转账操作由"+out+"到"+in+",金额:"+money);}
}

步骤4: 在转账的业务中添加记录日志

public interface AccountService {/*** 转账操作* @param out 传出方* @param in 转入方* @param money 金额*///配置当前接口方法具有事务public void transfer(String out,String in ,Double money)throws IOException ;
}
@Service
public class AccountServiceImpl implements AccountService {@Autowiredprivate AccountDao accountDao;@Autowiredprivate LogService logService;@Transactionalpublic void transfer(String out,String in ,Double money) {try{accountDao.outMoney(out,money);accountDao.inMoney(in,money);}finally {logService.log(out,in,money);}}
}

步骤5: 运行程序,发现问题

  • 当转账业务之间出现异常(int i =1/0),转账失败,tbl_account成功回滚,但是tbl_log表未添加数据
  • 失败原因:日志的记录与转账操作隶属同一个事务,同成功同失败

  • log方法inMoney方法和outMoney方法都属于增删改,分别有事务T1,T2,T3
  • transfer因为加了@Transactional注解,也开启了事务T
  • 前面我们讲过Spring事务会把T1,T2,T3都加入到事务T中
  • 所以当转账失败后,所有的事务都回滚,导致日志没有记录下来
  • 这和我们的需求不符,我们希望log方法单独是一个事务
5.6.3.2.1 事务传播行为

事务传播行为:事务协调员事务管理员所携带事务的处理态度。需要用到之前我们没有说的propagation属性

image-20231224162126444

代码修改:

@Service
public class LogServiceImpl implements LogService {@Autowiredprivate LogDao logDao;//propagation设置事务属性:传播行为设置为当前操作需要新事务@Transactional(propagation = Propagation.REQUIRES_NEW)public void log(String out,String in,Double money ) {logDao.log("转账操作由"+out+"到"+in+",金额:"+money);}
}

运行后,就能实现我们想要的结果,不管转账是否成功,都会记录日志。

image-20231224163428804

5.6.3.2.2 事务传播行为属性说明

image-20231224163445617

对于我们开发实际中使用的话,因为默认值需要事务是常态的。

根据开发过程选择其他的就可以了,例如案例中需要新事务就需要手工配置。

其实入账和出账操作上也有事务,采用的就是默认值。

image-20231224163538834

获取当前的日期和时间**

public interface LogDao {@Insert("insert into tbl_log (info,createDate) values(#{info},now())")void log(String info);
}

步骤3: 添加LogService接口与实现类

public interface LogService {void log(String out, String in, Double money);
}
@Service
public class LogServiceImpl implements LogService {@Autowiredprivate LogDao logDao;@Transactionalpublic void log(String out,String in,Double money ) {logDao.log("转账操作由"+out+"到"+in+",金额:"+money);}
}

步骤4: 在转账的业务中添加记录日志

public interface AccountService {/*** 转账操作* @param out 传出方* @param in 转入方* @param money 金额*///配置当前接口方法具有事务public void transfer(String out,String in ,Double money)throws IOException ;
}
@Service
public class AccountServiceImpl implements AccountService {@Autowiredprivate AccountDao accountDao;@Autowiredprivate LogService logService;@Transactionalpublic void transfer(String out,String in ,Double money) {try{accountDao.outMoney(out,money);accountDao.inMoney(in,money);}finally {logService.log(out,in,money);}}
}

步骤5: 运行程序,发现问题

  • 当转账业务之间出现异常(int i =1/0),转账失败,tbl_account成功回滚,但是tbl_log表未添加数据
  • 失败原因:日志的记录与转账操作隶属同一个事务,同成功同失败

  • log方法inMoney方法和outMoney方法都属于增删改,分别有事务T1,T2,T3
  • transfer因为加了@Transactional注解,也开启了事务T
  • 前面我们讲过Spring事务会把T1,T2,T3都加入到事务T中
  • 所以当转账失败后,所有的事务都回滚,导致日志没有记录下来
  • 这和我们的需求不符,我们希望log方法单独是一个事务
5.6.3.2.1 事务传播行为

事务传播行为:事务协调员事务管理员所携带事务的处理态度。需要用到之前我们没有说的propagation属性

image-20231224162126444

代码修改:

@Service
public class LogServiceImpl implements LogService {@Autowiredprivate LogDao logDao;//propagation设置事务属性:传播行为设置为当前操作需要新事务@Transactional(propagation = Propagation.REQUIRES_NEW)public void log(String out,String in,Double money ) {logDao.log("转账操作由"+out+"到"+in+",金额:"+money);}
}

运行后,就能实现我们想要的结果,不管转账是否成功,都会记录日志。

[外链图片转存中…(img-wVRbTG7l-1703410601946)]

5.6.3.2.2 事务传播行为属性说明

image-20231224163445617

对于我们开发实际中使用的话,因为默认值需要事务是常态的。

根据开发过程选择其他的就可以了,例如案例中需要新事务就需要手工配置。

其实入账和出账操作上也有事务,采用的就是默认值。

在这里插入图片描述

image-20231224163658888

相关文章:

Spring(3)Spring从零到入门 - Spring整合技术及AOP事务管理

Spring&#xff08;3&#xff09;Spring从零到入门 - Spring整合技术及AOP事务管理 文章目录 Spring&#xff08;3&#xff09;Spring从零到入门 - Spring整合技术及AOP事务管理4 Spring整合技术示例4.1 Spring整合Mybatis4.1.1 Mybatis开发回顾4.1.2 整合Spring分析4.1.3 Spri…...

适配器模式学习

适配器模式&#xff08;Adapter&#xff09;将一个类的接口转换成客户希望的另外一个接口。Adapter 模式使得原本由于接口不兼容而不能一起工作的那些类可以一起工作。 适配器模式分为类适配器模式和对象适配器模式两种&#xff0c;前者类之间的耦合度比后者高&#xff0c;且要…...

NET中使用Identity+CodeFirst+Jwt实现登录、鉴权

目录 前言 一、创建上下文类 1.自定义MyContext上下文类继承IdentityDbContext 2.在Program中添加AddDbContext服务 二、使用Migration数据迁移 1.在控制台中 依次使用add-migration 、updatebase 命令 2.如何修改表名 3.如何自定义字段 三、使用Identity实现登录、修改密码 …...

详解Keras3.0 API: Optimizers

Optimizers 优化器&#xff08;Optimizer&#xff09;是深度学习中用于更新模型参数的一种方法&#xff0c;它的目标是最小化损失函数。在训练神经网络时&#xff0c;我们通常使用梯度下降法来更新参数&#xff0c;而优化器就是实现这一过程的工具。优化器的主要作用是在每次迭…...

【数据结构】字符串匹配|BF算法|KMP算法|next数组的优化

字符串匹配算法是在实际工程中经常遇到的问题&#xff0c;也是各大公司笔试面试的常考题目&#xff0c;本文主要介绍BF算法&#xff08;最好想到的算法&#xff0c;也最好实现&#xff09;和KMP算法&#xff08;最经典的&#xff09; 一、BF算法 BF算法&#xff0c;即暴力(Bru…...

阿里云 ACK One 新特性:多集群网关,帮您快速构建同城容灾系统

云布道师 近日&#xff0c;阿里云分布式云容器平台 ACK One[1]发布“多集群网关”[2]&#xff08;ACK One Multi-cluster Gateways&#xff09;新特性&#xff0c;这是 ACK One 面向多云、多集群场景提供的云原生网关&#xff0c;用于对多集群南北向流量进行统一管理。 基于 …...

vscode自定义代码片段

前言 代码片段&#xff0c;指的是能够帮助输入重复代码模式&#xff0c;比如初始页面的模板。通过 snippet &#xff0c;我们仅仅输入一小段字符串&#xff0c;就可以在代码片引擎的帮助下&#xff0c;生成预定义的模板代码&#xff0c;接着我们还可以通过在预定义的光标位置之…...

【贪心算法】专题练习一

欢迎来到Cefler的博客&#x1f601; &#x1f54c;博客主页&#xff1a;那个传说中的man的主页 &#x1f3e0;个人专栏&#xff1a;题目解析 &#x1f30e;推荐文章&#xff1a;题目大解析&#xff08;3&#xff09; 前言 1.什么是贪心算法&#xff1f;——贪婪鼠目寸光 贪心策…...

【JMeter】使用nmon进行性能资源监控

一、前言 ​ 在工作中可能会遇到需要在压测的时候对Linux服务器进行性能资源监控的情况。这时可以用nmon来对服务器进行监控。 二、nmon的下载安装 1.查看系统信息 shell cat /etc/os-release 结果为 shell PRETTY_NAME"Debian GNU/Linux 12 (bookworm)" NAME&…...

Unity预设体

目录 预设体是什么&#xff1f; 如何创建预设体&#xff1f; 如何修改预设体&#xff1f; 如何删除预设体&#xff1f; 预设体是什么&#xff1f; Unity中的预设体&#xff08;Prefab&#xff09;是一种可重复使用的游戏对象模板。它允许开发者创建一个或多个游戏对象&…...

Elasticsearch 写入优化探索:是什么影响了refresh 耗时?

1、问题背景&#xff1a; 数据写入后&#xff0c;refresh耗时过长&#xff0c;能达到1s-5s。 想通过测试&#xff0c;探索确认影响refresh的因素&#xff0c;比如&#xff1a;写入操作是新增还是更新&#xff0c;deleted文档占比是否有影响&#xff0c;是否有其他索引配置&…...

Java8新特性——函数式接口

目录 一、介绍 二、示例 &#xff08;一&#xff09;Consumer 源码解析 测试示例 &#xff08;二&#xff09;Comparator &#xff08;三&#xff09;Predicate 三、应用 四、总结 一、介绍 FunctionalInterface是一种信息注解类型&#xff0c;用于指明接口类型声明…...

Epson打印机连接wifi

环境 Epson L3153 打印机联通无线光猫 背景 最近家里的联通宽带不太稳定&#xff0c;经常断网。今天打了联通客服电话&#xff0c;师傅上门来&#xff0c;说可能是光猫用的时间太长了&#xff0c;换了一个新的联通光猫&#xff0c;问题解决。 wifi的名称是 CU_Y3ft 和 CU_Y3…...

Chapter 7 - 6. Congestion Management in Ethernet Storage Networks以太网存储网络的拥塞管理

Dedicated and Converged Ethernet Network专用和融合以太网网络 Just because a network is configured as a converged Ethernet network (lossy and lossless traffic), doesn’t necessarily mean that lossy and lossless traffic runs on it simultaneously. For exampl…...

【论文笔记】NeuRAD: Neural Rendering for Autonomous Driving

原文链接&#xff1a;https://arxiv.org/abs/2311.15260 1. 引言 神经辐射场&#xff08;NeRF&#xff09;应用在自动驾驶中&#xff0c;可以创建可编辑的场景数字克隆&#xff08;可自由编辑视角和场景物体&#xff09;&#xff0c;以进行仿真。但目前的方法或者需要大量的训…...

通信原理 | 分贝dB、功率、功率谱、功率谱密度、信噪比

文章目录 分贝功率和分贝的关系能量谱功率谱功率谱和功率谱密度是不同的功率谱密度随机信号和确知信号信噪比基本定义分贝表示应用分贝 分贝:(用dB表示)是量度两个相同单位之间数量比例的计量单位,主要用于度量声音强度。 1贝尔(B)=10分布(dB),即1B = 10dB 分贝是以美国…...

Go中的Context是什么?

在 Go 编程语言&#xff08;通常称为 Golang&#xff09;中&#xff0c;术语 "上下文 "指的是上下文包及其定义的上下文类型。上下文包用于跨 API 边界和进程间传输截止日期、取消信号和其他请求范围值。 上下文包的主要目的是管理并发或分布式系统中操作的生命周期…...

碳排放预测 | 基于ARIMA和GM(1,1)的碳排放预测(Matlab)

目录 预测效果基本介绍模型描述ARIMA模型GM(1,1)模型 程序设计参考资料 预测效果 基本介绍 基于ARIMA和GM(1,1)的碳排放预测&#xff08;Matlab&#xff09; 基于ARIMA&#xff08;自回归移动平均模型&#xff09;和GM(1,1)&#xff08;灰色预测模型&#xff09;的碳排放预测是…...

FPFA.一种二倍频电路代码描述以及测量详情

一、前言 1、因为需要倍频电路所以找了个二倍频的电路&#xff0c;通过fpga实际测量发现经过倍频后的电路峰值降低。不过这个也正常&#xff0c;因为该电路只要过触发点就会开始发生波形变化&#xff0c;而电路的触发值不是峰值。​​​​​​​ 2、继续对电路做倍频后信号做二…...

dotnet命令创建C#项目,VSCode打开

在命令行中创建项目并运行 1.首先安装.net 下载地址:.NET | 构建。测试。部署。 2.在 cmd 控制台输入 dotnet --vesion 检查版本号是否正常 3.我用git bash环境输入命令创建项目 // 创建文件夹 mkdir MyVSCode // 进入该文件夹 cd MyVSCode/ // 创建控制台项目 dotnet …...

在GitHub找开源项目

在 GitHub 的搜索框里&#xff1a; 使用搜索关键词可以在 GitHub 上快速的找你需要的开源项目&#xff1a; 限制搜索范围 通过 in 关键词 (大小写不敏感) 限制搜索范围&#xff1a; 公式搜索范围in:name xxx项目名包含xxxin:description xxx项目描述包含xxxin:readme xxx项目…...

GAMES101-LAB1

文章目录 一、问题简述二、框架准备三、作业参考3.1 模型矩阵3.1 参考代码 3.2 投影矩阵3.2.1 压扁操作(透视投影)3.2.2 正交投影3.2.3 参考代码 四、附件 一、问题简述 接下来的三次作业&#xff0c;将模拟一个基于CPU的光栅化渲染器的简化版本本次作业的任务是实现一个旋转矩…...

Docker 编译OpenHarmony 4.0 release

一、背景介绍 1.1、环境配置 编译环境&#xff1a;Ubuntu 20.04OpenHarmony版本&#xff1a;4.0 release平台设备&#xff1a;RK3568 OpenHarmony 3.2更新至OpenHarmony 4.0后&#xff0c;公司服务器无法编译通过&#xff0c;总是在最后几十个文件时报错,错误码4000&#xf…...

Vue 3 表单处理精讲:打造响应式注册表单的艺术

&#x1f9d9;‍♂️ 诸位好&#xff0c;吾乃诸葛妙计&#xff0c;编程界之翘楚&#xff0c;代码之大师。算法如流水&#xff0c;逻辑如棋局。 &#x1f4dc; 吾之笔记&#xff0c;内含诸般技术之秘诀。吾欲以此笔记&#xff0c;传授编程之道&#xff0c;助汝解技术难题。 &…...

浅谈Guava Cache的参数使用

CacheLoader 用于数据加载方式比较固定且统一的场景&#xff0c;在缓存容器创建的时候就需要指定此具体的加载逻辑。通常开发中使用时我们需要继承CacheLoader类或写一个匿名实现类实现其load方法和reload方法 load方法 当执行get操作没有命中缓存或者判断缓存已经超出expir…...

交通流预测 | Matlab基于KNN-BiLSTM的交通流预测(对比SVR、LSTM、GRU、KNN-LSTM)

目录 预测效果基本介绍程序设计参考资料 预测效果 基本介绍 交通流预测 | Matlab基于KNN-BiLSTM的交通流预测&#xff08;对比SVR、LSTM、GRU、KNN-LSTM&#xff09; 程序设计 完整程序和数据获取方式&#xff1a;私信博主回复Matlab基于KNN-BiLSTM的交通流预测&#xff08;对…...

云卷云舒:面向业务的智能运维(上)

1、BAIOPS-业务智能运维 智能运维&#xff08;AIOps-Algorithmic IT Operations基于算法的IT运维&#xff09;是人工智能技术在IT运维领域的运用&#xff0c;引用Gartner 的报告的一段话“到2020年&#xff0c;将近50%的企业将会在他们的业务和IT运维方面采用AIOps&#xff0c…...

centos 7.4 docker

centos 7.4 docker 1.查看系统版本 cat /etc/redhat-release CentOS Linux release 7.4.1708 (Core)哈1 cat /etc/redhat-release CentOS Linux release 7.9.2009 (Core) 哈1-02 cat /etc/redhat-release CentOS Linux release 7.6.1810 (Core) 哈1-22 cat /etc/redhat…...

零基础学人工智能:TensorFlow 入门例子

识别手写图片 因为这个例子是 TensorFlow 官方的例子&#xff0c;不会说的太详细&#xff0c;会加入了一点个人的理解&#xff0c;因为TensorFlow提供了各种工具和库&#xff0c;帮助开发人员构建和训练基于神经网络的模型。TensorFlow 中最重要的概念是张量&#xff08;Tenso…...

go从0到1项目实战体系二一:gin框架安装

(1). 设置公用的代理服务地址: 如果设置了全局可忽略. $ export GOPROXYhttps://goproxy.io // linux > go env可以查看 $ export GOPROXYhttps://goproxy.cn // linux国内镜像 $ set GOPROXYhttps://goproxy.io // windows(2). 创建以下目录: 请忘记GOPATH目录…...

运用JavaSE知识实现图书管理系统

目录 一.Main函数二.用户类三.普通用户类四.管理员类五.图书类六.书架类七.操作类1.操作接口2.增加操作3.删除操作4.查找操作5.展示操作6.借阅操作7.归还操作8.退出系统 总结 这篇图书管理系统是对JavaSE知识总结复习的一个小作业&#xff0c;检测自己对知识的掌握程度。 一.Ma…...

微信小程序生成一个天气查询的小程序

微信小程序生成一个天气查询的小程序 基本的页面结构和逻辑 页面结构&#xff1a;包括一个输入框和一个查询按钮。 页面逻辑&#xff1a;在用户输入城市名称后&#xff0c;点击查询按钮&#xff0c;跳转到天气详情页面&#xff0c;并将城市名称作为参数传递。 主要代码 index…...

Seata源码——TCC模式解析02

初始化 在SpringBoot启动的时候通过自动注入机制将GlobalTransactionScanner注入进ioc而GlobalTransactionScanner继承AbstractAutoProxyCreatorAbstract 在postProcessAfterInitialization阶段由子类创建代理TccActionInterceptor GlobalTransactionScanner protected Obje…...

缓存-Redis

Springboot使用Redis 引入pom依赖&#xff1a; <dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-data-redis</artifactId> </dependency>在application.yml、application-dev.yml中配置Redis的访…...

PADS Layout安全间距检查报错

问题&#xff1a; 在Pads Layout完成layout后&#xff0c;进行工具-验证设计安全间距检查时&#xff0c;差分对BAK_FIXCLK_100M_P / BAK_FIXCLK_100M_N的安全间距检查报错&#xff0c;最小为3.94mil&#xff0c;但是应该大于等于5mil&#xff1b;如下两张图&#xff1a; 检查&…...

ebpf基础篇(二) ----- ebpf前世今生

bpf 要追述ebpf的历史,就不得不提bpf. bpf(Berkeley Packet Filter)从早(1992年)诞生于类Unix系统中,用于数据包分析. 它提供了数据链路层的接口,可以在数据链路层发送和接收数据.如果网卡支持混杂模式,所有的数据包都可以被接收,即使这些数据包的目的地址是其它主机. BPF最为…...

我的一天:追求专业成长与生活平衡

早晨的序幕&#xff1a;奋斗的开始 今天的一天始于清晨的6点47分。实现了昨天的早睡早起的蜕变计划。洗漱完成之后&#xff0c;7点17分出门&#xff0c;7点33分我抵达公司&#xff0c;为新的一天做好准备。7点52分&#xff0c;我开始我的学习之旅。正如我所体会的&#xff0c;“…...

【动态规划】斐波那契数列模型

欢迎来到Cefler的博客&#x1f601; &#x1f54c;博客主页&#xff1a;那个传说中的man的主页 &#x1f3e0;个人专栏&#xff1a;题目解析 &#x1f30e;推荐文章&#xff1a;题目大解析&#xff08;3&#xff09; 前言 算法原理 1.状态表示 是什么&#xff1f;dp表(一维数组…...

机器人运动学分析与动力学分析主要作用

机器人运动学分析和动力学分析是两个重要的概念&#xff0c;它们在研究和设计工业机器人时起着关键作用。 1. 机器人运动学分析&#xff1a; 机器人运动学是研究机器人运动的科学&#xff0c;它涉及机器人的位置、速度、加速度和轨迹等方面。机器人运动学分析主要包括正解和逆…...

【Java 基础】33 JDBC

文章目录 1. 数据库连接1&#xff09;加载驱动2&#xff09;建立连接 2. 常见操作1&#xff09;创建表2&#xff09;插入数据3&#xff09;查询数据4&#xff09;使用 PreparedStatement5&#xff09;事务管理 3. 注意事项总结 Java Database Connectivity&#xff08;JDBC&…...

Unity中Shader缩放矩阵

文章目录 前言一、直接相乘缩放1、在属性面板定义一个四维变量&#xff0c;用xyz分别控制在xyz轴上的缩放2、在常量缓存区申明该变量3、在顶点着色器对其进行相乘&#xff0c;来缩放变换4、我们来看看效果 二、使用矩阵乘法代替直接相乘缩放的原理1、我们按如下格式得到缩放矩阵…...

Nessus详细安装-windows (保姆级教程)

Nessus描述 Nessus 是一款广泛使用的网络漏洞扫描工具。它由 Tenable Network Security 公司开发&#xff0c;旨在帮助组织评估其计算机系统和网络的安全性。 Nessus 可以执行自动化的漏洞扫描&#xff0c;通过扫描目标系统、识别和评估可能存在的安全漏洞和弱点。它可以检测…...

Stream流的简单使用

stream流的三类方法 获取Stream流 ○ 创建一条流水线,并把数据放到流水线上准备进行操作中间方法 ○ 流水线上的操作 ○ 一次操作完毕之后,还可以继续进行其他操作终结方法 ○ 一个Stream流只能有一个终结方法 ○ 是流水线上的最后一个操作 其实Stream流非常简单&#xff0c;只…...

智能优化算法应用:基于蛇优化算法3D无线传感器网络(WSN)覆盖优化 - 附代码

智能优化算法应用&#xff1a;基于蛇优化算法3D无线传感器网络(WSN)覆盖优化 - 附代码 文章目录 智能优化算法应用&#xff1a;基于蛇优化算法3D无线传感器网络(WSN)覆盖优化 - 附代码1.无线传感网络节点模型2.覆盖数学模型及分析3.蛇优化算法4.实验参数设定5.算法结果6.参考文…...

vue和react diff的详解和不同

diff算法 简述&#xff1a;第一次对比真实dom和虚拟树之间的同层差别&#xff0c;后面为对比新旧虚拟dom树之间的同层差别。 虚拟dom 简述&#xff1a;js对象形容模拟真实dom 具体&#xff1a; 1.虚拟dom是存在内存中的js对象&#xff0c;利用内存的高效率运算。虚拟dom属…...

智能优化算法应用:基于鹈鹕算法3D无线传感器网络(WSN)覆盖优化 - 附代码

智能优化算法应用&#xff1a;基于鹈鹕算法3D无线传感器网络(WSN)覆盖优化 - 附代码 文章目录 智能优化算法应用&#xff1a;基于鹈鹕算法3D无线传感器网络(WSN)覆盖优化 - 附代码1.无线传感网络节点模型2.覆盖数学模型及分析3.鹈鹕算法4.实验参数设定5.算法结果6.参考文献7.MA…...

10:IIC通信

1&#xff1a;IIC通信 I2C总线&#xff08;Inter IC BUS&#xff09; 是由Philips公司开发的一种通用数据总线&#xff0c;应用广泛&#xff0c;下面是一些指标参数&#xff1a; 两根通信线&#xff1a;SCL&#xff08;Serial Clock&#xff0c;串行时钟线&#xff09;、SDA&a…...

互联网上门洗衣洗鞋小程序优势有哪些?

互联网洗鞋店小程序相较于传统洗鞋方式&#xff0c;具有以下优势&#xff1b; 1. 便捷性&#xff1a;用户只需通过手机即可随时随地下单并查询&#xff0c;省去了许多不必要的时间和精力。学生们无需走出宿舍或校园&#xff0c;就能轻松预约洗鞋并取件。 2. 精准定位&#xff1…...

Java中如何优雅地根治null值引起的Bug问题

1. Java对象为null会引发的问题 NullPointerException&#xff1a;当你尝试调用或访问一个null对象的属性或方法时&#xff0c;Java会抛出NullPointerException异常。例如&#xff0c;如果你有一个名为person的变量&#xff0c;它被设置为null&#xff0c;然后你尝试调用perso…...

C# WPF上位机开发(子窗口通知父窗口更新进度)

【 声明&#xff1a;版权所有&#xff0c;欢迎转载&#xff0c;请勿用于商业用途。 联系信箱&#xff1a;feixiaoxing 163.com】 这两天在编写代码的时候&#xff0c;正好遇到一个棘手的问题&#xff0c;解决之后感觉挺有意义的&#xff0c;所以先用blog记录一下&#xff0c;后…...