百度360必应搜狗淘宝本站头条
当前位置:网站首页 > 编程字典 > 正文

最佳实践之接口规范性检查插件

toyiye 2024-06-21 12:17 8 浏览 0 评论

前言

本文是针对微服务架构下各服务之间交互接口契约定义与声明规范性在实际生产中的一个最佳实践。在经历无数次组件间和项目间的扯皮、修改再修改的过程中,总结的一套契约规范,并辅之以有效的自动化检测手段即本文提及的检查插件,通过上述规范和工具统一各项目服务间的接口的理解,降低出错返工概率,最终提高开发效率,节省开发成本。

一、Maven插件是什么

Maven本质上是一个插件框架,它的核心并不执行任何具体的构建任务,所有这些任务都交给插件来完成,例如编译源代码是由maven- compiler-plugin完成的。进一步说,每个任务对应了一个插件目标(goal),每个插件会有一个或者多个目标,例如maven- compiler-plugin的compile目标用来编译位于src/main/java/目录下的主源码,testCompile目标用来编译位于src/test/java/目录下的测试源码。

用户可以通过两种方式调用Maven插件目标。第一种方式是将插件目标与生命周期阶段(lifecycle phase)绑定,这样用户在命令行只是输入生命周期阶段而已,例如Maven默认将maven-compiler-plugin的compile目标与 compile生命周期阶段绑定,因此命令mvn compile实际上是先定位到compile这一生命周期阶段,然后再根据绑定关系调用maven-compiler-plugin的compile目标。第二种方式是直接在命令行指定要执行的插件目标,例如mvn archetype:generate 就表示调用maven-archetype-plugin的generate目标,这种带冒号的调用方式与生命周期无关。
目前已经积累了大量有用的插件,参考:

  1. http://maven.apache.org/plugins/index.html
  2. http://mojo.codehaus.org/plugins.html

二、接口定义(Yaml)

为避免从代码生成yaml再到yaml编出的代码,两者间产生不一致和歧义,如下某Rest接口返回的对象模型:

@Getter
@Setter
@NoArgsConstructor
@ToString(callSuper = true)
@EqualsAndHashCode(callSuper = true)
@ApiModel(description = "com.xxxx.node.IpL2Node")
public class IpL2Node extends IpNode {

    @JsonProperty("admin-ip")
    @ApiModelProperty(name = "admin-ip", value = "管理地址")
    private String adminIp;

团队约定在编写接口代码里,要求遵守下列约定:

模型实体类上必须使用注解@ApiModel,并且description设置为该实体类的全路径名

模型实体类上尽量使用注解@JsonIgnoreProperties(ignoreUnknown = true)

模型实体类上必须使用注解@JsonNaming(PropertyNamingStrategy.KebabCaseStrategy.class),同时不允许使用@JsonProperty特殊指定实体类中某字段的序列化字符串

模型实体类中每个字段必须使用注解@ApiModelProperty,并且保证name定义与序列化出的JSON字符串完全一致

Rest接口不允许返回Set集合

接口代码中不允许使用注解@JsonInclud(JsonInclud.Include.NON_NULL)

Rest接口中,不要使用JAVA层面的方法重载,会导致生成yaml编译代码后,相同方法后会自动加_0参数用于区分,影响接口准确性

通过上述约定,基本上保证了代码输出的yaml与再次编出的代码含义一致,但不能寄希望于开发人员主观遵守,所以需要一种自动化的手段,能够在出错或不满足上述约定时,在开发阶段即通知开发人员,因此我们通过开发maven插件方式完成上述功能。

三、插件实现

具体插件的实现原理也十分简单,即配置该插件后,在代码的编译阶段,通过扫描classPath下所有类文件,通过注解或包括Json相关过滤出潜在模型类,然后按上述规则进行校验,并给出报错明细信息。具体代码开发逻辑记录如下:

该插件的工程目录:

POM依赖:

<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd">
    <modelVersion>4.0.0</modelVersion>
    <groupId>com.zte.sdn.oscp.topology</groupId>
    <artifactId>oscp-restapi-check</artifactId>
    <packaging>maven-plugin</packaging>
    <version>1.0-SNAPSHOT</version>
    <name>oscp-restapi-check Maven Mojo</name>
    <url>http://maven.apache.org</url>
    <dependencies>
        <dependency>
            <groupId>org.apache.maven</groupId>
            <artifactId>maven-plugin-api</artifactId>
            <version>2.0</version>
        </dependency>
        <dependency>
            <groupId>junit</groupId>
            <artifactId>junit</artifactId>
            <version>3.8.1</version>
        </dependency>
        <dependency>
            <groupId>org.apache.maven</groupId>
            <artifactId>maven-core</artifactId>
            <version>3.3.9</version>
        </dependency>
        <dependency>
            <groupId>org.apache.maven.plugin-tools</groupId>
            <artifactId>maven-plugin-annotations</artifactId>
            <version>3.4</version>
        </dependency>
        <dependency>
            <groupId>io.swagger</groupId>
            <artifactId>swagger-annotations</artifactId>
            <version>1.5.3</version>
        </dependency>
        <dependency>
            <groupId>com.fasterxml.jackson.core</groupId>
            <artifactId>jackson-core</artifactId>
            <version>2.9.6</version>
        </dependency>
        <dependency>
            <groupId>com.fasterxml.jackson.core</groupId>
            <artifactId>jackson-databind</artifactId>
            <version>2.9.6</version>
        </dependency>
    </dependencies>
    <build>
        <plugins>
            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-compiler-plugin</artifactId>
                <version>3.8.0</version>
                <configuration>
                    <source>1.8</source>
                    <target>1.8</target>
                    <encoding>UTF-8</encoding>
                </configuration>
            </plugin>
            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-plugin-plugin</artifactId>
                <version>3.5.2</version>
            </plugin>
        </plugins>
    </build>
</project>

入口类RestFormatCheckMojo.java,设置该插件的运行在PROCESS_CLASSES生命周期,保证编译完源码后能够进行模型的较验。

import static org.apache.maven.plugins.annotations.LifecyclePhase.PROCESS_CLASSES;

import java.io.File;
import java.lang.reflect.Field;
import java.lang.reflect.Modifier;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.stream.Collectors;

import io.swagger.annotations.ApiModel;
import io.swagger.annotations.ApiModelProperty;
import junit.framework.Assert;
import org.apache.maven.plugin.AbstractMojo;
import org.apache.maven.plugin.MojoExecutionException;
import org.apache.maven.plugins.annotations.Mojo;
import org.apache.maven.plugins.annotations.Parameter;
import org.apache.maven.plugins.annotations.ResolutionScope;
import org.apache.maven.project.MavenProject;

import com.fasterxml.jackson.annotation.JsonIgnore;
import com.fasterxml.jackson.annotation.JsonProperty;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.annotation.JsonNaming;
import com.google.common.collect.Lists;
import com.zte.sdn.oscp.topology.annotation.IgnoreFormatCheck;
import com.zte.sdn.oscp.topology.util.JavaClassScanner;

@Mojo(name = "yang2bean", defaultPhase = PROCESS_CLASSES, requiresDependencyResolution = ResolutionScope.COMPILE)
public class RestFormatCheckMojo extends AbstractMojo {

    private List<String> notCheckPkg = Lists.newArrayList();

    @Parameter(property = "skipCheckPkg")
    private String skipCheckPkg;

    @Parameter(property = "outDirectory", defaultValue = "${project.build.outputDirectory}")
    private File outputDirectory;

    @Parameter(defaultValue = "${project.basedir}")
    private File baseDir;


    @Parameter(defaultValue = "${project}", readonly = true)
    private MavenProject project;

    @Parameter(defaultValue = "${project.build.sourceDirectory}")
    private File srcDir;

    @Parameter(defaultValue = "${project.build.outputDirectory}")
    private File outDir;
    @Parameter(defaultValue = "${project.compileClasspathElements}", readonly = true, required = true)
    private List<String> compilePath;

    @Override
    public void execute() throws MojoExecutionException {
        if (skipCheckPkg != null && !"".equals(skipCheckPkg.trim())) {
            String[] split = skipCheckPkg.split(",");
            notCheckPkg = Arrays.asList(split);
        }
        JavaClassScanner scanner = new JavaClassScanner(project);
        scanner.scan(outDir);
        List<Class> clsList = scanner.getClsList();
        List<Class> models = filterByCondition(clsList);
        try {
            check(models);
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

    private void check(List<Class> models) throws Exception {
        ObjectMapper objectMapper = new ObjectMapper();
        for (Class<?> aClass : models) {
            Assert.assertTrue("[" + aClass.getName() + "] must annotated with AipModel", aClass.isAnnotationPresent(ApiModel.class));
            Assert.assertEquals(aClass.getName().replace("#34;, "."), ((ApiModel) aClass.getAnnotation(ApiModel.class)).description());
            String serialize = objectMapper.writeValueAsString(aClass.newInstance());
            if ("[]".equals(serialize)) {
                continue;
            }
            Map jsonPropertyMap = objectMapper.readValue(serialize, new MapTypeReference());
            jsonPropertyMap.remove("class");

            List<String> apiModelProperties = new ArrayList<>();
            List<Field> allFields = getAllFields(aClass);
            for (Field field : allFields) {
                if (field.isAnnotationPresent(JsonIgnore.class)) {
                    continue;
                }
                if (Modifier.isStatic(field.getModifiers())) {
                    continue;
                }
                //严格要求,必须定义ApiModelProperty
                Assert.assertTrue("[" + field.getName() + "] in [" + aClass.getName() + "] must annotated with ApiModelProperty", field.isAnnotationPresent(ApiModelProperty.class));
                String name = field.getAnnotation(ApiModelProperty.class).name();
                if (name == null || "".equals(name)) {
                    apiModelProperties.add(field.getName());
                } else {
                    apiModelProperties.add(name);
                }
            }
            Assert.assertEquals(jsonPropertyMap.size(), apiModelProperties.size());
            Set set = jsonPropertyMap.keySet();
            Assert.assertTrue("[" + aClass.getName() + "] JSON and ApiModelPorperty not same", set.containsAll(apiModelProperties));
            Assert.assertTrue("[" + aClass.getName() + "] JSON and ApiModelPorperty not same", apiModelProperties.containsAll(set));
        }
    }

    private List<Class> filterByCondition(List<Class> clsList) {
        List<Class> result = clsList.stream().filter(p -> {
            if (p.isInterface()) {
                return false;
            }
            for (String pkg : notCheckPkg) {
                if (p.getName().startsWith(pkg)) {
                    return false;
                }
            }
            if (p.isAnnotationPresent(IgnoreFormatCheck.class)) {
                return false;
            }
            if (p.isAnnotationPresent(ApiModel.class)) {
                return true;
            }
            if (p.isAnnotationPresent(JsonNaming.class)) {
                return true;
            }
            for (Field declaredField : p.getDeclaredFields()) {
                if (declaredField.isAnnotationPresent(JsonProperty.class)) {
                    return true;
                }
            }
            return false;
        }).collect(Collectors.toList());
        return result;
    }


    private List<Field> getAllFields(Class cls) {
        List<Field> fields = new ArrayList<>();
        Class current = cls;
        while (current != Object.class) {
            fields.addAll(Arrays.asList(current.getDeclaredFields()));
            current = current.getSuperclass();
        }
        return fields;
    }


}

在插件开发中,无法直接使用Class.forName进行类加载,需要通过动态读取目标项目所依赖的classpath并根据这些classpath生成相应的url数组,以这个url数组作为参数得到的类加载器可以实现在maven插件中动态加载目标项目类及第三方引用包的目的,具体可以参考:https://www.cnblogs.com/coder-chi/p/11305498.html

Please note that the plugin classloader does neither contain the dependencies of the current project nor its build output. Instead, plugins can query the project’s compile, runtime and test class path from the MavenProject in combination with the mojo annotation requiresDependencyResolution from the Mojo API Specification. For instance, flagging a mojo with @requiresDependencyResolution runtime enables it to query the runtime class path of the current project from which it could create further classloaders.

四、运行效果

如图所示:相关不满足规范的定义已经正确拦截

相关推荐

为何越来越多的编程语言使用JSON(为什么编程)

JSON是JavascriptObjectNotation的缩写,意思是Javascript对象表示法,是一种易于人类阅读和对编程友好的文本数据传递方法,是JavaScript语言规范定义的一个子...

何时在数据库中使用 JSON(数据库用json格式存储)

在本文中,您将了解何时应考虑将JSON数据类型添加到表中以及何时应避免使用它们。每天?分享?最新?软件?开发?,Devops,敏捷?,测试?以及?项目?管理?最新?,最热门?的?文章?,每天?花?...

MySQL 从零开始:05 数据类型(mysql数据类型有哪些,并举例)

前面的讲解中已经接触到了表的创建,表的创建是对字段的声明,比如:上述语句声明了字段的名称、类型、所占空间、默认值和是否可以为空等信息。其中的int、varchar、char和decimal都...

JSON对象花样进阶(json格式对象)

一、引言在现代Web开发中,JSON(JavaScriptObjectNotation)已经成为数据交换的标准格式。无论是从前端向后端发送数据,还是从后端接收数据,JSON都是不可或缺的一部分。...

深入理解 JSON 和 Form-data(json和formdata提交区别)

在讨论现代网络开发与API设计的语境下,理解客户端和服务器间如何有效且可靠地交换数据变得尤为关键。这里,特别值得关注的是两种主流数据格式:...

JSON 语法(json 语法 priority)

JSON语法是JavaScript语法的子集。JSON语法规则JSON语法是JavaScript对象表示法语法的子集。数据在名称/值对中数据由逗号分隔花括号保存对象方括号保存数组JS...

JSON语法详解(json的语法规则)

JSON语法规则JSON语法是JavaScript对象表示法语法的子集。数据在名称/值对中数据由逗号分隔大括号保存对象中括号保存数组注意:json的key是字符串,且必须是双引号,不能是单引号...

MySQL JSON数据类型操作(mysql的json)

概述mysql自5.7.8版本开始,就支持了json结构的数据存储和查询,这表明了mysql也在不断的学习和增加nosql数据库的有点。但mysql毕竟是关系型数据库,在处理json这种非结构化的数据...

JSON的数据模式(json数据格式示例)

像XML模式一样,JSON数据格式也有Schema,这是一个基于JSON格式的规范。JSON模式也以JSON格式编写。它用于验证JSON数据。JSON模式示例以下代码显示了基本的JSON模式。{"...

前端学习——JSON格式详解(后端json格式)

JSON(JavaScriptObjectNotation)是一种轻量级的数据交换格式。易于人阅读和编写。同时也易于机器解析和生成。它基于JavaScriptProgrammingLa...

什么是 JSON:详解 JSON 及其优势(什么叫json)

现在程序员还有谁不知道JSON吗?无论对于前端还是后端,JSON都是一种常见的数据格式。那么JSON到底是什么呢?JSON的定义...

PostgreSQL JSON 类型:处理结构化数据

PostgreSQL提供JSON类型,以存储结构化数据。JSON是一种开放的数据格式,可用于存储各种类型的值。什么是JSON类型?JSON类型表示JSON(JavaScriptO...

JavaScript:JSON、三种包装类(javascript 包)

JOSN:我们希望可以将一个对象在不同的语言中进行传递,以达到通信的目的,最佳方式就是将一个对象转换为字符串的形式JSON(JavaScriptObjectNotation)-JS的对象表示法...

Python数据分析 只要1分钟 教你玩转JSON 全程干货

Json简介:Json,全名JavaScriptObjectNotation,JSON(JavaScriptObjectNotation(记号、标记))是一种轻量级的数据交换格式。它基于J...

比较一下JSON与XML两种数据格式?(json和xml哪个好)

JSON(JavaScriptObjectNotation)和XML(eXtensibleMarkupLanguage)是在日常开发中比较常用的两种数据格式,它们主要的作用就是用来进行数据的传...

取消回复欢迎 发表评论:

请填写验证码