天天看點

實戰代理模式,模拟Mybatis實戰代理模式,模拟Mybatis

實戰代理模式,模拟Mybatis

在使用mybatis操作資料庫時,我們隻需要定義一個接口,然後在xml裡編寫對應的sql,就能查詢資料。其原理是Mybatis通過

@mypperscan

指定掃描的mapper接口路徑,對mapper接口進行動态代理,生成的代理類通過解析xml得到對應sql,最終開發人員隻需要調用接口就能執行sql了。

今天我們來實作一個簡單的demo,利用動态代理,模拟mybatis查詢資料。

代碼實作

自定義注解

@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.METHOD})
public @interface Select {
    // sql語句
    String value() default "";
}           

複制

DAO層接口使用注解綁定sql

public interface IUserDAO {
    @Select("'select user from user where userId = ' + #userId")
    String queryUser(String userId);
}           

複制

生産代理類bean的工廠bean

public class MapperFactoryBean<T> implements FactoryBean<T> {

    private Class<T> mapperInterface;

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

    @Override
    public T getObject() {
        InvocationHandler handler = (proxy, method, args) -> {
            final StandardEvaluationContext context = new StandardEvaluationContext();
            final Parameter[] parameters = method.getParameters();
            for (int i = 0; i < parameters.length; i++) {
                context.setVariable(parameters[i].getName(), args[i]);
            }

            final Select select = method.getAnnotation(Select.class);
            final SpelExpressionParser parser = new SpelExpressionParser();
            final String sql = parser.parseExpression(select.value())
                                     .getValue(context, String.class);
            System.out.println("執行SQL:" + sql);
            return "查詢結果:xxx";
        };
        return (T) Proxy.newProxyInstance(this.getClass().getClassLoader(), new Class[]{mapperInterface}, handler);
    }

    @Override
    public Class<?> getObjectType() {
        return mapperInterface;
    }

    @Override
    public boolean isSingleton() {
        return true;
    }
}           

複制

  • 由于需要定制化bean(DAO接口的動态代理類),這裡定義了個工廠bean,來生産代理類bean。
  • getObjectType()

    回報生産的bean的類型,使用構造函數透傳被代理類,在mybatis中也是使用這樣的方式進行透傳。
  • 将參數名,參數值放入

    SpEL

    表達式的上下文裡,再去解析出真正的sql
  • 通過反射擷取的參數名不對
實戰代理模式,模拟Mybatis實戰代理模式,模拟Mybatis
  • 解決方式一:javac編譯時增加參數

    javac -parameters

  • 解決方式二:maven設定編譯參數,重新編譯項目即可
實戰代理模式,模拟Mybatis實戰代理模式,模拟Mybatis
  • 成功擷取到參數名
實戰代理模式,模拟Mybatis實戰代理模式,模拟Mybatis

注冊工廠bean的bean定義到容器

public class RegisterBeanFactory implements BeanDefinitionRegistryPostProcessor {

    @Override
    public void postProcessBeanDefinitionRegistry(BeanDefinitionRegistry registry) throws BeansException {
        final GenericBeanDefinition beanDefinition = new GenericBeanDefinition();
        beanDefinition.setBeanClass(MapperFactoryBean.class);
        beanDefinition.setScope("singleton");
        beanDefinition.getConstructorArgumentValues()
                      .addGenericArgumentValue(IUserDAO.class);

        final BeanDefinitionHolder definitionHolder = new BeanDefinitionHolder(beanDefinition, "userDAO");
        BeanDefinitionReaderUtils.registerBeanDefinition(definitionHolder, registry);
    }

    @Override
    public void postProcessBeanFactory(ConfigurableListableBeanFactory beanFactory) throws BeansException {

    }
}           

複制

  • 覆寫name為

    userDAO

    的bean,下面會定義個同名的bean

注入到IOC裡

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-3.0.xsd"
       default-autowire="byName">

  <bean id="userDAO" class="com.yuyy.java.training.base.動态代理模拟Mybatis.RegisterBeanFactory"/>

</beans>           

複制

  • beanName同樣設定為

    userDAO

測試

@Test
public void test_IUserDao() {
    BeanFactory beanFactory = new ClassPathXmlApplicationContext("動态代理模拟Mybatis/spring-config.xml");
    IUserDAO userDao = beanFactory.getBean("userDAO", IUserDAO.class);
    System.out.println(userDao.queryUser("100001"));
}           

複制

結果

執行SQL:select user from user where userId = 100001
查詢結果:xxx           

複制

分析流程

解析spring配置檔案

實戰代理模式,模拟Mybatis實戰代理模式,模拟Mybatis
  • 讀取到name為userDAO,class為

    com.yuyy.java.training.base.動态代理模拟Mybatis.RegisterBeanFactory

    的bean

運作bean的postProcessBeanDefinitionRegistry()方法

實戰代理模式,模拟Mybatis實戰代理模式,模拟Mybatis
  • bean實作了bean定義注冊後置處理接口BeanDefinitionRegistryPostProcessor
  • 在spring初始化核心方法refresh中的這個階段
實戰代理模式,模拟Mybatis實戰代理模式,模拟Mybatis
  • 将beanName為userDAO的bean定義,覆寫成我們寫的工廠bean的bean定義
  • 将被代理類透傳到工廠bean

建立工廠bean

實戰代理模式,模拟Mybatis實戰代理模式,模拟Mybatis
  • 在refresh中的執行個體化所有非懶加載單例bean階段
實戰代理模式,模拟Mybatis實戰代理模式,模拟Mybatis

擷取name為userDAO的bean

實戰代理模式,模拟Mybatis實戰代理模式,模拟Mybatis
  • 指定需要的bean類型

通過工廠bean生産我們需要的對象

如果不是FactoryBean就直接傳回bean,如果是,就通過工廠bean來生産需要的bean

實戰代理模式,模拟Mybatis實戰代理模式,模拟Mybatis

得到工廠bean生産的動态代理類對象

實戰代理模式,模拟Mybatis實戰代理模式,模拟Mybatis

根據提供的bean類型做類型轉換

AbstractBeanFactory#doGetBean()

if (requiredType != null && bean != null && !requiredType.isInstance(bean)) {
    try {
        return getTypeConverter().convertIfNecessary(bean, requiredType);
    }
    catch (TypeMismatchException ex) {
        if (logger.isDebugEnabled()) {
            logger.debug("Failed to convert bean '" + name + "' to required type '" +
                    ClassUtils.getQualifiedName(requiredType) + "'", ex);
        }
        throw new BeanNotOfRequiredTypeException(name, requiredType, bean.getClass());
    }
}               

複制

運作代理類的增強方法

實戰代理模式,模拟Mybatis實戰代理模式,模拟Mybatis
  • 通過lambda表達式傳入的增強方法
實戰代理模式,模拟Mybatis實戰代理模式,模拟Mybatis