README
1、官网
-
Spring官网:Spring Framework
-
Github源码:GitHub - spring-projects/spring-framework: Spring Framework
-
Spring下载路径:JFrog (spring.io)
2、Spring5模块
笔记主要内容目前涵盖
-
核心容器:
Beans
、Core
、Context
、Expression
-
数据访问集成:
JDBC
、ORM
、Transactions
-
其他:
AOP
、Aspects
、Test
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 为快照版本,不稳定
- 确定好需要的版本后,点右上角 GitHub 图标,进入下载地址:GitHub - spring-projects/spring-framework: Spring Framework
- 找到
Access to Binaries
,点击进入 Spring Framework Artifacts
-
进入后,找到
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 name
、Project location
和Base 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
:依赖注入,就是注入属性(但需要在创建对象基础上进行)
IOC
和DI
的区别:DI
是IOC
的一种具体实现
两种注入方式: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>
-
属性值包含特殊符号:有两种方式
- 使用转义字符,如
<>
标识<>
- 使用转义字符,如
<!-- 字面量: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
的属性值进行覆盖,但前提是要有dept
的Getter
方法
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
根据属性名称注入,要求注入值bean
的id
值和类中属性名称一致 -
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**
的区别
singleton
和prototype
除了单实例和多实例的差别之外,还有以下区别
-
1)设置
scope
值是singleton
时,加载 Spring 配置文件时就会创建单实例对象 -
2)设置
scope
值是prototype
时,加载 Spring 配置文件时不会创建对象,而是在调用getBean
方法时创建多实例对象
**scope**
的其他值
scope
的属性值除了singleton
和prototype
之外,其实还有一些属性值,如
-
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
即不满足依赖异常,嵌套异常是NoUniqueBeanDefinitionException
即Bean定义不唯一异常,预期匹配单个 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)引入
context
和aop
名称空间 -
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...
小结
本节重点
-
AOP 概述
-
AOP 底层原理
-
AOP 术语
-
切入点表达式
-
AspectJ 实现
-
完全注解开发
以下总结仅供参考
04、 JdbcTemplate与声明式事务
1、JdbcTemplate
1.1、概述
前面我们已经学习了 Spring 中的Core Container
核心部分和AOP
、Aspects
等面向切面编程部分,接下来就是Data Access/Integration
即数据访问和集成部分
Spring 既可以单独使用,也可以集成其他框架,如Hibernate
、MyBatis
等。除此之外,其中对于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
层:与数据库进行交互
因此,我们搭建操作环境也按照典型的三层架构来实现,不过目前现阶段我们只关注Service
和Dao
两层
我们以银行转账为例,因为整个转账操作包括两个操作:出账的操作和入账的操作
过程概览
-
1)创建数据库表结构,添加几条记录
-
2)创建
Service
和Dao
类,完成对象创建和关系注入 -
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)**创建Service
和Dao
类,完成对象创建和关系注入
Service
中注入Dao
,Dao
中注入JdbcTemplate
,JdbcTemplate
中注入DataSource
Service
和Dao
类
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
实现类:JdbcTemplate
和MyBatis
框架使用到它 -
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"/>
- @EnableTransactionManagement:开启事务管理,等价于tx:annotation-driven标签,即
<!--开启事务注解-->
<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:
PlatformTransactionManager
、DataSourceTrasactionManager
、HibernateTransactionManager
-
声明式事务两种实现方式:注解方式和XML方式
-
事务相关参数有:传播行为、隔离级别、超时时间、是否只读、(不)回滚
-
传播行为:有7种传播属性,
REQUIRED
、REQUIRED_NEW
、SUPPORTS
、NOT_SOPPORTED
、MANDATORY
、NEVER
、NESTED
-
隔离级别:有3种典型“读”的问题,脏读、不可重复读、虚(幻)读,可设置4种隔离级别,
READ UNCOMMITTED
、READ COMMITTED
、REPEATABLE READ
、SERIALIZABLE
-
其他参数:
timeout
、readOnly
、rollbackFor
、noRollbackFor
-
声明式事务(注解方式):
@Transactional
-
声明式事务(XML方式):配置事务管理器;配置事务通知
<tx:advice>
;配置切入点和切面 -
完全注解开发:
@EnableTransactionManagement
、@Bean
、AnnotationConfigApplicationContext
总结
-
JdbcTemplate
的CRUD
操作 -
事务
ACID
特性、Spring事务管理 -
声明式事务的注解方式和XML方式
-
事务相关属性:传播行为、隔离级别、其他参数
下面思维导图经供参考
05、Spring5新功能
1、Spring5框架新功能
-
代码整体基于 Java8,运行时兼容 Java9,代码库中删除了许多不建议使用的类和方法
-
自带了通用的日志封装:移除了
Log4jConfigListener
,官方建议使用Log4j2
-
核心容器支持
@Nullable
注解 -
核心容器支持函数式风格
GenericApplicationContext
-
支持整合
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.LoggerFactory
中getLogger()
方法,并将当前类型作为参数传入,就可以获取到一个org.slf4j.Logger
对象
其中可以调用的日志级别有5种:trace
、debug
、info
、warn
、error
,分别对应方法
-
logger.trace
-
logger.debug
-
logger.info
-
logger.warn
-
logger.error
测试代码,这里以常用的info
、warn
和error
级别为例
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-test
和junit4
- 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
模块,由于该知识点需要诸如SpringMVC
、SpringBoot
等前置知识,所以此章节暂时先告一段落...
总结
Spring5 框架的新功能:
-
支持整合日志框架
log4j2
-
自动记录日志:配置
log4j2.xml
-
手动记录日志:
LoggerFactory.getLogger
-
支持
@Nullable
注解 -
支持函数式风格
-
支持整合
Junit
-
Junit4
:@RunWith
、@ContextConfiguration
、@org.junit.Test
-
Junit5
:@ExtendWith
、@ContextConfiguration
、@org.junit.jupiter.api.Test
-
复合注解:
@SpringJUnitConfig
导图仅供参考