Lombok实现原理解析

news2025/1/13 10:14:44

文章目录

  • 前言
  • 一、Lombok注解分析
  • 二、编译期的注解处理期
  • 三、Lombok使用方法
  • 四、自定义注解处理器
    • 1、自定义注解
    • 2、实现Processor接口
    • 3、注册注解处理器
  • 五、实战MyGetter注解
    • 1、新建Maven工程myLombok
    • 2、新建子模块myget
    • 3、新建子模块person
    • 4、编译并查看结果
  • 总结


前言

相信做java开发的小伙伴对Lombok都不陌生,基于Lombok我们可以通过给实体类添加一些简单的注解在不改变原有代码情况下在源代码中嵌入补充信息,比如常见的Get、Set方法。
那么有小伙伴想过其底层实现原理是什么?


一、Lombok注解分析

这里我们以使用最多的@Data为例进行分析。

package lombok;

import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

@Target({ElementType.TYPE})
@Retention(RetentionPolicy.SOURCE)
public @interface Data {
    String staticConstructor() default "";
}

说明:

  • 元注解@Target({ElementType.TYPE}) - 用来说明注解@Data是用在描述类、接口(包括注解类型)或枚举上的
  • 元注解@Retention(RetentionPolicy.SOURCE) - 用来说明注解@Data在源文件中有效(即源文件保留),编译时期会丢掉,在.class文件中不会保留注解信息。

我们在程序开发过程中,自定义注解用的最多的就是@Retention(RetentionPolicy.RUNTIME) 运行期注解,再结合切面、拦截器、反射等机制我们就可以在程序运行过程中根据类上面注解来进行一些逻辑处理。

而Lombok中的注解都是源文件保留级别的注解,编译成class文件就会丢失对应的注解信息,那么他是通过怎样的机制增加我们的实体类的呢???

补充:
注解信息的三种保留策略:

public enum RetentionPolicy {
    /**
     * Annotations are to be discarded by the compiler.
     * 注解信息被编译器丢弃
     */
    SOURCE,

    /**
     * Annotations are to be recorded in the class file by the compiler
     * but need not be retained by the VM at run time.  This is the default
     * behavior.
     * 注解信息会被编译器保留到class文件中,但是JVM运行期间不会保留。默认保留策略
     */
    CLASS,

    /**
     * Annotations are to be recorded in the class file by the compiler and
     * retained by the VM at run time, so they may be read reflectively.
     * 注解信息会被编译器保留再class文件中,并且在JVM运行期间保留
     * @see java.lang.reflect.AnnotatedElement
     */
    RUNTIME
}

二、编译期的注解处理期

在 JDK 6 后添加了 JSR 269: Pluggable Annotation Processing API (编译期的注解处理器) ,
通过该处理期我们可以实现在编译期间根据注解信息对生成的class信息进行增强,这也正是Lombok 实现的核心。

声明一系列的源文件级别的注解,在通过继承 AbstractProcessor 类自定义编译期的注解处理器,重写它的 init() 和 process() 方法,在编译期时把 Lombok 的注解转换为 Java 的常规方法的。

但同时 Lombok 也存在这一些使用上的缺点,比如:降低了可调试性、可能会有兼容性等问题,因此我们在使用时要根据自己的业务场景和实际情况,来选择要不要使用 Lombok,以及应该如何使用 Lombok。

接下来,我们进行lombok的原理分析,以Oracle的javac编译工具为例。自Java 6起,javac开始支持JSR 269 Pluggable Annotation Processing API规范,只要程序实现了该API,就能在java源码编译时调用定义的注解。
举例来说,现在有一个实现了"JSR 269 API"的程序A,那么使用javac编译源码的时候具体流程如下:
1、javac对源代码进行分析,生成一棵抽象语法树(AST);
2、运行过程中调用实现了"JSR 269 API"的A程序;
3、此时A程序就可以完成它自己的逻辑,包括修改第一步骤得到的抽象语法树(AST);
4、javac使用修改后的抽象语法树(AST)生成字节码文件;

详细的流程图如下:
在这里插入图片描述
可以看出,在编译期阶段,当 Java 源码被抽象成语法树 (AST) 之后,Lombok 会根据自己的注解处理器动态的修改 AST,增加新的代码 (节点),在这一切执行之后,再通过分析生成了最终的字节码 (.class) 文件,这就是 Lombok 的执行原理。

三、Lombok使用方法

使用Lombok项目的方法很简单,分为四个步骤:

  1. 安装插件,在编译类路径中加入lombok.jar包(具体安装方法可自己百度);
  2. 在需要简化的类或方法上,加上要使用的注解;
  3. 使用支持lombok的编译工具编译源代码(关于支持lombok的编译工具,见4.支持lombok的编译工具);
  4. 编译得到的字节码文件中自动生成Lombok注解对应的方法或代码;

以我们常见的IDEA开发工具为例,一定要首选在IDEA中安装Lombok插件,该步骤的作用就是添加Lombok注解的编译期的注解处理器。
在这里插入图片描述
项目中需要引入lombok的Meven依赖,里面主要包含lombok声明的全部注解信息。

  <dependency>
      <groupId>org.projectlombok</groupId>
      <artifactId>lombok</artifactId>
      <optional>true</optional>
  </dependency>

四、自定义注解处理器

实现一个自定义注解处理器需要有三个步骤:
第一是声明自定义注解,第二是实现Processor接口处理注解,第三是注册注解处理器。

1、自定义注解

import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

@Target({ElementType.TYPE})
@Retention(RetentionPolicy.SOURCE)
public @interface CustomAnnotation{

}

2、实现Processor接口

通过实现Processor接口可以自定义注解处理器,这里我们采用更简单的方法通过继承AbstractProcessor类实现自定义注解处理器。实现抽象方法process处理我们想要的功能。

注解处理器早在JDK1.5的时候就有这个功能了,只不过当时的注解处理器是apt,相关的api是在com.sun.mirror包下的。从JDK1.6开始,apt相关的功能已经包含在了javac中,并提供了新的api在javax.annotation.processing和javax.lang.model to process annotations这两个包中。旧版的注解处理器api在JDK1.7已经被标记为deprecated,并在JDK1.8中移除了apt和相关api。

public class CustomProcessor extends AbstractProcessor {
    //核心方法:注解处理过程
    @Override
    public boolean process(Set<? extends TypeElement> annotations, RoundEnvironment roundEnvironment) {
        return false;
    }

    //支持的注解类型
    @Override
    public Set<String> getSupportedAnnotationTypes() {
        Set<String> annotataions = new LinkedHashSet<String>();
        annotataions.add(CustomAnnotation.class.getCanonicalName());
        return annotataions;
    }

    //支持的java版本
    @Override
    public SourceVersion getSupportedSourceVersion() {
        return SourceVersion.latestSupported();
    }
}

也可以通过注解得方式指定支持的注解类型和JDK版本:

@SupportedAnnotationTypes({"com.laowan.annotation.CustomAnnotation"})
@SupportedSourceVersion(SourceVersion.RELEASE_8)
public class CustomProcessor extends AbstractProcessor {
    @Override
    public boolean process(Set<? extends TypeElement> annotations, RoundEnvironment roundEnvironment) {
        return false;
    }
}

因为兼容的原因,特别是针对Android平台,建议使用重载 getSupportedAnnotationTypes() 和 getSupportedSourceVersion()方法代替 @SupportedAnnotationTypes 和 @SupportedSourceVersion

3、注册注解处理器

最后我们还需要将我们自定义的注解处理器进行注册。新建resources文件夹,目录下新建META-INF文件夹,目录下新建services文件夹,目录下新建javax.annotation.processing.Processor文件,然后将我们自定义注解处理器的全类名写到此文件:

com.laowan.annotation.CustomProcessor

注意⚠️:
采用上面的方法注册自定义注解处理器时,一定要将resources文件夹设置为Resources Root,
不然执行编译期间会一只提示找不到javax.annotation.processing.Processor文件中配置的处理器。
在这里插入图片描述

示例,lombok中注册注解处理器:
在这里插入图片描述

上面这种注册的方式太麻烦了,谷歌帮我们写了一个注解处理器来生成这个文件。
github地址:https://github.com/google/auto

添加依赖:

<!-- https://mvnrepository.com/artifact/com.google.auto.service/auto-service -->
<dependency>
  <groupId>com.google.auto.service</groupId>
  <artifactId>auto-service</artifactId>
  <version>1.0.1</version>
</dependency>

添加注解:

@AutoService(Processor.class)
public class CustomProcessor extends AbstractProcessor {
    ...
}

Lombok中的示例:

@SupportedAnnotationTypes({"lombok.*"})
public static class ClaimingProcessor extends AbstractProcessor {
    public ClaimingProcessor() {
    }

    public boolean process(Set<? extends TypeElement> annotations, RoundEnvironment roundEnv) {
        return true;
    }

    public SourceVersion getSupportedSourceVersion() {
        return SourceVersion.latest();
    }
}

搞定,体会到注解处理器的强大木有。后面我们只需关注注解处理器中的处理逻辑即可。

五、实战MyGetter注解

我们实现一个简易版的 Lombok 自定义一个 Getter 方法,我们的实现步骤是:

  1. 自定义一个注解标签接口,并实现一个自定义的注解处理器;
  2. 利用 tools.jar 的 javac api 处理 AST (抽象语法树)
  3. 使用自定义的注解处理器编译代码。

1、新建Maven工程myLombok

其中包含2个子模块,myget用来存放自定义的注解和注解处理器,person模块用来使用自定义的注解。
在这里插入图片描述

myLombok工程的pom.xml文件:

<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 https://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>
    <packaging>pom</packaging>
    <modules>
        <module>myget</module>
        <module>person</module>
    </modules>

    <groupId>com.example</groupId>
    <artifactId>myLombok</artifactId>
    <version>0.0.1-SNAPSHOT</version>
    <name>myLombok</name>

    <properties>
        <java.version>1.8</java.version>
    </properties>
</project>

注意:
这里不要使用spring-boot-maven-plugin的编译器,不然会编译不通过。

2、新建子模块myget

1、添加Maven依赖

    <dependencies>
        <!--Processor中的解析过程需要依赖tools.jar-->
        <dependency>
            <groupId>com.sun</groupId>
            <artifactId>tools</artifactId>
            <version>1.6.0</version>
            <scope>system</scope>
            <systemPath>${java.home}/../lib/tools.jar</systemPath>
        </dependency>

        <!--采用google的auto-service来注入注解处理器-->
        <dependency>
            <groupId>com.google.auto.service</groupId>
            <artifactId>auto-service</artifactId>
            <version>1.0.1</version>
        </dependency>
    </dependencies>

2、首先创建一个 MyGetter.java 自定义一个注解,代码如下:

import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

@Retention(RetentionPolicy.SOURCE) // 注解只在源码中保留
@Target(ElementType.TYPE) // 用于修饰类
public @interface MyGetter { // 定义 Getter

}

2、再实现一个自定义的注解处理器MyGetterProcessor,代码如下:

//这里的导入最好直接拷贝过去
import com.sun.source.tree.Tree;
import com.sun.tools.javac.api.JavacTrees;
import com.sun.tools.javac.code.Flags;
import com.sun.tools.javac.code.Type;
import com.sun.tools.javac.processing.JavacProcessingEnvironment;
import com.sun.tools.javac.tree.JCTree;
import com.sun.tools.javac.tree.TreeMaker;
import com.sun.tools.javac.tree.TreeTranslator;
import com.sun.tools.javac.util.*;

import javax.annotation.processing.*;
import javax.lang.model.SourceVersion;
import javax.lang.model.element.Element;
import javax.lang.model.element.TypeElement;
import javax.tools.Diagnostic;
import java.util.Set;

@AutoService(Processor.class) //自动注入注解处理器
@SupportedSourceVersion(SourceVersion.RELEASE_8)
@SupportedAnnotationTypes("com.example.myget.annotation.MyGetter")
public class MyGetterProcessor extends AbstractProcessor {

    private Messager messager; // 编译时期输入日志的
    private JavacTrees javacTrees; // 提供了待处理的抽象语法树
    private TreeMaker treeMaker; // 封装了创建AST节点的一些方法
    private Names names; // 提供了创建标识符的方法

    @Override
    public synchronized void init(ProcessingEnvironment processingEnv) {
        super.init(processingEnv);
        this.messager = processingEnv.getMessager();
        this.javacTrees = 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> elementsAnnotatedWith = roundEnv.getElementsAnnotatedWith(MyGetter.class);
        elementsAnnotatedWith.forEach(e -> {
            JCTree tree = javacTrees.getTree(e);
            tree.accept(new TreeTranslator() {
                @Override
                public void visitClassDef(JCTree.JCClassDecl jcClassDecl) {
                    List<JCTree.JCVariableDecl> jcVariableDeclList = List.nil();
                    // 在抽象树中找出所有的变量
                    for (JCTree jcTree : jcClassDecl.defs) {
                        if (jcTree.getKind().equals(Tree.Kind.VARIABLE)) {
                            JCTree.JCVariableDecl jcVariableDecl = (JCTree.JCVariableDecl) jcTree;
                            jcVariableDeclList = jcVariableDeclList.append(jcVariableDecl);
                        }
                    }
                    // 对于变量进行生成方法的操作
                    jcVariableDeclList.forEach(jcVariableDecl -> {
                        messager.printMessage(Diagnostic.Kind.NOTE, jcVariableDecl.getName() + " has been processed");
                        jcClassDecl.defs = jcClassDecl.defs.prepend(makeGetterMethodDecl(jcVariableDecl));
                    });
                    super.visitClassDef(jcClassDecl);
                }
            });
        });
        return true;
    }

    private JCTree.JCMethodDecl makeGetterMethodDecl(JCTree.JCVariableDecl jcVariableDecl) {
        ListBuffer<JCTree.JCStatement> statements = new ListBuffer<>();
        // 生成表达式 例如 this.a = a;
        JCTree.JCExpressionStatement aThis = makeAssignment(treeMaker.Select(treeMaker.Ident(
                names.fromString("this")), jcVariableDecl.getName()), treeMaker.Ident(jcVariableDecl.getName()));
        statements.append(aThis);
        JCTree.JCBlock block = treeMaker.Block(0, statements.toList());

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

        // 生成返回对象
        JCTree.JCExpression methodType = treeMaker.Type(new Type.JCVoidType());
        return treeMaker.MethodDef(treeMaker.Modifiers(Flags.PUBLIC),
                getNewMethodName(jcVariableDecl.getName()), methodType, List.nil(),
                parameters, List.nil(), block, null);

    }

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

    private JCTree.JCExpressionStatement makeAssignment(JCTree.JCExpression lhs, JCTree.JCExpression rhs) {
        return treeMaker.Exec(
                treeMaker.Assign(
                        lhs,
                        rhs
                )
        );
    }
}

自定义的注解处理器是我们实现简易版的 Lombok 的重中之重,我们需要继承 AbstractProcessor 类,重写它的 init() 和 process() 方法,在 process() 方法中我们先查询所有的变量,在给变量添加对应的方法。我们使用 TreeMaker 对象和 Names 来处理 AST,这一步需要依赖 tool.jar, 如上代码所示。

3、新建子模块person

1、引入maven依赖

      <dependency>
          <groupId>com.example</groupId>
          <artifactId>myget</artifactId>
          <version>0.0.1-SNAPSHOT</version>
      </dependency>

2、新增Person类,并添加@MyGetter类注解

@MyGetter
public class Person {
    private String name;
}

4、编译并查看结果

1、执行编译打包
在这里插入图片描述

2、检查Person类的编译结果,自动生成了get方法,说明自定义的注解@MyGetter生效
在这里插入图片描述


总结

本文主要对Lombok的实现原理进行了介绍,并通过自定义注解@MyGetter演示了编译期注解处理器的使用过程。
1、通过元注解@Retention可以配置注解信息的保留策略RetentionPolicy:

  • SOURCE 源文件保留策略,编译过程会丢弃注解信息
  • CLASS class文件保留策略,注解信息会被编译器保留到class文件中,但是JVM运行期间不会保留。默认保留策略
  • RUNTIME 运行期保留策略,注解信息会被编译器保留再class文件中,并且在JVM运行期间保留

2、Lombok中的注解都是SOURCE源文件保留策略的注解,其实现原理是借助JDK 6 后添加的 JSR 269: Pluggable Annotation Processing API (编译期的注解处理器) ,通过该处理期实现在编译期间根据注解信息对生成的class信息进行增强。

3、自定义的编译期注解器的2种注册方式:resources方式和auto-service方式。

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

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

相关文章

171-有趣的OpenAI的chatGPT小实验

最近玩了一下chatGPT 问了他很多问题 然后我问了一个问题 帮我想10个帮女朋友过生日的办法 然后AI就回复了我10种 然后我继续问了我说再来10个 他又想了10种&#xff0c; 所以我特别想看看他到底有没有极限 10个 20个 30个 40个 50个 60个 70个 80个 90个 100个 接下去…

秋招---SQL学习

文章目录SQL的执行顺序一般是怎样的SQL如何性能优化1.select尽量不要查询全部*&#xff0c;而是查具体字段2.避免在where子句中使用 or 来连接条件3.尽量使用数值替代字符串类型tinyint,int,bigint,smallint类型4.用varchar代替char那什么时候要用char不用varchar呢链接&#x…

玩转华夏数艺

这里写自定义目录标题华夏数艺简述新的改变功能快捷键合理的创建标题&#xff0c;有助于目录的生成如何改变文本的样式插入链接与图片如何插入一段漂亮的代码片生成一个适合你的列表创建一个表格设定内容居中、居左、居右SmartyPants创建一个自定义列表如何创建一个注脚注释也是…

叶酸-葡聚糖-盐酸吡柔比星偶联物(FA-PRB-DEX-NPs)|丝裂霉素C-右旋糖酐交联物(MMC-D)

叶酸-葡聚糖-盐酸吡柔比星偶联物(FA-PRB-DEX-NPs) 产品描述&#xff1a;将葡聚糖,盐酸吡柔比星和叶酸按序化学合成,并进一步定量,采用体外细胞性实验(四甲基偶氮唑蓝法),观察游离盐酸吡柔比星,叶酸-葡聚糖-盐酸吡柔比星,叶酸-葡聚糖-盐酸吡柔比星游离叶酸对于不同浓度细胞株SG…

计算机毕业设计ssm+vue基本微信小程序的育教幼教知识学习系统 uniapp 小程序

项目介绍 随着互联网技术的发发展,计算机技术广泛应用在人们的生活中,逐渐成为日常工作、生活不可或缺的工具,各种管理系统层出不穷。时代对人们的知识水平和综合素质要求也越来越高了,因此出现了各种适合用户在线学习系统。广泛存在于PC系统,手机APP,电脑软件等等,其中用户量…

Effective C++条款29:为“异常安全”而努力是值得的(Strive for exception-safe code)

Effective C条款29&#xff1a;为“异常安全”而努力是值得的&#xff08;Strive for exception-safe code&#xff09;条款29&#xff1a;为“异常安全”而努力是值得的1、抛出异常的案例2、解决资源泄露的问题3、异常安全的三种保证4、两种解决异常安全的方法4.1 使用智能指针…

如何压缩动态图片大小?gif图太大了怎么压缩?

对于新媒体行业人员来说&#xff0c;平时在工作中需要存非常多的素材&#xff0c;这些素材中有很多就是gif格式的&#xff0c;随着积累的素材越来越多&#xff0c;这些素材会占用大量的储存空间&#xff0c;那么遇到这种情况应该怎么办呢&#xff1f;应该如何压缩动态图片大小&…

Flutter - 布局原理与约束(constraints)

尺寸限制类容器用于限制容器大小&#xff0c;Flutter中提供了多种这样的容器&#xff0c;如ConstrainedBox、SizedBox、UnconstrainedBox、AspectRatio 等 1 ConstrainedBox ConstrainedBox 用于对子组件添加额外的约束 一般作为最外层的父布局 2 BoxConstraints BoxConstrai…

[附源码]Python计算机毕业设计SSM基于社区人员管理系统(程序+LW)

项目运行 环境配置&#xff1a; Jdk1.8 Tomcat7.0 Mysql HBuilderX&#xff08;Webstorm也行&#xff09; Eclispe&#xff08;IntelliJ IDEA,Eclispe,MyEclispe,Sts都支持&#xff09;。 项目技术&#xff1a; SSM mybatis Maven Vue 等等组成&#xff0c;B/S模式 M…

阿里P8架构师精心整理:Dubbo+Docker+Kubernetes实战PDf,附面试题

前言 学习是一种基础性的能力。然而&#xff0c;“吾生也有涯&#xff0c;而知也无涯。”&#xff0c;如果学习不注意方法&#xff0c;则会“以有涯随无涯&#xff0c;殆矣”。 学习就像吃饭睡觉一样&#xff0c;是人的一种本能&#xff0c;人人都有学习的能力。我们在刚出生…

Keycloak之17.0.1 版本和Gerrit 整合-yellowcong

通过keycloak 来实现gerrit的用户管理。主要有几个步骤,1.安装gerrit,2.安装gerrit oauth 插件,3.配置gerrit . 4.创建keycloak的配置,添加realm,client,user ,三个,5.重启gerrit 测试。 17版本不一样的是,需要开启oauth,服务器增加前缀。 准备 Keycloak之17.0.1 版本安…

43. Python for 循环

43. Python for 循环 文章目录43. Python for 循环1. 课题导入2. 什么是循环3. 什么是for循环4. for 循环语法5. 可迭代对象6. for循环的执行流程7. for 循环的对象1. 循环对象为字符串2. 循环的对象不能为整数3. 循环的对象不能为浮点数4. 循环对象为布尔类型5. 循环对象为列表…

使用docker构建vue项目并成功运行在本地和线上

先说本地环境 windows10 node vue docker都已经安装齐全 获取nginx镜像 因为要用这个镜像来构建你的vue项目&#xff0c;就像给vue项目提供一个环境一样 docker pull nginx 创建 nginx config配置文件 在项目根目录下创建文件default.conf server {listen 80;s…

火灾报警产品-火灾探测报警产品

消防产品&#xff0c;是指专门用于火灾预防、灭火救援和火灾防护、避难、逃生的产品。适用范围 适用于消防联动控制系统设备、防火卷帘控制器、线型感温火灾探测器、城市消防远程监控产品。认证模式 型式试验初始工厂检查获证后监督。申请资料 1.认证委托人/生产者/生产企业的资…

全面支持 PyTorch 2.0:BladeDISC 5 月~11 月新功能发布

作者&#xff1a;BladeDISC研发团队 BladeDISC 上一次更新主要发布了 GPU AStitch 优化&#xff0c;方法来源于我们发表在 ASPLOS 2022上的论文AStitch。这一次&#xff0c;我们发布了 0.3.0 版本。 本次更新中 BladeDISC 社区全面支持了 PyTorch 2.0 编译&#xff0c;推进了…

同城跑腿系统搭建,灵活的配送选择满足更多场景

为了提供更加便捷的生活服务&#xff0c;同城跑腿系统搭建通过线上的同城跑腿服务平台&#xff0c;在网上用户可以申请同城服务的需求&#xff0c;平台的相关的工作人员快速的响应接单&#xff0c;快速进行同城的配送跑腿服务。 同城跑腿系统搭建&#xff0c;功能少是万万不能…

微信小程序第四篇:生成图片并保存到手机相册

系列文章传送门&#xff1a; 微信小程序第一篇&#xff1a;自定义组件详解 微信小程序第二篇&#xff1a;七种主流通信方法详解 微信小程序第三篇&#xff1a;获取页面节点信息 目录 一、封装分享组件 二、定义用户授权方法 三、调用流程 首先我们看一下要完成的效果&#x…

地理空间开发包 TatukGIS Developer Kernel 11.72.X Crack

TatukGIS Developer Kernel (DK) 是专业级 GIS SDK&#xff08;软件开发工具包&#xff09;&#xff0c;各行各业的客户都使用它来开发自定义 GIS 应用程序或向现有产品添加地理空间功能。DK 可作为多个 SDK 版本使用&#xff0c;每个版本都针对特定的开发平台进行本地编译&…

胡扯系列之私人AI助手系统的分析与设计

背景 随着时代的发展&#xff0c;计算机算力的提升和近些年来AI模型的井喷以及发展。人工智能应用已经深入我们的日常生活。如人脸识别&#xff0c;无人驾驶等等&#xff0c;同时为了更好地与用户进行交互&#xff0c;完成特定功能&#xff0c;智能对话助手应运而生。如今大量…

某宝付费买的价值上万的60G的Python学习资源,0基础轻松赚钱到手软,请低调使用,禁止外传

前言 你是否 还在为升职加薪发愁&#xff1f; 苦于领导看不到自己更多长处&#xff1f; 还在为房贷&#xff0c;车贷&#xff0c;生计而发愁&#xff1f; 苦于不上班如何轻松赚快钱补贴家用&#xff1f; 为了帮助财务、设计、运营、策划、销售、HR、金融从业者、电商从业…