前言
SpringBoot稍微学了集成redis、dubbo等的demo,项目看不下去,还是沉下去看看底层原理比较充实,不然太虚了。
Java设计模式
反射
反射技术
1 | import java.lang.reflect.InvocationTargetException; |
静态代理
1 | import java.lang.reflect.InvocationHandler; |
动态代理
JDK动态代理
1 | import java.lang.reflect.InvocationHandler; |
1 | 进入代理逻辑方法 |
Cglib动态代理
1 | import net.sf.cglib.proxy.Enhancer; |
拦截器
1 | import java.lang.reflect.InvocationHandler; |
1 | 反射方法前逻辑 |
责任链模式/多级代理
责任链模式的经典方法见设计模式,这里用多级代理实现责任链模式
1 | import java.lang.reflect.InvocationHandler; |
1 | 【拦截器3】的before方法 |
观察者模式
1 | import java.util.ArrayList; |
1 | 产品列表新增了产品:新增产品1 |
建造者模式
简单实现以及建造者模式对比
1 | import com.alibaba.fastjson.JSON; |
1 | {"itemName":"普通商品","type":1} |
静态内部类的实现
1 | class CopyItem{ |
1 | Exception in thread "main" java.lang.IllegalArgumentException: 库存数量错误 |
互联网持久层框架——MyBatis
认识MyBatis核心组件
Mybatis的核心组件
- SqlSessionFactoryBuilder(构造—):它会根据配置或者代码来生成SqlSessionFactory,采用的是分步构建的Builder模式;
- SqlSessionFactory (工厂接口):依靠它来生成SqlSession,使用的是工厂模式;
- SqlSession(会话):一个既可以发送SQL执行返回结果,也可以获取Mapper的接口。在现有的技术中,一般我们会让其在业务逻辑代码中“消失”,而使用的是MyBatis提供的SQL Mapper接口编程技术,它能提高代码的可读性和可维护性;
- SQL Mapper(映射器):MyBatis新设计存在的组件,它由一个Java接口和XML文件(或注解)构成,需要给出对应的SQL和映射规则。它负责发送SQL去执行,并返回结果。
SqlSessionFactory
XML构建方式(推荐)
1 |
|
1 | SqlSessionFactory sqlSessionFactory = null; |
代码构建方式
1 | package mybatis; |
SqlSession
1 | public static void helper(){ |
Mapper映射器
POJO和映射接口
1 | class Role{ |
XML映射文件实现
1 |
|
注解实现
1 | interface RoleMapper{ |
SqlSession发送SQL
1 | Role role = (Role) sqlSession.selectOne("RoleMapper.getRole", 1L); |
用Mapper接口发送SQL(推荐)
1 | RoleMapper roleMapper = sqlSession.getMapper(RoleMapper.class); |
生命周期
- SqlSessionFatoryBuilder的作用在于创建SqlSessionFactory,创建成功后,SqlSessionFactoryBuilder就失去了作用,所以它只能存在于创建SqlSessionFactory的方法中,而不要让其长期存在;
- SqlSessionFactory可以被认为是一个数据库连接池,它的作用是创建SqlSession接口对象。所以SqlSessionFactory的生命周期存在于整个MyBatis的应用之中,所以一旦创建了SqlSessionFactory,就要长期保存它,直至不再使用Mybatis。
- SqlSession相当于一个数据库连接,可以字啊一个事务里面执行多条SQL,然后commit、rollback等,其应该存活在一个业务请求中,处理完整个请求后,应当关闭连接,归还给SqlSessionFactory,否则数据库资源就很快被耗费光,系统就会瘫痪,所以需要try/catch/finally语句来保证其正确关闭
- Mapper是一个借口,由SqlSession创建,所以其最大生命周期至多和SqlSession保持一致,尽管它很好用,但是由于SqlSession的关闭,它的数据库连接资源也会消失,所以它的生命周期应该小于等于SqlSession的生命周期。
实例
log4j.properties
1 | log4j.rootLogger=DEBUG,stdout |
mybatis-config.xml
1 |
|
RoleMapper.xml
1 |
|
Role
1 | package mybatis; |
RoleMapper
1 | package mybatis; |
SqlSessionFactoryUtil
1 | package mybatis; |
运行结果
1 | "C:\Program Files\Java\jdk-14.0.2\bin\java.exe" "-javaagent:D:\IntelliJ IDEA 2020.1.1\lib\idea_rt.jar=59486:D:\IntelliJ IDEA 2020.1.1\bin" -Dfile.encoding=UTF-8 -classpath D:\JavaTest\out\production\JavaTest;D:\download\cglib-nodep-3.3.0.jar;D:\download\fastjson-1.2.76.jar;D:\download\commons-lang3-3.12.0.jar;D:\download\mybatis-3.5.7\mybatis-3.5.7.jar;D:\download\mysql-connector-java-8.0.19.jar;D:\download\mybatis-3.5.7\lib\asm-7.1.jar;D:\download\mybatis-3.5.7\lib\cglib-3.3.0.jar;D:\download\mybatis-3.5.7\lib\ognl-3.2.20.jar;D:\download\mybatis-3.5.7\lib\log4j-1.2.17.jar;D:\download\mybatis-3.5.7\lib\log4j-api-2.14.1.jar;D:\download\mybatis-3.5.7\lib\slf4j-api-1.7.30.jar;D:\download\mybatis-3.5.7\lib\log4j-core-2.14.1.jar;D:\download\mybatis-3.5.7\lib\commons-logging-1.2.jar;D:\download\mybatis-3.5.7\lib\javassist-3.27.0-GA.jar;D:\download\mybatis-3.5.7\lib\slf4j-log4j12-1.7.30.jar mybatis.SqlSessionFactoryUtils |
MyBatis配置
MyBatis配置文件并不复杂,其所有元素如下所示:
MyBatis配置项的顺序不能颠倒。
1 |
|
properties属性
properties属性可以给系统配置一些运行参数,可以放在XML文件或者properties文件中,而不是放在Java编码汇总,方便参数修改。
三种方式配置:
- property子元素
- properties文件
- 程序代码传递
property子元素
1 |
|
使用properties文件
1 | database.driver=com.mysql.cj.jdbc.Driver |
在xml配置通过
1 | <properties resource="jdbc.properties"> |
使用程序传递方式传递参数
1 | String resource = "mybatis-config.xml"; |
优先级
最优先的是程序传递的方式,其次是使用properties文件的方式,最后是使用property子元素的方式,MyBatis会根据优先级来覆盖原先配置的属性值。
settings设置
settings是MyBatis中最复杂的配置,它能深刻影响MyBatis底层的运行,但是在大部分情况下使用默认值便可以运行,所以在大部分情况下不需要大量配置它,只需要修改一些常用的规则即可,比如自动映射、驼峰命名映射、级联规则、是否启动缓存、执行器(Executor)类型等。
配置项 | 作用 | 配置选项说明 | 默认值 |
---|---|---|---|
cacheEnable | 改配置影响所有映射器中配置缓存的全局开关 | true|false | true |
lazyLoadingEnable | 延迟加载的全局开关。当开启时,所有关联对象都会延迟加载。在特定关联关系中可通过设置fetchType属性来覆盖该项的开关状态 | true|false | false |
aggressiveLazyLoading | 当启用时,对任意延迟属性的调用会使带有延迟加载属性的对象完整加载;反之,每种属性将会按需加载 | true|false | 版本3.4.1(不包含) 之前true,之后false |
multipleResultSetsEnabled | 是否允许单一语句返回多结果集(需要兼容驱动) | true|false | true |
useColumnLabel | 使用列标签代替列名。不同的驱动会有不同的表现,具体可参考相关驱动文档或通过测试这两种不同的模式来观察所用驱动的结果 | true\false | true |
useGeneratedKeys | 允许JDBC支持自动生成主键,需要驱动兼容。如果设置为true,则这个设置强制使用自动生成主键,尽管一些驱动不能兼容但仍可正常工作(比如Derby) | true\false | false |
autoMappingBehavior | 指定MyBatis应如何自动映射列到字段或属性。NONE表示取消自动映射:PARTIAL表示自动映射,没有定义嵌套结果集合映射结果集。FULL会自动映射任意复杂的结果集(无论是否嵌套) | NONE、PARTIAL、FULL | PARTIAL |
autoMappingUnknownColumnBehavior | 指定自动映射当中未知列(或未知属性类型)时的行为,默认是不处理,只有当日志级别达到WARN级别或者以下,才会显示相关日志,如果处理失败会抛出SqlSessionException异常 | NONE、PARTIAL、FULL | NONE |
defaultExecutorType | 配置默认的执行器。SIMPLE是普通的执行器;REUSE会重用预处理语句(prepared statements);BATCH执行器将重用语句并执行批量更新 | SIMPLE、REUSE、BATCH | SIMPLE |
defaultStatementTimeout | 设置超时时间,它决定驱动等待数据库响应的秒数 | 任何正整数 | Not Set(null) |
defaultFetchSize | 设置数据库驱动程序默认返回的条数限制,此参数可以重新设置 | 任何正整数 | Not Set(null) |
safeRowBoundsEnabled | 允许在嵌套语句中使用分页(RowBounds)。如果允许,设置false | true|false | false |
safeResultHandlerEnabled | 允许在嵌套语句中使用分页(ResultHandler)。如果允许,设置false | true|false | true |
mapUnderscoreTocAmelCase | 是否开启自动驼峰命名规则映射,即从经典数据库列名A_COLUMN到经典Java属性名aColumn的类似映射 | true|false | false |
localCacheScope | MyBatis利用本地缓存机制(Local Cache)防止循环引用(circular references)和加速重复嵌套查询。默认值为SESSION。这种情况下会缓存一个会话中执行的所有查询。若设置值为STATEMENT,本地会话仅用在语句执行上,对相同SqlSession的不同调用将不会共享数据 | SESSION|STATEMENT | SESSION |
jdbcTypeForNull | 当没有为参数提供特定的JDBC类型时,为空值指定JDBC类型。某些驱动需要指定列的JDBC类型,多数情况直接用一般类型即可,比如NULL,VARCHAR或OTHER | NULL、VARCHAR、OTHER | OTHER |
lazyLoadTriggerMethods | 指定哪个对象的方法除法一次延迟加载 | —— | equals、clone、hashCode、toString |
defaultScriptingLanuage | 指定动态SQL生成的默认语言 | —— | org.apache.ibatis.scripting. xmltags.XMLDynamicLanguageDriver |
callSettersOnNulls | 指定当结果集中值为null时,是否调用映射对象的setter(map对象时为put)方法,这对于Map.keysSet()依赖或null值初始化时是有用的。注意,基本类型(int、boolean等)不能设置成null | true|false | false |
logPrefix | 指定MyBatis增加到日志名称的前缀 | 任何字符串 | Not set |
logImpl | 指定MyBatis所用日志的具体实现,未指定时将自动查找 | SLF4J|LOG4J|LOG4J2| JDK_LOGGING|COMMONS_LOGGING| STDOUT_LOGGING|NO_LOGGING |
Not set |
proxyFactory | 指定MyBatis创建具有延迟加载能力的对象所用到的代理工具 | CGLIB|JAVASSIST | JAVASSIST (MyBatis版本为3.3及以上的) |
vfsImpl | 指定VFS的实现类 | 提供VFS类的全限定名,如果存在多个,可以使用逗号分隔 | Not set |
useActualParamName | 允许用方法参数中声明的实际名称引用参数。要使用此功能,项目必须被编译为Java8参数的选择。(从版本3.4.1开始可以使用) | true|false | true |
1 | <settings> |
typeAliases别名
由于类的全限定名称很长,需要大量使用的时候,总写那么长的名称不方便。
系统定义别名
在MyBatis的初始化过程中,系统自动初始化了一些别名。
1 | // |
Configuration配置的别名。
1 | //事务方式别名 |
自定义别名
1 | <typeAliases> |
如果有很多类需要定义别名,那么可以用以下方式进行配置,比如”com.louris.mybatis.pojo”包中的类,将其中第一个字母变为小写作为其别名,比如类Role的别名会编程role,User的别名为user。
1 | <typeAliases> |
上述这种可能出现重名,需要注解@Alias()进行区分。
1 |
|
typeHandler类型转换器
在JDBC中,需要在PreparedStatement对象中设置那些已经预编译过的SQL语句的参数。执行SQL后,会通过ResultSet对象获取得到数据库的数据,而这些MyBatis是根据数据的类型通过typeHandler来实现的。在typeHandler中,分为jdbcType和javaType,其中jdbcType用于定义数据库类型,而javaType用于定义Java类型,那么typeHandler的作用就是承担jdbcType和javaType之间的相互转换。多数情况下爱不需要我们配置typeHandler、jdbcType、javaType,因为MyBatis会探测应该使用什么类型的typeHandler进行处理,但是有些场景无法探测到。对于那些需要使用自定义枚举的场景,或者数据库使用特殊数据类型的场景,可以使用自定义的typeHandler去处理类型之间的转换问题。
系统定义的typeHandler
- BooleanTypeHandler
- ByteTypeHandler
- IntegerTypeHanlder
…
这些都是MyBatis系统已经创建好的typeHandler,大部的情况下无须显式地声明jdbcType和javaType,或者用typeHandler去指定typeHandler来实现数据类型转换,因为MyBatis系统会自己探测。
在MyBatis中typeHandler都要实现接口org.apache.ibatis.type.TypeHandler:
1 | public interface TypeHandler<T>{ |
而各类typeHandler都是集成org.apache.ibatis.BaseTypeHandler
1 | // |
通过BaseTypeHandler,MyBatis把javaType和jdbcType相互转换,并通过org.apache.ibatis.type.TypeHandlerRegistry类对象的register方法进行注册。
1 | public TypeHandlerRegistry(Configuration configuration) { |
自定义typeHandler
MyTypeHandler
1 | package mybatis; |
RoleMapper.xml
1 |
|
RoleMapper
1 | package mybatis; |
SqlSessionFactoryUtils
1 | package mybatis; |
运行结果
1 | "C:\Program Files\Java\jdk-14.0.2\bin\java.exe" "-javaagent:D:\IntelliJ IDEA 2020.1.1\lib\idea_rt.jar=60353:D:\IntelliJ IDEA 2020.1.1\bin" -Dfile.encoding=UTF-8 -classpath D:\JavaTest\out\production\JavaTest;D:\download\cglib-nodep-3.3.0.jar;D:\download\fastjson-1.2.76.jar;D:\download\commons-lang3-3.12.0.jar;D:\download\mybatis-3.5.7\mybatis-3.5.7.jar;D:\download\mysql-connector-java-8.0.19.jar;D:\download\mybatis-3.5.7\lib\asm-7.1.jar;D:\download\mybatis-3.5.7\lib\cglib-3.3.0.jar;D:\download\mybatis-3.5.7\lib\ognl-3.2.20.jar;D:\download\mybatis-3.5.7\lib\log4j-1.2.17.jar;D:\download\mybatis-3.5.7\lib\log4j-api-2.14.1.jar;D:\download\mybatis-3.5.7\lib\slf4j-api-1.7.30.jar;D:\download\mybatis-3.5.7\lib\log4j-core-2.14.1.jar;D:\download\mybatis-3.5.7\lib\commons-logging-1.2.jar;D:\download\mybatis-3.5.7\lib\javassist-3.27.0-GA.jar;D:\download\mybatis-3.5.7\lib\slf4j-log4j12-1.7.30.jar mybatis.SqlSessionFactoryUtils |
枚举typeHandler
在绝多大数情况下,typeHandler因为枚举而使用,MyBatis已经定义了两个类作为枚举类型的支持:
- EnumOrdinalTypeHandler
- EnumTypeHandler
数据库预先插入一条数据:
1 | insert t_user values(1, 'zhangsan', '123456', '1', '13699988874', '0755-88888888', 'zhangsan@163.com', 'note...'); |
UserMapper.xml
EnumOrdinalTypeHandler方式
EnumOrdinalTypeHandler是按MyBatis根据枚举数组下标索引的方式进行匹配的,也是枚举类型的默认转换类,它要求数据库返回一个整数作为其下标,它会根据下标找到对应的枚举类型。
详见运行结果①中,sex为1返回的是FEMAL的(0,‘女’)
1 |
|
EnumTypeHandler方式
EnumTypeHandler会把使用的名称转化为对应的枚举,比如它会根据数据库返回的字符串“MALE”,进行Enum.valueOf(SexEnum.class, “MALE”);转换;将对应sex字段改成”MALE”进行测试,见运行结果②;
1 |
|
SexEnum
1 | package mybatis; |
User
1 | package mybatis; |
UserMapper
1 | package mybatis; |
mybatis-config.xml
1 |
|
SqlSessionFactoryUtils
1 | package mybatis; |
运行结果
结果①
1 | "C:\Program Files\Java\jdk-14.0.2\bin\java.exe" "-javaagent:D:\IntelliJ IDEA 2020.1.1\lib\idea_rt.jar=59199:D:\IntelliJ IDEA 2020.1.1\bin" -Dfile.encoding=UTF-8 -classpath D:\JavaTest\out\production\JavaTest;D:\download\cglib-nodep-3.3.0.jar;D:\download\fastjson-1.2.76.jar;D:\download\commons-lang3-3.12.0.jar;D:\download\mybatis-3.5.7\mybatis-3.5.7.jar;D:\download\mysql-connector-java-8.0.19.jar;D:\download\mybatis-3.5.7\lib\asm-7.1.jar;D:\download\mybatis-3.5.7\lib\cglib-3.3.0.jar;D:\download\mybatis-3.5.7\lib\ognl-3.2.20.jar;D:\download\mybatis-3.5.7\lib\log4j-1.2.17.jar;D:\download\mybatis-3.5.7\lib\log4j-api-2.14.1.jar;D:\download\mybatis-3.5.7\lib\slf4j-api-1.7.30.jar;D:\download\mybatis-3.5.7\lib\log4j-core-2.14.1.jar;D:\download\mybatis-3.5.7\lib\commons-logging-1.2.jar;D:\download\mybatis-3.5.7\lib\javassist-3.27.0-GA.jar;D:\download\mybatis-3.5.7\lib\slf4j-log4j12-1.7.30.jar mybatis.SqlSessionFactoryUtils |
结果②
1 | "C:\Program Files\Java\jdk-14.0.2\bin\java.exe" "-javaagent:D:\IntelliJ IDEA 2020.1.1\lib\idea_rt.jar=63365:D:\IntelliJ IDEA 2020.1.1\bin" -Dfile.encoding=UTF-8 -classpath D:\JavaTest\out\production\JavaTest;D:\download\cglib-nodep-3.3.0.jar;D:\download\fastjson-1.2.76.jar;D:\download\commons-lang3-3.12.0.jar;D:\download\mybatis-3.5.7\mybatis-3.5.7.jar;D:\download\mysql-connector-java-8.0.19.jar;D:\download\mybatis-3.5.7\lib\asm-7.1.jar;D:\download\mybatis-3.5.7\lib\cglib-3.3.0.jar;D:\download\mybatis-3.5.7\lib\ognl-3.2.20.jar;D:\download\mybatis-3.5.7\lib\log4j-1.2.17.jar;D:\download\mybatis-3.5.7\lib\log4j-api-2.14.1.jar;D:\download\mybatis-3.5.7\lib\slf4j-api-1.7.30.jar;D:\download\mybatis-3.5.7\lib\log4j-core-2.14.1.jar;D:\download\mybatis-3.5.7\lib\commons-logging-1.2.jar;D:\download\mybatis-3.5.7\lib\javassist-3.27.0-GA.jar;D:\download\mybatis-3.5.7\lib\slf4j-log4j12-1.7.30.jar mybatis.SqlSessionFactoryUtils |
自定义枚举typeHandler
MyBatis内部提供的两种转换的typeHandler有很大局限,更多时候需要我们使用自定义的typeHandler
1 | update t_user set sex='1' where sex = 'MALE'; |
1 | package mybatis; |
这里涉及到两个注解,如下:
- @MappedTypes:指定与其关联的 Java类型列表。如果在javaType 属性中也同时指定,则注解上的配置将被忽略。
- @MappedJdbcTypes:指定与其关联的JDBC 类型列表。如果在jdbcType 属性中也同时指定,则注解上的配置将被忽略。
修改UserMapper中的typeHandler,执行结果如下:
1 | "C:\Program Files\Java\jdk-14.0.2\bin\java.exe" "-javaagent:D:\IntelliJ IDEA 2020.1.1\lib\idea_rt.jar=51458:D:\IntelliJ IDEA 2020.1.1\bin" -Dfile.encoding=UTF-8 -classpath D:\JavaTest\out\production\JavaTest;D:\download\cglib-nodep-3.3.0.jar;D:\download\fastjson-1.2.76.jar;D:\download\commons-lang3-3.12.0.jar;D:\download\mybatis-3.5.7\mybatis-3.5.7.jar;D:\download\mysql-connector-java-8.0.19.jar;D:\download\mybatis-3.5.7\lib\asm-7.1.jar;D:\download\mybatis-3.5.7\lib\cglib-3.3.0.jar;D:\download\mybatis-3.5.7\lib\ognl-3.2.20.jar;D:\download\mybatis-3.5.7\lib\log4j-1.2.17.jar;D:\download\mybatis-3.5.7\lib\log4j-api-2.14.1.jar;D:\download\mybatis-3.5.7\lib\slf4j-api-1.7.30.jar;D:\download\mybatis-3.5.7\lib\log4j-core-2.14.1.jar;D:\download\mybatis-3.5.7\lib\commons-logging-1.2.jar;D:\download\mybatis-3.5.7\lib\javassist-3.27.0-GA.jar;D:\download\mybatis-3.5.7\lib\slf4j-log4j12-1.7.30.jar mybatis.SqlSessionFactoryUtils |
文件操作 (不太常用)
使用Blob字段,用于存入文件
1 | create table t_file( |
1 | public class TestFile{ |
1 |
|
ObjectFactory(对象工厂)
当创建结果集时,MyBatis会使用一个对象工厂来完成创建这个结果集实例。在默认的情况下,MyBatis会使用其定义的对象工厂——DefaultObjectFactory(org.apache.ibatis.reflection.factory.DefaultObjectFactory)来完成对应的工作。
MyBatis允许注册自定义的ObjectFactory,需要实现接口org.apache.ibatis.reflection.factory.ObjectFactory,并给予配置。一般会考虑集成系统已经实现好的DefaultObjectFactory,通过一定的改写来完成我们所需要的工作。
MyObjectFactory
1 | package mybatis; |
mybatis-config.xml
1 | <objectFactory type="mybatis.MyObjectFactory"> |
运行结果
1 | "C:\Program Files\Java\jdk-14.0.2\bin\java.exe" "-javaagent:D:\IntelliJ IDEA 2020.1.1\lib\idea_rt.jar=55502:D:\IntelliJ IDEA 2020.1.1\bin" -Dfile.encoding=UTF-8 -classpath D:\JavaTest\out\production\JavaTest;D:\download\cglib-nodep-3.3.0.jar;D:\download\fastjson-1.2.76.jar;D:\download\commons-lang3-3.12.0.jar;D:\download\mybatis-3.5.7\mybatis-3.5.7.jar;D:\download\mysql-connector-java-8.0.19.jar;D:\download\mybatis-3.5.7\lib\asm-7.1.jar;D:\download\mybatis-3.5.7\lib\cglib-3.3.0.jar;D:\download\mybatis-3.5.7\lib\ognl-3.2.20.jar;D:\download\mybatis-3.5.7\lib\log4j-1.2.17.jar;D:\download\mybatis-3.5.7\lib\log4j-api-2.14.1.jar;D:\download\mybatis-3.5.7\lib\slf4j-api-1.7.30.jar;D:\download\mybatis-3.5.7\lib\log4j-core-2.14.1.jar;D:\download\mybatis-3.5.7\lib\commons-logging-1.2.jar;D:\download\mybatis-3.5.7\lib\javassist-3.27.0-GA.jar;D:\download\mybatis-3.5.7\lib\slf4j-log4j12-1.7.30.jar mybatis.SqlSessionFactoryUtils |
插件
插件是MyBatis中最强大和灵活的组件,同时也是最复杂、最难以使用的组件,而且其十分危险,因为它将覆盖MyBatis底层对象的核心方法和属性。如果操作不对那个将产生严重后果,甚至是摧毁MyBatis框架。待跟进。
enviroments(运行环境)
在MyBatis中,运行环境主要的作用是配置数据库信息,它可以配置多个数据库,一般而言只需要配置其中的一个就可以了。其下面又分为两个可配置的元素:
- 事务管理器(transactionManager)
- 数据源(dataSource)
1 | <environment id="development"> |
transactionManager(事务管理器)
简介
在MyBatis中,transactionManger提供了两个实现类,它需要实现接口Transaction(org.apache.ibatis.transaction.Transaction)
1 | // |
其主要工作就是提交、回滚和关闭数据库的事务
MyBatis为Transaction提供了两个实现类:
- JdbcTransaction
- ManagedTransaction
1 | <transactionManager type="JDBC"/> |
- JDBC使用JdbcTransactionFactory生成的JdbcTransaction对象实现。它是以JDBC的方式对数据库的提交和回滚进行操作;
- MANAGED使用ManagedTransactionFactory生成的ManagedTransaction对象实现。它的提交和回滚方法不用任何操作,而是把事务交给容器处理。在默认情况下,它会关闭连接,然而一些容器并不希望这样,因此需要closeConnection属性设置为false来组织它默认的关闭行为。
自定义事务工厂
可以通过自定义事务规则,满足特殊的需求。
1 | <transactionManager type="mybatis.MyTransactionFactory"/> |
1 | package mybatis; |
environment数据源环境
environment的主要作用是配置数据库,数据库通过三个工厂类来提供:
- PooledDataSourceFactory产生PooledDataSource类对象;
- UnpooledDataSourceFactory产生UnpooledDataSource类对象;
- JndiDataSourceFactory则会根据JNDI的信息拿到外部容器实现的数据库连接对象。
无论如何这三个工厂类,最后生成的产品都会是一个实现了DataSource接口的数据库连接对象。
1 | <dataSource type="POOLED"> |
UNPOOLED
采用非数据库池的管理方式,每次请求都会打开一个新的数据库连接,所以创建会比较慢。
可配置的属性:
- driver数据库驱动名,比如MySQL的com.mysql.jc.jdbc.Driver
- url连接数据库的URL
- username用户名
- password密码
- defaultTransactionIsolationLevel默认的连接事务隔离级别
POOLED
利用池的概念将JDBC的Connection对象组织起来,它开始会有一些空置,并且已经连接好的数据库连接,所以请求时,无须再建立和验证,省去了创建新的连接实例时所必需的初始化和认证时间。它还控制最大连接数,避免过多的连接导致系统瓶颈。
除了UNPOOLED下的属性外,会有更多属性用来配置POOLED的数据源:
- poolMaximumActiveConnections是在任意时间都存在的活动(也就是正在使用)连接数量,默认值为0;
- poolMaximumIdleConnections是任意时间可能存在的空闲连接数;
- poolMaximumCheckoutTime在被强制返回之前,池中连接被检出(Checked out)的时间,默认值为20000毫秒;
- poolTimeToWait是一个底层设置,如果获取连接花费相当长的时间,它会给连接池打印状态日志,并重新尝试获取一个连接(避免在误配置的情况下一直失败),默认值为20000毫秒;
- poolPingQuery为发送到数据库的侦测查询,用来检验连接是否处在正常工作秩序中,并准备接受请求。默认是“NO PING QUERY SET”,这会导致多数数据库驱动失败时带有一个恰当的错误消息。
- poolPingEnabled为是否启用侦测查询。若开启,也必须使用一个可执行的SQL语句设置poolPingQuery属性(最好是一个非常快的SQL),默认值为false;
- poolPingConnectionsNotUsedFor为配置poolPingQuery的使用额度。这可以被设置成匹配具体的数据库连接超时时间,来避免不必要的侦测,默认值为0(即所有连接每一时刻都被侦测——仅当poolPingEnabled为true时使用)。
JNDI
数据源JNDI的实现为了能在如EJB或应用服务器这类容器中使用,容器可以集中或在外部配置数据源,然后放置一个JNDI上下文的引用。这种数据源配置只需要两个属性:
- initial_context用来在InitialContext中寻找上下文(即,initialContext.lookup(initial_context))。initial_context是个可选属性,如果忽略,那么data_source属性将会直接从InitialContext中寻找;
- data_source是引用数据源实例位置上下文的路径。当提供initial_context配置时,data_source会在其返回的上下文中进行查找;当没有提供Initial_context时,data_source直接在InitialContext中查找。
第三方数据源
MyBatis也支持第三方数据源,例如使用DBCP数据源,那么需要提供一个自定义的DataSourceFactory。
1 | package mybatis; |
配置
1 | <dataSource type="mybatis.DbcpDataSourceFactory"> |
databaseIdProvider数据库厂商标识
databaseIdProvider元素主要是支持多种不同厂商的数据库,虽然这个元素并不常用,但是在一些公司却十分有用,因为有些软件公司需要给不同的客户提供系统,使用何种数据库往往是由客户决定的。例如,软件公司默认的是MySQL数据库,而客户只打算使用Oracle。
使用系统默认的databaseIdProvider
1 | <databaseIdProvider type="DB_VENDOR"> |
property元素属性:
- name是数据库名称,如果不确定如何填写,那么可以使用JDBC创建其数据库连接对象Connection,然后通过代码connection.getMetaData().getDatabase.ProductName()获取。
- value是数据库的别名,在MyBatis里可以通过这个别名标识一条SQL适用于哪种数据库运行。然后,改造映射器的SQL
配置文件中设置databaseId进行设置:
1 | <select id="getRole" parameterType="long" resultType="role" databaseId="oracle"> |
如果同时存在一条有databaseId和没有databaseId的标识会怎么样呢?
正常运行
1 | <select id="getRole" parameterType="long" resultType="role" databaseId="oracle"> |
运行异常
1 | <select id="getRole" parameterType="long" resultType="role" databaseId="oracle"> |
- 使用多数据库SQL时需要配置databaseIdProvidertype的属性。
- 当databaseIdProvidertype属性被配置时,系统会优先取到和数据库配置一致的SQL。
- 如果没有,则取没有databaseId的SQL,可以把它当做默认值。
- 如果还是取不到,则会抛出异常,说明无法匹配到对应的SQL。
不使用系统规则
1 | package mybatis; |
1 | <databaseIdProvider type="mybatis.MyDatabaseIdProvider"> |
引入映射器的方法
映射器是MyBatis最复杂、最核心的组件。
Mapper接口
1 | package mybatis; |
Mapper.xml
1 |
|
引入方法
1 | <mappers> |
1 | <mappers> |
1 | <mappers> |
1 | <mappers> |
映射器
映射器是MyBatis最复杂且最终啊哟的组件,由一个接口加上XML文件组成
select元素
简介
元素 | 说明 | 备注 |
---|---|---|
id | 它和Mapper接口的命名空间组合起来是唯一的,供MyBatis调用 | 如果命名空间和id结合起来不唯一,MyBatis将会抛出异常 |
parameterType | 可以给出类的全命名,也可以给出别名,但是别名必须是MyBatis内部定义或者自定义的 | 可以选择Java Bean、Map等简单的参数类型传递给SQL |
resultType | 定义类的全路径,在允许自动匹配的情况下,结果集将通过Java Bean的规范映射; 或定义为int/double/float/map等参数; 也可以使用别名,不能和resultMap同时使用 |
常用的参数之一 |
resultMap | 映射集的英勇,将执行强大的映射功能。我们可以使用resultType和resultMap其中一个,resultMap能提供自定义映射规则的机会。 | MyBatis最复杂的元素,可以配置映射规则、级联、typeHandler等 |
flushCache | 它的作用是在调用SQL后,是否要求MyBatis清空之前查询本地缓存和二级缓存 | 取值为布尔值。true|false。默认值为false |
useCache | 启动二级缓存开关,是否要求MyBatis将此次结果缓存 | true|false,默认true |
timeout | 设置超时参数,超时将抛出异常,单位秒 | |
fetchSize | 获取记录的总条数设定 | |
statementType | 告诉MyBatis使用哪个JDBC的Statement工作,取值为STATEMENT(Statement)、PREPARED(PreparedStatement)、CALLABLE(CallableStatement) | 默认PREPARED |
resultSetType | 对JDBC的resultSet接口而言,它的值包括FORWARD_ONLY(游标允许向前访问)、SCROLL_SENSITIVE(双向滚动、但不及时更新,就是如果数据库里的数据修改过,并不在resultSet中反映出来)、SCROLL_INSENSITIVE(双向滚动,并及时跟踪数据库的更新,以便更新resultSet中的数据) | 默认是数据库厂商提供的JDBC驱动所设置的 |
databaseId | 数据库厂商标识 | 提供多种数据库支持 |
resultOrdered | 仅适用于嵌套结果select语句。如果为true,就是假设包含了嵌套结果集或是分组了,当返回一个主结果行时,就不能应用前面结果集了。这就确保了在获取嵌套的结果集时不至于导致内存不够用 | true|false, 默认false |
resultSets | 适合于多个结果集的情况,它将列出执行SQL后每个结果集的名称,每个名称之间用逗号分隔 | 很少使用 |
自动映射和驼峰映射
MyBatis提供了自动映射功能,在默认情况下自动映射功能是开启的,使用它的好处在于能有效减少大量的映射配置,从而减少工作量。
在setting元素中有两个可以配置的选项autoMappingBehavior和mapUpderscoreToCamelCase,控制自动映射和驼峰映射的开关。
一般而言,自动映射会使用得多一些,而驼峰映射要求比较严苛,所以字啊实际中应用不算太广。
1 | public class Role{ |
1 | <select id="getRole" parameterType="long" resultType="mybatis.Role"> |
role_name被别名roleName代替,和POJO的属性名称保持一致,MyBatis将其映射到POJO的属性roleName上,自动完成映射。
如果不用as roleName,则按照驼峰命名规则进行了匹配,如果驼峰映射开启的话。
传递多个参数
不带注解传递
1 | package mybatis; |
1 | <select id="findRolesByMap" parameterType="map" resultType="role"> |
1 | Map<String, Object> parameterMap = new HashMap<>(); |
运行结果
1 | "C:\Program Files\Java\jdk-14.0.2\bin\java.exe" "-javaagent:D:\IntelliJ IDEA 2020.1.1\lib\idea_rt.jar=49861:D:\IntelliJ IDEA 2020.1.1\bin" -Dfile.encoding=UTF-8 -classpath D:\JavaTest\out\production\JavaTest;D:\download\cglib-nodep-3.3.0.jar;D:\download\fastjson-1.2.76.jar;D:\download\commons-lang3-3.12.0.jar;D:\download\mybatis-3.5.7\mybatis-3.5.7.jar;D:\download\mysql-connector-java-8.0.19.jar;D:\download\mybatis-3.5.7\lib\asm-7.1.jar;D:\download\mybatis-3.5.7\lib\cglib-3.3.0.jar;D:\download\mybatis-3.5.7\lib\ognl-3.2.20.jar;D:\download\mybatis-3.5.7\lib\log4j-1.2.17.jar;D:\download\mybatis-3.5.7\lib\log4j-api-2.14.1.jar;D:\download\mybatis-3.5.7\lib\slf4j-api-1.7.30.jar;D:\download\mybatis-3.5.7\lib\log4j-core-2.14.1.jar;D:\download\mybatis-3.5.7\lib\commons-logging-1.2.jar;D:\download\mybatis-3.5.7\lib\javassist-3.27.0-GA.jar;D:\download\mybatis-3.5.7\lib\slf4j-log4j12-1.7.30.jar;D:\download\commons-dbcp2-2.9.0\commons-dbcp2-2.9.0.jar mybatis.SqlSessionFactoryUtils |
带注解传递
1 | package mybatis; |
1 | List<Role> roles = roleMapper.findRolesByAnnotation("John", "Hello World!"); |
1 | <select id="findRolesByAnnotation" resultType="role"> |
运行结果
1 | "C:\Program Files\Java\jdk-14.0.2\bin\java.exe" "-javaagent:D:\IntelliJ IDEA 2020.1.1\lib\idea_rt.jar=64453:D:\IntelliJ IDEA 2020.1.1\bin" -Dfile.encoding=UTF-8 -classpath D:\JavaTest\out\production\JavaTest;D:\download\cglib-nodep-3.3.0.jar;D:\download\fastjson-1.2.76.jar;D:\download\commons-lang3-3.12.0.jar;D:\download\mybatis-3.5.7\mybatis-3.5.7.jar;D:\download\mysql-connector-java-8.0.19.jar;D:\download\mybatis-3.5.7\lib\asm-7.1.jar;D:\download\mybatis-3.5.7\lib\cglib-3.3.0.jar;D:\download\mybatis-3.5.7\lib\ognl-3.2.20.jar;D:\download\mybatis-3.5.7\lib\log4j-1.2.17.jar;D:\download\mybatis-3.5.7\lib\log4j-api-2.14.1.jar;D:\download\mybatis-3.5.7\lib\slf4j-api-1.7.30.jar;D:\download\mybatis-3.5.7\lib\log4j-core-2.14.1.jar;D:\download\mybatis-3.5.7\lib\commons-logging-1.2.jar;D:\download\mybatis-3.5.7\lib\javassist-3.27.0-GA.jar;D:\download\mybatis-3.5.7\lib\slf4j-log4j12-1.7.30.jar;D:\download\commons-dbcp2-2.9.0\commons-dbcp2-2.9.0.jar mybatis.SqlSessionFactoryUtils |
Java Bean传递
注解的方式在参数数量较多的时候,使用起来就比较麻烦,为此,MyBatis还提供传递Java Bean的形式;
1 | package mybatis; |
1 | <select id="findRolesByBean" parameterType="mybatis.RoleParams" resultType="role"> |
1 | RoleParams roleParams = new RoleParams(); |
混合传递方式
1 | public interface RoleMapper { |
1 | RoleParams roleParams = new RoleParams(); |
1 | <select id="findByMix" resultType="role"> |
运行结果
1 | "C:\Program Files\Java\jdk-14.0.2\bin\java.exe" "-javaagent:D:\IntelliJ IDEA 2020.1.1\lib\idea_rt.jar=56202:D:\IntelliJ IDEA 2020.1.1\bin" -Dfile.encoding=UTF-8 -classpath D:\JavaTest\out\production\JavaTest;D:\download\cglib-nodep-3.3.0.jar;D:\download\fastjson-1.2.76.jar;D:\download\commons-lang3-3.12.0.jar;D:\download\mybatis-3.5.7\mybatis-3.5.7.jar;D:\download\mysql-connector-java-8.0.19.jar;D:\download\mybatis-3.5.7\lib\asm-7.1.jar;D:\download\mybatis-3.5.7\lib\cglib-3.3.0.jar;D:\download\mybatis-3.5.7\lib\ognl-3.2.20.jar;D:\download\mybatis-3.5.7\lib\log4j-1.2.17.jar;D:\download\mybatis-3.5.7\lib\log4j-api-2.14.1.jar;D:\download\mybatis-3.5.7\lib\slf4j-api-1.7.30.jar;D:\download\mybatis-3.5.7\lib\log4j-core-2.14.1.jar;D:\download\mybatis-3.5.7\lib\commons-logging-1.2.jar;D:\download\mybatis-3.5.7\lib\javassist-3.27.0-GA.jar;D:\download\mybatis-3.5.7\lib\slf4j-log4j12-1.7.30.jar;D:\download\commons-dbcp2-2.9.0\commons-dbcp2-2.9.0.jar mybatis.SqlSessionFactoryUtils |
总结
- 使用map传递参数导致了业务可读性的丧失,导致后续扩展和维护的困难,在实际的应用中要果断废弃这种方式;
- 使用@Param注解传递多个参数,受到参数个数(n)的影响。当n $\leq$ 5 时,这是最佳的传参方式,它比用Java Bean更好,因为它更加直观;当 n $\ge$ 5 时,多个参数讲给调用带来困难,此时不推荐使用它。
- 当参数个数多于5个时,建议使用JavaBean方式。
- 对于使用混合参数的,姚明全参数的合理性。
使用resultMap映射结果集
自动映射和驼峰映射规则比较简单,无法定义多的属性,比如typeHandler、级联等。为了支持复杂的映射,select元素提供了resultMap属性。
- resultMap元素定义了一个roleMap,它的属性id代表它的标识,type代表使用哪个类作为其映射的类,可以使别名或者全限定名,role是mybatis.Role的别名
- 它的子元素id代表resultMap的主键,而result代表其属性,id和result元素的属性property代表POJO的属性名称,而column代表SQL的列名。把POJO的属性和SQL的列名做对应,例如POJO的属性roleName,就用SQL的列名role_name建立映射关系。
- 在select元素中的属性resultMap指定了采用哪个resultMap作为其映射规则。
1 |
|
分页参数RowBounds
MyBatis不仅支持分页,它还内置了一个专门处理分页的类——RowBounds。源码如下:
1 | // |
mapper.xml映射器文件中没有任何关于RowBounds参数的信息,它是MyBatis的一个附加参数,MyBatis会自动识别它,据此进行分页。
1 | <select id="findByRowBounds" resultType="role"> |
1 | RoleParams roleParams = new RoleParams(); |
运行结果
1 | "C:\Program Files\Java\jdk-14.0.2\bin\java.exe" "-javaagent:D:\IntelliJ IDEA 2020.1.1\lib\idea_rt.jar=59806:D:\IntelliJ IDEA 2020.1.1\bin" -Dfile.encoding=UTF-8 -classpath D:\JavaTest\out\production\JavaTest;D:\download\cglib-nodep-3.3.0.jar;D:\download\fastjson-1.2.76.jar;D:\download\commons-lang3-3.12.0.jar;D:\download\mybatis-3.5.7\mybatis-3.5.7.jar;D:\download\mysql-connector-java-8.0.19.jar;D:\download\mybatis-3.5.7\lib\asm-7.1.jar;D:\download\mybatis-3.5.7\lib\cglib-3.3.0.jar;D:\download\mybatis-3.5.7\lib\ognl-3.2.20.jar;D:\download\mybatis-3.5.7\lib\log4j-1.2.17.jar;D:\download\mybatis-3.5.7\lib\log4j-api-2.14.1.jar;D:\download\mybatis-3.5.7\lib\slf4j-api-1.7.30.jar;D:\download\mybatis-3.5.7\lib\log4j-core-2.14.1.jar;D:\download\mybatis-3.5.7\lib\commons-logging-1.2.jar;D:\download\mybatis-3.5.7\lib\javassist-3.27.0-GA.jar;D:\download\mybatis-3.5.7\lib\slf4j-log4j12-1.7.30.jar;D:\download\commons-dbcp2-2.9.0\commons-dbcp2-2.9.0.jar mybatis.SqlSessionFactoryUtils |
insert元素
简介
属性 | 描述 | 备注 |
---|---|---|
id | SQL编号,用于标识这条SQL | 命名空间+id+databaseId唯一,否则MyBatis会抛出异常 |
parameterType | 同select | |
flushCache | 同select | |
timeout | ||
statementType | ||
useGeneratedKyes | 是否启动JDBC的getGeneratedKeys方法来去除由数据库内部生成的主键 | 默认false |
keyProperty | (仅对insert和update有用)唯一标记一个属性,MyBatis会通过getGeneratedKeys的返回值,或者通过isnert语句的selectKey子元素设置它的剪枝。如果是符合主键,要把每一个名称用逗号隔开 | 默认值为unset, 不能和keyColumn连用 |
keyColumn | (仅对insert和update有用)通过生成的键值设置表中的列名,这个设置仅在某些数据库(像PostgreSQL)中是必须的,当主键列不是表中的第一列时需要设置。如果是符合主键,需要把一个名称用逗号隔开 | 不能和keyProperty连用 |
databaseId |
简单insert语句应用
1 | <insert id="insertRole" parameterType="role"> |
主键回填
前一个插入应用中没有插入id列,因为MySQL中的表格才用了自增主键。但有时候还可能需要继续使用这个主键。
JDBC中的Statement对象在执行插入的SQL后,可以通过getGeneratedKeys方法获得数据库生成的主键(需要数据库驱动支持),这样便能达到获取主键的功能。
1 | <insert id="insertRole" parameterType="role" useGeneratedKeys="true" keyProperty="id"> |
userGeneratedKeys代表采用JDBC的Statement对象的getGeneratedKeys方法返回主键,而keyProperty则代表将用哪个POJO的属性去匹配这个主键,这里是id,说明它会用数据库生成的主键去幅值给这个POJO,测试主键回填的结果。
1 | Role role = new Role(); |
运行结果
如果如同前面简单的插入语句,下面的role.getId()为空,而不是这里的role id: 10
1 | "C:\Program Files\Java\jdk-14.0.2\bin\java.exe" "-javaagent:D:\IntelliJ IDEA 2020.1.1\lib\idea_rt.jar=50726:D:\IntelliJ IDEA 2020.1.1\bin" -Dfile.encoding=UTF-8 -classpath D:\JavaTest\out\production\JavaTest;D:\download\cglib-nodep-3.3.0.jar;D:\download\fastjson-1.2.76.jar;D:\download\commons-lang3-3.12.0.jar;D:\download\mybatis-3.5.7\mybatis-3.5.7.jar;D:\download\mysql-connector-java-8.0.19.jar;D:\download\mybatis-3.5.7\lib\asm-7.1.jar;D:\download\mybatis-3.5.7\lib\cglib-3.3.0.jar;D:\download\mybatis-3.5.7\lib\ognl-3.2.20.jar;D:\download\mybatis-3.5.7\lib\log4j-1.2.17.jar;D:\download\mybatis-3.5.7\lib\log4j-api-2.14.1.jar;D:\download\mybatis-3.5.7\lib\slf4j-api-1.7.30.jar;D:\download\mybatis-3.5.7\lib\log4j-core-2.14.1.jar;D:\download\mybatis-3.5.7\lib\commons-logging-1.2.jar;D:\download\mybatis-3.5.7\lib\javassist-3.27.0-GA.jar;D:\download\mybatis-3.5.7\lib\slf4j-log4j12-1.7.30.jar;D:\download\commons-dbcp2-2.9.0\commons-dbcp2-2.9.0.jar mybatis.SqlSessionFactoryUtils |
自定义主键
有时候主键可能依赖于某些规则,比如取消主键id的递增规则,而将其修改为:
- 当角色表记录为空时,id设置为1;
- 当角色表记录不为空时,id设置为当前id加3;
1 | <insert id="insertRole" parameterType="role"> |
- keyProperty指定采用哪个属性作为POJO的主键
- resutType告诉MyBatis将返回一个long型的结果集
- order设置为BEFORE,说明它将于当前定义的SQL前执行。
update元素和delete元素
update元素和delete元素比较简单,其属性和insert差不多,执行完也会返回一个整数,用以标志改SQL语句影响了数据库的记录行数;
1 | <delete id="deleteRole" parameterType="long"> |
sql元素
- sql元素的作用在于可以定义一条SQL的一部分,方便后面的SQL应用它。
- 通常情况下要在select、insert等语句中反复编写它们,特别是那些字段较多的表更是如此。
1 |
|
sql元素还支持变量传递
1 | <sql id="roleCols"> |
参数
概述
一些数据库字段返回为null,而MyBatis系统又检测不到使用何种jdbcType进行处理时,会发生异常的情况,这个时候执行对应的typeHandler进行处理,MyBatis就指导采取哪个typeHandler进行处理了。
1 | insert into t_role(id, role_name, note) values(#{id}, #{roleName, typeHandler=org.apache.ibatis.type.StringTypeHandler}, #{note}) |
而事实上,大部分情况下都不需要这样编写,因为MyBatis会根据javaType和jdbcType去检测使用哪个typeHandler。如果roleName是一个没有注册的类型,那么就会发生异常,因为MyBatis无法找到对应的typeHandler来转换数据类型。此时可以自定义TypeHandler,通过类似的办法指定,就不会抛出异常了。
在一些因为数据库返回null,存在可能抛出异常的情况下,也可以指定对应的jdbcType,从而让MyBatis能够探测到使用哪个typeHandler进行转换,以避免空指针异常。
1 | #{age, javeType=int, jdbcType=NUMERIC, typeHandler=MyTypeHandler} |
存储过程参数支持
在MyBatis中提供了对存储过程的良好支持,对于简单的输出参数(比如INT,VARCHAR,DECIMAL)可以使用POJO通过映射来完成。
1 | #{id, mode=IN} |
- IN:输入参数是外接需要传递给存储过程的;
- OUT:输出参数是存储过程经过处理后返回的;
- INOUT:输入输出参数一方面外界需要可以传递给它,另一方面在最后存储过程也会将它返回给调用者。
特殊字符串的替换和处理(#和$)
类似于前面参数传递($)和普通参数(#)的区别。
resultMap元素
resultMap元素构成
1 | <resultMap id="" type=""> |
其中constructor元素用于配置构造方法。
一个POJO可能不存在没有参数的构造方法,可以使用constructor进行配置。
假设角色类RoleBean不存在没有参数的构造方法,那么需要配置结果集:
1 | public RoleBean(Integer id, String roleName); |
1 | <constructor> |
如此,MyBatis就会使用对应的构造方法来构造POJO了
id元素表示哪个列是主键,允许多个主键,多个主键则成为联合主键。
result配置POJO到SQL列名的映射关系。
result元素和idArg元素的属性,如表所示:
元素名称 | 说明 | 备注 |
---|---|---|
property | 映射到列结果的字段或属性。如果POJO的属性匹配的是存在的且与给定SQL列名(column元素)相同的,那么MyBatis就会映射到POJO上 | 可以使用导航式的字段,比如访问一个学生对象(Student)需要访问学生证(selfcard)的发送日期(issueDate),那么可以写成selfcard.issueDate |
column | 对应的是SQL的列 | |
javaType | 配置Java的类型 | 可以是特定的类完全限定名或者MyBatis上下文的别名 |
jdbcType | 配置数据库类型 | 这是一个JDBC的类型,MyBatis已经做了限定,支持大部分常用的数据库类型 |
typeHandler | 类型处理器 | 允许用特定的处理器来覆盖MyBatis默认的处理器。这就要指定jdbcType和javaType相互转化的规则 |
例子:
1 | <resultMap id="roleMapper" type="role"> |
使用map存储结果集(不推荐)
一般而言,任何select语句都可以使用map存储,使用map原则上是可以匹配所有结果集的,但是使用map接口意味着可读性的下降,因为使用map时需要进一步了解map键值的构成和数据类型,所以这不是一种推荐的方式,更多时候会推荐使用POJO方式
1 | <select id="findColorByNote" parameterType="string" resultType="map"> |
使用POJO存储结果集(推荐)
1 | <insert id="insertRole" parameterType="role"> |
使用resultMap(推荐)
1 | <resultMap id="roleMapper" type="role"> |
- resultMap属性id为这个resultMap的标识
- type代表需要映射的POJO,这里可以使用MyBatis定义好的类的别名,也可以使用自定义的类的全限定名
- 元素id标识这个对象的主键
- SQL语句的列名必须和roleMapper的column是一一对应的,使用XML配置的结果集,还可以配置typeHanlder、javaType、jdbcType等更多内容,但不能和resultType连用。
级联
级联是一个数据库实体的概念。级联不是必须的,级联的好处是获取关联数据十分便捷,但是级联过多会增加系统的复杂度,同时降低系统的性能,此增比减,所以当级联的层级超过3层时,就不要考虑使用级联了,因为这样会造成多个对象的关联,导致系统的耦合、复杂和难以维护。
此处略过,后面真得用得到在看来得及。
缓存
在MyBatis中允许使用缓存,缓存一般都放置在可高速读、写的存储器上,比如服务器的内存,它能够有效提高系统的性能。因为数据库在大部分场景下是把存储在磁盘上的数据索引出来。从硬件的角度分析,索引磁盘是一个较为缓慢的过程,读取内存或者高速缓存处理器的速度要比读取磁盘快得多,其速度是读取硬盘的几十倍到上百倍,但是内存和告诉缓存处理器的空间有限,所以一般只会把那些常用且命中率高的数据缓存起来,以便将来使用,而不缓存那些不常用且命中率低的数据缓存。
一级缓存和二级缓存
一级缓存是在SqlSession上的缓存,二级缓存是在SqlSessionFactory上的缓存。默认情况下,也就是没有任何配置的情况下,MyBatis系统会开启一级缓存,也就是对于SqlSession层面的缓存,这个缓存不需要POJO对象可序列化。
如果两次查询时完全相同的查询,且缓存没有超时就可以触发一级缓存:
- 传入的StatementId
- 查询时要求的结果集中的结果范围
- 这次查询所产生的最终要传递给JDBC java.sql.PreparedStatement的Sql语句字符串
- 传递给java.sqlStatement要设置的参数值
一级缓存触发
1 | public static void main(String[] args) { |
1 | "C:\Program Files\Java\jdk-14.0.2\bin\java.exe" "-javaagent:D:\IntelliJ IDEA 2020.1.1\lib\idea_rt.jar=65310:D:\IntelliJ IDEA 2020.1.1\bin" -Dfile.encoding=UTF-8 -classpath D:\JavaTest\out\production\JavaTest;D:\download\cglib-nodep-3.3.0.jar;D:\download\fastjson-1.2.76.jar;D:\download\commons-lang3-3.12.0.jar;D:\download\mybatis-3.5.7\mybatis-3.5.7.jar;D:\download\mysql-connector-java-8.0.19.jar;D:\download\mybatis-3.5.7\lib\asm-7.1.jar;D:\download\mybatis-3.5.7\lib\cglib-3.3.0.jar;D:\download\mybatis-3.5.7\lib\ognl-3.2.20.jar;D:\download\mybatis-3.5.7\lib\log4j-1.2.17.jar;D:\download\mybatis-3.5.7\lib\log4j-api-2.14.1.jar;D:\download\mybatis-3.5.7\lib\slf4j-api-1.7.30.jar;D:\download\mybatis-3.5.7\lib\log4j-core-2.14.1.jar;D:\download\mybatis-3.5.7\lib\commons-logging-1.2.jar;D:\download\mybatis-3.5.7\lib\javassist-3.27.0-GA.jar;D:\download\mybatis-3.5.7\lib\slf4j-log4j12-1.7.30.jar;D:\download\commons-dbcp2-2.9.0\commons-dbcp2-2.9.0.jar mybatis.SqlSessionFactoryUtils |
一级缓存未触发
1 | public static void main(String[] args) { |
1 | "C:\Program Files\Java\jdk-14.0.2\bin\java.exe" "-javaagent:D:\IntelliJ IDEA 2020.1.1\lib\idea_rt.jar=61431:D:\IntelliJ IDEA 2020.1.1\bin" -Dfile.encoding=UTF-8 -classpath D:\JavaTest\out\production\JavaTest;D:\download\cglib-nodep-3.3.0.jar;D:\download\fastjson-1.2.76.jar;D:\download\commons-lang3-3.12.0.jar;D:\download\mybatis-3.5.7\mybatis-3.5.7.jar;D:\download\mysql-connector-java-8.0.19.jar;D:\download\mybatis-3.5.7\lib\asm-7.1.jar;D:\download\mybatis-3.5.7\lib\cglib-3.3.0.jar;D:\download\mybatis-3.5.7\lib\ognl-3.2.20.jar;D:\download\mybatis-3.5.7\lib\log4j-1.2.17.jar;D:\download\mybatis-3.5.7\lib\log4j-api-2.14.1.jar;D:\download\mybatis-3.5.7\lib\slf4j-api-1.7.30.jar;D:\download\mybatis-3.5.7\lib\log4j-core-2.14.1.jar;D:\download\mybatis-3.5.7\lib\commons-logging-1.2.jar;D:\download\mybatis-3.5.7\lib\javassist-3.27.0-GA.jar;D:\download\mybatis-3.5.7\lib\slf4j-log4j12-1.7.30.jar;D:\download\commons-dbcp2-2.9.0\commons-dbcp2-2.9.0.jar mybatis.SqlSessionFactoryUtils |
注意看第二次未触发一级缓存会多一个Preparing: sql语句
一级缓存作用范围
SQL被执行了两次,这说明一级缓存是在SqlSession层面的,对于不同的SQlSession对象时不能共享的。
1 | public static void main(String[] args) { |
1 | "C:\Program Files\Java\jdk-14.0.2\bin\java.exe" "-javaagent:D:\IntelliJ IDEA 2020.1.1\lib\idea_rt.jar=55295:D:\IntelliJ IDEA 2020.1.1\bin" -Dfile.encoding=UTF-8 -classpath D:\JavaTest\out\production\JavaTest;D:\download\cglib-nodep-3.3.0.jar;D:\download\fastjson-1.2.76.jar;D:\download\commons-lang3-3.12.0.jar;D:\download\mybatis-3.5.7\mybatis-3.5.7.jar;D:\download\mysql-connector-java-8.0.19.jar;D:\download\mybatis-3.5.7\lib\asm-7.1.jar;D:\download\mybatis-3.5.7\lib\cglib-3.3.0.jar;D:\download\mybatis-3.5.7\lib\ognl-3.2.20.jar;D:\download\mybatis-3.5.7\lib\log4j-1.2.17.jar;D:\download\mybatis-3.5.7\lib\log4j-api-2.14.1.jar;D:\download\mybatis-3.5.7\lib\slf4j-api-1.7.30.jar;D:\download\mybatis-3.5.7\lib\log4j-core-2.14.1.jar;D:\download\mybatis-3.5.7\lib\commons-logging-1.2.jar;D:\download\mybatis-3.5.7\lib\javassist-3.27.0-GA.jar;D:\download\mybatis-3.5.7\lib\slf4j-log4j12-1.7.30.jar;D:\download\commons-dbcp2-2.9.0\commons-dbcp2-2.9.0.jar mybatis.SqlSessionFactoryUtils |
开启二级缓存:
需要在RoleMapper.xml上加入代码:
1 | <cache/> |
需要序列化对象,否则会报错
1 | public class Role implements Serializable {} |
1 | "C:\Program Files\Java\jdk-14.0.2\bin\java.exe" "-javaagent:D:\IntelliJ IDEA 2020.1.1\lib\idea_rt.jar=57888:D:\IntelliJ IDEA 2020.1.1\bin" -Dfile.encoding=UTF-8 -classpath D:\JavaTest\out\production\JavaTest;D:\download\cglib-nodep-3.3.0.jar;D:\download\fastjson-1.2.76.jar;D:\download\commons-lang3-3.12.0.jar;D:\download\mybatis-3.5.7\mybatis-3.5.7.jar;D:\download\mysql-connector-java-8.0.19.jar;D:\download\mybatis-3.5.7\lib\asm-7.1.jar;D:\download\mybatis-3.5.7\lib\cglib-3.3.0.jar;D:\download\mybatis-3.5.7\lib\ognl-3.2.20.jar;D:\download\mybatis-3.5.7\lib\log4j-1.2.17.jar;D:\download\mybatis-3.5.7\lib\log4j-api-2.14.1.jar;D:\download\mybatis-3.5.7\lib\slf4j-api-1.7.30.jar;D:\download\mybatis-3.5.7\lib\log4j-core-2.14.1.jar;D:\download\mybatis-3.5.7\lib\commons-logging-1.2.jar;D:\download\mybatis-3.5.7\lib\javassist-3.27.0-GA.jar;D:\download\mybatis-3.5.7\lib\slf4j-log4j12-1.7.30.jar;D:\download\commons-dbcp2-2.9.0\commons-dbcp2-2.9.0.jar mybatis.SqlSessionFactoryUtils |
缓存配置项、自定义和应用
上一节只是配置了cache元素,加入了这个元素后,MyBatis就会将对应的命名空间内所有select元素SQL查询结果进行缓存,而其中的insert、delete和update语句在操作时会刷新缓存。
属性 | 说明 | 取值 | 备注 |
---|---|---|---|
blocking | 是否使用阻塞性缓存,在读/写时它会加入JNI的锁进行操作 | true|false,默认false | 可保证读/写安全性,但加锁后性能不佳 |
readOnly | 缓存内容是否只读 | true|false,默认false | 如果为只读,则不会因为多个线程读/写造成不一致性 |
eviction | 缓存策略:LRU/FIFO/SOFT/WEAK | 默认值LRU | |
flushInterval | 这是一个整数,它以毫秒为单位,比如1分钟刷新一次,则配置60000。默认,null,也就是没有刷新时间,只有当执行update时,insert和delete语句才会刷新 | 正整数 | 超时后缓存失效 |
type | 自定义缓存类,要求实现org.apache.ibatis.cache.Cache | 用于自定义缓存类 | |
size | 缓存对象个数 | 正整数,默认1024 |
自定义缓存实现类
可以自定义缓存,只需要实现接口org.apache.ibatis.cache.Cache
1 | // |
其他缓存
现实中,可以使用Redis、MongoDB或者其他常用的缓存,可以如下配置:
1 | <cache type="mybatis.RedisCache"> |
这样配置后,MyBatis会启用缓存,同时调用setHost(String host)方法设置配置的内容:
语句自定义缓存
1 | <select ... flushCache="false" userCache="true"/> |
以上是默认配置,flushCache代表是否刷新缓存,userCache是select特有的,代表是否启用缓存。
缓存引用
一个映射器内配置,如RoleMapper.xml,那么其他映射器不能使用,如果其他映射器需要相同的配置,需要引用缓存配置:
1 | <cache-ref namespace="mybatis.RoleMapper"/> |
存储过程
- 输入参数
- 输出参数
- 输入输出参数
待跟进
动态SQL
概述
元素 | 作用 | 备注 |
---|---|---|
if | 判断语句 | 单条件分支判断 |
choose | 相当于Java中的switch和case语句 | 多条件分支判断 |
trim(where, set) | 辅助元素,用于处理特定的SQL拼装问题,比如去掉多余的and/or等 | |
foreach | 循环语句 |
if元素
1 | <select id="findRoles" parameterType="string" resultMap="roleResultMap"> |
choose、when、otherwise元素
类似于switch…case…default语句
1 | <select id="findRoles" parameterType="role" resultMap="roleResultMap"> |
trim、where、set元素
前面有$1 = 1$在where里,如果去掉,是一条错误语句,加上又很奇怪,可考虑一下写法去掉$1 = 1$
当where元素里的条件成立时,才会加入where这个SQL关键字
1 | <select id="findRoles" parameterType="role" resultMap="roleResultMap"> |
与上面等价
1 | <select id="findRoles" parameterType="string" resultMap="roleResultMap"> |
现实中,可能更新某一对象的某些字段,而非全部,可以使用set元素避免缺失问题:
1 | <update id="updateRole" parameterType="role"> |
set元素遇到逗号,会把对应的逗号去掉。
foreach元素
foreach元素是一个循环语句,用于遍历集合,能很好地支持数组和List、Set接口的集合。
1 | <select id="findUserBySex" resultType="user"> |
- collection配置的roleNoList是传递进来的参数名称,它可以是一个数组、List、Set等集合;
- item配置的是循环中当前的元素;
- index配置的是当前元素在集合的位置下标;
- open和close配置的是以什么符号将这些集合元素包装起来;
- separator是各个元素的间隔符
用test的属性判断字符串
test用于条件判断语句,它在MyBatis中使用广泛。test的作用相当于判断真假,在大部分场景中,它都是用以判断空和非空的。
1 | <select id="getRoleTest" parameterType="string" resultMap="roleResultMap"> |
bind元素
bind元素的作用通过OGNL表达式去定义一个上下文变量,更方便使用。
1 | <select id="findRole" parameterType="string" resultType="role"> |
MyBatis的解析和运行原理
构建SqlSessionFactory过程
SqlSessionFactory是MyBatis的核心类之一,其最重要的功能就是提供创建MyBatis的核心接口SqlSession,所以要先创建SqlSessionFactory,为此要提供配置文件和相关的参数。
MyBatis是一个复杂的系统,它采用Builder模式去创建SqlSessionFactory,在实际中可以通过SqlSessionFactoryBuilder去构建:
- 第一步:通过org.apache.ibatis.builder.xml.XMLConfigBuilder解析配置的XML文件,独处所配置的参数,并将读取的内容存入org.apache.ibatis.Configuration类对象中。而Configuraion采用的是单例模式,几乎所有的MyBatis配置内容都会存放在这个单例对象中,以便后续将这些内容独处。
- 第二步:使用Configuration对象去创建SqlSessionFactory。SqlSessionFactory是一个接口,而不是一个实现类,为此MyBatis提供了一个默认的实现类org.apache.ibatis.session.defaults.DefaultSqlSessionFactory。
XMLConfigBuilder
从该方法可以看到,其通过一步步解析XML的内容得到对应的信息,而这些信息正式我们在配置文件中配置的内容。
1 | private void parseConfiguration(XNode root) { |
1 | // |
构建Configuration
在SqlSessionFactory构建中,Configuration是最重要的,它的作用是:
- 读入配置文件,包括基础配置的XML和映射XML(或注解);
- 初始化一些基础配置,比如MyBatis的别名等,一些重要的类对象(比如插件、映射器、Object工厂、typeHandler对象等);
- 提供单例,为后续创建SessionFactory服务,提供配置的参数;
- 执行一些重要对象的初始化方法;
Configuration不会是一个很简单的类,MyBatis的配置信息都来自于此,其通过XMLConfigBuilder去构建的,首先它会读出所有XML配置的信息,然后把它们解析并保存在Configuration单例中。并初始化以下内容:
- properties全局参数
- typeAliases别名
- Plugins插件
- objectFactory对象工厂
- objectWrapperFactory对象包装工厂
- reflectionFactory反射工厂
- settings环境设置
- environments数据库环境
- databaseIdProvider数据库标识
- typeHandlers类型转换器
- Mappers映射器
它们都会以类似typeHandler注册那样的方法被存放到Configuration单例中。
构建映射器的内部组成
当XMLConfigBuilder解析XML时,会将每一个SQL和其配置的内容保存起来,一般而言,在MyBatis中一条SQL和它相关的配置信息是由3个部分组成的,它们分别是MappedStatement、SqlSource和BoundSql。
- MappedStatement的作用是保存一个映射器节点(select/insert/delete/update)的内容。它是一个类,包括许多我们配置的SQL、SQL的id、缓存信息、resultMap、parameterType、resultType等重要配置内容。他还有一个重要的属性sqlSourec。MyBatis通过读取它来获得某条SQL配置的所有信息。
- SqlSource是提供BoundSql对象的地方,它是MappedStatement的一个属性。它是一个接口,它有几个实现类DynamicSqlSource、ProviderSqlSource、RawSqlSource、StatisSqlSource。它的作用是根据上下文和参数解析生成需要的SQL、这个接口只定义了一个接口方法——getBoundSql(parameterObject),使用它就可以得到一个BoundSql对象。
- BoundSql是一个结果对象,也就是SqlSource通过对SQL和参数的联合解析得到的SQL和参数,它是建立SQL和参数的地方,它有3个常用的属性:sql、parameterObject、parameterMappings。
BoundSql会提供3个主要的属性:parameterMappings、parameterObject和sql。
(1)parameterObject为参数本身,可以传递简单对象、POJO或者Map、@Param注解的参数:
- 传递简单对象,包括int、String、float、double等。当传递基本数据类型时会装箱;
- 传递POJO或者Map,parameterObject就是传入的POJO或Map
- 传递多个参数,如果没有@Param注解,那么MyBatis就会把parameterObject变为一个Map<String, Object>对象,其键值的关系是按顺序来规划的,类似于{“1”:p1, “2”:p2…},所以在编写时可以使用#{param1}或者#{1}去应用第一个参数
- 使用@Param注解,MyBatis会把parameterObject也变成一个Map<String,Object>对象,类似于没有@Param对象,只是把其数字的键值替换成@Param注解的键值。{“key1”:p1, “key2”:p2,”roleName”:p3}
(2)parameterMappings是一个List,它的每一个元素都是ParameterMapping对象。对象绘描述参数,参数包括属性名称、表达式、javaType、jdbcType、typeHanlder等重要信息,一般不需要去改变它。通过它就可以实现参数和SQL的结合,以便PreparedStatement能够通过它找到parameterObject对象的属性设置参数。
(3)sql属性就是书写在映射器里面的一条被SqlSource解析后的SQL。在大部分时候无须修改它,只是在使用插件时可以根据需要进行改写。
构建SqlSessionFactory
通过文件流生成Configuration对象,进而构建SqlSessionFactory对象
1 | public static SqlSessionFactory getSqlSessionFactory(){ |
SqlSession运行过程
映射器(Mapper)的动态代理
1 | RoleMapper roleMapper = sqlSession.getMapper(RoleMapper.class); |
DefaultSqlSession
1 | // |
Configuration
追踪得到其通过MapperRegistry来获取对应的接口对象。
1 | // |
MapperRegistry
先判断是否需要注册一个Mapper,如果没有则会抛出异常信息,否则就会启用MapperProxyFactory工厂来生成一个代理实例。
1 | // |
MapperProxyFactory
这里Mapper映射时通过动态代理来实现的。这里可以看到动态代理对接口的绑定,它的作用就是生成动态代理对象(占位),而代理的方法则被放到了MapperProxy类中。
1 | // |
MapperProxy
可以看到这里的invoke方法逻辑,如果Mapper是一个JDK动态代理对象,那么就会运行到invoke方法里面。
invoke首先判断是否是一个类,这里Mapper是一个接口不是类,所以判定失败。然后会生成MapperMethod对象,它是通过cachedMapperMethod方法对其初始化的。最后执行execute方法,把SqlSession和当前运行的参数传递进去。
1 | // |
MapperMethod
1 | // |
SqlSession下的四大对象
映射器就是一个动态代理对进入到了MapperMethod的execute方法,然后它经过简单地判断进入了SqlSession的delete、update、insert、select等方法;
而这些方法是如何执行的呢?这是正确编写插件的根本。
SqlSession的执行过程是通过Executor、StatementHandler、ParameterHandler和ResultSetHandler来完成数据库操作和结果返回的。
- Executor代表执行器,由它调度StatementHandler、ParameterHandler、ResultSetHandler等来执行对应的SQL。其中StatementHandler是最重要的。
- StatementHandler的作用是使用数据库的Statement(PreparedStatement)执行操作,它是四大对象的核心,起到承上启下的作用,许多重要的插件都是通过拦截它来实现的。
- ParameterHandler是用来处理SQL参数的。
- ResultSetHandler是进行数据集(ResultSet)的封装返回处理的,它相当复杂,不过不常用。
Executor——执行器
MyBatis中有3种执行器,可以粽子配置文件中进行选择,即settings元素中的defaultExecutorType属性:
- SIMPLE——简易执行器,不配置时就作为默认执行器
- REUSE——它是一种能够执行重用预处理语句的执行器
- BATCH——执行器重用语句和批量更新,批量专用的执行器
MyBatis通过Configuration类创建Executor
1 | public Executor newExecutor(Transaction transaction, ExecutorType executorType) { |
以SimpleExcutor为例,通过Configuration来构建StatementHandler,然后使用prepareStatement方法,对SQL编译和参数进行初始化。其调用了StatementHandler进行预编译和基础设置,最后使用query方法,将ResultHandler传递进去
1 | // |
StatementHandler——数据库会话器
1 | public StatementHandler newStatementHandler(Executor executor, MappedStatement mappedStatement, Object parameterObject, RowBounds rowBounds, ResultHandler resultHandler, BoundSql boundSql) { |
创建一个RoutingStatementHandler对象,其实现了接口StatementHandler,并根据上下文环境决定创建哪个具体的StatementHandler对象实例。
1 | // |
ParameterHandler——参数处理器
- getParameterObject()方法的作用是返回参数对象
- setParameters()方法的作用是设置预编译SQL语句的参数。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15//
// Source code recreated from a .class file by IntelliJ IDEA
// (powered by Fernflower decompiler)
//
package org.apache.ibatis.executor.parameter;
import java.sql.PreparedStatement;
import java.sql.SQLException;
public interface ParameterHandler {
Object getParameterObject();
void setParameters(PreparedStatement var1) throws SQLException;
}
ResultSetHandler——结果处理器
- handleOutputParameters()方法是处理存储过程输出参数的。
- handleResultSets()是包装结果集的。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20//
// Source code recreated from a .class file by IntelliJ IDEA
// (powered by Fernflower decompiler)
//
package org.apache.ibatis.executor.resultset;
import java.sql.CallableStatement;
import java.sql.SQLException;
import java.sql.Statement;
import java.util.List;
import org.apache.ibatis.cursor.Cursor;
public interface ResultSetHandler {
<E> List<E> handleResultSets(Statement var1) throws SQLException;
<E> Cursor<E> handleCursorResultSets(Statement var1) throws SQLException;
void handleOutputParameters(CallableStatement var1) throws SQLException;
}
SqlSession运行总结
SqlSession是通过执行器Executor调度StatementHandler来运行的。
而StatementHandler经过3步:
- prepared预编译SQL
- parameterize设置参数
- query/update执行SQL
其中parameterize是调用parameterHandler的方法设置的,而参数是根据类型处理器typeHandler处理的。query/update方法通过ResultSetHandler进行处理结果的封装,如果是update语句,就返回整数,否则就通过typeHandler处理结果类型,然后用ObjectFactory提供的规则组装对象,返回给调用者。
插件
插件接口
在MyBatis中使用插件,就必须实现接口Interceptor
1 | // |
- intercept方法:它将直接覆盖拦截对象原有的方法,因此它是插件的核心方法。intercept里面有个参数Invocation对象,通过它可以反射调度原来对象的方法。
- plugin方法:target是被拦截对象,它的作用是给被拦截对象生成一个代理对象,并返回它。在MyBatis中,它提供了org.apache.ibatis.plugin.Plugin中的wrap静态方法生成代理对象,一般情况下都会使用它来生成代理对象。
- setProperties方法:允许在plugin元素汇总配置所需参数,方法在插件初始化时就被调用了一次,然后把插件对象存入到配置中,以便后面再取出。
插件的初始化
通过XMLConfigBuilder的代码可以发现,在解析配置文件时,在MyBatis的上下文初始化过程中,就开始读入插件节点和配置的参数,同时使用反射技术生成对应的插件实例,然后调用插件方法中的setProperties方法,设置我们配置的参数,将插件实例保存到配置对象中,以便读取和使用它。
1 | private void pluginElement(XNode parent) throws Exception { |
1 | public void addInterceptor(Interceptor interceptor) { |
1 | // |
完成初始化的插件保存在这个List对象里面等待将其取出使用。
插件的代理和反射设计
1 | executor = (Executor) interceptorChain.pluginAll(executor); |
其通过plugin方法生成代理对象,责任链模式。
1 | public Object pluginAll(Object target) { |
自己编写代理类工作量很大,为此MyBatis中提供了一个常用工具类,用来生成代理对象:Plugin
1 | // |
如果使用这个类为插件生成代理对象,那么代理对象在调用方法时就会进入到invoke方法中。在invoke方法中,如果存在签名的拦截方法,插件的intercept方法就会在这里调用,然后返回结果。如果不存在签名方法,那么将直接反射调度要执行的方法。
MyBatis把被代理对象、反射方法及其参数,都传递给了Invocation类的构造方法,用以生成一个Invocation类对象,其有一个proceed()方法。
1 | // |
如果有多个插件,那么通过wrap产生多层包装的代理对象。
常见工具类——MetaObject
MetaObject可以有效读取或者修改一些重要对象的属性。在MyBatis中,四大对象提供的public设置参数的方法很少难以通过其自身得到相关的属性信息,但是有了MetaObject这个工具类就可以通过其他的技术手段来读取或者修改这些重要对象的属性。
MetaObject有3个方法:
1 | //用于包装对象,不再使用,用SystemMetaObject.froObject(Object obj)替换 |
1 | StatementHandler statementHandler = (StatementHandler) invocation.getTarget(); |
确定需要拦截的签名
MyBatis允许拦截四大对象中的任意一个对象,而通过Plugin源码,我们也看到了需要先注册签名才能使用插件,因此首先要确定需要拦截的对象,才能进一步确定需要配置什么样的签名,进而完成拦截的方法逻辑。
1、确定需要拦截的对象
- Executor是执行SQL的全过程,包括组装参数、组装结果集返回和执行SQL过程,都可以拦截,较为广泛,一般用的不算太多。根据是否启动缓存参数,决定它是否使用CachingExecutor进行封装。
- StatementHandler是执行SQL的过程,我们可以重写执行SQL的过程,它是最常用的拦截对象。
- ParameterHandler主要拦截执行SQL的参数组装,我们可以重写组装参数规则。
- ResultSetHandler用于拦截执行结果的组装,我们可以重写组装结果的规则。
2、拦截方法和参数
当确定了需要拦截什么对象,接下来就要确定需要拦截什么方法及方法的参数,这些都是在理解了MyBatis四大对象运作的基础上才确定的。
查询的过程是通过Executor调度StatementHandler来完成的。调度StatementHandler的prepare方法预编译SQL,于是要拦截的方法便是prepare方法,在此之前完成SQL的重新编写。
1 | // |
以上的任何方法都可以拦截
设计拦截器
- @Intercepts说明它是一个拦截器
- @Signature是注册拦截器签名的地方,只有签名满足条件才能拦截
- type可以是四大对象中的一个,这里是StatementHandler
- method代表要拦截四大对象的某一种接口方法,而args
1
2
3
4
5
6
7
8
9
10
11({
.class, (type=StatementHandler
method = "prepare",
args = {Connection.class, Integer.class})})
class MyPlugin implements Interceptor{
public Object intercept(Invocation invocation) throws Throwable {
return null;
}
}
实现拦截方法
1 | package mybatis; |
MyBatis的配置文件配置plugins元素,注意其配置顺序。
1 | <plugins> |
运行结果
1 | "C:\Program Files\Java\jdk-14.0.2\bin\java.exe" "-javaagent:D:\IntelliJ IDEA 2020.1.1\lib\idea_rt.jar=55183:D:\IntelliJ IDEA 2020.1.1\bin" -Dfile.encoding=UTF-8 -classpath D:\JavaTest\out\production\JavaTest;D:\download\cglib-nodep-3.3.0.jar;D:\download\fastjson-1.2.76.jar;D:\download\commons-lang3-3.12.0.jar;D:\download\mybatis-3.5.7\mybatis-3.5.7.jar;D:\download\mysql-connector-java-8.0.19.jar;D:\download\mybatis-3.5.7\lib\asm-7.1.jar;D:\download\mybatis-3.5.7\lib\cglib-3.3.0.jar;D:\download\mybatis-3.5.7\lib\ognl-3.2.20.jar;D:\download\mybatis-3.5.7\lib\log4j-1.2.17.jar;D:\download\mybatis-3.5.7\lib\log4j-api-2.14.1.jar;D:\download\mybatis-3.5.7\lib\slf4j-api-1.7.30.jar;D:\download\mybatis-3.5.7\lib\log4j-core-2.14.1.jar;D:\download\mybatis-3.5.7\lib\commons-logging-1.2.jar;D:\download\mybatis-3.5.7\lib\javassist-3.27.0-GA.jar;D:\download\mybatis-3.5.7\lib\slf4j-log4j12-1.7.30.jar;D:\download\commons-dbcp2-2.9.0\commons-dbcp2-2.9.0.jar mybatis.SqlSessionFactoryUtils |
插件实例——分页插件
MyBatis中存在一个RowBounds用于分页,但是它是基于第一次查询结果的再分页,也就是先让SQL查询出所有的记录,然后分页,显然性能不高。
启动分页参数(PageParams)需要满足下列条件之一:
- 传递单个PageParams或者其子对象
- map中存在一个值为PageParams或者其子对象
- 在MyBatis中传递多个参数,但其中之一为PageParams或者其子对象
- 传递单个POJO参数,这个POJO有一个属性为PageParams或者其子对象,且提供了setter和getter方法。
自定义PageParamsCustom类
1 | package mybatis; |
自定义PagePlugin类
1 | package mybatis; |
mapper设置以及启动程序
配置文件设置
1 | <plugins> |
1 | Logger logger = Logger.getLogger(SqlSessionFactoryUtils.class); |
运行结果
1 | "C:\Program Files\Java\jdk-14.0.2\bin\java.exe" "-javaagent:D:\IntelliJ IDEA 2020.1.1\lib\idea_rt.jar=49924:D:\IntelliJ IDEA 2020.1.1\bin" -Dfile.encoding=UTF-8 -classpath D:\JavaTest\out\production\JavaTest;D:\download\cglib-nodep-3.3.0.jar;D:\download\fastjson-1.2.76.jar;D:\download\commons-lang3-3.12.0.jar;D:\download\mybatis-3.5.7\mybatis-3.5.7.jar;D:\download\mysql-connector-java-8.0.19.jar;D:\download\mybatis-3.5.7\lib\asm-7.1.jar;D:\download\mybatis-3.5.7\lib\cglib-3.3.0.jar;D:\download\mybatis-3.5.7\lib\ognl-3.2.20.jar;D:\download\mybatis-3.5.7\lib\log4j-1.2.17.jar;D:\download\mybatis-3.5.7\lib\log4j-api-2.14.1.jar;D:\download\mybatis-3.5.7\lib\slf4j-api-1.7.30.jar;D:\download\mybatis-3.5.7\lib\log4j-core-2.14.1.jar;D:\download\mybatis-3.5.7\lib\commons-logging-1.2.jar;D:\download\mybatis-3.5.7\lib\javassist-3.27.0-GA.jar;D:\download\mybatis-3.5.7\lib\slf4j-log4j12-1.7.30.jar;D:\download\commons-dbcp2-2.9.0\commons-dbcp2-2.9.0.jar mybatis.SqlSessionFactoryUtils |
Spring 基础
Spring IoC概述
IoC:Inverse of Control 控制反转
Spring IoC容器
1 | // |
- getBean的多个方法用于获取配置给SpringIoC容器的Bean。从参数类型看可以使字符串,也可以是Class类型,由于Class类型可以扩展接口也可以集成父类,所以在一定程度上会存在使用父类类型无法准确获得实例的异常,比如获取学生类,但是学生子类有男学生和女学生两类,这个时候通过学生类就无法从容器中得到实例,因为容器无法判断具体的实现类。
- isSingleton用于判断是否单例,如果判断为真,其意思是该Bean的容器中是作为一个唯一单例存在的。而isPrototype则相反,如果判断为真,意思是当你从容其中获取Bean,容器就为你生成了一个新的实例。在默认情况下,Spring会为Bean创建一个单例,也就是默认情况下isSingleton返回true,而isPrototype返回false。
- 关于type的匹配,这是一个按Java类型匹配的方式。
- getAliases方法是获取别名的方法。
Spring IoC容器的初始化和依赖注入
Bean的初始化分为3步:
(1)Resource定位,这步是Spring IoC容器根据开发者的配置,进行资源定位,在Spring的开发中,通过XML或者注解都是十分常见的方式,定位的内容是由开发者所提供的。
(2)BeanDefinition的载入,这个过程就是Spring根据开发者的配置获取对应的POJO,用以生成对应实例的过程。
(3)BeanDefinition的注册,这个步骤就相当于把之前通过BeanDefinition载入的POJO往Spring IoC容器中注册,这样就可以使得开发和测试人员都可以通过描述从中得到Spring IoC容器的Bean了。
以上3步完成后,Bean就在Spring IoC容器中得到了初始化,但是没有完全依赖注入,也就是没有注入其配置的资源给Bean,那么它还不能完全使用。对于依赖注入,Spring Bean还有一个配置选项——lazy-init,其含义就是是否初始化Spring Bean,默认false,即默认自动初始化Bean。否则,只有当我们通过Spring IoC容器的getBean方法获取它时,它才会进行初始化,完成依赖注入。
依赖注入实例
1 | package com.louris.springboot.IoCImpl.beans.pojo; |
1 | package com.louris.springboot.IoCImpl.beans.pojo; |
spring-config.xml,该文件在resources目录下,因为默认springboot读取扫描resources下的xml和其他资源文件,否则需要配置pom.xml的resources标签指定扫描目录:
1 | <build> |
spring-config.xml
1 |
|
1 | package com.louris.springboot; |
运行结果:
1 | "C:\Program Files\Java\jdk-14.0.2\bin\java.exe" -XX:TieredStopAtLevel=1 -noverify -Dspring.output.ansi.enabled=always -Dcom.sun.management.jmxremote -Dspring.jmx.enabled=true -Dspring.liveBeansView.mbeanDomain -Dspring.application.admin.enabled=true "-javaagent:D:\IntelliJ IDEA 2020.1.1\lib\idea_rt.jar=57146:D:\IntelliJ IDEA 2020.1.1\bin" -Dfile.encoding=UTF-8 -classpath D:\SpringBootCourse\001-springboot-first\target\classes;C:\Users\98383\.m2\repository\org\springframework\boot\spring-boot-starter-web\2.4.4\spring-boot-starter-web-2.4.4.jar;C:\Users\98383\.m2\repository\org\springframework\boot\spring-boot-starter\2.4.4\spring-boot-starter-2.4.4.jar;C:\Users\98383\.m2\repository\org\springframework\boot\spring-boot-autoconfigure\2.4.4\spring-boot-autoconfigure-2.4.4.jar;C:\Users\98383\.m2\repository\org\springframework\boot\spring-boot-starter-logging\2.4.4\spring-boot-starter-logging-2.4.4.jar;C:\Users\98383\.m2\repository\ch\qos\logback\logback-classic\1.2.3\logback-classic-1.2.3.jar;C:\Users\98383\.m2\repository\ch\qos\logback\logback-core\1.2.3\logback-core-1.2.3.jar;C:\Users\98383\.m2\repository\org\apache\logging\log4j\log4j-to-slf4j\2.13.3\log4j-to-slf4j-2.13.3.jar;C:\Users\98383\.m2\repository\org\apache\logging\log4j\log4j-api\2.13.3\log4j-api-2.13.3.jar;C:\Users\98383\.m2\repository\org\slf4j\jul-to-slf4j\1.7.30\jul-to-slf4j-1.7.30.jar;C:\Users\98383\.m2\repository\jakarta\annotation\jakarta.annotation-api\1.3.5\jakarta.annotation-api-1.3.5.jar;C:\Users\98383\.m2\repository\org\yaml\snakeyaml\1.27\snakeyaml-1.27.jar;C:\Users\98383\.m2\repository\org\springframework\boot\spring-boot-starter-json\2.4.4\spring-boot-starter-json-2.4.4.jar;C:\Users\98383\.m2\repository\com\fasterxml\jackson\core\jackson-databind\2.11.4\jackson-databind-2.11.4.jar;C:\Users\98383\.m2\repository\com\fasterxml\jackson\core\jackson-annotations\2.11.4\jackson-annotations-2.11.4.jar;C:\Users\98383\.m2\repository\com\fasterxml\jackson\core\jackson-core\2.11.4\jackson-core-2.11.4.jar;C:\Users\98383\.m2\repository\com\fasterxml\jackson\datatype\jackson-datatype-jdk8\2.11.4\jackson-datatype-jdk8-2.11.4.jar;C:\Users\98383\.m2\repository\com\fasterxml\jackson\datatype\jackson-datatype-jsr310\2.11.4\jackson-datatype-jsr310-2.11.4.jar;C:\Users\98383\.m2\repository\com\fasterxml\jackson\module\jackson-module-parameter-names\2.11.4\jackson-module-parameter-names-2.11.4.jar;C:\Users\98383\.m2\repository\org\springframework\boot\spring-boot-starter-tomcat\2.4.4\spring-boot-starter-tomcat-2.4.4.jar;C:\Users\98383\.m2\repository\org\apache\tomcat\embed\tomcat-embed-core\9.0.44\tomcat-embed-core-9.0.44.jar;C:\Users\98383\.m2\repository\org\glassfish\jakarta.el\3.0.3\jakarta.el-3.0.3.jar;C:\Users\98383\.m2\repository\org\apache\tomcat\embed\tomcat-embed-websocket\9.0.44\tomcat-embed-websocket-9.0.44.jar;C:\Users\98383\.m2\repository\org\springframework\spring-web\5.3.5\spring-web-5.3.5.jar;C:\Users\98383\.m2\repository\org\springframework\spring-webmvc\5.3.5\spring-webmvc-5.3.5.jar;C:\Users\98383\.m2\repository\org\springframework\spring-aop\5.3.5\spring-aop-5.3.5.jar;C:\Users\98383\.m2\repository\org\springframework\spring-expression\5.3.5\spring-expression-5.3.5.jar;C:\Users\98383\.m2\repository\org\slf4j\slf4j-api\1.7.30\slf4j-api-1.7.30.jar;C:\Users\98383\.m2\repository\org\springframework\spring-core\5.3.5\spring-core-5.3.5.jar;C:\Users\98383\.m2\repository\org\springframework\spring-jcl\5.3.5\spring-jcl-5.3.5.jar;C:\Users\98383\.m2\repository\org\springframework\spring-beans\5.3.5\spring-beans-5.3.5.jar;C:\Users\98383\.m2\repository\org\springframework\boot\spring-boot\2.4.4\spring-boot-2.4.4.jar;C:\Users\98383\.m2\repository\org\springframework\spring-context\5.3.5\spring-context-5.3.5.jar com.louris.springboot.Application |
Spring Bean的生命周期
Spring IoC容器在执行了初始化和依赖注入后,会执行一定的步骤来完成初始化,通过这些步骤我们就能自定义初始化,而在Spring IoC容器正常关闭的时候,它也会执行一定的步骤来关闭容器,释放资源。除需要了解整个生命周期的步骤外,还要知道这些生命周期的接口是针对什么而言的,首先介绍生命周期的步骤。
- 如果Bean实现了接口BeanNameAware的setBeanName方法,那么它就会调用这个方法。
- 如果Bean实现了接口BeanFactoryAware的setBeanFactory方法,那么它就会调用这个方法。
- 如果Bean实现了接口ApplicationContextAware的setApplicationContext方法,且Spring IoC容器也必须是一个ApplicationContext接口的实现类,那么才会调用这个方法,否则是不调用的。
- 如果Bean实现了接口BeanPostProcessor的postProcessBeforeInitialization方法,那么它就会调用这个方法。
- 如果Bean实现了接口BeanFactoryPostProcessor的afterPropertiesSet方法,那么它就会调用这个方法。
- 如果Bean自定义了初始化方法,它就会调用已定义的初始化方法。
- 如果Bean实现了接口BeanPostProcessor的postProcessAfterInitialization方法,完成了这些调用,这个时候Bean就完成了初始化,那么Bean就生存在Spring IoC的容器中了,使用者就可以从中获取Bean的服务。
当服务器正常关闭,或者遇到其他关闭Spring IoC容器的事件,它就会调用对应的方法完成Bean的销毁,步骤如下:
- 如果Bean实现了接口DisposableBean的destroy方法,那么就会调用它。
- 如果定义了自定义销毁方法,那么就会调用它。
因为有些步骤是在一些条件下才会执行的,如果不注意这些,往往就发现命名实现了一些借口,但是该方法并没有被执行。所有的Spring IoC容器最低的要求是实现BeanFactory接口而已,而非ApplicationContext接口,如果采用了非ApplicationContext子类创建Spring IoC容器,俺么即使是实现了ApplicationContextAware的setApplicationContext方法,它也不会在生命周期之中被调用。
此外,还要注意这些接口是针对什么而言的,上述生命周期的接口,大部分是针对单个Bean而言的;BeanPostProcessor接口则是针对所有Bean而言的;接口DisposableBean则是针对Spring IoC容器本身。当一个Bean实现了上述的接口,我们只需要在Spring IoC容器中定义它就可以了,Spring IoC容器会自动识别。
测试实现
1 | package com.louris.springboot.IoCImpl.beans.pojo; |
1 | package com.louris.springboot.IoCImpl.beans.pojo; |
1 | package com.louris.springboot.IoCImpl; |
1 | package com.louris.springboot.IoCImpl; |
1 | package com.louris.springboot; |
执行结果
1 | "C:\Program Files\Java\jdk-14.0.2\bin\java.exe" -XX:TieredStopAtLevel=1 -noverify -Dspring.output.ansi.enabled=always -Dcom.sun.management.jmxremote -Dspring.jmx.enabled=true -Dspring.liveBeansView.mbeanDomain -Dspring.application.admin.enabled=true "-javaagent:D:\IntelliJ IDEA 2020.1.1\lib\idea_rt.jar=54012:D:\IntelliJ IDEA 2020.1.1\bin" -Dfile.encoding=UTF-8 -classpath D:\SpringBootCourse\001-springboot-first\target\classes;C:\Users\98383\.m2\repository\org\springframework\boot\spring-boot-starter-web\2.4.4\spring-boot-starter-web-2.4.4.jar;C:\Users\98383\.m2\repository\org\springframework\boot\spring-boot-starter\2.4.4\spring-boot-starter-2.4.4.jar;C:\Users\98383\.m2\repository\org\springframework\boot\spring-boot-autoconfigure\2.4.4\spring-boot-autoconfigure-2.4.4.jar;C:\Users\98383\.m2\repository\org\springframework\boot\spring-boot-starter-logging\2.4.4\spring-boot-starter-logging-2.4.4.jar;C:\Users\98383\.m2\repository\ch\qos\logback\logback-classic\1.2.3\logback-classic-1.2.3.jar;C:\Users\98383\.m2\repository\ch\qos\logback\logback-core\1.2.3\logback-core-1.2.3.jar;C:\Users\98383\.m2\repository\org\apache\logging\log4j\log4j-to-slf4j\2.13.3\log4j-to-slf4j-2.13.3.jar;C:\Users\98383\.m2\repository\org\apache\logging\log4j\log4j-api\2.13.3\log4j-api-2.13.3.jar;C:\Users\98383\.m2\repository\org\slf4j\jul-to-slf4j\1.7.30\jul-to-slf4j-1.7.30.jar;C:\Users\98383\.m2\repository\jakarta\annotation\jakarta.annotation-api\1.3.5\jakarta.annotation-api-1.3.5.jar;C:\Users\98383\.m2\repository\org\yaml\snakeyaml\1.27\snakeyaml-1.27.jar;C:\Users\98383\.m2\repository\org\springframework\boot\spring-boot-starter-json\2.4.4\spring-boot-starter-json-2.4.4.jar;C:\Users\98383\.m2\repository\com\fasterxml\jackson\core\jackson-databind\2.11.4\jackson-databind-2.11.4.jar;C:\Users\98383\.m2\repository\com\fasterxml\jackson\core\jackson-annotations\2.11.4\jackson-annotations-2.11.4.jar;C:\Users\98383\.m2\repository\com\fasterxml\jackson\core\jackson-core\2.11.4\jackson-core-2.11.4.jar;C:\Users\98383\.m2\repository\com\fasterxml\jackson\datatype\jackson-datatype-jdk8\2.11.4\jackson-datatype-jdk8-2.11.4.jar;C:\Users\98383\.m2\repository\com\fasterxml\jackson\datatype\jackson-datatype-jsr310\2.11.4\jackson-datatype-jsr310-2.11.4.jar;C:\Users\98383\.m2\repository\com\fasterxml\jackson\module\jackson-module-parameter-names\2.11.4\jackson-module-parameter-names-2.11.4.jar;C:\Users\98383\.m2\repository\org\springframework\boot\spring-boot-starter-tomcat\2.4.4\spring-boot-starter-tomcat-2.4.4.jar;C:\Users\98383\.m2\repository\org\apache\tomcat\embed\tomcat-embed-core\9.0.44\tomcat-embed-core-9.0.44.jar;C:\Users\98383\.m2\repository\org\glassfish\jakarta.el\3.0.3\jakarta.el-3.0.3.jar;C:\Users\98383\.m2\repository\org\apache\tomcat\embed\tomcat-embed-websocket\9.0.44\tomcat-embed-websocket-9.0.44.jar;C:\Users\98383\.m2\repository\org\springframework\spring-web\5.3.5\spring-web-5.3.5.jar;C:\Users\98383\.m2\repository\org\springframework\spring-webmvc\5.3.5\spring-webmvc-5.3.5.jar;C:\Users\98383\.m2\repository\org\springframework\spring-aop\5.3.5\spring-aop-5.3.5.jar;C:\Users\98383\.m2\repository\org\springframework\spring-expression\5.3.5\spring-expression-5.3.5.jar;C:\Users\98383\.m2\repository\org\slf4j\slf4j-api\1.7.30\slf4j-api-1.7.30.jar;C:\Users\98383\.m2\repository\org\springframework\spring-core\5.3.5\spring-core-5.3.5.jar;C:\Users\98383\.m2\repository\org\springframework\spring-jcl\5.3.5\spring-jcl-5.3.5.jar;C:\Users\98383\.m2\repository\org\springframework\spring-beans\5.3.5\spring-beans-5.3.5.jar;C:\Users\98383\.m2\repository\org\springframework\boot\spring-boot\2.4.4\spring-boot-2.4.4.jar;C:\Users\98383\.m2\repository\org\springframework\spring-context\5.3.5\spring-context-5.3.5.jar com.louris.springboot.Application |
装配Spring Bean
依赖注入的3种方式
构造器注入
构造器注入依赖于构造方法实现,而构造方法可以是有参数的或者是无参数的。在大部分的情况下,我们都是通过类的构造方法来创建类对象,Spring也可以采用反射的方式,通过使用构造方式来完成注入,这就是构造器注入的原理。
为了让Spring完成对应的构造注入,我们有必要去描述具体的类、构造方法并设置对应的参数,这样Spring就会通过对应的信息用反射的形式创建对象。
1 | package com.louris.springboot.IoCImpl.beans.pojo; |
1 | <bean id="role" class="com.louris.springboot.IoCImpl.beans.pojo.Role"> |
constructor-arg元素用于定义类构造方法的参数,其中index用于定义参数的位置,而value则是设置值,通过这样的定义Spring便知道使用Role对应的构造方法创建对象。
缺点是如果参数很多,构造方法就比较复杂
使用setter注入
setter注入是Spring中最主流的注入方式。首先可以把构造方法声明为无参数的,然后使用setter注入为其设置对应的值,其实也是通过Java反射技术得以实现的。
1 | <bean id="role" class="com.louris.springboot.IoCImpl.beans.pojo.Role"> |
接口注入
有些时候资源并非来自于自身系统,而是来自于外界,比如数据库连接资源完全可以在Tomcat下配置,然后通过JNDI的形式去获取它,这样数据库连接资源是属于开发工程外的资源,这个时候可以采用接口注入的形式来获取它。
1 | <bean id="dataSource" class="org.springframework.jndi.JndiObjectFactoryBean"> |
装配Bean概述
Spring中提供了3种方法进行配置装配Bean到Spring IoC容器:
- 在XML中显示配置;
- 在Java的接口和类中实现配置;
- 隐式Bean的发现机制和自动装配原则;
明确3种方式的优先级:
(1)基于约定优于配置的原则,最优先的应该是通过隐式Bean的发现机制和自动装配的原则。减少程序开发者的决定权,简单又不是灵活;
(2)在没有办法使用自动装配原则的情况下应该优先考虑Java接口和类中实现配置,避免XML配置的泛滥。典型场景的例子是一个父类有多个子类,通过IoC容器初始化一个子类,容器将无法知道使用哪个子类去初始化,这个时候可以使用Java的注解配置去指定。
(3)在上述方法都无法使用的情况下,那么只能选择XML去配置Spring IoC容器。由于现实工作中常常用到第三方的类库,有些类并不是我们开发的,我们无法修改里面的代码,这个时候就通过XML的方式配置使用了。
通过XML配置装配Bean
简单配置类对象
1 |
|
装配集合等复杂对象
1 |
|
命名空间装配
- c表示通过构造器,c:_0表示构造器的第一个参数,c:_1表示构造器的第一个参数,其他同理,所以必须在POJO上实现对应参数的构造器
- p表示通过引用属性,p:id表示使用setId方法进行设置,其他同理
- util表示使用内置的对象进行设置,例如list、set
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:c="http://www.springframework.org/schema/c"
xmlns:p="http://www.springframework.org/schema/p"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd">
<bean id="role3" class="com.louris.springboot.IoCImpl.beans.pojo.Role"
c:_0="3"
c:_1="role_name_3"
c:_2="role_note_3"/>
<bean id="role4" class="com.louris.springboot.IoCImpl.beans.pojo.Role"
p:id="4"
p:roleName="role_name_4"
p:note="role_note_4"/>
</beans>1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:c="http://www.springframework.org/schema/c"
xmlns:p="http://www.springframework.org/schema/p"
xmlns:util="http://www.springframework.org/schema/util"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/util http://www.springframework.org/schema/util/spring-util.xsd">
<bean id="complexAssembly" class="com.louris.springboot.IoCImpl.beans.pojo.ComplexAssembly">
<property name="id" value="1"/>
<property name="list">
<list>
<value>value-list-1</value>
<value>value-list-2</value>
<value>value-list-3</value>
</list>
</property>
<property name="map">
<map>
<entry key="key1" value="value-key-1"/>
<entry key="key2" value="value-key-2"/>
<entry key="key3" value="value-key-3"/>
</map>
</property>
<property name="properties">
<props>
<prop key="prop1">value-prop-1</prop>
<prop key="prop2">value-prop-2</prop>
<prop key="prop3">value-prop-3</prop>
</props>
</property>
<property name="set">
<set>
<value>value-set-1</value>
<value>value-set-2</value>
<value>value-set-3</value>
</set>
</property>
<property name="array">
<array>
<value>value-array-1</value>
<value>value-array-2</value>
<value>value-array-3</value>
</array>
</property>
</bean>
<bean id="role1" class="com.louris.springboot.IoCImpl.beans.pojo.Role">
<property name="id" value="1"/>
<property name="roleName" value="role_name_1"/>
<property name="note" value="role_note_1"/>
</bean>
<bean id="role2" class="com.louris.springboot.IoCImpl.beans.pojo.Role">
<property name="id" value="2"/>
<property name="roleName" value="role_name_2"/>
<property name="note" value="note_note_2"/>
</bean>
<bean id="user1" class="com.louris.springboot.IoCImpl.beans.pojo.User">
<property name="id" value="1"/>
<property name="userName" value="user_name_1"/>
<property name="note" value="role_note_1"/>
</bean>
<bean id="user2" class="com.louris.springboot.IoCImpl.beans.pojo.User">
<property name="id" value="2"/>
<property name="userName" value="user_name_2"/>
<property name="note" value="role_note_1"/>
</bean>
<util:list id="list" >
<ref bean="role1"/>
<ref bean="role2"/>
</util:list>
<util:map id="map">
<entry key-ref="role1" value-ref="user1"/>
<entry key-ref="role2" value-ref="user2"/>
</util:map>
<util:set id="set">
<ref bean="role1"/>
<ref bean="role2"/>
</util:set>
<bean id="userRoleAssembly" class="com.louris.springboot.IoCImpl.beans.pojo.UserRoleAssembly"
p:id="1"
p:list-ref="list"
p:map-ref="map"
p:set-ref="set"/>
</beans>
通过注解装配Bean
更多的时候会考虑使用注解的方式去装配Bean,可以减少XML的配置,注解功能更为强大,它既能实现XML的功能,也提供了自动装配的功能,采用了自动装配后,程序员所需要做的决断就少了,更加有利于对程序的开发。
在Spring中,它提供了两种方式来让Spring IoC容器发现Bean
- 组件扫描:通过定义资源的方式,让Spring IoC容器扫描对应的包,从而把Bean装配进来;
- 自动装配:通过注解定义,使得一些依赖关系可以通过注解完成;
使用@Component装配Bean
注解Bean
1 | package com.louris.springboot.IoCImpl.beans.pojo; |
@ComponentScan配置扫描类
注意,@ComponentScan有两个参数:
basePackages,可以扫描多个包的bean,默认为当前Config类的包;
basePackageClasses,可以扫描多个指定的bean
如果采用多个@ComponentScan去定义对应的包,但是每定义一个@ComponentScan,Spring就会为所定义的类生成一个新的对象,也就是所配置的Bean将会生成多个实例,这往往不是我们需要的;
对于同时一定了上述两个参数的@ComponentScan,Spring会进行专门的区分,也就是说在同一个@ComponentScan中即使重复定义相同的包或者存在其子包定义,也不会造成因同一个Bean的多次扫描,而导致一次配置生成多个对象。
1
2
3
4
5
6
7
8
9
10package com.louris.springboot.IoCImpl.config;
import com.louris.springboot.IoCImpl.beans.pojo.Role;
import org.springframework.context.annotation.ComponentScan;
//@ComponentScan(basePackages = {"com.louris.springboot.IoCImpl.beans"})
.class}) (basePackageClasses = {Role
public class PojoConfig {
}
测试类
1 | package com.louris.springboot; |
运行结果
1 | "C:\Program Files\Java\jdk-14.0.2\bin\java.exe" -XX:TieredStopAtLevel=1 -noverify -Dspring.output.ansi.enabled=always -Dcom.sun.management.jmxremote -Dspring.jmx.enabled=true -Dspring.liveBeansView.mbeanDomain -Dspring.application.admin.enabled=true "-javaagent:D:\IntelliJ IDEA 2020.1.1\lib\idea_rt.jar=57856:D:\IntelliJ IDEA 2020.1.1\bin" -Dfile.encoding=UTF-8 -classpath D:\SpringBootCourse\001-springboot-first\target\classes;C:\Users\98383\.m2\repository\org\springframework\boot\spring-boot-starter-web\2.4.4\spring-boot-starter-web-2.4.4.jar;C:\Users\98383\.m2\repository\org\springframework\boot\spring-boot-starter\2.4.4\spring-boot-starter-2.4.4.jar;C:\Users\98383\.m2\repository\org\springframework\boot\spring-boot-autoconfigure\2.4.4\spring-boot-autoconfigure-2.4.4.jar;C:\Users\98383\.m2\repository\org\springframework\boot\spring-boot-starter-logging\2.4.4\spring-boot-starter-logging-2.4.4.jar;C:\Users\98383\.m2\repository\ch\qos\logback\logback-classic\1.2.3\logback-classic-1.2.3.jar;C:\Users\98383\.m2\repository\ch\qos\logback\logback-core\1.2.3\logback-core-1.2.3.jar;C:\Users\98383\.m2\repository\org\apache\logging\log4j\log4j-to-slf4j\2.13.3\log4j-to-slf4j-2.13.3.jar;C:\Users\98383\.m2\repository\org\apache\logging\log4j\log4j-api\2.13.3\log4j-api-2.13.3.jar;C:\Users\98383\.m2\repository\org\slf4j\jul-to-slf4j\1.7.30\jul-to-slf4j-1.7.30.jar;C:\Users\98383\.m2\repository\jakarta\annotation\jakarta.annotation-api\1.3.5\jakarta.annotation-api-1.3.5.jar;C:\Users\98383\.m2\repository\org\yaml\snakeyaml\1.27\snakeyaml-1.27.jar;C:\Users\98383\.m2\repository\org\springframework\boot\spring-boot-starter-json\2.4.4\spring-boot-starter-json-2.4.4.jar;C:\Users\98383\.m2\repository\com\fasterxml\jackson\core\jackson-databind\2.11.4\jackson-databind-2.11.4.jar;C:\Users\98383\.m2\repository\com\fasterxml\jackson\core\jackson-annotations\2.11.4\jackson-annotations-2.11.4.jar;C:\Users\98383\.m2\repository\com\fasterxml\jackson\core\jackson-core\2.11.4\jackson-core-2.11.4.jar;C:\Users\98383\.m2\repository\com\fasterxml\jackson\datatype\jackson-datatype-jdk8\2.11.4\jackson-datatype-jdk8-2.11.4.jar;C:\Users\98383\.m2\repository\com\fasterxml\jackson\datatype\jackson-datatype-jsr310\2.11.4\jackson-datatype-jsr310-2.11.4.jar;C:\Users\98383\.m2\repository\com\fasterxml\jackson\module\jackson-module-parameter-names\2.11.4\jackson-module-parameter-names-2.11.4.jar;C:\Users\98383\.m2\repository\org\springframework\boot\spring-boot-starter-tomcat\2.4.4\spring-boot-starter-tomcat-2.4.4.jar;C:\Users\98383\.m2\repository\org\apache\tomcat\embed\tomcat-embed-core\9.0.44\tomcat-embed-core-9.0.44.jar;C:\Users\98383\.m2\repository\org\glassfish\jakarta.el\3.0.3\jakarta.el-3.0.3.jar;C:\Users\98383\.m2\repository\org\apache\tomcat\embed\tomcat-embed-websocket\9.0.44\tomcat-embed-websocket-9.0.44.jar;C:\Users\98383\.m2\repository\org\springframework\spring-web\5.3.5\spring-web-5.3.5.jar;C:\Users\98383\.m2\repository\org\springframework\spring-webmvc\5.3.5\spring-webmvc-5.3.5.jar;C:\Users\98383\.m2\repository\org\springframework\spring-aop\5.3.5\spring-aop-5.3.5.jar;C:\Users\98383\.m2\repository\org\springframework\spring-expression\5.3.5\spring-expression-5.3.5.jar;C:\Users\98383\.m2\repository\org\slf4j\slf4j-api\1.7.30\slf4j-api-1.7.30.jar;C:\Users\98383\.m2\repository\org\springframework\spring-core\5.3.5\spring-core-5.3.5.jar;C:\Users\98383\.m2\repository\org\springframework\spring-jcl\5.3.5\spring-jcl-5.3.5.jar;C:\Users\98383\.m2\repository\org\springframework\spring-beans\5.3.5\spring-beans-5.3.5.jar;C:\Users\98383\.m2\repository\org\springframework\boot\spring-boot\2.4.4\spring-boot-2.4.4.jar;C:\Users\98383\.m2\repository\org\springframework\spring-context\5.3.5\spring-context-5.3.5.jar com.louris.springboot.Application |
自动装配——@Autowired
Spring先完成Bean的定义和生成,然后寻找需要注入的资源;也就是当Spring生成所有的Bean后,如果发现这个注解,它就会在Bean中查找,然后找到对应的类型,将其注入进来,这样就完成了依赖注入了。
自动装配技术是一种由Spring自己发现对应的Bean,自动完成装配工作的方式,它会应用到一个十分常用的注解@Autowired上,这个时候Spring会根据类型去寻找定义的Bean然后将其注入,按类型的方式。
RoleService
1 | package com.louris.springboot.IoCImpl.service; |
RoleServiceImpl
1 | package com.louris.springboot.IoCImpl.impl; |
PojoConfig
1 | package com.louris.springboot.IoCImpl.config; |
测试启动类
1 | package com.louris.springboot; |
运行结果
1 | "C:\Program Files\Java\jdk-14.0.2\bin\java.exe" -XX:TieredStopAtLevel=1 -noverify -Dspring.output.ansi.enabled=always -Dcom.sun.management.jmxremote -Dspring.jmx.enabled=true -Dspring.liveBeansView.mbeanDomain -Dspring.application.admin.enabled=true "-javaagent:D:\IntelliJ IDEA 2020.1.1\lib\idea_rt.jar=60803:D:\IntelliJ IDEA 2020.1.1\bin" -Dfile.encoding=UTF-8 -classpath D:\SpringBootCourse\001-springboot-first\target\classes;C:\Users\98383\.m2\repository\org\springframework\boot\spring-boot-starter-web\2.4.4\spring-boot-starter-web-2.4.4.jar;C:\Users\98383\.m2\repository\org\springframework\boot\spring-boot-starter\2.4.4\spring-boot-starter-2.4.4.jar;C:\Users\98383\.m2\repository\org\springframework\boot\spring-boot-autoconfigure\2.4.4\spring-boot-autoconfigure-2.4.4.jar;C:\Users\98383\.m2\repository\org\springframework\boot\spring-boot-starter-logging\2.4.4\spring-boot-starter-logging-2.4.4.jar;C:\Users\98383\.m2\repository\ch\qos\logback\logback-classic\1.2.3\logback-classic-1.2.3.jar;C:\Users\98383\.m2\repository\ch\qos\logback\logback-core\1.2.3\logback-core-1.2.3.jar;C:\Users\98383\.m2\repository\org\apache\logging\log4j\log4j-to-slf4j\2.13.3\log4j-to-slf4j-2.13.3.jar;C:\Users\98383\.m2\repository\org\apache\logging\log4j\log4j-api\2.13.3\log4j-api-2.13.3.jar;C:\Users\98383\.m2\repository\org\slf4j\jul-to-slf4j\1.7.30\jul-to-slf4j-1.7.30.jar;C:\Users\98383\.m2\repository\jakarta\annotation\jakarta.annotation-api\1.3.5\jakarta.annotation-api-1.3.5.jar;C:\Users\98383\.m2\repository\org\yaml\snakeyaml\1.27\snakeyaml-1.27.jar;C:\Users\98383\.m2\repository\org\springframework\boot\spring-boot-starter-json\2.4.4\spring-boot-starter-json-2.4.4.jar;C:\Users\98383\.m2\repository\com\fasterxml\jackson\core\jackson-databind\2.11.4\jackson-databind-2.11.4.jar;C:\Users\98383\.m2\repository\com\fasterxml\jackson\core\jackson-annotations\2.11.4\jackson-annotations-2.11.4.jar;C:\Users\98383\.m2\repository\com\fasterxml\jackson\core\jackson-core\2.11.4\jackson-core-2.11.4.jar;C:\Users\98383\.m2\repository\com\fasterxml\jackson\datatype\jackson-datatype-jdk8\2.11.4\jackson-datatype-jdk8-2.11.4.jar;C:\Users\98383\.m2\repository\com\fasterxml\jackson\datatype\jackson-datatype-jsr310\2.11.4\jackson-datatype-jsr310-2.11.4.jar;C:\Users\98383\.m2\repository\com\fasterxml\jackson\module\jackson-module-parameter-names\2.11.4\jackson-module-parameter-names-2.11.4.jar;C:\Users\98383\.m2\repository\org\springframework\boot\spring-boot-starter-tomcat\2.4.4\spring-boot-starter-tomcat-2.4.4.jar;C:\Users\98383\.m2\repository\org\apache\tomcat\embed\tomcat-embed-core\9.0.44\tomcat-embed-core-9.0.44.jar;C:\Users\98383\.m2\repository\org\glassfish\jakarta.el\3.0.3\jakarta.el-3.0.3.jar;C:\Users\98383\.m2\repository\org\apache\tomcat\embed\tomcat-embed-websocket\9.0.44\tomcat-embed-websocket-9.0.44.jar;C:\Users\98383\.m2\repository\org\springframework\spring-web\5.3.5\spring-web-5.3.5.jar;C:\Users\98383\.m2\repository\org\springframework\spring-webmvc\5.3.5\spring-webmvc-5.3.5.jar;C:\Users\98383\.m2\repository\org\springframework\spring-aop\5.3.5\spring-aop-5.3.5.jar;C:\Users\98383\.m2\repository\org\springframework\spring-expression\5.3.5\spring-expression-5.3.5.jar;C:\Users\98383\.m2\repository\org\slf4j\slf4j-api\1.7.30\slf4j-api-1.7.30.jar;C:\Users\98383\.m2\repository\org\springframework\spring-core\5.3.5\spring-core-5.3.5.jar;C:\Users\98383\.m2\repository\org\springframework\spring-jcl\5.3.5\spring-jcl-5.3.5.jar;C:\Users\98383\.m2\repository\org\springframework\spring-beans\5.3.5\spring-beans-5.3.5.jar;C:\Users\98383\.m2\repository\org\springframework\boot\spring-boot\2.4.4\spring-boot-2.4.4.jar;C:\Users\98383\.m2\repository\org\springframework\spring-context\5.3.5\spring-context-5.3.5.jar com.louris.springboot.Application |
可以看到,@Autowired自动注入到RoleServiceImpl中,避免了手动获取bean并幅值。
@Autowired方法配置
1 | package com.louris.springboot.IoCImpl.impl; |
自动装配的歧义性(@Primary 和 @Qualifier)
例如同一个接口有两个不同的实现类,那么注入的时候会有歧义性。
1 | package com.louris.springboot.IoCImpl.impl; |
1 | package com.louris.springboot.IoCImpl.impl; |
1 | package com.louris.springboot.IoCImpl.controller; |
@Primary
通过该注解让Spring优先将被注解的类注入,消除歧义性
实现
1 | package com.louris.springboot.IoCImpl.impl; |
1 | package com.louris.springboot.IoCImpl.config; |
1 | package com.louris.springboot; |
运行结果
1 | "C:\Program Files\Java\jdk-14.0.2\bin\java.exe" -XX:TieredStopAtLevel=1 -noverify -Dspring.output.ansi.enabled=always -Dcom.sun.management.jmxremote -Dspring.jmx.enabled=true -Dspring.liveBeansView.mbeanDomain -Dspring.application.admin.enabled=true "-javaagent:D:\IntelliJ IDEA 2020.1.1\lib\idea_rt.jar=54761:D:\IntelliJ IDEA 2020.1.1\bin" -Dfile.encoding=UTF-8 -classpath D:\SpringBootCourse\001-springboot-first\target\classes;C:\Users\98383\.m2\repository\org\springframework\boot\spring-boot-starter-web\2.4.4\spring-boot-starter-web-2.4.4.jar;C:\Users\98383\.m2\repository\org\springframework\boot\spring-boot-starter\2.4.4\spring-boot-starter-2.4.4.jar;C:\Users\98383\.m2\repository\org\springframework\boot\spring-boot-autoconfigure\2.4.4\spring-boot-autoconfigure-2.4.4.jar;C:\Users\98383\.m2\repository\org\springframework\boot\spring-boot-starter-logging\2.4.4\spring-boot-starter-logging-2.4.4.jar;C:\Users\98383\.m2\repository\ch\qos\logback\logback-classic\1.2.3\logback-classic-1.2.3.jar;C:\Users\98383\.m2\repository\ch\qos\logback\logback-core\1.2.3\logback-core-1.2.3.jar;C:\Users\98383\.m2\repository\org\apache\logging\log4j\log4j-to-slf4j\2.13.3\log4j-to-slf4j-2.13.3.jar;C:\Users\98383\.m2\repository\org\apache\logging\log4j\log4j-api\2.13.3\log4j-api-2.13.3.jar;C:\Users\98383\.m2\repository\org\slf4j\jul-to-slf4j\1.7.30\jul-to-slf4j-1.7.30.jar;C:\Users\98383\.m2\repository\jakarta\annotation\jakarta.annotation-api\1.3.5\jakarta.annotation-api-1.3.5.jar;C:\Users\98383\.m2\repository\org\yaml\snakeyaml\1.27\snakeyaml-1.27.jar;C:\Users\98383\.m2\repository\org\springframework\boot\spring-boot-starter-json\2.4.4\spring-boot-starter-json-2.4.4.jar;C:\Users\98383\.m2\repository\com\fasterxml\jackson\core\jackson-databind\2.11.4\jackson-databind-2.11.4.jar;C:\Users\98383\.m2\repository\com\fasterxml\jackson\core\jackson-annotations\2.11.4\jackson-annotations-2.11.4.jar;C:\Users\98383\.m2\repository\com\fasterxml\jackson\core\jackson-core\2.11.4\jackson-core-2.11.4.jar;C:\Users\98383\.m2\repository\com\fasterxml\jackson\datatype\jackson-datatype-jdk8\2.11.4\jackson-datatype-jdk8-2.11.4.jar;C:\Users\98383\.m2\repository\com\fasterxml\jackson\datatype\jackson-datatype-jsr310\2.11.4\jackson-datatype-jsr310-2.11.4.jar;C:\Users\98383\.m2\repository\com\fasterxml\jackson\module\jackson-module-parameter-names\2.11.4\jackson-module-parameter-names-2.11.4.jar;C:\Users\98383\.m2\repository\org\springframework\boot\spring-boot-starter-tomcat\2.4.4\spring-boot-starter-tomcat-2.4.4.jar;C:\Users\98383\.m2\repository\org\apache\tomcat\embed\tomcat-embed-core\9.0.44\tomcat-embed-core-9.0.44.jar;C:\Users\98383\.m2\repository\org\glassfish\jakarta.el\3.0.3\jakarta.el-3.0.3.jar;C:\Users\98383\.m2\repository\org\apache\tomcat\embed\tomcat-embed-websocket\9.0.44\tomcat-embed-websocket-9.0.44.jar;C:\Users\98383\.m2\repository\org\springframework\spring-web\5.3.5\spring-web-5.3.5.jar;C:\Users\98383\.m2\repository\org\springframework\spring-webmvc\5.3.5\spring-webmvc-5.3.5.jar;C:\Users\98383\.m2\repository\org\springframework\spring-aop\5.3.5\spring-aop-5.3.5.jar;C:\Users\98383\.m2\repository\org\springframework\spring-expression\5.3.5\spring-expression-5.3.5.jar;C:\Users\98383\.m2\repository\org\slf4j\slf4j-api\1.7.30\slf4j-api-1.7.30.jar;C:\Users\98383\.m2\repository\org\springframework\spring-core\5.3.5\spring-core-5.3.5.jar;C:\Users\98383\.m2\repository\org\springframework\spring-jcl\5.3.5\spring-jcl-5.3.5.jar;C:\Users\98383\.m2\repository\org\springframework\spring-beans\5.3.5\spring-beans-5.3.5.jar;C:\Users\98383\.m2\repository\org\springframework\boot\spring-boot\2.4.4\spring-boot-2.4.4.jar;C:\Users\98383\.m2\repository\org\springframework\spring-context\5.3.5\spring-context-5.3.5.jar com.louris.springboot.Application |
@Qualifier
上面出现的歧义性,重要原因是SPring采用@Autowired是按照type进行注入的,那么如果采用名称查找的方式就可以消除歧义性了,故可以使用@Qualifier注解。
1 | package com.louris.springboot.IoCImpl.controller; |
该注解设计IoC容器的底层接口——BeanFactory
1 | Object getBean(String name) throws BeansException; |
装载带有参数的构造方法类
@Autowired和@Qualifer可以注解到参数
1 | package com.louris.springboot.IoCImpl.controller; |
使用@Bean装配Bean
以上都是通过@Component装配Bean,但是@Component只能注解在类上,不能注解到方法上。而对于Java而言,大部分的开发都需要引入第三方的包,而且往往并没有这些包的源码,这时候将无法为这些包的类加入@Component注解,让它们变为开发环境的Bean。
@Bean可以注解到方法之上,并且将方法返回的对象作为Spring的Bean,存放在IoC容器中。
1 | "dataSource") (name = |
注解自定义Bean的初始化和销毁方法
@Bean的配置项:
- name:是一个字符串数组,允许配置多个BeanName
- autowire:标志是否是一个引用的Bean对象,默认值是Autowire.NO
- initMethod:自定义初始化方法
- destroyMethod:自定义销毁方法
1 | "juiceMaker2", initMethod = "init", destroyMethod = "destroy") (name = |
1 | "C:\Program Files\Java\jdk-14.0.2\bin\java.exe" -XX:TieredStopAtLevel=1 -noverify -Dspring.output.ansi.enabled=always -Dcom.sun.management.jmxremote -Dspring.jmx.enabled=true -Dspring.liveBeansView.mbeanDomain -Dspring.application.admin.enabled=true "-javaagent:D:\IntelliJ IDEA 2020.1.1\lib\idea_rt.jar=54328:D:\IntelliJ IDEA 2020.1.1\bin" -Dfile.encoding=UTF-8 -classpath D:\SpringBootCourse\001-springboot-first\target\classes;C:\Users\98383\.m2\repository\org\springframework\boot\spring-boot-starter-web\2.4.4\spring-boot-starter-web-2.4.4.jar;C:\Users\98383\.m2\repository\org\springframework\boot\spring-boot-starter\2.4.4\spring-boot-starter-2.4.4.jar;C:\Users\98383\.m2\repository\org\springframework\boot\spring-boot-autoconfigure\2.4.4\spring-boot-autoconfigure-2.4.4.jar;C:\Users\98383\.m2\repository\org\springframework\boot\spring-boot-starter-logging\2.4.4\spring-boot-starter-logging-2.4.4.jar;C:\Users\98383\.m2\repository\ch\qos\logback\logback-classic\1.2.3\logback-classic-1.2.3.jar;C:\Users\98383\.m2\repository\ch\qos\logback\logback-core\1.2.3\logback-core-1.2.3.jar;C:\Users\98383\.m2\repository\org\apache\logging\log4j\log4j-to-slf4j\2.13.3\log4j-to-slf4j-2.13.3.jar;C:\Users\98383\.m2\repository\org\apache\logging\log4j\log4j-api\2.13.3\log4j-api-2.13.3.jar;C:\Users\98383\.m2\repository\org\slf4j\jul-to-slf4j\1.7.30\jul-to-slf4j-1.7.30.jar;C:\Users\98383\.m2\repository\jakarta\annotation\jakarta.annotation-api\1.3.5\jakarta.annotation-api-1.3.5.jar;C:\Users\98383\.m2\repository\org\yaml\snakeyaml\1.27\snakeyaml-1.27.jar;C:\Users\98383\.m2\repository\org\springframework\boot\spring-boot-starter-json\2.4.4\spring-boot-starter-json-2.4.4.jar;C:\Users\98383\.m2\repository\com\fasterxml\jackson\core\jackson-databind\2.11.4\jackson-databind-2.11.4.jar;C:\Users\98383\.m2\repository\com\fasterxml\jackson\core\jackson-annotations\2.11.4\jackson-annotations-2.11.4.jar;C:\Users\98383\.m2\repository\com\fasterxml\jackson\core\jackson-core\2.11.4\jackson-core-2.11.4.jar;C:\Users\98383\.m2\repository\com\fasterxml\jackson\datatype\jackson-datatype-jdk8\2.11.4\jackson-datatype-jdk8-2.11.4.jar;C:\Users\98383\.m2\repository\com\fasterxml\jackson\datatype\jackson-datatype-jsr310\2.11.4\jackson-datatype-jsr310-2.11.4.jar;C:\Users\98383\.m2\repository\com\fasterxml\jackson\module\jackson-module-parameter-names\2.11.4\jackson-module-parameter-names-2.11.4.jar;C:\Users\98383\.m2\repository\org\springframework\boot\spring-boot-starter-tomcat\2.4.4\spring-boot-starter-tomcat-2.4.4.jar;C:\Users\98383\.m2\repository\org\apache\tomcat\embed\tomcat-embed-core\9.0.44\tomcat-embed-core-9.0.44.jar;C:\Users\98383\.m2\repository\org\glassfish\jakarta.el\3.0.3\jakarta.el-3.0.3.jar;C:\Users\98383\.m2\repository\org\apache\tomcat\embed\tomcat-embed-websocket\9.0.44\tomcat-embed-websocket-9.0.44.jar;C:\Users\98383\.m2\repository\org\springframework\spring-web\5.3.5\spring-web-5.3.5.jar;C:\Users\98383\.m2\repository\org\springframework\spring-webmvc\5.3.5\spring-webmvc-5.3.5.jar;C:\Users\98383\.m2\repository\org\springframework\spring-aop\5.3.5\spring-aop-5.3.5.jar;C:\Users\98383\.m2\repository\org\springframework\spring-expression\5.3.5\spring-expression-5.3.5.jar;C:\Users\98383\.m2\repository\org\slf4j\slf4j-api\1.7.30\slf4j-api-1.7.30.jar;C:\Users\98383\.m2\repository\org\springframework\spring-core\5.3.5\spring-core-5.3.5.jar;C:\Users\98383\.m2\repository\org\springframework\spring-jcl\5.3.5\spring-jcl-5.3.5.jar;C:\Users\98383\.m2\repository\org\springframework\spring-beans\5.3.5\spring-beans-5.3.5.jar;C:\Users\98383\.m2\repository\org\springframework\boot\spring-boot\2.4.4\spring-boot-2.4.4.jar;C:\Users\98383\.m2\repository\org\springframework\spring-context\5.3.5\spring-context-5.3.5.jar com.louris.springboot.Application |
装配的混合使用
一般本项目中的类通过注解注入,而外部包的类则通过XML进行bean注入。
实例
1 | package com.louris.springboot.IoCImpl.service; |
1 | package com.louris.springboot.IoCImpl.impl; |
1 | package com.louris.springboot.IoCImpl.config; |
1 |
|
1 | <!-- dbcp数据源 --> |
1 | package com.louris.springboot; |
1 | "C:\Program Files\Java\jdk-14.0.2\bin\java.exe" -XX:TieredStopAtLevel=1 -noverify -Dspring.output.ansi.enabled=always -Dcom.sun.management.jmxremote -Dspring.jmx.enabled=true -Dspring.liveBeansView.mbeanDomain -Dspring.application.admin.enabled=true "-javaagent:D:\IntelliJ IDEA 2020.1.1\lib\idea_rt.jar=53592:D:\IntelliJ IDEA 2020.1.1\bin" -Dfile.encoding=UTF-8 -classpath D:\SpringBootCourse\001-springboot-first\target\classes;C:\Users\98383\.m2\repository\org\springframework\boot\spring-boot-starter-web\2.4.4\spring-boot-starter-web-2.4.4.jar;C:\Users\98383\.m2\repository\org\springframework\boot\spring-boot-starter\2.4.4\spring-boot-starter-2.4.4.jar;C:\Users\98383\.m2\repository\org\springframework\boot\spring-boot-autoconfigure\2.4.4\spring-boot-autoconfigure-2.4.4.jar;C:\Users\98383\.m2\repository\org\springframework\boot\spring-boot-starter-logging\2.4.4\spring-boot-starter-logging-2.4.4.jar;C:\Users\98383\.m2\repository\ch\qos\logback\logback-classic\1.2.3\logback-classic-1.2.3.jar;C:\Users\98383\.m2\repository\ch\qos\logback\logback-core\1.2.3\logback-core-1.2.3.jar;C:\Users\98383\.m2\repository\org\apache\logging\log4j\log4j-to-slf4j\2.13.3\log4j-to-slf4j-2.13.3.jar;C:\Users\98383\.m2\repository\org\apache\logging\log4j\log4j-api\2.13.3\log4j-api-2.13.3.jar;C:\Users\98383\.m2\repository\org\slf4j\jul-to-slf4j\1.7.30\jul-to-slf4j-1.7.30.jar;C:\Users\98383\.m2\repository\jakarta\annotation\jakarta.annotation-api\1.3.5\jakarta.annotation-api-1.3.5.jar;C:\Users\98383\.m2\repository\org\yaml\snakeyaml\1.27\snakeyaml-1.27.jar;C:\Users\98383\.m2\repository\org\springframework\boot\spring-boot-starter-json\2.4.4\spring-boot-starter-json-2.4.4.jar;C:\Users\98383\.m2\repository\com\fasterxml\jackson\core\jackson-databind\2.11.4\jackson-databind-2.11.4.jar;C:\Users\98383\.m2\repository\com\fasterxml\jackson\core\jackson-annotations\2.11.4\jackson-annotations-2.11.4.jar;C:\Users\98383\.m2\repository\com\fasterxml\jackson\core\jackson-core\2.11.4\jackson-core-2.11.4.jar;C:\Users\98383\.m2\repository\com\fasterxml\jackson\datatype\jackson-datatype-jdk8\2.11.4\jackson-datatype-jdk8-2.11.4.jar;C:\Users\98383\.m2\repository\com\fasterxml\jackson\datatype\jackson-datatype-jsr310\2.11.4\jackson-datatype-jsr310-2.11.4.jar;C:\Users\98383\.m2\repository\com\fasterxml\jackson\module\jackson-module-parameter-names\2.11.4\jackson-module-parameter-names-2.11.4.jar;C:\Users\98383\.m2\repository\org\springframework\boot\spring-boot-starter-tomcat\2.4.4\spring-boot-starter-tomcat-2.4.4.jar;C:\Users\98383\.m2\repository\org\apache\tomcat\embed\tomcat-embed-core\9.0.44\tomcat-embed-core-9.0.44.jar;C:\Users\98383\.m2\repository\org\glassfish\jakarta.el\3.0.3\jakarta.el-3.0.3.jar;C:\Users\98383\.m2\repository\org\apache\tomcat\embed\tomcat-embed-websocket\9.0.44\tomcat-embed-websocket-9.0.44.jar;C:\Users\98383\.m2\repository\org\springframework\spring-web\5.3.5\spring-web-5.3.5.jar;C:\Users\98383\.m2\repository\org\springframework\spring-webmvc\5.3.5\spring-webmvc-5.3.5.jar;C:\Users\98383\.m2\repository\org\springframework\spring-aop\5.3.5\spring-aop-5.3.5.jar;C:\Users\98383\.m2\repository\org\springframework\spring-expression\5.3.5\spring-expression-5.3.5.jar;C:\Users\98383\.m2\repository\org\slf4j\slf4j-api\1.7.30\slf4j-api-1.7.30.jar;C:\Users\98383\.m2\repository\org\springframework\spring-core\5.3.5\spring-core-5.3.5.jar;C:\Users\98383\.m2\repository\org\springframework\spring-jcl\5.3.5\spring-jcl-5.3.5.jar;C:\Users\98383\.m2\repository\org\springframework\spring-beans\5.3.5\spring-beans-5.3.5.jar;C:\Users\98383\.m2\repository\org\springframework\boot\spring-boot\2.4.4\spring-boot-2.4.4.jar;C:\Users\98383\.m2\repository\org\springframework\spring-context\5.3.5\spring-context-5.3.5.jar;C:\Users\98383\.m2\repository\org\apache\commons\commons-dbcp2\2.8.0\commons-dbcp2-2.8.0.jar;C:\Users\98383\.m2\repository\org\apache\commons\commons-pool2\2.9.0\commons-pool2-2.9.0.jar;C:\Users\98383\.m2\repository\mysql\mysql-connector-java\8.0.23\mysql-connector-java-8.0.23.jar;C:\Users\98383\.m2\repository\com\sun\activation\jakarta.activation\1.2.2\jakarta.activation-1.2.2.jar com.louris.springboot.Application |
加载多个配置文件
1 | "com.louris.springboot.IoCImpl.beans", (basePackages = { |
有多个XML配置文件,希望通过其中一个XML文件引入其他的XML文件
1 | <import resource="spring-dataSource.xml"/> |
或通过context:component-scan替换注解@Component
1 |
|
使用Profile
在软件开发过程中,可能开发人员使用一套环境,而测试人员能使用另一套环境,而这两台系统的数据库是不一样的,毕竟测试人员也需要花费很多时间去构建测试数据,可不想老是被开发人员修改那些测试数据,这样就有了在不同环境中进行切换的需求了。
使用注解@Profile配置
配置两个数据库连接池,一个用于开发(dev),一个用于测试(test)。
1 | package com.louris.springboot.IoCImpl.profile; |
使用XML定义Profile
(1)配置文件的profile属性
该属性会导致一个配置文件所有的Bean都放在dev的Profile下;
1 |
|
(2)配置文件的beans元素的profile属性
1 |
|
启动Profile
- application.properties核心配置文件
- application-dev.properties开发环境核心配置文件
- application-test.properties测试环境核心配置文件
1 | spring.profiles.active=dev |
1 | package com.louris.springboot.IoCImpl.profile; |
1 | "C:\Program Files\Java\jdk-14.0.2\bin\java.exe" -XX:TieredStopAtLevel=1 -noverify -Dspring.output.ansi.enabled=always -Dcom.sun.management.jmxremote -Dspring.jmx.enabled=true -Dspring.liveBeansView.mbeanDomain -Dspring.application.admin.enabled=true "-javaagent:D:\IntelliJ IDEA 2020.1.1\lib\idea_rt.jar=65000:D:\IntelliJ IDEA 2020.1.1\bin" -Dfile.encoding=UTF-8 -classpath D:\SpringBootCourse\001-springboot-first\target\classes;C:\Users\98383\.m2\repository\org\springframework\boot\spring-boot-starter-web\2.4.4\spring-boot-starter-web-2.4.4.jar;C:\Users\98383\.m2\repository\org\springframework\boot\spring-boot-starter\2.4.4\spring-boot-starter-2.4.4.jar;C:\Users\98383\.m2\repository\org\springframework\boot\spring-boot-autoconfigure\2.4.4\spring-boot-autoconfigure-2.4.4.jar;C:\Users\98383\.m2\repository\org\springframework\boot\spring-boot-starter-logging\2.4.4\spring-boot-starter-logging-2.4.4.jar;C:\Users\98383\.m2\repository\ch\qos\logback\logback-classic\1.2.3\logback-classic-1.2.3.jar;C:\Users\98383\.m2\repository\ch\qos\logback\logback-core\1.2.3\logback-core-1.2.3.jar;C:\Users\98383\.m2\repository\org\apache\logging\log4j\log4j-to-slf4j\2.13.3\log4j-to-slf4j-2.13.3.jar;C:\Users\98383\.m2\repository\org\apache\logging\log4j\log4j-api\2.13.3\log4j-api-2.13.3.jar;C:\Users\98383\.m2\repository\org\slf4j\jul-to-slf4j\1.7.30\jul-to-slf4j-1.7.30.jar;C:\Users\98383\.m2\repository\jakarta\annotation\jakarta.annotation-api\1.3.5\jakarta.annotation-api-1.3.5.jar;C:\Users\98383\.m2\repository\org\yaml\snakeyaml\1.27\snakeyaml-1.27.jar;C:\Users\98383\.m2\repository\org\springframework\boot\spring-boot-starter-json\2.4.4\spring-boot-starter-json-2.4.4.jar;C:\Users\98383\.m2\repository\com\fasterxml\jackson\core\jackson-databind\2.11.4\jackson-databind-2.11.4.jar;C:\Users\98383\.m2\repository\com\fasterxml\jackson\core\jackson-annotations\2.11.4\jackson-annotations-2.11.4.jar;C:\Users\98383\.m2\repository\com\fasterxml\jackson\core\jackson-core\2.11.4\jackson-core-2.11.4.jar;C:\Users\98383\.m2\repository\com\fasterxml\jackson\datatype\jackson-datatype-jdk8\2.11.4\jackson-datatype-jdk8-2.11.4.jar;C:\Users\98383\.m2\repository\com\fasterxml\jackson\datatype\jackson-datatype-jsr310\2.11.4\jackson-datatype-jsr310-2.11.4.jar;C:\Users\98383\.m2\repository\com\fasterxml\jackson\module\jackson-module-parameter-names\2.11.4\jackson-module-parameter-names-2.11.4.jar;C:\Users\98383\.m2\repository\org\springframework\boot\spring-boot-starter-tomcat\2.4.4\spring-boot-starter-tomcat-2.4.4.jar;C:\Users\98383\.m2\repository\org\apache\tomcat\embed\tomcat-embed-core\9.0.44\tomcat-embed-core-9.0.44.jar;C:\Users\98383\.m2\repository\org\glassfish\jakarta.el\3.0.3\jakarta.el-3.0.3.jar;C:\Users\98383\.m2\repository\org\apache\tomcat\embed\tomcat-embed-websocket\9.0.44\tomcat-embed-websocket-9.0.44.jar;C:\Users\98383\.m2\repository\org\springframework\spring-web\5.3.5\spring-web-5.3.5.jar;C:\Users\98383\.m2\repository\org\springframework\spring-webmvc\5.3.5\spring-webmvc-5.3.5.jar;C:\Users\98383\.m2\repository\org\springframework\spring-aop\5.3.5\spring-aop-5.3.5.jar;C:\Users\98383\.m2\repository\org\springframework\spring-expression\5.3.5\spring-expression-5.3.5.jar;C:\Users\98383\.m2\repository\org\slf4j\slf4j-api\1.7.30\slf4j-api-1.7.30.jar;C:\Users\98383\.m2\repository\org\springframework\spring-core\5.3.5\spring-core-5.3.5.jar;C:\Users\98383\.m2\repository\org\springframework\spring-jcl\5.3.5\spring-jcl-5.3.5.jar;C:\Users\98383\.m2\repository\org\springframework\spring-beans\5.3.5\spring-beans-5.3.5.jar;C:\Users\98383\.m2\repository\org\springframework\boot\spring-boot\2.4.4\spring-boot-2.4.4.jar;C:\Users\98383\.m2\repository\org\springframework\spring-context\5.3.5\spring-context-5.3.5.jar;C:\Users\98383\.m2\repository\org\apache\commons\commons-dbcp2\2.8.0\commons-dbcp2-2.8.0.jar;C:\Users\98383\.m2\repository\org\apache\commons\commons-pool2\2.9.0\commons-pool2-2.9.0.jar;C:\Users\98383\.m2\repository\mysql\mysql-connector-java\8.0.23\mysql-connector-java-8.0.23.jar;C:\Users\98383\.m2\repository\com\sun\activation\jakarta.activation\1.2.2\jakarta.activation-1.2.2.jar com.louris.springboot.Application |
获取ApplicationContext
自定义新建ApplicationContext
加载XML配置文件方式
1 | ClassPathXmlApplicationContext applicationContext = new ClassPathXmlApplicationContext("spring-config.xml"); |
加载注解配置类方式
1 | "com.louris.springboot.IoCImpl.beans"}) (basePackages = { |
1 | ApplicationContext applicationContext = new AnnotationConfigApplicationContext(PojoConfig.class); |
启动类方式
1 | ApplicationContext applicationContext = SpringApplication.run(Application.class, args); |
加载属性(properties)文件
1 | server.port=8081 |
使用注解方式加载属性文件
@PropertySource注解
- name: 字符串,配置这次属性配置的名称
- value:字符串数组,可以配置多个属性文件
- ignoreResourceNotFound:默认false,找不到对应配置文件会抛出异常
1
2
3
"classpath:application.properties", ignoreResourceNotFound = false) (value =
public class ProfileDataSource {}
1 | ApplicationContext applicationContext = new AnnotationConfigApplicationContext(PojoConfig.class); |
1 | "C:\Program Files\Java\jdk-14.0.2\bin\java.exe" -XX:TieredStopAtLevel=1 -noverify -Dspring.output.ansi.enabled=always -Dcom.sun.management.jmxremote -Dspring.jmx.enabled=true -Dspring.liveBeansView.mbeanDomain -Dspring.application.admin.enabled=true "-javaagent:D:\IntelliJ IDEA 2020.1.1\lib\idea_rt.jar=62587:D:\IntelliJ IDEA 2020.1.1\bin" -Dfile.encoding=UTF-8 -classpath D:\SpringBootCourse\001-springboot-first\target\classes;C:\Users\98383\.m2\repository\org\springframework\boot\spring-boot-starter-web\2.4.4\spring-boot-starter-web-2.4.4.jar;C:\Users\98383\.m2\repository\org\springframework\boot\spring-boot-starter\2.4.4\spring-boot-starter-2.4.4.jar;C:\Users\98383\.m2\repository\org\springframework\boot\spring-boot-autoconfigure\2.4.4\spring-boot-autoconfigure-2.4.4.jar;C:\Users\98383\.m2\repository\org\springframework\boot\spring-boot-starter-logging\2.4.4\spring-boot-starter-logging-2.4.4.jar;C:\Users\98383\.m2\repository\ch\qos\logback\logback-classic\1.2.3\logback-classic-1.2.3.jar;C:\Users\98383\.m2\repository\ch\qos\logback\logback-core\1.2.3\logback-core-1.2.3.jar;C:\Users\98383\.m2\repository\org\apache\logging\log4j\log4j-to-slf4j\2.13.3\log4j-to-slf4j-2.13.3.jar;C:\Users\98383\.m2\repository\org\apache\logging\log4j\log4j-api\2.13.3\log4j-api-2.13.3.jar;C:\Users\98383\.m2\repository\org\slf4j\jul-to-slf4j\1.7.30\jul-to-slf4j-1.7.30.jar;C:\Users\98383\.m2\repository\jakarta\annotation\jakarta.annotation-api\1.3.5\jakarta.annotation-api-1.3.5.jar;C:\Users\98383\.m2\repository\org\yaml\snakeyaml\1.27\snakeyaml-1.27.jar;C:\Users\98383\.m2\repository\org\springframework\boot\spring-boot-starter-json\2.4.4\spring-boot-starter-json-2.4.4.jar;C:\Users\98383\.m2\repository\com\fasterxml\jackson\core\jackson-databind\2.11.4\jackson-databind-2.11.4.jar;C:\Users\98383\.m2\repository\com\fasterxml\jackson\core\jackson-annotations\2.11.4\jackson-annotations-2.11.4.jar;C:\Users\98383\.m2\repository\com\fasterxml\jackson\core\jackson-core\2.11.4\jackson-core-2.11.4.jar;C:\Users\98383\.m2\repository\com\fasterxml\jackson\datatype\jackson-datatype-jdk8\2.11.4\jackson-datatype-jdk8-2.11.4.jar;C:\Users\98383\.m2\repository\com\fasterxml\jackson\datatype\jackson-datatype-jsr310\2.11.4\jackson-datatype-jsr310-2.11.4.jar;C:\Users\98383\.m2\repository\com\fasterxml\jackson\module\jackson-module-parameter-names\2.11.4\jackson-module-parameter-names-2.11.4.jar;C:\Users\98383\.m2\repository\org\springframework\boot\spring-boot-starter-tomcat\2.4.4\spring-boot-starter-tomcat-2.4.4.jar;C:\Users\98383\.m2\repository\org\apache\tomcat\embed\tomcat-embed-core\9.0.44\tomcat-embed-core-9.0.44.jar;C:\Users\98383\.m2\repository\org\glassfish\jakarta.el\3.0.3\jakarta.el-3.0.3.jar;C:\Users\98383\.m2\repository\org\apache\tomcat\embed\tomcat-embed-websocket\9.0.44\tomcat-embed-websocket-9.0.44.jar;C:\Users\98383\.m2\repository\org\springframework\spring-web\5.3.5\spring-web-5.3.5.jar;C:\Users\98383\.m2\repository\org\springframework\spring-webmvc\5.3.5\spring-webmvc-5.3.5.jar;C:\Users\98383\.m2\repository\org\springframework\spring-aop\5.3.5\spring-aop-5.3.5.jar;C:\Users\98383\.m2\repository\org\springframework\spring-expression\5.3.5\spring-expression-5.3.5.jar;C:\Users\98383\.m2\repository\org\slf4j\slf4j-api\1.7.30\slf4j-api-1.7.30.jar;C:\Users\98383\.m2\repository\org\springframework\spring-core\5.3.5\spring-core-5.3.5.jar;C:\Users\98383\.m2\repository\org\springframework\spring-jcl\5.3.5\spring-jcl-5.3.5.jar;C:\Users\98383\.m2\repository\org\springframework\spring-beans\5.3.5\spring-beans-5.3.5.jar;C:\Users\98383\.m2\repository\org\springframework\boot\spring-boot\2.4.4\spring-boot-2.4.4.jar;C:\Users\98383\.m2\repository\org\springframework\spring-context\5.3.5\spring-context-5.3.5.jar;C:\Users\98383\.m2\repository\org\apache\commons\commons-dbcp2\2.8.0\commons-dbcp2-2.8.0.jar;C:\Users\98383\.m2\repository\org\apache\commons\commons-pool2\2.9.0\commons-pool2-2.9.0.jar;C:\Users\98383\.m2\repository\mysql\mysql-connector-java\8.0.23\mysql-connector-java-8.0.23.jar;C:\Users\98383\.m2\repository\com\sun\activation\jakarta.activation\1.2.2\jakarta.activation-1.2.2.jar com.louris.springboot.Application |
@PropertySourcesPlaceholderConfigure + @Value注解
1 | package com.louris.springboot.IoCImpl.config; |
1 | package com.louris.springboot.IoCImpl.profile; |
使用XML方式加载属性文件
略过。
条件化装配Bean
比如根据属性文件中的数据源信息进行装配Bean,如果属性不完整,则会创建失败。
自定义条件类
1 | package com.louris.springboot.IoCImpl.config; |
@Conditional注解
1 | package com.louris.springboot.IoCImpl.profile; |
Bean的作用域
- 单例(singleton):它是默认的选项,在整个应用中,Spring只为其生成一个Bean的实例;
- 原型(prototype):当每次注入,或者通过Spring IoC容器获取Bean时,Spring都会为它创建一个新的实例;
- 会话(session):在Web应用中使用,就是在会话过程中Spring只创建一个实例;
- 请求(request):在Web应用中使用,就是一个请求中Spring会创建一个实例;
1 | package com.louris.springboot.IoCImpl.beans.pojo; |
1 | ApplicationContext applicationContext = new AnnotationConfigApplicationContext(PojoConfig.class); |
1 | "C:\Program Files\Java\jdk-14.0.2\bin\java.exe" -XX:TieredStopAtLevel=1 -noverify -Dspring.output.ansi.enabled=always -Dcom.sun.management.jmxremote -Dspring.jmx.enabled=true -Dspring.liveBeansView.mbeanDomain -Dspring.application.admin.enabled=true "-javaagent:D:\IntelliJ IDEA 2020.1.1\lib\idea_rt.jar=59884:D:\IntelliJ IDEA 2020.1.1\bin" -Dfile.encoding=UTF-8 -classpath D:\SpringBootCourse\001-springboot-first\target\classes;C:\Users\98383\.m2\repository\org\springframework\boot\spring-boot-starter-web\2.4.4\spring-boot-starter-web-2.4.4.jar;C:\Users\98383\.m2\repository\org\springframework\boot\spring-boot-starter\2.4.4\spring-boot-starter-2.4.4.jar;C:\Users\98383\.m2\repository\org\springframework\boot\spring-boot-autoconfigure\2.4.4\spring-boot-autoconfigure-2.4.4.jar;C:\Users\98383\.m2\repository\org\springframework\boot\spring-boot-starter-logging\2.4.4\spring-boot-starter-logging-2.4.4.jar;C:\Users\98383\.m2\repository\ch\qos\logback\logback-classic\1.2.3\logback-classic-1.2.3.jar;C:\Users\98383\.m2\repository\ch\qos\logback\logback-core\1.2.3\logback-core-1.2.3.jar;C:\Users\98383\.m2\repository\org\apache\logging\log4j\log4j-to-slf4j\2.13.3\log4j-to-slf4j-2.13.3.jar;C:\Users\98383\.m2\repository\org\apache\logging\log4j\log4j-api\2.13.3\log4j-api-2.13.3.jar;C:\Users\98383\.m2\repository\org\slf4j\jul-to-slf4j\1.7.30\jul-to-slf4j-1.7.30.jar;C:\Users\98383\.m2\repository\jakarta\annotation\jakarta.annotation-api\1.3.5\jakarta.annotation-api-1.3.5.jar;C:\Users\98383\.m2\repository\org\yaml\snakeyaml\1.27\snakeyaml-1.27.jar;C:\Users\98383\.m2\repository\org\springframework\boot\spring-boot-starter-json\2.4.4\spring-boot-starter-json-2.4.4.jar;C:\Users\98383\.m2\repository\com\fasterxml\jackson\core\jackson-databind\2.11.4\jackson-databind-2.11.4.jar;C:\Users\98383\.m2\repository\com\fasterxml\jackson\core\jackson-annotations\2.11.4\jackson-annotations-2.11.4.jar;C:\Users\98383\.m2\repository\com\fasterxml\jackson\core\jackson-core\2.11.4\jackson-core-2.11.4.jar;C:\Users\98383\.m2\repository\com\fasterxml\jackson\datatype\jackson-datatype-jdk8\2.11.4\jackson-datatype-jdk8-2.11.4.jar;C:\Users\98383\.m2\repository\com\fasterxml\jackson\datatype\jackson-datatype-jsr310\2.11.4\jackson-datatype-jsr310-2.11.4.jar;C:\Users\98383\.m2\repository\com\fasterxml\jackson\module\jackson-module-parameter-names\2.11.4\jackson-module-parameter-names-2.11.4.jar;C:\Users\98383\.m2\repository\org\springframework\boot\spring-boot-starter-tomcat\2.4.4\spring-boot-starter-tomcat-2.4.4.jar;C:\Users\98383\.m2\repository\org\apache\tomcat\embed\tomcat-embed-core\9.0.44\tomcat-embed-core-9.0.44.jar;C:\Users\98383\.m2\repository\org\glassfish\jakarta.el\3.0.3\jakarta.el-3.0.3.jar;C:\Users\98383\.m2\repository\org\apache\tomcat\embed\tomcat-embed-websocket\9.0.44\tomcat-embed-websocket-9.0.44.jar;C:\Users\98383\.m2\repository\org\springframework\spring-web\5.3.5\spring-web-5.3.5.jar;C:\Users\98383\.m2\repository\org\springframework\spring-webmvc\5.3.5\spring-webmvc-5.3.5.jar;C:\Users\98383\.m2\repository\org\springframework\spring-aop\5.3.5\spring-aop-5.3.5.jar;C:\Users\98383\.m2\repository\org\springframework\spring-expression\5.3.5\spring-expression-5.3.5.jar;C:\Users\98383\.m2\repository\org\slf4j\slf4j-api\1.7.30\slf4j-api-1.7.30.jar;C:\Users\98383\.m2\repository\org\springframework\spring-core\5.3.5\spring-core-5.3.5.jar;C:\Users\98383\.m2\repository\org\springframework\spring-jcl\5.3.5\spring-jcl-5.3.5.jar;C:\Users\98383\.m2\repository\org\springframework\spring-beans\5.3.5\spring-beans-5.3.5.jar;C:\Users\98383\.m2\repository\org\springframework\boot\spring-boot\2.4.4\spring-boot-2.4.4.jar;C:\Users\98383\.m2\repository\org\springframework\spring-context\5.3.5\spring-context-5.3.5.jar;C:\Users\98383\.m2\repository\org\apache\commons\commons-dbcp2\2.8.0\commons-dbcp2-2.8.0.jar;C:\Users\98383\.m2\repository\org\apache\commons\commons-pool2\2.9.0\commons-pool2-2.9.0.jar;C:\Users\98383\.m2\repository\mysql\mysql-connector-java\8.0.23\mysql-connector-java-8.0.23.jar;C:\Users\98383\.m2\repository\com\sun\activation\jakarta.activation\1.2.2\jakarta.activation-1.2.2.jar com.louris.springboot.Application |
使用Spring表达式(Spring EL)
Spring还提供了更灵活的注入方式,那就是Spring表达式。
Spring EL 相关的类
- ExpressionParser
- TemplateAwareExpressionParser(abstract)
- InternalSpelExpressionParser
- SpelExpressionParser
表达式创建对象,调用对象方法
1 | //表达式解析器 |
1 | hello world |
Bean的属性和方法
@Value
- 直接字符串表示赋值
- #{}表示Spring EL
- ${}表示使用配置文件内的属性
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47package com.louris.springboot.IoCImpl.beans.pojo;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.beans.factory.config.ConfigurableBeanFactory;
import org.springframework.context.annotation.Scope;
import org.springframework.stereotype.Component;
"role") (value =
//@Scope(value = "prototype")
public class Role {
"#{1}") (
private Long id;
"#{'role_name_1'}") (
private String roleName;
"#{'role_note_1'}") (
private String note;
public Role(){}
public Role(Long id, String roleName, String note){
this.id = id;
this.roleName = roleName;
this.note = note;
}
public void setId(Long id) {
this.id = id;
}
public void setRoleName(String roleName) {
this.roleName = roleName;
}
public void setNote(String note) {
this.note = note;
}
public String getRoleName() {
return roleName;
}
public String getNote() {
return note;
}
public Long getId() {
return id;
}
}1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public class ElBean{
//同过beanName获取bean,然后注入
"#{role}") (
private Role role;
//获取bean的属性id
"#{role.id}") (
private Long id;
//调用bean的getNote方法,获取角色名称
"#{role.getNote()?.toString()}") //?表示当getNote()不为空,则调用toString()方法 (
private String note;
}
使用类的静态常量和方法
1 | "#{T(Math).PI}") ( |
Spring EL运算
1 | "#{role.id + 1}") ( |
面向切面编程
一个简单的约定游戏
约定规则
1 | package com.louris.springboot.aop.interceptors; |
1 | package com.louris.springboot; |
1 | package com.louris.springboot; |
1 | RoleService1 roleService1 = new RoleServiceImpl1(); |
1 | 准备打印角色信息 |
Spring AOP的基本概念
AOP的概念和使用原因
现实中有一些内容并不是面向对象(OOP)可以解决的,比如数据库事务,又如电商网站购物需要经过交易系统、财务系统,对于交易系统存在一个交易记录的对象,而财务系统则存在账户的信息对象。从这个角度而言,我们需要对交易记录和账户操作形成一个统一的事务管理。交易和账户的事务,要么全部成功,要么全部失败。
JDBC数据库事务
这里购买交易的产品和购买记录都在try…catch…finally…语句中,首先需要自己去获取对应的映射器,而业务流程中穿插着事务的提交和回滚,也就是如果交易可以成功,那么就会提交事务,交易如果发生异常,那么就回滚事务,最后在finally语句中会关闭SqlSession所持有的功能。
但这不是一个很好的设计。
1 | public void savePurchaseRecord(Long productId, PurchaseRecord record){ |
Spring AOP对应数据库事务
该代码除了一个注解@Transactional,没有任何关于打开或者关闭数据库资源的代码,更没有任何提交或者回滚数据库事务的代码,但是它却能完成上一节的全部功能。
注意,这段代码更简介,也更容易维护,主要都集中在业务处理上,而不是数据库事务和资源管控上,这就是AOP的魅力;
1 |
|
一个正常的SQL步骤:
- 打开通过数据库连接池获取数据库连接资源,并做一定的设置工作;
- 执行对应的SQL语句,对数据进行操作;
- 如果SQL执行过程中发生异常,回滚事务;
- 如果SQL执行过程中没有发生异常,最后提交事务;
- 到最后的阶段,需要关闭一些连接资源。
该过程与之前约定游戏的流程十分接近,也就说作为AOP,完全可以根据这个流程做一定的封装,然后通过动态代理技术,将代码组织入对应的流程环节。
流程如下:
- 打开获取数据库连接在before方法中完成;
- 执行SQL,按照读者的逻辑会采用反射的机制调用;
- 如果发生异常,则回滚事务;如果没有发生异常,则提交事务,然后关闭数据库连接资源;
如果一个AOP框架不需要我们去实现流程中的方法,而是在流程中提供一些通用的方法,并可以通过一定的配置满足各种功能,比如AOP框架帮助你完成了获取数据库,你就不需要知道如何获取数据库连接功能了,此外再增加一些关于事务的重要约定:
- 在方法标注为@Transactional时,则方法启用数据库事务功能;
- 在默认的情况下,如果原有方法出现异常,则回滚事务;如果没有发生异常,那么就提交事务,这样真个事务管理AOP就完成了整个流程,无须开发者编写任何代码去实现;
- 最后关闭数据库资源,这点也比较通用,这里AOP框架也帮你完成它。
这是使用最广的执行流程,符合约定优于配置的开发原则。这些约定的方法加入默认实现后,开发者需要做的只是执行SQL这步而已。大部分情况下,只需要使用默认的约定即可,或者进行一些特定的配置,来完成所需的功能,这样开发者就可以更为关注业务开发,而不是资源控制、事务异常处理,这些AOP框架都可以完成。
面向切面编程的术语
1、切面(Aspect)
切面就是在一个怎样的环境中工作,比如上一节,数据库的事务直接贯穿整个代码层面,这就是一个切面,它能够在被代理对象的方法之前、之后,产生异常或者正常返回后切入你的代码,甚至代替原来被代理对象的方法。
2、通知(Adice)
通知是切面开启后,切面的方法。它根据在代理对象真实方法调用前、后的顺序和逻辑区分,它和约定游戏的例子里的拦截器的方法十分接近。
- 前置通知(before):在动态代理反射原有对象方法或者执行环绕通知前执行的通知功能;
- 后置通知(after):在动态代理反射原有对象方法或者执行环绕通知后执行的通知后执行的通知功能。无论是否抛出异常,它都会被执行;
- 返回通知(afterReturning):在动态代理反射原有对象方法或者执行环绕通知后执行的通知功能;
- 异常通知(afterThrowing):在动态代理反射原有对象方法或者执行环绕通知产生异常后执行的通知功能;
- 环绕通知(aroundThrowing):在动态代理中,它可以取代当前被拦截对象的方法,通知参数或反射调用被拦截对象的方法。
3、引入(Introduction)
引入允许我们在现有的类里添加自定义的类和方法。
4、切点(Pointcut)
在动态代理中,被切面拦截的方法就是一个切点,切面将可以将其切点和被拦截的方法按照一定的逻辑织入到约定流程当中。
5、连接点(join point)
连接点是一个判断条件,由它可以指定哪些是切点。对于指定的切点,Spring会生成代理对象去使用对应的切面对其拦截,否则就不会拦截它。
6、织入(Weaving)
织入是一个生成代理对象的过程。实际代理的方法分为静态代理和动态代理。静态代理是在编译class文件时生成的代码逻辑,但是在Spring中并不适用这样的方式。
Spring对AOP的支持
AOP并不是Spring框架特有的,Spring只是支持AOP编程的框架之一。每一个框架对AOP的支持各有特点,有些AOP能够对方法的参数进行拦截,有些AOP对方法进行拦截。
而Spring AOP是一种基于方法拦截的AOP,换句话说Spring只能支持方法拦截的AOP,Spring中有4种方式去实现AOP的拦截功能:
- 使用ProxyFactoryBean和对应的接口实现AOP;
- 使用XML配置AOP;
- 使用@AspectJ注解驱动切面;
- 使用AspectJ注入切面
使用@AspectJ注解开发Spring AOP
注解使用了对应的正则式,这些正则式是连接点的问题,也就是要告诉Spring AOP,需要拦截什么对象的什么方法,为此我们要学习连接点的知识。
1 | <dependency> |
选择切点
1 | package com.louris.springboot.IoCImpl.service; |
1 | package com.louris.springboot.IoCImpl.impl; |
创建切面
1 | package com.louris.springboot.aspects; |
注解 | 通知 | 备注 |
---|---|---|
@Before | 在被代理对象的方法前先调用 | 前置通知 |
@Around | 将被代理对象的方法封装起来,并用环绕通知取代它 | 环绕通知,它将覆盖原有方法,但是允许你通过反射调用原有方法,后续会讨论 |
@After | 在被代理对象的方法后调用 | 后置通知 |
@AfterReturning | 在被代理对象的方法正常返回后调用 | 返回通知,要求被代理对象的方法执行过程中没有发生异常 |
@AfterThrowing | 在被代理对象的方法抛出异常后调用 | 异常通知,要求被代理对象的方法执行过程中产生异常 |
连接点
1 | execution(* com.louris.springboot.IoCImpl.impl.RoleServiceImpl1.printRole(..)) |
- execution:代表执行方法的时候触发
- *:代表任意返回类型的方法
- com.louris.springboot.IoCImpl.impl.RoleServiceImpl1:代表类的全限定名
- printRole:被拦截方法名称
- (..):任意的参数
AspectJ指示器 | 描述 |
---|---|
arg() | 限制连接点匹配参数为指定类型的方法 |
@args() | 限制连接点匹配参数为指定注解标注的执行方法 |
execution | 用于匹配连接点的执行方法,这是最常用的匹配,可以通过类似上面的正则式进行匹配 |
this() | 限制连接点匹配AOP代理的Bean,引用为指定类型的类 |
target | 限制连接点匹配被代理对象为指定的类型 |
@target() | 限制连接点匹配特定的执行对象,这些对象要符合指定的注解类型 |
within() | 限制连接点匹配指定的包 |
@within() | 限制连接点匹配指定的类型 |
@annotation | 限制匹配带有指定注解的连接点 |
通过within限制,Spring只会拿到com.louris.springboot.IoCImpl.impl包下面的类的printRole方法作为切点了。
1 | "execution(* com.louris.springboot.*.*.*.printRole(..)) (value = |
1 | package com.louris.springboot.aspects; |
测试AOP
注解实现
1 | package com.louris.springboot.IoCImpl.config; |
XML实现
1 |
|
测试
1 | ApplicationContext applicationContext = new AnnotationConfigApplicationContext(AopConfig.class); |
运行结果
1 | before .... |
环绕通知
环绕通知是Spring AOP中最强大的通知,它可以同时实现前置通知和后置通知。它保留了调度被代理对象原有方法的功能,所以它既强大,又灵活。但是由于强大,它的可控性不那么强,如果不需要大量改变业务逻辑,一般而言并不需要使用它。
1 | "print()") (value = |
1 | around before .... |
织入
织入是生成代理对象的过程,在上述的代码中,切点方法所在的类都是拥有接口的类,而事实上即使没有接口,Spring也能提供AOP的功能,所以是否拥有接口不是使用Spring AOP的一个强制要求。
- 当类的实现存在接口时,Spring将提供JDK动态代理,从而织入各个通知;
- 当类不存在接口的时候没有办法使用JDK动态代理,Spring会采用CGLIB来生成代理对象。
给通知传递参数
1 | public interface RoleService1 { |
1 |
|
1 | "execution(* com.louris.springboot.IoCImpl.impl.RoleServiceImpl1.printRole(..))" + (value = |
运行结果
1 | around before .... |
引入
Spring AOP只是通过动态代理技术,把各类通知织入到它所约定的流程当中,而事实上,有时候我们希望通过引入其他类的方法来得到更好的实现,这时就可以引入其他的方法了。
1 | package com.louris.springboot.IoCImpl.service; |
1 | package com.louris.springboot.IoCImpl.impl; |
注解@DeclareParents的配置:
- value 表示对对应类进行增强,也就是在对应类中引入一个新的接口
- defaultImpl 代表其默认的实现类,这里是RoleVerifierImpl
1 | package com.louris.springboot.aspects; |
1 | ApplicationContext applicationContext = new AnnotationConfigApplicationContext(AopConfig.class); |
1 | role is not null! |
可以看到通过Spirng AOP后,使用强制转换后就可以把roleService转化为roleVerifier接口对象,然后就可以使用verify方法。而RoleVerifer调用的方法verify,显然它就是通过RoleVerifierImpl来实现的。
其原理是通过Spring AOP依赖于动态代理来实现,生成动态代理对象是通过类似于下面这行代码来实现的。
1 | return Proxy.newProxyInstance(obj.getClass().getClassLoader(), obj.getClass().getInterfaces(), _this); |
obj.getClass().getInterfaces()意味着代理对象挂在多个接口之下,换句话说,只要Spring AOP让代理对象挂到RoleService和RoleVerifier两个接口之下,那么就可以把对应的Bean通过强制转换,让其在RoleService和RoleVerifier之间相互转换了。
同样的如果RoleServiceImpl没有接口,那么它会使用CGLIB动态代理,使用增强着(Enhancer)也会有一个Interfaces的属性,允许代理对象挂到对应的多个接口下,于是也可以按JDK动态代理那样使得对象可以在多个接口之间相互转换。
使用XML配置开发Spring AOP
切点不变,即RoleService和RoleServiceImpl不变
1 | package com.louris.springboot.aspects; |
1 |
|
1 | ApplicationContext applicationContext = new ClassPathXmlApplicationContext("spring-aop.xml"); |
经典Spring AOP应用程序
略过
多个切面
1 | package com.louris.springboot.IoCImpl.service; |
1 | package com.louris.springboot.IoCImpl.impl; |
1 | package com.louris.springboot.aspects; |
1 | package com.louris.springboot.aspects; |
1 | package com.louris.springboot.aspects; |
1 | package com.louris.springboot.IoCImpl.config; |
1 | ApplicationContext applicationContext = new AnnotationConfigApplicationContext(MultiConfig.class); |
多切面排序
运行结果:
(1)默认顺序:应该是切面类名字符排序
1 | before 1 .... |
(2)@Order注解指定顺序
1 | before 3 .... |
(3)其他方式:
1 |
|
1 | <aop:aspect ref="aspect1" order="1"> |
Spring和数据库编程
传统的JDBC代码的弊端
执行一条简单的SQL,其过程也不简单,太多try…catch…finally
1 | public Role getRole(Long id){ |
配置数据库资源
使用简单数据库配置
1 | <bean id="dataSource" class="org.springframework.jdbc.datasource.SimpleDriverDataSource"> |
使用第三方数据库连接池
1 | <bean id="dataSource" class="org.apache.commons.dbcp2.BasicDataSource"> |
使用JNDI数据库连接池
1 | <bean id="dataSource" class="org.springframework.jndi.JndiObjectFactoryBean"> |
JDBC代码失控的解决方案——JdbcTemplate
JdbcTemplate是Spring针对JDBC代码失控提供的解决方案,严格来说,它本身也不算成功。但是无论如何jdbcTemplate的方案也体现了Spring框架的主导思想之一:给予常用技术提供模板化的编程,减少了开发者的工作量。
JdbcTemplate的增删改查
1 | <bean id="jdbcTemplate" class="org.springframework.jdbc.core.JdbcTemplate"> |
1 | package com.louris.springboot.utils; |
1 | ApplicationContext applicationContext = new ClassPathXmlApplicationContext("spring-dataSource.xml"); |
运行结果
1 | "C:\Program Files\Java\jdk-14.0.2\bin\java.exe" -XX:TieredStopAtLevel=1 -noverify -Dspring.output.ansi.enabled=always -Dcom.sun.management.jmxremote -Dspring.jmx.enabled=true -Dspring.liveBeansView.mbeanDomain -Dspring.application.admin.enabled=true "-javaagent:D:\IntelliJ IDEA 2020.1.1\lib\idea_rt.jar=65017:D:\IntelliJ IDEA 2020.1.1\bin" -Dfile.encoding=UTF-8 -classpath D:\SpringBootCourse\001-springboot-first\target\classes;C:\Users\98383\.m2\repository\org\springframework\boot\spring-boot-starter-web\2.4.4\spring-boot-starter-web-2.4.4.jar;C:\Users\98383\.m2\repository\org\springframework\boot\spring-boot-starter\2.4.4\spring-boot-starter-2.4.4.jar;C:\Users\98383\.m2\repository\org\springframework\boot\spring-boot-autoconfigure\2.4.4\spring-boot-autoconfigure-2.4.4.jar;C:\Users\98383\.m2\repository\org\springframework\boot\spring-boot-starter-logging\2.4.4\spring-boot-starter-logging-2.4.4.jar;C:\Users\98383\.m2\repository\ch\qos\logback\logback-classic\1.2.3\logback-classic-1.2.3.jar;C:\Users\98383\.m2\repository\ch\qos\logback\logback-core\1.2.3\logback-core-1.2.3.jar;C:\Users\98383\.m2\repository\org\apache\logging\log4j\log4j-to-slf4j\2.13.3\log4j-to-slf4j-2.13.3.jar;C:\Users\98383\.m2\repository\org\apache\logging\log4j\log4j-api\2.13.3\log4j-api-2.13.3.jar;C:\Users\98383\.m2\repository\org\slf4j\jul-to-slf4j\1.7.30\jul-to-slf4j-1.7.30.jar;C:\Users\98383\.m2\repository\jakarta\annotation\jakarta.annotation-api\1.3.5\jakarta.annotation-api-1.3.5.jar;C:\Users\98383\.m2\repository\org\yaml\snakeyaml\1.27\snakeyaml-1.27.jar;C:\Users\98383\.m2\repository\org\springframework\boot\spring-boot-starter-json\2.4.4\spring-boot-starter-json-2.4.4.jar;C:\Users\98383\.m2\repository\com\fasterxml\jackson\core\jackson-databind\2.11.4\jackson-databind-2.11.4.jar;C:\Users\98383\.m2\repository\com\fasterxml\jackson\core\jackson-annotations\2.11.4\jackson-annotations-2.11.4.jar;C:\Users\98383\.m2\repository\com\fasterxml\jackson\core\jackson-core\2.11.4\jackson-core-2.11.4.jar;C:\Users\98383\.m2\repository\com\fasterxml\jackson\datatype\jackson-datatype-jdk8\2.11.4\jackson-datatype-jdk8-2.11.4.jar;C:\Users\98383\.m2\repository\com\fasterxml\jackson\datatype\jackson-datatype-jsr310\2.11.4\jackson-datatype-jsr310-2.11.4.jar;C:\Users\98383\.m2\repository\com\fasterxml\jackson\module\jackson-module-parameter-names\2.11.4\jackson-module-parameter-names-2.11.4.jar;C:\Users\98383\.m2\repository\org\springframework\boot\spring-boot-starter-tomcat\2.4.4\spring-boot-starter-tomcat-2.4.4.jar;C:\Users\98383\.m2\repository\org\apache\tomcat\embed\tomcat-embed-core\9.0.44\tomcat-embed-core-9.0.44.jar;C:\Users\98383\.m2\repository\org\glassfish\jakarta.el\3.0.3\jakarta.el-3.0.3.jar;C:\Users\98383\.m2\repository\org\apache\tomcat\embed\tomcat-embed-websocket\9.0.44\tomcat-embed-websocket-9.0.44.jar;C:\Users\98383\.m2\repository\org\springframework\spring-web\5.3.5\spring-web-5.3.5.jar;C:\Users\98383\.m2\repository\org\springframework\spring-webmvc\5.3.5\spring-webmvc-5.3.5.jar;C:\Users\98383\.m2\repository\org\springframework\spring-expression\5.3.5\spring-expression-5.3.5.jar;C:\Users\98383\.m2\repository\org\slf4j\slf4j-api\1.7.30\slf4j-api-1.7.30.jar;C:\Users\98383\.m2\repository\org\springframework\spring-core\5.3.5\spring-core-5.3.5.jar;C:\Users\98383\.m2\repository\org\springframework\spring-jcl\5.3.5\spring-jcl-5.3.5.jar;C:\Users\98383\.m2\repository\org\springframework\spring-beans\5.3.5\spring-beans-5.3.5.jar;C:\Users\98383\.m2\repository\org\springframework\boot\spring-boot\2.4.4\spring-boot-2.4.4.jar;C:\Users\98383\.m2\repository\org\springframework\spring-context\5.3.5\spring-context-5.3.5.jar;C:\Users\98383\.m2\repository\org\springframework\boot\spring-boot-starter-aop\2.4.4\spring-boot-starter-aop-2.4.4.jar;C:\Users\98383\.m2\repository\org\springframework\spring-aop\5.3.5\spring-aop-5.3.5.jar;C:\Users\98383\.m2\repository\org\aspectj\aspectjweaver\1.9.6\aspectjweaver-1.9.6.jar;C:\Users\98383\.m2\repository\org\apache\commons\commons-dbcp2\2.8.0\commons-dbcp2-2.8.0.jar;C:\Users\98383\.m2\repository\org\apache\commons\commons-pool2\2.9.0\commons-pool2-2.9.0.jar;C:\Users\98383\.m2\repository\mysql\mysql-connector-java\8.0.23\mysql-connector-java-8.0.23.jar;C:\Users\98383\.m2\repository\org\mybatis\spring\boot\mybatis-spring-boot-starter\2.0.0\mybatis-spring-boot-starter-2.0.0.jar;C:\Users\98383\.m2\repository\org\springframework\boot\spring-boot-starter-jdbc\2.4.4\spring-boot-starter-jdbc-2.4.4.jar;C:\Users\98383\.m2\repository\com\zaxxer\HikariCP\3.4.5\HikariCP-3.4.5.jar;C:\Users\98383\.m2\repository\org\springframework\spring-jdbc\5.3.5\spring-jdbc-5.3.5.jar;C:\Users\98383\.m2\repository\org\springframework\spring-tx\5.3.5\spring-tx-5.3.5.jar;C:\Users\98383\.m2\repository\org\mybatis\spring\boot\mybatis-spring-boot-autoconfigure\2.0.0\mybatis-spring-boot-autoconfigure-2.0.0.jar;C:\Users\98383\.m2\repository\org\mybatis\mybatis\3.5.0\mybatis-3.5.0.jar;C:\Users\98383\.m2\repository\org\mybatis\mybatis-spring\2.0.0\mybatis-spring-2.0.0.jar com.louris.springboot.Application |
执行多条SQL
一个JdbcTemplate只执行了一条SQL,当要多次执行SQL时,可以使用execute方法。它将允许传递ConnectionCallback或者StatementCallback等接口进行回调,从而完成对应的功能。
1 | public Role getRoleByConnectionCallback(JdbcTemplate jdbcTemplate, Long id){ |
JdbcTemplate源码分析
- 从源码中可以看到, Spring 要实现数据库连接资源获取和释放的逻辑,只要完成回调接口的方法逻辑即可,这便是它所提供的模板功能。但是我们并没有看到任何的事务管理,因为 jdbcTemplate 是不能支持事务的,还需要引入对应的事务管理器才能够支持事务。
- 在Spring 中,它会在内部再次判断事务是否交由事务管理器处理,如果是,则数据库连接将会从数据库事务管理器中获取,并且 jdbcTemplate 的资源链接请求的关闭也将由务管理器决定,而不是由 jdbcTemplate 自身决定 。由于这里只是简单应用 ,数据库事务并没有交由事务管理器管理,所以数据库资源是由 jdbcTemplate 自身管理。
1 |
|
MyBatis-Spring项目
配置SqlSessionFactoryBean
1 |
|
其他略过,springboot只需要配置properties文件就可以了
深入Spring数据库事务管理
Spring数据库事务管理器的设计
在Spring中数据库事务是通过PlatformTransactionManager进行管理的,而能够支持事务的是org.springframework.transaction.support.TransactionTemplate模板,它是Spring所提供的事务管理器的模板。
1 | // |
从源码可以看到:
- 事务的创建、提交和回滚是通过PlatformTransactionManager接口来完成的;
- 当事务产生异常时会回滚事务,在默认的实现中所有的异常都会回滚。我们可以通过配置去修改在某些异常发生时回滚或者不回滚事务;
- 当无异常时,会提交事务。
1 | // |
配置事务管理器
1 | <bean id="dataSource" class="org.apache.commons.dbcp2.BasicDataSource"> |
编程式事务
1 | package com.louris.springboot.IoCImpl.config; |
1 | ApplicationContext applicationContext = new AnnotationConfigApplicationContext(TransactionConfig.class); |
声明式事务
Transactional配置项
1 | // |
配置项 | 含义 | 备注 |
---|---|---|
value | 定义事务管理器 | 它是Spring IoC容器里的一个Bean id,这个Bean需要实现接口PlatformTransactionManager |
transactionManager | 同上 | 同上 |
isolation | 隔离级别 | 这是一个数据库在多个事务同时存在时的概念。默认值取数据库默认隔离级别 |
propagation | 传播行为 | 传播行为是方法之间调用的问题,默认值为Propagation.REQUIRED |
timeout | 超时时间 | 单位为秒,当超时时,会引发异常,默认会导致事务回滚 |
readOnly | 是否开启只读事务 | 默认值为false |
rollbackFor | 回滚事务的异常类定义 | 也就是只有当方法产生所定义异常时,才回滚事务,否则就提交事务 |
rollbackForClassName | 回滚事务的异常类名定义 | 同rollbackFor,只是使用类名称定义 |
noRollbackFor | 当产生哪些异常不回滚事务 | 当产生所定义异常时,Spring将继续提交事务 |
noRollbackForClassName | 同noRollbackFor | 同noRollbackFor,只是使用类的名称定义 |
注意,使用声明式事务需要配置注解驱动:
1 | <tx:annotation-driven transaction-manager="transactionManager"/> |
使用XML进行配置事务管理器
使用XML配置事务管理器的方法很多,但是也不常用,更多的还是会采用注解式的事务。
1 |
|
事务定义器
1 | // |
以上就是关于事务定义器的内容,除了异常的定义,其他关于事务的定义都可以在这里完成,而对于事务的回滚内容,它会议RollbackRuleAttribute和NoRollbackRuleAttribute两个类进行保存,这样在事务拦截器中就可以根据我们所配置的内容来处理事务方面的内容。
声明式事务的约定流程
@Transactional注解可以使用在方法或者类上,在Spring IoC容器初始化时,Spring会读入这个注解或者XML配置的事务信息,并且保存到一个事务定义类里面(TransactionDefinition接口的子类),以备将来使用。当运行时会让Spring拦截注解标注的某一个方法或者类的所有方法。谈到了拦截,可能会想到AOP,Spring也是如此。有了AOP的概念,那么它就会把你编写的代码织入到AOP的流程中,然后给出它的约定。
首先Spring通过事务管理器(PlatformTransactionManager的子类)创建事务,与此同时会把事务定义中的隔离级别、超时时间等属性根据配置内容往事务上设置。而根据传播行为配置采取一种特定的策略,后面会谈到传播行为的使用问题,这是Spring根据配置完成的内容,你只需要配置,无须编码。
然后,启动开发者提供的业务代码,我们知道Spring会通过反射的方式调度开发者的业务代码,但是反射的结果可能是正常返回或者产生异常返回,那么它给的约定是只要发生异常,并且符合事物定义类回滚条件的,Spring就会将数据库事务回滚,否则将数据库事务提交,这也是Spring自己完成的。
你会惊奇地发现,在整个开发过程中,只需要编写业务代码和对事务属性进行配置就可以了,并不需要使用代码干预,工作量比较少,代码逻辑也更为清晰,更有利于维护。
1 |
|
1 | //SpringBoot项目启动入口类 |
这里没有数据库的资源打开和释放代码,也没看到数据库提交的代码,只看到了注解@Transactional。它配置了Propagation.REQUIRED的传播行为,这意味着当别的方法调度时,如果存在事务就沿用下来,如果不存在事务就开启新的事务,而隔离级别采用默认的隔离级别,并且设置超时时间为3秒。其他的开发人员只要知道当roleDao的insert方法抛出异常时,Spring就会回滚事务,如果成功,就提交事务。这样Spring就让开发人员主要精力放在业务的开发上,而不是控制数据库的资源和事务上。
但是我们必须清楚的是,这里的神奇原理是Spring AOP技术,而其底层的实现原理是动态代理,也就是只有代理对象相互调用才能想AOP那么神奇。
数据库相关知识
数据库事务ACID特性
- 原子性(Atomicity)
- 一致性(Consistency)
- 隔离性(Isolation)
- 持久性(Durability)
丢失更新
第一类丢失更新
假设一个场景,一个账户存在互联网消费和刷卡消费,而一对夫妻共用这个账户。老公喜欢刷卡消费,老婆喜欢互联网消费。老公查询余额10000元,请客吃饭消费1000元,提交事务成功,余额9000元;同时老婆查询10000元月,网购1000元,在老公提交事务成功后,突然不想买了,取消购买,回滚事务,余额还是10000元,这是不符合事实的。
第二类丢失更新
同前一节,不同的是,这里老婆也提交事务成功,余额还是9000元,这是不符合事实的。
为了克服事务之间协助的一致性,数据库标准规范中定义了事务之间的隔离级别,来在不同程度上减少出现丢失更新的可能性。
隔离级别
读未提交、读已提交、可重复度、序列化
选择隔离级别和传播行为
选择隔离级别
- 在互联网应用中,不但要考虑数据库数据的一致性,而且要考虑系统的性能。一般而言,从脏读到序列化,系统性能直线下降。
- 在大部分场景下,企业会选择读/写提交的方式设置事务。这样既有助于提高并发,又压制了藏独,但是对于数据一致性问题并没有解决。
1 |
|
- 当业务并发量不是很大或者根本不需要考虑的情况下,使用序列化隔离级别用以保证数据的一致性,也是一个不错的选择。
- 总之,隔离级别需要根据并发的大小和性能作出决定,对于并发不打又要保证数据安全性的可以使用序列化的隔离级别,这样就能保证数据库在多事务环境汇总的一致性。
1 |
|
传播行为
传播行为是指方法之间的调用事务策略的问题。在大部分情况下,我们都希望事务能够同时成功或者同时失败。但是也会有例外,比如批次还款,不希望其中一次还款失败而导致整体其他已成功还款的业务回滚。
解决方法是,批次调度还款是产生一个新事务,如果这次异常,只会回滚本次,而不会回滚主事务。
类似这种一个方法调度另外一个方法时,可以对事务的特性进行传播配置,称为传播行为。
传播行为 | 含义 | 备注 |
---|---|---|
REQUIRED | 当方法调用时,如果不存在当前事务,那么就创建事务;如果之前的方法已经存在事务了,那么就沿用之前的事务 | 这是Spring默认的传播行为 |
SUPPORTS | 当方法调用时,如果不存在当前事务,那么不启用事务;如果存在当前事务,那么就沿用当前事务 | —— |
MADATORY | 方法必须在事务内运行 | 如果不存在当前事务,那么就抛出异常 |
REQUIRES_NEW | 无论是否存在当前事务,方法都会在新的事务中运行 | 也就是事务管理器会打开新的事务运行该方法 |
NOT_SUPPORTED | 不支持事务,如果不存在当前事务也不会创建事务;如果存在当前事务,则挂起它,直至该方法结束后才回复当前事务 | 适用于那些不需要事务的SQL |
NEVER | 不支持事务,只有在没有事务的环境中才能运行它 | 如果方法存在当前事务,则抛出异常 |
NESTED | 嵌套事务,也就是调用方法如果抛出异常只回滚自己内部执行的SQL,而不回滚主方法的SQL | 它的实现存在两种情况,如果当前数据库支持保存点(savepoint),那么它就会在当前事务上使用保存点技术;如果发生异常则将方法内执行的SQL回滚到保存点上,而不是全部回滚,否则就等同于REQUIRES_NEW创建新的事务运行方法代码 |
@Transactional的自调用失效问题
- 对于静态(static)方法和非public方法,注解@Transactional是失效的。
- 自调用失效
1 | package com.louris.springboot.IoCImpl.impl; |
1 | Java HotSpot(TM) 64-Bit Server VM warning: Options -Xverify:none and -noverify were deprecated in JDK 13 and will likely be removed in a future release. |
出现原因
在insertRoleList方法的实现中,它调用了自身类实现isertRole的方法,而insertRole声明式REQUIRES_NEW的传播行为,也就是每次调用就会产生新的事务运行,但是通过日志可以发现只有一次
1 | Create a new SqlSession |
说明角色插入两次都使用了同一事务,也就是说,在insertRole上标注的@Transactional失效了,这是一个很容易掉进去的陷阱。
出现这个问题的根本原因在于AOP的实现原理。由于@Transactional的实现原理是AOP,而AOP的实现原理是动态代理,而上述代码是自己调用自己的过程。换句话说,并不存在代理对象的调用,这样就不会产生AOP去为我们设置@Transactional配置的参数,这样就出现了自调用注解失败的问题!
解决方案
(1)可以使用两个服务类,Spring IoC容器中为你生成了RoleService的代理对象,这样就可以使用AOP,且不会出现该问题;
(2)也可以直接从容器中获取RoleService的代理对象,如下所示:
1 | package com.louris.springboot.IoCImpl.impl; |
1 | 2021-09-21 22:10:48.580 [main] DEBUG org.mybatis.spring.SqlSessionUtils - Creating a new SqlSession |
典型错误用法的剖析
错误使用Service
互联网往往曹勇模型——视图——控制器来搭建开发环境,因此在Controller中使用Service是十分常见的。
以下代码中,当一个Controller使用Service方法时,如果这个Service标注有@Transactional,那么它就会启用一个事务,而一个Service方法完成后,它就会释放该事务,所以前后两个insertRole的方法时在两个不同的事务中完成的。
1 | List<Role> list = new ArrayList<>(); |
日志可以证实这一点,明确地说明使用带有事务的Service,当调用时,如果不是调用Service方法,Spring会为你创建对应的数据库事务。如果多次调用,则不再同一个事务中,这会造成不同时提交和回滚不一致的问题。每一个Java EE开发者都要注意这类问题,以避免一些不必要的错误。
1 | 2021-09-22 10:14:49.893 [main] DEBUG org.mybatis.spring.SqlSessionUtils - Creating a new SqlSession |
过长时间占用事务
在企业的生产系统中,数据库事务资源是最宝贵的资源之一,使用了数据库事务之后,要及时释放数据库事务。换言之,我们应该尽可能地使用数据库事务资源去完成所需工作,但是在一些工作中需要使用到文件、对外连接等操作,而这些操作往往会占用较长时间,针对这些,如果开发者不注意细节,就很容易出现系统宕机的问题。
以下代码在insertRole方法结束后,Spring才会释放数据库事务资源,也就是说在运行doSomethingForFile方法时,Spring并没有释放数据库事务资源,而等到doSomethingForFile方法运行完成后,返回result后才会关闭数据库资源。
在大型互联网系统中,一个数据库的链接可能也就是50条左右,然而同时并发的请求则可能是成百上千。对于这些请求,大部分的并发请求都在等待50条占有数据库连接资源的文件操作了。加入平均一个doSomethingForFile的操作需要1秒,对于同时出现1000条并发请求的网站,就会出现请求卡顿的状态。因为大部分的请求都在等待数据库事务资源的分配,这是一个糟糕的结果。
1 |
|
正确做法是Controller进行非数据库操作,如下所示,当程序运行完isnertRole方法后,Spring会释放数据库事务资源,而不再占用。对于doSomethingForFile方法而言,已经在一个没有事务的环境中运行了,这样当前的请求就不会长期占用数据库事务资源,使得其他并发的请求被迫等待其释放了。
1 | "/addRole") ( |
错误捕获异常
模拟一段购买商品的代码,其中ProductService是产品服务类,而TransactionService是记录交易信息,需求显然就是产品减库存和保存交易在同一个事务里面,要么同时成功,要么同时失败,并且假设减库存和保存交易的传播行为都为REQUIRED。
1 |
|
这里的问题是方法已经存在异常了,由于开发者不了解Spring的事务约定,在两个操作的方法里面加入了自己的try…catch…语句,就可能发生这样的结果。当减少库存成功了,但是保存交易信息时失败而发生了异常,此时由于开发者加入了try…catch…语句,所以Spring在数据库事务所约定的流程中再也得不到任何异常信息了,此时Spring就会提交事务,这样就出现了库存减少,而交易记录却没有的糟糕情况。在那些需要大量异常处理的代码中,我们要小心这样的问题。
1 |
|
注意throw抛出了一个运行异常,这样在Spring的事务流程汇总,就会捕捉到抛出的这个异常,进行事务回滚,从而保证了产品减库存和交易记录保存的一致性,这才是正确的用法,使用事务时要时刻记住Spring和我们的约定流程。
Spring MVC 框架
Spring MVC的初始化和流程
MVC设计概述
SpringMVC的组件和流程
Spring MVC的核心在于其流程,这是使用Spring MVC框架的基础,Spring MVC是一种基于Servlet的技术,它提供了核心控制器DispatcherServlet和相关的组件,并制定了松散的结构,以适合各种灵活的需要。
首先,Spring MVC框架爱是围绕着DispatcherServlet而工作的,所以这个类是其最为重要的类。从它的名字来看,它是一个Servlet,它可以拦截HTTP发送过来的请求,在Servlet初始化(调用init方法)时,Spring MVC会根据配置,获取配置信息,从而得到统一资源标识符(URI, Uniform Resource Identifier)和处理器(Handler)之间的映射关系(HandlerMapping),为了更加灵活和增强功能,Spring MVC还会给处理器加入拦截器,所以还可以在处理器执行前后加入自己的代码,这样就构成了一个处理器的执行链(HandlerExecutionChain),并且根据上下文初始化视图解析器等内容,当处理器返回的时候就可以通过视图解析器定位视图,然后将数据模型渲染到视图中,用来响应用户的请求了。
当一个请求到来时,DispatcherServlet首先通过请求和事先解析好的HandlerMapping配置,找到对应的处理器(Handler),这样就准备开始运行处理器和拦截器组成的执行链,而运行处理器需要有一个对应的环境,这样它就有了一个处理器的适配器(HandlerAdapter),通过这个适配器就能运行对应的处理器及其拦截器,这里的处理器包含了控制器的内容和其他增强的功能,在处理器返回模型和视图给DispacherServlet后,DispacherServlet就会把对应的视图信息传递给视图解析器(ViewResolver)。注意,这一步取决于是否使用逻辑视图,如果使用逻辑视图,那么视图解析器就会解析它,然后把模型渲染到视图中去,最后响应用户的请求;如果不是逻辑视图,则不会进行处理,而是直接通过视图渲染数据模型。这就是一个Spring MVC完整的流程,它是一个松散的结构,所以可以满足各类请求的需要。
Spring MVC 入门的实例
启动thymeleaf
1 | <dependency> |
1 | package com.louris.springboot.IoCImpl.controller; |
1 |
|
Spring MVC开发流程详解
在目前的开发过程中,大部分都会采用注解的开发方式,使用注解在Spring MVC中十分简单,主要是以一个注解@Controller标注,一般只需要通过扫描设置,就能将其扫描出来,只是往往还要结合注解@RequestMapping去配置它。@RequestMapping可以配置在类或方法之上,它的作用是指定URI和哪个类(或方法)作为一个处理请求的处理器,为了更加灵活,Spring MVC还定义了处理器的拦截器,当启动Spring MVC的时候,Spring MVC就会去解析@Controller中的@RequestMapping的配置,再结合所配置的拦截器,这样它就会组成多个拦截器和一个控制器的形式,存放到一个HandlerMapping中去。当请求来到服务器,首先是通过请求信息找到对应的handlerMapping,进而可以找到对应的拦截器和处理器,这样就能够运行对应的控制器和拦截器。
配置@RequestMapping
1 | // |
控制器的开发
获取请求参数
(1)不建议使用Servlet容器所给予的API,因为这样控制器将会依赖于Servlet容器。
Spring MVC会自动解析代码中的方法参数session、request,然后传递关于Servlet容器的API参数,所以是可以获取到的。通过request或者session都可以很容易地得到HTTP请求过来的参数,这固然是一个方法,但并非一个好的方法。
因为如果这样做了,那么对于idnex2方法而言,它就和Servlet容器紧密关联了,不利于扩展和测试。
1 | "/index2", method = RequestMethod.GET) (value = |
(2)建议使用@RequestParam注解来获取参数(不写的话,默认与参数名相同)
@RequestParam参数
- required,默认true,不允许参数为空
- defaultValue,默认值为”\n\t\t\n\t\t\n\uE000\uE001\uE002\n\t\t\t\t\n”,可以修改为想要的内容。
通过注解和一些约定,可以发现index3方法对Servlet API的依赖没有了,它就很容易进行测试和扩展。1
2
3
4
5
6
7"/index3", method = RequestMethod.GET) (value =
public ModelAndView index3(@RequestParam(value = "ID", required = true) Long id){
System.out.println("params[id] = " + id);
ModelAndView mv = new ModelAndView();
mv.setViewName("index");
return mv;
}
(3)获取session当中的内容
假设在登录系统已经在Session中设置了userName,那么可以使用@SessionAttribute去从Session中获取对应的数据.
1 | "/index4", method = RequestMethod.GET) (value = |
实现逻辑和绑定视图
1 | package com.louris.springboot.IoCImpl.controller; |
视图渲染
(1)ModelAndView找到roleDetails.html或者roleDetail.jsp进行渲染
1 | "getRole", method = RequestMethod.GET) (value = |
1 | "getRole", method = RequestMethod.GET) (value = |
(2)Model加入参数,返回Stirng,找到roleDetails.html或者roleDetail.jsp进行渲染
1 | "getRole2", method = RequestMethod.GET) (value = |
(3)不渲染,@Response解析对象,返回json数据
1 | "getRole3", method = RequestMethod.GET) (value = |
深入Spring MVC组件开发
控制器接受各类请求参数
接收普通请求参数
1 |
|
1 | package com.louris.springboot.utils; |
1 | package com.louris.springboot.IoCImpl.controller; |
使用@RequestParam注解获取参数
1 | "/commonAnnotationParams") ( |
使用URL传递参数
一些网站使用URL的形式传递参数,这符合RESTFul风格。
1 |
|
传递JSON参数
有时候参数的传递还要更多的参数,比如角色的名称和备注,查询可能需要分页参数。
1 | public class PageParams{ |
1 | public class RoleParams{ |
1 | $(document).ready(function(){ |
1 | "/findRoles") ( |
接收列表数据和表单序列化
在一些场景下,如果要一次性删除多个角色,那么肯定是想将一个角色编号的数组传递给后台,或需要新增角色,甚至同时新增多个角色。无论如何,这都需要用到Java的集合或者数组去保存对应的参数。
简单数组
1 | $(document).ready(function(){ |
1 | "/deleteRoles") ( |
POJO数组
1 | $(document).ready(function(){ |
1 | "/deleteRoles") ( |
表单序列化
1 |
|
1 | "/serializeTest") ( |
重定向
方法1
1 | "/showRoleJsonInfo") ( |
方法2
1 | "/showRoleJsonInfo") ( |
传递参数
重定向如果需要传递参数,需要用到RedirectAttributes类,其有两种常见方法:
- addAtribute(), 通过url传递的方式http://locahost:8081?obj=obect
- addFlashAttribute(), 将数据放入session中,重定向后删除数据,通过@ModelAttribute注解获取对应参数。
1 | "/showRoleJsonInfo") ( |
保存并获取属性参数
有时候我们会暂存数据到HTTP的request对象或者Session对象汇总,在开发控制器的时候,有时也需要保存对应的数据到这些对象中去,或者从中获取数据。Spring MVC给予了支持:
- @RequestAttribute获取HTTP的请求(request)对象属性值,用来传递给控制器的参数;
- @SessionAttribute在HTTP的会话(Session)对象属性值中,用来传递给控制器的参数;
- @SessionAttribute可以给它配置一个字符串数组,这个数组对应的是数据模型对应的键值对,然后将这些键值对保存到Session中。
注解@RequestAttribute
1 | //设置请求属性 |
1 | "requestAttribute") ( |
注解@SessionAttribute和注解@SessionAttributes
这两个注解和 HTTP 的会话对象有 ,在浏览器和服务器保持联系的时候 HTTP 会创
个会话对象,这样可以让我们在和服务器会话期间(请注意这个时间范围)通过它读/
写会话对象的属性,缓存 定数据信息。
先来讨论 下设置会话属性,在控制器中可以使用注解@SessionAttributes 来设置对应
的键值对,不过这个注解只能对类进行标注,不能对方法或者参数注 它可以配置属性
名称或者属性类型。它的作用是当这个类被注解后, Spring MVC 执行完控制器的逻辑后,
将数据模型中对应的属性名称或者属性类型保存到 TTP Session 对象中。
读取后端设置的session属性
1 | <%@ page import="com.louris.springboot.IoCImpl.beans.pojo.Role" %><%-- |
1 | package com.louris.springboot.IoCImpl.controller; |
运行结果
1 | 2021-09-25 17:05:21.298 INFO 12204 --- [nio-8081-exec-1] o.a.c.c.C.[Tomcat].[localhost].[/] : Initializing Spring DispatcherServlet 'dispatcherServlet' |
1 | id = 2 |
读取前端设置的session属性
上面是后端设置session属性,现在需要读取前端设置的session属性
1 | <%@ page import="com.louris.springboot.IoCImpl.beans.pojo.Role" %><%-- |
1 | package com.louris.springboot.IoCImpl.controller; |
注解@CookieValue和注解@RequestHeader
从名称而言,这两个注解都很明确,就是从 Cooki BTTP 请求头获取对应的请求信
,它们的用法比较简单,且大同小异。只是对于 Cookie ,用户是可以禁用的,所以在使用的时候需要考虑这个问题
1 | "/getHeaderAndCookie") ( |
1 | 2021-09-25 17:38:16.756 INFO 17924 --- [nio-8081-exec-1] o.a.c.c.C.[Tomcat].[localhost].[/] : Initializing Spring DispatcherServlet 'dispatcherServlet' |
拦截器
- 拦截器是Spring MVC中强大的控件,它可以在进入处理器之前做一些操作,或者在处理器完成后进行操作,甚至是在渲染视图后进行操作。
- SpringMVC会在启动期间就通过@RequestMapping的注解解析URI和处理器的对应关系,在运行的时候通过请求找到对应的HandlerMapping,然后构建HandlerExecutionChain对象,它是一个执行的责任链对象。
拦截器的定义
1 | // |
拦截器的执行流程
前置方法 -> 判断是否返回true
true -> 处理器 -> 后置方法 -> 视图解析和渲染视图 -> 完成方法 -> 后续流程
false -> 后续流程
开发拦截器
这里模仿登录拦截,只有登录了才能访问需登录才能访问的页面,如果未登录情况下访问了需登录才能访问的页面,会跳转
自定义登录拦截器
1 | package com.louris.springboot.interceptor; |
拦截器配置类
1 | package com.louris.springboot.config; |
用户登录控制器
1 | package com.louris.springboot.web; |
验证表单
在实际工作中,得到数据后的第一步就是检验数据的正确性,如果存在录入上的问题,一般会通过注解校验,发现错误后返回给用户,但是对于一些逻辑上的错误,比如购买金额=购买数量×单价,这样的规则就很难使用注解方式进行验证了,这个时候可以使用Spring所提供的验证器(Validator)规则去验证。
导入依赖
1 | <dependency> |
POJO
1 | package com.louris.springboot.IoCImpl.beans.pojo; |
页面
1 | <%-- |
控制器
1 | package com.louris.springboot.IoCImpl.controller; |
使用验证器
验证器接口
1 | // |
自定义验证器
1 | package com.louris.springboot.utils; |
控制器绑定验证器
1 | package com.louris.springboot.IoCImpl.controller; |
数据模型
下面的例子中只是在数据模型中加入了数据,但没有把数据模型绑定给视图和模型,这一步是Spring MVC在完成控制器逻辑后,自动绑定的,并不需要我们绑定,也就没有绑定的代码了。
1 | "getRoleByModelMap", method = RequestMethod.GET) (value = |
视图和视图解析器
视图是展示给用户的内容,而在此之前,要通过控制器得到对应的数据模型,如果是非逻辑视图,则不会经过视图解析器定位视图,而是直接将数据模型渲染便结束了;而逻辑视图则要对其进一步解析,以定位真实视图,这就是视图解析器的作用了。而视图则是把从控制器查询回来的数据模型进行渲染,以显示给请求者查看。
视图
视图接口定义
1 | // |
非逻辑视图
将数据模型转换为一个JSON视图,展现给用户,无须对视图名字再进行下一步的解析。
常见的是MappingJackson2JsonView
1 | "/getRoleForJson", method = RequestMethod.GET) (value = |
逻辑视图
对于逻辑视图而言它需要一个视图解析器,无论是使用XML或者注解,其根本都是在创建一个视图解析器,通过前缀和后缀加上视图名称就能找到对应的JSP文件,然后把数据模型渲染到JSP文件中,这样便能展现视图给用户了。
1 | "viewResolver") (name = |
视图解析器
1 | // |
实例:Excel视图的使用
对于Excel而言,Spring MVC所推荐的是使用AbstractXlsView,它实现了视图接口,从其命名也可以知道它只是一个抽象类,不能生成实例对象。
AbstractXlsView抽象类
1 | // |
添加依赖
1 | <dependency> |
定义Excel导出接口
假设需要一个导出所有角色信息的功能,但是将来也许还有其他的导出功能。为了方便,先定义一个接口,这个接口主要是让开发者自定义生成Excel的规则。
1 | package com.louris.springboot.IoCImpl.service; |
自定义Excel视图类
1 | package com.louris.springboot.IoCImpl.view; |
角色信息导出控制器
1 | package com.louris.springboot.IoCImpl.controller; |
上传文件
Spring MVC为上传文件提供了良好的支持。首先Spring MVC的文件上传是通过MultipartRevolver处理的,它只是一个接口,它有两个实现类:
- CommonsMultipartResolver:依赖于Apache下的jakarta Common FileUpload项目解析Multipart请求,可以在Spring的各个版本中使用,只是它要依赖于第三方包才得以实现;
- StandardServletMultipartResolver:Spring3.1版本后的产物,它依赖于Servlet3.0或者更高版本的实现,它不用依赖第三方包;
配置properties文件
1 | # 配置上传文件 |
前端页面
1 | <%-- |
注入MultiPartResolver(SpringBoot无须注入)
在Spring中,既可以通过XML,也可以通过Java去配置MultipartResolver,这里只介绍注解配置方式。
注意,”multipartResolver”是Spring约定好的Bean name不可以修改。
1 | package com.louris.springboot.IoCImpl.config; |
控制器
1 | package com.louris.springboot.IoCImpl.controller; |
Spring MVC高级应用
Spring MVC的数据转换和格式化
首先当一个请求到达 DispatcherServlet 的时候,需要找到对应的Handler Mapping ,然后根据 HandlerMapping 去找到对应的 HandlerAdapter 执行处理器。处理器在要调用的控制器之前,需要先获取 HTTP 发送过来的信息,然后将其转变为控制器的各种不同类型参数,这就是各类注解能够得到丰富类型参数的原因。它首先用 HTTP消息转换器(HttpMessageConverter )对消息转换,但是这是 个比较原始的转换,它是String类型和文件类型比较简易的转换,它还需要进一步转换才能转换为 POJO 或者其他丰富的参数类型。为了拥有这样的能力, Spring 提供了转换器和格式化器,这样通过注解的信息和参数的类型,它就能够把 HTTP 发送过来的各种消息转换成为控制器所需要的各类参数了。
当处理器处理完了这些参数的转换,它就会进行验证,验证表单的方法在 15 章已经谈及。完成了这些内容,下 步就是调用开发者所提供的控制器了,将之前转换成功的参数传递进去,这样我 开发的控制器就能够得到丰富的 Java 类型的支持了,进而完成控制器的逻辑,控制器完成了对应的逻辑,返回结果后,处理器如果可以找到对应处理结果类型HttpMessageConverter 的实现类,它就会调用对应的 HttpMessageConverter 的实现类方法。对控制器返回的结果进行 HTTP 转换 步不是必须的,可以转换的前提是能够找到对应的转换器 在讨论注解@ResponseBody 会再次看到这个过程,做完这些处理器的功能就完成了。
接下来就是关于视图解析和视图解析器的流程了,在前面的章节我们已经有了详细地阐述。整个过程是比较复杂的, 有时候要自定义 些特殊的转换规则,比如我们和第合作,合作方可能提供的并不是一 良好的 JSON 式,而是一些特殊的规则,这个时候就可能需要使用自定义的消息转换规则,把消息转换为对应的 Java 类型,从而简化开发。
在Java 类型转换之前, Spring MVC 中,为了应对 HTTP 请求,它还定义了HttpMessageConverter ,它是 个总体的接口,通过它可以读入 HTT 请求内容。也就是说,在读取 HTTP 请求的参数和内容的时候会先用 HttpMessageConverter 读出,做一次简单转换为 Java 类型, 主要是字符串 String ),然后就可以使用各类转换器进行转换了,在逻辑业务处理完成后,还可以通过它把数据转换为响应给用户的内容。对于转换器而言,在 Spring 中分为两大类,一种是由 Converter 接口所定义的,另外种是 GenericConverter ,它们都可以使用注册机注册。注意,它们都是来自于 Spring Core 项目,而非 SpringMVC 项目,它的作用范围是 Java 内部各种类型之间的转换。
HttpMessageConverter和JSON消息转换器
1 | // |
JSON消息转换类
1 | package com.louris.springboot.IoCImpl.config; |
1 | <bean class="org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerAdapter"> |
@ResponseBody注解
通过注解@ResponseBody将标记Spring MVC,将响应结果转变为JSON,这样在控制器返回结果后,它会通过类型判断找到MappingJackson2HttpMessageConverter实例,进而在处理器内转变为JSON,从而满足JSON的转换的要求。
1 | "/json") ( |
一对一转换器(Converter)
1 | // |
Spring Core项目的部分转换器
转换器 | 说明 |
---|---|
CharacterToNumber | 将字符转换为数字 |
IntegerToEnum | 将整数转换为枚举类型 |
ObjectToStringConverter | 将对象转换为字符串 |
SerializingConverter | 序列化转换器 |
DeserializingConverter | 反序列化转换器 |
StringToBooleanConverter | 将字符串转换为布尔值 |
StringToEnum | 将字符串转换为枚举 |
StringToCurrencyConverter | 将字符串转换为金额 |
EnumToStringConverter | 将枚举转化为字符串 |
通过HttpMessageConverter把HTTP的消息读出后,Spring MVC就开始使用这些转换器来将HTTP的信息,转化为控制器的参数,这就是能在控制器上获得各类参数的原因。大部分情况下,Spring MVC所提供的功能,能够满足一般的需求,但是有时候我们需要进行自定义转化规则,这当然也不会太困难,只要实现接口Converter,然后注册给对应的转换服务类就可以了。
自定义SpringToRole转换器
@Component注解将bean注册给IoC容器,Spring MVC就会自动进行converter的注册
1 | package com.louris.springboot.utils; |
控制器
1 | "/updateRole") ( |
运行结果
1 | convert |
数组和集合转换器 GenericConverter
- 一对一的转换器只能从一种类型转换成另一种类型,不能进行一对多转换,比如把String转换为List
或者String[],甚至是List 。 - 为了克服这个问题,Spring Core项目还加入了另外一个转换器结构GenericConverter,它能够满足数组和集合转换的要求。在Spring MVC中,这是一个比较底层的接口,为了进行类型匹配判断,还定义了另外一个接口:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61//
// Source code recreated from a .class file by IntelliJ IDEA
// (powered by Fernflower decompiler)
//
package org.springframework.core.convert.converter;
import java.util.Set;
import org.springframework.core.convert.TypeDescriptor;
import org.springframework.lang.Nullable;
import org.springframework.util.Assert;
public interface GenericConverter {
//返回可接受的转化类型
Set<GenericConverter.ConvertiblePair> getConvertibleTypes();
//转换方法
Object convert(@Nullable Object var1, TypeDescriptor var2, TypeDescriptor var3);
//可转换匹配类
public static final class ConvertiblePair {
private final Class<?> sourceType; //源类型
private final Class<?> targetType; //目标类型
public ConvertiblePair(Class<?> sourceType, Class<?> targetType) {
Assert.notNull(sourceType, "Source type must not be null");
Assert.notNull(targetType, "Target type must not be null");
this.sourceType = sourceType;
this.targetType = targetType;
}
public Class<?> getSourceType() {
return this.sourceType;
}
public Class<?> getTargetType() {
return this.targetType;
}
public boolean equals(@Nullable Object other) {
if (this == other) {
return true;
} else if (other != null && other.getClass() == GenericConverter.ConvertiblePair.class) {
GenericConverter.ConvertiblePair otherPair = (GenericConverter.ConvertiblePair)other;
return this.sourceType == otherPair.sourceType && this.targetType == otherPair.targetType;
} else {
return false;
}
}
public int hashCode() {
return this.sourceType.hashCode() * 31 + this.targetType.hashCode();
}
public String toString() {
return this.sourceType.getName() + " -> " + this.targetType.getName();
}
}
}为了整合原有接口GenericConverter,有了一个新的接口:1
2
3
4
5
6
7
8
9
10
11
12
13//
// Source code recreated from a .class file by IntelliJ IDEA
// (powered by Fernflower decompiler)
//
package org.springframework.core.convert.converter;
import org.springframework.core.convert.TypeDescriptor;
public interface ConditionalConverter {
//如果返回true,才进行下一步转换
boolean matches(TypeDescriptor sourceType, TypeDescriptor targetType);
}基于该接口,Spring Core开发了不少的实现类,这些实现类都会注册到ConversionService对象里,通诺ConditioanlConverter的matches进行匹配。如果可以匹配,则会调用convert方法进行转换,它能够提供各种对数组和集合的转换。1
2
3
4
5
6
7
8
9//
// Source code recreated from a .class file by IntelliJ IDEA
// (powered by Fernflower decompiler)
//
package org.springframework.core.convert.converter;
public interface ConditionalGenericConverter extends GenericConverter, ConditionalConverter {
}
StringToArrayConverter
1 | // |
首先,创建StringToArrayConverter对象需要一个ConversionService对象,而getConvertibleTypes方法则是可以自定义的类型,matches方法匹配类型,通过源类型和目标类型判定使用何种Converter能够转换,那么convert方法用于完成对字符串和数字的转换。在大部分情况下,我们都不需要自定义ConditionalGenericConverter实现类,只需要使用Spring MVC提供的便可以了。
由于需要使用一个定义好的Converter,所以利用之前的转换器,按照StringToArrayConverter对象的逻辑,每一个数组的元素都需要用逗号分隔,所以在原有StringToRoleConverter的规则上,让每一个角色对象的传递加入一个逗号分隔便可以了。
1 | "/updateRole") ( |
1 | convert |
使用格式化器(Formatter)
有些数据需要格式化,比如说金额、日期等。传递 期格 yyyy-MM-dd 或者 yyyy-MM-dd hh ss rnrn ,这些是需要格式化的,对于金额也是如此,比如1万元币,正式场合往往要写作¥10000.00这些都要求字符串按一定的格式转为日期或者金额。为了对这些场景做出支持,Spring Context提供了相关的Formatter。它需要实现一个接口——Formatter,而Formatter又扩展了两个接口Printer和Parser。
在Spring内部用得比较多的两个注解是@DateTimeFormat和@NumberFormat,在客户端显示需要使用相关的标签。
注解参数
1 | <%-- |
1 | "/format") ( |
注解类变量
1 | package com.louris.springboot.IoCImpl.beans.pojo; |
1 | "/formatPojo") ( |
为控制器添加通知
与Spring AOP一样,Spring MVC也能够给控制器加入通知,它主要涉及4个注解:
- @ControllerAdvice,主要作用于类,用以标识全局性的控制器的拦截器,它将应用于对应的控制器;
- @InitBinder,是一个允许构建POJO参数的方法,允许在构造控制器参数的时候,加入一定的自定义控制;
- @ExceptionHandler,通过它可以注册一个控制器异常,使用当控制器发生注册异常时,就会跳转到该方法上;
- @ModelAttribute,是一种针对于数据模型的注解,它先于控制器方法运行,当标注方法返回对象时,它会保存到数据模型中。
1 | package com.louris.springboot.utils; |
首先,注解@ControllerAdvice 己经标记了@Component ,所以标注它, SpringMVC扫描的时候就会将其放置到 IoC 容器中,而它的属性 basePackages 则是指定拦截的控制器然后,通过注解@InitBinder 可以获得 个参数一-WebDataBinder,它是 个可以指定 POJO参数属性转换的数据绑定,这里使用了关于日期 CustomDateEditor ,并且指定格式为yyyy-MM-dd ,它还允许自定义验证器,在第 15 章看到了这个过程,它的作用是允许参数的转换和验证器进行自定义。这样被拦截的控制器关于日期对象的参数,都会被它处理,就不需要我们自己制定 ormatter 了。再次,@ModelAttribute 是关于数据模型的,它会在进入控制器方法前运行,加入一个数据模型键值对 projectName”- chapter! 。最后@ExceptionHandler 的作用是制定被拦截的控制器发生异常后,如果异常匹配,就会使用该方法处理,返回字符串 exception”,那它就会找到对应的异常 JSP 去响应,这样就可以避免异常页面的不友好。
而事实上,控制器也可以使用@InitBinder、@ExceptionHandler、@ModelAttribute。注意,它只对于当前控制器有效
1 |
|
- 注意,这个控制器并不在注解@ControllerAdvice 所扫描的包内,所以不会被公共通知所拦截,它内部的注解@ModelAttribute 只是针对当前控制器本身。它所注解的方法会在控制器之前运行。
- 这里定义变量名为 role ,这样在运行这个方法之后,返回查询的角色对象,系统就会把返回的角色对象以键 role 保存到数据模型。
- getRoleFromModelAttribute 方法的角色参数也只需要注解@ModelAttrib1 通过变量名 role 取出即可,这样就完成了参数传递.
处理异常
控制器的通知注解@ExceptionHandler 可以处理异常,这点之前我们己经讨论过。此外,SpringMVC 还提供了其他的异常处理机制,通过使用它们可以获取更为精确的信息,从而为定位问题带来方便。在默认的情况下, pring 会将自身产生的异常转换为合适的状态码,通过这些状态码可以进一步确定异常发生的原因,便于找到对应的问题
Spring异常 | 状态码 | 备注 |
---|---|---|
BindException | 400-Bad Request | 数据绑定异常 |
ConversionNotSupportedException | 500-Internal Server Error | 数据类型转换异常 |
HttpMediaTypeNotSupportedException | 406-Not Acceptable | HTTP媒体类型不可接受异常 |
HttpMediaTypeNotSupportedException | 415-Unsupported Media Type | HTTP媒体类型不支持异常 |
HttpMessageNotReadableException | 400-Bad Request | HTTP消息不可读异常 |
HttpMessageNotWritableException | 500-Internal Server Error | HTTP消息不可写异常 |
HttpRequestMethodNotSupportedException | 405-Method Not Allowed | HTTP请求找不到处理方法异常,往往是HandlerMapping找不到控制器或其方法响应 |
MethodArgumentNotValidException | 400-Bad Request | 控制器方法参数无效异常,一般是参数方面的问题 |
MissingServletRequestParameterException | 400-Bad Request | 缺失参数异常 |
MissingServletRequestPartException | 400-Bad Request | 方法中表明了采用“multipar/form-data”请求,而实际不是该请求 |
TypeMismatchException | 400-Bad Request | 当设置一个POJO属性的时候,发现类型不对 |
自定义异常
1 | package com.louris.springboot.utils; |
1 | "notFound") ( |
国际化
有时候可能需要国际化,比如开发一个需要提供中文和英文的网站,特别是外资企业更是如此,国际化一般分为语言、时区和国家等信息。
概述
DispatcherServlet会解析一个LocaleResolver接口对象,通过它来决定用户区域(User Locale),读出对应用户系统设定的语言或者用户选择的语言,以确定其国际化。注意,对于DispatcherServlet而言,只能注册一个LocaleResolver接口对象。LocaleContextResolver,能够处理一些用户区域上的问题,包括语言和时区的问题。
- AcceptHeaderLocaleResolver:Spring默认的区域解析器,它通过检验HTTP请求的accept-language头部来解析区域。这个头部是由用户的web浏览器根据底层操作系统的区域设置进行设定。注意,这个区域解析器无法改变用户的区域,因为它无法修改用户操作系统的区域设置,所以它并不需要开发,因此没进一步讨论的必要;
- FixedLocaleResolver:使用固定Locale国际化,不可修改Locale,这个可以由开发者提供固定的规则,一般不用,本书不再讨论它;
- CookieLocaleResolver:根据Cookie数据获取国际化数据,由于用户禁止Cookie或者没有设置,如果是这样,它会根据accept-language HTTP头部确定默认区域;
- SessionLocaleResolver:根据Session进行国际化,也就是根据用户Session的变量读取区域设置,所以它是可变的。如果Session也没有设置,那么它也会使用开发者设置默认的值。
只有CookieLocaleResolver和SessionLocaleResolver才能通过Cookie或者Session去修改国际化,而另两个是固定的,所以现实中使用得多的是CookieLocaleResolver和SessionLocaleResolver。
MessageSource接口
如何加载国际化文件的问题,MessageSource接口是Spring MVC为了加载消息所设置的接口。
- StaticMessageSource类是一种静态的消息
- DelegatingMessageSource实现的是一个代理的功能,这两者在实际应用中使用并不多
- ResourceBundleMessageSource使用的是JDK提供的ResourceBundle,它只能把文件放置在对应的类路径下。它不具备热加载的功能,也就是需要重启系统才能重新加载它。
- ReloadableResourceBundleMessageSource更为灵活,它可以把属性文件放置在任何地方,可以在系统不重启的情况下
注意”messageSource”是一个默认约定的名称,不能修改。
1 | package com.louris.springboot.IoCImpl.config; |
CookieLocaleResolver和SessionLocaleResolver
只是对于 Cookie 用户可以进行删除甚至禁用,使得其安全性难以得到保证,导致大量的使用默认值,也许这些并不是用户所期待的。为了避免这个问题,一般用得更多的是 SessionLocaleResolver ,它是基于 Session 实现的,具有更高的可靠性。
注意,”localeResolver”也是一个约定的名称。
1 | "localeResolver") (name = |
当然以上都可以用xml进行配置,自己查。
国际化拦截器(LocaleChangeInterceptor)
1 | package com.louris.springboot.IoCImpl.config; |
1 | package com.louris.springboot.IoCImpl.controller; |
1 | <%@ taglib prefix="spring" uri="http://www.springframework.org/tags" %> |
两个配置文件:
msg_en_US.propertires
1 | welcome=the project name is springboot |
msg_en_CN.properties
1 | welcome=项目名为SpringBoot |
Redis应用
Redis概述
- 缓存常用的数据
- 需要高速读/写的场合使用它快速读/写
Redis基本安装和使用
Windows下安装Redis
https://github.com/microsoftarchive/redis/releases
下载Redis解压后
新建文件startup.cmd
1 | redis-server redis.windows.conf |
Linux下安装Redis
1 | wget http://download.redis.io/releases/redis-6.2.6.tar.gz |
启动
1 | ./src/redis-server |
另一个客户端启动命令行窗口
1 | ./src/redis-cli |
Redis的Java API
在Java程序中使用Redis
1 | import redis.clients.jedis.Jedis; |
运行结果
1 | SLF4J: Failed to load class "org.slf4j.impl.StaticLoggerBinder". |
事实上,Redis的速度比这个操作速度快得多,这里慢是因为我们只是一条条地将命令发送给Redis去执行。如果使用流水线技术它会快得多,将可以达到10万次每秒的操作,十分有利于系统性能的提高。
在Spring中使用Redis
添加依赖以及配置
添加依赖
1 | <!-- SpringBoot集成Redis的起步依赖 --> |
Spring Data Redis的4种工厂模型
- JredisConnectionFactory
- JedisConnectionFactory
- LettuceConnectionFactory
- SrpConnectionFactory
RedisSerializer对象序列化接口
有了RedisConnectionFactory工厂,就可以使用RedisTemplate了。
普通的连接使用没有办法把Java对象直接存入Redis,而需要我们自己提供方案,这时往往需要将对象序列化,然后使用Redis进行存储,
而取回序列化的内容后,在通过转换变为java对象,Spring模板中提供了封装的方案,在它内部提供了RedisSerializer接口和一些实现类。
- GenericJackson2JsonRedisSerializer,通用的使用Json2.jar包,将Redis对象的序列化器;
- Jackson2JsonRedisSerializer
,通过Jackson2.jar包提供的序列化进行转换; - JdkSerializationRedisSerializer
,使用JDK的序列化器进行转化; - OxmSerializer,使用Spring O/X对象Object和XML相互转换;
- StringRedisSerializer,使用字符串进行序列化;
- GenericToStringSerializer,通过通用的字符串序列化进行相互转换;
为此,Spring提供的RedisTemplate还有两个属性:
- keySerializer:键序列化器;
- valueSerializer:值序列化器;
properties配置方式
1 | # 配置redis |
xml配置方式
1 |
|
配置类注解方式
1 | package com.louris.springboot.demo.config; |
对象序列化
1 | package com.louris.springboot.demo.pojo; |
测试结果
1 | ApplicationContext applicationContext = new AnnotationConfigApplicationContext(RedisConfig.class); |
1 | 09:56:36.673 [main] DEBUG org.springframework.data.redis.core.RedisConnectionUtils - Fetching Redis Connection from RedisConnectionFactory |
同一个Redis连接
- 注意,以上的使用都是基于RedisTemplate、基于连接池的操作,换句话说,并不能保证每次使用RedisTemplate是操作同一个对Redis的连接。
- set和get方法可拿起来很简单,它可能就来自于一个Redis连接池的不同Redis的连接。
- 为了使得所有的操作都来自于同一个连接,可以使用SessionCallback或者RedisCallback这两个接口,而RedisCallBack是比较底层的封装,其使用不是很友好,所以更多的时候会使用SessionCallback这个接口。
1 | //ApplicationContext applicationContext = new AnnotationConfigApplicationContext(RedisConfig.class); |
Redis的6种数据类型
数据类型 | 数据类型存储的值 | 说明 |
---|---|---|
STRING(字符串) | 可以是保存字符串、整数和浮点数 | 可以对字符串进行操作,比如增加字符或者求子串;如果是整数或者浮点数,可以实现计算,比如自增等 |
LIST(列表) | 它是一个链表,它的每一个节点都包含一个字符串 | Redis支持从链表的两端插入或者弹出节点,或者通过偏移对它进行剪裁;还可以读取一个或者多个节点,根据条件删除或者查找节点等 |
SET(集合) | 它是一个收集器,但是是无序的,在它里面每一个元素都是一个字符串,而且是独一无二,各不相同的 | 可以新增、读取、删除单个元素;检测一个元素是否在集合中;计算它和其他集合的交集、并集和差集等;随机从集合中读取元素 |
HASH(哈希散列表) | 它类似于Java语言中的Map,是一个键值对应的无序列表 | 可以增、删、查、改单个键值时,也可以获取所有的键值对 |
ZSET(有序集合) | 它是一个有序的集合,可以包含字符串、整数、浮点数、分值(score),元素的排序是依据分值的大小来决定的 | 可以增、删、查、改元素,根据分值的范围或者成员来获取对应的元素 |
HyperLogLog(基数) | 它的作用是计算重复的值,以确定存储的数量 | 只提供基数的运算,不提供返回的功能。 |
Redis和数据库的异同
- NoSQL的数据主要存储在内存中(部分可以持久化到磁盘),而数据库主要是磁盘;
- NoSQL数据结构比较简单,虽然能处理很多的问题,但是其功能毕竟是有限的,不如数据库的SQL语句强大,支持更为复杂的计算;
- NoSQL并不完全安全稳定,由于它基于内存,一旦停电或者机器故障数据就很容易丢失数据,其持久化能力也是有限的,而基于磁盘的数据库则不会出现这样的问题;
- NoSQL的数据完整性、事务能力、安全性、可靠性及可扩展性都不及数据库
Redis数据结构常用命令
Redis数据结构——字符串
常用操作
命令 | 说明 | 备注 |
---|---|---|
set key value | 设置键值对 | 最常用的写入命令 |
get key | 通过键获取值 | 最常用的读取命令 |
del key | 通过key,删除键值对 | 删除命令,返回删除数,注意,它是一个通用的命令,换句话说在其他数据结构中,也可以使用它 |
strlen key | 求key指向字符串的长度 | 返回长度 |
getset key value | 修改原来key的对应值,并将旧值返回 | 如果原来值为空,则返回为空,并设置新值 |
getrange key start end | 获取子串 | 记字符串的长度为len,把字符串看作一个数组,而Redis是以0开始计数的,所以start和end的取值范围为0到len-1 |
append key value | 将新的字符串value,加入到原来的key指向的字符串末 | 返回key指向新字符串的长度 |
本地操作测试
1 | D:\download\Redis-x64-3.2.100>redis-cli.exe -h 127.0.0.1 -p 6379 |
Spring操作测试
1 | "redisTemplate") (name = |
1 | ApplicationContext applicationContext = new AnnotationConfigApplicationContext(RedisConfig.class); |
1 | value1 |
简单运算
如果字符串是数字(整数或者浮点数),那么Redis还能支持简单的运算
命令 | 说明 | 备注 |
---|---|---|
incr key | 在原字段上加1 | 只能对整数操作 |
incrby key increment | 在原字段上加上整数(increment) | 只能对整数操作 |
decr key | 在原字段上减1 | 只能对整数操作 |
decrby key decrement | 在原字段上减去整数(decrement) | 只能对整数操作 |
incrbyfloat key increment | 在原字段上加上浮点数(increment) | 可以操作浮点数或者整数 |
本地测试
1 | 127.0.0.1:6379> set val 8 |
Spring测试
1 | ApplicationContext applicationContext = new AnnotationConfigApplicationContext(RedisConfig.class); |
1 | ApplicationContext applicationContext = new AnnotationConfigApplicationContext(RedisConfig.class); |
1 | 9 |
Redis数据结构——哈希
命令 | 说明 | 备注 |
---|---|---|
hdel key field1 [field2……] | 删除hash结构中的某个(些)字段 | 可以进行多个字段的删除 |
hexists key field | 判断hash结构中是否存在field字段 | 存在返回1,否则返回0 |
hgetall key | 获取所有hash结构中的键值 | 返回键和值 |
hincrby key field increment | 指定给hash结构中的某一字段加上一个整数 | 要求该字段也是整数字符串 |
hincrbyfloat key field increment | 指定给hash结构中的某一字段加上一个浮点数 | 要求该字段是数字型字符串 |
hkeys key | 返回hash中所有的键 | |
hlen key | 返回hash中键值对的数量 | |
hmget key field1 [field2……] | 返回hash中指定的键的值,可以是多个 | 依次返回值 |
hmget key filed1 value1 [field2 value2] | hash结构设置多个键值对 | |
hset key field value | 在hash结构中设置键值对 | 单个设值 |
hsetnx key field value | 当hash结构中不存在对应的键,才设置值 | |
hvals key | 获取hash结构中所有的值 |
- 哈希结构的大小,如果哈希结构是个很大的键值对,那么使用它要十分注意,尤其是关于hkeys、hgetall、hvals等返回所有哈希结构数据的命令,会造成大量数据的读取。这需要考虑性能和读取数据大小对JVM内存的影响。
- 对于数字的操作命令hincrby而言,要求存储的也是整数型的字符串;对于hincrbyfloat而言,则要求使用浮点数或者整数,否则命令会失败。
本地测试
1 | 127.0.0.1:6379> hmset role_1 id 001 roleName role_name_001 note note_001 |
Spring测试
1 | ApplicationContext applicationContext = new AnnotationConfigApplicationContext(RedisConfig.class); |
1 | 6 |
Redis数据结构——链表(linked-list)
Redis的链表是双向链表,支持左右插入删除等操作。
常规命令
命令 | 说明 | 备注 |
---|---|---|
lpush key node1 [node2..]… | 把节点node1加入到链表最左边 | 如果是node1、node2…noden这样加入,那么链表开头从左到右的顺序是noden…node2、node1 |
rpush key nod1 [node2..]… | 把节点node1加入到链表的最右边 | 如果node1、node2…noden这样加入,那么链表开头从左到右的顺序是node1、node2…noden |
lindex key index | 读取下标为index的节点 | 返回节点字符串,从0开始算 |
llen key | 求链表的长度 | 返回链表节点数 |
lpop key | 删除左边第一个节点,并将其返回 | |
rpop key | 删除右边第一个节点,并将其返回 | |
linsert key before|after pivot node | 插入一个节点node,并且可以指定在值为pivot的节点的前面(before)或者后面(after) | 如果list不存在,则报错;如果没有值为对应pivot的,也会插入失败返回-1 |
lpushx list node | 如果存在key为list的链表,则插入节点node,并且作为从左到右的第一个节点 | 如果list不存在,则失败 |
rpushx list node | 如果存在key为list的链表,则插入节点node,并且作为从左到右的最后一个节点 | 如果list不存在,则失败 |
lrange list start end | 获取链表list从start下标到end下标的节点值 | 包含start和end下标的值 |
lrem list count value | 如果count为0,则删除所有值等于value的节点;如果count不是0,则先对count取绝对值,假设记为abs,然后从左到右删除不大于abs个等于value的节点 | 注意,count为整数,如果是负数,则Redis会先求取其绝对值,然后传递到后台操作 |
lset key index node | 设置列表下标为index的节点的值为node | |
ltrim key start stop | 修剪链表,只保留从start到stop的区间的节点,其余的都删除掉 | 包含start和end的下标的节点会保留 |
本地测试
1 | D:\download\Redis-x64-3.2.100>redis-cli.exe -h 127.0.0.1 -p 6379 |
链表的阻塞命令
命令 | 说明 | 备注 |
---|---|---|
blpop key timeout | 移出并获取列表的第一个元素,如果列表没有元素会阻塞列表直到等待超时或发现可弹出元素为止 | 相对于lpop命令,它的操作是进程安全的 |
brpop key timeout | 移出并获取列表的最后一个元素,如果列表没有元素会阻塞列表直到等待超时或发现可弹出元素为止 | 相对于rpop命令,它的操作是进程安全的 |
rpoplpush key src dest | 按从左到右的顺序,将一个链表的最后一个元素移除,并插入到目标链表最左边 | 不能设置超时时间 |
brpoplpush key src dest timeout | 按从左到右的顺序,将一个链表的最后一个元素移除,并插入到目标链表最左边,并可以设置超时时间 | 可设置超时时间 |
本地测试
1 | 127.0.0.1:6379> lpush list1 node1 node2 node3 node4 node5 |
Spring测试
常规操作测试
1 | package com.louris.springboot.demo; |
1 | [new_head_value, before_node, node2, node3, end] |
阻塞操作测试
1 | package com.louris.springboot.demo; |
1 | [node5, node4, node3, node2, node1] |
Redis数据结构——集合
理论上一个集合可以存储$2^{31} - 1$个元素,因为采用哈希表结构,所以对于Redis集合的插入、删除和查找的时间复杂度都是$O(1)$
命令 | 说明 | 备注 |
---|---|---|
sadd key member1 [member2 member3 ……] | 给键为key的集合增加成员 | 可以同时增加多个 |
scard key | 统计键为key的集合成员数 | |
sdiff key1 [key2] | 找出两个集合的差集 | 参数如果是单key,那么Redis就返回这个key的所有元素 |
sdiffstore des key1 [key2] | 先按sdiff命令的规则,找出key1和key2两个集合的差集,然后将其保存到des集合中 | |
sinter key1 [key2] | 求key1和key2两个集合的交集 | 参数如果是单key,那么Redis就返回这个key的所有元素 |
sinterstore des key1 key2 | 先按sinter命令的规则,找出key1和key2两个结合的交集,然后保存到des中 | |
sismember key member | 判断member是否键为key的集合的成员 | 如果是返回0,否则返回0 |
smember key | 返回集合所有成员 | 如果数据量大,需要考虑迭代遍历的问题 |
smove src des member | 将成员member从集合src迁移到集合des中 | |
spop key | 随机弹出集合的一个元素 | 注意其随机性,因为集合是无序的 |
srandmember key [key] | 随机返回集合中一个或者多个元素,count为限制返回总数,如果count为负数,则先求其绝对值 | count为整数,如果不填默认为1,如果count大于等于集合总数,则返回整个集合 |
srem key member1 [member2……] | 移出集合中的元素,可以是多个元素 | 对于很大的集合可以通过它删除部分元素,避免删除大量数据引发Redis停顿 |
sunion key1 [key2] | 求两个集合的并集 | 参数如果是单key,那么Redis就返回这个key的所有元素 |
sunionstore des key1 key2 | 先执行sunion命令求出并集,然后保存到键为des的集合中 |
本地测试
1 | D:\download\Redis-x64-3.2.100>redis-cli.exe -h 127.0.0.1 -p 6379 |
Spring测试
1 | ApplicationContext applicationContext = new AnnotationConfigApplicationContext(RedisConfig.class); |
Redis数据结构——有序集合
添加、删除、查找的时间复杂度都是$O(1)$,集合最大成员数为$2^{31} - 1$
有序集合,可以支持对分数排序,底层实现有两种方式:字典和跳表
Redis基础命令
命令 | 说明 | 备注 |
---|---|---|
zadd key score1 value1 [score2 value2 ……] | 向有序集合的key,增加一个或者多个成员 | 如果不存在对应的key,则创建键为key的有序集合 |
zcard key | 获取有序集合的成员数 | |
zcount key min max | 根据分数返回对应的成员列表 | min为最小值,max为最大值,默认为包含min和max值,采用数学区间表示的方法,如果需要不包含,则在分数前面加入”(“,注意支持”[“ |
zincrby key increment member | 给有序集合成员值为member的分数增加increment | |
zinterstore desKey numkeys key1 [key2 key3 ……] | 求多个有序集合的交集,并且将结果保存到desKey中 | numKeys是一个整数,表示多个有序结合 |
zlexcount key min max | 求有序集合key成员值在min和max的范围 | 这里范围为key的成员值,Redis借助数据区间的表示方法,”[“表示包含该值,”(“表示不包含该值 |
zrange key start stop [winthscores] | 按照分值的大小(从小到大)返回成员,加入start和stop参数可以截取某一段返回。如果输入可选项withscores,则连同分数一起返回 | 这里记集合最大长度为len,则Redis会将集合排序后,形成一个从0到len-1的下标,然后根据start和stop控制的下标(包含start和stop)返回 |
zrank key member | 按从小到大求有序集合的排行 | |
zrangebylex key min max [limit offset count] | 根据值的大小,从小到大排序,min为最小值,max为最大值;limit选项可选,当Redis求出范围集合后,会产生下标0到n,然后根据偏移量offset和限定返回数count,返回对应的成员 | |
zrangebyscore key min max[withscores] [limit offset count] | 根据分数大小,从小到大求取范围,选项withscores和limit同上 | |
zremrangebyscore key start stop | 根据分数区间进行删除 | |
zremrangebyrank key start stop | 按照分数排行从小到大的排序删除,从0开始计算 | |
zremrangebylex key min max | 按照值的分布进行删除 | |
zrevrange key start stop [withscores] | 从大到小的按分数排序,参数同上 | |
zrevrangebyscore key max min [withscore] | 从大到小的按分数排序,参数同上 | |
zrevrank key member | 按从大到小的顺序,求元素的排行 | |
zscore key member | 返回成员的分数值 | |
zunionstore desKey numKeys [key1 key2 key3 key4 ……] | 求多个有序集合的并集,其中numKeys是有序集合的个数 |
本地测试
1 | 127.0.0.1:6379> zadd zset1 1 x1 2 x2 3 x3 4 x4 5 x5 6 x6 7 x7 8 x8 9 x9 |
Spring测试
1 | public static void main(String[] args) { |
1 | x2 |
基数——HyperLogLog
基数是一种算法。举个例子,一本英文著作由数百万个单词组成,你的内存却不足以存储它们,那么我们先分析一下业务。英文单词本身是有限的,在这本书的几百万个单词中有许许多多重复单词,扣去重复的单词,这本书中也就是几千到一万多个单词而已,那么内存就足够存储它们了。基数的作用是评估大约需要准备多少个存储单元去存储数据,但是基数的算法一般会存在一定的误差(一般是可控的)。
基数并不是存储元素,存储元素消耗内存空间比较大,而是给某一个有重复元素的数据集和(一般是很大的数据集合)评估需要的空间单元数,所以它没有办法进行存储,工作中用得不多。
命令 | 说明 | 备注 |
---|---|---|
pfadd key element | 添加指定元素到HyperLogLog中 | 如果已经存储元素,则返回为0,添加失败 |
pfcount key | 返回HyperLogLog的基数值 | |
pfmerge desKey key1 [key2 key3……] | 合并多个HyperLogLog,并将其保存在desKey中 |
1 | ApplicationContext applicationContext = new AnnotationConfigApplicationContext(RedisConfig.class); |
1 | 4 |
Redis的一些常用技术
Redis的事务时使用MULTI-EXEC的命令组合,使用它可以提供两个重要的保证:
- 事务是一个被隔离的操作,事务中的方法都会被Redis进行序列化并按顺序执行,事务在执行的过程中不会被其他客户端发生的命令所打断;
- 事务是一个原子性的操作,它要么全部执行,要么就什么都不执行
在一个Redis的连接中,请注意要求是一个连接,所以更多的时候在使用Spring中会使用SessionCallback接口进行处理,在Redis中使用事务会经过3个过程:
- 开启事务
- 命令进入队列
- 执行事务
命令 | 说明 | 备注 |
---|---|---|
multi | 开启事务命令,之后的命令就进入队列,而不会马上被执行 | 在事务生存期间,所有的Redis关于数据结构的命令都会入队 |
watch key1 [key2 ……] | 监听某些键,当被监听的键在事务执行前被修改,则事务会被回滚 | 使用乐观锁 |
unwatch key1 [key2 ……] | 取消监听某些键 | |
exec | 执行事务,如果被监听的键没有被修改,则采用执行命令,否则就回滚事务 | 在执行事务队列存储的命令前,Redis会检测被监听的键值对有没有发生变化,如果没有则执行命令,否则就回滚事务 |
discard | 回滚事务 | 回滚进入队列的事务命令,之后就不能再用exec命令提交了 |
Redis的基础事务
本地测试
在Redis中开启事务是multi命令,而执行事务时exec命令。multi到exec命令之间的Redis命令将采取进入队列的形式,直至exec命令的出现,才会一次性发送队列里的命令去执行,而在执行这些命令的时候其他客户端就不能再插入任何命令了,这就是Redis的事务机制。
1 | 127.0.0.1:6379> multi |
如果回滚事务,则可以使用discard命令,它就会进入在事务队列中的命令,这样事务中的方法就不会被执行了。
1 | 127.0.0.1:6379> multi |
Spring测试
1 | redisTemplate.setEnableTransactionSupport(true); |
1 | ApplicationContext applicationContext = new AnnotationConfigApplicationContext(RedisConfig.class); |
1 | 事务执行过程中,命令入队列,而没有被执行,所以value为空:value=null |
探索Redis事务回滚
对于Redis而言,不单单需要注意其事务处理的过程,其回滚的能力也和数据库不太一样。
在命令入队的时候,Redis就会检测事务的命令是否正确:
- 如果不正确则会产生错误。无论之前和之后的命令都会被事务所回滚,就变为什么都没有执行;
- 如果命令格式正确,而因为操作数据结构引起的错误,则该命令执行出现错误,而其之前和之后的命令都会被正常执行。
对于一些重要的操作,我们需要通过程序去检测数据的正确性,以保证Redis事务的正确执行,避免出现数据不一致的情况。Redis之所以保持这样简易的事务,完全是为了保证移动互联网的核心问题——性能。
命令正确,数据类型错误
1 | 127.0.0.1:6379> multi |
命令错误
1 | 127.0.0.1:6379> multi |
使用watch命令监控事务
在Redis中使用watch命令可以决定事务是执行还是回滚。一般而言,可以在multi命令之前使用watch命令监控某些键值对,然后使用multi命令开启事务,执行各类对数据结构进行操作的命令,这个时候这些命令就会进入队列。当Redis使用exec命令执行事务的是偶,它首先会去比对被watch命令所监控的键值对,如果没有发生变化,那么它会执行事务队列中的命令,提交事务;如果发生变化,那么它不会执行任何事务中的命令,而去事务回滚。无论事务是否回滚,Redis都会去取消执行事务前的watch命令。
正常事务提交
1 | 127.0.0.1:6379> flushdb |
事务回滚
客户端1:
1 | 127.0.0.1:6379> flushdb |
客户端2:在客户端1 设置key2键值对时,修改key1:
1 | 127.0.0.1:6379> set key1 val1 |
流水线(pipelined)
在事务中Redis提供了队列,这是一个可以批量执行任务的队列,这样性能就比较高,但是使用multi…exec事务命令是有系统开销的,因为它会检测对应的锁和序列化命令。有时候我们希望在没有任何附加条件的场景下去使用队列批量执行一系列的命令,从而提高系统性能,这就是Redis的流水线技术。
Java API测试
1 | import redis.clients.jedis.Jedis; |
1 | 耗时:742毫秒 |
Spring测试
1 | ApplicationContext applicationContext = new AnnotationConfigApplicationContext(RedisConfig.class); |
1 | 351 |
发布订阅
当使用银行卡消费的时候,银行往往会通过微信、短信或邮件通知用户这笔交易的信息,这便是一种发布订阅模式,这里的发布是交易信息的发布,订阅则是各个渠道。这在实际工作中十分常用,Redis支持这样的一个模式。
- 要有发送的消息渠道,让记账系统能够发送消息;
- 要有订阅者(短信、邮件、微信等系统)订阅这个渠道的消息
本地测试
可以看到客户端2在publish
消息后,客户端1收到了消息。
客户端1:订阅了一个名为chat的渠道
1 | C:\Program Files\Redis>redis-cli.exe -h 127.0.0.1 -p 6379 |
客户端2:向渠道chat发送消息
1 | C:\Program Files\Redis>redis-cli.exe -h 127.0.0.1 -p 6379 |
Spring测试
Redis发布订阅监听类
1 | package com.louris.springboot.utils; |
RedisConfig配置类
1 | package com.louris.springboot.IoCImpl.config; |
启动类
1 | ApplicationContext applicationContext = new AnnotationConfigApplicationContext(RedisConfig.class); |
测试结果
1 | I am lazy!! |
超时命令
命令 | 说明 | 备注 |
---|---|---|
persist key | 持久化key,取消超时时间 | 移除key的超时时间 |
ttl key | 查看key的超时时间 | 以秒计算,-1代表没有超时时间,如果不存在key或者key已经超时则为-2 |
expire key seconds | 设置超时时间戳 | 以秒为单位 |
expireat key timestamp | 设置超时时间点 | 用uninx时间戳确定 |
pptl key milliseconds | 查看key的超时时间戳 | 用毫秒计算 |
pexpire key | 设置键值超时的时间 | 以毫秒为单位 |
Pexpireat key stamptimes | 设置超时时间点 | 以毫秒为单位的uninx时间戳 |
本地测试
1 | 127.0.0.1:6379> set key1 value1 |
Spring测试
1 | ApplicationContext applicationContext = new AnnotationConfigApplicationContext(RedisConfig.class); |
如果key超时了,Redis会回收key的存储空间吗?
答案是不会。
Redis的key超时不会被其自动回收,它只会标识哪些键值对超时了。这样做的一个好处在于,如果一个很大的键值对超时,比如一个列表或者哈希结构,存在数以百万个元素,要对其回收需要很长的事件。如果采用超时回收,则可能产生停顿。坏处也很明显,这些超时的键值对会浪费比较多的空间。
Redis提供两种方式回收这些超时键值对:
- 定时回收:指在确定的某个事件触发一段代码,回收超时的键值对
- 惰性回收则是当一个超时的键,被再次用get命令访问时,将触发Redis将其从内存中清空
使用Lua语言
在Redis的2.6以上版本中,除了可以使用命令外,还可以使用Lua语言操作Redis,从前面的命令可以Redis命令的计算鞥哪里并不算很强大,而使用Lua语言则在很大程度上弥补了Redis的这个不足。只是在Redis中,执行Lua语言是原子性的,这个特性有助于Redis对并发数据一致性的支持。
执行输入Lua程序代码
一般命令格式
命令格式:eval lua-script key-num [key1 key2 key3 ...] [value1 value2 value3 ...]
其中:
- eval代表执行Lua语言的命令
- Lua-script代表Lua语言脚本
- key-num整数代表参数中有多少个key,需要注意的是Redis中key是从1开始的,如果没有key的参数,那么写0
[key1 key2 key3 ...]
是key作为参数传递给Lua语言,也可以不填它是key的参数,但是需要和key-num的个数对应起来。[value1 value2 value3 ...]
这些参数传递给Lua语言,它们是可填可不填的。
1 | C:\Program Files\Redis>redis-cli.exe -h 127.0.0.1 -p 6379 |
设置一个键值对,可以在Lua语言中采用redis.call(command, key[param1, param2...])
进行操作,其中:
- command是命令,包括set、get、del等
- Key是被操作的键
- param1,param2…代表给key的参数。
KEYS[1]代表读取传递给Lua脚本的第一个key参数,而ARGV[1]代表第一个非key参数。
多次执行同一脚本
有时可能需要多次执行同样一段样本,这个时候可以使用Redis缓存脚本的功能,在Redis中脚本会通过SHA-1签名算法加密脚本,然后返回一个标识字符串,可以通过这个字符串执行加密后的脚本。这样的一个好处在于,如果脚本很长,从客户端传输可能需要很长的时间,那么使用标识字符串,则只需要传递32位字符串即可,这样就能够提高传输的效率,从而提高性能。
首先使用命令:script load script
这个脚本的返回值是一个SHA-1签名过后的标识字符串记为shastring,通过它可以使用命令执行签名后的脚本:evalsha shastring keynum [key1 key2 key3 ...] [param1 param2 param3 ...]
1 | 127.0.0.1:6379> script load "redis.call('set', KEYS[1], ARGV[1])" |
Spring测试
1 | ApplicationContext applicationContext = SpringApplication.run(Application.class, args); |
1 | hello java |
1 | ApplicationContext applicationContext = SpringApplication.run(Application.class, args); |
1 | aac72bd8699ae9c333eff85158365d7bc61a7177 |
执行Lua文件
本地测试
1 | redis.call('set', KEYS[1], ARGV[1]) |
需要特别注意,键和参数之间需要用逗号隔开,逗号前后必须都有空格!!
1 | C:\Program Files\Redis>redis-cli --eval test.lua key1 key2, 2 4 |
Spring测试
1 | public static void main(String[] args) { |
1 | 2 |
Redis配置
Redis基础配置文件
- Windows系统,默认的配置文件就是redis.window.conf;
- Linux系统,则是redis.conf
Redis备份(持久化)
Redis中存在两种方式的备份:
- 快照(snapshotting),它是备份当前瞬间Redis在内存中的数据记录;
- 只追加文件(Append-Only File, AOF),其作用就是当Redis执行写命令后,在一定的条件下降执行过的写命令依次保存在Redis的文件中,将来就可以依次执行那些保存的命令恢复Redis的数据了。
(1)对于快照备份而言,如果当前Reids的数据量大,备份可能造成Redis卡顿,但是恢复重启比较快速。
(2)对于AOF备份而言,它只是追加写入命令,所以备份一般不会造成Redis卡顿,但是恢复重启要执行更多的命令,备份文件可能也很大,使用者使用的时候需要注意。
Redis中允许使用其中的一种、同时使用两种,或者两种都不用。
快照模式的配置项
1 | save 900 1 |
配置项的含义:
- 当900秒执行1个写命令时,启用快照备份;
- 当300秒执行10个写命令时,启用快照备份;
- 当60秒内执行10000个写命令时,启用快照备份
Redis执行save命令的时候,将禁止写入命令
1 | stop-writes-on-bgsave-error yes |
bgsave
命令,它是一个异步保存命令,也就是系统将启动另外一条进程,把Redis的数据保存到对应的数据文件中。它和save命令最大的不同是它不会阻塞客户端的写入,也就是在执行bgsave的时候,允许客户端继续读/写Redis。- 在默认情况下,如果Redis执行bgsave失败后,Redis将停止接受写操作,这样以一种强硬的方式让用户知道数据不能正确的持久化到磁盘,否则就会没人注意到灾难的发生;
- 如果后台保存进程重新启动工作了,Redis也将自动允许写操作。
- 然而如果安装了靠谱的监控,可能不希望Redis这样做,可以将其修改为no
1 | rdbchecksum yes |
这个命令的意思是是否对rdb文件进行检验
rdb文件实际是Redis持久化的数据文件,当采用快照模式备份时,Redis将使用它保存数据,将来可以使用它恢复数据
1 | dbfilename dump.rdb |
只追加文件模式的配置项
1 | appendonly no |
如果appendonly配置为no,则不启用AOF方式进行备份。如果appendonly配置为yes,则以AOF方式备份Redis数据,那么此时Redis会按照配置,在特定的时候执行追加命令,用以备份数据。
1 | appendfilename "appendonly.aof" |
这里定义追加的写入文件为appendonly.aof,采用AOF追加文件备份的时候命令都会写到这里。
1 | # appendfsync always |
- AOF文件和Redis命令是同步频率的,假设配置为always,其含义为当Redis执行命令的时候,则同时同步到AOF文件,这样会使得Redis同步刷新AOF文件,造成缓慢。
- 而采用everysec则代表每秒同步一次命令到AOF文件。
- 采用no的时候,则由客户端调用命令执行备份,Redis本身不备份文件。
- 对于always配置的时候,每次命令都会持久化,它的好处在于安全,坏处在于每次都持久化性能较差。
- 采用everysec的安全性不如always,别分可能会丢失一秒以内的命令,但是银行也不大,安全度上课,性能可以得到保障。
- 采用no,则性能有所保障,但是由于失去备份,所以安全性比较差。
- 建议采用默认配置everysec
1 | no-appendfsync-on-rewrite no |
它指定是否在后台AOF文件rewrite(重写)期间调用fsync,默认为no,表示要调用fsync(无论后台是否有子进程在刷盘)。Redis在后台写RDB文件或重写AOF文件期间会存在大量磁盘I/O,此时,在某些Linux系统中,调用fsync可能会阻塞。
1 | auto-aof-rewrite-percentage 100 |
它指定Redis重写AOF文件的条件,默认为100,表示与上次rewrite的AOF文件大小相比,当前AOF文件增长量超过上次AOF文件大小的100%时,就会触发background rewrite。若配置为0,则会禁用自动rewrite。
1 | auto-aof-rewrite-min-size 64mb |
它指定触发rewrite的AOF文件大小。若AOF文件小于该值,即使当前文件的增量比例达到auto-aof-rewrite-percentage的配置值,也不会触发自动rewrite。即这两个配置项同时满足时,才会触发rewrite。
1 | aof-load-truncated yes |
Redis在恢复时会忽略最后一条可能存在问题的指令,默认为yes。即在AOF写入时,可能存在指令写错的问题(突然断电、写了一半),这种情况下yes会log并继续,而no会直接恢复失败。
Redis内存回收策略
Redis也会因为内存不足而产生错误,也可能因为回收过久而导致系统长期的停顿,因此掌握执行回收策略十分有必要。在Redis的配置文件中,当Redis的内存达到规定的最大值时,允许配置6种策略中的一种进行淘汰键值,并且将一些键值对进行回收。
1 | # volatile-lru -> Evict using approximated LRU. only keys with an expire set. |
- volatile-lru:采用最近使用最少的淘汰策略,Redis将回收那些超时的(仅仅是超时的)键值对,也就是它只淘汰那些超时的键值对;
- allkeys-lru:采用淘汰最少使用的策略,Redis将对所有的(不仅仅是超时的)键值对采用最近使用最少的淘汰策略;
- volatile-random: 采用随机淘汰策略删除超时的(仅仅是超时的)键值对;
- allkeys-random:采用随机淘汰策略删除所有的(不仅仅是超时的)键值对,这个策略不常用;
- volatile-ttl:采用删除存货时间最短的键值对策略;
- noeviction:根本就不淘汰任何键值对,当内存已满时,如果做读操作,例如get命令,它将正常工作,而做些操作,它将返回错误。也就是说,当Redis采用这个策略内存达到最大的时候,它就只能读不能写了。
Redis在默认情况下会曹勇noeviction策略。换句话说,如果内存已满,则不再提供写入操作,而只提供读取操作。显然这往往并不能满足我们的要求,因为对于互联网系统而言,常常会涉及数以百万甚至更多的用户,所以往往需要设置回收策略。
注意:LRU算法或者TTL算法都不是很精确算法,而是一个近似算法。Redis不会通过对全部的键值对进行比较来确定最精确的时间值,从而确定删除哪个键值对,因为这将消耗太多的时间,导致回收垃圾执行的时间太长,造成服务停顿。而在Redis的默认配置文件中,存在着参数maxmemory-samples
,它的默认值为3,假设采取了volatile-ttl算法:
假设当前有5个即将超时的键值对:
键值对 | 剩余超时秒数 | 备注 |
---|---|---|
A1 | 6 | 属于探测样本 |
A2 | 3 | 属于探测样本中最短值,因此最先删除 |
A3 | 4 | 属于探测样本 |
A4 | 1 | 最短值,但是它不属于探测样本,所以没有最先删除 |
A5 | 9 | 但不属于样本 |
由于配置maxmemory-samples的值为3,如果Redis是按表中的顺序探测,那么它只会取到样本A1、A2、A3,然后进行比较,因为A2国企剩余秒数最少,所以决定淘汰A2,因此A2是最先被删除的。注意,此时即将过期且剩余超时秒数最短的A4却还在内存中,因为它不属于探测样本。这就是Redis中采用的近似算法。当设置maxmemory-samples越大,则Redis删除的就越精确,但是与此同时带来不利的是,Redis也就需要花更多的事件去计算和匹配更为精确的值。
回收超时策略的缺点是必须指明超时的键值对,这会给程序开发带来一些设置超时的代码,无疑增加了开发者的工作量。对所有的键值对进行回收,有可能把正在使用的键值对删掉,增加了存储的不稳定性。对于垃圾回收的策略,还需要注意的是回收的时间,因为在Redis对垃圾的回收期间,会造成系统缓慢。因此,控制其回收时间有一定好处,只是这个时间不能过短或过长。过短则会造成回收次数过于频繁,过长则导致系统单次垃圾回收停顿时间过长,都不利于系统的稳定性,这些都需要设计者在实际的工作中进行思考。
复制
尽管Redis的性能很好,但是有时候依旧满足不了应用的需要,比如过多的用户进入主页,导致Redis被频繁访问,此时就存在大量的读操作。对于一些热门网站的某个时刻(比如促销商品的时候)每秒成千上万的请求是司空见惯的,这个时候大量的读操作就会到达Redis服务器,触发许许多多的操作,显然单靠一台Redis服务器是完全不够用的。一些服务网站对安全性有较高的要求,当主服务器不能正常工作的时候,也需要从服务器代替原来的主服务器,作为灾备,以保证系统可以继续正常的工作。因此更多的时候我们更希望可以读/写分离,读/写分离的前提是读操作远远比写操作频繁得多,如果把数据都存放在多台服务器上那么就可以从多台服务器中读取数据,从而消除了单台服务器的压力,读/写分离的技术已经广泛用于数据库中了。
主从同步基本概念
主从架构设计的思路大概是:
- 在多台数据服务器中,只有一台主服务器,而主服务器只负责写入数据,不负责让外部程序读取数据;
- 存在多台从服务器,从服务器不写入数据,只负责同步主服务器的数据,并让外部程序读取数据;
- 主服务器在写入数据后,即刻将写入数据的命令发送给从服务器,从而使得主从数据同步;
- 应用程序可以随机读取某一台从服务器的数据,这样就分摊了读数据的压力;
- 当从服务器不能工作的时候,整个系统将不受影响;当主服务器不能工作的时候,可以方便地从服务器中选举一台来当主服务器。
Redis主从同步配置
对Redis进行主从同步的配置分为主机与从机,主机是一台,而从机可以是多台。
(1)首先,明确主机。当确定哪台机子是主机的时候,关键的两个配置时dir
和dbfilename
选项,当然必须保证这两个文件时可写的。对于Redis的默认配置而言,dir的默认值为./
,而对于dbfilename的默认值为dump.rbd
。换句话说,默认采用Redis当前的目录的dump.rdb文件进行同步。
(2)其次,在明确了从机之后,进行进一步配置所要关注的只有replicaof
这个配置选项,它的配置格式为:
1 | replicaof <masterip> <masterport> |
其中,masterip代表主机,port代表端口。
当从机Redis服务重启时,就会同步对应主机的数据了。当不想让从机继续复制主机的数据时,可以在从机的Redis命令客户端发送slaveof no one
命令,这样从机就不会再接收主服务器的数据更新了。又或者原来主服务器已经无法工作了,而你可能需要去复制新的主机,这个时候执行slaveof server port
就能让从机复制另外一台主机的数据了。
(3)在实际的Linux环境中,配置文件redis.conf
中还有一个bind
的配置,默认为127.0.0.1
,也就是只允许本机访问,把它修改为bind 0.0.0.0
,其他的服务器就能够访问了。bing
是绑定本机的IP地址,port
绑定端口,允许其他人访问该IP的端口进行redis连接。
Redis主从同步的过程
略,详见书。
哨兵(Sentinel)模式
- 主从切换技术的方法是:当主服务器宕机后,需要手动把一台从服务器切换为主服务器,这就需要人工干预,既费时费力,还回造成一段时间内服务不可用,这不是一种推荐的方式。
- 更多时候,优先考虑哨兵模式,它是当前企业应用的主流方式。
哨兵模式概述
Redis可以存在多台服务器,并且实现了主从复制的功能。哨兵模式是一种特殊的模式,首先Redis提供了哨兵的命令,哨兵是一个独立的进程,作为进程,它会独立运行。其原理是哨兵通过发送命令,等待Redis服务器响应,从而监控运行的多个Redis实例。
哨兵的两个作用:
- 通过发送命令,让Redis服务器返回检测其运行状态,包括主服务器核从服务器;
- 当哨兵检测到master宕机,会自动将slave切换成master,然后通过发布订阅模式通知到其他的从服务器,修改配置文件,让它们切换主机;
现实中只是一个哨兵进程对Redis服务器进行监控,也可能出现问题,为了处理这个问题,还可以使用多个哨兵的监控,而各个哨兵之间还会相互监控,这样就变为了多个哨兵模式。多个哨兵不仅监控各个Redis服务器,而且哨兵之间相互监控,看看哨兵们是否还“活”着。
故障切换(failover)的过程:假设主服务器宕机,哨兵1先检测到这个结果,当时系统并不会马上进行failover操作,而仅仅是哨兵1主观地认为主机已经不可用,这个现象被称为主观下线。当后面的哨兵也监测到了主服务器不可用,并且有了一定数量的哨兵认为主服务器不可用,那么哨兵之间就会形成一次投票,投票的结果由一个哨兵发起,进行failover操作,在failover操作的过程中切换成功后,就会通过发布订阅方式,让各个哨兵把自己监控的服务器实现切换主机,这个过程被称为客观下线。这样对于客户端而言,一切都是透明的。
搭建哨兵模式
略,以后用到了再来搞。
Spring缓存机制和Redis的结合
Redis和数据库的结合
Redis和数据库读操作
数据缓存往往会在Redis上设置超时时间,当设置Redis的数据超时后,Redis就没法读出数据了,这个时候就会触发程序读取数据库,然后将读取的数据库数据写入Redis(此时会给Redis重设超时时间),这样程序在读取的过程中就能按一定的时间间隔刷新数据了。
伪代码:
1 | public DataObject readMethod(args){ |
Redis和数据库写操作
- 写操作要考虑数据一致的问题,尤其是那些重要的业务数据。
- 写入业务数据,先从数据库中读取最新数据,然后进行业务操作,更新业务数据到数据库后,再将数据刷新到Redis缓存中,这样就完成了一次写操作。这样的操作就能避免将脏数据写入数据库汇总,这类问题在操作时要注意。
伪代码
1 | public DataObject writeMethod(args){ |
使用Spring缓存机制整合Redis
准备测试环境
配置MySQL和Redis以及Spring Cache配置
配置文件配置方式(推荐)
1 | 8081 = |
配置类自定义配置缓存管理器配置方式
有时候,在自定义时可能存在比较多的设置,也可以不采用Spring Boot自动配置的缓存管理器,而是使用自定义的缓存管理器,这也是没有问题的。
1 | package com.louris.springboot.IoCImpl.config; |
POJO
1 | package com.louris.springboot.IoCImpl.beans.pojo; |
RoleMapper.xml
1 |
|
RoleData持久层接口
1 | package com.louris.springboot.IoCImpl.mapper; |
RoleService服务接口
1 | package com.louris.springboot.IoCImpl.mapper; |
缓存注解简介
注解 | 描述 |
---|---|
@Cacheable | 表明在进入方法之前,Spring会先去缓存服务器中查找对应key的缓存值,如果找到缓存值,那么Spring将不会再调用方法,而是将缓存值读出,返回给调用者;如果找到缓存值,那么Spring就会执行你的方法,将最后的结果通过key保存到缓存服务器中 |
@CachePut | Spring会将该方法返回的值缓存到缓存服务器中,这里需要注意的是,Spring不会事先去缓存服务器中查找,而是直接执行方法,然后缓存。换句话说,该方法始终会被Spring所调用 |
@CacheEvict | 移除缓存对应的key的值 |
@Caching | 这是一个分组注解,它能够同时应用于其他缓存的注解 |
- 注解@Cacheable和@CachePut都可以保存缓存键值对,只是它们的方式略有不同,请注意二者的区别,它们只能运用于有返回值的方法中,而删除缓存key的@CacheEvict则可以用在void的方法上,因为它并不需要去保存任何值
- 上述注解都能标注到类或者方法之上,如果放到类上,则对所有的方法都有效;如果放到方法上,则只是对方法有效。
- 在大部分情况下,会放置到方法上。因为@Cacheable和@CachePut可以配置的属性接近,所以把它们归为一类去介绍,而@Caching因为不常用,就不介绍了。
- 一般而言,对于查询,我们会考虑使用@Cacheable;对于插入和修改,我们会考虑使用@CachePut;对于删除操作,我们会考虑使用@CacheEvict;
注解@Cacheable和@CachePut
属性 | 配置类型 | 描述 |
---|---|---|
value | String[] | 使用缓存的名称 |
condition | String | Spring表达式,如果表达式返回值为false,则不会将缓存应用到方法上,true则会 |
key | String | Spring表达式,可以通过它来计算对应缓存的key |
unless | String | Spring表达式,如果表达式返回值为true,则不会将方法的结果方挂到缓存上 |
Spring表达式值的引用
表达式 | 描述 | 备注 |
---|---|---|
#root.args | 定义传递给缓存方法的参数 | 不常用,不予讨论 |
#root.caches | 该方法执行是对应的缓存名称,它是一个数组 | 同上 |
#root.target | 执行缓存的目标对象 | 同上 |
#root.targetClass | 目标对象的类,它是#root.target.class的缩写 | 同上 |
#root.method | 缓存方法 | 同上 |
#root.methodName | 缓存方法的名称,它是#root.method.name的缩写 | 同上 |
#result | 方法返回结果值,还可以使用Spring表达式进一步读取其属性 | 请注意该表达式不能用于注解@Cacheable,因为该注解的方法可能不会被执行,这样返回值就无从谈起了。 |
#Argument | 任意方法的参数,可以通过方法本身的名称或者下标去定义 | 比如getRole(Long id)方法,想读取id这个参数,可以写为#id,或者#a0,#p0,笔者建议写为#id,这样可读性高 |
RoleServiceImpl实现类
1 | package com.louris.springboot.IoCImpl.impl; |
启动测试类
1 | //SpringBoot项目启动入口类 |
测试结果
配置文件配置方式测试结果
1 | 2021-10-14 19:25:55.819 [main] DEBUG org.mybatis.spring.SqlSessionUtils - Creating a new SqlSession |
因为我们设置了60秒超时时间,所以到期后,查不到数据了。
1 | 127.0.0.1:6379> keys * |
注解@CacheEvict
属性 | 类型 | 描述 |
---|---|---|
value | String[] | 要使用缓存的名称 |
key | String | 指定Spring表达式返回缓存的key |
condition | String | 指定Spring表达式,如果返回为true,则执行移除缓存,否则不执行 |
allEntries | boolean | 如果为true,则删除特定缓存所有键值对,默认值为false,请注意它将清除所有缓存服务器的缓存,这个属性慎用 |
beforeInvocation | boolean | 指定在方法前后移除缓存,如果指定为true,则在方法前删除缓存;如果为false,则在方法调用后删除缓存,默认值为false |
1 |
|
不适用缓存的方法
该方法不适用任何缓存注解标注在这个方法上。
注意:使用缓存的前提——高命中率,由于这里根据角色名称和备注查找角色信息,该方法的返回值会根据查询条件而多样化,导致其不确定和命中率地下,对于这样的场景,使用缓存并不能有效提高性能,所以这样的场景,就不再使用缓存了。
1 |
|
自调用失效问题
1 |
|
因为缓存注解也是基于Spring AOP实现的,对于Spring AOP的基础是动态代理技术,也就是只有代理对象的相互调用,AOP才有拦截的功能,才能执行缓存注解提供的功能。而这里的自调用是没有代理对象存在的,所以其注解功能也就失效了。
RedisTemplate的实例
在很多时候,我们也许需要使用一些更为高级的缓存服务器的API,如Redis的流水线、事务等,所以也许会使用到RedisTemplate本身。
1 | package com.louris.springboot.IoCImpl.service; |
1 | package com.louris.springboot.IoCImpl.impl; |
高并发业务
互联系统应用架构基础分析
- 防火墙
- 负载均衡
- NoSQL服务器集群
- Web服务器集群
- 数据库集群
负载均衡器的功能:
- 对业务请求做初步的分析,决定分不分发请求到 Web 服务器,这就好比以个把控的关卡,常见的分发软件比如 Nginx和Apache 等反向代理服务器,它们在关卡处可以通过配置禁止一些无效的请求,比如封禁经常作弊的IP地址,也可以使用 Lua语言联合 NoSQL 缓存技术进行业务分析,这样就可以初步分析业务,决定是否需要分发到服务器;
- 提供路由算法,它可以提供一些负载均衡的算法,根据各个服务器的负载能力进行合理分发,每一个Web服务器得到比较均衡的请求,从而降低单个服务器的压力,提高系统的响应能力;
- 限流,对于一些高并发时刻,需要通过限流来处理,因为可能某个时刻通过上述的算法让有效请求 过多到达服务器,使得一些Web服务器或者数据库服务器产生宕机。当某台机器宕机后,会使得其他服务器承受更大的请求量,这样就容易产生多台服务器连续宕机的可能性,持续下去就会引发服务器雪崩。因此在这种情况下,负载均衡器有限流的算法,对于请求过多的时刻,可以告知用户系统繁忙,稍后再试,从而保证系统持续可用。
高并发系统的分析和设计
有效请求和无效请求
无效请求有很多种类,比如通过脚本连续刷新网站首页,使得网站频繁访问数据库和其他资源,造成性能持续下降,还有 些为了得到抢购商品,使用刷票软件连续请求的行为。
系统设计
业务划分:
- 水平划分:分拆成各个独立的子业务,相互隔离,降低数据的复杂性,但是各个系统之间存在着关联,还要通过RPC远程过程调用协议处理这些信息,比较流行的RPC有Dubbo、Thrift和Hessian等。
- 垂直划分:按照不相干的各个同样的系统分摊下去来进行业务处理。
- 水平垂直结合的划分。
数据库设计
- 分库
- 分表
- MySQL优化
动静分离技术
对于互联网而言大部分数据都是静态数据,只有少数使用动态数据,动态数据的数据包很小,不会造成网络瓶颈,而静态的数据则不一样,静态数据包含图片、CSS(样式)、JavaScript(脚本)和视频等互联网的应用,尤其是图片和视频占据的流量很大,如果都从冬天服务器(比如Tomcat、WildFly和WebLogic等)获取,那么动态服务器的带宽压力会很大,这个时候应该考虑使用静态分离技术。
- CDN(Content Delivery Network)内容分发网络技术,它允许企业将自己的静态数据缓存到网络CDN的节点,就近响应。
- 静态HTTP服务器,将静态数据分离到静态HTTP服务器上。其原理大同小异,就是将资源分配到静态服务器上,这样图片、HTML、脚本等资源都可以从静态服务器上获取,尽量使用Cookie等技术,让客户端缓存能够缓存数据,避免多次请求,降低服务器的压力。
锁和高并发
无论区分有效请求和无效请求,水平划分还是垂直划分,动静分离技术,还是数据库分表、分库等技术的应用,都无法避免动态数据,而动态数据的请求最终也会落在一台Web服务器上。高并发系统存在一个麻烦是并发数据不一致问题。
在高并发的场景下可能出现错扣红包的情况,这样就会导致数据错误。由于在一个瞬间产生很高的并发,因此除了保证数据一致性,还要尽可能地保证系统的性能,加锁会影响并发,而不加锁就难以保证数据的一致性,这就是高并发和锁的矛盾。
为了解决这对矛盾,在当前互联网系统中,大部分企业提出了悲观锁和乐观锁的概念,而对于数据库而言,如果在那么短的时间内需要执行大量SQL,对于服务器的压力可想而知,需要优化数据库的表设计、索引、SQL语句等。有些企业提出了使用Redis事务和Lua语言所提供的原子性来取代现有的数据库的技术,从而提高数据的存储响应,以应对高并发场景,严格来说也属于乐观锁的概念。
搭建抢红包开发环境和超发现象
现在模拟20万元的红包,共分为2万个红包
大小红包数据库表
1 | create table T_RED_PACKET( |
Mapper接口
1 | package com.louris.springboot.IoCImpl.mapper; |
1 | package com.louris.springboot.IoCImpl.mapper; |
Mybatis的Mapper配置文件
1 |
|
1 |
|
Service接口
1 | package com.louris.springboot.IoCImpl.service; |
1 | package com.louris.springboot.IoCImpl.service; |
ServiceImpl实现类
1 | package com.louris.springboot.IoCImpl.impl; |
1 | package com.louris.springboot.IoCImpl.impl; |
页面
1 | <%-- |
控制器
1 | package com.louris.springboot.IoCImpl.controller; |
测试结果
超发现象:比如库存还剩200个,有300个请求,最后发现发了203个红包,stock库存为-3,出现了超发!
比如库存就剩下1个红包,现在有3个请求在读取库存,不加任何措施的情况下,会出现3个请求都读取库存为1的情况,从而多减了库存,发生超发现象。
悲观锁
利用数据库的行锁for update
即可。
修改mapper.xml文件
1 |
|
修改Mapper接口
1 | package com.louris.springboot.IoCImpl.mapper; |
修改实现类
1 | package com.louris.springboot.IoCImpl.impl; |
测试结果
成功消除超发现象,保证了数据的一致性,但是悲观锁的性能下降很多。
- 频繁加锁和释放,导致事务挂起,只有抢到锁才会执行事务,性能下降,同时十分消耗资源。
注意:这里事务的隔离级别是读已提交,并且对数据加了行锁,只有在拿到锁的事务读取并减完库存后,即锁释放后,其他事务才能读取,进行更新操作。
for update
用法以及加锁时机详见参考资料
乐观锁
乐观锁是一种不会阻塞其他线程并发的机制,它不会使用数据库的锁进行实现,它设计里面由于不阻塞其他线程,所以并不会引发线程频繁挂起和恢复。乐观锁使用的是CAS原理
CAS原理概述
在CAS原理中,对于多个线程共同的资源,先保存一个旧值(Old Value),比如进入线程后,查询当前存量为100个红包,那么先把旧值保存为100,然后经过一定的逻辑处理。当需要扣减红包的时候,先比较数据库当前的值和旧值是否一致,如果一致则进行扣减红包的操作,否则就认为它已经被其他线程修改过了,不再进行操作。
CAS原理并不排斥并发,也不独占资源,只是在线程开始阶段就读入线程共享数据,保存为旧值。当处理完逻辑,需要更新数据的时候,会进行一次比较,即比较各个线程当前共享的数据是否和旧值保持一致。如果一致,就开始更新数据;如果不一致,则认为该数据已经被其他线程修改了,那么就不再更新数据,可以考虑重试或者放弃。有时候可以重试,这样就是一个可重入锁,但是CAS原理会有一个问题,那就是ABA问题。
ABA问题
现有两个线程,线程1和线程2,初始化x=A,线程2修改x=B,而后又将其修改x=A,这个时候线程1需要修改x,发现与原来的值相同,所以修改。这里就出现了ABA问题,线程1并不知道x已经被修改!
可以通过version 变量,只要修改x就递增version,根据version判断是否修改,就可以避免ABA问题。
乐观锁实现抢红包业务
修改Mapper.xml
1 |
|
修改mapper接口
1 | package com.louris.springboot.IoCImpl.mapper; |
修改实现类
1 | package com.louris.springboot.IoCImpl.impl; |
1 | package com.louris.springboot.IoCImpl.impl; |
测试
可以发现,能够保证数据一致性以及高性能,但是还会存在大量的红包,也就是存在大量的因为版本不一致的原因造成抢红包失败的请求,不过这个失败率太高了一点。有时候会容忍这个失败,这取决于业务的需要。
为了克服这个问题,提高成功率,还回考虑使用重入机制。也就是一旦因为版本原因没有抢到红包,则重新尝试抢红包,但是过多的重入会造成大量的SQL执行,所以目前流行的重入会加入两种限制:
- 按时间戳重入,也就是在一定时间戳内(比如100毫秒),不成功的会循环到成功为止,直至超时时间戳,不成功才会退出,返回失败。
- 按次数,比如限定3次,程序尝试超过3次抢红包后,就判定请求失败,这样有助于提高用户抢红包的成功率。
乐观锁重入机制
时间戳方案
1 |
|
测试结果
从结果看,之前大量失败的场景消失了,也没有超发现象,所有红包都抢光了,避免了总是失败的结果,但是有时候时间戳并不是那么稳定,也会随着系统的空闲或者繁忙导致重试次数不一。
限制重试次数方案
1 |
|
测试结果
所有红包都被抢到了,也没有超发现象,这样就可以消除大量的请求失败,避免非重入的时候大量请求失败的场景。
但是现在是使用数据库的情况,有时候并不想使用数据库作为抢红包时刻的数据保存载体,而是选择性能优于数据库的Redis。
使用Redis实现抢红包
数据库最终会将数据保存到磁盘中,而Redis使用的是内存,内存的速度比磁盘速度快得多。但是需要知道的是Redis的功能不如数据库强大,事务也不完整,因此要保证数据的正确性,数据的正确性可以通过严格的验证得以保证。而
使用注解方式配置Redis
1 | package com.louris.springboot.IoCImpl.config; |
数据存储设计
Redis并不是一个严格的事务,而且事务的功能也是有限的。加上Redis本身的命令也比较有限,功能性不强,为了增强功能性,还可以使用Lua语言。Redis中的Lua语言是一种原子性的操作,可以保证数据的一致性。依据这个原理可以避免超发现象,完成抢红包的功能,而且对于性能而言,Redis会比数据库快得多。
第一次运行Lua脚本的时候,先在Redis中编译和缓存脚本,这样就可以得到一个SHA1字符串,之后通过SHA1字符串和参数就能调用Lua脚本了。
redPacket.lua脚本
1 | --缓存抢红包列表信息列表key |
流程:
- 判断是否存在可抢的库存,如果已经没有可抢夺的红包,则返回为0,结束流程;
- 有可抢夺的红包,对于红包的库存减一,然后重新设置库存;
- 将抢红包数据保存到Redis的链表当中,链表的key为red_packet_list_{id};
- 如果当前库存为0,那么返回2,这说明可以触发数据库对Redis链表数据的保存,链表的key为red_packet_list_{id},它将保存抢红包的用户名和抢的时间;
- 如果当前库存不为0,那么将返回1,这说明抢红包信息保存成功。
RedisRedPackerService接口
1 | package com.louris.springboot.IoCImpl.service; |
实现类
1 | package com.louris.springboot.IoCImpl.impl; |
异步配置类
1 | package com.louris.springboot.IoCImpl.config; |
抢红包实现类
1 | package com.louris.springboot.IoCImpl.impl; |
页面
1 | <%-- |
控制类
1 | "/grapRedPacketByRedis") ( |
测试
1 | 127.0.0.1:6379> hset red_packet_1 stock 200 |
1 | 10 |
并发思考
知识点:
仅读已提交隔离级别
如前面所述,减库存事务包含两步:读库存和减库存;结果是将发生超发现象。
读已提交对应二级封锁协议,以库存为1,有两个请求并发减库存为例:
事务1和事务2几乎同时对数据行r加S锁,都读到库存为1,随后都释放S锁,然后事务1先减库存加X锁,期间事务2阻塞,事务1释放X锁后,事务2才能修改数据,这样就造成了两次减库存,超发了!
for update悲观锁实现
乐观锁for update是对读取数据加X锁,只有update或者事务结束了才会释放X锁。
以库存为1,有两个请求并发减库存为例:
事务1先读数据,加了X锁,事务2这时候读阻塞,事务1更新完数据或者事务回退释放X锁后,事务2才能读库存和写库存,保证了数据一致性。
CAS乐观锁实现
以库存为1,有两个请求并发减库存为例:
事务1和事务2几乎同时对数据行r加S锁,都读到库存为1,随后都释放S锁,然后事务1先减库存加X锁,期间事务2减库存阻塞
- 如果事务1减库存成功,释放X锁后,事务2再次读取库存以及version,发现不一致,不进行减库存;
- 如果事务1减库存失败回退,释放X锁后,事务2再次读取库存以及version,发现一致,进行减库存;
这样也保证了数据的一致性,同时并没有像悲观锁那样更耗时。