mybatis execution principle
Core class
-
SqlSessionFactoryBuilder Mainly used for reading mybatis Profiles, creating SqlSessionFactory object
-
DefaultSqlSessionFactory SqlSessionFactory The default implementation class of the interface, holding Configuration,For obtaining SqlSession
-
Configuration hold MapperRegistry,And cache stateMentId reach MappedStatement Mapping relationship of
-
MapperRegistry hold Configuration Reference and cache Class To specific MapperProxyFactory Mapping of
-
MapperProxyFactory establish mapper The factory of the proxy object. The corresponding data needs to be passed in the construction method mapper Interface and cache Method reach MapperMethod Mapping of
-
MapperProxy realization InvocationHandler Interface, invoke Call in method sqlsession Complete database operation
-
MapperMethod Hold specific sql Statement and method related parameters, execute the corresponding sql sentence
-
DefaultSqlSession hold Configuration References and Executor Reference, call Executor implement sql
-
Executor The default implementation class is SimpleExecutor,Complete the initialization of database connection in it, and complete jdbc operation
Initialization process
- SqlSessionFactoryBuilder loads the mybatis Configuration, creates the Configuration object, and completes the initialization of MapperRegistry
- Call the build method to create the DefaultSqlSessionFactory.
Call process
-
Call the openSession method of SqlSessionFactory to obtain the SqlSession object
-
Call the getMapper method of SqlSession to obtain the proxy object. The internal call logic is as follows
DefaultSqlSession public <T> T getMapper(Class<T> type) { return configuration.<T>getMapper(type, this); } Configuration public <T> T getMapper(Class<T> type, SqlSession sqlSession) { return mapperRegistry.getMapper(type, sqlSession); } MapperRegistry public <T> T getMapper(Class<T> type, SqlSession sqlSession) { final MapperProxyFactory<T> mapperProxyFactory = (MapperProxyFactory<T>) knownMappers.get(type); if (mapperProxyFactory == null) { throw new BindingException("Type " + type + " is not known to the MapperRegistry."); } try { return mapperProxyFactory.newInstance(sqlSession); } catch (Exception e) { throw new BindingException("Error getting mapper instance. Cause: " + e, e); } } MapperProxyFactory public T newInstance(SqlSession sqlSession) { final MapperProxy<T> mapperProxy = new MapperProxy<T>(sqlSession, mapperInterface, methodCache); return newInstance(mapperProxy); } protected T newInstance(MapperProxy<T> mapperProxy) { return (T) Proxy.newProxyInstance(mapperInterface.getClassLoader(), new Class[] { mapperInterface }, mapperProxy); }
-
Call the interface method of the obtained proxy object. The specific call logic is as follows
MapperProxy 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); } MapperMethod public Object execute(SqlSession sqlSession, Object[] args) { Object result; switch (command.getType()) { case INSERT: { Object param = method.convertArgsToSqlCommandParam(args); result = rowCountResult(sqlSession.insert(command.getName(), param)); break; } //Omit code such as update and select default: throw new BindingException("Unknown execution method for: " + command.getName()); } if (result == null && method.getReturnType().isPrimitive() && !method.returnsVoid()) { throw new BindingException("Mapper method '" + command.getName() + " attempted to return null from a method with a primitive return type (" + method.getReturnType() + ")."); } return result; } DefaultSqlSession public int insert(String statement, Object parameter) { return update(statement, parameter); } public int update(String statement, Object parameter) { try { dirty = true; MappedStatement ms = configuration.getMappedStatement(statement); return executor.update(ms, wrapCollection(parameter)); } catch (Exception e) { throw ExceptionFactory.wrapException("Error updating database. Cause: " + e, e); } finally { ErrorContext.instance().reset(); } } executor.update Then it is the original jdbc logic
Write a simplified version of mybatis
Define SqlSessionFactoryBuilder, SqlSessionFactory and Configuration classes
public class SqlSessionFactoryBuilder { public SqlSessionFactory build(String fileName) { InputStream inputStream = SqlSessionFactoryBuilder.class.getClassLoader().getResourceAsStream(fileName); return build(inputStream); } public SqlSessionFactory build(InputStream inputStream) { try { Configuration.PROPS.load(inputStream); } catch (IOException e) { e.printStackTrace(); } return new DefaultSqlSessionFactory(new Configuration()); } }
public class DefaultSqlSessionFactory implements SqlSessionFactory { private final Configuration configuration; public DefaultSqlSessionFactory(Configuration configuration) { this.configuration = configuration; loadMappersInfo(Configuration.getProperty(Constant.MAPPER_LOCATION).replaceAll("\\.", "/")); } @Override public SqlSession openSession() { SqlSession session = new DefaultSqlSession(this.configuration); return session; } private void loadMappersInfo(String dirName) { URL resources = DefaultSqlSessionFactory.class.getClassLoader().getResource(dirName); File mappersDir = new File(resources.getFile()); if (mappersDir.isDirectory()) { // Display all files under the package File[] mappers = mappersDir.listFiles(); if (CommonUtil.isNotEmpty(mappers)) { for (File file : mappers) { // Continue recursion on folders if (file.isDirectory()) { loadMappersInfo(dirName + "/" + file.getName()); } else if (file.getName().endsWith(Constant.MAPPER_FILE_SUFFIX)) { // Parsing only XML files XmlUtil.readMapperXml(file, this.configuration); } } } } } }
public class Configuration { /** * Configuration item */ public static Properties PROPS = new Properties(); /** * mapper Proxy registrar */ protected final MapperRegistry mapperRegistry = new MapperRegistry(); /** * mapper id and SQL statement attribute of the select/update statement of the file **/ protected final Map<String, MappedStatement> mappedStatements = new HashMap<>(); /** * Gets a character type attribute (the default value is an empty string) * * @param key * @return */ public static String getProperty(String key) { return getProperty(key, ""); } /** * Get character type properties (default values can be specified) * * @param key * @param defaultValue Default value * @return */ public static String getProperty(String key, String defaultValue) { return PROPS.containsKey(key) ? PROPS.getProperty(key) : defaultValue; } /** * addMapper */ public <T> void addMapper(Class<T> type) { this.mapperRegistry.addMapper(type); } /** * getMapper */ public <T> T getMapper(Class<T> type, SqlSession sqlSession) { return this.mapperRegistry.getMapper(type, sqlSession); } /** * addMappedStatement */ public void addMappedStatement(String key, MappedStatement mappedStatement) { this.mappedStatements.put(key, mappedStatement); } /** * Get MappedStatement */ public MappedStatement getMappedStatement(String id) { return this.mappedStatements.get(id); } }
Define the built-in objects that are dependent in the configuration. MapperProxy is the key for mybatis to generate proxy objects
public class MapperRegistry { /** * the knownMappers */ private final Map<Class<?>, MapperProxyFactory<?>> knownMappers = new HashMap<>(); /** * Registered agent factory * * @param type */ public <T> void addMapper(Class<T> type) { this.knownMappers.put(type, new MapperProxyFactory<T>(type)); } /** * Get agent factory instance * * @param type * @param sqlSession * @return */ @SuppressWarnings("unchecked") public <T> T getMapper(Class<T> type, SqlSession sqlSession) { MapperProxyFactory<T> mapperProxyFactory = (MapperProxyFactory<T>) this.knownMappers.get(type); return mapperProxyFactory.newInstance(sqlSession); } }
public class MapperProxyFactory<T> { private final Class<T> mapperInterface; /** * Initialization method * * @param mapperInterface */ public MapperProxyFactory(Class<T> mapperInterface) { this.mapperInterface = mapperInterface; } /** * Create a proxy based on sqlSession * * @param sqlSession * @return * @see */ public T newInstance(SqlSession sqlSession) { MapperProxy<T> mapperProxy = new MapperProxy<T>(sqlSession, this.mapperInterface); return newInstance(mapperProxy); } /** * Returns an instance based on the mapper proxy * * @param mapperProxy * @return * @see */ @SuppressWarnings("unchecked") protected T newInstance(MapperProxy<T> mapperProxy) { return (T) Proxy.newProxyInstance(this.mapperInterface.getClassLoader(), new Class[]{this.mapperInterface}, mapperProxy); } }
public class MapperProxy<T> implements InvocationHandler, Serializable { private static final long serialVersionUID = -7861758496991319661L; private final SqlSession sqlSession; private final Class<T> mapperInterface; /** * Construction method * * @param sqlSession * @param mapperInterface */ public MapperProxy(SqlSession sqlSession, Class<T> mapperInterface) { this.sqlSession = sqlSession; this.mapperInterface = mapperInterface; } /** * Real execution method * * @param proxy * @param method * @param args * @return * @throws Throwable */ @Override public Object invoke(Object proxy, Method method, Object[] args) throws Throwable { try { if (Object.class.equals(method.getDeclaringClass())) { return method.invoke(this, args); } return this.execute(method, args); } catch (Exception e) { e.printStackTrace(); } return null; } /** * Perform corresponding operations according to SQL instructions * */ private Object execute(Method method, Object[] args) { String statementId = this.mapperInterface.getName() + "." + method.getName(); MappedStatement ms = this.sqlSession.getConfiguration().getMappedStatement(statementId); Object result = null; switch (ms.getSqlCommandType()) { case SELECT: { Class<?> returnType = method.getReturnType(); // If a list is returned, the method of querying multiple results should be called. Otherwise, just query a single record if (Collection.class.isAssignableFrom(returnType)) { //The ID is mapper class full name + method name result = sqlSession.selectList(statementId, args); } else { result = sqlSession.selectOne(statementId, args); } break; } case UPDATE: { sqlSession.update(statementId, args); break; } default: { break; } } return result; } }
Define the built-in objects that are dependent in the configuration. MapperProxy is the key for mybatis to generate proxy objects
public class DefaultSqlSession implements SqlSession { private final Configuration configuration; private final Executor executor; /** * Default construction method * * @param configuration */ public DefaultSqlSession(Configuration configuration) { this.configuration = configuration; this.executor = new SimpleExecutor(configuration); } /** * Query strip record */ @Override public <T> T selectOne(String statementId, Object parameter) { List<T> results = this.<T>selectList(statementId, parameter); return CommonUtil.isNotEmpty(results) ? results.get(0) : null; } /** * Query multiple records * * @param statementId ID mapper class full name + method name * @param parameter parameter list * @return */ @Override public <E> List<E> selectList(String statementId, Object parameter) { MappedStatement mappedStatement = this.configuration.getMappedStatement(statementId); return this.executor.<E>doQuery(mappedStatement, parameter); } /** * to update * * @param statementId * @param parameter */ @Override public void update(String statementId, Object parameter) { MappedStatement mappedStatement = this.configuration.getMappedStatement(statementId); this.executor.doUpdate(mappedStatement, parameter); } @Override public void insert(String statementId, Object parameter) { //TODO to be implemented } /** * Get Mapper */ @Override public <T> T getMapper(Class<T> type) { return configuration.<T>getMapper(type, this); } /** * getConfiguration * * @return */ @Override public Configuration getConfiguration() { return this.configuration; } }
public class SimpleExecutor implements Executor { /** * Database connection */ private static Connection connection; static { initConnect(); } private Configuration conf; /** * Initialization method * * @param configuration */ public SimpleExecutor(Configuration configuration) { this.conf = configuration; } /** * Statically initialize database connections * * @return */ private static void initConnect() { String driver = Configuration.getProperty(Constant.DB_DRIVER_CONF); String url = Configuration.getProperty(Constant.DB_URL_CONF); String username = Configuration.getProperty(Constant.DB_USERNAME_CONF); String password = Configuration.getProperty(Constant.db_PASSWORD); try { Class.forName(driver); connection = DriverManager.getConnection(url, username, password); System.out.println("Database connection succeeded"); } catch (Exception e) { e.printStackTrace(); } } /** * Query database according to parameters * * @param ms * @param parameter * @return */ @Override public <E> List<E> doQuery(MappedStatement ms, Object parameter) { try { //1. Get database connection Connection connection = getConnect(); //2. Get MappedStatement information, which contains sql information MappedStatement mappedStatement = conf.getMappedStatement(ms.getSqlId()); //3. Instantiate StatementHandler object, StatementHandler statementHandler = new SimpleStatementHandler(mappedStatement); //4. Obtain PreparedStatement through StatementHandler and connection PreparedStatement preparedStatement = statementHandler.prepare(connection); //5. Instantiate the ParameterHandler and add it to the SQL statement? Parameterization ParameterHandler parameterHandler = new DefaultParameterHandler(parameter); parameterHandler.setParameters(preparedStatement); //6. Execute SQL to get the result set ResultSet ResultSet resultSet = statementHandler.query(preparedStatement); //7. Instantiate ResultSetHandler and set the result in ResultSet to the target resultType object through reflection ResultSetHandler resultSetHandler = new DefaultResultSetHandler(mappedStatement); return resultSetHandler.handleResultSets(resultSet); } catch (Exception e) { e.printStackTrace(); } return null; } /** * doUpdate * * @param ms * @param parameter */ @Override public void doUpdate(MappedStatement ms, Object parameter) { try { //1. Get database connection Connection connection = getConnect(); //2. Get MappedStatement information, which contains sql information MappedStatement mappedStatement = conf.getMappedStatement(ms.getSqlId()); //3. Instantiate StatementHandler object, StatementHandler statementHandler = new SimpleStatementHandler(mappedStatement); //4. Obtain PreparedStatement through StatementHandler and connection PreparedStatement preparedStatement = statementHandler.prepare(connection); //5. Instantiate the ParameterHandler and add it to the SQL statement? Parameterization ParameterHandler parameterHandler = new DefaultParameterHandler(parameter); parameterHandler.setParameters(preparedStatement); //6. Execute SQL statementHandler.update(preparedStatement); } catch (Exception e) { e.printStackTrace(); } } /** * getConnect * * @return * @throws SQLException */ public Connection getConnect() throws SQLException { if (null != connection) { return connection; } else { throw new SQLException("Unable to connect to the database, please check the configuration"); } } }
Define the StatementHandler (sql preprocessing), ParameterHandler (setting parameters), ResultSetHandler (result mapping) that the Executor depends on
public class SimpleStatementHandler implements StatementHandler { /** * #{}Regular matching */ private static Pattern param_pattern = Pattern.compile("#\\{([^\\{\\}]*)\\}"); private MappedStatement mappedStatement; /** * Default construction method * * @param mappedStatement */ public SimpleStatementHandler(MappedStatement mappedStatement) { this.mappedStatement = mappedStatement; } /** * Replace #{} in the SQL statement with?, The source code is parsed in the SqlSourceBuilder class * * @param source * @return */ private static String parseSymbol(String source) { source = source.trim(); Matcher matcher = param_pattern.matcher(source); return matcher.replaceAll("?"); } @Override public PreparedStatement prepare(Connection paramConnection) throws SQLException { String originalSql = mappedStatement.getSql(); if (CommonUtil.isNotEmpty(originalSql)) { // Replace #{}, preprocess, prevent SQL injection return paramConnection.prepareStatement(parseSymbol(originalSql)); } else { throw new SQLException("original sql is null."); } } /** * query * * @param preparedStatement * @return * @throws SQLException */ @Override public ResultSet query(PreparedStatement preparedStatement) throws SQLException { return preparedStatement.executeQuery(); } /** * update * * @param preparedStatement * @throws SQLException */ @Override public void update(PreparedStatement preparedStatement) throws SQLException { preparedStatement.executeUpdate(); } }
public class DefaultParameterHandler implements ParameterHandler { private Object parameter; public DefaultParameterHandler(Object parameter) { this.parameter = parameter; } /** * Set the SQL parameter to PreparedStatement */ @Override public void setParameters(PreparedStatement ps) { try { if (null != parameter) { if (parameter.getClass().isArray()) { Object[] params = (Object[]) parameter; for (int i = 0; i < params.length; i++) { //Mapper ensures that the types of the incoming parameters match, so there is no type conversion here ps.setObject(i + 1, params[i]); } } } } catch (Exception e) { e.printStackTrace(); } } }
public class DefaultResultSetHandler implements ResultSetHandler { private final MappedStatement mappedStatement; /** * @param mappedStatement */ public DefaultResultSetHandler(MappedStatement mappedStatement) { this.mappedStatement = mappedStatement; } /** * Process the query results and set them to the returned entity class through reflection */ @SuppressWarnings("unchecked") @Override public <E> List<E> handleResultSets(ResultSet resultSet) { try { List<E> result = new ArrayList<>(); if (null == resultSet) { return null; } while (resultSet.next()) { // Instantiate the return class through reflection Class<?> entityClass = Class.forName(mappedStatement.getResultType()); E entity = (E) entityClass.newInstance(); Field[] declaredFields = entityClass.getDeclaredFields(); for (Field field : declaredFields) { // Assign values to member variables field.setAccessible(true); Class<?> fieldType = field.getType(); // At present, only string and int conversions are implemented if (String.class.equals(fieldType)) { field.set(entity, resultSet.getString(field.getName())); } else if (int.class.equals(fieldType) || Integer.class.equals(fieldType)) { field.set(entity, resultSet.getInt(field.getName())); } else { // Other types can be converted by themselves, which is directly set here field.set(entity, resultSet.getObject(field.getName())); } } result.add(entity); } return result; } catch (Exception e) { e.printStackTrace(); } return null; } }
The related dependencies and tool classes will not be posted, and the source code will be placed at the end of the article
mybatis advanced features
Plug in mechanism
mybatis adopts the responsibility chain mode, organizes multiple plug-ins through dynamic agents, and changes the default sql behavior through plug-ins. myabtis allows plug-ins to intercept four objects: Executor, ParameterHandler, ResultSetHandler and StatementHandler.
Related source code
//Create parameter processor public ParameterHandler newParameterHandler(MappedStatement mappedStatement, Object parameterObject, BoundSql boundSql) { //Create ParameterHandler ParameterHandler parameterHandler = mappedStatement.getLang().createParameterHandler(mappedStatement, parameterObject, boundSql); //The plug-in is inserted here parameterHandler = (ParameterHandler) interceptorChain.pluginAll(parameterHandler); return parameterHandler; } //Create result set processor public ResultSetHandler newResultSetHandler(Executor executor, MappedStatement mappedStatement, RowBounds rowBounds, ParameterHandler parameterHandler, ResultHandler resultHandler, BoundSql boundSql) { //Create defaultresultsethandler (the older version 3.1 is to create NestedResultSetHandler or FastResultSetHandler) ResultSetHandler resultSetHandler = new DefaultResultSetHandler(executor, mappedStatement, parameterHandler, resultHandler, boundSql, rowBounds); //The plug-in is inserted here resultSetHandler = (ResultSetHandler) interceptorChain.pluginAll(resultSetHandler); return resultSetHandler; } //Create statement processor public StatementHandler newStatementHandler(Executor executor, MappedStatement mappedStatement, Object parameterObject, RowBounds rowBounds, ResultHandler resultHandler, BoundSql boundSql) { //Create routing statement processor StatementHandler statementHandler = new RoutingStatementHandler(executor, mappedStatement, parameterObject, rowBounds, resultHandler, boundSql); //The plug-in is inserted here statementHandler = (StatementHandler) interceptorChain.pluginAll(statementHandler); return statementHandler; } public Executor newExecutor(Transaction transaction) { return newExecutor(transaction, defaultExecutorType); } //Generating actuator public Executor newExecutor(Transaction transaction, ExecutorType executorType) { //Determine the type of actuator used executorType = executorType == null ? defaultExecutorType : executorType; //This sentence is another protection to prevent careless people from setting defaultExecutorType to null? executorType = executorType == null ? ExecutorType.SIMPLE : executorType; Executor executor; //Then there are three simple branches, which produce three kinds of executors: batchexecutor / reuseexecution / simpleexecution 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 caching is required, another cacheingexecution is generated (there is caching by default), decorator mode, so cacheingexecution is returned by default if (cacheEnabled) { executor = new CachingExecutor(executor); } //The plug-in is called here. The behavior of the Executor can be changed through the plug-in executor = (Executor) interceptorChain.pluginAll(executor); return executor; }
Each interceptor proxies the target class once
/** *@target *@return Objects after layer proxy */ public Object pluginAll(Object target) { //Loop through each interceptor Plugin method for (Interceptor interceptor : interceptors) { target = interceptor.plugin(target); } return target; }
Interceptor interface description
public interface Interceptor { /** * Method of executing interception logic * * @param invocation Call information * @return Call result * @throws Throwable abnormal */ Object intercept(Invocation invocation) throws Throwable; /** * proxy class * * @param target * @return */ Object plugin(Object target); /** * Initialize the Interceptor method according to the configuration * @param properties */ void setProperties(Properties properties); }
Annotate interceptor and sign
@Intercepts(@Signature( type = StatementHandler.class, //Types of four objects to intercept method = "prepare", //Which method in the intercept object args = {Connection.class, Integer.class} //Parameters to be passed in ))
Handwriting paging plug-in
Passing paging parameters based on ThreadLocal to intercept StatementHandler
@Intercepts(@Signature( type = StatementHandler.class, method = "prepare", args = {Connection.class, Integer.class} )) public class PagePlugin implements Interceptor { // Core business of plug-ins @Override public Object intercept(Invocation invocation) throws Throwable { /** * 1,Get the original sql statement * 2,Modify the original sql and add paging select * from t_user limit 0,3 * 3,Total number of jdbc queries executed */ // Get our StatementHandler object from invocation StatementHandler statementHandler = (StatementHandler) invocation.getTarget(); // Get the original sql statement BoundSql boundSql = statementHandler.getBoundSql(); String sql = boundSql.getSql(); // statementHandler to metaObject MetaObject metaObject = SystemMetaObject.forObject(statementHandler); // spring context.getBean("userBean") MappedStatement mappedStatement = (MappedStatement) metaObject.getValue("delegate.mappedStatement"); // Gets the method name selectUserByPage in the mapper interface String mapperMethodName = mappedStatement.getId(); if (mapperMethodName.matches(".*ByPage")) { Page page = PageUtil.getPaingParam(); // select * from user; String countSql = "select count(0) from (" + sql + ") a"; System.out.println("Total number of queries sql : " + countSql); // Perform jdbc operations Connection connection = (Connection) invocation.getArgs()[0]; PreparedStatement countStatement = connection.prepareStatement(countSql); ParameterHandler parameterHandler = (ParameterHandler) metaObject.getValue("delegate.parameterHandler"); parameterHandler.setParameters(countStatement); ResultSet rs = countStatement.executeQuery(); if (rs.next()) { page.setTotalNumber(rs.getInt(1)); } rs.close(); countStatement.close(); // Modify sql limit String pageSql = this.generaterPageSql(sql, page); System.out.println("paging sql: " + pageSql); //Set the modified sql back metaObject.setValue("delegate.boundSql.sql", pageSql); } // Hand over the execution process to mybatis return invocation.proceed(); } // Add the customized plug-in to mybatis for execution @Override public Object plugin(Object target) { return Plugin.wrap(target, this); } // set a property @Override public void setProperties(Properties properties) { } // Generate sql with limit based on the original sql public String generaterPageSql(String sql, Page page) { StringBuffer sb = new StringBuffer(); sb.append(sql); sb.append(" limit " + page.getStartIndex() + " , " + page.getTotalSelect()); return sb.toString(); } }
Data @NoArgsConstructor public class Page { public Page(int currentPage,int pageSize){ this.currentPage=currentPage; this.pageSize=pageSize; } private int totalNumber;// Total number of entries in the current table private int currentPage;// The location of the current page private int totalPage; // PageCount private int pageSize = 3;// Page size private int startIndex; // Start of retrieval private int totalSelect;// Total number of retrieved public void setTotalNumber(int totalNumber) { this.totalNumber = totalNumber; // calculation this.count(); } public void count() { int totalPageTemp = this.totalNumber / this.pageSize; int plus = (this.totalNumber % this.pageSize) == 0 ? 0 : 1; totalPageTemp = totalPageTemp + plus; if (totalPageTemp <= 0) { totalPageTemp = 1; } this.totalPage = totalPageTemp;// PageCount if (this.totalPage < this.currentPage) { this.currentPage = this.totalPage; } if (this.currentPage < 1) { this.currentPage = 1; } this.startIndex = (this.currentPage - 1) * this.pageSize;// The starting position is equal to all previous page inputs multiplied by the page size this.totalSelect = this.pageSize;// The number of retrievals is equal to the page size } }
@Data public class PageResponse<T> { private int totalNumber; private int currentPage; private int totalPage; private int pageSize = 3; private T data; }
public class PageUtil { private static final ThreadLocal<Page> LOCAL_PAGE = new ThreadLocal<Page>(); public static void setPagingParam(int offset, int limit) { Page page = new Page(offset, limit); LOCAL_PAGE.set(page); } public static void removePagingParam() { LOCAL_PAGE.remove(); } public static Page getPaingParam() { return LOCAL_PAGE.get(); } }
Handwriting plug-in to separate reading and writing
Intercepting Executor based on spring dynamic data source and Theadlocal
@Intercepts({// mybatis execution process @Signature(type = Executor.class, method = "update", args = { MappedStatement.class, Object.class }), @Signature(type = Executor.class, method = "query", args = { MappedStatement.class, Object.class, RowBounds.class, ResultHandler.class }) }) @Slf4j public class DynamicPlugin implements Interceptor { private static final Map<String, String> cacheMap = new ConcurrentHashMap<>(); @Override public Object intercept(Invocation invocation) throws Throwable { Object[] objects = invocation.getArgs(); MappedStatement ms = (MappedStatement) objects[0]; String dynamicDataSource = null; if ((dynamicDataSource = cacheMap.get(ms.getId())) == null) { // Reading method if (ms.getSqlCommandType().equals(SqlCommandType.SELECT)) { // select * from user; update insert // ! selectKey is the auto increment ID query primary key (SELECT LAST_INSERT_ID()) method, using the main database if (ms.getId().contains(SelectKeyGenerator.SELECT_KEY_SUFFIX)) { dynamicDataSource = "write"; } else { // Load balancing for multiple read Libraries dynamicDataSource = "read"; } } else { dynamicDataSource = "write"; } log.info("method[{"+ms.getId()+"}] Used [{"+dynamicDataSource+"}] data source, SqlCommandType [{"+ms.getSqlCommandType().name()+"}].."); // Store the ID (method name) and data source in the map and execute it directly after the next hit cacheMap.put(ms.getId(), dynamicDataSource); } // Sets the data source used by the current thread DynamicDataSourceHolder.putDataSource(dynamicDataSource); 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) { } }
public final class DynamicDataSourceHolder { // Use ThreadLocal to record the data source key of the current thread private static final ThreadLocal<String> holder = new ThreadLocal<String>(); public static void putDataSource(String name){ holder.set(name); } public static String getDataSource(){ return holder.get(); } /** * Clean up data sources */ public static void clearDataSource() { holder.remove(); } }
public class DynamicDataSource extends AbstractRoutingDataSource { @Override protected Object determineCurrentLookupKey() { return DynamicDataSourceHolder.getDataSource(); } }
@Configuration public class DataSourceConfig { @Value("${spring.datasource.db01.jdbcUrl}") private String db01Url; @Value("${spring.datasource.db01.username}") private String db01Username; @Value("${spring.datasource.db01.password}") private String db01Password; @Value("${spring.datasource.db01.driverClassName}") private String db01DiverClassName; @Bean("dataSource01") public DataSource dataSource01(){ HikariDataSource dataSource01 = new HikariDataSource(); dataSource01.setJdbcUrl(db01Url); dataSource01.setDriverClassName(db01DiverClassName); dataSource01.setUsername(db01Username); dataSource01.setPassword(db01Password); return dataSource01; } @Value("${spring.datasource.db02.jdbcUrl}") private String db02Url; @Value("${spring.datasource.db02.username}") private String db02Username; @Value("${spring.datasource.db02.password}") private String db02Password; @Value("${spring.datasource.db02.driverClassName}") private String db02DiverClassName; @Bean("dataSource02") public DataSource dataSource02(){ HikariDataSource dataSource02 = new HikariDataSource(); dataSource02.setJdbcUrl(db02Url); dataSource02.setDriverClassName(db02DiverClassName); dataSource02.setUsername(db02Username); dataSource02.setPassword(db02Password); return dataSource02; } @Bean("multipleDataSource") public DataSource multipleDataSource(@Qualifier("dataSource01") DataSource dataSource01, @Qualifier("dataSource02") DataSource dataSource02) { Map<Object, Object> datasources = new HashMap<Object, Object>(); datasources.put("write", dataSource01); datasources.put("read", dataSource02); DynamicDataSource multipleDataSource = new DynamicDataSource(); multipleDataSource.setDefaultTargetDataSource(dataSource01); multipleDataSource.setTargetDataSources(datasources); return multipleDataSource; } }
public class DynamicDataSource extends AbstractRoutingDataSource { @Override protected Object determineCurrentLookupKey() { return DynamicDataSourceHolder.getDataSource(); } }
public class DynamicDataSourceTransactionManager extends DataSourceTransactionManager { private static final long serialVersionUID = 1L; public DynamicDataSourceTransactionManager(DataSource dataSource){ super(dataSource); } /** * Read only transaction to read library, read-write transaction to write library * * @param transaction * @param definition */ @Override protected void doBegin(Object transaction, TransactionDefinition definition) { // set up data sources boolean readOnly = definition.isReadOnly(); if (readOnly) { DynamicDataSourceHolder.putDataSource("read"); } else { DynamicDataSourceHolder.putDataSource("write"); } super.doBegin(transaction, definition); } /** * Clean up the data source of the local thread * * @param transaction */ @Override protected void doCleanupAfterCompletion(Object transaction) { super.doCleanupAfterCompletion(transaction); DynamicDataSourceHolder.clearDataSource(); } }
Configuration @MapperScan("com.example.dao") @EnableTransactionManagement public class MybatisConfig implements TransactionManagementConfigurer { private static String mybatisConfigPath = "mybatis-config.xml"; @Autowired @Qualifier("multipleDataSource") private DataSource multipleDataSource; @Bean("sqlSessionFactoryBean") public SqlSessionFactory sqlSessionFactoryBean() throws Exception { SqlSessionFactoryBean bean = new SqlSessionFactoryBean(); bean.setDataSource(multipleDataSource); bean.setTypeAliasesPackage("com.example.entity"); bean.setConfigLocation(new ClassPathResource(mybatisConfigPath)); ResourcePatternResolver resolver = new PathMatchingResourcePatternResolver(); bean.setMapperLocations(resolver.getResources("classpath*:mapper/*.xml")); return bean.getObject(); } public PlatformTransactionManager annotationDrivenTransactionManager() { return new DynamicDataSourceTransactionManager(multipleDataSource); } }
mybatis L2 cache
Mybatis enables the L1 cache by default. The L1 cache is at the sqlsession level, so it is useless in the actual scenario. Mybatis L2 cache is closed by default. The usage method is as follows
1. Add in global configuration file
<settings> <setting name="cacheEnabled" value="true" /> </settings>
2. In mapper using L2 cache Add in XML
<mapper namespace="com.study.mybatis.mapper.UserMapper"> <!--Open this mapper of namespace L2 cache under--> <cache eviction="LRU" flushInterval="100000" readOnly="true" size="1024"/> </mapper> <!--eviction:It represents the cache recycling policy. At present MyBatis Provide the following policies. (1) LRU,The least recently used and the longest unused object (2) FIFO,First in, first out, removes objects in the order they enter the cache (3) SOFT,Soft reference to remove objects based on garbage collector status and soft reference rules (4) WEAK,Weak references, more actively remove objects based on garbage collector status and weak reference rules. What is used here is LRU,Remove the image that has not been used for the longest time flushInterval:The refresh interval, in milliseconds, is configured to refresh in 100 seconds. If you do not configure it, when SQL The cache will be refreshed only when it is executed. size:The number of references, a positive integer, represents the maximum number of objects that can be stored in the cache. It should not be set too large. Setting too high will cause memory overflow. 1024 objects are configured here readOnly:Read only means that the cached data can only be read and cannot be modified. The advantage of this setting is that we can read the cache quickly. The disadvantage is that we can't modify the cache. Its default value is false,We are not allowed to modify -->
3. Distributed applications can introduce myabtis redis related dependencies to realize redis based distributed caching
< cache type="org.mybatis.caches.redis.RedisCache" />
4. You can also customize the Cache. myabtis reserves a Cache interface for us
mybatis custom type converter
It is usually used for unified conversion of special fields, encryption of sensitive fields, etc. the usage is as follows
public class MyTypeHandler implements TypeHandler { //private static String KEY = "123456"; /** * Set parameters through the preparedStatement object to store T-type data into the database. * * @param ps * @param i * @param parameter * @param jdbcType * @throws SQLException */ @Override public void setParameter(PreparedStatement ps, int i, Object parameter, JdbcType jdbcType) throws SQLException { try { String encrypt = EncryptUtil.encode(((String) parameter).getBytes()); ps.setString(i, encrypt); } catch (Exception e) { e.printStackTrace(); } } // The result data can be obtained by column name or subscript, or by CallableStatement. @Override public Object getResult(ResultSet rs, String columnName) throws SQLException { String result = rs.getString(columnName); if (result != null && result != "") { try { return EncryptUtil.decode(result.getBytes()); } catch (Exception e) { e.printStackTrace(); } } return result; } @Override public Object getResult(ResultSet rs, int columnIndex) throws SQLException { String result = rs.getString(columnIndex); if (result != null && result != "") { try { return EncryptUtil.decode(result.getBytes()); } catch (Exception e) { e.printStackTrace(); } } return result; } @Override public Object getResult(CallableStatement cs, int columnIndex) throws SQLException { String result = cs.getString(columnIndex); if (result != null && result != "") { try { return EncryptUtil.decode(result.getBytes()); } catch (Exception e) { e.printStackTrace(); } } return result; } }
public class EncryptUtil { //base64 decoding public static String decode(byte[] bytes) { return new String(Base64.decodeBase64(bytes)); } //base64 encoding public static String encode(byte[] bytes) { return new String(Base64.encodeBase64(bytes)); } }
Introducing type converters into mybatis configuration files
<plugins> <plugin interceptor="com.example.plugin.PagePlugin" > <property name="type" value="mysql"/> </plugin> </plugins>
Specify the type converter in the field you want to use
<resultMap id="resultListUser" type="com.example.entity.User" > <result column="password" property="password" typeHandler="com.example.typehandler.MyTypeHandler" /> </resultMap> <update id="updateUser" parameterType="com.example.entity.User"> UPDATE user userName=#{userName typeHandler="com.example.typehandler.MyTypeHandler"} WHERE id=#{id} </update>
Integration principle of spring and mybatis
The integration process is as follows
1. Introduce dependency
<dependency> <groupId>org.mybatis</groupId> <artifactId>mybatis-spring</artifactId> <version>1.2.2</version> </dependency>
2. The following configuration is introduced into the spring configuration file
<bean id="sqlSessionFactory" class="org.mybatis.spring.SqlSessionFactoryBean"> <!-- Load data source --> <property name="dataSource" ref="dataSource"/> <property name="mapperLocations" value="classpath*:mappers/*Mapper.xml"/> </bean> <bean class="org.mybatis.spring.mapper.MapperScannerConfigurer"> <!-- Specify the packages to scan, if there are multiple packages to use(comma,)division --> <property name="basePackage" value="com.test.bean"/> <property name="sqlSessionFactoryBeanName" value="sqlSessionFactory"/> </bean>
You can see that the core class has two mappercannerconfigurer and SqlSessionFactoryBean.
MapperScannerConfigurer
The mappercannerconfigurer class is declared as follows
public class MapperScannerConfigurer implements BeanDefinitionRegistryPostProcessor, InitializingBean, ApplicationContextAware, BeanNameAware { //Code omission }
The declaration of BeanDefinitionRegistryPostProcessor is as follows
public interface BeanDefinitionRegistryPostProcessor extends BeanFactoryPostProcessor { void postProcessBeanDefinitionRegistry(BeanDefinitionRegistry registry) throws BeansException; }
The main method in this class is the postprocessbeandefinitionregistry (beandefinitionregistry) method
public void postProcessBeanDefinitionRegistry(BeanDefinitionRegistry registry) { if (this.processPropertyPlaceHolders) { processPropertyPlaceHolders(); } ClassPathMapperScanner scanner = new ClassPathMapperScanner(registry); scanner.setAddToConfig(this.addToConfig); scanner.setAnnotationClass(this.annotationClass); scanner.setMarkerInterface(this.markerInterface); scanner.setSqlSessionFactory(this.sqlSessionFactory); scanner.setSqlSessionTemplate(this.sqlSessionTemplate); scanner.setSqlSessionFactoryBeanName(this.sqlSessionFactoryBeanName); scanner.setSqlSessionTemplateBeanName(this.sqlSessionTemplateBeanName); scanner.setResourceLoader(this.applicationContext); scanner.setBeanNameGenerator(this.nameGenerator); scanner.registerFilters(); scanner.scan(StringUtils.tokenizeToStringArray(this.basePackage, ConfigurableApplicationContext.CONFIG_LOCATION_DELIMITERS)); }
This method creates a spring - mybatis The classpathmappercanner scanner in the jar package inherits spring's ClassPathBeanDefinitionScanner. The main functions of this scanner are as follows:
1. Scan all class classes under the basePackage package
2. Second, encapsulate all class classes into spring's ScannedGenericBeanDefinition sbd object
3. Third, filter sbd objects and accept only interface classes
4. Fourth, set the properties of the sbd object, such as sqlSessionFactory and beanclass (note that the class object of MapperFactoryBean is put in, which will be used later). The specific code is as follows:
ClassPathMapperScanner private void processBeanDefinitions(Set<BeanDefinitionHolder> beanDefinitions) { GenericBeanDefinition definition; for (BeanDefinitionHolder holder : beanDefinitions) { definition = (GenericBeanDefinition) holder.getBeanDefinition(); definition.getConstructorArgumentValues().addGenericArgumentValue(definition.getBeanClassName()); definition.setBeanClass(this.mapperFactoryBean.getClass()); definition.getPropertyValues().add("addToConfig", this.addToConfig); boolean explicitFactoryUsed = false; if (StringUtils.hasText(this.sqlSessionFactoryBeanName)) { definition.getPropertyValues().add("sqlSessionFactory", new RuntimeBeanReference(this.sqlSessionFactoryBeanName)); explicitFactoryUsed = true; } else if (this.sqlSessionFactory != null) { definition.getPropertyValues().add("sqlSessionFactory", this.sqlSessionFactory); explicitFactoryUsed = true; } if (StringUtils.hasText(this.sqlSessionTemplateBeanName)) { if (explicitFactoryUsed) { logger.warn("Cannot use both: sqlSessionTemplate and sqlSessionFactory together. sqlSessionFactory is ignored."); } definition.getPropertyValues().add("sqlSessionTemplate", new RuntimeBeanReference(this.sqlSessionTemplateBeanName)); explicitFactoryUsed = true; } else if (this.sqlSessionTemplate != null) { if (explicitFactoryUsed) { logger.warn("Cannot use both: sqlSessionTemplate and sqlSessionFactory together. sqlSessionFactory is ignored."); } definition.getPropertyValues().add("sqlSessionTemplate", this.sqlSessionTemplate); explicitFactoryUsed = true; } if (!explicitFactoryUsed) { if (logger.isDebugEnabled()) { logger.debug("Enabling autowire by type for MapperFactoryBean with name '" + holder.getBeanName() + "'."); } definition.setAutowireMode(AbstractBeanDefinition.AUTOWIRE_BY_TYPE); } } }
5. Fifthly, register the filtered sbd object in the DefaultListableBeanFactory through the beandefinitionregistry register, which is the parameter in the method postprocessbeandefinitionregistry (beandefinitionregistry).
The above is the main work of instantiating mappercannerconfigurer class. To sum up, scan all mapper interface classes under the basePackage package, encapsulate the mapper interface class into a BeanDefinition object, and register it in the BeanFactory container of spring.
SqlSessionFactoryBean
Class declaration
public class SqlSessionFactoryBean implements FactoryBean<SqlSessionFactory>, InitializingBean, ApplicationListener<ApplicationEvent> { //Omit code }
In the process of creating this bean, the first method called is afterpropertieset, which is the method in the interface InitializingBean.
public void afterPropertiesSet() throws Exception { Assert.notNull(this.dataSource, "Property 'dataSource' is required"); Assert.notNull(this.sqlSessionFactoryBuilder, "Property 'sqlSessionFactoryBuilder' is required"); this.sqlSessionFactory = this.buildSqlSessionFactory(); }
Many things have been done in this buildSqlSessionFactory() method. To sum up, several objects have been created, including the core class Configuration of mybatis, the thing factory class SpringManagedTransactionFactory integrated by spring and mybatis, the Environment class of mybatis, and the DefaultSqlSessionFactory class of mybatis. At the same time, the xml file of mybatis has been parsed, And encapsulate the parsing results in the Configuration class.
protected SqlSessionFactory buildSqlSessionFactory() throws IOException { XMLConfigBuilder xmlConfigBuilder = null; Configuration configuration; configuration = new Configuration(); configuration.setVariables(this.configurationProperties); //Omit code if (this.transactionFactory == null) { this.transactionFactory = new SpringManagedTransactionFactory(); } Environment environment = new Environment(this.environment, this.transactionFactory, this.dataSource); configuration.setEnvironment(environment); if (!ObjectUtils.isEmpty(this.mapperLocations)) { Resource[] arr$ = this.mapperLocations; len$ = arr$.length; for(i$ = 0; i$ < len$; ++i$) { Resource mapperLocation = arr$[i$]; if (mapperLocation != null) { try { XMLMapperBuilder xmlMapperBuilder = new XMLMapperBuilder(mapperLocation.getInputStream(), configuration, mapperLocation.toString(), configuration.getSqlFragments()); xmlMapperBuilder.parse(); } catch (Exception var20) { throw new NestedIOException("Failed to parse mapping resource: '" + mapperLocation + "'", var20); } finally { ErrorContext.instance().reset(); } } } } return this.sqlSessionFactoryBuilder.build(configuration); }
sqlSessionFactoryBuilder. What build (Configuration) does is very simple. It directly creates DefaultSqlSessionFactory, which also holds a reference to the Configuration object.
SqlSessionFactoryBean implements the FactoryBean interface, so the getObject method will be called during initialization.
public SqlSessionFactory getObject() throws Exception { if (this.sqlSessionFactory == null) { this.afterPropertiesSet(); } return this.sqlSessionFactory; }
In terms of content, it directly returns an instance of sqlsessionfactory class. If it is empty, it calls afterpropertieset to initialize sqlsessionfactory object.
Instantiation process
Take the following code as an example
@Service public class UserServiceImpl implements IUserService { @Resource private UserMapper UserMapper; public UserServiceImpl(){ } }
During the initialization process of spring, the UserServiceImpl class will be created. After creation, the attribute will be assigned. The mybatis interface of UserMapper is an attribute of UserServiceImpl. First, obtain its BeanDefinition from spring's BeanFactory according to the name of the mapper, and then obtain BeanClass from BeanDefinition, The BeanClass corresponding to UserMapper is MapperFactoryBean, which is mentioned in the above analysis.
public class MapperFactoryBean<T> extends SqlSessionDaoSupport implements FactoryBean<T> { private Class<T> mapperInterface; private boolean addToConfig = true; public MapperFactoryBean() { //intentionally empty } public MapperFactoryBean(Class<T> mapperInterface) { this.mapperInterface = mapperInterface; } @Override protected void checkDaoConfig() { super.checkDaoConfig(); notNull(this.mapperInterface, "Property 'mapperInterface' is required"); Configuration configuration = getSqlSession().getConfiguration(); if (this.addToConfig && !configuration.hasMapper(this.mapperInterface)) { try { configuration.addMapper(this.mapperInterface); } catch (Exception e) { logger.error("Error while adding the mapper '" + this.mapperInterface + "' to configuration.", e); throw new IllegalArgumentException(e); } finally { ErrorContext.instance().reset(); } } } @Override public T getObject() throws Exception { return getSqlSession().getMapper(this.mapperInterface); } @Override public Class<T> getObjectType() { return this.mapperInterface; } @Override public boolean isSingleton() { return true; } public void setMapperInterface(Class<T> mapperInterface) { this.mapperInterface = mapperInterface; } public Class<T> getMapperInterface() { return mapperInterface; } public void setAddToConfig(boolean addToConfig) { this.addToConfig = addToConfig; } public boolean isAddToConfig() { return addToConfig; } }
The next step is to create the MapperFactoryBean object. After creation, you need to assign a value to the attribute, which is determined when creating the BeanDefinition object corresponding to UserMapper. You can know by reviewing the above part of creating the MapperScannerConfigurer object. One of the properties is SqlSessionFactoryBean, which triggers the creation process of SqlSessionFactoryBean.
After setting the properties of MapperFactoryBean object, call its getObject() method. The subsequent logic is the process of creating proxy object by mybatis mentioned above.