欧美bbbwbbbw肥妇,免费乱码人妻系列日韩,一级黄片

SpringBoot實(shí)現(xiàn)埋點(diǎn)監(jiān)控

 更新時(shí)間:2023年01月08日 11:40:51   作者:六月·飛雪  
本文主要介紹了SpringBoot實(shí)現(xiàn)埋點(diǎn)監(jiān)控,文中通過(guò)示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來(lái)一起學(xué)習(xí)學(xué)習(xí)吧

JVM應(yīng)用度量框架Micrometer實(shí)戰(zhàn)

前提

spring-actuator做度量統(tǒng)計(jì)收集,使用Prometheus(普羅米修斯)進(jìn)行數(shù)據(jù)收集,Grafana(增強(qiáng)ui)進(jìn)行數(shù)據(jù)展示,用于監(jiān)控生成環(huán)境機(jī)器的性能指標(biāo)和業(yè)務(wù)數(shù)據(jù)指標(biāo)。一般,我們叫這樣的操作為”埋點(diǎn)”。SpringBoot中的依賴spring-actuator中集成的度量統(tǒng)計(jì)API使用的框架是Micrometer,官網(wǎng)是Micrometer.io。

在實(shí)踐中發(fā)現(xiàn)了業(yè)務(wù)開(kāi)發(fā)者濫用了Micrometer的度量類(lèi)型Counter,導(dǎo)致無(wú)論什么情況下都只使用計(jì)數(shù)統(tǒng)計(jì)的功能。這篇文章就是基于Micrometer分析其他的度量類(lèi)型API的作用和適用場(chǎng)景。

Micrometer提供的度量類(lèi)庫(kù)

Meter是指一組用于收集應(yīng)用中的度量數(shù)據(jù)的接口,Meter單詞可以翻譯為”米”或者”千分尺”,但是顯然聽(tīng)起來(lái)都不是很合理,因此下文直接叫Meter,理解它為度量接口即可。Meter是由MeterRegistry創(chuàng)建和保存的,可以理解MeterRegistry是Meter的工廠和緩存中心,一般而言每個(gè)JVM應(yīng)用在使用Micrometer的時(shí)候必須創(chuàng)建一個(gè)MeterRegistry的具體實(shí)現(xiàn)。

Micrometer中,Meter的具體類(lèi)型包括:Timer,Counter,Gauge,DistributionSummary,LongTaskTimer,F(xiàn)unctionCounter,F(xiàn)unctionTimer和TimeGauge。

下面分節(jié)詳細(xì)介紹這些類(lèi)型的使用方法和實(shí)戰(zhàn)使用場(chǎng)景。而一個(gè)Meter具體類(lèi)型需要通過(guò)名字和Tag(這里指的是Micrometer提供的Tag接口)作為它的唯一標(biāo)識(shí),這樣做的好處是可以使用名字進(jìn)行標(biāo)記,通過(guò)不同的Tag去區(qū)分多種維度進(jìn)行數(shù)據(jù)統(tǒng)計(jì)。

MeterRegistry

MeterRegistry在Micrometer是一個(gè)抽象類(lèi),主要實(shí)現(xiàn)包括:

  • SimpleMeterRegistry:每個(gè)Meter的最新數(shù)據(jù)可以收集到SimpleMeterRegistry實(shí)例中,但是這些數(shù)據(jù)不會(huì)發(fā)布到其他系統(tǒng),也就是數(shù)據(jù)是位于應(yīng)用的內(nèi)存中的。
  • CompositeMeterRegistry:多個(gè)MeterRegistry聚合,內(nèi)部維護(hù)了一個(gè)MeterRegistry的列表。
  • 全局的MeterRegistry:工廠類(lèi)io.micrometer.core.instrument.Metrics中持有一個(gè)靜態(tài)final的CompositeMeterRegistry實(shí)例globalRegistry。

當(dāng)然,使用者也可以自行繼承MeterRegistry去實(shí)現(xiàn)自定義的MeterRegistry。SimpleMeterRegistry適合做調(diào)試的時(shí)候使用,它的簡(jiǎn)單使用方式如下:

MeterRegistry registry = new SimpleMeterRegistry(); 
Counter counter = registry.counter("counter");
counter.increment();

CompositeMeterRegistry實(shí)例初始化的時(shí)候,內(nèi)部持有的MeterRegistry列表是空的,如果此時(shí)用它新增一個(gè)Meter實(shí)例,Meter實(shí)例的操作是無(wú)效的

CompositeMeterRegistry composite = new CompositeMeterRegistry();
 
Counter compositeCounter = composite.counter("counter");
compositeCounter.increment(); // <- 實(shí)際上這一步操作是無(wú)效的,但是不會(huì)報(bào)錯(cuò)
 
SimpleMeterRegistry simple = new SimpleMeterRegistry();
composite.add(simple);  // <- 向CompositeMeterRegistry實(shí)例中添加SimpleMeterRegistry實(shí)例
 
compositeCounter.increment();  // <-計(jì)數(shù)成功

全局的MeterRegistry的使用方式更加簡(jiǎn)單便捷,因?yàn)橐磺兄恍枰僮鞴S類(lèi)Metrics的靜態(tài)方法:

Metrics.addRegistry(new SimpleMeterRegistry());
Counter counter = Metrics.counter("counter", "tag-1", "tag-2");
counter.increment();

Tag與Meter的命名

Micrometer中,Meter的命名約定使用英文逗號(hào)(dot,也就是”.”)分隔單詞。但是對(duì)于不同的監(jiān)控系統(tǒng),對(duì)命名的規(guī)約可能并不相同,如果命名規(guī)約不一致,在做監(jiān)控系統(tǒng)遷移或者切換的時(shí)候,可能會(huì)對(duì)新的系統(tǒng)造成破壞。

Micrometer中使用英文逗號(hào)分隔單詞的命名規(guī)則,再通過(guò)底層的命名轉(zhuǎn)換接口NamingConvention進(jìn)行轉(zhuǎn)換,最終可以適配不同的監(jiān)控系統(tǒng),同時(shí)可以消除監(jiān)控系統(tǒng)不允許的特殊字符的名稱和標(biāo)記等。開(kāi)發(fā)者也可以覆蓋NamingConvention實(shí)現(xiàn)自定義的命名轉(zhuǎn)換規(guī)則:

registry.config().namingConvention(myCustomNamingConvention);。

在Micrometer中,對(duì)一些主流的監(jiān)控系統(tǒng)或者存儲(chǔ)系統(tǒng)的命名規(guī)則提供了默認(rèn)的轉(zhuǎn)換方式,例如當(dāng)我們使用下面的命名時(shí)候:

MeterRegistry registry = ...
registry.timer("http.server.requests");

對(duì)于不同的監(jiān)控系統(tǒng)或者存儲(chǔ)系統(tǒng),命名會(huì)自動(dòng)轉(zhuǎn)換如下:

  • Prometheus - http_server_requests_duration_seconds。
  • Atlas - httpServerRequests。
  • Graphite - http.server.requests。
  • InfluxDB - http_server_requests。

其實(shí)NamingConvention已經(jīng)提供了5種默認(rèn)的轉(zhuǎn)換規(guī)則:dot、snakeCase、camelCase、upperCamelCase和slashes。

另外,Tag(標(biāo)簽)是Micrometer的一個(gè)重要的功能,嚴(yán)格來(lái)說(shuō),一個(gè)度量框架只有實(shí)現(xiàn)了標(biāo)簽的功能,才能真正地多維度進(jìn)行度量數(shù)據(jù)收集。Tag的命名一般需要是有意義的,所謂有意義就是可以根據(jù)Tag的命名可以推斷出它指向的數(shù)據(jù)到底代表什么維度或者什么類(lèi)型的度量指標(biāo)。

假設(shè)我們需要監(jiān)控?cái)?shù)據(jù)庫(kù)的調(diào)用和Http請(qǐng)求調(diào)用統(tǒng)計(jì),一般推薦的做法是:

MeterRegistry registry = ...
registry.counter("database.calls", "db", "users")
registry.counter("http.requests", "uri", "/api/users")

這樣,當(dāng)我們選擇命名為”database.calls”的計(jì)數(shù)器,我們可以進(jìn)一步選擇分組”db”或者”users”分別統(tǒng)計(jì)不同分組對(duì)總調(diào)用數(shù)的貢獻(xiàn)或者組成。一個(gè)反例如下:

MeterRegistry registry = ...
registry.counter("calls",
    "class", "database",
    "db", "users");
 
registry.counter("calls",
    "class", "http",
    "uri", "/api/users");

通過(guò)命名”calls”得到的計(jì)數(shù)器,由于標(biāo)簽混亂,數(shù)據(jù)是基本無(wú)法分組統(tǒng)計(jì)分析,這個(gè)時(shí)候可以認(rèn)為得到的時(shí)間序列的統(tǒng)計(jì)數(shù)據(jù)是沒(méi)有意義的??梢远x全局的Tag,也就是全局的Tag定義之后,會(huì)附加到所有的使用到的Meter上(只要是使用同一MeterRegistry),全局的Tag可以這樣定義:

MeterRegistry registry = ...
registry.counter("calls",
    "class", "database",
    "db", "users");
 
registry.counter("calls",
    "class", "http",
    "uri", "/api/users");
 
 
MeterRegistry registry = ...
registry.config().commonTags("stack", "prod", "region", "us-east-1");
// 和上面的意義是一樣的
registry.config().commonTags(Arrays.asList(Tag.of("stack", "prod"), Tag.of("region", "us-east-1")));

像上面這樣子使用,就能通過(guò)主機(jī),實(shí)例,區(qū)域,堆棧等操作環(huán)境進(jìn)行多維度深入分析。更多springboot實(shí)戰(zhàn)文章:SpringBoot內(nèi)容聚合

還有兩點(diǎn)點(diǎn)需要注意:

1、Tag的值必須不為null。

2、Micrometer中,Tag必須成對(duì)出現(xiàn),也就是Tag必須設(shè)置為偶數(shù)個(gè),實(shí)際上它們以Key=Value的形式存在,具體可以看io.micrometer.core.instrument.Tag接口:

public interface Tag extends Comparable<Tag> {
    String getKey();
 
    String getValue();
 
    static Tag of(String key, String value) {
        return new ImmutableTag(key, value);
    }
 
    default int compareTo(Tag o) {
        return this.getKey().compareTo(o.getKey());
    }
}

當(dāng)然,有些時(shí)候,我們需要過(guò)濾一些必要的標(biāo)簽或者名稱進(jìn)行統(tǒng)計(jì),或者為Meter的名稱添加白名單,這個(gè)時(shí)候可以使用MeterFilter。MeterFilter本身提供一些列的靜態(tài)方法,多個(gè)MeterFilter可以疊加或者組成鏈實(shí)現(xiàn)用戶最終的過(guò)濾策略。例如:

MeterRegistry registry = ...
registry.config()
    .meterFilter(MeterFilter.ignoreTags("http"))
    .meterFilter(MeterFilter.denyNameStartsWith("jvm"));

表示忽略”http”標(biāo)簽,拒絕名稱以”jvm”字符串開(kāi)頭的Meter。更多用法可以參詳一下MeterFilter這個(gè)類(lèi)。

Meter的命名和Meter的Tag相互結(jié)合,以命名為軸心,以Tag為多維度要素,可以使度量數(shù)據(jù)的維度更加豐富,便于統(tǒng)計(jì)和分析。

Meters

前面提到Meter主要包括:Timer,Counter,Gauge,DistributionSummary,LongTaskTimer,F(xiàn)unctionCounter,F(xiàn)unctionTimer和TimeGauge。下面逐一分析它們的作用和個(gè)人理解的實(shí)際使用場(chǎng)景(應(yīng)該說(shuō)是生產(chǎn)環(huán)境)。

Counter

Counter是一種比較簡(jiǎn)單的Meter,它是一種單值的度量類(lèi)型,或者說(shuō)是一個(gè)單值計(jì)數(shù)器。Counter接口允許使用者使用一個(gè)固定值(必須為正數(shù))進(jìn)行計(jì)數(shù)。準(zhǔn)確來(lái)說(shuō):Counter就是一個(gè)增量為正數(shù)的單值計(jì)數(shù)器。這個(gè)舉個(gè)很簡(jiǎn)單的使用例子:

使用場(chǎng)景:

Counter的作用是記錄XXX的總量或者計(jì)數(shù)值,適用于一些增長(zhǎng)類(lèi)型的統(tǒng)計(jì),例如下單、支付次數(shù)、Http請(qǐng)求總量記錄等等,通過(guò)Tag可以區(qū)分不同的場(chǎng)景,對(duì)于下單,可以使用不同的Tag標(biāo)記不同的業(yè)務(wù)來(lái)源或者是按日期劃分,對(duì)于Http請(qǐng)求總量記錄,可以使用Tag區(qū)分不同的URL。用下單業(yè)務(wù)舉個(gè)例子:

//實(shí)體
@Data
public class Order {
 
    private String orderId;
    private Integer amount;
    private String channel;
    private LocalDateTime createTime;
}
 
 
public class CounterMain {
 
    private static final DateTimeFormatter FORMATTER = DateTimeFormatter.ofPattern("yyyy-MM-dd");
 
    static {
            Metrics.addRegistry(new SimpleMeterRegistry());
        }
 
        public static void main(String[] args) throws Exception {
            Order order1 = new Order();
            order1.setOrderId("ORDER_ID_1");
            order1.setAmount(100);
            order1.setChannel("CHANNEL_A");
            order1.setCreateTime(LocalDateTime.now());
            createOrder(order1);
            Order order2 = new Order();
            order2.setOrderId("ORDER_ID_2");
            order2.setAmount(200);
            order2.setChannel("CHANNEL_B");
            order2.setCreateTime(LocalDateTime.now());
            createOrder(order2);
            Search.in(Metrics.globalRegistry).meters().forEach(each -> {
                StringBuilder builder = new StringBuilder();
                builder.append("name:")
                        .append(each.getId().getName())
                        .append(",tags:")
                        .append(each.getId().getTags())
                        .append(",type:").append(each.getId().getType())
                        .append(",value:").append(each.measure());
                System.out.println(builder.toString());
            });
    }
 
    private static void createOrder(Order order) {
        //忽略訂單入庫(kù)等操作
        Metrics.counter("order.create",
                "channel", order.getChannel(),
                "createTime", FORMATTER.format(order.getCreateTime())).increment();
    }
}

控制臺(tái)輸出

name:order.create,tags:[tag(channel=CHANNEL_A), tag(createTime=2018-11-10)],type:COUNTER,value:[Measurement{statistic='COUNT', value=1.0}]
name:order.create,tags:[tag(channel=CHANNEL_B), tag(createTime=2018-11-10)],type:COUNTER,value:[Measurement{statistic='COUNT', value=1.0}]

上面的例子是使用全局靜態(tài)方法工廠類(lèi)Metrics去構(gòu)造Counter實(shí)例,實(shí)際上,io.micrometer.core.instrument.Counter接口提供了一個(gè)內(nèi)部建造器類(lèi)Counter.Builder去實(shí)例化Counter,Counter.Builder的使用方式如下:

public class CounterBuilderMain {
 
        public static void main(String[] args) throws Exception{
            Counter counter = Counter.builder("name")  //名稱
                    .baseUnit("unit") //基礎(chǔ)單位
                    .description("desc") //描述
                    .tag("tagKey", "tagValue")  //標(biāo)簽
                    .register(new SimpleMeterRegistry());//綁定的MeterRegistry
            counter.increment();
        }
}

FunctionCounter

FunctionCounter是Counter的特化類(lèi)型,它把計(jì)數(shù)器數(shù)值增加的動(dòng)作抽象成接口類(lèi)型ToDoubleFunction,這個(gè)接口JDK1.8中對(duì)于Function的特化類(lèi)型接口。

FunctionCounter的使用場(chǎng)景和Counter是一致的,這里介紹一下它的用法:

public class FunctionCounterMain {
 
        public static void main(String[] args) throws Exception {
            MeterRegistry registry = new SimpleMeterRegistry();
            AtomicInteger n = new AtomicInteger(0);
            //這里ToDoubleFunction匿名實(shí)現(xiàn)其實(shí)可以使用Lambda表達(dá)式簡(jiǎn)化為AtomicInteger::get
            FunctionCounter.builder("functionCounter", n, new ToDoubleFunction<AtomicInteger>() {
                @Override
                public double applyAsDouble(AtomicInteger value) {
                    return value.get();
                }
            }).baseUnit("function")
                    .description("functionCounter")
                    .tag("createOrder", "CHANNEL-A")
                    .register(registry);
            //下面模擬三次計(jì)數(shù)        
            n.incrementAndGet();
            n.incrementAndGet();
            n.incrementAndGet();
        }
}

FunctionCounter使用的一個(gè)明顯的好處是,我們不需要感知FunctionCounter實(shí)例的存在,實(shí)際上我們只需要操作作為FunctionCounter實(shí)例構(gòu)建元素之一的AtomicInteger實(shí)例即可,這種接口的設(shè)計(jì)方式在很多框架里面都可以看到。

Timer

Timer(計(jì)時(shí)器)適用于記錄耗時(shí)比較短的事件的執(zhí)行時(shí)間,通過(guò)時(shí)間分布展示事件的序列和發(fā)生頻率。所有的Timer的實(shí)現(xiàn)至少記錄了發(fā)生的事件的數(shù)量和這些事件的總耗時(shí),從而生成一個(gè)時(shí)間序列。

Timer的基本單位基于服務(wù)端的指標(biāo)而定,但是實(shí)際上我們不需要過(guò)于關(guān)注Timer的基本單位,因?yàn)镸icrometer在存儲(chǔ)生成的時(shí)間序列的時(shí)候會(huì)自動(dòng)選擇適當(dāng)?shù)幕締挝?。Timer接口提供的常用方法如下:

public interface Timer extends Meter {
    ...
    void record(long var1, TimeUnit var3);
 
    default void record(Duration duration) {
        this.record(duration.toNanos(), TimeUnit.NANOSECONDS);
    }
 
    <T> T record(Supplier<T> var1);
 
    <T> T recordCallable(Callable<T> var1) throws Exception;
 
    void record(Runnable var1);
 
    default Runnable wrap(Runnable f) {
        return () -> {
            this.record(f);
        };
    }
 
    default <T> Callable<T> wrap(Callable<T> f) {
        return () -> {
            return this.recordCallable(f);
        };
    }
 
    long count();
 
    double totalTime(TimeUnit var1);
 
    default double mean(TimeUnit unit) {
        return this.count() == 0L ? 0.0D : this.totalTime(unit) / (double)this.count();
    }
 
    double max(TimeUnit var1);
    ...
}

實(shí)際上,比較常用和方便的方法是幾個(gè)函數(shù)式接口入?yún)⒌姆椒ǎ?/p>

Timer timer = ...
timer.record(() -> dontCareAboutReturnValue());
timer.recordCallable(() -> returnValue());
 
Runnable r = timer.wrap(() -> dontCareAboutReturnValue());
Callable c = timer.wrap(() -> returnValue());

使用場(chǎng)景:

根據(jù)個(gè)人經(jīng)驗(yàn)和實(shí)踐,總結(jié)如下:

  • 記錄指定方法的執(zhí)行時(shí)間用于展示。
  • 記錄一些任務(wù)的執(zhí)行時(shí)間,從而確定某些數(shù)據(jù)來(lái)源的速率,例如消息隊(duì)列消息的消費(fèi)速率等。

這里舉個(gè)實(shí)際的例子,要對(duì)系統(tǒng)做一個(gè)功能,記錄指定方法的執(zhí)行時(shí)間,還是用下單方法做例子:

public class TimerMain {
 
        private static final Random R = new Random();
 
        static {
            Metrics.addRegistry(new SimpleMeterRegistry());
        }
 
        public static void main(String[] args) throws Exception {
            Order order1 = new Order();
            order1.setOrderId("ORDER_ID_1");
            order1.setAmount(100);
            order1.setChannel("CHANNEL_A");
            order1.setCreateTime(LocalDateTime.now());
            Timer timer = Metrics.timer("timer", "createOrder", "cost");
            timer.record(() -> createOrder(order1));
        }
 
        private static void createOrder(Order order) {
            try {
                TimeUnit.SECONDS.sleep(R.nextInt(5)); //模擬方法耗時(shí)
            } catch (InterruptedException e) {
                //no-op
            }
        }
}

在實(shí)際生產(chǎn)環(huán)境中,可以通過(guò)spring-aop把記錄方法耗時(shí)的邏輯抽象到一個(gè)切面中,這樣就能減少不必要的冗余的模板代碼。上面的例子是通過(guò)Mertics構(gòu)造Timer實(shí)例,實(shí)際上也可以使用Builder構(gòu)造:

MeterRegistry registry = ...
Timer timer = Timer
    .builder("my.timer")
    .description("a description of what this timer does") // 可選
    .tags("region", "test") // 可選
    .register(registry);

另外,Timer的使用還可以基于它的內(nèi)部類(lèi)Timer.Sample,通過(guò)start和stop兩個(gè)方法記錄兩者之間的邏輯的執(zhí)行耗時(shí)。例如:

Timer.Sample sample = Timer.start(registry);
 
// 這里做業(yè)務(wù)邏輯
Response response = ...
 
sample.stop(registry.timer("my.timer", "response", response.status()));

FunctionTimer

FunctionTimer是Timer的特化類(lèi)型,它主要提供兩個(gè)單調(diào)遞增的函數(shù)(其實(shí)并不是單調(diào)遞增,只是在使用中一般需要隨著時(shí)間最少保持不變或者說(shuō)不減少):一個(gè)用于計(jì)數(shù)的函數(shù)和一個(gè)用于記錄總調(diào)用耗時(shí)的函數(shù),它的建造器的入?yún)⑷缦拢?/p>

public interface FunctionTimer extends Meter {
    static <T> Builder<T> builder(String name, T obj, ToLongFunction<T> countFunction,
                                  ToDoubleFunction<T> totalTimeFunction,
                                  TimeUnit totalTimeFunctionUnit) {
        return new Builder<>(name, obj, countFunction, totalTimeFunction, totalTimeFunctionUnit);
    }
    ...
}

官方文檔中的例子如下:

IMap<?, ?> cache = ...; // 假設(shè)使用了Hazelcast緩存
registry.more().timer("cache.gets.latency", Tags.of("name", cache.getName()), cache,
    c -> c.getLocalMapStats().getGetOperationCount(),  //實(shí)際上就是cache的一個(gè)方法,記錄緩存生命周期初始化的增量(個(gè)數(shù))
    c -> c.getLocalMapStats().getTotalGetLatency(),  // Get操作的延遲時(shí)間總量,可以理解為耗時(shí)
    TimeUnit.NANOSECONDS
);

按照個(gè)人理解,ToDoubleFunction用于統(tǒng)計(jì)事件個(gè)數(shù),ToDoubleFunction用于記錄執(zhí)行總時(shí)間,實(shí)際上兩個(gè)函數(shù)都只是Function函數(shù)的變體,還有一個(gè)比較重要的是總時(shí)間的單位totalTimeFunctionUnit。簡(jiǎn)單的使用方式如下:

public class FunctionTimerMain {
 
        public static void main(String[] args) throws Exception {
            //這個(gè)是為了滿足參數(shù),暫時(shí)不需要理會(huì)
            Object holder = new Object();
            AtomicLong totalTimeNanos = new AtomicLong(0);
            AtomicLong totalCount = new AtomicLong(0);
            FunctionTimer.builder("functionTimer", holder, p -> totalCount.get(), 
                    p -> totalTimeNanos.get(), TimeUnit.NANOSECONDS)
                    .register(new SimpleMeterRegistry());
            totalTimeNanos.addAndGet(10000000);
            totalCount.incrementAndGet();
        }
}

LongTaskTimer

LongTaskTimer也是一種Timer的特化類(lèi)型,主要用于記錄長(zhǎng)時(shí)間執(zhí)行的任務(wù)的持續(xù)時(shí)間,在任務(wù)完成之前,被監(jiān)測(cè)的事件或者任務(wù)仍然處于運(yùn)行狀態(tài),任務(wù)完成的時(shí)候,任務(wù)執(zhí)行的總耗時(shí)才會(huì)被記錄下來(lái)。

LongTaskTimer適合用于長(zhǎng)時(shí)間持續(xù)運(yùn)行的事件耗時(shí)的記錄,例如相對(duì)耗時(shí)的定時(shí)任務(wù)。在Spring應(yīng)用中,可以簡(jiǎn)單地使用@Scheduled和@Timed注解,基于spring-aop完成定時(shí)調(diào)度任務(wù)的總耗時(shí)記錄:

@Timed(value = "aws.scrape", longTask = true)
@Scheduled(fixedDelay = 360000)
void scrapeResources() {
    //這里做相對(duì)耗時(shí)的業(yè)務(wù)邏輯
}

當(dāng)然,在非spring體系中也能方便地使用LongTaskTimer:

public class LongTaskTimerMain {
 
        public static void main(String[] args) throws Exception{
            MeterRegistry meterRegistry = new SimpleMeterRegistry();
            LongTaskTimer longTaskTimer = meterRegistry.more().longTaskTimer("longTaskTimer");
            longTaskTimer.record(() -> {
 
                //這里編寫(xiě)Task的邏輯
            });
            //或者這樣
            Metrics.more().longTaskTimer("longTaskTimer").record(()-> {
                //這里編寫(xiě)Task的邏輯
            });
        }
} 

Gauge

Gauge(儀表)是獲取當(dāng)前度量記錄值的句柄,也就是它表示一個(gè)可以任意上下浮動(dòng)的單數(shù)值度量Meter。Gauge通常用于變動(dòng)的測(cè)量值,測(cè)量值用ToDoubleFunction參數(shù)的返回值設(shè)置,如當(dāng)前的內(nèi)存使用情況,同時(shí)也可以測(cè)量上下移動(dòng)的”計(jì)數(shù)”,比如隊(duì)列中的消息數(shù)量。

官網(wǎng)文檔中提到Gauge的典型使用場(chǎng)景是用于測(cè)量集合或映射的大小或運(yùn)行狀態(tài)中的線程數(shù)。Gauge一般用于監(jiān)測(cè)有自然上界的事件或者任務(wù),而Counter一般使用于無(wú)自然上界的事件或者任務(wù)的監(jiān)測(cè),所以像Http請(qǐng)求總量計(jì)數(shù)應(yīng)該使用Counter而非Gauge。

MeterRegistry中提供了一些便于構(gòu)建用于觀察數(shù)值、函數(shù)、集合和映射的Gauge相關(guān)的方法:

List<String> list = registry.gauge("listGauge", Collections.emptyList(), new ArrayList<>(), List::size); 
List<String> list2 = registry.gaugeCollectionSize("listSize2", Tags.empty(), new ArrayList<>()); 
Map<String, Integer> map = registry.gaugeMapSize("mapGauge", Tags.empty(), new HashMap<>());

上面的三個(gè)方法通過(guò)MeterRegistry構(gòu)建Gauge并且返回了集合或者映射實(shí)例,使用這些集合或者映射實(shí)例就能在其size變化過(guò)程中記錄這個(gè)變更值。更重要的優(yōu)點(diǎn)是,我們不需要感知Gauge接口的存在,只需要像平時(shí)一樣使用集合或者映射實(shí)例就可以了。

此外,Gauge還支持java.lang.Number的子類(lèi),java.util.concurrent.atomic包中的AtomicInteger和AtomicLong,還有Guava提供的AtomicDouble:

AtomicInteger n = registry.gauge("numberGauge", new AtomicInteger(0));
n.set(1);
n.set(2);

除了使用MeterRegistry創(chuàng)建Gauge之外,還可以使用建造器流式創(chuàng)建:

//一般我們不需要操作Gauge實(shí)例
Gauge gauge = Gauge
    .builder("gauge", myObj, myObj::gaugeValue)
    .description("a description of what this gauge does") // 可選
    .tags("region", "test") // 可選
    .register(registry);

使用場(chǎng)景:

根據(jù)個(gè)人經(jīng)驗(yàn)和實(shí)踐,總結(jié)如下:

  • 有自然(物理)上界的浮動(dòng)值的監(jiān)測(cè),例如物理內(nèi)存、集合、映射、數(shù)值等。
  • 有邏輯上界的浮動(dòng)值的監(jiān)測(cè),例如積壓的消息、(線程池中)積壓的任務(wù)等,其實(shí)本質(zhì)也是集合或者映射的監(jiān)測(cè)。

舉個(gè)相對(duì)實(shí)際的例子,假設(shè)我們需要對(duì)登錄后的用戶發(fā)送一條短信或者推送,做法是消息先投放到一個(gè)阻塞隊(duì)列,再由一個(gè)線程消費(fèi)消息進(jìn)行其他操作:

public class GaugeMain {
 
    private static final MeterRegistry MR = new SimpleMeterRegistry();
    private static final BlockingQueue<Message> QUEUE = new ArrayBlockingQueue<>(500);
    private static BlockingQueue<Message> REAL_QUEUE;
 
        static {
            REAL_QUEUE = MR.gauge("messageGauge", QUEUE, Collection::size);
        }
 
        public static void main(String[] args) throws Exception {
            consume();
            Message message = new Message();
            message.setUserId(1L);
            message.setContent("content");
            REAL_QUEUE.put(message);
        }
 
        private static void consume() throws Exception {
            new Thread(() -> {
                while (true) {
                    try {
                        Message message = REAL_QUEUE.take();
                        //handle message
                        System.out.println(message);
                    } catch (InterruptedException e) {
                        //no-op
                    }
                }
            }).start();
        }
}

上面的例子代碼寫(xiě)得比較糟糕,只為了演示相關(guān)使用方式,切勿用于生產(chǎn)環(huán)境。

TimeGauge

TimeGauge是Gauge的特化類(lèi)型,相比Gauge,它的構(gòu)建器中多了一個(gè)TimeUnit類(lèi)型的參數(shù),用于指定ToDoubleFunction入?yún)⒌幕A(chǔ)時(shí)間單位。這里簡(jiǎn)單舉個(gè)使用例子:

public class TimeGaugeMain {
 
        private static final SimpleMeterRegistry R = new SimpleMeterRegistry();
 
        public static void main(String[] args) throws Exception{
            AtomicInteger count = new AtomicInteger();
            TimeGauge.Builder<AtomicInteger> timeGauge = TimeGauge.builder("timeGauge", count,
                    TimeUnit.SECONDS, AtomicInteger::get);
            timeGauge.register(R);
            count.addAndGet(10086);
            print();
            count.set(1);
            print();
        }
 
        private static void print()throws Exception{
            Search.in(R).meters().forEach(each -> {
                StringBuilder builder = new StringBuilder();
                builder.append("name:")
                        .append(each.getId().getName())
                        .append(",tags:")
                        .append(each.getId().getTags())
                        .append(",type:").append(each.getId().getType())
                        .append(",value:").append(each.measure());
                System.out.println(builder.toString());
            });
        }
    }
 
//輸出
name:timeGauge,tags:[],type:GAUGE,value:[Measurement{statistic='VALUE', value=10086.0}]
name:timeGauge,tags:[],type:GAUGE,value:[Measurement{statistic='VALUE', value=1.0}]

DistributionSummary

Summary(摘要)主要用于跟蹤事件的分布,在Micrometer中,對(duì)應(yīng)的類(lèi)是DistributionSummary(分發(fā)摘要)。它的使用方式和Timer十分相似,但是它的記錄值并不依賴于時(shí)間單位。

常見(jiàn)的使用場(chǎng)景:使用DistributionSummary測(cè)量命中服務(wù)器的請(qǐng)求的有效負(fù)載大小。使用MeterRegistry創(chuàng)建DistributionSummary實(shí)例如下:

DistributionSummary summary = registry.summary("response.size");

通過(guò)建造器流式創(chuàng)建如下:

DistributionSummary summary = DistributionSummary
    .builder("response.size")
    .description("a description of what this summary does") // 可選
    .baseUnit("bytes") // 可選
    .tags("region", "test") // 可選
    .scale(100) // 可選
    .register(registry);

DistributionSummary中有很多構(gòu)建參數(shù)跟縮放和直方圖的表示相關(guān),見(jiàn)下一節(jié)。

使用場(chǎng)景:

根據(jù)個(gè)人經(jīng)驗(yàn)和實(shí)踐,總結(jié)如下:

1、不依賴于時(shí)間單位的記錄值的測(cè)量,例如服務(wù)器有效負(fù)載值,緩存的命中率等。

舉個(gè)相對(duì)具體的例子:

public class DistributionSummaryMain {
 
        private static final DistributionSummary DS  = DistributionSummary.builder("cacheHitPercent")
                .register(new SimpleMeterRegistry());
 
        private static final LoadingCache<String, String> CACHE = CacheBuilder.newBuilder()
                .maximumSize(1000)
                .recordStats()
                .expireAfterWrite(60, TimeUnit.SECONDS)
                .build(new CacheLoader<String, String>() {
                    @Override
                    public String load(String s) throws Exception {
                        return selectFromDatabase();
                    }
                });
 
        public static void main(String[] args) throws Exception{
            String key = "doge";
            String value = CACHE.get(key);
            record();
        }
 
        private static void record()throws Exception{
            CacheStats stats = CACHE.stats();
            BigDecimal hitCount = new BigDecimal(stats.hitCount());
            BigDecimal requestCount = new BigDecimal(stats.requestCount());
            DS.record(hitCount.divide(requestCount,2,BigDecimal.ROUND_HALF_DOWN).doubleValue());
        }
}

直方圖和百分?jǐn)?shù)配置

直方圖和百分?jǐn)?shù)配置適用于Summary和Timer,這部分相對(duì)復(fù)雜,等研究透了再補(bǔ)充。

基于SpirngBoot、Prometheus、Grafana集成

集成了Micrometer框架的JVM應(yīng)用使用到Micrometer的API收集的度量數(shù)據(jù)位于內(nèi)存之中,因此,需要額外的存儲(chǔ)系統(tǒng)去存儲(chǔ)這些度量數(shù)據(jù),需要有監(jiān)控系統(tǒng)負(fù)責(zé)統(tǒng)一收集和處理這些數(shù)據(jù),還需要有一些UI工具去展示數(shù)據(jù),一般大佬只喜歡看炫酷的圖表或者動(dòng)畫(huà)。

常見(jiàn)的存儲(chǔ)系統(tǒng)就是時(shí)序數(shù)據(jù)庫(kù),主流的有Influx、Datadog等。比較主流的監(jiān)控系統(tǒng)(主要是用于數(shù)據(jù)收集和處理)就是Prometheus(一般叫普羅米修斯,下面就這樣叫吧)。而展示的UI目前相對(duì)用得比較多的就是Grafana。

另外,Prometheus已經(jīng)內(nèi)置了一個(gè)時(shí)序數(shù)據(jù)庫(kù)的實(shí)現(xiàn),因此,在做一套相對(duì)完善的度量數(shù)據(jù)監(jiān)控的系統(tǒng)只需要依賴目標(biāo)JVM應(yīng)用,Prometheus組件和Grafana組件即可。下面花一點(diǎn)時(shí)間從零開(kāi)始搭建一個(gè)這樣的系統(tǒng),使用CentOS7。

SpirngBoot中使用Micrometer

SpringBoot中的spring-boot-starter-actuator依賴已經(jīng)集成了對(duì)Micrometer的支持,其中的metrics端點(diǎn)的很多功能就是通過(guò)Micrometer實(shí)現(xiàn)的,prometheus端點(diǎn)默認(rèn)也是開(kāi)啟支持的,實(shí)際上actuator依賴的spring-boot-actuator-autoconfigure中集成了對(duì)很多框架的開(kāi)箱即用的API。

其中prometheus包中集成了對(duì)Prometheus的支持,使得使用了actuator可以輕易地讓項(xiàng)目暴露出prometheus端點(diǎn),作為Prometheus收集數(shù)據(jù)的客戶端,Prometheus(服務(wù)端軟件)可以通過(guò)此端點(diǎn)收集應(yīng)用中Micrometer的度量數(shù)據(jù)。

我們先引入spring-boot-starter-actuator和spring-boot-starter-web,實(shí)現(xiàn)一個(gè)Counter和Timer作為示例。依賴:

<dependencyManagement>
      <dependencies>
          <dependency>
              <groupId>org.springframework.boot</groupId>
              <artifactId>spring-boot-dependencies</artifactId>
              <version>2.1.0.RELEASE</version>
              <type>pom</type>
              <scope>import</scope>
          </dependency>
      </dependencies>
  </dependencyManagement>
  <dependencies>
      <dependency>
          <groupId>org.springframework.boot</groupId>
          <artifactId>spring-boot-starter-web</artifactId>
      </dependency>
      <dependency>
          <groupId>org.springframework.boot</groupId>
          <artifactId>spring-boot-starter-actuator</artifactId>
      </dependency>
      <dependency>
          <groupId>org.springframework.boot</groupId>
          <artifactId>spring-boot-starter-aop</artifactId>
      </dependency>
      <dependency>
          <groupId>org.projectlombok</groupId>
          <artifactId>lombok</artifactId>
          <version>1.16.22</version>
      </dependency>
<dependency>
          <groupId>io.micrometer</groupId>
          <artifactId>micrometer-registry-prometheus</artifactId>
          <version>1.1.0</version>
      </dependency>
  </dependencies>

接著編寫(xiě)一個(gè)下單接口和一個(gè)消息發(fā)送模塊,模擬用戶下單之后向用戶發(fā)送消息:

//實(shí)體
@Data
public class Message {
 
        private String orderId;
        private Long userId;
        private String content;
    }
 
    @Data
    public class Order {
 
        private String orderId;
        private Long userId;
        private Integer amount;
        private LocalDateTime createTime;
    }
 
    //控制器和服務(wù)類(lèi)
    @RestController
    public class OrderController {
 
        @Autowired
        private OrderService orderService;
 
        @PostMapping(value = "/order")
        public ResponseEntity<Boolean> createOrder(@RequestBody Order order){
            return ResponseEntity.ok(orderService.createOrder(order));
        }
    }
 
    @Slf4j
    @Service
    public class OrderService {
 
        private static final Random R = new Random();
 
        @Autowired
        private MessageService messageService;
 
        public Boolean createOrder(Order order) {
            //模擬下單
            try {
                int ms = R.nextInt(50) + 50;
                TimeUnit.MILLISECONDS.sleep(ms);
                log.info("保存訂單模擬耗時(shí){}毫秒...", ms);
            } catch (Exception e) {
                //no-op
            }
            //記錄下單總數(shù)
            Metrics.counter("order.count", "order.channel", order.getChannel()).increment();
            //發(fā)送消息
            Message message = new Message();
            message.setContent("模擬短信...");
            message.setOrderId(order.getOrderId());
            message.setUserId(order.getUserId());
            messageService.sendMessage(message);
            return true;
        }
    }
 
    @Slf4j
    @Service
    public class MessageService implements InitializingBean {
 
        private static final BlockingQueue<Message> QUEUE = new ArrayBlockingQueue<>(500);
        private static BlockingQueue<Message> REAL_QUEUE;
        private static final Executor EXECUTOR = Executors.newSingleThreadExecutor();
        private static final Random R = new Random();
 
        static {
            REAL_QUEUE = Metrics.gauge("message.gauge", Tags.of("message.gauge", "message.queue.size"), QUEUE, Collection::size);
        }
 
        public void sendMessage(Message message) {
            try {
                REAL_QUEUE.put(message);
            } catch (InterruptedException e) {
                //no-op
            }
        }
 
        @Override
        public void afterPropertiesSet() throws Exception {
            EXECUTOR.execute(() -> {
                while (true) {
                    try {
                        Message message = REAL_QUEUE.take();
                        log.info("模擬發(fā)送短信,orderId:{},userId:{},內(nèi)容:{},耗時(shí):{}毫秒", message.getOrderId(), message.getUserId(),
                                message.getContent(), R.nextInt(50));
                    } catch (Exception e) {
                        throw new IllegalStateException(e);
                    }
                }
            });
        }
    }
 
    //切面類(lèi)
    @Component
    @Aspect
    public class TimerAspect {
 
        @Around(value = "execution(* club.throwable.smp.service.*Service.*(..))")
        public Object around(ProceedingJoinPoint joinPoint) throws Throwable {
            Signature signature = joinPoint.getSignature();
            MethodSignature methodSignature = (MethodSignature) signature;
            Method method = methodSignature.getMethod();
            Timer timer = Metrics.timer("method.cost.time", "method.name", method.getName());
            ThrowableHolder holder = new ThrowableHolder();
            Object result = timer.recordCallable(() -> {
                try {
                    return joinPoint.proceed();
                } catch (Throwable e) {
                    holder.throwable = e;
                }
                return null;
            });
            if (null != holder.throwable) {
                throw holder.throwable;
            }
            return result;
        }
 
        private class ThrowableHolder {
 
            Throwable throwable;
        }
}

yaml的配置如下:

server:
  port: 9091
management:
  server:
    port: 10091
  endpoints:
    web:
      exposure:
        include: '*'
      base-path: /management

注意多看spring官方文檔關(guān)于Actuator的詳細(xì)描述,在SpringBoot-2.x之后,配置Web端點(diǎn)暴露的權(quán)限控制和1.x有很大的不同。

總結(jié)一下就是:除了shutdown端點(diǎn)之外,其他端點(diǎn)默認(rèn)都是開(kāi)啟支持的這里僅僅是開(kāi)啟支持,并不是暴露為Web端點(diǎn),端點(diǎn)必須暴露為Web端點(diǎn)才能被訪問(wèn),禁用或者開(kāi)啟端點(diǎn)支持的配置方式如下:

management.endpoint.${端點(diǎn)ID}.enabled=true/false可以查

可以查看actuator-api文檔查看所有支持的端點(diǎn)的特性,這個(gè)是2.1.0.RELEASE版本的官方文檔,不知道日后鏈接會(huì)不會(huì)掛掉。端點(diǎn)只開(kāi)啟支持,但是不暴露為Web端點(diǎn),是無(wú)法通過(guò)http://{host}:{management.port}/{management.endpoints.web.base-path}/{endpointId}訪問(wèn)的。

暴露監(jiān)控端點(diǎn)為Web端點(diǎn)的配置是:

management.endpoints.web.exposure.include=info,health
management.endpoints.web.exposure.exclude=prometheus

management.endpoints.web.exposure.exclude用于指定不暴露為Web端點(diǎn)的監(jiān)控端點(diǎn),指定多個(gè)的時(shí)候用英文逗號(hào)分隔management.endpoints.web.exposure.include默認(rèn)指定的只有info和health兩個(gè)端點(diǎn),我們可以直接指定暴露所有的端點(diǎn):management.endpoints.web.exposure.include=*,如果采用YAML配置,記得要加單引號(hào)’‘。暴露所有Web監(jiān)控端點(diǎn)是一件比較危險(xiǎn)的事情,如果需要在生產(chǎn)環(huán)境這樣做,請(qǐng)務(wù)必先確認(rèn)http://{host}:{management.port}不能通過(guò)公網(wǎng)訪問(wèn)(也就是監(jiān)控端點(diǎn)訪問(wèn)的端口只能通過(guò)內(nèi)網(wǎng)訪問(wèn),這樣可以方便后面說(shuō)到的Prometheus服務(wù)端通過(guò)此端口收集數(shù)據(jù))。

Prometheus的安裝和配置

Prometheus目前的最新版本是2.5,鑒于筆者沒(méi)深入玩過(guò)Docker,這里還是直接下載它的壓縮包解壓安裝。

wget https://github.com/prometheus/prometheus/releases/download/v2.5.0/prometheus-2.5.0.linux-amd64.tar.gz
tar xvfz prometheus-*.tar.gz
cd prometheus-*

先編輯解壓出來(lái)的目錄下的prometheus配置文件prometheus.yml,主要修改scrape_configs節(jié)點(diǎn)的屬性:

scrape_configs:
  # The job name is added as a label `job=<job_name>` to any timeseries scraped from this config.
  - job_name: 'prometheus'
 
    # metrics_path defaults to '/metrics'
    # scheme defaults to 'http'.
    # 這里配置需要拉取度量信息的URL路徑,這里選擇應(yīng)用程序的prometheus端點(diǎn)
    metrics_path: /management/prometheus
    static_configs:
    # 這里配置host和port
    - targets: ['localhost:10091']

配置拉取度量數(shù)據(jù)的路徑為localhost:10091/management/metrics,此前記得把前一節(jié)提到的應(yīng)用在虛擬機(jī)中啟動(dòng)。接著啟動(dòng)Prometheus應(yīng)用:

# 參數(shù) --storage.tsdb.path=存儲(chǔ)數(shù)據(jù)的路徑,默認(rèn)路徑為./data
./prometheus --config.file=prometheus.yml

Prometheus引用的默認(rèn)啟動(dòng)端口是9090,啟動(dòng)成功后,日志如下:

此時(shí),訪問(wèn)ttp://${虛擬機(jī)host}:9090/targets就能看到當(dāng)前Prometheus中執(zhí)行的Job

訪問(wèn)ttp://${虛擬機(jī)host}:9090/graph以查找到我們定義的度量Meter和spring-boot-starter-actuator中已經(jīng)定義好的一些關(guān)于JVM或者Tomcat的度量Meter。

我們先對(duì)應(yīng)用的/order接口進(jìn)行調(diào)用,然后查看一下監(jiān)控前面在應(yīng)用中定義的rder_count_total``ethod_cost_time_seconds_sum

可以看到,Meter的信息已經(jīng)被收集和展示,但是顯然不夠詳細(xì)和炫酷,這個(gè)時(shí)候就需要使用Grafana的UI做一下點(diǎn)綴。

Grafana的安裝和使用

Grafana的安裝過(guò)程如下:

wget https://s3-us-west-2.amazonaws.com/grafana-releases/release/grafana-5.3.4-1.x86_64.rpm 
sudo yum localinstall grafana-5.3.4-1.x86_64.rpm

安裝完成后,通過(guò)命令service grafana-server start啟動(dòng)即可,默認(rèn)的啟動(dòng)端口為3000,通過(guò)ttp://${host}:3000即可。初始的賬號(hào)密碼都為admin,權(quán)限是管理員權(quán)限。接著需要在Home面板添加一個(gè)數(shù)據(jù)源,目的是對(duì)接Prometheus服務(wù)端從而可以拉取它里面的度量數(shù)據(jù)。數(shù)據(jù)源添加面板如下:

其實(shí)就是指向Prometheus服務(wù)端的端口就可以了。接下來(lái)可以天馬行空地添加需要的面板,就下單數(shù)量統(tǒng)計(jì)的指標(biāo),可以添加一個(gè)Graph的面板

配置面板的時(shí)候,需要在基礎(chǔ)(General)中指定Title:

接著比較重要的是Metrics的配置,需要指定數(shù)據(jù)源和Prometheus的查詢語(yǔ)句:

最好參考一下Prometheus的官方文檔,稍微學(xué)習(xí)一下它的查詢語(yǔ)言PromQL的使用方式,一個(gè)面板可以支持多個(gè)PromQL查詢。

前面提到的兩項(xiàng)是基本配置,其他配置項(xiàng)一般是圖表展示的輔助或者預(yù)警等輔助功能,這里先不展開(kāi),可以取Grafana的官網(wǎng)挖掘一下使用方式。然后我們?cè)僬{(diào)用一下下單接口,過(guò)一段時(shí)間,圖表的數(shù)據(jù)就會(huì)自動(dòng)更新和展示:

接著添加一下項(xiàng)目中使用的Timer的Meter,便于監(jiān)控方法的執(zhí)行時(shí)間,完成之后大致如下:

到此這篇關(guān)于SpringBoot實(shí)現(xiàn)埋點(diǎn)監(jiān)控的文章就介紹到這了,更多相關(guān)SpringBoot 埋點(diǎn)監(jiān)控內(nèi)容請(qǐng)搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!

相關(guān)文章

  • java配置變量的解釋,搬運(yùn)他人優(yōu)質(zhì)評(píng)論(推薦)

    java配置變量的解釋,搬運(yùn)他人優(yōu)質(zhì)評(píng)論(推薦)

    這篇文章主要介紹了java配置變量,文中通過(guò)示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來(lái)一起學(xué)習(xí)學(xué)習(xí)吧
    2019-04-04
  • java正則表達(dá)式校驗(yàn)日期格式實(shí)例代碼

    java正則表達(dá)式校驗(yàn)日期格式實(shí)例代碼

    如果使用得當(dāng),正則表達(dá)式是匹配各種模式的強(qiáng)大工具,下面這篇文章主要給大家介紹了關(guān)于java正則表達(dá)式校驗(yàn)日期格式的相關(guān)資料,文中通過(guò)實(shí)例代碼介紹的非常詳細(xì),需要的朋友可以參考下
    2023-05-05
  • 一篇文章帶你入門(mén)Java Script

    一篇文章帶你入門(mén)Java Script

    這篇文章主要介紹了新手入門(mén)JavaScript的的相關(guān)資料,文中講解非常細(xì)致,代碼幫助大家更好的理解和學(xué)習(xí),感興趣的朋友可以了解下
    2021-08-08
  • 淺析Spring Boot中的spring-boot-load模塊

    淺析Spring Boot中的spring-boot-load模塊

    spring-boot-loader模塊允許我們使用java -jar archive.jar運(yùn)行包含嵌套依賴jar的jar或者war文件,它提供了三種類(lèi)啟動(dòng)器。下面通過(guò)本文給大家介紹spring-boot-load模塊的相關(guān)知識(shí),感興趣的朋友一起看看吧
    2018-01-01
  • SpringBoot整合Mybatis-plus的具體使用

    SpringBoot整合Mybatis-plus的具體使用

    本文主要介紹了SpringBoot整合Mybatis-plus的具體使用,文中通過(guò)示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來(lái)一起學(xué)習(xí)學(xué)習(xí)吧
    2022-04-04
  • 歸并排序時(shí)間復(fù)雜度過(guò)程推導(dǎo)詳解

    歸并排序時(shí)間復(fù)雜度過(guò)程推導(dǎo)詳解

    這篇文章主要介紹了C語(yǔ)言實(shí)現(xiàn)排序算法之歸并排序,對(duì)歸并排序的原理及實(shí)現(xiàn)過(guò)程做了非常詳細(xì)的解讀,需要的朋友可以參考下,希望能幫助到你
    2021-08-08
  • IDEA2020.1創(chuàng)建springboot項(xiàng)目(國(guó)內(nèi)腳手架)安裝lombok

    IDEA2020.1創(chuàng)建springboot項(xiàng)目(國(guó)內(nèi)腳手架)安裝lombok

    這篇文章主要介紹了IDEA2020.1創(chuàng)建springboot項(xiàng)目(國(guó)內(nèi)腳手架)安裝lombok,文中通過(guò)示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來(lái)一起學(xué)習(xí)學(xué)習(xí)吧
    2020-06-06
  • eclipse 如何創(chuàng)建 user library 方法詳解

    eclipse 如何創(chuàng)建 user library 方法詳解

    這篇文章主要介紹了eclipse 如何創(chuàng)建 user library 方法詳解的相關(guān)資料,需要的朋友可以參考下
    2017-04-04
  • JavaWeb評(píng)論功能實(shí)現(xiàn)步驟以及代碼實(shí)例

    JavaWeb評(píng)論功能實(shí)現(xiàn)步驟以及代碼實(shí)例

    項(xiàng)目初始版本上線,有時(shí)間寫(xiě)點(diǎn)東西記錄一下項(xiàng)目中的心得體會(huì),通過(guò)這個(gè)項(xiàng)目學(xué)習(xí)了很多,要寫(xiě)下來(lái)的有很多,先從評(píng)論功能開(kāi)始吧,下面這篇文章主要給大家介紹了關(guān)于JavaWeb評(píng)論功能實(shí)現(xiàn)步驟以及代碼的相關(guān)資料,需要的朋友可以參考下
    2023-01-01
  • Java 數(shù)據(jù)庫(kù)時(shí)間返回前端顯示錯(cuò)誤(差8個(gè)小時(shí))的解決方法

    Java 數(shù)據(jù)庫(kù)時(shí)間返回前端顯示錯(cuò)誤(差8個(gè)小時(shí))的解決方法

    本文主要介紹了Java 數(shù)據(jù)庫(kù)時(shí)間返回前端顯示錯(cuò)誤(差8個(gè)小時(shí))的解決方法,文中通過(guò)示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來(lái)一起學(xué)習(xí)學(xué)習(xí)吧
    2023-08-08

最新評(píng)論