计划利用一些时间学习一下Spring框架(开始于 2021年7月6日)
视频参考: https://www.bilibili.com/video/BV185411477k
课程笔记参考: 《孙哥说Spring5》学习笔记_代码改变世界-CSDN博客
Spring框架版本 5.1.4 Release
工厂模式是Spring核心IOC(控制翻转)所参考的一个重要的设计模式, 可以解耦合
工厂模式理解了没有? - SegmentFault 思否
https://www.bilibili.com/video/BV185411477k?p=7
使用new来创建对象, 具有很强的耦合性
ProductService productService = new ProductServiceImpl();
ProductDao productdao = new ProductDaoImpl();
所以我们便可以利用工厂设计模式, 使用工厂对象来生产对象, 从而解耦合
/**
* 生产对象的工厂类: 利用反射来创建对象
*/
public class BeanFactory {
public static Properties properties = new Properties();
public static InputStream inputStream = null;
static{
try {
// 打开输入流,IO操作一般放在static块中, 只操作一次
InputStream inputStream = BeanFactory.class.getResourceAsStream("applicationContext.properties");
// 读取属性文件
properties.load(inputStream);
} catch (IOException e) {
e.printStackTrace();
} finally {
try {
inputStream.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
public static ProductService getProductServiceImpl(){
ProductService productService = null;
try {
// 利用反射创建对象
Class clazz = Class.forName(properties.getProperty("productServiceImpl"));
productService = (ProductService) clazz.newInstance();
} catch (ClassNotFoundException e) {
e.printStackTrace();
} catch (IllegalAccessException e) {
e.printStackTrace();
} catch (InstantiationException e) {
e.printStackTrace();
}
return productService;
}
public static ProductDao getProductDaoImpl(){
ProductDao productDao = null;
Class clazz = null;
try {
clazz = Class.forName(properties.getProperty("productDaoImpl"));
productDao = (ProductDao) clazz.newInstance();
} catch (ClassNotFoundException e) {
e.printStackTrace();
} catch (IllegalAccessException e) {
e.printStackTrace();
} catch (InstantiationException e) {
e.printStackTrace();
}
return productDao;
}
}
属性文件:
# 格式: key = value
productServiceImpl = com.shy.service.impl.ProductServiceImpl
productDaoImpl = com.shy.dao.impl.ProductDaoImpl
创建对象:
ProductDao productDao = BeanFactory.getProductDaoImpl();
ProductService productService = BeanFactory.getProductServiceImpl();
对于静态工厂, 假如有许多对象, 则需要对每一个对象都编写一个生产对象的方法. 这些代码明显是重复的, 所以可以抽出公共部分, 封装成一个方法, 成为通用工厂
package com.shy.factory;
import com.shy.dao.ProductDao;
import com.shy.service.ProductService;
import java.io.IOException;
import java.io.InputStream;
import java.util.Properties;
/**
* 生产对象的工厂类: 利用反射来创建对象
*/
public class BeanFactory {
public static Properties properties = new Properties();
public static InputStream inputStream = null;
static{
try {
// 打开输入流,IO操作一般放在static块中, 只操作一次
InputStream inputStream = BeanFactory.class.getResourceAsStream("/applicationContext.properties");
// 读取属性文件
properties.load(inputStream);
inputStream.close();
} catch (IOException e) {
e.printStackTrace();
}
}
// 将生产对象, 抽象成一个通用的方法
public static Object getBean(String key){
Object object = null;
Class clazz = null;
try {
clazz = Class.forName(properties.getProperty(key));
object = clazz.newInstance();
} catch (ClassNotFoundException e) {
e.printStackTrace();
} catch (IllegalAccessException e) {
e.printStackTrace();
} catch (InstantiationException e) {
e.printStackTrace();
}
return object;
}
}
创建对象:
ProductDao productDao = (ProductDao) BeanFactory.getBean("productDaoImpl");
ProductService productService = (ProductService) BeanFactory.getBean("productServiceImpl");
前文提到spring参考工厂模式创建对象, 而ApplicationContext
就是Spring框架用于创建对象的工厂
需要注意的是, 创建ApplicationContext
会占用大量资源
所以一般一个应用只创建一个ApplicationContext
对象
有可能被多个线程同时访问, 是线程安全的
此接口有两个实现类:
(非web环境下使用, 如单元测试)
首先创建一个spring工厂
ApplicationContext applicationContext = new ClassPathXmlApplicationContext("/applicationContext.xml");
关于路径问题:
观察
maven
编译后生成的target
目录, 会发现java
和resources
被合并到一个目录.所以可以认为
java
和resource
下的文件的根目录相同
在applicationContext.xml
中配置Product
实体类的信息
<bean id="product" name="p1,p2,p3" class="com.shy.entity.Product"/>
<bean class="com.shy.entity.Product"/>
<bean class="com.shy.entity.Product"/>
<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="product" class="com.shy.entity.Product"/>
beans>
然后利用该工厂创建对象
Product product = (Product) applicationContext.getBean("product");
下面简单了解一些ApplicationContext
的方法
getBean
创建对象
// 根据applicationContext.xml中bean的 id|name 值创建对象
Product product = (Product) applicationContext.getBean("product");
Product product = applicationContext.getBean("product",Product.class); // 不需要强转
// 根据applicationContext.xml中bean的class值创建对象
Product product = (Product) applicationContext.getBean(Product.class);
getBeanDefinitionNames
// 获取applicationContext.xml文件中所有bean标签的id值
String[] beanDefinitionNames = applicationContext.getBeanDefinitionNames();
for (String beanDefinitionName : beanDefinitionNames) {
System.out.println("beanDefinitionName = " + beanDefinitionName);
}
getBeanNamesForType
// 获取applicationContext.xml文件中所有特定类型的bean标签的id值
String[] beanDefinitionNames = applicationContext.getBeanNamesForType(Product.class);
containsBeanDefinition
判断是否存在指定 id 值的 bean
containsBean
判断是否存在指定 id|name 值的 bean
(web环境下使用)
(未涉及源码)
在实际开发中,并不是所有的对象都交由applicationContext
创建
像实体类entity
一般由数据库框架来创建
public class Product {
public String name;
public Product() {
System.out.println("调用了无参构造");
}
}
默认情况下, ApplicationContext在创建对象时, 会调用对象的无参构造
在实际开发中, 往往将applicationContext.xml
文件按照一定的层级分开(如DAO,Service,Controller), 从而方便维护
假设有以下几个applicationContext.xml文件:
applicationContext-DAO.xml
applicationContext-Service.xml
applicationContext-Controller.xml
使用通配符*
ApplicationContext applicationContext = (ApplicationContext) new ClassPathXmlApplicationContext("applicationContext-*.xml");
使用import
标签
<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">
<import resource="applicationContext-DAO.xml"/>
<import resource="applicationContext-Service.xml"/>
<import resource="applicationContext-Controller.xml"/>
beans>
ApplicationContext applicationContext = (ApplicationContext) new ClassPathXmlApplicationContext("applicationContext.xml");
首先在pom.xml
添加如下代码:
<dependency>
<groupId>org.slf4jgroupId>
<artifactId>slf4j-log4j12artifactId>
<version>1.7.21version>
dependency>
<dependency>
<groupId>log4jgroupId>
<artifactId>log4jartifactId>
<version>1.2.17version>
dependency>
然后在resources
下添加log4j.properties
配置文件即可
# resources文件夹根目录下
### 配置根
log4j.rootLogger = debug,console
### 日志输出到控制台显示
log4j.appender.console=org.apache.log4j.ConsoleAppender
log4j.appender.console.Target=System.out
log4j.appender.console.layout=org.apache.log4j.PatternLayout
log4j.appender.console.layout.ConversionPattern=%d{yyyy-MM-dd HH:mm:ss} %-5p %c{1}:%L - %m%n
logback
pom.xml
<dependency>
<groupId>org.slf4jgroupId>
<artifactId>slf4j-apiartifactId>
<version>1.7.25version>
dependency>
<dependency>
<groupId>org.slf4jgroupId>
<artifactId>jcl-over-slf4jartifactId>
<version>1.7.25version>
dependency>
<dependency>
<groupId>ch.qos.logbackgroupId>
<artifactId>logback-classicartifactId>
<version>1.2.3version>
dependency>
<dependency>
<groupId>ch.qos.logbackgroupId>
<artifactId>logback-coreartifactId>
<version>1.2.3version>
dependency>
<dependency>
<groupId>org.logback-extensionsgroupId>
<artifactId>logback-ext-springartifactId>
<version>0.1.4version>
dependency>
设置配置文件logback.xml
<configuration>
<appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender">
<encoder>
<pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{50} - %msg%npattern>
encoder>
appender>
<root level="DEBUG">
<appender-ref ref="STDOUT" />
root>
configuration>
什么是注入?
通过 Spring ⼯⼚及配置⽂件,为所创建对象的成员变量赋值
为什么需要注入?
与传统的使用setter或者constructor为成员变量赋值相比, Spring注入可以解耦合
product.setName("歼-20");
product = new Product("山东舰");
set注入即Spring 调⽤ Set 方法 通过 配置⽂件 为成员变量赋值;
public class Product {
public String name;
public Product() {
}
// 对需要赋值的属性提供set方法
public void setName(String name) {
System.out.println("调用了set方法");
this.name = name;
}
public String getName() {
return name;
}
}
<bean id="product" class="com.shy.entity.Product">
<property name="name">
<value>红色拖拉机value>
property>
bean>
@Test
public void test2(){
ClassPathXmlApplicationContext applicationContext = new ClassPathXmlApplicationContext("/applicationContext.xml");
Product product = (Product) applicationContext.getBean("product");
System.out.println("product.name = " + product.name);
}
Spring底层调用set方法,完成对属性的赋值
针对成员变量不同的类型, 在编写applicationContext.xml
时的语法不同, 但都应该首先提供set方法
String + 基本数据类型及其包装类
<property name="name">
<value>红色拖拉机value>
property>
或者
<property name="name" value="红色拖拉机">property>
数组/List
<property name="teams">
<list>
<value>McLarenvalue>
<value>Red Bullvalue>
<value>Alpha Taurivalue>
list>
property>
中的标签不是固定的,而是由数组/List
的类型决定
这里public String[] teams;
类型是String
,所以用
标签
Set
中的标签不是固定的,而是由set
的类型决定
<property name="drivers">
<set>
<value>Lando Norrisvalue>
<value>Pierre Gaslyvalue>
<value>Carlos Sainzvalue>
<value>Lando Norrisvalue>
set>
property>
Map
中的标签不是固定的,而是由map
的类型决定
<map>
<entry>
<key><value>McLarenvalue>key>
<value>Lando Norrisvalue>
entry>
<entry value="Pierre Gasly">
<key><value>Alpha Taurivalue>key>
entry>
<entry value="Pierre Gasly" key="Alpha Tauri"/>
map>
Properties
Properties
即Map
<property name="championships">
<props>
<prop key="Kimi raikkonen">0prop>
<prop key="Lewis Hamilton">7prop>
<prop key="Sebastian Vettel">4prop>
<prop key="Kimi raikkonen">1prop>
props>
property>
用户自定义类型
public class ProductServiceImpl implements ProductService {
// 使用Spring工厂创建productDao对象
private ProductDao productDao;
public ProductDao getProductDao() {
return productDao;
}
public void setProductDao(ProductDao productDao) {
this.productDao = productDao;
}
// ProductDao productDao = new ProductDaoImpl(); new创建对象
// ProductDao productDao = BeanFactory.getProductDaoImpl(); 静态工厂
// ProductDao productDao = (ProductDao) BeanFactory.getBean("productDaoImpl"); 通用工厂
public void insertProduct(Product product) {
productDao.insertProduct(product);
}
}
<bean id="productService" class="com.shy.service.impl.ProductServiceImpl">
<property name="productDao">
<bean id="productDao" class="com.shy.dao.impl.ProductDaoImpl"/>
property>
bean>
<bean id="productDao" class="com.shy.dao.impl.ProductDaoImpl">bean>
<bean id="productService" class="com.shy.service.impl.ProductServiceImpl">
<property name="productDao">
<ref bean="productDao">ref>
property>
或者
<property name="productDao" ref="productDao"/>
bean>
使用p命名空间可以简化Set注入的写法(像是一个语法糖, 不常用)
xmlns:p="http://www.springframework.org/schema/p"
<bean id="product" class="com.shy.entity.Product">
<property name="name">
<value>红色拖拉机value>
property>
bean>
简化为:
<bean id="product" class="com.shy.entity.Product" p:name="红色拖拉机">
<bean id="productService" class="com.shy.service.impl.ProductServiceImpl">
<property name="productDao" ref="productDao"/>
bean>
简化为:
<bean id="productService" class="com.shy.service.impl.ProductServiceImpl" p:productDao-ref="productDao">
构造注入即Spring 调⽤ 有参构造方法 通过 配置⽂件 为成员变量赋值
public class Driver {
public String name;
public int number;
public Driver(String name, int number) {
this.name = name;
this.number = number;
}
@Override
public String toString() {
return "Driver{" +
"name='" + name + '\'' +
", number=" + number +
'}';
}
}
<bean id="dirver" class="com.shy.entity.Driver">
<constructor-arg><value>Lando Norrisvalue>constructor-arg>
<constructor-arg><value>4value>constructor-arg>
bean>
@Test
public void test4(){
ApplicationContext applicationContext = new ClassPathXmlApplicationContext("/applicationContext.xml");
Driver dirver = (Driver) applicationContext.getBean("dirver");
System.out.println(dirver.toString());
}
与Set注入一样, 构造注入在针对不同类型的成员变量, 编写applicationContext.xml
时的语法不同, 具体参考set注入详解
,
但需要首先给出相应的构造器, 而构造方法存在重载的情况.
我们可以借助
的几个属性来处理重载的情况, 从而指定相应的构造器
index
指定将要被注入的成员变量在构造器中的位置
public Driver(String name, int number) {
this.name = name;
this.number = number;
}
<constructor-arg index="1">
<value>4value>
constructor-arg>
<constructor-arg index="0">
<value>Lando Norrisvalue>
constructor-arg>
type
指定将要被注入的成员变量的类型
public Driver(String name) {
this.name = name;
}
public Driver(int number) {
this.number = number;
}
Driver有重载了两个构造器, 可以用type
加以区分进行注入
<bean id="dirver" class="com.shy.entity.Driver">
<constructor-arg type="java.lang.String">
<value>Lando Norrisvalue>
constructor-arg>
bean>
或者
<bean id="dirver" class="com.shy.entity.Driver">
<constructor-arg type="int">
<value>4value>
constructor-arg>
bean>
name
直接指明要注入哪个成员变量
<bean id="dirver" class="com.shy.entity.Driver">
<constructor-arg name="number">
<value>4value>
constructor-arg>
<constructor-arg name="name">
<value>Lando Norrisvalue>
constructor-arg>
bean>
与p命名空间一样, 使用c命名空间可以简化构造注入的写法(像是一个语法糖, 不常用)
xmlns:c="http://www.springframework.org/schema/c"
<bean id="dirver" class="com.shy.entity.Driver" c:name="Pierre Gasly" c:number="10"/>
<bean id="dirver" class="com.shy.entity.Driver" c:_0="Max Verstappen" c:_1="33"/>
实战当中, Set注入更常用. 在Spring框架中也大量使用了Set注入
参考:彻底搞明白Spring中的自动装配和Autowired - 简书 (jianshu.com)
应用场景
可以理解为自动注入,即在对Bean的属性进行依赖注入时, 有时需要引用某一个已经注册好的Bean
,
此时就可以使用自动装配, 让Spring框架自动扫描完成注入, 而不用显示的进行配置.
<bean id="userDao" class="com.shy.dao.impl.UserDaoImpl"/>
<bean id="userService" class="com.shy.service.impl.UserServiceImpl">
<property name="userDao" ref="userDao"/>
bean>
自动装配:
<bean id="userDao" class="com.shy.dao.impl.UserDaoImpl"/>
<bean id="userService" class="com.shy.service.impl.UserServiceImpl" autowire="byName"/>
错误示范: 将autowire理解成注册
Bean
, 而不是对Bean
的属性进行注入
装配策略
所谓装配策略,指的是Spring如何与找到
bean_A
的属性对应的bean_B
,来完成依赖注入
byName
寻找与bean_A
属性名字相同的bean_B
byType
寻找与bean_A
属性类型相同的bean_B
constructor
寻找与bean_A
构造器参数类型相同的bean_B
实战中, 并不推荐在xml中使用自动装配.
不过,在Spring引入注解
@autowired
之后, 使用注解自动装配成为实战最受欢迎的一种方式详细内容见后
浅谈IOC–说清楚IOC是什么_哲-CSDN博客_ioc
控制反转(IOC: Inverse of Control) — 解耦合
获得依赖对象的过程被反转了。控制被反转之后,获得依赖对象的过程由自身管理变为了由IOC容器主动注入
在没有引入IOC容器之前,对象A依赖于对象B,那么对象A在初始化或者运行到某一点的时候,自己必须主动去创建对象B或者使用已经创建的对象B。无论是创建还是使用对象B,控制权都在自己手上。
在引入IOC容器之后,这种情形就完全改变了,由于IOC容器的加入,对象A与对象B之间失去了直接联系,所以,当对象A运行到需要对象B的时候,IOC容器会主动创建一个对象B注入到对象A需要的地方。
依赖注入(DI: Dependence Injection) — 实现IOC的方式
依赖注入,就是由IOC容器在运行期间,动态地将某种依赖关系注入到对象之中
在以上章节中, 我们都是通过applicationContext.getBean()
生产简单的Java对象POJO
那么, 如何使用Spring工厂生产复杂对象呢? 复杂对象又是什么呢?
复杂对象
简单说, 复杂对象就是不直接使用new来创建的对象, 如数据库连接Connection
public class ConnectionFactory {
public Connection getConnection(){
Connection connection = null;
try {
Class.forName("com.mysql.cj.jdbc.Driver");
connection = DriverManager.getConnection("jdbc:mysql://localhost:3306/spring?useUnicode=true&characterEncoding=utf-8&useSSL=false&serverTimezone=GMT&allowPublicKeyRetrieval=true","root","root");
} catch (ClassNotFoundException e) {
e.printStackTrace();
} catch (SQLException e) {
e.printStackTrace();
}
return connection;
}
}
public class ConnectionFactoryBean implements FactoryBean<Connection> {
private String driver;
private String url;
private String user;
private String password;
// 提供Set方法, 进行Set注入
public void setDriver(String driver) {
this.driver = driver;
}
public void setUrl(String url) {
this.url = url;
}
public void setUser(String user) {
this.user = user;
}
public void setPassword(String password) {
this.password = password;
}
public String getDriver() {
return driver;
}
public String getUrl() {
return url;
}
public String getUser() {
return user;
}
public String getPassword() {
return password;
}
// 创建复杂对象
public Connection getObject() throws Exception {
Connection connection = null;
Class.forName(driver);
connection = DriverManager.getConnection(url,user,password);
return connection;
}
// 返回创建的复杂对象的类型
public Class<?> getObjectType() {
return Connection.class;
}
// 是否为单例模式(此为default的方法, 可以不重写, 默认返回true)
public boolean isSingleton() {
return true;
}
}
<bean id="connection" class="com.shy.factory.ConnectionFactoryBean">
<property name="driver" value="com.mysql.cj.jdbc.Driver"/>
<property name="url" value="jdbc:mysql://localhost:3306/spring?serverTimezone=GMT%2B8&useUnicode=true&characterEncoding=UTF-8&allowPublicKeyRetrieval=true"/>
<property name="user" value="root"/>
<property name="password" value="niit"/>
bean>
@Test
public void test(){
ApplicationContext applicationContext = new ClassPathXmlApplicationContext("/applicationContext.xml");
Connection connection = (Connection) applicationContext.getBean("connection");
System.out.println("connection = " + connection);
}
getBean()
对于简单的Java对象来说, getBean()
直接获得在
中指定的类的对象
而对于复杂的Java对象, getBean()
并没有直接获取有在
中指定的类的对象,
因为对于复杂对象来说,
指定的是生产复杂对象的Factory
<bean id="connection" class="com.shy.factory.ConnectionFactoryBean">
所以getBean()
返回的是FactoryBean
接口中getObject()
方法返回的复杂对象
如果想要获得生产该对象的Factory
,可以加上&
ConnectionFactoryBean bean = (ConnectionFactoryBean) applicationContext.getBean("&connection");
boolean isSingleton()
此方法可以指定工厂生产的Bean是否为单例模式
// 是否为单例模式(此为default的方法, 可以不重写, 默认返回true)
public boolean isSingleton() {
return false;
}
Connection connection = (Connection) applicationContext.getBean("connection");
Connection connection2 = (Connection) applicationContext.getBean("connection");
FactoryBean
实现原理
Spring中FactoryBean的作用和实现原理 - 夜月归途 - 博客园 (cnblogs.com)
为什么需要这两种方法创建复杂对象?
避免Spring框架的侵入(疑问: 这两种方式还是需要配置applicationContext.xml
, 这不还是Spring框架的侵入吗)
整合遗留的代码
如果只有getConnection()
方法的.class
文件, 没有源代码, 则无法通过实现FactoryBean
接口来创建复杂对象
只能通过实例工厂和静态工厂来创建复杂对象
实例工厂
public class ConnectionFactory {
public Connection getConnection(){
Connection connection = null;
try {
Class.forName("com.mysql.jdbc.Driver");
connection = DriverManager.getConnection("jdbc:mysql://localhost:3306/spring?serverTimezone=GMT%2B8&useUnicode=true&characterEncoding=UTF-8&allowPublicKeyRetrieval=true","root","root");
} catch (ClassNotFoundException e) {
e.printStackTrace();
} catch (SQLException e) {
e.printStackTrace();
}
return connection;
}
}
假如已有以上方法, 现需要整合到Spring框架中, 借助Spring创建对象
<bean id="connectionFactory" class="com.shy.factory.ConnectionFactory"/>
<bean id="connection" factory-bean="connectionFactory" factory-method="getConnection"/>
@Test
public void test(){
ApplicationContext applicationContext = new ClassPathXmlApplicationContext("/applicationContext.xml");
Connection connection = (Connection) applicationContext.getBean("connection");
}
静态工厂
public class StaticConnectionFactory {
public static Connection getConnection(){
Connection connection = null;
try {
Class.forName("com.mysql.jdbc.Driver");
connection = DriverManager.getConnection("jdbc:mysql://localhost:3306/spring?serverTimezone=GMT%2B8&useUnicode=true&characterEncoding=UTF-8&allowPublicKeyRetrieval=true","root","niit");
} catch (ClassNotFoundException e) {
e.printStackTrace();
} catch (SQLException e) {
e.printStackTrace();
}
return connection;
}
}
假如已有以上静态方法getConnection()
, 现需要整合到Spring框架中, 借助Spring创建对象
<bean id="connection" class="com.shy.factory.StaticConnectionFactory" factory-method="getConnection"/>
@Test
public void test(){
ApplicationContext applicationContext = new ClassPathXmlApplicationContext("/applicationContext.xml");
Connection connection = (Connection) applicationContext.getBean("connection");
}
创建复杂对象时需要的参数, 可以直接注入
<bean id="dateFormat" class="java.text.SimpleDateFormat"> <constructor-arg value="yyyy-mm-dd"/> bean> <bean id="driver" class="com.shy.entity.Driver"> <property name="name" value="Lando Norris"/> <property name="number" value="4"/> <property name="birthday"> <bean factory-bean="dateFormat" factory-method="parse"> <constructor-arg value="1999-11-13"/> bean> property> bean>
为什么要控制Spring工厂创建对象的次数? – 节省内存的消耗
一般可以被共用, 线程安全 或是 重量级资源只创建一次 如 DAO Service
而不可以被共用, 非线程安全的要创建多次 如 Connection Session
如何控制Spring工厂创建对象的次数呢 ?
简单对象 – 通过
控制
<bean id="driver" class="com.shy.entity.Driver" scope="prototype"/>
<bean id="driver" class="com.shy.entity.Driver" scope="singleton"/>
复杂对象
对于实现了FactoryBean
接口的对象, 通过重写isSingleton()
方法来控制
// 是否为单例模式(此为default的方法, 可以不重写, 默认返回true)
public boolean isSingleton() {
return false;
}
对于无法实现上述接口的对象(如实例工厂 静态工厂),通过
控制
<bean id="connection" class="com.shy.factory.StaticConnectionFactory" factory-method="getConnection" scope="prototype"/>
在Spring框架中, 由Spring容器管理Bean的生命周期
对于singleton
对象
在Spring工厂创建时, 对象就会一起被创建
ApplicationContext applicationContext = new ClassPathXmlApplicationContext("/applicationContext.xml");
// -----对象被创建
applicationContext.getBean("driver");
可以指定lazy-init
, 这样单例就不再工厂创建时被创建, 而是在第一次使用时被创建,
(可以减少项目的启动时间)
<bean id="driver" class="com.shy.entity.Driver" lazy-init="true"/>
ApplicationContext applicationContext = new ClassPathXmlApplicationContext("/applicationContext.xml");
applicationContext.getBean("driver");
// -----对象被创建
对于prototype
对象
当需要该对象时再创建该对象
ApplicationContext applicationContext = new ClassPathXmlApplicationContext("/applicationContext.xml");
applicationContext.getBean("driver");
// -----对象被创建
Spring 工厂在创建完对象后,Spring工厂调用程序员提供的对象的初始化方法,完成对应的初始化操作(主要是对资源的初始化)
提供初始化方法
实现InitializingBean
接口的afterPropertiesSet()
方法
public class Driver implements InitializingBean {
// ......
// 自定义对象的初始化方法, 由Spring工厂调用
@Override
public void afterPropertiesSet() throws Exception {
System.out.println("Driver.afterPropertiesSet");
}
}
用
自定义初始化方法
并不需要实现InitializingBean
接口
public class Driver{
// ......
// 自定义对象的初始化方法, 由Spring工厂调用
public void myInitMethod() {
System.out.println("Driver.myInitMethod");
}
}
<bean id="driver" class="com.shy.entity.Driver" init-method="myInitMethod"/>
如果⼀个对象既实现 afterPropertiesSet()
同时⼜提供了普通的初始化方法, 则先执行前者, 再执行后者
Spring工厂在创建对象之后, 先完成对依赖注入, 再进行初始化操作
Spring 工厂在销毁对象
ctx.close()
前,会调用程序员提供的对象的初始化方法,完成对应的销毁操作(主要是对资源的销毁
销毁操作只会对scope="singleton"
的bean生效. 并且需要显式调用close()
方法销毁对象
spring可以管理 singleton作用域的bean的生命周期,spring可以精确地知道该bean何时被创建,何时被初始化完成,容器合适准备销毁该bean实例。
spring无法管理prototype作用域的bean的生命周期,每次客户端请求prototype作用域的bean,bean实例都会完全交给客户端管理,容器不再跟踪其生命周期
@Test
public void test(){
ClassPathXmlApplicationContext applicationContext = new ClassPathXmlApplicationContext("/applicationContext.xml");
applicationContext.getBean("driver");
applicationContext.close();
// 需要注意的是 close()没有在ApplicationContext接口中定义. 需要将引用更换为子接口ClassPathXmlApplicationContext 才能调用该方法. (因为父类只能调用子类继承自该父类的方法[多态])
}
提供销毁方法
实现DisposableBean
接口的destroy()
方法
public class Driver implements DisposableBean {
// ......
// 自定义对象的销毁方法, 由Spring工厂调用
@Override
public void destroy() throws Exception {
System.out.println("Driver.destroy");
}
}
用
自定义销毁方法
并不需要实现DisposableBean
接口
public class Driver implements DisposableBean {
// ......
// 自定义对象的初始化方法, 由Spring工厂调用
public void myDestroyMethod() {
System.out.println("Driver.myDestroyMethod");
}
}
<bean id="driver" class="com.shy.entity.Driver" destroy-method="myDestroyMethod"/>
如果⼀个对象既实现 destroy()
同时⼜自定义了销毁方法, 则先执行前者, 再执行后者
为什么需要配置文件参数化?
便于维护: 我们可以将applicationContext.xml
中经常需要修改的配置信息转移到配置文件.properties
中
演示:
以JDBC
的相关配置信息作为演示
<bean id="connection" class="com.shy.factory.ConnectionFactoryBean">
<property name="driver" value="com.mysql.cj.jdbc.Driver"/>
<property name="url" value="jdbc:mysql://localhost:3306/spring?useUnicode=true&characterEncoding=utf-8&useSSL=false&serverTimezone=GMT&allowPublicKeyRetrieval=true"/>
<property name="user" value="root"/>
<property name="password" value="root"/>
bean>
dbconfig.properties
配置文件jdbc.driver=com.mysql.cj.jdbc.Driver
jdbc.url=jdbc:mysql://localhost:3306/spring?useUnicode=true&characterEncoding=utf-8&useSSL=false&serverTimezone=GMT&allowPublicKeyRetrieval=true
jdbc.username=root
jdbc.password=root
applicationContext.xml
中引入dbconfig.properties
<context:property-placeholder location="classpath:dbconfig.properties"/>
classpath 指的是 编译后 classes 包的路径
在引入过程中, 出现了如下错误:
通配符的匹配很全面, 但无法找到元素 ‘context:property-placeholder’ 的声明。
解决:
在引入context命名空间的同时,
xmlns:context="http://www.springframework.org/schema/context"
在xsi:schemaLocation字符串中添加context相关的解析文件
xsi:schemaLocation=" ... http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context-4.2.xsd"
3)在applicationContext.xml
将相关配置参数化即可
<bean id="connection" class="com.shy.factory.ConnectionFactoryBean">
<property name="driver" value="${jdbc.driver}"/>
<property name="url" value="${jdbc.url}"/>
<property name="user" value="${jdbc.username}"/>
<property name="password" value="${jdbc.password}"/>
bean>
什么是类型转换器
考虑上图中, 在
中将字符串"1"赋值给了Integer类型. 在此过程中, 就借助了Spring框架内置的类型转换器来完成了类型转换.
当Spring框架内置的类型转换器无法满足我们的需求时(当然, 绝大多数情况下会满足的),
<bean id="driver" class="com.shy.entity.Driver">
<property name="name" value="Lando Norris"/>
<property name="number" value="4"/>
<property name="birthday" value="1999-11-13"/>
bean>
public class Driver implements InitializingBean, DisposableBean {
public String name;
public int number;
public Date birthday;
// .......
}
我们便可以自定义类型转换器
实现Converter
接口, 自定义转换策略
public class DateConverter implements Converter<String, Date> {
private String pattern;
public String getPattern() {
return pattern;
}
public void setPattern(String pattern) {
this.pattern = pattern;
}
@Override
public Date convert(String source) {
// 既然使用Spring框架, 可以使用依赖注入设置pattern (DI)
SimpleDateFormat simpleDateFormat = new SimpleDateFormat(pattern);
Date date = null;
try {
date = simpleDateFormat.parse(source);
} catch (ParseException e) {
e.printStackTrace();
}
return date;
}
}
接下来将此Converter类添加至IOC容器中
<bean id="dateConverter" class="com.shy.converter.DateConverter">
<property name="pattern" value="yyyy-MM-dd"/>
bean>
最后我们需要将此Converter作为属性注入到Spring生产Converter的工厂中
因为我们需要告知
Spring
, 我自定义了一个Converter
, 并且采取了何种转换策略
<bean id="conversionService" class="org.springframework.context.support.ConversionServiceFactoryBean">
<property name="converters">
<set>
<ref bean="dateConverter"/>
set>
property>
bean>
@Test
public void test10(){
ApplicationContext applicationContext = new ClassPathXmlApplicationContext("/applicationContext.xml");
Driver driver = (Driver) applicationContext.getBean("driver");
System.out.println("driver.birthday = " + driver.birthday);
}
事实上, Spring框架也为我们提供了 String -> Date
类型的转换器
<property name="birthday" value="1999/11/13"/>
写在最后
个人感觉这种方式比较笨重, 不如借助
SimpleDateFormat
的parse()
方法,先将
String
转为Date
,再将其注入到Bean(这样就不会涉及到Spring框架的类型转换问题)演示:
<bean id="dateFormat" class="java.text.SimpleDateFormat"> <constructor-arg value="yyyy-mm-dd"/> bean> <bean id="driver" class="com.shy.entity.Driver"> <property name="name" value="Lando Norris"/> <property name="number" value="4"/> <property name="birthday"> <bean factory-bean="dateFormat" factory-method="parse"> <constructor-arg value="1999-11-13"/> bean> property> bean>
BeanPostProcessor
前置 / 后置 处理bean
BeanPostProcessor
接口, 对象后处理器(后: 指实例化对象后)
实现此接口提供的两个方法, 可以让Spring在初始化Bean前后对其进行处理
public interface BeanPostProcessor {
// 在Bean初始化之前进行处理
@Nullable
default Object postProcessBeforeInitialization(Object bean, String beanName) throws BeansException {
return bean;
}
// 在Bean初始化之后进行处理
@Nullable
default Object postProcessAfterInitialization(Object bean, String beanName) throws BeansException {
return bean;
}
}
演示:
首先实现BeanPostProcessor
接口
public class CustomBeanPostProcessor implements BeanPostProcessor {
// 当Bean没有进行初始化操作时, Before和After几乎没有区别, 一般只实现After即可
@Override
public Object postProcessBeforeInitialization(Object bean, String beanName) throws BeansException {
return bean; // 即使不实现Before, 也要将Bean返回(继续传递)
}
@Override
public Object postProcessAfterInitialization(Object bean, String beanName) throws BeansException {
// 需要注意的是 BeanPostProcessor 会对所有Bean进行再加工, 所以要进行类型判断
if(bean instanceof Driver){
Driver driver = (Driver)bean;
driver.setName("Daniel Ricciardo");
}
return bean; // 这里bean 和 driver 引用的是同一块内容
}
}
然后注册该Bean
<bean id="customBeanPostProcessor" class="com.shy.beanProcessor.CustomBeanPostProcessor"/>
测试
<bean id="driver" class="com.shy.entity.Driver">
<property name="name" value="Lando Norris"/>
<property name="number" value="4"/>
<property name="birthday" value="1999/11/13"/>
bean>
@Test
public void test11(){
ApplicationContext applicationContext = new ClassPathXmlApplicationContext("/applicationContext.xml");
Driver driver = (Driver) applicationContext.getBean("driver");
System.out.println("driver.name = " + driver.name);
}
// driver.name = Daniel Ricciardo
给女朋友讲解什么是代理模式-java3y
什么是代理设计模式
代理设计模式是Spring核心AOP(面向切面编程)所参考的一个重要的设计模式
什么是代理设计模式 ?
简单来说, 代理设计模式即当前对象不愿意干的,没法干的东西委托给别的对象来做
通过代理类,为原始类提供额外的功能, 同时方便原始类的维护
原始类只专注与核心业务的实现, 而代理类为原始类添加额外功能, 如日志, 事务等
方便维护核心业务以及额外功能
没有使用代理模式时
public class UserServiceImpl implements UserService {
private UserDao userDao; // set注入
public UserDao getUserDao() {
return userDao;
}
public void setUserDao(UserDao userDao) {
this.userDao = userDao;
}
@Override
// 核心代码和额外功能耦合到一起, 不利于维护
public boolean login(String username, String password) {
System.out.println("额外日志功能: 记录登录相关信息.....");
System.out.println("调用UserDao的相关方法, 完成登录的核心代码......");
return true;
}
}
使用静态代理模式
原始类
public class UserServiceImpl implements UserService {
private UserDao userDao; // set注入
public UserDao getUserDao() {
return userDao;
}
public void setUserDao(UserDao userDao) {
this.userDao = userDao;
}
@Override
public boolean login(String username, String password) {
// System.out.println("额外日志功能: 记录登录相关信息....."); 额外功能交给代理类来做
System.out.println("调用UserDao的相关方法, 完成登录的核心代码......");
return true;
}
}
代理类
// 代理类必须实现原始类已实现的接口
public class UserServiceProxy implements UserService {
private UserService userService; // 代理类必须持有原始类的对象(set注入拿到userServiceImpl);
public UserService getUserService() {
return userService;
}
public void setUserService(UserService userService) {
this.userService = userService;
}
@Override
public boolean login(String username, String password) {
System.out.println("额外日志功能: 记录登录相关信息....."); // 代理类实现额外的日志功能
return userService.login(username,password); // 原始类的核心代码
}
}
调用
@Test
public void test13(){
ApplicationContext applicationContext = new ClassPathXmlApplicationContext("/applicationContext.xml");
UserServiceProxy userServiceProxy = (UserServiceProxy) applicationContext.getBean("userServiceProxy");
userServiceProxy.login("username","password"); // 通过代理类调用相关方法
}
/*额外日志功能: 记录登录相关信息.....
调用UserDao的相关方法, 完成登录的核心代码......*/
静态代理模式有何缺点?
试想一下, 假设有数以百计的类, 而每一个类都需要一个代理类, 这就造成项目文件的数量过于庞大, 难以维护.
另外, 假设这些代理类都实现了相同的日志功能, 而一天需求改变了, 需要修改日志功能, 则需要对这些代理类同时做出修改. 难以维护.
何为动态代理?
与静态代理不同, 动态代理的代理类在程序运行时创建.
利用java.lang.reflect
包下的Proxy
类实现动态代理
JDK动态代理的三要素: 1.原始类 2.额外功能 3.代理类实现与原始类相同的接口
@Test
public void test14(){
ApplicationContext applicationContext = new ClassPathXmlApplicationContext("/applicationContext.xml");
UserService userService = (UserService) applicationContext.getBean("userService");
// 利用 java.lang.reflect的Proxy类 实现动态代理
// getClassLoder() 用来借用一个类加载器(任意一个), 以便创建代理类
// 为什么借用?
// 因为代理类是用动态字节码技术创建,没有相应的.class文件. 所以JVM无法为其分配一个ClassLoader, 所以借用
// getInterfaces() 提供原始类的接口, 从而让代理类实现与原始类相同的接口
// InvocationHandler() 用于实现额外功能
UserService userServiceProxy = (UserService) Proxy.newProxyInstance(userService.getClass().getClassLoader(), userService.getClass().getInterfaces(), new InvocationHandler() {
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
// 为login方法增加日志功能
if(method.getName().equals("login")){
System.out.println("额外日志功能: 记录登录相关信息.....");
return (Boolean)method.invoke(userService,args); // 调用原始类的核心功能
}else{
return (Boolean)method.invoke(userService,args);
}
}
});
// 通过代理类调用方法
boolean result = userServiceProxy.login("username","password");
}
JDK动态代理的原理分析
孙哥说Spring5 P94-P96
前文JDK动态代理中, 代理类需要与原始类实现相同的接口. 这样可以保证代理类与原始类方法一致,从而进行额外功能的增强
假如原始类没有接口, 该如何进行代理呢?
可以使用继承, 从而保证方法一致, 这也是cglib动态代理的特点
利用org.springframework.cglib.proxy
包下的Enhancer
实现cglib
动态代理
// 原始类
public class DriveService {
// 进站
public void box(){
System.out.println("Box this lap");
}
// 打开drs
public void openDrs(){
System.out.println("打开drs");
}
}
/**
* Cglib动态代理
*/
@Test
public void test18(){
ApplicationContext applicationContext = new ClassPathXmlApplicationContext("/applicationContext.xml");
final DriveService driveService = (DriveService) applicationContext.getBean("driveService");
Enhancer enhancer = new Enhancer(); // 利用 org.springframework.cglib.proxy包下的Enhancer实现cglib动态代理
enhancer.setClassLoader(driveService.getClass().getClassLoader());//设置类加载器(用于创建代理类,借用任意一个)
enhancer.setSuperclass(driveService.getClass()); // 设置父类(cglib使用继承实现动态代理)
enhancer.setCallback(new MethodInterceptor() {
@Override
// 原始类对象, 调用的原始类方法, 参数, 原始类的代理
public Object intercept(Object o, Method method, Object[] args, MethodProxy methodProxy) throws Throwable {
if(method.getName().equals("box")){
Object result = method.invoke(driveService,args);
System.out.println("进站耗时13s, 我法乙烷"); // 实现额外功能
return result;
}else{
return method.invoke(driveService,args);
}
}
}); // 利用MethodInterceptor设置CallBack(实现额外功能)
DriveService driveServiceProxy = (DriveService) enhancer.create(); // 创建代理类
driveServiceProxy.box();
driveService.openDrs();
}
/*
Box this lap
进站耗时13s, 我法乙烷
打开drs
*/
Spring AOP 参照以上两种动态代理模式, 封装了Spring框架下的动态代理, 借助此功能, 更容易实现动态代理.
AOP(Aspect Oriented Programing): 面向切面编程, 以切面(切点 +额外功能)为基本单位的程序开发,通过切面间的彼此协同,相互调用,完成程序的构建. 是对OOP(面向对象编程)的补充
动态: Spring框架在运行时, 通过动态字节码技术在JVM中创建代理类.
首先需要导入相关依赖
<dependency>
<groupId>org.springframeworkgroupId>
<artifactId>spring-aopartifactId>
<version>5.3.9version>
dependency>
<dependency>
<groupId>org.aspectjgroupId>
<artifactId>aspectjrtartifactId>
<version>1.9.7version>
<scope>runtimescope>
dependency>
<dependency>
<groupId>org.aspectjgroupId>
<artifactId>aspectjweaverartifactId>
<version>1.9.7version>
<scope>runtimescope>
dependency>
<dependency>
<groupId>org.springframeworkgroupId>
<artifactId>spring-coreartifactId>
<version>5.3.9version>
dependency>
然后定义原始类
public interface UserService {
boolean login(String username, String password);
}
public class UserServiceImpl implements UserService {
private UserDao userDao; // set注入
public UserDao getUserDao() {
return userDao;
}
public void setUserDao(UserDao userDao) {
this.userDao = userDao;
}
@Override
public boolean login(String username, String password) {
System.out.println("调用UserDao的相关方法, 完成登录的核心代码......");
return true;
}
}
然后实现MethodBeforeAdvice
接口, 编写额外功能
public class Log implements MethodBeforeAdvice {
// before方法: 定义额外方法, 在执行原始方法之前执行
@Override
public void before(Method method, Object[] objects, Object o) throws Throwable {
System.out.println("method = " + method);
for (Object object : objects) {
System.out.println(object);
}
System.out.println("o = " + o);
}
}
然后定义切入点. 并组装切面
<bean id="log" class="com.shy.dynamic.Log"/>
<aop:config>
<aop:pointcut id="pc" expression="execution(* * (..))"/>
<aop:advisor advice-ref="log" pointcut-ref="pc"/>
aop:config>
测试
<bean id="userDao" class="com.shy.dao.impl.UserDaoImpl"/>
<bean id="userService" class="com.shy.service.impl.UserServiceImpl">
<property name="userDao" ref="userDao"/>
bean>
@Test
public void test15(){
ApplicationContext applicationContext = new ClassPathXmlApplicationContext("/applicationContext.xml");
UserService userService = (UserService) applicationContext.getBean("userService");
userService.login("username","password");
}
/*
method = public abstract boolean com.shy.service.UserService.login(java.lang.String,java.lang.String)
username
password
o = com.shy.service.impl.UserServiceImpl@4bff7da0
调用UserDao的相关方法, 完成登录的核心代码......
*/
public interface MethodBeforeAdvice extends BeforeAdvice {
/*
在原始方法执行之前执行
method:将额外功能添加给了哪个原始方法
args: 原始方法的参数
target:原始方法的类
*/
void before(Method method, Object[] args, @Nullable Object target) throws Throwable;
}
MethodInterceptor, 即方法拦截器. 该接口要比MethodBeforeAdvice灵活的多
根据需要, 可以将额外方法运行在原始方法调用之前(后), 或者在原始方法抛出异常时
此接口与JDK动态代理
中的invokeHandler接口
类似
原始方法调用之前/后
public class Log2 implements MethodInterceptor {
@Override
// 类似于invokeHandler接口的invoke方法. 只不过invokcation封装了原始方法,原始方法的参数等
public Object invoke(MethodInvocation invocation) throws Throwable {
System.out.println("插入到调用原始方法之前");
Object object = invocation.proceed(); // 调用原始方法
System.out.println("插入到调用原始方法之后");
return object; // 原始方法的返回值
}
}
原始方法抛出异常时
public class Log2 implements MethodInterceptor {
@Override
public Object invoke(MethodInvocation invocation) throws Throwable {
Object object = null;
try {
object = invocation.proceed();
}catch (Exception exception){
System.out.println("在原始方法抛出异常时, 执行额外方法");
}
return object;
}
}
另外, 通过改变MethodInterceptor.invoke()
的返回值, 可以直接影响原方法的返回值
/* @return the result of the call to {@link Joinpoint#proceed()};
* might be intercepted by the interceptor
*/
切入点决定了额外功能加入的位置
一般省略 modify-pattern 和 throw-pattern
以方法作为切入点
指定一些方法作为切入点,为其添加额外功能
指定方法名
execution(* deleteProduct (..)) // 指定所有deleteProduct方法作为切入点
指定方法的参数
execution(* * (com.shy.entity.Product,String)) // ,分隔 非java.lang下的要写全限定名
execution(* * (*, ..)) // * 表示任意类型的参数 ..表示任意个(包括0个)任意类型的参数
指定方法的返回值
execution(String * (..)) // 指定所有返回值为String的方法作为切入点
以类作为切入点
指定一些类作为切入点, 为其中的所有方法添加额外功能
execution(* com.shy.service.impl.ProductServiceImpl.*(..))
// 指定com.shy.service.ProductServiceImpl类作为切入点
以包作为切入点
execution(* com.shy.service.*.* (..)) // 指定com.shy.service作为切入点, 不包括service子包下的方法
execution(* com.shy.service..*.* (..)) // 指定com.shy.service作为切入点, 包括service子包下的方法
execution()
可以将 方法 / 类 / 包 作为切入点, 比较全面
args()
args()
用于匹配方法参数的匹配
// 匹配参数为两个String的方法
args(String,String) 或者 execution(* *(String, String))
within()
within()
用于类/包切入点的匹配
// 匹配 ProductServiceImpl类
within(com.shy.service.impl.ProductServiceImpl)
或 execution(* com.shy.service.impl.ProductServiceImpl.*(..))
// 匹配 com.shy.service包
within(com.shy.service..*)
或 execution(* com.shy.service..*.* (..))
annotation()
annotation()
用于匹配具有指定注解的方法
// 匹配具有 @Override 注解的方法
@annotation(java.lang.Override)
另外切入点函数支持逻辑运算 and
和or
在前面实现Spring动态代理时, 曾对applicationContext.xml
进行过配置
<bean id="userService" class="com.shy.service.impl.UserServiceImpl">
<property name="userDao" ref="userDao"/>
bean>
为什么注册的是UserServiceImpl
, 最终拿到的确实它的代理类? 原因在于BeanPostProcessor
的处理
利用BeanPostProcessor
模拟Spring AOP
实现BeanPostProcessor
接口
public class MyBeanPostProcessor implements BeanPostProcessor {
@Override
public Object postProcessBeforeInitialization(Object bean, String beanName) throws BeansException {
return bean;
}
@Override
public Object postProcessAfterInitialization(Object bean, String beanName) throws BeansException {
final Object finalBean = bean;
InvocationHandler handler = new InvocationHandler() {
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
System.out.println("额外功能----");
return method.invoke(finalBean,args);
}
};
// 通过JDK动态代理(也可以使用cglib动态代理)拿到Bean的代理类
bean = Proxy.newProxyInstance(bean.getClass().getClassLoader(), bean.getClass().getInterfaces(), handler);
return bean;
}
}
注册MyBeanPostProcessor
<bean id="myBeanPostProcessor" class="com.shy.beanProcessor.MyBeanPostProcessor"/>
这样便可以拿到代理类
@Test
public void test19(){
ApplicationContext applicationContext = (ApplicationContext) new ClassPathXmlApplicationContext("/applicationContext.xml");
UserService userService = (UserService) applicationContext.getBean("userService"); // 拿到userServiceImpl的代理类
}
编写切面类, 在切面类中定义切点以及额外功能
利用注解声明切面类
@Aspect // @Aspect声明该类是一个切面(在其中 定义切点 + 定义额外功能)
public class MyAspect {
@Around("execution(* login(..))") // 在该@Around注解中定义切入点
// 在around方法中实现额外功能 返回值和参数固定, 方法名可以自定义
// 类似于非注解实现Spring动态代理时的 public Object invoke(MethodInvocation invocation); 方法
public Object around(ProceedingJoinPoint joinPoint) throws Throwable {
System.out.println("利用注解, 添加额外功能...");
Object result = joinPoint.proceed(); // 执行原方法
return result;
}
}
在applicationContext.xml
中注册该切面类, 并告知Spring框架利用了注解形式实现动态代理
<bean id="around" class="com.shy.aspect.MyAspect"/>
<aop:aspectj-autoproxy/>
测试
@Test
public void test(){
ApplicationContext applicationContext = (ApplicationContext) new ClassPathXmlApplicationContext("/applicationContext.xml");
UserService userService = (UserService) applicationContext.getBean("userService");
userService.login("username","password");
userService.register("username","password");
}
/*
利用注解, 添加额外功能...
UserServiceImpl.login
UserServiceImpl.register
*/
@Pointcut
可以定义一个切点, 在@Aspect
中可以直接使用, 方便维护
@Aspect
public class MyAspect {
@Pointcut("execution(* login(..))") // 定义一个切点
public void pointcut(){} // pointcut() 代表了这个切点
@Around(value = "pointcut()")
public Object around(ProceedingJoinPoint joinPoint) throws Throwable {
System.out.println("利用注解, 添加额外功能...");
Object result = joinPoint.proceed();
return result;
}
@Around("pointcut()")
public Object around2(ProceedingJoinPoint joinPoint) throws Throwable {
System.out.println("又一个额外功能...");
Object result = (Object) joinPoint.proceed();
return result;
}
}
对于实现了接口的实现类
Spring默认使用JDK动态代理
, 可以利用proxy-target-class="true
切换为Cglib动态代理
<aop:aspectj-autoproxy proxy-target-class="true"/>
<aop:config proxy-target-class="true">
......
aop:config>
对于没有实现接口的类
Spring只能使用Cglib动态代理
, 因为JDK动态代理
只能对实现了接口的类生成代理
此时,
proxy-target-class
的值是无效的, 只能使用Cglib动态代理
public class UserServiceImpl implements UserService, ApplicationContextAware {
.......
private ApplicationContext applicationContext;
@Override
public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
this.applicationContext = applicationContext;
}
.......
}
实现ApplicationContextAware
接口, 可以为当前Bean
传入Spring上下文中已经存在的ApplicationContext
对象.
而不用重新new
一个, 从而节省资源(ApplicationContext
对象十分消耗资源)
如我想在UserServiceImpl
的register
方法中调用该类的代理类的login
方法
public class UserServiceImpl implements UserService, ApplicationContextAware {
private UserDao userDao; // set注入
public UserDao getUserDao() {
return userDao;
}
public void setUserDao(UserDao userDao) {
this.userDao = userDao;
}
private ApplicationContext applicationContext; // 利用setApplicationContext()传入对象
@Override
public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
this.applicationContext = applicationContext;
}
@Override
public boolean login(String username, String password) {
System.out.println("UserServiceImpl.login");
return true;
}
@Override
public boolean register(String username, String password) {
System.out.println("UserServiceImpl.register");
System.out.println("在register方法内部调用login方法");
UserService userService = (UserService) applicationContext.getBean("userService"); // 拿到代理类
userService.login(username,password); // 调用代理类的login方法
// this.login(); 此调用的并不是代理类的login()方法,而是本类的login()
return true;
}
}
参考: Spring AOP(一) AOP基本概念 - SegmentFault 思否
AOP
AOP主要由以Spring AOP
为代表的动态代理 和 以AspectJ
为代表的静态代理
切面 Aspect
切面由切入点和通知(横切逻辑, 即额外功能的逻辑)组成, 可以包括同一类型的不同增强方法
Spring AOP 负责将切面定义的横切逻辑织入切入点中
@Aspect
public class LogAspect {
// 切入点
@Pointcut("execution(* login(..))")
public void pointcut(){}
@Around(value = "pointcut()")
public Object logMethod1(ProceedingJoinPoint joinPoint) throws Throwable {
System.out.println("日志方法1"); // 通知
Object result = joinPoint.proceed();
return result;
}
@Around(value = "pointcut()")
public Object logMethod2(ProceedingJoinPoint joinPoint) throws Throwable {
System.out.println("日志方法2"); // 通知
Object result = joinPoint.proceed();
return result;
}
}
Pointcut
Advice
Weaving
Spring从2.x版本开始引入注解编程, 达到简化xml配置的效果. 在后序版本逐渐完善. 随着Spring Boot的推出, 开始推广注解编程
在Spring框架中采用注解形式编程, 首先需要引入注解扫描
<context:component-scan base-package="com.shy"/>
需要注意的是, Spring框架中, 注解注入会在xml注入之前执行, 因而会被xml注入的结果覆盖
Annotation injection is performed before XML injection, thus the latter configuration will override the former for properties wired through both approaches.
@Component
用来注册相应的Bean
, 可以用来替代
使用@Component
注解, bean
的class
由Spring框架通过反射自动获得,id
默认为类名首字母小写, 也可以自定义id
@Component("u")
public class User {
}
另外Spring为了更加语义化, 提供了几个与@Component
用法一模一样的衍生注解
@Repository
public class UserDaoImpl{}
@Service
public class UserServiceImpl{}
@Controller
public class UserController{}
@Scope
与@Component
一起连用时,可以控制Spring创建对象的次数
@Component
@Scope("singleton") // 可以不写该注解, 默认值为单例对象, 随着ApplicationContext对象一起被创建
public class User {}
@Scope("prototype") // 多例对象
@Lazy
可以延迟创建单例对象, 即当需要该对象时再创建
<bean id="user" class="com.shy.entity.User" lazy-init="true | false"/>
@Lazy
@Component
public class User {}
@PostConstruct
用于声明在Bean
的初始化阶段, 用于初始化操作的方法
@PreDestroy
用于声明在Bean
的销毁阶段, 用于销毁操作的方法
以往的xml形式
<bean id="user" class="com.shy.entity.User" init-method="myInit" destroy-method="myDestroy"/>
注解
@Component
public class User {
.......
@PostConstruct
public void myInit(){
System.out.println("User.myInit");
}
@PreDestroy
public void myDestroy(){
System.out.println("User.myDestroy");
}
}
这两个注解并不是 Spring 提供的,而是兼容了JSR(JavaEE规范)250
对非JDK类型变量的注入,主要包括@Autowired
(更常用) 和 @Resource
两种
对于接口,
@Autowired
会注入其实现类
@Autowired
注解可以**基于by type
**完成[自动装配](#autowire 自动装配), 可以写在以下三个地方
成员变量前(更常用)
@Service
public class UserServiceImpl implements UserService{
@Autowired // 利用反射进行注入
private UserDao userDao;
}
set方法前
@Service
public class UserServiceImpl implements UserService{
private UserDao userDao;
@Autowired // 调用set方法进行注入
public void setUserDao(UserDao userDao) {
this.userDao = userDao;
}
}
构造函数前
@Service
public class UserServiceImpl implements UserService{
private UserDao userDao;
@Autowired // 调用该构造函数进行注入
public UserServiceImpl(UserDao userDao) {
this.userDao = userDao;
}
}
// Autowiring by type from bean name 'userServiceImpl' via constructor to bean named 'userDaoImpl'
对于自动装配, 我们发现虽然都是对接口进行注入private UserDao userDao
, 但经过测试会发现Spring
为其注入的是userDaoImpl
Creating shared instance of singleton bean 'userDaoImpl'
这是因为接口是无法实例化的, 所以都是其实现类. 这也符合applicationContext.xml
中注册时的习惯
<bean id="userDao" class="com.shy.dao.impl.userDaoImpl"/>
此外, 注入对象的类型还可以是其子类.
@Component
public class UserServiceImpl implements UserService {
@Autowired
private UserDaoImpl userDaoImpl;
}
@Component
public class SonClass extends UserDaoImpl{
}
userService.getUserDaoImpl() = com.shy.service.SonClass@4f2b503c
两个异常
在上述案例中, 进行注入时, 一定要保证userDao
已经被Spring创建好了, 否则会出NoSuchBeanDefinitionException
异常
不过,可以通过设置@Autowired(required = false)
来避免.
此外, 使用@Autowired
进行自动转配时, 可能出现找到多个候选的情况, 此时会出现NoUniqueBeanDefinitionException
异常
@Repository
public class UserDaoImpl implements UserDao {
}
@Repository
public class UserDaoImpl2 implements UserDao {
}
// expected single matching bean but found 2: userDaoImpl,userDaoImpl2
@Qualifier
注解用来配合@Autowired
使用, 使@Autowired
注解基于by name
(bean
的id
值)自动装配
public class UserServiceImpl implements UserService, ApplicationContextAware {
@Autowired // 必须有, @Qualifier只是限定一下名称
@Qualifier("userDaoImpl2")
private UserDao userDao;
}
@Resource
不是 Spring 提供的,而是兼容了JSR(JavaEE规范)250
@Resource
支持by name
(默认) 和 by type
两种方式进行自动装配
@Value
注解配合.properties
文件可以完成对Java基本数据类型变量
的注入
演示:
编写driver.properties
文件
name=Lando Norris
number=4
birthday=2021/08/08
编写实体类
@PropertySource("/driver.properties") // 引入 driver.properties
@Component
public class Driver {
@Value("${name}")
public String name;
@Value("${number}")
public int number;
@Value("${birthday}")
public Date birthday;
}
@PropertySource
注解用来引入.properties
文件底层实现是
PropertySourcePalaceholderConfigurer
,它根据当前 Spring Environment及其PropertySources集解析 bean 定义属性值和@Value注释中的 ${…} 占位符。
测试
@Test
public void test(){
ApplicationContext applicationContext = new ClassPathXmlApplicationContext("applicationContext2.xml");
Driver driver = (Driver) applicationContext.getBean("driver");
System.out.println("driver.name = " + driver.name);
System.out.println("driver.number = " + driver.number);
System.out.println("driver.birthday = " + driver.birthday);
}
// driver.name = Lando Norris
// driver.number = 4
// driver.birthday = Sun Aug 08 00:00:00 CST 2021
此方式并不支持集合类型, 个人猜测应该是.properties
文件不支持书写相关集合类型如果想要注入集合类型, 需要使用SpELl表达式
myList=1,2,3,4,5
@Value("#{'${myList}'.split(',')}") public List<Integer> myList;
注入Map类型
myMap={'name':'Lando Norris', 'number':'4'}
@Value("#{${myMap}}") public Map<String, String> myMap;
官方对
component-scan
的解释:Scans the classpath for annotated components that will be auto-registered as
Spring beans. By default, the Spring-provided @Component, @Repository, @Service,
@Controller, @RestController, @ControllerAdvice, and @Configuration stereotypes
will be detected.
<context:component-scan base-package="com.shy"/> // 对com.shy及其子包进行注解扫描
在进行注解扫描时, 我们曾配置过注解扫描. Spring注解扫描主要有两种策略: 排除方式 和 包含方式.
排除方式
不对特定的包/类进行注解扫描
<context:component-scan base-package="com.shy">
<context:exclude-filter type="" expression=""/>
context:component-scan>
type
属性可以是:
annotation
不对带有特定的注解的类进行扫描
<context:exclude-filter type="annotation" expression="org.springframework.stereotype.Component"/>
assignable
不对特定的类进行扫描
<context:exclude-filter type="assignable" expression="com.shy.entity.Driver"/>
aspectj
根据切入点表达式排除特定的类
<context:exclude-filter type="aspectj" expression="* *(..)"/>
此外,还有两种不常用的属性:
regex
根据正则表达式排除特定的类custom
自定义规则(底层框架开发中大量使用)包含方式
只对特定的包/类进行注解扫描
<context:component-scan base-package="com.shy" use-default-filters="false">
<context:include-filter type="" expression=""/>
context:component-scan>
type
的值与上同, 不过需要设置use-default-filters="false"
,既不采用默认的材料进行注解扫描
以上所涉及的注解均为Spring2.x
时引入的注解, 目的是简化xml
的配置
以下所涉及的注解均为Spring3.x
后引入的注解, 目的是取代xml
开发方式
@Configuration
注解标注的类可以成为配置Bean
(更专业的术语是 JavaConfig
)效果上等同于applicationContext.xml
从而来代替applicationContext.xml
配置文件
@Configuration
public class AppConfiguration {
}
创建Spring工厂
ApplicationContext applicationContext = new AnnotationConfigApplicationConext(AppConfiguration.class);
// Spring自动扫描 com.shy.config及其子包下所有带有@Configuration注解的Bean, 并进行注册
ApplicationContext applicationContext = new AnnotationConfigApplicationContext("com.shy.config");
@Bean
可以在@Configuration
标注的类中使用, 相当于
标签,用于bean
的注册
Q: 有
@Component
注解来注册Bean
,还需要@Bean
注解 ?A: 假如在注册第三方提供的类时, 无法修改为其源码加上
@Component
注解, 这时就需要@Bean
注解
此外@Bean
可以自定义id
, 并且控制对象的创建次数
@Configuration
public class AppConfiguration {
@Bean("u")
@Scope("singleton")
// 方法名即id值
public UserDao userDao(){
// 创建对象的相关代码
return new UserDaoImpl();
}
}
底层原理:
使用
@Bean
注解, 实际上是把对象交给IOC容器进行管理.我们想要完成的核心功能是 new 一个对象, 而Spring为其添加了额外功能, 从而管理
Bean
的生命周期.本质上来说, 这也是
AOP
的一种体现, 通过Cglib动态代理
实现
自定义类型
@Configuration
public class AppConfiguration {
@Bean
public UserDao userDao(){
return new UserDaoImpl();
}
@Bean
// 入参UserDao会由Spring自动寻找(前提是UserDao也是被Spring管理的bean)
public UserService userService(UserDao userDao){
UserServiceImpl userServiceImpl = new UserServiceImpl();
userServiceImpl.setUserDao(userDao); // 注入userDao
return userServiceImpl();
}
}
Q: 这种方式既new对象又set属性, 怎么体现解耦合的?
A: 其实我也有点疑惑. 按照目前的理解, 这种方式依然是由
Spring
进行管理Bean
的生命周期, 所以解耦合
@Configuration
public class AppConfiguration {
@Bean
public UserDao userDao(){
System.out.println("AppConfiguration.userDao");
return new UserDaoImpl();
}
@Bean
public UserService userService(){
UserServiceImpl userServiceImpl = new UserServiceImpl();
userServiceImpl.setUserDao(userDao());
return userServiceImpl;
}
}
也可以不使用入参UserDao, 而是调用useDao()
配置类内部可以通过方法调用来处理依赖,并且能够保证是同一个实例,都指向IoC内的那个单例
Spring的@Configuration配置类-Full和Lite模式_demon7552003的小本本-CSDN博客
Java基本数据类型
@Configuration
@PropertySource("/driver.properties")
public class AppConfiguration2 {
@Value("${name}")
private String name;
@Value("${number}")
private int number;
@Bean
public Driver driver(){
Driver driver = new Driver();
driver.setName(name);
driver.setNumber(number);
return driver;
}
}
@ComponentScan
用来替代
标签, 进行注解扫描
默认扫描范围是该类所在的包
具体属性的用法参照注解扫描详解
排除方式
@Configuration
@ComponentScan(basePackages = "com.shy",
excludeFilters = {
@ComponentScan.Filter(type = FilterType.ANNOTATION, value = {Repository.class}),
@ComponentScan.Filter(type = FilterType.ASSIGNABLE_TYPE, value = {Driver.class}),
@ComponentScan.Filter(type = FilterType.ASPECTJ, pattern = "*..User")
})
public class AppConfiguration {}
包含方式
@Configuration
@ComponentScan(basePackages = "com.shy",
includeFilters = {
@ComponentScan.Filter(type = FilterType.ANNOTATION, value = {Repository.class}),
@ComponentScan.Filter(type = FilterType.ASSIGNABLE_TYPE, value = {Driver.class}),
@ComponentScan.Filter(type = FilterType.ASPECTJ, pattern = "*..User")
})
public class AppConfiguration {}
经过前面的学习, 无论是@Component
, 还是@Bean
, 又或是
都可以用来注册Bean.
那么选择哪种方式更好呢 ?
需要注意的是, 这几种方式是有优先级的:@Component
< @Bean
<
优先级高的方式可以覆盖优先级低的配置.
在项目当中, 注册Bean
可能不只使用@Component
这种方式. 有可能也使用了@Bean
和
来注册Bean
.
倘若我们对项目进行维护, 并把@Component
作为注册Bean
的主要方式.
我们该如何将带有@Component
的Bean 和 在applicationContext.xml
中配置的Bean 整合到@Configuration
标注的配置类中呢 ?
多个配置Bean
的整合
将@Component
标注的Bean
整合到配置Bean
中
利用@ComponentScan
注解扫描注解
@Configuration
@ComponentScan("com.shy.dao")
public class AppConfig4 {
@Autowired
private UserDao userDao;
@Bean
public UserServiceImpl userService(){
UserServiceImpl userService = new UserServiceImpl();
userService.setUserDao(userDao);
return userService;
}
}
@Repository
public class UserDaoImpl implements UserDao {
}
将applicationContext.xml
中配置的Bean
整合到配置Bean
中
利用@ImportResource
注解导入配置文件
@Configuration
@ImportResource("/applicationContext2.xml")
public class AppConfig4 {
@Autowired
private UserDao userDao;
@Bean
public UserServiceImpl userService(){
UserServiceImpl userService = new UserServiceImpl();
userService.setUserDao(userDao);
return userService;
}
}
public class UserDaoImpl implements UserDao {
}
<bean id="userDao" class="com.shy.dao.impl.UserDaoImpl"/>
以上三种场景下进行配置Bean
的整合时, 假如需要进行注入时, 可以使用@Autowired
自动注入
原始类
public class ProductServiceImpl implements ProductService {
private ProductDao productDao;
@Override
public void insertProduct(Product product) {
System.out.println("ProductServiceImpl.insertProduct");
}
@Override
public void deleteProduct(Product product){
System.out.println("ProductServiceImpl.deleteProduct");
}
}
切面类
@Aspect
@Component
public class MyAspect {
@Pointcut("execution(* com.shy.service.impl.ProductServiceImpl.* (..))")
public void pointCut(){};
@Around(value = "pointCut()")
public Object around2(ProceedingJoinPoint proceedingJoinPoint) throws Throwable {
System.out.println("纯注解实现Spring动态代理");
Object result = proceedingJoinPoint.proceed();
return result;
}
}
配置Bean
@EnableAspectJAutoProxy
相当于, 即使用注解形式自动配置AOP动态代理
@Configuration
@ComponentScan("com.shy")
@EnableAspectJAutoProxy
public class AppConfig {
}
测试类
@Test
public void test38(){
ApplicationContext applicationContext = new AnnotationConfigApplicationContext(AppConfig.class);
ProductServiceImpl productServiceImpl = (ProductServiceImpl) applicationContext.getBean("productServiceImpl");
productServiceImpl.insertProduct(new Product());
productServiceImpl.deleteProduct(new Product());
}
/**
纯注解实现Spring动态代理
ProductServiceImpl.insertProduct
纯注解实现Spring动态代理
ProductServiceImpl.deleteProduct
**
参考: mybatis-spring官方文档
SSM整合官方演示项目
我们首先考虑一下在只使用Mybatis时的开发步骤:
1.创建实体类
2.创建并配置mybatis-config.xml(如 environment setting 等)
3.在mybatis-config.xml中配置实体别名 alias
4.创建表
5.创建XXXMapper.java接口
6.创建并实现XXXMapper.xml配置文件
7.在mybatis-config.xml中注册XXXMapper.xml
8.编写MybatisUtil获取SqlSessionFactory,进而获取SqlSession, 进而getMapper
9.进行CRUD操作(对于修改操作, 还需要提交事务)
可以看到在只使用Mybatis进行开发时的步骤还是相当繁琐的, 其中一些步骤是重复的.
2.创建并配置mybatis-config.xml(如 environment setting 等)
3.在mybatis-config.xml中配置实体别名 alias
7.在mybatis-config.xml中注册XXXMapper.xml
8.编写MybatisUtil获取SqlSessionFactory,进而获取SqlSession, 进而getMapper
对于以上这些步骤, 我们完全可以交由Spring框架
进行管理, 从而简化开发,专注于核心业务逻辑的开发
引入整合需要的相关依赖(省略了Mybatis,Spring,Mysql相关依赖)
<dependency>
<groupId>org.mybatisgroupId>
<artifactId>mybatis-springartifactId>
<version>2.0.6version>
dependency>
<dependency>
<groupId>com.alibabagroupId>
<artifactId>druidartifactId>
<version>1.2.6version>
dependency>
<dependency>
<groupId>org.springframeworkgroupId>
<artifactId>spring-jdbcartifactId>
<version>5.3.9version>
dependency>
在applicationContext.xml中进行相关配置
值得注意的是, 在使用Spring整合Mybatis之后,我们可以省略
mybais-config.xml
配置文件
相关配置, 可以通过Bean的属性进行注入
主要完成三步:
- 配置 dataSource
- 创建 SqlSessionFatory
- 将XXXMapper接口交由Spring管理
<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:context="http://www.springframework.org/schema/context"
xmlns:aop="http://www.springframework.org/schema/aop" xmlns:mybatis="http://mybatis.org/schema/mybatis-spring"
xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context-4.2.xsd http://www.springframework.org/schema/aop https://www.springframework.org/schema/aop/spring-aop.xsd http://mybatis.org/schema/mybatis-spring http://mybatis.org/schema/mybatis-spring.xsd">
<context:property-placeholder location="dbconfig.properties"/>
<bean id="dataSource" class="com.alibaba.druid.pool.DruidDataSource">
<property name="driverClassName" value="${jdbc.driver}"/>
<property name="url" value="${jdbc.url}"/>
<property name="username" value="${jdbc.username}"/>
<property name="password" value="${jdbc.password}"/>
bean>
<bean id="sqlSessionFactoryBean" class="org.mybatis.spring.SqlSessionFactoryBean">
<property name="dataSource" ref="dataSource"/>
<property name="typeAliasesPackage" value="com.shy.entity"/>
<property name="mapperLocations" value="classpath:mapper/*Mapper.xml"/>
bean>
<mybatis:scan base-package="com.shy.mapper"/>
beans>
进行测试即可
@Test
public void testActorMapper(){
ApplicationContext applicationContext = new ClassPathXmlApplicationContext("/applicationContext.xml");
// 整合之后, 便可以直接getBean()获取XXXMapper
ActorMapper actorMapper = (ActorMapper) applicationContext.getBean(ActorMapper.class);
Actor actor = actorMapper.selectActorById(1);
System.out.println(actor.toString());
actor.setActorChineseName("吉米");
actorMapper.updateActorById(actor);
// 这里不需要提交事务, Druid连接池会默认提交事务
// 在学习Spring事务管理之后, 我们会使用Spring来管理事务
}
引入配置Bean
主要任务包括:
配置数据源
-引入外部properties文件
通过SqlSessionFatoryBean拿到SQLSessionFactory
-添加数据源
-设置实体别名
-注册mapper.xml
-扫描所有Mapper接口
开启注解扫描
@Configuration
@MapperScan("com.shy.mapper") // 扫描注册所有的Mapper接口, 作用类似于MapperScannerConfigurer
@ComponentScan("com.shy") // 扫描注解
@PropertySource("/dbconfig.properties") // 引入外部属性文件
public class SpringMybatisConfig {
@Value("${jdbc.driver}")
private String driverName;
@Value("${jdbc.url}")
private String url;
@Value("${jdbc.username}")
private String username;
@Value("${jdbc.password}")
private String password;
/**
* 配置Druid连接池
*/
@Bean
public DruidDataSource dataSource() {
DruidDataSource dataSource = new DruidDataSource();
dataSource.setDriverClassName(driverName);
dataSource.setUrl(url);
dataSource.setUsername(username);
dataSource.setPassword(password);
return dataSource;
}
/**
* 配置SqlSessionFactory
*/
@Bean
public SqlSessionFactory sqlSessionFactory() throws Exception {
SqlSessionFactoryBean sqlSessionFactoryBean = new SqlSessionFactoryBean();
// 配置数据源
sqlSessionFactoryBean.setDataSource(dataSource());
// 配置实体类别名
sqlSessionFactoryBean.setTypeAliasesPackage("com.shy.entity");
// 注册 Mapper文件
// Spring提供了ResourcePatternResolver,可以使用通配符注册Mapper文件
ResourcePatternResolver resourcePatternResolver = new PathMatchingResourcePatternResolver();
Resource[] resources = resourcePatternResolver.getResources("mapper/*Mapper.xml");
sqlSessionFactoryBean.setMapperLocations(resources);
// 创建sqlSessionFactory
SqlSessionFactory sqlSessionFactory = sqlSessionFactoryBean.getObject();
return sqlSessionFactory;
}
}
测试
@Service
public class ActorServiceImpl implements ActorService {
@Autowired // 自动注入的其实是接口的代理类
private ActorMapper actorMapper;
// ......
}
@Test
public void test01(){
ApplicationContext applicationContext = new AnnotationConfigApplicationContext(SpringMybatisConfig.class);
ActorService actorService = (ActorService) applicationContext.getBean("actorServiceImpl");
Actor actor = actorService.getActorById(1);
System.out.println(actor.toString());
}
// Actor{ActorId=1, ActorChineseName='鲍勃·奥登科克', ActorOriginName='Bob Odenkirk', ActorGender='男'}
参考: mybatis-spring–事务
Spring事务管理提供了多种方式, 这里我们主要介绍 标签式事务管理
声明式事务管理建立在AOP之上,其本质是对方法前后进行拦截,然后在目标方法开始之前创建或者加入一个事务,执行完目标方法之后根据执行的情况提交或者回滚。
事务(Transaction)简单来说, 就是数据库进行的一系列操作, 而这一系列操作是一个原子整体, 不可被分割.
倘若事务中的某一步出现异常, 则必须回滚. 也就是说事务的一系列操作要么全部完成, 要么全部不完成, 从而保证了数据库的完整性
事务的四个特性: ACID
无论是何种框架, 底层均通过
Connection
对象来完成事务控制
JDBC
Connection.setAutoCommit(false);
Connection.commit()
Connection.rollback()
Mybatis
Mybatis 自动开启事务
SqlSession.commit()
SqlSession.rollback()
SqlSession.close() // 封装了 SqlSession.rollback
引入依赖
<dependency>
<groupId>org.springframeworkgroupId>
<artifactId>spring-txartifactId>
<version>5.3.9version>
dependency>
创建Service, 并交由Spring管理
实战当中, Service层往往编写核心业务逻辑, 需要事务控制
public class ActorServiceImpl implements ActorService {
private ActorMapper actorMapper; // 整合Mybatis时注入Spring容器
public ActorMapper getActorMapper() {
return actorMapper;
}
public void setActorMapper(ActorMapper actorMapper) {
this.actorMapper = actorMapper;
}
@Override
public Actor getActorById(int id) {
Actor actor = actorMapper.selectActorById(id);
return actor;
}
}
<bean id="actorService" class="com.shy.service.impl.ActorServiceImpl">
<property name="actorMapper" ref="actorMapper"/>
bean>
使用Spring进行事务管理
<bean id="dataSourceTransactionManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
<property name="dataSource" ref="dataSource"/>
bean>
<tx:annotation-driven transaction-manager="dataSourceTransactionManager"/>
为相应的类或方法添加事务控制
// @Transactional 为该类添加事务
public class ActorServiceImpl implements ActorService {
// ......
@Transactional // 为该方法添加事务
@Override
public Actor getActorById(int id) {
Actor actor = actorMapper.selectActorById(id);
return actor;
}
}
测试
/**
* 测试事务控制
*/
@Test
public void test02(){
ApplicationContext applicationContext = new ClassPathXmlApplicationContext("/applicationContext.xml");
ActorService actorService = (ActorService) applicationContext.getBean("actorService");
Actor actor = actorService.getActorById(1);
System.out.println(actor.toString());
}
// 通过debug信息看出, 现在由Spring来管理事务
12:07:39.120 [main] DEBUG org.springframework.jdbc.datasource.DataSourceTransactionManager - Creating new transaction with name [com.shy.service.impl.ActorServiceImpl.getActorById]: PROPAGATION_REQUIRED,ISOLATION_DEFAULT
引入依赖
同半注解形式
创建Service, 并交由Spring管理
同半注解形式
使用Spring进行事务管理
不再需要
@Transactional
注解
<tx:advice id="transactionInterceptor" transaction-manager="dataSourceTransactionManager">
<tx:attributes>
<tx:method name="com.shy.service.impl.ActorService.getActorById" isolation="DEFAULT"/>
<tx:method name="*"/>
tx:attributes>
tx:advice>
<aop:config>
<aop:pointcut id="pc" expression="execution(* com.shy.service.ActorService.getActorById(..))"/>
<aop:advisor advice-ref="transactionInterceptor" pointcut-ref="pc"/>
aop:config>
测试
/**
* 测试事务控制
*/
@Test
public void test02(){
ApplicationContext applicationContext = new ClassPathXmlApplicationContext("/applicationContext.xml");
ActorService actorService = (ActorService) applicationContext.getBean("actorService");
Actor actor = actorService.getActorById(1);
System.out.println(actor.toString());
}
14:09:02.303 [main] DEBUG org.springframework.transaction.interceptor.NameMatchTransactionAttributeSource - Adding transactional method [com.shy.service.impl.ActorService.getActorById] with attribute [PROPAGATION_REQUIRED,ISOLATION_DEFAULT]
主要任务包括:
配置数据源
配置事务管理器
通过
@EnableTransactionManagement
开启注解扫描
引入配置Bean
@Configuration
@ComponentScan("com.shy") // 扫描注解
@PropertySource("/dbconfig.properties") // 引入外部属性文件
@EnableTransactionManagement // 开启注解扫描, 为@Transactional的类或方法添加事务控制, 作用类似于
public class SpringMybatisConfig {
@Value("${jdbc.driver}")
private String driverName;
@Value("${jdbc.url}")
private String url;
@Value("${jdbc.username}")
private String username;
@Value("${jdbc.password}")
private String password;
/**
* 配置Druid连接池
*/
@Bean
public DruidDataSource dataSource() {
DruidDataSource dataSource = new DruidDataSource();
dataSource.setDriverClassName(driverName);
dataSource.setUrl(url);
dataSource.setUsername(username);
dataSource.setPassword(password);
return dataSource;
}
/**
* 配置Spring事务管理器
*/
@Bean
public DataSourceTransactionManager dataSourceTransactionManager(){
DataSourceTransactionManager dataSourceTransactionManager = new DataSourceTransactionManager();
dataSourceTransactionManager.setDataSource(dataSource());
return dataSourceTransactionManager;
}
}
测试
@Test
public void test01(){
ApplicationContext applicationContext = new AnnotationConfigApplicationContext(SpringMybatisConfig.class);
ActorService actorService = (ActorService) applicationContext.getBean("actorServiceImpl");
Actor actor = actorService.getActorById(1);
System.out.println(actor.toString());
}
// 15:08:44.051 [main] DEBUG o.s.jdbc.datasource.DataSourceTransactionManager - Creating new transaction with name [com.shy.service.impl.ActorServiceImpl.getActorById]: PROPAGATION_REQUIRED,ISOLATION_DEFAULT,timeout_2,-java.lang.RuntimeException,+java.lang.Exception
// 成功使Spring管理事务
参考: 脏写、脏读、不可重复读和幻读 - 知乎 (zhihu.com)
Spring事务传播属性和隔离级别 - Eunice_Sun - 博客园 (cnblogs.com)https://www.cnblogs.com/mseddl/p/11577846.html)
隔离属性解决了多个事务并发时, 可能会产生的问题
脏读
事务A读取了事务B修改但还没有提交的数据, 而在事务B操作失败回滚之后, 事务A读取的数据就成为了不存在的数据
解决:
@Transactional(isolation = Isolation.READ_COMMITTED)
保证一个事务修改的数据提交后才能被另外一个事务读取
Q: 为什么一个事务能读取到其他事务修改后未提交的数据?
不可重复读
事务A读取某一行数据, 而在事务A还未结束时, 事务B对该行数据进行了修改并提交. 事务A再次读取同一行数据时, 数据发生了改变.
即同一事务前后读取的同一行数据不同
解决:
@Transactional(isolation = Isolation.REPEATABLE_READ)
本质是为该行数据加上了一把行锁, 在A结束事务之后, 其他事务才可访问该行数据
幻读
事务A根据条件查询到了一些数据, 而在事务A还未结束时, 事务B插入了几条符合A查询条件的数据,
这时事务A再次根据条件查询数据, 发现符合条件的数据多了.
解决:
@Transactional(isolation = Isolation.SERIALIZABLE)
本质是为该行数据加上了一把表, 在A结束事务之后, 其他事务才可访问该表
默认值
Isolation.DEFAULT // 使用数据库默认隔离级别, 实战当中推荐使用. 而并发产生的问题通过乐观锁来解决
select @@transaction_isolation; // 查看数据库默认隔离级别
总结
传播属性用来解决事务嵌套问题
事务嵌套有可能会导致外层事务丧失原子性
默认值
Propagation.REQUIRED //
// 所以在实战当中, 增删改直接采用默认传播属性, 而对查询方法显式指定其传播属性为 SUPPORTS
针对只进行查询的业务方法, 可以设置readOnly = true
, 提高效率
超时属性指定了事务等待的最长时间
在事务并发时, 事务访问的数据有可能被另一事务加了锁, 这是需要等待
timeout = -1 // 默认值, 采用数据库默认超时时间. 实战一般不需要修改
// 对于RuntimeException及其子类, 默认回滚
rollbackFor = {RuntimeException.class}
// 对于Exception及其子类, 默认提交
noRollbackFor = {Exception.class}
// 实战一般保持默认即可
YAML与xml, properties一样, 都是用来进行配置的文件, 不过YAML比后两者语法上更为简单.
YAML 入门教程 | 菜鸟教程 (runoob.com)
【教程】十分钟让你了解yaml - 轩脉刃de刀光剑影_哔哩哔哩_bilibili
yaml.org
编写driver.yaml
name: Pierre Gasly
number: 10
birthday: 2021/08/14
Spring框架默认是不支持YAML的. 我们可以引入第三方JAR, 解析使用YAML.
这里我们选用SnakeYAML
<dependency>
<groupId>org.yamlgroupId>
<artifactId>snakeyamlartifactId>
<version>1.28version>
dependency>
利用YamlPropertiesFactoryBean
读取driver.yaml
, 并将其转成Properties
集合
利用该Properties
集合构造PropertySourcesPlaceholderConfigurer
对象(@Value
的底层实现)
@Configuration
@ComponentScan("com.shy.entity")
public class APPConfig6 {
@Bean
public PropertySourcesPlaceholderConfigurer propertySourcesPlaceholderConfigurer(){
YamlPropertiesFactoryBean yamlPropertiesFactoryBean = new YamlPropertiesFactoryBean();
yamlPropertiesFactoryBean.setResources(new ClassPathResource("driver.yaml"));
Properties properties = (Properties) yamlPropertiesFactoryBean.getObject();
PropertySourcesPlaceholderConfigurer propertySourcesPlaceholderConfigurer = new PropertySourcesPlaceholderConfigurer();
propertySourcesPlaceholderConfigurer.setProperties(properties);
return propertySourcesPlaceholderConfigurer;
}
}
@Component
public class Driver {
@Value("${name}")
public String name;
@Value("${number}")
public int number;
@Value("${birthday}")
public Date birthday;
测试
@Test
public void test39(){
ApplicationContext applicationContext = new AnnotationConfigApplicationContext(APPConfig6.class);
Driver driver = (Driver) applicationContext.getBean("driver");
System.out.println("driver.name = " + driver.name);
System.out.println("driver.number = " + driver.number);
System.out.println("driver.birthday = " + driver.birthday);
}
/**
driver.name = Pierre Gasly
driver.number = 10
driver.birthday = Sat Aug 14 00:00:00 CST 2021
**/
@Value
注解无法解析.yaml
文件中定义的list
集合
championships:
- 2022
- 2023
- 2024
@Value("${championships}")
public List<Integer> championships;
// Could not resolve placeholder 'championships' in value "${championships}"
(此问题, 在SpringBoot中可以使用@ConfigurationProperties
解决)
在Spring中, 可以利用SpEL表达式来解决
championships: 2022,2023,2024
@Value("#{'${championships}'.split(',')}")
public List<Integer> championships;