一、概述
1.Spring5框架概述
1、Spring 是轻量级的开源的 JavaEE 框架
2、Spring 可以解决企业应用开发的复杂性
3、Spring 有两个核心部分:IOC 和 Aop
(1)IOC:控制反转,把创建对象过程交给 Spring 进行管理
(2)Aop:面向切面,不修改源代码进行功能增强
4、Spring 特点
(1)方便解耦,简化开发
(2)Aop 编程支持
(3)方便程序测试
(4)方便和其他框架进行整合
(5)方便进行事务操作
(6)降低 API 开发难度
二、入门案例
1.下载
可直接访问下载网址:https://repo.spring.io/ui/native/release/org/springframework/spring/
第一步:
第二步:
第三步:
第四步:
第五步:
第六步:
第七步:
注:由于现在已会使用maven进行项目管理,则使用maven来构建Spring项目,而不是用上方下载jar导入项目的方式
2.项目构建
第一步:
第二步:
第三步:
第四步:
第五步:
第六步:
编写测试代码,点击进行测试
测试时报出以下异常:(这是由于运行时找不到Spring配置文件造成的)
解决方法:
解决方法参考博客:https://blog.csdn.net/qq_32575047/article/details/78243314
https://blog.csdn.net/weixin_39903133/article/details/87258360
测试通过结果:
三、IOC
IOC:控制反转(Inversion of Control,缩写为lOC),是面向对象编程中的一种设计原则,可以用来减低计算机代码之间的耦合度。其中最常见的方式叫做依赖注入(Dependency Injection,简称DI),还有一种方式叫“依赖查找”(Dependency Lookup)。通过控制反转,对象在被创建的时候,由一个调控系统内所有对象的外界实体将其所依赖的对象的引用传递给它。也可以说,依赖被注入到对象中。(简单理解:控制反转,把对象创建和对象之间的调用过程,交给 Spring 进行管理)
3.1 IOC 底层原理
- xml 解析
- 工厂模式
- 反射
IOC底层原理的实现例子:
3.2 IOC接口
1、IOC 思想基于 IOC 容器完成,IOC 容器底层就是对象工厂
2、Spring 提供 IOC 容器实现两种方式:(两个接口)
- BeanFactory:IOC 容器基本实现,是Spring内部的使用接口,不提供开发人员进行使用
- 加载配置文件时候不会创建对象,在获取对象(使用)才去创建对象
- ApplicationContext:BeanFactory接口的子接口,提供更多更强大的功能,一般由开发人员进行使用
- 加载配置文件时候就会把在配置文件对象进行创建(虽然这种方式会浪费时间,但我们仍使用这种方式,因为我们应该把耗时的操作留给服务器来进行,而不应该让程序在使用时来耗时)
3、ApplicationContext 接口主要是有以下两个实现类
public class TestSpring5 {
@Test
public void testAdd(){
//1.加载Spring配置文件
ApplicationContext context = new ClassPathXmlApplicationContext("bean1.xml");
//2.获取配置文件创建的对象
User user = context.getBean("user", User.class);
System.out.println(user);
user.add();
}
}
在上方测试程序中,加载Spring配置文件时,若使用FileSystemXmlApplicationContext类则在传参时应该使用“Spring配置文件在盘符中的位置”即盘符路径作为参数;若使用ClassPathXmlApplicationContext类则应使用类路径即classpath作为参数
具体实现说明如下:
4、BeanFactory接口
3.3 IOC操作Bean管理
- 什么是 Bean 管理
- Bean 管理指的是两个操作
- Spring 创建对象
- Spirng 注入属性
- Bean 管理操作有两种方式
- 基于 xml 配置文件方式实现
- 基于注解方式实现
3.3.1 基于XML配置文件方式
1.基于xml方式创建对象
(1)在 spring 配置文件中,使用 bean 标签,标签里面添加对应属性,就可以实现对象创建
(2)在 bean 标签有很多属性,介绍常用的属性
id 属性:唯一标识
class 属性:类全路径(包类路径)
(3)创建对象时候,默认是执行无参数构造方法完成对象创建
如下图:
2.基于xml方式注入属性
DI:依赖注入,就是注入属性(要在创建好对象后才能进行)
①第一种注入方式:使用set方法进行注入
(1)创建类,定义属性和对应的 set 方法
public class Book {
//创建属性
private String bname;
private String bauthor;
//创建属性对应的 set 方法
public void setBname(String bname) {
this.bname = bname;
}
public void setBauthor(String bauthor) {
this.bauthor = bauthor;
}
//重写了toString方法
}
(2)在 spring 配置文件配置对象创建,配置属性注入
<!--2 set 方法注入属性-->
<bean id="book" class="com.atguigu.spring5.Book">
<!--使用 property完成属性注入 name:类里面属性名称 value:向属性注入的值-->
<property name="bname" value="易筋经"></property>
<property name="bauthor" value="达摩老祖"></property>
</bean>
编写测试代码
@Test
public void testBook(){
ApplicationContext context = new ClassPathXmlApplicationContext("bean1.xml");
Book book = context.getBean("book", Book.class);
System.out.println(book); //Book{bname='易筋经', bauthor='达摩'}
}
//从输入结果可以看出在配置文件成功创建对象并给属性赋值
②第二种注入方式:使用有参构造函数进行注入
(1)创建类,定义属性和对应有参构造函数
public class Book {
private String bname;
private String bauthor;
public Book(String bname, String bauthor) {
this.bname = bname;
this.bauthor = bauthor;
}
//重写了toString方法
(2)在 spring 配置文件配置利用有参构造函数创建对象同时给属性赋值
<bean id="book" class="Bean.Book">
<!--使用constructor-arg标签调用有参构造创建对象完成-->
<constructor-arg name="bname" value="生死疲劳"/>
<constructor-arg name="bauthor" value="莫言"/>
<!--同样也可以使用<constructor-arg index="0" value="生死疲劳"/>进行配置,其中index表示有参构造函数中的第几个参数,0表示第一个参数-->
</bean>
编写测试代码
@Test
public void testBook(){
ApplicationContext context = new ClassPathXmlApplicationContext("bean1.xml");
Book book = context.getBean("book", Book.class);
System.out.println(book); //Book{bname='生死疲劳', bauthor='莫言'}
}
③使用p名称空间注入(了解)
使用 p 名称空间注入,可以简化基于 xml 配置方式
(1)第一步 添加 p 名称空间在配置文件中
<?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"
xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd">
(2)第二步 进行属性注入,在 bean 标签里面进行操作
<bean id="book" class="Bean.Book" p:bname="九阳真经" p:bauthor="无名氏"/>
注:这种方式的底层仍是使用set方法进行注入,只是对写法的一种简化,所以实体类也应该具备对属性的set方法和无参构造函数
3.基于xml方式注入属性-字面量
①注入null值
比如,要给下方Book类的address属性注入空值
public class Book {
private String bname;
private String bauthor;
private String address;
public void setBname(String bname) {
this.bname = bname;
}
public void setBauthor(String bauthor) {
this.bauthor = bauthor;
}
public void setAddress(String address) {
this.address = address;
}
}
则可以通过<null/>
标签进行注入
<bean id="book" class="Bean.Book">
<property name="bname" value="易筋经"/>
<property name="bauthor" value="达摩"/>
<property name="address">
<null/>
</property>
</bean>
②注入特殊符号
- 把<>使用
<
>
进行转义 - 把带特殊符号内容写到 CDATA
<bean id="book" class="Bean.Book">
<property name="bname" value="易筋经"/>
<property name="bauthor" value="达摩"/>
<!--注入特殊符号-->
<property name="address">
<value><{% gallery %}![CDATA[<<南京>>]]>
```
##### 4.基于xml方式注入属性-外部bean
(1)创建两个类 service 类和 dao 类
(2)在 service 中需要调用 dao 里面的方法
(3)在 spring 配置文件中进行配置
```java
public class UserDaoImpl implements UserDao {
@Override
public void update() {
System.out.println("daoImpl update........");
}
}
```
```java
public class UserService {
//需要创建UserDao对象属性
private UserDao userDao;
//声明set方法
public void setUserDao(UserDao userDao) {
this.userDao = userDao;
}
public void add(){
System.out.println("service add........ ");
userDao.update();
}
}
```
```xml
```
```java
//测试程序
@Test
public void testService(){
ApplicationContext context = new ClassPathXmlApplicationContext("bean2.xml");
UserService userService = context.getBean("userService", UserService.class);
userService.add();
//service add........
//daoImpl update........
//从输入结果可以看出userService在调用自身的add方法时同样也调用了userDao的update方法,说明对象创建成功并且成功给外部bean属性赋值
}
```
##### 5.基于xml方式注入属性-内部bean
(1)一对多关系:部门和员工
一个部门有多个员工,一个员工属于一个部门
部门是一,员工是多
(2)在实体类之间表示**一对多**关系,员工表示所属部门,使用对象类型属性进行表示
```java
//首先创建出员工类和部门类,并表示出它们的关联关系
public class Department {
private String dname;
public void setDname(String dname) {
this.dname = dname;
}
}
public class Employee {
private String ename;
private String egender;
//员工属于某个部门,用对象形式表示
private Department department;
public void setEname(String ename) {
this.ename = ename;
}
public void setEgender(String egender) {
this.egender = egender;
}
public void setDepartment(Department department) {
this.department = department;
}
}
//均重写了toString方法
```
```xml
```
```java
//测试代码
@Test
public void testDepartment(){
ApplicationContext context = new ClassPathXmlApplicationContext("bean3.xml");
Employee employee = context.getBean("employee", Employee.class);
System.out.println(employee);//Employee{ename='lucy', egender='女', department=Department{dname='安保部'}}
}
```
##### 6.基于xml方式注入属性-级联赋值
同样使用内部bean的例子
###### ①第一种方式:与外部bean的写法相同
```xml
```
###### ②第二种方式:
```xml
```
**注:这种方式需要在赋值类提供被赋值类的get方法,以便于能获取到被赋值类对象,然后对被赋值类的属性进行赋值**。比如,上述例子若使用第二种方式的级联赋值时,应在employee类中提供一个getDepartment方法返回department对象属性
另外,由于在测试第二种方式时并未删除` `,但又添加了` `,而输出结果又是财务部,可以发现**第二种方式的优先级要高于第一种方式**
##### 7.基于xml方式注入集合类型属性
###### ①注入集合类型属性
(1)创建好例子实体类,包括数组,List,Map,Set类型属性,生成对应set方法
```java
public class Student {
//1.数组类型属性
private String[] courses;
//2.List类型属性
private List list;
//3.Map类型属性
private Map map;
//4.Set类型属性
private Set set;
public void setCourses(String[] courses) {
this.courses = courses;
}
public void setList(List list) {
this.list = list;
}
public void setMap(Map map) {
this.map = map;
}
public void setSet(Set set) {
this.set = set;
}
}
```
(2)配置Spring文件
```xml
java课程
数据库课程
张三
小三
MySQL
Redis
```
测试代码
```java
@Test
public void testStudent(){
ApplicationContext context = new ClassPathXmlApplicationContext("bean4.xml");
Student student = context.getBean("student", Student.class);
System.out.println(student);//Student{courses=[java课程, 数据库课程], list=[张三, 小三], map={JAVA=java, PHP=php}, set=[MySQL, Redis]}
}
```
###### ②在集合属性中设置对象类型
首先,新创建一个类Family,包含属性fname并提供set方法
然后,在上述例子Student类的基础上再添加一个`List`类型属性`families`,用于并提供set方法
然后,再在上例的String配置文件中创建多个Family对象,并使用下方方式注入到student对象的families属性
```xml
```
测试结果如下:
```java
@Test
public void testStudent(){
ApplicationContext context = new ClassPathXmlApplicationContext("bean4.xml");
Student student = context.getBean("student", Student.class);
System.out.println(student);//Student{courses=[java课程, 数据库课程], list=[张三, 小三], map={JAVA=java, PHP=php}, set=[MySQL, Redis], families=[Family{fname='爸爸'}, Family{fname='妈妈'}]}
}
```
###### ③提取集合属性公共部分
防止混乱,重新创建一个类
```java
public class BookList {
private List bookList;
public void setBookList(List bookList) {
this.bookList = bookList;
}
}
```
然后,在Spring配置文件中配置注入到属性
```xml
Java课程设计
数据结构与算法
操作系统
```
测试代码
```java
@Test
public void testBookList(){
ApplicationContext context = new ClassPathXmlApplicationContext("bean5.xml");
BookList books = context.getBean("books", BookList.class);
System.out.println(books); //BookList{bookList=[Java课程设计, 数据结构与算法, 操作系统]}
}
```
#### 3.3.2 Bean管理
##### 1.工厂Bean
1、Spring 有两种类型 bean,一种**普通 bean**,另外一种**工厂 bean(FactoryBean)**
2、普通 bean:在**配置文件中定义的bean类型就是返回类型**(前方举例创建的Bean对象都是普通Bean)
3、工厂 bean:在**配置文件定义的bean类型可以和返回类型不一样**
第一步 创建类,让这个类作为工厂 bean,**实现接口 FactoryBean**
第二步 实现接口里面的方法,在**实现的方法中定义返回的 bean 类型**
```java
//工厂Bean举例
public class MyBeanFactory implements FactoryBean {//指定工厂Bean返回的泛型对象类型为Course
//实现接口中定义的抽象方法
@Override
public Course getObject() throws Exception {
Course course = new Course();
course.setCname("abc");
//在实现的getObject方法中指定工厂Bean返回的对象类型为Course
return course;
}
@Override
public Class> getObjectType() {
return null;
}
@Override
public boolean isSingleton() {
return false;
}
}
```
```xml
```
```java
//测试代码
@Test
public void test() {
ApplicationContext context = new ClassPathXmlApplicationContext("bean.xml");
Course course = context.getBean("myBeanFactory", Course.class);
System.out.println(course);//Course{cname="abc"}
}
```
##### 2.Bean的作用域
1、在 Spring 里面,创建 bean 实例**分为单实例还是多实例**
2、在 Spring 里面,**默认情况下 bean 是单实例对象**
```xml
```
```java
@Test
public void test() {
ApplicationContext context = new ClassPathXmlApplicationContext("bean.xml");
Book book1 = context.getBean("book", Book.class);
Book book2 = context.getBean("book", Book.class);
System.out.println(book1);//Bean.Book@5d11346a
System.out.println(book2);//Bean.Book@5d11346a
}
//从测试结果可以看出单实例对象两次创建的对象相同
```
3、配置Bean对象为多实例对象
(1)在spring配置文件 bean 标签里面有**属性(scope)**用于设置单实例还是多实例
(2)scope 属性值
第一个值 默认值**singleton,表示是单实例对象**
第二个值 **prototype,表示是多实例对象**
```xml
```
4、singleton 和 prototype **区别**
* 第一 singleton 单实例,prototype 多实例
* 第二 设置 scope 值是 singleton 时,**加载 spring 配置文件时候就会创建单实例对象**
设置 scope 值是 prototype 时,不是在加载 spring 配置文件时候创建 对象,在**调用getBean 方法时候创建多实例对象**
* 另外,scope还有两个值分别为request,session;request表示这个实例对象的**作用域为一次请求**,而session表示这个实例对象的**作用域为一次会话**
##### 3.Bean的生命周期
1、生命周期
(1)从对象**创建到销毁的过程**
2、bean 生命周期
(1)通过构造器**创建 bean 实例**(无参数构造)
(2)为 bean 的**属性设置值**和引用其他 bean(调用 set 方法)
(3)调用 **bean 的初始化**的方法(需要进行配置初始化的方法)
(4)**bean 可以使用**了(对象获取到了)
(5)当**容器关闭**时候,**调用 bean 的销毁的方法**(需要进行配置销毁的方法)
3、实例演示
```java
//创建一个Order实体类
public class Order {
private String oname;
public Order(){
System.out.println("执行无参构造函数创建bean实例");
}
public void setOname(String oname) {
this.oname = oname;
System.out.println("调用set方法为属性注入值");
}
public void initMethod(){
System.out.println("执行初始化方法");
}
public void destroyMethod(){
System.out.println("执行销毁方法");
}
}
```
```xml
```
```java
//测试代码
@Test
public void testOrder(){
//ApplicationContext context = new ClassPathXmlApplicationContext("orderBean.xml");
ClassPathXmlApplicationContext context = new ClassPathXmlApplicationContext("orderBean.xml");
Order order = context.getBean("order", Order.class);
System.out.println("成功创建bean实例对象");
System.out.println(order);
//手动销毁bean实例
context.close();//这个close方法是ApplicationContext的子接口ConfigurableApplicationContext新增的方法,
//然后在AbstractApplicationContext抽象类中实现,而ClassPathXmlApplicationContext类是继承了该类的子孙类,
//所以这里的context对象的类型不能是ApplicationContext
//输出结果:
//执行无参构造函数创建bean实例
//调用set方法为属性注入值
//执行初始化方法
//成功创建bean实例对象
//Bean.Order@10dba097
//执行销毁方法
}
//从输出结果即可看出Bean的生命周期:①调用构造器创建bean实例→②调用set方法给属性注入值和引用其他bean→③调用初始化方法→④成功创建bean对象→⑤销毁bean对象
```
4、bean的后置处理器,bean生命周期有七步(即在bean生命周期的基础上多了两步)
(1)通过构造器创建 bean 实例(无参数构造)
(2)为 bean 的属性设置值和引用其他 bean(调用 set 方法)
(3)**把 bean 实例传递给 bean 后置处理器的方法 postProcessBeforeInitialization**
(4)调用 bean 的初始化的方法(需要进行配置初始化的方法)
(5)**把 bean 实例传递给 bean 后置处理器的方法 postProcessAfterInitialization**
(6)bean 可以使用了(对象获取到了)
(7)当容器关闭时候,调用 bean 的销毁的方法(需要进行配置销毁的方法)
5、实例演示
```java
//创建一个后置处理器实体类,实现BeanPostProcessor接口
public class MyBeanPost implements BeanPostProcessor {
@Override
public Object postProcessBeforeInitialization(Object bean, String beanName) throws BeansException {
System.out.println("在初始化前执行");
return bean;
}
@Override
public Object postProcessAfterInitialization(Object bean, String beanName) throws BeansException {
System.out.println("在初始化后执行");
return bean;
}
}
```
```xml
```
```java
再次执行测试代码,运行结果如下:
①执行无参构造函数创建bean实例
②调用set方法为属性注入值
③在初始化前执行
④执行初始化方法
⑤在初始化后执行
⑥成功创建bean实例对象
Bean.Order@eb21112
⑦执行销毁方法
```
##### 4.xml自动装配
什么时自动装配?根据**指定装配规则**(属性名称或者属性类型),Spring **自动将匹配的属性值进行注入**
如何实现自动装配?
* bean 标签属性 autowire,配置自动装配
* autowire 属性常用两个值:
* byName 根据属性名称注入
* byType 根据属性类型注入
###### ①根据属性名自动注入
```java
//创建两个有关联的实体类
public class Department {
@Override
public String toString() {
return "Department{}";
}
}
public class Employee {
private Department department;
public void setDepartment(Department department) {
this.department = department;
}
@Override
public String toString() {
return "Employee{" +
"department=" + department +
'}';
}
}
```
```xml
```
```java
//测试代码
@Test
public void testAutowire(){
ApplicationContext context = new ClassPathXmlApplicationContext("autowire.xml");
Autowire.Employee employee = context.getBean("employee", Autowire.Employee.class);
System.out.println(employee); //Employee{department=Department{}}
//测试结果说明实现自动装配成功
}
```
###### ②根据属性类型自动装配
```xml
```
需要注意的是,当使用`autowire="byType"`时,若当前配置文件中有多个相同类型的bean对象则会报错,此时只能使用byName实现自动装载
##### 5.引入外部属性文件
以配置数据库信息为例:
(1)配置德鲁伊连接池
(2)引入德鲁伊连接池依赖 jar 包(druid.jar)(此处本人使用maven直接导入)
```xml
```
**引入外部属性文件配置数据库连接池**
(1)创建外部属性文件,properties 格式文件,写好数据库信息
{% gallery %}![image-20220302233927691](https://rainvex-1305747533.cos.ap-chengdu.myqcloud.com/images/202203022339872.png){% endgallery %}
(2)把外部 properties 属性文件引入到 spring 配置文件中
①引入 context 名称空间
```xml
<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">
②在 spring 配置文件使用标签引入外部属性文件
<!--引入外部属性文件-->
<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>
3.3.3 基于注解方式
1、什么是注解
(1)注解是代码特殊标记,格式:@注解名称(属性名称=属性值, 属性名称=属性值..)
(2)使用注解,注解作用在类上面,方法上面,属性上面
(3)使用注解目的:简化 xml 配置
2、Spring 针对 Bean 管理中创建对象提供的注解
(1)@Component:Spring提供的一种普通注解,使用它也可以直接创建对象
(2)@Service:主要用于service(业务)层
(3)@Controller:主要用于web(表现)层
(4)@Repository:主要用于dao(持久)层
上面四个注解功能是一样的,都可以用来创建 bean 实例,没有特意指明哪个注解必须要用在什么层,只是方便开发时区分
1.基于注解创建对象
①引入spring-aop依赖(或使用maven自动导入)
②在Spring配置文件配置开启组件扫描
<?xml version="1.0" encoding="UTF-8"?>
<!--1、引入context名称空间-->
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:context="http://www.springframework.org/schema/context"
xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context.xsd">
<!--2、开启组件扫描
如果需要扫描多个包:
1 多个包之间使用逗号隔开
2 将被扫描包的上层目录赋值给base-package属性
-->
<!--<context:component-scan base-package="com.bean,com.service,com.dao"/>-->
<context:component-scan base-package="com"/>
</beans>
③创建类,添加注解便于创建对象
//在注解里面 value 属性值可以省略不写
//默认值是类名称,首字母小写
//UserService -- userService,等同于xml配置方式中bean标签的id属性值
@Service(value = "userService") //等同于<bean id="userService" class=".."/>
public class UserService {
public void add(){
System.out.println("userService add......");
}
}
测试
@Test
public void testService(){
ApplicationContext context = new ClassPathXmlApplicationContext("beanCreate.xml");
UserService userService = context.getBean("userService", UserService.class);//其中的userService就是注解中value属性的值
System.out.println(userService);
userService.add();
//com.service.UserService@790a251b
//userService add......
//测试通过,输出了userService对象引用并且调用了方法说明通过注解创建对象成功
}
2.组件扫描配置细节
<!--默认情况-->
<context:component-scan base-package="com"/> <!--默认扫描包中所有类-->
<!--示例 1
use-default-filters="false" 表示现在不使用默认 filter,自己配置 filter
context:include-filter,设置扫描哪些内容
-->
<context:component-scan base-package="com" use-default-filters="false">
<context:include-filter type="annotation" expression="org.springframework.stereotype.Service"/> <!--表示只扫描带有org.springframework.stereotype.Service注解的类-->
</context:component-scan>
<!--示例 2
这个配置仍是扫描包所有内容
但是不扫描context:exclude-filter标签设置的内容
-->
<context:component-scan base-package="com">
<context:exclude-filter type="annotation" expression="org.springframework.stereotype.Controller"/> <!--表示不扫描带有org.springframework.stereotype.Controller注解的类-->
</context:component-scan>
3.基于注解实现属性注入
①@Autowire注解
@Autowire
:根据属性类型进行自动装配
第一步 创建 service 和 dao 对象:在 service 和 dao 类添加创建对象注解(同时要在配置文件开启组件扫描确保这些类能被扫描到)
public interface UserDao {
public void add();
}
@Repository(value = "userDaoImpl1") //默认value = "userDaoImpl",这里设置value属性的值是为了解释使用@Qulifiler等其他注解的情况
public class UserDaoImpl implements UserDao {
@Override
public void add() {
System.out.println("userDaoImpl add......");
}
}
第二步 在 service 注入 dao 对象:在 service 类添加 dao 类型属性,在属性上面使用注解
@Service
public class UserService {
//定义 dao 类型属性
//不需要添加 set 方法
//添加注入属性注解
@Autowired //根据类型注入
private UserDao userDao;
public void add(){
System.out.println("userService add......");
userDao.add();
}
}
测试代码
@Test
public void testService(){
ApplicationContext context = new ClassPathXmlApplicationContext("beanCreate.xml");
UserService userService = context.getBean("userService", UserService.class);//其中的userService就是注解中value属性的值
System.out.println(userService);
userService.add();
//com.service.UserService@702b06fb
//userService add......
//userDaoImpl add......
}
②@Qualifier注解
@Qualifier
:根据名称进行注入,这个注解的使用需要和@Autowired
配合使用
//在下方类中使用@Autowired和@Qualifiler
@Service
public class UserService {
@Autowired //根据类型注入
@Qualifiler(value = "userDaoImpl1") //当value = "userDaoImpl1"时就可以找到UserDaoImpl类并将其作为对象属性进行注入,否则就会找不到需要注入的对象类导致无法创建userDao对象
private UserDao userDao;
public void add(){
System.out.println("userService add......");
userDao.add();
}
}
③@Resource注解
@Resource
:可以根据类型注入,可以根据名称注入
@Service
public class UserService {
//@Resource 根据类型进行注入
@Resource(value = "userDaoImpl1") //根据名称进行注入
private UserDao userDao;
public void add(){
System.out.println("userService add......");
userDao.add();
}
}
④@Value注解
@Value
:注入普通类型属性
@Service
public class UserService {
@Value(value = "abc") //使用@Value注解给普通类型属性注入值
private String name;
public String getName(){
return name;
}
}
//给这个普通属性提供get方法后,就可在测试方法中通过userService对象得到这个name属性的值
4.完全注解开发
创建配置类,替代 xml 配置文件(这种开发模式主要是在SpringBoot中使用)
@Configuration //标记该类为配置类,替代xml配置文件
@ComponentScan(basePackages = {"com"}) //表示开启组件扫描,需要扫描的类通过basePackages属性指定其上级目录
//另外,ComponentScan注解还有一个basePackageClasses属性,根据属性名可以知道是通过指定某些类的类名来标记其为被扫描对象;而basePackages是根据指定某些软件包的包名来标记其中的类能被扫描
public class SpringConfig {
}
测试代码
@Test
public void testService(){
ApplicationContext context = new AnnotationConfigApplicationContext(SpringConfig.class);//使用AnnotationConfigApplicationContext类加载配置类
UserService userService = context.getBean("userService", UserService.class);
System.out.println(userService);
userService.add();
System.out.println(userService.getName());
//com.service.UserService@475646d4
//userService add......
//userDaoImpl add......
//abc
}
四、AOP
4.1 基本概念
概念:AOP(Aspect Oriented Programming),意为:面向切面(方面)编程,通过预编译方式和运行期间动态代理实现程序功能的统一维护的一种技术。AOP是OOP的延续,是软件开发中的一个热点,也是Spring框架中的一个重要内容,是函数式编程的一种衍生范型。
优点:利用AOP可以对业务逻辑的各个部分进行隔离,从而使得业务逻辑各部分之间的耦合度降低,提高程序的可重用性,同时提高了开发的效率。
主要意图:将日志记录,性能统计,安全控制,事务处理,异常处理等代码从业务逻辑代码中划分出来,通过对这些行为的分离,我们希望可以将它们独立到非指导业务逻辑的方法中,进而改变这些行为的时候不影响业务逻辑的代码(即不通过修改源代码方式,在主干功能里面添加新功能)。
例子:
4.2 底层原理
AOP 底层使用动态代理,有两种情况动态代理:
第一种 有接口情况,使用 JDK 动态代理
创建接口实现类的代理对象,增强类的方法第二种 没有接口情况,使用 CGLIB 动态代理
创建子类的代理对象,增强类的方法
4.3 JDK动态代理
使用 JDK 动态代理,使用 Proxy 类里面的方法创建代理对象
Proxy类:
创建对象使用的方法:
方法有三个参数:
第一参数,类加载器
第二参数,增强方法的所在类,这个类实现的接口(支持多个接口),也可以理解成代理类需要实现的接口
第三参数,实现 InvocationHandler 这个接口,创建代理对象,写增强的部分
实例:
①创建接口,定义方法
public interface UserService {
public void add();
public int update(int a,int b);
}
②创建接口实现类,实现方法
public class UserServiceImpl implements UserService{
@Override
public void add() {
System.out.println("UserServiceImpl的add方法执行了...");
}
@Override
public int update(int a, int b) {
System.out.println("UserServiceImpl的update方法执行了...");
return a + b;
}
}
③创建代理对象类,实现InvocationHandler接口
//创建的一个UserService接口代理对象类
public class UserServiceProxy implements InvocationHandler {
//通过有参构造函数把需要被代理的对象传给代理类
//这儿使用的Object可以改为具体的被代理类的对象的类型,使用Object类型只是为了能达到可以代理多个对象的目的
private Object obj;
public UserServiceProxy(Object obj){
this.obj = obj;
}
//将原先功能需要增强的业务逻辑写在这里面
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
//执行被增强方法之前
System.out.println("方法执行之前...");
System.out.println("执行的方法是:" + method.getName());
System.out.println("传递的参数是:" + Arrays.toString(args));
//执行被增强的方法
Object invoke = method.invoke(obj, args); //底层的方法调用,这个方法可以在java基础的反射那一章节查看具体的,或在java.lang.reflect包中的Method类中查看该方法的具体讲解
//执行被增强方法之后
System.out.println("方法执行之后...");
System.out.println("该方法返回的值:" + invoke);
System.out.println("执行该方法的对象是:" + obj);
//System.out.println(proxy); 为什么这儿我想输出proxy会让程序一直执行导致堆栈溢出异常,希望如果有大佬仔细看见了能帮我解答一下
return invoke;
}
//InvocationHandler接口的invoke方法介绍:(个人根据文档的简易理解)
//proxy - 调用该方法的代理实例
//method - 使用代理对象调用的方法
//args - 代理对象调用的方法所需要的参数阵列
//返回结果 - 被调用方法的返回值
//返回结果原文介绍:从代理实例上的方法调用返回的值。如果接口方法的声明返回类型是原始类型,则此方法返回的值必须是对应的基本包装类的实例;
// 否则,它必须是可声明返回类型的类型。 如果此方法返回的值是null和接口方法的返回类型是基本类型,那么NullPointerException将由代理实例的方法调用抛出。
// 如上所述,如果此方法返回的值,否则不会与接口方法的声明的返回类型兼容,一个ClassCastException将代理实例的方法调用将抛出。
}
④使用Proxy类创建接口代理对象
public class JDKProxy {
public static void main(String[] args) {
//创建接口实现类的代理对象
Class[] interfaces = {UserService.class}; //创建出的代理对象需要实现的接口类型
//1、通过创建InvocationHandler匿名实现类对象来创建UserService接口实现类的代理对象的方式
//Proxy.newProxyInstance(JDKProxy.class.getClassLoader(), interfaces, new InvocationHandler() {
// @Override
// public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
// return null;
// }
//});
//2、通过创建InvocationHandler实现类的对象来创建UserService接口实现类的代理对象的方式
UserServiceImpl userServiceImplProxied = new UserServiceImpl(); //创建接口实现类对象(被代理类对象)
System.out.println("被代理的接口实现类对象:" + userServiceImplProxied);
//创建出UserService接口实现类UserServiceImpl的代理对象
UserService userServiceImplProxy = (UserService) Proxy.newProxyInstance(JDKProxy.class.getClassLoader(), interfaces, new UserServiceProxy(userServiceImplProxied));
userServiceImplProxy.add(); //创建出的代理对象调用被代理对象的方法
//执行结果:
//被代理的接口实现类对象:com.spring5.UserServiceImpl@61bbe9ba
//方法执行之前...
//执行的方法是:add
//传递的参数是:null
//UserServiceImpl的add方法执行了...
//方法执行之后...
//该方法返回的值:null
//执行该方法的对象是:com.spring5.UserServiceImpl@61bbe9ba
}
}
4.4 操作术语
AOP中有四个常用的操作术语,分别是:
- 连接点
- 一个类中可以被增强的方法就叫连接点
- 切入点
- 实际被增强的方法叫切入点
- 通知(增强)
- 实际增强的逻辑代码部分称为通知
- 通知的几种类型:
- 前置通知
- 后置通知
- 环绕通知
- 异常通知
- 最终通知
- 切面
- 是一个动作,即把通知应用到切入点的过程叫切面
4.5 AOP操作
4.5.1 准备工作
1、Spring 框架一般都是基于 AspectJ 实现 AOP 操作
- AspectJ 不是 Spring 组成部分,独立 AOP 框架,一般把 AspectJ 和 Spirng 框架一起使用,进行 AOP 操作
2、基于 AspectJ 实现 AOP 操作
- 基于 xml 配置文件实现
- 基于注解方式实现(通常使用)
3、在项目工程里面引入 AOP 相关依赖
<!--maven导入-->
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-aop</artifactId>
<version>5.3.15</version>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-aspects</artifactId>
<version>5.3.16</version>
</dependency>
<dependency>
<groupId>org.aspectj</groupId>
<artifactId>aspectjrt</artifactId>
<version>1.9.8</version>
</dependency>
<dependency>
<groupId>org.aspectj</groupId>
<artifactId>aspectjweaver</artifactId>
<version>1.9.8</version>
</dependency>
<dependency>
<groupId>aopalliance</groupId>
<artifactId>aopalliance</artifactId>
<version>1.0</version>
</dependency>
<dependency>
<groupId>net.sourceforge.cglib</groupId>
<artifactId>com.springsource.net.sf.cglib</artifactId>
<version>2.2.0</version>
</dependency>
4、切入点表达式
- 切入点表达式作用:知道对哪个类里面的哪个方法进行增强
- 语法结构:
execution([权限修饰符] [返回类型] [类全路径] [方法名称]([参数列表]))
- 举例 1:对 com.spring5.dao.BookDao 类里面的 add 方法进行增强
execution(* com.spring5.dao.BookDao.add(..))
- 举例 2:对 com.spring5.dao.BookDao 类里面的所有的方法进行增强
execution(* com.spring5.dao.BookDao.* (..))
- 举例 3:对 com.spring5.dao 包里面所有类中的所有方法进行增强
execution(* com.atguigu.dao.*.* (..))
4.5.2 基于aspectj的注解方式
1.注解方式实现AOP
1、创建类,在类里面定义方法
public class User {
public void add(){
System.out.println("user add...");
}
}
2、创建增强类(编写增强逻辑),在增强类里面,创建方法,让不同方法代表不同通知类型
//代理类
public class UserProxy {
public void before(){
System.out.println("前置通知...");
}
}
3、进行通知的配置
①在spring配置文件中,开启注解扫描(也可用注解方式开启注解扫描)
//注解方式
@Configuration
@ComponentScan(basePackages = {"com.spring5.aopannotation"})
public class SpringConfig {
}
<!--配置文件方式-->
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:context="http://www.springframework.org/schema/context"
xmlns: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.spring5.aopannotation"></context:component-scan>
</beans>
②使用注解创建User和 UserProxy对象
@Component
public class User {
public void add(){
System.out.println("user add...");
}
}
@Component
public class UserProxy {
public void before(){
System.out.println("前置通知...");
}
}
③在增强(代理)类上面添加注解@Aspect
@Component
@Aspect
public class UserProxy {
public void before(){
System.out.println("前置通知...");
}
}
④在spring配置文件中开启生成代理对象
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:context="http://www.springframework.org/schema/context"
xmlns: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.spring5.aopannotation"/>
<!-- 开启 Aspect 生成代理对象-->
<aop:aspectj-autoproxy/>
</beans>
4、配置不同类型的通知,在增强类中作为通知方法的上面添加通知类型注解,使用切入点表达式配置
@Component
@Aspect
public class UserProxy {
//前置通知
//@Before 注解表示作为前置通知
@Before("execution(* com.spring5.aopannotation.User.add())")
public void before(){
System.out.println("before...");
}
//最终通知——不管执行过程中有没有异常都会执行
@After("execution(* com.spring5.aopannotation.User.add())")
public void after(){
System.out.println("after...");
}
//后置通知(返回通知)——执行过程中出现异常就不执行
@AfterReturning("execution(* com.spring5.aopannotation.User.add())")
public void afterReturning(){
System.out.println("afterReturning...");
}
//异常通知
@AfterThrowing("execution(* com.spring5.aopannotation.User.add())")
public void afterThrowing(){
System.out.println("afterThrowing...");
}
//环绕通知
@Around("execution(* com.spring5.aopannotation.User.add())")
public void around(ProceedingJoinPoint proceedingJoinPoint) throws Throwable {
System.out.println("环绕之前...");
//执行被增强的方法
proceedingJoinPoint.proceed();
System.out.println("环绕之后...");
}
}
5、测试
public class TestSpring5 {
@Test
public void test(){
ApplicationContext context = new ClassPathXmlApplicationContext("bean1.xml");
User user = context.getBean("user", User.class);
user.add();
//环绕之前...
//before...
//user add...
//afterReturning...
//after...
//环绕之后...
//后面三个可能由于Spring版本原因输出结果不一样,弹幕中好像有人说从5.3.7开始底层就改变了
}
}
//另外,在Around通知的方法中如果使用try-catch处理异常的话当代码执行出现异常时“环绕之后”仍会执行输出,但如果使用throws的方式处理异常的话当代码出现异常时“环绕之后”则不会执行输出
2.提取公共切入点
@Pointcut("execution(* com.spring5.aopannotation.User.add())")
public void pointDemo(){
}
//前置通知
//@Before 注解表示作为前置通知
@Before("pointDemo()")
public void before(){
System.out.println("before...");
}
//最终通知
@After("pointDemo()")
public void after(){
System.out.println("after...");
}
3.多个增强类的优先级
当有多个增强类对同一个方法进行增强时,可以通过在增强类上添加@Order(数字类型值)
注解设置增强类的优先级,数字类型值越小优先级越高
//PersonProxy增强类
@Component
@Aspect
@Order(1)
public class PersonProxy {
@Pointcut("execution(* com.spring5.aopannotation.User.add())")
public void pointDemo(){
}
//前置通知
@Before("pointDemo()")
public void before(){
System.out.println("personProxy before...");
}
//最终通知
@After("pointDemo()")
public void after(){
System.out.println("personProxy after...");
}
}
//UserProxy增强类
@Component
@Aspect
@Order(3)
public class UserProxy {
@Pointcut("execution(* com.spring5.aopannotation.User.add())")
public void pointDemo(){
}
//前置通知
//@Before 注解表示作为前置通知
@Before("pointDemo()")
public void before(){
System.out.println("userProxy before...");
}
//最终通知
@After("pointDemo()")
public void after(){
System.out.println("userProxy after...");
}
}
//测试
@Test
public void test(){
ApplicationContext context = new ClassPathXmlApplicationContext("bean1.xml");
User user = context.getBean("user", User.class);
user.add();
//personProxy before...
//userProxy before...
//user add...
//userProxy after...
//personProxy after...
}
4.完全注解开发
//创建配置类,不需要创建 xml 配置文件
@Configuration
@ComponentScan(basePackages = {"com.spring5.aopannotation"})
@EnableAspectJAutoProxy(proxyTargetClass = true) //开启Aspect生成代理对象
public class ConfigAop {
}
4.5.3 基于aspectj的xml配置文件方式
1、创建两个类,增强类和被增强类,创建方法
public class Book {
public void buy(){
System.out.println("book buy...");
}
}
public class BookProxy {
public void before() {
System.out.println("bookProxy before...");
}
}
2、在 spring 配置文件中创建两个类对象
<!--创建对象-->
<bean id="book" class="com.spring5.aopxml.Book"/>
<bean id="bookProxy" class="com.spring5.aopxml.BookProxy"/>
3、在 spring 配置文件中配置切入点
<!--需要引入aop名称空间-->
<!--配置aop增强-->
<aop:config>
<!--配置切入点-->
<aop:pointcut id="proxied" expression="execution(* com.spring5.aopxml.Book.buy())"/>
<!--配置切面(切面)-->
<aop:aspect ref="bookProxy">
<!--增强作用在具体的方法上(通知)-->
<aop:before method="before" pointcut-ref="proxied"/>
</aop:aspect>
</aop:config>
测试
@Test
public void test1(){
ApplicationContext context = new ClassPathXmlApplicationContext("bean2.xml");
Book book = context.getBean("book", Book.class);
book.buy();
//bookProxy before...
//book buy...
}
五、JdbcTemplate
5.1 概述和准备工作
5.1.1 概述
什么是 JdbcTemplate?Spring 框架对 JDBC 进行封装,使用 JdbcTemplate 方便实现对数据库操作
5.1.2 准备工作
1.引入jar包
<!--maven依赖-->
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>druid</artifactId>
<version>1.2.8</version>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>8.0.27</version>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-jdbc</artifactId>
<version>5.3.16</version>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-orm</artifactId>
<version>5.3.16</version>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-tx</artifactId>
<version>5.3.16</version>
</dependency>
2.在Spring配置文件中配置数据库连接池
<!--配置创建 DataSource 数据池对象-->
<bean id="dataSource" class="com.alibaba.druid.pool.DruidDataSource" destroy-method="close">
<property name="url" value="jdbc:mysql://localhost:3306/user_db"/>
<property name="username" value="root" />
<property name="password" value="root" />
<property name="driverClassName" value="com.mysql.cj.jdbc.Driver" />
</bean>
3.配置JdbcTemplate对象,注入DataSource
<!-- 配置创建 JdbcTemplate 对象 -->
<bean id="jdbcTemplate" class="org.springframework.jdbc.core.JdbcTemplate">
<!--注入 dataSource-->
<!--通过查看源码发现,JdbcTemplate继承自JdbcAccessor类,创建JdbcTemplate对象时会将DataSource对象通过有参构造传入,然后通过调用set方法来将传入的DataSource对象赋值给JdbcAccessor类中的DataSource类型属性-->
<property name="dataSource" ref="dataSource"/>
</bean>
4.创建 service 类,dao 类,在 dao 注入 jdbcTemplate 对象
①配置开启组件扫描
<!-- 配置文件方式配置开启组件扫描 -->
<context:component-scan base-package="com.spring5"></context:component-scan>
②创建Service和Dao
使用注解方式创建Service对象和Dao对象
@Service
public class UserService {
//注入dao
@Autowired
private UserDao userDao;
}
public interface UserDao {
public void add(User user);
}
@Repository
public class UserDaoImpl implements UserDao {
//注入JdbcTemplate
@Autowired
private JdbcTemplate jdbcTemplate;
}
5.2 JdbcTemplate操作数据库
5.2.1 添加操作
1.创建实体类
//根据数据库表结构创建对应实体类
public class User {
private Integer id;
private String username;
private String password;
private String status;
public Integer getId() {
return id;
}
public void setId(Integer id) {
this.id = id;
}
public String getUsername() {
return username;
}
public void setUsername(String username) {
this.username = username;
}
public String getPassword() {
return password;
}
public void setPassword(String password) {
this.password = password;
}
public String getStatus() {
return status;
}
public void setStatus(String status) {
this.status = status;
}
@Override
public String toString() {
return "User{" +
"id=" + id +
", username='" + username + '\'' +
", password='" + password + '\'' +
", status='" + status + '\'' +
'}';
}
}
2.编写service和dao层添加用户的操作
@Service
public class UserService {
//注入dao
@Autowired
private UserDao userDao;
public void addUser(User user){
userDao.add(user);
}
}
@Repository
public class UserDaoImpl implements UserDao {
//注入jdbcTemplate
@Autowired
private JdbcTemplate jdbcTemplate;
@Override
public void add(User user) {
//创建sql语句
String sql = "insert into t_user values (?,?,?,?)";
//调用方法实现
Object[] args = {user.getId(),user.getUsername(),user.getPassword(),user.getStatus()};
int update = jdbcTemplate.update(sql, args);
System.out.println(update);
}
}
3.测试
@Test
public void testJdbcTemplate() {
ApplicationContext context = new ClassPathXmlApplicationContext("bean.xml");
UserService userService = context.getBean("userService", UserService.class);
User user = new User();
user.setId(1);
user.setUsername("admin");
user.setPassword("12345");
user.setStatus("1");
userService.addUser(user);
}
//执行测试后可以发现数据库中被添加了一条新数据
5.2.2 修改删除操作
修改和删除操作基本均和添加操作一样
//在UserService中添加方法
public void updateUser(User user){
userDao.update(user);
}
public void deleteUser(Integer id){
userDao.delete(id);
}
//在UserDao中添加方法
@Override
public void update(User user) {
//创建sql语句
String sql = "update t_user set username=?,status=? where id=?";
//调用方法实现
Object[] args = {user.getUsername(), user.getStatus(), user.getId()};
int update = jdbcTemplate.update(sql, args);
System.out.println(update);
}
@Override
public void delete(Integer id) {
//创建sql语句
String sql = "delete from t_user where id=?";
//调用方法实现
int update = jdbcTemplate.update(sql, id);
System.out.println(update);
}
测试代码
@Test
public void testJdbcTemplate1() {
ApplicationContext context = new ClassPathXmlApplicationContext("bean.xml");
UserService userService = context.getBean("userService", UserService.class);
User user = new User();
user.setId(1);
user.setUsername("admin1");
user.setPassword("12345");
user.setStatus("2");
userService.updateUser(user);
}
@Test
public void testJdbcTemplate2() {
ApplicationContext context = new ClassPathXmlApplicationContext("bean.xml");
UserService userService = context.getBean("userService", UserService.class);
userService.deleteUser(1);
}
5.2.3 查询操作
1.查询返回某个值
使用 JdbcTemplate 实现查询返回某个值代码queryForObject(String sql,Class<T> requiredType)
- 第一个参数:sql 语句
- 第二个参数:返回值类型 Class
//Service层添加方法
public int selectCount(){
return userDao.selectCount();
}
//Dao层添加方法
@Override
public Integer selectCount() {
String sql = "select count(*) from t_user";
return jdbcTemplate.queryForObject(sql, Integer.class);
}
@Test
public void testJdbcTemplate3() {
ApplicationContext context = new ClassPathXmlApplicationContext("bean.xml");
UserService userService = context.getBean("userService", UserService.class);
System.out.println(userService.selectCount());//3
}
2.查询返回对象
使用JdbcTemplate方法实现查询返回集合queryForObject(String sql,RowMapper<T> rowMapper,Object... args)
- 第一个参数:sql 语句
- 第二个参数:RowMapper 是接口,针对返回不同类型数据,使用这个接口的实现类完成数据封装
- 第三个参数:sql 语句值
//Service层添加方法
public User selectUser(Integer id){
return userDao.selectUser(id);
}
//Dao层添加方法
@Override
public User selectUser(Integer id) {
String sql = "select * from t_user where id = ?";
return jdbcTemplate.queryForObject(sql, new BeanPropertyRowMapper<User>(User.class), id);
}
@Test
public void testJdbcTemplate4() {
ApplicationContext context = new ClassPathXmlApplicationContext("bean.xml");
UserService userService = context.getBean("userService", UserService.class);
System.out.println(userService.selectUser(2));//User{id=2, username='admin1', password='12345', status='2'}
}
3.查询返回列表
调用 JdbcTemplate 方法实现查询返回集合query(String sql,RowMapper rowMapper,Object... args)
- 第一个参数:sql 语句
- 第二个参数:RowMapper 是接口,针对返回不同类型数据,使用这个接口的实现类完成数据封装
- 第三个参数:sql 语句值(可选)
//Service层添加方法
public List<User> selectAllUser(){
return userDao.selectAllUser();
}
//Dao层添加方法
@Override
public List<User> selectAllUser() {
String sql = "select * from t_user";
return jdbcTemplate.query(sql, new BeanPropertyRowMapper<User>(User.class));
}
@Test
public void testJdbcTemplate5() {
ApplicationContext context = new ClassPathXmlApplicationContext("bean.xml");
UserService userService = context.getBean("userService", UserService.class);
System.out.println(userService.selectAllUser());
//[User{id=1, username='admin', password='12345', status='1'},
//User{id=2, username='admin1', password='12345', status='2'},
//User{id=3, username='admin2', password='12345', status='3'}]
}
5.3 JdbcTemplate批量操作
其实添加、修改、删除的操作都一样,都是调用batchUpdate(String sql,List<Object[]> batchArgs)
方法,只是sql语句不一样…该方法返回的是一个int类型数组,保存的是添加每条数据的成功与否,成功则元素均为1
5.3.1 批量添加操作
使用JdbcTemplate中的batchUpdate(String sql,List<Object[]> batchArgs)
实现批量添加操作
- 第一个参数:sql 语句
- 第二个参数:List 集合,添加多条记录数据
//Service层添加方法
public void batchAdd(List<Object[]> batchArgs){
userDao.batchInsert(batchArgs);
}
//Dao层添加方法
@Override
public void batchInsert(List<Object[]> batchArgs) {
String sql = "insert into t_user (username,password,status) values (?,?,?)";
int[] ints = jdbcTemplate.batchUpdate(sql, batchArgs);
}
//测试
@Test
public void testJdbcTemplate6() {
ApplicationContext context = new ClassPathXmlApplicationContext("bean.xml");
UserService userService = context.getBean("userService", UserService.class);
List<Object[]> list = new ArrayList<>();
Object[] o1 = {"admin3","12345","4"};
Object[] o2 = {"admin4","12345","5"};
Object[] o3 = {"admin5","12345","6"};
list.add(o1);
list.add(o2);
list.add(o3);
userService.batchAdd(list);
}
5.3.2 批量修改操作
使用JdbcTemplate中的batchUpdate(String sql,List<Object[]> batchArgs)
实现批量修改操作
- 第一个参数:sql 语句
- 第二个参数:List 集合,修改多条记录数据
//Service层添加方法
public void batchUpdate(List<Object[]> batchArgs){
userDao.batchUpdate(batchArgs);
}
//Dao层添加方法
@Override
public void batchUpdate(List<Object[]> batchArgs) {
String sql = "update t_user set username=?,password=?,status=? where id=?";
int[] update = jdbcTemplate.batchUpdate(sql, batchArgs);
}
//测试
@Test
public void testJdbcTemplate7() {
ApplicationContext context = new ClassPathXmlApplicationContext("bean.xml");
UserService userService = context.getBean("userService", UserService.class);
List<Object[]> list = new ArrayList<>();
Object[] o1 = {"admin3+","123456","4+","4"};
Object[] o2 = {"admin4+","123456","5+","5"};
Object[] o3 = {"admin5+","123456","6+","6"};
list.add(o1);
list.add(o2);
list.add(o3);
userService.batchUpdate(list);
}
5.3.3 批量删除操作
使用JdbcTemplate中的batchUpdate(String sql,List<Object[]> batchArgs)
实现批量删除操作
- 第一个参数:sql 语句
- 第二个参数:List 集合,删除多条记录数据
//Service层添加方法
public void batchDelete(List<Object[]> batchArgs){
userDao.batchDelete(batchArgs);
}
//Dao层添加方法
@Override
public void batchDelete(List<Object[]> batchArgs) {
String sql = "delete from t_user where id=?";
int[] delete = jdbcTemplate.batchUpdate(sql, batchArgs);
}
//测试
@Test
public void testJdbcTemplate8() {
ApplicationContext context = new ClassPathXmlApplicationContext("bean.xml");
UserService userService = context.getBean("userService", UserService.class);
List<Object[]> list = new ArrayList<>();
Object[] o1 = {4};
Object[] o2 = {5};
list.add(o1);
list.add(o2);
userService.batchDelete(list);
}
六、事务
6.1 事务概念
1、什么事务
- 事务是数据库操作最基本单元,逻辑上一组操作,要么都成功,如果有一个失败所有操作都失败
- 典型场景:银行转账
- lucy 转账 100 元 给 mary
- lucy 少 100,mary 多 100
2、事务四个特性(ACID)
- 原子性:过程中不可分割,要么都成功,要么就失败
- 一致性:操作之前和操作之后总量是不变的
- 隔离性:多事务操作时不会相互影响
- 持久性:事务提交后数据库表的数据跟着变化
6.2 搭建事务操作环境
1.创建数据库,添加记录
2.配置组件扫描、DataSource和JdbcTemplate对象
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:context="http://www.springframework.org/schema/context"
xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context.xsd">
<!-- 配置文件方式配置开启组件扫描 -->
<context:component-scan base-package="com.spring5"/>
<!--配置创建 DataSource 数据池对象-->
<bean id="dataSource" class="com.alibaba.druid.pool.DruidDataSource" destroy-method="close">
<property name="url" value="jdbc:mysql://localhost:3306/user_db"/>
<property name="username" value="root" />
<property name="password" value="root" />
<property name="driverClassName" value="com.mysql.cj.jdbc.Driver" />
</bean>
<!-- 配置创建 JdbcTemplate 对象 -->
<bean id="jdbcTemplate" class="org.springframework.jdbc.core.JdbcTemplate">
<!--注入 dataSource-->
<property name="dataSource" ref="dataSource"/>
</bean>
</beans>
3.创建service,dao层,完成对象创建、注入关系及方法创建
//service层
@Service
public class UserService {
@Autowired
private UserDao userDao;
//转账方法
public void transfer(){
userDao.reduce();//mary账户转账100
userDao.add();//lucy账户转入100
}
}
//dao层
public interface UserDao {
public void reduce();
public void add();
}
@Repository
public class UserDaoImpl implements UserDao {
@Autowired
private JdbcTemplate jdbcTemplate;
//实现mary转账100给lucy
@Override
public void reduce() {
String sql = "update t_account set money=money-? where username=?";
jdbcTemplate.update(sql,100,"mary");
}
@Override
public void add() {
String sql = "update t_account set money=money+? where username=?";
jdbcTemplate.update(sql,100,"lucy");
}
}
4.测试
@Test
public void test(){
ApplicationContext context = new ClassPathXmlApplicationContext("bean.xml");
UserService userService = context.getBean("userService", UserService.class);
userService.transfer();
}
//测试执行正常,mary的账户money剩下900,lucy剩1100
//但是我的没有正常执行,不知道为什么产生死锁了,但是事务处理的大致思路就是这样
5.模拟异常
上面代码,如果正常执行没有问题的,但是如果代码执行过程中出现异常则会有问题
//转账方法
public void transfer(){
userDao.reduce();//mary账户转账100
//模拟异常
int i = 10/0;
userDao.add();//lucy账户转入100
}
//如上,如果在执行过程中出现异常,此时查看数据库会发现mary账户只剩900,但是lucy的账户仍是1000,并没有得到mary转的100
出现异常如何处理?使用事务进行处理
//转账方法
public void transfer(){
try {
//第一步,开启事务
//第二步,进行业务操作
userDao.reduce();//mary账户转账100
//模拟异常
int i = 10/0;
userDao.add();//lucy账户转入100
//第三步,若没有异常,提交事务
} catch (Exception e) {
//第四步,事务回滚
e.printStackTrace();
}
}
6.3 事务管理
1、事务添加到 JavaEE 三层结构里面 Service 层(业务逻辑层)
2、在 Spring 进行事务管理操作有两种方式:编程式事务管理和声明式事务管理(一般使用声明式)
- 编程式事务管理就是编写代码进行事务管理,这种方式特别麻烦,如果需要对多个业务进行事务管理就会造成代码冗余,重复,所以一般不使用这种方式
3、声明式事务管理
- 基于注解方式(使用)
- 基于 xml 配置文件方式
4、在 Spring 进行声明式事务管理,底层使用 AOP 原理
5、Spring 事务管理提供的 API
提供一个接口,代表事务管理器,这个接口针对不同的框架提供不同的实现类
6.3.1 声明式事务管理
1.注解方式实现声明式事务管理
①开启注解方式事务管理
以下操作均在上面搭建好的事务管理操作环境的xml配置文件基础上添加
1、在Spring配置文件中配置事务管理器
<!--创建配置事务管理器-->
<bean id="transactionManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
<!--注入数据源-->
<!--在源码中同样可发现此实现类也是通过set方法来配置DataSource对象-->
<property name="dataSource" ref="dataSource"/>
</bean>
2、在Spring配置文件中开启事务注解
<!--首先要引入tx名称空间-->
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:context="http://www.springframework.org/schema/context"
xmlns:tx="http://www.springframework.org/schema/tx"
xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context.xsd
http://www.springframework.org/schema/tx http://www.springframework.org/schema/tx/spring-tx.xsd">
<!--开启事务注解-->
<tx:annotation-driven transaction-manager="transactionManager"/>
3、在 service 类上面(或者 service 类里面方法上面)添加事务注解
@Transactional
这个注解添加到类上面,也可以添加方法上面- 如果把这个注解添加类上面,这个类里面所有的方法都添加事务
- 如果把这个注解添加方法上面,为这个方法添加事务
@Service
@Transactional
public class UserService {
}
②事务管理参数配置
通过查看@Transactional
注解可知道有以下参数:
1、propagation:事务传播行为
多事务方法直接进行调用,这个过程中事务是如何进行管理的,比如,
- 一个有事务的方法去调用一个没有事务的方法
- 一个没有事务的方法去调用一有事务的方法
- 一个有事务的方法去调用另一个有事务的方法
- 还有多个事务方法之间的调用等,在这些过程中事务的管理是怎样的,这就是事务传播行为
事务传播行为的几种方式:
@Service
@Transactional(propagation = Propagation.REQUIRED)
public class UserService {
}
2、ioslation:事务隔离级别
事务有一个特性为隔离性,多事务操作之间不会产生影响。不考虑隔离性就会产生很多问题
有三个读问题:脏读、不可重复读、虚读(幻读)
脏读:一个未提交事务读取到另一个未提交事务的数据
不可重复读:一个未提交事务读取到另一提交事务修改的数据
虚读:一个未提交事务读取到另一提交事务添加数据
解决以上几种问题的方法就是:通过设置事务隔离级别,解决读问题
@Service
@Transactional(isolation = Isolation.REPEATABLE_READ)
public class UserService {
}
3、timeout:超时时间
- 事务需要在一定时间内进行提交,如果不提交进行回滚
- 默认值是 -1 ,设置时间以秒单位进行计算
4、readOnly:是否只读
- 读:查询操作,写:添加修改删除操作
- readOnly 默认值 false,表示可以查询,可以添加修改删除操作
- 设置 readOnly 值是 true,只能查询
5、rollbackFor:回滚
设置出现哪些异常进行事务回滚
6、noRollbackFor:不回滚
设置出现哪些异常不进行事务回滚
2.xml配置文件方式实现声明式事务管理
1、配置事务管理器
这一步和使用注解方式实现事务管理一样,只是不需要开启事务注解的配置
<!-- 配置文件方式配置开启组件扫描 -->
<context:component-scan base-package="com.spring5"/>
<!--配置创建 DataSource 数据池对象-->
<bean id="dataSource" class="com.alibaba.druid.pool.DruidDataSource" destroy-method="close">
<property name="url" value="jdbc:mysql://localhost:3306/bank_db"/>
<property name="username" value="root" />
<property name="password" value="xk123" />
<property name="driverClassName" value="com.mysql.cj.jdbc.Driver" />
</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">
<!--注入数据源-->
<!--在源码中同样可发现此实现类也是通过set方法来配置DataSource对象-->
<property name="dataSource" ref="dataSource"/>
</bean>
2、配置通知
<!--配置通知(即配置要增强的功能,这里就是配置事务功能)-->
<tx:advice id="txadvice" transaction-manager="transactionManager">
<!--配置事务参数-->
<tx:attributes>
<!--指定在哪种规则的方法上面添加事务-->
<!--下面就是在这个方法名的方法上添加事务-->
<tx:method name="transfer" propagation="REQUIRES_NEW"/> <!--在这个标签里面还可以配置属性的值来配置事务的参数-->
<!--<tx:method name="*"/>--> <!--这种在所有方法上添加事务-->
</tx:attributes>
</tx:advice>
3、配置切入点和切面
<!--配置切入点和切面-->
<aop:config>
<!--配置切入点(即配置要增强功能的方法,在这儿其实就是配置要给哪些方法添加事务)-->
<aop:pointcut id="pt" expression="execution(* com.spring5.service.UserService.*(..))"/> <!--表示给该类中所有方法添加事务-->
<!--配置切面(即配置将增强的功能增强到切入点的具体过程)-->
<aop:advisor advice-ref="txadvice" pointcut-ref="pt"/>
</aop:config>
3.完全注解实现声明式事务
@Configuration //配置类
@ComponentScan(basePackages = "com.spring5") //开启组件扫描
@EnableTransactionManagement //开启事务管理
public class SpringConfig {
//创建数据库连接池
@Bean
public DruidDataSource getDruidDataSource() {
DruidDataSource dataSource = new DruidDataSource();
dataSource.setDriverClassName("com.mysql.cj.jdbc.Driver");
dataSource.setUrl("jdbc:mysql://localhost:3306/user_db");
dataSource.setUsername("root");
dataSource.setPassword("root");
return dataSource;
}
//创建 JdbcTemplate 对象
@Bean
public JdbcTemplate getJdbcTemplate(DataSource dataSource) {
//到 ioc 容器中根据类型找到 dataSource
JdbcTemplate jdbcTemplate = new JdbcTemplate();
//注入 dataSource
jdbcTemplate.setDataSource(dataSource);
return jdbcTemplate;
}
//创建事务管理器
@Bean
public DataSourceTransactionManager getDataSourceTransactionManager(DataSource dataSource) {
DataSourceTransactionManager transactionManager = new DataSourceTransactionManager();
transactionManager.setDataSource(dataSource);
return transactionManager;
}
}
//通过测试可以发现数据库中的数据因为出现异常发生事务回滚,所以数据均没有发生改变
七、Spring5新功能
整个 Spring5 框架的代码基于 Java8,运行时兼容 JDK9,许多不建议使用的类和方法已从代码库中删除
7.1 整合日志框架
Spring 5.0 框架自带了通用的日志封装
- Spring5 已经移除 Log4jConfigListener,官方建议使用 Log4j2
7.1.1 Spring5 框架整合 Log4j2
1、导入jar包
<!--maven依赖-->
<dependency>
<groupId>org.apache.logging.log4j</groupId>
<artifactId>log4j-api</artifactId>
<version>2.17.0</version>
</dependency>
<dependency>
<groupId>org.apache.logging.log4j</groupId>
<artifactId>log4j-core</artifactId>
<version>2.17.0</version>
</dependency>
<dependency>
<groupId>org.apache.logging.log4j</groupId>
<artifactId>log4j-slf4j-impl</artifactId>
<version>2.17.0</version>
</dependency>
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-api</artifactId>
<version>2.0.0-alpha6</version>
</dependency>
<!--下面这个依赖在slf4j的1.8版本后需要加上,否则会报no SLF4J provider were found,具体原因可查看报错时给出的链接进行查看-->
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-nop</artifactId>
<version>2.0.0-alpha6</version>
</dependency>
2、创建log4j2.xml配置文件(配置文件名必须为log4j2.xml)
<?xml version="1.0" encoding="UTF-8"?>
<!--日志级别以及优先级排序: OFF > FATAL > ERROR > WARN > INFO > DEBUG > TRACE > ALL -->
<!--Configuration 后面的 status 用于设置 log4j2 自身内部的信息输出,可以不设置,当设置成 trace 时,可以看到 log4j2 内部各种详细输出-->
<configuration status="INFO">
<!--先定义所有的 appender-->
<appenders>
<!--输出日志信息到控制台-->
<console name="Console" target="SYSTEM_OUT">
<!--控制日志输出的格式-->
<PatternLayout pattern="%d{yyyy-MM-dd HH:mm:ss.SSS} [%t] %-5level %logger{36} - %msg%n"/>
</console>
</appenders>
<!--然后定义 logger,只有定义 logger 并引入的 appender,appender 才会生效-->
<!--root:用于指定项目的根日志,如果没有单独指定 Logger,则会使用 root 作为默认的日志输出-->
<loggers>
<root level="info">
<appender-ref ref="Console"/>
</root>
</loggers>
</configuration>
3、测试代码(手动打印日志)
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
public class UserLog {
private static final Logger logger = LogManager.getLogger(UserLog.class);
public static void main(String[] args) {
logger.info("hello log4j2 info...");
logger.warn("hello log4j2 warn...");
}
}
在测试代码时因看视频中的Logger和LogFactory是从org.slf4j包中导入的,所以我在测试时也是导入的这个包,但是当我运行时发现没有任何日志信息打印出来,此时我就去搜查了一下,原来视频中的那种写法是log4j的写法,其实用于log4j2是错误的,我也不清楚为什么视频中就能测试出来,于是我看了下面这篇博客后导入正确的包才成功解决了这个问题https://blog.csdn.net/zqg4919/article/details/78321580
7.2 支持@Nullable注解
@Nullable
注解可以使用在方法上面,属性上面,参数上面,表示方法返回可以为空,属性值可以为空,参数值可以为空
public class NullableTest {
//注解使用在属性上面,属性值可以为空
@Nullable
private String name;
//注解用在方法上面,方法返回值可以为空
@Nullable
public void testNullable(@Nullable String parameter){//注解使用在方法参数里面,方法参数可以为空
}
}
7.3 函数式风格
Spring5 核心容器支持函数式风格 GenericApplicationContext
我们知道,在Spring框架中,对象的创建都有Spring容器自己完成,但是当我们想使用new
关键字自己创建对象时Spring容器是得不到这个对象的
举例:(为了方便实体类使用上述的NullableTest类)
NullableTest nullableTest = new NullableTest();
//当我们像上方那样创建好对象后,如果我们想通过getBean方法获取到这个对象或将它注入到其他地方,那么Spring容器是不能识别到这个对象从而进行这些行为的
//此时我们就需要用到Spring5的新功能
@Test
public void testGenericApplicationContext() {
//1 创建 GenericApplicationContext 对象
GenericApplicationContext context = new GenericApplicationContext();
//2 调用 context 的方法对象注册
context.refresh();
//registerBean方法中需要传入——创建的对象名,创建的对象类型,创建的对象——作为参数,其中对象名可以省略
//context.registerBean(NullableTest.class,() -> new NullableTest());//这种是省略对象名的写法
context.registerBean("nullableTest",NullableTest.class,() -> new NullableTest());//这种是不省略对象名的写法
//3 获取在 spring 注册的对象
//NullableTest nullableTest = (NullableTest) context.getBean("com.spring5.NullableTest");//省略对象名注册对象的方式
NullableTest nullableTest = (NullableTest) context.getBean("nullableTest");//不省略对象名注册对象的方式
//上述两种方式均能将我们自己创建的对象注册到Spring容器中
System.out.println(nullableTest);//com.spring5.NullableTest@78452606
}
7.4 整合JUnit5单元测试框架
7.4.1 JUnit4
1、引入Spring测试相关依赖或导入jar包
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>4.13.2</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-test</artifactId>
<version>5.3.16</version>
</dependency>
2、编写测试类
//创建测试类,使用注解方式完成
@RunWith(SpringJUnit4ClassRunner.class) //单元测试框架
//@ContextConfiguration("classpath:bean.xml") //读取配置文件的的方式,这儿可以自己看看这个注解的具体源码就知道了
@ContextConfiguration(classes = {SpringConfig.class}) //读取配置类的方式
public class TestSpring5 {
@Autowired
private NullableTest nullableTest; //自动装载
@Test
public void testJUnit(){
System.out.println(nullableTest);//com.spring5.NullableTest@5aac4250
//当我们需要用到nullableTest对象时它会帮我们自动创建并装载,此时我们就不需要再通过ClassPathXmlApplicationContext类或AnnotationConfigApplicationContext类来获取对象了
}
}
从这里也就解决了我在前面提到的疑惑“为什么每次测试我们仍需要通过getBean方法来获取service对象”,原来是测试类没有获取到配置文件或配置类的配置并且测试类也没有没有注入到Spring容器中,就不能获取到配置从而就不能自动创建对象了
7.4.2 JUnit5
1、引入JUnit5相关依赖或导入jar包
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-test</artifactId>
<version>5.3.16</version>
</dependency>
<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter-api</artifactId>
<version>5.8.2</version>
<scope>test</scope>
</dependency>
2、编写测试类
//创建测试类,使用注解方式完成
//方式几乎和JUnit4一样,只不过需要注意导入的包必须要是junit-jupiter-api
@ExtendWith(SpringExtension.class)
//@ContextConfiguration("classpath:bean.xml")
@ContextConfiguration(classes = {SpringConfig.class})
public class TestJUnit5 {
@Autowired
private NullableTest nullableTest;
@Test
public void testJUnit5(){
System.out.println(nullableTest);//com.spring5.NullableTest@6ffab045
}
}
另外,在JUnit5中,测试类上方的两个注解可以用一个复合注解代替,即@SpringJUnitConfig(locations = "classpath:bean1.xml")
或@SpringJUnitConfig(classes = {SpringConfig.class})
7.5 Webflux
7.5.1 基本概念
Webflux是 Spring5 添加新的模块,用于 web 开发的,功能和 SpringMVC 类似的,Webflux 使用当前一种比较流行的响应式编程出现的框架。
目前使用的传统 web 框架,比如 SpringMVC,这些都是基于 Servlet 容器。
Webflux 是一种异步非阻塞的框架,异步非阻塞的框架在 Servlet3.1 以后才支持,核心是基于 Reactor 的相关 API 实现的。
什么是异步非阻塞?
- 异步和同步
- 非阻塞和阻塞
- 上面两种术语针对对象不一样
- 异步和同步针对调用者,调用者发送请求,如果等着对方回应之后才去做其他事情就是同步,如果发送请求之后不用等对方回应就去做其他事情就是异步(调用者需要等到一个请求被响应后才能发出下一个请求就是同步,调用者发出一个请求后还可以发送其他请求就是异步)
- 阻塞和非阻塞针对被调用者,被调用者受到请求之后,做完请求任务之后才给出反馈就是阻塞,受到请求之后马上给出反馈然后再去做事情就是非阻塞(被调用者处理完请求后再响应是阻塞,被调用者响应后再处理请求是非阻塞)
Webflux的特点:
- 非阻塞式:在有限资源下,提高系统吞吐量和伸缩性,以 Reactor 为基础实现响应式编程
- 函数式编程:Spring5 框架基于 java8,Webflux 使用 Java8 函数式编程方式实现路由请求
Spring WebFlux和Spring MVC的异同:
- 两个框架都可以使用注解方式,都运行在 Tomcat 等容器中
- SpringMVC 采用命令式编程,Webflux 采用异步响应式编程
7.5.2 响应式编程
什么是响应式编程?
响应式编程是一种面向数据流和变化传播的编程范式。这意味着可以在编程语言中很方便地表达静态或动态的数据流,而相关的计算模型会自动将变化的值通过数据流进行传播。比如,Excel的电子表格就是响应式编程的一个例子,单元格可以包含字面值或类似”=B1+C1”的公式,而包含公式的单元格的值会依据其他单元格的值的变化而变化。
1.Java实现响应式编程
在JDK8 及之前的版本中提供了观察者模式的两个类 Observer 和 Observable
其中,观察者类需要实现Observer接口,被观察者类需要继承Observable类
public class ObserverDemo extends Observable {
public static void main(String[] args) {
ObserverDemo observerDemo = new ObserverDemo();
observerDemo.addObserver((o, arg) -> {
System.out.println("数据发生了变化");
});
observerDemo.addObserver((o, arg) -> {
System.out.println("收到观察者通知");
});
observerDemo.addObserver((o, arg) -> {
System.out.println("收到观察者通知1");
});
//若没有下方使数据改变并且通知观察者的代码,则不会输出任何语句
observerDemo.setChanged();//使数据发生改变
observerDemo.notifyObservers();//通知观察者
//收到被观察者通知1
//收到被观察者通知
//数据发生了变化
//从输出顺序可以看出,观察者被通知的顺序是就近原则,可能是这三个观察者对象被存放到了类似栈的数据结构中(个人认为)
}
}
在JDK9中,Observer和Observable就被Flow类替代了,在Flow类中,通过内部接口Publisher中的subscribe方法实现观察者模式
public class ObserverDemo extends Observable {
public static void main(String[] args) {
//subscriber——订阅者/观察者,Publisher——发布者/被观察者
//在这里面subscriber应该是充当观察者,当观察到有数据变化publisher则做出相应处理方式
Flow.Publisher<String> publisher = subscriber -> {
subscriber.onNext("1");
subscriber.onNext("2");
subscriber.onError(new RuntimeException("出错"));
}
}
}
想要弄懂这里需要了解什么是观察者模式?
观察者模式其实也是发布/订阅模式,这是一种对象间一对多(一是被观察者,多是观察者)的依赖关系,每当一个对象状态发生改变时,与其依赖的对象都会得到通知并自动更新。
2.Reactor实现响应式编程
Reactor官网文档:https://projectreactor.io/docs/core/release/reference/
响应式编程操作中,需要满足 Reactive 规范,而Reactor就是满足 Reactive 规范的一种框架
Reactor 有两个核心类,Mono
和 Flux
,这两个类实现接口 Publisher,提供丰富的操作符。Flux 对象实现发布者,返回 N 个元素;而Mono 实现发布者,返回 0 或者 1 个元素
Flux 和 Mono 都是数据流的发布者,使用 Flux 和 Mono 都可以发出三种数据信号:元素值,错误信号,完成信号(错误信号和完成信号都代表终止信号,终止信号用于告诉订阅者数据流结束了,错误信号终止数据流同时把错误信息传递给订阅者)
代码演示
<!--导入maven依赖-->
<dependency>
<groupId>io.projectreactor</groupId>
<artifactId>reactor-core</artifactId>
<version>3.4.14</version>
</dependency>
//测试代码
public class ReactorTest {
public static void main(String[] args) {
//just 方法直接声明数据流
Flux.just(1,2,3,4);
Mono.just(1);
//其他的方法
Integer[] array = {1,2,3,4};
Flux.fromArray(array); //数组形式
List<Integer> list = Arrays.asList(array);
Flux.fromIterable(list); //集合形式
Stream<Integer> stream = list.stream();
Flux.fromStream(stream); //流形式
}
}
//调用 just 或者其他方法只是声明数据流,数据流并没有发出,只有进行订阅之后才会触发数据流,不订阅什么都不会发生的
//只有像这样调用subscribe方法订阅后才会触发数据流
Flux.just(1,2,3,4).subscribe(System.out::println);
Mono.just(1).subscribe(System.out::println);
三种信号特点
- 错误信号和完成信号都是终止信号,不能共存的
- 如果没有发送任何元素值,而是直接发送错误或者完成信号,表示是空数据流
- 如果没有错误信号,没有完成信号,表示是无限数据流
3.操作符
操作符是对数据流进行一道道操作,最终得到我们想要的数据流,比如工厂流水线,其中对产品进行加工的一道道工序就是相当于是操作符
常见的操作符:
map :将元素映射为新元素
flatMap:将元素映射为流,把每个元素转换流,把转换之后的多个流合并大的流
7.5.3 执行流程和核心API
SpringWebflux 基于 Reactor,默认使用容器是 Netty,Netty 是高性能的 NIO 框架,异步非阻塞的框架
1.NIO框架
2.SpringWebflux 执行过程和 SpringMVC 相似
SpringWebflux 核心控制器 DispatcherHandler
类,该类实现接口 WebHandler
,实现了接口 WebHandler 中的一个方法 handle
该方法在DispatcherHandler类中的具体实现:(需要创建SpringBoot项目导入spring-boot-starter-webflux
依赖才能查看)
WebHandler接口不同实现类的不同功能:
3.SpringWebflux 里面 DispatcherHandler,负责请求的处理
- HandlerMapping:请求查询到处理的方法
- HandlerAdapter:真正负责请求处理
- HandlerResultHandler:响应结果处理
4.SpringWebflux 实现函数式编程,两个接口:RouterFunction(路由处理)和 HandlerFunction(处理函数)
7.5.4 注解编程模型
SpringWebflux 实现方式有两种:注解编程模型和函数式编程模型
使用注解编程模型的方式,和 SpringMVC 使用相似,只需要把相关依赖配置到项目中,SpringBoot 自动配置相关运行容器,默认情况下使用 Netty 服务器
首先,通过代码实现注解编程模型
1.创建一个SpringBoot项目
在新建项目中选择新建Spring Initializr项目
完成项目创建
2.导入依赖
在pom文件中将spring-boot-starter-web
依赖更改为spring-boot-starter-webflux
3.在配置文件中配置服务器端口号
4.创建好相关包和类
//实体类
public class User {
private String name;
private String gender;
private Integer age;
public User(String name, String gender, Integer age) {
this.name = name;
this.gender = gender;
this.age = age;
}
//省略setter,getter和toString方法
}
//service
@Service
public class UserServiceImpl implements UserService {
//由于为了方便不连接数据库,所以这里设置一个Map来存放数据
private final Map<Integer,User> users = new HashMap<>();
//在构造函数中对数据进行初始化
public UserServiceImpl(){
this.users.put(1,new User("Eric","男",23));
this.users.put(2,new User("Jack","男",26));
this.users.put(3,new User("Lucy","女",20));
}
//根据id查询用户
@Override
public Mono<User> getUserById(Integer id) {
return Mono.justOrEmpty(this.users.get(id)); //justOrEmpty方法:创建一个新的 Mono ,如果非 null 则发出指定的项目,否则只发出 完成
}
//查询所有用户
@Override
public Flux<User> getAllUser() {
return Flux.fromIterable(this.users.values());
//fromXxx方法
//fromArray(从数组)、fromIterable(从迭代器)、fromStream(从 Java Stream 流) 的方式来创建 Flux
}
//添加新用户
@Override
public Mono<Void> addUser(Mono<User> user) {
return user.doOnNext(person -> { //doOnNext方法:添加行为时触发 Mono 成功发出数据
//向集合添加新数据
int id = users.size() + 1;
users.put(id,person);
}).thenEmpty(Mono.empty());//生成一个空的有限流,这儿用来终止数据流
}
}
//controller
@RestController
public class UserController {
@Autowired
private UserService userService;
//id 查询
@GetMapping("/user/{id}")
public Mono<User> getUserById(@PathVariable int id) {
return userService.getUserById(id);
}
//查询所有
@GetMapping("/users")
public Flux<User> getUsers() {
return userService.getAllUser();
}
//添加
@PostMapping("/addUser")
public Mono<Void> saveUser(@RequestBody User user) {
Mono<User> userMono = Mono.just(user);//just方法的作用:将数据作为流发出并结束这段数据流
return userService.addUser(userMono);
}
}
//若以上方法不清楚功能,可以返回到7.5.2节的第二部分中点击链接查看Reactor官网文档
5.启动项目
启动项目可以在控制台发现SpringBoot项目使用的是Netty容器,并且使用的端口是配置的8081端口
然后在浏览器访问数据,数据在浏览器中成功显示则代表成功
注解编程模型实现的说明:
- SpringMVC 方式实现——是同步阻塞的方式,基于
SpringMVC+Servlet+Tomcat
- SpringWebflux 方式实现——异步非阻塞方式,基于
SpringWebflux+Reactor+Netty
7.5.5 函数式编程模型
在使用函数式编程模型操作时候,需要自己初始化服务器
基于函数式编程模型时候,有两个核心接口:RouterFunction
(实现路由功能,请求转发给对应的 handler)和 HandlerFunction
(处理请求,生成响应的函数)。核心任务就是定义这两个函数式接口的实现类并且启动需要的服务器
SpringWebflux 请求和响应不再是 ServletRequest 和 ServletResponse ,而是ServerRequest
和 ServerResponse
函数式编程各个部分之间的联系:
1.构建项目
在注解编程模型的基础上删除controller层,保留service和entity
2.创建Handler
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.parseInt(request.pathVariable("id"));
//空值处理
Mono<ServerResponse> notFound = 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(notFound);
//如果userMono不为空,服务响应成功,并将userMono转换成json数据格式并流化处理,将其作为响应返回;如果userMono为空,则返回刚刚创建的空数据
//从方法名可以看出:
//contentType方法是设置数据的格式类型,body方法是设置响应体的数据,switchIfEmpty方法是判断我们要传的数据是否为空
}
//查询所有
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) {
//得到 user 对象
Mono<User> userMono = request.bodyToMono(User.class);
return ServerResponse.ok().build(this.userService.addUser(userMono));
}
}
3.初始化服务器,编写router
public class Server {
//1 创建 Router 路由
public RouterFunction<ServerResponse> routingFunction() {
//创建 hanler 对象
UserService userService = new UserServiceImpl();
UserHandler handler = new UserHandler(userService);
//设置路由
return RouterFunctions.route(GET("/users/{id}").and(accept(APPLICATION_JSON)),handler::getUserById)
.andRoute(GET("/users").and(accept(APPLICATION_JSON)),handler::getAllUsers);
//设置 请求的方式、接收数据类型、处理请求的具体方法
//被路由的方法中都需要有ServerRequest参数,否则这里会无法解析
}
//2 创建服务器完成适配
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();
}
}
4.调用
public static void main(String[] args) throws Exception{
Server server = new Server();
server.createReactorServer();
System.out.println("enter to exit");
System.in.read();
}
运行main方法,在控制台可以看到开启的服务器及端口
在浏览器中访问路径,得到数据
在控制台还可以看见请求响应的具体信息:
5.使用WebClient调用
public class Client {
public static void main(String[] args) {
//调用服务器地址
WebClient webClient = WebClient.create("http://127.0.0.1:55642");
//根据 id 查询
String id = "1";
User user = webClient.get().uri("/users/{id}", id).accept(MediaType.APPLICATION_JSON).retrieve().bodyToMono(User.class).block();
System.out.println(user);//User{name='Eric', gender='男', age=23}
//查询所有
Flux<User> results = webClient.get().uri("/users").accept(MediaType.APPLICATION_JSON).retrieve().bodyToFlux(User.class);
results.map(stu -> stu.getName()).buffer().doOnNext(System.out::println).blockFirst();//[Eric, Jack, Lucy]
}
}