MyBatis的基本应用

源码地址

01.MyBatis环境搭建

  1. 添加MyBatis的坐标

            <!--mybatis坐标-->
            <dependency>
                <groupId>org.mybatis</groupId>
                <artifactId>mybatis</artifactId>
                <version>3.5.9</version>
            </dependency>
            <!--mysql驱动坐标-->
            <dependency>
                <groupId>mysql</groupId>
                <artifactId>mysql-connector-java</artifactId>
                <version>8.0.33</version>
                <scope>runtime</scope>
            </dependency>
            <!--单元测试坐标-->
            <dependency>
                <groupId>junit</groupId>
                <artifactId>junit</artifactId>
                <version>4.13.2</version>
            </dependency>
    
  2. 创建数据表

  3. 编写DO实体类

  4. 编写映射⽂件UserMapper.xml

    <?xml version="1.0" encoding="UTF-8" ?>
    <!DOCTYPE mapper
            PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
            "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
    
    <mapper namespace="com.muchfish.dao.IUserDao">
        <!--namespace : 名称空间:与id组成sql的唯一标识
            resultType: 表明返回值类型-->
        
        <!--查询用户-->
        <select id="findAll" resultType="com.muchfish.pojo.User">
            select * from User
        </select>
        
        
    </mapper>
    
  5. 编写核⼼⽂件SqlMapConfig.xml

    <?xml version="1.0" encoding="UTF-8" ?>
    <!DOCTYPE configuration PUBLIC "-//mybatis.org//DTD Config 3.0//EN"
            "http://mybatis.org/dtd/mybatis-3-config.dtd">
    
    <configuration>
    
        <!--加载外部的properties文件-->
        <properties resource="jdbc.properties"/>
    
        <!--environments:运行环境-->
        <environments default="development">
            <environment id="development">
                <!--当前事务交由JDBC进行管理-->
                <transactionManager type="JDBC"/>
                <!--当前使用mybatis提供的连接池-->
                <dataSource type="POOLED">
                    <property name="driver" value="${jdbc.driver}"/>
                    <property name="url" value="${jdbc.url}"/>
                    <property name="username" value="${jdbc.username}"/>
                    <property name="password" value="${jdbc.password}"/>
                </dataSource>
            </environment>
        </environments>
    
        <!--引入映射配置文件-->
        <mappers>
            <mapper resource="UserMapper.xml"/>
        </mappers>
    
    
    
    </configuration>
    

    jdbc.properties

    jdbc.driver=com.mysql.cj.jdbc.Driver
    jdbc.url=jdbc:mysql:///mybatis
    jdbc.username=root
    jdbc.password=123456
    
  6. 编写测试类

        @Test
        public void test1() throws IOException {
            //1.Resources工具类,配置文件的加载,把配置文件加载成字节输入流
            InputStream resourceAsStream = Resources.getResourceAsStream("sqlMapConfig.xml");
            //2.解析了配置文件,并创建了sqlSessionFactory工厂
            SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(resourceAsStream);
            //3.生产sqlSession
            SqlSession sqlSession = sqlSessionFactory.openSession();// 默认开启一个事务,但是该事务不会自动提交
            //在进行增删改操作时,要手动提交事务
            //4.sqlSession调用方法:查询所有selectList  查询单个:selectOne 添加:insert  修改:update 删除:delete
            List<User> users = sqlSession.selectList("com.muchfish.dao.IUserDao.findAll");
            for (User user : users) {
                System.out.println(user);
            }
            sqlSession.close();
    
        }
    

02.MyBatis的CRUD

  1. CRUD的API

    • sqlSession.selectList()、sqlSession.selectOne()
    • sqlSession.insert()
    • sqlSession.update()
    • sqlSession.delete()
  2. 注意问题

    • 在进行增删改操作时,要手动提交事务。

      sqlSessionFactory.openSession()默认开启一个事务,但是该事务不会自动提交

    • mapper.xml中的Sql语句中使⽤#{任意字符串}⽅式引⽤传递的单个参数

      <!--删除-->
      <delete id="deleteUser" parameterType="int">
          delete from user where id = #{abc}
      </delete>
      
    • sqlSession.close():释放资源

    • sqlSession.commit()

    • sqlSession.rollback()

    • sqlSessionFactory.openSession(true):事务自动提交

03.MyBatis相关配置文件

sqlMapConfig.xml

在这里插入图片描述
在这里插入图片描述

  • mapper标签

    该标签的作⽤是加载映射的,加载⽅式有如下⼏种:

    •使⽤相对于类路径的资源引⽤,例如:
    <mapper resource="org/mybatis/builder/AuthorMapper.xml"/>
    
    •使⽤完全限定资源定位符(URL),例如:
    <mapper url="file:///var/mappers/AuthorMapper.xml"/>
    
    •使⽤映射器接⼝实现类的完全限定类名,例如:
    <mapper class="org.mybatis.builder.AuthorMapper"/>
    注意:保证接口名和xml文件名一致且包结构一致(是否包结构一直皆可,文件名可以不一致)
    
    •将包内的映射器接⼝实现全部注册为映射器,例如:
    <package name="org.mybatis.builder"/>
    注意:保证接口名和xml文件名一致且包结构一致。(是否包结构一直皆可,文件名可以不一致。测试得:文件名也必须一致)
    

XXXmapper.xml

在这里插入图片描述

04.MyBatis的Dao层代理开发方式与mappers标签测试

mappers标签测试

•使⽤映射器接⼝实现类的完全限定类名,例如:
<mapper class="com.muchfish.dao.IUserDao"/>
注意:保证接口名和xml文件名一致且包结构一致

•将包内的映射器接⼝实现全部注册为映射器,例如:
<package name="com.muchfish.dao"/>
注意:保证接口名和xml文件名一致且包结构一致。

在这里插入图片描述

Dao层代理开发

Mapper 接⼝开发需要遵循以下规范:

  1. Mapper.xml⽂件中的namespace与mapper接⼝的全限定名相同
  2. Mapper接⼝⽅法名和Mapper.xml中定义的每个statement的id相同
  3. Mapper接⼝⽅法的输⼊参数类型和mapper.xml中定义的每个sql的parameterType的类型相同
  4. Mapper接⼝⽅法的输出参数类型和mapper.xml中定义的每个sql的resultType的类型相同

在这里插入图片描述

05.MyBatis的多对多复杂映射

  1. DO类

    public class User {
    
        private Integer id;
        private String username;
        private String password;
        private String birthday;
        //表示用户关联的角色
        private List<Role> roleList = new ArrayList<>();
        
        //。。。省略getter/setter
    }    
    
  2. Mapper.xml

        <resultMap id="userRoleMap" type="com.muchfish.pojo.User">
            <result property="id" column="userid"></result>
            <result property="username" column="username"></result>
            <collection property="roleList" ofType="com.muchfish.pojo.Role">
                <result property="id" column="roleid"></result>
                <result property="roleName" column="roleName"></result>
                <result property="roleDesc" column="roleDesc"></result>
            </collection>
        </resultMap>
    
        <select id="findAllUserAndRole" resultMap="userRoleMap">
            select * from user u left join sys_user_role ur on u.id = ur.userid
                                 left join sys_role r on r.id = ur.roleid
        </select>
    
  3. Dao接口

    public interface IUserDao {
    	public List<User> findAllUserAndRole();
    }
    

06.MyBatis的注解开发

  • MyBatis的常⽤注解

    • @Insert:实现新增
    • @Update:实现更新
    • @Delete:实现删除
    • @Select:实现查询
    • @Result:实现结果集封装
    • @Results:可以与@Result ⼀起使⽤,封装多个结果集
    • @One:实现⼀对⼀结果集封装
    • @Many:实现⼀对多结果集封装
  • 注解一对多查询

    public interface IOrderDao {
    
    
        //查询订单的同时还查询该订单所属的用户
    
        @Results({
                @Result(property = "id",column = "id"),
                @Result(property = "orderTime",column = "orderTime"),
                @Result(property = "total",column = "total"),
                @Result(property = "user",column = "uid",javaType = User.class,
                        one=@One(select = "com.muchfish.dao.IUserDao.findUserById"))
        })
        @Select("select * from orders")
        public List<Order> findOrderAndUser();
    
    
        @Select("select * from orders where uid = #{uid}")
        public List<Order> findOrderByUid(Integer uid);
    
    }
    
    • 注解和xml混合使用命中同一个statementId会报错

07.MyBatis缓存

⼀级缓存

private IUserDao userMapper;
private SqlSession sqlSession;
private SqlSessionFactory sqlSessionFactory;



@Before
public void before() throws IOException {
    InputStream resourceAsStream = Resources.getResourceAsStream("sqlMapConfig.xml");
    sqlSessionFactory = new SqlSessionFactoryBuilder().build(resourceAsStream);
    sqlSession = sqlSessionFactory.openSession();
    userMapper = sqlSession.getMapper(IUserDao.class);

}

@Test
public void test1() {
    //第⼀次查询,发出sql语句,并将查询出来的结果放进缓存中
    User u1 = userMapper.findUserById(1);
    System.out.println(u1);
    //第⼆次查询,由于是同⼀个sqlSession,会在缓存中查询结果
    //如果有,则直接从缓存中取出来,不和数据库进⾏交互
    User u2 = userMapper.findUserById(1);
    System.out.println(u2);
    sqlSession.close();
}

查看控制台打印情况:

在这里插入图片描述

    @Test
    public void test2(){
        //根据 sqlSessionFactory 产⽣ session
        //第⼀次查询,发出sql语句,并将查询的结果放⼊缓存中
        User u1 = userMapper.findUserById( 1 );
        System.out.println(u1);
        //第⼆步进⾏了⼀次更新操作, sqlSession.commit()
        u1.setPassword("23131");
        userMapper.updateUserByUserId(u1);
        sqlSession.commit();
        //第⼆次查询,由于是同⼀个sqlSession.commit(),会清空缓存信息
        //则此次查询也会发出sql语句
        User u2 = userMapper.findUserById(1);
        System.out.println(u2);
        sqlSession.close();
    }

查看控制台打印情况:
日志略:第⼆次查询会打印sql语句

在这里插入图片描述

  • 总结
    1. 第⼀次发起查询⽤户id为1的⽤户信息,先去找缓存中是否有id为1的⽤户信息,如果没有,从 数据库查询⽤户信息。得到⽤户信息,将⽤户信息存储到⼀级缓存中。
    2. 如果中间sqlSession去执⾏commit操作(执⾏插⼊、更新、删除),则会清空SqlSession中的 ⼀级缓存,这样做的⽬的为了让缓存中存储的是最新的信息,避免脏读。
    3. 第⼆次发起查询⽤户id为1的⽤户信息,先去找缓存中是否有id为1的⽤户信息,缓存中有,直 接从缓存中获取⽤户信息

二级缓存

在这里插入图片描述

  1. 使用二级缓存

    1. 开启⼆级缓存

      1. 在全局配置⽂件sqlMapConfig.xml⽂件中开启

        <!--开启⼆级缓存 注意<settings>标签的顺序,在<properties>标签后面-->
        <settings>
        <setting name="cacheEnabled" value="true"/>
        </settings>
        
      2. 在Mapper.xml⽂件中开启缓存

            <!--使用二级缓存-->
            <cache></cache>
        
      3. 在DAO接口中开启缓存(可选。使用@CacheNamespace会报错。)

        @CacheNamespace
        public interface IUserDao {
        
        }
        

        @CacheNamespaceRef(IUserDao.class) 
        //@CacheNamespace
        public interface IUserDao {
            
        }
        
        • xml和对应dao接口上同时开启二级缓存会报错,此时只能使用@CacheNamespaceRef
    2. DO序列化

      public class User implements Serializable {
      	//。。。
      }
      
    3. 测试

       @Test
          public void SecondLevelCache(){
              //根据 sqlSessionFactory 产⽣ session
              SqlSession sqlSession1 = sqlSessionFactory.openSession();
              SqlSession sqlSession2 = sqlSessionFactory.openSession();
              SqlSession sqlSession3 = sqlSessionFactory.openSession();
              String statement = "com.lagou.pojo.UserMapper.selectUserByUserld" ;
              IUserDao userMapper1 = sqlSession1.getMapper(IUserDao. class );
              IUserDao userMapper2 = sqlSession2.getMapper(IUserDao. class );
              IUserDao userMapper3 = sqlSession2.getMapper(IUserDao. class );
              //第⼀次查询,发出sql语句,并将查询的结果放⼊缓存中
              User u1 = userMapper1.findById( 1 );
              System.out.println(u1);
              sqlSession1.close(); //第⼀次查询完后关闭sqlSession
              //执⾏更新操作, commit()。注释掉此处,sqlSession2的查询会走缓存。放开此处,会走数据库
      //        u1.setUsername( "aaa" );
      //        userMapper3.updateUserByUserId(u1);
      //        sqlSession3.commit();
              //第⼆次查询,由于上次更新操作,缓存数据已经清空(防⽌数据脏读),这⾥必须再次发出sql语
              User u2 = userMapper2.findById( 1 );
              System.out.println(u2);
              sqlSession2.close();
      
      
          }
      
  2. useCache和flushCache

    1. useCache:开启或禁用缓存

          <select id="findById" resultType="com.muchfish.pojo.User" useCache="true">
              select * from user where id = #{id}
          </select>
      
    2. flushCache:刷新缓存

          <select id="findById" resultType="com.muchfish.pojo.User" useCache="true" flushCache="true">
              select * from user where id = #{id}
          </select>
      

二级缓存整合redis

  1. pom⽂件

            <dependency>
                <groupId>org.mybatis.caches</groupId>
                <artifactId>mybatis-redis</artifactId>
                <version>1.0.0-beta2</version>
            </dependency>
    
  2. 配置⽂件

    <?xml version="1.0" encoding="UTF-8" ?>
    <!DOCTYPE mapper
            PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
            "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
    
    <mapper namespace="com.muchfish.dao.IUserDao">
        <!--namespace : 名称空间:与id组成sql的唯一标识
            resultType: 表明返回值类型-->
    
        <!--使用二级缓存-->
        <cache type="org.mybatis.caches.redis.RedisCache" />
    
  3. redis.properties

    redis.host=localhost
    redis.port=6379
    redis.connectionTimeout=5000
    redis.password=
    redis.database=0
    
  4. 测试

        @Test
        public void xmlSecondLevelCache(){
            //根据 sqlSessionFactory 产⽣ session
            SqlSession sqlSession1 = sqlSessionFactory.openSession();
            SqlSession sqlSession2 = sqlSessionFactory.openSession();
            SqlSession sqlSession3 = sqlSessionFactory.openSession();
            String statement = "com.lagou.pojo.UserMapper.selectUserByUserld" ;
            IUserDao userMapper1 = sqlSession1.getMapper(IUserDao. class );
            IUserDao userMapper2 = sqlSession2.getMapper(IUserDao. class );
            IUserDao userMapper3 = sqlSession2.getMapper(IUserDao. class );
            //第⼀次查询,发出sql语句,并将查询的结果放⼊缓存中
            User u1 = userMapper1.findById( 1 );
            System.out.println(u1);
            sqlSession1.close(); //第⼀次查询完后关闭sqlSession
            //执⾏更新操作, commit()。注释掉此处,sqlSession2的查询会走缓存。放开此处,会走数据库
    //        u1.setUsername( "aaa" );
    //        userMapper3.updateUserByUserId(u1);
    //        sqlSession3.commit();
            //第⼆次查询,由于上次更新操作,缓存数据已经清空(防⽌数据脏读),这⾥必须再次发出sql语
            User u2 = userMapper2.findById( 1 );
            System.out.println(u2);
            sqlSession2.close();
    
    
        }
    
    
  5. RedisCache实现原理

    /**
     * Cache adapter for Redis.
     *
     * @author Eduardo Macarron
     */
    public final class RedisCache implements Cache {
    
      private final ReadWriteLock readWriteLock = new DummyReadWriteLock();
    
      private String id;
    
      private static JedisPool pool;
    
      public RedisCache(final String id) {
        if (id == null) {
          throw new IllegalArgumentException("Cache instances require an ID");
        }
        this.id = id;
        RedisConfig redisConfig = RedisConfigurationBuilder.getInstance().parseConfiguration();
    	pool = new JedisPool(redisConfig, redisConfig.getHost(), redisConfig.getPort(),
    			redisConfig.getConnectionTimeout(), redisConfig.getSoTimeout(), redisConfig.getPassword(),
    			redisConfig.getDatabase(), redisConfig.getClientName());
      }
    
      private Object execute(RedisCallback callback) {
        Jedis jedis = pool.getResource();
        try {
          return callback.doWithRedis(jedis);
        } finally {
          jedis.close();
        }
      }
    
      @Override
      public String getId() {
        return this.id;
      }
    
      @Override
      public int getSize() {
        return (Integer) execute(new RedisCallback() {
          @Override
          public Object doWithRedis(Jedis jedis) {
            Map<byte[], byte[]> result = jedis.hgetAll(id.toString().getBytes());
            return result.size();
          }
        });
      }
    
      @Override
      public void putObject(final Object key, final Object value) {
        execute(new RedisCallback() {
          @Override
          public Object doWithRedis(Jedis jedis) {
            jedis.hset(id.toString().getBytes(), key.toString().getBytes(), SerializeUtil.serialize(value));
            return null;
          }
        });
      }
    
      @Override
      public Object getObject(final Object key) {
        return execute(new RedisCallback() {
          @Override
          public Object doWithRedis(Jedis jedis) {
            return SerializeUtil.unserialize(jedis.hget(id.toString().getBytes(), key.toString().getBytes()));
          }
        });
      }
    
      @Override
      public Object removeObject(final Object key) {
        return execute(new RedisCallback() {
          @Override
          public Object doWithRedis(Jedis jedis) {
            return jedis.hdel(id.toString(), key.toString());
          }
        });
      }
    
      @Override
      public void clear() {
        execute(new RedisCallback() {
          @Override
          public Object doWithRedis(Jedis jedis) {
            jedis.del(id.toString());
            return null;
          }
        });
    
      }
    
      @Override
      public ReadWriteLock getReadWriteLock() {
        return readWriteLock;
      }
    
      @Override
      public String toString() {
        return "Redis {" + id + "}";
      }
    
    }
    
    
    /**
     * Converter from the Config to a proper {@link RedisConfig}.
     *
     * @author Eduardo Macarron
     */
    final class RedisConfigurationBuilder {
    
    	/**
    	 * This class instance.
    	 */
    	private static final RedisConfigurationBuilder INSTANCE = new RedisConfigurationBuilder();
    
    	private static final String SYSTEM_PROPERTY_REDIS_PROPERTIES_FILENAME = "redis.properties.filename";
    
    	private static final String REDIS_RESOURCE = "redis.properties";
    
    	private final String redisPropertiesFilename;
    
    	/**
    	 * Hidden constructor, this class can't be instantiated.
    	 */
    	private RedisConfigurationBuilder() {
    		redisPropertiesFilename = System.getProperty(SYSTEM_PROPERTY_REDIS_PROPERTIES_FILENAME, REDIS_RESOURCE);
    	}
    
    	/**
    	 * Return this class instance.
    	 *
    	 * @return this class instance.
    	 */
    	public static RedisConfigurationBuilder getInstance() {
    		return INSTANCE;
    	}
    
    	/**
    	 * Parses the Config and builds a new {@link RedisConfig}.
    	 *
    	 * @return the converted {@link RedisConfig}.
    	 */
    	public RedisConfig parseConfiguration() {
    		return parseConfiguration(getClass().getClassLoader());
    	}
    
    	/**
    	 * Parses the Config and builds a new {@link RedisConfig}.
    	 *
    	 * @param the
    	 *            {@link ClassLoader} used to load the
    	 *            {@code memcached.properties} file in classpath.
    	 * @return the converted {@link RedisConfig}.
    	 */
    	public RedisConfig parseConfiguration(ClassLoader classLoader) {
    		Properties config = new Properties();
    
    		InputStream input = classLoader.getResourceAsStream(redisPropertiesFilename);
    		if (input != null) {
    			try {
    				config.load(input);
    			} catch (IOException e) {
    				throw new RuntimeException(
    						"An error occurred while reading classpath property '"
    								+ redisPropertiesFilename
    								+ "', see nested exceptions", e);
    			} finally {
    				try {
    					input.close();
    				} catch (IOException e) {
    					// close quietly
    				}
    			}
    		}
    
    		RedisConfig jedisConfig = new RedisConfig();
    		setConfigProperties(config, jedisConfig);
    		return jedisConfig;
    	}
    
    	private void setConfigProperties(Properties properties,
    			RedisConfig jedisConfig) {
    		if (properties != null) {
    			MetaObject metaCache = SystemMetaObject.forObject(jedisConfig);
    			for (Map.Entry<Object, Object> entry : properties.entrySet()) {
    				String name = (String) entry.getKey();
    				String value = (String) entry.getValue();
    				if (metaCache.hasSetter(name)) {
    					Class<?> type = metaCache.getSetterType(name);
    					if (String.class == type) {
    						metaCache.setValue(name, value);
    					} else if (int.class == type || Integer.class == type) {
    						metaCache.setValue(name, Integer.valueOf(value));
    					} else if (long.class == type || Long.class == type) {
    						metaCache.setValue(name, Long.valueOf(value));
    					} else if (short.class == type || Short.class == type) {
    						metaCache.setValue(name, Short.valueOf(value));
    					} else if (byte.class == type || Byte.class == type) {
    						metaCache.setValue(name, Byte.valueOf(value));
    					} else if (float.class == type || Float.class == type) {
    						metaCache.setValue(name, Float.valueOf(value));
    					} else if (boolean.class == type || Boolean.class == type) {
    						metaCache.setValue(name, Boolean.valueOf(value));
    					} else if (double.class == type || Double.class == type) {
    						metaCache.setValue(name, Double.valueOf(value));
    					} else {
    						throw new CacheException("Unsupported property type: '"
    								+ name + "' of type " + type);
    					}
    				}
    			}
    		}
    	}
    
    }
    
    

    在这里插入图片描述

  6. 小结

    1. 二级缓存使用redis可以实现分布式缓存
    2. 自定义实现二级缓存,通过实现Cache接口
    3. RedisCache通过RedisConfigurationBuilder加载redis.properties中的配置
    4. RedisCache使用了JedisPool
    5. RedisCache使用了模板方法,完成putObjectgetObjectremoveObjectclear等操作

08.MyBatis插件

Mybatis插件原理

  1. 注册插件

    //XMLConfigBuilder类的parseConfiguration方法 (解析SqlMapConfig.xml时)
    private void parseConfiguration(XNode root) {
        try {
          // issue #117 read properties first
          propertiesElement(root.evalNode("properties"));
          Properties settings = settingsAsProperties(root.evalNode("settings"));
          loadCustomVfs(settings);
          loadCustomLogImpl(settings);
          typeAliasesElement(root.evalNode("typeAliases"));
          //解析插件  
          pluginElement(root.evalNode("plugins"));
          objectFactoryElement(root.evalNode("objectFactory"));
          objectWrapperFactoryElement(root.evalNode("objectWrapperFactory"));
          reflectorFactoryElement(root.evalNode("reflectorFactory"));
          settingsElement(settings);
          // read it after objectFactory and objectWrapperFactory issue #631
          environmentsElement(root.evalNode("environments"));
          databaseIdProviderElement(root.evalNode("databaseIdProvider"));
          typeHandlerElement(root.evalNode("typeHandlers"));
          mapperElement(root.evalNode("mappers"));
        } catch (Exception e) {
          throw new BuilderException("Error parsing SQL Mapper Configuration. Cause: " + e, e);
        }
      }
    
      private void pluginElement(XNode parent) throws Exception {
        if (parent != null) {
          for (XNode child : parent.getChildren()) {
            String interceptor = child.getStringAttribute("interceptor");
            Properties properties = child.getChildrenAsProperties();
            Interceptor interceptorInstance = (Interceptor) resolveClass(interceptor).getDeclaredConstructor().newInstance();
            interceptorInstance.setProperties(properties);
            //注册插件
            configuration.addInterceptor(interceptorInstance);
          }
        }
      }
    
      public void addInterceptor(Interceptor interceptor) {
        interceptorChain.addInterceptor(interceptor);
      }
    
    public class InterceptorChain {
      //插件列表
      private final List<Interceptor> 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<Interceptor> getInterceptors() {
        return Collections.unmodifiableList(interceptors);
      }
    
    }
    
    • 通过Bean注入的方式注册拦截器的方式代码不在此处
  2. 应用插件

    //Configuration类中
      public ParameterHandler newParameterHandler(MappedStatement mappedStatement, Object parameterObject, BoundSql boundSql) {
        ParameterHandler parameterHandler = mappedStatement.getLang().createParameterHandler(mappedStatement, parameterObject, boundSql);
          //应用插件,拦截ParameterHandler
        parameterHandler = (ParameterHandler) interceptorChain.pluginAll(parameterHandler);
        return parameterHandler;
      }
    
      public ResultSetHandler newResultSetHandler(Executor executor, MappedStatement mappedStatement, RowBounds rowBounds, ParameterHandler parameterHandler,
          ResultHandler resultHandler, BoundSql boundSql) {
        ResultSetHandler resultSetHandler = new DefaultResultSetHandler(executor, mappedStatement, parameterHandler, resultHandler, boundSql, rowBounds);
          //应用插件,拦截ResultSetHandler
        resultSetHandler = (ResultSetHandler) interceptorChain.pluginAll(resultSetHandler);
        return resultSetHandler;
      }
    
      public StatementHandler newStatementHandler(Executor executor, MappedStatement mappedStatement, Object parameterObject, RowBounds rowBounds, ResultHandler resultHandler, BoundSql boundSql) {
        StatementHandler statementHandler = new RoutingStatementHandler(executor, mappedStatement, parameterObject, rowBounds, resultHandler, boundSql);
          //应用插件,拦截StatementHandler
        statementHandler = (StatementHandler) interceptorChain.pluginAll(statementHandler);
        return statementHandler;
      }
    
      public Executor newExecutor(Transaction transaction) {
        return newExecutor(transaction, defaultExecutorType);
      }
    
      public Executor newExecutor(Transaction transaction, ExecutorType executorType) {
        executorType = executorType == null ? defaultExecutorType : executorType;
        executorType = executorType == null ? ExecutorType.SIMPLE : executorType;
        Executor executor;
        if (ExecutorType.BATCH == executorType) {
          executor = new BatchExecutor(this, transaction);
        } else if (ExecutorType.REUSE == executorType) {
          executor = new ReuseExecutor(this, transaction);
        } else {
          executor = new SimpleExecutor(this, transaction);
        }
        if (cacheEnabled) {
          executor = new CachingExecutor(executor);
        }
          //应用插件,拦截Executor
        executor = (Executor) interceptorChain.pluginAll(executor);
        return executor;
      }
    
    public class InterceptorChain {
    
      private final List<Interceptor> interceptors = new ArrayList<>();
      //应用拦截器,对目标对象进行拦截。target为ParameterHandler、ResultSetHandler、StatementHandler、Executor
      public Object pluginAll(Object target) {
        for (Interceptor interceptor : interceptors) {
          //链式拦截
          //遍历所有插件,对target进行拦截后,返回值为target,target将被下一个拦截器拦截
          target = interceptor.plugin(target);
        }
        return target;
      }
    }
    
    /**
     * @author Clinton Begin
     */
    public interface Interceptor {
    
      Object intercept(Invocation invocation) throws Throwable;
      //拦截器拦截
      default Object plugin(Object target) {
          //拦截器拦截
        return Plugin.wrap(target, this);
      }
    
      default void setProperties(Properties properties) {
        // NOP
      }
    
    }
    
  3. 生成插件拦截代理类

    //Plugin类中  
    public static Object wrap(Object target, Interceptor interceptor) {
          //1.构建签名映射表signatureMap。
          //记录拦截器所关注的方法签名及其对应的拦截逻辑
        Map<Class<?>, Set<Method>> signatureMap = getSignatureMap(interceptor);
          //2.确定目标对象接口
        Class<?> type = target.getClass();
          //找出所有需要被代理的接口
        Class<?>[] interfaces = getAllInterfaces(type, signatureMap);
          //3.创建代理对象
        if (interfaces.length > 0) {
            //该类,在signatureMap中有需要被拦截的方法才生成代理类
          return Proxy.newProxyInstance(
              type.getClassLoader(),
              interfaces,
              new Plugin(target, interceptor, signatureMap));
        }
        return target;
      }
    
    • 构建签名映射表
      • K:StatementHandler.class、Executor.class、ParameterHandler.class、ResultSetHandler.class
      • V:update, query, flushStatements, commit, rollback…等
    //Plugin类中  
    private static Map<Class<?>, Set<Method>> getSignatureMap(Interceptor interceptor) {
        Intercepts interceptsAnnotation = interceptor.getClass().getAnnotation(Intercepts.class);
        if (interceptsAnnotation == null) {
          throw new PluginException("No @Intercepts annotation was found in interceptor " + interceptor.getClass().getName());
        }
          //1.提取@Intercepts注解中的签名数组
        Signature[] sigs = interceptsAnnotation.value();
        Map<Class<?>, Set<Method>> signatureMap = new HashMap<>();
        for (Signature sig : sigs) {
          Set<Method> methods = MapUtil.computeIfAbsent(signatureMap, sig.type(), k -> new HashSet<>());
          try {
              //2.获取拦截的类的方法。该方法是最终被拦截的方法
            Method method = sig.type().getMethod(sig.method(), sig.args());
            methods.add(method);
          } catch (NoSuchMethodException e) {
            throw new PluginException("Could not find method on " + sig.type() + " named " + sig.method() + ". Cause: " + e, e);
          }
        }
        return signatureMap;
      }
    

    找出所有需要被代理的接口

    //Plugin类中
    private static Class<?>[] getAllInterfaces(Class<?> type, Map<Class<?>, Set<Method>> signatureMap) {
      Set<Class<?>> interfaces = new HashSet<>();
      while (type != null) {
        for (Class<?> c : type.getInterfaces()) {
          if (signatureMap.containsKey(c)) {
            interfaces.add(c);
          }
        }
        type = type.getSuperclass();
      }
      return interfaces.toArray(new Class<?>[0]);
    }
    
  4. 执行拦截

    //Plugin是一个InvocationHandler
    public class Plugin implements InvocationHandler {
    
      private final Object target;
      private final Interceptor interceptor;
      private final Map<Class<?>, Set<Method>> signatureMap;
    
      private Plugin(Object target, Interceptor interceptor, Map<Class<?>, Set<Method>> signatureMap) {
        this.target = target;
        this.interceptor = interceptor;
        this.signatureMap = signatureMap;
      }
    
      //执行拦截
      @Override
      public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
        try {
          Set<Method> methods = signatureMap.get(method.getDeclaringClass());
            //1.找到被拦截的方法
          if (methods != null && methods.contains(method)) {
              //2.执行拦截逻辑
              //interceptor.intercept方法由用户实现
            return interceptor.intercept(new Invocation(target, method, args));
          }
          return method.invoke(target, args);
        } catch (Exception e) {
          throw ExceptionUtil.unwrapThrowable(e);
        }
      }
    }  
    
  5. 小结

    1. 被拦截的类和方法

      拦截的类拦截的方法
      Executorupdate, query, flushStatements, commit, rollback,getTransaction, close, isClosed
      ParameterHandlergetParameterObject, setParameters
      StatementHandlerprepare, parameterize, batch, update, query
      ResultSetHandlerhandleResultSets, handleOutputParameters
    2. 代码执行链路

      parseConfiguration-解析SqlMapConfig.xml
      pluginElement-解析插件
      configuration.addInterceptor-注册拦截器
      interceptorChain.addInterceptor-加入拦截器列表
      InterceptorChain.pluginAll-应用全部插件
      Interceptor.plugin-组装递归式插件链
      Plugin.wrap-组装单个插件
      interceptorChain.pluginAll-拦截parameterHandler
      interceptorChain.pluginAll-拦截resultSetHandler
      interceptorChain.pluginAll-拦截statementHandler
      interceptorChain.pluginAll-拦截executor
      getSignatureMap-构建签名映射表
      getAllInterfaces-需要被代理的接口
      Proxy.newProxyInstance-创建代理
      interceptsAnnotation.value-提取Intercepts注解中的签名数组
      getMethod-获取拦截的类的方法.该方法是最终被拦截的方法
      Plugin.invoke-触发代理类拦截
      interceptor.intercept-用户自定义的插件拦截器方法
    3. 核心类图

      在这里插入图片描述

      • Interceptor:拦截器,定义了拦截逻辑,以及拦截器链的注册。
      • InterceptorChain:拦截器链,将多个拦截器组成拦截器链。
      • Plugin:Plugin 则是一种利用 Interceptor 实现的插件机制,它可以对指定的目标对象进行代理,并在方法调用前后插入额外的逻辑
      • Invocation:封装了被拦截方法的信息,包括被拦截方法的对象,被拦截方法的方法,被拦截方法的参数。
      • Plugin 是基于 Interceptor 实现的插件机制,而 Interceptor 是实现拦截器功能的接口。

自定义插件

  1. MyBaits的Interceptor定义

    public interface Interceptor {
    
      Object intercept(Invocation invocation) throws Throwable;
    
      default Object plugin(Object target) {
        return Plugin.wrap(target, this);
      }
    
      default void setProperties(Properties properties) {
        // NOP
      }
    
    }
    
    • intercept方法:核心方法,最终会被调用,执行自定义的拦截逻辑。
    • plugin方法:⽣成target的代理对象
    • setProperties方法:传递插件所需参数,插件初始化的时候调⽤,也只调⽤⼀次
  2. 自定义Interceptor实现类

    @Intercepts({//注意看这个⼤花括号,也就这说这⾥可以定义多个@Signature对多个地⽅拦截,都⽤这个拦截器
            @Signature(type = StatementHandler.class,//这是指拦截哪个接⼝
                    method = "prepare",//这个接⼝内的哪个⽅法名,不要拼错了
                    args = {Connection.class, Integer.class})// 这是拦截的⽅法的⼊参,按顺序写到这,不要多也不要少,如果⽅法重载,可是要通过⽅法名和⼊参来确定唯⼀的
    })
    public class MyPlugin implements Interceptor {
    
        /*
            拦截方法:只要被拦截的目标对象的目标方法被执行时,每次都会执行intercept方法
         */
        @Override
        public Object intercept(Invocation invocation) throws Throwable {
            System.out.println("对方法进行了增强....");
            return invocation.proceed(); //原方法执行
        }
    
    
        /**
         * 包装⽬标对象 为⽬标对象创建代理对象,会将当前Interceptor也包装进去
         *
         * @param target 要拦截的对象
         * @return 代理对象
         */
        @Override
        public Object plugin(Object target) {
            Object wrap = Plugin.wrap(target, this);
            return wrap;
        }
    
        /*
            获取配置文件的参数
            插件初始化的时候调⽤,也只调⽤⼀次,插件配置的属性从这⾥设置进来
         */
        @Override
        public void setProperties(Properties properties) {
            System.out.println("获取到的配置文件的参数是:" + properties);
        }
    }
    
  3. sqlMapConfig.xml中配置插件,启用自定义的Interceptor实现类

        <plugins>
           <plugin interceptor="com.muchfish.plugin.MyPlugin">
               <property name="name" value="tom"/>
           </plugin>
        </plugins>
    
  4. 测试

    public class PluginTest {
    
        @Test
        public void test() throws IOException {
            InputStream resourceAsStream =
                    Resources.getResourceAsStream("sqlMapConfig.xml");
            SqlSessionFactory sqlSessionFactory = new
                    SqlSessionFactoryBuilder().build(resourceAsStream);
            SqlSession sqlSession = sqlSessionFactory.openSession();
            IUserDao userMapper = sqlSession.getMapper(IUserDao.class);
            List<User> byPaging = userMapper.findAllUserAndRole();
            for (User user : byPaging) {
                System.out.println(user);
            }
        }
    }
    

pageHelper分⻚插件

  1. 导⼊通⽤PageHelper的坐标

            <dependency>
                <groupId>com.github.pagehelper</groupId>
                <artifactId>pagehelper</artifactId>
                <version>3.7.5</version>
            </dependency>
            <dependency>
                <groupId>com.github.jsqlparser</groupId>
                <artifactId>jsqlparser</artifactId>
                <version>0.9.1</version>
            </dependency>
    
  2. 在mybatis核⼼配置⽂件中配置PageHelper插件

        <plugins>
         <!--   <plugin interceptor="com.muchfish.plugin.MyPlugin">
                <property name="name" value="tom"/>
            </plugin>-->
            <!--注意:分⻚助⼿的插件 配置在通⽤馆mapper之前*-->
            <plugin interceptor="com.github.pagehelper.PageHelper">
                <property name="dialect" value="mysql"/>
            </plugin>
    
        </plugins>
    
  3. 测试分⻚数据获取

        @Test
        public void testPageHelper() throws IOException {
            InputStream resourceAsStream =
                    Resources.getResourceAsStream("sqlMapConfig.xml");
            SqlSessionFactory sqlSessionFactory = new
                    SqlSessionFactoryBuilder().build(resourceAsStream);
            SqlSession sqlSession = sqlSessionFactory.openSession();
            IUserDao userMapper = sqlSession.getMapper(IUserDao.class);
            //设置分⻚参数
            PageHelper.startPage(1, 2);
            List<User> select = userMapper.findAllUserAndRole();
            for (User user : select) {
                System.out.println(user);
            }
        }
    

通⽤ mapper

基于插件实现单表增删改查?

  1. 导⼊通⽤tk.mybatis的坐标

    <!--通用mapper-->
    <dependency>
        <groupId>tk.mybatis</groupId>
        <artifactId>mapper</artifactId>
        <version>3.1.2</version>
    </dependency>
    
  2. 在mybatis核⼼配置⽂件中配置tk.mybatis插件

        <plugins>
            <plugin interceptor="com.muchfish.plugin.MyPlugin">
                <property name="name" value="tom"/>
            </plugin>
            <!--注意:分⻚助⼿的插件 配置在通⽤馆mapper之前*-->
            <plugin interceptor="com.github.pagehelper.PageHelper">
                <property name="dialect" value="mysql"/>
            </plugin>
            <plugin interceptor="tk.mybatis.mapper.mapperhelper.MapperInterceptor">
                <!--指定当前通用mapper接口使用的是哪一个-->
                <property name="mappers" value="tk.mybatis.mapper.common.Mapper"/>
            </plugin>
        </plugins>
    
  3. 实体类设置主键

    @Table(name = "user")
    public class UserDO {
    
        @Id
        @GeneratedValue(strategy = GenerationType.IDENTITY)
        private Integer id;
        private String username;
        private String password;
        private String birthday;
        
        //...省略getter/setter
    }    
    
  4. 定义Dao继承Mapper类

    import com.muchfish.pojo.UserDO;
    import tk.mybatis.mapper.common.Mapper;
    
    public interface UserMapper extends Mapper<UserDO> {
    }
    
  5. 测试

        @Test
        public void mapperTest() throws IOException {
            InputStream resourceAsStream = Resources.getResourceAsStream("sqlMapConfig.xml");
            SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(resourceAsStream);
            SqlSession sqlSession = sqlSessionFactory.openSession();
            UserMapper mapper = sqlSession.getMapper(UserMapper.class);
            UserDO user = new UserDO();
            user.setId(1);
            UserDO user1 = mapper.selectOne(user);
            System.out.println(user1);
    
    
            //2.example方法
            Example example = new Example(User.class);
            example.createCriteria().andEqualTo("id",1);
            List<UserDO> users = mapper.selectByExample(example);
            for (UserDO user2 : users) {
                System.out.println(user2);
            }
    
        }
    
  6. 通用Mapper拦截器部分源码

    /**
     * 通用Mapper拦截器
     *
     * @author liuzh
     */
    @Intercepts({
            @Signature(type = Executor.class, method = "query", args = {MappedStatement.class, Object.class, RowBounds.class, ResultHandler.class}),
            @Signature(type = Executor.class, method = "update", args = {MappedStatement.class, Object.class})
    })
    public class MapperInterceptor implements Interceptor {
    
        private final MapperHelper mapperHelper = new MapperHelper();
    
        @Override
        public Object intercept(Invocation invocation) throws Throwable {
            Object[] objects = invocation.getArgs();
            MappedStatement ms = (MappedStatement) objects[0];
            String msId = ms.getId();
            //不需要拦截的方法直接返回
            if (mapperHelper.isMapperMethod(msId)) {
                //第一次经过处理后,就不会是ProviderSqlSource了,一开始高并发时可能会执行多次,但不影响。以后就不会在执行了
                if (ms.getSqlSource() instanceof ProviderSqlSource) {
                    mapperHelper.setSqlSource(ms);
                }
            }
            return invocation.proceed();
        }
    
        @Override
        public Object plugin(Object target) {
            if (target instanceof Executor) {
                return Plugin.wrap(target, this);
            } else {
                return target;
            }
        }
    
        @Override
        public void setProperties(Properties properties) {
            mapperHelper.setProperties(properties);
        }
    }
    
    
      
    // MapperHelper类中
        /**
         * 注册的通用Mapper接口
         */
        private Map<Class<?>, MapperTemplate> registerMapper = new ConcurrentHashMap<Class<?>, MapperTemplate>();
    
    	/**
         * 判断当前的接口方法是否需要进行拦截
         *
         * @param msId
         * @return
         */
        public boolean isMapperMethod(String msId) {
            if (msIdSkip.get(msId) != null) {
                return msIdSkip.get(msId);
            }
            for (Map.Entry<Class<?>, MapperTemplate> entry : registerMapper.entrySet()) {
                if (entry.getValue().supportMethod(msId)) {
                    msIdSkip.put(msId, true);
                    return true;
                }
            }
            msIdSkip.put(msId, false);
            return false;
        }
    
      // MapperHelper类中
        /**
         * 注册的通用Mapper接口
         */
        private Map<Class<?>, MapperTemplate> registerMapper = new ConcurrentHashMap<Class<?>, MapperTemplate>();
    
     /**
     * 配置属性
     *
     * @param properties
     */
    public void setProperties(Properties properties) {
        if (properties == null) {
            return;
        }
        String UUID = properties.getProperty("UUID");
        if (UUID != null && UUID.length() > 0) {
            setUUID(UUID);
        }
      	//...省略
        //注册通用接口
        String mapper = properties.getProperty("mappers");
        if (mapper != null && mapper.length() > 0) {
            String[] mappers = mapper.split(",");
            for (String mapperClass : mappers) {
                if (mapperClass.length() > 0) {
                    registerMapper(mapperClass);
                }
            }
        }
    }
    
        /**
         * 注册通用Mapper接口
         *
         * @param mapperClass
         * @throws Exception
         */
        public void registerMapper(Class<?> mapperClass) {
            if (!registerMapper.containsKey(mapperClass)) {
                registerMapper.put(mapperClass, fromMapperClass(mapperClass));
            }
            //自动注册继承的接口
            Class<?>[] interfaces = mapperClass.getInterfaces();
            if (interfaces != null && interfaces.length > 0) {
                for (Class<?> anInterface : interfaces) {
                    registerMapper(anInterface);
                }
            }
        }
    
    • 只增强了Executor
    • MapperInterceptor.setProperties进行了mapper注册
    • 会在注册的mapper中进行匹配,判断是否对该MappedStatementId进行拦截
  • 14
    点赞
  • 14
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

摸魚散人

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

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

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

打赏作者

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

抵扣说明:

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

余额充值