Spring

传统Javaweb 开发的困惑

  1. 层与层之间紧密耦合在了一起;接口与具体实现紧密耦合在了一起
  2. 通用的事务功能耦合在业务代码中;通用的日志功能耦合在业务代码中

框架

  • 框架( Framework ),是基于基础技术之上,从众多业务中抽取出的通用解决方案
  • 框架是一个半成品,使用框架规定的语法开发可以提高开发效率,可以用简单的代码就能完成复杂的基础业务;
  • 框架内部使用大量的设计模式、算法、底层代码操作技术,如反射、内省、xml 解析、注解解析等;
  • 框架一般都具备扩展性;
  • 有了框架,我们可以将精力尽可能的投入在纯业务开发上而不用去费心技术实现以及一些辅助业务。

Java中常用的框架

  • 基础框架:完成基本业务操作的框架,如 MyBatis 、 Spring 、 SpringMVC 、 Struts2 、 Hibernate 等
  • 服务框架:特定领域的框架,一般还可以对外提供服务框架,如 MQ 、 ES 、 Nacos等

概述

spring 是一个开源的轻量级 Java 开发应用框架,可以简化企业级应用开发。 Spring 解决了开发者在 JavaEE 开发
中遇到的许多常见的问题,提供了功能强大 IOC 、 AOP 及 Web MVC 等功能。是当前企业中 Java 开发几乎不能
缺少的框架之一。 Spring 的生态及其完善,不管是 Spring 哪个领域的解决方案都是依附于在 Spring Framework 基础框架的。

Spring官网

系统架构

核心概念

问题

  1. 业务层需要调用数据层的方法,就需要在业务层new数据层的对象
  2. 如果数据层的实现类发生变化,那么业务层的代码也需要跟着改变,发生变更后,都需要进行编译打包和重部署
  3. 所以,现在代码在编写的过程中存在的问题是:==耦合度偏高==

解决方案:使用对象时,在程序中不要主动使用new产生对象,转换为由==外部==提供对象

IOC(Inversion of Control)控制反转

使用对象时,由主动new产生对象转换为由外部提供对象,此过程中对象创建控制权由程序转移到外部,此思想称为控制反转

Spring技术对IOC思想进行了实现

  • Spring提供了一个容器,称为IOC容器,用来充当IOC思想中的外部

IOC容器的作用以及内部存放的是什么

  • IOC容器负责对象的创建、初始化等一系列工作,其中包含了数据层和业务层的类对象
  • 被创建或被管理的对象在IOC容器中统称为Bean

DI(Dependency Injection)依赖注入

在容器中建立bean与bean之间的依赖关系的整个过程,称为依赖注入。如业务层需要依赖数据层,service就要和dao建立依赖关系

充分解耦

  • 使用IOC容器管理bean(IOC)
  • 在IOC容器内将有依赖关系的bean进行关系绑定(DI)
  • 使用对象时不仅可以直接从IOC容器中获取,并且获取到的bean已经绑定了所有的依赖关系.

IOC

  1. Spring是使用容器来管理bean对象的,那么管什么?(主要管理项目中所使用到的类对象)

  2. 如何将被管理的对象告知IOC容器?(配置)

  3. 被管理的对象交给IOC容器,要想从容器中获取对象,就先得思考如何获取到IOC容器?(接口)

  4. IOC容器得到后,如何从容器中获取bean?(接口方法)

  5. 使用Spring导入哪些坐标?(在pom.xml添加对应的依赖)

简单实现

(1)创建Maven项目

(2)添加Spring的依赖jar包

1
2
3
4
5
6
7
8
9
10
11
12
13
<dependencies>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-context</artifactId>
<version>5.2.10.RELEASE</version>
</dependency>
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>4.12</version>
<scope>test</scope>
</dependency>
</dependencies>

(3)添加需要的类

(4)添加spring配置文件(resources下添加spring配置文件applicationContext.xml,并完成bean的配置)

(5)在配置文件中完成bean的配置

1
2
3
4
5
6
7
8
9
10
11
12
13
<?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标签标示配置bean
id属性标示给bean起名字
class属性表示给bean定义类型
-->
<bean id="Dao" class="top.qianqianzyk.dao.impl.DaoImpl"/>
<bean id="Service" class="top.qianqianzyk.service.impl.ServiceImpl"/>

</beans>

(6)获取IOC容器

1
2
3
4
5
6
public class App {
public static void main(String[] args) {
//获取IOC容器
ApplicationContext ctx = new ClassPathXmlApplicationContext("applicationContext.xml");
}
}

(7)从容器中获取对象进行方法调用

1
2
3
4
5
6
7
8
9
10
public class App {
public static void main(String[] args) {
//获取IOC容器
ApplicationContext ctx = new ClassPathXmlApplicationContext("applicationContext.xml");
// Dao dao = (Dao) ctx.getBean("Dao");
// dao.save();
Service service = (Service) ctx.getBean("Service");
service.save();
}
}

(8)运行程序

bean

bean基础配置

name属性
1
2
3
4
5
6
7
8
9
10
11
12
13
<?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">

<!--name:为bean指定别名,别名可以有多个,使用逗号,分号,空格进行分隔-->
<bean id="Service" name="service service4 Ebi" class="top.qianqianzyk.service.impl.ServiceImpl">
<property name="Dao" ref="Dao"/>
</bean>

<!--scope:为bean设置作用范围,可选值为单例singloton,非单例prototype-->
<bean id="Dao" name="dao" class="top.qianqianzyk.dao.impl.DaoImpl"/>
</beans>
作用范围scope配置

默认为单例。配置bean为非单例

1
<bean id="Dao" name="dao" class="top.qianqianzyk.dao.impl.DaoImpl" scope="prototype"/>
  • singleton默认为单例
  • prototype为非单例
思考
  • 为什么bean默认为单例?
    • bean为单例的意思是在Spring的IOC容器中只会有该类的一个对象
    • bean对象只有一个就避免了对象的频繁创建与销毁,达到了bean对象的复用,性能高
  • bean在容器中是单例的,会不会产生线程安全问题?
    • 如果对象是有状态对象,即该对象有成员变量可以用来存储数据的,
    • 因为所有请求线程共用一个bean对象,所以会存在线程安全问题。
    • 如果对象是无状态对象,即该对象没有成员变量没有进行数据存储的,
    • 因方法中的局部变量在方法调用完成后会被销毁,所以不会存在线程安全问题。
  • 哪些bean对象适合交给容器进行管理?
    • 表现层对象
    • 业务层对象
    • 数据层对象
    • 工具对象
  • 哪些bean对象不适合交给容器进行管理?
    • 封装实例的域对象,因为会引发线程安全问题,所以不适合

bean实例化

bean本质上就是对象,对象在new的时候会使用构造方法完成

bean是如何创建的?实例化bean的三种方式,构造方法,静态工厂实例工厂

  • 构造方法(常用)
  • 静态工厂(了解)
  • 实例工厂(了解)
    • FactoryBean(实用)
构造方法

Spring底层使用的是类的无参构造方法

静态工厂
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public interface OrderDao {
public void save();
}

public class OrderDaoImpl implements OrderDao {
public void save() {
System.out.println("order dao save ...");
}
}

//静态工厂创建对象
public class OrderDaoFactory {
public static OrderDao getOrderDao(){
return new OrderDaoImpl();
}
}
1
<bean id="orderDao" class="top.qianqianzyk.factory.OrderDaoFactory" factory-method="getOrderDao"/>
实例工厂与FactoryBean
实例工厂
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public interface UserDao {
public void save();
}

public class UserDaoImpl implements UserDao {

public void save() {
System.out.println("user dao save ...");
}
}

public class UserDaoFactory {
public UserDao getUserDao(){
return new UserDaoImpl();
}
}
1
2
<bean id="userFactory" class="top.qianqianzyk.factory.UserDaoFactory"/>
<bean id="userDao" factory-method="getUserDao" factory-bean="userFactory"/>
FactoryBean
1
2
3
4
5
6
7
8
9
10
11
12
13
14
public class UserDaoFactoryBean implements FactoryBean<UserDao> {
//代替原始实例工厂中创建对象的方法
public UserDao getObject() throws Exception {
return new UserDaoImpl();
}
//返回所创建类的Class对象
public Class<?> getObjectType() {
return UserDao.class;
}
//非单例
public boolean isSingleton(){
return false;
}
}
1
<bean id="userDao" class="top.qianqianzyk.factory.UserDaoFactoryBean"/>

bean的生命周期

  • bean生命周期是什么?(bean对象从创建到销毁的整体过程)
  • bean生命周期控制是什么?(在bean创建后到销毁前做一些事情)

具体的控制有两个阶段:

  • bean创建之后,想要添加内容,比如用来初始化需要用到资源
  • bean销毁之前,想要添加内容,比如用来释放用到的资源
close关闭容器
1
2
3
4
5
6
7
8
9
10
11
12
13
public class BookDaoImpl implements BookDao {
public void save() {
System.out.println("book dao save ...");
}
//表示bean初始化对应的操作
public void init(){
System.out.println("init...");
}
//表示bean销毁前对应的操作
public void destory(){
System.out.println("destory...");
}
}
1
<bean id="bookDao" class="top.qianqianzyk.dao.impl.BookDaoImpl" init-method="init" destroy-method="destory"/>

这里会存在一个问题,可以初始化但无法销毁。原因:

  • Spring的IOC容器是运行在JVM中
  • 运行main方法后,JVM启动,Spring加载配置文件生成IOC容器,从容器获取bean对象,然后调方法执行
  • main方法执行完后,JVM退出,这个时候IOC容器中的bean还没有来得及销毁就已经结束了
  • 所以没有调用对应的destroy方法

解决方法

  1. 将ApplicationContext更换成ClassPathXmlApplicationContext
  2. 调用ctx的close()方法
1
2
ClassPathXmlApplicationContext ctx = new ClassPathXmlApplicationContext("applicationContext.xml");
ctx.close();
注册钩子关闭容器

调用ctx的registerShutdownHook()方法

1
ctx.registerShutdownHook();
调用Spring提供的接口
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public class BookServiceImpl implements BookService, InitializingBean, DisposableBean {
private BookDao bookDao;
public void setBookDao(BookDao bookDao) {
this.bookDao = bookDao;
}
public void save() {
System.out.println("book service save ...");
bookDao.save();
}
public void destroy() throws Exception {
System.out.println("service destroy");
}
public void afterPropertiesSet() throws Exception {
System.out.println("service init");
}
}

注:初始化方法会在类中属性设置之后执行

核心容器

容器的创建方式

  1. ApplicationContext ctx = new ClassPathXmlApplicationContext("applicationContext.xml");
  2. ApplicationContext ctx = new FileSystemXmlApplicationContext("\your\path\to\applicationContext.xml"); (需要写具体路径,耦合度较高)

Bean的三种获取方式

  1. BookDao bookDao = (BookDao) ctx.getBean("bookDao");(每次获取的时候都需要进行类型转换)
  2. BookDao bookDao = ctx.getBean("bookDao",BookDao.class);
  3. BookDao bookDao = ctx.getBean(BookDao.class);(类似依赖注入中的按类型注入。必须要确保IOC容器中该类型对应的bean对象只能有一个)

容器类层次结构

(1) 在IDEA中双击shift,输入BeanFactory

(2) 点击进入BeanFactory类,ctrl+h,就能查看到其层次关系

BeanFactory的使用

使用BeanFactory来创建IOC容器

1
2
3
4
5
6
7
8
public class AppForBeanFactory {
public static void main(String[] args) {
Resource resources = new ClassPathResource("applicationContext.xml");
BeanFactory bf = new XmlBeanFactory(resources);
BookDao bookDao = bf.getBean(BookDao.class);
bookDao.save();
}
}
  • BeanFactory是延迟加载,只有在获取bean对象的时候才会去创建
  • ApplicationContext是立即加载,容器加载的时候就会创建bean对象
  • ApplicationContext要想成为延迟加载,可以按照如下方式进行配置
1
2
3
4
5
6
7
<?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="bookDao" class="com.itheima.dao.impl.BookDaoImpl" lazy-init="true"/>
</beans>

总结

DI实现

  1. 要想实现依赖注入,必须要基于IOC管理Bean

  2. Service中使用new形式创建的Dao对象是否保留?(否)

  3. Service中需要的Dao对象如何进入到Service中?(提供方法)

  4. Service与Dao间的关系如何描述?(配置)

实现

(1)去除代码中的new

(2)为属性提供setter方法

(3)修改配置完成注入

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
<?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标签标示配置bean
id属性标示给bean起名字
class属性表示给bean定义类型
-->
<bean id="Dao" class="top.qianqianzyk.dao.impl.DaoImpl"/>

<bean id="Service" class="top.qianqianzyk.service.impl.ServiceImpl">
<!--配置server与dao的关系-->
<!--property标签表示配置当前bean的属性
name属性表示配置哪一个具体的属性
ref属性表示参照哪一个bean
-->
<property name="Dao" ref="Dao"/>
</bean>

</beans>

(4)运行程序

依赖注入

setter注入

  • 对于引用数据类型使用的是<property name="" ref=""/>
  • 对于简单数据类型使用的是<property name="" value=""/>
注入引用数据类型
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public class BookServiceImpl implements BookService{
private BookDao bookDao;
private UserDao userDao;

public void setUserDao(UserDao userDao) {
this.userDao = userDao;
}
public void setBookDao(BookDao bookDao) {
this.bookDao = bookDao;
}

public void save() {
System.out.println("book service save ...");
bookDao.save();
userDao.save();
}
}
1
2
3
4
5
6
7
8
9
10
11
12
<?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="bookDao" class="top.qianqianzyk.dao.impl.BookDaoImpl"/>
<bean id="userDao" class="top.qianqianzyk.dao.impl.UserDaoImpl"/>
<bean id="bookService" class="top.qianqianzyk.service.impl.BookServiceImpl">
<property name="bookDao" ref="bookDao"/>
<property name="userDao" ref="userDao"/>
</bean>
</beans>
注入简单数据类型
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public class BookDaoImpl implements BookDao {

private String databaseName;
private int connectionNum;

public void setConnectionNum(int connectionNum) {
this.connectionNum = connectionNum;
}

public void setDatabaseName(String databaseName) {
this.databaseName = databaseName;
}

public void save() {
System.out.println("book dao save ..."+databaseName+","+connectionNum);
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
<?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="bookDao" class="top.qianqianzyk.dao.impl.BookDaoImpl">
<property name="databaseName" value="mysql"/>
<property name="connectionNum" value="10"/>
</bean>
<bean id="userDao" class="top.qianqianzyk.dao.impl.UserDaoImpl"/>
<bean id="bookService" class="top.qianqianzyk.service.impl.BookServiceImpl">
<property name="bookDao" ref="bookDao"/>
<property name="userDao" ref="userDao"/>
</bean>
</beans>

构造器注入

注入引用数据类型

添加有参构造方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public class BookServiceImpl implements BookService{
private BookDao bookDao;
private UserDao userDao;

public BookServiceImpl(BookDao bookDao,UserDao userDao) {
this.bookDao = bookDao;
this.userDao = userDao;
}

public void save() {
System.out.println("book service save ...");
bookDao.save();
userDao.save();
}
}
1
2
3
4
5
6
7
8
9
10
11
12
<?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="bookDao" class="top.qianqianzyk.dao.impl.BookDaoImpl"/>
<bean id="userDao" class="top.qianqianzyk.dao.impl.UserDaoImpl"/>
<bean id="bookService" class="top.qianqianzyk.service.impl.BookServiceImpl">
<constructor-arg name="bookDao" ref="bookDao"/>
<constructor-arg name="userDao" ref="userDao"/>
</bean>
</beans>
注入简单数据类型
1
2
3
4
5
6
7
8
9
10
11
12
13
public class BookDaoImpl implements BookDao {
private String databaseName;
private int connectionNum;

public BookDaoImpl(String databaseName, int connectionNum) {
this.databaseName = databaseName;
this.connectionNum = connectionNum;
}

public void save() {
System.out.println("book dao save ..."+databaseName+","+connectionNum);
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
<?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="bookDao" class="top.qianqianzyk.dao.impl.BookDaoImpl">
<constructor-arg name="databaseName" value="mysql"/>
<constructor-arg name="connectionNum" value="666"/>
</bean>
<bean id="userDao" class="top.qianqianzyk.dao.impl.UserDaoImpl"/>
<bean id="bookService" class="top.qianqianzyk.service.impl.BookServiceImpl">
<constructor-arg name="bookDao" ref="bookDao"/>
<constructor-arg name="userDao" ref="userDao"/>
</bean>
</beans>

具体该如何选择注入方式?

  1. 强制依赖使用构造器进行,使用setter注入有概率不进行注入导致null对象出现
    • 强制依赖指对象在创建的过程中必须要注入指定的参数
  2. 可选依赖使用setter注入进行,灵活性强
    • 可选依赖指对象在创建过程中注入的参数可有可无
  3. Spring框架倡导使用构造器,第三方框架内部大多数采用构造器注入的形式进行数据初始化,相对严谨
  4. 如果有必要可以两者同时使用,使用构造器注入完成强制依赖的注入,使用setter注入完成可选依赖的注入
  5. 实际开发过程中还要根据实际情况分析,如果受控对象没有提供setter方法就必须使用构造器注入
  6. 自己开发的模块推荐使用setter注入

自动配置

手动注入比较麻烦,我们可以选择依赖自动装配

  • IOC容器根据bean所依赖的资源在容器中自动查找并注入到bean中的过程称为自动装配

按照类型注入的配置

1
2
3
4
5
6
7
8
9
10
<?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 class="top.qianqianzyk.dao.impl.BookDaoImpl"/>
<!--autowire属性:开启自动装配,通常使用按类型装配-->
<bean id="bookService" class="top.qianqianzyk.service.impl.BookServiceImpl" autowire="byType"/>

</beans>

按照名称注入

1
2
3
4
5
6
7
8
9
10
<?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 class="top.qianqianzyk.dao.impl.BookDaoImpl"/>
<!--autowire属性:开启自动装配,通常使用按类型装配-->
<bean id="bookService" class="top.qianqianzyk.service.impl.BookServiceImpl" autowire="byName"/>

</beans>

注意:

  1. 自动装配用于引用类型依赖注入,不能对简单类型进行操作
  2. 使用按类型装配时(byType)必须保障容器中相同类型的bean唯一,推荐使用
  3. 使用按名称装配时(byName)必须保障容器中具有指定名称的bean,因变量名与配置耦合,不推荐使用
  4. 自动装配优先级低于setter注入与构造器注入,同时出现时自动装配配置失效

集合注入

在bean标签中使用进行注入

注入数组类型数据

1
2
3
4
5
6
7
<property name="array">
<array>
<value>100</value>
<value>200</value>
<value>300</value>
</array>
</property>

注入List类型数据

1
2
3
4
5
6
7
8
<property name="list">
<list>
<value>1</value>
<value>11</value>
<value>111</value>
<value>1111</value>
</list>
</property>

注入Set类型数据

1
2
3
4
5
6
7
8
<property name="set">
<set>
<value>1</value>
<value>11</value>
<value>111</value>
<value>1111</value>
</set>
</property>

注入Map类型数据

1
2
3
4
5
6
7
<property name="map">
<map>
<entry key="country" value="china"/>
<entry key="province" value="zhejiang"/>
<entry key="city" value="hangzhou"/>
</map>
</property>

注入Properties类型数据

1
2
3
4
5
6
7
<property name="properties">
<props>
<prop key="country">china</prop>
<prop key="province">zhejiang</prop>
<prop key="city">hangzhou</prop>
</props>
</property>

说明

  • property标签表示setter方式注入,构造方式注入constructor-arg标签内部也可以写<array><list><set><map><props>标签
  • List的底层也是通过数组实现的,所以<list><array>标签是可以混用
  • 集合中要添加引用类型,只需要把<value>标签改成<ref>标签,这种方式用的比较少

IOC/DI配置管理第三方bean

如果有需求让我们去管理第三方jar包中的类

实现Druid管理

pom.xml

1
2
3
4
5
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>druid</artifactId>
<version>1.1.16</version>
</dependency>

applicationContext.xml

1
2
3
4
5
6
7
8
9
10
11
12
13
14
<?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">
<!--管理DruidDataSource对象-->
<bean class="com.alibaba.druid.pool.DruidDataSource">
<property name="driverClassName" value="com.mysql.jdbc.Driver"/>
<property name="url" value="jdbc:mysql://localhost:3306/spring_db"/>
<property name="username" value="root"/>
<property name="password" value="root"/>
</bean>
</beans>

实现C3P0管理

pom,xml

1
2
3
4
5
6
7
8
9
10
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>5.1.47</version>
</dependency>
<dependency>
<groupId>c3p0</groupId>
<artifactId>c3p0</artifactId>
<version>0.9.1.2</version>
</dependency>

applicationContext.xml

1
2
3
4
5
6
7
<bean id="dataSource" class="com.mchange.v2.c3p0.ComboPooledDataSource">
<property name="driverClass" value="com.mysql.jdbc.Driver"/>
<property name="jdbcUrl" value="jdbc:mysql://localhost:3306/spring_db"/>
<property name="user" value="root"/>
<property name="password" value="root"/>
<property name="maxPoolSize" value="1000"/>
</bean>

注意:

  • ComboPooledDataSource的属性是通过setter方式进行注入
  • 想注入属性就需要在ComboPooledDataSource类或其上层类中有提供属性对应的setter方法
  • C3P0的四个属性和Druid的四个属性是不一样的
  • 数据连接池在配置属性的时候,除了可以注入数据库连接四要素外还可以配置很多其他的属性,具体都有哪些属性用到的时候再去查,一般配置基础的四个,其他都有自己的默认值
  • Druid和C3P0在没有导入mysql驱动包的前提下,一个没报错一个报错,说明Druid在初始化的时候没有去加载驱动,而C3P0刚好相反
  • Druid程序运行虽然没有报错,但是当调用DruidDataSource的getConnection()方法获取连接的时候,也会报找不到驱动类的错误

加载properties文件

实际开发中我们需要将这些配置信息提取到配置文件中,如用户名或密码

resources下创建一个jdbc.properties文件,并添加对应的属性键值对

1
2
3
4
jdbc.driver=com.mysql.jdbc.Driver
jdbc.url=jdbc:mysql://127.0.0.1:3306/spring_db
jdbc.username=root
jdbc.password=root

在applicationContext.xml中开context命名空间

1
2
3
4
5
6
7
8
9
10
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:context="http://www.springframework.org/schema/context"
xsi:schemaLocation="
http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/context
http://www.springframework.org/schema/context/spring-context.xsd">
</beans>

在配置文件中使用context命名空间下的标签来加载properties配置文件

1
<context:property-placeholder location="jdbc.properties"/>

使用${key}来读取properties配置文件中的内容并完成属性注入

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:context="http://www.springframework.org/schema/context"
xsi:schemaLocation="
http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/context
http://www.springframework.org/schema/context/spring-context.xsd">

<context:property-placeholder location="jdbc.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>
</beans>

问题一:

在properties中配置键值对的时候,如果key设置为username,运行后,在控制台打印的却不是root666,而是自己电脑的用户名

出现问题的原因是<context:property-placeholder/>标签会加载系统的环境变量,而且环境变量的值会被优先加载

1
2
3
4
public static void main(String[] args) throws Exception{
Map<String, String> env = System.getenv();
System.out.println(env);
}

解决方法:

system-properties-mode:设置为NEVER,表示不加载系统属性,就可以解决上述问题

1
2
3
4
5
6
7
8
9
10
11
12
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:context="http://www.springframework.org/schema/context"
xsi:schemaLocation="
http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/context
http://www.springframework.org/schema/context/spring-context.xsd">

<context:property-placeholder location="jdbc.properties" system-properties-mode="NEVER"/>
</beans>

问题二:

有多个properties配置文件需要被加载

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:context="http://www.springframework.org/schema/context"
xsi:schemaLocation="
http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/context
http://www.springframework.org/schema/context/spring-context.xsd">
<!--方式一 -->
<context:property-placeholder location="jdbc.properties,jdbc2.properties" system-properties-mode="NEVER"/>
<!--方式二-->
<context:property-placeholder location="*.properties" system-properties-mode="NEVER"/>
<!--方式三 -->
<context:property-placeholder location="classpath:*.properties" system-properties-mode="NEVER"/>
<!--方式四-->
<context:property-placeholder location="classpath*:*.properties" system-properties-mode="NEVER"/>
</beans>
  • 方式一:可以实现,如果配置文件多的话,每个都需要配置
  • 方式二:*.properties代表所有以properties结尾的文件都会被加载,可以解决方式一的问题,但是不标准
  • 方式三:标准的写法,classpath:代表的是从根路径下开始查找,但是只能查询当前项目的根路径
  • 方式四:不仅可以加载当前项目还可以加载当前项目所依赖的所有项目的根路径下的properties配置文件

IOC/DI注解开发

要想真正简化开发,就需要用到Spring的注解开发

注解开发定义bean

  1. 将配置文件中的<bean>标签删除掉<bean id="bookDao" class="com.itheima.dao.impl.BookDaoImpl"/>
  2. 在BookDaoImpl类上添加@Component注解@Component("bookDao")(注意:@Component注解不可以添加在接口上,因为接口无法创建对象)
  3. 配置Spring的注解包扫描
1
2
3
4
5
6
7
<?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">
<context:component-scan base-package="top.qianqianzyk"/>
</beans>

说明:

component-scan

  • component:组件,Spring将管理的bean视作自己的一个组件
  • scan:扫描

base-package指定Spring框架扫描的包路径,它会扫描指定包及其子包中的所有类上的注解。

  • 包路径越多[如:top.qianqianzyk.dao.impl],扫描的范围越小速度越快
  • 包路径越少[如:top.qianqianzyk],扫描的范围越大速度越慢
  • 一般扫描到项目的组织名称即Maven的groupId下[如:top.qianqianzyk]即可。
  1. 在BookServiceImpl类上也添加@Component交给Spring框架管理

说明:

  • BookServiceImpl类没有起名称,所以在App中是按照类型来获取bean对象

  • @Component注解如果不起名称,会有一个默认值就是当前类名首字母小写,所以也可以按照名称获取,如

    1
    2
    BookService bookService = (BookService)ctx.getBean("bookServiceImpl");
    System.out.println(bookService);

对于@Component注解,还衍生出了其他三个注解@Controller@Service@Repository

这三个注解和@Component注解的作用是一样的,方便后期在编写类的时候能很好的区分出这个类是属于表现层业务层还是数据层的类

纯注解开发模式

上面已经可以使用注解来配置bean,但是依然有用到配置文件,在配置文件中对包进行了扫描,Spring在3.0版已经支持纯注解开发

Spring3.0已开启了纯注解开发模式,使用Java类替代配置文件,开启了Spring快速开发赛道

将配置文件applicationContext.xml删除掉,使用类来替换

  1. 创建一个配置类
1
2
public class SpringConfig {
}
  1. 在配置类上添加@Configuration注解,将其标识为一个配置类,替换applicationContext.xml
1
2
3
@Configuration
public class SpringConfig {
}
  1. 在配置类上添加包扫描注解@ComponentScan替换<context:component-scan base-package=""/>
1
2
3
4
5
@Configuration
@ComponentScan("top.qianqianzyk")
// @ComponentScan({top.qianqianzyk.service","top.qianqianzyk.dao"})
public class SpringConfig {
}
  1. 创建一个新的运行类AppForAnnotation(读取Spring核心配置文件初始化容器对象切换为读取Java配置类初始化容器对象)
1
2
3
4
5
6
7
8
9
10
public class AppForAnnotation {

public static void main(String[] args) {
ApplicationContext ctx = new AnnotationConfigApplicationContext(SpringConfig.class);
BookDao bookDao = (BookDao) ctx.getBean("bookDao");
System.out.println(bookDao);
BookService bookService = ctx.getBean(BookService.class);
System.out.println(bookService);
}
}

注解开发bean作用范围与生命周期管理

Bean的作用范围

在其类上添加@scope注解,可用于设置创建出的bean是否为单例对象

value:默认值singleton(单例),可选值prototype(非单例)

Bean的生命周期

pom.xml添加依赖

1
2
3
4
5
<dependency>
<groupId>javax.annotation</groupId>
<artifactId>javax.annotation-api</artifactId>
<version>1.3.2</version>
</dependency>
  1. 在类中添加两个方法,initdestroy,方法名可以任意
  2. 对方法进行标识,在对应的方法上添加@PostConstruct(init-method)和@PreDestroy(destroy-method)注解即可

注解开发依赖注入

Spring只提供了自动装配的注解实现

1
2
3
4
5
6
7
<dependencies>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-context</artifactId>
<version>5.2.10.RELEASE</version>
</dependency>
</dependencies>

假如在类中提供了setter方法,但是目前是没有提供配置注入的,所以相应的对象为Null,调用其方法就会报空指针异常

实现按照类型注入

  1. 在需要注入的属性上添加@Autowired注解

注意:

  • @Autowired可以写在属性上,也可也写在setter方法上,最简单的处理方式是写在属性上并将setter方法删除掉
  • 为什么setter方法可以删除呢?
    • 自动装配基于反射设计创建对象并通过暴力反射为私有属性进行设值
    • 普通反射只能获取public修饰的内容
    • 暴力反射除了获取public修饰的内容还可以获取private修改的内容
    • 所以此处无需提供setter方法
  • 如果有多个实现类,运行程序就会报错,因为按照类型注入就无法区分到底注入哪个对象
  • @Autowired默认按照类型自动装配,如果IOC容器中同类的Bean找到多个,就按照变量名和Bean的名称匹配

实现按照名称注入

1
2
3
4
5
6
7
8
9
10
11
@Service
public class BookServiceImpl implements BookService {
@Autowired
@Qualifier("bookDao1")
private BookDao bookDao;

public void save() {
System.out.println("book service save ...");
bookDao.save();
}
}

@Qualifier注解后的值就是需要注入的bean的名称

实现简单数据类型注入

数据类型换了,对应的注解也要跟着换,使用@Value注解,将值写入注解的参数

不过@Value一般会被用在从properties配置文件中读取内容进行使用

1
2
3
4
5
6
7
8
@Repository("bookDao")
public class BookDaoImpl implements BookDao {
@Value("qianqianzyk")
private String name;
public void save() {
System.out.println("book dao save ..." + name);
}
}

读取properties配置文件

jdbc.properties

1
name=qianqianzyk

使用注解加载properties配置文件,在配置类上添加@PropertySource注解

1
2
3
4
5
@Configuration
@ComponentScan("top.qianqianzyk")
@PropertySource("jdbc.properties")
public class SpringConfig {
}

使用@Value读取配置文件中的内容

1
2
3
4
5
6
7
8
@Repository("bookDao")
public class BookDaoImpl implements BookDao {
@Value("${name}")
private String name;
public void save() {
System.out.println("book dao save ..." + name);
}
}

注意:

  • 如果读取的properties配置文件有多个,可以使用@PropertySource的属性来指定多个

    1
    @PropertySource({"jdbc.properties","xxx.properties"})
  • @PropertySource注解属性中不支持使用通配符*,运行会报错

  • @PropertySource注解属性中可以把classpath:加上,代表从当前项目的根路径找文件

    1
    @PropertySource({"classpath:jdbc.properties"})

IOC/DI注解开发管理第三方bean

前面定义bean的时候都是在自己开发的类上面写个注解就完成了,但如果是第三方的类,这些类都是在jar包中,我们没有办法在类上面添加注解,这个时候该怎么办?

遇到上述问题,我们就需要有一种更加灵活的方式来定义bean,这种方式不能在原始代码上面书写注解,一样能定义bean,这就用到了一个全新的注解@Bean

注解开发管理第三方bean

例:完成对Druid数据源的管理

导入jar包

1
2
3
4
5
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>druid</artifactId>
<version>1.1.16</version>
</dependency>

在配置类中添加一个方法

注意该方法的返回值就是要创建的Bean对象类型

1
2
3
4
5
6
7
8
9
10
11
@Configuration
public class SpringConfig {
public DataSource dataSource(){
DruidDataSource ds = new DruidDataSource();
ds.setDriverClassName("com.mysql.jdbc.Driver");
ds.setUrl("jdbc:mysql://localhost:3306/spring_db");
ds.setUsername("root");
ds.setPassword("root");
return ds;
}
}

在方法上添加@Bean注解

@Bean注解的作用是将方法的返回值制作为Spring管理的一个bean对象

1
2
3
4
5
6
7
8
9
10
11
12
@Configuration
public class SpringConfig {
@Bean
public DataSource dataSource(){
DruidDataSource ds = new DruidDataSource();
ds.setDriverClassName("com.mysql.jdbc.Driver");
ds.setUrl("jdbc:mysql://localhost:3306/spring_db");
ds.setUsername("root");
ds.setPassword("root");
return ds;
}
}

**注意:不能使用DataSource ds = new DruidDataSource()**因为DataSource接口中没有对应的setter方法来设置属性

如果有多个bean要被Spring管理,直接在配置类中多些几个方法,方法上添加@Bean注解即可

引入外部配置类

如果把所有的第三方bean都配置到Spring的配置类SpringConfig中,会不利于代码阅读和分类管理

对于数据源的bean,我们新建一个JdbcConfig配置类,并把数据源配置到该类下

那么,这个配置类如何能被Spring配置类加载到,并创建DataSource对象在IOC容器中?

使用包扫描引入

在Spring的配置类上添加包扫描

1
2
3
4
5
@Configuration
@ComponentScan("top.qianqianzyk.config")
public class SpringConfig {

}

在JdbcConfig上添加配置注解

1
2
3
4
5
6
7
8
9
10
11
12
@Configuration
public class JdbcConfig {
@Bean
public DataSource dataSource(){
DruidDataSource ds = new DruidDataSource();
ds.setDriverClassName("com.mysql.jdbc.Driver");
ds.setUrl("jdbc:mysql://localhost:3306/spring_db");
ds.setUsername("root");
ds.setPassword("root");
return ds;
}
}

不推荐使用(无法快速的知道导入了哪些配置)

使用@Import引入

去除JdbcConfig类上的注解

1
2
3
4
5
6
7
8
9
10
11
public class JdbcConfig {
@Bean
public DataSource dataSource(){
DruidDataSource ds = new DruidDataSource();
ds.setDriverClassName("com.mysql.jdbc.Driver");
ds.setUrl("jdbc:mysql://localhost:3306/spring_db");
ds.setUsername("root");
ds.setPassword("root");
return ds;
}
}

在Spring配置类中引入

1
2
3
4
5
@Configuration
@Import({JdbcConfig.class})
public class SpringConfig {

}

注意:

  • 扫描注解可以移除
  • @Import参数需要的是一个数组,可以引入多个配置类。
  • @Import注解在配置类中只能写一次

注解开发实现为第三方bean注入资源

在使用@Bean创建bean对象的时候,如果方法在创建的过程中需要其他资源该怎么办?

注入简单数据类型

类中提供四个属性

使用@Value注解引入值

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
public class JdbcConfig {
@Value("com.mysql.jdbc.Driver")
private String driver;
@Value("jdbc:mysql://localhost:3306/spring_db")
private String url;
@Value("root")
private String userName;
@Value("password")
private String password;
@Bean
public DataSource dataSource(){
DruidDataSource ds = new DruidDataSource();
ds.setDriverClassName(driver);
ds.setUrl(url);
ds.setUsername(userName);
ds.setPassword(password);
return ds;
}
}

引用数据类型

在SpringConfig中扫描BookDao

1
2
3
4
5
@Configuration
@ComponentScan("top.qianqianzyk.dao")
@Import({JdbcConfig.class})
public class SpringConfig {
}

在JdbcConfig类的方法上添加参数

引用类型注入只需要为bean定义方法设置形参即可,容器会根据类型自动装配对象

1
2
3
4
5
6
7
8
9
@Bean
public DataSource dataSource(BookDao bookDao){
DruidDataSource ds = new DruidDataSource();
ds.setDriverClassName(driver);
ds.setUrl(url);
ds.setUsername(userName);
ds.setPassword(password);
return ds;
}

Spring整合

Spring整合Mybatis

回顾Mybatis开发

步骤1:准备数据库表

Mybatis是来操作数据库表,所以先创建一个数据库及表

步骤2:创建项目导入jar包

项目的pom.xml添加相关依赖

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
<dependencies>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-context</artifactId>
<version>5.2.10.RELEASE</version>
</dependency>
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>druid</artifactId>
<version>1.1.16</version>
</dependency>
<dependency>
<groupId>org.mybatis</groupId>
<artifactId>mybatis</artifactId>
<version>3.5.6</version>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>5.1.47</version>
</dependency>
</dependencies>

步骤3:根据表创建模型类

1
2
3
4
5
6
7
public class Account implements Serializable {

private Integer id;
private String name;
private Double money;
//setter...getter...toString...方法略
}

步骤4:创建Dao接口

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public interface AccountDao {

@Insert("insert into tbl_account(name,money)values(#{name},#{money})")
void save(Account account);

@Delete("delete from tbl_account where id = #{id} ")
void delete(Integer id);

@Update("update tbl_account set name = #{name} , money = #{money} where id = #{id} ")
void update(Account account);

@Select("select * from tbl_account")
List<Account> findAll();

@Select("select * from tbl_account where id = #{id} ")
Account findById(Integer id);
}

步骤5:创建Service接口和实现类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
public interface AccountService {

void save(Account account);

void delete(Integer id);

void update(Account account);

List<Account> findAll();

Account findById(Integer id);

}

@Service
public class AccountServiceImpl implements AccountService {

@Autowired
private AccountDao accountDao;

public void save(Account account) {
accountDao.save(account);
}

public void update(Account account){
accountDao.update(account);
}

public void delete(Integer id) {
accountDao.delete(id);
}

public Account findById(Integer id) {
return accountDao.findById(id);
}

public List<Account> findAll() {
return accountDao.findAll();
}
}

步骤6:添加jdbc.properties文件

resources目录下添加,用于配置数据库连接四要素

1
2
3
4
jdbc.driver=com.mysql.jdbc.Driver
jdbc.url=jdbc:mysql://localhost:3306/spring_db?useSSL=false
jdbc.username=root
jdbc.password=root

useSSL:关闭MySQL的SSL连接

步骤7:添加Mybatis核心配置文件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE configuration
PUBLIC "-//mybatis.org//DTD Config 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-config.dtd">
<configuration>
<!--读取外部properties配置文件-->
<properties resource="jdbc.properties"></properties>
<!--别名扫描的包路径-->
<typeAliases>
<package name="top.qianqianzyk.domain"/>
</typeAliases>
<!--数据源-->
<environments default="mysql">
<environment id="mysql">
<transactionManager type="JDBC"></transactionManager>
<dataSource type="POOLED">
<property name="driver" value="${jdbc.driver}"></property>
<property name="url" value="${jdbc.url}"></property>
<property name="username" value="${jdbc.username}"></property>
<property name="password" value="${jdbc.password}"></property>
</dataSource>
</environment>
</environments>
<!--映射文件扫描包路径-->
<mappers>
<package name="top.qianqianzyk.dao"></package>
</mappers>
</configuration>

步骤8:编写应用程序

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
public class App {
public static void main(String[] args) throws IOException {
// 1. 创建SqlSessionFactoryBuilder对象
SqlSessionFactoryBuilder sqlSessionFactoryBuilder = new SqlSessionFactoryBuilder();
// 2. 加载SqlMapConfig.xml配置文件
InputStream inputStream = Resources.getResourceAsStream("SqlMapConfig.xml.bak");
// 3. 创建SqlSessionFactory对象
SqlSessionFactory sqlSessionFactory = sqlSessionFactoryBuilder.build(inputStream);
// 4. 获取SqlSession
SqlSession sqlSession = sqlSessionFactory.openSession();
// 5. 执行SqlSession对象执行查询,获取结果User
AccountDao accountDao = sqlSession.getMapper(AccountDao.class);

Account ac = accountDao.findById(1);
System.out.println(ac);

// 6. 释放资源
sqlSession.close();
}
}

整合思路分析

从APP中可知,真正需要交给Spring管理的是SqlSessionFactory

Mybatis配置文件分析

  • 第一行读取外部properties配置文件,Spring有提供具体的解决方案@PropertySource,需要交给Spring
  • 第二行起别名包扫描,为SqlSessionFactory服务的,需要交给Spring
  • 第三行主要用于做连接池,Spring之前我们已经整合了Druid连接池,这块也需要交给Spring
  • 前面三行一起都是为了创建SqlSession对象用的,那么用Spring管理SqlSession对象吗?只需要将SqlSessionFactory交给Spring管理即可
  • 第四行是Mapper接口和映射文件[如果使用注解就没有该映射文件],这个是在获取到SqlSession以后执行具体操作的时候用,所以它和SqlSessionFactory创建的时机都不在同一个时间,可能需要单独管理

整合实现

第一件事是:Spring要管理MyBatis中的SqlSessionFactory

第二件事是:Spring要管理Mapper接口的扫描

步骤1:项目中导入整合需要的jar包

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
<dependency>
<!--Spring操作数据库需要该jar包-->
<groupId>org.springframework</groupId>
<artifactId>spring-jdbc</artifactId>
<version>5.2.10.RELEASE</version>
</dependency>
<dependency>
<!--
Spring与Mybatis整合的jar包
这个jar包mybatis在前面,是Mybatis提供的
-->
<groupId>org.mybatis</groupId>
<artifactId>mybatis-spring</artifactId>
<version>1.3.0</version>
</dependency>

步骤2:创建Spring的主配置类

1
2
3
4
5
6
//配置类注解
@Configuration
//包扫描,主要扫描的是项目中的AccountServiceImpl类
@ComponentScan("top.qianqianzyk")
public class SpringConfig {
}

步骤3:创建数据源的配置类

在配置类中完成数据源的创建

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
public class JdbcConfig {
@Value("${jdbc.driver}")
private String driver;
@Value("${jdbc.url}")
private String url;
@Value("${jdbc.username}")
private String userName;
@Value("${jdbc.password}")
private String password;

@Bean
public DataSource dataSource(){
DruidDataSource ds = new DruidDataSource();
ds.setDriverClassName(driver);
ds.setUrl(url);
ds.setUsername(userName);
ds.setPassword(password);
return ds;
}
}

步骤4:主配置类中读properties并引入数据源配置类

1
2
3
4
5
6
@Configuration
@ComponentScan("top.qianqianzyk")
@PropertySource("classpath:jdbc.properties")
@Import(JdbcConfig.class)
public class SpringConfig {
}

步骤5:创建Mybatis配置类并配置SqlSessionFactory

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
public class MybatisConfig {
//定义bean,SqlSessionFactoryBean,用于产生SqlSessionFactory对象
@Bean
public SqlSessionFactoryBean sqlSessionFactory(DataSource dataSource){
SqlSessionFactoryBean ssfb = new SqlSessionFactoryBean();
//设置模型类的别名扫描
ssfb.setTypeAliasesPackage("top.qianqianzyk.domain");
//设置数据源
ssfb.setDataSource(dataSource);
return ssfb;
}
//定义bean,返回MapperScannerConfigurer对象
@Bean
public MapperScannerConfigurer mapperScannerConfigurer(){
MapperScannerConfigurer msc = new MapperScannerConfigurer();
msc.setBasePackage("top.qianqianzyk.dao");
return msc;
}
}

说明:

  • 使用SqlSessionFactoryBean封装SqlSessionFactory需要的环境信息
    • SqlSessionFactoryBean是FactoryBean的一个子类,在该类中将SqlSessionFactory的创建进行了封装,简化对象的创建,我们只需要将其需要的内容设置即可
    • 方法中有一个参数为dataSource,当前Spring容器中已经创建了Druid数据源,类型刚好是DataSource类型,此时在初始化SqlSessionFactoryBean这个对象的时候,发现需要使用DataSource对象,而容器中刚好有这么一个对象,就自动加载了DruidDataSource对象
  • 使用MapperScannerConfigurer加载Dao接口,创建代理对象保存到IOC容器中
    • 这个MapperScannerConfigurer对象也是MyBatis提供的专用于整合的jar包中的类,用来处理原始配置文件中的mappers相关配置,加载数据层的Mapper接口类
    • MapperScannerConfigurer有一个核心属性basePackage,就是用来设置所扫描的包路径

步骤6:主配置类中引入Mybatis配置类

1
2
3
4
5
6
@Configuration
@ComponentScan("top.qianqianzyk")
@PropertySource("classpath:jdbc.properties")
@Import({JdbcConfig.class,MybatisConfig.class})
public class SpringConfig {
}

步骤7:编写运行类

在运行类中,从IOC容器中获取Service对象,调用方法获取结果

1
2
3
4
5
6
7
8
9
10
public class App2 {
public static void main(String[] args) {
ApplicationContext ctx = new AnnotationConfigApplicationContext(SpringConfig.class);

AccountService accountService = ctx.getBean(AccountService.class);

Account ac = accountService.findById(1);
System.out.println(ac);
}
}

支持Spring与Mybatis的整合就已经完成了,其中主要用到的两个类分别是:

  • SqlSessionFactoryBean
  • MapperScannerConfigurer

Spring整合Junit

整合Junit与整合Druid和MyBatis差异比较大。因为Junit是一个搞单元测试用的工具,它不是程序的主体,也不会参加最终程序的运行,从作用上来说就和之前的东西不一样,它不是做功能的,看做是一个辅助工具就可以了

步骤1:引入依赖

pom.xml

1
2
3
4
5
6
7
8
9
10
11
12
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>4.12</version>
<scope>test</scope>
</dependency>

<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-test</artifactId>
<version>5.2.10.RELEASE</version>
</dependency>

步骤2:编写测试类

在test\java下创建一个AccountServiceTest,这个名字任意

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
//设置类运行器
@RunWith(SpringJUnit4ClassRunner.class)
//设置Spring环境对应的配置类
@ContextConfiguration(classes = {SpringConfiguration.class}) //加载配置类
//@ContextConfiguration(locations={"classpath:applicationContext.xml"})//加载配置文件
public class AccountServiceTest {
//支持自动装配注入bean
@Autowired
private AccountService accountService;
@Test
public void testFindById(){
System.out.println(accountService.findById(1));

}
@Test
public void testFindAll(){
System.out.println(accountService.findAll());
}
}

注意:

  • 单元测试,如果测试的是注解配置类,则使用@ContextConfiguration(classes = 配置类.class)
  • 单元测试,如果测试的是配置文件,则使用@ContextConfiguration(locations={配置文件名,...})
  • Junit运行后是基于Spring环境运行的,所以Spring提供了一个专用的类运行器,这个务必要设置,这个类运行器就在Spring的测试专用包中提供的,导入的坐标就是这个东西SpringJUnit4ClassRunner
  • 上面两个配置都是固定格式,当需要测试哪个bean时,使用自动装配加载对应的对象,下面的工作就和以前做Junit单元测试完全一样了

AOP

介绍

AOP(Aspect Oriented Programming)面向切面编程,一种编程范式,指导开发者如何组织程序结构

可以在不惊动(改动)原有设计(代码)的前提下,想给谁添加功能就给谁添加。这个也就是Spring的理念:无入侵式/无侵入式

(1) Spring的AOP是对一个类的方法在不进行任何修改的前提下实现增强。对于上面的案例中BookServiceImpl中有save,update,deleteselect方法,这些方法我们给起了一个名字叫连接点

(2) 在BookServiceImpl的四个方法中,updatedelete只有打印没有计算万次执行消耗时间,但是在运行的时候已经有该功能,那也就是说updatedelete方法都已经被增强,所以对于需要增强的方法我们给起了一个名字叫切入点

(3) 执行BookServiceImpl的update和delete方法的时候都被添加了一个计算万次执行消耗时间的功能,将这个功能抽取到一个方法中,换句话说就是存放共性功能的方法,我们给起了个名字叫通知

(4) 通知是要增强的内容,会有多个,切入点是需要被增强的方法,也会有多个,那哪个切入点需要添加哪个通知,就需要提前将它们之间的关系描述清楚,那么对于通知和切入点之间的关系描述,我们给起了个名字叫切面

(5)通知是一个方法,方法不能独立存在需要被写在一个类中,这个类我们也给起了个名字叫通知类

至此AOP中的核心概念就已经介绍完了,总结下:

  • 连接点(JoinPoint):程序执行过程中的任意位置,粒度为执行方法、抛出异常、设置变量等
    • 在SpringAOP中,理解为方法的执行
  • 切入点(Pointcut):匹配连接点的式子
    • 在SpringAOP中,一个切入点可以描述一个具体方法,也可也匹配多个方法
      • 一个具体的方法:如top.qianqianzyk.dao包下的BookDao接口中的无形参无返回值的save方法
      • 匹配多个方法:所有的save方法,所有的get开头的方法,所有以Dao结尾的接口中的任意方法,所有带有一个参数的方法
    • 连接点范围要比切入点范围大,是切入点的方法也一定是连接点,但是是连接点的方法就不一定要被增强,所以可能不是切入点
  • 通知(Advice):在切入点处执行的操作,也就是共性功能
    • 在SpringAOP中,功能最终以方法的形式呈现
  • 通知类:定义通知的类
  • 切面(Aspect):描述通知与切入点的对应关系

简单案例

使用SpringAOP的注解方式完成在方法执行的前打印出当前系统时间

  • pom.xml添加Spring依赖

    1
    2
    3
    4
    5
    6
    7
    <dependencies>
    <dependency>
    <groupId>org.springframework</groupId>
    <artifactId>spring-context</artifactId>
    <version>5.2.10.RELEASE</version>
    </dependency>
    </dependencies>
  • 添加BookDao和BookDaoImpl类

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    public interface BookDao {
    public void save();
    public void update();
    }

    @Repository
    public class BookDaoImpl implements BookDao {

    public void save() {
    System.out.println(System.currentTimeMillis());
    System.out.println("book dao save ...");
    }

    public void update(){
    System.out.println("book dao update ...");
    }
    }
  • 创建Spring的配置类

    1
    2
    3
    4
    @Configuration
    @ComponentScan("top.qianqianzyk")
    public class SpringConfig {
    }
  • 编写App运行类

    1
    2
    3
    4
    5
    6
    7
    public class App {
    public static void main(String[] args) {
    ApplicationContext ctx = new AnnotationConfigApplicationContext(SpringConfig.class);
    BookDao bookDao = ctx.getBean(BookDao.class);
    bookDao.save();
    }
    }

(1)添加依赖

pom.xml

1
2
3
4
5
<dependency>
<groupId>org.aspectj</groupId>
<artifactId>aspectjweaver</artifactId>
<version>1.9.4</version>
</dependency>
  • 因为spring-context中已经导入了spring-aop,所以不需要再单独导入spring-aop
  • 导入AspectJ的jar包,AspectJ是AOP思想的一个具体实现,Spring有自己的AOP实现,但是相比于AspectJ来说比较麻烦,所以我们直接采用Spring整合ApsectJ的方式进行AOP开发

(2)定义接口与实现类

(3)定义通知类和通知

1
2
3
4
5
public class MyAdvice {
public void method(){
System.out.println(System.currentTimeMillis());
}
}

(4)定义切入点

1
2
3
4
5
6
7
public class MyAdvice {
@Pointcut("execution(void top.qianqianzyk.dao.BookDao.update())")
private void pt(){}
public void method(){
System.out.println(System.currentTimeMillis());
}
}
  • 切入点定义依托一个不具有实际意义的方法进行,即无参数、无返回值、方法体无实际逻辑。

(5)制作切面

1
2
3
4
5
6
7
8
9
public class MyAdvice {
@Pointcut("execution(void com.itheima.dao.BookDao.update())")
private void pt(){}

@Before("pt()")
public void method(){
System.out.println(System.currentTimeMillis());
}
}
  • 绑定切入点与通知关系,并指定通知添加到原始连接点的具体执行位置

(6)将通知类配给容器并标识其为切面类

1
2
3
4
5
6
7
8
9
10
11
@Component
@Aspect
public class MyAdvice {
@Pointcut("execution(void com.itheima.dao.BookDao.update())")
private void pt(){}

@Before("pt()")
public void method(){
System.out.println(System.currentTimeMillis());
}
}

(7)开启AOP功能

1
2
3
4
5
@Configuration
@ComponentScan("top.qianqianzyk")
@EnableAspectJAutoProxy
public class SpringConfig {
}

AOP工作流程

AOP工作流程

AOP是基于Spring容器管理的bean做的增强

流程1:Spring容器启动

  • 加载bean:需要被增强的类,如:BookServiceImpl;通知类,如:MyAdvice

流程2:读取所有切面配置中的切入点

  • 没有被使用的切入点将不会被读取

流程3:初始化bean

  • 要被实例化bean对象的类中的方法和切入点进行匹配
    • 匹配失败,创建原始对象
      • 匹配失败说明不需要增强,直接调用原始对象的方法即可
    • 匹配成功,创建原始对象(目标对象)的代理对象,如:
      • 匹配成功说明需要对其进行增强
      • 对哪个类做增强,这个类对应的对象就叫做目标对象
      • 因为要对目标对象进行功能增强,而采用的技术是动态代理,所以会为其创建一个代理对象
      • 最终运行的是代理对象的方法,在该方法中会对原始方法进行功能增强

流程4:获取bean执行方法

  • 获取的bean是原始对象时,调用方法并执行,完成操作
  • 获取的bean是代理对象时,根据代理对象的运行模式运行原始方法与增强的内容,完成操作

AOP核心概念

  • 目标对象(Target):原始功能去掉共性功能对应的类产生的对象,这种对象是无法直接完成最终工作的
    • 目标对象就是要增强的类[如:BookServiceImpl类]对应的对象,也叫原始对象,不能说它不能运行,只能说它在运行的过程中对于要增强的内容是缺失的
  • 代理(Proxy):目标对象无法直接完成工作,需要对其进行功能回填,通过原始对象的代理对象实现
    • SpringAOP是在不改变原有设计(代码)的前提下对其进行增强的,它的底层采用的是代理模式实现的,所以要对原始对象进行增强,就需要对原始对象创建代理对象,在代理对象中的方法把通知[如:MyAdvice中的method方法]内容加进去,就实现了增强,这就是我们所说的代理(Proxy)

AOP配置管理

AOP切入点表达式

语法格式

描述方式一:执行top.qianqianzyk.dao包下的BookDao接口中的无参数update方法

1
execution(void top.qianqianzyk.dao.BookDao.update())

描述方式二:执行top.qianqianzyk.dao.impl包下的BookDaoImpl类中的无参数update方法

1
execution(void top.qianqianzyk.dao.impl.BookDaoImpl.update())

因为调用接口方法的时候最终运行的还是其实现类的方法,所以上面两种描述方式都是可以的

对于切入点表达式的语法为:

  • 切入点表达式标准格式:动作关键字(访问修饰符 返回值 包名.类/接口名.方法名(参数) 异常名)

对于这个格式,我们不需要硬记,通过一个例子,理解它:

1
execution(public User top.qianqianzyk.service.UserService.findById(int))
  • execution:动作关键字,描述切入点的行为动作,例如execution表示执行到指定切入点
  • public:访问修饰符,还可以是public,private等,可以省略
  • User:返回值,写返回值类型
  • top.qianqianzyk.service:包名,多级包使用点连接
  • UserService:类/接口名称
  • findById:方法名
  • int:参数,直接写参数的类型,多个类型用逗号隔开
  • 异常名:方法定义中抛出指定异常,可以省略

切入点表达式就是要找到需要增强的方法,所以它就是对一个具体方法的描述,但是方法的定义会有很多,所以如果每一个方法对应一个切入点表达式,想想这块就会觉得将来编写起来会比较麻烦,有没有更简单的方式呢?就需要用到通配符了java

通配符

我们使用通配符描述切入点,主要的目的就是简化之前的配置

  • *:单个独立的任意符号,可以独立出现,也可以作为前缀或者后缀的匹配符出现

    1
    execution(public * top.qianqianzyk.*.UserService.find*(*))

    匹配top.qianqianzyk包下的任意包中的UserService类或接口中所有find开头的带有一个参数的方法

  • ..:多个连续的任意符号,可以独立出现,常用于简化包名与参数的书写

    1
    execution(public User top..UserService.findById(..))

    匹配top包下的任意包中的UserService类或接口中所有名称为findById的方法

  • +:专用于匹配子类类型

    1
    execution(* *..*Service+.*(..))

    这个使用率较低,描述子类的,因为做JavaEE开发,继承机会就一次,使用都很慎重,所以很少用它。*Service+,表示所有以Service结尾的接口的子类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
execution(void top.qianqianzyk.dao.BookDao.update())
匹配接口,能匹配到
execution(void top.qianqianzyk.dao.impl.BookDaoImpl.update())
匹配实现类,能匹配到
execution(* top.qianqianzyk.dao.impl.BookDaoImpl.update())
返回值任意,能匹配到
execution(* top.qianqianzyk.dao.impl.BookDaoImpl.update(*))
返回值任意,但是update方法必须要有一个参数,无法匹配,要想匹配需要在update接口和实现类添加参数
execution(void top.*.*.*.*.update())
返回值为void,top包下的任意包三层包下的任意类的update方法,匹配到的是实现类,能匹配
execution(void top.*.*.*.update())
返回值为void,top包下的任意两层包下的任意类的update方法,匹配到的是接口,能匹配
execution(void *..update())
返回值为void,方法名是update的任意包下的任意类,能匹配
execution(* *..*(..))
匹配项目中任意类的任意方法,能匹配,但是不建议使用这种方式,影响范围广
execution(* *..u*(..))
匹配项目中任意包任意类下只要以u开头的方法,update方法能满足,能匹配
execution(* *..*e(..))
匹配项目中任意包任意类下只要以e结尾的方法,update和save方法能满足,能匹配
execution(void top..*())
返回值为void,top包下的任意包任意类任意方法,能匹配,*代表的是方法
execution(* top.qianqianzyk.*.*Service.find*(..))
将项目中所有业务层方法的以find开头的方法匹配
execution(* top.qianqianzyk.*.*Service.save*(..))
将项目中所有业务层方法的以save开头的方法匹配

后面两种更符合我们平常切入点表达式的编写规则

书写技巧

对于切入点表达式的编写其实是很灵活的,那么在编写的时候,有没有什么好的技巧让我们用用:

  • 所有代码按照标准规范开发,否则以下技巧全部失效
  • 描述切入点通常描述接口,而不描述实现类,如果描述到实现类,就出现紧耦合了
  • 访问控制修饰符针对接口开发均采用public描述(可省略访问控制修饰符描述
  • 返回值类型对于增删改类使用精准类型加速匹配,对于查询类使用*通配快速描述
  • **包名**书写*尽量不使用..匹配**,效率过低,常用做单个包描述匹配,或精准匹配
  • **接口名/类名**书写名称与模块相关的*采用*匹配**,例如UserService书写成Service,绑定业务层接口名
  • **方法名**书写以**动词*进行*精准匹配**,名词采用匹配,例如getById书写成getBy,selectAll书写成selectAll
  • 参数规则较为复杂,根据业务方法灵活调整
  • 通常不使用异常作为匹配规则

AOP通知类型

类型介绍
  • 前置通知:追加功能到方法执行前,类似于在代码1或者代码2添加内容
  • 后置通知:追加功能到方法执行后,不管方法执行的过程中有没有抛出异常都会执行,类似于在代码5添加内容
  • **环绕通知(重点)**:环绕通知功能比较强大,它可以追加功能到方法执行的前后,这也是比较常用的方式,它可以实现其他四种通知类型的功能
  • 返回后通知(了解):追加功能到方法执行后,只有方法正常执行结束后才进行,类似于在代码3添加内容,如果方法执行抛出异常,返回后通知将不会被添加
  • 抛出异常后通知(了解):追加功能到方法抛出异常后,只有方法执行出异常才进行,类似于在代码4添加内容,只有方法抛出异常后才会被添加

环境准备
  • pom.xml添加Spring依赖

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    <dependencies>
    <dependency>
    <groupId>org.springframework</groupId>
    <artifactId>spring-context</artifactId>
    <version>5.2.10.RELEASE</version>
    </dependency>
    <dependency>
    <groupId>org.aspectj</groupId>
    <artifactId>aspectjweaver</artifactId>
    <version>1.9.4</version>
    </dependency>
    </dependencies>
  • 添加BookDao和BookDaoImpl类

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    public interface BookDao {
    public void update();
    public int select();
    }

    @Repository
    public class BookDaoImpl implements BookDao {
    public void update(){
    System.out.println("book dao update ...");
    }
    public int select() {
    System.out.println("book dao select is running ...");
    return 100;
    }
    }
  • 创建Spring的配置类

    1
    2
    3
    4
    5
    @Configuration
    @ComponentScan("top.qianqianzyk")
    @EnableAspectJAutoProxy
    public class SpringConfig {
    }
  • 创建通知类

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    @Component
    @Aspect
    public class MyAdvice {
    @Pointcut("execution(void top.qianqianzyk.dao.BookDao.update())")
    private void pt(){}

    public void before() {
    System.out.println("before advice ...");
    }

    public void after() {
    System.out.println("after advice ...");
    }

    public void around(){
    System.out.println("around before advice ...");
    System.out.println("around after advice ...");
    }

    public void afterReturning() {
    System.out.println("afterReturning advice ...");
    }

    public void afterThrowing() {
    System.out.println("afterThrowing advice ...");
    }
    }
  • 编写App运行类

    1
    2
    3
    4
    5
    6
    7
    public class App {
    public static void main(String[] args) {
    ApplicationContext ctx = new AnnotationConfigApplicationContext(SpringConfig.class);
    BookDao bookDao = ctx.getBean(BookDao.class);
    bookDao.update();
    }
    }
通知类型的使用
前置通知

修改MyAdvice,在before方法上添加@Before注解

1
2
3
4
5
6
7
8
9
10
11
@Component
@Aspect
public class MyAdvice {
@Pointcut("execution(void top.qianqianzyk.dao.BookDao.update())")
private void pt(){}

@Before("pt()")
public void before() {
System.out.println("before advice ...");
}
}
后置通知
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
@Component
@Aspect
public class MyAdvice {
@Pointcut("execution(void top.qianqianzyk.dao.BookDao.update())")
private void pt(){}

@Before("pt()")
public void before() {
System.out.println("before advice ...");
}
@After("pt()")
public void after() {
System.out.println("after advice ...");
}
}
环绕通知

基本使用

1
2
3
4
5
6
7
8
9
10
11
12
@Component
@Aspect
public class MyAdvice {
@Pointcut("execution(void top.qianqianzyk.dao.BookDao.update())")
private void pt(){}

@Around("pt()")
public void around(){
System.out.println("around before advice ...");
System.out.println("around after advice ...");
}
}

执行后发现原始方法的内容却没有被执行

因为环绕通知需要在原始方法的前后进行增强,所以环绕通知就必须要能对原始操作进行调用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
@Component
@Aspect
public class MyAdvice {
@Pointcut("execution(void top.qianqianzyk.dao.BookDao.update())")
private void pt(){}

@Around("pt()")
public void around(ProceedingJoinPoint pjp) throws Throwable{
System.out.println("around before advice ...");
//表示对原始操作的调用
pjp.proceed();
System.out.println("around after advice ...");
}
}

注意事项

  • 当原始方法有返回值的处理,我们就要根据原始方法的返回值来设置环绕通知的返回值
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
@Component
@Aspect
public class MyAdvice {
@Pointcut("execution(void top.qianqianzyk.dao.BookDao.update())")
private void pt(){}

@Pointcut("execution(int top.qianqianzyk.dao.BookDao.select())")
private void pt2(){}

@Around("pt2()")
public Object aroundSelect(ProceedingJoinPoint pjp) throws Throwable {
System.out.println("around before advice ...");
//表示对原始操作的调用
Object ret = pjp.proceed();
System.out.println("around after advice ...");
return ret;
}
}

环绕通知注意事项

  1. 环绕通知必须依赖形参ProceedingJoinPoint才能实现对原始方法的调用,进而实现原始方法调用前后同时添加通知
  2. 通知中如果未使用ProceedingJoinPoint对原始方法进行调用将跳过原始方法的执行
  3. 对原始方法的调用可以不接收返回值,通知方法设置成void即可,如果接收返回值,最好设定为Object类型
  4. 原始方法的返回值如果是void类型,通知方法的返回值类型可以设置成void,也可以设置成Object
  5. 由于无法预知原始方法运行后是否会抛出异常,因此环绕通知方法必须要处理Throwable异常
返回后通知
1
2
3
4
5
6
7
8
9
10
11
12
13
14
@Component
@Aspect
public class MyAdvice {
@Pointcut("execution(void top.qianqianzyk.dao.BookDao.update())")
private void pt(){}

@Pointcut("execution(int top.qianqianzyk.dao.BookDao.select())")
private void pt2(){}

@AfterReturning("pt2()")
public void afterReturning() {
System.out.println("afterReturning advice ...");
}
}
异常后通知
1
2
3
4
5
6
7
8
9
10
11
12
13
14
@Component
@Aspect
public class MyAdvice {
@Pointcut("execution(void top.qianqianzyk.dao.BookDao.update())")
private void pt(){}

@Pointcut("execution(int top.qianqianzyk.dao.BookDao.select())")
private void pt2(){}

@AfterReturning("pt2()")
public void afterThrowing() {
System.out.println("afterThrowing advice ...");
}
}

业务层接口执行效率

一个简单的案例,目的是查看每个业务层执行的时间,这样就可以监控出哪个业务比较耗时,将其查找出来方便优化

环境准备
  • pom.xml添加Spring依赖

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    <dependencies>
    <dependency>
    <groupId>org.springframework</groupId>
    <artifactId>spring-context</artifactId>
    <version>5.2.10.RELEASE</version>
    </dependency>
    <dependency>
    <groupId>org.springframework</groupId>
    <artifactId>spring-jdbc</artifactId>
    <version>5.2.10.RELEASE</version>
    </dependency>
    <dependency>
    <groupId>org.springframework</groupId>
    <artifactId>spring-test</artifactId>
    <version>5.2.10.RELEASE</version>
    </dependency>
    <dependency>
    <groupId>org.aspectj</groupId>
    <artifactId>aspectjweaver</artifactId>
    <version>1.9.4</version>
    </dependency>
    <dependency>
    <groupId>mysql</groupId>
    <artifactId>mysql-connector-java</artifactId>
    <version>5.1.47</version>
    </dependency>
    <dependency>
    <groupId>com.alibaba</groupId>
    <artifactId>druid</artifactId>
    <version>1.1.16</version>
    </dependency>
    <dependency>
    <groupId>org.mybatis</groupId>
    <artifactId>mybatis</artifactId>
    <version>3.5.6</version>
    </dependency>
    <dependency>
    <groupId>org.mybatis</groupId>
    <artifactId>mybatis-spring</artifactId>
    <version>1.3.0</version>
    </dependency>
    <dependency>
    <groupId>junit</groupId>
    <artifactId>junit</artifactId>
    <version>4.12</version>
    <scope>test</scope>
    </dependency>
    </dependencies>
  • 添加AccountService、AccountServiceImpl、AccountDao与Account类

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    public interface AccountService {
    void save(Account account);
    void delete(Integer id);
    void update(Account account);
    List<Account> findAll();
    Account findById(Integer id);
    }

    @Service
    public class AccountServiceImpl implements AccountService {

    @Autowired
    private AccountDao accountDao;

    public void save(Account account) {
    accountDao.save(account);
    }

    public void update(Account account){
    accountDao.update(account);
    }

    public void delete(Integer id) {
    accountDao.delete(id);
    }

    public Account findById(Integer id) {
    return accountDao.findById(id);
    }

    public List<Account> findAll() {
    return accountDao.findAll();
    }
    }
    public interface AccountDao {

    @Insert("insert into tbl_account(name,money)values(#{name},#{money})")
    void save(Account account);

    @Delete("delete from tbl_account where id = #{id} ")
    void delete(Integer id);

    @Update("update tbl_account set name = #{name} , money = #{money} where id = #{id} ")
    void update(Account account);

    @Select("select * from tbl_account")
    List<Account> findAll();

    @Select("select * from tbl_account where id = #{id} ")
    Account findById(Integer id);
    }

    public class Account implements Serializable {

    private Integer id;
    private String name;
    private Double money;
    //setter..getter..toString方法省略
    }
  • resources下提供一个jdbc.properties

    1
    2
    3
    4
    jdbc.driver=com.mysql.jdbc.Driver
    jdbc.url=jdbc:mysql://localhost:3306/spring_db?useSSL=false
    jdbc.username=root
    jdbc.password=root
  • 创建相关配置类

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    //Spring配置类:SpringConfig
    @Configuration
    @EnableAspectJAutoProxy
    @ComponentScan("top.qianqianzyk")
    @PropertySource("classpath:jdbc.properties")
    @Import({JdbcConfig.class,MybatisConfig.class})
    public class SpringConfig {
    }
    //JdbcConfig配置类
    public class JdbcConfig {
    @Value("${jdbc.driver}")
    private String driver;
    @Value("${jdbc.url}")
    private String url;
    @Value("${jdbc.username}")
    private String userName;
    @Value("${jdbc.password}")
    private String password;

    @Bean
    public DataSource dataSource(){
    DruidDataSource ds = new DruidDataSource();
    ds.setDriverClassName(driver);
    ds.setUrl(url);
    ds.setUsername(userName);
    ds.setPassword(password);
    return ds;
    }
    }
    //MybatisConfig配置类
    public class MybatisConfig {

    @Bean
    public SqlSessionFactoryBean sqlSessionFactory(DataSource dataSource){
    SqlSessionFactoryBean ssfb = new SqlSessionFactoryBean();
    ssfb.setTypeAliasesPackage("com.itheima.domain");
    ssfb.setDataSource(dataSource);
    return ssfb;
    }

    @Bean
    public MapperScannerConfigurer mapperScannerConfigurer(){
    MapperScannerConfigurer msc = new MapperScannerConfigurer();
    msc.setBasePackage("com.itheima.dao");
    return msc;
    }
    }
  • 编写Spring整合Junit的测试类

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    @RunWith(SpringJUnit4ClassRunner.class)
    @ContextConfiguration(classes = SpringConfig.class)
    public class AccountServiceTestCase {
    @Autowired
    private AccountService accountService;

    @Test
    public void testFindById(){
    Account ac = accountService.findById(2);
    }

    @Test
    public void testFindAll(){
    List<Account> all = accountService.findAll();
    }

    }
功能开发
  • 创建AOP的通知类
1
2
3
4
5
6
7
8
9
10
11
@Component
@Aspect
public class ProjectAdvice {
//配置业务层的所有方法
@Pointcut("execution(* top.qianqianzyk.service.*Service.*(..))")
private void servicePt(){}

public void runSpeed(){

}
}
  • 添加环绕通知
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
@Component
@Aspect
public class ProjectAdvice {
//配置业务层的所有方法
@Pointcut("execution(* top.qianqianzyk.service.*Service.*(..))")
private void servicePt(){}
//@Around("ProjectAdvice.servicePt()") 可以简写为下面的方式
@Around("servicePt()")
public void runSpeed(ProceedingJoinPoint pjp){
//获取执行签名信息
Signature signature = pjp.getSignature();
//通过签名获取执行操作名称(接口名)
String className = signature.getDeclaringTypeName();
//通过签名获取执行操作名称(方法名)
String methodName = signature.getName();

long start = System.currentTimeMillis();
for (int i = 0; i < 10000; i++) {
pjp.proceed();
}
long end = System.currentTimeMillis();
System.out.println("万次执行:"+ className+"."+methodName+"---->" +(end-start) + "ms");
}
}
  • 运行单元测试类

AOP通知获取数据

目前仅仅是在原始方法前后追加一些操作,接下来将从获取参数获取返回值获取异常三个方面来研究切入点的相关信息

分析一下五种通知类型都会有参数,返回值和异常吗?

  • 获取切入点方法的参数,所有的通知类型都可以获取参数
    • JoinPoint:适用于前置、后置、返回后、抛出异常后通知
    • ProceedingJoinPoint:适用于环绕通知
  • 获取切入点方法返回值,前置和抛出异常后通知是没有返回值,后置通知可有可无
    • 返回后通知
    • 环绕通知
  • 获取切入点方法运行异常信息,前置和返回后通知是不会有,后置通知可有可无
    • 抛出异常后通知
    • 环绕通知
环境准备
  • pom.xml添加Spring依赖

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    <dependencies>
    <dependency>
    <groupId>org.springframework</groupId>
    <artifactId>spring-context</artifactId>
    <version>5.2.10.RELEASE</version>
    </dependency>
    <dependency>
    <groupId>org.aspectj</groupId>
    <artifactId>aspectjweaver</artifactId>
    <version>1.9.4</version>
    </dependency>
    </dependencies>
  • 添加BookDao和BookDaoImpl类

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    public interface BookDao {
    public String findName(int id);
    }
    @Repository
    public class BookDaoImpl implements BookDao {

    public String findName(int id) {
    System.out.println("id:"+id);
    return "qianqian";
    }
    }
  • 创建Spring的配置类

    1
    2
    3
    4
    5
    @Configuration
    @ComponentScan("top.qianqianzyk")
    @EnableAspectJAutoProxy
    public class SpringConfig {
    }
  • 编写通知类

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    @Component
    @Aspect
    public class MyAdvice {
    @Pointcut("execution(* top.qianqianzyk.dao.BookDao.findName(..))")
    private void pt(){}

    @Before("pt()")
    public void before() {
    System.out.println("before advice ..." );
    }

    @After("pt()")
    public void after() {
    System.out.println("after advice ...");
    }

    @Around("pt()")
    public Object around() throws Throwable{
    Object ret = pjp.proceed();
    return ret;
    }
    @AfterReturning("pt()")
    public void afterReturning() {
    System.out.println("afterReturning advice ...");
    }


    @AfterThrowing("pt()")
    public void afterThrowing() {
    System.out.println("afterThrowing advice ...");
    }
    }
  • 编写App运行类

    1
    2
    3
    4
    5
    6
    7
    8
    public class App {
    public static void main(String[] args) {
    ApplicationContext ctx = new AnnotationConfigApplicationContext(SpringConfig.class);
    BookDao bookDao = ctx.getBean(BookDao.class);
    String name = bookDao.findName(100);
    System.out.println(name);
    }
    }
获取参数
非环绕通知获取方式

在方法上添加JoinPoint,通过JoinPoint来获取参数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
@Component
@Aspect
public class MyAdvice {
@Pointcut("execution(* top.qianqianzyk.dao.BookDao.findName(..))")
private void pt(){}

@Before("pt()")
public void before(JoinPoint jp)
Object[] args = jp.getArgs();
System.out.println(Arrays.toString(args));
System.out.println("before advice ..." );
}
//...其他的略
}
环绕通知获取方式
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
@Component
@Aspect
public class MyAdvice {
@Pointcut("execution(* top.qianqianzyk.dao.BookDao.findName(..))")
private void pt(){}

@Around("pt()")
public Object around(ProceedingJoinPoint pjp)throws Throwable {
Object[] args = pjp.getArgs();
System.out.println(Arrays.toString(args));
Object ret = pjp.proceed();
return ret;
}
//其他的略
}
  • pjp.proceed()方法是有两个构造方法

    • 调用无参数的proceed,当原始方法有参数,会在调用的过程中自动传入参数

    • 当需要修改原始方法的参数时,就只能采用带有参数的方法

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      @Component
      @Aspect
      public class MyAdvice {
      @Pointcut("execution(* top.qianqianzyk.dao.BookDao.findName(..))")
      private void pt(){}

      @Around("pt()")
      public Object around(ProceedingJoinPoint pjp) throws Throwable{
      Object[] args = pjp.getArgs();
      System.out.println(Arrays.toString(args));
      args[0] = 666;
      Object ret = pjp.proceed(args);
      return ret;
      }
      //其他的略
      }

      有了这个特性后,就可以在环绕通知中对原始方法的参数进行拦截过滤,避免由于参数的问题导致程序无法正确运行

获取返回值
环绕通知获取返回值
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
@Component
@Aspect
public class MyAdvice {
@Pointcut("execution(* top.qianqianzyk.dao.BookDao.findName(..))")
private void pt(){}

@Around("pt()")
public Object around(ProceedingJoinPoint pjp) throws Throwable{
Object[] args = pjp.getArgs();
System.out.println(Arrays.toString(args));
args[0] = 666;
Object ret = pjp.proceed(args);
return ret;
}
//其他的略
}

上述代码中,ret就是方法的返回值,是可以直接获取的,不但可以获取,如果需要还可以进行修改

返回后通知获取返回值
1
2
3
4
5
6
7
8
9
10
11
12
@Component
@Aspect
public class MyAdvice {
@Pointcut("execution(* top.qianqianzyk.dao.BookDao.findName(..))")
private void pt(){}

@AfterReturning(value = "pt()",returning = "ret")
public void afterReturning(Object ret) {
System.out.println("afterReturning advice ..."+ret);
}
//其他的略
}

(1)参数名的问题returning = "ret",Object ret,这两个参数名字必须一致

(2)afterReturning方法参数类型的问题:参数类型可以写成String,但是为了能匹配更多的参数类型,建议写成Object类型

(3)afterReturning方法参数的顺序问题:如果有JoinPoint参数,其必须要放在第一位

获取异常

对于获取抛出的异常,只有抛出异常后AfterThrowing和环绕Around这两个通知类型可以获取,具体如何获取?

环绕通知获取异常
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
@Component
@Aspect
public class MyAdvice {
@Pointcut("execution(* top.qianqianzyk.dao.BookDao.findName(..))")
private void pt(){}

@Around("pt()")
public Object around(ProceedingJoinPoint pjp){
Object[] args = pjp.getArgs();
System.out.println(Arrays.toString(args));
args[0] = 666;
Object ret = null;
try{
ret = pjp.proceed(args);
}catch(Throwable throwable){
t.printStackTrace();
}
return ret;
}
//其他的略
}

在catch方法中就可以获取到异常

抛出异常后通知获取异常
1
2
3
4
5
6
7
8
9
10
11
12
@Component
@Aspect
public class MyAdvice {
@Pointcut("execution(* top.qianqianzyk.dao.BookDao.findName(..))")
private void pt(){}

@AfterThrowing(value = "pt()",throwing = "t")
public void afterThrowing(Throwable t) {
System.out.println("afterThrowing advice ..."+t);
}
//其他的略
}

如何让原始方法抛出异常,方式有很多,

1
2
3
4
5
6
7
8
9
10
11
@Repository
public class BookDaoImpl implements BookDao {

public String findName(int id,String password) {
System.out.println("id:"+id);
if(true){
throw new NullPointerException();
}
return "qianqian";
}
}

百度网盘密码数据兼容处理

对百度网盘分享链接输入密码时尾部多输入的空格做兼容处理

环境准备
  • pom.xml添加Spring依赖

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    <dependencies>
    <dependency>
    <groupId>org.springframework</groupId>
    <artifactId>spring-context</artifactId>
    <version>5.2.10.RELEASE</version>
    </dependency>
    <dependency>
    <groupId>org.aspectj</groupId>
    <artifactId>aspectjweaver</artifactId>
    <version>1.9.4</version>
    </dependency>
    </dependencies>
  • 添加ResourcesService,ResourcesServiceImpl,ResourcesDao和ResourcesDaoImpl类

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    public interface ResourcesDao {
    boolean readResources(String url, String password);
    }
    @Repository
    public class ResourcesDaoImpl implements ResourcesDao {
    public boolean readResources(String url, String password) {
    //模拟校验
    return password.equals("root");
    }
    }
    public interface ResourcesService {
    public boolean openURL(String url ,String password);
    }
    @Service
    public class ResourcesServiceImpl implements ResourcesService {
    @Autowired
    private ResourcesDao resourcesDao;

    public boolean openURL(String url, String password) {
    return resourcesDao.readResources(url,password);
    }
    }
  • 创建Spring的配置类

    1
    2
    3
    4
    @Configuration
    @ComponentScan("top.qianqianzyk")
    public class SpringConfig {
    }
  • 编写App运行类

    1
    2
    3
    4
    5
    6
    7
    8
    public class App {
    public static void main(String[] args) {
    ApplicationContext ctx = new AnnotationConfigApplicationContext(SpringConfig.class);
    ResourcesService resourcesService = ctx.getBean(ResourcesService.class);
    boolean flag = resourcesService.openURL("http://pan.baidu.com/haha", "root");
    System.out.println(flag);
    }
    }
具体实现

(1)开启SpringAOP的注解功能

1
2
3
4
5
@Configuration
@ComponentScan("top.qianqianzyk")
@EnableAspectJAutoProxy
public class SpringConfig {
}

(2)编写通知类

1
2
3
4
5
6
7
@Component
@Aspect
public class DataAdvice {
@Pointcut("execution(boolean top.qianqianzyk.service.*Service.*(*,*))")
private void servicePt(){}

}

(3)添加环绕通知

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
@Component
@Aspect
public class DataAdvice {
@Pointcut("execution(boolean top.qianqianzyk.service.*Service.*(*,*))")
private void servicePt(){}

@Around("DataAdvice.servicePt()")
// @Around("servicePt()")这两种写法都对
public Object trimStr(ProceedingJoinPoint pjp) throws Throwable {
//获取原始方法的参数
Object[] args = pjp.getArgs();
for (int i = 0; i < args.length; i++) {
//判断参数是不是字符串
if(args[i].getClass().equals(String.class)){
args[i] = args[i].toString().trim();
}
}
//将修改后的参数传入到原始方法的执行中
Object ret = pjp.proceed(args);
return ret;
}
}

AOP总结

AOP的核心概念

  • 概念:AOP(Aspect Oriented Programming)面向切面编程,一种编程范式
  • 作用:在不惊动原始设计的基础上为方法进行功能==增强==
  • 核心概念
    • 代理(Proxy):SpringAOP的核心本质是采用代理模式实现的
    • 连接点(JoinPoint):在SpringAOP中,理解为任意方法的执行
    • 切入点(Pointcut):匹配连接点的式子,也是具有共性功能的方法描述
    • 通知(Advice):若干个方法的共性功能,在切入点处执行,最终体现为一个方法
    • 切面(Aspect):描述通知与切入点的对应关系
    • 目标对象(Target):被代理的原始对象成为目标对象

切入点表达式

  • 切入点表达式标准格式:动作关键字(访问修饰符 返回值 包名.类/接口名.方法名(参数)异常名)

    1
    execution(* com.itheima.service.*Service.*(..))
  • 切入点表达式描述通配符:

    • 作用:用于快速描述,范围描述
    • *:匹配任意符号(常用)
    • .. :匹配多个连续的任意符号(常用)
    • +:匹配子类类型
  • 切入点表达式书写技巧

    1.按标准规范开发

    2.查询操作的返回值建议使用*匹配

    3.减少使用..的形式描述包

    4.对接口进行描述,使用*表示模块名,例如UserService的匹配描述为*Service

    5.方法名书写保留动词,例如get,使用*表示名词,例如getById匹配描述为getBy*

    6.参数根据实际情况灵活调整

五种通知类型

  • 前置通知
  • 后置通知
  • 环绕通知(重点)
    • 环绕通知依赖形参ProceedingJoinPoint才能实现对原始方法的调用
    • 环绕通知可以隔离原始方法的调用执行
    • 环绕通知返回值设置为Object类型
    • 环绕通知中可以对原始方法调用过程中出现的异常进行处理
  • 返回后通知
  • 抛出异常后通知

AOP事务管理

Spring事务介绍

相关概念

  • 事务作用:在数据层保障一系列的数据库操作同成功同失败
  • Spring事务作用:在数据层或业务层保障一系列的数据库操作同成功同失败

Spring为了管理事务,提供了一个平台事务管理器PlatformTransactionManager

1
2
3
4
5
6
7
8
public interface PlatformTransactionManager{
void commit(TransactionStatus status) throws TransactionException;
void rollback(TransactionStatus status) throws TransactionException;
}
// 具体实现
public class DataSourceTransactionManager {
······
}

只需要给它一个DataSource对象,它就可以帮你去在业务层管理事务。其内部采用的是JDBC的事务。所以说如果你持久层采用的是JDBC相关的技术,就可以采用这个事务管理器来管理你的事务。而Mybatis内部采用的就是JDBC的事务

简单案例

需求: 实现任意两个账户间转账操作

为了实现上述的业务需求,我们可以按照下面步骤来实现下: ①:数据层提供基础操作,指定账户减钱(outMoney),指定账户加钱(inMoney)

②:业务层提供转账操作(transfer),调用减钱与加钱的操作

③:提供2个账号和操作金额执行转账操作

④:基于Spring整合MyBatis环境搭建上述操作

环境准备

pom.xml

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
<dependencies>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-context</artifactId>
<version>5.2.10.RELEASE</version>
</dependency>
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>druid</artifactId>
<version>1.1.16</version>
</dependency>

<dependency>
<groupId>org.mybatis</groupId>
<artifactId>mybatis</artifactId>
<version>3.5.6</version>
</dependency>

<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>5.1.47</version>
</dependency>

<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-jdbc</artifactId>
<version>5.2.10.RELEASE</version>
</dependency>

<dependency>
<groupId>org.mybatis</groupId>
<artifactId>mybatis-spring</artifactId>
<version>1.3.0</version>
</dependency>

<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>4.12</version>
<scope>test</scope>
</dependency>

<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-test</artifactId>
<version>5.2.10.RELEASE</version>
</dependency>

</dependencies>

根据表创建模型类

1
2
3
4
5
6
7
public class Account implements Serializable {

private Integer id;
private String name;
private Double money;
//setter...getter...toString...方法略
}

创建Dao接口

1
2
3
4
5
6
7
8
public interface AccountDao {

@Update("update tbl_account set money = money + #{money} where name = #{name}")
void inMoney(@Param("name") String name, @Param("money") Double money);

@Update("update tbl_account set money = money - #{money} where name = #{name}")
void outMoney(@Param("name") String name, @Param("money") Double money);
}

创建Service接口和实现类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
public interface AccountService {
/**
* 转账操作
* @param out 传出方
* @param in 转入方
* @param money 金额
*/
public void transfer(String out,String in ,Double money) ;
}

@Service
public class AccountServiceImpl implements AccountService {
@Autowired
private AccountDao accountDao;

public void transfer(String out,String in ,Double money) {
accountDao.outMoney(out,money);
accountDao.inMoney(in,money);
}
}

添加jdbc.properties文件

1
2
3
4
jdbc.driver=com.mysql.jdbc.Driver
jdbc.url=jdbc:mysql://localhost:3306/spring_db?useSSL=false
jdbc.username=root
jdbc.password=root

创建JdbcConfig配置类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
public class JdbcConfig {
@Value("${jdbc.driver}")
private String driver;
@Value("${jdbc.url}")
private String url;
@Value("${jdbc.username}")
private String userName;
@Value("${jdbc.password}")
private String password;

@Bean
public DataSource dataSource(){
DruidDataSource ds = new DruidDataSource();
ds.setDriverClassName(driver);
ds.setUrl(url);
ds.setUsername(userName);
ds.setPassword(password);
return ds;
}
}

创建MybatisConfig配置类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public class MybatisConfig {
@Bean
public SqlSessionFactoryBean sqlSessionFactory(DataSource dataSource){
SqlSessionFactoryBean ssfb = new SqlSessionFactoryBean();
ssfb.setTypeAliasesPackage("top.qianqianzyk.domain");
ssfb.setDataSource(dataSource);
return ssfb;
}
@Bean
public MapperScannerConfigurer mapperScannerConfigurer(){
MapperScannerConfigurer msc = new MapperScannerConfigurer();
msc.setBasePackage("top.qianqianzyk.dao");
return msc;
}
}

创建SpringConfig配置类

1
2
3
4
5
6
@Configuration
@ComponentScan("top.qianqianzyk")
@PropertySource("classpath:jdbc.properties")
@Import({JdbcConfig.class,MybatisConfig.class})
public class SpringConfig {
}

编写测试类

1
2
3
4
5
6
7
8
9
10
11
12
@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(classes = SpringConfig.class)
public class AccountServiceTest {
@Autowired
private AccountService accountService;

@Test
public void testTransfer() throws IOException {
accountService.transfer("Tom","Jerry",100D);
}

}
事务管理

(1)在需要被事务管理的方法上添加注解

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
public interface AccountService {
/**
* 转账操作
* @param out 传出方
* @param in 转入方
* @param money 金额
*/
//配置当前接口方法具有事务
public void transfer(String out,String in ,Double money) ;
}

@Service
public class AccountServiceImpl implements AccountService {

@Autowired
private AccountDao accountDao;
@Transactional
public void transfer(String out,String in ,Double money) {
accountDao.outMoney(out,money);
int i = 1/0;
accountDao.inMoney(in,money);
}

}

@Transactional可以写在接口类上、接口方法上、实现类上和实现类方法上

  • 写在接口类上,该接口的所有实现类的所有方法都会有事务
  • 写在接口方法上,该接口的所有实现类的该方法都会有事务
  • 写在实现类上,该类中的所有方法都会有事务
  • 写在实现类方法上,该方法上有事务
  • 建议写在实现类或实现类的方法上

(2)在JdbcConfig类中配置事务管理器

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
public class JdbcConfig {
@Value("${jdbc.driver}")
private String driver;
@Value("${jdbc.url}")
private String url;
@Value("${jdbc.username}")
private String userName;
@Value("${jdbc.password}")
private String password;

@Bean
public DataSource dataSource(){
DruidDataSource ds = new DruidDataSource();
ds.setDriverClassName(driver);
ds.setUrl(url);
ds.setUsername(userName);
ds.setPassword(password);
return ds;
}

//配置事务管理器,mybatis使用的是jdbc事务
@Bean
public PlatformTransactionManager transactionManager(DataSource dataSource){
DataSourceTransactionManager transactionManager = new DataSourceTransactionManager();
transactionManager.setDataSource(dataSource);
return transactionManager;
}
}

注意:事务管理器要根据使用技术进行选择,Mybatis框架使用的是JDBC事务,可以直接使用DataSourceTransactionManager

(3)开启事务注解

在SpringConfig的配置类中开启

1
2
3
4
5
6
7
8
@Configuration
@ComponentScan("top.qianqianzyk")
@PropertySource("classpath:jdbc.properties")
@Import({JdbcConfig.class,MybatisConfig.class
//开启注解式事务驱动
@EnableTransactionManagement
public class SpringConfig {
}

(4)运行测试类

Spring事务角色

开启Spring的事务管理后

  • transfer上添加了@Transactional注解,在该方法上就会有一个事务T
  • AccountDao的outMoney方法的事务T1加入到transfer的事务T中
  • AccountDao的inMoney方法的事务T2加入到transfer的事务T中
  • 这样就保证他们在同一个事务中,当业务层中出现异常,整个事务就会回滚,保证数据的准确性

两个概念就应运而生

  • 事务管理员:发起事务方,在Spring中通常指代业务层开启事务的方法
  • 事务协调员:加入事务方,在Spring中通常指代数据层方法,也可以是业务层方法

目前的事务管理是基于DataSourceTransactionManagerSqlSessionFactoryBean使用的是同一个数据源

Spring事务属性

事务配置

上面这些属性都可以在@Transactional注解的参数上进行设置。

  • readOnly:true只读事务,false读写事务,增删改要设为false,查询设为true。
  • timeout:设置超时时间单位秒,在多长时间之内事务没有提交成功就自动回滚,-1表示不设置超时时间。
  • rollbackFor:当出现指定异常进行事务回滚
  • noRollbackFor:当出现指定异常不进行事务回滚
    • rollbackFor是指定回滚异常,对于异常事务不应该都回滚么,为什么还要指定?
      • 这块需要更正一个知识点,并不是所有的异常都会回滚事务
      • Spring的事务只会对Error异常RuntimeException异常及其子类进行事务回顾,其他的异常类型是不会回滚的,比如IOException不符合上述条件所以不回滚
  • rollbackForClassName等同于rollbackFor,只不过属性为异常的类全名字符串
  • noRollbackForClassName等同于noRollbackFor,只不过属性为异常的类全名字符串
  • isolation设置事务的隔离级别
    • DEFAULT :默认隔离级别, 会采用数据库的隔离级别
    • READ_UNCOMMITTED : 读未提交
    • READ_COMMITTED : 读已提交
    • REPEATABLE_READ : 重复读取
    • SERIALIZABLE: 串行化

转账业务追加日志案例

在前面的转案例的基础上添加新的需求,完成转账后记录日志

  • 需求:实现任意两个账户间转账操作,并对每次转账操作在数据库进行留痕
  • 需求微缩:A账户减钱,B账户加钱,数据库记录日志

①:基于转账操作案例添加日志模块,实现数据库中记录日志

②:业务层转账操作(transfer),调用减钱、加钱与记录日志功能

无论转账操作是否成功,均进行转账操作的日志留痕

环境准备

(1)创建日志表

1
2
3
4
5
create table tbl_log(
id int primary key auto_increment,
info varchar(255),
createDate datetime
)

(2)添加LogDao接口

1
2
3
4
public interface LogDao {
@Insert("insert into tbl_log (info,createDate) values(#{info},now())")
void log(String info);
}

(3)添加LogService接口与实现类

1
2
3
4
5
6
7
8
9
10
11
12
13
public interface LogService {
void log(String out, String in, Double money);
}
@Service
public class LogServiceImpl implements LogService {

@Autowired
private LogDao logDao;
@Transactional
public void log(String out,String in,Double money ) {
logDao.log("转账操作由"+out+"到"+in+",金额:"+money);
}
}

(4)在转账的业务中添加记录日志

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
public interface AccountService {
/**
* 转账操作
* @param out 传出方
* @param in 转入方
* @param money 金额
*/
//配置当前接口方法具有事务
public void transfer(String out,String in ,Double money)throws IOException ;
}
@Service
public class AccountServiceImpl implements AccountService {

@Autowired
private AccountDao accountDao;
@Autowired
private LogService logService;
@Transactional
public void transfer(String out,String in ,Double money) {
try{
accountDao.outMoney(out,money);
accountDao.inMoney(in,money);
}finally {
logService.log(out,in,money);
}
}
}

(5)运行程序

  • 当程序正常运行,tbl_account表中转账成功,tbl_log表中日志记录成功
  • 当转账业务之间出现异常(int i =1/0),转账失败,tbl_account成功回滚,但是tbl_log表未添加数据
  • 失败原因:日志的记录与转账操作隶属同一个事务,同成功同失败
  • 最终效果:无论转账操作是否成功,日志必须保留

事务传播行为

  • log方法、inMoney方法和outMoney方法都属于增删改,分别有事务T1,T2,T3
  • transfer因为加了@Transactional注解,也开启了事务T
  • Spring事务会把T1,T2,T3都加入到事务T中

所以当转账失败后,所有的事务都回滚,导致日志没有记录下来

要想解决这个问题,就需要用到事务传播行为,所谓的事务传播行为指的是:

事务传播行为:事务协调员对事务管理员所携带事务的处理态度

这时候propagation属性就派上用场了

(1)修改logService改变事务的传播行为

1
2
3
4
5
6
7
8
9
10
11
@Service
public class LogServiceImpl implements LogService {

@Autowired
private LogDao logDao;
//propagation设置事务属性:传播行为设置为当前操作需要新事务
@Transactional(propagation = Propagation.REQUIRES_NEW)
public void log(String out,String in,Double money ) {
logDao.log("转账操作由"+out+"到"+in+",金额:"+money);
}
}

运行后,就能实现我们想要的结果,不管转账是否成功,都会记录日志

(2)事务传播行为的可选值

注解快查

IOC/DI注解开发

@Component等

名称 @Component/@Controller/@Service/@Repository
类型 类注解
位置 类定义上方
作用 设置该类为spring管理的bean
属性 value(默认):定义bean的id

@Configuration

名称 @Configuration
类型 类注解
位置 类定义上方
作用 设置该类为spring配置类
属性 value(默认):定义bean的id

@ComponentScan

名称 @ComponentScan
类型 类注解
位置 类定义上方
作用 设置spring配置类扫描路径,用于加载使用注解格式定义的bean
属性 value(默认):扫描路径,此路径可以逐层向下扫描

@Scope

名称 @Scope
类型 类注解
位置 类定义上方
作用 设置该类创建对象的作用范围 可用于设置创建出的bean是否为单例对象
属性 value(默认):定义bean作用范围, ==默认值singleton(单例),可选值prototype(非单例)==

@PostConstruct

名称 @PostConstruct
类型 方法注解
位置 方法上
作用 设置该方法为初始化方法
属性

@PreDestroy

名称 @PreDestroy
类型 方法注解
位置 方法上
作用 设置该方法为销毁方法
属性

@Autowired

名称 @Autowired
类型 属性注解 或 方法注解(了解) 或 方法形参注解(了解)
位置 属性定义上方 或 标准set方法上方 或 类set方法上方 或 方法形参前面
作用 为引用类型属性设置值
属性 required:true/false,定义该属性是否允许为null

@Qualifier

名称 @Qualifier
类型 属性注解 或 方法注解(了解)
位置 属性定义上方 或 标准set方法上方 或 类set方法上方
作用 为引用类型属性指定注入的beanId
属性 value(默认):设置注入的beanId

@Value

名称 @Value
类型 属性注解 或 方法注解(了解)
位置 属性定义上方 或 标准set方法上方 或 类set方法上方
作用 为 基本数据类型 或 字符串类型 属性设置值
属性 value(默认):要注入的属性值

@PropertySource

名称 @PropertySource
类型 类注解
位置 类定义上方
作用 加载properties文件中的属性值
属性 value(默认):设置加载的properties文件对应的文件名或文件名组成的数组

IOC/DI注解开发管理第三方bean

@Bean

名称 @Bean
类型 方法注解
位置 方法定义上方
作用 设置该方法的返回值作为spring管理的bean
属性 value(默认):定义bean的id

@Import

名称 @Import
类型 类注解
位置 类定义上方
作用 导入配置类
属性 value(默认):定义导入的配置类类名, 当配置类有多个时使用数组格式一次性导入多个配置类

Spring整合Junit

@RunWith

名称 @RunWith
类型 测试类注解
位置 测试类定义上方
作用 设置JUnit运行器
属性 value(默认):运行所使用的运行期

@ContextConfiguration

名称 @ContextConfiguration
类型 测试类注解
位置 测试类定义上方
作用 设置JUnit加载的Spring核心配置
属性 classes:核心配置类,可以使用数组的格式设定加载多个配置类 locations:配置文件,可以使用数组的格式设定加载多个配置文件名称

AOP

@EnableAspectJAutoProxy

名称 @EnableAspectJAutoProxy
类型 配置类注解
位置 配置类定义上方
作用 开启注解格式AOP功能

@Aspect

名称 @Aspect
类型 类注解
位置 切面类定义上方
作用 设置当前类为AOP切面类

@Pointcut

名称 @Pointcut
类型 方法注解
位置 切入点方法定义上方
作用 设置切入点方法
属性 value(默认):切入点表达式

@Before

名称 @Before
类型 方法注解
位置 通知方法定义上方
作用 设置当前通知方法与切入点之间的绑定关系,当前通知方法在原始切入点方法前运行

AOP通知类型

@After

名称 @After
类型 方法注解
位置 通知方法定义上方
作用 设置当前通知方法与切入点之间的绑定关系,当前通知方法在原始切入点方法后运行

@AfterReturning

名称 @AfterReturning
类型 方法注解
位置 通知方法定义上方
作用 设置当前通知方法与切入点之间绑定关系,当前通知方法在原始切入点方法正常执行完毕后执行

@AfterThrowing

名称 @AfterThrowing
类型 方法注解
位置 通知方法定义上方
作用 设置当前通知方法与切入点之间绑定关系,当前通知方法在原始切入点方法运行抛出异常后执行

@Around

名称 @Around
类型 方法注解
位置 通知方法定义上方
作用 设置当前通知方法与切入点之间的绑定关系,当前通知方法在原始切入点方法前后运行

AOP事务管理

@EnableTransactionManagement

名称 @EnableTransactionManagement
类型 配置类注解
位置 配置类定义上方
作用 设置当前Spring环境中开启注解式事务支持

@Transactional

名称 @Transactional
类型 接口注解 类注解 方法注解
位置 业务层接口上方 业务层实现类上方 业务方法上方
作用 为当前业务层方法添加事务(如果设置在类或接口上方则类或接口中所有方法均添加事务)

感谢

  1. Spring官网
  2. 黑马程序员授课视频