Spring5从入门到精通
0.前言
本文章是基于尚硅谷谷粒学院的spring5框架视频,加入了自己的一些见解,文中有出错的地方还请指正,码字不易,支持一下吧!
文中涉及的所有源码点击此处下载
本笔记是基于maven3.8.4、java8操作完成的
1.概述及入门案例
概述
-
1、Spring是轻量级的开源的
JavaEE
框架 -
2、Spring 可以解决企业应用开发的复杂性
-
3、Spring有两个核心部分:
IOC
和Aop
- (1)
IOC
:控制反转,把创建对象过程交给 Spring进行管理 - (2)
Aop
:面向切面,不修改源代码进行功能增强
- (1)
-
4、Spring特点
-
(1)方便解耦,简化开发
-
(2)Aop编程支持
-
(3)方便程序测试
-
(4)方便和其他框架进行整合
-
(5)方便进行事务操作
-
(6)降低 API 开发难度
-
入门案例
- 下载Spring5,目前最新稳定版本为
5.3.15
- 下载地址:
https://repo.spring.io/ui/native/release/org/springframework/spring/
-
解压后
其中对应的所有
jar
包都在libs
目录中
- 相关结构图:
-
目前做测试,以下只用到了核心部分
-
idea中新建maven项目
spring_demo
-
通过maven引入相关的jar包,也可手动导入
<!-- https://mvnrepository.com/artifact/org.springframework/spring-expression --> <dependency> <groupId>org.springframework</groupId> <artifactId>spring-expression</artifactId> <version>5.3.15</version> </dependency> <!-- https://mvnrepository.com/artifact/org.springframework/spring-context --> <dependency> <groupId>org.springframework</groupId> <artifactId>spring-context</artifactId> <version>5.3.15</version> </dependency> <!-- https://mvnrepository.com/artifact/org.springframework/spring-core --> <dependency> <groupId>org.springfram ework</groupId> <artifactId>spring-core</artifactId> <version>5.3.15</version> </dependency> <!-- https://mvnrepository.com/artifact/org.springframework/spring-beans --> <dependency> <groupId>org.springframework</groupId> <artifactId>spring-beans</artifactId> <version>5.3.15</version> </dependency> <!-- https://mvnrepository.com/artifact/commons-logging/commons-logging --> <dependency> <groupId>commons-logging</groupId> <artifactId>commons-logging</artifactId> <version>1.2</version> </dependency>
-
创建一个类
com.soberw.spring.HelloSpring
,并声明一个方法hello()
:package com.soberw.spring; /** * @author soberw * @Classname HelloSpring * @Description * @Date 2022-02-10 14:21 */ public class HelloSpring { public void hello(){ System.out.println("Hello Spring..."); } }
-
创建一个Spring配置文件,在配置文件中配置创建的对象
- 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"> <!-- 配置HelloSpring对象创建--> <bean id="helloSpring" class="com.soberw.spring.HelloSpring"></bean> </beans>
-
编写测试类进行测试
TestSpring
import com.soberw.spring.HelloSpring; import org.junit.Test; import org.springframework.context.ApplicationContext; import org.springframework.context.support.ClassPathXmlApplicationContext; /** * @author soberw * @Classname TestSpring * @Description * @Date 2022-02-10 14:28 */ public class TestSpring { @Test public void testSpring(){ // 1.加在spring配置文件 ApplicationContext context = new ClassPathXmlApplicationContext("/bean1.xml"); // 2. 获取配置创建的对象,通过配置得id值获取 HelloSpring helloSpring = context.getBean("helloSpring", HelloSpring.class); // 3.测试输出 System.out.println(helloSpring); helloSpring.hello(); } }
2.IOC容器
控制反转(Inversion of Control,缩写为IoC),是面向对象编程
中的一种设计原则,可以用来减低计算机代码
之间的耦合度
。其中最常见的方式叫做**依赖注入
(Dependency Injection,简称DI**),还有一种方式叫“依赖查找”(Dependency Lookup)。通过控制反转,对象在被创建的时候,由一个调控系统内所有对象的外界实体将其所依赖的对象的引用传递给它。也可以说,依赖被注入到对象中。
什么是IOC?
- 控制反转,把对象的创建和对象之间的调用过程,交给Spring进行管理
- 使用IOC目的:为了耦合度降低
- 做的入门案例就是通过IOC实现的
IOC底层原理
主要使用的技术:
- xml解析
- 工厂设计模式
- 注解反射技术
原始的创建类对象方式,耦合度太高,通过工厂模式,可以降低原始方式的高耦合度,但也不是最优,即没有将耦合度降到最低
引入IOC的目的是为了进一步降低耦合度,使得程序维护起来更方便,其底层原理如下,通过此过程可以进一步降低耦合度
IOC接口(BeanFactory
)
IOC思想基于IOC容器完成,IOC容器底层就是工厂对象
Spring提供IOC容器的两种实现方式(两个接口):
-
BeanFactory
:IOC容器基本实现,是Spring内部的使用接口,不提供开发人员进行使用- 加载配置文件的使用不会去创建对象,在获取对象(使用)才会去创建对象
-
ApplicationContext
:BeanFactory接口的子接口,提供了更多更强大的功能,一般由开发人员进行使用- 加载配置文件时候就会把在配置文件中的对象进行创建
-
ApplicationContext
接口的实现类:ClassPathXmlApplicationContext
,在src目录下可以写文件名
FileSystemXmlApplicationContext
,在src目录下,必须写绝对路径
IOC操作Bean管理(基于xml)
什么是Bean
管理?
Bean管理指的是两个操作:
- 用Spring创建对象
- 用Spring注入属性(对象的属性)
Bean管理操作的两种实现方式:
- 基于xml配置文件方式实现
- 基于注解方式实现
xml注入属性
普通属性
-
基于xml方式创建对象
(1). 在spring配置文件中,使用bean标签,标签里面添加对应的属性,就可以实现对象的创建
(2). 在bean标签里面有很多的属性,常用的有:
- id:唯一标识,相当于创建的类的对象名
- class:全类名(类的全路径,包类路径)
- name:作用于id一样,唯一的标识一个类,目前使用不多,与id的区别是可以使用特殊符号
(3). 在创建对象的时候,默认也是执行
无参构造方法
完成对对象的创建 -
基于xml方式注入属性
DI
:依赖注入,就是注入属性,可以理解为IOC的一种具体实现主要有两种注入方式:
- 第一种:使用
set
方式进行注入(常用) - 第二种:使用
有参构造
进行注入
- 第一种:使用
-
第一种注入方式:使用set方法进行注入(要求类中必须有属性对应的set方法):
(1). 创建类,定义属性以及对应的
set
,get
方法package com.soberw.spring; /** * 演示使用set方法进行注入属性 * * @author soberw * @Classname Book * @Description * @Date 2022-02-10 15:36 */ public class Book { //创建属性 private String bookName; private String bookAuthor; //生成方法 public void setBookAuthor(String bookAuthor) { this.bookAuthor = bookAuthor; } public void setBookName(String bookName) { this.bookName = bookName; } public String getBookName() { return bookName; } public String getBookAuthor() { return bookAuthor; } }
(2). 在spring配置文件中配置对象创建,配置属性注入
<!--1.set方法注入属性--> <!-- 1.配置Book对象创建--> <bean id="book" class="com.soberw.spring.Book"> <!-- 2.使用property完成属性的注入 name: 类里面的属性名称,是设置属性值的依据, 即如果name设置为xxx,则会通过类中的setXxx()方法给属性赋值 value: 向属性注入的值 --> <property name="bookName" value="Thinking Java"></property> <property name="bookAuthor" value="John"></property> </bean>
(3). 测试配置
@Test public void testBook(){ ApplicationContext app = new ClassPathXmlApplicationContext("/MyBean.xml"); Book book = app.getBean("book", Book.class); System.out.println(book); System.out.printf("书本名称:%s,书本作者:%s。",book.getBookName(),book.getBookAuthor()); }
-
第二种注入方式:使用有参构造进行注入:
(1). 创建类,定义属性,创建属性对应的有参构造方法
package com.soberw.spring; /** * 使用有参数构造注入 * * @author soberw * @Classname Orders * @Description * @Date 2022-02-10 16:13 */ public class Orders { //创建属性 private String name; private String address; //创建有参构造 public Orders(String name, String address) { this.name = name; this.address = address; } }
(2). 在spring配置文件中进行配置
<!--3.有参构造注入属性--> <bean id="orders" class="com.soberw.spring.Orders"> <constructor-arg name="name" value="电脑"></constructor-arg> <constructor-arg name="address" value="china"></constructor-arg> <!--也可以根据索引赋值,即设置其index属性,下标默认从0开始--> </bean>
(3). 测试配置
@Test public void testOrders() { ApplicationContext app = new ClassPathXmlApplicationContext("/MyBean.xml"); Orders orders = app.getBean("orders", Orders.class); System.out.println(orders); }
-
p名称空间注入(了解即可)
使用p名称空间注入,可以简化xml配置方式
- 第一步:添加
p
名称空间在配置文件中
- 第二步:进行属性注入,即在bean标签里面进行操作,通过添加bean标签的属性完成类属性的赋值
<!--3.p空间注入属性--> <bean id="book" class="com.soberw.spring.Book" p:bookName="sql" p:bookAuthor="Joy"> </bean>
- 测试配置
- 第一步:添加
字面量
注入属性:字面量
(1). null
值
- 在
Book
中添加address
属性,并在配置文件中赋值为null
<!--null值-->
<property name="address">
<null/>
</property>
- 测试
@Test
public void testBook() {
ApplicationContext app = new ClassPathXmlApplicationContext("/MyBean.xml");
Book book = app.getBean("book", Book.class);
System.out.println(book);
System.out.printf("书本名称:%s,书本作者:%s。\n", book.getBookName(), book.getBookAuthor());
System.out.println(book.getAddress());
}
(2). 属性值包括特殊符号
<!--属性值包括特殊符号
1. 把<>进行转义 < >
2. 把带特殊符号内容写到CDATA,即<![CDATA[...]]>
-->
<property name="address">
<value>
<![CDATA[<>南京<>]]>
</value>
</property>
xml注入对象类型属性
注入外部bean属性
(1). 创建两个类:service
和dao
(2). 在service调用dao里面的方法
- UserService类
package com.soberw.spring.service;
import com.soberw.spring.dao.UserDao;
/**
* @author soberw
* @Classname UserService
* @Description
* @Date 2022-02-10 17:50
*/
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对象
// UserDao userDao = new UserDaoImpl();
userDao.update();
}
}
- UserDao接口
package com.soberw.spring.dao;
/**
* @author soberw
* @Classname UserDao
* @Description
* @Date 2022-02-10 17:50
*/
public interface UserDao {
void update();
}
- UserDaoImpl类实现UserDao接口
package com.soberw.spring.dao;
/**
* @author soberw
* @Classname UserDaoImpl
* @Description
* @Date 2022-02-10 17:51
*/
public class UserDaoImpl implements UserDao{
@Override
public void update() {
System.out.println("dao update ... ");
}
}
(3). 在spring配置文件中进行配置
<!--1.service和dao对象的创建-->
<bean id="userService" class="com.soberw.spring.service.UserService">
<!--注入userDao对象
name属性值:类里面的属性名称
ref属性值:创建userDao对象bean标签id值
-->
<property name="userDao" ref="userDaoImpl"></property>
</bean>
<bean id="userDaoImpl" class="com.soberw.spring.dao.UserDaoImpl"></bean>
(4). 测试配置
@Test
public void TestUserService(){
ApplicationContext app = new ClassPathXmlApplicationContext("/bean2.xml");
UserService userService = app.getBean("userService", UserService.class);
System.out.println(userService);
userService.add();
}
注入内部bean属性
通过一个典型的例子引入:
在关系规则中有三种对应关系:一对一,一对多,多对多
以一对多关系为例:部门和员工
一个部门有多个员工,一个员工属于一个部门 部门是一,员工是多
(1). 在实体类之间表示一对多关系
- 员工表示所属部门,使用对象类型属性进行表示
- 部门类
department
package com.soberw.spring.bean;
/**
* @author soberw
* @Classname Department
* @Description
* @Date 2022-02-10 19:05
*/
public class Department {
private String deptname;
public void setDeptname(String deptname) {
this.deptname = deptname;
}
@Override
public String toString() {
return deptname;
}
}
- 员工类
Employee
package com.soberw.spring.bean;
/**
* @author soberw
* @Classname Employee
* @Description
* @Date 2022-02-10 19:05
*/
public class Employee {
private String empname;
private String gender;
//员工属于某一个部门,使用对象形式表示
private Department dept;
public void setEmpname(String empname) {
this.empname = empname;
}
public void setGender(String gender) {
this.gender = gender;
}
public void setDept(Department dept) {
this.dept = dept;
}
@Override
public String toString() {
return String.format("%s::%s::%s\n", empname, gender, dept);
}
}
(2). 配置spring文件
<!--2. 内部bean的创建-->
<bean id="employee" class="com.soberw.spring.bean.Employee">
<!--设置两个普通属性-->
<property name="empname" value="张三"></property>
<property name="gender" value="男"></property>
<!--设置对象类型属性-->
<property name="dept">
<bean id="department" class="com.soberw.spring.bean.Department">
<property name="deptname" value="IT部门"></property>
</bean>
</property>
</bean>
(3). 测试配置
@Test
public void TestEmp(){
ApplicationContext app = new ClassPathXmlApplicationContext("/bean2.xml");
Employee employee = app.getBean("employee", Employee.class);
System.out.println(employee);
}
级联赋值属性
(1). 第一种写法:
<!--3. 级联赋值-->
<!--第一种写法-->
<bean id="employee" class="com.soberw.spring.bean.Employee">
<!--设置两个普通属性-->
<property name="empname" value="lucy"></property>
<property name="gender" value="女"></property>
<!--级联赋值-->
<property name="dept" ref="department"></property>
</bean>
<bean id="department" class="com.soberw.spring.bean.Department">
<property name="deptname" value="财务部"></property>
</bean>
(2). 第二种写法:
<!--第二种写法-->
<bean id="employee" class="com.soberw.spring.bean.Employee">
<!--设置两个普通属性-->
<property name="empname" value="lucy"></property>
<property name="gender" value="女"></property>
<!--级联赋值-->
<property name="dept" ref="department"></property>
<!--直接设置会报错,需要在employee类中添加dept的get方法-->
<property name="dept.deptname" value="技术部"></property>
</bean>
<bean id="department" class="com.soberw.spring.bean.Department"></bean>
xml注入集合类型属性
注入集合类型可以概括为以下四种:
-
注入数组类型属性
-
注入List集合类型属性
-
注入set集合类型属性
-
注入Map集合类型属性
基本实现
-
创建类
Stu
,定义数组、list、set、map类型属性,生成对应的set
方法package com.soberw.spring5.collectiontype; import java.util.List; import java.util.Map; import java.util.Set; /** * @author soberw * @Classname Stu * @Description * @Date 2022-02-10 20:04 */ public class Stu { // 1. 数组类型属性 private String[] courses; // 2. list集合类型属性 private List<String> list; // 3. map集合类型属性 private Map<String,String> maps; // 4. set集合类型属性 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 setSet(Set<String> sets) { this.sets = sets; } public void show() { System.out.println("array->" + Arrays.toString(courses)); System.out.println("list->" + list); System.out.println("set->" + sets); System.out.println("map->" + maps); } }
-
在spring配置文件中进行配置
<!--1.集合类型属性的注入--> <bean id="stu" class="com.soberw.spring5.collectiontype.Stu"> <!--数组类型的属性注入--> <property name="courses"> <!--array和list标签都可--> <array> <value>java</value> <value>mysql</value> <value>web</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> <entry key="PHP" value="php"></entry> </map> </property> <!--set类型的属性注入--> <property name="sets"> <set> <value>html</value> <value>css</value> <value>JavaScript</value> </set> </property> </bean>
-
测试配置
@Test public void testStu(){ ApplicationContext app = new ClassPathXmlApplicationContext("/bean.xml"); Stu stu = app.getBean("stu", Stu.class); stu.show(); }
以上都只是对一些基本类型以及字符串的属性赋值,如果想要在集合中存放一些对象类型呢?下面针对上面例子进行优化。
优化一:在集合中设置对象属性值
-
创建一个课程类
Course
:package com.soberw.spring5.collectiontype; /** * @author soberw * @Classname Course * @Description * @Date 2022-02-10 20:52 */ public class Course { //课程名称 private String cname; public void setCname(String cname) { this.cname = cname; } }
-
在
Stu
类中创建一个集合courseList
存放学生所选的多门课程,并生成set
方法://学生所选的多门课程 private List<Course> courseList; public void setCourseList(List<Course> courseList) { this.courseList = courseList; } public List<Course> getCourseList() { return courseList; }
-
在spring配置文件中进行配置
<!--创建多个course对象,存放多门课程--> <bean id="course1" class="com.soberw.spring5.collectiontype.Course"> <property name="cname" value="java"></property> </bean> <bean id="course2" class="com.soberw.spring5.collectiontype.Course"> <property name="cname" value="web"></property> </bean> <bean id="course3" class="com.soberw.spring5.collectiontype.Course"> <property name="cname" value="mysql"></property> </bean> <!--注入list集合类型,值为对象Course--> <property name="courseList"> <list> <ref bean="course1"></ref> <ref bean="course2"></ref> <ref bean="course3"></ref> </list> </property>
-
测试配置
如果集合中存放的值供好几个类调用,那么这种设置方法就显得比较笨拙了,需要在每个类中都引入外部注入,下面进行优化
优化二:抽取集合注入的部分
以list,map为例,其他类同
-
创建一个类
Book
package com.soberw.spring5.collectiontype; import java.util.List; import java.util.Map; /** * @author soberw * @Classname Book * @Description * @Date 2022-02-10 21:15 */ public class Book { //存放书本名称 private List<String> bookList; //存放学科的教材用书 private Map<Course, String> courseBook; public void setBookList(List<String> bookList) { this.bookList = bookList; } public void setCourseBook(Map<Course, String> courseBook) { this.courseBook = courseBook; } public List<String> getBookList() { return bookList; } public Map<Course, String> getCourseBook() { return courseBook; } }
-
在spring配置文件中配置:
- 在xml中引入名称空间
util
- 利用
util
名称空间完成集合注入提取
<?xml version="1.0" encoding="UTF-8"?> <beans xmlns="http://www.springframework.org/schema/beans" xmlns:util="http://www.springframework.org/schema/util" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd http://www.springframework.org/schema/util http://www.springframework.org/schema/util/spring-util.xsd"> <!--提取list集合类型属性的注入--> <util:list id="bookList"> <value>Java核心技术</value> <value>数据结构</value> </util:list> <!--提取map集合类型属性的注入--> <util:map id="courseBook"> <entry key-ref="course1" value="Java入门教程"></entry> <entry key-ref="course2" value="Web核心技术"></entry> <entry key-ref="course3" value="MySQL精讲"></entry> </util:map> <!--创建多个course对象,存放多门学科--> <bean id="course1" class="com.soberw.spring5.collectiontype.Course"> <property name="cname" value="java"></property> </bean> <bean id="course2" class="com.soberw.spring5.collectiontype.Course"> <property name="cname" value="web"></property> </bean> <bean id="course3" class="com.soberw.spring5.collectiontype.Course"> <property name="cname" value="mysql"></property> </bean> <!--注入使用--> <bean id="book" class="com.soberw.spring5.collectiontype.Book"> <property name="bookList" ref="bookList"></property> <property name="courseBook" ref="courseBook"></property> </bean> </beans>
- 在xml中引入名称空间
-
测试配置
@Test public void testBook(){ ApplicationContext app = new ClassPathXmlApplicationContext("/bean2.xml"); Book book = app.getBean("book", Book.class); System.out.println(book.getBookList()); System.out.println(book.getCourseBook()); }
工厂bean(FactoryBean)
Spring有两种类型的bean:
- 一种是
普通的bean
- 在配置文件中定义的bean类型,就是返回类型,之前写的都是普通的bean
- 另一种是
工厂bean
(FactoryBean)- 在配置文件中定义的bean类型可以和返回类型不一样
其中所谓的
定义的bean类型
,实际上指的就是bean的class属性所对应的类的全类名而
返回类型
就是在读取配置文件时获取到的bean对象
比如定义的bean类型是
那么普通bean得到的就是Book的对象
而工厂bean则可以不一样,即得到的也可以不是Book的对象
创建一个工厂bean
主要有以下两个步骤:
- 第一步:创建类,让这个类作为工厂bean,实现接口
FactoryBean
- 第二步:实现接口里面的方法,在实现的方法中定义返回的bean类型
具体演示
-
创建一个类
MyBean
,实现FactoryBean
接口,注意这里我指定的返回类型是Course
类package com.soberw.spring5.factorybean; import com.soberw.spring5.collectiontype.Course; import org.springframework.beans.factory.FactoryBean; /** * @author soberw * @Classname MyBean * @Description * @Date 2022-02-11 9:29 */ public class MyBean implements FactoryBean<Course>{ //返回bean @Override public Course getObject() throws Exception { //创建一个course对象并对属性赋值用以返回 Course course = new Course(); course.setCname("abx"); return course; } @Override public Class<?> getObjectType() { return null; } }
-
在spring配置文件中设置
<bean id="myBean" class="com.soberw.spring5.factorybean.MyBean"> </bean>
-
测试配置
@Test public void testMyBean(){ ApplicationContext app = new ClassPathXmlApplicationContext("/MyBean.xml"); //当前暂不知道获取到的是那种类型,暂用Object接收 Object myBean = app.getBean("myBean"); System.out.println(myBean); //判断输出 if(myBean instanceof Course) { System.out.println("course..."); }else if(myBean instanceof MyBean){ System.out.println("mybean..."); } }
答案显而易见,返回的是
Course
类型的对象,查看FactoryBean接口API源码:不难发现,返回的类型就是我们指定的泛型类型。
bean作用域
在Spring里,我们可以去设置创建的bean实例是单实例
的还是多实例
的
-
在
默认
情况下,创建的bean都是单实例对象:- 创建一个类
Scope
,并进行配置:
package com.soberw.spring5.scope; /** * @author soberw * @Classname Scope * @Description * @Date 2022-02-11 10:15 */ public class Scope { private String namespace; public String getNamespace() { return namespace; } public void setNamespace(String namespace) { this.namespace = namespace; } }
<bean id="scope" class="com.soberw.spring5.scope.Scope"> <property name="namespace" value="单实例"></property> </bean>
- 测试:
@Test public void testScope() { ApplicationContext app = new ClassPathXmlApplicationContext("/MyBean.xml"); //获取两次实例对象 Scope scope1 = app.getBean("scope", Scope.class); Scope scope2 = app.getBean("scope", Scope.class); System.out.println("scope1:" + scope1.getNamespace()); System.out.println("scope2:" + scope2.getNamespace()); System.out.println(scope1); System.out.println(scope2); }
- 创建一个类
-
那么如何设置为多实例呢?即如何设置单实例还是多实例
-
在Spring配置文件中的bean标签里面有属性(
scope
)专门用于设置单实例还是多实例,scope常用的取值有:singleton(默认值)
,表示单实例对象,不写即为singleton,常用prototype
,表示多实例对象,常用request
,将每次创建的实例对象放入request域对象中,不常用session
,将每次创建的实例对象放入session域对象中,不常用
-
在上述例子中,修改配置文件bean标签的scope属性为
prototype
<bean id="scope" class="com.soberw.spring5.scope.Scope" scope="prototype"> <property name="namespace" value="多实例"></property> </bean>
再次测试:
-
singleton
与prototype
的区别-
singleton 是单实例,prototype 是多实例
-
如果设置scope值为
singleton
,加载spring配置文件时就会创建好单实例对象如果设置scope值为
prototype
,则不是在加载spring配置文件时创建对象,而是在调用getBean()
方法时才创建多实例对象
-
bean生命周期
生命周期,就是指对象从创建到销毁的过程。
bean的生命周期主要有五个过程:
- 通过构造器创建bean实例(无参构造)
- 为bean的属性设置值和对其他bean引用(调用set方法)
- 调用bean的初始化方法(需要进行配置初始化的方法)
- bean可以使用了(对象获取到了)
- 当容器关闭时,调用bean的销毁方法(需要进行配置销毁的方法)
演示生命周期:
- 创建
Orders
类
package com.soberw.spring5.bean;
/**
* @author soberw
* @Classname Orders
* @Description
* @Date 2022-02-11 11:04
*/
public class Orders {
private String oname;
public Orders() {
System.out.println("第一步 执行无参数构造器创建实例对象...");
}
public void setOname(String oname) {
this.oname = oname;
System.out.println("第二步 调用set方法设置属性值...");
}
//创建执行的初始化方法,方法名随意
//需要在bean标签中配置 init-method 属性
public void initMethod(){
System.out.println("第三步 执行初始化方法...");
}
//创建销毁的初始化方法,方法名随意
//需要在bean标签中配置 destroy-method 属性
public void destroyMethod(){
System.out.println("第五步 执行销毁方法...");
}
}
- 配置spring配置文件
<!--其中init-method指定的是初始化方法,destroy-method指定的是销毁方法 -->
<bean id="orders" class="com.soberw.spring5.bean.Orders" init-method="initMethod" destroy-method="destroyMethod">
<property name="oname" value="手机"></property>
</bean>
- 测试配置
@Test
public void testBean() {
// ApplicationContext app = new ClassPathXmlApplicationContext("/bean4.xml");
ClassPathXmlApplicationContext app = new ClassPathXmlApplicationContext("/bean4.xml");
Orders orders = app.getBean("orders", Orders.class);
System.out.println("第四步 获取创建的对象实例...");
System.out.println(orders);
//手动销毁bean实例
// ((ClassPathXmlApplicationContext) app).close();
app.close();
}
实际上上面的五步只是bean的基本过程,bean还有两个后置处理器
,因此完整的bean的生命周期有七步:
- 通过构造器创建bean实例(无参构造)
- 为bean的属性设置值和对其他bean引用(调用set方法)
- 把bean实例传递给bean后置处理器的方法POSTProcessBeforeInitialization
- 调用bean的初始化方法(需要进行配置初始化的方法)
- 把bean实例传递给bean后置处理器的方法POSTProcessAfterInitialization
- bean可以使用了(对象获取到了)
- 当容器关闭时,调用bean的销毁方法(需要进行配置销毁的方法)
下面基于上面实例进行改动:
- 创建类
MyBeanPost
,实现接口BeanPostProcessor
package com.soberw.spring5.bean;
import org.springframework.beans.BeansException;
import org.springframework.beans.factory.config.BeanPostProcessor;
/**
* @author soberw
* @Classname MyBeanPost
* @Description
* @Date 2022-02-11 11:43
*/
public class MyBeanPost implements BeanPostProcessor {
@Override
public Object postProcessBeforeInitialization(Object bean, String beanName) throws BeansException {
System.out.println("在初始化之前执行的方法...");
return BeanPostProcessor.super.postProcessBeforeInitialization(bean, beanName);
}
@Override
public Object postProcessAfterInitialization(Object bean, String beanName) throws BeansException {
System.out.println("在初始化之后执行的方法...");
return BeanPostProcessor.super.postProcessAfterInitialization(bean, beanName);
}
}
- 在spring配置文件中配置
<!--配置后置处理器-->
<!--加上后置处理器后,本配置文件中的所有bean实例都会添加上后置处理器-->
<bean id="myBeanPost" class="com.soberw.spring5.bean.MyBeanPost"></bean>
- 测试执行
xml自动装配
在上面的所有例子中,我们想要给一个属性注入值,都是手动的在bean标签内添加property标签并匹配属性赋值,对于这一过程,我们称之为手动装配
。
有手动装配,自然就有自动装配,什么是自动装配呢?
自动装配
是指根据指定的装配规则(属性名或者属性类型),spring自动将匹配的属性值进行注入。
但是实际开发中并不常用xml配置文件进行自动装配,而是采用注解的方式。
演示自动装配过程:
-
创建两个类
Emp
和Dept
分别代表员工和部门,一个部门可以有多个员工,而一个员工只能有一个部门package com.soberw.spring5.autowire; /** * @author soberw * @Classname Emp * @Description * @Date 2022-02-11 14:55 */ public class Emp { private Dept dept; public void setDept(Dept dept) { this.dept = dept; } @Override public String toString() { return "Emp{" + "dept=" + dept + '}'; } public Dept getDept() { return dept; } }
package com.soberw.spring5.autowire; /** * @author soberw * @Classname Dept * @Description * @Date 2022-02-11 14:55 */ public class Dept { @Override public String toString() { return "Dept{}"; } }
-
配置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" xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd"> <!--传统方式:手动装配--> <!--<bean id="emp" class="com.soberw.spring5.autowire.Emp">--> <!--<property name="dept" ref="dept"></property>--> <!--</bean>--> <!--实现自动装配 bean标签属性 autowire 可以实现自动装配 autowire 属性常用两个值: byName: 根据属性名称注入,确保注入值bean的id值一定要和类的属性名称一样 byType: 根据属性类型注入,一定要确保相同类型的bean标签不能定义多 --> <!--(1)根据属性名称自动注入--> <bean id="emp" class="com.soberw.spring5.autowire.Emp" autowire="byName"></bean> <!--(2)根据属性类型自动注入--> <!--<bean id="emp" class="com.soberw.spring5.autowire.Emp" autowire="byType"></bean>--> <bean id="dept" class="com.soberw.spring5.autowire.Dept"> </bean> </beans>
-
测试配置:
@Test public void testAutoWire(){ ApplicationContext app = new ClassPathXmlApplicationContext("/bean5.xml"); Emp emp = app.getBean("emp", Emp.class); System.out.println(emp); }
引入外部属性文件
在管理spring配置文件xml属性文件时,我们通常需要在配置文件中创建管理很多bean,他们也可能有很多属性。但是我们知道,xml文件中存在各种配置,这样我们在维护起来就比较麻烦,尤其是当我们想要修改某个属性时,就要进行繁琐的操作。
因此我们可以将一些常用的或者易变的属性值配置封装起来,放入一个独立的外部文件中(比如properties
属性文件)中去,然后通过引入的形式加载到xml中,方便我们的维护和管理。实际上,我们在编写HTML网页时就是这样做的。
哪些场景会用到呢?典型的一个场景就是我们在配置数据库连接的时候,可以将配置信息放在一个专门的properties属性文件中,通过外部引入的方式连接。
下面我将简单实现这一操作:
-
首先看一下在配置文件中直接配置数据库信息
-
配置德鲁伊连接池
-
通过maven引入德鲁伊连接池
<!-- https://mvnrepository.com/artifact/com.alibaba/druid --> <dependency> <groupId>com.alibaba</groupId> <artifactId>druid</artifactId> <version>1.2.8</version> </dependency>
- 配置spring配置文件
<!--直接配置连接池--> <bean id="dataSource" class="com.alibaba.druid.pool.DruidDataSource"> <property name="driverClassName" value="com.mysql.cj.jdbc.Driver"></property> <property name="url" > <value><![CDATA[jdbc:mysql://localhost:3306/shop_soberw?useUnicode=true&characterEncodeing=UTF-8&useSSL=false&serverTimezone=GMT]]></value> </property> <property name="username" value="shop"></property> <property name="password" value="shop123456"></property> </bean>
- 测试是否连接成功:
@Test public void jdbcTest(){ ApplicationContext app = new ClassPathXmlApplicationContext("/jdbcconnection.xml"); DruidDataSource dataSource = app.getBean("dataSource", DruidDataSource.class); System.out.println(dataSource); }
-
-
引入外部属性文件配置数据库连接池
- 创建外部属性文件,properties格式文件,编写数据库连接信息
prop.driverClassName=com.mysql.cj.jdbc.Driver prop.url=jdbc:mysql://localhost:3306/shop_soberw?useUnicode=true&characterEncodeing=UTF-8&useSSL=false&serverTimezone=GMT prop.username=shop prop.password=shop123456
- 把外部properties属性文件引入到spring配置文件中去
- 在引入之前,要先引入
context
名称空间
- 在spring配置文件中使用context标签引入外部属性文件
<?xml version="1.0" encoding="UTF-8"?> <beans xmlns="http://www.springframework.org/schema/beans" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:context="http://www.springframework.org/schema/context" xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context.xsd"> <!--直接配置连接池--> <!--<bean id="dataSource" class="com.alibaba.druid.pool.DruidDataSource"> <property name="driverClassName" value="com.mysql.cj.jdbc.Driver"></property> <property name="url" > <value><![CDATA[jdbc:mysql://localhost:3306/shop_soberw?useUnicode=true&characterEncodeing=UTF-8&useSSL=false&serverTimezone=GMT]]></value> </property> <property name="username" value="shop"></property> <property name="password" value="shop123456"></property> </bean>--> <!--引入外部属性文件--> <context:property-placeholder location="classpath:jdbc.properties"/> <!--配置连接池--> <bean id="dataSource" class="com.alibaba.druid.pool.DruidDataSource"> <property name="driverClassName" value="${prop.driverClassName}"></property> <property name="url" value="${prop.url}"></property> <property name="username" value="${prop.username}"></property> <property name="password" value="${prop.password}"></property> </bean> </beans>
-
测试:
-
连接成功!这样外部引入的方式可以让我们更好的维护配置文件,避免乱作一团。
IOC操作Bean管理(基于注解)
前面我们说到了IOC操作Bean有两种方式:基于xml和基于注解。
下面开始介绍如何基于注解实现对Bean的管理。
首先,简单回顾一下什么是注解:
-
- 注解是代码的特殊标记,格式:@注解名称(属性名=属性值,属性名=属性值…)
-
- 注解的作用范围:类,方法,属性,构造方法等…
-
- 使用注解的目的是:简化xml配置,使代码更加优雅,更加简洁的呈现
那么Spring针对Bean管理中创建对象提供的注解有:
-
@Component
- 一种较为普通的注解,都可以用它创建对象
-
@Service
- 一般使用在业务逻辑层
-
@Controller
- 一般使用在web层上
-
@Repository
- 一般使用在dao层上
注意:上面四个注解所能实现的功能是一样的,都可以用来创建bean实例对象,只不过我们习惯于进行区分
如果你把@Controller用在业务逻辑层上,也是可以的,只不过为了开发的方便,建议尽量区分使用
简单了解了注解的概念后,下面通过例子演示如何通过注解管理Bean。
基于注解实现创建对象
-
第一步:
引入maven依赖
<!-- https://mvnrepository.com/artifact/org.springframework/spring-aop --> <dependency> <groupId>org.springframework</groupId> <artifactId>spring-aop</artifactId> <version>5.3.15</version> </dependency>
-
第二步:
开启组件扫描
何谓组件扫描,其实通俗来说就是,告诉Spring容器,我想要在哪个包中加入注解,然后让spring去扫描那个包里面的类,并给加入了注解的类创建对象
- 如何开启呢?
在配置文件中引入名称空间
context
:<?xml version="1.0" encoding="UTF-8"?> <beans xmlns="http://www.springframework.org/schema/beans" xmlns:context="http://www.springframework.org/schema/context" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context.xsd"> <!--开启组件扫描: 1. 如果扫描多个包,多个包之间可以用逗号隔开 2. 如果多个包都有一个公共的父目录,直接写上层父级目录即可 --> <!--<context:component-scan base-package="com.soberw.spring.service,com.soberw.spring.bean,com.soberw.spring.dao"/>--> <context:component-scan base-package="com.soberw.spring"/> </beans>
-
第三步:创建类,在类上面添加创建对象的注解,这里以创建
UserService
类为例:package com.soberw.spring.service; import org.springframework.stereotype.Component; import org.springframework.stereotype.Controller; import org.springframework.stereotype.Repository; import org.springframework.stereotype.Service; /** * @author soberw * @Classname UserService * @Description * @Date 2022-02-11 18:29 * * 在注解里面 value 属性值可以省略不写 * 如果不写,默认值为类名称的首字母小写 UserService --> UserService * 四个注解加哪一个最终创建的对象都是一样的,但因为目前是service层,推荐添加 @Service 注解 */ //@Component(value = "userService") //@Component //与上一个效果一致 //@Controller //@Repository //推荐使用 @Service public class UserService { public void show() { System.out.println("创建对象成功!"); } }
-
第四步:测试:
@Test public void testUserService(){ ApplicationContext app = new ClassPathXmlApplicationContext("/bean.xml"); //注意,这里的值匹配的是注解的value属性值,如果给value赋值了,就写具体的值,如果没有赋值就写类名称且首字母小写 UserService userService = app.getBean("userService", UserService.class); System.out.println(userService); userService.show(); }
上面几歩已经实现了通过注解创建类对象实例,但是对于组件扫描部分,我们还可以进一步优化。
思考一下:如果我们按照上面的配置方式,在指定好扫描包路径后,其实默认是将包下的所有类都扫描了一遍,如果包内所包含的类特别多且关系复杂,而我只想要扫描我指定的某一部分类,即给扫描加上一个过滤条件,如何实现呢?
开启组件扫描细节配置(加入过滤条件)
过滤扫描有很多种方式,这里我主要分享两种,也是比较常用的两种:
-
设置扫描哪些内容
<!--设置哪些内容需要扫描 use-default-filters 属性支持传入两个值: true: 默认值,不写也为true,默认全部扫描 false:表示现在不使用默认filter(全部扫描),而是自己配置filter context:include-filter 设置扫描哪些内容 --> <context:component-scan base-package="com.soberw.spring" use-default-filters="false"> <!--type是扫描的类型,expression是此类型对应的import导入路径--> <context:include-filter type="annotation" expression="org.springframework.stereotype.Controller"/> </context:component-scan>
-
设置不扫描哪些内容
<!--设置哪些内容不需要扫描 context:exclude-filter 设置不扫描哪些内容 --> <context:component-scan base-package="com.soberw.spring" use-default-filters="false"> <context:exclude-filter type="annotation" expression="org.springframework.stereotype.Controller"/> </context:component-scan>
有了过滤条件,spring在进行组件扫描时就会更加高效。
基于注解实现属性注入
基于注解实现属性的注入,其中可供我们使用的注解主要有以下四种:
-
@Autowired
:根据属性类型进行自动装配
-
@Qualifier
:根据名称进行注入(需要搭配@Autowired一起使用)
-
@Resource
:可以根据类型注入,也可以根据名称注入
-
@Value
:上面三种都是用于注入对象类型的属性,此注解则用于注入普通类型属性
@Autowired
注解
-
创建service和dao对象,在service和dao类中添加创建对象注解
-
在service注入dao对象,在service类添加dao类型属性,在属性上面使用注解
UserService
类
package com.soberw.spring.service; import com.soberw.spring.dao.UserDao; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; /** * @author soberw * @Classname UserService * @Description * @Date 2022-02-11 18:29 */ @Service public class UserService { //定义dao类型属性 //不需要添加set方法 //添加注入属性注解 @Autowired private UserDao userDao; public void show() { System.out.println("service show..."); userDao.show(); } }
UserDao
接口以及UserDaoImpl
实现类
package com.soberw.spring.dao; /** * @author soberw * @Classname UserDao * @Description * @Date 2022-02-11 20:01 */ public interface UserDao { void show(); }
package com.soberw.spring.dao; import org.springframework.stereotype.Controller; import org.springframework.stereotype.Repository; /** * @author soberw * @Classname UserDaoImpl * @Description * @Date 2022-02-11 20:02 */ @Repository public class UserDaoImpl implements UserDao{ @Override public void show() { System.out.println("dao show..."); } }
- 测试:
@Qualifier
注解
根据名称进行注入,必须要搭配@Autowired
进行使用
因为我们声明的属性是接口类型,因此在加了@Autowired注解后,就会自动去找他的实现类进行创建
但如果我们定义的接口有多个实现类,此时就需要具体的指定名称,按照我们指定的名称去寻找对应的类去创建
这就用到了@Qualifier这个注解
-
新建一个类
MyUserDaoImpl
实现UserDao
接口package com.soberw.spring.dao; import org.springframework.stereotype.Repository; /** * @author soberw * @Classname MyUserDaoImpl * @Description * @Date 2022-02-11 20:24 */ @Repository public class MyUserDaoImpl implements UserDao{ @Override public void show() { System.out.println("myuserdao show..."); } }
-
此时如果不给创建类型指定具体名称,运行会出错:
-
给dao类型属性通过
@Qualifier
注解指定具体的名称: -
再次执行:
@Resource
注解
可以根据类型注入,也可以根据名称注入,实际上可以通过这一个注解完成上面两个注解所完成的
-
修改dao类型属性为使用
@Resource
注解 -
测试:
需要注意的是,@Resource并不是Spring官方给我们提供的
他是位于
import javax.annotation.Resource
的类因此虽然使用此注解也能实现属性注入,但还是建议使用上面两种注解方式
@Value
注解
对于一些普通的属性,我们可以使用此注解方式,比如String类型,int类型等…
@Value注解可以加在属性定义上,也可以加在属性对应的set方法上
-
加在属性上(如果加在属性上,就不必再创建属性的set方法了,其实spring内部自动帮我们做了):
-
加在set方法上(我们当然可以手动创建set方法)
-
如果都加上呢?
注意:我们应该尽量避免直接在属性上面属性注入,因为那破坏了面向对象的封装性
基于注解设置bean作用域
前面我们通过在xml配置文件中配置bean标签的scope属性可以设置我们创建的实例对象是单实例的还是多实例的。
那么基于注解,我们也可以实现这一操作,这里使用的是@Scope
注解:
以上面的代码为例:
-
在使用@Scope注解之前:
@Test public void testScope(){ ApplicationContext app = new ClassPathXmlApplicationContext("/bean.xml"); //注意,这里的值匹配的是注解的value属性值,如果给value赋值了,就写具体的值,如果没有赋值就写类名称且首字母小写 UserService userService1 = app.getBean("userService", UserService.class); UserService userService2 = app.getBean("userService", UserService.class); System.out.println(userService1); System.out.println(userService2); }
-
在使用@Scope注解之后:
- 在类上加入注解:
-
测试:
全注解开发
既然我们常用的一些配置都可以通过注解来实现,那么我们能不能完全使用注解开发,以代替xml配置文件呢?
-
首先我们需要声明一个
配置类
,来代替xml配置文件 -
创建类
SpringConfig
,作为配置类package com.soberw.spring.config; import org.springframework.context.annotation.ComponentScan; import org.springframework.context.annotation.Configuration; /** * @author soberw * @Classname SpringConfig * @Description * @Date 2022-02-11 22:14 */ @Configuration //作为配置类,替代xml配置文件 @ComponentScan(basePackages = {"com.soberw.spring"}) public class SpringConfig { }
-
测试
-
同样的,使用xml配置文件能搞定的,使用注解一样能做到,比如
扫描过滤
,我们要做的很简单,只需要给相应的注解添加属性值即可:
可以说是非常全面了。
基于注解的bean生命周期
前文说到,bean对象的生命周期有7个过程:
其中有一些过程是需要我们手动去配置的,比如bean后置处理器
,在不借助xml配置文件的情况下,如何通过注解实现呢?
因为我们这里使用的是注解的方式,所以在模拟实现之前,我们要首先要知道都需要用到什么
-
首先既然是全注解方式,首先需要创建一个
配置类
,这里我还用上面的SpringConfig
来实现 -
在xml配置时,我们配置初始化和销毁方法时,需要添加
init-method
,init-destory
属性,这里就需要两个注解:@PostConstruct 和 @PreDestroy 分别对应着初始化和销毁,在对应方法上添加此注解即可
-
新建一个service类:
MyService
package com.soberw.spring.service; import org.springframework.beans.factory.annotation.Value; import org.springframework.context.annotation.Bean; import org.springframework.stereotype.Service; import javax.annotation.PostConstruct; import javax.annotation.PreDestroy; /** * @author soberw * @Classname MyService * @Description * @Date 2022-02-11 21:39 */ @Service public class MyService { private String name; public MyService() { System.out.println("第一步 执行无参数构造器创建实例对象..."); } @Value(value = "soberw") public void setName(String name) { this.name = name; System.out.println("第二步 调用set方法设置属性值..."); } @PostConstruct public void initMethod(){ System.out.println("第四步 执行初始化方法..."); } @PreDestroy public void destroyMethod(){ System.out.println("第七步 执行销毁方法..."); } }
-
接下来就是,两个
后置处理器
方法的创建,这里我依旧是选择实现BeanPostProcessor
接口:package com.soberw.spring.bean; import org.springframework.beans.BeansException; import org.springframework.beans.factory.config.BeanPostProcessor; import org.springframework.stereotype.Component; /** * @author soberw * @Classname MyBeanPost * @Description * @Date 2022-02-11 21:43 */ //加上必要的注解 @Component public class MyBeanPost implements BeanPostProcessor { @Override public Object postProcessBeforeInitialization(Object bean, String beanName) throws BeansException { if ("myService".equals(beanName)) { System.out.println("第三步 在初始化之前执行的方法..."); } return BeanPostProcessor.super.postProcessBeforeInitialization(bean, beanName); } @Override public Object postProcessAfterInitialization(Object bean, String beanName) throws BeansException { if ("myService".equals(beanName)) { System.out.println("第五步 在初始化之后执行的方法..."); } return BeanPostProcessor.super.postProcessAfterInitialization(bean, beanName); } }
-
测试:
@Test public void testBean() { // ApplicationContext context = new AnnotationConfigApplicationContext(SpringConfig.class); AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(SpringConfig.class); MyService myService = context.getBean("myService", MyService.class); System.out.println("第六步 获取创建的对象实例..."); System.out.println(myService); //手动销毁bean实例 // ((AnnotationConfigApplicationContext) context).close(); context.close(); }
注意
注意:创建后置处理方法时,在方法返回之前,我加入了判断,如果不加判断直接输出,那么程序运行时,当spring扫描包后(因为我配置类配置的是默认扫描,没加过滤),所有被扫描过的类都会被绑定上这两句话,然后输出一遍,也就是这样的…
会发现后置处理方法执行了好几遍,这是因为,后置处理方法是默认执行的方法,就是你不显式写出来,他也是存在的,因此当你不加任何判断的输出之后,他就会给你每个类都绑定上。
而又因为我目前声明的所有类都是
单实例(singleton)
的,单实例的特点就是配置时就会创建好实例等你来调用,而我们的后置方法就是在这时候执行的,所以也就出现了这种情况。
解决起来也很简单,要么就像我上面那样,加上判断;要么就给配置类加上扫描过滤
注解与xml的对比
注解优点在于方便、直观、高效(代码少,没有配置文件的书写那么复杂)。
但是也存在一定的弊端:以硬编码的方式写入到 Java 代码中,修改是需要重新编译代码的。
XML 方式优点在于配置和代码是分离的,在 xml 中做修改,无需编译代码,只需重启服务器即可将新的配置加载。
其缺点是编写麻烦,效率低,大型项目过于复杂。
另外,xml方式可以通过bean标签创建一个类的多个对象,但注解方式并不能,因为一个类不能存在两个同名的注解。
3.AOP
AOP Aspect Oriented Programming的缩写,意为:面向切面编程
,通过预编译方式和运行期间动态代理实现程序功能的统一维护的一种技术。AOP是OOP的延续,是软件开发中的一个热点,也是Spring框架中的一个重要内容,是函数式编程的一种衍生范型。利用AOP可以对业务逻辑的各个部分进行隔离,从而使得业务逻辑各部分之间的耦合度降低,提高程序的可重用性,同时提高了开发的效率。
如何理解切面编程?就是以切面为核心设计开发应用,具体就是
- 设计项目时,找出切面的功能
- 安排切面的执行时间,执行的位置
AOP
有何作用
- 让切面功能复用
- 让开发人员专注业务逻辑,提高开发效率
- 实现业务逻辑和其它非业务功能的解耦合
- 给存在的业务方法添加新功能,但不修改原来的代码
通俗来说:就是不通过修改源代码的方式,在主干功能上添加新功能
例如下面的登录功能例子:
AOP底层原理
AOP
底层实际上主要使用的就是我们设计模式中的代理模式(动态代理),关于动态代理,详情请移步java代理模式详解
这里在简单介绍一下:
我们目前有两种方式实现动态代理:
-
有接口情况,使用
JDK
动态代理-
创建接口实现类代理对象,增强类的方法
-
-
没有接口情况,使用
CGLIB
无侵入动态代理-
创建子类的代理对象,增强类的方法
-
这里我简单演示一下JDK
动态代理的情况:
-
使用JDK动态代理,主要就是使用
Proxy
类里面的方法去创建代理对象调用的是类中的
newProxyInstance()
方法:- 第一个参数:类加载器
- 第二个参数:增强方法所在的类,这个类实现的接口,支持多个接口(因为是数组形式)
- 第三个参数:实现这个接口
InvocationHandler
,创建代理对象,写增强的方法
-
创建一个接口
UserDao
,定义方法package com.soberw.spring.dao; /** * @author soberw * @Classname UserDao * @Description * @Date 2022-02-14 9:45 */ public interface UserDao { int add(int i,int j); String update(String id); }
-
创建实现类
UserDaoImpl
,实现方法:package com.soberw.spring.dao; /** * @author soberw * @Classname UserDaoImpl * @Description * @Date 2022-02-14 9:47 */ public class UserDaoImpl implements UserDao{ @Override public int add(int i, int j) { return i + j; } @Override public String update(String id) { return id; } }
-
使用Proxy创建接口的代理对象:
package com.soberw.spring.proxy; import com.soberw.spring.dao.UserDao; import com.soberw.spring.dao.UserDaoImpl; import java.lang.reflect.InvocationHandler; import java.lang.reflect.Method; import java.lang.reflect.Proxy; import java.util.Arrays; /** * @author soberw * @Classname JDKProxy * @Description * @Date 2022-02-14 9:51 */ public class JDKProxy { public static void main(String[] args) { //此种写法为生成内部匿名类,一般不建议这样做 // Proxy.newProxyInstance(UserDao.class.getClassLoader(), UserDao.class, new InvocationHandler() { // @Override // public Object invoke(Object proxy, Method method, Object[] args) throws Throwable { // return null; // } // }); Class[] interfaces = {UserDao.class}; UserDao userDao = new UserDaoImpl(); UserDao dao = (UserDao) Proxy.newProxyInstance(UserDao.class.getClassLoader(), interfaces, new UserDaoProxy(userDao)); int add = dao.add(1, 2); System.out.println(add); System.out.println("================="); String s = dao.update("soberw"); System.out.println(s); } } //创建代理对象代码 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 { //方法之前 String methodName = method.getName(); System.out.println("调用的方法是" + methodName + ",传递的参数是" + Arrays.toString(args)); //被增强的方法执行 Object res = method.invoke(obj, args); //方法之后 System.out.println("调用的对象是" + obj); //将方法的返回值返回 return res; } }
-
运行:
AOP相关术语
比如我这里有一个User类,里面假如有几个方法:
Target目标对象
我们想给哪一个对象增强功能,那么这个对象就是目标对象。这里的User类对象就是目标对象。
JoinPoint连接点
目标对象中有哪些方法可以被增强,这些方法就被称为连接点。
Pointcut切入点
我们实际将要增强的方法,称为切入点,比如这里我就想增强insert()方法,那么这个方法就是切入点。
Advice通知(增强)
实际增强的逻辑部分称为通知,比如我想在insert()执行之后记录一下插入时间,那个这个记录插入时间的功能就称为通知。
通知又可分为五种:
-
Before前置通知
:在切入点执行之前执行的 -
AfterRetunring后置通知
:在切入点执行之前执行的 -
Around环绕通知
:在切入点执行之前和之后都执行的 -
AfterThrowing异常通知
:如果切入点产生了异常就执行异常通知 -
After最终通知
:切入点一旦发生异常,其后置通知就不会再执行了,而最终通知则确保无论是否异常都会执行,有点类似于异常捕获中的finally
Aspect切面
是一个动作,把通知
应用到切入点
的过程,比如我想在insert()执行之前加入一个权限判断,加入的过程就称之为切面
。
AOP中重要的三个要素: Aspect切面, Advice通知, Pointcut切入点. 这个概念的理解是: 在Advice的时间,在Pointcut的位置, 执行Aspect
AOP是一个动态的思想,而我们的Spring框架就很好的对AOP思想进行了实现。
但是Spring实现AOP操作也不是直接实现的,而是基于另一个框架AspectJ
,他并不是Spring的组成部分,而是一个独立的AOP框架,我们开发中一般将AspectJ
和 Spirng
框架一起使 用,进行 AOP操作。
其中AspectJ
实现AOP有两种方式:基于xml配置文件,基于注解(常用),下面分别进行讲解。
准备工作
在开始讲解之前,需要先掌握一个知识点:切入点表达式
-
作用是指明对哪个类里面的哪些方法进行增强,即指定具体的切入点
-
语法结构:
execution
([权限修饰符
] [返回类型
] [类全路径
] [方法名称
] ([参数列表
]))-
举例1:对com.soberw.dao.BookDao类里面的add增强:
execution(* com.soberw.dao.BookDao.add(...))
-
举例 2:对 com.atguigu.dao.BookDao 类里面的所有的方法进行增强
execution(* com.soberw.dao.BookDao.* (..))
-
举例 3:对 com.atguigu.dao包里面所有类,类里面所有方法进行增强
execution(* com.soberw.dao.*.* (..))
-
另外必须导入相关的jar包,这里放上maven依赖:
<dependencies>
<!-- https://mvnrepository.com/artifact/org.springframework/spring-expression -->
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-expression</artifactId>
<version>5.3.15</version>
</dependency>
<!-- https://mvnrepository.com/artifact/org.springframework/spring-context -->
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-context</artifactId>
<version>5.3.15</version>
</dependency>
<!-- https://mvnrepository.com/artifact/org.springframework/spring-core -->
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-core</artifactId>
<version>5.3.15</version>
</dependency>
<!-- https://mvnrepository.com/artifact/org.springframework/spring-aop -->
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-aop</artifactId>
<version>5.3.15</version>
</dependency>
<!-- https://mvnrepository.com/artifact/org.springframework/spring-beans -->
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-beans</artifactId>
<version>5.3.15</version>
</dependency>
<!-- https://mvnrepository.com/artifact/org.springframework/spring-aspects -->
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-aspects</artifactId>
<version>5.3.15</version>
</dependency>
<!-- https://mvnrepository.com/artifact/net.sourceforge.cglib/com.springsource.net.sf.cglib -->
<dependency>
<groupId>net.sourceforge.cglib</groupId>
<artifactId>com.springsource.net.sf.cglib</artifactId>
<version>2.2.0</version>
</dependency>
<!-- https://mvnrepository.com/artifact/org.aopalliance/com.springsource.org.aopalliance -->
<dependency>
<groupId>org.aopalliance</groupId>
<artifactId>com.springsource.org.aopalliance</artifactId>
<version>1.0.0</version>
</dependency>
<!-- https://mvnrepository.com/artifact/org.aspectj/com.springsource.org.aspectj.weaver -->
<dependency>
<groupId>org.aspectj</groupId>
<artifactId>com.springsource.org.aspectj.weaver</artifactId>
<version>1.6.8.RELEASE</version>
</dependency>
<!-- https://mvnrepository.com/artifact/commons-logging/commons-logging -->
<dependency>
<groupId>commons-logging</groupId>
<artifactId>commons-logging</artifactId>
<version>1.2</version>
</dependency>
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>4.13.2</version>
<scope>test</scope>
</dependency>
<!-- https://mvnrepository.com/artifact/com.alibaba/druid -->
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>druid</artifactId>
<version>1.2.8</version>
</dependency>
</dependencies>
AspectJ实现AOP操作(基于注解)
-
首先,创建一个类
User
作为目标对象,在类中定义方法作为切入点:package com.soberw.spring.aopanno; /** * @author soberw * @Classname User * @Description * @Date 2022-02-14 14:46 */ public class User { public String show(String s){ System.out.println("show()方法执行了"); return s; } }
-
创建增强类
UserProxy
,在增强类里面创建方法,让不同方法代表不同通知类型,这里分别对应五种通知类型创建五种方法,我先将方法定义出来,一会针对不同的通知类型具体详细实现说明:package com.soberw.spring.aopanno; /** * @author soberw * @Classname UserProxy * @Description * @Date 2022-02-14 14:54 */ public class UserProxy { public void before() {} public void afterReturning() {} public void around() {} public void afterThrow() {} public void after() {} }
-
至此已将雏形搭建起来了,下面具体展开:
-
使用注解创建
User
和UserProxy
对象: -
在增强类上添加注解
@Aspect
,用以生成代理对象
-
创建一个
SpringConfig
类作为配置类:package com.soberw.spring.aopanno; import org.springframework.context.annotation.ComponentScan; import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.EnableAspectJAutoProxy; import org.springframework.stereotype.Component; /** * @author soberw * @Classname SpringConfig * @Description * @Date 2022-02-14 15:03 */ @Configuration @ComponentScan(basePackages = {"com.soberw.spring.aopanno"}) @EnableAspectJAutoProxy(proxyTargetClass = true) public class SpringConfig { }
如果这里是使用配置文件的话,你需要在配置文件中这样写:
-
先引入两个命名空间:
aop
和context
<!--开启注解扫描--> <context:component-scan base-package="com.soberw.spring.aopanno"> </context:component-scan> <!--开启Aspect生成代理对象--> <aop:aspectj-autoproxy> </aop:aspectj-autoproxy>
前面两个我们知道,是将此类作为配置类的注解,最后一个注解
@EnableAspectJAutoProxy
内有参数属性proxyTargetClass
默认为false,这里需要手动设置为true他的作用是调用AspectJ框架中的功能,寻找spring容器中的所有目标对象,
把每个目标对象加入切面类中的功能,生成代理
这个代理对象是修改的内存中的目标对象,这个目标对象就是代理对象(UserProxy)
-
-
接下来就可以在增强类中编写我们的代码了,需要在通知方法上添加通知类注解,并使用
切入点表达式
配置:@Before
前置通知@AfterReturning
后置通知(返回通知)@After
最终通知@AfterThrowing
异常通知@Around
环绕通知
下面依次举例展开详细说明。
@Before
前置通知
-
修改方法
before()
如下:/** * @Before: 前置通知 * 属性: value 切入点表达式 * 特点: * 1)执行时间:在目标方法之前先执行 * 2)不会影响目标方法的执行 * 3)不会修改目标方法的执行结果 * 4)方法必须定义为 public void * <p> * 可以传参数,也可以不传, * 但是:如果传入参数,则必须是 JoinPoint 类型的参数 * <p> * JoinPoint:表示正在执行的业务方法。相当于反射中的 Method * 使用要求:必须是参数列表的第一个 * 作用:获取方法执行时的 信息,例如方法名称,方法的参数集合等等... * * @description: 前置通知 * @return: void * @author: soberw * @time: 2022/2/14 14:58 */ @Before(value = "execution(* com.soberw.spring.aopanno.User.show(..))") public void before(JoinPoint jp) { //获取方法的定义 System.out.println("前置通知中,获取方法的定义:" + jp.getSignature()); System.out.println("前置通知中,获取方法的名称:" + jp.getSignature().getName()); //获取方法的执行时参数,返回一个数组 Object[] args = jp.getArgs(); System.out.println("前置通知中,获取方法的所有参数:" + Arrays.toString(args)); }
@AfterReturning
后置通知(返回通知)
-
修改方法
afterRuturning()
如下:/** * @AfterReturning: 后置通知 * 属性:value 切入点表达式 * returning 自定义变量,表示目标方法的返回值(可以不写此属性,不写按默认) * 注意:自定义变量名必须和通知方法的形参名一致 * 特点: * 1)在目标方法之后执行 * 2)能获取到目标方法的执行结果包括返回值 * 3)不会影响目标方法的执行 * 4)方法必须定义为 public void * * 方法的参数:可以不传,但如果传入的话,必须是: * JoinPoint jp:如果加,必须加在第一个位置 * Object(推荐使用Object):表示目标方法的返回值,使用此表示目标方法的返回结果 * 注意:参数名必须和returning属性值一致 * * @description: 后置通知(返回通知) * @return: void * @author: soberw * @time: 2022/2/14 14:58 */ @AfterReturning(value = "execution(* com.soberw.spring.aopanno.User.show(..))",returning = "res") public void afterReturning(Object res) { System.out.println("后置通知中,获取方法的执行结果:" + res); //修改目标方法的返回结果 res = "hello world..."; System.out.println("后置通知中,修改目标方法的返回结果:" + res); }
-
在后置通知中,我将目标方法的返回值进行了修改,但是结果发现目标方法的执行结果并没有变化
这说明在在后置通知中,修改返回值, 是不会影响目标方法的最后调用结果的。
可真的是这样吗?
要知道我这里调用的是一些基本的数据类型,如果这里换成
引用(对象)类型
,我在后置通知方法中,修改这个对象的属性值,会是什么样呢? -
新建一个类
Student
,声明最基本的属性和方法:package com.soberw.spring.aopanno; /** * @author soberw * @Classname Student * @Description * @Date 2022-02-14 19:12 */ public class Student { private String name; private int age; public Student(String name, int age) { this.name = name; this.age = age; } public String getName() { return name; } public void setName(String name) { this.name = name; } public int getAge() { return age; } public void setAge(int age) { this.age = age; } public void info() { System.out.printf("我叫%s,今年%d岁了\n", name, age); } }
-
在
User
类中添加一个方法showInfo()
:public Student showInfo(Student stu) { System.out.println("showInfo()方法执行了"); return stu; }
-
在增强类中添加方法
stuAfter()
作为此方法的后置通知:@AfterReturning(value = "execution(* com.soberw.spring.aopanno.User.showInfo(..))", returning = "res") public void stuAfter(Object res) { //获取方法返回值并转换为Student类型 Student stu = null; if (res != null && res instanceof Student) { stu = (Student) res; } System.out.print("后置通知:修改方法返回值之前:"); stu.info(); //修改属性值 stu.setAge(stu.getAge() + 100); System.out.print("后置通知:修改方法返回值之后:"); }
-
此时发现返回值已经被修改了。
总结:
如果目标方法返回是String ,Integer ,Long等基本类型,
在后置通知中,修改返回值, 是不会影响目标方法的最后调用结果的。如果目标方法返回的结果是对象类型,例如Student。
在后置通知方法中,修改这个Student对象的属性值,会影响最后调用结果。
@Around
环绕通知
-
修改方法
around()
如下:/** * @Around: 环绕通知 * 属性:value 切入点表达式 * 特点: * 1)在目标方法的前后都能增强功能 * 2)可以控制目标方法是否执行 * 3)可以修改目标方法的执行结果 * 4)方法必须是 public * 5)此方法必须有返回值,一般推荐使用 Object 类型 * 6)方法必须有 ProceedingJoinPoint 参数 * <p> * 参数说明: ProceedingJoinPoint, 是JoinPoint的子类,相当于反射中 Method。 * 作用:执行目标方法的,等于Method.invoke() * @description: 环绕通知 * @return: void * @author: soberw * @time: 2022/2/14 14:58 */ @Around(value = "execution(* com.soberw.spring.aopanno.User.show(..))") public Object around(ProceedingJoinPoint pjp) throws Throwable { //获取方法执行时的参数值 Object[] args = pjp.getArgs(); System.out.println("环绕通知中,获取到的目标方法参数值为" + Arrays.toString(args)); //执行目标方法并接收返回值、 Object res = pjp.proceed(); //方法真正的返回值 System.out.println("环绕通知中,获取到的目标方法的执行结果为" + res); //修改返回值 res = "goodbye world ..."; System.out.println(); System.out.println("环绕通知中,修改目标方法的返回结果:" + res); return res; }
注意:环绕通知是可以修改目标方法的返回结果的。
@AfterThrowing
异常通知
-
修改方法
afterThrow()
如下:/** * @AfterThrowing: 异常通知 * 属性:value 切入点表达式 * throwing 自定义变量,表示目标方法可能抛出的异常(可以不写此属性,不写按默认) * 注意:变量名必须和通知方法的形参名一样 * 特点: * 1)在目标方法抛出异常时执行,没有异常正常运行时不会执行 * 2)能获取到的目标方法的异常信息 * 3)不是异常处理程序。但可以得到发生异常的通知,例如可以发送邮件,短信通知开发人员 * 实际上类似于目标方法的监控程序 * 4)方法必须定义为 public void * <p> * 方法的参数:可以不传,但如果传入的话,必须是: * Exception :表示目标方法的错误类型 * 注意:参数名必须和 throwing属性值一致 * @description: 异常通知 * @return: void * @author: soberw * @time: 2022/2/14 14:58 */ @AfterThrowing(value = "execution(* com.soberw.spring.aopanno.User.show(..))", throwing = "e") public void afterThrow(Exception e) { System.out.println("方法出错了,错误的原因是:" + e.getMessage()); /* 异常发生可以做: 1.记录异常的时间,位置,等信息。 2.发送邮件,短信,通知开发人员 */ }
-
因为此通知如果目标方法没有异常时不会执行的,因此我就修改一下目标方法,使其产生异常:
@After
最终通知
-
修改方法
after()
如下:/** * @After: 最终通知 * 属性:value 切入点表达式 * <p> * 特点: * 1)在目标方法之后执行的 * 2)总是会被执行 * 3)可以用来做程序最后的收尾工作。例如清除临时数据,变量。 清理内存 * 4)方法必须定义为 public void * <p> * 方法的参数:无 * @description: 最终通知 * @return: void * @author: soberw * @time: 2022/2/14 14:58 */ @After(value = "execution(* com.soberw.spring.aopanno.User.show(..))") public void after() { System.out.println("最终都会执行的代码。"); }
可以理解为java异常捕获中的
finally
语句,一定会被执行。
通知类注解总结
上面我都是对通知逐一测试的,如果我现在都打开,其执行流程回是什么样呢?会产生怎样的结果?
经测试如下:
而当程序出现错误:
基于此总结出:
- 如果目标方法运行正常,则所有通知都会如期执行,只有异常通知不会执行
- 如果目标方法执行异常:
- 前置通知不受影响
- 后置通知不执行
- 环绕通知中目标方法执行前的操作正常执行,之后的操作则不执行
- 异常通知会执行
- 最终通知会执行
@Pointcut抽取相同切入点
在上面的例子中,我们发现,在添加通知注解时,所传入的切入点表达式都是一样的,因此就会想,有没有什么方法可以优化一下呢?
这里就引入一个注解:@Pointcut,可以帮我们定义和管理切入点
具体实现:
-
在增强类中添加一个任意方法,尽量为空方法体,这里我命名为
mypt()
@Pointcut(value = "execution(* com.soberw.spring.aopanno.User.show(..))") public void mypt() { //无需代码 }
-
这就可以了,使用时只需要将此方法名作为通知类注解的属性值就行了
-
以前置通知为例:
可以发现正常执行。
增强类优先级
有时候我们可能不只有一个增强类,那当我们定义了多个增强类时,如何确保程序能按照想要的顺序执行呢?
我们需要使用到一个注解:@Order(数字类型值),通过里面的数值来决定优先级,且数值越小,优先级越高
-
新建一个增强类
PersonProxy
,这里以前置通知为例,在类中也为目标方法添加前置通知并在两个增强类中设置不同优先级package com.soberw.spring.aopanno; import org.aspectj.lang.JoinPoint; import org.aspectj.lang.annotation.Aspect; import org.aspectj.lang.annotation.Before; import org.springframework.core.annotation.Order; import org.springframework.stereotype.Component; /** * @author soberw * @Classname PersonProxy * @Description * @Date 2022-02-14 20:38 */ @Component @Aspect @Order(1) public class PersonProxy { @Before(value = "execution(* com.soberw.spring.aopanno.User.show(..))") public void before(JoinPoint jp) { System.out.println("此方法来自PersonProxy增强类,实现" + jp.getSignature().getName() + "方法的前置通知"); } }
-
给增强类
UserProxy
设置优先级为2:
AspectJ实现AOP操作(基于xml配置文件)
除了基于注解完成AOP的相关操作,我么还可以基于xml配置问价来完成相关操作。
但是相比于注解方式比较麻烦,因此实际应用中并不常用,作为了解即可。
-
创建一个类
Book
作为目标对象类package com.soberw.spring.aopxml; /** * @author soberw * @Classname Book * @Description * @Date 2022-02-14 20:51 */ public class Book { public void buy(){ System.out.println("buy.............."); } }
-
创建一个类
BookProxy
作为增强类,创建一个方法作为通知方法,这里还以前置通知为例:package com.soberw.spring.aopxml; /** * @author soberw * @Classname BookProxy * @Description * @Date 2022-02-14 20:51 */ public class BookProxy { //作为目标方法的前置方法 public void before(){ System.out.println("before..........."); } }
-
在配置文件中配置,全部配置如下:
<?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.soberw.spring.aopxml.Book"></bean> <bean id="bookProxy" class="com.soberw.spring.aopxml.BookProxy"></bean> <!-- 配置aop增强 --> <aop:config> <!-- 配置切入点 --> <aop:pointcut id="p" expression="execution(* com.soberw.spring.aopxml.Book.buy(..))"/> <!-- 配置切面,即增强类对象 --> <aop:aspect ref="bookProxy"> <!--增强作用在具体那个方法上--> <aop:before method="before" pointcut-ref="p"/> </aop:aspect> </aop:config> </beans>
-
测试:
@Test public void testXML(){ ApplicationContext ap = new ClassPathXmlApplicationContext("/book.xml"); Book book = ap.getBean("book", Book.class); book.buy(); }
4.JdbcTemplate
概念引入
JDBC
已经能够满足大部分用户最基本的需求,但是在使用JDBC时,必须自己来管理数据库资源如:获取PreparedStatement,设置SQL语句参数,关闭连接等步骤。
JdbcTemplate
是Spring对JDBC的封装,目的是使JDBC更加易于使用。JdbcTemplate是Spring的一部分。JdbcTemplate处理了资源的建立和释放。他帮助我们避免一些常见的错误,比如忘了总要关闭连接。他运行核心的JDBC工作流,如Statement的建立和执行,而我们只需要提供SQL语句和提取结果。
在使用JdbcTemplate之前,必须要先引入几个jar包。
在上面引入的jar包的基础上,还需要引入mysql的连接包以及spring中相关的依赖。
对应的maven依赖如下:
<!-- https://mvnrepository.com/artifact/org.springframework/spring-jdbc -->
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-jdbc</artifactId>
<version>5.3.15</version>
</dependency>
<!-- https://mvnrepository.com/artifact/org.springframework/spring-orm -->
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-orm</artifactId>
<version>5.3.15</version>
</dependency>
<!-- https://mvnrepository.com/artifact/org.springframework/spring-tx -->
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-tx</artifactId>
<version>5.3.15</version>
</dependency>
<!-- https://mvnrepository.com/artifact/mysql/mysql-connector-java -->
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>8.0.21</version>
</dependency>
<!-- https://mvnrepository.com/artifact/com.alibaba/druid -->
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>druid</artifactId>
<version>1.2.8</version>
</dependency>
有了这些相应的依赖,我们就可以开始我们的工作了。
基础环境配置
既然是操作数据库,肯定首先要先连接到数据库,在IOC章节已经模拟演示过了。这里再操作一下,这次我选用外部属性文件导入的方式。
-
在开始之前,先创建一个数据库,这里我命名为
user_soberw
-
创建一个外部
properties
属性文件prop.driverClassName=com.mysql.cj.jdbc.Driver prop.url=jdbc:mysql://localhost:3306/localhost?useUnicode=true&characterEncodeing=UTF-8&useSSL=false&serverTimezone=GMT prop.username=root prop.password=123456
-
在spring配置文件中引入外部文件,并配置数据库连接池
<!--引入外部文件--> <context:property-placeholder location="classpath:MyJDBC.properties" /> <!--配置数据库连接池--> <bean id="dataSource" class="com.alibaba.druid.pool.DruidDataSource" destroy-method="close"> <property name="driverClassName" value="${prop.driverClassName}"/> <property name="url" value="${prop.url}"/> <property name="username" value="${prop.username}"/> <property name="password" value="${prop.password}"/> </bean>
-
配置
JdbcTemplate
对象,注入DataSource
:<!--创建JdbcTemplate对象--> <bean id="jdbcTemplate" class="org.springframework.jdbc.core.JdbcTemplate"> <!--set注入dataSource--> <property name="dataSource" ref="dataSource"></property> </bean> <!--组件扫描--> <context:component-scan base-package="com.soberw.spring"/>
-
创建dao类接口
BookDao
以及实现类BookDaoImpl
,在dao中注入jdbcTemplate
-
创建service类
BookService
,在service中注入BookDao
package com.soberw.spring.dao; import org.springframework.jdbc.core.JdbcTemplate; import org.springframework.stereotype.Repository; /** * @author soberw * @Classname BookDao * @Description * @Date 2022-02-15 10:20 */ public interface BookDao { }
package com.soberw.spring.dao; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.jdbc.core.JdbcTemplate; import org.springframework.stereotype.Repository; /** * @author soberw * @Classname BookDaoImpl * @Description * @Date 2022-02-15 10:20 */ @Repository public class BookDaoImpl implements BookDao { @Autowired private JdbcTemplate jdbcTemplate; }
package com.soberw.spring.service; import com.soberw.spring.dao.BookDao; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; /** * @author soberw * @Classname BookService * @Description * @Date 2022-02-15 10:21 */ @Service public class BookService { @Autowired private BookDao bookDao; }
-
在数据库中创建一个表
t_book
-
相应的,我们要在项目中创建一个对相应的bean实体类
Book
,对应表中的字段package com.soberw.spring.bean; /** * @author soberw * @Classname Book * @Description * @Date 2022-02-15 14:32 */ public class Book { private String bookId; private String bookname; private String bstatus; public String getBookId() { return bookId; } public void setBookId(String bookId) { this.bookId = bookId; } public String getBookname() { return bookname; } public void setBookname(String bookname) { this.bookname = bookname; } public String getBstatus() { return bstatus; } public void setBstatus(String bstatus) { this.bstatus = bstatus; } }
-
前期准备就绪,接下来就可以进行数据库的操作了。
JdbcTemplate操作数据库
操作数据库无外乎就是对数据的增删改查操作,接下来逐一进行演示。
添加修改和删除
在开始之前,先引入一个函数JdbcTemplate.update()
,对数据库的增删改操作都是使用这个函数
public int update(String sql, Object… args)
- 参数一:传入预编译SQL语句,参数用占位符?代替
- 参数二:可变参数,设置SQL语句的参数
-
在
BookService
类中添加一个方法addBook()
,用来完成添加操作。 -
对应的,在
BookDao
中声明add()
方法并在实现类中实现:@Override public void add(Book book) { //创建一个SQL语句 String sql = "insert into t_book values(?,?,?)"; //参数 Object[] args = {book.getBookId(), book.getBookname(), book.getBstatus()}; //调用方法实现 int update = jdbcTemplate.update(sql, args); System.out.println(update); }
-
进行测试:
@Test public void testAdd(){ ApplicationContext app = new ClassPathXmlApplicationContext("ApplicationContext.xml"); BookService bookService = app.getBean("bookService", BookService.class); Book book = new Book(); book.setBookId("1"); book.setBookname("java"); book.setBstatus("soberw"); bookService.addBook(book); }
-
添加成功。
-
在
BookService
类中添加两个方法updateBook()
和delete()
,用来完成修改删除操作。//修改的方法 public void updateBook(Book book){ bookDao.updateBook(book); } //删除的方法 public void delete(String id){ bookDao.delete(id); }
-
对应的,在
BookDao
中声明方法并在实现类中实现:@Override public void updateBook(Book book) { String sql = "update t_book set bookname=?,bstatus=? where book_id=?"; Object[] args = {book.getBookname(), book.getBstatus(), book.getBookId()}; int update = jdbcTemplate.update(sql, args); System.out.println(update); } @Override public void delete(String id) { String sql = "delete from t_book where book_id=?"; int update = jdbcTemplate.update(sql,id); System.out.println(update); }
-
测试:
-
修改上面添加的那条数据:
//修改 Book book = new Book(); book.setBookId("1"); book.setBookname("java入门到精通"); book.setBstatus("soberw"); bookService.updateBook(book);
-
删除数据:
//删除 bookService.delete("1");
-
查询操作
一般我们开发中使用量最大的就是查询操作了,也是比较复杂的,我们需要对查询的返回值进行处理。而根据对查询返回集的不同,我这里将查询分为一下三部分。
在开始之前,先在表中添加若干数据。
查询返回某个值
此类针对的是查询结果返回的是某一个值的情况,例如,查询员工的平均工资,查询某个部门一共多少人,查询单日客流量等…
返回的都只是一个单一值。这就用到了一个函数:JdbcTemplate.queryForObject()
T queryForObject(String sql, Class requiredType)
第一个参数:SQL语句
第二个参数:返回值类型Class
-
这里以查询book表总个数为例:
-
同样的添加方法:
//查询返回某个值 public int findCount(){ return bookDao.selectCount(); }
@Override public int selectCount() { String sql = "select count(*) from t_book"; return jdbcTemplate.queryForObject(sql, Integer.class); }
-
测试:
查询返回对象
比如当用户点击商品详情页面的时候,页面中就会显示商品的详细信息,而此信息一般都是对应的数据库的一张表,或者视图,在项目中也一般会对应一个bean类。
因此返回的是一个对象,所用到的函数是:JdbcTemplate.queryForObject()
public T queryForObject(String sql, RowMapper rowMapper, Object… args)
- 第一个参数:SQL语句
- 第二个参数:RowMapper是接口,针对返回不同类型数据,使用这个接口里面实现类完成数据封装
- 第三个参数:SQL语句参数值
-
这里以查询返回一个book对象为例:
-
添加查询方法
//查询返回对象 public Book findOne(String id){ return bookDao.findBookInfo(id); }
@Override public Book findBookInfo(String id) { String sql = "select book_id,bookname,bstatus from t_book where book_id=?"; Book book = jdbcTemplate.queryForObject(sql, new BeanPropertyRowMapper<Book>(Book.class), id); return book; }
-
以查询book_id为5的数据为例:
查询返回集合
那么实际开发中使用最多的还是这个操作了,因为上面的都是返回某一个值的情况,但是实际情况中我们往往需要返回的是一堆数据。
例如商品列表显示,分页等操作,返回的都是一个集合。
这里用到的方法是JdbcTemplate.query()
List query(String sql, RowMapper rowMapper, @Nullable Object… args)
参数与之前的一样
-
这里以查询表中所有的记录为例
-
添加查询方法
//查询返回集合 public List<Book> findAll(){ return bookDao.findAllBook(); }
@Override public List<Book> findAllBook() { String sql = "select * from t_book"; List<Book> bookList = jdbcTemplate.query(sql, new BeanPropertyRowMapper<Book>(Book.class)); return bookList; }
-
测试:
批量操作
单条记录的操作一般都过于繁琐,大多时候使用的是批量操作。
执行批量操作这里用到的方法是:JdbcTemplate.bacthUpdate()
public int[] batchUpdate(String sql, List<Object[]> batchArgs)
第一个参数:SQL语句
第二个参数:List集合,添加多条记录
-
批量添加:
//批量添加操作 public void batchAdd(List<Object[]> batchArgs){ bookDao.batchAdd(batchArgs); }
@Override public void batchAdd(List<Object[]> batchArgs) { String sql = "insert into t_book values (?,?,?)"; int[] ints = jdbcTemplate.batchUpdate(sql, batchArgs); System.out.println(Arrays.toString(ints)); }
-
测试:
//批量添加 List<Object[]> batchArgs = new ArrayList<>(); batchArgs.add(new Object[]{"8","spring","adgh"}); batchArgs.add(new Object[]{"9","springmvc","afgdf"}); batchArgs.add(new Object[]{"10","springboot","adagtrghgh"}); batchArgs.add(new Object[]{"11","springcloud","adggujrty"}); bookService.batchAdd(batchArgs);
-
-
批量修改
//批量修改操作 public void batchUpdate(List<Object[]> batchArgs){ bookDao.batchUpdate(batchArgs); }
@Override public void batchUpdate(List<Object[]> batchArgs) { String sql = "update t_book set bookname=?,bstatus=? where book_id=?"; int[] ints = jdbcTemplate.batchUpdate(sql, batchArgs); System.out.println(Arrays.toString(ints)); }
-
测试
//批量修改 List<Object[]> batchArgs = new ArrayList<>(); batchArgs.add(new Object[]{"spring","soberw","8"}); batchArgs.add(new Object[]{"springmvc","soberw","9"}); batchArgs.add(new Object[]{"springboot","soberw","10"}); batchArgs.add(new Object[]{"springcloud","soberw","11"}); bookService.batchUpdate(batchArgs);
-
-
批量删除
//批量删除操作 public void batchDelete(List<Object[]> batchArgs){ bookDao.batchDelete(batchArgs); }
@Override public void batchDelete(List<Object[]> batchArgs) { String sql = "delete from t_book where book_id=?"; int[] ints = jdbcTemplate.batchUpdate(sql, batchArgs); System.out.println(Arrays.toString(ints)); }
-
测试:
//批量删除 List<Object[]> batchArgs = new ArrayList<>(); batchArgs.add(new Object[]{"8"}); batchArgs.add(new Object[]{"9"}); batchArgs.add(new Object[]{"10"}); batchArgs.add(new Object[]{"11"}); bookService.batchDelete(batchArgs);
-
至此,使用JdbcTemplate操作数据库已经基本实现了,可以发现,相比于我们自己封装,确实方便了不少,而在类中提供的方法还远不止这些,针对不同场景,JdbcTemplate都给我们提供了相应的重载方法供我们使用,由于篇幅原因,这里不在演示。
5.事务管理
事务概念
事务是数据库操作最基本单元,逻辑上的一组操作,要么都成功,如果有一个失败所有操作都失败,用来确保数据的完整性和一致性。
了解数据库的都知道,事务有四个特性(ACID
):
-
原子性(Atomicity)
: 原子性是指事务是一个不可分割的工作单位,事务中的操作 要么都发生,要么都不发生。 -
一致性(Consistency)
:一致性是指事务必须使数据库从一个一致性状态变换到另一个一致性状态,也就是说一个事务执行之前和执行之后都必须处于一致性状态。 -
隔离性(Isolation)
: 隔离性是指当多个用户并发访问数据库时,比如操作同一张表时,数据库为每一个用户开启的事物,不能被其他事物的操作干扰,多个并发事物之间要互相隔离,对任何两个并发的事物T1和T2,T1看来,T2要么在T1之前就结束,要么在T1结束之后才开始,这样每个事物都感觉不到有其他事物在并发执行。 -
持久性(Durability)
:持久性是指一个事务一旦被提交了,那么对数据库中的数据的改变是永久性的,即便在数据库系统遇到故障的情况下也不会丢失提交事务的操作。
典型的场景就是银行转账操作,一个人给另一个人转账,中间出现任何变故(比如断网)转账都会失败。
引入案例
接下来就以转账操作为例,演示如果没有事务,会是怎么样。
-
业务分析
-
创建数据表
t_account
,并添加记录 -
其他配置以及相关的jar包的导入完全与JdbcTemplate操作一致。
-
创建dao类接口
UserDao
以及实现类UserDaoImpl
,在dao中注入jdbcTemplate
-
创建service类
UserService
,在service中注入UserDao
package com.soberw.spring.dao; /** * @author soberw * @Classname UserDao * @Description * @Date 2022-02-15 21:02 */ public interface UserDao { }
package com.soberw.spring.dao; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.jdbc.core.JdbcTemplate; import org.springframework.stereotype.Repository; /** * @author soberw * @Classname UserDaoImpl * @Description * @Date 2022-02-15 21:10 */ @Repository public class UserDaoImpl implements UserDao{ @Autowired private JdbcTemplate jdbcTemplate; }
package com.soberw.spring.service; import com.soberw.spring.dao.UserDao; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; /** * @author soberw * @Classname UserService * @Description * @Date 2022-02-15 21:09 */ @Service public class UserService { @Autowired private UserDao userDao; }
-
在dao创建两个方法:多钱的方法
addMoney()
以及少钱的方法reduceMoney()
,实现Lucy给Mary转账100元。@Override public void addMoney() { String sql = "update t_account set money=money-? where username=?"; jdbcTemplate.update(sql,100,"lucy"); } @Override public void reduceMoney() { String sql = "update t_account set money=money+? where username=?"; jdbcTemplate.update(sql,100,"mary"); }
-
在 service 创建方法(转账的方法)
//转账的方法 public void accountMoney(){ //Lucy少100 userDao.reduceMoney(); //Mary多100 userDao.addMoney(); }
-
测试:
@Test public void testAccount(){ ApplicationContext app = new ClassPathXmlApplicationContext("ApplicationContext.xml"); UserService userService = app.getBean("userService", UserService.class); userService.accountMoney(); }
思考一下:上面的案例代码,如果正常执行,那肯定是没有问题的,但是如果出现了异常,就会存在严重问题,比如在Lucy在少100后,,突然断网了,这时候会怎样呢?这里简单模拟一下。给程序手动加一个异常。
//转账的方法
public void accountMoney(){
//Lucy少100
userDao.reduceMoney();
//模拟异常
int i = 10/0;
//Mary多100
userDao.addMoney();
}
上面的问题应该如何解决呢?我们可以利用事务的特性,通过事务管理解决。
基本流程
try {
//第一步 开启事务
//第二步 进行业务操作
//Lucy少100
userDao.reduceMoney();
//模拟异常
int i = 10 / 0;
//Mary多100
userDao.addMoney();
//第三步 没有发生异常,提交事务
}catch (Exception e){
//第四步 出现异常,事务回滚
}
但是不同的数据库访问技术,处理操作事务是不同的,例如:
-
使用jdbc访问数据库, 事务处理。
public void updateAccount(){ Connection conn = ... conn.setAutoCommit(false); stat.insert() stat.update(); conn.commit(); con.setAutoCommit(true) }
-
mybatis执行数据库,处理事务
public void updateAccount(){ SqlSession session = SqlSession.openSession(false); try{ session.insert("insert into student..."); session.update("update school ..."); session.commit(); }catch(Exception e){ session.rollback(); } }
spring事务管理
而在spring里面,事务的操作就更简单了,使用spring的事务管理器,管理不同数据库访问技术的事务处理。 开发人员只需要掌握spring的事务处理一个方案, 就可以实现使用不同数据库访问技术的事务管理。管理事务面向的是spring, 有spring管理事务,做事务提交,事务回顾。
事务管理介绍
-
我们一般推荐将事务添加到JavaEE三层结构上面的service层(业务逻辑层)上
-
使用spring进行事务管理操作,一般有两种方式:
-
编程式事务管理
-
使用硬编码的方式,将代码嵌入到我们的方法逻辑中,比如上面的流程。
-
实际开发中我们一般不用,因为难以维护,并且臃肿。
-
-
声明式事务管理
【常用】- 在spring进行声明式事务管理,其底层使用的是
AOP
- 在spring进行声明式事务管理,其底层使用的是
- 在spring中进行声明式事务管理,有两种方式:
基于注解方式
【常用】- 基于xml配置文件方式【了解即可】
-
spring框架提供了一个API接口,代表事务管理器,这个接口针对于不同的框架,提供了不同的实现类
如图:事务管理器有很多实现类: 一种数据库的访问技术有一个实现类。 由实现类具体完成事务的提交,回顾。
例如:jdbc或者mybatis访问数据库有自己的事务管理器实现类
DataSourceTranactionManager
hibernate框架,他的事务管理器实现类
HibernateTransactionManager
,这就增加了代码的通用性。
注解方式实现声明式事务管理
-
在spring配置文件中创建事务管理器对象
<!--创建事务管理器对象--> <bean id="transactionManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager"> <!--注入数据源--> <property name="dataSource" ref="dataSource"></property> </bean>
-
在spring配置文件中开启事务注解
-
引入命名空间
tx
-
开启事务注解
<!--开启事务注解--> <tx:annotation-driven transaction-manager="transactionManager"/>
-
在service类上面或者service类方法上加入事务注解
-
@Transactional
,这个注解添加在类或者方法上
-
- 如果把这个注解添加在类上,这个类里面的所有方法都添加了事务
-
- 如果把这个注解添加在方法上,则只为这个方法添加事务
-
-
测试一下,现在我还模拟一下异常情况:
//转账的方法 public void accountMoney(){ //Lucy少100 userDao.reduceMoney(); //模拟异常 int i = 10 / 0; //Mary多100 userDao.addMoney(); }
-
注解参数说明
注解@Transactional
中可以传入这些参数
- 我们主要配置这些参数:
propagation
:事务传播行为isolation
:事务隔离级别timeout
:超时时间readOnly
:是否只读rollbackFor
:回滚noRollbackFor
:不回滚
事务传播行为
-
传播行为:业务方法在调用时,事务在方法之间的,传递和使用。使用传播行为,标识方法有无事务。
-
事务传播行为:在多事务方法直接进行调用时,这个过程中,事务是如何进行管理的
-
有七种事务传播行为:
-
重点掌握三个:
-
PROPAGATION_REQUIRED
-
spring
默认
传播行为,方法在调用的时候,如果存在事务就是使用当前的事务,如果没有事务,则新建事务,方法在新事务中执行。 -
举个例子,李四发现张三开着热点,李四就连上了张三的热点,实际上他们使用的就是一个网络了,也就是当前存在事务时,就加入到这个事务中运行;后来张三发现有人蹭网就把热点关了,李四发现没热点了,就打开了自己的数据,也就是如果没有事务,则新建事务
-
-
PROPAGATION_SUPPORTS
- 支持的意思,方法有事务可以正常执行,没有事务也可以正常执行。
- 一般多用在查询操作上
-
PROPAGATION_REQUIRES_NEW
- 方法需要一个
新事务
。如果调用方法时,存在一个事务,则原来的事务暂停。直到新事务执行完毕。如果方法调用时,没有事务,则新建一个事务,在新事务执行代码。
- 方法需要一个
-
事务隔离级别
事务的特性称为隔离性,多事务操作之间不会产生影响。
不考虑隔离性会产生很多问题。
有三个读问题:脏读、不可重复读、幻读
。
-
脏读:一个未提交事务读取到另一个未提交事务的数据。
-
不可重复读:一个未提交事务读取到另一个已提交事务的数据。
-
幻读(虚读):一个未提交事务读取到了另一个提交事务添加的数据。
通过设置事务的隔离级别,就能解决读问题。
-
1)
DEFAULT
:采用 DB 默认的事务隔离级别。MySql的默认为REPEATABLE_READ;Oracle 默认为READ_COMMITTED。 -
2)
READ_UNCOMMITTED
:读未提交。未解决任何并发问题。 -
3)
READ_COMMITTED
:读已提交。解决脏读,存在不可重复读与幻读。 -
4)
REPEATABLE_READ
:可重复读。解决脏读、不可重复读,存在幻读 -
5)
SERIALIZABLE
:串行化。不存在并发问题。
对比图:
事务其他参数
-
timeout
:超时时间- 事务需要在一定时间内进行提交,如果不提交或者执行超时则回滚
- 默认值为-1,设置时间以秒为单位计算
-
readOnly
:是否只读- 读:查询操作;写:添加修改删除操作
- 默认值为false,表示可以读写操作
- 可以设置为true,表示只能进行查询操作
-
rollbackFor
:回滚- 设置出现哪些异常进行事务回滚
-
noRollbackFor
:不回滚-
设置出现哪些异常不进行事务回滚
-
这两个方法传入的都是对应的异常类
-
例如:rollbackFor = {Exception.class}
-
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" xmlns:tx="http://www.springframework.org/schema/tx" xmlns:aop="http://www.springframework.org/schema/aop" xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context.xsd http://www.springframework.org/schema/tx http://www.springframework.org/schema/tx/spring-tx.xsd http://www.springframework.org/schema/aop http://www.springframework.org/schema/aop/spring-aop.xsd"> <!--引入外部文件--> <context:property-placeholder location="classpath:MyJDBC.properties"/> <!--配置数据库连接池--> <bean id="dataSource" class="com.alibaba.druid.pool.DruidDataSource" destroy-method="close"> <property name="driverClassName" value="${prop.driverClassName}"/> <property name="url" value="${prop.url}"/> <property name="username" value="${prop.username}"/> <property name="password" value="${prop.password}"/> </bean> <!--创建JdbcTemplate对象--> <bean id="jdbcTemplate" class="org.springframework.jdbc.core.JdbcTemplate"> <!--set注入dataSource--> <property name="dataSource" ref="dataSource"></property> </bean> <!--组件扫描--> <context:component-scan base-package="com.soberw.spring"/> <!--======================================以下是配置事务管理部分====================================================--> <!--1.创建事务管理器对象--> <bean id="transactionManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager"> <!--注入数据源--> <property name="dataSource" ref="dataSource"></property> </bean> <!--开启事务注解--> <!-- <tx:annotation-driven transaction-manager="transactionManager"/>--> <!--2.配置通知,即声明业务方法的事务属性(隔离级别,传播行为,超时等) id:给业务方法配置事务段代码起个名称,唯一值 transaction-manager:事务管理器的id --> <tx:advice id="txadvice" transaction-manager="transactionManager"> <!--配置事务参数,即给具体的业务方法增加事务的说明--> <tx:attributes> <!--指定哪种规则,哪个方法上添加事务 给具体的业务方法,说明他需要的事务属性 name: 业务方法名称。 配置name的值: 1. 业务方法的名称; 2. 带有部分通配符(*)的方法名称; 3 使用* propagation:指定传播行为的值 isolation:隔离级别 read-only:是否只读,默认是false timeout:超时时间 rollback-for:指定回滚的异常类列表,使用的异常全限定名称 --> <tx:method name="accountMoney" propagation="REQUIRED" isolation="DEFAULT" read-only="false" timeout="20" rollback-for="java.lang.NullPointerException"/> <!--在业务方法有命名规则后, 可以对一些方法使用事务--> <tx:method name="add*" propagation="REQUIRES_NEW" rollback-for="java.lang.Exception" /> <tx:method name="modify*" propagation="REQUIRED" rollback-for="java.lang.Exception" /> <tx:method name="remove*" propagation="REQUIRED" rollback-for="java.lang.Exception" /> <!--以上方法以外的 * :querySale, findSale, searchSale --> <tx:method name="*" propagation="SUPPORTS" read-only="true" /> </tx:attributes> </tx:advice> <!--3.配置切入点和切面,即声明切入点表达式: 表明那些包中的类,类中的方法参与事务--> <aop:config> <!--配置切入点,即声明切入点表达式 expression:切入点表达式, 表示那些类和类中的方法要参与事务 id:切入点表达式的名称,唯一值 --> <aop:pointcut id="pt" expression="execution(* com.soberw.spring.service.UserService.*(..))"/> <!--配置切面,即关联切入点表达式和事务通知--> <aop:advisor advice-ref="txadvice" pointcut-ref="pt"/> </aop:config> </beans>
完全注解方式
即完全使用注解来实现,包括连接数据库连接池,配置事务管理等…
-
创建一个配置类:
package com.soberw.spring.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; /** * @author soberw * @Classname SpringConfig * @Description * @Date 2022-02-16 14:12 */ @Configuration //配置类 @ComponentScan(basePackages = "com.soberw.spring") //组件扫描 @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_soberw?useUnicode=true&characterEncodeing=UTF-8&useSSL=false&serverTimezone=GMT"); dataSource.setUsername("root"); dataSource.setPassword("123456"); return dataSource; } //创建JdbcTemplate对象 @Bean public JdbcTemplate getJdbcTemplate(DataSource dataSource){ //到ioc容器中根据类型找到dataSource JdbcTemplate jdbcTemplate = new JdbcTemplate(); //注入 jdbcTemplate.setDataSource(dataSource); return jdbcTemplate; } //创建事务管理器 @Bean public DataSourceTransactionManager getDataSourceTransactionManager(DataSource dataSource){ DataSourceTransactionManager dataSourceTransactionManager = new DataSourceTransactionManager(); dataSourceTransactionManager.setDataSource(dataSource); return dataSourceTransactionManager; } }
-
测试一下:
@Test public void testAnno(){ ApplicationContext app = new AnnotationConfigApplicationContext(SpringConfig.class); UserService userService = app.getBean("userService", UserService.class); userService.accountMoney(); }
6.Spring5新特性
查看中文文档请点击,里面标注了spring5框架完整的新功能。
这里对几个重大的变动进行简单说明:
-
- 整个 Spring5 框架的代码基于
Java8
,运行时兼容JDK9
,许多不建议使用的类和方法在代码库中删除
- 整个 Spring5 框架的代码基于
-
- Spring 5.0 框架自带了通用的日志封装
-
- Spring5 框架核心容器支持
@Nullable
注解
- Spring5 框架核心容器支持
-
- Spring5 核心容器支持函数式风格
GenericApplicationContext
- Spring5 核心容器支持函数式风格
-
- Spring5 支持整合
JUnit5
- Spring5 支持整合
-
- 新增模块
Webflux
,用于web开发
- 新增模块
日志封装
Spring 5.0 框架自带了通用的日志封装。
- Spring5 已经移除
Log4jConfigListener
,官方建议使用Log4j2
- Spring5 框架整合
Log4j2
-
第一步:导入jar包,或者引入maven依赖
<!-- https://mvnrepository.com/artifact/org.apache.logging.log4j/log4j-core --> <dependency> <groupId>org.apache.logging.log4j</groupId> <artifactId>log4j-core</artifactId> <version>2.17.1</version> </dependency> <!-- https://mvnrepository.com/artifact/org.apache.logging.log4j/log4j-api --> <dependency> <groupId>org.apache.logging.log4j</groupId> <artifactId>log4j-api</artifactId> <version>2.17.1</version> </dependency> <!-- https://mvnrepository.com/artifact/org.apache.logging.log4j/log4j-slf4j-impl --> <dependency> <groupId>org.apache.logging.log4j</groupId> <artifactId>log4j-slf4j-impl</artifactId> <version>2.17.1</version> <scope>test</scope> </dependency> <!-- https://mvnrepository.com/artifact/org.slf4j/slf4j-api --> <dependency> <groupId>org.slf4j</groupId> <artifactId>slf4j-api</artifactId> <version>1.7.30</version> </dependency>
-
第二步:创建固定名字的配置文件
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>
-
这就实现了日志信息的输出,在不做任何操作的情况下,运行上面的测试代码
-
当然我们也可以手动输出日志,新建一个类
UserLog
模拟一下:import org.slf4j.Logger; import org.slf4j.LoggerFactory; /** * @author soberw * @Classname UserLog * @Description * @Date 2022-02-16 17:18 */ public class UserLog { private static final Logger logger = LoggerFactory.getLogger(UserLog.class); public static void main(String[] args) { logger.info("hello log4j2"); logger.warn("hello log4j2"); } }
支持@Nullable注解
Spring5 框架核心容器开始支持@Nullable 注解
@Nullable
注解可以使用在方法上面,属性上面,参数上面- 注解用在方法上面,方法返回值可以为空
- 注解使用在方法参数里面,方法参数可以为空
- 注解使用在属性上面,属性值可以为空
支持函数式风格
java8新增的一个很重大的内容就是lambda表达式
了,在spring5中,也提供了对lambda的支持,用的是GenericApplicationContext
实现类
使用函数式风格创建对象,并交给spring进行管理。
以上面的类UserLog
为例:
//函数式风格创建对象,交给spring进行管理
@Test
public void testGeneric(){
//1. 创建GenericApplicationContext对象
GenericApplicationContext context = new GenericApplicationContext();
//2. 调用方法注册
context.refresh();
context.registerBean("userLog",UserLog.class, UserLog::new);
//3. 获取在spring注册的对象
UserLog ul = context.getBean("userLog",UserLog.class);
System.out.println(ul);
}
整合JUnit5
配置文件一次获取,多次调用,我们不再需要像上面那样,每次测试都要重新读取一次配置文件。
-
整合JUnit4实现
第一步:导入Spring相关针对测试依赖
<!-- https://mvnrepository.com/artifact/org.springframework/spring-test --> <dependency> <groupId>org.springframework</groupId> <artifactId>spring-test</artifactId> <version>5.3.15</version> <scope>test</scope> </dependency>
第二步:创建测试类,使用注解完成
package com.soberw.spring.test; import com.soberw.spring.service.UserService; import org.junit.Test; import org.junit.runner.RunWith; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.annotation.Configuration; import org.springframework.test.context.ContextConfiguration; import org.springframework.test.context.junit4.SpringJUnit4ClassRunner; /** * @author soberw * @Classname JTest4 * @Description * @Date 2022-02-16 18:45 */ @RunWith(SpringJUnit4ClassRunner.class)//单元测试框架 @ContextConfiguration("classpath:ApplicationContext.xml")//加载配置文件 public class JTest4 { @Autowired private UserService userService; @Test public void test1(){ userService.accountMoney(); } }
-
上面这个功能我们使用JUnit4、5都能实现,但是在JUnit5中对这一写法做出了改动,使其更加简洁。
-
先引入JUnit5的jar包、其实这里我们可以借助我们的高级工具自动引入:
编写测试类:
package com.soberw.spring.test; import com.soberw.spring.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; /** * @author soberw * @Classname JTest5 * @Description * @Date 2022-02-16 19:03 */ @ExtendWith(SpringExtension.class)//单元测试框架 @ContextConfiguration("classpath:ApplicationContext.xml") public class JTest5 { @Autowired private UserService userService; @Test public void test1(){ userService.accountMoney(); } }
-
上面这样做其实和4版本没区别,只是导入的包不同而已。
-
5版本支持使用一个复合注解替代上面两个注解完成整合。
另外,spring还推出了一个全新的模块
Webflux
,用于 web 开发的,功能和 SpringMVC 类似的,Webflux 使用 当前一种比较流程响应式编程出现的框架。此模块需要基于SpringMVC,SpringBoot等知识进行实现,篇幅原因,这里不在赘述,感兴趣的同学可以点击此处了解