实战代理模式,模拟Mybatis

2022-09-21 10:06:42 浏览数 (1)

实战代理模式,模拟Mybatis

在使用mybatis操作数据库时,我们只需要定义一个接口,然后在xml里编写对应的sql,就能查询数据。其原理是Mybatis通过@mypperscan指定扫描的mapper接口路径,对mapper接口进行动态代理,生成的代理类通过解析xml得到对应sql,最终开发人员只需要调用接口就能执行sql了。

今天我们来实现一个简单的demo,利用动态代理,模拟mybatis查询数据。

代码实现

自定义注解

代码语言:javascript复制
@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.METHOD})
public @interface Select {
    // sql语句
    String value() default "";
}

DAO层接口使用注解绑定sql

代码语言:javascript复制
public interface IUserDAO {
    @Select("'select user from user where userId = '   #userId")
    String queryUser(String userId);
}

生产代理类bean的工厂bean

代码语言:javascript复制
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
  • 通过反射获取的参数名不对
  • 解决方式一:javac编译时增加参数javac -parameters
  • 解决方式二:maven设置编译参数,重新编译项目即可
  • 成功获取到参数名

注册工厂bean的bean定义到容器

代码语言:javascript复制
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里

代码语言:javascript复制
<?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

测试

代码语言:javascript复制
@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"));
}

结果

代码语言:javascript复制
执行SQL:select user from user where userId = 100001
查询结果:xxx

分析流程

解析spring配置文件

  • 读取到name为userDAO,class为com.yuyy.java.training.base.动态代理模拟Mybatis.RegisterBeanFactory的bean

运行bean的postProcessBeanDefinitionRegistry()方法

  • bean实现了bean定义注册后置处理接口BeanDefinitionRegistryPostProcessor
  • 在spring初始化核心方法refresh中的这个阶段
  • 将beanName为userDAO的bean定义,覆盖成我们写的工厂bean的bean定义
  • 将被代理类透传到工厂bean

创建工厂bean

  • 在refresh中的实例化所有非懒加载单例bean阶段

获取name为userDAO的bean

  • 指定需要的bean类型

通过工厂bean生产我们需要的对象

如果不是FactoryBean就直接返回bean,如果是,就通过工厂bean来生产需要的bean

得到工厂bean生产的动态代理类对象

根据提供的bean类型做类型转换 AbstractBeanFactory#doGetBean()

代码语言:javascript复制
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());
    }
}    

运行代理类的增强方法

  • 通过lambda表达式传入的增强方法

0 人点赞