资讯专栏INFORMATION COLUMN

mybatis模仿1之我先看看

tylin / 2769人阅读

摘要:通过工厂获得对象。咱直接看,这个命名明显告诉玩家,就在前面了。是个抽象方法,我们得去看实现。看到想看的东西了,之后将结果交给处理。执行完操作之后,将结果交给。

</>复制代码

  1. 用了挺久的mybatis,但一直停留在用的层面上,觉得不行的呀,得走出舒适区。
    所以想自己看看mybatis的实现,然后模仿着写一个,哈哈,当然一开始不会要求完成度很高。
    这一篇就先看下mybatis奥秘。

    这里参考的mybatis源码版本是3.4.5

首先,先写一个mybatis简单使用的例子。

</>复制代码

  1. // 使用
  2. public static void main(String[] args) throws IOException {
  3. //根据配置文件创建一个SqlSessionFactory对象
  4. String resource = "mybatis-config.xml";
  5. InputStream inputStream = Resources.getResourceAsStream(resource);
  6. SqlSessionFactory sqlSessionFactory
  7. = new SqlSessionFactoryBuilder().build(inputStream);
  8. // 获取sqlSession对象
  9. SqlSession session = sqlSessionFactory.openSession();
  10. try{
  11. // 获取接口的实现类实例
  12. IUserMapper mapper = session.getMapper(IUserMapper.class);
  13. // 调用方法
  14. User user = mapper.findById(1);
  15. System.out.println(user.getName());
  16. }finally{
  17. session.close();
  18. }
  19. }

回忆一下,使用Mybatis的步骤就是

写配置文件,配置连接数据库的参数,mybatis的参数。

定义接口,并且通过注解或者xml文件的形式提供SQL语句。之后要在配置文件中注册这个接口。

创建SqlSessionFactory,传入配置文件。通过工厂获得SqlSession对象。

通过SqlSession对象获取自定义的接口的实例,然后就是调用接口的方法。

整个过程中,玩家就只参与了配置参数,还有提供SQL这两步。所以这两步就是看mybatis怎么操作的入口,是进入mybatis地下城的大门。
配置参数这部分,使用框架时基本都有这个操作,比较常见。所以算是个分支剧情,而提供SQL算是mybatis的主线剧情,这里先通关主线剧情。

剧情1 之 发生了什么

</>复制代码

  1. IUserMapper mapper = session.getMapper(IUserMapper.class);
  2. User user = mapper.findById(1);

可以看到,在使用时,我们获取到了我们的接口的一个实现类实例,
燃鹅,我们没有写这个接口的实现的呀。所以我觉得是魔法的原因,在这里要打个断点。

在getMapper的方法上断点,我们进入了DefaultSqlSession.getMapper(Class),
所以默认我们从SqlSessionFactory拿到的是一个DefaultSqlSession的实例。

</>复制代码

  1. /*
  2. 通过configuration的getMapper方法,传入我们的接口类型以及SqlSession实例,返
  3. 回一个泛型。这里也就是我们的IUserMapper接口的实现类的实例。*/
  4. @Override
  5. public T getMapper(Class type) {
  6. return configuration.getMapper(type, this);
  7. }

再进去是Configuration.getMapper(Class, SqlSession)。

</>复制代码

  1. /*
  2. 这里又从mapperRegistry里拿到对象,
  3. mapperRegistry是Configuration类的一个属性*/
  4. public T getMapper(Class type, SqlSession sqlSession) {
  5. return mapperRegistry.getMapper(type, sqlSession);
  6. }

看一下MapperRegistry的getMapper里边是什么。
这里看到了令人激动的字眼,就是Proxy,
猜测我们最终拿到的IUserMapper的实例是个代理对象

</>复制代码

  1. @SuppressWarnings("unchecked")
  2. public T getMapper(Class type, SqlSession sqlSession) {
  3. // 看了下,knownMappers是个Map对象,Map, MapperProxyFactory>
  4. final MapperProxyFactory mapperProxyFactory
  5. = (MapperProxyFactory) knownMappers.get(type);
  6. if (mapperProxyFactory == null) {
  7. throw new BindingException("Type " + type +
  8. " is not known to the MapperRegistry.");
  9. }
  10. try {
  11. /*新建一个实例,需要进去看下*/
  12. return mapperProxyFactory.newInstance(sqlSession);
  13. } catch (Exception e) {
  14. throw new BindingException("Error getting mapper instance. Cause: " + e, e);
  15. }
  16. }

MapperProxyFactory的newInstance 一探究竟
其实名字叫xxFactory的肯定是生产xx的,可以猜到返回的是个MapperProxy

</>复制代码

  1. public T newInstance(SqlSession sqlSession) {
  2. /*这里new了一个MapperProxy,然后调用newInstance*/
  3. final MapperProxy mapperProxy
  4. = new MapperProxy(sqlSession, mapperInterface, methodCache);
  5. return newInstance(mapperProxy);
  6. }

MapperProxy是个啥

</>复制代码

  1. /* 这个类实现了InvocationHandler,动态代理的接口。*/
  2. public class MapperProxy implements InvocationHandler, Serializable

看看newInstance(mapperProxy)做了啥。
使用Proxy构造实现我们IUserMapper接口的代理类的实例!

</>复制代码

  1. @SuppressWarnings("unchecked")
  2. protected T newInstance(MapperProxy mapperProxy) {
  3. return (T) Proxy.newProxyInstance(mapperInterface.getClassLoader(), new Class[] { mapperInterface }, mapperProxy);
  4. }

消化一下,开始的疑问是我们没有提供IUserMapper的实现,但是通过SqlSession的getMapper方法能拿到一个IUserMapper的实现类的对象。
谜底就是最终返回了我们接口的一个代理类的实例。
而MapperProxy实现了InvocationHandler接口,在我们构造代理对象时传入了MapperProxy对象,
因此在调IUserMapper的所有方法时,都会进入到MapperProxy类的invoke方法。
其实不像上边那样操作,通过直接打印这个对象也可以看出来..

</>复制代码

  1. System.out.println(mapper);
  2. System.out.println(Proxy.isProxyClass(mapper.getClass()));
  3. // 打印结果,贴图片太丑了,就不贴结果图了。
  4. org.apache.ibatis.binding.MapperProxy@e580929
  5. true

剧情2 之 MapperProxy你干了啥

一般使用动态代理,实现了InvocationHandler接口的类中都会持有被代理类的引用,这里也就是MapperProxy。然后在invoke方法里边先执行额外的操作,再调用被代理类的方法。在MapperProxy这个类里却没找到被代理类的引用。

</>复制代码

  1. public class MapperProxy implements InvocationHandler, Serializable {
  2. private static final long serialVersionUID = -6424540398559729838L;
  3. private final SqlSession sqlSession;
  4. private final Class mapperInterface;
  5. private final Map methodCache;
  6. 。。。

所以穿山甲说了什么?
所以当我们调用 IUserMapper 的 findById 时发生了什么?
这里就要看下MapperProxy的invoke方法了。

</>复制代码

  1. @Override
  2. public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
  3. try {
  4. if (Object.class.equals(method.getDeclaringClass())) {
  5. return method.invoke(this, args);
  6. } else if (isDefaultMethod(method)) {
  7. return invokeDefaultMethod(proxy, method, args);
  8. }
  9. } catch (Throwable t) {
  10. throw ExceptionUtil.unwrapThrowable(t);
  11. }
  12. final MapperMethod mapperMethod = cachedMapperMethod(method);
  13. return mapperMethod.execute(sqlSession, args);
  14. }

首先进行一个 if 判断,逻辑是 如果调的这个方法的提供类是Object类,那个就直接执行这个方法。
这里容易想偏,哪个类不是Object的子类呀..
其实应该是 如果是Object中的方法,那就直接执行。
Object有哪些方法呢?toString这些。调mapper.toString()时,就直接被执行,不走下边的逻辑了。

</>复制代码

  1. if (Object.class.equals(method.getDeclaringClass())) {
  2. return method.invoke(this, args);

之后是第二个 if,逻辑是,如果这个方法的权限修饰符是public并且是由接口提供的,则执行invokeDefaultMethod方法。
比如在IUserMapper写了一个默认方法,执行这个方法isDefaultMethod就会返回true了。
这里我们的方法的提供方是代理类,不是接口,所以返回了false。

</>复制代码

  1. else if (isDefaultMethod(method)) {
  2. return invokeDefaultMethod(proxy, method, args);
  3. }
  4. // isDefaultMethod
  5. private boolean isDefaultMethod(Method method) {
  6. return
  7. ((method.getModifiers() & (Modifier.ABSTRACT | Modifier.PUBLIC | Modifier.STATIC))
  8. == Modifier.PUBLIC)
  9. &&
  10. method.getDeclaringClass().isInterface();
  11. }

前两步都是过滤作用,下边的才是重点。
可以看到通过 cachedMapperMethod方法 拿到了一个 MapperMethod 对象。
看名字是从缓存里拿。然后就执行MapperMethod的execute方法。

</>复制代码

  1. final MapperMethod mapperMethod = cachedMapperMethod(method);
  2. return mapperMethod.execute(sqlSession, args);

缓一缓,小结一下。开始的疑问是,MapperProxy类里边竟然没有被代理类对象的引用。
那它想干什么。在invoke方法中我们找到答案。
通过invoke方法的method参数,拿到了一个MapperMethod 对象,
然后执行了这个对象的execute方法,就没了。中间一些常规的方法就直接执行。
所以纯粹就是为了进入invoke方法,拿到MapperMethod ,至始至终都不存在被代理类。
哇,代理的神奇用法,小本本记起来。

接着我们看看怎么通过method参数拿到MapperMethod
这里就很简单了,Map里边有就直接返回,没有就新建。接口的一个方法就对应一个MapperMethod。
so easy ~

</>复制代码

  1. //cachedMapperMethod
  2. private MapperMethod cachedMapperMethod(Method method) {
  3. // methodCache 是个Map
  4. MapperMethod mapperMethod = methodCache.get(method);
  5. if (mapperMethod == null) {
  6. mapperMethod
  7. = new MapperMethod(mapperInterface, method, sqlSession.getConfiguration());
  8. methodCache.put(method, mapperMethod);
  9. }
  10. return mapperMethod;
  11. }

看下MapperMethod的构造过程,发现传入了接口信息,方法信息,还有配置信息。
主要工作是初始化 command 还有 method 字段。
command里边就保存方法的名称(com.mapper.IUserMapper.findById),还有对应的SQL类型(SELECT)。
method里边保存了方法的返回类型,是否是集合,是否是游标等信息。
看到这里,其实我一直在忽略Configuration这个类里边是什么东西,等要模仿再去看。

</>复制代码

  1. public MapperMethod(Class mapperInterface, Method method, Configuration config) {
  2. this.command = new SqlCommand(config, mapperInterface, method);
  3. this.method = new MethodSignature(config, mapperInterface, method);
  4. }

剧情3 之 MapperMethod你跟着干了啥

我们调用 mapper.findByid, 最终是通过MapperMethod执行execute得到结果。
所以接下来要看看execute方法中隐藏了什么秘密。

下边是execute方法的内容

</>复制代码

  1. public Object execute(SqlSession sqlSession, Object[] args) {
  2. Object result;
  3. switch (command.getType()) {
  4. case INSERT: {
  5. Object param = method.convertArgsToSqlCommandParam(args);
  6. result = rowCountResult(sqlSession.insert(command.getName(), param));
  7. break;
  8. }
  9. case UPDATE: {
  10. Object param = method.convertArgsToSqlCommandParam(args);
  11. result = rowCountResult(sqlSession.update(command.getName(), param));
  12. break;
  13. }
  14. case DELETE: {
  15. Object param = method.convertArgsToSqlCommandParam(args);
  16. result = rowCountResult(sqlSession.delete(command.getName(), param));
  17. break;
  18. }
  19. case SELECT:
  20. if (method.returnsVoid() && method.hasResultHandler()) {
  21. executeWithResultHandler(sqlSession, args);
  22. result = null;
  23. } else if (method.returnsMany()) {
  24. result = executeForMany(sqlSession, args);
  25. } else if (method.returnsMap()) {
  26. result = executeForMap(sqlSession, args);
  27. } else if (method.returnsCursor()) {
  28. result = executeForCursor(sqlSession, args);
  29. } else {
  30. Object param = method.convertArgsToSqlCommandParam(args);
  31. result = sqlSession.selectOne(command.getName(), param);
  32. }
  33. break;
  34. case FLUSH:
  35. result = sqlSession.flushStatements();
  36. break;
  37. default:
  38. throw new BindingException("Unknown execution method for: " + command.getName());
  39. }
  40. if (result == null
  41. && method.getReturnType().isPrimitive()
  42. && !method.returnsVoid()) {
  43. throw new BindingException("Mapper method "" + command.getName() + " attempted to return null from a method with a primitive return type (" + method.getReturnType() + ").");
  44. }
  45. return result;
  46. }

可以看到 INSERT UPDATE 这些熟悉的字眼的了。
通过MapperMethod里的command的属性,进入不同分支。
这里调用的是findById,进入了SELECT分支,最终执行了下边的语句,第一句是装配参数,第二句是执行查询。

</>复制代码

  1. Object param = method.convertArgsToSqlCommandParam(args);
  2. result = sqlSession.selectOne(command.getName(), param);

看下 sqlSession.selectOne(),里边调用了selectList的方法,然后将结果返回。

</>复制代码

  1. @Override
  2. public T selectOne(String statement, Object parameter) {
  3. // Popular vote was to return null on 0 results and throw exception on too many.
  4. List list = this.selectList(statement, parameter);
  5. if (list.size() == 1) {
  6. return list.get(0);
  7. } else if (list.size() > 1) {
  8. throw new TooManyResultsException("Expected one result (or null) to be returned by selectOne(), but found: " + list.size());
  9. } else {
  10. return null;
  11. }
  12. }

进入到selectList瞧瞧,感觉流程要走完了,都已经开始select了。
这里又看到了令人激动的字眼,statement。感觉已经在靠近JDBC啦。
有个MappedStatement的对象需要关注一下。

</>复制代码

  1. @Override
  2. public List selectList(String statement, Object parameter, RowBounds rowBounds) {
  3. try {
  4. MappedStatement ms = configuration.getMappedStatement(statement);
  5. return executor.query(ms, wrapCollection(parameter), rowBounds, Executor.NO_RESULT_HANDLER);
  6. } catch (Exception e) {
  7. throw ExceptionFactory.wrapException("Error querying database. Cause: " + e, e);
  8. } finally {
  9. ErrorContext.instance().reset();
  10. }
  11. }

回忆下基本JDBC是怎么用的。

</>复制代码

  1. /*
  2. 1 加载对应数据库驱动
  3. Class.forName(driverClass);
  4. 2 获取数据库连接
  5. Connection con = DriverManager.getConnection(jdbcUrl, user, password);
  6. 3 准备SQL语句
  7. String sql = " ... ";
  8. 4 执行操作
  9. Statement statement = con.createStatement();
  10. statement.executeUpdate(sql);
  11. 5 释放资源
  12. statement.close();
  13. con.close();*/

MappedStatement是个什么东西呢,它对应着我们的一条SQL语句。
是通过MapperMethod的command对象的name属性,从configuration里边拿到的。

拿到MappedStatement之后调用 executor的query方法,这个方法是CachingExecutor提供的。
可以看到,这里通过MappedStatement获取了我们的SQL,然后生成一个缓存key,想起我记忆深处的mybatis一级二级缓存。
之后返回调用query方法的结果。

</>复制代码

  1. @Override
  2. public List query(MappedStatement ms, Object parameterObject, RowBounds rowBounds, ResultHandler resultHandler) throws SQLException {
  3. BoundSql boundSql = ms.getBoundSql(parameterObject);
  4. CacheKey key = createCacheKey(ms, parameterObject, rowBounds, boundSql);
  5. return query(ms, parameterObject, rowBounds, resultHandler, key, boundSql);
  6. }

还是进入CachingExecutor的query方法。看来Executor这样的类就是真正执行数据库操作的类了。
看到先是从MappedStatement里边拿缓存,如果是空的,就调用delegate.query,
delegate是SimpleExecutor类型,顾名思义CachingExecutor委派了SimpleExecutor来进行数据库操作。

</>复制代码

  1. @Override
  2. public List query(MappedStatement ms, Object parameterObject, RowBounds rowBounds, ResultHandler resultHandler, CacheKey key, BoundSql boundSql)
  3. throws SQLException {
  4. Cache cache = ms.getCache();
  5. if (cache != null) {
  6. flushCacheIfRequired(ms);
  7. if (ms.isUseCache() && resultHandler == null) {
  8. ensureNoOutParams(ms, parameterObject, boundSql);
  9. @SuppressWarnings("unchecked")
  10. List list = (List) tcm.getObject(cache, key);
  11. if (list == null) {
  12. list = delegate. query(ms, parameterObject, rowBounds, resultHandler, key, boundSql);
  13. tcm.putObject(cache, key, list); // issue #578 and #116
  14. }
  15. return list;
  16. }
  17. }
  18. return delegate. query(ms, parameterObject, rowBounds, resultHandler, key, boundSql);
  19. }

继续看SimpleExecutor里边的query,看到SimpleExecutor里边并没有query方法,
而是SimpleExecutor继承了BaseExecutor,query是BaseExecutor类提供的。
第一句断点进去之后,看到的是存起来的"executing a query",这是出异常时的堆栈信息。
emm..然后就是很多是否存在缓存是否使用缓存的代码。
咱直接看queryFromDatabase(),这个命名明显告诉玩家,BOSS就在前面了。

</>复制代码

  1. @SuppressWarnings("unchecked")
  2. @Override
  3. public List query(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, CacheKey key, BoundSql boundSql) throws SQLException {
  4. ErrorContext.instance().resource(ms.getResource()).activity("executing a query").object(ms.getId());
  5. if (closed) {
  6. throw new ExecutorException("Executor was closed.");
  7. }
  8. if (queryStack == 0 && ms.isFlushCacheRequired()) {
  9. clearLocalCache();
  10. }
  11. List list;
  12. try {
  13. queryStack++;
  14. list = resultHandler == null ? (List) localCache.getObject(key) : null;
  15. if (list != null) {
  16. handleLocallyCachedOutputParameters(ms, key, parameter, boundSql);
  17. } else {
  18. list = queryFromDatabase(ms, parameter, rowBounds, resultHandler, key, boundSql);
  19. }
  20. } finally {
  21. queryStack--;
  22. }
  23. if (queryStack == 0) {
  24. for (DeferredLoad deferredLoad : deferredLoads) {
  25. deferredLoad.load();
  26. }
  27. // issue #601
  28. deferredLoads.clear();
  29. if (configuration.getLocalCacheScope() == LocalCacheScope.STATEMENT) {
  30. // issue #482
  31. clearLocalCache();
  32. }
  33. }
  34. return list;
  35. }

同样是BaseExecutor 提供的 queryFromDatabase()方法。
首先put进去了一个缓存,key是我们之前的缓存键,值是一个默认的值,感觉是占位的意思。
然后执行doQuery方法,看到do开头的方法,就知道不简单。doGet doPost
doQuery是个抽象方法,我们得去SimpleExecutor看实现。

</>复制代码

  1. private List queryFromDatabase(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, CacheKey key, BoundSql boundSql) throws SQLException {
  2. List list;
  3. localCache.putObject(key, EXECUTION_PLACEHOLDER);
  4. try {
  5. list = doQuery(ms, parameter, rowBounds, resultHandler, boundSql);
  6. } finally {
  7. localCache.removeObject(key);
  8. }
  9. localCache.putObject(key, list);
  10. if (ms.getStatementType() == StatementType.CALLABLE) {
  11. localOutputParameterCache.putObject(key, parameter);
  12. }
  13. return list;
  14. }

SimpleExecutor.doQuery
来啦! Statement!而且还有我们熟悉的prepareStatement字眼。哈哈 都是JDBC呀
最后看到是由一个Handler来执行的,看一看这个Handler。

</>复制代码

  1. @Override
  2. public List doQuery(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, BoundSql boundSql) throws SQLException {
  3. Statement stmt = null;
  4. try {
  5. Configuration configuration = ms.getConfiguration();
  6. StatementHandler handler = configuration.newStatementHandler(wrapper, ms, parameter, rowBounds, resultHandler, boundSql);
  7. stmt = prepareStatement(handler, ms.getStatementLog());
  8. return handler.query(stmt, resultHandler);
  9. } finally {
  10. closeStatement(stmt);
  11. }
  12. }

先是进入到了RoutingStatementHandler,然后RoutingStatementHandler委托给了PreparedStatementHandler,下边是PreparedStatementHandler的query。
看到想看的东西了,ps.execute()
之后将结果交给resultSetHandler处理。

</>复制代码

  1. @Override
  2. public List query(Statement statement, ResultHandler resultHandler) throws SQLException {
  3. PreparedStatement ps = (PreparedStatement) statement;
  4. ps.execute();
  5. return resultSetHandler. handleResultSets(ps);
  6. }

回顾一下,我们的问题是MapperMethod对象的execute方法做了什么,结论就是,
我们通过MapperMethod的command属性和method属性,知道了要执行的SQL的类型,
这里我们走的是SELECT路线。知道类型之后,交由SqlSession执行selectOne方法。
然后又调用了DefaultSqlSession的selectList方法,DefaultSqlSession表示不想干活,
就交给了勤劳的BaseExecutor,BaseExecutor的里边有query方法,query方法做一些通用操作,
看一眼有没有缓存呀这些。在没有或不用缓存的情况下,再去调doQuery方法,doQuery方法有不同的实现。
在doQuery以及其要调用方法里边使用的就是我们熟悉的JDBC。执行完操作之后,将结果交给resultSetHandler。

总结

我们使用的是什么?

</>复制代码

  1. 答:使用的是我们的接口的代理类的实例。
    在构造代理类的实例时,
    我们传入了实现了InvocationHandler接口的MapperProxy实例,
    当代理对象调用方法时,会进入MapperProxy的invoke方法。
    在invoke方法中通过Method对象找MapperMethod,
    然后执行MapperMethod对象的execute方法。
    在这里,代理的作用是,让我们知道哪个接口的哪个方法被使用了。

    MapperProxy 对应了我们的一个接口,
    MapperMethod 对应接口里的一个方法,
    MappedStatement 对应一条SQL

从上边流程中,总结各个类的职责

</>复制代码

  1. MapperProxy: 定义代理对象调用方法时执行的动作。
    即在invoke()里拿到调用的方法对应的MapperMethod,然后调用MapperMethod的execute

    MapperMethod: 对应我们接口里的方法,持有SqlCommand(command)和MethodSignature(method),
    可以知道方法的全名以及对应的SQL的类型。

  2. MappedStatement: 保存的SQL的信息。

  3. SqlSession: 玩家获取Mapper的地方。假装执行SQL,实际交给了Executor。

  4. Executor: 真正执行数据库操作。

大致知道流程是什么样的,接着就可以模仿着写一写了...
emm...感觉没这么简单。

文章版权归作者所有,未经允许请勿转载,若此文章存在违规行为,您可以联系管理员删除。

转载请注明本文地址:https://www.ucloud.cn/yun/76701.html

相关文章

  • MongoDB之我是怎么成为Primary节点的

    摘要:此文已由作者温正湖授权网易云社区发布。欢迎访问网易云社区,了解更多网易技术产品运营经验。而严格的不会出现这个情况。最后安利下,网易蜂巢云服务已经重磅上线,蜂巢由业界著名的数据库专家姜承尧亲自把关架构设计,免费提供售前技术支持。 此文已由作者温正湖授权网易云社区发布。 欢迎访问网易云社区,了解更多网易技术产品运营经验。 Primary(主)是MongoDB复制集中的最重要的角色,是能够接...

    microcosm1994 评论0 收藏0
  • Spring框架之我见(一)——工厂模式

    摘要:相对于工厂模式,抽象工厂模式生产的对象更加具体,也更加丰富,但相对编码也更加复杂。具体的抽象工厂模式的实现大家可以参考菜鸟教程。知道了工厂模式和抽象工厂模式的区别,请大家使用的时候应该根据具体的情况进行选择。 大家好,今天给大家分享一些Spring的学习心得,在讲Spring之前,先和大家分享Spring中核心的设计模式。 工厂模式 在聊概念之前我先问问大家:什么是工厂? 这个很简单,...

    venmos 评论0 收藏0
  • 教你手写Mybatis框架

    摘要:前言嗨,小伙伴们,这篇博文将带大家手写,让大家对的核心原理以及工作流程有更加深刻的理解。模块顾名思义,就是框架配置类,用于解析配置文件加载相关环境。配置模块这里的对框架的配置使用了简单的,主要原因还是简单易懂然后节省时间。 前言 (。・∀・)ノ゙嗨,小伙伴们,这篇博文将带大家手写mybatis,让大家对mybaits的核心原理以及工作流程有更加深刻的理解。在上篇Spring-Mybat...

    antyiwei 评论0 收藏0
  • Spring框架之我见(五)——Spring Boot

    摘要:通过我们可以更轻松地入门,更简单的使用的框架。团队为了摆脱框架中各类繁复纷杂的配置,使用约定优于配置的思想,在基础上整合了大量常用的第三方库的开发框架。这里还要说的一点,的出现并不是单纯的为了简化开发,更是为做铺垫。 说完了Spring 我们来聊聊Spring的进阶版Spring Boot,如果你还不知道Spring Boot,那希望这篇文章能够为你指明方向。 Spring Boot ...

    张巨伟 评论0 收藏0

发表评论

0条评论

最新活动
阅读需要支付1元查看
<