【实用工具】JSR-269 插入式注解处理器AbstractProcessor

news2024/11/23 11:21:40

JSR-269原理浅析

初次使用lombok时,都需要在idea安装lombok插件,这让我们怀疑lombok的实现是通过提供自己的编译器实现的,然而实际情况并非如此,在脱离idea使用javac编译时,只要类路径有lombok的jar包,项目也可以正常编译通过。其原理在于JSR-269规范。

Java6开始纳入了JSR-269规范:Pluggable Annotation Processing API(插件式注解处理器)。JSR-269提供一套标准API来处理Annotations,具体来说,我们只需要继承AbstractProcessor类,重写process方法实现自己的注解处理逻辑,并且在META-INF/services目录下创建javax.annotation.processing.Processor文件注册自己实现的Annotation Processor,在javac编译过程中编译器便会调用我们实现的Annotation Processor,从而使得我们有机会对java编译过程中生产的抽象语法树进行修改。

javac的编译过程,大致可以分为3个过程:

  1. 解析与填充符号表过程
  2. 插入式注解处理器的注解处理过程
  3. 分析与字节码生成过程

解析与填充符号表过程会将源码转换为一棵抽象语法树(Abstract Syntax Tree,AST),AST是一种用来描述程序代码语法结构的树形表示方式,语法树的每一个节点都代表着程序代码中的一个语法结构(Construct),例如包、类型、修饰符、运算符、接口、返回值甚至代码注释等都可以是一个语法结构。在插入式注解处理器的注解处理过程中,lombok对第一步骤得到的AST进行处理,找到类似@Data注解所在类对应的语法树(AST),然后修改该语法树(AST),增加getter和setter等方法的相应树节点,javac使用修改后的抽象语法树(AST)生成字节码文件,因此最终生成的class文件中包含了这些自动生成的getter,setter方法。

创建注解处理器项目

  1. 添加依赖
 		<dependency>
            <groupId>com.google.auto.service</groupId>
            <artifactId>auto-service</artifactId>
            <version>1.0-rc2</version>
        </dependency>
  1. 添加注解
@Retention(RetentionPolicy.SOURCE)
public @interface HelloWorld {
}
  1. 自定义注解处理器
@SupportedAnnotationTypes("com.charles.annotations.HelloWorld")
@SupportedSourceVersion(SourceVersion.RELEASE_8)
@AutoService(Processor.class)
public class MyProcessor extends AbstractProcessor {
    Messager messager;

    @Override
    public synchronized void init(ProcessingEnvironment processingEnv) {
        super.init(processingEnv);
        messager = processingEnv.getMessager();
    }

    @Override
    public boolean process(Set<? extends TypeElement> annotations, RoundEnvironment roundEnv) {
        processingEnv.getMessager().printMessage(Diagnostic.Kind.NOTE, "Hello World 123!");
        return false;
    }
}
  1. maven clean install

创建项目引入依赖

        <dependency>
            <groupId>com.charles</groupId>
            <artifactId>demo-annotation-processor</artifactId>
            <version>0.0.1-SNAPSHOT</version>
        </dependency>

调试自定义处理器

方式一:

  1. 命令行执行mvnDebug clean package
    在这里插入图片描述
  2. 设置idea远程调试模式,端口设置为8000

在这里插入图片描述

  1. debug启动远程调试(MyProcessor打断点)

在这里插入图片描述

方式二:

public class DemoAnnotationProcessorApplication {
    public static void main(String[] args) {
        javax.tools.JavaCompiler javaCompiler = ToolProvider.getSystemJavaCompiler();

        String[] strings = {"-d", "./", "src/main/java/com/charles/demo/HelloWorldDemo.java"};
        // 0 表示成功, 其他表示出现了错误
        int i = javaCompiler.run(null, null, null, strings);

        if (i == 0) {
            System.out.println("成功");
        } else {
            System.out.println("错误");
        }
    }
}

AutoService的使用

AutoService是一个注解处理器。AutoService框架的作用是自动生成SPI清单文件(META-INF/services下的文件)。不用它也行,如果不使用它就需要手动去创建这个文件、手动往这个文件里添加服务(接口实现)。

AutoService源码分析

主要逻辑在AutoServiceProcessor#process方法中,通过实现AbstractProcessor的process方法来实现功能。获取所有的AutoService的元素,将接口和实现存放在providers

  @Override
  public boolean process(Set<? extends TypeElement> annotations, RoundEnvironment roundEnv) {
    try {
      return processImpl(annotations, roundEnv);
    } catch (Exception e) {
      // We don't allow exceptions of any kind to propagate to the compiler
      StringWriter writer = new StringWriter();
      e.printStackTrace(new PrintWriter(writer));
      fatalError(writer.toString());
      return true;
    }
  }

  private boolean processImpl(Set<? extends TypeElement> annotations, RoundEnvironment roundEnv) {
    if (roundEnv.processingOver()) {
      generateConfigFiles();
    } else {
      processAnnotations(annotations, roundEnv);
    }

    return true;
  }

  private void processAnnotations(Set<? extends TypeElement> annotations,
      RoundEnvironment roundEnv) {

    Set<? extends Element> elements = roundEnv.getElementsAnnotatedWith(AutoService.class);

    log(annotations.toString());
    log(elements.toString());

    for (Element e : elements) {
      // TODO(gak): check for error trees?
      TypeElement providerImplementer = (TypeElement) e;
      AnnotationMirror providerAnnotation = getAnnotationMirror(e, AutoService.class).get();
      DeclaredType providerInterface = getProviderInterface(providerAnnotation);
      TypeElement providerType = (TypeElement) providerInterface.asElement();

      log("provider interface: " + providerType.getQualifiedName());
      log("provider implementer: " + providerImplementer.getQualifiedName());

      if (!checkImplementer(providerImplementer, providerType)) {
        String message = "ServiceProviders must implement their service provider interface. "
            + providerImplementer.getQualifiedName() + " does not implement "
            + providerType.getQualifiedName();
        error(message, e, providerAnnotation);
      }

      String providerTypeName = getBinaryName(providerType);
      String providerImplementerName = getBinaryName(providerImplementer);
      log("provider interface binary name: " + providerTypeName);
      log("provider implementer binary name: " + providerImplementerName);

      providers.put(providerTypeName, providerImplementerName);
    }
  }

AutoServiceProcessor#generateConfigFiles,生成SPI注册文件。如果SPI路径上文件已经存在,先要把已存在的SPI清单读进内存,再把新的provider加进去,然后全部写出,覆盖原来的文件。

  private void generateConfigFiles() {
    Filer filer = processingEnv.getFiler();

    for (String providerInterface : providers.keySet()) {
      String resourceFile = "META-INF/services/" + providerInterface;
      log("Working on resource file: " + resourceFile);
      try {
        SortedSet<String> allServices = Sets.newTreeSet();
        try {
          // would like to be able to print the full path
          // before we attempt to get the resource in case the behavior
          // of filer.getResource does change to match the spec, but there's
          // no good way to resolve CLASS_OUTPUT without first getting a resource.
          FileObject existingFile = filer.getResource(StandardLocation.CLASS_OUTPUT, "",
              resourceFile);
          log("Looking for existing resource file at " + existingFile.toUri());
          Set<String> oldServices = ServicesFiles.readServiceFile(existingFile.openInputStream());
          log("Existing service entries: " + oldServices);
          allServices.addAll(oldServices);
        } catch (IOException e) {
          // According to the javadoc, Filer.getResource throws an exception
          // if the file doesn't already exist.  In practice this doesn't
          // appear to be the case.  Filer.getResource will happily return a
          // FileObject that refers to a non-existent file but will throw
          // IOException if you try to open an input stream for it.
          log("Resource file did not already exist.");
        }

        Set<String> newServices = new HashSet<String>(providers.get(providerInterface));
        if (allServices.containsAll(newServices)) {
          log("No new service entries being added.");
          return;
        }

        allServices.addAll(newServices);
        log("New service file contents: " + allServices);
        FileObject fileObject = filer.createResource(StandardLocation.CLASS_OUTPUT, "",
            resourceFile);
        OutputStream out = fileObject.openOutputStream();
        ServicesFiles.writeServiceFile(allServices, out);
        out.close();
        log("Wrote to: " + fileObject.toUri());
      } catch (IOException e) {
        fatalError("Unable to create " + resourceFile + ", " + e);
        return;
      }
    }
  }

模拟lombok生成get/set方法

GetterProcessorGetter注解的处理器

//对Getter感兴趣
@SupportedAnnotationTypes("com.charles.processor.Getter")
//支持的版本,使用1.8就写这个
//@SupportedSourceVersion(SourceVersion.RELEASE_8)
public class GetterProcessor extends AbstractProcessor {

    // 编译时期输入日志的
    private Messager messager;

    // 将Element转换为JCTree的工具,提供了待处理的抽象语法树
    private JavacTrees trees;

    // 封装了创建AST节点的一些方法
    private TreeMaker treeMaker;

    // 提供了创建标识符的方法
    private Names names;

    @Override
    public SourceVersion getSupportedSourceVersion() {
        return SourceVersion.latestSupported();
    }

    @Override
    public synchronized void init(ProcessingEnvironment processingEnv) {
        super.init(processingEnv);
        this.messager = processingEnv.getMessager();
        this.trees = JavacTrees.instance(processingEnv);
        Context context = ((JavacProcessingEnvironment) processingEnv).getContext();
        this.treeMaker = TreeMaker.instance(context);
        this.names = Names.instance(context);
    }

    @Override
    public boolean process(Set<? extends TypeElement> annotations, RoundEnvironment roundEnv) {

        // 获取被@Getter注解标记的所有元素(这个元素可能是类、变量、方法等等)
        Set<? extends Element> set = roundEnv.getElementsAnnotatedWith(Getter.class);

        set.forEach(element -> {
            // 将Element转换为JCTree
            JCTree jcTree = trees.getTree(element);
            jcTree.accept(new TreeTranslator() {
                /***
                 * JCTree.Visitor有很多方法,我们可以通过重写对应的方法,(从该方法的形参中)来获取到我们想要的信息:
                 * 如: 重写visitClassDef方法, 获取到类的信息;
                 *     重写visitMethodDef方法, 获取到方法的信息;
                 *     重写visitVarDef方法, 获取到变量的信息;
                 * @param jcClassDecl
                 */
                @Override
                public void visitClassDef(JCTree.JCClassDecl jcClassDecl) {

                    //创建一个变量语法树节点的List
                    List<JCTree.JCVariableDecl> jcVariableDeclList = List.nil();

                    // 遍历defs,即是类定义的详细语句,包括字段、方法的定义等等
                    for (JCTree tree : jcClassDecl.defs) {
                        if (tree.getKind().equals(Tree.Kind.VARIABLE)) {
                            JCTree.JCVariableDecl jcVariableDecl = (JCTree.JCVariableDecl) tree;
                            jcVariableDeclList = jcVariableDeclList.append(jcVariableDecl);
                        }
                    }
                    // 对于变量进行生成方法的操作
                    jcVariableDeclList.forEach(jcVariableDecl -> {
                        messager.printMessage(Diagnostic.Kind.NOTE, "get " + jcVariableDecl.getName() + " has been processed");
                        treeMaker.pos = jcVariableDecl.pos;
                        //类里的前面追加生成的Getter方法
                        jcClassDecl.defs = jcClassDecl.defs.prepend(makeGetterMethodDecl(jcVariableDecl));
                    });
                    super.visitClassDef(jcClassDecl);
                }

            });
        });
        //我们有修改过AST,所以返回true
        return true;
    }

    private JCTree.JCMethodDecl makeGetterMethodDecl(JCTree.JCVariableDecl jcVariableDecl) {
        /***
         * JCStatement:声明语法树节点,常见的子类如下
         * JCBlock:语句块语法树节点
         * JCReturn:return语句语法树节点
         * JCClassDecl:类定义语法树节点
         * JCVariableDecl:字段/变量定义语法树节点
         * JCMethodDecl:方法定义语法树节点
         * JCModifiers:访问标志语法树节点
         * JCExpression:表达式语法树节点,常见的子类如下
         * JCAssign:赋值语句语法树节点
         * JCIdent:标识符语法树节点,可以是变量,类型,关键字等等
         */
        ListBuffer<JCTree.JCStatement> statements = new ListBuffer<>(); statements.append(treeMaker.Return(treeMaker.Select(treeMaker.Ident(names.fromString("this")), jcVariableDecl.getName())));

        JCTree.JCBlock body = treeMaker.Block(0, statements.toList());

        return treeMaker.MethodDef(
                treeMaker.Modifiers(Flags.PUBLIC),//mods:访问标志
                getNewMethodName(jcVariableDecl.getName()),//name:方法名
                jcVariableDecl.vartype,//restype:返回类型
                List.nil(),//typarams:泛型参数列表
                List.nil(),//params:参数列表
                List.nil(),//thrown:异常声明列表
                body,//方法体
                null);
    }

    private Name getNewMethodName(Name name) {
        String s = name.toString();
        return names.fromString("get" + s.substring(0, 1).toUpperCase() + s.substring(1, name.length()));
    }
}

SetterProcessorSetter注解的处理器

@SupportedAnnotationTypes("com.charles.processor.Setter")
//@SupportedSourceVersion(SourceVersion.RELEASE_8)
public class SetterProcessor extends AbstractProcessor {

    private Messager messager;

    private JavacTrees trees;

    private TreeMaker treeMaker;

    private Names names;

    @Override
    public SourceVersion getSupportedSourceVersion() {
        return SourceVersion.latestSupported();
    }

    @Override
    public synchronized void init(ProcessingEnvironment processingEnv) {
        super.init(processingEnv);
        this.messager = processingEnv.getMessager();
        this.trees = JavacTrees.instance(processingEnv);
        Context context = ((JavacProcessingEnvironment) processingEnv).getContext();
        this.treeMaker = TreeMaker.instance(context);
        this.names = Names.instance(context);
    }

    @Override
    public boolean process(Set<? extends TypeElement> annotations, RoundEnvironment roundEnv) {
        Set<? extends Element> set = roundEnv.getElementsAnnotatedWith(Setter.class);

        set.forEach(element -> {
            JCTree jcTree = trees.getTree(element);
            jcTree.accept(new TreeTranslator() {
                @Override
                public void visitClassDef(JCTree.JCClassDecl jcClassDecl) {
                    List<JCTree.JCVariableDecl> jcVariableDeclList = List.nil();
                    for (JCTree tree : jcClassDecl.defs) {
                        if (tree.getKind().equals(Tree.Kind.VARIABLE)) {
                            JCTree.JCVariableDecl jcVariableDecl = (JCTree.JCVariableDecl) tree;
                            jcVariableDeclList = jcVariableDeclList.append(jcVariableDecl);
                        }
                    }
                    jcVariableDeclList.forEach(jcVariableDecl -> {
                        messager.printMessage(Diagnostic.Kind.NOTE, "set " + jcVariableDecl.getName() + " has been processed");
                        treeMaker.pos = jcVariableDecl.pos;
                        jcClassDecl.defs = jcClassDecl.defs.prepend(makeSetterMethodDecl(jcVariableDecl));

                    });
                    super.visitClassDef(jcClassDecl);
                }

            });
        });
        return true;
    }


    private JCTree.JCMethodDecl makeSetterMethodDecl(JCTree.JCVariableDecl jcVariableDecl) {
        ListBuffer<JCTree.JCStatement> statements = new ListBuffer<>();
        statements.append(
                treeMaker.Exec(
                        treeMaker.Assign(
                                treeMaker.Select(
                                        treeMaker.Ident(names.fromString("this")),
                                        names.fromString(jcVariableDecl.name.toString())
                                ),
                                treeMaker.Ident(names.fromString(jcVariableDecl.name.toString()))
                        )
                )
        );

        JCTree.JCBlock body = treeMaker.Block(0, statements.toList());

        // 生成入参
        JCTree.JCVariableDecl param = treeMaker.VarDef(treeMaker.Modifiers(Flags.PARAMETER), jcVariableDecl.getName(),jcVariableDecl.vartype, null);

        List<JCTree.JCVariableDecl> paramList = List.of(param);

        return treeMaker.MethodDef(
                treeMaker.Modifiers(Flags.PUBLIC), // 方法限定值
                setNewMethodName(jcVariableDecl.getName()), // 方法名
                treeMaker.Type(new Type.JCVoidType()), // 返回类型
                List.nil(),
                paramList, // 入参
                List.nil(),
                body,
                null
        );

    }

    private Name setNewMethodName(Name name) {
        String s = name.toString();
        return names.fromString("set" + s.substring(0, 1).toUpperCase() + s.substring(1, name.length()));
    }
}

在这里插入图片描述

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/490455.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

Android-源码分析-分析手机热点里的AP Band(频段)被隐藏/置灰的原因?

本博文记录寻找手机热点中AP Band(频段)被隐藏/置灰的原因&#xff0c;相似问题同理去查找解决。 先放上一张MTK平台手机-热点-AP Band界面效果图&#xff1a; 很明显&#xff0c;界面中的AP Band选项变灰&#xff0c;无法点击编辑修改内容&#xff0c;如果是AP Band 被隐藏或…

C#,生信软件实践(01)——DNA序列数据库FASTA文件合并工具的源代码

1 生物信息学简介 生物信息学&#xff08;BioInformatics&#xff09;是研究生物信息的采集、处理、存储、传播&#xff0c;分析和解释等各方面的学科&#xff0c;也是随着生命科学和计算机科学的迅猛发展&#xff0c;生命科学和计算机科学相结合形成的一门新学科。它通过综合…

JavaWeb ( 四 ) JavaEE

2.JavaEE 2.1.Java版本 J2SE : 适用于桌面系统的Java 2平台标准版&#xff08;Java 2 Platform Standard Edition&#xff0c;J2SE&#xff09; J2EE : 适用于创建服务器应用程序和服务的Java 2平台企业版&#xff08;Java 2 Platform Enterprise Edition&#xff0c;J2EE&a…

学企业管理

工业社会的代表产品是交通运输设备&#xff0c;如火车/高铁、汽车、飞机/火箭、船舶/航母&#xff0c;其核心是发动机。信息社会的代表产品是计算设备&#xff0c;如大型机小型机、工作站/PC台式机电脑/PC笔记本电脑/PC平板电脑、智能手机。 汽车这个产品&#xff0c;既属于高精…

调试别人的API,一般有哪些步骤?

当我们使用了一些由别人实现的API接口时&#xff0c;该如何进行调试呢&#xff1f;当我们使用的API返回一些意想不到错误时&#xff0c;该怎么办呢?这个问题可能是由于用户输入或者API本身&#xff0c;或者其他完全无关的内容等引起的。调试是我们进行定位并修复由单个API调用…

漫天花雨HTML特效+3D相册

大家好&#xff0c;我是csdn的博主&#xff1a;lqj_本人 这是我的个人博客主页&#xff1a; lqj_本人的博客_CSDN博客-微信小程序,前端,python领域博主lqj_本人擅长微信小程序,前端,python,等方面的知识https://blog.csdn.net/lbcyllqj?spm1011.2415.3001.5343哔哩哔哩欢迎关注…

软件工程期末复习(背题家速成)

文章目录 前言一、选择题1、第一章 软件工程综述2、第二章 软件过程3、第三章 可行性研究4、第四章 结构化需求分析5、第五章 结构化软件设计6、第六章 面向对象的需求分析7、第七章 第7章面向对象设计8、第八章 基于构件的开发9、第九章 软件项目的测试10、第十章 软件实施、维…

每天一道算法练习题--Day2 第一章 --算法专题 --- ----------位运算

我这里总结了几道位运算的题目分享给大家&#xff0c;分别是 136 和 137&#xff0c; 260 和 645&#xff0c; 总共加起来四道题。 四道题全部都是位运算的套路&#xff0c;如果你想练习位运算的话&#xff0c;不要错过哦&#xff5e;&#xff5e; 前菜 开始之前我们先了解下…

Umi 插件实战教程

引言 笔者最近开发了一款 umi 插件&#xff1a;plugin-umi-cmdk[1],该插件的功能主要是&#xff1a;在 umi 项目里可以方便的集成 cmd k &#xff0c;实现菜单等搜索。 主体功能并不复杂&#xff0c;但是在集成作为 umi 插件过程中踩了不少坑&#xff0c;主要是 umi 官方文档的…

【计算机网络】面试高频问题汇总及详细解答

【C语言部分】面试高频问题汇总及详细解答 【操作系统(Linux)】面试高频问题汇总及详细解答 【数据库】面试高频问题汇总及详细解答 本文目录 1. 简述网络七层参考模型及每一层的作用2. 简述静态路由和动态路由3. 说说有哪些路由协议&#xff0c;都是如何更新的4. 简述域名解析…

PostgreSQL Explain 复杂执行计划怎么看 --- 逐个分解PG执行计划的那些操作

开头还是介绍一下群&#xff0c;如果感兴趣polardb ,mongodb ,mysql ,postgresql ,redis 等有问题&#xff0c;有需求都可以加群群内有各大数据库行业大咖&#xff0c;CTO&#xff0c;可以解决你的问题。加群请联系 liuaustin3 &#xff0c;在新加的朋友会分到2群&#xff08;共…

Codeforces-Round-805-Div-3-E-Split-Into-Two-Sets

title: Codeforces Round 805 (Div. 3) E. Split Into Two Sets date: 2023-04-25 18:14:41 categories: AlgorithmCodeforces tags:codeforces并查集1600 E. Split Into Two Sets 题目大意 给你n组数&#xff0c;每组里面有两个数字&#xff0c;问你能不能把这n组数分为两组…

【数据架构系列-06】一文搞懂数据模型的3种类型——概念模型、逻辑模型、物理模型

数据模型就是模拟现实世界的方法论&#xff0c;是通向智慧世界的基石&#xff01; 从现实世界发展到智慧世界&#xff0c;要数经历现实世界、信息世界、计算机世界、数据世界、智慧世界五个不同的世界&#xff0c;我们天生具有从混沌的世界抽象信息变为信息世界的能力&#xff…

基于jenkinsfile布置java工程

需求 通过jenkins发布java项目到服务器 预备环境 项目地址&#xff1a; https://gitee.com/asaland/sb-docker-appJenkins 2.387.3 通过Jenkinsfile实现方式 jenkins ui 配置pipeline 什么是pipeline? 直接看注释吧&#xff0c;简单点就是编排可以多个跨时间的构建代理…

JavaScript全解析——canvas 绘制变换和渐变

绘制变换 ●在 cancas 内, 也可以向 css 中一样, 出现一些 2d 变换的效果 ●先来绘制一个基本矩形 // 0. 获取到页面上的 canvas 标签元素节点 const canvasEle document.querySelector(#canvas)// 1. 获取当前这个画布的工具箱 const ctx canvasEle.getContext(2d)// 2. 绘…

ip数据报计算首部检验和

当我们在使用互联网进行数据传输时&#xff0c;数据可能会被篡改或者损坏。为了保证数据传输的可靠性和完整性&#xff0c;计算IP数据包首部检验和是一种非常重要的校验机制。本文将会介绍计算IP数据包首部检验和的方法。 IP数据包首部 首先&#xff0c;我们需要了解IP数据包…

5 系统数据文件和信息

5.1 口令文件 口令文件包含了下表中所示的各字段&#xff0c;这些字段包含在<pwd.h>中定义的passwd结构中。 /etc/passwd文件中的字段 说 明struct passwd 成员用户名char *pw_name加密口令char *pw_passwd数值用户IDuid_t pw_uid数值组IDgid_t pw_gid注释字段char *pw_g…

Unity实现GPU Cull渲染

前言 开放世界游戏中植被和物件的数量往往是巨大, 而传统组织大量植被渲染的方式是利用QuadTree/Octree/Kd-Tree等数据结构对植被Intance数据进行预先生成一个个Cluster&#xff0c;然后在运行时进行FrustumCull,收集可视的所有Cluster&#xff0c;最后进行DrawInstance. 这…

Yolov1 源码讲解 loss.py

结构 1.lt rb我觉得不是很合适 正确来说是lb rt 因为比较出来的都是左下和右上坐标 比如前两个&#xff0c;都是max出来的 选两个box左下坐标中最大的&#xff0c; 后两个则是右上坐标中最小的 那也就形成了交集面积 但是代码中仍然是lt rb我也就直接这样说 而算出lt和r…

zynq基于XDMA实现PCIE X8视频采集HDMI输出 提供工程源码和QT上位机程序和技术支持

目录 1、前言2、我已有的PCIE方案3、基于zynq架构的PCIE4、总体设计思路和方案视频输入通路PCIE数据缓存通路视频输出通路 5、vivado工程详解6、SDK 工程详解7、驱动安装8、QT上位机软件9、上板调试验证10、福利&#xff1a;工程代码的获取 1、前言 PCIE&#xff08;PCI Expre…