【Spring 基础】

【Spring 基础】

一、 Spring 介绍

1. 简述

  • Spring 技术是 JavaEE 开发必备技能,企业开发技术选型
  • 专业角度
    • 简化开发,降低企业级开发的复杂性
      • IoC
      • AOP
        • 事务处理
    • 框架整合,高效整合其他技术,提高企业级应用开发与运行效率
      • MyBatis
      • MyBatis-plus
      • Struts
      • Struts2
      • Hibernate……
  1. 学习 Spring 架构设计思想
  2. 学习基础操作,思考操作与思想间的联系
  3. 学习案例,熟练应用操作的同时,体会思想

2. Spring 发展

Spring 官网
Spring 发展到今天已经形成了一种开发的生态圈,Spring 提供了若干个项目,每个项目用于完成特定的功能

(1) Spring 应用

Spring 应用

(2) Spring 家族
  • 排位越靠前,使用量越大,越重要
    Spring 家族
(3) Spring 的侧重学习
  • Spring Framework :Spring 框架是 Java 平台的一个开源的全栈(full-stack)应用程序框架和控制反转容器实现,一般被直接称为 Spring
  • Spring Boot :Spring Boot 是基于 Spring Framework 4.0 派生的,用于快速搭建独立的基于生产级别的 Spring 应用的框架,可以以最小的依赖引入来构建一个 Spring 应用
  • Spring Cloud :Spring Cloud 为开发者提供了快速构建分布式系统中一些常用模式的工具(如配置管理、服务发现、断路器、智能路由、微代理、控制总线、一次性令牌、全局锁、领导选举、分布式会话、集群状态),分布式系统的协调导致样板模式,使用 Spring Cloud 开发人员可以快速建立实现这些模式的服务和应用程序
(4) Spring 发展
  1. Spring 1.0 :使用纯配置的形式开发
  2. Spring 2.0 :引入了注解的功能
  3. Spring 3.0 :演化成了可以不写配置的模式
  4. Spring 4.0 :紧跟 JDK 版本的升级,对个别的 API 进行了调整
  5. Spring 5.0 :全面支持 JDK 8
  6. Spring 6.0 :基于 JDK 17 源代码水平

二、 Spring Framework

1. Spring Framework 系统架构图

  • Spring Framework 4.0 系统架构图(之后版本基本趋于稳定,没有太大变化)

系统架构图

  • 在技术中上层总是是基于下层实现的
  • 第一部分:
    • Core Container :核心容器
  • 第二部分:
    • AOP :面向切面编程
    • Aspects :AOP 思想实现
  • 第三部分:
    • Data Access :数据访问
    • Data Integration :数据集成
  • 第四部分:
    • Web :Web 开发
  • 第五部分:
    • Test :单元测试与集成测试

2. Spring Framework 学习线路图

  • 基本可以简单描述为:
    1. Core container
    2. Data Access/Integration
    3. AOP、Aspects
    4. Transactions

线路

  • 具体表现为:
    1. 第一部分:核心容器
      • 核心概念(IoC/DI)
      • 容器基本操作
    2. 第二部分:整合(整合在第四部分时体现更明显)
      • 整合数据层技术 MyBatis
    3. 第三部分:AOP
      • 核心概念
      • AOP 基础操作
      • AOP 实用开发
    4. 第四部分:事务
      • 事务实用开发
    5. 第五部分:家族
      • SpringMVC
      • Spring Boot
      • Spring Cloud

三、 核心容器

1. 核心容器(Core Container)的核心概念

主要涉及 IoC/DI、IoC 容器、Bean 三部分核心概念

  • 问题的提出:
    • 业务层的定义
      业务层
    • 数据层的实现:
      数据层
    • 一旦数据层的增加或更替,实现就无法成功
      数据层
  • 代码书写现状
    • 耦合度偏高
  • 解决方案
    • 使用对象时,在程序中不要主动使用 new 创建对象,转换为由外部提供对象
  • IoC(Inversion of Control)控制反转
    • 对象的创建控制权由程序转移到外部,这种思想称为控制反转
    • 使用对象时,由主动 new 产生对象转换为由外部提供对象,此过程中对象创建控制权由程序转移到外部
  • Spring 技术对 IoC 思想进行了实现
    • Spring 提供了一个容器,称为 IoC 容器,用来充当 IoC 思想中的“外部”
    • IoC 容器负责对象的创建,初始化等一系列工作,被创建或被管理的对象在 IoC 容器中统称为 Bean

业务层

IoC 容器

  • 由于业务层中的 Service 依赖 Dao 对象运行,并且 IoC 容器中包含着 service 和 dao 对象,所以 IoC 容器中将 service 和 dao 的依赖关系进行绑定

  • DI(Dependency Injection)依赖注入

    • 在容器中建立 bean 与 bean 之间的依赖关系的整个过程,称为依赖注入
  • 总结:

    • 目标:充分解耦
      • 使用 IoC 容器管理 bean(IoC)
      • 在 IoC 容器内将所有依赖关系的 bean 进行关系绑定(DI)
    • 最终效果
      • 使用对象时不仅可以直接从 IoC 容器中获取,并且获取到的 bean 已经绑定了所有的依赖关系

2. IoC (控制反转)入门

  • 思路分析:
    1. 管理什么?(Service 与 Dao)
    2. 如何将被管理的对象告知 IoC 容器?(配置)
    3. 被管理的对象交给 IoC 容器,如何获取到 IoC 容器?(接口)
    4. IoC 容器得到后,如何从容器中获取 bean ?(接口方法)
    5. 使用 Spring 导入哪些坐标?(pom.xml 文件中)
(1) 不使用 Spring 配置
  • 实现:
    • 创建或使用 Maven 项目
      示例
    • 以此目录演示不使用 Spring 配置
public interface BookDao {
    public void save();
}
public class BookDaoImpl implements BookDao {
    @Override
    public void save() {
        System.out.println("book dao save...");
    }
}
public interface BookService {
    public void save();
}
public class BookServiceImpl implements BookService {
    private BookDao bookDao=new BookDaoImpl();
    @Override
    public void save() {
        System.out.println("book service save...");
        bookDao.save();
    }
}
public class App {
    public static void main(String[] args) {
        BookServiceImpl bookService = new BookServiceImpl();
        bookService.save();
    }
}
(2) 使用 Spring 配置
  • pom.xml 中导入依赖项(自行选择版本即可)
        <dependency>
            <groupId>org.springframework</groupId>
            <artifactId>spring-context</artifactId>
            <version>5.3.24</version>
        </dependency>
  • 之后右击 resources 目录,选择新建
    新建
  • 选择 Spring 配置(Spring config)
    Spring 配置
  • 创建名称为 applicationContext.xml 的配置文件(以上一个 Maven 项目为基础)
  • 编辑 applicationContext.xml 配置文件
    • bean 定义时 id 属性在同一个上下文中不能重复
<?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">
    <!-- 1. 导入 Spring 的坐标 spring-context,及其对应版本-->
    <!-- 2. 配置 bean-->
    <!-- bean 标签表示配置 bean-->
    <!-- id 属性表示给 bean 起名字-->
    <!-- class 属性表示给 bean 定义类型-->
    <bean id="bookDao" class="com.example.dao.impl.BookDaoImpl"/>
    <bean id="bookService" class="com.example.service.impl.BookServiceImpl"/>
</beans>
  • 进行实现:
public class App {
    public static void main(String[] args) {
        // 获取 IoC 容器(加载配置文件得到上下文对象,也就是容器对象)
        ApplicationContext applicationContext = new ClassPathXmlApplicationContext("applicationContext.xml");
        // 获取 bean
        BookDao bookDao = (BookDao) applicationContext.getBean("bookDao");
        bookDao.save();
        BookService bookService = (BookService) applicationContext.getBean("bookService");
        bookService.save();
    }
}
  • 但是此时仍然需要实现方法中的创建对象操作,没有达到解耦的效果

3. DI (依赖注入)入门

  • 思路分析:

    1. 基于 IoC 管理 bean
    2. Service 中使用 new 形式创建的 Dao 对象是否保留?(否)
    3. Service 中需要的 Dao 对象如何进入到 Service 中?(提供方法)
    4. Service 与 Dao 间的关系如何描述?(配置)
  • 更改上文中的 BookServiceImpl 实现类和 applicationContext.xml 配置文件即可实现

public class BookServiceImpl implements BookService {
    // 删除业务层中使用 new 的方式创建的 dao 对象
    private BookDao bookDao;
    @Override
    public void save() {
        System.out.println("book service save...");
        bookDao.save();
    }
    // 提供对应的 set 方法
    public void setBookDao(BookDao bookDao) {
        this.bookDao = bookDao;
    }
}
<?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">
    <!-- 1. 导入 Spring 的坐标 spring-context,及其对应版本-->
    <!-- 2. 配置 bean-->
    <!-- bean 标签表示配置 bean-->
    <!-- id 属性表示给 bean 起名字-->
    <!-- class 属性表示给 bean 定义类型-->
    <bean id="bookDao" class="com.example.dao.impl.BookDaoImpl"/>
<!--    <bean id="bookService" class="com.example.service.impl.BookServiceImpl"/>-->
    <bean id="bookService" class="com.example.service.impl.BookServiceImpl">
        <!-- 配置 service 与 dao 的关系-->
        <!-- property 标签表示配置当前 bean 的属性-->
        <!-- name 属性表示配置哪一个具体的属性(即此属性下的 “bookDao” 为 BookServiceImpl 类中的对象)-->
        <!-- ref 属性表示参照哪一个 bean(即此属性下的 “bookDao” 为此配置文件中上文中的 id 属性 “bookDao”)-->
        <property name="bookDao" ref="bookDao"/>
    </bean>
</beans>
  • 依旧可以使用上文测试方式实现

4. bean 配置

(1) bean 基础配置

bean 配置

(2) bean 别名配置
  • 例如,修改配置文件,此处 bean 标签中的 name 属性可以定义多个别名
    • bean 标签下的 ref 属性也可以指向此别名(但是不建议,规范写法就是指向 id 属性)
    <bean id="bookService" name="service service2 service3" class="com.example.service.impl.BookServiceImpl">
        <property name="bookDao" ref="bookDao"/>
    </bean>
  • 对应的实现测试方法中,直接输入别名就可以执行
    public static void main(String[] args) {
        // 获取 IoC 容器
        ApplicationContext applicationContext = new ClassPathXmlApplicationContext("applicationContext.xml");
        // 获取 bean
        BookService bookService = (BookService) applicationContext.getBean("service");
        bookService.save();
    }

别名

  • 注意:
    • 获取 bean 无论是通过 id 还是 name 属性获取,如果无法获取到,将抛出异常 NoSuchBeanDefinitionException
    • NoSuchBeanDefinitionException:No bean named 'xxxxxx' available
(3) bean 作用范围配置
  • 测试:
    public static void main(String[] args) {
        ApplicationContext applicationContext = new ClassPathXmlApplicationContext("applicationContext.xml");
        BookDao bookDao01 = (BookDao) applicationContext.getBean("bookDao");
        System.out.println(bookDao01);
        BookDao bookDao02 = (BookDao) applicationContext.getBean("bookDao");
        System.out.println(bookDao02);
    }
  • 得出结果两个输出地址相同,说明 Spring 默认创建 bean 的是一个单例
  • 修改配置文件,增加 scope 属性
    <bean id="bookDao" class="com.example.dao.impl.BookDaoImpl" scope="prototype"/>
  • 此时,就会得到不同的两个地址

属性

  • 由于 Spring 创建的 bean 默认为单例的,得出
    • 适合交给容器进行管理的 bean
      • 表现层对象
      • 业务层对象
      • 数据层对象
      • 工具对象
    • 不适合交给容器进行管理的 bean
      • 封装实体的域对象

5. bean 实例化

实例化 bean 的三种方式(最后一种是基于第三种的改良)

(1) 构造方法实例化
  • Spring 创建 bean 的时候调用的是无参的构造方法,不论公共还是私有的都可以访问到,进一步说明了 Spring 通过反射进行访问的
    • 无参构造方法如果不存在,将会抛出异常 BeanCreationException
  • Spring 构造下的报错信息的阅读:采用从后向前的读法(最前面的报错信息是所有错误的整合),查找,以 Caused by: 开头的报错信息,解决问题
(2) 静态工厂实例化

这种方式了解即可

  • 首先创建 OrderDao 接口和 OrderDaoImpl 实现类,创建 factory 目录(与 dao 目录同级)其中创建 OrderDaoFactory 静态工厂类实现创建 bean 对象
public interface OrderDao {
    public void save();
}
public class OrderDaoImpl implements OrderDao {
    @Override
    public void save() {
        System.out.println("order dao save...");
    }
}
  • 静态工厂
public class OrderDaoFactory{
	public static OrderDao getOrderDao(){
		// 此处的工厂类中还可以执行一些其他操作,所以不能直接进行 new 对象操作,而跳过工厂这一步
		return new OrderDaoImpl();
	}
}
  • 配置文件
<!-- 使用静态工厂实例化 bean-->
<!-- factory-method 属性就是配置目标类中:真正实现创建对象的方法名-->
<bean id="orderDao" class="com.example.factory.OrderDaoFactory" factory-method="getOrderDao"/>
  • 测试
    public static void main(String[] args) {
        ApplicationContext applicationContext = new ClassPathXmlApplicationContext("applicationContext.xml");
        OrderDao orderDao = (OrderDao) applicationContext.getBean("orderDao");
        orderDao.save();
    }
(3) 实例工厂实例化

这种方法了解即可

  • 首先创建 UserDao 接口和 UserDaoImpl 实现类,创建 factory 目录(与 dao 目录同级)其中创建 UserDaoFactory 实例工厂类实现创建 bean 对象
public interface UserDao {
    public void save();
}
public class UserDaoImpl implements UserDao {
    @Override
    public void save() {
        System.out.println("user dao save...");
    }
}
  • 实例工厂
public class UserDaoFactory{
	public UserDao getUserDao(){
		return new UserDaoImpl();
	}
}
  • 配置文件
<!-- 使用实例工厂实例化 bean-->
<bean id="userFactory" class="com.example.factory.UserDaoFactory"/>
<bean id="userDao" factory-method="getUserDao" factory-bean="userFactory"/>
  • 测试
    public static void main(String[] args) {
        ApplicationContext applicationContext = new ClassPathXmlApplicationContext("applicationContext.xml");
        UserDao userDao = (UserDao) applicationContext.getBean("userDao");
        userDao.save();
    }
  • 缺陷显而易见,前一个 bean 标签,配合使用,但是本身实际无意义,第二个 bean 标签中的方法名不固定,每次使用都需要配置
(4) FactoryBean 实例化方式

这种方式是基于实例工厂,Spring 进行的改良

  • 依旧使用创建的 UserDao 接口和 UserDaoImpl 实现类,在 factory 目录中创建 UserDaoFactoryBean 实例工厂类实现创建 bean 对象
public class UserDaoFactoryBean implements FactoryBean<UserDao> {
    /**
     * 代替原始实例工厂中创建对象的方法
     * @return
     * @throws Exception
     */
    @Override
    public UserDao getObject() throws Exception {
        return new UserDaoImpl();
    }

    @Override
    public Class<?> getObjectType() {
        return UserDao.class;
    }
}
  • 配置文件
    <bean id="userDao" class="com.example.factory.UserDaoFactoryBean"/>
  • 依旧使用同样的测试方式
  • 这种创建方式出现的 bean 依旧是单例的,如果需要非单例,则需要多实现一个 isSingleton() 方法
public class UserDaoFactoryBean implements FactoryBean<UserDao> {
    @Override
    public UserDao getObject() throws Exception {
        return new UserDaoImpl();
    }

    @Override
    public Class<?> getObjectType() {
        return UserDao.class;
    }

    @Override
    public boolean isSingleton() {
        // return true; // 单例
        return false; // 非单例
    }
}

6. bean 的生命周期

生命周期:从创建到消亡的完整过程
bean 的生命周期:bean 从创建到销毁的整体过程
bean 生命周期控制:在 bean 创建后到销毁前做的一些事情

  • 生命周期测试
public interface BookDao {
    public void save();
}
public class BookDaoImpl implements BookDao {
    @Override
    public void save() {
        System.out.println("book dao save...");
    }
    // 表示 bean 初始化对应的条件
    public void init(){
        System.out.println("init...");
    }
    // 表示 bean 销毁前对应的操作
    public void destroy(){
        System.out.println("destroy...");
    }
}
    <bean id="bookDao" class="com.example.dao.impl.BookDaoImpl" init-method="init" destroy-method="destroy"/>
    <bean id="bookService" class="com.example.service.impl.BookServiceImpl">
        <property name="bookDao" ref="bookDao"/>
    </bean>
    public static void main(String[] args) {
        ApplicationContext applicationContext = new ClassPathXmlApplicationContext("applicationContext.xml");
        BookDao bookDao = (BookDao) applicationContext.getBean("bookDao");
        bookDao.save();
    }
  • 这种情况下的运行结果,并没有最后 destroy 方法的显示
init...
book dao save...
  • 解释:

    • 虚拟机启动,IoC 容器加载配置并启动
    • 之后 bean 初始化,从容器中拿到 bean 并执行结束
    • 之后程序执行完后,虚拟机退出了
    • 虚拟机没有给容器销毁 bean 的机会,所以没有 destroy 方法的显示
  • 显示 bean 生命周期 destroy 方法

    • 在虚拟机退出之前,先将容器关闭,这种方式偏暴力,直接将容器关闭
    • 设置容器“关闭钩子”
    public static void main(String[] args) {
        ClassPathXmlApplicationContext applicationContext = new ClassPathXmlApplicationContext("applicationContext.xml");
        BookDao bookDao = (BookDao) applicationContext.getBean("bookDao");
        bookDao.save();
        applicationContext.close();
    }
    public static void main(String[] args) {
        ClassPathXmlApplicationContext applicationContext = new ClassPathXmlApplicationContext("applicationContext.xml");
        applicationContext.registerShutdownHook(); // 这段代码写在之后也是可以执行的
        BookDao bookDao = (BookDao) applicationContext.getBean("bookDao");
        bookDao.save();
    }
  • 使用接口方式显示 bean 生命周期
    • 只需要使 Service 实现类实现 InitializingBean、DisposableBean 接口
public class BookServiceImpl implements BookService , InitializingBean, DisposableBean {
    // 删除业务层中使用 new 的方式创建的 dao 对象
    private BookDao bookDao;
    @Override
    public void save() {
        System.out.println("book service save...");
        bookDao.save();
    }
    // 提供对应的 set 方法
    public void setBookDao(BookDao bookDao) {
        this.bookDao = bookDao;
    }

    @Override
    public void destroy() throws Exception {
        System.out.println("service destroy...");
    }

    @Override
    public void afterPropertiesSet() throws Exception {
        System.out.println("service init...");
    }
}
    public static void main(String[] args) {
        ClassPathXmlApplicationContext applicationContext = new ClassPathXmlApplicationContext("applicationContext.xml");
        BookDao bookDao = (BookDao) applicationContext.getBean("bookDao");
        bookDao.save();
        applicationContext.close();
    }
  • 结果
init...
service init...
book dao save...
service destroy...
destroy...
  • 总结:bean 的生命周期

    • 初始化容器
      1. 创建对象(内存分配)
      2. 执行构造方法
      3. 执行属性注入(set 操作)
      4. 执行 bean 初始化方法
    • 使用 bean
      • 执行业务操作
    • 关闭/销毁容器
      • 执行 bean 销毁方法
  • bean 的销毁时机

    • 容器关闭前触发 bean 的销毁
    • 关闭容器方式
      • 手动关闭容器
        • ConfigurableApplicationContext 接口的 close() 操作
      • 注册关闭钩子,在虚拟机退出前先关闭容器再退出虚拟机
        • ConfigurableApplicationContext 接口的 registerShutdownHook() 操作
  • bean 生命周期的控制

    • 配置
      • init-method
      • destroy-method
    • 接口
      • InitializingBean
      • DisposableBean
  • 关闭容器

    • ConfigurableApplicationContext
      • close()
      • registerShutdownHook()

7. 依赖注入方式

引入:
思考:向一个类中传递数据的方式有几种?

  • 普通方法(set 方法)
  • 构造方法

思考:依赖注入描述了在容器中建立 bean 与 bean 之间依赖关系的过程,如果 bean 运行需要的是数字或字符串呢?

  • 引用类型
  • 简单类型(基本数据类型与字符串(String))
  • 依赖注入方式
    • setter 注入
      • 简单类型
      • 引用类型
    • 构造器注入
      • 简单类型
      • 引用类型
(1) setter 注入
① 引用类型
  • 在 bean 中定义引用类型属性并提供可访问的 set 方法
public class BookServiceImpl implements BookService {
    private BookDao bookDao;
    public void setBookDao(BookDao bookDao) {
        this.bookDao = bookDao;
    }
}
  • 配置中使用 property 标签 ref 属性注入引用类型对象
    <bean id="bookDao" class="com.example.dao.impl.BookDaoImpl"/>
    <bean id="bookService" class="com.example.service.impl.BookServiceImpl">
        <property name="bookDao" ref="bookDao"/>
    </bean>
② 简单类型
  • 在 bean 中定义引用类型属性并提供可访问的 set 方法
public class BookDaoImpl implements BookDao {
    private int connectionNum;
    private String databaseName;

    public void setConnectionNum(int connectionNum) {
        this.connectionNum = connectionNum;
    }

    public void setDatabaseName(String databaseName) {
        this.databaseName = databaseName;
    }

    @Override
    public void save() {
        System.out.println("book dao save..."+databaseName+","+connectionNum);
    }
}
  • 配置中使用 property 标签 value 属性注入简单类型数据
    <bean id="bookDao" class="com.example.dao.impl.BookDaoImpl" >
        <property name="databaseName" value="mysql"/>
        <property name="connectionNum" value="10"/>
    </bean>
    <bean id="bookService" class="com.example.service.impl.BookServiceImpl">
        <property name="bookDao" ref="bookDao"/>
    </bean>
(2) 构造器注入
① 引用类型
public class BookServiceImpl implements BookService{
    private BookDao bookDao;
    @Override
    public void save() {
        System.out.println("book service save...");
        bookDao.save();
    }

    public BookServiceImpl(BookDao bookDao) {
        this.bookDao = bookDao;
    }
}
    <bean id="bookDao" class="com.example.dao.impl.BookDaoImpl"/>
    <bean id="bookService" class="com.example.service.impl.BookServiceImpl">
        <!-- 这里的 name 属性中的 “bookDao” 是构造器的形参名称-->
        <constructor-arg name="bookDao" ref="bookDao"/>
    </bean>
        ApplicationContext applicationContext = new ClassPathXmlApplicationContext("applicationContext.xml");
        BookService bookService = (BookService) applicationContext.getBean("bookService");
        bookService.save();
② 简单类型
public class BookDaoImpl implements BookDao {
    private int connectionNum;
    private String databaseName;

    public BookDaoImpl(int connectionNum, String databaseName) {
        this.connectionNum = connectionNum;
        this.databaseName = databaseName;
    }

    @Override
    public void save() {
        System.out.println("book dao save..."+databaseName+","+connectionNum);
    }
}
    <bean id="bookDao" class="com.example.dao.impl.BookDaoImpl">
        <constructor-arg name="databaseName" value="mysql"/>
        <constructor-arg name="connectionNum" value="10"/>
    </bean>
    <bean id="bookService" class="com.example.service.impl.BookServiceImpl">
        <!-- 这里的 name 属性中的 “bookDao” 是构造器的形参名称-->
        <constructor-arg name="bookDao" ref="bookDao"/>
    </bean>
  • 由于具有形参与配置的耦合,所以采用一种去耦合的方式(但是这种方式一旦具有多个相同类型的形参,就无法使用)
    <bean id="bookDao" class="com.example.dao.impl.BookDaoImpl">
        <constructor-arg type="java.lang.String" value="mysql"/>
        <constructor-arg type="int" value="10"/>
    </bean>
    <bean id="bookService" class="com.example.service.impl.BookServiceImpl">
        <!-- 这里的 name 属性中的 “bookDao” 是构造器的形参名称-->
        <constructor-arg name="bookDao" ref="bookDao"/>
    </bean>
  • 为解决参数类型重复问题,引入了一种使用索引的方式,规定形参的位置顺序
    <bean id="bookDao" class="com.example.dao.impl.BookDaoImpl">
        <constructor-arg index="1" value="mysql"/>
        <constructor-arg index="0" value="10"/>
    </bean>
    <bean id="bookService" class="com.example.service.impl.BookServiceImpl">
        <!-- 这里的 name 属性中的 “bookDao” 是构造器的形参名称-->
        <constructor-arg name="bookDao" ref="bookDao"/>
    </bean>
(3) 依赖注入方式的选择
  1. 强制依赖使用构造器进行,使用 setter 注入有概率不进行注入导致 null 对象出现
  2. 可选依赖使用 setter 注入进行,灵活性强
  3. Spring 框架倡导使用构造器,第三方框架内部大多数采用构造器注入的形式进行数据初始化,相对严谨
  4. 如果有必要可以两者同时使用,使用构造器注入完成强制依赖的注入,使用 setter 注入完成可选依赖的注入
  5. 实际开发过程中还要根据实际情况分析,如果受控对象没有提供 setter 方法就必须使用构造器注入
  6. 自己开发的模块推荐使用 setter 注入

8. 依赖自动装配

IoC 容器根据 bean 所依赖的资源在容器中自动查找并注入到 bean 中的过程称为自动装配

  • 自动装配方式
    • 按类型(常用)
    • 按名称
    • 按构造方法
    • 不启用自动装配
(1) 按类型
public class BookServiceImpl implements BookService{
    private BookDao bookDao;
    
    @Override
    public void save() {
        System.out.println("book service save...");
        bookDao.save();
    }

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

}
    <bean id="bookDao" class="com.example.dao.impl.BookDaoImpl"/>
    <bean id="bookService" class="com.example.service.impl.BookServiceImpl" autowire="byType"/>
  • 无法实现多个或零个实现类(满足 dao 接口的实现类)的装配(即 <bean id="bookDao" class="com.example.dao.impl.BookDaoImpl"/> 不可以没有或者是多个)
  • 如果需要实现多个,就需要使用按名称自动装配
(2) 按名称
    <bean id="bookService" class="com.example.service.impl.BookServiceImpl" autowire="byName"/>
  • 使用按名称自动装配的方式,它是按照 setBookDao() 方法中去掉 set 首字母变小写的那个名称 bookDao 判断区分,进行分类的(所以需要用标准命名法)
(3) 依赖自动装配特征
  • 自动装配用于引用类型依赖注入,不能对简单类型进行操作
  • 使用按类型装配时(byType)必须保证容器中相同类型的 bean 唯一,推荐使用
  • 使用按名称装配时(byName)必须保证容器中具有指定名称的 bean,因变量名与配置耦合,不推荐使用
  • 自动装配优先级低于 setter 注入与构造器注入,同时出现时自动装配配置失效

9. 集合注入

五种集合方式:

  1. 数组
  2. List
  3. Set
  4. Map
  5. Properties
public interface BookDao {
    public void save();
}
public class BookDaoImpl implements BookDao {
    private int[] array;
    private List<String> list;
    private Set<String> set;
    private Map<String,String> map;
    private Properties properties;

    public void setArray(int[] array) {
        this.array = array;
    }

    public void setList(List<String> list) {
        this.list = list;
    }

    public void setSet(Set<String> set) {
        this.set = set;
    }

    public void setMap(Map<String, String> map) {
        this.map = map;
    }

    public void setProperties(Properties properties) {
        this.properties = properties;
    }

    @Override
    public void save() {
        System.out.println("book dao save...");
        System.out.println("遍历数组"+ Arrays.toString(array));
        System.out.println("遍历 List"+list);
        System.out.println("遍历 Set"+set);
        System.out.println("遍历 Map"+map);
        System.out.println("遍历 Properties"+properties);
    }
}
    <bean id="bookDao" class="com.example.dao.impl.BookDaoImpl">
        <property name="array">
            <array>
                <!-- 简单类型-->
                <value>1</value>
                <value>2</value>
                <value>3</value>
                <!-- 引用类型-->
                <!-- <ref bean="beanID">-->
            </array>
        </property>
        <property name="list">
            <list>
                <value>one</value>
                <value>two</value>
                <value>three</value>
            </list>
        </property>
        <property name="set">
            <set>
                <value>when</value>
                <value>where</value>
                <value>what</value>
                <value>what</value>
            </set>
        </property>
        <property name="map">
            <map>
                <entry key="country" value="China"/>
                <entry key="province" value="广东"/>
                <entry key="city" value="深圳"/>
            </map>
        </property>
        <property name="properties">
            <props>
                <prop key="country">China</prop>
                <prop key="province">浙江</prop>
                <prop key="city">杭州</prop>
            </props>
        </property>
    </bean>
    public static void main(String[] args) {
        ApplicationContext applicationContext = new ClassPathXmlApplicationContext("applicationContext.xml");
        BookDao bookDao = (BookDao) applicationContext.getBean("bookDao");
        bookDao.save();
    }

10. 数据源对象管理

第三方资源配置管理,以 Druid 连接池和 C3P0 连接池为例

  • 以 Druid 连接池为例
  • 导入依赖
        <dependency>
            <groupId>com.alibaba</groupId>
            <artifactId>druid</artifactId>
            <version>1.2.15</version>
        </dependency>
  • 配置文件
    <!-- 管理 DruidDataSource 对象-->
    <bean id="dataSource" class="com.alibaba.druid.pool.DruidDataSource">
        <property name="driverClassName" value="com.mysql.cj.jdbc.Driver"/>
        <property name="url" value="jdbc:mysql://localhost:3306/spring_db"/>
        <property name="username" value="root"/>
        <property name="password" value="root"/>
    </bean>
  • 测试
    public static void main(String[] args) {
        ApplicationContext applicationContext = new ClassPathXmlApplicationContext("applicationContext.xml");
        DataSource dataSource = (DataSource) applicationContext.getBean("dataSource");
        System.out.println(dataSource);
    }
  • 以 C3P0 连接池为例
  • 导入依赖
        <dependency>
            <groupId>c3p0</groupId>
            <artifactId>c3p0</artifactId>
            <version>0.9.1.2</version>
        </dependency>
        <dependency>
            <groupId>mysql</groupId>
            <artifactId>mysql-connector-java</artifactId>
            <version>5.1.46</version>
        </dependency>
  • 配置文件
    <bean id="dataSource" class="com.mchange.v2.c3p0.ComboPooledDataSource">
        <property name="driverClass" value="com.mysql.jdbc.Driver"/>
        <property name="jdbcUrl" value="jdbc:mysql://localhost:3306/spring_db"/>
        <property name="user" value="root"/>
        <property name="password" value="root"/>
    </bean>

11. Spring 加载 properties 文件

  • properties 配置文件
jdbc.driver=com.mysql.jdbc.Driver
jdbc.url=jdbc:mysql://localhost:3306/spring_db
jdbc.username=root
jdbc.password=root
  • 配置文件,需要配置三部分
    1. 开启 context 命名空间
    2. 使用 context 命名空间,加载指定 properties 文件
    3. 使用 ${} 读取加载的属性值
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xmlns:context="http://www.springframework.org/schema/context"
       xsi:schemaLocation="
       http://www.springframework.org/schema/beans
       http://www.springframework.org/schema/beans/spring-beans.xsd
       http://www.springframework.org/schema/context
       http://www.springframework.org/schema/context/spring-context.xsd
       ">

    <!-- 1. 开启 context 命名空间 -->
    <!-- 2. 使用 context 空间加载 properties 文件 -->
    <context:property-placeholder location="jdbc.properties"/>
    <!-- 3. 使用属性占位符 ${} 读取 properties 文件中的属性 -->
    <bean id="dataSource" class="com.mchange.v2.c3p0.ComboPooledDataSource">
        <property name="driverClass" value="${jdbc.driver}"/>
        <property name="jdbcUrl" value="${jdbc.url}"/>
        <property name="user" value="${jdbc.username}"/>
        <property name="password" value="${jdbc.password}"/>
    </bean>
</beans>
  • 为了显示效果,需要借助一些配置和接口实现方法来展示是否配置成功
public class BookDaoImpl implements BookDao {
    private String name;

    public void setName(String name) {
        this.name = name;
    }

    @Override
    public void save() {
        System.out.println("bookDao save..."+name);
    }
}
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xmlns:context="http://www.springframework.org/schema/context"
       xsi:schemaLocation="
       http://www.springframework.org/schema/beans
       http://www.springframework.org/schema/beans/spring-beans.xsd
       http://www.springframework.org/schema/context
       http://www.springframework.org/schema/context/spring-context.xsd
       ">

    <!-- 1. 开启 context 命名空间 -->
    <!-- 2. 使用 context 空间加载 properties 文件 -->
    <context:property-placeholder location="jdbc.properties"/>
    <!-- 3. 使用属性占位符 ${} 读取 properties 文件中的属性 -->
    <bean id="dataSource" class="com.mchange.v2.c3p0.ComboPooledDataSource">
        <property name="driverClass" value="${jdbc.driver}"/>
        <property name="jdbcUrl" value="${jdbc.url}"/>
        <property name="user" value="${jdbc.username}"/>
        <property name="password" value="${jdbc.password}"/>
    </bean>
    <!-- 此处的配置,仅仅是为了通过 bookDao 展示配置效果-->
    <bean id="bookDao" class="com.example.dao.impl.BookDaoImpl">
        <property name="name" value="${jdbc.url}"/>
    </bean>
</beans>
    public static void main(String[] args) {
        ApplicationContext applicationContext = new ClassPathXmlApplicationContext("applicationContext.xml");
		// DataSource dataSource = (DataSource) applicationContext.getBean("dataSource");
		// System.out.println(dataSource);
        BookDao bookDao = (BookDao) applicationContext.getBean("bookDao");
        bookDao.save();
    }
  • 对于加载 properties 文件,context 空间加载的多种配置
  1. 不加载系统属性(为了避免使用中的操作系统设置干扰)
    <context:property-placeholder location="jdbc.properties" system-properties-mode="NEVER"/>
  1. 加载多个 properties 文件
    <context:property-placeholder location="jdbc.properties,msg.properties"/>
  1. 加载所有 properties 文件
    <context:property-placeholder location="*.properties"/>
  1. 加载 properties 文件标准格式(本工程中的 properties 文件)(推荐使用)
    <context:property-placeholder location="classpath:*.properties"/>
  1. 从类路径或 jar 包中搜索并加载 properties 文件(其他工程和 jar 包中的 properties 文件)
    <context:property-placeholder location="classpath*:*.properties"/>

12. 容器

(1) 创建容器
  • 创建容器分为两种
        // 1. 加载类路径下的配置文件
        ApplicationContext applicationContext01 = new ClassPathXmlApplicationContext("applicationContext.xml");
        // 2. 从文件系统下加载配置文件(绝对路径)
        ApplicationContext applicationContext02 = new ClassPathXmlApplicationContext("E:\\Test\\spring-test\\src\\main\\resources\\applicationContext.xml");
        // 加载多个配置文件
        ApplicationContext applicationContext01 = new ClassPathXmlApplicationContext("bean01.xml,bean02.xml");
(2) 获取 bean
  • 获取 bean 分为三种
        // 1. 获取 bean ,常规的类型强转式写法
        BookDao bookDao01 = (BookDao) applicationContext.getBean("bookDao");
        // 2. 获取 bean 的时候,通过它的名称指定它的类型
        BookDao bookDao02 = applicationContext.getBean("bookDao",BookDao.class);
        // 3. 获取 bean 的时候,直接按类型找,但是前提条件容器中对应的 bean 只有它一个(必须唯一)
        BookDao bookDao03 = applicationContext.getBean(BookDao.class);
(3) 容器类层次结构

层次结构
(CTRL + H :查看该类或接口的层次结构)

(4) BeanFactory(bean 的最高层次接口)
  • 使用 BeanFactroy 接口实现需求的操作
    • 类路径加载配置文件
    <bean id="bookDao" class="com.example.dao.impl.BookDaoImpl"/>
    public static void main(String[] args) {
        Resource resources = new ClassPathResource("applicationContext.xml");
        BeanFactory beanFactory = new XmlBeanFactory(resources);
        BookDao bookDao = beanFactory.getBean(BookDao.class);
        bookDao.save();
    }
  • BeanFactory 与 ApplicationContext 的核心区别:两者加载 bean 的时机不同
    • BeanFactory 是延迟加载 bean ,ApplicationContext 是立即加载 bean(启动容器直接初始化完毕)
    • 验证:通过构造器测试
public class BookDaoImpl implements BookDao {
    public BookDaoImpl() {
        System.out.println("constructor...");
    }

    @Override
    public void save() {
        System.out.println("bookDao save...");
    }
}
    public static void main(String[] args) {
    	// 1. BeanFactory 的加载
        Resource resources = new ClassPathResource("applicationContext.xml");
        BookDao bookDao = beanFactory.getBean(BookDao.class);
		// 2. ApplicationContext 的加载
        ApplicationContext applicationContext = new ClassPathXmlApplicationContext("applicationContext.xml");
    }
  • 要想 ApplicationContext 实现延迟加载,需要在配置中加入一个属性 lazy-init="true"
    <bean id="bookDao" class="com.example.dao.impl.BookDaoImpl" lazy-init="true"/>

13. 核心容器总结

(1) 容器相关
  • BeanFactory 是 IoC 容器的顶层接口,初始化 BeanFactory 对象时,加载的 bean 延迟加载
  • ApplicationContext 接口是 Spring 容器的核心接口,初始化时 bean 立即加载
  • ApplicationContext 接口提供基础的 bean 操作相关方法,通过其他接口扩展其功能
  • ApplicationContext 接口常用初始化类
    • ClassPathXmlApplicationContext
    • FileSystemXmlApplicationContext
(2) bean 相关
    <bean id="bookDao"                                      bean  ID
          name="bookDaoImpl daoImpl"                        bean 的别名
          class="com.example.dao.impl.BookDaoImpl"          bean 的类型,静态工厂类,FactoryBean 
          scope="singleton"                                 控制 bean 的实例数量
          init-method="init"                                生命周期的初始化方法
          destroy-method="destroy"                          生命周期的销毁方法
          autowire="byType"                                 自动装配类型
          factory-method="getInstance"                      bean 工厂方法,应用于静态工厂或实例工厂
          factory-bean="com.example.factory.BookDaoFactory" 实例工厂 bean
          lazy-init="true"                                  控制 bean 延迟加载
    />
(3) 依赖注入相关
    <bean id="bookService" class="com.example.service.impl.BookServiceImpl">
        <constructor-arg name="bookDao" ref="bookDao"/>                     构造器注入引用类型
        <constructor-arg name="userDao" ref="userDao"/> 
        <constructor-arg name="msg" value="WARN"/>                          构造器注入简单类型
        <constructor-arg type="java.lang.String" index="3" value="WARN"/>   类型匹配与索引匹配
        <property name="bookDao" ref="bookDao"/>                            setter 注入引用类型
        <property name="userDao" ref="userDao"/>
        <property name="msg" value="WARN"/>                                 setter 注入简单类型
        <property name="names">                                             setter 注入集合类型
            <list>                                                          list 集合
                <value>example</value>                                      集合注入简单类型
                <ref bean="dataSource"/>                                    集合注入引用类型
            </list>
        </property>
    </bean>

四、 注解开发

1. 注解开发定义 bean

  • 使用 @Component 定义 bean
@Component("bookDao") // 可以理解为指定名称,指定的名称就是 ID
public class BookDaoImpl implements BookDao {
    @Override
    public void save() {
        System.out.println("bookDao save...");
    }
}
@Component
public class BookServiceImpl implements BookService{
    private BookDao bookDao;

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

    public void setBookDao(BookDao bookDao) {
        this.bookDao = bookDao;
    }
}
  • 核心配置文件中通过组件加载 bean
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xmlns:context="http://www.springframework.org/schema/context"
       xsi:schemaLocation="
       http://www.springframework.org/schema/beans
       http://www.springframework.org/schema/beans/spring-beans.xsd
       http://www.springframework.org/schema/context
       http://www.springframework.org/schema/context/spring-context.xsd
       ">

    <!--指定到哪一级,系统就会递归查找该级目录下的所有类以及子包中的类-->
    <context:component-scan base-package="com.example"/>

</beans>
    public static void main(String[] args) {
        ApplicationContext applicationContext = new ClassPathXmlApplicationContext("applicationContext.xml");
        BookDao bookDao = (BookDao) applicationContext.getBean("bookDao");
        System.out.println(bookDao);
        BookServiceImpl bookService = applicationContext.getBean(BookServiceImpl.class);
        System.out.println(bookService);
    }
  • Spring 提供了 @Component 注解的三个衍生注解(功能和 @Component 一样,只是为了方便理解和区分)
    • @Controller :用于表现层 bean 定义
    • @Service :用于业务层 bean 定义
    • @Repository :用于数据层 bean 定义

2. 纯注解开发

Spring 3.0 升级了纯注解开发模式,使用 Java 类替代配置文件,开启了 Spring 快速开发的赛道

  • 创建 SpringConfig 类代替 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"
       xmlns:context="http://www.springframework.org/schema/context"
       xsi:schemaLocation="
       http://www.springframework.org/schema/beans
       http://www.springframework.org/schema/beans/spring-beans.xsd
       http://www.springframework.org/schema/context
       http://www.springframework.org/schema/context/spring-context.xsd
       ">
    <context:component-scan base-package="com.example"/>
</beans>
  • 改为写成配置类,作用完全一样
@Configuration
@ComponentScan("com.example")
public class SpringConfig {
}
  • 测试类
    public static void main(String[] args) {
        ApplicationContext annotationConfigApplicationContext = new AnnotationConfigApplicationContext(SpringConfig.class);
        BookDao bookDao = (BookDao) annotationConfigApplicationContext.getBean("bookDao");
        System.out.println(bookDao);
        BookServiceImpl bookService = annotationConfigApplicationContext.getBean(BookServiceImpl.class);
        System.out.println(bookService);
    }
  • 注意:
    • @Configuration 注解用于设定当前类为配置类
    • @ComponentScan 注解用于设定扫描路径,此注解只能添加一次,多个数据需要使用数组格式
    • 两种方式对比:读取 Spring 核心配置文件初始化容器对象切换为读取 Java 配置类初始化容器对象
@ComponentScan({"com.example.service","com.example.dao"})

3. bean 的管理

(1) bean 的作用范围
  • 使用 @Scope 注解定义 bean 的作用范围
@Repository
// @Scope("singleton") // 单例模式
@Scope("prototype") // 非单例模式
public class BookDaoImpl implements BookDao {
    @Override
    public void save() {
        System.out.println("bookDao save...");
    }
}
@Configuration
@ComponentScan("com.example")
public class SpringConfig {
}
    public static void main(String[] args) {
        ApplicationContext annotationConfigApplicationContext = new AnnotationConfigApplicationContext(SpringConfig.class);
        BookDao bookDao01 = annotationConfigApplicationContext.getBean(BookDao.class);
        BookDao bookDao02 = annotationConfigApplicationContext.getBean(BookDao.class);
        System.out.println(bookDao01);
        System.out.println(bookDao02);
    }
(2) bean 的生命周期
  • 使用 @PostConstruct、@PreDestroy 注解定义 bean 生命周期
@Repository
@Scope("singleton") // 单例模式
public class BookDaoImpl implements BookDao {
    @Override
    public void save() {
        System.out.println("bookDao save...");
    }
    @PostConstruct // 在构造器之后
    public void init(){
        System.out.println("init...");
    }
    @PreDestroy // 在销毁之前
    public void destroy(){
        System.out.println("destroy...");
    }
}
  • 注意销毁时的,“关闭钩子”或“关闭容器”操作,需要使用 AnnotationConfigApplicationContext 这个类
    public static void main(String[] args) {
        AnnotationConfigApplicationContext annotationConfigApplicationContext = new AnnotationConfigApplicationContext(SpringConfig.class);
        BookDao bookDao01 = annotationConfigApplicationContext.getBean(BookDao.class);
        BookDao bookDao02 = annotationConfigApplicationContext.getBean(BookDao.class);
        System.out.println(bookDao01);
        System.out.println(bookDao02);
        annotationConfigApplicationContext.close();
    }

4. 依赖注入自动装配

(1) 引用类型注入
  • 使用 @Autowired 注解开启自动装配模式(按类型)
@Configuration
@ComponentScan("com.example")
public class SpringConfig {
}
  • 单个数据层的实现类按类型进行装配
@Repository
public class BookDaoImpl implements BookDao {
    @Override
    public void save() {
        System.out.println("bookDao save...");
    }
}
@Service
public class BookServiceImpl implements BookService{
    @Autowired // 此处是按类型装配的,使用反射中的暴力反射
    private BookDao bookDao;

    @Override
    public void save() {
        System.out.println("book service save...");
        bookDao.save();
    }
}
    public static void main(String[] args) {
        AnnotationConfigApplicationContext annotationConfigApplicationContext = new AnnotationConfigApplicationContext(SpringConfig.class);
        BookService bookService = annotationConfigApplicationContext.getBean(BookService.class);
        bookService.save();
    }
  • 多个数据层的实现类,使用按名称进行装配
    • 使用 @Qualifier 注解开启指定名称装配 bean
    • @Qualifier 注解无法单独使用,必须配合 @Autowired 注解使用
@Repository("bookDao01")
public class BookDaoImpl01 implements BookDao {
    @Override
    public void save() {
        System.out.println("bookDao01 save...");
    }
}
@Repository("bookDao02")
public class BookDaoImpl02 implements BookDao {
    @Override
    public void save() {
        System.out.println("bookDao02 save...");
    }
}
@Service
public class BookServiceImpl implements BookService{
    @Autowired
    @Qualifier("bookDao02") // 使用该注解,还必须依赖 @Autowired 注解
    // 如果没有相同类型的 bean,一般使用不到这个注解
    private BookDao bookDao;

    @Override
    public void save() {
        System.out.println("book service save...");
        bookDao.save();
    }
}
  • 注意:
    • 自动装配基于反射设计创建对象并暴力反射对应属性为私有属性初始化数据,因此无需提供 setter 方法
    • 自动装配建议使用无参构造方法创建对象(默认),如果不提供对应的构造方法,需要提供唯一的构造方法
(2) 简单类型注入

由于 value 这个单词用于简单类型的注入,所以简单类型也常被称为“值类型”

  • 使用 @Value 实现简单类型注入
@Repository("bookDao")
public class BookDaoImpl implements BookDao {
    @Value("example")
    private String name;
    @Override
    public void save() {
        System.out.println("bookDao save..."+name);
    }
}
  • 简单类型的数据多数是会变化的,所以可能会用到引入 properties 文件引入数据
name=example
@Configuration
@ComponentScan("com.example")
@PropertySource("data.properties")
public class SpringConfig {
}
@Repository("bookDao")
public class BookDaoImpl implements BookDao {
    @Value("${name}")
    private String name;
    @Override
    public void save() {
        System.out.println("bookDao save..."+name);
    }
}
  • 注意:
    • @PropertySource 注解需要引入多个配置文件时,需要使用 @PropertySource({"data01.properties","data02.properties","data03.properties"}) 的格式
    • @PropertySource 注解路径仅支持单一文件配置,不支持使用通配符

5. 第三方 bean 管理

第三方 bean 管理
第三方 bean 依赖注入

(1) 第三方 bean 管理
① 基本写法
  • 导入 Druid 依赖坐标
        <dependency>
            <groupId>com.alibaba</groupId>
            <artifactId>druid</artifactId>
            <version>1.2.15</version>
        </dependency>
  • 使用 @Bean 注解配置第三方 bean ,然后定义一个方法,方法定义完成后,方法的返回值就是需要的 bean(这种操作与工厂模式很像,就是用来替代工厂模式的)
@Configuration
public class SpringConfig {
    // 1. 定义一个方法获得要管理的对象
    // 2. 添加 @Bean 注解,表示当前方法的返回值是一个 bean
    @Bean
    public DataSource dataSource(){
        DruidDataSource druidDataSource = new DruidDataSource();
        druidDataSource.setDriverClassName("com.mysql.cj.jdbc.Driver");
        druidDataSource.setUrl("jdbc:mysql://localhost:3306/spring_db");
        druidDataSource.setUsername("root");
        druidDataSource.setPassword("root");
        return druidDataSource;
    }
}
    public static void main(String[] args) {
        AnnotationConfigApplicationContext annotationConfigApplicationContext = new AnnotationConfigApplicationContext(SpringConfig.class);
        DataSource dataSource = annotationConfigApplicationContext.getBean(DataSource.class);
        System.out.println(dataSource);
    }
② 解耦写法

由于示例中 JDBC 所属的配置文件写到 Spring 配置文件中,假如再多一些配置,会导致 Spring 配置臃肿,所以一般采用单独抽出需求的配置,最后导入 Spring 配置的方式

  • 将独立的配置加入到核心配置中
Ⅰ、 扫描式
  • 将 JDBC 配置文件,也添加为配置类,之后在 Spring 配置文件中扫描加载导入(不推荐使用)
  • 使用 @ComponentScan 注解扫描配置类所在的包,加载对应的配置类信息
@Configuration
@ComponentScan("com.example.config")
public class SpringConfig {
}
@Configuration
public class JdbcConfig {
    @Bean
    public DataSource dataSource(){
        DruidDataSource druidDataSource = new DruidDataSource();
        druidDataSource.setDriverClassName("com.mysql.cj.jdbc.Driver");
        druidDataSource.setUrl("jdbc:mysql://localhost:3306/spring_db");
        druidDataSource.setUsername("root");
        druidDataSource.setPassword("root");
        return druidDataSource;
    }
}
Ⅱ、 导入式
  • 这种方式可以清楚的看清导入了哪些配置文件(推荐使用)
  • 使用 @Import 注解手动加入配置到核心配置,此注解只能添加一次,多个数据需要使用数组格式
@Configuration
@Import({JdbcConfig.class}) // 依旧是支持数组形式的导入
public class SpringConfig {
}
public class JdbcConfig {
    @Bean
    public DataSource dataSource(){
        DruidDataSource druidDataSource = new DruidDataSource();
        druidDataSource.setDriverClassName("com.mysql.cj.jdbc.Driver");
        druidDataSource.setUrl("jdbc:mysql://localhost:3306/spring_db");
        druidDataSource.setUsername("root");
        druidDataSource.setPassword("root");
        return druidDataSource;
    }
}
(2) 第三方 bean 依赖注入
① 简单类型注入
public class JdbcConfig {
    @Value("com.mysql.cj.jdbc.Driver")
    private String driver;
    @Value("jdbc:mysql://localhost:3306/spring_db")
    private String url;
    @Value("root")
    private String username;
    @Value("root")
    private String password;

    @Bean
    public DataSource dataSource() {
        DruidDataSource druidDataSource = new DruidDataSource();
        druidDataSource.setDriverClassName(driver);
        druidDataSource.setUrl(url);
        druidDataSource.setUsername(username);
        druidDataSource.setPassword(password);
        return druidDataSource;
    }
}
② 引用类型注入
  • 假如此处 JDBC 配置文件需要导入 BookDao 这个类
  • 在核心配置文件中,扫描加载导入
@Configuration
@ComponentScan("com.example")
@Import({JdbcConfig.class})
public class SpringConfig {
}
@Repository
public class BookDaoImpl implements BookDao {
    @Override
    public void save() {
        System.out.println("bookDao save...");
    }
}
public class JdbcConfig {
    @Value("com.mysql.cj.jdbc.Driver")
    private String driver;
    @Value("jdbc:mysql://localhost:3306/spring_db")
    private String url;
    @Value("root")
    private String username;
    @Value("123456")
    private String password;

    @Bean
    public DataSource dataSource(BookDao bookDao) { // 引用类型需要在方法名之后的形参位置填上,所需要的引用数据
        System.out.println(bookDao);
        DruidDataSource druidDataSource = new DruidDataSource();
        druidDataSource.setDriverClassName(driver);
        druidDataSource.setUrl(url);
        druidDataSource.setUsername(username);
        druidDataSource.setPassword(password);
        return druidDataSource;
    }
}
    public static void main(String[] args) {
        AnnotationConfigApplicationContext annotationConfigApplicationContext = new AnnotationConfigApplicationContext(SpringConfig.class);
        DataSource dataSource = annotationConfigApplicationContext.getBean(DataSource.class);
        System.out.println(dataSource);
    }
  • 原理:通过自动装配的方法达到只添加一个形参变量可以达到导入的效果,系统检测到了配置正在配置一个的 bean,所以系统就会在容器中找这个类型的 bean 并自动提供(按类型自动装配)
    • 只要在该示例中将 BookDaoImpl@Repository 注解删除就会导致系统找不到这个类型的 bean 报错
    • 简单来说:引用类型注入只需要为 bean 定义方法设置形参即可,容器会根据类型自动装配对象

6. 总结

  • XML 配置与注解配置比较
    比较

五、 Spring 整合

1. Spring 整合 Mybatis

(1) 简单的 Mybatis 应用项目
  • 数据库中创建一张简易表格table_account,作为数据查询
    表格
  • 导入依赖
        <dependency>
            <groupId>org.mybatis</groupId>
            <artifactId>mybatis</artifactId>
            <version>3.5.6</version>
        </dependency>
        <dependency>
            <groupId>com.alibaba</groupId>
            <artifactId>druid</artifactId>
            <version>1.2.15</version>
        </dependency>
        <dependency>
            <groupId>mysql</groupId>
            <artifactId>mysql-connector-java</artifactId>
            <version>5.1.47</version>
        </dependency>
        <dependency>
            <groupId>org.springframework</groupId>
            <artifactId>spring-context</artifactId>
            <version>5.3.24</version>
        </dependency>

  • 新建 domain 目录并创建 Account 实体类
public class Account implements Serializable {
	private Integer id;
	private String name;
	private Double money;

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

    public Integer getId() {
        return id;
    }

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

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public Double getMoney() {
        return money;
    }

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

  • JDBC 配置文件 jdbc.properties
jdbc.driver=com.mysql.jdbc.Driver
jdbc.url=jdbc:mysql://localhost:3306/spring_data?useSSL=false
jdbc.username=root
jdbc.password=root
  • 数据层接口 AccountDao 类
public interface AccountDao {
    @Insert("insert into table_account(name,money) values(#{name},#{money})")
    void save(Account account);
    @Delete("delete from table_account where id=#{id}")
    void delete(Integer id);
    @Update("update table_account set name=#{name},money=#{money} where id=#{id}")
    void update(Account account);
    @Select("select * from table_account")
    List<Account> findAll();
    @Select("select * from table_account where id=#{id}")
    Account findById(Integer id);
}
  • 业务层接口 AccountService 接口
public interface AccountService {
    public void save(Account account);
    public void update(Account account);
    public void delete(Integer id);
    public Account findById(Integer id);
    public List<Account> findAll();
}
  • 接口实现类
public class AccountServiceImpl implements AccountService {
    private AccountDao accountDao;
    @Override
    public void save(Account account) {
        accountDao.save(account);
    }

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

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

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

    @Override
    public List<Account> findAll() {
        return accountDao.findAll();
    }
}
  • 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 druidDataSource = new DruidDataSource();
        druidDataSource.setDriverClassName(driver);
        druidDataSource.setUrl(url);
        druidDataSource.setUsername(username);
        druidDataSource.setPassword(password);
        return druidDataSource;
    }
}
  • Mybatis 核心配置类
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE configuration
        PUBLIC "-//mybatis.org//DTD Config 3.0//EN"
               "http://mybatis.org/dtd/mybatis-3-config.dtd">
<configuration>
    <properties resource="jdbc.properties"></properties>
        <typeAliases>
            <package name="com.example.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.example.dao"></package>
    </mappers>
</configuration>
  • 测试类
    public static void main(String[] args) throws Exception {
        // 1. 创建 SqlSessionFactoryBuilder 对象
        SqlSessionFactoryBuilder sqlSessionFactoryBuilder = new SqlSessionFactoryBuilder();
        // 2. 加载 SqlMapConfig.xml 配置文件
        InputStream inputStream = Resources.getResourceAsStream("SqlMapConfig.xml");
        // 3. 创建 SqlSessionFactory 对象
        SqlSessionFactory sqlSessionFactory =sqlSessionFactoryBuilder.build(inputStream);
        // 4. 获取 SqlSession
        SqlSession sqlSession = sqlSessionFactory.openSession();
        // 5. 执行 SqlSession 对象执行查询,获取结果 User
        AccountDao accountDao = sqlSession.getMapper(AccountDao.class);
        Account account = accountDao.findById(1);
        System.out.println(account);
        // 6.  释放资源
        sqlSession.close();
    }
(2) 思路分析
  • 分析测试类,得到操作的核心对象是 SqlSessionFactory
    public static void main(String[] args) throws Exception {
    
    	// 初始化 SqlSessionFactory
        // 1. 创建 SqlSessionFactoryBuilder 对象
        SqlSessionFactoryBuilder sqlSessionFactoryBuilder = new SqlSessionFactoryBuilder();
        // 2. 加载 SqlMapConfig.xml 配置文件
        InputStream inputStream = Resources.getResourceAsStream("SqlMapConfig.xml");
        // 3. 创建 SqlSessionFactory 对象
        SqlSessionFactory sqlSessionFactory =sqlSessionFactoryBuilder.build(inputStream);

		// 获取连接,获取实现
        // 4. 获取 SqlSession
        SqlSession sqlSession = sqlSessionFactory.openSession();
        // 5. 执行 SqlSession 对象执行查询,获取结果 User
        AccountDao accountDao = sqlSession.getMapper(AccountDao.class);

		// 获取数据层接口
        Account account = accountDao.findById(1);
        System.out.println(account);
	
		// 关闭连接
        // 6.  释放资源
        sqlSession.close();
    }
  • 分析 Mybatis 核心配置类
<!-- 初始化属性数据 -->
<configuration>
    <properties resource="jdbc.properties"></properties>

<!-- 初始化类型别名 -->
        <typeAliases>
            <package name="com.example.domain"/>
        </typeAliases>

<!-- 初始化 DataSource -->
        <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.example.dao"></package>
    </mappers>
</configuration>
  • 得到 Mybatis 应该管理 SqlSessionFactory 对象
(3) Spring 整合 Mybatis
  • 导入依赖(注意版本对应)
        <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>
  • 使用注解进行开发
  • 创建 config 目录并在该目录下创建 SpringConfig
@Configuration
@ComponentScan("com.example")
@PropertySource("classpath:jdbc.properties")
@Import({JdbcConfig.class,MybatisConfig.class})
public class SpringConfig {
}
  • 实现类添加注解
@Service
public class AccountServiceImpl implements AccountService {
    @Autowired
    private AccountDao accountDao;
    @Override
    public void save(Account account) {
        accountDao.save(account);
    }

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

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

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

    @Override
    public List<Account> findAll() {
        return accountDao.findAll();
    }
}
  • 配置数据源,在 config 目录下创建 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(BookDao bookDao) {
        DruidDataSource druidDataSource = new DruidDataSource();
        druidDataSource.setDriverClassName(driver);
        druidDataSource.setUrl(url);
        druidDataSource.setUsername(username);
        druidDataSource.setPassword(password);
        return druidDataSource;
    }
}
  • config 目录下创建 MybatisConfig 类,进行替代 Mybatis 核心配置类
public class MybatisConfig {
    @Bean
    public SqlSessionFactoryBean sqlSessionFactoryBean(DataSource dataSource){
        SqlSessionFactoryBean sqlSessionFactoryBean = new SqlSessionFactoryBean();
        sqlSessionFactoryBean.setTypeAliasesPackage("com.example.domain");
        sqlSessionFactoryBean.setDataSource(dataSource);
        return sqlSessionFactoryBean;
    }
    @Bean
    public MapperScannerConfigurer mapperScannerConfigurer(){
        MapperScannerConfigurer mapperScannerConfigurer = new MapperScannerConfigurer();
        mapperScannerConfigurer.setBasePackage("com.example.dao");
        return mapperScannerConfigurer;
    }
}
  • 测试:
    public static void main(String[] args) throws IOException {
        ApplicationContext applicationContext = new AnnotationConfigApplicationContext(SpringConfig.class);
        AccountService accountService = applicationContext.getBean(AccountService.class);
        Account account = accountService.findById(1);
        System.out.println(account);
    }

2. Spring 整合 JUnit

在上文中整合 Mybatis 的前提下,进行 Junit 整合

(1) 导入依赖
		<dependency>
            <groupId>junit</groupId>
            <artifactId>junit</artifactId>
            <version>4.13.2</version>
            <scope>test</scope>
        </dependency>
        <dependency>
            <groupId>org.springframework</groupId>
            <artifactId>spring-test</artifactId>
            <version>5.2.10.RELEASE</version>
        </dependency>
(2) 测试
  • 使用 Spring 整合 Junit 专用的类加载器
@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(classes = SpringConfig.class)
public class AccountServiceTest {
    @Autowired
    private AccountService accountService;
    @Test
    public void testFindById(){
        System.out.println(accountService.findById(2));
    }
    @Test
    public void testFindAll(){
        System.out.println(accountService.findAll());
    }
}

六、 AOP

1. AOP 简介

(1) AOP 核心概念
  • AOP(Aspect Oriented Programming)面向切面编程,一种编程范式,知道开发者如何组织程序结构
    • OOP(Object Oriented Programming)面向对象编程
  • AOP 作用:
    • 在不惊动原始设计的基础上为其进行功能增强
  • Spring 理念:无侵入式/无入侵式
  • 核心概念:
    • 代理(Proxy):SpringAOP 的核心本质是采用代理模式实现的
    • 连接点(JoinPoint):程序执行过程中的任意位置,粒度为执行方法,抛出异常,设置变量等
      • 在 SpringAOP 中,理解为任意方法的执行
    • 切入点(PointCut):匹配连接点的式子
      • 一个具体的方法:com.example.dao 包下的 BookDao 接口中的无形参无返回值的 save 方法
      • 匹配多个方法:所有的 save 方法,所有 get 开头的方法,所有以 Dao 结尾的接口中的任意方法,所有带有一个参数的方法
    • 通知(Advice):在切入点处执行的操作,也就是共性功能
      • 在 SpringAOP 中,功能最终以方法的形式呈现(若干方法的共性功能,在切入点处执行,最终体现为一个方法)
    • 通知类:定义通知的类
    • 切面(Aspect):描述通知与切入点的对应关系
    • 目标对象(Target):被代理的原始对象称为目标对象

2. AOP 入门案例

案例设定:测试接口执行效率
简化设定:在接口执行前输出当前系统时间
开发模式:XML 或者注解

  • 思路分析:
    1. 导入坐标(pom.xml
    2. 制作连接点方法(原始操作,Dao 接口与实现类)
    3. 制作共性功能(通知类与通知)
(1) 导入依赖
        <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>
  • 导入 spring-context 依赖默认自动导入 spring_aop 依赖
    依赖
  • 常规的定义接口,定义实现类
public interface BookDao {
    public void save();
    public void update();
}
@Repository
public class BookDaoImpl implements BookDao {
    @Override
    public void save() {
        System.out.println("book dao save");
        System.out.println(System.currentTimeMillis()); // 原本程序中的方法
    }

    @Override
    public void update() {
        System.out.println("book dao update");
    }
}
  • 正常的运行测试类
    public static void main(String[] args) {
        ApplicationContext applicationContext = new AnnotationConfigApplicationContext(SpringConfig.class);
        BookDao bookDao = applicationContext.getBean(BookDao.class);
        bookDao.update();
    }
  • 仅需改动配置类中的部分内容,以及定义一个通知类
  • 创建一个 aop 目录,定义通知类 MyAdvice
    • 定义切入点:切入点定义依托一个不具有实际意义的方法进行,即无参数,无返回值,方法体无实际逻辑
    • 绑定切入点与通知关系,并指定通知添加到原始连接点的具体执行位置
@Component
@Aspect
public class MyAdvice {
    // 定义切入点(在哪里执行)
    @Pointcut("execution(void com.example.dao.BookDao.update())")
    public void pointCut(){}

    @Before("pointCut()") // 绑定切入点和通知
    // 定义通知(共性功能)
    public void method(){
        System.out.println(System.currentTimeMillis());
    }
}
@Configuration
@ComponentScan("com.example")
@EnableAspectJAutoProxy
public class SpringConfig {
}

3. AOP 工作流程

  1. Spring 容器启动
  2. 读取所有切面配置中的切入点(只会读取配置的切入点)
    工作流程
  3. 初始化 bean,判定 bean 对应的类中的方法是否匹配到任意切入点
    • 匹配失败,创建对象
    • 匹配成功,创建原始对象(目标对象)的代理对象
  4. 获取 bean 执行方法
    • 获取 bean ,调用方法并执行,完成操作
    • 获取的 bean 是代理对象时,根据代理对象的运行模式运行原始方法与增强的内容,完成操作
  • AOP 核心概念
    • 目标对象(Target):原始功能去掉共性功能对应的类产生的对象,这种对象是无法直接完成最终工作的
    • 代理(Proxy):目标对象无法直接完成工作,需要对其进行功能回填,通过原始对象的代理对象实现

4. AOP 切入点表达式

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

  • 切入点表达式:要进行增强的方法的描述方式

  • 对于上述示例

  1. 描述方式一:执行 com.example.dao 包下的 BookDao 接口中的无参数 update 方法:execution(void com.example.dao.BookDao.update())
  2. 描述方式二:执行 com.example.dao.impl 包下的 BookDaoImpl 类中的无参数 update 方法:execution(void com.example.dao.impl.BookDaoImpl.update())
  • 切入点表达式标准格式:动作关键字(访问修饰符 返回值 包名.类/接口名.方法名(参数)异常名):execution(public User com.example.service.UserService.findById(int))
    • 动作关键字:描述切入点行为动作,例如 execution 表示执行到指定切入点
    • 访问修饰符:public,private 等,可以省略
    • 返回值
    • 包名
    • 类/接口名
    • 方法名
    • 参数
    • 异常名:方法定义中抛出指定异常,可以省略
(2) 通配符
  • 可以使用通配符描述切入点,快速描述
    • * :单个独立的任意符号,可以独立出现,也可以作为前缀或者后缀的匹配符出现
      • execution(public * com.example.*.UserService.find*(*))
      • 匹配 com.example 包下的任意包中的 UserService 类或接口中所有 find 开头的带有一个参数的方法
    • .. :多个连续的任意符号,可以独立出现,常用于简化包名与参数的书写
      • execution(public User com..UserService.findById(..))
      • 匹配 com 包下的任意包中的 UserService 类或接口中的所有名称为 findById 的方法
    • + :专用于匹配子类类型
      • execution(* *..*Service+.*(..))
(3) 书写技巧
  • 书写技巧:
    • 所有代码按照标准规范开发,否则一下技巧全部失效
    • 描述切入点通常描述接口,而不描述实现类
    • 访问控制修饰符针对接口开发均采用 public 描述(可省略访问控制修饰符描述
    • 返回值类型对于增删改类使用精准类型加速匹配,对于查询类使用 * 通配符快速描述
    • 包名书写尽量不使用 .. 匹配,效率过低,常用 * 做单个包描述匹配,或精准匹配
    • 接口名/类名书写名称与模块相关的采用 * 匹配,例如 UserService 书写成 *Service ,绑定业务层接口名
    • 方法名书写以动词进行精准匹配,名词采用 * 匹配,例如 getById 书写成 getBy*selectAll 书写成 selectAll
    • 参数规则较为复杂,根据业务方法灵活调整
    • 通常不使用异常作为匹配规则

5. AOP 通知类型

  • AOP 通知描述了抽取的共性功能,根据共性功能抽取的位置不同,最终运行代码时要将其加入到合适的位置
  • AOP 通知共分为5种类型
    • 前置通知
    • 后置通知
    • 环绕通知(重点)
      • 环绕通知依赖形参 ProceedingJoinPoint 才能实现对原始方法的调用
      • 环绕通知可以隔离原始方法的调用执行
      • 环绕通知返回值设置为 Object 类型
      • 环绕通知中可以对原始方法调用过程中出现的异常进行处理
    • 返回后通知(了解)
    • 抛出异常后通知(了解)
(1) 前置和后置通知
① 前置通知
  • 名称:@Before
  • 类型:方法注解
  • 位置:通知方法定义上方
  • 作用:设置当前通知方法与切入点之间的绑定关系,当前通知方法在原始切入点方法后运行
  • 相关属性:value(默认):切入点方法,格式为: 类名.方法名()
② 后置通知
  • 名称:@After
  • 类型:方法注解
  • 位置:通知方法定义上方
  • 作用:设置当前通知方法与切入点之间的绑定关系,当前通知方法在原始切入点方法前运行
  • 相关属性:value(默认):切入点方法,格式为: 类名.方法名()
③ 测试使用
  • 接口
public interface BookDao {
    public void update();
    public int select();
}
  • 实现类
@Repository
public class BookDaoImpl implements BookDao {

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

    @Override
    public int select() {
        System.out.println("book dao select is running...");
        return 100;
    }
}
  • 配置
@Configuration
@ComponentScan("com.example")
@EnableAspectJAutoProxy
public class SpringConfig {
}
  • 通知类
@Component
@Aspect
public class MyAdvice {
    @Pointcut("execution(void com.example.dao.BookDao.update())")
    public void pointCut(){}

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

    @After("pointCut()")
    public void after(){
        System.out.println("after advice ...");
    }
}
  • 测试
    public static void main(String[] args) {
        ApplicationContext applicationContext = new AnnotationConfigApplicationContext(SpringConfig.class);
        BookDao bookDao = applicationContext.getBean(BookDao.class);
        bookDao.update();
    }
(2) 环绕通知
① 环绕通知
  • 名称:@Around
  • 类型:方法注解
  • 位置:通知方法定义上方
  • 作用:设置当前通知方法与切入点之间的绑定关系,当前通知方法在原始切入点方法前后运行
  • 相关属性:value(默认):切入点方法,格式为: 类名.方法名()
② 测试使用
@Component
@Aspect
public class MyAdvice {
    @Pointcut("execution(void com.example.dao.BookDao.update())")
    public void pointCut(){}
    @Pointcut("execution(int com.example.dao.BookDao.select())")
    public void pointCut02(){}

    @Around("pointCut()")
    public void around(ProceedingJoinPoint proceedingJoinPoint) throws Throwable { // 所以这里标准的写法应该是返回 Object 类
        System.out.println("around before advice ...");
        // 表示对原始操作的调用
        proceedingJoinPoint.proceed();
        System.out.println("around after advice ...");
    }

    @Around("pointCut02()")
    public Object aroundSelect(ProceedingJoinPoint proceedingJoinPoint) throws Throwable {
        System.out.println("around before advice ...");
        // 表示对原始操作的调用
        Object proceed = proceedingJoinPoint.proceed();
        // 环绕通知对于存在返回值的方法可以取出返回值,通常使用 Object 类返回,如果需要具体的类型,需要进行强制类型转换
        System.out.println("around after advice ...");
        return proceed;
    }
}
  • 测试
    public static void main(String[] args) {
        ApplicationContext applicationContext = new AnnotationConfigApplicationContext(SpringConfig.class);
        BookDao bookDao = applicationContext.getBean(BookDao.class);
        int select = bookDao.select();
        System.out.println(select);
    }
③ Around 注意事项
  1. 环绕通知必须依赖形参 ProceedingJoinPoint 才能实现对原始方法的调用,进而实现原始方法调用前后同时添加通知
  2. 通知中如果未使用 ProceedingJoinPoint 对原始方法进行调用将跳过原始方法的执行(就会产生一种对原始操作隔离的效果,可以做一些权限校验
  3. 对原始方法的调用可以不接收返回值,通知方法设置成 void 即可,如果接收返回值,必须设定成 Object 类型
  4. 原始方法的返回值如果是 void 类型,通知方法的返回值类型可以设置成 void,也可以设置成 Object
  5. 由于无法预知原始方法运行后是否会抛出异常,因此环绕通知方法必须抛出 Throwable 对象
  • 规范写法:
    @Around("pointCut()")
    public Object aroundSelect(ProceedingJoinPoint proceedingJoinPoint) throws Throwable {
        System.out.println("around before advice ...");
        Object ret = proceedingJoinPoint.proceed();
        System.out.println("around after advice ...");
        return ret;
    }
(3) 返回后通知
① 返回后通知
  • 名称:@AfterReturning
  • 类型:方法注解
  • 位置:通知方法定义上方
  • 作用:设置当前通知方法与切入点之间的绑定关系,当前通知方法在原始切入点方法正常执行完毕后运行
  • 相关属性:value(默认):切入点方法,格式为: 类名.方法名()
② 测试使用
  • 对比于后置通知
@Repository
public class BookDaoImpl implements BookDao {

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

    @Override
    public int select() {
        System.out.println("book dao select is running...");
        int i=1/0; // 故意写出错误
        return 100;
    }
}
@Component
@Aspect
public class MyAdvice {
    @Pointcut("execution(void com.example.dao.BookDao.update())")
    public void pointCut(){}
    @Pointcut("execution(int com.example.dao.BookDao.select())")
    public void pointCut02(){}

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

    @AfterReturning("pointCut02()") // 只有在方法正常运行没有抛异常时,此方法才会运行
    public void afterReturning(){
        System.out.println("afterReturning advice ...");
    }
}
(4) 抛出异常后通知
① 返回后通知
  • 名称:@AfterThrowing
  • 类型:方法注解
  • 位置:通知方法定义上方
  • 作用:设置当前通知方法与切入点之间的绑定关系,当前通知方法在原始切入点方法运行抛出异常后执行
  • 相关属性:value(默认):切入点方法,格式为: 类名.方法名()
② 测试使用
@Repository
public class BookDaoImpl implements BookDao {

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

    @Override
    public int select() {
        System.out.println("book dao select is running...");
        // int i=1/0; // 故意写出错误
        // 如果此处没有异常,则不会显示抛出异常后通知,反之,如果存在异常,则会执行后文中的抛出异常后通知
        return 100;
    }
}
@Component
@Aspect
public class MyAdvice {
    @Pointcut("execution(void com.example.dao.BookDao.update())")
    public void pointCut(){}
    @Pointcut("execution(int com.example.dao.BookDao.select())")
    public void pointCut02(){}

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

6. 案例:测量业务层接口万次执行效率

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

  • 分析:

    1. 业务功能:业务层接口执行前后分别记录时间,求差值得到执行效率
    2. 通知类型选择前后均可以增强的类型,使用**环绕通知 **
  • 依赖于上文示例 Account 数据表中的案例

  • 环境准备:依赖导入、jdbc.propertiesJdbcConfigMybatisConfigAccountAccountDaoAccountServiceAccountServiceImpl

  • 改动配置

@Configuration
@ComponentScan("com.example")
@PropertySource("classpath:jdbc.properties")
@Import({JdbcConfig.class,MybatisConfig.class})
@EnableAspectJAutoProxy
public class SpringConfig {
}
  • AOP 的核心配置
@Component
@Aspect
public class ProjectAdvice {
    // 匹配业务层的所有方法
    @Pointcut("execution(* com.example.service.*Service.*(..))")
    public void servicePointCut(){}
    @Around("ProjectAdvice.servicePointCut()")
    public void runSpeed(ProceedingJoinPoint proceedingJoinPoint) throws Throwable {
        Signature signature = proceedingJoinPoint.getSignature();
        String className = signature.getDeclaringTypeName();
        String methodName = signature.getName();
        long start = System.currentTimeMillis();
        for (int i = 0; i < 10000; i++) {
            proceedingJoinPoint.proceed();
        }
        long end = System.currentTimeMillis();
        System.out.println("万次执行:"+className+"."+methodName+"------> "+(end-start)+"ms");
    }
}
  • 测试类,使用 Junit 测试类中创建方法进行测试
@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(classes = SpringConfig.class)
public class AccountServiceTestCase {
    @Autowired
    private AccountService accountService;
    @Test
    public void testFindById(){
        Account account = accountService.findById(1);
        System.out.println(account);
    }
    @Test
    public void testFindAll(){
        List<Account> all = accountService.findAll();
        System.out.println(all);
    }
}

7. AOP 通知获取数据

  1. 获取参数

    • 获取切入点方法的参数
      • JoinPoint:适用于前置、后置、返回后、抛出异常后通知
      • ProceedJoinPoint:适用于环绕通知
  2. 获取返回值

    • 获取切入点方法返回值
      • 返回后通知
      • 环绕通知
  3. 获取异常

    • 获取切入点方法运行异常信息
      • 抛出异常后通知
      • 环绕通知
(1) 获取参数
  • 测试使用:

    • JointPoint 对象描述了连接点方法的运行状态,可以获取到原始方法的调用参数
    • ProceedJoinPointJoinPoint 的子类
  • 使用上文中的依赖

  • 接口

public interface BookDao {
    public String findName(int id,String password);
}
  • 实现类
@Repository
public class BookDaoImpl implements BookDao {
    @Override
    public String findName(int id, String password) {
        System.out.println("id:"+id);
        return "example";
    }
}
  • 配置
@Configuration
@ComponentScan("com.example")
@EnableAspectJAutoProxy
public class SpringConfig {
}
  • 核心 AOP 配置
@Component
@Aspect
public class MyAdvice {
    @Pointcut("execution(* com.example.dao.BookDao.findName(..))")
    private void pointCut(){}

	// 此处物种方式都能实现获取参数,此处仅演示了前三种
	
    // @Before("pointCut()")
    public void before(JoinPoint joinPoint){
        Object[] args = joinPoint.getArgs();
        System.out.println(Arrays.toString(args));
        System.out.println("before advice ...");
    }
    // @After("pointCut()")
    public void after(JoinPoint joinPoint){
        Object[] args = joinPoint.getArgs();
        System.out.println(Arrays.toString(args));
        System.out.println("after advice ...");
    }
    @Around("pointCut()")
    public Object around(ProceedingJoinPoint proceedingJoinPoint) throws Throwable {
        Object[] args = proceedingJoinPoint.getArgs();
        System.out.println(Arrays.toString(args));
        args[0]=666; // 在调用原方法传递参数之前,改动参数值,如果本需要传递的参数有问题,此处就可以添加默认值,增强程序的健壮性
        Object ret = proceedingJoinPoint.proceed(args);
        return ret;
    }
    // @AfterReturning("pointCut()")
    public void afterReturning(){
        System.out.println("afterReturning advice ...");
    }
    // @AfterThrowing("pointCut()")
    public void afterThrowing(){
        System.out.println("afterThrowing advice ...");
    }
}
  • 测试
    public static void main(String[] args) {
        ApplicationContext applicationContext = new AnnotationConfigApplicationContext(SpringConfig.class);
        BookDao bookDao = applicationContext.getBean(BookDao.class);
        String name = bookDao.findName(100,"123456");
        System.out.println(name);
    }
(2) 获取返回值
  • 测试使用:
    • 抛出异常后通知可以获取切入点方法中出现的异常信息,使用形参可以接收对应的异常对象
    • 环绕通知中可以手工书写对原始方法的调用,得到的结果即为原始方法的返回值
@Component
@Aspect
public class MyAdvice {
    @Pointcut("execution(* com.example.dao.BookDao.findName(..))")
    private void pointCut(){}

	// 以下两种操作都可以实现,主要演示后一种
    @Around("pointCut()")
    public Object around(ProceedingJoinPoint proceedingJoinPoint) throws Throwable {
        Object[] args = proceedingJoinPoint.getArgs();
        System.out.println(Arrays.toString(args));
        args[0]=666; // 在调用原方法传递参数之前,改动参数值,如果本需要传递的参数有问题,此处就可以添加默认值,增强程序的健壮性
        Object ret = proceedingJoinPoint.proceed(args);
        return ret;
    }
    // 
    @AfterReturning(value = "pointCut()",returning = "ret")
    public void afterReturning(JoinPoint joinPoint,Object ret){ // 如果此处同时使用 JoinPoint 和返回值,必须 JoinPoint 在前
        System.out.println("afterReturning advice ..."+ ret);
    }
}
(3) 获取异常
  • 抛出异常后通知可以获取切入点中出现的异常信息,使用形参可以接收对应的异常对象
  • 抛出异常后通知可以获取切入点方法运行的异常信息,使用形参可以接收运行时抛出的异常对象
@Component
@Aspect
public class MyAdvice {
    @Pointcut("execution(* com.example.dao.BookDao.findName(..))")
    private void pointCut(){}

	// 以下两种方式均可实现
    @Around("pointCut()")
    public Object around(ProceedingJoinPoint proceedingJoinPoint) {
        Object[] args = proceedingJoinPoint.getArgs();
        System.out.println(Arrays.toString(args));
        args[0]=666; // 在调用原方法传递参数之前,改动参数值,如果本需要传递的参数有问题,此处就可以添加默认值,增强程序的健壮性
        Object ret = null;
        try {
            ret = proceedingJoinPoint.proceed(args);
        } catch (Throwable e) {
            e.printStackTrace(); // 此处执行具体的操作
        }
        return ret;
    }
    
    @AfterThrowing(value = "pointCut()",throwing = "throwable")
    public void afterThrowing(Throwable throwable){ // 如果使用这种方式,需要原程序出现异常
        System.out.println("afterThrowing advice ..."+ throwable);
    }
}

8. 案例:百度网盘分享链接输入密码数据错误兼容性处理

  • 需求:对百度网盘分享链接输入密码时尾部多输入的空格做兼容处理

  • 分析

    1. 在业务方法执行之前对所有的输入参数进行格式处理:trim()
    2. 使用处理后的参数调用原始方法,使用环绕通知中存在对原始方法的调用
  • 依赖导入

        <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>
  • 接口及实现类
public interface ResourcesService {
    public boolean openURL(String url,String password);
}
@Service
public class ResourcesServiceImpl implements ResourcesService {
    @Autowired
    private ResourcesDao resourcesDao;
    @Override
    public boolean openURL(String url, String password) {
        return resourcesDao.readResources(url,password);
    }
}
public interface ResourcesDao {
    boolean readResources(String url, String password);
}
@Repository
public class ResourcesDaoImpl implements ResourcesDao {
    @Override
    public boolean readResources(String url, String password) {
        // 模拟校验
        return password.equals("root");
    }
}
  • 配置
@Configuration
@ComponentScan("com.example")
@EnableAspectJAutoProxy
public class SpringConfig {
}
  • AOP 核心配置
@Component
@Aspect
public class DataAdvice {
    @Pointcut("execution(boolean com.example.service.*Service.*(*,*))")
    private void servicePointCut(){}
    @Around("DataAdvice.servicePointCut()")
    public Object trimString(ProceedingJoinPoint proceedingJoinPoint) throws Throwable {
        Object[] args = proceedingJoinPoint.getArgs();
        for (int i = 0; i < args.length; i++) {
            if (args[i].getClass().equals(String.class)){
                args[i] = args[i].toString().trim();
            }
        }
        Object ret = proceedingJoinPoint.proceed(args);
        return ret;
    }
}
    public static void main(String[] args) {
        ApplicationContext applicationContext = new AnnotationConfigApplicationContext(SpringConfig.class);
        ResourcesService resourcesService = applicationContext.getBean(ResourcesService.class);
        boolean flag = resourcesService.openURL("http://pan.baidu.com/resources", "root ");
        System.out.println(flag);
    }

七、 Spring 事务

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

1. Spring 事务简介

以案例形式进行演示: 案例:模拟银行账户间转账业务

(1) 案例分析

需求:实现任意两个账户间转账操作
需求微缩:A 账户减钱,B 账户加钱

  • 分析:
    1. 数据层提供基础操作,指定账户减钱(outMoney),指定账户加钱(inMoney)
    2. 业务层提供转账操作(transfer),调用减钱与加钱的操作
    3. 提供两个账号和操作金额执行转账操作
    4. 基于 Spring 整合 MyBatis 环境搭建上述操作
  • 结果分析:
    • 程序正常执行时,账户金额 A 减少 B 增加,没有出现问题
    • 程序出现异常,转账失败,但是异常之前的操作成功,异常之后操作失败,整体业务失败
(2) 代码实现
  • 数据库,依旧采用前文中的 Account 账户,JDBC 配置文件
public class Account implements Serializable {
    private Integer id;
    private String name;
    private Double money;

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

    public Integer getId() {
        return id;
    }

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

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public Double getMoney() {
        return money;
    }

    public void setMoney(Double money) {
        this.money = money;
    }
}
  • 导入依赖:
		<dependency>
            <groupId>junit</groupId>
            <artifactId>junit</artifactId>
            <version>4.13.2</version>
            <scope>test</scope>
        </dependency>
        <dependency>
            <groupId>org.springframework</groupId>
            <artifactId>spring-test</artifactId>
            <version>5.2.10.RELEASE</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>org.mybatis</groupId>
            <artifactId>mybatis</artifactId>
            <version>3.5.6</version>
        </dependency>
        <dependency>
            <groupId>com.alibaba</groupId>
            <artifactId>druid</artifactId>
            <version>1.1.16</version>
        </dependency>
        <dependency>
            <groupId>mysql</groupId>
            <artifactId>mysql-connector-java</artifactId>
            <version>5.1.47</version>
        </dependency>
  • 接口及实现类
public interface AccountDao {
    @Update("update table_account set money = money + #{money} where name=#{name}")
    void inMoney(@Param("name") String name,@Param("money") Double money);
    @Update("update table_account set money = money - #{money} where name= #{name}")
    void outMoney(@Param("name") String name,@Param("money") Double money);
}
  • 整合 MyBatis :
public class MybatisConfig {
    @Bean
    public SqlSessionFactoryBean sessionFactoryBean(DataSource dataSource){
        SqlSessionFactoryBean sqlSessionFactoryBean = new SqlSessionFactoryBean();
        sqlSessionFactoryBean.setTypeAliasesPackage("com.example.domain");
        sqlSessionFactoryBean.setDataSource(dataSource);
        return sqlSessionFactoryBean;
    }
    @Bean
    public MapperScannerConfigurer mapperScannerConfigurer(){
        MapperScannerConfigurer mapperScannerConfigurer = new MapperScannerConfigurer();
        mapperScannerConfigurer.setBasePackage("com.example.dao");
        return mapperScannerConfigurer;
    }
}
  • 在业务层接口上添加 Spring 事务管理
public interface AccountService {
    /**
     * 在接口处开启事务管理
     * @param out
     * @param in
     * @param money
     */
    @Transactional
    public void transfer(String out,String in,Double money);
}
  • 注意事项:
    • Spring 注解式事务通常添加在业务层接口中而不会添加到业务层实现类中,降低耦合
    • 注解式事务可以添加到业务方法上表示当前方法开启事务,也可以添加到接口上表示当前接口所有方法开启事务
@Service
public class AccountServiceImpl implements AccountService {
    @Autowired
    private AccountDao accountDao;
    public void transfer(String out,String in,Double money){
        accountDao.outMoney(out,money);
        int i=1/0; // 故意定义一个错误,查看事务处理
        accountDao.inMoney(in,money);
    }
}
  • JDBC 配置文件,设置事务管理器
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 druidDataSource = new DruidDataSource();
        druidDataSource.setDriverClassName(driver);
        druidDataSource.setUrl(url);
        druidDataSource.setUsername(username);
        druidDataSource.setPassword(password);
        return druidDataSource;
    }

    /**
     * 定义一个事务管理器,并且将这个事务管理器交给 Spring 管理
     * @param dataSource
     * @return
     */
    @Bean
    public PlatformTransactionManager transactionManager(DataSource dataSource){
        DataSourceTransactionManager transactionManager = new DataSourceTransactionManager();
        transactionManager.setDataSource(dataSource);
        return transactionManager;
    }
}
  • 注意事项:

    • 事务管理器要根据实现技术进行选择
    • MyBatis 框架使用的是 JDBC 事务
  • Spring 配置文件:开启注解式事务驱动

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

2. Spring 事务角色

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

3. Spring 事务属性

(1) 事务属性

事务属性

  • 关于设置事务回滚异常
    • 以下两种情况下事务才会回滚,否则,其他异常情况下不会进行事务回滚
      • Error 系列(例如:内存溢出)
      • 运行时异常(例如:空指针异常)
  • 测试并解决设置事务回滚异常
  • 故意抛出异常
@Service
public class AccountServiceImpl implements AccountService{
    @Autowired
    private AccountDao accountDao;
    public void transfer(String out,String in,Double money) throws IOException {
        accountDao.outMoney(out,money);
        if (true) throw new IOException(); // 故意定义一个异常,查看事务处理
        accountDao.inMoney(in,money);
    }
}
  • 设置事务回滚异常解决
public interface AccountService {
    /**
     * 在接口处开启事务管理
     * @param out
     * @param in
     * @param money
     */
    @Transactional(rollbackFor = {IOException.class})
    public void transfer(String out,String in,Double money) throws IOException;
}
(2) 案例:转账业务追加日志

需求:实现任意两个账户间转账操作,并对每次转账操作在数据库进行留痕
需求微缩:A 账户减少钱,B 账户增加钱,数据库记录日志

  • 分析:
    1. 基于转账操作案例添加日志模块,实现数据库中记录日志
    2. 业务层转账操作(transfer),调用减少钱,增加钱与记录日志功能
  • 实现效果预期:
    • 无论转账操作是否成功,均进行转账操作的日志留痕
① 环境准备
  • 创建一张 MySQL 数据表
    • id 字段,数据类型为 INT
    • info 字段,数据类型为 LONGTEXT
    • createDate 字段,数据类型为 DATETIME
② 改动与添加部分
  • 数据层日志接口
public interface LogDao {
    @Insert("insert into table_log (info,createDate) values(#{info},now())")
    void log(String info);
}
  • 业务层日志接口
public interface LogService {
    @Transactional
    void log(String put,String in,Double money);
}
  • 业务层日志实现类
@Service
public class LogServiceImpl implements LogService {
    @Autowired
    private LogDao logDao;
    @Override
    public void log(String out, String in, Double money) {
        logDao.log("转账操作由"+out+"到"+in+",金额:"+money);
    }
}
  • 改动账户实现类添加日志记录操作
@Service
public class AccountServiceImpl implements AccountService{
    @Autowired
    private AccountDao accountDao;
    @Autowired
    private LogService logService;
    public void transfer(String out,String in,Double money){
        try {
            accountDao.outMoney(out,money);
            accountDao.inMoney(in,money);
        } finally { // 保证记录日志的代码一定执行
            logService.log(out,in,money);
        }
    }
}
③ 事务传播问题出现
  • 存在的问题:
    • 日志的记录与转账操作隶属于同一个事务,同成功同失败
  • 实现效果预期改进
    • 无论转账操作是否成功,日志必须保留
(3) 事务传播行为

事务传播行为:事务协调员事务管理员所携带事务的处理态度

  • 事务传播行为
    事务传播行为

  • 在日志的业务层接口中设置事务的传播行为

    • 在业务层接口上添加 Spring 事务,设置事务传播行为 REQUIRES_NEW需要新事务
public interface LogService {
    @Transactional(propagation = Propagation.REQUIRES_NEW)
    void log(String put,String in,Double money);
}
  • 更改账户业务层使其故意出现错误
@Service
public class AccountServiceImpl implements AccountService{
    @Autowired
    private AccountDao accountDao;
    @Autowired
    private LogService logService;
    public void transfer(String out,String in,Double money){
        try {
            accountDao.outMoney(out,money);
            int i=1/0;
            accountDao.inMoney(in,money);
        } finally { // 保证记录日志的代码一定执行
            logService.log(out,in,money);
        }
    }
}
  • 最终结果就是出现账户金额并未变动,但日志成功记录
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值