> 实战代理模式,模拟Mybatis - Yuyy
Yuyy
Yuyy
实战代理模式,模拟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

  • 通过反射获取的参数名不对

    http://bed.yuyy.info//20211018115035.png

    • 解决方式一:javac编译时增加参数javac -parameters

    • 解决方式二:maven设置编译参数,重新编译项目即可

    http://bed.yuyy.info//20211019110654.png

    • 成功获取到参数名

    http://bed.yuyy.info//20211018141902.png

注册工厂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配置文件

http://bed.yuyy.info//20211019094958.png

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

运行bean的postProcessBeanDefinitionRegistry()方法

http://bed.yuyy.info//20211019094927.png

  • bean实现了bean定义注册后置处理接口BeanDefinitionRegistryPostProcessor

  • 在spring初始化核心方法refresh中的这个阶段
    http://bed.yuyy.info//20211019093849.png

  • 将beanName为userDAO的bean定义,覆盖成我们写的工厂bean的bean定义

  • 将被代理类透传到工厂bean

创建工厂bean

http://bed.yuyy.info//20211019095135.png

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

    http://bed.yuyy.info//20211019095326.png

获取name为userDAO的bean

http://bed.yuyy.info//20211019095435.png

  • 指定需要的bean类型

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

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

    http://bed.yuyy.info//20211018092327.png

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

    http://bed.yuyy.info//20211019100339.png

  • 根据提供的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());
        }
    }    

运行代理类的增强方法

http://bed.yuyy.info//20211019100455.png

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

    http://bed.yuyy.info//20211019100916.png

发表评论

textsms
account_circle
email

Yuyy

实战代理模式,模拟Mybatis
实战代理模式,模拟Mybatis 在使用mybatis操作数据库时,我们只需要定义一个接口,然后在xml里编写对应的sql,就能查询数据。其原理是Mybatis通过@mypperscan指定扫描的mapper接口路径,…
扫描二维码继续阅读
2021-10-19
友情链接
标签
归档
近期文章
分类
近期文章