diff --git a/css/css.md b/css/css.md index aee7d21..1a27145 100644 --- a/css/css.md +++ b/css/css.md @@ -1,102 +1,102 @@ -# CSS -- ## css选择器 - - 根据标签名选择 - ```css - - h1,p,li { - color:red; - } - ``` - - 根据class进行选择 - - 通过class属性来进行选择 - ```css - .disk-block { - border:1px black dashed; - } - ``` - - 通过标签名和class同时来进行选择 - ```css - li.pink-css,div.pink-css { - color:pink; - } - ``` - - 后代选择器 - - 后代选择其会根据标签的位置来进行选择,多个标签之间通过空格隔开 - ```css - li em { - font-style: italic; - } - li span.pink-style { - color:pink; - } - ``` - - 相邻兄弟选择器 - - 相邻兄弟选择器会选择其相邻的下一个兄弟节点 - ```css - li + li { - color:red; - } - ``` - - 子选择器 - - 相比与后代选择器,子选择器只会选择其节点的直接后代,间接的后代不会被选中 - ```css - li > em { - color:pink; - } - ``` -- ## css使用方法 - - 引用外部css文件 - ```html - - - - ``` - - 内部样式 - ```html - - - - ``` - - 内联样式 - ```html -

Hello

- ``` -- ## 多条样式规则应用于同一个元素 - - 多个样式规则的选择器范围相同 - - 在选择其范围相同的情况下,后出现的选择器样式会覆盖先前出现的选择器样式 - ```css - p { - color:red; - } - p { - color:blue; - } - /* 后出现的p样式会覆盖之前出现的p样式,最终颜色为blue */ - ``` - - 多个样式规则的选择器范围不同 - - 如果多个样式的选择器范围不同,那么范围更小更特殊的选择器样式胜出 - ```css - .red-p { - color:red; - } - p { - color:blue - } - /* 此时类选择器比元素选择器更特殊,故而最终颜色为red */ - ``` - - 在同一个选择器中重复指定样式 - - 在同一选择器中重复指定样式,位于后面的样式会覆盖位于前面的样式 - ```css - p { - color:blue; - color:red; - } - ``` - - +# CSS +- ## css选择器 + - 根据标签名选择 + ```css + + h1,p,li { + color:red; + } + ``` + - 根据class进行选择 + - 通过class属性来进行选择 + ```css + .disk-block { + border:1px black dashed; + } + ``` + - 通过标签名和class同时来进行选择 + ```css + li.pink-css,div.pink-css { + color:pink; + } + ``` + - 后代选择器 + - 后代选择其会根据标签的位置来进行选择,多个标签之间通过空格隔开 + ```css + li em { + font-style: italic; + } + li span.pink-style { + color:pink; + } + ``` + - 相邻兄弟选择器 + - 相邻兄弟选择器会选择其相邻的下一个兄弟节点 + ```css + li + li { + color:red; + } + ``` + - 子选择器 + - 相比与后代选择器,子选择器只会选择其节点的直接后代,间接的后代不会被选中 + ```css + li > em { + color:pink; + } + ``` +- ## css使用方法 + - 引用外部css文件 + ```html + + + + ``` + - 内部样式 + ```html + + + + ``` + - 内联样式 + ```html +

Hello

+ ``` +- ## 多条样式规则应用于同一个元素 + - 多个样式规则的选择器范围相同 + - 在选择其范围相同的情况下,后出现的选择器样式会覆盖先前出现的选择器样式 + ```css + p { + color:red; + } + p { + color:blue; + } + /* 后出现的p样式会覆盖之前出现的p样式,最终颜色为blue */ + ``` + - 多个样式规则的选择器范围不同 + - 如果多个样式的选择器范围不同,那么范围更小更特殊的选择器样式胜出 + ```css + .red-p { + color:red; + } + p { + color:blue + } + /* 此时类选择器比元素选择器更特殊,故而最终颜色为red */ + ``` + - 在同一个选择器中重复指定样式 + - 在同一选择器中重复指定样式,位于后面的样式会覆盖位于前面的样式 + ```css + p { + color:blue; + color:red; + } + ``` + + diff --git a/java se/CompletableFuture.md b/java se/CompletableFuture.md index 60415b9..be1f554 100644 --- a/java se/CompletableFuture.md +++ b/java se/CompletableFuture.md @@ -1,114 +1,114 @@ -# CompletableFuture -对于Future对象,需要调用其get方法来获取值,get方法会阻塞当前线程直到该值可获取。 -而CompletableFuture实现了Future接口,并且其提供了其他机制来获取result。通过CompletableFuture可以注册一个callback,该回调会在result可获取时调用。 -```java -CompletableFuture f = . . .; -f.thenAccept(s -> Process the result string s); -``` -**通过这种方法,就无需等待result处于可获取状态之后再对其进行处理。** -通常情况下,很少有方法返回类型为CompletableFuture,此时,需要自己指定返回类型。CompletableFuture使用方法如下: -```java -public CompletableFuture readPage(URL url) -{ - return CompletableFuture.supplyAsync(() -> - { - try - { - return new String(url.openStream().readAllBytes(), "UTF-8"); - } - catch (IOException e) - { - throw new UncheckedIOException(e); - } - }, executor); -} -``` -> Compatable.supplyAsync方法接受一个Supplier而不是一个Callable,虽然它们都没有参数且返回一个T,但是Callable允许抛出Exception,但Supplier不许抛出Checked Exception。 -> 并且,**再没有显式为supplyAsync方法指定Executor参数时,其会默认采用ForkJoinPool.commonPool()** - -Complatable可以以两种方式执行结束:(1)是否有返回值(2)是否有未捕获异常。要处理以上两种情况,需要使用whenComplete方法。提供给whenComplete函数的方法将会和执行result(如果没有返回值则为null)与抛出异常exception(如果没有则为null)一起被调用。 -```java -f.whenComplete((s, t) -> { - if (t == null) - { - Process the result s; - } - else - { - Process the Throwable t; - } -}); -``` -CompletableFuture可以手动设置completable value。虽然CompletableFuture对象的完成状态在supplyAsync方法的task执行结束之后会被隐式设置为已完成,但是通过手动设置完成状态可以提供更大的灵活性,例如可以同时以两种方式进行计算,任一方式先计算结束则将CompletableFuture对象的完成状态设置为已完成: -```java -var f = new CompletableFuture(); -executor.execute(() -> -{ - int n = workHard(arg); - f.complete(n); -}); -executor.execute(() -> -{ - int n = workSmart(arg); - f.complete(n); -}); -``` -如果想要以抛出异常的方式将CompletableFuture对象的完成状态设置为已完成,可以调用completeExcetpionally方法: -```java -Throwable t = . . .; -f.completeExceptionally(t); -``` -> 在不同的线程中调用同一个对象的complete或completeExceptionally方法是线程安全的,如果该completableFuture对象的完成状态为已完成,那么再次调用complete或completeExceptionally方法无效。 - -> ## Caution -> 并不像Future对象,CompletableFuture对象在调用其cancel方法时,并不会对其task执行的线程进行中断操作,而是**仅仅将completableFuture对象的完成状态设置为抛出CancellationException的已完成状态。** -> 因为对于一个CompletableFuture对象,可能并没有一个唯一的线程对应其task的执行(future对象对应task可能未在任何线程中执行,也可能在多个线程中执行) - -## CompletableFuture组合 -异步方法的非阻塞调用是通过callback来实现的,调用者注册一个action,该action在task被完成时调用。如果注册的action仍然是异步的,那么该action的下一个action位于另一个完全不同的callback中。 -```java -f1.whenComplete((s,t)->{ - CompletableFuture<> f2 = ... - f2.whencomplete((s,t)->{ - //... - }) -}) -``` -这样可能会造成callback hell的情况,并且异常处理也特别不方便,由此CompletableFuture提供了一种机制来组合多个CompletableFuture: -```java -CompletableFuture contents = readPage(url); -CompletableFuture> imageURLs = -contents.thenApply(this::getLinks); -``` -在completableFuture对象上调用thenApply方法,thenApply方法也不会阻塞,其会返回另一个completableFuture对象,并且,当第一个Future对象contents返回之后,返回结果会填充到getLinks方法的参数中。 -### CompletableFuture常用方法 -CompletableFuture组合可以有如下常用方法: -- thenApply(T -> U):对CompletableFuture对象的返回结果执行操作,并且产生一个返回值 -- thenAccept(T -> void):类似thenApply,操作上一个future的返回值,但是返回类型为void -- handle(T,Throwable)->U:处理上一个future返回的result并且产生一个返回值 -- thenCompose(T->CompletableFuture\):将上一个Future返回值作为参数传递,并且返回CompletableFuture\ -- whenComplete(T,Throwable)->void:类似于handle,但是不产生返回值 -- exceptionally(Throwable->T):处理异常并返回一个结果 -- completeOnTimeout(T,long,TimeUnit):当超时时,将传入的参数T作为返回结果 -- orTimeOut(long,TimeUnit):当超时时,产生一个TimeoutExcetpion作为结果 -- thenRun(Runnable):执行该Runnable并且返回一个CompletableFuture -如上的每一个方法都有另一个Async版本,如thenApplyAsync: -```java -CompletableFuture future.thenApply(f); -CompletableFuture future.thenApplyAsync(f); -``` -thenApply中,会对future的返回结果执行f操作;而在thenApplyAsync中,对f操作的执行会在另一个线程中。 -thenCompose通常用来连接两个返回类型都为CompletableFuture的方法。 - -### CompletableFuture Combine -- thenCombine(CompletableFuture\,(T,U)->V):执行两个future,并且通过f对两个future的返回值进行combine -- thenAcceptBoth(CompletableFuture\,(T,U)->Void):执行两个future,并通过f处理两个future的返回值,但是返回类型为void -- runAfterBoth(CompletableFuture\<?\>,Runnable):当两个future都执行完之后,执行Runnable -- applyToEither(CompletableFuture\,(T)-> U):当任一future执行完成,通过该result产生返回值 -- acceptEither(CompletableFuture\,T->Void):类似applyToEither,但是返回值为void -- runAfterEither(CompletableFuture\<?\>,Runnable):在任一future执行完成之后,执行Runnable -- static allOf(CompletableFuture\...):在参数中所有future执行完成之后,返回的future处于完成状态 -- static anyOf(CompletableFuture\...):在参数中任一future执行完成之后,返回的future处于完成状态 - -### completedFuture -CompletableFuture.completedFuture会返回一个已经执行完成的CompletableFuture对象,并且该future对象返回值为T +# CompletableFuture +对于Future对象,需要调用其get方法来获取值,get方法会阻塞当前线程直到该值可获取。 +而CompletableFuture实现了Future接口,并且其提供了其他机制来获取result。通过CompletableFuture可以注册一个callback,该回调会在result可获取时调用。 +```java +CompletableFuture f = . . .; +f.thenAccept(s -> Process the result string s); +``` +**通过这种方法,就无需等待result处于可获取状态之后再对其进行处理。** +通常情况下,很少有方法返回类型为CompletableFuture,此时,需要自己指定返回类型。CompletableFuture使用方法如下: +```java +public CompletableFuture readPage(URL url) +{ + return CompletableFuture.supplyAsync(() -> + { + try + { + return new String(url.openStream().readAllBytes(), "UTF-8"); + } + catch (IOException e) + { + throw new UncheckedIOException(e); + } + }, executor); +} +``` +> Compatable.supplyAsync方法接受一个Supplier而不是一个Callable,虽然它们都没有参数且返回一个T,但是Callable允许抛出Exception,但Supplier不许抛出Checked Exception。 +> 并且,**再没有显式为supplyAsync方法指定Executor参数时,其会默认采用ForkJoinPool.commonPool()** + +Complatable可以以两种方式执行结束:(1)是否有返回值(2)是否有未捕获异常。要处理以上两种情况,需要使用whenComplete方法。提供给whenComplete函数的方法将会和执行result(如果没有返回值则为null)与抛出异常exception(如果没有则为null)一起被调用。 +```java +f.whenComplete((s, t) -> { + if (t == null) + { + Process the result s; + } + else + { + Process the Throwable t; + } +}); +``` +CompletableFuture可以手动设置completable value。虽然CompletableFuture对象的完成状态在supplyAsync方法的task执行结束之后会被隐式设置为已完成,但是通过手动设置完成状态可以提供更大的灵活性,例如可以同时以两种方式进行计算,任一方式先计算结束则将CompletableFuture对象的完成状态设置为已完成: +```java +var f = new CompletableFuture(); +executor.execute(() -> +{ + int n = workHard(arg); + f.complete(n); +}); +executor.execute(() -> +{ + int n = workSmart(arg); + f.complete(n); +}); +``` +如果想要以抛出异常的方式将CompletableFuture对象的完成状态设置为已完成,可以调用completeExcetpionally方法: +```java +Throwable t = . . .; +f.completeExceptionally(t); +``` +> 在不同的线程中调用同一个对象的complete或completeExceptionally方法是线程安全的,如果该completableFuture对象的完成状态为已完成,那么再次调用complete或completeExceptionally方法无效。 + +> ## Caution +> 并不像Future对象,CompletableFuture对象在调用其cancel方法时,并不会对其task执行的线程进行中断操作,而是**仅仅将completableFuture对象的完成状态设置为抛出CancellationException的已完成状态。** +> 因为对于一个CompletableFuture对象,可能并没有一个唯一的线程对应其task的执行(future对象对应task可能未在任何线程中执行,也可能在多个线程中执行) + +## CompletableFuture组合 +异步方法的非阻塞调用是通过callback来实现的,调用者注册一个action,该action在task被完成时调用。如果注册的action仍然是异步的,那么该action的下一个action位于另一个完全不同的callback中。 +```java +f1.whenComplete((s,t)->{ + CompletableFuture<> f2 = ... + f2.whencomplete((s,t)->{ + //... + }) +}) +``` +这样可能会造成callback hell的情况,并且异常处理也特别不方便,由此CompletableFuture提供了一种机制来组合多个CompletableFuture: +```java +CompletableFuture contents = readPage(url); +CompletableFuture> imageURLs = +contents.thenApply(this::getLinks); +``` +在completableFuture对象上调用thenApply方法,thenApply方法也不会阻塞,其会返回另一个completableFuture对象,并且,当第一个Future对象contents返回之后,返回结果会填充到getLinks方法的参数中。 +### CompletableFuture常用方法 +CompletableFuture组合可以有如下常用方法: +- thenApply(T -> U):对CompletableFuture对象的返回结果执行操作,并且产生一个返回值 +- thenAccept(T -> void):类似thenApply,操作上一个future的返回值,但是返回类型为void +- handle(T,Throwable)->U:处理上一个future返回的result并且产生一个返回值 +- thenCompose(T->CompletableFuture\):将上一个Future返回值作为参数传递,并且返回CompletableFuture\ +- whenComplete(T,Throwable)->void:类似于handle,但是不产生返回值 +- exceptionally(Throwable->T):处理异常并返回一个结果 +- completeOnTimeout(T,long,TimeUnit):当超时时,将传入的参数T作为返回结果 +- orTimeOut(long,TimeUnit):当超时时,产生一个TimeoutExcetpion作为结果 +- thenRun(Runnable):执行该Runnable并且返回一个CompletableFuture +如上的每一个方法都有另一个Async版本,如thenApplyAsync: +```java +CompletableFuture future.thenApply(f); +CompletableFuture future.thenApplyAsync(f); +``` +thenApply中,会对future的返回结果执行f操作;而在thenApplyAsync中,对f操作的执行会在另一个线程中。 +thenCompose通常用来连接两个返回类型都为CompletableFuture的方法。 + +### CompletableFuture Combine +- thenCombine(CompletableFuture\,(T,U)->V):执行两个future,并且通过f对两个future的返回值进行combine +- thenAcceptBoth(CompletableFuture\,(T,U)->Void):执行两个future,并通过f处理两个future的返回值,但是返回类型为void +- runAfterBoth(CompletableFuture\<?\>,Runnable):当两个future都执行完之后,执行Runnable +- applyToEither(CompletableFuture\,(T)-> U):当任一future执行完成,通过该result产生返回值 +- acceptEither(CompletableFuture\,T->Void):类似applyToEither,但是返回值为void +- runAfterEither(CompletableFuture\<?\>,Runnable):在任一future执行完成之后,执行Runnable +- static allOf(CompletableFuture\...):在参数中所有future执行完成之后,返回的future处于完成状态 +- static anyOf(CompletableFuture\...):在参数中任一future执行完成之后,返回的future处于完成状态 + +### completedFuture +CompletableFuture.completedFuture会返回一个已经执行完成的CompletableFuture对象,并且该future对象返回值为T diff --git a/mybatis/mybatis.md b/mybatis/mybatis.md index 07f94df..2e8779c 100644 --- a/mybatis/mybatis.md +++ b/mybatis/mybatis.md @@ -1,1080 +1,1080 @@ -- [mybatis框架](#mybatis框架) - - [Mybatis中主要的类及其生命周期](#mybatis中主要的类及其生命周期) - - [SqlSessionFactoryBuilder](#sqlsessionfactorybuilder) - - [SqlSessionFactory](#sqlsessionfactory) - - [SqlSession](#sqlsession) - - [Mapper实例](#mapper实例) - - [Mybatis中的配置文件](#mybatis中的配置文件) - - [properties](#properties) - - [settings](#settings) - - [typeAliases](#typealiases) - - [typeHandler](#typehandler) - - [EnumTypeHandler](#enumtypehandler) - - [ObjectFactory](#objectfactory) - - [environment](#environment) - - [transactionManager](#transactionmanager) - - [datasource](#datasource) - - [mappers](#mappers) -- [Mybatis Mapper](#mybatis-mapper) - - [select](#select) - - [update、insert、delete](#updateinsertdelete) - - [selectKey](#selectkey) - - [sql](#sql) - - [parameter](#parameter) - - [mybatis ${}](#mybatis-) - - [resultMap](#resultmap) - - [resultMap的元素和属性](#resultmap的元素和属性) - - [id和result元素](#id和result元素) - - [constructor元素](#constructor元素) - - [association元素](#association元素) - - [嵌套select](#嵌套select) - - [嵌套result](#嵌套result) - - [collection元素](#collection元素) - - [collection元素的嵌套select](#collection元素的嵌套select) - - [collection元素的嵌套result](#collection元素的嵌套result) - - [discriminator](#discriminator) - - [automapping](#automapping) - - [mybatis缓存](#mybatis缓存) - - [cache-ref](#cache-ref) - - [动态sql](#动态sql) - - [if](#if) - - [choose、when、otherwise](#choosewhenotherwise) - - [where、trim、set](#wheretrimset) - - [where](#where) - - [trim](#trim) - - [set](#set) - - [foreach](#foreach) - - [Java API](#java-api) - - [SqlSession](#sqlsession-1) - - [SqlSessionFactory](#sqlsessionfactory-1) - - [SqlSessionFactoryBuilder](#sqlsessionfactorybuilder-1) - - [SqlSessionFactoryBuilder详解](#sqlsessionfactorybuilder详解) - - [创建SqlSessionFactory的示例](#创建sqlsessionfactory的示例) - - [通过Configuration来创建SqlSessionFactory](#通过configuration来创建sqlsessionfactory) - - [SqlSessionFactory构建SqlSession实例](#sqlsessionfactory构建sqlsession实例) - - [openSession参数](#opensession参数) - -# mybatis框架 -## Mybatis中主要的类及其生命周期 -### SqlSessionFactoryBuilder -对于SqlSessionFactoryBuilder,其推荐的生命周期是创建之后立即销毁,不必被保留;其理想的作用域是方法的作用域。 -> 可以将SqlSessionFactoryBuilder进行保留,并用其创建多个SqlSessionFactory实例。但是,为了保证XML解析后资源被正常的释放,应该在将其创建之后立刻丢弃 -### SqlSessionFactory -对于SqlSessionFactory实例,其理想的作用域和生命周期都应该是整个应用的执行期间。在应用运行期间,都会通过该工厂实例来获取SqlSession对象。 -> SqlSessionFactory实例在创建完成之后,都不应该被修改或者是被重新创建。 -### SqlSession -对于SqlSession对象来说,其生命周期和作用域应该是方法级别或者是请求级别。SqlSession对象并不是线程安全的,无法在多线程环境之下被共享。因而,无法将其作为Servlet或是HttpSession的属性或是静态属性,否则会产生非预期的并发安全问题。 -推荐情况下,SqlSession应该在接受到http请求时被创建,并且在请求返回httpResponse时被销毁。 -并且,SqlSession必须要被保证正确的关闭。 -```java -try(SqlSession sqlSession=sqlSessionFactory.openSession()) { - // operations on sqlSession -} -``` -### Mapper实例 -Mapper实例是通过SqlSession实例来获取的,故而,Mapper实例最大的生命周期和作用域和SqlSession。Mapper最推荐的生命周期和作用域是方法级别的,在方法中,获取Mapper并使用后即可丢弃,无需在方法外保存Mapper对象。 -虽然Mapper也可以和SqlSession一样在request范围内有效,但是会发现保留太多Mapper级别的资源而不销毁会很快的产生失控。 -Mapper实例和SqlSession实例不同,并不需要被显式的销毁。 -## Mybatis中的配置文件 -Mybatis中配置文件主要有properties和setting两部分。 -### properties -对于那些可外部化、可替换的属性,可以通过java的.properties文件进行配置,或者通过<properties>标签中配置的子属性来进行传入,具体如下: -```xml - - - - -``` -此时,在整个xml配置文件中,都可以使用${username}和${password}变量或者从config.properties文件中导入的变量。 -在向SqlSessionFactoryBuilder.build方法中传入propertis属性时,优先级如下: -```java -/* -* 首先,会读入xml配置文件中properties标签中的变量内容 -* 然后,会读入properties标签resource或者url属性指定的properties属性 -* 最后,会读入作为参数传入的props属性 -* 按上述顺序读入属性,后读入的属性会覆盖前面读入的同名属性 -*/ - -SqlSessionFactory factory = - sqlSessionFactoryBuilder.build(reader, props); - -// ... or ... - -SqlSessionFactory factory = - new SqlSessionFactoryBuilder.build(reader, environment, props); -``` -### settings -配置settings属性可以修改mybatis运行时的行为。 -### typeAliases -typeAliases标签中可以定义全限定类名的别名,在定义typeAliases之后,xml文件中可以使用别名来代替全限定类名 -```xml - - - -``` -如下是java常用类型的别名: - - - - - - - - - - - - - - - - - - - - - - - - -
AiasMapped Type
_bytebyte
_intint
byteByte
stringString
longLong
- -### typeHandler -每当mybatis向PreparedStatement设置值或者从ResultSet中获取值时,typeHandler都会使用合适的方法来将ResultSet中的属性传递给java类对象或者从Java类对象中获取属性并将其传递给PreparedStatement的sql语句。 -mybatis内置了很多typeHandler,用户可以实现自己的TypeHandler,通过实现TypeHandler接口或者继承BaseTypeHandler类。 -```java -/** - * 该方法会覆盖默认的针对String类型和varchar类型的typeHandler -**/ -// ExampleTypeHandler.java -@MappedJdbcTypes(JdbcType.VARCHAR) -public class ExampleTypeHandler extends BaseTypeHandler { - - @Override - public void setNonNullParameter(PreparedStatement ps, int i, - String parameter, JdbcType jdbcType) throws SQLException { - ps.setString(i, parameter); - } - - @Override - public String getNullableResult(ResultSet rs, String columnName) - throws SQLException { - return rs.getString(columnName); - } - - @Override - public String getNullableResult(ResultSet rs, int columnIndex) - throws SQLException { - return rs.getString(columnIndex); - } - - @Override - public String getNullableResult(CallableStatement cs, int columnIndex) - throws SQLException { - return cs.getString(columnIndex); - } -} -``` -```xml - - - - -``` -mybatis会通过泛型参数来获知其typeHandler想要处理的javaType,但是可以通过两种方法来覆盖该种行为: -- 在typeHandler元素中添加javaType属性(例如,javaType="String") -- 通过@MappedTypes注解指定该typeHandler想要对应的javaType列表(如果javaType属性在typeHandler元素中被指定,那么该注解内容将会被忽略) - -同样的,jdbcType也可以通过两种方式来指定: -- 在typeHandler元素中添加jdbcType属性(例如,jdbcType="VARCHAR") -- 为typeHandler类添加@MappedJdbcTypes注解,并且在jdbcType属性被指定时该注解被忽略 - -当决定在ResultMap中使用typeHandler时,java类型已知但是jdbc类型未知。因此ResultMap采用javaType=[javaType],jdbcType=null的组合来选择typeHandler。为了使typeHandler在ResultMap中能够被使用,需要在@MappedJdbcTypes加入includeNullJdbcType=true。 -> 如果对于某一个java类型,只有一个typeHandler被注册,那么该typeHandler会自动被使用,即使includeNullJdbcType=true并没有被设置。 -### EnumTypeHandler -对于枚举类型的处理,可以使用EnumTypeHandler和EnumOridinalTypeHandler。 -> 默认情况下,mybatis会使用EnumTypeHandler来处理枚举类型,会将枚举类型的转化成其名字的字符串。 -> 可以强制将EnumOridinalTypeHandler指定给sql语句的typeHandler属性,此时会将枚举类型转换成其对应的数字值。 -```xml - - - - - -``` -可以在不同的地方使用不同的枚举类型处理器,将其映射为整数类型或字符串类型。只需在mapper使用非默认类型时显示指定即可。 -```xml - - - - - - - - - - - - insert into users (id, name, funkyNumber, roundingMode) values ( - #{id}, #{name}, #{funkyNumber}, #{roundingMode} - ) - - - - - - - - - - - - - insert into users2 (id, name, funkyNumber, roundingMode) values ( - #{id}, #{name}, #{funkyNumber}, #{roundingMode, typeHandler=org.apache.ibatis.type.EnumTypeHandler} - ) - - - -``` -### ObjectFactory -mybatis会使用ObjectFactory来创建返回对象。ObjectFactory仅仅会调用默认构造函数或者参数化的构造函数来创建返回对象。 -### environment -Mybatis可以设置复数个环境,但是对于每一个SqlSessionFactory对象,只能够选择一个环境。为了指定构建的环境,可以将其以参数的方式传递给SqlSessionFactoryBuilder.build。如果environment参数被省略,那么默认的环境将会被采用。 -环境的配置格式如下: -```xml - - - - - - - - - - - - - - -``` -#### transactionManager -当在spring中使用mybatis框架时,无需在environment中指定transactionManager,Spring会用自己的transactionManager来覆盖environment中的定义。 -#### datasource -在datasource标签中,通过标准jdbc接口来定义。datasource可以分为如下三个类型:POOLED|UNPOOLED|JNDI -- UNPOOLED:对于每次请求,都会都会打开一个新的连接,并且在请求结束之后关闭该链接。 -- POOLED:会在连接池中池化链接,可以避免每次请求都会创建链接和身份认证的开销。在高并发场景下,池化数据源的响应时间要由于未池化的数据源。 -- JNDI:JNDI数据源通常和特定容器一起使用,例如EJB服务器,该类服务器会集中或者在外部配置该数据源。 -#### mappers -通过mappers属性可以向mybatis注册mapper.xml文件。 -```xml - - - - - -``` -# Mybatis Mapper -## select -通常来说,数据库操作中select的频率远远大于update、insert、delete的频率。 -select元素有如下属性: -- id: 命名空间中唯一的标识符,用来引用该sql声明 -- parameterType: 传递参数的全限定类名或者alias,该属性是可选的,mybatis会根据根据实际传递的参数来计算应该使用的typeHandler -- resultType:方法预期返回类型的全类名 -> 如果方法的返回类型是集合类型,那么resultType为集合中包含元素的类型,而不是集合本身的类型。 -- resultMap:对于外部resultMap的命名引用 -> 对于resultType和resultMap,应该只使用其中的一个,同一条select语句中不应该既包含resultType又包含resultMap -- flushCache:如果该属性设置为true,在该Statement被调用时,会导致本地缓存和二级缓存都被刷新。默认情况下,flushCache被设置为false。 -- useCache:如果该属性设置为true,会导致该Statement的查询结果被缓存在二级缓存中。默认情况下,useCache属性为true。 -> mybatis缓存结构: -> - 一级缓存: mybatis一级缓存是针对SqlSession的缓存,如果SqlSession查询数据会将第一次查询到的数据存放在Map缓冲区中。如果后续SqlSession没有对数据进行添加、修改、删除等操作,那么缓存将会失效。 -> 默认情况下,若未开启事务,那么每条语句都可以看作一个事务,多条select语句之间都会刷新缓 存,一级缓存不会起作用。 -> - 二级缓存:mybatis中二级缓存是针对Mapper级别的。二级缓存针对mapper.xml中所有Statement的作用域。二级缓存可以在多个SqlSession之间进行共享。 -> 在执行sql查询之前,如果执行了插入或者删除操作,那么二级缓存会失效。 -> 二级缓存需要手动配置,settings标签中cacheEnabled默认是true,只需要在需开启缓存的mapper.xml中加入cache标签即可开启二级缓存 -- timeout:drvier会等待数据库返回查询结果的超时时间,如果超过该timeout上线,那么会抛出异常。默认情况下,该timeout是未设置的,取决于底层的driver -## update、insert、delete -修改语句通常有如下属性: -- id -- parameterType -- flushCache -- timeout -- statementType: STATEMENT/PREPARED/CALLABLE其中之一,默认值为PREPARED -> statementType通常可以设置为如下值之一: -> - STATEMENT:mybatis会使用Statement -> - PREPARED:mybatis会使用PreparedStatement -> - CALLABLE:mybatis会使用CallableStatement -- useGeneratedKeys:当useGeneratedKeys被设置为true时,会调用JDBC中的getGeneratedKeys方法从数据库内部获取自动生成的key。默认情况下该值为false -> 该属性仅针对insert和update语句 -- keyProperty:后跟随字段或者字段列表,对于想要数据库内部自动生成的字段,mybatis会通过getGeneratedKeys方法返回的值来设置该字段的内容;对于想要自定义字段自动生成(如随机生成)的字段,mybatis会通过insert元素selectKey子元素中的值来设置 -```xml - - - insert into Author (username,password,email,bio) - values (#{username},#{password},#{email},#{bio}) - - - - - - - select CAST(RANDOM()*1000000 as INTEGER) a from SYSIBM.SYSDUMMY1 - - insert into Author - (id, username, password, email,bio, favourite_section) - values - (#{id}, #{username}, #{password}, #{email}, #{bio}, #{favouriteSection,jdbcType=VARCHAR}) - -``` -### selectKey -对于selectKey子元素,其通常具有如下属性: -- keyProperty:该属性指定selectKey生成的结果应该被设置到insert语句的哪个地方 -- resultType:selectKey语句执行结果的返回类型 -- order:可以是“BEFORE"或者”AFTER",如果被设置为“BEFORE",那么会先执行selectKey,然后将产生结果设置到keyProperty,最后才会执行insert语句 - -## sql -sql标签通常被用来定义一些可以复用的sql片段,sql片段可以接受一个参数。 -sql标签定义的sql片段可以被include标签进行引用,并且include标签的refid属性对应sql标签的id属性。 -> 如果一个sql片段嵌套了另一个sql片段,那么在sql语句include外层sql片段时,可以同时为内层和外层sql片段的变量进行赋值操作 -```xml - - ${prefix}Table - - - - from - - - - -``` -## parameter -- 简单的参数映射 -```xml - - -``` -- 向参数传递复杂对象,例如User类型对象,那么将会从对象中获取id、username、password等属性并传递给参数 -```xml - - insert into users (id, username, password) - values (#{id}, #{username}, #{password}) - -``` -## mybatis ${} -对于mybatis,可以通过#{}来设置PreparedStatement的占位参数,但是,当想要动态设置sql statement中的元数据(如表名称、字段名称)时,可以通过${}来插入一个未修改的字符串进行拼串。 -```java -// 未使用${} -@Select("select * from user where id = #{id}") -User findById(@Param("id") long id); - -@Select("select * from user where name = #{name}") -User findByName(@Param("name") String name); - -@Select("select * from user where email = #{email}") -User findByEmail(@Param("email") String email); - -// 使用${}之后 -@Select("select * from user where ${column} = #{value}") -User findByColumn(@Param("column") String column, @Param("value") String value); -``` -## resultMap -- 对于简单的statement,其映射不需要用到resultMap -```xml - - -``` -- 对于POJO,当select的返回列名和POJO类的属性名相同时,会自动生成resultMap来将column和POJO对象属性关联到一起 -```xml - -``` -- 自定义外部的resultMap来映射到User类 -```xml - - - - - - - -``` -- 高级结果集映射 -```xml - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -``` -### resultMap的元素和属性 -- resultMap中可嵌套如下元素: - - constructor:在通过构造方法实例化类时,将select语句返回的result注入到构造方法中 - - idArg:标识为id的参数,将result标识为id可以提高整体的性能 - - arg:将result注入到constructor中 - - id:标识为id的result,将result标识为id可以提高整体性能 - - result:select语句返回的result,将被注入到返回POJO类型的field - - association:复杂关联,返回results中的部分results将会被包装到这种关联中 - - 关联被嵌套在resultMap中,关联本身可以是resultMap,或者可以引用一个外部resultMap - - collection:复杂类型的集合 - - 集合被嵌套在resultMap中,其本身可以是一个resultMap,或者其可以引用要给外部的resultMap - - discrimination:鉴别器,可以通过结果值来判断使用哪个resultMap - - case:条件选项,当值为xx时使用特定的resultMap - - case同样是一个resultMap,case可以是resultMap本身,也可以引用其他的resultMap -- resultMap元素可包含如下属性: - - id:在命名空间内,可以通过该id引用该resultMap - - type:java类的全限定类名或typeAlias - - autoMapping:为resultMap开启或关闭自动映射 -### id和result元素 -id和result元素是resultMap的基础,id和result都将一个column字段值映射到java类型的field域。 -> id和result标签的区别是,当result被标识为id时,该result将会被看作对象的标识符,在比较对象时id result将会被使用。这样可以提高整体性能,尤其是在缓存或是嵌套resultMap时。 - -- id和result元素的属性: - - property:column映射到的java对象的field - - column:数据库表的列名 - - javaType:java类型的全限定类名或者typeAlias,通常映射到java bean时mybatis可以推断java类型,但是当映射到HashMap时,需要显式指定javaType - - jdbcType - - typeHandler -### constructor元素 -当想要构建不可变对象时,可以通过constructor属性来向构造方法中注入result构建对象。 -```java -public class User { - //... - public User(Integer id, String username, int age) { - //... - } -//... -} -``` -```xml - - - - - - -``` -- constructor元素的属性: - - column:数据库表列名 - - javaType - - jdbcType - - typeHandler - - resultMap - - select -### association元素 -association元素通常用来标识“has-a”的关系,通过关联,有两种方式将数据加载到关联中去 -- 嵌套select:执行另一个sql statement并且返回预期的复杂对象 -- 嵌套result:通过嵌套的result mapping来处理连接结果的重复子集 -#### 嵌套select -嵌套select,其会先返回查询的主表数据,对关联的子表数据会通过association标签指定的select属性值来再次执行sql查找预期数据并组装城复杂对象 -当使用嵌套select来处理association时,association标签具有如下属性: -- column:数组库表的列名,该列保存的值将会被传递给嵌套的statement作为输入参数。 -> 当想要传递复合key作为输入参数时,可以通过column="{prop1=col1,prop2=col2}"的形式来传递参数,这会导致传递给嵌套statement的参数对象的prop1和prop2属性被设置 -- select:会返回预期复杂类型的其他select statement标签的id -- fetchType:可选参数,可以为“lazy”或者“eager”,会覆盖全局配置中的lazyLoadingEnable -```xml - - - - - - - - -``` -> 对于嵌套select方法来加载association,其会导致***N+1***的问题 -> 例如,当一条select语句返回一个数据集时,通过嵌套select语句来加载association,其会: -> #### 对数据集中的每一条数据,都会再次执行一个select语句来为该条数据加载association信息,当数据集中数据条数很多时,如果未设置延迟加载,那么成百上千的sql语句被执行,会严重影响性能 - -#### 嵌套result -不同于嵌套select先查主表信息后再次执行sql语句查询关联子表数据,嵌套result会通过A JOIN B关联来通过查询需要的主表信息和子表信息 -嵌套result不需要像嵌套select一样分次执行sql语句 -> #### 使用嵌套result时association可以使用的属性 -> - resultMap:指定用于将results加载到association的resultMap id -> - columnPrefix:可以将具有某一个前缀的列映射到外部的resultMap中,从而即使在select语句中重新为列名定义了alias(添加了前缀,如"co_"等),还是能复用外部的同一个resultMap -> - notNullColumn:该属性可以指定多个列,只有当指定的多个列中任何一个不为空时,mybatis -> 才会创建该子对象 -> - autoMapping:开启或关闭自动映射,该属性对外部映射无效,故而不能搭配select或者resultMap使用 -```xml - - - - - - - - - - - - - - - - - -``` -> 在嵌套result中,id元素(被标识为id的result)非常重要,应该指定一个或多个属性标识为id来唯一标识该对象. -> 当没有指定id result时,mybatis也会正常运行,但是会产生严重的性能问题 -> 应尽可能的将能唯一标识该结果的最小字段集标识为id(可以选择主键或符合主键) - -对于嵌套result,除了上述的办法,还可以将association对应的resultMap直接嵌套写入association的子标签中,样例如下: -```xml - - - - - - - - - - - -``` -共同作者:通过columnPrefix属性复用外部resultMap的情况 -```xml - - - - - - - - - - - - - - - - - - -``` -### collection元素 -collection元素用来处理A类中含有List<B>属性的情况,其用法基本和association元素相同 -```xml - - - - - -``` -和association元素相同,coolection元素也可以通过嵌套select或者嵌套result来映射复杂类型 -#### collection元素的嵌套select -```xml - - - - - - - -``` -#### collection元素的嵌套result -```xml - - - - - - - - - - - - - - - - - - - - - - - - -``` -## discriminator -某些情况下,单条sql查询语句会返回具有不同类型的结果集(如不同的Car数据,但是有卡车和轿车的区别). -discriminator可以通过某些字段的值来选择性采用某些resultMap,并且支持继承层次. -```xml - - - - - - - - - - - - - - - - - - - -``` -如果想要在使用carResult时也执行vehicleResult中的映射,可以在carResult中指定extends属性 -```xml - - - -``` -## automapping -对于automapping,其会获取resultset中的字段名并且在java对象中查找同名的property(忽略大小写,如果想将数据表字段名的underscore形式映射到java类的camelCase属性,可以在mybatis config文件中添加mapUnderscoreToCamelCase配置). -即使手动指定了resultMap,automapping一样会发挥作用.对于每个指定的resultMap,所有在resultSet中出现过的字段如果没有在resultMap中手动为其配置一个映射,那么会对该字段执行automapping. -```xml - - - - - - -``` -> 自动映射有三种等级,可以在mybatis config文件中进行配置 -> - NONE:禁用自动配置 -> - PARTIAL:会对除嵌套result映射以外的属性进行自动行摄 -> - FULL:会对所有属性执行自动映射 -```xml - - - - - - - - - - -``` -默认情况下,automapping级别为partial,并且可以为resultMap元素指定autoMapping属性为false来为该resultMap关闭autoMapping -```xml - - - -``` -## mybatis缓存 -默认情况下,mybatis只启用了会话级别的缓存,其缓存数据只在sqlsession期间有效. -如果想要开启二级缓存,只需要在接口对应的mapper文件中添加一行 -```xml - -``` -该行语句的效果如下: -- 所有select语句的执行结果都会被缓存 -- 所有insert/update/delete语句都会清除缓存 -- 缓存会通过LRU算法来作为淘汰策略 -- 缓存不定期的刷新 -- 缓存会存储1024个引用指向列表或者对象(不管查询方法会返回哪种类型) -- 缓存被设计为可读写缓存,通过缓存获取到的对象并不会和其他线程共享,故而可以安全的对获取到的对象进行共享 -> 缓存只会作用于位于mapper文件中的语句,如果使用java api和xml混合的方式,那么在共用接口声明的sql语句并不会被缓存. - -可以自定义二级缓存 -```xml - - -``` -对于二级缓存,可以使用的淘汰策略有: -- LRU:默认策略 -- FIFO -- SOFT:基于垃圾回收器状态和软引用规则移除对象 -- WEAK:弱引用,基于垃圾回收器状态和弱引用规则移出对象 - -flushInterval: -- 定义cache的刷新间隔,单位为ms,默认没有刷新间隔 - -size: -- 定义缓存大小,为引用数目,默认为1024 - -readOnly: -- true:只读,会给所有调用者返回相同的缓存对象实例,调用者如果进行修改可能会产生线程安全问题 -- false:可读写,会给所有调用者返回缓存对象的拷贝,调用者可以安全的对返回对象进行修改,默认情况下缓存类型是可读写的 - -> ### 二级缓存的事务性 -> 当sqlsession完成并且提交,或完成并回滚时,即使没有执行flushCache=true的insert/delete/update,缓存也会进行刷新 - -> select/update/insert/delete -> sql语句都会有flushCache属性,指定是否在执行完成后刷新cache,默认情况下,select标签的flushCache属性为false,insert/update/delete语句的flushCache属性为true - -### cache-ref -默认情况下,某一命名空间中的语句只会对当前命名空间中的cache进行刷新.但是,如果想在多个命名空间(Mapper)之间共享缓存,可以通过cache-ref来引用其他命名空间中的缓存. -```xml - -``` - -## 动态sql -### if -可以通过<if>标签根据条件向where子句中添加查询条件 -```xml - -``` -### choose、when、otherwise -不同于if会检测所有的条件,choose、when、otherwise只会从多个条件中选择一个使用,类似于java中的switch。 -```xml - - -``` -### where、trim、set -#### where -对于where标签,只有在子元素返回任何内容不为空的前提下才会插入where子句,并且,如果子句开头返回的内容为”or“或者”and“,则where标签会删除子句开头的”and“或者”or“ -```xml - -``` -#### trim -如果where标签并不能满足场景需求,那么可以通过trim来自定义想要实现的场景需求。 -和where标签等价的trim标签为 -```xml - - ... - -``` -对于trim标签,其会匹配prefixOverrides属性中指定的内容,文本内容通过'|'符号分隔,并将开头prefixOverrides中的内容删除,并在首部加入prefix属性指定的内容 -#### set -set标签用于update语句中动态设置属性,并且删除额外的逗号 -```xml - - update Author - - username=#{username}, - password=#{password}, - email=#{email}, - bio=#{bio} - - where id=#{id} - -``` -与set标签等效的trim标签是 -```xml - - - ... - -``` -### foreach -foreach标签允许在动态sql中遍历集合,通常用于构建in条件。 -```xml - -``` -> 可以通过foreach执行便利操作,如果foreach指定的collection为array,那么index对应的时数组下标 -> 若collection为map类型,那么index对应的是entry中的key,而value对应的是value - -## Java API -### SqlSession -SqlSession是Mybatis Java主要的接口,可以通过SqlSession来执行sql语句、获取Mapper实例、管理事务。 -### SqlSessionFactory -SqlSession实例是通过SqlSessionFactory方法来创建的,SqlSessionFactory对象包含创建SqlSession实例的各种方法。 -### SqlSessionFactoryBuilder -SqlSessionFactory本身是由SqlSessionFactoryBuilder来创建的,SqlSessionFactoryBuilder可以通过xml配置、注解或java配置代码来创建SqlSessionFactory。 -### SqlSessionFactoryBuilder详解 -SqlSessionFactoryBuilder有多个build方法,调用不同的build方法可以根据不同的资源创建SqlSessionFactory -```java -// 接受一个InputStream,该InputStream指向mybatis配置的xml文件 -// * environment参数是可选的,决定加载哪个环境,如果指定environment不存在,那么会抛出一个错误 -// 如果调用的build方法中没有environment参数,那么会使用默认的environment配置 -// * properties参数同样是可选的,如果调用的build方法制定了properties参数,那么properties参数中指定 -// 的属性可以在mybatis配置文件中使用,以${propertyName}的形式使用 -SqlSessionFactory build(InputStream inputStream) -SqlSessionFactory build(InputStream inputStream, String environment) -SqlSessionFactory build(InputStream inputStream, Properties properties) -SqlSessionFactory build(InputStream inputStream, String env, Properties props) -``` -> #### 配置文件中加载properties的优先顺序 -> * 在mybatis config配置文件的properties元素中指定的properties最先被加载 -> * 在properties元素的resouce属性或url属性中指定指定的properties文件中导入的属性其次加载,这次加载的properties属性会覆盖上一步中指定的属性 -> * 向build方法中传入的properties参数会最后被读取,并且覆盖之前所有的同名属性,具有最高的优先级 -> **故而,properties优先级顺序为“properties参数>url/resource属性指定的properties>properties元素体中指定的属性”** - -#### 创建SqlSessionFactory的示例 -```java -String resource = "org/mybatis/builder/mybatis-config.xml"; -InputStream inputStream = Resources.getResourceAsStream(resource); -SqlSessionFactoryBuilder builder = new SqlSessionFactoryBuilder(); -SqlSessionFactory factory = builder.build(inputStream); -``` -#### 通过Configuration来创建SqlSessionFactory -之前在mybatis-config.xml中配置的所有mybatis配置属性都可以在Configuration中通过Java API来进行配置 -```java -DataSource dataSource = BaseDataTest.createBlogDataSource(); -TransactionFactory transactionFactory = new JdbcTransactionFactory(); - -Environment environment = new Environment("development", transactionFactory, dataSource); - -Configuration configuration = new Configuration(environment); -configuration.setLazyLoadingEnabled(true); -configuration.setEnhancementEnabled(true); -configuration.getTypeAliasRegistry().registerAlias(Blog.class); -configuration.getTypeAliasRegistry().registerAlias(Post.class); -configuration.getTypeAliasRegistry().registerAlias(Author.class); -configuration.addMapper(BoundBlogMapper.class); -configuration.addMapper(BoundAuthorMapper.class); - -SqlSessionFactoryBuilder builder = new SqlSessionFactoryBuilder(); -SqlSessionFactory factory = builder.build(configuration); -``` -### SqlSessionFactory构建SqlSession实例 -SqlSessionFactory同样有多种方法来创建SqlSession实例 -- #### SqlSession openSession() -默认openSession()无参数的方法会创建具有如下特征的SqlSession -> 1. 事务作用域将会开启(自动提交关闭) -> 2. 从当前环境配置的Datasource中获取Connection -> 3. 事务的隔离级别将会使用驱动或者数据源的默认设置 -> 4. PreparedStatement(预处理语句)并不会被复用,也不会批量更新语句 - -- #### openSesion()的非空参方法 -```java -SqlSession openSession(boolean autoCommit) -SqlSession openSession(Connection connection) -SqlSession openSession(TransactionIsolationLevel level) -SqlSession openSession(ExecutorType execType, TransactionIsolationLevel level) -SqlSession openSession(ExecutorType execType) -SqlSession openSession(ExecutorType execType, boolean autoCommit) -SqlSession openSession(ExecutorType execType, Connection connection) -``` -#### openSession参数 -- autoCommit : boolean类型,为true则开启自动提交功能 -- connection :Connection类型,如果要使用自己的connection实例,可以将其传递给connection参数 -- level :TransactionIsolationLevel枚举类型,控制事务的隔离级别,可选的五个隔离级别为(NONE,READ_UNCOMMITTED,READ_COMMITTED,REPEATABLE_READ,SERIALIZABLE) -- ExecutorType :执行器类型 - - ExecutorType.SIMPLE:该类型的执行器为每个语句创建一个新的预处理语句(PreparedStatement) - - ExecutorType.REUSE:该类型的执行器会复用预处理语句 - - ExecutorType.BATCH:该类型的执行器会批量执行所有的更新语句,如果select语句位于多条update语句之间,那么必要时会将多条更新语句分割成不同的batch,以便于理解 -> #### batch(批量处理) -> batch会将相关的sql语句分组到一个batch文件中,并且一次提交到database server端。 -> - addBatch方法会将单独的sql语句添加到batch中,而executeBatch方法会执行batch中所有被分组到一起的sql语句 -> executeBatch方法会返回一个Integer数组,数组中每一个元素代表每个update语句的update count +- [mybatis框架](#mybatis框架) + - [Mybatis中主要的类及其生命周期](#mybatis中主要的类及其生命周期) + - [SqlSessionFactoryBuilder](#sqlsessionfactorybuilder) + - [SqlSessionFactory](#sqlsessionfactory) + - [SqlSession](#sqlsession) + - [Mapper实例](#mapper实例) + - [Mybatis中的配置文件](#mybatis中的配置文件) + - [properties](#properties) + - [settings](#settings) + - [typeAliases](#typealiases) + - [typeHandler](#typehandler) + - [EnumTypeHandler](#enumtypehandler) + - [ObjectFactory](#objectfactory) + - [environment](#environment) + - [transactionManager](#transactionmanager) + - [datasource](#datasource) + - [mappers](#mappers) +- [Mybatis Mapper](#mybatis-mapper) + - [select](#select) + - [update、insert、delete](#updateinsertdelete) + - [selectKey](#selectkey) + - [sql](#sql) + - [parameter](#parameter) + - [mybatis ${}](#mybatis-) + - [resultMap](#resultmap) + - [resultMap的元素和属性](#resultmap的元素和属性) + - [id和result元素](#id和result元素) + - [constructor元素](#constructor元素) + - [association元素](#association元素) + - [嵌套select](#嵌套select) + - [嵌套result](#嵌套result) + - [collection元素](#collection元素) + - [collection元素的嵌套select](#collection元素的嵌套select) + - [collection元素的嵌套result](#collection元素的嵌套result) + - [discriminator](#discriminator) + - [automapping](#automapping) + - [mybatis缓存](#mybatis缓存) + - [cache-ref](#cache-ref) + - [动态sql](#动态sql) + - [if](#if) + - [choose、when、otherwise](#choosewhenotherwise) + - [where、trim、set](#wheretrimset) + - [where](#where) + - [trim](#trim) + - [set](#set) + - [foreach](#foreach) + - [Java API](#java-api) + - [SqlSession](#sqlsession-1) + - [SqlSessionFactory](#sqlsessionfactory-1) + - [SqlSessionFactoryBuilder](#sqlsessionfactorybuilder-1) + - [SqlSessionFactoryBuilder详解](#sqlsessionfactorybuilder详解) + - [创建SqlSessionFactory的示例](#创建sqlsessionfactory的示例) + - [通过Configuration来创建SqlSessionFactory](#通过configuration来创建sqlsessionfactory) + - [SqlSessionFactory构建SqlSession实例](#sqlsessionfactory构建sqlsession实例) + - [openSession参数](#opensession参数) + +# mybatis框架 +## Mybatis中主要的类及其生命周期 +### SqlSessionFactoryBuilder +对于SqlSessionFactoryBuilder,其推荐的生命周期是创建之后立即销毁,不必被保留;其理想的作用域是方法的作用域。 +> 可以将SqlSessionFactoryBuilder进行保留,并用其创建多个SqlSessionFactory实例。但是,为了保证XML解析后资源被正常的释放,应该在将其创建之后立刻丢弃 +### SqlSessionFactory +对于SqlSessionFactory实例,其理想的作用域和生命周期都应该是整个应用的执行期间。在应用运行期间,都会通过该工厂实例来获取SqlSession对象。 +> SqlSessionFactory实例在创建完成之后,都不应该被修改或者是被重新创建。 +### SqlSession +对于SqlSession对象来说,其生命周期和作用域应该是方法级别或者是请求级别。SqlSession对象并不是线程安全的,无法在多线程环境之下被共享。因而,无法将其作为Servlet或是HttpSession的属性或是静态属性,否则会产生非预期的并发安全问题。 +推荐情况下,SqlSession应该在接受到http请求时被创建,并且在请求返回httpResponse时被销毁。 +并且,SqlSession必须要被保证正确的关闭。 +```java +try(SqlSession sqlSession=sqlSessionFactory.openSession()) { + // operations on sqlSession +} +``` +### Mapper实例 +Mapper实例是通过SqlSession实例来获取的,故而,Mapper实例最大的生命周期和作用域和SqlSession。Mapper最推荐的生命周期和作用域是方法级别的,在方法中,获取Mapper并使用后即可丢弃,无需在方法外保存Mapper对象。 +虽然Mapper也可以和SqlSession一样在request范围内有效,但是会发现保留太多Mapper级别的资源而不销毁会很快的产生失控。 +Mapper实例和SqlSession实例不同,并不需要被显式的销毁。 +## Mybatis中的配置文件 +Mybatis中配置文件主要有properties和setting两部分。 +### properties +对于那些可外部化、可替换的属性,可以通过java的.properties文件进行配置,或者通过<properties>标签中配置的子属性来进行传入,具体如下: +```xml + + + + +``` +此时,在整个xml配置文件中,都可以使用${username}和${password}变量或者从config.properties文件中导入的变量。 +在向SqlSessionFactoryBuilder.build方法中传入propertis属性时,优先级如下: +```java +/* +* 首先,会读入xml配置文件中properties标签中的变量内容 +* 然后,会读入properties标签resource或者url属性指定的properties属性 +* 最后,会读入作为参数传入的props属性 +* 按上述顺序读入属性,后读入的属性会覆盖前面读入的同名属性 +*/ + +SqlSessionFactory factory = + sqlSessionFactoryBuilder.build(reader, props); + +// ... or ... + +SqlSessionFactory factory = + new SqlSessionFactoryBuilder.build(reader, environment, props); +``` +### settings +配置settings属性可以修改mybatis运行时的行为。 +### typeAliases +typeAliases标签中可以定义全限定类名的别名,在定义typeAliases之后,xml文件中可以使用别名来代替全限定类名 +```xml + + + +``` +如下是java常用类型的别名: + + + + + + + + + + + + + + + + + + + + + + + + +
AiasMapped Type
_bytebyte
_intint
byteByte
stringString
longLong
+ +### typeHandler +每当mybatis向PreparedStatement设置值或者从ResultSet中获取值时,typeHandler都会使用合适的方法来将ResultSet中的属性传递给java类对象或者从Java类对象中获取属性并将其传递给PreparedStatement的sql语句。 +mybatis内置了很多typeHandler,用户可以实现自己的TypeHandler,通过实现TypeHandler接口或者继承BaseTypeHandler类。 +```java +/** + * 该方法会覆盖默认的针对String类型和varchar类型的typeHandler +**/ +// ExampleTypeHandler.java +@MappedJdbcTypes(JdbcType.VARCHAR) +public class ExampleTypeHandler extends BaseTypeHandler { + + @Override + public void setNonNullParameter(PreparedStatement ps, int i, + String parameter, JdbcType jdbcType) throws SQLException { + ps.setString(i, parameter); + } + + @Override + public String getNullableResult(ResultSet rs, String columnName) + throws SQLException { + return rs.getString(columnName); + } + + @Override + public String getNullableResult(ResultSet rs, int columnIndex) + throws SQLException { + return rs.getString(columnIndex); + } + + @Override + public String getNullableResult(CallableStatement cs, int columnIndex) + throws SQLException { + return cs.getString(columnIndex); + } +} +``` +```xml + + + + +``` +mybatis会通过泛型参数来获知其typeHandler想要处理的javaType,但是可以通过两种方法来覆盖该种行为: +- 在typeHandler元素中添加javaType属性(例如,javaType="String") +- 通过@MappedTypes注解指定该typeHandler想要对应的javaType列表(如果javaType属性在typeHandler元素中被指定,那么该注解内容将会被忽略) + +同样的,jdbcType也可以通过两种方式来指定: +- 在typeHandler元素中添加jdbcType属性(例如,jdbcType="VARCHAR") +- 为typeHandler类添加@MappedJdbcTypes注解,并且在jdbcType属性被指定时该注解被忽略 + +当决定在ResultMap中使用typeHandler时,java类型已知但是jdbc类型未知。因此ResultMap采用javaType=[javaType],jdbcType=null的组合来选择typeHandler。为了使typeHandler在ResultMap中能够被使用,需要在@MappedJdbcTypes加入includeNullJdbcType=true。 +> 如果对于某一个java类型,只有一个typeHandler被注册,那么该typeHandler会自动被使用,即使includeNullJdbcType=true并没有被设置。 +### EnumTypeHandler +对于枚举类型的处理,可以使用EnumTypeHandler和EnumOridinalTypeHandler。 +> 默认情况下,mybatis会使用EnumTypeHandler来处理枚举类型,会将枚举类型的转化成其名字的字符串。 +> 可以强制将EnumOridinalTypeHandler指定给sql语句的typeHandler属性,此时会将枚举类型转换成其对应的数字值。 +```xml + + + + + +``` +可以在不同的地方使用不同的枚举类型处理器,将其映射为整数类型或字符串类型。只需在mapper使用非默认类型时显示指定即可。 +```xml + + + + + + + + + + + + insert into users (id, name, funkyNumber, roundingMode) values ( + #{id}, #{name}, #{funkyNumber}, #{roundingMode} + ) + + + + + + + + + + + + + insert into users2 (id, name, funkyNumber, roundingMode) values ( + #{id}, #{name}, #{funkyNumber}, #{roundingMode, typeHandler=org.apache.ibatis.type.EnumTypeHandler} + ) + + + +``` +### ObjectFactory +mybatis会使用ObjectFactory来创建返回对象。ObjectFactory仅仅会调用默认构造函数或者参数化的构造函数来创建返回对象。 +### environment +Mybatis可以设置复数个环境,但是对于每一个SqlSessionFactory对象,只能够选择一个环境。为了指定构建的环境,可以将其以参数的方式传递给SqlSessionFactoryBuilder.build。如果environment参数被省略,那么默认的环境将会被采用。 +环境的配置格式如下: +```xml + + + + + + + + + + + + + + +``` +#### transactionManager +当在spring中使用mybatis框架时,无需在environment中指定transactionManager,Spring会用自己的transactionManager来覆盖environment中的定义。 +#### datasource +在datasource标签中,通过标准jdbc接口来定义。datasource可以分为如下三个类型:POOLED|UNPOOLED|JNDI +- UNPOOLED:对于每次请求,都会都会打开一个新的连接,并且在请求结束之后关闭该链接。 +- POOLED:会在连接池中池化链接,可以避免每次请求都会创建链接和身份认证的开销。在高并发场景下,池化数据源的响应时间要由于未池化的数据源。 +- JNDI:JNDI数据源通常和特定容器一起使用,例如EJB服务器,该类服务器会集中或者在外部配置该数据源。 +#### mappers +通过mappers属性可以向mybatis注册mapper.xml文件。 +```xml + + + + + +``` +# Mybatis Mapper +## select +通常来说,数据库操作中select的频率远远大于update、insert、delete的频率。 +select元素有如下属性: +- id: 命名空间中唯一的标识符,用来引用该sql声明 +- parameterType: 传递参数的全限定类名或者alias,该属性是可选的,mybatis会根据根据实际传递的参数来计算应该使用的typeHandler +- resultType:方法预期返回类型的全类名 +> 如果方法的返回类型是集合类型,那么resultType为集合中包含元素的类型,而不是集合本身的类型。 +- resultMap:对于外部resultMap的命名引用 +> 对于resultType和resultMap,应该只使用其中的一个,同一条select语句中不应该既包含resultType又包含resultMap +- flushCache:如果该属性设置为true,在该Statement被调用时,会导致本地缓存和二级缓存都被刷新。默认情况下,flushCache被设置为false。 +- useCache:如果该属性设置为true,会导致该Statement的查询结果被缓存在二级缓存中。默认情况下,useCache属性为true。 +> mybatis缓存结构: +> - 一级缓存: mybatis一级缓存是针对SqlSession的缓存,如果SqlSession查询数据会将第一次查询到的数据存放在Map缓冲区中。如果后续SqlSession没有对数据进行添加、修改、删除等操作,那么缓存将会失效。 +> 默认情况下,若未开启事务,那么每条语句都可以看作一个事务,多条select语句之间都会刷新缓 存,一级缓存不会起作用。 +> - 二级缓存:mybatis中二级缓存是针对Mapper级别的。二级缓存针对mapper.xml中所有Statement的作用域。二级缓存可以在多个SqlSession之间进行共享。 +> 在执行sql查询之前,如果执行了插入或者删除操作,那么二级缓存会失效。 +> 二级缓存需要手动配置,settings标签中cacheEnabled默认是true,只需要在需开启缓存的mapper.xml中加入cache标签即可开启二级缓存 +- timeout:drvier会等待数据库返回查询结果的超时时间,如果超过该timeout上线,那么会抛出异常。默认情况下,该timeout是未设置的,取决于底层的driver +## update、insert、delete +修改语句通常有如下属性: +- id +- parameterType +- flushCache +- timeout +- statementType: STATEMENT/PREPARED/CALLABLE其中之一,默认值为PREPARED +> statementType通常可以设置为如下值之一: +> - STATEMENT:mybatis会使用Statement +> - PREPARED:mybatis会使用PreparedStatement +> - CALLABLE:mybatis会使用CallableStatement +- useGeneratedKeys:当useGeneratedKeys被设置为true时,会调用JDBC中的getGeneratedKeys方法从数据库内部获取自动生成的key。默认情况下该值为false +> 该属性仅针对insert和update语句 +- keyProperty:后跟随字段或者字段列表,对于想要数据库内部自动生成的字段,mybatis会通过getGeneratedKeys方法返回的值来设置该字段的内容;对于想要自定义字段自动生成(如随机生成)的字段,mybatis会通过insert元素selectKey子元素中的值来设置 +```xml + + + insert into Author (username,password,email,bio) + values (#{username},#{password},#{email},#{bio}) + + + + + + + select CAST(RANDOM()*1000000 as INTEGER) a from SYSIBM.SYSDUMMY1 + + insert into Author + (id, username, password, email,bio, favourite_section) + values + (#{id}, #{username}, #{password}, #{email}, #{bio}, #{favouriteSection,jdbcType=VARCHAR}) + +``` +### selectKey +对于selectKey子元素,其通常具有如下属性: +- keyProperty:该属性指定selectKey生成的结果应该被设置到insert语句的哪个地方 +- resultType:selectKey语句执行结果的返回类型 +- order:可以是“BEFORE"或者”AFTER",如果被设置为“BEFORE",那么会先执行selectKey,然后将产生结果设置到keyProperty,最后才会执行insert语句 + +## sql +sql标签通常被用来定义一些可以复用的sql片段,sql片段可以接受一个参数。 +sql标签定义的sql片段可以被include标签进行引用,并且include标签的refid属性对应sql标签的id属性。 +> 如果一个sql片段嵌套了另一个sql片段,那么在sql语句include外层sql片段时,可以同时为内层和外层sql片段的变量进行赋值操作 +```xml + + ${prefix}Table + + + + from + + + + +``` +## parameter +- 简单的参数映射 +```xml + + +``` +- 向参数传递复杂对象,例如User类型对象,那么将会从对象中获取id、username、password等属性并传递给参数 +```xml + + insert into users (id, username, password) + values (#{id}, #{username}, #{password}) + +``` +## mybatis ${} +对于mybatis,可以通过#{}来设置PreparedStatement的占位参数,但是,当想要动态设置sql statement中的元数据(如表名称、字段名称)时,可以通过${}来插入一个未修改的字符串进行拼串。 +```java +// 未使用${} +@Select("select * from user where id = #{id}") +User findById(@Param("id") long id); + +@Select("select * from user where name = #{name}") +User findByName(@Param("name") String name); + +@Select("select * from user where email = #{email}") +User findByEmail(@Param("email") String email); + +// 使用${}之后 +@Select("select * from user where ${column} = #{value}") +User findByColumn(@Param("column") String column, @Param("value") String value); +``` +## resultMap +- 对于简单的statement,其映射不需要用到resultMap +```xml + + +``` +- 对于POJO,当select的返回列名和POJO类的属性名相同时,会自动生成resultMap来将column和POJO对象属性关联到一起 +```xml + +``` +- 自定义外部的resultMap来映射到User类 +```xml + + + + + + + +``` +- 高级结果集映射 +```xml + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +``` +### resultMap的元素和属性 +- resultMap中可嵌套如下元素: + - constructor:在通过构造方法实例化类时,将select语句返回的result注入到构造方法中 + - idArg:标识为id的参数,将result标识为id可以提高整体的性能 + - arg:将result注入到constructor中 + - id:标识为id的result,将result标识为id可以提高整体性能 + - result:select语句返回的result,将被注入到返回POJO类型的field + - association:复杂关联,返回results中的部分results将会被包装到这种关联中 + - 关联被嵌套在resultMap中,关联本身可以是resultMap,或者可以引用一个外部resultMap + - collection:复杂类型的集合 + - 集合被嵌套在resultMap中,其本身可以是一个resultMap,或者其可以引用要给外部的resultMap + - discrimination:鉴别器,可以通过结果值来判断使用哪个resultMap + - case:条件选项,当值为xx时使用特定的resultMap + - case同样是一个resultMap,case可以是resultMap本身,也可以引用其他的resultMap +- resultMap元素可包含如下属性: + - id:在命名空间内,可以通过该id引用该resultMap + - type:java类的全限定类名或typeAlias + - autoMapping:为resultMap开启或关闭自动映射 +### id和result元素 +id和result元素是resultMap的基础,id和result都将一个column字段值映射到java类型的field域。 +> id和result标签的区别是,当result被标识为id时,该result将会被看作对象的标识符,在比较对象时id result将会被使用。这样可以提高整体性能,尤其是在缓存或是嵌套resultMap时。 + +- id和result元素的属性: + - property:column映射到的java对象的field + - column:数据库表的列名 + - javaType:java类型的全限定类名或者typeAlias,通常映射到java bean时mybatis可以推断java类型,但是当映射到HashMap时,需要显式指定javaType + - jdbcType + - typeHandler +### constructor元素 +当想要构建不可变对象时,可以通过constructor属性来向构造方法中注入result构建对象。 +```java +public class User { + //... + public User(Integer id, String username, int age) { + //... + } +//... +} +``` +```xml + + + + + + +``` +- constructor元素的属性: + - column:数据库表列名 + - javaType + - jdbcType + - typeHandler + - resultMap + - select +### association元素 +association元素通常用来标识“has-a”的关系,通过关联,有两种方式将数据加载到关联中去 +- 嵌套select:执行另一个sql statement并且返回预期的复杂对象 +- 嵌套result:通过嵌套的result mapping来处理连接结果的重复子集 +#### 嵌套select +嵌套select,其会先返回查询的主表数据,对关联的子表数据会通过association标签指定的select属性值来再次执行sql查找预期数据并组装城复杂对象 +当使用嵌套select来处理association时,association标签具有如下属性: +- column:数组库表的列名,该列保存的值将会被传递给嵌套的statement作为输入参数。 +> 当想要传递复合key作为输入参数时,可以通过column="{prop1=col1,prop2=col2}"的形式来传递参数,这会导致传递给嵌套statement的参数对象的prop1和prop2属性被设置 +- select:会返回预期复杂类型的其他select statement标签的id +- fetchType:可选参数,可以为“lazy”或者“eager”,会覆盖全局配置中的lazyLoadingEnable +```xml + + + + + + + + +``` +> 对于嵌套select方法来加载association,其会导致***N+1***的问题 +> 例如,当一条select语句返回一个数据集时,通过嵌套select语句来加载association,其会: +> #### 对数据集中的每一条数据,都会再次执行一个select语句来为该条数据加载association信息,当数据集中数据条数很多时,如果未设置延迟加载,那么成百上千的sql语句被执行,会严重影响性能 + +#### 嵌套result +不同于嵌套select先查主表信息后再次执行sql语句查询关联子表数据,嵌套result会通过A JOIN B关联来通过查询需要的主表信息和子表信息 +嵌套result不需要像嵌套select一样分次执行sql语句 +> #### 使用嵌套result时association可以使用的属性 +> - resultMap:指定用于将results加载到association的resultMap id +> - columnPrefix:可以将具有某一个前缀的列映射到外部的resultMap中,从而即使在select语句中重新为列名定义了alias(添加了前缀,如"co_"等),还是能复用外部的同一个resultMap +> - notNullColumn:该属性可以指定多个列,只有当指定的多个列中任何一个不为空时,mybatis +> 才会创建该子对象 +> - autoMapping:开启或关闭自动映射,该属性对外部映射无效,故而不能搭配select或者resultMap使用 +```xml + + + + + + + + + + + + + + + + + +``` +> 在嵌套result中,id元素(被标识为id的result)非常重要,应该指定一个或多个属性标识为id来唯一标识该对象. +> 当没有指定id result时,mybatis也会正常运行,但是会产生严重的性能问题 +> 应尽可能的将能唯一标识该结果的最小字段集标识为id(可以选择主键或符合主键) + +对于嵌套result,除了上述的办法,还可以将association对应的resultMap直接嵌套写入association的子标签中,样例如下: +```xml + + + + + + + + + + + +``` +共同作者:通过columnPrefix属性复用外部resultMap的情况 +```xml + + + + + + + + + + + + + + + + + + +``` +### collection元素 +collection元素用来处理A类中含有List<B>属性的情况,其用法基本和association元素相同 +```xml + + + + + +``` +和association元素相同,coolection元素也可以通过嵌套select或者嵌套result来映射复杂类型 +#### collection元素的嵌套select +```xml + + + + + + + +``` +#### collection元素的嵌套result +```xml + + + + + + + + + + + + + + + + + + + + + + + + +``` +## discriminator +某些情况下,单条sql查询语句会返回具有不同类型的结果集(如不同的Car数据,但是有卡车和轿车的区别). +discriminator可以通过某些字段的值来选择性采用某些resultMap,并且支持继承层次. +```xml + + + + + + + + + + + + + + + + + + + +``` +如果想要在使用carResult时也执行vehicleResult中的映射,可以在carResult中指定extends属性 +```xml + + + +``` +## automapping +对于automapping,其会获取resultset中的字段名并且在java对象中查找同名的property(忽略大小写,如果想将数据表字段名的underscore形式映射到java类的camelCase属性,可以在mybatis config文件中添加mapUnderscoreToCamelCase配置). +即使手动指定了resultMap,automapping一样会发挥作用.对于每个指定的resultMap,所有在resultSet中出现过的字段如果没有在resultMap中手动为其配置一个映射,那么会对该字段执行automapping. +```xml + + + + + + +``` +> 自动映射有三种等级,可以在mybatis config文件中进行配置 +> - NONE:禁用自动配置 +> - PARTIAL:会对除嵌套result映射以外的属性进行自动行摄 +> - FULL:会对所有属性执行自动映射 +```xml + + + + + + + + + + +``` +默认情况下,automapping级别为partial,并且可以为resultMap元素指定autoMapping属性为false来为该resultMap关闭autoMapping +```xml + + + +``` +## mybatis缓存 +默认情况下,mybatis只启用了会话级别的缓存,其缓存数据只在sqlsession期间有效. +如果想要开启二级缓存,只需要在接口对应的mapper文件中添加一行 +```xml + +``` +该行语句的效果如下: +- 所有select语句的执行结果都会被缓存 +- 所有insert/update/delete语句都会清除缓存 +- 缓存会通过LRU算法来作为淘汰策略 +- 缓存不定期的刷新 +- 缓存会存储1024个引用指向列表或者对象(不管查询方法会返回哪种类型) +- 缓存被设计为可读写缓存,通过缓存获取到的对象并不会和其他线程共享,故而可以安全的对获取到的对象进行共享 +> 缓存只会作用于位于mapper文件中的语句,如果使用java api和xml混合的方式,那么在共用接口声明的sql语句并不会被缓存. + +可以自定义二级缓存 +```xml + + +``` +对于二级缓存,可以使用的淘汰策略有: +- LRU:默认策略 +- FIFO +- SOFT:基于垃圾回收器状态和软引用规则移除对象 +- WEAK:弱引用,基于垃圾回收器状态和弱引用规则移出对象 + +flushInterval: +- 定义cache的刷新间隔,单位为ms,默认没有刷新间隔 + +size: +- 定义缓存大小,为引用数目,默认为1024 + +readOnly: +- true:只读,会给所有调用者返回相同的缓存对象实例,调用者如果进行修改可能会产生线程安全问题 +- false:可读写,会给所有调用者返回缓存对象的拷贝,调用者可以安全的对返回对象进行修改,默认情况下缓存类型是可读写的 + +> ### 二级缓存的事务性 +> 当sqlsession完成并且提交,或完成并回滚时,即使没有执行flushCache=true的insert/delete/update,缓存也会进行刷新 + +> select/update/insert/delete +> sql语句都会有flushCache属性,指定是否在执行完成后刷新cache,默认情况下,select标签的flushCache属性为false,insert/update/delete语句的flushCache属性为true + +### cache-ref +默认情况下,某一命名空间中的语句只会对当前命名空间中的cache进行刷新.但是,如果想在多个命名空间(Mapper)之间共享缓存,可以通过cache-ref来引用其他命名空间中的缓存. +```xml + +``` + +## 动态sql +### if +可以通过<if>标签根据条件向where子句中添加查询条件 +```xml + +``` +### choose、when、otherwise +不同于if会检测所有的条件,choose、when、otherwise只会从多个条件中选择一个使用,类似于java中的switch。 +```xml + + +``` +### where、trim、set +#### where +对于where标签,只有在子元素返回任何内容不为空的前提下才会插入where子句,并且,如果子句开头返回的内容为”or“或者”and“,则where标签会删除子句开头的”and“或者”or“ +```xml + +``` +#### trim +如果where标签并不能满足场景需求,那么可以通过trim来自定义想要实现的场景需求。 +和where标签等价的trim标签为 +```xml + + ... + +``` +对于trim标签,其会匹配prefixOverrides属性中指定的内容,文本内容通过'|'符号分隔,并将开头prefixOverrides中的内容删除,并在首部加入prefix属性指定的内容 +#### set +set标签用于update语句中动态设置属性,并且删除额外的逗号 +```xml + + update Author + + username=#{username}, + password=#{password}, + email=#{email}, + bio=#{bio} + + where id=#{id} + +``` +与set标签等效的trim标签是 +```xml + + + ... + +``` +### foreach +foreach标签允许在动态sql中遍历集合,通常用于构建in条件。 +```xml + +``` +> 可以通过foreach执行便利操作,如果foreach指定的collection为array,那么index对应的时数组下标 +> 若collection为map类型,那么index对应的是entry中的key,而value对应的是value + +## Java API +### SqlSession +SqlSession是Mybatis Java主要的接口,可以通过SqlSession来执行sql语句、获取Mapper实例、管理事务。 +### SqlSessionFactory +SqlSession实例是通过SqlSessionFactory方法来创建的,SqlSessionFactory对象包含创建SqlSession实例的各种方法。 +### SqlSessionFactoryBuilder +SqlSessionFactory本身是由SqlSessionFactoryBuilder来创建的,SqlSessionFactoryBuilder可以通过xml配置、注解或java配置代码来创建SqlSessionFactory。 +### SqlSessionFactoryBuilder详解 +SqlSessionFactoryBuilder有多个build方法,调用不同的build方法可以根据不同的资源创建SqlSessionFactory +```java +// 接受一个InputStream,该InputStream指向mybatis配置的xml文件 +// * environment参数是可选的,决定加载哪个环境,如果指定environment不存在,那么会抛出一个错误 +// 如果调用的build方法中没有environment参数,那么会使用默认的environment配置 +// * properties参数同样是可选的,如果调用的build方法制定了properties参数,那么properties参数中指定 +// 的属性可以在mybatis配置文件中使用,以${propertyName}的形式使用 +SqlSessionFactory build(InputStream inputStream) +SqlSessionFactory build(InputStream inputStream, String environment) +SqlSessionFactory build(InputStream inputStream, Properties properties) +SqlSessionFactory build(InputStream inputStream, String env, Properties props) +``` +> #### 配置文件中加载properties的优先顺序 +> * 在mybatis config配置文件的properties元素中指定的properties最先被加载 +> * 在properties元素的resouce属性或url属性中指定指定的properties文件中导入的属性其次加载,这次加载的properties属性会覆盖上一步中指定的属性 +> * 向build方法中传入的properties参数会最后被读取,并且覆盖之前所有的同名属性,具有最高的优先级 +> **故而,properties优先级顺序为“properties参数>url/resource属性指定的properties>properties元素体中指定的属性”** + +#### 创建SqlSessionFactory的示例 +```java +String resource = "org/mybatis/builder/mybatis-config.xml"; +InputStream inputStream = Resources.getResourceAsStream(resource); +SqlSessionFactoryBuilder builder = new SqlSessionFactoryBuilder(); +SqlSessionFactory factory = builder.build(inputStream); +``` +#### 通过Configuration来创建SqlSessionFactory +之前在mybatis-config.xml中配置的所有mybatis配置属性都可以在Configuration中通过Java API来进行配置 +```java +DataSource dataSource = BaseDataTest.createBlogDataSource(); +TransactionFactory transactionFactory = new JdbcTransactionFactory(); + +Environment environment = new Environment("development", transactionFactory, dataSource); + +Configuration configuration = new Configuration(environment); +configuration.setLazyLoadingEnabled(true); +configuration.setEnhancementEnabled(true); +configuration.getTypeAliasRegistry().registerAlias(Blog.class); +configuration.getTypeAliasRegistry().registerAlias(Post.class); +configuration.getTypeAliasRegistry().registerAlias(Author.class); +configuration.addMapper(BoundBlogMapper.class); +configuration.addMapper(BoundAuthorMapper.class); + +SqlSessionFactoryBuilder builder = new SqlSessionFactoryBuilder(); +SqlSessionFactory factory = builder.build(configuration); +``` +### SqlSessionFactory构建SqlSession实例 +SqlSessionFactory同样有多种方法来创建SqlSession实例 +- #### SqlSession openSession() +默认openSession()无参数的方法会创建具有如下特征的SqlSession +> 1. 事务作用域将会开启(自动提交关闭) +> 2. 从当前环境配置的Datasource中获取Connection +> 3. 事务的隔离级别将会使用驱动或者数据源的默认设置 +> 4. PreparedStatement(预处理语句)并不会被复用,也不会批量更新语句 + +- #### openSesion()的非空参方法 +```java +SqlSession openSession(boolean autoCommit) +SqlSession openSession(Connection connection) +SqlSession openSession(TransactionIsolationLevel level) +SqlSession openSession(ExecutorType execType, TransactionIsolationLevel level) +SqlSession openSession(ExecutorType execType) +SqlSession openSession(ExecutorType execType, boolean autoCommit) +SqlSession openSession(ExecutorType execType, Connection connection) +``` +#### openSession参数 +- autoCommit : boolean类型,为true则开启自动提交功能 +- connection :Connection类型,如果要使用自己的connection实例,可以将其传递给connection参数 +- level :TransactionIsolationLevel枚举类型,控制事务的隔离级别,可选的五个隔离级别为(NONE,READ_UNCOMMITTED,READ_COMMITTED,REPEATABLE_READ,SERIALIZABLE) +- ExecutorType :执行器类型 + - ExecutorType.SIMPLE:该类型的执行器为每个语句创建一个新的预处理语句(PreparedStatement) + - ExecutorType.REUSE:该类型的执行器会复用预处理语句 + - ExecutorType.BATCH:该类型的执行器会批量执行所有的更新语句,如果select语句位于多条update语句之间,那么必要时会将多条更新语句分割成不同的batch,以便于理解 +> #### batch(批量处理) +> batch会将相关的sql语句分组到一个batch文件中,并且一次提交到database server端。 +> - addBatch方法会将单独的sql语句添加到batch中,而executeBatch方法会执行batch中所有被分组到一起的sql语句 +> executeBatch方法会返回一个Integer数组,数组中每一个元素代表每个update语句的update count diff --git a/mysql/mysql底层/binlog.md b/mysql/mysql底层/binlog.md index effcf2d..1c30d42 100644 --- a/mysql/mysql底层/binlog.md +++ b/mysql/mysql底层/binlog.md @@ -1,6 +1,6 @@ -# binlog 日志 -* ## binlog(二进制日志) - * 定义:binlog日志文件也称之为变更日志文件,记录了数据库记录的所有DDL、DML等数据库更新事件的语句,但是不包括任何没有更新数据的语句。 - * 用途: - * 数据恢复:如果mysql数据库进程意外停止,可以通过binlog中的记录来恢复数据库中的数据 +# binlog 日志 +* ## binlog(二进制日志) + * 定义:binlog日志文件也称之为变更日志文件,记录了数据库记录的所有DDL、DML等数据库更新事件的语句,但是不包括任何没有更新数据的语句。 + * 用途: + * 数据恢复:如果mysql数据库进程意外停止,可以通过binlog中的记录来恢复数据库中的数据 * 数据复制:可以通过binlog来实现mysql数据库的主从一致 \ No newline at end of file diff --git a/mysql/mysql底层/mvcc.md b/mysql/mysql底层/mvcc.md index 97b5fa9..5580fe3 100644 --- a/mysql/mysql底层/mvcc.md +++ b/mysql/mysql底层/mvcc.md @@ -1,49 +1,49 @@ -# 多版本并发控制 -* ## 多版本并发控制解决的问题: - * 在多个事务并发访问资源时,可能会发生并发安全问题。为了解决多个事务同时对资进行读写的问题,可以采用如下解决方案: - * 多事务同时对资源进行读取:并发安全 - * 多事务同时对资源进行写操作:非并发安全,可以对写操作进行加锁来解决并发安全问题 - * 多事务同时对资源进行读和写操作:非并发安全 - 1. 对读操作和写操作都加锁:性能较差 - 2. 通过MVCC机制来解决并发读写的问题,对写操作加锁,并用MVCC机制来使读操作读到先前的值 -* ## 多版本并发控的概念: - * MVCC通过数据库行的多个版本管理来实现数据库的并发控制。当一个写事务正在更新数据行的值时,通过MVCC机制,可以在另一个事物对数据行进行读操作时读取到被写事务更新之前的值。 - * 通过MVCC机制,可以更好的解决事务在读-写操作同时发生时的性能问题。MVCC可以避免在写事务时另一个读事务必须等待当前写事务释放排他锁,而是可以通过MVCC读取资源被写事务修改之前的值。 - -* ## mysql中读操作的种类: - * 当前读:读取数据库中当前的值,为了解决并发安全问题,需要对读操作进行加锁操作,可以尝试加排他锁或者共享锁,当当前事务对资源进行写操作时,读事务会阻塞直到写事务释放锁: - ```mysql - # 为select语句加上共享锁 - select * from user where... lock in share mode - - # 为select语句加上排他锁 - select * from user where... for update - ``` - * 快照读:mysql中默认select语句的读取方式,当当前事务对资源进行读操作时,如果另一个事务正在对资源进行写操作,那么读操作并不会阻塞而是会读取资源被写事务修改之前的值 - -* ## MVCC原理 - * 隐藏字段:对于使用innodb存储引擎的表,其聚簇索引包含两个隐藏字段: - 1. trx_id:每个事务对每条记录进行更改时,该trx_id字段都会记载最后一次修改该行记录的事务id - 2. roll_pointer:指向undo log中关于修改该条数据的记录 - * ReadView:ReadView是MVCC机制中事务对数据进行快照读时产生的一个读视图。 - * ReadView原理: - * 当事务开启时,会产生当前数据库系统的一个快照,innodb为每个事务构造了一个数组,用来维护当前系统中活跃事务的ID(活跃事务为当前开启了但是尚未提交的事务id) - * ReadView中保存了如下信息: - * creator_trx_id:创建当前ReadView的事务id - * trx_ids:记录创建失误时当前mysql系统中活跃的事务id集合(已经开始但是还没有被提交的事务id) - * up_limit_id:trx_ids中最小的事务id - * low_limit_id:表示生成ReadView中当前系统应该分配给下一个事务的id - * MVCC仅针对读已提交和可重复读的情况,在读已提交的情况下,事务中每执行一次select操作,ReadView都会重复生成;而在可重复读的隔离级别下,事务仅仅会在第一次select操作时生成ReadView - * MVCC细节: - * 当select语句想要对一条记录中的数据进行读取时,首先会查看记录的trx_id是是否对当前事务的读操作是可见的,判断事务是否可见的标准如下所示: - * 如果记录的trx_id大于low_limit_id,那么说明在创建ReadView时对记录进行修改的事务还没有被创建,当然修改对当前读事务来说是不可见的 - * 如果trx_id小于up_limit_id,那么说明对该记录进行修改的事务id小于创建ReadView时最小的活跃事务id,在创建ReadView时修改记录的事物已经被提交,修改对当前事务来说可见 - * 如果trx_id位于up_limit_id和low_limit_id之间,那么: - * trx_id如果与trx_ids中保存的某个活跃事务id相同,那么说明在创建ReadView时修改事务的id尚未被提交,修改对当前失误不可见 - * 如果trx_id与trx_ids中每个活跃事物id都不相同,那么修改事务在创建ReadView事物之时已经被提交,修改对当前事务可见 - * 根据上述规则,如果想要读取的记录trx_id对当前事务来说可见,那么获取该事务id所对应的数据值;如果trx_id对当前ReadView来说不可见,那么沿着roll_pointer沿着undo log向前寻找,知道找到对当前ReadView可见的事务id;如果undo log中所有的事务id对当前ReadView来说都不可见,那么对当前事物来说数据表中该条记录并不可见 - -* ## MVCC与幻读问题 - * MVCC仅在读已提交和可重复读的情况下起作用,而关于幻读问题,MVCC仅在可重复读的隔离级别下解决。 - * 在可重复读的隔离级别下,ReadView仅仅在第一次select语句时生成,故而在同一事务中多次读取之间,其他事务插入了新数据,那么该新数据对应的trx_id在readView创建时应该处于活跃或者未创建的状态,故而对ReadView所对应事物来说,即使其他事务插入了新数据,那么新插入的数据也不可见。 +# 多版本并发控制 +* ## 多版本并发控制解决的问题: + * 在多个事务并发访问资源时,可能会发生并发安全问题。为了解决多个事务同时对资进行读写的问题,可以采用如下解决方案: + * 多事务同时对资源进行读取:并发安全 + * 多事务同时对资源进行写操作:非并发安全,可以对写操作进行加锁来解决并发安全问题 + * 多事务同时对资源进行读和写操作:非并发安全 + 1. 对读操作和写操作都加锁:性能较差 + 2. 通过MVCC机制来解决并发读写的问题,对写操作加锁,并用MVCC机制来使读操作读到先前的值 +* ## 多版本并发控的概念: + * MVCC通过数据库行的多个版本管理来实现数据库的并发控制。当一个写事务正在更新数据行的值时,通过MVCC机制,可以在另一个事物对数据行进行读操作时读取到被写事务更新之前的值。 + * 通过MVCC机制,可以更好的解决事务在读-写操作同时发生时的性能问题。MVCC可以避免在写事务时另一个读事务必须等待当前写事务释放排他锁,而是可以通过MVCC读取资源被写事务修改之前的值。 + +* ## mysql中读操作的种类: + * 当前读:读取数据库中当前的值,为了解决并发安全问题,需要对读操作进行加锁操作,可以尝试加排他锁或者共享锁,当当前事务对资源进行写操作时,读事务会阻塞直到写事务释放锁: + ```mysql + # 为select语句加上共享锁 + select * from user where... lock in share mode + + # 为select语句加上排他锁 + select * from user where... for update + ``` + * 快照读:mysql中默认select语句的读取方式,当当前事务对资源进行读操作时,如果另一个事务正在对资源进行写操作,那么读操作并不会阻塞而是会读取资源被写事务修改之前的值 + +* ## MVCC原理 + * 隐藏字段:对于使用innodb存储引擎的表,其聚簇索引包含两个隐藏字段: + 1. trx_id:每个事务对每条记录进行更改时,该trx_id字段都会记载最后一次修改该行记录的事务id + 2. roll_pointer:指向undo log中关于修改该条数据的记录 + * ReadView:ReadView是MVCC机制中事务对数据进行快照读时产生的一个读视图。 + * ReadView原理: + * 当事务开启时,会产生当前数据库系统的一个快照,innodb为每个事务构造了一个数组,用来维护当前系统中活跃事务的ID(活跃事务为当前开启了但是尚未提交的事务id) + * ReadView中保存了如下信息: + * creator_trx_id:创建当前ReadView的事务id + * trx_ids:记录创建失误时当前mysql系统中活跃的事务id集合(已经开始但是还没有被提交的事务id) + * up_limit_id:trx_ids中最小的事务id + * low_limit_id:表示生成ReadView中当前系统应该分配给下一个事务的id + * MVCC仅针对读已提交和可重复读的情况,在读已提交的情况下,事务中每执行一次select操作,ReadView都会重复生成;而在可重复读的隔离级别下,事务仅仅会在第一次select操作时生成ReadView + * MVCC细节: + * 当select语句想要对一条记录中的数据进行读取时,首先会查看记录的trx_id是是否对当前事务的读操作是可见的,判断事务是否可见的标准如下所示: + * 如果记录的trx_id大于low_limit_id,那么说明在创建ReadView时对记录进行修改的事务还没有被创建,当然修改对当前读事务来说是不可见的 + * 如果trx_id小于up_limit_id,那么说明对该记录进行修改的事务id小于创建ReadView时最小的活跃事务id,在创建ReadView时修改记录的事物已经被提交,修改对当前事务来说可见 + * 如果trx_id位于up_limit_id和low_limit_id之间,那么: + * trx_id如果与trx_ids中保存的某个活跃事务id相同,那么说明在创建ReadView时修改事务的id尚未被提交,修改对当前失误不可见 + * 如果trx_id与trx_ids中每个活跃事物id都不相同,那么修改事务在创建ReadView事物之时已经被提交,修改对当前事务可见 + * 根据上述规则,如果想要读取的记录trx_id对当前事务来说可见,那么获取该事务id所对应的数据值;如果trx_id对当前ReadView来说不可见,那么沿着roll_pointer沿着undo log向前寻找,知道找到对当前ReadView可见的事务id;如果undo log中所有的事务id对当前ReadView来说都不可见,那么对当前事物来说数据表中该条记录并不可见 + +* ## MVCC与幻读问题 + * MVCC仅在读已提交和可重复读的情况下起作用,而关于幻读问题,MVCC仅在可重复读的隔离级别下解决。 + * 在可重复读的隔离级别下,ReadView仅仅在第一次select语句时生成,故而在同一事务中多次读取之间,其他事务插入了新数据,那么该新数据对应的trx_id在readView创建时应该处于活跃或者未创建的状态,故而对ReadView所对应事物来说,即使其他事务插入了新数据,那么新插入的数据也不可见。 * 而在读已提交的隔离级别下,ReadView在每次读操作的情况下都会被创建,故而在两次读操作之间,如果新的数据被插入,那么新插入的数据对后一次读操作创建的ReadView来说是已经提交的数据,是可见的。故而MVCC在读已提交的隔离级别下并不能够解决幻读的问题。 \ No newline at end of file diff --git a/mysql/mysql底层/mysql加锁.md b/mysql/mysql底层/mysql加锁.md index 0333c59..587edb7 100644 --- a/mysql/mysql底层/mysql加锁.md +++ b/mysql/mysql底层/mysql加锁.md @@ -1,64 +1,64 @@ -# mysql锁 -* ## msyql中锁的分类 - * 从数据库操作的类型划分,mysql中的锁可以分为读锁/共享锁和写锁/排他锁 - * 读锁(S锁,Share) - * 写锁(X锁,Exclusive) - * 写锁和读锁示例 - ```mysql - # 为语句加上写锁 - select * from table_name for udpate - - # 为查询语句加上读锁 - select * from table_name lock in share mode - # or - select * from table_name for share(mysql 8.0新增语法) - ``` - * 在获取读锁或者写锁之后,只有当事务结束之后,获取到的读锁或者写锁才会被释放。在innodb中,for update或者for share语句只会为查询匹配的行数据上锁,并且,当事务结束或者回滚之后,会释放为数据行所加的排他锁或者共享锁 - * 在对mysql中数据进行写操作(UPDATE、INSERT、DELETE)时,会通过如下方式加锁: - * DELETE操作:对mysql表中数据执行DELETE操作,需要为要删除数据添加排他锁(X锁) - * UPDATE操作:对mysql表中数据执行UPDATE操作分如下情况: - * update操作不改变记录的键值,且更新的列在更新前后占用的空间未发生变化: - * 此时会在B+数中定位到该条记录,并且为该条记录添加X锁,在修改完成之后释放X锁 - * update操作没有修改键值,但是更新前后该条记录的某一列占用空间发生了变化: - * 此时,会先对原先的记录加上X锁,并进行DELETE操作,并且在DELETE操作后,通过INSERT操作并添加隐式锁来插入修改后的数据 - * update操作修改了键值: - * 同样,也会通过先DELETE再INSERT的操作来更新数据 - * INSERT操作:通过添加隐式锁的操作来保证mysql中多事务的并发安全 - * 从mysql数据库的粒度来对锁进行划分 - * 表锁: - * 表级锁会锁住mysql中的整张表,其粒度较大,受其影响并发性能较低 - * 表级锁是mysql中最基本的加锁策略,并不依赖于存储引擎,不管是innodb还是myisam都支持表级锁 - * 表级锁的使用场景: - * 通过的select或者update、delete、insert操作innodb并不会加表级锁,但是对于alter table、drop table这类ddl语句对表的结构进行修改时,需要对其表的元数据进行加锁(MDL),对元数据加锁的过程中,想要对该表中数据进行select、delete、update、insert的操作会被阻塞 - * 对表锁进行加锁的语句: - ```mysql - # 对表锁进行加锁 - # 在对表进行上锁操作后,无法再去读取其他未上锁的表 - lock tables table_name read/write - - # 对上锁的表进行释放操作 - unlock tables; - ``` - * 对于myisam存储引擎,读操作之前会为涉及到的表加上读锁,并且在读操作执行完成之后释放上锁的表;而对于写操作,myisam存储引擎会在写操作执行之前为涉及到的表加上写锁。反之,innodb存储引擎在对数据进行查找和修改时不会在表级别加锁,而是会在更细的粒度上为行数据进行加锁,以此来提高程序的并发性能。 - * 元数据锁(MDL):对于表结构的修改(DDL操作),会对其元数据锁进行加锁,而对于该表的增删改操作,则是会默认加上元数据的读锁。故而在对表结构进行修改时,想要对表中数据进行増删改操作需要获取其元数据的读锁,此时对表数据的増删改操作会被阻塞。 - * 意向锁:意向锁通常用来简化行级锁和表级锁是否兼容的判断操作。如果为一个表中某条数据加上行级锁,那么innodb存储引擎会自动的为该行记录所在的页和表加上页级和表级的意向锁,那么当接下来有事务想要对该表进行表级加锁操作时,就无需查看该表中所有数据来判断是否存在表中数据的锁和表级锁冲突。只需判断该表的意向锁和想要加上的表级锁是否冲突即可。 - * 行锁 - * 相对与表锁,行锁的粒度更小,故而其并发度更高,并发性能更好。但是行锁可能会造成死锁问题,加锁的开销也更大。 - * 是否支持行锁取决与存储引擎,比如myisam不支持行锁但是innodb却支持行锁 - * 行锁种类: - * 记录锁:记录锁针对于单条记录,在一个事物中,如果存在update、delete等修改操作,其默认会获得想要修改的那条记录的记录锁,并且在执行修改操作之后才会被释放。若一个事务中对某条记录调用update操作,那么直到当前事务提交或者回滚前,若其他事务想要修改同一行记录,会进入阻塞状态,知道持有记录写锁的退出才能继续执行。 - * 间隙锁:间隙锁对记录的范围进行加锁。间隙锁是不冲突的,多个事务可以同时持有某一个范围的间隙锁。用间隙锁或者mvcc机制都能够解决死锁问题。但是间隙锁可能会导致死锁问题,如果多个事务各自持有自己范围的间隙锁,并同时向对方持有间隙锁的范围插入数据,此时两个事务都会等待对方释放间隙锁,在这种情况下会发生死锁问题。 - * 临键锁(next-key lock):在锁住某条记录的同时也锁定某个范围内的数据,使之无法被其他事务插入数据 - * 页锁:在页面的粒度上进行上锁 - * 在粒度上,不同层级的所的数量是有上限的,例如innodb,其优先会选用行锁来进行加锁。当行锁数量过多,占用空间超过表空间上限时,会进行锁升级的行为。行锁会升级为页面锁,此时锁的粒度会增加,锁花费的开销也会变小,但是粒度变大后并发性能也会变低。 - * 通过对待锁的方式进行划分 - * 乐观锁:乐观锁假设每次对数据进行修改时,其他事务不会访问数据,故而不会对数据正真的加锁,只是会在修改时检查数据是否被其他事务修改过。 - * 乐观锁的实现方式: - * cas - * 版本号机制 - * 悲观锁: - * 悲观锁假设一个事务在对数据进行修改时,其他事务也会对数据进行修改,故而在每次修改数据时都会对要修改的数据进行加锁,悲观锁是通过mysql内部提供的锁机制来实现的 - * 通过加锁方式进行划分: - * 隐式锁:隐式锁通常用于插入操作。在某一个事务在对所记录进行插入操作时,如果其他事务想要访问该条记录,会查看最后修改该条记录的事务id是否是活跃的事务,如果是活跃事务,那么其会帮助插入事务创建一个X锁并且让自己进入阻塞状态。 - * 隐式锁是一种延迟加锁的机制,只有当有其他事务想要访问未提交事务插入的记录时,隐式锁才会被创建。该机制能够有效减少创建锁的数量。 +# mysql锁 +* ## msyql中锁的分类 + * 从数据库操作的类型划分,mysql中的锁可以分为读锁/共享锁和写锁/排他锁 + * 读锁(S锁,Share) + * 写锁(X锁,Exclusive) + * 写锁和读锁示例 + ```mysql + # 为语句加上写锁 + select * from table_name for udpate + + # 为查询语句加上读锁 + select * from table_name lock in share mode + # or + select * from table_name for share(mysql 8.0新增语法) + ``` + * 在获取读锁或者写锁之后,只有当事务结束之后,获取到的读锁或者写锁才会被释放。在innodb中,for update或者for share语句只会为查询匹配的行数据上锁,并且,当事务结束或者回滚之后,会释放为数据行所加的排他锁或者共享锁 + * 在对mysql中数据进行写操作(UPDATE、INSERT、DELETE)时,会通过如下方式加锁: + * DELETE操作:对mysql表中数据执行DELETE操作,需要为要删除数据添加排他锁(X锁) + * UPDATE操作:对mysql表中数据执行UPDATE操作分如下情况: + * update操作不改变记录的键值,且更新的列在更新前后占用的空间未发生变化: + * 此时会在B+数中定位到该条记录,并且为该条记录添加X锁,在修改完成之后释放X锁 + * update操作没有修改键值,但是更新前后该条记录的某一列占用空间发生了变化: + * 此时,会先对原先的记录加上X锁,并进行DELETE操作,并且在DELETE操作后,通过INSERT操作并添加隐式锁来插入修改后的数据 + * update操作修改了键值: + * 同样,也会通过先DELETE再INSERT的操作来更新数据 + * INSERT操作:通过添加隐式锁的操作来保证mysql中多事务的并发安全 + * 从mysql数据库的粒度来对锁进行划分 + * 表锁: + * 表级锁会锁住mysql中的整张表,其粒度较大,受其影响并发性能较低 + * 表级锁是mysql中最基本的加锁策略,并不依赖于存储引擎,不管是innodb还是myisam都支持表级锁 + * 表级锁的使用场景: + * 通过的select或者update、delete、insert操作innodb并不会加表级锁,但是对于alter table、drop table这类ddl语句对表的结构进行修改时,需要对其表的元数据进行加锁(MDL),对元数据加锁的过程中,想要对该表中数据进行select、delete、update、insert的操作会被阻塞 + * 对表锁进行加锁的语句: + ```mysql + # 对表锁进行加锁 + # 在对表进行上锁操作后,无法再去读取其他未上锁的表 + lock tables table_name read/write + + # 对上锁的表进行释放操作 + unlock tables; + ``` + * 对于myisam存储引擎,读操作之前会为涉及到的表加上读锁,并且在读操作执行完成之后释放上锁的表;而对于写操作,myisam存储引擎会在写操作执行之前为涉及到的表加上写锁。反之,innodb存储引擎在对数据进行查找和修改时不会在表级别加锁,而是会在更细的粒度上为行数据进行加锁,以此来提高程序的并发性能。 + * 元数据锁(MDL):对于表结构的修改(DDL操作),会对其元数据锁进行加锁,而对于该表的增删改操作,则是会默认加上元数据的读锁。故而在对表结构进行修改时,想要对表中数据进行増删改操作需要获取其元数据的读锁,此时对表数据的増删改操作会被阻塞。 + * 意向锁:意向锁通常用来简化行级锁和表级锁是否兼容的判断操作。如果为一个表中某条数据加上行级锁,那么innodb存储引擎会自动的为该行记录所在的页和表加上页级和表级的意向锁,那么当接下来有事务想要对该表进行表级加锁操作时,就无需查看该表中所有数据来判断是否存在表中数据的锁和表级锁冲突。只需判断该表的意向锁和想要加上的表级锁是否冲突即可。 + * 行锁 + * 相对与表锁,行锁的粒度更小,故而其并发度更高,并发性能更好。但是行锁可能会造成死锁问题,加锁的开销也更大。 + * 是否支持行锁取决与存储引擎,比如myisam不支持行锁但是innodb却支持行锁 + * 行锁种类: + * 记录锁:记录锁针对于单条记录,在一个事物中,如果存在update、delete等修改操作,其默认会获得想要修改的那条记录的记录锁,并且在执行修改操作之后才会被释放。若一个事务中对某条记录调用update操作,那么直到当前事务提交或者回滚前,若其他事务想要修改同一行记录,会进入阻塞状态,知道持有记录写锁的退出才能继续执行。 + * 间隙锁:间隙锁对记录的范围进行加锁。间隙锁是不冲突的,多个事务可以同时持有某一个范围的间隙锁。用间隙锁或者mvcc机制都能够解决死锁问题。但是间隙锁可能会导致死锁问题,如果多个事务各自持有自己范围的间隙锁,并同时向对方持有间隙锁的范围插入数据,此时两个事务都会等待对方释放间隙锁,在这种情况下会发生死锁问题。 + * 临键锁(next-key lock):在锁住某条记录的同时也锁定某个范围内的数据,使之无法被其他事务插入数据 + * 页锁:在页面的粒度上进行上锁 + * 在粒度上,不同层级的所的数量是有上限的,例如innodb,其优先会选用行锁来进行加锁。当行锁数量过多,占用空间超过表空间上限时,会进行锁升级的行为。行锁会升级为页面锁,此时锁的粒度会增加,锁花费的开销也会变小,但是粒度变大后并发性能也会变低。 + * 通过对待锁的方式进行划分 + * 乐观锁:乐观锁假设每次对数据进行修改时,其他事务不会访问数据,故而不会对数据正真的加锁,只是会在修改时检查数据是否被其他事务修改过。 + * 乐观锁的实现方式: + * cas + * 版本号机制 + * 悲观锁: + * 悲观锁假设一个事务在对数据进行修改时,其他事务也会对数据进行修改,故而在每次修改数据时都会对要修改的数据进行加锁,悲观锁是通过mysql内部提供的锁机制来实现的 + * 通过加锁方式进行划分: + * 隐式锁:隐式锁通常用于插入操作。在某一个事务在对所记录进行插入操作时,如果其他事务想要访问该条记录,会查看最后修改该条记录的事务id是否是活跃的事务,如果是活跃事务,那么其会帮助插入事务创建一个X锁并且让自己进入阻塞状态。 + * 隐式锁是一种延迟加锁的机制,只有当有其他事务想要访问未提交事务插入的记录时,隐式锁才会被创建。该机制能够有效减少创建锁的数量。 * 显示锁:可以通过命令查看到的锁,通常会用for update、lock in share mode 或者 update、delete操作会生成显示锁 \ No newline at end of file diff --git a/mysql/mysql底层/redo log & undo log.md b/mysql/mysql底层/redo log & undo log.md index 87f673a..beec0de 100644 --- a/mysql/mysql底层/redo log & undo log.md +++ b/mysql/mysql底层/redo log & undo log.md @@ -1,5 +1,5 @@ -# redo log & undo log -* ## undo log - * undo log通常用于事务的回滚操作,用来保证事务的原子性。 - * 每当发生数据修改操作时(update、insert、delete),关于当前修改操作的相反操作会被记录到undo log中,通常用于在回滚时将数据回复到修改之前的值。 +# redo log & undo log +* ## undo log + * undo log通常用于事务的回滚操作,用来保证事务的原子性。 + * 每当发生数据修改操作时(update、insert、delete),关于当前修改操作的相反操作会被记录到undo log中,通常用于在回滚时将数据回复到修改之前的值。 * undo log默认被记录到mysql的表空间中,因而对undo log进行追加时,对表中页数据进行修改时也会产生redo log,对undo log的追加会通过fsync操作持久化到redo log中。这样即使在一个事务尚未被提交或是回滚之前,mysql服务器崩溃,下次重启时,也可以通过redo log恢复对数据的修改和undo log的内容,回滚事务时也能将数据恢复到崩溃前尚未被事务修改的状态。 \ No newline at end of file diff --git a/mysql/mysql底层/索引.md b/mysql/mysql底层/索引.md index d0748a2..4dac5da 100644 --- a/mysql/mysql底层/索引.md +++ b/mysql/mysql底层/索引.md @@ -1,24 +1,24 @@ -# mysql索引 -* ## mysql索引的分类 - * 从功能逻辑上对索引进行分类: - * 普通索引:只是用于提升查询效率,没有任何的附加约束 - * 唯一性索引:通过unique关键字可以设定唯一性索引,其会限制该索引值必须是唯一的,但是允许为null - * 主键索引:特殊的唯一性索引,在唯一性索引的基础上,主键索引还增加了不为空的约束 - * 单列索引:作用在一个字段上的索引 - * 联合索引:作用于多个字段上的索引 -* ## 索引的创建、删除操作 -```mysql -# 索引的创建方式 -alter table table_name add [unique/index] idx_name (col_name...) -# or -create [unique/index] on table_name(col_name...) - -# 索引的删除方式 -alter table table_name drop index idx_name -# or -drop index idx_name on table_name -``` -* ## 索引的可见性 -```mysql -# 通过修改索引的可见性,可以比较创建 +# mysql索引 +* ## mysql索引的分类 + * 从功能逻辑上对索引进行分类: + * 普通索引:只是用于提升查询效率,没有任何的附加约束 + * 唯一性索引:通过unique关键字可以设定唯一性索引,其会限制该索引值必须是唯一的,但是允许为null + * 主键索引:特殊的唯一性索引,在唯一性索引的基础上,主键索引还增加了不为空的约束 + * 单列索引:作用在一个字段上的索引 + * 联合索引:作用于多个字段上的索引 +* ## 索引的创建、删除操作 +```mysql +# 索引的创建方式 +alter table table_name add [unique/index] idx_name (col_name...) +# or +create [unique/index] on table_name(col_name...) + +# 索引的删除方式 +alter table table_name drop index idx_name +# or +drop index idx_name on table_name +``` +* ## 索引的可见性 +```mysql +# 通过修改索引的可见性,可以比较创建 ``` \ No newline at end of file diff --git a/mysql/mysql文档/Data Type.md b/mysql/mysql文档/Data Type.md index 6f73fed..ef46bd6 100644 --- a/mysql/mysql文档/Data Type.md +++ b/mysql/mysql文档/Data Type.md @@ -1,884 +1,884 @@ -- [Mysql Data Type](#mysql-data-type) - - [Mysql数字类型语法](#mysql数字类型语法) - - [整型数据类型](#整型数据类型) - - [非整型数据类型](#非整型数据类型) - - [zerofill](#zerofill) - - [unsigned](#unsigned) - - [serial](#serial) - - [BIT[(M)]](#bitm) - - [TINYINT[(M)] [UNSIGNED] [ZEROFILL]](#tinyintm-unsigned-zerofill) - - [BOOL,BOOLEAN](#boolboolean) - - [SMALLINT[(M)] [UNSIGNED] [ZEROFILL]](#smallintm-unsigned-zerofill) - - [MEDIUMINT[(M)] [UNSIGNED] [ZEROFILL]](#mediumintm-unsigned-zerofill) - - [INT[(M)] [UNSIGNED] [ZEROFILL]](#intm-unsigned-zerofill) - - [BIGINT[(M)] [UNSIGNED] [ZEROFILL]](#bigintm-unsigned-zerofill) - - [DECIMAL[(M[,D])] [UNSIGNED] [ZEROFILL]](#decimalmd-unsigned-zerofill) - - [FLOAT[(M,D)] [UNSIGNED] [ZEROFILL]](#floatmd-unsigned-zerofill) - - [DOUBLE[(M,D)] [UNSIGNED] [ZEROFILL]](#doublemd-unsigned-zerofill) - - [DECIMAL和NUMERIC](#decimal和numeric) - - [BIT](#bit) - - [mysql日期和时间类型](#mysql日期和时间类型) - - [日期和时间类型语法](#日期和时间类型语法) - - [date](#date) - - [datetime(fsp)](#datetimefsp) - - [timestamp(fsp)](#timestampfsp) - - [time(fsp)](#timefsp) - - [YEAR(4)](#year4) - - [mysql中timestamp的存储](#mysql中timestamp的存储) - - [mysql中的时区](#mysql中的时区) - - [time、datetime和timestamp传入无效值时](#timedatetime和timestamp传入无效值时) - - [tmestamp转化为datetime](#tmestamp转化为datetime) - - [mysql中date部分](#mysql中date部分) - - [time类型](#time类型) - - [time类型的缩写](#time类型的缩写) - - [year类型](#year类型) - - [timestamp类型和datetime类型自动初始化和更新](#timestamp类型和datetime类型自动初始化和更新) - - [timestamp或datetime指定小数位精度](#timestamp或datetime指定小数位精度) - - [explicit_defaults_for_timestamp](#explicit_defaults_for_timestamp) - - [时间函数返回带小数位的时间](#时间函数返回带小数位的时间) - - [mysql中时间类型之间的转换](#mysql中时间类型之间的转换) - - [date类型](#date类型) - - [datetime类型和timestamp类型](#datetime类型和timestamp类型) - - [time类型](#time类型-1) - - [时间类型之间的显式转换](#时间类型之间的显式转换) - - [将时间类型转化为数值类型](#将时间类型转化为数值类型) - - [String Data Type](#string-data-type) - - [mysql中String类型的长度单位](#mysql中string类型的长度单位) - - [为字符类型String指定字符集和排序规则](#为字符类型string指定字符集和排序规则) - - [BINARY属性](#binary属性) - - [CHAR[(M)] [CHARACTER SET charset_name] [COLLATE collation_name]](#charm-character-set-charset_name-collate-collation_name) - - [VARCHAR(M) [CHARACTER SET charset_name] [COLLATE collation_name]](#varcharm-character-set-charset_name-collate-collation_name) - - [BINARY[(M)]](#binarym) - - [VARBINARY(M)](#varbinarym) - - [tinyblob](#tinyblob) - - [tinytext [CHARACTER SET charset_name] [COLLATE collation_name]](#tinytext-character-set-charset_name-collate-collation_name) - - [blob[(M)]](#blobm) - - [text[(M)]](#textm) - - [mediumblob](#mediumblob) - - [mediumtext [CHARACTER SET charset_name] [COLLATE collation_name]](#mediumtext-character-set-charset_name-collate-collation_name) - - [longblob](#longblob) - - [longtext [CHARACTER SET charset_name] [COLLATE collation_name]](#longtext-character-set-charset_name-collate-collation_name) - - [ENUM('value1','value2',...) [CHARACTER SET charset_name] [COLLATE collation_name]](#enumvalue1value2-character-set-charset_name-collate-collation_name) - - [SET('value1','value2',...) [CHARACTER SET charset_name] [COLLATE collation_name]](#setvalue1value2-character-set-charset_name-collate-collation_name) - - [char和varchar类型](#char和varchar类型) - - [varchar类型的存储](#varchar类型的存储) - - [varchar和char类型的赋值](#varchar和char类型的赋值) - - [在比较varchar类型和char类型时对后缀空格的处理](#在比较varchar类型和char类型时对后缀空格的处理) - - [binary和varbinary类型](#binary和varbinary类型) - - [binary存储值时的逻辑](#binary存储值时的逻辑) - - [varbinary存储时的逻辑](#varbinary存储时的逻辑) - - [blob类型和text类型](#blob类型和text类型) - - [关于text类型作为索引的比较逻辑](#关于text类型作为索引的比较逻辑) - - [blob/text类型与varchar/varbinary类型的区别](#blobtext类型与varcharvarbinary类型的区别) - - [blob类型和text类型的排序](#blob类型和text类型的排序) - - [enum类型](#enum类型) - - [枚举列的创建](#枚举列的创建) - - [枚举类型的值和序号](#枚举类型的值和序号) - - [枚举类型插入invalid值](#枚举类型插入invalid值) - - [枚举值的比较](#枚举值的比较) - - [枚举值的定义](#枚举值的定义) - - [set类型](#set类型) - - [set类型值的存储](#set类型值的存储) - - [set类型的值赋值即错误处理](#set类型的值赋值即错误处理) - - [set类型值中查找某set元素是否存在](#set类型值中查找某set元素是否存在) - - [Json类型](#json类型) - - [对Json值进行部分更新](#对json值进行部分更新) - - [构造Json值](#构造json值) - - [构造Json Array](#构造json-array) - - [构造Json Object](#构造json-object) - - [嵌套Json Object和Json Array](#嵌套json-object和json-array) - - [json值的插入](#json值的插入) - - [json_type函数](#json_type函数) - - [json_array函数](#json_array函数) - - [json_object函数](#json_object函数) - - [json_merge_preserve函数](#json_merge_preserve函数) - - [json_valid函数](#json_valid函数) - - [json值赋值给用户定义的变量中](#json值赋值给用户定义的变量中) - - [通过cast函数将字符串类型转化为json值](#通过cast函数将字符串类型转化为json值) - - [反斜杠转义'和"](#反斜杠转义和) - - [获取json值中属性的值](#获取json值中属性的值) - - [Json值的normalization,merging和autowarpping](#json值的normalizationmerging和autowarpping) - - [json值的规范化](#json值的规范化) - - [merge json的值](#merge-json的值) - - [合并array](#合并array) - - [合并json object](#合并json-object) - - [autowrapping](#autowrapping) - - [当array和object进行merge](#当array和object进行merge) - - [查找和修改json值](#查找和修改json值) - - [路径表达式](#路径表达式) - - [**通配符样例](#通配符样例) - - [选中array的一个子集](#选中array的一个子集) - - [last的用法](#last的用法) - - [json_set](#json_set) - - [json_insert](#json_insert) - - [json_replace](#json_replace) - - [json_remove](#json_remove) - - -# Mysql Data Type -## Mysql数字类型语法 -### 整型数据类型 -对于整型数据类型,M代表最大的显示宽度。该最大显示宽度是255。显示宽度和该整型类型可以存储范围的值没有关系。***在mysql 8.0.17中,整型的M值已经被遗弃。*** -```sql -# 将M限定为4并不会限制INT类型可以存储值的范围 -INT(4) -``` -> 将M限定为4时,当应用使用该字段整型值时,如果该字段值显示宽度小于M,会在左边填充空格 - -### 非整型数据类型 -对于浮点类型和fixed-point数据类型,M代表该字段总共能够存储多少位,M为能存储的最多位长度(十进制)。 - -### zerofill -对于数字类型的类型,可以为其指定zerofill,在指定为zerofill后,会为其自动添加unsigned约束,该数字类型字段存储的值不论是整型还是浮点数均需要为非负值。 -```sql -# 被zerofill修饰的字段,如果显示宽度小于M,那么都会在左边填充0 - -# 其等同于INT(10) UNSIGNED ZEROFILL -INT(10) ZEROFILL - -DECIMAL(28,2) ZEROFILL -``` -***在mysql8.0.17,zerofill对于数值类型字段也被遗弃了*** - -### unsigned -对于数值类型的字段,可以支持unsiged和signed属性。默认情况下数值类型是signed的,可以手动将其指定为unsigned。 -***在mysql8.0.17,对于float、decimal、double类型,unsigned属性也被遗弃了。可以通过简单的check来进行替换*** - -### serial -serial是一个对于BIGINT UNSIGNED NOT NULL AUTO_INCREMENT UNIQUE的alias。 -```sql -# 如下字段定义 -id serial -# 等同于 -id bigint unsigned not null auto_increment unique -``` - -### BIT[(M)] -bit值类型,M代表每个值的bit位数,M值可以从1到64,默认情况下若M省略,M为1 - -### TINYINT[(M)] [UNSIGNED] [ZEROFILL] -8位整型,有符号范围为-128到127,无符号范围为0到255 -### BOOL,BOOLEAN -该类型等同于TINYINT(1),值为0代表false,值非0代表true -### SMALLINT[(M)] [UNSIGNED] [ZEROFILL] -16位整型,有符号范围为 -32768到32767,无符号范围为0 到 65535 -### MEDIUMINT[(M)] [UNSIGNED] [ZEROFILL] -24位整型,有符号范围为-8388608到8388607,无符号范围为 0 到16777215 -### INT[(M)] [UNSIGNED] [ZEROFILL] -32位整型,有符号范围为-2147483648 到2147483647,无符号范围为0 到4294967295 -### BIGINT[(M)] [UNSIGNED] [ZEROFILL] -64位整型,有符号范围为 -9223372036854775808到9223372036854775807,无符号范围为0到18446744073709551615 -> ***所有算数都是使用带符号的bigint或者double进行运算,故而不要使用bigint大于 9223372036854775807 (63bit)*** - -### DECIMAL[(M[,D])] [UNSIGNED] [ZEROFILL] -fixed-point nubmer,M是非整型的总共的位数,而D对应的则是小数点之后的位数。小数点和负的符号并不包含在M中。 -如果D是0,则代表该数没有小数部分。***对于DECIMAL来说,M最大值为65,D受支持的最大值为30,如果D省略,则D默认为0,若M省略,那么M的默认值为10 - -### FLOAT[(M,D)] [UNSIGNED] [ZEROFILL] -单精度浮点数,M为非整型总共的位数,D为小数点之后的位数。如果M和D省略,那么会根据硬件的限制来存储。单精度浮点数大概精确到小数点后7位。 -> ***FLOAT(M,D)是一个非标准的mysql拓展,在mysql 8.0.17中,该语法被废弃*** - -### DOUBLE[(M,D)] [UNSIGNED] [ZEROFILL] -双精度浮点数,其精度大概精确到小数点后15位 - -### DECIMAL和NUMERIC -DECIMAL和NUMERIC用于存储高精度的数值类型,当保存数字的精度很重要时需要使用这些类型,如货币数据。在mysql中,NUMERIC类型被实现为DECIMAL - -### BIT -对于bit类型,其赋值可以使用b'value',其中value为二进制类型。 -```sql -# 其返回值为true -select b'011' = 3; -``` -## mysql日期和时间类型 -### 日期和时间类型语法 -对于time、datetime、timestamp类型,mysql允许second(秒)具有小数部分,最高精度到小数点后6位(微秒)。 -> ***time、datetime、timestamp定义小数部分格式如下*** -> ***type_name(fsp)*** -> - type_name为date、datetime、timestamp中的一个 -> - fsp为second的小数部分精度 -```sql -CREATE TABLE t1 (t TIME(3), dt DATETIME(6), ts TIMESTAMP(0)); -``` -对于fsp,其必须位于0~6的范围内,0代表其没有小数部分。若fsp省略,则默认情况下fsp为0. -### date -支持的日期是'1000-01-01'到'9999-12-31'。mysql通过'YYYY-MM-DD'的格式来显示date数据,但是对于date类型的赋值,既可以通过字符串类型也可以通过数字类型。 -### datetime(fsp) -***datetime***类型是***date***和***time***类型的组合。datetime类型支持的范围是 '1000-01-01 00:00:00.000000'到'9999-12-31 23:59:59.999999',mysql展示datetime类型值是通过'YYYY-MM-DD hh:mm:ss[.fraction]'格式。同样的,datetime类型的赋值可以通过字符串类型和数字类型。 -### timestamp(fsp) -时间戳类型。timestamp类型支持的范围是'1970-01-01 00:00:01.000000' UTC 到 '2038-01-19 03:14:07.999999' UTC。timestamp类型的值存储着从('1970-01-01 00:00:00' UTC)其经过的秒数。 -> ***timestamp类型不能表示'1970-01-01 00:00:00',因为该时间点等于timestamp类型的计时起点,但是timestamp类型中0已经被预留来代表 '0000-00-00 00:00:00'*** - -> ***explicit_defaults_for_timestamp已启用*** -> 如果 explicit_defaults_for_timestamp 属性已经被启用,那么不会将CURRENT_TIMESTAMP赋值给timestamp字段做默认值,也不会在更新时自动将CURRENT_TIMESTAMP赋值给timestamp字段。并且,任何没有声明为not null的timestamp字段都是可为空的。 - -> ***explicit_defaults_for_timestamp未启用*** -> 除非被显式指定,否则数据表中的timestamp列都会被赋值为最近修改的datetime。该属性开未开启时,可以通过timestamp字段记录上次update或insert时间。 -> 可以通过为timestamp字段赋值一个null来将该字段设置为当前时间,除非该字段在定义时通过NULL属性声明该字段可以为空。 - -> ***自动赋值为当前时间和记录更新时间设置*** -> 可以在定义该timestamp列字段时指定DEFAULT CURRENT_TIMESTAMP和ON UPDATE CURRENT_TIMESTAMP,为该timestamp字段指定默认值和记录最近更新时间。***若explicit_defaults_for_timestamp未启用,默认情况下,若第一个timestamp列没有指定default和on update属性中的任何一个,第一个timestamp列字段具有这些属性,数据表中的任何timestamp字段也可以手动指定这些属性。*** - -### time(fsp) -time类型的范围为'-838:59:59.000000'到'838:59:59.000000',msyql显示time类型的格式为'hh:mm:ss[.fraction]',但是对time类型的赋值既可以用字符串类型也可以用数字类型。 -### YEAR(4) -year类型用来表示年,year类型通过YYYY格式进行展示,为year类型字段赋值既可以通过字符串类型也可以通过数字类型。 - -> 对于时间类型,avg和sum等聚合函数不起作用。该类聚合函数会将值转化为数字,并在遇到第一个非数字字符时转换失败。如果想要对时间类型使用聚合函数,需要在使用前将时间类型转化为数字单元,并在聚合后将数字单元转化为时间类型,如下: -> ```sql -> SELECT SEC_TO_TIME(SUM(TIME_TO_SEC(time_col))) FROM tbl_name; -> SELECT FROM_DAYS(SUM(TO_DAYS(date_col))) FROM tbl_name; -> ``` - -### mysql中timestamp的存储 -在mysql中,timestamp在存储时,会从本地时区转化为UTC时区然后再存储到数据库中,在获取存储的timestamp时会从UTC时区转化为本地时区(该行为在其他类型,如datetime,中则不会发生)。 -### mysql中的时区 -在默认情况下,对于每个Connection,当前时区为mysql server的时区。但是,Connection时区也可以针对每个Connection单独设置(在jdbc:mysql://{server}:{port}/{db_name}?{追加参数}中单独设置)。 -> 由于时区统一转化为UTC后再存储于数据库中,那么在存储了timestamp之后,若再次访问timestamp时时区发生了改变,那么将存储的UTC时区timestamp转化为Connection对应时区时,转化后时区时间和存储时的时区时间并不相同。 - -### time、datetime和timestamp传入无效值时 -对于无效的time、datetime、timestamp值,会将其转化为该类型默认的‘0’值,例如'0000-00-00'或者'0000-00-00 00:00:00'。 - -### tmestamp转化为datetime -可以通过cast()函数和at time zone操作符将timestamp类型转化为datetime类型。 -```sql -SELECT col, - CAST(col AT TIME ZONE INTERVAL '+00:00' AS DATETIME) AS ut - FROM ts ORDER BY id; -``` - -### mysql中date部分 -- 在mysql中,对于date类型或者timestamp或datetime中的date部分,任何标点符号都可以作为分隔符 - ```sql - '10:11:12' 代表 '2010-11-12' - '10:11:12 00:00:00' 代表 '2010-11-12 00:00:00' - ``` -- 当date中年份只包含两位数时,若年份为00~69,则会变为2000~2069,若年份为70~99,则年份为1970~1999 - -### time类型 -mysql中获取和展示time类型的格式为'hh:mm:ss'。time的范围为'-838:59:59' 到 '838:59:59'。 -> ***time范围*** -> 之所以time的范围会如此之大并且支持负值,是因为time类型不仅可以用来表示一天中的时间点,还可以用来表示两个时间点之间的间隔(间隔可能超过24h,也有可能为负值) - -### time类型的缩写 -- 缩写中带有冒号':'时,值将会被解释为一天中的时间点,并且,’11:12'将会被解释为'11:12:00'而不是'00:11:12' -- 缩写中不带冒号时,将会被解释为两个时间点之间的间隔。并且,最右边的两个数将会被解释为秒,'1112'和1112也会被解释为'00:11:12' - -### year类型 -在mysql 8.0.19中,不再支持year(4)这种显式指定宽度的方式已经被废弃,并且,过去支持的year(2)类型在8.0.19中也不再被支持。 -> year类型支持的范围是'1901'到'2155',也可通过数字类型1901到2155指定 - -### timestamp类型和datetime类型自动初始化和更新 -对于timestamp类型和datetime类型,可以被自动初始化和更新到当前的时间。 -> 当对timestamp或者datetime字段设置了on update current_timestamp时,若某行中任一其他列的值发生变化,该列变化前后值不同,那么设置了on update的datetime或timestamp字段会更新到当前时间。若update前后所有的列值都与修改之前相同,那么即使执行了update操作,on update列的时间也不会更新。 -> ***如果想要防止设置了on update的列在其他列被修改时更新为当前时间,可以显式将该列设置为该列当前的值。*** -> ```sql -> # 即使dt被设置了on update,id变更后dt仍然为当前的值 -> update dt=dt,salary+=10 wherre id = 1000 - -> ### current_timestamp的同义词 -> 如下同义词都和current_timestamp具有相同的含义 -> - current_timestamp() -> - now() -> - localtime -> - localtime() -> - localtimestamp -> - localtimestamp() - -### timestamp或datetime指定小数位精度 -如果timestamp或者datetime类型在声明时使用了小数位精度,那么在current_timestamp后也要指定相同的精度。 -```sql -CREATE TABLE t1 ( - ts TIMESTAMP(6) DEFAULT CURRENT_TIMESTAMP(6) ON UPDATE CURRENT_TIMESTAMP(6) -); -``` - -### explicit_defaults_for_timestamp -当explicit_defaults_for_timestamp关闭时,timestamp默认情况下是not null的,该类字段不能含有null值,若将字段显式设置为null时,字段值实际被设置为当前时间。若想要允许该字段包含null,可以在声明该字段时显式指定null属性,此时默认值也会变为null。 -当explicit_defaults_for_timestamp启用时,只有指定timestamp字段null属性时该字段才能为空。并且,在explicit_defaults_for_timestamp启用时不可以通过赋值字段为null的形式来将timestamp字段设置为当前值。 -> ***当explicit_defaults_for_timestamp开启时,默认条件下timestamp类型声明会自动添加null default null*** - -### 时间函数返回带小数位的时间 -默认情况下,now()或current_timestamp()函数返回当前的时间戳,如果参数为空,返回结果并不含有小数位。如果想要为now()或者current_timestamp()函数的返回结果指定小数位,可以为函数参数指定一个0~6的整型参数用来限制返回结果时间的小数点位数。 -```sql -# 返回时间戳带有4位小数 -select current_timestamp(4); -``` - -### mysql中时间类型之间的转换 -#### date类型 -- date类型转化为datetime或timestamp类型时,时间类型将会添加'00:00:00'部分 -- date类型转化位time类型时并没有用处,只会转化为'00:00:00' -#### datetime类型和timestamp类型 -- 当datetime类型或timestamp类型转化为date类型时,会考虑time部分秒的小数部分并且对time部分进行舍入,若time部分为‘23:59:59.499’会舍入到当天,而‘23:59:59.500’则是会舍入到下一天。 - ```sql - '1999-12-31 23:59:59.499' -> '1999-12-31' # 舍入到当天 - '1999-12-31 23:59:59.500' -> '2000-01-01' # 舍入到下一天 - ``` -- 当datetime或timestamp类型转化为time类型时,会丢弃date部分 -#### time类型 -- 如果time类型转化为datetime类型或是timestamp类型,current_date()返回的date将会被用作date部分。***并且,time部分会看作时间段,并且附加在current_date()返回的date部分上。如果time类型的值范围位于'00:00:00'和'23:59:59'之外,那么date部分会根据time部分的值向前或向后变化。*** -```sql -# 若当前current_date()的返回值为'2012-01-01' - -# 当time为'12:00:00'时,转化成的datetime为 -'2012-01-01 12:00:00' - -# 当time为'24:00:00'时,转化成的datetime为 -'2012-01-02 00:00:00' - -# 当time为'-12:00:00'时,转化成的datetime为 -'2011-12-31 12:00:00' -``` -- 当time类型转化为date时,逻辑和上面一样,但是会舍弃得到datetime结果的time部分,即结果为'2012-01-01'、'2012-01-02'或'2011-12-31' -#### 时间类型之间的显式转换 -时间类型之间的显式转换可以覆盖隐式转换。例如,在比较date类型和datetime类型时,默认会将date类型转化为datetime类型之后,再比较两个datetime类型。 -如果想要再比较date类型和datetime类型时,忽略datetime的time部分,可以手动将datetime类型转化为date类型之后再进行比较。 -```sql -# 将datetime类型手动通过cast函数转化为date类型之后再进行比较 -date_col = CAST(datetime_col AS DATE) -``` -#### 将时间类型转化为数值类型 -如果想要将时间类型转化为数值类型,可以使用如下方式: -```sql -select datetime_col+0,date_col+0,time_col+0,timestamp_col+0; -``` -当时间类型转化为数值类型时,如果时间类型没有小数部分(N为0),则会转化为整型类型;如果时间类型含有小数部分,则会转化为decimal类型,decimal类型具有n位小数。 - -## String Data Type -mysql中String类型可以分为如下: -- CHAR -- VARCHAR -- BINARY -- VARBINARY -- BLOB -- TEXT -- ENUM -- SET -### mysql中String类型的长度单位 -> 对于字符类型String的列(例如CHAR,VARCHAR,TEXT),mysql将其长度规格指定为字符单元。 -> 对于二进制类型的String列(例如BLOB,BINARY,VARBINARY),mysql将长度规格指定为字节单元。 -### 为字符类型String指定字符集和排序规则 -对于字符类型的String字段(CHAR,VARCHAR,TEXT,ENUM,SET),可以通过CHARACTER SET属性指定该String字段的字符集,并且可以通过COLLATE属性指定该字段的排序规则。 -> 为字符类型String指定character set和collate的规则: -> 1. 如果charset和collate都被指定,则指定的charset和collate被使用 -> 2. 如果charset被指定而collate未被指定,那么指定的charset和charset默认的collate会被使用 -> 3. 如果collate被指定但是charset未被指定,那么指定的collate和collate关联的charset会被使用 -> 4. 如果charset和collate都没有被指定,那么该字符类型String字段会使用该table表对应的charset和collate - -#### BINARY属性 -binary属性是用来指定二进制collate的快捷方式。当使用binary属性来修饰字符类型String字段后,该字段的比较和排序都基于的是字符编码的数值而不是字符排序。 -```sql -CREATE TABLE t -( - c1 VARCHAR(10) CHARACTER SET latin1 BINARY, - c2 TEXT BINARY -) CHARACTER SET utf8mb4; -``` -### CHAR[(M)] [CHARACTER SET charset_name] [COLLATE collation_name] -该String为固定长度为M的字符串,存储时会再字符串的右边一直填充空格字符直到字符串长度为M。M代表 -### VARCHAR(M) [CHARACTER SET charset_name] [COLLATE collation_name] -M代表该列字段最长支持的字符长度(以字符为单位,而不是字节)。M的范围可以为 0到65,535。varchar类型的最长长度受mysql表数据的最大row size限制(row size最大为65,535 bytes,该上限由该行数据的所有字段之间进行共享)。***除了rowsize限制之外,varchar类型的最长长度也受到选用字符集的限制***,如utf8mb3字符集每个字符最多需要3个字节,故而对于utf8mb3字符集最多可以含21,844个字符。***每个字符需要的字节数也会影响该varchar类型含有的字符数M的上限。*** -### BINARY[(M)] -BINARY类型和CHAR和VARCHAR类型类似,但是BINARY类型存储的是二进制类型,以字节为单位。M代表该列的长度,以二进制字节为单位。***如果M省略,则M默认为1*** -### VARBINARY(M) -VARBINARY类型和VARCHAR类型类似,但是存储的是以字节为单位的二进制数据。M代表该列字段以字节为单位的最长长度。 -### tinyblob -tinyblob类型最多可以存储255个字节长度的二进制数据。每个tinyblob类型的数据存储都需要用一个字节的前缀来记录该tinyblob值的长度(以字节为单位)。 -### tinytext [CHARACTER SET charset_name] [COLLATE collation_name] -text类型的数据,但是最多存储255个字节长度的字符串。如果字符串中包含的字符占用多个字节,那么字符的最大个数小于255.每个tinytext类型值采用一个字节来记录该tinytext类型值的长度(以字节为单位)。 -### blob[(M)] -一个blob列,最多含有65535(2^16-1)个字节。blob类型会使用两个字节的前缀来记录该blob类型的最大长度(以字节为单位)。 -> 可以为blob类型指定一个M,如果M被指定,那么mysql在创建该列时会使用可以包含M个字节数据的最小的blob类型。 - -### text[(M)] -一个text列,最多可以含有65535个字符的数据。如果text类型的值含有多字节字符,那么最大字符数量将小于65535.每个text列都会采用2字节的前缀用来记录该text值长度(以字节为单位)。 -> 可以为text类型指定一个M,如果M被指定,那么mysql在创建该列时会采用可以包含M个***字符***的最小类型的text。 - -### mediumblob -一个blob列,最多可以存储16,777,215(2^24-1,约16MB)个字节的二进制数据,该类型会使用3字节的前缀来记录长度。 -### mediumtext [CHARACTER SET charset_name] [COLLATE collation_name] -一个text列,最多可以存储16,777,215(2^24-1,约16MB)个字符的文数据,如果该text列的值含有多字节字符,那么该列存储字符的最大数量少于该上限。每个mediumtext类型使用3字节的前缀来记录长度。 -### longblob -一个blob列,最多可以存储4,294,967,295 (2^32-1,约4GB)字节的二进制数据。每个longblob类型都使用4字节前缀来记录该longblob列的长度(以字节为单位)。 -### longtext [CHARACTER SET charset_name] [COLLATE collation_name] -一个text列,最多可以存储4,294,967,295个字符的文本。如果含有多字节字符,那么存储的最大字符数量会小于该上限。每个longtext类型会使用4字节的前缀来存储该值长度(以字节为单位)。 -### ENUM('value1','value2',...) [CHARACTER SET charset_name] [COLLATE collation_name] -枚举类型,一个字段可以含有value1,value2,……中的一个值或者null。enum类型在内部是通过整数类型来表示的,value1从1开始,可以通过select enum_col+0的形式来查看整数值。 -> enum类型最多可以含有 65,535 个不同的enum元素。 -> 对于每个enum元素,其最大支持长度需要满足如下条件: -> - M<=255 -> - M * w<=1020 -> -> 其中M是字符串文本的字符长度,而w则是该文本字符所属字符集中单个字符所需要的最大字节数 - -### SET('value1','value2',...) [CHARACTER SET charset_name] [COLLATE collation_name] -string集合,每个string对象可以0个或多个值,每个值都必须属于value1,value2,……。set类型在内部通过int表示。 -> set类型最多可以含有64个不同的值。 -> 对于每个set元素,其最大支持长度需要满足如下条件: -> - M<=255 -> - M * w<=1020 -> -> 其中M是字符串文本的字符长度,而w则是该文本字符所属字符集中单个字符所需要的最大字节数 - -> 对于set类型字段,插入时如果包含set类型中多个值,多个值之间可以通过逗号','分隔 -> 对于包含多个set元素的值,其值中元素的顺序和出现次数并不重要,'b,b,a,c'和'c,b,b,c,a'在插入后都会变为'a,b,c' - -> 可以通过find_in_set('value',set_col)来查询某个值是否在set_col中 -### char和varchar类型 -对于char类型,其长度是固定的,并且长度需要在创建表字段时指定(长度位于0到255之间)。当char类型的值在存储时,***会右填充空格直到长度到达指定长度M***。当char类型的值在获取时,***会删除尾随填充的空格,除非PAD_CHAR_TO_FULL_LENGTH的sql mode被启用***。 -> 如果字段类型被设置为char类型,并且pad_char_to_full_length关闭,那么如果向char类型字段中存入字符串,***在取出存入字符串时,存入字符串末尾若存在空格,那么在取出字符串时会删除尾随空格,即使尾随空格不是存入时填充的而是字符串本身就含有的尾随空格*** -> ```sql -> char_col char(30) -> -> # 插入字符串值存在尾随空格 -> insert into string_tb(char_col) values (' a '); -> # 获取时,字符串本身含有的尾随空格也会被删除 -> # 返回值并不是' a 1'而是' a1' -> select concat(char_col,'1') from string_tb; -> ``` -#### varchar类型的存储 -相较于char类型,varchar类型在存储时,会额外存储1到2个字节长度的前缀,用来记录该varchar类型的长度(以字节为单位)。***当该varchar类型值存储不超过255字节时,使用一个字节来记录长度,否则使用两个字节来记录长度***。 -#### varchar和char类型的赋值 -如果对一个varchar或char类型的字段进行赋值操作,且赋值超过该类型的最大长度: -- 如果strict_mode没有开启,那么赋的值会被截断,并且产生一个warning。 -- 如果strict_mode已经被启用,那么在截断非空格字符时,会产生一个异常,并且阻止插入操作。 -- 对于varchar类型,在插入时,超过该列长度限制的尾随空格将会被截断,并且会生成一个warning,不论strict_mode是否开启。 -- 对于char类型,在插入时,超过该列长度限制的尾随空格也会被截断,并且截断操作是静默的,并不会生成warning。 - -#### 在比较varchar类型和char类型时对后缀空格的处理 -mysql的collate具有一个pad attribute,可以有两个值: -- pad space(绝大多数的mysql collate都具有pad space属性) -- no pad - -pad space和no pad的区别: -- pad space:在比较时,尾随空格并不会参加比较,尾随空格并不重要 -- no pad:在比较时,尾随空格参与比较,尾随空格在比较时重要 -> 默认情况下,mysql默认字符集为utf8mb4,该字符集默认collate为utf8mb4_0900_ai_ci,其pad attribute为no pad。故而,在默认情况下,mysql对字符串进行比较时,***忽略大小写并且尾随空格参与比较***。 - -> 如果某字符串列其collate的pad attribute为pad space,且该列拥有unique约束,那么在插入值时,'a'和'a '同时插入会对该列产生duplicate-key错误。 - -### binary和varbinary类型 -binary,varbinary类型和char,varchar类型相似,不同的是binary、varbinary类型存储二进制形式的字符串。 -> 当插入字符串超过binary或varbinary的最长限制长度时,若***strict mode未启用,则字符串会被截断来适应该二进制列的最大长度,并且会产生一个warning***;若***strcit mode已启用,那么会抛出异常并且插入失败*** - -#### binary存储值时的逻辑 -当binary类型的值被存储时,该插入的值会右填充(0x00)的值直到到达指定长度。并且,在对binary值进行获取时,***并不会删除尾随(0x00)。*** -在对binary类型值进行比较时,所有字节都重要,比较时并不会忽略尾随(0x00)字节。 -> 对于binary类型,其和char类型不同,并不删除尾随字符,其意味着如果‘a’同时插入binary(3)的col_3列和binary(6)的col_6列,从col_3和col_6中取出存储的值进行比较时,col_3和col_6取出的值并不相同,col_3为0x610000,col_6为0x610000000000。 - -#### varbinary存储时的逻辑 -对于varbinary类型,存储时并不会插入尾随字符,获取时也不会删除尾随字符。 - -### blob类型和text类型 -blob类型可以看作一个存储大量二进制字符串数据的对象,而text类型可以看作一个存储大量字符型字符串的对象。 -> 如果strict mode没有开启,那么在给blob类型字段分配超过其长度限制的值时,会对分配的值进行截断并且抛出一个warning;如果strict mode已经开启,那么当截断非空格字符时会抛出异常并且插入失败。 -> 对于text类型,如果插入时对插入值的尾随空格进行截断,那么会抛出警告,无论strict mode是否开启。 - -对于blob类型和text类型,在插入时并不会有向右的填充字符,在获取时也不会删除尾随字符。(类似于varchar和varbinary)。 - -#### 关于text类型作为索引的比较逻辑 -如果将text类型的列作为索引,必须要指定索引前缀长度,当text值小于索引前缀长度时对索引列的text类型值进行比较时会向右填充空格。即如果该索引需要unique约束,那么两个除了后缀空格数目外完全相同的text值将会产生duplicate-key异常。 -#### blob/text类型与varchar/varbinary类型的区别 -在大多数情况下,blob/text类型和varchar/varbinary都相同,但是在如下方面会有所区别: -- 在对blob/text类型字段添加索引时,必须要指定索引前缀长度M。(在对单列创建索引时,必须要复制索引列的值到特定的数据结构如B+树中,故而强制要求blob/text类型创建索引时指定索引前缀长度能够有效的减少复制索引值消耗的空间和比较索引的开销) - ```sql - create table tb_blob_index ( - blob_col blob, - index(blob_col(M)) - ); - ``` -- 对于blob类型和text类型的字段并不能为其指定default默认值 - -#### blob类型和text类型的排序 -由于blob类型和text类型的值可能会很长,故而在对blob类型和text类型的值进行排序时,只会对列开头max_sort_length字节长度的内容进行排序。max_sort_length默认值为1024. -```sql -show variables like 'max_sort_length'; # default 1024 -``` -### enum类型 -枚举类型是一个String对象,该对象的值可以从允许的值范围中进行选取。允许的值范围在定义枚举字段时显式指定。 -#### 枚举列的创建 -```sql -create tb_enum ( - id bigint auto_increment primary key, - size enum('x-small', 'small', 'medium', 'large', 'x-large') -); -``` -当在插入枚举时,可以有效降低空间的占用量。对于‘medium’值的插入,一百万条枚举类型值的插入只消耗一百万字节,而将'medium‘类型作为字符串插入时,一百万条数据的插入会消耗六百万字节的空间。 -#### 枚举类型的值和序号 -- mysql中,对于每个枚举类型的值都有一个序号。对于在枚举类型列定义时在括号中指定的枚举类型值,都对应了一个整数序号,序号从1开始(如"small","medium","large"对应1,2,3)。 -- 对枚举空字符串(代表error),其对应的index是0. -- 对于枚举值NULL,其index也为NULL - -如果想要通过数值的形式获取枚举值,可以通过select enum_col+0的形式,获取枚举值时该枚举值对应的index被返回。 - -#### 枚举类型插入invalid值 -如果对枚举类型插入invalid值(插入值不位于enum定义时的可选值中): -- strict mode没有开启时,会插入一个''作为错误值,该''对应的index是0 -- 若strict mode开启,那么插入invalid值会直接导致异常 - -如果枚举列声明可为null,那么null对于该枚举列则是有效值,该枚举列的默认值为null;如果枚举列声明为not null,那么该枚举列的默认值是index为1对应的枚举值。 - -#### 枚举值的比较 -枚举值的比较是通过比较枚举值的index来进行的。 -```sql -create table tb_enum (enum_col enum('b','a')); - -# 若enum_col_b对应的枚举值是’b',enum_col_a对应的枚举值为enum_col_a -# 那么 -enum_col_b < enum_col_a -``` -#### 枚举值的定义 -在定义枚举值时,不应该通过表达式来指定枚举值。 -```sql -# 无法通过concat来指定枚举值为'medium' -CREATE TABLE sizes ( - size ENUM('small', CONCAT('med','ium'), 'large') -); - -# 无法将枚举值指定为变量 -SET @mysize = 'medium'; -CREATE TABLE sizes ( - size ENUM('small', @mysize, 'large') -); -``` -### set类型 -set类型是一个字符串类型,其可以含有0个或多个值,每个值都从许可值的范围内选择。set类型的值由一系列set类型的成员组成,成员之间通过逗号','分隔。 -```sql -# 例如,set('one','two')类型,可以含有如下可能值 -'one' -'one,two' -'two' -'' -``` -每个set类型最多可以含有64个不同的成员,再set类型字段定义时,如果为set类型字段指定重复的set成员,会触发警告或抛出异常(取决于strict sql mode是否被开启)。 -> 在set类型字段的定义中,如果set类型的成员含有尾随空格,那么尾随空格将会在表创建时自动被移除。 - -***可以为set类型字段分配字符集和collate,对于二进制或者大小写敏感的collate,在赋值时大小写是敏感的。*** -> 默认情况下,set类型字段和字符集和collate与数据表相同,在对set类型字段赋值或者进行比较时并不会区分大小写。 - -#### set类型值的存储 -mysql通过整型来存储set的值,每个候选值对应整型的一个bit。 -| set member | decimal value | binary value | -| :-: | :-: | :-: | -| 'a' | 1 | 0001 | -| 'b' | 2 | 0010 | -| 'c' | 4 | 0100 | -| 'd' | 8 | 1000 | - -```sql -# 如果mysql赋值给set类型字段的值为9, -# 则代表该set类型值组成为8+1 -# 则set字段值为'a,d' -``` -如果一个set类型的值含有多个set元素,那么元素之间的排列顺序并没有影响,'a,d'和'd,a'完全相同,值均为9.并且,对于set类型的值,其中一个元素出现的次数也对set值完全没有影响,'a,d,d,d,a,d,a'和'a,d'也完全相同,值均为9. -#### set类型的值赋值即错误处理 -对于set类型,如果赋值中出现了一个定义该set类型字段时没有出现的元素,那么mysql会进行如下处理: -- 若strict sql mode开启,那么会产生异常 -- 如果strict sql mode关闭,那么该值会被忽略,赋值正常执行,并且产生一个warning -#### set类型值中查找某set元素是否存在 -对于在set值中查找某元素是否存在,可以通过find_in_set(eleVal,setVal)函数或者like操作符。find_in_set会返回元素在set值中第一次出现的位置,如果元素在set值中没有出现,那么会返回0. -```sql -mysql> SELECT * FROM tbl_name WHERE FIND_IN_SET('value',set_col)>0; -mysql> SELECT * FROM tbl_name WHERE set_col LIKE '%value%'; -``` -想要查找set值中是否存在某元素,同样可以通过如下方法: -```sql -# 查找set值中是否存在第一个元素'a' -mysql> SELECT * FROM tbl_name WHERE set_col & 1; -``` -想要对set值进行精准比较,可以通过如下方法 -```sql -# 在比较时, -# set_col='val1,val2' -# 和 -# set_col='val2,val1' -# 的返回结果是不同的,在指定set值是元素值顺序应该按照定义set列时 -# 指定的顺序 -mysql> SELECT * FROM tbl_name WHERE set_col = 'val1,val2'; -``` - -## Json类型 -Mysql本地支持Json类型,允许对Json中的数据进行高效访问。同传统的通过字符串来存储json数据的方式相比,通过Json String来存储json数据具有如下优势: -- 对存储的Json数据进行自动验证,无效的Json数据将会产生异常 -- 优化存储格式,存储在json列中的json数据将会允许对json数据属性的快速读访问。以二进制格式存储在json列中的json数据,如果稍后要对其进行访问,该存储的值无需转化为文本表示之后再解析。***二进制结构的值使服务器能够通过key或者数组index来查找子对象或者嵌套值,而不需要读取所有位于他们之前或之后的值。*** -### 对Json值进行部分更新 -在mysql 8.0中,优化器可以对Json值执行部分更新而不是移除整个旧Json值然后写入新的Json值。 -Json部分更新需要满足如下条件: -1. 该部分更新的列需要是Json列 -2. 该部分更新的update语句需要使用如下三个函数json_set(),json_replace(),json_remove()来更新Json列。任何对Json列的直接赋值都***不会***使用部分更新 -3. 该更新语句的输入列和输出列需要是相同的列,如果UPDATE mytable SET jcol1 = JSON_SET(jcol2, '$.a', 100),其中jcol2和jcol1的列不同,那么更新jcol1时并不会使用部分更新的特性 - > 只要输入列和输出列的值相同,可以通过上诉三个函数的嵌套组合来对json列进行部分更新 -4. 更新语句只是对现存对象或者array的值进行修改,而不会在父对象或者array中加入新值 -5. 被替换的旧值只要要和要替换的新值一样大,旧值所占空间大小不能比新值小 - > 此需求可能存在例外,如果在为旧值进行部分更新时,留下的空间足够容纳更大的新值,那么在对新值进行部分更新时也不会抛出异常 - -### 构造Json值 -#### 构造Json Array -```sql -["abc", 10, null, true, false] -``` -##### 构造Json Object -```sql -{"k1": "value", "k2": 10} -``` -#### 嵌套Json Object和Json Array -```sql -[99, {"id": "HK500", "cost": 75.99}, ["hot", "cold"]] -{"k1": "value", "k2": [10, 20]} -``` -#### json值的插入 -```sql -INSERT INTO t1 VALUES('{"key1": "value1", "key2": "value2"}'); -``` -#### json_type函数 -该函数接受一个json值,并且返回该json值的类型 -```sql -# 返回结果Object -select json_type('{"name":"kazusa","age":17}'); - -# 返回结果Array -select json_type('[{"name":"kazusa","age":17}]'); -``` -#### json_array函数 -json_array函数接受一系列值并且返回json array -```sql -# 返回结果 ["a", 1, "2015-07-27 09:43:47.000000"] -SELECT JSON_ARRAY('a', 1, NOW()); -``` -#### json_object函数 -json_object函数接受一系列的键值对,并且返回一个json object对象 -```sql -# 返回结果 {"key1": 1, "key2": "abc"} -SELECT JSON_OBJECT('key1', 1, 'key2', 'abc'); -``` -#### json_merge_preserve函数 -json_merge_reserve函数接受两个或更多的json值并且返回一个整合后的结果 -```sql -# 返回结果是 ["a", 1, {"key": "value"}] -SELECT JSON_MERGE_PRESERVE('["a", 1]', '{"key": "value"}'); - -# 返回结果是 {"age": 17, "name": "kazusa", "roles": "painoist", "is_male": false} -select json_merge_preserve('{"name":"kazusa","age":17}','{"roles":"painoist","is_male":false}'); -``` -#### json_valid函数 -json_valid函数接受一个表示json值的字符串,并且返回该字符串是否是合规的json值 -```sql -# 返回结果为1,0,0 -select json_valid('null'),json_valid('Null'),json_valid('NULL'); -``` -#### json值赋值给用户定义的变量中 -在mysql中,可以将json值赋值给变量。***但是,变量值并不支持json类型,在将json值赋值给用户自定义变量时,json值会被转化为字符串类型。*** -> 将Json值转化为字符串类型时,字符串类型具有和Json类型相同的charset和collate,Json转化为的字符串其字符集为utf8mb4,并且其collate为utf8mb4_bin(大小写敏感) -```sql -SET @j = JSON_OBJECT('key', 'value'); -SELECT @j; -``` -由于json值的collate是utf8mb4_bin,即json值是大小写敏感的,故而'null','Null','NULL'中只有'null'是有效的json值 -```sql -# 返回结果为1,0,0 -select json_valid('null'),json_valid('Null'),json_valid('NULL'); -``` -#### 通过cast函数将字符串类型转化为json值 -```sql -cast('null' as json) -``` -#### 反斜杠转义'和" -在json值的String表示中,如果想在json值中的文本值中含有'或"符号,需要在前面加上反斜杠进行转义。 -```sql -# 通过create_object插入json值,只需要在前面加上一个反斜杠进行转义 -mysql> INSERT INTO facts VALUES - > (JSON_OBJECT("mascot", "Our mascot is a dolphin named \"Sakila\".")); - -# 通过values插入json值时,需要在前面加上两个反斜杠进行转义 -mysql> INSERT INTO facts VALUES - > ('{"mascot": "Our mascot is a dolphin named \\"Sakila\\"."}'); -``` -#### 获取json值中属性的值 -```sql -# 查询json值中属性的值时用""将结果括起来并含有转义符 -mysql> SELECT sentence->"$.mascot" FROM facts; -+---------------------------------------------+ -| col->"$.mascot" | -+---------------------------------------------+ -| "Our mascot is a dolphin named \"Sakila\"." | -+---------------------------------------------+ -1 row in set (0.00 sec) - -# 查询json值中属性的值时不用”“和转义符 -mysql> SELECT sentence->>"$.mascot" FROM facts; -+-----------------------------------------+ -| sentence->>"$.mascot" | -+-----------------------------------------+ -| Our mascot is a dolphin named "Sakila". | -+-----------------------------------------+ -``` -### Json值的normalization,merging和autowarpping -#### json值的规范化 -当json值被验证为有效的json文本时,其也会被规范化。***当从左到右对json值进行解析时,如果存在重复的key,那么位于靠前位置的key和value将会被抛弃***。、 -```sql -# key1重复,位于靠后位置的key1将会覆盖位于靠前位置的key1 -mysql> SELECT JSON_OBJECT('key1', 1, 'key2', 'abc', 'key1', 'def'); -+------------------------------------------------------+ -| JSON_OBJECT('key1', 1, 'key2', 'abc', 'key1', 'def') | -+------------------------------------------------------+ -| {"key1": "def", "key2": "abc"} | -+------------------------------------------------------+ -``` -> 在向Json列的插入语句执行时,规范化行为会被执行 - -#### merge json的值 -##### 合并array -在组合多个数组的上下文中,多个数组被merge为一个数组,json_merge_preserve函数会将后面数组的元素串联到前一个数组的末尾。 -```sql -# 返回值为[1, 2, "a", "b", "c", true, false] -JSON_MERGE_PRESERVE('[1, 2]', '["a", "b", "c"]', '[true, false]') -``` -##### 合并json object -在合并多个对象时会产生一个对象,若多个对象中存在相同的key, -- json_merge_preserve函数会将所有对象中具有相同key的value组合到一个array中,从而保证key重复时在新生成对象中的唯一性。 -- json_merge_patch在合并多个对象时,则会将前面哪些重复key对应的value丢弃,只保存最后一个key对应的value -```sql -mysql> SELECT - -> JSON_MERGE_PRESERVE('{"a": 1, "b": 2}', '{"c": 3, "a": 4}', '{"c": 5, "d": 3}') AS Preserve, - -> JSON_MERGE_PATCH('{"a": 3, "b": 2}', '{"c": 3, "a": 4}', '{"c": 5, "d": 3}') AS Patch\G -*************************** 1. row *************************** -Preserve: {"a": [1, 4], "b": 2, "c": [3, 5], "d": 3} - Patch: {"a": 4, "b": 2, "c": 5, "d": 3} -``` -#### autowrapping -当非array值被使用在需要array的上下文时,非array值会被autowrap,该非array值会被[和]包围将其转换为一个array。 -> 故而,在json_merge_preserve对array值和非array值进行merge时,会将非array值autowrap成一个array然后对两个array进行merge -```sql -mysql> SELECT - -> JSON_MERGE_PRESERVE('1', '2') AS Preserve, - -> JSON_MERGE_PATCH('1', '2') AS Patch\G -*************************** 1. row *************************** -Preserve: [1, 2] - Patch: 2 -``` -#### 当array和object进行merge -当array和object进行merge时,会将object autowrap成一个array,然后再对两个array执行merge操作 -```sql -mysql> SELECT - -> JSON_MERGE_PRESERVE('[10, 20]', '{"a": "x", "b": "y"}') AS Preserve, - -> JSON_MERGE_PATCH('[10, 20]', '{"a": "x", "b": "y"}') AS Patch\G -*************************** 1. row *************************** -Preserve: [10, 20, {"a": "x", "b": "y"}] - Patch: {"a": "x", "b": "y"} -``` -### 查找和修改json值 -json值通过json路径表达式来访问其中属性对应的值。 -```sql -SELECT JSON_EXTRACT('{"id": 14, "name": "Aztalan"}', '$.name'); -``` -#### 路径表达式 -路径表达式通过$符号来代表json值的根对象,后面跟着选择器代表该json对象更具体的部分。 -- .keyname用来选择keyname对应的值 -- .path[N]代表path选中的是一个array,下标从0开始,如果path选中的不是一个array,那么path[0]代表的是path本身 -- [M to N]代表了array的一个子集,开始位置为M结束位置为N -- path可以含有*或者**通配符 - - .[*]通配json object中所有的成员属性的值 - - [*]通配array中所有的元素 - - prefix[**]suffix通配所有以prefix开始,以suffix结束的路径 -- 如果path没有在object中出现过,那么path对应的值为NULL -> 如果json的key名中含有空格,则路径表达式中该key名必须用”“括起来 -> ```sql -> {"a fish": "shark", "a bird": "sparrow"} -> $."a fish" evaluates to shark -> $."a bird" evaluates to sparrow -> ``` - -> 如果路径表达式中含有通配通配多个值,那么多个值会作为array返回 -> ```sql -> mysql> SELECT JSON_EXTRACT('{"a": 1, "b": 2, "c": [3, 4, 5]}', '$.*'); -> +---------------------------------------------------------+ -> | JSON_EXTRACT('{"a": 1, "b": 2, "c": [3, 4, 5]}', '$.*') | -> +---------------------------------------------------------+ -> | [1, 2, [3, 4, 5]] | -> +---------------------------------------------------------+ -> mysql> SELECT JSON_EXTRACT('{"a": 1, "b": 2, "c": [3, 4, 5]}', '$.c[*]'); -> +------------------------------------------------------------+ -> | JSON_EXTRACT('{"a": 1, "b": 2, "c": [3, 4, 5]}', '$.c[*]') | -> +------------------------------------------------------------+ -> | [3, 4, 5] | -> +------------------------------------------------------------+ -> ``` - -#### **通配符样例 -```sql -mysql> SELECT JSON_EXTRACT('{"a": {"b": 1}, "c": {"b": 2}}', '$**.b'); -+---------------------------------------------------------+ -| JSON_EXTRACT('{"a": {"b": 1}, "c": {"b": 2}}', '$**.b') | -+---------------------------------------------------------+ -| [1, 2] | -+---------------------------------------------------------+ -``` - -#### 选中array的一个子集 -```sql -mysql> SELECT JSON_EXTRACT('[1, 2, 3, 4, 5]', '$[1 to 3]'); -+----------------------------------------------+ -| JSON_EXTRACT('[1, 2, 3, 4, 5]', '$[1 to 3]') | -+----------------------------------------------+ -| [2, 3, 4] | -+----------------------------------------------+ -1 row in set (0.00 sec) -``` - -#### last的用法 -```sql -mysql> SELECT JSON_EXTRACT('[1, 2, 3, 4, 5]', '$[last-3 to last-1]'); -+--------------------------------------------------------+ -| JSON_EXTRACT('[1, 2, 3, 4, 5]', '$[last-3 to last-1]') | -+--------------------------------------------------------+ -| [2, 3, 4] | -+--------------------------------------------------------+ -1 row in set (0.01 sec) -``` -#### json_set -json_set对于已经存在的path,会替换其path对应的值,对于不存在的path,会对该path添加新值 -```sql -mysql> SET @j = '["a", {"b": [true, false]}, [10, 20]]'; - -mysql> SELECT JSON_SET(@j, '$[1].b[0]', 1, '$[2][2]', 2); -+--------------------------------------------+ -| JSON_SET(@j, '$[1].b[0]', 1, '$[2][2]', 2) | -+--------------------------------------------+ -| ["a", {"b": [1, false]}, [10, 20, 2]] | -+--------------------------------------------+ -``` -#### json_insert -json_insert会插入新值但是不会替换已有的值 -```sql -mysql> SET @j = '["a", {"b": [true, false]}, [10, 20]]'; -mysql> SELECT JSON_INSERT(@j, '$[1].b[0]', 1, '$[2][2]', 2); -+-----------------------------------------------+ -| JSON_INSERT(@j, '$[1].b[0]', 1, '$[2][2]', 2) | -+-----------------------------------------------+ -| ["a", {"b": [true, false]}, [10, 20, 2]] | -+-----------------------------------------------+ -``` -#### json_replace -json_replace会替换已有的值但是不会插入新值 -```sql -mysql> SET @j = '["a", {"b": [true, false]}, [10, 20]]'; -mysql> SELECT JSON_REPLACE(@j, '$[1].b[0]', 1, '$[2][2]', 2); -+------------------------------------------------+ -| JSON_REPLACE(@j, '$[1].b[0]', 1, '$[2][2]', 2) | -+------------------------------------------------+ -| ["a", {"b": [1, false]}, [10, 20]] | -+------------------------------------------------+ -``` -#### json_remove -json_remove会删除path对应的值 -```sql -mysql> SET @j = '["a", {"b": [true, false]}, [10, 20]]'; -mysql> SELECT JSON_REMOVE(@j, '$[2]', '$[1].b[1]', '$[1].b[1]'); -+---------------------------------------------------+ -| JSON_REMOVE(@j, '$[2]', '$[1].b[1]', '$[1].b[1]') | -+---------------------------------------------------+ -| ["a", {"b": [true]}] | -+---------------------------------------------------+ -``` - +- [Mysql Data Type](#mysql-data-type) + - [Mysql数字类型语法](#mysql数字类型语法) + - [整型数据类型](#整型数据类型) + - [非整型数据类型](#非整型数据类型) + - [zerofill](#zerofill) + - [unsigned](#unsigned) + - [serial](#serial) + - [BIT[(M)]](#bitm) + - [TINYINT[(M)] [UNSIGNED] [ZEROFILL]](#tinyintm-unsigned-zerofill) + - [BOOL,BOOLEAN](#boolboolean) + - [SMALLINT[(M)] [UNSIGNED] [ZEROFILL]](#smallintm-unsigned-zerofill) + - [MEDIUMINT[(M)] [UNSIGNED] [ZEROFILL]](#mediumintm-unsigned-zerofill) + - [INT[(M)] [UNSIGNED] [ZEROFILL]](#intm-unsigned-zerofill) + - [BIGINT[(M)] [UNSIGNED] [ZEROFILL]](#bigintm-unsigned-zerofill) + - [DECIMAL[(M[,D])] [UNSIGNED] [ZEROFILL]](#decimalmd-unsigned-zerofill) + - [FLOAT[(M,D)] [UNSIGNED] [ZEROFILL]](#floatmd-unsigned-zerofill) + - [DOUBLE[(M,D)] [UNSIGNED] [ZEROFILL]](#doublemd-unsigned-zerofill) + - [DECIMAL和NUMERIC](#decimal和numeric) + - [BIT](#bit) + - [mysql日期和时间类型](#mysql日期和时间类型) + - [日期和时间类型语法](#日期和时间类型语法) + - [date](#date) + - [datetime(fsp)](#datetimefsp) + - [timestamp(fsp)](#timestampfsp) + - [time(fsp)](#timefsp) + - [YEAR(4)](#year4) + - [mysql中timestamp的存储](#mysql中timestamp的存储) + - [mysql中的时区](#mysql中的时区) + - [time、datetime和timestamp传入无效值时](#timedatetime和timestamp传入无效值时) + - [tmestamp转化为datetime](#tmestamp转化为datetime) + - [mysql中date部分](#mysql中date部分) + - [time类型](#time类型) + - [time类型的缩写](#time类型的缩写) + - [year类型](#year类型) + - [timestamp类型和datetime类型自动初始化和更新](#timestamp类型和datetime类型自动初始化和更新) + - [timestamp或datetime指定小数位精度](#timestamp或datetime指定小数位精度) + - [explicit_defaults_for_timestamp](#explicit_defaults_for_timestamp) + - [时间函数返回带小数位的时间](#时间函数返回带小数位的时间) + - [mysql中时间类型之间的转换](#mysql中时间类型之间的转换) + - [date类型](#date类型) + - [datetime类型和timestamp类型](#datetime类型和timestamp类型) + - [time类型](#time类型-1) + - [时间类型之间的显式转换](#时间类型之间的显式转换) + - [将时间类型转化为数值类型](#将时间类型转化为数值类型) + - [String Data Type](#string-data-type) + - [mysql中String类型的长度单位](#mysql中string类型的长度单位) + - [为字符类型String指定字符集和排序规则](#为字符类型string指定字符集和排序规则) + - [BINARY属性](#binary属性) + - [CHAR[(M)] [CHARACTER SET charset_name] [COLLATE collation_name]](#charm-character-set-charset_name-collate-collation_name) + - [VARCHAR(M) [CHARACTER SET charset_name] [COLLATE collation_name]](#varcharm-character-set-charset_name-collate-collation_name) + - [BINARY[(M)]](#binarym) + - [VARBINARY(M)](#varbinarym) + - [tinyblob](#tinyblob) + - [tinytext [CHARACTER SET charset_name] [COLLATE collation_name]](#tinytext-character-set-charset_name-collate-collation_name) + - [blob[(M)]](#blobm) + - [text[(M)]](#textm) + - [mediumblob](#mediumblob) + - [mediumtext [CHARACTER SET charset_name] [COLLATE collation_name]](#mediumtext-character-set-charset_name-collate-collation_name) + - [longblob](#longblob) + - [longtext [CHARACTER SET charset_name] [COLLATE collation_name]](#longtext-character-set-charset_name-collate-collation_name) + - [ENUM('value1','value2',...) [CHARACTER SET charset_name] [COLLATE collation_name]](#enumvalue1value2-character-set-charset_name-collate-collation_name) + - [SET('value1','value2',...) [CHARACTER SET charset_name] [COLLATE collation_name]](#setvalue1value2-character-set-charset_name-collate-collation_name) + - [char和varchar类型](#char和varchar类型) + - [varchar类型的存储](#varchar类型的存储) + - [varchar和char类型的赋值](#varchar和char类型的赋值) + - [在比较varchar类型和char类型时对后缀空格的处理](#在比较varchar类型和char类型时对后缀空格的处理) + - [binary和varbinary类型](#binary和varbinary类型) + - [binary存储值时的逻辑](#binary存储值时的逻辑) + - [varbinary存储时的逻辑](#varbinary存储时的逻辑) + - [blob类型和text类型](#blob类型和text类型) + - [关于text类型作为索引的比较逻辑](#关于text类型作为索引的比较逻辑) + - [blob/text类型与varchar/varbinary类型的区别](#blobtext类型与varcharvarbinary类型的区别) + - [blob类型和text类型的排序](#blob类型和text类型的排序) + - [enum类型](#enum类型) + - [枚举列的创建](#枚举列的创建) + - [枚举类型的值和序号](#枚举类型的值和序号) + - [枚举类型插入invalid值](#枚举类型插入invalid值) + - [枚举值的比较](#枚举值的比较) + - [枚举值的定义](#枚举值的定义) + - [set类型](#set类型) + - [set类型值的存储](#set类型值的存储) + - [set类型的值赋值即错误处理](#set类型的值赋值即错误处理) + - [set类型值中查找某set元素是否存在](#set类型值中查找某set元素是否存在) + - [Json类型](#json类型) + - [对Json值进行部分更新](#对json值进行部分更新) + - [构造Json值](#构造json值) + - [构造Json Array](#构造json-array) + - [构造Json Object](#构造json-object) + - [嵌套Json Object和Json Array](#嵌套json-object和json-array) + - [json值的插入](#json值的插入) + - [json_type函数](#json_type函数) + - [json_array函数](#json_array函数) + - [json_object函数](#json_object函数) + - [json_merge_preserve函数](#json_merge_preserve函数) + - [json_valid函数](#json_valid函数) + - [json值赋值给用户定义的变量中](#json值赋值给用户定义的变量中) + - [通过cast函数将字符串类型转化为json值](#通过cast函数将字符串类型转化为json值) + - [反斜杠转义'和"](#反斜杠转义和) + - [获取json值中属性的值](#获取json值中属性的值) + - [Json值的normalization,merging和autowarpping](#json值的normalizationmerging和autowarpping) + - [json值的规范化](#json值的规范化) + - [merge json的值](#merge-json的值) + - [合并array](#合并array) + - [合并json object](#合并json-object) + - [autowrapping](#autowrapping) + - [当array和object进行merge](#当array和object进行merge) + - [查找和修改json值](#查找和修改json值) + - [路径表达式](#路径表达式) + - [**通配符样例](#通配符样例) + - [选中array的一个子集](#选中array的一个子集) + - [last的用法](#last的用法) + - [json_set](#json_set) + - [json_insert](#json_insert) + - [json_replace](#json_replace) + - [json_remove](#json_remove) + + +# Mysql Data Type +## Mysql数字类型语法 +### 整型数据类型 +对于整型数据类型,M代表最大的显示宽度。该最大显示宽度是255。显示宽度和该整型类型可以存储范围的值没有关系。***在mysql 8.0.17中,整型的M值已经被遗弃。*** +```sql +# 将M限定为4并不会限制INT类型可以存储值的范围 +INT(4) +``` +> 将M限定为4时,当应用使用该字段整型值时,如果该字段值显示宽度小于M,会在左边填充空格 + +### 非整型数据类型 +对于浮点类型和fixed-point数据类型,M代表该字段总共能够存储多少位,M为能存储的最多位长度(十进制)。 + +### zerofill +对于数字类型的类型,可以为其指定zerofill,在指定为zerofill后,会为其自动添加unsigned约束,该数字类型字段存储的值不论是整型还是浮点数均需要为非负值。 +```sql +# 被zerofill修饰的字段,如果显示宽度小于M,那么都会在左边填充0 + +# 其等同于INT(10) UNSIGNED ZEROFILL +INT(10) ZEROFILL + +DECIMAL(28,2) ZEROFILL +``` +***在mysql8.0.17,zerofill对于数值类型字段也被遗弃了*** + +### unsigned +对于数值类型的字段,可以支持unsiged和signed属性。默认情况下数值类型是signed的,可以手动将其指定为unsigned。 +***在mysql8.0.17,对于float、decimal、double类型,unsigned属性也被遗弃了。可以通过简单的check来进行替换*** + +### serial +serial是一个对于BIGINT UNSIGNED NOT NULL AUTO_INCREMENT UNIQUE的alias。 +```sql +# 如下字段定义 +id serial +# 等同于 +id bigint unsigned not null auto_increment unique +``` + +### BIT[(M)] +bit值类型,M代表每个值的bit位数,M值可以从1到64,默认情况下若M省略,M为1 + +### TINYINT[(M)] [UNSIGNED] [ZEROFILL] +8位整型,有符号范围为-128到127,无符号范围为0到255 +### BOOL,BOOLEAN +该类型等同于TINYINT(1),值为0代表false,值非0代表true +### SMALLINT[(M)] [UNSIGNED] [ZEROFILL] +16位整型,有符号范围为 -32768到32767,无符号范围为0 到 65535 +### MEDIUMINT[(M)] [UNSIGNED] [ZEROFILL] +24位整型,有符号范围为-8388608到8388607,无符号范围为 0 到16777215 +### INT[(M)] [UNSIGNED] [ZEROFILL] +32位整型,有符号范围为-2147483648 到2147483647,无符号范围为0 到4294967295 +### BIGINT[(M)] [UNSIGNED] [ZEROFILL] +64位整型,有符号范围为 -9223372036854775808到9223372036854775807,无符号范围为0到18446744073709551615 +> ***所有算数都是使用带符号的bigint或者double进行运算,故而不要使用bigint大于 9223372036854775807 (63bit)*** + +### DECIMAL[(M[,D])] [UNSIGNED] [ZEROFILL] +fixed-point nubmer,M是非整型的总共的位数,而D对应的则是小数点之后的位数。小数点和负的符号并不包含在M中。 +如果D是0,则代表该数没有小数部分。***对于DECIMAL来说,M最大值为65,D受支持的最大值为30,如果D省略,则D默认为0,若M省略,那么M的默认值为10 + +### FLOAT[(M,D)] [UNSIGNED] [ZEROFILL] +单精度浮点数,M为非整型总共的位数,D为小数点之后的位数。如果M和D省略,那么会根据硬件的限制来存储。单精度浮点数大概精确到小数点后7位。 +> ***FLOAT(M,D)是一个非标准的mysql拓展,在mysql 8.0.17中,该语法被废弃*** + +### DOUBLE[(M,D)] [UNSIGNED] [ZEROFILL] +双精度浮点数,其精度大概精确到小数点后15位 + +### DECIMAL和NUMERIC +DECIMAL和NUMERIC用于存储高精度的数值类型,当保存数字的精度很重要时需要使用这些类型,如货币数据。在mysql中,NUMERIC类型被实现为DECIMAL + +### BIT +对于bit类型,其赋值可以使用b'value',其中value为二进制类型。 +```sql +# 其返回值为true +select b'011' = 3; +``` +## mysql日期和时间类型 +### 日期和时间类型语法 +对于time、datetime、timestamp类型,mysql允许second(秒)具有小数部分,最高精度到小数点后6位(微秒)。 +> ***time、datetime、timestamp定义小数部分格式如下*** +> ***type_name(fsp)*** +> - type_name为date、datetime、timestamp中的一个 +> - fsp为second的小数部分精度 +```sql +CREATE TABLE t1 (t TIME(3), dt DATETIME(6), ts TIMESTAMP(0)); +``` +对于fsp,其必须位于0~6的范围内,0代表其没有小数部分。若fsp省略,则默认情况下fsp为0. +### date +支持的日期是'1000-01-01'到'9999-12-31'。mysql通过'YYYY-MM-DD'的格式来显示date数据,但是对于date类型的赋值,既可以通过字符串类型也可以通过数字类型。 +### datetime(fsp) +***datetime***类型是***date***和***time***类型的组合。datetime类型支持的范围是 '1000-01-01 00:00:00.000000'到'9999-12-31 23:59:59.999999',mysql展示datetime类型值是通过'YYYY-MM-DD hh:mm:ss[.fraction]'格式。同样的,datetime类型的赋值可以通过字符串类型和数字类型。 +### timestamp(fsp) +时间戳类型。timestamp类型支持的范围是'1970-01-01 00:00:01.000000' UTC 到 '2038-01-19 03:14:07.999999' UTC。timestamp类型的值存储着从('1970-01-01 00:00:00' UTC)其经过的秒数。 +> ***timestamp类型不能表示'1970-01-01 00:00:00',因为该时间点等于timestamp类型的计时起点,但是timestamp类型中0已经被预留来代表 '0000-00-00 00:00:00'*** + +> ***explicit_defaults_for_timestamp已启用*** +> 如果 explicit_defaults_for_timestamp 属性已经被启用,那么不会将CURRENT_TIMESTAMP赋值给timestamp字段做默认值,也不会在更新时自动将CURRENT_TIMESTAMP赋值给timestamp字段。并且,任何没有声明为not null的timestamp字段都是可为空的。 + +> ***explicit_defaults_for_timestamp未启用*** +> 除非被显式指定,否则数据表中的timestamp列都会被赋值为最近修改的datetime。该属性开未开启时,可以通过timestamp字段记录上次update或insert时间。 +> 可以通过为timestamp字段赋值一个null来将该字段设置为当前时间,除非该字段在定义时通过NULL属性声明该字段可以为空。 + +> ***自动赋值为当前时间和记录更新时间设置*** +> 可以在定义该timestamp列字段时指定DEFAULT CURRENT_TIMESTAMP和ON UPDATE CURRENT_TIMESTAMP,为该timestamp字段指定默认值和记录最近更新时间。***若explicit_defaults_for_timestamp未启用,默认情况下,若第一个timestamp列没有指定default和on update属性中的任何一个,第一个timestamp列字段具有这些属性,数据表中的任何timestamp字段也可以手动指定这些属性。*** + +### time(fsp) +time类型的范围为'-838:59:59.000000'到'838:59:59.000000',msyql显示time类型的格式为'hh:mm:ss[.fraction]',但是对time类型的赋值既可以用字符串类型也可以用数字类型。 +### YEAR(4) +year类型用来表示年,year类型通过YYYY格式进行展示,为year类型字段赋值既可以通过字符串类型也可以通过数字类型。 + +> 对于时间类型,avg和sum等聚合函数不起作用。该类聚合函数会将值转化为数字,并在遇到第一个非数字字符时转换失败。如果想要对时间类型使用聚合函数,需要在使用前将时间类型转化为数字单元,并在聚合后将数字单元转化为时间类型,如下: +> ```sql +> SELECT SEC_TO_TIME(SUM(TIME_TO_SEC(time_col))) FROM tbl_name; +> SELECT FROM_DAYS(SUM(TO_DAYS(date_col))) FROM tbl_name; +> ``` + +### mysql中timestamp的存储 +在mysql中,timestamp在存储时,会从本地时区转化为UTC时区然后再存储到数据库中,在获取存储的timestamp时会从UTC时区转化为本地时区(该行为在其他类型,如datetime,中则不会发生)。 +### mysql中的时区 +在默认情况下,对于每个Connection,当前时区为mysql server的时区。但是,Connection时区也可以针对每个Connection单独设置(在jdbc:mysql://{server}:{port}/{db_name}?{追加参数}中单独设置)。 +> 由于时区统一转化为UTC后再存储于数据库中,那么在存储了timestamp之后,若再次访问timestamp时时区发生了改变,那么将存储的UTC时区timestamp转化为Connection对应时区时,转化后时区时间和存储时的时区时间并不相同。 + +### time、datetime和timestamp传入无效值时 +对于无效的time、datetime、timestamp值,会将其转化为该类型默认的‘0’值,例如'0000-00-00'或者'0000-00-00 00:00:00'。 + +### tmestamp转化为datetime +可以通过cast()函数和at time zone操作符将timestamp类型转化为datetime类型。 +```sql +SELECT col, + CAST(col AT TIME ZONE INTERVAL '+00:00' AS DATETIME) AS ut + FROM ts ORDER BY id; +``` + +### mysql中date部分 +- 在mysql中,对于date类型或者timestamp或datetime中的date部分,任何标点符号都可以作为分隔符 + ```sql + '10:11:12' 代表 '2010-11-12' + '10:11:12 00:00:00' 代表 '2010-11-12 00:00:00' + ``` +- 当date中年份只包含两位数时,若年份为00~69,则会变为2000~2069,若年份为70~99,则年份为1970~1999 + +### time类型 +mysql中获取和展示time类型的格式为'hh:mm:ss'。time的范围为'-838:59:59' 到 '838:59:59'。 +> ***time范围*** +> 之所以time的范围会如此之大并且支持负值,是因为time类型不仅可以用来表示一天中的时间点,还可以用来表示两个时间点之间的间隔(间隔可能超过24h,也有可能为负值) + +### time类型的缩写 +- 缩写中带有冒号':'时,值将会被解释为一天中的时间点,并且,’11:12'将会被解释为'11:12:00'而不是'00:11:12' +- 缩写中不带冒号时,将会被解释为两个时间点之间的间隔。并且,最右边的两个数将会被解释为秒,'1112'和1112也会被解释为'00:11:12' + +### year类型 +在mysql 8.0.19中,不再支持year(4)这种显式指定宽度的方式已经被废弃,并且,过去支持的year(2)类型在8.0.19中也不再被支持。 +> year类型支持的范围是'1901'到'2155',也可通过数字类型1901到2155指定 + +### timestamp类型和datetime类型自动初始化和更新 +对于timestamp类型和datetime类型,可以被自动初始化和更新到当前的时间。 +> 当对timestamp或者datetime字段设置了on update current_timestamp时,若某行中任一其他列的值发生变化,该列变化前后值不同,那么设置了on update的datetime或timestamp字段会更新到当前时间。若update前后所有的列值都与修改之前相同,那么即使执行了update操作,on update列的时间也不会更新。 +> ***如果想要防止设置了on update的列在其他列被修改时更新为当前时间,可以显式将该列设置为该列当前的值。*** +> ```sql +> # 即使dt被设置了on update,id变更后dt仍然为当前的值 +> update dt=dt,salary+=10 wherre id = 1000 + +> ### current_timestamp的同义词 +> 如下同义词都和current_timestamp具有相同的含义 +> - current_timestamp() +> - now() +> - localtime +> - localtime() +> - localtimestamp +> - localtimestamp() + +### timestamp或datetime指定小数位精度 +如果timestamp或者datetime类型在声明时使用了小数位精度,那么在current_timestamp后也要指定相同的精度。 +```sql +CREATE TABLE t1 ( + ts TIMESTAMP(6) DEFAULT CURRENT_TIMESTAMP(6) ON UPDATE CURRENT_TIMESTAMP(6) +); +``` + +### explicit_defaults_for_timestamp +当explicit_defaults_for_timestamp关闭时,timestamp默认情况下是not null的,该类字段不能含有null值,若将字段显式设置为null时,字段值实际被设置为当前时间。若想要允许该字段包含null,可以在声明该字段时显式指定null属性,此时默认值也会变为null。 +当explicit_defaults_for_timestamp启用时,只有指定timestamp字段null属性时该字段才能为空。并且,在explicit_defaults_for_timestamp启用时不可以通过赋值字段为null的形式来将timestamp字段设置为当前值。 +> ***当explicit_defaults_for_timestamp开启时,默认条件下timestamp类型声明会自动添加null default null*** + +### 时间函数返回带小数位的时间 +默认情况下,now()或current_timestamp()函数返回当前的时间戳,如果参数为空,返回结果并不含有小数位。如果想要为now()或者current_timestamp()函数的返回结果指定小数位,可以为函数参数指定一个0~6的整型参数用来限制返回结果时间的小数点位数。 +```sql +# 返回时间戳带有4位小数 +select current_timestamp(4); +``` + +### mysql中时间类型之间的转换 +#### date类型 +- date类型转化为datetime或timestamp类型时,时间类型将会添加'00:00:00'部分 +- date类型转化位time类型时并没有用处,只会转化为'00:00:00' +#### datetime类型和timestamp类型 +- 当datetime类型或timestamp类型转化为date类型时,会考虑time部分秒的小数部分并且对time部分进行舍入,若time部分为‘23:59:59.499’会舍入到当天,而‘23:59:59.500’则是会舍入到下一天。 + ```sql + '1999-12-31 23:59:59.499' -> '1999-12-31' # 舍入到当天 + '1999-12-31 23:59:59.500' -> '2000-01-01' # 舍入到下一天 + ``` +- 当datetime或timestamp类型转化为time类型时,会丢弃date部分 +#### time类型 +- 如果time类型转化为datetime类型或是timestamp类型,current_date()返回的date将会被用作date部分。***并且,time部分会看作时间段,并且附加在current_date()返回的date部分上。如果time类型的值范围位于'00:00:00'和'23:59:59'之外,那么date部分会根据time部分的值向前或向后变化。*** +```sql +# 若当前current_date()的返回值为'2012-01-01' + +# 当time为'12:00:00'时,转化成的datetime为 +'2012-01-01 12:00:00' + +# 当time为'24:00:00'时,转化成的datetime为 +'2012-01-02 00:00:00' + +# 当time为'-12:00:00'时,转化成的datetime为 +'2011-12-31 12:00:00' +``` +- 当time类型转化为date时,逻辑和上面一样,但是会舍弃得到datetime结果的time部分,即结果为'2012-01-01'、'2012-01-02'或'2011-12-31' +#### 时间类型之间的显式转换 +时间类型之间的显式转换可以覆盖隐式转换。例如,在比较date类型和datetime类型时,默认会将date类型转化为datetime类型之后,再比较两个datetime类型。 +如果想要再比较date类型和datetime类型时,忽略datetime的time部分,可以手动将datetime类型转化为date类型之后再进行比较。 +```sql +# 将datetime类型手动通过cast函数转化为date类型之后再进行比较 +date_col = CAST(datetime_col AS DATE) +``` +#### 将时间类型转化为数值类型 +如果想要将时间类型转化为数值类型,可以使用如下方式: +```sql +select datetime_col+0,date_col+0,time_col+0,timestamp_col+0; +``` +当时间类型转化为数值类型时,如果时间类型没有小数部分(N为0),则会转化为整型类型;如果时间类型含有小数部分,则会转化为decimal类型,decimal类型具有n位小数。 + +## String Data Type +mysql中String类型可以分为如下: +- CHAR +- VARCHAR +- BINARY +- VARBINARY +- BLOB +- TEXT +- ENUM +- SET +### mysql中String类型的长度单位 +> 对于字符类型String的列(例如CHAR,VARCHAR,TEXT),mysql将其长度规格指定为字符单元。 +> 对于二进制类型的String列(例如BLOB,BINARY,VARBINARY),mysql将长度规格指定为字节单元。 +### 为字符类型String指定字符集和排序规则 +对于字符类型的String字段(CHAR,VARCHAR,TEXT,ENUM,SET),可以通过CHARACTER SET属性指定该String字段的字符集,并且可以通过COLLATE属性指定该字段的排序规则。 +> 为字符类型String指定character set和collate的规则: +> 1. 如果charset和collate都被指定,则指定的charset和collate被使用 +> 2. 如果charset被指定而collate未被指定,那么指定的charset和charset默认的collate会被使用 +> 3. 如果collate被指定但是charset未被指定,那么指定的collate和collate关联的charset会被使用 +> 4. 如果charset和collate都没有被指定,那么该字符类型String字段会使用该table表对应的charset和collate + +#### BINARY属性 +binary属性是用来指定二进制collate的快捷方式。当使用binary属性来修饰字符类型String字段后,该字段的比较和排序都基于的是字符编码的数值而不是字符排序。 +```sql +CREATE TABLE t +( + c1 VARCHAR(10) CHARACTER SET latin1 BINARY, + c2 TEXT BINARY +) CHARACTER SET utf8mb4; +``` +### CHAR[(M)] [CHARACTER SET charset_name] [COLLATE collation_name] +该String为固定长度为M的字符串,存储时会再字符串的右边一直填充空格字符直到字符串长度为M。M代表 +### VARCHAR(M) [CHARACTER SET charset_name] [COLLATE collation_name] +M代表该列字段最长支持的字符长度(以字符为单位,而不是字节)。M的范围可以为 0到65,535。varchar类型的最长长度受mysql表数据的最大row size限制(row size最大为65,535 bytes,该上限由该行数据的所有字段之间进行共享)。***除了rowsize限制之外,varchar类型的最长长度也受到选用字符集的限制***,如utf8mb3字符集每个字符最多需要3个字节,故而对于utf8mb3字符集最多可以含21,844个字符。***每个字符需要的字节数也会影响该varchar类型含有的字符数M的上限。*** +### BINARY[(M)] +BINARY类型和CHAR和VARCHAR类型类似,但是BINARY类型存储的是二进制类型,以字节为单位。M代表该列的长度,以二进制字节为单位。***如果M省略,则M默认为1*** +### VARBINARY(M) +VARBINARY类型和VARCHAR类型类似,但是存储的是以字节为单位的二进制数据。M代表该列字段以字节为单位的最长长度。 +### tinyblob +tinyblob类型最多可以存储255个字节长度的二进制数据。每个tinyblob类型的数据存储都需要用一个字节的前缀来记录该tinyblob值的长度(以字节为单位)。 +### tinytext [CHARACTER SET charset_name] [COLLATE collation_name] +text类型的数据,但是最多存储255个字节长度的字符串。如果字符串中包含的字符占用多个字节,那么字符的最大个数小于255.每个tinytext类型值采用一个字节来记录该tinytext类型值的长度(以字节为单位)。 +### blob[(M)] +一个blob列,最多含有65535(2^16-1)个字节。blob类型会使用两个字节的前缀来记录该blob类型的最大长度(以字节为单位)。 +> 可以为blob类型指定一个M,如果M被指定,那么mysql在创建该列时会使用可以包含M个字节数据的最小的blob类型。 + +### text[(M)] +一个text列,最多可以含有65535个字符的数据。如果text类型的值含有多字节字符,那么最大字符数量将小于65535.每个text列都会采用2字节的前缀用来记录该text值长度(以字节为单位)。 +> 可以为text类型指定一个M,如果M被指定,那么mysql在创建该列时会采用可以包含M个***字符***的最小类型的text。 + +### mediumblob +一个blob列,最多可以存储16,777,215(2^24-1,约16MB)个字节的二进制数据,该类型会使用3字节的前缀来记录长度。 +### mediumtext [CHARACTER SET charset_name] [COLLATE collation_name] +一个text列,最多可以存储16,777,215(2^24-1,约16MB)个字符的文数据,如果该text列的值含有多字节字符,那么该列存储字符的最大数量少于该上限。每个mediumtext类型使用3字节的前缀来记录长度。 +### longblob +一个blob列,最多可以存储4,294,967,295 (2^32-1,约4GB)字节的二进制数据。每个longblob类型都使用4字节前缀来记录该longblob列的长度(以字节为单位)。 +### longtext [CHARACTER SET charset_name] [COLLATE collation_name] +一个text列,最多可以存储4,294,967,295个字符的文本。如果含有多字节字符,那么存储的最大字符数量会小于该上限。每个longtext类型会使用4字节的前缀来存储该值长度(以字节为单位)。 +### ENUM('value1','value2',...) [CHARACTER SET charset_name] [COLLATE collation_name] +枚举类型,一个字段可以含有value1,value2,……中的一个值或者null。enum类型在内部是通过整数类型来表示的,value1从1开始,可以通过select enum_col+0的形式来查看整数值。 +> enum类型最多可以含有 65,535 个不同的enum元素。 +> 对于每个enum元素,其最大支持长度需要满足如下条件: +> - M<=255 +> - M * w<=1020 +> +> 其中M是字符串文本的字符长度,而w则是该文本字符所属字符集中单个字符所需要的最大字节数 + +### SET('value1','value2',...) [CHARACTER SET charset_name] [COLLATE collation_name] +string集合,每个string对象可以0个或多个值,每个值都必须属于value1,value2,……。set类型在内部通过int表示。 +> set类型最多可以含有64个不同的值。 +> 对于每个set元素,其最大支持长度需要满足如下条件: +> - M<=255 +> - M * w<=1020 +> +> 其中M是字符串文本的字符长度,而w则是该文本字符所属字符集中单个字符所需要的最大字节数 + +> 对于set类型字段,插入时如果包含set类型中多个值,多个值之间可以通过逗号','分隔 +> 对于包含多个set元素的值,其值中元素的顺序和出现次数并不重要,'b,b,a,c'和'c,b,b,c,a'在插入后都会变为'a,b,c' + +> 可以通过find_in_set('value',set_col)来查询某个值是否在set_col中 +### char和varchar类型 +对于char类型,其长度是固定的,并且长度需要在创建表字段时指定(长度位于0到255之间)。当char类型的值在存储时,***会右填充空格直到长度到达指定长度M***。当char类型的值在获取时,***会删除尾随填充的空格,除非PAD_CHAR_TO_FULL_LENGTH的sql mode被启用***。 +> 如果字段类型被设置为char类型,并且pad_char_to_full_length关闭,那么如果向char类型字段中存入字符串,***在取出存入字符串时,存入字符串末尾若存在空格,那么在取出字符串时会删除尾随空格,即使尾随空格不是存入时填充的而是字符串本身就含有的尾随空格*** +> ```sql +> char_col char(30) +> +> # 插入字符串值存在尾随空格 +> insert into string_tb(char_col) values (' a '); +> # 获取时,字符串本身含有的尾随空格也会被删除 +> # 返回值并不是' a 1'而是' a1' +> select concat(char_col,'1') from string_tb; +> ``` +#### varchar类型的存储 +相较于char类型,varchar类型在存储时,会额外存储1到2个字节长度的前缀,用来记录该varchar类型的长度(以字节为单位)。***当该varchar类型值存储不超过255字节时,使用一个字节来记录长度,否则使用两个字节来记录长度***。 +#### varchar和char类型的赋值 +如果对一个varchar或char类型的字段进行赋值操作,且赋值超过该类型的最大长度: +- 如果strict_mode没有开启,那么赋的值会被截断,并且产生一个warning。 +- 如果strict_mode已经被启用,那么在截断非空格字符时,会产生一个异常,并且阻止插入操作。 +- 对于varchar类型,在插入时,超过该列长度限制的尾随空格将会被截断,并且会生成一个warning,不论strict_mode是否开启。 +- 对于char类型,在插入时,超过该列长度限制的尾随空格也会被截断,并且截断操作是静默的,并不会生成warning。 + +#### 在比较varchar类型和char类型时对后缀空格的处理 +mysql的collate具有一个pad attribute,可以有两个值: +- pad space(绝大多数的mysql collate都具有pad space属性) +- no pad + +pad space和no pad的区别: +- pad space:在比较时,尾随空格并不会参加比较,尾随空格并不重要 +- no pad:在比较时,尾随空格参与比较,尾随空格在比较时重要 +> 默认情况下,mysql默认字符集为utf8mb4,该字符集默认collate为utf8mb4_0900_ai_ci,其pad attribute为no pad。故而,在默认情况下,mysql对字符串进行比较时,***忽略大小写并且尾随空格参与比较***。 + +> 如果某字符串列其collate的pad attribute为pad space,且该列拥有unique约束,那么在插入值时,'a'和'a '同时插入会对该列产生duplicate-key错误。 + +### binary和varbinary类型 +binary,varbinary类型和char,varchar类型相似,不同的是binary、varbinary类型存储二进制形式的字符串。 +> 当插入字符串超过binary或varbinary的最长限制长度时,若***strict mode未启用,则字符串会被截断来适应该二进制列的最大长度,并且会产生一个warning***;若***strcit mode已启用,那么会抛出异常并且插入失败*** + +#### binary存储值时的逻辑 +当binary类型的值被存储时,该插入的值会右填充(0x00)的值直到到达指定长度。并且,在对binary值进行获取时,***并不会删除尾随(0x00)。*** +在对binary类型值进行比较时,所有字节都重要,比较时并不会忽略尾随(0x00)字节。 +> 对于binary类型,其和char类型不同,并不删除尾随字符,其意味着如果‘a’同时插入binary(3)的col_3列和binary(6)的col_6列,从col_3和col_6中取出存储的值进行比较时,col_3和col_6取出的值并不相同,col_3为0x610000,col_6为0x610000000000。 + +#### varbinary存储时的逻辑 +对于varbinary类型,存储时并不会插入尾随字符,获取时也不会删除尾随字符。 + +### blob类型和text类型 +blob类型可以看作一个存储大量二进制字符串数据的对象,而text类型可以看作一个存储大量字符型字符串的对象。 +> 如果strict mode没有开启,那么在给blob类型字段分配超过其长度限制的值时,会对分配的值进行截断并且抛出一个warning;如果strict mode已经开启,那么当截断非空格字符时会抛出异常并且插入失败。 +> 对于text类型,如果插入时对插入值的尾随空格进行截断,那么会抛出警告,无论strict mode是否开启。 + +对于blob类型和text类型,在插入时并不会有向右的填充字符,在获取时也不会删除尾随字符。(类似于varchar和varbinary)。 + +#### 关于text类型作为索引的比较逻辑 +如果将text类型的列作为索引,必须要指定索引前缀长度,当text值小于索引前缀长度时对索引列的text类型值进行比较时会向右填充空格。即如果该索引需要unique约束,那么两个除了后缀空格数目外完全相同的text值将会产生duplicate-key异常。 +#### blob/text类型与varchar/varbinary类型的区别 +在大多数情况下,blob/text类型和varchar/varbinary都相同,但是在如下方面会有所区别: +- 在对blob/text类型字段添加索引时,必须要指定索引前缀长度M。(在对单列创建索引时,必须要复制索引列的值到特定的数据结构如B+树中,故而强制要求blob/text类型创建索引时指定索引前缀长度能够有效的减少复制索引值消耗的空间和比较索引的开销) + ```sql + create table tb_blob_index ( + blob_col blob, + index(blob_col(M)) + ); + ``` +- 对于blob类型和text类型的字段并不能为其指定default默认值 + +#### blob类型和text类型的排序 +由于blob类型和text类型的值可能会很长,故而在对blob类型和text类型的值进行排序时,只会对列开头max_sort_length字节长度的内容进行排序。max_sort_length默认值为1024. +```sql +show variables like 'max_sort_length'; # default 1024 +``` +### enum类型 +枚举类型是一个String对象,该对象的值可以从允许的值范围中进行选取。允许的值范围在定义枚举字段时显式指定。 +#### 枚举列的创建 +```sql +create tb_enum ( + id bigint auto_increment primary key, + size enum('x-small', 'small', 'medium', 'large', 'x-large') +); +``` +当在插入枚举时,可以有效降低空间的占用量。对于‘medium’值的插入,一百万条枚举类型值的插入只消耗一百万字节,而将'medium‘类型作为字符串插入时,一百万条数据的插入会消耗六百万字节的空间。 +#### 枚举类型的值和序号 +- mysql中,对于每个枚举类型的值都有一个序号。对于在枚举类型列定义时在括号中指定的枚举类型值,都对应了一个整数序号,序号从1开始(如"small","medium","large"对应1,2,3)。 +- 对枚举空字符串(代表error),其对应的index是0. +- 对于枚举值NULL,其index也为NULL + +如果想要通过数值的形式获取枚举值,可以通过select enum_col+0的形式,获取枚举值时该枚举值对应的index被返回。 + +#### 枚举类型插入invalid值 +如果对枚举类型插入invalid值(插入值不位于enum定义时的可选值中): +- strict mode没有开启时,会插入一个''作为错误值,该''对应的index是0 +- 若strict mode开启,那么插入invalid值会直接导致异常 + +如果枚举列声明可为null,那么null对于该枚举列则是有效值,该枚举列的默认值为null;如果枚举列声明为not null,那么该枚举列的默认值是index为1对应的枚举值。 + +#### 枚举值的比较 +枚举值的比较是通过比较枚举值的index来进行的。 +```sql +create table tb_enum (enum_col enum('b','a')); + +# 若enum_col_b对应的枚举值是’b',enum_col_a对应的枚举值为enum_col_a +# 那么 +enum_col_b < enum_col_a +``` +#### 枚举值的定义 +在定义枚举值时,不应该通过表达式来指定枚举值。 +```sql +# 无法通过concat来指定枚举值为'medium' +CREATE TABLE sizes ( + size ENUM('small', CONCAT('med','ium'), 'large') +); + +# 无法将枚举值指定为变量 +SET @mysize = 'medium'; +CREATE TABLE sizes ( + size ENUM('small', @mysize, 'large') +); +``` +### set类型 +set类型是一个字符串类型,其可以含有0个或多个值,每个值都从许可值的范围内选择。set类型的值由一系列set类型的成员组成,成员之间通过逗号','分隔。 +```sql +# 例如,set('one','two')类型,可以含有如下可能值 +'one' +'one,two' +'two' +'' +``` +每个set类型最多可以含有64个不同的成员,再set类型字段定义时,如果为set类型字段指定重复的set成员,会触发警告或抛出异常(取决于strict sql mode是否被开启)。 +> 在set类型字段的定义中,如果set类型的成员含有尾随空格,那么尾随空格将会在表创建时自动被移除。 + +***可以为set类型字段分配字符集和collate,对于二进制或者大小写敏感的collate,在赋值时大小写是敏感的。*** +> 默认情况下,set类型字段和字符集和collate与数据表相同,在对set类型字段赋值或者进行比较时并不会区分大小写。 + +#### set类型值的存储 +mysql通过整型来存储set的值,每个候选值对应整型的一个bit。 +| set member | decimal value | binary value | +| :-: | :-: | :-: | +| 'a' | 1 | 0001 | +| 'b' | 2 | 0010 | +| 'c' | 4 | 0100 | +| 'd' | 8 | 1000 | + +```sql +# 如果mysql赋值给set类型字段的值为9, +# 则代表该set类型值组成为8+1 +# 则set字段值为'a,d' +``` +如果一个set类型的值含有多个set元素,那么元素之间的排列顺序并没有影响,'a,d'和'd,a'完全相同,值均为9.并且,对于set类型的值,其中一个元素出现的次数也对set值完全没有影响,'a,d,d,d,a,d,a'和'a,d'也完全相同,值均为9. +#### set类型的值赋值即错误处理 +对于set类型,如果赋值中出现了一个定义该set类型字段时没有出现的元素,那么mysql会进行如下处理: +- 若strict sql mode开启,那么会产生异常 +- 如果strict sql mode关闭,那么该值会被忽略,赋值正常执行,并且产生一个warning +#### set类型值中查找某set元素是否存在 +对于在set值中查找某元素是否存在,可以通过find_in_set(eleVal,setVal)函数或者like操作符。find_in_set会返回元素在set值中第一次出现的位置,如果元素在set值中没有出现,那么会返回0. +```sql +mysql> SELECT * FROM tbl_name WHERE FIND_IN_SET('value',set_col)>0; +mysql> SELECT * FROM tbl_name WHERE set_col LIKE '%value%'; +``` +想要查找set值中是否存在某元素,同样可以通过如下方法: +```sql +# 查找set值中是否存在第一个元素'a' +mysql> SELECT * FROM tbl_name WHERE set_col & 1; +``` +想要对set值进行精准比较,可以通过如下方法 +```sql +# 在比较时, +# set_col='val1,val2' +# 和 +# set_col='val2,val1' +# 的返回结果是不同的,在指定set值是元素值顺序应该按照定义set列时 +# 指定的顺序 +mysql> SELECT * FROM tbl_name WHERE set_col = 'val1,val2'; +``` + +## Json类型 +Mysql本地支持Json类型,允许对Json中的数据进行高效访问。同传统的通过字符串来存储json数据的方式相比,通过Json String来存储json数据具有如下优势: +- 对存储的Json数据进行自动验证,无效的Json数据将会产生异常 +- 优化存储格式,存储在json列中的json数据将会允许对json数据属性的快速读访问。以二进制格式存储在json列中的json数据,如果稍后要对其进行访问,该存储的值无需转化为文本表示之后再解析。***二进制结构的值使服务器能够通过key或者数组index来查找子对象或者嵌套值,而不需要读取所有位于他们之前或之后的值。*** +### 对Json值进行部分更新 +在mysql 8.0中,优化器可以对Json值执行部分更新而不是移除整个旧Json值然后写入新的Json值。 +Json部分更新需要满足如下条件: +1. 该部分更新的列需要是Json列 +2. 该部分更新的update语句需要使用如下三个函数json_set(),json_replace(),json_remove()来更新Json列。任何对Json列的直接赋值都***不会***使用部分更新 +3. 该更新语句的输入列和输出列需要是相同的列,如果UPDATE mytable SET jcol1 = JSON_SET(jcol2, '$.a', 100),其中jcol2和jcol1的列不同,那么更新jcol1时并不会使用部分更新的特性 + > 只要输入列和输出列的值相同,可以通过上诉三个函数的嵌套组合来对json列进行部分更新 +4. 更新语句只是对现存对象或者array的值进行修改,而不会在父对象或者array中加入新值 +5. 被替换的旧值只要要和要替换的新值一样大,旧值所占空间大小不能比新值小 + > 此需求可能存在例外,如果在为旧值进行部分更新时,留下的空间足够容纳更大的新值,那么在对新值进行部分更新时也不会抛出异常 + +### 构造Json值 +#### 构造Json Array +```sql +["abc", 10, null, true, false] +``` +##### 构造Json Object +```sql +{"k1": "value", "k2": 10} +``` +#### 嵌套Json Object和Json Array +```sql +[99, {"id": "HK500", "cost": 75.99}, ["hot", "cold"]] +{"k1": "value", "k2": [10, 20]} +``` +#### json值的插入 +```sql +INSERT INTO t1 VALUES('{"key1": "value1", "key2": "value2"}'); +``` +#### json_type函数 +该函数接受一个json值,并且返回该json值的类型 +```sql +# 返回结果Object +select json_type('{"name":"kazusa","age":17}'); + +# 返回结果Array +select json_type('[{"name":"kazusa","age":17}]'); +``` +#### json_array函数 +json_array函数接受一系列值并且返回json array +```sql +# 返回结果 ["a", 1, "2015-07-27 09:43:47.000000"] +SELECT JSON_ARRAY('a', 1, NOW()); +``` +#### json_object函数 +json_object函数接受一系列的键值对,并且返回一个json object对象 +```sql +# 返回结果 {"key1": 1, "key2": "abc"} +SELECT JSON_OBJECT('key1', 1, 'key2', 'abc'); +``` +#### json_merge_preserve函数 +json_merge_reserve函数接受两个或更多的json值并且返回一个整合后的结果 +```sql +# 返回结果是 ["a", 1, {"key": "value"}] +SELECT JSON_MERGE_PRESERVE('["a", 1]', '{"key": "value"}'); + +# 返回结果是 {"age": 17, "name": "kazusa", "roles": "painoist", "is_male": false} +select json_merge_preserve('{"name":"kazusa","age":17}','{"roles":"painoist","is_male":false}'); +``` +#### json_valid函数 +json_valid函数接受一个表示json值的字符串,并且返回该字符串是否是合规的json值 +```sql +# 返回结果为1,0,0 +select json_valid('null'),json_valid('Null'),json_valid('NULL'); +``` +#### json值赋值给用户定义的变量中 +在mysql中,可以将json值赋值给变量。***但是,变量值并不支持json类型,在将json值赋值给用户自定义变量时,json值会被转化为字符串类型。*** +> 将Json值转化为字符串类型时,字符串类型具有和Json类型相同的charset和collate,Json转化为的字符串其字符集为utf8mb4,并且其collate为utf8mb4_bin(大小写敏感) +```sql +SET @j = JSON_OBJECT('key', 'value'); +SELECT @j; +``` +由于json值的collate是utf8mb4_bin,即json值是大小写敏感的,故而'null','Null','NULL'中只有'null'是有效的json值 +```sql +# 返回结果为1,0,0 +select json_valid('null'),json_valid('Null'),json_valid('NULL'); +``` +#### 通过cast函数将字符串类型转化为json值 +```sql +cast('null' as json) +``` +#### 反斜杠转义'和" +在json值的String表示中,如果想在json值中的文本值中含有'或"符号,需要在前面加上反斜杠进行转义。 +```sql +# 通过create_object插入json值,只需要在前面加上一个反斜杠进行转义 +mysql> INSERT INTO facts VALUES + > (JSON_OBJECT("mascot", "Our mascot is a dolphin named \"Sakila\".")); + +# 通过values插入json值时,需要在前面加上两个反斜杠进行转义 +mysql> INSERT INTO facts VALUES + > ('{"mascot": "Our mascot is a dolphin named \\"Sakila\\"."}'); +``` +#### 获取json值中属性的值 +```sql +# 查询json值中属性的值时用""将结果括起来并含有转义符 +mysql> SELECT sentence->"$.mascot" FROM facts; ++---------------------------------------------+ +| col->"$.mascot" | ++---------------------------------------------+ +| "Our mascot is a dolphin named \"Sakila\"." | ++---------------------------------------------+ +1 row in set (0.00 sec) + +# 查询json值中属性的值时不用”“和转义符 +mysql> SELECT sentence->>"$.mascot" FROM facts; ++-----------------------------------------+ +| sentence->>"$.mascot" | ++-----------------------------------------+ +| Our mascot is a dolphin named "Sakila". | ++-----------------------------------------+ +``` +### Json值的normalization,merging和autowarpping +#### json值的规范化 +当json值被验证为有效的json文本时,其也会被规范化。***当从左到右对json值进行解析时,如果存在重复的key,那么位于靠前位置的key和value将会被抛弃***。、 +```sql +# key1重复,位于靠后位置的key1将会覆盖位于靠前位置的key1 +mysql> SELECT JSON_OBJECT('key1', 1, 'key2', 'abc', 'key1', 'def'); ++------------------------------------------------------+ +| JSON_OBJECT('key1', 1, 'key2', 'abc', 'key1', 'def') | ++------------------------------------------------------+ +| {"key1": "def", "key2": "abc"} | ++------------------------------------------------------+ +``` +> 在向Json列的插入语句执行时,规范化行为会被执行 + +#### merge json的值 +##### 合并array +在组合多个数组的上下文中,多个数组被merge为一个数组,json_merge_preserve函数会将后面数组的元素串联到前一个数组的末尾。 +```sql +# 返回值为[1, 2, "a", "b", "c", true, false] +JSON_MERGE_PRESERVE('[1, 2]', '["a", "b", "c"]', '[true, false]') +``` +##### 合并json object +在合并多个对象时会产生一个对象,若多个对象中存在相同的key, +- json_merge_preserve函数会将所有对象中具有相同key的value组合到一个array中,从而保证key重复时在新生成对象中的唯一性。 +- json_merge_patch在合并多个对象时,则会将前面哪些重复key对应的value丢弃,只保存最后一个key对应的value +```sql +mysql> SELECT + -> JSON_MERGE_PRESERVE('{"a": 1, "b": 2}', '{"c": 3, "a": 4}', '{"c": 5, "d": 3}') AS Preserve, + -> JSON_MERGE_PATCH('{"a": 3, "b": 2}', '{"c": 3, "a": 4}', '{"c": 5, "d": 3}') AS Patch\G +*************************** 1. row *************************** +Preserve: {"a": [1, 4], "b": 2, "c": [3, 5], "d": 3} + Patch: {"a": 4, "b": 2, "c": 5, "d": 3} +``` +#### autowrapping +当非array值被使用在需要array的上下文时,非array值会被autowrap,该非array值会被[和]包围将其转换为一个array。 +> 故而,在json_merge_preserve对array值和非array值进行merge时,会将非array值autowrap成一个array然后对两个array进行merge +```sql +mysql> SELECT + -> JSON_MERGE_PRESERVE('1', '2') AS Preserve, + -> JSON_MERGE_PATCH('1', '2') AS Patch\G +*************************** 1. row *************************** +Preserve: [1, 2] + Patch: 2 +``` +#### 当array和object进行merge +当array和object进行merge时,会将object autowrap成一个array,然后再对两个array执行merge操作 +```sql +mysql> SELECT + -> JSON_MERGE_PRESERVE('[10, 20]', '{"a": "x", "b": "y"}') AS Preserve, + -> JSON_MERGE_PATCH('[10, 20]', '{"a": "x", "b": "y"}') AS Patch\G +*************************** 1. row *************************** +Preserve: [10, 20, {"a": "x", "b": "y"}] + Patch: {"a": "x", "b": "y"} +``` +### 查找和修改json值 +json值通过json路径表达式来访问其中属性对应的值。 +```sql +SELECT JSON_EXTRACT('{"id": 14, "name": "Aztalan"}', '$.name'); +``` +#### 路径表达式 +路径表达式通过$符号来代表json值的根对象,后面跟着选择器代表该json对象更具体的部分。 +- .keyname用来选择keyname对应的值 +- .path[N]代表path选中的是一个array,下标从0开始,如果path选中的不是一个array,那么path[0]代表的是path本身 +- [M to N]代表了array的一个子集,开始位置为M结束位置为N +- path可以含有*或者**通配符 + - .[*]通配json object中所有的成员属性的值 + - [*]通配array中所有的元素 + - prefix[**]suffix通配所有以prefix开始,以suffix结束的路径 +- 如果path没有在object中出现过,那么path对应的值为NULL +> 如果json的key名中含有空格,则路径表达式中该key名必须用”“括起来 +> ```sql +> {"a fish": "shark", "a bird": "sparrow"} +> $."a fish" evaluates to shark +> $."a bird" evaluates to sparrow +> ``` + +> 如果路径表达式中含有通配通配多个值,那么多个值会作为array返回 +> ```sql +> mysql> SELECT JSON_EXTRACT('{"a": 1, "b": 2, "c": [3, 4, 5]}', '$.*'); +> +---------------------------------------------------------+ +> | JSON_EXTRACT('{"a": 1, "b": 2, "c": [3, 4, 5]}', '$.*') | +> +---------------------------------------------------------+ +> | [1, 2, [3, 4, 5]] | +> +---------------------------------------------------------+ +> mysql> SELECT JSON_EXTRACT('{"a": 1, "b": 2, "c": [3, 4, 5]}', '$.c[*]'); +> +------------------------------------------------------------+ +> | JSON_EXTRACT('{"a": 1, "b": 2, "c": [3, 4, 5]}', '$.c[*]') | +> +------------------------------------------------------------+ +> | [3, 4, 5] | +> +------------------------------------------------------------+ +> ``` + +#### **通配符样例 +```sql +mysql> SELECT JSON_EXTRACT('{"a": {"b": 1}, "c": {"b": 2}}', '$**.b'); ++---------------------------------------------------------+ +| JSON_EXTRACT('{"a": {"b": 1}, "c": {"b": 2}}', '$**.b') | ++---------------------------------------------------------+ +| [1, 2] | ++---------------------------------------------------------+ +``` + +#### 选中array的一个子集 +```sql +mysql> SELECT JSON_EXTRACT('[1, 2, 3, 4, 5]', '$[1 to 3]'); ++----------------------------------------------+ +| JSON_EXTRACT('[1, 2, 3, 4, 5]', '$[1 to 3]') | ++----------------------------------------------+ +| [2, 3, 4] | ++----------------------------------------------+ +1 row in set (0.00 sec) +``` + +#### last的用法 +```sql +mysql> SELECT JSON_EXTRACT('[1, 2, 3, 4, 5]', '$[last-3 to last-1]'); ++--------------------------------------------------------+ +| JSON_EXTRACT('[1, 2, 3, 4, 5]', '$[last-3 to last-1]') | ++--------------------------------------------------------+ +| [2, 3, 4] | ++--------------------------------------------------------+ +1 row in set (0.01 sec) +``` +#### json_set +json_set对于已经存在的path,会替换其path对应的值,对于不存在的path,会对该path添加新值 +```sql +mysql> SET @j = '["a", {"b": [true, false]}, [10, 20]]'; + +mysql> SELECT JSON_SET(@j, '$[1].b[0]', 1, '$[2][2]', 2); ++--------------------------------------------+ +| JSON_SET(@j, '$[1].b[0]', 1, '$[2][2]', 2) | ++--------------------------------------------+ +| ["a", {"b": [1, false]}, [10, 20, 2]] | ++--------------------------------------------+ +``` +#### json_insert +json_insert会插入新值但是不会替换已有的值 +```sql +mysql> SET @j = '["a", {"b": [true, false]}, [10, 20]]'; +mysql> SELECT JSON_INSERT(@j, '$[1].b[0]', 1, '$[2][2]', 2); ++-----------------------------------------------+ +| JSON_INSERT(@j, '$[1].b[0]', 1, '$[2][2]', 2) | ++-----------------------------------------------+ +| ["a", {"b": [true, false]}, [10, 20, 2]] | ++-----------------------------------------------+ +``` +#### json_replace +json_replace会替换已有的值但是不会插入新值 +```sql +mysql> SET @j = '["a", {"b": [true, false]}, [10, 20]]'; +mysql> SELECT JSON_REPLACE(@j, '$[1].b[0]', 1, '$[2][2]', 2); ++------------------------------------------------+ +| JSON_REPLACE(@j, '$[1].b[0]', 1, '$[2][2]', 2) | ++------------------------------------------------+ +| ["a", {"b": [1, false]}, [10, 20]] | ++------------------------------------------------+ +``` +#### json_remove +json_remove会删除path对应的值 +```sql +mysql> SET @j = '["a", {"b": [true, false]}, [10, 20]]'; +mysql> SELECT JSON_REMOVE(@j, '$[2]', '$[1].b[1]', '$[1].b[1]'); ++---------------------------------------------------+ +| JSON_REMOVE(@j, '$[2]', '$[1].b[1]', '$[1].b[1]') | ++---------------------------------------------------+ +| ["a", {"b": [true]}] | ++---------------------------------------------------+ +``` + diff --git a/spring/Apache Shiro/Apache Shiro Authentication.md b/spring/Apache Shiro/Apache Shiro Authentication.md index e5ebdbf..05d73fc 100644 --- a/spring/Apache Shiro/Apache Shiro Authentication.md +++ b/spring/Apache Shiro/Apache Shiro Authentication.md @@ -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。 + + diff --git a/spring/Apache Shiro/Apache Shiro Authorization.md b/spring/Apache Shiro/Apache Shiro Authorization.md index c76084c..8d7ded1 100644 --- a/spring/Apache Shiro/Apache Shiro Authorization.md +++ b/spring/Apache Shiro/Apache Shiro Authorization.md @@ -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 +} ``` \ No newline at end of file diff --git a/spring/Apache Shiro/Apache Shiro QuickStart.md b/spring/Apache Shiro/Apache Shiro QuickStart.md index 9dfe684..d93df2e 100644 --- a/spring/Apache Shiro/Apache Shiro QuickStart.md +++ b/spring/Apache Shiro/Apache Shiro QuickStart.md @@ -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(); ``` \ No newline at end of file diff --git a/spring/Apache Shiro/Apache Shiro Realm.md b/spring/Apache Shiro/Apache Shiro Realm.md index 43d435c..816367d 100644 --- a/spring/Apache Shiro/Apache Shiro Realm.md +++ b/spring/Apache Shiro/Apache Shiro Realm.md @@ -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方法会被调用,用来检测待检测权限是否隐含在其中 diff --git a/spring/Apache Shiro/Apache Shiro.md b/spring/Apache Shiro/Apache Shiro.md index aa57a6c..26b15e5 100644 --- a/spring/Apache Shiro/Apache Shiro.md +++ b/spring/Apache Shiro/Apache Shiro.md @@ -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时的任何代码。** + + + + + diff --git a/spring/Spring Security/Spring Security.md b/spring/Spring Security/Spring Security.md index ab49cdd..51421a3 100644 --- a/spring/Spring Security/Spring Security.md +++ b/spring/Spring Security/Spring Security.md @@ -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中保存 + diff --git a/spring/Spring core/POJO.md b/spring/Spring core/POJO.md index ceba0cc..02a06b9 100644 --- a/spring/Spring core/POJO.md +++ b/spring/Spring core/POJO.md @@ -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接口,也不一定要拥有无参构造方法。 diff --git a/spring/Spring core/SpEL.md b/spring/Spring core/SpEL.md index b91a3af..e1f0726 100644 --- a/spring/Spring core/SpEL.md +++ b/spring/Spring core/SpEL.md @@ -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 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 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); ``` \ No newline at end of file diff --git a/spring/Spring core/Spring Core AOP.md b/spring/Spring core/Spring Core AOP.md index 75e3418..891b19c 100644 --- a/spring/Spring core/Spring Core AOP.md +++ b/spring/Spring core/Spring Core AOP.md @@ -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 + + diff --git a/spring/Spring core/Spring Core IOC.md b/spring/Spring core/Spring Core IOC.md index b12b852..676b87b 100644 --- a/spring/Spring core/Spring Core IOC.md +++ b/spring/Spring core/Spring Core IOC.md @@ -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 - */ - - @Autowired - public Shiromiya(String[] waifus) { - this.waifus=waifus; - } - } - - // 同样,可以通过Map类型来获取所有相同类型bean对象的name和value - // key:对应bean对象的name - // value:对应该bean对象的值 - @Component - class Shiromiya { - private Map waifus; - - @Autowired - public void setWaifus(Map 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 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 + */ + + @Autowired + public Shiromiya(String[] waifus) { + this.waifus=waifus; + } + } + + // 同样,可以通过Map类型来获取所有相同类型bean对象的name和value + // key:对应bean对象的name + // value:对应该bean对象的值 + @Component + class Shiromiya { + private Map waifus; + + @Autowired + public void setWaifus(Map 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 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到类型匹配 \ No newline at end of file diff --git a/spring/Spring core/Spring Data Access.md b/spring/Spring core/Spring Data Access.md index a12d119..32a865d 100644 --- a/spring/Spring core/Spring Data Access.md +++ b/spring/Spring core/Spring Data Access.md @@ -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语句仍然可以被提交*** diff --git a/spring/Spring core/SpringMVC.md b/spring/Spring core/SpringMVC.md index d84dafd..628146c 100644 --- a/spring/Spring core/SpringMVC.md +++ b/spring/Spring core/SpringMVC.md @@ -1,802 +1,802 @@ -- [SpringMVC](#springmvc) - - [SpringMVC简介](#springmvc简介) - - [DispatcherServlet](#dispatcherservlet) - - [Context层次结构](#context层次结构) - - [WebApplicationContext的继承关系](#webapplicationcontext的继承关系) - - [Spring MVC中特殊的bean类型](#spring-mvc中特殊的bean类型) - - [HandlerMapping](#handlermapping) - - [HandlerAdapter](#handleradapter) - - [HandlerExceptionResolver](#handlerexceptionresolver) - - [ViewResolver](#viewresolver) - - [Web MVC Config](#web-mvc-config) - - [Servlet Config](#servlet-config) - - [请求处理过程](#请求处理过程) - - [路径匹配](#路径匹配) - - [Interception](#interception) - - [preHandle](#prehandle) - - [postHandle](#posthandle) - - [afterCompletion](#aftercompletion) - - [Exceptions](#exceptions) - - [Resolver Chain](#resolver-chain) - - [exception resolver的返回规范](#exception-resolver的返回规范) - - [container error page](#container-error-page) - - [视图解析](#视图解析) - - [处理](#处理) - - [重定向](#重定向) - - [转发](#转发) - - [Controller](#controller) - - [AOP代理](#aop代理) - - [Request Mapping](#request-mapping) - - [URI Pattern](#uri-pattern) - - [Pattern Comparasion](#pattern-comparasion) - - [消费media-type](#消费media-type) - - [产生media-type](#产生media-type) - - [Parameters \& Headers](#parameters--headers) - - [handler method](#handler-method) - - [类型转换](#类型转换) - - [Matrix Variable](#matrix-variable) - - [@RequestParam](#requestparam) - - [@RequestHeader](#requestheader) - - [@CookieValue](#cookievalue) - - [@ModelAttribute](#modelattribute) - - [@SessionAttributes](#sessionattributes) - - [@SessionAttribute](#sessionattribute) - - [Multipart](#multipart) - - [@RequestBody](#requestbody) - - [HttpEntity](#httpentity) - - [@ResponseBody](#responsebody) - - [ResponseEntity](#responseentity) - - [Jackson JSON](#jackson-json) - - [@JsonView](#jsonview) - - [Model](#model) - - [@ModelAttribute注解用法](#modelattribute注解用法) - - [@ModelAttribute作用于Controller类中普通方法上](#modelattribute作用于controller类中普通方法上) - - [@ModelAttribute作用于@RequestMapping方法上](#modelattribute作用于requestmapping方法上) - - [DataBinder](#databinder) - - [Model Design](#model-design) - - [Exception](#exception) - - [Controller](#controller-1) - - [CORS](#cors) - - [@CrossOrigin](#crossorigin) - - [spring boot全局配置CORS](#spring-boot全局配置cors) - - -# SpringMVC -## SpringMVC简介 -SpringMVC是一款基于Servlet API的web框架,并基于前端控制器的模式被设计。前端控制器有一个中央的控制器(DispatcherServlet),通过一个共享的算法来集中分发请求,请求实际是通过可配置的委托组件(@Controller类)来处理的。 -> Spring Boot遵循不同的初始化流程。相较于hooking到servlet容器的生命周期中,Spring Boot使用Spring配置来启动其自身和内置servlet容器。filter和servlet声明在在spring配置中被找到并且被注入到容器中。 - -## DispatcherServlet -### Context层次结构 -DispatcherServlet需要一个WebApplicationContext(ApplicationContext的拓展类,Spirng容器)来进行配置。WebApplicationContext拥有一个指向ServletContext和与ServletContext关联Servlet的链接。 -**同样的,也可以通过ServeltContext来获取关联的ApplicationContext,可以通过RequestContextUtils中的静态方法来获取ServletContext关联的ApplicationContext。** -#### WebApplicationContext的继承关系 -对大多数应用,含有一个WebApplicationContext就足够了。也可以存在一个Root WebApplicationContext在多个DispatcherServlet实例之间共享,同时DispatcherServlet实例也含有自己的WebApplicationContext。 -通常,被共享的root WebApplicationContext含有repository或service的bean对象,这些bean对象可以被多个DispatcherServlet实例的子WebApplicationContext共享。同时,子WebApplicationContext在继承root WebApplicationContext中bean对象的同时,还能对root WebApplicationContext中的bean对象进行覆盖。 -> #### WebApplicationContext继承机制 -> 只有当Servlet私有的子WebApplicationContext中没有找到bean对象时,才会从root WebApplicationContext中查找bean对象,此行为即是对root WebApplicationContext的继承 - -### Spring MVC中特殊的bean类型 -DispatcherServlet将处理请求和渲染返回的工作委托给特定的bean对象。Spring MVC中核心的bean类型如下。 -#### HandlerMapping -将请求映射到handler和一系列用于pre/post处理的拦截器。 -#### HandlerAdapter -HandlerAdapter主要是用于帮助DispatcherServlet调用request请求映射到的handler对象。 -通常,在调用含有注解的controller时需要对注解进行解析,而HandlerAdapter可以向DispatcherServlet隐藏这些细节,DispatcherServlet不必关心handler是如何被调用的。 -#### HandlerExceptionResolver -解析异常的策略,可能将异常映射到某个handler,或是映射到html error页面。 -#### ViewResolver -将handler返回的基于字符串的view名称映射到一个特定的view,并通过view来渲染返回的响应。 - -### Web MVC Config -在应用中可以定义上小节中包含的特殊类型bean对象。DispatcherServlet会检查WebApplicationContext中存在的特殊类型bean对象,如果某特殊类型的bean对象在容器中不存在,那么会存在一个回滚机制,使用DispatcherServlet.properties中默认的bean类型来创造bean对象(例如,DispatcherServlet.properties中指定的默认HandlerMapping类型是 org.springframework.web.servlet.handler.BeanNameUrlHandlerMapping和org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerMapping.)。 - -### Servlet Config -在Servlet环境中,可以选择通过java代码的方式或者web.xml的方式来配置servlet容器。 -配置Servlet的详细方式,参照Spring MVC文档。 - -### 请求处理过程 -DispatcherServlet按照如下方式来处理Http请求 -1. 首先,会找到WebApplicationContext并且将其绑定到请求中,此后controller和其他元素在请求处理的过程中可以使用该WebApplicationContext。**默认情况下,WebApplicationContext被绑定到DispatcherServlet.WEB_APPLICATION_CONTEXT_ATTRIBUTE属性中** -2. locale resolver被绑定到请求中,在请求处理过程中可以被其他元素使用 -3. theme resolver被绑定到请求中,在请求处理过程中可以被其他元素使用 -4. 如果指定了multipart file resolver,请求会被检查是否存在multipart。如果multipart被找到,该请求会被包装在一个MultipartHttpServletRequest中并等待对其进行进一步的处理 -5. 一个合适的handler将会被找到,并且和该handler相关联的execution chain(pre/post/Controller)会被执行,返回一个model用于渲染。 -6. 如果model被返回,那么返回的model会被渲染。如果model没有返回,那么没有view会被渲染。 - -在WebApplicationContext中声明的HandlerExceptionResolver会用于解析处理请求中抛出的异常。 -### 路径匹配 -Servlet API将完整的请求路径暴露为requestURI,并且进一步划分requestURI为如下部分:contextPath,servletPath,pathInfo。 -> #### contextPath, servletPath, pathInfo区别 -> - contextPath:contextPath为应用被访问的路径,是整个应用的根路径。默认情况下,SpringBoot的contextPath为"/"。可以通过server.servlet.context-path="/demo"来改变应用的根路径。 -> - servletPath:servletPath代表main DispatcherServlet的路径。默认情况下,servletPath的值仍为"/"。可以通过spring.mvc.servlet.path来自定义该值。 -### Interception -所有HandlerMapping的实现类都支持handler interceptor,当想要为特定请求执行指定逻辑时会相当有用。拦截器必须要实现HandlerInterceptor,该接口提供了三个方法: -- preHandle:在实际handler处理之前 -- postHandle:在 实际handler处理之后 -- afterCompletion:在请求完成之后 -#### preHandle -preHandle方法会返回一个boolean值,可以通过指定该方法的返回值来阻止执行链继续执行。当preHandle的返回值是true时,后续执行链会继续执行,当返回值是false时,DispatcherServlet会假设该拦截器本身已经处理了请求,并不会继续执行execution chain中的其他拦截器和实际handler。 -#### postHandle -对于@ResponseBody或返回值为ResponseEntity的方法,postHandle不起作用,这些方法在HandlerAdapter中已经写入且提交了返回响应,时间位于postHandle之前。到postHandle方法执行时,已经无法再对响应做任何修改,如添加header也不再被允许。对于这些场景,可以**实现ResponseBodyAdvice或者声明一个ControllerAdvice**。 -#### afterCompletion -调用时机位于DispaterServlet渲染view之后。 -### Exceptions -如果异常在request mapping过程中被抛出或者从request handler中被抛出,DispatcherServlet会将改异常委托给HandlerExceptionResolver chain来处理,通常是返回一个异常响应。 -如下是可选的HandlerExceptionResolver实现类: -1. SimpleMappingExceptionResolver:一个从exception class name到error view name的映射,用于渲染错误页面 -2. DefaultHandlerExceptionResolver:解析Spring MVC抛出的异常,并且将它们映射为Http状态码 -3. ResponseStatusExceptionResolver:通过@ResponseStatus注解来指定异常的状态码,并且将异常映射为Http状态码,状态码的值为@ResponseStatus注解指定的值 -4. ExceptionHandlerExceptionResolver:通过@Controller类内@ExceptionHandler方法或@ControllerAdvice类来解析异常 -#### Resolver Chain -可以声明多个HandlerExceptionResolver bean对象来声明一个Exception Resolver链,并可以通过指定order属性来指定resolver chain中的顺序,order越大,resolver在链中的顺序越靠后。 -#### exception resolver的返回规范 -HandlerExceptionResolver返回值可以按照如下规范返回: -- 一个指向ModelAndView的error view -- 一个空的ModelAndView,代表异常已经在该Resolver中被处理 -- null,代表该exception仍未被解析,resolver chain中后续的resolver会继续尝试处理该exception,如果exception在chain的末尾仍然存在,该异常会被冒泡到servlet container中 -#### container error page -如果exception直到resolver chain的最后都没有被解析,或者,response code被设置为错误的状态码(如4xx,5xx),servlet container会渲染一个默认的error html page。 -### 视图解析 -#### 处理 -类似于Exception Resolver,也可以声明一个view resolver chain,并且可以通过设置order属性来设置view resolver在resolver chain中的顺序。 -view resolver返回null时,代表该view无法被找到。 -#### 重定向 -以“redirect:”前缀开头的view name允许执行重定向,redirect:之后指定的是重定向的url。 -```shell -# 重定向实例如下 -# 1. 基于servlet context重定向 -redirect:/myapp/some/resource -# 2. 基于绝对路径进行重定向 -redirect:https://myhost.com/some/arbitrary/path -``` -> 如果controller的方法用@ResponseStatus注解标识,该注解值的优先级**高于**RedirectView返回的response status。 - -#### 转发 -以“forward:”前缀开头的view name会被转发。 -## Controller -### AOP代理 -在某些时候,可能需要AOP代理来装饰Controller,对于Controller AOP,推荐使用基于class的aop。 -例如想要为@Controller注解的类添加@Transactional注解,此时需要手动指定@Transactional注解的proxyTargetClass=true来启用基于class的动态代理。 -> 当为@Transactional注解指定了proxyTargetClass=true之后,其不光会将@Transactional的代理变为基于cglib的,还会将整个context中所有的autoproxy bean代理方式都变为基于cglib类代理的 -### Request Mapping -可以通过@RequestMapping来指定请求对应的url、http请求种类、请求参数、header和media type。 -还可以使用如下注解来映射特定的http method: -- @GetMapping -- @PostMapping -- @DeleteMapping -- @PutMapping -- @PatchMapping -相对于上述的注解,@RequestMapping映射所有的http method。 -#### URI Pattern -Spring MVC支持如下URI Pattern: -- /resources/ima?e.png : ?匹配一个字符 -- /resources/*.png : *匹配0个或多个字符,但是位于同一个path segment内 -- /resource/** : **可以匹配多个path segment(但是\*\*只能用于末尾) -- /projects/{project}/versions : 匹配一个path segment,并且将该path segment捕获到一个变量中,变量可以通过@PathVariable访问 -- /projects/{project:[a-z]+}/versions : 匹配一个path segment,并且该path segment需要满足指定的regex - -{varName:regex}可以将符合regex的path segment捕获到varName变量中,例如: -```java -@GetMapping("/{name:[a-z-]+}-{version:\\d\\.\\d\\.\\d}{ext:\\.[a-z]+}") -public void handle(@PathVariable String name, @PathVariable String version, @PathVariable String ext) { - // ... -} -``` -#### Pattern Comparasion -如果多个pattern都匹配当前URI,那么最佳匹配将会被采用。 -多个pattern中,更加具体的pattern会被采用。URI的计分方式如下: -- 每个URI变量计1分 -- *符号计1分 -- **符号计两分 -- 如果分数相同,那么更长的pattern会被选择 -- 如果分数和pattern length都相同,那么拥有比wildchar更多的的URI变量的模式会被选择 - -分数越高,那么pattern将会是更佳的匹配 -#### 消费media-type -可以在@RequestMapping中指定请求中的Content-Type类型来缩小请求映射范围 -```java -@PostMapping(path = "/pets", consumes = "application/json") -public void addPet(@RequestBody Pet pet) { - // ... -} -``` -consumes属性还支持“非”的操作,如下所示: -```java -// 其映射除text/plain之外的content-type -@PostMapping(path = "/pets", consumes = "!text/plain") -``` -#### 产生media-type -可以在@RequestMapping中指定produces属性来根据http请求header中的Accept属性来缩小映射范围 -```java -// 该方法会映射Accept属性为application/json的http请求 -@GetMapping(path = "/pets/{petId}", produces = "application/json") -@ResponseBody -public Pet getPet(@PathVariable String petId) { - // ... -} -``` -#### Parameters & Headers -可以通过请求参数或header来缩小@RequestMapping的映射。 -支持过滤条件为某参数是否存在、某参数值是否为预期值,header中某值是否存在、header中某值是否等于预期值。 -```java -// 参数是否存在 : myParam -// 参数是否不存在 : !myParam -// 参数是否为预期值 : myParam=myValue -@GetMapping(path = "/pets/{petId}", params = "myParam=myValue") -public void findPet(@PathVariable String petId) { - // ... -} - -// headers校验同样类似于params -@GetMapping(path = "/pets", headers = "myHeader=myValue") -public void findPet(@PathVariable String petId) { - // ... -} -``` -### handler method -#### 类型转换 -当handler方法的参数为非String类型时,需要进行类型转换。在此种场景下,类型转换是自动执行的。默认情况下,简单类型(int,long,Date,others)是被支持的。 -可以通过WebDataBinder来进行自定义类型转换。 -在执行类型转换时,空字符串""在转换为其他类型时可能被转化为null(如转化为long,int,Date).如果允许null被注入给参数,**需要将参数注解的required属性指定为false,或者为参数指定@Nullable属性。** -#### Matrix Variable -在Matrix Variable中,允许在uri中指定key-value对。path segment中允许包含key-value对,其中变量之间通过分号分隔,值如果存在多个,多个值之间通过逗号分隔。 -```shell -/cars;color=red,green;year=2012 -``` -当URL中预期含有Matrix变量时,被映射方法的URI中必须含有一个URI Variable({var})来覆盖该Matrix变量(即通过{var}来捕获URL中的Matrix变量),以此确保URL能被正确的映射。例子如下所示 -```java -// GET /pets/42;q=11;r=22 - -@GetMapping("/pets/{petId}") -public void findPet(@PathVariable String petId, @MatrixVariable int q) { - - // petId == 42 - // q == 11 -} -``` -在所有的path segment中都有可能含有matrix variable,如果在不同path segment中含有相同名称的matrix variable,可以通过如下方式进行区分: -```java -// GET /owners/42;q=11/pets/21;q=22 - -@GetMapping("/owners/{ownerId}/pets/{petId}") -public void findPet( - @MatrixVariable(name="q", pathVar="ownerId") int q1, - @MatrixVariable(name="q", pathVar="petId") int q2) { - - // q1 == 11 - // q2 == 22 -} -``` -matrix variable参数也可以被设置为可选的,并且也能为其指派一个默认值: -```java -// GET /pets/42 - -@GetMapping("/pets/{petId}") -public void findPet(@MatrixVariable(required=false, defaultValue="1") int q) { - - // q == 1 -} -``` -如果要获取所有的Matrix Variable并且将其缓存到一个map中,可以通过如下方式 -```java -// GET /owners/42;q=11;r=12/pets/21;q=22;s=23 - -@GetMapping("/owners/{ownerId}/pets/{petId}") -public void findPet( - @MatrixVariable MultiValueMap matrixVars, - @MatrixVariable(pathVar="petId") MultiValueMap petMatrixVars) { - - // matrixVars: ["q" : [11,22], "r" : 12, "s" : 23] - // petMatrixVars: ["q" : 22, "s" : 23] -} -``` -#### @RequestParam -@RequestParam注解用于将http request中的param赋值给handler method的参数,使用方法如下: -```java -@GetMapping - public String setupForm(@RequestParam("petId") int petId, Model model) { - Pet pet = this.clinic.loadPet(petId); - model.addAttribute("pet", pet); - return "petForm"; - } -``` -当@RequestParam注解的参数类型不是String时,类型转换会被自动的执行。 -将@RequestParam注解的参数类型指定为list或array时,会将具有相同param name的多个param value解析到其中。 -可以用@RequestParam来注解一个Map或MultiValValue类型,不要指定@RequestParam属性,此时,map将会被自动注入。 -#### @RequestHeader -可以通过@RequestHeader注解来将http header值赋值给handler method参数。 -```shell -# http header -Host localhost:8080 -Accept text/html,application/xhtml+xml,application/xml;q=0.9 -Accept-Language fr,en-gb;q=0.7,en;q=0.3 -Accept-Encoding gzip,deflate -Accept-Charset ISO-8859-1,utf-8;q=0.7,*;q=0.7 -Keep-Alive 300 -``` -可以通过如下方式来获取header中的值: -```java -@GetMapping("/demo") -public void handle( - @RequestHeader("Accept-Encoding") String encoding, - @RequestHeader("Keep-Alive") long keepAlive) { - //... -} -``` -同样的,如果被注解参数的类型不是String,类型转换将会被自动执行。 -同样,也可以用@RequestHeader注解来标注Map或MultiValueMap或HttpHeaders类型参数,map会自动被header name和header value注入。 -#### @CookieValue -可以通过@CookieValue注解将Http Cookie赋值给handler method参数,如果存在如下Cookie: -```cookie -JSESSIONID=415A4AC178C59DACE0B2C9CA727CDD84 -``` -可以通过如下方式来获取cookie的值 -```java -@GetMapping("/demo") -public void handle(@CookieValue("JSESSIONID") String cookie) { - //... -} -``` -如果注解标注参数的类型不是String,会自动执行类型转换。 -#### @ModelAttribute -可以为handler method的参数添加@ModelAttribute注解,在添加该注解之后,参数属性的值可以从Model中进行获取,如果model中不存在则对该属性进行初始化。 -**Model属性会被http servlet param中的值覆盖,如果request param name和field name相同。** -> @ModelAttribute注解,其name的默认值为方法参数或者返回值类型的首字母小写: -> 例如,“orderAddress" for class "mypackage.OrderAddress" - -@ModelAttribute的使用具体可如下所示: -```java -@PostMapping("/owners/{ownerId}/pets/{petId}/edit") -public String processSubmit(@ModelAttribute Pet pet) { - // method logic... -} -``` -参数pet可以通过如下方式进行获取: -1. pet参数已经加上了@ModelAttribute属性,故而可以从model中进行获取 -2. 如果该model attribute已经出现在类级别的@SessionAttribute中,则可以在session中进行获取 -3. 如果model attribute name和request param name或者path variable name相匹配,那么可以通过converter来进行获取 -4. 通过默认的构造器进行初始化 -5. 通过primary constructor进行构造,主构造器参数和servlet request param相匹配 - -在使用@ModelAttribute时,可以创建一个Converter类型的bean对象用于类型转换,当model attribute name和path variable或者request param name相匹配,且Converter对象存在时,会调用该Converter进行类型转换: -```java -@Component -class StringPersonConverter implements Converter { - @Override - public Person convert(String source) { - return new Person("kazusa",true,21); - } -} - -@RestController -public class ModelAttributeController { - - @PostMapping("/hello") - public Person sayHello(@ModelAttribute(name="name",binding=true) Person person, Model model) { - System.out.println(model); - return person; - } -} -``` -在model attribute被获取之后,会执行data binding。WebDataBinder会将request param name和目标对象的field进行匹配,匹配的field会通过类型转换进行注入。 -如果想要访问Model Attribute但是不想使用data binding,可以直接在handler method参数中使用Model,或者将@ModelAttribute注解的binding属性设置为false。 -```java -@PostMapping("update") -public String update(@Valid AccountForm form, BindingResult result, - @ModelAttribute(binding=false) Account account) { - // ... -} -``` -#### @SessionAttributes -@SessionAttributes注解用于将model attribute存储在http session中,从而在不同的请求间都可以访问该attribute。 -@SessionAttributes为class-level的注解,注解的value应该列出model attribute name或model attribute的类型。 -该注解使用如下所示: -```java -@Controller -@SessionAttributes("pet") -public class EditPetForm { - - // ... - - @PostMapping("/pets/{id}") - public String handle(Pet pet, BindingResult errors, SessionStatus status) { - if (errors.hasErrors) { - // ... - } - status.setComplete(); - // ... - } -} -``` -当第一个请求到来时,name为pet的Model Attribute被添加到model中,其会被自动提升并保存到http session中,知道另一个controller method通过SessionStatus方法来清除存储。 -#### @SessionAttribute -如果想要访问已经存在的Session Attribute,可以在handler method的参数上添加@SessionAttribute,如下所示: -```java -@RequestMapping("/") -public String handle(@SessionAttribute User user) { - // ... -} -``` -#### Multipart -在MultipartResolver被启用之后,Post请求体中为multipart/form-data格式的数据将被转化并可以作为handler method的参数访问。如下显示了文件上传的用法: -```java -@Controller -public class FileUploadController { - - @PostMapping("/form") - public String handleFormUpload(@RequestParam("name") String name, - @RequestParam("file") MultipartFile file) { - - if (!file.isEmpty()) { - byte[] bytes = file.getBytes(); - // store the bytes somewhere - return "redirect:uploadSuccess"; - } - return "redirect:uploadFailure"; - } -} -``` -> 在上述代码中,可以将MultipartFile改为List\来解析多个文件名 -> **当@RequestParam没有指定name属性并且参数类型为Map或MultiValueMap类型时,会根据name自动将所有匹配的MultipartFile注入到map中** - -#### @RequestBody -可以通过@RequestBody注解读取请求体内容,并且通过HttpMessageConverter将内容反序列化为Object。具体使用方法如下: -```java -@PostMapping("/accounts") -public void handle(@RequestBody Account account) { - // ... -} -``` -#### HttpEntity -将参数类型指定为HttpEntity与使用@RequestBody注解类似,其作为一个容器包含了请求体转化后的对象(account实例)和请求头(HttpHeaders)。其使用如下: -```java -@PostMapping("/accounts") -public void handle(HttpEntity entity) { - // ... -} -``` -#### @ResponseBody -通过使用@ResponseBody注解,可以将handler method的返回值序列化到响应体中,通过HttpMessageConverter。具体使用如下所示: -```java -@GetMapping("/accounts/{id}") -@ResponseBody -public Account handle() { - // ... -} -``` -同样的,@ResponseBody注解也支持class-level级别的使用,在使用@ResponseBody标注类后对所有controller method都会起作用。 -通过@RestController可以起到同样作用。 -#### ResponseEntity -ResponseEntity使用和@ResponseBody类似,但是含有status和headers。ResponseEntity使用如下所示: -```java -@GetMapping("/something") -public ResponseEntity handle() { - String body = ... ; - String etag = ... ; - return ResponseEntity.ok().eTag(etag).body(body); -} -``` -#### Jackson JSON -Spring MVC为Jackson序列化view提供了内置的支持,可以渲染对象中的字段子集。 -如果想要在序列化返回值时仅仅序列化某些字段,可以通过@JsonView注解来指明序列化哪些字段。 -##### @JsonView -@JsonView的使用如下所示, -- 通过interface来指定渲染的视图 -- 在字段的getter方法上通过@JsonView来标注视图类名 -- interface支持继承,如WithPasswordView继承WithoutPasswordView,故而WithPasswordView在序列化时不仅会包含其自身的password字段,还会包含从WithoutPasswordView中继承而来的name字段 -> 对于一个handler method,只能够通过@JsonView指定view class,如果想要激活多个视图,可以使用合成的view class -```java -@RestController -public class UserController { - - @GetMapping("/user") - @JsonView(User.WithoutPasswordView.class) - public User getUser() { - return new User("eric", "7!jd#h23"); - } -} - -public class User { - - public interface WithoutPasswordView {}; - public interface WithPasswordView extends WithoutPasswordView {}; - - private String username; - private String password; - - public User() { - } - - public User(String username, String password) { - this.username = username; - this.password = password; - } - - @JsonView(WithoutPasswordView.class) - public String getUsername() { - return this.username; - } - - @JsonView(WithPasswordView.class) - public String getPassword() { - return this.password; - } -} -``` -### Model -#### @ModelAttribute注解用法 -- 将@ModelAttribute注解标记在handler method的参数上,用于创建或访问一个对象,该对象从model中获取,并且该对象通过WebDataBinder与http request绑定在一起。 -- 将@ModelAttribute注解绑定在位于@Controller或@ControllerAdvice类中的方法上,用于在任何handler method调用之前初始化model -- 将@ModelAttribute注解绑定在@RequestMapping方法上,用于标注该方法的返回值是一个model attribute - -#### @ModelAttribute作用于Controller类中普通方法上 -对于上述的第二种使用,一个controller类中可以含有任意数量个@ModelAttribute方法,所有的这些方法都会在@RequestMapping方法调用之前被调用。(**同一个@ModelAttribute方法,也可以通过@ControllerAdvice在多个controllers之间进行共享**)。 -@ModelAttribute方法支持可变的参数形式,其参数形式可以和@RequestMapping方法中的参数形式一样。(**但是,@ModelAttribute方法的参数中不能含有@ModelAttribute注解本身,参数也不能含有和http请求体相关的内容**)。 -```java -@ModelAttribute -public void populateModel(@RequestParam String number, Model model) { - model.addAttribute(accountRepository.findAccount(number)); - // add more ... -} -``` -也可以通过如下方式向model中添加attribute: -```java -// 只向model中添加一个属性 -@ModelAttribute -public Account addAccount(@RequestParam String number) { - return accountRepository.findAccount(number); -} -``` -> 在向model中添加属性时,如果attribute name没有显式指定,那么attribute name将会基于attribute value的类型来决定。可以通过model.addAttribute(attributeName,attributeValue)来指定attribute name,或者通过指定@ModelAttribute的name属性来指定attribute name - -#### @ModelAttribute作用于@RequestMapping方法上 -对于第三种使用,可以将@ModelAttribute注解标注在@RequestMapping方法之上,在这种情况下方法的返回值将会被解释为model attribute。**在这种情况下@ModelAttribute注解不是必须的,应为该行为是html controller的默认行为,除非返回值是String类型,此时返回值会被解释为view name。** -可以通过如下方式来自定义返回model attribute的attribute name,如下图所示: -```java -// 指定attribute name为“myAccount” -@GetMapping("/accounts/{id}") -@ModelAttribute("myAccount") -public Account handle() { - // ... - return account; -} -``` -### DataBinder -对于@Controller和@ControllerAdvice类,在类中可以包含@InitBinder方法,该方法用来初始化WebDataBinder实例: -- 将请求参数绑定到model -- 将基于String类型的请求值(例如请求参数,pathVariable,headers,cookies或其他)转化为controller method参数的类型 -- 在html渲染时,将model object value转化为String的形式 - -@InitBinder可以针对特定的Controller注册java.beans.PropertyEditor或Spring Converter和Formatter 组件。 -@InitBinder支持和@RequestMapping方法一样的参数形式,但是参数不能使用@ModelAttribute注解。通常,@InitBinder方法的参数为WebDataBinder,且返回类型为void: -```java -@Controller -public class FormController { - // WebDataBinder参数用于注册 - @InitBinder - public void initBinder(WebDataBinder binder) { - SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd"); - dateFormat.setLenient(false); - binder.registerCustomEditor(Date.class, new CustomDateEditor(dateFormat, false)); - } - - // ... -} -``` -同样,当使用一个共享的FormattingConversionService来设置格式时,可以注册针对特定Controller的Formatter实现,例如: -```java -@Controller -public class FormController { - - @InitBinder - protected void initBinder(WebDataBinder binder) { - binder.addCustomFormatter(new DateFormatter("yyyy-MM-dd")); - } - - // ... -} -``` -### Model Design -在web应用的上下文中,data binding涉及将http请求中的参数绑定到model object及其内层嵌套对象中。 -默认情况下,所有spring允许绑到model object中所有的公共属性(有public的getter和setter)。 -通常情况下,会自定义一个特定的model object类,并且该类中的public属性与表单中提交的参数相关联。 -```java -// 只会对如下两个public属性进行data binding -public class ChangeEmailForm { - - private String oldEmailAddress; - private String newEmailAddress; - - public void setOldEmailAddress(String oldEmailAddress) { - this.oldEmailAddress = oldEmailAddress; - } - - public String getOldEmailAddress() { - return this.oldEmailAddress; - } - - public void setNewEmailAddress(String newEmailAddress) { - this.newEmailAddress = newEmailAddress; - } - - public String getNewEmailAddress() { - return this.newEmailAddress; - } - -} -``` -### Exception -在@Controller和@ControllerAdvice类中,可以含有@ExceptionHandler方法,该方法用于处理controller方法中抛出的异常,使用如下所示: -```java -@Controller -public class SimpleController { - - // ... - - @ExceptionHandler - public ResponseEntity handle(IOException ex) { - // ... - } -} -``` -该异常参数会匹配被抛出的顶层异常(例如,被直接抛出的IOException),也会匹配被包装的内层cause(例如,被包装在IllegalStateException中的IOException)。**该参数会匹配任一层级的cause exception,并不是只有该异常的直接cause exception才会被处理。** -> 只要@ExceptionHandler方法的异常参数类型匹配异常抛出stacktrace中任一层次的异常类型,异常都会被捕获并且处理。 -```java - @PostMapping("/shiro") - public String shiro() { - throw new RuntimeException(new JarException("fuck")); - } - - @ExceptionHandler - public String handleJarException(JarException e) { - return e.getMessage() + ", Jar"; - } -``` -> 如果有多个@ExceptionHandler方法匹配抛出的异常链,那么root exception匹配会优先于cause exception匹配。 -> ExceptionDepthComparator会根据各个异常类型相对于抛出异常类型(root exception)的深度来进行排序。 -> ```java -> // 在调用/shiro接口时,IOException离root exception(RuntimeException)更近, -> // 故而会优先调用handleIOException方法 -> @PostMapping("/shiro") -> public String shiro() throws IOException { -> throw new RuntimeException(new IOException(new SQLException("fuck"))); -> } -> -> @ExceptionHandler -> public String handleIOException(IOException e) { -> return e.getMessage() + ", IO"; -> } -> -> @ExceptionHandler -> public String handleSQLException(SQLException e) { -> return e.getMessage() + ",Exception"; -> } -> ``` - -对于@ExceptionHandler,可以指定该方法处理的异常类型来缩小范围,如下方法都只会匹配root exception为指定异常或异常链中包含指定异常的场景: -```java -@ExceptionHandler({FileSystemException.class, RemoteException.class}) -public ResponseEntity handle(IOException ex) { - // ... -} - -// 甚至,可以将参数的异常类型指定为一个非常宽泛的类型,例如Exception -@ExceptionHandler({FileSystemException.class, RemoteException.class}) -public ResponseEntity handle(Exception ex) { - // ... -} -``` -上述两种写法的区别是: -1. 如果参数类型为IOException,那么当cause exception为FileSystemException或RemoteException,且root exception为IOException时,实际的cause exception要通过ex.getCause来获取 -2. 如果参数类型为Exception,那么当cause exception为FileSystemException或RemoteException且root exception不为指定类型异常时,指定类型异常统一都通过ex.getCause来获取 -> 通常情况下,@ExceptionHandler方法的参数类型应该尽可能的具体,而且推荐将一个宽泛的@ExceptionHandler拆分成多个独立的@ExceptionHandler方法,每个方法负责处理特定类型的异常。 - -**对于@ExceptionHandler方法,在捕获异常之后可以对异常进行rethrow操作,重新抛出之后的异常会在剩余的resolution chain中传播,就好像给定的@ExceptionHandler在开始就没有匹配** - -### Controller -对于@ModelAttribute、@InitBinder、@ExceptionHandler注解,若其在@Controller类中使用,那么该类仅对其所在的@Controller类有效。 -**如果这些注解定义在@ControllerAdvice或@RestControllerAdvice类中,那么它们对所有的@Controller类都有效。** -**位于@ControllerAdvice类中的@ExceptionHandler方法,其可以用于处理任何@Controller类中抛出的异常或任何其他exception handler中抛出的异常。** -> 对于@ControllerAdvice中的@ExceptionHandler方法,其会在其他所有位于@Controller类中的@ExceptionHandler方法执行完**之后**才会执行;而@InitBinder和@ModelAttribute方法,则是位于@ControllerAdive方法中的**优先于**位于@Controller类中的执行 -> -@ControllerAdive使用如下: -```java -// Target all Controllers annotated with @RestController -@ControllerAdvice(annotations = RestController.class) -public class ExampleAdvice1 {} - -// Target all Controllers within specific packages -@ControllerAdvice("org.example.controllers") -public class ExampleAdvice2 {} - -// Target all Controllers assignable to specific classes -@ControllerAdvice(assignableTypes = {ControllerInterface.class, AbstractController.class}) -public class ExampleAdvice3 {} -``` - -## CORS -Spring MVC允许处理跨域问题。 -> ### 跨域问题 -> 处于对安全的考虑,浏览器禁止ajax访问非当前origin的资源。 -> 对于简单请求,浏览器会直接通过请求和响应中特定header来判断当前资源是否能够被访问;对于非简单请求,则是在请求之前会发送一个预检请求(OPTIONS请求,判断当前资源能否被访问)。 - -在Spring MVC中,HandlerMapping实现提供了对CORS的支持。在实际将一个请求映射到handler后,HandlerMapping实现将会检查请求和handler的CORS配置并且做进一步的处理。 -如果想要启用CORS,需要显式声明CORS配置,如果匹配的CORS配置未被找到,那么预检请求将会被拒绝。(CORS header将不会添加到简单请求和实际CORS请求的响应中,浏览器将会拒绝没有CORS header的响应)。 -### @CrossOrigin -可以对handler method使用@CrossOrigin注解以允许跨域请求,使用示例如下: -```java -@RestController -@RequestMapping("/account") -public class AccountController { - - @CrossOrigin - @GetMapping("/{id}") - public Account retrieve(@PathVariable Long id) { - // ... - } - - @DeleteMapping("/{id}") - public void remove(@PathVariable Long id) { - // ... - } -} -``` -默认情况下,@CrossOrigin会允许来自所有origin、含有任意header和所有http method。 -- allowCredentials默认情况下没有启用,除非allowOrigins或allowOriginPatterns被指定了一个非"*"的值。 -- maxAge默认会设置为30min - -@CrossOrigin注解支持在类上使用,类上注解会被方法继承 -```java -@CrossOrigin(origins = "https://domain2.com", maxAge = 3600) -@RestController -@RequestMapping("/account") -public class AccountController { - - @GetMapping("/{id}") - public Account retrieve(@PathVariable Long id) { - // ... - } - - @DeleteMapping("/{id}") - public void remove(@PathVariable Long id) { - // ... - } -} -``` -@CrossOrigin注解可同时在类和方法上使用 -```java -@CrossOrigin(maxAge = 3600) -@RestController -@RequestMapping("/account") -public class AccountController { - - @CrossOrigin("https://domain2.com") - @GetMapping("/{id}") - public Account retrieve(@PathVariable Long id) { - // ... - } - - @DeleteMapping("/{id}") - public void remove(@PathVariable Long id) { - // ... - } -} -``` -### spring boot全局配置CORS -在spring boot中,可以通过如下方式全局配置CORS -```java -@Configuration -public class WebConfig implements WebMvcConfigurer { - @Override - public void addCorsMappings(CorsRegistry registry) { - registry.addMapping("/api/**") - .allowedOrigins("http://localhost:4200") - .allowedMethods("*") - .allowedHeaders("*") - .allowCredentials(false) - .maxAge(3600); - } -} -``` - +- [SpringMVC](#springmvc) + - [SpringMVC简介](#springmvc简介) + - [DispatcherServlet](#dispatcherservlet) + - [Context层次结构](#context层次结构) + - [WebApplicationContext的继承关系](#webapplicationcontext的继承关系) + - [Spring MVC中特殊的bean类型](#spring-mvc中特殊的bean类型) + - [HandlerMapping](#handlermapping) + - [HandlerAdapter](#handleradapter) + - [HandlerExceptionResolver](#handlerexceptionresolver) + - [ViewResolver](#viewresolver) + - [Web MVC Config](#web-mvc-config) + - [Servlet Config](#servlet-config) + - [请求处理过程](#请求处理过程) + - [路径匹配](#路径匹配) + - [Interception](#interception) + - [preHandle](#prehandle) + - [postHandle](#posthandle) + - [afterCompletion](#aftercompletion) + - [Exceptions](#exceptions) + - [Resolver Chain](#resolver-chain) + - [exception resolver的返回规范](#exception-resolver的返回规范) + - [container error page](#container-error-page) + - [视图解析](#视图解析) + - [处理](#处理) + - [重定向](#重定向) + - [转发](#转发) + - [Controller](#controller) + - [AOP代理](#aop代理) + - [Request Mapping](#request-mapping) + - [URI Pattern](#uri-pattern) + - [Pattern Comparasion](#pattern-comparasion) + - [消费media-type](#消费media-type) + - [产生media-type](#产生media-type) + - [Parameters \& Headers](#parameters--headers) + - [handler method](#handler-method) + - [类型转换](#类型转换) + - [Matrix Variable](#matrix-variable) + - [@RequestParam](#requestparam) + - [@RequestHeader](#requestheader) + - [@CookieValue](#cookievalue) + - [@ModelAttribute](#modelattribute) + - [@SessionAttributes](#sessionattributes) + - [@SessionAttribute](#sessionattribute) + - [Multipart](#multipart) + - [@RequestBody](#requestbody) + - [HttpEntity](#httpentity) + - [@ResponseBody](#responsebody) + - [ResponseEntity](#responseentity) + - [Jackson JSON](#jackson-json) + - [@JsonView](#jsonview) + - [Model](#model) + - [@ModelAttribute注解用法](#modelattribute注解用法) + - [@ModelAttribute作用于Controller类中普通方法上](#modelattribute作用于controller类中普通方法上) + - [@ModelAttribute作用于@RequestMapping方法上](#modelattribute作用于requestmapping方法上) + - [DataBinder](#databinder) + - [Model Design](#model-design) + - [Exception](#exception) + - [Controller](#controller-1) + - [CORS](#cors) + - [@CrossOrigin](#crossorigin) + - [spring boot全局配置CORS](#spring-boot全局配置cors) + + +# SpringMVC +## SpringMVC简介 +SpringMVC是一款基于Servlet API的web框架,并基于前端控制器的模式被设计。前端控制器有一个中央的控制器(DispatcherServlet),通过一个共享的算法来集中分发请求,请求实际是通过可配置的委托组件(@Controller类)来处理的。 +> Spring Boot遵循不同的初始化流程。相较于hooking到servlet容器的生命周期中,Spring Boot使用Spring配置来启动其自身和内置servlet容器。filter和servlet声明在在spring配置中被找到并且被注入到容器中。 + +## DispatcherServlet +### Context层次结构 +DispatcherServlet需要一个WebApplicationContext(ApplicationContext的拓展类,Spirng容器)来进行配置。WebApplicationContext拥有一个指向ServletContext和与ServletContext关联Servlet的链接。 +**同样的,也可以通过ServeltContext来获取关联的ApplicationContext,可以通过RequestContextUtils中的静态方法来获取ServletContext关联的ApplicationContext。** +#### WebApplicationContext的继承关系 +对大多数应用,含有一个WebApplicationContext就足够了。也可以存在一个Root WebApplicationContext在多个DispatcherServlet实例之间共享,同时DispatcherServlet实例也含有自己的WebApplicationContext。 +通常,被共享的root WebApplicationContext含有repository或service的bean对象,这些bean对象可以被多个DispatcherServlet实例的子WebApplicationContext共享。同时,子WebApplicationContext在继承root WebApplicationContext中bean对象的同时,还能对root WebApplicationContext中的bean对象进行覆盖。 +> #### WebApplicationContext继承机制 +> 只有当Servlet私有的子WebApplicationContext中没有找到bean对象时,才会从root WebApplicationContext中查找bean对象,此行为即是对root WebApplicationContext的继承 + +### Spring MVC中特殊的bean类型 +DispatcherServlet将处理请求和渲染返回的工作委托给特定的bean对象。Spring MVC中核心的bean类型如下。 +#### HandlerMapping +将请求映射到handler和一系列用于pre/post处理的拦截器。 +#### HandlerAdapter +HandlerAdapter主要是用于帮助DispatcherServlet调用request请求映射到的handler对象。 +通常,在调用含有注解的controller时需要对注解进行解析,而HandlerAdapter可以向DispatcherServlet隐藏这些细节,DispatcherServlet不必关心handler是如何被调用的。 +#### HandlerExceptionResolver +解析异常的策略,可能将异常映射到某个handler,或是映射到html error页面。 +#### ViewResolver +将handler返回的基于字符串的view名称映射到一个特定的view,并通过view来渲染返回的响应。 + +### Web MVC Config +在应用中可以定义上小节中包含的特殊类型bean对象。DispatcherServlet会检查WebApplicationContext中存在的特殊类型bean对象,如果某特殊类型的bean对象在容器中不存在,那么会存在一个回滚机制,使用DispatcherServlet.properties中默认的bean类型来创造bean对象(例如,DispatcherServlet.properties中指定的默认HandlerMapping类型是 org.springframework.web.servlet.handler.BeanNameUrlHandlerMapping和org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerMapping.)。 + +### Servlet Config +在Servlet环境中,可以选择通过java代码的方式或者web.xml的方式来配置servlet容器。 +配置Servlet的详细方式,参照Spring MVC文档。 + +### 请求处理过程 +DispatcherServlet按照如下方式来处理Http请求 +1. 首先,会找到WebApplicationContext并且将其绑定到请求中,此后controller和其他元素在请求处理的过程中可以使用该WebApplicationContext。**默认情况下,WebApplicationContext被绑定到DispatcherServlet.WEB_APPLICATION_CONTEXT_ATTRIBUTE属性中** +2. locale resolver被绑定到请求中,在请求处理过程中可以被其他元素使用 +3. theme resolver被绑定到请求中,在请求处理过程中可以被其他元素使用 +4. 如果指定了multipart file resolver,请求会被检查是否存在multipart。如果multipart被找到,该请求会被包装在一个MultipartHttpServletRequest中并等待对其进行进一步的处理 +5. 一个合适的handler将会被找到,并且和该handler相关联的execution chain(pre/post/Controller)会被执行,返回一个model用于渲染。 +6. 如果model被返回,那么返回的model会被渲染。如果model没有返回,那么没有view会被渲染。 + +在WebApplicationContext中声明的HandlerExceptionResolver会用于解析处理请求中抛出的异常。 +### 路径匹配 +Servlet API将完整的请求路径暴露为requestURI,并且进一步划分requestURI为如下部分:contextPath,servletPath,pathInfo。 +> #### contextPath, servletPath, pathInfo区别 +> - contextPath:contextPath为应用被访问的路径,是整个应用的根路径。默认情况下,SpringBoot的contextPath为"/"。可以通过server.servlet.context-path="/demo"来改变应用的根路径。 +> - servletPath:servletPath代表main DispatcherServlet的路径。默认情况下,servletPath的值仍为"/"。可以通过spring.mvc.servlet.path来自定义该值。 +### Interception +所有HandlerMapping的实现类都支持handler interceptor,当想要为特定请求执行指定逻辑时会相当有用。拦截器必须要实现HandlerInterceptor,该接口提供了三个方法: +- preHandle:在实际handler处理之前 +- postHandle:在 实际handler处理之后 +- afterCompletion:在请求完成之后 +#### preHandle +preHandle方法会返回一个boolean值,可以通过指定该方法的返回值来阻止执行链继续执行。当preHandle的返回值是true时,后续执行链会继续执行,当返回值是false时,DispatcherServlet会假设该拦截器本身已经处理了请求,并不会继续执行execution chain中的其他拦截器和实际handler。 +#### postHandle +对于@ResponseBody或返回值为ResponseEntity的方法,postHandle不起作用,这些方法在HandlerAdapter中已经写入且提交了返回响应,时间位于postHandle之前。到postHandle方法执行时,已经无法再对响应做任何修改,如添加header也不再被允许。对于这些场景,可以**实现ResponseBodyAdvice或者声明一个ControllerAdvice**。 +#### afterCompletion +调用时机位于DispaterServlet渲染view之后。 +### Exceptions +如果异常在request mapping过程中被抛出或者从request handler中被抛出,DispatcherServlet会将改异常委托给HandlerExceptionResolver chain来处理,通常是返回一个异常响应。 +如下是可选的HandlerExceptionResolver实现类: +1. SimpleMappingExceptionResolver:一个从exception class name到error view name的映射,用于渲染错误页面 +2. DefaultHandlerExceptionResolver:解析Spring MVC抛出的异常,并且将它们映射为Http状态码 +3. ResponseStatusExceptionResolver:通过@ResponseStatus注解来指定异常的状态码,并且将异常映射为Http状态码,状态码的值为@ResponseStatus注解指定的值 +4. ExceptionHandlerExceptionResolver:通过@Controller类内@ExceptionHandler方法或@ControllerAdvice类来解析异常 +#### Resolver Chain +可以声明多个HandlerExceptionResolver bean对象来声明一个Exception Resolver链,并可以通过指定order属性来指定resolver chain中的顺序,order越大,resolver在链中的顺序越靠后。 +#### exception resolver的返回规范 +HandlerExceptionResolver返回值可以按照如下规范返回: +- 一个指向ModelAndView的error view +- 一个空的ModelAndView,代表异常已经在该Resolver中被处理 +- null,代表该exception仍未被解析,resolver chain中后续的resolver会继续尝试处理该exception,如果exception在chain的末尾仍然存在,该异常会被冒泡到servlet container中 +#### container error page +如果exception直到resolver chain的最后都没有被解析,或者,response code被设置为错误的状态码(如4xx,5xx),servlet container会渲染一个默认的error html page。 +### 视图解析 +#### 处理 +类似于Exception Resolver,也可以声明一个view resolver chain,并且可以通过设置order属性来设置view resolver在resolver chain中的顺序。 +view resolver返回null时,代表该view无法被找到。 +#### 重定向 +以“redirect:”前缀开头的view name允许执行重定向,redirect:之后指定的是重定向的url。 +```shell +# 重定向实例如下 +# 1. 基于servlet context重定向 +redirect:/myapp/some/resource +# 2. 基于绝对路径进行重定向 +redirect:https://myhost.com/some/arbitrary/path +``` +> 如果controller的方法用@ResponseStatus注解标识,该注解值的优先级**高于**RedirectView返回的response status。 + +#### 转发 +以“forward:”前缀开头的view name会被转发。 +## Controller +### AOP代理 +在某些时候,可能需要AOP代理来装饰Controller,对于Controller AOP,推荐使用基于class的aop。 +例如想要为@Controller注解的类添加@Transactional注解,此时需要手动指定@Transactional注解的proxyTargetClass=true来启用基于class的动态代理。 +> 当为@Transactional注解指定了proxyTargetClass=true之后,其不光会将@Transactional的代理变为基于cglib的,还会将整个context中所有的autoproxy bean代理方式都变为基于cglib类代理的 +### Request Mapping +可以通过@RequestMapping来指定请求对应的url、http请求种类、请求参数、header和media type。 +还可以使用如下注解来映射特定的http method: +- @GetMapping +- @PostMapping +- @DeleteMapping +- @PutMapping +- @PatchMapping +相对于上述的注解,@RequestMapping映射所有的http method。 +#### URI Pattern +Spring MVC支持如下URI Pattern: +- /resources/ima?e.png : ?匹配一个字符 +- /resources/*.png : *匹配0个或多个字符,但是位于同一个path segment内 +- /resource/** : **可以匹配多个path segment(但是\*\*只能用于末尾) +- /projects/{project}/versions : 匹配一个path segment,并且将该path segment捕获到一个变量中,变量可以通过@PathVariable访问 +- /projects/{project:[a-z]+}/versions : 匹配一个path segment,并且该path segment需要满足指定的regex + +{varName:regex}可以将符合regex的path segment捕获到varName变量中,例如: +```java +@GetMapping("/{name:[a-z-]+}-{version:\\d\\.\\d\\.\\d}{ext:\\.[a-z]+}") +public void handle(@PathVariable String name, @PathVariable String version, @PathVariable String ext) { + // ... +} +``` +#### Pattern Comparasion +如果多个pattern都匹配当前URI,那么最佳匹配将会被采用。 +多个pattern中,更加具体的pattern会被采用。URI的计分方式如下: +- 每个URI变量计1分 +- *符号计1分 +- **符号计两分 +- 如果分数相同,那么更长的pattern会被选择 +- 如果分数和pattern length都相同,那么拥有比wildchar更多的的URI变量的模式会被选择 + +分数越高,那么pattern将会是更佳的匹配 +#### 消费media-type +可以在@RequestMapping中指定请求中的Content-Type类型来缩小请求映射范围 +```java +@PostMapping(path = "/pets", consumes = "application/json") +public void addPet(@RequestBody Pet pet) { + // ... +} +``` +consumes属性还支持“非”的操作,如下所示: +```java +// 其映射除text/plain之外的content-type +@PostMapping(path = "/pets", consumes = "!text/plain") +``` +#### 产生media-type +可以在@RequestMapping中指定produces属性来根据http请求header中的Accept属性来缩小映射范围 +```java +// 该方法会映射Accept属性为application/json的http请求 +@GetMapping(path = "/pets/{petId}", produces = "application/json") +@ResponseBody +public Pet getPet(@PathVariable String petId) { + // ... +} +``` +#### Parameters & Headers +可以通过请求参数或header来缩小@RequestMapping的映射。 +支持过滤条件为某参数是否存在、某参数值是否为预期值,header中某值是否存在、header中某值是否等于预期值。 +```java +// 参数是否存在 : myParam +// 参数是否不存在 : !myParam +// 参数是否为预期值 : myParam=myValue +@GetMapping(path = "/pets/{petId}", params = "myParam=myValue") +public void findPet(@PathVariable String petId) { + // ... +} + +// headers校验同样类似于params +@GetMapping(path = "/pets", headers = "myHeader=myValue") +public void findPet(@PathVariable String petId) { + // ... +} +``` +### handler method +#### 类型转换 +当handler方法的参数为非String类型时,需要进行类型转换。在此种场景下,类型转换是自动执行的。默认情况下,简单类型(int,long,Date,others)是被支持的。 +可以通过WebDataBinder来进行自定义类型转换。 +在执行类型转换时,空字符串""在转换为其他类型时可能被转化为null(如转化为long,int,Date).如果允许null被注入给参数,**需要将参数注解的required属性指定为false,或者为参数指定@Nullable属性。** +#### Matrix Variable +在Matrix Variable中,允许在uri中指定key-value对。path segment中允许包含key-value对,其中变量之间通过分号分隔,值如果存在多个,多个值之间通过逗号分隔。 +```shell +/cars;color=red,green;year=2012 +``` +当URL中预期含有Matrix变量时,被映射方法的URI中必须含有一个URI Variable({var})来覆盖该Matrix变量(即通过{var}来捕获URL中的Matrix变量),以此确保URL能被正确的映射。例子如下所示 +```java +// GET /pets/42;q=11;r=22 + +@GetMapping("/pets/{petId}") +public void findPet(@PathVariable String petId, @MatrixVariable int q) { + + // petId == 42 + // q == 11 +} +``` +在所有的path segment中都有可能含有matrix variable,如果在不同path segment中含有相同名称的matrix variable,可以通过如下方式进行区分: +```java +// GET /owners/42;q=11/pets/21;q=22 + +@GetMapping("/owners/{ownerId}/pets/{petId}") +public void findPet( + @MatrixVariable(name="q", pathVar="ownerId") int q1, + @MatrixVariable(name="q", pathVar="petId") int q2) { + + // q1 == 11 + // q2 == 22 +} +``` +matrix variable参数也可以被设置为可选的,并且也能为其指派一个默认值: +```java +// GET /pets/42 + +@GetMapping("/pets/{petId}") +public void findPet(@MatrixVariable(required=false, defaultValue="1") int q) { + + // q == 1 +} +``` +如果要获取所有的Matrix Variable并且将其缓存到一个map中,可以通过如下方式 +```java +// GET /owners/42;q=11;r=12/pets/21;q=22;s=23 + +@GetMapping("/owners/{ownerId}/pets/{petId}") +public void findPet( + @MatrixVariable MultiValueMap matrixVars, + @MatrixVariable(pathVar="petId") MultiValueMap petMatrixVars) { + + // matrixVars: ["q" : [11,22], "r" : 12, "s" : 23] + // petMatrixVars: ["q" : 22, "s" : 23] +} +``` +#### @RequestParam +@RequestParam注解用于将http request中的param赋值给handler method的参数,使用方法如下: +```java +@GetMapping + public String setupForm(@RequestParam("petId") int petId, Model model) { + Pet pet = this.clinic.loadPet(petId); + model.addAttribute("pet", pet); + return "petForm"; + } +``` +当@RequestParam注解的参数类型不是String时,类型转换会被自动的执行。 +将@RequestParam注解的参数类型指定为list或array时,会将具有相同param name的多个param value解析到其中。 +可以用@RequestParam来注解一个Map或MultiValValue类型,不要指定@RequestParam属性,此时,map将会被自动注入。 +#### @RequestHeader +可以通过@RequestHeader注解来将http header值赋值给handler method参数。 +```shell +# http header +Host localhost:8080 +Accept text/html,application/xhtml+xml,application/xml;q=0.9 +Accept-Language fr,en-gb;q=0.7,en;q=0.3 +Accept-Encoding gzip,deflate +Accept-Charset ISO-8859-1,utf-8;q=0.7,*;q=0.7 +Keep-Alive 300 +``` +可以通过如下方式来获取header中的值: +```java +@GetMapping("/demo") +public void handle( + @RequestHeader("Accept-Encoding") String encoding, + @RequestHeader("Keep-Alive") long keepAlive) { + //... +} +``` +同样的,如果被注解参数的类型不是String,类型转换将会被自动执行。 +同样,也可以用@RequestHeader注解来标注Map或MultiValueMap或HttpHeaders类型参数,map会自动被header name和header value注入。 +#### @CookieValue +可以通过@CookieValue注解将Http Cookie赋值给handler method参数,如果存在如下Cookie: +```cookie +JSESSIONID=415A4AC178C59DACE0B2C9CA727CDD84 +``` +可以通过如下方式来获取cookie的值 +```java +@GetMapping("/demo") +public void handle(@CookieValue("JSESSIONID") String cookie) { + //... +} +``` +如果注解标注参数的类型不是String,会自动执行类型转换。 +#### @ModelAttribute +可以为handler method的参数添加@ModelAttribute注解,在添加该注解之后,参数属性的值可以从Model中进行获取,如果model中不存在则对该属性进行初始化。 +**Model属性会被http servlet param中的值覆盖,如果request param name和field name相同。** +> @ModelAttribute注解,其name的默认值为方法参数或者返回值类型的首字母小写: +> 例如,“orderAddress" for class "mypackage.OrderAddress" + +@ModelAttribute的使用具体可如下所示: +```java +@PostMapping("/owners/{ownerId}/pets/{petId}/edit") +public String processSubmit(@ModelAttribute Pet pet) { + // method logic... +} +``` +参数pet可以通过如下方式进行获取: +1. pet参数已经加上了@ModelAttribute属性,故而可以从model中进行获取 +2. 如果该model attribute已经出现在类级别的@SessionAttribute中,则可以在session中进行获取 +3. 如果model attribute name和request param name或者path variable name相匹配,那么可以通过converter来进行获取 +4. 通过默认的构造器进行初始化 +5. 通过primary constructor进行构造,主构造器参数和servlet request param相匹配 + +在使用@ModelAttribute时,可以创建一个Converter类型的bean对象用于类型转换,当model attribute name和path variable或者request param name相匹配,且Converter对象存在时,会调用该Converter进行类型转换: +```java +@Component +class StringPersonConverter implements Converter { + @Override + public Person convert(String source) { + return new Person("kazusa",true,21); + } +} + +@RestController +public class ModelAttributeController { + + @PostMapping("/hello") + public Person sayHello(@ModelAttribute(name="name",binding=true) Person person, Model model) { + System.out.println(model); + return person; + } +} +``` +在model attribute被获取之后,会执行data binding。WebDataBinder会将request param name和目标对象的field进行匹配,匹配的field会通过类型转换进行注入。 +如果想要访问Model Attribute但是不想使用data binding,可以直接在handler method参数中使用Model,或者将@ModelAttribute注解的binding属性设置为false。 +```java +@PostMapping("update") +public String update(@Valid AccountForm form, BindingResult result, + @ModelAttribute(binding=false) Account account) { + // ... +} +``` +#### @SessionAttributes +@SessionAttributes注解用于将model attribute存储在http session中,从而在不同的请求间都可以访问该attribute。 +@SessionAttributes为class-level的注解,注解的value应该列出model attribute name或model attribute的类型。 +该注解使用如下所示: +```java +@Controller +@SessionAttributes("pet") +public class EditPetForm { + + // ... + + @PostMapping("/pets/{id}") + public String handle(Pet pet, BindingResult errors, SessionStatus status) { + if (errors.hasErrors) { + // ... + } + status.setComplete(); + // ... + } +} +``` +当第一个请求到来时,name为pet的Model Attribute被添加到model中,其会被自动提升并保存到http session中,知道另一个controller method通过SessionStatus方法来清除存储。 +#### @SessionAttribute +如果想要访问已经存在的Session Attribute,可以在handler method的参数上添加@SessionAttribute,如下所示: +```java +@RequestMapping("/") +public String handle(@SessionAttribute User user) { + // ... +} +``` +#### Multipart +在MultipartResolver被启用之后,Post请求体中为multipart/form-data格式的数据将被转化并可以作为handler method的参数访问。如下显示了文件上传的用法: +```java +@Controller +public class FileUploadController { + + @PostMapping("/form") + public String handleFormUpload(@RequestParam("name") String name, + @RequestParam("file") MultipartFile file) { + + if (!file.isEmpty()) { + byte[] bytes = file.getBytes(); + // store the bytes somewhere + return "redirect:uploadSuccess"; + } + return "redirect:uploadFailure"; + } +} +``` +> 在上述代码中,可以将MultipartFile改为List\来解析多个文件名 +> **当@RequestParam没有指定name属性并且参数类型为Map或MultiValueMap类型时,会根据name自动将所有匹配的MultipartFile注入到map中** + +#### @RequestBody +可以通过@RequestBody注解读取请求体内容,并且通过HttpMessageConverter将内容反序列化为Object。具体使用方法如下: +```java +@PostMapping("/accounts") +public void handle(@RequestBody Account account) { + // ... +} +``` +#### HttpEntity +将参数类型指定为HttpEntity与使用@RequestBody注解类似,其作为一个容器包含了请求体转化后的对象(account实例)和请求头(HttpHeaders)。其使用如下: +```java +@PostMapping("/accounts") +public void handle(HttpEntity entity) { + // ... +} +``` +#### @ResponseBody +通过使用@ResponseBody注解,可以将handler method的返回值序列化到响应体中,通过HttpMessageConverter。具体使用如下所示: +```java +@GetMapping("/accounts/{id}") +@ResponseBody +public Account handle() { + // ... +} +``` +同样的,@ResponseBody注解也支持class-level级别的使用,在使用@ResponseBody标注类后对所有controller method都会起作用。 +通过@RestController可以起到同样作用。 +#### ResponseEntity +ResponseEntity使用和@ResponseBody类似,但是含有status和headers。ResponseEntity使用如下所示: +```java +@GetMapping("/something") +public ResponseEntity handle() { + String body = ... ; + String etag = ... ; + return ResponseEntity.ok().eTag(etag).body(body); +} +``` +#### Jackson JSON +Spring MVC为Jackson序列化view提供了内置的支持,可以渲染对象中的字段子集。 +如果想要在序列化返回值时仅仅序列化某些字段,可以通过@JsonView注解来指明序列化哪些字段。 +##### @JsonView +@JsonView的使用如下所示, +- 通过interface来指定渲染的视图 +- 在字段的getter方法上通过@JsonView来标注视图类名 +- interface支持继承,如WithPasswordView继承WithoutPasswordView,故而WithPasswordView在序列化时不仅会包含其自身的password字段,还会包含从WithoutPasswordView中继承而来的name字段 +> 对于一个handler method,只能够通过@JsonView指定view class,如果想要激活多个视图,可以使用合成的view class +```java +@RestController +public class UserController { + + @GetMapping("/user") + @JsonView(User.WithoutPasswordView.class) + public User getUser() { + return new User("eric", "7!jd#h23"); + } +} + +public class User { + + public interface WithoutPasswordView {}; + public interface WithPasswordView extends WithoutPasswordView {}; + + private String username; + private String password; + + public User() { + } + + public User(String username, String password) { + this.username = username; + this.password = password; + } + + @JsonView(WithoutPasswordView.class) + public String getUsername() { + return this.username; + } + + @JsonView(WithPasswordView.class) + public String getPassword() { + return this.password; + } +} +``` +### Model +#### @ModelAttribute注解用法 +- 将@ModelAttribute注解标记在handler method的参数上,用于创建或访问一个对象,该对象从model中获取,并且该对象通过WebDataBinder与http request绑定在一起。 +- 将@ModelAttribute注解绑定在位于@Controller或@ControllerAdvice类中的方法上,用于在任何handler method调用之前初始化model +- 将@ModelAttribute注解绑定在@RequestMapping方法上,用于标注该方法的返回值是一个model attribute + +#### @ModelAttribute作用于Controller类中普通方法上 +对于上述的第二种使用,一个controller类中可以含有任意数量个@ModelAttribute方法,所有的这些方法都会在@RequestMapping方法调用之前被调用。(**同一个@ModelAttribute方法,也可以通过@ControllerAdvice在多个controllers之间进行共享**)。 +@ModelAttribute方法支持可变的参数形式,其参数形式可以和@RequestMapping方法中的参数形式一样。(**但是,@ModelAttribute方法的参数中不能含有@ModelAttribute注解本身,参数也不能含有和http请求体相关的内容**)。 +```java +@ModelAttribute +public void populateModel(@RequestParam String number, Model model) { + model.addAttribute(accountRepository.findAccount(number)); + // add more ... +} +``` +也可以通过如下方式向model中添加attribute: +```java +// 只向model中添加一个属性 +@ModelAttribute +public Account addAccount(@RequestParam String number) { + return accountRepository.findAccount(number); +} +``` +> 在向model中添加属性时,如果attribute name没有显式指定,那么attribute name将会基于attribute value的类型来决定。可以通过model.addAttribute(attributeName,attributeValue)来指定attribute name,或者通过指定@ModelAttribute的name属性来指定attribute name + +#### @ModelAttribute作用于@RequestMapping方法上 +对于第三种使用,可以将@ModelAttribute注解标注在@RequestMapping方法之上,在这种情况下方法的返回值将会被解释为model attribute。**在这种情况下@ModelAttribute注解不是必须的,应为该行为是html controller的默认行为,除非返回值是String类型,此时返回值会被解释为view name。** +可以通过如下方式来自定义返回model attribute的attribute name,如下图所示: +```java +// 指定attribute name为“myAccount” +@GetMapping("/accounts/{id}") +@ModelAttribute("myAccount") +public Account handle() { + // ... + return account; +} +``` +### DataBinder +对于@Controller和@ControllerAdvice类,在类中可以包含@InitBinder方法,该方法用来初始化WebDataBinder实例: +- 将请求参数绑定到model +- 将基于String类型的请求值(例如请求参数,pathVariable,headers,cookies或其他)转化为controller method参数的类型 +- 在html渲染时,将model object value转化为String的形式 + +@InitBinder可以针对特定的Controller注册java.beans.PropertyEditor或Spring Converter和Formatter 组件。 +@InitBinder支持和@RequestMapping方法一样的参数形式,但是参数不能使用@ModelAttribute注解。通常,@InitBinder方法的参数为WebDataBinder,且返回类型为void: +```java +@Controller +public class FormController { + // WebDataBinder参数用于注册 + @InitBinder + public void initBinder(WebDataBinder binder) { + SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd"); + dateFormat.setLenient(false); + binder.registerCustomEditor(Date.class, new CustomDateEditor(dateFormat, false)); + } + + // ... +} +``` +同样,当使用一个共享的FormattingConversionService来设置格式时,可以注册针对特定Controller的Formatter实现,例如: +```java +@Controller +public class FormController { + + @InitBinder + protected void initBinder(WebDataBinder binder) { + binder.addCustomFormatter(new DateFormatter("yyyy-MM-dd")); + } + + // ... +} +``` +### Model Design +在web应用的上下文中,data binding涉及将http请求中的参数绑定到model object及其内层嵌套对象中。 +默认情况下,所有spring允许绑到model object中所有的公共属性(有public的getter和setter)。 +通常情况下,会自定义一个特定的model object类,并且该类中的public属性与表单中提交的参数相关联。 +```java +// 只会对如下两个public属性进行data binding +public class ChangeEmailForm { + + private String oldEmailAddress; + private String newEmailAddress; + + public void setOldEmailAddress(String oldEmailAddress) { + this.oldEmailAddress = oldEmailAddress; + } + + public String getOldEmailAddress() { + return this.oldEmailAddress; + } + + public void setNewEmailAddress(String newEmailAddress) { + this.newEmailAddress = newEmailAddress; + } + + public String getNewEmailAddress() { + return this.newEmailAddress; + } + +} +``` +### Exception +在@Controller和@ControllerAdvice类中,可以含有@ExceptionHandler方法,该方法用于处理controller方法中抛出的异常,使用如下所示: +```java +@Controller +public class SimpleController { + + // ... + + @ExceptionHandler + public ResponseEntity handle(IOException ex) { + // ... + } +} +``` +该异常参数会匹配被抛出的顶层异常(例如,被直接抛出的IOException),也会匹配被包装的内层cause(例如,被包装在IllegalStateException中的IOException)。**该参数会匹配任一层级的cause exception,并不是只有该异常的直接cause exception才会被处理。** +> 只要@ExceptionHandler方法的异常参数类型匹配异常抛出stacktrace中任一层次的异常类型,异常都会被捕获并且处理。 +```java + @PostMapping("/shiro") + public String shiro() { + throw new RuntimeException(new JarException("fuck")); + } + + @ExceptionHandler + public String handleJarException(JarException e) { + return e.getMessage() + ", Jar"; + } +``` +> 如果有多个@ExceptionHandler方法匹配抛出的异常链,那么root exception匹配会优先于cause exception匹配。 +> ExceptionDepthComparator会根据各个异常类型相对于抛出异常类型(root exception)的深度来进行排序。 +> ```java +> // 在调用/shiro接口时,IOException离root exception(RuntimeException)更近, +> // 故而会优先调用handleIOException方法 +> @PostMapping("/shiro") +> public String shiro() throws IOException { +> throw new RuntimeException(new IOException(new SQLException("fuck"))); +> } +> +> @ExceptionHandler +> public String handleIOException(IOException e) { +> return e.getMessage() + ", IO"; +> } +> +> @ExceptionHandler +> public String handleSQLException(SQLException e) { +> return e.getMessage() + ",Exception"; +> } +> ``` + +对于@ExceptionHandler,可以指定该方法处理的异常类型来缩小范围,如下方法都只会匹配root exception为指定异常或异常链中包含指定异常的场景: +```java +@ExceptionHandler({FileSystemException.class, RemoteException.class}) +public ResponseEntity handle(IOException ex) { + // ... +} + +// 甚至,可以将参数的异常类型指定为一个非常宽泛的类型,例如Exception +@ExceptionHandler({FileSystemException.class, RemoteException.class}) +public ResponseEntity handle(Exception ex) { + // ... +} +``` +上述两种写法的区别是: +1. 如果参数类型为IOException,那么当cause exception为FileSystemException或RemoteException,且root exception为IOException时,实际的cause exception要通过ex.getCause来获取 +2. 如果参数类型为Exception,那么当cause exception为FileSystemException或RemoteException且root exception不为指定类型异常时,指定类型异常统一都通过ex.getCause来获取 +> 通常情况下,@ExceptionHandler方法的参数类型应该尽可能的具体,而且推荐将一个宽泛的@ExceptionHandler拆分成多个独立的@ExceptionHandler方法,每个方法负责处理特定类型的异常。 + +**对于@ExceptionHandler方法,在捕获异常之后可以对异常进行rethrow操作,重新抛出之后的异常会在剩余的resolution chain中传播,就好像给定的@ExceptionHandler在开始就没有匹配** + +### Controller +对于@ModelAttribute、@InitBinder、@ExceptionHandler注解,若其在@Controller类中使用,那么该类仅对其所在的@Controller类有效。 +**如果这些注解定义在@ControllerAdvice或@RestControllerAdvice类中,那么它们对所有的@Controller类都有效。** +**位于@ControllerAdvice类中的@ExceptionHandler方法,其可以用于处理任何@Controller类中抛出的异常或任何其他exception handler中抛出的异常。** +> 对于@ControllerAdvice中的@ExceptionHandler方法,其会在其他所有位于@Controller类中的@ExceptionHandler方法执行完**之后**才会执行;而@InitBinder和@ModelAttribute方法,则是位于@ControllerAdive方法中的**优先于**位于@Controller类中的执行 +> +@ControllerAdive使用如下: +```java +// Target all Controllers annotated with @RestController +@ControllerAdvice(annotations = RestController.class) +public class ExampleAdvice1 {} + +// Target all Controllers within specific packages +@ControllerAdvice("org.example.controllers") +public class ExampleAdvice2 {} + +// Target all Controllers assignable to specific classes +@ControllerAdvice(assignableTypes = {ControllerInterface.class, AbstractController.class}) +public class ExampleAdvice3 {} +``` + +## CORS +Spring MVC允许处理跨域问题。 +> ### 跨域问题 +> 处于对安全的考虑,浏览器禁止ajax访问非当前origin的资源。 +> 对于简单请求,浏览器会直接通过请求和响应中特定header来判断当前资源是否能够被访问;对于非简单请求,则是在请求之前会发送一个预检请求(OPTIONS请求,判断当前资源能否被访问)。 + +在Spring MVC中,HandlerMapping实现提供了对CORS的支持。在实际将一个请求映射到handler后,HandlerMapping实现将会检查请求和handler的CORS配置并且做进一步的处理。 +如果想要启用CORS,需要显式声明CORS配置,如果匹配的CORS配置未被找到,那么预检请求将会被拒绝。(CORS header将不会添加到简单请求和实际CORS请求的响应中,浏览器将会拒绝没有CORS header的响应)。 +### @CrossOrigin +可以对handler method使用@CrossOrigin注解以允许跨域请求,使用示例如下: +```java +@RestController +@RequestMapping("/account") +public class AccountController { + + @CrossOrigin + @GetMapping("/{id}") + public Account retrieve(@PathVariable Long id) { + // ... + } + + @DeleteMapping("/{id}") + public void remove(@PathVariable Long id) { + // ... + } +} +``` +默认情况下,@CrossOrigin会允许来自所有origin、含有任意header和所有http method。 +- allowCredentials默认情况下没有启用,除非allowOrigins或allowOriginPatterns被指定了一个非"*"的值。 +- maxAge默认会设置为30min + +@CrossOrigin注解支持在类上使用,类上注解会被方法继承 +```java +@CrossOrigin(origins = "https://domain2.com", maxAge = 3600) +@RestController +@RequestMapping("/account") +public class AccountController { + + @GetMapping("/{id}") + public Account retrieve(@PathVariable Long id) { + // ... + } + + @DeleteMapping("/{id}") + public void remove(@PathVariable Long id) { + // ... + } +} +``` +@CrossOrigin注解可同时在类和方法上使用 +```java +@CrossOrigin(maxAge = 3600) +@RestController +@RequestMapping("/account") +public class AccountController { + + @CrossOrigin("https://domain2.com") + @GetMapping("/{id}") + public Account retrieve(@PathVariable Long id) { + // ... + } + + @DeleteMapping("/{id}") + public void remove(@PathVariable Long id) { + // ... + } +} +``` +### spring boot全局配置CORS +在spring boot中,可以通过如下方式全局配置CORS +```java +@Configuration +public class WebConfig implements WebMvcConfigurer { + @Override + public void addCorsMappings(CorsRegistry registry) { + registry.addMapping("/api/**") + .allowedOrigins("http://localhost:4200") + .allowedMethods("*") + .allowedHeaders("*") + .allowCredentials(false) + .maxAge(3600); + } +} +``` + diff --git a/spring/json/gson/gson.md b/spring/json/gson/gson.md index 7c89300..6223c1d 100644 --- a/spring/json/gson/gson.md +++ b/spring/json/gson/gson.md @@ -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 - - - - com.google.code.gson - gson - 2.9.1 - compile - - -``` -### 基本类型的序列化和反序列化 -```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 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>(){}.getType(); -Collection 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 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 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>(){}.getType(); -String json = "{\"key\": \"value\"}"; - -// Deserialization -Map 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 value; -} -Gson gson = new Gson(); -Foo foo = new Foo(); -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>() {}.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 + + + + com.google.code.gson + gson + 2.9.1 + compile + + +``` +### 基本类型的序列化和反序列化 +```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 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>(){}.getType(); +Collection 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 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 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>(){}.getType(); +String json = "{\"key\": \"value\"}"; + +// Deserialization +Map 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 value; +} +Gson gson = new Gson(); +Foo foo = new Foo(); +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>() {}.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来调用 + + diff --git a/spring/log/log.md b/spring/log/log.md index af187dc..5177dbe 100644 --- a/spring/log/log.md +++ b/spring/log/log.md @@ -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 + diff --git a/spring/lombok/lombok标签简介.md b/spring/lombok/lombok标签简介.md index 1464c11..4fdbfcc 100644 --- a/spring/lombok/lombok标签简介.md +++ b/spring/lombok/lombok标签简介.md @@ -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隐藏这些方法 \ No newline at end of file diff --git a/spring/spring boot/Spring Boot Async.md b/spring/spring boot/Spring Boot Async.md index c0903b2..f78a16c 100644 --- a/spring/spring boot/Spring Boot Async.md +++ b/spring/spring boot/Spring Boot Async.md @@ -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 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 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 * * * *) | diff --git a/spring/spring boot/Spring Data Redis.md b/spring/spring boot/Spring Data Redis.md index d1d368a..f52ceda 100644 --- a/spring/spring boot/Spring Data Redis.md +++ b/spring/spring boot/Spring Data Redis.md @@ -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 redisTemplate; - - // 将RedisTemplate转换成其某一个视图之后再注入 - @Resource(name="redisTemplate") - private ListOperations 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 redisTemplate(RedisConnectionFactory redisConnectionFactory) { - RedisTemplate 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 redisTemplate; + + // 将RedisTemplate转换成其某一个视图之后再注入 + @Resource(name="redisTemplate") + private ListOperations 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 redisTemplate(RedisConnectionFactory redisConnectionFactory) { + RedisTemplate redisTemplate = new RedisTemplate<>(); + redisTemplate.setConnectionFactory(redisConnectionFactory); + redisTemplate.setDefaultSerializer(new GenericJackson2JsonRedisSerializer()); + return redisTemplate; + } + } ``` \ No newline at end of file diff --git a/spring/spring boot/Spring Data.md b/spring/spring boot/Spring Data.md index 6ddfe33..2846453 100644 --- a/spring/spring boot/Spring Data.md +++ b/spring/spring boot/Spring Data.md @@ -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,那么自动装配将不会发生。 \ No newline at end of file diff --git a/spring/spring boot/UnitTest.md b/spring/spring boot/UnitTest.md index c9c7804..18bec1f 100644 --- a/spring/spring boot/UnitTest.md +++ b/spring/spring boot/UnitTest.md @@ -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 \ No newline at end of file diff --git a/spring/spring boot/json.md b/spring/spring boot/json.md index 21ee1b2..ea32a80 100644 --- a/spring/spring boot/json.md +++ b/spring/spring boot/json.md @@ -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 { - - @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 { - - @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 { - - @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 { - - @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 { + + @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 { + + @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 { + + @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 { + + @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类域字段的映射关系 + diff --git a/spring/文件上传和下载/File Upload&Download.md b/spring/文件上传和下载/File Upload&Download.md index 4c53764..5b07da8 100644 --- a/spring/文件上传和下载/File Upload&Download.md +++ b/spring/文件上传和下载/File Upload&Download.md @@ -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 ``` \ No newline at end of file diff --git a/分布式事务/Saga.md b/分布式事务/Saga.md index 4715aea..01a05cf 100644 --- a/分布式事务/Saga.md +++ b/分布式事务/Saga.md @@ -1,22 +1,22 @@ -# 分布式事务: Saga模式 -## Saga模型 -- 每个Saga都由一系列子事务Ti组成 -- 每个子事务Ti都有其对应的补偿动作Ci,补偿操作用于对Ti操作产生的结果进行撤销 -## Saga的执行顺序 -1. T1, T2, T3, ... ,Tn(全部执行成功) -2. T1, T2, T3, ..., Tj, Cj, Cj-1,... , C1 (执行出错之后回滚之前所有成功的子事务) -## 执行出错时,Saga的恢复策略 -1. 向后恢复(backword recovery),对所有已经完成的子事务进行补偿操作,任一子事务Ti执行失败,撤销掉之前所有执行成功的子事务,使得整个Saga的执行结果都撤销 -2. 向前恢复(forward recovery),重新尝试失败的事务。假设每个事务最终都会执行成功。其执行顺序为T1, ... Tj(失败), Tj(重试),...,...,Tn。该种情况下并不需要Ci操作对子事务Ti的操作结果进行撤销 -## Saga模式的结构 -Saga的嵌套结构只允许存在两个层次: -1. 顶层的Saga -2. 简单的子事务 -## Saga特性 -- 在外层(各个Sagas都在执行时,各个Sagas在执行过程中已经提交的子事务Ti,其执行结果对其他Sagas可见),隔离性并无法被满足,Sagas可能看到其他Sagas的执行结果 -- 每个子事务都是一个独立事务,各个子事务为独立的原子行为 -## Saga ACID -- 原子性:正常情况下可以保证 -- 一致性:执行过程中,可能会有A库和B库违反一致性要求的情况,但是最终成功之后是一致的 -- 隔离性:Sagas A能看到Sagas B部分执行的执行结果 +# 分布式事务: Saga模式 +## Saga模型 +- 每个Saga都由一系列子事务Ti组成 +- 每个子事务Ti都有其对应的补偿动作Ci,补偿操作用于对Ti操作产生的结果进行撤销 +## Saga的执行顺序 +1. T1, T2, T3, ... ,Tn(全部执行成功) +2. T1, T2, T3, ..., Tj, Cj, Cj-1,... , C1 (执行出错之后回滚之前所有成功的子事务) +## 执行出错时,Saga的恢复策略 +1. 向后恢复(backword recovery),对所有已经完成的子事务进行补偿操作,任一子事务Ti执行失败,撤销掉之前所有执行成功的子事务,使得整个Saga的执行结果都撤销 +2. 向前恢复(forward recovery),重新尝试失败的事务。假设每个事务最终都会执行成功。其执行顺序为T1, ... Tj(失败), Tj(重试),...,...,Tn。该种情况下并不需要Ci操作对子事务Ti的操作结果进行撤销 +## Saga模式的结构 +Saga的嵌套结构只允许存在两个层次: +1. 顶层的Saga +2. 简单的子事务 +## Saga特性 +- 在外层(各个Sagas都在执行时,各个Sagas在执行过程中已经提交的子事务Ti,其执行结果对其他Sagas可见),隔离性并无法被满足,Sagas可能看到其他Sagas的执行结果 +- 每个子事务都是一个独立事务,各个子事务为独立的原子行为 +## Saga ACID +- 原子性:正常情况下可以保证 +- 一致性:执行过程中,可能会有A库和B库违反一致性要求的情况,但是最终成功之后是一致的 +- 隔离性:Sagas A能看到Sagas B部分执行的执行结果 - 持久性:对于Sagas中的子事务Ti,其执行完成之后就会commit,而其撤销操作则是会调用Ci对已经提交的操作进行撤销操作 \ No newline at end of file diff --git a/分布式事务/Seata框架/Seata简介.md b/分布式事务/Seata框架/Seata简介.md index 7fd6eb5..83cb182 100644 --- a/分布式事务/Seata框架/Seata简介.md +++ b/分布式事务/Seata框架/Seata简介.md @@ -1,8 +1,8 @@ -# Seata -## Seata简介 -Seata是一款开源的分布式事务解决方案,向客户提供了高性能且简单易用的分布式事务服务。Seata向客户提供了AT,TCC,SAGA,XA等模式。 -## AT模式 -### 前提 -- 基于的关系型数据库要支持本地事务的acid -- java应用,通过jdbc访问数据库 +# Seata +## Seata简介 +Seata是一款开源的分布式事务解决方案,向客户提供了高性能且简单易用的分布式事务服务。Seata向客户提供了AT,TCC,SAGA,XA等模式。 +## AT模式 +### 前提 +- 基于的关系型数据库要支持本地事务的acid +- java应用,通过jdbc访问数据库 - \ No newline at end of file diff --git a/分布式事务/TCC.md b/分布式事务/TCC.md index de79ad9..d797638 100644 --- a/分布式事务/TCC.md +++ b/分布式事务/TCC.md @@ -1,32 +1,32 @@ -# 分布式事务: TCC模式 -## TCC事务 -TCC是Try、Confirm、Cancel三个单词的首字母缩写,TCC分为三个阶段:预处理(Try)、确认(Confirm)、撤销(Cancel)。 -## TCC工作流 -## 预处理阶段(Try phase) -在预处理阶段,请求者会请求服务的提供者做一些尝试性的操作。在该阶段,服务提供者会完成一些业务检查与验证,并且预占需要的业务资源。 -## 确认阶段(Confirm phase) -- 如果服务提供者成功执行了Try阶段,并且请求者决定继续执行操作,那么请求者可以在确认阶段执行确认操作。 -- ***具体的业务在确认阶段被执行***。 -- 在确认阶段,不会执行更多的验证操作,所有验证操作都会在Try阶段执行 -- 在Confirm阶段,只会使用Try阶段预留的业务资源 -> 通常,认为Confirm阶段是不会执行失败的,如果Try阶段被成功执行,那么Confirm阶段也能成功执行,如果Try成功而Confirm执行失败,会对Confirm阶段进行重试 -## 撤销阶段(Cancel phase) -在撤销阶段,如果Try阶段未正确完成且请求者决定不再继续执行,请求者可以在服务提供者上执行撤销操作。在Try阶段预占的业务资源应该在撤销阶段被正确释放 -***撤销阶段(Cancel)是对预处理阶段(Try)的反向操作*** -## TCC事务 -对于TCC事务,全局事务管理器首先会发起所有分支事务的Try操作 -- 若任何一个分支事务的Try操作执行失败,都会导致所有分支事务的Cancel操作被执行 -- 若所有分支事务的Try操作都执行成功,那么全局事务管理器会发起所有分支事务的Confirm操作 -在执行分支事务Confirm/Cancel操作时,如果执行失败,那么全局事务管理器会对失败操作进行重试 -> 通常,Cancel阶段也被认为是一定执行成功的,如果在Cancel执行时发生错误,需要进行重试或进行人工处理 -## TCC事务中需要注意的要点 -### 空回滚 -若是在没有调用TCC Try阶段的情况下直接调用二阶段的Cancel方法,Cancel方法需要识别出之前并未调用过Try阶段方法,直接返回Cancel成功 -### 幂等 -由于在各阶段执行时都存在重试机制,故而要保证Try、Confirm、Cancel接口都要保证幂等性,保证占用资源或释放资源的操作不会被重复执行 -### 悬挂 -在RPC调用过程中,由于网络拥堵等原因,Cancel操作可能先于Try操作执行。 -> 1. 如果先调用Try时,网络拥堵发生超时,那么TM会通知RM回滚分布式事务,调用Cancel操作 -> 2. 因为网络拥堵,可能调用完Cancel后,RPC的Try请求才到来 -> 3. 此时到来的Try请求会预留资源,而预留资源无法被其他事务所使用 - +# 分布式事务: TCC模式 +## TCC事务 +TCC是Try、Confirm、Cancel三个单词的首字母缩写,TCC分为三个阶段:预处理(Try)、确认(Confirm)、撤销(Cancel)。 +## TCC工作流 +## 预处理阶段(Try phase) +在预处理阶段,请求者会请求服务的提供者做一些尝试性的操作。在该阶段,服务提供者会完成一些业务检查与验证,并且预占需要的业务资源。 +## 确认阶段(Confirm phase) +- 如果服务提供者成功执行了Try阶段,并且请求者决定继续执行操作,那么请求者可以在确认阶段执行确认操作。 +- ***具体的业务在确认阶段被执行***。 +- 在确认阶段,不会执行更多的验证操作,所有验证操作都会在Try阶段执行 +- 在Confirm阶段,只会使用Try阶段预留的业务资源 +> 通常,认为Confirm阶段是不会执行失败的,如果Try阶段被成功执行,那么Confirm阶段也能成功执行,如果Try成功而Confirm执行失败,会对Confirm阶段进行重试 +## 撤销阶段(Cancel phase) +在撤销阶段,如果Try阶段未正确完成且请求者决定不再继续执行,请求者可以在服务提供者上执行撤销操作。在Try阶段预占的业务资源应该在撤销阶段被正确释放 +***撤销阶段(Cancel)是对预处理阶段(Try)的反向操作*** +## TCC事务 +对于TCC事务,全局事务管理器首先会发起所有分支事务的Try操作 +- 若任何一个分支事务的Try操作执行失败,都会导致所有分支事务的Cancel操作被执行 +- 若所有分支事务的Try操作都执行成功,那么全局事务管理器会发起所有分支事务的Confirm操作 +在执行分支事务Confirm/Cancel操作时,如果执行失败,那么全局事务管理器会对失败操作进行重试 +> 通常,Cancel阶段也被认为是一定执行成功的,如果在Cancel执行时发生错误,需要进行重试或进行人工处理 +## TCC事务中需要注意的要点 +### 空回滚 +若是在没有调用TCC Try阶段的情况下直接调用二阶段的Cancel方法,Cancel方法需要识别出之前并未调用过Try阶段方法,直接返回Cancel成功 +### 幂等 +由于在各阶段执行时都存在重试机制,故而要保证Try、Confirm、Cancel接口都要保证幂等性,保证占用资源或释放资源的操作不会被重复执行 +### 悬挂 +在RPC调用过程中,由于网络拥堵等原因,Cancel操作可能先于Try操作执行。 +> 1. 如果先调用Try时,网络拥堵发生超时,那么TM会通知RM回滚分布式事务,调用Cancel操作 +> 2. 因为网络拥堵,可能调用完Cancel后,RPC的Try请求才到来 +> 3. 此时到来的Try请求会预留资源,而预留资源无法被其他事务所使用 +