MyBatis事务管理解析:颠覆你心中对事务的理解

MyBatis事务管理解析:颠覆你心中对事务的理解!

1 .说到数据库事务,人们脑海里自然不自然的就会浮现出事务的四大特性、四大隔离级别、七大传播特性。
四大还好说,问题是七大传播特性是哪儿来的?是Spring在当前线程内,处理多个数据库操作方法事务时所做的一种事务应用策 略。
事务本身并不存在什么传播特性,不要混淆事务本身和Spring的事务应用策略。(当然,找工作面试时,还是可以巧妙的描述传播 特性的)
2 .一说到事务,人们可能又会想起create、begin、commit、rollback、close、suspend。
可实际上,只有commit、rollback是实际存在的,剩下的create、begin、close、suspend都是虚幻的,是业务层或数据库底层 应用语意,而非JDBC事务的真实命令。
create (事务创建):不存在。
begin (事务开始):姑且认为存在于DB的命令行中,比如Mysql的start transaction命令,以及其他数据库中的begin transaction命令。JDBC中不存在。
close (事务关闭):不存在。应用程序接口中的close()方法,是为了把connection放回数据库连接池中,供下一次使用,与事务 毫无关系。
suspend (事务挂起):不存在。
Spring中事务挂起的含义是,需要新事务时,将现有的connection1保存起来(它还有尚未提交的事务),然后创建 connection?, connection2提交、回滚、关闭完毕后,再把connection1取出来,完成提交、回滚、关闭等动作,保存 connection1的动作称之为事务挂起。
在JDBC中,是根本不存在事务挂起的说法的,也不存在这样的接口方法。
因此,记住事务的三个真实存在的方法,不要被各种事务状态名词所迷惑,它们分别是:conn.setAutoCommit()、conn.commit()、 conn.rollback()。
conn.close()含义为关闭一个数据库连接,这已经不再是事务方法了。
1 . Mybaits中的事务接口Transaction
public interface Transaction {
ConnectiongetConnection() throwsSQLException ;
void commit() throwsSQLException ;
void rollback。throwsSQLException ;
void close() throwsSQLException ;
}

有了文章开头的分析,当你再次看到close()方法时,千万别再认为是关闭一个事务了,而是关闭一个conn连接,或者是把conn连 接放回连接池内。
事务类层次结构图:
在这里插入图片描述

JdbcTransaction:单独使用Mybatis时,默认的事务管理实现类,就和它的名字一样,它就是我们常说的JDBC事务的极简封装, 和编程使用mysql-connector-java-5.1.38-bin.jar事务驱动没啥差别。其极简封装,仅是让connection支持连接池而已。
ManagedTransaction:含义为托管事务,空壳事务管理器,皮包公司。仅是提醒用户,在其它环境中应用时,把事务托管给其它 框架,比如托管给Spring,让Spring去管理事务。
org.apache.ibatis.transaction.jdbc.JdbcTransaction.java部分源码。
@Override
public void close() throwsSQLException {
if(connection!= null){
resetAutoCommit();
if(log.isDebugEnabled()){
log.debug(“ClosingJDBCConnection[”+connection+“]”);
}
connection.close();
}
}

面对上面这段代码,我们不禁好奇,connection.close()之前,居然调用了一^个resetAutoCommit(),含义为重置autoCommit属 性值。
connection.close()含义为销毁conn,既然要销毁conn,为何还多此一举的调用一^个resetAutoCommit()呢?消失之前多喝口 水,真的没有必要。
其实,原因是这样的,connection.close()不意味着真的要销毁conn,而是要把conn放回连接池,供下一次使用,既然还要使 用,自然就需要重置AutoCommit属性了。
通过生成connection代理类,来实现重回连接池的功能。如果connection是普通的Connection实例,那么代码也是没有问题的, 双重支持。
2 .事务工厂TransactionFactory
。JdbcTransactionFactory O ManagedTransactionFactory
顾名思义,一个生产JdbcTransaction实例,一个生产ManagedTransaction实例。两个毫无实际意义的工厂类,除了“6川之外,没有其他代码。
<transactionmanagertype=“JDBC”/>
mybatis-config.xml配置文件内,可配置事务管理类型。
3 . Transaction的用法
无论是SqlSession,还是Executor,它们的事务方法,最终都指向了Transaction的事务方法,即都是由Transaction来完成事务 提交、回滚的。
配一个简单的时序图。
在这里插入图片描述

代码样例:
public static void main(String[]args){
SqlSessionsqlSession:MybatisSqlSessionFactory.openSession();
try{
StudentMapperstudentMapper:sqlSession.getMapper(StudentMapper.class);
Studentstudent: newStudent(); student.setName( “yy”);
student.setEmail( “email@email.com”);
student.setDob( newDate());
student.setPhone( newPhoneNumber(“123-2568-8947”));
studentMapper.insertStudent(student);
sqlSession.commit();
} catch(Exceptione){ sqlSession.rollback();
} finally{ sqlSession.close();
}
}

注:Executor在执行insertStudent(student)方法时,与事务的提交、回滚、关闭毫无瓜葛(方法内部不会提交、回滚事务),需 要像上面的代码一样,手动显示调用commit。、rollback。、close()等方法。
因此,后续在分析到类似insert。、update()等方法内部时,需要忘记事务的存在,不要试图在insert()等方法内部寻找有关事务的 任何方法。
4 .你可能关心的有关事务的几种特殊场景表现(重要)

  1. 一个conn生命周期内,可以存在无数多个事务。
    〃执行yconnection.setAutoCommit(false),并返回
    SqlSessionsqlSession=MybatisSqlSessionFactory.openSession();
    try{
    StudentMapperstudentMapper:sqlSession.getMapper(StudentMapper.class);
    Studentstudent= newStudent();
    student.setName( “yy”);
    student.setEmail( “email@email.com”);
    student.setDob( new Date());
    student.setPhone( newPhoneNumber( “123-2568-8947”));
    studentMapper.insertStudent(student);

    //提交
    sqlSession.commit();
    studentMapper.insertStudent(student);

    〃多次提交
    sqlSession.commit();
    } catch(Exceptione){
    〃回滚,只能回滚当前未提交的事务
    sqlSession.rollback();
    } finally{
    sqlSession.close();
    }

    对于JDBC来说,autoCommit=false时,是自动开启事务的,执行commit()后,该事务结束。
    以上代码正常情况下,开启了2个事务,向数据库插入了2条数据。
    JDBC中不存在Hibernate中的session的概念,在JDBC中,insert了几次,数据库就会有几条记录,切勿混淆。而rollback。,只 能回滚当前未提交的事务。
  2. autoCommit=false,没有执行commit(),仅执行close(),会发生什么?
    try{
    studentMapper.insertStudent(student);
    }finally{
    sqlSession.close();
    }
    就像上面这样的代码,没有commit(),固执的程序员总是好奇这样的特例。
    insert后,close之前,如果数据库的事务隔离级别是read uncommitted,那么,我们可以在数据库中查询到该条记录。
    接着执行sqlSession.close()时,经过SqlSession的判断,决定执行rollback。操作,于是,事务回滚,数据库记录消失。
    下面,我们看看org.apache.ibatis.session.defaults.DefaultSqlSession.java中的close()方法源码。
    @Override
    public void close() {
    try{
    executor.close(isCommitOrRollbackRequired(false));
    dirty= false;
    } finally{
    ErrorContext.instance().reset();
    }
    }

    事务是否回滚,依靠isCommitOrRoUbackRequired(false)方法来判断。
    private boolean isCommitOrRollbackRequired(booleanforce) { return(!autoCommit&&dirty)| force;
    }

    在上面的条件判断中,!autoCommit=true (取反当然是truey), force=false,最终是否回滚事务,只有dirty参数了,dirty含 义为是否是脏数据。
    @Override
    public int insert(Stringstatement,Objectparameter) {
    returnupdate(statement,parameter);
    }
    @Override
    public int update(Stringstatement,Objectparameter) {
    try{
    dirty: true;
    MappedStatementms二configu ration.getMappedStatement(statement);
    returnexecutor.update(ms,wrapCollection(parameter));
    } catch(Exceptione){
    throwExceptionFactory.wrapException(“Errorupdatingdatabase. Cause:”+e,e);
    } finally{
    ErrorContext.instance().reset();
    }
    }

    源码很明确,只要执行update操作,就设置dirty=true。insert、delete最终也是执行update操作。
    只有在执行完commit()、roUback()、close()等方法后,才会再次设置dirty=false。
    @Override
    public void commit(booleanforce) {
    try{
    executor.commit(isCommitOrRollbackRequired(force));
    dirty= false;
    } catch(Exceptione){
    throwExceptionFactory.wrapException( “Errorcommittingtransaction. Cause:”+e,e);
    } finally{
    ErrorContext.instance().reset();
    }
    }

    因此,得出结论:autoCommit=false,但是没有手动commit, 在sqlSession.close()时,Mybatis会将事务进行rollback。操作, 然后才执行conn.close()关闭连接,当然数据最终也就没能持久化到数据库中了。
  3. autoCommit=false,没有8由由储也没有日0$6,会发生什么?
    studentMapper.insertStudent(student);
    干脆,就这一句话,即不commit,也不close。
    结论:insert后,jvm结束前,如果事务隔离级别是read uncommitted,我们可以查到该条记录。jvm结束后,事务被 rollback(),记录消失。通过断点debug方式,你可以看到效果。
    这说明JDBC驱动实现,已经Kao虑到这样的特例情况,底层已经有相应的处理机制了。这也超出了我们的探究范围。
    但是,一万个屈丝程序员会对你说:Don’t do it like this. Go right way。
    警告:请按正确的try-catch-finally编程方式处理事务,若不从,本人概不负责后果。
    注:无参的openSession()方法,会自动设置autoCommit=false。
    总结:Mybatis的JdbcTransaction,和纯粹的Jdbc事务,几乎没有差别,它仅是扩展支持了连接池的connection。 另外,需要明确,无论你是否手动处理了事务,只要是对数据库进行任何update操作(update、delete、insert),都一定是在事 务中进行的,这是数据库的设计规范之一。

在使用mybatis过程中,当手写JavaBean和XML写的越来越多的时候,就越来越同意出错。这种重复性的工作,我们当然 不希望做那么多。
还好,mybatis为我们提供了强大的代码生成–MybatisGenerator。
通过简单的配置,我们就可以生成各种类型的实体类,Mapper接口,MapperXML文件,Example对象等。通过这些生成的文 件,我们就可以方便的进行单表进行增删改查的操作。

以下的工具使用的都是IDEA
1.1 创建Maven项目
1.1.1 菜单上选择新建项目
File | New | Project
1.1.2 选择左侧的Maven
,«Java Project SDK; | ■1S (java version ▼ New…
验 Java Enterprise
C J Boss 口 Create from archetype | Add Archetype-
■日 J2ME A com.at!assian.mave’ archetypes bamboo-plugin-archetype
■ Clouds ► com.Btlassian.maven.archetypes confluence-plugin-archetype
4 Spring > corruatlassian.maven^rchetypesyira-plugin-archetype
一 , 〜 ► com.rfc^noven.ordietj pe5jpa-mAven-archetype
Java FX
. ► de^kquinetjbossccybosscc-seam-archetype
Android > netdatabinder:data-app
. IntdliJ Platform Plugin > nediftweo lift-archetype-basic
噂 Spring Initializr ► netJiftweb lift-archetype-blank
/T) fl4jp/on > net.sf.mavcn harmaven-drehetype-har
► net.sf.maven–…ir;maven-archetype-iar
M Gradle 』 , ,. .
► c.rg.apache i ir-,r- r< h-‘’ - - xamel-archetype-activemq
Groovy > org.apache.camelarch€^pes:cannel-archetype*compon«nt
・ Griffon Ik camel-archetype-java
.Grails » orgrapache.camcl.arcbct)pe5:camel-archetype-scah
_ ► org-apa<he.c-amef.jrchet,»pe’s;canr)el-archetype-spring
♦ Application Forge
► org-apache.camel^rchetypet-xamel-archetype-war
・ S1a加 Web ► org.apache.cocoon;cocoon-22-archetype-block
弊 Hash ► org.apache.cocoon:cocoon-22-archetype-block-plain
K Kotlin » org.apache.cococnrcocoon-22-archetype-webapp
► org.apache.mavenujrchetyp,- maven-archetype-j2ee-simple
■ P" , ► org-«pacbe.meven.archetyp^:maven-archetype-marmalade-rnqjo
► org.apachr.maven.archetypt–. maven-archetype-mojo
► org.apache.maven.archetypesimaven-archetype-portiet
► org.apache.maven.archetjpc:;maven-archetype-profiles
:-h-丁册[壮?愉 esdn. net/weixin 3713919/
► crg epncHc.nuve^ arcnrtypr-; maven-drcnetype-srte
F
Bl New Project
X

由于我们只是创建一个普通的项目,此处点击Next即可。
1.1.3 输入GroupId和ArtifactId
•在我的项目中,
GroupId 填 com.homejim.mybatis
ArtifactId 填 mybatis-generator
点击Next。
1.1.4 Finish
通过以上步骤,一个普通的Maven项目就创建好了。
1.2 配置 generator.xml
其实名字无所谓,只要跟下面的pom.xml文件中的对应上就好了。

<?xml version="1.0" encoding="UTF-8" ?>

-K Compare With… Ctrl+D Database Changes tGeneia-
Compare File with Editor Designer
? Compare with Clipboard Event Log 的配置-
Quick Switch Scheme… Ctrl Image Layers
Toolbar
'成— 4 山蚌甲
View Navigate Code Analyze Refactor Build Run Tools VCS Window Hei J

1.4.2 Maven Projects 中双击 mybatis-generator
在右侧此时可以看到Maven Projects 了。找到mybatis-generator插件。
mybatis-generator | Plugins | mybatis-generator | mybatis-generator
▼篇 mybatis-generator
►彩 Lifecycle
▼畤 Plugins

焦 clean (org.apache.rr) ► (^compiler (org.apach ► deploy (org.apache. ► ^install (org.apache.n ►儒jar (org.apache.mav
▼像 mybatis-generator i mybatis-generat(
牛 mybatis-generat( ► 儒 resources (org.apac ►儒 site (org.apache.ma, ► surefire (org.apache
1.4.3双击运行
运行正确后,生成代码,得到如下的结构
V mybatis-generator C:\Users\Administrator\Doi
A ■ ,idea
▼ ■ src
▼ ■ main
▼ java
▼ A com.homejim.mybatis
▼ Ei entity
@ Blog
@ BlogExample
▼ B mapper
❶ BlogMapper
▼跑 resources
▼ EB mybatis.mapper
翡 Blog Mappenxml
品 generator.xml
A ■ test
mybatis-generator.iml
m pom.xml
仅仅是上面那么简单的使用还不够爽。那么我们就可以通过更改generator.xml配置文件的方式进行生成的配置。
2.1 文档
推荐查看官方的文档。
英文不错的:http://www.mybatis.org/generator/configreference/xmlconfig.html
中文翻译版:http://mbg.cndocs.ml/index.html
2.2 官网没有的
2.2.1 property 标签
该标签在官网中只是说用来指定元素的属性,至于怎么用没有详细的讲解。
2.2.1.1 分隔符相关
〈property name=“autoDelimitKeywords” value=“true”/>
〈property name=“beginningDelimiter” value=“'”/>
〈property name=“endingDelimiter” value="’ "/>
以上的配置对应的是mysql,当数据库中的字段和数据库的关键字一样时,就会使用分隔符。
比如我们的数据列是delete,按以上的配置后,在它出现的地方,就变成’delete’。
2.2.1.2 编码
默认是使用当前的系统环境的编码,可以配置为GBK或UTF-8。
〈property name=“javaFileEncoding” value=“UTF-8”/>
我想项目为UTF-8,如果指定生成GBK,则自动生成的中文就是乱码。
2.2.1.3 格式化
〈property name="javaFormatter“ value=“org.mybatis.generator.api.dom.DefaultJavaFormatter”/>
<property name="xmlFormatter“ value=“org.mybatis.generator.api.dom.DefaultXmlFormatter”/>
这些显然都是可以自定义实现的的。
2.2.2 plugins 标签
plugins标签用来扩展或修改代码生成器生成的代码。
在生成的XML中,是没有这个标签的。该标签是配置缓存的。
如果我们想生成这个标签,那么可以plugins中进行配置。
〈p山gin type=“org.mybatis.generator.p山gins.CacheP山gin” >

〈/p仙gin> 
|caqh.e eviction= LRU=〉
WARNING - <Pmbg. generated
This element is automatically generated by MyBatis Generator, do not modify,
https://blog. csdn. net/weixin_37139197
比如你想生成的JavaBean中自行实现Serializable接口。

tudent inclements Sexializable (

  • This field vas generated by MyBatis Generator.
  • This field coiijesnonds to th^ database, colusu ftud典工 $宜去g
  • https://biog. cson. net/weixin_3713219/
    还能自定义插件。
    这些插件都蛮有用的,感觉后续可以专门开一篇文章来讲解。
    看名称,就知道是用来生成注释用的。
    默认配置:

    〈property name=“suppressA11comments” value=“false”/>
    〈property name=“suppressDate” value=“false”/>
    〈property name=“addRemarkComments” value=“false”/>

    suppressAllComments:阻止生成注释,默认值是false。
    suppressDate:阻止生成的注释包含时间戳,默认为false。
    addRemarkComments:注释中添加数据库的注释,默认为false。
    还有一个就是我们可以通过 type 属性指定我们自定义的注解实现类, 生成我们自己想要的注解。自定义的实现类需要实
    现 org.mybatis.generator.api.CommentGenerator。
    2.2.4源码
    https://github.com/homejim/mybatis-cn

微信扫描二维码,关注我的公众号
阅读原文
声明:pdf仅供学习使用,一切版权归原创公众号所有;建议持续关注原创公众号获取最新文章,学习愉快!
MyBatis多数据源读写分离(注解实现)
Java后端 2019-11-09
点击上方Java后端,选择
优质文章,及时送达
作者 | hy_xiaobin
来源 | juejin.im/post/5d8705e65188253f4b629f47
首先需要建立两个库进行测试,我这里使用的是master_test和slave_test两个库,两张库都有一张同样的表(偷懒,喜喜),表结 构
表名t_user
字段名 类型 备注
id int 主键自增ID
name varchar 名称

表中分别添加两条不同数据,方便测试主数据库记录name为xiaobin,从库为xiaoliu开始使用Springboot整合mybatis,首先 引入pom文件

<?xml version="1.0" encoding="UTF-8"?>

〈project xmlns=“http://maven.apache.org/POM/4.0.0”
xmlns:xsi=“http://www.w3.org/2001/XMLSchema-instance”
xsi:schemaLocation=“http://maven.apache.org/POM/4.0.0http://maven.apache.org/xsd/maven-4.0.0.xsd”>
4.0.0

org.springframework.boot
spring-boot-sta rter-parent
2.1.4.RELEASE

com.xiaobin
mysql_master_slave
1.0-SNAPSHOT

<java.version>1.8</java.version>
<lombok.version>1.18.6</lombok.version>
<mybatis.version>1.3.2</mybatis.version>
<lombox.version>1.18.6</lombox.version>

〈dependencies〉

〈dependency〉
org.springframework.boot
spring-boot-starter-web
〈/dependency〉
<!一添加[ombok工具坐标–>

nrcr nrniprtlnmhnk 
lombok</a rtifactId>
${lombok.version}

org.springframework.boot spring-boot-starter-test <!一添Mombox测试坐标--> org.projectlombok lombok ${lombox.version} <!一添加my bats依赖坐标--> org.mybatis.spring.boot mybatis-spring-boot-starter ${mybatis.version} <!—添加mysql驱动器坐标--> mysql mysql-connector-java org.springframework.boot spring-boot-starter-jdbc <!—添加druid数据源坐标--> com.alibaba druid-spring-boot-starter 1.1.10 <!—添加AOP坐标-> org.springframework.boot spring-boot-starter-aop 动态数据源配置 这里使用的数据源为druid,实现数据源之间的切换用@DataSource自定义注解,配置Aop进行切换application.yml配置文件  spring: datasource: type: com.alibaba.druid.pool.DruidDataSource druid: xiaobin-master: driverClassName: com.mysql.jdbc.Driver username: root password: root url: jdbc:mysql: 〃localhost:3306/maste_ test?serverTimezone=GM T%2B8&useUnicod=true&characterEncoding=utf8 xiaobin-slave: driverClassName: com.mysql.jdbc.Driver username: root password: root url: jdbc:mysql: //localhost:3306/sla ve_ test?serverTimezone=GMT%2B8&use Unicode=true&characterEncoding=utf8 mybatis: mapper-locations: classpath:mappe *.xml 多数据源配置类 @Configuration @Component public class DynamicDataSourceConfig { @Bean @ConfigurationProperties("spring.datasource.druid.xiaobin-master") public DataSource xiaobinMasterDataSource(){ return DruidDataSourceBuilder.create().build(); } @Bean @ConfigurationProperties("spring.datasource.druid.xiaobin-slave") public DataSource xiaobinSlaveDataSource(){ return DruidDataSourceBuilder.create().build(); } @Bean @Primary public DynamicDataSource dataSource(DataSource xiaobinMasterDataSource, DataSource xiaobinSlaveDataSource) { Map

目录结构
源码地址(数据库需要自己创建)
https://gitee.com/MyXiaoXiaoBin/learning-to-share/tree/master/mysql_master_slave
-END-

学Java,请关注公众号:Java后端
喜欢文章,点个在看
阅读原文
声明:pdf仅供学习使用,一切版权归原创公众号所有;建议持续关注原创公众号获取最新文章,学习愉快! 
MyBatis大揭秘:Plugin插件设计原理
Java后端 2019-12-02
点击上方Java后端,选择
优质文章,及时送达
作者|祖大俊
链接 | my.oschina.net/zudajun/blog/738973
大多数框架,都支持插件,用户可通过编写插件来自行扩展功能,Mybatis也不例外。
我们从插件配置、插件编写、插件运行原理、插件注册与执行拦截的时机、初始化插件、分页插件的原理等六个方面展开阐述。
1.插件配置
Mybatis的插件配置在configuration内部,初始化时,会读取这些插件,保存于Configuration对象的InterceptorChain中。

<?xml version="1.0" encoding="UTF-8"?>

〈configuration)

〈property name=“value” value=“100” />


〈/configuration)
public class Configuration {
protected final InterceptorChain interceptorChain = new InterceptorChain(); }
org.apache.ibatis.plugin.InterceptorChain.java源码。
public class InterceptorChain {
private final List interceptors = new ArrayList();
public Object pluginAll(Object target) {
for (Interceptor interceptor : interceptors) { target = interceptor.plugin(target);
}
return target;
}
public void addInterceptor(Interceptor interceptor) { interceptors.add(interceptor);
}
public List getInterceptors() {
return Collections.unmodifiableList(interceptors);
}
}
上面的for循环代表了只要是插件,都会以责任链的方式逐一执行(别指望它能跳过某个节点),所谓插件,其实就类似于拦截 
器。
2.如何编写一个插件
插件必须实现org.apache.ibatis.plugin.Interceptor接口。
public interface Interceptor {
Object intercept(Invocation invocation) throws Throwable;
Object plugin (Object target);
void setProperties (Properties properties);
}
■ intercept。方法:执行拦截内容的地方,比如想收点保护费。由plugin()方法触发,interceptor.plugin(target)足以证 明。
■ plugin()方法:决定是否触发intercept。方法。
■ setProperties()方法:给自定义的拦截器传递xml配置的属性参数。
下面自定义一个拦截器:
@Intercepts({
@Signature(type = Executor.class, method = “query”, args = { MappedStatement.class, Object.class,
RowBounds.class, ResultHandler.class }),
@Signature(type = Executor.class, method = “close”, args = { boolean.class }) })
public class MyBatisInterceptor implements Interceptor {
private Integer value;
@Override
public Object intercept(Invocation invocation) throws Throwable { return invocation.proceed();
}
@Override
public Object plugin(Object target) {
System.out.println(value);
/Plugin类是插件的核心类,用于给arget创建一个JDK的动态代理对象,触发Mtercep(方法
return Plugin.wrap(target, this);
}
@Override
public void setProperties(Properties properties) {
value = Integer.valueOf((String) properties.get(“value”)); }
}
面对上面的代码,我们需要解决两个疑问:
1 .为什么要写Annotation注解?注解都是什么含义?
答:Mybatis规定插件必须编写Annotation注解,是必须,而不是可选。
@Intercepts注解:装载一 j@Signature列表,一个@Signature其实就是一个需要拦截的方法封装。那么,一个拦截器要拦截 
多个方法,自然就是一个@Signature列表。
type = Executor.class, method = “'query”, args = { MappedStatement.class, Object.class, RowBounds.class, ResultHandler.class}
解释:要拦截Executor接口内的query()方法,参数类型为args列表。
2 . Plugin.wrap(target, this)是干什么的?
答:使用JDK的动态代理,给target对象创建一个delegate代理对象,以此来实现方法拦截和增强功能,它会回调intercept。方 法。
org.apache.ibatis.plugin.Plugin.java源码:
public class Plugin implements InvocationHandler {
private Object target;
private Interceptor interceptor;
private Map<Class<?>, Set> signatureMap; private Plugin(Object target, Interceptor interceptor, Map

推荐阅读
2 .你在公司项目里面看过哪些操蛋的代码?
3 .你知道 Spring Batch 吗?
4 .面试题:Lucene、Solr、ElasticSearch
5 . 3分钟带你彻底搞懂Java泛型背后的秘密
6 .团队开发中Git最佳实践

喜欢文章,点个在看 
MyBatis源码:原来resultMap解析完是这样
阿进的写字台Java后端2019-11-16

更多详情,请参考mybatis百科-列映射类ResultMapping
1.2结果集映射类ResultMap
ResultMap对应的是结果集<resultMap>中的一^个结果集。其基本组成部分中,含有ResultMapping对象。
其组成大致如下:

更多详情,请参考mybatis百科-结果集映射类ResultMap
2.解析
2.1 人口函数
resultMap是mapper.xml文件下的,因此其是解析Mapper的一个环节。
resultMapElements(context.evalNodes(“/mapper/resultMap”));
解析<障531乂3口>,由于<resultMap>是可以有多个的, 因此,context.evalNodes(“/mapper/resultMap”)返回的是一^个
List
private void resultMapElements(List<XNode> list) throws Exception {
“遍历,解析
for (XNode resultMapNode : list) {
try {
resultMapElement(resultMapNode);
} catch (IncompleteElementException e) {
// ignore, it will be retried
}
}
}
2.2 解析流程
整个过程就是resultMapElement这个函数。其流程大体如下

对应的代码
private ResultMap resultMapElement(XNode resultMapNode) throws Exception { return resultMapElement(resultMapNode, Collections. emptyList());
}
/*
"处理节点,将节点解析成ResultMap对象,下面包含有ResutMapping对象组成的列表

  • @param resultMapNode resutMap 节点
    " @param additionalResultMappings 另外的 ResutMapping 列
    " @return ResultMap 对象
    " @ th rows Exception
    /
    private ResultMap resultMapElement(XNode resultMapNode, List additionalResultMappings) throws Exceptic ErrorContext.instance().activity("processing " + resultMapNode.getValueBasedIdentifier());
    /获取ID,默认值会拼装所有父节点的id或value或property
    String id = resultMapNode.getStringAttribute(“id”, resultMapNode.getVa山eBasedIdentifier());
    /获取type属性,表示结果集将被映射为type指定类型的对象
    String type = resultMapNode.getStringAttribute(“type”,
    resultMapNode.getStringAttribute(“ofType”,
    resultMapNode.getStringAttribute(“resultType”, resultMapNode.getStringAttribute(“javaType”))));
    //获取extends属性,其表示结果集的继承
    String extend = resultMapNode.getStringAttribute(“extends”);
    /自动映射属性。将列名自动映射为属性
    Boolean autoMapping = resultMapNode.getBooleanAttribute(“autoMapping”);
    //解析type,获取其类型
    Class<?> typeClass = resolveClass(type);
    Discriminator discriminator = null;
    /记录解析的结果
    List resultMappings = new ArrayList<>();
    resultMappings.addAll(additionalResultMappings);
    //处理子节点
    List resultchildren = resultMapNode.getChildren();
    for (XNode resultchild : resultchildren) {
    // 处理 constructor 节点
    if (“constructor”.equals(resultChild.getName())) {
    //解析构造函数元素,其下的没每一个子节点都会生产一个ResutMapping对象 processConstructorElement(resultChild, typeClass, resultMappings);
    // 处理 discriminator 节点
    } else if (“discriminator”.equals(resultChild.getName())) {
    discriminator = processDiscriminatorElement(resultChild, typeClass, resultMappings);
    //处理其余节点,如 id, result, assosation d等
    } else {
    List flags = new ArrayList<>();
    if (“id”.equals(resultChild.getName())) {
    flags.add(ResultFlag.ID);
    }
    // 创建 resutMapping 对象,并添加到 resultMappings 中
    resultMappings.add(buildResultMappingFromContext(resultChild, typeClass, flags));
    }
    }
    //创建ResultMapResolver对象,该对象可以生成ResultMap对象
    ResultMapResolver resultMapResolver = new ResultMapResolver(builderAssistant, id, typeClass, extend, discriminator, resultM try {
    return resultMapResolver.resolve();
    } catch (IncompleteElementException e) {
    〃如果无法创建ResultMap对象,则将该结果添加到incompleteResultMaps集合中
    configuration.addlncompleteResultMap(resultMapResolver);
    throw e; 
    id对于resultMap来说是很重要的,它是一个身份标识。具有唯一性
    “获取ID,默认值会拼装所有父节点的id或value或property。
    String id = resultMapNode.getStringAttribute(“id”, resultMapNode.getValueBasedIdentifier());
    这里涉及到XNode对象中的两个函数
    public String getStringAttribute (String name, String def) {
    String value = attributes.getProperty(name);
    if (value == null) {
    return def;
    } else {
    return value;
    }
    }
    该函数是获取XNode对象对应XML节点的name属性值,如果该属性不存在,则返回传入的默认值def
    而在获取id的过程中,默认值是下面这个函数
    /*
    *生成元素节点的基础id
  • @ return
    /
    public String getValueBasedIdentifier() {
    StringBuilder builder = new StringBuilder();
    XNode current = this;
    //当前的节点不为空
    while (current != null) {
    “如果节点不等于this,则在0之前插入_符号,因为是不断的获取父节点的,因此是插在前面
    if (current != this) {
    builder.insert(0, “");
    }
    //获取id, id不存在则获取value, value不存在则获取property。
    String value = current.getStringAttribute(“id”,
    current.getStringAttribute(“value”,
    current.getStringAttribute(“property”, null)));
    / value非空,则将.替换为一 并将value的值加上]
    if (value != null) {
    value = value.replace(‘.’, '
    ');
    builder.insert(0, “]”);
    builder.insert(0,
    value);
    builder.insert(0, “[”);
    }
    /不管value是否存在,前面都添加上节点的名称
    builder.insert(0, current.getName());
    〃获取父节点
    current = current.getParent();
    }
    return builder.toString();
    }
    该函数是生成元素节点的id,如果是这样子的XML
    〈employee id=”${id_var}“>

    <first_n ame>Jim </first_ name >
    <last_name>Smith</last_name>
    <birth_date>
    1970
    6
    15
    </birth_date>
    〈height units=“ft”>5.8
    〈weight units=“lbs”>200
    true
    〈/employee〉
    我们调用
    XNode node = parser.evalNode(”/employee/height"); node.getVa山eBasedIdentifier();
    则,返回值应该是
    employee] {id_var}]_height
    2.4 解析结果集的类型
    结果集的类型,对应的是一个JavaBean对象。通过反射来获得该类型。
    /获取type, type不存在则获取ofType, ofType
    //不存在则获取resultType, resultType不存在则获取javaType
    String type = resultMapNode.getStringAttribute(“type”,
    resultMapNode.getStringAttribute(“ofType”,
    resultMapNode.getStringAttribute(“resultType”, resultMapNode.getStringAttribute(“javaType”))));
    ”……
    /获取type对应的Class对象
    Class<?> typeClass = resolveClass(type);
    看源码,有很多个def值,也就是说,我们在配置结果集的类型的时候都是有优先级的。但是,这里有一个奇怪的地方,我源
    代码版本(3.5.0-SNAPSHOT)的 的属性,只有 type,没有 ofType/resultType/javaType。以下为相应的 DTD 约 束:
<!ELEMENT resultMap (constructor?,id*,result*,association*,collection*, discriminator?)》 <!ATTLIST resultMap id CDATA #REQUIRED type CDATA #REQUIRED extends CDATA #IMPLIED autoMapping (true|fa Ise) #IMPLIED >

我怀疑是兼容以前的版本。
2.5 获取继承结果集和自动映射
String extend = resultMapNode.getStringAttribute(“extends”);
Boolean autoMapping = resultMapNode.getBooleanAttribute(“autoMapping”);
这个两个属性都是在配置XML的时候可有可无的。
先看DTD约束

<!ELEMENT resultMap (constructor?,id*,result*,association*,collection*, discriminator?)》 可以有以下几个子节点: 子节点 数量 constructor 出现0次或1次 id 出现0次或多次 result 出现0次或多次 association 出现0次或多次 collection 出现0次或多次 discriminator 出现。次或1次 子节点解析过程很简单 根据类型进行解析,最后获得resultMappings (List),有可能会获得discriminator (Discriminator)。 “创建一个resultMappings的链表 List resultMappings = new ArrayList<>();

/将从其他地方传入的additonaResutMappings添加到该链表中
resultMappings.addAll(additionalResultMappings);
//获取子节点
List resultchildren = resultMapNode.getChildren();
//遍历解析子节点
for (XNode resultchild : resultchildren) {
if (“constructor”.equals(resultChild.getName())) {
〃解析构造函数元素,其下的没每一个子节点都会生产一个ResutMapping对象
processConstructorElement(resultChild, typeClass, resultMappings);
} else if (“discriminator”.equals(resultChild.getName())) {
// 解析 discriminator 节点
discriminator = processDiscriminatorElement(resultChild, typeClass, resultMappings);
} else {
//解析其余的节点
List flags = new ArrayList<>();
if (“id”.equals(resultChild.getName())) {
flags.add(ResultFlag.ID);
}
resultMappings.add(buildResultMappingFromContext(resultChild, typeClass, flags));
}
除了 discriminator节点,其余节点最后都会回到buildResultMappingFromContext方法上,该方法是创建 ResultMapping
对象。
*
*获取一行,如]03等,取得他们所有的属性,通过这些属性建立ResultMapping对象

  • @param context对于节点本身
  • @param resultType resutMap 的结果类型
  • @param flags fag属性,对应ResutFiag枚举中的属性。一般情况下为空
  • @ return 返回 ResultMapping
  • @ th rows Exception

private ResultMapping buildResultMappingFromContext(XNode context, Class<?> resultType, List flags) throws Ex String property; //获取节点的属性,如果节点是构造函数(只有。,由自属性,没有popery), //则获取的是name,否则获取property if (flags.contains(ResultFlag.CONSTRUCTOR)) { property = context.getStringAttribute("name"); } else { property = context.getStringAttribute("property"); } String column = context.getStringAttribute("column"); String javaType = context.getStringAttribute("javaType"); String jdbcType = context.getStringAttribute("jdbcType"); String nestedSelect = context.getStringAttribute("select"); //获取嵌套的结果集 String nestedResultMap = context.getStringAttribute("resultMap", processNestedResultMappings(context, Collections. emptyList())); String notNullColumn = context.getStringAttribute("notNullColumn"); String columnPrefix = context.getStringAttribute("columnPrefix"); String typeHandler = context.getStringAttribute("typeHandler"); String resultSet = context.getStringAttribute("resultSet"); String foreignColumn = context.getStringAttribute("foreignColumn"); boolean lazy = "lazy".equals(context.getStringAttribute("fetchType", configuration.isLazyLoadingEnabled() ? "lazy" : "eager") //以上获取各个属性节点 〃解析 javaType, typeHandler, jdbcType Class<?> javaTypeClass = resolveClass(javaType);
@SuppressWarnings(“unchecked”)
Class<? extends TypeHandler<?>> typeHandlerClass = (Class<? extends TypeHandler<?>>) resolveClass(typeHandler);
JdbcType jdbcTypeEnum = resolveJdbcType(jdbcType);
// 仓9建resutMapping对"象
return builderAssistant.buildResultMapping(resultType, property, column, javaTypeClass, jdbcTypeEnum, nestedSelect, neste
}
如果是discriminator,则处理该元素并创建鉴别器。
/*

  • 处理鉴别器
  • @param context 节点
  • @param resultType 结果类型
  • @param resultMappings 列结果集合
  • @return鉴别器
  • @ th rows Exception

private Discriminator processDiscriminatorElement(XNode context, Class<?> resultType, List resultMappings) String column = context.getStringAttribute("column"); String javaType = context.getStringAttribute("javaType"); String jdbcType = context.getStringAttribute("jdbcType"); String typeHandler = context.getStringAttribute("typeHandler"); 〃先获取各个属性 /取得jav Type对应的类型 Class<?> javaTypeClass = resolveClass(javaType);
取得typeHandler对应的类型
@SuppressWarnings(“unchecked”)
Class<? extends TypeHandler<?>> typeHandlerClass = (Class<? extends TypeHandler<?>>) resolveClass(typeHandler);
“取得jdbcType对应的类型
JdbcType jdbcTypeEnum = resolveJdbcType(jdbcType);
// 创建 discriminatorMap,并遍历子节点,以 vaiue->resutMap 的方式放入 discriminatorMap中
Map<String, String> discriminatorMap = new HashMap<>();
for (XNode caseChild : context.getChildren()) {
String value = caseChild.getStringAttribute(“value”);
String resultMap = caseChild.getStringAttribute(“resultMap”, processNestedResultMappings(caseChild, resultMappings));
discriminatorMap.put(value, resultMap);
}
/创建鉴别器
return builderAssistant.buildDiscriminator(resultType, column, javaTypeClass, jdbcTypeEnum, typeHandlerClass, discriminat }
鉴别器内部,也是含有ResultMapping的
public class Discriminator {
private ResultMapping resultMapping;
private Map<String, String> discriminatorMap;
}
2.7创建ResultMap对象 在解析完<resultMap>的各个属性和子节点之后。创建ResultMapResolver对象,通过对象可以生成ResultMap。
/

"创建并添加ResutMap到Configuration对象中
@param id id,配置了 id可以提高效率
" @param type 类型
@param extend 继承
@param discriminator 鉴别器
@param resutMappings 列集
@param autoMapping是否自动映射
@ ret urn返回创建的ResutMap对象
*
public ResultMap addResultMap(
String id,
Class<?> type,
String extend,
Discriminator discriminator,
List resultMappings,
Boolean autoMapping) {
id = applyCurrentNamespace(id, false);
extend = applyCurrentNamespace(extend, true);
if (extend != null) {
if (!configuration.hasResultMap(extend)) {
throw new IncompleteElementException(“Could not find a parent resultmap with id '” + extend + “'”); }
//从 configuration中获取继承的结果集
ResultMap resultMap = configuration.getResultMap(extend);
//获取所集成结果集的所有ResutMapping集合
List extendedResultMappings = new ArrayList<>(resultMap.getResultMappings());
/移除需要覆盖的ResutMapping集合
extendedResultMappings.removeAll(resultMappings);
〃如果该resultMap中定义了构造节点,则移除其父节点的构造器
boolean declaresConstructor = false;
for (ResultMapping resultMapping : resultMappings) {
if (resultMapping.getFlags().contains(ResultFlag.CONSTRUCTOR)) { declaresConstructor = true;
break;
}
}
if (declaresConstructor) {
Iterator extendedResultMappingsIter = extendedResultMappings.iterator();
while (extendedResultMappingsIter.hasNext()) {
if (extendedResultMappingsIter.next().getFlags().contains(ResultFlag.CONSTRUCTOR)) { extendedResultMappingsIter.remove();
}
}
}
“添加需要被继承的ResutMapping集合
resultMappings.addAll(extendedResultMappings);
}
/通过建造者模式创建ResultMap对象
ResultMap resultMap = new ResultMap.Builder(configuration, id, type, resultMappings, autoMapping) .discriminator(discriminator) .build();
*添加到Configuration对象中
configuration.addResultMap(resultMap);
return resultMap;
}
3解析结果
有如下的数据库表

通过代码生成器生成XML和Mapper。
添加结果集
<resultMap id=wdetailedBlogResultMapM type»nBlog">
<!— 定义映射中使用的构造函数 —>

推荐阅读
1从零搭建一个Spring Boot开发环境
2 .Redis实现「附近的人」这个功能
3 . 一个秒杀系统的设计思考
4,零基础认识Spring Boot
5.团队开发中Git最佳实践

喜欢文章,点个在看
阅读原文
声明:pdf仅供学习使用,一切版权归原创公众号所有;建议持续关注原创公众号获取最新文章,学习愉快!
Spring Boot + MyBatis + Druid + PageHelper 实现多数据源并分页
虚无境Java后端2019-11-12

接收文章,更加及时
作者|虚无境
链接 | cnblogs.com/xuwujing/p/8964927.html
刖后
本篇文章主要讲述的是Spring Boo t整合Mybatis、Druid和PageHelper并实现多数据源和分页。其中Spring Boot整 合Mybatis这块,在之前的的一篇文章中已经讲述了,这里就不过多说明了。重点是讲述在多数据源下的如何配置使用 Druid和PageHelper。
http://www.cnblogs.eom/xuwujing/p/8260935.html
Druid介绍和使用
在使用Druid之前,先来简单的了解下Druid。
Druid是一个数据库连接池。Druid可以说是目前最好的数据库连接池!因其优秀的功能、性能和扩展性方面,深受开发人员 的青睐。
Druid已经在阿里巴巴部署了超过600个应用,经过一年多生产环境大规模部署的严苛考验。Druid是阿里巴巴开发的号称 为监控而生的数据库连接池!
同时Druid不仅仅是一个数据库连接池,Druid核心主要包括三部分:
■ 基于Filter-Chain模式的插件体系。
■ DruidDataSource高效可管理的数据库连接池。
■ SQLParser
Druid的主要功能如下:
■ 是一个高效、功能强大、可扩展性好的数据库连接池。
■ 可以监控数据库访问性能。
■ 数据库密码加密
■ 获得SQL执行日志
■ 扩展JDBC
介绍方面这块就不再多说,具体的可以看官方文档。那么开始介绍Druid如何使用。
首先是Maven依赖,只需要添加druid这一个jar就行了。 
〈dependency〉
com.alibaba
druid
1.1.8
〈/dependency〉
Tips:可以关注微信公众号:Java后端,获取Maven教程和每日技术博文推送。
配置方面,主要的只需要在application.properties或application.yml添加如下就可以了。
说明:因为这里我是用来两个数据源,所以稍微有些不同而已。Druid配置的说明在下面中已经说的很详细了,这里我 就不在说明了。
##默认的数据源
master.datasource.url=jdbc:mysql://localhost:3306/springBoot?useUnicode=true&characterEncoding=utf8&allowMultiQueries二 master.datasource.username=root
master.datasource.password=123456
master.datasource.driverClassName=com.mysql.jdbc.Driver
##另一个的数据源
duster.datasource.url=jdbc:mysql://localhost:3306/springBoot_test?useUnicode=true&characterEncoding=utf8
duster.datasource.username=root
cluster.datasource.password=123456
cluster.datasource.driverClassName=com.mysql.jdbc.Driver
#连接池的配置信息
#初始化大小,最小,最大
spring.datasource.type=com.alibaba.druid.pool.DruidDataSource
spring.datasource.initialSize=5
spring.datasource.minIdle=5
spring.datasource.maxActive=20
#配置获取连接等待超时的时间
spring.datasource.maxWait=60000
#配置间隔多久才进行一次检测,检测需要关闭的空闲连接,单位是毫秒
spring.datasource.timeBetweenEvictionRunsMillis=60000
#配置一个连接在池中最小生存的时间,单位是毫秒
spring.datasource.minEvictableIdleTimeMillis=300000
spring.datasource.validationQuery=SELECT 1 FROM DUAL
spring.datasource.testWhileldle=true
spring.datasou rce.testO n Borrow=fa Ise
spring.datasou rce.testO n Return=fa Ise
#打开PSCache,并且指定每个连接上PSCache的大小
spring.datasource.poolPreparedStatements=true
spring.datasource.maxPoolPreparedStatementPerConnectionSize=20
#配置监控统计拦截的filters,去掉后监控界面sql无法统计,'wall’用于防火墙
spring.datasou rce.filte rs=stat,wa ll,log4j
#通过connectProperties属性来打开mergeSql功能;慢SQL记录
spring.datasource.connectionProperties=druid.stat.mergeSql=true;druid.stat.slowSqlMillis=5000
I Jd
成功添加了配置文件之后,我们再来编写Druid相关的类。
首先是MasterDataSourceConfig.java这个类,这个是默认的数据源配置类。
@Configuration
@MapperScan(basePackages = MasterDataSourceConfig.PACKAGE, sqlSessionFactoryRef = “masterSqlSessionFactory”)
public class MasterDataSourceConfig {
static final String PACKAGE = “com.pancm.dao.master”;
static final String MAPPER_LOCATION = “classpath:mapper/master/*.xml”; 
@Value(“ m a s t e r . d a t a s o u r c e . u r l " ) p r i v a t e S t r i n g u r l ; @ V a l u e ( " {master.datasource.url}") private String url; @Value(" master.datasource.url")privateStringurl;@Value("{master.datasource.username}”) private String username;
@Value(“ m a s t e r . d a t a s o u r c e . p a s s w o r d " ) p r i v a t e S t r i n g p a s s w o r d ; @ V a l u e ( " {master.datasource.password}") private String password; @Value(" master.datasource.password")privateStringpassword;@Value("{master.datasource.driverClassName}”) private String driverClassName;
@Value(“ s p r i n g . d a t a s o u r c e . i n i t i a l S i z e " ) p r i v a t e i n t i n i t i a l S i z e ; @ V a l u e ( " {spring.datasource.initialSize}") private int initialSize; @Value(" spring.datasource.initialSize")privateintinitialSize;@Value("{spring.datasource.minIdle}”)
private int minIdle;
@Value(“ s p r i n g . d a t a s o u r c e . m a x A c t i v e " ) p r i v a t e i n t m a x A c t i v e ; @ V a l u e ( " {spring.datasource.maxActive}") private int maxActive; @Value(" spring.datasource.maxActive")privateintmaxActive;@Value("{spring.datasource.maxWait}”)
private int maxWait;
@Value(“ s p r i n g . d a t a s o u r c e . t i m e B e t w e e n E v i c t i o n R u n s M i l l i s " ) p r i v a t e i n t t i m e B e t w e e n E v i c t i o n R u n s M i l l i s ; @ V a l u e ( " {spring.datasource.timeBetweenEvictionRunsMillis}") private int timeBetweenEvictionRunsMillis; @Value(" spring.datasource.timeBetweenEvictionRunsMillis")privateinttimeBetweenEvictionRunsMillis;@Value("{spring.datasource.minEvictableIdleTimeMillis}”)
private int minEvictableIdleTimeMillis;
@Value(“ s p r i n g . d a t a s o u r c e . v a l i d a t i o n Q u e r y " ) p r i v a t e S t r i n g v a l i d a t i o n Q u e r y ; @ V a l u e ( " {spring.datasource.validationQuery}") private String validationQuery; @Value(" spring.datasource.validationQuery")privateStringvalidationQuery;@Value("{spring.datasource.testWhileIdle}”)
private boolean testWhileIdle;
@Value(“ s p r i n g . d a t a s o u r c e . t e s t O n B o r r o w " ) p r i v a t e b o o l e a n t e s t O n B o r r o w ; @ V a l u e ( " {spring.datasource.testOnBorrow}") private boolean testOnBorrow; @Value(" spring.datasource.testOnBorrow")privatebooleantestOnBorrow;@Value("{spring.datasource.testOnReturn}”)
private boolean testOnReturn;
@Value(“ s p r i n g . d a t a s o u r c e . p o o l P r e p a r e d S t a t e m e n t s " ) p r i v a t e b o o l e a n p o o l P r e p a r e d S t a t e m e n t s ; @ V a l u e ( " {spring.datasource.poolPreparedStatements}") private boolean poolPreparedStatements; @Value(" spring.datasource.poolPreparedStatements")privatebooleanpoolPreparedStatements;@Value("{spring.datasource.maxPoolPreparedStatementPerConnectionSize}”) private int maxPoolPreparedStatementPerConnectionSize;
@Value(“KaTeX parse error: Expected 'EOF', got '}' at position 1862: …getObject(); } }̲ 其中这两个注解说明下: ■ …{cluster.datasource.url}”) private String url;
@Value(“ c l u s t e r . d a t a s o u r c e . u s e r n a m e " ) p r i v a t e S t r i n g u s e r n a m e ; @ V a l u e ( " {cluster.datasource.username}") private String username; @Value(" cluster.datasource.username")privateStringusername;@Value("{cluster.datasource.password}”) private String password;
@Value(“${cluster.datasource.driverClassName}”) private String driverClass;
//fUMasterDataSourceConfig—^,这里略
@Bean(name = “clusterDataSource”)
public DataSource clusterDataSource() {
DruidDataSource dataSource = new DruidDataSource();
dataSource.setUrl(url);
dataSource.setUsername(username);
dataSource.setPassword(password);
dataSource.setDriverClassName(driverClass);
//和MasterDataSourceConfg一样,这里略…
return dataSource;
}
@Bean(name = “clusterT ransactionManager”)
public DataSourceTransactionManager clusterTransactionManager() {
return new DataSourceTransactionManager(clusterDataSource()); }
@Bean(name = “clusterSqlSessionFactory”)
public SqlSessionFactory clusterSqlSessionFactory(@Qualifier(“clusterDataSource”) DataSource clusterDataSource)
throws Exception {
final SqlSessionFactoryBean sessionFactory = new SqlSessionFactoryBean();
sessionFactory.setDataSource(clusterDataSource);
sessionFactory.setMapperLocations(new PathMatchingResourcePatternResolver().getResources(ClusterDataSourceConfig.M> return sessionFactory.getObject();
}
}
1 2d
成功写完配置之后,启动程序,进行测试。
分别在springBoot和springBoot_test库中使用接口进行添加数据。
t user —
POST http://localhost:8084/api/user
{“name”:“张三”,“age”:25}
{“name”:“李四”,“age”:25}
{“name”:“王五”,“age”:25}
t student — 
POST http://localhost:8084/api/student
{“name”:“学生A”,“age”:16}
{“name”:“学生B”,“age”:17}
{“name”:“学生C”,“age”:18}
成功添加数据之后,然后进行调用不同的接口进行查询。
请求:
GET http://localhost:8084/api/user?name=李四
返回:
{
“id”: 2,
“name”:“李四”,
“age”: 25
}
请求:
GET http://localhost:8084/api/student?name=学生C
返回:
{
“id”: 1,
“name”:“学生C”,
“age”: 16
}
通过数据可以看出,成功配置了多数据源了。
PageHelper分页实现
PageHelper是Mybatis的一个分页插件,非常的好用!这里强烈推荐!!!
PageHelper的使用很简单,只需要在Maven中添加pagehelper这个依赖就可以了。
Maven的依赖如下:
〈dependency〉
com.github.pagehelper
pagehelper-spring-boot-starter
1.2.3
〈/dependency〉
注:这里我是用springBoot版的!也可以使用其它版本的。
添加依赖之后,只需要添加如下配置或代码就可以了。
第一种,在application.properties或application.yml添加 
pagehelper:
helperDialect: mysql
offsetAsPageNum: true
rowBoundsWithCount: true reasonable: false
第二种,在mybatis.xml配置中添加

〈property name=“dataSource” ref=“dataSource” />


<–配置分页插件–>





helperDialect=mysql
offsetAsPageNum=true
rowBoundsWithCount=true
reasonable=false






第三种,在代码中添加,使用@Bean注解在启动程序的时候初始化。
@Bean
public PageHelper pageHelper(){
PageHelper pageHelper = new PageHelper();
Properties properties = new Properties。;
〃数据库
properties.setProperty(“helperDialect”, “mysql”);
〃是否将参数offset乍为PagNum使用
properties.setProperty(“offsetAsPageNum”, “true”);
//是否进行count查询
properties.setProperty(“rowBoundsWithCount”, “true”);
〃是否分页合理化
properties.setProperty(“reasonable”, “false”);
pageHelper.setProperties(properties); }
因为这里我们使用的是多数据源,所以这里的配置稍微有些不同。我们需要在sessionFactory这里配置。这里就对 MasterDataSourceConfig.java进行相应的修改。
在masterSqlSessionFactory方法中,添加如下代码。 
@Bean(name = “masterSqlSessionFactory”)
@Primary
public SqlSessionFactory masterSqlSessionFactory(@Qualifier(“masterDataSource”) DataSource masterDataSource) throws Exception {
final SqlSessionFactoryBean sessionFactory = new SqlSessionFactoryBean();
sessionFactory.setDataSource(masterDataSource);
sessionFactory.setMapperLocations(new PathMatchingResourcePatternResolver()
.getResources(MasterDataSourceConfig.MAPPER_LOCATION));
〃分页插件
Interceptor interceptor = new PageInterceptor();
Properties properties = new Properties。;
〃数据库
properties.setProperty(“helperDialect”, “mysql”);
〃是否将参数offset乍为PagNum使用
properties.setProperty(“offsetAsPageNum”, “true”);
//是否进行count查询
properties.setProperty(“rowBoundsWithCount”, “true”);
〃是否分页合理化
properties.setProperty(“reasonable”, “false”);
interceptor.setProperties(properties);
sessionFactory.setPlugins(new Interceptor[] {interceptor});
return sessionFactory.getObject();
}
注:其它的数据源也想进行分页的时候,参照上面的代码即可。
这里需要注意的是年35。”353参数,表示分页合理化,默认值为false。如果该参数设置为true时,pageNum<=0时会查
询第一页,pageNum>pages(超过总数时),会查询最后一页。默认false时,直接根据参数进行查询。
设置完PageHelper之后,使用的话,只需要在查询的sql前面添加PageHelper.startPage(pageNum,pageSize);,如果
是想知道总数的话,在查询的sql语句后买呢添加page.getTotal()就可以了。
代码示例:
public List findByListEntity(T entity) {
List list = null;
try {
Page<?> page =PageHelper.startPage(1,2);
System.out.println(getClassName(entity)+”设置第一页两条数据!”);
list = getMapper().findByListEntity(entity);
System.out.println(“总共有:”+page.getTotal()+”条数据,实际返回:"+list.size()+"两条数据!”);
} catch (Exception e) {
logger.error(“查询”+getClassName(entity)+“失败!原因是:”,e);
}
return list;
代码编写完毕之后,开始进行最后的测试。
查询t_user表的所有的数据,并进行分页。
请求:
GET http://localhost:8084/api/user
返回: 
[
{
“id”: 1,
“name”:“张三”,
“age”: 25
},
{
“id”: 2,
“name”:“李四”,
“age”: 25
}
]
控制台打印:
开始查询…
User设置第一页两条数据!
2018-04-27 19:55:50.769 DEBUG 6152 — [io-8084-exec-10] c.p.d.m.UserDao.findByListEntity_COUNT : ==> Preparing: SELECT cou
2018-04-27 19:55:50.770 DEBUG 6152 — [io-8084-exec-10] c.p.d.m.UserDao.findByListEntity_COUNT : > Parameters:
2018-04-27 19:55:50.771 DEBUG 6152 — [io-8084-exec-10] c.p.d.m.UserDao.findByListEntity_COUNT : <
Total: 1
2018-04-27 19:55:50.772 DEBUG 6152 — [io-8084-exec-10] c.p.dao.master.UserDao.findByListEntity : ==> Preparing: select id, nar
2018-04-27 19:55:50.773 DEBUG 6152 — [io-8084-exec-10] c.p.dao.master.UserDao.findByListEntity : > Parameters: 2(Integer)
2018-04-27 19:55:50.774 DEBUG 6152 — [io-8084-exec-10] c.p.dao.master.UserDao.findByListEntity : <
Total: 2 总共有:3条数据,实际返回:2两条数据!
W 1 E
查询t_student表的所有的数据,并进行分页。
请求:
GET http://localhost:8084/api/student
返回:
[
{
“id”: 1,
“name”:“学生人”,
“age”: 16
},
{
“id”: 2,
“name”:“学生8”,
“age”: 17
}
]
控制台打印:
开始查询…
Studnet设置第一页两条数据!
2018-04-27 19:54:56.155 DEBUG 6152 — [nio-8084-exec-8] c.p.d.c.S.findByListEntity_COUNT : ==> Preparing: SELECT count(0) FR
2018-04-27 19:54:56.155 DEBUG 6152 — [nio-8084-exec-8] c.p.d.c.S.findByListEntity_COUNT : > Parameters:
2018-04-27 19:54:56.156 DEBUG 6152 — [nio-8084-exec-8] c.p.d.c.S.findByListEntity_COUNT : <
Total: 1
2018-04-27 19:54:56.157 DEBUG 6152 — [nio-8084-exec-8] c.p.d.c.StudentDao.findByListEntity : ==> Preparing: select id, name, a:
2018-04-27 19:54:56.157 DEBUG 6152 — [nio-8084-exec-8] c.p.d.c.StudentDao.findByListEntity : > Parameters: 2(Integer)
2018-04-27 19:54:56.157 DEBUG 6152 — [nio-8084-exec-8] c.p.d.c.StudentDao.findByListEntity : <
Total: 2 总共有:3条数据,实际返回:2两条数据!
8) E 
查询完毕之后,我们再来看Druid的监控界面。

可以很清晰的看到操作记录!
如果想知道更多的Druid相关知识,可以查看官方文档!
结语
这篇终于写完了,在进行代码编写的时候,碰到过很多问题,然后慢慢的尝试和找资料解决了。本篇文章只是很浅的介绍了 这些相关的使用,在实际的应用可能会更复杂。如果有有更好的想法和建议,欢迎留言进行讨论!
参考文章:
https://www.bysocket.com/?p=1712
Durid官方地址:
https://github.com/alibaba/druid
PageHelper官方地址:
https://github.com/pagehelper/Mybatis-PageHelper
文中源码:
https://github.com/xuwujing/springBoot
-END-

喜欢文章,点个在看
声明:pdf仅供学习使用,一切版权归原创公众号所有;建议持续关注原创公众号获取最新文章,学习愉快!
Spring Boot + MyBatis多模块项目搭建教程
枫本非凡Java后端2019-09-30
点击上工Java后端选择“设为星标:
优质文章,及时送达
作者|枫本非凡
链接 | cnblogs.com/orzlin/p/9717399.html
上篇| IDEA远程一键部署Spring Boot至U Docker
一、前言
最近公司项目准备开始重构,框架选定为SpringBoot+Mybatis,本篇主要记录了在2£人中搭建SpringBoot多模块项目的过程。
1、开发工具及系统环境
• IDE:
IntelliJ IDEA 2018.2
• 系统环境:
mac OSX
2、项目目录结构
• biz层:
业务逻辑层
• dao层:
数据持久层
• web层:
请求处理层
二、搭建步骤
1、创建父工程
IDEA 工具栏选择菜单 File -> New -> Project…

选择Spring Initializr,Initializr默认选择Default,点击Next
填写输入框,点击Next
这步不需要选择直接点Next

点击尸而5卜创建项目
最终得到的项目目录结构如下

beta [-/Workspace/JAVA/beta]
、Add Configuration.
mW to
国 Project •
▼ ybeta tspece/jAVA/beta
► iBidM
►・.mvn
►・src
日.gitIgnore
■ beta.iml
(| mvnw
(| mvnw.ond
息 pom. xml
► Ufa External Libraries
► ’ Scratches and Consoles

Search Everywhere Double 分
Go to File OXN
Recent Files *E
Navigation Bar ‘’
Drop files here to open

■ Terminal 3 g: TODO
QEwntLog
JReM renxxe servari log
1783 at 2014M

删除无用的.mvn目录、src目录、mvnw及mvnw.cmd文件,最终只留相照"0『6和pom.xml
■ beta [-/Wockspace/JAVA/beta] - beta
BeUApplication ▼

<?xal version:”1.。" encoding- UTF-8"?>

<project «lns=,http://Baven.apache.org/POM/4.9.9" XBlns:xsi=,http://ww.w3.org/2Ml/XMLSche«a-instance" ,八. xsi:scheaLocation-"http://aaven.apache.Org/P0M/4.0.0http://Baven.apache.Org/xsd/aaven-4.0.0.) [ D 9
■caode iVe rs ion>4.0.0</aode iVe rs ion>
coa.yibao.beta
beta</a rtifactld>
l.0.0-SNAPSHOT
Jar
beta
Oeao project for Spring Boot

org.springfraaework.boot
spring-boot-starter-parent
2.0.3. RELEASE, ve rs ion>
<!— lookup parent fro« repository —>
</parent


<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding〉
<java.version>l.8</java.version>
〈/properties〉
dependencies*

org.springfraaework.boot
spring-boot-starter


<flroupldx) rg.springfraaework.boot
spring-boot-starter-test
test
〈/dependency*
〈/dependencies〉
project name
B Terminal . Spring 3 Q: TOGO Q Evant Log ,J Rebel remote sarvart ICQ
2、创建子模块
选择项目根目录beta右键呼出菜单,选择New -> Module
IntelliJ IDEA File Edit
View
Navigate Code Analyze Refactor
Build RL
New
Add Framework Support…
HprX Cut
,” । 晅 Copy
Copy Path
Copy Relative Path □ Paste
选择Maven, 点击Next
, Java
咻 Java Enterprise
C JBoss
Module SDK: 蟹 Project SDK (1.8)
Create from archetype
► 12 Module
i File
邕 Scratch File
邦X ■i Directory
3€C
。用C HTML File
翡 Stylesheet
邦V JavaScript File
• Y fl 21 FT

New Module
New.
3d Archetype…
(8 J2ME
・ Clouds
“ Spring
(D1 Android
f IntelliJ Platform Plugin
/ Spring Initializr
Maven
. Gradle
0 Groovy
■ Griffon
■ Grails
• Application Forge
• Static Web
■ Flash
K Kotlin
► com.atlassian.maven.archetypesbamboo-plugin-archetype
► com.atlassian.maven.archetypes confluence-plugin-archetype
► com.atlassian.maven.archetypesjira-plugin-archetype
► com.rfc.maven.archetypesjpa-maven-archetype
► de.akquinet.jbossccjbosscc-seam-archetype
► net.databinder :data-app
► net.liftweb:lift-archetype-basic
► net.liftweb:lift-archetype-blank
► n et. sf. maven - h a r : maven -archetype-har
► net.sf.maven-sar maven-archetype-sar
► org.apache.camel.archetypescamel-archetype-activemq
► org.apache.camel.archetypes camel-archetype-component
► org.apache.camel.archetypescamel-archetype-java
► org.apache.camel.archetypescamel-archetype-scala
► org.apache.camel.archetypes: camel-archetype-spring
► org.apache.camel.archetypes:camel-archetype-war
► org.apache.cocoon cocoon-22-archetype-block
► org.apache.cocoon :cocoon-22-archetype-block-plain
► org.apache.cocoon cocoon-22-archetype-webapp
► org.apache.maven.archetypes:maven-archetype-j2ee-simple
► org.apache.maven.archetypes:maven-archetype-marmalade-mojo
► org.apache.maven.archetypes:maven-archetype-mojo
► org.apache.maven.archetypes:maven-archetype-porttet
► org.apache.maven.archetypes:maven-archetype-profiles
► orn anarhR mavnn archatvn-mavRn-archatvna-ciiickstart
Help
Cancel
填写ArifactId,点击Next

修改Module name增加横杠提升可读性,点击尸血5h
同理添加beta-dao、beta-web子模块,最终得到项目目录结构如下图

■ beta ['/Workspace/JAVA/betaJ - beta

3、运行项目
在beta-web层创建com.yibao.beta.web包(注意:这是多层目录结构并非单个目录名,com >> yibao >> beta >> web)并添 力口入口类BetaWebApplication.java
@SpringBootApplication
public class BetaWebApplication {
3
public static void main(String[] args) {
SpringApplication.run(BetaWebApplication.class, args); } }
在com.yibao.beta.web包中添加controller目录并新建一个controller,添加test方法测试接口是否可以正常访问
@RestController
@RequestMapping(“demo”) public class DemoController {
5 @GetMapping(“test”
6 )
public String test() { return “Hello World!”
;
运行BetaWebApplication类中的main方法启动项目,默认端口为8080,访问http://localhost:8080/demo/test得到如下效果 
.localhost:8080/demo/test X +
<–> C © localhost:8080/demo/test
Hello World!
以上虽然项目能正常启动,但是模块间的依赖关系却还未添加,下面继续完善。微信搜索web_esouce获取更多推送
4、配置模块间的依赖关系
各个子模块的依赖关系:biz层依赖dao层,web层依赖biz层
父pom文件中声明所有子模块依赖(dependencyManagement及dependencies的区别自行查阅文档)
(dependencies
3 >
(dependency
com.yibao.beta</groupId
beta-biz</artifactId
9 >
b e t a . v e r s i o n < / v e r s i o n 11 > 〈 / d e p e n d e n c y 13 > < d e p e n d e n c y 15 > < g r o u p I d > c o m . y i b a o . b e t a < / g r o u p I d 17 > < a r t i f a c t I d > b e t a − d a o < / a r t i f a c t I d 19 > < v e r s i o n > {beta.version}</version 11 > 〈/dependency 13 > <dependency 15 > <groupId>com.yibao.beta</groupId 17 > <artifactId>beta-dao</artifactId 19 > <version> beta.version</version11>/dependency13><dependency15><groupId>com.yibao.beta</groupId17><artifactId>betadao</artifactId19><version>{beta.version}</version
</dependency
<dependency
com.yibao.beta</groupId
beta-web</artifactId
${beta.version}</version
</dependency
「dependencies epen e nc e s


其中$也613入640口}定义在properties标签中
在beta-web层中的pom文件中添加beta-biz依赖
〈dependencies〉
(dependency
com.yibao.beta</groupId
beta-biz</artifactId
(/dependency

在beta-biz层中的pom文件中添加beta-dao依赖

<dependency
com.yibao.beta</groupId
beta-dao</artifactId
</dependency

  1. web层调用biz层接口测试
    在beta-biz层创建com.yibao.beta.biz包,添加service目录并在其中创建DemoService接口类,微信搜索web_resource获取 更多推送
    public interface DemoService {
    2} String test0
    @Service
    public class DemoServicelmpl implements DemoService {
    4 ©Override
    public String test()
    return “test” 
    }
    DemoController通过@Autowired注解注入DemoService,修改DemoController的test方法使之调用DemoService的test方 法,最终如下所示:
    package com.yibao.beta.web.controller; @RestController @RequestMapping(“demo”) public class DemoController { 4 @Autowired
    private DemoService demoService;
    7
    8 @GetMapping(“test”
    9 )
    public String test() {
    return demoService.test(); } }
    再次运行BetaWebApplication类中的main方法启动项目,发现如下报错

APPLICATION FAILED TO START ***************************
4
Description:
Field demoService in com.yibao.beta.web.controller.DemoController required a bean of type 'com.yit 7
Action:
Consider defining a bean of type ‘com.yibao.beta.biz.service.DemoService’ in your configuration.
原因是找不到DemoService类,此时需要在BetaWebApplication入口类中增加包扫描,设置@SpringBootApplication注解中 的scanBasePackages值为com.yibao.beta,最终如下所示
package com.yibao.beta.web;
2
@SpringBootApplication(scanBasePackages = “com.yibao.beta” )
@MapperScan(“com.yibao.beta.dao.mapper”) public class BetaWebApplication {
7
public static void main(String[] args) { SpringApplication.run(BetaWebApplication.class, args)
;
} }
设置完后重新运行main方法,项目正常启动,访问http://localhost:8080/demo/test得到如下效果
• • • . localhost:8080/demo/test X +
<–> C © localhost:8080/demo/test
test
6.集成Mybatis
父pom文件中声明mybatis-spring-boot-starter及lombok依赖
dependencyManagement>
(dependencies
(dependency
org.mybatis.spring.boot</groupId
mybatis-spring-boot-starter</artifactId
1.3.2</version
〈/dependency
(dependency
org.projectlombok</groupId
lombok</artifactId
1.16.22</version
〈/dependency
「dependencies epen e nc e s

在beta-dao层中的pom文件中添加上述依赖

<dependency
mybatis-spring-boot-starter</artifactId
〈/dependency
9 >
(dependency
11 >
mysql</groupId
13 >
mysql-connector-java</artifactId
</dependency
(dependency
org.projectlombok</groupId
lombok</artifactId
</dependency

在beta-dao层创建com.yibao.beta.dao包,通过mybatis-genertaor工具生成dao层相关文件(DO、Mapper、xml),存放目 录如下

applicatio.properties文件添力口jdbc及mybatis相应配置项
spring.datasource.driverClassName = com.mysql.jdbc.Driver
spring.datasource.url = jdbc:mysql://192.168.1.1/test?useUnicode=true&characterEncoding=utf-8 spring.datasource.username = test spring.datasource.password = 123456
5
mybatis.mapper-locations = classpath:mybatis/*.xml
mybatis.type-aliases-package = com.yibao.beta.dao.entity
8
DemoService通过@Autowired注解注入UserMapper,修改口6巾。56口忆6的test方法使之调用UserMapper的 
selectByPrimaryKey方法,最终如下所示
package com.yibao.beta.biz.service.impl;
@Service public class DemoServicelmpl implements DemoService {
@Autowired private UserMapper userMapper;
9 ©Override public String test()
{
UserDO user = userMapper.selectByPrimaryKey(l) ;
return user.toString();
再次运行BetaWebApplication类中的main方法启动项目,发现如下报错
APPLICATION FAILED TO START


3
Description:
Field userMapper in com.yibao.beta.biz.service.impl.DemoServiceImpl required a bean of type 'com.)
6 .
7
8
Action:
Consider defining a bean of type ‘com.yibao.beta.dao.mapper.UserMapper’ in your configuration.

原因是找不到UserMapper类,此时需要在BetaWebApplication入口类中增加dao层包扫描,添加@MapperScan注解并设置其 值为com.yibao.beta.dao.mapper,最终如下所示

package com.yibao.beta.web;
2
@SpringBootApplication(scanBasePackages = “com.yibao.beta” )
@MapperScan(“com.yibao.beta.dao.mapper”) public class BetaWebApplication {
7
public static void main(String[] args) { SpringApplication.run(BetaWebApplication.class, args)
; } }
设置完后重新运行main方法,项目正常启动,访问http://localhost:8080/demo/test得到如下效果
至此,一个简单的SpringBoot+Mybatis多模块项目已经搭建完毕,我们也通过启动项目调用接口验证其正确性。
四、总结
一个层次分明的多模块工程结构不仅方便维护,而且有利于后续微服务化。在此结构的基础上还可以扩展common层(公共组 件)、server层(如dubbo对外提供的服务)微信搜索web_esouce获取更多推送
此为项目重构的第一步,后续还会的框架中集成logback、disconf、redis、dubbo等组件
五、未提到的坑
在搭建过程中还遇到一个maven私服的问题,原因是公司内部的maven私服配置的中央仓库为阿里的远程仓库,它与maven自 带的远程仓库相比有些jar包版本并不全,导致在搭建过程中好几次因为没拉到相应jar包导致项目启动不了。
-END-
如果看到这里,说明你喜欢这篇文章,请转发、点赞。微信搜索「web_resource」,关注后回复「进群」或者扫描下方二维码 即可进入无广告交流群。
1扫描二维码进群1
8久一&

扫一扫上面的二维码图案,加我微信

推荐阅读
1 Java后端优质文章整理
2 . I DEA 远程一键部署 Spring Boot 到 Docker
3 .这26条,你赞同几个?
4 . 7个开源的Spring Boot前后端分离项目 
5 .如何设计API接口,实现统一格式返回?

Java后端
长按识别二维码,关注我的公众号
喜欢文章,点个在看
阅读原文
声明:pdf仅供学习使用,一切版权归原创公众号所有;建议持续关注原创公众号获取最新文章,学习愉快!
Spring Boot + Mybatis配合AOP和注解实现动态数据源切换配置
韩数 Java后端 2019-10-25
点击上方Java后端,选择设为星标
技术博文,及时送达
来自|韩数
链接 | juejin.im/post/5d830944f265da03963bd153
0、前言
随着应用用户数量的增加,相应的并发请求的数量也会跟着不断增加,慢慢地,单个数据库已经没有办法满足我们频繁的数据库 操作请求了。
在某些场景下,我们可能会需要配置多个数据源,使用多个数据源(例如实现数据库的读写分离)来缓解系统的压力等,同样 的,Springboot官方提供了相应的实现来帮助开发者们配置多数据源,一般分为两种方式(目前我所了解到的),分包和AOP。
而在Springboot+Mybatis实现多数据源配置中,我们实现了静态多数据源的配置,但是这种方式怎么说呢,在实际的使用中不 够灵活,为了解决这个问题,我们可以使用上文提到的第二种方法,即使用AOP面向切面编程的方式配合我们的自定义注解来实现 在不同数据源之间动态切换的目的。
1、数据库准备
数据库准备仍然和之前的例子相同,具体建表sql语句则不再详细说明,表格如下:
数据库 testdatasourcel testdatasource2
sys_user y s u 5 e r 2 u s e r i d ( i n t ) r u ys_u5er2 user_id(int)r u ysu5er2userid(int)ruer_name(varchar) 'user_age (int) 同

并分别插入两条记录,为了方便对比,其中testdatasourcel为芳年25岁的张三,testdatasource2为芳年30岁的李四。
2、环境准备
首先新建一个Springboot项目,我这里版本是2.1.7.RELEASE,并在pom文件中引入相关依赖,和上次相比,这次主要额外新增 了aop相关的依赖,如下:
〈dependency〉
mysql
mysql-connector-java
〈/dependency〉

org.springframework.boot
spring-boot-starter-jdbc
々dependency〉
(dependency〉
spring-aop
5.1.5.RELEASE
〈/dependency〉
〈dependency〉
j unit
j unit
〈/dependency〉
(dependency〉
org.aspectj
aspectjweaver
1.9.2
々dependency〉
3、代码部分 首先呢,在我们Springboot的配置文件中配置我们的datasourse,和以往不一样的是,因为我们有两个数据源,所以要指定相 关数据库的名称,其中主数据源为primary,次数据源为secondary如下:
#配置主数据库
spring.datasource.primary.jdbc-url=jdbc:mysql://localhost:3306/testdatasource1?useUnicode=true&ch
spring.datasource.primary.username=root
spring.datasource.primary.password=root
spring.datasource.primary.driver-class-name=com.mysql.cj.jdbc.Driver
6
##配置次数据库
spring.datasource.secondary.jdbc-url=jdbc:mysql://localhost:3306/testdatasource2?useUnicode=true&c
spring.datasource.secondary.username=root
spring.datasource.secondary.password=root
spring.datasource.secondary.driver-class-name=com.mysql.cj.jdbc.Driver
12
13
spring.http.encoding.charset=UTF-8
spring.http.encoding.enabled=true
spring.http.encoding.force=true
需要我们注意的是,Springboot2.0在配置数据库连接的时候需要使用jdbc-url,如果只使用url的话会报jdbcUrl is required with driverClassName.错误。
新建一个配置文件,DynamicDataSourceConfig用来配置我们相关的bean,代码如下
©Configuration
@MapperScan(basePackages = “com.mzd.multipledatasources.mapper”, sqlSessionFactoryRef = "SqlSessi public class DynamicDataSourceConfig {
4
//将这个对象放入Spring容器中
@Bean(name = “PrimaryDataSource”)
//表示这个数据源是默认数据源
©Primary
//读取application .properties中的配置参数映射成为一^个对象
// prefix表示参数的前缀
@ConfigurationProperties(prefix = “spring.datasource.primary”) 
public Datasource getDateSource1() {
return DataSourceBuilder.create().build(); }
@Bean(name = “SecondaryDataSource”)
@ConfigurationProperties(prefix = “spring.datasource.secondary”)
public DataSource getDateSource2() {
return DataSourceBuilder.create().build(); }
@Bean(name = “dynamicDataSource”)
public DynamicDataSource DataSource(@Qualifier(“PrimaryDataSource”) DataSource primaryDataSou @Qualifier(“SecondaryDataSource”) DataSource secondaryDat
〃这个地方是比较核心的targetDataSource集合是我们数据库和名字之间的映射
Map<Object, Object〉 targetDataSource = new HashMap<>();
targetDataSource.put(DataSourceType.DataBaseType.Primary, primaryDataSource);
targetDataSource.put(DataSourceType.DataBaseType.Secondary, SecondaryDataSource);
DynamicDataSource dataSource = new DynamicDataSource。;
dataSource.setTargetDataSources(targetDataSource);
dataSource. setDefaultTargetDataSource(primaryDataSource); //设置默认对象 return dataSource;
}
@Bean(name = “SqlSessionFactory”)
public SqlSessionFactory SqlSessionFactory(@Qualifier(“dynamicDataSource”) DataSource dynamict throws Exception {
SqlSessionFactoryBean bean = new SqlSessionFactoryBean();
bean.setDataSource(dynamicDataSource);
bean.setMapperLocations(
new PathMatchingResourcePatternResolver().getResources("classpath*:mapping//.xmJ return bean.getObject();
}
而在这所有的配置中,最核心的地方就是DynamicDataSource这个类了,DynamicDataSource是我们自定义的动态切换数据源 的类,该类继承了AbstractRoutingDataSource 类并重写了它的determineCurrentLookupKey()方法。
AbstractRoutingDataSource类内部维护了一个名为targetDataSources的Map,并提供的setter方法用于设置数据源关键字与 数据源的关系,实现类被要求实现其determineCurrentLookupKey()方法,由此方法的返回值决定具体从哪个数据源中获取连 接。同时AbstractRoutingDataSource类提供了程序运行时动态切换数据源的方法,在dao类或方法上标注需要访问数据源的关 键字,路由到指定数据源,获取连接。
DynamicDataSource代码如下:
public class DynamicDataSource extends AbstractRoutingDataSource {
2
@Override
protected Object determineCurrentLookupKey() {
DataSourceType.DataBaseType dataBaseType = DataSourceType.getDataBaseType(); 
return dataBaseType; }
DataSourceType类的代码如下:
public class DataSourceType {
2
〃内部枚举类,用于选择特定的数据类型
public enum DataBaseType { Primary, Secondary
}
7
//使用ThreadLocal保证线程安全
private static final ThreadLocal TYPE = new ThreadLocal();
10
//往当前线程里设置数据源类型
public static void setDataBaseType(DataBaseType dataBaseType) { if (dataBaseType == null) {
throw new NullPointerException(); }
TYPE.set(dataBaseType);
}
18
//获取数据源类型
public static DataBaseType getDataBaseType() {
DataBaseType dataBaseType = TYPE.get() == null ? DataBaseType.Primary : TYPE.get(); return dataBaseType;
}
24
//清空数据类型
public static void clearDataBaseType() { TYPE.remove();
}
29
}
接下来编写我们相关的Mapper和xml文件,代码如下:
©Component
@Mapper
public interface PrimaryUserMapper {
List findAll();
}
©Component
@Mapper
public interface SecondaryUserMapper {
List findAll():
}

<?xml version="1.0" encoding="UTF-8" ?> 6 〈select id="findAll" resultType="com.jdkcb.mybatisstuday.pojo.User"> select * from sys_user; 10 12 13 <?xml version="1.0" encoding="UTF-8" ?> 19 select * from sys_user2; 23 24 相关接口文件编写好之后,就可以编写我们的aop代码了: @Aspect @Component public class DataSourceAop { 〃在primary方法前执行 @Before("execution(* com.jdkcb.mybatisstuday.controller.UserController.primary(..))") public void setDataSource2test01() { System .err.println("Primary业务"); DataSourceType.setDataBaseType(DataSourceType.DataBaseType.Primary); } 10 〃在secondary方法前执行 @Before("execution(* com.jdkcb.mybatisstuday.controller.UserController.secondary(..))") public void setDataSource2test02() { System .err.println("Secondary业务"); DataSourceType.setDataBaseType(DataSourceType.DataBaseType.Secondary); } 17 } 编写我们的测试UserController,代码如下: @RestController public class Usercontroller { @Autowired private PrimaryuserMapper primaryUserMapper; @Autowired private SecondaryUserMapper secondaryUserMapper; @RequestMapping("primary") public Object primary(){ List list = primaryUserMapper.findAll(); return list; } @RequestMapping("secondary") public Object secondary(){ List list = secondaryUserMapper.findAll(); return list; } 20 21 } 4、测试 启动项目,在浏览器中分别输入http://127.0.0.1:8080/primary ®http://127.0.0.1:8080/primary ,结果如下: [{"user_id":1,"user_name":"张三","user_age":25}] [{"user_id":1,"user_name":"李四","user_age":30}] 5、等等 等等,啧啧啧,我看你这不行啊,还不够灵活,几个菜啊,喝成这样,这就算灵活了? 那肯定不能的,aop也有aop的好处,比如两个包下的代码分别用两个不同的数据源,就可以直接用aop表达式就可以完成了, 是,如果想本例中方法级别的拦截,就显得优点不太灵活了,这个适合就需要我们的注解上场了。 6、配合注解实现 首先自定义我们的注解@DataSource /** *切换数据注解可以用于类或者方法级别方法级别优先级 > 类级别 */ @Target({ElementType.METHOD, ElementType.TYPE, ElementType.PARAMETER}) @Retention(RetentionPolicy.RUNTIME) ©Documented public ©interface DataSource { String value() default "primary"; //该值即key值,默认使用默认数据库 }  通过使用aop拦截,获取注解的属性value的值。如果value的值并没有在我们DataBaseType里面,则使用我们默认的数据源, 如果有的话,则切换为相应的数据源。 @Aspect ©Component public class DynamicDataSourceAspect { private static final Logger logger = LoggerFactory.getLogger(DynamicDataSourceAspect.class); 5 @Before("@annotation(dataSource)")//拦截我们的注解 public void changeDataSource(JoinPoint point, DataSource dataSource) throws Throwable { String value = dataSource.value(); if (value.equals("primary")){ DataSourceType.setDataBaseType(DataSourceType.DataBaseType.Primary); }else if (value.equals("secondary")){ DataSourceType.setDataBaseType(DataSourceType.DataBaseType.Secondary); }else { DataSourceType. setDataBaseType(DataSourceType. DataBaseType. Primary); //默认使用主数据库 } 16 } 18 @After("@annotation(dataSource)") //清除数据源的配置 public void restoreDataSource(JoinPoint point, DataSource dataSource) { DataSourceType.clearDataBaseType(); 22 23 } 25 } 7、测试 修改我们的mapper,添加我们的自定义的@DataSouse注解,并注解掉我们DataSourceAop类里面的内容。如下: ©Component @Mapper public interface PrimaryUserMapper { ©DataSource List findAll(); ©Component @Mapper public interface SecondaryUserMapper { 12 @DataSource("secondary")//指定数据源为:secondary List findAll(); } 启动项目,在浏览器中分别输入http://127.0.0.1:8080/primary ®http://127.0.0.1:8080/primary ,结果如下:  [{"user_id":1,"user_name":"张三","user_age":25}] [{"user_id":1,"user_name":"李四","user_age":30}] 到此,就算真正的大功告成啦。 8、源码 github.com/hanshuaikang/HanShu-Note 学Java,请关注公众号:Java后端

Java后端
长按识别二维码,关注我的公众号
声明:pdf仅供学习使用,一切版权归原创公众号所有;建议持续关注原创公众号获取最新文章,学习愉快!
Spring MVC+Spring+MyBatis实现支付宝扫码支付功能(图文详解)
纪莫 Java后端 2019-11-13

14 »/
15 public class AlipayConfig {
16 请在这里配置您的基本信息
17 //应用ID,您的APPID,收款型,既是纪的APPID对应支付宝聚号
L8 public static Stringropp_id|= “2016090900468822”;
19 .
20 //商户私钥,您的P"宇梭式RM?乱铜
21 public static String |merc6ar?t ptita亡e he>| = "MIIEvQIBADANBgkqhkiG9w0BAQEF
22
23 // 支付宝公铜.查看地斗:httny//cnpnhcmp alipay.com/platform/keyManage一htm 7
24 public static String alipay publijkey |= “MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8Ar, 25
26 //,务器异步通知页面和至产十叫〃格式的完整路径,不能加?id=123这类自定义参数
27 public static String notify_urL - “http://localhost:8080/alipay.trade.page.
28 =””
29 //页面跳转同步通知页惮住血卬:〃格式的完整路径,不能加?id=123这类自定义参专
public static String return_urL = “http://localhost:8080/alipay.trade.page.
31 //签名方式 ——
32 public static String sign_type - MRSA2”;
33 〃字存房码格式
34 public static String charset = "utf-8**;
35 //支付宝网关 I 1
public static Strind gatewayUrl 卜"https://openapi.alipaydev.com/gateway.de
37 //支付宝网关 1 1
38 public static String Log_path = wd:\test\pay\w;
39 清在这里配置您的基本信息小溪—宁静而致远©2017
readme.txt请好好看一下。
只有一个Java配置类,其余都是JSP。
3.配置AUpayConfig ⑴注册蚂蚁金服开发者账号(免费,不像苹果会收取费用) 注册地址:https://open.alipay.com ,用你的支付宝账号扫码登录,完善个人信息,选择服务类型(我选的是自研)。

⑵设置app_id和gatewayUrl
r? ▼
® ▼ 0 X •二匕 二 O ▼ ▼
区 AlipayConfig.java 区
唯长期稳定,每周日中午12点至每周一中午12点沙箱环境进行维护,期间可能出现不可£
//应用ID,好的赢石丁丽丽厩是绦的APPIDk应支付宝账号 public static String app_id = -2016090900468822";
诋❶
必看部分
APPID ❶
18
19
//支付宝回关
//商户私必然的PKCS8格式RSA2私仍
static String merchant_private_key - NMIIEvQIBADANBgkqhkiG9wOB
// 支付宝公铜,查看地址,https://openhone. alipay.coa/platform/keyHanage public static String atipay_pubLic_key = "MIIBIjANBgkqhkiG9w0BAQEFAA£
//图务器异步通知页面谿径需http:〃格式的完整路径,不能加?id=1.23这类自定 public static ing notify_urL = "http://localhost:8080/alipay.trade
//页面
同步通知页面路径需http:〃格式的完整路径,不能加?id=123这类自 static String return_urL « "http://localhost:8080/alipay.trade
public static String sign_type = “RSA2”;
//字符编码格式 一
public static String charset = “utf-8”;
//支付宝网关
pnhHr fCtic. String gateMoyUrl = "https://openapi.alipaydev.coni/gate
public static String Log_path = “d:\test\pay\”;
小溪_宁静而致远©2017

其中密钥需要自己生成,appID和支付宝网关是已经给好的,网关有dev字样,表明是用于开发测试。
⑶设置密钥

育技术同五点我
APPID O
2016090900468822
支付宝网关O
cd cUr\d,M,、,z«z»
开发者要保证接口中使用的私钥与此处的公钥脸,否则无法调用接口,可—住成方法」沙箱支付宝公钥与线上不同,清更换代码中配・;
RSA2(SHA256陛钥(推荐回 查看应用公钥|查看支付宝公钥
小溪—宁静而致远©2017
点击“生成方法”,打开界面如下:

下周密钥生成工具,解压打开后,选择2048位生成密钥:
如果没有设置过,此时显示文本是"设置应用公钥",我这里是已经设置过得。

必看部分
APPID O 2016090900468822
支付宝网关❶ https://openapi.alipaydev.com/gateway.do
RSA2(SHA256度钥(膈)❶ 杳看应用公钥|查看支付宝公钥
小溪—宁静而致远©2017
设置方法,"打开密钥文件路径”:

复制应用公钥2048.txt中的内容到点击”设置应用公钥”的弹出框中,保存: 
应用公钥(SHA256withRsa)
❷ 使用SHA256withRsa ,支付宝会用SHA256withRsa算法进行接口调用时的
方法
MIIBIjANBgkqhkiG9wOBAQEFAAOCAQ8AMIIBCgKCAQEAn7IQghEEJMSO. Pv8ks2qKOsgYndSNcMd1+ByDCxNFBkuBBBTI+mNIGf7LoYxFe74raNx4k2 CZnklZ9g5aJJjNkFVtRjswzvK9ObpqSfMgj2zwPPaA6/4uHh5ccdbHRM0JPS edW4rBs05UpCs40DS+W7uA33Dg5KwJMS5EgliPUJI74CZhDMvJhWYh2iJ wDsulBRKKsU6mAcAOI+M2RiA3SBgJwlDAQAB
验证公钥正确性>>
近 ©2017
• 商户私钥(merchant_private_key)
复制 应用私钥2048.txt中的内容到merchant_private_key中。
•支付宝公钥(alipay_public_key)
2016090900468822
https://openapi.alipaydev.com/gateway.do
小溪一宁静而致远©2017
点击如上图链接,复制弹出框里面的内容到alipay_public_key。
如果这个设置不对,结果是:支付成功,但是验签失败。
如果是正式环境,需要上传到对应的应用中:
快速入门

上传应用公钥并获取支付宝公钥

更新时间:2017/01/04 访问次数:44807
上传应用公钥并获取支付…
阅读角色:支付宝账号管理者

使用应用私钥生成请求签名

⑷服务器异步通知页面路径(notify_url)
如果没有改名,修改IP和端口号就可以了,我自己的如下:
http://localhost:8080/alipay.trade.page.pay-JAVA-UTF-8/notify_url.jsp
⑸页面跳转同步通知页面的路径(return_url)
http://localhost:8080/alipay.trade.page.pay-JAVA-UTF-8/return_url.jsp
4.测试运行

测试用的支付宝买家账户可以在"沙箱账”这个页面可以找到:
支付成功后,验签结果:
问题解决
由于我们使用的是沙箱测试环境,测试环境和正式上线的环境的网关是不一样的,如果配置错误,会出现,appid错误的问题。
配置如下:

源代码下载,可以关注微信公众号Java后端回复扫码即可获取。
将支付宝支付整合到ssm框架
1、项目架构

• 项目架构:spring+springmvc+mybatis
• 数据库:mysql
• 部署环境:tomcat9.0
• 开发环境:jdk9、idea
• 支付:支付宝、微信 整合到ssm一样,我们需要像沙箱测试环境一样,需要修改支付的配置信息

2、数据库代码
主要包括以下的数据库表:
• user:用户表
• order:支付产生的订单
• flow:流水账
• product:商品表:用于模拟购买商品。
drop table if exists user;
/================================/

  • Table: user *
    /================================/ create table user
    (
    id varchar(20) not null,
    username varchar(128),
    sex varchar(20), primary key (id)
    );
    alter table user comment ‘用户表’;
    CREATE TABLE ‘flow’ (
    ‘id’ varchar(20) NOT NULL,
    ‘flow_num’ varchar(20) DEFAULT NULL COMMENT ‘流水号’,
    ‘order_num’ varchar(20) DEFAULT NULL COMMENT ‘订单号’,
    ’ product_id’ varchar(20) DEFAULT NULL COMMENT ‘产品主键ID’,
    ‘paid_amount’ varchar(11) DEFAULT NULL COMMENT ‘支付金额’,
    ‘paid_method’ int(11) DEFAULT NULL COMMENT ‘支付方式\r\n 1:支付宝\r\n 2:微信’,
    ’ buy_counts’ int(11) DEFAULT NULL COMMENT ‘购买个数’,
    ’ create_time’ datetime DEFAULT NULL COMMENT ‘创建时间’, PRIMARY KEY (‘id’)
    )ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT=‘流水表’;
    CREATE TABLE ‘orders’ (
    ‘id’ varchar(20) NOT NULL,
    ‘order_num’ varchar(20) DEFAULT NULL COMMENT ‘订单号’,
    ‘order_status’ varchar(20) DEFAULT NULL COMMENT ‘订单状态\r\n 10:待付款\r\n 20:已付款’,
    ‘order_amount’ varchar(11) DEFAULT NULL COMMENT ‘订单金额’,
    ‘paid_amount’ varchar(11) DEFAULT NULL COMMENT ‘实际支付金额’,
    ’ product_id’ varchar(20) DEFAULT NULL COMMENT ‘产品表外键ID’,
    ’ buy_counts’ int(11) DEFAULT NULL COMMENT ‘产品购买的个数’,
    ’ create_time’ datetime DEFAULT NULL COMMENT ‘订单创建时间’,
    ’ paid_time’ datetime DEFAULT NULL COMMENT ‘支付时间’,
    PRIMARY KEY (‘id’)
    )ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT=‘订单表’;
    CREATE TABLE ‘product’ (
    ‘id’ varchar(20) NOT NULL,
    ‘name’ varchar(20) DEFAULT NULL COMMENT ‘产品名称’,
    ’ price’ varchar(11) DEFAULT NULL COMMENT ‘价格’,
    PRIMARY KEY (‘id’)
    )ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT=‘产品表’;
    3、~3。数据接口层 这里就不介绍了,这个只包括简单的curd,可以使用’通用mapper’,或者’逆向工程’就行。以订单order为例给出: 
    public interface OrdersMapper {
    int countByExample(OrdersExample example);
    int deleteByExample(OrdersExample example);
    int deleteByPrimaryKey( String id);
    int insert(Orders record);
    int insertSelective( Orders record);
    List selectByExa mple (OrdersExample example);
    Orders selectByPrimaryKey (String id);
    int updateByExampleSelective(@Param(“record”) Orders record, @Param(“example”) OrdersExample example);
    int updateByExample(@Param(“record”) Orders record, @Param(“example”) OrdersExample example);
    int updateByPrimaryKeySelective( Orders record);
    int updateByPrimaryKey (Orders record);
    }
    4、service层
    同上,最后在项目源代码里可见。以订单order为例给出:

*订单操作service @author ibm
*
*
public interface OrdersService {
*

  • 新增订单
    @param order

public void saveOrder(Orders order);
*
*
@Titl : OrdersService.java
@Package com. sihai.service
@Descriptio :修改叮当状态,改为支付成功,已付款;同时新增支付流水

  • Copyright: Copyright © 2017
  • CompanyFURUBOKE. SCIENCE.AND. TECHNOLOGY

@author sihai
@date 2017年8月23日下午9:04:35
@version V1.0
*
public void updateOrderStatus(String orderld, String alpayFlowNum, String paidAmount);
*

  • 获取订单
    @param orderd
    @return

public Orders getOrderById(String orderld);
} 
4、支付宝支付controller (支付流程)
支付流程图

首先,启动项目后,输入http:〃localhost:8080/,会进入到商品页面,如下

G| 命 | ① Iocalhost8080
[
*最常访问 包火狐官方站点 ■新手上路 包常用网址E3京东商城 中最常访问 白火孤官方站点 产品编号产品名称产品价格操作
1 辣条 0.01 购买
2 面包 0.02 购买
下面是页面代码
商品页面(products.jsp)
▼ ■ WEB-INF
▼・)印
耨 a I i pay Su ccess j sp
耨 goConfirmjsp
[gjgoPay.jsp
患 payQrCodejsp
耨 paySuccessjsp
患 productsjsp
招 user.jsp
…@web.xml .
https:/iJbTgisdn. net/sihai 12345
代码实现:

产品编号产品名称产品价格 操作
${p.id }${p.name }${p.price } 购买

填写个数,然后点击生成订单,调用如下代码 
/**
*分段提交

©RequestMapping(value = z createOrder")
©ResponseBody
public LeeJSONResult createOrder(Orders order) throws Excejgtion {
Product p = productService. getProductByld(order. getProductldO);
String orderld = sid. nextShort();
order, setld(orderld);
order. setOrderNum(orderld):
order. setCreateTime(new Date());
order, set Order Amount (String. valueOf(^ 1 oat. valueOf^. getPrice ()) * order. getBuyCounts ()));
order. setOrderStatus (OrderStatusEnum. WAIT_PAY. key);
orderService. saveOrder(order);
return LeeJSONResult. oA’Corderid);
} https://blog. csdn. net/sihai12345
根据SID (生成id的工具)等信息生成订单,保存到数据库。
Tips:可以关注微信公众号:Java后端,获取更多类似技术博文推送。
进入到选择支付页面

调用了如下代码:
/**

  • 分段提交
  • 第二段
  • @param orderld
  • ©return
  • ©throws Exception
    */
    @RequestMapping(value = goPay")
    public ModelAndView goPay(String orderld) throws Exception {
    Orders order = orderService. getOrderByld(orderld);
    Product p = productservice. getProductByld(order. getProductldO);
    ModelAndView mv = new ModelAndView( viewName: rgoPay" );
    mv. addObject ( attributeName: order \ order);
    mv. addObject ( attributeName: p z, p);
    return mv;
    } https: I blog. csdn. net si ha i 12345
    然后,我们选择支付宝支付,进入到了我们支付的页面了,大功告成!

调用了如下代码: 
@RequestMapping(va山e = “/goAlipay”, produces = “text/html; charset=UTF-8”)
@ResponseBody
public String goAlipay(String orderId, HttpServletRequest request, HttpServletRequest response) throws Exception {
Orders order = orderService.getOrderById(orderId);
Product product = productService.getProductById(order.getProductId());
〃获得初始化的AlpayQient
AlipayClient alipayClient = new DefaultAlipayClient(AlipayConfig.gatewayUrl, AlipayConfig.app_id, AlipayConfig.merchant_private_ke:
〃设置请求参数
AlipayT radePagePayRequest alipayRequest = new AlipayT radePagePayRequest();
alipayRequest.setReturnUrl(AlipayConfig.return_url);
alipayRequest.setNotifyUrl(AlipayConfig.notify_url);
〃商户订单号,商户网站订单系统中唯一订单号,必填
String out_trade_no = orderId;
〃付款金额,必填
String total_amount = order.getOrderAmount();
〃订单名称,必填
String subject = product.getName();
〃商品描述,可空
String body =“用户订购商品个数:”+ order.getBuyCounts();
“该笔订单允许的最晚付款时间,逾期将关闭交易。取值范围:1m〜15d° m-分钟,h-小时,d-天, lc-当天 (1c-当天的情况下,无论交易何时£ String timeout_express = “1c”;
alipayRequest.setBizContent(“{“out_trade_no”:”“+ out_trade_no +”“,”

  • ““total_amount”:”“+ total_amount +”“,”
  • ““subject”:”“+ subject +”“,”
  • ““body”:”“+ body +”“,”
  • ““timeout_express”:”“+ timeout_express +”“,”
  • ““product_code”:“FAST_INSTANT_TRADE_PAY”}”);
    //请求
    String result = alipayClient.pageExecute(alipayRequest).getBody();
    return result;
    }
    “1 2d
    这段代码都可以在阿里支付的demo里面找到的,只需要复制过来,然后改改,整合到ssm环境即可。
    上面就是将阿里支付宝支付整合到ssm的全过程了。本文参考了博主「小溪_宁静而致远」的支付教程文章,欢迎搜索博主关注。
    -END-
    如果看到这里,说明你喜欢这篇文章,请转发、点赞。微信搜索「web_resource」,关注后回复「进群」或者扫描下方二维码 即可进入无广告交流群。
    J扫描二维码进群1

推荐阅读
1 Spring Boot + PageHelper实现多数据源并分页
2 . Spring Boot微信点餐系统
3 .Spring MVC至U Spring Boot的简化之路
4 12306的架构到底有多牛逼?
5 .团队开发中Git最佳实践

喜欢文章,点个在看
阅读原文
声明:pdf仅供学习使用,一切版权归原创公众号所有;建议持续关注原创公众号获取最新文章,学习愉快!
【MyBatis】写了 10年的代码,我最怕写MyBatis这些配置,现在有详解了
Java后端2月28日

作者|阿进的写字台
编辑|公众号(Java后端)
链接 | cnblogs.com/homejim/p/9782403.html
在使用mybatis过程中,当手写JavaBean和XML写的越来越多的时候,就越来越同意出错。这种重复性的工作,我们当然 不希望做那么多。
还好,mybatis为我们提供了强大的代码生成–MybatisGenerator。
通过简单的配置,我们就可以生成各种类型的实体类,Mapper接口,MapperXML文件,Example对象等。通过这些生成的文 件,我们就可以方便的进行单表进行增删改查的操作。
Tips:关注微信公众号:Java后端,获取每日推送。
以下的工具使用的都是IDEA
1.1 创建Maven项目
1.1.1 菜单上选择新建项目
File | New | Project
1.1.2 选择左侧的Maven
H New Project X
Project D I G 1 . 3 v e r s i o n ▼ N e w . . . C r e a t e f r o m a r c h e t y p « A d d A r c h e t y p e 一► 7 o m . a T ′ 3 S s i a n . − r i 3 > e ′ . a r c h e t − , p < s : b a m b o o − p l u g i n − a r c h e t y p e ► l o m . a : a s s i a n . n a , − : − . 3 r c h € ? / p e s : c o n f l u e n c e − p l u g i n − a r c h e t y p e ► n i a v c n r c h e t ; , p c s j i r a − p l u g i n − a r c h e t y p e ► c o m . r f c j n a v e n . a r c h e t y p e 5 j p a − m a v e n − a r c h e t y p e ► d e a k q u i n e t . j b o ′ i t c c j b o DIG ^1.3 version ▼ New... Create from archetyp« Add Archetype一 ► 7om.aT'3Ssian.-ri3> e'.archet-,p<s:bamboo-plugin-archetype ► lom.a: assian.na,-:-.3rch€?/pes:confluence-plugin-archetype ► niavcn^rchet;, pcs jira-plugin-archetype ► com.rfcjnaven.archetype5 jpa-maven-archetype ► deakquinet.jbo'itcc jbo DIG1.3versionNew...Createfromarchetyp«AddArchetype►7om.aT3Ssian.ri3>e.archet,p<sbamboopluginarchetypelom.a:assian.na,.3rch?/pes:confluencepluginarchetypeniavcnrchet;,pcsjirapluginarchetypecom.rfcjnaven.archetype5jpamavenarchetypedeakquinet.jboitccjbo c c − cc- cceam-archetype
► netdatabinde: data-app
► netliftwcc; lift-archetype-basic
► netl(ftwcb:lift-archetype- blank
► net.sfrnavefi-bn^maven-archetype-har
〉 一, -maven-orchetype-s«r
► q apaJ - I io- camel-archetype-activefnq
► ^rg.apache.came .archetype.xamd-archetype-component
I* ’ ,•一 camel-archetype java
► org.apachc.camc archetypt: camel-archetypc-scala ► org^f ’ c^mel-archetype-spnng
► orgqpacheqmdarchetypemel-archetype-war
► orgxipache.cocoon:cocoon-22-archetype-block
► org.apaehe.cocoon cocoon-22-archetype-block-plain
► org.apache.cocoon cocoon-22-archetype-webapp
► : rg^jpache.mavenujrchetyp’" , :maven-archetype-j2ee-simple
► rg^pache.maven.archet .i)".:mav€n-archrtype-marmalade-mojo
► 1 g.apache.ni.11.-1. .T< het.: - maven-archetype-mojo
► org.apache.ma^en.archetypesimaven-archetype-portiet
► org.apache.mavcn.archetype;:maven-archetype-profiles
:―)一”E■喷诙愉 esdn. net/weixin 37139197
► rg.apacre.mdven^.v< hrtyp maven-archetype-srte
由于我们只是创建一个普通的项目,此处点击Next即可。
1.1.3 输入GroupId和ArtifactId
•在我的项目中,
GroupId 填 com.homejim.mybatis
ArtifactId 填 mybatis-generator
点击Next。
1.1.4 Finish
通过以上步骤,一个普通的Maven项目就创建好了。
1.2 配置 generator.xml
其实名字无所谓,只要跟下面的pom.xml文件中的对应上就好了。

<?xml version="1.0" encoding="UTF-8" ?>

-K Compare With… Ctrl+D Database Changes tGeneia-
Compare File with Editor Designer
? Compare with Clipboard Event Log 的配置-
Quick Switch Scheme… Ctrl Image Layers
Toolbar
'成— 4 山蚌甲
View Navigate Code Analyze Refactor Build Run Tools VCS Window Hei J

1.4.2 Maven Projects 中双击 mybatis-generator
在右侧此时可以看到Maven Projects 了。找到mybatis-generator插件。
mybatis-generator | Plugins | mybatis-generator | mybatis-generator
▼ 1T mybatis-generator
►% Lifecycle
▼ Bg Plugins
► iS clean (org.apache.rrj ► compiler (org.apach ► [^deploy (org.apache. ► (^install (org.apache.n ►焦jar (org.apache.mav ▼儒 mybatis-generator f
R mybati s - g en eratc R mybati s - g en eratc
► 儒 resources (org.apac
► 薛 site (org.apache.ma1 ► surefire (org.apache
1.4.3双击运行
运行正确后,生成代码,得到如下的结构
♦彳 mybatis-generator C:\Users\Administrator\Doi
► ■ .idea
▼ ■ src
▼・ main
T ・java
▼ EB com.homejim.mybatis
▼ Z entity
④ Blog
@ Blog Example
▼ > mapper
❶ BlogMapper
▼跑 resources
▼ B mybatis.mapper
患 BlogMapperjcml
翡 generatonxml
► ・ test
jf_ mybatis-generator.iml
/7) pom.xml 
仅仅是上面那么简单的使用还不够爽。那么我们就可以通过更改generator.xml配置文件的方式进行生成的配置。
2.1 文档
推荐查看官方的文档。
英文不错的:http://www.mybatis.org/generator/configreference/xmlconfig.html
中文翻译版:http://mbg.cndocs.ml/index.html
2.2 官网没有的
2.2.1 property 标签
该标签在官网中只是说用来指定元素的属性,至于怎么用没有详细的讲解。
2.2.1.1 分隔符相关
〈property name=“autoDelimitKeywords” value=“true”/>
〈property name=“beginningDelimiter” value=“'”/>
〈property name=“endingDelimiter” value="’ "/>
以上的配置对应的是mysql,当数据库中的字段和数据库的关键字一样时,就会使用分隔符。
比如我们的数据列是delete,按以上的配置后,在它出现的地方,就变成’delete’。
2.2.1.2 编码
默认是使用当前的系统环境的编码,可以配置为GBK或UTF-8。
〈property name=“javaFileEncoding” value=“UTF-8”/>
我想项目为UTF-8,如果指定生成GBK,则自动生成的中文就是乱码。
2.2.1.3 格式化
〈property name="javaFormatter“ value=“org.mybatis.generator.api.dom.DefaultJavaFormatter”/>
<property name="xmlFormatter“ value=“org.mybatis.generator.api.dom.DefaultXmlFormatter”/>
这些显然都是可以自定义实现的的。
2.2.2 plugins 标签
plugins标签用来扩展或修改代码生成器生成的代码。
在生成的XML中,是没有这个标签的。该标签是配置缓存的。
如果我们想生成这个标签,那么可以plugins中进行配置。
〈p山gin type=“org.mybatis.generator.p山gins.CacheP山gin” >

〈/p仙gin> 
,paghe eviction="LRU”>
<! —
宵 ARNING - ®mbg generated
’ Illis element is auton|atically generated by MyBatis Genexatox. do not modify.
£ https://blog. csdn. net/weixin_37139197
/—h.0
比如你想生成的JavaBean中自行实现Serializable接口。

还能自定义插件。
这些插件都蛮有用的,感觉后续可以专门开一篇文章来讲解。
看名称,就知道是用来生成注释用的。
默认配置:

〈property name=“suppressA11comments” value=“false”/>
〈property name=“suppressDate” value=“false”/>
〈property name=“addRemarkComments” value=“false”/>

suppressAllComments:阻止生成注释,默认值是false。
suppressDate:阻止生成的注释包含时间戳,默认为false。
addRemarkComments:注释中添加数据库的注释,默认为false。
还有一个就是我们可以通过 type 属性指定我们自定义的注解实现类,生成我们自己想要的注解。自定义的实现类需要实
现 0rg.mybatis.generator.api.CommentGenerator。
2.2.4 源码
https://github.com/homejim/mybatis-cn
E homejirn / mybatis-cn ® Watch» 2 | * Unstar | 16 ] 曾 | 4
<> Code ① Issues 0 fl Pull requests 0 ■ Projects 0 EK Wiki 通 Insights * Settings
mybatis源码的中文J博以及mybatis的使用和源码解析 Edit
mybatis mybatis-sources mybatis-Chinese Manage topics
® 83 commits 炉 1 branch 4 0 releases 211 contributor 杀 View license
| Branch: master- New poll request Create new file Lfplosd files j Find file
本文作者:阿进的写字台,原文链接:cnblogs.com/homejim/p/9782403.html
由公众号Java后端)编辑,转载请著名本句话及原文出处。 
-END-
如果看到这里,说明你喜欢这篇文章,请转发、点赞。微信搜索「web_resource」,欢迎添加小编微信「focusoncode」,每 日朋友圈更新一篇高质量技术博文(无广告)。

推荐阅读
1浅谈Web网站架构演变过程
2. 一个非常实用的特性,很多人没用过
3.Spring Boot 2.x操作缓存的新姿势
4 MyBatis中SQL写法技巧小总结

声明:pdf仅供学习使用,一切版权归原创公众号所有;建议持续关注原创公众号获取最新文章,学习愉快!
【小技巧】MyBatis中SQL写法技巧小总结
koala Java后端2月27日

最近有个兄弟在搞mybatis,问我怎么写sql ,说简单一点巾丫53甘5就是写原生sql,官方都说了 mybatis的动态sql语句是基于 OGNL表达式的。可以方便的在sql语句中实现某些逻辑.总体说来mybatis动态SQL语句主要有以下几类:

  1. if语句(简单的条件判断)
  2. choose (when,otherwize),相当于java 语言中的 switch ,与 jstl 中的choose 很类似.
  3. trim (对包含的内容加上prefix,或者suffix等,前缀,后缀)
  4. where (主要是用来简化sql语句中where条件判断的,能智能的处理and or ,不必担心多余导致语法错误)
  5. set (主要用于更新时)
  6. foreach (在实现mybatis in语句查询时特别有用)
    我说一下:if when都仅仅对于Map类型的才能进行判断,Integer, String那些都不能进行判断,虽然说mybatis最后都是把参 数封装为一个Map集合再通过占位符接入的。另一个就是trim很强大,可以说where和set能干的他都能干。choose在进行”单 个“模糊查询时候很方便。
    往期优质技术文章可以关注徵信公众号:Java后端,后台回复技术博文获取。
    下面分别介绍这几种处理方式
  7. mybaits if语句处理
    〈select id=“dynamiclfTest” parameterType=“Blog” resultType=“Blog”>
    select * from t_blog where 1 = 1

    and title = #{title}


    and content = #{content}


    and owner = #{owner}


    这条语句的意思非常简单,如果你提供了title参数,那么就要满足title=#{title},同样如果你提供了Content和。wner的时候, 它们也需要满足相应的条件,之后就是返回满足这些条件的所有Blog,这是非常有用的一个功能,以往我们使用其他类型框架或 者直接使用JDBC的时候,如果我们要达到同样的选择效果的时候,我们就需要拼SQL语句,这是极其麻烦的,比起来,上述的 动态SQL就要简单多了。
  8. choose (when,otherwize),相当于java 语言中的 switch,与 jstl 中的choose 很类似 
    〈select id=“dynamicChooseTest” parameterType=“Blog” resultType=“Blog”>
    select * from t_blog where 1 = 1

    〈when test=“title != null”>
    and title = {{title}

    〈when test=“content != null”>
    and content = {{content}

    〈otherwise)
    and owner = “ownerl”
    〈/otherwise)

    〈/select)
    when元素表示当when中的条件满足的时候就输出其中的内容,跟JAVA中的switch效果差不多的是按照条件的顺序,当when中 有条件满足的时候,就会跳出choose,即所有的when和。therwise条件中,只有一个会输出,当所有的我很条件都不满足的时 候就输出otherwise中的内容。所以上述语句的意思非常简单,当title!=null的时候就输出and titlte = #{title},不再往下判断条 件,当title为空且content!=null的时候就输出and content = #{content},当所有条件都不满足的时候就输出otherwise中的内 容。
    3.trim (对包含的内容加上prefix,或者suffix等,前缀,后缀)
    〈select id=“dynamicTrimTest” parameterType=“Blog” resultType=“Blog”>
    select * from t_blog

    <if test=“title != null”)
    title = #{title}

    <if test=“content != null”)
    and content = #{content}

    <if test=“owner != null”)
    or owner = #{owner}
    </if)
    〈/trim)
    〈/select)
    后缀,与之对应的属性是prefix和suffix;可以把包含内容的首部某些内容覆盖,即忽略,也可以把尾部的某些内容覆盖,对应的 属性是prefixOverrides和suffixOverrides;正因为切巾有这样的功能,所以我们也可以非常简单的利用trim来代替where元素 的功能。
  9. where (主要是用来简化sql语句中where条件判断的,能智能的处理and or条件 
    〈select id=“dynamicWhereTest” parameterType=“Blog” resultType=“Blog”>
    select * from t_blog

    title = #{title}


    and content = #{content}


    and owner = #{owner}



    where元素的作用是会在写入where元素的地方输出一个where,另外一个好处是你不需要考虑where元素里面的条件输出是什 么样子的,MyBatis会智能的帮你处理,如果所有的条件都不满足那么MyBatis就会查出所有的记录,如果输出后是and 开头
    的,乂丫33也会把第一个and忽略,当然如果是or开头的,MyBatis也会把它忽略;此外,在where元素中你不需要考虑空格的问 题,MyBatis会智能的帮你加上。像上述例子中,如果title=null,而content != null,那么输出的整个语句会是select * from t_blog where content = #{content},而不是select * from t_blog where and content = #{content},因为乂丫33甘5会智能的 把首个and或or给忽略。
    5.set (主要用于更新时)
    〈update id=“dynamicSetTest” parameterType=“Blog”>
    update t_blog

    title = #{titie}

    content = #{ontent}
    owner = #{owner}


    where id = #{id}

    set元素主要是用在更新操作的时候,它的主要功能和where元素其实是差不多的,主要是在包含的语句前输出一个set,然后如 果包含的语句是以逗号结束的话将会把该逗号忽略,如果set包含的内容为空的话则会出错。有了set元素我们就可以动态的更新 那些修改了的字段。
  10. foreach (在实现mybatis in语句查询时特别有用)
    foreach的主要用在构建in条件中,它可以在SQL语句中进行迭代一个集合。foreach元素的属性主要有
    item, index, collection, open, separator, close。item表示集合中每一个元素进行迭代时的别名,index指定一个名字, 用于表示在迭代过程中,每次迭代到的位置,open表示该语句以什么开始,separator表示在每次进行迭代之间以什么符号作为 分隔符,close表示以什么结束,在使用foreach的时候最关键的也是最容易出错的就是collection属性,该属性是必须指定的, 但是在不同情况下,该属性的值是不一样的,主要有一下3种情况: 
    • 如果传入的是单参数且参数类型是一个List的时候,collection属性值为list。
    • 如果传入的是单参数且参数类型是一个3『^丫数组的时候,collection的属性值为array。
    • 如果传入的参数是多个的时候,我们就需要把它们封装成一个Mapy,当然单参数也可以封装成map,实际上如果你在传入 参数的时候,在MyBatis里面也是会把它封装成一个Map的,map的key就是参数名,所以这个时候collection属性值就是传 入的List或array对象在自己封装的map里面的key。
    作者:牧羊人影视
    原文链接:https://blog.csdn.net/tengxing007/article/details/54864897
    -END-
    如果看到这里,说明你喜欢这篇文章,请转发、点赞。微信搜索「web_resource」,欢迎添加小编微信「focusoncode」,每 日朋友圈更新一篇高质量技术博文(无广告)。
    I扫描二维码添加小编1

推荐阅读
工狠!删库跑路!
2 .分布式与集群的区别究竟是什么?
3 .看完这篇HTTP,面试官就难不倒你了
4 .快速建站利器!

声明:pdf仅供学习使用,一切版权归原创公众号所有;建议持续关注原创公众号获取最新文章,学习愉快!
从0开始手写一个Mybatis框架,三步搞定!
我叫刘半仙Java后端1月9日

来自:开源中国,作者:我叫刘半仙 链接:my.oschina.net/liughDevelop/blog/1631006
继上一篇手写SpringMVC之后,我最近趁热打铁,研究了一下Mybatis。MyBatis框架的核心功能其实不难,无非就是动态代理 和jdbc的操作,难的是写出来可扩展,高内聚,低耦合的规范的代码。本文完成的Mybatis功能比较简单,代码还有许多需要改 进的地方,大家可以结合Mybatis源码去动手完善。
一、Mybatis框架流程简介

在手写自己的Mybatis框架之前,我们先来了解一下Mybatis,它的源码中使用了大量的设计模式,阅读源码并观察设计模式在其 中的应用,才能够更深入的理解源码(ref: Mybatis源码解读-设计模式总结http://www.crazyant.net/2022.html)。我们对 上图进行分析总结:
1、mybatis的配置文件有2类
• mybatisconfig.xml,配置文件的名称不是固定的,配置了全局的参数的配置,全局只能有一个配置文件。
• Mapper.xml配置多个statemement,也就是多个sql,整个mybatis框架中可以有多个Mappe.xml配置文件。
2、通过mybatis配置文件得到SqlSessionFactory
3、通过SqlSessionFactory得到SqlSession,用SqlSession就可以操作数据了。
4、SqlSession通过底层的Executor (执行器),执行器有2类实现:
, * Executor - org.apache.ibatis.executor
» ♦第 BaseExecutor - o|~g.apache.ibatis.executor
年 Caching Executor - org.apache.ibatis.executor
•基本实现
•带有缓存功能的实现
5、MappedStatement是通过Mapper.xml中定义statement生成的对象。
6、参数输入执行并输出结果集,无需手动判断参数类型和参数下标位置,且自动将结果集映射为Java对象
• HashMap, KV格式的数据类型
• Java的基本数据类型
• POJO, java的对象
二、梳理自己的Mybatis的设计思路
根据上文Mybatis流程,我简化了下,分为以下步骤:

1、读取xml*件,建立连接
从图中可以看出,MyConfiguration负责与人交互。待读取xml后,将属性和连接数据库的操作封装在MyConfiguration对象中 供后面的组件调用。本文将使用dom4j来读取xml文件,它具有性能优异和非常方便使用的特点。
2、创建SqlSession,搭建Configuration和Executor之间的桥梁
我们经常在使用框架时看到Session, Session到底是什么呢? 一个Session仅拥有一个对应的数据库连接。类似于一个前段请求 Request,它可以直接调用exec(SQL)来执行SQL语句。从流程图中的箭头可以看出,MySqlSession的成员变量中必须得有 MyExecutor和MyConfiguration去集中做调配,箭头就像是一种关联关系。我们自己的MySqlSession将有一^个getMapper方 法,然后使用动态代理生成对象后,就可以做数据库的操作了。
3、创建Executor,封装JDBC操作数据库
Executor是一个执行器,负责SQL语句的生成和查询缓存(缓存还没完成)的维护,也就是jdbc的代码将在这里完成,不过本文 只实现了单表,有兴趣的同学可以尝试完成多表。
4、创建MapperProxy,使用动态代理生成Mapper对象
我们只是希望对指定的接口生成一个对象,使得执行它的时候能运行一句sql罢了,而接口无法直接调用方法,所以这里使用动态 代理生成对象,在执行时还是回到MySqlSession中调用查询,最终由MyExecutor做JDBC查询。这样设计是为了单一职责,可 扩展性更强。 
三、实现自己的Mybatis
工程文件及目录:
/ 理 liughi-imybatis
▲ * src/main/java
/ ■ com.liugh
/ SS bean

D User.java
d 器 config
D Functionjava
D MapperBean.java
/ SS mapper
[j UserMapper.java
▲ . sqlSession
{j Excutorjava
D MyConfigu ration Java
曲 MyExcutorjava
■ MyMapperProxyjava
D My Sq I session java
D D TestMybatisjava
a src/main/resources
Q config.xml
Q UserMapper.xml
% src/test/java
% src/test/resou rces
0 J RE System Library [JavaSE-1.8]
3 Maven Dependencies
» a
» target
B pom.xml
首先,新建一个maven项目,在pom.xml中导入以下依赖:
〈project xmlns=“http://maven.apache.Org/POM/4.0.0” xmlns:xsi=“http://www.w3.org/2001/XMLSchema-instance” xsi:schemaLocation="h
4.0.0
com.liugh
liugh-mybatis
0.0.1-SNAPSHOT
jar
〈properties)
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<maven.compiler.source>1.8</maven.compiler.source>
<maven.compiler.target>1.8</maven.compiler.target>
<java.version>1.8</java.version>
〈/properties)
〈dependencies)
<!一读取xml文件->
〈dependency)
dom4j
〈artifactId>dom4j〈/artifactId>
〈version>1.6.1〈/version>
〈/dependency)
<–MySQL -->
〈dependency)
〈groupId)mysql〈/groupId)
〈artifactId)mysql-connector-java〈/artifactId)
〈version)5.1.29〈/version)
</dependency)
〈/dependencies)
〈/project) 
创建我们的数据库xml配置文件:

<?xml version="1.0" encoding="UTF-8"?>

〈database)
〈property name=“driverClassName”>com.mysql.jdbc.Driver
〈property name=“url”>jdbc:mysql://localhost:3306/test?useUnicode=true&characterEncoding=utf8
〈property name=“username”>root
〈property name="password”>123456
〈/database)
然后在数据库创建test库,执行如下SQL语句:
CREATE TABLE ‘user’ (
’ id’ varchar(64) NOT NULL,
’ password’ varchar(255) DEFAULT NULL,
’ username’ varchar(255) DEFAULT NULL,
PRIMARY KEY (‘id’)
)ENGINE=InnoDB AUTO_INCREMENT=2 DEFAULT CHARSET=utf8;
INSERT INTO ‘test’.’ user’ (’ id’, ’ password’, ’ username’) VALUES (T, ‘123456’, ‘liugh’);
创建User实体类,和UserMapper接口和对应的xml文件:
package com.liugh.bean;
public class User {
private String id;
private String username;
private String password;
//省略ge t se t toString方法…
}
package com.liugh.mapper;
import com.liugh.bean.User;
public interface UserMapper {
public User getUserById (String id);
}

<?xml version="1.0" encoding="UTF-8"?> select from user where id = ? 基本操作配置完成,接下来我们开始实现MyConfiguration: * ,读取与解析配置信息,并返回处理后的Environment / public class MyConfiguration { private static ClassLoader loader = ClassLoader.getSystemClassLoader(); .j/-i/\i\eauer r eauer new ea er ; Document document = reader.read(stream); Element root = document.getRootElement(); return evalDataSource(root); } catch (Exception e) { throw new RuntimeException("error occured while evaling xml " + resource); } } private Connection evalDataSource(Element node) throws ClassNotFoundException { if (!node.getName().equals("database")) { throw new RuntimeException("root should be〈database〉"); } String driverClassName = null; String url = null; String username = null; String password = null; 〃获取属性节点 for (Object item : node.elements("property")) { Element i = (Element) item; String value = getValue(i); String name = i.attributeValue("name"); if (name == null || value == null) { throw new RuntimeException("[database]:〈property〉should contain name and value"); } //赋值 switch (name) { case "url" : url = value; break; case "username" : username = value; break; case "password" : password = value; break; case "driverClassName" : driverClassName = value; break; default : throw new RuntimeException("[database]:〈property〉unknown name"); } } Class.forName(driverClassName); Connection connection = null; try { 〃建立数据库链接 connection = DriverManager.getConnection(url, username, password); } catch (SQLException e) { / TODO Auto-genera ted catch block e.printStackTrace(); } return connection; } 〃获取property属性的值如果有value值则读取没有设置value,则读取内容 private String getValue(Element node) { return node.hasContent() ? node.getText() : node.attributeValue("value"); } @SuppressWarnings("rawtypes") public MapperBean readMapper(String path){ MapperBean mapper = new MapperBean(); try{ InputStream stream = loader.getResourceAsStream(path); SAXReader reader = new SAXReader(); Document document = reader.read(stream); Element root = document.getRootElement(); mapper.setInterfaceName(root.attributeValue("nameSpace").trim()); /^mapper 节点的nameSpace 值存为接口名 List list = new ArrayList(); 〃用来存储方法的List  for(Iterator rootIter = root.elementIterator();rootIter.hasNext();) //遍历根节点下所有子节点 Function fun = new Function。; //用来存储一条方法的信息 Element e = (Element) rootIter.next(); String sqltype = e.getName().trim(); String funcName = e.attributeVa山e("id").trim(); String sql = e.getText().trim(); String resultType = e.attributeVa山e("resultType").trim(); fun.setSqltype(sqltype); fun.setFuncName(funcName); Object newInstance=null; try { newInstance = Class.forName(resultType).newInstance(); } catch (InstantiationException e1) { e1.printStackTrace(); } catch (IllegalAccessException e1) { e1.printStackTrace(); } catch (ClassNotFoundException e1) { e1.printStackTrace(); } fun.setResultType(newInstance); fun.setSql(sql); list.add(fun); } mapper.setList(list); } catch (DocumentException e) { e.printStackTrace(); } return mapper; } } 用面向对象的思想设计读取xml配置后: package com.liugh.config; import java.util.List; public class MapperBean { private String interfaceName; 〃接口名 private List list; 〃接口下所有方法 〃省略get set方法… } Function对象包括sql的类型、方法名、sql语句、返回类型和参数类型。 package com.liugh.config; public class Function { private String sqltype; private String funcName; private String sql; private Object resultType; private String parameterType; //省略get set方法 } 接下来实现我们的MySqlSession,首先的成员变量里得有Excutor和MyConfiguration,代码的精髓就在getMapper的方法里 package com.liugh.sqlSession; import java.lang.reflect. Proxy; public class MySqlsession { private Excutor excutor= new MyExcutor(); private MyConfiguration myConfiguration = new MyConfiguration(); public T selectOne(String statement,Object parameter){ return excutor.query(statement, parameter); } @SuppressWarnings(" unchecked") public T getMapper(Class clas){ 〃动态代理调用 return (T)Proxy.newProxyInstance(clas.getClassLoader(),new Class[]{clas}, new MyMapperProxy(myConfiguration,this)); } } 紧接着创建Excutor和实现类: package com.liugh.sqlSession; public interface Excutor { public T query(String statement,Object parameter); } MyExcutor中封装yJDBC的操作: public class MyExcutor implements Excutor{ private MyConfiguration xmlConfiguration = new MyConfiguration(); @Override public T query(String sql, Object parameter) { Connection connection=getConnection(); ResultSet set =null; PreparedStatement pre =null; try { pre = connection.prepareStatement(sql); “设置参数 pre.setString(1, parameter.toString()); set = pre.executeQuery(); User u=new User(); /遍历结果集 while(set.next()){ u.setId(set.getString(1)); u.setUsername(set.getString(2)); u.setPassword(set.getString(3)); } return (T) u; } catch (SQLException e) { e.printStackT race(); } finally{ try { if(set!=null){ set.close(); }if(pre!=null){ pre.close(); }if (connection!=null){ connection.close(); } }catch(Exception e2){ e2.printStackTrace(); } } return null; } private Connection getConnection() { try { Connection connection =xmlConfiguration.build("config.xml"); return connection; } catch (Exception e) { e.printStackT race(); } return null; } } MyMapperProxy代理类完成xml方法和真实方法对应,执行查询:  public class MyMapperProxy implements I nvocation Handler{ private MySqlsession mySqlsession; private MyConfiguration myConfiguration; public MyMapperProxy(MyConfiguration myConfiguration,MySqlsession mySqlsession) { this.myConfiguration=myConfiguration; this.mySqlsession=mySqlsession; } @Override public Object invoke(Object proxy, Method method, Object口 args) throws Throwable { MapperBean readMapper = myConfiguration.readMapper("UserMapper.xml"); //是否是xmi文件对应的接口 if(!method.getDeclaringClass().getName().equals(readMapper.getInterfaceName())){ return null; } List list = readMapper.getList(); if (null != list || 0 != list.size()){ for (Function function : list) { //id是否和接口方法名一样 if(method.getName().equals(function.getFuncName())){ return mySqlsession.selectOne(function.getSql(), String. valueOf(args[0])); } } } return null; } } 到这里,就完成了自己的Mybatis框架,我们测试一下: public class TestMybatis { public static void main(String口 args) { MySqlsession sqlsession=new MySqlsession。; UserMapper mapper = sqlsession.getMapper(UserMapper.class); User user = mapper.getUserById("1"); System.out.println(user); } } 执行结果: ■19* public static void main(String[] args) { MySqlsession sqlsession=new MySqlsession(); ■21 UserMapper mapper = sqlsession.getMapper(UserMapper.class); User user = mapper.getUserByld(nln); System.out.println(user); |24 } 25 26 } 27 R Problems

喜欢文章,点个在看
阅读原文
声明:pdf仅供学习使用,一切版权归原创公众号所有;建议持续关注原创公众号获取最新文章,学习愉快! 
从零搭建一个Spring Boot开发环境! Spring Boot+Mybatis+Swagger2环境搭建
calebman Java后端 2019-11-15

优质文章,及时送达
作者 | calebman
链接 | www.jianshu.com/p/95946d6b0c7d
本文简介
• 为什么使用Spring Boot
• 搭建怎样一个环境
• 开发环境
• 导入快速启动项目
• 集成前准备
• 集成Mybatis
• 集成Swagger2
• 多环境配置
• 多环境下的日志配置
• 常用配置
为什么使用Spring Boot
Spring Boot相对于传统的SSM框架的优点是提供了默认的样板化配置,简化了Spring应用的初始搭建过程,如果你不想被众多 的xml配置文件困扰,可以考虑使用Spring Boot替代
搭建怎样一个环境
本文将基于Spring官方提供的快速启动项目模板集成Mybatis、Swagger2框架,并讲解mybatis generator一键生成代码插件、 logback、一键生成文档以及多环境的配置方法,最后再介绍一下自定义配置的注解获取、全局异常处理等经常用到的东西。
开发环境
本人使用IDEA作为开发工具,IDEA下载时默认集成了SpringBoot的快速启动项目可以直接创建,如果使用Eclipse的同学可以考 虑安装SpringBoot插件或者直接从这里配置并下载SpringBoot快速启动项目,需要注意的是本次环境搭建选择的是 SpringBoot2.0的快速启动框架,Spring Boot2.0要求jdk版本必须要在1.8及以上。
导入快速启动项目
不管是由IDEA导入还是现实下载模板工程都需要初始化快速启动工程的配置,如果使用IDEA,在新建项目时选择Spring
Initializr,主要配置如下图
IDEA新建SpringBoot项目-填写项目/包名

IDEA新建SpringBoot项目-选择依赖包
点击next之后finish之后IDEA显示正在下载模板工程,下载完成后会根据pom.xml下载包依赖,依赖下载完毕后模板项目就算创 建成功了,如果是直接从官方网站配置下载快速启动项目可参考下图配置
SPRING INITIALIZR
Generate a ,with J2
Project Metadata
Artrf«ct coordinates
Group
co*, cprutf. 4MO
Artifact
spxu>("boom aple

start spring io s powwvd by
直接下载SpringBoot快速启动项目-项目配置
从Search for dependencies框中输入并选择Web、Mysql、Mybatis加入依赖,点击66件46 Project下载快速启动项目,然 后在IDE中选择导入Maven项目,项目导入完成后可见其目录结构如下图
v spring-boot-example D:\CJH\idea-chee-workspace\spring-boot-example > .mvn
v . src ▼ main
2 java y com. spring.demo.springbootexample
SpringBootExampleApplication
v resources static
、]application.properties v* java
▼ com.spring.demo.springbootexample
c SpringBootExampleApplicationTests m .gitignore =mvnw =mvnw.cmd
I m pom.xml I
快速启动项目-项目结构
需要关注红色方框圈起来的部分,由上往下第一个java类是用来启动项目的入口函数,第二个properties后缀的文件是项目的配 置文件,第三个是项目的依赖包以及执行插件的配置
集成前准备
修改.properties为.yml
yml相对于properties更加精简而且很多官方给出的Demo都是yml的配置形式,在这里我们采用yml的形式代替properties,相 对于properties形式主要有以下两点不同
1 .对于键的描述由原有的"."分割变成了树的形状
2 .对于所有的键的后面一个要跟一个空格,不然启动项目会报配置解析错误

properties式语法描述

spring.datasource.name = mysql
spring.datasource.url = jdbc:mysql://localhost:3306/db?characterEncoding=utf-8
spring.datasource.username = root
spring.datasource.password = 123

yml式语法描述

spring:
datasource:
name: mysql
url: jdbc:mysql://localhost:3306/db?characterEncoding=utf-8
username: root
password: 123
配置所需依赖
快速启动项目创建成功后我们观察其pom.xml文件中的依赖如下图,包含了我们选择的Web、Mybatis以及Mysql 

org.springframework.boot
spring-boot-starter-web


org.mybatis.spring.boot
mybatis-spring-boot-starter
1.3.1


mysql
mysql-connector-java
runtime


org.springframework.boot
spring-boot-starter-test
test

但是我们使用ORM框架一般还会配合数据库连接池以及分页插件来使用,在这里我选择了阿里的druid以及pagehelper这个分页 插件,再加上我们还需要整合swagger2文档自动化构建框架,所以增加了以下四个依赖项

com.github.pagehelper
pagehelper-spring-boot-starter
1.2.3


com.alibaba
druid-spring-boot-starter
1.1.1


com.alibaba
fastjson
1.2.31


io.springfox
springfox-swagger2
2.5.0

集成Mybatis
Mybatis的配置主要包括了druid数据库连接池、pagehelper分页插件、mybatis-generator代码逆向生成插件以及mapper、 pojo扫描配置
配置druid数据库连接池 添加以下配置至application.yml文件中 
spring:
datasource:

如果存在多个数据源,监控的时候可以通过名字来区分开来 name: mysql

连接数据库的url

url: jdbc:mysql //[oca[host3306/db?characterEncoding=utf-8

连接数据库的账号

username: root

连接数据库的密码

password: 123

使用druid数据源

type: com.alibaba.druid.pool.DruidDataSource

扩展插件

监控统计用的filter:stat日志用的filter:log4j防御sql注入的filter:wall

filters: stat

最大连接池数量

maxActive: 20

初始化时建立物理连接的个数。初始化发生在显示调用init方法,或者第一次getConnection时 initialsize: 1

获取连接时最大等待时间,单位毫秒

maxWait: 60000

最小连接池数量

minldle: 1
timeBetweenEvictionRunsMillis: 60000

连接保持空闲而不被驱逐的最长时间

minEvictableldleTimeMillis: 300000

用来检测连接是否有效的sql,要求是一个查询语句

如果validationQuery为null, testOnBorrow、testOnReturn、testWhileldle都不会其作用 validationQuery: select count(1) from ‘table’

申请连接的时候检测,如果空闲时间大于timeBetweenEvictionRunsMillis,执行validationQuery检测连接是否有效 testWhileldle: true

申请连接时执行validationQuery检测连接是否有效,做了这个配置会降低性能 testOnBorrow: false

归还连接时执行validationQuery检测连接是否有效,做了这个配置会降低性能

testOnReturn: false

是否缓存preparedStatement,即PSCache

poolPreparedStatements: false

要启用PSCache,必须配置大于0,当大于0时,poolPreparedStatements自动触发修改为true maxOpenPreparedStatements: -1

配置pagehelper分页插件

pagehelper分页插件

pagehelper:
#数据库的方言
helperDialect: mysql
#启用合理化,如果pageNum < 1会查询第一页,如果pageNum > pages会查询最后一页
reasonable: true
代码逆向生成插件mybatis-generator的配置及运行
mybatis-generator插件的使用主要分为以下三步
1 . pom.xml中添加mybatis-generator插件 



org.springframework.boot
spring-boot-maven-p山gin


org.mybatis.generator
mybatis-generator-maven-plugin
1.3.2
〈configuration)

${basedir}/src/main/resources/generator/generatorConfig.xml

true
true
〈/configuration)



2 .创建逆向代码生成配置文件generatorConfig.xml
参照pom.xml插件配置中的扫描位置,在resources目录下创建generator文件夹,在新建的文件夹中创建generatorConfig.xml 配置文件,文件的详细配置信息如下

<?xml version="1.0" encoding="UTF-8"?> 〈properties resource="generator/generator.properties"/> 〈context id="DB2Tables" targetRuntime="MyBatis3"> 〈property name="forceBigDecimals" value="false"/> 〈javaModelGenerator targetPackage="${pojoTargetPackage}" targetProject="src/main/java"> 〈property name="enableSubPackages" value="true"/> 〈/javaModelGenerator> 〈sqlMapGenerator targetPackage="${mapperTargetPackage}" targetProject="src/main/resources"> 〈/sqlMapGenerator> 〈javaClientGenerator type="XMLMAPPER" targetPackage="${daoTargetPackage}" targetProject="src/main/java"> 〈/javaClientGenerator> 〈/generatorConfiguration> 为了将generatorConfig.xml配置模板化,在这里将变动性较大的配置项单独提取出来作为一个generatorConfig.xml的配置文 件,然后通过properties标签读取此文件的配置,这样做的好处是当需要多处复用此乂巾怕时只需要关注少量的配置项。 在generatorConfig.xml同级创建generator.properties文件,现只需要配置generator.properties文件即可,配置内容如下 #请手动配置以下选项 #数据库驱动:选择你的本地硬盘上面的数据库驱动包 classPathEntry = D:/CJH/maven-repository/mysql/mysql-connector-java/5.1.30/mysql-connector-java-5.1.30.jar #数据库名称、用户名、密码 db = db userid = root password = 123 #生成pojo的包名位置在src/main/ava目录下 pojoTargetPackage = com.spring.demo.springbootexample.mybatis.po #生成DA O的包名位置在src/main/java目录下 daoTargetPackage = com.spring.demo.springbootexample.mybatis.mapper #生成Mapper的包名位置位于src/main/resources目录下 mapperTargetPackage = mapper 3 .运行mybatis-generator插件生成Dao、Model、Mapping #打开命令行cd到项目pom.xml同级目录运行以下命令 mvn mybatis-generator:generate -e  mybatis扫描包配置 至此已经生成了指定数据库对应的实体、映射类,但是还不能直接使用,需要配置mybatis扫描地址后才能正常调用 1.在3口口1位3甘0爪丫巾1配置mapper.xml以及pojo的包地址 mybatis: # mapper.xml包地址 mapper-locations: classpath:mapper/*.xml # pojo生成包地址 type-aliases-package: com .spring.demo.springbootexample.mybatis.po 2.在SpringBootExampleApplication.java中开启Mapper扫描注解 @SpringBootApplication @MapperScan("com.spring.demo.springbootexample.mybatis.mapper") public class SpringBootExampleApplication { public static void main(String口 args) { SpringApplication.run(SpringBootExampleApplication.class, args); } } 测试mapper的有效性 @Controller public class Testcontroller { @Autowired UserMapper userMapper; @RequestMapping("/test") @ResponseBody public Object test(){ return userMapper.selectByExample(null); } } 启动SpringBootExampleApplication.java的巾3而函数,如果没有在application.yml特意配置server.port那么springboot会 采用默认的8080端口运行,运行成功将打印如下日志 Tomcat started on port(s): 8080 (http) with context path '' 在浏览器输入地址如果返回表格的中的所有数据代表mybatis集成成功 http://localhost:8080/test 集成Swagger2 Swagger2是一个文档快速构建工具,能够通过注解自动生成一个Restful风格json形式的接口文档,并可以通过如swagger-ui等 工具生成html网页形式的接口文档,swagger2的集成比较简单,使用需要稍微熟悉一下,集成、注解与使用分如下四步 1 .建立SwaggerConfig文件  @Configuration public class SwaggerConfig { private final String version = "1.0"; private final String title = "SpringBoot示例工程”; private final String description = "API文档自动生成示例"; private final String termsOfServiceUrl = "http://www.kingeid.com"; private final String license = "MIT"; private final String licenseUrl = "https://mit-license.org/"; private final Contact contact = new Contact("calebman", "https://github.com/calebman", "chenjianhui0428@gmail.com"); @Bean public Docket buildDocket() { return new Docket(DocumentationType.SWAGGER_2).apiInfo(buildApiInf()) .select().build(); } private ApiInfo buildApiInf() { return new ApiInfoBuilder().title(title).termsOfServiceUrl(termsOfServiceUrl).description(description) .version(version).license(license).licenseUrl(licenseUrl).contact(contact).build(); } } 2 .在SpringBootExampleApplication.java中启用Swagger2注角星 在@SpringBootApplication注解下面加上@£口35后5川3886『2注解 3 .常用注解示例 @Controller @RequestMapping("/v1/product") @Api(va山e = "DocController", tags = {"restful api 示例"}) public class DocController extends BaseController { @RequestMapping(value = "/{id}", method = RequestMethod.PUT) @ResponseBody @ApiOperation(value ="修改指定产品",httpMethod = "PUT", produces = "application/json") @ApiImplicitParams({@ApiImplicitParam(name = "id", value ="产品ID", required = true, paramType = "path")}) public WebResult update(@PathVariable("id") Integer id, @ModelAttribute Product product) { logger.debug("修改指定产品接收产品id与产品信息=>%~,{}", id, product); if (id == null || "".equals(id)) { logger.debug("产品id不能为空"); return WebResult.error(ERRORDetail.RC_0101001); } return WebResult .success。; } } @ApiModel(value ="产品信息") public class Product { @ApiModelProperty(required = true, name = "name", value ="产品名称",dataType = "query") private String name; @ApiModelProperty(name = "type", value ="产品类型",dataType = "query") private String type; } 4 .生成json形式的文档 集成成功后启动项目控制台会打印级别为INFO的日志,截取部分如下,表明可通过访问应用的v2/api-docs接口得到文档api的 json格式数据,可在浏览器输入指定地址验证集成是否成功 Mapped "{[/v2/api-docs],methods=[GET],produces=[application/json || application/hal+json]}" http://localhost:8080/v2/api-docs 多环境配置 应用研发过程中多环境是不可避免的,假设我们现在有开发、演示、生产三个不同的环境其配置也不同,如果每次都在打包环节 来进行配置难免出错,SpringBoot支持通过命令启动不同的环境,但是配置文件需要满足application-{profile}.properties的格 式,profile代表对应环境的标识,加载时可通过不同命令加载不同环境。 application-dev.properties:开发环境 application-test.properties:演示环境 application-prod.properties:生产环境 #运行演示环境命令 java -jar spring-boot-example-0.0.1-SNAPSHOT --spring.profiles.active=test 基于现在的项目实现多环境我们需要在application.yml同级目录新建application-dev.yml、application-test.yml、 application-prod.yml三个不同环境的配置文件,将不变的公有配置如druid的大部分、pagehelper分页插件以及mybatis包扫 描配置放置于application.yml中,并在application.yml中配置默认采用开发环境,那么如果不带--spring.profiles.active启动 应用就默认为开发环境启动,变动较大的配置如数据库的账号密码分别写入不同环境的配置文件中  spring: profiles: #默认使用开发环境 active: dev 配置到这里我们的项目目录结构如下图所示 ▼ java v com.spring.demo.springbootexample > L' beans v [ config . c Config . c SwaggerConfig v controller c DocController c TestController 7 1 - mybatis v mapper i UserMapper 7 PO c User c UserExample 》 result & SpringBootExampleApplication src/main/java 目录结构 7,resources v t generator 11 generator, pro perties generatorConfig.xml v mapper M UserMapper.xml static templates 崛 application.yml 4 application-dev.yml 用 application-prod.yml ⑨ application-testyml src/main/resources目录结构 至此我们分别完成yMybatis、Swagger2以及多环境的集成,接下来我们配置多环境下的logger。对于logger我们总是希望在 项目研发过程中越多越好,能够给予足够的信息定位bug,项目处于演示或者上线状态时为了不让日志打印影响程序性能我们只 需要警告或者错误的日志,并且需要写入文件,那么接下来就基于logback实现多环境下的日志配置 多环境下的日志配置 仓建logback-spring.xml在application.yml的同级目录,springboot推荐使用logback-spring.xml而不是logback.xml文 件,logback-spring.xml的配置内容如下所示 <?xml version="1.0" encoding="UTF-8"?> 〈configuration scan="true" scanPeriod="60 seconds" debug="false"> 〈property name="maxsize" value="30MB" /> 〈property name="maxdays" value="90" />  

优质文章,及时送达
作者 | smile_lg
链接 | blog.csdn.net/smile_lg/article/details/71215619
用来循环容器的标签forEach,查看例子
foreach元素的属性主要有item, index, collection, open, separator, close。
■ item:集合中元素迭代时的别名,
■ index:集合中元素迭代时的索引
■ open:常用语where语句中,表示以什么开始,比如以’(‘开始
■ separator:表示在每次进行迭代时的分隔符,
■ close常用语where语句中,表示以什么结束,
在使用foreach的时候最关键的也是最容易出错的就是collection属性,该属性是必须指定的,但是在不同情况下,该属性的值是 不一样的,主要有一下3种情况:
■ 如果传入的是单参数且参数类型是一个List的时候,collection属性值为list.
■ 如果传入的是单参数且参数类型是一个array数组的时候,collection的属性值为array .
■ 如果传入的参数是多个的时候,我们就需要把它们封装成一个Map了,当然单参数也可以封装成map,实际上如果你在传 入参数的时候,在MyBatis里面也是会把它封装成一个Map的,map的key就是参数名,所以这个时候collection属性值就 是传入的List或array对象在自己封装的map里面的key.
针对最后一条,我们来看一下官方说法:
注意你可以将一个List实例或者数组作为参数对象传给MyBatis,当你这么做的时候,MyBatis会自动将它包装在一个 Map中并以名称为键。List实例将会以“list”作为键,而数组实例的键将是“array”。
所以,不管是多参数还是单参数的list,array类型,都可以封装为map进行传递。如果传递的是一个List,则mybatis会封装为一 个list为key,list值为object的map,如果是array,则封装成一^个array为key, array的值为object的map,如果自己封装呢, 则colloection里放的是自己封装的map里的key值
〃mapperp我们要为这个方法传递的是一个容器将容器中的元素一个一个的
〃拼接到xml的方法中就要使用这个OrEach这个标签了
public List queryById(List userids);
〃对应的xml中如下
〈select id=“queryByld” resultMap=“BaseResEMap” >
select * FROM entity where id in
<foreach collection=“userids” item=“userid” index=“index” open=“(“separator=”,“close=”)”>
#{userid}


8慎,1模糊查询
〃比如说我们想要进行条件查询,但是几个条件不是每次都要使用,那么我们就可以
〃通过判断是否拼接到sql中
〈select id=“queryByld” resultMap=“BascResultMap” parameterType=“entity”>
SELECT * from entity


name like concat(’%‘,concat(#{name},’%‘))



choose (when, otherwise)标签
choose标签是按顺序判断其内部when标签中的test条件出否成立,如果有一个成立,则choose结束。当choose中所有when 的条件都不满则时,则执行otherwise中的sql。类似于Java的switch语句,choose为switch, when为case, otherwise 则为 default。
例如下面例子,同样把所有可以限制的条件都写上,方面使用。choose会从上到下选择一个when标签的test为true的sql执行。 安全考虑,我们使用where将choose包起来,放置关键字多于错误。
! $0052判」断参数)-按顺序将实体类User第一个不为空的属性作为:where条件–>
〈select id=“getUserList_choose” resultMap=“resultMap_user” parameterType=“com.yiibai.pojo.User”> SELECT *
FROM User u



u.username LIKE CONCAT(CONCAT(’%‘, #{username, jdbcType=VARCHAR}),’%')


AND u.sex = #{sex, jdbcType=INTEGER}


AND u.birthday = #{birthday, jdbcType=DATE}

〈otherwise)
〈/otherwise)



selectKey 标签
在insert语句中,在Oracle经常使用序列、在MySQL中使用函数来自动生成插入表的主键,而且需要方法能返回这个生成主键。 使用myBatis的selectKey标签可以实现这个效果。
下面例子,使用mysql数据库自定义函数nextval(‘student’),用来生成一个key,并把他设置到传入的实体类中的studentId属 性上。所以在执行完此方法后,边可以通过这个实体类获取生成的key。
<–插入学生自动主键一〉
〈insert id=“createStudentAutoKey” parameterType=“liming.student.manager.data.model.StudentEntity” keyProperty=“studentId”>

select nextval(‘student’)

INSERT INTO STUDENT_TBL(STUDENT_ID,
STUDENT_NAME,
STUDENT_SEX,
STUDENT_BIRTHDAY,
STUDENT_PHOTO,
CLASS_ID,
PLACE_ID)
VALUES (#{studentId},
#{studentName},
#{studentSex},
#{studentBirthday},
#{studentPhoto, javaType=byte口, jdbcType=BLOB, typeHandler=org.apache.ibatis.type.BlobTypeHandler},
#{classId},
#{placeId})

调用接口方法,和获取自动生成key
StudentEntity entity = new StudentEntity();
entity.setStudentName(“黎明你好”);
entity.setStudentSex(l);
entity.setStudentBirthday(DateUtil.parse(“1985-05-28”));
entity.setClassId(“20000001”);
entity.setPlaceId(“70000001”);
this.dynamicSqlMapper.createStudentAutoKey(entity);
System.out.println("新增学生ID: " + entity.getStudentId());
if标签
if标签可用在许多类型的sql语句中,我们以查询为例。首先看一个很普通的查询:

SELECT * from STUDENT_TBL ST WHERE ST.STUDENT_NAME LIKE CONCAT(CONCAT('%', #{studentName}),'%') 但是此时如果studentName为null,此语句很可能报错或查询结果为空。此时我们使用if动态sql语句先进行判断,如果值为null 或等于空字符串,我们就不进行此条件的判断,增加灵活性。 参数为实体类StudentEntity。将实体类中所有的属性均进行判断,如果不为空则执行判断条件。  〈select id="getStudentList_if" resultMap="resultMap_studentEntity" parameterType="liming.student.manager.data.model.StudentEntit SELECT ST.STUDENT_ID, ST.STUDENT_NAME, ST.STUDENT_SEX, ST.STUDENT_BIRTHDAY, ST.STUDENT_PHOTO, ST.CLASS_ID, ST.PLACE_ID FROM STUDENT_TBL ST WHERE ST.STUDENT_NAME LIKE CONCAT(CONCAT('%', #{studentName,jdbcType=VARCHAR},%) AND ST.STUDENT_SEX = #{studentSex, jdbcType=INTEGER} AND ST.STUDENT_BIRTHDAY = #{studnBithday, jdbcType二DATE} AND ST.CLASS_ID = {{classId, jdbcType= VARCHAR} AND ST.CLASS_ID = #{dassEntity.classld, jdbcType= VARCHAR} AND ST.PLACE_ID = {{placeld, jdbcType 二 VARCHAR} AND ST.PLACE_ID = #{placeEnttyplaceld, jdbcType 二 VARCHAR} AND ST.STUDENT_ID = #{studentd, jdbcType= VARCHAR} <[ I 使用时比较灵活,new一个这样的实体类,我们需要限制那个条件,只需要附上相应的值就会where这个条件,相反不去赋值就 可以不在where中判断。 public void select_test_2_1() { StudentEntity entity = new StudentEntity(); entity.setStudentName(""); entity.setStudentSex(l); entity.setStudentBirthday(DateUtil.parse("1985-05-28")); entity.setClassId("20000001"); /entty.setPlacefd(,70000001"); List list = this.dynamicSqlMapper.getStudentList_if(entity); for (StudentEntity e : list) { System.out.println(e.toString()); } } if+where的条件判断 当where中的条件使用的if标签较多时,这样的组合可能会导致错误。我们以在3.1中的查询语句为例子,当java代码按如下方法 调用时:  @Test public void select_test_2_1() { StudentEntity entity = new StudentEntity(); entity.setStudentName(null); entity.setStudentSex(l); List list = this.dynamicSqlMapper.getStudentList_if(entity); for (StudentEntity e : list) { System.out.println(e.toString()); } } 如果上面例子,参数studentName为null,将不会进行STUDENT_NAME列的判断,则会直接导“WHERE AND”关键字多余的 错误SQL。 这时我们可以使用where动态语句来解决。这个“where”标签会知道如果它包含的标签中有返回值的话,它就插入一 个‘where’。此外,如果标签返回的内容是以AND或OR开头的,则它会剔除掉。 上面例子修改为: 〈select id="getStudentList_whereIf" resultMap="resultMap_studentEntity" parameterType="liming.student.manager.data.model.Studen SELECT ST.STUDENT_ID, ST.STUDENT_NAME, ST.STUDENT_SEX, ST.STUDENT_BIRTHDAY, ST.STUDENT_PHOTO, ST.CLASS_ID, ST.PLACE_ID FROM STUDENT_TBL ST ST.STUDENT_NAME LIKE CONCAT(CONCAT('%', #{studenName,jdbcType=VARCHAR},%) AND ST.STUDENT_SEX = #{studentSex, jdbcType=INTEGER} AND ST.STUDENT_BIRTHDAY = #{studenBithday, jdbcType二DATE} AND ST.CLASS_ID = {{classId, jdbcType= VARCHAR} AND ST.CLASS_ID = #{dassEntity.classld, jdbcType= VARCHAR} AND ST.PLACE_ID = {{placeld, jdbcType 二 VARCHAR} AND ST.PLACE_ID = #{placeEnttyplaceld, jdbcType 二 VARCHAR} AND ST.STUDENT_ID = #{studentd, jdbcType= VARCHAR} if+set实现修改语句  当update语句中没有使用if标签时,如果有一个参数为“31,都会导致错误。 当在update语句中使用if标签时,如果前面的if没有执行,则或导致逗号多余错误。使用set标签可以将动态的配置SET关键字, 和剔除追加到条件末尾的任何不相关的逗号。使用if+set标签修改后,如果某项为null则不进行更新,而是保持数据库原值。 如下示例: 〈update id="updateStudent_if_set" parameterType="liming.student.manager.data.model.StudentEntity"> UPDATESTUDENT_TBL STUDENT_TBL.STUDENT_NAME = #{sUdntName} STUDENT_TBL.STUDENT_SEX = #{studenSex} STUDENT_TBL.STUDENT_BIRTHDAY = #{studentBirthday} STUDENT_TBL.STUDENT_PHOTO = #{studentPhoto, javaType=byte] jdbcTypeBLOB, typeHandler=org.apache.ibatis.type.BlobTyp STUDENT_TBL.CLASS_ID = {{clsssld} STUDENT_TBL.PLACE_ID = #paceld} WHERE STUDENT_TBL.STUDENT_ID = #{studntd}; if+trim代替where/set标签 trim是更灵活的去处多余关键字的标签,他可以实践where和set的效果。 trim代替where 〈select id="getStudentList_if_trim" resultMap="resultMap_studentEntity"> SELECT ST.STUDENT_ID, ST.STUDENT_NAME, ST.STUDENT_SEX, ST.STUDENT_BIRTHDAY, ST.STUDENT_PHOTO, ST.CLASS_ID, ST.PLACE_ID FROM STUDENT_TBL ST 〈trim prefix="WHERE" prefixOverrides="AND|OR"> ST.STUDENT_NAME LIKE CONCAT(CONCAT('%', #{sUdntName, jdbcType= VARCHAR},%) AND ST.STUDENT_SEX = #{studentSex, jdbcType=INTEGER} AND ST.STUDENT_BIRTHDAY = #{studnBithday, jdbcType二DATE} AND ST.CLASS_ID = {{classId, jdbcType= VARCHAR} AND ST.CLASS_ID = {{dassEntityclassd, jdbcType= VARCHAR} AND ST.PLACE_ID = {{placed, jdbcType 二 VARCHAR} AND ST.PLACE_ID = {{placeEntitypaced, jdbcType 二 VARCHAR} AND ST.STUDENT_ID = {{studentd, jdbcType= VARCHAR} trim代替set 〈update id="updateStudent_if_trim" parameterType="liming.student.manager.data.model.StudentEntity"> UPDATESTUDENT_TBL 〈trim prefix="SET" suffixOverrides=","> STUDENT_TBL.STUDENT_NAME = #{sUdntName} STUDENT_TBL.STUDENT_SEX = #{studenSex} STUDENT_TBL.STUDENT_BIRTHDAY = #{studentBirthday} STUDENT_TBL.STUDENT_PHOTO = #{studentPhoto, javaType=byte] jdbcTypeBLOB, typeHandler=org.apache.ibatis.type.BlobTyp STUDENT_TBL.CLASS_ID = {{clsssld}, STUDENT_TBL.PLACE_ID = #paceld} WHERE STUDENT_TBL.STUDENT_ID = #{studntd} 8) E foreach 对于动态SQL非常必须的,主是要迭代一个集合,通常是用于IN条件。List实例将使用“list”做为键,数组实例以“array”做 为键。 foreach元素是非常强大的,它允许你指定一个集合,声明集合项和索引变量,它们可以用在元素体内。它也允许你指定开放和 关闭的字符串,在迭代之间放置分隔符。这个元素是很智能的,它不会偶然地附加多余的分隔符。 注意:你可以传递一个List实例或者数组作为参数对象传给MyBatis。当你这么做的时候,MyBatis会自动将它包装在一个 Map中,用名称在作为键。List实例将会以“list”作为键,而数组实例将会以“array”作为键。 这个部分是对关于XML配置文件和XML映射文件的而讨论的。下一部分将详细讨论Java API,所以你可以得到你已经创建的最有 效的映射。 1.参数为array示例的写法 接口的方法声明: public List getStudentListByClassIds_foreach_array(String口 classIds); 动态SQL语句:  <—71匕侔a匕仪循环3门己丫参数)-作为where中in的条件--> 〈select id="getStudentListByClassIds_foreach_array" resultMap="resultMap_studentEntity"> SELECT ST.STUDENT_ID, ST.STUDENT_NAME, ST.STUDENT_SEX, ST.STUDENT_BIRTHDAY, ST.STUDENT_PHOTO, ST.CLASS_ID, ST.PLACE_ID FROM STUDENT_TBL ST WHERE ST.CLASS_ID IN

喜欢文章,点个在看:::
声明:pdf仅供学习使用,一切版权归原创公众号所有;建议持续关注原创公众号获取最新文章,学习愉快! 
腊月二十八,聊聊MyBatis中的设计模式
crazyant Java后端 1月22日

作者 | crazyant
链接 | www.crazyant.net/2022.html
虽然我们都知道有26个设计模式,但是大多停留在概念层面,真实开发中很少遇到,Mybatis源码中使用了大量的设计模式,阅 读源码并观察设计模式在其中的应用,能够更深入的理解设计模式。
Mybatis至少遇到了以下的设计模式的使用:
1 . Builder模式,例如SqlSessionFactoryBuilder、XMLConfigBuilder、XMLMapperBuilder、XMLStatementBuilder、
CacheBuilder;
2 .工厂模式,例如541565510口尸3壮0»、ObjectFactory、MapperProxyFactory;
3 .单例模式,例如ErrorContext和LogFactory;
4 .代理模式,Mybatis实现的核心,比如MapperProxy、ConnectionLogger,用的jdk的动态代理;还有executor.loader包使 用了cglib或者javassist达到延迟加载的效果;
5 .组合模式,例如SqlNode和各个子类ChooseSqlNode等;
6 .模板方法模式,例如BaseExecutor和SimpleExecutor,还有BaseTypeHandler和所有的子类例如IntegerTypeHandler;
7 .适配器模式,例如1。8的Mybatis接口和它对jdbc、log4j等各种日志框架的适配实现;
8 .装饰者模式,例如Cache包中的cache.decorators子包中等各个装饰者的实现;
9 .迭代器模式,例如迭代器模式PropertyTokenizer;
接下来挨个模式进行解读,先介绍模式自身的知识,然后解读在Mybatis中怎样应用了该模式。
1、8536「模式
Builder模式的定义是“将一个复杂对象的构建与它的表示分离,使得同样的构建过程可以创建不同的表示。”,它属于创建类模 式,一般来说,如果一个对象的构建比较复杂,超出了构造函数所能包含的范围,就可以使用工厂模式和Builder模式,相对于工 厂模式会产出一个完整的产品,Builder应用于更加复杂的对象的构建,甚至只会构建产品的一个部分。

在Mybatis环境的初始化过程中,SqlSessionFactoryBuilder会调用XMLConfigBuilder读取所有的MybatisMapConfig.xm^Mk 有的Mapper.xml文件,构建Mybatis运行的核心对象Configuration对象,然后将该Configuration对象作为参数构建一个 SqlSessionFactory对象。
其中乂乂1加口施8饰96『在构建Configuration对象时,也会调用XMLMapperBuilder用于读取
Mapper文件,而 XMLMapperBuilder会使用XMLStatementBuilder来读取和build所有的SQL语句。
在这个过程中,有一个相似的特点,就是这些Builder会读取文件或者配置,然后做大量的XpathParser解析、配置或语法的解 析、反射生成对象、存入结果缓存等步骤,这么多的工作都不是一个构造函数所能包括的,因此大量采用yBuilder模式来解决。
对于builder的具体类,方法都大都用build*开头,比如SqlSessionFactoryBuilder为例,它包含以下方法:
由 org.apache, ibatis.session
▼。口 > SqlSessionFactoryBuilder
• build(Configuration) : SqlSessionFactory
• build(lnputStream): Sq I Session Factory
• buildflnputStream, Properties): Sq I Sess ion Factory
• build(lnputStream, String): SqlSessionFactory
• build(lnputStream, String, Properties) : Sq I Sess ion Factory
• build(Reader) : SqlSessionFactory
• build(Reader, Properties): SqlSessionFactory
• buildfRcader, String): So SessionFactory
• buildfRcader, String, Properties) : SqlSessionFactory
即根据不同的输入参数来构建SqlSessionFactory这个工厂对象。
2、工厂模式
在Mybatis中比如SqlSessionFactory使用的是工厂模式,该工厂没有那么复杂的逻辑,是一个简单工厂模式。
简单工厂模式(Simple Factory Pattern):又称为静态工厂方法(Static Factory Method)模式,它属于类创建型模式。在简单工 厂模式中,可以根据参数的不同返回不同类的实例。简单工厂模式专门定义一个类来负责创建其他类的实例,被创建的实例通常 都具有共同的父类。

SqlSession可以认为是一个Mybatis工作的核心的接口,通过这个接口可以执行执行SQL语句、获取Mappers、管理事务。类似 于连接MySQL的Connection对象。
田 org.apache.ibatis.session
▼ O j > SqlSessionFactory
• " getConfigurationO : Configuration
• " openSessionO : SqlSession
• " openSession(boolean): SqlSession
• A openSession(Connection): SqlSession
• A openSession(ExecutorType) : SqlSession
• A openSession(ExecutorType, boolean) : SqlSession
• ’ openSession(ExecutorType, Connection) : SqISess on
• 4 openSession(ExecutorType, TransactionlsolationLevel) : SqlSession
• " open Session (Transaction I so lat ion Leve I): SqlSession
可以看到,该Factory的openSession方法重载了很多个,分别支持autoCommit、Executor、Transaction等参数的输入,来 构建核心的SqlSession对象。
在DefaultSqlSessionFactory的默认工厂实现里,有一个方法可以看出工厂怎么产出一个产品:
private SqlSession openSessionFromDataSource(ExecutorType execType, TransactionIsolationLevel level, boolean autoCommit) {
Transaction tx = null;
try {
final Environment environment = configuration.getEnvironment();
final TransactionFactory transactionFactory = getTransactionFactoryFromEnvironment(environment);
tx = transactionFactory.newTransaction(environment.getDataSource(), level, autoCommit);
final Executor executor = configuration.newExecutor(tx, execType);
return new DefaultSqlSession(configuration, executor, autoCommit);
} catch (Exception e) {
closeTransaction(tx); //may ha ve fetched a connection so lets cal
// cose(
throw ExceptionFactory.wrapException("Error opening session. Cause: " + e, e);
} finally {
ErrorContext.instance().reset();
}
}
这是一个openSession调用的底层方法,该方法先从configuration读取对应的环境配置,然后初始化TransactionFactory获得 一个Transaction对象,然后通过Transaction获取一个Executor对象,最后通过configuration、Executor、是否autoCommit 三个参数构建ySqlSession。
在这里其实也可以看到端倪,SqlSession的执行,其实是委托给对应的Executor来进行的。
而对于LogFactory,它的实现代码:
public final class LogFactory {
private static Constructor<? extends Log> logConstructor; private LogFactory() { // disable construction } public static Log getLog(Class<?> aClass) {
return getLog(aClass.getName());
}
这里有个特别的地方,是1。8变量的的类型是Constructor<?extendsLog》,也就是说该工厂生产的不只是一个产品,而是具有 Log公共接口的一系列产品,比如Log4jImpl、Slf4jImpl等很多具体的Log。
3、单例模式
单例模式(Singleton Pattern):单例模式确保某一个类只有一个实例,而且自行实例化并向整个系统提供这个实例,这个类称为 单例类,它提供全局访问的方法。
单例模式的要点有三个:一是某个类只能有一个实例;二是它必须自行创建这个实例;三是它必须自行向整个系统提供这个实 例。单例模式是一种对象创建型模式。单例模式又名单件模式或单态模式。 
class Singleton ) instance
A
Singleton
- instance :Singleton
if (instance == NULL)
instance = new SingletonQ; return instance;
I , + getlnstanceO :Singleton

  • singletonOperationO rvoid « constructor®
  • SingletonQ :void

在Mybatis中有两个地方用到单例模式,ErrorContext和LogFactory, 其中ErrorContext是用在每个线程范围内的单例,用于记 录该线程的执行环境错误信息,而LogFactory则是提供给整个Mybatis使用的日志工厂,用于获得针对项目配置好的日志对象。
ErrorContext的单例实现代码:
public class ErrorContext {
private static final ThreadLocal LOCAL = new ThreadLocal();
private ErrorContext () {
}
public static ErrorContext instance () {
ErrorContext context = LOCAL.get();
if (context == null) {
context = new ErrorContext。;
LOCAL.set(context);
}
return context;
}
构造函数是private修饰,具有一个static的局部instance变量和一个获取instance变量的方法,在获取实例的方法中,先判断是 否为空如果是的话就先创建,然后返回构造好的对象。
只是这里有个有趣的地方是,LOCAL的静态实例变量使用了ThreadLocal修饰,也就是说它属于每个线程各自的数据,而在 instance。方法中,先获取本线程的该实例,如果没有就创建该线程独有的ErrorContext。
4、代理模式
代理模式可以认为是Mybatis的核心使用的模式,正是由于这个模式,我们只需要编写Mapper.java接口,不需要实现,由 Mybatis后台帮我们完成具体SQL的执行。
代理模式(Proxy Pattern):给某一个对象提供一个代理,并由代理对象控制对原对象的引用。代理模式的英文叫做Proxy或 Surrogate,它是一种对象结构型模式。
代理模式包含如下角色:
• Subject:抽象主题角色
• Proxy:代理主题角色
• RealSubject:真实主题角色

这里有两个步骤,第一个是提前创建一个Proxy,第二个是使用的时候会自动请求Proxy,然后由Proxy来执行具体事务;
当我们使用Configuration的getMapper方法时,会调用mapperRegistry.getMapper方法,而该方法又会调用 mapperProxyFactory.newInstance(sqlSession)来生成一^个具体的代理:
*
@author Lasse Voss /
public class MapperProxyFactory {
private final Class mapperinterface;
private final Map<Method, MapperMethod> methodCache = new ConcurrentHashMap<Method, MapperMethod>();
public MapperProxyFactory(Class mapperinterface) {
this.mapperinterface = mapperinterface;
}
public Class getMapperInterface() { return mapperinterface;
}
public Map<Method, MapperMethod> getMethodCache () { return methodCache;
}
@SuppressWarnings(" unchecked")
protected T newInstance(MapperProxy mapperProxy) {
return (T) Proxy.newProxyinstance(mapperinterface.getClassLoader(), new Class[] { mapperinterface }, mapperProxy);
}
public T newInstance(SqlSession sqlSession) {
final MapperProxy mapperProxy = new MapperProxy(sqlSession, mapperinterface, methodCache);
return newinstance(mapperProxy);
}
}
在这里,先通过T newinstance(SqlSession sqlSession)方法会得到一^个MapperProxy对象,然后调用T
newinstance(MapperProxy mapperProxy)生成代理对象然后返回。
而查看MapperProxy的代码,可以看到如下内容:
public class MapperProxy implements InvocationHandler, Serializable {
@Override
public Object invoke(Object proxy, Method method, Object口 args) throws Throwable { try {
if (Object.class.equals(method.getDeclaringClass())) { return method.invoke(this, args);
} else if (isDefaultMethod(method)) {
return invokeDefaultMethod(proxy, method, args);
}
} catch (Throwable t) {
throw ExceptionUtil.unwrapThrowable(t);
}
final MapperMethod mapperMethod = cachedMapperMethod(method);
return mapperMethod.execute(sqlSession, args);
}
非常典型的,该MapperProxy类实现yinvocationHandler接口,并且实现了该接口的invoke方法。
通过这种方式,我们只需要编写Mapper.java接口类,当真正执行一个Mapper接口的时候,就会转发给MapperProxy.invoke方 法,而该方法则会调用后续的sqlSession.cud>executor.execute>prepareStatement等一系列方法,完成SQL的执行和返回。 
5、组合模式 组合模式组合多个对象形成树形结构以表示“整体-部分”的结构层次。
组合模式对单个对象(叶子对象)和组合对象(组合对象)具有一致性,它将对象组织到树结构中,可以用来描述整体与部分的关系。 同时它也模糊了简单元素(叶子对象)和复杂元素(容器对象)的概念,使得客户能够像处理简单元素一样来处理复杂元素,从而使客 户程序能够与复杂元素的内部结构解耦。
在使用组合模式中需要注意一点也是组合模式最关键的地方:叶子对象和组合对象实现相同的接口。这就是组合模式能够将叶子 节点和对象节点进行一致处理的原因。

Mybatis支持动态SQL的强大功能,比如下面的这个SQL:
〈update id=“update” parameterType=“org.format.dynamicproxy.mybatis.bean.User”>
UPDATE users
〈trim prefix=“SET” prefixOverrides=“,”>

name = #{name}


,age = {{age}


,birthday = #{bithday}


where id = ${id}

在这里面使用到了"1巾、[£等动态元素,可以根据条件来生成不同情况下的SQL; 在DynamicSqlSource.getBoundSql方法里,调用yrootSqlNode.apply(context)方法,apply方法是所有的动态节点都实现 的接口: 
public interface SqINode {
boolean apply(DynamicContext context);
}
对于实现该SqlSource接口的所有节点,就是整个组合模式树的各个节点:
Type hierarchy of ‘org.apache.ibatis.scripting.xmhags.SqlNode1: ▼ O SqINode - org.apache.ibatis.scripting.xmltags
ChooseSqINode - org.apacheJbatis.scripting.xmltags ForEachSqiNode - org.apache, i bat is .script ing. xm Itags IfSqINode ・ org.apache.ibatis.scripting.xmltags MixedSqINode - org.apache.ibatis.scripting.xmltags StaticTextSq’ Node org.apache.ibatis.scripting.xmltags TextSqINode org.apache. oatis scripting.xmltags TrirnSqlNode - org.apache.ibatis.scripting.xmltags
0 SetSqINode org apache.ibat s scripting.xm tags
O WhereSqINode - org.apache.ibatis.scripting.xmltags O VarDecISqINodc org apache.ibatis.scripting.xmltags
Press XT to see the supertype hierarchy
组合模式的简单之处在于,所有的子节点都是同一类节点,可以递归的向下执行,比如对于TextSqINode,因为它是最底层的叶 子节点,所以直接将对应的内容append到SQL语句中:
@Override
public boolean apply(DynamicContext context) {
GenericTokenParser parser = createParser(new BindingTokenParser(context, injectionFilter));
context.appendSql(parser.parse(text));
return true;
}
但是对于IfSqlNode,就需要先做判断,如果判断通过,仍然会调用子元素的SqINode,即contents.apply方法,实现递归的解 析。
@Override
public boolean apply(DynamicContext context) {
if (evaluator.evaluateBoolean(test, context.getBindings())) {
contents.apply(context);
return true;
}
return false;
}
6、模板方法模式
模板方法模式是所有模式中最为常见的几个模式之一,是基于继承的代码复用的基本技术。
模板方法模式需要开发抽象类和具体子类的设计师之间的协作。一个设计师负责给出一个算法的轮廓和骨架,另一些设计师则负
责给出这个算法的各个逻辑步骤。代表这些具体逻辑步骤的方法称做基本方法(primitive method);而将这些基本方法汇总起来 的方法叫做模板方法(template method),这个设计模式的名字就是从此而来。
模板类定义一个操作中的算法的骨架,而将一些步骤延迟到子类中。使得子类可以不改变一个算法的结构即可重定义该算法的某 些特定步骤。

在乂丫53也中,sqlSession的SQL执行,都是委托给Executor实现的,Executor包含以下结构:

其中的BaseExecutor就采用了模板方法模式,它实现了大部分的SQL执行逻辑,然后把以下几个方法交给子类定制化完成:
protected abstract int doUpdate(MappedStatement ms, Object parameter) throws SQLException;
protected abstract List doFlushStatements (boolean isRollback) throws SQLException;
protected abstract List doQuery(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, BoundSql boundSql) throws SQLException;
该模板方法类有几个子类的具体实现,使用了不同的策略:
• 简单SimpleExecutor:每执行一次update或select,就开启一^个Statement对象,用完立刻关闭Statement对象。(可以 是Statement或PrepareStatement对象)
• 重用ReuseExecutor:执行update或select,以sql作为卜6丫查找51316巾6口1对象,存在就使用,不存在就创建,用完后,
不关闭Statement对象,而是放置于Map<String, Statement〉内,供下一次使用。(可以是Statement或
PrepareStatement对象)
• 批量BatchExecutor:执行update (没有select, JDBC批处理不支持select),将所有sql都添加到批处理中 (addBatch()),等待统一执行(executeBatch()),它缓存了多个Statement对象,每个Statement对象都是 
addBatch()完毕后,等待逐一执行executeBatch()批处理的;BatchExecutor相当于维护了多个桶,每个桶里都装了很多 属于自己的SQL,就像苹果蓝里装了很多苹果,番茄蓝里装了很多番茄,最后,再统一倒进仓库。(可以是Statement或 PrepareStatement对象)
比如在SimpleExecutor中这样实现update方法:
@Override
public int doUpdate(MappedStatement ms, Object parameter) throws SQLException {
Statement stmt = null;
try {
Configuration configuration = ms.getConfiguration();
StatementHandler handler = configuration.newStatementHandler(this, ms, parameter, RowBounds.DEFAULT, null,
null);
stmt = prepareStatement(handler, ms.getStatementLog());
return handler.update(stmt);
} finally {
closeStatement(stmt);
}
}
7、适配器模式
适配器模式(Adapter Pattern):将一个接口转换成客户希望的另一个接口,适配器模式使接口不兼容的那些类可以一起工作, 其别名为包装器(Wrapper)。适配器模式既可以作为类结构型模式,也可以作为对象结构型模式。

在Mybatsi的logging包中,有一个Log接口:
*
@author Clinton Begin
/
public interface Log {
boolean isDebugEnabled ();
boolean isTraceEnabled();
void error(String s, Throwable e);
void error(String s);
void debug(String s);
void trace(String s);
void warn(String s);
}
该接口定义了Mybatis直接使用的日志方法,而Log接口具体由谁来实现呢? Mybatis提供了多种日志框架的实现,这些实现都匹 配这个Log接口所定义的接口方法,最终实现了所有外部曰志框架到Mybatis曰志包的适配:
:Type hierarchy of 'org.apache.ibatis.logging.Log1:
JakartaCommonsLogg:nglmpl - org.apacr s. logg i ng .com mon s
Jdk14Logginglmpl - org.apacheJbatis.loggingjdk14
Log4>2AbstractLoggcrlmpl - org.apache.ibatis.logging.Iog4j2
Log4j2lmpl - org.apache.ibatis.logging. og4j2
Log4j2Loggerlmpl - org.apacheJbatisJoggingJog4j2
Log4jlmpl - org.apache.ibatisJogging.Iog4j
NoLogginglmpI - org.apache.ibatis.logging.nologging
Slf4jl(npl - org.apache.ibatis.logging.slf4j
Slf4jLocationAwareLoggerlmpl - org.apacheJbatis.logging.slf4j
Slf4jLoggerlmpl - org.apache.ibatis.I ogging.slf4j
StdOutlmpI - org.apache.ibatis. logging.stdout
比如对于Log4jImpl的实现来说,该实现持有了org.apache.log4j.Logger的实例,然后所有的日志方法,均委托该实例来实现。
public class Log4jImpl implements Log {
private static final String FQCN = Log4jImpl.class.getName();
private Logger log;
public Log4jImpl(String clazz) { log = Logger.getLogger(clazz);
}
@Override
public boolean isDebugEnabled() {
return log.isDebugEnabled();
}
@Override
public boolean isTraceEnabled () {
return log.isTraceEnabled();
}
@Override
public void error(String s, Throwable e) { log.log(FQCN, Level.ERROR, s, e);
}
@Override
public void error(String s) { log.log(FQCN, Level.ERROR, s, null);
}
@Override
public void debug(String s) { log.log(FQCN, Level.DEBUG, s, null);
}
@Override
public void trace(String s) { log.log(FQCN, Level.TRACE, s, null);
}
@Override
public void warn (String s) { log.log(FQCN, Level.WARN, s, null);
}
}
8、装饰者模式 装饰模式(Decorator Pattern):动态地给一个对象增加一些额外的职责(Responsibility),就增加对象功能来说,装饰模式比生 成子类实现更为灵活。其别名也可以称为包装器(Wrapper),与适配器模式的别名相同,但它们适用于不同的场合。根据翻译的 不同,装饰模式也有人称之为“油漆工模式”,它是一种对象结构型模式。

在mybatis中,缓存的功能由根接口Cache (org.apache.ibatis.cache.Cache)定义。整个体系采用装饰器设计模式,数据存储 和缓存的基本功能由PerpetualCache (org.apache.ibatis.cache.impl.PerpetualCache)永久缓存实现,然后通过一系列的装 饰器来对PerpetualCache永久缓存进行缓存策略等方便的控制。如下图:

用于装饰PerpetualCache的标准装饰器共有8个(全部在org.apache.ibatis.cache.decorators包中):

  1. FifoCache:先进先出算法,缓存回收策略
  2. LoggingCache:输出缓存命中的日志信息
  3. LruCache:最近最少使用算法,缓存回收策略
  4. ScheduledCache:调度缓存,负责定时清空缓存
  5. SerializedCache:缓存序列化和反序列化存储
  6. SoftCache:基于软引用实现的缓存管理策略
  7. SynchronizedCache:同步的缓存装饰器,用于防止多线程并发访问
  8. WeakCache:基于弱引用实现的缓存管理策略
    另外,还有一个特殊的装饰器TransactionalCache:事务性的缓存
    正如大多数持久层框架一样,mybatis缓存同样分为一级缓存和二级缓存
    • 一级缓存,又叫本地缓存,是PerpetualCache类型的永久缓存,保存在执行器中(BaseExecutor),而执行器又在 SqlSession (DefaultSqlSession)中,所以一级缓存的生命周期与SqlSession是相同的。
    • 二级缓存,又叫自定义缓存,实现了Cache接口的类都可以作为二级缓存,所以可配置如encache等的第三方缓存。二级缓 存以namespace名称空间为其唯一标识,被保存在Configuration核心配置对象中。
    二级缓存对象的默认类型为PerpetualCache,如果配置的缓存是默认类型,则mybatis会根据配置自动追加一系列装饰器。
    Cache对象之间的引用顺序为:
    SynchronizedCache->LoggingCache->SerializedCache->ScheduledCache->LruCache->PerpetualCache
    9、迭代器模式
    迭代器(Iterator)模式,又叫做游标(Cursor)模式。GOF给出的定义为:提供一种方法访问一个容器(container)对象中各 个元素,而又不需暴露该对象的内部细节。

Java的Iterator就是迭代器模式的接口,只要实现了该接口,就相当于应用了迭代器模式:
• O lterator
• 0 forEachRemaining(Consumer<? super E>): void
• A hasNextO : boolean
• A next() : E
• 口 removed : void
比如Mybatis的PropertyTokenizer是property包中的重量级类,该类会被reflection包中其他的类频繁的引用到。这个类实现了 Iterator接口,在使用时经常被用到的是Iterator接口中的hasNext这个函数。 
public class PropertyTokenizer implements Iterator {
private String name;
private String indexedName;
private String index;
private String children;
public PropertyTokenizer (String fullname) {
int delim = fullname.indexOf(‘.’);
if (delim > -1) {
name = fullname.substring(0, delim);
children = fullname.substring(delim + 1);
} else {
name = fullname;
children = null;
}
indexedName = name;
delim = name.indexOf(‘[’);
if (delim > -1) {
index = name.substring(delim + 1, name.length() - 1);
name = name.substring(0, delim);
}
}
public String getName() { return name;
}
public String getIndex () { return index;
}
public String getIndexedName() {
return indexedName;
}
public String getChildren() {
return children;
}
@Override
public boolean hasNext() {
return children != null;
}
@Override
public PropertyTokenizer next() {
return new PropertyTokenizer(children);
}
@Override
public void remove () {
throw new UnsupportedOperationException(
“Remove is not supported, as it has no meaning in the context of properties.”);
}
}
可以看到,这个类传入一个字符串到构造函数,然后提供了iterator方法对解析后的子串进行遍历,是一个很常用的方法类。
参考资料:
•图说设计模式:http://design-patterns.readthedocs.io/zh_CN/latest/index.html 
•深入浅出Mybatis系列(十)一SQL执行流程分析(源码篇):http://www.cnblogs.eom/dongying/p/4142476.html
•设计模式读书笔记一-组合模式http://www.cnblogs.eom/chenssy/p/3299719.html
• Mybatis3.3.x技术内幕(四):五鼠闹东京之执行器£乂6切1。『设计原
本http://blog.csdn.net/wagcy/article/details/32963235
• mybatis缓存机制详解(一) Cache https://my.oschina.net/lixin91/blog/620068
本文地址:http://crazyant.net/2022.html,转载请注明来源。

微信扫描二维码,关注我的公众号
声明:pdf仅供学习使用,一切版权归原创公众号所有;建议持续关注原创公众号获取最新文章,学习愉快!
面试官:讲一下Mybatis初始化原理
亦山 Java后端 2019-11-23

优质文章,及时送达
作者|亦山
原文 | https://urlify.cn/zaYRJv
对于任何框架而言,在使用前都要进行一系列的初始化,MyBatis也不例外。本章将通过以下几点详细介绍MyBatis的初始 化过程。
1 . MyBatis的初始化做了什么
2 . MyBatis基于XML配置文件创建Configuration对象的过程
3 .手动加载XML配置文件创建Configuration对象完成初始化,创建并使用SqlSessionFactory对象
4 .涉及到的设计模式
一、乂丫8,也$的初始化做了什么
任何框架的初始化,无非是加载自己运行时所需要的配置信息。MyBatis的配置信息,大概包含以下信息,其高层级结构如 下:
x configuration 配置
X properties 属性
x settings 设置
X typeAliases类型命名
X typeHandlers类型处理器
X objectFactory 对象工厂
x plugins 插件
X environments 壬环境
X environment环境变量
X transactionManager 事务管理器
X dataSource 数据源
乂映射器
MyBatis的上述配置信息会配置在XML配置文件中,那么,这些信息被加载进入MyBatis内部,MyBatis是怎样维护的呢?
MyBatis采用了一个非常直白和简单的方式—使用org.apache.ibatis.session.Configuration对象作为一个所有配置 信息的容器,Configuration对象的组织结构和XML配置文件的组织结构几乎完全一样(当然,Configuration对象的功能 并不限于此,它还负责创建一些MyBatis内部使用的对象,如Executor等,这将在后续的文章中讨论)。如下图所示:

MyBatis根据初始化好Configuration信息,这时候用户就可以使用MyBatis进行数据库操作了。
可以这么说,MyBatis初始化的过程,就是创建Configuration对象的过程。
MyBatis的初始化可以有两种方式:
。基于XML配置文件:基于XML配置文件的方式是将MyBatis的所有配置信息放在XML文件中,MyBatis通过加载并 XML配置文件,将配置文信息组装成内部的Configuration对象
。基于Java API:这种方式不使用XML配置文件,需要MyBatis使用者在Java代码中,手动创建Configuration对象, 然后将配置参数set进入Configuration对象中
(PS: MyBatis具体配置信息有哪些,又分别表示什么意思,不在本文的叙述范围,读者可以参考我的《Java Persistence
withMyBatis 3 (中文版)》的第二章引导MyBatis中有详细的描述)
接下来我们将通过基于XML配置文件方式的MyBatis初始化,深入探讨MyBatis是如何通过配置文件构建Configuration 对象,并使用它的。 
二、MyBatis基于XML配置文件创建Configuration对象的过程
现在就从使用MyBatis的简单例子入手,深入分析一下MyBatis是怎样完成初始化的,都初始化了什么。看以下代码:
String resource = “mybatis-config.xml”;
Inputstream inputstream = Resources.getResourceAsStream(resource);
SqlSessionFactory SqlSessionFactory = new SqlSessionFactoryBuilder().build(inputs tream);
SqlSession sqlSession = SqlSessionFactory.openSession();
List list = sqlSession.selectList(“com.foo.bean.BlogMapper.queryAHBloglnfo”);
有过MyBatis使用经验的读者会知道,上述语句的作用是执行com.foo.bean.BlogMapper.queryAllBlogInfo 定义的 SQL语句,返回一个List结果集。总的来说,上述代码经历了巾丫53^5初始化–>创建SqlSession -->执行SQL语句返回结 果三个过程。
上述代码的功能是根据配置文件mybatis-config.xml配置文件,创建SqlSessionFactory对象,然后产生SqlSession, 执行SQL语句。而mybatis的初始化就发生在第三句:SqlSessionFactory SqlSessionFactory = new
SqlSessionFactoryBuilder().build(inputStream);现在就让我们看看第三句到底发生了什么。
乂丫83籍5初始化基本过程:
SqlSessionFactoryBuilder根据传入的数据流生成Configuration对象,然后根据Configuration对象创建默认的 SqlSessionFactory实例。
初始化的基本过程如下序列图所示:
5 . buildfconfiguration)返回
new DefaultSqlSessionFactory(config)
由上图所示,mybatis初始化要经过简单的以下几步:

  1. 调用SqlSessionFactoryBuilder对象的build(inputStream)方法;
  2. SqlSessionFactoryBuilder会根据输入流inputStream等信息创建XMLConfigBuilder对象;
  3. SqlSessionFactoryBuilder调用XMLConfigBuilder对象的parse()方法;
  4. XMLConfigBuilder对象返回Configuration对象;
  5. SqlSessionFactoryBuilder根据Configuration对象创建一^个DefaultSessionFactory对象;
  6. SqlSessionFactoryBuilder返回 DefaultSessionFactory对象给Client,供Client使用。
    SqlSessionFactoryBuilder相关的代码如下所示:
    public SqlSessionFactory build(Inputstream inputstream)
    return build(inputstream, null, null);
    public SqlSessionFactory build(Inputstream inputStream, String environment. Properties properties)
    try
    (
    //2.创建XMLConfigBuilder对象用来解析XML配置文件,生成Configuration对象
    XMLConfigBuilder parser = new XMLConfigBuilder(inputStreanij environment, properties);
    //3.将XML配置文件内的信息解析成Java对象Configuration对象
    Configuration config = parser.parse();
    〃4.根据 Configuration 对象创建出 SqlSessionFactory 对象
    return build(config);
    catch (Exception e)
    (
    throw ExceptionFactory.wrapException(*'Error building SqlSession. e)
    finally (

上述的初始化过程中,涉及到了以下几个对象:
S SqlSessionFactoryBuilder :SqlSessionFactory的构造器,用于创建SqlSessionFactory,采用yBuilder设计 模式
。Configuration :该对象是mybatis-config.xml文件中所有mybatis配置信息
。SqlSessionFactory:SqlSession工厂类,以工厂形式创建SqlSession对象,采用yFactory工厂设计模式
。XmlConfigParser :负责将mybatis-config.xml配置文件解析成Configuration对象,共
SqlSessonFactoryBuilder使用,仓I」建SqlSessionFactory
创建Configuration对象的过程
接着上述的MyBatis初始化基本过程讨论,当541565510口尸3前0»85旧6『执行build()方法,调用yXMLConfiigBuilder 的parse()方法,然后返回了口口甘目④山日。1对象。那么parse()方法是如何处理XML文件,生成Configuration对象的呢?
1 . XMLConfigBuilder会将XML配置文件的信息转换为Document对象,而XML配置定义文件DTD转换成
XMLMapperEntityResolver对象,然后将二者封装到XpathParser对象中,XpathParser的作用是提供根据Xpath表达 式获取基本的DOM节点N。de信息的操作。如下图所示:

2 .之后XMLConfigBuilder调用parse()方法:会从XPathParser中取出<configuration>节点对应的Node对象,然后 解析此Node节点的子Node:properties, settings, typeAliases,typeHandlers, objectFactory,
objectWrapperFactory, plugins, environments,databaseIdProvider, mappers
public Configuration parse()
(
if (parsed)
(
throw new BuilderException(“Each XMLConfigBuilder can only be used once.0);
)
parsed = true;
〃源码中没有这一句,只有
parseConfiguration(parser.evalNode(”/conf iguration**));
〃为了让读者看得更明麻,源码拆分为以下两句
XNode conf igurationNode = parser. evalNode(r’/configur at ion");
parseConfiguration(configurationNode);
return configuration;
)
/*
解析"/configuration”节点下的子节点信息,然后将解析的结果设置到Configuration对象中
*/
private void parseConfiguration(XNode root) {
try {
〃1 .苜先处理properties节点
propertiesElement(root.evalNode(MpropertiesM)); //issue #117 read properties first
〃2 .处理 typeAliases
typeAliasesElement(root.evalNode(“typeAliases”));
〃3.处理插件
pluginElement(root.evalNode(“plugins”));
〃4.处理 object Factory
objectFactoryElement(root.evalNode(“objectFactory”));
//5.objectWrapperFactory
objectHrapperFactoryElement(root .evalNodeCobjectWrapperFactory"));
//6.settings
settingsElement(root.eva!Node( “settings **));
〃7 .处理 environments
environmentsElement(root.evalNode(“environmentsM)); // read it after objectFactory and objectWrapperFactory issue #631
//8.database
databaseIdProviderElement(root.evalNodeC’databaseldProvider”));
//9. typeHandlers
typeHandlerElement(root.evalNode(“typeHandlers”));
//10 mappers
mapperElement(root.evalNodef"mappers”));
} catch (Exception e) {
throw new BuilderException(“Error parsing SQL Mapper Configuration. Cause: + e, e);
注意:在上述代码中,还有一个非常重要的地方,就是解析XML配置文件子节点<mappers》的方法 mapperElements(root.evalNode(“mappers”)),它将解析我们配置的Mapper.xml配置文件,Mapper配置文件可以 说是MyBatis的核心,MyBatis的特性和理念都体现在此Mapper的配置和设计上,我们将在后续的文章中讨论它,敬请期 待〜
3 .然后将这些值解析出来设置到Configuration对象中。
解析子节点的过程这里就不一一介绍了,用户可以参照MyBatis源码仔细揣摩,我们就看上述的 environmentsElement(root.evalNode(“environments”));方法是如何将environments的信息解析出来,设置到 Configuration对象中的:
解析environments节点,并将结果设置到Configuration对象中
注意:创建envronment时,如果SqlSessionFactopyBuilder指定了特定的环境(即数据源); 则返回指定环境(数据源)的Environment对象,否则返回默认的Environment对象; 这种方式实现了 MyBatis可以连接多数据源
*/
private void environmentsElement(XNode context) throws Exception
{
if (context != null)
(
if (environment == null)
(
environment = context.getStringAttribute(“default”);
)
for (XNode child : context.getChildren()) (
String id = child.getStringAttribute(“id”);
if (isSpecifiedEnvironB>ent(id))
(
〃▲.创建事务工厂 TransactionFactory
TransactionFactory txFactory =
transactionManagerElement(child.evalNode(“transactionManager”)); DataSourceFactory dsFactory =
dataSourceElement (child. evalNode( ^dataSource1’));
//2.创建数据源Dat aSource
DataSource dataSource = dsFactory.getDataSource();
//3.构造Environment对象
Environment.Builder environmentBuilder = new
Environment.Builder(id)
.transactionFactory(txFactory)
.dataSource(dataSource);
//4.将创建的Envronment对象设置到configuration对象中
configuration.setEnvironment(environmentBuilder.build()); }
private boolean isSpecifiedEnvironment(String id) {
if (environment == null) (
throw new BuilderException(MNo environment specified.”);
}
else if (id == null) (
throw new BuilderException(MEnvironment requires an id attribute.0); } else if (environment.equals(id)) (
return true;
}
return false;
4 .返回Configuration对象
我们将上述的MyBatis初始化基本过程的序列图细化。

三、手动加载XML配置文件创建Configuration对象完成初始化,创建并使用SqlSessionFactory对象
我们可以使用XMLConfigBuilder手动解析XML配置文件来创建Configuration对象,代码如下:
String resource = "mybatis-config-xml1’;
Inputstream inputStream = Resources.getResourceAsStream(resource);
〃手动创建XMLConfigBuilder,并解析创建Configuration对象
XMLConfigBuilder parser = new XMLConfigBuilder(inputStreaiDj null,null);
Configuration configuration=parse();
〃使用 Conf igu ration 对象创建 SqlSessionFactory
SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(config □ration);
四、涉及到的设计模式
初始化的过程涉及到创建各种对象,所以会使用一些创建型的设计模式。在初始化的过程中,Builder模式运用的比较多。
Builder模式应用1:SqlSessionFactory的创建
(Reader)
(Reader, String)
(Reader? Propertiesj
(Reader, Stringy Properties)
(InputS tr earn)
(Inputstre孙 String)
(Input士tream, Properties) ttuputStream, String, Properties)
'.Lonfi gur ati on)
Designed by LouLuan
http://blog-cadn.net/luanlouis
由于构造时参数不定,可以为其创建一个构造器Builder,将SqlSessionFactory的构建过程和表示分开:
C SqlSessi onFactoryBuilder
T build (Header) S^SessionFactory
ni build (Reader, String) SqlSessi onFact ory
m build(R«td«r, Proptrti<s) SqlStssionFtdory
m build<B*«dtrr String, Proptrtits) SqlStssionFactory
m build Q.nput Str earn) SqlSessionFactory
m build^TnputStrearn. String) SqlSessionFactory
m builddi9utStream, Properti es) SqlSessionFactory
m build(Jiq>utStreain/ String Properties) SqlSessionFactory
m build (Configuration) SqlSessi onFactory tcreatej
1 SqlSessionFactory

	Designed by LouLuan http://blog.csdn.net/luanlouis

MyBatis将SqlSessionFactoryBuilder和SqlSessionFactory相互独立。
Builder模式应用2:数据库连接环境Environment对象的创建 在构建Configuration对象的过程中,XMLConfiigParser解析 mybatis XML配置文件节点<6"“什0"巾6口6节点时,会有 以下相应的代码:
private void environmentsElement(XNode context) throws Exception {
if (context != null) {
if (environment == null) { environment = context.getStringAttribute(“default”);
}
for (XNode child : context.getChildren()) { String id = child.getStringAttribute(Mid”); 〃是和默认的环境相同时,解析之 if (isSpecifiedEnvironment(id)) {
TransactionFactory txFactory =
transactionManagerElement(child.evalNode(“transactionManager”));
DataSourceFactory dsFactory =
dataSourceElementCchild.evalNodeCdataSource"));
DataSource dataSource = dsFactory.getDataSource();
〃使用了Environment内置的构造器Builder,传递id事务工厂和数据源
Environment.Builder environmentBuilder = new Environment.Builder(id) ,transactionFactory(txFactory) .dataSource(dataSource);
configuration.setEnvironment(environmentBuilder.build());
在Environment内部,定义了静态内部Builder类:
public final class Environment {
private final String id;
private final TransactionFactory transactionFactory;
private final DataSource dataSource;
public Environment(String id, TransactionFactory transactionFactory, DataSource dataSource) {
if (id == null) {
throw new IllegalArgumentException(“Parameter ‘id’ must not be null”);
}
if (transactionFactory == null) {
throw new IllegalArgumentException( ,Parameter ‘transactionFactory’ must n ot be null");
}
this.id = id;
if (dataSource == null) {
throw new IllegalArgumentException("Parameter 'dataSource* must not be null
}
this.transactionFactory = transactionFactory;
this.dataSource = dataSource;
}
public static class Builder {
private String id;
private TransactionFactory transactionFactory;
private DataSource dataSource;
public Builder(String id) {
以上就是本文《深入理解mybatis原理》Mybatis初始化机制详解的全部内容,希望对大家有所帮助!上述内容如有不妥之 处,还请读者指出,共同探讨,共同进步!
【END】
推荐阅读
1我采访了一位Pornhub工程师,聊了这些纯纯的话题
2 .常见排序算法总结-Java实现
3 .Java:如何更优雅的处理空值?
4 . MySQL:数据库优化,可以看看这篇文章
5 .团队开发中Git最佳实践

喜欢文章,点个在看**J
声明:pdf仅供学习使用,一切版权归原创公众号所有;建议持续关注原创公众号获取最新文章,学习愉快!

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

奔跑吧茂林小子

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值