Si usted acaba de escribir un mantenimiento, este conjunto de tecnologías que nunca tocó,

I. Introducción

Escribir este artículo cuando pienso en la mayoría de los programadores podrían incluir usted y yo, a menudo están ocupados con el desarrollo de negocios o correr en el camino de mantenimiento rutinario y reparación ERROR, cuando la tecnología no se puede sacar nutrientes y cambiar el status quo, como una constante de la máquina en funcionamiento, una definición de la velocidad de círculo de escape en el universo. Usted también puede tener sus propias dificultades, horas extras y no tiene tiempo para aprender demasiado tarde, el fin de semana no es demasiada energía en las tareas de la casa, no hay un plan de vacaciones sin horario es demasiado lleno. En resumen, se procederá al archivo del estudio. Y cuando en el año pasado, cuando su edad y capacidad no es un partido y luego se lamentan de no poner en más tiempo para crecer y aprender.

Especialmente primera línea de codificación de los técnicos, además de lo que podemos ver en el marco de tecnología (SSM) desarrollado por código de negocio, se ha encontrado con un cuello de botella de aprendizaje, y este cuello de botella es que no se sabe lo que no lo hará al igual que estas tecnologías por debajo de la lista, usted tiene que entender lo mucho;

El javaagent 1.
2. ASM
3. JVMTI
4. javaassit
5. El Netty
6. El algoritmo, el motor de búsqueda
7. El CGLIB
8. El caos Ingeniería
9. Desarrollo de middleware
10. Prueba avanzada; prueba de esfuerzo, prueba de enlace, el flujo de reproducción, teñido fluir
11. serie de fallo; incursión reproducidos, perforar
12. la consistencia de los datos distribuidos
13. la operación de archivo; ES, colmena
14. un centro de registro; ZOOKEEPER, el Eureka
15. una pila tecnología de ingeniería de Internet; primavera, mybaits, puerta de entrada, RPC (Thrift, GRPC, dubbo), mq, caché redis, sub-biblioteca sub-tabla, las tareas programadas, transacción distribuida, de limitación de corriente, fusible, descenso
16. base de datos analítica binlog 
17. El diseño de la arquitectura; DDD diseño de la unidad de campo, micro-servicios, servicio de tratamiento
18. buque; K8S, acoplable
19. Un almacenamiento distribuido; Ceph
20. Un servicio istio
21. Un jmter de detección de presión
22. A ansible código java proyecto de implementación Jenkins-+
23. Un seguimiento completo enlace, pista distribuido
24. El reconocimiento del habla, el habla síntesis
26. lvs nginx haproxy iptables
27. hadoop mapreduce colmena Sqoop hbase flink kylin druida

  1. agentes de clase, tales como cglib
  2. Proyecto caos
  3. La ingeniería inversa
  4. Javaagent realiza en conjunto con la monitorización no invasiva, el método consume mucho tiempo, registros, rendimiento de la máquina, etc.
  5. grieta

ASM es un marco de la manipulación de código de bytes de Java. Se puede utilizar para generar dinámicamente la clase o clases de mejoras existentes. ASM puede tener un archivos directos de clase binaria también se pueden cargar antes de que el comportamiento de la clase de cambio de Java Virtual Machine de forma dinámica en la clase. clase Java es estrictamente en el formato definido en el archivo .class, archivos de clase tienen repositorio de metadatos suficiente para resolver todos los elementos de la clase: nombres de las clases, métodos, propiedades y el código de bytes de Java (instrucción). Después de la información de clase desde el archivo ASM se lee, es posible alterar el análisis del comportamiento de información de clase, una nueva clase se puede generar incluso de acuerdo con las necesidades del usuario.

Para que sea más fácil de aprender ASM, voy a "ASM4 Manual", así como algunos puntos técnicos organizados en un documento, para facilitar el acceso en cualquier momento (http://asm.itstack.org);

También en los ejemplos de código de este artículo que aparece, puede responder en un número público (pila agujero de gusano bugstack), para obtener el código fuente de descarga.

En segundo lugar, la configuración del entorno

  1. JDK 1.8
  2. idea 03/01/2019
  3. asm-commons 6.2.1

En tercer lugar, la información del proyecto

  • itstack-demo-ASM-01: Programa de código de bytes, HelloWorld
  • itstack-demo-ASM-02: Programa de código de bytes, y los dos números
  • parámetros de código de bytes, de entrada y salida: itstack-demo-ASM-03
  • itstack-demo-ASM-04: código de bytes, llame a un método externo

Cuatro, HelloWorld También puede escribir

Usted está familiarizado HelloWorld no es así;

public class HelloWorld {
    public static void main(String[] var0) {
        System.out.println("Hello World");
    }
}

Entonces usted tiene que tratar las instrucciones de montaje en virtud de vista anti-analítica de su clase en la que, javap -c el HelloWorld

public class org.itstack.demo.test.HelloWorld {
  public org.itstack.demo.test.HelloWorld();
    Code:
       0: aload_0
       1: invokespecial #1                  // Method java/lang/Object."<init>":()V
       4: return

  public static void main(java.lang.String[]);
    Code:
       0: getstatic     #2                  // Field java/lang/System.out:Ljava/io/PrintStream;
       3: ldc           #3                  // String Hello World
       5: invokevirtual #4                  // Method java/io/PrintStream.println:(Ljava/lang/String;)V
       8: return
}
instrucción descripción
getstatic Obtiene el valor del campo estático
LDC valor constante en la piscina constante de la pila
invokevirtual El método de unión a llamar a un método de funcionamiento
regreso     devuelve la función void

Si está interesado en otras instrucciones, pueden referirse a esta lista de instrucciones de código de bytes: Go!

Bueno! Por encima de él, es que soy muy familiar pieza de código, por lo que ahora tenemos este código escrito en modo ASM;

import org.objectweb.asm.ClassWriter;
import org.objectweb.asm.MethodVisitor;
import org.objectweb.asm.Opcodes;

private static byte[] generate() {
    ClassWriter classWriter = new ClassWriter(0);
    // 定义对象头;版本号、修饰符、全类名、签名、父类、实现的接口
    classWriter.visit(Opcodes.V1_7, Opcodes.ACC_PUBLIC, "org/itstack/demo/asm/AsmHelloWorld", null, "java/lang/Object", null);
    // 添加方法;修饰符、方法名、描述符、签名、异常
    MethodVisitor methodVisitor = classWriter.visitMethod(Opcodes.ACC_PUBLIC + Opcodes.ACC_STATIC, "main", "([Ljava/lang/String;)V", null, null);
    // 执行指令;获取静态属性
    methodVisitor.visitFieldInsn(Opcodes.GETSTATIC, "java/lang/System", "out", "Ljava/io/PrintStream;");
    // 加载常量 load constant
    methodVisitor.visitLdcInsn("Hello World");
    // 调用方法
    methodVisitor.visitMethodInsn(Opcodes.INVOKEVIRTUAL, "java/io/PrintStream", "println", "(Ljava/lang/String;)V", false);
    // 返回
    methodVisitor.visitInsn(Opcodes.RETURN);
    // 设置操作数栈的深度和局部变量的大小
    methodVisitor.visitMaxs(2, 1);
    // 方法结束
    methodVisitor.visitEnd();
    // 类完成
    classWriter.visitEnd();
    // 生成字节数组
    return classWriter.toByteArray();
}

El código anterior, "Mire, ¿tiene mucho que decir hola ??? ^ 1024", de hecho, el código es el código del marco de ASM, que todas las operaciones dentro y usar nuestro uso javap XXX -c de la anti-analizada código de bytes es el mismo, sólo a su vez utiliza la instrucción de escribir código.

  1. Un generador de definición de clase ClassWriter
  2. Estableciendo versión, modificadores, el nombre completo de la clase, la firma, la clase padre, interfaces implementadas, de hecho, es la frase; pública la clase HelloWorld
  3. El método de crear el siguiente arranque, el método también requiere ajuste; modificador, los nombres de métodos, y otros descriptores. Y hay varios identificación fijo;

descriptor de tipos

| El tipo Java | tipo de descriptor |
|: - |: - |
| booleana | Z |
| Char | C |
| byte | B |
| Corto | S |
| int | I |
| un flotador | F |
| larga | J |
| doble | D |
| objeto | Ljava / lang / Object; |
| int [] | [I |
| Object [] [] | [[Ljava / lang / Object; |

método descriptor

| Declaración de método en el archivo de fuente | descriptor método |
|: - |: - |
| void m (int I, un flotador F) | (el IF) V |
| int m (Object O) | (Ljava / lang / Object; ) el I |
| int [] m (I int, String S) | (ILjava / lang / cadena;) [la I |
| Object m (int [] I) | ([el I) Ljava / lang / Object; |

([Ljava / lang / cadena;) V == void main (String [] args)

  1. Ejecutar instrucciones; obtener las propiedades estáticas. El principal es obtener System.out
  2. Carga constante carga constante, la salida de nuestra HelloWorld methodVisitor.visitLdcInsn ( "Hola Mundo");
  3. Finalmente, el conjunto de llamadas y método de salida vacías rendimientos, mientras que al final de la profundidad para establecer el tamaño de las variables locales y operando pila

Tal salida HelloWorld todavía no es muy interesante, aunque se puede pensar que esto es demasiado difícil, hasta la codificación, pero también muy difícil de entender. En primer lugar, si usted lee mi columna, la "máquina virtual de Java para escribir una JVM", entonces usted puede sentir de que hay conocimiento o no tan extraña. También se escribe aquí, ASM también ofrece plug-ins, se puede hacer fácilmente a desarrollar el código de bytes. Luego nos dicen sobre el uso.

En quinto lugar, para ayudar a desarrollar código de plug-byte no es difícil

Para los recién llegados si el código de bytes algo realmente difícil de desarrollar, bloque de especial complejidad de operación de la instrucción de código bytecode es muy difícil. Entonces, también hay una forma sencilla es utilizar el plugin ASM. Este plugin permite fácilmente que usted pueda ver la secuencia de comandos y cómo usar una pieza de código para desarrollar ASM.

Tapón (ASM Bytecode Esquema)

prueba de uso

¿No ver con la ayuda de plug-ins, y mi corazón se ha excitado, así que las cosas tienen que escribir al menos el punto de partida. Por lo que puede mejorar fácilmente el código de bytes para operar algunas de las funciones.

Seis, y el número de escribir un código de cálculo de dos bytes

Bueno! Con los anteriores plug-ins, pero también tenemos una cierta comprensión de los conceptos básicos. Por lo tanto, hemos desarrollado un método para calcular la suma de los dos números y, a continuación, ejecute los resultados.

Es nuestra meta

public class SumOfTwoNumbers {

    public int sum(int i, int m) {
        return i + m;
    }

}

Programación implementada utilizando el código de bytes

import org.objectweb.asm.ClassWriter;
import org.objectweb.asm.MethodVisitor;
import org.objectweb.asm.Opcodes;

private static byte[] generate() {
    ClassWriter classWriter = new ClassWriter(0);
    {
        MethodVisitor methodVisitor = classWriter.visitMethod(Opcodes.ACC_PUBLIC, "<init>", "()V", null, null);
        methodVisitor.visitVarInsn(Opcodes.ALOAD, 0);
        methodVisitor.visitMethodInsn(Opcodes.INVOKESPECIAL, "java/lang/Object", "<init>", "()V", false);
        methodVisitor.visitInsn(Opcodes.RETURN);
        methodVisitor.visitMaxs(1, 1);
        methodVisitor.visitEnd();
    }
    {
        // 定义对象头;版本号、修饰符、全类名、签名、父类、实现的接口
        classWriter.visit(Opcodes.V1_7, Opcodes.ACC_PUBLIC, "org/itstack/demo/asm/AsmSumOfTwoNumbers", null, "java/lang/Object", null);
        // 添加方法;修饰符、方法名、描述符、签名、异常
        MethodVisitor methodVisitor = classWriter.visitMethod(Opcodes.ACC_PUBLIC, "sum", "(II)I", null, null);
        methodVisitor.visitVarInsn(Opcodes.ILOAD, 1);
        methodVisitor.visitVarInsn(Opcodes.ILOAD, 2);
        methodVisitor.visitInsn(Opcodes.IADD);
        // 返回
        methodVisitor.visitInsn(Opcodes.IRETURN);
        // 设置操作数栈的深度和局部变量的大小
        methodVisitor.visitMaxs(2, 3);
        methodVisitor.visitEnd();
    }
    // 类完成
    classWriter.visitEnd();
    // 生成字节数组
    return classWriter.toByteArray();
}

Hay dos soportes superiores {}, que se utiliza para generar un primer constructor nula

public AsmSumOfTwoNumbers() {
}

La siguiente instrucción es relativamente simple, en primer lugar utilizando dos empuje iLoad numérica operando pila se va a poner la operación, la siguiente ejecución de inicio IADD, la adición de dos números.

Finalmente devolver resultados IRETURN, tenga en cuenta el tipo regresé. Este método se da cuenta rápidamente que esto sea completo. Decompile sigue;

//
// Source code recreated from a .class file by IntelliJ IDEA
// (powered by Fernflower decompiler)
//

package org.itstack.demo.asm;

public class AsmSumOfTwoNumbers {
    public AsmSumOfTwoNumbers() {
    }

    public int doSum(int var1, int var2) {
        return var1 + var2;
    }
}

Bloque de código

public static void main(String[] args) throws Exception {
    // 生成二进制字节码
    byte[] bytes = generate();
    // 输出字节码
    outputClazz(bytes);
    // 加载AsmSumOfTwoNumbers
    GenerateSumOfTwoNumbers generateSumOfTwoNumbers = new GenerateSumOfTwoNumbers();
    Class<?> clazz = generateSumOfTwoNumbers.defineClass("org.itstack.demo.asm.AsmSumOfTwoNumbers", bytes, 0, bytes.length);
    // 反射获取 main 方法
    Method method = clazz.getMethod("sum", int.class, int.class);
    Object obj = method.invoke(clazz.newInstance(), 6, 2);
    System.out.println(obj);
}

La implementación de esta operación y usamos la misma operación Java reflexión, es relativamente fácil. En este momento estamos llamando a un nuevo código de bytes de clases, y también nos ayudará a ver la clase clase de salida de código de bytes generada.

Seven, en el método consume mucho tiempo de monitoreo original de mejora bytecode

Entendemos que el programa básico de código de bytes puede generar dinámicamente una clase. Sin embargo, en el uso real, que puede ser a veces la necesidad de modificar el método existente, añadir un poco de código al principio y al final, requiere mucho tiempo para supervisar este método. Este es el modelo más básico de la monitorización no invasiva.

Definir un método

public class MyMethod {

    public String queryUserInfo(String uid) {
        System.out.println("xxxx");
        System.out.println("xxxx");
        System.out.println("xxxx");
        System.out.println("xxxx");
        return uid;
    }

}

Al igual que el método de inserción del monitor

public class TestMonitor extends ClassLoader {

    public static void main(String[] args) throws IOException, NoSuchMethodException, IllegalAccessException, InstantiationException, InvocationTargetException {

        ClassReader cr = new ClassReader(MyMethod.class.getName());
        ClassWriter cw = new ClassWriter(cr, ClassWriter.COMPUTE_MAXS);

        {
            MethodVisitor methodVisitor = cw.visitMethod(Opcodes.ACC_PUBLIC, "<init>", "()V", null, null);
            methodVisitor.visitVarInsn(Opcodes.ALOAD, 0);
            methodVisitor.visitMethodInsn(Opcodes.INVOKESPECIAL, "java/lang/Object", "<init>", "()V", false);
            methodVisitor.visitInsn(Opcodes.RETURN);
            methodVisitor.visitMaxs(1, 1);
            methodVisitor.visitEnd();
        }

        ClassVisitor cv = new ProfilingClassAdapter(cw, MyMethod.class.getSimpleName());
        cr.accept(cv, ClassReader.EXPAND_FRAMES);

        byte[] bytes = cw.toByteArray();
        outputClazz(bytes);

        Class<?> clazz = new TestMonitor().defineClass("org.itstack.demo.asm.MyMethod", bytes, 0, bytes.length);
        Method queryUserInfo = clazz.getMethod("queryUserInfo", String.class);
        Object obj = queryUserInfo.invoke(clazz.newInstance(), "10001");
        System.out.println("测试结果:" + obj);

    }

    static class ProfilingClassAdapter extends ClassVisitor {

        public ProfilingClassAdapter(final ClassVisitor cv, String innerClassName) {
            super(ASM5, cv);
        }

        public MethodVisitor visitMethod(int access,
                                         String name,
                                         String desc,
                                         String signature,
                                         String[] exceptions) {
            System.out.println("access:" + access);
            System.out.println("name:" + name);
            System.out.println("desc:" + desc);

            if (!"queryUserInfo".equals(name)) return null;

            MethodVisitor mv = cv.visitMethod(access, name, desc, signature, exceptions);
            
            return new ProfilingMethodVisitor(mv, access, name, desc);
        }

    }

    static class ProfilingMethodVisitor extends AdviceAdapter {

        private String methodName = "";

        protected ProfilingMethodVisitor(MethodVisitor methodVisitor, int access, String name, String descriptor) {
            super(ASM5, methodVisitor, access, name, descriptor);
            this.methodName = name;
        }

        @Override
        protected void onMethodEnter() {
            mv.visitMethodInsn(INVOKESTATIC, "java/lang/System", "nanoTime", "()J", false);
            mv.visitVarInsn(LSTORE, 2);
            mv.visitVarInsn(ALOAD, 1);
        }

        @Override
        protected void onMethodExit(int opcode) {
            if ((IRETURN <= opcode && opcode <= RETURN) || opcode == ATHROW) {
                mv.visitFieldInsn(GETSTATIC, "java/lang/System", "out", "Ljava/io/PrintStream;");

                mv.visitTypeInsn(NEW, "java/lang/StringBuilder");
                mv.visitInsn(DUP);
                mv.visitMethodInsn(INVOKESPECIAL, "java/lang/StringBuilder", "<init>", "()V", false);
                mv.visitLdcInsn("方法执行耗时(纳秒)->" + methodName+":");
                mv.visitMethodInsn(INVOKEVIRTUAL, "java/lang/StringBuilder", "append", "(Ljava/lang/String;)Ljava/lang/StringBuilder;", false);

                mv.visitMethodInsn(INVOKESTATIC, "java/lang/System", "nanoTime", "()J", false);
                mv.visitVarInsn(LLOAD, 2);
                mv.visitInsn(LSUB);

                mv.visitMethodInsn(INVOKEVIRTUAL, "java/lang/StringBuilder", "append", "(J)Ljava/lang/StringBuilder;", false);
                mv.visitMethodInsn(INVOKEVIRTUAL, "java/lang/StringBuilder", "toString", "()Ljava/lang/String;", false);
                mv.visitMethodInsn(INVOKEVIRTUAL, "java/io/PrintStream", "println", "(Ljava/lang/String;)V", false);

            }
        }
    }

}

El bloque de código poco de todo grande, podemos mirar en bloques, de la siguiente manera;

  1. ClassReader cr = new ClassReader (MyMethod.class.getName ()); leer la clase original, se ha mejorado el código de bytes de inicio
  2. ClassVisitor cv = new ProfilingClassAdapter (cw, MyMethod.class.getSimpleName ()); comenzó a aumentar bytecode
  3. onMethodEnter, onMethodExit, código de complemento para llevar a cabo el método consume mucho tiempo en el momento de los métodos de entrada y salida.

Resultados del ensayo:

Ejecutar directamente TestMonitor.java;

access:1
name:<init>
desc:()V
access:1
name:queryUserInfo
desc:(Ljava/lang/String;)Ljava/lang/String;
ASM类输出路径:/E:/itstack/git/github.com/itstack-demo-asm/itstack-demo-asm-03/target/classes/AsmTestMonitor.class
xxxx
xxxx
xxxx
xxxx
方法执行耗时(纳秒)->queryUserInfo:132300
测试结果:10001

VIII bytecode controlar los parámetros de impresión en el

Así, además de supervisar la aplicación del método consume mucho tiempo, también puede acercarse a la información de referencia se imprime. Esto puede, en algunas circunstancias inusuales, para ver la información del registro.

Otro código es el mismo que el anterior, sólo uno cambian de lugar enumeran aquí

static class ProfilingMethodVisitor extends AdviceAdapter {
    private String methodName = "";
    protected ProfilingMethodVisitor(MethodVisitor methodVisitor, int access, String name, String descriptor) {
        super(ASM5, methodVisitor, access, name, descriptor);
        this.methodName = name;
    }
    @Override
    protected void onMethodEnter() {
        mv.visitFieldInsn(GETSTATIC, "java/lang/System", "out", "Ljava/io/PrintStream;");
        mv.visitVarInsn(ALOAD, 1);
        mv.visitMethodInsn(INVOKEVIRTUAL, "java/io/PrintStream", "println", "(Ljava/lang/String;)V", false);
    }
    @Override
    protected void onMethodExit(int opcode) {
    }
}

Como puede verse aquí, en el proceso de introducir el tiempo de uso de scripts getstatic, obtener una clases de objetos de salida
utilizados junto aload, el tipo de valor de referencia de carga de la pila de las variables locales en una
salida final de la información de referencia

Resultados del ensayo:

Ejecutar directamente TestMonitor.java;

Class<?> clazz = new TestMonitor().defineClass("org.itstack.demo.asm.MyMethod", bytes, 0, bytes.length);
 Method queryUserInfo = clazz.getMethod("queryUserInfo", String.class);
 Object obj = queryUserInfo.invoke(clazz.newInstance(), "10001");
 System.out.println("测试结果:" + obj);

resultados;

access:1
name:<init>
desc:()V
access:1
name:queryUserInfo
desc:(Ljava/lang/String;)Ljava/lang/String;
ASM类输出路径:/E:/itstack/git/github.com/itstack-demo-asm/itstack-demo-asm-04/target/classes/AsmTestMonitor.class
10001

...

10001 es nuestro enfoque para el Senado

Nueve, con el método de refuerzo externo llama al código de bytes

Bueno! A continuación, para ejecutarlo, podemos pensar en si tan sólo parte de la información se imprimirá en la consola todavía no hay manera de hacer negocios, que necesitamos en este momento de llamar a varios fuera de la información de atributos de la clase, se envía al servidor. Tal uso; mq, logs.

Las definiciones de clases de registro de salida

public class MonitorLog {

    public static void info(String name, int... parameters) {
        System.out.println("方法:" + name);
        System.out.println("参数:" + "[" + parameters[0] + "," + parameters[1] + "]");
    }

}

Después de la clase principal de código de bytes analógica, algunos de la invocación del método de emisión de información

bytecode mejorada

static class ProfilingMethodVisitor extends AdviceAdapter {
    private String name;
    
    ...
     
    @Override
    protected void onMethodEnter() {
        // 输出方法和参数
        mv.visitLdcInsn(name);
        mv.visitInsn(ICONST_2);
        mv.visitIntInsn(NEWARRAY, T_INT);
        mv.visitInsn(DUP);
        mv.visitInsn(ICONST_0);
        mv.visitVarInsn(ILOAD, 1);
        mv.visitInsn(IASTORE);
        mv.visitInsn(DUP);
        mv.visitInsn(ICONST_1);
        mv.visitVarInsn(ILOAD, 2);
        mv.visitInsn(IASTORE);
        mv.visitMethodInsn(INVOKESTATIC, "org/itstack/demo/asm/MonitorLog", "info", "(Ljava/lang/String;[I)V", false);
    }
}

Aquí bytecode parte la manipulación, de hecho, después de que el efecto de reforzar la final sigue;

public int sum(int i, int m) {
   Monitor.info("sum", i, m);
   return i + m;
}

Resultados del ensayo:

access:1
name:sum
desc:(II)I
signature:null
ASM类输出路径:/E:/itstack/git/github.com/itstack-demo-asm/itstack-demo-asm-05/target/classes/AsmTestMonitor.class
方法:sum
参数:[6,2]
结果:8

Puede ser visto a través del contenido de la prueba, hemos presentado un nombre de parámetro de método e imprimir la información completa. Bueno! Con esto ya tenemos un programa básico ASM código de bytes puerta de entrada

Resumen X.

Contenido de Tecnología Avanzada para más que eso, no sólo para las funciones momento de lograr, y renunciar a la oportunidad de llegar al fondo de la excavación. Tal vez a mejorar constantemente y ampliar sus conocimientos y habilidades, le permitirá más distintivo.
ASM código de bytes programar esta aplicación es muy amplio, pero puede de hecho por lo general no se ve, porque es juntos como un servicio de apoyo en combinación con otros marcos. Tecnología como esta hay muchos, como javaassit, Netty y así sucesivamente.
Por el momento realmente desea aprender la misma tecnología, no sólo tiene que buscar un texto fresco, texto fresco, pero también hizo darle un escalón. Cuando se desea un profundo conocimiento del conocimiento, lo más importante es aprender el sistema! Presione su propio tiempo, de hacer algo significativo, es 3--7 años los desarrolladores lo más correcto!
----------------
responsabilidad: Este artículo es RDCC blogger artículo original "hermano pequeño Fu" y seguimiento 4.0 CC BY-SA acuerdo de derecho de autor, que se reproduce, por favor incluya la fuente original y el enlace esta declaración.
fuente original: https: //blog.csdn.net/generalfu/article/details/105110600

Publicado 50 artículos originales · ganado elogios 1706 · Vistas 2,22 millones +

Supongo que te gusta

Origin blog.csdn.net/zl1zl2zl3/article/details/105203674
Recomendado
Clasificación