Spring

Stone大约 31 分钟

Spring

Spring 是一个开源的 Java 应用框架,旨在简化企业级 Java 开发。Spring 框架提供了广泛的功能来支持应用开发,包括但不限于控制反转(IoC)容器、面向切面编程(AOP)、数据访问、事务管理、消息传递、Web 集成等。以下是 Spring 的一些主要特点和简介:

  • 控制反转(IoC)和依赖注入(DI):
    • Spring 容器负责管理应用程序组件的生命周期和依赖关系。
    • 通过依赖注入(DI),Spring 容器在创建对象时自动注入它们所依赖的其他对象。
  • 面向切面编程(AOP):
    • AOP 允许开发者定义横切关注点,如日志、事务管理等,并将它们与业务逻辑分离。
    • Spring AOP 通过代理模式实现,支持在运行时动态地将代码切入到类的指定方法、指定位置上。
  • 数据访问:
    • Spring 提供了与多种数据库交互的模板,如 JdbcTemplate、HibernateTemplate 等,以简化数据库操作。
    • Spring Data 项目提供了对多种 NoSQL 数据库的集成支持,如 MongoDB、Redis 等。
  • 事务管理:
    • Spring 提供了声明式事务管理,允许开发者通过注解或 XML 配置来管理事务。
    • Spring 支持编程式事务管理,也支持与多种事务管理器(如 Hibernate 事务管理器、JDBC 事务管理器等)的集成。
  • Web 集成:
    • Spring MVC 是 Spring 框架的一个模块,用于构建 Web 应用程序。它提供了模型-视图-控制器(MVC)架构的实现,并支持多种视图技术(如 JSP、Thymeleaf 等)。
    • Spring Boot 进一步简化了 Web 应用程序的搭建和开发,通过自动配置和约定优于配置(Convention Over Configuration)的原则,快速启动和运行 Web 应用程序。
  • 消息传递:
    • Spring 提供了对 JMS(Java Message Service)的支持,用于在企业应用程序中传递消息。
    • Spring Integration 项目为更复杂的消息传递场景提供了高级支持,包括路由、过滤、转换等功能。
  • 测试:
    • Spring 提供了对 JUnit 的集成支持,并提供了自己的测试框架 Spring Test,用于简化单元测试和集成测试。
    • Spring Boot Test 进一步简化了 Spring Boot 应用程序的测试过程。
  • 扩展性:
    • Spring 框架是一个可扩展的平台,允许开发者通过实现自定义的 BeanFactory、ApplicationContext 等接口来扩展其功能。
    • Spring 社区提供了大量的第三方库和插件,用于增强 Spring 框架的功能。
  • 社区支持:
    • Spring 拥有庞大的用户社区和丰富的文档资源,为开发者提供了大量的帮助和支持。
    • Spring 项目持续更新和演进,不断引入新的功能和改进。

总之,Spring 是一个功能强大、易于使用且广泛应用的 Java 应用框架,它简化了企业级 Java 开发的过程,提高了开发效率和代码质量。

Spring 体系结构:

image-20221216132513449

Start

IoC 入门案例

在 IDEA 中创建 Maven 项目,在 pom.xml 文件中引入 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 http://maven.apache.org/xsd/maven-4.0.0.xsd">
  <modelVersion>4.0.0</modelVersion>

  <groupId>com.itheima</groupId>
  <artifactId>spring_01_quickstart</artifactId>
  <version>1.0-SNAPSHOT</version>

  <dependencies>
    <dependency>
      <groupId>org.springframework</groupId>
      <artifactId>spring-context</artifactId>
      <version>5.2.10.RELEASE</version>
    </dependency>
    <dependency>
      <groupId>junit</groupId>
      <artifactId>junit</artifactId>
      <version>4.12</version>
      <scope>test</scope>
    </dependency>
  </dependencies>
</project>

创建接口和实现类:

public interface BookDao {
    public void save();
}

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

resources 目录下创建 Bean 的配置文件 applicationContext.xml

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd">

    <!--bean 标签表示配置 bean
    id 属性标示给 bean 起名字
    class 属性表示给 bean 定义类型-->
    <bean id="bookDao" class="net.stonecoding.dao.impl.BookDaoImpl"/>

</beans>

创建容器,从容器中获取 Bean,然后调用 Bean 的方法:

public class App {
    public static void main(String[] args) {
        // 获取 IoC 容器
        ApplicationContext ctx = new ClassPathXmlApplicationContext("applicationContext.xml");
        // 获取 bean(根据 bean 配置 id 获取)
        BookDao bookDao = (BookDao) ctx.getBean("bookDao");
        bookDao.save();
    }
}
输出:
book dao save ...

DI 入门案例

增加接口和实现类,并在实现类中引用其他类:

public interface BookService {
    public void save();
}

public class BookServiceImpl implements BookService {
    private BookDao bookDao;

    public void save() {
        System.out.println("book service save ...");
        bookDao.save();
    }

    public void setBookDao(BookDao bookDao) {
        this.bookDao = bookDao;
    }
}

在配置文件 applicationContext.xml 配置依赖关系:

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd">

    <bean id="bookDao" class="com.itheima.dao.impl.BookDaoImpl"/>

    <bean id="bookService" class="com.itheima.service.impl.BookServiceImpl">
        <!--7.配置 server 与 dao 的依赖关系-->
        <!--property 标签表示配置当前 bean 的属性
        name 属性表示配置哪一个具体的属性
        ref 属性表示参照哪一个 bean-->
        <property name="bookDao" ref="bookDao"/>
    </bean>

</beans>

创建容器,从容器中获取 Bean,然后调用 Bean 的方法,此时会再去调用注入的类的方法:

public class App2 {
    public static void main(String[] args) {
        ApplicationContext ctx = new ClassPathXmlApplicationContext("applicationContext.xml");
        BookService bookService = (BookService) ctx.getBean("bookService");
        bookService.save();
    }
}
输出:
book service save ...
book dao save ...

IoC

核心概念:

  • Spring IoC 容器:Spring 容器是 Spring 框架的核心。容器将创建对象,把它们连接在一起,配置它们,并管理他们的整个生命周期从创建到销毁。这些对象被称为 Spring Beans。Spring 容器使用依赖注入(DI)来管理组成一个应用程序的组件。
  • IOC(Inversion of Control)控制反转:使用对象时,由主动 new 产生对象转换为由外部提供对象,此过程中对象创建控制权由程序转移到外部,此思想称之为控制反转。Spring 提供了一个容器,称为 IOC 容器,用来充当 IOC 思想中的“外部”。IOC 容器负责对象的创建、初始化等一系列工作。
  • DI(Dependency Injection)依赖注入:在容器中建立 Bean 与 Bean 之间的依赖关系的整个过程,称之为依赖注入。依赖注入是一种实现控制反转的技术,通过它,我们可以将对象的依赖关系从硬编码的方式转变为在运行时由 Spring 容器注入的方式。
  • 解耦:Spring 框架的主要目标之一是充分解耦。通过使用 IOC 容器管理 Bean 以及在 IOC 容器内将有依赖关系的 Bean 进行关系绑定(DI),可以实现对象之间的松耦合,从而提高代码的可重用性和可维护性。

Spring 配置 Bean:

  • 配置形式:基于 XML 文件
  • 配置方式:通过全类名(反射)

Spring 支持 3 种依赖注入的方式:

  • 属性注入:通过 setter 方法注入 Bean 的属性或依赖的对象,使用 <property> 元素,使用 name 属性指定 Bean 的属性名称,value 属性或 <value> 子节点指定属性值。属性注入是实际应用中最常用的注入方式。
  • 构造器注入:通过构造方法注入 Bean 的属性或者依赖的对象,它包装了 Bean 实例在实例化后就可以使用。构造器注入在 <constructor-arg> 元素里声明属性,<constructor-arg> 没有 name 属性。使用构造器注入属性值可以指定参数的位置和参数的类型,以区分重载的构造器。
  • 工厂方法注入(很少使用,不推荐)

使用场景:

  • 强制依赖使用构造器进行,使用 setter 注入有概率不进行注入导致null对象出现
    • 强制依赖指对象在创建的过程中必须要注入指定的参数
  • 可选依赖使用 setter 注入进行,灵活性强
    • 可选依赖指对象在创建过程中注入的参数可有可无
  • Spring 框架倡导使用构造器,第三方框架内部大多数采用构造器注入的形式进行数据初始化,相对严谨
  • 如果有必要可以两者同时使用,使用构造器注入完成强制依赖的注入,使用 setter 注入完成可选依赖的注入
  • 实际开发过程中还要根据实际情况分析,如果受控对象没有提供 setter 方法就必须使用构造器注入
  • 自己开发的模块推荐使用 setter 注入

在 Spring 3.0 版本之前,需要使用配置文件配置 IoC和 DI,比较复杂。从 3.0 版本开始,就可以使用纯注解方式,大大简化了开发。

纯注解开发

Spring 纯注解开发是一种不依赖 XML 配置文件,使用 Java 类替代配置文件,通过注解来定义和配置 Spring 容器的开发方式。

基本步骤

  • 添加 Spring 相关依赖: 在项目的构建文件(如 Maven 的 pom.xml 或 Gradle 的 build.gradle)中添加 Spring 的相关依赖,如 spring-context 等。
  • 创建配置类: 使用 @Configuration 注解定义一个配置类,用于替代 XML 配置文件。在配置类中,可以使用 @Bean 注解来定义和初始化 Bean。
@Configuration  
@ComponentScan(basePackages = "com.example")  
public class AppConfig {  
  
    @Bean  
    public MyService myService() {  
        return new MyServiceImpl();  
    }
}
  • 组件扫描: 使用 @ComponentScan 注解来指定 Spring 需要扫描的包路径,以便发现带有 @Component@Service@Repository@Controller 等注解的类,并将它们注册为 Bean。

  • 依赖注入: 在需要使用其他 Bean 的地方,使用 @Autowired@Inject 注解来进行自动依赖注入。同时,可以使用 @Qualifier 注解来指定注入的 Bean 的名称。

@Service  
public class AnotherService {  
  
    @Autowired  
    private MyService myService;  
  
    // ...  
}
  • 其他配置: 根据项目的需要,可能还需要配置数据源、事务管理、MVC 等。这些配置都可以通过注解来实现。

常用注解

  • @Configuration: 用于声明当前类是一个配置类,相当于一个 XML 配置文件。
  • @Bean: 用于在配置类中定义一个 Bean,并指定其名称和初始化方法。
  • @ComponentScan: 用于指定Spring需要扫描的包路径,以便发现带有 @Component@Service@Repository@Controller 等注解的类,并将它们注册为 Bean。
  • @Component@Service@Repository@Controller: 用于声明一个类是一个Spring的组件,其中 @Service 通常用于业务逻辑层,@Repository 用于数据访问层,@Controller 用于 Web 层。这些注解都是 @Component 的特化,它们的区别主要是语义上的。
  • @Autowired@Inject: 用于自动注入依赖的 Bean。其中 @Autowired 是 Spring 提供的注解,而 @Inject 是 JSR-330 规范提供的注解,两者在功能上类似。
  • @Qualifier: 用于指定注入的 Bean 的名称,当存在多个相同类型的 Bean 时,可以使用该注解来指定注入哪一个。
  • @Value: 用于注入基本类型值、字符串、其他 Bean 的引用等。
  • @Profile: 用于定义 Spring 环境的配置,可以根据不同的环境(如开发、测试、生产)来加载不同的配置。
  • @PropertySource: 用于加载 .properties 文件中的配置属性,配合 @Value 注解使用。
  • @Enable 系列注解(如 @EnableWebMvc@EnableTransactionManagement 等): 用于开启 Spring 的某些功能或模块。

创建 Maven 项目,并在 pom.xml 中添加 Spring 依赖:

<dependencies>
    <dependency>
        <groupId>org.springframework</groupId>
        <artifactId>spring-context</artifactId>
        <version>5.2.10.RELEASE</version>
    </dependency>
</dependencies>

创建 BookDaoBookDaoImplBookServiceBookServiceImpl 类:

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

public class BookServiceImpl implements BookService {
    public void save() {
        System.out.println("book service save ...");
    }
}

创建配置类 SpringConfig,在配置类上添加 @Configuration 注解,将其标识为一个配置类,并在配置类上添加包扫描注解 @ComponentScan 设定扫描路径:

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

创建运行类 AppForAnnotation

public class AppForAnnotation {

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

BookServiceImpl 类的 bookDao 属性上添加 @Autowired 注解:

@Service
public class BookServiceImpl implements BookService {
    @Autowired
    private BookDao bookDao;
    
//	  public void setBookDao(BookDao bookDao) {
//        this.bookDao = bookDao;
//    }
    public void save() {
        System.out.println("book service save ...");
        bookDao.save();
    }
}

注意:

  • @Autowired 可以写在属性上,也可也写在 setter 方法上,最简单的处理方式是写在属性上并将 setter 方法删除掉。
  • 因为自动装配基于反射设计创建对象并通过暴力反射为私有属性进行设值,普通反射只能获取 public 修饰的内容,暴力反射除了获取 public 修饰的内容还可以获取 private 修改的内容,所以此处无需提供 setter 方法。

由于 @Autowired 默认是按照类型注入并自动装配,单对应 BookDao 接口如果有多个实现类时,需要给实现类在注解中指定 Bean 名称:

@Repository("bookDao")
public class BookDaoImpl implements BookDao {
    public void save() {
        System.out.println("book dao save ..." );
    }
}
@Repository("bookDao2")
public class BookDaoImpl2 implements BookDao {
    public void save() {
        System.out.println("book dao save ...2" );
    }
}

此时就可以注入成功。

但如果容器中所有的 Bean 名称都与与变量名不匹配时:

@Repository("bookDao1")
public class BookDaoImpl implements BookDao {
    public void save() {
        System.out.println("book dao save ..." );
    }
}
@Repository("bookDao2")
public class BookDaoImpl2 implements BookDao {
    public void save() {
        System.out.println("book dao save ...2" );
    }
}

需要在变量名上面使用 @Qualifier 来指定注入哪个名称的 Bean:

@Service
public class BookServiceImpl implements BookService {
    @Autowired
    @Qualifier("bookDao1")
    private BookDao bookDao;
    
    public void save() {
        System.out.println("book service save ...");
        bookDao.save();
    }
}

使用 @Value 注解进行简单类型的注入:

@Repository("bookDao")
public class BookDaoImpl implements BookDao {
    @Value("itheima")
    private String name;
    public void save() {
        System.out.println("book dao save ..." + name);
    }
}

对于 resource 目录下的属性文件 jdbc.properties

name=itheima888

可以在配置类上使用 @PropertySource 注解引入:

@Configuration
@ComponentScan("com.itheima")
@PropertySource("jdbc.properties")
public class SpringConfig {
}

然后就可以使用 @Value 读取属性文件中的内容:

@Repository("bookDao")
public class BookDaoImpl implements BookDao {
    @Value("${name}")
    private String name;
    public void save() {
        System.out.println("book dao save ..." + name);
    }
}

使用 @Bean 注解来管理第三方 Bean:

public class JdbcConfig {
	@Bean
    public DataSource dataSource(){
        DruidDataSource ds = new DruidDataSource();
        ds.setDriverClassName("com.mysql.jdbc.Driver");
        ds.setUrl("jdbc:mysql://localhost:3306/spring_db");
        ds.setUsername("root");
        ds.setPassword("root");
        return ds;
    }
}

在 Spring 配置类上使用 @Import 注解手动引入需要加载的配置类:

@Configuration
@ComponentScan("com.itheima")
@Import({JdbcConfig.class})
public class SpringConfig {
	
}

注意:

  • @Import 参数需要的是一个数组,可以引入多个配置类。

AOP

Spring AOP(Aspect-Oriented Programming)是 Spring 框架中的一个重要部分,它基于面向切面编程(AOP)的思想。AOP 是一种编程范式,旨在提高模块化,它通过将横切关注点从它们所影响的业务逻辑中分离出来,从而实现代码的重用和降低耦合度。

在 Spring AOP 中,横切关注点通常指的是与核心业务逻辑无关但需要在多个地方进行处理的非功能性需求,例如日志记录、事务管理、性能监控、权限控制等。通过使用 Spring AOP,开发人员可以定义这些横切关注点,并将它们作为切面(Aspect)应用到需要的位置,而无需修改原有的业务逻辑代码。

Spring AOP 的实现主要基于动态代理技术,即在运行时动态地为目标对象创建一个代理对象,代理对象会拦截目标对象的方法调用,并在方法调用前后添加相应的横切逻辑。通过这种方式,Spring AOP 可以在不修改原有代码的情况下,为系统添加新的功能或修改现有功能。

核心概念:

  • 切面(Aspect):切面是 AOP 中的核心概念,它表示一个横切关注点。切面是一个包含了通知(Advice)和切点(Pointcut)的类,通知和切点共同定义了切面的行为。
  • 通知(Advice):通知是 AOP 框架在特定切入点执行的增强处理,它是在连接点之前或之后要执行的代码。通知类型包括前置通知(Before advice)、后置通知(After advice)、环绕通知(Around advice)和异常通知(Throws advice)等。
  • 切入点(Pointcut):切入点定义了通知应该应用到哪些连接点。切入点通过表达式语言来定义,这些表达式描述了方法调用的特定方面,如方法名、参数类型和返回类型等。
  • 连接点(Joinpoint):连接点是程序执行过程中能够应用通知的所有点,如方法的调用、异常的处理等。连接点是 AOP 框架的一个核心概念,但通常与开发人员直接打交道的是切点,而不是连接点。
  • 织入(Weaving):织入是将切面应用到目标对象并创建新的代理对象的过程。这个过程可以在编译时、类加载时或运行时完成。在 Spring AOP 中,织入通常是在运行时完成的,通过动态代理技术实现。
  • 目标对象(Target Object):目标对象是被一个或多个切面所通知的对象。在 Spring AOP 中,目标对象通常是一个或多个 Spring Bean。
  • 代理对象(Proxy):代理对象是由 AOP 框架创建的对象,它包含了目标对象的所有方法,并在方法调用时添加通知逻辑。代理对象对于客户端来说是透明的,客户端通常不会直接访问目标对象,而是通过代理对象来调用目标对象的方法。

image-20240515171930071

入门

在 IDEA 中创建 Maven 项目,在 pom.xml 文件中引入依赖:

<dependencies>
    <dependency>
        <groupId>org.springframework</groupId>
        <artifactId>spring-context</artifactId>
        <version>5.2.10.RELEASE</version>
    </dependency>
</dependencies>
<dependency>
    <groupId>org.aspectj</groupId>
    <artifactId>aspectjweaver</artifactId>
    <version>1.9.4</version>
</dependency>

创建接口和实现类:

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 ...");
    }
}

创建 Spring 的配置类,使用 @EnableAspectJAutoProxy 开启注解格式 AOP 功能:

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

创建启动类:

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

使用 Spring AOP 的方式在不改变 update 方法的前提下让其具有打印系统时间的功能,创建通知类和通知方法:

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

其中:

  • @Aspect:设置当前类为 AOP 切面类。
  • @Pointcut:定义切入点,依托一个不具有实际意义的方法进行,即无参数、无返回值、方法体无实际逻辑。
  • @Before:设置当前通知方法与切入点之间的绑定关系,当前通知方法在切入点指定的方法之前运行。

运行程序:

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

切入点表达式

在使用 @Pointcut 定义切入点时,其语法为:

动作关键字(访问修饰符  返回值  包名.类/接口名.方法名(参数) 异常名)

例如:

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

可以使用通配符描述切入点,简化配置,包括:

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

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

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

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

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

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

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

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

    这个使用率较低,描述子类的,很少使用。*Service+ 表示所有以 Service 结尾的接口的子类。

例如:

// 匹配接口,能匹配到
execution(void com.itheima.dao.BookDao.update())

// 匹配实现类,能匹配到
execution(void com.itheima.dao.impl.BookDaoImpl.update())

// 返回值任意,能匹配到
execution(* com.itheima.dao.impl.BookDaoImpl.update())

// 返回值任意,但是 update 方法必须要有一个参数
execution(* com.itheima.dao.impl.BookDaoImpl.update(*))

// 返回值为 void,com 包下的任意包三层包下的任意类的 update 方法,匹配到的是实现类,能匹配
execution(void com.*.*.*.*.update())

// 返回值为 void,com 包下的任意两层包下的任意类的 update 方法,匹配到的是接口,能匹配
execution(void com.*.*.*.update())

// 返回值为 void,方法名是 update 的任意包下的任意类,能匹配
execution(void *..update())

// 匹配项目中任意类的任意方法,能匹配,但是不建议使用这种方式,影响范围广
execution(* *..*(..))

// 匹配项目中任意包任意类下只要以 u 开头的方法,update 方法能满足,能匹配
execution(* *..u*(..))

// 匹配项目中任意包任意类下只要以 e 结尾的方法,update 和 save 方法能满足,能匹配
execution(* *..*e(..))

// 返回值为 void,com 包下的任意包任意类任意方法,能匹配,* 代表的是方法
execution(void com..*())

// 将项目中所有业务层方法的以 find 开头的方法匹配
execution(* com.itheima.*.*Service.find*(..))

// 将项目中所有业务层方法的以 save 开头的方法匹配
execution(* com.itheima.*.*Service.save*(..))

通知类型

在 Spring AOP 中,通知(Advice)是定义切面(Aspect)行为的方式。Spring AOP 提供了以下几种主要的通知类型:

  • 前置通知(Before):在目标方法执行之前运行的通知。适用于在方法调用之前执行某些操作,例如日志记录、权限检查等。
@Before("execution(* com.example.service.*.*(..))")
public void beforeAdvice(JoinPoint joinPoint) {
    System.out.println("Before method: " + joinPoint.getSignature().getName());
}
  • 后置通知(After):在目标方法执行之后运行的通知,无论目标方法是否成功完成。适用于执行清理工作或记录日志。
@After("execution(* com.example.service.*.*(..))")
public void afterAdvice(JoinPoint joinPoint) {
    System.out.println("After method: " + joinPoint.getSignature().getName());
}
  • 返回通知(After Returning):在目标方法成功完成并返回结果之后运行的通知。适用于根据返回值执行后续操作。
@AfterReturning(pointcut = "execution(* com.example.service.*.*(..))", returning = "result")
public void afterReturningAdvice(JoinPoint joinPoint, Object result) {
    System.out.println("Method returned: " + result);
}
  • 异常通知(After Throwing):在目标方法抛出异常后运行的通知。适用于异常处理、日志记录等。
@AfterThrowing(pointcut = "execution(* com.example.service.*.*(..))", throwing = "error")
public void afterThrowingAdvice(JoinPoint joinPoint, Throwable error) {
    System.out.println("Method threw exception: " + error);
}
  • 环绕通知(Around):包围目标方法的通知,可以在方法调用前后自定义行为。适用于事务管理、性能监控等场景。
@Around("execution(* com.example.service.*.*(..))")
public Object aroundAdvice(ProceedingJoinPoint joinPoint) throws Throwable {
    System.out.println("Before method: " + joinPoint.getSignature().getName());
    Object result = joinPoint.proceed(); // 执行目标方法
    System.out.println("After method: " + joinPoint.getSignature().getName());
    return result;
}

注意:

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

在 Spring AOP 中,可以通过多种方式在通知(Advice)中获取数据。这些数据通常包括目标方法的参数、返回值和抛出的异常等。下面详细介绍几种获取数据的方法和示例:

  • 获取目标方法的参数:可以通过 JoinPoint 获取目标方法的参数。
@Before("execution(* com.example.service.*.*(..))")
public void beforeAdvice(JoinPoint joinPoint) {
    System.out.println("Method Name: " + joinPoint.getSignature().getName());
    System.out.println("Arguments: " + Arrays.toString(joinPoint.getArgs()));
}
  • 获取目标方法的返回值:可以在 @AfterReturning 通知中获取目标方法的返回值。
@AfterReturning(pointcut = "execution(* com.example.service.*.*(..))", returning = "result")
public void afterReturningAdvice(JoinPoint joinPoint, Object result) {
    System.out.println("Method returned: " + result);
}
  • 获取目标方法抛出的异常:可以在 @AfterThrowing 通知中获取目标方法抛出的异常。
@AfterThrowing(pointcut = "execution(* com.example.service.*.*(..))", throwing = "error")
public void afterThrowingAdvice(JoinPoint joinPoint, Throwable error) {
    System.out.println("Method threw exception: " + error);
}
  • 使用环绕通知获取并操作方法执行的全过程:在 @Around 通知中,可以通过 ProceedingJoinPoint 获取并操作方法执行的全过程,包括参数、返回值以及异常。
@Around("execution(* com.example.service.*.*(..))")
public Object aroundAdvice(ProceedingJoinPoint proceedingJoinPoint) throws Throwable {
    System.out.println("Before method: " + proceedingJoinPoint.getSignature().getName());
    
    Object[] args = proceedingJoinPoint.getArgs();
    System.out.println("Method Arguments: " + Arrays.toString(args));
    
    Object result;
    try {
        result = proceedingJoinPoint.proceed(); // 执行目标方法
        System.out.println("Method returned: " + result);
    } catch (Throwable throwable) {
        System.out.println("Method threw exception: " + throwable);
        throw throwable;
    }
    
    System.out.println("After method: " + proceedingJoinPoint.getSignature().getName());
    return result;
}

Transaction

Spring 框架提供了强大的事务管理功能,使得开发者能够更容易地处理数据库操作中的事务。在 Spring 中,事务管理主要分为编程式事务管理和声明式事务管理两种方式。

  • 编程式事务管理:需要开发者在代码中显式地调用事务管理接口,例如使用 PlatformTransactionManagerTransactionDefinition 接口来定义和管理事务。这种方式提供了更大的灵活性,但代码会相对复杂一些。
  • 声明式事务管理:主要通过 AOP(面向切面编程)实现,开发者只需在配置文件中进行声明,或者在注解中进行配置,Spring 会自动将事务管理代码嵌入到目标方法执行流程中。声明式事务管理大大简化了事务管理的代码,是 Spring 推荐使用的方式。

声明式事务

声明式事务管理的基本步骤:

  • 配置数据源:首先需要配置数据源(DataSource),指定连接数据库的信息。
  • 配置事务管理器:配置事务管理器(TransactionManager),Spring 提供了多种事务管理器,例如 DataSourceTransactionManager 用于 JDBC 数据源。
  • 配置事务通知:定义事务通知(Transaction Advice),即告诉 Spring 在何时(例如方法执行前、后)进行事务管理。
  • 配置 AOP 切面:将事务通知应用到目标方法上,这通常通过配置 AOP 切面实现。

以转账为例,整合 MyBatiopen in new windows 进行测试,先创建测试表:

create table tbl_account(
    id int primary key auto_increment,
    name varchar(35),
    money double
);
insert into tbl_account values(1,'Tom',1000);
insert into tbl_account values(2,'Jerry',1000);

在 IDEA 中创建 Maven 项目,在 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>

    <dependency>
      <groupId>org.springframework</groupId>
      <artifactId>spring-jdbc</artifactId>
      <version>5.2.10.RELEASE</version>
    </dependency>

    <dependency>
      <groupId>org.mybatis</groupId>
      <artifactId>mybatis-spring</artifactId>
      <version>1.3.0</version>
    </dependency>

    <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>

</dependencies>

创建实体类:

public class Account implements Serializable {

    private Integer id;
    private String name;
    private Double money;
	//setter...getter...toString...方法略    
}

创建 Dao 接口及其实现类:

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);
}

创建 Service 接口及其实现类:

public interface AccountService {
    /**
     * 转账操作
     * @param out 传出方
     * @param in 转入方
     * @param money 金额
     */
    public void transfer(String out,String in ,Double money) ;
}

@Service
public class AccountServiceImpl implements AccountService {

    @Autowired
    private AccountDao accountDao;

    public void transfer(String out,String in ,Double money) {
        accountDao.outMoney(out,money);
        accountDao.inMoney(in,money);
    }

}

创建 jdbc.properties 属性文件:

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

创建 JdbcConfig 配置类:

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;

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

创建 MybatisConfig 配置类:

public class MybatisConfig {

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

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

创建 SpringConfig 配置类:

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

创建测试类:

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

    @Autowired
    private AccountService accountService;

    @Test
    public void testTransfer() throws IOException {
        accountService.transfer("Tom","Jerry",100D);
    }

}

在需要被事务管理的方法上添加注解:

public interface AccountService {
    /**
     * 转账操作
     * @param out 传出方
     * @param in 转入方
     * @param money 金额
     */
    //配置当前接口方法具有事务
    public void transfer(String out,String in ,Double money) ;
}

@Service
public class AccountServiceImpl implements AccountService {

    @Autowired
    private AccountDao accountDao;
    
	@Transactional
    public void transfer(String out,String in ,Double money) {
        accountDao.outMoney(out,money);
        int i = 1/0;
        accountDao.inMoney(in,money);
    }

}

注意:

  • @Transactional 可以写在接口类上、接口方法上、实现类上和实现类方法上
  • 写在接口类上,该接口的所有实现类的所有方法都会有事务
  • 写在接口方法上,该接口的所有实现类的该方法都会有事务
  • 写在实现类上,该类中的所有方法都会有事务
  • 写在实现类方法上,该方法上有事务
  • 建议写在实现类或实现类的方法上

JdbcConfig 类中配置事务管理器:

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;

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

    //配置事务管理器,MyBatis 使用的是 jdbc 事务
    @Bean
    public PlatformTransactionManager transactionManager(DataSource dataSource){
        DataSourceTransactionManager transactionManager = new DataSourceTransactionManager();
        transactionManager.setDataSource(dataSource);
        return transactionManager;
    }
}

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

SpringConfig 配置类中开启事务注解:

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

事务属性

@Transactional 注解上还可以配置如下属性:

  • readOnlytrue 表示只读事务,false 表示读写事务,默认为 false

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

  • rollbackFor:指定需要回滚的异常类数组。当抛出这些异常时,事务将回滚。并不是所有的异常都会回滚事务,Spring 的事务只会对 Error 异常和 RuntimeException 异常及其子类进行事务回滚,其他的异常类型不会自动回滚。

  • noRollbackFor:指定不需要回滚的异常类数组。当抛出这些异常时,事务不会回滚。

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

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

  • isolation:设置事务的隔离级别

    • DEFAULT:默认隔离级别, 会采用数据库的隔离级别
    • READ_UNCOMMITTED:读未提交
    • READ_COMMITTED:读已提交
    • REPEATABLE_READ:重复读取
    • SERIALIZABLE:串行化
  • propagation:指定事务传播行为。
属性取值含义
Propagation.REQUIRED支持当前事务,如果当前没有事务,就新建一个事务。这是默认值,也是最常见的选择。
Propagation.REQUIRES_NEW新建事务,如果当前存在事务,把当前事务挂起。
Propagation.SUPPORTS支持当前事务,如果当前没有事务,就以非事务方式执行。
Propagation.MANDATORY支持当前事务,如果当前没有事务,就抛出异常。
Propagation.NOT_SUPPORTED以非事务方式执行操作,如果当前存在事务,就把当前事务挂起。
Propagation.NEVER以非事务方式执行,如果当前存在事务,则抛出异常。
Propagation.NESTED如果当前存在事务,则在嵌套事务内执行。如果当前没有事务,则进行与REQUIRED相同的操作。

例子:在一个事务中,增加日志功能,不论事务是否执行成功,都需要记录日志,此时就需要对记录日志作为一个单独事务,即指定传播行为为 Propagation.REQUIRES_NEW

创建表:

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

添加 LogDao 接口:

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

添加 LogService 接口与实现类:

public interface LogService {
    void log(String out, String in, Double money);
}
@Service
public class LogServiceImpl implements LogService {

    @Autowired
    private LogDao logDao;
	@Transactional
    public void log(String out,String in,Double money ) {
        logDao.log("转账操作由"+out+"到"+in+",金额:"+money);
    }
}

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

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 {

    @Autowired
    private AccountDao accountDao;
    @Autowired
    private LogService logService;
	@Transactional
    public void transfer(String out,String in ,Double money) {
        try{
            accountDao.outMoney(out,money);
            accountDao.inMoney(in,money);
        }finally {
            logService.log(out,in,money);
        }
    }

}

修改 logService 改变事务的传播行为:

@Service
public class LogServiceImpl implements LogService {

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

Integration

整合 MyBatis

在 IDEA 中创建 Maven 项目,在 pom.xml 文件中引入 Spring 及 MyBatis 依赖:

<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>
    <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>
</dependencies>

resources 目录下添加 jdbc.properties 文件,用于配置数据库连接四要素:

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

创建数据源的配置类:

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;

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

创建 MyBatis 配置类并配置 SqlSessionFactory

public class MybatisConfig {
    //定义 Bean,SqlSessionFactoryBean,用于产生 SqlSessionFactory 对象
    @Bean
    public SqlSessionFactoryBean sqlSessionFactory(DataSource dataSource){
        SqlSessionFactoryBean ssfb = new SqlSessionFactoryBean();
        //设置模型类的别名扫描
        ssfb.setTypeAliasesPackage("com.itheima.domain");
        //设置数据源
        ssfb.setDataSource(dataSource);
        return ssfb;
    }
    //定义 Bean,返回 MapperScannerConfigurer 对象
    @Bean
    public MapperScannerConfigurer mapperScannerConfigurer(){
        MapperScannerConfigurer msc = new MapperScannerConfigurer();
        msc.setBasePackage("com.itheima.dao");
        return msc;
    }
}
  • SqlSessionFactoryBeanFactoryBean 的一个子类,在该类中将 SqlSessionFactory 的创建进行了封装,简化对象的创建,只需要将其需要的内容设置即可。
  • 方法中有一个参数为 dataSource,当前 Spring 容器中已经创建了 Druid 数据源,类型刚好是 DataSource 类型,此时在初始化 SqlSessionFactoryBean 这个对象的时候,发现需要使用 DataSource 对象,而容器中刚好有这么一个对象,就自动加载了 DruidDataSource 对象。
  • MapperScannerConfigurer 对象也是 MyBatis 提供的专用于整合的 Jar 包中的类,用来处理原始配置文件中的 Mappers 相关配置,加载数据层的 Mapper 接口类,创建代理对象保存到 IoC 容器中。
  • MapperScannerConfigurer 有一个核心属性 basePackage,就是用来设置所扫描的包路径。

主配置类中引入 MyBatis 配置类:

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

创建数据库及表:

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
);

根据表创建模型类:

public class Account implements Serializable {

    private Integer id;
    private String name;
    private Double money;
	//setter...getter...toString...方法略    
}

创建 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);
}

创建 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 {

    @Autowired
    private 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();
    }
}

编写运行类,从 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);
    }
}

整合 JUnit

在 IDEA 中创建 Maven 项目,在 pom.xml 文件中引入 Spring 及 JUnit 依赖:

<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>

test\java 下创建测试类:

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

注意:

  • 单元测试,如果测试的是注解配置类,则使用 @ContextConfiguration(classes = 配置类.class)
  • 单元测试,如果测试的是配置文件,则使用 @ContextConfiguration(locations={配置文件名,...})
  • JUnit 运行后是基于 Spring 环境运行的,所以 Spring 提供了一个专用的类运行器,这个务必要设置,这个类运行器就在 Spring 的测试专用包中提供的。
  • 上面两个配置都是固定格式,当需要测试哪个 Bean 时,使用自动装配加载对应的对象,下面的工作就和以前做 JUnit 单元测试完全一样了。
上次编辑于:
贡献者: stonebox,stone