切换默认shell到wsl
This commit is contained in:
@@ -1,96 +1,96 @@
|
||||
- [Apache Shiro Authentication](#apache-shiro-authentication)
|
||||
- [Apache Shiro Authentication简介](#apache-shiro-authentication简介)
|
||||
- [Apache Authentication概念](#apache-authentication概念)
|
||||
- [subject](#subject)
|
||||
- [principals](#principals)
|
||||
- [credentials](#credentials)
|
||||
- [realms](#realms)
|
||||
- [Shiro Authentication过程](#shiro-authentication过程)
|
||||
- [Shiro框架的Authentication过程](#shiro框架的authentication过程)
|
||||
- [收集用户的principals和credentials](#收集用户的principals和credentials)
|
||||
- [将收集的principals和credentials提交给认证系统](#将收集的principals和credentials提交给认证系统)
|
||||
- [身份认证后对访问进行allow/retry authentication/block](#身份认证后对访问进行allowretry-authenticationblock)
|
||||
- [rememberMe支持](#rememberme支持)
|
||||
- [remembered和authenticated的区别](#remembered和authenticated的区别)
|
||||
- [logging out](#logging-out)
|
||||
|
||||
# Apache Shiro Authentication
|
||||
## Apache Shiro Authentication简介
|
||||
Authentication是一个对用户进行身份认证的过程,在认证过程中用户需要向应用提供用于证明用户的凭据。
|
||||
## Apache Authentication概念
|
||||
### subject
|
||||
在应用的角度,subject即是一个用户
|
||||
### principals
|
||||
主体,用于标识一个用户,可以是username、social security nubmer等
|
||||
### credentials
|
||||
凭据,在用户认证过程中用于认证用户的身份,可以是密码、生物识别数据(如指纹、面容等)
|
||||
### realms
|
||||
专用于security的dao对象,用于和后端的datasource进行沟通。
|
||||
## Shiro Authentication过程
|
||||
### Shiro框架的Authentication过程
|
||||
1. 收集用户的principals和credentials
|
||||
2. 向应用的认证系统提交用户的principals和credentials
|
||||
3. 认证结束之后,根据认证结果允许访问、重试访问请求或者阻塞访问
|
||||
### 收集用户的principals和credentials
|
||||
可以通过UsernamePasswordToken来存储用户提交的username和password,并可以调用UsernamePasswordToken.rememberMe方法来启用Shiro的“remember-me”功能。
|
||||
```java
|
||||
//Example using most common scenario:
|
||||
//String username and password. Acquire in
|
||||
//system-specific manner (HTTP request, GUI, etc)
|
||||
UsernamePasswordToken token = new UsernamePasswordToken( username, password );
|
||||
|
||||
//”Remember Me” built-in, just do this:
|
||||
token.setRememberMe(true);
|
||||
```
|
||||
### 将收集的principals和credentials提交给认证系统
|
||||
在收集完用户的principals和credentials之后,需要将其提交给应用的认证系统。
|
||||
在Shiro中,代表认证系统的是Realm,其从存放安全数据的datasource中获取数据,并且对用户提交的principals和credentials进行校验。
|
||||
在Shiro中,该过程用如下代码表示:
|
||||
```java
|
||||
//With most of Shiro, you'll always want to make sure you're working with the currently
|
||||
//executing user, referred to as the subject
|
||||
Subject currentUser = SecurityUtils.getSubject();
|
||||
|
||||
//Authenticate the subject by passing
|
||||
//the user name and password token
|
||||
//into the login method
|
||||
currentUser.login(token);
|
||||
```
|
||||
> 在Shiro中,subject可以被看做是用户,**在当前执行的线程中永远有一个subject与其相关联。**
|
||||
> **可以通过SecurityUtils.getSubject()方法来获取当前执行线程相关联的subject。**
|
||||
|
||||
> 在获取当前执行线程关联subject之后,需要对当前subject进行身份认证,通过subject.login(token)来对用户提交的principals和credentials进行Authentication
|
||||
|
||||
### 身份认证后对访问进行allow/retry authentication/block
|
||||
在调用subject.login(token)之后,如果身份认证成功,用户将在seesion的生命周期内维持他们的identity。但是如果身份认证失败,可以为抛出的异常指定不同的异常处理逻辑来定义登录失败之后的行为。
|
||||
```java
|
||||
try {
|
||||
currentUser.login(token);
|
||||
} catch ( UnknownAccountException uae ) { ...
|
||||
} catch ( IncorrectCredentialsException ice ) { ...
|
||||
} catch ( LockedAccountException lae ) { ...
|
||||
} catch ( ExcessiveAttemptsException eae ) { ...
|
||||
} ... your own ...
|
||||
} catch ( AuthenticationException ae ) {
|
||||
//unexpected error?
|
||||
}
|
||||
//No problems, show authenticated view…
|
||||
```
|
||||
## rememberMe支持
|
||||
Apache Shiro除了正常的Authentication流程外,还支持rememberMe功能。
|
||||
Shiro中Subject对象拥有两个方法,isRemembered()和isAuthenticated()。
|
||||
> - 一个remembered subject,其identity和principals自上次session成功认证后就被记住
|
||||
> - 一个authenticated subject,其identity只在本次会话中有效
|
||||
|
||||
### remembered和authenticated的区别
|
||||
在Shiro中,一个remembered subject并不代表该subject已经被authenticated。如果一个subject被remembered,仅仅会向系统提示该subject可能是系统的某个用户,但是不会对subject的身份提供保证。但是如果subject被authenticated,该subject的identity在当前会话中已经被认证。
|
||||
> 故而,isRemembered校验可以用来执行一些非敏感的操作,如用户自定义界面视图等。但是,敏感性操作如金额信息和变动操作等,必须通过isAuthenticated校验而不是isRemembered校验,敏感性操作的用户身份必须得到认证。
|
||||
|
||||
## logging out
|
||||
在Shiro中,登出操作可以通过如下代码实现
|
||||
```java
|
||||
currentUser.logout(); //removes all identifying information and invalidates their session too.
|
||||
```
|
||||
当执行登出操作时,Shiro会关闭当前session,并且会移除当前subject的任何identity。如果在web环境中使用rememberMe,logout默认会从浏览器中删除rememberMe cookie。
|
||||
|
||||
|
||||
- [Apache Shiro Authentication](#apache-shiro-authentication)
|
||||
- [Apache Shiro Authentication简介](#apache-shiro-authentication简介)
|
||||
- [Apache Authentication概念](#apache-authentication概念)
|
||||
- [subject](#subject)
|
||||
- [principals](#principals)
|
||||
- [credentials](#credentials)
|
||||
- [realms](#realms)
|
||||
- [Shiro Authentication过程](#shiro-authentication过程)
|
||||
- [Shiro框架的Authentication过程](#shiro框架的authentication过程)
|
||||
- [收集用户的principals和credentials](#收集用户的principals和credentials)
|
||||
- [将收集的principals和credentials提交给认证系统](#将收集的principals和credentials提交给认证系统)
|
||||
- [身份认证后对访问进行allow/retry authentication/block](#身份认证后对访问进行allowretry-authenticationblock)
|
||||
- [rememberMe支持](#rememberme支持)
|
||||
- [remembered和authenticated的区别](#remembered和authenticated的区别)
|
||||
- [logging out](#logging-out)
|
||||
|
||||
# Apache Shiro Authentication
|
||||
## Apache Shiro Authentication简介
|
||||
Authentication是一个对用户进行身份认证的过程,在认证过程中用户需要向应用提供用于证明用户的凭据。
|
||||
## Apache Authentication概念
|
||||
### subject
|
||||
在应用的角度,subject即是一个用户
|
||||
### principals
|
||||
主体,用于标识一个用户,可以是username、social security nubmer等
|
||||
### credentials
|
||||
凭据,在用户认证过程中用于认证用户的身份,可以是密码、生物识别数据(如指纹、面容等)
|
||||
### realms
|
||||
专用于security的dao对象,用于和后端的datasource进行沟通。
|
||||
## Shiro Authentication过程
|
||||
### Shiro框架的Authentication过程
|
||||
1. 收集用户的principals和credentials
|
||||
2. 向应用的认证系统提交用户的principals和credentials
|
||||
3. 认证结束之后,根据认证结果允许访问、重试访问请求或者阻塞访问
|
||||
### 收集用户的principals和credentials
|
||||
可以通过UsernamePasswordToken来存储用户提交的username和password,并可以调用UsernamePasswordToken.rememberMe方法来启用Shiro的“remember-me”功能。
|
||||
```java
|
||||
//Example using most common scenario:
|
||||
//String username and password. Acquire in
|
||||
//system-specific manner (HTTP request, GUI, etc)
|
||||
UsernamePasswordToken token = new UsernamePasswordToken( username, password );
|
||||
|
||||
//”Remember Me” built-in, just do this:
|
||||
token.setRememberMe(true);
|
||||
```
|
||||
### 将收集的principals和credentials提交给认证系统
|
||||
在收集完用户的principals和credentials之后,需要将其提交给应用的认证系统。
|
||||
在Shiro中,代表认证系统的是Realm,其从存放安全数据的datasource中获取数据,并且对用户提交的principals和credentials进行校验。
|
||||
在Shiro中,该过程用如下代码表示:
|
||||
```java
|
||||
//With most of Shiro, you'll always want to make sure you're working with the currently
|
||||
//executing user, referred to as the subject
|
||||
Subject currentUser = SecurityUtils.getSubject();
|
||||
|
||||
//Authenticate the subject by passing
|
||||
//the user name and password token
|
||||
//into the login method
|
||||
currentUser.login(token);
|
||||
```
|
||||
> 在Shiro中,subject可以被看做是用户,**在当前执行的线程中永远有一个subject与其相关联。**
|
||||
> **可以通过SecurityUtils.getSubject()方法来获取当前执行线程相关联的subject。**
|
||||
|
||||
> 在获取当前执行线程关联subject之后,需要对当前subject进行身份认证,通过subject.login(token)来对用户提交的principals和credentials进行Authentication
|
||||
|
||||
### 身份认证后对访问进行allow/retry authentication/block
|
||||
在调用subject.login(token)之后,如果身份认证成功,用户将在seesion的生命周期内维持他们的identity。但是如果身份认证失败,可以为抛出的异常指定不同的异常处理逻辑来定义登录失败之后的行为。
|
||||
```java
|
||||
try {
|
||||
currentUser.login(token);
|
||||
} catch ( UnknownAccountException uae ) { ...
|
||||
} catch ( IncorrectCredentialsException ice ) { ...
|
||||
} catch ( LockedAccountException lae ) { ...
|
||||
} catch ( ExcessiveAttemptsException eae ) { ...
|
||||
} ... your own ...
|
||||
} catch ( AuthenticationException ae ) {
|
||||
//unexpected error?
|
||||
}
|
||||
//No problems, show authenticated view…
|
||||
```
|
||||
## rememberMe支持
|
||||
Apache Shiro除了正常的Authentication流程外,还支持rememberMe功能。
|
||||
Shiro中Subject对象拥有两个方法,isRemembered()和isAuthenticated()。
|
||||
> - 一个remembered subject,其identity和principals自上次session成功认证后就被记住
|
||||
> - 一个authenticated subject,其identity只在本次会话中有效
|
||||
|
||||
### remembered和authenticated的区别
|
||||
在Shiro中,一个remembered subject并不代表该subject已经被authenticated。如果一个subject被remembered,仅仅会向系统提示该subject可能是系统的某个用户,但是不会对subject的身份提供保证。但是如果subject被authenticated,该subject的identity在当前会话中已经被认证。
|
||||
> 故而,isRemembered校验可以用来执行一些非敏感的操作,如用户自定义界面视图等。但是,敏感性操作如金额信息和变动操作等,必须通过isAuthenticated校验而不是isRemembered校验,敏感性操作的用户身份必须得到认证。
|
||||
|
||||
## logging out
|
||||
在Shiro中,登出操作可以通过如下代码实现
|
||||
```java
|
||||
currentUser.logout(); //removes all identifying information and invalidates their session too.
|
||||
```
|
||||
当执行登出操作时,Shiro会关闭当前session,并且会移除当前subject的任何identity。如果在web环境中使用rememberMe,logout默认会从浏览器中删除rememberMe cookie。
|
||||
|
||||
|
||||
|
||||
@@ -1,94 +1,94 @@
|
||||
- [Apache Shiro Authorization](#apache-shiro-authorization)
|
||||
- [Authorization简介](#authorization简介)
|
||||
- [Authorization的核心元素](#authorization的核心元素)
|
||||
- [Permission](#permission)
|
||||
- [权限粒度级别](#权限粒度级别)
|
||||
- [Roles](#roles)
|
||||
- [Role分类](#role分类)
|
||||
- [User](#user)
|
||||
- [在Apache Shiro中实行Authorization](#在apache-shiro中实行authorization)
|
||||
- [通过java code实现authorization](#通过java-code实现authorization)
|
||||
- [基于String的权限鉴定](#基于string的权限鉴定)
|
||||
- [通过注解实现Authorization](#通过注解实现authorization)
|
||||
|
||||
# Apache Shiro Authorization
|
||||
## Authorization简介
|
||||
Authorization(访问控制),分配访问某资源的特定权限。
|
||||
## Authorization的核心元素
|
||||
### Permission
|
||||
Permission是最原子级别的安全策略,用来控制用户与应用进行交互时可以执行哪些操作。**格式良好的permission描述了资源的类型和与该资源交互时可以执行的操作。**
|
||||
对于与数据相关的资源,权限通常有create、read、update、delete(CRUD)。
|
||||
#### 权限粒度级别
|
||||
在Shiro中,可以在任何粒度对permission进行定义。如下是permission粒度的一些定义:
|
||||
1. Resource级别:该级别是最广泛和最容易构建的粒度级别,在该级别用户可以对资源执行特定的操作。**在Resource级别,该资源类型被指定,但是没有限制用户操作特定的资源实例(即用户可以对该Resource类型的所有实例进行操作)**
|
||||
2. Instance级别:该级别限定了Permission可以操作的Resource Instance,在该级别用户只能够对特定的Resource实例进行操作。
|
||||
3. Attribute级别:该级别比限定了Permission可以操作Resouce类型或Resource实例的某个属性
|
||||
### Roles
|
||||
Roles是一个Permission的集合,用于简化权限和用户管理过程。用户可以被授予特定的角色来获得操作某些资源的权限。
|
||||
#### Role分类
|
||||
1. Role不实际关联具体的Permission,当你具有banker的角色时,其角色隐含你可以对账户进行操作的权限;当你具有waiter的角色时,默认可以对厨房的door进行open/close操作
|
||||
2. Role实际关联具体的Permission,在该情况下Role即为一系列Permission的集合,你可以对银行账号进行create、delete操作,因为操作银行账号是你已分配的admin角色的一个下属权限
|
||||
### User
|
||||
在Shiro中,User即是一个Subject实例。在Shiro中,Subject可以是任何与系统进行交互的主体,可以是浏览器、客户端、crond定时任务等。
|
||||
## 在Apache Shiro中实行Authorization
|
||||
在Apache Shiro中,Authorization可以通过如下方式执行:
|
||||
1. 通过代码实现:即在java程序中通过代码实现访问控制
|
||||
2. jdk注解:可以在你的方法上加上authorization注解
|
||||
3. jsp/gsp taglibs
|
||||
### 通过java code实现authorization
|
||||
可以通过如下代码进行角色鉴定:
|
||||
```java
|
||||
//get the current Subject
|
||||
Subject currentUser = SecurityUtils.getSubject();
|
||||
|
||||
if (currentUser.hasRole("administrator")) {
|
||||
//show a special button
|
||||
} else {
|
||||
//don’t show the button?)
|
||||
}
|
||||
```
|
||||
可以通过如下代码实现对权限的鉴定操作:
|
||||
```java
|
||||
Subject currentUser = SecurityUtils.getSubject();
|
||||
|
||||
Permission printPermission = new PrinterPermission("laserjet3000n","print");
|
||||
|
||||
If (currentUser.isPermitted(printPermission)) {
|
||||
//do one thing (show the print button?)
|
||||
} else {
|
||||
//don’t show the button?
|
||||
}
|
||||
```
|
||||
#### 基于String的权限鉴定
|
||||
如果不想构造Permission对象,可以通过构造一个字符串来代表权限。该字符串可以是任何格式,只要你的Realm能够识别该格式并且与权限进行交互。
|
||||
```java
|
||||
String perm = "printer:print:laserjet4400n";
|
||||
|
||||
if(currentUser.isPermitted(perm)){
|
||||
//show the print button?
|
||||
} else {
|
||||
//don’t show the button?
|
||||
}
|
||||
```
|
||||
### 通过注解实现Authorization
|
||||
可以通过java注解来实现Authorization过程,**在使用注解之前,必须先开启aop**。
|
||||
如果在执行openAccount之前,当前Subject必须拥有account:create权限,那么可以通过如下方式来实现权限鉴定。如果当前用户未被直接授予或通过role间接授予该权限,那么会抛出AuthorizationException异常。
|
||||
```java
|
||||
//Will throw an AuthorizationException if none
|
||||
//of the caller’s roles imply the Account
|
||||
//'create' permission
|
||||
@RequiresPermissions("account:create")
|
||||
public void openAccount( Account acct ) {
|
||||
//create the account
|
||||
}
|
||||
```
|
||||
如果要在执行方法之前进行角色校验,可以通过如下方式加上注解达到预期功能。
|
||||
```java
|
||||
//Throws an AuthorizationException if the caller
|
||||
//doesn’t have the ‘teller’ role:
|
||||
@RequiresRoles( "teller" )
|
||||
public void openAccount( Account acct ) {
|
||||
//do something in here that only a teller
|
||||
//should do
|
||||
}
|
||||
- [Apache Shiro Authorization](#apache-shiro-authorization)
|
||||
- [Authorization简介](#authorization简介)
|
||||
- [Authorization的核心元素](#authorization的核心元素)
|
||||
- [Permission](#permission)
|
||||
- [权限粒度级别](#权限粒度级别)
|
||||
- [Roles](#roles)
|
||||
- [Role分类](#role分类)
|
||||
- [User](#user)
|
||||
- [在Apache Shiro中实行Authorization](#在apache-shiro中实行authorization)
|
||||
- [通过java code实现authorization](#通过java-code实现authorization)
|
||||
- [基于String的权限鉴定](#基于string的权限鉴定)
|
||||
- [通过注解实现Authorization](#通过注解实现authorization)
|
||||
|
||||
# Apache Shiro Authorization
|
||||
## Authorization简介
|
||||
Authorization(访问控制),分配访问某资源的特定权限。
|
||||
## Authorization的核心元素
|
||||
### Permission
|
||||
Permission是最原子级别的安全策略,用来控制用户与应用进行交互时可以执行哪些操作。**格式良好的permission描述了资源的类型和与该资源交互时可以执行的操作。**
|
||||
对于与数据相关的资源,权限通常有create、read、update、delete(CRUD)。
|
||||
#### 权限粒度级别
|
||||
在Shiro中,可以在任何粒度对permission进行定义。如下是permission粒度的一些定义:
|
||||
1. Resource级别:该级别是最广泛和最容易构建的粒度级别,在该级别用户可以对资源执行特定的操作。**在Resource级别,该资源类型被指定,但是没有限制用户操作特定的资源实例(即用户可以对该Resource类型的所有实例进行操作)**
|
||||
2. Instance级别:该级别限定了Permission可以操作的Resource Instance,在该级别用户只能够对特定的Resource实例进行操作。
|
||||
3. Attribute级别:该级别比限定了Permission可以操作Resouce类型或Resource实例的某个属性
|
||||
### Roles
|
||||
Roles是一个Permission的集合,用于简化权限和用户管理过程。用户可以被授予特定的角色来获得操作某些资源的权限。
|
||||
#### Role分类
|
||||
1. Role不实际关联具体的Permission,当你具有banker的角色时,其角色隐含你可以对账户进行操作的权限;当你具有waiter的角色时,默认可以对厨房的door进行open/close操作
|
||||
2. Role实际关联具体的Permission,在该情况下Role即为一系列Permission的集合,你可以对银行账号进行create、delete操作,因为操作银行账号是你已分配的admin角色的一个下属权限
|
||||
### User
|
||||
在Shiro中,User即是一个Subject实例。在Shiro中,Subject可以是任何与系统进行交互的主体,可以是浏览器、客户端、crond定时任务等。
|
||||
## 在Apache Shiro中实行Authorization
|
||||
在Apache Shiro中,Authorization可以通过如下方式执行:
|
||||
1. 通过代码实现:即在java程序中通过代码实现访问控制
|
||||
2. jdk注解:可以在你的方法上加上authorization注解
|
||||
3. jsp/gsp taglibs
|
||||
### 通过java code实现authorization
|
||||
可以通过如下代码进行角色鉴定:
|
||||
```java
|
||||
//get the current Subject
|
||||
Subject currentUser = SecurityUtils.getSubject();
|
||||
|
||||
if (currentUser.hasRole("administrator")) {
|
||||
//show a special button
|
||||
} else {
|
||||
//don’t show the button?)
|
||||
}
|
||||
```
|
||||
可以通过如下代码实现对权限的鉴定操作:
|
||||
```java
|
||||
Subject currentUser = SecurityUtils.getSubject();
|
||||
|
||||
Permission printPermission = new PrinterPermission("laserjet3000n","print");
|
||||
|
||||
If (currentUser.isPermitted(printPermission)) {
|
||||
//do one thing (show the print button?)
|
||||
} else {
|
||||
//don’t show the button?
|
||||
}
|
||||
```
|
||||
#### 基于String的权限鉴定
|
||||
如果不想构造Permission对象,可以通过构造一个字符串来代表权限。该字符串可以是任何格式,只要你的Realm能够识别该格式并且与权限进行交互。
|
||||
```java
|
||||
String perm = "printer:print:laserjet4400n";
|
||||
|
||||
if(currentUser.isPermitted(perm)){
|
||||
//show the print button?
|
||||
} else {
|
||||
//don’t show the button?
|
||||
}
|
||||
```
|
||||
### 通过注解实现Authorization
|
||||
可以通过java注解来实现Authorization过程,**在使用注解之前,必须先开启aop**。
|
||||
如果在执行openAccount之前,当前Subject必须拥有account:create权限,那么可以通过如下方式来实现权限鉴定。如果当前用户未被直接授予或通过role间接授予该权限,那么会抛出AuthorizationException异常。
|
||||
```java
|
||||
//Will throw an AuthorizationException if none
|
||||
//of the caller’s roles imply the Account
|
||||
//'create' permission
|
||||
@RequiresPermissions("account:create")
|
||||
public void openAccount( Account acct ) {
|
||||
//create the account
|
||||
}
|
||||
```
|
||||
如果要在执行方法之前进行角色校验,可以通过如下方式加上注解达到预期功能。
|
||||
```java
|
||||
//Throws an AuthorizationException if the caller
|
||||
//doesn’t have the ‘teller’ role:
|
||||
@RequiresRoles( "teller" )
|
||||
public void openAccount( Account acct ) {
|
||||
//do something in here that only a teller
|
||||
//should do
|
||||
}
|
||||
```
|
||||
@@ -1,92 +1,92 @@
|
||||
- [Apache Shiro Quick Start](#apache-shiro-quick-start)
|
||||
- [Apache Shiro常用API](#apache-shiro常用api)
|
||||
- [获取当前用户](#获取当前用户)
|
||||
- [设置用户Session](#设置用户session)
|
||||
- [通过用户名和密码对用户进行身份认证](#通过用户名和密码对用户进行身份认证)
|
||||
- [对身份认证失败的情况进行异常处理](#对身份认证失败的情况进行异常处理)
|
||||
- [对已经登录的用户进行role检验](#对已经登录的用户进行role检验)
|
||||
- [检测某用户是否具有某项特定权限](#检测某用户是否具有某项特定权限)
|
||||
- [在实例级别对用户的权限进行检测](#在实例级别对用户的权限进行检测)
|
||||
- [用户登出](#用户登出)
|
||||
|
||||
# Apache Shiro Quick Start
|
||||
## Apache Shiro常用API
|
||||
### 获取当前用户
|
||||
在任何环境中,都可以通过如下代码来获取当前执行的用户:
|
||||
```java
|
||||
Subject currentUser = SecurityUtils.getSubject();
|
||||
```
|
||||
### 设置用户Session
|
||||
可以通过如下代码获取用户的Shiro Session,并可以向Session中设置属性和值,设置的值在用户会话期间内都可以使用。
|
||||
**Shiro Session在使用时并不要求当前位于HTTP环境下**
|
||||
```java
|
||||
Session session = currentUser.getSession();
|
||||
session.setAttribute( "someKey", "aValue" );
|
||||
```
|
||||
> 如果当前应用部署于Web环境下,那么Shiro Session默认会使用HttpSession,但是如果当前应用部署在非Web环境下时,Shiro Session会使用其Enterprise Session Management。
|
||||
|
||||
### 通过用户名和密码对用户进行身份认证
|
||||
通过如下代码,可以通过UsernamePasswordToken来对未认证的用户进行身份认证。
|
||||
```java
|
||||
if ( !currentUser.isAuthenticated() ) {
|
||||
//collect user principals and credentials in a gui specific manner
|
||||
//such as username/password html form, X509 certificate, OpenID, etc.
|
||||
//We'll use the username/password example here since it is the most common.
|
||||
//(do you know what movie this is from? ;)
|
||||
UsernamePasswordToken token = new UsernamePasswordToken("lonestarr", "vespa");
|
||||
//this is all you have to do to support 'remember me' (no config - built in!):
|
||||
token.setRememberMe(true);
|
||||
currentUser.login(token);
|
||||
}
|
||||
```
|
||||
### 对身份认证失败的情况进行异常处理
|
||||
如果在身份认证的过程中失败,可以通过如下代码捕获认证失败抛出的异常,并对异常进行异常处理
|
||||
```java
|
||||
try {
|
||||
currentUser.login( token );
|
||||
//if no exception, that's it, we're done!
|
||||
} catch ( UnknownAccountException uae ) {
|
||||
//username wasn't in the system, show them an error message?
|
||||
} catch ( IncorrectCredentialsException ice ) {
|
||||
//password didn't match, try again?
|
||||
} catch ( LockedAccountException lae ) {
|
||||
//account for that username is locked - can't login. Show them a message?
|
||||
}
|
||||
... more types exceptions to check if you want ...
|
||||
} catch ( AuthenticationException ae ) {
|
||||
//unexpected condition - error?
|
||||
}
|
||||
```
|
||||
### 对已经登录的用户进行role检验
|
||||
如果用户已经登录,如果要检测该用户是否被授予某role权限,可以通过如下代码进行检验
|
||||
```java
|
||||
if ( currentUser.hasRole( "schwartz" ) ) {
|
||||
log.info("May the Schwartz be with you!" );
|
||||
} else {
|
||||
log.info( "Hello, mere mortal." );
|
||||
}
|
||||
```
|
||||
### 检测某用户是否具有某项特定权限
|
||||
如果要对已经登录的用户执行检测,检测其是否被授予某项特定的前线,可以通过如下方式进行检测。
|
||||
```java
|
||||
if ( currentUser.isPermitted( "lightsaber:wield" ) ) {
|
||||
log.info("You may use a lightsaber ring. Use it wisely.");
|
||||
} else {
|
||||
log.info("Sorry, lightsaber rings are for schwartz masters only.");
|
||||
}
|
||||
```
|
||||
### 在实例级别对用户的权限进行检测
|
||||
在Shiro中,可以检测用户是否对某实例具有特定权限,通过如下代码:
|
||||
```java
|
||||
if ( currentUser.isPermitted( "winnebago:drive:eagle5" ) ) {
|
||||
log.info("You are permitted to 'drive' the 'winnebago' with license plate (id) 'eagle5'. " +
|
||||
"Here are the keys - have fun!");
|
||||
} else {
|
||||
log.info("Sorry, you aren't allowed to drive the 'eagle5' winnebago!");
|
||||
}
|
||||
```
|
||||
### 用户登出
|
||||
如果已经登录的用户想要执行登出操作,可以通过如下代码进行登录:
|
||||
```java
|
||||
currentUser.logout();
|
||||
- [Apache Shiro Quick Start](#apache-shiro-quick-start)
|
||||
- [Apache Shiro常用API](#apache-shiro常用api)
|
||||
- [获取当前用户](#获取当前用户)
|
||||
- [设置用户Session](#设置用户session)
|
||||
- [通过用户名和密码对用户进行身份认证](#通过用户名和密码对用户进行身份认证)
|
||||
- [对身份认证失败的情况进行异常处理](#对身份认证失败的情况进行异常处理)
|
||||
- [对已经登录的用户进行role检验](#对已经登录的用户进行role检验)
|
||||
- [检测某用户是否具有某项特定权限](#检测某用户是否具有某项特定权限)
|
||||
- [在实例级别对用户的权限进行检测](#在实例级别对用户的权限进行检测)
|
||||
- [用户登出](#用户登出)
|
||||
|
||||
# Apache Shiro Quick Start
|
||||
## Apache Shiro常用API
|
||||
### 获取当前用户
|
||||
在任何环境中,都可以通过如下代码来获取当前执行的用户:
|
||||
```java
|
||||
Subject currentUser = SecurityUtils.getSubject();
|
||||
```
|
||||
### 设置用户Session
|
||||
可以通过如下代码获取用户的Shiro Session,并可以向Session中设置属性和值,设置的值在用户会话期间内都可以使用。
|
||||
**Shiro Session在使用时并不要求当前位于HTTP环境下**
|
||||
```java
|
||||
Session session = currentUser.getSession();
|
||||
session.setAttribute( "someKey", "aValue" );
|
||||
```
|
||||
> 如果当前应用部署于Web环境下,那么Shiro Session默认会使用HttpSession,但是如果当前应用部署在非Web环境下时,Shiro Session会使用其Enterprise Session Management。
|
||||
|
||||
### 通过用户名和密码对用户进行身份认证
|
||||
通过如下代码,可以通过UsernamePasswordToken来对未认证的用户进行身份认证。
|
||||
```java
|
||||
if ( !currentUser.isAuthenticated() ) {
|
||||
//collect user principals and credentials in a gui specific manner
|
||||
//such as username/password html form, X509 certificate, OpenID, etc.
|
||||
//We'll use the username/password example here since it is the most common.
|
||||
//(do you know what movie this is from? ;)
|
||||
UsernamePasswordToken token = new UsernamePasswordToken("lonestarr", "vespa");
|
||||
//this is all you have to do to support 'remember me' (no config - built in!):
|
||||
token.setRememberMe(true);
|
||||
currentUser.login(token);
|
||||
}
|
||||
```
|
||||
### 对身份认证失败的情况进行异常处理
|
||||
如果在身份认证的过程中失败,可以通过如下代码捕获认证失败抛出的异常,并对异常进行异常处理
|
||||
```java
|
||||
try {
|
||||
currentUser.login( token );
|
||||
//if no exception, that's it, we're done!
|
||||
} catch ( UnknownAccountException uae ) {
|
||||
//username wasn't in the system, show them an error message?
|
||||
} catch ( IncorrectCredentialsException ice ) {
|
||||
//password didn't match, try again?
|
||||
} catch ( LockedAccountException lae ) {
|
||||
//account for that username is locked - can't login. Show them a message?
|
||||
}
|
||||
... more types exceptions to check if you want ...
|
||||
} catch ( AuthenticationException ae ) {
|
||||
//unexpected condition - error?
|
||||
}
|
||||
```
|
||||
### 对已经登录的用户进行role检验
|
||||
如果用户已经登录,如果要检测该用户是否被授予某role权限,可以通过如下代码进行检验
|
||||
```java
|
||||
if ( currentUser.hasRole( "schwartz" ) ) {
|
||||
log.info("May the Schwartz be with you!" );
|
||||
} else {
|
||||
log.info( "Hello, mere mortal." );
|
||||
}
|
||||
```
|
||||
### 检测某用户是否具有某项特定权限
|
||||
如果要对已经登录的用户执行检测,检测其是否被授予某项特定的前线,可以通过如下方式进行检测。
|
||||
```java
|
||||
if ( currentUser.isPermitted( "lightsaber:wield" ) ) {
|
||||
log.info("You may use a lightsaber ring. Use it wisely.");
|
||||
} else {
|
||||
log.info("Sorry, lightsaber rings are for schwartz masters only.");
|
||||
}
|
||||
```
|
||||
### 在实例级别对用户的权限进行检测
|
||||
在Shiro中,可以检测用户是否对某实例具有特定权限,通过如下代码:
|
||||
```java
|
||||
if ( currentUser.isPermitted( "winnebago:drive:eagle5" ) ) {
|
||||
log.info("You are permitted to 'drive' the 'winnebago' with license plate (id) 'eagle5'. " +
|
||||
"Here are the keys - have fun!");
|
||||
} else {
|
||||
log.info("Sorry, you aren't allowed to drive the 'eagle5' winnebago!");
|
||||
}
|
||||
```
|
||||
### 用户登出
|
||||
如果已经登录的用户想要执行登出操作,可以通过如下代码进行登录:
|
||||
```java
|
||||
currentUser.logout();
|
||||
```
|
||||
@@ -1,133 +1,133 @@
|
||||
- [Apache Shiro Realm](#apache-shiro-realm)
|
||||
- [Realm简介](#realm简介)
|
||||
- [Realm配置](#realm配置)
|
||||
- [Realm Authentication](#realm-authentication)
|
||||
- [支持Authentication](#支持authentication)
|
||||
- [处理AuthenticationToken](#处理authenticationtoken)
|
||||
- [credentials匹配](#credentials匹配)
|
||||
- [简单比较是否相等](#简单比较是否相等)
|
||||
- [Hash Credentials](#hash-credentials)
|
||||
- [通过sha-256算法来生成账户信息](#通过sha-256算法来生成账户信息)
|
||||
- [指定HashedCredentialsMatcher](#指定hashedcredentialsmatcher)
|
||||
- [SaltedAuthenticationInfo](#saltedauthenticationinfo)
|
||||
- [关闭Realm的Authentication](#关闭realm的authentication)
|
||||
- [Realm Authorization](#realm-authorization)
|
||||
- [基于role的authorization](#基于role的authorization)
|
||||
- [基于permission的authorization](#基于permission的authorization)
|
||||
|
||||
# Apache Shiro Realm
|
||||
## Realm简介
|
||||
Realm是一个组件,用来访问针对特定应用的安全数据,例如user、role或permission。Realm负责将这些安全信息翻译为Shiro能够理解的格式。
|
||||
由于大多数数据源都会同时存储authentication和authorization信息,故而Realm能够同时执行authentication和authorization操作。
|
||||
## Realm配置
|
||||
对于Realm的配置,可以在ini文件中进行如下配置:
|
||||
```ini
|
||||
fooRealm = com.company.foo.Realm
|
||||
barRealm = com.company.another.Realm
|
||||
bazRealm = com.company.baz.Realm
|
||||
|
||||
; 如下指定的顺序会影响Authentication/Authorization过程中的顺序
|
||||
securityManager.realms = $fooRealm, $barRealm, $bazRealm
|
||||
```
|
||||
## Realm Authentication
|
||||
### 支持Authentication
|
||||
在Realm被询问去执行Authentication时,首先会调用该Realm的supports方法,如果supports方法的返回值是true时getAuthenticationInfo方法才会被调用。
|
||||
通常情况下,Realm会对提交的Token类型进行检测,并查看当前Realm是否能对该类型Token进行处理。
|
||||
### 处理AuthenticationToken
|
||||
如果Realm支持该提交的Token类型,那么Authenticator会调用Realm的getAuthenticationInfo方法,该方法代表了通过Realm数据库来进行认证尝试。
|
||||
该方法会按照如下顺序进行执行:
|
||||
1. 查看Token中存储的principals信息
|
||||
2. 根据Token中的principals,在data source中查找对应的账户信息
|
||||
3. 确保提交Token中的credentials和data source中查找出的credentials相匹配
|
||||
4. 如果credentials匹配,那么会将用户账户的信息封装到AuthenticationInfo中并返回
|
||||
5. 如果credentials不匹配,会抛出AuthenticationException
|
||||
### credentials匹配
|
||||
为了确保在credentials匹配的过程中,该过程是可插入(pluggable)和可自定义的(customizable)的,AuthenticationRealm支持CredentialsMatcher的概念,通过CredentialsMatcher来进行credentials的比较。
|
||||
在从data source中查询到账户数据之后,会将其和提交Token中的credentials一起传递给CredentialsMatcher,由CredentialsMatcher来判断credentials是否相等。
|
||||
可以通过如下方式来定义CredentialsMatcher的比较逻辑:
|
||||
```java
|
||||
Realm myRealm = new com.company.shiro.realm.MyRealm();
|
||||
CredentialsMatcher customMatcher = new com.company.shiro.realm.CustomCredentialsMatcher();
|
||||
myRealm.setCredentialsMatcher(customMatcher);
|
||||
```
|
||||
或
|
||||
```ini
|
||||
[main]
|
||||
...
|
||||
customMatcher = com.company.shiro.realm.CustomCredentialsMatcher
|
||||
myRealm = com.company.shiro.realm.MyRealm
|
||||
myRealm.credentialsMatcher = $customMatcher
|
||||
...
|
||||
```
|
||||
### 简单比较是否相等
|
||||
Shiro中所有开箱即用的Realm,其实现都默认使用SimpleCredentialsMatcher,SimpleCredentialsMatcher简单会对存储在data source中的principals和提交Token中的credentials进行比较相等操作。
|
||||
### Hash Credentials
|
||||
将用户提交的principals不做任何转换直接存储在data source中是一种不安全的做法,通常是将其进行单向hash之后再存入数据库。
|
||||
这样可以确保用户的credentials不会以raw text的方式存储再data source中,即使数据库数据被泄露,用户credentials的原始值也不会被任何人知道。
|
||||
为了支持Token中credentials和data source中hash之后credentials的比较,Shiro提供了HashedCredentialsMatcher实现,可以通过配置HashedCredentialsMatcher来取代SimpleCredentialsMatcher。
|
||||
#### 通过sha-256算法来生成账户信息
|
||||
```java
|
||||
import org.apache.shiro.crypto.hash.Sha256Hash;
|
||||
import org.apache.shiro.crypto.RandomNumberGenerator;
|
||||
import org.apache.shiro.crypto.SecureRandomNumberGenerator;
|
||||
...
|
||||
|
||||
//We'll use a Random Number Generator to generate salts. This
|
||||
//is much more secure than using a username as a salt or not
|
||||
//having a salt at all. Shiro makes this easy.
|
||||
//
|
||||
//Note that a normal app would reference an attribute rather
|
||||
//than create a new RNG every time:
|
||||
RandomNumberGenerator rng = new SecureRandomNumberGenerator();
|
||||
Object salt = rng.nextBytes();
|
||||
|
||||
//Now hash the plain-text password with the random salt and multiple
|
||||
//iterations and then Base64-encode the value (requires less space than Hex):
|
||||
String hashedPasswordBase64 = new Sha256Hash(plainTextPassword, salt, 1024).toBase64();
|
||||
|
||||
User user = new User(username, hashedPasswordBase64);
|
||||
//save the salt with the new account. The HashedCredentialsMatcher
|
||||
//will need it later when handling login attempts:
|
||||
user.setPasswordSalt(salt);
|
||||
userDAO.create(user);
|
||||
```
|
||||
#### 指定HashedCredentialsMatcher
|
||||
可以通过如下方式来指定特定HashedCredentialsMatcher实现类。
|
||||
```ini
|
||||
[main]
|
||||
...
|
||||
credentialsMatcher = org.apache.shiro.authc.credential.Sha256CredentialsMatcher
|
||||
# base64 encoding, not hex in this example:
|
||||
credentialsMatcher.storedCredentialsHexEncoded = false
|
||||
credentialsMatcher.hashIterations = 1024
|
||||
# This next property is only needed in Shiro 1.0\. Remove it in 1.1 and later:
|
||||
credentialsMatcher.hashSalted = true
|
||||
|
||||
...
|
||||
myRealm = com.company.....
|
||||
myRealm.credentialsMatcher = $credentialsMatcher
|
||||
...
|
||||
```
|
||||
#### SaltedAuthenticationInfo
|
||||
如果制定了HashedCredentialsMatcher,那么Realm.getAuthenticationInfo必须返回一个SaltedAuthenticationInfo实例而不是普通的Authentication实例。该SaltedAuthenticationInfo确保在创建用户信息时使用的salt可以在CredentialsMatcher中被使用。
|
||||
HashedCredentialsMatcher在对Token中提交的credentials进行hash时,需要使用到salt值来将用户提交的credentials进行和创建用户时相同的散列。
|
||||
### 关闭Realm的Authentication
|
||||
如果对某个Realm,想要对该realm不执行Authentication,可以将其实现类的supports方法只返回false,此时该realm在authentication过程中绝对不会被询问。
|
||||
## Realm Authorization
|
||||
SecurityManager将校验permission和role的工作委托给了Authorizer,默认是ModularRealmAuthorizer。
|
||||
### 基于role的authorization
|
||||
当subject的hasRoles或checkRoles被调用,其具体的执行流程如下:
|
||||
1. subject将校验role的任务委托给SecurityManager
|
||||
2. SecurityManager将任务委托给Authorizer
|
||||
3. Authorizier会调用所有的Authorizing Realm直到该role被分配给subject。如果所有realm都没有授予subject该role,那么访问失败,返回false。
|
||||
4. Authorizing Realm的AuthorizationInfo.getRoles方法会获取所有分配给该subject的role
|
||||
5. 如果待检测的role在getRoles返回的role list中,那么授权成功,subject可以对该资源进行访问
|
||||
### 基于permission的authorization
|
||||
当subject的isPermitted或checkPermission方法被调用时,其执行流程如下:
|
||||
1. subject将检测Permission的任务委托给SecurityManager
|
||||
2. SecurityManager将该任务委托给Authorizer
|
||||
3. Authorizer会以此访问所有的Authorizer Realm直到Permission被授予。如果所有的realm都没有授予该subject权限,那么subject授权失败。
|
||||
4. Realm按照如下顺序来检测Permission是否被授予:
|
||||
1. 其会调用AuthorizationInfo的getObjectPermissions方法和getStringPermissions方法并聚合结果,从而获取直接分配给该subject的所有权限
|
||||
2. 如果RolePermissionRegister被注册,那么会根据subject被授予的role来获取role相关的permission,根据RolePermissionResolver.resolvePermissionsInRole()方法
|
||||
3. 对于上述返回的权限集合,implies方法会被调用,用来检测待检测权限是否隐含在其中
|
||||
- [Apache Shiro Realm](#apache-shiro-realm)
|
||||
- [Realm简介](#realm简介)
|
||||
- [Realm配置](#realm配置)
|
||||
- [Realm Authentication](#realm-authentication)
|
||||
- [支持Authentication](#支持authentication)
|
||||
- [处理AuthenticationToken](#处理authenticationtoken)
|
||||
- [credentials匹配](#credentials匹配)
|
||||
- [简单比较是否相等](#简单比较是否相等)
|
||||
- [Hash Credentials](#hash-credentials)
|
||||
- [通过sha-256算法来生成账户信息](#通过sha-256算法来生成账户信息)
|
||||
- [指定HashedCredentialsMatcher](#指定hashedcredentialsmatcher)
|
||||
- [SaltedAuthenticationInfo](#saltedauthenticationinfo)
|
||||
- [关闭Realm的Authentication](#关闭realm的authentication)
|
||||
- [Realm Authorization](#realm-authorization)
|
||||
- [基于role的authorization](#基于role的authorization)
|
||||
- [基于permission的authorization](#基于permission的authorization)
|
||||
|
||||
# Apache Shiro Realm
|
||||
## Realm简介
|
||||
Realm是一个组件,用来访问针对特定应用的安全数据,例如user、role或permission。Realm负责将这些安全信息翻译为Shiro能够理解的格式。
|
||||
由于大多数数据源都会同时存储authentication和authorization信息,故而Realm能够同时执行authentication和authorization操作。
|
||||
## Realm配置
|
||||
对于Realm的配置,可以在ini文件中进行如下配置:
|
||||
```ini
|
||||
fooRealm = com.company.foo.Realm
|
||||
barRealm = com.company.another.Realm
|
||||
bazRealm = com.company.baz.Realm
|
||||
|
||||
; 如下指定的顺序会影响Authentication/Authorization过程中的顺序
|
||||
securityManager.realms = $fooRealm, $barRealm, $bazRealm
|
||||
```
|
||||
## Realm Authentication
|
||||
### 支持Authentication
|
||||
在Realm被询问去执行Authentication时,首先会调用该Realm的supports方法,如果supports方法的返回值是true时getAuthenticationInfo方法才会被调用。
|
||||
通常情况下,Realm会对提交的Token类型进行检测,并查看当前Realm是否能对该类型Token进行处理。
|
||||
### 处理AuthenticationToken
|
||||
如果Realm支持该提交的Token类型,那么Authenticator会调用Realm的getAuthenticationInfo方法,该方法代表了通过Realm数据库来进行认证尝试。
|
||||
该方法会按照如下顺序进行执行:
|
||||
1. 查看Token中存储的principals信息
|
||||
2. 根据Token中的principals,在data source中查找对应的账户信息
|
||||
3. 确保提交Token中的credentials和data source中查找出的credentials相匹配
|
||||
4. 如果credentials匹配,那么会将用户账户的信息封装到AuthenticationInfo中并返回
|
||||
5. 如果credentials不匹配,会抛出AuthenticationException
|
||||
### credentials匹配
|
||||
为了确保在credentials匹配的过程中,该过程是可插入(pluggable)和可自定义的(customizable)的,AuthenticationRealm支持CredentialsMatcher的概念,通过CredentialsMatcher来进行credentials的比较。
|
||||
在从data source中查询到账户数据之后,会将其和提交Token中的credentials一起传递给CredentialsMatcher,由CredentialsMatcher来判断credentials是否相等。
|
||||
可以通过如下方式来定义CredentialsMatcher的比较逻辑:
|
||||
```java
|
||||
Realm myRealm = new com.company.shiro.realm.MyRealm();
|
||||
CredentialsMatcher customMatcher = new com.company.shiro.realm.CustomCredentialsMatcher();
|
||||
myRealm.setCredentialsMatcher(customMatcher);
|
||||
```
|
||||
或
|
||||
```ini
|
||||
[main]
|
||||
...
|
||||
customMatcher = com.company.shiro.realm.CustomCredentialsMatcher
|
||||
myRealm = com.company.shiro.realm.MyRealm
|
||||
myRealm.credentialsMatcher = $customMatcher
|
||||
...
|
||||
```
|
||||
### 简单比较是否相等
|
||||
Shiro中所有开箱即用的Realm,其实现都默认使用SimpleCredentialsMatcher,SimpleCredentialsMatcher简单会对存储在data source中的principals和提交Token中的credentials进行比较相等操作。
|
||||
### Hash Credentials
|
||||
将用户提交的principals不做任何转换直接存储在data source中是一种不安全的做法,通常是将其进行单向hash之后再存入数据库。
|
||||
这样可以确保用户的credentials不会以raw text的方式存储再data source中,即使数据库数据被泄露,用户credentials的原始值也不会被任何人知道。
|
||||
为了支持Token中credentials和data source中hash之后credentials的比较,Shiro提供了HashedCredentialsMatcher实现,可以通过配置HashedCredentialsMatcher来取代SimpleCredentialsMatcher。
|
||||
#### 通过sha-256算法来生成账户信息
|
||||
```java
|
||||
import org.apache.shiro.crypto.hash.Sha256Hash;
|
||||
import org.apache.shiro.crypto.RandomNumberGenerator;
|
||||
import org.apache.shiro.crypto.SecureRandomNumberGenerator;
|
||||
...
|
||||
|
||||
//We'll use a Random Number Generator to generate salts. This
|
||||
//is much more secure than using a username as a salt or not
|
||||
//having a salt at all. Shiro makes this easy.
|
||||
//
|
||||
//Note that a normal app would reference an attribute rather
|
||||
//than create a new RNG every time:
|
||||
RandomNumberGenerator rng = new SecureRandomNumberGenerator();
|
||||
Object salt = rng.nextBytes();
|
||||
|
||||
//Now hash the plain-text password with the random salt and multiple
|
||||
//iterations and then Base64-encode the value (requires less space than Hex):
|
||||
String hashedPasswordBase64 = new Sha256Hash(plainTextPassword, salt, 1024).toBase64();
|
||||
|
||||
User user = new User(username, hashedPasswordBase64);
|
||||
//save the salt with the new account. The HashedCredentialsMatcher
|
||||
//will need it later when handling login attempts:
|
||||
user.setPasswordSalt(salt);
|
||||
userDAO.create(user);
|
||||
```
|
||||
#### 指定HashedCredentialsMatcher
|
||||
可以通过如下方式来指定特定HashedCredentialsMatcher实现类。
|
||||
```ini
|
||||
[main]
|
||||
...
|
||||
credentialsMatcher = org.apache.shiro.authc.credential.Sha256CredentialsMatcher
|
||||
# base64 encoding, not hex in this example:
|
||||
credentialsMatcher.storedCredentialsHexEncoded = false
|
||||
credentialsMatcher.hashIterations = 1024
|
||||
# This next property is only needed in Shiro 1.0\. Remove it in 1.1 and later:
|
||||
credentialsMatcher.hashSalted = true
|
||||
|
||||
...
|
||||
myRealm = com.company.....
|
||||
myRealm.credentialsMatcher = $credentialsMatcher
|
||||
...
|
||||
```
|
||||
#### SaltedAuthenticationInfo
|
||||
如果制定了HashedCredentialsMatcher,那么Realm.getAuthenticationInfo必须返回一个SaltedAuthenticationInfo实例而不是普通的Authentication实例。该SaltedAuthenticationInfo确保在创建用户信息时使用的salt可以在CredentialsMatcher中被使用。
|
||||
HashedCredentialsMatcher在对Token中提交的credentials进行hash时,需要使用到salt值来将用户提交的credentials进行和创建用户时相同的散列。
|
||||
### 关闭Realm的Authentication
|
||||
如果对某个Realm,想要对该realm不执行Authentication,可以将其实现类的supports方法只返回false,此时该realm在authentication过程中绝对不会被询问。
|
||||
## Realm Authorization
|
||||
SecurityManager将校验permission和role的工作委托给了Authorizer,默认是ModularRealmAuthorizer。
|
||||
### 基于role的authorization
|
||||
当subject的hasRoles或checkRoles被调用,其具体的执行流程如下:
|
||||
1. subject将校验role的任务委托给SecurityManager
|
||||
2. SecurityManager将任务委托给Authorizer
|
||||
3. Authorizier会调用所有的Authorizing Realm直到该role被分配给subject。如果所有realm都没有授予subject该role,那么访问失败,返回false。
|
||||
4. Authorizing Realm的AuthorizationInfo.getRoles方法会获取所有分配给该subject的role
|
||||
5. 如果待检测的role在getRoles返回的role list中,那么授权成功,subject可以对该资源进行访问
|
||||
### 基于permission的authorization
|
||||
当subject的isPermitted或checkPermission方法被调用时,其执行流程如下:
|
||||
1. subject将检测Permission的任务委托给SecurityManager
|
||||
2. SecurityManager将该任务委托给Authorizer
|
||||
3. Authorizer会以此访问所有的Authorizer Realm直到Permission被授予。如果所有的realm都没有授予该subject权限,那么subject授权失败。
|
||||
4. Realm按照如下顺序来检测Permission是否被授予:
|
||||
1. 其会调用AuthorizationInfo的getObjectPermissions方法和getStringPermissions方法并聚合结果,从而获取直接分配给该subject的所有权限
|
||||
2. 如果RolePermissionRegister被注册,那么会根据subject被授予的role来获取role相关的permission,根据RolePermissionResolver.resolvePermissionsInRole()方法
|
||||
3. 对于上述返回的权限集合,implies方法会被调用,用来检测待检测权限是否隐含在其中
|
||||
|
||||
@@ -1,175 +1,175 @@
|
||||
- [Apache Shiro](#apache-shiro)
|
||||
- [Shiro简介](#shiro简介)
|
||||
- [Shiro中常用的概念](#shiro中常用的概念)
|
||||
- [Subject](#subject)
|
||||
- [SecurityManager](#securitymanager)
|
||||
- [realms](#realms)
|
||||
- [Authentication](#authentication)
|
||||
- [Authorization](#authorization)
|
||||
- [Session Management](#session-management)
|
||||
- [Shiro Session可在任何应用中使用](#shiro-session可在任何应用中使用)
|
||||
- [Shiro加密](#shiro加密)
|
||||
- [shiro hash](#shiro-hash)
|
||||
- [Shiro Ciphers](#shiro-ciphers)
|
||||
- [Shiro框架的Web支持](#shiro框架的web支持)
|
||||
- [Web Session管理](#web-session管理)
|
||||
- [Shiro Native Session](#shiro-native-session)
|
||||
|
||||
# Apache Shiro
|
||||
## Shiro简介
|
||||
Shiro是一个简单易用且功能强大的Java安全框架,用于实现认证、授权、加密、session管理等场景,并且Shiro可以被用于任何应用,包括命令行应用、移动应用、大型web应用或是企业应用。
|
||||
Shiro在如下方面提供Security API:
|
||||
- Authentication:为用户提供身份认证
|
||||
- Authorization:为应用提供用户的访问控制
|
||||
- 加密:避免应用数据处于明文状态
|
||||
- session管理:每个用户的时间敏感状态
|
||||
## Shiro中常用的概念
|
||||
Shiro框架的结构主要分为三个部分:Subject、SecurityManager、Realms
|
||||
### Subject
|
||||
Subject:Subject是一个安全术语,通常意味着当前执行的用户。
|
||||
```java
|
||||
import org.apache.shiro.subject.Subject;
|
||||
import org.apache.shiro.SecurityUtils;
|
||||
// 获取Subject对象
|
||||
Subject currentUser = SecurityUtils.getSubject();
|
||||
```
|
||||
### SecurityManager
|
||||
相对于Subject代表对于当前用户的安全操作,SecurityManager代表对所有用户的安全操作。SecurityManager是Shiro结构的核心,其由多个security component嵌套组成。
|
||||
> 一旦SecurityManager和其嵌套的security component被配置完成,那么用户将不再使用SecurityManager而是调用Subject API。
|
||||
|
||||
对每个应用中,只存在一个SecurityManager,SecurityManager是应用范围内的单例。默认SecurityManager的实现是POJO,可以通过java代码、Spring xml、yaml、properties等方式来进行配置。
|
||||
|
||||
### realms
|
||||
realms是shiro和应用中security data(如账户登录的登录数据或授权管理的权限数据)之间的连接器。当Shiro和security data进行交互时,shiro会从配置好的一个或者多个realm中获取security data。
|
||||
> 在上述情况下,realm类似于一个安全数据的特定DAO,其封装了到数据源连接的详细信息并且为Shiro提供安全数据。当配置Shiro时,必须指定至少一个Realm用于身份认证和权限认证。
|
||||
|
||||
> Shiro提供了开箱即用的Realm来连接很多种security data source,比如LDAP,关系型数据库(JDBC),基于文本配置的data source例如ini或properties文件。可以通过自定义Realm的实现来访问自定义的data source,如果默认的Realm不能满足需求。
|
||||
|
||||
## Authentication
|
||||
认证过程用于认证用户的身份。认证过程的主要流程如下:
|
||||
1. 收集用户的身份标识信息(通常称之为主体,principal)和身份证明(通常称之为凭据,credentials)
|
||||
2. 向系统提交主体和凭据
|
||||
3. 如果提交的凭据和系统中该主体对应的凭据相同,那么该用户会被认为是“通过认证的”。如果提交的凭据不符,那么该用户会被认为是“认证失败的”。
|
||||
```java
|
||||
// 认证流程代码
|
||||
//1. Acquire submitted principals and credentials:
|
||||
AuthenticationToken token =
|
||||
new UsernamePasswordToken(username, password);
|
||||
//2. Get the current Subject:
|
||||
Subject currentUser = SecurityUtils.getSubject();
|
||||
|
||||
//3. Login:
|
||||
currentUser.login(token);
|
||||
```
|
||||
当login方法被调用时,SecurityMananger将会收到AuthenticationToken并且将其分配到一个或多个已经配置好的Realm中,并且让每个Realm执行需要的认证流程。每个Reaml都能对提交的AuthenticaitonToken做出处理。
|
||||
如果认证过程失败,那么会抛出AuthenticationException,可以通过捕获该异常来对失败的认证进行处理。
|
||||
```java
|
||||
//3. Login:
|
||||
try {
|
||||
currentUser.login(token);
|
||||
} catch (IncorrectCredentialsException ice) { …
|
||||
} catch (LockedAccountException lae) { …
|
||||
}
|
||||
…
|
||||
catch (AuthenticationException ae) {…
|
||||
}
|
||||
```
|
||||
**当用户通过身份认证之后,其被认为是“通过身份认证的”,并且被允许使用应用。但是,通过身份认证并不意味着可以在系统中执行任何操作。通过授权,可以决定用户能够在系统中执行哪些操作。**
|
||||
|
||||
## Authorization
|
||||
Authorization的实质是访问控制,通常用于控制用户在系统中能够使用哪些资源。大多数情况下,可以通过role或permission等形式来实现访问控制,用户通过分配给他们的角色或者权限来执行操作。通过检查用户的role或者permission,系统可以决定将哪些功能暴露给用户。
|
||||
```java
|
||||
// 通过如下代码,Subject可以实现对用户的role检测
|
||||
if ( subject.hasRole(“administrator”) ) {
|
||||
//show the ‘Create User’ button
|
||||
} else {
|
||||
//grey-out the button?
|
||||
}
|
||||
|
||||
// 通过如下代码,可以实现权限分配而不是角色检测
|
||||
if ( subject.isPermitted(“user:create”) ) {
|
||||
//show the ‘Create User’ button
|
||||
} else {
|
||||
//grey-out the button?
|
||||
}
|
||||
```
|
||||
权限控制甚至支持非常细粒度的权限,譬如实例级别的权限控制。
|
||||
```java
|
||||
// 如下代码检测用户是否拥有删除jsmith用户的权限
|
||||
if ( subject.isPermitted(“user:delete:jsmith”) ) {
|
||||
//delete the ‘jsmith’ user
|
||||
} else {
|
||||
//don’t delete ‘jsmith’
|
||||
}
|
||||
```
|
||||
和Authentication类似,Authorization也会进入SecurityManager,并且通过一个或者多个Realm来决定是否允许访问。
|
||||
**根据需要,Realm既会响应Authentication过程,也会响应Authority过程**
|
||||
|
||||
## Session Management
|
||||
Apache提供了一致的会话管理,在任何类型和架构的程序中都可以使用,该Session API从小型的守护进程到大型集群web应用都可以使用。
|
||||
并且,Shiro提供的Session API是容器无关的,在任何容器环境下Session API都相同。Shiro结构也支持可插入的session存储,可以将session存储在企业缓存、关系型数据库或者nosql中。
|
||||
Shiro Seesion的另一个好处是Shiro Session可以在不同技术实现的客户端之间进行共享,譬如Swing桌面客户端可以和web客户端一起加入同一个应用会话(用户同时使用swing客户端和web客户端时很有用)。
|
||||
```java
|
||||
/**
|
||||
* 用户获取Session
|
||||
*/
|
||||
// 如果当前用户存在会话,则获取已存在会话,
|
||||
// 如果当前用户不存在会话,则创建新的会话
|
||||
Session session = subject.getSession();
|
||||
// 接受一个boolean值,该boolean值标识如果当前用户不存在会话,是否创建一个新的会话
|
||||
Session session = subject.getSession(boolean create);
|
||||
```
|
||||
### Shiro Session可在任何应用中使用
|
||||
```java
|
||||
Session session = subject.getSession();
|
||||
session.getAttribute(“key”, someValue);
|
||||
Date start = session.getStartTimestamp();
|
||||
Date timestamp = session.getLastAccessTime();
|
||||
session.setTimeout(millis);
|
||||
```
|
||||
## Shiro加密
|
||||
Shiro加密是独立于用户(Subject)的,故而Shiro加密可以独立于Subject使用。
|
||||
Shiro加密主要关注于如下两个模块:
|
||||
- hash加密(又名消息摘要,message digest)
|
||||
- ciphers加密
|
||||
### shiro hash
|
||||
在Shiro hash中,如果想要快速计算文件的MD5值,可以通过如下方式快速计算:
|
||||
```java
|
||||
String hex = new Md5Hash(myFile).toHex();
|
||||
```
|
||||
在shiro hash中,如果想要对密码进行sha-512进行hash操作并且对结果进行base64编码成可打印字符串,可以进行如下操作:
|
||||
```java
|
||||
String encodedPassword =
|
||||
new Sha512Hash(password, salt, count).toBase64();
|
||||
```
|
||||
Shiro框架在很大程度上简化了hash和编码。
|
||||
### Shiro Ciphers
|
||||
Ciphers是一种算法,可以通过指定的key将加密后的数据还原到加密之前(不同于hash操作,hash操作通常是不可逆的)。通常用Ciphers来保证数据传输时的安全,防止数据在传输时被窥探。
|
||||
Shiro通过引入CipherService API来简化加密的流程,**CipherService是一个简单,无状态并且线程安全的API,可以对应用数据进行加密或者解密操作。**
|
||||
```java
|
||||
// Shiro CipherService对数据进行加密操作
|
||||
|
||||
AesCipherService cipherService = new AesCipherService();
|
||||
cipherService.setKeySize(256);
|
||||
//create a test key:
|
||||
byte[] testKey = cipherService.generateNewKey();
|
||||
|
||||
//encrypt a file’s bytes:
|
||||
byte[] encrypted =
|
||||
cipherService.encrypt(fileBytes, testKey);
|
||||
```
|
||||
## Shiro框架的Web支持
|
||||
### Web Session管理
|
||||
对于web应用,shiro默认情况下其session会使用已经存在的servlet容器session。当使用subject.getSession()或者subject.getSession(boolean)获取session实例时,**Shiro将会返回一个基于Servlet容器的HttpSession实例来作为Session实例返回值**。
|
||||
> 对于Shiro来说,其业务层代码通过subject.getSession来获取Shiro Session实例,即使当前运行于Servlet容器,业务层代码在与Shiro Session交互时也不知道与其交互的是HttpSession。
|
||||
> 故而在使用Shiro Session时,其Session是独立与环境的,Web应用和非Web应用都可以通过相同的Shiro Session API与Shiro Session进行交互,而Shiro Session是否是基于Servlet容器的HttpSession,用户是无感知的。
|
||||
|
||||
### Shiro Native Session
|
||||
当启用Shiro Native Session之后,对于Web应用,如果想使用Shiro Session来代替基于Servlet容器的HttpSession,无需修改HttpServletRequest.getSession()和HttpSession API为Shiro Session API。
|
||||
Shiro Session完全实现了Servlet Session的标准,以此支持Shiro Session在Web应用中的使用。在使用了Shiro Native Session后,任何对HttpServletRequest和HttpSession API的调用都会被Shiro拦截,Shiro会用Shiro Native Session API来代理这些请求。
|
||||
> **故而,当想要在Web环境中使用Shiro Session API时,无需变动Web环境先前未使用Shiro Session API时的任何代码。**
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
- [Apache Shiro](#apache-shiro)
|
||||
- [Shiro简介](#shiro简介)
|
||||
- [Shiro中常用的概念](#shiro中常用的概念)
|
||||
- [Subject](#subject)
|
||||
- [SecurityManager](#securitymanager)
|
||||
- [realms](#realms)
|
||||
- [Authentication](#authentication)
|
||||
- [Authorization](#authorization)
|
||||
- [Session Management](#session-management)
|
||||
- [Shiro Session可在任何应用中使用](#shiro-session可在任何应用中使用)
|
||||
- [Shiro加密](#shiro加密)
|
||||
- [shiro hash](#shiro-hash)
|
||||
- [Shiro Ciphers](#shiro-ciphers)
|
||||
- [Shiro框架的Web支持](#shiro框架的web支持)
|
||||
- [Web Session管理](#web-session管理)
|
||||
- [Shiro Native Session](#shiro-native-session)
|
||||
|
||||
# Apache Shiro
|
||||
## Shiro简介
|
||||
Shiro是一个简单易用且功能强大的Java安全框架,用于实现认证、授权、加密、session管理等场景,并且Shiro可以被用于任何应用,包括命令行应用、移动应用、大型web应用或是企业应用。
|
||||
Shiro在如下方面提供Security API:
|
||||
- Authentication:为用户提供身份认证
|
||||
- Authorization:为应用提供用户的访问控制
|
||||
- 加密:避免应用数据处于明文状态
|
||||
- session管理:每个用户的时间敏感状态
|
||||
## Shiro中常用的概念
|
||||
Shiro框架的结构主要分为三个部分:Subject、SecurityManager、Realms
|
||||
### Subject
|
||||
Subject:Subject是一个安全术语,通常意味着当前执行的用户。
|
||||
```java
|
||||
import org.apache.shiro.subject.Subject;
|
||||
import org.apache.shiro.SecurityUtils;
|
||||
// 获取Subject对象
|
||||
Subject currentUser = SecurityUtils.getSubject();
|
||||
```
|
||||
### SecurityManager
|
||||
相对于Subject代表对于当前用户的安全操作,SecurityManager代表对所有用户的安全操作。SecurityManager是Shiro结构的核心,其由多个security component嵌套组成。
|
||||
> 一旦SecurityManager和其嵌套的security component被配置完成,那么用户将不再使用SecurityManager而是调用Subject API。
|
||||
|
||||
对每个应用中,只存在一个SecurityManager,SecurityManager是应用范围内的单例。默认SecurityManager的实现是POJO,可以通过java代码、Spring xml、yaml、properties等方式来进行配置。
|
||||
|
||||
### realms
|
||||
realms是shiro和应用中security data(如账户登录的登录数据或授权管理的权限数据)之间的连接器。当Shiro和security data进行交互时,shiro会从配置好的一个或者多个realm中获取security data。
|
||||
> 在上述情况下,realm类似于一个安全数据的特定DAO,其封装了到数据源连接的详细信息并且为Shiro提供安全数据。当配置Shiro时,必须指定至少一个Realm用于身份认证和权限认证。
|
||||
|
||||
> Shiro提供了开箱即用的Realm来连接很多种security data source,比如LDAP,关系型数据库(JDBC),基于文本配置的data source例如ini或properties文件。可以通过自定义Realm的实现来访问自定义的data source,如果默认的Realm不能满足需求。
|
||||
|
||||
## Authentication
|
||||
认证过程用于认证用户的身份。认证过程的主要流程如下:
|
||||
1. 收集用户的身份标识信息(通常称之为主体,principal)和身份证明(通常称之为凭据,credentials)
|
||||
2. 向系统提交主体和凭据
|
||||
3. 如果提交的凭据和系统中该主体对应的凭据相同,那么该用户会被认为是“通过认证的”。如果提交的凭据不符,那么该用户会被认为是“认证失败的”。
|
||||
```java
|
||||
// 认证流程代码
|
||||
//1. Acquire submitted principals and credentials:
|
||||
AuthenticationToken token =
|
||||
new UsernamePasswordToken(username, password);
|
||||
//2. Get the current Subject:
|
||||
Subject currentUser = SecurityUtils.getSubject();
|
||||
|
||||
//3. Login:
|
||||
currentUser.login(token);
|
||||
```
|
||||
当login方法被调用时,SecurityMananger将会收到AuthenticationToken并且将其分配到一个或多个已经配置好的Realm中,并且让每个Realm执行需要的认证流程。每个Reaml都能对提交的AuthenticaitonToken做出处理。
|
||||
如果认证过程失败,那么会抛出AuthenticationException,可以通过捕获该异常来对失败的认证进行处理。
|
||||
```java
|
||||
//3. Login:
|
||||
try {
|
||||
currentUser.login(token);
|
||||
} catch (IncorrectCredentialsException ice) { …
|
||||
} catch (LockedAccountException lae) { …
|
||||
}
|
||||
…
|
||||
catch (AuthenticationException ae) {…
|
||||
}
|
||||
```
|
||||
**当用户通过身份认证之后,其被认为是“通过身份认证的”,并且被允许使用应用。但是,通过身份认证并不意味着可以在系统中执行任何操作。通过授权,可以决定用户能够在系统中执行哪些操作。**
|
||||
|
||||
## Authorization
|
||||
Authorization的实质是访问控制,通常用于控制用户在系统中能够使用哪些资源。大多数情况下,可以通过role或permission等形式来实现访问控制,用户通过分配给他们的角色或者权限来执行操作。通过检查用户的role或者permission,系统可以决定将哪些功能暴露给用户。
|
||||
```java
|
||||
// 通过如下代码,Subject可以实现对用户的role检测
|
||||
if ( subject.hasRole(“administrator”) ) {
|
||||
//show the ‘Create User’ button
|
||||
} else {
|
||||
//grey-out the button?
|
||||
}
|
||||
|
||||
// 通过如下代码,可以实现权限分配而不是角色检测
|
||||
if ( subject.isPermitted(“user:create”) ) {
|
||||
//show the ‘Create User’ button
|
||||
} else {
|
||||
//grey-out the button?
|
||||
}
|
||||
```
|
||||
权限控制甚至支持非常细粒度的权限,譬如实例级别的权限控制。
|
||||
```java
|
||||
// 如下代码检测用户是否拥有删除jsmith用户的权限
|
||||
if ( subject.isPermitted(“user:delete:jsmith”) ) {
|
||||
//delete the ‘jsmith’ user
|
||||
} else {
|
||||
//don’t delete ‘jsmith’
|
||||
}
|
||||
```
|
||||
和Authentication类似,Authorization也会进入SecurityManager,并且通过一个或者多个Realm来决定是否允许访问。
|
||||
**根据需要,Realm既会响应Authentication过程,也会响应Authority过程**
|
||||
|
||||
## Session Management
|
||||
Apache提供了一致的会话管理,在任何类型和架构的程序中都可以使用,该Session API从小型的守护进程到大型集群web应用都可以使用。
|
||||
并且,Shiro提供的Session API是容器无关的,在任何容器环境下Session API都相同。Shiro结构也支持可插入的session存储,可以将session存储在企业缓存、关系型数据库或者nosql中。
|
||||
Shiro Seesion的另一个好处是Shiro Session可以在不同技术实现的客户端之间进行共享,譬如Swing桌面客户端可以和web客户端一起加入同一个应用会话(用户同时使用swing客户端和web客户端时很有用)。
|
||||
```java
|
||||
/**
|
||||
* 用户获取Session
|
||||
*/
|
||||
// 如果当前用户存在会话,则获取已存在会话,
|
||||
// 如果当前用户不存在会话,则创建新的会话
|
||||
Session session = subject.getSession();
|
||||
// 接受一个boolean值,该boolean值标识如果当前用户不存在会话,是否创建一个新的会话
|
||||
Session session = subject.getSession(boolean create);
|
||||
```
|
||||
### Shiro Session可在任何应用中使用
|
||||
```java
|
||||
Session session = subject.getSession();
|
||||
session.getAttribute(“key”, someValue);
|
||||
Date start = session.getStartTimestamp();
|
||||
Date timestamp = session.getLastAccessTime();
|
||||
session.setTimeout(millis);
|
||||
```
|
||||
## Shiro加密
|
||||
Shiro加密是独立于用户(Subject)的,故而Shiro加密可以独立于Subject使用。
|
||||
Shiro加密主要关注于如下两个模块:
|
||||
- hash加密(又名消息摘要,message digest)
|
||||
- ciphers加密
|
||||
### shiro hash
|
||||
在Shiro hash中,如果想要快速计算文件的MD5值,可以通过如下方式快速计算:
|
||||
```java
|
||||
String hex = new Md5Hash(myFile).toHex();
|
||||
```
|
||||
在shiro hash中,如果想要对密码进行sha-512进行hash操作并且对结果进行base64编码成可打印字符串,可以进行如下操作:
|
||||
```java
|
||||
String encodedPassword =
|
||||
new Sha512Hash(password, salt, count).toBase64();
|
||||
```
|
||||
Shiro框架在很大程度上简化了hash和编码。
|
||||
### Shiro Ciphers
|
||||
Ciphers是一种算法,可以通过指定的key将加密后的数据还原到加密之前(不同于hash操作,hash操作通常是不可逆的)。通常用Ciphers来保证数据传输时的安全,防止数据在传输时被窥探。
|
||||
Shiro通过引入CipherService API来简化加密的流程,**CipherService是一个简单,无状态并且线程安全的API,可以对应用数据进行加密或者解密操作。**
|
||||
```java
|
||||
// Shiro CipherService对数据进行加密操作
|
||||
|
||||
AesCipherService cipherService = new AesCipherService();
|
||||
cipherService.setKeySize(256);
|
||||
//create a test key:
|
||||
byte[] testKey = cipherService.generateNewKey();
|
||||
|
||||
//encrypt a file’s bytes:
|
||||
byte[] encrypted =
|
||||
cipherService.encrypt(fileBytes, testKey);
|
||||
```
|
||||
## Shiro框架的Web支持
|
||||
### Web Session管理
|
||||
对于web应用,shiro默认情况下其session会使用已经存在的servlet容器session。当使用subject.getSession()或者subject.getSession(boolean)获取session实例时,**Shiro将会返回一个基于Servlet容器的HttpSession实例来作为Session实例返回值**。
|
||||
> 对于Shiro来说,其业务层代码通过subject.getSession来获取Shiro Session实例,即使当前运行于Servlet容器,业务层代码在与Shiro Session交互时也不知道与其交互的是HttpSession。
|
||||
> 故而在使用Shiro Session时,其Session是独立与环境的,Web应用和非Web应用都可以通过相同的Shiro Session API与Shiro Session进行交互,而Shiro Session是否是基于Servlet容器的HttpSession,用户是无感知的。
|
||||
|
||||
### Shiro Native Session
|
||||
当启用Shiro Native Session之后,对于Web应用,如果想使用Shiro Session来代替基于Servlet容器的HttpSession,无需修改HttpServletRequest.getSession()和HttpSession API为Shiro Session API。
|
||||
Shiro Session完全实现了Servlet Session的标准,以此支持Shiro Session在Web应用中的使用。在使用了Shiro Native Session后,任何对HttpServletRequest和HttpSession API的调用都会被Shiro拦截,Shiro会用Shiro Native Session API来代理这些请求。
|
||||
> **故而,当想要在Web环境中使用Shiro Session API时,无需变动Web环境先前未使用Shiro Session API时的任何代码。**
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -1,363 +1,363 @@
|
||||
# Spring Security
|
||||
## Spring Security简介
|
||||
Spring Security作为一个安全框架,向使用者提供了用户认证、授权、常规攻击保护等功能。
|
||||
## Spring Security自动配置
|
||||
默认情况下,在引入Spring Security的启动器依赖之后,Spring Boot自动配置会做如下工作:
|
||||
- 启用Spring Security的默认配置,创建一个的bean对象,bean对象名称为“springSecurityFilterChain”,bean对象类型为SecurityFilterChain,实现了Filter。该bean对象为负责应用中所有与安全相关的操作(例如验证提交的username和password,保护应用的url等)
|
||||
- 创建一个UserDetailsService的bean对象,并且产生一个为”user“的username和一个随机产生的password,随机产生的password会输出在console日志中
|
||||
- 将名为springSecurityFilterChain的bean对象注册到servlet容器中,用来对每个servlet请求进行处理
|
||||
> Spring Security会通过Spring Security会通过BCtypt(Hash解密算法)来对密码的存储进行保护
|
||||
|
||||
## Spring Security结构
|
||||
### DelegatingFilterProxy
|
||||
Spring提供了Filter的一个实现类DelegatingFilterProxy,其将Servlet容器的生命周期和Spring的ApplicationContext连接在一起。
|
||||
***DelegatingFilterProxy会通过标准的servlet容器机制被注册,但是将所有工作都委托给Spring容器中实现了Filter的bean对象***。
|
||||
> DelegatingFilterProxy会在ApplicationContext中查找实现了Filter的bean对象,并且调用该bean对象的doFilter方法
|
||||
> ```java
|
||||
> public void doFilter(ServletRequest request,
|
||||
> ServletResponse response, FilterChain chain) {
|
||||
> // Lazily get Filter that was registered as a Spring Bean
|
||||
> // For the example in DelegatingFilterProxy delegate is an instance of Bean Filter0
|
||||
> Filter delegate = getFilterBean(someBeanName);
|
||||
> // delegate work to the Spring Bean
|
||||
> delegate.doFilter(request, response);
|
||||
> }
|
||||
> ```
|
||||
|
||||
### FilterChainProxy
|
||||
Spring Security支持FilterChainProxy,FilterChainProxy是一个由Spring Security提供的特殊Filter,FilterChainProxy通过SecurityFilterChain允许向多个Filters委托工作。
|
||||
***FilterChainProxy是一个bean对象,通过被包含在DelegatingFilterProxy中。***
|
||||
|
||||
### SecurityFilterChain
|
||||
SecurityFilterChain通常被FilterChainProxy使用,用来决定在该次请求中调用那些Spring Security Filters。
|
||||
|
||||
### SecurityFilters
|
||||
Security Filters通过SecurityFilterChain API被插入到FilterChainProxy中。
|
||||
### 处理Security异常
|
||||
ExceptionTranslationFilter将认证异常和权限异常翻译为http response。
|
||||
> ExceptionTranslationFilter被插入到FilterChainProxy中,作为SecurityFilterChain中的一个。
|
||||
> 如果应用程序没有抛出AccessDeniedException或AuthenticationException,那么ExceptionTranslationFilter并不会做任何事情。
|
||||
|
||||
```java
|
||||
// ExceptionTranslationFilter的伪代码
|
||||
try {
|
||||
filterChain.doFilter(request, response);
|
||||
} catch (AccessDeniedException | AuthenticationException ex) {
|
||||
if (!authenticated || ex instanceof AuthenticationException) {
|
||||
startAuthentication();
|
||||
} else {
|
||||
accessDenied();
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Spring Security身份认证的结构
|
||||
### SecurityContextHolder
|
||||
Spring Security身份认证的核心模型,SecurityContextHolder包含有SecurityContext。
|
||||
> Spring Security将被认证用户的详细信息(details)存储在SecurityContextHolder中。Spring Security不在乎该SecurityContextHolder是如何被填充的,只要该SecurityContextHolder有值,那么其将会被用作当前已经被认证的用户。
|
||||
|
||||
> ***将用户标识为已认证的最简单的方法是为该用户设置SecurityContextHolder。***
|
||||
|
||||
```java
|
||||
// 设置SecurityContextHolder
|
||||
SecurityContext context = SecurityContextHolder.createEmptyContext();
|
||||
Authentication authentication =
|
||||
new TestingAuthenticationToken("username", "password", "ROLE_USER");
|
||||
context.setAuthentication(authentication);
|
||||
|
||||
SecurityContextHolder.setContext(context);
|
||||
```
|
||||
默认情况下,SecurityContextHolder通过ThreadLocal来存储SecurityContext,故而SecurityContext对于位于同一线程之下的方法来说都可以访问。
|
||||
> 使用ThreadLocal来存储SecurityContext是相当安全的,如果想要在该已认证主体的请求被处理完成之后清除SecurityContext,Spring Security中的FilterChainProxy会保证该SecurityContext被清除。
|
||||
|
||||
### SecurityContext
|
||||
SecurityContext从SecurityContextHolder中获得,SecurityContext中含有Authentication对象。
|
||||
|
||||
### Authentication
|
||||
Authentication在Spring Security中具有两个目的:
|
||||
- 作为AuthenticationManager的输入,用于提供待认证用户的认证凭证。当用于该场景下时,isAuthenticated()方法返回值应该为false
|
||||
- 代表当前已经认证过的用户。当前的Authentication可以从SecurityContext中获取,而默认情况下SecurityContext是存储在ThreadLocal中的
|
||||
|
||||
Authentication含有如下部分:
|
||||
- 主体(principal):用于标识用户,当通过username/password进行认证时,其通常是UserDetails类的实例
|
||||
- 凭据(credentials):通常是password,在许多场景下凭据会在用户认证成功之后被清空,为了保证凭据不会被泄露
|
||||
- 权限(authorities):该GrantedAuthority集合是用户被授予的高层次许可。许可通常是用户角色或者作用域范围。
|
||||
|
||||
### GrantedAuthority
|
||||
GrantedAuthority是用户被授予的高层次许可,譬如用户角色或者作用域范围。
|
||||
GrantedAuthority可以通过Authentication.getAuthorities()方法来获得,该方法会返回一个GrantedAuthentication的集合。每个GrantedAuthentication都是一项被授予该用户的权限。
|
||||
|
||||
### AuthenticationManager
|
||||
AuthenticationManager的API定义了Security Filters如何来执行身份认证。对于身份认证返回的Authentication,会被调用AuthenticationManager的controller设置到SecurityContextHolder中。
|
||||
> AuthenticationManager的实现可以是任何类,但是最通用的实现仍然是ProviderManager
|
||||
|
||||
### ProviderManager
|
||||
ProviderManager是AuthenticationManager的最通用实现。ProviderManager将工作委托给一系列AuthenticationProvider。
|
||||
> 对于每个ProviderManager,都可以决定将该认证标识为成功、失败,或者将认证工作委托给下游AuthenticationProvider。
|
||||
> 如果所有的AuthenticationProvider都没有将该认证标识为成功或者失败,那么整个认证流程失败,并且抛出ProviderNotFoundException异常。
|
||||
> ProviderNotFoundException是一个特殊的AuthenticationException,该异常代表对传入Authentication的类型并没有配置该类型的ProviderManager
|
||||
|
||||
> 在实践中,每个AuthenticationProvider知道如何处理一种特定类型的Authentication
|
||||
|
||||
默认情况下,ProviderManager在认证请求成功后会尝试清除返回的Authentication对象中任何敏感的凭据信息,这将会保证password等敏感信息保存时间尽可能地短,减少泄露的风险。
|
||||
|
||||
### AuthenticationProvider
|
||||
复数个AuthenticationProvider可以被注入到ProviderManager中,每个AuthenticationProvider可以负责一种专门类型的认证,例如DaoAuthenticationProvider负责username/password认证,JwtAuthenticationProvider负责jwt token的认证。
|
||||
|
||||
### AuthenticationEntryPoint
|
||||
AuthenticationEntryPoint用来发送一个Http Response,用来向客户端请求认证凭据。
|
||||
某些情况下,客户端在请求资源时会主动在请求中包含凭据,如username/password等。在这种情况下,服务端并不需要再专门发送Http Response来向客户端请求认证凭据。
|
||||
> AuthenticationEntryPoint用来向客户端请求认证凭据,AuthenticationEntryPoint的实现可能会执行一个从定向操作,将请求重定向到一个登录页面用于获取凭据,并且返回一个WWW-Authentication Header。
|
||||
|
||||
### AbstractAuthenticationProcessingFilter
|
||||
该类作为base filter用来对用户的凭据进行认证。再凭据被认证之前,Spring Security通常会通过AuthenticationEntryPoint向客户端请求认证凭据。
|
||||
之后,AbstractAuthenticationProcessingFilter会对提交的任何认证请求进行认证。
|
||||
#### 认证流程
|
||||
1. 当用户提交认证凭据之后,AbstractAuthenticationProcesingFilter会根据HttpServletRequest对象创建一个Authentication对象用于认证,该Authentication的类型取决于AbstractAuthenticationProcessingFilter的子类类型。例如,UsernamePasswordAuthenticationFilter会通过提交request中的username和password创建UsernamePasswordAuthenticationToken。
|
||||
2. 然后将构建产生的Authentication对象传入到AuthenticationManager中,进行认证
|
||||
3. 如果认证失败,那么会失败,SecurityContextHolder会被清空,RememberMeService.logFail方法将会被调用,AuthenticationFailureHandler也会被调用
|
||||
4. 如果认证成功,那么SessionAuthenticationStrategy将会收到登录的通知
|
||||
5. 该Authentication对象再认证成功之后将会被设置到SecurityContextHolder中
|
||||
6. RemeberMeService.logSuccess将会被调用
|
||||
7. ApplicationEventPublisher发布InteractiveAuthenticationSuccessEvent.
|
||||
8. AuthenticationSuccessHandler被调用
|
||||
|
||||
## 用户名/密码认证
|
||||
### Form Login
|
||||
Spring Security为以表单形式提供的用户名和密码认证提供支持。
|
||||
#### 用户被重定向到登录页面的过程
|
||||
1. 用户发送了一个没有经过身份认证的请求到指定资源,并且待请求的资源对该用户来说是未授权的
|
||||
2. Spring Security中FilterSecurityInterceptor抛出AccessDeniedException,代表该未授权的请求被拒绝
|
||||
3. 因为该用户没有经过认证,故而ExceptionTransactionFilter发起了开始认证的过程,并且使用配置好的AuthenticationEntryPoint向登录页面发起了重定向。在大多数情况下AuthenticationEntryPoint都是LoginUrlAuthenticationEntryPoint
|
||||
4. 浏览器接下来会请求重定向到的登陆页面
|
||||
|
||||
当用户名和密码提交后,UsernamePasswordAuthenticationFilter会对username和password进行认证。UsernamePasswordAuthenticationFilter继承了AbstractAuthenticationProcessingFilter。
|
||||
|
||||
#### 认证用户名和密码过程
|
||||
1. 当用户提交了其用户名和密码之后,UsernamePasswordAuthenticationFilter会创建一个UsernamePasswordAuthenticationToken
|
||||
2. 创建的UsernamePasswordAuthenticationToken会传入AuthenticationManager中进行认证
|
||||
3. 如果认证失败,那么SecurityContextHolder会被清除,RememberMeService.logFailure和AuthenticationFailureHandler会被调用
|
||||
4. 如果认证成功,那么SessionAuthenticationStrategy将会收到登录的通知,RemeberMeService.logSuccess和AuthenticationSuccessHandler会被调用,ApplicationEventPublisher发布InteractiveAuthenticationSuccessEvent
|
||||
|
||||
Spring Security Form Login默认情况下是开启的,但是,一旦任何基于servlet的配置被提供,那么基于表单的login也必须要显式指定。
|
||||
```java
|
||||
// 显式指定form login的配置
|
||||
@Bean
|
||||
public SecurityFilterChain filterChain(HttpSecurity http) {
|
||||
http
|
||||
.formLogin(withDefaults());
|
||||
// ...
|
||||
}
|
||||
```
|
||||
如果想要自定义login form page,可以使用如下配置
|
||||
```java
|
||||
public SecurityFilterChain filterChain(HttpSecurity http) {
|
||||
http
|
||||
.formLogin(form -> form
|
||||
.loginPage("/login")
|
||||
.permitAll()
|
||||
);
|
||||
// ...
|
||||
}
|
||||
```
|
||||
### 基本Authentication
|
||||
#### 基本Authentication的认证流程
|
||||
1. 用户向私有资源发送未认证请求,其中对私有资源的访问并没有被授权
|
||||
2. SpringSecurity的FilterSecurityInterceptor表明该未认证的请求被拒绝访问,抛出AccessDeniedException
|
||||
3. 由于该请求没有经过身份认证,故而ExceptionTranslationFilter启动身份认证,被配置好的AuthenticationEntryPoint是一个BasicAuthenticationEntryPoint类的实例,该实例会发送WWW-Authentication的header。RequestCache通常是一个NullRequestCache,不会保存任何的http request请求,因为客户端能够重新发送其原来发送过的请求。
|
||||
4. 当客户端获取到WWW-Authentication的header,客户端会知道其接下来会通过username和password重新尝试,重新发送http请求。
|
||||
|
||||
默认情况下,basic authentication是被开启的。但是,如果有任何基于基于servlet的配置被提供,那么必须通过如下方式显式开启basic authentication。
|
||||
```java
|
||||
@Bean
|
||||
public SecurityFilterChain filterChain(HttpSecurity http) {
|
||||
http
|
||||
// ...
|
||||
.httpBasic(withDefaults());
|
||||
return http.build();
|
||||
}
|
||||
```
|
||||
|
||||
### Digest Authentication(摘要认证,***不安全***)
|
||||
在目前,不应该在现代应用程序中使用Digest Authentication,因为使用摘要认证时必须将password通过纯文本、加密或MD5的格式存储(MD5已经被证实不安全)。相对的,应该使用单向的密码散列(如bCrypt, PBKDF2, SCrypt)来存储认证凭证,但是这些都不被Digest Authentication所支持。
|
||||
> 摘要认证主要用来解决Basic Authentication中存在的问题,摘要认证确保了认证凭证在网络上不会以明文的方式传输。
|
||||
> 如果想要使用非https的方式并且最大限度的加强认证过程,那么可以考虑使用Digest Authentication。
|
||||
|
||||
#### 摘要认证中的随机数
|
||||
摘要认证中的核心是随机数,该随机数的值由服务端产生,Spring Security中随机数次啊用如下格式:
|
||||
```txt
|
||||
base64(expirationTime + ":" + md5Hex(expirationTime + ":" + key))
|
||||
expirationTime: The date and time when the nonce expires, expressed in milliseconds
|
||||
key: A private key to prevent modification of the nonce token
|
||||
```
|
||||
需要为存储不安全的密码文本配置使用NoOpPasswordEncoder。可以通过如下方式来配置Digest Authentication。
|
||||
```java
|
||||
@Autowired
|
||||
UserDetailsService userDetailsService;
|
||||
|
||||
DigestAuthenticationEntryPoint entryPoint() {
|
||||
DigestAuthenticationEntryPoint result = new DigestAuthenticationEntryPoint();
|
||||
result.setRealmName("My App Relam");
|
||||
result.setKey("3028472b-da34-4501-bfd8-a355c42bdf92");
|
||||
}
|
||||
|
||||
DigestAuthenticationFilter digestAuthenticationFilter() {
|
||||
DigestAuthenticationFilter result = new DigestAuthenticationFilter();
|
||||
result.setUserDetailsService(userDetailsService);
|
||||
result.setAuthenticationEntryPoint(entryPoint());
|
||||
}
|
||||
|
||||
@Bean
|
||||
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
|
||||
http
|
||||
// ...
|
||||
.exceptionHandling(e -> e.authenticationEntryPoint(authenticationEntryPoint()))
|
||||
.addFilterBefore(digestFilter());
|
||||
return http.build();
|
||||
}
|
||||
```
|
||||
## 密码存储方式
|
||||
### 内存中存储密码
|
||||
Spring Security中InMemoryUserDetailsManager实现了UserDetailsService,用于向基于存储在内存中的密码认证提供支持。
|
||||
InMemoryUserDetailsManager通过实现UserDetailsManager接口来提供对UserDetails的管理。基于UserDetails的认证主要用来接受基于用户名/密码的认证。
|
||||
InMemoryUserDetailsManager可以通过如下方式进行配置:
|
||||
```java
|
||||
@Bean
|
||||
public UserDetailsService users() {
|
||||
UserDetails user = User.builder()
|
||||
.username("user")
|
||||
.password("{bcrypt}$2a$10$GRLdNijSQMUvl/au9ofL.eDwmoohzzS7.rmNSJZ.0FxO/BTk76klW")
|
||||
.roles("USER")
|
||||
.build();
|
||||
UserDetails admin = User.builder()
|
||||
.username("admin")
|
||||
.password("{bcrypt}$2a$10$GRLdNijSQMUvl/au9ofL.eDwmoohzzS7.rmNSJZ.0FxO/BTk76klW")
|
||||
.roles("USER", "ADMIN")
|
||||
.build();
|
||||
return new InMemoryUserDetailsManager(user, admin);
|
||||
}
|
||||
```
|
||||
#### 内存中存储密码时使用defaultPasswordEncoder
|
||||
***通过defaultPasswordEncoder来指定密码编码器时,无法防止通过反编译字节码来获取密码的攻击。***
|
||||
```java
|
||||
@Bean
|
||||
public UserDetailsService users() {
|
||||
// The builder will ensure the passwords are encoded before saving in memory
|
||||
UserBuilder users = User.withDefaultPasswordEncoder();
|
||||
UserDetails user = users
|
||||
.username("user")
|
||||
.password("password")
|
||||
.roles("USER")
|
||||
.build();
|
||||
UserDetails admin = users
|
||||
.username("admin")
|
||||
.password("password")
|
||||
.roles("USER", "ADMIN")
|
||||
.build();
|
||||
return new InMemoryUserDetailsManager(user, admin);
|
||||
}
|
||||
```
|
||||
|
||||
### JDBC Authentication
|
||||
Spring Security的JdbcDaoImpl实现了UserDetailsService来基于username/password的认证提供从jdbc获取密码的支持。JdbcUserDetailsManager继承了JdbcDaoImpl来通过DetailsManager的接口提供对UserDetails的管理。
|
||||
Spring Security为基于jdbc的认证提供了默认的查询语句。
|
||||
#### User Schema
|
||||
JdbcDaoImpl需要数据表来导入密码、账户状态和用户的一系列权限。JdbcDaoImpl默认需要的schema如下:
|
||||
```sql
|
||||
# 创建用户表和权限表,并且将用户表和权限表之间用外键关联
|
||||
# 用户表需要提供username、password、用户状态
|
||||
# 权限表需要提供用户名和权限名称
|
||||
create table users(
|
||||
username varchar_ignorecase(50) not null primary key,
|
||||
password varchar_ignorecase(500) not null,
|
||||
enabled boolean not null
|
||||
);
|
||||
|
||||
create table authorities (
|
||||
username varchar_ignorecase(50) not null,
|
||||
authority varchar_ignorecase(50) not null,
|
||||
constraint fk_authorities_users foreign key(username) references users(username)
|
||||
);
|
||||
create unique index ix_auth_username on authorities (username,authority);
|
||||
```
|
||||
#### Group Schema
|
||||
如果你的程序中使用了Group,那么还额外需要一张group的表,默认如下:
|
||||
```sql
|
||||
# 如果要为group配置权限,需要引入三张表,group表,权限表和group_member表
|
||||
create table groups (
|
||||
id bigint auto_increment primary key,
|
||||
group_name varchar_ignorecase(50) not null
|
||||
);
|
||||
|
||||
create table group_authorities (
|
||||
group_id bigint not null,
|
||||
authority varchar(50) not null,
|
||||
constraint fk_group_authorities_group foreign key(group_id) references groups(id)
|
||||
);
|
||||
|
||||
create table group_members (
|
||||
id bigint auto_increment primary key,
|
||||
username varchar(50) not null,
|
||||
group_id bigint not null,
|
||||
constraint fk_group_members_group foreign key(group_id) references groups(id)
|
||||
);
|
||||
```
|
||||
|
||||
#### 配置Datasource
|
||||
```java
|
||||
// 生产环境时,应该通过对外部数据库的连接来建立数据源
|
||||
@Bean
|
||||
DataSource dataSource() {
|
||||
return new EmbeddedDatabaseBuilder()
|
||||
.setType(H2)
|
||||
.addScript(JdbcDaoImpl.DEFAULT_USER_SCHEMA_DDL_LOCATION)
|
||||
.build();
|
||||
}
|
||||
```
|
||||
#### 创建JdbcUserDetailsManager Bean对象
|
||||
```java
|
||||
@Bean
|
||||
UserDetailsManager users(DataSource dataSource) {
|
||||
UserDetails user = User.builder()
|
||||
.username("user")
|
||||
.password("{bcrypt}$2a$10$GRLdNijSQMUvl/au9ofL.eDwmoohzzS7.rmNSJZ.0FxO/BTk76klW")
|
||||
.roles("USER")
|
||||
.build();
|
||||
UserDetails admin = User.builder()
|
||||
.username("admin")
|
||||
.password("{bcrypt}$2a$10$GRLdNijSQMUvl/au9ofL.eDwmoohzzS7.rmNSJZ.0FxO/BTk76klW")
|
||||
.roles("USER", "ADMIN")
|
||||
.build();
|
||||
JdbcUserDetailsManager users = new JdbcUserDetailsManager(dataSource);
|
||||
users.createUser(user);
|
||||
users.createUser(admin);
|
||||
return users;
|
||||
}
|
||||
```
|
||||
### UserDetails
|
||||
UserDetails是通过UserDetailsService返回的。DaoAuthenticationProvider对UserrDetails进行验证并且返回Authentication.
|
||||
|
||||
### UserDetailsService
|
||||
UserDetailsService被DaoAuthenticationProvider调用,用来获取username、password和其他随着password/username一起认证的信息。对于UserDetailsService,Spring Security提供了in-memory和jdbc两种实现形式。
|
||||
可以通过自定义UserDetailsService类bean对象的方式来自定义认证过程。
|
||||
```java
|
||||
// 自定义UserDetailsService的bean对象
|
||||
@Bean
|
||||
CustomUserDetailsService customUserDetailsService() {
|
||||
return new CustomUserDetailsService();
|
||||
}
|
||||
```
|
||||
|
||||
### PasswordEncoder
|
||||
Spring Security支持PasswordEncoder来安全的存储密码。可以通过自定义PasswordEncoder类的bean对象的形式来自定义Spring Security安全存储密码的过程。
|
||||
|
||||
### DaoAuthenticationProvider
|
||||
DaoAuthenticationProvider是AuthenticationProvider的一个实现类,通过调用UserDetailsService和PasswordEncoder来认证用户名和密码。
|
||||
Spring Security中DaoAuthenticationProvider的工作流程:
|
||||
1. authentication filter会读取username和password并且将其封装到UsernamePasswordAuthenticationToken中传递给AuthenticationManager,ProviderManager实现了AuthenticationManager
|
||||
2. ProviderManager被配置为使用DaoAuthenticationProvider
|
||||
3. DaoAuthenticationProvider通过UserDetailsService来查找UserDetails
|
||||
4. DaoAuthenticationProvider通过PasswordEncoder来验证UserDetails中的密码
|
||||
5. 当验证成功时,会返回UsernamePasswordAuthenticationToken类型的Authentication,并且返回的Authentication拥有一个主体为UserDetailsService返回的UserDetails
|
||||
6. 返回的UsernamePasswordAuthenticationToken会在SecurityContextHolder中保存
|
||||
|
||||
# Spring Security
|
||||
## Spring Security简介
|
||||
Spring Security作为一个安全框架,向使用者提供了用户认证、授权、常规攻击保护等功能。
|
||||
## Spring Security自动配置
|
||||
默认情况下,在引入Spring Security的启动器依赖之后,Spring Boot自动配置会做如下工作:
|
||||
- 启用Spring Security的默认配置,创建一个的bean对象,bean对象名称为“springSecurityFilterChain”,bean对象类型为SecurityFilterChain,实现了Filter。该bean对象为负责应用中所有与安全相关的操作(例如验证提交的username和password,保护应用的url等)
|
||||
- 创建一个UserDetailsService的bean对象,并且产生一个为”user“的username和一个随机产生的password,随机产生的password会输出在console日志中
|
||||
- 将名为springSecurityFilterChain的bean对象注册到servlet容器中,用来对每个servlet请求进行处理
|
||||
> Spring Security会通过Spring Security会通过BCtypt(Hash解密算法)来对密码的存储进行保护
|
||||
|
||||
## Spring Security结构
|
||||
### DelegatingFilterProxy
|
||||
Spring提供了Filter的一个实现类DelegatingFilterProxy,其将Servlet容器的生命周期和Spring的ApplicationContext连接在一起。
|
||||
***DelegatingFilterProxy会通过标准的servlet容器机制被注册,但是将所有工作都委托给Spring容器中实现了Filter的bean对象***。
|
||||
> DelegatingFilterProxy会在ApplicationContext中查找实现了Filter的bean对象,并且调用该bean对象的doFilter方法
|
||||
> ```java
|
||||
> public void doFilter(ServletRequest request,
|
||||
> ServletResponse response, FilterChain chain) {
|
||||
> // Lazily get Filter that was registered as a Spring Bean
|
||||
> // For the example in DelegatingFilterProxy delegate is an instance of Bean Filter0
|
||||
> Filter delegate = getFilterBean(someBeanName);
|
||||
> // delegate work to the Spring Bean
|
||||
> delegate.doFilter(request, response);
|
||||
> }
|
||||
> ```
|
||||
|
||||
### FilterChainProxy
|
||||
Spring Security支持FilterChainProxy,FilterChainProxy是一个由Spring Security提供的特殊Filter,FilterChainProxy通过SecurityFilterChain允许向多个Filters委托工作。
|
||||
***FilterChainProxy是一个bean对象,通过被包含在DelegatingFilterProxy中。***
|
||||
|
||||
### SecurityFilterChain
|
||||
SecurityFilterChain通常被FilterChainProxy使用,用来决定在该次请求中调用那些Spring Security Filters。
|
||||
|
||||
### SecurityFilters
|
||||
Security Filters通过SecurityFilterChain API被插入到FilterChainProxy中。
|
||||
### 处理Security异常
|
||||
ExceptionTranslationFilter将认证异常和权限异常翻译为http response。
|
||||
> ExceptionTranslationFilter被插入到FilterChainProxy中,作为SecurityFilterChain中的一个。
|
||||
> 如果应用程序没有抛出AccessDeniedException或AuthenticationException,那么ExceptionTranslationFilter并不会做任何事情。
|
||||
|
||||
```java
|
||||
// ExceptionTranslationFilter的伪代码
|
||||
try {
|
||||
filterChain.doFilter(request, response);
|
||||
} catch (AccessDeniedException | AuthenticationException ex) {
|
||||
if (!authenticated || ex instanceof AuthenticationException) {
|
||||
startAuthentication();
|
||||
} else {
|
||||
accessDenied();
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Spring Security身份认证的结构
|
||||
### SecurityContextHolder
|
||||
Spring Security身份认证的核心模型,SecurityContextHolder包含有SecurityContext。
|
||||
> Spring Security将被认证用户的详细信息(details)存储在SecurityContextHolder中。Spring Security不在乎该SecurityContextHolder是如何被填充的,只要该SecurityContextHolder有值,那么其将会被用作当前已经被认证的用户。
|
||||
|
||||
> ***将用户标识为已认证的最简单的方法是为该用户设置SecurityContextHolder。***
|
||||
|
||||
```java
|
||||
// 设置SecurityContextHolder
|
||||
SecurityContext context = SecurityContextHolder.createEmptyContext();
|
||||
Authentication authentication =
|
||||
new TestingAuthenticationToken("username", "password", "ROLE_USER");
|
||||
context.setAuthentication(authentication);
|
||||
|
||||
SecurityContextHolder.setContext(context);
|
||||
```
|
||||
默认情况下,SecurityContextHolder通过ThreadLocal来存储SecurityContext,故而SecurityContext对于位于同一线程之下的方法来说都可以访问。
|
||||
> 使用ThreadLocal来存储SecurityContext是相当安全的,如果想要在该已认证主体的请求被处理完成之后清除SecurityContext,Spring Security中的FilterChainProxy会保证该SecurityContext被清除。
|
||||
|
||||
### SecurityContext
|
||||
SecurityContext从SecurityContextHolder中获得,SecurityContext中含有Authentication对象。
|
||||
|
||||
### Authentication
|
||||
Authentication在Spring Security中具有两个目的:
|
||||
- 作为AuthenticationManager的输入,用于提供待认证用户的认证凭证。当用于该场景下时,isAuthenticated()方法返回值应该为false
|
||||
- 代表当前已经认证过的用户。当前的Authentication可以从SecurityContext中获取,而默认情况下SecurityContext是存储在ThreadLocal中的
|
||||
|
||||
Authentication含有如下部分:
|
||||
- 主体(principal):用于标识用户,当通过username/password进行认证时,其通常是UserDetails类的实例
|
||||
- 凭据(credentials):通常是password,在许多场景下凭据会在用户认证成功之后被清空,为了保证凭据不会被泄露
|
||||
- 权限(authorities):该GrantedAuthority集合是用户被授予的高层次许可。许可通常是用户角色或者作用域范围。
|
||||
|
||||
### GrantedAuthority
|
||||
GrantedAuthority是用户被授予的高层次许可,譬如用户角色或者作用域范围。
|
||||
GrantedAuthority可以通过Authentication.getAuthorities()方法来获得,该方法会返回一个GrantedAuthentication的集合。每个GrantedAuthentication都是一项被授予该用户的权限。
|
||||
|
||||
### AuthenticationManager
|
||||
AuthenticationManager的API定义了Security Filters如何来执行身份认证。对于身份认证返回的Authentication,会被调用AuthenticationManager的controller设置到SecurityContextHolder中。
|
||||
> AuthenticationManager的实现可以是任何类,但是最通用的实现仍然是ProviderManager
|
||||
|
||||
### ProviderManager
|
||||
ProviderManager是AuthenticationManager的最通用实现。ProviderManager将工作委托给一系列AuthenticationProvider。
|
||||
> 对于每个ProviderManager,都可以决定将该认证标识为成功、失败,或者将认证工作委托给下游AuthenticationProvider。
|
||||
> 如果所有的AuthenticationProvider都没有将该认证标识为成功或者失败,那么整个认证流程失败,并且抛出ProviderNotFoundException异常。
|
||||
> ProviderNotFoundException是一个特殊的AuthenticationException,该异常代表对传入Authentication的类型并没有配置该类型的ProviderManager
|
||||
|
||||
> 在实践中,每个AuthenticationProvider知道如何处理一种特定类型的Authentication
|
||||
|
||||
默认情况下,ProviderManager在认证请求成功后会尝试清除返回的Authentication对象中任何敏感的凭据信息,这将会保证password等敏感信息保存时间尽可能地短,减少泄露的风险。
|
||||
|
||||
### AuthenticationProvider
|
||||
复数个AuthenticationProvider可以被注入到ProviderManager中,每个AuthenticationProvider可以负责一种专门类型的认证,例如DaoAuthenticationProvider负责username/password认证,JwtAuthenticationProvider负责jwt token的认证。
|
||||
|
||||
### AuthenticationEntryPoint
|
||||
AuthenticationEntryPoint用来发送一个Http Response,用来向客户端请求认证凭据。
|
||||
某些情况下,客户端在请求资源时会主动在请求中包含凭据,如username/password等。在这种情况下,服务端并不需要再专门发送Http Response来向客户端请求认证凭据。
|
||||
> AuthenticationEntryPoint用来向客户端请求认证凭据,AuthenticationEntryPoint的实现可能会执行一个从定向操作,将请求重定向到一个登录页面用于获取凭据,并且返回一个WWW-Authentication Header。
|
||||
|
||||
### AbstractAuthenticationProcessingFilter
|
||||
该类作为base filter用来对用户的凭据进行认证。再凭据被认证之前,Spring Security通常会通过AuthenticationEntryPoint向客户端请求认证凭据。
|
||||
之后,AbstractAuthenticationProcessingFilter会对提交的任何认证请求进行认证。
|
||||
#### 认证流程
|
||||
1. 当用户提交认证凭据之后,AbstractAuthenticationProcesingFilter会根据HttpServletRequest对象创建一个Authentication对象用于认证,该Authentication的类型取决于AbstractAuthenticationProcessingFilter的子类类型。例如,UsernamePasswordAuthenticationFilter会通过提交request中的username和password创建UsernamePasswordAuthenticationToken。
|
||||
2. 然后将构建产生的Authentication对象传入到AuthenticationManager中,进行认证
|
||||
3. 如果认证失败,那么会失败,SecurityContextHolder会被清空,RememberMeService.logFail方法将会被调用,AuthenticationFailureHandler也会被调用
|
||||
4. 如果认证成功,那么SessionAuthenticationStrategy将会收到登录的通知
|
||||
5. 该Authentication对象再认证成功之后将会被设置到SecurityContextHolder中
|
||||
6. RemeberMeService.logSuccess将会被调用
|
||||
7. ApplicationEventPublisher发布InteractiveAuthenticationSuccessEvent.
|
||||
8. AuthenticationSuccessHandler被调用
|
||||
|
||||
## 用户名/密码认证
|
||||
### Form Login
|
||||
Spring Security为以表单形式提供的用户名和密码认证提供支持。
|
||||
#### 用户被重定向到登录页面的过程
|
||||
1. 用户发送了一个没有经过身份认证的请求到指定资源,并且待请求的资源对该用户来说是未授权的
|
||||
2. Spring Security中FilterSecurityInterceptor抛出AccessDeniedException,代表该未授权的请求被拒绝
|
||||
3. 因为该用户没有经过认证,故而ExceptionTransactionFilter发起了开始认证的过程,并且使用配置好的AuthenticationEntryPoint向登录页面发起了重定向。在大多数情况下AuthenticationEntryPoint都是LoginUrlAuthenticationEntryPoint
|
||||
4. 浏览器接下来会请求重定向到的登陆页面
|
||||
|
||||
当用户名和密码提交后,UsernamePasswordAuthenticationFilter会对username和password进行认证。UsernamePasswordAuthenticationFilter继承了AbstractAuthenticationProcessingFilter。
|
||||
|
||||
#### 认证用户名和密码过程
|
||||
1. 当用户提交了其用户名和密码之后,UsernamePasswordAuthenticationFilter会创建一个UsernamePasswordAuthenticationToken
|
||||
2. 创建的UsernamePasswordAuthenticationToken会传入AuthenticationManager中进行认证
|
||||
3. 如果认证失败,那么SecurityContextHolder会被清除,RememberMeService.logFailure和AuthenticationFailureHandler会被调用
|
||||
4. 如果认证成功,那么SessionAuthenticationStrategy将会收到登录的通知,RemeberMeService.logSuccess和AuthenticationSuccessHandler会被调用,ApplicationEventPublisher发布InteractiveAuthenticationSuccessEvent
|
||||
|
||||
Spring Security Form Login默认情况下是开启的,但是,一旦任何基于servlet的配置被提供,那么基于表单的login也必须要显式指定。
|
||||
```java
|
||||
// 显式指定form login的配置
|
||||
@Bean
|
||||
public SecurityFilterChain filterChain(HttpSecurity http) {
|
||||
http
|
||||
.formLogin(withDefaults());
|
||||
// ...
|
||||
}
|
||||
```
|
||||
如果想要自定义login form page,可以使用如下配置
|
||||
```java
|
||||
public SecurityFilterChain filterChain(HttpSecurity http) {
|
||||
http
|
||||
.formLogin(form -> form
|
||||
.loginPage("/login")
|
||||
.permitAll()
|
||||
);
|
||||
// ...
|
||||
}
|
||||
```
|
||||
### 基本Authentication
|
||||
#### 基本Authentication的认证流程
|
||||
1. 用户向私有资源发送未认证请求,其中对私有资源的访问并没有被授权
|
||||
2. SpringSecurity的FilterSecurityInterceptor表明该未认证的请求被拒绝访问,抛出AccessDeniedException
|
||||
3. 由于该请求没有经过身份认证,故而ExceptionTranslationFilter启动身份认证,被配置好的AuthenticationEntryPoint是一个BasicAuthenticationEntryPoint类的实例,该实例会发送WWW-Authentication的header。RequestCache通常是一个NullRequestCache,不会保存任何的http request请求,因为客户端能够重新发送其原来发送过的请求。
|
||||
4. 当客户端获取到WWW-Authentication的header,客户端会知道其接下来会通过username和password重新尝试,重新发送http请求。
|
||||
|
||||
默认情况下,basic authentication是被开启的。但是,如果有任何基于基于servlet的配置被提供,那么必须通过如下方式显式开启basic authentication。
|
||||
```java
|
||||
@Bean
|
||||
public SecurityFilterChain filterChain(HttpSecurity http) {
|
||||
http
|
||||
// ...
|
||||
.httpBasic(withDefaults());
|
||||
return http.build();
|
||||
}
|
||||
```
|
||||
|
||||
### Digest Authentication(摘要认证,***不安全***)
|
||||
在目前,不应该在现代应用程序中使用Digest Authentication,因为使用摘要认证时必须将password通过纯文本、加密或MD5的格式存储(MD5已经被证实不安全)。相对的,应该使用单向的密码散列(如bCrypt, PBKDF2, SCrypt)来存储认证凭证,但是这些都不被Digest Authentication所支持。
|
||||
> 摘要认证主要用来解决Basic Authentication中存在的问题,摘要认证确保了认证凭证在网络上不会以明文的方式传输。
|
||||
> 如果想要使用非https的方式并且最大限度的加强认证过程,那么可以考虑使用Digest Authentication。
|
||||
|
||||
#### 摘要认证中的随机数
|
||||
摘要认证中的核心是随机数,该随机数的值由服务端产生,Spring Security中随机数次啊用如下格式:
|
||||
```txt
|
||||
base64(expirationTime + ":" + md5Hex(expirationTime + ":" + key))
|
||||
expirationTime: The date and time when the nonce expires, expressed in milliseconds
|
||||
key: A private key to prevent modification of the nonce token
|
||||
```
|
||||
需要为存储不安全的密码文本配置使用NoOpPasswordEncoder。可以通过如下方式来配置Digest Authentication。
|
||||
```java
|
||||
@Autowired
|
||||
UserDetailsService userDetailsService;
|
||||
|
||||
DigestAuthenticationEntryPoint entryPoint() {
|
||||
DigestAuthenticationEntryPoint result = new DigestAuthenticationEntryPoint();
|
||||
result.setRealmName("My App Relam");
|
||||
result.setKey("3028472b-da34-4501-bfd8-a355c42bdf92");
|
||||
}
|
||||
|
||||
DigestAuthenticationFilter digestAuthenticationFilter() {
|
||||
DigestAuthenticationFilter result = new DigestAuthenticationFilter();
|
||||
result.setUserDetailsService(userDetailsService);
|
||||
result.setAuthenticationEntryPoint(entryPoint());
|
||||
}
|
||||
|
||||
@Bean
|
||||
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
|
||||
http
|
||||
// ...
|
||||
.exceptionHandling(e -> e.authenticationEntryPoint(authenticationEntryPoint()))
|
||||
.addFilterBefore(digestFilter());
|
||||
return http.build();
|
||||
}
|
||||
```
|
||||
## 密码存储方式
|
||||
### 内存中存储密码
|
||||
Spring Security中InMemoryUserDetailsManager实现了UserDetailsService,用于向基于存储在内存中的密码认证提供支持。
|
||||
InMemoryUserDetailsManager通过实现UserDetailsManager接口来提供对UserDetails的管理。基于UserDetails的认证主要用来接受基于用户名/密码的认证。
|
||||
InMemoryUserDetailsManager可以通过如下方式进行配置:
|
||||
```java
|
||||
@Bean
|
||||
public UserDetailsService users() {
|
||||
UserDetails user = User.builder()
|
||||
.username("user")
|
||||
.password("{bcrypt}$2a$10$GRLdNijSQMUvl/au9ofL.eDwmoohzzS7.rmNSJZ.0FxO/BTk76klW")
|
||||
.roles("USER")
|
||||
.build();
|
||||
UserDetails admin = User.builder()
|
||||
.username("admin")
|
||||
.password("{bcrypt}$2a$10$GRLdNijSQMUvl/au9ofL.eDwmoohzzS7.rmNSJZ.0FxO/BTk76klW")
|
||||
.roles("USER", "ADMIN")
|
||||
.build();
|
||||
return new InMemoryUserDetailsManager(user, admin);
|
||||
}
|
||||
```
|
||||
#### 内存中存储密码时使用defaultPasswordEncoder
|
||||
***通过defaultPasswordEncoder来指定密码编码器时,无法防止通过反编译字节码来获取密码的攻击。***
|
||||
```java
|
||||
@Bean
|
||||
public UserDetailsService users() {
|
||||
// The builder will ensure the passwords are encoded before saving in memory
|
||||
UserBuilder users = User.withDefaultPasswordEncoder();
|
||||
UserDetails user = users
|
||||
.username("user")
|
||||
.password("password")
|
||||
.roles("USER")
|
||||
.build();
|
||||
UserDetails admin = users
|
||||
.username("admin")
|
||||
.password("password")
|
||||
.roles("USER", "ADMIN")
|
||||
.build();
|
||||
return new InMemoryUserDetailsManager(user, admin);
|
||||
}
|
||||
```
|
||||
|
||||
### JDBC Authentication
|
||||
Spring Security的JdbcDaoImpl实现了UserDetailsService来基于username/password的认证提供从jdbc获取密码的支持。JdbcUserDetailsManager继承了JdbcDaoImpl来通过DetailsManager的接口提供对UserDetails的管理。
|
||||
Spring Security为基于jdbc的认证提供了默认的查询语句。
|
||||
#### User Schema
|
||||
JdbcDaoImpl需要数据表来导入密码、账户状态和用户的一系列权限。JdbcDaoImpl默认需要的schema如下:
|
||||
```sql
|
||||
# 创建用户表和权限表,并且将用户表和权限表之间用外键关联
|
||||
# 用户表需要提供username、password、用户状态
|
||||
# 权限表需要提供用户名和权限名称
|
||||
create table users(
|
||||
username varchar_ignorecase(50) not null primary key,
|
||||
password varchar_ignorecase(500) not null,
|
||||
enabled boolean not null
|
||||
);
|
||||
|
||||
create table authorities (
|
||||
username varchar_ignorecase(50) not null,
|
||||
authority varchar_ignorecase(50) not null,
|
||||
constraint fk_authorities_users foreign key(username) references users(username)
|
||||
);
|
||||
create unique index ix_auth_username on authorities (username,authority);
|
||||
```
|
||||
#### Group Schema
|
||||
如果你的程序中使用了Group,那么还额外需要一张group的表,默认如下:
|
||||
```sql
|
||||
# 如果要为group配置权限,需要引入三张表,group表,权限表和group_member表
|
||||
create table groups (
|
||||
id bigint auto_increment primary key,
|
||||
group_name varchar_ignorecase(50) not null
|
||||
);
|
||||
|
||||
create table group_authorities (
|
||||
group_id bigint not null,
|
||||
authority varchar(50) not null,
|
||||
constraint fk_group_authorities_group foreign key(group_id) references groups(id)
|
||||
);
|
||||
|
||||
create table group_members (
|
||||
id bigint auto_increment primary key,
|
||||
username varchar(50) not null,
|
||||
group_id bigint not null,
|
||||
constraint fk_group_members_group foreign key(group_id) references groups(id)
|
||||
);
|
||||
```
|
||||
|
||||
#### 配置Datasource
|
||||
```java
|
||||
// 生产环境时,应该通过对外部数据库的连接来建立数据源
|
||||
@Bean
|
||||
DataSource dataSource() {
|
||||
return new EmbeddedDatabaseBuilder()
|
||||
.setType(H2)
|
||||
.addScript(JdbcDaoImpl.DEFAULT_USER_SCHEMA_DDL_LOCATION)
|
||||
.build();
|
||||
}
|
||||
```
|
||||
#### 创建JdbcUserDetailsManager Bean对象
|
||||
```java
|
||||
@Bean
|
||||
UserDetailsManager users(DataSource dataSource) {
|
||||
UserDetails user = User.builder()
|
||||
.username("user")
|
||||
.password("{bcrypt}$2a$10$GRLdNijSQMUvl/au9ofL.eDwmoohzzS7.rmNSJZ.0FxO/BTk76klW")
|
||||
.roles("USER")
|
||||
.build();
|
||||
UserDetails admin = User.builder()
|
||||
.username("admin")
|
||||
.password("{bcrypt}$2a$10$GRLdNijSQMUvl/au9ofL.eDwmoohzzS7.rmNSJZ.0FxO/BTk76klW")
|
||||
.roles("USER", "ADMIN")
|
||||
.build();
|
||||
JdbcUserDetailsManager users = new JdbcUserDetailsManager(dataSource);
|
||||
users.createUser(user);
|
||||
users.createUser(admin);
|
||||
return users;
|
||||
}
|
||||
```
|
||||
### UserDetails
|
||||
UserDetails是通过UserDetailsService返回的。DaoAuthenticationProvider对UserrDetails进行验证并且返回Authentication.
|
||||
|
||||
### UserDetailsService
|
||||
UserDetailsService被DaoAuthenticationProvider调用,用来获取username、password和其他随着password/username一起认证的信息。对于UserDetailsService,Spring Security提供了in-memory和jdbc两种实现形式。
|
||||
可以通过自定义UserDetailsService类bean对象的方式来自定义认证过程。
|
||||
```java
|
||||
// 自定义UserDetailsService的bean对象
|
||||
@Bean
|
||||
CustomUserDetailsService customUserDetailsService() {
|
||||
return new CustomUserDetailsService();
|
||||
}
|
||||
```
|
||||
|
||||
### PasswordEncoder
|
||||
Spring Security支持PasswordEncoder来安全的存储密码。可以通过自定义PasswordEncoder类的bean对象的形式来自定义Spring Security安全存储密码的过程。
|
||||
|
||||
### DaoAuthenticationProvider
|
||||
DaoAuthenticationProvider是AuthenticationProvider的一个实现类,通过调用UserDetailsService和PasswordEncoder来认证用户名和密码。
|
||||
Spring Security中DaoAuthenticationProvider的工作流程:
|
||||
1. authentication filter会读取username和password并且将其封装到UsernamePasswordAuthenticationToken中传递给AuthenticationManager,ProviderManager实现了AuthenticationManager
|
||||
2. ProviderManager被配置为使用DaoAuthenticationProvider
|
||||
3. DaoAuthenticationProvider通过UserDetailsService来查找UserDetails
|
||||
4. DaoAuthenticationProvider通过PasswordEncoder来验证UserDetails中的密码
|
||||
5. 当验证成功时,会返回UsernamePasswordAuthenticationToken类型的Authentication,并且返回的Authentication拥有一个主体为UserDetailsService返回的UserDetails
|
||||
6. 返回的UsernamePasswordAuthenticationToken会在SecurityContextHolder中保存
|
||||
|
||||
|
||||
@@ -1,15 +1,15 @@
|
||||
# POJO
|
||||
## POJO定义
|
||||
POJO(Plain Old Java Object)是一种直接的类型,POJO并不包含对任何框架的引用。
|
||||
> 对于POJO类型,该类属性和方法的定义并没有特定的约束和限制
|
||||
## Java Bean命名约束
|
||||
由于对POJO本身并没有对POJO类属性和方法的定义强制指定命名约束,因而许多框默认支持Java Bean命名约束。
|
||||
> ### Java Bean命名约束
|
||||
> 在Java Bean命名约束中,为POJO类属性和方法的命名指定了如下规则:
|
||||
> 1. 属性的访问权限都被设置为private,属性通过getter和setter向外暴露
|
||||
> 2. 对于方法的命名,getter和setter遵循getXXX/setXXX的命名规范(对于boolean属性的getter,可以使用isXXX形式
|
||||
> 3. Java Bean命名规范要求Java Bean对象需要提供无参构造方法
|
||||
> 4. 实现Serializable接口,能够将对象以二进制的格式进行存储
|
||||
## 其他命名规范
|
||||
由于Java Bean命名规范中有些规则强制对Java Bean的命名进行限制可能会带来弊端,故而如今许多框架在接受Java Bean命名规范之余,仍然支持其他的POJO命名规范
|
||||
> 如在Spring中,通过@Component注解注册Bean对象时,被@Component注解的类并不一定要实现Serializable接口,也不一定要拥有无参构造方法。
|
||||
# POJO
|
||||
## POJO定义
|
||||
POJO(Plain Old Java Object)是一种直接的类型,POJO并不包含对任何框架的引用。
|
||||
> 对于POJO类型,该类属性和方法的定义并没有特定的约束和限制
|
||||
## Java Bean命名约束
|
||||
由于对POJO本身并没有对POJO类属性和方法的定义强制指定命名约束,因而许多框默认支持Java Bean命名约束。
|
||||
> ### Java Bean命名约束
|
||||
> 在Java Bean命名约束中,为POJO类属性和方法的命名指定了如下规则:
|
||||
> 1. 属性的访问权限都被设置为private,属性通过getter和setter向外暴露
|
||||
> 2. 对于方法的命名,getter和setter遵循getXXX/setXXX的命名规范(对于boolean属性的getter,可以使用isXXX形式
|
||||
> 3. Java Bean命名规范要求Java Bean对象需要提供无参构造方法
|
||||
> 4. 实现Serializable接口,能够将对象以二进制的格式进行存储
|
||||
## 其他命名规范
|
||||
由于Java Bean命名规范中有些规则强制对Java Bean的命名进行限制可能会带来弊端,故而如今许多框架在接受Java Bean命名规范之余,仍然支持其他的POJO命名规范
|
||||
> 如在Spring中,通过@Component注解注册Bean对象时,被@Component注解的类并不一定要实现Serializable接口,也不一定要拥有无参构造方法。
|
||||
|
||||
@@ -1,109 +1,109 @@
|
||||
# SpEL(Spring Expression Language)
|
||||
- ## SpEL的用法
|
||||
- SpEL如何将表达式从字符串转化为计算后的值
|
||||
- 在转化过程中,在parseExpression方法执行时可能会抛出ParseException异常,在执行getValue方法时可能会抛出EvaluationException
|
||||
```java
|
||||
ExpressionParser parser = new SpelExpressionParser();
|
||||
Expression exp = parser.parseExpression("'Hello World'");
|
||||
String message = (String) exp.getValue();
|
||||
```
|
||||
- 在SpEL中获取String的字节数组
|
||||
```java
|
||||
ExpressionParser parser=new SpelExpressionParser();
|
||||
Expression exp=parser.parseExpression("'Hello World'.bytes");
|
||||
byte[] bytes=(byte[])exp.getValue();
|
||||
```
|
||||
- 在调用Expression类型的getValue方法时,可以不用进行强制类型转换,而是在getValue方法中传入一个Class参数,返回值将会被自动转换成Class对应的目标类型,当转换失败时会抛出EvaluationException
|
||||
```java
|
||||
ExpressionParser parser=new SpelExpressionParser();
|
||||
Expression exp=parser.parseExpression("'Hello World'.bytes.length");
|
||||
Integer bytes=exp.getValue(Integer.class);
|
||||
```
|
||||
- SpEL可以针对特定的对象,给出一个表达式并且在getValue方法中传入一个对象,那么表达式中的变量将会针对该对象中的特定属性
|
||||
```java
|
||||
// 如下步骤会比较waifu对象的name属性是否为"touma"字符串
|
||||
ExpressionParser parser=new SpelExpressionParser();
|
||||
Expression exp=parser.parseExpression("name=='touma'");
|
||||
Boolean equals=exp.getValue(waifu,Boolean.class);
|
||||
```
|
||||
- 可以为parser设置一个parserconfiguration,用于处理当列表或集合元素的index操作超过集合长度时的默认行为
|
||||
```java
|
||||
class Demo {
|
||||
public List<String> list;
|
||||
}
|
||||
|
||||
// Turn on:
|
||||
// - auto null reference initialization
|
||||
// - auto collection growing
|
||||
SpelParserConfiguration config = new SpelParserConfiguration(true, true);
|
||||
|
||||
ExpressionParser parser = new SpelExpressionParser(config);
|
||||
|
||||
Expression expression = parser.parseExpression("list[3]");
|
||||
|
||||
Demo demo = new Demo();
|
||||
|
||||
Object o = expression.getValue(demo);
|
||||
|
||||
// demo.list will now be a real collection of 4 entries
|
||||
// Each entry is a new empty String
|
||||
```
|
||||
- ## SpEL在bean对象定义时的使用
|
||||
- 在使用@Value注解时,可以结合SpEL表达式进行使用,@Value注解可以运用在域变量、方法、方法和构造器的参数上。@Value会指定默认值
|
||||
- ## SpEL对List、Map的支持
|
||||
- 可以通过{}来直接表示list
|
||||
```java
|
||||
List numbers = (List) parser.parseExpression("{1,2,3,4}").getValue(context);
|
||||
|
||||
List listOfLists = (List) parser.parseExpression("{{'a','b'},{'x','y'}}").getValue(context);
|
||||
```
|
||||
- 可以通过{key:value}形式来直接表示map,空map用{:}来进行表示
|
||||
```java
|
||||
// evaluates to a Java map containing the two entries
|
||||
Map inventorInfo = (Map) parser.parseExpression("{name:'Nikola',dob:'10-July-1856'}").getValue(context);
|
||||
|
||||
Map mapOfMaps = (Map) parser.parseExpression("{name:{first:'Nikola',last:'Tesla'},dob:{day:10,month:'July',year:1856}}").getValue(context);
|
||||
```
|
||||
- 可以通过new int[]{}的形式为SpEL指定数组
|
||||
```java
|
||||
int[] numbers1 = (int[]) parser.parseExpression("new int[4]").getValue(context);
|
||||
|
||||
// Array with initializer
|
||||
int[] numbers2 = (int[]) parser.parseExpression("new int[]{1,2,3}").getValue(context);
|
||||
|
||||
// Multi dimensional array
|
||||
int[][] numbers3 = (int[][]) parser.parseExpression("new int[4][5]").getValue(context);
|
||||
```
|
||||
- ## SpEL支持的特殊操作符
|
||||
- instanceof
|
||||
```java
|
||||
boolean falseValue = parser.parseExpression(
|
||||
"'xyz' instanceof T(Integer)").getValue(Boolean.class);
|
||||
```
|
||||
- 正则表达式
|
||||
```java
|
||||
boolean trueValue = parser.parseExpression(
|
||||
"'5.00' matches '^-?\\d+(\\.\\d{2})?$'").getValue(Boolean.class);
|
||||
```
|
||||
- 类型操作符,获取类型的Class对象、调用静态方法
|
||||
```java
|
||||
Class dateClass = parser.parseExpression("T(java.util.Date)").getValue(Class.class);
|
||||
|
||||
Class stringClass = parser.parseExpression("T(String)").getValue(Class.class);
|
||||
|
||||
boolean trueValue = parser.parseExpression(
|
||||
"T(java.math.RoundingMode).CEILING < T(java.math.RoundingMode).FLOOR")
|
||||
.getValue(Boolean.class);
|
||||
```
|
||||
- new操作符:
|
||||
- 可以在SpEL表达式中通过new操作符来调用构造器,但是除了位于java.lang包中的类,对其他的类调用构造器时都必须指定类的全类名
|
||||
```java
|
||||
Inventor einstein = p.parseExpression(
|
||||
"new org.spring.samples.spel.inventor.Inventor('Albert Einstein', 'German')")
|
||||
.getValue(Inventor.class);
|
||||
|
||||
// create new Inventor instance within the add() method of List
|
||||
p.parseExpression(
|
||||
"Members.add(new org.spring.samples.spel.inventor.Inventor(
|
||||
'Albert Einstein', 'German'))").getValue(societyContext);
|
||||
# SpEL(Spring Expression Language)
|
||||
- ## SpEL的用法
|
||||
- SpEL如何将表达式从字符串转化为计算后的值
|
||||
- 在转化过程中,在parseExpression方法执行时可能会抛出ParseException异常,在执行getValue方法时可能会抛出EvaluationException
|
||||
```java
|
||||
ExpressionParser parser = new SpelExpressionParser();
|
||||
Expression exp = parser.parseExpression("'Hello World'");
|
||||
String message = (String) exp.getValue();
|
||||
```
|
||||
- 在SpEL中获取String的字节数组
|
||||
```java
|
||||
ExpressionParser parser=new SpelExpressionParser();
|
||||
Expression exp=parser.parseExpression("'Hello World'.bytes");
|
||||
byte[] bytes=(byte[])exp.getValue();
|
||||
```
|
||||
- 在调用Expression类型的getValue方法时,可以不用进行强制类型转换,而是在getValue方法中传入一个Class参数,返回值将会被自动转换成Class对应的目标类型,当转换失败时会抛出EvaluationException
|
||||
```java
|
||||
ExpressionParser parser=new SpelExpressionParser();
|
||||
Expression exp=parser.parseExpression("'Hello World'.bytes.length");
|
||||
Integer bytes=exp.getValue(Integer.class);
|
||||
```
|
||||
- SpEL可以针对特定的对象,给出一个表达式并且在getValue方法中传入一个对象,那么表达式中的变量将会针对该对象中的特定属性
|
||||
```java
|
||||
// 如下步骤会比较waifu对象的name属性是否为"touma"字符串
|
||||
ExpressionParser parser=new SpelExpressionParser();
|
||||
Expression exp=parser.parseExpression("name=='touma'");
|
||||
Boolean equals=exp.getValue(waifu,Boolean.class);
|
||||
```
|
||||
- 可以为parser设置一个parserconfiguration,用于处理当列表或集合元素的index操作超过集合长度时的默认行为
|
||||
```java
|
||||
class Demo {
|
||||
public List<String> list;
|
||||
}
|
||||
|
||||
// Turn on:
|
||||
// - auto null reference initialization
|
||||
// - auto collection growing
|
||||
SpelParserConfiguration config = new SpelParserConfiguration(true, true);
|
||||
|
||||
ExpressionParser parser = new SpelExpressionParser(config);
|
||||
|
||||
Expression expression = parser.parseExpression("list[3]");
|
||||
|
||||
Demo demo = new Demo();
|
||||
|
||||
Object o = expression.getValue(demo);
|
||||
|
||||
// demo.list will now be a real collection of 4 entries
|
||||
// Each entry is a new empty String
|
||||
```
|
||||
- ## SpEL在bean对象定义时的使用
|
||||
- 在使用@Value注解时,可以结合SpEL表达式进行使用,@Value注解可以运用在域变量、方法、方法和构造器的参数上。@Value会指定默认值
|
||||
- ## SpEL对List、Map的支持
|
||||
- 可以通过{}来直接表示list
|
||||
```java
|
||||
List numbers = (List) parser.parseExpression("{1,2,3,4}").getValue(context);
|
||||
|
||||
List listOfLists = (List) parser.parseExpression("{{'a','b'},{'x','y'}}").getValue(context);
|
||||
```
|
||||
- 可以通过{key:value}形式来直接表示map,空map用{:}来进行表示
|
||||
```java
|
||||
// evaluates to a Java map containing the two entries
|
||||
Map inventorInfo = (Map) parser.parseExpression("{name:'Nikola',dob:'10-July-1856'}").getValue(context);
|
||||
|
||||
Map mapOfMaps = (Map) parser.parseExpression("{name:{first:'Nikola',last:'Tesla'},dob:{day:10,month:'July',year:1856}}").getValue(context);
|
||||
```
|
||||
- 可以通过new int[]{}的形式为SpEL指定数组
|
||||
```java
|
||||
int[] numbers1 = (int[]) parser.parseExpression("new int[4]").getValue(context);
|
||||
|
||||
// Array with initializer
|
||||
int[] numbers2 = (int[]) parser.parseExpression("new int[]{1,2,3}").getValue(context);
|
||||
|
||||
// Multi dimensional array
|
||||
int[][] numbers3 = (int[][]) parser.parseExpression("new int[4][5]").getValue(context);
|
||||
```
|
||||
- ## SpEL支持的特殊操作符
|
||||
- instanceof
|
||||
```java
|
||||
boolean falseValue = parser.parseExpression(
|
||||
"'xyz' instanceof T(Integer)").getValue(Boolean.class);
|
||||
```
|
||||
- 正则表达式
|
||||
```java
|
||||
boolean trueValue = parser.parseExpression(
|
||||
"'5.00' matches '^-?\\d+(\\.\\d{2})?$'").getValue(Boolean.class);
|
||||
```
|
||||
- 类型操作符,获取类型的Class对象、调用静态方法
|
||||
```java
|
||||
Class dateClass = parser.parseExpression("T(java.util.Date)").getValue(Class.class);
|
||||
|
||||
Class stringClass = parser.parseExpression("T(String)").getValue(Class.class);
|
||||
|
||||
boolean trueValue = parser.parseExpression(
|
||||
"T(java.math.RoundingMode).CEILING < T(java.math.RoundingMode).FLOOR")
|
||||
.getValue(Boolean.class);
|
||||
```
|
||||
- new操作符:
|
||||
- 可以在SpEL表达式中通过new操作符来调用构造器,但是除了位于java.lang包中的类,对其他的类调用构造器时都必须指定类的全类名
|
||||
```java
|
||||
Inventor einstein = p.parseExpression(
|
||||
"new org.spring.samples.spel.inventor.Inventor('Albert Einstein', 'German')")
|
||||
.getValue(Inventor.class);
|
||||
|
||||
// create new Inventor instance within the add() method of List
|
||||
p.parseExpression(
|
||||
"Members.add(new org.spring.samples.spel.inventor.Inventor(
|
||||
'Albert Einstein', 'German'))").getValue(societyContext);
|
||||
```
|
||||
@@ -1,177 +1,177 @@
|
||||
# Spring AOP
|
||||
- ## Spring AOP的核心概念
|
||||
- Aspect:切面,一个模块化的考虑
|
||||
- Joint Point:连接点,程序执行时的一个时间点,通常是方法的执行
|
||||
- Advice:当切面在一个切入点执行多做时,执行的动作被称之为Advice,Advice有不同的类型:before、after、around
|
||||
- Pointcut:切入点,advice通常运行在满足pointcut的join point上,pointcut表达式与join point相关联,Spring中默认使用AspectJ切入点表达式
|
||||
- Introduction:在类中声明新的方法、域变量甚至是接口实现
|
||||
- linking:将应用类型或对象和切面链接起来
|
||||
- ## Spring AOP的类型
|
||||
- before:在连接点之前运行,但是无法阻止后续连接点的执行
|
||||
- after returning:在连接点正常返回之后进行
|
||||
- after throwing:在链接点抛出异常正常退出之后进行
|
||||
- after finally:上两种的结合,不管连接点是正常退出还是抛出异常退出,都会在其之后执行
|
||||
- around:around可以自定义连接点之前和之后的执行内容,其也能够选择时候执行连接点的方法
|
||||
- ## Spring AOP的特点
|
||||
- 区别于AspectJ AOP框架,Spring AOP框架是基于代理来实现的
|
||||
- 对于实现了接口的类,Spring AOP通常是通过JDK动态代理来实现的,对于没有实现接口的类,Spring AOP是通过cglib来实现的
|
||||
- 可以强制Spring AOP使用cglib,在如下场景:
|
||||
- 如果想要advise类中方法,而该方法没有在接口中定义
|
||||
- 如果想要将代理对象传递给一个具有特定类型的方法作为参数
|
||||
- ## Spring AOP的AspectJ注解支持
|
||||
- Spring AOP支持AspectJ注解,Spring AOP可以解释和AspectJ 5相同的注解,通过使用AspectJ提供的包来进行切入点解析和匹配
|
||||
- 但是,即使使用了AspectJ注解,AOP在运行时仍然是纯粹的Spring AOP,项目不需要引入AspectJ的编译器和weaver
|
||||
- Spring AOP对AspectJ注解支持的开启
|
||||
- 通过@EnableAspectJAutoProxy注解,会自动的为满足切入点匹配的连接点bean对象创建移动代理对象
|
||||
```java
|
||||
@Configuration
|
||||
@EnableAspectJAutoProxy
|
||||
class AspectJConfiguration {
|
||||
// ...
|
||||
}
|
||||
```
|
||||
- ## 声明Spring AOP切面
|
||||
- 在容器中,任何bean对象,如其类型具有@AspectJ注解,将会被自动探知到并且用来配置spring aop
|
||||
- 在Spring AOP中,aspect其自身是无法作为其他aspect的目标对象的。被标记为@Aspect的类,不仅标明其为aspect,并且将其从自动代理中排除
|
||||
- 如果为某个bean对象配置了切面,那么在后续创建该bean对象时,实际上是创建该bean对象的代理对象
|
||||
```java
|
||||
@Component // 将该类型声明为bean对象
|
||||
@Aspect // 声明切面
|
||||
public class ProxyAspect {
|
||||
|
||||
}
|
||||
```
|
||||
- ## 声明Spring AOP切入点
|
||||
- 由于Spring AOP仅仅支持方法的连接点,故而可以将切入点看做对bean对象方法的匹配
|
||||
- Join Point expression的种类:
|
||||
- execution:匹配目标方法的执行,可以在括号中接收一个函数签名,包含返回类型、函数名和函数参数类型
|
||||
```java
|
||||
// 被@JointPoint注解标注的方法必须具有void的返回类型
|
||||
@Pointcut("execution(* Point.*(..))")
|
||||
void methodInjected() {
|
||||
|
||||
}
|
||||
```
|
||||
- within:匹配声明在某一特定类中的方法
|
||||
```java
|
||||
@Pointcut("within(Point)")
|
||||
```
|
||||
- this:匹配生成的代理对象为该类型的一个实例
|
||||
- target:匹配目标对象为该类型的一个实例
|
||||
- args:匹配特定参数
|
||||
- @args:传递参数的类型具有指定的注解
|
||||
- @target:运行时该对象的类具有指定的注解
|
||||
- @within:运行时执行的方法,其方法定义在具有指定注解的类中(可以是继承父类的方法,父类指定了注解
|
||||
- @annotation:执行的方法具有指定注解
|
||||
- Spring AOP同样支持将JoinPoint匹配为具有特定name的Spring bean对象
|
||||
```java
|
||||
@Pointcut("bean(nameA) || bean(nameB))")
|
||||
```
|
||||
- ## Spring AOP中的Advice
|
||||
- Advice和Pointcut Expresion相关联,主要可以分为before、after、around等种类
|
||||
- Before:
|
||||
```java
|
||||
@Before("execution(* Point.*(..))")
|
||||
public void doSomething() {
|
||||
|
||||
}
|
||||
```
|
||||
- AfterReturning:
|
||||
```java
|
||||
// AfterReturning支持获取切入点执行后返回的值
|
||||
@AfterReturning(
|
||||
pointcut="execution(* Point.*(..))",
|
||||
returning="retVal")
|
||||
public void doSomething(int retVal) {
|
||||
|
||||
}
|
||||
```
|
||||
- AfterThrowing:
|
||||
```java
|
||||
@AfterThrowing(
|
||||
pointcut="execution(* Point.*())",
|
||||
throwing="ex"
|
||||
)
|
||||
public void doSomething(Throwable ex) {
|
||||
|
||||
}
|
||||
```
|
||||
- After:After不管是切入点正常返回还是抛出异常,都会执行,类似于finally
|
||||
```java
|
||||
@After("execution(* Point.*())")
|
||||
public void doSomething() {
|
||||
|
||||
}
|
||||
```
|
||||
- Around:其方法必须会一个Oject类型的返回值,并且方法的第一个参数类型是ProceedingJoinPoint
|
||||
```java
|
||||
@Around("execution(* Point.*())")
|
||||
public Object doSomething(ProceedingJoinPoint pjp) {
|
||||
return isCacheExisted()?returnFromCache():pjp.proceed();
|
||||
}
|
||||
```
|
||||
- ## Spring AOP中Advice方法对JoinPoint的访问
|
||||
- 任何advice方法,都可以声明声明其第一个参数为JoinPoint类型。@Around标注的adivce方法其第一个参数的类型必须为ProceedingJoinPoint类型,该类型为JoinPoint的子类型
|
||||
- JoinPoint接口具有如下方法:
|
||||
- getArgs:返回方法参数
|
||||
- getThis:返回代理对象
|
||||
- getTarget:返回目标对象
|
||||
- getSignature:返回函数的签名
|
||||
- toString:返回该advice方法的描述信息
|
||||
- ## Advice方法通过参数来获取传递给底层方法的参数
|
||||
- 在pointcut表达式的args中,如果用advice方法中的参数名来代替参数类型,那么该类型的参数值会被传递给该参数
|
||||
```java
|
||||
@Before("execution(* Point.*(..) && args(position,..))")
|
||||
public void adviceMethod(Position position) {
|
||||
|
||||
}
|
||||
```
|
||||
- 或者,可以通过如下方式,先通过一个Pointcut获取参数,在在另一个方法中获取named pointcut已获取的参数
|
||||
```java
|
||||
// 此时,adviceMethodTwo同样能够获取Position参数
|
||||
@Pointcut("execution(* Point.*(..)) && args(position,..)")
|
||||
public void adviceMethodOne(Position position) {
|
||||
|
||||
}
|
||||
|
||||
@Before("adviceMethodOne(position)")
|
||||
public void adviceMethodTwo(Position position) {
|
||||
|
||||
}
|
||||
```
|
||||
- Spring AOP可以通过如下方式来约束泛型的参数
|
||||
```java
|
||||
@Before("execution(* GenericsInterface+.method(*) && args(param))")
|
||||
public void adviceMethod(DesiredType param) {
|
||||
|
||||
}
|
||||
```
|
||||
- ## 通过Spring AOP对参数进行预处理
|
||||
```java
|
||||
@Around("execution(* Point.area(*) && args(width,height))")
|
||||
public double caculateInCM(ProceedingJoinPoint jp,double width,double height) {
|
||||
width*=100;
|
||||
height*=100;
|
||||
return jp.proceed(width,height);
|
||||
}
|
||||
```
|
||||
- ## Spring AOP中多个advice对应到同一个Pointcut
|
||||
- 如果多个advice都具有相同的pointcut,那么多个advice之间的执行顺序是未定义的。可以为Aspect类实现Ordered接口,或者添加@Order标记来定义该advice的执行优先级,那么具有具有较小order值的方法将会优先被执行
|
||||
- ## Spring AOP Introduction
|
||||
- 在Spring AOP中,可以通过Introduction来声明一个对象继承了某接口,并且为被代理的对象提供被继承接口的实现
|
||||
- 可以通过@DeclareParent注解为指定对象添加接口并且指明该接口默认的实现类,完成后可以直接将生成的代理对象复制给接口变量
|
||||
```java
|
||||
@Aspect
|
||||
public class MyAspect {
|
||||
@DeclareParent(value="cc.rikakonatsumi.interfaces.*+",defaultImpl=DefaultImpl.class)
|
||||
private static MyInterface myInterface;
|
||||
|
||||
// 之后,可以直接通过this(ref)在pointcut表达式中获取服务对象,也可以通过getBean方法获取容器中的对象
|
||||
}
|
||||
```
|
||||
- ## @RestControllerAdvice的使用
|
||||
- @RestControllerAdvice是@Componnent注解的一个特例,@RestControllerAdivce注解的组成包含@Component
|
||||
- @RestControllerAdivce组合了@ControllerAdvice和@ResponseBody两个注解
|
||||
- 通常,@RestControllerAdvice用作为spring mvc的所有方法做ExceptionHandler
|
||||
|
||||
|
||||
# Spring AOP
|
||||
- ## Spring AOP的核心概念
|
||||
- Aspect:切面,一个模块化的考虑
|
||||
- Joint Point:连接点,程序执行时的一个时间点,通常是方法的执行
|
||||
- Advice:当切面在一个切入点执行多做时,执行的动作被称之为Advice,Advice有不同的类型:before、after、around
|
||||
- Pointcut:切入点,advice通常运行在满足pointcut的join point上,pointcut表达式与join point相关联,Spring中默认使用AspectJ切入点表达式
|
||||
- Introduction:在类中声明新的方法、域变量甚至是接口实现
|
||||
- linking:将应用类型或对象和切面链接起来
|
||||
- ## Spring AOP的类型
|
||||
- before:在连接点之前运行,但是无法阻止后续连接点的执行
|
||||
- after returning:在连接点正常返回之后进行
|
||||
- after throwing:在链接点抛出异常正常退出之后进行
|
||||
- after finally:上两种的结合,不管连接点是正常退出还是抛出异常退出,都会在其之后执行
|
||||
- around:around可以自定义连接点之前和之后的执行内容,其也能够选择时候执行连接点的方法
|
||||
- ## Spring AOP的特点
|
||||
- 区别于AspectJ AOP框架,Spring AOP框架是基于代理来实现的
|
||||
- 对于实现了接口的类,Spring AOP通常是通过JDK动态代理来实现的,对于没有实现接口的类,Spring AOP是通过cglib来实现的
|
||||
- 可以强制Spring AOP使用cglib,在如下场景:
|
||||
- 如果想要advise类中方法,而该方法没有在接口中定义
|
||||
- 如果想要将代理对象传递给一个具有特定类型的方法作为参数
|
||||
- ## Spring AOP的AspectJ注解支持
|
||||
- Spring AOP支持AspectJ注解,Spring AOP可以解释和AspectJ 5相同的注解,通过使用AspectJ提供的包来进行切入点解析和匹配
|
||||
- 但是,即使使用了AspectJ注解,AOP在运行时仍然是纯粹的Spring AOP,项目不需要引入AspectJ的编译器和weaver
|
||||
- Spring AOP对AspectJ注解支持的开启
|
||||
- 通过@EnableAspectJAutoProxy注解,会自动的为满足切入点匹配的连接点bean对象创建移动代理对象
|
||||
```java
|
||||
@Configuration
|
||||
@EnableAspectJAutoProxy
|
||||
class AspectJConfiguration {
|
||||
// ...
|
||||
}
|
||||
```
|
||||
- ## 声明Spring AOP切面
|
||||
- 在容器中,任何bean对象,如其类型具有@AspectJ注解,将会被自动探知到并且用来配置spring aop
|
||||
- 在Spring AOP中,aspect其自身是无法作为其他aspect的目标对象的。被标记为@Aspect的类,不仅标明其为aspect,并且将其从自动代理中排除
|
||||
- 如果为某个bean对象配置了切面,那么在后续创建该bean对象时,实际上是创建该bean对象的代理对象
|
||||
```java
|
||||
@Component // 将该类型声明为bean对象
|
||||
@Aspect // 声明切面
|
||||
public class ProxyAspect {
|
||||
|
||||
}
|
||||
```
|
||||
- ## 声明Spring AOP切入点
|
||||
- 由于Spring AOP仅仅支持方法的连接点,故而可以将切入点看做对bean对象方法的匹配
|
||||
- Join Point expression的种类:
|
||||
- execution:匹配目标方法的执行,可以在括号中接收一个函数签名,包含返回类型、函数名和函数参数类型
|
||||
```java
|
||||
// 被@JointPoint注解标注的方法必须具有void的返回类型
|
||||
@Pointcut("execution(* Point.*(..))")
|
||||
void methodInjected() {
|
||||
|
||||
}
|
||||
```
|
||||
- within:匹配声明在某一特定类中的方法
|
||||
```java
|
||||
@Pointcut("within(Point)")
|
||||
```
|
||||
- this:匹配生成的代理对象为该类型的一个实例
|
||||
- target:匹配目标对象为该类型的一个实例
|
||||
- args:匹配特定参数
|
||||
- @args:传递参数的类型具有指定的注解
|
||||
- @target:运行时该对象的类具有指定的注解
|
||||
- @within:运行时执行的方法,其方法定义在具有指定注解的类中(可以是继承父类的方法,父类指定了注解
|
||||
- @annotation:执行的方法具有指定注解
|
||||
- Spring AOP同样支持将JoinPoint匹配为具有特定name的Spring bean对象
|
||||
```java
|
||||
@Pointcut("bean(nameA) || bean(nameB))")
|
||||
```
|
||||
- ## Spring AOP中的Advice
|
||||
- Advice和Pointcut Expresion相关联,主要可以分为before、after、around等种类
|
||||
- Before:
|
||||
```java
|
||||
@Before("execution(* Point.*(..))")
|
||||
public void doSomething() {
|
||||
|
||||
}
|
||||
```
|
||||
- AfterReturning:
|
||||
```java
|
||||
// AfterReturning支持获取切入点执行后返回的值
|
||||
@AfterReturning(
|
||||
pointcut="execution(* Point.*(..))",
|
||||
returning="retVal")
|
||||
public void doSomething(int retVal) {
|
||||
|
||||
}
|
||||
```
|
||||
- AfterThrowing:
|
||||
```java
|
||||
@AfterThrowing(
|
||||
pointcut="execution(* Point.*())",
|
||||
throwing="ex"
|
||||
)
|
||||
public void doSomething(Throwable ex) {
|
||||
|
||||
}
|
||||
```
|
||||
- After:After不管是切入点正常返回还是抛出异常,都会执行,类似于finally
|
||||
```java
|
||||
@After("execution(* Point.*())")
|
||||
public void doSomething() {
|
||||
|
||||
}
|
||||
```
|
||||
- Around:其方法必须会一个Oject类型的返回值,并且方法的第一个参数类型是ProceedingJoinPoint
|
||||
```java
|
||||
@Around("execution(* Point.*())")
|
||||
public Object doSomething(ProceedingJoinPoint pjp) {
|
||||
return isCacheExisted()?returnFromCache():pjp.proceed();
|
||||
}
|
||||
```
|
||||
- ## Spring AOP中Advice方法对JoinPoint的访问
|
||||
- 任何advice方法,都可以声明声明其第一个参数为JoinPoint类型。@Around标注的adivce方法其第一个参数的类型必须为ProceedingJoinPoint类型,该类型为JoinPoint的子类型
|
||||
- JoinPoint接口具有如下方法:
|
||||
- getArgs:返回方法参数
|
||||
- getThis:返回代理对象
|
||||
- getTarget:返回目标对象
|
||||
- getSignature:返回函数的签名
|
||||
- toString:返回该advice方法的描述信息
|
||||
- ## Advice方法通过参数来获取传递给底层方法的参数
|
||||
- 在pointcut表达式的args中,如果用advice方法中的参数名来代替参数类型,那么该类型的参数值会被传递给该参数
|
||||
```java
|
||||
@Before("execution(* Point.*(..) && args(position,..))")
|
||||
public void adviceMethod(Position position) {
|
||||
|
||||
}
|
||||
```
|
||||
- 或者,可以通过如下方式,先通过一个Pointcut获取参数,在在另一个方法中获取named pointcut已获取的参数
|
||||
```java
|
||||
// 此时,adviceMethodTwo同样能够获取Position参数
|
||||
@Pointcut("execution(* Point.*(..)) && args(position,..)")
|
||||
public void adviceMethodOne(Position position) {
|
||||
|
||||
}
|
||||
|
||||
@Before("adviceMethodOne(position)")
|
||||
public void adviceMethodTwo(Position position) {
|
||||
|
||||
}
|
||||
```
|
||||
- Spring AOP可以通过如下方式来约束泛型的参数
|
||||
```java
|
||||
@Before("execution(* GenericsInterface+.method(*) && args(param))")
|
||||
public void adviceMethod(DesiredType param) {
|
||||
|
||||
}
|
||||
```
|
||||
- ## 通过Spring AOP对参数进行预处理
|
||||
```java
|
||||
@Around("execution(* Point.area(*) && args(width,height))")
|
||||
public double caculateInCM(ProceedingJoinPoint jp,double width,double height) {
|
||||
width*=100;
|
||||
height*=100;
|
||||
return jp.proceed(width,height);
|
||||
}
|
||||
```
|
||||
- ## Spring AOP中多个advice对应到同一个Pointcut
|
||||
- 如果多个advice都具有相同的pointcut,那么多个advice之间的执行顺序是未定义的。可以为Aspect类实现Ordered接口,或者添加@Order标记来定义该advice的执行优先级,那么具有具有较小order值的方法将会优先被执行
|
||||
- ## Spring AOP Introduction
|
||||
- 在Spring AOP中,可以通过Introduction来声明一个对象继承了某接口,并且为被代理的对象提供被继承接口的实现
|
||||
- 可以通过@DeclareParent注解为指定对象添加接口并且指明该接口默认的实现类,完成后可以直接将生成的代理对象复制给接口变量
|
||||
```java
|
||||
@Aspect
|
||||
public class MyAspect {
|
||||
@DeclareParent(value="cc.rikakonatsumi.interfaces.*+",defaultImpl=DefaultImpl.class)
|
||||
private static MyInterface myInterface;
|
||||
|
||||
// 之后,可以直接通过this(ref)在pointcut表达式中获取服务对象,也可以通过getBean方法获取容器中的对象
|
||||
}
|
||||
```
|
||||
- ## @RestControllerAdvice的使用
|
||||
- @RestControllerAdvice是@Componnent注解的一个特例,@RestControllerAdivce注解的组成包含@Component
|
||||
- @RestControllerAdivce组合了@ControllerAdvice和@ResponseBody两个注解
|
||||
- 通常,@RestControllerAdvice用作为spring mvc的所有方法做ExceptionHandler
|
||||
|
||||
|
||||
|
||||
@@ -1,183 +1,183 @@
|
||||
# Spring Core IOC
|
||||
- ## IOC容器和bean简介
|
||||
- IOC简介:
|
||||
- IOC(控制反转)也被称之为依赖注入(DI),对象通过构造函数参数、工厂方法参数、或者在构造后通过setter来设置属性来定义依赖。在对象被创建时,IOC容器会将依赖注入到bean对象中,
|
||||
- IOC容器:
|
||||
- IOC容器接口:
|
||||
- BeanFactory:BeanFactory是一个接口,提供了高级配置功能来管理任何类型的对象
|
||||
- ApplicationContext:ApplicationContext是BeanFactory的一个子接口,在BeanFactory的基础上,其添加了一些更为特殊的特性。
|
||||
- IOC容器职责
|
||||
- IOC容器负责来初始化、配置、组装bean对象
|
||||
- ## 基于注解的Spring容器配置
|
||||
- @Required
|
||||
- @Required应用于bean对象属性的setter方法,表示该属性在配置时必须被填充,通过依赖注入或是用xml定义bean时显式指定值
|
||||
- 该注解当前已经被弃用
|
||||
- @Autowired
|
||||
- 通过在构造函数上标明@Autowired来对方法参数进行注入
|
||||
- 当在构造函数上标记@Autowired时,如果当前类中只有一个构造函数,那么@Autowired注解可以被省略;如果当前类有多个构造函数,那么应该在某个构造函数上指明@Autowired注解
|
||||
```java
|
||||
@Component
|
||||
class Shiromiya {
|
||||
private JdbcTemplate jdbcTemplate;
|
||||
|
||||
@Autowired
|
||||
public Shiromiya(JdbcTemplate jdbcTemplate) {
|
||||
this.jdbcTemplate=jdbcTemplate;
|
||||
}
|
||||
}
|
||||
```
|
||||
- 同样,可以在setter上应用@Component
|
||||
```java
|
||||
@Component
|
||||
class Shiromiya {
|
||||
private JdbcTemplate jdbcTemplate;
|
||||
|
||||
@Autowired
|
||||
public setJdbcTemplate(JdbcTemplate jdbcTemplate) {
|
||||
this.jdbcTemplate=jdbcTemplate;
|
||||
}
|
||||
}
|
||||
```
|
||||
- 将@Autowired应用于字段
|
||||
```java
|
||||
@Component
|
||||
class Shiromiya {
|
||||
@Autowired
|
||||
private JdbcTemplate jdbcTemplate;
|
||||
}
|
||||
```
|
||||
- 通过@Autowired获取相同类型的所有bean对象
|
||||
```java
|
||||
@Component
|
||||
class Shiromiya {
|
||||
private String[] waifus;
|
||||
/*
|
||||
* 这里,同样也支持Collections类型
|
||||
* 例如 List<String>
|
||||
*/
|
||||
|
||||
@Autowired
|
||||
public Shiromiya(String[] waifus) {
|
||||
this.waifus=waifus;
|
||||
}
|
||||
}
|
||||
|
||||
// 同样,可以通过Map类型来获取所有相同类型bean对象的name和value
|
||||
// key:对应bean对象的name
|
||||
// value:对应该bean对象的值
|
||||
@Component
|
||||
class Shiromiya {
|
||||
private Map<String,String> waifus;
|
||||
|
||||
@Autowired
|
||||
public void setWaifus(Map<String,String> waifus) {
|
||||
this.waifus=waifus;
|
||||
}
|
||||
}
|
||||
```
|
||||
- 在@Autowired标记在构造函数上时,即使required为true,在参数为多bean类型时,即使没有匹配的bean,该属性会赋值为{}(空集合)而不是抛出异常
|
||||
- @Autowired作用与构造函数的规则
|
||||
- 当required属性为其默认值true时,在bean类型中只有一个构造函数可以用@Autowired标注
|
||||
- 如果bean类型中有多个构造函数标注了@Autowired注解,那么那么他们都必须将required属性设置为false,并且所有标注了@Autowired属性的构造函数都会被视为依赖注入的候选构造函数
|
||||
- 如果有多个候选的构造函数,那么在IOC容器中可以满足的匹配bean最多的构造函数将会被选中
|
||||
- 如果没有候选函数被选中,那么其会采用默认构造函数,如无默认构造函数,则抛出异常
|
||||
- 如果bean有多个构造函数,并且所有构造函数都没有标明@Autowired注解,那么会采用默认构造函数,如果默认构造函数不存在,抛出异常
|
||||
- 如果bean类型只有一个构造函数,那么该构造函数会被用来进行依赖注入,即使该构造函数没有标注@Autowired注解
|
||||
- 除了使用@Autowired的required属性,还可以使用@Nullable注解来标注可为空
|
||||
```java
|
||||
@Component
|
||||
public class Shiromiya {
|
||||
@Autowired
|
||||
@Nullable
|
||||
private int num;
|
||||
}
|
||||
```
|
||||
- @Primary
|
||||
- @Autowired注解是通过类型注入,如果相同类型存在多个bean时,可以通过@Primary注解来表明一个primary bean
|
||||
```java
|
||||
@Configuration
|
||||
public class BeanConfiguration {
|
||||
@Bean
|
||||
@Primary
|
||||
public String name_1() {
|
||||
return "kazusa";
|
||||
}
|
||||
|
||||
@Bean
|
||||
public String name_2() {
|
||||
return "ogiso";
|
||||
}
|
||||
}
|
||||
/*
|
||||
* 此时,若通过@Autowired注入String类型,“kazusa”将会是默认值
|
||||
*/
|
||||
```
|
||||
- @Qualifier
|
||||
- 可以通过@Qualifier来指定bean的name导入特定bean,并且可以为bean指定默认的qualifier
|
||||
```java
|
||||
@Component
|
||||
public class Shiromiya {
|
||||
@Autowired
|
||||
@Qualifier("name_2")
|
||||
private String n_2;
|
||||
|
||||
private String n_1;
|
||||
|
||||
@Autowired
|
||||
public Shiromiya(@Qualifier("name_1") String n) {
|
||||
this.n_1=n;
|
||||
}
|
||||
}
|
||||
```
|
||||
- bean对象的qualifier并不需要唯一,可以为不同的bean对象赋值相同的qualifier,并且在注入bean集合的时候根据qualifier过滤
|
||||
```java
|
||||
@Configuration
|
||||
@Qualifier("config")
|
||||
class BeanConfiguration {
|
||||
@Bean
|
||||
@Qualifier("name")
|
||||
public String name_1() {
|
||||
return "kazusa";
|
||||
}
|
||||
|
||||
@Bean
|
||||
@Qualifier("name")
|
||||
public String name_2() {
|
||||
return "ogiso";
|
||||
}
|
||||
|
||||
@Bean
|
||||
@Qualifier("not-name")
|
||||
public String not_name_1() {
|
||||
return "fuck";
|
||||
}
|
||||
}
|
||||
|
||||
@Component
|
||||
public class Person {
|
||||
/* 此nameList属性会注入qualifier为name的所有bean
|
||||
* 在此处为"kazusa"和"ogiso"
|
||||
*/
|
||||
@Autowired
|
||||
@Qualifier("name")
|
||||
Map<String,String> nameList;
|
||||
|
||||
@Autowired
|
||||
@Qualifier("config")
|
||||
BeanConfiguration conf;
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return "Person{" +
|
||||
"nameList=" + nameList +
|
||||
", conf=" + conf +
|
||||
'}';
|
||||
}
|
||||
}
|
||||
```
|
||||
- 作为一种回退机制,当bean的qualifier未被定义时,bean的name属性将会被作为其qualifier,autowired时会根据@Qualifier注解中指定的值匹配具有相同name的bean对象
|
||||
- 若想根据bean的name进行匹配,无需@Qualifier注解,只需要将注入点的name(filed的变量名,标注为@Autowired函数的形参名)和bean的name进行比较,如果相同则匹配成功,否则匹配失败
|
||||
- @Autowired同样支持自身引用的注入,但是自身引用的注入只能作为一种fallback机制。如果当前IOC容器中存在其他的同类型对象,那么其他对象会被优先注入,对象自己并不会参与候选的对象注入。但是,如果IOC中并不存在其他同类型对象,那么自身对象将会被作为引用注入。
|
||||
- @Resource
|
||||
- @Resource标签类似于@Autowired标签,但是@Resource具有一个name属性用来匹配bean对象的name属性
|
||||
# Spring Core IOC
|
||||
- ## IOC容器和bean简介
|
||||
- IOC简介:
|
||||
- IOC(控制反转)也被称之为依赖注入(DI),对象通过构造函数参数、工厂方法参数、或者在构造后通过setter来设置属性来定义依赖。在对象被创建时,IOC容器会将依赖注入到bean对象中,
|
||||
- IOC容器:
|
||||
- IOC容器接口:
|
||||
- BeanFactory:BeanFactory是一个接口,提供了高级配置功能来管理任何类型的对象
|
||||
- ApplicationContext:ApplicationContext是BeanFactory的一个子接口,在BeanFactory的基础上,其添加了一些更为特殊的特性。
|
||||
- IOC容器职责
|
||||
- IOC容器负责来初始化、配置、组装bean对象
|
||||
- ## 基于注解的Spring容器配置
|
||||
- @Required
|
||||
- @Required应用于bean对象属性的setter方法,表示该属性在配置时必须被填充,通过依赖注入或是用xml定义bean时显式指定值
|
||||
- 该注解当前已经被弃用
|
||||
- @Autowired
|
||||
- 通过在构造函数上标明@Autowired来对方法参数进行注入
|
||||
- 当在构造函数上标记@Autowired时,如果当前类中只有一个构造函数,那么@Autowired注解可以被省略;如果当前类有多个构造函数,那么应该在某个构造函数上指明@Autowired注解
|
||||
```java
|
||||
@Component
|
||||
class Shiromiya {
|
||||
private JdbcTemplate jdbcTemplate;
|
||||
|
||||
@Autowired
|
||||
public Shiromiya(JdbcTemplate jdbcTemplate) {
|
||||
this.jdbcTemplate=jdbcTemplate;
|
||||
}
|
||||
}
|
||||
```
|
||||
- 同样,可以在setter上应用@Component
|
||||
```java
|
||||
@Component
|
||||
class Shiromiya {
|
||||
private JdbcTemplate jdbcTemplate;
|
||||
|
||||
@Autowired
|
||||
public setJdbcTemplate(JdbcTemplate jdbcTemplate) {
|
||||
this.jdbcTemplate=jdbcTemplate;
|
||||
}
|
||||
}
|
||||
```
|
||||
- 将@Autowired应用于字段
|
||||
```java
|
||||
@Component
|
||||
class Shiromiya {
|
||||
@Autowired
|
||||
private JdbcTemplate jdbcTemplate;
|
||||
}
|
||||
```
|
||||
- 通过@Autowired获取相同类型的所有bean对象
|
||||
```java
|
||||
@Component
|
||||
class Shiromiya {
|
||||
private String[] waifus;
|
||||
/*
|
||||
* 这里,同样也支持Collections类型
|
||||
* 例如 List<String>
|
||||
*/
|
||||
|
||||
@Autowired
|
||||
public Shiromiya(String[] waifus) {
|
||||
this.waifus=waifus;
|
||||
}
|
||||
}
|
||||
|
||||
// 同样,可以通过Map类型来获取所有相同类型bean对象的name和value
|
||||
// key:对应bean对象的name
|
||||
// value:对应该bean对象的值
|
||||
@Component
|
||||
class Shiromiya {
|
||||
private Map<String,String> waifus;
|
||||
|
||||
@Autowired
|
||||
public void setWaifus(Map<String,String> waifus) {
|
||||
this.waifus=waifus;
|
||||
}
|
||||
}
|
||||
```
|
||||
- 在@Autowired标记在构造函数上时,即使required为true,在参数为多bean类型时,即使没有匹配的bean,该属性会赋值为{}(空集合)而不是抛出异常
|
||||
- @Autowired作用与构造函数的规则
|
||||
- 当required属性为其默认值true时,在bean类型中只有一个构造函数可以用@Autowired标注
|
||||
- 如果bean类型中有多个构造函数标注了@Autowired注解,那么那么他们都必须将required属性设置为false,并且所有标注了@Autowired属性的构造函数都会被视为依赖注入的候选构造函数
|
||||
- 如果有多个候选的构造函数,那么在IOC容器中可以满足的匹配bean最多的构造函数将会被选中
|
||||
- 如果没有候选函数被选中,那么其会采用默认构造函数,如无默认构造函数,则抛出异常
|
||||
- 如果bean有多个构造函数,并且所有构造函数都没有标明@Autowired注解,那么会采用默认构造函数,如果默认构造函数不存在,抛出异常
|
||||
- 如果bean类型只有一个构造函数,那么该构造函数会被用来进行依赖注入,即使该构造函数没有标注@Autowired注解
|
||||
- 除了使用@Autowired的required属性,还可以使用@Nullable注解来标注可为空
|
||||
```java
|
||||
@Component
|
||||
public class Shiromiya {
|
||||
@Autowired
|
||||
@Nullable
|
||||
private int num;
|
||||
}
|
||||
```
|
||||
- @Primary
|
||||
- @Autowired注解是通过类型注入,如果相同类型存在多个bean时,可以通过@Primary注解来表明一个primary bean
|
||||
```java
|
||||
@Configuration
|
||||
public class BeanConfiguration {
|
||||
@Bean
|
||||
@Primary
|
||||
public String name_1() {
|
||||
return "kazusa";
|
||||
}
|
||||
|
||||
@Bean
|
||||
public String name_2() {
|
||||
return "ogiso";
|
||||
}
|
||||
}
|
||||
/*
|
||||
* 此时,若通过@Autowired注入String类型,“kazusa”将会是默认值
|
||||
*/
|
||||
```
|
||||
- @Qualifier
|
||||
- 可以通过@Qualifier来指定bean的name导入特定bean,并且可以为bean指定默认的qualifier
|
||||
```java
|
||||
@Component
|
||||
public class Shiromiya {
|
||||
@Autowired
|
||||
@Qualifier("name_2")
|
||||
private String n_2;
|
||||
|
||||
private String n_1;
|
||||
|
||||
@Autowired
|
||||
public Shiromiya(@Qualifier("name_1") String n) {
|
||||
this.n_1=n;
|
||||
}
|
||||
}
|
||||
```
|
||||
- bean对象的qualifier并不需要唯一,可以为不同的bean对象赋值相同的qualifier,并且在注入bean集合的时候根据qualifier过滤
|
||||
```java
|
||||
@Configuration
|
||||
@Qualifier("config")
|
||||
class BeanConfiguration {
|
||||
@Bean
|
||||
@Qualifier("name")
|
||||
public String name_1() {
|
||||
return "kazusa";
|
||||
}
|
||||
|
||||
@Bean
|
||||
@Qualifier("name")
|
||||
public String name_2() {
|
||||
return "ogiso";
|
||||
}
|
||||
|
||||
@Bean
|
||||
@Qualifier("not-name")
|
||||
public String not_name_1() {
|
||||
return "fuck";
|
||||
}
|
||||
}
|
||||
|
||||
@Component
|
||||
public class Person {
|
||||
/* 此nameList属性会注入qualifier为name的所有bean
|
||||
* 在此处为"kazusa"和"ogiso"
|
||||
*/
|
||||
@Autowired
|
||||
@Qualifier("name")
|
||||
Map<String,String> nameList;
|
||||
|
||||
@Autowired
|
||||
@Qualifier("config")
|
||||
BeanConfiguration conf;
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return "Person{" +
|
||||
"nameList=" + nameList +
|
||||
", conf=" + conf +
|
||||
'}';
|
||||
}
|
||||
}
|
||||
```
|
||||
- 作为一种回退机制,当bean的qualifier未被定义时,bean的name属性将会被作为其qualifier,autowired时会根据@Qualifier注解中指定的值匹配具有相同name的bean对象
|
||||
- 若想根据bean的name进行匹配,无需@Qualifier注解,只需要将注入点的name(filed的变量名,标注为@Autowired函数的形参名)和bean的name进行比较,如果相同则匹配成功,否则匹配失败
|
||||
- @Autowired同样支持自身引用的注入,但是自身引用的注入只能作为一种fallback机制。如果当前IOC容器中存在其他的同类型对象,那么其他对象会被优先注入,对象自己并不会参与候选的对象注入。但是,如果IOC中并不存在其他同类型对象,那么自身对象将会被作为引用注入。
|
||||
- @Resource
|
||||
- @Resource标签类似于@Autowired标签,但是@Resource具有一个name属性用来匹配bean对象的name属性
|
||||
- @Resource标签首先会对具有相同name的bean对象,如果没有匹配到具有相同name的bean对象,才会fallback到类型匹配
|
||||
@@ -1,71 +1,71 @@
|
||||
# Spring Data Access
|
||||
- ## Spring事务
|
||||
- 本地事务和全局事务:
|
||||
- 全局事务:全局事务允许使用多个事务资源,应用服务器来对全局事务进行管理
|
||||
- 本地事务:本地事务无法管理多个事务资源
|
||||
- 本地事务和全局事务的优缺点
|
||||
- 全局事务的使用需要和服务器环境相绑定,降低了代码的重用性
|
||||
- 本地事务无法使用多个事务资源,无法通过JTA等框架来对多个事务资源进行管理,无法使用分布式事务
|
||||
- ## 声明式事务
|
||||
- Spring中声明式事务是通过AOP来实现的
|
||||
- 在声明式事务中,可以为方法级的粒度指定事务行为
|
||||
- 声明式事务的回滚规则:
|
||||
- 在Spring声明式事务中,可以为事务指定回滚规则,即指定针对哪些异常,事务会自动执行回滚操作
|
||||
- 在默认情况下,只有抛出unchecked异常(通常为RuntimeException)时,声明式事务才会进行回滚
|
||||
- 声明式事务的实现细节:
|
||||
- 声明式事务通过aop代理实现,并且事务的advice是通过xml元数据配置来驱动的
|
||||
- aop和事务元数据联合产生了一个aop代理对象,并且该代理对象通过使用TransactionInterceptor和TransactionManager来实现事务
|
||||
- @Transactional通常和线程绑定的事务一起工作,线程绑定的事务由PlatformTransactionManager管理。@Transactional会将事务暴露给当前执行线程中所有的dao操作
|
||||
- 声明式事务的回滚:
|
||||
- 在Spring事务中推荐让事务回滚的方式是在事务执行的方法中抛出一个异常
|
||||
- Spring事务在默认情况下只会针对unchecked异常(RuntimeException)进行回滚,对于Error,Spring事务也会执行回滚操作
|
||||
- checked异常并不会导致事务的回滚操作,可以注册rollback rule来指定对特定的异常(包括checked异常)进行回滚操作
|
||||
- rollback rule:
|
||||
- 回滚规则(rollback rule)通常用来指定当一个异常被抛出时,是否为该异常执行事务的回滚操作
|
||||
- 在@Transactional注解中,可以指定rollbackFor/noRollbackFor、rollbackForClassName/noRollbackForClassName来指定为那些异常类执行回滚操作
|
||||
> 当指定rollbackFor属性为checked异常时(如rollbackFor=FileNotFoundException.class),此时指定的异常不会覆盖其默认行为(为RuntimeException和Error异常执行回滚操作)。
|
||||
> 故而指定后其默认会为Error、RuntimeException、FileNotFoundException三类异常执行回滚操作
|
||||
|
||||
```java
|
||||
@Transactional(rollbackFor={MyException.class})
|
||||
public void myOperation() {
|
||||
// ...
|
||||
}
|
||||
```
|
||||
- ## 基于注解的声明式事务
|
||||
- @Transactional既可以作用于类上,也可以作用于方法上。当作用于类上时,该声明类中所有的方法都会被声明是事务的,该类子类中所有的方法也都是事务的
|
||||
- @Transactional是可以继承的,被@Inherited元注解修饰。
|
||||
>@Inherited类是元注解,用来修饰注解类。如果一个注解类被@Inherited注解标识,那么在对class查询该注解类时,如果当前class没有声明该注解,将会在当前class的父类中查找该注解,依次递归。直到在父类中找到该注解或是到达继承结构的顶部(Object类)。@Inherited标注的注解仅能在类继承中有效,如注解被标注在接口上,那么将@Inherited标注的注解将不会被递归查询到。
|
||||
- 并且,class级别的@Transactional并不应用在从父类继承的方法上,即若一个类被@Transactional注解标注,并且该类从父类继承方法,那么该类从父类继承的方法并不会被看作是事务的,除非在该类中重新声明继承的方法。
|
||||
- 通过在Configuration类上注解@EnableTransactionManagement,配合@Transactional可以将一个bean对象声明是事务的
|
||||
- 当基于标准spring配置时,应该仅将@Transactional注解标注于public方法,当将@Transactional注解标注于非public方法时无效
|
||||
- 在Spring中,仅推荐将@Transactional注解应用于类上,不推荐将其应用在接口(接口方法)上。如果将其应用在接口上,那么该事务配置仅仅对基于接口的动态代理有效,对基于class的代理无效。
|
||||
- 当类级别和方法级别都设置了@Transactional注解时,方法级别的设置会优先被使用
|
||||
- ## @Transactional注解的配置
|
||||
- 事务的传播: 默认情况下,@Transactional的propagation属性是PROPAGATION_REQUIRED
|
||||
- 事务的隔离级别: 默认情况下,@Transactional的isolation属性是ISOLATION_DEFAULT,使用数据库默认的隔离级别
|
||||
- readOnly: 默认情况下,@Transactional的readOnly属性是false,默认事务是可读写的
|
||||
- timeout: 默认况下下,@Transactional的超时属性取决于底层的事务系统,如果底层事务系统不支持timeout,则timeout属性为none
|
||||
- rollbackFor: 默认情况下,@Transactional会针对unchecked异常和Error进行回滚操作
|
||||
- transactionManager: 默认情况下,@Transactional注解会使用项目中默认的事务管理器(即bean name为transactionManager的事务管理器)。可以为@Transactional注解指定value属性或是transactionManager属性来指定想要采用的事务管理器的bean name或是qualifier
|
||||
- ## Transaction Propagation
|
||||
- ### PROPAGATION.REQUIRED
|
||||
- 在Spring中,事务的传播行为默认是PROPAGATION_REQUIRED,默认情况下该选项会强制的要求一个物理事务
|
||||
- 如果当前作用域中不存在事务,那么会创建一个新的事务
|
||||
- 如果当前作用域的外层作用域已经存在事务,那么会加入到当前作用域的事务中去
|
||||
- 在Spring中,默认情况下,当嵌套事务加入到外层的事务中时,会忽略内层事务定义的隔离级别、timeout设置和读写标志等。
|
||||
> 如果想要对外层事务进行验证,可以手动将事务管理器的validateExistingTransaction属性设置为true。这样,当加入到一个隔离级别与内层事务完全不同的外层事务中时,该加入操作会被拒绝。在该设置下,如果read-write内层事务想要加入到外层的read-only事务中时,该加入操作也会被拒绝。
|
||||
- 在事务传播行为被设置为PROPAGATION_REQUIRED的情况下,会为每个被设置事务的方法创建一个逻辑的事务作用域。各个逻辑事务作用域之间都是相互独立的,在不同逻辑事务作用域之间都可以独立设置事务的rollback-only属性。但是,在PROPAGATION_REQUIRED的情况下,内层事务和外层事务都映射到同一个物理事务,内层事务加入到外层事务中,故而在内层逻辑事务中为物理事务设置rollback-only会切实影响到外层事务的提交。
|
||||
- 当事务传播行为被设置为PROPAGATION_REQUIRED时,如果内层事务设置了rollback-only标记,那么会导致外层物理事务的回滚。当外层事务尝试提交并失败回滚后,会抛出一个UnexceptedRollbackException异常,外层事务commit方法的调用者会接受到该UnexceptedRollbackException,代表内层发生了非预期的回滚操作
|
||||
- ### PROPAGATION.REQUIRES_NEW
|
||||
- 相对于PROPAGATION_REQUIRED,PROPAGATION.REQUIRES_NEW传播行为会一直使用独立的物理事务,而不会尝试区加入外部已经存在的物理事务。
|
||||
- 对于PROPAGATION_NEW,其内层事务和外层事务都可以独立的提交或回滚,内层事务的回滚并不会导致外层事务的回滚。
|
||||
- 将事务传播行为设置为PROPAGATION.REQUIRES_NEW时,内层事务可以独立定义自己的隔离级别、timeout值、read-only属性,而不必继承外部事务的这些属性。在PROPAGATION_REQUIRED中,内部事务自定义这些属性将会被忽略,内部事务加入外部事务后会采用外部事务的设置。
|
||||
- ### PROPAGATION.NESTED
|
||||
- 和PROPAGATION_REQUIRED类似,PROPAGATION_NESTED同样也只有一个物理事务。但是其支持多个savepoint(存档点),该物理事务可以回滚到特定的存档点而非必须回滚整个事务。
|
||||
- 由于PROPAGATION_NESTED对存档点的支持,故而在PROPAGATION_NESTED条件下,可以进行部分回滚。内层事务的回滚操作并不会造成外部事务的回滚,内层事务回滚后外层事务仍然能够继续执行和提交。
|
||||
> 由于PROPAGATION_NESTED需要JDBC savepoint(存档点)的支持,故而该设置仅仅对JDBC事务资源有效。
|
||||
|
||||
> 当事务被回滚之后,当前事务无法再被提交,故而:
|
||||
> 若在子事务中已经回滚(子事务传播行为为required),那么父事务的状态已经被回滚,即使父事务捕获子事务抛出的异常,那么在捕获异常之后执行的sql操作也不会被提交到数据库中,父事务状态处于已回滚,无法再次提交
|
||||
> ***但是,当子事务传播行为为nested时,子事务虽然和父事务共用一个事务,子事务回滚时只会回滚到子事务开启之前的存档点,父事务在捕获子事务抛出异常之后执行的sql语句仍然可以被提交***
|
||||
# Spring Data Access
|
||||
- ## Spring事务
|
||||
- 本地事务和全局事务:
|
||||
- 全局事务:全局事务允许使用多个事务资源,应用服务器来对全局事务进行管理
|
||||
- 本地事务:本地事务无法管理多个事务资源
|
||||
- 本地事务和全局事务的优缺点
|
||||
- 全局事务的使用需要和服务器环境相绑定,降低了代码的重用性
|
||||
- 本地事务无法使用多个事务资源,无法通过JTA等框架来对多个事务资源进行管理,无法使用分布式事务
|
||||
- ## 声明式事务
|
||||
- Spring中声明式事务是通过AOP来实现的
|
||||
- 在声明式事务中,可以为方法级的粒度指定事务行为
|
||||
- 声明式事务的回滚规则:
|
||||
- 在Spring声明式事务中,可以为事务指定回滚规则,即指定针对哪些异常,事务会自动执行回滚操作
|
||||
- 在默认情况下,只有抛出unchecked异常(通常为RuntimeException)时,声明式事务才会进行回滚
|
||||
- 声明式事务的实现细节:
|
||||
- 声明式事务通过aop代理实现,并且事务的advice是通过xml元数据配置来驱动的
|
||||
- aop和事务元数据联合产生了一个aop代理对象,并且该代理对象通过使用TransactionInterceptor和TransactionManager来实现事务
|
||||
- @Transactional通常和线程绑定的事务一起工作,线程绑定的事务由PlatformTransactionManager管理。@Transactional会将事务暴露给当前执行线程中所有的dao操作
|
||||
- 声明式事务的回滚:
|
||||
- 在Spring事务中推荐让事务回滚的方式是在事务执行的方法中抛出一个异常
|
||||
- Spring事务在默认情况下只会针对unchecked异常(RuntimeException)进行回滚,对于Error,Spring事务也会执行回滚操作
|
||||
- checked异常并不会导致事务的回滚操作,可以注册rollback rule来指定对特定的异常(包括checked异常)进行回滚操作
|
||||
- rollback rule:
|
||||
- 回滚规则(rollback rule)通常用来指定当一个异常被抛出时,是否为该异常执行事务的回滚操作
|
||||
- 在@Transactional注解中,可以指定rollbackFor/noRollbackFor、rollbackForClassName/noRollbackForClassName来指定为那些异常类执行回滚操作
|
||||
> 当指定rollbackFor属性为checked异常时(如rollbackFor=FileNotFoundException.class),此时指定的异常不会覆盖其默认行为(为RuntimeException和Error异常执行回滚操作)。
|
||||
> 故而指定后其默认会为Error、RuntimeException、FileNotFoundException三类异常执行回滚操作
|
||||
|
||||
```java
|
||||
@Transactional(rollbackFor={MyException.class})
|
||||
public void myOperation() {
|
||||
// ...
|
||||
}
|
||||
```
|
||||
- ## 基于注解的声明式事务
|
||||
- @Transactional既可以作用于类上,也可以作用于方法上。当作用于类上时,该声明类中所有的方法都会被声明是事务的,该类子类中所有的方法也都是事务的
|
||||
- @Transactional是可以继承的,被@Inherited元注解修饰。
|
||||
>@Inherited类是元注解,用来修饰注解类。如果一个注解类被@Inherited注解标识,那么在对class查询该注解类时,如果当前class没有声明该注解,将会在当前class的父类中查找该注解,依次递归。直到在父类中找到该注解或是到达继承结构的顶部(Object类)。@Inherited标注的注解仅能在类继承中有效,如注解被标注在接口上,那么将@Inherited标注的注解将不会被递归查询到。
|
||||
- 并且,class级别的@Transactional并不应用在从父类继承的方法上,即若一个类被@Transactional注解标注,并且该类从父类继承方法,那么该类从父类继承的方法并不会被看作是事务的,除非在该类中重新声明继承的方法。
|
||||
- 通过在Configuration类上注解@EnableTransactionManagement,配合@Transactional可以将一个bean对象声明是事务的
|
||||
- 当基于标准spring配置时,应该仅将@Transactional注解标注于public方法,当将@Transactional注解标注于非public方法时无效
|
||||
- 在Spring中,仅推荐将@Transactional注解应用于类上,不推荐将其应用在接口(接口方法)上。如果将其应用在接口上,那么该事务配置仅仅对基于接口的动态代理有效,对基于class的代理无效。
|
||||
- 当类级别和方法级别都设置了@Transactional注解时,方法级别的设置会优先被使用
|
||||
- ## @Transactional注解的配置
|
||||
- 事务的传播: 默认情况下,@Transactional的propagation属性是PROPAGATION_REQUIRED
|
||||
- 事务的隔离级别: 默认情况下,@Transactional的isolation属性是ISOLATION_DEFAULT,使用数据库默认的隔离级别
|
||||
- readOnly: 默认情况下,@Transactional的readOnly属性是false,默认事务是可读写的
|
||||
- timeout: 默认况下下,@Transactional的超时属性取决于底层的事务系统,如果底层事务系统不支持timeout,则timeout属性为none
|
||||
- rollbackFor: 默认情况下,@Transactional会针对unchecked异常和Error进行回滚操作
|
||||
- transactionManager: 默认情况下,@Transactional注解会使用项目中默认的事务管理器(即bean name为transactionManager的事务管理器)。可以为@Transactional注解指定value属性或是transactionManager属性来指定想要采用的事务管理器的bean name或是qualifier
|
||||
- ## Transaction Propagation
|
||||
- ### PROPAGATION.REQUIRED
|
||||
- 在Spring中,事务的传播行为默认是PROPAGATION_REQUIRED,默认情况下该选项会强制的要求一个物理事务
|
||||
- 如果当前作用域中不存在事务,那么会创建一个新的事务
|
||||
- 如果当前作用域的外层作用域已经存在事务,那么会加入到当前作用域的事务中去
|
||||
- 在Spring中,默认情况下,当嵌套事务加入到外层的事务中时,会忽略内层事务定义的隔离级别、timeout设置和读写标志等。
|
||||
> 如果想要对外层事务进行验证,可以手动将事务管理器的validateExistingTransaction属性设置为true。这样,当加入到一个隔离级别与内层事务完全不同的外层事务中时,该加入操作会被拒绝。在该设置下,如果read-write内层事务想要加入到外层的read-only事务中时,该加入操作也会被拒绝。
|
||||
- 在事务传播行为被设置为PROPAGATION_REQUIRED的情况下,会为每个被设置事务的方法创建一个逻辑的事务作用域。各个逻辑事务作用域之间都是相互独立的,在不同逻辑事务作用域之间都可以独立设置事务的rollback-only属性。但是,在PROPAGATION_REQUIRED的情况下,内层事务和外层事务都映射到同一个物理事务,内层事务加入到外层事务中,故而在内层逻辑事务中为物理事务设置rollback-only会切实影响到外层事务的提交。
|
||||
- 当事务传播行为被设置为PROPAGATION_REQUIRED时,如果内层事务设置了rollback-only标记,那么会导致外层物理事务的回滚。当外层事务尝试提交并失败回滚后,会抛出一个UnexceptedRollbackException异常,外层事务commit方法的调用者会接受到该UnexceptedRollbackException,代表内层发生了非预期的回滚操作
|
||||
- ### PROPAGATION.REQUIRES_NEW
|
||||
- 相对于PROPAGATION_REQUIRED,PROPAGATION.REQUIRES_NEW传播行为会一直使用独立的物理事务,而不会尝试区加入外部已经存在的物理事务。
|
||||
- 对于PROPAGATION_NEW,其内层事务和外层事务都可以独立的提交或回滚,内层事务的回滚并不会导致外层事务的回滚。
|
||||
- 将事务传播行为设置为PROPAGATION.REQUIRES_NEW时,内层事务可以独立定义自己的隔离级别、timeout值、read-only属性,而不必继承外部事务的这些属性。在PROPAGATION_REQUIRED中,内部事务自定义这些属性将会被忽略,内部事务加入外部事务后会采用外部事务的设置。
|
||||
- ### PROPAGATION.NESTED
|
||||
- 和PROPAGATION_REQUIRED类似,PROPAGATION_NESTED同样也只有一个物理事务。但是其支持多个savepoint(存档点),该物理事务可以回滚到特定的存档点而非必须回滚整个事务。
|
||||
- 由于PROPAGATION_NESTED对存档点的支持,故而在PROPAGATION_NESTED条件下,可以进行部分回滚。内层事务的回滚操作并不会造成外部事务的回滚,内层事务回滚后外层事务仍然能够继续执行和提交。
|
||||
> 由于PROPAGATION_NESTED需要JDBC savepoint(存档点)的支持,故而该设置仅仅对JDBC事务资源有效。
|
||||
|
||||
> 当事务被回滚之后,当前事务无法再被提交,故而:
|
||||
> 若在子事务中已经回滚(子事务传播行为为required),那么父事务的状态已经被回滚,即使父事务捕获子事务抛出的异常,那么在捕获异常之后执行的sql操作也不会被提交到数据库中,父事务状态处于已回滚,无法再次提交
|
||||
> ***但是,当子事务传播行为为nested时,子事务虽然和父事务共用一个事务,子事务回滚时只会回滚到子事务开启之前的存档点,父事务在捕获子事务抛出异常之后执行的sql语句仍然可以被提交***
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,177 +1,177 @@
|
||||
- [gson](#gson)
|
||||
- [gson简介](#gson简介)
|
||||
- [gson使用](#gson使用)
|
||||
- [Gson库通过Maven引入](#gson库通过maven引入)
|
||||
- [基本类型的序列化和反序列化](#基本类型的序列化和反序列化)
|
||||
- [对象的序列化和反序列化](#对象的序列化和反序列化)
|
||||
- [gson和对象联用的使用规范](#gson和对象联用的使用规范)
|
||||
- [gson和嵌套类的关联使用](#gson和嵌套类的关联使用)
|
||||
- [gson和数组的关联使用](#gson和数组的关联使用)
|
||||
- [gson对java中的集合进行序列化和反序列化](#gson对java中的集合进行序列化和反序列化)
|
||||
- [gson对Map类型的序列化和反序列化](#gson对map类型的序列化和反序列化)
|
||||
- [序列化和反序列化泛型对象](#序列化和反序列化泛型对象)
|
||||
- [序列化和反序列化集合,集合中保存任意类型的对象](#序列化和反序列化集合集合中保存任意类型的对象)
|
||||
|
||||
# gson
|
||||
## gson简介
|
||||
gson是一个java库,通常用来将java对象转化为其json表示的字符串,或者将json格式的字符串转化为其等价的java对象。
|
||||
## gson使用
|
||||
在gson中,使用最频繁的类是Gson。可以通过new Gson()构造函数来创建Gson对象,也可以通过GsonBuilder来创建Gson对象,GsonBuidler在创建Gson对象时能够指定一些设置,如版本控制等。
|
||||
由于Gson对象在执行json操作时并不会保存任何状态,故而Gson对象是线程安全的,单一的Gson对象可以在多线程环境下被重复使用。
|
||||
### Gson库通过Maven引入
|
||||
```xml
|
||||
<dependencies>
|
||||
<!-- Gson: Java to JSON conversion -->
|
||||
<dependency>
|
||||
<groupId>com.google.code.gson</groupId>
|
||||
<artifactId>gson</artifactId>
|
||||
<version>2.9.1</version>
|
||||
<scope>compile</scope>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
```
|
||||
### 基本类型的序列化和反序列化
|
||||
```java
|
||||
// Serialization
|
||||
Gson gson = new Gson();
|
||||
gson.toJson(1); // ==> 1
|
||||
gson.toJson("abcd"); // ==> "abcd"
|
||||
gson.toJson(new Long(10)); // ==> 10
|
||||
int[] values = { 1 };
|
||||
gson.toJson(values); // ==> [1]
|
||||
|
||||
// Deserialization
|
||||
int one = gson.fromJson("1", int.class);
|
||||
Integer one = gson.fromJson("1", Integer.class);
|
||||
Long one = gson.fromJson("1", Long.class);
|
||||
Boolean false = gson.fromJson("false", Boolean.class);
|
||||
String str = gson.fromJson("\"abc\"", String.class);
|
||||
String[] anotherStr = gson.fromJson("[\"abc\"]", String[].class);
|
||||
```
|
||||
### 对象的序列化和反序列化
|
||||
类似于java自带的序列化机制,当成员字段被transient修饰时,并不会序列化该字段
|
||||
```java
|
||||
class BagOfPrimitives {
|
||||
private int value1 = 1;
|
||||
private String value2 = "abc";
|
||||
private transient int value3 = 3;
|
||||
BagOfPrimitives() {
|
||||
// no-args constructor
|
||||
}
|
||||
}
|
||||
|
||||
// Serialization
|
||||
BagOfPrimitives obj = new BagOfPrimitives();
|
||||
Gson gson = new Gson();
|
||||
String json = gson.toJson(obj);
|
||||
|
||||
// ==> json is {"value1":1,"value2":"abc"}
|
||||
|
||||
// Deserialization
|
||||
BagOfPrimitives obj2 = gson.fromJson(json, BagOfPrimitives.class);
|
||||
// ==> obj2 is just like obj
|
||||
```
|
||||
### gson和对象联用的使用规范
|
||||
- gson使用过程中,待序列化或反序列化的成员字段可以是private的,同时也推荐将待序列化或反序列化的成员字段声明为private
|
||||
- 没有必要对成员字段使用注解来特定标明在序列化或者反序列化中包含该字段,默认情况下该对象所有成员字段和该对象父类对象所包含的所有字段都会被序列化
|
||||
- 类似于jdk自带的序列化和反序列化机制,如果一个字段被标明为transient,该字段将不会被包含到序列化和反序列化过程中
|
||||
- gson实现能够正确处理字段为空的情况
|
||||
- 当序列化过程中,为null的字段将会被省略
|
||||
- 当反序列化过程中,如果一个字段在json串中并没有被设置,反序列化得到的对象中该字段将会被设置为默认值:引用类型默认值为null、数字类型默认值为0,boolean类型其默认值为false
|
||||
- 在内部类、匿名类、本地类中关联外部类的字段将会被忽略,并不会包含在序列化和反序列化过程中
|
||||
### gson和嵌套类的关联使用
|
||||
gson可以单独的对静态内部类进行序列化和反序列化,因为静态内部类并不包含对外部类的引用;但是gson无法单独的反序列化内部类,因为在反序列化内部类的过程中,其无参构造器需要一个指向其外部类对象的引用,但是该外部类对象在对内部类进行反序列化时是不可访问的。
|
||||
可以通过将内部类改为静态的内部类,此时对内部类的反序列化将不会需要指向外部类对象的引用。
|
||||
### gson和数组的关联使用
|
||||
gson支持多维数组,并且支持任意的复杂元素类型
|
||||
```java
|
||||
Gson gson = new Gson();
|
||||
int[] ints = {1, 2, 3, 4, 5};
|
||||
String[] strings = {"abc", "def", "ghi"};
|
||||
|
||||
// Serialization
|
||||
gson.toJson(ints); // ==> [1,2,3,4,5]
|
||||
gson.toJson(strings); // ==> ["abc", "def", "ghi"]
|
||||
|
||||
// Deserialization
|
||||
int[] ints2 = gson.fromJson("[1,2,3,4,5]", int[].class);
|
||||
// ==> ints2 will be same as ints
|
||||
```
|
||||
### gson对java中的集合进行序列化和反序列化
|
||||
gson可以序列化任意对象的集合,但是无法对其进行反序列化,因为在反序列化时用户没有任何方法去指定其生成的集合中元素的类型。因而,需要通过typeToken来告知Gson需要反序列化的类型。
|
||||
```java
|
||||
Gson gson = new Gson();
|
||||
Collection<Integer> ints = Arrays.asList(1,2,3,4,5);
|
||||
|
||||
// Serialization
|
||||
String json = gson.toJson(ints); // ==> json is [1,2,3,4,5]
|
||||
|
||||
// Deserialization
|
||||
Type collectionType = new TypeToken<Collection<Integer>>(){}.getType();
|
||||
Collection<Integer> ints2 = gson.fromJson(json, collectionType);
|
||||
// ==> ints2 is same as ints
|
||||
```
|
||||
### gson对Map类型的序列化和反序列化
|
||||
默认情况下,gson会将java中任意的Map实现类型序列化为JSON对象。由于JSON对象其key只支持字符串类型,gson会将待序列化的Map key调用toString转化为字符串。如果map中的key为null,则序列化后的key为"null"
|
||||
```java
|
||||
/**
|
||||
* gson对map进行序列化
|
||||
**/
|
||||
Gson gson = new Gson();
|
||||
Map<String, String> stringMap = new LinkedHashMap<>();
|
||||
stringMap.put("key", "value");
|
||||
stringMap.put(null, "null-entry");
|
||||
|
||||
// Serialization
|
||||
String json = gson.toJson(stringMap); // ==> json is {"key":"value","null":"null-entry"}
|
||||
|
||||
Map<Integer, Integer> intMap = new LinkedHashMap<>();
|
||||
intMap.put(2, 4);
|
||||
intMap.put(3, 6);
|
||||
|
||||
// Serialization
|
||||
String json = gson.toJson(intMap); // ==> json is {"2":4,"3":6}
|
||||
```
|
||||
在反序列化的过程中,gson会使用为Map key类型注册的TypeAdapter的read方法来进行反序列化。为了让gson知道反序列化得到的Map对象的key和value类型,需要使用TypeToken。
|
||||
```java
|
||||
Gson gson = new Gson();
|
||||
Type mapType = new TypeToken<Map<String, String>>(){}.getType();
|
||||
String json = "{\"key\": \"value\"}";
|
||||
|
||||
// Deserialization
|
||||
Map<String, String> stringMap = gson.fromJson(json, mapType);
|
||||
// ==> stringMap is {key=value}
|
||||
```
|
||||
默认情况下,gson序列化map时,复杂类型的key会调用toString方法来将其转化成字符串。但是,gson同样支持开启复杂类型key的序列化操作。通过Gson.enableComplexMapKeySerialization()方法来开启,Gson会调用为Map的key类型注册的TypeAdapter的write方法来序列化key,而不是通过toString方法将key转化为字符串。
|
||||
> 当Map中任意一条Entry的key通过Adapter被序列化为了JSON数组或者对象,那么Gson会将整个Map序列化为Json数组,数组元素为map中entry的键值对。如果map中所有的entry key都不会被序列化为json object或json array,那么该map将会被序列化为json对象
|
||||
|
||||
> 在对枚举型key进行反序列化的过程中,如果enum找不到一个具有匹配name()值的常量时,其会采用一个回退机制,根据枚举常量的toString()值来进行反序列化的匹配。
|
||||
|
||||
### 序列化和反序列化泛型对象
|
||||
当gson对object对象调用toJson方法时,gson会调用object.getClass()来获取需要序列化的字段信息。类似的,在gson调用fromJson时,会向fromJson方法传递一个MyClass.class对象。该方法在序列化和反序列化类型是非泛型类型时能够正常运行。
|
||||
但是,当待序列化和反序列化的类型是泛型类型时,在序列化和反序列化对象时泛型类型信息会丢失,因为泛型采用的是类型擦除。
|
||||
```java
|
||||
class Foo<T> {
|
||||
T value;
|
||||
}
|
||||
Gson gson = new Gson();
|
||||
Foo<Bar> foo = new Foo<Bar>();
|
||||
gson.toJson(foo); // May not serialize foo.value correctly
|
||||
|
||||
gson.fromJson(json, foo.getClass()); // Fails to deserialize foo.value as Bar
|
||||
```
|
||||
> 上述代码中,foo.getClass()方法返回的只是Foo.class对象,并不会包含泛型类型Bar的信息,故而在反序列化时gson并不知道应该将value反序列化为Bar类型
|
||||
|
||||
可以通过向fromJson中传入Type参数来详细指定想要将json串转化成的泛型类型信息
|
||||
```java
|
||||
Type fooType = new TypeToken<Foo<Bar>>() {}.getType();
|
||||
gson.toJson(foo, fooType);
|
||||
|
||||
gson.fromJson(json, fooType);
|
||||
```
|
||||
### 序列化和反序列化集合,集合中保存任意类型的对象
|
||||
当JSON串中数组包含各种类型元素时,将其转化为包含任意类型的java集合,可以有如下方法:
|
||||
- 使用Gson Parser API(JsonParser,底层parser api)将json串中数组转化为JsonArray,并且为每个元素调用Gson.fromJson。该方法是推荐的方法
|
||||
> gson.fromJson可以针对String、Reader、JsonElement来调用
|
||||
|
||||
|
||||
- [gson](#gson)
|
||||
- [gson简介](#gson简介)
|
||||
- [gson使用](#gson使用)
|
||||
- [Gson库通过Maven引入](#gson库通过maven引入)
|
||||
- [基本类型的序列化和反序列化](#基本类型的序列化和反序列化)
|
||||
- [对象的序列化和反序列化](#对象的序列化和反序列化)
|
||||
- [gson和对象联用的使用规范](#gson和对象联用的使用规范)
|
||||
- [gson和嵌套类的关联使用](#gson和嵌套类的关联使用)
|
||||
- [gson和数组的关联使用](#gson和数组的关联使用)
|
||||
- [gson对java中的集合进行序列化和反序列化](#gson对java中的集合进行序列化和反序列化)
|
||||
- [gson对Map类型的序列化和反序列化](#gson对map类型的序列化和反序列化)
|
||||
- [序列化和反序列化泛型对象](#序列化和反序列化泛型对象)
|
||||
- [序列化和反序列化集合,集合中保存任意类型的对象](#序列化和反序列化集合集合中保存任意类型的对象)
|
||||
|
||||
# gson
|
||||
## gson简介
|
||||
gson是一个java库,通常用来将java对象转化为其json表示的字符串,或者将json格式的字符串转化为其等价的java对象。
|
||||
## gson使用
|
||||
在gson中,使用最频繁的类是Gson。可以通过new Gson()构造函数来创建Gson对象,也可以通过GsonBuilder来创建Gson对象,GsonBuidler在创建Gson对象时能够指定一些设置,如版本控制等。
|
||||
由于Gson对象在执行json操作时并不会保存任何状态,故而Gson对象是线程安全的,单一的Gson对象可以在多线程环境下被重复使用。
|
||||
### Gson库通过Maven引入
|
||||
```xml
|
||||
<dependencies>
|
||||
<!-- Gson: Java to JSON conversion -->
|
||||
<dependency>
|
||||
<groupId>com.google.code.gson</groupId>
|
||||
<artifactId>gson</artifactId>
|
||||
<version>2.9.1</version>
|
||||
<scope>compile</scope>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
```
|
||||
### 基本类型的序列化和反序列化
|
||||
```java
|
||||
// Serialization
|
||||
Gson gson = new Gson();
|
||||
gson.toJson(1); // ==> 1
|
||||
gson.toJson("abcd"); // ==> "abcd"
|
||||
gson.toJson(new Long(10)); // ==> 10
|
||||
int[] values = { 1 };
|
||||
gson.toJson(values); // ==> [1]
|
||||
|
||||
// Deserialization
|
||||
int one = gson.fromJson("1", int.class);
|
||||
Integer one = gson.fromJson("1", Integer.class);
|
||||
Long one = gson.fromJson("1", Long.class);
|
||||
Boolean false = gson.fromJson("false", Boolean.class);
|
||||
String str = gson.fromJson("\"abc\"", String.class);
|
||||
String[] anotherStr = gson.fromJson("[\"abc\"]", String[].class);
|
||||
```
|
||||
### 对象的序列化和反序列化
|
||||
类似于java自带的序列化机制,当成员字段被transient修饰时,并不会序列化该字段
|
||||
```java
|
||||
class BagOfPrimitives {
|
||||
private int value1 = 1;
|
||||
private String value2 = "abc";
|
||||
private transient int value3 = 3;
|
||||
BagOfPrimitives() {
|
||||
// no-args constructor
|
||||
}
|
||||
}
|
||||
|
||||
// Serialization
|
||||
BagOfPrimitives obj = new BagOfPrimitives();
|
||||
Gson gson = new Gson();
|
||||
String json = gson.toJson(obj);
|
||||
|
||||
// ==> json is {"value1":1,"value2":"abc"}
|
||||
|
||||
// Deserialization
|
||||
BagOfPrimitives obj2 = gson.fromJson(json, BagOfPrimitives.class);
|
||||
// ==> obj2 is just like obj
|
||||
```
|
||||
### gson和对象联用的使用规范
|
||||
- gson使用过程中,待序列化或反序列化的成员字段可以是private的,同时也推荐将待序列化或反序列化的成员字段声明为private
|
||||
- 没有必要对成员字段使用注解来特定标明在序列化或者反序列化中包含该字段,默认情况下该对象所有成员字段和该对象父类对象所包含的所有字段都会被序列化
|
||||
- 类似于jdk自带的序列化和反序列化机制,如果一个字段被标明为transient,该字段将不会被包含到序列化和反序列化过程中
|
||||
- gson实现能够正确处理字段为空的情况
|
||||
- 当序列化过程中,为null的字段将会被省略
|
||||
- 当反序列化过程中,如果一个字段在json串中并没有被设置,反序列化得到的对象中该字段将会被设置为默认值:引用类型默认值为null、数字类型默认值为0,boolean类型其默认值为false
|
||||
- 在内部类、匿名类、本地类中关联外部类的字段将会被忽略,并不会包含在序列化和反序列化过程中
|
||||
### gson和嵌套类的关联使用
|
||||
gson可以单独的对静态内部类进行序列化和反序列化,因为静态内部类并不包含对外部类的引用;但是gson无法单独的反序列化内部类,因为在反序列化内部类的过程中,其无参构造器需要一个指向其外部类对象的引用,但是该外部类对象在对内部类进行反序列化时是不可访问的。
|
||||
可以通过将内部类改为静态的内部类,此时对内部类的反序列化将不会需要指向外部类对象的引用。
|
||||
### gson和数组的关联使用
|
||||
gson支持多维数组,并且支持任意的复杂元素类型
|
||||
```java
|
||||
Gson gson = new Gson();
|
||||
int[] ints = {1, 2, 3, 4, 5};
|
||||
String[] strings = {"abc", "def", "ghi"};
|
||||
|
||||
// Serialization
|
||||
gson.toJson(ints); // ==> [1,2,3,4,5]
|
||||
gson.toJson(strings); // ==> ["abc", "def", "ghi"]
|
||||
|
||||
// Deserialization
|
||||
int[] ints2 = gson.fromJson("[1,2,3,4,5]", int[].class);
|
||||
// ==> ints2 will be same as ints
|
||||
```
|
||||
### gson对java中的集合进行序列化和反序列化
|
||||
gson可以序列化任意对象的集合,但是无法对其进行反序列化,因为在反序列化时用户没有任何方法去指定其生成的集合中元素的类型。因而,需要通过typeToken来告知Gson需要反序列化的类型。
|
||||
```java
|
||||
Gson gson = new Gson();
|
||||
Collection<Integer> ints = Arrays.asList(1,2,3,4,5);
|
||||
|
||||
// Serialization
|
||||
String json = gson.toJson(ints); // ==> json is [1,2,3,4,5]
|
||||
|
||||
// Deserialization
|
||||
Type collectionType = new TypeToken<Collection<Integer>>(){}.getType();
|
||||
Collection<Integer> ints2 = gson.fromJson(json, collectionType);
|
||||
// ==> ints2 is same as ints
|
||||
```
|
||||
### gson对Map类型的序列化和反序列化
|
||||
默认情况下,gson会将java中任意的Map实现类型序列化为JSON对象。由于JSON对象其key只支持字符串类型,gson会将待序列化的Map key调用toString转化为字符串。如果map中的key为null,则序列化后的key为"null"
|
||||
```java
|
||||
/**
|
||||
* gson对map进行序列化
|
||||
**/
|
||||
Gson gson = new Gson();
|
||||
Map<String, String> stringMap = new LinkedHashMap<>();
|
||||
stringMap.put("key", "value");
|
||||
stringMap.put(null, "null-entry");
|
||||
|
||||
// Serialization
|
||||
String json = gson.toJson(stringMap); // ==> json is {"key":"value","null":"null-entry"}
|
||||
|
||||
Map<Integer, Integer> intMap = new LinkedHashMap<>();
|
||||
intMap.put(2, 4);
|
||||
intMap.put(3, 6);
|
||||
|
||||
// Serialization
|
||||
String json = gson.toJson(intMap); // ==> json is {"2":4,"3":6}
|
||||
```
|
||||
在反序列化的过程中,gson会使用为Map key类型注册的TypeAdapter的read方法来进行反序列化。为了让gson知道反序列化得到的Map对象的key和value类型,需要使用TypeToken。
|
||||
```java
|
||||
Gson gson = new Gson();
|
||||
Type mapType = new TypeToken<Map<String, String>>(){}.getType();
|
||||
String json = "{\"key\": \"value\"}";
|
||||
|
||||
// Deserialization
|
||||
Map<String, String> stringMap = gson.fromJson(json, mapType);
|
||||
// ==> stringMap is {key=value}
|
||||
```
|
||||
默认情况下,gson序列化map时,复杂类型的key会调用toString方法来将其转化成字符串。但是,gson同样支持开启复杂类型key的序列化操作。通过Gson.enableComplexMapKeySerialization()方法来开启,Gson会调用为Map的key类型注册的TypeAdapter的write方法来序列化key,而不是通过toString方法将key转化为字符串。
|
||||
> 当Map中任意一条Entry的key通过Adapter被序列化为了JSON数组或者对象,那么Gson会将整个Map序列化为Json数组,数组元素为map中entry的键值对。如果map中所有的entry key都不会被序列化为json object或json array,那么该map将会被序列化为json对象
|
||||
|
||||
> 在对枚举型key进行反序列化的过程中,如果enum找不到一个具有匹配name()值的常量时,其会采用一个回退机制,根据枚举常量的toString()值来进行反序列化的匹配。
|
||||
|
||||
### 序列化和反序列化泛型对象
|
||||
当gson对object对象调用toJson方法时,gson会调用object.getClass()来获取需要序列化的字段信息。类似的,在gson调用fromJson时,会向fromJson方法传递一个MyClass.class对象。该方法在序列化和反序列化类型是非泛型类型时能够正常运行。
|
||||
但是,当待序列化和反序列化的类型是泛型类型时,在序列化和反序列化对象时泛型类型信息会丢失,因为泛型采用的是类型擦除。
|
||||
```java
|
||||
class Foo<T> {
|
||||
T value;
|
||||
}
|
||||
Gson gson = new Gson();
|
||||
Foo<Bar> foo = new Foo<Bar>();
|
||||
gson.toJson(foo); // May not serialize foo.value correctly
|
||||
|
||||
gson.fromJson(json, foo.getClass()); // Fails to deserialize foo.value as Bar
|
||||
```
|
||||
> 上述代码中,foo.getClass()方法返回的只是Foo.class对象,并不会包含泛型类型Bar的信息,故而在反序列化时gson并不知道应该将value反序列化为Bar类型
|
||||
|
||||
可以通过向fromJson中传入Type参数来详细指定想要将json串转化成的泛型类型信息
|
||||
```java
|
||||
Type fooType = new TypeToken<Foo<Bar>>() {}.getType();
|
||||
gson.toJson(foo, fooType);
|
||||
|
||||
gson.fromJson(json, fooType);
|
||||
```
|
||||
### 序列化和反序列化集合,集合中保存任意类型的对象
|
||||
当JSON串中数组包含各种类型元素时,将其转化为包含任意类型的java集合,可以有如下方法:
|
||||
- 使用Gson Parser API(JsonParser,底层parser api)将json串中数组转化为JsonArray,并且为每个元素调用Gson.fromJson。该方法是推荐的方法
|
||||
> gson.fromJson可以针对String、Reader、JsonElement来调用
|
||||
|
||||
|
||||
|
||||
@@ -1,85 +1,85 @@
|
||||
- [Spring Logging](#spring-logging)
|
||||
- [Log Format](#log-format)
|
||||
- [控制台输出](#控制台输出)
|
||||
- [文件输出](#文件输出)
|
||||
- [File Rotation](#file-rotation)
|
||||
- [Log Level](#log-level)
|
||||
- [Log Group](#log-group)
|
||||
|
||||
# Spring Logging
|
||||
## Log Format
|
||||
默认Spring Boot输出日志的格式如下
|
||||
```console
|
||||
2022-08-18 05:33:51.660 INFO 16378 --- [ main] o.s.b.d.f.s.MyApplication : Starting MyApplication using Java 1.8.0_345 on myhost with PID 16378 (/opt/apps/myapp.jar started by myuser in /opt/apps/)
|
||||
2022-08-18 05:33:51.664 INFO 16378 --- [ main] o.s.b.d.f.s.MyApplication : No active profile set, falling back to 1 default profile: "default"
|
||||
2022-08-18 05:33:53.907 INFO 16378 --- [ main] o.s.b.w.embedded.tomcat.TomcatWebServer : Tomcat initialized with port(s): 8080 (http)
|
||||
2022-08-18 05:33:53.939 INFO 16378 --- [ main] o.apache.catalina.core.StandardService : Starting service [Tomcat]
|
||||
2022-08-18 05:33:53.939 INFO 16378 --- [ main] org.apache.catalina.core.StandardEngine : Starting Servlet engine: [Apache Tomcat/9.0.65]
|
||||
2022-08-18 05:33:54.217 INFO 16378 --- [ main] o.a.c.c.C.[Tomcat].[localhost].[/] : Initializing Spring embedded WebApplicationContext
|
||||
2022-08-18 05:33:54.217 INFO 16378 --- [ main] w.s.c.ServletWebServerApplicationContext : Root WebApplicationContext: initialization completed in 2343 ms
|
||||
2022-08-18 05:33:55.396 INFO 16378 --- [ main] o.s.b.w.embedded.tomcat.TomcatWebServer : Tomcat started on port(s): 8080 (http) with context path ''
|
||||
2022-08-18 05:33:55.640 INFO 16378 --- [ main] o.s.b.d.f.s.MyApplication : Started MyApplication in 5.456 seconds (JVM running for 6.299)
|
||||
```
|
||||
- 日期与时间 : 精度为ms
|
||||
- log level : ERROR, WARN, INFO, DEBUG, TRACE
|
||||
- 进程ID
|
||||
- 线程名称 : [main]
|
||||
- logger name : 输出日志类的类名(通常为缩写)
|
||||
- log信息
|
||||
## 控制台输出
|
||||
默认情况下,log日志信息会回显输出到console中,默认ERROR, WARN, DEBUG三个级别的信息将会被日志输出。
|
||||
可以通过--debug选项来启用“debug”模式
|
||||
```shell
|
||||
java -jar xxx.jar --debug
|
||||
```
|
||||
通过在application.properties中指定debug=true也可以开启“debug”模式
|
||||
```properties
|
||||
debug=true
|
||||
```
|
||||
当“debug”模式被开启后,一部分核心的logger(内嵌容器、Hibernate、SpringBoot)将会被配置输出更多的信息。
|
||||
> ***开启debug模式并不意味着输出所有日志级别为Debug的信息***
|
||||
|
||||
> ***同样,也可以通过--trace或者在properties中指定trace=true来开启trace模式***
|
||||
|
||||
## 文件输出
|
||||
默认情况下,Spring Boot只会将日志输出到console中,如果想要额外定义将日志输出到文件中,需要在application.properties中定义logging.file.name或者logging.file.path
|
||||
| logging.file.name | logging.file.path | example | description |
|
||||
| :-: | :-: | :-: | :-: |
|
||||
| (none) | (none) | | 只在控制台输出 |
|
||||
| 特定文件 | (none) | my.log | 特定log文件路径,可以是绝对路径或相对路径) |
|
||||
| (none) | 特定目录 | /var/log | 将日志输出到该路径下的spring.log文件,可以是绝对路径或相对路径|
|
||||
> 当log文件大小到达10MB时将会旋转重写,和console log一样,log文件也会输出ERROR, WARN和INFO
|
||||
|
||||
> ***logging properties和实际的logging机制是相互独立的,因而,特定的配置属性(如logback.configurationFile)并不由SpringBoot管理***
|
||||
|
||||
## File Rotation
|
||||
如果使用的是Logback,则可以在application.properties中定义file rotation行为。
|
||||
| Name | Description |
|
||||
| :-: | :-: |
|
||||
| logging.logback.rollingpolicy.file-name-pattern | 定义创建log归档的命名模式 |
|
||||
| logging.logback.rollingpolicy.clean-history-on-start | 定义是否应该在项目启动时清理历史日志 |
|
||||
| logging.logback.rollingpolicy.max-file-size | 定义日志在归档前的最大大小 |
|
||||
| logging.logback.rollingpolicy.total-size-cap | 日志归档在被删除前可以占用的最大大小 |
|
||||
| logging.logback.rollingpolicy.max-history | 要保留归档日志文件的最大数量 |
|
||||
|
||||
## Log Level
|
||||
所有的日志系统都可以通过Application.properties定义logging.level.<logger-name>=<level>来定义事务级别,事务级别可以是TRACE, DEBUG, INFO, WARN, ERROR, FATAL, OFF。
|
||||
可以通过logging.level.root来定义root logger的隔离级别。
|
||||
```properties
|
||||
logging.level.root=warn
|
||||
logging.level.org.springframework.web=debug
|
||||
logging.level.org.hibernate=error
|
||||
```
|
||||
## Log Group
|
||||
可以通过log group将关联的logger组合在一起,并且对log group统一指定日志级别。
|
||||
```properties
|
||||
# 定义一个名为“tomcat”的log group
|
||||
logging.group.tomcat=org.apache.catalina,org.apache.coyote,org.apache.tomcat
|
||||
|
||||
# 为名为“tomcat”的log group统一指定log level
|
||||
logging.level.tomcat=trace
|
||||
```
|
||||
> Spring Boot具有如下先定义好的log group,可以开箱即用
|
||||
> - web : org.springframework.core.codec, org.springframework.http, org.springframework.web, org.springframework.boot.actuate.endpoint.web, org.springframework.boot.web.servlet.ServletContextInitializerBeans
|
||||
> - sql : org.springframework.jdbc.core, org.hibernate.SQL, org.jooq.tools.LoggerListener
|
||||
|
||||
- [Spring Logging](#spring-logging)
|
||||
- [Log Format](#log-format)
|
||||
- [控制台输出](#控制台输出)
|
||||
- [文件输出](#文件输出)
|
||||
- [File Rotation](#file-rotation)
|
||||
- [Log Level](#log-level)
|
||||
- [Log Group](#log-group)
|
||||
|
||||
# Spring Logging
|
||||
## Log Format
|
||||
默认Spring Boot输出日志的格式如下
|
||||
```console
|
||||
2022-08-18 05:33:51.660 INFO 16378 --- [ main] o.s.b.d.f.s.MyApplication : Starting MyApplication using Java 1.8.0_345 on myhost with PID 16378 (/opt/apps/myapp.jar started by myuser in /opt/apps/)
|
||||
2022-08-18 05:33:51.664 INFO 16378 --- [ main] o.s.b.d.f.s.MyApplication : No active profile set, falling back to 1 default profile: "default"
|
||||
2022-08-18 05:33:53.907 INFO 16378 --- [ main] o.s.b.w.embedded.tomcat.TomcatWebServer : Tomcat initialized with port(s): 8080 (http)
|
||||
2022-08-18 05:33:53.939 INFO 16378 --- [ main] o.apache.catalina.core.StandardService : Starting service [Tomcat]
|
||||
2022-08-18 05:33:53.939 INFO 16378 --- [ main] org.apache.catalina.core.StandardEngine : Starting Servlet engine: [Apache Tomcat/9.0.65]
|
||||
2022-08-18 05:33:54.217 INFO 16378 --- [ main] o.a.c.c.C.[Tomcat].[localhost].[/] : Initializing Spring embedded WebApplicationContext
|
||||
2022-08-18 05:33:54.217 INFO 16378 --- [ main] w.s.c.ServletWebServerApplicationContext : Root WebApplicationContext: initialization completed in 2343 ms
|
||||
2022-08-18 05:33:55.396 INFO 16378 --- [ main] o.s.b.w.embedded.tomcat.TomcatWebServer : Tomcat started on port(s): 8080 (http) with context path ''
|
||||
2022-08-18 05:33:55.640 INFO 16378 --- [ main] o.s.b.d.f.s.MyApplication : Started MyApplication in 5.456 seconds (JVM running for 6.299)
|
||||
```
|
||||
- 日期与时间 : 精度为ms
|
||||
- log level : ERROR, WARN, INFO, DEBUG, TRACE
|
||||
- 进程ID
|
||||
- 线程名称 : [main]
|
||||
- logger name : 输出日志类的类名(通常为缩写)
|
||||
- log信息
|
||||
## 控制台输出
|
||||
默认情况下,log日志信息会回显输出到console中,默认ERROR, WARN, DEBUG三个级别的信息将会被日志输出。
|
||||
可以通过--debug选项来启用“debug”模式
|
||||
```shell
|
||||
java -jar xxx.jar --debug
|
||||
```
|
||||
通过在application.properties中指定debug=true也可以开启“debug”模式
|
||||
```properties
|
||||
debug=true
|
||||
```
|
||||
当“debug”模式被开启后,一部分核心的logger(内嵌容器、Hibernate、SpringBoot)将会被配置输出更多的信息。
|
||||
> ***开启debug模式并不意味着输出所有日志级别为Debug的信息***
|
||||
|
||||
> ***同样,也可以通过--trace或者在properties中指定trace=true来开启trace模式***
|
||||
|
||||
## 文件输出
|
||||
默认情况下,Spring Boot只会将日志输出到console中,如果想要额外定义将日志输出到文件中,需要在application.properties中定义logging.file.name或者logging.file.path
|
||||
| logging.file.name | logging.file.path | example | description |
|
||||
| :-: | :-: | :-: | :-: |
|
||||
| (none) | (none) | | 只在控制台输出 |
|
||||
| 特定文件 | (none) | my.log | 特定log文件路径,可以是绝对路径或相对路径) |
|
||||
| (none) | 特定目录 | /var/log | 将日志输出到该路径下的spring.log文件,可以是绝对路径或相对路径|
|
||||
> 当log文件大小到达10MB时将会旋转重写,和console log一样,log文件也会输出ERROR, WARN和INFO
|
||||
|
||||
> ***logging properties和实际的logging机制是相互独立的,因而,特定的配置属性(如logback.configurationFile)并不由SpringBoot管理***
|
||||
|
||||
## File Rotation
|
||||
如果使用的是Logback,则可以在application.properties中定义file rotation行为。
|
||||
| Name | Description |
|
||||
| :-: | :-: |
|
||||
| logging.logback.rollingpolicy.file-name-pattern | 定义创建log归档的命名模式 |
|
||||
| logging.logback.rollingpolicy.clean-history-on-start | 定义是否应该在项目启动时清理历史日志 |
|
||||
| logging.logback.rollingpolicy.max-file-size | 定义日志在归档前的最大大小 |
|
||||
| logging.logback.rollingpolicy.total-size-cap | 日志归档在被删除前可以占用的最大大小 |
|
||||
| logging.logback.rollingpolicy.max-history | 要保留归档日志文件的最大数量 |
|
||||
|
||||
## Log Level
|
||||
所有的日志系统都可以通过Application.properties定义logging.level.<logger-name>=<level>来定义事务级别,事务级别可以是TRACE, DEBUG, INFO, WARN, ERROR, FATAL, OFF。
|
||||
可以通过logging.level.root来定义root logger的隔离级别。
|
||||
```properties
|
||||
logging.level.root=warn
|
||||
logging.level.org.springframework.web=debug
|
||||
logging.level.org.hibernate=error
|
||||
```
|
||||
## Log Group
|
||||
可以通过log group将关联的logger组合在一起,并且对log group统一指定日志级别。
|
||||
```properties
|
||||
# 定义一个名为“tomcat”的log group
|
||||
logging.group.tomcat=org.apache.catalina,org.apache.coyote,org.apache.tomcat
|
||||
|
||||
# 为名为“tomcat”的log group统一指定log level
|
||||
logging.level.tomcat=trace
|
||||
```
|
||||
> Spring Boot具有如下先定义好的log group,可以开箱即用
|
||||
> - web : org.springframework.core.codec, org.springframework.http, org.springframework.web, org.springframework.boot.actuate.endpoint.web, org.springframework.boot.web.servlet.ServletContextInitializerBeans
|
||||
> - sql : org.springframework.jdbc.core, org.hibernate.SQL, org.jooq.tools.LoggerListener
|
||||
|
||||
|
||||
@@ -1,13 +1,13 @@
|
||||
# lombok标签简介
|
||||
## @NonNull
|
||||
- 如果用@NonNull标签来修饰一个参数,在方法或构造器的开头会插入一个空校验来检查该参数是否为空。
|
||||
- 如果将@NonNull标签用来修饰一个field,任何通过注解产生的方法(如@Setter产生的Setter)都会在试图分配该field一个值时进行空校验
|
||||
## @RequiredArgsConstructor
|
||||
被该注解修饰的类,会产生一个包含required args的构造器。required args包含final field和被特殊约束的field(如被@NonNull约束)
|
||||
## @ToString
|
||||
产生一个toString方法的实现,并且该实现会被所有对象继承
|
||||
## @Data
|
||||
@Data是一个快捷的注解,其将@ToString,@EqualsAndHashCode,@Getter/@Setter,@RequiredArgsConstructor等注解整合到了一起
|
||||
- 对于@Data注解标注的类,如果类中包含一个方法,其方法名和@Data将要产生的方法相同并且参数个数也相同(不需要参数类型相同),那么该方法将不会被产生,并且不会产生任何警告或错误
|
||||
- 对于@Data标注的类,如果该类显示声明了一个构造器,那么@Data不会再生成任何构造器
|
||||
# lombok标签简介
|
||||
## @NonNull
|
||||
- 如果用@NonNull标签来修饰一个参数,在方法或构造器的开头会插入一个空校验来检查该参数是否为空。
|
||||
- 如果将@NonNull标签用来修饰一个field,任何通过注解产生的方法(如@Setter产生的Setter)都会在试图分配该field一个值时进行空校验
|
||||
## @RequiredArgsConstructor
|
||||
被该注解修饰的类,会产生一个包含required args的构造器。required args包含final field和被特殊约束的field(如被@NonNull约束)
|
||||
## @ToString
|
||||
产生一个toString方法的实现,并且该实现会被所有对象继承
|
||||
## @Data
|
||||
@Data是一个快捷的注解,其将@ToString,@EqualsAndHashCode,@Getter/@Setter,@RequiredArgsConstructor等注解整合到了一起
|
||||
- 对于@Data注解标注的类,如果类中包含一个方法,其方法名和@Data将要产生的方法相同并且参数个数也相同(不需要参数类型相同),那么该方法将不会被产生,并且不会产生任何警告或错误
|
||||
- 对于@Data标注的类,如果该类显示声明了一个构造器,那么@Data不会再生成任何构造器
|
||||
- 可以通过为@Data标注类中的方法添加@lombok.experimental.Tolerate来为lombok隐藏这些方法
|
||||
@@ -1,238 +1,238 @@
|
||||
- [Spring Boot Async](#spring-boot-async)
|
||||
- [Spring Executor和Scheduler的自动配置](#spring-executor和scheduler的自动配置)
|
||||
- [Task Execution and Scheduling](#task-execution-and-scheduling)
|
||||
- [Task Execution Abstraction](#task-execution-abstraction)
|
||||
- [TaskExecutor接口的实现种类](#taskexecutor接口的实现种类)
|
||||
- [TaskExecutor的使用](#taskexecutor的使用)
|
||||
- [Spring TaskScheduler Abstraction](#spring-taskscheduler-abstraction)
|
||||
- [Trigger接口](#trigger接口)
|
||||
- [Trigger实现类](#trigger实现类)
|
||||
- [TaskScheduler实现类](#taskscheduler实现类)
|
||||
- [对任务调度和异步执行的注解支持](#对任务调度和异步执行的注解支持)
|
||||
- [启用Scheduling注解](#启用scheduling注解)
|
||||
- [@Scheduled注解](#scheduled注解)
|
||||
- [@Async注解](#async注解)
|
||||
- [@Async方法的异常处理](#async方法的异常处理)
|
||||
- [Cron表达式](#cron表达式)
|
||||
- [Macros(宏)](#macros宏)
|
||||
|
||||
|
||||
# Spring Boot Async
|
||||
## Spring Executor和Scheduler的自动配置
|
||||
当当前上下文中没有Executor类型的bean对象时,spring boot会自动配置一个ThreadPoolTaskExecutor类型的bean对象,并且将该bean对象和异步task执行(@EnableAsync)和spring mvc异步请求处理关联在一起。
|
||||
该默认创建的ThreadPoolTaskExecutor默认使用8个核心线程,并且线程数量可以根据负载动态的增加或者减少。
|
||||
可以通过如下方式对ThreadPoolTaskExecutor进行配置:
|
||||
```properties
|
||||
# 该线程池最多含有16个线程
|
||||
spring.task.execution.pool.max-size=16
|
||||
# 有有界队列存放task,上限为100
|
||||
spring.task.execution.pool.queue-capacity=100
|
||||
# 当线程空闲10s(默认60s)时会进行回收
|
||||
spring.task.execution.pool.keep-alive=10s
|
||||
# 设置核心线程数量
|
||||
spring.task.execution.pool.core-size=8
|
||||
```
|
||||
如果使用@EnableScheduling,一个ThreadPoolTaskScheduler也可以被配置。该线程池默认使用一个线程,但是也可以动态设置:
|
||||
```properties
|
||||
spring.task.scheduling.thread-name-prefix=scheduling-
|
||||
spring.task.scheduling.pool.size=2
|
||||
```
|
||||
## Task Execution and Scheduling
|
||||
Spring通过TaskExecutor和TaskScheduler接口为task的异步执行和调度提供了抽象。
|
||||
### Task Execution Abstraction
|
||||
Executor对应的是JDK中线程池的概念。在Spring中,TaskExecutor接口和java.util.concurrent.Executor接口相同,该接口中只有一个execute方法(execute(Runnable task)),接受一个task。
|
||||
### TaskExecutor接口的实现种类
|
||||
Spring中包含许多预制的TaskExecutor实现类,该实现类如下:
|
||||
- SyncTaskExecutor:该实现类不会执行异步的调用,所有任务的执行都会发生在调用该executor的线程中。(通常使用在不需要多线程的场景)
|
||||
- SimpleAsyncTaskExecutor:该实现类不会复用任何的线程,相反的,对于每次调用该executor,都会使用一个全新的线程。但是,该实现类的确支持对并发数量的限制,该executor会阻塞任何超过并发数量限制的调用,直到slot被释放为止。(SimpleAsyncTaskExecutor并不支持池化技术)
|
||||
- ConcurrentTaskExecutor:该实现类是java.util.concurrent.Executor实例的adapter,其可以将java.util.concurrent.Executor实例的配置参数以bean properties的形式暴露。(当ThreadPoolTaskExecutor的灵活性无法满足需求时,可以使用ConcurrentTaskExecutor)
|
||||
- ThreadPoolTaskExecutor:该实现类型是最广泛被使用的。该实现类可以通过bean properties来配置java.util.concurrent.ThreadPoolExecutor实例,并且将该实例wrap在TaskExecutor实例中。当想要使用另一种java.util.concurrent.Executor时,可以使用ConcurrentTaskExecutor。
|
||||
- DefaultManagedTaskExecutor:该实现使用了通过JNDI获取的ManagedExecutorService
|
||||
|
||||
### TaskExecutor的使用
|
||||
在springboot中,当使用@EnableAsync时,可以通过配置ThreadPoolExecutor的bean properties来配置线程池的核心线程数和最大线程数等属性。
|
||||
```properties
|
||||
# 该线程池最多含有16个线程
|
||||
spring.task.execution.pool.max-size=16
|
||||
# 有有界队列存放task,上限为100
|
||||
spring.task.execution.pool.queue-capacity=100
|
||||
# 当线程空闲10s(默认60s)时会进行回收
|
||||
spring.task.execution.pool.keep-alive=10s
|
||||
# 设置核心线程数量
|
||||
spring.task.execution.pool.core-size=8
|
||||
```
|
||||
|
||||
## Spring TaskScheduler Abstraction
|
||||
为了在特定的时间点执行task,Spring引入了TaskScheduler接口,接口定义如下:
|
||||
```java
|
||||
public interface TaskScheduler {
|
||||
|
||||
ScheduledFuture schedule(Runnable task, Trigger trigger);
|
||||
|
||||
ScheduledFuture schedule(Runnable task, Instant startTime);
|
||||
|
||||
ScheduledFuture scheduleAtFixedRate(Runnable task, Instant startTime, Duration period);
|
||||
|
||||
ScheduledFuture scheduleAtFixedRate(Runnable task, Duration period);
|
||||
|
||||
ScheduledFuture scheduleWithFixedDelay(Runnable task, Instant startTime, Duration delay);
|
||||
|
||||
ScheduledFuture scheduleWithFixedDelay(Runnable task, Duration delay);
|
||||
```
|
||||
> scheduleAtFixedRate和scheduleAtFixedDelay区别:
|
||||
> - fixed rate:表示两个task执行开始时间的间隔
|
||||
> - fixed delay:表示上一个task结束时间和下一个task开始时间的间隔
|
||||
>
|
||||
> 实例如下:
|
||||
> - fixed rate:TTWWWTTTWWT...(开始时间间隔为5)
|
||||
> - fixed delay:TTWWWWWTTTTWWWWWTTTTTTTWWWWWT...(上次结束和下次开始之间的间隔为5)
|
||||
>
|
||||
> **通常,TaskScheduler默认情况下是单线程执行的,故而fixed rate执行时,如果一个Task执行时间超过period时,在当前task执行完成之前,下一个task并不会开始执行。下一个task会等待当前task执行完成之后立马执行。**
|
||||
|
||||
### Trigger接口
|
||||
Trigger接口的核心理念是下次执行事件由上次执行的结果决定。上次执行的结果存储在TriggerContext中。
|
||||
Trigger接口如下:
|
||||
```java
|
||||
public interface Trigger {
|
||||
|
||||
Date nextExecutionTime(TriggerContext triggerContext);
|
||||
}
|
||||
```
|
||||
TriggerContext接口如下:(其具有默认的实现类SimpleTriggerContext)
|
||||
```java
|
||||
public interface TriggerContext {
|
||||
|
||||
Date lastScheduledExecutionTime();
|
||||
|
||||
Date lastActualExecutionTime();
|
||||
|
||||
Date lastCompletionTime();
|
||||
}
|
||||
```
|
||||
### Trigger实现类
|
||||
Spring为Trigger提供了两个实现类,其中CronTrigger允许task的调度按照cron expression来执行(类似linux中的crond)。
|
||||
```java
|
||||
scheduler.schedule(task, new CronTrigger("0 15 9-17 * * MON-FRI"));
|
||||
```
|
||||
Spring的另一个Trigger实现是PeriodicTrigger,其接受一个固定的period期间,一个可选的初始delay值,并接收一个boolean值标识该period是fixed-rate还是fixed-delay。
|
||||
### TaskScheduler实现类
|
||||
如果不需要外部的线程管理,可以使用spring提供的ThreadPoolTaskScheduler,其会将任务委托给ScheduledExecutorService来提供bean-properties形式的配置。
|
||||
|
||||
## 对任务调度和异步执行的注解支持
|
||||
Spring同时对任务调度和异步方法的执行提供注解支持。
|
||||
### 启用Scheduling注解
|
||||
想要启用@Async和@Scheduled注解,必须将@EnableAsync注解和@EnableScheduling注解添加到一个@Configuration类上。
|
||||
```java
|
||||
@Configuration
|
||||
@EnableAsync
|
||||
@EnableScheduling
|
||||
public class AppConfig {
|
||||
}
|
||||
```
|
||||
> **@Async注解的实现是通过proxy模式来实现的,故而如果在类内部调用位于同一个类中的@Async方法,那么代理拦截会失效,此时调用的@Async方法将会同步执行而非异步执行**
|
||||
|
||||
> @Async可以接收一个value值,用于指定目标的执行器(Executor或TaskExecutor的beanName)
|
||||
|
||||
### @Scheduled注解
|
||||
在将@Scheduled注解标注到方法时,可以为其指定一个trigger的元数据,使用示例如下:
|
||||
```java
|
||||
@Scheduled(fixedDelay = 5000)
|
||||
public void doSomething() {
|
||||
// something that should run periodically
|
||||
}
|
||||
```
|
||||
> 默认情况下,fixedDelay、fixedRate、initialDelay的时间单位都是ms,可以指定timeUnit来指定其他的时间单位:
|
||||
> ```java
|
||||
> @Scheduled(fixedDelay = 5, timeUnit = TimeUnit.SECONDS)
|
||||
> public void doSomething() {
|
||||
> // something that should run periodically
|
||||
> }
|
||||
> ```
|
||||
|
||||
可以为@Scheduled注解使用cron表达式
|
||||
```java
|
||||
@Scheduled(cron="*/5 * * * * MON-FRI")
|
||||
public void doSomething() {
|
||||
// something that should run on weekdays only
|
||||
}
|
||||
```
|
||||
|
||||
### @Async注解
|
||||
通过为方法指定@Async注解,可以让该方法异步执行,该方法的执行通过TaskExecutor。
|
||||
```java
|
||||
@Async
|
||||
void doSomething() {
|
||||
// this will be run asynchronously
|
||||
}
|
||||
```
|
||||
可以为@Async标注的方法指定参数和返回值,但是异步方法的返回值只能为void或是Future类型。
|
||||
在该异步方法的调用方调用返回Future实例的get方法之前,调用方仍然能够执行其他操作,异步方法的执行位于TaskExecutor的线程中。
|
||||
```java
|
||||
@Async
|
||||
Future<String> returnSomething(int i) {
|
||||
// this will be run asynchronously
|
||||
}
|
||||
```
|
||||
不能将@Async注解和生命周期回调(例如@PostConstruct)进行混用,如果想要异步的初始化bean对象,需要按照如下方法:
|
||||
```java
|
||||
public class SampleBeanImpl implements SampleBean {
|
||||
|
||||
@Async
|
||||
void doSomething() {
|
||||
// ...
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
public class SampleBeanInitializer {
|
||||
|
||||
private final SampleBean bean;
|
||||
|
||||
public SampleBeanInitializer(SampleBean bean) {
|
||||
this.bean = bean;
|
||||
}
|
||||
|
||||
@PostConstruct
|
||||
public void initialize() {
|
||||
bean.doSomething();
|
||||
}
|
||||
|
||||
}
|
||||
```
|
||||
### @Async方法的异常处理
|
||||
当@Async方法的返回类型是Future类型时,处理async方法执行时抛出的异常非常简单,当在Future对象上调用get方法时异常会被抛出。**但当@Async方法的返回类型是void时,执行时抛出的异常既无法被捕获也无法被传输。可以指定一个AsyncUncaughtExceptionHandler来处理此类异常。**
|
||||
```java
|
||||
public class MyAsyncUncaughtExceptionHandler implements AsyncUncaughtExceptionHandler {
|
||||
|
||||
@Override
|
||||
public void handleUncaughtException(Throwable ex, Method method, Object... params) {
|
||||
// handle exception
|
||||
}
|
||||
}
|
||||
```
|
||||
### Cron表达式
|
||||
在Spring中,cron表达式格式如下:
|
||||
\* \* \* \* \* \*
|
||||
其代表为(s,min,hour,day of month/\*(1~31)\*/,month/\*(1~12)\*/,day of week/\*(0~7,0 or 7 is sun)\*/)
|
||||
> 规则如下:
|
||||
> - 所有字段都可以用(*)来匹配所有值
|
||||
> - 逗号(,)可以用来分隔同一字段中多个值
|
||||
> - 分号(-)可以用来指定范围,指定的范围左右都包含,eg,1-5代表[1,5]
|
||||
> - 在范围(或是*)后跟随下划线(/)代表间隔,如在分钟字段指定*/20,代表该小时内每过20min
|
||||
> - 对于月份或者day of week,可以使用英文名的前三个字母来代替,大小写不敏感
|
||||
> - 在day of month或day of week字段中可以包含L字母
|
||||
> - 在day of month字段,L代表该月的最后一天,在该字段还可以为L指定一个负的偏移量,如L-n代表该月的倒数第n+1天
|
||||
> - 在day of week字段,L代表该周的最后一天,L前还可以前缀月份的数字或是月份的前三个字母(dL或DDDL,如7L或sunL,代表该月份的最后一个星期日),代该月份的最后一个day of week
|
||||
> - day of month字段可以指定为nW,代表离day of month为n最近的一个工作日,如果n为周六,则该该字段代表的值为周五的day of month,如果n为周六,则该字段代表下周一的day of month(如果n为1其位于周六,其也代表下周一的day of month,1W代表该月的第一个工作日)
|
||||
> - 如果day of month的值为LW,则代表该月的最后一个工作日
|
||||
> - day of week字段还能指定为d#n或DDD#n的形式,代表该月的的第几个d in week(例如SUN#2,代表当前月的第二个星期日)
|
||||
|
||||
#### Macros(宏)
|
||||
Cron表达式可读性不太好,可以使用如下预定义的宏:
|
||||
| Macro | meaning |
|
||||
|:-:|:-:|
|
||||
| @yearly (or @annually) | once a year (0 0 0 1 1 *) |
|
||||
| @monthly | once a month (0 0 0 1 * *) |
|
||||
| @weekly | once a week (0 0 0 * * 0) |
|
||||
| @daily (or @midnight) | once a day (0 0 0 * * *), or |
|
||||
| @hourly | once an hour, (0 0 * * * *) |
|
||||
- [Spring Boot Async](#spring-boot-async)
|
||||
- [Spring Executor和Scheduler的自动配置](#spring-executor和scheduler的自动配置)
|
||||
- [Task Execution and Scheduling](#task-execution-and-scheduling)
|
||||
- [Task Execution Abstraction](#task-execution-abstraction)
|
||||
- [TaskExecutor接口的实现种类](#taskexecutor接口的实现种类)
|
||||
- [TaskExecutor的使用](#taskexecutor的使用)
|
||||
- [Spring TaskScheduler Abstraction](#spring-taskscheduler-abstraction)
|
||||
- [Trigger接口](#trigger接口)
|
||||
- [Trigger实现类](#trigger实现类)
|
||||
- [TaskScheduler实现类](#taskscheduler实现类)
|
||||
- [对任务调度和异步执行的注解支持](#对任务调度和异步执行的注解支持)
|
||||
- [启用Scheduling注解](#启用scheduling注解)
|
||||
- [@Scheduled注解](#scheduled注解)
|
||||
- [@Async注解](#async注解)
|
||||
- [@Async方法的异常处理](#async方法的异常处理)
|
||||
- [Cron表达式](#cron表达式)
|
||||
- [Macros(宏)](#macros宏)
|
||||
|
||||
|
||||
# Spring Boot Async
|
||||
## Spring Executor和Scheduler的自动配置
|
||||
当当前上下文中没有Executor类型的bean对象时,spring boot会自动配置一个ThreadPoolTaskExecutor类型的bean对象,并且将该bean对象和异步task执行(@EnableAsync)和spring mvc异步请求处理关联在一起。
|
||||
该默认创建的ThreadPoolTaskExecutor默认使用8个核心线程,并且线程数量可以根据负载动态的增加或者减少。
|
||||
可以通过如下方式对ThreadPoolTaskExecutor进行配置:
|
||||
```properties
|
||||
# 该线程池最多含有16个线程
|
||||
spring.task.execution.pool.max-size=16
|
||||
# 有有界队列存放task,上限为100
|
||||
spring.task.execution.pool.queue-capacity=100
|
||||
# 当线程空闲10s(默认60s)时会进行回收
|
||||
spring.task.execution.pool.keep-alive=10s
|
||||
# 设置核心线程数量
|
||||
spring.task.execution.pool.core-size=8
|
||||
```
|
||||
如果使用@EnableScheduling,一个ThreadPoolTaskScheduler也可以被配置。该线程池默认使用一个线程,但是也可以动态设置:
|
||||
```properties
|
||||
spring.task.scheduling.thread-name-prefix=scheduling-
|
||||
spring.task.scheduling.pool.size=2
|
||||
```
|
||||
## Task Execution and Scheduling
|
||||
Spring通过TaskExecutor和TaskScheduler接口为task的异步执行和调度提供了抽象。
|
||||
### Task Execution Abstraction
|
||||
Executor对应的是JDK中线程池的概念。在Spring中,TaskExecutor接口和java.util.concurrent.Executor接口相同,该接口中只有一个execute方法(execute(Runnable task)),接受一个task。
|
||||
### TaskExecutor接口的实现种类
|
||||
Spring中包含许多预制的TaskExecutor实现类,该实现类如下:
|
||||
- SyncTaskExecutor:该实现类不会执行异步的调用,所有任务的执行都会发生在调用该executor的线程中。(通常使用在不需要多线程的场景)
|
||||
- SimpleAsyncTaskExecutor:该实现类不会复用任何的线程,相反的,对于每次调用该executor,都会使用一个全新的线程。但是,该实现类的确支持对并发数量的限制,该executor会阻塞任何超过并发数量限制的调用,直到slot被释放为止。(SimpleAsyncTaskExecutor并不支持池化技术)
|
||||
- ConcurrentTaskExecutor:该实现类是java.util.concurrent.Executor实例的adapter,其可以将java.util.concurrent.Executor实例的配置参数以bean properties的形式暴露。(当ThreadPoolTaskExecutor的灵活性无法满足需求时,可以使用ConcurrentTaskExecutor)
|
||||
- ThreadPoolTaskExecutor:该实现类型是最广泛被使用的。该实现类可以通过bean properties来配置java.util.concurrent.ThreadPoolExecutor实例,并且将该实例wrap在TaskExecutor实例中。当想要使用另一种java.util.concurrent.Executor时,可以使用ConcurrentTaskExecutor。
|
||||
- DefaultManagedTaskExecutor:该实现使用了通过JNDI获取的ManagedExecutorService
|
||||
|
||||
### TaskExecutor的使用
|
||||
在springboot中,当使用@EnableAsync时,可以通过配置ThreadPoolExecutor的bean properties来配置线程池的核心线程数和最大线程数等属性。
|
||||
```properties
|
||||
# 该线程池最多含有16个线程
|
||||
spring.task.execution.pool.max-size=16
|
||||
# 有有界队列存放task,上限为100
|
||||
spring.task.execution.pool.queue-capacity=100
|
||||
# 当线程空闲10s(默认60s)时会进行回收
|
||||
spring.task.execution.pool.keep-alive=10s
|
||||
# 设置核心线程数量
|
||||
spring.task.execution.pool.core-size=8
|
||||
```
|
||||
|
||||
## Spring TaskScheduler Abstraction
|
||||
为了在特定的时间点执行task,Spring引入了TaskScheduler接口,接口定义如下:
|
||||
```java
|
||||
public interface TaskScheduler {
|
||||
|
||||
ScheduledFuture schedule(Runnable task, Trigger trigger);
|
||||
|
||||
ScheduledFuture schedule(Runnable task, Instant startTime);
|
||||
|
||||
ScheduledFuture scheduleAtFixedRate(Runnable task, Instant startTime, Duration period);
|
||||
|
||||
ScheduledFuture scheduleAtFixedRate(Runnable task, Duration period);
|
||||
|
||||
ScheduledFuture scheduleWithFixedDelay(Runnable task, Instant startTime, Duration delay);
|
||||
|
||||
ScheduledFuture scheduleWithFixedDelay(Runnable task, Duration delay);
|
||||
```
|
||||
> scheduleAtFixedRate和scheduleAtFixedDelay区别:
|
||||
> - fixed rate:表示两个task执行开始时间的间隔
|
||||
> - fixed delay:表示上一个task结束时间和下一个task开始时间的间隔
|
||||
>
|
||||
> 实例如下:
|
||||
> - fixed rate:TTWWWTTTWWT...(开始时间间隔为5)
|
||||
> - fixed delay:TTWWWWWTTTTWWWWWTTTTTTTWWWWWT...(上次结束和下次开始之间的间隔为5)
|
||||
>
|
||||
> **通常,TaskScheduler默认情况下是单线程执行的,故而fixed rate执行时,如果一个Task执行时间超过period时,在当前task执行完成之前,下一个task并不会开始执行。下一个task会等待当前task执行完成之后立马执行。**
|
||||
|
||||
### Trigger接口
|
||||
Trigger接口的核心理念是下次执行事件由上次执行的结果决定。上次执行的结果存储在TriggerContext中。
|
||||
Trigger接口如下:
|
||||
```java
|
||||
public interface Trigger {
|
||||
|
||||
Date nextExecutionTime(TriggerContext triggerContext);
|
||||
}
|
||||
```
|
||||
TriggerContext接口如下:(其具有默认的实现类SimpleTriggerContext)
|
||||
```java
|
||||
public interface TriggerContext {
|
||||
|
||||
Date lastScheduledExecutionTime();
|
||||
|
||||
Date lastActualExecutionTime();
|
||||
|
||||
Date lastCompletionTime();
|
||||
}
|
||||
```
|
||||
### Trigger实现类
|
||||
Spring为Trigger提供了两个实现类,其中CronTrigger允许task的调度按照cron expression来执行(类似linux中的crond)。
|
||||
```java
|
||||
scheduler.schedule(task, new CronTrigger("0 15 9-17 * * MON-FRI"));
|
||||
```
|
||||
Spring的另一个Trigger实现是PeriodicTrigger,其接受一个固定的period期间,一个可选的初始delay值,并接收一个boolean值标识该period是fixed-rate还是fixed-delay。
|
||||
### TaskScheduler实现类
|
||||
如果不需要外部的线程管理,可以使用spring提供的ThreadPoolTaskScheduler,其会将任务委托给ScheduledExecutorService来提供bean-properties形式的配置。
|
||||
|
||||
## 对任务调度和异步执行的注解支持
|
||||
Spring同时对任务调度和异步方法的执行提供注解支持。
|
||||
### 启用Scheduling注解
|
||||
想要启用@Async和@Scheduled注解,必须将@EnableAsync注解和@EnableScheduling注解添加到一个@Configuration类上。
|
||||
```java
|
||||
@Configuration
|
||||
@EnableAsync
|
||||
@EnableScheduling
|
||||
public class AppConfig {
|
||||
}
|
||||
```
|
||||
> **@Async注解的实现是通过proxy模式来实现的,故而如果在类内部调用位于同一个类中的@Async方法,那么代理拦截会失效,此时调用的@Async方法将会同步执行而非异步执行**
|
||||
|
||||
> @Async可以接收一个value值,用于指定目标的执行器(Executor或TaskExecutor的beanName)
|
||||
|
||||
### @Scheduled注解
|
||||
在将@Scheduled注解标注到方法时,可以为其指定一个trigger的元数据,使用示例如下:
|
||||
```java
|
||||
@Scheduled(fixedDelay = 5000)
|
||||
public void doSomething() {
|
||||
// something that should run periodically
|
||||
}
|
||||
```
|
||||
> 默认情况下,fixedDelay、fixedRate、initialDelay的时间单位都是ms,可以指定timeUnit来指定其他的时间单位:
|
||||
> ```java
|
||||
> @Scheduled(fixedDelay = 5, timeUnit = TimeUnit.SECONDS)
|
||||
> public void doSomething() {
|
||||
> // something that should run periodically
|
||||
> }
|
||||
> ```
|
||||
|
||||
可以为@Scheduled注解使用cron表达式
|
||||
```java
|
||||
@Scheduled(cron="*/5 * * * * MON-FRI")
|
||||
public void doSomething() {
|
||||
// something that should run on weekdays only
|
||||
}
|
||||
```
|
||||
|
||||
### @Async注解
|
||||
通过为方法指定@Async注解,可以让该方法异步执行,该方法的执行通过TaskExecutor。
|
||||
```java
|
||||
@Async
|
||||
void doSomething() {
|
||||
// this will be run asynchronously
|
||||
}
|
||||
```
|
||||
可以为@Async标注的方法指定参数和返回值,但是异步方法的返回值只能为void或是Future类型。
|
||||
在该异步方法的调用方调用返回Future实例的get方法之前,调用方仍然能够执行其他操作,异步方法的执行位于TaskExecutor的线程中。
|
||||
```java
|
||||
@Async
|
||||
Future<String> returnSomething(int i) {
|
||||
// this will be run asynchronously
|
||||
}
|
||||
```
|
||||
不能将@Async注解和生命周期回调(例如@PostConstruct)进行混用,如果想要异步的初始化bean对象,需要按照如下方法:
|
||||
```java
|
||||
public class SampleBeanImpl implements SampleBean {
|
||||
|
||||
@Async
|
||||
void doSomething() {
|
||||
// ...
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
public class SampleBeanInitializer {
|
||||
|
||||
private final SampleBean bean;
|
||||
|
||||
public SampleBeanInitializer(SampleBean bean) {
|
||||
this.bean = bean;
|
||||
}
|
||||
|
||||
@PostConstruct
|
||||
public void initialize() {
|
||||
bean.doSomething();
|
||||
}
|
||||
|
||||
}
|
||||
```
|
||||
### @Async方法的异常处理
|
||||
当@Async方法的返回类型是Future类型时,处理async方法执行时抛出的异常非常简单,当在Future对象上调用get方法时异常会被抛出。**但当@Async方法的返回类型是void时,执行时抛出的异常既无法被捕获也无法被传输。可以指定一个AsyncUncaughtExceptionHandler来处理此类异常。**
|
||||
```java
|
||||
public class MyAsyncUncaughtExceptionHandler implements AsyncUncaughtExceptionHandler {
|
||||
|
||||
@Override
|
||||
public void handleUncaughtException(Throwable ex, Method method, Object... params) {
|
||||
// handle exception
|
||||
}
|
||||
}
|
||||
```
|
||||
### Cron表达式
|
||||
在Spring中,cron表达式格式如下:
|
||||
\* \* \* \* \* \*
|
||||
其代表为(s,min,hour,day of month/\*(1~31)\*/,month/\*(1~12)\*/,day of week/\*(0~7,0 or 7 is sun)\*/)
|
||||
> 规则如下:
|
||||
> - 所有字段都可以用(*)来匹配所有值
|
||||
> - 逗号(,)可以用来分隔同一字段中多个值
|
||||
> - 分号(-)可以用来指定范围,指定的范围左右都包含,eg,1-5代表[1,5]
|
||||
> - 在范围(或是*)后跟随下划线(/)代表间隔,如在分钟字段指定*/20,代表该小时内每过20min
|
||||
> - 对于月份或者day of week,可以使用英文名的前三个字母来代替,大小写不敏感
|
||||
> - 在day of month或day of week字段中可以包含L字母
|
||||
> - 在day of month字段,L代表该月的最后一天,在该字段还可以为L指定一个负的偏移量,如L-n代表该月的倒数第n+1天
|
||||
> - 在day of week字段,L代表该周的最后一天,L前还可以前缀月份的数字或是月份的前三个字母(dL或DDDL,如7L或sunL,代表该月份的最后一个星期日),代该月份的最后一个day of week
|
||||
> - day of month字段可以指定为nW,代表离day of month为n最近的一个工作日,如果n为周六,则该该字段代表的值为周五的day of month,如果n为周六,则该字段代表下周一的day of month(如果n为1其位于周六,其也代表下周一的day of month,1W代表该月的第一个工作日)
|
||||
> - 如果day of month的值为LW,则代表该月的最后一个工作日
|
||||
> - day of week字段还能指定为d#n或DDD#n的形式,代表该月的的第几个d in week(例如SUN#2,代表当前月的第二个星期日)
|
||||
|
||||
#### Macros(宏)
|
||||
Cron表达式可读性不太好,可以使用如下预定义的宏:
|
||||
| Macro | meaning |
|
||||
|:-:|:-:|
|
||||
| @yearly (or @annually) | once a year (0 0 0 1 1 *) |
|
||||
| @monthly | once a month (0 0 0 1 * *) |
|
||||
| @weekly | once a week (0 0 0 * * 0) |
|
||||
| @daily (or @midnight) | once a day (0 0 0 * * *), or |
|
||||
| @hourly | once an hour, (0 0 * * * *) |
|
||||
|
||||
@@ -1,53 +1,53 @@
|
||||
# Spring Data Redis
|
||||
- ## Spring Data Redis
|
||||
- 在Spring框架中,与Redis进行通信,既提供了低层次的api与字节数据进行交互,也提供了高度抽象的api供用户使用
|
||||
- Redis Connection支持通过连接器和Redis服务端进行低层次的交互
|
||||
- RedisTemplate则是向用户提供了高层次api与Rdis服务端进行交互
|
||||
- ## 连接Redis
|
||||
- 为了通过IOC容器连接Redis,需要使用统一的Spring Redis API。对于任何库提供的Redis Connector,都会统一向外提供一致的Spring Redis API。
|
||||
- Spring Redis API通过RedisConnection和RedisConnectionFactory接口进行工作,并且从Redis处获取连接
|
||||
- ## RedisConnection和RedisConnectionFactory
|
||||
- RdisConnection用于提供和Redis后端的交互。RedisConnection接口会自动将底层Connector库异常转化为统一的Spring DAO异常,因而在使用ReidsConnection的过程中,如果切换底层链接库,并不需要代码的改动。
|
||||
- RedisConnection时通过RedisConnectionFactory来获取的
|
||||
- ## RedisTemplate
|
||||
- RedisTemplate是线程安全的,可以在多个线程中被并发安全的使用
|
||||
- RedisTemplate使用java底层的序列化机制,在通过RedisTemplate读取或者写入数据时,会通过Java的序列化机制将对象序列化/反序列化。
|
||||
- RedisTemplate要求key是非空的,但是可以接受空的value值
|
||||
- 当想要使用RedisTemplate的某个视图时,可以将RedisTemplate对象注入给该名为xxxOperations的视图。
|
||||
```java
|
||||
@Service
|
||||
public class RedisTemplateOperations {
|
||||
// 直接注入
|
||||
@Autowired
|
||||
private RedisTemplate<String,String> redisTemplate;
|
||||
|
||||
// 将RedisTemplate转换成其某一个视图之后再注入
|
||||
@Resource(name="redisTemplate")
|
||||
private ListOperations<String,String> opsForList;
|
||||
}
|
||||
```
|
||||
- ## StringRedisTemplate
|
||||
- 在Redis操作中,key和Value通常都是String类型。故而,Spring Data Redis提供了操作StringRedisTemplate类
|
||||
- StringRedisTemplate是RedisTemplate的子类,并且在应用启动时IOC容器中会注入redisTemplate和stringRedisTemplate两个对象
|
||||
- 相比较于RedisTemplate,StringRedisTemplate底层使用StringRedisSerializer来进行序列化,其序列化的key和value都是可读的
|
||||
- ## 序列化Serializer选择
|
||||
- 除了先前提到的jdk默认的序列化机制和StringRedisSerializer,Spring Data Redis还提供了其他Serializer
|
||||
- 例如,可以选择将key和value化为json字符串格式,可以选用Jackson2JsonSerializer或者GenericJackson2JsonSerializer
|
||||
> 相对于Jackson2JsonSerializer,GenericJackson2JsonSerializer在序列化对象为json串时添加了对象的java类型信息,故而在将json串反序列化并且转换为原有类型时不会抛出异常
|
||||
```java
|
||||
/**
|
||||
* 如果想要自定义redisTemplate的序列化方式,可以添加如下配置类
|
||||
* RedisTemplate支持分别自定义key/value/hashkey/hashvalue的序列化方式
|
||||
* RedisTemplate也支持设置defaultSerializer,当key/value/hashkey/hashvalue的Serializer没有显式指定时,会应用defaultSerializer
|
||||
**/
|
||||
@Configuration
|
||||
public class SerializerConfig {
|
||||
@Bean(name="redisTemplate")
|
||||
RedisTemplate<Object,Object> redisTemplate(RedisConnectionFactory redisConnectionFactory) {
|
||||
RedisTemplate<Object, Object> redisTemplate = new RedisTemplate<>();
|
||||
redisTemplate.setConnectionFactory(redisConnectionFactory);
|
||||
redisTemplate.setDefaultSerializer(new GenericJackson2JsonRedisSerializer());
|
||||
return redisTemplate;
|
||||
}
|
||||
}
|
||||
# Spring Data Redis
|
||||
- ## Spring Data Redis
|
||||
- 在Spring框架中,与Redis进行通信,既提供了低层次的api与字节数据进行交互,也提供了高度抽象的api供用户使用
|
||||
- Redis Connection支持通过连接器和Redis服务端进行低层次的交互
|
||||
- RedisTemplate则是向用户提供了高层次api与Rdis服务端进行交互
|
||||
- ## 连接Redis
|
||||
- 为了通过IOC容器连接Redis,需要使用统一的Spring Redis API。对于任何库提供的Redis Connector,都会统一向外提供一致的Spring Redis API。
|
||||
- Spring Redis API通过RedisConnection和RedisConnectionFactory接口进行工作,并且从Redis处获取连接
|
||||
- ## RedisConnection和RedisConnectionFactory
|
||||
- RdisConnection用于提供和Redis后端的交互。RedisConnection接口会自动将底层Connector库异常转化为统一的Spring DAO异常,因而在使用ReidsConnection的过程中,如果切换底层链接库,并不需要代码的改动。
|
||||
- RedisConnection时通过RedisConnectionFactory来获取的
|
||||
- ## RedisTemplate
|
||||
- RedisTemplate是线程安全的,可以在多个线程中被并发安全的使用
|
||||
- RedisTemplate使用java底层的序列化机制,在通过RedisTemplate读取或者写入数据时,会通过Java的序列化机制将对象序列化/反序列化。
|
||||
- RedisTemplate要求key是非空的,但是可以接受空的value值
|
||||
- 当想要使用RedisTemplate的某个视图时,可以将RedisTemplate对象注入给该名为xxxOperations的视图。
|
||||
```java
|
||||
@Service
|
||||
public class RedisTemplateOperations {
|
||||
// 直接注入
|
||||
@Autowired
|
||||
private RedisTemplate<String,String> redisTemplate;
|
||||
|
||||
// 将RedisTemplate转换成其某一个视图之后再注入
|
||||
@Resource(name="redisTemplate")
|
||||
private ListOperations<String,String> opsForList;
|
||||
}
|
||||
```
|
||||
- ## StringRedisTemplate
|
||||
- 在Redis操作中,key和Value通常都是String类型。故而,Spring Data Redis提供了操作StringRedisTemplate类
|
||||
- StringRedisTemplate是RedisTemplate的子类,并且在应用启动时IOC容器中会注入redisTemplate和stringRedisTemplate两个对象
|
||||
- 相比较于RedisTemplate,StringRedisTemplate底层使用StringRedisSerializer来进行序列化,其序列化的key和value都是可读的
|
||||
- ## 序列化Serializer选择
|
||||
- 除了先前提到的jdk默认的序列化机制和StringRedisSerializer,Spring Data Redis还提供了其他Serializer
|
||||
- 例如,可以选择将key和value化为json字符串格式,可以选用Jackson2JsonSerializer或者GenericJackson2JsonSerializer
|
||||
> 相对于Jackson2JsonSerializer,GenericJackson2JsonSerializer在序列化对象为json串时添加了对象的java类型信息,故而在将json串反序列化并且转换为原有类型时不会抛出异常
|
||||
```java
|
||||
/**
|
||||
* 如果想要自定义redisTemplate的序列化方式,可以添加如下配置类
|
||||
* RedisTemplate支持分别自定义key/value/hashkey/hashvalue的序列化方式
|
||||
* RedisTemplate也支持设置defaultSerializer,当key/value/hashkey/hashvalue的Serializer没有显式指定时,会应用defaultSerializer
|
||||
**/
|
||||
@Configuration
|
||||
public class SerializerConfig {
|
||||
@Bean(name="redisTemplate")
|
||||
RedisTemplate<Object,Object> redisTemplate(RedisConnectionFactory redisConnectionFactory) {
|
||||
RedisTemplate<Object, Object> redisTemplate = new RedisTemplate<>();
|
||||
redisTemplate.setConnectionFactory(redisConnectionFactory);
|
||||
redisTemplate.setDefaultSerializer(new GenericJackson2JsonRedisSerializer());
|
||||
return redisTemplate;
|
||||
}
|
||||
}
|
||||
```
|
||||
@@ -1,9 +1,9 @@
|
||||
# Spring Data
|
||||
- ## Spring Boot中选择连接池的算法:
|
||||
- HikariCP的表现和并发性都很好,如果HikariCP可以被获取,选择HikariCP
|
||||
- 如果HikariCP不能获取,选用Tomcat Datasource
|
||||
- 如果Tomcat Datasource也不能获取,选用DBCP2
|
||||
- 如果上述都无法获取,选用Oracle UCP
|
||||
- Spring Boot使用自定义连接池:
|
||||
- 可以通过显式指定自定义的连接池种类来绕过该算法,通过指定spring.datasource.type来自定义连接池
|
||||
# Spring Data
|
||||
- ## Spring Boot中选择连接池的算法:
|
||||
- HikariCP的表现和并发性都很好,如果HikariCP可以被获取,选择HikariCP
|
||||
- 如果HikariCP不能获取,选用Tomcat Datasource
|
||||
- 如果Tomcat Datasource也不能获取,选用DBCP2
|
||||
- 如果上述都无法获取,选用Oracle UCP
|
||||
- Spring Boot使用自定义连接池:
|
||||
- 可以通过显式指定自定义的连接池种类来绕过该算法,通过指定spring.datasource.type来自定义连接池
|
||||
- 可以通过DatasourceBuilder来定义额外的datasource。如果定义了自己的datasouce bean,那么自动装配将不会发生。
|
||||
@@ -1,49 +1,49 @@
|
||||
# Spring单元测试
|
||||
## SpringBootTest
|
||||
在SpringBoot中,提供了@SpringBootTest注解。当需要SpringBoot特性时,可以通过使用@SpringBootTest注解来作为@ContextConfiguration的替代。@SprngBootTest创建ApplicationContext,该context在test中被使用。
|
||||
## @AfterAll
|
||||
该注解标明的方法,会在所有Test执行完成之后再执行
|
||||
> ### @AfterAll注解特性
|
||||
> - @AfterAll注解的方法必须含有void类型返回值
|
||||
> - @AfterAll注解标注方法不能为private
|
||||
> - 默认情况下,@AfterAll注解的方法必须是static修饰的
|
||||
|
||||
> ### 在非static方法中标注@AfterAll
|
||||
> 在非static方法上标注@AfterAll,需要在class上标注@TestInstance(TestInstance.Lifecycle.PER_CLASS)。
|
||||
> 因为默认情况下TestInstance的默认生命周期是PER_METHOD
|
||||
> ```JAVA
|
||||
> @TestInstance(TestInstance.Lifecycle.PER_CLASS)
|
||||
> public class BeforeAndAfterAnnotationsUnitTest {
|
||||
>
|
||||
> String input;
|
||||
> Long result;
|
||||
> @BeforeAll
|
||||
> public void setup() {
|
||||
> input = "77";
|
||||
> }
|
||||
>
|
||||
> @AfterAll
|
||||
> public void teardown() {
|
||||
> input = null;
|
||||
> result = null;
|
||||
> }
|
||||
>
|
||||
> @Test
|
||||
> public void whenConvertStringToLong_thenResultShouldBeLong() {
|
||||
> result = Long.valueOf(input);
|
||||
> Assertions.assertEquals(77l, result);
|
||||
> }
|
||||
> }
|
||||
> ```
|
||||
|
||||
## @AfterEach
|
||||
该注解标明的方法,在每次@Test标注方法执行完成之后都会被执行。
|
||||
> ### @AfterEach特性
|
||||
> - 标注方法返回值为空
|
||||
> - 标注方法不为private
|
||||
> - 标注方法不是static
|
||||
|
||||
## @BeforeAll
|
||||
类似@AfterAll
|
||||
## @BeforeEach
|
||||
# Spring单元测试
|
||||
## SpringBootTest
|
||||
在SpringBoot中,提供了@SpringBootTest注解。当需要SpringBoot特性时,可以通过使用@SpringBootTest注解来作为@ContextConfiguration的替代。@SprngBootTest创建ApplicationContext,该context在test中被使用。
|
||||
## @AfterAll
|
||||
该注解标明的方法,会在所有Test执行完成之后再执行
|
||||
> ### @AfterAll注解特性
|
||||
> - @AfterAll注解的方法必须含有void类型返回值
|
||||
> - @AfterAll注解标注方法不能为private
|
||||
> - 默认情况下,@AfterAll注解的方法必须是static修饰的
|
||||
|
||||
> ### 在非static方法中标注@AfterAll
|
||||
> 在非static方法上标注@AfterAll,需要在class上标注@TestInstance(TestInstance.Lifecycle.PER_CLASS)。
|
||||
> 因为默认情况下TestInstance的默认生命周期是PER_METHOD
|
||||
> ```JAVA
|
||||
> @TestInstance(TestInstance.Lifecycle.PER_CLASS)
|
||||
> public class BeforeAndAfterAnnotationsUnitTest {
|
||||
>
|
||||
> String input;
|
||||
> Long result;
|
||||
> @BeforeAll
|
||||
> public void setup() {
|
||||
> input = "77";
|
||||
> }
|
||||
>
|
||||
> @AfterAll
|
||||
> public void teardown() {
|
||||
> input = null;
|
||||
> result = null;
|
||||
> }
|
||||
>
|
||||
> @Test
|
||||
> public void whenConvertStringToLong_thenResultShouldBeLong() {
|
||||
> result = Long.valueOf(input);
|
||||
> Assertions.assertEquals(77l, result);
|
||||
> }
|
||||
> }
|
||||
> ```
|
||||
|
||||
## @AfterEach
|
||||
该注解标明的方法,在每次@Test标注方法执行完成之后都会被执行。
|
||||
> ### @AfterEach特性
|
||||
> - 标注方法返回值为空
|
||||
> - 标注方法不为private
|
||||
> - 标注方法不是static
|
||||
|
||||
## @BeforeAll
|
||||
类似@AfterAll
|
||||
## @BeforeEach
|
||||
类似@AfterEach
|
||||
@@ -1,71 +1,71 @@
|
||||
# Spring Boot JSON
|
||||
## Spring Boot JSON简介
|
||||
在Spring Boot中为JSON提供了三个集成的内置库:Gson、Jackson、JSON-B。
|
||||
> 其中,Jackson是Spring Boot推荐并且默认的JSON库。
|
||||
|
||||
Spring Boot项目为Jackson提供了自动装配,并且Jackson是spring-boot-starter-json启动器的一部分。当Jackson依赖在classpath下时,ObjectMapper的bean对象会自动的被配置。
|
||||
## 自定义序列化器和反序列化器
|
||||
如果使用Jackson进行json数据的序列化和反序列化,你可能需要实现自己的序列化类和反序列化类。可以通过@JsonComponent注解来定义自己的JsonSerializer, JsonDeserializer,JsonObjectSerializer,JsonObjectDeserializer
|
||||
```java
|
||||
@JsonComponent
|
||||
public class MyJsonComponent {
|
||||
|
||||
public static class Serializer extends JsonSerializer<MyObject> {
|
||||
|
||||
@Override
|
||||
public void serialize(MyObject value, JsonGenerator jgen, SerializerProvider serializers) throws IOException {
|
||||
jgen.writeStartObject();
|
||||
jgen.writeStringField("name", value.getName());
|
||||
jgen.writeNumberField("age", value.getAge());
|
||||
jgen.writeEndObject();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
public static class Deserializer extends JsonDeserializer<MyObject> {
|
||||
|
||||
@Override
|
||||
public MyObject deserialize(JsonParser jsonParser, DeserializationContext ctxt) throws IOException {
|
||||
ObjectCodec codec = jsonParser.getCodec();
|
||||
JsonNode tree = codec.readTree(jsonParser);
|
||||
String name = tree.get("name").textValue();
|
||||
int age = tree.get("age").intValue();
|
||||
return new MyObject(name, age);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@JsonComponent
|
||||
public class MyJsonComponent {
|
||||
|
||||
public static class Serializer extends JsonObjectSerializer<MyObject> {
|
||||
|
||||
@Override
|
||||
protected void serializeObject(MyObject value, JsonGenerator jgen, SerializerProvider provider)
|
||||
throws IOException {
|
||||
jgen.writeStringField("name", value.getName());
|
||||
jgen.writeNumberField("age", value.getAge());
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
public static class Deserializer extends JsonObjectDeserializer<MyObject> {
|
||||
|
||||
@Override
|
||||
protected MyObject deserializeObject(JsonParser jsonParser, DeserializationContext context, ObjectCodec codec,
|
||||
JsonNode tree) throws IOException {
|
||||
String name = nullSafeValue(tree.get("name"), String.class);
|
||||
int age = nullSafeValue(tree.get("age"), Integer.class);
|
||||
return new MyObject(name, age);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
```
|
||||
## Jackson使用
|
||||
Jackson使用ObjectMapper来将json转化为java对象,或者将java对象转化为json。
|
||||
### ObjectMapper处理json域字段和java类域字段的映射关系
|
||||
|
||||
# Spring Boot JSON
|
||||
## Spring Boot JSON简介
|
||||
在Spring Boot中为JSON提供了三个集成的内置库:Gson、Jackson、JSON-B。
|
||||
> 其中,Jackson是Spring Boot推荐并且默认的JSON库。
|
||||
|
||||
Spring Boot项目为Jackson提供了自动装配,并且Jackson是spring-boot-starter-json启动器的一部分。当Jackson依赖在classpath下时,ObjectMapper的bean对象会自动的被配置。
|
||||
## 自定义序列化器和反序列化器
|
||||
如果使用Jackson进行json数据的序列化和反序列化,你可能需要实现自己的序列化类和反序列化类。可以通过@JsonComponent注解来定义自己的JsonSerializer, JsonDeserializer,JsonObjectSerializer,JsonObjectDeserializer
|
||||
```java
|
||||
@JsonComponent
|
||||
public class MyJsonComponent {
|
||||
|
||||
public static class Serializer extends JsonSerializer<MyObject> {
|
||||
|
||||
@Override
|
||||
public void serialize(MyObject value, JsonGenerator jgen, SerializerProvider serializers) throws IOException {
|
||||
jgen.writeStartObject();
|
||||
jgen.writeStringField("name", value.getName());
|
||||
jgen.writeNumberField("age", value.getAge());
|
||||
jgen.writeEndObject();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
public static class Deserializer extends JsonDeserializer<MyObject> {
|
||||
|
||||
@Override
|
||||
public MyObject deserialize(JsonParser jsonParser, DeserializationContext ctxt) throws IOException {
|
||||
ObjectCodec codec = jsonParser.getCodec();
|
||||
JsonNode tree = codec.readTree(jsonParser);
|
||||
String name = tree.get("name").textValue();
|
||||
int age = tree.get("age").intValue();
|
||||
return new MyObject(name, age);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@JsonComponent
|
||||
public class MyJsonComponent {
|
||||
|
||||
public static class Serializer extends JsonObjectSerializer<MyObject> {
|
||||
|
||||
@Override
|
||||
protected void serializeObject(MyObject value, JsonGenerator jgen, SerializerProvider provider)
|
||||
throws IOException {
|
||||
jgen.writeStringField("name", value.getName());
|
||||
jgen.writeNumberField("age", value.getAge());
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
public static class Deserializer extends JsonObjectDeserializer<MyObject> {
|
||||
|
||||
@Override
|
||||
protected MyObject deserializeObject(JsonParser jsonParser, DeserializationContext context, ObjectCodec codec,
|
||||
JsonNode tree) throws IOException {
|
||||
String name = nullSafeValue(tree.get("name"), String.class);
|
||||
int age = nullSafeValue(tree.get("age"), Integer.class);
|
||||
return new MyObject(name, age);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
```
|
||||
## Jackson使用
|
||||
Jackson使用ObjectMapper来将json转化为java对象,或者将java对象转化为json。
|
||||
### ObjectMapper处理json域字段和java类域字段的映射关系
|
||||
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
# File Upload and Download in Spring Boot
|
||||
## SpringBoot上传文件配置
|
||||
在application.properties中,可以进行如下配置
|
||||
```properties
|
||||
spring.servlet.multipart.enabled=true
|
||||
spring.servlet.multipart.max-file-size=10MB
|
||||
spring.servlet.multipart.max-request-size=15MB
|
||||
# File Upload and Download in Spring Boot
|
||||
## SpringBoot上传文件配置
|
||||
在application.properties中,可以进行如下配置
|
||||
```properties
|
||||
spring.servlet.multipart.enabled=true
|
||||
spring.servlet.multipart.max-file-size=10MB
|
||||
spring.servlet.multipart.max-request-size=15MB
|
||||
```
|
||||
Reference in New Issue
Block a user