【读Ngbatis源码记】当启动Ngbatis时,发生了什么

点进这篇文章的同学可能不了解Ngbatis,但是说起mybatis应该不陌生。

Ngbatis是为NebulaGraph图数据库量身定制一款ORM框架,它允许开发者通过XML映射或Java注解来编写NebulaGraph查询语言(nGQL),实现与图数据库的交互。它借鉴了MyBatis的使用习惯进行开发,并包含了一些类似于mybatis-plus的单表操作,同时还包括了图数据库特有的实体-关系基本操作。

这里先贴上仓库地址:github.com/nebula-cont…,欢迎来玩~

这篇文章的主要目的是帮助自己更好地熟悉ngbatis源码,做的一些笔记。

先贴个ngbatis启动时初始化过程的时序图,来源于ngbatis官网,地址贴到最后了

从启动类出发:NgbatisContextInitializer

NgbatisContextInitializer 方法主要作用是为了初始化一些配置信息。包括 NebulaPoolConfig 连接池初始化、NgbatisConfig Ngbatis的配置信息初始化、NebulaJdbcProperties 连接配置初始化等。并在初始化时加入了 NgbatisBeanFactoryPostProcessor 前置处理器。

1.执行initialize方法,从配置文件中读取配置信息

 

java

代码解读

复制代码

@Override public void initialize(ConfigurableApplicationContext context) { //确保 Env 能够使用Spring管理的类加载器 Env.classLoader = context.getClassLoader(); //获取Spring应用上下文的可配置环境对象 ConfigurableEnvironment environment = context.getEnvironment(); //①调用NgbatisContextInitializer.getNebulaPoolConfig方法,实例化一个nebula数据库连接池,且加载自定义配置 NebulaPoolConfig nebulaPool = getNebulaPoolConfig(environment); //②调用NgbatisContextInitializer.getNebulaNgbatisConfig方法,加载yaml中关于ngbatis的自定义配置 NgbatisConfig ngbatisConfig = getNebulaNgbatisConfig(environment); //③调用NgbatisContextInitializer.getNebulaJdbcProperties方法读取并应用 Nebula 数据库连接的配置信息 if (environment.getProperty("nebula.hosts") != null) { NebulaJdbcProperties nebulaJdbcProperties = getNebulaJdbcProperties(environment) .setPoolConfig(nebulaPool) .setNgbatis(ngbatisConfig); ParseCfgProps parseCfgProps = readParseCfgProps(environment); //注册一个新的 NgbatisBeanFactoryPostProcessor context.addBeanFactoryPostProcessor( new NgbatisBeanFactoryPostProcessor(nebulaJdbcProperties, parseCfgProps, context) ); } }

Env类是ngbatis框架存储全局环境信息的地方,具体如下:

整理了一份Java面试题。这份面试笔记包括了:Java面试、Spring、JVM、MyBatis、Redis、MySQL、并发编程、微服务、Linux、Springboot、SpringCloud、MQ、Kafka 面试专题

需要全套面试笔记【点击此处】即可免费获取

java

代码解读

复制代码

public class Env { //类加载器 public static ClassLoader classLoader; // 使用 fastjson 安全模式,规避任意代码执行风险 static { ParserConfig.getGlobalInstance().setSafeMode(true); } private Logger log = LoggerFactory.getLogger(Env.class); // 模板引擎 默认 beetl private TextResolver textResolver; // 结果集路由 private ResultResolver resultResolver; // 参数解析器 private ArgsResolver argsResolver; // 参数名格式化器 private ArgNameFormatter argNameFormatter; // 解析的参数配置 private ParseCfgProps cfgProps; // 应用上下文 private ApplicationContext context; // 用户名 private String username; // 密码 private String password; // 连接是否支持重连 private boolean reconnect = false; // 图空间 private String space; // 主键生成器 private PkGenerator pkGenerator; // 本地会话调度器 private SessionDispatcher dispatcher; // xml 中标签所声明的信息或方法类 private MapperContext mapperContext; ...构造器 /** * 获取 Nebula SessionPool * @return SessionPool */ public SessionPool getSessionPool(String spaceName) { return mapperContext.getNebulaSessionPoolMap().get(spaceName); } /** * <p>获取nebula graph的会话。</p> * @return session */ public Session openSession() { try { return mapperContext.getNebulaPool().getSession(username, password, reconnect); } catch (Throwable e) { throw new RuntimeException(e); } } ...get和set方法 }

① 调用本类的getNebulaPoolConfig方法如下:
 

java

代码解读

复制代码

private NebulaPoolConfig getNebulaPoolConfig(ConfigurableEnvironment environment) { NebulaPoolConfig nebulaPoolConfig = new NebulaPoolConfig() .setMinConnSize( environment.getProperty("nebula.pool-config.min-conns-size", Integer.class, 0)) .setMaxConnSize( environment.getProperty("nebula.pool-config.max-conns-size", Integer.class, 10)) .setTimeout(environment.getProperty("nebula.pool-config.timeout", Integer.class, 0)) .setIdleTime(environment.getProperty("nebula.pool-config.idle-time", Integer.class, 0)) .setIntervalIdle( environment.getProperty("nebula.pool-config.interval-idle", Integer.class, -1)) .setWaitTime(environment.getProperty("nebula.pool-config.wait-time", Integer.class, 0)); return nebulaPoolConfig; }

application.yaml文件中关于库的配置如下:

 

yaml

代码解读

复制代码

nebula: pool-config: min-conns-size: 0 max-conns-size: 10 timeout: 6000 idle-time: 0 interval-idle: -1 wait-time: 6000 min-cluster-health-rate: 1.0 enable-ssl: false

问题:为什么可以从environment直接获取yaml中关于数据库的配置?

Spring Boot 在启动时会自动加载 application.yml 或 application.properties 文件中的配置,并将其放入环境中。

问题:这些关于连接池的配置项是什么意思?

  1. min-conns-size: 最小连接数。这是连接池始终维持的最小空闲连接数。即使没有活动数据库操作,连接池也至少会有这么多连接已经建立并等待被使用。
  2. max-conns-size: 最大连接数。这是连接池允许的最大连接数。超过这个数量的连接请求将被排队,直到有可用的连接。
  3. timeout: 超时时间。这是数据库操作的超时时间,单位通常是秒或毫秒。如果一个数据库操作超过这个时间限制还没有完成,它将抛出一个超时异常。
  4. idle-time: 空闲时间。这是连接在被关闭之前可以在池中保持空闲状态的最长时间。超过这个时间限制的空闲连接将被回收。
  5. interval-idle: 空闲间隔。这个配置可能用于设置检测并回收空闲连接的时间间隔。在每个间隔之后,连接池可能会检查并关闭一些空闲时间超过 idle-time 的连接。
  6. wait-time: 等待时间。这是当连接池中没有可用连接时,连接请求必须等待的最长时间。如果在这个时间内没有可用的连接返回到池中,请求可能会失败或抛出异常。
② 调用本类的getNebulaNgbatisConfig方法如下:
 

java

代码解读

复制代码

private NgbatisConfig getNebulaNgbatisConfig(ConfigurableEnvironment environment) { return new NgbatisConfig() .setSessionLifeLength( // environment.getProperty("nebula.ngbatis.session-life-length", Long.class) ) .setCheckFixedRate( environment.getProperty("nebula.ngbatis.check-fixed-rate", Long.class) ) .setUseSessionPool( environment.getProperty("nebula.ngbatis.use-session-pool", Boolean.class) ); }

application.yaml文件中关于ngbatis的配置如下:

 

yaml

代码解读

复制代码

nebula: ngbatis: session-life-length: 300000 check-fixed-rate: 300000 # space name needs to be informed through annotations(@Space) or xml(space="test") # default false(false: Session pool map will not be initialized) use-session-pool: false

问题:这些关于ngbatis的配置项是什么意思?

  • session-life-length:会话存活有效期,300000毫秒即5分钟。一个会话在没有活动时可以保持打开状态的最长时间。超过这个时间限制后,会话将自动关闭,以释放资源。
  • checkFixedRate:会话健康检查的固定间隔时间。
  • use-session-pool:是否使用 Nebula-java 的会话池。
③ 调用本类的getNebulaJdbcProperties方法如下:
 

java

代码解读

复制代码

private NebulaJdbcProperties getNebulaJdbcProperties(ConfigurableEnvironment environment) { NebulaJdbcProperties nebulaJdbcProperties = new NebulaJdbcProperties(); String hosts = getProperty(environment, "nebula.hosts"); String username = getProperty(environment, "nebula.username"); String password = getProperty(environment, "nebula.password"); String space = getProperty(environment, "nebula.space"); return nebulaJdbcProperties .setHosts(hosts) .setUsername(username) .setPassword(password) .setSpace(space); }

再到BeanFactoryPostProcessor类:NgbatisBeanFactoryPostProcessor

这个类是“Ngbatis 创建动态代理的主程”,实现了BeanFactoryPostProcessor类。

Kimi🌓这样说: 在Spring框架中,BeanFactoryPostProcessor是一个接口,它允许开发者在容器实例化所有的bean之前,对BeanFactory进行自定义的修改或扩展。具体来说,BeanFactoryPostProcessor的目的是:

  1. 修改Bean定义:在Spring容器创建bean实例之前,可以修改bean的属性值,比如修改bean的scope、lazy-init属性等。
  2. 添加新的Bean定义:可以在BeanFactory中注册新的bean定义,这些定义在配置文件中没有被显式地定义。
  3. 删除Bean定义:可以移除BeanFactory中的某些bean定义。
  4. 修改Bean的创建过程:可以影响bean的创建过程,比如自定义初始化和销毁方法。

BeanFactoryPostProcessor接口定义了两个方法:

  • postProcessBeanFactory(ConfigurableListableBeanFactory beanFactory):这个方法在BeanFactory标准初始化之后,所有的bean定义已加载但实例化之前调用。开发者可以在这里执行自定义逻辑来修改BeanFactory。

2.执行postProcessBeanFactory方法,创建连接池和全局上下文MapperContext

 

java

代码解读

复制代码

@Override public void postProcessBeanFactory( ConfigurableListableBeanFactory configurableListableBeanFactory) throws BeansException { //创建连接池 NebulaPool nebulaPool = nebulaPool(); //创捷全局上下文 mapperContext(nebulaPool); }

创建连接池,调用本类的nebulaPool方法如下:
 

java

代码解读

复制代码

public NebulaPool nebulaPool() { NebulaPool pool = new NebulaPool(); try { //⭐调用连接池的init方法 pool.init( nebulaJdbcProperties.getHostAddresses(), nebulaJdbcProperties.getPoolConfig() ); } catch (UnknownHostException e) { throw new RuntimeException("Can not connect to Nebula Graph"); } return pool; }

image.png

如图所示,init 方法传入了两个参数,一个是地址集合参数,一个是线程池配置参数。

NebulaGraph 的连接池其实就是基于 apache commons-pool2 对象池框架对连接进行了池化管理。

具体init方法的介绍可参考这篇文章 [Ngbatis源码学习]Ngbatis源码阅读之连接池的创建 - knqiufan - 博客园 (cnblogs.com)

创建全局上下文,调用本类的mapperContext方法如下:
 

java

代码解读

复制代码

public MapperContext mapperContext(NebulaPool nebulaPool) { //实例化了一个基础类资源加载器 DaoResourceLoader daoBasicResourceLoader = new DaoResourceLoader(parseCfgProps); //实例化MapperContext MapperContext context = MapperContext.newInstance(); context.setResourceRefresh(parseCfgProps.isResourceRefresh()); context.setNgbatisConfig(nebulaJdbcProperties.getNgbatis()); //①调用MapperResourceLoader.load方法读取用户创建的 XXXDao.xml 并解析,存入上下文 Map<String, ClassModel> interfaces = daoBasicResourceLoader.load(); //②调用DaoResourceLoader.loadTpl方法读取 NebulaDaoBasic 的模板文件并解析,存入上下文 Map<String, String> daoBasicTpl = daoBasicResourceLoader.loadTpl(); context.setDaoBasicTpl(daoBasicTpl); context.setNebulaPool(nebulaPool); context.setInterfaces(interfaces); context.setNebulaPoolConfig(nebulaJdbcProperties.getPoolConfig()); //③调用NgbatisBeanFactoryPostProcessor.figureTagTypeMapping方法,将实体类型设置到MapperContext中 figureTagTypeMapping(interfaces.values(), context.getTagTypeMapping()); //④调用本类的setNebulaSessionPool方法,将全局上下文放进SessionPool中 setNebulaSessionPool(context); //⑤调用本类的registerBean方法,为所有的动态代理类注册Bean到SpringBoot registerBean(context); return context; }

①调用MapperResourceLoader.load方法如下:
👉Mapper资源加载器:MapperResourceLoader

继承了PathMatchingResourcePatternResolver类

Kimi🌓这样说: 继承 PathMatchingResourcePatternResolver 后,MapperResourceLoader 可以利用 Spring 的资源抽象和模式匹配功能,同时添加自己的特定功能,以满足应用程序的特定需求。这在开发需要处理大量配置文件、映射文件或其他资源的应用程序时非常有用,比如在开发数据库访问层时,可能需要加载和解析大量的 SQL 映射文件。

提供了许多方法:

  • load方法:加载resource目录下所有用户自定义的xxxDao.xml文件

    image.png

  • parseClassModel方法:解析每个xxxDao.xml文件
 

java

代码解读

复制代码

public Map<String, ClassModel> load() { Map<String, ClassModel> resultClassModel = new HashMap<>(); try { //获取资源路径 mapperLocations = "mapper/**/*.xml" Resource[] resources = getResources(parseConfig.getMapperLocations()); for (Resource resource : resources) { //⭐遍历每个xml文件,调用本类的parseClassModel方法解析,存入结果集中 resultClassModel.putAll(parseClassModel(resource)); } } catch (IOException | NoSuchMethodException e) { throw new ResourceLoadException(e); } return resultClassModel; } public Map<String, ClassModel> parseClassModel(Resource resource) throws IOException, NoSuchMethodException { Map<String, ClassModel> result = new HashMap<>(); // 从资源中获取文件信息,IO 读取 Document doc = Jsoup.parse(resource.getInputStream(), "UTF-8", "http://example.com/"); // 传入 xml 解析器,获取 xml 信息 Elements elementsByTag = doc.getElementsByTag(parseConfig.getMapper()); for (Element element : elementsByTag) { ClassModel cm = new ClassModel(); cm.setResource(resource); // 调用本身的match方法,获取 namespace 设置到 ClassModel 的 namespace 属性上 match(cm, element, "namespace", parseConfig.getNamespace()); // 与上行代码同理,前提是xml中有设置space match(cm, element, "space", parseConfig.getSpace()); // 如果没在xml中设置space,则从注解获取 space if (null == cm.getSpace()) { //调用自身的setClassModelBySpaceAnnotation方法 setClassModelBySpaceAnnotation(cm); } //如果开启了sessionPool会把space的name放进sessionPool中进行初始化 addSpaceToSessionPool(cm.getSpace()); // 获取 子节点 List<Node> nodes = element.childNodes(); // ⭐便历子节点,调用自身的parseMethodModel方法,获取 MethodModel Map<String, MethodModel> methods = parseMethodModel(cm, nodes); cm.setMethods(methods); result.put(cm.getNamespace().getName() + PROXY_SUFFIX, cm); } return result; }

  • match方法:通过反射给model赋值

    image.png

  • castValue方法:将value字符串转成type对象
  • setClassModelBySpaceAnnotation方法:从注解中获取space的name
 

java

代码解读

复制代码

private void match(Object model, Node node, String javaAttr, String attr) { String attrTemp = null; try { String attrText = node.attr(attr); if (isBlank(attrText)) { return; } attrTemp = attrText; //getDeclaredField方法获取类中声明的与指定名称匹配的 Field 对象 Field field = model.getClass().getDeclaredField(javaAttr); Class<?> type = field.getType(); //调用本身的castValue方法将 attrText 字符串转为 type 类型的对象 Object value = castValue(attrText, type); //调用ReflectUtil的setValue方法,利用反射将value设置到model的field属性 ReflectUtil.setValue(model, field, value); } catch (ClassNotFoundException e) { throw new ParseException("类型 " + attrTemp + " 未找到"); } catch (Exception e) { e.printStackTrace(); } } private Object castValue(String attrText, Class<?> type) throws ClassNotFoundException { if (type == Class.class) { return Class.forName(attrText); } else if (boolean.class.equals(type)) { return Boolean.valueOf(attrText); } else { return attrText; } } //如果xml中没配置space,则通过这个方法从注解中获取space private void setClassModelBySpaceAnnotation(ClassModel cm) { try { //getGenericInterfaces方法获取该类或接口实现的接口 Type[] genericInterfaces = cm.getNamespace().getGenericInterfaces(); if (genericInterfaces.length == 0) { return; } //获取第一个泛型接口的具体类型,转为ParameterizedType类型 ParameterizedType nebulaDaoBasicType = (ParameterizedType) genericInterfaces[0]; Type[] genericTypes = nebulaDaoBasicType.getActualTypeArguments(); if (genericTypes.length == 0) { return; } String spaceClassName = genericTypes[0].getTypeName(); //使用 Class.forName 加载类名对应的类,并获取该类上的@Space注解 Space annotation = Class.forName(spaceClassName).getAnnotation(Space.class); if (null != annotation && !annotation.name().equals("")) { cm.setSpace(annotation.name()); } } catch (ClassNotFoundException e) { e.printStackTrace(); } }

  • parseMethodModel方法:解析每个xxxDao.xml的多个方法
  • getMethodNames方法:过滤出所有 Element 类型的节点,并获取它们的ID,将这些ID收集到一个列表中返回
  • parseNgqlModel方法:将xxxDao.xml中的<nGQL>标签的内容打包成NgqlModel
  • parseMethodModel(Node node)方法:解析xxxDao.xml中的<mapper>方法打包成MethodModel

    image.png

  • nodesToString方法:获取<mapper>子标签默认插槽内的文本
  • checkReturnType方法:检查返回参数
 

java

代码解读

复制代码

private Map<String, MethodModel> parseMethodModel(ClassModel cm, List<Node> nodes) throws NoSuchMethodException { Class namespace = cm.getNamespace(); Map<String, MethodModel> methods = new HashMap<>(); List<String> methodNames = getMethodNames(nodes); for (Node methodNode : nodes) { if (methodNode instanceof Element) { //<nGQL id="include-test"> if (((Element) methodNode).tagName().equalsIgnoreCase("nGQL")) { if (Objects.isNull(cm.getNgqls())) { cm.setNgqls(new HashMap<>()); } //调用自身的parseNgqlModel方法解析xxxDao.xml中的<nGQL>方法 NgqlModel ngqlModel = parseNgqlModel((Element) methodNode); cm.getNgqls().put(ngqlModel.getId(),ngqlModel); } else { //⭐调用自身的parseMethodModel方法解析xxxDao.xml中的多个<mapper>方法 MethodModel methodModel = parseMethodModel(methodNode); addSpaceToSessionPool(methodModel.getSpace()); //调用ReflectUtil.getNameUniqueMethod方法根据dao接口和方法名获取唯一方法 Method method = getNameUniqueMethod(namespace, methodModel.getId()); methodModel.setMethod(method); Assert.notNull(method, "接口 " + namespace.getName() + " 中,未声明 xml 中的出现的方法:" + methodModel.getId()); //调用自身的checkReturnType方法检查返回参数 checkReturnType(method, namespace); //⭐调用自身的方法,对需要分页的方法进行支持 pageSupport(method, methodModel, methodNames, methods, namespace); methods.put(methodModel.getId(), methodModel); } } } return methods; } protected NgqlModel parseNgqlModel(Element ngqlEl) { return new NgqlModel(ngqlEl.id(),ngqlEl.text()); } private List<String> getMethodNames(List<Node> nodes) { return nodes.stream().map(node -> { if (node instanceof Element) { return ((Element) node).id(); } return null; }).collect(Collectors.toList()); } protected MethodModel parseMethodModel(Node node) { MethodModel model = new MethodModel(); match(model, node, "id", parseConfig.getId()); match(model, node, "parameterType", parseConfig.getParameterType()); match(model, node, "resultType", parseConfig.getResultType()); match(model, node, "space", parseConfig.getSpace()); match(model, node, "spaceFromParam", parseConfig.getSpaceFromParam()); List<Node> nodes = node.childNodes(); //调用自身的nodesToString方法,将node的内容转为字符串解码后存入MethodModel model.setText(nodesToString(nodes)); return model; } protected String nodesToString(List<? extends Node> nodes) { StringBuilder builder = new StringBuilder(); for (Node node : nodes) { if (node instanceof TextNode) { builder.append(((TextNode) node).getWholeText()); builder.append("\n"); } } String mapperText = builder.toString(); //调用Jsoup库Entities.unescape方法,解码xml特殊符号 String unescape = Entities.unescape(mapperText); return unescape; } //ReturnUtils.getNameUniqueMethod方法:根据方法名,获取唯一的方法 public static Method getNameUniqueMethod(Class<?> daoInterface, String methodName) { Method[] daoMethods = daoInterface.getMethods(); for (Method method : daoMethods) { if (nullSafeEquals(method.getName(), methodName)) { return method; } } return null; } private void checkReturnType(Method method, Class namespace) { Class<?> returnType = method.getReturnType(); if (NEED_SEALING_TYPES.contains(returnType)) { //对暂未支持的 未封箱基础类型 进行检查并给出友好报错 throw new ResourceLoadException( "目前不支持返回基本类型,请使用对应的包装类,接口:" + namespace.getName() + "." + method.getName()); } }

  • pageSupport方法:将需要分页的接口,自动追加两个接口,用于生成动态代理
  • createPageMethod方法:创建 分页中查询范围条目方法 的模型
  • setParamAnnotations方法:给pageMethodModel或者countMethodModel设置上参数列表的参数注解
  • getPageParamName方法:得到分页在参数中的下标或者@param注解的内容作为变量名

多参数或者有@Param注解,会把ngql变成这样: 

image.png

以上两种情况之外:

image.png

  • createCountMethod方法:创建分页中的 条数统计接口 的方法模型。 

    image.png

 

java

代码解读

复制代码

//检查分页 private void pageSupport(Method method, MethodModel methodModel, List<String> methodNames, Map<String, MethodModel> methods, Class<?> namespace) throws NoSuchMethodException { Class<?>[] parameterTypes = method.getParameterTypes(); List<Class<?>> parameterTypeList = Arrays.asList(parameterTypes); //检查参数列表中有没有Page类型 if (parameterTypeList.contains(Page.class)) { //调用自身的 createPageMethod 方法创建一个分页范围的模型 int pageParamIndex = parameterTypeList.indexOf(Page.class); MethodModel pageMethod = createPageMethod( methodModel, methodNames, parameterTypes, pageParamIndex, namespace ); methods.put(pageMethod.getId(), pageMethod); //调用自身的 createCountMethod 方法创建一个分页条数的模型 MethodModel countMethod = createCountMethod( methodModel, methodNames, parameterTypes, namespace ); methods.put(countMethod.getId(), countMethod); } } private MethodModel createPageMethod(MethodModel methodModel, List<String> methodNames, Class<?>[] parameterTypes, int pageParamIndex, Class<?> namespace) throws NoSuchMethodException { //根据methodName,给分页方法起名字 //例:selectCustomPage -> selectCustomPage$Page String methodName = methodModel.getId(); String pageMethodName = String.format("%s$Page", methodName); Assert.isTrue(!methodNames.contains(pageMethodName), "There is a method name conflicts with " + pageMethodName); MethodModel pageMethodModel = new MethodModel(); //调用自身的setParamAnnotations方法,获取参数注解数组存入pageMethodModel Annotation[][] parameterAnnotations = setParamAnnotations(parameterTypes, namespace, methodName, pageMethodModel); pageMethodModel.setParameterTypes(parameterTypes); pageMethodModel.setId(pageMethodName); pageMethodModel.setSpaceFromParam(methodModel.isSpaceFromParam()); pageMethodModel.setSpace(methodModel.getSpace()); String cql = methodModel.getText(); //调用getPageParamName的到分页变量名 String pageParamName = getPageParamName(parameterAnnotations, pageParamIndex); if (pageParamName != null) { String format = "%s\t\tSKIP $%s.startRow LIMIT $%s.pageSize"; cql = String.format(format, cql, pageParamName, pageParamName); } else { String format = "%s\t\tSKIP $startRow LIMIT $pageSize"; cql = String.format(format, cql); } pageMethodModel.setText(cql); pageMethodModel.setResultType(methodModel.getResultType()); pageMethodModel.setReturnType(methodModel.getMethod().getReturnType()); return pageMethodModel; } private static Annotation[][] setParamAnnotations(Class<?>[] parameterTypes, Class<?> namespace, String methodName, MethodModel methodModel) throws NoSuchMethodException { //调用getDeclaredMethod方法根据方法名和参数获取方法 Method declaredMethod = namespace.getDeclaredMethod(methodName, parameterTypes); //调用getParameterAnnotations方法获取该方法参数的注解数组 Annotation[][] parameterAnnotations = declaredMethod.getParameterAnnotations(); //把参数注解放进methodModel中 methodModel.setParamAnnotations(parameterAnnotations); return parameterAnnotations; } private String getPageParamName(Annotation[][] parameterAnnotations, int pageParamIndex) { if (parameterAnnotations.length > pageParamIndex) { Annotation[] parameterAnnotation = parameterAnnotations[pageParamIndex]; for (int i = 0; i < parameterAnnotation.length; i++) { Annotation ifParam = parameterAnnotation[i]; if (ifParam.annotationType() == Param.class) { Param param = (Param) ifParam; String paramName = param.value(); if (isNotBlank(paramName)) { return paramName; } } } } // 多参数,并且没有注解时,使用 pN 的参数格式来表示参数名 if (parameterAnnotations.length > 1) { return "p" + pageParamIndex; } return null; } private MethodModel createCountMethod(MethodModel methodModel, List<String> methodNames, Class<?>[] parameterTypes, Class<?> namespace) throws NoSuchMethodException { //例:selectCustomPage -> selectCustomPage$Count String methodName = methodModel.getId(); String countMethodName = String.format("%s$Count", methodName); Assert.isTrue(!methodNames.contains(countMethodName), "There is a method name conflicts with " + countMethodName); MethodModel countMethodModel = new MethodModel(); setParamAnnotations(parameterTypes, namespace, methodName, countMethodModel); countMethodModel.setParameterTypes(parameterTypes); countMethodModel.setId(countMethodName); // Fix: Set the specified space in the original method to the proxy method for paging, countMethodModel.setSpaceFromParam(methodModel.isSpaceFromParam()); countMethodModel.setSpace(methodModel.getSpace()); String cql = methodModel.getText(); String with = cql.replaceAll("(RETURN)|(return)", "WITH"); cql = String.format("%s\t\tRETURN count(*);", with); countMethodModel.setText(cql); countMethodModel.setReturnType(Long.class); return countMethodModel; }

②调用DaoResourceLoader.loadTpl方法如下:
👉基础类资源加载器:DaoResourceLoader

继承了MapperResourceLoader类,可以使用MapperResourceLoader类提供的load()和nodesToString()方法。

提供了两个方法:

  • loadTpl():根据NebulaDaoBasic.xml资源路径获取Resource,且调用自身的parse()方法
  • parse():使用Jsoup解析xml文件,返回结果Map(基类接口方法名,nGQL模板)
 

java

代码解读

复制代码

public class DaoResourceLoader extends MapperResourceLoader { public DaoResourceLoader(ParseCfgProps parseConfig) { super(parseConfig); } public Map<String, String> loadTpl() { try { Resource resource = getResource(parseConfig.getMapperTplLocation()); return parse(resource); } catch (IOException e) { throw new ResourceLoadException(e); } } private Map<String, String> parse(Resource resource) throws IOException { Document doc = Jsoup.parse(resource.getInputStream(), "UTF-8", "http://example.com/"); Map<String, String> result = new HashMap<>(); Method[] methods = NebulaDaoBasic.class.getMethods(); for (Method method : methods) { String name = method.getName(); Element elementById = doc.getElementById(name); if (elementById != null) { List<TextNode> textNodes = elementById.textNodes(); //调用MapperResourceLoader.nodesToString方法将textNode转为nGQL模板字符串 String tpl = nodesToString(textNodes); result.put(name, tpl); } } return result; } }

③调用本类的figureTagTypeMapping方法如下:
 

java

代码解读

复制代码

private void figureTagTypeMapping( Collection<ClassModel> classModels, Map<String, Class<?>> tagTypeMapping) { for (ClassModel classModel : classModels) { //调用NebulaDaoBasicExt.entityTypeAndIdType方法获取泛型类型 Class<?>[] entityTypeAndIdType = entityTypeAndIdType(classModel.getNamespace()); if (entityTypeAndIdType != null) { Class<?> entityType = entityTypeAndIdType[0]; String vertexName = vertexName(entityType); tagTypeMapping.putIfAbsent(vertexName, entityType); } } }

👉NebulaDaoBasic的扩展类:NebulaDaoBasicExt

提供给NebulaDaoBasic调用的拓展方法。 与具体执行gql的方法分离,避免干扰通用dao的继承。

  • entityTypeAndIdType方法:根据dao接口类型,通过它的泛型,取得其管理的实体类型与主键类型

    image.png

 

java

代码解读

复制代码

public static Class<?>[] entityTypeAndIdType(Class<?> currentType) { Class<?>[] result = null; //调用getGenericInterfaces方法返回由这个类直接实现的接口 Type[] genericInterfaces = currentType.getGenericInterfaces(); for (Type genericInterface : genericInterfaces) { //调用ReflectUtil.isCurrentTypeOrParentType方法判断参数1是否是参数2子类或者实现类 if (isCurrentTypeOrParentType(genericInterface.getClass(), ParameterizedType.class)) { //获取泛型参数数组 Type[] actualTypeArguments = ((ParameterizedType) genericInterface).getActualTypeArguments(); result = new Class<?>[]{ (Class<?>) actualTypeArguments[0], // T {@link NebulaDaoBasic } (Class<?>) actualTypeArguments[1] // ID {@link NebulaDaoBasic } }; } else if (genericInterface instanceof Class) { result = entityTypeAndIdType((Class<?>) genericInterface); } } return result; }

④调用本类的setNebulaSessionPool方法如下:

当开启了sessionPool才会执行

 

java

代码解读

复制代码

public void setNebulaSessionPool(MapperContext context) { NgbatisConfig ngbatisConfig = nebulaJdbcProperties.getNgbatis(); if (ngbatisConfig.getUseSessionPool() == null || !ngbatisConfig.getUseSessionPool()) { return; } context.getSpaceNameSet().add(nebulaJdbcProperties.getSpace()); Map<String, SessionPool> nebulaSessionPoolMap = context.getNebulaSessionPoolMap(); for (String spaceName : context.getSpaceNameSet()) { SessionPool sessionPool = initSessionPool(spaceName); if (sessionPool == null) { log.error("{} session pool init failed.", spaceName); continue; } nebulaSessionPoolMap.put(spaceName, sessionPool); } }

⑤调用本类的registerBean方法如下:

这里主要做的事是:注册 XXXDao 对象形成由 spring 管理的 bean

 

java

代码解读

复制代码

private void registerBean(MapperContext context) { Map<String, ClassModel> interfaces = context.getInterfaces(); for (ClassModel cm : interfaces.values()) { //调用MapperProxyClassGenerator.setClassCode方法生成代理类 beanFactory.setClassCode(cm); } //将代理类加载到 jvm 中,执行方:RAMClassLoader RamClassLoader ramClassLoader = new RamClassLoader(context.getInterfaces()); for (ClassModel cm : interfaces.values()) { try { String className = cm.getNamespace().getName() + PROXY_SUFFIX; //将加载的类注册成bean,交由spring boot管理 registerBean(cm, ramClassLoader.loadClass(className)); log.info("Bean had been registed (代理类注册成bean): {}", className); } catch (ClassNotFoundException e) { e.printStackTrace(); } } }

👉MapperProxyClassGenerator

基于 ASM 对接口进行动态代理,并生成 Bean 代理的实现类

  • setClassCode方法:根据 ClassModel 对象中扫描得到的信息,生成代理类,并将字节码设置到 ClassModel 对象中
 

java

代码解读

复制代码

public byte[] setClassCode(ClassModel cm) { String fullNameType = getFullNameType(cm); ClassWriter cw = new ClassWriter(0); // public class XXX extends Object implement XXX cw.visit( V1_8, ACC_PUBLIC, fullNameType, null, "java/lang/Object", new String[]{getFullNameType(cm.getNamespace().getName())} ); // 无参构造 constructor(cw); // 生成代理方法 methods(cw, cm); // 完成 cw.visitEnd(); byte[] code = cw.toByteArray(); cm.setClassByte(code); //将生成的字节码,写入本地文件,形成 .class 文件供调试时使用 writeFile(cm); return code; }

visit方法Kimi解释如下:

  • visit方法用于开始定义一个类。参数如下:

    • V1_8:指定生成的字节码版本为Java 8。ASM使用版本号来确保生成的字节码与特定版本的Java虚拟机兼容。
    • ACC_PUBLIC:访问标志,表示这个类是公开的(public)。
    • fullNameType:类名的完全限定名,包括包名。例如,如果类在包com.example中,名为MyClass,则fullNameType将是"com/example/MyClass"
    • null:签名字段,这里没有使用泛型,所以是null
    • "java/lang/Object":超类名称,这里指定所有Java类的默认超类java.lang.Object
    • new String[]{getFullNameType(cm.getNamespace().getName())}:接口数组,这里调用getFullNameType方法获取接口的完全限定名,并将结果作为字符串数组传递。

image.png

  • constructor方法:给类生成无参构造器
 

java

代码解读

复制代码

private void constructor(ClassWriter cw) { MethodVisitor constructor = cw.visitMethod(ACC_PUBLIC, "<init>", "()V", null, null); // 将this参数入栈 constructor.visitCode(); constructor.visitVarInsn(ALOAD, 0); constructor.visitMethodInsn(INVOKESPECIAL, "java/lang/Object", "<init>", "()V", false); constructor.visitInsn(RETURN); // 指定局部变量栈的空间大小 constructor.visitMaxs(1, 1); // 构造方法的结束 constructor.visitEnd(); }

  • method方法:生成代理方法
 

java

代码解读

复制代码

private void methods(ClassWriter cw, ClassModel cm) { // 读取配置,并根据配置向 class 文件写人代理方法 Map<String, MethodModel> methods = cm.getMethods(); for (Map.Entry<String, MethodModel> entry : methods.entrySet()) { method(cw, cm, entry); } } private void method(ClassWriter cw, ClassModel cm, Map.Entry<String, MethodModel> mmEntry) { String methodName = mmEntry.getKey(); MethodModel mm = mmEntry.getValue(); /* return Mapper.invoke( "接口名 namespace", "方法名 method", new Object[]{ arg1, arg2, ... } ); ----- start */ Method method = mm.getMethod(); String methodSignature = ReflectUtil.getMethodSignature(mm); MethodVisitor mapper = cw.visitMethod( ACC_PUBLIC, methodName, methodSignature, null, null ); mapper.visitCode(); String className = cm.getNamespace().getName(); mapper.visitLdcInsn(className); mapper.visitLdcInsn(mm.getId()); int parameterCount = addParams(mapper, mm.getParameterCount()); mapper.visitMethodInsn( INVOKESTATIC, getFullNameType(MapperProxy.class.getName()), "invoke", "(Ljava/lang/String;Ljava/lang/String;[Ljava/lang/Object;)Ljava/lang/Object;", false ); /* -------------------------------- end --------------------------------*/ // *2,每多一个方法参数,需要多定义 2 个局部变量,下标变量 // +3: 3 个固定参数位,namespace、methodName、args mapper.visitMaxs(Integer.MAX_VALUE, Integer.MAX_VALUE); // 检查类型转换 Class<?> returnType = mm.getReturnType(); mapper.visitTypeInsn(CHECKCAST, getFullNameType(returnType.getTypeName())); // 基本类型封箱 // sealingReturnType(mapper, returnType ); // FIXME 处理基本类型的封箱 int returnTypeInsn = getReturnTypeInsn(returnType); mapper.visitInsn(returnTypeInsn); mapper.visitEnd(); }

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值