【别再做XX外卖啦!和我从零到1编写Mini版Easy-ES】完成一个Mapper模型

【从零到1编写Mini版Easy-ES】完成一个Mapper模型

作者:沈自在

代码仓库:gitee.com/tian-haoran...

本节教程分支:gitee.com/tian-haoran...

⚠️注意:本项目会持续更新,直到功能完善

1 前置知识

1.1 Spring 相关

1.1.1 什么是 FactoryBean接口?

很多同学都知道BeanFactory接口,这个是大名鼎鼎的Spring中的核心接口,IOC的根本所在。而这个FactoryBean的作用是用来创建一类bean,它的源代码是这样的:

csharp 复制代码
public interface FactoryBean<T> {
  // 获取 ObjectType 的一个对象
  T getObject() throws Exception;
  
  // 当前实现类所要创建的对象类型
  Class<?> getObjectType();
  
  default boolean isSingleton() {
    return true;
  }
}

上面这个图可以很简单的去概括这个接口的作用,就是要一个对象,然后给一个对象的逻辑。

1.1.1.1 小小的深入一点

对于 FactoryBean接口,其实还有一个子接口,叫做SmartFactoryBean

csharp 复制代码
public interface SmartFactoryBean<T> extends FactoryBean<T> {
  // 最核心的一个方法 --> 如果说这里返回 true 那么则会在 Spring容器初始化的时候就将这个Bean实例化
  default boolean isEagerInit() {
    return false;
  }
}

下面这段代码则是对于SmartFactoryBean的迫切加载在Spring中的体现:

scss 复制代码
// 这段代码来自 DefaultListableBeanFactory -> 922 行
public void preInstantiateSingletons() throws BeansException {
    // 省略部分代码
    for (String beanName : beanNames) {
      RootBeanDefinition bd = getMergedLocalBeanDefinition(beanName);
      if (!bd.isAbstract() && bd.isSingleton() && !bd.isLazyInit()) {
        if (isFactoryBean(beanName)) {
          Object bean = getBean(FACTORY_BEAN_PREFIX + beanName);
          // 核心点便是这里了
          if (bean instanceof FactoryBean) {
            FactoryBean<?> factory = (FactoryBean<?>) bean;
            boolean isEagerInit;
            if (System.getSecurityManager() != null && factory instanceof SmartFactoryBean) {
              isEagerInit = AccessController.doPrivileged(
                  (PrivilegedAction<Boolean>) ((SmartFactoryBean<?>) factory)::isEagerInit,
                  getAccessControlContext());
            }
            else {
              isEagerInit = (factory instanceof SmartFactoryBean &&
                  ((SmartFactoryBean<?>) factory).isEagerInit());
            }
            if (isEagerInit) {
              getBean(beanName);
            }
          }
        }
        else {
          getBean(beanName);
        }
      }
    }
​
  }

1.1.1.2 实战一把

对于FactoryBean的使用其实只分为俩步:

  • 实现FactoryBean接口
  • 将实现类注入Bean工厂

下面这段代码来自Easy ES的源码,同理也可以在Mybatis的底层代码中找到类似的设计(对SqlSession的FactoryBean),这样便相当于托管给实现类创建一类Bean的能力

java 复制代码
@Component // 注入BeanFactory
public class MapperFactoryBean<T> implements FactoryBean<T> {
​
​
    private final Class<T> mapperInterface;
​
    public MapperFactoryBean(Class<T> mapperInterface) {
        this.mapperInterface = mapperInterface;
    }
​
    @Override
    @SuppressWarnings("all")
    public T getObject() throws Exception {
        EsMapperProxy<T> esMapperProxy = new EsMapperProxy<>(mapperInterface);
        return (T) Proxy.newProxyInstance(mapperInterface.getClassLoader(), new Class[]{mapperInterface}, esMapperProxy);
    }
    
    // 这个便是这个FactoryBean所要创建的类型
    @Override
    public Class<?> getObjectType() {
        return this.mapperInterface;
    }
}
​

1.1.2 BeanDefinitionRegistryPostProcessor扩展点

java 复制代码
public interface BeanDefinitionRegistryPostProcessor extends BeanFactoryPostProcessor {
 void postProcessBeanDefinitionRegistry(BeanDefinitionRegistry var1) throws BeansException;
}

首先看一下BeanDefinitionRegistryPostProcessor的父类BeanFactoryPostProcessor,您可能对BeanDefinitionRegistryPostProcessor有些陌生,但想必对BeanFactoryPostProcessor一定不陌生吧,这个是在Spring容器刷新时,创建完BeanFactory后会调用的后置处理器

scss 复制代码
// 代码来自AbstractApplicationContext 545 行
public void refresh() throws BeansException, IllegalStateException {
    synchronized (this.startupShutdownMonitor) {
    
        StartupStep beanPostProcess = this.applicationStartup.start("spring.context.beans.post-process");
        // 嘿哥们,请注意,所有的BeanFactory后置处理器都是在这里被调用哒
        invokeBeanFactoryPostProcessors(beanFactory);
​
        // Register bean processors that intercept bean creation.
        registerBeanPostProcessors(beanFactory);
        beanPostProcess.end();
​
        // Initialize message source for this context.
        initMessageSource();
​
        // Initialize event multicaster for this context.
        initApplicationEventMulticaster();
​
        // Initialize other special beans in specific context subclasses.
        onRefresh();
​
        // Check for listener beans and register them.
        registerListeners();
​
        // Instantiate all remaining (non-lazy-init) singletons.
        finishBeanFactoryInitialization(beanFactory);
​
        // Last step: publish corresponding event.
        finishRefresh();
      }
  }

从上一步点进去之后就可以看到下面这段:

scss 复制代码
// 代码来自 AbstractApplicationContext 746行
protected void invokeBeanFactoryPostProcessors(ConfigurableListableBeanFactory beanFactory) {
    // 再从 invokeBeanFactoryPostProcessors 这里点进去
    PostProcessorRegistrationDelegate.invokeBeanFactoryPostProcessors(beanFactory, getBeanFactoryPostProcessors());
​
    // Detect a LoadTimeWeaver and prepare for weaving, if found in the meantime
    // (e.g. through an @Bean method registered by ConfigurationClassPostProcessor)
    if (!NativeDetector.inNativeImage() && beanFactory.getTempClassLoader() == null && beanFactory.containsBean(LOAD_TIME_WEAVER_BEAN_NAME)) {
      beanFactory.addBeanPostProcessor(new LoadTimeWeaverAwareProcessor(beanFactory));
      beanFactory.setTempClassLoader(new ContextTypeMatchClassLoader(beanFactory.getBeanClassLoader()));
    }
  }

接下来:

ini 复制代码
// 代码来自:PostProcessorRegistrationDelegate 78行
if (beanFactory instanceof BeanDefinitionRegistry) {
      BeanDefinitionRegistry registry = (BeanDefinitionRegistry) beanFactory;
      List<BeanFactoryPostProcessor> regularPostProcessors = new ArrayList<>();
      List<BeanDefinitionRegistryPostProcessor> registryProcessors = new ArrayList<>();
​
      for (BeanFactoryPostProcessor postProcessor : beanFactoryPostProcessors) {
        // 查出来所有的 BeanDefinitionRegistryPostProcessor 后置处理器
        if (postProcessor instanceof BeanDefinitionRegistryPostProcessor) {
          BeanDefinitionRegistryPostProcessor registryProcessor =
              (BeanDefinitionRegistryPostProcessor) postProcessor;
          registryProcessor.postProcessBeanDefinitionRegistry(registry);
          registryProcessors.add(registryProcessor);
        }
        else {
          regularPostProcessors.add(postProcessor);
        }
      }
    }
 // 执行它们!!!!
 invokeBeanFactoryPostProcessors(registryProcessors, beanFactory);

这便是这个扩展点的渊源啦

1.1.2.1 如何使用呢?

java 复制代码
// 这是在手写Mini版本Easy Es 中初期的一段代码,用于替换Mapper的扫描和BeanDefinition
@Component
public class MapperScannerRegister implements BeanDefinitionRegistryPostProcessor {
​
    @Override
    public void postProcessBeanDefinitionRegistry(BeanDefinitionRegistry registry) throws BeansException {
        // 1. 扫描包
        Set<Class<?>> classes = ClassScanner.scanPackage("tax.szz.mini.test.mapper");
        for (Class<?> clazz : classes) {
​
            // 1. 创建 BeanDefinition
            RootBeanDefinition beanDefinition = new RootBeanDefinition(clazz);
            String beanClassName = clazz.getName();
​
            // 2. 设置 BeanName
            beanDefinition.setBeanClassName(beanClassName);
            beanDefinition.getConstructorArgumentValues().addGenericArgumentValue(beanClassName);
            beanDefinition.setAutowireMode(AbstractBeanDefinition.AUTOWIRE_BY_TYPE);
            beanDefinition.setBeanClass(MapperFactoryBean.class);
​
            registry.registerBeanDefinition(StrUtil.lowerFirst(clazz.getSimpleName()), beanDefinition);
        }
    }
​
    @Override
    public void postProcessBeanFactory(ConfigurableListableBeanFactory beanFactory) throws BeansException {
​
    }
}

1.1.3 Spring boot 的 spring.factories 机制

Spring Bootspring.factories 配置机制类似于 Java SPI,工程代码中在 META-INF/spring.factories 文件中配置接口的实现类名称,然后 Spring Boot 在启动时扫描该配置文件并实例化配置文件中的Bean

比如:

ini 复制代码
org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
  tax.szz.mini.core.EsAutoConfiguration

Spring框架则会自己去扫描这个文件夹并把配置的这个类加载并且实例化

ini 复制代码
// 代码来自 SpringFactoriesLoader 95行 (如果你看过Dubbo SPI部分的源码的话会发现逻辑其实大差不差的)
 public static <T> List<T> loadFactories(Class<T> factoryType, @Nullable ClassLoader classLoader) {
    Assert.notNull(factoryType, "'factoryType' must not be null");
    ClassLoader classLoaderToUse = classLoader;
    if (classLoaderToUse == null) {
      classLoaderToUse = SpringFactoriesLoader.class.getClassLoader();
    }
    List<String> factoryImplementationNames = loadFactoryNames(factoryType, classLoaderToUse);
    if (logger.isTraceEnabled()) {
      logger.trace("Loaded [" + factoryType.getName() + "] names: " + factoryImplementationNames);
    }
    List<T> result = new ArrayList<>(factoryImplementationNames.size());
    for (String factoryImplementationName : factoryImplementationNames) {
      // 同时在这里你会发现,Spring会讲这些扫描到的类一个一个全部实例化
      result.add(instantiateFactory(factoryImplementationName, factoryType, classLoaderToUse));
    }
    AnnotationAwareOrderComparator.sort(result);
    return result;
  }

而这也是SpringBoot插件机制的来源(Starter)

2.2 动态代理相关

2.2.1 什么是动态代理?

动态代理是指在运行时创建代理对象的过程,而不是在编译时确定。JDK动态代理利用Java的反射机制,在运行时动态生成代理类和代理对象,从而实现代理功能。这意味着我们可以在运行时为任何接口创建代理对象,而无需手动编写代理类。

2.2.2 如何使用JDK动态代理?

使用JDK动态代理非常简单,只需遵循以下几个步骤:

  1. 定义一个接口:首先,我们需要定义一个接口,该接口将成为代理对象和被代理对象之间的契约。接口应包含代理对象和被代理对象共同的方法。
  2. 实现被代理类:创建一个实现接口的被代理类,该类将包含实际的业务逻辑。
  3. 创建InvocationHandler:实现InvocationHandler接口,并重写invoke方法。invoke方法将在代理对象的方法被调用时执行,我们可以在该方法中添加额外的逻辑。
  4. 创建代理对象:使用Proxy类的newProxyInstance方法创建代理对象。该方法接受三个参数:类加载器、接口数组和InvocationHandler对象。通过调用该方法,我们将得到一个实现了指定接口的代理对象。
  5. 使用代理对象:现在,我们可以使用代理对象来调用接口中定义的方法。代理对象会在调用方法时自动触发InvocationHandlerinvoke方法,从而允许我们在方法调用前后添加自定义的逻辑。

2.2.3 一个简单的案例

java 复制代码
// 这段代码来自 Easy ES 中,可以直接 双击 shift 查到
// 而这段代码的作用就是对 Mapper 进行代理,同时可以在MapperFactoryBean(下面那个代码块)中也可以看到它的核心逻辑就是 (定义Mapper 接口 -> 创建 Mapper 代理)
public class EsMapperProxy<T> implements InvocationHandler, Serializable {
​
    private final Class<T> mapperInterface;
​
    public EsMapperProxy(Class<T> mapperInterface) {
        this.mapperInterface = mapperInterface;
    }
​
    @Override
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
        BaseEsMapper<?> baseEsMapper = new BaseEsMapperImpl<>();
        return method.invoke(baseEsMapper, args);
    }
}
​
java 复制代码
public class MapperFactoryBean<T> implements FactoryBean<T> {
​
​
    private final Class<T> mapperInterface;
​
    public MapperFactoryBean(Class<T> mapperInterface) {
        this.mapperInterface = mapperInterface;
    }
​
    @Override
    @SuppressWarnings("all")
    public T getObject() throws Exception {
        EsMapperProxy<T> esMapperProxy = new EsMapperProxy<>(mapperInterface);
        return (T) Proxy.newProxyInstance(mapperInterface.getClassLoader(), new Class[]{mapperInterface}, esMapperProxy);
    }
​
    @Override
    public Class<?> getObjectType() {
        return this.mapperInterface;
    }
}
​

2.2.4 动态代理的优势和应用场景

使用JDK动态代理有以下几个优势:

  • 灵活性:动态代理允许我们在运行时为任意接口创建代理对象,无需手动编写代理类,从而提供了更大的灵活性。
  • 解耦合:代理模式可以将代理对象和被代理对象解耦,使得它们可以独立进行修改和扩展。
  • 横切关注点处理 :动态代理可以用于处理横切关注点,例如日志记录、性能监控、事务管理等。我们可以通过在InvocationHandlerinvoke方法中添加相应的逻辑来实现这些功能。

JDK动态代理在以下场景中特别有用:

  • 日志记录 :通过在InvocationHandler中添加日志记录逻辑,我们可以方便地记录方法的调用信息,用于调试和分析。
  • 事务管理 :通过在InvocationHandler中添加事务管理逻辑,我们可以实现对方法的事务性控制,例如开启事务、提交事务、回滚事务等。
  • 权限控制 :通过在InvocationHandler中添加权限验证逻辑,我们可以对方法的调用进行权限控制,以确保只有具备相应权限的用户才能执行特定操作。

2 Mapper 模型设计

首先我们去分析一下要创建一个Mapper的映射需要做哪些工作:

  • 第一:需要扫描到 Mapper
  • 第二:Mapper 只是一个接口,我们需要提供一个实际操作(肯定是动态代理啦)

工程结构如下:

markdown 复制代码
.
| |____src
| | |____main
| | | |____resources
| | | |____java
|____pom.xml
|____mini-easy-es-core
| |____src
| | |____main
| | | |____resources
| | | | |____META-INF
| | | | | |____spring.factories
| | | |____java
| | | | |____tax
| | | | | |____szz
| | | | | | |____mini
| | | | | | | |____core
| | | | | | | | |____core
| | | | | | | | | |____BaseEsMapper.java
| | | | | | | | | |____BaseEsMapperImpl.java
| | | | | | | | |____proxy
| | | | | | | | | |____EsMapperProxy.java
| | | | | | | | |____register
| | | | | | | | | |____MapperFactoryBean.java
| | | | | | | | |____factory
| | | | | | | | | |____MapperScannerRegister.java
| | | | | | | | |____EsAutoConfiguration.java
|____mini-easy-es-test
| |____src
| | |____test
| | | |____java
| | | | |____tax
| | | | | |____szz
| | | | | | |____mini
| | | | | | | |____test
| | | | | | | | |____TestSpringApplication.java
| | | | | | | | |____api
| | | | | | | | | |____course_02
| | | | | | | | | | |____ApiTest.java
| | |____main
| | | |____java
| | | | |____tax
| | | | | |____szz
| | | | | | |____mini
| | | | | | | |____test
| | | | | | | | |____mapper
| | | | | | | | | |____DocumentMapper.java
| | | | | | | | |____document
| | | | | | | | | |____Document.java
​

2.1 抽象Mapper

众所周知,一个Mapper其实就是一个接口,比如我们在使用MybatisPlus时候,可能会去继承BaseMapper以获得一些基础功能比如:

  • selectOne()
  • save()
  • 。。。

那我们也借鉴这种思想去定义一个 BaseEsMapper

csharp 复制代码
public interface BaseEsMapper<T> {
    // 粗浅定义一个方法去创建索引
    Boolean createIndex(String indexName);
}
​

接下来有了基础方法,那么肯定需要对方法进行实现啦,我们编写一个BaseEsMapperImpl对此进行实现

kotlin 复制代码
public class BaseEsMapperImpl<T> implements BaseEsMapper<T> {
    @Override
    public Boolean createIndex(String indexName) {
        System.out.println("创建 Index");
        System.out.println("indexName = " + indexName);
        return Boolean.TRUE;
    }
}

okok stop, 听我say一下

我们在用Mybatis Plus的时候是不是经常会有这样的写法:

kotlin 复制代码
public class ApiTest {
​
    @Autowired
    private UserMapper userMapper;
}

对,没错,Mapper是要注入到Spring容器当中的,只有这样,我们才可以用注解去自动注入

(敲黑板)⚠️注意: 现在我们的BaseEsMapper还没有和我们将来自己的业务中的Mapper产生关系。那么缕一下思路,我们还差什么:

  • 业务Mapper要和BaseEsMapper产生一个关系
  • Mapper要注入到Spring中

那么这个绑定关系就要用动态代理来维持了,比如下面这种实现

java 复制代码
// 我们对 Mapper 接口代理和 BaseMapper 产生一个代理绑定关系
public class EsMapperProxy<T> implements InvocationHandler, Serializable {
​
    private final Class<T> mapperInterface;
​
    public EsMapperProxy(Class<T> mapperInterface) {
        this.mapperInterface = mapperInterface;
    }
​
    @Override
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
        BaseEsMapper<?> baseEsMapper = new BaseEsMapperImpl<>();
        return method.invoke(baseEsMapper, args);
    }
}

接下来就是要把这种关系交给Spring来维持:

所谓维持就分为俩个点:

  • Mapper对象的创建
  • Mapper的注入

首先解决第一个问题,创建一个Mapper对象怎么办,很明显这是一类对象的创建,这个特点不就和 FactoryBean 很类似嘛

java 复制代码
// 这下当要去获取某个Mapper接口的时候,不久会调用getObject()拿到我们提供的Mapper和BaseMapper之间的绑定代理了嘛,神奇的一批
public class MapperFactoryBean<T> implements FactoryBean<T> {
​
​
    private final Class<T> mapperInterface;
​
    public MapperFactoryBean(Class<T> mapperInterface) {
        this.mapperInterface = mapperInterface;
    }
​
    @Override
    @SuppressWarnings("all")
    public T getObject() throws Exception {
        EsMapperProxy<T> esMapperProxy = new EsMapperProxy<>(mapperInterface);
        return (T) Proxy.newProxyInstance(mapperInterface.getClassLoader(), new Class[]{mapperInterface}, esMapperProxy);
    }
​
    @Override
    public Class<?> getObjectType() {
        return this.mapperInterface;
    }
}

那么现在不就剩下了最后一步------注册Mapper了嘛,请看下文~~

2.2 注册Mapper

对于注册Mapper一般会有俩个方法:

  • 自定义Scanner,比如去继承ClassPathBeanDefinitionScanner然后重写 doScan() 方法
  • 实现BeanDefinitionRegistryPostProcessor,自己去扫描Mapper接口然后封装BeanDefinition注册

都可以解决问题,这里先暂且用BeanDefinitionRegistryPostProcessor顶着,这样逻辑会更清晰一点

java 复制代码
public class MapperScannerRegister implements BeanDefinitionRegistryPostProcessor {
​
    @Override
    public void postProcessBeanDefinitionRegistry(BeanDefinitionRegistry registry) throws BeansException {
        // 1. 扫描包
        Set<Class<?>> classes = ClassScanner.scanPackage("tax.szz.mini.test.mapper");
        for (Class<?> clazz : classes) {
​
            // 1. 创建 BeanDefinition
            RootBeanDefinition beanDefinition = new RootBeanDefinition(clazz);
            String beanClassName = clazz.getName();
​
            // 2. 设置 Bean的一些属性
            beanDefinition.setBeanClassName(beanClassName);
            beanDefinition.getConstructorArgumentValues().addGenericArgumentValue(beanClassName);
            beanDefinition.setAutowireMode(AbstractBeanDefinition.AUTOWIRE_BY_TYPE);
          // ⚠️注意这里(偷梁换柱),假设这里是UserMapper 那么这样不就会在获取 userMapper Bean的时候去调用MapperFactoryBean去拿对象啦,一环扣一环就这样建立了联系
            beanDefinition.setBeanClass(MapperFactoryBean.class);
            // 3. 注册 BeanDefinition
            registry.registerBeanDefinition(StrUtil.lowerFirst(clazz.getSimpleName()), beanDefinition);
        }
    }
​
    @Override
    public void postProcessBeanFactory(ConfigurableListableBeanFactory beanFactory) throws BeansException {
​
    }
}

2.3 自动注册

resources中创建目录META-INF并且在该目录下添加文件spring.factories

ini 复制代码
org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
  tax.szz.mini.core.EsAutoConfiguration
typescript 复制代码
// 这样就完成了一个基本的自动加载
@Configuration
public class EsAutoConfiguration {
​
    @Bean
    public MapperScannerRegister mapperScannerRegister() {
        return new MapperScannerRegister();
    }
}

2.4 测试一下

下面是测试工程结构:

markdown 复制代码
.
|____src
| |____test
| | |____java
| | | |____tax
| | | | |____szz
| | | | | |____mini
| | | | | | |____test
| | | | | | | |____TestSpringApplication.java
| | | | | | | |____api
| | | | | | | | |____course_02
| | | | | | | | | |____ApiTest.java
| |____main
| | |____java
| | | |____tax
| | | | |____szz
| | | | | |____mini
| | | | | | |____test
| | | | | | | |____mapper
| | | | | | | | |____DocumentMapper.java
| | | | | | | |____document
| | | | | | | | |____Document.java
​
kotlin 复制代码
public class Document {
}
​
csharp 复制代码
public interface DocumentMapper extends BaseEsMapper<DocumentMapper> {
​
}
less 复制代码
@Disabled
@SpringBootTest(classes = TestSpringApplication.class)
public class ApiTest {
​
    @Autowired
    private DocumentMapper documentMapper;
​
    @Test
    void test(){
        documentMapper.createIndex("hello");
    }
}
typescript 复制代码
@SpringBootApplication
public class TestSpringApplication {
​
    public static void main(String[] args) {
        SpringApplication.run(TestSpringApplication.class, args);
    }
}
​

那么执行这个测试的结果就是:

相关推荐
【D'accumulation】30 分钟前
典型的MVC设计模式:使用JSP和JavaBean相结合的方式来动态生成网页内容典型的MVC设计模式
java·设计模式·mvc
试行44 分钟前
Android实现自定义下拉列表绑定数据
android·java
茜茜西西CeCe1 小时前
移动技术开发:简单计算器界面
java·gitee·安卓·android-studio·移动技术开发·原生安卓开发
救救孩子把1 小时前
Java基础之IO流
java·开发语言
小菜yh1 小时前
关于Redis
java·数据库·spring boot·redis·spring·缓存
宇卿.1 小时前
Java键盘输入语句
java·开发语言
浅念同学1 小时前
算法.图论-并查集上
java·算法·图论
立志成为coding大牛的菜鸟.1 小时前
力扣1143-最长公共子序列(Java详细题解)
java·算法·leetcode
鱼跃鹰飞1 小时前
Leetcode面试经典150题-130.被围绕的区域
java·算法·leetcode·面试·职场和发展·深度优先
爱上语文3 小时前
Springboot的三层架构
java·开发语言·spring boot·后端·spring