Spring
概述
什么是 Spring?
Spring 是一个开源的 Java 应用程序框架,用于构建企业级应用程序。它提供了全面的基础设施支持,比如:依赖注入(Ioc)、依赖注入(DI)、AOP(面向切面编程) 等功能,让开发者可以更加专注于对业务逻辑的实现。
Spring 的优点
-
提供依赖注入(DI):
使得 Spring 维护不同组件之间的依赖关系,降低组件之间的耦合度。
-
面向切面编程(AOP):
开发者可以将不同功能抽象成切面,并可以将这些切面和不同的组件关联起来,从而提高代码重用率和可维护性
-
提供 Ioc 容器:
Spring 提供的 Ioc 容器可以对不同组件进行管理,并支持对组件的 AOP 增强,从而实现程序的解耦和高度可配置性。
-
降低了对其他框架的整合难度。
-
支持声明式事务管理:
开发者可以通过配置来管理程序中的事务,简化事务管理过程。
-
便于测试
Spring 可以方便的进行单元测试和集成测试,提高代码的可测试性和可靠性。
Spring 八大模块
Spring Webflux模块是在 Spring 5 的时候添加的。
Spring 的核心模块是 Spring Core 和 Spring AOP。Spring AOP 又是基于 Spring Core 实现的,因此Spring Core 是整个 Spring 的基石。Spring Core 是 Spring 中最基础的部分,它提供了 依赖注入特征 来实现容器对 Bean 的管理。核心容器的主要组件是 BeanFactory,BeanFactory是工厂模式的一个实现,是任何 Spring 应用的核心部分,它使用 IoC 将应用配置和依赖从实际的应用代码中分离出来。
入门项目的搭建
-
导入依赖
<!-- https://mvnrepository.com/artifact/org.springframework/spring-context --> <dependency> <groupId>org.springframework</groupId> <artifactId>spring-context</artifactId> <version>5.3.18</version> </dependency>
对于 Spring 6 需要使用 Java 17
<properties> <maven.compiler.source>17</maven.compiler.source> <maven.compiler.target>17</maven.compiler.target> <java.version>17</java.version> </properties>
-
在
/src/main/resouces
下创建 Spring 的核心配置文件:spring-config.xml
<?xml version="1.0" encoding="UTF-8"?> <beans xmlns="http://www.springframework.org/schema/beans" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd"> </beans>
-
编写一个用于测试的实体类
User.java
package com.guyi.spring.model.entity; public class User { /** * 用户名 */ private String username; /** * 用户编号 */ private Integer no; // todo // get、set、toString 方法自行补充 }
-
在
spring-config.xml
注册 User Bean
<bean id="userBean" class="com.guyi.spring.model.entity.User"/>
这里的 id 就是 Bean 的名称id 不能重复
-
编写测试 demo
@Test public void testHelloSpringTest() { // 加载 Spring 配置文件, 这种方式只适用于配置文件在类路径的情况 ClassPathXmlApplicationContext classPathXmlApplicationContext = new ClassPathXmlApplicationContext("spring-config.xml"); // 根据 beanId 从 Spring 容器中获取对象 Object userBean = classPathXmlApplicationContext.getBean("userBean"); User user = (User) userBean; user.setUsername("张三"); user.setNo(1); System.out.println(userBean); }
前瞻
-
Spring 默认调用类的无参构造方法来实例化对象
只要类中有公共的构造器,那就可以使用 Spring 实例化
-
对象创建好后,会存储到一个 Map<String, Object> 集合中
-
可以指定多个 Spring 配置文件
-
如果 Bean 的 id 不存在,会出现异常:
NoSuchBeanDefinitionException
-
使用 Spring 实例化 Date 对象时,需要对这个对象进行日期格式化并且对得到的对象进行类型强制转化,或者使用如下代码:
/* 第一个参数传 bean 的 id 第二个参数传要的期望得到类型的clss */ Date nowTime = applicationContext.getBean("nowTime", Date.class)
-
如果 Spring 配置文件不在类路径下,使用如下代码加载:
ApplicationContext applicationContext = new FileSystemXmlApplicationContext("path");
Spring 启动 Log4j2 日志
-
导入依赖
<!-- log4j2日志依赖 --> <dependency> <groupId>org.apache.logging.log4j</groupId> <artifactId>log4j-core</artifactId> <version>2.19.0</version> </dependency> <dependency> <groupId>org.apache.logging.log4j</groupId> <artifactId>log4j-slf4j2-impl</artifactId> <version>2.19.0</version> </dependency>
-
在
src/main/resouces
下提供log4j2.xml
配置文件及相关配置,注意:配置文件的名字和位置都是固定的!!!
<?xml version="1.0" encoding="UTF-8"?> <configuration> <Loggers> <root level="DEBUG"> <appenderRef ref="console"/> </root> </Loggers> <appenders> <console name="console" target="SYSTEM_OUT"> <PatternLayout pattern="%d{yyyy-MM-dd HH:mm:ss.SSS} [%t] %-3level %logger{1024} - %msg%n"/> </console> </appenders> </configuration>
-
测试日志框架是否引入成功
@Test public void testLog4j2() { // 创建日志记录器对象 // 获取 HelloDemo 类的日志记录器对象, 也就是这个类中的代码执行记录日志的话, 就输出相应信息 Logger logger = LoggerFactory.getLogger(HelloDemo.class); // 记录日志, 根据不同级别输出日志 logger.info("我是一条消息"); logger.debug("我是一条调试消息"); logger.error("我是一条错误消息"); }
对于日志记录器对象一般设置为常量
Spring 对 Ioc 的实现
声明:之后的代码运行结果截图可能是不完整的,对于 Bean 创建的日志信息不会截下来,只截我自己设定的输出,类似下面的信息不会出现在运行结果截图中:
IoC 概述
-
Ioc 是一种编程思想,是一种设计模式;
-
在程序中不采用硬编码方式实例化对象,也不采用硬编码的方式维护对象与对象之间的依赖关系,即反转对象的创建权和对象和对象之间的维护权。
-
Ioc 降低了程序的耦合度,符合 OPC 和 DIP 原则。
-
Ioc 通过 依赖注入(DI) 实现。
依赖注入
Spring 通过依赖注入的方式完成 Bean 管理,即完成对 Bean 对象的创建以及对象属性的赋值,也可以说是对 Bean 对象之间关系的维护。
实现依赖注入的方式
-
set 方式注入:使用 set 方法来完成依赖的注入
-
构造注入:使用构造器来完成依赖注入
-
属性注入:在 xml 中使用
property
标签的value
属性进行注入或者在属性上使用注解时,依赖的注入方式就为属性注入
注入简单数据类型
简单数据类型如下:
-
基本数据类型的包装类
-
枚举类型
-
实现了
CharSequence
接口的类,比如:String
-
实现了
Number
接口的类,比如:Integer
。
只要是数字即可。
-
Date 类型:java.util.Date
-
Class 类型
-
Temporal 类型:java8新特性,时间时区
-
URI 类型
-
URL 类型
-
Locale 类型
以 User.java
为例,User 类中有如下的实例变量:
-
String username
-
Integer no
这些都是简单数据类型的实例变量,在注册 Bean 的时候,可以使用 prooerty
标签的 name
属性指定对应实例变量,value
属性指定要赋给队友实例变量的值,如下:
<bean id="userBean2" class="com.guyi.spring.model.entity.User"> <property name="username" value="张三"/> <property name="no" value="20"/> </bean>
测试一下
/** * 测试注入简单类型数据 */ @Test public void testSimpleData() { ClassPathXmlApplicationContext classPathXmlApplicationContext = new ClassPathXmlApplicationContext("spring-config.xml"); Object userBean = classPathXmlApplicationContext.getBean("userBean2"); // 注意这里并没有给对象赋值 System.out.println(userBean); }
对于 Date,我们一般不将其视为简单数据类型来注入,因为使用注入简单类型数据的方式注入 Date 对象,注入形式比较复杂
<bean id="" class=""> <!-- 如果把 Date 当成简单类型, 那 value 属性值很复杂 一般不把Date当简单数据类型--> <property name="birth" value="Wed Oct 19 16:28:13 CST 2023"/> </bean>
"Wed Oct 19 16:28:13 CST 2023" 这一串时间字符串不符合我们的使用习惯
简单数据类型的应用
注入一个数据源
-
自定义一个数据源
import javax.sql.DataSource; import java.io.PrintWriter; import java.sql.Connection; import java.sql.SQLException; import java.sql.SQLFeatureNotSupportedException; import java.util.logging.Logger; /** * 数据源 * * 所有数据源都要实现 javax.sql.DataSource * 数据源能够提供 Connection 对象 */ public class MyDataSource implements DataSource { // 把数据源交给 Spring 管理 /** * 驱动 */ private String driver; /** * 链接 */ private String url; /** * 用户名 */ private String username; /** * 密码 */ private String password; @Override public Connection getConnection() throws SQLException { // 获取连接对象需要的信息:driver、url、username、password return null; } public void setDriver(String driver) { this.driver = driver; } public void setUrl(String url) { this.url = url; } public void setUsername(String username) { this.username = username; } public void setPassword(String password) { this.password = password; } @Override public String toString() { return "MyDataSource{" + "driver='" + driver + '\'' + ", url='" + url + '\'' + ", username='" + username + '\'' + ", password='" + password + '\'' + '}'; } @Override public Connection getConnection(String username, String password) throws SQLException { return null; } @Override public PrintWriter getLogWriter() throws SQLException { return null; } @Override public void setLogWriter(PrintWriter out) throws SQLException { } @Override public void setLoginTimeout(int seconds) throws SQLException { } @Override public int getLoginTimeout() throws SQLException { return 0; } @Override public Logger getParentLogger() throws SQLFeatureNotSupportedException { return null; } @Override public <T> T unwrap(Class<T> iface) throws SQLException { return null; } @Override public boolean isWrapperFor(Class<?> iface) throws SQLException { return false; } }
-
在
spring-config.xml
注册数据源对于的 Bean:
<!-- Spring 管理数据源 --> <bean id="myDataSourceBean" class="com.guyi.spring.datasource.MyDataSource"> <property name="driver" value="com.mysql.cj.jdbc.driver"/> <property name="url" value="jdbc:mysql://127.0.0.1:3306:/spring6"/> <property name="username" value="root"/> <property name="password" value="root"/> </bean>
这里的自定义数据源没有具体的实现细节,无法测试。以后开发中可以放入 Druid 数据库连接池时,可以参考这里的做法。
注入 Date
-
配置日期格式,在
spring-config.xml
中添加如下内容:
<bean id="conversionService" class="org.springframework.format.support.FormattingConversionServiceFactoryBean"> <property name="converters"> <set> <bean class="org.springframework.format.datetime.DateFormatter"> <property name="pattern" value="yyyy-MM-dd"/> </bean> </set> </property> </bean>
-
注入 Date 类型数据
<bean id="myDate" class="java.util.Date"> <constructor-arg value="2022-01-01"/> </bean> <bean id="myBean" class="com.example.MyBean"> <property name="date" ref="myDate"/> </bean>
注入非简单数据类型
注入非简单数据类型,需要使用 property
标签的 ref
来注入,
-
创建一个
MyKey
类,表示现实生活中的钥匙,用于打开锁
/** * 模拟现实中的钥匙 */ public class MyKey { // 暂时什么也没有 }
-
创建一个
MyLock
类,表示现实生活中的锁,MyLock
有一个key
属性对应打开它的钥匙:
/** * 模拟现实中的锁 */ public class MyLock { /** * 能够打开锁的钥匙 */ private MyKey key; // todo // set、get、toString 方法 }
-
在
spring-config.xml
中添加如下 Bean
<bean id="myKeyBean" class="com.guyi.spring.model.entity.MyKey"/> <bean id="myLockBean" class="com.guyi.spring.model.entity.MyLock"> <property name="key" ref="myKeyBean"/> </bean>
注意
<property name="key" ref="myKeyBean"/>
用的是ref
属性
-
测试
set 注入
上边对 简单数据类型 和 非简单数据类型 的注入演示,使用的都是 set 注入。使用 set 注入,必须提供无参构造和 set 方法!因为 set 注入是先调用无参构造进行实例化对象,在调用对应的 set 方法来注入依赖的。
外部注入
外部 Bean 的特点:要注入的 Bean 定义在外面,在 property
标签中使用 ref
属性进行注入,是常用的注入方式。
-
创建一个
UserDao.java
类
public class UserDao { }
-
创建一个
OrderDao.java
类
public class OrderDao { /** * 假设 OrderDao 依赖于 userDao */ private UserDao userDao; public void addOrder() { System.out.println("正在为用户-" + userDao + "创建订单..."); System.out.println("新的订单创建成功..."); } // todo // set、get、toString 方法 }
-
在
spring-config.xml
中注册两个 Bean:
<bean id="userDaoBean" class="com.guyi.spring.dao.UserDao" /> <bean id="orderDaoBean" class="com.guyi.spring.dao.OrderDao"> <property name="userDao" ref="userDaoBean" /> </bean>
property
是用来给 Bean 的实例变量赋值的。name 属性用来指定实例变量,值为对应的实例变量名对于 orderDaoBean 来说,userDaoBean 是一个外部 Bean,向orderDaoBean 注入 userDaoBean,称为外部注入
-
测试
/** * 测试 set 注入,使用外部注入的方式实现 */ @Test public void testExterior() { ClassPathXmlApplicationContext classPathXmlApplicationContext = new ClassPathXmlApplicationContext("spring-config.xml"); Object orderDaoBean = classPathXmlApplicationContext.getBean("orderDaoBean"); OrderDao orderDao = (OrderDao) orderDaoBean; orderDao.addOrder(); }
外部注入
-
还是用
UserDao.java
和OrderDao.java
进行演示,在spring-config.xml
中新注册一个 Bean
<bean id="orderDaoBean2" class="com.guyi.spring.dao.OrderDao"> <property name="userDao"> <bean class="com.guyi.spring.dao.UserDao" /> </property> </bean>
注意:此时的 UserDao 对应的 Bean 注册在 orderDaoBean2 的内部,并且可以不设置 id 属性(设置了之后 IDE 也会提示是多余的)。此时,这个内部 Bean 是没办法复用的!!!
-
测试
小结
-
set 注入,是通过 set 方法完成依赖注入的,使用时必须确保类中有 set 方法。
-
外部注入可以实现对被依赖的 Bean 的复用,外部注入无法实现对被依赖的 Bean 的复用,推荐使用外部注入。
-
简单分析 set 注入的原理:
-
首先 Spring 通过反射机制实例化一个 Bean 对象(此时这个 Bean 是未初始化的,即实例变量没有注入对应的值),并获取其 Class 对象
-
Spring 根据得到的 Class 对象,获取到 Bean 对象所有的 set 方法
-
对于每个 set 方法,Spring 会解析方法名,提取方法名中 "set" 后面的部分作为属性名,将首字母转为小写
-
接下来,Spring 会判断 Bean 各个属性的类型,并根据类型来确定如何注入属性值:
-
基本类型(简单数据类型)注入:通过解析属性的 setter 方法参数类型,将配置文件中的值转换成对应的基本类型,并调用 setter 方法注入属性值。
-
引用类型注入:如果 setter 方法参数是其他 Bean 对象的类型,则 Spring 会通过容器中的 BeanFactory 获取对应的 Bean 对象,并调用 setter 方法注入引用。这样就实现了 Bean 之间的依赖注入
-
-
最后,Spring 会将属性值注入到 Bean 对象中,通过调用 setter 方法完成属性的设置
构造器注入
根据参数的位置注入
-
在
/src/main/resouces
下新创建一个 Spring 的配置文件:spring-config-constructor.xml
<?xml version="1.0" encoding="UTF-8"?> <beans xmlns="http://www.springframework.org/schema/beans" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd"> </beans>
-
在
User.java
中中添加一个全参数构造器
// username 的位置为 0;no 的位置为 1 public User(String username, Integer no) { this.username = username; this.no = no; }
注意:除了提供一个全参数构造器之外,还要提供一个无参构造器,否则再去运行 set 注入的例子,会报错的。
-
在
spring-config-constructor.xml
中注册一个 Bean:
<!-- 构造器注入:根据参数位置注入 --> <bean id="userConstructorIndexBean" class="com.guyi.spring.model.entity.User"> <constructor-arg index="0" value="张三"/> <constructor-arg index="1" value="1"/> </bean>
使用构造器注入,通过标签 constructor-arg
实现。可以通过 index
属性指明需要注入依赖的参数的下标,value/ref
属性来指定注入的值。
-
测试
/** * 构造器注入:根据参数位置注入 */ @Test public void testIndexInject() { ClassPathXmlApplicationContext classPathXmlApplicationContext = new ClassPathXmlApplicationContext("spring-config-constructor.xml"); Object userConstructorBean = classPathXmlApplicationContext.getBean("userConstructorIndexBean"); System.out.println(userConstructorBean); }
根据参数的名称注入
-
在
spring-config-constructor.xml
中注册一个 Bean:
<!-- 构造器注入:根据参数名称注入 --> <bean id="userConstructorNameBean" class="com.guyi.spring.model.entity.User"> <constructor-arg name="username" value="李四"/> <constructor-arg name="no" value="2"/> </bean>
通过标签 constructor-arg
标签的 name
属性指明需要注入依赖的参数的参数名,value/ref
属性来指定注入的值。
-
测试
/** * 构造器注入:根据参数名称注入 */ @Test public void testNameInject() { ClassPathXmlApplicationContext classPathXmlApplicationContext = new ClassPathXmlApplicationContext("spring-config-constructor.xml"); Object userConstructorBean = classPathXmlApplicationContext.getBean("userConstructorNameBean"); System.out.println(userConstructorBean); }
根据类型注入
这种方式不指定 index 属性,也不指定 name 属性,框架根据类型自动注入。
-
在
MyLock.java
中添加实例变量
/** * 锁的名称 */ private String name; /** * 锁的价格 */ private Integer price;
对应的 setter、getter、toString、无参构造器、全参数构造器请自行添加
-
在
spring-config-constructor.xml
中注册如下 Bean:
<!-- 构造器注入:根据参数类型注入 --> <bean id="myKeyConstructorTypeBean" class="com.guyi.spring.model.entity.MyKey" /> <bean id="myLockConstructorTypeBean" class="com.guyi.spring.model.entity.MyLock"> <constructor-arg ref="myKeyConstructorTypeBean"/> <constructor-arg> <bean class="java.lang.Integer"> <constructor-arg value="10" /> </bean> </constructor-arg> <constructor-arg> <bean class="java.lang.String"> <constructor-arg value="王五" /> </bean> </constructor-arg> </bean>
-
这种方式是将每个参数的类型都当成了非简单数据类型;
-
在参数类型都不同时,可以不需要注意声明注入的依赖的顺序
-
当有重复类型的参数时,要注意相同类型的依赖的声明顺序
-
写法复杂,不推荐使用
<!-- 构造器注入:根据参数类型注入 --> <bean id="myKeyConstructorTypeBean" class="com.guyi.spring.model.entity.MyKey" /> <bean id="myLockConstructorTypeBean" class="com.guyi.spring.model.entity.MyLock"> <constructor-arg ref="myKeyConstructorTypeBean"/> <!-- 注意顺序 --> <constructor-arg value="王五"/> <constructor-arg value="10"/> </bean>
-
这种方式只有在参数类型不是简单类型时才使用 ref 或者创建内部 Bean 的方式来注入依赖
-
在参数类型(简单数据类型视为同一类)都不同时,可以不需要注意声明注入的依赖的顺序
-
推荐按照参数列表的顺序来声明要注入的依赖
-
写法简洁,推荐使用
-
测试
/** * 构造器注入:根据参数类型匹配注入 */ @Test public void testTypeInject() { ClassPathXmlApplicationContext classPathXmlApplicationContext = new ClassPathXmlApplicationContext("spring-config-constructor.xml"); Object userConstructorBean = classPathXmlApplicationContext.getBean("myLockConstructorTypeBean"); System.out.println(userConstructorBean); }
Spring Bean 的作用域
-
Singleton(默认):在整个应用程序的上下文中,只创建一个 Bean 实例。无论何时都返回同一个实例对象。
-
Prototype:每次请求 Bean 时都会创建一个新的 Bean 实例。
以下作用域是在 Web 项目中才可以使用
-
Request:每个 HTTP 请求都会创建一个新的Bean实例。在单次请求的处理过程中,所有的 Bean 引用都指向同一个 Bean 实例。
-
Session:每个用户会话都会创建一个新的 Bean 实例。在一个用户的整个会话期间,所有的 Bean 引用都指向同一个 Bean 实例。
-
Global Session:类似于 Session 作用域,但仅在基于 Portlet 的 web 应用程序中才有效。
-
Application:在 Web 应用程序的整个生命周期内,只创建一个 Bean 实例。无论何时都返回同一个实例对象。
-
WebSocket:在 WebSocket 会话期间创建的 Bean 实例。每个 WebSocket 连接都会创建一个新的Bean实例。
这些作用域可以通过在 Spring 的 XML 配置文件中使用 <bean>
元素的 scope
属性来指定。例如:
<bean id="myBean" class="com.example.MyBean" scope="prototype" />
!-- 让 @Repository 失效 --> <context:exclude-filter type="annotation" expression="org.springframework.stereotype.Repository"/> </context:component-scan>
依赖注入的 Bean
注解 | 作用 |
---|---|
@Value | 注入简单类型依赖 |
@Autowired | 默认根据类型注入依赖 |
@Qualifier | 和 @Autowired 配合使用,可以根据名称注入依赖 |
@Resource | 先根据名称注入依赖,根据名称没找到依赖,就会根据类型注入依赖 |
Value 注解
可以标注属性、setter 方法、形参上。如果标注在属性上,必须提供无参构造器。
-
创建一个
annotation
包,在这个包下定义一个 Dog 类
/** * use Value Annotation */ @Component("dog") public class Dog { @Value("小黑") private String bogName; @Value("2") private Integer bogAge; // todo // toString 方法 }
使用这种方式不需要提供 setter 方法
-
创建一个配置文件
spring-config-annotation.xml
,引入context 命名空间,并指定要扫描的包
<!-- 指定扫描的包 --> <context:component-scan base-package="com.guyi.spring.annotation"/>
-
测试
@Test public void testValue() { ApplicationContext applicationContext = new ClassPathXmlApplicationContext("spring-config-annotation.xml"); Dog dog = applicationContext.getBean("dog", Dog.class); System.out.println(dog); }
Autowired 注解
可以用来标注构造方法、普通方法、形参、注解,required 属性用来声明依赖的 Bean 是否必须存在。Autowired 注解源码
@Target({ElementType.CONSTRUCTOR, ElementType.METHOD, ElementType.PARAMETER, ElementType.FIELD, ElementType.ANNOTATION_TYPE}) @Retention(RetentionPolicy.RUNTIME) @Documented public @interface Autowired { /** * 声明带注解的依赖项是否是必需的。 */ boolean required() default true; }
Autowired
注解一般会和 Qualifier
注解一起使用:
@Autowired @Qualifier("被注入 Bean 的名字") private OrderDao oreder;
优先根据类型进行装配,如果没有找到对于类型Bean,就根据 Qualifier 指定的名称寻找 Bean 进行注入。
需要注意的是:如果只是根据类型装配,那装配的接口下只能有一个实现类,否则就会报错,不能使用多态,很糟糕。
@Autowired private OrderDao oreder;
Resource 注解
-
Resource
注解是 JDK 拓展包中提供的,是 JDK 的一部分,是标准注解,更具有通用性。 -
Resource
注解默认根据名称装配,没有指定 name 时,使用属性名作为 name, 通过 name 找不到要注入的 Bean 对象,会开启通过类型的装配。 -
Resource 可以标注属性,setter 方法、组件类上。
-
Resource 在 JDK 拓展包中,需要额外引入以下依赖(JDK8 不需要额外引入,高于 JDK11或低于JDK8需要引入):
<!-- spring5 及以下版本引入依赖 --> <dependency> <groupId>javax.annotation</groupId> <artifactId>javax.annotation-api</artifactId> <version>版本</version> </dependency>
<!-- 使用@Resource spring6+ 版本引入依赖 --> <dependency> <groupId>jakarta.annotation</groupId> <artifactId>jakarta.annotation-api</artifactId> <version>版本</version> </dependency>
AOP
AOP 能够捕捉系统常用功能,将其转化为组件。AOP(Aspect Oriented Programming):面向切片编程、面向切面编程。AOP 是对 OOP 的补充延展。AOP 底层通过动态代理实现:JDK 动态代理 + CGLIB 动态代理技术。Spring 在这两种动态代理之间灵活切换。如果代理的是接口,会默认使用 JDK 动态代理;如果要代理某个类,这个类又没有实现接口,就会使CGLIB。当然,可以通过配置让 Spring 只使用 CGLIB。
使用 Spring AOP应当导入 AOP 依赖
<dependency> <groupId>org.springframework</groupId> <artifactId>spring-aspects</artifactId> <version>5.3.18</version> </dependency>
应用场景
一个系统当中会有一些系统服务:日志、事务管理、安全验证等。这些系统服务被称为:交叉业务。如果这些 交叉业务 都掺杂到业务代码中,会存在一些问题:
-
交叉业务的代码反复出现,代码没有得到复用
-
如果要修改这些交叉业务代码,必须修改多处
-
开发人员无法专注于核心业务代码的编写,在编写核心业务代码是可能还需要处理这些交叉业务
这时使用 AOP 可以轻松的解决以上问题。
AOP 的优点
AOP 可以将与业务无关的代码独立出来,形成独立的组件,然后以横向交叉的方式应用到业务流程中。
-
代码复用性增强
-
代码易维护
-
使开发者可以更好的关注业务逻辑
AOP 七大概念
-
连接点 Joinpoint
-
在程序的整个执行流程中,可以织入切面的位置。方法的执行前后,异常抛出后的位置等。
-
连接点描述的是位置。
-
-
切点 Pointcut
-
在程序执行流程中,真正织入切面的方法。一个切点可以对应多个连接点。
-
切点本质是方法,真正织入切面的那个方法叫做切点。
-
-
通知 Advice
-
又叫增强,就是具体织入的代码,事务代码、日志代码等。
-
通知包括:
-
前置通知
-
后置通知
-
环绕通知
-
异常通知
-
最终通知
-
-
-
切面 Aspect
-
切点 + 通知就是切面
-
-
织入 Weaving
-
把通知应用到目标对象的过程
-
-
代理对象 Proxy
-
一个目标对象被织入通知后产生的新对象
-
-
目标对象 Target
-
被织入通知的对象
-
切面实现方式
实现方式:
-
Spring 框架结合 AsprctJ 框架实现的 AOP,基于注解开发
-
Spring 框架结合 AsprctJ 框架实现的 AOP,基于 XML 开发
-
Spring 框架自己实现的 AOP,基于 XML 配置方式
定义切面
使用 @Aspect
标注切面类
/** * 则会使一个切面类 */ @Aspect @Component public class myAspect { }
可以使用 @Order
定义切面加载的优先级,值越小越早加载。
@Aspect @Order(5) @Component public class myAspect { }
如果没有指定
@Order
,切面的加载顺序就按照类的名称排序进行加载
AOP 通知相关注解
使用时需要配合切面表达式一起使用
通知类型 | 标签 | 说明 |
---|---|---|
前置通知 | @Before | |
后置通知 | @AfterReturning | |
环绕通知 | @Around | |
异常通知 | @AfterThrowing | |
最终通知 | @After | 相当于是写在 finally 语句块中的 |
通知的执行顺序
-
不发生异常时:
-
发生异常时:
切面表达式
切面表达式类型有多种,这里只记录 exection 类型的切面表达式对于切面表达式,没必要记,忘了就看笔记或者上网查
execution([控制访问服] 返回值类型 [全限定名称] 方法名(形参列表) [异常])
-
访问权限控制修饰符
-
可选
-
不写,就是四个修饰符都有
-
-
返回值类型
-
必填项
-
"*" 表示返回值类型任意
-
-
全限定类名
-
可选
-
".." 表示当前包以及子包下的所有类
-
省略时表示所有的类
-
-
方法名
-
必填项
-
"*" 表示所有的方法
-
"set*" 表示所有的 set 方法
-
-
形参列表
-
必填项
-
"()" 表示没有参数的方法
-
"(..)" 参数类型和个数随意的方法
-
"(*)" 只有一个参数的方法
-
"(*, String)" 第一个参数类型随意,第二个参数是String
-
-
异常
-
可选项
-
不填表示任意类型的异常
-
示例
-
给 service 包下的所有类中以 delete 开始的所有方法添加切面
execution(public * com.guyi.spring.service.*.delete*(..))
具体含义:service 包下任意类中使用 public 修饰的、返回值任意、只有一个形参的 delete 方法
-
spring 包下的所有的方法
ececution(* com.guyi.spring..*..(..))
-
所有类的所有方法
execution(* *(..))
模拟转帐和取款业务流程
-
在
service
包下新建一个AccountService.java
,模拟用户的转账、取款行为
/** * 目标对象 */ @Service public class AccountService { /** * 转账的业务方法 */ public void transfer() { System.out.println("银行户正在转账..."); } /** * 取款的业务方法 */ public void withdraw() { System.out.println("银行账户正在取款..."); } }
-
在
service
包下新建OrderService.java
,模拟生成订单业务
/** * 目标对象 */ @Service public class OrderService { /** * 生成订单的业务方法 */ public void generate() { System.out.println("正在生成订单..."); } /** * 取消订单的业务方法 */ public void cancel() { // 故意出异常 String s = null; s.toString(); System.out.println("订单已取消..."); } }
-
在
aspect
包下创建TransactionAspect.java
,处理事务的切面
/** * 处理事务的切面 */ @Component @Aspect public class TransactionAspect { /** * 环绕通知 * @param joinPoint 连接点 */ @Around("execution(* com.guyi.spring6.service..*(..))") public void aroundAdvice(ProceedingJoinPoint joinPoint) { try { // 前环绕 System.out.println("开启事务"); // 执行目标 joinPoint.proceed(); // 后环绕 System.out.println("提交事务"); } catch (Throwable e) { System.out.println("回滚事务"); } } }
不要忘记了导依赖
-
编写一个配置类
@Configuration @ComponentScan({"com.guyi.spring.service", "com.guyi.spring.aspect"}) @EnableAspectJAutoProxy(proxyTargetClass = true) public class SpringConfig { }
-
测试
@Test public void testAOP () { // 注意:这里使用配置类替代了配置文件, 使用的是:AnnotationConfigApplicationContext AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(SpringConfig.class); AccountService accountService = context.getBean("accountService", AccountService.class); accountService.transfer(); // 转账 accountService.withdraw(); // 取款 OrderService orderService = context.getBean("orderService", OrderService.class); orderService.generate(); // 生成订单 orderService.cancel(); // 取消订单 }
Spring 事务支持
什么是事务
在一个业务流程中,可能会需要多条的 DML 语句共同完成,事务就是保证一个业务流程中的多条 DML 语句同时执行成功,或者同时执行失败,从而保证数据的一致性。
事务有如下特性:
-
原子性:事务是最小的工作单元,不可再分,所有操作要么同时提交,要么同时回滚。
-
一致性:多条 DML 语句必须同时执行成功或者同时执行失败,执行失败后数据要回滚。
-
隔离性:不同事务之间不能相互影响。
-
持久性:事务一旦提交,其所做的修改必须永久保存到数据库中。即使系统方式故障或者宕机,数据也能够保持不变。
事务的处理过程:
-
开启事务
-
执行业务的核心代码
-
提交事务 / 回滚事务
Spring 实现事务的方式
-
编程式事务:通过编写代码实现对业务的管理
-
声明式事务
-
基于注解
-
基于 XML 配置
-
开启声明式事务
方式一:
-
在 Spring 配置文件中配置数据源,这里使用德鲁伊
德鲁伊依赖
<!-- https://mvnrepository.com/artifact/com.alibaba/druid --> <dependency> <groupId>com.alibaba</groupId> <artifactId>druid</artifactId> <version></version> </dependency>
配置数据源
<!-- 配置数据源 --> <bean id="dataSource" class="com.alibaba.druid.pool.DruidDataSource"> <property name="driverClassName" value=""/> <property name="url" value=""/> <property name="username" value=""/> <property name="password" value=""/> </bean>
-
配置事务管理器
<!-- 配置事务管理器 --> <bean id="dataSource" class="org.springframework.jdbc.datasource.DataSourceTransactionManager"> <property name="" ref="数据源"/> </bean>
-
开启事务支持
需要引入 tx 和 aop 命名空间
xmlns:tx="http://www.springframework.org/schema/tx" xmlns:aop="http://www.springframework.org/schema/aop" 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
需要导入以下依赖
<dependency> <groupId>org.springframework</groupId> <artifactId>spring-aspects</artifactId> <version>5.3.18</version> </dependency> <dependency> <groupId>org.springframework</groupId> <artifactId>spring-jdbc</artifactId> <version>5.3.18</version> </dependency> <dependency> <groupId>org.springframework</groupId> <artifactId>spring-tx</artifactId> <version>5.3.18</version> </dependency>
-
声明式事务配置:
在需要应用事务的bean或方法上使用tx:advice和tx:attributes元素进行配置,如下:
<bean id="userService" class="com.example.UserService"> <property name="userRepository" ref="userRepository"/> </bean> <tx:advice id="transactionAdvice" transaction-manager="transactionManager"> <tx:attributes> <tx:method name="*" propagation="REQUIRED"/> </tx:attributes> </tx:advice> <aop:config> <aop:pointcut id="userServicePointcut" expression="execution(* com.example.UserService.*(..))"/> <aop:advisor advice-ref="transactionAdvice" pointcut-ref="userServicePointcut"/> </aop:config>
或者开启事务管理器,再需要进行事务的管理的类或者方法上添加 @Transactional
注解即可。
<!-- 配置事务管理器 --> <bean id="transactionManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager"> <property name="dataSource" ref="dataSource"/> </bean>
加在类上:这个类的的所有方法都应用事务加在方法上:只对这个方法应用事务
方式二:
方式一比较繁琐,不建议使用
-
编写配置类
@Configuration @EnableTransactionManagement // 开启事务注解驱动管理器 @ComponentScan public class SpringConfig2 { /** * 定义数据源 * * @return 数据源对象 */ @Bean public DruidDataSource getDruidDataSource() { DruidDataSource druidDataSource = new DruidDataSource(); druidDataSource.setUsername(""); druidDataSource.setPassword(""); druidDataSource.setUrl(""); // 实际是给 jdbcUrl 赋值 druidDataSource.setDriverClassName(""); // 实际是给 driverClass 赋值 return druidDataSource; } /** * 定义事务管理器 * * @param druidDataSource * @return */ @Bean public PlatformTransactionManager transactionManager(DruidDataSource druidDataSource) { return new DataSourceTransactionManager(druidDataSource); } }
这样就可以使用 @Transactional
来管理事务了
设置事务属性
事务传播行为(Propagation)
service 类中有 a() 方法和 b() 方法,两个方法上都有事务,当 a() 方法执行过程中调用了 b() 方法,事务是如何传递的?合并到一个事务中?还是开启一个新的事务?这就是事务传播行为。
事务传播行为在 Spring 中被定义为枚举类型:
public enum Propagation() { REQUIRED(0), // 默认 SUPPORIS(1), MANDATORY(2), REQUIRES_NEW(3), NOT_SUPPORTED(4), ENVER(5), ENSTED(6); }
-
REQUIRED(0):支持当前事务,如果不存在就新建一个(没有就新建,有就加入。这个是默认的)
-
SUPPORIS(1):支持当前事务,如果当前没有事务,就以非事务方式执行(有就加入,没有就算了)
-
MANDATORY(2):必须允许在一个事务当中,如果当前没有事务发生,就抛异常(有就加入,没有就抛异常)
-
REQUIRES_NEW(3):开启一个新的事物,如果一个事务已经存在,就将这个事务挂起(不存在事务嵌套)
-
NOT_SUPPORTED(4):以非事务方式执行,如果事务存在,挂起当前事务(不支持事务,有就挂起)
-
ENVER(5):以非事务方式执行,如果存在事务,抛异常(不支持事务,存在抛异常)
-
ENSTED(6):如果当前事务正在进行,则该事务的对应方法应当执行在一个嵌套式事务中,被嵌套的事务可以独立于外层事务进行提交或回滚,如果外层事务存在,行为就像 REQUIRES_NEW 一样(有事务,就在这个事务里嵌套一个完全独立的事务,嵌套的事务可以独立的提交或回滚,没有事务就和 REQUIRES_NEW 一样)
在代码中设置事务的传播行为
@Transactional(propagation = Propagation.REQUIRED)
事务隔离级别
public enum Isolation { DEFAULT(-1), // 默认级别; mysql的默认级别是4; 甲骨文的默认级别是2 READ_UNCOMMITED(1), // 读未提交 READ_COMMITED(2), // 读已提交 REPEATABLE_READ(4), // 可重复读 SERIALIZABLE(8); // 序列化 }
设置事务隔离级别
@Transactional(isolation = Isolation.READ_COMMITED)
事务超时
@Transactional(timeout = 10)
-
表示设置事务的超时时间为10秒。如果超过 10 秒,该事务的 DML 语句还没有执行完,就选择回滚。
-
默认值是 -1,表示没有时间限制
-
事务超时时间是指哪段时间?
-
在当前事务中,最后一条 DML 语句执行之前的时间。如果最后一条 DML 语句后面还有很多的业务逻辑,这些业务代码执行的时间不被计入超时时间。
-
只读事务
@Transactional(readOnly = true)
将当前事务设置为只读事务,也就是当前事务只能执行 select 语句,不能执行增、删、改。作用是:启动 Spring 的优化策略,提高查询结果。如果该事务中确实没有增、删、改操作,建议设置为只读事务。
异常回滚设置
@Transactional(rollbackFor = RuntimeException.class)
表示只有发生了 RuntimeException异常或其子类异常才会回滚
@Transactional(noRollbackFor = NullPointerException.class)
表示发生 NullPointerException 异常或其子类异常不回滚
Spring 常用注解汇总
组件相关
Bean 注解:大白话讲解Spring的@bean注解 - 知乎
注解 | 说明 |
---|---|
@Component | 用于标识一个类作为Spring容器中的组件(Bean),会被自动扫描和注册到容器中。 |
@Contorller | 用于标识一个类作为Spring MVC中的控制器(Controller),处理请求和返回视图。 |
@Service | 用于标识一个类作为业务逻辑层(service)的组件,通常被注入到其他类中使用。 |
@Repository | 用于标识一个类作为数据访问层(Dao)的组件,提供对数据库的访问操作。 |
@Bean | 将方法的返回值(是一个对象)交给 Spring 管理 |
依赖注入相关
注解 | 作用 |
---|---|
@Value | 注入简单类型依赖 |
@Autowired | 默认根据类型注入依赖 |
@Qualifier | 和 @Autowired 配合使用,可以根据名称注入依赖 |
@Resource | 先根据名称注入依赖,根据名称没找到依赖,就会根据类型注入依赖 |
配置相关
注解 | 作用 |
---|---|
@Configuration | 用于标识一个类作为 Spring 配置类,替代传统的XML配置文件,用于定义、组合和装配Bean。 |
@EnableTransactionManagement | 用在配置类上,表示开启事务注解驱动,让程序支持使用注解开启事务 |
@Transactional | 用在类上、方法上,对于类,被标记类的所有方法都开启事务,对于方法,别标记的方法开启事务 |
@ComponentScan | 组件扫描器,可以将指定包下的被组件相关注解标注的类注册到日期中 |
@EnableAspectJAutoProxy | 用在配置类上,开启注解式 AOP,通过 AspectJ 自动代理完成 |
AOP 相关
注解 | 作用 |
---|---|
@Aspect | 声明一个类为切面类。 |
@Before | 在目标方法执行前执行切面逻辑。 |
@After | 在目标方法执行后(不论是否发生异常)执行切面逻辑。 |
@AfterReturning | 在目标方法执行后,如果没有发生异常,则执行切面逻辑。 |
@AfterThrowing | 在目标方法执行后,如果发生异常,则执行切面逻辑。 |
@Around | 在目标方法执行前和执行后执行切面逻辑。 |
@Pointcut | 声明一个切入点,用于定义切面逻辑的执行位置。 |