反射、注解和動(dòng)態(tài)代理

反射是指計(jì)算機(jī)程序在運(yùn)行時(shí)訪問(wèn)、檢測(cè)和修改它本身狀態(tài)或行為的一種能力,是一種元編程語(yǔ)言特性,有很多語(yǔ)言都提供了對(duì)反射機(jī)制的支持,它使程序能夠編寫程序。Java的反射機(jī)制使得Java能夠動(dòng)態(tài)的獲取類的信息和調(diào)用對(duì)象的方法。

一、Java反射機(jī)制及基本用法

在Java中,Class(類類型)是反射編程的起點(diǎn),代表運(yùn)行時(shí)類型信息(RTTI,Run-Time Type Identification)。java.lang.reflect包含了Java支持反射的主要組件,如Constructor、Method和Field等,分別表示類的構(gòu)造器、方法和域,它們的關(guān)系如下圖所示。

Java反射機(jī)制主要組件

Constructor和Method與Field的區(qū)別在于前者繼承自抽象類Executable,是可以在運(yùn)行時(shí)動(dòng)態(tài)調(diào)用的,而Field僅僅具備可訪問(wèn)的特性,且默認(rèn)為不可訪問(wèn)。下面了解下它們的基本用法:

Java反射類及核心方法
  • 獲取Class對(duì)象有三種方式,Class.forName適合于已知類的全路徑名,典型應(yīng)用如加載JDBC驅(qū)動(dòng)。對(duì)同一個(gè)類,不同方式獲得的Class對(duì)象是相同的。
// 1. 采用Class.forName獲取類的Class對(duì)象
Class clazz0 = Class.forName("com.yhthu.java.ClassTest");
System.out.println("clazz0:" + clazz0);
// 2. 采用.class方法獲取類的Class對(duì)象
Class clazz1 = ClassTest.class;
System.out.println("clazz1:" + clazz1);
// 3. 采用getClass方法獲取類的Class對(duì)象
ClassTest classTest = new ClassTest();
Class clazz2 = classTest.getClass();
System.out.println("clazz2:" + clazz2);
// 4. 判斷Class對(duì)象是否相同
System.out.println("Class對(duì)象是否相同:" + ((clazz0.equals(clazz1)) && (clazz1.equals(clazz2))));

注意:三種方式獲取的Class對(duì)象相同的前提是使用了相同的類加載器,比如上述代碼中默認(rèn)采用應(yīng)用程序類加載器(sun.misc.Launcher$AppClassLoader)。不同類加載器加載的同一個(gè)類,也會(huì)獲取不同的Class對(duì)象:

// 自定義類加載器
ClassLoader myLoader = new ClassLoader() {
    @Override
    public Class<?> loadClass(String name) throws ClassNotFoundException {
        try {
            String fileName = name.substring(name.lastIndexOf(".") + 1) + ".class";
            InputStream is = getClass().getResourceAsStream(fileName);
            if (is == null) {
                return super.loadClass(name);
            }
            byte[] b = new byte[is.available()];
            is.read(b);
            return defineClass(name, b, 0, b.length);
        } catch (IOException e) {
            throw new ClassNotFoundException(name);
        }
    }
};
// 采用自定義類加載器加載
Class clazz3 = Class.forName("com.yhthu.java.ClassTest", true, myLoader);
// clazz0與clazz3并不相同
System.out.println("Class對(duì)象是否相同:" + clazz0.equals(clazz3));
  • 通過(guò)Class的getDeclaredXxxx和getXxx方法獲取構(gòu)造器、方法和域?qū)ο?,兩者的區(qū)別在于前者返回的是當(dāng)前Class對(duì)象申明的構(gòu)造器、方法和域,包含修飾符為private的;后者只返回修飾符為public的構(gòu)造器、方法和域,但包含從基類中繼承的。
// 返回申明為public的方法,包含從基類中繼承的
for (Method method: String.class.getMethods()) {
    System.out.println(method.getName());
}
// 返回當(dāng)前類申明的所有方法,包含private的
for (Method method: String.class.getDeclaredMethods()) {
    System.out.println(method.getName());
}
  • 通過(guò)Class的newInstance方法和Constructor的newInstance方法方法均可新建類型為Class的對(duì)象,通過(guò)Method的invoke方法可以在運(yùn)行時(shí)動(dòng)態(tài)調(diào)用該方法,通過(guò)Field的set方法可以在運(yùn)行時(shí)動(dòng)態(tài)改變域的值,但需要首先設(shè)置其為可訪問(wèn)(setAccessible)。

二、 注解

注解(Annontation)是Java5引入的一種代碼輔助工具,它的核心作用是對(duì)類、方法、變量、參數(shù)和包進(jìn)行標(biāo)注,通過(guò)反射來(lái)訪問(wèn)這些標(biāo)注信息,以此在運(yùn)行時(shí)改變所注解對(duì)象的行為。Java中的注解由內(nèi)置注解和元注解組成。內(nèi)置注解主要包括:

  • @Override - 檢查該方法是否是重載方法。如果發(fā)現(xiàn)其父類,或者是引用的接口中并沒(méi)有該方法時(shí),會(huì)報(bào)編譯錯(cuò)誤。
  • @Deprecated - 標(biāo)記過(guò)時(shí)方法。如果使用該方法,會(huì)報(bào)編譯警告。
  • @SuppressWarnings - 指示編譯器去忽略注解中聲明的警告。
  • @SafeVarargs - Java 7 開始支持,忽略任何使用參數(shù)為泛型變量的方法或構(gòu)造函數(shù)調(diào)用產(chǎn)生的警告。
  • @FunctionalInterface - Java 8 開始支持,標(biāo)識(shí)一個(gè)匿名函數(shù)或函數(shù)式接口。

這里,我們重點(diǎn)關(guān)注元注解,元注解位于java.lang.annotation包中,主要用于自定義注解。元注解包括:

  • @Retention - 標(biāo)識(shí)這個(gè)注解怎么保存,是只在代碼中,還是編入class文件中,或者是在運(yùn)行時(shí)可以通過(guò)反射訪問(wèn),枚舉類型分為別SOURCE、CLASS和RUNTIME;
  • @Documented - 標(biāo)記這些注解是否包含在用戶文檔中。
  • @Target - 標(biāo)記這個(gè)注解應(yīng)該是哪種Java 成員,枚舉類型包括TYPE、FIELD、METHOD、CONSTRUCTOR等;
  • @Inherited - 標(biāo)記這個(gè)注解可以繼承超類注解,即子類Class對(duì)象可使用getAnnotations()方法獲取父類被@Inherited修飾的注解,這個(gè)注解只能用來(lái)申明類。
  • @Repeatable - Java 8 開始支持,標(biāo)識(shí)某注解可以在同一個(gè)聲明上使用多次。

自定義元注解需重點(diǎn)關(guān)注兩點(diǎn):1)注解的數(shù)據(jù)類型;2)反射獲取注解的方法。首先,注解中的方法并不支持所有的數(shù)據(jù)類型,僅支持八種基本數(shù)據(jù)類型、String、Class、enum、Annotation和它們的數(shù)組。比如以下代碼會(huì)產(chǎn)生編譯時(shí)錯(cuò)誤:

@Documented
@Inherited
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
public @interface AnnotationTest {
    // 1. 注解數(shù)據(jù)類型不能是Object;2. 默認(rèn)值不能為null
    Object value() default null;
    // 支持的定義方式
    String value() default "";
}

其次,上節(jié)中提到的反射相關(guān)類(Class、Constructor、Method和Field)和Package均實(shí)現(xiàn)了AnnotatedElement接口,該接口定義了訪問(wèn)反射信息的方法,主要如下:

// 獲取指定注解類型
getAnnotation(Class<T>):T;
// 獲取所有注解,包括從父類繼承的
getAnnotations():Annotation[];
// 獲取指定注解類型,不包括從父類繼承的
getDeclaredAnnotation(Class<T>):T
// 獲取所有注解,不包括從父類繼承的
getDeclaredAnnotations():Annotation[];
// 判斷是否存在指定注解
isAnnotationPresent(Class<? extends Annotation>:boolean

當(dāng)使用上例中的AnnotationTest 標(biāo)注某個(gè)類后,便可在運(yùn)行時(shí)通過(guò)該類的反射方法訪問(wèn)注解信息了。

@AnnotationTest("yhthu")
public class AnnotationReflection {

    public static void main(String[] args) {
        AnnotationReflection ar = new AnnotationReflection();
        Class clazz = ar.getClass();
        // 判斷是否存在指定注解
        if (clazz.isAnnotationPresent(AnnotationTest.class)) {
            // 獲取指定注解類型
            Annotation annotation = clazz.getAnnotation(AnnotationTest.class);
            // 獲取該注解的值
            System.out.println(((AnnotationTest) annotation).value());
        }
    }
}

當(dāng)自定義注解只有一個(gè)方法value()時(shí),使用注解可只寫值,例如:@AnnotationTest("yhthu")

三、動(dòng)態(tài)代理

代理是一種結(jié)構(gòu)型設(shè)計(jì)模式,當(dāng)無(wú)法或不想直接訪問(wèn)某個(gè)對(duì)象,或者訪問(wèn)某個(gè)對(duì)象比較復(fù)雜的時(shí)候,可以通過(guò)一個(gè)代理對(duì)象來(lái)間接訪問(wèn),代理對(duì)象向客戶端提供和真實(shí)對(duì)象同樣的接口功能。經(jīng)典設(shè)計(jì)模式中,代理模式有四種角色:

  • Subject抽象主題類——申明代理對(duì)象和真實(shí)對(duì)象共同的接口方法;
  • RealSubject真實(shí)主題類——實(shí)現(xiàn)了Subject接口,真實(shí)執(zhí)行業(yè)務(wù)邏輯的地方;
  • ProxySubject代理類——實(shí)現(xiàn)了Subject接口,持有對(duì)RealSubject的引用,在實(shí)現(xiàn)的接口方法中調(diào)用RealSubject中相應(yīng)的方法執(zhí)行;
  • Cliect客戶端類——使用代理對(duì)象的類。
代理模式

在實(shí)現(xiàn)上,代理模式分為靜態(tài)代理和動(dòng)態(tài)代理,靜態(tài)代理的代理類二進(jìn)制文件是在編譯時(shí)生成的,而動(dòng)態(tài)代理的代理類二進(jìn)制文件是在運(yùn)行時(shí)生成并加載到虛擬機(jī)環(huán)境的。JDK提供了對(duì)動(dòng)態(tài)代理接口的支持,開源的動(dòng)態(tài)代理庫(kù)(Cglib、Javassist和Byte Buddy)提供了對(duì)接口和類的代理支持,本節(jié)將簡(jiǎn)單比較JDK和Cglib實(shí)現(xiàn)動(dòng)態(tài)代理的異同,后續(xù)章節(jié)會(huì)對(duì)Java字節(jié)碼編程做詳細(xì)分析。

3.1 JDK動(dòng)態(tài)代理接口

JDK實(shí)現(xiàn)動(dòng)態(tài)代理是通過(guò)Proxy類的newProxyInstance方法實(shí)現(xiàn)的,該方法的三個(gè)入?yún)⒎謩e表示:

public static Object newProxyInstance(ClassLoader loader, Class<?>[] interfaces, InvocationHandler h)
  • ClassLoader loader,定義代理生成的類的加載器,可以自定義類加載器,也可以復(fù)用當(dāng)前Class的類加載器;
  • Class<?>[] interfaces,定義代理對(duì)象需要實(shí)現(xiàn)的接口;
  • InvocationHandler h,定義代理對(duì)象調(diào)用方法的處理,其invoke方法中的Object proxy表示生成的代理對(duì)象,Method表示代理方法, Object[]表示方法的參數(shù)。

通常的使用方法如下:

private Object getProxy() {
    return Proxy.newProxyInstance(JDKProxyTest.class.getClassLoader(), new Class<?>[]{Subject.class},
            new MyInvocationHandler(new RealSubject()));
}

private static class MyInvocationHandler implements InvocationHandler {
    private Object realSubject;

    public MyInvocationHandler(Object realSubject) {
        this.realSubject = realSubject;
    }

    @Override
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
        System.out.println("Some thing before method invoke");
        Object result = method.invoke(realSubject, args);
        System.out.println("Some thing after method invoke");
        return result;
    }
}

類加載器采用當(dāng)前類的加載器,默認(rèn)為應(yīng)用程序類加載器(sun.misc.Launcher$AppClassLoader);接口數(shù)組以Subject.class為例,調(diào)用方法處理類MyInvocationHandler實(shí)現(xiàn)InvocationHandler接口,并在構(gòu)造器中傳入Subject的真正的業(yè)務(wù)功能服務(wù)類RealSubject,在執(zhí)行invoke方法時(shí),可以在實(shí)際方法調(diào)用前后織入自定義的處理邏輯,這也就是AOP(面向切面編程)的原理。
關(guān)于JDK動(dòng)態(tài)代理,有兩個(gè)問(wèn)題需要清楚:

  • Proxy.newProxyInstance的代理類是如何生成的?Proxy.newProxyInstance生成代理類的核心分成兩步:
// 1. 獲取代理類的Class對(duì)象
Class<?> cl = getProxyClass0(loader, intfs);
// 2. 利用Class獲取Constructor,通過(guò)反射生成對(duì)象
cons.newInstance(new Object[]{h});

與反射獲取Class對(duì)象時(shí)搜索classpath路徑的.class文件不同的是,這里的Class對(duì)象完全是“無(wú)中生有”的。getProxyClass0根據(jù)類加載器和接口集合返回了Class對(duì)象,這里采用了緩存的處理。

// 緩存(key, sub-key) -> value,其中key為類加載器,sub-key為代理的接口,value為Class對(duì)象
private static final WeakCache<ClassLoader, Class<?>[], Class<?>>
    proxyClassCache = new WeakCache<>(new KeyFactory(), new ProxyClassFactory());
// 如果實(shí)現(xiàn)了代理接口的類已存在就返回緩存對(duì)象,否則就通過(guò)ProxyClassFactory生成
private static Class<?> getProxyClass0(ClassLoader loader, Class<?>... interfaces) {
    if (interfaces.length > 65535) {
        throw new IllegalArgumentException("interface limit exceeded");
    }
    return proxyClassCache.get(loader, interfaces);
}

如果實(shí)現(xiàn)了代理接口的類已存在就返回緩存對(duì)象,否則就通過(guò)ProxyClassFactory生成。ProxyClassFactory又是通過(guò)下面的代碼生成Class對(duì)象的。

// 生成代理類字節(jié)碼文件
byte[] proxyClassFile = ProxyGenerator.generateProxyClass(proxyName, interfaces, accessFlags);
try {
    // defineClass0為native方法,生成Class對(duì)象
    return defineClass0(loader, proxyName, proxyClassFile, 0, proxyClassFile.length);
} catch (ClassFormatError e) {
    throw new IllegalArgumentException(e.toString());
}

generateProxyClass方法是用來(lái)生成字節(jié)碼文件的,根據(jù)生成的字節(jié)碼文件,再在native層生成Class對(duì)象。

  • InvocationHandler的invoke方法是怎樣調(diào)用的?
    回答這個(gè)問(wèn)題得先看下上面生成的Class對(duì)象究竟是什么樣的,將ProxyGenerator生成的字節(jié)碼保存成文件,然后反編譯打開(IDEA直接打開),可見生成的Proxy.class主要包含equals、toString、hashCode和代理接口的request方法實(shí)現(xiàn)。
public final class $Proxy extends Proxy implements Subject {
    // m1 = Object的equals方法
    private static Method m1;
    // m2 = Object的toString方法
    private static Method m2;
    // Subject的request方法
    private static Method m3;
    // Object的hashCode方法
    private static Method m0;
 
    // 省略m1/m2/m0,此處只列出request方法實(shí)現(xiàn)
    public final void request() throws  {
        try {
            super.h.invoke(this, m3, (Object[])null);
        } catch (RuntimeException | Error var2) {
            throw var2;
        } catch (Throwable var3) {
            throw new UndeclaredThrowableException(var3);
        }
    }   
}

由于生成的代理類繼承自Proxy,super.h即是Prxoy的InvocationHandler,即代理類的request方法直接調(diào)用了InvocationHandler的實(shí)現(xiàn),這就回答了InvocationHandler的invoke方法是如何被調(diào)用的了。

3.2 Cglib動(dòng)態(tài)代理接口和類

Cglib的動(dòng)態(tài)代理是通過(guò)Enhancer類實(shí)現(xiàn)的,其create方法生成動(dòng)態(tài)代理的對(duì)象,有五個(gè)重載方法:

create():Object
create(Class, Callback):Object
create(Class, Class[], Callback):Object
create(Class, Class[], CallbackFilter, Callback):Object
create(Class[], Object):Object

常用的是第二個(gè)和第三個(gè)方法,分別用于動(dòng)態(tài)代理類和動(dòng)態(tài)代理接口,其使用方法如下:

private Object getProxy() {
    // 1. 動(dòng)態(tài)代理類
    return Enhancer.create(RealSubject.class, new MyMethodInterceptor());
    // 2. 動(dòng)態(tài)代理接口
    return Enhancer.create(Object.class, new Class<?>[]{Subject.class}, new MyMethodInterceptor());
}

private static class MyMethodInterceptor implements MethodInterceptor {

    @Override
    public Object intercept(Object obj, Method method, Object[] args, MethodProxy proxy) throws Throwable {
        System.out.println("Some thing before method invoke");
        Object result = proxy.invokeSuper(obj, args);
        System.out.println("Some thing after method invoke");
        return result;
    }
}

從上小節(jié)可知,JDK只能代理接口,代理生成的類實(shí)現(xiàn)了接口的方法;而Cglib是通過(guò)繼承被代理的類、重寫其方法來(lái)實(shí)現(xiàn)的,如:create方法入?yún)⒌牡谝粋€(gè)參數(shù)就是被代理類的類型。當(dāng)然,Cglib也能代理接口,比如getProxy()方法中的第二種方式。

四、案例:Android端dubbo:reference化的網(wǎng)絡(luò)訪問(wèn)

Dubbo是一款高性能的Java RPC框架,是服務(wù)治理的重量級(jí)中間件。Dubbo采用dubbo:service描述服務(wù)提供者,dubbo:reference描述服務(wù)消費(fèi)者,其共同必填屬性為interface,即Java接口。Dubbo正是采用接口來(lái)作為服務(wù)提供者和消費(fèi)者之間的“共同語(yǔ)言”的。
在移動(dòng)網(wǎng)絡(luò)中,Android作為服務(wù)消費(fèi)者,一般通過(guò)HTTP網(wǎng)關(guān)調(diào)用后端服務(wù)。在國(guó)內(nèi)的大型互聯(lián)網(wǎng)公司中,Java后端大多采用了Dubbo及其變種作為服務(wù)治理、服務(wù)水平擴(kuò)展的解決方案。因此,HTTP網(wǎng)關(guān)通常需要Android的網(wǎng)絡(luò)請(qǐng)求中提供調(diào)用的服務(wù)名稱、服務(wù)方法、服務(wù)版本、服務(wù)分組等信息,然后通過(guò)這些信息反射調(diào)用Java后端提供的RPC服務(wù),實(shí)現(xiàn)從HTTP協(xié)議到RPC協(xié)議的轉(zhuǎn)換。

關(guān)于Android訪問(wèn)網(wǎng)關(guān)請(qǐng)求,其分層結(jié)構(gòu)可參考《基于Retrofit+RxJava的Android分層網(wǎng)絡(luò)請(qǐng)求框架》。

那么,Android端能否以dubbo:reference化的方式申明需要訪問(wèn)的網(wǎng)絡(luò)服務(wù)呢?如何這樣,將極大提高Android開發(fā)人員和Java后端開發(fā)之間的溝通效率,以及Android端的代碼效率。
首先,自定義服務(wù)的消費(fèi)者注解Reference,通過(guò)該注解標(biāo)記某個(gè)服務(wù)。

@Inherited
@Target({ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface Reference {
    // 服務(wù)接口名
    String service() default "";
    // 服務(wù)版本
    String version() default "";
    // 服務(wù)分組
    String group() default "";
    // 省略字段
}

其次,通過(guò)接口定義某個(gè)服務(wù)消費(fèi)(如果可以直接引入后端接口,此步驟可省略),在注解中指明該服務(wù)對(duì)應(yīng)的后端服務(wù)接口名、服務(wù)版本、服務(wù)分組等信息;

@Reference(service = "com.yhthu.java.ClassTestService",  group = "yhthu",  version = "v_test_0.1")
public interface ClassTestService {
    // 實(shí)例方法
    Response echo(String pin);
}

這樣就完成了服務(wù)的申明,接下來(lái)的問(wèn)題是如何實(shí)現(xiàn)服務(wù)的調(diào)用呢?上述申明的服務(wù)接口如何定義實(shí)現(xiàn)呢?這里就涉及依賴注入和動(dòng)態(tài)代理。我們先定義一個(gè)標(biāo)記注解@Service,標(biāo)識(shí)需要被注入實(shí)現(xiàn)的服務(wù)申明。

@Target(ElementType.FIELD)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface Service {
}
// 在需要使用服務(wù)的地方(比如Activity中)申明需要調(diào)用的服務(wù)
@Service
private ClassTestService classTestService;

在調(diào)用classTestService的方法之前,需要注入該接口服務(wù)的實(shí)現(xiàn),因此,該操作可以在調(diào)用組件初始化的時(shí)候進(jìn)行。

// 接口與對(duì)應(yīng)實(shí)現(xiàn)的緩存
private Map<Class<?>, Object> serviceContainer = new HashMap<>();
// 依賴注入
public void inject(Object obj) {
    // 1. 掃描該類中所有添加@Service注解的域
    Field[] fields = obj.getClass().getDeclaredFields();
    for (Field field : fields) {
        if (field.isAnnotationPresent(Service.class)) {
            Class<?> clazz = field.getType();
            if (clazz.getAnnotation(Reference.class) == null) {
                Log.e("ClassTestService", "接口地址未配置");
                continue;
            }
            // 2. 從緩存中取出或生成接口類的實(shí)現(xiàn)(動(dòng)態(tài)代理)
            Object impl = serviceContainer.get(clazz);
            if (impl == null) {
                impl = create(clazz);
                serviceContainer.put(clazz, impl);
            }
            // 3. 設(shè)置服務(wù)接口實(shí)現(xiàn)
            try {
                field.setAccessible(true);
                field.set(obj, impl);
            } catch (IllegalAccessException e) {
                e.printStackTrace();
            }
        }
    }
}

inject方法的關(guān)鍵有三步:

  • 掃描該類中所有添加@Service注解的字段,即可得到上述代碼示例中的ClassTestService字段;
  • 從緩存中取出或生成接口類的實(shí)現(xiàn)。由于通過(guò)接口定義了服務(wù),并且實(shí)現(xiàn)不同服務(wù)的實(shí)現(xiàn)方式基本一致(即將服務(wù)信息發(fā)送HTTP網(wǎng)關(guān)),在生成實(shí)現(xiàn)上可選擇JDK的動(dòng)態(tài)代理。
  • 設(shè)置服務(wù)接口實(shí)現(xiàn),完成為接口注入實(shí)現(xiàn)。
private <T> T create(final Class<T> service) {
    return (T) Proxy.newProxyInstance(service.getClassLoader(), new Class<?>[]{service}, new InvocationHandler() {
        @Override
        public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
            // 1. 獲取服務(wù)信息
            Annotation reference = service.getAnnotation(Reference.class);
            String serviceName = ((Reference) reference).service();
            String versionName = ((Reference) reference).version();
            String groupName = ((Reference) reference).group();
            // 2. 獲取方法名
            String methodName = method.getName();
            // 3. 根據(jù)服務(wù)信息發(fā)起請(qǐng)求,返回調(diào)用結(jié)果
            return Request.request(serviceName, versionName, groupName, methodName, param);
        }
    });
}

在HTTP網(wǎng)關(guān)得到服務(wù)名稱、服務(wù)方法、服務(wù)版本、服務(wù)分組等信息之后,即可實(shí)現(xiàn)對(duì)后端服務(wù)的反射調(diào)用??偟膩?lái)講,即可實(shí)現(xiàn)Android端dubbo:reference化的網(wǎng)絡(luò)訪問(wèn)。

// 調(diào)用ClassTestService服務(wù)的方法
classTestService.echo("yhthu").callback(// ……);

上述代碼實(shí)現(xiàn)均為偽代碼,僅說(shuō)明解決方案思路。

在該案例中,綜合使用了自定義注解、反射以及動(dòng)態(tài)代理,是對(duì)上述理論知識(shí)的一個(gè)具體應(yīng)用。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

  • Spring Cloud為開發(fā)人員提供了快速構(gòu)建分布式系統(tǒng)中一些常見模式的工具(例如配置管理,服務(wù)發(fā)現(xiàn),斷路器,智...
    卡卡羅2017閱讀 136,537評(píng)論 19 139
  • 昨天11點(diǎn)50開始寫作業(yè),匆匆忙忙湊字?jǐn)?shù),自己都不知道寫了些啥,今天縷了縷思路好好講自己的想法吧。 前天去了趟磨鐵...
    見月醬閱讀 323評(píng)論 1 2
  • 過(guò)往的文字 過(guò)往的文字 陌生得如同一把 冰冷的刀 削去 眼睛、耳朵還有 顫抖的心房 如今我只剩骸骨一堆 在太陽(yáng)落山...
    月明如素閱讀 416評(píng)論 2 3
  • 小玉每天都安時(shí)完成老師留的作業(yè),還新開了英語(yǔ)課,今天還得了獎(jiǎng)狀,棒棒的寶貝
    趙晗煜閱讀 262評(píng)論 0 0

友情鏈接更多精彩內(nèi)容