Loading

Spring 5 笔记


课程内容介绍

尚硅谷 Spring 5

视频地址:https://www.bilibili.com/video/BV1Vf4y127N5

  1. Spring 概念

  2. IoC 容器

    1. IoC 底层原理

    2. IoC 接口(BeanFactory)

    3. IoC 操作 Bean 管理(基于xml)

    4. IoC 操作 Bean 管理(基于注解)

  3. AOP

  4. JdbcTemplate

  5. 事务管理

  6. Spring 5 新特性

Spring 框架概述

  1. Spring 是轻量级的开源的 JavaEE 框架

  2. Spring 可以解决企业应用开发的复杂性

  3. 两个核心部分:IoC、AOP

    • IoC:Inversion of Control,控制反转。把创建对象过程交给 Spring 进行管理
    • AOP:Aspect Oriented Programming,面向切面编程。不修改源代码进行功能增强
  4. Spring 特点

    1. 方便解耦,简化开发
    2. AOP 编程支持
    3. 方便程序测试
    4. 方便和其他框架进行整合
    5. 方便进行事务操作
    6. 降低 API 开发难度
  5. 本课程选取 Spring 5.x

IoC

概念和原理

1、什么是 IoC ?

控制反转,把对象创建和对象之间的调用过程,交给 Spring 进行管理

使用 IoC 目的:降低耦合度

入门案例就是 IoC 实现

2、IoC 底层原理

xml 解析、工厂模式、反射

IoC 原理

接口

1、IoC 思想基于 IoC 容器完成,IoC 容器底层就是对象工厂

2、Spring 提供 IoC 容器实现两种方式(两个接口):

(1) BeanFactory :IoC 容器基本实现,是 Spring 内部使用接口,一般不提供开发人员使用

加载配置文件时不会创建对象,在获取(使用)对象时才创建

(2) ApplicationContextBeanFactory 接口的子接口,提供更多更强大的功能,一般由开发人员使用

加载配置文件时就会将配置的对象创建

3、 ApplicationContext 接口有实现类

BeanFactory 接口继承树

IoC 操作 Bean 管理

1、什么是 Bean 管理

Bean 管理指的是两个操作:Spring 创建对象,Spring 注入属性

2、Bean 管理操作有两种方式

  • 基于 xml 配置文件方式实现

  • 基于注解方式实现

基于 xml 方式

基于 xml 方式创建对象

<bean id="alice" class="com.yin.spring5.bean.User"></bean>
  1. 在 Spring 配置文件中,使用 bean 标签,标签中添加对应属性,就可以实现对象创建

  2. bean 标签有很多属性,常用属性:

    • id:唯一标识
    • class:全类名
  3. 创建对象的时候,默认执行无参构造器

基于 xml 方式注入属性

DI:Dependency Injection,依赖注入,是 IoC 的一种实现方式

  1. 第一种注入方式:set 方法注入

    <!--set 方法注入-->
    <bean id="alice" class="com.yin.spring5.bean.User">
        <property name="name" value="Alice"/>
        <property name="age" value="20"/>
    </bean>
    
  2. 第二种注入方式:有参构造器注入

    <!--有参构造器注入-->
    <bean id="bob" class="com.yin.spring5.bean.User">
        <constructor-arg name="name" value="Bob"/>
        <constructor-arg name="age" value="20"/>
    </bean>
    
  3. p 名称空间注入(了解即可)

    添加约束:

    xmlns:p="http://www.springframework.org/schema/p"
    
    <!--p命名空间-->
    <bean id="cindy" class="com.yin.spring5.bean.User" p:name="Cindy" p:age="18">
    </bean>
    
  4. c 名称空间注入(了解即可)

    添加约束:

    xmlns:c="http://www.springframework.org/schema/c"
    
    <!--c命名空间-->
    <bean id="david" class="com.yin.spring5.bean.User" c:name="David" c:age="18">
    </bean>
    
xml 注入其他类型
  1. 字面量

    • 注入 null

      <!--注入null-->
      <bean id="eva" class="com.yin.spring5.bean.User">
          <property name="name" value="Eva"/>
          <property name="age">
              <null/>
          </property>
      </bean>
      
    • 属性值包含特殊字符

      • 将特殊字符进行转义

        <!--属性值包含特殊字符-->
        <bean id="special" class="com.yin.spring5.bean.User">
            <!--将特殊字符进行转义-->
            <property name="name" value="&lt;&lt;TRUMP&gt;&gt;"/>
            <property name="age" value="18"/>
        </bean>
        

        输出:User{name='<<TRUMP>>', age=18}

      • 使用 CDATA

        <!--属性值包含特殊字符-->
        <bean id="special" class="com.yin.spring5.bean.User">
            <!--使用 CDATA-->
            <property name="name">
                <value><![CDATA[<<BIDEN>>]]></value>
            </property>
            <property name="age" value="18"/>
        </bean>
        

        输出:User{name='<<BIDEN>>', age=18}

  2. 外部 bean

    public class UserDao {
        public void delete() {
            System.out.println("UserDao#delete()...");
        }
    }
    
    public class UserService {
        UserDao dao;
    
        public void setDao(UserDao dao) {
            this.dao = dao;
        }
    
        public void deleteUser() {
            System.out.println("UserService#deleteUser()...");
            dao.delete();
        }
    }
    
    <bean id="userDao" class="com.yin.spring5.dao.UserDao">
    </bean>
    <bean id="userService" class="com.yin.spring5.service.UserService">
        <!--注入外部 bean-->
        <property name="dao" ref="userDao"/>
    </bean>
    
  3. 内部 bean

    public class Department {
        private String departName;
        // getter setter toString 等方法在此省略
    }
    
    public class Employee {
        private String empName;
        private String email;
        private Department dept;
        // getter setter toString 等方法在此省略
    }
    
    <bean id="employee" class="com.yin.spring5.bean.Employee">
        <property name="empName" value="杰克"/>
        <property name="email" value="jack@jack.com"/>
        <!--内部 bean-->
        <property name="dept">
            <bean class="com.yin.spring5.bean.Department">
                <property name="departName" value="开发部"/>
            </bean>
        </property>
    </bean>
    

    输出:Employee{empName='杰克', email='jack@jack.com', dept=Department{departName='开发部'}}

  4. 级联赋值

    写法一:

    <bean id="employee2" class="com.yin.spring5.bean.Employee">
        <property name="empName" value="Pony"/>
        <property name="email" value="pony@pony.com"/>
        <property name="dept" ref="department"/>
    </bean>
    <bean id="department" class="com.yin.spring5.bean.Department">
        <property name="departName" value="运维部"/>
    </bean>
    

    输出:Employee{empName='Pony', email='pony@pony.com', dept=Department{departName='运维部'}}

    写法二:

    <bean id="employee2" class="com.yin.spring5.bean.Employee">
        <property name="empName" value="Pony"/>
        <property name="email" value="pony@pony.com"/>
        <property name="dept" ref="department"/>
        <!--注意这里-->
        <property name="dept.departName" value="开发部"/>
    </bean>
    <bean id="department" class="com.yin.spring5.bean.Department">
        <property name="departName" value="运维部"/>
    </bean>
    

    输出:Employee{empName='Pony', email='pony@pony.com', dept=Department{departName='开发部'}}

xml 注入集合属性
  1. 注入数组类型属性

    <!--数组类型注入-->
    <property name="array">
        <array>
            <value>array1</value>
            <value>array2</value>
            <value>array3</value>
        </array>
    </property>
    
  2. 注入 List 集合类型属性

    <!--List 类型注入-->
    <property name="list">
        <list>
            <value>list1</value>
            <value>list2</value>
            <value>list3</value>
        </list>
    </property>
    
  3. 注入 Map 集合类型属性

    <!--Map 类型注入-->
    <property name="map">
        <map>
            <entry key="key1" value="value1"/>
            <entry key="key2" value="value2"/>
            <entry key="key3" value="value3"/>
        </map>
    </property>
    
  4. 注入 Set 集合类型属性

    <!--Set 类型注入-->
    <property name="set">
        <set>
            <value>set1</value>
            <value>set2</value>
            <value>set3</value>
        </set>
    </property>
    

    输出:

    Student{stuName='Pony', array=[array1, array2, array3], list=[list1, list2, list3], map={key1=value1, key2=value2, key3=value3}, set=[set1, set2, set3]}
    
  5. 在集合里面设置对象类型

    <bean id="student" class="com.yin.spring5.bean.Student">
    	<!--省略部分代码,只看有关的-->
        <!--在集合里面设置对象类型-->
        <property name="courses">
            <list>
                <ref bean="course1"/>
                <ref bean="course2"/>
            </list>
        </property>
    </bean>
    
    <bean id="course1" class="com.yin.spring5.bean.Course">
        <property name="courseName" value="数据结构"/>
        <property name="credit" value="5"/>
    </bean>
    <bean id="course2" class="com.yin.spring5.bean.Course">
        <property name="courseName" value="操作系统"/>
        <property name="credit" value="4"/>
    </bean>
    

    输出:

    [Course{courseName='数据结构', credit=5}, Course{courseName='操作系统', credit=4}]
    
  6. 把集合注入部分提取出来

    a. 在配置文件中引入 util 名称空间

    <?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:util="http://www.springframework.org/schema/util"
           xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd
                http://www.springframework.org/schema/util http://www.springframework.org/schema/util/spring-util.xsd">
        
    </beans>
    

    b. 使用 util 标签完成 List 集合注入提取

    public class Book {
        private List<String> list;
        // getter setter toString 等方法在此省略
    }
    
    <bean id="book" class="com.yin.spring5.bean.Book">
        <property name="list" ref="bookList"/>
    </bean>
    
    <!--List 集合注入提取-->
    <util:list id="bookList">
        <value>Java编程思想</value>
        <value>算法导论</value>
        <value>Effective Java</value>
    </util:list>
    

    输出:

    [Java编程思想, 算法导论, Effective Java]
    

FactoryBean

普通 bean:配置文件中定义的 bean 类型就是返回类型

工厂 bean:配置文件中定义的 bean 类型可以和返回类型不一样

  1. 创建类,实现 FactoryBean 接口
  2. 实现接口的方法
/**
 * MyFactoryBean 工厂Bean
 */
public class MyFactoryBean implements FactoryBean<Course> {
    @Override
    public Course getObject() throws Exception {
        return new Course();
    }

    @Override
    public Class<?> getObjectType() {
        return null;
    }

    @Override
    public boolean isSingleton() {
        return false;
    }
}
<bean id="myFactoryBean" class="com.yin.spring5.bean.MyFactoryBean">
</bean>
@Test
public void test3() {
    ApplicationContext ioc =
        new ClassPathXmlApplicationContext("spring3.xml");
    Course course = ioc.getBean("myFactoryBean", Course.class);
    course.setCourseName("数据结构");
    course.setCredit(5);
    System.out.println(course);
}

测试:

Course{courseName='数据结构', credit=5}

Bean 作用域

在 Spring 中,设置创建 bean 实例是单实例还是多实例(默认单实例

@Test
public void test4() {
    ApplicationContext ioc =
        new ClassPathXmlApplicationContext("spring2.xml");
    Book book1 = ioc.getBean("book", Book.class);
    Book book2 = ioc.getBean("book", Book.class);
    // 这里打印对象地址,需要把其 toString() 方法注释掉
    System.out.println(book1);
    System.out.println(book2);
    System.out.println(book1==book2);
}

输出:

com.yin.spring5.bean.Book@433defed
com.yin.spring5.bean.Book@433defed
true

如何设置单实例/多实例?bean 标签中 scope 属性设置作用域

scope 属性值:

  • singleton :单实例
  • prototype :多实例

将 scope 属性值设置为 `` 后,再进行上面测试:

<bean id="book" class="com.yin.spring5.bean.Book" scope="prototype">

输出:

com.yin.spring5.bean.Book@548a24a
com.yin.spring5.bean.Book@433defed
false

singletonprototype 的区别:

  1. singleton 是单实例,prototype 是多实例

  2. singleton 在 Spring 加载配置文件时就会创建单实例对象

    prototype 在获取对象时才会进行创建

Bean 生命周期

生命周期:从对象创建到销毁的过程

bean 生命周期:

  1. 通过构造器创建 bean 实例(无参构造)
  2. 为 bean 注入属性和对其他 bean 的引用(set 方法)
  3. 调用 bean 的初始化方法(需要进行配置初始化方法)
  4. 获取并使用 bean
  5. 当容器关闭时,调用 bean 的销毁方法(需要进行配置销毁方法)

代码验证:

public class User {
    private String name;
    private Integer age;
    private Character gender;

    public User() {
        System.out.println("User 无参构造器执行。。。");
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        System.out.println("User.setName 执行。。。");
        this.name = name;
    }

    public Integer getAge() {
        return age;
    }

    public void setAge(Integer age) {
        System.out.println("User.setAge 执行。。。");
        this.age = age;
    }

    public Character getGender() {
        return gender;
    }

    public void setGender(Character gender) {
        System.out.println("User.setGender 执行。。。");
        this.gender = gender;
    }

    public void init() {
        System.out.println("自定义 User.init 执行。。。");
    }

    public void destroy() {
        System.out.println("自定义 User.destroy 执行。。。");
    }
}
<bean id="user" class="com.yin.spring5.bean.User"
      init-method="init" destroy-method="destroy">
    <property name="name" value="Alice"/>
    <property name="gender" value="F"/>
    <property name="age" value="20"/>
</bean>
@Test
public void test5() {
    ClassPathXmlApplicationContext ioc =
        new ClassPathXmlApplicationContext("spring4.xml");
    User user = ioc.getBean("user", User.class);
    System.out.println(user);
    // 关闭容器
    ioc.close();
}

输出:

User 无参构造器执行。。。
User.setName 执行。。。
User.setGender 执行。。。
User.setAge 执行。。。
自定义 User.init 执行。。。
com.yin.spring5.bean.User@16eb3ea3
自定义 User.destroy 执行。。。

其实完整来讲,bean 的声明周期有 7 步。在初始化方法执行前后,分别有 postProcessBeforeInitializationpostProcessAfterInitialization 方法执行。

在上述代码的基础上,再添加以下内容:

public class MyBeanPost implements BeanPostProcessor {
    @Override
    public Object postProcessBeforeInitialization(Object bean,
                                                  String beanName) throws BeansException {
        System.out.println("    postProcessBeforeInitialization 执行。。。");
        return null;
    }

    @Override
    public Object postProcessAfterInitialization(Object bean,
                                                 String beanName) throws BeansException {
        System.out.println("    postProcessAfterInitialization 执行。。。");
        return null;
    }
}
<bean id="myBeanPost" class="com.yin.spring5.bean.MyBeanPost"></bean>

再次进行测试,输出如下:

User 无参构造器执行。。。
User.setName 执行。。。
User.setGender 执行。。。
User.setAge 执行。。。
    postProcessBeforeInitialization 执行。。。
自定义 User.init 执行。。。
    postProcessAfterInitialization 执行。。。
com.yin.spring5.bean.User@33bc72d1
自定义 User.destroy 执行。。。

综上,bean 生命周期完整表述如下:

  1. 通过构造器创建 bean 实例(无参构造)
  2. 为 bean 注入属性和对其他 bean 的引用(set 方法)
  3. 把 bean 实例传递给初始化前的后置处理 postProcessBeforeInitialization
  4. 调用 bean 的初始化方法(需要进行配置初始化方法)
  5. 把 bean 实例传递给初始化后的后置处理 postProcessAfterInitialization
  6. 获取并使用 bean
  7. 当容器关闭时,调用 bean 的销毁方法(需要进行配置销毁方法)

xml 自动装配

什么是自动装配?

根据指定装配规则(属性名称或属性类型),Spring 自动将匹配的属性值进行注入

bean 标签中的 autowire 属性可以配置自动装配,autowire 常用属性值:

  • byName :根据名称装配(注入 bean 的 id 要与被注入 bean 的 set 方法一致
  • byType :根据类型装配(有多个同类型的 bean 时,此属性值无法使用)

代码演示:

public class Department {
    // 此处略去 get set toString 等方法
    private String deptName;
}
public class Employee {
    // 此处略去 get set toString 等方法
    private String empName;
    private Department dept;
}
<bean id="dept" class="com.yin.spring5.autowire.Department">
    <property name="deptName" value="开发部"/>
</bean>
<bean id="dept2" class="com.yin.spring5.autowire.Department">
    <property name="deptName" value="测试部"/>
</bean>

<bean id="emp" class="com.yin.spring5.autowire.Employee" autowire="byName">
    <property name="empName" value="Pony"/>
</bean>

输出:

Employee{empName='Pony', dept=Department{deptName='开发部'}}

若将 Employee 类中默认生成的 setDept 方法名修改为 setDept2,则输出变为:

Employee{empName='Pony', dept=Department{deptName='测试部'}}

由此证实:byName 的自动注入是根据 set 方法判断的,而不是类中的属性名

外部属性文件

以数据库连接池为例:

<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/jdbc_template?serverTimezone=Asia/Shanghai"/>
    <property name="username" value="root"/>
    <property name="password" value="123456"/>
</bean>

将数据库配置抽取到 properties 文件

jdbc.driver=com.mysql.cj.jdbc.Driver
jdbc.url=jdbc:mysql://localhost:3306/jdbc_template?serverTimezone=Asia/Shanghai
jdbc.username=root
jdbc.password=123456

则 Spring 配置文件改写如下,注意需要引入 context 名称空间

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

    <!--引入外部 properties 文件-->
    <context:property-placeholder location="classpath:db-config.properties"/>

    <bean id="dataSource" class="com.alibaba.druid.pool.DruidDataSource">
        <!--取出 properties 文件中的值-->
        <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>
</beans>

基于注解方式

创建 bean

Spring 创建 bean 的四个注解:@Component@Controller@Service@Repository

<!--开启组件扫描,将会扫描 base-package 及其子包下所有组件-->
<context:component-scan base-package="com.yin.spring5"/>

排除某些组件,以排除 @Controller 组件扫描为例:

<!--开启组件扫描-->
<context:component-scan base-package="com.yin.spring5">
    <!--排除 Controller 组件扫描-->
    <context:exclude-filter type="annotation"
                            expression="org.springframework.stereotype.Controller"/>
</context:component-scan>

context:component-scan 标签部分属性:

  • base-package :要扫描组件的基础包,默认将会扫描 base-package 及其子包下所有组件
  • use-default-filters :是否使用默认的扫描规则

context:component-scan 标签的子标签:

  • context:exclude-filter :指定排除规则
  • context:include-filter :指定包含规则

属性注入

  • @Autowired :根据属性类型自动装配
  • @Qualifier :根据属性名称注入
  • @Resource :可以根据类型注入,可以根据名称注入(这是 javax 包下的注解,已移除)
  • @Value :注入普通类型属性

AOP

AOP,Aspect Oriented Programming,面向切面编程

利用 AOP 可以对业务逻辑的各个部分进行隔离,从而使得业务逻辑各部分之间的耦合度降低,提高程序的可重用性,同时提高了开发的效率。

底层原理

AOP 底层使用动态代理。有两种情况:

  • 有接口,使用 JDK 动态代理

    创建接口实现类代理对象,增强类的方法

    JDK动态代理

  • 没有接口,使用 CGLIB 动态代理

    创建子类的代理对象,增强类的方法

    CGLIB动态代理

JDK 动态代理

使用 JDK 动态代理,使用 java.lang.reflect.Proxy 类里面的方法创建代理对象

newProxyInstance方法

newProxyInstance方法参数

代码演示:

  1. 创建接口,定义方法

    public interface UserDao {
        int add(int a, int b);
    
        String update(String id);
    }
    
  2. 创建接口实现类,实现方法

    public class UserDaoImpl implements UserDao {
        @Override
        public int add(int a, int b) {
            System.out.println("add 方法执行");
            return a + b;
        }
    
        @Override
        public String update(String id) {
            System.out.println("update 方法执行");
            return id;
        }
    }
    
  3. 使用 java.lang.reflect.Proxy 类创建接口代理对象

    public class JdkProxy {
        public static void main(String[] args) {
            // 创建接口实现类代理对象
            Class[] interfaces = {UserDao.class};
            UserDao userDao = new UserDaoImpl(); // 被代理对象
            UserDao proxyInstance =
                    (UserDao) Proxy.newProxyInstance(JdkProxy.class.getClassLoader(),
                            interfaces, new UserDaoProxy(userDao));
            System.out.println(proxyInstance.add(2, 3));
            System.out.println(proxyInstance.update("abc"));
        }
    }
    
    // 创建代理对象代码
    class UserDaoProxy implements InvocationHandler {
        private Object obj;
    
        // 把被代理对象传递过来
        public UserDaoProxy(Object obj) {
            this.obj = obj;
        }
    
        // 增强的逻辑
        @Override
        public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
            // 方法之前
            System.out.println(method.getName() + " 方法之前执行,参数:" + Arrays.toString(args));
            // 被增强的方法执行
            Object returnValue = method.invoke(obj, args);
            // 方法之后
            System.out.println(method.getName() + " 方法之后执行");
            return returnValue;
        }
    }
    

输出:

add 方法之前执行,参数:[2, 3]
add 方法执行
add 方法之后执行
5
update 方法之前执行,参数:[abc]
update 方法执行
update 方法之后执行
abc

InvocationHandler接口

InvocationHandler.invoke方法.png

AOP 术语

  1. 连接点:类中可以被增强的方法称为连接点
  2. 切入点:实际被真正增强的方法称为切入点
  3. 通知(增强):实际增强的逻辑部分称为通知(增强)
    • 前置通知
    • 后置通知
    • 环绕通知
    • 异常通知
    • 最终通知
  4. 切面:把通知应用到切入点的过程

AOP 操作

Spring 框架一般基于 AspectJ 实现 AOP 操作

AspectJ 不是 Spring 组成部分,而是独立的 AOP 框架,但是一般把 AspectJ 和 Spring 框架一起使用,进行 AOP 操作

Spring 基于 AspectJ 实现 AOP 操作有两种方式:xml 配置文件方式、注解方式。

引入依赖

<dependencies>
    <dependency>
        <groupId>org.springframework</groupId>
        <artifactId>spring-context</artifactId>
        <version>${spring.version}</version>
    </dependency>
    <!-- https://mvnrepository.com/artifact/org.springframework/spring-aspects -->
    <dependency>
        <groupId>org.springframework</groupId>
        <artifactId>spring-aspects</artifactId>
        <version>${spring.version}</version>
    </dependency>
</dependencies>

AOP依赖图

切入点表达式

切入点表达式作用:指明对哪个类的哪个方法进行增强

语法结构:

execution([权限修饰符][返回类型][全类名][方法名]([参数列表]))

例 1:对 com.yin.spring5.UserDao#add 方法进行增强

execution(* com.yin.spring5.UserDao.add(..))    // 权限修饰符省略

例 2:对 com.yin.spring5.UserDao 类中所有方法增强

execution(* com.yin.spring5.UserDao.*(..))    // 权限修饰符省略

例 2:对 com.yin.spring5 包下所有类中所有方法增强

execution(* com.yin.spring5.*.*(..))    // 权限修饰符省略

基于注解

  1. 创建类,在类中定义方法

    public class User {
        public void add() {
            System.out.println("User.add 方法执行");
        }
    }
    
  2. 创建增强类,编写增强逻辑

    在增强类中,创建方法,让不同的方法代表不同的通知

    public class UserProxy {
        // 前置通知
        public void before() {
            System.out.println("UserProxy.before 方法执行");
        }
        ......
    }
    
  3. 进行通知的配置

    1. 配置文件开启注解扫描

      <context:component-scan base-package="com.yin.spring5.anno"/>
      
    2. 使用注解创建 User 和 UserProxy 对象

      @Component
      public class User {...}
      
      @Component
      public class UserProxy {...}
      
    3. 在增强类上面添加注解 @Aspect

      @Component
      @Aspect
      public class UserProxy {...}
      
    4. 配置文件开启生成代理对象

      <!--开启 Aspect 生成代理对象,要引入aop名称空间-->
      <aop:aspectj-autoproxy/>
      
  4. 配置不同类型的通知

    在增强类的里面,在作为通知方法上面添加通知类型注解,使用切入点表达式配置

    @Component
    @Aspect
    public class UserProxy {
        // 前置通知
        @Before("execution(* com.yin.spring5.anno.User.add(..))")
        public void before() {
            System.out.println("UserProxy.before 方法执行");
        }
    
        // 最终通知
        @After("execution(* com.yin.spring5.anno.User.add(..))")
        public void after() {
            System.out.println("UserProxy.after 方法执行");
        }
    
        // 后置通知(返回通知)
        @AfterReturning("execution(* com.yin.spring5.anno.User.add(..))")
        public void afterReturning() {
            System.out.println("UserProxy.afterReturning 方法执行");
        }
    
        // 异常通知
        @AfterThrowing("execution(* com.yin.spring5.anno.User.add(..))")
        public void afterThrowing() {
            System.out.println("UserProxy.afterThrowing 方法执行");
        }
    
        // 环绕通知
        @Around("execution(* com.yin.spring5.anno.User.add(..))")
        public void around(ProceedingJoinPoint proceedingJoinPoint) throws Throwable {
            System.out.println("环绕之前。。。");
    
            // 被增强的方法执行
            proceedingJoinPoint.proceed();
    
            System.out.println("环绕之后。。。");
        }
    }
    

测试:

@Test
public void test1() {
    ApplicationContext context = new ClassPathXmlApplicationContext("spring1.xml");
    User user = context.getBean("user", User.class);
    user.add();
}

输出:

环绕之前。。。
UserProxy.before 方法执行
User.add 方法执行
UserProxy.afterReturning 方法执行
UserProxy.after 方法执行
环绕之后。。。

抽取相同的切入点

@Component
@Aspect
public class UserProxy {
    // 抽取相同的切入点
    @Pointcut("execution(* com.yin.spring5.anno.User.add(..))")
    public void pointDemo() {
    }

    // 前置通知
    @Before("pointDemo()")
    public void before() {
        System.out.println("UserProxy.before 方法执行");
    }
    
    ...
}

有多个增强类对同一个方法进行增强,设置增强类优先级

在增强类上添加注解 @Order(value = ) ,数字越小,优先级越高

完全使用注解

创建配置类,不需要创建 xml 配置文件

@Configuration
@ComponentScan(basePackages = "com.yin.spring5.anno")
@EnableAspectJAutoProxy(proxyTargetClass = true)
public class AopConfig {
}

基于 xml 配置文件


JdbcTemplate

什么是 JdbcTemplate ?

Spring 对 JDBC 进行封装,使用 JdbcTemplate 方便实现对数据库操作

环境配置

引入依赖

<dependencies>
    <dependency>
        <groupId>org.springframework</groupId>
        <artifactId>spring-context</artifactId>
        <version>${spring.version}</version>
    </dependency>
    <!-- https://mvnrepository.com/artifact/org.springframework/spring-jdbc -->
    <dependency>
        <groupId>org.springframework</groupId>
        <artifactId>spring-jdbc</artifactId>
        <version>${spring.version}</version>
    </dependency>
    <!-- https://mvnrepository.com/artifact/mysql/mysql-connector-java -->
    <dependency>
        <groupId>mysql</groupId>
        <artifactId>mysql-connector-java</artifactId>
        <version>8.0.19</version>
    </dependency>
    <!-- https://mvnrepository.com/artifact/com.alibaba/druid -->
    <dependency>
        <groupId>com.alibaba</groupId>
        <artifactId>druid</artifactId>
        <version>1.1.22</version>
    </dependency>
</dependencies>

JdbcTemplate依赖图

配置数据库连接池

<!--引入外部数据库配置文件-->
<context:property-placeholder location="classpath:db-config.properties"/>
<!--Druid 数据源-->
<bean id="dataSource" class="com.alibaba.druid.pool.DruidDataSource"
      destroy-method="close">
    <property name="driverClassName" value="${jdbc.driverClass}"/>
    <property name="url" value="${jdbc.url}"/>
    <property name="username" value="${jdbc.username}"/>
    <property name="password" value="${jdbc.password}"/>
</bean>

配置 JdbcTemplate

<!--JdbcTemplate-->
<bean id="jdbcTemplate" class="org.springframework.jdbc.core.JdbcTemplate">
    <!--注入数据源-->
    <property name="dataSource" ref="dataSource"/>
</bean>

编写类文件

编写 Book 实体类,创建对应数据表

public class Book {
    private Integer id;
    private String title;
    private String author;
    private BigDecimal price;
    // 此处省略 getter setter toString 等方法
}

环境一览

<?xml version="1.0" encoding="UTF-8"?>
<beans>
    <!--注意:此处文件头等信息略去-->
    <!--组件扫描-->
    <context:component-scan base-package="com.yin.spring5"/>
    <!--引入外部数据库配置文件-->
    <context:property-placeholder location="classpath:db-config.properties"/>

    <!--Druid 数据源-->
    <bean id="dataSource" class="com.alibaba.druid.pool.DruidDataSource"
          destroy-method="close">
        <property name="driverClassName" value="${jdbc.driverClass}"/>
        <property name="url" value="${jdbc.url}"/>
        <property name="username" value="${jdbc.username}"/>
        <property name="password" value="${jdbc.password}"/>
    </bean>

    <!--JdbcTemplate-->
    <bean id="jdbcTemplate" class="org.springframework.jdbc.core.JdbcTemplate">
        <!--注入数据源-->
        <property name="dataSource" ref="dataSource"/>
    </bean>
</beans>
public interface BookDao {
}
@Repository
public class BookDaoImpl implements BookDao {
    JdbcTemplate jdbcTemplate;

    @Autowired
    public void setJdbcTemplate(JdbcTemplate jdbcTemplate) {
        this.jdbcTemplate = jdbcTemplate;
    }
}
@Service
public class BookService {
    BookDao bookDao;

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

操作数据库

@Override
public int insert(Book book) {
    String sql = "INSERT INTO book(title,author,price) VALUES(?,?,?)";
    return jdbcTemplate.update(sql, book.getTitle(), book.getAuthor(), book.getPrice());
}

@Override
public int update(Book book) {
    String sql = "UPDATE book SET title=?, author=?, price=? WHERE id=?";
    return jdbcTemplate.update(sql, book.getTitle(), book.getAuthor(),
                               book.getPrice(), book.getId());
}

@Override
public int delete(Integer id) {
    String sql = "DELETE FROM book WHERE id=?";
    return jdbcTemplate.update(sql, id);
}

某个值

@Override
public int recordCount() {
    String sql = "SELECT COUNT(*) FROM book";
    return jdbcTemplate.queryForObject(sql, Integer.TYPE);
}

对象

@Override
public Book getOneById(Integer id) {
    String sql = "SELECT id, title, author, price FROM book WHERE id=?";
    return jdbcTemplate.queryForObject(sql, new BeanPropertyRowMapper<>(Book.class), id);
}

集合

@Override
public List<Book> getAll() {
    String sql = "SELECT * FROM book";
    return jdbcTemplate.query(sql, new BeanPropertyRowMapper<>(Book.class));
}

批量操作

批量添加

@Override
public int[] batchInsert(List<Object[]> bookList) {
    String sql = "INSERT INTO book(title,author,price) VALUES(?,?,?)";
    return jdbcTemplate.batchUpdate(sql, bookList);
}
@Test
public void testBatchAdd() {
    ApplicationContext context = new ClassPathXmlApplicationContext("spring1.xml");
    BookService bookService = context.getBean("bookService", BookService.class);
    List<Object[]> bookList = new ArrayList<>();
    // 按占位符顺序写参数
    Object[] book1 = {"城南旧事", "林海音", 29.00};
    Object[] book2 = {"追风筝的人", "忘了", 29.00};
    Object[] book3 = {"小王子", "安东尼", 19.00};
    bookList.add(book1);
    bookList.add(book2);
    bookList.add(book3);
    bookService.batchAddBook(bookList);
}

批量修改

@Override
public int[] batchUpdate(List<Object[]> bookList) {
    String sql = "UPDATE book SET title=?, author=?, price=? WHERE id=?";
    return jdbcTemplate.batchUpdate(sql, bookList);
}
@Test
public void testBatchUpdate() {
    ApplicationContext context = new ClassPathXmlApplicationContext("spring1.xml");
    BookService bookService = context.getBean("bookService", BookService.class);
    List<Object[]> bookList = new ArrayList<>();
    // 按占位符顺序写参数
    Object[] book1 = {"城南旧事", "林海音(台湾)", 26.00, 24};
    Object[] book2 = {"追风筝的人", "还是没想起来", 29.00, 25};
    Object[] book3 = {"小王子", "安东尼奥", 20.00, 26};
    bookList.add(book1);
    bookList.add(book2);
    bookList.add(book3);
    bookService.batchUpdateBook(bookList);
}

批量删除

@Override
public int[] batchDelete(List<Object[]> idList) {
    String sql = "DELETE FROM book WHERE id=?";
    return jdbcTemplate.batchUpdate(sql, idList);
}
@Test
public void testBatchDelete() {
    ApplicationContext context = new ClassPathXmlApplicationContext("spring1.xml");
    BookService bookService = context.getBean("bookService", BookService.class);
    List<Object[]> idList = new ArrayList<>();
    // 按占位符顺序写参数
    Object[] id1 = {19};
    Object[] id2 = {20};
    Object[] id3 = {21};
    Object[] id4 = {25};
    idList.add(id1);
    idList.add(id2);
    idList.add(id3);
    idList.add(id4);
    bookService.batchDeleteBook(idList);
}

事务

概念

  1. 什么是事务?

    事务是数据库操作的最基本单元,逻辑上的一组操作,要么都成功,如果有一个操作失败,所有操作都失败

    典型场景:银行转账

  2. 事务四个特性 ACID

    • 原子性 Atomicity

      原子性是指事务是一个不可分割的工作单位,事务中的操作要么都成功,要么都不成功

    • 一致性 Consistency

      事务必须使数据库从一个一致性状态变换到另一个一致性状态

    • 隔离性 Isolation

      事务的隔离性是指一个事务的执行不能被其他事务干扰,即一个事务内部的操作及使用的数据对并发的其他事务是隔离的,并发执行的各个事务之间不能互相干扰

    • 持久性 Durability

      持久性是指一个事务一旦被提交,它对数据库中数据的改变就是永久性的,接下来的其他操作和数据库故障不应该对其有任何影响

环境搭建

引入依赖

<dependencies>
    <dependency>
        <groupId>org.springframework</groupId>
        <artifactId>spring-context</artifactId>
        <version>${spring.version}</version>
    </dependency>
    <!-- https://mvnrepository.com/artifact/org.springframework/spring-jdbc -->
    <dependency>
        <groupId>org.springframework</groupId>
        <artifactId>spring-jdbc</artifactId>
        <version>${spring.version}</version>
    </dependency>
    <!-- https://mvnrepository.com/artifact/org.springframework/spring-orm -->
    <dependency>
        <groupId>org.springframework</groupId>
        <artifactId>spring-orm</artifactId>
        <version>${spring.version}</version>
    </dependency>
    <!-- https://mvnrepository.com/artifact/mysql/mysql-connector-java -->
    <dependency>
        <groupId>mysql</groupId>
        <artifactId>mysql-connector-java</artifactId>
        <version>8.0.19</version>
    </dependency>
    <!-- https://mvnrepository.com/artifact/com.alibaba/druid -->
    <dependency>
        <groupId>com.alibaba</groupId>
        <artifactId>druid</artifactId>
        <version>1.1.22</version>
    </dependency>
</dependencies>

事务依赖图

创建数据库表

use `jdbc_template`;

create table `account` (
    `id`      int primary key auto_increment comment 'id',
    `name`    varchar(50) comment '姓名',
    `balance` int comment '余额'
);

insert into `account`(`name`, `balance`)
VALUES ('亚索', 1000),
       ('永恩', 1000);

编写实体类、DAO、Service 等

public class Account {
    private Integer id;
    private String name;
    private Integer balance;
    // 此处省略 getter setter toString 等方法
}
public interface AccountDao {
    /**
     * 增加金额
     *
     * @param id    用户id
     * @param money 金额数目
     */
    void addMoney(int id, int money);

    /**
     * 扣除金额
     *
     * @param id    用户id
     * @param money 金额数目
     */
    void deductMoney(int id, int money);
}
@Repository
public class AccountDaoImpl implements AccountDao {
    final JdbcTemplate jdbcTemplate;

    @Autowired
    public AccountDaoImpl(JdbcTemplate jdbcTemplate) {
        this.jdbcTemplate = jdbcTemplate;
    }

    @Override
    public void addMoney(int id, int money) {
        String sql = "UPDATE account SET balance=balance+? WHERE id=?";
        jdbcTemplate.update(sql, money, id);
    }

    @Override
    public void deductMoney(int id, int money) {
        String sql = "UPDATE account SET balance=balance-? WHERE id=?";
        jdbcTemplate.update(sql, money, id);
    }
}
public class AccountService {
    final AccountDao accountDao;

    @Autowired
    public AccountService(AccountDao accountDao) {
        this.accountDao = accountDao;
    }

    /**
     * 转账业务
     *
     * @param deductId 扣除金额方id
     * @param addId    增加金额方id
     * @param money    转账金额数目
     */
    public void transferAccount(int deductId, int addId, int money) {
        accountDao.deductMoney(deductId, money);
        accountDao.addMoney(addId, money);
    }
}

上面的代码,如果正常执行是没有问题的,但是如果代码执行过程中出现异常,会有问题

比如:

public void transferAccount(int deductId, int addId, int money) {
    accountDao.deductMoney(deductId, money);
    // 模拟异常
    int i = 10 / 0;
    accountDao.addMoney(addId, money);
}

为避免诸如上述问题,就需要事务。

事务操作流程:

  1. 开启事务
  2. 业务操作
  3. 若无异常,提交事务
  4. 出现异常,回滚事务

Spring 事务管理

  1. 事务一般添加到 Service 层

  2. Spring 进行事务管理有两种方式:编程式事务管理 和 声明式事务管理。一般都使用声明式事务管理。

  3. 声明式事务管理有两种方式:基于注解,基于 xml 配置文件。

  4. Spring 进行声明式事务管理,底层使用 AOP 原理

  5. Spring 事务管理 API

    提供 PlatformTransactionManager 接口,代表事务管理器,这个接口针对不同框架提供不同的实现类

    PlatformTransactionManager接口继承树

基于注解的声明式事务管理

配置事务管理器

<!--配置事务管理器-->
<bean id="transactionManager"
      class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
    <!--注入数据源-->
    <property name="dataSource" ref="dataSource"/>
</bean>

开启事务注解

<!--开启事务注解-->
<tx:annotation-driven transaction-manager="transactionManager"/>

上面讲到,事务一般添加到 Service 层

在 Service 类上(或者类中的方法上)添加事务注解 @Transactional

事务注解 @Transactional 既可以添加到类上,也可以添加到方法上

  • 如果添加到类上,则该类中的所有方法都会添加事务
  • 如果添加到方法上,则只会给该方法添加事务
@Service
@Transactional
public class AccountService {...}

事务注解 @Transactional 部分参数:

  • propagation :事务传播类型(默认 REQUIRED

    多事务方法直接进行调用,这个过程中事务是如何进行管理的

  • isolation :事务隔离级别

    事务特性之一。不考虑隔离性会产生很多问题,三个读问题:脏读、不可重复读、幻读(虚读)

    • 脏读:一个未提交事务读取到另一个未提交事务的数据
    • 不可重复读:一个未提交事务读取到另一个提交事务修改数据
    • 幻读:一个未提交事务读取到另一个提交事务添加数据
  • timeout :事务超时时间(秒,默认 -1 即永不超时)

    事务需要在指定时间内进行提交,如果不提交就会回滚

  • readOnly :事务是否只读(默认 false

  • rollbackFor :指明哪些异常类型进行事务回滚

  • noRollbackFor :指明哪些异常类型不进行事务回滚

事务方法:对数据库表数据进行变化的操作

事务的传播行为可以由传播类型指定,Spring 定义了 7 种传播类型

传播类型 描述
REQUIRED 如果有事务在运行,当前的方法就在这个事务内运行;否则,就启动一个新的事务,并在自己的事务内运行
REQUIRES_NEW 当前的方法必须启动新事务,并在它自己的事务内运行;如果有事务正在运行,应该将它挂起
SUPPORTS 如果有事务在运行,当前的方法就在这个事务内运行,否则它可以不运行在事务中
NOT_SUPPORTED 当前的方法不应该运行在事务中,如果有运行的事务,将它挂起
MANDATORY 当前的方法必须运行在事务内部,如果没有正在运行的事务,就抛出异常
NEVER 当前的方法不应该运行在事务中,如果有运行的事务,就抛出异常
NESTED 如果有事务在运行,当前的方法就应该在这个事务的嵌套事务内运行;否则,就启动一个新的事务,并在它自己的事务内运行

事务隔离级别

隔离级别 脏读 不可重复读 幻读
READ_UNCOMMITTED(读未提交)
READ_COMMITTED(读已提交)
REPEATABLE_READ(可重复读,MySQL默认)
SERIALIZABLE(串行化)

基于 XML 的声明式事务管理

第一步,配置事务管理器;第二步,配置通知;第三步,配置切入点和切面

<!--基于 XML 的事务-->
<!--1.配置事务管理器-->
<bean id="transactionManager2"
      class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
    <!--注入数据源-->
    <property name="dataSource" ref="dataSource2"/>
</bean>

<!--2.配置通知-->
<tx:advice id="txAdvice">
    <!--配置事务参数-->
    <tx:attributes>
        <!--指定哪种规则的方法上添加事务-->
        <tx:method name="transferAccount" propagation="REQUIRED"
                   isolation="REPEATABLE_READ"/>
    </tx:attributes>
</tx:advice>

<!--3.配置切入点和切面-->
<aop:config>
    <!--配置切入点-->
    <aop:pointcut id="myPointCut"
                  expression="execution(* com.yin.spring5.service.AccountService.*(..))"/>
    <!--配置切面-->
    <aop:advisor advice-ref="txAdvice" pointcut-ref="myPointCut"/>
</aop:config>

完全注解

创建配置类,代替 xml 配置文件

@Configuration    // 配置类
@ComponentScan(basePackages = "com.yin.spring5")    // 组件扫描
@EnableTransactionManagement    // 开启事务
public class TxConfig {
    @Bean
    public DataSource dataSource() {
        DruidDataSource dataSource = new DruidDataSource();
        dataSource.setDriverClassName("com.mysql.cj.jdbc.Driver");
        dataSource.setUrl("jdbc:mysql://localhost:3306/jdbc_template" +
                "?serverTimezone=Asia/Shanghai");
        dataSource.setUsername("root");
        dataSource.setPassword("123456");
        return dataSource;
    }

    @Bean
    public JdbcTemplate jdbcTemplate(DataSource dataSource) {
        JdbcTemplate jdbcTemplate = new JdbcTemplate();
        jdbcTemplate.setDataSource(dataSource);
        return jdbcTemplate;
    }

    @Bean
    public DataSourceTransactionManager transactionManager(DataSource dataSource) {
        DataSourceTransactionManager transactionManager =
                new DataSourceTransactionManager();
        transactionManager.setDataSource(dataSource);
        return transactionManager;
    }
}

Spring 5 新功能

  1. Spring 5 基于 Java 8,运行时兼容 Java 9,移除了不建议使用的类和方法。

  2. Spring 5 自带了通用的日志封装

    Spring 5 移除了 Log4jConfigListener,官方建议使用 Log4j2

  3. Spring 5 核心注解支持 @Nullable 注解

    @Nullable 注解可以用在方法上、属性上、参数上,表示方法返回值、属性、参数可以为空

  4. 支持函数式风格 GenericApplicationContext / AnnotationConfigApplicationContext

    @Test
    public void test4() {
        GenericApplicationContext context = new GenericApplicationContext();
        context.refresh();
        // 注册 bean
        context.registerBean("acct", Account.class, Account::new);
        // 获取注册的对象
        Account acct = context.getBean("acct", Account.class);
        System.out.println(acct);
    }
    
  5. Spring 5 支持整合 JUnit 5

Spring 5 整合 Log4j2

  1. 引入依赖
  2. 创建 log4j2.xml 配置文件(文件名固定)

Spring WebFlux

Spring WebFlux 介绍

Spring WebFlux 是 Spring 5 新增的模块,用于 Web 开发,功能与 Spring MVC 类似,使用当前比较流行的响应式编程

Spring 5 模块图

使用传统 Web 框架,比如 Spring MVC,这些基于 Servlet 容器。WebFlux 是一种异步非阻塞式的框架,异步非阻塞的框架在 Servlet 3.1 以后才支持,核心是基于 Reactor 的相关 API 实现的。

什么是异步非阻塞?

  • 同步与异步

    异步和同步针对调用者。调用者发送请求,如果等着对方回应之后才去左其他事就是同步;如果发送请求后不等着对方回应就去做其他事就是异步。

  • 阻塞与非阻塞

    阻塞和非阻塞针对被调用者。被调用者收到请求之后,做完请求任务之后才给出反馈就是阻塞,收到请求之后马上给出反馈然后再去做事情就是非阻塞。

WebFlux 特点

  1. 非阻塞式:在有限资源下,提高系统吞吐量和伸缩性,以 Reactor 为基础实现响应式编程
  2. 函数式编程:使用 Java 8 函数式编程方式实现路由请求

对比 Spring MVC

  1. 两种方式都可以使用注解方式,都运行在 Tomcat 等容器中
  2. SpringMVC 采用命令式编程,WebFlux 采用异步响应式编程

SpringMVC对比WebFlux

响应式编程

什么是响应式编程?

响应式编程是一种面向数据流和变化传播的编程范式。这意味着可以在编程语言中很方便地表达静态或动态的数据流,而相关的计算模型会自动将变化的值通过数据流进行传播。

例如,在命令式编程环境中,a=b+c 表示将表达式的结果赋给 a,而之后改变 b 或 c 的值不会影响 a。但在响应式编程中,a 的值会随着 b 或 c 的更新而更新。
电子表格程序就是响应式编程的一个例子。单元格可以包含字面值或类似 "=B1+C1" 的公式,而包含公式的单元格的值会依据其他单元格的值的变化而变化。

Java 8 及之前版本

提供的观察者模式两个类:Observer 接口,Observable 类(两者从 Java 9 开始被遗弃)

Java8及之前响应式

响应式编程(Reactor 实现)

  • 响应式编程操作中,Reactor 是满足 Reactive 规范框架
  • Reactor 有两个核心类,Mono 和 Flux,这两个类实现 Publisher 接口,提供丰富操作符。Flux 对象实现发布者,返回 N 个元素;Mono 实现发布者,返回 0 或 1 个元素
  • Flux 和 Mono 都是数据流的发布者,使用 Flux 和 Mono 都可以发出三种数据信号:元素值、错误信号、完成信号错误信号和完成信号都表示终止信号,终止信号用于告诉订阅者数据流结束了,错误信号终止数据流同时把错误信息传递给订阅者
  • 三种信号特点:
    • 错误信号和完成信号都是终止信号,二者不能共存
    • 如果没有发送任何元素值,而是直接发送终止信号,表示是空数据流
    • 如果没有终止信号,表示是无限数据流

代码演示 Flux 和 Mono

引入依赖

<!-- https://mvnrepository.com/artifact/io.projectreactor/reactor-core -->
<dependency>
    <groupId>io.projectreactor</groupId>
    <artifactId>reactor-core</artifactId>
    <version>3.4.0</version>
</dependency>

调用 just 方法或者其他方法只是声明数据流,数据流并没有发出,只有进行订阅之后才会触发数据流,不订阅什么都不会发生。

// just 方法直接声明
Flux.just(1, 2, 3, 4).subscribe(System.out::println);
Mono.just(5).subscribe(System.out::println);

// 其他的方法
Integer[] array = {1,2,3,4};
Flux.fromArray(array);
List<Integer> list = Arrays.asList(array);
Flux.fromIterable(list);
Stream<Integer> stream = list.stream();
Flux.fromStream(stream);

操作符

对数据流进行一道道操作,称为操作符,比如工厂流水线

  • map:将元素映射为新元素

    map操作符示意图

  • flatMap:将元素映射为流。把每个元素转换流,把转换之后多个流合并为大的流

    flatMap操作符示意图

WebFlux执行流程和核心 API

Spring WebFlux 基于 Reactor,默认使用容器是 Netty,Netty 是高性能 NIO(Non-blocking IO,非阻塞IO) 框架。(与 NIO 相对的是 BIO,即 Blocking IO)

BIO 的通信方式:

BIO的通信方式

NIO 的通信方式:

NIO的通信方式

Spring WebFlux 执行过程与 Spring MVC 相似

Spring WebFlux 核心控制器 DispatchHandler,实现接口 WebHandler

WebHandler接口

DispatcherHandler.handle 方法:

DispatcherHandler.handle方法

在 Spring WebFlux 中,DispatcherHandler 负责请求的处理

以下三个都是接口:

  • HandlerMapping:请求查询到处理的方法。

    Interface to be implemented by objects that define a mapping between requests and handler objects.

    由定义请求和处理程序对象之间的映射关系的对象实现的接口。

  • HandlerAdapter:真正负责请求处理。

    Contract that decouples the {@link DispatcherHandler} from the details of invoking a handler and makes it possible to support any handler type.

    使 DispatcherHandler 与调用处理程序的详细信息分离的契约,并且可以支持任何处理程序类型。

  • HandlerResultHandler:响应结果处理。

    Process the {@link HandlerResult}, usually returned by an {@link HandlerAdapter}.

    处理 HandlerResult,通常由 HandlerAdapter 返回。

Spring WebFlux 实现函数式编程,两个接口:

  • RouterFunction :Represents a function that routes to a {@linkplain HandlerFunction handler function}.
  • HandlerFunction :Represents a function that handles a {@linkplain ServerRequest request}.

基于注解编程模型

使用注解方式,与之前 Spring MVC 使用相似,只需要引入相关依赖,Spring Boot 自动配置相关运行容器,默认情况下使用 Netty 服务器。

引入依赖

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-webflux</artifactId>
</dependency>

编写代码

public interface UserService {
    /**
     * 根据 id 查询 User
     *
     * @param id 要查询的 id
     * @return 查询的结果
     */
    Mono<User> getUserById(int id);

    /**
     * 查询所有 User
     *
     * @return 查询的结果
     */
    Flux<User> getAllUsers();

    /**
     * 添加 User
     *
     * @param user 要添加的 User
     * @return 。
     */
    Mono<Void> saveUser(Mono<User> user);
}
@Service
public class UserServiceImpl implements UserService {
    // 为了方便,这里使用 Map 代替数据库存储数据
    private final Map<Integer, User> map = new HashMap<>();

    public UserServiceImpl() {
        this.map.put(1, new User(1, "Alice", 'F', 18));
        this.map.put(2, new User(2, "Bella", 'F', 20));
        this.map.put(3, new User(3, "Cindy", 'F', 18));
        this.map.put(4, new User(4, "Diana", 'F', 20));
        this.map.put(5, new User(5, "Emily", 'F', 18));
    }

    @Override
    public Mono<User> getUserById(int id) {
        return Mono.justOrEmpty(this.map.get(id));
    }

    @Override
    public Flux<User> getAllUsers() {
        return Flux.fromIterable(this.map.values());
    }

    @Override
    public Mono<Void> saveUser(Mono<User> userMono) {
        return userMono.doOnNext(user -> map.put(user.getId(), user))
                .thenEmpty(Mono.empty());
    }
}
@RestController
public class UserController {
    UserService userService;

    @Autowired
    public void setUserService(UserService userService) {
        this.userService = userService;
    }

    @GetMapping("/user/{id}")
    public Mono<User> getUserById(@PathVariable("id") int id) {
        return userService.getUserById(id);
    }

    @GetMapping("/users")
    public Flux<User> getAllUsers() {
        return userService.getAllUsers();
    }

    @PostMapping("/user")
    public Mono<Void> saveUser(User user) {
        Mono<User> userMono = Mono.just(user);
        return userService.saveUser(userMono);
    }
}
  • Spring MVC 方式,同步阻塞的方式,基于 Spring MVC + Servlet + Tomcat;
  • Spring WebFlux 方式,异步非阻塞的方式,基于 Spring WebFlux + Reactor + Netty。

基于函数式编程模型

  • 使用函数式编程模型,需要自己初始化服务器
  • 两个核心接口:RouterFunction(实现路由功能,请求转发给对应的 handler)和 HandlerFunction(处理请求生成响应的函数)。核心任务定义两个函数式接口的实现并且启动需要的服务器。
  • Spring WebFlux 的请求和响应不再是 ServletRequest 和 ServletResponse,而是 ServerRequest 和 ServerResponse。
  1. 把基于注解的复制一份,删去 controller 部分

  2. 创建 Handler

    public class UserHandler {
        private final UserService userService;
    
        public UserHandler(UserService userService) {
            this.userService = userService;
        }
    
        public Mono<ServerResponse> getUserById(ServerRequest request) {
            // 获取 id 值
            int id = Integer.parseInt(request.pathVariable("id"));
            // 空值处理
            Mono<ServerResponse> notFound = ServerResponse.notFound().build();
            // 调用 service 得到数据
            Mono<User> userMono = userService.getUserById(id);
            // 把 userMono 进行转换返回,使用 Reactor 操作符 flatMap
            return userMono.flatMap(user -> ServerResponse.ok()
                    .contentType(MediaType.APPLICATION_JSON)
                    .body(BodyInserters.fromValue(user)))
                    .switchIfEmpty(notFound);
        }
    
        public Mono<ServerResponse> getAllUsers() {
            Flux<User> allUsers = userService.getAllUsers();
            return ServerResponse.ok().contentType(MediaType.APPLICATION_JSON)
                    .body(allUsers, User.class);
        }
    
        public Mono<ServerResponse> saveUser(ServerRequest request) {
            Mono<User> userMono = request.bodyToMono(User.class);
            return ServerResponse.ok().build(userService.saveUser(userMono));
        }
    }
    
  3. 初始化服务器,编写 Router

    • 创建路由的方法
    • 创建服务器完成适配
    • 最终调用
    public class Server {
        public static void main(String[] args) throws IOException {
            Server server = new Server();
            server.createReactorServer();
            System.out.println("enter to exit");
            System.in.read();
        }
    
        // 1. 创建 Router 路由
        public RouterFunction<ServerResponse> routingFunction() {
            // 创建 handler 对象
            UserService userService = new UserServiceImpl();
            UserHandler userHandler = new UserHandler(userService);
            // 设置路由
            return RouterFunctions.route(GET("/user/{id}").and(accept(MediaType.APPLICATION_JSON)), userHandler::getUserById)
                    .andRoute(GET("/users").and(accept(MediaType.APPLICATION_JSON)),
                            userHandler::getAllUsers);
        }
    
        // 2. 创建服务器完成适配
        public void createReactorServer() {
            // 路由和 handler 适配
            RouterFunction<ServerResponse> route = routingFunction();
            HttpHandler httpHandler = toHttpHandler(route);
            ReactorHttpHandlerAdapter handlerAdapter =
                    new ReactorHttpHandlerAdapter(httpHandler);
            // 创建服务器
            HttpServer httpServer = HttpServer.create();
            httpServer.handle(handlerAdapter).bindNow();
        }
    }
    
  4. 使用 WebClient 调用

    public class Client {
        public static void main(String[] args) {
            WebClient webClient = WebClient.create("http://localhost:8235");
    
            User user = webClient.get().uri("/user/{id}", 1)
                    .accept(MediaType.APPLICATION_JSON).retrieve().bodyToMono(User.class)
                    .block();
            System.out.println(user);
    
            Flux<User> users = webClient.get().uri("/users")
                    .accept(MediaType.APPLICATION_JSON).retrieve().bodyToFlux(User.class);
            users.map(User::toString).buffer().doOnNext(System.out::println).blockFirst();
        }
    }
    

课程总结

  1. Spring 框架概述

    轻量级开源 JavaEE 框架,核心 IoC 和 AOP

  2. IoC 容器

    1. IoC 底层原理(工厂、反射等)
    2. IoC 接口(BeanFactory)
    3. IoC 操作 Bean 管理(基于 xml)
    4. IoC 操作 Bean 管理(基于注解)
  3. AOP

    1. AOP 底层原理:动态代理,有接口(JDK 动态代理),没有接口(CGLIB 动态代理)
    2. 术语:切入点、增强(通知)、切面
    3. 基于 AspectJ 实现 AOP 操作
  4. JdbcTemplate

    1. 使用 JdbcTemplate 实现 CRUD 操作
    2. 使用 JdbcTemplate 实现批量操作
  5. 事务管理

    1. 事务概念
    2. 重要概念(传播行为、隔离级别)
    3. 基于注解实现声明式事务管理
    4. 完全注解方式
  6. Spring 5 新特性

    1. 整合日志框架
    2. @Nullable 注解
    3. 函数式注册对象
    4. 整合 JUnit 5 单元测试框架
    5. Spring WebFlux
posted @ 2021-01-28 17:30  江南笑书生  阅读(73)  评论(0编辑  收藏  举报