Spring相关
目录
文章目录
前言
计划利用一些时间学习一下Spring框架(开始于 2021年7月6日)
视频参考: https://www.bilibili.com/video/BV185411477k
课程笔记参考: 《孙哥说Spring5》学习笔记_代码改变世界-CSDN博客
Spring框架版本 5.1.4 Release
工厂设计模式
工厂模式是Spring核心IOC(控制翻转)所参考的一个重要的设计模式, 可以解耦合
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");
ApplicationContext
前文提到spring参考工厂模式创建对象, 而ApplicationContext
就是Spring框架用于创建对象的工厂
需要注意的是, 创建ApplicationContext
会占用大量资源
所以一般一个应用只创建一个ApplicationContext
对象
有可能被多个线程同时访问, 是线程安全的
此接口有两个实现类:
ClassPathXmlApplicationContext
(非web环境下使用, 如单元测试)
首先创建一个spring工厂
ApplicationContext applicationContext = new ClassPathXmlApplicationContext("/applicationContext.xml");
关于路径问题:
观察
maven
编译后生成的target
目录, 会发现java
和resources
被合并到一个目录.所以可以认为
java
和resource
下的文件的根目录相同
在applicationContext.xml
中配置Product
实体类的信息
<bean>标签
<!--id 唯一的表示 name 该bean的别名 class该bean所代表的的类的全限定名-->
<bean id="product" name="p1,p2,p3" class="com.shy.entity.Product"/>
<!-- 可以不指定 id,name. 系统会自动生成id-->
<bean class="com.shy.entity.Product"/> <!--com.shy.entity.Product#0 -->
<bean class="com.shy.entity.Product"/> <!--com.shy.entity.Product#1 -->
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd">
<bean id="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
XmlWebApplicationContext
(web环境下使用)
ApplicationContext创建对象的原理
(未涉及源码)
在实际开发中,并不是所有的对象都交由applicationContext
创建
像实体类entity
一般由数据库框架来创建
public class Product {
public String name;
public Product() {
System.out.println("调用了无参构造");
}
}
默认情况下, ApplicationContext在创建对象时, 会调用对象的无参构造
整合多个applicationContext.xml
在实际开发中, 往往将applicationContext.xml
文件按照一定的层级分开(如DAO,Service,Controller), 从而方便维护
假设有以下几个applicationContext.xml文件:
applicationContext-DAO.xml
applicationContext-Service.xml
applicationContext-Controller.xml
-
使用通配符
*
ApplicationContext applicationContext = (ApplicationContext) new ClassPathXmlApplicationContext("applicationContext-*.xml");
-
使用
import
标签<!-- applicationContext.xml --> <?xml version="1.0" encoding="UTF-8"?> <beans xmlns="http://www.springframework.org/schema/beans" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" 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");
Spring整合日志框架
- log4j
首先在pom.xml
添加如下代码:
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-log4j12</artifactId>
<version>1.7.21</version>
</dependency>
<dependency>
<groupId>log4j</groupId>
<artifactId>log4j</artifactId>
<version>1.2.17</version>
</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.slf4j</groupId> <artifactId>slf4j-api</artifactId> <version>1.7.25</version> </dependency> <dependency> <groupId>org.slf4j</groupId> <artifactId>jcl-over-slf4j</artifactId> <version>1.7.25</version> </dependency> <dependency> <groupId>ch.qos.logback</groupId> <artifactId>logback-classic</artifactId> <version>1.2.3</version> </dependency> <dependency> <groupId>ch.qos.logback</groupId> <artifactId>logback-core</artifactId> <version>1.2.3</version> </dependency> <dependency> <groupId>org.logback-extensions</groupId> <artifactId>logback-ext-spring</artifactId> <version>0.1.4</version> </dependency>
设置配置文件
logback.xml
<?xml version="1.0" encoding="UTF-8"?> <configuration> <!-- 控制台输出 --> <appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender"> <encoder> <!--格式化输出:%d表示日期,%thread表示线程名,%-5level:级别从左显示5个字符宽度%msg:日志消息,%n是换行符--> <pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{50} - %msg%n</pattern> </encoder> </appender> <root level="DEBUG"> <appender-ref ref="STDOUT" /> </root> </configuration>
Spring注入
-
什么是注入?
通过 Spring ⼯⼚及配置⽂件,为所创建对象的成员变量赋值
-
为什么需要注入?
与传统的使用setter或者constructor为成员变量赋值相比, Spring注入可以解耦合
product.setName("歼-20"); product = new Product("山东舰");
Set注入
1) set注入步骤
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;
}
}
<!-- applicationContext.xml -->
<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);
}
2) set注入原理
Spring底层调用set方法,完成对属性的赋值
3) set注入详解
针对成员变量不同的类型, 在编写applicationContext.xml
时的语法不同, 但都应该首先提供set方法
-
String + 基本数据类型及其包装类
<property name="name"> <value>红色拖拉机</value> </property> 或者 <property name="name" value="红色拖拉机"></property>
-
数组/List
<!-- List<String> teams --> <property name="teams"> <list> <value>McLaren</value> <value>Red Bull</value> <value>Alpha Tauri</value> </list> </property>
<list>
中的标签不是固定的,而是由数组/List
的类型决定这里
public String[] teams;
类型是String
,所以用<value>
标签 -
Set
<set>
中的标签不是固定的,而是由set
的类型决定<!-- Set<String> drivers --> <property name="drivers"> <set> <value>Lando Norris</value> <value>Pierre Gasly</value> <value>Carlos Sainz</value> <value>Lando Norris</value> <!-- Set集合会自动去重 --> </set> </property>
-
Map
<entry>/<key>
中的标签不是固定的,而是由map
的类型决定<!-- Map<String,String> relationships 若key值相等, 则map会将此前的value覆盖--> <map> <entry> <key><value>McLaren</value></key> <value>Lando Norris</value> </entry> <entry value="Pierre Gasly"> <key><value>Alpha Tauri</value></key> </entry> <entry value="Pierre Gasly" key="Alpha Tauri"/> </map>
-
Properties
Properties
即Map<String,String>
<!-- Properties championships --> <property name="championships"> <props> <prop key="Kimi raikkonen">0</prop> <prop key="Lewis Hamilton">7</prop> <prop key="Sebastian Vettel">4</prop> <prop key="Kimi raikkonen">1</prop> <!-- 覆盖之前的value --> </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> <!-- 上面的方法, 假设有多个自定义类型需要创建productDao对象, 则会多次创建, 浪费资源 --> <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> <!-- 引用上面创建好的 bean --> </property> 或者 <property name="productDao" ref="productDao"/> </bean>
4) p命名空间
使用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">
构造注入
1) 构造注入步骤
构造注入即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 Norris</value></constructor-arg>
<constructor-arg><value>4</value></constructor-arg>
</bean>
@Test
public void test4(){
ApplicationContext applicationContext = new ClassPathXmlApplicationContext("/applicationContext.xml");
Driver dirver = (Driver) applicationContext.getBean("dirver");
System.out.println(dirver.toString());
}
2) 构造注入详解
与Set注入一样, 构造注入在针对不同类型的成员变量, 编写applicationContext.xml
时的语法不同, 具体参考set注入详解
,
但需要首先给出相应的构造器, 而构造方法存在重载的情况.
我们可以借助<constructor-arg>
的几个属性来处理重载的情况, 从而指定相应的构造器
-
index
指定将要被注入的成员变量在构造器中的位置
public Driver(String name, int number) { this.name = name; this.number = number; }
<constructor-arg index="1"> <!-- number --> <value>4</value> </constructor-arg> <constructor-arg index="0"> <!-- name --> <value>Lando Norris</value> </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 Norris</value> </constructor-arg> </bean> 或者 <bean id="dirver" class="com.shy.entity.Driver"> <constructor-arg type="int"> <value>4</value> </constructor-arg> </bean>
-
name
直接指明要注入哪个成员变量
<bean id="dirver" class="com.shy.entity.Driver"> <constructor-arg name="number"> <value>4</value> </constructor-arg> <constructor-arg name="name"> <value>Lando Norris</value> </constructor-arg> </bean>
3) c命名空间
与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注入
autowire 自动装配
-
应用场景
可以理解为自动注入,即在对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: 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容器在运行期间,动态地将某种依赖关系注入到对象之中
Spring工厂生产复杂对象
什么是复杂对象?
在以上章节中, 我们都是通过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; } }
创建复杂对象
实现FactoryBean接口
- 实现FactoryBean接口创建复杂对象
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()
直接获得在<bean class="">
中指定的类的对象而对于复杂的Java对象,
getBean()
并没有直接获取有在<bean class="">
中指定的类的对象,因为对于复杂对象来说,
<bean class="">
指定的是生产复杂对象的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框架的侵入(疑问: 这两种方式还是需要配置
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"); }
创建复杂对象时需要的参数, 可以直接注入
<!-- 利用实例工厂 SimpleDateFormat.parse() 将日期类型注入到Bean --> <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"/> <!-- 直接注入 parse方法需要的参数 --> </bean> </property> </bean>
控制对象创建次数
-
为什么要控制Spring工厂创建对象的次数? – 节省内存的消耗
一般可以被共用, 线程安全 或是 重量级资源只创建一次 如
DAO Service
而不可以被共用, 非线程安全的要创建多次 如
Connection Session
如何控制Spring工厂创建对象的次数呢 ?
-
简单对象 – 通过
<bean scope="">
控制<!-- 控制对象创建次数 --> <!-- 允许 Bean的定义可以被实例化任意次(每次调用都创建一个实例)--> <bean id="driver" class="com.shy.entity.Driver" scope="prototype"/> <!--在每一个 Spring容器中,一个Bean只有一个对象实例(默认, 随着ApplicationContext对象一起被创建) --> <bean id="driver" class="com.shy.entity.Driver" scope="singleton"/>
-
复杂对象
对于实现了
FactoryBean
接口的对象, 通过重写isSingleton()
方法来控制// 是否为单例模式(此为default的方法, 可以不重写, 默认返回true) public boolean isSingleton() { return false; }
对于无法实现上述接口的对象(如实例工厂 静态工厂),通过
<bean scope="">
控制<bean id="connection" class="com.shy.factory.StaticConnectionFactory" factory-method="getConnection" scope="prototype"/>
Spring Bean的生命周期
在Spring框架中, 由Spring容器管理Bean的生命周期
1) 创建阶段
-
对于
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"); // -----对象被创建
2) 初始化阶段
Spring 工厂在创建完对象后,Spring工厂调用程序员提供的对象的初始化方法,完成对应的初始化操作(主要是对资源的初始化)
提供初始化方法
-
实现
InitializingBean
接口的afterPropertiesSet()
方法public class Driver implements InitializingBean { // ...... // 自定义对象的初始化方法, 由Spring工厂调用 @Override public void afterPropertiesSet() throws Exception { System.out.println("Driver.afterPropertiesSet"); } }
-
用
<bean init-method="">
自定义初始化方法并不需要实现
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工厂在创建对象之后, 先完成对依赖注入, 再进行初始化操作
3) 销毁阶段
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"); } }
-
用
<bean destroy-method="">
自定义销毁方法并不需要实现
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>
- 创建
自定义类型转换器
-
什么是类型转换器
考虑上图中, 在
<property name="id" value="1">
中将字符串"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
, 并且采取了何种转换策略<!-- id必须为conversionService --> <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"/> <!-- 格式: yyyy/mm/dd -->
-
写在最后
个人感觉这种方式比较笨重, 不如借助
SimpleDateFormat
的parse()
方法,先将
String
转为Date
,再将其注入到Bean(这样就不会涉及到Spring框架的类型转换问题)演示:
<!-- 利用实例工厂 SimpleDateFormat.parse() 将日期类型注入到Bean --> <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
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
代理设计模式
代理设计模式是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的相关方法, 完成登录的核心代码......*/
-
动态代理模式
-
静态代理模式有何缺点?
试想一下, 假设有数以百计的类, 而每一个类都需要一个代理类, 这就造成项目文件的数量过于庞大, 难以维护.
另外, 假设这些代理类都实现了相同的日志功能, 而一天需求改变了, 需要修改日志功能, 则需要对这些代理类同时做出修改. 难以维护.
-
何为动态代理?
与静态代理不同, 动态代理的代理类在程序运行时创建.
JDK动态代理
-
利用
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"); }
-
Proxy.newProxyInstance()
-
InvocationHandler.invoke()
与静态代理一样, 假设有数以百计的类需要代理, 且都实现了相同的日志功能, 则可以实现一个
InvocationHandler
接口的invoke()
方法,将其作为参数传入Proxy.newProxyInstance()
中.这样就必须要数以百计的代理类了, 极大地方便了代码维护
-
-
JDK动态代理的原理分析
Cglib动态代理
前文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 动态代理
Spring AOP 参照以上两种动态代理模式, 封装了Spring框架下的动态代理, 借助此功能, 更容易实现动态代理.
AOP(Aspect Oriented Programing): 面向切面编程, 以切面(切点 +额外功能)为基本单位的程序开发,通过切面间的彼此协同,相互调用,完成程序的构建. 是对OOP(面向对象编程)的补充
动态: Spring框架在运行时, 通过动态字节码技术在JVM中创建代理类.
实现
-
首先需要导入相关依赖
<!-- https://mvnrepository.com/artifact/org.springframework/spring-aop --> <dependency> <groupId>org.springframework</groupId> <artifactId>spring-aop</artifactId> <version>5.3.9</version> </dependency> <!-- https://mvnrepository.com/artifact/org.aspectj/aspectjrt --> <dependency> <groupId>org.aspectj</groupId> <artifactId>aspectjrt</artifactId> <version>1.9.7</version> <scope>runtime</scope> </dependency> <!-- https://mvnrepository.com/artifact/org.aspectj/aspectjweaver --> <dependency> <groupId>org.aspectj</groupId> <artifactId>aspectjweaver</artifactId> <version>1.9.7</version> <scope>runtime</scope> </dependency> <!-- https://mvnrepository.com/artifact/org.springframework/spring-core --> <dependency> <groupId>org.springframework</groupId> <artifactId>spring-core</artifactId> <version>5.3.9</version> </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"> <!--此时拿到的其实是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的相关方法, 完成登录的核心代码...... */
MethodBeforeAdvice接口
public interface MethodBeforeAdvice extends BeforeAdvice {
/*
在原始方法执行之前执行
method:将额外功能添加给了哪个原始方法
args: 原始方法的参数
target:原始方法的类
*/
void before(Method method, Object[] args, @Nullable Object target) throws Throwable;
}
MethodInterceptor接口
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 动态代理的底层原理
在前面实现Spring动态代理时, 曾对applicationContext.xml
进行过配置
<bean id="userService" class="com.shy.service.impl.UserServiceImpl">
<!--此时拿到的其实是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的代理类 }
切面类实现Spring动态代理
@Aspect 切面类
编写切面类, 在切面类中定义切点以及额外功能
利用注解声明切面类
@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框架利用了注解形式实现动态代理
<!--注册 切面myAspect-->
<bean id="around" class="com.shy.aspect.MyAspect"/>
<!-- 告知Spring框架, 使用注解形式实现AOP -->
<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 切入点复用
@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;
}
}
JDK与Cglib动态代理的切换
-
对于实现了接口的实现类
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动态代理
ApplicationContextAware接口
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;
}
}
对AOP的一些术语的理解
-
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注解编程
Spring从2.x版本开始引入注解编程, 达到简化xml配置的效果. 在后序版本逐渐完善. 随着Spring Boot的推出, 开始推广注解编程
配置
在Spring框架中采用注解形式编程, 首先需要引入注解扫描
<context:component-scan base-package="com.shy"/>
<!-- 告诉Spring框架, 我要采用注解编程, Spring框架对相应的包进行部件扫描 -->
注解与xml混用
需要注意的是, 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
@Component
用来注册相应的Bean
, 可以用来替代<bean id="" class=""
使用@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
@Scope
与@Component
一起连用时,可以控制Spring创建对象的次数
- 以往的xml形式
<bean id="user" class="..." scope="singeton | prototype"
- 注解
@Component
@Scope("singleton") // 可以不写该注解, 默认值为单例对象, 随着ApplicationContext对象一起被创建
public class User {}
@Scope("prototype") // 多例对象
@Lazy
@Lazy
可以延迟创建单例对象, 即当需要该对象时再创建
- 以往的xml形式
<bean id="user" class="com.shy.entity.User" lazy-init="true | false"/>
- 注解
@Lazy
@Component
public class User {}
@PostConstruct / @PreDestroy
@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
@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
@Qualifier
注解用来配合@Autowired
使用, 使@Autowired
注解基于by name
(bean
的id
值)自动装配
public class UserServiceImpl implements UserService, ApplicationContextAware {
@Autowired // 必须有, @Qualifier只是限定一下名称
@Qualifier("userDaoImpl2")
private UserDao userDao;
}
@Resource
@Resource
不是 Spring 提供的,而是兼容了JSR(JavaEE规范)250
@Resource
支持by name
(默认) 和 by type
两种方式进行自动装配
Java类型变量的注入
@Value
@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
@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
注册Bean
@Bean
可以在@Configuration
标注的类中使用, 相当于<bean>
标签,用于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动态代理
实现
对Bean的属性进行注入
-
自定义类型
@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
@ComponentScan
用来替代<context:component-scan>
标签, 进行注解扫描
默认扫描范围是该类所在的包
具体属性的用法参照注解扫描详解
-
排除方式
@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 {}
选择哪种方式注册Bean ?
经过前面的学习, 无论是@Component
, 还是@Bean
, 又或是<bean id="" class="" />
都可以用来注册Bean.
那么选择哪种方式更好呢 ?
需要注意的是, 这几种方式是有优先级的:@Component
< @Bean
< <bean>
优先级高的方式可以覆盖优先级低的配置.
整合Bean的配置信息
在项目当中, 注册Bean
可能不只使用@Component
这种方式. 有可能也使用了@Bean
和<bean>
来注册Bean
.
倘若我们对项目进行维护, 并把@Component
作为注册Bean
的主要方式.
我们该如何将带有@Component
的Bean 和 在applicationContext.xml
中配置的Bean 整合到@Configuration
标注的配置类中呢 ?
-
多个配置
Bean
的整合-
方式一
将配置
Bean
放在同一包下 -
方式二
选择一个配置
Bean
作为主配置Bean
, 利用@Import
将其他配置Bean
导入 -
方式三
ApplicationContext applicationContext = new AnnotationConfigApplicationContext(AppConfig1.Class, AppConfig.Class);
-
-
将
@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
自动注入
纯注解实现Spring动态代理
-
原始类
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:aspectj-autoproxy/>
, 即使用注解形式自动配置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 **
Spring整合Mybatis
为什么要整合
我们首先考虑一下在只使用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.mybatis</groupId> <artifactId>mybatis-spring</artifactId> <version>2.0.6</version> </dependency> <!-- druid连接池 --> <dependency> <groupId>com.alibaba</groupId> <artifactId>druid</artifactId> <version>1.2.6</version> </dependency> <dependency> <groupId>org.springframework</groupId> <artifactId>spring-jdbc</artifactId> <version>5.3.9</version> </dependency>
-
在applicationContext.xml中进行相关配置
值得注意的是, 在使用Spring整合Mybatis之后,我们可以省略
mybais-config.xml
配置文件
相关配置, 可以通过Bean的属性进行注入主要完成三步:
- 配置 dataSource
- 创建 SqlSessionFatory
- 将XXXMapper接口交由Spring管理
<?xml version="1.0" encoding="UTF-8"?> <beans xmlns="http://www.springframework.org/schema/beans" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns: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"/> <!-- 使用第三方数据源 Druid--> <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> <!-- 通过configuration设置Mybatis的全局行为--> <!--<bean id="configuration" class="org.apache.ibatis.session.Configuration">--> <!-- <property name="mapUnderscoreToCamelCase" value="true"--> <!--</bean>--> <!-- sqlSessionFactoryBean 用来创建 sqlSessionFactory --> <bean id="sqlSessionFactoryBean" class="org.mybatis.spring.SqlSessionFactoryBean"> <!-- 引用已配置的数据源 necessary --> <property name="dataSource" ref="dataSource"/> <!-- 为com.shy.entity下的实体设置别名--> <property name="typeAliasesPackage" value="com.shy.entity"/> <!-- 注册所有XXXMapper.xml文件--> <property name="mapperLocations" value="classpath:mapper/*Mapper.xml"/> <!-- <property name="configLocation" value="..."/>--> <!-- 可以读取 mybatis-config.xml的配置 --> <!-- <property name="configuration" ref="configuration"/>--> <!-- 引用 configuration Bean --> <!-- 若configLocation configuration均未配置, 则使用默认配置 --> </bean> <!-- 发现注册器: 让Spring管理mapper接口 从而可以直接通过getBean方式获取相应的XXXMapper.java--> <mybatis:scan base-package="com.shy.mapper"/> <!-- 也可使用下面这种方式发现注册器--> <!-- <bean id="mapperScannerConfigurer" class="org.mybatis.spring.mapper.MapperScannerConfigurer">--> <!-- <property name="sqlSessionFactoryBeanName" value="sqlSessionFactoryBean"/>--> <!-- <property name="basePackage" value="com.shy.mapper"/>--> <!-- </bean>--> </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='男'}
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.springframework</groupId> <artifactId>spring-tx</artifactId> <version>5.3.9</version> </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进行事务管理
<!-- Spring 控制事务 --> <bean id="dataSourceTransactionManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager"> <!-- spring事务控制, 底层需要使用Connection. 这里采用连接池来管理Connection --> <!-- 这里引用整合Mybatis时, 创建的druid连接池 --> <property name="dataSource" ref="dataSource"/> </bean> <!-- 开启注解扫描, 为@Transactional注解的类和方法添加事务控制 --> <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"/> <!-- 可以使用通配符, 这里*表示除了getActorById()的所有方法--> <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的类或方法添加事务控制, 作用类似于<tx:annotation-driven /> 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管理事务
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)
保证一个事务修改的数据提交后才能被另外一个事务读取
A:
-
不可重复读
事务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进行Spring开发
什么是YAML?
YAML与xml, properties一样, 都是用来进行配置的文件, 不过YAML比后两者语法上更为简单.
YAML语法介绍
使用YAML进行Spring开发
编写driver.yaml
name: Pierre Gasly
number: 10
birthday: 2021/08/14
Spring框架默认是不支持YAML的. 我们可以引入第三方JAR, 解析使用YAML.
这里我们选用SnakeYAML
<dependency>
<groupId>org.yaml</groupId>
<artifactId>snakeyaml</artifactId>
<version>1.28</version>
</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
**/
YAML开发中存在的问题
@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;