09.搭建基础框架


title: 09.搭建ORM基础框架
tag: 笔记 手写SSM ORM

搭建一个仿Mybatis框架的基础框架逻辑,包括XML解析,代理对象工厂MapperProxyFactory,映射器注册MapperRegistry。

目标

目前我们的Summer实现Spring的IOC和AOP,基本还原了Spring框架的功能。现在我们想要整合一个持久层的框架,因此我们选择再实现一个Mybatis并将其整合到我们自己实现的Summer框架中。我们首先来搭建Mybatis的基础框架,并且本节不会设计到JDBC数据库的操作,我们只是还原一下Mybatis的基本原理。

实现映射器代理工厂

在学习Mybatis使用的时候我们知道我们调用接口的方法时拿到的是Mybatis生成的Mapper代理对象。这里需要使用我们在实现AOP时使用到的动态代理技术,Mybatis生成代理对象是根据接口来生成,因此我们只需要使用JDK自带的Proxy类即可实现。下面我们将实现一个映射器代理工厂来创建代理类:

MapperProxy

Mapper代理类:

public class MapperProxy<T> implements InvocationHandler, Serializable {

    private static final long serialVersionUID = -6424540398559729838L;

    private Map<String, String> sqlSession;
    private final Class<T> mapperInterface;

    public MapperProxy(Map<String, String> sqlSession, Class<T> mapperInterface) {
        this.sqlSession = sqlSession;
        this.mapperInterface = mapperInterface;
    }

    @Override
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
        if (Object.class.equals(method.getDeclaringClass())) {
            return method.invoke(this, args);
        } else {
            return "你的被代理了!" + sqlSession.get(mapperInterface.getName() + "." + method.getName());
        }
    }
}
  • 我们使用一个Map来模拟SqlSession,Mybatis中的SqlSession包含着Mybatis需要的信息。
  • 它实现了InvocationHandler,在invoke方法执行到这个方法在SqlSession中的执行信息。

MapperProxyFactory

Mapper代理类工厂:

public class MapperProxyFactory<T> {

    private final Class<T> mapperInterface;

    public MapperProxyFactory(Class<T> mapperInterface) {
        this.mapperInterface = mapperInterface;
    }

    public T newInstance(Map<String, String> sqlSession) {
        final MapperProxy<T> mapperProxy = new MapperProxy<>(sqlSession, mapperInterface);
        return (T) Proxy.newProxyInstance(mapperInterface.getClassLoader(), new Class[]{mapperInterface}, mapperProxy);
    }

}
  • 这里使用了工厂设计模式封装了Mapper代理类的创建过程。
  • Proxy.newProxyInstance用于根据接口创建代理对象。

总结

这里的URL图:

image-20240127134519711
  • MapperProxy 负责实现 InvocationHandler 接口的 invoke 方法,最终所有的实际调用都会调用到这个方法包装的逻辑。
  • MapperProxyFactory 是对 MapperProxy 的包装,对外提供实例化对象的操作。当我们后面开始给每个操作数据库的接口映射器注册代理的时候,就需要使用到这个工厂类了。

实现映射器注册和使用

在实现了映射器代理工厂之后,我们可以使用工厂来创建代理Mapper。但目前我们还存在两个问题:

  • 需要编码告知 MapperProxyFactory 要对哪个接口进行代理
  • 编写一个假的 SqlSession 处理实际调用接口时的返回结果

针对以上两点,我们需要对映射器的注册提供一个注册机处理用户可以在使用的时候提供一个包的路径即可完成扫描和注册。与此同时需要对 SqlSession 进行规范化处理,让它可以把我们的映射器代理和方法调用进行包装,建立一个生命周期模型结构,便于后续的内容的添加。

MapperRegistry

public class MapperRegistry {
    /**
     * 将已添加的映射器代理加入到 HashMap
     */
    private final Map<Class<?>, MapperProxyFactory<?>> knownMappers = new HashMap<>();

    public <T> T getMapper(Class<T> type, SqlSession sqlSession) {
        final MapperProxyFactory<T> mapperProxyFactory = (MapperProxyFactory<T>) knownMappers.get(type);
        if (mapperProxyFactory == null) {
            throw new RuntimeException("Type " + type + " is not known to the MapperRegistry.");
        }
        try {
            return mapperProxyFactory.newInstance(sqlSession);
        } catch (Exception e) {
            throw new RuntimeException("Error getting mapper instance. Cause: " + e, e);
        }
    }

    public <T> void addMapper(Class<T> type) {
        /* Mapper 必须是接口才会注册 */
        if (type.isInterface()) {
            if (hasMapper(type)) {
                // 如果重复添加了,报错
                throw new RuntimeException("Type " + type + " is already known to the MapperRegistry.");
            }
            // 注册映射器代理工厂
            knownMappers.put(type, new MapperProxyFactory<>(type));
        }
    }

    public void addMappers(String packageName) {
        Set<Class<?>> mapperSet = ClassScanner.scanPackage(packageName);
        for (Class<?> mapperClass : mapperSet) {
            addMapper(mapperClass);
        }
    }
}
  • 这个类使用了一个HashMap保存了**接口类型(Key)映射器工厂MapperProxyFactory(Value)**之间的映射关系。
  • 该类提供了注册Mapper和获取Mapper的方式。
  • 获取Mapper的方式可以传入扫描包的方式添加或者传入接口添加,但本质上都是通过接口来添加。

SqlSession

我们将SqlSession定义为一个接口:

public interface SqlSession {

    /**
     * Retrieve a single row mapped from the statement key
     * 根据指定的SqlID获取一条记录的封装对象
     *
     * @param <T>       the returned object type 封装之后的对象类型
     * @param statement sqlID
     * @return Mapped object 封装之后的对象
     */
    <T> T selectOne(String statement);

    /**
     * Retrieve a single row mapped from the statement key and parameter.
     * 根据指定的SqlID获取一条记录的封装对象,只不过这个方法容许我们可以给sql传递一些参数
     * 一般在实际使用中,这个参数传递的是pojo,或者Map或者ImmutableMap
     *
     * @param <T>       the returned object type
     * @param statement Unique identifier matching the statement to use.
     * @param parameter A parameter object to pass to the statement.
     * @return Mapped object
     */
    <T> T selectOne(String statement, Object parameter);

    /**
     * Retrieves a mapper.
     * 得到映射器,这个巧妙的使用了泛型,使得类型安全
     *
     * @param <T>  the mapper type
     * @param type Mapper interface class
     * @return a mapper bound to this SqlSession
     */
    <T> T getMapper(Class<T> type);

}
  • 在 SqlSession 中定义用来执行 SQL、获取映射器对象以及后续管理事务操作的标准接口。
  • 目前这个接口中对于数据库的操作仅仅只提供了 selectOne,后续还会有相应其他方法的定义。

我们提供一个该接口的默认实现DefaultSqlSession

public class DefaultSqlSession implements SqlSession {

    /**
     * 映射器注册机
     */
    private MapperRegistry mapperRegistry;

    // 省略构造函数

    @Override
    public <T> T selectOne(String statement, Object parameter) {
        return (T) ("你被代理了!" + "方法:" + statement + " 入参:" + parameter);
    }

    @Override
    public <T> T getMapper(Class<T> type) {
        return mapperRegistry.getMapper(type, this);
    }

}
  • SelSession具有一个MapperRegistry注册机的属性。
  • SelSession同样需要实现getMapper方法,并且这个方法委托给了MapperRegistry,将来这里将变换委托给配置类Configration,而Configration实际上也是委托给MapperRegistry,但在Configration中除了Mapper的注册信息还包含着其它的信息。

SqlSessionFactory

SqlSessionFactory是一个接口,用于打开一个SqlSession,目前仅有一个方法:

public interface SqlSessionFactory {

    /**
     * 打开一个 session
     * @return SqlSession
     */
   SqlSession openSession();

}

我们同样给SqlSessionFactory提供一个默认的实现:

public class DefaultSqlSessionFactory implements SqlSessionFactory {

    private final MapperRegistry mapperRegistry;

    public DefaultSqlSessionFactory(MapperRegistry mapperRegistry) {
        this.mapperRegistry = mapperRegistry;
    }

    @Override
    public SqlSession openSession() {
        return new DefaultSqlSession(mapperRegistry);
    }

}
  • 默认的简单工厂实现,处理开启 SqlSession 时,对 DefaultSqlSession 的创建以及传递 mapperRegistry,这样就可以在使用 SqlSession 时获取每个代理类的映射器对象了。

总结

xxxxxxxxxx @Componentpublic class AOPProxyCreator implements BeanPostProcessor, BeansAware {    Map<String, Object> originBeans = new HashMap<>();    public Map<String, BeanDefinition> beans;    public final Map<Class<? extends Annotation>, List> proxyRule = new ConcurrentHashMap<>();    public List aspectInstance = new ArrayList<>(8);    ProxyFactory proxyResolver = new ProxyFactory();    private final Logger logger = LoggerFactory.getLogger(getClass());​    @Override    public Object postProcessBeforeInitialization(Object bean, String beanName) {        Class<?> beanClass = bean.getClass();        if(checkJoinPoint(beanClass)){            originBeans.put(beanName, bean);            return proxyResolver.createProxy(bean, new DynamicAopProxy(proxyRule, bean));       }        return bean;​   }​​    private boolean checkJoinPoint(Class<?> beanClass) {        for (Annotation annotation : beanClass.getAnnotations()) {            if(proxyRule.containsKey(annotation.annotationType())){                return true;           }       }        for (Method method : beanClass.getMethods()) {            for (Annotation annotation : method.getAnnotations()) {                if(proxyRule.containsKey(annotation.annotationType())){                    return true;               }           }       }        return false;   }​    @Override    public Object postProcessOnSetProperty(Object bean, String beanName) {        Object origin = this.originBeans.get(beanName);        return origin != null ? origin : bean;   }​    @Override    public void setApplicationContext(Map<String, BeanDefinition> beans) {        this.beans = beans;        aspectInstance = getAspectInstance();        parseAspectjClass();        logger.debug(“解析后的拦截规则为:{}”, proxyRule);   }​    private void parseAspectjClass(){        for (Object aspect : aspectInstance) {            for (Method method : aspect.getClass().getMethods()) {                Around around = method.getAnnotation(Around.class);                if(around != null){                    Advice advice = new Advice(method, aspect);                    Class<? extends Annotation> targetAnno = around.targetAnno();                    if(proxyRule.containsKey(targetAnno)){                        proxyRule.get(targetAnno).add(advice);                   }else {                        List proxyChains = new ArrayList<>();                        proxyChains.add(advice);                        proxyRule.put(targetAnno, proxyChains);                   }               }           }       }   }    public List getAspectInstance() {        List aspectDef = beans.values().                stream()               .filter(definition -> definition.getBeanClass().isAnnotationPresent(Aspect.class))               .toList();        return aspectDef.stream().map(BeanDefinition::getInstance).toList();   }}java

  • MapperRegistry 提供包路径的扫描和映射器代理类注册机服务,完成接口对象的代理类注册处理。
  • SqlSessionDefaultSqlSession 用于定义执行 SQL 标准、获取映射器以及将来管理事务等方面的操作。基本我们平常使用 Mybatis 的 API 接口也都是从这个接口类定义的方法进行使用的。
  • SqlSessionFactory 是一个简单工厂模式,用于提供 SqlSession 服务,屏蔽创建细节,延迟创建过程。

Mapper XML解析和注册

现在我们已经实现了一个包含Mapper注册信息的SqlSession,但我们实现一个可用的SqlSession还需要更多的配置信息。因此,我们会将框架需要的全部配置信息都存放到Configration中,包括之前实现的映射器注册机MapperRegisry。而这些信息我们需要从相应的配置文件中去获取,比如XML配置文件。因此,在本节我们需要实现下面几个目标:

  • 实现Configration类用来保存SqlSession需要的配置信息。
  • 通过读取配置文件拿到构建Configration的配置信息。
  • 通过Configration构建一个SqlSession

构建SqlSessionFactory建造者

我们最终目标是为了构建一个可以使用的SqlSession,而我们使用工厂模式创建SqlSession,最后我们再提供一个建造者SqlSessionFactory包装 XML 解析处理作为Mybatis的入口:

public class SqlSessionFactoryBuilder {

    public SqlSessionFactory build(Reader reader) {
        XMLConfigBuilder xmlConfigBuilder = new XMLConfigBuilder(reader);
        return build(xmlConfigBuilder.parse());
    }

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

}
  • SqlSessionFactoryBuilder作为框架的入口类,通过配置文件XML的IO来引导Mybatis的启动。
  • 在这个类我们引入了XMLConfigBuilder和之前提过的Configration中,XMLConfigBuilder用于解析XML配置文件并通过配置信息构建Configration。

解析XML

我们解析XML的信息后需要构建一个Configuration,因此我们可以先抽象一个基类ConfigBuilder

public abstract class ConfigBuilder {

    protected final Configuration configuration;
    
    public abstract Configuration parse();

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

    public Configuration getConfiguration() {
        return configuration;
    }

}
  • 这个基类有一个Configuration的属性,并且可以通过parse方法解析配置信息构建Configration并且通过getConfiguration获得到Configuration

我们目前仅提供XML配置的方式构建Configuration,因此我们提供一个XMLConfigBuilder继承ConfigBuilder

public class XMLConfigBuilder extends ConfigBuilder {

    private Element root;

    public XMLConfigBuilder(Reader reader) {
        // 1. 调用父类初始化Configuration
        super(new Configuration());
        // 2. dom4j 处理 xml
        SAXReader saxReader = new SAXReader();
        try {
            Document document = saxReader.read(new InputSource(reader));
            root = document.getRootElement();
        } catch (DocumentException e) {
            e.printStackTrace();
        }
    }

    /**
     * 解析配置;类型别名、插件、对象工厂、对象包装工厂、设置、环境、类型转换、映射器
     *
     * @return Configuration
     */
    public Configuration parse() {
        try {
            // 解析映射器
            mapperElement(root.element("mappers"));
        } catch (Exception e) {
            throw new RuntimeException("Error parsing SQL Mapper Configuration. Cause: " + e, e);
        }
        return configuration;
    }

    private void mapperElement(Element mappers) throws Exception {
        List<Element> mapperList = mappers.elements("mapper");
        for (Element e : mapperList) {
            String resource = e.attributeValue("resource");
            Reader reader = Resources.getResourceAsReader(resource);
            SAXReader saxReader = new SAXReader();
            Document document = saxReader.read(new InputSource(reader));
            Element root = document.getRootElement();
            //命名空间
            String namespace = root.attributeValue("namespace");

            // SELECT
            List<Element> selectNodes = root.elements("select");
            for (Element node : selectNodes) {
                String id = node.attributeValue("id");
                String parameterType = node.attributeValue("parameterType");
                String resultType = node.attributeValue("resultType");
                String sql = node.getText();

                // ? 匹配
                Map<Integer, String> parameter = new HashMap<>();
                Pattern pattern = Pattern.compile("(#\\{(.*?)})");
                Matcher matcher = pattern.matcher(sql);
                for (int i = 1; matcher.find(); i++) {
                    String g1 = matcher.group(1);
                    String g2 = matcher.group(2);
                    parameter.put(i, g2);
                    sql = sql.replace(g1, "?");
                }

                String msId = namespace + "." + id;
                String nodeName = node.getName();
                SqlCommandType sqlCommandType = SqlCommandType.valueOf(nodeName.toUpperCase(Locale.ENGLISH));
                MappedStatement mappedStatement = new MappedStatement.Builder(configuration, msId, sqlCommandType, parameterType, resultType, sql, parameter).build();
                // 添加解析 SQL
                configuration.addMappedStatement(mappedStatement);
            }

            // 注册Mapper映射器
            configuration.addMapper(Resources.classForName(namespace));
        }
    }
}
  • 我们在该类中需要从XML中解析到类型别名、插件、对象工厂、对象包装工厂、设置、环境、类型转换、映射器,但目前我们还不需要那么多,所以只做一些必要的 SQL 解析处理。
  • 我们增加了一个类MappedStatement,这个类代表一个接口方法的映射信息,我们将在下面介绍。

Configration配置类

现在我们来介绍存储配置信息的类Configration,目前我们需要的配置信息其实只有两个:

  • Mapper注册信息,就是我们之前实现的MapperRegistry
  • Mapper映射语句的信息,我们使用一个Map来实现,key是接口方法的全路径,value即MappedStatement映射语句的信息。
public class Configuration {

    /**
     * 映射注册机
     */
    protected MapperRegistry mapperRegistry = new MapperRegistry(this);

    /**
     * 映射的语句,存在Map里
     */
    protected final Map<String, MappedStatement> mappedStatements = new HashMap<>();

    public void addMappers(String packageName) {
        mapperRegistry.addMappers(packageName);
    }

    public <T> void addMapper(Class<T> type) {
        mapperRegistry.addMapper(type);
    }

    public <T> T getMapper(Class<T> type, SqlSession sqlSession) {
        return mapperRegistry.getMapper(type, sqlSession);
    }

    public boolean hasMapper(Class<?> type) {
        return mapperRegistry.hasMapper(type);
    }

    public void addMappedStatement(MappedStatement ms) {
        mappedStatements.put(ms.getId(), ms);
    }

    public MappedStatement getMappedStatement(String id) {
        return mappedStatements.get(id);
    }
}
  • 我们为两个属性分别提供了注册MappedStatement和注册Mapper的方法。

MappedStatement表示一个映射语句的信息:

public class MappedStatement {

    private Configuration configuration;
    private String id;
    private SqlCommandType sqlCommandType;

    private String parameterType;
    private String resultType;
    private String sql;
    private Map<Integer, String> parameter;

    MappedStatement() {
        // constructor disabled
    }
    /**
     * 建造者
     */
    public static class Builder {

        private MappedStatement mappedStatement = new MappedStatement();

        public Builder(Configuration configuration, String id, SqlCommandType sqlCommandType, String parameterType, String resultType, String sql, Map<Integer, String> parameter) {
            mappedStatement.configuration = configuration;
            mappedStatement.id = id;
            mappedStatement.sqlCommandType = sqlCommandType;
            mappedStatement.parameterType = parameterType;
            mappedStatement.resultType = resultType;
            mappedStatement.sql = sql;
            mappedStatement.parameter = parameter;
        }

        public MappedStatement build() {
            assert mappedStatement.configuration != null;
            assert mappedStatement.id != null;
            return mappedStatement;
        }

    }
  • 这个类保存着一个映射语句的信息,并且我们提供了一个Builder建造者来构建这个MappedStatement

MapperMethod和缓存

我们现在提供一个MapperMethod来封装一个映射方法:

public class MapperMethod {

    private final SqlCommand command;

    public MapperMethod(Class<?> mapperInterface, Method method, Configuration configuration) {
        this.command = new SqlCommand(configuration, mapperInterface, method);
    }

    public Object execute(SqlSession sqlSession, Object[] args) {
        return switch (command.getType()) {
            case INSERT -> sqlSession.selectOne(command.getName(), args);
            case DELETE -> sqlSession.selectOne(command.getName(), args);
            case UPDATE -> sqlSession.selectOne(command.getName(), args);
            case SELECT -> sqlSession.selectOne(command.getName(), args);
            default -> throw new RuntimeException("Unknown execution method for: " + command.getName());
        };
    }

    /**
     * SQL 指令
     */
    public static class SqlCommand {

        private final String name;
        private final SqlCommandType type;

        public SqlCommand(Configuration configuration, Class<?> mapperInterface, Method method) {
            String statementName = mapperInterface.getName() + "." + method.getName();
            MappedStatement ms = configuration.getMappedStatement(statementName);
            name = ms.getId();
            type = ms.getSqlCommandType();
        }

        public String getName() {
            return name;
        }

        public SqlCommandType getType() {
            return type;
        }
    }
}
  • SqlCommand包含两个信息,方法的全路径和sql的种类,创建MapperMethod时会同步解析出SqlCommand属性。
  • excute方法用于执行这个映射方法的,我们之后会将excute执行过程委托给其它来执行,目前我们只是打印出sql的一些信息。

总结

@Test
public void test_SqlSessionFactory() throws IOException {
    // 1. 从SqlSessionFactory中获取SqlSession
    Reader reader = Resources.getResourceAsReader("mybatis-config-datasource.xml");
    SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(reader);
    SqlSession sqlSession = sqlSessionFactory.openSession();

    // 2. 获取映射器对象
    IUserDao userDao = sqlSession.getMapper(IUserDao.class);

    // 3. 测试验证
    String res = userDao.queryUserInfoById("10001");
    logger.info("测试结果:{}", res);
}

至此,我们已经可以通过上面的代码得到下面的执行结果:

方法:test_03.dao.IUserDao.queryUserInfoById
入参:[Ljava.lang.Object;@704921a5
待执行SQLSELECT id, userId, userHead, createTime
        FROM user
        where id = ?

现在我们框架的启动流程大致如下:

image-20240129163427275

我们已经实现了这个框架启动的大致流程,我们下面的工作将会集中处理如何在数据库中调用sql语句并将结果返回

r(IUserDao.class);

// 3. 测试验证
String res = userDao.queryUserInfoById("10001");
logger.info("测试结果:{}", res);

}


至此,我们已经可以通过上面的代码得到下面的执行结果:

```java
方法:test_03.dao.IUserDao.queryUserInfoById
入参:[Ljava.lang.Object;@704921a5
待执行SQL:
        SELECT id, userId, userHead, createTime
        FROM user
        where id = ?

现在我们框架的启动流程大致如下:

image-20240129163427275

我们已经实现了这个框架启动的大致流程,我们下面的工作将会集中处理如何在数据库中调用sql语句并将结果返回

  • 8
    点赞
  • 14
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值