聊聊如何通过APT+AST来实现AOP功能

前言

如果有使用过spring aop功能的小伙伴,应该都会知道spring aop主要是通过动态代理在运行时,对业务进行切面拦截操作。今天我们就来实现一下如何通过APT+AST在编译期时实现AOP功能。不过在此之前先科普一下APT和AST相关内容

APT(注解处理器)

apt可以查看我之前写过的文章聊聊如何运用JAVA注解处理器(APT)

AST(抽象语法树)

什么是AST

抽象语法树(Abstract Syntax Tree,AST),是源代码语法结构的一种抽象表示。它以树状的形式表现编程语言的语法结构,树上的每个节点都表示源代码中的一种结构。比如包、类型、修饰符、运算符、接口、返回值都可以是一个语法结构。

示例:

package com.example.adams.astdemo;
public class TestClass {
    int x = 0;
    int y = 1;
    public int testMethod(){
        int z = x + y;
        return z;
    }
}

对应的抽象语法树如下:

java的编译过程

重点关注步骤一和步骤二生成AST的过程

步骤一:词法分析,将源代码的字符流转变为 Token 列表。

通过词法分析器分析源文件中的所有字符,将所有的单词或字符都转化成符合规范的Token

规范化的token可以分成一下三种类型:

java关键字:public, static, final, String, int等等;
自定义的名称:包名,类名,方法名和变量名;
运算符或者逻辑运算符等符号:+、-、*、/、&&,|| 等等。

步骤二: 语法分析,根据 Token 流来构造树形表达式也就是 AST。

语法树的每一个节点都代表着程序代码中的一个语法结构,如类型、修饰符、运算符等。经过这个步骤后,编译器就基本不会再对源码文件进行操作了,后续的操作都建立在抽象语法树之上。

AST的应用场景

AST 定义了代码的结构,通过操作 AST,我们可以精准地定位到声明语句、赋值语句、运算语句等,实现对源代码的分析、优化、变更等操作。

注: AST操作属于编译器级别,对程序运行完全没有影响,效率相对其他AOP更高

java抽象语法树常用API类介绍

JCTree

JCTree 是语法树元素的基类,包含一个重要的字段 pos,该字段用于指明当前语法树节点(JCTree)在语法树中的位置,因此我们不能直接用 new 关键字来创建语法树节点,即使创建了也没有意义。

重点介绍几个JCTree的子类:

1、JCStatement:声明语法树节点,常见的子类如下

  • JCBlock:语句块语法树节点
  • JCReturn:return 语句语法树节点
  • JCClassDecl:类定义语法树节点
  • JCVariableDecl:字段 / 变量定义语法树节点

2、JCMethodDecl:方法定义语法树节点
3、JCModifiers:访问标志语法树节点
4、JCExpression:表达式语法树节点,常见的子类如下

  • JCAssign:赋值语句
  • JCAssignOp:+=
  • JCIdent:标识符,可以是变量,类型,关键字等等
  • JCLiteral: 字面量表达式,如123, “string”等
  • JCBinary:二元操作符

JCTrees更多API的介绍可以查看如下链接

https://blog.csdn.net/u013998373/article/details/90050810

TreeMaker

TreeMaker 用于创建一系列的语法树节点,我们上面说了创建 JCTree 不能直接使用 new 关键字来创建,所以 Java 为我们提供了一个工具,就是 TreeMaker,它会在创建时为我们创建的 JCTree 对象设置 pos 字段,所以必须使用上下文相关的 TreeMaker 对象来创建语法树节点。

着重介绍一下常用的几个方法

TreeMaker.Modifiers

TreeMaker.Modifiers 方法用于创建访问标志语法树节点(JCModifiers),源码如下

public JCModifiers Modifiers(long flags) {
    return Modifiers(flags, List.< JCAnnotation >nil());
}

public JCModifiers Modifiers(long flags,
    List<JCAnnotation> annotations) {
        JCModifiers tree = new JCModifiers(flags, annotations);
        boolean noFlags = (flags & (Flags.ModifierFlags | Flags.ANNOTATION)) == 0;
        tree.pos = (noFlags && annotations.isEmpty()) ? Position.NOPOS : pos;
        return tree;
}
  1. flags:访问标志
  2. annotations:注解列表

其中 flags 可以使用枚举类 com.sun.tools.javac.code.Flags 来表示,例如我们可以这样用,就生成了下面的访问标志了。

示例:
创建访问修饰符 public

treeMaker.Modifiers(Flags.PUBLIC);

TreeMaker.ClassDef

TreeMaker.ClassDef 用于创建类定义语法树节点(JCClassDecl), 源码如下:

public JCClassDecl ClassDef(JCModifiers mods,
    Name name,
    List<JCTypeParameter> typarams,
    JCExpression extending,
    List<JCExpression> implementing,
    List<JCTree> defs) {
        JCClassDecl tree = new JCClassDecl(mods,
                                     name,
                                     typarams,
                                     extending,
                                     implementing,
                                     defs,
                                     null);
        tree.pos = pos;
        return tree;
}

  1. mods:访问标志,可以通过 TreeMaker.Modifiers 来创建
  2. name:类名
  3. typarams:泛型参数列表
  4. extending:父类
  5. implementing:实现的接口
  6. defs:类定义的详细语句,包括字段、方法的定义等等

TreeMaker.MethodDef

TreeMaker.MethodDef 用于创建方法定义语法树节点(JCMethodDecl),源码如下

public JCMethodDecl MethodDef(JCModifiers mods,
    Name name,
    JCExpression restype,
    List<JCTypeParameter> typarams,
    List<JCVariableDecl> params,
    List<JCExpression> thrown,
    JCBlock body,
    JCExpression defaultValue) {
        JCMethodDecl tree = new JCMethodDecl(mods,
                                       name,
                                       restype,
                                       typarams,
                                       params,
                                       thrown,
                                       body,
                                       defaultValue,
                                       null);
        tree.pos = pos;
        return tree;
}

public JCMethodDecl MethodDef(MethodSymbol m,
    Type mtype,
    JCBlock body) {
        return (JCMethodDecl)
            new JCMethodDecl(
                Modifiers(m.flags(), Annotations(m.getAnnotationMirrors())),
                m.name,
                Type(mtype.getReturnType()),
                TypeParams(mtype.getTypeArguments()),
                Params(mtype.getParameterTypes(), m),
                Types(mtype.getThrownTypes()),
                body,
                null,
                m).setPos(pos).setType(mtype);
}
  1. mods:访问标志
  2. name:方法名
  3. restype:返回类型
  4. typarams:泛型参数列表
  5. params:参数列表
  6. thrown:异常声明列表
  7. body:方法体
  8. defaultValue:默认方法(可能是 interface 中的哪个 default)
  9. m:方法符号
  10. mtype:方法类型。包含多种类型,泛型参数类型、方法参数类型、异常参数类型、返回参数类型。

注: 返回类型 restype 填写 null 或者 treeMaker.TypeIdent(TypeTag.VOID) 都代表返回 void 类型

示例

创建方法

    public String getUserName(String userName){
       return userName;
    }
ListBuffer<JCTree.JCStatement> usernameStatement = new ListBuffer<>();
usernameStatement.append(treeMaker.Return(treeMaker.Ident(names.fromString("userName"))));
JCTree.JCBlock usernameBody = treeMaker.Block(0, usernameStatement .toList());

// 生成入参
JCTree.JCVariableDecl param = treeMaker.VarDef(treeMaker.Modifiers(Flags.PARAMETER), names.fromString("userName"),treeMaker.Ident(names.fromString("String")), null);
com.sun.tools.javac.util.List<JCTree.JCVariableDecl> parameters = com.sun.tools.javac.util.List.of(param);

JCTree.JCMethodDecl username = treeMaker.MethodDef(
        treeMaker.Modifiers(Flags.PUBLIC), 
        names.fromString("getUserName"), // 方法名
        treeMaker.Ident(names.fromString("String")), // 返回类型
        com.sun.tools.javac.util.List.nil(),
        parameters, // 入参
        com.sun.tools.javac.util.List.nil(),
        usernameBody ,
        null
);

TreeMaker.VarDef

TreeMaker.VarDef 用于创建字段 / 变量定义语法树节点(JCVariableDecl),源码如下

public JCVariableDecl VarDef(JCModifiers mods,
    Name name,
    JCExpression vartype,
    JCExpression init) {
        JCVariableDecl tree = new JCVariableDecl(mods, name, vartype, init, null);
        tree.pos = pos;
        return tree;
}

public JCVariableDecl VarDef(VarSymbol v,
    JCExpression init) {
        return (JCVariableDecl)
            new JCVariableDecl(
                Modifiers(v.flags(), Annotations(v.getAnnotationMirrors())),
                v.name,
                Type(v.type),
                init,
                v).setPos(pos).setType(v.type);
}
  1. mods:访问标志
  2. name:参数名称
  3. vartype:类型
  4. init:初始化语句
  5. v:变量符号

示例:

创建变量: private String username = “lyb-geek”;

treeMaker.VarDef(treeMaker.Modifiers(Flags.PRIVATE), names.fromString("useranme"), treeMaker.Ident(names.fromString("String"),  treeMaker.Literal("lyb-geek");

TreeMaker.Ident

TreeMaker.Ident 用于创建标识符语法树节点(JCIdent)可以表示类、变量引用或者方法。源码如下

public JCIdent Ident(Name name) {
        JCIdent tree = new JCIdent(name, null);
        tree.pos = pos;
        return tree;
}

public JCIdent Ident(Symbol sym) {
        return (JCIdent)new JCIdent((sym.name != names.empty)
                                ? sym.name
                                : sym.flatName(), sym)
            .setPos(pos)
            .setType(sym.type);
}

public JCExpression Ident(JCVariableDecl param) {
        return Ident(param.sym);
}

示例:

创建username的引用

treeMaker.Ident(names.fromString("username"))))

TreeMaker.Return

TreeMaker.Return 用于创建 return 语句(JCReturn),源码如下

public JCReturn Return(JCExpression expr) {
        JCReturn tree = new JCReturn(expr);
        tree.pos = pos;
        return tree;
}

示例
return this.username

treeMaker.Return(treeMaker.Select(treeMaker.Ident(names.fromString("this")),names.fromString("useranme")));

TreeMaker.Select

TreeMaker.Select 用于创建域访问 / 方法访问(这里的方法访问只是取到名字,方法的调用需要用 TreeMaker.Apply)语法树节点(JCFieldAccess),源码如下

public JCFieldAccess Select(JCExpression selected,
    Name selector) 
{
        JCFieldAccess tree = new JCFieldAccess(selected, selector, null);
        tree.pos = pos;
        return tree;
}

public JCExpression Select(JCExpression base,
    Symbol sym) {
        return new JCFieldAccess(base, sym.name, sym).setPos(pos).setType(sym.type);
}
  1. selected:. 运算符左边的表达式
  2. selector:. 运算符右边的表达式

示例

获取方法logDTO.setArgs()

treeMaker.Select(treeMaker.Ident(getNameFromString("logDTO")),
                        getNameFromString("setArgs")

TreeMaker.NewClass

TreeMaker.NewClass 用于创建 new 语句语法树节点(JCNewClass), 源码如下:

public JCNewClass NewClass(JCExpression encl,
    List<JCExpression> typeargs,
    JCExpression clazz,
    List<JCExpression> args,
    JCClassDecl def) {
        JCNewClass tree = new JCNewClass(encl, typeargs, clazz, args, def);
        tree.pos = pos;
        return tree;
}
  1. encl:不太明白此参数的含义,我看很多例子中此参数都设置为 null
  2. typeargs:参数类型列表
  3. clazz:待创建对象的类型
  4. args:参数列表
  5. def:类定义

示例:

创建 List args = new ArrayList();

 JCTree.JCNewClass argsListclass = treeMaker.NewClass(null, null, memberAccess("java.util.ArrayList"), List.nil(), null);

        
        JCTree.JCVariableDecl args = makeVarDef(treeMaker.Modifiers(0),
                memberAccess("java.util.List"),
                "args",
                argsListclass
        );

TreeMaker.Apply

TreeMaker.Apply 用于创建方法调用语法树节点(JCMethodInvocation),源码如下:

public JCMethodInvocation Apply(List<JCExpression> typeargs,
    JCExpression fn,
    List<JCExpression> args) {
        JCMethodInvocation tree = new JCMethodInvocation(typeargs, fn, args);
        tree.pos = pos;
        return tree;
}
  1. typeargs:参数类型列表
  2. fn:调用语句
  3. args:参数列表

TreeMaker.Assign
TreeMaker.Assign 用户创建赋值语句语法树节点(JCAssign),源码如下:

public JCAssign Assign(JCExpression lhs,
    JCExpression rhs) {
        JCAssign tree = new JCAssign(lhs, rhs);
        tree.pos = pos;
        return tree;
}
  1. lhs:赋值语句左边表达式
  2. rhs:赋值语句右边表达式

示例
创建 username = “lyb-geek”

treeMaker.Assign(treeMaker.Ident(names.fromString("username")))), treeMaker.Literal("lyb-geek"))

TreeMaker.Exec

TreeMaker.Exec 用于创建可执行语句语法树节点(JCExpressionStatement),源码如下:

public JCExpressionStatement Exec(JCExpression expr) {
        JCExpressionStatement tree = new JCExpressionStatement(expr);
        tree.pos = pos;
        return tree;
}

注: TreeMaker.Apply 以及 TreeMaker.Assign 就需要外面包一层 TreeMaker.Exec 来获得一个 JCExpressionStatement

示例:

username = “lyb-geek”

treeMaker.Exec(treeMaker.Assign(treeMaker.Ident(names.fromString("username")),treeMaker.Binary(JCTree.Tag.PLUS,treeMaker.Literal("lyb"),treeMaker.Literal("-geek"))))

TreeMaker.Block

TreeMaker.Block 用于创建组合语句的语法树节点(JCBlock),源码如下:

public JCBlock Block(long flags,
    List<JCStatement> stats) {
        JCBlock tree = new JCBlock(flags, stats);
        tree.pos = pos;
        return tree;
}
  1. flags:访问标志
  2. stats:语句列表

示例

创建代码块

List<JCTree.JCStatement> jcStatementList = List.nil();
treeMaker.Block(0, jcStatementList);

TreeMaker更多详细API可以查看如下链接
http://www.docjar.com/docs/api/com/sun/tools/javac/tree/TreeMaker.html

Names

Names封装了操作标识符的方法,类、方法、参数的名称都可以通过names来获取

大家如果对AST感兴趣,可以通过https://astexplorer.net/在线体验一下

实战

示例主要通过APT+AST实现一个统计方法调用耗时以及记录日志的功能

注: 大家可以通过JavaParserJavaParser来简化对AST的操作。

本示例通过jdk自带的tools.jar工具类进行操作

1、在pom引入tools.jar gav

  <dependency>
            <groupId>com.sun</groupId>
            <artifactId>tools</artifactId>
            <version>1.8</version>
            <scope>system</scope>
            <systemPath>${java.home}/../lib/tools.jar</systemPath>
        </dependency>

2、自定义注解CostTimeRecoder

@Target(ElementType.METHOD)
@Retention(RetentionPolicy.SOURCE)
@Documented
public @interface CostTimeRecoder {
}

3、编写注解处理器

@AutoService(Processor.class)
@SupportedOptions("debug")
public class CostTimeRecordProcessor extends AbstractComponentProcessor {


    /**
     * 元素辅助类
     */
    private Elements elementUtils;

    /**
     * 日志输出工具类
     */
    private Messager meessager;

    /**
     * 抽象语法树
     */
    private JavacTrees trees;

    /**
     * 封装了创建或者修改AST节点的一些方法
     */
    private TreeMaker treeMaker;

    /**
     * 封装了操作标识符的方法
     */
    private Names names;



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


    @Override
    public Set<String> getSupportedAnnotationTypes() {
        return Collections.singleton(CostTimeRecoder.class.getName());
    }


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

        if (annotations == null || annotations.isEmpty()) {
            return false;
        }


        Set<? extends Element> elements = roundEnv.getElementsAnnotatedWith(CostTimeRecoder.class);
        if (elements == null || elements.isEmpty()){
            return false;
        }

        if (!roundEnv.processingOver()) {
            elements.stream() .filter(element -> element instanceof ExecutableElement)
                    .map(element -> (ExecutableElement) element)
                    .forEach(method -> {
                        TypeElement typeElement = (TypeElement)method.getEnclosingElement();
                        JCTree.JCClassDecl tree = trees.getTree(typeElement);
                        JCTree.JCMethodDecl methodDecl = trees.getTree(method);
                        CostTimeRecordAstTranslator costTimeRecordAstTranslator = new CostTimeRecordAstTranslator(treeMaker,names,meessager,tree,methodDecl);
                        costTimeRecordAstTranslator.setTrees(trees);
                        // 导入引用类,如果不配置import,则方法调用,需配置全类路径,
                        // 比如LogFactory.getLogger(),如果没导入LogFactory,则方法需写成com.github.lybgeek.log.factory.LogFactory.getLogger
                        // 配置后,仅需写成LogFactory.getLogger即可
                        costTimeRecordAstTranslator.addImportInfo(typeElement, LogFactory.class.getPackage().getName(),LogFactory.class.getSimpleName());
                        costTimeRecordAstTranslator.addImportInfo(typeElement,LogDTO.class.getPackage().getName(),LogDTO.class.getSimpleName());
//                        costTimeRecordAstTranslator.addImportInfo(typeElement, LogService.class.getPackage().getName(),LogService.class.getSimpleName());

                        tree.accept(costTimeRecordAstTranslator);

                    });
        }



        return false;
    }


    private String getPackageName(TypeElement typeElement) {
        return elementUtils.getPackageOf(typeElement).getQualifiedName()
                .toString();
    }




}

3、编写AST TreeTranslator

:省略业务的TreeTranslator,就列出基类,可能对大家比较有用,需要业务的实现方法,直接见下方demo链接

public abstract class AbstractTreeTranslator extends TreeTranslator {

    /**
     * 封装了创建或者修改AST节点的一些方法
     */
    protected TreeMaker treeMaker;

    /**
     * 封装了操作标识符的方法
     */
    protected Names names;

    /**
     * 日志输出工具类
     */
    protected Messager meessager;

    /**
     * 抽象语法树
     */
    private JavacTrees trees;

    public AbstractTreeTranslator(TreeMaker treeMaker, Names names, Messager meessager) {
        this.treeMaker = treeMaker;
        this.names = names;
        this.meessager = meessager;
    }


    /**
     * 根据字符串获取Name
     * @param s
     * @return
     */
    public Name getNameFromString(String s) { return names.fromString(s); }


    /**
     * 创建变量语句
     * @param modifiers 访问修饰符
     * @param name 参数名称
     * @param varType 参数类型
     * @param init 初始化赋值语句
     * 示例
     *   JCTree.JCVariableDecl var = makeVarDef(treeMaker.Modifiers(0), "xiao", memberAccess("java.lang.String"), treeMaker.Literal("methodName"));
     *   生成语句为:String xiao = "methodName";
     * @return
     */
    public JCTree.JCVariableDecl makeVarDef(JCTree.JCModifiers modifiers, JCTree.JCExpression varType,String name, JCTree.JCExpression init) {
        return treeMaker.VarDef(
                modifiers,
                getNameFromString(name),
                varType,
                init
        );
    }

    /**
     * 创建 域/方法 的多级访问, 方法的标识只能是最后一个
     * @param components 比如java.lang.System.out.println
     * @return
     */
    public JCTree.JCExpression memberAccess(String components) {
        String[] componentArray = components.split("\\.");
        JCTree.JCExpression expr = treeMaker.Ident(getNameFromString(componentArray[0]));
        for (int i = 1; i < componentArray.length; i++) {
            expr = treeMaker.Select(expr, getNameFromString(componentArray[i]));
        }
        return expr;
    }

    /**
     * 给变量赋值
     * @param lhs
     * @param rhs
     * @return
     * 示例:makeAssignment(treeMaker.Ident(getNameFromString("xiao")), treeMaker.Literal("assignment test"));
     * 生成的赋值语句为:xiao = "assignment test";
     */
    public JCTree.JCExpressionStatement makeAssignment(JCTree.JCExpression lhs, JCTree.JCExpression rhs) {
        return treeMaker.Exec(
                treeMaker.Assign(
                        lhs,
                        rhs
                )
        );
    }

    /**
     * 导入方法依赖的package包
     * @param packageName
     * @param className
     * @return
     */
    public JCTree.JCImport buildImport(String packageName, String className) {
        JCTree.JCIdent ident = treeMaker.Ident(names.fromString(packageName));
        JCTree.JCImport jcImport = treeMaker.Import(treeMaker.Select(
                ident, names.fromString(className)), false);
         meessager.printMessage(Diagnostic.Kind.NOTE,jcImport.toString());
        return jcImport;
    }

    /**
     * 导入方法依赖的package包
     * @param element  class
     * @param packageName
     * @param className
     * @return
     */
    public void addImportInfo(TypeElement element, String packageName, String className) {
        TreePath treePath = getTrees().getPath(element);
        Tree leaf = treePath.getLeaf();
        if (treePath.getCompilationUnit() instanceof JCTree.JCCompilationUnit && leaf instanceof JCTree) {
            JCTree.JCCompilationUnit jccu = (JCTree.JCCompilationUnit) treePath.getCompilationUnit();

            for (JCTree jcTree : jccu.getImports()) {
                if (jcTree != null && jcTree instanceof JCTree.JCImport) {
                    JCTree.JCImport jcImport = (JCTree.JCImport) jcTree;
                    if (jcImport.qualid != null && jcImport.qualid instanceof JCTree.JCFieldAccess) {
                        JCTree.JCFieldAccess jcFieldAccess = (JCTree.JCFieldAccess) jcImport.qualid;
                        try {
                            if (packageName.equals(jcFieldAccess.selected.toString()) && className.equals(jcFieldAccess.name.toString())) {
                                return;
                            }
                        } catch (NullPointerException e) {
                            e.printStackTrace();
                        }
                    }
                }
            }
            java.util.List<JCTree> trees = new ArrayList<>();
            trees.addAll(jccu.defs);
            JCTree.JCImport jcImport = buildImport(packageName,className);
            if (!trees.contains(jcImport)) {
                trees.add(0, jcImport);
            }
            jccu.defs = List.from(trees);
        }
    }


    public JavacTrees getTrees() {
        return trees;
    }

    public void setTrees(JavacTrees trees) {
        this.trees = trees;
    }
}

4、测试

编写测试类

public class HelloService {

    @CostTimeRecoder
    public String sayHello(String username){
        try {
            TimeUnit.SECONDS.sleep(3);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        return "hello : " + username;
    }
}

测试主类

public class AptAstMainTest {

    public static void main(String[] args) {


        System.out.println(new HelloService().sayHello("zhangsan"));


    }
}

运行查看控制台

会发现多了耗时,以及日志打印。我们查看HelloService .class文件,会发现多了如下内容

public class HelloService {
    public HelloService() {
    }

    public String sayHello(String username) {
        Long startTime = System.currentTimeMillis();

        try {
            TimeUnit.SECONDS.sleep(3L);
        } catch (InterruptedException var7) {
            var7.printStackTrace();
        }

        Long endTime = System.currentTimeMillis();
        Long costTime = endTime - startTime;
        String msg = String.format("costTime = %s(ms)", costTime);
        System.out.println(msg);
        List args = new ArrayList();
        args.add(username);
        this.saveLog(costTime, args);
        return "hello : " + username;
    }

    private void saveLog(Long costTime, List args) {
        LogDTO logDTO = new LogDTO();
        logDTO.setMethodName("sayHello");
        logDTO.setClassName("com.github.lybgeek.test.service.HelloService");
        logDTO.setCostTime(costTime);
        logDTO.setArgs(args);
        LogService logService = LogFactory.getLogger();
        logService.save(logDTO);
    }
}

总结

本文主要重点介绍AST的用法,对AOP的实现基本上是一笔带过。原因主要是平时除非是对性能有特别要求,我们实现AOP通常会在运行期实现,而非在编译期实现。其次AST比较偏底层,如果出问题,排查难度会比较高。当然如果团队有对AST很熟悉的话,能兼顾性能是最好的。

demo链接

https://github.com/lyb-geek/springboot-learning/tree/master/springboot-apt-ast

参考链接

https://my.oschina.net/u/4030990/blog/3211858
https://blog.csdn.net/a_zhenzhen/article/details/86065063
https://www.jianshu.com/p/ff8ec920f5b9

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

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

openEuler-linux下部署zabbix-超级详细

一、准备工作 下载&#xff1a;zabbix包 地址&#xff1a;下载Zabbix 准备2台openEuler-linux虚拟机&#xff1a; linux-1&#xff1a;当服务器端 IP地址&#xff1a;192.168.100.100 修改hosts文件 [rootzbx ~]# vim /etc/hosts 192.168.100.100 zbx.xx.cn linux-2&…

[Java]JavaWeb开发中的MVC设计模式

一、有关Java Web与MVC设计模式 学习过基本Java Web开发的人都已经了解了如何编写基本的Servlet&#xff0c;如何编写jsp及如何更新浏览器中显示的内容。但是我们之前自己编写的应用一般存在无条理性&#xff0c;对于一个小型的网站这样的编写没有任何问题&#xff0c;但是一但…

ETL工具-pentaho企业实战部署

&#x1f4e2;&#x1f4e2;&#x1f4e2;&#x1f4e3;&#x1f4e3;&#x1f4e3; 哈喽&#xff01;大家好&#xff0c;我是【IT邦德】&#xff0c;江湖人称jeames007&#xff0c;10余年DBA及大数据工作经验 一位上进心十足的【大数据领域博主】&#xff01;&#x1f61c;&am…

TinyOS 配置教程

系列文章目录 TinyOS 系列文章【一】&#xff1a;TinyOS 配置教程 TinyOS 系列文章【二】&#xff1a;Tossim 教程 文章目录 系列文章目录前言1. 安装1.1. 实验环境1.2. TinyOS基础工作1.3. TinyOS 的配置1.4. 安装 java1.5. 安装编译器 2. 测试仿真程序总结 前言 本文主要用…

kafka集群搭建

1.本次搭建涉及3台centos7主机&#xff0c;防火墙与selinux服务均关闭 2.主机参数如下表所示 nameIPportserviceA10.1.60.1122128、2888、3888、9092kafka、zookeeperB10.1.60.1142128、2888、3888、9092kafka、zookeeperC10.1.60.1152128、2888、3888、9092kafka、zookeeper…

让人悲观的国内ChatGPT的未来

最近关于ChatGPT的火爆已经不是简单的AI圈的事了&#xff0c;它已经席卷了所有的IT、媒体圈子&#xff0c;甚至是不同领域不同行业的人&#xff0c;只要你还对于变化与AI有一定的了解&#xff0c;那我相信你一定能知道ChatGPT是什么了。ChatGPT在某种程度上已经是相当于AGI通用…

图论-匈牙利算法学习

本文讲述的是匈牙利算法&#xff0c;即图论中寻找最大匹配的算法。解决的问题是从二分图中找到尽量多的匹配。 原题-华为-HJ28 素数伴侣 描述 题目描述 若两个正整数的和为素数&#xff0c;则这两个正整数称之为“素数伴侣”&#xff0c;如2和5、6和13&#xff0c;它们能应用…

【已解决】SpringBoot 工程 war包服务部署与调用测试

1.开发环境&#xff1a;IDEA&#xff0c;JDK1.8 2.服务打包类型&#xff1a; war包 3.war包部署环境&#xff1a;Linux系统&#xff0c;tomcat服务器&#xff0c;端口号&#xff1a;8081 4.war包部署位置&#xff1a;tomcat-8081/webapps/temp.war 5.服务名为&#xff1a;t…

瑞吉外卖项目——瑞吉外卖

软件开发整体介绍 软件开发流程 需求分析&#xff1a;产品原型、需求规格说明书 设计&#xff1a;产品文档、UI界面设计、概要设计、详细设计、数据库设计 编码&#xff1a;项目代码、单元测试 测试&#xff1a;测试用例、测试报告 上线运维&#xff1a;软件环境安装、配置…

python-day6(补充四:私有属性与函数)

私有属性与函数 私有属性与函数的用途如何定义私有属性与函数如何访问私有属性与函数 私有属性与函数的用途 在面向对象的封装中&#xff0c;私有的属性与函数其根本目的是防止它们在类的外部被使用&#xff0c;python中主要通过命名来进行区分。 把可能使用到的东西封装起来…

从零基础到条码高手:傻瓜式操作,告别excel、AI和PS的烦恼

条形码是一种用于商品识别、库存管理等方面的编码标识系统&#xff0c;它是通过将数字和字符以特定的图案排列组合起来&#xff0c;从而形成一组能被机器扫描和识别的条纹图案。 通常情况下&#xff0c;条形码的生成可以分为如下几个步骤&#xff1a; 1、编号&#xff1a;首先…

数据可视化工具汇总:数字孪生产品的得力助手

数字孪生技术是一项快速发展的新兴技术&#xff0c;已经在许多领域得到广泛应用。数字孪生技术不仅可以提供完整的虚拟模型&#xff0c;还可以模拟物理系统的行为。在数字孪生技术的推动下&#xff0c;越来越多的数字孪生产品开始涌现出来&#xff0c;为不同的领域提供支持和解…

如何通过FAQ页面减轻客户支持压力,提高工作效率?

作为现代企业不可或缺的一部分&#xff0c;客户支持服务是为客户提供解决方案、回答问题和解决技术难题的关键部分。无论是产品管理还是销售环节&#xff0c;客户支持都是重要的一环。然而&#xff0c;有效地处理技术支持问题和客户请求并不容易。卓越的客户支持需要组织结构&a…

excle表格打印相关问题

ps&#xff1a;无论是打印word,还是打印excel, 最后最好都保存成pdf&#xff0c;再打印。 ps&#xff1a;无论是打印word,还是打印excel, 最后最好都保存成pdf&#xff0c;再打印。 ps&#xff1a;无论是打印word,还是打印excel, 最后最好都保存成pdf&#xff0c;再打印。 …

ThreadLocal InheritableThreadLocal TransmittableThreadLocal的使用以及原理

ThreadLocal 每个线程向ThreadLocal设置值&#xff0c;再取值&#xff0c;实现线程之间的隔离 public class ThreadLocalCase1 {private static ThreadLocal<Integer> threadLocal new ThreadLocal<>();public static void main(String[] args) {Random random …

浅析提高倾斜摄影超大场景的三维模型轻量化的数据质量关键技术

浅析提高倾斜摄影超大场景的三维模型轻量化的数据质量关键技术 倾斜摄影超大场景的三维模型轻量化的质量关键技术主要包括&#xff1a; 1、保持数据精度。在进行轻量化处理时&#xff0c;必须确保数据的精度不受损失&#xff0c;否则会影响后续分析和应用方案。因此&#xff0…

【Leetcode -剑指Offer 22.链表中倒数第k个结点 -203.移除链表元素】

Leetcode Leetcode -剑指Offer 22.链表中倒数第k个结点Leetcode -203.移除链表元素 Leetcode -剑指Offer 22.链表中倒数第k个结点 题目&#xff1a;输入一个链表&#xff0c;输出该链表中倒数第k个节点。为了符合大多数人的习惯&#xff0c;本题从1开始计数&#xff0c;即链表…

DAY829

学习目标&#xff1a;成就上瘾&#xff0c;学到欲罢不能 4月&#xff08;复习完高数18讲内容&#xff0c;背诵21篇短文&#xff0c;熟词僻义300词基础词&#xff09; 学习内容&#xff1a; 暴力英语&#xff1a;背单词150个&#xff0c;背《死亡诗社》经典语段&#xff0c;抄写…

【Spring Cloud】Spring Cloud 是什么?

文章目录 前言一、子项目二、常用组件三、把 Spring Cloud 官方、Netflix、Alibaba 三者整理成如下表格&#xff1a; 前言 Spring 以 Bean&#xff08;对象&#xff09; 为中心&#xff0c;提供 IOC、AOP 等功能。Spring Boot 以 Application&#xff08;应用&#xff09; 为中…

LightGBM面试题

1.偏差 vs 方差? 偏差是指由有所采样得到的大小为m的训练数据集&#xff0c;训练出的所有模型的输出的平均值和真实模型输出之间的偏差。 通常是由对学习算法做了错误的假设导致的描述模型输出结果的期望与样本真实结果的差距。分类器表达能力有限导致的系统性错误&#xff0c…
最新文章