首页 > 其他分享 >Spring

Spring

时间:2022-11-16 00:34:10浏览次数:80  
标签:String Spring void class org public

README

1、官网

2、Spring5模块

笔记主要内容目前涵盖

  • 核心容器:BeansCoreContextExpression

  • 数据访问集成:JDBCORMTransactions

  • 其他:AOPAspectsTest

3、相关依赖

核心

  • spring-beans

  • spring-core

  • spring-context

  • spring-expression

  • commons-logging

AOP 相关

  • spring-aop

  • aspectjrt

  • aspectjweaver

JdbcTemplate 相关

  • druid

  • mysql-connector-java

  • spring-jdbc

  • spring-orm

  • spring-tx

日志相关

  • log4j-api

  • log4j-core

  • log4j-slf4j-impl

  • slf4j-api

Junit 相关

  • spring-test
<!--核心-->
<dependency>
    <groupId>org.springframework</groupId>
    <artifactId>spring-beans</artifactId>
    <version>5.3.16</version>
</dependency>
<dependency>
    <groupId>org.springframework</groupId>
    <artifactId>spring-core</artifactId>
    <version>5.3.16</version>
</dependency>
<dependency>
    <groupId>org.springframework</groupId>
    <artifactId>spring-context</artifactId>
    <version>5.3.16</version>
</dependency>
<dependency>
    <groupId>org.springframework</groupId>
    <artifactId>spring-expression</artifactId>
    <version>5.3.16</version>
</dependency>
<dependency>
    <groupId>commons-logging</groupId>
    <artifactId>commons-logging</artifactId>
    <version>1.2</version>
</dependency>
<!--AOP 相关-->
<dependency>
    <groupId>org.springframework</groupId>
    <artifactId>spring-aop</artifactId>
    <version>5.3.16</version>
</dependency>
<dependency>
    <groupId>org.aspectj</groupId>
    <artifactId>aspectjrt</artifactId>
    <version>1.9.8</version>
    <scope>runtime</scope>
</dependency>
<dependency>
    <groupId>org.aspectj</groupId>
    <artifactId>aspectjweaver</artifactId>
    <version>1.9.8</version>
    <scope>runtime</scope>
</dependency>
<!--JdbcTemplate 相关-->
<dependency>
    <groupId>com.alibaba</groupId>
    <artifactId>druid</artifactId>
    <version>1.2.8</version>
</dependency>
<dependency>
    <groupId>mysql</groupId>
    <artifactId>mysql-connector-java</artifactId>
    <version>8.0.28</version>
</dependency>
<dependency>
    <groupId>org.springframework</groupId>
    <artifactId>spring-jdbc</artifactId>
    <version>5.3.16</version>
</dependency>
<dependency>
    <groupId>org.springframework</groupId>
    <artifactId>spring-orm</artifactId>
    <version>5.3.16</version>
</dependency>
<dependency>
    <groupId>org.springframework</groupId>
    <artifactId>spring-tx</artifactId>
    <version>5.3.16</version>
</dependency>
<!--日志相关-->
<dependency>
    <groupId>org.apache.logging.log4j</groupId>
    <artifactId>log4j-api</artifactId>
    <version>2.17.2</version>
</dependency>
<dependency>
    <groupId>org.apache.logging.log4j</groupId>
    <artifactId>log4j-core</artifactId>
    <version>2.17.2</version>
</dependency>
<dependency>
    <groupId>org.apache.logging.log4j</groupId>
    <artifactId>log4j-slf4j-impl</artifactId>
    <version>2.17.2</version>
    <scope>test</scope>
</dependency>
<dependency>
    <groupId>org.slf4j</groupId>
    <artifactId>slf4j-api</artifactId>
    <version>1.7.36</version>
</dependency>
<!--Junit 相关-->
<dependency>
    <groupId>org.springframework</groupId>
    <artifactId>spring-test</artifactId>
    <version>5.3.16</version>
    <scope>test</scope>
</dependency>

4、导图

00-Spring简介

01-IOC容器

02-AOP

03-JdbcTemplate与声明式事务

04-Spring5新功能

01、Spring简介

1、Spring 课程内容介绍

  • 1)Spring 概念

  • 2)IOC 容器

  • 3)AOP

  • 4)JdbcTemplate

  • 5)事务管理

  • 6)Spring5 新特性

2、Spring 框架概述

Spring 是轻量级的开源的 J2EE 框架,可以解决企业应用开发的复杂性

Spring 有两个核心部分:IOC 和 AOP

  • IOC:控制反转,把创建对象过程交给 Spring 进行管理

  • AOP:面向切面,不修改源代码进行功能增强

Spring 特点

  • 1)方便解耦,简化开发

  • 2)支持 AOP 编程

  • 3)方便程序测试

  • 4)方便整合其他框架

  • 5)方便进行事务操作

  • 6)降低 API 开发难度

在课程中选取 Spring 版本 5.x 讲解

3、Spring 入门案例

1)下载 Spring5

  • 查看 Spring 官网 提供的 Spring 发布版本,这里使用 Spring 最新的稳定版本 5.3.15

  • GA(General Availability,普遍可用)为稳定版本

  • SNAPSHOT 为快照版本,不稳定

  • 进入后,找到Downloading a Distribution,点击 https://repo.spring.io 进入

  • 左侧选择Artifactory-Artifacts,右侧选择release-com-org-springframework-spring

  • 复制右侧地址或直接点击打开:JFrog,找到所需版本点击进入

  • 点击Download Link一栏链接,即可进行下载(网络问题,可能很慢)

  • 下载完毕,进行解压

2)创建普通 Java 工程

打开 IDEA 工具,点击File-New-Project

选择Java,创建一个普通工程

勾选Create project from template

填写Project nameProject locationBase package

3)导入 Spring5 相关 jar 包

通过下载解压的包中,提供了很多jar包,但并不需要所有都引入

  • *-5.3.15.jar:编译包(正是我们需要的)

  • *-5.3.15-javadoc.jar:文档包

  • *-5.3.15-sources.jar:源码包

我们再看下 Spring5 模块

其中的Core Container核心模块有

  • Beans

  • Core核心包

  • Context上下文

  • Expression表达式

我们目前导入这四个核心模块的包即可

  • spring-beans-5.3.15.jar

  • spring-core-5.3.15.jar

  • spring-context-5.3.15.jar

  • spring-expression-5.3.15.jar

  • commons-logging-1.2.jar(不是 Spring 的包,但有依赖关系,不引入会报错)

在工程中新建一个lib文件夹,存放这些包

将这些jar包导入项目中

选中lib下的jar

选中后效果,最后点击OK即可

4)创建普通类和方法

public class User {
    public void add(){
        System.out.println("Hello World: User.add()方法");
    }
}

5)创建 Spring 配置文件,配置创建的对象

在src上点击New-XML Configuration File-Spring Config

创建xml配置文件

创建成功的xml文件已经有了基本的根标签

接下来,配置相关对象的即可

<!--配置User对象-->
<bean id="user" class="com.vectorx.spring5.User"></bean>

6)进行测试代码的编写

@Test
public void testAdd() {
    // 1、加载自定义的Spring配置文件
    ApplicationContext context = new ClassPathXmlApplicationContext("bean1.xml");

    // 2、获取配置的User对象
    User user = context.getBean("user", User.class);

    // 3、操作User对象
    System.out.println(user);
    user.add();
}

测试结果如下

02、IOC 容器

1、IOC 的概念原理

1.1、IOC 是什么?

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

    • 2)使用 IOC 的目的:降低耦合性

1.2、IOC 底层实现

  • 1)xml 解析

  • 2)工厂模式

  • 3)反射技术

1.3、图解 IOC 原理

*原始方法 *

代码示例

public class UserDao {
    public void add(){
        // ...
    }
}
public class UserService {
    public void execute(){
        UserDao dao = new UserDao();
        dao.add();
    }
}

工厂模式

代码示例

public class UserDao {
    public void add(){
        // ...
    }
}
public class UserFactory{
    public static UserDao getDao(){
        return new UserDao();
    }
}
public class UserService {
    public void execute(){
        UserDao dao = UserFactory.getDao();
        dao.add();
    }
}

IOC 过程

代码示例

public class UserFactory{
    public static UserDao getDao(){
        // 1、xml解析
        String classValue = class属性值;
        // 2、通过反射创建对象
        Class clazz = Class.forName(classValue);
        return (UserDao)clazz.newInstance();
    }
}

2、IOC 接口

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

2)Spring 提供了IOC 容器实现的两种方式(即两个接口)

  • BeanFactory

  • IOC 容器基本实现,是 Spring 的内部接口,不提供给开发人员使用

  • 加载配置文件时不会创建对象,使用对象时才会创建对象

  • ApplicationContext

  • BeanFactory的子接口,提供更多功能,提供给开发人员使用

  • 加载配置文件就创建对象

2.1、ApplicationContext

ApplicationContext接口的实现类

  • FileSystemXmlApplicationContext:xml 配置文件在系统盘中的文件全路径名
new FileSystemXmlApplicationContext("D:\workspace\NOTE_Spring\Spring5_HelloWorld\src\bean1.xml");
  • ClassPathXmlApplicationContext:xml 配置文件在项目 src 下的相对路径名
new ClassPathXmlApplicationContext("bean1.xml");

2.2、BeanFactory

BeanFactory接口的子接口和实现类

  • ConfigurableApplicationContext:包含一些相关的扩展功能

3、IOC 操作 Bean 管理

3.1、Bean 管理是什么

Bean 管理指的是两个操作

  • 1)Spring 创建对象

  • 2)Spring 注入属性

public class User{
    private String userName;

    public void setUserName(String userName){
        this.userName = userName;
    }
}

3.2、Bean 管理实现方式

  • 1)基于 XML 配置文件方式实现

  • 2)基于注解方式实现

4、基于 XML 方式

4.1、创建对象

<!--配置User对象-->
<bean id="user" class="com.vectorx.spring5.User"></bean>

1)在 Spring 配置文件中,使用bean标签,标签里添加对应属性,就可以实现对象的创建

2)bean标签中有很多属性,介绍常用的属性

  • id属性:唯一标识

  • class属性:类全限定名、类全路径

  • name属性:了解即可,早期为Struts框架服务,现已“退役”,作用跟id属性一样

  • 其他属性:后面再做介绍...

3)创建对象时,默认执行无参构造方法

如果只提供一个有参构造方法,如下

public class User {
    private String userName;

    public User(String userName) {
        this.userName = userName;
    }

    // ...
}

仍然按照之前获取 User 对象创建方式,即

// 1、加载自定义的Spring配置文件
ApplicationContext context = new ClassPathXmlApplicationContext("bean1.xml");
// 2、获取配置的User对象
User user = context.getBean("user", User.class);

则会报错

警告: Exception encountered during context initialization - cancelling refresh attempt: org.springframework.beans.factory.BeanCreationException: Error creating bean with name 'user' defined in class path resource [bean1.xml]: Instantiation of bean failed; nested exception is org.springframework.beans.BeanInstantiationException: Failed to instantiate [com.vectorx.spring5.User]: No default constructor found; nested exception is java.lang.NoSuchMethodException: com.vectorx.spring5.User.<init>()

org.springframework.beans.factory.BeanCreationException: Error creating bean with name 'user' defined in class path resource [bean1.xml]: Instantiation of bean failed; nested exception is org.springframework.beans.BeanInstantiationException: Failed to instantiate [com.vectorx.spring5.User]: No default constructor found; nested exception is java.lang.NoSuchMethodException: com.vectorx.spring5.User.<init>()

    at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.instantiateBean(AbstractAutowireCapableBeanFactory.java:1334)
    at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.createBeanInstance(AbstractAutowireCapableBeanFactory.java:1232)
    at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.doCreateBean(AbstractAutowireCapableBeanFactory.java:582)
    at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.createBean(AbstractAutowireCapableBeanFactory.java:542)
    at org.springframework.beans.factory.support.AbstractBeanFactory.lambda$doGetBean$0(AbstractBeanFactory.java:335)
    at org.springframework.beans.factory.support.DefaultSingletonBeanRegistry.getSingleton(DefaultSingletonBeanRegistry.java:234)
    at org.springframework.beans.factory.support.AbstractBeanFactory.doGetBean(AbstractBeanFactory.java:333)
    at org.springframework.beans.factory.support.AbstractBeanFactory.getBean(AbstractBeanFactory.java:208)
    at org.springframework.beans.factory.support.DefaultListableBeanFactory.preInstantiateSingletons(DefaultListableBeanFactory.java:953)
    at org.springframework.context.support.AbstractApplicationContext.finishBeanFactoryInitialization(AbstractApplicationContext.java:918)
    at org.springframework.context.support.AbstractApplicationContext.refresh(AbstractApplicationContext.java:583)
    at org.springframework.context.support.ClassPathXmlApplicationContext.<init>(ClassPathXmlApplicationContext.java:144)
    at org.springframework.context.support.ClassPathXmlApplicationContext.<init>(ClassPathXmlApplicationContext.java:85)
    at com.vectorx.spring5.testdemo.TestSpring5.testAdd(TestSpring5.java:13)
    at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
    at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)
    at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
    at java.lang.reflect.Method.invoke(Method.java:498)
    at org.junit.runners.model.FrameworkMethod$1.runReflectiveCall(FrameworkMethod.java:59)
    at org.junit.internal.runners.model.ReflectiveCallable.run(ReflectiveCallable.java:12)
    at org.junit.runners.model.FrameworkMethod.invokeExplosively(FrameworkMethod.java:56)
    at org.junit.internal.runners.statements.InvokeMethod.evaluate(InvokeMethod.java:17)
    at org.junit.runners.ParentRunner$3.evaluate(ParentRunner.java:306)
    at org.junit.runners.BlockJUnit4ClassRunner$1.evaluate(BlockJUnit4ClassRunner.java:100)
    at org.junit.runners.ParentRunner.runLeaf(ParentRunner.java:366)
    at org.junit.runners.BlockJUnit4ClassRunner.runChild(BlockJUnit4ClassRunner.java:103)
    at org.junit.runners.BlockJUnit4ClassRunner.runChild(BlockJUnit4ClassRunner.java:63)
    at org.junit.runners.ParentRunner$4.run(ParentRunner.java:331)
    at org.junit.runners.ParentRunner$1.schedule(ParentRunner.java:79)
    at org.junit.runners.ParentRunner.runChildren(ParentRunner.java:329)
    at org.junit.runners.ParentRunner.access$100(ParentRunner.java:66)
    at org.junit.runners.ParentRunner$2.evaluate(ParentRunner.java:293)
    at org.junit.runners.ParentRunner$3.evaluate(ParentRunner.java:306)
    at org.junit.runners.ParentRunner.run(ParentRunner.java:413)
    at org.junit.runner.JUnitCore.run(JUnitCore.java:137)
    at com.intellij.junit4.JUnit4IdeaTestRunner.startRunnerWithArgs(JUnit4IdeaTestRunner.java:69)
    at com.intellij.rt.junit.IdeaTestRunner$Repeater.startRunnerWithArgs(IdeaTestRunner.java:33)
    at com.intellij.rt.junit.JUnitStarter.prepareStreamsAndStart(JUnitStarter.java:221)
    at com.intellij.rt.junit.JUnitStarter.main(JUnitStarter.java:54)
Caused by: org.springframework.beans.BeanInstantiationException: Failed to instantiate [com.vectorx.spring5.User]: No default constructor found; nested exception is java.lang.NoSuchMethodException: com.vectorx.spring5.User.<init>()
    at org.springframework.beans.factory.support.SimpleInstantiationStrategy.instantiate(SimpleInstantiationStrategy.java:83)
    at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.instantiateBean(AbstractAutowireCapableBeanFactory.java:1326)
    ... 38 more
Caused by: java.lang.NoSuchMethodException: com.vectorx.spring5.User.<init>()
    at java.lang.Class.getConstructor0(Class.java:3082)
    at java.lang.Class.getDeclaredConstructor(Class.java:2178)
    at org.springframework.beans.factory.support.SimpleInstantiationStrategy.instantiate(SimpleInstantiationStrategy.java:78)
    ... 39 more

其中主要报错信息

Failed to instantiate [com.vectorx.spring5.User]: No default constructor found
...
Caused by: java.lang.NoSuchMethodException: com.vectorx.spring5.User.<init>()

就是说:初始化 User 对象失败,因为没有找到默认构造,所以抛出了一个NoSuchMethodException异常,即 User 中没有<init>()方法

4.2、注入属性

DI:依赖注入,就是注入属性(但需要在创建对象基础上进行)

IOCDI的区别:DIIOC的一种具体实现

两种注入方式:Setter 方式、有参构造方法

  • 第一种注入方式:通过 Setter 方式进行注入
public class Book{
    private String bname;

    // Setter 方法注入
    public void setBname(String bname){
        this.bname = bname;
    }

    public static void main(String[] args){
        Book book = new Book();
        book.setBname("book1");
    }
}
  • 第二种注入方式:通过有参构造方法进行注入
public class Book{
    private String bname;

    // 有参构造注入
    public Book(String bname){
        this.bname = bname;
    }

    public static void main(String[] args){
        Book book = new Book("book1");
    }
}

1)通过 Setter 方式注入

① 创建类,定义属性和 Setter 方法

public class Book {
    private String bname;
    private String bauthor;

    public void setBname(String bname) {
        this.bname = bname;
    }
    public void setBauthor(String bauthor) {
        this.bauthor = bauthor;
    }
}

② 在 Spring 配置文件中配置对象创建,配置属性注入

<!-- 2、Setter方法注入属性 -->
<bean id="book" class="com.vectorx.spring5.s1_xml.setter.Book">
    <!-- 使用property完成属性注入
            name: 类中属性名称
            value: 向属性中注入的值
    -->
    <property name="bname" value="Spring实战 第5版"></property>
    <property name="bauthor" value="克雷格·沃斯(Craig Walls)"></property>
</bean>

2)通过有参构造注入

① 创建类,定义属性,创建属性对应有参构造方法

public class Orders {
    private String oname;
    private String address;

    public Orders(String oname, String address) {
        this.oname = oname;
        this.address = address;
    }
}

② 在 Spring 配置文件中配置对象创建,配置有参构造注入

<!-- 3、有参构造注入属性 -->
<bean id="order" class="com.vectorx.spring5.s2_xml.constructor.Orders">
    <constructor-arg name="oname" value="Spring微服务实战"></constructor-arg>
    <constructor-arg name="address" value="[美]约翰·卡内尔(John Carnell)"></constructor-arg>
</bean>

或者使用index属性代替name属性,索引值大小是几就表示有参构造中的第几个参数(索引从0开始)

<!-- 3、有参构造注入属性 -->
<bean id="order" class="com.vectorx.spring5.s2_xml.constructor.Orders">
    <constructor-arg index="0" value="冰墩墩"></constructor-arg>
    <constructor-arg index="1" value="Peking"></constructor-arg>
</bean>

3)p 名称空间注入(了解)

① 基于 p 名称空间注入,可以简化基于 xml 的配置方式

bean1.xml配置文件中添加 p 名称空间:xmlns:p="http://www.springframework.org/schema/p",如下

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

bean标签中进行属性注入操作

<!-- 4、p名称空间注入属性 -->
<bean id="book1" class="com.vectorx.spring5.s1_xml.setter.Book" p:bname="SpringBoot实战" p:bauthor="[美]克雷格·沃斯"></bean>

需要注意的是:p 名称空间只能进行属性注入

4.3、注入其他类型属性

1)字面量

  • null值:使用<null>
<bean id="book2" class="com.vectorx.spring5.s1_xml.setter.Book">
    <property name="bname" value="Spring实战 第5版"></property>
    <property name="bauthor">
        <null></null>
    </property>
</bean>
  • 属性值包含特殊符号:有两种方式

    • 使用转义字符,如&lt;&gt;标识<>
<!-- 字面量:property方式注入含有特殊符号的属性 -->
<bean id="book3" class="com.vectorx.spring5.s1_xml.setter.Book">
    <property name="bname" value="Spring实战 第5版"></property>
    <property name="bauthor" value="<Test>"</property>
</bean>
  • 使用CDATA结构,如<![CDATA[<Test>]]>
<!-- 字面量:property方式注入含有特殊符号的属性 -->
<bean id="book4" class="com.vectorx.spring5.s1_xml.setter.Book">
    <property name="bname" value="Spring实战 第5版"></property>
    <property name="bauthor">
        <value><![CDATA[<Test>]]></value>
    </property>
</bean>

2)外部 Bean

① 创建两个类:Service类和Dao

public interface UserDao {
    void update();
}
public class UserDaoImpl implements UserDao{
    @Override
    public void update() {
        System.out.println("dao update...");
    }
}

② 在Service中调用Dao中的方法

public class UserService {
    private UserDao userDao;

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

    public void updateUser(){
        System.out.println("service update...");
        userDao.update();
    }
}

③ 在 Spring 配置文件中进行配置

<!--  1、配置service和dao创建  -->
<bean id="userService" class="com.vectorx.spring5.s3_xml.outerbean.service.UserService">
    <!-- 2、注入userDao对象
               name属性:类里面属性名称
               ref属性:创建userDao对象bean标签id值
       -->
    <property name="userDao" ref="userDaoImpl"></property>
</bean>
<bean id="userDaoImpl" class="com.vectorx.spring5.s3_xml.outerbean.dao.UserDaoImpl"></bean>

3)内部 Bean

① 一对多关系:部门和员工。部门里有多个员工,一个员工属于一个部门。部门是一的一方,员工是多的一方

② 在实体类之间表示一对多关系。在员工类中使用对象类型表示所属部门

public class Dept {
    private String dname;

    public String getDname() {
        return dname;
    }
    public void setDname(String dname) {
        this.dname = dname;
    }
}
public class Emp {
    private String ename;
    private String gender;
    private Dept dept;

    public void setDept(Dept dept) {
        this.dept = dept;
    }
    public String getEname() {
        return ename;
    }
    public void setEname(String ename) {
        this.ename = ename;
    }
    public String getGender() {
        return gender;
    }
    public void setGender(String gender) {
        this.gender = gender;
    }
}

③ 在 Spring 配置文件中进行配置

<bean id="emp1" class="com.vectorx.spring5.s4_xml.innerbean.Emp">
    <property name="ename" value="Lucy"></property>
    <property name="gender" value="female"></property>
    <property name="dept">
        <bean id="dept1" class="com.vectorx.spring5.s4_xml.innerbean.Dept">
            <property name="dname" value="Develop Department"></property>
        </bean>
    </property>
</bean>

4)级联赋值

第一种写法

<!-- 级联赋值 -->
<bean id="emp2" class="com.vectorx.spring5.s4_xml.innerbean.Emp">
    <property name="ename" value="Nancy"></property>
    <property name="gender" value="female"></property>
    <property name="dept" ref="dept2"></property>
</bean>
<bean id="dept2" class="com.vectorx.spring5.s4_xml.innerbean.Dept">
    <property name="dname" value="Sallery Department"></property>
</bean>

第二种写法<property name="dept.dname" value="Manage Department"></property>

<!-- 级联赋值 -->
<bean id="emp2" class="com.vectorx.spring5.s4_xml.innerbean.Emp">
    <property name="ename" value="Nancy"></property>
    <property name="gender" value="female"></property>
    <property name="dept" ref="dept2"></property>
    <property name="dept.dname" value="Manage Department"></property>
</bean>
<bean id="dept2" class="com.vectorx.spring5.s4_xml.innerbean.Dept">
    <property name="dname" value="Sallery Department"></property>
</bean>

这种写法可以对外部Bean的属性值进行覆盖,但前提是要有deptGetter方法

public Dept getDept() {
    return dept;
}

否则 XML 文件就会爆红

强行使用就会报如下错误

Caused by: org.springframework.beans.NotWritablePropertyException: Invalid property 'dept.dname' of bean class [com.vectorx.spring5.s4_xml.innerbean.Emp]: Nested property in path 'dept.dname' does not exist; nested exception is org.springframework.beans.NotReadablePropertyException: Invalid property 'dept' of bean class [com.vectorx.spring5.s4_xml.innerbean.Emp]: Bean property 'dept' is not readable or has an invalid getter method: Does the return type of the getter match the parameter type of the setter?

4.4、注入集合属性

  • 1)注入数组类型属性

  • 2)注入 List 集合类型属性

  • 3)注入 Map 集合类型属性

① 创建类,定义数组、list、map、set 类型属性,生成对应 setter 方法

public class Stu {
    private String[] arrs;
    private List<String> lists;
    private Map<String, String> maps;
    private Set<String> sets;
    public void setArrs(String[] arrs){
        this.arrs = arrs;
    }
    public void setLists(List<String> lists){
        this.lists = lists;
    }
    public void setMaps(Map<String, String> maps){
        this.maps = maps;
    }
    public void setSets(Set<String> sets){
        this.sets = sets;
    }
}

② 在 Spring 配置文件中进行配置

<!-- 集合类型属性注入 -->
<bean id="stu" class="com.vectorx.spring5.s5_xml.collection.Stu">
    <!--  1 数组属性注入  -->
    <property name="arrs">
        <array>
            <value>数组</value>
            <value>属性</value>
            <value>注入</value>
        </array>
    </property>
    <!--  2 list属性注入  -->
    <property name="lists">
        <list>
            <value>list</value>
            <value>属性</value>
            <value>注入</value>
        </list>
    </property>
    <!--  3 map属性注入  -->
    <property name="maps">
        <map>
            <entry key="k1" value="v1"></entry>
            <entry key="k2" value="v2"></entry>
            <entry key="k3" value="v3"></entry>
        </map>
    </property>
    <!--  4 set属性注入  -->
    <property name="sets">
        <set>
            <value>set</value>
            <value>属性</value>
            <value>注入</value>
        </set>
    </property>
</bean>
  • 4)注入 List 类型属性值,值为对象
<bean id="stu" class="com.vectorx.spring5.s5_xml.collection.Stu">
    <!--注入 List 类型属性值,值为对象-->
    <property name="lists2">
        <list>
            <ref bean="course1"></ref>
            <ref bean="course2"></ref>
            <ref bean="course3"></ref>
        </list>
    </property>
</bean>
<bean id="course1" class="com.vectorx.spring5.s5_xml.collection.Course">
    <property name="cname" value="c1"></property>
</bean>
<bean id="course2" class="com.vectorx.spring5.s5_xml.collection.Course">
    <property name="cname" value="c2"></property>
</bean>
<bean id="course3" class="com.vectorx.spring5.s5_xml.collection.Course">
    <property name="cname" value="c3"></property>
</bean>
  • 5)把集合注入部分提取出来

① 在 Spring 配置文件中引入util命名空间

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

② 使用util标签完成 list 集合注入提取

<!--提取属性注入-->
<util:list id="utilList">
    <value>111</value>
    <value>222</value>
    <value>333</value>
</util:list>
<!--提取属性注入使用-->
<bean id="stu2" class="com.vectorx.spring5.s5_xml.collection.Stu">
    <property name="lists" ref="utilList"></property>
</bean>

4.5、自动装配

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

XML 实现自动装配:bean标签中有个属性autowire,里面有两个常用的属性值

  • byName根据属性名称注入,要求注入值beanid值和类中属性名称一致

  • byType根据属性类型注入,要求注入值bean的类型在配置文件中只存在一份

1)根据属性名称进行自动注入

<bean id="emp" class="com.vectorx.spring5.s9_xml.autowire.Emp" autowire="byName"></bean>
<bean id="dept" class="com.vectorx.spring5.s9_xml.autowire.Dept"></bean>

2)根据属性类型进行自动注入

<bean id="emp" class="com.vectorx.spring5.s9_xml.autowire.Emp" autowire="byType"></bean>
<bean id="dept" class="com.vectorx.spring5.s9_xml.autowire.Dept"></bean>

4.6、外部属性文件

1、直接配置数据库信息

  • 1)引入Druid连接池依赖jar

  • 2)配置Druid连接池

<bean id="dataSource" class="com.alibaba.druid.pool.DruidDataSource">
    <property name="driverClassName" value="com.mysql.jdbc.Driver"></property>
    <property name="url" value="jdbc:mysql://localhost:3306"></property>
    <property name="username" value="root"></property>
    <property name="password" value="root"></property>
</bean>

2、引入外部属性文件配置数据库连接池

  • 1)创建properties格式的外部属性文件,配置数据库连接信息
mysql.driverClassName=com.mysql.jdbc.Driver
mysql.url=jdbc:mysql://localhost:3306
mysql.username=root
mysql.password=root
  • 2)将外部properties属性文件引入到 Spring 配置文件中
<!--引入context名称空间-->
<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xmlns:context="http://www.springframework.org/schema/context"
       xsi:schemaLocation="
                           http://www.springframework.org/schema/beans 
                           http://www.springframework.org/schema/beans/spring-beans.xsd
                           http://www.springframework.org/schema/context
                           http://www.springframework.org/schema/context/spring-context.xsd">
    <!--引入外部属性文件-->
    <context:property-placeholder location="classpath:jdbc.properties"/>
    <!--使用Spring表达式配置连接池-->
    <bean id="dataSource" class="com.alibaba.druid.pool.DruidDataSource">
        <property name="driverClassName" value="${mysql.driverClassName}"></property>
        <property name="url" value="${mysql.url}"></property>
        <property name="username" value="${mysql.username}"></property>
        <property name="password" value="${mysql.password}"></property>
    </bean>
</beans>

5、FactoryBean

Spring 有两种类型 Bean,一种是普通 Bean,另外一种是工厂 Bean(FactoryBean)

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

  • 工厂 Bean:在配置文件中定义的 Bean 类型可以和返回类型不一致

上述的例子都是普通 Bean 的类型,那么工厂 Bean 该怎么实现呢?

  • 1)创建类,实现 FactoryBean 接口,使其作为一个工厂 Bean

  • 2)实现接口中的方法,在实现方法中定义返回的 Bean 类型

public class MyFactoryBean implements FactoryBean<Course> {
    @Override
    public Course getObject() throws Exception {
        Course course = new Course();
        course.setCname("CourseName");
        return course;
    }

    @Override
    public Class<?> getObjectType() {
        return null;
    }
}
  • 3)在 Spring 配置文件中进行配置
<bean id="myFactoryBean" class="com.vectorx.spring5.s6_xml.factorybean.MyFactoryBean"></bean>

由于是 FactoryBean,所以再通过上下文获取时,需要使用实现 FactoryBean 时传入的泛型类型进行接收

ApplicationContext applicationContext = new ClassPathXmlApplicationContext("bean5.xml");
Course course = applicationContext.getBean("myFactoryBean", Course.class);

如果仍然使用配置文件中定义的 Bean 类型,则会报错

Exception in thread "main" org.springframework.beans.factory.BeanNotOfRequiredTypeException: Bean named 'myFactoryBean' is expected to be of type 'com.vectorx.spring5.s6_xml.factorybean.MyFactoryBean' but was actually of type 'com.vectorx.spring5.s6_xml.factorybean.Course'
    at org.springframework.beans.factory.support.AbstractBeanFactory.adaptBeanInstance(AbstractBeanFactory.java:417)
    at org.springframework.beans.factory.support.AbstractBeanFactory.doGetBean(AbstractBeanFactory.java:398)
    at org.springframework.beans.factory.support.AbstractBeanFactory.getBean(AbstractBeanFactory.java:213)
    at org.springframework.context.support.AbstractApplicationContext.getBean(AbstractApplicationContext.java:1160)
    at com.vectorx.spring5.s6_xml.factorybean.TestFactoryBean.main(TestFactoryBean.java:11)

6、Bean 作用域和生命周期

6.1、Bean 作用域

在 Spring 里面,可以设置创建 Bean 的实例是单实例还是多实例,默认情况下是单实例

<bean id="book" class="com.vectorx.spring5.s7_xml.setter.Book"></bean>

测试

ApplicationContext context = new ClassPathXmlApplicationContext("bean6.xml");
Book book1 = context.getBean("book", Book.class);
Book book2 = context.getBean("book", Book.class);
System.out.println(book1 == book2); // true 表示是同一个对象,证明默认情况下是单实例

如何设置单实例多实例?

在 Spring 配置文件中 bean 标签里scope属性用于设置单实例还是多实例

  • 1)singleton,单实例,默认情况下不写也是它

  • 2)prototype,多实例

<bean id="book2" class="com.vectorx.spring5.s7_xml.setter.Book" scope="prototype"></bean>

测试

Book book3 = context.getBean("book2", Book.class);
Book book4 = context.getBean("book2", Book.class);
System.out.println(book3 == book4); // false 表示不是同一个对象,证明scope为prototype时是多实例

**singleton****prototype**的区别

singletonprototype除了单实例和多实例的差别之外,还有以下区别

  • 1)设置scope值是singleton时,加载 Spring 配置文件时就会创建单实例对象

  • 2)设置scope值是prototype时,加载 Spring 配置文件时不会创建对象,而是在调用getBean方法时创建多实例对象

**scope**的其他值

scope的属性值除了singletonprototype之外,其实还有一些属性值,如

  • request,每个request创建一个新的 bean

  • session,同一session中的 bean 是一样的

不过这两个属性值使用非常少,了解即可

6.2、Bean 生命周期

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

Bean 生命周期

  • 1)通过构造器创建 Bean 实例(无参构造)

  • 2)为 Bean 属性设置值和对其他 Bean 引用(调用 setter 方法)

  • 3)调用 Bean 的初始化方法(需要进行配置初始化方法)

  • 4)Bean 就可以使用了(对象获取到了)

  • 5)当容器关闭时,调用 Bean 的销毁方法(需要进行配置销毁方法)

代码演示

public class Orders {
    public Orders() {
        System.out.println("Step1.执行无参构造创建Bean实例.");
    }

    private String oname;

    public void setOname(String oname) {
        this.oname = oname;
        System.out.println("Step2.通过setter方法设置属性值.");
    }

    public void initMethod(){
        System.out.println("Step3.执行初始化方法.");
    }

    public void destoryMethod(){
        System.out.println("Step5.执行销毁方法.");
    }
}

Spring 配置文件中的配置

<bean id="orders" class="com.vectorx.spring5.s8_xml.lifecycle.Orders" init-method="initMethod"
      destroy-method="destoryMethod">
    <property name="oname" value="Phone"></property>
</bean>

测试

ClassPathXmlApplicationContext context = new ClassPathXmlApplicationContext("bean7.xml");
Orders orders = context.getBean("orders", Orders.class);
System.out.println("Step4.获取创建Bean实例对象.");
System.out.println(orders);
// 手动销毁Bean实例
context.close();

执行结果

Step1.执行无参构造创建Bean实例.
Step2.通过setter方法设置属性值.
Step3.执行初始化方法.
Step4.获取创建Bean实例对象.
com.vectorx.spring5.s8_xml.lifecycle.Orders@210366b4
Step5.执行销毁方法.

Spring 中 Bean 更加完整的生命周期其实不止上述 5 步,另外还有 2 步操作叫做 Bean 的后置处理器

Bean 后置处理器

加上 Bean 后置处理器,Bean 生命周期如下

  • 1)通过构造器创建 Bean 实例(无参构造)

  • 2)为 Bean 属性设置值和对其他 Bean 引用(调用 setter 方法)

  • 3)把 Bean 的实例传递给 Bean 后置处理器的方法postProcessBeforeInitialization

  • 4)调用 Bean 的初始化方法(需要进行配置初始化方法)

  • 5)把 Bean 的实例传递给 Bean 后置处理器的方法postProcessAfterInitialization

  • 6)Bean 就可以使用了(对象获取到了)

  • 7)当容器关闭时,调用 Bean 的销毁方法(需要进行配置销毁方法)

代码演示

  • 1)创建类,实现接口BeanPostProcessor,创建后置处理器
public class MyBeanPost implements BeanPostProcessor {
    @Override
    public Object postProcessBeforeInitialization(Object bean, String beanName) throws BeansException {
        System.out.println("Step.初始化之前执行的方法");
        return BeanPostProcessor.super.postProcessBeforeInitialization(bean, beanName);
    }

    @Override
    public Object postProcessAfterInitialization(Object bean, String beanName) throws BeansException {
        System.out.println("Step.初始化之后执行的方法");
        return BeanPostProcessor.super.postProcessAfterInitialization(bean, beanName);
    }
}
  • 2)Spring 配置文件中配置后置处理器
<!--配置后置处理器,会为当前配置文件中所有bean添加后置处理器-->
<bean id="myBeanPost" class="com.vectorx.spring5.s8_xml.lifecycle.MyBeanPost"></bean>

执行结果

Step1.执行无参构造创建Bean实例.
Step2.通过setter方法设置属性值.
Step.初始化之前执行的方法
Step3.执行初始化方法.
Step.初始化之后执行的方法
Step4.获取创建Bean实例对象.
com.vectorx.spring5.s8_xml.lifecycle.Orders@74e52ef6
Step5.执行销毁方法.

7、注解方式

7.1、什么是注解

  • 注解是一种代码特殊标记,格式:@注解名称(属性名称=属性值,属性名称=属性值...)

  • 注解作用:在类上面,方法上面,属性上面

  • 注解目的:简化 XML 配置

7.2、创建对象

  • @Component

  • @Service

  • @Controller

  • @Repository

上面四个注解功能是一样的,都可以用来创建 Bean 实例

  • 1)引入依赖

  • 2)开启组件扫描
<?xml version="1.0" encoding="UTF-8"?>
<!--引入context名称空间-->
<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xmlns:context="http://www.springframework.org/schema/context"
       xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd
                           http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context.xsd">
    <!--开启组件扫描:
        多个包用逗号隔开-->
    <context:component-scan
                            base-package="com.vectorx.spring5.s11_annotation.dao,com.vectorx.spring5.s11_annotation.service"></context:component-scan>
</beans>
  • 3)创建类,在类上添加创建对象注解
/**
 * value可省略,默认值为类名首字母小写
 */
@Component(value = "userService")
public class UserService {
    public void add(){
        System.out.println("UserService add...");
    }
}

7.3、组件扫描配置

设置扫描

  • use-default-filters表示现在不使用默认filter,自己配置filter

  • include-filter设置扫描哪些内容

<context:component-scan
                        base-package="com.vectorx.spring5.s11_annotation" use-default-filters="false">
    <context:include-filter type="annotation" expression="org.springframework.stereotype.Controller"/>
</context:component-scan>

设置不扫描

  • 配置扫描包下所有内容

  • exclude-filter设置不扫描哪些内容

<context:component-scan
                        base-package="com.vectorx.spring5.s11_annotation">
    <context:exclude-filter type="annotation" expression="org.springframework.stereotype.Controller"/>
</context:component-scan>

7.4、属性注入

  • @Autowired根据属性类型进行自动装配

  • @Qualifier根据属性名称进行注入,需要和@Autowired一起使用

  • @Resource可以根据类型和名称注入

  • @Value根据普通类型注入

@Autowired

  • 1)创建 Service 和 Dao 对象,在 Service 和 Dao 类上添加创建对象注解
public interface UserDao {
    void add();
}
@Repository
public class UserDaoImpl implements UserDao{
    @Override
    public void add() {
        System.out.println("UserDaoImpl add...");
    }
}
@Service
public class UserService {
    public void add() {
        System.out.println("UserService add...");
    }
}
  • 2)在 Service 类中添加 Dao 类型属性,在属性上面使用注解注入 Dao 对象
@Service
public class UserService {

    @Autowired
    private UserDao userDao;

    public void add() {
        System.out.println("UserService add...");
        userDao.add();
    }
}

因为@Autowired是根据属性类型进行注入的,如果 UserDao 的实现类不止一个,比如新增一个 UserDaoImpl2 类

@Repository
public class UserDaoImpl2 implements UserDao {
    @Override
    public void add() {
        System.out.println("UserDaoImpl2 add...");
    }
}

那么此时测试程序就会报错

Exception in thread "main" org.springframework.beans.factory.UnsatisfiedDependencyException: Error creating bean with name 'userService': Unsatisfied dependency expressed through field 'userDao'; nested exception is org.springframework.beans.factory.NoUniqueBeanDefinitionException: No qualifying bean of type 'com.vectorx.spring5.s11_annotation.dao.UserDao' available: expected single matching bean but found 2: userDaoImpl,userDaoImpl2
...
Caused by: org.springframework.beans.factory.NoUniqueBeanDefinitionException: No qualifying bean of type 'com.vectorx.spring5.s11_annotation.dao.UserDao' available: expected single matching bean but found 2: userDaoImpl,userDaoImpl2
...

大概意思就是说,主程序抛出了一个UnsatisfiedDependencyException不满足依赖异常,嵌套异常是NoUniqueBeanDefinitionExceptionBean定义不唯一异常,预期匹配单个 Bean 但是找到了两个 Bean

此时想要指定装配某一个实现类,就需要用到@Qualifier注解

@Qualifier

书接上回,如果我们想要从多个实现类中装配具体某一个实现类,可以这么写

@Autowired
@Qualifier(value = "userDaoImpl")
private UserDao userDao;

其中value值为具体的实现类上配置的注解中value

@Repository
public class UserDaoImpl implements UserDao{
    @Override
    public void add() {
        System.out.println("UserDaoImpl add...");
    }
}
@Repository
public class UserDaoImpl2 implements UserDao {
    @Override
    public void add() {
        System.out.println("UserDaoImpl2 add...");
    }
}

由于上述例子中,我们没有对@Repository配置相应的value,所以默认为首字母小写的类名

如果想使用 UserDaoImpl2 类,则

@Autowired
@Qualifier(value = "userDaoImpl2")
private UserDao userDao;

如果指定名称有误,即不存在名称为value对应的类,则会报NoSuchBeanDefinitionException异常,即找不到对应类

Exception in thread "main" org.springframework.beans.factory.UnsatisfiedDependencyException: Error creating bean with name 'userService': Unsatisfied dependency expressed through field 'userDao'; nested exception is org.springframework.beans.factory.NoSuchBeanDefinitionException: No qualifying bean of type 'com.vectorx.spring5.s11_annotation.dao.UserDao' available: expected at least 1 bean which qualifies as autowire candidate. Dependency annotations: {@org.springframework.beans.factory.annotation.Autowired(required=true), @org.springframework.beans.factory.annotation.Qualifier(value=userDaoImpl1)}
...
Caused by: org.springframework.beans.factory.NoSuchBeanDefinitionException: No qualifying bean of type 'com.vectorx.spring5.s11_annotation.dao.UserDao' available: expected at least 1 bean which qualifies as autowire candidate. Dependency annotations: {@org.springframework.beans.factory.annotation.Autowired(required=true), @org.springframework.beans.factory.annotation.Qualifier(value=userDaoImpl1)}

@Resource

  • 根据类型注入
@Resource
private UserDao userDao;
  • 根据名称注入
@Resource(name = "userDaoImpl")
private UserDao userDao;

需要注意的是@Resource注解所在包为javax.annotation即 Java 扩展包,所以 Spring 官方不建议使用该注解而推崇@Autowired@Qualifier注解

@Value

上述注解都是对对象类型的属性进行注入,如果想要装配普通类型属性,如基本数据类型及其包装类等,则可以需要使用@Value注解

@Value(value = "vector")
private String name;
@Value(value = "100")
private Integer age;
@Value(value = "200.0d")
private Double length;
@Value(value = "true")
private boolean isOk;
@Value(value = "0,a,3,6,test")
private String[] arrs;

7.5、完全注解开发

  • 1)创建配置类,替代 XML 配置文件
@Configuration
@ComponentScan(basePackages = "com.vectorx.spring5.s11_annotation")
public class SpringConfig {
}
  • 2)编写测试类
ApplicationContext context = new AnnotationConfigApplicationContext(SpringConfig.class);
UserService userService = context.getBean("userService", UserService.class);
userService.add();

与之前的不同点就是用AnnotationConfigApplicationContext代替了ClassPathXmlApplicationContext对象

03、AOP

1、AOP 概述

  • 定义:AOP(Aspect Oriented Programming,面向切面编程),通过预编译和运行时动态代理扩展程序功能

  • 作用:利用 AOP 可以对业务逻辑的各个部分进行隔离,降低耦合性,提高程序可重用性和开发效率

  • 场景:日志记录,性能统计,安全控制,事务处理,异常处理

  • 通俗描述:不修改源代码,在主干功能中添加新功能

使用登录功能案例说明 AOP

2、AOP 底层原理

  • 底层原理:动态代理

  • 有接口情况:JDK动态代理

  • 无接口情况:CGLib动态代理

如果学习过设计模式,应该对上述两种代理方式非常了解了。没有学习过也没关系,我们接着往下看

2.1、JDK 动态代理

public interface UserDao {
    void login();
}
public class UserDaoImpl implements UserDao {
    @Override
    public void login(){
        //登录实现过程
    }
}

有接口情况:创建 UserDao 接口实现类代理对象

2.2、CGlib 动态代理

public class User {
    public void add(){
        //...
    }
}
// 原始方法:通过子类继承,重写User类方法
public class Person extends User {
    @Override
    public void add(){
        super.add();
        //增强代码逻辑
    }
}

无接口情况:创建 User 类子类代理对象


由于 Spring5 中对上述代理已经做了很好的封装,我们只需要通过最简单的方式进行配置即可

但仍然需要我们对原理有一定的认识,只有做到“知其然,知其所以然”,才能真正“以不变应万变”

3、JDK 动态代理实现

实现方式:使用Proxy中的方法创建代理对象

具体方法newProxyInstance()

方法参数

  • ClassLoader loader:类加载器

  • Class<?>[] interfaces:增强方法所在类实现的接口数组

  • InvocationHandler h:实现InvocationHandler接口,创建代理对象,编写增强方法

常言道:“Talking is cheap, show me the code"。话不多说,下面上代码~

  • 1)创建 UserDao 接口和对应实现类
public interface UserDao {
    int add(int a, int b);
    String update(String id);
}
public class UserDaoImpl implements UserDao {
    @Override
    public int add(int a, int b) {
        return a + b;
    }
    @Override
    public String update(String id) {
        return id;
    }
}
  • 2)创建 UserDao 代理对象
public class UserDaoProxy {
    private UserDao target;
    public UserDaoProxy(UserDao target) {
        this.target = target;
    }
    public UserDao newProxyInstance() {
        Class<?> targetClass = target.getClass();
        ClassLoader classLoader = targetClass.getClassLoader();
        Class<?>[] interfaces = targetClass.getInterfaces();
        return (UserDao) Proxy.newProxyInstance(classLoader, interfaces, new UserDaoInvocationHandler());
    }
    class UserDaoInvocationHandler implements InvocationHandler {

        @Override
        public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
            // 被代理对象方法前置逻辑
            System.out.print("method=" + method.getName() + ", args=" + Arrays.toString(args));
            // 被代理对象方法
            Object result = method.invoke(target, args);
            // 被代理对象方法后置逻辑
            System.out.println(", result=" + result);
            return result;
        }
    }
}
  • 3)测试
UserDao target = new UserDaoImpl();
UserDaoProxy userDaoProxy = new UserDaoProxy(target);
UserDao userDao = userDaoProxy.newProxyInstance();
userDao.add(1, 2);
userDao.update("UUID1");
// method=add, args=[1, 2], result=3
// method=update, args=[UUID1], result=UUID1

4、AOP 术语

  • 连接点:类中可以被增强的方法,称为连接点

  • 切入点:类中实际被增强的方法,称为切入点

  • 通知(增强):实际增强的逻辑部分,称为通知
    通知分为五种类型:

  • 前置通知:方法执行之前的处理

  • 后置通知:方法执行之后的处理

  • 环绕通知:方法执行前后的处理

  • 异常通知:方法抛出异常的处理

  • 最终通知:方法执行最终的处理(相当于try-catch-finally中的finally

  • 切面:是一个动作,即把通知应用到切入点的过程

5、AOP 准备工作

5.1、AspectJ 介绍

Spring 一般都是基于AspectJ实现 AOP 操作的

  • AspectJ不是 Spring 的一部分,而是一个独立的 AOP 框架

  • 一般会把AspectJ和 Spring 搭配使用,进行 AOP 操作,因为这样更加方便

基于 AspectJ 进行 AOP 操作的两种方式:

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

  • 基于注解方式实现(推荐使用)

5.2、引入 AOP 相关依赖

<dependency>
    <groupId>org.aspectj</groupId>
    <artifactId>aspectjrt</artifactId>
    <version>1.9.8</version>
</dependency>
<dependency>
    <groupId>org.aspectj</groupId>
    <artifactId>aspectjweaver</artifactId>
    <version>1.9.8</version>
</dependency>

5.3、切入点表达式

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

语法结构:execution([权限修饰符][返回类型][类全路径][方法名]([参数列表]))

举例

 举例1:对com.vectorx.dao.BookDao中的add()方法进行增强

execution(* com.vectorx.dao.BookDao.add(..))

 举例2:对com.vectorx.dao.BookDao中的所有方法进行增强

execution(* com.vectorx.dao.BookDao.*(..))

 举例3:对com.vectorx.dao包中所有类的所有方法进行增强

execution(* com.vectorx.dao.*.*(..))

6、AspectJ 注解实现

6.1、Spring 配置文件

  • 1)引入contextaop名称空间

  • 2)配置组件扫描基础包

  • 3)开启AspectJ生成代理对象

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:context="http://www.springframework.org/schema/context"
       xmlns:aop="http://www.springframework.org/schema/aop"
       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 
                           http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context.xsd 
                           http://www.springframework.org/schema/aop http://www.springframework.org/schema/aop/spring-aop.xsd">

    <!--组件扫描配置-->
    <context:component-scan base-package="com.vectorx.spring5.s13_aspectj_annatation"/>

    <!--开启AspectJ生成代理对象-->
    <aop:aspectj-autoproxy/>
</beans>

6.2、创建被增强对象和增强对象

  • 1)创建 User 对象,并添加@Component注解

  • 2)创建 UserProxy 对象,并添加@Component注解

@Component
public class User {
    public void add() {
        System.out.println("add...");
    }
}
@Component
public class UserProxy {
    /**
     * 前置通知
     */
    public void before() {
        System.out.println("before...");
    }

    /**
     * 后置通知
     */
    public void afterReturning() {
        System.out.println("afterReturning...");
    }

    /**
     * 最终通知
     */
    public void after() {
        System.out.println("after...");
    }

    /**
     * 异常通知
     */
    public void afterThrowing() {
        System.out.println("afterThrowing...");
    }

    /**
     * 环绕通知
     */
    public void around() {
        System.out.println("around...");
    }
}

6.3、添加增强类注解和切入点表达式

@Component
@Aspect
public class UserProxy {
    /**
     * 前置通知
     */
    @Before(value = "execution(* com.vectorx.spring5.s13_aspectj_annatation.User.add(..))")
    public void before() {
        System.out.println("before...");
    }

    /**
     * 后置通知
     */
    @AfterReturning(value = "execution(* com.vectorx.spring5.s13_aspectj_annatation.User.add(..))")
    public void afterReturning() {
        System.out.println("afterReturning...");
    }

    /**
     * 最终通知
     */
    @After(value = "execution(* com.vectorx.spring5.s13_aspectj_annatation.User.add(..))")
    public void after() {
        System.out.println("after...");
    }

    /**
     * 异常通知
     */
    @AfterThrowing(value = "execution(* com.vectorx.spring5.s13_aspectj_annatation.User.add(..))")
    public void afterThrowing() {
        System.out.println("afterThrowing...");
    }

    /**
     * 环绕通知
     */
    @Around(value = "execution(* com.vectorx.spring5.s13_aspectj_annatation.User.add(..))")
    public void around(ProceedingJoinPoint joinPoint) throws Throwable {
        System.out.println("around before...");
        // 执行被增强的方法
        joinPoint.proceed();
        System.out.println("around after...");
    }
}

6.4、代码测试

ApplicationContext context = new ClassPathXmlApplicationContext("bean11.xml");
User user = context.getBean("user", User.class);
user.add();

结果

around before...
before...
add...
afterReturning...
after...
around after...

为了演示异常通知,需要修改下被增强对象中的方法,模拟一个异常

@Component
public class User {
    public void add() {
        System.out.println("add...");
        // 模拟一个异常
        int i = 2 / 0;
    }
}

运行结果

around before...
before...
add...
afterThrowing...
after...

对比正常情况下,发现少了afterReturning即后置异常和around after即环绕增强的后置处理

6.5、抽取相同切入点表达式

通过上述的例子,应该对AspectJ注解实现有了一定的了解

同时我们发现切入点表达式都是完全一样的,可以对这些相同的切入点表达式进行抽取,以达到重用切入点表达式定义的目的

  • 1)首先想到的应该是定义成员变量
private final String execution = "execution(* com.vectorx.spring5.s13_aspectj_annatation.User.add(..))";

@Before(value = execution)
public void before() {
    System.out.println("before...");
}
  • 2)AspectJ中提供了Pointcut注解(推荐)
@Pointcut(value = "execution(* com.vectorx.spring5.s13_aspectj_annatation.User.add(..))")
private void pointcut(){}

@Before(value = "pointcut()")
public void before() {
    System.out.println("before...");
}

6.6、设置增强类优先级

如果有多个增强类对类中同一个方法进行增强,可以设置增强类的优先级,来决定哪个增强类先执行,哪个增强类后执行

使用@Order注解设置增强类的优先级,其中指定优先级数字,注解格式:@Order(数字类型值)

  • 数字类型值越小,优先级越高

  • 数字类型值越大,优先级越低

最佳实践

@Component
@Aspect
@Order(1)
public class PersonProxy {
    //...
}
@Component
@Aspect
@Order(3)
public class UserProxy {
    //...
}

测试结果

person around before...
person before...
user around before...
user before...
add...
user afterReturning...
user after...
user around after...
person afterReturning...
person after...
person around after...

我们发现:

  • PersonProxy 中的前置通知先于 UserProxy 中的前置通知执行

  • PersonProxy 中的后置通知晚于 UserProxy 中的后置通知执行

6.7、完全注解开发

如果要用完全注解的方式进行开发,可以使用注解类代替 Spring 配置文件

@Configuration
@ComponentScan(value = "com.vectorx.spring5.s13_aspectj_annatation")
@EnableAspectJAutoProxy(proxyTargetClass = true)
public class AopConfig {
}

其中:

  • 注解@ComponentScan(value = "com.vectorx.spring5.s13_aspectj_annatation")代替了<context:component-scan base-package="com.vectorx.spring5.s13_aspectj_annatation"/>进行组件扫描的配置

  • 注解@EnableAspectJAutoProxy(proxyTargetClass = true)代替了<aop:aspectj-autoproxy/>开启AspectJ生成代理对象

对应关系

注解方式 配置文件方式
@ComponentScan <context:component-scan>
@EnableAspectJAutoProxy <aop:aspectj-autoproxy>

7、AspectJ 配置文件实现

7.1、创建被增强对象和增强对象

public class Book {
    public void buy() {
        System.out.println("buy...");
    }
}
public class BookProxy {
    public void before() {
        System.out.println("before...");
    }
    public void afterReturning() {
        System.out.println("afterReturning...");
    }
    public void after() {
        System.out.println("after...");
    }
    public void afterThrowing() {
        System.out.println("afterThrowing...");
    }
    public void around(ProceedingJoinPoint joinPoint) throws Throwable {
        System.out.println("around before...");
        joinPoint.proceed();
        System.out.println("around after...");
    }
}

7.2、Spring 配置文件

  • 1)引入aop名称空间

  • 2)配置被增强对象和增强对象创建

  • 3)配置aop增强

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

    <!--创建对象-->
    <bean id="book" class="com.vectorx.spring5.s14_aspectj_xml.Book"></bean>
    <bean id="bookProxy" class="com.vectorx.spring5.s14_aspectj_xml.BookProxy"></bean>

    <!--配置aop增强-->
    <aop:config>
        <!--配置切入点-->
        <aop:pointcut id="p" expression="execution(* com.vectorx.spring5.s14_aspectj_xml.Book.buy(..))"/>
        <!--配置切面-->
        <aop:aspect ref="bookProxy">
            <!--前置通知-->
            <aop:before method="before" pointcut-ref="p"/>
            <!--后置通知-->
            <aop:after-returning method="afterReturning" pointcut-ref="p"/>
            <!--最终通知-->
            <aop:after method="after" pointcut-ref="p"/>
            <!--异常通知-->
            <aop:after-throwing method="afterThrowing" pointcut-ref="p"/>
            <!--环绕通知-->
            <aop:around method="around" pointcut-ref="p"/>
        </aop:aspect>
    </aop:config>
</beans>

其中,配置文件的标签与注解的对应关系如下表

   
配置文件方式 注解方式
aop:pointcut @Pointcut
aop:aspect @Aspect
aop:before @Before
aop:after-returning @AfterReturning
aop:after @After
aop:after-throwing @AfterThrowing
aop:around @Around

7.3、代码测试

ApplicationContext context = new ClassPathXmlApplicationContext("bean12.xml");
Book book = context.getBean("book", Book.class);
book.buy();

测试结果

before...
around before...
buy...
around after...
after...
afterReturning...

小结

本节重点

  1. AOP 概述

  2. AOP 底层原理

  3. AOP 术语

  4. 切入点表达式

  5. AspectJ 实现

  6. 完全注解开发

以下总结仅供参考

04、 JdbcTemplate与声明式事务

1、JdbcTemplate

1.1、概述

前面我们已经学习了 Spring 中的Core Container核心部分和AOPAspects等面向切面编程部分,接下来就是Data Access/Integration即数据访问和集成部分

Spring 既可以单独使用,也可以集成其他框架,如HibernateMyBatis等。除此之外,其中对于JDBC也做了封装,即本章节的JdbcTemplate,用它可以比较方便地对数据库进行增删改查等操作

总结一下:

  • JdbcTemplate就是 Spring 框架对JDBC技术进行的二次封装模板,能够简化对数据库的操作

1.2、准备工作

步骤预览

  • 1)引入相关jar

  • 2)Spring 配置文件配置Druid连接池信息

  • 3)配置JdbcTemplate对象,注入dataSource

  • 4)创建 Service 和 Dao 类,在 Dao 类中注入JdbcTemplate对象

详细操作

  • 1)引入相关jar包(或依赖)

  • druid

  • mysql-connector-java

  • spring-jdbc

  • spring-orm

  • spring-tx

  • 2)Spring 配置文件配置Druid连接池信息
<context:property-placeholder location="classpath:jdbc.properties"/>
<bean id="dataSource" class="com.alibaba.druid.pool.DruidDataSource">
    <property name="driverClassName" value="${mysql.driverClassName}"/>
    <property name="url" value="${mysql.url}"/>
    <property name="username" value="${mysql.username}"/>
    <property name="password" value="${mysql.password}"/>
</bean>

沿用之前章节的Jdbc.properties配置信息,但稍作修改

mysql.driverClassName=com.mysql.jdbc.Driver
mysql.url=jdbc:mysql:///book_db
mysql.username=root
mysql.password=root
  • 3)配置JdbcTemplate对象,注入dataSource
<!--配置JdbcTemplate-->
<bean id="jdbcTemplate" class="org.springframework.jdbc.core.JdbcTemplate">
    <!--属性注入dataSource-->
    <property name="dataSource" ref="dataSource"></property>
</bean>

为何使用属性注入?

JdbcTemplate虽然含有DataSource的有参构造,但其调用了setDataSource()方法

这个方法是在其父类中定义了的

  • 4)创建 Service 和 Dao 类,在 Dao 类中注入JdbcTemplate对象

Dao 类

public interface BookDao {
}
@Repository
public class BookDaoImpl implements BookDao {
    @Autowired
    private JdbcTemplate jdbcTemplate;
}

Service 类

@Service
public class BookService {
    @Autowired
    private BookDao bookDao;
}

别忘了开启注解扫描

<!--开启注解扫描-->
<context:component-scan base-package="com.vectorx.spring5.s15_jdbctemplate"/>

配置文件整体结构

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

    <!--开启注解扫描-->
    <context:component-scan base-package="com.vectorx.spring5.s15_jdbctemplate"/>

    <!--配置dataSource-->
    <context:property-placeholder location="classpath:jdbc.properties"/>
    <bean id="dataSource" class="com.alibaba.druid.pool.DruidDataSource">
        <property name="driverClassName" value="${mysql.driverClassName}"/>
        <property name="url" value="${mysql.url}"/>
        <property name="username" value="${mysql.username}"/>
        <property name="password" value="${mysql.password}"/>
    </bean>

    <!--配置JdbcTemplate-->
    <bean id="jdbcTemplate" class="org.springframework.jdbc.core.JdbcTemplate">
        <!--属性注入dataSource-->
        <property name="dataSource" ref="dataSource"></property>
    </bean>
</beans>

1.3、添加操作

步骤预览

  • 1)创建数据库中t_book表对应的实体对象

  • 2)编写 Service 和 Dao 代码,增加添加图书的功能逻辑

  • 3)代码测试

详细操作

  • 1)创建数据库中t_book表对应的实体对象
public class Book {
    private String bid;
    private String bname;
    private String bstatus;

    public String getBid() {
        return bid;
    }
    public void setBid(String bid) {
        this.bid = bid;
    }
    public String getBname() {
        return bname;
    }
    public void setBname(String bname) {
        this.bname = bname;
    }
    public String getBstatus() {
        return bstatus;
    }
    public void setBstatus(String bstatus) {
        this.bstatus = bstatus;
    }
}
  • 2)编写 Service 和 Dao 代码,增加添加图书的功能逻辑

Service 类:添加addBook()方法

@Service
public class BookService {
    @Autowired
    private BookDao bookDao;

    public int addBook(Book book) {
        return bookDao.add(book);
    }
}

Dao 类:通过操作JdbcTemplate对象的update()方法可实现插入,其中两个参数分别是

  • 第一个参数sql:编写插入数据对应的sql语句,可使用通配符?做占位符

  • 第二个参数args:可变参数列表,设置占位符对应的参数值

public interface BookDao {
    int add(Book book);
}
@Repository
public class BookDaoImpl implements BookDao {
    @Autowired
    private JdbcTemplate jdbcTemplate;

    @Override
    public int add(Book book) {
        //操作JdbcTemplate对象,使用update方法进行添加操作
        String sql = "insert into t_book(bid,bname,bstatus) values(?,?,?)";
        Object[] args = {book.getBid(), book.getBname(), book.getBstatus()};
        return jdbcTemplate.update(sql, args);
    }
}
  • 3)代码测试
ApplicationContext context = new ClassPathXmlApplicationContext("bean13.xml");
BookService bookService = context.getBean("bookService", BookService.class);

//模拟新增图书
Book book = new Book();
book.setBid("1");
book.setBname("Spring JdbcTemplate");
book.setBstatus("1");
int result = bookService.addBook(book);
System.out.println(result);

测试结果

Loading class `com.mysql.jdbc.Driver'. This is deprecated. The new driver class is `com.mysql.cj.jdbc.Driver'. The driver is automatically registered via the SPI and manual loading of the driver class is generally unnecessary.
三月 06, 2022 10:25:49 下午 com.alibaba.druid.pool.DruidDataSource info
信息: {dataSource-1} inited
1

刷新数据库中t_book表数据,核验是否插入成功

可以看到,表中成功新增了一条数据

1.4、修改和删除

修改、删除操作和添加操作代码逻辑基本一致

BookService 类:添加updateBook()deleteBook()方法

// 修改
public int updateBook(Book book) {
    return bookDao.update(book);
}
//删除
public int deleteBook(String id) {
    return bookDao.delete(id);
}

BookDao 类:添加update()delete()方法

// 修改
int update(Book book);
// 删除
int delete(String id);

BookDaoImpl 类:实现update()delete()方法

// 修改
@Override
public int update(Book book) {
    String sql = "update t_book set bname=?,bstatus=? where bid=?";
    Object[] args = {book.getBname(), book.getBstatus(), book.getBid()};
    return jdbcTemplate.update(sql, args);
}
// 删除
@Override
public int delete(String id) {
    String sql = "delete from t_book where bid=? ";
    return jdbcTemplate.update(sql, id);
}

测试修改

//修改图书信息
Book book = new Book();
book.setBid("1");
book.setBname("JdbcTemplate");
book.setBstatus("update");
int result2 = bookService.updateBook(book);
System.out.println(result2);

测试结果

测试删除

//删除图书
int result3 = bookService.deleteBook("1");
System.out.println(result3);

测试结果

1.5、查询操作

这里演示三种查询操作:

  • 1)查询返回某个值

  • 2)查询返回对象

  • 3)查询返回集合

为了演示效果,需要先在数据库的t_book表中添加两条数据

接着我们先将代码完成,最后再作进一步的分析说明

代码实现

BookService 类:添加findCount()findById()findAll()方法

// 查找返回一个值
public int findCount() {
    return bookDao.selectCount();
}
// 查找返回对象
public Book findById(String id) {
    return bookDao.selectById(id);
}
// 查找返回集合
public List<Book> findAll() {
    return bookDao.selectAll();
}

BookDao 类:添加selectCount()selectById()selectAll()方法

// 查找返回一个值
int selectCount();
// 查找返回对象
Book selectById(String id);
// 查找返回集合
List<Book> selectAll();

BookDaoImpl 类:实现selectCount()selectById()selectAll()方法

// 查找返回一个值
@Override
public int selectCount() {
    String sql = "select count(0) from t_book";
    return jdbcTemplate.queryForObject(sql, Integer.class);
}
// 查找返回对象
@Override
public Book selectById(String id) {
    String sql = "select * from t_book where bid=?";
    return jdbcTemplate.queryForObject(sql, new BeanPropertyRowMapper<>(Book.class), id);
}
// 查找返回集合
@Override
public List<Book> selectAll() {
    String sql = "select * from t_book where 1=1";
    return jdbcTemplate.query(sql, new BeanPropertyRowMapper<>(Book.class));
}

测试代码

int count = bookService.findCount();
System.out.println(count);
Book book = bookService.findById("1");
System.out.println(book);
List<Book> bookList = bookService.findAll();
System.out.println(bookList);

测试结果

2
Book{bid='1', bname='Spring', bstatus='add'}
[Book{bid='1', bname='Spring', bstatus='add'}, Book{bid='2', bname='SpringMVC', bstatus='add'}]

代码分析

上述代码逻辑中使用到了queryForObject()query()方法

jdbcTemplate.queryForObject(sql, Integer.class);
jdbcTemplate.queryForObject(sql, new BeanPropertyRowMapper<>(Book.class), id);
jdbcTemplate.query(sql, new BeanPropertyRowMapper<>(Book.class));

分别对应JdbcTemplate中的三个方法

public <T> T queryForObject(String sql, Class<T> requiredType);
public <T> T queryForObject(String sql, RowMapper<T> rowMapper, Object... args);
public <T> List<T> query(String sql, RowMapper<T> rowMapper);

其中,有两个参数值得关注,一个是Class<T> requiredType,另一个是RowMapper<T> rowMapper

  • Class<T> requiredType:返回值的Class类型

  • RowMapper<T> rowMapper:是一个接口,返回不同类型数据,可以使用其实现类进行数据的封装。其实现类有很多,因为我们需要返回一个数据库实体对象,所以可以选择使用BeanPropertyRowMapper

另外,queryForObject(String sql, RowMapper<T> rowMapper, Object... args)query(String sql, RowMapper<T> rowMapper)

区别在于

  • queryForObject返回一个对象

  • query返回一个集合

1.6、批量操作

JdbcTemplate中提供了batchUpdate()可供我们进行批量操作,如:批量添加、批量修改、批量删除等,代码实现上大同小异,我们对代码进行快速实现

代码实现

BookService 类:添加batchAddBook()batchUpdateBook()batchDelBook()方法

// 批量添加
public void batchAddBook(List<Object[]> bookList) {
    bookDao.batchAdd(bookList);
}
// 批量修改
public void batchUpdateBook(List<Object[]> bookList) {
    bookDao.batchUpdate(bookList);
}
// 批量删除
public void batchDelBook(List<Object[]> bookList) {
    bookDao.batchDel(bookList);
}

BookDao 类:添加batchAdd()batchUpdate()batchDel()方法

// 批量添加
void batchAdd(List<Object[]> bookList);
// 批量修改
void batchUpdate(List<Object[]> bookList);
// 批量删除
void batchDel(List<Object[]> bookList);

BookDaoImpl 类:实现batchAdd()batchUpdate()batchDel()方法

// 批量添加
@Override
public void batchAdd(List<Object[]> bookList) {
    String sql = "insert into t_book(bid,bname,bstatus) values(?,?,?)";
    extractBatch(sql, bookList);
}
// 批量修改
@Override
public void batchUpdate(List<Object[]> bookList) {
    String sql = "update t_book set bname=?,bstatus=? where bid=?";
    extractBatch(sql, bookList);
}
// 批量删除
@Override
public void batchDel(List<Object[]> bookList) {
    String sql = "delete from t_book where bid=? ";
    extractBatch(sql, bookList);
}
private void extractBatch(String sql, List<Object[]> bookList,) {
    int[] ints = jdbcTemplate.batchUpdate(sql, bookList);
    System.out.println(ints);
}

代码测试

测试批量添加

// 批量添加
List<Object[]> bookList = new ArrayList<>();
Object[] book1 = {"3", "Java", "batchAdd"};
Object[] book2 = {"4", "Python", "batchAdd"};
Object[] book3 = {"5", "C#", "batchAdd"};
bookList.add(book1);
bookList.add(book2);
bookList.add(book3);
bookService.batchAddBook(bookList);

测试结果

测试批量修改

// 批量修改
List<Object[]> bookList = new ArrayList<>();
Object[] book1 = {"Java++", "batchUpdate", "3"};
Object[] book2 = {"Python++", "batchUpdate", "4"};
Object[] book3 = {"C#++", "batchUpdate", "5"};
bookList.add(book1);
bookList.add(book2);
bookList.add(book3);
bookService.batchUpdateBook(bookList);

测试结果

测试批量删除

// 批量删除
List<Object[]> bookList = new ArrayList<>();
Object[] book1 = {"3"};
Object[] book2 = {"4"};
bookList.add(book1);
bookList.add(book2);
bookService.batchDelBook(bookList);

测试结果

可以看出,上述测试都完全符合我们的预期

小结

简单总结下JdbcTemplate操作数据库的各个方法

  • 添加、修改、删除操作:update()方法

  • 查询操作:queryForObject()query()方法,关注两个参数:

  • Class<T> requiredType:返回值的Class类型

  • RowMapper<T> rowMapper:接口,具体实现类BeanPropertyRowMapper,封装对象实体

  • 批量操作:batchUpdate()方法

2、事务

2.1、事务概念

  • 1)事务是数据库操作的最基本单元,是逻辑上的一组操作。这一组操作,要么都成功,要么都失败(只要有一个操作失败,所有操作都失败)

  • 2)典型场景:银行转账。Lucy 转账 100 元给 Mary,Lucy 少 100,Mary 多 100。转账过程中若出现任何问题,双方都不会多钱或少钱,转账就不会成功

2.2、事务四个特性(ACID)

  • 原子性(Atomicity):一个事务中的所有操作,要么都成功,要么都失败,整个过程不可分割

  • 一致性(Consistency):事务操作之前和操作之后,总量保持不变

  • 隔离性(Isolation):多事务操作时,相互之间不会产生影响

  • 持久性(Durability):事务最终提交后,数据库表中数据才会真正发生变化

2.3、搭建事务操作环境

我们知道 JavaEE 中的三层架构分为:表示层(web层)、业务逻辑层(service层)、数据访问层(dao层)

  • web层:与客户端进行交互

  • service层:处理业务逻辑

  • dao层:与数据库进行交互

因此,我们搭建操作环境也按照典型的三层架构来实现,不过目前现阶段我们只关注ServiceDao两层

我们以银行转账为例,因为整个转账操作包括两个操作:出账的操作和入账的操作

过程概览

  • 1)创建数据库表结构,添加几条记录

  • 2)创建ServiceDao类,完成对象创建和关系注入

  • 3)Dao中创建两个方法:出账的方法、入账的方法;Service中创建转账的方法

过程详解

**1)**创建数据库表结构,添加几条记录

# 建表语句
create table t_account
(
    id       varchar(20) not null,
    username varchar(50) null,
    amount   int         null,
    constraint transfer_record_pk
        primary key (id)
);
# 添加语句
INSERT INTO book_db.t_account (id, username, amount) VALUES ('1', 'Lucy', 1000);
INSERT INTO book_db.t_account (id, username, amount) VALUES ('2', 'Mary', 1000);

添加完成效果

**2)**创建ServiceDao类,完成对象创建和关系注入

Service中注入DaoDao中注入JdbcTemplateJdbcTemplate中注入DataSource

ServiceDao

public interface TransferRecordDao {
}
@Repository
public class TransferRecordDaoImpl implements TransferRecordDao {
    @Autowired
    private JdbcTemplate jdbcTemplate;
}
@Service
public class TransferRecordService {
    @Autowired
    private TransferRecordDao transferRecordDao;
}

Spring 配置文件

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xmlns:context="http://www.springframework.org/schema/context"
       xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd
                           http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context.xsd">
    <!--开启注解扫描-->
    <context:component-scan base-package="com.vectorx.spring5.s16_transaction"/>
    <!--配置dataSource-->
    <context:property-placeholder location="classpath:jdbc.properties"/>
    <bean id="dataSource" class="com.alibaba.druid.pool.DruidDataSource">
        <property name="driverClassName" value="${mysql.driverClassName}"/>
        <property name="url" value="${mysql.url}"/>
        <property name="username" value="${mysql.username}"/>
        <property name="password" value="${mysql.password}"/>
    </bean>
    <!--配置JdbcTemplate-->
    <bean id="jdbcTemplate" class="org.springframework.jdbc.core.JdbcTemplate">
        <!--属性注入dataSource-->
        <property name="dataSource" ref="dataSource"></property>
    </bean>
</beans>

3)Dao中创建两个方法:出账的方法、入账的方法;Service中创建转账的方法

  • Dao负责数据库操作,所以需要创建两个方法:出账的方法、入账的方法
public interface TransferRecordDao {
    void transferOut(int amount, String username);
    void transferIn(int amount, String username);
}
@Repository
public class TransferRecordDaoImpl implements TransferRecordDao {
    @Autowired
    private JdbcTemplate jdbcTemplate;

    @Override
    public void transferOut(int amount, String username) {
        String sql = "update t_account set amount=amount-? where username=?";
        Object[] args = {amount, username};
        jdbcTemplate.update(sql, args);
    }
    @Override
    public void transferIn(int amount, String username) {
        String sql = "update t_account set amount=amount+? where username=?";
        Object[] args = {amount, username};
        jdbcTemplate.update(sql, args);
    }
}
  • Service负责业务操作,所以需要创建一个方法,来调用Dao中两个方法
@Service
public class TransferRecordService {
    @Autowired
    private TransferRecordDao transferRecordDao;

    public void transferAccounts(int amount, String fromUser, String toUser) {
        transferRecordDao.transferOut(amount, fromUser);
        transferRecordDao.transferIn(amount, toUser);
    }
}

测试代码

ApplicationContext context = new ClassPathXmlApplicationContext("bean14.xml");
TransferRecordService transferRecordService = context.getBean("transferRecordService", TransferRecordService.class);
transferRecordService.transferAccounts(100, "Lucy", "Mary");

测试结果

可以发现,转账如期完成了。但真的没有一点问题么?

2.4、引入事务场景

我们模拟下在转账中途发生网络异常,修改TransferRecordService中转账方法

public void transferAccounts(int amount, String fromUser, String toUser) {
    transferRecordDao.transferOut(amount, fromUser);
    //模拟网络异常而导致操作中断
    int i = 10 / 0;
    transferRecordDao.transferIn(amount, toUser);
}

为了更清晰直观地看到数据的变化,我们还原数据表数据到最初状态

按照期望,转账应该失败,即双方账户不应该有任何变化。事实真的能够如我们所料么?

我们执行测试方法,如期抛出异常

Exception in thread "main" java.lang.ArithmeticException: / by zero
    at com.vectorx.spring5.s16_transaction.service.TransferRecordService.transferAccounts(TransferRecordService.java:15)
    at com.vectorx.spring5.s16_transaction.TestTransfer.main(TestTransfer.java:11)

那数据表是否也如期变化呢?

我们发现,Lucy虽然成功转出了 100 元,但Mary没有成功到账 100 元。从现实的角度来说,这个问题很严重!!!

从事务的角度来说,这个转账操作没有遵循事务的原子性、一致性,即没有做到“要么都成功,要么都失败”,也没有做到“操作前后的总量不变”

综上所述,我们需要引入事务

2.5、事务基本操作

事务的基本操作过程如下

  • Step1、开启一个事务

  • Step2、进行业务逻辑实现

  • Step3、没有异常,则提交事务

  • Step4、发生异常,则回滚事务

事务的一般实现如下

try {
    // Step1、开启一个事务
    // Step2、进行业务逻辑实现
    transferRecordDao.transferOut(amount, fromUser);
    //模拟网络异常而导致操作中断
    int i = 10 / 0;
    transferRecordDao.transferIn(amount, toUser);
    // Step3、没有异常,则提交事务
} catch (Exception e) {
    // Step4、发生异常,则回滚事务
}

不过,在 Spring 框架中提供了更方便的方式实现事务。“欲知后事如何,且听下回分解”

小结

本小结主要内容关键点

  • 事务的基本概念:数据库操作的基本单元,逻辑上的一组操作,要么都成功,要么都失败

  • 事务的四个基本特性:ACID,即原子性、一致性、隔离性和持久性

3、声明式事务

3.1、Spring事务管理

事务一般添加到三层结构中的Service层(业务逻辑层)

在 Spring 中进行事务管理操作有两种方式:编程式事务管理和声明式事务管理

  • 编程式事务管理(不推荐):上述事务的一般实现就是典型的编程式事务管理实现。但这种方式虽然并不好,但仍然需要我们有一定的了解,知道有这么一个过程即可。一般不推荐使用这种方式,主要原因如下

  • 1)实现不方便

  • 2)造成代码臃肿

  • 3)维护起来麻烦

  • 声明式事务管理(推荐使用):底层原理就是AOP,就是在不改变原代码基础上,扩展代码功能。有两种实现方式

  • 基于注解方式(推荐方式)

  • 基于XML配置文件方式

3.2、Spring事务管理API

提供了一个接口,代表事务管理器,针对不同框架存在不同实现类

主要掌握

  • PlatformTransactionManager接口:即事务管理器,有多个不同的抽象类和具体实现类,可满足不同的框架

  • DataSourceTrasactionManager实现类:JdbcTemplateMyBatis框架使用到它

  • HibernateTransactionManager实现类:Hibernate框架使用到它

3.3、声明式事务(注解方式)

  • 1)在 Spring 配置文件中配置事务管理器:配置DataSourceTransactionManager对象创建
<!--配置事务管理器-->
<bean id="transactionManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
    <property name="dataSource" ref="dataSource"></property>
</bean>
  • 2)在 Spring 配置文件中开启事务:引入xmlns:tx的名称空间,并配置<tx:annotation-driven>标签以开启事务注解
<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"
       xmlns:tx="http://www.springframework.org/schema/tx"
       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
                           http://www.springframework.org/schema/tx http://www.springframework.org/schema/tx/spring-tx.xsd">
    <!--其他配置信息略-->
    <!--开启事务注解-->
    <tx:annotation-driven transaction-manager="transactionManager"></tx:annotation-driven>
</beans>
  • 3)在Service(或Service的方法)上添加事务注解@Transactional
@Service
@Transactional
public class TransferRecordService {
    //...
}

首先还原数据表信息至初始状态

测试代码后刷新数据表

这一次数据没有发生变化,说明事务回滚了,符合我们预期

3.4、事务参数

Service类上面添加注解@Transactional,在这个注解里面可以配置事务相关参数

主要介绍参数有

  • propagation:事务传播行为

  • isolation:事务隔离级别

  • timeout:超时时间

  • readOnly:是否只读

  • rollbackFor:回滚

  • noRollbackFor:不回滚

3.5、传播行为

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

  • 事务方法:让数据表数据发生变化的操作

事务的传播行为可以有传播属性指定。Spring 框架中定义了 7 种类传播行为:

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

举个例子:定义两个方法add()update()

@Transactional
public void add(){
    // 调用update方法
    update();
}
public void update(){
    // do something
}

则按照不同的传播属性,可以有以下解释

  • REQUIRED

  • 如果add()方法本身有事务,调用update()方法之后,update()使用当前add()方法里面事务;

  • 如果add()方法本身没有事务,调用update()方法之后,创建新的事务

  • REQUIRED_NEW

  • 使用add()方法调用update()方法,无论add()是否有事务,都创建新的事务

代码实现

@Service
@Transactional(propagation = Propagation.REQUIRED)
public class TransferRecordService {
    //...
}

等价于

@Service
@Transactional
public class TransferRecordService {
    //...
}

即默认不写,其事务传播行为就是使用的REQUIRED

3.6、隔离级别

在事务的四个特性中,隔离性(isolation)指的是多事务之间互不影响。不考虑隔离性,在并发时会产生一系列问题

有三个典型的“读”的问题:脏读、不可重复读、虚(幻)读

  • 脏读:一个未提交事务读取到了另一个未提交事务修改的数据

  • 不可重复读:一个未提交事务读取到了另一个已提交事务修改的数据(不能算问题,只是算现象)

  • 虚(幻)读:一个未提交事务读取到了另一个已提交事务添加的数据

通过设置隔离级别,可以解决上述“读”的问题

事务隔离级别 脏读 不可重复读 幻读
READ UNCOMMITTED(读未提交)
READ COMMITTED(读已提交) ×
REPEATABLE READ(可重复读) × ×
SERIALIZABLE(串行化) × × ×

代码实现

@Service
@Transactional(isolation = Isolation.REPEATABLE_READ)
public class TransferRecordService {
    //...
}

小课堂:MySQL 中默认事务隔离级别为REPEATABLE READ(可重复读)

3.7、其他参数

  • timeout:设置事务超时时间。事务需要在一定的时间内进行提交,若设定时间内事务未完成提交,则对事务进行回滚。默认值为-1,设置时间以秒为单位
@Service
@Transactional(timeout = 5)
public class TransferRecordService {
    @Autowired
    private TransferRecordDao transferRecordDao;

    public void transferAccounts(int amount, String fromUser, String toUser) {
        transferRecordDao.transferOut(amount, fromUser);
        //模拟处理超时
        try {
            Thread.sleep(6000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        transferRecordDao.transferIn(amount, toUser);
    }
}

设置超时时间后,执行测试代码,则日志信息会抛出TransactionTimedOutException事务超时异常

Exception in thread "main" org.springframework.transaction.TransactionTimedOutException: Transaction timed out: deadline was Wed Mar 09 21:30:33 CST 2022
    at org.springframework.transaction.support.ResourceHolderSupport.checkTransactionTimeout(ResourceHolderSupport.java:155)
    at org.springframework.transaction.support.ResourceHolderSupport.getTimeToLiveInMillis(ResourceHolderSupport.java:144)
    at org.springframework.transaction.support.ResourceHolderSupport.getTimeToLiveInSeconds(ResourceHolderSupport.java:128)
    at org.springframework.jdbc.datasource.DataSourceUtils.applyTimeout(DataSourceUtils.java:341)
...
  • readOnly:是否只读。

    • 默认值为false,表示读写操作都允许,可以进行增、删、改、查等操作;

    • 可设置为true,表示只允许读操作,即只能进行查询操作

@Service
@Transactional(readOnly = true)
public class TransferRecordService {
    //...
}

设置只读后,执行测试代码,则日志信息会抛出TransientDataAccessResourceException瞬态数据访问资源异常,同时还会抛出SQLException,并指出“连接是只读的,查询导致数据修改是不允许的”

Exception in thread "main" org.springframework.dao.TransientDataAccessResourceException: PreparedStatementCallback; SQL [update t_account set amount=amount-? where username=?]; Connection is read-only. Queries leading to data modification are not allowed; nested exception is java.sql.SQLException: Connection is read-only. Queries leading to data modification are not allowed
    ...
Caused by: java.sql.SQLException: Connection is read-only. Queries leading to data modification are not allowed
    ...
  • rollbackFor:设置出现哪些异常才进行回滚
@Service
@Transactional(rollbackFor = ArithmeticException.class)
public class TransferRecordService {
    @Autowired
    private TransferRecordDao transferRecordDao;

    public void transferAccounts(int amount, String fromUser, String toUser) {
        transferRecordDao.transferOut(amount, fromUser);
        //模拟网络异常而导致操作中断
        int i = 10 / 0;
        transferRecordDao.transferIn(amount, toUser);
    }
}

上述代码表明,只有在抛出的异常为ArithmeticException时,才会进行事务的回滚操作

此时运行测试代码,后台会抛出ArithmeticException异常,因此会进行回滚,转账过程不会成功

此时数据库中的数据,就不会有任何变化

  • noRollbackFor:设置出现哪些异常不进行回滚
@Service
@Transactional(noRollbackFor = ArithmeticException.class)
public class TransferRecordService {
    @Autowired
    private TransferRecordDao transferRecordDao;

    public void transferAccounts(int amount, String fromUser, String toUser) {
        transferRecordDao.transferOut(amount, fromUser);
        //模拟网络异常而导致操作中断
        int i = 10 / 0;
        transferRecordDao.transferIn(amount, toUser);
    }
}

因为设置了noRollbackFor = ArithmeticException.class,即表示抛出ArithmeticException异常时不会进行回滚

此时运行测试代码,后台会抛出ArithmeticException异常,但不会进行回滚,转账事务中只有出账操作会成功

Exception in thread "main" java.lang.ArithmeticException: / by zero

此时数据库中的数据,就会是下面情况(显然,这并不是我们想要的)

3.8、声明式事务(XML方式)

  • Step1、配置事务管理器
<!--配置事务管理器-->
<bean id="transactionManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
    <property name="dataSource" ref="dataSource"></property>
</bean>
  • Step2、配置事务通知
<!--1、配置事务通知-->
<tx:advice id="txAdvice">
    <tx:attributes>
        <tx:method name="transferAccounts" propagation="REQUIRED" isolation="REPEATABLE_READ" read-only="false"
                   timeout="5" rollback-for="java.lang.ArithmeticException"/>
    </tx:attributes>
</tx:advice>
  • Step3、配置切入点和切面
<!--2、配置切入点和切面-->
<aop:config>
    <aop:pointcut id="p"
                  expression="execution(* com.vectorx.spring5.s17_transaction_xml.service.TransferRecordService.*(..))"/>
    <aop:advisor advice-ref="txAdvice" pointcut-ref="p"></aop:advisor>
</aop:config>

Spring 配置文件整体内容

<?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"
       xmlns:tx="http://www.springframework.org/schema/tx"
       xmlns:aop="http://www.springframework.org/schema/aop"
       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
                           http://www.springframework.org/schema/tx http://www.springframework.org/schema/tx/spring-tx.xsd
                           http://www.springframework.org/schema/aop http://www.springframework.org/schema/aop/spring-aop.xsd">

    <!--配置dataSource-->
    <context:property-placeholder location="classpath:jdbc.properties"/>
    <bean id="dataSource" class="com.alibaba.druid.pool.DruidDataSource">
        <property name="driverClassName" value="${mysql.driverClassName}"/>
        <property name="url" value="${mysql.url}"/>
        <property name="username" value="${mysql.username}"/>
        <property name="password" value="${mysql.password}"/>
    </bean>

    <!--配置JdbcTemplate-->
    <bean id="jdbcTemplate" class="org.springframework.jdbc.core.JdbcTemplate">
        <!--属性注入dataSource-->
        <property name="dataSource" ref="dataSource"></property>
    </bean>

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

    <!--配置Dao创建和属性注入-->
    <bean id="transferRecordDao" class="com.vectorx.spring5.s17_transaction_xml.dao.TransferRecordDaoImpl">
        <property name="jdbcTemplate" ref="jdbcTemplate"></property>
    </bean>

    <!--配置Service创建和属性注入-->
    <bean id="transferRecordService" class="com.vectorx.spring5.s17_transaction_xml.service.TransferRecordService">
        <property name="transferRecordDao" ref="transferRecordDao"></property>
    </bean>

    <!--1、配置事务通知-->
    <tx:advice id="txAdvice">
        <tx:attributes>
            <tx:method name="transferAccounts" propagation="REQUIRED" isolation="REPEATABLE_READ" read-only="false"
                       timeout="5" rollback-for="java.lang.ArithmeticException"/>
        </tx:attributes>
    </tx:advice>

    <!--2、配置切入点和切面-->
    <aop:config>
        <aop:pointcut id="p"
                      expression="execution(* com.vectorx.spring5.s17_transaction_xml.service.TransferRecordService.*(..))"/>
        <aop:advisor advice-ref="txAdvice" pointcut-ref="p"></aop:advisor>
    </aop:config>
</beans>

对Service和Dao类去除注解,并对代码稍作修改

public class TransferRecordDaoImpl implements TransferRecordDao {
    private JdbcTemplate jdbcTemplate;

    public void setJdbcTemplate(JdbcTemplate jdbcTemplate) {
        this.jdbcTemplate = jdbcTemplate;
    }
    //...
}
public class TransferRecordService {
    private TransferRecordDao transferRecordDao;

    public void setTransferRecordDao(TransferRecordDao transferRecordDao) {
        this.transferRecordDao = transferRecordDao;
    }
    //...
}

*运行测试代码 *

后台结果

Exception in thread "main" java.lang.ArithmeticException: / by zero

数据库结果

3.9、完全注解开发

// 表示此类为配置类
@Configuration
// 自动扫描包
@ComponentScan(basePackages = "com.vectorx.spring5.s18_transaction_annotation")
// 开启事务
@EnableTransactionManagement
// 读取外部配置文件
@PropertySource(value = {"classpath:jdbc.properties"})
public class TxConfig {
    @Value(value = "${mysql.driverClassName}")
    private String driverClassName;
    @Value(value = "${mysql.url}")
    private String url;
    @Value(value = "${mysql.username}")
    private String username;
    @Value(value = "${mysql.password}")
    private String password;

    //配置dataSource
    @Bean
    public DruidDataSource getDruidDataSource() {
        DruidDataSource dataSource = new DruidDataSource();
        dataSource.setDriverClassName(driverClassName);
        dataSource.setUrl(url);
        dataSource.setUsername(username);
        dataSource.setPassword(password);
        return dataSource;
    }

    //配置JdbcTemplate
    @Bean
    public JdbcTemplate getJdbcTemplate(DataSource dataSource) {
        //IOC容器会根据类型找到对应DataSource
        JdbcTemplate jdbcTemplate = new JdbcTemplate();
        jdbcTemplate.setDataSource(dataSource);
        return jdbcTemplate;
    }

    //配置事务管理器
    @Bean
    public DataSourceTransactionManager getDataSourceTransactionManager(DataSource dataSource) {
        DataSourceTransactionManager transactionManager = new DataSourceTransactionManager();
        transactionManager.setDataSource(dataSource);
        return transactionManager;
    }
}

这里我们对各个注解进行一一说明

  • @Configuration:表示此类为一个配置类,其作用等同于创建一个bean.xml,即
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd">
</beans>
  • @ComponentScan:自动扫描包,basePackages属性配置为需要扫描的包路径,等价于<context:component-scan>标签,即
<!--开启注解扫描-->
<context:component-scan base-package="com.vectorx.spring5.s18_transaction_annotation"/>
<!--开启事务注解-->
<tx:annotation-driven transaction-manager="transactionManager"></tx:annotation-driven>
  • @PropertySource:引入外部文件,value配置外部文件路径,等价于<context:property-placeholder>标签
<context:property-placeholder location="classpath:jdbc.properties"/>
  • @Value:对普通类型的属性进行注入,同时其属性值可以使用${}表达式对外部文件配置信息进行获取

  • @Bean:配置对象创建,等价于<bean>标签。可以在被修饰的方法参数列表中传入受IOC容器管理的类型,IOC容器会自动根据类型找到对应对象并注入到此方法中。因此无论是配置JdbcTemplate还是配置事务管理器,都可以使用这种方式对外部Bean进行引用

//配置JdbcTemplate
@Bean
public JdbcTemplate getJdbcTemplate(DataSource dataSource) {...}
//配置事务管理器
@Bean
public DataSourceTransactionManager getDataSourceTransactionManager(DataSource dataSource) {...}

测试代码

需要注意的是,之前创建的对象是ClassPathXmlApplicationContext,而现在是完全注解开发,所以需要创建的对象是AnnotationConfigApplicationContext,构造参数中传入配置类的class类型即可,其他代码与之前一致

ApplicationContext context = new AnnotationConfigApplicationContext(TxConfig.class);
TransferRecordService transferRecordService = context.getBean("transferRecordService", TransferRecordService.class);
transferRecordService.transferAccounts(100, "Lucy", "Mary");

测试结果

小结

重点掌握

  • Spring事务管理两种方式:编程式事务管理(不推荐)、声明式事务管理(推荐)

  • Spring事务管理API:PlatformTransactionManagerDataSourceTrasactionManagerHibernateTransactionManager

  • 声明式事务两种实现方式:注解方式和XML方式

  • 事务相关参数有:传播行为、隔离级别、超时时间、是否只读、(不)回滚

  • 传播行为:有7种传播属性,REQUIREDREQUIRED_NEWSUPPORTSNOT_SOPPORTEDMANDATORYNEVERNESTED

  • 隔离级别:有3种典型“读”的问题,脏读、不可重复读、虚(幻)读,可设置4种隔离级别,READ UNCOMMITTEDREAD COMMITTEDREPEATABLE READSERIALIZABLE

  • 其他参数:timeoutreadOnlyrollbackFornoRollbackFor

  • 声明式事务(注解方式):@Transactional

  • 声明式事务(XML方式):配置事务管理器;配置事务通知<tx:advice>;配置切入点和切面

  • 完全注解开发:@EnableTransactionManagement@BeanAnnotationConfigApplicationContext

总结

  1. JdbcTemplateCRUD操作

  2. 事务ACID特性、Spring事务管理

  3. 声明式事务的注解方式和XML方式

  4. 事务相关属性:传播行为、隔离级别、其他参数

下面思维导图经供参考

05、Spring5新功能

1、Spring5框架新功能

  1. 代码整体基于 Java8,运行时兼容 Java9,代码库中删除了许多不建议使用的类和方法

  2. 自带了通用的日志封装:移除了Log4jConfigListener,官方建议使用Log4j2

  3. 核心容器支持@Nullable注解

  4. 核心容器支持函数式风格GenericApplicationContext

  5. 支持整合Junit 5单元测试

2、整合日志框架

  • 1)引入jar

  • 2)创建log4j2.xml配置文件,配置日志相关信息
<?xml version="1.0" encoding="UTF-8"?>
<!--日志级别以及优先级排序:OFF > FATAL > ERROR > WARN > INFO > DEBUG > TRACE > ALL -->
<!--Configuration后面的status用于设置log4j2自身内部的信息新出,可以不设置。当设置成trace时,可以看到log4j2内部各种详细输出-->
<configuration status="INFO">
    <!--先定义所有的appender-->
    <appenders>
        <!--输出日志信息到控制台-->
        <console name="Console" target="SYSTEM_OUT">
            <!--控制日志输出的格式-->
            <PatternLayout pattern="%d{yyyy-MM-dd HH:mm:ss.SSS} [%t] %-5level %logger{36} - %msg%n"/>
        </console>
    </appenders>
    <!--然后定义logger,只有定义了logger并引入appender,appender才会生效-->
    <!--root:用于指定项目的根日志,如果没有单独指定logger,则会使用root作为默认的日志输出-->
    <loggers>
        <root level="info">
            <appender-ref ref="Console"/>
        </root>
    </loggers>
</configuration>

随便一段代码,输入以下日志

2022-03-11 21:38:41.342 [main] INFO  com.alibaba.druid.pool.DruidDataSource - {dataSource-1} inited

如果将log4j2.xml配置文件级别从INFO改成DEBUG,则会打印更多的底层调用信息

2022-03-11 21:40:41,054 main DEBUG Apache Log4j Core 2.17.2 initializing configuration XmlConfiguration[location=D:\workspace\NOTE_Spring\Spring5_HelloWorld\out\production\Spring5_HelloWorld\log4j2.xml]
2022-03-11 21:40:41,061 main DEBUG PluginManager 'Core' found 127 plugins
2022-03-11 21:40:41,062 main DEBUG PluginManager 'Level' found 0 plugins
2022-03-11 21:40:41,066 main DEBUG PluginManager 'Lookup' found 16 plugins
2022-03-11 21:40:41,069 main DEBUG Building Plugin[name=layout, class=org.apache.logging.log4j.core.layout.PatternLayout].
2022-03-11 21:40:41,092 main DEBUG PluginManager 'TypeConverter' found 26 plugins
2022-03-11 21:40:41,112 main DEBUG PatternLayout$Builder(pattern="%d{yyyy-MM-dd HH:mm:ss.SSS} [%t] %-5level %logger{36} - %msg%n", PatternSelector=null, Configuration(D:\workspace\NOTE_Spring\Spring5_HelloWorld\out\production\Spring5_HelloWorld\log4j2.xml), Replace=null, charset="null", alwaysWriteExceptions="null", disableAnsi="null", noConsoleNoAnsi="null", header="null", footer="null")
2022-03-11 21:40:41,113 main DEBUG PluginManager 'Converter' found 45 plugins
2022-03-11 21:40:41,131 main DEBUG Building Plugin[name=appender, class=org.apache.logging.log4j.core.appender.ConsoleAppender].
2022-03-11 21:40:41,142 main DEBUG ConsoleAppender$Builder(target="SYSTEM_OUT", follow="null", direct="null", bufferedIo="null", bufferSize="null", immediateFlush="null", ignoreExceptions="null", PatternLayout(%d{yyyy-MM-dd HH:mm:ss.SSS} [%t] %-5level %logger{36} - %msg%n), name="Console", Configuration(D:\workspace\NOTE_Spring\Spring5_HelloWorld\out\production\Spring5_HelloWorld\log4j2.xml), Filter=null, ={})
2022-03-11 21:40:41,145 main DEBUG Starting OutputStreamManager SYSTEM_OUT.false.false
2022-03-11 21:40:41,145 main DEBUG Building Plugin[name=appenders, class=org.apache.logging.log4j.core.config.AppendersPlugin].
2022-03-11 21:40:41,148 main DEBUG createAppenders(={Console})
2022-03-11 21:40:41,149 main DEBUG Building Plugin[name=appender-ref, class=org.apache.logging.log4j.core.config.AppenderRef].
2022-03-11 21:40:41,155 main DEBUG createAppenderRef(ref="Console", level="null", Filter=null)
2022-03-11 21:40:41,156 main DEBUG Building Plugin[name=root, class=org.apache.logging.log4j.core.config.LoggerConfig$RootLogger].
2022-03-11 21:40:41,159 main DEBUG LoggerConfig$RootLogger$Builder(additivity="null", level="INFO", levelAndRefs="null", includeLocation="null", ={Console}, ={}, Configuration(D:\workspace\NOTE_Spring\Spring5_HelloWorld\out\production\Spring5_HelloWorld\log4j2.xml), Filter=null)
2022-03-11 21:40:41,163 main DEBUG Building Plugin[name=loggers, class=org.apache.logging.log4j.core.config.LoggersPlugin].
2022-03-11 21:40:41,164 main DEBUG createLoggers(={root})
2022-03-11 21:40:41,165 main DEBUG Configuration XmlConfiguration[location=D:\workspace\NOTE_Spring\Spring5_HelloWorld\out\production\Spring5_HelloWorld\log4j2.xml] initialized
2022-03-11 21:40:41,166 main DEBUG Starting configuration XmlConfiguration[location=D:\workspace\NOTE_Spring\Spring5_HelloWorld\out\production\Spring5_HelloWorld\log4j2.xml]
2022-03-11 21:40:41,166 main DEBUG Started configuration XmlConfiguration[location=D:\workspace\NOTE_Spring\Spring5_HelloWorld\out\production\Spring5_HelloWorld\log4j2.xml] OK.
2022-03-11 21:40:41,168 main DEBUG Shutting down OutputStreamManager SYSTEM_OUT.false.false-1
2022-03-11 21:40:41,169 main DEBUG OutputStream closed
2022-03-11 21:40:41,177 main DEBUG Shut down OutputStreamManager SYSTEM_OUT.false.false-1, all resources released: true
2022-03-11 21:40:41,178 main DEBUG Appender DefaultConsole-1 stopped with status true
2022-03-11 21:40:41,179 main DEBUG Stopped org.apache.logging.log4j.core.config.DefaultConfiguration@6e9175d8 OK
2022-03-11 21:40:41,249 main DEBUG Registering MBean org.apache.logging.log4j2:type=18b4aac2
2022-03-11 21:40:41,255 main DEBUG Registering MBean org.apache.logging.log4j2:type=18b4aac2,component=StatusLogger
2022-03-11 21:40:41,258 main DEBUG Registering MBean org.apache.logging.log4j2:type=18b4aac2,component=ContextSelector
2022-03-11 21:40:41,263 main DEBUG Registering MBean org.apache.logging.log4j2:type=18b4aac2,component=Loggers,name=
2022-03-11 21:40:41,266 main DEBUG Registering MBean org.apache.logging.log4j2:type=18b4aac2,component=Appenders,name=Console
2022-03-11 21:40:41,271 main DEBUG org.apache.logging.log4j.core.util.SystemClock does not support precise timestamps.
2022-03-11 21:40:41,278 main DEBUG Reconfiguration complete for context[name=18b4aac2] at URI D:\workspace\NOTE_Spring\Spring5_HelloWorld\out\production\Spring5_HelloWorld\log4j2.xml (org.apache.logging.log4j.core.LoggerContext@1b410b60) with optional ClassLoader: null
2022-03-11 21:40:41,279 main DEBUG Shutdown hook enabled. Registering a new one.
2022-03-11 21:40:41,285 main DEBUG LoggerContext[name=18b4aac2, org.apache.logging.log4j.core.LoggerContext@1b410b60] started OK.
Loading class `com.mysql.jdbc.Driver'. This is deprecated. The new driver class is `com.mysql.cj.jdbc.Driver'. The driver is automatically registered via the SPI and manual loading of the driver class is generally unnecessary.
2022-03-11 21:40:41.927 [main] INFO  com.alibaba.druid.pool.DruidDataSource - {dataSource-1} inited
...
2022-03-11 21:40:44,518 pool-1-thread-1 DEBUG Stopping LoggerContext[name=18b4aac2, org.apache.logging.log4j.core.LoggerContext@1b410b60]
2022-03-11 21:40:44,518 pool-1-thread-1 DEBUG Stopping LoggerContext[name=18b4aac2, org.apache.logging.log4j.core.LoggerContext@1b410b60]...
2022-03-11 21:40:44,521 pool-1-thread-1 DEBUG Shutting down OutputStreamManager SYSTEM_OUT.false.false
2022-03-11 21:40:44,522 pool-1-thread-1 DEBUG OutputStream closed
2022-03-11 21:40:44,522 pool-1-thread-1 DEBUG Shut down OutputStreamManager SYSTEM_OUT.false.false, all resources released: true
2022-03-11 21:40:44,522 pool-1-thread-1 DEBUG Appender Console stopped with status true
2022-03-11 21:40:44,523 pool-1-thread-1 DEBUG Stopped XmlConfiguration[location=D:\workspace\NOTE_Spring\Spring5_HelloWorld\out\production\Spring5_HelloWorld\log4j2.xml] OK
2022-03-11 21:40:44,523 pool-1-thread-1 DEBUG Stopped LoggerContext[name=18b4aac2, org.apache.logging.log4j.core.LoggerContext@1b410b60] with status true

上述配置都是自动进行日志输出,那如何手动进行日志输出呢?

这里需要用到org.slf4j.LoggerFactorygetLogger()方法,并将当前类型作为参数传入,就可以获取到一个org.slf4j.Logger对象

其中可以调用的日志级别有5种:tracedebuginfowarnerror,分别对应方法

  • logger.trace

  • logger.debug

  • logger.info

  • logger.warn

  • logger.error

测试代码,这里以常用的infowarnerror级别为例

public class TestLog {
    private static final Logger logger = LoggerFactory.getLogger(TestLog.class);

    public static void main(String[] args) {
        logger.info("This is info log test.");
        logger.warn("This is warn log test.");
        logger.error("This is error log test.");
    }
}

测试结果

2022-03-11 22:01:56.784 [main] INFO  com.vectorx.spring5.s19_log4j2.TestLog - This is info log test.
2022-03-11 22:01:56.789 [main] WARN  com.vectorx.spring5.s19_log4j2.TestLog - This is warn log test.
2022-03-11 22:01:56.789 [main] ERROR com.vectorx.spring5.s19_log4j2.TestLog - This is error log test.

3、@Nullable注解

@Nullable注解可以用在方法、属性和参数上面,分别表示方法可以为空、属性可以为空、参数可以为空

  • 1)@Nullable注解作用在方法上,方法返回值可以为空
@Nullable
String getId();
  • 2)@Nullable注解作用在属性上,属性值可以为空
@Nullable
private ResourceLoader resourceLoader;
  • 3)@Nullable注解作用在参数上,参数值可以为空
public <T> void registerBean(@Nullable String beanName, Class<T> beanClass, @Nullable Supplier<T> supplier, BeanDefinitionCustomizer... customizers) {
    this.reader.registerBean(beanClass, beanName, supplier, customizers);
}

4、函数式风格

函数式风格创建对象,交给 Spring 进行管理

//1、创建GenericApplicationContext对象
GenericApplicationContext context = new GenericApplicationContext();
//2、调用context的方法进行注册
context.refresh();
context.registerBean(User.class, () -> new User());
//3、获取Spring注册的对象
User user = context.getBean("user", User.class);
System.out.println(user);

运行结果:抛出了NoSuchBeanDefinitionException异常,No bean named 'user' available表明Spring容器中没有名为user的对象

org.springframework.beans.factory.NoSuchBeanDefinitionException: No bean named 'user' available
    at org.springframework.beans.factory.support.DefaultListableBeanFactory.getBeanDefinition(DefaultListableBeanFactory.java:872)
    at org.springframework.beans.factory.support.AbstractBeanFactory.getMergedLocalBeanDefinition(AbstractBeanFactory.java:1344)
    at org.springframework.beans.factory.support.AbstractBeanFactory.doGetBean(AbstractBeanFactory.java:309)
    at org.springframework.beans.factory.support.AbstractBeanFactory.getBean(AbstractBeanFactory.java:213)
    at org.springframework.context.support.AbstractApplicationContext.getBean(AbstractApplicationContext.java:1160)
    at com.vectorx.spring5.s20_functional.TestGenericApplicationContext.test1(TestGenericApplicationContext.java:16)

这是为什么呢?因为我们没有指定被注册对象的name,不像注解式编程中 Spring 会帮我们指定一个以类名首字母小写的name

//1、创建GenericApplicationContext对象
GenericApplicationContext context = new GenericApplicationContext();
//2、调用context的方法进行注册
context.refresh();
context.registerBean("user", User.class, () -> new User());
//3、获取Spring注册的对象
User user = context.getBean("user", User.class);
System.out.println(user);

运行结果

com.vectorx.spring5.s20_functional.User@4d95d2a2

还有一种方式,不在注册的时候指定name,而是获取的时候指明类的全路径,即使用类的全限定名

//1、创建GenericApplicationContext对象
GenericApplicationContext context = new GenericApplicationContext();
//2、调用context的方法进行注册
context.refresh();
context.registerBean(User.class, () -> new User());
//3、获取Spring注册的对象
User user = context.getBean("com.vectorx.spring5.s20_functional.User", User.class);
System.out.println(user);

运行结果

com.vectorx.spring5.s20_functional.User@4d95d2a2

5、整合Junit

5.1、整合Junit4

  • 1)引入相关的jar包:spring-testjunit4

  • 2)创建测试类,使用注解方式完成:@RunWith@ContextConfiguration@org.junit.Test
//单元测试框架
@RunWith(SpringJUnit4ClassRunner.class)
//加载配置文件
@ContextConfiguration("classpath:bean15.xml")
public class JTest4 {
    @Autowired
    private TransferRecordService transferRecordService;

    @Test
    public void test() {
        transferRecordService.transferAccounts(100, "Lucy", "Mary");
    }
}

5.2、整合Junit5

  • 1)引入相关的jar

  • 2)创建测试类,使用注解完成:@ExtendWith@ContextConfiguration@org.junit.jupiter.api.Test
//单元测试框架
@ExtendWith(SpringExtension.class)
//加载配置文件
@ContextConfiguration("classpath:bean15.xml")
public class JTest5 {
    @Autowired
    private TransferRecordService transferRecordService;

    @Test
    public void test() {
        transferRecordService.transferAccounts(100, "Lucy", "Mary");
    }
}

不过 Spring 中提供了一个更为简单的方式可以简化代码

  • 4)使用一个复合注解代替上述两个注解完成整合:@SpringJUnitConfig
//使用复合注解简化写法
@SpringJUnitConfig(locations = "classpath:bean15.xml")
public class JTest5 {
    @Autowired
    private TransferRecordService transferRecordService;

    @Test
       public void test() {
        transferRecordService.transferAccounts(100, "Lucy", "Mary");
    }
}

未完待续

课程中还介绍了SpringWebFlux模块,由于该知识点需要诸如SpringMVCSpringBoot等前置知识,所以此章节暂时先告一段落...

总结

Spring5 框架的新功能:

  • 支持整合日志框架log4j2

  • 自动记录日志:配置log4j2.xml

  • 手动记录日志:LoggerFactory.getLogger

  • 支持@Nullable注解

  • 支持函数式风格

  • 支持整合Junit

  • Junit4@RunWith@ContextConfiguration@org.junit.Test

  • Junit5@ExtendWith@ContextConfiguration@org.junit.jupiter.api.Test

  • 复合注解:@SpringJUnitConfig

导图仅供参考

标签:String,Spring,void,class,org,public
From: https://www.cnblogs.com/zhaostudy/p/16894537.html

相关文章