Spring学习记录

Spring相关

目录

文章目录

  • Spring相关
    • 目录
    • 前言
    • 工厂设计模式
      • 静态工厂模式
      • 通用工厂模式
    • ApplicationContext
      • ClassPathXmlApplicationContext
      • XmlWebApplicationContext
      • ApplicationContext创建对象的原理
      • 整合多个applicationContext.xml
    • Spring整合日志框架
    • Spring注入
      • Set注入
        • 1) set注入步骤
        • 2) set注入原理
        • 3) set注入详解
        • 4) p命名空间
      • 构造注入
        • 1) 构造注入步骤
        • 2) 构造注入详解
        • 3) c命名空间
      • 对比两种注入方式
      • autowire 自动装配
    • 控制反转与依赖注入
    • Spring工厂生产复杂对象
      • 什么是复杂对象?
      • 创建复杂对象
        • 实现FactoryBean接口
        • 实例工厂与静态工厂
    • 控制对象创建次数
    • Spring Bean的生命周期
      • 1) 创建阶段
      • 2) 初始化阶段
      • 3) 销毁阶段
    • 配置文件参数化
    • 自定义类型转换器
    • BeanPostProcessor
    • 代理设计模式
      • 静态代理模式
      • 动态代理模式
        • JDK动态代理
        • Cglib动态代理
    • Spring 动态代理
      • 实现
      • MethodBeforeAdvice接口
      • MethodInterceptor接口
      • 切入点
        • 切入点表达式
        • 切入点函数
      • Spring 动态代理的底层原理
      • 切面类实现Spring动态代理
        • @Aspect 切面类
        • @Pointcut 切入点复用
      • JDK与Cglib动态代理的切换
      • ApplicationContextAware接口
      • 对AOP的一些术语的理解
    • Spring注解编程
      • 配置
      • 注解与xml混用
      • @Component
      • @Scope
      • @Lazy
      • @PostConstruct / @PreDestroy
      • 自定义类型变量的注入
        • @Autowired
        • @Qualifier
        • @Resource
      • Java类型变量的注入
        • @Value
      • 注解扫描详解
      • ------分割线-------
      • @Configuration
      • @Bean
        • 注册Bean
        • 对Bean的属性进行注入
      • @ComponentScan
    • 选择哪种方式注册Bean ?
    • 整合Bean的配置信息
    • 纯注解实现Spring动态代理
    • Spring整合Mybatis
      • 为什么要整合
      • 非注解形式整合
      • 注解形式整合
    • Spring事务管理
      • 什么是事务 ?
      • 传统的事务管理方式
      • 半注解形式
      • 非注解形式
      • 全注解形式
      • Spring事务管理属性
        • 隔离属性
        • 传播属性
        • 只读属性
        • 超时属性
        • 回滚策略
    • 使用YAML进行Spring开发
      • 什么是YAML?
      • YAML语法介绍
      • 使用YAML进行Spring开发
      • YAML开发中存在的问题

前言

计划利用一些时间学习一下Spring框架(开始于 2021年7月6日)

视频参考: https://www.bilibili.com/video/BV185411477k

课程笔记参考: 《孙哥说Spring5》学习笔记_代码改变世界-CSDN博客

Spring框架版本 5.1.4 Release

工厂设计模式

工厂模式是Spring核心IOC(控制翻转)所参考的一个重要的设计模式, 可以解耦合

工厂模式理解了没有? - SegmentFault 思否

https://www.bilibili.com/video/BV185411477k?p=7

使用new来创建对象, 具有很强的耦合性

ProductService productService = new ProductServiceImpl();
ProductDao productdao = new ProductDaoImpl();

所以我们便可以利用工厂设计模式, 使用工厂对象来生产对象, 从而解耦合

静态工厂模式

/**
 * 生产对象的工厂类: 利用反射来创建对象
 */
public class BeanFactory {

    public static Properties properties = new Properties();
    public static InputStream inputStream = null;

    static{
        try {
            // 打开输入流,IO操作一般放在static块中, 只操作一次
            InputStream inputStream = BeanFactory.class.getResourceAsStream("applicationContext.properties");
            // 读取属性文件
            properties.load(inputStream);
        } catch (IOException e) {
            e.printStackTrace();
        } finally {
            try {
                inputStream.close();
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
    }

    public static ProductService getProductServiceImpl(){
        ProductService productService = null;
        try {
            // 利用反射创建对象
            Class clazz = Class.forName(properties.getProperty("productServiceImpl"));
            productService = (ProductService) clazz.newInstance();
        } catch (ClassNotFoundException e) {
            e.printStackTrace();
        } catch (IllegalAccessException e) {
            e.printStackTrace();
        } catch (InstantiationException e) {
            e.printStackTrace();
        }
        return productService;
    }

    public static ProductDao getProductDaoImpl(){
        ProductDao productDao = null;
        Class clazz = null;
        try {
            clazz = Class.forName(properties.getProperty("productDaoImpl"));
            productDao = (ProductDao) clazz.newInstance();
        } catch (ClassNotFoundException e) {
            e.printStackTrace();
        } catch (IllegalAccessException e) {
            e.printStackTrace();
        } catch (InstantiationException e) {
            e.printStackTrace();
        }
        return productDao;
    }
}

属性文件:

# 格式: key = value
productServiceImpl = com.shy.service.impl.ProductServiceImpl
productDaoImpl = com.shy.dao.impl.ProductDaoImpl

创建对象:

ProductDao productDao = BeanFactory.getProductDaoImpl();
ProductService productService = BeanFactory.getProductServiceImpl();

通用工厂模式

对于静态工厂, 假如有许多对象, 则需要对每一个对象都编写一个生产对象的方法. 这些代码明显是重复的, 所以可以抽出公共部分, 封装成一个方法, 成为通用工厂

package com.shy.factory;

import com.shy.dao.ProductDao;
import com.shy.service.ProductService;

import java.io.IOException;
import java.io.InputStream;
import java.util.Properties;

/**
 * 生产对象的工厂类: 利用反射来创建对象
 */
public class BeanFactory {

    public static Properties properties = new Properties();
    public static InputStream inputStream = null;

    static{
        try {
            // 打开输入流,IO操作一般放在static块中, 只操作一次
            InputStream inputStream = BeanFactory.class.getResourceAsStream("/applicationContext.properties");
            // 读取属性文件
            properties.load(inputStream);
            inputStream.close();
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

    // 将生产对象, 抽象成一个通用的方法
    public static Object getBean(String key){
        Object object = null;
        Class clazz = null;
        try {
            clazz = Class.forName(properties.getProperty(key));
            object = clazz.newInstance();
        } catch (ClassNotFoundException e) {
            e.printStackTrace();
        } catch (IllegalAccessException e) {
            e.printStackTrace();
        } catch (InstantiationException e) {
            e.printStackTrace();
        }
        return object;
    }
}

创建对象:

ProductDao productDao = (ProductDao) BeanFactory.getBean("productDaoImpl");        
ProductService productService = (ProductService) BeanFactory.getBean("productServiceImpl");

ApplicationContext

前文提到spring参考工厂模式创建对象, 而ApplicationContext就是Spring框架用于创建对象的工厂

需要注意的是, 创建ApplicationContext会占用大量资源

所以一般一个应用只创建一个ApplicationContext对象

有可能被多个线程同时访问, 是线程安全的

此接口有两个实现类:

ClassPathXmlApplicationContext

(非web环境下使用, 如单元测试)

首先创建一个spring工厂

ApplicationContext applicationContext = new ClassPathXmlApplicationContext("/applicationContext.xml");

关于路径问题:

观察maven编译后生成的target目录, 会发现 javaresources被合并到一个目录.

所以可以认为javaresource下的文件的根目录相同

Spring学习记录_第1张图片

Spring学习记录_第2张图片

applicationContext.xml中配置Product实体类的信息

标签


<bean id="product" name="p1,p2,p3" class="com.shy.entity.Product"/>
  
<bean class="com.shy.entity.Product"/> 
<bean class="com.shy.entity.Product"/> 

image-20210707220127545


<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="product" class="com.shy.entity.Product"/>
beans>

然后利用该工厂创建对象

Product product = (Product) applicationContext.getBean("product");

下面简单了解一些ApplicationContext 的方法

  1. getBean 创建对象

    // 根据applicationContext.xml中bean的 id|name 值创建对象
    Product product = (Product) applicationContext.getBean("product");
    Product product = applicationContext.getBean("product",Product.class); // 不需要强转
    // 根据applicationContext.xml中bean的class值创建对象
    Product product = (Product) applicationContext.getBean(Product.class);
    
  2. getBeanDefinitionNames

    // 获取applicationContext.xml文件中所有bean标签的id值
    String[] beanDefinitionNames = applicationContext.getBeanDefinitionNames();
    for (String beanDefinitionName : beanDefinitionNames) {
        System.out.println("beanDefinitionName = " + beanDefinitionName);
    }
    
  3. getBeanNamesForType

    // 获取applicationContext.xml文件中所有特定类型的bean标签的id值
    String[] beanDefinitionNames = applicationContext.getBeanNamesForType(Product.class);
    
  4. containsBeanDefinition 判断是否存在指定 id 值的 bean

  5. containsBean判断是否存在指定 id|name 值的 bean

XmlWebApplicationContext

(web环境下使用)

ApplicationContext创建对象的原理

(未涉及源码)

在实际开发中,并不是所有的对象都交由applicationContext创建

像实体类entity一般由数据库框架来创建

Spring学习记录_第3张图片

public class Product {
    public String name;

    public Product() {
        System.out.println("调用了无参构造");
    }
}

默认情况下, ApplicationContext在创建对象时, 会调用对象的无参构造

整合多个applicationContext.xml

在实际开发中, 往往将applicationContext.xml文件按照一定的层级分开(如DAO,Service,Controller), 从而方便维护

假设有以下几个applicationContext.xml文件:

applicationContext-DAO.xml
applicationContext-Service.xml
applicationContext-Controller.xml 
  • 使用通配符*

    ApplicationContext applicationContext = (ApplicationContext) new ClassPathXmlApplicationContext("applicationContext-*.xml");
    
  • 使用import标签

    
    
    <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">
        <import resource="applicationContext-DAO.xml"/>
        <import resource="applicationContext-Service.xml"/>
        <import resource="applicationContext-Controller.xml"/>
    beans>
    
    ApplicationContext applicationContext = (ApplicationContext) new ClassPathXmlApplicationContext("applicationContext.xml");
    

Spring整合日志框架

  • log4j

首先在pom.xml添加如下代码:

<dependency>
  <groupId>org.slf4jgroupId>
  <artifactId>slf4j-log4j12artifactId>
  <version>1.7.21version>
dependency>
<dependency>
  <groupId>log4jgroupId>
  <artifactId>log4jartifactId>
  <version>1.2.17version>
dependency>

然后在resources下添加log4j.properties配置文件即可

# resources文件夹根目录下
### 配置根
log4j.rootLogger = debug,console

### 日志输出到控制台显示
log4j.appender.console=org.apache.log4j.ConsoleAppender
log4j.appender.console.Target=System.out
log4j.appender.console.layout=org.apache.log4j.PatternLayout
log4j.appender.console.layout.ConversionPattern=%d{yyyy-MM-dd HH:mm:ss} %-5p %c{1}:%L - %m%n

image-20210708204002739

  • logback

    pom.xml

    <dependency>
      <groupId>org.slf4jgroupId>
      <artifactId>slf4j-apiartifactId>
      <version>1.7.25version>
    dependency>
    
    <dependency>
      <groupId>org.slf4jgroupId>
      <artifactId>jcl-over-slf4jartifactId>
      <version>1.7.25version>
    dependency>
    
    <dependency>
      <groupId>ch.qos.logbackgroupId>
      <artifactId>logback-classicartifactId>
      <version>1.2.3version>
    dependency>
    
    <dependency>
      <groupId>ch.qos.logbackgroupId>
      <artifactId>logback-coreartifactId>
      <version>1.2.3version>
    dependency>
    
    <dependency>
      <groupId>org.logback-extensionsgroupId>
      <artifactId>logback-ext-springartifactId>
      <version>0.1.4version>
    dependency>
    

    设置配置文件logback.xml

    
    <configuration>
        
        <appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender">
            <encoder>
        
                <pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{50} - %msg%npattern>
            encoder>
        appender>
    
        <root level="DEBUG">
            <appender-ref ref="STDOUT" />
        root>
    configuration>
    

Spring注入

  • 什么是注入?

    通过 Spring ⼯⼚及配置⽂件,为所创建对象的成员变量赋值

  • 为什么需要注入?

    与传统的使用setter或者constructor为成员变量赋值相比, Spring注入可以解耦合

    product.setName("歼-20");
    product = new Product("山东舰");
    

Set注入

1) set注入步骤

set注入即Spring 调⽤ Set 方法 通过 配置⽂件 为成员变量赋值;

public class Product {
    public String name;

    public Product() {
    }

    // 对需要赋值的属性提供set方法
    public void setName(String name) {
        System.out.println("调用了set方法");
        this.name = name;
    }
    public String getName() {
        return name;
    }
}

<bean id="product" class="com.shy.entity.Product">
    <property name="name">
        <value>红色拖拉机value>
    property>
bean>
@Test
public void test2(){
    ClassPathXmlApplicationContext applicationContext = new ClassPathXmlApplicationContext("/applicationContext.xml");
    Product product = (Product) applicationContext.getBean("product");
    System.out.println("product.name = " + product.name);
}

image-20210708212332869

2) set注入原理

Spring底层调用set方法,完成对属性的赋值

Spring学习记录_第4张图片

3) set注入详解

针对成员变量不同的类型, 在编写applicationContext.xml时的语法不同, 但都应该首先提供set方法

  • String + 基本数据类型及其包装类

    <property name="name">
        <value>红色拖拉机value>
    property>	
    或者
    <property name="name" value="红色拖拉机">property>
    
  • 数组/List

    
    <property name="teams">
        <list>
            <value>McLarenvalue>
            <value>Red Bullvalue>
            <value>Alpha Taurivalue>
        list>
    property>
    

    中的标签不是固定的,而是由数组/List的类型决定

    这里public String[] teams;类型是String,所以用标签

  • Set

    中的标签不是固定的,而是由set的类型决定

    
    <property name="drivers">
        <set>
            <value>Lando Norrisvalue>
            <value>Pierre Gaslyvalue>
            <value>Carlos Sainzvalue>
            <value>Lando Norrisvalue> 
        set>
    property>
    
  • Map

    /中的标签不是固定的,而是由map的类型决定

    
    <map>
        <entry>
            <key><value>McLarenvalue>key>
            <value>Lando Norrisvalue>
        entry>
        <entry value="Pierre Gasly">
            <key><value>Alpha Taurivalue>key>
        entry>
        <entry value="Pierre Gasly" key="Alpha Tauri"/>
    map>
    
  • Properties

    PropertiesMap

    
    <property name="championships">
        <props>
            <prop key="Kimi raikkonen">0prop>
            <prop key="Lewis Hamilton">7prop>
            <prop key="Sebastian Vettel">4prop>
            <prop key="Kimi raikkonen">1prop> 
        props>
    property>
    
  • 用户自定义类型

    public class ProductServiceImpl implements ProductService {
    
        // 使用Spring工厂创建productDao对象
        private  ProductDao productDao;
    
        public ProductDao getProductDao() {
            return productDao;
        }
    
        public void setProductDao(ProductDao productDao) {
            this.productDao = productDao;
        }
        //    ProductDao productDao = new ProductDaoImpl(); new创建对象
        //    ProductDao productDao = BeanFactory.getProductDaoImpl(); 静态工厂
        //    ProductDao productDao = (ProductDao) BeanFactory.getBean("productDaoImpl"); 通用工厂
        public void insertProduct(Product product) {
            productDao.insertProduct(product);
        }
    }
    
    <bean id="productService" class="com.shy.service.impl.ProductServiceImpl">
        <property name="productDao">
            <bean id="productDao" class="com.shy.dao.impl.ProductDaoImpl"/>
        property>
    bean>
    
    <bean id="productDao" class="com.shy.dao.impl.ProductDaoImpl">bean>
    <bean id="productService" class="com.shy.service.impl.ProductServiceImpl">
        <property name="productDao">
            <ref bean="productDao">ref> 
        property>
        或者
        <property name="productDao" ref="productDao"/>
    bean>
    

4) p命名空间

使用p命名空间可以简化Set注入的写法(像是一个语法糖, 不常用)

xmlns:p="http://www.springframework.org/schema/p" 
 <bean id="product" class="com.shy.entity.Product">
     <property name="name">
         <value>红色拖拉机value>
     property>
 bean>
 简化为:
 <bean id="product" class="com.shy.entity.Product" p:name="红色拖拉机">
<bean id="productService" class="com.shy.service.impl.ProductServiceImpl">
    <property name="productDao" ref="productDao"/>
bean>
简化为:
<bean id="productService" class="com.shy.service.impl.ProductServiceImpl" p:productDao-ref="productDao">

构造注入

1) 构造注入步骤

构造注入即Spring 调⽤ 有参构造方法 通过 配置⽂件 为成员变量赋值

public class Driver {
    public String name;
    public int number;

    public Driver(String name, int number) {
        this.name = name;
        this.number = number;
    }
    
    @Override
    public String toString() {
        return "Driver{" +
                "name='" + name + '\'' +
                ", number=" + number +
                '}';
    }
}
<bean id="dirver" class="com.shy.entity.Driver">
        <constructor-arg><value>Lando Norrisvalue>constructor-arg>
        <constructor-arg><value>4value>constructor-arg>
bean>
@Test
public void test4(){
    ApplicationContext applicationContext = new ClassPathXmlApplicationContext("/applicationContext.xml");
    Driver dirver = (Driver) applicationContext.getBean("dirver");
    System.out.println(dirver.toString());
}

image-20210709125600459

2) 构造注入详解

与Set注入一样, 构造注入在针对不同类型的成员变量, 编写applicationContext.xml时的语法不同, 具体参考set注入详解,

但需要首先给出相应的构造器, 而构造方法存在重载的情况.

我们可以借助的几个属性来处理重载的情况, 从而指定相应的构造器

  • index

    指定将要被注入的成员变量在构造器中的位置

    public Driver(String name, int number) {
        this.name = name;
        this.number = number;
    }
    
    <constructor-arg index="1"> 
        <value>4value>
    constructor-arg>
    <constructor-arg index="0"> 
        <value>Lando Norrisvalue>
    constructor-arg>
    
  • type

    指定将要被注入的成员变量的类型

    public Driver(String name) {
        this.name = name;
    }
    
    public Driver(int number) {
        this.number = number;
    }
    

    Driver有重载了两个构造器, 可以用type加以区分进行注入

    <bean id="dirver" class="com.shy.entity.Driver">
        <constructor-arg type="java.lang.String">
            <value>Lando Norrisvalue>
        constructor-arg>
    bean>
    或者
    <bean id="dirver" class="com.shy.entity.Driver">
        <constructor-arg type="int">
            <value>4value>
        constructor-arg>
    bean>
    
  • name

    直接指明要注入哪个成员变量

    <bean id="dirver" class="com.shy.entity.Driver">
        <constructor-arg name="number">
            <value>4value>
        constructor-arg>
        <constructor-arg name="name">
            <value>Lando Norrisvalue>
        constructor-arg>
    bean>
    

3) c命名空间

与p命名空间一样, 使用c命名空间可以简化构造注入的写法(像是一个语法糖, 不常用)

xmlns:c="http://www.springframework.org/schema/c" 
<bean id="dirver" class="com.shy.entity.Driver" c:name="Pierre Gasly" c:number="10"/>
<bean id="dirver" class="com.shy.entity.Driver" c:_0="Max Verstappen" c:_1="33"/>

对比两种注入方式

实战当中, Set注入更常用. 在Spring框架中也大量使用了Set注入

Spring学习记录_第5张图片

autowire 自动装配

参考:彻底搞明白Spring中的自动装配和Autowired - 简书 (jianshu.com)

  • 应用场景

    可以理解为自动注入,即在对Bean的属性进行依赖注入时, 有时需要引用某一个已经注册好的Bean,

    此时就可以使用自动装配, 让Spring框架自动扫描完成注入, 而不用显示的进行配置.

    <bean id="userDao" class="com.shy.dao.impl.UserDaoImpl"/>
    <bean id="userService" class="com.shy.service.impl.UserServiceImpl">
        <property name="userDao" ref="userDao"/> 
    bean>
    

    自动装配:

    <bean id="userDao" class="com.shy.dao.impl.UserDaoImpl"/>
    <bean id="userService" class="com.shy.service.impl.UserServiceImpl" autowire="byName"/>
    

    错误示范: 将autowire理解成注册Bean, 而不是对Bean的属性进行注入

  • 装配策略

    所谓装配策略,指的是Spring如何与找到bean_A的属性对应的bean_B,来完成依赖注入

    • byName

      寻找与bean_A属性名字相同bean_B

    • byType

      寻找与bean_A属性类型相同bean_B

    • constructor

      寻找与bean_A构造器参数类型相同的bean_B

    实战中, 并不推荐在xml中使用自动装配.

    不过,在Spring引入注解@autowired之后, 使用注解自动装配成为实战最受欢迎的一种方式

    详细内容见后

控制反转与依赖注入

浅谈IOC–说清楚IOC是什么_哲-CSDN博客_ioc

  1. 控制反转(IOC: Inverse of Control) — 解耦合

    获得依赖对象的过程被反转了。控制被反转之后,获得依赖对象的过程由自身管理变为了由IOC容器主动注入

    在没有引入IOC容器之前,对象A依赖于对象B,那么对象A在初始化或者运行到某一点的时候,自己必须主动去创建对象B或者使用已经创建的对象B。无论是创建还是使用对象B,控制权都在自己手上。

    在引入IOC容器之后,这种情形就完全改变了,由于IOC容器的加入,对象A与对象B之间失去了直接联系,所以,当对象A运行到需要对象B的时候,IOC容器会主动创建一个对象B注入到对象A需要的地方。

    Spring学习记录_第6张图片

    Spring学习记录_第7张图片

  2. 依赖注入(DI: Dependence Injection) — 实现IOC的方式

    依赖注入,就是由IOC容器在运行期间,动态地将某种依赖关系注入到对象之中

Spring工厂生产复杂对象

什么是复杂对象?

在以上章节中, 我们都是通过applicationContext.getBean()生产简单的Java对象POJO

那么, 如何使用Spring工厂生产复杂对象呢? 复杂对象又是什么呢?

Spring学习记录_第8张图片

  • 复杂对象

    简单说, 复杂对象就是不直接使用new来创建的对象, 如数据库连接Connection

    public class ConnectionFactory {
        public Connection getConnection(){
            Connection connection = null;
            try {
                Class.forName("com.mysql.cj.jdbc.Driver");
                connection = DriverManager.getConnection("jdbc:mysql://localhost:3306/spring?useUnicode=true&characterEncoding=utf-8&useSSL=false&serverTimezone=GMT&allowPublicKeyRetrieval=true","root","root");
            } catch (ClassNotFoundException e) {
                e.printStackTrace();
            } catch (SQLException e) {
                e.printStackTrace();
            }
            return connection;
        }
    }
    

创建复杂对象

实现FactoryBean接口

  1. 实现FactoryBean接口创建复杂对象
public class ConnectionFactoryBean implements FactoryBean<Connection> {

 private String driver;
 private String url;
 private String user;
 private String password;

 // 提供Set方法, 进行Set注入
 public void setDriver(String driver) {
     this.driver = driver;
 }

 public void setUrl(String url) {
     this.url = url;
 }

 public void setUser(String user) {
     this.user = user;
 }

 public void setPassword(String password) {
     this.password = password;
 }

 public String getDriver() {
     return driver;
 }

 public String getUrl() {
     return url;
 }

 public String getUser() {
     return user;
 }

 public String getPassword() {
     return password;
 }

 // 创建复杂对象
 public Connection getObject() throws Exception {
     Connection connection = null;
     Class.forName(driver);
     connection = DriverManager.getConnection(url,user,password);
     return connection;
 }

 // 返回创建的复杂对象的类型
 public Class<?> getObjectType() {
     return Connection.class;
 }

 // 是否为单例模式(此为default的方法, 可以不重写, 默认返回true)
 public boolean isSingleton() {
     return true;
 }	
}
<bean id="connection" class="com.shy.factory.ConnectionFactoryBean">
    <property name="driver" value="com.mysql.cj.jdbc.Driver"/>
    <property name="url" value="jdbc:mysql://localhost:3306/spring?serverTimezone=GMT%2B8&useUnicode=true&characterEncoding=UTF-8&allowPublicKeyRetrieval=true"/>
    <property name="user" value="root"/>
    <property name="password" value="niit"/>
bean>
@Test
public void test(){
    ApplicationContext applicationContext = new ClassPathXmlApplicationContext("/applicationContext.xml");
    Connection connection = (Connection) applicationContext.getBean("connection");
    System.out.println("connection = " + connection);
}

image-20210709162500784

  1. getBean()

    对于简单的Java对象来说, getBean()直接获得在中指定的类的对象

    而对于复杂的Java对象, getBean()并没有直接获取有在中指定的类的对象,

    因为对于复杂对象来说, 指定的是生产复杂对象的Factory

    <bean id="connection" class="com.shy.factory.ConnectionFactoryBean">
    

    所以getBean()返回的是FactoryBean接口中getObject()方法返回的复杂对象

    如果想要获得生产该对象的Factory,可以加上&

    Spring学习记录_第9张图片

    ConnectionFactoryBean bean = (ConnectionFactoryBean) applicationContext.getBean("&connection");
    

    image-20210709162646723

  2. boolean isSingleton()

    此方法可以指定工厂生产的Bean是否为单例模式

    // 是否为单例模式(此为default的方法, 可以不重写, 默认返回true)
        public boolean isSingleton() {
            return false;
        }
    
    Connection connection = (Connection) applicationContext.getBean("connection");
    Connection connection2 = (Connection) applicationContext.getBean("connection");
    

    image-20210709163220705

  3. FactoryBean实现原理

    Spring中FactoryBean的作用和实现原理 - 夜月归途 - 博客园 (cnblogs.com)

Spring学习记录_第10张图片

实例工厂与静态工厂

  • 为什么需要这两种方法创建复杂对象?

    • 避免Spring框架的侵入(疑问: 这两种方式还是需要配置applicationContext.xml, 这不还是Spring框架的侵入吗)

    • 整合遗留的代码

      如果只有getConnection()方法的.class文件, 没有源代码, 则无法通过实现FactoryBean接口来创建复杂对象

      只能通过实例工厂和静态工厂来创建复杂对象

  • 实例工厂

    public class ConnectionFactory {
        public Connection getConnection(){
            Connection connection = null;
            try {
                Class.forName("com.mysql.jdbc.Driver");
                connection = DriverManager.getConnection("jdbc:mysql://localhost:3306/spring?serverTimezone=GMT%2B8&useUnicode=true&characterEncoding=UTF-8&allowPublicKeyRetrieval=true","root","root");
            } catch (ClassNotFoundException e) {
                e.printStackTrace();
            } catch (SQLException e) {
                e.printStackTrace();
            }
            return connection;
        }
    }
    

    假如已有以上方法, 现需要整合到Spring框架中, 借助Spring创建对象

    
    <bean id="connectionFactory" class="com.shy.factory.ConnectionFactory"/>
    
    <bean id="connection" factory-bean="connectionFactory" factory-method="getConnection"/>
    
    @Test
    public void test(){
        ApplicationContext applicationContext = new ClassPathXmlApplicationContext("/applicationContext.xml");
        Connection connection = (Connection) applicationContext.getBean("connection");
    }
    
  • 静态工厂

    public class StaticConnectionFactory {
        public static Connection getConnection(){
            Connection connection = null;
            try {
                Class.forName("com.mysql.jdbc.Driver");
                connection = DriverManager.getConnection("jdbc:mysql://localhost:3306/spring?serverTimezone=GMT%2B8&useUnicode=true&characterEncoding=UTF-8&allowPublicKeyRetrieval=true","root","niit");
            } catch (ClassNotFoundException e) {
                e.printStackTrace();
            } catch (SQLException e) {
                e.printStackTrace();
            }
            return connection;
        }
    }
    

    假如已有以上静态方法getConnection(), 现需要整合到Spring框架中, 借助Spring创建对象

    
    
    <bean id="connection" class="com.shy.factory.StaticConnectionFactory" factory-method="getConnection"/>
    
    @Test
    public void test(){
        ApplicationContext applicationContext = new ClassPathXmlApplicationContext("/applicationContext.xml");
        Connection connection = (Connection) applicationContext.getBean("connection");
    }
    

创建复杂对象时需要的参数, 可以直接注入


<bean id="dateFormat" class="java.text.SimpleDateFormat">
 <constructor-arg value="yyyy-mm-dd"/>
bean>
   <bean id="driver" class="com.shy.entity.Driver">
 <property name="name" value="Lando Norris"/>
 <property name="number" value="4"/>
    <property name="birthday">
        <bean factory-bean="dateFormat" factory-method="parse">
            <constructor-arg value="1999-11-13"/> 
        bean>
    property>
   bean>

控制对象创建次数

  • 为什么要控制Spring工厂创建对象的次数? – 节省内存的消耗

    一般可以被共用, 线程安全 或是 重量级资源只创建一次 如 DAO Service

    而不可以被共用, 非线程安全的要创建多次 如 Connection Session

如何控制Spring工厂创建对象的次数呢 ?

  • 简单对象 – 通过 控制

    
    
    <bean id="driver" class="com.shy.entity.Driver" scope="prototype"/>
    
    <bean id="driver" class="com.shy.entity.Driver" scope="singleton"/>
    
  • 复杂对象

    对于实现了FactoryBean接口的对象, 通过重写isSingleton()方法来控制

    // 是否为单例模式(此为default的方法, 可以不重写, 默认返回true)
    public boolean isSingleton() {
        return false;
    }
    

    对于无法实现上述接口的对象(如实例工厂 静态工厂),通过 控制

    <bean id="connection" class="com.shy.factory.StaticConnectionFactory" factory-method="getConnection" scope="prototype"/>
    

Spring Bean的生命周期

在Spring框架中, 由Spring容器管理Bean的生命周期

1) 创建阶段

  • 对于singleton对象

    在Spring工厂创建时, 对象就会一起被创建

    ApplicationContext applicationContext = new ClassPathXmlApplicationContext("/applicationContext.xml");
    // -----对象被创建
    applicationContext.getBean("driver");
    

    可以指定lazy-init, 这样单例就不再工厂创建时被创建, 而是在第一次使用时被创建,

    (可以减少项目的启动时间)

    <bean id="driver" class="com.shy.entity.Driver" lazy-init="true"/>
    
    ApplicationContext applicationContext = new ClassPathXmlApplicationContext("/applicationContext.xml");
    applicationContext.getBean("driver");
    // -----对象被创建
    
  • 对于prototype对象

    当需要该对象时再创建该对象

    ApplicationContext applicationContext = new ClassPathXmlApplicationContext("/applicationContext.xml");
    applicationContext.getBean("driver");
    // -----对象被创建
    

2) 初始化阶段

Spring 工厂在创建完对象后,Spring工厂调用程序员提供的对象的初始化方法,完成对应的初始化操作(主要是对资源的初始化)

提供初始化方法

  • 实现InitializingBean接口的afterPropertiesSet()方法

    public class Driver implements InitializingBean {
        // ......
        // 自定义对象的初始化方法, 由Spring工厂调用
        @Override
        public void afterPropertiesSet() throws Exception {
            System.out.println("Driver.afterPropertiesSet");
        }
    }
    
  • 自定义初始化方法

    并不需要实现InitializingBean接口

    public class Driver{
        // ......
        // 自定义对象的初始化方法, 由Spring工厂调用
        public void myInitMethod() {
            System.out.println("Driver.myInitMethod");
        }
    }
    
    <bean id="driver" class="com.shy.entity.Driver" init-method="myInitMethod"/>
    

如果⼀个对象既实现 afterPropertiesSet() 同时⼜提供了普通的初始化方法, 则先执行前者, 再执行后者

image-20210710120214731

Spring工厂在创建对象之后, 先完成对依赖注入, 再进行初始化操作

Spring学习记录_第11张图片

3) 销毁阶段

Spring 工厂在销毁对象ctx.close()前,会调用程序员提供的对象的初始化方法,完成对应的销毁操作(主要是对资源的销毁

销毁操作只会对scope="singleton"的bean生效. 并且需要显式调用close()方法销毁对象

spring可以管理 singleton作用域的bean的生命周期,spring可以精确地知道该bean何时被创建,何时被初始化完成,容器合适准备销毁该bean实例。

spring无法管理prototype作用域的bean的生命周期,每次客户端请求prototype作用域的bean,bean实例都会完全交给客户端管理,容器不再跟踪其生命周期

@Test
public void test(){
    ClassPathXmlApplicationContext applicationContext = new ClassPathXmlApplicationContext("/applicationContext.xml");
    applicationContext.getBean("driver");
    applicationContext.close();
    
    // 需要注意的是 close()没有在ApplicationContext接口中定义. 需要将引用更换为子接口ClassPathXmlApplicationContext 才能调用该方法. (因为父类只能调用子类继承自该父类的方法[多态]) 
}

提供销毁方法

  • 实现DisposableBean接口的destroy()方法

    public class Driver implements DisposableBean {
        // ......
        // 自定义对象的销毁方法, 由Spring工厂调用
        @Override
        public void destroy() throws Exception {
            System.out.println("Driver.destroy");
        }
    }
    
  • 自定义销毁方法

    并不需要实现DisposableBean接口

    public class Driver implements DisposableBean {
        // ......
        // 自定义对象的初始化方法, 由Spring工厂调用
        public void myDestroyMethod() {
            System.out.println("Driver.myDestroyMethod");
        }
    }
    
    <bean id="driver" class="com.shy.entity.Driver" destroy-method="myDestroyMethod"/>
    

如果⼀个对象既实现 destroy() 同时⼜自定义了销毁方法, 则先执行前者, 再执行后者

image-20210710134659333

配置文件参数化

  • 为什么需要配置文件参数化?

    便于维护: 我们可以将applicationContext.xml中经常需要修改的配置信息转移到配置文件.properties

  • 演示:

    JDBC的相关配置信息作为演示

    <bean id="connection" class="com.shy.factory.ConnectionFactoryBean">
        <property name="driver" value="com.mysql.cj.jdbc.Driver"/>
        <property name="url" value="jdbc:mysql://localhost:3306/spring?useUnicode=true&characterEncoding=utf-8&useSSL=false&serverTimezone=GMT&allowPublicKeyRetrieval=true"/>
        <property name="user" value="root"/>
        <property name="password" value="root"/>
    bean>
    
    1. 创建dbconfig.properties配置文件
    jdbc.driver=com.mysql.cj.jdbc.Driver
    jdbc.url=jdbc:mysql://localhost:3306/spring?useUnicode=true&characterEncoding=utf-8&useSSL=false&serverTimezone=GMT&allowPublicKeyRetrieval=true
    jdbc.username=root
    jdbc.password=root
    
    1. applicationContext.xml中引入dbconfig.properties
    <context:property-placeholder location="classpath:dbconfig.properties"/>
    

    classpath 指的是 编译后 classes 包的路径

    Spring学习记录_第12张图片

    在引入过程中, 出现了如下错误:

    通配符的匹配很全面, 但无法找到元素 ‘context:property-placeholder’ 的声明。

    解决:

    在引入context命名空间的同时,

    xmlns:context="http://www.springframework.org/schema/context"
    

    在xsi:schemaLocation字符串中添加context相关的解析文件

    xsi:schemaLocation=" ...  http://www.springframework.org/schema/context  http://www.springframework.org/schema/context/spring-context-4.2.xsd"      
    

    3)在applicationContext.xml将相关配置参数化即可

    <bean id="connection" class="com.shy.factory.ConnectionFactoryBean">
        <property name="driver" value="${jdbc.driver}"/>
        <property name="url" value="${jdbc.url}"/>
        <property name="user" value="${jdbc.username}"/>
        <property name="password" value="${jdbc.password}"/>
    bean>
    

自定义类型转换器

  • 什么是类型转换器

    Spring学习记录_第13张图片

    考虑上图中, 在中将字符串"1"赋值给了Integer类型. 在此过程中, 就借助了Spring框架内置的类型转换器来完成了类型转换.

    当Spring框架内置的类型转换器无法满足我们的需求时(当然, 绝大多数情况下会满足的),

    <bean id="driver" class="com.shy.entity.Driver">
        <property name="name" value="Lando Norris"/>
        <property name="number" value="4"/>
        <property name="birthday" value="1999-11-13"/>
    bean>
    
    public class Driver implements InitializingBean, DisposableBean {
        public String name;
        public int number;
        public Date birthday;
        // .......
    }
    

    image-20210710164813752

  • 我们便可以自定义类型转换器

    1. 实现Converter接口, 自定义转换策略

      Spring学习记录_第14张图片

      public class DateConverter implements Converter<String, Date> {
          private String pattern;
      
          public String getPattern() {
              return pattern;
          }
      
          public void setPattern(String pattern) {
              this.pattern = pattern;
          }
      
          @Override
          public Date convert(String source) {
      
              // 既然使用Spring框架, 可以使用依赖注入设置pattern (DI)
              SimpleDateFormat simpleDateFormat = new SimpleDateFormat(pattern);
              Date date = null;
              try {
                  date = simpleDateFormat.parse(source);
              } catch (ParseException e) {
                  e.printStackTrace();
              }
              return date;
          }
      }
      
    2. 接下来将此Converter类添加至IOC容器中

      <bean id="dateConverter" class="com.shy.converter.DateConverter">
          <property name="pattern" value="yyyy-MM-dd"/> 
      bean>
      
    3. 最后我们需要将此Converter作为属性注入到Spring生产Converter的工厂中

      因为我们需要告知Spring, 我自定义了一个Converter, 并且采取了何种转换策略

      image-20210710171954344

      
      <bean id="conversionService" class="org.springframework.context.support.ConversionServiceFactoryBean">
          <property name="converters">
              <set>
                  <ref bean="dateConverter"/>
              set>
          property>
      bean>
      
      @Test
      public void test10(){
          ApplicationContext applicationContext = new ClassPathXmlApplicationContext("/applicationContext.xml");
          Driver driver = (Driver) applicationContext.getBean("driver");
          System.out.println("driver.birthday = " + driver.birthday);
      }
      

      image-20210710172039867

  • 事实上, Spring框架也为我们提供了 String -> Date 类型的转换器

    <property name="birthday" value="1999/11/13"/> 
    
    
  • 写在最后

    个人感觉这种方式比较笨重, 不如借助SimpleDateFormatparse()方法,

    先将String转为Date,再将其注入到Bean(这样就不会涉及到Spring框架的类型转换问题)

    演示:

    
    <bean id="dateFormat" class="java.text.SimpleDateFormat">
        <constructor-arg value="yyyy-mm-dd"/>
    bean>
    <bean id="driver" class="com.shy.entity.Driver">
        <property name="name" value="Lando Norris"/>
        <property name="number" value="4"/>
        <property name="birthday">
            <bean factory-bean="dateFormat" factory-method="parse">
                <constructor-arg value="1999-11-13"/>
            bean>
        property>
    bean>
    

BeanPostProcessor

BeanPostProcessor 前置 / 后置 处理bean

Spring学习记录_第15张图片

BeanPostProcessor接口, 对象后处理器(后: 指实例化对象后)

实现此接口提供的两个方法, 可以让Spring在初始化Bean前后对其进行处理

public interface BeanPostProcessor {

	// 在Bean初始化之前进行处理
	@Nullable
	default Object postProcessBeforeInitialization(Object bean, String beanName) throws BeansException {
		return bean;
	}

    // 在Bean初始化之后进行处理
	@Nullable
	default Object postProcessAfterInitialization(Object bean, String beanName) throws BeansException {
		return bean;
	}

}

演示:

首先实现BeanPostProcessor接口

public class CustomBeanPostProcessor implements BeanPostProcessor {

    // 当Bean没有进行初始化操作时, Before和After几乎没有区别, 一般只实现After即可
    @Override
    public Object postProcessBeforeInitialization(Object bean, String beanName) throws BeansException {
        return bean; // 即使不实现Before, 也要将Bean返回(继续传递)
    }

    @Override
    public Object postProcessAfterInitialization(Object bean, String beanName) throws BeansException {

        // 需要注意的是 BeanPostProcessor 会对所有Bean进行再加工, 所以要进行类型判断
        if(bean instanceof Driver){
            Driver driver = (Driver)bean;
            driver.setName("Daniel Ricciardo");
        }
        return bean; // 这里bean 和 driver 引用的是同一块内容
    }
}

然后注册该Bean

<bean id="customBeanPostProcessor" class="com.shy.beanProcessor.CustomBeanPostProcessor"/>

测试

<bean id="driver" class="com.shy.entity.Driver">
    <property name="name" value="Lando Norris"/>
    <property name="number" value="4"/>
    <property name="birthday" value="1999/11/13"/>
bean>
@Test
public void test11(){
    ApplicationContext applicationContext = new ClassPathXmlApplicationContext("/applicationContext.xml");
    Driver driver = (Driver) applicationContext.getBean("driver");
    System.out.println("driver.name = " + driver.name);
}

// driver.name = Daniel Ricciardo

代理设计模式

给女朋友讲解什么是代理模式-java3y

什么是代理设计模式

代理设计模式是Spring核心AOP(面向切面编程)所参考的一个重要的设计模式

  • 什么是代理设计模式 ?

    简单来说, 代理设计模式即当前对象不愿意干的,没法干的东西委托给别的对象来做

    通过代理类,为原始类提供额外的功能, 同时方便原始类的维护

    原始类只专注与核心业务的实现, 而代理类为原始类添加额外功能, 如日志, 事务等

    方便维护核心业务以及额外功能

静态代理模式

  • 没有使用代理模式时

    public class UserServiceImpl implements UserService {
    
        private UserDao userDao; // set注入
    
        public UserDao getUserDao() {
            return userDao;
        }
    
        public void setUserDao(UserDao userDao) {
            this.userDao = userDao;
        }
    
        @Override
        // 核心代码和额外功能耦合到一起, 不利于维护
        public boolean login(String username, String password) {
            System.out.println("额外日志功能: 记录登录相关信息.....");
            System.out.println("调用UserDao的相关方法, 完成登录的核心代码......");
            return true;
        }
    }
    
  • 使用静态代理模式

    • 原始类

    public class UserServiceImpl implements UserService {

    private UserDao userDao; // set注入
    
    public UserDao getUserDao() {
        return userDao;
    }
    
    public void setUserDao(UserDao userDao) {
        this.userDao = userDao;
    }
    
    @Override
    public boolean login(String username, String password) {
        // System.out.println("额外日志功能: 记录登录相关信息....."); 额外功能交给代理类来做
        System.out.println("调用UserDao的相关方法, 完成登录的核心代码......");
        return true;
    }
    

    }

    • 代理类

      // 代理类必须实现原始类已实现的接口
      public class UserServiceProxy implements UserService {
      
          private UserService userService; // 代理类必须持有原始类的对象(set注入拿到userServiceImpl);
      
          public UserService getUserService() {
              return userService;
          }
      
          public void setUserService(UserService userService) {
              this.userService = userService;
          }
      
          @Override
          public boolean login(String username, String password) {
              System.out.println("额外日志功能: 记录登录相关信息....."); // 代理类实现额外的日志功能
              return userService.login(username,password); // 原始类的核心代码
          }
      }
      
    • 调用

      @Test
      public void test13(){
          ApplicationContext applicationContext = new ClassPathXmlApplicationContext("/applicationContext.xml");
          UserServiceProxy userServiceProxy = (UserServiceProxy) applicationContext.getBean("userServiceProxy");
          userServiceProxy.login("username","password"); // 通过代理类调用相关方法
      }
      
      /*额外日志功能: 记录登录相关信息.....
      调用UserDao的相关方法, 完成登录的核心代码......*/
      

动态代理模式

  • 静态代理模式有何缺点?

    试想一下, 假设有数以百计的类, 而每一个类都需要一个代理类, 这就造成项目文件的数量过于庞大, 难以维护.

    另外, 假设这些代理类都实现了相同的日志功能, 而一天需求改变了, 需要修改日志功能, 则需要对这些代理类同时做出修改. 难以维护.

  • 何为动态代理?

    与静态代理不同, 动态代理的代理类在程序运行时创建.

JDK动态代理

  • 利用java.lang.reflect包下的Proxy类实现动态代理

    JDK动态代理的三要素: 1.原始类 2.额外功能 3.代理类实现与原始类相同的接口

    @Test
    public void test14(){
        ApplicationContext applicationContext = new ClassPathXmlApplicationContext("/applicationContext.xml");
        UserService userService = (UserService) applicationContext.getBean("userService");
        // 利用 java.lang.reflect的Proxy类 实现动态代理
        // getClassLoder() 用来借用一个类加载器(任意一个), 以便创建代理类
        // 为什么借用? 
        // 因为代理类是用动态字节码技术创建,没有相应的.class文件. 所以JVM无法为其分配一个ClassLoader, 所以借用
        
        // getInterfaces() 提供原始类的接口, 从而让代理类实现与原始类相同的接口
        // InvocationHandler() 用于实现额外功能
        UserService userServiceProxy = (UserService) Proxy.newProxyInstance(userService.getClass().getClassLoader(), userService.getClass().getInterfaces(), new InvocationHandler() {
            @Override
            public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
                // 为login方法增加日志功能
                if(method.getName().equals("login")){
                    System.out.println("额外日志功能: 记录登录相关信息.....");
                    return (Boolean)method.invoke(userService,args); // 调用原始类的核心功能
                }else{
                    return (Boolean)method.invoke(userService,args);
                }
            }
        });
    
        // 通过代理类调用方法
        boolean result = userServiceProxy.login("username","password");
    }
    
    • Proxy.newProxyInstance()

      Spring学习记录_第16张图片

    • InvocationHandler.invoke()

      与静态代理一样, 假设有数以百计的类需要代理, 且都实现了相同的日志功能, 则可以实现一个InvocationHandler接口的invoke()方法,将其作为参数传入Proxy.newProxyInstance()中.

      这样就必须要数以百计的代理类了, 极大地方便了代码维护

      Spring学习记录_第17张图片

  • JDK动态代理的原理分析

    孙哥说Spring5 P94-P96

Cglib动态代理

前文JDK动态代理中, 代理类需要与原始类实现相同的接口. 这样可以保证代理类与原始类方法一致,从而进行额外功能的增强

假如原始类没有接口, 该如何进行代理呢?

可以使用继承, 从而保证方法一致, 这也是cglib动态代理的特点

  • 利用org.springframework.cglib.proxy包下的Enhancer实现cglib动态代理

    // 原始类
    public class DriveService {
        // 进站
        public void box(){
            System.out.println("Box this lap");
        }
        // 打开drs
        public void openDrs(){
            System.out.println("打开drs");
        }
    }
    
    /**
         * Cglib动态代理
         */
    @Test
    public void test18(){
        ApplicationContext applicationContext = new ClassPathXmlApplicationContext("/applicationContext.xml");
        final DriveService driveService = (DriveService) applicationContext.getBean("driveService");
        Enhancer enhancer = new Enhancer(); // 利用 org.springframework.cglib.proxy包下的Enhancer实现cglib动态代理 
        enhancer.setClassLoader(driveService.getClass().getClassLoader());//设置类加载器(用于创建代理类,借用任意一个)
        enhancer.setSuperclass(driveService.getClass()); // 设置父类(cglib使用继承实现动态代理)
        enhancer.setCallback(new MethodInterceptor() {
            @Override
            // 原始类对象, 调用的原始类方法, 参数, 原始类的代理
            public Object intercept(Object o, Method method, Object[] args, MethodProxy methodProxy) throws Throwable {
                if(method.getName().equals("box")){
                    Object result = method.invoke(driveService,args);
                    System.out.println("进站耗时13s, 我法乙烷"); // 实现额外功能
                    return result;
                }else{
                    return method.invoke(driveService,args);
                }
            }
        }); // 利用MethodInterceptor设置CallBack(实现额外功能)
        DriveService driveServiceProxy = (DriveService) enhancer.create(); // 创建代理类
        driveServiceProxy.box();
        driveService.openDrs();
    }
    /*
    Box this lap
    进站耗时13s, 我法乙烷
    打开drs
    */
    

Spring 动态代理

Spring AOP 参照以上两种动态代理模式, 封装了Spring框架下的动态代理, 借助此功能, 更容易实现动态代理.

AOP(Aspect Oriented Programing): 面向切面编程, 以切面(切点 +额外功能)为基本单位的程序开发,通过切面间的彼此协同,相互调用,完成程序的构建. 是对OOP(面向对象编程)的补充

动态: Spring框架在运行时, 通过动态字节码技术在JVM中创建代理类.

实现

  • 首先需要导入相关依赖

    
    <dependency>
        <groupId>org.springframeworkgroupId>
        <artifactId>spring-aopartifactId>
        <version>5.3.9version>
    dependency>
    
    
    <dependency>
        <groupId>org.aspectjgroupId>
        <artifactId>aspectjrtartifactId>
        <version>1.9.7version>
        <scope>runtimescope>
    dependency>
    
    
    <dependency>
        <groupId>org.aspectjgroupId>
        <artifactId>aspectjweaverartifactId>
        <version>1.9.7version>
        <scope>runtimescope>
    dependency>
    
    <dependency>
        <groupId>org.springframeworkgroupId>
        <artifactId>spring-coreartifactId>
        <version>5.3.9version>
    dependency>
    
  • 然后定义原始类

    public interface UserService {
        boolean login(String username, String password);
    }
    
    public class UserServiceImpl implements UserService {
    
        private UserDao userDao; // set注入
    
        public UserDao getUserDao() {
            return userDao;
        }
    
        public void setUserDao(UserDao userDao) {
            this.userDao = userDao;
        }
    
        @Override
        public boolean login(String username, String password) {
            System.out.println("调用UserDao的相关方法, 完成登录的核心代码......");
            return true;
        }
    }
    
    
  • 然后实现MethodBeforeAdvice接口, 编写额外功能

    public class Log implements MethodBeforeAdvice {
        // before方法: 定义额外方法, 在执行原始方法之前执行
        @Override
        public void before(Method method, Object[] objects, Object o) throws Throwable {
            System.out.println("method = " + method);
            for (Object object : objects) {
                System.out.println(object);
            }
            System.out.println("o = " + o);
        }
    }
    
    
  • 然后定义切入点. 并组装切面

    
    <bean id="log" class="com.shy.dynamic.Log"/>
    <aop:config>
        
        <aop:pointcut id="pc" expression="execution(* * (..))"/>
        
        <aop:advisor advice-ref="log" pointcut-ref="pc"/>
    aop:config>
    
  • 测试

    <bean id="userDao" class="com.shy.dao.impl.UserDaoImpl"/> 
    <bean id="userService" class="com.shy.service.impl.UserServiceImpl">
        
        <property name="userDao" ref="userDao"/>
    bean>
    
    @Test
    public void test15(){
        ApplicationContext applicationContext = new ClassPathXmlApplicationContext("/applicationContext.xml");
        UserService userService = (UserService) applicationContext.getBean("userService");
        userService.login("username","password");
    }
    /*
    method = public abstract boolean com.shy.service.UserService.login(java.lang.String,java.lang.String)
    username
    password
    o = com.shy.service.impl.UserServiceImpl@4bff7da0
    调用UserDao的相关方法, 完成登录的核心代码......
    */
    

MethodBeforeAdvice接口

public interface MethodBeforeAdvice extends BeforeAdvice {
    /*
    	在原始方法执行之前执行
    	method:将额外功能添加给了哪个原始方法
    	args: 原始方法的参数
    	target:原始方法的类
	 */
    void before(Method method, Object[] args, @Nullable Object target) throws Throwable;
}

MethodInterceptor接口

MethodInterceptor, 即方法拦截器. 该接口要比MethodBeforeAdvice灵活的多

根据需要, 可以将额外方法运行在原始方法调用之前(后), 或者在原始方法抛出异常时

此接口与JDK动态代理中的invokeHandler接口类似

  • 原始方法调用之前/后

    public class Log2 implements MethodInterceptor {
        @Override
        // 类似于invokeHandler接口的invoke方法. 只不过invokcation封装了原始方法,原始方法的参数等
        public Object invoke(MethodInvocation invocation) throws Throwable {
            System.out.println("插入到调用原始方法之前");
            Object object = invocation.proceed(); // 调用原始方法
            System.out.println("插入到调用原始方法之后");
            return object; // 原始方法的返回值
        }
    }
    
  • 原始方法抛出异常时

    public class Log2 implements MethodInterceptor {
        @Override
        public Object invoke(MethodInvocation invocation) throws Throwable {
            Object object = null;
            try {
                 object = invocation.proceed();
            }catch (Exception exception){
                System.out.println("在原始方法抛出异常时, 执行额外方法");
            }
            return object;
        }
    }
    

另外, 通过改变MethodInterceptor.invoke()的返回值, 可以直接影响原方法的返回值

/* @return the result of the call to {@link Joinpoint#proceed()};
* might be intercepted by the interceptor
*/

切入点

切入点决定了额外功能加入的位置

切入点表达式

Spring学习记录_第18张图片

一般省略 modify-pattern 和 throw-pattern

  • 以方法作为切入点

    指定一些方法作为切入点,为其添加额外功能

    • 指定方法名

      execution(* deleteProduct (..)) // 指定所有deleteProduct方法作为切入点
      
    • 指定方法的参数

      execution(* * (com.shy.entity.Product,String)) // ,分隔 非java.lang下的要写全限定名
      execution(* * (*, ..)) // * 表示任意类型的参数 ..表示任意个(包括0个)任意类型的参数    
      
    • 指定方法的返回值

      execution(String * (..)) // 指定所有返回值为String的方法作为切入点
      
  • 以类作为切入点

    指定一些类作为切入点, 为其中的所有方法添加额外功能

    execution(* com.shy.service.impl.ProductServiceImpl.*(..))
    // 指定com.shy.service.ProductServiceImpl类作为切入点
    
  • 以包作为切入点

    execution(* com.shy.service.*.* (..)) // 指定com.shy.service作为切入点, 不包括service子包下的方法
    execution(* com.shy.service..*.* (..)) // 指定com.shy.service作为切入点, 包括service子包下的方法
    

切入点函数

  • execution()

    可以将 方法 / 类 / 包 作为切入点, 比较全面

  • args()

    args()用于匹配方法参数的匹配

    // 匹配参数为两个String的方法
    args(String,String) 或者 execution(* *(String, String))
    
  • within()

    within()用于类/包切入点的匹配

    // 匹配 ProductServiceImpl类
    within(com.shy.service.impl.ProductServiceImpl)execution(* com.shy.service.impl.ProductServiceImpl.*(..))
    // 匹配 com.shy.service包
    within(com.shy.service..*)execution(* com.shy.service..*.* (..))
    
  • annotation()

    annotation()用于匹配具有指定注解的方法

    // 匹配具有 @Override 注解的方法
    @annotation(java.lang.Override)
    

另外切入点函数支持逻辑运算 andor

Spring 动态代理的底层原理

在前面实现Spring动态代理时, 曾对applicationContext.xml进行过配置

<bean id="userService" class="com.shy.service.impl.UserServiceImpl">
    
    <property name="userDao" ref="userDao"/>
bean>

为什么注册的是UserServiceImpl, 最终拿到的确实它的代理类? 原因在于BeanPostProcessor的处理

Spring学习记录_第19张图片

  • 利用BeanPostProcessor模拟Spring AOP

    实现BeanPostProcessor接口

    public class MyBeanPostProcessor implements BeanPostProcessor {
        @Override
        public Object postProcessBeforeInitialization(Object bean, String beanName) throws BeansException {
            return bean;
        }
    
        @Override
        public Object postProcessAfterInitialization(Object bean, String beanName) throws BeansException {
            final Object finalBean = bean;
            InvocationHandler handler = new InvocationHandler() {
                @Override
                public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
                    System.out.println("额外功能----");
                    return method.invoke(finalBean,args);
                }
            };
            // 通过JDK动态代理(也可以使用cglib动态代理)拿到Bean的代理类
            bean = Proxy.newProxyInstance(bean.getClass().getClassLoader(), bean.getClass().getInterfaces(), handler);
            return bean;
        }
    }
    

    注册MyBeanPostProcessor

    <bean id="myBeanPostProcessor" class="com.shy.beanProcessor.MyBeanPostProcessor"/>
    

    这样便可以拿到代理类

    @Test
    public void test19(){
        ApplicationContext applicationContext = (ApplicationContext) new ClassPathXmlApplicationContext("/applicationContext.xml");
        UserService userService = (UserService) applicationContext.getBean("userService"); // 拿到userServiceImpl的代理类
    }
    

切面类实现Spring动态代理

@Aspect 切面类

编写切面类, 在切面类中定义切点以及额外功能

利用注解声明切面类

@Aspect // @Aspect声明该类是一个切面(在其中 定义切点 + 定义额外功能)
public class MyAspect {

    @Around("execution(* login(..))") // 在该@Around注解中定义切入点
    // 在around方法中实现额外功能 返回值和参数固定, 方法名可以自定义
    // 类似于非注解实现Spring动态代理时的  public Object invoke(MethodInvocation invocation); 方法
    public Object around(ProceedingJoinPoint joinPoint) throws Throwable {
        System.out.println("利用注解, 添加额外功能...");
        Object result = joinPoint.proceed(); // 执行原方法
        return result;
    }
}

applicationContext.xml中注册该切面类, 并告知Spring框架利用了注解形式实现动态代理


<bean id="around" class="com.shy.aspect.MyAspect"/>

<aop:aspectj-autoproxy/>

测试

@Test
public void test(){
    ApplicationContext applicationContext = (ApplicationContext) new ClassPathXmlApplicationContext("/applicationContext.xml");
    UserService userService = (UserService) applicationContext.getBean("userService");
    userService.login("username","password");
    userService.register("username","password");
}
/*
利用注解, 添加额外功能...
UserServiceImpl.login
UserServiceImpl.register
*/

@Pointcut 切入点复用

@Pointcut可以定义一个切点, 在@Aspect中可以直接使用, 方便维护

@Aspect
public class MyAspect {

    @Pointcut("execution(* login(..))") // 定义一个切点
    public void pointcut(){} // pointcut() 代表了这个切点

    @Around(value = "pointcut()") 
    public Object around(ProceedingJoinPoint joinPoint) throws Throwable {
        System.out.println("利用注解, 添加额外功能...");
        Object result = joinPoint.proceed(); 
        return result;
    }

    @Around("pointcut()")
    public Object around2(ProceedingJoinPoint joinPoint) throws Throwable {
        System.out.println("又一个额外功能...");
        Object result = (Object) joinPoint.proceed();
        return result;
    }
}

JDK与Cglib动态代理的切换

  • 对于实现了接口的实现类

    Spring默认使用JDK动态代理, 可以利用proxy-target-class="true切换为Cglib动态代理

    <aop:aspectj-autoproxy proxy-target-class="true"/> 
    
    <aop:config proxy-target-class="true"> 
       ......
    aop:config>
    
  • 对于没有实现接口的类

    Spring只能使用Cglib动态代理, 因为JDK动态代理只能对实现了接口的类生成代理

    此时, proxy-target-class的值是无效的, 只能使用Cglib动态代理

ApplicationContextAware接口

public class UserServiceImpl implements UserService, ApplicationContextAware {
	.......
    private ApplicationContext applicationContext;
    @Override
    public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
        this.applicationContext = applicationContext;
    }
    .......
}

实现ApplicationContextAware接口, 可以为当前Bean传入Spring上下文中已经存在的ApplicationContext对象.

而不用重新new一个, 从而节省资源(ApplicationContext对象十分消耗资源)

如我想在UserServiceImplregister方法中调用该类的代理类的login方法

public class UserServiceImpl implements UserService, ApplicationContextAware {
    private UserDao userDao; // set注入

    public UserDao getUserDao() {
        return userDao;
    }

    public void setUserDao(UserDao userDao) {
        this.userDao = userDao;
    }

    private ApplicationContext applicationContext; // 利用setApplicationContext()传入对象
    
    @Override
    public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
        this.applicationContext = applicationContext;
    }
    @Override
    public boolean login(String username, String password) {
        System.out.println("UserServiceImpl.login");
        return true;
    }

    @Override
    public boolean register(String username, String password) {
        System.out.println("UserServiceImpl.register");
        System.out.println("在register方法内部调用login方法");
        UserService userService = (UserService) applicationContext.getBean("userService"); // 拿到代理类
        userService.login(username,password); // 调用代理类的login方法
        
        // this.login(); 此调用的并不是代理类的login()方法,而是本类的login()
        return true;
    }


}

对AOP的一些术语的理解

参考: Spring AOP(一) AOP基本概念 - SegmentFault 思否

  • AOP

    AOP主要由以Spring AOP为代表的动态代理 和 以AspectJ为代表的静态代理

  • 切面 Aspect

    切面由切入点和通知(横切逻辑, 即额外功能的逻辑)组成, 可以包括同一类型的不同增强方法

    Spring AOP 负责将切面定义的横切逻辑织入切入点中

    @Aspect 
    public class LogAspect {
        // 切入点
        @Pointcut("execution(* login(..))") 
        public void pointcut(){}
    
        @Around(value = "pointcut()")
        public Object logMethod1(ProceedingJoinPoint joinPoint) throws Throwable {
            System.out.println("日志方法1"); // 通知
            Object result = joinPoint.proceed(); 
            return result;
        }
    
        @Around(value = "pointcut()")
        public Object logMethod2(ProceedingJoinPoint joinPoint) throws Throwable {
            System.out.println("日志方法2"); // 通知
            Object result = joinPoint.proceed(); 
            return result;
        }
    }
    
    • 切入点 Pointcut
    • 通知Advice
    • 织入Weaving

Spring注解编程

Spring从2.x版本开始引入注解编程, 达到简化xml配置的效果. 在后序版本逐渐完善. 随着Spring Boot的推出, 开始推广注解编程

配置

在Spring框架中采用注解形式编程, 首先需要引入注解扫描

<context:component-scan base-package="com.shy"/>

注解与xml混用

需要注意的是, Spring框架中, 注解注入会在xml注入之前执行, 因而会被xml注入的结果覆盖

Annotation injection is performed before XML injection, thus the latter configuration will override the former for properties wired through both approaches.

@Component

@Component用来注册相应的Bean, 可以用来替代

Spring学习记录_第20张图片

使用@Component注解, beanclass由Spring框架通过反射自动获得,id默认为类名首字母小写, 也可以自定义id

@Component("u")
public class User {
}

另外Spring为了更加语义化, 提供了几个与@Component用法一模一样的衍生注解

@Repository
public class UserDaoImpl{}
@Service
public class UserServiceImpl{}
@Controller
public class UserController{}

@Scope

@Scope@Component一起连用时,可以控制Spring创建对象的次数

  • 以往的xml形式
  • 注解
@Component
@Scope("singleton") // 可以不写该注解, 默认值为单例对象, 随着ApplicationContext对象一起被创建
public class User {}

@Scope("prototype") // 多例对象

@Lazy

@Lazy可以延迟创建单例对象, 即当需要该对象时再创建

  • 以往的xml形式
 <bean id="user" class="com.shy.entity.User" lazy-init="true | false"/>
  • 注解
@Lazy
@Component
public class User {}

@PostConstruct / @PreDestroy

@PostConstruct用于声明在Bean的初始化阶段, 用于初始化操作的方法

@PreDestroy用于声明在Bean的销毁阶段, 用于销毁操作的方法

  • 以往的xml形式

    <bean id="user" class="com.shy.entity.User" init-method="myInit"  destroy-method="myDestroy"/>
    
  • 注解

    @Component
    public class User {
        .......
    
        @PostConstruct
        public void myInit(){
            System.out.println("User.myInit");
        }
    
        @PreDestroy
        public void myDestroy(){
            System.out.println("User.myDestroy");
        }
    }
    

这两个注解并不是 Spring 提供的,而是兼容了JSR(JavaEE规范)250

自定义类型变量的注入

对非JDK类型变量的注入,主要包括@Autowired (更常用) 和 @Resource两种

对于接口, @Autowired会注入其实现类

@Autowired

@Autowired注解可以**基于by type**完成[自动装配](#autowire 自动装配), 可以写在以下三个地方

  • 成员变量前(更常用)

    @Service
    public class UserServiceImpl implements UserService{
    	@Autowired // 利用反射进行注入
        private UserDao userDao; 
    }
    
  • set方法前

    @Service
    public class UserServiceImpl implements UserService{
        private UserDao userDao; 
        
        @Autowired // 调用set方法进行注入
        public void setUserDao(UserDao userDao) {
            this.userDao = userDao;
        }
    }
    
  • 构造函数前

    @Service
    public class UserServiceImpl implements UserService{
        private UserDao userDao; 
        
        @Autowired // 调用该构造函数进行注入
        public UserServiceImpl(UserDao userDao) {
            this.userDao = userDao;
        }
    }
    // Autowiring by type from bean name 'userServiceImpl' via constructor to bean named 'userDaoImpl'
    

对于自动装配, 我们发现虽然都是对接口进行注入private UserDao userDao, 但经过测试会发现Spring为其注入的是userDaoImpl

Creating shared instance of singleton bean 'userDaoImpl'

这是因为接口是无法实例化的, 所以都是其实现类. 这也符合applicationContext.xml中注册时的习惯

<bean id="userDao" class="com.shy.dao.impl.userDaoImpl"/>

此外, 注入对象的类型还可以是其子类.

@Component
public class UserServiceImpl implements UserService {
    @Autowired
    private UserDaoImpl userDaoImpl;
}
@Component
public class SonClass extends UserDaoImpl{
}
userService.getUserDaoImpl() = com.shy.service.SonClass@4f2b503c
  • 两个异常

    在上述案例中, 进行注入时, 一定要保证userDao已经被Spring创建好了, 否则会出NoSuchBeanDefinitionException异常

    不过,可以通过设置@Autowired(required = false)来避免.

    此外, 使用@Autowired进行自动转配时, 可能出现找到多个候选的情况, 此时会出现NoUniqueBeanDefinitionException异常

    @Repository
    public class UserDaoImpl implements UserDao {
    }
    
    @Repository
    public class UserDaoImpl2 implements UserDao {
    }
    // expected single matching bean but found 2: userDaoImpl,userDaoImpl2
    

@Qualifier

@Qualifier注解用来配合@Autowired使用, 使@Autowired注解基于by name(beanid值)自动装配

public class UserServiceImpl implements UserService, ApplicationContextAware {
    @Autowired // 必须有, @Qualifier只是限定一下名称
    @Qualifier("userDaoImpl2")
    private UserDao userDao; 
}

@Resource

@Resource不是 Spring 提供的,而是兼容了JSR(JavaEE规范)250

@Resource支持by name(默认) 和 by type两种方式进行自动装配

Spring学习记录_第21张图片

Java类型变量的注入

@Value

@Value注解配合.properties文件可以完成对Java基本数据类型变量的注入

演示:

  • 编写driver.properties文件

    name=Lando Norris
    number=4
    birthday=2021/08/08
    
  • 编写实体类

    @PropertySource("/driver.properties") // 引入 driver.properties
    
    @Component
    public class Driver {
        @Value("${name}")
        public String name;
        @Value("${number}")
        public int number;
        @Value("${birthday}")
        public Date birthday;
    
    }
    
    

    @PropertySource注解用来引入.properties文件

    底层实现是PropertySourcePalaceholderConfigurer,它根据当前 Spring Environment及其PropertySources集解析 bean 定义属性值和@Value注释中的 ${…} 占位符。

  • 测试

    @Test
    public void test(){
        ApplicationContext applicationContext = new ClassPathXmlApplicationContext("applicationContext2.xml");
        Driver driver = (Driver) applicationContext.getBean("driver");
        System.out.println("driver.name = " + driver.name);
        System.out.println("driver.number = " + driver.number);
        System.out.println("driver.birthday = " + driver.birthday);
    }
    
    // driver.name = Lando Norris
    // driver.number = 4
    // driver.birthday = Sun Aug 08 00:00:00 CST 2021
    

    此方式并不支持集合类型, 个人猜测应该是.properties文件不支持书写相关集合类型

    如果想要注入集合类型, 需要使用SpELl表达式

    myList=1,2,3,4,5
    
    @Value("#{'${myList}'.split(',')}")
    public List<Integer> myList;
    

    注入Map类型

    myMap={'name':'Lando Norris', 'number':'4'}
    
    @Value("#{${myMap}}")
    public  Map<String, String> myMap;
    

注解扫描详解

官方对component-scan的解释:

Scans the classpath for annotated components that will be auto-registered as
Spring beans. By default, the Spring-provided @Component, @Repository, @Service,
@Controller, @RestController, @ControllerAdvice, and @Configuration stereotypes
will be detected.

<context:component-scan base-package="com.shy"/> // 对com.shy及其子包进行注解扫描

在进行注解扫描时, 我们曾配置过注解扫描. Spring注解扫描主要有两种策略: 排除方式 和 包含方式.

  • 排除方式

    不对特定的包/类进行注解扫描

    <context:component-scan base-package="com.shy">
        <context:exclude-filter type="" expression=""/>
    context:component-scan>
    

    type属性可以是:

    • annotation 不对带有特定的注解的类进行扫描

      <context:exclude-filter type="annotation" expression="org.springframework.stereotype.Component"/>
      
    • assignable 不对特定的类进行扫描

      <context:exclude-filter type="assignable" expression="com.shy.entity.Driver"/>
      
    • aspectj根据切入点表达式排除特定的类

      <context:exclude-filter type="aspectj" expression="* *(..)"/>
      

    此外,还有两种不常用的属性:

    • regex根据正则表达式排除特定的类
    • custom自定义规则(底层框架开发中大量使用)
  • 包含方式

    只对特定的包/类进行注解扫描

    <context:component-scan base-package="com.shy" use-default-filters="false">
        <context:include-filter type="" expression=""/>
    context:component-scan>
    

    type的值与上同, 不过需要设置use-default-filters="false",既不采用默认的材料进行注解扫描

以上所涉及的注解均为Spring2.x时引入的注解, 目的是简化xml的配置

------分割线-------

以下所涉及的注解均为Spring3.x后引入的注解, 目的是取代xml开发方式

@Configuration

@Configuration注解标注的类可以成为配置Bean(更专业的术语是 JavaConfig)效果上等同于applicationContext.xml

从而来代替applicationContext.xml配置文件

@Configuration
public class AppConfiguration {
}

创建Spring工厂

ApplicationContext applicationContext = new AnnotationConfigApplicationConext(AppConfiguration.class);
// Spring自动扫描 com.shy.config及其子包下所有带有@Configuration注解的Bean, 并进行注册
ApplicationContext applicationContext = new AnnotationConfigApplicationContext("com.shy.config");

@Bean

注册Bean

@Bean可以在@Configuration标注的类中使用, 相当于标签,用于bean的注册

Q: 有@Component注解来注册Bean,还需要@Bean注解 ?

A: 假如在注册第三方提供的类时, 无法修改为其源码加上@Component注解, 这时就需要@Bean注解

Spring学习记录_第22张图片

此外@Bean可以自定义id, 并且控制对象的创建次数

@Configuration
public class AppConfiguration {

    @Bean("u")
    @Scope("singleton")
    // 方法名即id值
    public UserDao userDao(){
        // 创建对象的相关代码
        return new UserDaoImpl();
    }
}

底层原理:

使用@Bean注解, 实际上是把对象交给IOC容器进行管理.

我们想要完成的核心功能是 new 一个对象, 而Spring为其添加了额外功能, 从而管理Bean的生命周期.

本质上来说, 这也是AOP的一种体现, 通过Cglib动态代理实现

image-20210812151330473

对Bean的属性进行注入

  • 自定义类型

    @Configuration
    public class AppConfiguration {
    
        @Bean
        public UserDao userDao(){
            return new UserDaoImpl();
        }
        @Bean
        // 入参UserDao会由Spring自动寻找(前提是UserDao也是被Spring管理的bean)
        public UserService userService(UserDao userDao){
            UserServiceImpl userServiceImpl = new UserServiceImpl();
            userServiceImpl.setUserDao(userDao); // 注入userDao
            return userServiceImpl();
        }
    }
    

    Q: 这种方式既new对象又set属性, 怎么体现解耦合的?

    A: 其实我也有点疑惑. 按照目前的理解, 这种方式依然是由Spring进行管理Bean的生命周期, 所以解耦合

    @Configuration
    public class AppConfiguration {
    
        @Bean
        public UserDao userDao(){
            System.out.println("AppConfiguration.userDao");
            return new UserDaoImpl();
        }
        @Bean
        public UserService userService(){
            UserServiceImpl userServiceImpl = new UserServiceImpl();
            userServiceImpl.setUserDao(userDao()); 
            return userServiceImpl;
        }
    }
    

    也可以不使用入参UserDao, 而是调用useDao()

    配置类内部可以通过方法调用来处理依赖,并且能够保证是同一个实例,都指向IoC内的那个单例

    Spring的@Configuration配置类-Full和Lite模式_demon7552003的小本本-CSDN博客

  • Java基本数据类型

    @Configuration
    @PropertySource("/driver.properties")
    public class AppConfiguration2 {
        @Value("${name}")
        private String name;
        @Value("${number}")
        private int number;
        @Bean
        public Driver driver(){
            Driver driver = new Driver();
            driver.setName(name);
            driver.setNumber(number);
            return driver;
        }
    }
    

@ComponentScan

@ComponentScan用来替代标签, 进行注解扫描

默认扫描范围是该类所在的包

具体属性的用法参照注解扫描详解

  • 排除方式

    @Configuration
    @ComponentScan(basePackages = "com.shy",
            excludeFilters = {
                    @ComponentScan.Filter(type = FilterType.ANNOTATION, value = {Repository.class}),
                    @ComponentScan.Filter(type = FilterType.ASSIGNABLE_TYPE, value = {Driver.class}),
                    @ComponentScan.Filter(type = FilterType.ASPECTJ, pattern = "*..User")
            })
    public class AppConfiguration {}
    
  • 包含方式

    @Configuration
    @ComponentScan(basePackages = "com.shy",
            includeFilters = {
                    @ComponentScan.Filter(type = FilterType.ANNOTATION, value = {Repository.class}),
                    @ComponentScan.Filter(type = FilterType.ASSIGNABLE_TYPE, value = {Driver.class}),
                    @ComponentScan.Filter(type = FilterType.ASPECTJ, pattern = "*..User")
            })
    public class AppConfiguration {}
    

选择哪种方式注册Bean ?

经过前面的学习, 无论是@Component, 还是@Bean, 又或是 都可以用来注册Bean.

那么选择哪种方式更好呢 ?

Spring学习记录_第23张图片

需要注意的是, 这几种方式是有优先级的:@Component < @Bean <

优先级高的方式可以覆盖优先级低的配置.

整合Bean的配置信息

在项目当中, 注册Bean可能不只使用@Component这种方式. 有可能也使用了@Bean来注册Bean.

倘若我们对项目进行维护, 并把@Component作为注册Bean的主要方式.

我们该如何将带有@Component的Bean 和 在applicationContext.xml中配置的Bean 整合到@Configuration标注的配置类中呢 ?

  • 多个配置Bean的整合

    • 方式一

      ​ 将配置Bean放在同一包下

      Spring学习记录_第24张图片

    • 方式二

      选择一个配置Bean作为主配置Bean, 利用@Import将其他配置Bean导入

      Spring学习记录_第25张图片

    • 方式三

      ApplicationContext applicationContext = new AnnotationConfigApplicationContext(AppConfig1.Class, AppConfig.Class);
      
  • @Component标注的Bean整合到配置Bean

    利用@ComponentScan注解扫描注解

    @Configuration
    @ComponentScan("com.shy.dao")
    public class AppConfig4 {
    
        @Autowired
        private UserDao userDao;
    
        @Bean
        public UserServiceImpl userService(){
            UserServiceImpl userService = new UserServiceImpl();
            userService.setUserDao(userDao);
            return userService;
        }
    }
    
    @Repository
    public class UserDaoImpl implements UserDao {
    }
    
  • applicationContext.xml中配置的Bean整合到配置Bean

    利用@ImportResource注解导入配置文件

    @Configuration
    @ImportResource("/applicationContext2.xml")
    public class AppConfig4 {
    
        @Autowired
        private UserDao userDao;
    
        @Bean
        public UserServiceImpl userService(){
            UserServiceImpl userService = new UserServiceImpl();
            userService.setUserDao(userDao);
            return userService;
        }
    }
    
    public class UserDaoImpl implements UserDao {
    }
    
    <bean id="userDao" class="com.shy.dao.impl.UserDaoImpl"/>
    

以上三种场景下进行配置Bean的整合时, 假如需要进行注入时, 可以使用@Autowired自动注入

纯注解实现Spring动态代理

  • 原始类

    public class ProductServiceImpl implements ProductService {
    
        private  ProductDao productDao;
    
        @Override
        public void insertProduct(Product product) {
            System.out.println("ProductServiceImpl.insertProduct");
        }
        @Override
        public void deleteProduct(Product product){
            System.out.println("ProductServiceImpl.deleteProduct");
        }
    }
    
  • 切面类

    @Aspect
    @Component
    public class MyAspect {
        @Pointcut("execution(* com.shy.service.impl.ProductServiceImpl.* (..))")
        public void pointCut(){};
    
        @Around(value = "pointCut()")
        public Object around2(ProceedingJoinPoint proceedingJoinPoint) throws Throwable {
            System.out.println("纯注解实现Spring动态代理");
            Object result = proceedingJoinPoint.proceed();
            return result;
            }
    }
    
  • 配置Bean

    @EnableAspectJAutoProxy 相当于 , 即使用注解形式自动配置AOP动态代理

    @Configuration
    @ComponentScan("com.shy")
    @EnableAspectJAutoProxy
    public class AppConfig {
    }
    
  • 测试类

    @Test
    public void test38(){
        ApplicationContext applicationContext = new AnnotationConfigApplicationContext(AppConfig.class);
        ProductServiceImpl productServiceImpl = (ProductServiceImpl) applicationContext.getBean("productServiceImpl");
        productServiceImpl.insertProduct(new Product());
        productServiceImpl.deleteProduct(new Product());
    }
    
    /**
    纯注解实现Spring动态代理
    ProductServiceImpl.insertProduct
    纯注解实现Spring动态代理
    ProductServiceImpl.deleteProduct
    **
    

Spring整合Mybatis

参考: mybatis-spring官方文档
SSM整合官方演示项目

为什么要整合

我们首先考虑一下在只使用Mybatis时的开发步骤:

1.创建实体类
2.创建并配置mybatis-config.xml(如 environment setting 等)
3.在mybatis-config.xml中配置实体别名 alias
4.创建表
5.创建XXXMapper.java接口
6.创建并实现XXXMapper.xml配置文件
7.在mybatis-config.xml中注册XXXMapper.xml
8.编写MybatisUtil获取SqlSessionFactory,进而获取SqlSession, 进而getMapper
9.进行CRUD操作(对于修改操作, 还需要提交事务)

可以看到在只使用Mybatis进行开发时的步骤还是相当繁琐的, 其中一些步骤是重复的.

2.创建并配置mybatis-config.xml(如 environment setting 等)
3.在mybatis-config.xml中配置实体别名 alias
7.在mybatis-config.xml中注册XXXMapper.xml
8.编写MybatisUtil获取SqlSessionFactory,进而获取SqlSession, 进而getMapper

对于以上这些步骤, 我们完全可以交由Spring框架进行管理, 从而简化开发,专注于核心业务逻辑的开发

非注解形式整合

  • 引入整合需要的相关依赖(省略了Mybatis,Spring,Mysql相关依赖)

    <dependency>
        <groupId>org.mybatisgroupId>
        <artifactId>mybatis-springartifactId>
        <version>2.0.6version>
    dependency>
    
    <dependency>
        <groupId>com.alibabagroupId>
        <artifactId>druidartifactId>
        <version>1.2.6version>
    dependency>
    <dependency>
        <groupId>org.springframeworkgroupId>
        <artifactId>spring-jdbcartifactId>
        <version>5.3.9version>
    dependency>
    
  • 在applicationContext.xml中进行相关配置

    值得注意的是, 在使用Spring整合Mybatis之后,我们可以省略mybais-config.xml配置文件
    相关配置, 可以通过Bean的属性进行注入

    主要完成三步:

    1. 配置 dataSource
    2. 创建 SqlSessionFatory
    3. 将XXXMapper接口交由Spring管理
    
    <beans xmlns="http://www.springframework.org/schema/beans"
           xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:p="http://www.springframework.org/schema/p"
           xmlns:context="http://www.springframework.org/schema/context"
           xmlns:aop="http://www.springframework.org/schema/aop" xmlns:mybatis="http://mybatis.org/schema/mybatis-spring"
           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-4.2.xsd http://www.springframework.org/schema/aop https://www.springframework.org/schema/aop/spring-aop.xsd http://mybatis.org/schema/mybatis-spring http://mybatis.org/schema/mybatis-spring.xsd">
        <context:property-placeholder location="dbconfig.properties"/>
        
        <bean id="dataSource" class="com.alibaba.druid.pool.DruidDataSource">
            <property name="driverClassName" value="${jdbc.driver}"/>
            <property name="url" value="${jdbc.url}"/>
            <property name="username" value="${jdbc.username}"/>
            <property name="password" value="${jdbc.password}"/>
        bean>
        
        
        
        
        
        <bean id="sqlSessionFactoryBean" class="org.mybatis.spring.SqlSessionFactoryBean">
            
            <property name="dataSource" ref="dataSource"/>
            
            <property name="typeAliasesPackage" value="com.shy.entity"/>
            
            <property name="mapperLocations" value="classpath:mapper/*Mapper.xml"/>
             
             
            
        bean>
        
        <mybatis:scan base-package="com.shy.mapper"/>
        
        
        
        
        
    beans>
    
  • 进行测试即可

    @Test
    public void testActorMapper(){
        ApplicationContext applicationContext = new ClassPathXmlApplicationContext("/applicationContext.xml");
        // 整合之后, 便可以直接getBean()获取XXXMapper
        ActorMapper actorMapper = (ActorMapper) applicationContext.getBean(ActorMapper.class);
        Actor actor = actorMapper.selectActorById(1);
        System.out.println(actor.toString());
        actor.setActorChineseName("吉米");
        actorMapper.updateActorById(actor);
        // 这里不需要提交事务, Druid连接池会默认提交事务
        // 在学习Spring事务管理之后, 我们会使用Spring来管理事务
    }
    

注解形式整合

  • 引入配置Bean

    主要任务包括:

    配置数据源

    ​ -引入外部properties文件

    通过SqlSessionFatoryBean拿到SQLSessionFactory

    -添加数据源
    

    ​ -设置实体别名

    ​ -注册mapper.xml

    ​ -扫描所有Mapper接口

    开启注解扫描

    @Configuration
    @MapperScan("com.shy.mapper") // 扫描注册所有的Mapper接口, 作用类似于MapperScannerConfigurer
    @ComponentScan("com.shy") // 扫描注解
    @PropertySource("/dbconfig.properties") // 引入外部属性文件
    public class SpringMybatisConfig {
    
    
        @Value("${jdbc.driver}")
        private String driverName;
        @Value("${jdbc.url}")
        private String url;
        @Value("${jdbc.username}")
        private String username;
        @Value("${jdbc.password}")
        private String password;
    
    
        /**
         * 配置Druid连接池
         */
        @Bean
        public DruidDataSource dataSource() {
            DruidDataSource dataSource = new DruidDataSource();
            dataSource.setDriverClassName(driverName);
            dataSource.setUrl(url);
            dataSource.setUsername(username);
            dataSource.setPassword(password);
            return dataSource;
        }
    
        /**
         * 配置SqlSessionFactory
          */
        @Bean
        public SqlSessionFactory sqlSessionFactory() throws Exception {
            SqlSessionFactoryBean sqlSessionFactoryBean = new SqlSessionFactoryBean();
            // 配置数据源
            sqlSessionFactoryBean.setDataSource(dataSource());
            // 配置实体类别名
            sqlSessionFactoryBean.setTypeAliasesPackage("com.shy.entity");
            // 注册 Mapper文件
            // Spring提供了ResourcePatternResolver,可以使用通配符注册Mapper文件
            ResourcePatternResolver resourcePatternResolver = new PathMatchingResourcePatternResolver();
            Resource[] resources = resourcePatternResolver.getResources("mapper/*Mapper.xml");
            sqlSessionFactoryBean.setMapperLocations(resources);
    
            // 创建sqlSessionFactory
            SqlSessionFactory sqlSessionFactory = sqlSessionFactoryBean.getObject();
            return sqlSessionFactory;
        }
    }
    
  • 测试

    @Service
    public class ActorServiceImpl implements ActorService {
    
        @Autowired // 自动注入的其实是接口的代理类
        private ActorMapper actorMapper;
    	// ......
    }
    
    @Test
    public void test01(){
        ApplicationContext applicationContext = new AnnotationConfigApplicationContext(SpringMybatisConfig.class);
        ActorService actorService = (ActorService) applicationContext.getBean("actorServiceImpl");
        Actor actor = actorService.getActorById(1);
        System.out.println(actor.toString());
    }
    // Actor{ActorId=1, ActorChineseName='鲍勃·奥登科克', ActorOriginName='Bob Odenkirk', ActorGender='男'}
    

Spring事务管理

参考: mybatis-spring–事务

Spring事务管理提供了多种方式, 这里我们主要介绍 标签式事务管理

声明式事务管理建立在AOP之上,其本质是对方法前后进行拦截,然后在目标方法开始之前创建或者加入一个事务,执行完目标方法之后根据执行的情况提交或者回滚。

什么是事务 ?

事务(Transaction)简单来说, 就是数据库进行的一系列操作, 而这一系列操作是一个原子整体, 不可被分割.

倘若事务中的某一步出现异常, 则必须回滚. 也就是说事务的一系列操作要么全部完成, 要么全部不完成, 从而保证了数据库的完整性

事务的四个特性: ACID

Spring学习记录_第26张图片

传统的事务管理方式

无论是何种框架, 底层均通过Connection对象来完成事务控制

  • JDBC

    Connection.setAutoCommit(false);
    Connection.commit()
    Connection.rollback()        
    
  • Mybatis

    Mybatis 自动开启事务
    SqlSession.commit()
    SqlSession.rollback()
    SqlSession.close() // 封装了 SqlSession.rollback
    

半注解形式

  • 引入依赖

    <dependency>
        <groupId>org.springframeworkgroupId>
        <artifactId>spring-txartifactId>
        <version>5.3.9version>
    dependency>
    
  • 创建Service, 并交由Spring管理

    实战当中, Service层往往编写核心业务逻辑, 需要事务控制

    public class ActorServiceImpl implements ActorService {
    
        private ActorMapper actorMapper; // 整合Mybatis时注入Spring容器
    
        public ActorMapper getActorMapper() {
            return actorMapper;
        }
    
        public void setActorMapper(ActorMapper actorMapper) {
            this.actorMapper = actorMapper;
        }
    
        @Override
        public Actor getActorById(int id) {
            Actor actor = actorMapper.selectActorById(id);
            return actor;
        }
    }
    
    <bean id="actorService" class="com.shy.service.impl.ActorServiceImpl">
        <property name="actorMapper" ref="actorMapper"/>
    bean>
    
  • 使用Spring进行事务管理

    image-20210919134344361

    image-20210919134455685

    
    <bean id="dataSourceTransactionManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
        
        
        <property name="dataSource" ref="dataSource"/> 
    bean>
    
    <tx:annotation-driven transaction-manager="dataSourceTransactionManager"/>
    
  • 为相应的类或方法添加事务控制

    // @Transactional 为该类添加事务
    public class ActorServiceImpl implements ActorService {
    
        // ......
        @Transactional // 为该方法添加事务
        @Override
        public Actor getActorById(int id) {
            Actor actor = actorMapper.selectActorById(id);
            return actor;
        }
    }
    
  • 测试

    /**
    * 测试事务控制
    */
    @Test
    public void test02(){
        ApplicationContext applicationContext = new ClassPathXmlApplicationContext("/applicationContext.xml");
        ActorService actorService = (ActorService) applicationContext.getBean("actorService");
        Actor actor = actorService.getActorById(1);
        System.out.println(actor.toString());
    }
    
    // 通过debug信息看出, 现在由Spring来管理事务
    12:07:39.120 [main] DEBUG org.springframework.jdbc.datasource.DataSourceTransactionManager - Creating new transaction with name [com.shy.service.impl.ActorServiceImpl.getActorById]: PROPAGATION_REQUIRED,ISOLATION_DEFAULT
    

非注解形式

  • 引入依赖

    同半注解形式

  • 创建Service, 并交由Spring管理

    同半注解形式

  • 使用Spring进行事务管理

    不再需要 @Transactional 注解

    
    <tx:advice id="transactionInterceptor" transaction-manager="dataSourceTransactionManager">
        <tx:attributes>
            
            <tx:method name="com.shy.service.impl.ActorService.getActorById" isolation="DEFAULT"/>
            
            <tx:method name="*"/>
        tx:attributes>
    tx:advice>
    <aop:config>
        
        <aop:pointcut id="pc" expression="execution(* com.shy.service.ActorService.getActorById(..))"/>
        
        <aop:advisor advice-ref="transactionInterceptor" pointcut-ref="pc"/>
    aop:config>
    
  • 测试

    /**
    * 测试事务控制
    */
    @Test
    public void test02(){
        ApplicationContext applicationContext = new ClassPathXmlApplicationContext("/applicationContext.xml");
        ActorService actorService = (ActorService) applicationContext.getBean("actorService");
        Actor actor = actorService.getActorById(1);
        System.out.println(actor.toString());
    }
    
    14:09:02.303 [main] DEBUG org.springframework.transaction.interceptor.NameMatchTransactionAttributeSource - Adding transactional method [com.shy.service.impl.ActorService.getActorById] with attribute [PROPAGATION_REQUIRED,ISOLATION_DEFAULT]
    

全注解形式

主要任务包括:

​ 配置数据源

​ 配置事务管理器

​ 通过@EnableTransactionManagement开启注解扫描

  • 引入配置Bean

    @Configuration
    @ComponentScan("com.shy") // 扫描注解
    @PropertySource("/dbconfig.properties") // 引入外部属性文件
    @EnableTransactionManagement // 开启注解扫描, 为@Transactional的类或方法添加事务控制, 作用类似于
    public class SpringMybatisConfig {
    
    
        @Value("${jdbc.driver}")
        private String driverName;
        @Value("${jdbc.url}")
        private String url;
        @Value("${jdbc.username}")
        private String username;
        @Value("${jdbc.password}")
        private String password;
    
    
        /**
         * 配置Druid连接池
         */
        @Bean
        public DruidDataSource dataSource() {
            DruidDataSource dataSource = new DruidDataSource();
            dataSource.setDriverClassName(driverName);
            dataSource.setUrl(url);
            dataSource.setUsername(username);
            dataSource.setPassword(password);
            return dataSource;
        }
    
        /**
         * 配置Spring事务管理器
         */
        @Bean
        public DataSourceTransactionManager dataSourceTransactionManager(){
            DataSourceTransactionManager dataSourceTransactionManager = new DataSourceTransactionManager();
            dataSourceTransactionManager.setDataSource(dataSource());
            return dataSourceTransactionManager;
        }
    
    }
    
  • 测试

    @Test
    public void test01(){
        ApplicationContext applicationContext = new AnnotationConfigApplicationContext(SpringMybatisConfig.class);
        ActorService actorService = (ActorService) applicationContext.getBean("actorServiceImpl");
        Actor actor = actorService.getActorById(1);
        System.out.println(actor.toString());
    }
    // 15:08:44.051 [main] DEBUG o.s.jdbc.datasource.DataSourceTransactionManager - Creating new transaction with name [com.shy.service.impl.ActorServiceImpl.getActorById]: PROPAGATION_REQUIRED,ISOLATION_DEFAULT,timeout_2,-java.lang.RuntimeException,+java.lang.Exception
    // 成功使Spring管理事务
    

Spring事务管理属性

隔离属性

参考: 脏写、脏读、不可重复读和幻读 - 知乎 (zhihu.com)

​ Spring事务传播属性和隔离级别 - Eunice_Sun - 博客园 (cnblogs.com)https://www.cnblogs.com/mseddl/p/11577846.html)

隔离属性解决了多个事务并发时, 可能会产生的问题

  • 脏读

    事务A读取了事务B修改但还没有提交的数据, 而在事务B操作失败回滚之后, 事务A读取的数据就成为了不存在的数据

    解决: @Transactional(isolation = Isolation.READ_COMMITTED)

    保证一个事务修改的数据提交后才能被另外一个事务读取

    Q: 为什么一个事务能读取到其他事务修改后未提交的数据?

    A: image-20210919151324456

  • 不可重复读

    事务A读取某一行数据, 而在事务A还未结束时, 事务B对该行数据进行了修改并提交. 事务A再次读取同一行数据时, 数据发生了改变.

    即同一事务前后读取的同一行数据不同

    解决: @Transactional(isolation = Isolation.REPEATABLE_READ)

    本质是为该行数据加上了一把行锁, 在A结束事务之后, 其他事务才可访问该行数据

  • 幻读

    事务A根据条件查询到了一些数据, 而在事务A还未结束时, 事务B插入了几条符合A查询条件的数据,

    这时事务A再次根据条件查询数据, 发现符合条件的数据多了.

    解决: @Transactional(isolation = Isolation.SERIALIZABLE)

    本质是为该行数据加上了一把表, 在A结束事务之后, 其他事务才可访问该表

  • 默认值

    Isolation.DEFAULT // 使用数据库默认隔离级别, 实战当中推荐使用. 而并发产生的问题通过乐观锁来解决
    
    select @@transaction_isolation; // 查看数据库默认隔离级别
    
  • 总结

    image-20210919151033355

    Spring学习记录_第27张图片

传播属性

传播属性用来解决事务嵌套问题

事务嵌套有可能会导致外层事务丧失原子性

Spring学习记录_第28张图片

  • 默认值

    Propagation.REQUIRED // 
    // 所以在实战当中, 增删改直接采用默认传播属性, 而对查询方法显式指定其传播属性为 SUPPORTS
    

只读属性

针对只进行查询的业务方法, 可以设置readOnly = true, 提高效率

超时属性

超时属性指定了事务等待的最长时间

在事务并发时, 事务访问的数据有可能被另一事务加了锁, 这是需要等待

timeout = -1 // 默认值, 采用数据库默认超时时间. 实战一般不需要修改

回滚策略

// 对于RuntimeException及其子类, 默认回滚 
rollbackFor = {RuntimeException.class}
// 对于Exception及其子类, 默认提交
noRollbackFor = {Exception.class}
// 实战一般保持默认即可

使用YAML进行Spring开发

什么是YAML?

YAML与xml, properties一样, 都是用来进行配置的文件, 不过YAML比后两者语法上更为简单.

YAML语法介绍

YAML 入门教程 | 菜鸟教程 (runoob.com)

【教程】十分钟让你了解yaml - 轩脉刃de刀光剑影_哔哩哔哩_bilibili

yaml.org

使用YAML进行Spring开发

编写driver.yaml

name: Pierre Gasly
number: 10
birthday: 2021/08/14

Spring框架默认是不支持YAML的. 我们可以引入第三方JAR, 解析使用YAML.

这里我们选用SnakeYAML

<dependency>
    <groupId>org.yamlgroupId>
    <artifactId>snakeyamlartifactId>
    <version>1.28version>
dependency>

利用YamlPropertiesFactoryBean读取driver.yaml, 并将其转成Properties集合

利用该Properties集合构造PropertySourcesPlaceholderConfigurer对象(@Value的底层实现)

@Configuration
@ComponentScan("com.shy.entity")
public class APPConfig6 {

    @Bean
    public PropertySourcesPlaceholderConfigurer propertySourcesPlaceholderConfigurer(){
        YamlPropertiesFactoryBean yamlPropertiesFactoryBean = new YamlPropertiesFactoryBean();
        yamlPropertiesFactoryBean.setResources(new ClassPathResource("driver.yaml"));
        Properties properties = (Properties) yamlPropertiesFactoryBean.getObject();
        PropertySourcesPlaceholderConfigurer propertySourcesPlaceholderConfigurer = new PropertySourcesPlaceholderConfigurer();
        propertySourcesPlaceholderConfigurer.setProperties(properties);
        return propertySourcesPlaceholderConfigurer;
    }
}
@Component
public class Driver {
    @Value("${name}")
    public String name;
    @Value("${number}")
    public int number;
    @Value("${birthday}")
    public Date birthday;

测试

@Test
public void test39(){
    ApplicationContext applicationContext = new AnnotationConfigApplicationContext(APPConfig6.class);
    Driver driver = (Driver) applicationContext.getBean("driver");
    System.out.println("driver.name = " + driver.name);
    System.out.println("driver.number = " + driver.number);
    System.out.println("driver.birthday = " + driver.birthday);

}
/**
driver.name = Pierre Gasly
driver.number = 10
driver.birthday = Sat Aug 14 00:00:00 CST 2021
**/

YAML开发中存在的问题

@Value注解无法解析.yaml文件中定义的list集合

championships:
  - 2022
  - 2023
  - 2024
@Value("${championships}")
public List<Integer> championships;
// Could not resolve placeholder 'championships' in value "${championships}"

(此问题, 在SpringBoot中可以使用@ConfigurationProperties解决)

在Spring中, 可以利用SpEL表达式来解决

championships: 2022,2023,2024
@Value("#{'${championships}'.split(',')}")
public List<Integer> championships;

你可能感兴趣的:(spring,java,后端)