Spring
本篇当前字数近九万,涵盖大量代码,不仅讲基本使用,更讲底层原理,后续有新的感悟会继续补充! 如有谬误不吝赐教,如有收获请您三连! 末尾的拓展知识不可不品,应该是市面常规教程鲜少涉及或讲解错误的!
概念
Spring 是一个轻量级的、企业级的、基于控制反转(IoC) 的面向切面编程(AOP)的胶合剂框架平台。
狭义上的Spring就是指Spring Framework,包含了控制反转(IoC)容器、面向切面编程(AOP)、数据访问抽象(如 JDBC、事务管理)、Web 开发模块(如 MVC)、消息处理、测试等核心组件。
广义上的Spring指的是基于Spring Framwork构建起的庞大生态体系,包含了Spring Boot、Spring Data、Spring Security、Spring Cloud等项目
Spring Framework 主要组件一览表
IoC 容器 | spring-core , spring-beans , spring-context |
AOP 支持 | spring-aop |
数据访问 | spring-jdbc , spring-tx , spring-orm |
Web 开发 | spring-web , spring-webmvc |
消息处理 | spring-messaging , spring-websocket |
测试支持 | spring-test |
Spring 家族主要项目一览表
Spring Boot | 快速构建 Spring 应用,自动配置、内嵌服务器,开箱即用 |
Spring Cloud | 微服务架构解决方案,包括注册中心、负载均衡、配置中心等 |
Spring Security | 提供认证授权、权限控制的安全框架 |
Spring Data | 统一的数据访问层抽象(支持 JPA、MongoDB、Redis 等) |
Spring Batch | 大规模批处理任务的支持框架 |
Spring Integration | 企业集成模式(EIP)的实现,用于系统间通信 |
Spring Messaging | 支持 WebSocket、STOMP 等消息协议 |
Spring WebFlux | 响应式编程模型,支持非阻塞 I/O |
什么是IoC?什么是DI?
IoC(Inversion of Control)
DI(Dependency Injection)
控制反转是一种设计理念,依赖注入则是控制反转的实现方式
控制反转就是把对象的控制权交给了容器,由容器进行对象的管理(创建、依赖、销毁等)
Bean 就是由 Spring IoC 容器根据配置(XML 或注解)管理的 Java 对象实例
解耦是软件设计的核心目标之一,分层则是解耦的一种方式
IoC本质就是增加一个容器层,使得使得类的定义者和类的使用者解耦,两者通过容器(容器则配置进行Bean的管理)交互
什么是AOP?
AOP(Aspect Oriented Programming)
AOP是一种编程范式,旨在通过分离横切关注点(cross-cutting concerns)来提高模块化程度。在传统的程序设计中,如面向对象编程(OOP),我们通常将系统分解为多个模块,每个模块负责一部分功能。然而,有些功能(例如日志记录、事务管理、安全性检查等)往往会影响到多个模块,这些功能被称为横切关注点。如果直接在业务逻辑代码中实现这些功能,会导致代码分散且难以维护。
- Aspect(切面): 一个跨越多个类的关注点的模块化。事务管理是企业级Java应用中横切关注点的一个很好的例子。在Spring AOP中,切面是通过使用常规类(基于 schema 的方法)或使用
@Aspect
注解的常规类(@AspectJ 风格)实现的。 - Join point: 程序执行过程中的一个点,例如一个方法的执行或一个异常的处理。在Spring AOP中,一个连接点总是代表一个方法的执行。
- Advice: 一个切面在一个特定的连接点采取的行动。不同类型的advice包括 "around"、"before" 和 "after" 的advice(Advice 类型将在后面讨论)。许多AOP框架,包括Spring,都将advice建模为一个拦截器,并在连接点(Join point)周围维护一个拦截器链。
- Pointcut: 一个匹配连接点的谓词(predicate)。advice与一个切点表达式相关联,并在切点匹配的任何连接点上运行(例如,执行一个具有特定名称的方法)。由切点表达式匹配的连接点概念是AOP的核心,Spring默认使用AspectJ的切点表达式语言。
- Introduction: 代表一个类型声明额外的方法或字段。Spring AOP允许你为任何 advice 的对象引入新的接口(以及相应的实现)。例如,你可以使用引入来使一个bean实现
IsModified
接口,以简化缓存。(介绍在AspectJ社区中被称为类型间声明)。 - Target object: 被一个或多个切面所 advice 的对象。也被称为 "advised object"。由于Spring AOP是通过使用运行时代理来实现的,这个对象总是一个被代理的对象。
- AOP proxy: 一个由AOP框架创建的对象,以实现切面契约(advice 方法执行等)。在Spring框架中,AOP代理是一个JDK动态代理或CGLIB代理。
- Weaving(织入): 将aspect与其他应用程序类型或对象连接起来,以创建一个 advice 对象。这可以在编译时(例如,使用AspectJ编译器)、加载时或运行时完成。Spring AOP和其他纯Java AOP框架一样,在运行时进行织入。
项目创建
创建Maven项目,设置组ID和工件ID,选择保存位置,删除src目录
载入依赖
<!--
内部包含
spring-core
spring-beans
spring-aop
spring-expression
spring-context
spring-jcl
-->
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-context</artifactId>
<version>5.3.31</version>
</dependency>
基于XML标签的IoC
模块创建
基于之前创建的项目创建模块,设置工件ID
pojo类均在src/main/java/xyz/ssydx/pojo目录下
xml配置均在src/main/resources目录下
测试类均在test/java目录下
依赖注入
constructor 注入
PeoOtherInfo.java
package xyz.ssydx.pojo;
public class PeoOtherInfo {
private String address;
private String phone;
private String email;
public PeoOtherInfo() {
System.out.println("通过无参构造进行 PeoOtherInfo 实例化, 暂未给属性赋值(依赖未注入)");
}
public PeoOtherInfo(String address, String phone, String email) {
this.address = address;
this.phone = phone;
this.email = email;
System.out.println("通过有参构造进行 PeoOtherInfo 实例化, 同时给属性赋值(依赖注入)");
}
public String getAddress() {
return address;
}
public void setAddress(String address) {
this.address = address;
System.out.println("通过 setter 设置 address 属性");
}
public String getPhone() {
return phone;
}
public void setPhone(String phone) {
this.phone = phone;
System.out.println("通过 setter 设置 phone 属性");
}
public String getEmail() {
return email;
}
public void setEmail(String email) {
this.email = email;
System.out.println("通过 setter 设置 email 属性");
}
@Override
public String toString() {
return "PeoOtherInfo{" +
"address='" + address + '\'' +
", phone='" + phone + '\'' +
", email='" + email + '\'' +
'}';
}
}
beans01.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">
<!-- constructor 注入 -->
<!-- 属性名称赋值, 常用 -->
<!--
id bean 的标识, 本质是名称
class bean 的类型, 完全限定名(包名+类名)
name bean 的名称, 本质是别名 可以有多个, 以空格、逗号、分号分隔均可, 例如 name="a b,c;d"
-->
<bean id="peoOtherInfo1" class="xyz.ssydx.pojo.PeoOtherInfo">
<constructor-arg name="address" value="巴伐利亚" />
<constructor-arg name="phone" value="233333" />
<constructor-arg name="email" value="**********" />
</bean>
<!-- 属性索引赋值, 不常用 -->
<bean id="peoOtherInfo2" class="xyz.ssydx.pojo.PeoOtherInfo" name="peoOtherInfo">
<constructor-arg index="0" value="阿塞拜疆" />
<constructor-arg index="1" value="32879" />
<constructor-arg index="2" value="**********" />
</bean>
<!-- 属性类型赋值, 同类型属性会造成冲突, 不建议使用 -->
<!-- 别名的另一种写法 -->
<alias name="peoOtherInfo1" alias="info" />
<!-- 无参构造器实例化, 未进行依赖注入 -->
<bean id="peoOtherInfo3" class="xyz.ssydx.pojo.PeoOtherInfo" />
</beans>
TestBeans01.java
import org.springframework.context.ApplicationContext;
import org.springframework.context.support.ClassPathXmlApplicationContext;
import xyz.ssydx.pojo.PeoOtherInfo;
public class TestBeans01 {
public static void main(String[] args) {
/*
1.载入 beans01.xml 文件
2.解析 beans01.xml 文件
3.实例化所有 bean 通过构造器注入时实例化和依赖注入是同步发生的
4.对每个 bean 的进行依赖注入 依赖注入可以近似等价于属性赋值
5.初始化每个 bean 初始化bean的逻辑是由类的创建者定义的, 即一个bean也可能不初始化(例如pojo类)
6.将所有 bean 都放入容器中
下面的代码完成了以上一系列的工作并返回应用上下文(容器上下文)
即在执行该代码后, beans01.xml 中定义的所有 bean 已经存在于容器中, 而不是在获取某个 bean 时才对其进行实例化等操作
注: 以上仅适用于单例模式(默认情况, 最常用), 原型模式则是在获取时创建
*/
ApplicationContext context = new ClassPathXmlApplicationContext("beans01.xml");
// 通过 id 获取 bean, 强转为指定类型
PeoOtherInfo peoOtherInfo1 = (PeoOtherInfo) context.getBean("peoOtherInfo1");
// 通过 name 获取 bean, 同时指定其所属类型
PeoOtherInfo peoOtherInfo2 = context.getBean("peoOtherInfo", PeoOtherInfo.class);
System.out.println(peoOtherInfo1);
System.out.println(peoOtherInfo2);
/*
控制台输出如下:
通过有参构造进行 PeoOtherInfo 实例化, 同时给属性赋值(依赖注入)
通过有参构造进行 PeoOtherInfo 实例化, 同时给属性赋值(依赖注入)
通过无参构造进行 PeoOtherInfo 实例化, 暂未给属性赋值(依赖未注入)
PeoOtherInfo{address='巴伐利亚', phone='233333', email='**********'}
PeoOtherInfo{address='阿塞拜疆', phone='32879', email='**********'}
*/
}
}
setter 注入
ManyInfo.java
package xyz.ssydx.pojo;
import java.util.*;
public class ManyInfo {
private int id;
private String name;
private Integer age;
private PeoOtherInfo otherInfo;
private String[] novels;
private List<String> musics;
private Set<String> mangas;
private Map<String,String> movies;
private Properties properties;
public ManyInfo() {
}
public int getId() {
return id;
}
public void setId(int id) {
this.id = id;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public Integer getAge() {
return age;
}
public void setAge(Integer age) {
this.age = age;
}
public PeoOtherInfo getOtherInfo() {
return otherInfo;
}
public void setOtherInfo(PeoOtherInfo otherInfo) {
this.otherInfo = otherInfo;
}
public String[] getNovels() {
return novels;
}
public void setNovels(String[] novels) {
this.novels = novels;
}
public List<String> getMusics() {
return musics;
}
public void setMusics(List<String> musics) {
this.musics = musics;
}
public Set<String> getMangas() {
return mangas;
}
public void setMangas(Set<String> mangas) {
this.mangas = mangas;
}
public Map<String, String> getMovies() {
return movies;
}
public void setMovies(Map<String, String> movies) {
this.movies = movies;
}
public Properties getProperties() {
return properties;
}
public void setProperties(Properties properties) {
this.properties = properties;
}
@Override
public String toString() {
return "ManyInfo{" +
"id=" + id +
", name='" + name + '\'' +
", age=" + age +
", otherInfo=" + otherInfo +
", novels=" + Arrays.toString(novels) +
", musics=" + musics +
", mangas=" + mangas +
", movies=" + movies +
", properties=" + properties +
'}';
}
}
beans02.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">
<!-- setter 注入 -->
<bean id="peoOtherInfo" class="xyz.ssydx.pojo.PeoOtherInfo">
<property name="address" value="巴伐利亚" />
<property name="phone" value="233333" />
<property name="email" value="**********" />
</bean>
<!-- 各种类型的注入同样适用于 constructor -->
<bean id="manyInfo" class="xyz.ssydx.pojo.ManyInfo">
<!-- 基本类型 -->
<property name="id" value="1" />
<!-- 字符串类型 -->
<property name="name">
<value type="java.lang.String">wangwu</value>
</property>
<!-- 包装类型, 注入空值 -->
<property name="age">
<null />
</property>
<!-- 引用类型 -->
<property name="otherInfo" ref="peoOtherInfo" />
<!-- 数组类型 -->
<property name="novels">
<array>
<value>西游记</value>
<value>红楼梦</value>
<value>水浒传</value>
</array>
</property>
<!-- 列表类型 -->
<property name="musics">
<list>
<value>许嵩</value>
<value>汪苏泷</value>
<value>徐良</value>
</list>
</property>
<!-- 集合类型 -->
<property name="mangas">
<set>
<value>龙珠</value>
<value>JOJO</value>
<value>全职猎人</value>
</set>
</property>
<!-- 映射类型 -->
<property name="movies">
<map>
<entry key="乱" value="黑泽明" />
<entry>
<key>
<value>龙猫</value>
</key>
<value>宫崎骏</value>
</entry>
<entry>
<key>
<value>教父</value>
</key>
<value>科波拉</value>
</entry>
</map>
</property>
<!-- 属性类型 -->
<property name="properties">
<props>
<prop key="a">1</prop>
<prop key="b">2</prop>
<prop key="c">3</prop>
</props>
</property>
</bean>
</beans>
TestBeans02.java
import org.springframework.context.ApplicationContext;
import org.springframework.context.support.ClassPathXmlApplicationContext;
import xyz.ssydx.pojo.ManyInfo;
import xyz.ssydx.pojo.PeoOtherInfo;
public class TestBeans02 {
public static void main(String[] args) {
ApplicationContext context = new ClassPathXmlApplicationContext("beans02.xml");
// 通过 name 获取 bean, 同时指定其所属类型
ManyInfo manyInfo = context.getBean("manyInfo", ManyInfo.class);
System.out.println(manyInfo);
/*
控制台输出如下:
通过无参构造进行 PeoOtherInfo 实例化, 暂未给属性赋值(依赖未注入)
通过 setter 设置 address 属性
通过 setter 设置 phone 属性
通过 setter 设置 email 属性
ManyInfo{id=1, name='wangwu', age=null,
otherInfo=PeoOtherInfo{address='巴伐利亚', phone='233333', email='**********'},
novels=[西游记, 红楼梦, 水浒传], musics=[许嵩, 汪苏泷, 徐良], mangas=[龙珠, JOJO, 全职猎人],
movies={乱=黑泽明, 龙猫=宫崎骏, 教父=科波拉}, properties={b=2, a=1, c=3}}
*/
}
}
PeoOtherInfo见前文,两种注入方式对照理解
namespace 注入
beans03.xml
<?xml version="1.0" encoding="UTF-8"?>
<!-- 引入 c 命名空间(用于简化 constructor 注入) -->
<!-- 引入 p 命名空间(用于简化 setter 注入) -->
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:p="http://www.springframework.org/schema/p"
xmlns:c="http://www.springframework.org/schema/c"
xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd">
<!-- constructor 注入 -->
<bean id="peoOtherInfo1" class="xyz.ssydx.pojo.PeoOtherInfo" c:address="巴伐利亚" c:phone="233333" c:email="**********" />
<!-- setter 注入 -->
<bean id="peoOtherInfo2" class="xyz.ssydx.pojo.PeoOtherInfo" p:address="阿塞拜疆" p:phone="32879" p:email="**********" />
</beans>
TestBeans03.java
import org.springframework.context.ApplicationContext;
import org.springframework.context.support.ClassPathXmlApplicationContext;
import xyz.ssydx.pojo.PeoOtherInfo;
public class TestBeans03 {
public static void main(String[] args) {
ApplicationContext context = new ClassPathXmlApplicationContext("beans03.xml");
PeoOtherInfo peoOtherInfo1 = (PeoOtherInfo) context.getBean("peoOtherInfo1");
PeoOtherInfo peoOtherInfo2 = context.getBean("peoOtherInfo2", PeoOtherInfo.class);
System.out.println(peoOtherInfo1);
System.out.println(peoOtherInfo2);
/*
控制台输出如下:
通过有参构造进行 PeoOtherInfo 实例化, 同时给属性赋值(依赖注入)
通过无参构造进行 PeoOtherInfo 实例化, 暂未给属性赋值(依赖未注入)
通过 setter 设置 address 属性
通过 setter 设置 email 属性
通过 setter 设置 phone 属性
PeoOtherInfo{address='巴伐利亚', phone='233333', email='**********'}
PeoOtherInfo{address='阿塞拜疆', phone='32879', email='**********'}
*/
}
}
依赖传递
该示例表明:如果 bean A 通过 constructor 注入 bean B,则 bean B 会先进行创建,再进行 bean A 的创建;如果 bean A 通过 setter 注入 bean B,则 bean A 会先进行实例化,然后创建 bean B,再进行 bean A 的依赖注入,最终完成 bean A 的创建
注:此处的 bean 的创建指的是 bean实例化、bean依赖注入、bean初始化、bean放入容器上下文的全过程
People.java
package xyz.ssydx.pojo;
public class People {
private Integer id;
private String name;
private Integer age;
private PeoOtherInfo otherInfo;
public People() {
System.out.println("通过无参构造进行People实例化, 暂未给属性赋值");
}
public People(Integer id, String name, Integer age, PeoOtherInfo otherInfo) {
this.id = id;
this.name = name;
this.age = age;
this.otherInfo = otherInfo;
System.out.println("通过有参构造进行People实例化, 同时给属性赋值");
}
public Integer getId() {
return id;
}
public void setId(Integer id) {
this.id = id;
System.out.println("通过setter设置id属性");
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
System.out.println("通过setter设置name属性");
}
public Integer getAge() {
return age;
}
public void setAge(Integer age) {
this.age = age;
System.out.println("通过setter设置age属性");
}
public PeoOtherInfo getOtherInfo() {
return otherInfo;
}
public void setOtherInfo(PeoOtherInfo otherInfo) {
this.otherInfo = otherInfo;
System.out.println("通过setter设置otherInfo属性");
}
@Override
public String toString() {
return "People{" +
"id=" + id +
", name='" + name + '\'' +
", age=" + age +
", otherInfo=" + otherInfo +
'}';
}
}
beans04.xml
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd">
<bean id="people1" class="xyz.ssydx.pojo.People">
<constructor-arg name="id" value="1" />
<constructor-arg name="name" value="zhangsan" />
<constructor-arg name="age" value="25" />
<constructor-arg name="otherInfo" ref="peopleOtherInfo2" />
</bean>
<bean id="people2" class="xyz.ssydx.pojo.People">
<property name="id" value="2" />
<property name="name" value="lisi" />
<property name="age" value="30" />
<property name="otherInfo" ref="peopleOtherInfo1" />
</bean>
<bean id="people3" class="xyz.ssydx.pojo.People" />
<bean id="peopleOtherInfo2" class="xyz.ssydx.pojo.PeoOtherInfo">
<property name="address" value="阿塞拜疆" />
<property name="phone" value="32879" />
<property name="email" value="**********" />
</bean>
<bean id="peopleOtherInfo1" class="xyz.ssydx.pojo.PeoOtherInfo">
<constructor-arg name="address" value="巴伐利亚" />
<constructor-arg name="phone" value="233333" />
<constructor-arg name="email" value="**********" />
</bean>
</beans>
TestBeans04.java
import org.springframework.context.ApplicationContext;
import org.springframework.context.support.ClassPathXmlApplicationContext;
import xyz.ssydx.pojo.PeoOtherInfo;
import xyz.ssydx.pojo.People;
public class TestBeans04 {
public static void main(String[] args) {
ApplicationContext context = new ClassPathXmlApplicationContext("beans04.xml");
People people1 = context.getBean("people1", People.class);
System.out.println(people1);
People people2 = context.getBean("people2", People.class);
System.out.println(people2);
People people3 = context.getBean("people3", People.class);
System.out.println(people3);
/*
控制台输出如下:
通过无参构造进行 PeoOtherInfo 实例化, 暂未给属性赋值(依赖未注入)
通过 setter 设置 address 属性
通过 setter 设置 phone 属性
通过 setter 设置 email 属性
通过有参构造进行People实例化, 同时给属性赋值
通过无参构造进行People实例化, 暂未给属性赋值
通过有参构造进行 PeoOtherInfo 实例化, 同时给属性赋值(依赖注入)
通过setter设置id属性
通过setter设置name属性
通过setter设置age属性
通过setter设置otherInfo属性
通过无参构造进行People实例化, 暂未给属性赋值
People{id=1, name='zhangsan', age=25, otherInfo=PeoOtherInfo{address='阿塞拜疆', phone='32879', email='**********'}}
People{id=2, name='lisi', age=30, otherInfo=PeoOtherInfo{address='巴伐利亚', phone='233333', email='**********'}}
People{id=null, name='null', age=null, otherInfo=null}
*/
}
}
Bean的作用域
暂只讨论原型和单例
beans05.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">
<!-- 作用域默认是单例模式(singleton)的, 即多次从容器中取出的 peoOtherInfo1 是同一个实例 -->
<bean id="peoOtherInfo1" class="xyz.ssydx.pojo.PeoOtherInfo">
<constructor-arg name="address" value="巴伐利亚" />
<constructor-arg name="phone" value="233333" />
<constructor-arg name="email" value="**********" />
</bean>
<!-- 修改作用域为原型模式(prototype), 此时多次从容器中取出的 peoOtherInfo2 是不同的实例 -->
<bean id="peoOtherInfo2" class="xyz.ssydx.pojo.PeoOtherInfo" scope="prototype">
<property name="address" value="阿塞拜疆" />
<property name="phone" value="32879" />
<property name="email" value="**********" />
</bean>
</beans>
TestBeans05.java
import org.springframework.context.ApplicationContext;
import org.springframework.context.support.ClassPathXmlApplicationContext;
import xyz.ssydx.pojo.PeoOtherInfo;
import xyz.ssydx.pojo.People;
public class TestBeans05 {
public static void main(String[] args) {
ApplicationContext context = new ClassPathXmlApplicationContext("beans05.xml");
PeoOtherInfo peoOtherInfo1_1 = context.getBean("peoOtherInfo1", PeoOtherInfo.class);
PeoOtherInfo peoOtherInfo1_2 = context.getBean("peoOtherInfo1", PeoOtherInfo.class);
System.out.println(peoOtherInfo1_1 == peoOtherInfo1_2);
PeoOtherInfo peoOtherInfo2_1 = context.getBean("peoOtherInfo2", PeoOtherInfo.class);
PeoOtherInfo peoOtherInfo2_2 = context.getBean("peoOtherInfo2", PeoOtherInfo.class);
System.out.println(peoOtherInfo2_1 == peoOtherInfo2_2);
/*
控制台输出如下:
通过有参构造进行 PeoOtherInfo 实例化, 同时给属性赋值(依赖注入)
true
通过无参构造进行 PeoOtherInfo 实例化, 暂未给属性赋值(依赖未注入)
通过 setter 设置 address 属性
通过 setter 设置 phone 属性
通过 setter 设置 email 属性
通过无参构造进行 PeoOtherInfo 实例化, 暂未给属性赋值(依赖未注入)
通过 setter 设置 address 属性
通过 setter 设置 phone 属性
通过 setter 设置 email 属性
false
*/
}
}
自动装配
Book.java
package xyz.ssydx.pojo;
public class Book {
private String title;
private String author;
public Book(String title, String author) {
this.title = title;
this.author = author;
}
public String getTitle() {
return title;
}
public String getAuthor() {
return author;
}
@Override
public String toString() {
return "Book{" +
"title='" + title + '\'' +
", author='" + author + '\'' +
'}';
}
}
Reader.java
package xyz.ssydx.pojo;
public class Reader {
private String name;
private Book book;
public Reader() {
}
public Reader(String name) {
this.name = name;
}
public Reader(Book book) {
this.book = book;
}
public Reader(String name, Book book) {
this.book = book;
this.name = name;
}
public Book getBook() {
return book;
}
public void setBook(Book book) {
this.book = book;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public void read() {
System.out.println(name + "正在阅读: " + book.getTitle());
}
}
beans06.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:p="http://www.springframework.org/schema/p"
xmlns:c="http://www.springframework.org/schema/c"
xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd">
<bean id="book" class="xyz.ssydx.pojo.Book" c:title="百年孤独" c:author="马尔克斯" />
<!--
自动装配的方式:
byType 基于类型, 本质是 setter 注入
byName 基于名称, 本质是 setter 注入
default 等价于 no
constructor 基于构造器, 类似 byType, 但 constructor 注入
no 不自动装配, 默认
-->
<!-- 基于类型进行自动装配, 如果找不到匹配类型的 bean 或 存在多个匹配类型的 bean 均会报错 -->
<!-- 混合注入, 自动装配的 byType 是 setter 注入, c 装配是 constructor 注入 -->
<bean id="reader01" class="xyz.ssydx.pojo.Reader" autowire="byType" c:name="zhangsan" />
<!-- 基于名称(id)进行自动装配, 如果找不到匹配名称的 bean 则会报错 -->
<!-- 本质是 setter 注入(自动装配的 byName 是 setter 注入, p 装配也是 setter 注入) -->
<!-- <bean id="reader02" class="xyz.ssydx.pojo.Reader" autowire="byName" p:name="lisi" /> -->
<!-- 基于构造器进行自动装配, 如果找不到匹配类型的 bean 或存在多个匹配类型的 bean 均会报错 -->
<!-- 混合注入, 自动装配的 constructor 是 constructor 注入, p 装配是 setter 注入 -->
<bean id="reader02" class="xyz.ssydx.pojo.Reader" autowire="constructor" p:name="lisi"/>
<!-- 通常不建议使用混合注入, 需要同时维护 setter 和 多个 constructor, 容易出错 -->
</beans>
TestBeans06.java
import org.springframework.context.ApplicationContext;
import org.springframework.context.support.ClassPathXmlApplicationContext;
import xyz.ssydx.pojo.Book;
import xyz.ssydx.pojo.Reader;
public class TestBeans06 {
public static void main(String[] args) {
ApplicationContext context = new ClassPathXmlApplicationContext("beans06.xml");
Book book = context.getBean("book", Book.class);
System.out.println(book);
Reader reader01 = context.getBean("reader01", Reader.class);
reader01.read();
Reader reader02 = context.getBean("reader02", Reader.class);
reader02.read();
/*
控制台输出如下:
Book{title='百年孤独', author='马尔克斯'}
zhangsan正在阅读: 百年孤独
lisi正在阅读: 百年孤独
*/
}
}
拓展知识
Bean.java
package xyz.ssydx.pojo;
public class Bean {
private String name;
public void init() {
System.out.println("Bean init");
}
public Bean(String name) {
this.name = name;
System.out.println("通过有参构造进行 Bean 实例化, 同时给属性赋值(依赖注入)");
}
@Override
public String toString() {
return "Bean{" +
"name='" + name + '\'' +
'}';
}
public void destroy() {
System.out.println("Bean destroy");
}
}
beans07.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:p="http://www.springframework.org/schema/p"
xmlns:c="http://www.springframework.org/schema/c"
xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd">
<!-- bean在实例化、依赖注入后会调用该初始化方法, 容器上下文关闭前(前提是在程序关闭前先关闭上下文)会调用该销毁方法 -->
<bean id="bean01" class="xyz.ssydx.pojo.Bean" c:name="ssydx" init-method="init" destroy-method="destroy" />
<!--
启用懒加载, 即只在主动 getBean 时才进行 bean 的创建
优点是加快程序启动速度, 弊端是如果存在依赖错误等会被延迟到实际加载时才暴露, 通常不建议开启
-->
<bean id="bean02" class="xyz.ssydx.pojo.Bean" c:name="ssydx" init-method="init" destroy-method="destroy" lazy-init="true" />
</beans>
TestBeans07.java
import org.springframework.context.ApplicationContext;
import org.springframework.context.support.ClassPathXmlApplicationContext;
import xyz.ssydx.pojo.Bean;
import xyz.ssydx.pojo.Book;
import xyz.ssydx.pojo.Reader;
public class TestBeans07 {
public static void main(String[] args) {
// ApplicationContext 接口中未定义关闭上下文或注册钩子的方法, 因此此处不能向上转型为 ApplicationContext
ClassPathXmlApplicationContext context = new ClassPathXmlApplicationContext("beans07.xml");
// 向 JVM 注册一个关闭程序前的钩子, 使得 JVM 在程序结束时主动调用关闭容器上下文的方法, 进而调用 bean 的销毁方法
// 等价于 在操作完毕后主动调用 context.close()
context.registerShutdownHook();
Bean bean01 = context.getBean("bean01", Bean.class);
System.out.println(bean01);
// 在主程序结束前就先关闭容器上下文, 此时会销毁容器内的 bean, 进而调用 bean 的销毁方法
// context.close();
/*
控制台输出如下:
通过有参构造进行 Bean 实例化, 同时给属性赋值(依赖注入)
Bean init
Bean{name='ssydx'}
Bean destroy
*/
}
}
前文中反复提及的容器上下文其实就是容器本身的代表,几乎一切关于容器的操作都是通过容器上下文这个对象进行的
基于Spring注解的IoC
通过配置文件进行 bean 的配置有时会显得过于脱离(解耦)源代码,也比较繁琐
另一个选择是通过注解进行 bean 的配置,甚至是混合使用两种方式
配置文件和注解哪个更好?
两者各有优劣,可以说一种方式的优点同时也可视为其缺点,一般来说大多数配置都可通过注解进行,复杂配置还是借助配置文件更具灵活性和整体性
模块创建
基于之前创建的项目创建模块,设置工件ID
pojo类均在src/main/java/xyz/ssydx/pojo目录下
config类均在src/main/java/xyz/ssydx/config目录下
xml配置均在src/main/resources目录下
测试类均在test/java目录下
AutoWired|Qualifier
Book.java
package xyz.ssydx.pojo;
import org.springframework.beans.factory.annotation.Value;
public class Book {
// 值注入
@Value("百年孤独")
private String title;
public String getTitle() {
return title;
}
}
Reader.java
package xyz.ssydx.pojo;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.lang.Nullable;
public class Reader {
// 声明该属性可以不注入
@Autowired(required = false)
private String name;
/*
autowired原理:
此类既不存在 setter, 也不存在 constructor(除了无参构造器), 它如何进行依赖注入的呢?
答案是通过反射机制进行的字段注入, 这种方式性能比直接通过 setter 或 constructor 要差一点, 但损耗通常可以忽略
自动注入的查询逻辑是首先按类型查找, 如果不存在则报错, 如果存在单个则注入, 如果存在多个则再按名称查找, 如果不存在同名则报错
即注入失败存在两种可能: 不存在匹配类型的 bean, 存在多个匹配类型但不存在匹配名称的 bean
依赖注入的方式层级顺序是: 构造器注入(多个构造器会自动选择最匹配的) > 设置器注入 > 反射注入
在任一层级上进行@autowired注解均会屏蔽下层(即如果设置器层级添加了autowired, 不会影响先按构造器尝试注入, 但会影响反射注入, 反射注入不会执行)
此外, 如果在某构造器添加了 autowired(只允许一个构造器添加) 如果注入失败不会再考虑其他构造器注入, 更不会考虑设置器注入和反射注入
什么是最匹配?即参数最多且满足要求(满足要求指的是容器中存在这些参数对应的 bean)的构造器
实际很少会给 setter 添加注解(不推荐)
constructor 添加注解也很少见
一般会 提供不加注解的全参构造器明确依赖关系 或 提供都添加注解的字段
*/
// 此处开启自动注入, 前提是 Book 存在被容器接管的 bean
@Autowired
// 指定按特定名称注入, 适合存在多个同类型 bean 的情况, 尤其适用于 bean 的名称与字段名不同的情形
@Qualifier("book01")
private Book book;
// 添加 @Nullable 注解使得即使该属性未被注入也可以正常创建 bean
// 等价于 在对应字段上添加 @Autowired(required = false)
// public Reader(@Nullable String name) {
// this.name = name;
// }
public void read() {
System.out.println(name + "正在阅读: " + book.getTitle());
}
}
beans01.xml
<?xml version="1.0" encoding="UTF-8"?>
<!-- 注意该标签属性与纯配置文件配置 bean 时不同 -->
<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
https://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/context
https://www.springframework.org/schema/context/spring-context.xsd">
<!-- 开启注解解析, 不会根据注解注册为 bean -->
<context:annotation-config/>
<!-- 此处并未进行依赖注入 -->
<bean id="book01" class="xyz.ssydx.pojo.Book" />
<bean id="book02" class="xyz.ssydx.pojo.Book" />
<bean id="reader" class="xyz.ssydx.pojo.Reader" />
</beans>
TestBeans01.java
import org.springframework.context.ApplicationContext;
import org.springframework.context.support.ClassPathXmlApplicationContext;
import xyz.ssydx.pojo.Reader;
public class TestBeans01 {
public static void main(String[] args) {
ApplicationContext context = new ClassPathXmlApplicationContext("beans01.xml");
Reader reader = context.getBean("reader", Reader.class);
reader.read();
/*
控制台输出如下:
null正在阅读: 百年孤独
*/
}
}
Java EE 提供了 @Resource 注解用于自动注入,但其与 @Autowired 有诸多差异
1.前者按照先名称(默认按属性名称)后类型的顺序查找,后者按照先类型后名称的顺序查找
2.前者支持指定名称,后者需配合 Qualifier 实现指定名称
3.前者不支持层级屏蔽和回退机制,后者则支持复杂的注入逻辑
注意:@Autowired注解 和 标签中的autowire具有不同的查找逻辑,后者不支持分支查找
Component
User.java
package xyz.ssydx.pojo;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;
// 声明为组件, 便于自动注册为 bean, 名称未指定时默认为类名的首字母小写版本
@Component("user")
public class User {
@Value("ssydx")
private String name;
@Value("25")
private int age;
@Override
public String toString() {
return "User{" +
"name='" + name + '\'' +
", age=" + age +
'}';
}
}
beans02.xml
<?xml version="1.0" encoding="UTF-8"?>
<!-- 注意该标签属性与纯配置文件配置 bean 时不同 -->
<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
https://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/context
https://www.springframework.org/schema/context/spring-context.xsd">
<!-- 开启注解扫描, 不仅支持注解的解析, 还可以把使用特定注解的类自动注册为 bean, 扫描指定的包 -->
<context:component-scan base-package="xyz.ssydx.pojo" />
</beans>
TestBeans02.java
import org.springframework.context.ApplicationContext;
import org.springframework.context.support.ClassPathXmlApplicationContext;
import xyz.ssydx.pojo.Reader;
import xyz.ssydx.pojo.User;
public class TestBeans02 {
public static void main(String[] args) {
ApplicationContext context = new ClassPathXmlApplicationContext("beans02.xml");
User user = context.getBean("user", User.class);
System.out.println(user);
/*
控制台输出如下:
User{name='ssydx', age=25}
*/
}
}
Controller、Repository、Service等很多注解的本质都是Component
@Scope 注解可以设置 bean 的作用域
Configuration|Bean|Primary
Dog.java
package xyz.ssydx.pojo;
public class Dog {
private String name;
public Dog(String name) {
this.name = name;
}
public String getName() {
return name;
}
}
People.java
package xyz.ssydx.pojo;
public class People {
private String name;
private Dog dog;
public People(String name, Dog dog) {
this.name = name;
this.dog = dog;
}
public void walk() {
System.out.println(name + "在遛" + dog.getName());
}
}
BeanConfig.java
package xyz.ssydx.config;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Primary;
import xyz.ssydx.pojo.Dog;
import xyz.ssydx.pojo.People;
// 被 @Configuration 注解的类叫配置类, 其相当于 beans.xml 配置文件
@Configuration
public class BeanConfig {
// bean 的默认名称是方法名
@Bean
// 声明默认情况下, 在存在多个同类型的 bean 时优先注入这个 bean
@Primary
public Dog dog01() {
return new Dog("233");
}
// 显式指定 bean 的名称(id)
@Bean("dog02")
public Dog dog02() {
return new Dog("666");
}
@Bean
// 注入 Dog 的 bean, 默认注入被 @Primary 注解修饰的那个
public People getPeople01(Dog dog) {
return new People("zhangsan", dog);
}
@Bean("people")
// 也可显式指定要注入的 bean 的名称
public People getPeople02(@Qualifier("dog02") Dog dog) {
return new People("lisi", dog);
}
}
TestBean03.java
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
import xyz.ssydx.pojo.People;
import xyz.ssydx.config.BeanConfig;
public class TestBeans03 {
public static void main(String[] args) {
// 载入配置类,其具体的执行逻辑同 beans.xml 配置文件
AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(BeanConfig.class);
// 默认 bean 名称是方法名
People getPeople01 = context.getBean("getPeople01", People.class);
// 自定义了 bean 的名称
People people = context.getBean("people", People.class);
getPeople01.walk();
people.walk();
/*
控制台输出如下:
zhangsan在遛233
lisi在遛666
*/
}
}
ComponentScan
Mouse.java
package xyz.ssydx.pojo;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;
import org.springframework.stereotype.Service;
// 本质也是 Component, 此处意在说明这点, 实际开发中不可随意使用
@Service
public class Mouse {
@Value("jerry")
private String name;
public String getName() {
return name;
}
}
Cat.java
package xyz.ssydx.pojo;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;
@Component
public class Cat {
@Value("tom")
private String name;
public String getName() {
return name;
}
}
AllConfig.java
package xyz.ssydx.config;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;
// 不但声明该类为配置类, 且开启注解扫描, 相当于 beans.xml 中 设置 <context:component-scan base-package="xyz.ssydx.pojo" />
@Configuration
@ComponentScan("xyz.ssydx.pojo")
public class AllConfig {
}
TestBean04.java
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
import xyz.ssydx.config.AllConfig;
import xyz.ssydx.config.BeanConfig;
import xyz.ssydx.pojo.Cat;
import xyz.ssydx.pojo.Mouse;
import xyz.ssydx.pojo.People;
public class TestBeans04 {
public static void main(String[] args) {
// 载入配置类,其具体的执行逻辑同 beans.xml 配置文件
AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(AllConfig.class);
Cat cat = context.getBean(Cat.class);
Mouse mouse = context.getBean(Mouse.class);
System.out.println(cat.getName() + "在追" + mouse.getName());
/*
控制台输出如下:
tom在追jerry
*/
}
}
至此,已经完全实现了由配置文件向注解配置类的转变
基于spring-aop的AOP
模块创建
参考前文略作修改即可
proxy
一个对象proxy代替另一个对象target执行操作,可以在不修改target的基础上进行操作扩展,例如:在执行具体操作前先执行某些操作,在执行具体操作后执行某些操作
典型如:日志记录
静态代理
缺乏灵活性:静态代理需要为每个真实主题(RealSubject)类创建一个代理类,在编译期就确定下来,这意味着如果需要更改或增加功能,就需要修改或添加新的代理类,不够灵活。 代码冗余:因为每一个真实主题都需要一个对应的代理类,所以如果有多个真实主题类,那么就需要编写同样数量的代理类,这会导致代码量增大,并且很多代理类之间的代码可能是重复的,降低了代码的可维护性。 扩展性差:当需要对一个新的服务接口提供代理时,必须手动编写相应的代理类,无法像动态代理那样自动适应接口的变化,对于频繁变更需求的项目来说,维护成本较高。 难以应对多样的需求变化:在实际开发中,可能需要根据不同的条件使用不同的代理逻辑,静态代理由于是在编译期确定的,很难做到这一点;而动态代理可以在运行时决定具体的代理逻辑,更加灵活。 不利于解耦:静态代理通常会将一些业务逻辑直接写入到代理类中,这样会导致业务逻辑与控制逻辑混合在一起,不利于代码的解耦和重用。
以代驾为例
Drive
package xyz.ssydx.proxy.static_proxy;
public interface Drive {
public void drive();
}
Driver
package xyz.ssydx.proxy.static_proxy;
public class Driver implements Drive {
@Override
public void drive() {
System.out.println("开车");
}
}
DesignatedDriver
package xyz.ssydx.proxy.static_proxy;
public class DesignatedDriver implements Drive {
private final Drive drive;
public DesignatedDriver(Drive drive) {
this.drive = drive;
}
@Override
public void drive() {
beforeDrive();
drive.drive();
afterDrive();
}
private void beforeDrive() {
System.out.println("联系车主");
}
private void afterDrive() {
System.out.println("车主付款");
}
}
TestDrive
package xyz.ssydx.proxy.static_proxy;
public class TestDrive {
public static void main(String[] args) {
DesignatedDriver designatedDriver = new DesignatedDriver(new Driver());
designatedDriver.drive();
/*
控制台输出如下:
联系车主
开车
车主付款
*/
}
}
动态代理
主要分类基于接口的动态代理和基于类的动态代理,前者是JDK原生支持的,后者借助cglib
JDK代理主要依靠两个对象:Proxy类 和 InvocationHandler接口
网络上充斥着诸如:静态代理需要为每个目标类编写代理类 之类的说法,这根本没揭示动态代理的本质优势(静态代理并不需要为每个目标类都编写代理类)
动态代理相对于静态代理的本质区别是InvocationHandler的
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {} 方法封装了通过反射调用目标类方法的过程,外部使用者无需关心反射的具体逻辑,invoke也不必枚举出接口中的每个方法,简化了逻辑,提高了可维护性
Proxy则动态生成代理类
Drive
同静态代理
Driver
同静态代理
MyInvicationHandler
package xyz.ssydx.proxy.dynamic_proxy;
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;
public class MyInvocationHandler implements InvocationHandler {
private final Object target;
public MyInvocationHandler(Object target) {
this.target = target;
}
public Object getProxy() {
return Proxy.newProxyInstance(this.getClass().getClassLoader(), target.getClass().getInterfaces(), this);
}
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
before();
// 核心在于此处可以对目标类(接口)方法进行灵活处理
Object ret = method.invoke(target, args);
after();
return ret;
}
private void before() {
System.out.println("联系车主");
}
private void after() {
System.out.println("车主付款");
}
}
TestDrive
同静态代理
再次强调
动态代理的本质不是不需要写代理类
,而是通过 InvocationHandler 的 invoke() 方法,实现了一个统一、集中、透明的方法拦截和增强机制,这才是它真正强大的地方。
spring-aop
通知接口
MethodBeforeAdvice | org.springframework.aop |
有 | 在目标方法执行前执行 |
AfterReturningAdvice | org.springframework.aop |
有 | 在目标方法成功返回后执行 |
ThrowsAdvice | org.springframework.aop |
没有 | 在目标方法抛出异常时执行 |
MethodInterceptor | org.aopalliance.intercept |
有 | 环绕通知,可以控制目标方法的执行前后和返回值甚至异常处理 |
ThrowsAdvice 非常特殊,spring会通过反射识别你在其中定义的方法(只支持两种固定的方法签名)
UserService
package xyz.ssydx.service;
public interface UserService {
public void add();
public void delete();
public void update();
public void select();
}
UserServiceImpl
package xyz.ssydx.service;
public class UserServiceImpl implements UserService {
@Override
public void add() {
System.out.println("增加用户");
}
@Override
public void delete() {
System.out.println("删除用户");
}
@Override
public void update() {
System.out.println("更新用户");
// 不妨尝试抛出异常, 观察 TestAOP01 执行结果有什么不同
// throw new IllegalArgumentException("参数错误");
}
@Override
public void select() {
System.out.println("查询用户");
}
}
Aspect_Before
package xyz.ssydx.aspect;
import org.springframework.aop.MethodBeforeAdvice;
import java.lang.reflect.Method;
public class Aspect_Before implements MethodBeforeAdvice {
@Override
public void before(Method method, Object[] args, Object target) throws Throwable {
System.out.println("Before Method: " + method.getName());
}
}
Aspect_AfterReturning
package xyz.ssydx.aspect;
import org.springframework.aop.AfterReturningAdvice;
import java.lang.reflect.Method;
public class Aspect_AfterReturning implements AfterReturningAdvice {
@Override
public void afterReturning(Object returnValue, Method method, Object[] args, Object target) throws Throwable {
System.out.println("AfterReturning Method: " + method.getName());
}
}
Aspect_AfterThrowing
package xyz.ssydx.aspect;
import org.springframework.aop.ThrowsAdvice;
import java.lang.reflect.Method;
// 虽然接口中并未定义方法, 但 spring 只会识别以下两种方法签名, 且如果同时使用两者, 只会调用第二种
// 注意: 其只能监控异常, 而不能捕获异常(捕获指的是异常传递到此处时可以选择只输出信息不再抛出), 实现捕获要借助环绕通知
public class Aspect_AfterThrowing implements ThrowsAdvice {
// 方式一:简单处理异常
public void afterThrowing(Exception ex) {
System.out.println("throw: " + ex.getMessage());
}
// 方式二:更详细的异常处理(Spring 会优先使用这个)
public void afterThrowing(Method method, Object[] args, Object target, Exception ex) {
System.out.println("AfterThrowing Method: " + method.getName() + " throw: " + ex.getMessage());
}
}
Aspect_Around
package xyz.ssydx.aspect;
import org.aopalliance.intercept.MethodInterceptor;
import org.aopalliance.intercept.MethodInvocation;
public class Aspect_Around implements MethodInterceptor {
@Override
public Object invoke(MethodInvocation invocation) throws Throwable {
System.out.println("Around Before Method: " + invocation.getMethod().getName());
Object result = invocation.proceed();
System.out.println("Around After Method: " + invocation.getMethod().getName());
return result;
}
}
beans01.xml
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="
http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans.xsd
">
<!-- 目标对象 代理要替谁做 -->
<bean id="userServiceImpl" class="xyz.ssydx.service.UserServiceImpl" />
<!-- 通知(实现了官方通知接口) 代理要做什么 -->
<bean id="aspect_Before" class="xyz.ssydx.aspect.Aspect_Before" />
<bean id="aspect_AfterReturning" class="xyz.ssydx.aspect.Aspect_AfterReturning" />
<bean id="aspect_AfterThrowing" class="xyz.ssydx.aspect.Aspect_AfterThrowing" />
<bean id="aspect_Around" class="xyz.ssydx.aspect.Aspect_Around" />
<!-- 切点(按方法名称匹配) 代理要在哪里做 -->
<bean id="pointcut01" class="org.springframework.aop.support.NameMatchMethodPointcut">
<property name="mappedNames" value="add,delete" />
</bean>
<bean id="pointcut02" class="org.springframework.aop.support.NameMatchMethodPointcut">
<property name="mappedNames" value="add,update">
</property>
</bean>
<bean id="pointcut03" class="org.springframework.aop.support.NameMatchMethodPointcut">
<property name="mappedNames" value="update">
</property>
</bean>
<!-- 切面, 绑定通知和切点(尚未绑定到具体的 bean) 代理在哪里做什么的关系表 -->
<bean id="aspect01" class="org.springframework.aop.support.DefaultBeanFactoryPointcutAdvisor">
<!-- 切点 -->
<property name="pointcut" ref="pointcut01"/>
<!-- 通知 -->
<property name="advice" ref="aspect_Before"/>
</bean>
<bean id="aspect02" class="org.springframework.aop.support.DefaultBeanFactoryPointcutAdvisor">
<property name="pointcut" ref="pointcut02"/>
<property name="advice" ref="aspect_AfterReturning"/>
</bean>
<bean id="aspect03" class="org.springframework.aop.support.DefaultBeanFactoryPointcutAdvisor">
<property name="pointcut" ref="pointcut03"/>
<property name="advice" ref="aspect_AfterThrowing"/>
</bean>
<!-- 代理(生成代理) 代理替谁在哪里做什么 -->
<bean id="userServiceProxy" class="org.springframework.aop.framework.ProxyFactoryBean">
<!-- 目标类 -->
<property name="target" ref="userServiceImpl"/>
<!-- 目标接口 -->
<property name="proxyInterfaces" value="xyz.ssydx.service.UserService"/>
<!-- 切面或通知(直接传入通知相当于在每个方法进行通知) -->
<property name="interceptorNames" value="aspect01,aspect02,aspect03,aspect_Around" />
</bean>
<!--
以上实现的效果:
在 add, delete 方法前增加前置通知
在 add, update 方法后增加正常结束通知
在 update 方法后增加异常结束通知
在 add, delete, update, select 方法前后增加环绕通知
-->
</beans>
TestAOP01
import org.springframework.context.ApplicationContext;
import org.springframework.context.support.ClassPathXmlApplicationContext;
import xyz.ssydx.service.UserService;
public class TestAOP01 {
public static void main(String[] args) {
ApplicationContext context = new ClassPathXmlApplicationContext("beans01.xml");
UserService userService = context.getBean("userServiceProxy", UserService.class);
userService.add();
userService.delete();
userService.update();
userService.select();
/*
控制台输出如下(userService.update() 未抛出异常时):
Before Method: add
Around Before Method: add
增加用户
Around After Method: add
AfterReturning Method: add
Before Method: delete
Around Before Method: delete
删除用户
Around After Method: delete
Around Before Method: update
更新用户
Around After Method: update
AfterReturning Method: update
Around Before Method: select
查询用户
Around After Method: select
控制台输出如下(userService.update() 抛出异常时):
Before Method: add
Around Before Method: add
增加用户
Around After Method: add
AfterReturning Method: add
Before Method: delete
Around Before Method: delete
删除用户
Around After Method: delete
Around Before Method: update
更新用户
AfterThrowing Method: update throw: 参数错误
Exception in thread "main" java.lang.IllegalArgumentException: 参数错误
...省略异常堆栈
*/
}
}
TestAOP02
import org.springframework.aop.Advisor;
import org.springframework.aop.framework.ProxyFactory;
import org.springframework.aop.support.DefaultPointcutAdvisor;
import org.springframework.aop.support.NameMatchMethodPointcut;
import xyz.ssydx.aspect.Aspect_AfterReturning;
import xyz.ssydx.aspect.Aspect_AfterThrowing;
import xyz.ssydx.aspect.Aspect_Around;
import xyz.ssydx.aspect.Aspect_Before;
import xyz.ssydx.service.UserService;
import xyz.ssydx.service.UserServiceImpl;
public class TestAOP02 {
public static void main(String[] args) {
// 1. 创建目标对象
UserService userServiceImpl = new UserServiceImpl();
// 2. 创建通知对象
Aspect_Before beforeAdvice = new Aspect_Before();
Aspect_AfterReturning afterReturningAdvice = new Aspect_AfterReturning();
Aspect_AfterThrowing throwsAdvice = new Aspect_AfterThrowing();
Aspect_Around aroundAdvice = new Aspect_Around();
// 3. 创建切点并设置匹配方法名
NameMatchMethodPointcut pointcut01 = new NameMatchMethodPointcut();
pointcut01.setMappedNames("add", "delete");
NameMatchMethodPointcut pointcut02 = new NameMatchMethodPointcut();
pointcut02.setMappedNames("add", "update");
NameMatchMethodPointcut pointcut03 = new NameMatchMethodPointcut();
pointcut03.setMappedNames("update");
// 4. 创建切面,绑定切点和通知
Advisor advisor01 = new DefaultPointcutAdvisor(pointcut01, beforeAdvice);
Advisor advisor02 = new DefaultPointcutAdvisor(pointcut02, afterReturningAdvice);
Advisor advisor03 = new DefaultPointcutAdvisor(pointcut03, throwsAdvice);
// 5. 创建代理工厂
ProxyFactory proxyFactory = new ProxyFactory();
proxyFactory.setTarget(userServiceImpl);
proxyFactory.setInterfaces(UserService.class); // 设置代理接口JDK动态代理
// 6. 添加拦截器(即切面或通知),顺序重要!
proxyFactory.addAdvisor(advisor01);
proxyFactory.addAdvisor(advisor02);
proxyFactory.addAdvisor(advisor03);
proxyFactory.addAdvice(aroundAdvice); // 环绕通知直接添加即可
// 7. 获取代理对象
UserService proxy = (UserService) proxyFactory.getProxy();
// 8. 测试执行
proxy.add();
proxy.delete();
proxy.update();
proxy.select();
/*
控制台输出如下:
同 TestAOP01
*/
}
}
TestAOP02 是 beans01.xml 配置文件的纯 Java 代码版本,不难理解其底层使用的正是 JDK 的动态代理
切点的设置除了按方法名称外还有 JdkRegexpMethodPointcut 正则设置等形式
代理方式
spring-aop 除了支持 JDK 动态代理外,同时也支持 cglib 动态代理,它会自动按照下列规则进行选择
目标类实现了至少一个接口,并且你未显式设置接口 | 使用 JDK 动态代理 |
目标类没有实现任何接口 | 自动回退到 CGLIB 代理 |
显式设置了接口(如 setInterfaces(UserService.class) ) |
使用 JDK 动态代理 |
设置了 setProxyTargetClass(true) |
强制使用 CGLIB 代理,即使有接口 |
动态接口(拓展知识)
借助 cglib 的动态代理机制可以实现在不修改原类代码的前提下给其增加新的方法
OneInterface
package xyz.ssydx.service;
public interface OneInterface {
public void doSomething();
}
OneClass
package xyz.ssydx.service;
public class OneClass implements OneInterface{
@Override
public void doSomething() {
System.out.println("OneClass doSomething");
}
}
TestAOP03
import org.springframework.aop.framework.ProxyFactory;
import org.springframework.aop.support.DefaultIntroductionAdvisor;
import org.springframework.aop.support.DelegatingIntroductionInterceptor;
import xyz.ssydx.service.OneClass;
import xyz.ssydx.service.OneInterface;
import xyz.ssydx.service.UserService;
import xyz.ssydx.service.UserServiceImpl;
public class TestAOP03 {
public static void main(String[] args) {
// 1. 创建目标对象
UserService userServiceImpl = new UserServiceImpl();
// 2. 创建混入对象
OneClass oneClass = new OneClass();
DelegatingIntroductionInterceptor interceptor = new DelegatingIntroductionInterceptor(oneClass);
DefaultIntroductionAdvisor advisor = new DefaultIntroductionAdvisor(interceptor, OneInterface.class);
// 3. 创建代理工厂
ProxyFactory proxyFactory = new ProxyFactory();
proxyFactory.setTarget(userServiceImpl);
// 强制使用 cglib 进行动态代理, 因为 userServiceImpl 已经实现了其他接口, 会默认使用 JDK 的动态代理
proxyFactory.setProxyTargetClass(true);
// 混入对象
proxyFactory.addAdvisor(advisor);
// 4. 获取代理对象
UserService proxy = (UserService) proxyFactory.getProxy();
// 5. 测试执行
proxy.add();
proxy.delete();
proxy.update();
proxy.select();
if (proxy instanceof OneInterface) {
OneInterface oneInterface = (OneInterface) proxy;
oneInterface.doSomething();
}
/*
控制台输出如下:
增加用户
删除用户
更新用户
查询用户
OneClass doSomething
*/
}
}
基于aspectjweaver的AOP
模块创建
参考前文略作修改即可
载入依赖
<dependency>
<groupId>org.aspectj</groupId>
<artifactId>aspectjweaver</artifactId>
<version>1.9.23</version>
</dependency>
基于XML标签的AOP
UserService|UserServiceImpl
同前文
Aspect01
package xyz.ssydx.aspect;
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.ProceedingJoinPoint;
public class Aspect01 {
public void before(JoinPoint joinPoint) {
System.out.println("Before Method: " + joinPoint.getSignature().getName());
}
public void afterReturning(JoinPoint joinPoint, Object result) {
System.out.println("AfterReturning Method: " + joinPoint.getSignature().getName());
}
public void afterThrowing(JoinPoint joinPoint, Throwable ex) {
System.out.println("AfterThrowing Method: " + joinPoint.getSignature().getName() + " throw: " + ex.getMessage());
}
// 相比 spring-aop 多了一个后置通知(无论方法执行成功与否都会调用)
public void after(JoinPoint joinPoint) {
System.out.println("After Method: " + joinPoint.getSignature().getName());
}
public Object around(ProceedingJoinPoint proceedingJoinPoint) throws Throwable {
System.out.println("Around Before Method: " + proceedingJoinPoint.getSignature().getName());
Object result = proceedingJoinPoint.proceed();
System.out.println("Around After Method: " + proceedingJoinPoint.getSignature().getName());
return result;
}
}
bean01.xml
<?xml version="1.0" encoding="UTF-8"?>
<!-- 注意: 此处标签属性有所不同 -->
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:aop="http://www.springframework.org/schema/aop"
xsi:schemaLocation="
http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/aop
http://www.springframework.org/schema/aop/spring-aop.xsd
">
<bean id="userServiceImpl" class="xyz.ssydx.service.UserServiceImpl" />
<bean id="aspect01" class="xyz.ssydx.aspect.Aspect01" />
<aop:config>
<aop:pointcut id="all" expression="execution(* xyz.ssydx.service.UserServiceImpl.*(..))"/>
<aop:pointcut id="update" expression="execution(* xyz.ssydx.service.UserServiceImpl.update(..))"/>
<aop:pointcut id="add_and_delete" expression="execution(* xyz.ssydx.service.UserServiceImpl.add(..)) || execution(* xyz.ssydx.service.UserServiceImpl.delete(..))"/>
<aop:pointcut id="add_and_update" expression="execution(* xyz.ssydx.service.UserServiceImpl.add(..)) || execution(* xyz.ssydx.service.UserServiceImpl.update(..))"/>
<aop:aspect ref="aspect01">
<aop:before method="before" pointcut-ref="add_and_delete" />
<aop:after-returning method="afterReturning" pointcut-ref="add_and_update" returning="result" />
<aop:after-throwing method="afterThrowing" pointcut-ref="update" throwing="ex" />
<aop:after method="after" pointcut-ref="all" />
<aop:around method="around" pointcut-ref="all" />
</aop:aspect>
</aop:config>
</beans>
TestAOP01
import org.springframework.context.ApplicationContext;
import org.springframework.context.support.ClassPathXmlApplicationContext;
import xyz.ssydx.service.UserService;
public class TestAOP01 {
public static void main(String[] args) {
ApplicationContext context = new ClassPathXmlApplicationContext("beans01.xml");
UserService userService = context.getBean("userServiceImpl", UserService.class);
userService.add();
userService.delete();
userService.update();
userService.select();
/*
控制台输出如下(userService.update() 未抛出异常时):
Before Method: add
Around Before Method: add
增加用户
Around After Method: add
After Method: add
AfterReturning Method: add
Before Method: delete
Around Before Method: delete
删除用户
Around After Method: delete
After Method: delete
Around Before Method: update
更新用户
Around After Method: update
After Method: update
AfterReturning Method: update
Around Before Method: select
查询用户
Around After Method: select
After Method: select
控制台输出如下(userService.update() 抛出异常时):
Before Method: add
Around Before Method: add
增加用户
Around After Method: add
After Method: add
AfterReturning Method: add
Before Method: delete
Around Before Method: delete
删除用户
Around After Method: delete
After Method: delete
Around Before Method: update
更新用户
After Method: update
AfterThrowing Method: update throw: 参数错误
Exception in thread "main" java.lang.IllegalArgumentException: 参数错误
...省略异常堆栈
*/
}
}
spring-aop结合aspectjweaver(拓展知识)
Aspect_Before|Aspect_AfterReturning|Aspect_AfterThrowing|Aspect_Around
同前文
bean02.xml
<?xml version="1.0" encoding="UTF-8"?>
<!-- 注意: 此处标签属性有所不同 -->
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:aop="http://www.springframework.org/schema/aop"
xsi:schemaLocation="
http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/aop
http://www.springframework.org/schema/aop/spring-aop.xsd
">
<bean id="userServiceImpl" class="xyz.ssydx.service.UserServiceImpl" />
<bean id="aspect_Before" class="xyz.ssydx.aspect.Aspect_Before" />
<bean id="aspect_AfterReturning" class="xyz.ssydx.aspect.Aspect_AfterReturning" />
<bean id="aspect_AfterThrowing" class="xyz.ssydx.aspect.Aspect_AfterThrowing" />
<bean id="aspect_Around" class="xyz.ssydx.aspect.Aspect_Around" />
<aop:config>
<aop:pointcut id="all" expression="execution(* xyz.ssydx.service.UserServiceImpl.*(..))"/>
<aop:pointcut id="update" expression="execution(* xyz.ssydx.service.UserServiceImpl.update(..))"/>
<aop:pointcut id="add_and_delete" expression="execution(* xyz.ssydx.service.UserServiceImpl.add(..)) || execution(* xyz.ssydx.service.UserServiceImpl.delete(..))"/>
<aop:pointcut id="add_and_update" expression="execution(* xyz.ssydx.service.UserServiceImpl.add(..)) || execution(* xyz.ssydx.service.UserServiceImpl.update(..))"/>
<aop:advisor advice-ref="aspect_Before" pointcut-ref="add_and_delete" />
<aop:advisor advice-ref="aspect_AfterReturning" pointcut-ref="add_and_update" />
<aop:advisor advice-ref="aspect_AfterThrowing" pointcut-ref="update" />
<aop:advisor advice-ref="aspect_Before" pointcut-ref="all" />
</aop:config>
</beans>
TestAOP02
import org.springframework.context.ApplicationContext;
import org.springframework.context.support.ClassPathXmlApplicationContext;
import xyz.ssydx.service.UserService;
public class TestAOP02 {
public static void main(String[] args) {
ApplicationContext context = new ClassPathXmlApplicationContext("beans02.xml");
UserService userService = context.getBean("userServiceImpl", UserService.class);
userService.add();
userService.delete();
userService.update();
userService.select();
/*
控制台输出如下:
同前文(spring-aop)的 TestAOP01
*/
}
}
此处的结合指的是结合两种不同的 AOP 风格,实际 aspectjweaver 同样需要 spring-aop 的支持才能实现动态代理
基于Spring注解的AOP
UserService
见前文
UserServiceImpl
仅仅增加了一个 @Service 注解
package xyz.ssydx.service;
import org.springframework.stereotype.Service;
@Service
public class UserServiceImpl implements UserService {
@Override
public void add() {
System.out.println("增加用户");
}
@Override
public void delete() {
System.out.println("删除用户");
}
@Override
public void update() {
System.out.println("更新用户");
// 不妨尝试抛出异常, 观察 TestAOP01 执行结果有什么不同
// throw new IllegalArgumentException("参数错误");
}
@Override
public void select() {
System.out.println("查询用户");
}
}
Aspect02
package xyz.ssydx.aspect;
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.*;
import org.springframework.stereotype.Component;
// 声明这是一个切面类
@Aspect
// 让 Spring 自动扫描并注册为 Bean
@Component
public class Aspect02 {
// 定义切入点表达式
@Pointcut("execution(* xyz.ssydx.service.UserServiceImpl.*(..))")
public void allMethods() {}
@Pointcut("execution(* xyz.ssydx.service.UserServiceImpl.add(..))")
public void addMethod() {}
@Pointcut("execution(* xyz.ssydx.service.UserServiceImpl.delete(..))")
public void deleteMethod() {}
@Pointcut("execution(* xyz.ssydx.service.UserServiceImpl.update(..))")
public void updateMethod() {}
@Pointcut("addMethod() || deleteMethod()")
public void addAndDelete() {}
@Pointcut("addMethod() || updateMethod()")
public void addAndUpdate() {}
@Before("addAndDelete()")
public void before(JoinPoint joinPoint) {
System.out.println("Before Method: " + joinPoint.getSignature().getName());
}
// 定义通知的同时绑定切点
@AfterReturning(pointcut = "addAndUpdate()", returning = "result")
public void afterReturning(JoinPoint joinPoint, Object result) {
System.out.println("AfterReturning Method: " + joinPoint.getSignature().getName());
}
@AfterThrowing(pointcut = "updateMethod()", throwing = "ex")
public void afterThrowing(JoinPoint joinPoint, Throwable ex) {
System.out.println("AfterThrowing Method: " + joinPoint.getSignature().getName() + " throw: " + ex.getMessage());
}
// 后置通知(无论成功或异常都会执行)
@After("allMethods()")
public void after(JoinPoint joinPoint) {
System.out.println("After Method: " + joinPoint.getSignature().getName());
}
@Around("allMethods()")
public Object around(ProceedingJoinPoint proceedingJoinPoint) throws Throwable {
System.out.println("Around Before Method: " + proceedingJoinPoint.getSignature().getName());
Object result = proceedingJoinPoint.proceed();
System.out.println("Around After Method: " + proceedingJoinPoint.getSignature().getName());
return result;
}
}
通过 @Order 注解可以实现自定义切面执行顺序(当有多个同类通知作用在同一个类方法上时需要区分顺序)
AllConfig
package xyz.ssydx.config;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.EnableAspectJAutoProxy;
@Configuration
// 开启切面自动代理, 相当于 beans.xml 中的 <aop:aspectj-autoproxy />
@EnableAspectJAutoProxy
@ComponentScan(basePackages = {"xyz.ssydx.aspect", "xyz.ssydx.service"})
public class AllConfig {
}
TestAOP03
import org.springframework.context.ApplicationContext;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
import xyz.ssydx.config.AllConfig;
import xyz.ssydx.service.UserService;
public class TestAOP03 {
public static void main(String[] args) {
ApplicationContext context = new AnnotationConfigApplicationContext(AllConfig.class);
UserService userService = context.getBean(UserService.class);
userService.add();
userService.delete();
userService.update();
userService.select();
/*
控制台输出如下:
同 TestAOP01
*/
}
}
Spring 注解 vs XML 标签对照表
常用
配置 | @Configuration |
整个 XML 文件本身就是配置 |
注解驱动 | @ComponentScan 和 @Configuration |
<context:annotation-config /> |
组件扫描 | @ComponentScan() |
<context:component-scan base-package="" /> |
自动注入 | @Autowired @Resource @Inject |
<bean autowire="" /> |
指定注入 | @Qualifier() |
<qualifier type="" value="" /> |
字段赋值 | @Value() |
<property name="" value="" /> |
定义 Bean | @Bean |
<bean id="" class="" /> |
视图控制层 Bean | @Controller @RestController |
同上 |
业务逻辑层 Bean | @Service |
同上 |
数据访问层 Bean | @Repository |
同上 |
设置名称 | @Component() @Service() ... |
<bean id=" " name="" /> |
设置作用域 | @Scope() |
<bean scope="" /> |
声明主 Bean | @Primary |
<bean primary="true" /> |
初始化方法 | @PostConstruct |
<bean init-method="" /> |
销毁方法 | @PreDestroy |
<bean destroy-method="" /> |
自动代理 | @EnableAspectJAutoProxy |
<aop:aspectj-autoproxy /> |
切面顺序 | @Order() |
<aop:aspect order="" /> |
声明切面 | @Aspect 和 @Component |
<aop:aspect ref="" /> |
切面名称 | @Component("myAspect") |
<bean id="" name="" /> |
切点 | @Pointcut() |
<aop:pointcut id="" expression="" /> |
切点组合 | @Pointcut() |
不支持 |
前置通知 | @Before() |
<aop:before method="" pointcut-ref="" /> |
后置通知 | @After() |
<aop:after method="" pointcut-ref="" /> |
环绕通知 | @Around() |
<aop:around method="" pointcut-ref="" /> |
后置返回通知 | @AfterReturning(pointcut = "", returning = "result") |
<aop:after-returning method="" pointcut-ref="" returning="result" /> |
后置异常通知 | @AfterThrowing(pointcut = "", throwing = "ex") |
<aop:after-throwing method="" pointcut-ref="" throwing="ex" /> |
代理方式 | `@EnableAspectJAutoProxy(proxyTargetClass="true | false")` |
其他
导入配置 | @Import({ConfigA.class, ConfigB.class}) |
<import resource="classpath:other-config.xml" /> |
事务管理 | @EnableTransactionManagement |
<tx:annotation-driven /> |
定时任务 | @EnableScheduling |
<task:annotation-driven /> |
异步方法 | @EnableAsync |
<task:annotation-driven executor="myExecutor" scheduler="myScheduler"/> |
外部文件(properties) | @PropertySource("classpath:app.properties") |
<context:property-placeholder location="classpath:app.properties"/> |
加载外部属性文件(YAML 不支持) | PropertySourcesPlaceholderConfigurer |
<context:property-placeholder location="classpath:app.properties"/> |
条件加载 | @ConditionalOnClass , @ConditionalOnMissingBean |
可通过自定义 Condition 实现,或使用 <beans profile="dev"> |
激活特定 Profile | @Profile("dev") 或者启动时设置 spring.profiles.active=dev |
<beans profile="dev"> |
定义 Bean 工厂方法 | @Bean(name = "xxx", factoryMethod = "createBean") |
<bean factory-method="createBean" class="com.example.BeanFactory"/> |
定义静态工厂方法创建 Bean | @Bean 结合静态方法 |
<bean factory-method="createBean" class="com.example.BeanFactory"/> |
定义实例工厂方法创建 Bean | 在配置类中先注入工厂 Bean,再调用其方法 | <bean id="factory" class="com.example.BeanFactory"/>``<bean factory-bean="factory" factory-method="createBean"/> |
注册事件监听器 | @EventListener |
使用 ApplicationListener 接口实现,或者 <bean> 注册监听器 |
集成开发
集成Mybatis
依然创建一个模块
注意:该示例有一定难度,且并未逐步深入,学习时不可直接照搬
载入依赖
<dependencies>
<!-- mysql 数据库驱动 -->
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>8.0.33</version>
</dependency>
<!-- mybatis ORM框架 -->
<dependency>
<groupId>org.mybatis</groupId>
<artifactId>mybatis</artifactId>
<version>3.5.16</version>
</dependency>
<!-- 可整合进 spring 的 mybatis -->
<dependency>
<groupId>org.mybatis</groupId>
<artifactId>mybatis-spring</artifactId>
<version>2.1.2</version>
</dependency>
<!-- 测试框架 -->
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>4.13.2</version>
<scope>test</scope>
</dependency>
<!-- 需要使用其中的事务支持 -->
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-jdbc</artifactId>
<version>5.3.20</version>
</dependency>
<!-- 需要使用它的语言风格 -->
<dependency>
<groupId>org.aspectj</groupId>
<artifactId>aspectjweaver</artifactId>
<version>1.9.23</version>
</dependency>
</dependencies>
<build>
<!-- 明确资源打包时包含 resources 目录和 java 目录下的 xml 文件 -->
<resources>
<resource>
<directory>src/main/resources</directory>
<includes>
<include>**/*.xml</include>
</includes>
</resource>
<resource>
<directory>src/main/java</directory>
<includes>
<include>**/*.xml</include>
</includes>
</resource>
</resources>
</build>
spring-mybatis.xml
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="
http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans.xsd">
<bean id="dataSource" class="org.springframework.jdbc.datasource.DriverManagerDataSource">
<property name="driverClassName" value="com.mysql.cj.jdbc.Driver" />
<property name="url" value="jdbc:mysql://localhost:3306/mybatis" />
<property name="username" value="ssydx" />
<property name="password" value="" />
</bean>
<bean id="sqlSessionFactory" class="org.mybatis.spring.SqlSessionFactoryBean">
<property name="dataSource" ref="dataSource" />
<property name="typeAliasesPackage" value="xyz.ssydx.pojo" />
<property name="mapperLocations" value="classpath:xyz/ssydx/mapper/*.xml" />
</bean>
<bean id="sqlSessionTemplate" class="org.mybatis.spring.SqlSessionTemplate">
<constructor-arg index="0" ref="sqlSessionFactory" />
</bean>
<!-- 启用注解扫描, 启用后不必再自行创建实现 mapper 接口的类 -->
<!-- <bean class="org.mybatis.spring.mapper.MapperScannerConfigurer">-->
<!-- <!– 指定要扫描的包路径,可以是多个,用逗号分隔 –>-->
<!-- <property name="basePackage" value="xyz.ssydx.mapper"/>-->
<!-- <!– 如果你的 SqlSessionFactory Bean 名称不是默认的 'sqlSessionFactory',需要指定 –>-->
<!--<!– <property name="sqlSessionFactoryBeanName" value="sqlSessionFactory"/>–>-->
<!-- <!– 如果你使用的是 SqlSessionTemplate 而不是 SqlSessionFactory,可以指定它的名称 –>-->
<!-- <property name="sqlSessionTemplateBeanName" value="sqlSessionTemplate"/>-->
<!-- </bean>-->
</beans>
applicationContext.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: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/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
">
<!-- 载入其他配置文件 -->
<import resource="spring-mybatis.xml" />
<bean id="userMapper01" class="xyz.ssydx.mapper.UserMapperImpl01">
<constructor-arg name="sqlSessionTemplate" ref="sqlSessionTemplate" />
</bean>
<bean id="userMapper02" class="xyz.ssydx.mapper.UserMapperImpl02">
<property name="sqlSessionFactory" ref="sqlSessionFactory" />
</bean>
<bean id="transactionManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
<constructor-arg name="dataSource" ref="dataSource" />
</bean>
<!-- 启用注解驱动的事务管理 -->
<!-- <tx:annotation-driven transaction-manager="transactionManager" />-->
<!-- 本质是一个 spring-aop 风格的通知器 -->
<tx:advice id="txAdvice" transaction-manager="transactionManager">
<tx:attributes>
<tx:method name="addAndDelete" propagation="REQUIRED"/>
</tx:attributes>
</tx:advice>
<!-- spring-aop 风格 -->
<bean id="pointcut01" class="org.springframework.aop.support.NameMatchMethodPointcut">
<property name="mappedNames" value="addAndDelete" />
</bean>
<bean id="aspect01" class="org.springframework.aop.support.DefaultBeanFactoryPointcutAdvisor">
<property name="pointcut" ref="pointcut01"/>
<property name="advice" ref="txAdvice"/>
</bean>
<bean id="userServiceProxy" class="org.springframework.aop.framework.ProxyFactoryBean">
<property name="target">
<bean class="xyz.ssydx.service.UserService">
<constructor-arg type="xyz.ssydx.mapper.UserMapper" ref="userMapper01"/>
</bean>
</property>
<property name="interceptorNames" value="aspect01" />
</bean>
<!-- aspectj 风格 -->
<!-- <aop:config>-->
<!-- <aop:pointcut id="pointcut01" expression="execution(* xyz.ssydx.service.*.*(..))"/>-->
<!-- 整合spring-aop风格的通知器(实现通知接口的类的 bean) -->
<!-- <aop:advisor pointcut-ref="pointcut01" advice-ref="txAdvice" />-->
<!-- </aop:config>-->
</beans>
User
可自行选择映射数据库中的其他数据表
package xyz.ssydx.pojo;
public class User {
private int id;
private String name;
private String password;
// 省略无参构造器、全参构造器、getter、setter、toString
}
UserMapper
package xyz.ssydx.mapper;
import xyz.ssydx.pojo.User;
import java.util.List;
public interface UserMapper {
public List<User> findAll();
public void save(User user);
public void delete(int id);
}
UserMapper.xml
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper
PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="xyz.ssydx.mapper.UserMapper">
<select id="findAll" resultType="user">
select * from user_info;
</select>
<insert id="save" parameterType="user">
insert into user_info(name, password) values (#{name}, #{password});
</insert>
<delete id="delete" parameterType="int">
delete from user_info where id = #{id};
</delete>
</mapper>
UserMapperImpl01
package xyz.ssydx.mapper;
import org.mybatis.spring.SqlSessionTemplate;
import xyz.ssydx.pojo.User;
import java.util.List;
public class UserMapperImpl01 implements UserMapper {
private SqlSessionTemplate sqlSessionTemplate;
public UserMapperImpl01(SqlSessionTemplate sqlSessionTemplate) {
this.sqlSessionTemplate = sqlSessionTemplate;
}
@Override
public List<User> findAll() {
return sqlSessionTemplate.getMapper(UserMapper.class).findAll();
}
@Override
public void save(User user) {
sqlSessionTemplate.getMapper(UserMapper.class).save(user);
}
@Override
public void delete(int id) {
sqlSessionTemplate.getMapper(UserMapper.class).delete(id);
}
}
UserMapperImpl02
package xyz.ssydx.mapper;
import org.mybatis.spring.SqlSessionTemplate;
import org.mybatis.spring.support.SqlSessionDaoSupport;
import xyz.ssydx.pojo.User;
import java.util.List;
public class UserMapperImpl02 extends SqlSessionDaoSupport implements UserMapper {
@Override
public List<User> findAll() {
return getSqlSession().getMapper(UserMapper.class).findAll();
}
@Override
public void save(User user) {
getSqlSession().getMapper(UserMapper.class).save(user);
}
@Override
public void delete(int id) {
getSqlSession().getMapper(UserMapper.class).delete(id);
}
}
UserService
package xyz.ssydx.service;
import org.springframework.transaction.annotation.Transactional;
import xyz.ssydx.mapper.UserMapper;
import xyz.ssydx.pojo.User;
public class UserService {
private UserMapper userMapper;
public UserService(UserMapper userMapper) {
this.userMapper = userMapper;
}
// @Transactional
public void addAndDelete(User user, int id) {
userMapper.save(user);
if (id < 10) {
throw new RuntimeException("未知错误");
}
userMapper.delete(id);
}
}
TestMybatis01
import org.junit.Test;
import org.springframework.context.ApplicationContext;
import org.springframework.context.support.ClassPathXmlApplicationContext;
import xyz.ssydx.mapper.UserMapper;
import xyz.ssydx.pojo.User;
import java.io.IOException;
import java.util.List;
public class TestMybatis01 {
@Test
public void test() throws IOException {
ApplicationContext context = new ClassPathXmlApplicationContext("applicationContext.xml");
UserMapper userMapper01 = context.getBean("userMapper01", UserMapper.class);
List<User> users01 = userMapper01.findAll();
for (User user : users01) {
System.out.println(user);
}
UserMapper userMapper02 = context.getBean("userMapper02", UserMapper.class);
List<User> users02 = userMapper01.findAll();
for (User user : users02) {
System.out.println(user);
}
/*
控制台输出如下(仅供示例):
User{id=1, name='zhangsan', password='zhangsan123456'}
User{id=2, name='lisi', password='lisi123456'}
User{id=3, name='wangwu', password='wangwu123456'}
*2
*/
}
}
TestMybatis02
import org.junit.Test;
import org.springframework.context.ApplicationContext;
import org.springframework.context.support.ClassPathXmlApplicationContext;
import xyz.ssydx.mapper.UserMapper;
import xyz.ssydx.pojo.User;
import xyz.ssydx.service.UserService;
import java.io.IOException;
import java.util.List;
public class TestMybatis02 {
@Test
public void test() throws IOException {
ApplicationContext context = new ClassPathXmlApplicationContext("applicationContext.xml");
UserService userService = context.getBean(UserService.class);
User user = new User();
user.setName("ssydx");
user.setPassword("ssydx123456");
userService.addAndDelete(user, 4);
/*
控制台输出如下:
java.lang.RuntimeException: 未知错误
at xyz.ssydx.service.UserService.addAndDelete(UserService.java:16)
...省略异常堆栈
查看数据会发现数据并未插入, 被自动回滚了
*/
}
}
进阶必备(拓展知识)
AOP
动态代理对比
核心机制 | 动态生成子类并重写方法 | 字节码增强,插入切面逻辑 | 使用 JDK 动态代理或 CGLIB 生成代理类 |
是否修改原始类字节码 | 否(只生成子类) | 是(直接修改原类) | 否(使用代理类) |
是否需要接口支持 | 否(通过继承实现) | 否(可对任意类生效) | 是(JDK 动态代理需接口),否(CGLIB 不需要) |
是否创建新类 | 是(生成子类) | 否(修改原类) | 是(生成代理类) |
是否运行时生效 | 是 | 是(LTW),也可以是编译期生效 | 是 |
拦截能力 | 仅 public 方法 | 所有方法(private/static/构造函数) | 仅 public 方法 |
是否依赖 Spring | 否 | 否(但 Spring 可以整合它) | 是 |
是否影响 IOC 容器行为 | 是(可能引入两个 Bean) | 否(只有一个 Bean) | 是(代理类和目标类共存) |
适用场景 | 简单代理需求,Spring AOP 内部使用 | 复杂 AOP 场景,如日志、事务、权限等 | IOC 容器内的 AOP 需求 |
JDK 动态代理之所以必须依赖接口,是因为 Proxy 类在设计之初就只支持对接口进行代理,这是语言层面的设计选择,而不是实现上的限制,即Proxy 已经继承了 java.lang.reflect.Proxy,它不能再继承其他类(比如想要代理的 UserService),所以它只能实现接口。
JDK 动态代理生成的代理类是一个“既实现了目标接口、又继承自 Proxy 类”的特殊类。
在进行 AOP 时:JDK 动态代理会创建目标类和代理类;AspectJ 则只创建一个被修改后的原类;CGLIB 则同时创建父类和子类。
一个不算很棘手的小问题
通过 spring-aop 风格在 xml 配置的会造成目标类(父类)和代理类(子类)并行存在,浪费资源,还易造成误操作
该问题的本质是 spring-aop 风格的代理不是真正意义上的动态代理,还是手动配置了代理类
解决办法(推荐第一种,不会占用ioc容器额外的空间)
1.使用注解风格或aop系列标签(注意,无论注解还是aop标签其本质上都还是基于jdk和cglib代理,这点极易造成误解,之所以使用这两者需要引入apsectj包,仅仅是为了使用这两者罢了,并未真的使用aspectj的代理方式),这样spring在注册bean时会隐式的把目标类(父类)抛弃,只把代理类(子类)放入ioc容器
2.<bean id="userService" class="org.springframework.aop.framework.ProxyFactoryBean"> <property name="interceptorNames" value="txAdvice"/> <property name="target"> <bean class="xyz.ssydx.service.UserService"/> </property> </bean>
使用内部bean 可以避免其被实际注册(存在于目标类的bean中,外部不可访问)
总结
JDK 动态代理必须依赖接口,这是 Java 原生语言设计决定的; CGLIB 是通过继承目标类生成子类实现代理; Spring AOP 虽然借用了 AspectJ 的语法,但本质仍是 JDK/CGLIB 代理; 只有使用自动代理机制(注解驱动或 aop:config)才能真正做到只保留代理类,避免目标类污染容器。
Mybatis
接口实现
手动实现接口(内部调用SqlSession去动态代理接口),这个过程比较绕,其实是两层,先自己静态代理了接口,后调用SqlSession再去动态代理接口,第一层是不必要存在的
如果借助注解扫描或xml扫描,底层会直接调用SqlSession去动态代理接口
无论是手动写的静态代理类,还是 Spring 自动生成的动态代理类,它们的本质都是为了把接口的方法映射到 XML(或注解)定义的 SQL 上。
SqlSessionTemplate 是对 SqlSession 的一个线程安全、Spring 管理的封装版本
Properties | 属性文件 | key=value 格式 | 基础配置、环境变量 | 简单易懂,但嵌套差 |
YAML (yml) | YAML Ain't Markup Language | 缩进结构 | 多环境配置、Spring Boot 推荐 | 支持嵌套,结构清晰 |
XML | Extensible Markup Language | 标签结构 | Spring 早期核心配置 | 强大但繁琐 |
Java Config | Java-based Configuration | Java 类 + 注解 | Spring Boot 默认方式 | 类型安全、可编程性强 |
数据源
数据源是数据库连接的管理者,负责数据库连接的创建销毁复用检测等
连接池管理 | 支持(POOLED / JNDI) | 强大(HikariCP、Druid 等) |
事务管理 | 手动控制 | 声明式事务(@Transactional ) |
依赖注入 | 不支持 | 全面支持(@Autowired ) |
多数据源支持 | 需手动切换 | 易于配置多个 DataSource |
自动装配与整合 | 需要手动绑定 SqlSessionFactory 等 | Spring Boot 自动配置 |
监控与运维 | 基本无 | 可接入 Actuator、Druid 监控面板 |
可配置性 | XML 配置有限 | 支持 Java Config + YAML/properties |
生命周期管理 | 需自行管理 | Spring 容器统一管理 Bean 生命周期 |
本专栏包含Java、MySQL、JavaWeb、Spring、Redis、Docker等等,作为个人学习记录及知识总结,将长期进行更新! 如果浏览、点赞、收藏、订阅过少,考虑停更或删除了(😄有点打击积极性)