JAVA-Spring

引言:

Java Spring框架笔记

先要看看反射&注解的详细知识。

Spring

家族图谱

第一阶段Spring Core、Spring Security、Spring Data,实现将单体应用开发服务好。不仅仅提供的便捷的数据库访问,web 中SpringMVC等必要功能。使用IOC、AOP实现应用低耦合、可扩展。

1567131737917

利用工厂模式(DI)和代理模式(AOP)来解耦应用组件,然后实现了web应用的框架(SpringMVC);

第二阶段推出的SpringBoot不仅仅提高了开发效率,而且将程序由可用变为好用。

1567132198944

第三阶段的Spring Cloud,推动了微服务架构的落地

1567132290549

第四阶段:Spring Cloud DataFlow

DataFlow将实时消息的处理任务和临时运行的任务都作为组件处理,定义这样组件的交互。

1567132348627

SpringIOC

概述

1567132527700

IOC是一种思想,使我们从繁琐的对象交互中解脱出来,进而专注于对象本身,更进一步了解面向对对象。

一般的设计思路:

1567132665135

先设计轮子,根据轮子设计底盘,根据底盘设计箱体…

1567132802864

这里每个类的构造函数都直接调用了底层代码的构造函数,这样底层需求变动时会影响整个上层的代码,是不可取的。

依赖注入的思路

1567133056969

1567133089064

其思想是反转资源获取的方向。传统的资源查找方式要求组件向容器发起请求查找资源。作为回应,容器适时的返回资源。而应用了 IOC 之后,则是容器主动地将资源推送给它所管理的组件,组件所要做的仅是选择一种合适的方式来接受资源。这种行为也被称为查找的被动形式

1567133141092

DI(Dependency Injection)— IOC 的另一种表述方式:即组件以一些预先定义好的方式接受来自如容器的资源注入。

  • set:实现特定属性的public set()方法,让IOC容器注入所依赖类型的对象
  • 接口:实现特定接口,让IOC容器注入所依赖类型的对象
  • 构造函数:实现特定参数的构造函数,实现在创建对象时让IOC容器注入所依赖类型的对象
  • 注解:通过Java的注解机制,让IOC容器注入所依赖类型的对象

依赖倒置原则、IOC、DI、IOC容器

什么是依赖倒置原则?

假设我们设计行李箱:先设计轮子,然后根据轮子大小设计底盘,接着根据底盘设计箱体,最后根据箱体设计好整个箱子。这里就出现了一个“依赖”关系:箱子依赖箱体,箱体依赖底盘,底盘依赖轮子。

1567133889794

上面的依赖关系是该原则所反对的,该原则的思想为高层模块不应该依赖与底层模块,两者都应该依赖于其抽象。

依赖倒置原则思想的指导才有了IOC的思路,有了IOC的思路则需要DI方法的支撑。Spring框架基于IOC才提出了容器的概念,容器管理着Bean的生命周期,控制着Bean的依赖注入。

控制反转(Inversion of Control) 就是依赖倒置原则的一种代码设计的思路。具体采用的方法就是所谓的依赖注入(Dependency Injection)。这几种概念的关系大概如下:

img

那什么是控制反转容器(IoC Container)呢?对行李箱类进行初始化的那段代码发生的地方,就是控制反转容器。

1567134047279

显然你也应该观察到了,因为采用了依赖注入,在初始化的过程中就不可避免的会写大量的new。这里IoC容器就解决了这个问题。这个容器可以自动对你的代码进行初始化,你只需要维护一个Configuration(可以是xml可以是一段代码),而不用每次初始化行李箱都要亲手去写那一大段初始化的代码。这是引入IoC Container的第一个好处。

IOC的优势:

1567134582679

而IoC Container在进行这个工作的时候是反过来的,它先从最上层开始往下找依赖关系,到达最底层之后再往上一步一步new(有点像深度优先遍历):

1567134623398

我们就像是工厂的客户。我们只需要向工厂请求一个Luggage实例,然后它就给我们按照Config创建了一个Luggage实例。我们完全不用管这个Luggage实例是怎么一步一步被创建出来。

IOC容器

容器:Spring 提供了两种类型的 IOC 容器实现.

  • BeanFactory:IOC 容器的基本实现。
  • ApplicationContext:提供了更多的高级特性。是 BeanFactory 的子接口(都用这个)

实际Spring IOC容器是怎么实现对象的创建和依赖的:

1567135465808

1、Spring启动时读取应用程序提供的Bean配置信息,并在容器生成一份相应的Bean配置注册表

2、根据注册表加载、实例化bean、建立Bean与Bean之间的依赖关系

这里利用的是Java语言的反射功能实例化bean、并建立Bean之间的依赖关系

3、将这些准备就绪的Bean放到Map缓存池中,为上层提供就绪的运行环境,等待应用程序执行调用

1567135556369

1567135577177

Spring 作者设计这两个核心接口用以表示容器

相关接口:

1567134859951

Spring容器在启动时会将xml或者注解里的Bean的定义解析为Spring内部的BeanDefinition

1567134968136

Spring 将Bean的定义解析为BeanDefinition后会通过BeanDefinitionRegistry 以BeanName为key,BeanDefinition为value存储到BeanDefinitionMap (这是个ConcurrentHashMap类型的map结构)中。

核心接口

BeanFactory接口

1567135018588

包含Bean的各种定义,以便在接收客户端请求时可以实例化Bean,并在实例化对象时建立Bean 之间的依赖关系,这将使Bean从Bean客户端中解放出来。

BeanFactory源码:

1567137963853

各种getBean()方法,可以看到可以从Spring中按类型 / 按名称获取Bean

1567138042701

判断是否为单例方法,SpringIOC中,默认Bean都是以单例存在的

1567138130281

与上面相反,判断为多例的

ApplicationContext接口

1567138269117

1567138332244

3表示可以管理message,4表示可以发布实事件给监听器,实现监听

可以看出这不单单是个工厂,是整个应用上下文,代表整个大容器的所有功能

1567138744314

SpringBoot的自带启动类的run()方法,深入进去其最终会执行createApplicationContext()方法,会发现其会用Class.forName加载AnnotationConfigServleWebServerApplicationContext类。

案例分析

案例一:

装载对象

  1. 建Category类
  2. 在src目录下新建applicationContext.xml文件
    applicationContext.xml是Spring的核心配置文件,通过关键字c即可获取Category对象,该对象获取的时候,即被注入了字符串”category 1“到name属性中
1
2
3
<bean name="c" class="pojo.Category">
<property name="name" value="category 1" />
</bean>
  1. 测试
    演示通过spring获取Category对象,以及该对象被注入的name属性。
    可以打印出通过Spring拿到的Category对象的name属性
1
2
3
4
5
6
7
8
9
10
public class TestSpring {
public static void main(String[] args) {
ApplicationContext context = new ClassPathXmlApplicationContext(
new String[] { "applicationContext.xml" });

Category c = (Category) context.getBean("c");

System.out.println(c.getName());
}
}

注入对象

上文对Category的name属性注入了”category 1”字符串
下面对Product对象,注入一个Category对象

  1. Product类
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 class Product {
private int id;
private String name;
private Category category;
public int getId() {
return id;
}
public void setId(int id) {
this.id = id;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
// 对Category对象的setter getter
public Category getCategory() {
return category;
}
public void setCategory(Category category) {
this.category = category;
}
}
  1. applicationContext.xml 配置
1
2
3
4
<bean name="p" class="com.how2java.pojo.Product">
<property name="name" value="product1" />
<property name="category" ref="c" />
</bean>
  1. 测试
1
2
3
4
5
6
7
8
public class TestSpring {
public static void main(String[] args) {
ApplicationContext context = new ClassPathXmlApplicationContext(new String[] { "applicationContext.xml" });
Product p = (Product) context.getBean("p");
System.out.println(p.getName());
System.out.println(p.getCategory().getName());
}
}
案例二:注解方式

注入对象行为的注解案例:

1
2
3
4
5
6
7
<context:annotation-config/>
<bean name="c" class="com.how2java.pojo.Category">
<property name="name" value="category 1" />
</bean>
<bean name="p" class="com.how2java.pojo.Product">
<property name="name" value="product1" />
</bean>

@Autowired注解

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
package com.how2java.pojo;

import org.springframework.beans.factory.annotation.Autowired;

public class Product {
private int id;
private String name;

@Autowired
private Category category;

public int getId() { return id; }

public void setId(int id) { this.id = id; }

public String getName() { return name; }

public void setName(String name) { this.name = name; }

public Category getCategory() { return category; }

public void setCategory(Category category) { this.category = category; }
}

测试

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
package com.how2java.test;

import org.springframework.context.ApplicationContext;
import org.springframework.context.support.ClassPathXmlApplicationContext;

import com.how2java.pojo.Product;

public class TestSpring {
public static void main(String[] args) {
ApplicationContext context = new ClassPathXmlApplicationContext(new String[] { "applicationContext.xml" });
Product p = (Product) context.getBean("p");
System.out.println(p.getName());
System.out.println(p.getCategory().getName());
}
}

对Bean进行注解

1
<context:component-scan base-package="com.how2java.pojo"/>
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
package com.how2java.pojo;

import javax.annotation.Resource;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;

@Component("p")
public class Product {

private int id;
private String name="product 1";

@Autowired
private Category category;

public int getId() { return id; }

public void setId(int id) { this.id = id; }

public String getName() { return name; }

public void setName(String name) { this.name = name; }

public Category getCategory() { return category; }

public void setCategory(Category category) { this.category = category; }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
package com.how2java.pojo;

import org.springframework.stereotype.Component;

@Component("c")
public class Category {

public int getId() { return id; }
public void setId(int id) { this.id = id; }
public String getName() { return name; }
public void setName(String name) { this.name = name; }

private int id;
private String name="category 1";
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
package com.how2java.test;

import org.springframework.context.ApplicationContext;
import org.springframework.context.support.ClassPathXmlApplicationContext;

import com.how2java.pojo.Product;

public class TestSpring {

public static void main(String[] args) {
ApplicationContext context = new ClassPathXmlApplicationContext(new String[] { "applicationContext.xml" });
Product p = (Product) context.getBean("p");
System.out.println(p.getName());
System.out.println(p.getCategory().getName());
}
}
Bean装载案例:

Person Bean:

1567139561022

扫描装配Bean,使用注解@Component(指定要扫描的类进入IOC容器中)@Value()是赋值

启动类:

1567139336948

@SpringBootApplication注解包含了启动扫描的功能。

Bean依赖注入案例:

1567139966763

装载Dog 类进入IOC容器

1567139981229

注入Pet(Pet是个接口),@Autowired会根据属性类型找到对应的Bean进行注入(最基本是用getBean()方法根据类型注入),这里的Dog类是Pet的一种实现,所以会根据类型查找到Dog,由SpringIOC容器将Dog的实例注入Person中。

当有多个Pet的实现时(例如还有Cat、Bird类),需要在要注入的类上加上@Primary注解。

getBean()解析

会调用doGetBean()放法;

1567141357464

1567141421420

1、先获取Bean名称beanName;2、获取一个(共享的)实例;3、试着从缓存或者实例工厂中获取实例;

1567141491889

Bean 的作用域

1567141574312

Bean 的生命周期

1567141612844

1、实例化Bean对象以及设置属性

2、 对 Aware 接口的实现,目的是在Bean中设置对IOC容器的感知

3、Bean的前置初始化方法,对Spring容器完成实例化的Bean添加自定义的处理逻辑

4、… 5、…

6、Bena的后置初始化方法,Bean实例初始化后的自定义工作,3、6和AOP相关

1567141729918

SpringAOP

1567142579843

分类与合并(织入)

1567142610000

Spring 采用的方式不需要特殊的Java编译器,但性能开销多一些

1567143238161

1567143257267

1567143313889

1567143353134

1567144837023

Java动态代理

所谓静态代理,其实质是自己手写(或者用工具生成)代理类,也就是在程序运行前就已经存在的编译好的代理类。

  • 但是,如果我们需要很多的代理,每一个都这么去创建实属浪费时间,而且会有大量的重复代码,

动态代理可以在程序运行期间根据需要动态的创建代理类及其实例来完成具体的功能

总的来说,根据代理类的创建时机和创建方式的不同,我们可以将代理分为静态代理和动态代理两种形式

  • 代理对象存在的价值主要用于拦截对真实业务对象的访问;
  • 代理对象应该具有和目标对象(真实业务对象)相同的方法,即实现共同的接口或继承于同一个类;
  • 代理对象应该是目标对象的增强,否则我们就没有必要使用代理了。

事实上,真正的业务功能还是由目标类来实现,代理类只是用于扩展、增强目标类的行为。例如,在项目开发中我们没有加入缓冲、日志这些功能而后期需要加入,我们就可以使用代理来实现,而没有必要去直接修改已经封装好的目标类。

应用

AOP 专门用于处理系统中分布于各个模块(不同方法)中的交叉关注点的问题,在 Java EE 应用中,常常通过 AOP 来处理一些具有横切性质的系统级服务,如事务管理、安全检查、缓存、对象池管理等,AOP 已经成为一种非常常用的解决方案。

面向切面编程,通过预编译方式和运行期动态代理实现程序功能的统一维护的一种技术。AOP是OOP的延续,是软件开发中的一个热点,也是Spring框架中的一个重要内容,是函数式编程的一种衍生范型。利用AOP可以对业务逻辑的各个部分进行隔离,从而使得业务逻辑各部分之间的耦合度降低,提高程序的可重用性,同时提高了开发的效率。

AOP机制是 Spring 所提供的核心功能之一,其既是Java动态代理机制的经典应用,也是动态AOP实现的代表。Spring AOP默认使用Java动态代理来创建AOP代理,具体通过以下几个步骤来完成:

  • Spring IOC 容器创建Bean(目标类对象);

  • Bean创建完成后,Bean后处理器(BeanPostProcessor)根据具体的切面逻辑及Bean本身使用Java动态代理技术生成代理对象;

  • 应用程序使用上述生成的代理对象替代原对象来完成业务逻辑,从而达到增强处理的目的。

要实现动态代理必须要有接口的,动态代理是基于接口来代理的(实现接口的所有方法),如果没有接口的话我们可以考虑cglib代理。cglib代理也叫子类代理,从内存中构建出一个子类来扩展目标对象的功能

Spring DAO模块

Spring的DAO模块对JDBC的支持,以及Spring对事务的控制

回顾对模版代码优化过程

我们来回忆一下我们怎么对模板代码进行优化的!

  • 首先来看一下我们原生的JDBC:需要手动去数据库的驱动从而拿到对应的连接..
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
try {
String sql = "insert into t_dept(deptName) values('test');";
Connection con = null;
Statement stmt = null;
Class.forName("com.mysql.jdbc.Driver");
// 连接对象
con = DriverManager.getConnection("jdbc:mysql:///hib_demo", "root", "root");
// 执行命令对象
stmt = con.createStatement();
// 执行
stmt.execute(sql);

// 关闭
stmt.close();
con.close();
} catch (Exception e) {
e.printStackTrace();
}
  • 因为JDBC是面向接口编程的,因此数据库的驱动都是由数据库的厂商给做到好了,我们只要加载对应的数据库驱动,便可以获取对应的数据库连接….因此,我们写了一个工具类,专门来获取与数据库的连接(Connection),当然啦,为了更加灵活,我们的工具类是读取配置文件的方式来做的
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
/*
* 连接数据库的driver,url,username,password通过配置文件来配置,可以增加灵活性
* 当我们需要切换数据库的时候,只需要在配置文件中改以上的信息即可
* */

private static String driver = null;
private static String url = null;
private static String username = null;
private static String password = null;
static {
try {
//获取配置文件的读入流
InputStream inputStream = UtilsDemo.class.getClassLoader().getResourceAsStream("db.properties");

Properties properties = new Properties();
properties.load(inputStream);

//获取配置文件的信息
driver = properties.getProperty("driver");
url = properties.getProperty("url");
username = properties.getProperty("username");
password = properties.getProperty("password");

//加载驱动类
Class.forName(driver);
} catch (IOException e) {
e.printStackTrace();
} catch (ClassNotFoundException e) {
e.printStackTrace();
}
}

public static Connection getConnection() throws SQLException {
return DriverManager.getConnection(url,username,password);
}
public static void release(Connection connection, Statement statement, ResultSet resultSet) {
if (resultSet != null) {
try {
resultSet.close();
} catch (SQLException e) {
e.printStackTrace();
}
}
if (statement != null) {
try {
statement.close();
} catch (SQLException e) {
e.printStackTrace();
}
}
if (connection != null) {
try {
connection.close();
} catch (SQLException e) {
e.printStackTrace();
}
}
}
  • 经过上面一层的封装,我们可以在使用的地方直接使用工具类来得到与数据库的连接…那么比原来就方便很多了!但是呢,每次还是需要使用Connection去创建一个Statement对象。并且无论是什么方法,其实就是SQL语句和传递进来的参数不同!
  • 于是,我们就自定义了一个JDBC的工具类,详情可以看http://blog.csdn.net/hon_3y/article/details/53760782#t6
  • 我们自定义的工具类其实就是以DbUtils组件为模板来写的,因此我们在开发的时候就一直使用DbUtils组件了

使用Spring的JDBC

上面已经回顾了一下以前我们的JDBC开发了,那么看看Spring对JDBC又是怎么优化的

首先,想要使用Spring的JDBC模块,就必须引入两个jar文件:

  • 引入jar文件
    • spring-jdbc-3.2.5.RELEASE.jar
    • spring-tx-3.2.5.RELEASE.jar
  • 首先还是看一下我们原生的JDBC代码:获取Connection是可以抽取出来的,直接使用dataSource来得到Connection就行了
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
public void save() {
try {
String sql = "insert into t_dept(deptName) values('test');";
Connection con = null;
Statement stmt = null;
Class.forName("com.mysql.jdbc.Driver");
// 连接对象
con = DriverManager.getConnection("jdbc:mysql:///hib_demo", "root", "root");
// 执行命令对象
stmt = con.createStatement();
// 执行
stmt.execute(sql);
// 关闭
stmt.close();
con.close();
} catch (Exception e) {
e.printStackTrace();
}
}
  • 值得注意的是,JDBC对C3P0数据库连接池是有很好的支持的。因此我们直接可以使用Spring的依赖注入,在配置文件中配置dataSource就行了
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
<bean id="dataSource" class="com.mchange.v2.c3p0.ComboPooledDataSource">
<property name="driverClass" value="com.mysql.jdbc.Driver"></property>
<property name="jdbcUrl" value="jdbc:mysql:///hib_demo"></property>
<property name="user" value="root"></property>
<property name="password" value="root"></property>
<property name="initialPoolSize" value="3"></property>
<property name="maxPoolSize" value="10"></property>
<property name="maxStatements" value="100"></property>
<property name="acquireIncrement" value="2"></property>
</bean>
// IOC容器注入
private DataSource dataSource;
public void setDataSource(DataSource dataSource) {
this.dataSource = dataSource;
}
public void save() {
try {
String sql = "insert into t_dept(deptName) values('test');";
Connection con = null;
Statement stmt = null;
// 连接对象
con = dataSource.getConnection();
// 执行命令对象
stmt = con.createStatement();
// 执行
stmt.execute(sql);
// 关闭
stmt.close();
con.close();
} catch (Exception e) {
e.printStackTrace();
}
}
  • Spring来提供了JdbcTemplate这么一个类给我们使用!它封装了DataSource,也就是说我们可以在Dao中使用JdbcTemplate就行了。
  • 创建dataSource,创建jdbcTemplate对象
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
<?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"
xmlns:c="http://www.springframework.org/schema/c"
xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context.xsd">
<bean id="dataSource" class="com.mchange.v2.c3p0.ComboPooledDataSource">
<property name="driverClass" value="com.mysql.jdbc.Driver"></property>
<property name="jdbcUrl" value="jdbc:mysql:///zhongfucheng"></property>
<property name="user" value="root"></property>
<property name="password" value="root"></property>
<property name="initialPoolSize" value="3"></property>
<property name="maxPoolSize" value="10"></property>
<property name="maxStatements" value="100"></property>
<property name="acquireIncrement" value="2"></property>
</bean>
<!--扫描注解-->
<context:component-scan base-package="bb"/>
<!-- 2. 创建JdbcTemplate对象 -->
<bean id="jdbcTemplate" class="org.springframework.jdbc.core.JdbcTemplate">
<property name="dataSource" ref="dataSource"></property>
</bean>
</beans>
  • userDao
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
package bb;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.stereotype.Component;

@Component
public class UserDao implements IUser {
//使用Spring的自动装配
@Autowired
private JdbcTemplate template;
@Override
public void save() {
String sql = "insert into user(name,password) values('zhoggucheng','123')";
template.update(sql);
}
}
  • 测试:
1
2
3
4
5
6
@Test
public void test33() {
ApplicationContext ac = new ClassPathXmlApplicationContext("bb/bean.xml");
UserDao userDao = (UserDao) ac.getBean("userDao");
userDao.save();
}

这里写图片描述


JdbcTemplate查询

我们要是使用JdbcTemplate查询会发现有很多重载了query()方法

这里写图片描述

一般地,如果我们使用queryForMap(),那么只能封装一行的数据,如果封装多行的数据、那么就会报错!并且,Spring是不知道我们想把一行数据封装成是什么样的,因此返回值是Map集合…我们得到Map集合的话还需要我们自己去转换成自己需要的类型。


我们一般使用下面这个方法:

这里写图片描述

我们可以实现RowMapper,告诉Spring我们将每行记录封装成怎么样的

1
2
3
4
5
6
7
8
9
10
11
12
13
14
public void query(String id) {
String sql = "select * from USER where password=?";
List<User> query = template.query(sql, new RowMapper<User>() {
//将每行记录封装成User对象
@Override
public User mapRow(ResultSet resultSet, int i) throws SQLException {
User user = new User();
user.setName(resultSet.getString("name"));
user.setPassword(resultSet.getString("password"));
return user;
}
},id);
System.out.println(query);
}

这里写图片描述


当然了,一般我们都是将每行记录封装成一个JavaBean对象的,因此直接实现RowMapper,在使用的时候创建就好了

1
2
3
4
5
6
7
8
9
10
class MyResult implements RowMapper<Dept>{
// 如何封装一行记录
@Override
public Dept mapRow(ResultSet rs, int index) throws SQLException {
Dept dept = new Dept();
dept.setDeptId(rs.getInt("deptId"));
dept.setDeptName(rs.getString("deptName"));
return dept;
}
}

事务控制概述

下面主要讲解Spring的事务控制,如何使用Spring来对程序进行事务控制….

  • Spring的事务控制是属于Spring Dao模块的

一般地,我们事务控制都是在service层做的。。为什么是在service层而不是在dao层呢??有没有这样的疑问…

service层是业务逻辑层,service的方法一旦执行成功,那么说明该功能没有出错

一个service方法可能要调用dao层的多个方法,如果在dao层做事务控制的话,一个dao方法出错了,仅仅把事务回滚到当前dao的功能,这样是不合适的[因为我们的业务由多个dao方法组成]。如果没有出错,调用完dao方法就commit了事务,这也是不合适的[导致太多的commit操作]。

事务控制分为两种:

  • 编程式事务控制
  • 声明式事务控制

编程式事务控制

自己手动控制事务,就叫做编程式事务控制。

  • Jdbc代码:
    • Conn.setAutoCommite(false); // 设置手动控制事务
  • Hibernate代码:
    • Session.beginTransaction(); // 开启一个事务
  • 【细粒度的事务控制: 可以对指定的方法、指定的方法的某几行添加事务控制】
  • (比较灵活,但开发起来比较繁琐: 每次都要开启、提交、回滚.)

声明式事务控制

Spring提供对事务的控制管理就叫做声明式事务控制

Spring提供了对事务控制的实现。

  • 如果用户想要使用Spring的事务控制,只需要配置就行了
  • 当不用Spring事务的时候,直接移除就行了。
  • Spring的事务控制是基于AOP实现的。因此它的耦合度是非常低的。
  • 【粗粒度的事务控制: 只能给整个方法应用事务,不可以对方法的某几行应用事务。
    • (因为aop拦截的是方法。)

Spring给我们提供了事务的管理器类,事务管理器类又分为两种,因为JDBC的事务和Hibernate的事务是不一样的

  • Spring声明式事务管理器类:
    • Jdbc技术:DataSourceTransactionManager
    • Hibernate技术:HibernateTransactionManager

声明式事务控制

我们基于Spring的JDBC来做例子吧

引入相关jar包

  • AOP相关的jar包【因为Spring的声明式事务控制是基于AOP的,那么就需要引入AOP的jar包。】
  • 引入tx名称空间
  • 引入AOP名称空间
  • 引入jdbcjar包【jdbc.jar包和tx.jar包】

搭建配置环境

  • 编写一个接口
1
2
3
public interface IUser {
void save();
}
  • UserDao实现类,使用JdbcTemplate对数据库进行操作!
1
2
3
4
5
6
7
8
9
10
11
@Repository
public class UserDao implements IUser {
//使用Spring的自动装配
@Autowired
private JdbcTemplate template;
@Override
public void save() {
String sql = "insert into user(name,password) values('zhong','222')";
template.update(sql);
}
}
  • userService
1
2
3
4
5
6
7
8
@Service
public class UserService {
@Autowired
private UserDao userDao;
public void save() {
userDao.save();
}
}
  • bean.xml配置:配置数据库连接池、jdbcTemplate对象、扫描注解
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
<?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"
xmlns:c="http://www.springframework.org/schema/c"
xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context.xsd">
<!--数据连接池配置-->
<bean id="dataSource" class="com.mchange.v2.c3p0.ComboPooledDataSource">
<property name="driverClass" value="com.mysql.jdbc.Driver"></property>
<property name="jdbcUrl" value="jdbc:mysql:///zhongfucheng"></property>
<property name="user" value="root"></property>
<property name="password" value="root"></property>
<property name="initialPoolSize" value="3"></property>
<property name="maxPoolSize" value="10"></property>
<property name="maxStatements" value="100"></property>
<property name="acquireIncrement" value="2"></property>
</bean>
<!--扫描注解-->
<context:component-scan base-package="bb"/>
<!-- 2. 创建JdbcTemplate对象 -->
<bean id="jdbcTemplate" class="org.springframework.jdbc.core.JdbcTemplate">
<property name="dataSource" ref="dataSource"></property>
</bean>
</beans>

前面搭建环境的的时候,是没有任何的事务控制的。

也就是说,当我在service中调用两次userDao.save(),即时在中途中有异常抛出,还是可以在数据库插入一条记录的

  • Service代码:
1
2
3
4
5
6
7
8
9
10
@Service
public class UserService {
@Autowired
private UserDao userDao;
public void save() {
userDao.save();
int i = 1 / 0;
userDao.save();
}
}
  • 测试代码:
1
2
3
4
5
6
7
8
public class Test2 {
@Test
public void test33() {
ApplicationContext ac = new ClassPathXmlApplicationContext("bb/bean.xml");
UserService userService = (UserService) ac.getBean("userService");
userService.save();
}
}

这里写图片描述


XML方式实现声明式事务控制

首先,我们要配置事务的管理器类:因为JDBC和Hibernate的事务控制是不同的。

1
2
3
4
5
<!--1.配置事务的管理器类:JDBC-->
<bean id="txManage" class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
<!--引用数据库连接池-->
<property name="dataSource" ref="dataSource"/>
</bean>

再而,配置事务管理器类如何管理事务

1
2
3
4
5
6
7
8
<!--2.配置如何管理事务-->
<tx:advice id="txAdvice" transaction-manager="txManage">
<!--配置事务的属性-->
<tx:attributes>
<!--所有的方法,并不是只读-->
<tx:method name="*" read-only="false"/>
</tx:attributes>
</tx:advice>

最后,配置拦截哪些方法,

1
2
3
4
5
<!--3.配置拦截哪些方法+事务的属性-->
<aop:config>
<aop:pointcut id="pt" expression="execution(* bb.UserService.*(..) )"/>
<aop:advisor advice-ref="txAdvice" pointcut-ref="pt"></aop:advisor>
</aop:config>

配置完成之后,service中的方法都应该被Spring的声明式事务控制了。因此我们再次测试一下:

1
2
3
4
5
6
@Test
public void test33() {
ApplicationContext ac = new ClassPathXmlApplicationContext("bb/bean.xml");
UserService userService = (UserService) ac.getBean("userService");
userService.save();
}

这里写图片描述


使用注解的方法实现事务控制

当然了,有的人可能觉得到XML文件上配置太多东西了。Spring也提供了使用注解的方式来实现对事务控制

第一步和XML的是一样的,必须配置事务管理器类:

1
2
3
4
5
<!--1.配置事务的管理器类:JDBC-->
<bean id="txManage" class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
<!--引用数据库连接池-->
<property name="dataSource" ref="dataSource"/>
</bean>

第二步:开启以注解的方式来实现事务控制

1
2
<!--开启以注解的方式实现事务控制-->
<tx:annotation-driven transaction-manager="txManage"/>

最后,想要控制哪个方法事务,在其前面添加@Transactional这个注解就行了!如果想要控制整个类的事务,那么在类上面添加就行了。

1
2
3
4
5
6
@Transactional
public void save() {
userDao.save();
int i = 1 / 0;
userDao.save();
}

这里写图片描述


事务属性

其实我们在XML配置管理器类如何管理事务,就是在指定事务的属性!我们来看一下事务的属性有什么:

这里写图片描述

对于事务的隔离级别,不清楚的朋友可参考之前的博文:http://blog.csdn.net/hon_3y/article/details/53760782

事务传播行为

看了上面的事务属性,没有接触过的其实就这么一个:propagation = Propagation.REQUIRED事务的传播行为。

事务传播行为的属性有以下这么多个,常用的就只有两个:

  • Propagation.REQUIRED【如果当前方法已经有事务了,加入当前方法事务
  • Propagation.REQUIRED_NEW【如果当前方法有事务了,当前方法事务会挂起。始终开启一个新的事务,直到新的事务执行完、当前方法的事务才开始】

这里写图片描述

当事务传播行为是Propagation.REQUIRED

  • 现在有一个日志类,它的事务传播行为是Propagation.REQUIRED

1
2
3
4
Class Log{
Propagation.REQUIRED
insertLog();
}
  • 现在,我要在保存之前记录日志
1
2
3
4
5
Propagation.REQUIRED
Void saveDept(){
insertLog();
saveDept();
}

saveDept()本身就存在着一个事务,当调用insertLog()的时候,insertLog()的事务会加入到saveDept()事务中

也就是说,saveDept()方法内始终是一个事务,如果在途中出现了异常,那么insertLog()的数据是会被回滚的【因为在同一事务内】

1
2
3
4
5
Void  saveDept(){
insertLog(); // 加入当前事务
.. 异常, 会回滚
saveDept();
}

当事务传播行为是Propagation.REQUIRED_NEW

  • 现在有一个日志类,它的事务传播行为是Propagation.REQUIRED_NEW
1
2
3
4
Class Log{
Propagation.REQUIRED
insertLog();
}
  • 现在,我要在保存之前记录日志
1
2
3
4
5
Propagation.REQUIRED
Void saveDept(){
insertLog();
saveDept();
}

当执行到saveDept()中的insertLog()方法时,insertLog()方法发现 saveDept()已经存在事务了,insertLog()会独自新开一个事务,直到事务关闭之后,再执行下面的方法

如果在中途中抛出了异常,insertLog()是不会回滚的,因为它的事务是自己的,已经提交了

1
2
3
4
5
Void  saveDept(){
insertLog(); // 始终开启事务
.. 异常, 日志不会回滚
saveDept();
}

Spring IOC总结

结合《Spring 实战 (第4版)》和《精通Spring4.x 企业应用开发实战》两本书的IOC章节将其知识点整理起来~

IOC和DI概述

在《精通Spring4.x 企业应用开发实战》中对IOC的定义是这样的:

IoC(Inversion of Control)控制反转,包含了两个方面:一、控制。二、反转

我们可以简单认为:

  • 控制指的是:当前对象对内部成员的控制权
  • 反转指的是:这种控制权不由当前对象管理了,由其他(类,第三方容器)来管理。

IOC不够开门见山,于是Martin Fowler提出了DI(dependency injection)来替代IoC,即让调用类对某一接口实现类的依赖关系由第三方(容器或协作类)注入,以移除调用类对某一接口实现类的依赖。

在《Spring 实战 (第4版)》中并没有提及到IOC,而是直接来说DI的:

通过DI,对象的依赖关系将由系统中负责协调各对象的第三方组件在创建对象的时候进行设定,对象无需自行创建或管理它们的依赖关系,依赖关系将被自动注入到需要它们的对象当中去

从书上我们也可以发现:IoC和DI的定义(区别)并不是如此容易就可以说得清楚的了。这里我就简单摘抄一下:

  • IoC(思想,设计模式)主要的实现方式有两种:依赖查找,依赖注入
  • 依赖注入是一种更可取的方式(实现的方式)

对我们而言,其实也没必要分得那么清,混合一谈也不影响我们的理解…

再通过昨天写过的工厂模式理解了没有?,我们现在就可以很清楚的发现,其实所谓的IOC容器就是一个大工厂【第三方容器】(Spring实现的功能很强大!比我们自己手写的工厂要好很多)。

使用IOC的好处(知乎@Intopass的回答):

  1. 不用自己组装,拿来就用。
  2. 享受单例的好处,效率高,不浪费空间。
  3. 便于单元测试,方便切换mock组件。
  4. 便于进行AOP操作,对于使用者是透明的。
  5. 统一配置,便于修改。

参考资料:

IOC容器的原理

从上面就已经说了:IOC容器其实就是一个大工厂,它用来管理我们所有的对象以及依赖关系。

  • 原理就是通过Java的反射技术来实现的!通过反射我们可以获取类的所有信息(成员变量、类名等等等)!
  • 再通过配置文件(xml)或者注解来描述类与类之间的关系
  • 我们就可以通过这些配置信息和反射技术来构建出对应的对象和依赖关系了!

上面描述的技术只要学过点Java的都能说出来,这一下子可能就会被面试官问倒了,我们简单来看看实际Spring IOC容器是怎么实现对象的创建和依赖的:

img

  1. 根据Bean配置信息在容器内部创建Bean定义注册表
  2. 根据注册表加载、实例化bean、建立Bean与Bean之间的依赖关系
  3. 将这些准备就绪的Bean放到Map缓存池中,等待应用程序调用

Spring容器(Bean工厂)可简单分成两种:

  • BeanFactory
    • 这是最基础、面向Spring的
  • ApplicationContext
    • 这是在BeanFactory基础之上,面向使用Spring框架的开发者。提供了一系列的功能!

几乎所有的应用场合都是使用ApplicationContext!

BeanFactory的继承体系:

img

ApplicationContext的继承体系:

img

其中在ApplicationContext子类中又有一个比较重要的:WebApplicationContext

  • 专门为Web应用准备的

img

Web应用与Spring融合:

img

我们看看BeanFactory的生命周期:

img

接下来我们再看看ApplicationContext的生命周期:

img

初始化的过程都是比较长,我们可以分类来对其进行解析:

  • Bean自身的方法:如调用 Bean 构造函数实例化 Bean,调用 Setter 设置 Bean 的属性值以及通过的 init-method 和 destroy-method 所指定的方法;
  • Bean级生命周期接口方法:如 BeanNameAware、 BeanFactoryAware、 InitializingBean 和 DisposableBean,这些接口方法由 Bean 类直接实现;
  • 容器级生命周期接口方法:在上图中带“★” 的步骤是由 InstantiationAwareBean PostProcessor 和 BeanPostProcessor 这两个接口实现,一般称它们的实现类为“ 后处理器” 。 后处理器接口一般不由 Bean 本身实现,它们独立于 Bean,实现类以容器附加装置的形式注册到Spring容器中并通过接口反射为Spring容器预先识别。当Spring 容器创建任何 Bean 的时候,这些后处理器都会发生作用,所以这些后处理器的影响是全局性的。当然,用户可以通过合理地编写后处理器,让其仅对感兴趣Bean 进行加工处理

ApplicationContext和BeanFactory不同之处在于:

  • ApplicationContext会利用Java反射机制自动识别出配置文件中定义的BeanPostProcessor、 InstantiationAwareBeanPostProcesso 和BeanFactoryPostProcessor后置器,并自动将它们注册到应用上下文中。而BeanFactory需要在代码中通过手工调用addBeanPostProcessor()方法进行注册
  • ApplicationContext在初始化应用上下文的时候就实例化所有单实例的Bean。而BeanFactory在初始化容器的时候并未实例化Bean,直到第一次访问某个Bean时实例化目标Bean。

有了上面的知识点了,我们再来详细地看看Bean的初始化过程:

img

简要总结:

  • BeanDefinitionReader读取Resource所指向的配置文件资源,然后解析配置文件。配置文件中每一个<bean>解析成一个BeanDefinition对象,并保存到BeanDefinitionRegistry中;
  • 容器扫描BeanDefinitionRegistry中的BeanDefinition;调用InstantiationStrategy进行Bean实例化的工作;使用BeanWrapper完成Bean属性的设置工作;
  • 单例Bean缓存池:Spring 在DefaultSingletonBeanRegistry类中提供了一个用于缓存单实例 Bean 的缓存器,它是一个用HashMap实现的缓存器,单实例的Bean以beanName为键保存在这个HashMap中。

IOC容器装配Bean

装配Bean方式

Spring4.x开始IOC容器装配Bean有4种方式:

  • XML配置
  • 注解
  • JavaConfig
  • 基于Groovy DSL配置(这种很少见)

总的来说:我们以XML配置+注解来装配Bean得多,其中注解这种方式占大部分

依赖注入方式

依赖注入的方式有3种方式:

  • 属性注入–>通过setter()方法注入
  • 构造函数注入
  • 工厂方法注入

总的来说使用属性注入是比较灵活和方便的,这是大多数人的选择!

对象之间关系

<bean>对象之间有三种关系:

  • 依赖–>挺少用的(使用depends-on就是依赖关系了–>前置依赖【依赖的Bean需要初始化之后,当前Bean才会初始化】)
  • 继承–>可能会用到(指定abstract和parent来实现继承关系)
  • 引用–>最常见(使用ref就是引用关系了)

Bean的作用域

  • 单例Singleton
  • 多例prototype
  • 与Web应用环境相关的Bean作用域
    • reqeust
    • session

使用到了Web应用环境相关的Bean作用域的话,是需要我们手动配置代理的~

img

原因也很简单:因为我们默认的Bean是单例的,为了适配Web应用环境相关的Bean作用域—>每个request都需要一个对象,此时我们返回一个代理对象出去就可以完成我们的需求了!

img


将Bean配置单例的时候还有一个问题:

  • 如果我们的Bean配置的是单例,而Bean对象里边的成员对象我们希望是多例的话。那怎么办呢??
  • 默认的情况下我们的Bean单例,返回的成员对象也默认是单例的(因为对象就只有那么一个)!

此时我们需要用到了lookup方法注入,使用也很简单,看看例子就明白了:

img

处理自动装配的歧义性

昨天在刷书的时候刚好看到了有人在知乎邀请我回答这个问题:

img

结合两本书的知识点,可以归纳成两种解决方案:

  • 使用@Primary注解设置为首选的注入Bean
  • 使用@Qualifier注解设置特定名称的Bean来限定注入!
    • 也可以使用自定义的注解来标识

引用属性文件以及Bean属性

之前在写配置文件的时候都是直接将我们的数据库配置信息在里面写死的了:

img

其实我们有更优雅的做法:将这些配置信息写到配置文件上(因为这些配置信息很可能是会变的,而且有可能被多个配置文件引用).

  • 如此一来,我们改的时候就十分方便了。

img

引用配置文件的数据使用的是${}

除了引用配置文件上的数据,我们还可以引用Bean的属性

img

img

引用Bean的属性使用的是#{}

在这种技术在《Spring 实战 第四版》称之为Spring EL,跟我们之前学过的EL表达式是类似的。主要的功能就是上面的那种,想要更深入了解可参考下面的链接:

组合配置文件

xml文件之间组合:

img

xml和javaconfig互相组合的方式:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
public static void main(String[] args) {

//1.通过构造函数加载配置类
ApplicationContext ctx = new AnnotationConfigApplicationContext(AppConf.class);

//2.通过编码方式注册配置类
AnnotationConfigApplicationContext ctx = new AnnotationConfigApplicationContext();
ctx.register(DaoConfig.class);
ctx.register(ServiceConfig.class);
ctx.refresh();

//3.通过XML组装@Configuration配置类所提供的配置信息
ApplicationContext ctx = new ClassPathXmlApplicationContext("com/smart/conf/beans2.xml");

//4.通过@Configuration组装XML配置所提供的配置信息
ApplicationContext ctx = new AnnotationConfigApplicationContext(LogonAppConfig.class);

//5.@Configuration的配置类相互引用
ApplicationContext ctx = new AnnotationConfigApplicationContext(DaoConfig.class,ServiceConfig.class);
LogonService logonService = ctx.getBean(LogonService.class);
System.out.println((logonService.getLogDao() !=null));
logonService.printHelllo();
}

第一种的例子:

img

第二种的例子:

img

img

第三种的例子:

img

第四种的例子:

img

第五种的例子:

  • 代码由上可见

装配Bean总结

总的来说,Spring IOC容器就是在创建Bean的时候有很多的方式给了我们实现,其中也包括了很多关于Bean的配置~

对于Bean相关的注入教程代码和简化配置(p和c名称空间)我就不一一说明啦,你们去看Spring入门这一篇就够了Spring【依赖注入】就是这么简单就行了。

总的对比图:

img
img

分别的应用场景:

img

至于一些小的知识点:

  • 方法替换
    • 使用某个Bean的方法替换成另一个Bean的方法
  • 属性编辑器
    • Spring可以对基本类型做转换就归结于属性编辑器的功劳!
  • 国际化
    • 使用不同语言(英语、中文)的操作系统去显式不同的语言
  • profile与条件化的Bean
    • 满足了某个条件才初始化Bean,这可以方便切换生产环境和开发环境~
  • 容器事件
    • 类似于我们的Servlet的监听器,只不过它是在Spring中实现了~

上面这些小知识点比较少情况会用到,这也不去讲解啦。知道有这么一回事,到时候查查就会用啦~

参考资料:

Spring AOP总结

结合《Spring 实战 (第4版)》和《精通Spring4.x 企业应用开发实战》两本书的AOP章节将其知识点整理起来~

AOP概述

AOP称为面向切面编程,那我们怎么理解面向切面编程??

我们可以先看看下面这段代码:

img

我们学Java面向对象的时候,如果代码重复了怎么办啊??可以分成下面几个步骤:

  • 1:抽取成方法
  • 2:抽取类

抽取成类的方式我们称之为:纵向抽取

  • 通过继承的方式实现纵向抽取

但是,我们现在的办法不行:即使抽取成类还是会出现重复的代码,因为这些逻辑(开始、结束、提交事务)依附在我们业务类的方法逻辑中

img

现在纵向抽取的方式不行了,AOP的理念:就是将分散在各个业务逻辑代码中相同的代码通过横向切割的方式抽取到一个独立的模块中!

img

上面的图也很清晰了,将重复性的逻辑代码横切出来其实很容易(我们简单可认为就是封装成一个类就好了),但我们要将这些被我们横切出来的逻辑代码融合到业务逻辑中,来完成和之前(没抽取前)一样的功能!这就是AOP首要解决的问题了!

Spring AOP原理

被我们横切出来的逻辑代码融合到业务逻辑中,来完成和之前(没抽取前)一样的功能

没有学Spring AOP之前,我们就可以使用代理来完成。

  • 如果看过我写的给女朋友讲解什么是代理模式这篇文章的话,一定就不难理解上面我说的那句话了
  • 代理能干嘛?代理可以帮我们增强对象的行为!使用动态代理实质上就是调用时拦截对象方法,对方法进行改造、增强

其实Spring AOP的底层原理就是动态代理

来源《精通Spring4.x 企业应用开发实战》一段话:

Spring AOP使用纯Java实现,它不需要专门的编译过程,也不需要特殊的类装载器,它在运行期通过代理方式向目标类织入增强代码。在Spring中可以无缝地将Spring AOP、IoC和AspectJ整合在一起。

来源《Spring 实战 (第4版)》一句话:

Spring AOP构建在动态代理基础之上,因此,Spring对AOP的支持局限于方法拦截

在Java中动态代理有两种方式:

  • JDK动态代理
  • CGLib动态代理

img

JDK动态代理是需要实现某个接口了,而我们类未必全部会有接口,于是CGLib代理就有了~~

  • CGLib代理其生成的动态代理对象是目标类的子类
  • Spring AOP默认是使用JDK动态代理,如果代理的类没有接口则会使用CGLib代理

那么JDK代理和CGLib代理我们该用哪个呢??在《精通Spring4.x 企业应用开发实战》给出了建议:

  • 如果是单例的我们最好使用CGLib代理,如果是多例的我们最好使用JDK代理

原因:

  • JDK在创建代理对象时的性能要高于CGLib代理,而生成代理对象的运行性能却比CGLib的低。
  • 如果是单例的代理,推荐使用CGLib

看到这里我们就应该知道什么是Spring AOP(面向切面编程)了:将相同逻辑的重复代码横向抽取出来,使用动态代理技术将这些重复代码织入到目标对象方法中,实现和原来一样的功能

  • 这样一来,我们就在写业务时只关心业务代码,而不用关心与业务无关的代码

AOP的实现者

AOP除了有Spring AOP实现外,还有著名的AOP实现者:AspectJ,也有可能大家没听说过的实现者:JBoss AOP~~

我们下面来说说AspectJ扩展一下知识面:

AspectJ是语言级别的AOP实现,扩展了Java语言,定义了AOP语法,能够在编译期提供横切代码的织入,所以它有专门的编译器用来生成遵守Java字节码规范的Class文件。

而Spring借鉴了AspectJ很多非常有用的做法,融合了AspectJ实现AOP的功能。但Spring AOP本质上底层还是动态代理,所以Spring AOP是不需要有专门的编辑器的~

AOP的术语

嗯,AOP搞了好几个术语出来~~两本书都有讲解这些术语,我会尽量让大家看得明白的:

连接点(Join point):

  • 能够被拦截的地方:Spring AOP是基于动态代理的,所以是方法拦截的。每个成员方法都可以称之为连接点~

切点(Poincut):

  • 具体定位的连接点:上面也说了,每个方法都可以称之为连接点,我们具体定位到某一个方法就成为切点

增强/通知(Advice):

  • 表示添加到切点的一段逻辑代码,并定位连接点的方位信息
    • 简单来说就定义了是干什么的,具体是在哪干
    • Spring AOP提供了5种Advice类型给我们:前置、后置、返回、异常、环绕给我们使用!

织入(Weaving):

  • 增强/通知添加到目标类的具体连接点上的过程。

引入/引介(Introduction):

  • 引入/引介允许我们向现有的类添加新方法或属性。是一种特殊的增强!

切面(Aspect):

  • 切面由切点和增强/通知组成,它既包括了横切逻辑的定义、也包括了连接点的定义。

在《Spring 实战 (第4版)》给出的总结是这样子的:

通知/增强包含了需要用于多个应用对象的横切行为;连接点是程序执行过程中能够应用通知的所有点;切点定义了通知/增强被应用的具体位置。其中关键的是切点定义了哪些连接点会得到通知/增强。

总的来说:

  • 这些术语可能翻译过来不太好理解,但对我们正常使用AOP的话影响并没有那么大~~看多了就知道它是什么意思了。

Spring对AOP的支持

Spring提供了3种类型的AOP支持:

  • 基于代理的经典SpringAOP
    • 需要实现接口,手动创建代理
  • 纯POJO切面
    • 使用XML配置,aop命名空间
  • @AspectJ注解驱动的切面
    • 使用注解的方式,这是最简洁和最方便的!

基于注解和命名空的AOP编程

Spring在新版本中对AOP功能进行了增强,体现在这么几个方面:

  • 在XML配置文件中为AOP提供了aop命名空间
  • 增加了AspectJ切点表达式语言的支持
  • 可以无缝地集成AspectJ

那我们使用@AspectJ来玩AOP的话,学什么??其实也就是上面的内容,学如何设置切点、创建切面、增强的内容是什么…

img

具体的切点表达式使用还是前往:Spring【AOP模块】就这么简单看吧~~

对应的增强注解:

img

img

使用引介/引入功能实现为Bean引入新方法

其实前置啊、后置啊这些很容易就理解了,整篇文章看下来就只有这个引介/引入切面有点搞头。于是我们就来玩玩吧~

我们来看一下具体的用法吧,现在我有个服务员的接口:

1
2
3
4
5
6
public interface Waiter {
// 向客人打招呼
void greetTo(String clientName);
// 服务
void serveTo(String clientName);
}

一位年轻服务员实现类:

1
2
3
4
5
6
7
8
9
public class NaiveWaiter implements Waiter {
public void greetTo(String clientName) {
System.out.println("NaiveWaiter:greet to " + clientName + "...");
}
@NeedTest
public void serveTo(String clientName) {
System.out.println("NaiveWaiter:serving " + clientName + "...");
}
}

现在我想做的就是:想这个服务员可以充当售货员的角色,可以卖东西!当然了,我肯定不会加一个卖东西的方法到Waiter接口上啦,因为这个是暂时的~

所以,我搞了一个售货员接口:

1
2
3
4
public interface Seller {
// 卖东西
int sell(String goods, String clientName);
}

一个售货员实现类:

1
2
3
4
5
6
7
8
public class SmartSeller implements Seller {
// 卖东西
public int sell(String goods,String clientName) {
System.out.println("SmartSeller: sell "+goods +" to "+clientName+"...");
return 100;
}

}

此时,我们的类图是这样子的:

img

现在我想干的就是:借助AOP的引入/引介切面,来让我们的服务员也可以卖东西

我们的引入/引介切面具体是这样干的:

1
2
3
4
5
6
@Aspect
public class EnableSellerAspect {
@DeclareParents(value = "com.smart.NaiveWaiter", // 指定服务员具体的实现
defaultImpl = SmartSeller.class) // 售货员具体的实现
public Seller seller; // 要实现的目标接口
}

写了这个切面类会发生什么??

  • 切面技术将SmartSeller融合到NaiveWaiter中,这样NaiveWaiter就实现了Seller接口!!!!

是不是很神奇??我也觉得很神奇啊,我们来测试一下:

我们的bean.xml文件很简单:

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:aop="http://www.springframework.org/schema/aop"
xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-4.0.xsd
http://www.springframework.org/schema/aop http://www.springframework.org/schema/aop/spring-aop-4.0.xsd">
<aop:aspectj-autoproxy/>
<bean id="waiter" class="com.smart.NaiveWaiter"/>
<bean class="com.smart.aspectj.basic.EnableSellerAspect"/>
</beans>

测试一下:

1
2
3
4
5
6
7
8
9
10
11
12
public class Test {
public static void main(String[] args) {
ClassPathXmlApplicationContext ctx = new ClassPathXmlApplicationContext("com/smart/aspectj/basic/beans.xml");
Waiter waiter = (Waiter) ctx.getBean("waiter");
// 调用服务员原有的方法
waiter.greetTo("Java3y");
waiter.serveTo("Java3y");
// 通过引介/引入切面已经将waiter服务员实现了Seller接口,所以可以强制转换
Seller seller = (Seller) waiter;
seller.sell("水军", "Java3y");
}
}

img

具体的调用过程是这样子的:

当引入接口方法被调用时,代理对象会把此调用委托给实现了新接口的某个其他对象。实际上,一个Bean的实现被拆分到多个类中

img

在XML中声明切面

我们知道注解很方便,但是,要想使用注解的方式使用Spring AOP就必须要有源码(因为我们要在切面类上添加注解)。如果没有源码的话,我们就得使用XML来声明切面了~

其实就跟注解差不多的功能:

img

我们就直接来个例子终结掉它吧:

首先我们来测试一下与传统的SpringAOP结合的advisor是怎么用的:

实现类:

img

xml配置文件:

img

…….

一个一个来讲解还是太花时间了,我就一次性用图的方式来讲啦:

img

最后还有一个切面类型总结图,看完就几乎懂啦:

img

总结

看起来AOP有很多很多的知识点,其实我们只要记住AOP的核心概念就行啦。

下面是我的简要总结AOP:

  • AOP的底层实际上是动态代理,动态代理分成了JDK动态代理和CGLib动态代理。如果被代理对象没有接口,那么就使用的是CGLIB代理(也可以直接配置使用CBLib代理)
  • 如果是单例的话,那我们最好使用CGLib代理,因为CGLib代理对象运行速度要比JDK的代理对象要快
  • AOP既然是基于动态代理的,那么它只能对方法进行拦截,它的层面上是方法级别的
  • 无论经典的方式、注解方式还是XML配置方式使用Spring AOP的原理都是一样的,只不过形式变了而已。一般我们使用注解的方式使用AOP就好了。
  • 注解的方式使用Spring AOP就了解几个切点表达式,几个增强/通知的注解就完事了,是不是贼简单…使用XML的方式和注解其实没有很大的区别,很快就可以上手啦。
  • 引介/引入切面也算是一个比较亮的地方,可以用代理的方式为某个对象实现接口,从而能够使用借口下的方法。这种方式是非侵入式的~
  • 要增强的方法还可以接收与被代理方法一样的参数、绑定被代理方法的返回值这些功能…

Spring 事务

  • 如果嵌套调用含有事务的方法,在Spring事务管理中,这属于哪个知识点?
  • 我们使用的框架可能是Hibernate/JPA或者是Mybatis,都知道的底层是需要一个session/connection对象来帮我们执行操作的。要保证事务的完整性,我们需要多组数据库操作要使用同一个session/connection对象,而我们又知道Spring IOC所管理的对象默认都是单例的,这为啥我们在使用的时候不会引发线程安全问题呢?内部Spring到底干了什么?
  • 人家所说的BPP又是啥东西?
  • Spring事务管理重要接口有哪几个?

基础知识

阅读这篇文章的同学我默认大家都对Spring事务相关知识有一定的了解了。(ps:如果不了解点解具体的文章去阅读再回到这里来哦)

我们都知道,Spring事务是Spring AOP的最佳实践之一,所以说AOP入门基础知识(简单配置,使用)是需要先知道的。如果想更加全面了解AOP可以看这篇文章:AOP重要知识点(术语介绍、全面使用)。说到AOP就不能不说AOP底层原理:动态代理设计模式。到这里,对AOP已经有一个基础的认识了。于是我们就可以使用XML/注解方式来配置Spring事务管理

在IOC学习中,可以知道的是Spring中Bean的生命周期(引出BPP对象)并且IOC所管理的对象默认都是单例的:单例设计模式,单例对象如果有”状态“(有成员变量),那么多线程访问这个单例对象,可能就造成线程不安全。那么何为线程安全?,解决线程安全有很多方式,但其中有一种:让每一个线程都拥有自己的一个变量:ThreadLocal

两个不靠谱直觉的例子

第一个例子

之前朋友问了我一个例子:

在Service层抛出Exception,在Controller层捕获,那如果在Service中有异常,那会事务回滚吗?

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
// Service方法
@Transactional
public Employee addEmployee() throws Exception {
Employee employee = new Employee("3y", 23);
employeeRepository.save(employee);
// 假设这里出了Exception
int i = 1 / 0;
return employee;
}
// Controller调用
@RequestMapping("/add")
public Employee addEmployee() {
Employee employee = null;
try {
employee = employeeService.addEmployee();
} catch (Exception e) {
e.printStackTrace();
}
return employee;
}

第一反应:不会回滚吧。

  • 我当时是这样想的:因为Service层已经抛出了异常,由Controller捕获。那是否回滚应该由Controller的catch代码块中逻辑来决定,如果catch代码块没有回滚,那应该是不会回滚。

但朋友经过测试说,可以回滚阿。(pappapa打脸)

发生了运行时Exception,Spring事务管理自动回滚

看了一下文档,原来文档有说明:

By default checked exceptions do not result in the transactional interceptor marking the transaction for rollback and instances of RuntimeException and its subclasses do

结论:如果是编译时异常不会自动回滚,如果是运行时异常,那会自动回滚

第二个例子

第二个例子来源于知乎@柳树文章,文末会给出相应的URL

我们都知道,带有@Transactional注解所包围的方法就能被Spring事务管理起来,那如果我在当前类下使用一个没有事务的方法去调用一个有事务的方法,那我们这次调用会怎么样?是否会有事务呢?

用代码来描述一下:

1
2
3
4
5
6
7
8
9
10
11
12
// 没有事务的方法去调用有事务的方法
public Employee addEmployee2Controller() throws Exception {
return this.addEmployee();
}
@Transactional
public Employee addEmployee() throws Exception {
employeeRepository.deleteAll();
Employee employee = new Employee("3y", 23);
// 模拟异常
int i = 1 / 0;
return employee;
}

我第一直觉是:这跟Spring事务的传播机制有关吧。

其实这跟Spring事务的传播机制没有关系,下面我讲述一下:

  • Spring事务管理用的是AOP,AOP底层用的是动态代理。所以如果我们在类或者方法上标注注解@Transactional,那么会生成一个代理对象

接下来我用图来说明一下:

Spring会自动生成代理对象

显然地,我们拿到的是代理(Proxy)对象,调用addEmployee2Controller()方法,而addEmployee2Controller()方法的逻辑是target.addEmployee(),调用回原始对象(target)的addEmployee()。所以这次的调用压根就没有事务存在,更谈不上说Spring事务传播机制了。

原有的数据:

原有的数据

测试结果:压根就没有事务的存在

没有事务的存在

再延伸一下

从上面的测试我们可以发现:如果是在本类中没有事务的方法来调用标注注解@Transactional方法,最后的结论是没有事务的。那如果我将这个标注注解的方法移到别的Service对象上,有没有事务?

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
@Service
public class TestService {
@Autowired
private EmployeeRepository employeeRepository;
@Transactional
public Employee addEmployee() throws Exception {
employeeRepository.deleteAll();
Employee employee = new Employee("3y", 23);
// 模拟异常
int i = 1 / 0;
return employee;
}
}
@Service
public class EmployeeService {
@Autowired
private TestService testService;
// 没有事务的方法去调用别的类有事务的方法
public Employee addEmployee2Controller() throws Exception {
return testService.addEmployee();
}
}

测试结果:

抛出了运行时异常,但我们的数据还是存在的!

因为我们用的是代理对象(Proxy)去调用addEmployee()方法,那就当然有事务了。

Spring事务传播机制

如果嵌套调用含有事务的方法,在Spring事务管理中,这属于哪个知识点?

在当前含有事务方法内部调用其他的方法(无论该方法是否含有事务),这就属于Spring事务传播机制的知识点范畴了。

Spring事务基于Spring AOP,Spring AOP底层用的动态代理,动态代理有两种方式:

  • 基于接口代理(JDK代理)
    • 基于接口代理,凡是类的方法非public修饰,或者用了static关键字修饰,那这些方法都不能被Spring AOP增强
  • 基于CGLib代理(子类代理)
    • 基于子类代理,凡是类的方法使用了private、static、final修饰,那这些方法都不能被Spring AOP增强

值得说明的是:那些不能被Spring AOP增强的方法并不是不能在事务环境下工作了。只要它们被外层的事务方法调用了,由于Spring事务管理的传播级别,内部方法也可以工作在外部方法所启动的事务上下文中

多线程问题

我们使用的框架可能是Hibernate/JPA或者是Mybatis,都知道的底层是需要一个session/connection对象来帮我们执行操作的。要保证事务的完整性,我们需要多组数据库操作要使用同一个session/connection对象,而我们又知道Spring IOC所管理的对象默认都是单例的,这为啥我们在使用的时候不会引发线程安全问题呢?内部Spring到底干了什么?

回想一下当年我们学Mybaits的时候,是怎么编写Session工具类

Mybatis工具类部分代码截图

没错,用的就是ThreadLocal,同样地,Spring也是用的ThreadLocal。

以下内容来源《精通 Spring4.x》

我们知道在一般情况下,只有无状态的Bean才可以在多线程环境下共享,在Spring中,绝大部分Bean都可以声明为singleton作用域。就是因为Spring对一些Bean(如RequestContextHolder、TransactionSynchronizationManager、LocaleContextHolder等)中非线程安全状态的“状态性对象”采用ThreadLocal封装,让它们也成为线程安全的“状态性对象”,因此,有状态的Bean就能够以singleton的方式在多线程中工作。

我们可以试着点一下进去TransactionSynchronizationManager中看一下:

全都是ThreadLocal

啥是BPP?

BBP的全称叫做:BeanPostProcessor,一般我们俗称对象后处理器

  • 简单来说,通过BeanPostProcessor可以对我们的对象进行“加工处理”。

Spring管理Bean(或者说Bean的生命周期)也是一个常考的知识点,我在秋招也重新整理了一下步骤,因为比较重要,所以还是在这里贴一下吧:

  1. ResouceLoader加载配置信息
  2. BeanDefintionReader解析配置信息,生成一个一个的BeanDefintion
  3. BeanDefintion由BeanDefintionRegistry管理起来
  4. BeanFactoryPostProcessor对配置信息进行加工(也就是处理配置的信息,一般通过PropertyPlaceholderConfigurer来实现)
  5. 实例化Bean
  6. 如果该Bean配置/实现了InstantiationAwareBean,则调用对应的方法
  7. 使用BeanWarpper来完成对象之间的属性配置(依赖)
  8. 如果该Bean配置/实现了Aware接口,则调用对应的方法
  9. 如果该Bean配置了BeanPostProcessor的before方法,则调用
  10. 如果该Bean配置了init-method或者实现InstantiationBean,则调用对应的方法
  11. 如果该Bean配置了BeanPostProcessor的after方法,则调用
  12. 将对象放入到HashMap中
  13. 最后如果配置了destroy或者DisposableBean的方法,则执行销毁操作

Application中Bean的声明周期

其中也有关于BPP图片:

BBP所在的位置

为什么特意讲BPP?

Spring AOP编程底层通过的是动态代理技术,在调用的时候肯定用的是代理对象。那么Spring是怎么做的呢?

我只需要写一个BPP,在postProcessBeforeInitialization或者postProcessAfterInitialization方法中,对对象进行判断,看他需不需要织入切面逻辑,如果需要,那我就根据这个对象,生成一个代理对象,然后返回这个代理对象,那么最终注入容器的,自然就是代理对象了。

Spring提供了BeanPostProcessor,就是让我们可以对有需要的对象进行“加工处理”啊!

认识Spring事务几个重要的接口

Spring事务可以分为两种:

  • 编程式事务(通过代码的方式来实现事务)
  • 声明式事务(通过配置的方式来实现事务)

编程式事务在Spring实现相对简单一些,而声明式事务因为封装了大量的东西(一般我们使用简单,里头都非常复杂),所以声明式事务实现要难得多。

在编程式事务中有以下几个重要的了接口:

  • TransactionDefinition:定义了Spring兼容的事务属性(比如事务隔离级别、事务传播、事务超时、是否只读状态)
  • TransactionStatus:代表了事务的具体运行状态(获取事务运行状态的信息,也可以通过该接口间接回滚事务等操作)
  • PlatformTransactionManager:事务管理器接口(定义了一组行为,具体实现交由不同的持久化框架来完成—类比JDBC)

PlatformTransactionManager解析

在声明式事务中,除了TransactionStatus和PlatformTransactionManager接口,还有几个重要的接口:

  • TransactionProxyFactoryBean:生成代理对象
  • TransactionInterceptor:实现对象的拦截
  • TransactionAttrubute:事务配置的数据

参考资料:

Spring 与设计模式

IoC(Inversion of Control,控制翻转)是Spring中一个非常非常重要的概念,它不是什么技术,而是一种解耦的设计思想。它的主要目的是借助于“第三方”(Spring中的IOC容器)实现具有依赖关系的对象之间的解耦(IOC容易管理对象,你只管使用即可),从而降低代码之间的耦合度。IOC是一个原则,而不是一个模式,以下模式(但不限于)实现了IoC原则。

Spring IOC容器就像是一个工厂一样,当我们需要创建一个对象的时候,只需要配置好配置文件/注解即可,完全不用考虑对象是如何被创建出来的。IOC容器负责创建对象,将对象连接在一起,配置这些对象,并从创建中处理这些对象的整个生命周期,直到它们被完全销毁。

在实际项目中一个Service类如果有几百甚至上千个类作为它的底层,我们需要实例化这个Service,你可能要每次都要搞清这个Service所有底层类的构造函数,这可能会把人逼疯。如果利用IOC的话,你只需要配置好,然后在需要的地方引用就行了,这大大增加了项目的可维护性且降低了开发难度。关于Spring IOC的理解,推荐看这一下知乎的一个回答,非常不错。

控制翻转怎么理解呢?举个例子:”对象a依赖了对象b,当对象a需要使用对象b的时候必须自己去创建。但是当系统引入了IOC容器后,对象a和对象b之前就失去了直接的联系。这个时候,当对象a需要使用对象b的时候,我们可以指定IOC容器去创建一个对象b注入到对象a中”。对象a获得依赖对象b的过程,由主动行为变为了被动行为,控制权翻转,这就是控制反转名字的由来。

DI(Dependecy Inject,依赖注入)是实现控制反转的一种设计模式,依赖注入就是将实例变量传入到一个对象中去。

工厂设计模式

Spring使用工厂模式可以通过BeanFactoryApplicationContext创建bean对象。

两者对比:

  • BeanFactory:延迟注入(使用到某个bean的时候才会注入),相比于BeanFactory来说会占用更少的内存,程序启动速度更快。
  • ApplicationContext:容器启动的时候,不管你用没用到,一次性创建所有bean 。BeanFactory仅提供了最基本的依赖注入支持,ApplicationContext扩展了BeanFactory,除了有BeanFactory的功能还有额外更多功能,所以一般开发人员使用ApplicationContext会更多。

ApplicationContext的三个实现类:

  1. ClassPathXmlApplication:把上下文文件当成类路径资源。
  2. FileSystemXmlApplication:从文件系统中的XML 文件载入上下文定义信息。
  3. XmlWebApplicationContext:从Web系统中的XML文件载入上下文定义信息。

Example:

1
2
3
4
5
6
7
8
9
10
11
import org.springframework.context.ApplicationContext;
import org.springframework.context.support.FileSystemXmlApplicationContext;

public class App {
public static void main(String[] args) {
ApplicationContext context = new FileSystemXmlApplicationContext(
"C:/work/IOC Containers/springframework.applicationcontext/src/main/resources/bean-factory-config.xml");
HelloApplicationContext obj = (HelloApplicationContext) context.getBean("helloApplicationContext");
obj.getMsg();
}
}

单例设计模式

在我们的系统中,有一些对象其实我们只需要一个,比如说:线程池、缓存、对话框、注册表、日志对象、充当打印机、显卡等设备驱动程序的对象。事实上,这一类对象只能有一个实例,如果制造出多个实例就可能会导致一些问题的产生,比如:程序的行为异常、资源使用过量、或者不一致性的结果。

使用单例模式的好处:

  • 对于频繁使用的对象,可以省略创建对象所花费的时间,这对于那些重量级对象而言,是非常可观的一笔系统开销;
  • 由于new 操作的次数减少,因而对系统内存的使用频率也会降低,这将减轻GC 压力,缩短GC 停顿时间。

Spring中bean的默认作用域就是singleton(单例)的。除了singleton作用域,Spring中bean还有下面几种作用域:

  • prototype : 每次请求都会创建一个新的bean 实例。
  • request : 每一次HTTP请求都会产生一个新的bean,该bean仅在当前HTTP request内有效。
  • session : 每一次HTTP请求都会产生一个新的bean,该bean仅在当前HTTP session 内有效。
  • global-session: 全局session作用域,仅仅在基于portlet的web应用中才有意义,Spring5已经没有了。Portlet是能够生成语义代码(例如:HTML)片段的小型Java Web插件。它们基于portlet容器,可以像servlet一样处理HTTP请求。但是,与 servlet 不同,每个 portlet 都有不同的会话

Spring 实现单例的方式:

  • xml : <bean id="userService" class="top.snailclimb.UserService" scope="singleton"/>
  • 注解:@Scope(value = "singleton")

Spring 通过 ConcurrentHashMap 实现单例注册表的特殊方式实现单例模式。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
// 通过 ConcurrentHashMap(线程安全) 实现单例注册表
private final Map<String, Object> singletonObjects = new ConcurrentHashMap<String, Object>(64);

public Object getSingleton(String beanName, ObjectFactory<?> singletonFactory) {
Assert.notNull(beanName, "'beanName' must not be null");
synchronized (this.singletonObjects) {
// 检查缓存中是否存在实例
Object singletonObject = this.singletonObjects.get(beanName);
if (singletonObject == null) {
//...省略了很多代码
try {
singletonObject = singletonFactory.getObject();
}
//...省略了很多代码
// 如果实例对象在不存在,我们注册到单例注册表中。
addSingleton(beanName, singletonObject);
}
return (singletonObject != NULL_OBJECT ? singletonObject : null);
}
}
//将对象添加到单例注册表
protected void addSingleton(String beanName, Object singletonObject) {
synchronized (this.singletonObjects) {
this.singletonObjects.put(beanName, (singletonObject != null ? singletonObject : NULL_OBJECT));
}
}
}

代理模式

代理模式在 AOP 中的应用

AOP(Aspect-Oriented Programming:面向切面编程)能够将那些与业务无关,却为业务模块所共同调用的逻辑或责任(例如事务处理、日志管理、权限控制等)封装起来,便于减少系统的重复代码降低模块间的耦合度,并有利于未来的可拓展性和可维护性

Spring AOP 就是基于动态代理的,如果要代理的对象,实现了某个接口,那么Spring AOP会使用JDK Proxy,去创建代理对象,而对于没有实现接口的对象,就无法使用 JDK Proxy 去进行代理了,这时候Spring AOP会使用Cglib ,这时候Spring AOP会使用 Cglib 生成一个被代理对象的子类来作为代理,如下图所示:

SpringAOPProcess

当然你也可以使用 AspectJ ,Spring AOP 已经集成了AspectJ ,AspectJ 应该算的上是 Java 生态系统中最完整的 AOP 框架了。

使用 AOP 之后我们可以把一些通用功能抽象出来,在需要用到的地方直接使用即可,这样大大简化了代码量。我们需要增加新功能时也方便,这样也提高了系统扩展性。日志功能、事务管理等等场景都用到了 AOP 。

Spring AOP 和 AspectJ AOP 有什么区别?

Spring AOP 属于运行时增强,而 AspectJ 是编译时增强。 Spring AOP 基于代理(Proxying),而 AspectJ 基于字节码操作(Bytecode Manipulation)。

Spring AOP 已经集成了 AspectJ ,AspectJ 应该算的上是 Java 生态系统中最完整的 AOP 框架了。AspectJ 相比于 Spring AOP 功能更加强大,但是 Spring AOP 相对来说更简单,

如果我们的切面比较少,那么两者性能差异不大。但是,当切面太多的话,最好选择 AspectJ ,它比Spring AOP 快很多。

QA

将SpringIOC相关知识点整理了一遍,要想知道哪些知识点是比较重要的。很简单,我们去找找相关的面试题就知道了,如果该面试题是常见的,那么说明这个知识点还是相对比较重要的啦!

以下的面试题从各种博客上摘抄下来,摘抄量较大的会注明出处的~

1什么是spring?

什么是spring?

Spring 是个java企业级应用的开源开发框架。Spring主要用来开发Java应用,但是有些扩展是针对构建J2EE平台的web应用。Spring框架目标是简化Java企业级应用开发,并通过POJO为基础的编程模型促进良好的编程习惯。

2使用Spring框架的好处是什么?

使用Spring框架的好处是什么?

  • 轻量:Spring 是轻量的,基本的版本大约2MB。
  • 控制反转:Spring通过控制反转实现了松散耦合,对象们给出它们的依赖,而不是创建或查找依赖的对象们。
  • 面向切面的编程(AOP):Spring支持面向切面的编程,并且把应用业务逻辑和系统服务分开。
  • 容器:Spring 包含并管理应用中对象的生命周期和配置。
  • MVC框架:Spring的WEB框架是个精心设计的框架,是Web框架的一个很好的替代品。
  • 事务管理:Spring 提供一个持续的事务管理接口,可以扩展到上至本地事务下至全局事务(JTA)。
  • 异常处理:Spring 提供方便的API把具体技术相关的异常(比如由JDBC,Hibernate or JDO抛出的)转化为一致的unchecked 异常。

3Spring由哪些模块组成?

Spring由哪些模块组成?

简单可以分成6大模块:

  • Core
  • AOP
  • ORM
  • DAO
  • Web
  • Spring EE

img

4BeanFactory 实现举例

BeanFactory 实现举例

Bean工厂是工厂模式的一个实现,提供了控制反转功能,用来把应用的配置和依赖从正真的应用代码中分离

在spring3.2之前最常用的是XmlBeanFactory的,但现在被废弃了,取而代之的是:XmlBeanDefinitionReader和DefaultListableBeanFactory

5什么是Spring的依赖注入?

什么是Spring的依赖注入?

依赖注入,是IOC的一个方面,是个通常的概念,它有多种解释。这概念是说你不用创建对象,而只需要描述它如何被创建。你不在代码里直接组装你的组件和服务,但是要在配置文件里描述哪些组件需要哪些服务,之后一个容器(IOC容器)负责把他们组装起来。

6有哪些不同类型的IOC(依赖注入)方式?

有哪些不同类型的IOC(依赖注入)方式?

  • 构造器依赖注入:构造器依赖注入通过容器触发一个类的构造器来实现的,该类有一系列参数,每个参数代表一个对其他类的依赖。
  • Setter方法注入:Setter方法注入是容器通过调用无参构造器或无参static工厂 方法实例化bean之后,调用该bean的setter方法,即实现了基于setter的依赖注入。
  • 工厂注入:这个是遗留下来的,很少用的了!

7哪种依赖注入方式你建议使用,构造器注入,还是 Setter方法注入?

哪种依赖注入方式你建议使用,构造器注入,还是 Setter方法注入?

你两种依赖方式都可以使用,构造器注入和Setter方法注入。最好的解决方案是用构造器参数实现强制依赖,setter方法实现可选依赖

8什么是Spring beans?

什么是Spring beans?

Spring beans 是那些形成Spring应用的主干的java对象。它们被Spring IOC容器初始化,装配,和管理。这些beans通过容器中配置的元数据创建。比如,以XML文件中<bean/>的形式定义。

这里有四种重要的方法给Spring容器提供配置元数据

  • XML配置文件。
  • 基于注解的配置。
  • 基于java的配置。
  • Groovy DSL配置

9解释Spring框架中bean的生命周期

解释Spring框架中bean的生命周期

  • Spring容器 从XML 文件中读取bean的定义,并实例化bean。
  • Spring根据bean的定义填充所有的属性。
  • 如果bean实现了BeanNameAware 接口,Spring 传递bean 的ID 到 setBeanName方法。
  • 如果Bean 实现了 BeanFactoryAware 接口, Spring传递beanfactory 给setBeanFactory 方法。
  • 如果有任何与bean相关联的BeanPostProcessors,Spring会在postProcesserBeforeInitialization()方法内调用它们。
  • 如果bean实现IntializingBean了,调用它的afterPropertySet方法,如果bean声明了初始化方法,调用此初始化方法。
  • 如果有BeanPostProcessors 和bean 关联,这些bean的postProcessAfterInitialization() 方法将被调用。
  • 如果bean实现了 DisposableBean,它将调用destroy()方法。

10解释不同方式的自动装配

解释不同方式的自动装配

  • no:默认的方式是不进行自动装配,通过显式设置ref 属性来进行装配。
  • byName:通过参数名 自动装配,Spring容器在配置文件中发现bean的autowire属性被设置成byname,之后容器试图匹配、装配和该bean的属性具有相同名字的bean。
  • byType::通过参数类型自动装配,Spring容器在配置文件中发现bean的autowire属性被设置成byType,之后容器试图匹配、装配和该bean的属性具有相同类型的bean。如果有多个bean符合条件,则抛出错误。
  • constructor:这个方式类似于byType, 但是要提供给构造器参数,如果没有确定的带参数的构造器参数类型,将会抛出异常。
  • autodetect:首先尝试使用constructor来自动装配,如果无法工作,则使用byType方式。

只用注解的方式时,注解默认是使用byType的

11IOC的优点是什么?

IOC的优点是什么?

IOC 或 依赖注入把应用的代码量降到最低。它使应用容易测试,单元测试不再需要单例和JNDI查找机制。最小的代价和最小的侵入性使松散耦合得以实现。IOC容器支持加载服务时的饿汉式初始化和懒加载

12哪些是重要的bean生命周期方法? 你能重载它们吗?

哪些是重要的bean生命周期方法? 你能重载它们吗?

有两个重要的bean 生命周期方法,第一个是setup, 它是在容器加载bean的时候被调用。第二个方法是 teardown它是在容器卸载类的时候被调用。

The bean 标签有两个重要的属性(init-methoddestroy-method)。用它们你可以自己定制初始化和注销方法。它们也有相应的注解(@PostConstruct@PreDestroy)。

13怎么回答面试官:你对Spring的理解?

怎么回答面试官:你对Spring的理解?

来源:

下面我就截几个答案:

一、

img

二、

img

14Spring框架中的单例Beans是线程安全的么?

Spring框架中的单例Beans是线程安全的么?

Spring框架并没有对单例bean进行任何多线程的封装处理。关于单例bean的线程安全和并发问题需要开发者自行去搞定。但实际上,大部分的Spring bean并没有可变的状态(比如Serview类和DAO类),所以在某种程度上说Spring的单例bean是线程安全的。如果你的bean有多种状态的话(比如 View Model 对象),就需要自行保证线程安全

最浅显的解决办法就是将多态bean的作用域由“singleton”变更为“prototype”

15FileSystemResource和ClassPathResource有何区别?

FileSystemResource和ClassPathResource有何区别?

在FileSystemResource 中需要给出spring-config.xml文件在你项目中的相对路径或者绝对路径。在ClassPathResource中spring会在ClassPath中自动搜寻配置文件,所以要把ClassPathResource文件放在ClassPath下。

如果将spring-config.xml保存在了src文件夹下的话,只需给出配置文件的名称即可,因为src文件夹是默认。

简而言之,ClassPathResource在环境变量中读取配置文件,FileSystemResource在配置文件中读取配置文件

synchronized锁问题

开启10000个线程,每个线程给员工表的money字段【初始值是0】加1,没有使用悲观锁和乐观锁,但是在业务层方法上加了synchronized关键字,问题是代码执行完毕后数据库中的money 字段不是10000,而是小于10000 问题出在哪里?

Service层代码:

代码

SQL代码(没有加悲观/乐观锁):

SQL代码(没有加悲观/乐观锁)

用1000个线程跑代码:

用1000个线程跑代码:

简单来说:多线程跑一个使用synchronized关键字修饰的方法,方法内操作的是数据库,按正常逻辑应该最终的值是1000,但经过多次测试,结果是低于1000。这是为什么呢?

我的思考

既然测试出来的结果是低于1000,那说明这段代码不是线程安全的。不是线程安全的,那问题出现在哪呢?众所周知,synchronized方法能够保证所修饰的代码块、方法保证有序性、原子性、可见性

讲道理,以上的代码跑起来,问题中Service层的increaseMoney()有序的、原子的、可见的,所以断定跟synchronized应该没关系。

(参考我之前写过的synchronize锁笔记:Java锁机制了解一下)

既然Java层面上找不到原因,那分析一下数据库层面的吧(因为方法内操作的是数据库)。在increaseMoney()方法前加了@Transcational注解,说明这个方法是带有事务的。事务能保证同组的SQL要么同时成功,要么同时失败。讲道理,如果没有报错的话,应该每个线程都对money值进行+1。从理论上来说,结果应该是1000的才对。

(参考我之前写过的Spring事务:一文带你看懂Spring事务!)

根据上面的分析,我怀疑是提问者没测试好(hhhh,逃),于是我也跑去测试了一下,发现是以提问者的方式来使用是真的有问题

首先贴一下我的测试代码:

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
@RestController
public class EmployeeController {
@Autowired
private EmployeeService employeeService;
@RequestMapping("/add")
public void addEmployee() {
for (int i = 0; i < 1000; i++) {
new Thread(() -> employeeService.addEmployee()).start();
}
}
}
@Service
public class EmployeeService {
@Autowired
private EmployeeRepository employeeRepository;
@Transactional
public synchronized void addEmployee() {
// 查出ID为8的记录,然后每次将年龄增加一
Employee employee = employeeRepository.getOne(8);
System.out.println(employee);
Integer age = employee.getAge();
employee.setAge(age + 1);
employeeRepository.save(employee);
}
}

简单地打印了每次拿到的employee值,并且拿到了SQL执行的顺序,如下(贴出小部分):

SQL执行的顺序

从打印的情况我们可以得出:多线程情况下并没有串行执行addEmployee()方法。这就导致对同一个值做重复的修改,所以最终的数值比1000要少。

图解出现的原因

发现并不是同步执行的,于是我就怀疑synchronized关键字和Spring肯定有点冲突。于是根据这两个关键字搜了一下,找到了问题所在。

我们知道Spring事务的底层是Spring AOP,而Spring AOP的底层是动态代理技术。跟大家一起回顾一下动态代理:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
public static void main(String[] args) {
// 目标对象
Object target ;
Proxy.newProxyInstance(ClassLoader.getSystemClassLoader(), Main.class, new InvocationHandler() {
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
// 但凡带有@Transcational注解的方法都会被拦截
// 1... 开启事务
method.invoke(target);
// 2... 提交事务
return null;
}
});
}

(详细请参考我之前写过的动态代理:给女朋友讲解什么是代理模式)

实际上Spring做的处理跟以上的思路是一样的,我们可以看一下TransactionAspectSupport类中invokeWithinTransaction()

Spring事务管理是如何实现的

调用方法开启事务,调用方法提交事务

Spring事务和synchronized锁互斥问题

在多线程环境下,就可能会出现:方法执行完了(synchronized代码块执行完了),事务还没提交,别的线程可以进入被synchronized修饰的方法,再读取的时候,读到的是还没提交事务的数据,这个数据不是最新的,所以就出现了这个问题。

事务未提交,别的线程读取到旧数据

解决问题

从上面我们可以发现,问题所在是因为@Transcational注解和synchronized一起使用了,加锁的范围没有包括到整个事务。所以我们可以这样做:

新建一个名叫SynchronizedService类,让其去调用addEmployee()方法,整个代码如下:

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
@RestController
public class EmployeeController {
@Autowired
private SynchronizedService synchronizedService ;
@RequestMapping("/add")
public void addEmployee() {
for (int i = 0; i < 1000; i++) {
new Thread(() -> synchronizedService.synchronizedAddEmployee()).start();
}
}
}
// 新建的Service类
@Service
public class SynchronizedService {
@Autowired
private EmployeeService employeeService ;
// 同步
public synchronized void synchronizedAddEmployee() {
employeeService.addEmployee();
}
}
@Service
public class EmployeeService {
@Autowired
private EmployeeRepository employeeRepository;
@Transactional
public void addEmployee() {
// 查出ID为8的记录,然后每次将年龄增加一
Employee employee = employeeRepository.getOne(8);
System.out.println(Thread.currentThread().getName() + employee);
Integer age = employee.getAge();
employee.setAge(age + 1);
employeeRepository.save(employee);
}
}

我们将synchronized锁的范围包含到整个Spring事务上,这就不会出现线程安全的问题了。在测试的时候,我们可以发现1000个线程跑起来比之前要慢得多,当然我们的数据是正确的:

正确的数据