Spring5
整体内容介绍
1、Spring框架概述
2、IOC容器
3、Aop
4、JdbcTemplate
5、事务管理
6、Spring5新特性
一、Spring框架概述
内容概述
1、Spring是一个轻量级的开源的JavaEE框架
2、Spring可以解决企业应用开发的复杂性
3、Spring有两个核心部分:IOC和Aop
(1) IOC:控制反转,把创建对象的过程交给Spring来管理
(2) Aop:面向切面,在不修改源代码的情况下进行功能的增加
4、Spring特点:
(1) 方便解耦,简化开发
(2) Aop编程支持
(3) 方便程序测试
(4) 方便和其它框架进行整合
(5) 方便进行事务操作
(6) 降低API的开发难度
(7) Spring的源码是Java中的经典学习规范
5、学习Spring5.x版本
下载方式:
1、打开浏览器输入"spring.io"进入官网,找到"Projects"并点击其中的"Spring Framework"
2、点击"LEARN",查看最新稳定版本,而后点击"小猫"进入GitHub
3、进入GitHub网站,向下翻找,找到"Access to Binaries" 并点击其中的"Spring Framework Artifacts"
4、进入页面后下滑至底部而后点击"https://repo.spring.io"的地址(后续可直接通过此网址进行下载)
5、按照如下图所示步骤依次点击选择(或在第四步中展开srping进行版本选择)
6、找到我们需要的版本号进行下载
7、解压
下载地址:https://repo.spring.io/release/org/springframework/spring/
入门案例
1、创建一个项目
2、导入最基础的5个jar包
所需要的最基础的5个jar包:
spring-beans-xxx.jar (spring自带jar包)
spring-context-xxx.jar (spring自带jar包)
spring-core-xxx.jar (spring自带jar包)
spring-expression-xxx.jar (spring自带jar包)
commons-logging-xxx.jar (自行手动下载)
3、新建一个类
public class User {
public void add(){
System.out.println("这是add方法");
}
}
其结构如下:
4、创建该类的对象
(1) 使用以前的方法(使用new关键字创建对象)
User user = new User();
(2) 使用Spring创建对象
1.创建Spring配置文件,在配置文件中配置我们想要创建的对象
<1> Spring配置文件使用.xml格式
<2> 配置文件初始内容
<3> 我们添加的内容
<!--配置User对象-->
<!--
使用<bean></bean>完成配置
id="" 是我们给其取的名字,理论上符合命名规范即可随意
class="" 指的是此配置所指向的类所在的位置
-->
<bean id="user" class="com.hassder.spring.User"></bean>
2.编写测试代码
import com.hassder.spring.User;
import org.junit.Test;
import org.springframework.context.ApplicationContext;
import org.springframework.context.support.ClassPathXmlApplicationContext;
public class TestSpring {
@Test
public void testAdd(){
// 加载spring配置文件
ApplicationContext context =
new ClassPathXmlApplicationContext("bean1.xml");
// 获取配置创建的对象
User user = context.getBean("user", User.class);
System.out.println(user);
user.add();
}
}
测试结果如下:
这说明我们成功创建出了一个User的对象
二、IOC容器
IOC概念和原理
1、什么叫IOC
(1) 控制反转,把对象的创建和对象之间的调用过程都交给Spring管理
(2) 使用IOC的目的:为了降低耦合度
概述中的入门案例就是使用IOC来实现的
2、IOC底层原理
(1) xml解析、工厂模式、反射
3、IOC解析
假设我们有两个类UserA与UserB,UserA中拥有方法a,UserB中拥有方法b,我们要如何在方法a中调用方法b呢?
两个类的代码如下:
// UserA :
class UserA{
a(){
}
}
// UserB :
class UserB{
b(){
}
}
(1) 原始方式
在最基础的方式中,我们可以通过直接在方法a中构建类UserB的对象,而后通过这个对象来调用方法b,代码如下:
// UserA :
class UserA{
a(){
UserB userb = new UserB();
userb.b();
}
}
// UserB :
class UserB{
b(){
}
}
这种方式的代码耦合度太高
(2) 工厂模式
在工厂模式中,我们不直接在方法a中创建UserB的对象,而是通过一个"工厂"来完成,我们创建第三个类用于完成此项工作
// 工厂类 UserFactory
class UserFactory{
public static UserB getUserB(){
return new UserB();
}
}
那么我们只需要在方法a中直接调用工厂类中的静态方法getUserB即可
// UserA :
class UserA{
a(){
UserB userb = UserFactory.getUserB();
userb.b();
}
}
// UserB :
class UserB{
b(){
}
}
使用这种方式看似降低了UserA与UserB之间的耦合度,但是却增加了UserFactory与UserA、UserB之间的耦合,并没有完全达到我们想要的最大限度的降低耦合度的愿望
(3)使用IOC原理
IOC过程:
第一步:xml配置文件,配置创建的对象(前提:UserA与UserB已存在)
<!--id="唯一标识" class="路径"-->
<bean id = "UserB" class ="com.hassder.spring.UserB" ></bean>
第二步:创建工厂类,通过xml配置文件和反射机制来实现
class Factory{
public static UserB getUserB(){
// 1、xml解析
String classValue = class属性值; // 等于xml配置文件中的对应的class属性值
// 2、通过反射创建对象
// 2.1、通过Class.forName(路径)方法得到对应的Class
Class classB = Class.forName(classValue);
// 2.2、返回通过classB生成的新对象
return (UserB) classB.newInstance();
}
}
IOC接口(BeanFactory)
1、IOC思想基于IOC容器完成,IOC容器底层就是对象工厂
2、Spring提供IOC容器实现的两种方式(两个接口)
(1) BeanFactory:IOC容器基本实现,是Spring内部的使用接口,一般不提供给开发人员使用
BeanFactory在加载配置文件时不会创建对象,而在我们使用(获取)对象的时候才去创建对象
(2) ApplicationContext: 是BeanFactory的一个子接口,提供更多更强大的接口,一般由开发人员进行使用
ApplicationContext在加载配置文件时就会把在配置文件里的对象创建出来
3、ApplicationContest接口有实现类
主要的实现类:
区别:使用FileSystemXmlApplicationContext时需要写出全路径(从盘符开始)
使用ClassPathXmlApplicationContext时使用项目下的路径(从src开始的路径)
IOC操作Bean管理
Bean基本信息
Spring中有两种类型的Bean, 一种普通Bean, 一种工厂Bean(FactoryBean)
普通Bean: 在配置文件中定义Bean类型就是返回类型
工厂Bean: 在配置文件定义Bean类型可以和返回类型不一样
Bean的作用域
-
在Spring里设置创建Bean实例是单实例还是多实例(默认创建为单实例对象)
验证:在示例中使用一个(以Book为例),在不改变任何配置信息的情况下,修改测试类为如下:
@Test public void testBook(){ ApplicationContext applicationContext = new ClassPathXmlApplicationContext("com/hassder/spring02/bean2.xml"); Book book1 = applicationContext.getBean("book", Book.class); Book book2 = applicationContext.getBean("book", Book.class); System.out.println(book1); System.out.println("--------------------------"); System.out.println(book2); }
在默认(无额外配置说明)情况下创建的应该是单实例对象, 运行此测试方法查看输出结果如下(为了输出对象地址,暂时去掉了toString()方法):
可以看到,我们虽然创建了两个分别名为book1与book2的对象,但是这两个对象的地址是相同的,说明这两个对象实际上是一个对象
-
如何设置为多实例
在spring配置文件bean标签里有scope属性用于设置单实例还是多实例
scope属性值: 默认值: singleton : 表示单实例对象, 没有手动设置scope属性时,默认为此值;prototype : 表示多实例对象,想要设置为多实例,需要手动在bean标签内添加scope属性并将其值设置为prototype
singleton与prototype的区别:设置为singleton或者不设置时, 对象会在加载配置文件的时候被创建
而设置为prototype时,则要在调用getBean()方法的时候才会创建对应的对象
测试:在Book类的配置信息bean标签里添加scope=“prototype”,而后使用上述测试方法查看结果
<bean id="book" class="com.hassder.spring02.collectiontype.Book" scope="prototype"> <property name="list" ref="bookList" ></property> </bean>
Bean的生命周期
什么是生命周期:
一个对象/一个实例/一个方法/一个属性等从创建到销毁的过程
Bean的生命周期
-
通过构造器创建bean实例(无参构造)
-
为bean的属性设置值和对其他bean引用(调用set方法)
-
调用bean的初始化的方法(需要进行配置)
-
bena可以使用了(对象获取到了)
-
当容器关闭的时候,调用bean的销毁的方法(需要进行配置)
销毁方法:
演示Bean的生命周期
创建演示类Olders:
package com.hassder.bean;
public class Olders {
private String oname ;
public Olders() {
System.out.println("第一步,执行了Olders的无参构造方法");
}
public Olders(String oname) {
this.oname = oname;
}
public void setOname(String oname) {
this.oname = oname;
System.out.println("第二步,调用了set方法设置属性值");
}
// 创建执行的初始化方法
// 这只是一个普通方法, 需要在xml配置文件中进行配置才能变成初始化方法
// 在配置文件中的bean标签内使用init-method属性,并将其值设置为此方法名(无需加括号)
public void initMethod(){
System.out.println("第三步,执行初始化方法");
}
// 创建执行的销毁的方法
// 与初始化方法一样,需要在xml配置文件中进行配置
// 在配置文件中的bean标签内使用destroy-method属性,并将其值设置为此方法名(无需加括号)
public void destoryMethod(){
System.out.println("第五步,执行销毁方法");
}
}
配置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">
<bean id="olders" class="com.hassder.bean.Olders" init-method="initMethod" destroy-method="destoryMethod">
<property name="oname" value="Bean..."/>
</bean>
</beans>
测试方法:
public void testOlders(){
ApplicationContext applicationContext =
new ClassPathXmlApplicationContext("com/hassder/bean/bean1.xml");
Olders olders = applicationContext.getBean("olders",Olders.class);
System.out.println("第四步,创建获取=bean实例对象");
// 手动让bean实例销毁
((ClassPathXmlApplicationContext)applicationContext).close();
}
测试结果:
从测试结果中我们可以看到: 五个步骤是依次执行的
补充说明: 在我们手动调用close()方法销毁实例时,由于在ApplicationContext接口中并不存在close()方法,所以将其强转为它的实现类ClassPathXmlApplicationContext(因为在ClassPathXmlApplicationContext实现类中是存在这个方法的). 若要直观表达,可以使用如下形式的测试方法,其结果是相同的:
@Test
public void testOlders(){
ClassPathXmlApplicationContext applicationContext =
new ClassPathXmlApplicationContext("com/hassder/bean/bean1.xml");
Olders olders = applicationContext.getBean("olders",Olders.class);
System.out.println("第四步,创建获取=bean实例对象");
// 手动让bean实例销毁
applicationContext.close();
}
Bean的后置处理器
Bean的时候生命周期共有七步,除了上述五步之外,还有后置处理器的两步
- 通过构造器创建bean实例(无参构造)
- 为bean的属性设置值和对其他bean引用(调用set方法)
- 把bean实例传递给bean后置处理器的postProcessBeforeInitialization()方法
- 调用bean的初始化的方法(需要进行配置)
- 把bean实例传递给bean后置处理器的postProcessAfterInitialization()方法
- bena可以使用了(对象获取到了)
- 当容器关闭的时候,调用bean的销毁的方法(需要进行配置)
演示添加后置处理器效果
-
创建类,实现接口,并重写其中的两个方法:
package com.hassder.bean; import org.springframework.beans.BeansException; import org.springframework.beans.factory.config.BeanPostProcessor; public class MyBeanPost implements BeanPostProcessor { @Override public Object postProcessBeforeInitialization(Object bean,String beanName) throws BeansException { System.out.println("调用了postProcessBeforeInitialization方法"); return bean; } @Override public Object postProcessAfterInitialization(Object bean,String beanName) throws BeansException { System.out.println("调用了postProcessAfterInitialization方法"); return 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"> <bean id="olders" class="com.hassder.bean.Olders" init-method="initMethod" destroy-method="destoryMethod"> <property name="oname" value="Bean..."/> </bean> <!--配置后置处理器--> <!--只需要这样配置即可,因为在MyBeanPost类中实现了BeanPostProcessor接口, 所以当加载到这一步时就会自动将其作为后置处理器--> <!--其作用范围为当前bean.xml文件内所有配置的类--> <bean id="myBeanPost" class="com.hassder.bean.MyBeanPost"/> </beans>
-
运行结果:
工厂Bean
第一步 创建类, 让这个类作为工厂Bean, 实现接口FactoryBean
第二步 实现接口里的方法, 在实现的方法中定义返回的Bean类型
MyBean类:
package com.hassder.spring02.factorybean;
import com.hassder.spring02.collectiontype.Course;
import org.springframework.beans.factory.FactoryBean;
public class MyBean implements FactoryBean<Course> {
//定义返回Bean
@Override
public Course getObject() throws Exception {
Course course = new Course();
course.setName("abc");
return course;
}
@Override
public Class<?> getObjectType() {
return null;
}
@Override
public boolean isSingleton() {
return false;
}
}
配置文件:
<?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">
<bean id="myBean" class="com.hassder.spring02.factorybean.MyBean"></bean>
</beans>
测试方法:
@Test
public void testMyBean(){
ApplicationContext applicationContext =
new ClassPathXmlApplicationContext("com/hassder/spring02/bean3.xml");
Course course = applicationContext.getBean("myBean", Course.class);
System.out.println(course.toString());
}
可以看到我们在测试时用来接收的类型是Course而不是MyBean
普通Bean
1、什么是Bean管理
(1) Spring创建对象
(2) Spring注入属性
2、Bean管理操作有两种方式
(1) 基于xml配置文件方式实现
(2) 基于注解方式实现
基于xml实现
1、基于xml方式创建对象
(1) 在spring配置文件中,使用bean标签,标签里添加对应属性,就可以实现对象创建
(2) 在bean标签里有很多属性,其中的常用属性有:
id属性: 唯一标识
class属性: 创建对象所在类的全路径(包类路径)
name属性:作用与id属性类似,区别在于id属性所取的id名不能含有特殊字符,而name属性中可以使用特殊字符
(3) 创建对象的时候,默认使用无参构造方法
2、基于xml方式注入属性
使用xml方式注入普通属性
(1) DI: 依赖注入,就是注入属性
第一种注入方式: 通过set方法注入:
public class Book {
private String bName ;
public void setbName(String bName) {
this.bName = bName;
}
public static void main(String[] args) {
Book book = new Book();
// 通过set方法注入属性
book.setbName("十万个为什么");
}
}
第二种注入方式: 通过有参构造注入:
public class Book {
private String bName ;
public Book(String bName) {
this.bName = bName;
}
public static void main(String[] args) {
// 通过有参构造注入
Book book = new Book("十万个为什么");
}
}
(2) spring注入属性:
第一种注入方式: set方法注入:
xml配置:
<!-- 一、配置Book对象-->
<bean id="book" class="com.hassder.spring.Book">
<!-- 二、在bean标签内使用property标签完成属性注入
name: 类里面的属性名称(变量名)
value: 向该属性注入的值
-->
<property name="bName" value="十万个为什么"></property>
<property name="bAuthor" value="不知道"></property>
</bean>
第二种注入方式: 有参构造注入
类结构:
public class Orders {
private String oName;
private String oAddress;
public Orders(String oName, String oAddress) {
this.oName = oName;
this.oAddress = oAddress;
}
public void testOrders(){
System.out.println(oName+"--"+oAddress);
}
}
xml配置:
<bean id="Orders" class="com.hassder.spring.testdemo.Orders">
<constructor-arg name="oName" value="电脑"></constructor-arg>
<constructor-arg name="oAddress" value="湖南"></constructor-arg>
<!-- 还有另一个属性value可以使用: value表示 属性在构造方法参数列表中的索引
<constructor-arg value="0" value="电脑"></constructor-arg>
<constructor-arg value="1" value="湖南"></constructor-arg>
-->
</bean>
p名称空间注入(简化)
使用p名称空间注入,可以简化基于xml的配置方式
第一步: 在配置文件中添加p名称空间
<?xml version="1.0" encoding="UTF-8"?>
<!-- 添加一个xmlns: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标签里进行操作
<bean id="book" class="com.hassder.spring.Book" p:bName="十万个为什么" p:bAuthor="不知道">
</bean>
使用xml方式注入特殊类型属性(如空值,特殊符号,对象等)
1、字面量
(1) null值
以Book类为例,要将Book类中的bAuthor设置为空值,则:
<!-- 设置为null值-->
<property name="bAuthor">
<null/>
</property>
(2) 属性值包含特殊符号
例如:
<property name="bAuthor" value="<<aa>>"></property>
在value中直接带有特殊符号会导致报错
解决方法:
<1> 将特殊符号进行转义(略)
<2> 使用CDATA: <![CDATA[包含特殊符号的值]]>,即使用<![CDATA[与]]>将含有特殊符号的值包裹起来,达到一个告诉系统"这里面的值不需要编译,你只要原样放进去就行了"的效果
<property name="bAuthor">
<value><![CDATA[<<AA>>]]></value>
</property>
2、注入属性-外部bean
代码结构如下:
package com.hassder.spring.dao;
public class UserDaoImpl implements UserDao{
@Override
public void update() {
System.out.println("dao update.....");
}
}
package com.hassder.spring.service;
public class UserService{
public void add() {
System.out.println("service add--------");
}
}
如何在service中调用dao中的方法?
普通方式实现:
package com.hassder.spring.service;
import com.hassder.spring.dao.UserDao;
import com.hassder.spring.dao.UserDaoImpl;
public class UserService{
public void add() {
System.out.println("service add--------");
// 原始方式: 创建UserDao对象
// 代码改动如下
UserDao userDao = new UserDaoImpl();
userDao.update();
}
}
配置文件方式实现:
在UserService类中创建UserDao属性:
import com.hassder.spring.dao.UserDao;
public class UserService{
// 创建UserDao类型属性,生成set方法
private UserDao userDao;
public void setUserDao(UserDao userDao) {
this.userDao = userDao;
}
public void add() {
System.out.println("service add--------");
userDao.update();
}
}
通过xml配置文件为userService对象注入属性值(userDao对象):
<!--1 dao和service对象创建-->
<bean id="userDaoImpl_1" class="com.hassder.spring.dao.UserDaoImpl"></bean>
<bean id="userService" class="com.hassder.spring.service.UserService">
<!--注入userDao对象
name属性: 类里面的属性名称
ref属性: 创建userDao对象性的bean标签的id值
-->
<property name="userDao" ref="userDaoImpl_1"></property>
</bean>
测试类:
public class TestBean {
@Test
public void testBean(){
//1 加载配置文件
ApplicationContext applicationContext =
new ClassPathXmlApplicationContext("bean2.xml");
//2 获取配置创建的对象
UserService userService = applicationContext.getBean("userService",UserService.class);
userService.add();
}
}
3、注入属性-内部bean和级联赋值
public class TestBean2 {
@Test
public void testBean2(){
ApplicationContext applicationContext =
new ClassPathXmlApplicationContext("bean3.xml");
Emp emp = applicationContext.getBean("emp", Emp.class);
System.out.println(emp);
System.out.println(emp.getDept());
emp.test();
}
}
(1) 一对多关系: 部门和员工
一个部门里有多个员工,而一个员工只属于一个部门
(2) 在实体类中表示一对多关系,在员工类中使用对象类型的属性来表示其所属的部门
package com.hassder.spring.bean;
// 部门类
public class Dept {
private String dname ;
public void setDname(String dname) {
this.dname = dname;
}
public String getDname() {
return dname;
}
}
package com.hassder.spring.bean;
// 员工类
public class Emp {
private String ename ;
private String gender;
// 员工属于某一个部门,使用部门对象来表示
private Dept dept ;
public void setDept(Dept dept) {
this.dept = dept;
}
public void setEname(String ename) {
this.ename = ename;
}
public void setGender(String gender) {
this.gender = gender;
}
public Dept getDept() {
return dept;
}
// 用于测试的测试方法
public void test(){
System.out.println(ename+"---"+gender+"---"+dept.getDname());
}
}
(2) 在spring配置文件中进行相关配置
在一个bean的内部使用另一个bean,通过"嵌套"的方式实现
<!--内部bean-->
<!--配置Emp对象 -->
<bean id="emp" class="com.hassder.spring.bean.Emp">
<!--设置两个普通属性-->
<property name="ename" value="lucy"></property>
<property name="gender" value="女"></property>
<!--设置对象类型属性-->
<property name="dept" >
<bean id="dept" class="com.hassder.spring.bean.Dept">
<property name="dname" value="安保部"></property>
</bean>
</property>
</bean>
测试类:
public class TestBean2 {
@Test
public void testBean2(){
ApplicationContext applicationContext =
new ClassPathXmlApplicationContext("bean3.xml");
Emp emp = applicationContext.getBean("emp", Emp.class);
System.out.println(emp);
System.out.println(emp.getDept());
emp.test();
}
}
4、注入属性-级联赋值
使用3中部门与员工的案例进行说明
(1) 第一种写法
<bean id="emp" class="com.hassder.spring.bean.Emp">
<!--设置两个普通属性-->
<property name="ename" value="lucy"></property>
<property name="gender" value="女"></property>
<!--设置对象类型属性-->
<property name="dept" ref="dept"></property>
</bean>
<bean id="dept" class="com.hassder.spring.bean.Dept">
<property name="dname" value="安保部"></property>
</bean>
(2) 第二种写法
<bean id="emp" class="com.hassder.spring.bean.Emp">
<!--设置两个普通属性-->
<property name="ename" value="lucy"></property>
<property name="gender" value="女"></property>
<!--设置对象类型属性-->
<property name="dept" ref="dept"></property>
<property name="dept.dname" value="财务部"></property>
</bean>
<bean id="dept" class="com.hassder.spring.bean.Dept">
<property name="dname" value="安保部"></property>
</bean>
注意: 在这种写法中,实际生成的dept对象的dname值是"财务部"而不是"安保部"
且若要使用这种写法,则必须在Emp类中生成Dept属性的get方法
5、注入属性-注入集合类型属性
示例类代码:
package com.hassder.spring02.collectiontype;
import java.lang.reflect.Array;
import java.util.Arrays;
import java.util.List;
import java.util.Map;
import java.util.Set;
public class Stu {
private String[] courses ;
private List<String> list ;
private Map<String,String> maps ;
private Set<String> sets ;
public void setCourses(String[] courses) {
this.courses = courses;
}
public void setList(List<String> list) {
this.list = list;
}
public void setMaps(Map<String, String> maps) {
this.maps = maps;
}
public void setSets(Set<String> sets) {
this.sets = sets;
}
// 测试方法 用于测试时调用,以判断是否注入成功
public void test(){
System.out.println(Arrays.toString(courses));
System.out.println(list);
System.out.println(maps);
System.out.println(sets);
}
}
(1) 注入数组类型属性
(2) 注入List集合类型属性
(3) 注入Map集合类型属性
<!--1 集合类型属性注入-->
<bean id="stu" class="com.hassder.spring02.collectiontype.Stu">
<!--数组类型属性注入-->
<property name="courses">
<array>
<value>Java课程</value>
<value>数据库课程</value>
<value>Python课程</value>
<value>C语言课程</value>
</array>
</property>
<!--list类型属性注入-->
<property name="list">
<list>
<value>张三</value>
<value>张小三</value>
</list>
</property>
<!--map类型属性注入-->
<property name="maps">
<map>
<entry key="JAVA" value="java"/>
<entry key="PHP" value="php"/>
</map>
</property>
<!--set类型属性注入-->
<property name="sets">
<set>
<value>MySQL</value>
<value>Redis</value>
</set>
</property>
</bean>
测试类:
public class TestStu {
@Test
public void testStu(){
ApplicationContext applicationContext =
new ClassPathXmlApplicationContext("com/hassder/spring02/bean1.xml");
Stu stu = applicationContext.getBean("stu",Stu.class);
stu.test();
}
}
(4) 在集合内设置对象类型值
新建一个Course类
package com.hassder.spring02.collectiontype;
public class Course {
private String name ;
public void setName(String name) {
this.name = name;
}
// 为方便测试 重写其toString方法
@Override
public String toString() {
return "Course{" +
"name='" + name + '\'' +
'}';
}
}
在原Stu类中新增一Course类型的List属性,并为其生成set方法
private List<Course> courseList ;
public void setCourseList(List<Course> courseList) {
this.courseList = courseList;
}
<!--1 集合类型属性注入-->
<bean id="stu" class="com.hassder.spring02.collectiontype.Stu">
<!--注入对象类型集合-->
<property name="courseList">
<list>
<ref bean="course1"/>
<ref bean="course2"/>
</list>
</property>
</bean>
<!--创建多个course对象-->
<bean name="course1" class="com.hassder.spring02.collectiontype.Course">
<property name="name" value="Java基础"/>
</bean>
<bean name="course2" class="com.hassder.spring02.collectiontype.Course">
<property name="name" value="JavaEE"/>
</bean>
(5) 把集合注入部分提取出来
<1> 在spring配置文件中引入名称空间util
初始配置:
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd">
</beans>
改后配置:
<?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:p="http://www.springframework.org/schema/p"
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>
<2> 使用util标签完成list集合注入抽取
如图所示, list、map、set等各种类型都可以提取,此处仅以list为例,其它类型办法类似
<!--1 提取list集合类型属性注入-->
<util:list id="bookList">
<value>JAVA基础</value>
<value>JAVA进阶</value>
<!--如果是对象类型的集合,则可以使用这种方式(与上面类似):<ref bean=""></ref>-->
</util:list>
<!--2 提取list集合类型属性注入使用-->
<bean id="book" class="com.hassder.spring02.collectiontype.Book">
<property name="list" ref="bookList"></property>
</bean>
测试方法:
@Test
public void testBook(){
ApplicationContext applicationContext =
new ClassPathXmlApplicationContext("com/hassder/spring02/bean2.xml");
Book book = applicationContext.getBean("book", Book.class);
System.out.println(book.toString());
}
基于xml方式实现自动装配
什么是自动装配
(1) 根据指定的装配规则(基于属性名称或者属性类型),Spring自动将匹配的属性值注入
演示自动装配过程
新建两个类与一个测试方法:
package com.hassder.autowire;
public class Dept {
private String name ;
public void setName(String name) {
this.name = name;
}
@Override
public String toString() {
return "Dept{" +
"name='" + name + '\'' +
'}';
}
}
package com.hassder.autowire;
public class Emp {
private Dept dept ;
public void setDept(Dept dept) {
this.dept = dept;
}
@Override
public String toString() {
return "Emp{" +
"dept=" + dept +
'}';
}
// 用于测试
public void test(){
System.out.println(dept);
}
}
@Test
public void testAutowite(){
ApplicationContext applicationContext =
new ClassPathXmlApplicationContext("com/hassder/autowire/bean1.xml");
Emp emp = applicationContext.getBean("emp",Emp.class);
System.out.println(emp.toString());
emp.test();
}
(1) 基于属性名称
<?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">
<!--手动使用外部bean-->
<!--<bean id="emp" class="com.hassder.autowire.Emp">
<property name="dept" ref="dept"/>
</bean>
<bean id="dept" class="com.hassder.autowire.Dept">
<property name="name" value="姓名"/>
</bean>-->
<!--实现自动装配-->
<!--bean标签属性autowire 配置自动装配
autowite属性常用两个值:
byName是根据属性名称注入 若要使用byName方式注入,则bean配置内的id值要与对应类中属性名称一样,否则无法注入
byType是根据类型注入
-->
<bean id="emp" class="com.hassder.autowire.Emp" autowire="byName"/>
<bean id="dept" class="com.hassder.autowire.Dept">
<property name="name" value="姓名"/>
</bean>
</beans>
(2) 基于属性类型
基于类型时只需要将上述autowire属性中的值改为byType即可
但是,若要使用byType则需要注意,在一个配置文件中,不能创建多个需要自动装配的属性类型的bean,否则会产生错误(存在多个同一类型的bean 不知道该使用哪一个)
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd">
<!--手动使用外部bean-->
<!--<bean id="emp" class="com.hassder.autowire.Emp">
<property name="dept" ref="dept"/>
</bean>
<bean id="dept" class="com.hassder.autowire.Dept">
<property name="name" value="姓名"/>
</bean>-->
<!--实现自动装配-->
<!--bean标签属性autowire 配置自动装配
autowite属性常用两个值:
byName是根据属性名称注入 若要使用byName方式注入,则bean配置内的id值要与对应类中属性名称一样,否则无法注入
byType是根据类型注入
-->
<bean id="emp" class="com.hassder.autowire.Emp" autowire="byType"/>
<bean id="dept" class="com.hassder.autowire.Dept">
<property name="name" value="姓名"/>
</bean>
</beans>
引入外部属性文件
1、 直接配置数据库信息
(1) 配置德鲁伊连接池
<bean id="dataSource" class="com.alibaba.druid.pool.DruidDataSource">
<property name="driverClassName" value="com.mysql.jdbc.Driver"/>
<property name="url" value="jdbc:mysql://localhost:3306/userDb"/>
<property name="username" value="root"/>
<property name="password" value="123456"/>
</bean>
2、 引入外部属性文案配置数据库连接池
(1) 创建外部属性文件, properties格式文件, 在里面写数据库信息
prop.driverClass=com.mysql.jdbc.Driver
prop.url=jdbc:mysql://localhost:3306/userDb
prop.username=root
prop.password=123456
理论上等号左边可以写任意值,但是为了避免与其它内容产生冲突,可以在前面加上prop. 或者其他值
(2) 把外部properties属性文件引入到spring配置文件中
<1> 引入context名称空间
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:context="http://www.springframework.org/schema/context"
xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context.xsd">
</beans>
<2> 在xml配置文件中, 引入外部属性文件
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:context="http://www.springframework.org/schema/context"
xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context.xsd">
<!--先引入外部属性文件-->
<context:property-placeholder location="classpath:jdbc.properties"/>
<!--配置连接池-->
<bean id="dataSource" class="com.alibaba.druid.pool.DruidDataSource">
<property name="driverClassName" value="${prop.driverClass}"/>
<property name="url" value="${prop.url}"/>
<property name="username" value="${prop.username}"/>
<property name="password" value="${prop.password}"/>
</bean>
</beans>
基于注解实现
什么是注解
注解是代码的特殊标记, 格式@注解名称(属性名称1=属性值1,属性名称2=属性值2…)
注解可以作用在类上,方法上,属性上
使用注解是为了简化xml配置, 使用注解或者注解+少量xml配置就可以代替大量的xml配置
引入依赖
若要使用注解,则除了最基础的5个jar包之外,还需要引入一个额外的spring-aop-xxx.jar包
1、基于注解方式创建对象
(1) @Component
(2) @Service
(3) @Controller
(4) @Repository
这四个注解的功能是一样的, 都可以用来创建bean实例, 只是不同的注解在实际开发中使用的位置不同, 便于开发人员更加清晰当前组件所扮演的角色. 但是并没有强制限制区分, 混用也是可以实现功能的, 为了规范应该尽量遵守在不同情况使用不同的注解
(1) 开启组件扫描
告诉spring容器我在哪里使用了注解,让容器到什么位置去找注解
先开启context名称空间(上面已经说过开启方式)
然后开启组件扫描:
开启组件扫描的细节设置:
<!--
表示扫描com.hassder.spring03下的所有注解
-->
<context:component-scan base-package="com.hassder.spring03"/>
<!--
细化扫描范围 1
use-default-filters="false" 表示不使用默认的filters (默认扫描所有)
而后自己设置扫描范围
type="annotation" 表示扫描注解
expression="org.springframework.stereotype.Controller" 表示只扫描注解名为Controller的注解
表示只扫描com.hassder.spring03包下的@Controller注解
-->
<context:component-scan base-package="com.hassder.spring03" use-default-filters="false">
<context:include-filter type="annotation"
expression="org.springframework.stereotype.Controller"/>
</context:component-scan>
<!--
与上面相反
下列配置表示扫描com.hassder.spring03包下除了Service之外的所有注解
-->
<context:component-scan base-package="com.hassder.spring03">
<context:exclude-filter type="annotation"
expression="org.springframework.stereotype.Service"/>
</context:component-scan>
(2) 创建类,在类上添加创建对象的注解
package com.hassder.spring03.service;
import org.springframework.stereotype.Component;
// 在注解里value属性值可以省略不写
// 不写的话calue属性的默认值为当前类名首字母小写
// 也可以主动使用()写出
// 在这里我们使用 @Component、@Service、@Controller、@Repository中的任一注解都是同样效果
@Component(value = "userService") // 相当于<bean id="userService" class="当前路径"/>
public class UserService {
public void add(){
System.out.println("UserService...add...");
}
}
测试类:
package com.hassder.spring03.testdemo;
import com.hassder.spring03.service.UserService;
import org.junit.Test;
import org.springframework.context.ApplicationContext;
import org.springframework.context.support.ClassPathXmlApplicationContext;
public class TestDemo {
@Test
public void testUserService(){
ApplicationContext applicationContext =
new ClassPathXmlApplicationContext("com/hassder/spring03/bean1.xml");
UserService userService = applicationContext.getBean("userService",UserService.class);
userService.add();
}
}
2、基于注解方式实现属性注入
(1) @Autowired
根据属性类型进行自动注入(针对对象类型属性)
(2) @Qualifier
根据属性名称进行注入(针对对象类型属性)
(3) @Resource
可以根据类型注入,也可以根据名称注入(针对对象类型属性)
(4) @Value
注入普通类型属性
使用@Autowired进行注入
创建service和dao对象
在service和dao类添加创建对象的注解
在service中注入dao对象
在service类中添加dao类型的属性,在属性上面使用注解(不需要添加set方法)
测试
代码:
dao接口:
package com.hassder.spring03.dao;
public interface UserDao {
public void add();
}
dao实现类:
package com.hassder.spring03.dao;
import org.springframework.stereotype.Repository;
@Repository
public class UserDaoImpl implements UserDao{
@Override
public void add() {
System.out.println("UserDao...add...");
}
}
service类:
package com.hassder.spring03.service;
import com.hassder.spring03.dao.UserDao;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
@Service(value = "userService")
public class UserService {
//定义dao类型属性
@Autowired // 根据类型注入
private UserDao userDao ;
public void add(){
System.out.println("UserService...add...");
userDao.add();
}
}
测试方法:
@Test
public void testAutowried(){
ApplicationContext applicationContext =
new ClassPathXmlApplicationContext("com/hassder/spring03/bean1.xml");
UserService userService = applicationContext.getBean("userService",UserService.class);
userService.add();
}
使用@Qualifier进行注入
@Qualifier需要与@Autowired配合使用,(比如: service类中定义了dao类型的属性,但是dao接口有多个实现类,这是时候仅靠@Autowired来根据类型注入属性,就无法准确找到我们需要的具体对象,加上名称才能指定具体对象)
使用@Resource进行注入
注: 部分版本的jdk可能不自带此注释,需要用户手动下载导入jar包,或者使用Maven导入
手动下载地址: http://www.java2s.com/Code/Jar/j/Downloadjavaxannotation30jar.htm
使用@Value进行注入(普通属性)
完全注解开发
(1) 创建配置类,代替xml配置文件
package com.hassder.spring03.config;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;
@Configuration // 使用@Configuration注解将该类作为配置类,代替xml配置文件
// 使用@ComponentScan注解的basePackages属性来配置需要扫描的包路径 同xml中的base-package
@ComponentScan(basePackages = "com.hassder.spring03")
public class SpringConfig {
}
(2) 编写测试方法
@Test
public void testConfig(){
ApplicationContext applicationContext =
new AnnotationConfigApplicationContext(SpringConfig.class);
UserService userService = applicationContext.getBean("userService",UserService.class);
userService.add();
}
三、AOP
1、AOP基本概念
(1) 面向切面(方面)编程, 利用AOP可以对业务逻辑的各个部分进行隔离,从而使得业务逻辑各部分之间的耦合度降低,提高程序的可重用性,同时提高了开发的效率
(2) 通俗描述:不通过修改源代码的方式,在主干功能里面添加新功能
2、AOP术语
1、连接点
类中某个或者某些方法可以被增强,则称每一个可以被增强的方法为一个连接点, 比如说在,在一个类中,有四个方法,理论上四个方法都是可以被增强的,因此这四个方法都是连接点
2、切入点
类中实际被增强的方法,每个被增强的方法都是一个切入点,比如说,在一个类中有四个方法都是连接点,但是在实际应用过程中,只对其中的一个方法做了增强,其它三个方法并没有被增强,那么,被增强的这个方法就叫切入点
3、通知(/增强)
实际增强的逻辑部分被称为通知(增强)
通知有多种类型: 通知(增强)
前置通知(增强): 在被通知(增强)的方法执行之前执行的通知(增强)
后置通知(增强): 在被通知(增强)的方法执行之后执行的通知(增强)
环绕通知(增强): 在被通知(增强)的方法执行之前和之后都作执行的通知(增强)
异常通知(增强): 在被通知(增强)的方法出现异常时才执行的通知(增强)
最终通知(增强): 不管被通知(增强)的方法有没有异常,都一定会执行的通知(增强)(普通后置方法,在被通知(增强)方法出现异常时,就不会再执行后置方法了)
4、切面
切面是一个"动作",是把通知(增强)应用到切入点的过程就叫切面
3、AOP底层原理
1、AOP底层使用动态代理
有两种情况的动态代理
(1) 有接口的情况,使用JDK动态代理
JDK动态代理实现:
(1) 使用JDK动态代理时,使用Proxy类里的方法来创建代理对象
java.lang.reflect包下的Proxy类里的newProxyInstance()方法
newProxyInstance()方法有三个参数:
第一个参数:ClassLoader loader ,类加载器
第二个参数:Class<?>[] interfaces ,增强方法所在的类所实现的接口,支持多个接口
第三个参数: InvocationHandler h ,实现这个接口InvocationHandler,创建代理对象,写增强的方法
编写JDK动态代理代码(底层原理)
(1) 创建接口,编写方法
package com.hassder.spring5.demo04;
public interface UserDao {
public int add(int a , int b);
public void update(String id);
}
(2) 创建实现类
package com.hassder.spring5.demo04;
public class UserDaoImpl implements UserDao{
@Override
public int add(int a, int b) {
System.out.println("add方法执行了");
return a+b ;
}
@Override
public String update(String id) {
System.out.println("update方法执行了");
return id ;
}
}
(3) 使用Proxy类创建接口代理对象
package com.hassder.spring5.demo04;
import java.lang.reflect.Array;
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;
import java.util.Arrays;
public class JDKProxy {
public static void main(String[] args) {
// 创建接口实现类代理对象
Class[] interfaces = {UserDao.class};
// 有两种方式实现第三个参数InvocationHandler
// 第一个是直接使用匿名内部类
// 第二个则是创建一个类实现InvocationHandler接口
//A 使用匿名内部类
/*Proxy.newProxyInstance(JDKProxy.class.getClassLoader(), interfaces, new InvocationHandler() {
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
return null;
}
});*/
UserDaoImpl userDao = new UserDaoImpl();
UserDao dao = (UserDao) Proxy.newProxyInstance(JDKProxy.class.getClassLoader(), interfaces, new UserDaoProxy(userDao));
System.out.println(dao.add(1,2));
System.out.println(dao.update("a"));
}
}
//B 创建实现类
// 创建代理对象代码
class UserDaoProxy implements InvocationHandler{
//1 把需要代理的对象传过来
// 通过有参构造传递
private Object obj ;
public UserDaoProxy(Object obj){
this.obj = obj ;
}
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
// 在这里写增强的逻辑
// 被增强的方法执行之前执行
System.out.println("在被增强的方法执行之前执行..."+method.getName()+":传递的参数是:"+ Arrays.toString(args));
// 被增强的方法执行
Object res = method.invoke(obj, args);
// 被增强的方法执行之后执行
System.out.println("被增强的方法执行之后执行..."+obj);
return res;
}
}
package com.hassder.spring5.demo04;
import java.lang.reflect.Array;
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;
import java.util.Arrays;
public class JDKProxy {
public static void main(String[] args) {
Class[] interfaces = {UserDao.class};
UserDaoImpl userDao = new UserDaoImpl();
UserDao dao = (UserDao) Proxy.newProxyInstance(JDKProxy.class.getClassLoader(), interfaces, new UserDaoProxy(userDao));
System.out.println(dao.add(1,2));
System.out.println(dao.update("a"));
}
}
class UserDaoProxy implements InvocationHandler{
private Object obj ;
public UserDaoProxy(Object obj){
this.obj = obj ;
}
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
Object res ;
if (method.getName().equals("add")){
System.out.println("add方法之前..."+method.getName()+":传递的参数是:"+ Arrays.toString(args));
res = method.invoke(obj, args);
System.out.println("add方法之后..."+obj);
}else {
System.out.println("update方法之前..."+method.getName()+":传递的参数是:"+ Arrays.toString(args));
res = method.invoke(obj, args);
System.out.println("update方法之后..."+obj);
}
return res;
}
}
(2) 没有接口的情况,使用CGLIB动态代理
编写CGLIB动态代理代码(底层原理)
(1) 创建类,编写方法
package com.jpeony.spring.proxy.cglib;
public class HelloService {
public HelloService() {
System.out.println("HelloService构造");
}
/**
* 该方法不能被子类覆盖,Cglib是无法代理final修饰的方法的
*/
final public String sayOthers(String name) {
System.out.println("HelloService:sayOthers>>"+name);
return null;
}
public void sayHello() {
System.out.println("HelloService:sayHello");
}
}
(2) 自定义类实现MethodInterceptor接口
package com.jpeony.spring.proxy.cglib;
import net.sf.cglib.proxy.MethodInterceptor;
import net.sf.cglib.proxy.MethodProxy;
import java.lang.reflect.Method;
/**
* 自定义MethodInterceptor
*/
public class MyMethodInterceptor implements MethodInterceptor{
/**
* sub:cglib生成的代理对象
* method:被代理对象方法
* objects:方法入参
* methodProxy: 代理方法
*/
@Override
public Object intercept(Object sub, Method method, Object[] objects, MethodProxy methodProxy) throws Throwable {
System.out.println("======插入前置通知======");
Object object = methodProxy.invokeSuper(sub, objects);
System.out.println("======插入后者通知======");
return object;
}
}
生成CGLIB代理对象调用目标方法:
package com.jpeony.spring.proxy.cglib;
import net.sf.cglib.core.DebuggingClassWriter;
import net.sf.cglib.proxy.Enhancer;
public class Client {
public static void main(String[] args) {
// 代理类class文件存入本地磁盘方便我们反编译查看源码
System.setProperty(DebuggingClassWriter.DEBUG_LOCATION_PROPERTY, "D:\\code");
// 通过CGLIB动态代理获取代理对象的过程
Enhancer enhancer = new Enhancer();
// 设置enhancer对象的父类
enhancer.setSuperclass(HelloService.class);
// 设置enhancer的回调对象
enhancer.setCallback(new MyMethodInterceptor());
// 创建代理对象
HelloService proxy= (HelloService)enhancer.create();
// 通过代理对象调用目标方法
proxy.sayHello();
}
}
使用Spring实现AOP操作
Spring框架一般都是基于AspectJ实现AOP操作
AspectJ不是Spring的组成部分,而是一个独立的AOP框架,一般把AspectJ和Spring一起使用,进行AOP操作(更加方便)
基于AspectJ实现AOP操作
在项目工程里引入AOP相关依赖
在前置内容所导入的jar包的基础上,我们还需要导入如下几个jar包:
spring-aspects-xxx.jar (spring自带jar包)
com.springsource.net.sf.cglib-xxx.jar(自行手动下载)
com.springsource.org.aopalliance-xxx.jar(自行手动下载)
com.springsource.org.aspectj.weaver-xxx.jar(自行手动下载)
截止目前我们所使用到的jar包有:
切入点表达式
(1) 切入点表达式的作用
表示要对哪个类中的哪个方法进行增强
(2) 语法结构
execution([权限修饰符(*代表全部)] [返回值类型(可省略)] [类的全路径].[方法名]([参数列表]))
package com.hassder.spring5.demo04;
public interface UserDao {
public int add(int a, int b);
public String update(String id);
}
例如,要对上面的类中的add()方法进行增强,应该写execution(* com.hassder.spring5.demo04.UserDao.add(…))
其中 * 是权限修饰符(*表示任意修饰符), 返回值类型可以省略,com.hassder.spring5.demo04.UserDao.add(…)是全路径.方法名(参数)
若要对上面类中所有方法进行增强 ,则: execution(* com.hassder.spring5.demo04.UserDao.*(…))
若要对上面类所在包下所有类的所有方法进行增强 ,则: execution(* com.hassder.spring5.demo04.*.*(…))
(1) 基于注解方式实现(常用)
1、创建类(被增强类),定义方法
package com.hassder.spring5.demo04.aopanno;
public class User {
public void add(){
System.out.println("add...");
}
}
2、创建增强类(增强的逻辑)
(1) 在增强类里面,创建方法,让不同方法代表不同的通知类型
package com.hassder.spring5.demo04.aopanno;
public class UserProxy {
// 作为前置增强方法
public void before(){
System.out.println("before...");
}
}
3、进行通知(增强)的配置
(1) 在spring配置文件中,开启注解扫描(使用上面说的配置类也可以)
引入名称空间,开启注解扫描
<?xml version="1.0" encoding="UTF-8"?>
<!--引入context和aop名称空间-->
<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: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/aop http://www.springframework.org/schema/aop/spring-aop.xsd">
<!--开启注解扫描-->
<context:component-scan base-package="com.hassder.spring5.demo04.aopanno"/>
</beans>
(2) 使用注解创建User和UserProxy对象
User类:
package com.hassder.spring5.demo04.aopanno;
import org.springframework.stereotype.Component;
@Component
public class User {
public void add(){
System.out.println("add...");
}
}
UserProxy类:
package com.hassder.spring5.demo04.aopanno;
import org.springframework.stereotype.Component;
@Component
public class UserProxy {
// 作为前置增强方法
public void before(){
System.out.println("before...");
}
}
(3) 在通知(增强)类上面添加注解@Aspect
package com.hassder.spring5.demo04.aopanno;
import org.aspectj.lang.annotation.Aspect;
import org.springframework.stereotype.Component;
@Component
@Aspect
public class UserProxy {
// 作为前置增强方法
public void before(){
System.out.println("before...");
}
}
(4) 在spring配置文件中开启生成代理对象
<?xml version="1.0" encoding="UTF-8"?>
<!--引入context和aop名称空间-->
<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: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/aop http://www.springframework.org/schema/aop/spring-aop.xsd">
<!--开启注解扫描-->
<context:component-scan base-package="com.hassder.spring5.demo04.aopanno"/>
<!--开启Aspect生成代理对象-->
<aop:aspectj-autoproxy/>
</beans>
(5) 配置不同类型通知
在增强类里作为通知(增强)的方法上面,添加通知类型的注解,使用切入点表达式配置
package com.hassder.spring5.demo04.aopanno;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import org.springframework.stereotype.Component;
@Component
@Aspect
public class UserProxy {
// 作为前置增强方法
@Before(value = "execution(* com.hassder.spring5.demo04.aopanno.User.add(..))")
public void before(){
System.out.println("before...");
}
}
测试方法:
package com.hassder.spring5.demo04.tesedemo;
import com.hassder.spring5.demo04.aopanno.User;
import org.junit.Test;
import org.springframework.context.ApplicationContext;
import org.springframework.context.support.ClassPathXmlApplicationContext;
public class TsetDemo {
@Test
public void testAopAnno(){
ApplicationContext applicationContext =
new ClassPathXmlApplicationContext("com/hassder/spring5/demo04/aopanno/bean1.xml");
User user = applicationContext.getBean("user", User.class);
user.add();
}
}
测试其它增强(通知)类型的注解:
修改UserProxy类如下,其它内容不变:
package com.hassder.spring5.demo04.aopanno;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.*;
import org.springframework.stereotype.Component;
@Component
@Aspect
public class UserProxy {
// 作为前置增强(通知)方法
@Before(value = "execution(* com.hassder.spring5.demo04.aopanno.User.add(..))")
public void before(){
System.out.println("before...");
}
// 最终通知,即无论被增强方法是否发生异常,都会执行
// 在被增强方法执行完之后执行
@After(value = "execution(* com.hassder.spring5.demo04.aopanno.User.add(..))")
public void after(){
System.out.println("after...");
}
// 后置增强(通知)方法
// 在被增强方法返回返回值之后执行
@AfterReturning(value = "execution(* com.hassder.spring5.demo04.aopanno.User.add(..))")
public void afterReturning(){
System.out.println("afterReturning...");
}
// 异常增强(通知)方法
@AfterThrowing(value = "execution(* com.hassder.spring5.demo04.aopanno.User.add(..))")
public void afterThrowing(){
System.out.println("afterThrowing...");
}
// 环绕增强(通知)方法
// 环绕增强方法需要一个ProceedingJoinPoint类型的参数,然后调用其中的proceed()方法来执行被增强方法
// proceedingJoinPoint.proceed();语句之前的部分为环绕的前面部分 之后的部分为环绕之后的部分
// 并且需要抛出一个Throwable异常
@Around(value = "execution(* com.hassder.spring5.demo04.aopanno.User.add(..))")
public void around(ProceedingJoinPoint proceedingJoinPoint) throws Throwable {
System.out.println("环绕之前...");
proceedingJoinPoint.proceed();
System.out.println("环绕之后...");
}
}
使用测试方法运行, 结果如下:
结合运行结果以及修改后的UserProxy类可以发现,异常增强(通知)方法并没有执行, 这是由于我们执行的add方法中并没有异常,接下来我们手动为add方法内添加一个异常, 修改User类如下:
package com.hassder.spring5.demo04.aopanno;
import org.springframework.stereotype.Component;
@Component
public class User {
public void add(){
// 手动创造异常
int i = 10/0 ;
System.out.println("add...");
}
}
我们使用一个int i = 10/0; 来创造一个异常(除数不能为0),其它内容不变,再次运行测试方法:
从运行结果中我们可以看到,在被增强方法中出现了异常的时候, 只执行了环绕增强的前置部分,前置增强,异常增强以及最终增强四个增强方法
4、相同切入点的抽取
在上面的例子中,可以看到,我们使用一个被通知(增强)方法以及多个通知(增强)方法实现了所有通知(增强)类型的展示, 虽然在UserProxy类中,我们所写的不同类型的通知(增强)方法所使用的的注解不同, 但是他们都是对同一个add方法进行通知(增强)效果, 即切入点是一样的, 切入点表达式也是一样的, 因此我们在这种多个不同的通知(增强)作用于同一个方法(切入点表达式相同)的时候,把切入点抽取出来,也叫作重用切入点
package com.hassder.spring5.demo04.aopanno;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.*;
import org.springframework.stereotype.Component;
@Component
@Aspect
public class UserProxy {
// 重用切入点
// 新建一个方法(空方法,无意义,只取注解部分)
// 使用@Pointcut注解 在其value值内写入公共切入点的表达式
// 在其他使用这个切入点的注解的value值内填入此方法名即可
@Pointcut(value = "execution(* com.hassder.spring5.demo04.aopanno.User.add(..))")
public void suiBianQuDe(){
}
// 作为前置增强(通知)方法
@Before(value = "suiBianQuDe()")
public void before(){
System.out.println("before...");
}
// 最终通知,即无论被增强方法是否发生异常,都会执行
// 在被增强方法执行完之后执行
@After(value = "suiBianQuDe()")
public void after(){
System.out.println("after...");
}
// 后置增强(通知)方法
// 在被增强方法返回返回值之后执行
@AfterReturning(value = "suiBianQuDe()")
public void afterReturning(){
System.out.println("afterReturning...");
}
// 异常增强(通知)方法
@AfterThrowing(value = "suiBianQuDe()")
public void afterThrowing(){
System.out.println("afterThrowing...");
}
// 环绕增强(通知)方法
// 环绕增强方法需要一个ProceedingJoinPoint类型的参数,然后调用其中的proceed()方法来执行被增强方法
// proceedingJoinPoint.proceed();语句之前的部分为环绕的前面部分 之后的部分为环绕之后的部分
// 并且需要抛出一个Throwable异常
@Around(value = "suiBianQuDe()")
public void around(ProceedingJoinPoint proceedingJoinPoint) throws Throwable {
System.out.println("环绕之前...");
proceedingJoinPoint.proceed();
System.out.println("环绕之后...");
}
}
5、多个增强类对同一个方法进行增强, 设置增强类优先级
在增强类上面添加注解@Oeder(数字类型值) ,数字最小为0,数字越小代表优先级越高
测试:
新建一个PersonProxy类,在其中为上面的add方法添加一个前置通知(增强)方法,并在类上添加一个@Order注解为其设置一个值:
package com.hassder.spring5.demo04.aopanno;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import org.springframework.core.annotation.Order;
import org.springframework.stereotype.Component;
@Component
@Aspect
@Order(3)
public class PersonProxy {
@Before(value = "execution(* com.hassder.spring5.demo04.aopanno.User.add(..))")
public void before(){
System.out.println("PersonProxy before...");
}
}
修改原UserProxy类,先将其他通知(增强)方法暂时注释,只留下前置通知方法,并在类上添加一个@Order注解,也为其设置一个值(首先设置一个比PersonProxy类值小的值):
package com.hassder.spring5.demo04.aopanno;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.*;
import org.springframework.core.annotation.Order;
import org.springframework.stereotype.Component;
@Component
@Aspect
@Order(1)
public class UserProxy {
// 限于篇幅 直接将注释内容删除了
// 作为前置增强(通知)方法
@Before(value = "execution(* com.hassder.spring5.demo04.aopanno.User.add(..))")
public void before(){
System.out.println("before...");
}
}
测试方法不变, 运行测试方法结果如下:
在我的代码中,设置的UserProxy类的增强方法优先级为1 , 而PersonProxy类的优先级为3,因此先执行UserProxy类中的增强方法,接下来,修改两者的优先级,使PersonProxy类中优先级更高(数字更小),其它内容不变,再次测试,结果如下:
通过修改@Order注解值的方式可以修改通知(增强)方法的优先级
6、完全注解开发实现AOP
使用配置类代替xml配置文件,并使用@Configuration @ComponentScan @EnableAspectJAutoProxy三个注解来配置信息:
package com.hassder.spring5.demo04.config;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.EnableAspectJAutoProxy;
@Configuration
// 使用@ComponentScan(basePackages = {"扫描路径"})实现组件扫描
@ComponentScan(basePackages = {"com.hassder.spring5.demo04"})
// 使用@EnableAspectJAutoProxy(proxyTargetClass = true) 开启Aspect生成代理对象
// @EnableAspectJAutoProxy()括号内必须手动将proxyTargetClass的值设置为true 否则默认为false
@EnableAspectJAutoProxy(proxyTargetClass = true)
public class ConfigAop {
}
(2)基于xml配置文件实现
1、创建两个类, 增强类和被增强类, 创建方法
被增强类BookProxy:
package com.hassder.spring5.demo04.aopxml;
public class BookProxy {
// 前置方法
public void before(){
System.out.println("before......");
}
}
增强类Book:
package com.hassder.spring5.demo04.aopxml;
public class Book {
// 被增强方法
public void buy(){
System.out.println("buy.......");
}
}
2、在spring配置文件中创建两个类对象
3、在spring配置文件中配置切入点
xml配置文件:
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns: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 http://www.springframework.org/schema/aop/spring-aop.xsd">
<!--创建对象-->
<bean id="book" class="com.hassder.spring5.demo04.aopxml.Book"/>
<bean id="bookProxy" class="com.hassder.spring5.demo04.aopxml.BookProxy" />
<!--配置aop增强-->
<aop:config>
<!--配置切入点-->
<!--<aop:pointcut id="切入点名称" expression="切入点表达式"/>-->
<aop:pointcut id="p" expression="execution(* com.hassder.spring5.demo04.aopxml.Book.buy(..))"/>
<!--配置切面-->
<!--<aop:aspect ref="用于增强的对象">-->
<aop:aspect ref="bookProxy">
<!--配置增强作用在具体的方法上-->
<!--<aop:通知类型 method="用于增强的方法名" pointcut-ref="切入点"/>-->
<aop:before method="before" pointcut-ref="p"/>
</aop:aspect>
</aop:config>
</beans>
测试方法:
@Test
public void testAopXml(){
ApplicationContext applicationContext =
new ClassPathXmlApplicationContext("com/hassder/spring5/demo04/aopxml/bean1.xml");
Book book = applicationContext.getBean("book", Book.class);
book.buy();
}
运行结果:
在上面的代码中,我们仅在xml配置文件里进行了配置,此外没有使用到任何注解(@Test 测试注解除外),在测试方法中调用了book对象的buy()方法,可以看到在buy()方法执行之前,先执行了通知(增强)方法
四、JdbcTemplate
1、概念和准备工作
概念
什么是JdbcTemplate
Spring框架对JDBC进行封装,使用JdbcTemplate可以更方便的实现对数据库的操作
准备工作
(1) 引入相关依赖(jar包)
druid-xxx.jar(在上面举例过程中已经引入,自行下载)
mysql-connector-java-xxx.jar(自行下载)
spring-jdbc-xxx.jar (spring自带jar包)
spring-tx-xxx.jar (spring自带jar包)
spring-orm-xxx.jar (spring自带jar包)
截止目前使用到的jar包:
(2) 在spring的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">
<!--数据库连接池-->
<bean id="dataSource" class="com.alibaba.druid.pool.DruidDataSource">
<property name="driverClassName" value="com.mysql.cj.jdbc.Driver"/>
<property name="url" value="jdbc:mysql:///spring5"/>
<property name="username" value="root"/>
<property name="password" value="123456"/>
</bean>
</beans>
(3) 配置JdbcTemplate对象, 注入DataSource
<!--配置JdbcTemplate对象-->
<bean id="jdbcTemplate" class="org.springframework.jdbc.core.JdbcTemplate">
<!--注入DataSource-->
<property name="dataSource" ref="dataSource"/>
</bean>
(4) 创建service类, 创建dao类, 在dao注入jdbcTemplate对象
在xml文件开启组件扫描:
<!--组件扫描-->
<context:component-scan base-package="com.hassder.spring5.demo05"/>
service类:
package com.hassder.spring5.demo05.service;
import com.hassder.spring5.demo05.dao.BookDao;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
@Service
public class BookService {
// 注入dao
@Autowired
private BookDao bookDao ;
}
dao类:
package com.hassder.spring5.demo05.dao;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.stereotype.Repository;
@Repository
public class BookDaaoImpl implements BookDao{
// 注入JdbcTemplate
@Autowired
private JdbcTemplate jdbcTemplate ;
}
2、JdbcTemplate操作数据库
准备工作:
在此部分中,需要使用到MySQL,在需要直接操作数据库的时候,本人使用的是sqlyog,
首先创建一个数据库,我将其命名为spring5,在数据库spring5中建表如下:
CREATE TABLE `t_book` (
`user_id` bigint NOT NULL,
`username` varchar(100) NOT NULL,
`ustatus` varchar(50) NOT NULL,
PRIMARY KEY (`user_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci;
添加、修改和删除
1、对应数据库创建实体类
package com.hassder.spring5.demo05.entity;
public class Book {
private String userId ;
private String username ;
private String ustatus ;
public Book() {
}
public Book(String userId, String username, String ustatus) {
this.userId = userId;
this.username = username;
this.ustatus = ustatus;
}
public String getUserId() {
return userId;
}
public void setUserId(String userId) {
this.userId = userId;
}
public String getUsername() {
return username;
}
public void setUsername(String username) {
this.username = username;
}
public String getUstatus() {
return ustatus;
}
public void setUstatus(String ustatus) {
this.ustatus = ustatus;
}
}
2、编写service和dao
(1) 在dao进行数据库添加操作
(2) 调用JdbcTemplate对象里的update()方法实现添加操作
dao层:
package com.hassder.spring5.demo05.dao;
import com.hassder.spring5.demo05.entity.Book;
public interface BookDao {
// 添加的方法
void addBook(Book book);
// 修改的方法
void updateBook(Book book);
// 删除的方法
void deleteBook(String id);
}
package com.hassder.spring5.demo05.dao;
import com.hassder.spring5.demo05.entity.Book;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.stereotype.Repository;
@Repository
public class BookDaoImpl implements BookDao{
// 注入JdbcTemplate
@Autowired
private JdbcTemplate jdbcTemplate ;
@Override
public void addBook(Book book) {
// 通过调用jdbcTemplate对象中的update方法来实现添加操作
// update()方法可以有多个参数
// 第一个参数为sql语句
// 后续为可变参数,表示sql语句的语句值
// 如在下列示例中,在sql语句中使用了3个"?"(占位符),表示有三个值,那么update方法除sql语句这一个参数之外
// 还应该包含三个参数,且这三个参数依次对应sql语句中的三个"?"
// update()方法返回值类型为int 表示的是执行完此条语句后,数据库中受影响的数据条数
// 1 创建sql语句
String sql = "insert into t_book values(?,?,?)" ;
// 2 调用方法实现
int update = jdbcTemplate.update(sql, book.getUserId(), book.getUsername(), book.getUstatus());
// 由于update()方法的实际参数数量类型都不确定(可变),除sql语句之外的参数实质上都是一个数组
// 因此我们也可以使用下面这种写法,效果相同
// Object[] objects = {book.getUserId(), book.getUsername(), book.getUstatus()};
// int update = jdbcTemplate.update(sql,objects);
System.out.println("成功添加了"+update+"行");
}
@Override
public void updateBook(Book book) {
String sql = "update t_book set username=?,ustatus=? where user_id=?" ;
int update = jdbcTemplate.update(sql, book.getUsername(), book.getUstatus(), book.getUserId());
System.out.println("成功修改了"+update+"行");
}
@Override
public void deleteBook(String id) {
String sql = "delete from t_book where user_id=?" ;
int update = jdbcTemplate.update(sql, id);
System.out.println("成功删除了"+update+"行");
}
}
service层:
package com.hassder.spring5.demo05.service;
import com.hassder.spring5.demo05.dao.BookDao;
import com.hassder.spring5.demo05.entity.Book;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
@Service
public class BookService {
// 注入dao
@Autowired
private BookDao bookDao ;
// 添加的方法
public void addBook(Book book){
bookDao.addBook(book) ;
}
public void updateBook(Book book){
bookDao.updateBook(book) ;
}
public void deleteBook(String id){
bookDao.deleteBook(id) ;
}
}
测试方法:
package com.hassder.spring5.demo05.testdemo;
import com.hassder.spring5.demo05.entity.Book;
import com.hassder.spring5.demo05.service.BookService;
import org.junit.Test;
import org.springframework.context.ApplicationContext;
import org.springframework.context.support.ClassPathXmlApplicationContext;
public class TestDemo {
@Test
public void testBookAdd(){
ApplicationContext applicationContext =
new ClassPathXmlApplicationContext("com/hassder/spring5/demo05/bean1.xml");
BookService bookService = applicationContext.getBean("bookService", BookService.class);
// 暂时手动创建对象用于添加
Book book = new Book("1","javaEE","a") ;
bookService.addBook(book);
}
@Test
public void testBookUpdate(){
ApplicationContext applicationContext =
new ClassPathXmlApplicationContext("com/hassder/spring5/demo05/bean1.xml");
BookService bookService = applicationContext.getBean("bookService", BookService.class);
// 暂时手动创建对象用于添加
Book book = new Book("1","javaEE","bbb") ;
bookService.updateBook(book);
}
@Test
public void testBookDelete(){
ApplicationContext applicationContext =
new ClassPathXmlApplicationContext("com/hassder/spring5/demo05/bean1.xml");
BookService bookService = applicationContext.getBean("bookService", BookService.class);
String id = "1" ;
bookService.deleteBook(id);
}
}
为了便于观察效果,我们使用了三个测试方法来对三种不同的语句进行分开测试
- 添加:
程序结果:
数据库数据信息:
可以看到数据库表中多了一条数据,正是我们想要添加的book对象的数据
- 修改:
程序结果:
数据库数据:
- 删除
程序结果:
数据库数据:
查询
为供查询使用,提前在表中插入一些数据,如下:
insert into `t_book`(`user_id`,`username`,`ustatus`) values
(1,'十万个为什么','a'),
(2,'十万个为什么','b'),
(3,'JavaEE','a'),
(4,'JavaEE','b'),
(5,'JavaSE','a'),
(6,'JavaSE','b'),
(7,'Python','a');
查询返回某个值
查询表里有多少条记录,返回值肯定是某个值:
在BookDao接口中添加:
// 查询表中数据条数的方法
// 返回值为数据条数
int selectCount();
在接口的实现类BookDaoImpl类中将其实现:
使用jdbcTemplate对象中的queryForObject()方法
@Override
public int selectCount() {
String sql = "select count(*) from t_book" ;
// 使用jdbcTemplate对象中的queryForObject()方法来进行此类查询
// 在查询某个值时queryForObject()方法需要两个参数
// 第一个参数为sql语句
// 第二个参数为返回值类型的class
// 当返回值类型为基本数据类型时,也可以使用对应的包装类类型class
Integer count = jdbcTemplate.queryForObject(sql, int.class);
return count ;
}
在BookService类中调用此方法:
public int findCount(){
return bookDao.selectCount() ;
}
在测试方法中调用bookService对象中的findCount()方法:
@Test
public void testSelectCount(){
ApplicationContext applicationContext =
new ClassPathXmlApplicationContext("com/hassder/spring5/demo05/bean1.xml");
BookService bookService = applicationContext.getBean("bookService", BookService.class);
int count = bookService.findCount();
System.out.println("表中共有"+count+"条数据");
}
测试结果:
查询返回某个对象
根据id值查到某一本书,返回值应该为一本书(一个Book类型对象):
在BookDao接口中添加:
// 查询表中某一本书
// 返回值为Book类型对象 参数为要查询的书的id值
Book findBookInfo(String id);
在接口的实现类BookDaoImpl类中将其实现:
使用jdbcTemplate对象中的queryForObject()方法
@Override
public Book findBookInfo(String id) {
String sql = "select * from t_book where user_id=?" ;
// 在查询某个值时queryForObject()方法需要三个参数
// 第一个参数为sql语句
// 第二个参数为RowMapper,是一个接口,用于返回不同类型的数据
// 使用这个接口的实现类可以实现数据的封装
// 第三个参数为sql语句值
// 当返回值类型为基本数据类型时,也可以使用对应的包装类类型class
// queryForObject(sql语句, new BeanPropertyRowMapper<返回对象类型>(返回对象类型.class),sql语句值)
Book book = jdbcTemplate.queryForObject(sql, new BeanPropertyRowMapper<Book>(Book.class), id);
return book;
}
在BookService类中调用此方法:
// 查询返回对象
// 靠id值来查询
public Book findOne(String id){
return bookDao.findBookInfo(id) ;
}
在测试方法中调用bookService对象中的findCount()方法(为了对比数据,分别输出返回的book对象的地址以及属性值):
@Test
public void testFindOne(){
ApplicationContext applicationContext =
new ClassPathXmlApplicationContext("com/hassder/spring5/demo05/bean1.xml");
BookService bookService = applicationContext.getBean("bookService", BookService.class);
String id = "5" ;
Book book = bookService.findOne(id);
System.out.println(book);
System.out.println(book.getUserId()+" || "+
book.getUsername()+" || "+book.getUstatus());
}
测试结果:
在数据库表中找到id为5的书本信息如下:
显然,二者是相同的,通过queryForObject()方法将数据库中的一条书籍数据信息封装成了一个Book对象并返回
查询返回集合
查询图书列表分页,返回全部或者某些图书的集合(Book类型的集合)
在BookDao接口中添加:
// 查询表中所有书
List<Book> findAllBook();
在接口的实现类BookDaoImpl类中将其实现:
使用jdbcTemplate对象中的query()方法
@Override
public List<Book> findAllBook() {
String sql = "select * from t_book" ;
// 在查询某个集合时query()方法需要三个参数
// 第一个参数为sql语句
// 第二个参数为RowMapper,是一个接口,用于返回不同类型的数据
// 使用这个接口的实现类可以实现数据的封装
// 第三个参数为sql语句值(若有多个语句值,则可以写成多个参数,或者一个数组类型参数,没有语句值时可以不写)
// 当返回值类型为基本数据类型时,也可以使用对应的包装类类型class
// query(sql语句, new BeanPropertyRowMapper<返回对象类型>(返回对象类型.class),sql语句值)
List<Book> books = jdbcTemplate.query(sql, new BeanPropertyRowMapper<Book>(Book.class));
return books;
}
在BookService类中调用此方法:
// 查询返回集合
public List<Book> findAll (){
return bookDao.findAllBook() ;
}
在测试方法中调用bookService对象中的findAll()方法:
@Test
public void testFindAll(){
ApplicationContext applicationContext =
new ClassPathXmlApplicationContext("com/hassder/spring5/demo05/bean1.xml");
BookService bookService = applicationContext.getBean("bookService", BookService.class);
List<Book> books = bookService.findAll();
for (Book book:books){
System.out.println(book.getUserId()+" || "+
book.getUsername()+" || "+book.getUstatus());
}
}
测试结果:
数据库表信息如下:
可以看到,测试方法的输出结果与数据库表中所有书籍的信息都是一样的,而测试方法的输出结果是我们使用得到的Book类型的一个集合遍历来进行输出的,query()方法将查询的sql语句查询出来的所有结果(多个),逐个封装成Book类型的对象,并将多个Book类型对象放在了一个集合内
批量操作
什么是批量操作
在刚才的添加、修改和删除的操作中,都只是一次对数据库表中的某一条数据进行操作,那么该如何同时对多条数据进行操作?实现同时对条数据进行处理的操作就是批量操作
实现批量操作
批量添加
在BookDao接口中添加:
// 向表中批量添加数据
void batchAddBook(List<Object[]> batchArgs);
在接口的实现类BookDaoImpl类中将其实现:
使用jdbcTemplate对象中的batchUpdate()方法
@Override
public void batchAddBook(List<Object[]> batchArgs) {
String sql = "insert into t_book values (?,?,?)" ;
// 批量添加时调用jdbcTemplate对象中的batchUpdate方法
// batchUpdate()方法有两个参数
// 第一个参数为sql语句
// 第二个参数为一个集合
// 集合内是需要添加的多条数据
int[] ints = jdbcTemplate.batchUpdate(sql, batchArgs);
System.out.println(Arrays.toString(ints));
}
在BookService类中调用此方法:
// 向表中批量添加数据
public void batchAdd(List<Object[]> batchArgs){
bookDao.batchAddBook(batchArgs) ;
}
在测试方法中调用bookService对象中的batchAdd()方法:
@Test
public void testBatchAdd(){
ApplicationContext applicationContext =
new ClassPathXmlApplicationContext("com/hassder/spring5/demo05/bean1.xml");
BookService bookService = applicationContext.getBean("bookService", BookService.class);
List<Object[]> batchArgs = new ArrayList<>();
Object[] o1 = {"8","C语言","a"};
Object[] o2 = {"9","C语言","b"};
Object[] o3 = {"10","C语言","c"};
batchArgs.add(o1);
batchArgs.add(o2);
batchArgs.add(o3);
bookService.batchAdd(batchArgs);
}
测试结果:
数据库表信息如下:
在程序中,方法的返回值为一个int[], 这是因为批量操作数据时,对同一条SQL语句执行了多次(在测试中插入了三次,执行了三次sql语句),每一次添加数据,都影响了数据库表中的一行数据,因此返回了3个1
在数据库中也可以看到,确实将我们想要插入的三条数据进行了插入
批量修改
批量修改与批量添加所调用的方法是相同的,不同点只是sql语句不同
在BookDao接口中添加:
// 向表中批量修改数据
void batchUpdateBook(List<Object[]> batchArgs);
在接口的实现类BookDaoImpl类中将其实现:
使用jdbcTemplate对象中的batchUpdate()方法
@Override
public void batchUpdateBook(List<Object[]> batchArgs) {
String sql = "update t_book set username=? ,ustatus=? where user_id=?" ;
int[] ints = jdbcTemplate.batchUpdate(sql, batchArgs);
System.out.println(Arrays.toString(ints));
}
在BookService类中调用此方法:
// 向表中批量修改数据
public void batchUpdate(List<Object[]> batchArgs){
bookDao.batchUpdateBook(batchArgs) ;
}
在测试方法中调用bookService对象中的batchAdd()方法:
@Test
public void testBatchUpdate(){
ApplicationContext applicationContext =
new ClassPathXmlApplicationContext("com/hassder/spring5/demo05/bean1.xml");
BookService bookService = applicationContext.getBean("bookService", BookService.class);
List<Object[]> batchArgs = new ArrayList<>();
// 注意数据的顺序,要与sql的?对应
Object[] o1 = {"C++","c","8"};
Object[] o2 = {"C++","a","9"};
Object[] o3 = {"C++","b","10"};
batchArgs.add(o1);
batchArgs.add(o2);
batchArgs.add(o3);
bookService.batchUpdate(batchArgs);
}
测试结果:
数据库表信息如下:
由于我们所进行的修改操作是根据user_id值来修改的,而user_id是主键,是唯一的,所以每次只能修改一行,三条数据,sql语句执行三次,返回3个1
批量删除
批量删除与批量添加所调用的方法是相同的,不同点只是sql语句不同
在BookDao接口中添加:
// 向表中批量删除数据
void batchRemoveBook(List<Object[]> batchArgs);
在接口的实现类BookDaoImpl类中将其实现:
使用jdbcTemplate对象中的batchUpdate()方法
@Override
public void batchRemoveBook(List<Object[]> batchArgs) {
String sql = "delete from t_book where user_id=?" ;
int[] ints = jdbcTemplate.batchUpdate(sql, batchArgs);
System.out.println(Arrays.toString(ints));
}
在BookService类中调用此方法:
// 向表中批量删除数据
public void batchRemove(List<Object[]> batchArgs){
bookDao.batchRemoveBook(batchArgs) ;
}
在测试方法中调用bookService对象中的batchAdd()方法:
@Test
public void testBatchRemove(){
ApplicationContext applicationContext =
new ClassPathXmlApplicationContext("com/hassder/spring5/demo05/bean1.xml");
BookService bookService = applicationContext.getBean("bookService", BookService.class);
List<Object[]> batchArgs = new ArrayList<>();
// 注意数据的顺序,要与sql的?对应
Object[] o1 = {"8"};
Object[] o2 = {"9"};
Object[] o3 = {"10"};
batchArgs.add(o1);
batchArgs.add(o2);
batchArgs.add(o3);
bookService.batchRemove(batchArgs);
}
测试结果:
数据库表信息如下:
程序返回结果依然是3个1,而在数据库表中我们可以看到,user_id值为8、9、10的三条记录已经被删除掉了
五、在Spring中实现事务操作
1、事务的概念
什么叫事务?
(1) 事务是数据库操作的基本单元,逻辑上的一组操作,要么都成功,要么都失败
比如:银行转账,假设A账户里有2000元钱,B账户里有3000元钱,A要给B转1000元钱,那么在这个过程中,必须要有两个步骤(简化):第一步首先给A账户里扣除1000元钱,第二步是给B账户里添加1000元钱,假如没有事务这个概念,那么这就是两个独立的操作,那么可能会遇到一些特殊情况,比如说刚刚给A账户里扣除了1000元钱,而B账户里还没有进行添加的时候,操作中断了(比如停电等,因为这两个步骤肯定是两条不同的语句,有可能发生执行第一步的语句,而没有执行第二步语句的时候突然断电了),这个时候就会发生A账户里的钱少了,但是B账户里的钱并没有增加的情况,这显然是不合理的,所以可以使用事务这个概念,把这两个步骤当做一个整体,要么两者都执行成功,如果这两个步骤里有一个没有执行成功,那么就全部执行失败,返回到执行语句之前的状态.
事务的四大特性(ACID)
(1) 原子性 : 不可分割,比如在转账的过程中,给一个账户扣钱和个另一个账户加钱,这就是一个整体,不可分割
(2) 一致性 : 操作之前和操作之后总量不变,比如在转账之前,A、B两个账户的总金额为5000元,那么转账完成之后就应该还是5000元,不会变多也不会变少
(3) 隔离性 : 多事务操作时,不同事务之间互不影响
(4) 持久性 : 事务提交之后,永久有效
2、事务环境搭建
- 准备工作,创建数据库表,添加部分数据:
CREATE TABLE `t_account` (
`id` varchar(20) NOT NULL,
`username` varchar(50) DEFAULT NULL,
`money` int DEFAULT NULL,
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci;
insert into `t_account`(`id`,`username`,`money`) values
('1','lucy',2000),
('2','mary',3000);
相当于给银行新增了两个用户,一个名为lucy,存款为2000,另一个名为mary,存款为3000,我们就使用这两个用户之间转账的操作来完成对事物的演示
分析:
为了便于理解,将Dao中多钱和少钱的两个方法合并为一个钱的数目变化的方法,至于是多钱还是少钱,使用参数中的±来表示
- 创建service, 搭建dao, 完成对象创建和注入关系
结构:
(1) service注入dao, 在dao中注入jdbcTemplate,在jdbcTemplate注入DataSource
UserDao:
package com.hassder.spring5.demo06.dao;
public interface UserDao {
}
UserDaoImpl:
package com.hassder.spring5.demo06.dao;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.stereotype.Repository;
@Repository
public class UserDaoImpl implements UserDao{
// 注入jdbcTemplate
@Autowired
private JdbcTemplate jdbcTemplate ;
}
UserService:
package com.hassder.spring5.demo06.service;
import com.hassder.spring5.demo06.dao.UserDao;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
@Service
public class UserService {
// 注入dao
@Autowired
private UserDao userDao ;
}
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"
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.hassder.spring5.demo05"/>
<!--数据库连接池-->
<bean id="dataSource" class="com.alibaba.druid.pool.DruidDataSource">
<property name="driverClassName" value="com.mysql.cj.jdbc.Driver"/>
<property name="url" value="jdbc:mysql:///spring5"/>
<property name="username" value="root"/>
<property name="password" value="123456"/>
</bean>
<!--配置JdbcTemplate对象-->
<bean id="jdbcTemplate" class="org.springframework.jdbc.core.JdbcTemplate">
<!--注入DataSource-->
<property name="dataSource" ref="dataSource"/>
</bean>
</beans>
- 在dao创建一个钱的变化的方法:,在service中创建一个转账的方法(需要同时完成转出和转入操作).
UserDao:
package com.hassder.spring5.demo06.dao;
public interface UserDao {
// 金额变化
public void updateMoney(String uName,int money) ;
}
UserDaoImpl:
package com.hassder.spring5.demo06.dao;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.stereotype.Repository;
@Repository
public class UserDaoImpl implements UserDao{
// 注入jdbcTemplate
@Autowired
private JdbcTemplate jdbcTemplate ;
@Override
public void updateMoney(String uName, int money) {
String sql = "update t_account set money=money+? where username=?" ;
int update = jdbcTemplate.update(sql, money, uName);
// 为了更直观的观察结果, 增加一点输出信息:
if (money>0){
System.out.println("操作成功,向"+uName+"转入"+money+"元");
}else if (money<0){
System.out.println("操作成功,从"+uName+"转出"+(-money)+"元");
}else {
System.out.println("无操作");
}
}
}
- 测试
写一个测试方法,调用userService中的updateMoney()方法,运行并查看结果:
@Test
public void testD1(){
ApplicationContext applicationContext =
new ClassPathXmlApplicationContext("com/hassder/spring5/demo06/bean.xml");
UserService userService = applicationContext.getBean("userService", UserService.class);
userService.transferAccounts();
}
运行结果:
数据库表信息如下:
可以看到,在程序运行之后,在数据库中已经完成了对lucy扣钱1000和对mary加钱1000的操作
至此,事务环境搭建完成
注意: 在上面的代码中,并没有使用"事务"这个概念,在正常情况下是可以正确完成"转账"这一正常操作的,但是如果发生了特殊情况,updateMoney()方法中两次调用的jdbcTemplate.update()方法如果并没有都正确的执行(情况很多,比如运行到一半突然断电,或者实际使用过程中,方法中的代码较复杂,可能出现异常),那么还是会出现之前说的问题(比如一方扣钱了但是另一方并没有加钱,或者一方加钱了但是另一方没有扣钱)
异常演示:
在transferAccounts()方法中添加一个异常:
package com.hassder.spring5.demo06.service;
import com.hassder.spring5.demo06.dao.UserDao;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
@Service
public class UserService {
// 注入dao
@Autowired
private UserDao userDao ;
public void transferAccounts(){
// 给lucy扣1000
userDao.updateMoney("lucy",-1000);
// 手动添加异常
int i = 10/0 ;
// 给mary加1000
userDao.updateMoney("mary",1000);
}
}
调用测试方法并查看运行结果:
数据库表信息如下:
可以看到,lucy的钱减少了,但是mary的钱并没有增加,这就出现了问题
使用事务可以解决上述问题
为1便于后续演示,将lucy与mary的金额恢复到2000与3000
事务的逻辑步骤:
package com.hassder.spring5.demo06.service;
import com.hassder.spring5.demo06.dao.UserDao;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
@Service
public class UserService {
// 注入dao
@Autowired
private UserDao userDao ;
public void transferAccounts(){
try {
// 第一步 开启事务
// 第二步 进行业务操作
// 给lucy扣1000
userDao.updateMoney("lucy",-1000);
// 给mary加1000
userDao.updateMoney("mary",1000);
/*
* 事务的提交与回滚,不了解的话可以进行SQL的学习
* 在SQL学习的事务学习阶段
* */
// 第三步 没有发生异常 则提交事务(进到事务执行完的状态)
}catch (Exception e){
// 第四步 若发生异常 则进行事务回滚(回到事务执行前的状态)
}
}
}
使用Spring进行事务管理操作
1、说明
1、事务添加到JavaE三层结构里的servile层(业务逻辑层)(理论上加在哪一层都可以,但是加在servile层最好,在实际应用过程中一般都加在servile层上)
2、在Spring进行事务管理操作的方式(两种):
(1) 编程式事务管理操作(不方便)
上面介绍事务逻辑步骤的代码中,指出的第一步第二步等操作的方法就是编程式事务管理操作, 会造成代码臃肿,不方便,一般不使用这种方式
(2) 声明式事务管理操作(常用)
在Spring中进行声明式事务管理,底层是使用AOP原理实现的
<1> 基于注解方式实现(简单,方便,常用)
<2> 基于xml配置文件方式实现(复杂,麻烦,不常用)
3、Spring事务管理中用到的API
(1) 提供了一个PlatformTransactionManager接口,代表事务管理器,这个接口针对不同框架提供了不同的实现类,使用不同的框架时具体使用不同的实现类
2、注解声明式事务管理
演示注解声明式事务管理
(1) 在spring配置文件中配置事务管理器
<!--创建事务管理器-->
<bean id="transactionManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
<!--注入数据源-->
<property name="dataSource" ref="dataSource"/>
</bean>
(2) 在spring配置文件中开启事务注解
<1> 引入tx名称空间
<2> 开启事务注解
bean.xml全部内容:
<?xml version="1.0" encoding="UTF-8"?>
<!--引入tx名称空间,context名称空间是之前引入的-->
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:tx="http://www.springframework.org/schema/tx"
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
http://www.springframework.org/schema/tx http://www.springframework.org/schema/tx/spring-tx.xsd">
<!--组件扫描-->
<context:component-scan base-package="com.hassder.spring5.demo06"/>
<!--数据库连接池-->
<bean id="dataSource" class="com.alibaba.druid.pool.DruidDataSource">
<property name="driverClassName" value="com.mysql.cj.jdbc.Driver"/>
<property name="url" value="jdbc:mysql:///spring5"/>
<property name="username" value="root"/>
<property name="password" value="123456"/>
</bean>
<!--配置JdbcTemplate对象-->
<bean id="jdbcTemplate" class="org.springframework.jdbc.core.JdbcTemplate">
<!--注入DataSource-->
<property name="dataSource" ref="dataSource"/>
</bean>
<!--创建事务管理器-->
<bean id="transactionManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
<!--注入数据源-->
<property name="dataSource" ref="dataSource"/>
</bean>
<!--开启事务注解-->
<tx:annotation-driven transaction-manager="transactionManager"/>
</beans>
(3) 在service类上(或者service类里对应方法上面)添加事务注解
<1> @Transactional, 这个注解添加到类上,可爷爷添加在具体方法上(如果添加到类上,就表示此类下的所有方法都添加了事务,若添加在方法上,就只表示为某一个具体的方法添加事务)
UserService类:
package com.hassder.spring5.demo06.service;
import com.hassder.spring5.demo06.dao.UserDao;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
@Service
@Transactional // 事务注解 添加在类上表示为此类下的所有方法添加事务
public class UserService {
// 注入dao
@Autowired
private UserDao userDao ;
// @Transactional // 添加在方法上,表示为单独一个方法添加事务
public void transferAccounts(){
// 给lucy扣1000
userDao.updateMoney("lucy",-1000);
// 手动添加异常
int i = 10/0 ;
// 给mary加1000
userDao.updateMoney("mary",1000);
}
}
然后再运行测试方法,查看运行结果(程序异常还在):
程序结果显示仍然是执行了一个方法之后遇到了异常
数据库表信息如下:
可以看到,在数据库的表中,lucy与mary的金额仍然是2000与3000(上面测试完之后手动操作数据恢复到了初始状态),并没有发生数据变化(确认我把表刷新了).
这说明,事务开启成功了,必须要在@Transactional注解下的方法整体无误的执行完的情况下,方法中的步骤才会对数据库的数据造成实质性的更改(提交),若是方法没有执行完(遇到了错误,异常等突发情况),则在程序中断之前执行过的部分也不会对数据库的数据造成实质性的更改(回滚)
声明式事务管理参数配置
整体说明
在我们上面演示过程中使用到了@Transactional注解,实际上这个注解并不只是一个简单的标记作用,在这个注解中还可以配置大量的与事务相关的参数,其中的常用参数有:
1、propagation: 事务传播行为
多事务方法直接进行调用,这个过程中事务是如何进行管理的
事务方法: 对数据库表数据进行变化的操作,(比如增删改,查不会导致数据变化,所以查不算)
比如说 一个方法A调用另一个方法B,就有多种不同情况,比如:
方法A有事务,B无事务; A有事务,B有事务; A无事务,B无事务; A无事务,B有事务.
在不同的情况之下A,B之间该怎么处理呢,在不同的情况之下A,B需不需要事务,或者说是需要合在一个事务中还是分开为两个事务?那么如果方法A中调用了更多的方法呢?
Spring框架事务传播行为有7种:
传播属性 | 描述 |
---|---|
REQUIRED | 如果有事务在运行,当前的方法就在这个事务内运行,否则就启动一个新的事务,并在自己的事务内运行 |
REQUIRED_NEW | 当前的方法必须启动新事务,并在它自己的事务内运行,如果有事务正在运行,应该将它挂起 |
SUPPORTS | 如果有事务在运行,当前的方法就在这个事务内运行,否则它可以不运行在事务中 |
NOT_SUPPORTE | 当前的方法不应该运行在事务中,如果有运行的事务,将它挂起 |
MANDATORY | 当前的方法必须运行在事务内部,如果没有正在运行的事务,就抛出异常 |
NEVER | 当前的方法不应该运行在事务中,如果有运行的事务,就抛出异常 |
NESTED | 如果有事务在运行,当前的方法就应该在这个事务的嵌套事务内运行,否则,就启动一个新的事务,并在它自己的事务内运行 |
比如说,现在有两个方法add()和update(),其中add()方法添加了事务,且在add()方法内调用了update()方法,而update()方法没有添加事务,如下:
@Transactional
public void add(){
// 调用update方法
update();
}
public void update(){
}
这个时候,我们如果使用不同的传播行为,那么系统会怎么做呢?
REQUIRED与REQUIRED_NEW是使用最多的,其它几种了解即可
REQUIRED: 如果add()方法本身就有事务,调用update()方法之后,update()方法使用当前add()中的事务
(如果add()方法中本身没有事务,则在调用update()方法之后,会创建新事务)
REQUIRED_NEW:使用add()方法钓鱼update()方法,无论add()方法是否有事务,都会创建一个新的事务提供给 update()来使用
设置方法:
@Transactional(propagation = Propagation.REQUIRED) // 不设置此值的时候,默认为REQUIRED
2、islation: 事务隔离级别
事务的四大特性中,有一个就是隔离性,即多事务操作之间不会互相影响
如果不考虑隔离性,就会产生很多问题
三个读的问题: 脏读、不可重复读、虚读(幻读)
脏读: 一个未提交事务读取到另一个未提交事务的数据
不可重复读: 一个未提交事务读取到另一个提交的事务中修改的数据
虚读(幻读): 一个未提交事务读取到另一个提交的事务中添加的数据
通过设置事务的隔离级别来解决这三个问题
隔离级别 | 脏读 | 不可重复读 | 虚读(幻读) |
---|---|---|---|
READ UNCOMMITTED(读未提交) | 有 | 有 | 有 |
READ COMMITTED(读已提交) | 无 | 有 | 有 |
REPEATABLE READ(可重复读) | 无 | 无 | 有 |
SERIALIZABLE(串行化) | 无 | 无 | 无 |
(有表示存在对应问题(即没有解决),无表示不存在对应问题(表示可以解决))
MySQL中默认隔离级别为REPEATABLE_READ
设置方式:
// 与其它参数同时设置
@Transactional(propagation = Propagation.REQUIRED,isolation = Isolation.REPEATABLE_READ)
// 单独设置
@Transactional(isolation = Isolation.REPEATABLE_READ)
3、timeout: 超时时间
事务需要在超时时间内进行提交, 若一直不提交,则到达超时时间后就会自动进行回滚
spring中超时时间的默认值为"-1"(不超时),我们可以手动修改超时时间(单位为秒)
设置方式:
// 设置超时时间为10秒
// 与其它参数同时设置
@Transactional(propagation = Propagation.REQUIRED,timeout = 10)
// 单独设置
@Transactional(timeout = 10)
4、readOnly: 是否只读
读: 查询操作; 写: 增删改操作
readOnly默认值为flase,表示不是只读,可以进行增删改查操作
可以手动设置readOnly值为true, 设置为true之后,只能进行查询操作
设置方式:
// 设置readOnly值为true
// 和其它参数同时设置
@Transactional(propagation = Propagation.REQUIRED , readOnly = true)
// 单独设置
@Transactional(readOnly = true)
5、rollbackFor: 回滚
设置出现哪些异常时进行事务回滚
6、noRollbackFor: 不回滚
设置出现哪些异常时不进行事务回滚
3、xml配置文件声明事务管理
在spring配置文件中进行配置:
第一步 配置事务管理器
第二步 配置事务(通知)
第三步 配置切入点和切面
(去掉UserService里的配置事务的注解)
新建一个bean2.xml用作配置:
<?xml version="1.0" encoding="UTF-8"?>
<!--引入tx名称空间,context名称空间是之前引入的-->
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:tx="http://www.springframework.org/schema/tx"
xmlns:context="http://www.springframework.org/schema/context"
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 https://www.springframework.org/schema/aop/spring-aop.xsd">
<!--组件扫描-->
<context:component-scan base-package="com.hassder.spring5.demo06"/>
<!--数据库连接池-->
<bean id="dataSource" class="com.alibaba.druid.pool.DruidDataSource">
<property name="driverClassName" value="com.mysql.cj.jdbc.Driver"/>
<property name="url" value="jdbc:mysql:///spring5"/>
<property name="username" value="root"/>
<property name="password" value="123456"/>
</bean>
<!--配置JdbcTemplate对象-->
<bean id="jdbcTemplate" class="org.springframework.jdbc.core.JdbcTemplate">
<!--注入DataSource-->
<property name="dataSource" ref="dataSource"/>
</bean>
<!--1 创建事务管理器-->
<bean id="transactionManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
<!--注入数据源-->
<property name="dataSource" ref="dataSource"/>
</bean>
<!--2 配置事务-->
<tx:advice id="txadvice">
<!--配置事务参数-->
<tx:attributes>
<!--指定在哪种规则的方法上添加事务-->
<!--
<tx:method name="transferAccounts"/>
即:<tx:method name="方法名全称"/>
或者:<tx:method name="transfer*"/>
表示方法名以transfer开头的所有方法都添加事务
标签内可加之前说的六个参数
-->
<tx:method name="transferAccounts"/>
</tx:attributes>
</tx:advice>
<!--3 配置切入点和切面-->
<aop:config>
<!--配置切入点-->
<aop:pointcut id="pt" expression="execution(* com.hassder.spring5.demo06.service.UserService.*(..))"/>
<!--配置切面-->
<aop:advisor advice-ref="txadvice" pointcut-ref="pt"/>
</aop:config>
</beans>
测试方法:
@Test
public void testD2(){
ApplicationContext applicationContext =
new ClassPathXmlApplicationContext("com/hassder/spring5/demo06/bean2.xml");
UserService userService = applicationContext.getBean("userService", UserService.class);
userService.transferAccounts();
}
运行结果:
数据库表信息如下:
模仿上面的内容,手动添加异常之后再尝试运行:
UserService类:
package com.hassder.spring5.demo06.service;
import com.hassder.spring5.demo06.dao.UserDao;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
@Service
// @Transactional // 事务注解 添加在类上表示为此类下的所有方法添加事务
public class UserService {
// 注入dao
@Autowired
private UserDao userDao ;
// @Transactional // 添加在方法上,表示为单独一个方法添加事务
public void transferAccounts(){
// 给lucy扣1000
userDao.updateMoney("lucy",-1000);
// 手动添加异常
int i = 10/0 ;
// 给mary加1000
userDao.updateMoney("mary",1000);
}
}
测试方法不变
运行结果:
数据库表信息:
虽然程序中执行了一句给lucy扣钱1000的语句,但是在数据库中,并没有观察到数量变化,说明事务成功了,在代码异常(没有全部执行成功)的情况下,进行了回滚
4、完全注解开发实现声明事务管理
创建配置类, 使用配置类代替xml配置文件
(恢复UserService类中的事务注解,去掉手动制造的异常)
package com.hassder.spring5.demo06.config;
import com.alibaba.druid.pool.DruidDataSource;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.jdbc.datasource.DataSourceTransactionManager;
import org.springframework.transaction.annotation.EnableTransactionManagement;
import javax.sql.DataSource;
@Configuration // 表示这是一个配置类
@ComponentScan(basePackages = "com.hassder") // 组件扫描
@EnableTransactionManagement // 开启事务
public class TxConfig {
// 创建数据库连接池
// 使用@Bean注解
// 可以参考bean文件里数据库连接池的信息,进行对比
// 创建一个DruidDataSource的实例并返回
@Bean
public DruidDataSource getDruidDataSource(){
DruidDataSource dataSource = new DruidDataSource();
dataSource.setDriverClassName("com.mysql.cj.jdbc.Driver");
dataSource.setUrl("jdbc:mysql:///spring5");
dataSource.setUsername("root");
dataSource.setPassword("123456");
return dataSource ;
}
// 创建JdbcTemplate对象
// 同上
// 因为在上面的方法中已经创建了DataSource对象,因此不需要在下面方法再次创建
// 直接将DataSource对象当成参数传入
// 系统到ioc容器中根据类型去找到DataSource将其传入
@Bean
public JdbcTemplate getJdbcTemplate(DataSource dataSource){
JdbcTemplate jdbcTemplate = new JdbcTemplate();
// 注入DataSource
jdbcTemplate.setDataSource(dataSource);
return jdbcTemplate ;
}
// 创建事务管理器对象
// 同上
@Bean
public DataSourceTransactionManager getDataSourceTransactionManager(DataSource dataSource){
DataSourceTransactionManager dataSourceTransactionManager = new DataSourceTransactionManager();
dataSourceTransactionManager.setDataSource(dataSource);
return dataSourceTransactionManager ;
}
}
测试方法:
@Test
public void testD3(){
ApplicationContext applicationContext =
new AnnotationConfigApplicationContext(TxConfig.class);
UserService userService = applicationContext.getBean("userService", UserService.class);
userService.transferAccounts();
}
运行结果:
数据库表信息:
执行成功
将UserService类中的异常添加进去再次测试
运行结果:
数据库表信息:
事务成功
六、Spring5的新功能
概要
1、 整个Spring5框架的代码基于Java8, 运行时兼容JDK9, 将许多不建议使用的类和方法从代码库中删除
2、 Spring5.0框架自带了通用的日志封装(也可以自行封装别的日志)
(1) Spring5已经移除Log4jConfigListener, 官方建议使用Log4j2
(2) Spring5框架整合Log4j2
(3) Spring5框架核心容器支持@Nullable注解
1、演示整合Log4j2
1、引入相关jar包
需要的jar包有:
log4j-api-xxx.jar
log4j-core-xxx.jar
log4j-slf4j-impl-xxx.jar
slf4j-api-xxx.jar
整篇笔记到目前为止,所用到的jar包有:
2、创建log4j2.xml配置文件
注意:这个配置文件名是固定的,不能改名
在src目录下添加log4j2.xml文件
log4j2.xml:
<?xml version="1.0" encoding="UTF-8"?>
<!--
<!–日志级别以及优先级牌序: OFF > FATAL > ERROR > WARN > INFO > DEBUG > TRACE > ALL –>
<!–status:Log4j2内部日志的输出级别,设置为TRACE对学习Log4j2非常有用 –>
<!–monitorInterval:定时检测配置文件的修改,有变化则自动重新加载配置,时间单位为秒,最小间隔为5s –>
<Configuration status="WARN" monitorInterval="600">
<!–properties:设置全局变量 –>
<properties>
<!–LOG_HOME:指定当前日志存放的目录 –>
<property name="LOG_HOME">logs</property>
<!–FILE_NAME:指定日志文件的名称 –>
<property name="FILE_NAME">test</property>
</properties>
<!–Appenders:定义日志输出目的地,内容和格式等 –>
<Appenders>
<!–Console:日志输出到控制台标准输出 –>
<Console name="Console" target="SYSTEM_OUT">
<!–pattern:日期,线程名,日志级别,日志名称,日志信息,换行 –>
<PatternLayout pattern="%d{yyyy-MM-dd HH:mm:ss.SSS} [%t] %-5level [%L] - %msg%n" />
</Console>
<!–RollingFile:日志输出到文件,下面的文件都使用相对路径 –>
<!–fileName:当前日志输出的文件名称 –>
<!–filePattern:备份日志文件名称,备份目录为logs下面以年月命名的目录,备份时使用gz格式压缩 –>
<RollingFile name="RollingFile" fileName="${LOG_HOME}/${FILE_NAME}.log"
filePattern="${LOG_HOME}/$${date:yyyy-MM}/${FILE_NAME}-%d{yyyy-MM-dd}-%i.log.gz">
<PatternLayout pattern="%d{yyyy-MM-dd HH:mm:ss.SSS} [%t] %-5level [%L] - %msg%n" />
<!–Policies:触发策略决定何时执行备份 –>
<Policies>
<!–TimeBasedTriggeringPolicy:日志文件按照时间备份 –>
<!–interval:每1天生成一个新文件,时间单位需要结合filePattern时间%d{yyyy-MM-dd} –>
<!–同理,如果要每1小时生成一个新文件,则改成%d{yyyy-MM-ddHH} –>
<!–modulate:对备份日志的生成时间纠偏,纠偏以0为基准进行,"0+interval"决定启动后第一次备份时间 –>
<TimeBasedTriggeringPolicy interval="1" modulate="true" />
<!–SizeBasedTriggeringPolicy:日志文件按照大小备份 –>
<!–size:指定日志文件最大为100MB,单位可以为KB、MB或GB –>
<SizeBasedTriggeringPolicy size="100MB" />
</Policies>
<!–DefaultRolloverStrategy:翻转策略决定如何执行备份 –>
<!–max:最多保存5个备份文件,结合时间使用后,在每个时间段内最多有5个备份,多出来的会被覆盖 –>
<!–compressionLevel:配置日志压缩级别,范围0-9,0不压缩,1压缩速度最快,9压缩率最好,目前只对于zip压缩文件类型有效 –>
<DefaultRolloverStrategy max="5" compressionLevel="1">
<!–Delete:删除匹配到的过期备份文件 –>
<!–maxDepth:由于备份文件保存在${LOG_HOME}/$${date:yyyy-MM},所以目录深度设置为2 –>
<Delete basePath="${LOG_HOME}" maxDepth="2">
<!–IfFileName:匹配文件名称 –>
<!–glob:匹配2级目录深度下的以.log.gz结尾的备份文件 –>
<IfFileName glob="*/*.log.gz" />
<!–IfLastModified:匹配文件修改时间 –>
<!–age:匹配超过180天的文件,单位D、H、M、S分别表示天、小时、分钟、秒–>
<IfLastModified age="180D" />
</Delete>
</DefaultRolloverStrategy>
</RollingFile>
</Appenders>
<!–Loggers:定义日志级别和使用的Appenders –>
<Loggers>
<!–name: 打印日志的类的包路径 –>
<!–additivity: true当前的Logger打印的日志附加到Root,false仅仅打印到RollingFile –>
<Logger name="org.apache.logging.log4j" level="ERROR" additivity="true">
<AppenderRef ref="RollingFile" />
</Logger>
<!–Root:日志默认打印到控制台 –>
<!–level日志级别: ALL < TRACE < DEBUG < INFO < WARN < ERROR < FATAL < OFF –>
<Root level="ERROR">
<AppenderRef ref="Console" />
</Root>
</Loggers>
</Configuration>
-->
<!--上面是完整的xml配置文件内容,在我们学习过程中,使用下面部分即可-->
<configuration status="INFO">
<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>
<loggers>
<root level="info">
<appender-ref ref="Console"/>
</root>
</loggers>
</configuration>
3、测试日志效果
在此前的代码中任选一段, 运行并查看控制台输出内容
可以看到 在控制台除了输出结果之外,还输出了一行日志内容,比较此前的同类输出结果,可以发现,在使用log4j2之前,系统也有一个日志内容输出,可以看上面的笔记,字体为红色的日志内容,配置了log4j2之后,log4j2的日志内容取代了之前的日志内容
修改日志级别为DEBUG:
<configuration status="DEBUG">
<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>
<loggers>
<root level="info">
<appender-ref ref="Console"/>
</root>
</loggers>
</configuration>
再次运行同一段代码查看结果:
可以明显看到,日志信息更多了,内容更详细
日常使用时级别设置为INFO即可
4、手动输出日志
写一个测试类:
package com.hassder.spring5.demo06.test;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
public class UserLog {
private static final Logger log = LoggerFactory.getLogger(UserLog.class) ;
public static void main(String[] args) {
log.info("hello,log4j2");
log.warn("hello,log4j2");
}
}
运行结果:
2、@Nullable注解和函数式注册对象
@Nullable注解
使用范围
此注解可以使用在方法上,属性上以及参数上
作用
使用在方法上时,表示方法返回值可以为空
使用在属性上时,表示属性值可以为空
使用在参数上时,表示参数值可以为空
函数式注册对象
Spring5核心容器支持函数式风格GenericApplicationContext
// 函数式风格创建对象,交给Spring管理
User类:
package com.hassder.spring5.demo06.test;
public class User {
}
测试方法:
@Test
public void testGenericApplicationContext(){
//1 创建GenericApplicationContext对象
GenericApplicationContext context = new GenericApplicationContext();
//2 调用context的方法进行对象注册
context.refresh();
// 方式1
// 获取在Spring中注册的对象
// 使用类的全路径
context.registerBean(User.class,()->new User());
User user = (User) context.getBean("com.hassder.spring5.demo06.test.User");
System.out.println(user);
// 方式2
// 或者用下面这种方式
context.registerBean("u1",User.class,()->new User());
User user1 = (User) context.getBean("u1");
System.out.println(user1);
}
运行结果:
和传统方式不同的是, 我们使用的是一个GenericApplicationContext类对象,将我们想要的对象A创建并提交给它,然后在我们需要使用对象A的时候再从GenericApplicationContext对象中通过getBean()方法获取, 在获取的时候有两种方式, 如果我们在提交对象A的时候没有给对象"取名",那么我们就要根据对象A类的全路径来获取,如果在提交对象A的时候已经给对象A"取名"了,那么直接根据取的名字来获取即可
3、Spring5支持整合JUnit5
(1) 整合JUnit4
在以往的测试中,几乎每一个测试方法都需要有如下内容(加载配置文件等):
ApplicationContext applicationContext =
new ClassPathXmlApplicationContext("com/hassder/spring5/demo06/bean1.xml");
UserService userService = applicationContext.getBean("userService", UserService.class);
此过程太为繁琐,因此Spring提供了优化的办法:
第一步 引入Spring相关针对测试的依赖
spring-test-xxx.jar(Spring自带jar包)
第二步 创建测试类, 使用注解方式完成
package com.hassder.spring5.demo06.test;
import com.hassder.spring5.demo06.service.UserService;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
@RunWith(SpringJUnit4ClassRunner.class) // 单元测试框架
@ContextConfiguration("classpath:com/hassder/spring5/demo06/bean1.xml") // 加载配置文件
public class TestJUnit4 {
/*由于在注解中已经加载过配置文件了
所以可以直接注入UserService对象
供该类的所有测试方法使用
* */
@Autowired
private UserService userService ;
@Test
public void test1(){
userService.transferAccounts();
}
}
在此修改了一个以前写的测试类用作参考:
package com.hassder.spring5.demo05.testdemo;
import com.hassder.spring5.demo05.entity.Book;
import com.hassder.spring5.demo05.service.BookService;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.ApplicationContext;
import org.springframework.context.support.ClassPathXmlApplicationContext;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
import java.util.ArrayList;
import java.util.List;
@RunWith(SpringJUnit4ClassRunner.class) // 单元测试框架
@ContextConfiguration("classpath:com/hassder/spring5/demo05/bean1.xml") // 加载配置文件
public class TestDemo {
/*由于在注解中已经加载过配置文件了
所以可以直接注入UserService对象
供该类的所有测试方法使用*/
@Autowired
private BookService bookService ;
@Test
public void testBookAdd(){
/*ApplicationContext applicationContext =
new ClassPathXmlApplicationContext("com/hassder/spring5/demo05/bean1.xml");
BookService bookService = applicationContext.getBean("bookService", BookService.class);*/
// 暂时手动创建对象用于添加
Book book = new Book("1","javaEE","a") ;
bookService.addBook(book);
}
@Test
public void testBookUpdate(){
/*ApplicationContext applicationContext =
new ClassPathXmlApplicationContext("com/hassder/spring5/demo05/bean1.xml");
BookService bookService = applicationContext.getBean("bookService", BookService.class);*/
// 暂时手动创建对象用于添加
Book book = new Book("1","javaEE","bbb") ;
bookService.updateBook(book);
}
@Test
public void testBookDelete(){
/*ApplicationContext applicationContext =
new ClassPathXmlApplicationContext("com/hassder/spring5/demo05/bean1.xml");
BookService bookService = applicationContext.getBean("bookService", BookService.class);*/
String id = "1" ;
bookService.deleteBook(id);
}
@Test
public void testSelectCount(){
/*ApplicationContext applicationContext =
new ClassPathXmlApplicationContext("com/hassder/spring5/demo05/bean1.xml");
BookService bookService = applicationContext.getBean("bookService", BookService.class);*/
int count = bookService.findCount();
System.out.println("表中共有"+count+"条数据");
}
@Test
public void testFindOne(){
/*ApplicationContext applicationContext =
new ClassPathXmlApplicationContext("com/hassder/spring5/demo05/bean1.xml");
BookService bookService = applicationContext.getBean("bookService", BookService.class);*/
String id = "5" ;
Book book = bookService.findOne(id);
System.out.println(book);
System.out.println(book.getUserId()+" || "+
book.getUsername()+" || "+book.getUstatus());
}
@Test
public void testFindAll(){
/*ApplicationContext applicationContext =
new ClassPathXmlApplicationContext("com/hassder/spring5/demo05/bean1.xml");
BookService bookService = applicationContext.getBean("bookService", BookService.class)*/;
List<Book> books = bookService.findAll();
for (Book book:books){
System.out.println(book.getUserId()+" || "+
book.getUsername()+" || "+book.getUstatus());
}
}
@Test
public void testBatchAdd(){
/*ApplicationContext applicationContext =
new ClassPathXmlApplicationContext("com/hassder/spring5/demo05/bean1.xml");
BookService bookService = applicationContext.getBean("bookService", BookService.class);*/
List<Object[]> batchArgs = new ArrayList<>();
Object[] o1 = {"8","C语言","a"};
Object[] o2 = {"9","C语言","b"};
Object[] o3 = {"10","C语言","c"};
batchArgs.add(o1);
batchArgs.add(o2);
batchArgs.add(o3);
bookService.batchAdd(batchArgs);
}
@Test
public void testBatchUpdate(){
/*ApplicationContext applicationContext =
new ClassPathXmlApplicationContext("com/hassder/spring5/demo05/bean1.xml");
BookService bookService = applicationContext.getBean("bookService", BookService.class);*/
List<Object[]> batchArgs = new ArrayList<>();
// 注意数据的顺序,要与sql的?对应
Object[] o1 = {"C++","c","8"};
Object[] o2 = {"C++","a","9"};
Object[] o3 = {"C++","b","10"};
batchArgs.add(o1);
batchArgs.add(o2);
batchArgs.add(o3);
bookService.batchUpdate(batchArgs);
}
@Test
public void testBatchRemove(){
/*ApplicationContext applicationContext =
new ClassPathXmlApplicationContext("com/hassder/spring5/demo05/bean1.xml");
BookService bookService = applicationContext.getBean("bookService", BookService.class);*/
List<Object[]> batchArgs = new ArrayList<>();
// 注意数据的顺序,要与sql的?对应
Object[] o1 = {"8"};
Object[] o2 = {"9"};
Object[] o3 = {"10"};
batchArgs.add(o1);
batchArgs.add(o2);
batchArgs.add(o3);
bookService.batchRemove(batchArgs);
}
}
节约了测试时的步骤,提高了效率
(2) JUnit5
JUnit5在JUnit4的前提下,使用了一些新的注解,进一步精简了代码
第一步 引入JUnit5的jar包
点击OK, 而后等待其自动下载完成即可
第二步 创建测试类,使用注解方式完成(类似JUnit4,部分注解不同)
第一种方式:
package com.hassder.spring5.demo06.test;
import com.hassder.spring5.demo06.service.UserService;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.junit.jupiter.SpringExtension;
@ExtendWith(SpringExtension.class)
@ContextConfiguration("classpath:com/hassder/spring5/demo06/bean1.xml")
public class TestJUnit5 {
@Autowired
private UserService userService ;
@Test
public void test1(){
userService.transferAccounts();
}
}
第二种方式:
package com.hassder.spring5.demo06.test;
import com.hassder.spring5.demo06.service.UserService;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.test.context.junit.jupiter.SpringJUnitConfig;
/*@SpringJUnitConfig是一个复合注解
这一个注解相当于@ExtendWith与@ContextConfiguration两个注解
* */
@SpringJUnitConfig(locations = "classpath:com/hassder/spring5/demo06/bean1.xml")
public class TestJUnit5 {
@Autowired
private UserService userService ;
@Test
public void test1(){
userService.transferAccounts();
}
}
4、SpringWebFlux
(1) SpringWebFlux基本介绍
<1> 是Spring5添加的新的模块,是用于web开发的, 功能与SpringMVC类似的, WebFlux是使用当前一种目前比较流行的响应式编程出现的框架
<2> 使用传统web框架, 比如SpringMVC, 这些基于Servlet容器, WebFlux是一种异步非阻塞的框架, 异步非阻塞是在Serviet3.1以后才支持的, 其核心是基于Reactor的相关API实现的
什么是异步非阻塞:
异步与同步; 非阻塞与阻塞
这两者都是针对的对象不一样
异步与同步是针对调用者而言的,比如说,调用者向另一方发送一个请求,必须要等到另一方有返回信息(反馈)之后,调用方才进行其它操作,这就是同步(比如说使用Scanner获取控制台输入时,程序在运行到对应语句之后会停下来等待控制台信息的输入,在输入完成之后才继续进行后续操作),而如果调用者给另一方发送请求之后, 无论对方是否有反馈,调用者都会继续进行其它动作,就叫异步
阻塞与非阻塞是针对被调用者而言的, 如果被调用者在收到调用者的请求后,即时给予了反馈,就叫非阻塞,如果没有即时反馈,就叫阻塞(比如说A发送请求给B,希望B进行一个添加操作,B收到请求之后,就去添加,添加完了之后再给A返回信息,这就是阻塞,而如果B在收到请求后,直接给A返回了信息,而后才进行添加操作,这就是非阻塞),通俗来讲就是,需要等待的就是阻塞,不需要等待的就是非阻塞
<3> WebFlux特点:
第一点: 异步非阻塞式, 可以在有限的资源下提高系统吞吐量,伸缩性(可以在有限的资源下处理更多请求),以Reactor为基础实现响应式编程
第二点: 函数式编程, Spring5框架基于Java8, WebFlux可以使用Java8的函数式编程方式实现路由请求
<4> SpringWebFlux与SpringMVC比较
a. 两个框架都可以使用注解方式, 都可以运行在Tomcat等容器中
b. SpringMVC采用命令式编程,WebFlux采用异步响应式编程
(2) 响应式编程
1 基本原理
后续部分内容使用SpringBoot项目
如何创建一个SpringBoot项目 由于之前我使用的是JDK15(idea版本为20.2.3), 后续有部分内容使用到JDK8(因为这一部分在JDK8之前与JDK8之后有较大差距,而现在实际应用过程中普遍使用的都是JDK8,因此使用JDK8来演示), 因此我直接换了我的另一个idea版本为19.3的, 我在这个版本的idea里使用的JDK为JDK8,我使用的19.3版本的idea中有部分汉化:
1. 什么是响应式编程
响应式编程式一种面向数据流和变化传播的编程范式, 这意味着可以在编程语言中很方便地表达静态或者动态的数据流, 而相关的计算模型会自动将变化的值通过数据流进行传播
命令式与响应式: 以"a=b+c;"为例, 在代码中,它表示将b+c的运算结果赋值给a,假如b=1,c=2,那么a就等于3, 在得到a=3这个结果之后,假如我们把b的值修改为4, 如果是命令式编程,则a依旧为3,不变, 但是在响应式编程中,a的值会重新进行计算,得到a=6的结果
2. Java8及其之前版本
提供的观察者模式的两个类Observer和Observable
package com.hassder.demo1.reactor8;
import java.util.Observable;
public class ObserverDemo extends Observable {
public static void main(String[] args) {
ObserverDemo observer = new ObserverDemo();
// 添加观察者
observer.addObserver((o,arg) -> {
System.out.println("发生变化");
});
observer.addObserver((o,arg) -> {
System.out.println("被观察者通知,准备改变");
});
observer.setChanged(); // 数据变化
observer.notifyObservers(); // 通知
}
}
3. Java9及其之后版本
使用Flow类取代了之前的两个类Observer和Observable
2 Reactor实现
<1> 在进行响应式编程操作中, 需要满足Reactive规范,而Reactor就是满足Reactive规范的一个框架,
<2> 在Reactor中有两个类Mono和Flux, 这两个类都实现了接口Publisher,这两个类提供了丰富的操作符,Flux对象实现发布者,返回N个元素,Mono也实现了发布者,但却是返回或1个元素
<3> Flux和Mono都是数据流的发布者, 使用Flux和Mono都可以发出三种数据信号: 元素值,错误信号,与完成信号,其中,错误信号与完成信号都代表终止信号,完成信号会告诉订阅者数据流结束了,而错误信号会终止数据流,同时将错误信息传递给订阅者
<4> 代码演示Flux和Mono
1.引入依赖(pom.xml中):
<dependency>
<groupId>io.projectreactor</groupId>
<artifactId>reactor-core</artifactId>
<version>3.1.5.RELEASE</version>
</dependency>
引入方式应该不用多说吧,在pom.xml文件中,肯定存在别的依赖,将其与其它依赖并列即可(都放在<dependencies> </dependencies>标签对内部即可),如果没有别的依赖,没有<dependencies> </dependencies>标签对,就自己写一个<dependencies> </dependencies>,然后将上面的依赖信息复制进去即可
2.代码演示:
package com.hassder.demo1.reactor8;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;
import java.util.Arrays;
public class TestReactor {
public static void main(String[] args) {
// 最基础的方法 使用just方法直接声明相关元素
// Flux可以一次发送多个
Flux.just(1,2,3,4) ;
// Mono一次发送0或者1个
Mono.just(1) ;
// 其它方法
Integer[] arr = {1,2,3,4};
// fromArray方法传入数组
// Mono没有fromArray方法.因为Mono一次只能传递0或1个
Flux.fromArray(arr);
// fromIterable方法传入集合
Flux.fromIterable(Arrays.asList(arr));
// fromStream方法传入stream流
Flux.fromStream(Arrays.asList(arr).stream());
}
}
<5> 三种信号的特点
错误信号和完成信号都是终止信号,不能共存
如果没有发送任何元素值,而是直接发送错误信号或者完成信号
如果没有错误信号也没有完成信号,表示是无限数据流
<6> 调用just方法或者其它发只是声明数据流,数据流并没有发出,只有"订阅"之后才会触发数据流,不订阅是什么都不会发生的
<7> 操作符
对数据流进行一道道操作,称为操作符,比如工厂流水线上的一个个步骤
map 元素映射为新元素
flatMap 元素映射为流
(3) SpringWebFlux执行流程和核心API
SpringWebFlux基于Reactor, 默认使用的容器是Netty , Netty是一个高性能的,基于异步事件驱动的NIO(非阻塞)框架
<1> Netty
具体内容较多,此处不提供
<2> SpringWebFlux执行过程和SpringMVC相似
SpringWebFlux核心控制器DispatchHandler, 实现类接口WebHandler
接口WebHandler中有一个handle方法
<3> SpringWebFlux里的DispatcherHandler,负责请求的处理
HandlerMapping: 请求查询到处理的方法
HandlerAdapter: 真正负责请求处理
HandlerResultHandler: 响应结果处理
<4> SpringWebFlux要实现函数式编程, 有两个接口: RouterFunction(路由处理)和HandlerFunction(处理函数)
(4) SpringWebFlux(基于注解编程模型)
使用注解编程模型方式,和之前SpringMVC使用相似, 只需要把相关依赖配置到项目中,SpringBoot自动配置相关运行容器, 默认情况下使用Netty服务器
第一步: 创建SpringBoot工程,引入WebFlux相关依赖
创建工程(略)后,修改pom.xml文件部分内容
将以下内容修改:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter</artifactId>
</dependency>
修改为:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-webflux</artifactId>
</dependency>
第二步: 配置启动的端口号
打开项目src目录下的resources目录里的application.properties文件(初始状态下,此文件内容为空),在里面配置启动的端口号
#server.port=端口号
server.port=8081
第三步 创建包和相关接口、类
创建接口 定义操作的方法:
package com.hassder.webfluxdemo1.service;
import com.hassder.webfluxdemo1.entity.User;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;
// 用户操作的接口
public interface UserService {
// 根据idC查询用户
Mono<User> getUserById(int id) ;
// 查询所有用户
Flux<User> getAllUser() ;
// 添加用户
Mono<Void> saveUserInfo (Mono<User> user) ;
}
接口实现类:
package com.hassder.webfluxdemo1.service.impl;
import com.hassder.webfluxdemo1.entity.User;
import com.hassder.webfluxdemo1.service.UserService;
import org.springframework.stereotype.Repository;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;
import java.util.HashMap;
import java.util.Map;
@Repository // 交给Spring管理
public class UserServiceImpl implements UserService {
// 并没有实际操作数据库
// 创建map集合模拟数据库存储数据
private final Map<Integer,User> users = new HashMap<>() ;
public UserServiceImpl(){
this.users.put(1,new User("lucy","女",20)) ;
this.users.put(2,new User("mary","男",20)) ;
this.users.put(3,new User("jack","男",20)) ;
}
@Override
public Mono<User> getUserById(int id) {
return Mono.justOrEmpty(this.users.get(id));
}
@Override
public Flux<User> getAllUser() {
return Flux.fromIterable(this.users.values());
}
@Override
public Mono<Void> saveUserInfo(Mono<User> userMono) {
// userMono.doOnNext 取出userMono里的值(User类型对象)
// person就是userMono里的User类型对象
return userMono.doOnNext(person -> {
// 向map里放值
int id = users.size() + 1;
users.put(id,person) ;
}).thenEmpty(Mono.empty()) ;
// thenEmpty(Mono.empty()) 清空
// 把Mono<User>变成Mono<Void>
}
}
创建controller
package com.hassder.webfluxdemo1.controller;
import com.hassder.webfluxdemo1.entity.User;
import com.hassder.webfluxdemo1.service.UserService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;
@RestController
public class UserController {
// 注入service
@Autowired
private UserService userService ;
// id查询
@GetMapping("/user/{id}")
public Mono<User> getUserId(@PathVariable int id){
return userService.getUserById(id) ;
}
// 查询所有
@GetMapping("/user")
public Flux<User> getUsers(){
return userService.getAllUser() ;
}
// 添加操作
@PostMapping("/saveuser")
public Mono<Void> saveUser(@RequestBody User user){
Mono<User> userMono = Mono.just(user) ;
return userService.saveUserInfo(userMono) ;
}
}
测试:
先启动
测试使用id查询:
测试查询所有:
说明:
使用SpringMVC实现,是同步非阻塞,基于SpringMVC+Service+Tomcat实现
SpringWebFlux实现,是异步非阻塞,基于SpringWebFlux+Reactor+Netty实现
(5) SpringWebFlux(基于函数式编程模型)
-
在使用函数式编程模型操作的时候,需要自己初始化服务器
-
基于函数式编程模型操作的时候,有两个核心接口:RouterFunction(实现路由功能,请求转发个对应的handler),和HandlerFunction(处理请求生成响应的函数).核心任务是定义这两个函数式接口的实现并且启动需要的服务器
-
SpringWebFlux请求和响应不再是ServletRequest和ServletResponse,而是ServerRequest和ServerResponse
第一步 把注解编程模型工程复制一份,删除里面的controller包(基于函数式编程模型实现,不需要这一部分)
第二步 创建Handler(具体实现办法)
创建handler包
创建类:
package com.hassder.webfluxdemo1.handler;
import com.hassder.webfluxdemo1.entity.User;
import com.hassder.webfluxdemo1.service.UserService;
import org.springframework.http.MediaType;
import org.springframework.web.reactive.function.server.ServerRequest;
import org.springframework.web.reactive.function.server.ServerResponse;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;
import static org.springframework.web.reactive.function.BodyInserters.fromObject;
public class UserHandler {
private final UserService userService ;
public UserHandler(UserService userService){
this.userService = userService ;
}
// 根据id查询
public Mono<ServerResponse> getUserById(ServerRequest request){
// 获取id值
int userId = Integer.valueOf(request.pathVariable("id"));
// 空值处理
Mono<ServerResponse> notFond = ServerResponse.notFound().build();
// 调用service中的方法得到数据
Mono<User> userMono = this.userService.getUserById(userId);
// 把userMono进行转换 返回
// 使用Reactor操作符flatMap
return userMono.flatMap(person -> ServerResponse.ok().contentType(MediaType.APPLICATION_JSON).body(fromObject(person))).switchIfEmpty(notFond);
}
// 查询所有
public Mono<ServerResponse> getAllUsers(){
// 调用service得到结果
Flux<User> users = this.userService.getAllUser() ;
return ServerResponse.ok().contentType(MediaType.APPLICATION_JSON).body(users,User.class);
}
// 添加
public Mono<ServerResponse> saveUser(ServerRequest request){
Mono<User> userMono = request.bodyToMono(User.class);
return ServerResponse.ok().build(this.userService.saveUserInfo(userMono));
}
}
第三步 初始化服务器,编写Router
第四步 创建服务器完成适配
package com.hassder.webfluxdemo1;
import com.hassder.webfluxdemo1.handler.UserHandler;
import com.hassder.webfluxdemo1.service.UserService;
import com.hassder.webfluxdemo1.service.impl.UserServiceImpl;
import org.springframework.http.server.reactive.HttpHandler;
import org.springframework.http.server.reactive.ReactorHttpHandlerAdapter;
import org.springframework.web.reactive.function.server.RouterFunction;
import org.springframework.web.reactive.function.server.RouterFunctions;
import org.springframework.web.reactive.function.server.ServerResponse;
import reactor.netty.http.server.HttpServer;
import java.io.IOException;
import static org.springframework.http.MediaType.APPLICATION_JSON;
import static org.springframework.web.reactive.function.server.RequestPredicates.GET;
import static org.springframework.web.reactive.function.server.RequestPredicates.accept;
import static org.springframework.web.reactive.function.server.RouterFunctions.toHttpHandler;
public class Server {
// 最终调用
public static void main(String[] args) throws IOException {
Server server = new Server();
server.createReactorServer();
System.out.println("enter to exit");
System.in.read();
}
//第三步 创建Router路由
public RouterFunction<ServerResponse> routingFunction(){
// 创建handler对象
UserService userService = new UserServiceImpl();
UserHandler handler = new UserHandler(userService);
// 设置路由
return RouterFunctions.route(
GET("/user/{id}").and(accept(APPLICATION_JSON)),handler::getUserById)
.andRoute(GET("/users").and(accept(APPLICATION_JSON)),handler::getAllUsers);
}
// 第四步 创建服务器完成适配
public void createReactorServer(){
// 路由和handler适配
RouterFunction<ServerResponse> route = routingFunction() ;
HttpHandler httpHandler = toHttpHandler(route) ;
ReactorHttpHandlerAdapter adapter = new ReactorHttpHandlerAdapter(httpHandler);
// 创建服务器
HttpServer httpServer = HttpServer.create();
httpServer.handle(adapter).bindNow();
}
}
测试: 启动Server里的main 进行测试:
程序结果:
浏览器测试:
使用WebClient调用
提示: 要使用这个必须要先启动上面启动Server里的main方法, 并且端口号要调一致(上面的main每次运行端口号不一定相同)
package com.hassder.webfluxdemo1;
import com.hassder.webfluxdemo1.entity.User;
import org.springframework.http.MediaType;
import org.springframework.web.reactive.function.client.WebClient;
import reactor.core.publisher.Flux;
public class Client {
public static void main(String[] args) {
// 调用服务器地址
// http://127.0.0.1:60575 本机ip:端口号
WebClient webClient = WebClient.create("http://127.0.0.1:60575");
//根据id查询
String id = "1" ;
User user = webClient.get().uri("/user/{id}", id).accept(MediaType.APPLICATION_JSON).retrieve().bodyToMono(User.class).block();
System.out.println(user.getName()+" "+user.getGender()+" "+user.getAge());
System.out.println("-----------------------------------------------");
// 查询所有
Flux<User> userFlux = webClient.get().uri("/users").accept(MediaType.APPLICATION_JSON).retrieve().bodyToFlux(User.class);
userFlux.map(person -> person.getName()).buffer().doOnNext(System.out::println).blockFirst();
}
}
程序结果: