Handwritten mybatis-2 source code parsing

Keywords: Mybatis SQL Session xml

Handwritten mybatis-2 source code parsing

Article directory

0 origin

This article follows above. Handwritten mybatis - 1 Overview & code The purpose of this article is to explain the basic composition and design idea of the code. The code in this article is based on my handwritten mybatis. The package structure and class name are very similar to mybatis, which is equivalent to a simplified version of mybatis.

1 mybatis core process

mybatis core business processes are actually quite simple

1 parse mybatis system configuration file and encapsulate it as Configuration
 2 parse mapper.xml and encapsulate it as MapperStatement for later use
 3 Specific business code, calling dao's proxy class
 Mapping and Processing of 4 Parameters
 5 Dynamic generation of executed sql statements based on input parameters and apperStatement
 6. Processing result set mapping through resulthandle and returning result set

2 Source Code Analysis

 //Read mybatis master configuration file
InputStream is = Resources.getResourceAsStream("MybatisConfig.xml");
//Create SqlSession FactoryBuilder
SqlSessionFactoryBuilder builder = new SqlSessionFactoryBuilder();
//Create factories through Builder Design Patterns
SqlSessionFactory factory = builder.build(is);
//Creating sqlSession through Factory Mode
SqlSession sqlSession = factory.openSession();
//Getting the corresponding mapper through dynamic proxy
UserMapper mapper = sqlSession.getMapper(UserMapper.class);
//Calling method
List<User> userList = mapper.getUserList();
//Traversing print result sets
for (User user : userList) {
    System.out.println(user);
}

2.1 SqlSessionFactoryBuilder

Responsible for loading and parsing configuration files and returning to SqlSessionFactory

public class SqlSessionFactoryBuilder {

    public SqlSessionFactory build(InputStream inputStream) {
        try {
            XMLConfigBuilder parser = new XMLConfigBuilder(inputStream);
            return build(parser.parse());
        } catch (Exception e) {
            e.printStackTrace();
        }

        return null;
    }

    public SqlSessionFactory build(Configuration config) {
        return new DefaultSqlSessionFactory(config);
    }
}

2.2 XMLConfigBuilder

Specific parsing logic parses the input stream into a Configuration for later use

public class XMLConfigBuilder {

    private InputStream inputStream;

    public XMLConfigBuilder(InputStream inputStream) {
        this.inputStream = inputStream;
    }

    public Configuration parse() {
        return loadXmlConfiguration(inputStream);
    }

    /**
     * Read the main configuration file
     *
     * @param inputStream
     * @return
     */
    public static Configuration loadXmlConfiguration(InputStream inputStream) {
        //Parsing the main configuration file using dom4j and xpath
        SAXReader reader = new SAXReader();
        Configuration config = new Configuration();
        try {
            //Get the document object by streaming the main configuration file binary
            Document document = reader.read(inputStream);
            //Get the property node set (the four parameters used to generate the connection object)
            List<Element> list = document.selectNodes("//property");
            for (Element element : list) {
                String name = element.attributeValue("name");
                String value = element.attributeValue("value");
                if (name.equals("username")) config.setUsername(value);
                if (name.equals("driver")) config.setDriver(value);
                if (name.equals("password")) config.setPassword(value);
                if (name.equals("url")) config.setUrl(value);
            }
            //Read all mapper mapping files under the main configuration file
            List<Element> listMapper = document.selectNodes("//mapper");
            for (Element element : listMapper) {
                //Traverse through each mapper tag to get the resource attribute (the full class name of the mapper mapping file)
                String resource = element.attributeValue("resource");
                //Parsing mapper mapping files using dom4j and xpath
                Map<String, MappedStatement> loadMapper = loadMapper(resource, config);
                config.setMappedStatements(loadMapper);
            }
        } catch (Exception e) {
            e.printStackTrace();
            throw new RuntimeException();
        }
        return config;
    }

    /**
     * Parsing mapper mapping files using dom4j and xpath
     *
     * @param resource
     * @return
     */
    private static Map<String, MappedStatement> loadMapper(String resource, Configuration config) {
        Map<String, MappedStatement> mappers = new HashMap<String, MappedStatement>();
        //Converting configuration files to binary streams using class loaders
        InputStream inputStream = Resources.getResourceAsStream(resource);
        SAXReader reader = new SAXReader();
        try {
            Document document = reader.read(inputStream);
            //Get the root node object
            Element rootElement = document.getRootElement();
            //Get the namespace attribute value of the root node
            String namespace = rootElement.attributeValue("namespace");
            //Get all select nodes under the root node
            List<Element> selectNodes = rootElement.selectNodes("//select");
            //Traverse to get the id, resultType, and sql statement equivalents of each select node
            for (Element selectNode : selectNodes) {
                String id = selectNode.attributeValue("id");
                String resultType = selectNode.attributeValue("resultType");
                SqlSource sqlSource = parseDynamicSqlNode(selectNode);
                //Store more than one value with a mapper object
                MappedStatement mapper = new MappedStatement();
                mapper.setConfiguration(config);
                mapper.setId(id);
                mapper.setResultType(Class.forName(resultType));
                mapper.setSqlSource(sqlSource);
                //A mapper mapping file can have more than one select (statment). In different mapping files, the id of the select can be the same.
                //So namespace+"."+id is used as a unique flag to distinguish and store in the map set.
                mappers.put(namespace + "." + id, mapper);
            }
            return mappers;
        } catch (Exception e) {
            e.printStackTrace();
            throw new RuntimeException();
        }
    }

    private static SqlSource parseDynamicSqlNode(Element selectNode) {
        String sql = selectNode.getTextTrim();
        StaticSqlSource staticSqlSource = new StaticSqlSource(sql);
        return staticSqlSource;
    }
}

2.3 Configuration

Specific configuration objects, mainly save connection information and parse mappedStatements mapping generated by mapper.xml

private String username;

    private String password;

    private String url;

    private String driver;

    protected ExecutorType defaultExecutorType = ExecutorType.SIMPLE;

    protected final InterceptorChain interceptorChain = new InterceptorChain();

    protected Map<String, MappedStatement> mappedStatements = new HashMap();

    public Executor newExecutor(ExecutorType executorType) {
        executorType = executorType == null ? defaultExecutorType : executorType;

        Executor executor = null;
        if (ExecutorType.BATCH == executorType) {
        } else if (ExecutorType.REUSE == executorType) {
        } else {
            executor = new SimpleExecutor(this);
        }
        executor = (Executor) interceptorChain.pluginAll(executor);
        return executor;
    }

2.4 SqlSessionFactory

After completing configuration parsing, session factories are generated according to the Configuration object. What's the use of session factories? To put it plainly, they are constantly opening sessions.

public class DefaultSqlSessionFactory implements SqlSessionFactory {

    private Configuration configuration;

    public DefaultSqlSessionFactory(Configuration configuration) {
        this.configuration = configuration;
    }

    public Configuration getConfiguration() {
        return configuration;
    }

    public void setConfiguration(Configuration configuration) {
        this.configuration = configuration;
    }

    @Override
    public SqlSession openSession() {
        return openSessionFromDataSource(configuration.getDefaultExecutorType());
    }

    private SqlSession openSessionFromDataSource(ExecutorType execType) {
        try {
            final Executor executor = configuration.newExecutor(execType);
            return new DefaultSqlSession(configuration, executor);
        } catch (Exception e) {
            e.printStackTrace();
        }

        return null;
    }
}

// Interface of Conversation Factory
public interface SqlSessionFactory {

    SqlSession openSession();
}

2.5 SqlSession

A session can be opened by the session factory generated above

SqlSession sqlSession = factory.openSession();

DefaultSqlSessionFactory

...

 @Override
    public SqlSession openSession() {
        return openSessionFromDataSource(configuration.getDefaultExecutorType());
    }

    private SqlSession openSessionFromDataSource(ExecutorType execType) {
        try {
            final Executor executor = configuration.newExecutor(execType);
            return new DefaultSqlSession(configuration, executor);
        } catch (Exception e) {
            e.printStackTrace();
        }

        return null;
    }

2.6 Executor & SimpleExecutor

Actuator, according to the above configuration object can generate the corresponding executor, the executor is specific work, responsible for parsing MappedStatement according to parameters and above, as well as ResultSetHandler, parsing sql, processing results and returning.

public interface Executor {

    ResultSetHandler NO_RESULT_HANDLER = null;

    <E> List<E> query(MappedStatement ms, Object parameter, ResultSetHandler resultSetHandler) throws SQLException;
    
    ...

}

SimpleExecutor Namely Executor Simple implementation

public class SimpleExecutor implements Executor {

    protected Configuration configuration;

    public SimpleExecutor(Configuration configuration) {
        this.configuration = configuration;
    }

    @Override
    public <E> List<E> query(MappedStatement mappedStatement, Object parameter, ResultSetHandler resultSetHandler) throws SQLException {
        Statement stmt = null;
        Configuration configuration = mappedStatement.getConfiguration();
        StatementHandler handler = new SimpleStatementHandler(mappedStatement, parameter, resultSetHandler);
        return handler.query(stmt, resultSetHandler);
    }
}

2.7 DefaultSqlSession

With the executor and the default configuration object, we can generate the default session, which is actually responsible for adding or deleting the query. Actually, the executor is also called SqlSession to execute the corresponding logic.

public class DefaultSqlSession implements SqlSession {

    private final Configuration configuration;
    private final Executor executor;


    public DefaultSqlSession(Configuration configuration, Executor executor) {
        this.configuration = configuration;
        this.executor = executor;
    }

    @Override
    public <T> T selectOne(String statement) throws RuntimeException {
        return this.<T>selectOne(statement, null);
    }

    @Override
    public <T> T selectOne(String statement, Object parameter) throws RuntimeException {
        // Popular vote was to return null on 0 results and throw exception on too many.
        List<T> list = this.<T>selectList(statement, parameter);
        if (list.size() == 1) {
            return list.get(0);
        } else if (list.size() > 1) {
            throw new RuntimeException("Expected one result (or null) to be returned by selectOne(), but found: " + list.size());
        } else {
            return null;
        }
    }

    @Override
    public <E> List<E> selectList(String statement, Object parameter) throws RuntimeException {
        try {
            MappedStatement ms = configuration.getMappedStatement(statement);
            return executor.query(ms, parameter, Executor.NO_RESULT_HANDLER);
        } catch (Exception e) {
            throw new RuntimeException("Error querying database.  Cause: " + e);
        }
    }

    @Override
    public Configuration getConfiguration() {
        return configuration;
    }

    @Override
    public <T> T getMapper(Class<T> clazz){
        try {
            T proxyObj = (T) Proxy.newProxyInstance(clazz.getClassLoader(), new Class[]{clazz}, new MapperProxy(this, clazz));

            return proxyObj;
        } catch (Exception e) {
            e.printStackTrace();
        }

        return null;
    }
}

2.8 MapperProxy

MapperProxy is a proxy for mapper objects. In other words, you define the interface of dao and use jdk dynamic proxy to generate a proxy object.

> //Getting the corresponding mapper through dynamic proxy
UserMapper mapper = sqlSession.getMapper(UserMapper.class);

...

 public <T> T getMapper(Class<T> clazz){
        try {
            T proxyObj = (T) Proxy.newProxyInstance(clazz.getClassLoader(), new Class[]{clazz}, new MapperProxy(this, clazz));

            return proxyObj;
        } catch (Exception e) {
            e.printStackTrace();
        }

        return null;
    }

MapperProxy actually zu

public class MapperProxy implements InvocationHandler {
    private SqlSession sqlSession;
    private Class mapperInterface;

    public MapperProxy(SqlSession sqlSession, Class mapperInterface) {
        this.sqlSession = sqlSession;
        this.mapperInterface = mapperInterface;
    }

    @Override
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
        //If the return value is a List collection, the following code is executed
        Class<?> returnType = method.getReturnType();
        if (returnType == List.class) {
            //Any method invoked by the proxy object is executed here
            //True execution of Sql statements
            //Work to execute Sql statements: selectList() of the sqlSession object, that is, I'm calling that method here.
            //key is the fully qualified name of the Mapper interface +"."+method name
            String methodName = method.getName();//Method name
            Class<?> clazz = method.getDeclaringClass();
            String clazzName = clazz.getName();//Gets the fully qualified name of the interface
            String key = clazzName + "." + methodName;
            List<Object> list = sqlSession.selectList(key, args);
            return list;
        }else {
            return null;
        }
    }

2.9 dao Implementation

The invoke method of the proxy object is actually executed when the dao method is called

//Calling method
List<User> userList = mapper.getUserList();

--> MapperProxy --> invoke

--> sqlSession-->selectList

DefaultSqlSession

...

@Override
    public <E> List<E> selectList(String statement, Object parameter) throws RuntimeException {
        try {
            MappedStatement ms = configuration.getMappedStatement(statement);
            return executor.query(ms, parameter, Executor.NO_RESULT_HANDLER);
        } catch (Exception e) {
            throw new RuntimeException("Error querying database.  Cause: " + e);
        }
    }
    
//Let's look at executor's query

SimpleExecutor

...

@Override
    public <E> List<E> query(MappedStatement mappedStatement, Object parameter, ResultSetHandler resultSetHandler) throws SQLException {
        Statement stmt = null;
        Configuration configuration = mappedStatement.getConfiguration();
        StatementHandler handler = new SimpleStatementHandler(mappedStatement, parameter, resultSetHandler);
        return handler.query(stmt, resultSetHandler);
    }

StatementHandler

//After seeing it for so long, I finally saw the original jdbc.

...

public class SimpleStatementHandler implements StatementHandler {

    private MappedStatement mappedStatement;

    private ResultSetHandler resultHandler;

    private Object parameter;

    public SimpleStatementHandler(MappedStatement mappedStatement, Object parameter, ResultSetHandler resultHandler) {
        this.mappedStatement = mappedStatement;
        this.resultHandler = resultHandler;
        this.parameter = parameter;
    }

    @Override
    public <E> List<E> query(Statement statement, ResultSetHandler resultHandler) throws SQLException {
        Connection conn = null;
        PreparedStatement preparedStatement = null;
        try {
            conn = getConnection();
            preparedStatement = conn.prepareStatement(mappedStatement.getBoundSql(parameter).getSql());
            ParameterHandler parameterHandler = new DefaultParameterHandler(parameter);
            parameterHandler.setParameters(preparedStatement);
            preparedStatement.execute();
            resultHandler = new DefaultResultSetHandler(mappedStatement.getResultType(), preparedStatement.getResultSet());
            return resultHandler.handleResultSets();
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            try {
                if (preparedStatement != null) {
                    preparedStatement.close();
                }
                if (conn != null) {
                    conn.close();
                }
            } catch (SQLException e) {
                e.printStackTrace();
            }
        }
        return null;
    }

    private Connection getConnection() throws SQLException, ClassNotFoundException {
        Configuration configuration = mappedStatement.getConfiguration();
        Class.forName(configuration.getDriver());
        return DriverManager.getConnection(configuration.getUrl(), configuration.getUsername(), configuration.getPassword());
    }
}

2.10 SimpleStatementHandler

It's dealing with jdbc. ParameterHandler handles the parameters, ResultSetHandler handles the returned results and returns them.

DefaultParameterHandler

public class DefaultParameterHandler implements ParameterHandler {

    private Object parameter;

    public DefaultParameterHandler(Object parameter) {
        this.parameter = parameter;
    }

    @Override
    public void setParameters(PreparedStatement psmt) throws SQLException {
        try {
            Object[] parameters = (Object[]) parameter;

            if (parameters != null && parameters.length > 0) {
                for (int i = 0; i < parameters.length; i++) {
                    if (parameters[i] instanceof Integer) {
                        psmt.setInt(i + 1, (Integer) parameters[i]);
                    } else if (parameters[i] instanceof Long) {
                        psmt.setLong(i + 1, (Long) parameters[i]);
                    } else if (parameters[i] instanceof String) {
                        psmt.setString(i + 1, String.valueOf(parameters[i]));
                    } else if (parameters[i] instanceof Boolean) {
                        psmt.setBoolean(i + 1, (Boolean) parameters[i]);
                    } else {
                        psmt.setString(i + 1, String.valueOf(parameters[i]));
                    }
                }
            }
        } catch (SQLException e) {
            e.printStackTrace();
        }
    }
}


DefaultResultSetHandler

...

public class DefaultResultSetHandler<E> implements ResultSetHandler {

    private Class type;
    private ResultSet resultSet;

    public DefaultResultSetHandler(Class type, ResultSet resultSet) {
        this.type = type;
        this.resultSet = resultSet;
    }

    @Override
    public <E> List<E> handleResultSets() {
        try {
            ArrayList<E> resultList = new ArrayList<>();
            Object resultObject = null;
            while (resultSet.next()) {
                resultObject = new DefaultObjectFactory().create(type);
                for (Field field : resultObject.getClass().getDeclaredFields()) {
                    setValue(resultObject, field, resultSet);
                }

                resultList.add( (E) resultObject);
            }

            return resultList;
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            try {
                if (resultSet != null) {
                    resultSet.close();
                }
            } catch (SQLException e) {
                e.printStackTrace();
            }
        }
        return null;
    }

    public void setValue(Object resultObject, Field field, ResultSet resultSet) throws NoSuchMethodException, InvocationTargetException, IllegalAccessException, SQLException {
        Method method = type.getMethod("set" + upperCapital(field.getName()), field.getType());
        method.invoke(resultObject, getResult(field, resultSet));
    }

    public Object getResult(Field field, ResultSet rs) throws SQLException {
        Class<?> type = field.getType();
        if (Integer.class == type) {
            return rs.getInt(field.getName());
        } else if (String.class == type) {
            return rs.getString(field.getName());
        } else if (Long.class == type) {
            return rs.getLong(field.getName());
        } else {
            return rs.getString(field.getName());
        }
    }

    private String upperCapital(String name) {
        String first = name.substring(0, 1);
        String tail = name.substring(1);
        return first.toUpperCase() + tail;
    }
}


3 expansion

The core logic of mybatis ends here, but mybatis goes far beyond these things, such as sql dynamic generation, plug-in mechanism, caching mechanism, transaction mechanism, which bloggers will write articles about later. Look forward to it.

Posted by kmutz22 on Tue, 23 Apr 2019 13:00:34 -0700