Dunwu Blog

大道至简,知易行难

Spring 国际化

Spring 国际化使用场景

  • 普通国际化文案
  • Bean Validation 校验国际化文案
  • Web 站点页面渲染
  • Web MVC 错误消息提示

Spring 国际化接口

  • 核心接口:org.springframework.context.MessageSource
  • 主要概念
    • 文案模板编码(code)
    • 文案模板参数(args)
    • 区域(Locale)

层次性 MessageSource

  • Spring 层次性接口回顾
    • org.springframework.beans.factory.HierarchicalBeanFactory
    • org.springframework.context.ApplicationContext
    • org.springframework.beans.factory.config.BeanDefinition
  • Spring 层次性国际化接口
    • org.springframework.context.HierarchicalMessageSource

Java 国际化标准实现

核心接口:

  • 抽象实现 - java.util.ResourceBundle
  • Properties 资源实现 - java.util.PropertyResourceBundle
  • 例举实现 - java.util.ListResourceBundle

ResourceBundle 核心特性

  • Key-Value 设计
  • 层次性设计
  • 缓存设计
  • 字符编码控制 - java.util.ResourceBundle.Control(@since 1.6)
  • Control SPI 扩展 - java.util.spi.ResourceBundleControlProvider(@since 1.8)

Java 文本格式化

  • 核心接口
    • java.text.MessageFormat
  • 基本用法
    • 设置消息格式模式- new MessageFormat(…)
    • 格式化 - format(new Object[]{…})
  • 消息格式模式
    • 格式元素:{ArgumentIndex (,FormatType,(FormatStyle))}
    • FormatType:消息格式类型,可选项,每种类型在 number、date、time 和 choice 类型选其一
    • FormatStyle:消息格式风格,可选项,包括:short、medium、long、full、integer、currency、
      percent
  • 高级特性
    • 重置消息格式模式
    • 重置 java.util.Locale
    • 重置 java.text.Format

MessageSource 开箱即用实现

  • 基于 ResourceBundle + MessageFormat 组合 MessageSource 实现
  • org.springframework.context.support.ResourceBundleMessageSource
  • 可重载 Properties + MessageFormat 组合 MessageSource 实现
  • org.springframework.context.support.ReloadableResourceBundleMessageSource

MessageSource 內建依赖

  • MessageSource 內建 Bean 可能来源
  • 预注册 Bean 名称为:“messageSource”,类型为:MessageSource Bean
  • 默认內建实现 - DelegatingMessageSource
  • 层次性查找 MessageSource 对象

问题

Spring Boot 为什么要新建 MessageSource Bean

  • AbstractApplicationContext 的实现决定了 MessageSource 內建实现
  • Spring Boot 通过外部化配置简化 MessageSource Bean 构建
  • Spring Boot 基于 Bean Validation 校验非常普遍

Spring 国际化接口有哪些

  • 核心接口 - MessageSource
  • 层次性接口 - org.springframework.context.HierarchicalMessageSource

Spring 有哪些 MessageSource 內建实现

  • org.springframework.context.support.ResourceBundleMessageSource
  • org.springframework.context.support.ReloadableResourceBundleMessageSource
  • org.springframework.context.support.StaticMessageSource
  • org.springframework.context.support.DelegatingMessageSource

如何实现配置自动更新 MessageSource

主要技术

  • Java NIO 2:java.nio.file.WatchService
  • Java Concurrency : java.util.concurrent.ExecutorService
  • Spring:org.springframework.context.support.AbstractMessageSource

参考资料

Spring 配置元数据

Spring 配置元信息

  • Spring Bean 配置元信息 - BeanDefinition
  • Spring Bean 属性元信息 - PropertyValues
  • Spring 容器配置元信息
  • Spring 外部化配置元信息 - PropertySource
  • Spring Profile 元信息 - @Profile

Spring Bean 配置元信息

Bean 配置元信息 - BeanDefinition

  • GenericBeanDefinition:通用型 BeanDefinition
  • RootBeanDefinition:无 Parent 的 BeanDefinition 或者合并后 BeanDefinition
  • AnnotatedBeanDefinition:注解标注的 BeanDefinition

Spring Bean 属性元信息

  • Bean 属性元信息 - PropertyValues
    • 可修改实现 - MutablePropertyValues
    • 元素成员 - PropertyValue
  • Bean 属性上下文存储 - AttributeAccessor
  • Bean 元信息元素 - BeanMetadataElement

Spring 容器配置元信息

Spring XML 配置元信息 - beans 元素相关

beans 元素属性 默认值 使用场景
profile null(留空) Spring Profiles 配置值
default-lazy-init default 当 outter beans “default-lazy-init” 属性存在时,继承该值,否则为“false”
default-merge default 当 outter beans “default-merge” 属性存在时,继承该值,否则为“false”
default-autowire default 当 outter beans “default-autowire” 属性存在时,继承该值,否则为“no”
default-autowire-candidates null(留空) 默认 Spring Beans 名称 pattern
default-init-method null(留空) 默认 Spring Beans 自定义初始化方法
default-destroy-method null(留空) 默认 Spring Beans 自定义销毁方法

Spring XML 配置元信息 - 应用上下文相关

XML 元素 使用场景
<context:annotation-config /> 激活 Spring 注解驱动
<context:component-scan /> Spring @Component 以及自定义注解扫描
<context:load-time-weaver /> 激活 Spring LoadTimeWeaver
<context:mbean-export /> 暴露 Spring Beans 作为 JMX Beans
<context:mbean-server /> 将当前平台作为 MBeanServer
<context:property-placeholder /> 加载外部化配置资源作为 Spring 属性配
<context:property-override /> 利用外部化配置资源覆盖 Spring 属

基于 XML 文件装载 Spring Bean 配置元信息

底层实现 - XmlBeanDefinitionReader

XML 元素 使用场景
<beans:beans /> 单 XML 资源下的多个 Spring Beans 配置
<beans:bean /> 单个 Spring Bean 定义(BeanDefinition)配置
<beans:alias /> 为 Spring Bean 定义(BeanDefinition)映射别名
<beans:import /> 加载外部 Spring XML 配置资源

基于 Properties 文件装载 Spring Bean 配置元信息

底层实现 - PropertiesBeanDefinitionReader

Properties 属性名 使用场景
class Bean 类全称限定名
abstract 是否为抽象的 BeanDefinition
parent 指定 parent BeanDefinition 名称
lazy-init 是否为延迟初始化
ref 引用其他 Bean 的名称
scope 设置 Bean 的 scope 属性
${n} n 表示第 n+1 个构造器参数

基于 Java 注解装载 Spring Bean 配置元信息

Spring 模式注解

Spring 注解 场景说明 起始版本
@Repository 数据仓储模式注解 2.0
@Component 通用组件模式注解 2.5
@Service 服务模式注解 2.5
@Controller Web 控制器模式注解 2.5
@Configuration 配置类模式注解 3.0

Spring Bean 定义注解

| Spring 注解 | 场景说明 | 起始版本 |
| ———— | —————————————— | ———– | — |
| @Bean | 替换 XML 元素 <bean> | 3.0 |
| @DependsOn | 替代 XML 属性 <bean depends-on="..."/> | 3.0 |
| @Lazy | 替代 XML 属性 <bean lazy-init="true | falses" /> | 3.0 |
| @Primary | 替换 XML 元素 <bean primary="true | false" /> | 3.0 |
| @Role | 替换 XML 元素 <bean role="..." /> | 3.1 |
| @Lookup | 替代 XML 属性 <bean lookup-method="..."> | 4.1 |

Spring Bean 依赖注入注解

Spring 注解 场景说明 起始版本
@Autowired Bean 依赖注入,支持多种依赖查找方式 2.5
@Qualifier 细粒度的 @Autowired 依赖查找 2.5
Java 注解 场景说明 起始版本
@Resource 类似于 @Autowired 2.5
@Inject 类似于 @Autowired 2.5

Spring Bean 条件装配注解

Spring 注解 场景说明 起始版本
@Profile 配置化条件装配 3.1
@Conditional 编程条件装配 4.0

Spring Bean 生命周期回调注解

Spring 注解 场景说明 起始版本
@PostConstruct 替换 XML 元素 <bean init-method="..." /> 或 InitializingBean 2.5
@PreDestroy 替换 XML 元素 <bean destroy-method="..." /> 或 DisposableBean 2.5

Spring BeanDefinition 解析与注册

Spring 注解 场景说明 起始版本
XML 资源 XmlBeanDefinitionReader 1.0
Properties 资源 PropertiesBeanDefinitionReader 1.0
Java 注解 AnnotatedBeanDefinitionReader 3.0

Spring Bean 配置元信息底层实现

Spring XML 资源 BeanDefinition 解析与注册

核心 API - XmlBeanDefinitionReader

  • 资源 - Resource
  • 底层 - BeanDefinitionDocumentReader
    • XML 解析 - Java DOM Level 3 API
    • BeanDefinition 解析 - BeanDefinitionParserDelegate
    • BeanDefinition 注册 - BeanDefinitionRegistry

Spring Properties 资源 BeanDefinition 解析与注册

核心 API - PropertiesBeanDefinitionReader

  • 资源
    • 字节流 - Resource
    • 字符流 - EncodedResouce
  • 底层
    • 存储 - java.util.Properties
    • BeanDefinition 解析 - API 内部实现
    • BeanDefinition 注册 - BeanDefinitionRegistry

Spring Java 注册 BeanDefinition 解析与注册

核心 API - AnnotatedBeanDefinitionReader

  • 资源
    • 类对象 - java.lang.Class
  • 底层
    • 条件评估 - ConditionEvaluator
    • Bean 范围解析 - ScopeMetadataResolver
    • BeanDefinition 解析 - 内部 API 实现
    • BeanDefinition 处理 - AnnotationConfigUtils.processCommonDefinitionAnnotations
    • BeanDefinition 注册 - BeanDefinitionRegistry

基于 XML 文件装载 Spring IoC 容器配置元信息

Spring IoC 容器相关 XML 配置

命名空间 所属模块 Schema 资源 URL
beans spring-beans https://www.springframework.org/schema/beans/spring-beans.xsd
context spring-context https://www.springframework.org/schema/context/spring-context.xsd
aop spring-aop https://www.springframework.org/schema/aop/spring-aop.xsd
tx spring-tx https://www.springframework.org/schema/tx/spring-tx.xsd
util spring-beans beans https://www.springframework.org/schema/util/spring-util.xsd
tool spring-beans https://www.springframework.org/schema/tool/spring-tool.xsd

基于 Java 注解装载 Spring IoC 容器配置元信息

Spring IoC 容器装配注解

Spring 注解 场景说明 起始版本
@ImportResource 替换 XML 元素 <import> 3.0
@Import 导入 Configuration Class 3.0
@ComponentScan 扫描指定 package 下标注 Spring 模式注解的类 3.1

Spring IoC 配属属性注解

Spring 注解 场景说明 起始版本
@PropertySource 配置属性抽象 PropertySource 注解 3.1
@PropertySources @PropertySource 集合注解 4.0

基于 Extensible XML authoring 扩展 SpringXML 元素

Spring XML 扩展

  • 编写 XML Schema 文件:定义 XML 结构
  • 自定义 NamespaceHandler 实现:命名空间绑定
  • 自定义 BeanDefinitionParser 实现:XML 元素与 BeanDefinition 解析
  • 注册 XML 扩展:命名空间与 XML Schema 映射

Extensible XML authoring 扩展原理

触发时机

  • AbstractApplicationContext#obtainFreshBeanFactory
    • AbstractRefreshableApplicationContext#refreshBeanFactory
      • AbstractXmlApplicationContext#loadBeanDefinitions
          • XmlBeanDefinitionReader#doLoadBeanDefinitions
              • BeanDefinitionParserDelegate#parseCustomElement

核心流程

BeanDefinitionParserDelegate#parseCustomElement(org.w3c.dom.Element, BeanDefinition)

  • 获取 namespace
  • 通过 namespace 解析 NamespaceHandler
  • 构造 ParserContext
  • 解析元素,获取 BeanDefinintion

基于 Properties 文件装载外部化配置

注解驱动

  • @org.springframework.context.annotation.PropertySource
  • @org.springframework.context.annotation.PropertySources

API 编程

  • org.springframework.core.env.PropertySource
  • org.springframework.core.env.PropertySources

基于 YAML 文件装载外部化配置

API 编程

  • org.springframework.beans.factory.config.YamlProcessor
    • org.springframework.beans.factory.config.YamlMapFactoryBean
    • org.springframework.beans.factory.config.YamlPropertiesFactoryBean

问题

Spring 內建 XML Schema 常见有哪些

命名空间 所属模块 Schema 资源 URL
beans spring-beans https://www.springframework.org/schema/beans/spring-beans.xsd
context spring-context https://www.springframework.org/schema/context/spring-context.xsd
aop spring-aop https://www.springframework.org/schema/aop/spring-aop.xsd
tx spring-tx https://www.springframework.org/schema/tx/spring-tx.xsd
util spring-beans beans https://www.springframework.org/schema/util/spring-util.xsd
tool spring-beans https://www.springframework.org/schema/tool/spring-tool.xsd

Spring 配置元信息具体有哪些

  • Bean 配置元信息:通过媒介(如 XML、Proeprties 等),解析 BeanDefinition
  • IoC 容器配置元信息:通过媒介(如 XML、Proeprties 等),控制 IoC 容器行为,比如注解驱动、AOP 等
  • 外部化配置:通过资源抽象(如 Proeprties、YAML 等),控制 PropertySource
  • Spring Profile:通过外部化配置,提供条件分支流程

Extensible XML authoring 的缺点

  • 高复杂度:开发人员需要熟悉 XML Schema,spring.handlers,spring.schemas 以及 Spring API
  • 嵌套元素支持较弱:通常需要使用方法递归或者其嵌套解析的方式处理嵌套(子)元素
  • XML 处理性能较差:Spring XML 基于 DOM Level 3 API 实现,该 API 便于理解,然而性能较差
  • XML 框架移植性差:很难适配高性能和便利性的 XML 框架,如 JAXB

参考资料

Spring Bean 生命周期

Spring Bean 元信息配置阶段

BeanDefinition 配置

  • 面向资源
    • XML 配置
    • Properties 资源配置
  • 面向注解
  • 面向 API

Spring Bean 元信息解析阶段

  • 面向资源 BeanDefinition 解析
    • BeanDefinitionReader
    • XML 解析器 - BeanDefinitionParser
  • 面向注解 BeanDefinition 解析
    • AnnotatedBeanDefinitionReader

Spring Bean 注册阶段

BeanDefinition 注册接口:BeanDefinitionRegistry

Spring BeanDefinition 合并阶段

BeanDefinition 合并

父子 BeanDefinition 合并

  • 当前 BeanFactory 查找
  • 层次性 BeanFactory 查找

Spring Bean Class 加载阶段

  • ClassLoader 类加载
  • Java Security 安全控制
  • ConfigurableBeanFactory 临时 ClassLoader

Spring Bean 实例化前阶段

实例化方式

  • 传统实例化方式:实例化策略(InstantiationStrategy)
  • 构造器依赖注入

Spring Bean 实例化阶段

非主流生命周期 - Bean 实例化前阶段

InstantiationAwareBeanPostProcessor#postProcessBeforeInstantiation

Spring Bean 实例化后阶段

Bean 属性赋值(Populate)判断

InstantiationAwareBeanPostProcessor#postProcessAfterInstantiation

Spring Bean 属性赋值前阶段

  • Bean 属性值元信息
    • PropertyValues
  • Bean 属性赋值前回调
    • Spring 1.2 - 5.0:InstantiationAwareBeanPostProcessor#postProcessPropertyValues
    • Spring 5.1:InstantiationAwareBeanPostProcessor#postProcessProperties

Spring Bean Aware 接口回调阶段

Spring Aware 接口:

  • BeanNameAware
  • BeanClassLoaderAware
  • BeanFactoryAware
  • EnvironmentAware
  • EmbeddedValueResolverAware
  • ResourceLoaderAware
  • ApplicationEventPublisherAware
  • MessageSourceAware
  • ApplicationContextAware

Spring Bean 初始化前阶段

已完成:

  • Bean 实例化

  • Bean 属性赋值

  • Bean Aware 接口回调

方法回调:

  • BeanPostProcessor#postProcessBeforeInitialization

Spring Bean 初始化阶段

Bean 初始化(Initialization)

  • @PostConstruct 标注方法
  • 实现 InitializingBean 接口的 afterPropertiesSet() 方法
  • 自定义初始化方法

Spring Bean 初始化后阶段

方法回调:BeanPostProcessor#postProcessAfterInitialization

Spring Bean 初始化完成阶段

方法回调:Spring 4.1 +:SmartInitializingSingleton#afterSingletonsInstantiated

Spring Bean 销毁前阶段

方法回调:DestructionAwareBeanPostProcessor#postProcessBeforeDestruction

Spring Bean 销毁阶段

Bean 销毁(Destroy)

  • @PreDestroy 标注方法
  • 实现 DisposableBean 接口的 destroy() 方法
  • 自定义销毁方法

Spring Bean 垃圾收集

Bean 垃圾回收(GC)

  • 关闭 Spring 容器(应用上下文)
  • 执行 GC
  • Spring Bean 覆盖的 finalize() 方法被回调

问题

BeanPostProcessor 的使用场景有哪些

BeanPostProcessor 提供 Spring Bean 初始化前和初始化后的生命周期回调,分别对应 postProcessBeforeInitialization 以及 postProcessAfterInitialization 方法,允许对关心的 Bean 进行扩展,甚至是替换。

加分项:其中,ApplicationContext 相关的 Aware 回调也是基于 BeanPostProcessor 实现,即 ApplicationContextAwareProcessor。

BeanFactoryPostProcessor 与 BeanPostProcessor 的区别

BeanFactoryPostProcessor 是 Spring BeanFactory(实际为 ConfigurableListableBeanFactory) 的后置处理器,用于扩展 BeanFactory,或通过 BeanFactory 进行依赖查找和依赖注入。

BeanFactoryPostProcessor 必须有 Spring ApplicationContext 执行,BeanFactory 无法与其直接交互。

而 BeanPostProcessor 则直接与 BeanFactory 关联,属于 N 对 1 的关系。

BeanFactory 是怎样处理 Bean 生命周期

BeanFactory 的默认实现为 DefaultListableBeanFactory,其中 Bean生命周期与方法映射如下:

  • BeanDefinition 注册阶段 - registerBeanDefinition
  • BeanDefinition 合并阶段 - getMergedBeanDefinition
  • Bean 实例化前阶段 - resolveBeforeInstantiation
  • Bean 实例化阶段 - createBeanInstance
  • Bean 初始化后阶段 - populateBean
  • Bean 属性赋值前阶段 - populateBean
  • Bean 属性赋值阶段 - populateBean
  • Bean Aware 接口回调阶段 - initializeBean
  • Bean 初始化前阶段 - initializeBean
  • Bean 初始化阶段 - initializeBean
  • Bean 初始化后阶段 - initializeBean
  • Bean 初始化完成阶段 - preInstantiateSingletons
  • Bean 销毁前阶段 - destroyBean
  • Bean 销毁阶段 - destroyBean

参考资料

Spring Bean 作用域

Spring Bean 作用域

来源 说明
singleton 默认 Spring Bean 作用域,一个 BeanFactory 有且仅有一个实例
prototype 原型作用域,每次依赖查找和依赖注入生成新 Bean 对象
request 将 Spring Bean 存储在 ServletRequest 上下文中
session 将 Spring Bean 存储在 HttpSession 中
application 将 Spring Bean 存储在 ServletContext 中

“singleton” Bean 作用域

“prototype” Bean 作用域

Spring 容器没有办法管理 prototype Bean 的完整生命周期,也没有办法记录实例的存在。销毁回调方法将不会执行,可以利用 BeanPostProcessor 进行清扫工作。

“request” Bean 作用域

  • 配置
    • XML - <bean class="..." scope = “request" />
    • Java 注解 - @RequestScope@Scope(WebApplicationContext.SCOPE_REQUEST)
  • 实现
    • API - RequestScope

“session” Bean 作用域

  • 配置
    • XML - <bean class="..." scope = “session" />
    • Java 注解 - @SessionScope@Scope(WebApplicationContext.SCOPE_SESSION)
  • 实现
    • API - SessionScope

“application” Bean 作用域

  • 配置
    • XML - <bean class="..." scope = “application" />
    • Java 注解 - @ApplicationScope@Scope(WebApplicationContext.SCOPE_APPLICATION)
  • 实现
    • API - ServletContextScope

自定义 Bean 作用域

  • 实现 Scope

    • org.springframework.beans.factory.config.Scope
  • 注册 Scope

    • API - org.springframework.beans.factory.config.ConfigurableBeanFactory#registerScope
  • 配置

    1
    2
    3
    4
    5
    6
    7
    8
    <bean class="org.springframework.beans.factory.config.CustomScopeConfigurer">
    <property name="scopes">
    <map>
    <entry key="...">
    </entry>
    </map>
    </property>
    </bean>

问题

Spring 內建的 Bean 作用域有几种?

singleton、prototype、request、session、application 以及 websocket

singleton Bean 是否在一个应用是唯一的?

否。singleton bean 仅在当前 Spring IoC 容器(BeanFactory)中是单例对象。

application Bean 是否可以被其他方案替代?

可以的,实际上,“application” Bean 与“singleton” Bean 没有本质区别

参考资料

Spring IoC 依赖来源

依赖查找的来源

查找来源

来源 配置元数据
Spring BeanDefinition <bean id ="user" class="xxx.xxx.User">
@Bean public User user() {...}
BeanDefinitionBuilder
单例对象 API 实现

Spring 內建 BeanDefintion

Bean 名称 Bean 实例 使用场景
org.springframework.context.annotation.internalConfigurationAnnotationProcessor ConfigurationClassPostProcessor 对象 处理 Spring 配置类
org.springframework.context.annotation.internalAutowiredAnnotationProcessor AutowiredAnnotationBeanPostProcessor 对象 处理 @Autowired 以及 @Value 注解
org.springframework.context.annotation.internalCommonAnnotationProcessor CommonAnnotationBeanPostProcessor 对象 (条件激活)处理 JSR-250 注解,如 @PostConstruct 等
org.springframework.context.event.internalEventListenerProcessor EventListenerMethodProcessor 对象 处理标注 @EventListener 的 Spring 事件监听方法

Spring 內建单例对象

Bean 名称 Bean 实例 使用场景
environment Environment 对象 外部化配置以及 Profiles
systemProperties java.util.Properties 对象 Java 系统属性
systemEnvironment java.util.Map 对象 操作系统环境变量
messageSource MessageSource 对象 国际化文案
lifecycleProcessor LifecycleProcessor 对象 Lifecycle Bean 处理器
applicationEventMulticaster ApplicationEventMulticaster 对象 Spring 事件广播器

依赖注入的来源

来源 配置元数据
Spring BeanDefinition <bean id ="user" class="xxx.xxx.User">
@Bean public User user() {...}
BeanDefinitionBuilder
单例对象 API 实现
非 Spring 容器管理对象

Spring 容器管理和游离对象

来源 Spring Bean 对象 生命周期管理 配置元信息 使用场景
Spring BeanDefinition 依赖查找、依赖注入
单体对象 依赖查找、依赖注入
Resolvable Dependency 依赖注入

Spring BeanDefinition 作为依赖来源

  • 元数据:BeanDefinition
  • 注册:BeanDefinitionRegistry#registerBeanDefinition
  • 类型:延迟和非延迟
  • 顺序:Bean 生命周期顺序按照注册顺序

单例对象作为依赖来源

  • 要素
    • 来源:外部普通 Java 对象(不一定是 POJO)
    • 注册:SingletonBeanRegistry#registerSingleton
  • 限制
    • 无生命周期管理
    • 无法实现延迟初始化 Bean

非 Spring 对象容器管理对象作为依赖来源

  • 要素
    • 注册:ConfigurableListableBeanFactory#registerResolvableDependency
  • 限制
    • 无生命周期管理
    • 无法实现延迟初始化 Bean
    • 无法通过依赖查找

外部化配置作为依赖来源

  • 要素
    • 类型:非常规 Spring 对象依赖来源
  • 限制
    • 无生命周期管理
    • 无法实现延迟初始化 Bean
    • 无法通过依赖查找

问题

注入和查找的依赖来源是否相同?

否,依赖查找的来源仅限于 Spring BeanDefinition 以及单例对象,而依赖注入的来源还包括 Resolvable Dependency 以及 @Value 所标注的外部化配置

单例对象能在 IoC 容器启动后注册吗?

可以的,单例对象的注册与 BeanDefinition 不同,BeanDefinition 会被 ConfigurableListableBeanFactory#freezeConfiguration() 方法影响,从而冻结注册,单例对象则没有这个限制。

Spring 依赖注入的来源有哪些?

  • Spring BeanDefinition
  • 单例对象
  • Resolvable Dependency
  • @Value 外部化配置

参考资料

安全漏洞防护

XSS

概念

跨站脚本(Cross-site scripting,通常简称为XSS) 是一种网站应用程序的安全漏洞攻击,是代码注入的一种。它允许恶意用户将代码注入到网页上,其他用户在观看网页时就会受到影响。这类攻击通常包含了 HTML 以及用户端脚本语言。

XSS 攻击示例:

假如有下面一个 textbox

1
<input type="text" name="address1" value="value1from" />

value1from 是来自用户的输入,如果用户不是输入 value1from,而是输入 "/><script>alert(document.cookie)</script><!- 那么就会变成:

1
2
3
4
5
<input type="text" name="address1" value="" />
<script>
alert(document.cookie)
</script>
<!- ">

嵌入的 JavaScript 代码将会被执行。攻击的威力,取决于用户输入了什么样的脚本。

攻击手段和目的

常用的 XSS 攻击手段和目的有:

  • 盗用 cookie,获取敏感信息。
  • 利用植入 Flash,通过 crossdomain 权限设置进一步获取更高权限;或者利用 Java 等得到类似的操作。
  • 利用 iframeframeXMLHttpRequest 或上述 Flash 等方式,以(被攻击)用户的身份执行一些管理动作,或执行一些一般的如发微博、加好友、发私信等操作。
  • 利用可被攻击的域受到其他域信任的特点,以受信任来源的身份请求一些平时不允许的操作,如进行不当的投票活动。
  • 在访问量极大的一些页面上的 XSS 可以攻击一些小型网站,实现 DDoS 攻击的效果。

应对手段

  • 过滤特殊字符 - 将用户所提供的内容进行过滤,从而避免 HTML 和 Jascript 代码的运行。如 > 转义为 &gt< 转义为 &lt 等,就可以防止大部分攻击。为了避免对不必要的内容错误转移,如 3<5 中的 < 需要进行文本匹配后再转移,如:<img src= 这样的上下文中的 < 才转义。
  • 设置 Cookie 为 HttpOnly - 设置了 HttpOnly 的 Cookie 可以防止 JavaScript 脚本调用,就无法通过 document.cookie 获取用户 Cookie 信息。

:point_right: 参考阅读:

CSRF

概念

**跨站请求伪造(Cross-site request forgery,CSRF)**,也被称为 one-click attack 或者 session riding,通常缩写为 CSRF 或者 XSRF。它是一种挟持用户在当前已登录的 Web 应用程序上执行非本意的操作的攻击方法。和跨站脚本(XSS)相比,XSS 利用的是用户对指定网站的信任,CSRF 利用的是网站对用户网页浏览器的信任。

攻击手段和目的

可以如此理解 CSRF:攻击者盗用了你的身份,以你的名义发送恶意请求。

CSRF 能做的事太多:

  • 以你名义发送邮件,发消息
  • 用你的账号购买商品
  • 用你的名义完成虚拟货币转账
  • 泄露个人隐私

应对手段

  • 表单 Token - CSRF 是一个伪造用户请求的操作,所以需要构造用户请求的所有参数才可以。表单 Token 通过在请求参数中添加随机数的办法来阻止攻击者获得所有请求参数。
  • 验证码 - 请求提交时,需要用户输入验证码,以避免用户在不知情的情况下被攻击者伪造请求。
  • Referer check - HTTP 请求头的 Referer 域中记录着请求资源,可通过检查请求来源,验证其是否合法。

:point_right: 参考阅读:

SQL 注入

概念

**SQL 注入攻击(SQL injection)**,是发生于应用程序之数据层的安全漏洞。简而言之,是在输入的字符串之中注入 SQL 指令,在设计不良的程序当中忽略了检查,那么这些注入进去的指令就会被数据库服务器误认为是正常的 SQL 指令而运行,因此遭到破坏或是入侵。

攻击示例:

考虑以下简单的登录表单:

1
2
3
4
5
<form action="/login" method="POST">
<p>Username: <input type="text" name="username" /></p>
<p>Password: <input type="password" name="password" /></p>
<p><input type="submit" value="登陆" /></p>
</form>

我们的处理里面的 SQL 可能是这样的:

1
2
3
username:=r.Form.Get("username")
password:=r.Form.Get("password")
sql:="SELECT * FROM user WHERE username='"+username+"' AND password='"+password+"'"

如果用户的输入的用户名如下,密码任意

1
myuser' or 'foo' = 'foo' --

那么我们的 SQL 变成了如下所示:

1
SELECT * FROM user WHERE username='myuser' or 'foo' = 'foo' --'' AND password='xxx'

在 SQL 里面 -- 是注释标记,所以查询语句会在此中断。这就让攻击者在不知道任何合法用户名和密码的情况下成功登录了。

对于 MSSQL 还有更加危险的一种 SQL 注入,就是控制系统,下面这个可怕的例子将演示如何在某些版本的 MSSQL 数据库上执行系统命令。

1
2
sql:="SELECT * FROM products WHERE name LIKE '%"+prod+"%'"
Db.Exec(sql)

如果攻击提交 a%' exec master..xp_cmdshell 'net user test testpass /ADD' -- 作为变量 prod 的值,那么 sql 将会变成

1
sql:="SELECT * FROM products WHERE name LIKE '%a%' exec master..xp_cmdshell 'net user test testpass /ADD'--%'"

MSSQL 服务器会执行这条 SQL 语句,包括它后面那个用于向系统添加新用户的命令。如果这个程序是以 sa 运行而 MSSQLSERVER 服务又有足够的权限的话,攻击者就可以获得一个系统帐号来访问主机了。

虽然以上的例子是针对某一特定的数据库系统的,但是这并不代表不能对其它数据库系统实施类似的攻击。针对这种安全漏洞,只要使用不同方法,各种数据库都有可能遭殃。

攻击手段和目的

  • 数据表中的数据外泄,例如个人机密数据,账户数据,密码等。
  • 数据结构被黑客探知,得以做进一步攻击(例如 SELECT * FROM sys.tables)。
  • 数据库服务器被攻击,系统管理员账户被窜改(例如 ALTER LOGIN sa WITH PASSWORD='xxxxxx')。
  • 获取系统较高权限后,有可能得以在网页加入恶意链接、恶意代码以及 XSS 等。
  • 经由数据库服务器提供的操作系统支持,让黑客得以修改或控制操作系统(例如 xp_cmdshell “net stop iisadmin”可停止服务器的 IIS 服务)。
  • 破坏硬盘数据,瘫痪全系统(例如 xp_cmdshell “FORMAT C:”)。

应对手段

  • 使用参数化查询 - 建议使用数据库提供的参数化查询接口,参数化的语句使用参数而不是将用户输入变量嵌入到 SQL 语句中,即不要直接拼接 SQL 语句。例如使用 database/sql 里面的查询函数 PrepareQuery ,或者 Exec(query string, args ...interface{})
  • 单引号转换 - 在组合 SQL 字符串时,先针对所传入的参数进行字符替换(将单引号字符替换为连续 2 个单引号字符)。

:point_right: 参考阅读:

DoS

**拒绝服务攻击(denial-of-service attack, DoS)亦称洪水攻击**,是一种网络攻击手法,其目的在于使目标电脑的网络或系统资源耗尽,使服务暂时中断或停止,导致其正常用户无法访问。

当黑客使用网络上两个或以上被攻陷的电脑作为“僵尸”向特定的目标发动“拒绝服务”式攻击时,称为分布式拒绝服务攻击(distributed denial-of-service attack,缩写:DDoS attack、DDoS)。

攻击方式

  • 带宽消耗型攻击
  • 资源消耗型攻击

应对手段

  • 防火墙 - 允许或拒绝特定通讯协议,端口或 IP 地址。当攻击从少数不正常的 IP 地址发出时,可以简单的使用拒绝规则阻止一切从攻击源 IP 发出的通信。
  • 路由器、交换机 - 具有速度限制和访问控制能力。
  • 流量清洗 - 通过采用抗 DoS 软件处理,将正常流量和恶意流量区分开。

:point_right: 参考阅读:

认证设计

认证和授权

什么是认证

认证 (Authentication) 是根据凭据验明访问者身份的流程。即验证“你是你所说的那个人”的过程。

身份认证,通常通过用户名/邮箱/手机号以及密码匹配来完成,也可以通过手机/邮箱验证码或者生物特征(如:指纹、虹膜)等其他因素。在某些应用系统中,为了追求更高的安全性,往往会要求多种认证因素叠加使用,这就是我们经常说的多因素认证。

常见的认证方式

  • 用户名、密码认证
  • 手机和短信验证码认证
  • 邮箱和邮件验证码认证
  • 人脸识别、指纹识别等生物因素认证
  • 令牌认证
  • OTP 认证
  • Radius 网络认证

什么是授权

授权 (Authorization) 是指向经过身份认证的访问者授予执行某项操作的权限(如访问资源,执行文件/数据读写权限等)。 简言之,授权是验证“你被允许做你想做的事”的过程。

虽然授权通常在身份验证后立即发生(例如,登录计算机系统时),但这并不意味着授权以身份验证为前提:匿名代理可以被授权执行有限的操作集。

由于 Http 是一种无状态的协议,服务器单从网络连接上无从知道客户身份。会话跟踪是 Web 程序中常用的技术,用来跟踪用户的整个会话。常用会话跟踪技术是 Cookie 与 Session。

Cookie 实际上是存储在客户端上的文本信息,并保留了各种跟踪的信息。

Cookie 保存在客户端浏览器中,而 Session 保存在服务器上。如果说 Cookie 机制是通过检查客户身上的“通行证”来确定客户身份的话,那么 Session 机制就是通过检查服务器上的“客户明细表”来确认客户身份。

单点登录

SSO(Single Sign On),即单点登录。所谓单点登录,就是同平台的诸多应用登陆一次,下一次就免登陆的功能。

SSO 需要解决多个异构系统之间的问题:

  • Session 共享问题
  • 跨域问题

Session 共享问题

分布式 Session 的几种实现策略:

  • 粘性 Session - 缺点:当服务器节点宕机时,将丢失该服务器节点上的所有 Session
  • 应用服务器间的 Session 复制共享 - 缺点:占用过多内存同步过程占用网络带宽以及服务器处理器时间
  • 基于缓存的 Session 共享 ✅ (推荐方案) - 不过需要程序自身控制 Session 读写,可以考虑基于 spring-session + redis 这种成熟的方案来处理。

Cookie 不能跨域!比如:浏览器不会把 www.google.com 的 cookie 传给 www.baidu.com。

这就存在一个问题:由于域名不同,用户在系统 A 登录后,浏览器记录系统 A 的 Cookie,但是访问系统 B 的时候不会携带这个 Cookie。

针对 Cookie 不能跨域 的问题,有几种解决方案:

  • 服务端生成 Cookie 后,返回给客户端,客户端解析 Cookie ,提取 Token (比如 JWT),此后每次请求都携带这个 Token。
  • 多个域名共享 Cookie,在返回 Cookie 给客户端的时候,在 Cookie 中设置 domain 白名单。
  • 将 Token 保存在 SessionStroage 中(不依赖 Cookie 就没有跨域的问题了)。

CAS

CAS 是实现 SSO 的主流方式。

CAS 分为两部分,CAS Server 和 CAS Client

  • CAS Server - 负责用户的认证工作,就像是把第一次登录用户的一个标识存在这里,以便此用户在其他系统登录时验证其需不需要再次登录。
  • CAS Client - 业务应用,需要接入 CAS Server。当用户访问我们的应用时,首先需要重定向到 CAS Server 端进行验证,要是原来登陆过,就免去登录,重定向到下游系统,否则进行用户名密码登陆操作。

术语:

  • Ticket Granting Ticket (TGT) - 可以认为是 CAS Server 根据用户名、密码生成的一张票,存在 Server 端。
  • Ticket Granting Cookie (TGC) - 其实就是一个 Cookie,存放用户身份信息,由 Server 发给 Client 端。
  • Service Ticket (ST) - 由 TGT 生成的一次性票据,用于验证,只能用一次。

CAS 工作流程:

img

  1. 用户访问 CAS Client A(业务系统),第一次访问,重定向到认证服务中心(CAS Server)。CAS Server 发现当前请求中没有 Cookie,再重定向到 CAS Server 的登录页面。重定向请求的 URL 中包含访问地址,以便认证成功后直接跳转到访问页面。
  2. 用户在登录页面输入用户名、密码等认证信息,认证成功后,CAS Server 生成 TGT,再用 TGT 生成一个 ST。然后返回 ST 和 TGC(Cookie)给浏览器。
  3. 浏览器携带 ST 再度访问之前想访问的 CAS Client A 页面。
  4. CAS Client A 收到 ST 后,向 CAS Server 验证 ST 的有效性。验证通过则允许用户访问页面。
  5. 此时,如果登录另一个 CAS Client B,会先重定向到 CAS Server,CAS Server 可以判断这个 CAS Client B 是第一次访问,但是本地有 TGC,所以无需再次登录。用 TGC 创建一个 ST,返回给浏览器。
  6. 重复类似 3、4 步骤。

img

以上了归纳总结如下:

  1. 访问服务 - 用户访问 SSO Client 资源。
  2. 定向认证 - SSO Client 重定向用户请求到 SSO Server。
  3. 用户认证 - 用户身份认证。
  4. 发放票据 - SSO Server 会产生一个 Service Ticket (ST) 并返回给浏览器。
  5. 验证票据 - 浏览器每次访问 SSO Client 时,携带 ST,SSO Client 向 SSO Server 验证票据。只有验证通过,才允许访问。
  6. 传输用户信息 - SSO Server 验证票据通过后,传输用户认证结果信息给 SSO Client。

JWT

JSON Web Token (JWT,RFC 7519 (opens new window)),是为了在网络应用环境间传递声明而执行的一种基于 JSON 的开放标准((RFC 7519)。该 token 被设计为紧凑且安全的,特别适用于分布式站点的单点登录(SSO)场景。JWT 的声明一般被用来在身份提供者和服务提供者间传递被认证的用户身份信息,以便于从资源服务器获取资源,也可以增加一些额外的其它业务逻辑所必须的声明信息,该 token 也可直接被用于认证,也可被加密。

详细内容可以参考这篇文章:什么是 JWT (opens new window)

Oauth2.0

OAuth 是一个关于授权(Authorization)的开放网络标准,在全世界得到广泛应用,目前的版本是 2.0 版。

简单来说,OAuth 是一种授权机制。资源的所有者告诉系统,同意授权第三方应用进入系统,访问这些资源。系统从而产生一个短期的进入令牌(token),用来代替密码,供第三方应用使用。

客户端必须得到用户的授权(authorization grant),才能获得令牌(access token)。

OAuth 2.0 定义了四种授权方式。

  • 授权码模式(authorization code)
  • 简化模式(implicit)
  • 密码模式(resource owner password credentials)
  • 客户端模式(client credentials)

授权码模式

授权码(authorization code)方式,指的是第三方应用先申请一个授权码,然后再用该授权码获取令牌。

这种方式是最常用的流程,安全性也最高,它适用于那些有后端的 Web 应用。授权码通过前端传送,令牌则是储存在后端,而且所有与资源服务器的通信都在后端完成。这样的前后端分离,可以避免令牌泄漏。

隐藏模式

有些 Web 应用是纯前端应用,没有后端。这时就不能用上面的方式了,必须将令牌储存在前端。RFC 6749 就规定了第二种方式,允许直接向前端颁发令牌。这种方式没有授权码这个中间步骤,所以称为(授权码)”隐藏式”(implicit)。

密码模式

如果你高度信任某个应用,RFC 6749 也允许用户把用户名和密码,直接告诉该应用。该应用就使用你的密码,申请令牌,这种方式称为”密码式”(password)。

客户端凭证模式

适用于没有前端的命令行应用,即在命令行下请求令牌。

令牌的更新

如果用户访问的时候,客户端的”访问令牌”已经过期,则需要使用”更新令牌”申请一个新的访问令牌。

客户端发出更新令牌的 HTTP 请求,包含以下参数:

  • granttype:表示使用的授权模式,此处的值固定为”refreshtoken”,必选项。
  • refresh_token:表示早前收到的更新令牌,必选项。
  • scope:表示申请的授权范围,不可以超出上一次申请的范围,如果省略该参数,则表示与上一次一致

OIDC

ID Token

ID Token 相当于用户的身份凭证,开发者的前端访问后端接口时可以携带 ID Token,开发者服务器可以校验用户的 ID Token 以确定用户身份,验证通过后返回相关资源。

ID Token 本质上是一个 JWT Token,包含了该用户身份信息相关的 key/value 键值对,例如:

1
2
3
4
5
6
7
8
9
10
{
"iss": "https://server.example.com",
"sub": "24400320", // subject 的缩写,为用户 ID
"aud": "s6BhdRkqt3",
"nonce": "n-0S6_WzA2Mj",
"exp": 1311281970,
"iat": 1311280970,
"auth_time": 1311280969,
"acr": "urn:mace:incommon:iap:silver"
}

ID Token 本质上是一个 JWT Token 意味着:

用户的身份信息直接被编码进了 id_token,你不需要额外请求其他的资源来获取用户信息;

id_token 可以验证其没有被篡改过,详情请见如何验证 ID Token。

Access Token

Access Token 用于基于 Token 的认证模式,允许应用访问一个资源 API。用户认证授权成功后,认证系统会签发 Access Token 给应用。应用需要携带 Access Token 访问资源 API,资源服务 API 会通过拦截器查验 Access Token 中的 scope 字段是否包含特定的权限项目,从而决定是否返回资源。

如果你的用户通过社交账号登录,例如微信登录,微信作为身份提供商会颁发自己的 Access Token,你的应用可以利用 Access Token 调用微信相关的 API。这些 Access Token 是由社交账号服务方控制的,格式也是任意的。

Refresh Token

AccessToken 和 IdToken 是 JSON Web Token (opens new window),有效时间通常较短。通常用户在获取资源的时候需要携带 AccessToken,当 AccessToken 过期后,用户需要获取一个新的 AccessToken。

Refresh Token 用于获取新的 AccessToken。这样可以缩短 AccessToken 的过期时间保证安全,同时又不会因为频繁过期重新要求用户登录。

用户在初次认证时,Refresh Token 会和 AccessToken、IdToken 一起返回。你的应用必须安全地存储 Refresh Token,它的重要性和密码是一样的,因为 Refresh Token 能够一直让用户保持登录。

参考资料

授权设计

授权模式

最简单的授权形式可能是根据是否已对发出请求的实体进行身份验证来授予或拒绝访问权限。 如果请求者可证明自己是所自称的身份,则可访问受保护的资源或功能。

常见的授权模式有以下几种:

  • ACL:ACL 即 通过访问控制列表。ACL 进行的授权涉及到维护明确的特定实体列表,这些实体有权或无权访问资源或功能。 ACL 提供对身份验证即授权的精细控制,但管理工作会随着实体数量的增加而变得困难。
  • RBAC:RBAC 即 基于角色的权限控制(Role-Based Access Control)。RBAC 应该是最常见的授权模式。 使用 RBAC 时,会对角色进行定义,以说明实体可执行的活动类型。 应用程序开发人员向角色而非单个实体授予访问权限。 然后,管理员可再将角色分配给不同的实体,从而控制哪些实体有权访问哪些资源和功能。在高级 RBAC 实现中,可将角色映射到权限集合,其中权限描述了可执行的细化操作或活动。 然后,会将角色配置为权限组合。 通过将授予给为实体分配的各种角色的权限进行相交,计算实体的总体权限集。
  • ABAC:ABAC 即 基于属性的访问控制 是一种更精细的访问控制机制。在此方法中,规则应用于实体、所访问的资源和当前环境。 这些规则用于确定对资源和功能的访问级别。 例如,可能只允许拥有管理员身份的用户在工作日上午 9 点至下午 5 点期间访问使用元数据标记“仅限工作时间的管理员”标识的文件。 在这种情况下,通过检查用户的属性(状态为管理员)、资源属性(文件上的元数据标记)以及环境属性(当前时间)来确定访问权限。
    • ABAC 的优点:可通过规则和条件评估实现更精细的动态访问控制,而无需创建大量特定的角色和 RBAC 分配。

RBAC

RBAC(Role-Based Access Control)即:基于角色的权限控制。通过角色关联用户,角色关联权限的方式间接赋予用户权限。

每个用户关联一个或多个角色,每个角色关联一个或多个权限,从而可以实现了非常灵活的权限管理。角色可以根据实际业务需求灵活创建,这样就省去了每新增一个用户就要关联一遍所有权限的麻烦。简单来说 RBAC 就是:用户关联角色,角色关联权限。

img

角色继承(Hierarchical Role) 就是指角色可以继承于其他角色,在拥有其他角色权限的同时,自己还可以关联额外的权限。这种设计可以给角色分组和分层,一定程度简化了权限管理工作。

img

职责分离(Separation of Duty)

为了避免用户拥有过多权限而产生利益冲突,例如一个篮球运动员同时拥有裁判的权限(看一眼就给你判犯规狠不狠?),另一种职责分离扩展版的 RBAC 被提出。

职责分离有两种模式:

静态职责分离(Static Separation of Duty):用户无法同时被赋予有冲突的角色。

img

动态职责分离(Dynamic Separation of Duty):用户在一次会话(Session)中不能同时激活自身所拥有的、互相有冲突的角色,只能选择其一。

img

讲了这么多 RBAC,都还只是在用户和权限之间进行设计,并没有涉及到用户和对象之间的权限判断,而在实际业务系统中限制用户能够使用的对象是很常见的需求。

RBAC0 模型

最简单的用户、角色、权限模型。这里面又包含了 2 种:

  1. 用户和角色是多对一关系,即:一个用户只充当一种角色,一种角色可以有多个用户担当。
  2. 用户和角色是多对多关系,即:一个用户可同时充当多种角色,一种角色可以有多个用户担当。

那么,什么时候该使用多对一的权限体系,什么时候又该使用多对多的权限体系呢?

如果系统功能比较单一,使用人员较少,岗位权限相对清晰且确保不会出现兼岗的情况,此时可以考虑用多对一的权限体系。其余情况尽量使用多对多的权限体系,保证系统的可扩展性。如:张三既是行政,也负责财务工作,那张三就同时拥有行政和财务两个角色的权限。

RBAC1 模型

相对于 RBAC0 模型,增加了子角色,引入了继承概念,即子角色可以继承父角色的所有权限。

img

使用场景:如某个业务部门,有经理、主管、专员。主管的权限不能大于经理,专员的权限不能大于主管,如果采用 RBAC0 模型做权限系统,极可能出现分配权限失误,最终出现主管拥有经理都没有的权限的情况。

而 RBAC1 模型就很好解决了这个问题,创建完经理角色并配置好权限后,主管角色的权限继承经理角色的权限,并且支持在经理权限上删减主管权限。

RBAC2 模型

基于 RBAC0 模型,增加了对角色的一些限制:角色互斥、基数约束、先决条件角色等。

  • 角色互斥:同一用户不能分配到一组互斥角色集合中的多个角色,互斥角色是指权限互相制约的两个角色。案例:财务系统中一个用户不能同时被指派给会计角色和审计员角色。
  • 基数约束:一个角色被分配的用户数量受限,它指的是有多少用户能拥有这个角色。例如:一个角色专门为公司 CEO 创建的,那这个角色的数量是有限的。
  • 先决条件角色:指要想获得较高的权限,要首先拥有低一级的权限。例如:先有副总经理权限,才能有总经理权限。
  • 运行时互斥:例如,允许一个用户具有两个角色的成员资格,但在运行中不可同时激活这两个角色。

RBAC3 模型

称为统一模型,它包含了 RBAC1 和 RBAC2,利用传递性,也把 RBAC0 包括在内,综合了 RBAC0、RBAC1 和 RBAC2 的所有特点,这里就不在多描述了。

img

什么是权限

说了这么久用户-角色-权限,可能小伙伴们都了解了什么是用户、什么是角色。但是有的小伙伴会好奇,那权限又是个什么玩意呢?

权限是资源的集合,这里的资源指的是软件中所有的内容,包括模块、菜单、页面、字段、操作功能(增删改查)等等。具体的权限配置上,目前形式多种多样,按照我个人的理解,可以将权限分为:页面权限、操作权限和数据权限(这种分类法,主要是结合自己在工作中的实际情况理解总结而来,若有不足之处,也请大家指出)。

页面权限:所有系统都是由一个个的页面组成,页面再组成模块,用户是否能看到这个页面的菜单、是否能进入这个页面就称为页面权限。

如下图:

img

客户列表、客户黑名单、客户审批页面组成了客户管理这个模块。对于普通用户,不能进行审批操作,即无客户审批页面权限,在他的账号登录后侧边导航栏只显示客户列表、客户黑名单两个菜单。

操作权限:用户凡是在操作系统中的任何动作、交互都是操作权限,如增删改查等。

数据权限:一般业务管理系统,都有数据私密性的要求:哪些人可以看到哪些数据,不可以看到哪些数据。

简单举个例子:某系统中有销售部门,销售专员负责推销商品,销售主管负责管理销售专员日常工作,经理负责组织管理销售主管作业。

如下图:

img

按照实际理解,‘销售专员张三’登录时,只能看到自己负责的数据;销售主管 2 登录时,能看到他所领导的所有业务员负责的数据,但看不到其他团队业务员负责的数据。

换另外一句话就是:我的客户只有我和我的直属上级以及直属上级的领导能看到,这就是我理解的数据权限。

要实现数据权限有多种方式:

  1. 可以利用 RBAC1 模型,通过角色分级来实现。
  2. 在‘用户-角色-权限’的基础上,增加用户与组织的关联关系,用组织决定用户的数据权限。

具体如何做呢?

① 组织层级划分:

img

② 数据可视权限规则制定:上级组织只能看到下级组织员工负责的数据,而不能看到其他平级组织及其下级组织的员工数据等。

通过以上两点,系统就可以在用户登录时,自动判断要给用户展示哪些数据了。

用户组的使用

当平台用户基数增大,角色类型增多时,如果直接给用户配角色,管理员的工作量就会很大。这时候我们可以引入一个概念“用户组”,就是将相同属性的用户归类到一起。

例如:加入用户组的概念后,可以将部门看做一个用户组,再给这个部门直接赋予角色(1 万员工部门可能就几十个),使部门拥有部门权限,这样这个部门的所有用户都有了部门权限,而不需要为每一个用户再单独指定角色,极大的减少了分配权限的工作量。

同时,也可以为特定的用户指定角色,这样用户除了拥有所属用户组的所有权限外,还拥有自身特定的权限。

用户组的优点,除了减少工作量,还有更便于理解、增加多级管理关系等。如:我们在进行组织机构配置的时候,除了加入部门,还可以加入科室、岗位等层级,来为用户组内部成员的权限进行等级上的区分。

关于用户组的详细疑难解答,请查看https://wen.woshipm.com/question/detail/88fues.html。在这里也十分感谢为我解答疑惑的朋友们!

实例分析一、如何设计 RBAC 权限系统

首先,我们思考一下一个简单的权限系统应该具备哪些内容?

答案显而易见,RBAC 模型:用户-角色-权限。所以最基本的我们应该具备用户、角色、权限这三个内容。

接下来,我们思考,究竟如何将三者关联起来。回顾前文,角色作为枢纽,关联用户、权限。所以在 RBAC 模型下,我们应该:创建一个角色,并为这个角色赋予相应权限,最后将角色赋予用户

将这个问题抽象为流程,如下图:

img

现在,基本的流程逻辑已经抽象出来了,接下来,分析该如何设计呢?

  • 第一步,需要角色管理列表,在角色管理列表能快速创建一个角色,且创建角色的同时能为角色配置权限,并且支持创建成功的角色列表能随时进行权限配置的的修改;
  • 第二步,需要用户管理列表,在用户管理列表能快速添加一个用户,且添加用户时有让用户关联角色的功能。

简单来说权限系统设计就包含以上两步,接下来为大家进行实例分析。

实例分析二、

① 创建角色列表

img

在角色列表快速创建一个角色:点击创建角色,支持创建角色时配置权限。

img

② 创建用户列表

img

在用户列表快速创建一个用户:支持用户关联角色的功能。

img

上述案例是基于最简单的 RBAC0 模型创建,适用于大部分常规的权限管理系统。

下面再分析一下 RBAC1 中角色分级具体如何设计。

  1. 在 RBAC0 的基础上,加上角色等级这个字段。
  2. 权限分配规则制定:低等级角色只能在高等级角色权限基础上进行删减权限。

具体界面呈现如下图:

img

以上就是简单的 RBAC 系统设计,若需更复杂的,还请读者根据上面的分析自行揣摩思考,尽管样式不同,但万变不离其宗,理解清楚 RBAC 模型后,结合自己的业务就可以设计出一套符合自己平台需求的角色权限系统,具体的就不再多阐述了。

OAuth2.0

OAuth2.0 简介

OAuth 是一个授权标准协议。OAuth 在全世界得到广泛应用,目前的版本是 2.0 版。

简单来说,OAuth 是一种授权机制。资源的所有者告诉系统,同意授权第三方应用进入系统,访问这些资源。系统从而产生一个短期的进入令牌(token),用来代替密码,供第三方应用使用。

客户端必须得到用户的授权(authorization grant),才能获得令牌(access token)。

根据 OAuth 2.0 协议规范,主要有四个主体

  • 授权服务器:负责颁发 Access Token。
  • 资源所有者:你的应用的用户是资源的所有者,授权其他人访问他的资源。
  • 调用方:调用方请求获取 Access Token,经过用户授权后,授权服务器为其颁发 Access Token。调用方可以携带 Access Token 到资源服务器访问用户的资源。
  • 资源服务器:接受 Access Token,然后验证它的被赋予的权限项目,最后返回资源。

其他重要概念:

  • 一次 OAuth 2.0 授权是指用户授权调用方相关的权限。
  • Code 授权码是由授权服务器颁发的,用于调用方使用 Code 换取 Token。
  • Access Token 由授权服务器颁发,持有 Access Token 说明完成了用户授权。
  • Refresh Token 是一个可选的 Token,用于在 Access Token 过期后获取一个新的 Access Token。

OAuth 2.0 授权模式

OAuth 2.0 定义了四种授权方式。

  • 授权码模式(authorization code)
  • 简化模式(implicit)
  • 密码模式(resource owner password credentials)
  • 客户端模式(client credentials)

授权码模式

授权码(authorization code)方式,指的是第三方应用先申请一个授权码,然后再用该授权码获取令牌。

这种方式是最常用的流程,安全性也最高,它适用于那些有后端的 Web 应用。授权码通过前端传送,令牌则是储存在后端,而且所有与资源服务器的通信都在后端完成。这样的前后端分离,可以避免令牌泄漏。

隐藏模式

有些 Web 应用是纯前端应用,没有后端。这时就不能用上面的方式了,必须将令牌储存在前端。RFC 6749 就规定了第二种方式,允许直接向前端颁发令牌。这种方式没有授权码这个中间步骤,所以称为(授权码)”隐藏式”(implicit)。

密码模式

如果你高度信任某个应用,RFC 6749 也允许用户把用户名和密码,直接告诉该应用。该应用就使用你的密码,申请令牌,这种方式称为”密码式”(password)。

客户端凭证模式

适用于没有前端的命令行应用,即在命令行下请求令牌。

令牌

如果用户访问的时候,客户端的”访问令牌”已经过期,则需要使用”更新令牌”申请一个新的访问令牌。

客户端发出更新令牌的 HTTP 请求,包含以下参数:

  • granttype:表示使用的授权模式,此处的值固定为”refreshtoken”,必选项。
  • refresh_token:表示早前收到的更新令牌,必选项。
  • scope:表示申请的授权范围,不可以超出上一次申请的范围,如果省略该参数,则表示与上一次一致

参考资料

Spring 之事务

Spring 针对 Java Transaction API (JTA)、JDBC、Hibernate 和 Java Persistence API(JPA) 等事务 API,实现了一致的编程模型,而 Spring 的声明式事务功能更是提供了极其方便的事务配置方式,配合 Spring Boot 的自动配置,大多数 Spring Boot 项目只需要在方法上标记 @Transactional 注解,即可一键开启方法的事务性配置。

理解事务

在软件开发领域,全有或全无的操作被称为事务(transaction)。事务允许你将几个操作组合成一个要么全部发生要么全部不发生的工作单元。传统上 Java EE 开发对事务管理有两种选择:全局事务本地事务,两者都有很大的局限性。

事务的特性

事务应该具有 4 个属性:原子性、一致性、隔离性、持久性。这四个属性通常称为 ACID

  • 原子性(Atomic):一个事务是一个不可分割的工作单位,事务中包括的诸操作要么都做,要么都不做。
  • 一致性(Consistent):事务必须是使数据库从一个一致性状态变到另一个一致性状态。一致性与原子性是密切相关的。
  • 隔离性(Isolated):一个事务的执行不能被其他事务干扰。即一个事务内部的操作及使用的数据对并发的其他事务是隔离的,并发执行的各个事务之间不能互相干扰。
  • 持久性(Durable):持久性也称永久性(permanence),指一个事务一旦提交,它对数据库中数据的改变就应该是永久性的。接下来的其他操作或故障不应该对其有任何影响。

全局事务

全局事务允许您使用多个事务资源,通常是关系数据库和消息队列。应用服务器通过 JTA 管理全局事务,这是一个繁琐的 API(部分原因在于其异常模型)。此外,JTA UserTransaction 通常需要来自 JNDI,这意味着您还需要使用 JNDI 才能使用 JTA。全局事务的使用限制了应用程序代码的任何潜在重用,因为 JTA 通常仅在应用程序服务器环境中可用。

以前,使用全局事务的首选方式是通过 EJB CMT(容器管理事务)。 CMT 是一种声明式事务管理(不同于程序化事务管理)。 EJB CMT 消除了对与事务相关的 JNDI 查找的需要,尽管使用 EJB 本身就需要使用 JNDI。它消除了大部分(但不是全部)编写 Java 代码来控制事务的需要。其明显的缺点是 CMT 与 JTA 和应用程序服务器环境相关联。此外,它仅在选择在 EJB 中实现业务逻辑(或至少在事务性 EJB 外观之后)时才可用。一般来说,EJB 的负面影响是如此之大,以至于这不是一个有吸引力的提议,尤其是在面对声明式事务管理的引人注目的替代方案时。

本地事务

本地事务是指定资源的,例如与 JDBC 连接关联的事务。本地事务可能更容易使用,但有一个明显的缺点:它们不能跨多个事务资源工作。例如,使用 JDBC 连接管理事务的代码不能在全局 JTA 事务中运行。因为应用服务器不参与事务管理,它不能确保跨多个资源的正确性(值得注意的是,大多数应用程序使用单个事务资源。)。另一个缺点是本地事务对编程模型具有侵入性。

Spring 对事务的支持

Spring 通过回调机制将实际的事务实现从事务性的代码中抽象出来。Spring 解决了全局和本地事务的缺点。它允许开发人员在任何环境中使用一致的编程模型。您只需编写一次代码,它就可以从不同环境中的不同事务管理策略中受益。Spring 提供了对编码式和声明式事务管理的支持,大多数情况下都推荐使用声明式事务管理。

  • 编码式事务允许用户在代码中精确定义事务的边界
  • 声明式事务(基于 AOP)有助于用户将操作与事务规则进行解耦

通过程序化事务管理,开发人员可以使用 Spring 事务抽象,它可以在任何底层事务基础上运行。使用首选的声明性模型,开发人员通常编写很少或根本不编写与事务管理相关的代码,因此不依赖 Spring 事务 API 或任何其他事务 API。

Spring 事务的优点

Spring 框架为事务管理提供了一致的抽象,具有以下好处:

  • 跨不同事务 API 的一致编程模型,例如 Java Transaction API (JTA)、JDBC、Hibernate 和 Java Persistence API (JPA)。
  • 支持声明式事务管理。
  • 用于编程事务管理的 API 比复杂事务 API(如 JTA)更简单。
  • 与 Spring 的数据访问抽象完美集成。

核心 API

TransactionManager

Spring 事务抽象的关键是事务策略的概念。事务策略由 TransactionManager 定义,特别是用于命令式事务管理的 org.springframework.transaction.PlatformTransactionManager 接口和用于响应式事务管理的 org.springframework.transaction.ReactiveTransactionManager 接口。

PlatformTransactionManager

以下清单显示了 PlatformTransactionManager API 的定义:

1
2
3
4
5
6
7
8
public interface PlatformTransactionManager extends TransactionManager {

TransactionStatus getTransaction(TransactionDefinition definition) throws TransactionException;

void commit(TransactionStatus status) throws TransactionException;

void rollback(TransactionStatus status) throws TransactionException;
}

PlatformTransactionManager 是一个 SPI 接口,所以使用者可以以编程方式使用它。因为 PlatformTransactionManager 是一个接口,所以可以根据需要轻松地 MOCK 或存根。它不依赖于查找策略,例如 JNDI。 PlatformTransactionManager 实现的定义与 Spring IoC 容器中的任何其他对象(或 bean)一样。仅此一项优势就使 Spring 事务成为有价值的抽象,即使您使用 JTA 也是如此。与直接使用 JTA 相比,您可以更轻松地测试事务代码。

同样,为了与 Spring 的理念保持一致,任何 PlatformTransactionManager 接口的方法可以抛出的 TransactionException 都是未经检查的(也就是说,它扩展了 java.lang.RuntimeException 类)。事务架构故障几乎总是致命的。极少数情况下,应用程序可以从事务失败中恢复,开发人员可以选择捕获和处理 TransactionException。重点是开发人员并非被迫这样做。

getTransaction(..) 方法根据 TransactionDefinition 参数返回一个 TransactionStatus 对象。如果当前调用堆栈中存在匹配的事务,则返回的 TransactionStatus 可能表示新事务或可以表示现有事务。后一种情况的含义是,与 Java EE 事务上下文一样,TransactionStatus 与执行线程相关联。

从以上可以看出,具体的事务管理机制对 Spring 来说是透明的,它并不关心那些,那些是对应各个平台需要关心的,所以 Spring 事务管理的一个优点就是为不同的事务 API 提供一致的编程模型,如 JTA、JDBC、Hibernate、JPA。下面分别介绍各个平台框架实现事务管理的机制。

JDBC 事务

如果应用程序中直接使用 JDBC 来进行持久化,DataSourceTransactionManager 会为你处理事务边界。为了使用 DataSourceTransactionManager,你需要使用如下的 XML 将其装配到应用程序的上下文定义中:

1
2
3
<bean id="transactionManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
<property name="dataSource" ref="dataSource" />
</bean>

实际上,DataSourceTransactionManager 是通过调用 java.sql.Connection 来管理事务,而后者是通过 DataSource 获取到的。通过调用连接的 commit() 方法来提交事务,同样,事务失败则通过调用 rollback() 方法进行回滚。

Hibernate 事务

如果应用程序的持久化是通过 Hibernate 实现的,那么你需要使用 HibernateTransactionManager。对于 Hibernate3,需要在 Spring 上下文定义中添加如下的 bean 声明:

1
2
3
<bean id="transactionManager" class="org.springframework.orm.hibernate3.HibernateTransactionManager">
<property name="sessionFactory" ref="sessionFactory" />
</bean>

sessionFactory 属性需要装配一个 Hibernate 的 session 工厂,HibernateTransactionManager 的实现细节是它将事务管理的职责委托给 org.hibernate.Transaction 对象,而后者是从 Hibernate Session 中获取到的。当事务成功完成时,HibernateTransactionManager 将会调用 Transaction 对象的 commit() 方法,反之,将会调用 rollback() 方法。

Java 持久化 API 事务(JPA)

Hibernate 多年来一直是事实上的 Java 持久化标准,但是现在 Java 持久化 API 作为真正的 Java 持久化标准进入大家的视野。如果你计划使用 JPA 的话,那你需要使用 Spring 的 JpaTransactionManager 来处理事务。你需要在 Spring 中这样配置 JpaTransactionManager

1
2
3
<bean id="transactionManager" class="org.springframework.orm.jpa.JpaTransactionManager">
<property name="sessionFactory" ref="sessionFactory" />
</bean>

JpaTransactionManager 只需要装配一个 JPA 实体管理工厂(javax.persistence.EntityManagerFactory 接口的任意实现)。JpaTransactionManager 将与由工厂所产生的 JPA EntityManager 合作来构建事务。

Java 原生 API 事务(JTA)

如果你没有使用以上所述的事务管理,或者是跨越了多个事务管理源(比如两个或者是多个不同的数据源),你就需要使用JtaTransactionManager

1
2
3
<bean id="transactionManager" class="org.springframework.transaction.jta.JtaTransactionManager">
<property name="transactionManagerName" value="java:/TransactionManager" />
</bean>

JtaTransactionManager 将事务管理的责任委托给 javax.transaction.UserTransactionjavax.transaction.TransactionManager 对象,其中事务成功完成通过 UserTransaction.commit() 方法提交,事务失败通过 UserTransaction.rollback() 方法回滚。

ReactiveTransactionManager

Spring 还为使用响应式类型或 Kotlin 协程的响应式应用程序提供了事务管理抽象。以下清单显示了 org.springframework.transaction.ReactiveTransactionManager 定义的事务策略:

1
2
3
4
5
6
7
8
public interface ReactiveTransactionManager extends TransactionManager {

Mono<ReactiveTransaction> getReactiveTransaction(TransactionDefinition definition) throws TransactionException;

Mono<Void> commit(ReactiveTransaction status) throws TransactionException;

Mono<Void> rollback(ReactiveTransaction status) throws TransactionException;
}

响应式事务管理器主要是一个 SPI,所以使用者可以以编程方式使用它。因为 ReactiveTransactionManager 是一个接口,所以可以根据需要轻松地 MOCK 或存根。

TransactionDefinition

PlatformTransactionManager 通过 getTransaction(TransactionDefinition definition) 方法来得到事务,这个方法里面的参数是 TransactionDefinition 类,这个类就定义了一些基本的事务属性。事务属性可以理解成事务的一些基本配置,描述了事务策略如何应用到方法上。

TransactionDefinition 接口内容如下:

1
2
3
4
5
6
public interface TransactionDefinition {
int getPropagationBehavior(); // 返回事务的传播行为
int getIsolationLevel(); // 返回事务的隔离级别,事务管理器根据它来控制另外一个事务可以看到本事务内的哪些数据
int getTimeout(); // 返回事务必须在多少秒内完成
boolean isReadOnly(); // 事务是否只读,事务管理器能够根据这个返回值进行优化,确保事务是只读的
}

我们可以发现 TransactionDefinition 正好用来定义事务属性,下面详细介绍一下各个事务属性。

传播行为

事务的传播行为(propagation behavior)是指:当事务方法被另一个事务方法调用时,必须指定事务应该如何传播。例如:方法可能继续在现有事务中运行,也可能开启一个新事务,并在自己的事务中运行。Spring 定义了七种传播行为:

传播行为 含义
PROPAGATION_REQUIRED 表示当前方法必须运行在事务中。如果当前事务存在,方法将会在该事务中运行。否则,会启动一个新的事务
PROPAGATION_SUPPORTS 表示当前方法不需要事务上下文,但是如果存在当前事务的话,那么该方法会在这个事务中运行
PROPAGATION_MANDATORY 表示该方法必须在事务中运行,如果当前事务不存在,则会抛出一个异常
PROPAGATION_REQUIRED_NEW 表示当前方法必须运行在它自己的事务中。一个新的事务将被启动。如果存在当前事务,在该方法执行期间,当前事务会被挂起。如果使用 JTATransactionManager 的话,则需要访问 TransactionManager
PROPAGATION_NOT_SUPPORTED 表示该方法不应该运行在事务中。如果存在当前事务,在该方法运行期间,当前事务将被挂起。如果使用 JTATransactionManager 的话,则需要访问 TransactionManager
PROPAGATION_NEVER 表示当前方法不应该运行在事务上下文中。如果当前正有一个事务在运行,则会抛出异常
PROPAGATION_NESTED 表示如果当前已经存在一个事务,那么该方法将会在嵌套事务中运行。嵌套的事务可以独立于当前事务进行单独地提交或回滚。如果当前事务不存在,那么其行为与 PROPAGATION_REQUIRED 一样。注意各厂商对这种传播行为的支持是有所差异的。可以参考资源管理器的文档来确认它们是否支持嵌套事务

注:以下具体讲解传播行为的内容参考自 Spring 事务机制详解

  1. PROPAGATION_REQUIRED 如果存在一个事务,则支持当前事务。如果没有事务则开启一个新的事务。
1
2
3
4
5
6
// 事务属性 PROPAGATION_REQUIRED
methodA {
……
methodB();
……
}
1
2
3
4
// 事务属性 PROPAGATION_REQUIRED
methodB {
……
}

使用 spring 声明式事务,spring 使用 AOP 来支持声明式事务,会根据事务属性,自动在方法调用之前决定是否开启一个事务,并在方法执行之后决定事务提交或回滚事务。

单独调用 methodB 方法:

1
2
3
main {
metodB();
}

相当于

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
Main {
Connection con=null;
try{
con = getConnection();
con.setAutoCommit(false);

//方法调用
methodB();

//提交事务
con.commit();
} Catch(RuntimeException ex) {
//回滚事务
con.rollback();
} finally {
//释放资源
closeCon();
}
}

Spring 保证在 methodB 方法中所有的调用都获得到一个相同的连接。在调用 methodB 时,没有一个存在的事务,所以获得一个新的连接,开启了一个新的事务。
单独调用 MethodA 时,在 MethodA 内又会调用 MethodB.

执行效果相当于:

1
2
3
4
5
6
7
8
9
10
11
12
main{
Connection con = null;
try{
con = getConnection();
methodA();
con.commit();
} catch(RuntimeException ex) {
con.rollback();
} finally {
closeCon();
}
}

调用 MethodA 时,环境中没有事务,所以开启一个新的事务.当在 MethodA 中调用 MethodB 时,环境中已经有了一个事务,所以 methodB 就加入当前事务。

  1. PROPAGATION_SUPPORTS 如果存在一个事务,支持当前事务。如果没有事务,则非事务的执行。但是对于事务同步的事务管理器,PROPAGATION_SUPPORTS 与不使用事务有少许不同。
1
2
3
4
5
6
7
8
9
//事务属性 PROPAGATION_REQUIRED
methodA(){
methodB();
}

//事务属性 PROPAGATION_SUPPORTS
methodB(){
……
}

单纯的调用 methodB 时,methodB 方法是非事务的执行的。当调用 methdA 时,methodB 则加入了 methodA 的事务中,事务地执行。

  1. PROPAGATION_MANDATORY 如果已经存在一个事务,支持当前事务。如果没有一个活动的事务,则抛出异常。
1
2
3
4
5
6
7
8
9
//事务属性 PROPAGATION_REQUIRED
methodA(){
methodB();
}

//事务属性 PROPAGATION_MANDATORY
methodB(){
……
}

当单独调用 methodB 时,因为当前没有一个活动的事务,则会抛出异常 throw new IllegalTransactionStateException(“Transaction propagation ‘mandatory’ but no existing transaction found”);当调用 methodA 时,methodB 则加入到 methodA 的事务中,事务地执行。

  1. PROPAGATION_REQUIRES_NEW 总是开启一个新的事务。如果一个事务已经存在,则将这个存在的事务挂起。
1
2
3
4
5
6
7
8
9
10
11
//事务属性 PROPAGATION_REQUIRED
methodA(){
doSomeThingA();
methodB();
doSomeThingB();
}

//事务属性 PROPAGATION_REQUIRES_NEW
methodB(){
……
}

调用 A 方法:

1
2
3
main(){
methodA();
}

相当于

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
main(){
TransactionManager tm = null;
try{
//获得一个JTA事务管理器
tm = getTransactionManager();
tm.begin();//开启一个新的事务
Transaction ts1 = tm.getTransaction();
doSomeThing();
tm.suspend();//挂起当前事务
try{
tm.begin();//重新开启第二个事务
Transaction ts2 = tm.getTransaction();
methodB();
ts2.commit();//提交第二个事务
} Catch(RunTimeException ex) {
ts2.rollback();//回滚第二个事务
} finally {
//释放资源
}
//methodB执行完后,恢复第一个事务
tm.resume(ts1);
doSomeThingB();
ts1.commit();//提交第一个事务
} catch(RunTimeException ex) {
ts1.rollback();//回滚第一个事务
} finally {
//释放资源
}
}

在这里,我把 ts1 称为外层事务,ts2 称为内层事务。从上面的代码可以看出,ts2 与 ts1 是两个独立的事务,互不相干。Ts2 是否成功并不依赖于 ts1。如果 methodA 方法在调用 methodB 方法后的 doSomeThingB 方法失败了,而 methodB 方法所做的结果依然被提交。而除了 methodB 之外的其它代码导致的结果却被回滚了。使用 PROPAGATION_REQUIRES_NEW,需要使用 JtaTransactionManager 作为事务管理器。

  1. PROPAGATION_NOT_SUPPORTED 总是非事务地执行,并挂起任何存在的事务。使用 PROPAGATION_NOT_SUPPORTED,也需要使用 JtaTransactionManager 作为事务管理器。(代码示例同上,可同理推出)
  2. PROPAGATION_NEVER 总是非事务地执行,如果存在一个活动事务,则抛出异常。
  3. PROPAGATION_NESTED 如果一个活动的事务存在,则运行在一个嵌套的事务中. 如果没有活动事务, 则按 TransactionDefinition.PROPAGATION_REQUIRED 属性执行。这是一个嵌套事务,使用 JDBC 3.0 驱动时,仅仅支持 DataSourceTransactionManager 作为事务管理器。需要 JDBC 驱动的 java.sql.Savepoint 类。有一些 JTA 的事务管理器实现可能也提供了同样的功能。使用 PROPAGATION_NESTED,还需要把 PlatformTransactionManager 的 nestedTransactionAllowed 属性设为 true;而 nestedTransactionAllowed 属性值默认为 false。
1
2
3
4
5
6
7
8
9
10
11
//事务属性 PROPAGATION_REQUIRED
methodA(){
doSomeThingA();
methodB();
doSomeThingB();
}

//事务属性 PROPAGATION_NESTED
methodB(){
……
}

如果单独调用 methodB 方法,则按 REQUIRED 属性执行。如果调用 methodA 方法,相当于下面的效果:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
main(){
Connection con = null;
Savepoint savepoint = null;
try{
con = getConnection();
con.setAutoCommit(false);
doSomeThingA();
savepoint = con2.setSavepoint();
try{
methodB();
} catch(RuntimeException ex) {
con.rollback(savepoint);
} finally {
//释放资源
}
doSomeThingB();
con.commit();
} catch(RuntimeException ex) {
con.rollback();
} finally {
//释放资源
}
}

当 methodB 方法调用之前,调用 setSavepoint 方法,保存当前的状态到 savepoint。如果 methodB 方法调用失败,则恢复到之前保存的状态。但是需要注意的是,这时的事务并没有进行提交,如果后续的代码(doSomeThingB()方法)调用失败,则回滚包括 methodB 方法的所有操作。

嵌套事务一个非常重要的概念就是内层事务依赖于外层事务。外层事务失败时,会回滚内层事务所做的动作。而内层事务操作失败并不会引起外层事务的回滚。

PROPAGATION_NESTED 与 PROPAGATION_REQUIRES_NEW 的区别:它们非常类似,都像一个嵌套事务,如果不存在一个活动的事务,都会开启一个新的事务。使用 PROPAGATION_REQUIRES_NEW 时,内层事务与外层事务就像两个独立的事务一样,一旦内层事务进行了提交后,外层事务不能对其进行回滚。两个事务互不影响。两个事务不是一个真正的嵌套事务。同时它需要 JTA 事务管理器的支持。

使用 PROPAGATION_NESTED 时,外层事务的回滚可以引起内层事务的回滚。而内层事务的异常并不会导致外层事务的回滚,它是一个真正的嵌套事务。DataSourceTransactionManager 使用 savepoint 支持 PROPAGATION_NESTED 时,需要 JDBC 3.0 以上驱动及 1.4 以上的 JDK 版本支持。其它的 JTA TrasactionManager 实现可能有不同的支持方式。

PROPAGATION_REQUIRES_NEW 启动一个新的, 不依赖于环境的 “内部” 事务. 这个事务将被完全 commited 或 rolled back 而不依赖于外部事务, 它拥有自己的隔离范围, 自己的锁, 等等. 当内部事务开始执行时, 外部事务将被挂起, 内务事务结束时, 外部事务将继续执行。

另一方面, PROPAGATION_NESTED 开始一个 “嵌套的” 事务, 它是已经存在事务的一个真正的子事务. 潜套事务开始执行时, 它将取得一个 savepoint. 如果这个嵌套事务失败, 我们将回滚到此 savepoint. 潜套事务是外部事务的一部分, 只有外部事务结束后它才会被提交。

由此可见, PROPAGATION_REQUIRES_NEW 和 PROPAGATION_NESTED 的最大区别在于, PROPAGATION_REQUIRES_NEW 完全是一个新的事务, 而 PROPAGATION_NESTED 则是外部事务的子事务, 如果外部事务 commit, 嵌套事务也会被 commit, 这个规则同样适用于 roll back.

PROPAGATION_REQUIRED 应该是我们首先的事务传播行为。它能够满足我们大多数的事务需求。

隔离级别

事务的第二个维度就是隔离级别(isolation level)。隔离级别定义了一个事务可能受其他并发事务影响的程度。

  1. 并发事务引起的问题

在典型的应用程序中,多个事务并发运行,经常会操作相同的数据来完成各自的任务。并发虽然是必须的,但可能会导致一下的问题。

  • 脏读(Dirty reads)——脏读发生在一个事务读取了另一个事务改写但尚未提交的数据时。如果改写在稍后被回滚了,那么第一个事务获取的数据就是无效的。
  • 不可重复读(Nonrepeatable read)——不可重复读发生在一个事务执行相同的查询两次或两次以上,但是每次都得到不同的数据时。这通常是因为另一个并发事务在两次查询期间进行了更新。
  • 幻读(Phantom read)——幻读与不可重复读类似。它发生在一个事务(T1)读取了几行数据,接着另一个并发事务(T2)插入了一些数据时。在随后的查询中,第一个事务(T1)就会发现多了一些原本不存在的记录。

不可重复读与幻读的区别

不可重复读的重点是修改:
同样的条件, 你读取过的数据, 再次读取出来发现值不一样了
例如:在事务 1 中,Mary 读取了自己的工资为 1000,操作并没有完成

1
2
con1 = getConnection();
select salary from employee empId ="Mary";

在事务 2 中,这时财务人员修改了 Mary 的工资为 2000,并提交了事务.

1
2
3
con2 = getConnection();
update employee set salary = 2000;
con2.commit();

在事务 1 中,Mary 再次读取自己的工资时,工资变为了 2000

1
2
//con1
select salary from employee empId ="Mary";

在一个事务中前后两次读取的结果并不一致,导致了不可重复读。

幻读的重点在于新增或者删除:
同样的条件, 第 1 次和第 2 次读出来的记录数不一样
例如:目前工资为 1000 的员工有 10 人。事务 1,读取所有工资为 1000 的员工。

1
2
con1 = getConnection();
Select * from employee where salary =1000;

共读取 10 条记录

这时另一个事务向 employee 表插入了一条员工记录,工资也为 1000

1
2
3
con2 = getConnection();
Insert into employee(empId,salary) values("Lili",1000);
con2.commit();

事务 1 再次读取所有工资为 1000 的员工

1
2
//con1
select * from employee where salary =1000;

共读取到了 11 条记录,这就产生了幻像读。

从总的结果来看, 似乎不可重复读和幻读都表现为两次读取的结果不一致。但如果你从控制的角度来看, 两者的区别就比较大。
对于前者, 只需要锁住满足条件的记录。
对于后者, 要锁住满足条件及其相近的记录。

  1. 隔离级别
隔离级别 含义
ISOLATION_DEFAULT 使用后端数据库默认的隔离级别
ISOLATION_READ_UNCOMMITTED 最低的隔离级别,允许读取尚未提交的数据变更,可能会导致脏读、幻读或不可重复读
ISOLATION_READ_COMMITTED 允许读取并发事务已经提交的数据,可以阻止脏读,但是幻读或不可重复读仍有可能发生
ISOLATION_REPEATABLE_READ 对同一字段的多次读取结果都是一致的,除非数据是被本身事务自己所修改,可以阻止脏读和不可重复读,但幻读仍有可能发生
ISOLATION_SERIALIZABLE 最高的隔离级别,完全服从 ACID 的隔离级别,确保阻止脏读、不可重复读以及幻读,也是最慢的事务隔离级别,因为它通常是通过完全锁定事务相关的数据库表来实现的

只读

事务的第三个特性是它是否为只读事务。如果事务只对后端的数据库进行该操作,数据库可以利用事务的只读特性来进行一些特定的优化。通过将事务设置为只读,你就可以给数据库一个机会,让它应用它认为合适的优化措施。

事务超时

为了使应用程序很好地运行,事务不能运行太长的时间。因为事务可能涉及对后端数据库的锁定,所以长时间的事务会不必要的占用数据库资源。事务超时就是事务的一个定时器,在特定时间内事务如果没有执行完毕,那么就会自动回滚,而不是一直等待其结束。

回滚规则

事务五边形的最后一个方面是一组规则,这些规则定义了哪些异常会导致事务回滚而哪些不会。默认情况下,事务只有遇到运行期异常时才会回滚,而在遇到检查型异常时不会回滚(这一行为与 EJB 的回滚行为是一致的)
但是你可以声明事务在遇到特定的检查型异常时像遇到运行期异常那样回滚。同样,你还可以声明事务遇到特定的异常不回滚,即使这些异常是运行期异常。

TransactionStatus

TransactionStatus 接口为事务代码提供了一种简单的方式来控制事务执行和查询事务状态。这些概念应该很熟悉,因为它们对所有事务 API 都是通用的。以下清单显示了 TransactionStatus 接口:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
public interface TransactionStatus extends TransactionExecution, SavepointManager, Flushable {

@Override
boolean isNewTransaction();

boolean hasSavepoint();

@Override
void setRollbackOnly();

@Override
boolean isRollbackOnly();

void flush();

@Override
boolean isCompleted();
}

可以发现这个接口描述的是一些处理事务提供简单的控制事务执行和查询事务状态的方法,在回滚或提交的时候需要应用对应的事务状态。

TransactionTemplate

Spring 提供了对编程式事务和声明式事务的支持。编程式事务允许用户在代码中精确定义事务的边界,而声明式事务(基于 AOP)有助于用户将操作与事务规则进行解耦。TransactionTemplate 就是用于支持编程式事务的核心 API。

采用 TransactionTemplate 和采用其他 Spring 模板,如 JdbcTempalte 和 HibernateTemplate 是一样的方法。它使用回调方法,把应用程序从处理取得和释放资源中解脱出来。如同其他模板,TransactionTemplate 是线程安全的。代码片段:

1
2
3
4
5
6
7
8
TransactionTemplate tt = new TransactionTemplate(); // 新建一个TransactionTemplate
Object result = tt.execute(
new TransactionCallback(){
public Object doTransaction(TransactionStatus status){
updateOperation();
return resultOfUpdateOperation();
}
}); // 执行execute方法进行事务管理

使用 TransactionCallback()可以返回一个值。如果使用 TransactionCallbackWithoutResult 则没有返回值。

声明式事务管理

大多数 Spring 用户选择声明式事务管理。此选项对应用程序代码的影响最小,因此最符合非侵入式轻量级容器的理想。

Spring 框架的声明式事务管理是通过 Spring AOP 实现的。然而,由于事务方面代码随 Spring 发行版一起提供并且可以以样板方式使用,因此通常不必理解 AOP 概念即可有效地使用此代码。

Spring 框架的声明式事务管理类似于 EJB CMT,因为您可以指定事务行为(或缺少它)到单个方法级别。如有必要,您可以在事务上下文中进行 setRollbackOnly() 调用。两种类型的事务管理之间的区别是:

  • 与绑定到 JTA 的 EJB CMT 不同,Spring 框架的声明式事务管理适用于任何环境。通过调整配置文件,它可以使用 JDBC、JPA 或 Hibernate 处理 JTA 事务或本地事务。
  • 您可以将 Spring 声明式事务管理应用于任何类,而不仅仅是诸如 EJB 之类的特殊类。
  • Spring 提供声明性回滚规则,这是一个没有 EJB 等效功能的特性。提供了对回滚规则的编程和声明性支持。
  • Spring 允许您使用 AOP 自定义事务行为。例如,您可以在事务回滚的情况下插入自定义行为。您还可以添加任意 advice 以及事务性 advice。使用 EJB CMT,您无法影响容器的事务管理,除非使用 setRollbackOnly()
  • Spring 不像高端应用服务器那样支持跨远程调用传播事务上下文。如果您需要此功能,我们建议您使用 EJB。但是,在使用这种特性之前要仔细考虑,因为通常情况下,不希望事务跨越远程调用。

回滚规则的概念很重要。它们让您指定哪些异常(和 throwable)应该导致自动回滚。您可以在配置中以声明方式指定它,而不是在 Java 代码中。因此,尽管您仍然可以在 TransactionStatus 对象上调用 setRollbackOnly() 来回滚当前事务,但通常您可以指定 MyApplicationException 必须始终导致回滚的规则。此选项的显着优势是业务对象不依赖于事务基础架构。例如,它们通常不需要导入 Spring 事务 API 或其他 Spring API。

尽管 EJB 容器默认行为会在系统异常(通常是运行时异常)上自动回滚事务,但 EJB CMT 不会在应用程序异常(即除 java.rmi.RemoteException 之外的检查异常)上自动回滚事务。虽然声明式事务管理的 Spring 默认行为遵循 EJB 约定(回滚仅在未经检查的异常上自动),但自定义此行为通常很有用。

Spring 声明式事务管理的实现

关于 Spring 框架的声明式事务支持,最重要的概念是这种支持是通过 AOP 代理启用的,并且事务 advice 是由元数据驱动的(目前是基于 XML 或基于注释的)。 AOP 与事务元数据的结合产生了一个 AOP 代理,它使用 TransactionInterceptor 和适当的 TransactionManager 实现来驱动围绕方法调用的事务。

Spring 的 TransactionInterceptor 为命令式和响应式编程模型提供事务管理。拦截器通过检查方法返回类型来检测所需的事务管理风格。返回响应式类型的方法,例如 Publisher 或 Kotlin Flow(或它们的子类型)有资格进行响应式事务管理。包括 void 在内的所有其他返回类型都使用代码路径进行命令式事务管理。

事务管理风格会影响需要哪个事务管理器。命令式事务需要 PlatformTransactionManager,而响应式事务使用 ReactiveTransactionManager 实现。

@Transactional 通常与 PlatformTransactionManager 管理的线程绑定事务一起使用,将事务公开给当前执行线程中的所有数据访问操作。注意:这不会传播到方法中新启动的线程。

ReactiveTransactionManager 管理的反应式事务使用 Reactor 上下文而不是线程本地属性。因此,所有参与的数据访问操作都需要在同一个反应式管道中的同一个 Reactor 上下文中执行。

下图显示了在事务代理上调用方法的概念视图:

声明式事务示例

考虑以下接口及其伴随的实现。此示例使用 Foo 和 Bar 类作为占位符,以便您可以专注于事务使用,而无需关注特定的域模型。就本示例而言,DefaultFooService 类在每个已实现方法的主体中抛出 UnsupportedOperationException 实例这一事实很好。该行为使您可以看到正在创建的事务,然后回滚以响应 UnsupportedOperationException 实例。

以下清单显示了 FooService 接口:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// the service interface that we want to make transactional

package x.y.service;

public interface FooService {

Foo getFoo(String fooName);

Foo getFoo(String fooName, String barName);

void insertFoo(Foo foo);

void updateFoo(Foo foo);

}

以下示例显示了上述接口的实现:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
package x.y.service;

public class DefaultFooService implements FooService {

@Override
public Foo getFoo(String fooName) {
// ...
}

@Override
public Foo getFoo(String fooName, String barName) {
// ...
}

@Override
public void insertFoo(Foo foo) {
// ...
}

@Override
public void updateFoo(Foo foo) {
// ...
}
}

假设 FooService 接口的前两个方法 getFoo(String) 和 getFoo(String, String) 必须在具有只读语义的事务上下文中运行,并且其他方法 insertFoo(Foo) 和 updateFoo(Foo ),必须在具有读写语义的事务上下文中运行。以下配置将在接下来的几段中详细说明:

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
<!-- from the file 'context.xml' -->
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:aop="http://www.springframework.org/schema/aop"
xmlns:tx="http://www.springframework.org/schema/tx"
xsi:schemaLocation="
http://www.springframework.org/schema/beans
https://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/tx
https://www.springframework.org/schema/tx/spring-tx.xsd
http://www.springframework.org/schema/aop
https://www.springframework.org/schema/aop/spring-aop.xsd">

<!-- this is the service object that we want to make transactional -->
<bean id="fooService" class="x.y.service.DefaultFooService"/>

<!-- the transactional advice (what 'happens'; see the <aop:advisor/> bean below) -->
<tx:advice id="txAdvice" transaction-manager="txManager">
<!-- the transactional semantics... -->
<tx:attributes>
<!-- all methods starting with 'get' are read-only -->
<tx:method name="get*" read-only="true"/>
<!-- other methods use the default transaction settings (see below) -->
<tx:method name="*"/>
</tx:attributes>
</tx:advice>

<!-- ensure that the above transactional advice runs for any execution
of an operation defined by the FooService interface -->
<aop:config>
<aop:pointcut id="fooServiceOperation" expression="execution(* x.y.service.FooService.*(..))"/>
<aop:advisor advice-ref="txAdvice" pointcut-ref="fooServiceOperation"/>
</aop:config>

<!-- don't forget the DataSource -->
<bean id="dataSource" class="org.apache.commons.dbcp.BasicDataSource" destroy-method="close">
<property name="driverClassName" value="oracle.jdbc.driver.OracleDriver"/>
<property name="url" value="jdbc:oracle:thin:@rj-t42:1521:elvis"/>
<property name="username" value="scott"/>
<property name="password" value="tiger"/>
</bean>

<!-- similarly, don't forget the TransactionManager -->
<bean id="txManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
<property name="dataSource" ref="dataSource"/>
</bean>

<!-- other <bean/> definitions here -->

</beans>

检查前面的配置。它假定您要使服务对象 fooService bean 具有事务性。要应用的事务语义封装在 <tx:advice/> 定义中。<tx:advice/> 定义读作“所有以 get 开头的方法都将在只读事务的上下文中运行,所有其他方法都将以默认事务语义运行”。<tx:advice/> 标签的 transaction-manager 属性设置为将驱动事务的 TransactionManager bean 的名称(在本例中为 txManager bean)。

如果要连接的 TransactionManager 的 bean 名称具有名称 transactionManager,则可以省略事务 advice (tx:advice/) 中的 transaction-manager 属性。如果要连接的 TransactionManager bean 有任何其他名称,则必须显式使用 transaction-manager 属性,如前面的示例所示。

<aop:config/> 定义确保由 txAdvice bean 定义的事务性建议在程序中的适当位置运行。首先,您定义一个切入点,该切入点与 FooService 接口 (fooServiceOperation) 中定义的任何操作的执行相匹配。然后,您使用一个 adviser 将切入点与 txAdvice 相关联。结果表明,在执行 fooServiceOperation 时,会运行 txAdvice 定义的建议。

一个常见的要求是使整个服务层具有事务性。最好的方法是更改切入点表达式以匹配服务层中的任何操作。以下示例显示了如何执行此操作:

1
2
3
4
<aop:config>
<aop:pointcut id="fooServiceMethods" expression="execution(* x.y.service.*.*(..))"/>
<aop:advisor advice-ref="txAdvice" pointcut-ref="fooServiceMethods"/>
</aop:config>

前面显示的配置用于围绕从 fooService bean 定义创建的对象创建事务代理。代理配置了事务 advice,以便在代理上调用适当的方法时,根据与该方法关联的事务配置,启动、暂停、标记为只读等事务。考虑以下测试驱动前面显示的配置的程序:

1
2
3
4
5
6
7
8
public final class Boot {

public static void main(final String[] args) throws Exception {
ApplicationContext ctx = new ClassPathXmlApplicationContext("context.xml");
FooService fooService = ctx.getBean(FooService.class);
fooService.insertFoo(new Foo());
}
}

回滚一个声明性事务

Spring 框架中,触发事务回滚的推荐方式是在事务上下文的代码中抛出异常。Spring 事务框架会捕获任何未处理的异常,并确定是否将事务标记为回滚。

在其默认配置中,Spring 事务框架只会将存在运行时且未经检查异常的事务标记为回滚。也就是说,当抛出的异常是 RuntimeException 的实例或子类时。 (默认情况下,错误实例也会导致回滚)。从事务方法抛出的检查异常不会导致默认配置中的回滚。

您可以通过指定回滚规则,明确指定哪些异常类型将导致事务回滚。

回滚规则约定在抛出指定异常时是否应回滚事务,并且规则基于模式。模式可以是完全限定的类名或异常类型的完全限定类名的子字符串(必须是 Throwable 的子类),目前不支持通配符。例如,javax.servlet.ServletExceptionServletException 的值将匹配 javax.servlet.ServletException 及其子类。

回滚规则可以通过 rollback-forno-rollback-for 属性在 XML 中配置,这允许将模式指定为字符串。使用 @Transactional 时,可以通过 rollbackFor / noRollbackForrollbackForClassName / noRollbackForClassName 属性配置回滚规则,它们允许将模式分别指定为类引用或字符串。当异常类型被指定为类引用时,其完全限定名称将用作模式。因此,@Transactional(rollbackFor = example.CustomException.class) 等价于 @Transactional(rollbackForClassName = 'example.CustomException')

以下 XML 片段演示了如何通过 rollback-for 属性提供异常模式来为已检查的、特定的 Exception 类型配置回滚:

1
2
3
4
5
6
<tx:advice id="txAdvice" transaction-manager="txManager">
<tx:attributes>
<tx:method name="get*" read-only="true" rollback-for="NoProductInStockException"/>
<tx:method name="*"/>
</tx:attributes>
</tx:advice>

如果您不希望在抛出异常时回滚事务,您还可以指定“不回滚”规则。下面的例子告诉 Spring 事务框架,即使在面对未处理的 InstrumentNotFoundException 时也要提交伴随事务。

1
2
3
4
5
6
<tx:advice id="txAdvice">
<tx:attributes>
<tx:method name="updateStock" no-rollback-for="InstrumentNotFoundException"/>
<tx:method name="*"/>
</tx:attributes>
</tx:advice>

当 Spring Framework 事务框架捕获到异常,并检查配置的回滚规则以确定是否将事务标记为回滚时,由最重要的匹配规则决定。因此,在以下配置的情况下,除 InstrumentNotFoundException 之外的任何异常都会导致伴随事务的回滚。

1
2
3
4
5
<tx:advice id="txAdvice">
<tx:attributes>
<tx:method name="*" rollback-for="Throwable" no-rollback-for="InstrumentNotFoundException"/>
</tx:attributes>
</tx:advice>

您还可以以编程方式指示所需的回滚。虽然很简单,但这个过程非常具有侵入性,并且将您的代码与 Spring Framework 的事务基础设施紧密耦合。以下示例显示如何以编程方式指示所需的回滚。

1
2
3
4
5
6
7
8
public void resolvePosition() {
try {
// some business logic...
} catch (NoProductInStockException ex) {
// trigger rollback programmatically
TransactionAspectSupport.currentTransactionStatus().setRollbackOnly();
}
}

如果可能的话,强烈建议您使用声明性方法进行回滚。如果您绝对需要,可以使用程序化回滚,但它的使用与实现干净的基于 POJO 的架构背道而驰。

为不同的 Bean 配置不同的事务语义

考虑您有许多服务层对象的场景,并且您希望对每个对象应用完全不同的事务配置。您可以通过定义具有不同 <aop:advisor/> 元素和不同 advice-ref 属性值的切点来实现这一点。

作为一个比较点,首先假设您的所有服务层类都定义在根 x.y.service 包中。 要使作为该包(或子包)中定义的类的实例并且名称以 Service 结尾的所有 bean 都具有默认的事务配置,您可以编写以下内容:

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
<?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"
xmlns:tx="http://www.springframework.org/schema/tx"
xsi:schemaLocation="
http://www.springframework.org/schema/beans
https://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/tx
https://www.springframework.org/schema/tx/spring-tx.xsd
http://www.springframework.org/schema/aop
https://www.springframework.org/schema/aop/spring-aop.xsd">

<aop:config>

<aop:pointcut id="serviceOperation"
expression="execution(* x.y.service..*Service.*(..))"/>

<aop:advisor pointcut-ref="serviceOperation" advice-ref="txAdvice"/>

</aop:config>

<!-- these two beans will be transactional... -->
<bean id="fooService" class="x.y.service.DefaultFooService"/>
<bean id="barService" class="x.y.service.extras.SimpleBarService"/>

<!-- ... and these two beans won't -->
<bean id="anotherService" class="org.xyz.SomeService"/> <!-- (not in the right package) -->
<bean id="barManager" class="x.y.service.SimpleBarManager"/> <!-- (doesn't end in 'Service') -->

<tx:advice id="txAdvice">
<tx:attributes>
<tx:method name="get*" read-only="true"/>
<tx:method name="*"/>
</tx:attributes>
</tx:advice>

<!-- other transaction infrastructure beans such as a TransactionManager omitted... -->

</beans>

以下示例显示了如何使用完全不同的事务设置配置两个不同的 bean

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
<?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"
xmlns:tx="http://www.springframework.org/schema/tx"
xsi:schemaLocation="
http://www.springframework.org/schema/beans
https://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/tx
https://www.springframework.org/schema/tx/spring-tx.xsd
http://www.springframework.org/schema/aop
https://www.springframework.org/schema/aop/spring-aop.xsd">

<aop:config>

<aop:pointcut id="defaultServiceOperation"
expression="execution(* x.y.service.*Service.*(..))"/>

<aop:pointcut id="noTxServiceOperation"
expression="execution(* x.y.service.ddl.DefaultDdlManager.*(..))"/>

<aop:advisor pointcut-ref="defaultServiceOperation" advice-ref="defaultTxAdvice"/>

<aop:advisor pointcut-ref="noTxServiceOperation" advice-ref="noTxAdvice"/>

</aop:config>

<!-- this bean will be transactional (see the 'defaultServiceOperation' pointcut) -->
<bean id="fooService" class="x.y.service.DefaultFooService"/>

<!-- this bean will also be transactional, but with totally different transactional settings -->
<bean id="anotherFooService" class="x.y.service.ddl.DefaultDdlManager"/>

<tx:advice id="defaultTxAdvice">
<tx:attributes>
<tx:method name="get*" read-only="true"/>
<tx:method name="*"/>
</tx:attributes>
</tx:advice>

<tx:advice id="noTxAdvice">
<tx:attributes>
<tx:method name="*" propagation="NEVER"/>
</tx:attributes>
</tx:advice>

<!-- other transaction infrastructure beans such as a TransactionManager omitted... -->

</beans>

<tx:advice/> 配置

<tx:advice/> 的默认配置为:

  • 传播设置是 REQUIRED

  • 隔离级别为 DEFAULT

  • 事务是 read-write

  • 事务超时默认为底层事务系统的默认超时,如果不支持超时,则为无。

  • 任何 RuntimeException 都会触发回滚,而任何已检查的 Exception 都不会

<tx:advice/> 配置属性

属性 是否必要 默认值 描述
name Yes 与事务属性关联的方法名称。支持通配符,如:get*handle*on*Event
propagation No REQUIRED 事务传播行为
isolation No DEFAULT 事务隔离级别。仅适用于 REQUIREDREQUIRES_NEW 的传播设置。
timeout No -1 事务超时时间(单位:秒)。仅适用于 REQUIREDREQUIRES_NEW 的传播设置。
read-only No false read-write 或 read-only 事务。
rollback-for No 触发回滚的 Exception 实例列表(通过逗号分隔)。
no-rollback-for No 不触发回滚的 Exception 实例列表(通过逗号分隔)。

使用 @Transactional 注解

除了基于 XML 的声明式事务配置方法之外,您还可以使用基于注解的方法。

下面是一个使用 @Transactional 注解的示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
@Transactional
public class DefaultFooService implements FooService {

@Override
public Foo getFoo(String fooName) {
// ...
}

@Override
public Foo getFoo(String fooName, String barName) {
// ...
}

@Override
public void insertFoo(Foo foo) {
// ...
}

@Override
public void updateFoo(Foo foo) {
// ...
}
}

如上所述在类级别使用,@Transactional 注解表明声明类(及其子类)的所有方法都使用默认事务配置。 或者,可以单独为每个方法指定注解。请注意,类级别的注解不适用于类层次结构中的祖先类; 在这种情况下,继承的方法需要在本地重新声明才能参与子类级别的注解。

当上面的 POJO 类在 Spring 上下文中定义为 bean 时,您可以通过 @Configuration 类中的 @EnableTransactionManagement 注解使 bean 实例具有事务性。

在 XML 配置中, <tx:annotation-driven/> 标签提供了类似的便利:

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
<!-- from the file 'context.xml' -->
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:aop="http://www.springframework.org/schema/aop"
xmlns:tx="http://www.springframework.org/schema/tx"
xsi:schemaLocation="
http://www.springframework.org/schema/beans
https://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/tx
https://www.springframework.org/schema/tx/spring-tx.xsd
http://www.springframework.org/schema/aop
https://www.springframework.org/schema/aop/spring-aop.xsd">

<!-- this is the service object that we want to make transactional -->
<bean id="fooService" class="x.y.service.DefaultFooService"/>

<!-- enable the configuration of transactional behavior based on annotations -->
<!-- a TransactionManager is still required -->
<tx:annotation-driven transaction-manager="txManager"/>

<bean id="txManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
<!-- (this dependency is defined somewhere else) -->
<property name="dataSource" ref="dataSource"/>
</bean>

<!-- other <bean/> definitions here -->

</beans>

@Transactional 配置

Property Type Description
value String Optional qualifier that specifies the transaction manager to be used.
transactionManager String Alias for value.
label Array of String labels to add an expressive description to the transaction. Labels may be evaluated by transaction managers to associate implementation-specific behavior with the actual transaction.
propagation enum: Propagation Optional propagation setting.
isolation enum: Isolation Optional isolation level. Applies only to propagation values of REQUIRED or REQUIRES_NEW.
timeout int (in seconds of granularity) Optional transaction timeout. Applies only to propagation values of REQUIRED or REQUIRES_NEW.
timeoutString String (in seconds of granularity) Alternative for specifying the timeout in seconds as a String value — for example, as a placeholder.
readOnly boolean Read-write versus read-only transaction. Only applicable to values of REQUIRED or REQUIRES_NEW.
rollbackFor Array of Class objects, which must be derived from Throwable. Optional array of exception types that must cause rollback.
rollbackForClassName Array of exception name patterns. Optional array of exception name patterns that must cause rollback.
noRollbackFor Array of Class objects, which must be derived from Throwable. Optional array of exception types that must not cause rollback.
noRollbackForClassName Array of exception name patterns. Optional array of exception name patterns that must not cause rollback.

多事务管理器场景下使用 @Transactional

某些情况下,应用程序中可能需要接入多个数据源,相应的,也需要多个独立的事务管理器。使用者可以使用 @Transactional 注释的 value 或 transactionManager 属性来选择性地指定要使用的 TransactionManager 的标识。这可以是 bean 名称或事务管理器 bean 的限定符值。

1
2
3
4
5
6
7
8
9
10
11
public class TransactionalService {

@Transactional("order")
public void setSomething(String name) { ... }

@Transactional("account")
public void doSomething() { ... }

@Transactional("reactive-account")
public Mono<Void> doSomethingReactive() { ... }
}

下面展示如何定义 TransactionManager

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
<tx:annotation-driven/>

<bean id="transactionManager1" class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
...
<qualifier value="order"/>
</bean>

<bean id="transactionManager2" class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
...
<qualifier value="account"/>
</bean>

<bean id="transactionManager3" class="org.springframework.data.r2dbc.connectionfactory.R2dbcTransactionManager">
...
<qualifier value="reactive-account"/>
</bean>

在这种情况下,TransactionalService 上的各个方法在单独的事务管理器下运行,由 order、account 和 reactive-account 限定符区分。 如果没有找到明确指定的 TransactionManager bean,则仍使用默认的 <tx:annotation-driven> 目标 bean 名称。

自定义组合注解

如果您发现在许多不同的方法上重复使用 @Transactional 相同的属性,可以使用 Spring 的元注解自定义组合注解。

1
2
3
4
5
6
7
8
9
10
11
@Target({ElementType.METHOD, ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Transactional(transactionManager = "order", label = "causal-consistency")
public @interface OrderTx {
}

@Target({ElementType.METHOD, ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Transactional(transactionManager = "account", label = "retryable")
public @interface AccountTx {
}

使用示例:

1
2
3
4
5
6
7
8
9
10
11
12
public class TransactionalService {

@OrderTx
public void setSomething(String name) {
// ...
}

@AccountTx
public void doSomething() {
// ...
}
}

在上面的示例中,我们使用语法来定义事务管理器限定符和事务标签,但我们也可以包括传播行为、回滚规则、超时和其他特性。

事务传播

在 Spring 管理的事务中,请注意物理事务和逻辑事务之间的差异,以及传播设置如何应用于这种差异。

PROPAGATION_REQUIRED 强制执行物理事务,如果尚不存在事务,则在当前范围的本地执行或参与更大范围定义的现有“外部”事务。 这是同一线程内的常见调用堆栈安排中的一个很好的默认设置(例如,委托给多个存储库方法的服务外观,其中所有底层资源都必须参与服务级事务)。

当传播设置为 PROPAGATION_REQUIRED 时,将为应用该设置的每个方法创建一个逻辑事务范围。每个这样的逻辑事务范围可以单独确定仅回滚状态,外部事务范围在逻辑上独立于内部事务范围。在标准 PROPAGATION_REQUIRED 行为的情况下,所有这些范围都映射到同一个物理事务。因此,在内部事务范围内设置的仅回滚标记确实会影响外部事务实际提交的机会。

但是,在内部事务范围设置了仅回滚标记的情况下,外部事务尚未决定回滚本身,因此回滚(由内部事务范围静默触发)是意外的。此时会引发相应的 UnexpectedRollbackException。这是预期的行为,因此事务的调用者永远不会被误导以为执行了提交,而实际上并没有执行。因此,如果内部事务(外部调用者不知道)默默地将事务标记为仅回滚,外部调用者仍会调用提交。外部调用者需要接收 UnexpectedRollbackException 以清楚地指示执行了回滚。

PROPAGATION_REQUIRES_NEW 与 PROPAGATION_REQUIRED 相比,始终为每个受影响的事务范围使用独立的物理事务,从不参与外部范围的现有事务。 在这种安排下,底层资源事务是不同的,因此可以独立提交或回滚,外部事务不受内部事务回滚状态的影响,内部事务的锁在完成后立即释放。 这样一个独立的内部事务也可以声明自己的隔离级别、超时和只读设置,而不是继承外部事务的特性。

JDBC 异常抽象

Spring 会将数据操作的异常转换为 DataAccessException

Spring 是怎么认识那些错误码的

通过 SQLErrorCodeSQLExceptionTranslator 解析错误码

ErrorCode 定义(sql-error-codes.xml 文件)

Spring 事务最佳实践

img

Spring 事务未生效

使用 @Transactional 注解开启声明式事务时, 最容易忽略的问题是,很可能事务并没有生效。

@Transactional 生效原则:

@Transactional 方法必须是 public

原则一:除非特殊配置(比如使用 AspectJ 静态织入实现 AOP),否则只有定义在 public 方法上的 @Transactional 才能生效。原因是,Spring 默认通过动态代理的方式实现 AOP,对目标方法进行增强,private 方法无法代理到,Spring 自然也无法动态增强事务处理逻辑。

【示例】错误使用 @Transactional 案例一

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
@Transactional
void createUserPrivate(UserEntity entity) {
userRepository.save(entity);
if (entity.getName().contains("test")) { throw new RuntimeException("invalid username!"); }
}

//私有方法
public int createUserWrong1(String name) {
try {
this.createUserPrivate(new UserEntity(name));
} catch (Exception ex) {
log.error("create user failed because {}", ex.getMessage());
}
return userRepository.findByName(name).size();
}

当传入名为 test 的用户实体,会抛出异常,但 @Transactional 未生效,不会触发回滚。

必须通过 Spring 注入的 Bean 进行调用

原则二:必须通过代理过的类从外部调用目标方法才能生效

【示例】错误使用 @Transactional 案例二

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
//自调用
public int createUserWrong2(String name) {
try {
this.createUserPublic(new UserEntity(name));
} catch (Exception ex) {
log.error("create user failed because {}", ex.getMessage());
}
return userRepository.findByName(name).size();
}

//可以传播出异常
@Transactional
public void createUserPublic(UserEntity entity) {
userRepository.save(entity);
if (entity.getName().contains("test")) { throw new RuntimeException("invalid username!"); }
}

当传入名为 test 的用户实体,会抛出异常,但 @Transactional 未生效,不会触发回滚。

说明:Spring 通过 AOP 技术对方法进行字节码增强,要调用增强过的方法必然是调用代理后的对象。

事务虽然生效但未回滚

通过 AOP 实现事务处理可以理解为,使用 try…catch… 来包裹标记了 @Transactional 注解的方法,当方法出现了异常并且满足一定条件的时候,在 catch 里面我们可以设置事务回滚,没有异常则直接提交事务。

“一定条件”,主要包括两点:

第一,只有异常传播出了标记了 @Transactional 注解的方法,事务才能回滚。在 Spring 的 TransactionAspectSupport 里有个 invokeWithinTransaction 方法,里面就是处理事务的逻辑。

第二,默认情况下,出现 RuntimeException(非受检异常)或 Error 的时候,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
@Service
@Slf4j
public class UserService {

@Autowired
private UserRepository userRepository;

//异常无法传播出方法,导致事务无法回滚
@Transactional
public void createUserWrong1(String name) {
try {
userRepository.save(new UserEntity(name));
throw new RuntimeException("error");
} catch (Exception ex) {
log.error("create user failed", ex);
}
}

//即使出了受检异常也无法让事务回滚
@Transactional
public void createUserWrong2(String name) throws IOException {
userRepository.save(new UserEntity(name));
otherTask();
}

//因为文件不存在,一定会抛出一个IOException
private void otherTask() throws IOException {
Files.readAllLines(Paths.get("file-that-not-exist"));
}

}

在 createUserWrong1 方法中会抛出一个 RuntimeException,但由于方法内 catch 了所有异常,异常无法从方法传播出去,事务自然无法回滚。

在 createUserWrong2 方法中,注册用户的同时会有一次 otherTask 文件读取操作,如果文件读取失败,我们希望用户注册的数据库操作回滚。虽然这里没有捕获异常,但因为 otherTask 方法抛出的是受检异常,createUserWrong2 传播出去的也是受检异常,事务同样不会回滚。

【解决方案一】如果你希望自己捕获异常进行处理的话,也没关系,可以手动设置 TransactionAspectSupport.currentTransactionStatus().setRollbackOnly(); 让当前事务处于回滚状态

1
2
3
4
5
6
7
8
9
10
@Transactional
public void createUserRight1(String name) {
try {
userRepository.save(new UserEntity(name));
throw new RuntimeException("error");
} catch (Exception ex) {
log.error("create user failed", ex);
TransactionAspectSupport.currentTransactionStatus().setRollbackOnly();
}
}

【解决方案二】在注解中声明 @Transactional(rollbackFor = Exception.class),期望遇到所有的 Exception 都回滚事务(来突破默认不回滚受检异常的限制):

1
2
3
4
5
@Transactional(rollbackFor = Exception.class)
public void createUserRight2(String name) throws IOException {
userRepository.save(new UserEntity(name));
otherTask();
}

细化事务传播方式

如果方法涉及多次数据库操作,并希望将它们作为独立的事务进行提交或回滚,那么
我们需要考虑进一步细化配置事务传播方式,也就是 @Transactional 注解的 Propagation 属性。

1
2
3
4
5
6
7
8
9
/**
* {@link Propagation#REQUIRES_NEW} 表示执行到这个方法时需要开启新的事务,并挂起当前事务
*/
@Transactional(propagation = Propagation.REQUIRES_NEW)
public void createSubUserWithExceptionRight(UserEntity entity) {
log.info("createSubUserWithExceptionRight start");
userRepository.save(entity);
throw new RuntimeException("invalid status");
}

参考资料

数据库连接池

数据库连接池负责分配、管理和释放数据库连接,它允许应用程序重复使用一个现有的数据库连接,而不是再重新建立一个;释放空闲时间超过最大空闲时间的数据库连接来避免因为没有释放数据库连接而引起的数据库连接遗漏。这项技术能明显提高对数据库操作的性能。——摘自百度百科

什么是数据库连接池

数据库连接是一种关键的有限的昂贵的资源,这一点在多用户的网页应用程序中体现得尤为突出。 一个数据库连接对象均对应一个物理数据库连接,每次操作都打开一个物理连接,使用完都关闭连接,这样造成系统的 性能低下。 数据库连接池的解决方案是在应用程序启动时建立足够的数据库连接,并讲这些连接组成一个连接池(简单说:在一个“池”里放了好多半成品的数据库联接对象),由应用程序动态地对池中的连接进行申请、使用和释放。对于多于连接池中连接数的并发请求,应该在请求队列中排队等待。并且应用程序可以根据池中连接的使用率,动态增加或减少池中的连接数。 连接池技术尽可能多地重用了消耗内存地资源,大大节省了内存,提高了服务器地服务效率,能够支持更多的客户服务。通过使用连接池,将大大提高程序运行效率,同时,我们可以通过其自身的管理机制来监视数据库连接的数量、使用情况等。

为什么需要数据库连接池

不使用数据库连接池

不使用数据库连接池的步骤

  1. TCP 建立连接的三次握手
  2. MySQL 认证的三次握手
  3. 真正的 SQL 执行
  4. MySQL 的关闭
  5. TCP 的四次握手关闭

不使用数据库连接池的特性:

  • 优点:实现简单
  • 缺点
    • 网络 IO 较多
    • 数据库的负载较高
    • 响应时间较长及 QPS 较低
    • 应用频繁的创建连接和关闭连接,导致临时对象较多,GC 频繁
    • 在关闭连接后,会出现大量 TIME_WAIT 的 TCP 状态(在 2 个 MSL 之后关闭)

使用数据库连接池

使用数据库连接池的步骤:只有第一次访问的时候,需要建立连接。 但是之后的访问,均会复用之前创建的连接,直接执行 SQL 语句。

使用数据库连接池的优点

  • 减少了网络开销
  • 系统的性能会有一个实质的提升
  • 没有了 TIME_WAIT 状态

数据库连接池如何工作

数据库连接池工作的核心在于以下几点:

  1. 创建连接池:与线程池等池化对象类似,数据库连接池会在进程启动之初,根据配置初始化,并在池中创建了几个连接对象,以便使用时能从连接池中获取。连接池中的连接不能随意创建和关闭,以避免创建、关闭所带来的系统开销。

  2. 使用、管理连接池中:连接池管理策略是连接池机制的核心,连接池内连接的分配和释放对系统的性能有很大的影响。合理的策略可以保证数据库连接的有效复用,避免频繁的建立、释放连接所带来的系统资源开销。通常,数据库连接池的管理策略如下:

    1. 当请求数据库连接时,首先查看连接池中是否有空闲连接。
    2. 如果存在空闲连接,则将连接分配给客户使用。
    3. 如果没有空闲连接,则查看当前所开的连接数是否已经达到最大连接数。若未达到,就重新创建一个连接,并分配给请求的客户;如果达到,就按设定的最大等待时间进行等待,若超出最大等待时间,则抛出异常给客户。
    4. 当客户释放数据库连接时,先判断该连接的引用次数是否超过了规定值。如果超过,就从连接池中删除该连接;否则保留为其他客户服务。
  3. 关闭连接池:当应用程序退出时,关闭连接池中所有的连接,释放连接池相关的资源,该过程正好与创建相反。

数据库连接池的核心参数

使用数据库连接池,需要为其配置一些参数,以控制其工作。

通常,数据库连接池都会包含以下核心参数:

  • 最小连接数:是连接池一直保持的数据库连接,所以如果应用程序对数据库连接的使用量不大,将会有大量的数据库连接资源被浪费.
  • 最大连接数:是连接池能申请的最大连接数,如果数据库连接请求超过次数,后面的数据库连接请求将被加入到等待队列中,这会影响以后的数据库操作
  • 最大空闲时间
  • 获取连接超时时间
  • 超时重试连接次数

数据库连接池的问题

并发问题:为了保证连接管理服务具有最大的通用性,必须考虑多线程环境,即并发问题。

事务处理:我们知道,事务具有原子性,此时要求对数据库的操作符合“ALL-OR-NOTHING”原则,即对于一组 SQL 语句要么全做,要么全不做。我们知道当 2 个线程共用一个连接 Connection 对象,而且各自都有自己的事务要处理时候,对于连接池是一个很头疼的问题,因为即使 Connection 类提供了相应的事务支持,可是我们仍然不能确定那个数据库操作是对应那个事务的,这是由于我们有2个线程都在进行事务操作而引起的。为此我们可以使用每一个事务独占一个连接来实现,虽然这种方法有点浪费连接池资源但是可以大大降低事务管理的复杂性。

连接池的分配与释放:连接池的分配与释放,对系统的性能有很大的影响。合理的分配与释放,可以提高连接的复用度,从而降低建立新连接的开销,同时还可以加快用户的访问速度。 对于连接的管理可使用一个 List。即把已经创建的连接都放入 List 中去统一管理。每当用户请求一个连接时,系统检查这个 List 中有没有可以分配的连接。如果有就把那个最合适的连接分配给他;如果没有就抛出一个异常给用户。

连接池的配置与维护:连接池中到底应该放置多少连接,才能使系统的性能最佳?系统可采取设置最小连接数(minConnection)和最大连接数(maxConnection)等参数来控制连接池中的连接。比方说,最小连接数是系统启动时连接池所创建的连接数。如果创建过多,则系统启动就慢,但创建后系统的响应速度会很快;如果创建过少,则系统启动的很快,响应起来却慢。这样,可以在开发时,设置较小的最小连接数,开发起来会快,而在系统实际使用时设置较大的,因为这样对访问客户来说速度会快些。最大连接数是连接池中允许连接的最大数目,具体设置多少,要看系统的访问量,可通过软件需求上得到。 如何确保连接池中的最小连接数呢?有动态和静态两种策略。动态即每隔一定时间就对连接池进行检测,如果发现连接数量小于最小连接数,则补充相应数量的新连接,以保证连接池的正常运转。静态是发现空闲连接不够时再去检查。

数据库连接池技术选型

常见的数据库连接池:

  • HikariCP:HiKariCP 号称是跑的最快的连接池,并且是 SpringBoot 框架的默认连接池。
  • Druid:Druid 是阿里巴巴开源的数据库连接池。Druid 内置强大的监控功能,监控特性不影响性能。功能强大,能防 SQL 注入,内置 Loging 能诊断 Hack 应用行为。
  • DBCP: 由 Apache 开发的一个 Java 数据库连接池。commons-dbcp2 基于 commons-pool2 来实现底层的对象池机制。单线程,性能较差,适用于小型系统。官方自 2021 年后没有再更新。
  • C3P0:开源的 JDBC 连接池,实现了数据源和 JNDI 绑定,支持 JDBC3 规范和 JDBC2 的标准扩展。单线程,性能较差,适用于小型系统。官方自 2019 年后再没有更新。
  • Tomcat-jdbc:Tomcat 在 7.0 以前使用 DBCP 做为连接池组件,从 7.0 后新增了 Tomcat jdbc pool 模块,基于 Tomcat JULI,使用 Tomcat 日志框架,完全兼容 dbcp,通过异步方式获取连接,支持高并发应用环境,超级简单核心文件只有 8 个,支持 JMX,支持 XA Connection。

来自 Druid 的竞品对比(https://github.com/alibaba/druid/wiki/Druid%E8%BF%9E%E6%8E%A5%E6%B1%A0%E4%BB%8B%E7%BB%8D):

功能类别 功能 Druid HikariCP DBCP Tomcat-jdbc C3P0
性能 PSCache
LRU
SLB 负载均衡支持
稳定性 ExceptionSorter
扩展 扩展 Filter JdbcIntercepter
监控 监控方式 jmx/log/http jmx/metrics jmx jmx jmx
支持 SQL 级监控
Spring/Web 关联监控
诊断支持 LogFilter
连接泄露诊断 logAbandoned
安全 SQL 防注入
支持配置加密

从数据库连接池最重要的性能角度来看:HikariCP 应该性能最好;Druid 也不错,并且有更多、更久的生产实践,更为可靠;而其他常见的数据库连接池性能远远不如。

从功能角度来看:Druid 功能最全面,除基本的数据库连接池能力以外,还支持 sql 级监控、扩展、SQL 防注入以及监控等功能。

综合来看:HikariCP 是 Spring Boot 首选数据库连接池,对于 Spring Boot 项目来说,无疑适配性最好。而非 Spring Boot 项目,可以优先考虑 Druid,在国内有大规模应用,中文社区支持良好。

HikariCP

HiKariCP 号称是跑的最快的连接池,并且是 SpringBoot 框架的默认连接池。

HiKariCP 为了提升性能,做了很多细节上的优化,例如:

  • 使用 FastList 替代 ArrayList,通过初始化的默认值,减少了越界检查的操作
  • 优化并精简了字节码,通过使用 Javassist,减少了动态代理的性能损耗,比如使用 invokestatic 指令代替 invokevirtual 指令
  • 实现了无锁的 ConcurrentBag,减少了并发场景下的锁竞争

HikariCP 关键配置:

  • maximum-pool-size:池中最大连接数(包括空闲和正在使用的连接)。默认值是 10,这个一般预估应用的最大连接数,后期根据监测得到一个最大值的一个平均值。要知道,最大连接并不是越多越好,一个 connection 会占用系统的带宽和存储。但是 当连接池没有空闲连接并且已经到达最大值,新来的连接池请求(HikariPool#getConnection)会被阻塞直到connectionTimeout(毫秒),超时后便抛出 SQLException。
  • minimum-idle:池中最小空闲连接数量。默认值 10,小于池中最大连接数,一般根据系统大部分情况下的数据库连接情况取一个平均值。Hikari 会尽可能、尽快地将空闲连接数维持在这个数量上。如果为了获得最佳性能和对峰值需求的响应能力,我们也不妨让他和最大连接数保持一致,使得 HikariCP 成为一个固定大小的数据库连接池。
  • connection-timeout:连接超时时间。默认值为 30s,可以接收的最小超时时间为 250ms。但是连接池请求也可以自定义超时时间(com.zaxxer.hikari.pool.HikariPool#getConnection(long))。
  • idle-timeout:空闲连接存活最大时间,默认 600000(十分钟)
  • max-lifetime:连接池中连接的最大生命周期。当连接一致处于闲置状态时,超过 8 小时数据库会主动断开连接。为了防止大量的同一时间处于空闲连接因为数据库方的闲置超时策略断开连接(可以理解为连接雪崩),一般将这个值设置的比数据库的“闲置超时时间”小几秒,以便这些连接断开后,HikariCP 能迅速的创建新一轮的连接。
  • pool-name:连接池的名字。一般会出现在日志和 JMX 控制台中。默认值:auto-genenrated。建议取一个合适的名字,便于监控。
  • auto-commit:是否自动提交池中返回的连接。默认值为 true。一般是有必要自动提交上一个连接中的事物的。如果为 false,那么就需要应用层手动提交事物。

参考配置:

1
2
3
4
5
6
7
8
9
10
11
12
13
# 连接池名称
spring.datasource.hikari.pool-name = SpringTutorialHikariPool
# 最大连接数,小于等于 0 会被重置为默认值 10;大于零小于 1 会被重置为 minimum-idle 的值
spring.datasource.hikari.maximum-pool-size = 10
# 最小空闲连接,默认值10,小于 0 或大于 maximum-pool-size,都会重置为 maximum-pool-size
spring.datasource.hikari.minimum-idle = 10
# 连接超时时间(单位:毫秒),小于 250 毫秒,会被重置为默认值 30 秒
spring.datasource.hikari.connection-timeout = 60000
# 空闲连接超时时间,默认值 600000(10分钟),大于等于 max-lifetime 且 max-lifetime>0,会被重置为0;不等于 0 且小于 10 秒,会被重置为 10 秒
# 只有空闲连接数大于最大连接数且空闲时间超过该值,才会被释放
spring.datasource.hikari.idle-timeout = 600000
# 连接最大存活时间,不等于 0 且小于 30 秒,会被重置为默认值 30 分钟。该值应该比数据库所设置的超时时间短
spring.datasource.hikari.max-lifetime = 1800000

Druid

Druid 是阿里巴巴开源的数据库连接池。Druid 连接池为监控而生,内置强大的监控功能,监控特性不影响性能。功能强大,能防 SQL 注入,内置 Loging 能诊断 Hack 应用行为。

Druid 关键配置:

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
# 数据库访问配置
# 主数据源,默认的
spring.datasource.type=com.alibaba.druid.pool.DruidDataSource
spring.datasource.driver-class-name=com.mysql.jdbc.Driver
spring.datasource.url=jdbc:mysql://localhost:3306/druid
spring.datasource.username=root
spring.datasource.password=root

# 下面为连接池的补充设置,应用到上面所有数据源中
# 初始化大小,最小,最大
spring.datasource.initialSize=5
spring.datasource.minIdle=5
spring.datasource.maxActive=20
# 配置获取连接等待超时的时间
spring.datasource.maxWait=60000
# 配置间隔多久才进行一次检测,检测需要关闭的空闲连接,单位是毫秒
spring.datasource.timeBetweenEvictionRunsMillis=60000
# 配置一个连接在池中最小生存的时间,单位是毫秒
spring.datasource.minEvictableIdleTimeMillis=300000
spring.datasource.validationQuery=SELECT 1 FROM DUAL
spring.datasource.testWhileIdle=true
spring.datasource.testOnBorrow=false
spring.datasource.testOnReturn=false
# 打开PSCache,并且指定每个连接上PSCache的大小
spring.datasource.poolPreparedStatements=true
spring.datasource.maxPoolPreparedStatementPerConnectionSize=20
# 配置监控统计拦截的filters,去掉后监控界面sql无法统计,'wall'用于防火墙
spring.datasource.filters=stat,wall,log4j
# 通过connectProperties属性来打开mergeSql功能;慢SQL记录
spring.datasource.connectionProperties=druid.stat.mergeSql=true;druid.stat.slowSqlMillis=5000
# 合并多个DruidDataSource的监控数据
#spring.datasource.useGlobalDataSourceStat=true

参考资料