Spring Cloud學(xué)習(xí)教程之Zuul統(tǒng)一異常處理與回退
前言
Zuul 是Netflix 提供的一個(gè)開源組件,致力于在云平臺(tái)上提供動(dòng)態(tài)路由,監(jiān)控,彈性,安全等邊緣服務(wù)的框架。也有很多公司使用它來(lái)作為網(wǎng)關(guān)的重要組成部分,碰巧今年公司的架構(gòu)組決定自研一個(gè)網(wǎng)關(guān)產(chǎn)品,集動(dòng)態(tài)路由,動(dòng)態(tài)權(quán)限,限流配額等功能為一體,為其他部門的項(xiàng)目提供統(tǒng)一的外網(wǎng)調(diào)用管理,最終形成產(chǎn)品(這方面阿里其實(shí)已經(jīng)有成熟的網(wǎng)關(guān)產(chǎn)品了,但是不太適用于個(gè)性化的配置,也沒有集成權(quán)限和限流降級(jí))。
本文主要給大家介紹了關(guān)于Spring Cloud Zuul統(tǒng)一異常處理與回退的相關(guān)內(nèi)容,分享出來(lái)供大家參考學(xué)習(xí),下面話不多說(shuō)了,來(lái)一起看看詳細(xì)的介紹吧。
一、Filter中統(tǒng)一異常處理
其實(shí)在SpringCloud的Edgware SR2版本中對(duì)于ZuulFilter中的錯(cuò)誤有統(tǒng)一的處理,但是在實(shí)際開發(fā)當(dāng)中對(duì)于錯(cuò)誤的響應(yīng)方式,我想每個(gè)團(tuán)隊(duì)都有自己的處理規(guī)范。那么如何做到自定義的異常處理呢?
我們可以先參考一下SpringCloud提供的SendErrorFilter:
/* * Copyright 2013-2015 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package org.springframework.cloud.netflix.zuul.filters.post; import javax.servlet.RequestDispatcher; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; import org.springframework.beans.factory.annotation.Value; import org.springframework.cloud.netflix.zuul.util.ZuulRuntimeException; import org.springframework.util.ReflectionUtils; import org.springframework.util.StringUtils; import com.netflix.zuul.ZuulFilter; import com.netflix.zuul.context.RequestContext; import com.netflix.zuul.exception.ZuulException; import static org.springframework.cloud.netflix.zuul.filters.support.FilterConstants.ERROR_TYPE; import static org.springframework.cloud.netflix.zuul.filters.support.FilterConstants.SEND_ERROR_FILTER_ORDER; /** * Error {@link ZuulFilter} that forwards to /error (by default) if {@link RequestContext#getThrowable()} is not null. * * @author Spencer Gibb */ //TODO: move to error package in Edgware public class SendErrorFilter extends ZuulFilter { private static final Log log = LogFactory.getLog(SendErrorFilter.class); protected static final String SEND_ERROR_FILTER_RAN = "sendErrorFilter.ran"; @Value("${error.path:/error}") private String errorPath; @Override public String filterType() { return ERROR_TYPE; } @Override public int filterOrder() { return SEND_ERROR_FILTER_ORDER; } @Override public boolean shouldFilter() { RequestContext ctx = RequestContext.getCurrentContext(); // only forward to errorPath if it hasn't been forwarded to already return ctx.getThrowable() != null && !ctx.getBoolean(SEND_ERROR_FILTER_RAN, false); } @Override public Object run() { try { RequestContext ctx = RequestContext.getCurrentContext(); ZuulException exception = findZuulException(ctx.getThrowable()); HttpServletRequest request = ctx.getRequest(); request.setAttribute("javax.servlet.error.status_code", exception.nStatusCode); log.warn("Error during filtering", exception); request.setAttribute("javax.servlet.error.exception", exception); if (StringUtils.hasText(exception.errorCause)) { request.setAttribute("javax.servlet.error.message", exception.errorCause); } RequestDispatcher dispatcher = request.getRequestDispatcher( this.errorPath); if (dispatcher != null) { ctx.set(SEND_ERROR_FILTER_RAN, true); if (!ctx.getResponse().isCommitted()) { ctx.setResponseStatusCode(exception.nStatusCode); dispatcher.forward(request, ctx.getResponse()); } } } catch (Exception ex) { ReflectionUtils.rethrowRuntimeException(ex); } return null; } ZuulException findZuulException(Throwable throwable) { if (throwable.getCause() instanceof ZuulRuntimeException) { // this was a failure initiated by one of the local filters return (ZuulException) throwable.getCause().getCause(); } if (throwable.getCause() instanceof ZuulException) { // wrapped zuul exception return (ZuulException) throwable.getCause(); } if (throwable instanceof ZuulException) { // exception thrown by zuul lifecycle return (ZuulException) throwable; } // fallback, should never get here return new ZuulException(throwable, HttpServletResponse.SC_INTERNAL_SERVER_ERROR, null); } public void setErrorPath(String errorPath) { this.errorPath = errorPath; } }
在這里我們可以找到幾個(gè)關(guān)鍵點(diǎn):
1)在上述代碼中,我們可以發(fā)現(xiàn)filter已經(jīng)將相關(guān)的錯(cuò)誤信息放到request當(dāng)中了:
request.setAttribute("javax.servlet.error.status_code", exception.nStatusCode);
request.setAttribute("javax.servlet.error.exception", exception);
request.setAttribute("javax.servlet.error.message", exception.errorCause);
2)錯(cuò)誤處理完畢后,會(huì)轉(zhuǎn)發(fā)到 xxx/error的地址來(lái)處理
那么我們可以來(lái)做個(gè)試驗(yàn),我們?cè)趃ateway-service項(xiàng)目模塊里,創(chuàng)建一個(gè)會(huì)拋出異常的filter:
package com.hzgj.lyrk.springcloud.gateway.server.filter; import com.netflix.zuul.ZuulFilter; import lombok.extern.slf4j.Slf4j; import org.springframework.stereotype.Component; @Component @Slf4j public class MyZuulFilter extends ZuulFilter { @Override public String filterType() { return "post"; } @Override public int filterOrder() { return 9; } @Override public boolean shouldFilter() { return true; } @Override public Object run() { log.info("run error test ..."); throw new RuntimeException(); // return null; } }
緊接著我們定義一個(gè)控制器,來(lái)做錯(cuò)誤處理:
package com.hzgj.lyrk.springcloud.gateway.server.filter; import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.RestController; import javax.servlet.http.HttpServletRequest; @RestController public class ErrorHandler { @GetMapping(value = "/error") public ResponseEntity<ErrorBean> error(HttpServletRequest request) { String message = request.getAttribute("javax.servlet.error.message").toString(); ErrorBean errorBean = new ErrorBean(); errorBean.setMessage(message); errorBean.setReason("程序出錯(cuò)"); return new ResponseEntity<>(errorBean, HttpStatus.BAD_GATEWAY); } private static class ErrorBean { private String message; private String reason; public String getMessage() { return message; } public void setMessage(String message) { this.message = message; } public String getReason() { return reason; } public void setReason(String reason) { this.reason = reason; } } }
啟動(dòng)項(xiàng)目后,我們通過(guò)網(wǎng)關(guān)訪問(wèn)一下試試:
二、關(guān)于zuul回退的問(wèn)題
1、關(guān)于zuul的超時(shí)問(wèn)題:
這個(gè)問(wèn)題網(wǎng)上有很多解決方案,但是我還要貼一下源代碼,請(qǐng)關(guān)注這個(gè)類 AbstractRibbonCommand,在這個(gè)類里集成了hystrix與ribbon。
/* * Copyright 2013-2016 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * */ package org.springframework.cloud.netflix.zuul.filters.route.support; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; import org.springframework.cloud.netflix.ribbon.RibbonClientConfiguration; import org.springframework.cloud.netflix.ribbon.RibbonHttpResponse; import org.springframework.cloud.netflix.ribbon.support.AbstractLoadBalancingClient; import org.springframework.cloud.netflix.ribbon.support.ContextAwareRequest; import org.springframework.cloud.netflix.zuul.filters.ZuulProperties; import org.springframework.cloud.netflix.zuul.filters.route.RibbonCommand; import org.springframework.cloud.netflix.zuul.filters.route.RibbonCommandContext; import org.springframework.cloud.netflix.zuul.filters.route.ZuulFallbackProvider; import org.springframework.cloud.netflix.zuul.filters.route.FallbackProvider; import org.springframework.http.client.ClientHttpResponse; import com.netflix.client.AbstractLoadBalancerAwareClient; import com.netflix.client.ClientRequest; import com.netflix.client.config.DefaultClientConfigImpl; import com.netflix.client.config.IClientConfig; import com.netflix.client.config.IClientConfigKey; import com.netflix.client.http.HttpResponse; import com.netflix.config.DynamicIntProperty; import com.netflix.config.DynamicPropertyFactory; import com.netflix.hystrix.HystrixCommand; import com.netflix.hystrix.HystrixCommandGroupKey; import com.netflix.hystrix.HystrixCommandKey; import com.netflix.hystrix.HystrixCommandProperties; import com.netflix.hystrix.HystrixCommandProperties.ExecutionIsolationStrategy; import com.netflix.hystrix.HystrixThreadPoolKey; import com.netflix.zuul.constants.ZuulConstants; import com.netflix.zuul.context.RequestContext; /** * @author Spencer Gibb */ public abstract class AbstractRibbonCommand<LBC extends AbstractLoadBalancerAwareClient<RQ, RS>, RQ extends ClientRequest, RS extends HttpResponse> extends HystrixCommand<ClientHttpResponse> implements RibbonCommand { private static final Log LOGGER = LogFactory.getLog(AbstractRibbonCommand.class); protected final LBC client; protected RibbonCommandContext context; protected ZuulFallbackProvider zuulFallbackProvider; protected IClientConfig config; public AbstractRibbonCommand(LBC client, RibbonCommandContext context, ZuulProperties zuulProperties) { this("default", client, context, zuulProperties); } public AbstractRibbonCommand(String commandKey, LBC client, RibbonCommandContext context, ZuulProperties zuulProperties) { this(commandKey, client, context, zuulProperties, null); } public AbstractRibbonCommand(String commandKey, LBC client, RibbonCommandContext context, ZuulProperties zuulProperties, ZuulFallbackProvider fallbackProvider) { this(commandKey, client, context, zuulProperties, fallbackProvider, null); } public AbstractRibbonCommand(String commandKey, LBC client, RibbonCommandContext context, ZuulProperties zuulProperties, ZuulFallbackProvider fallbackProvider, IClientConfig config) { this(getSetter(commandKey, zuulProperties, config), client, context, fallbackProvider, config); } protected AbstractRibbonCommand(Setter setter, LBC client, RibbonCommandContext context, ZuulFallbackProvider fallbackProvider, IClientConfig config) { super(setter); this.client = client; this.context = context; this.zuulFallbackProvider = fallbackProvider; this.config = config; } protected static HystrixCommandProperties.Setter createSetter(IClientConfig config, String commandKey, ZuulProperties zuulProperties) { int hystrixTimeout = getHystrixTimeout(config, commandKey); return HystrixCommandProperties.Setter().withExecutionIsolationStrategy( zuulProperties.getRibbonIsolationStrategy()).withExecutionTimeoutInMilliseconds(hystrixTimeout); } protected static int getHystrixTimeout(IClientConfig config, String commandKey) { int ribbonTimeout = getRibbonTimeout(config, commandKey); DynamicPropertyFactory dynamicPropertyFactory = DynamicPropertyFactory.getInstance(); int defaultHystrixTimeout = dynamicPropertyFactory.getIntProperty("hystrix.command.default.execution.isolation.thread.timeoutInMilliseconds", 0).get(); int commandHystrixTimeout = dynamicPropertyFactory.getIntProperty("hystrix.command." + commandKey + ".execution.isolation.thread.timeoutInMilliseconds", 0).get(); int hystrixTimeout; if(commandHystrixTimeout > 0) { hystrixTimeout = commandHystrixTimeout; } else if(defaultHystrixTimeout > 0) { hystrixTimeout = defaultHystrixTimeout; } else { hystrixTimeout = ribbonTimeout; } if(hystrixTimeout < ribbonTimeout) { LOGGER.warn("The Hystrix timeout of " + hystrixTimeout + "ms for the command " + commandKey + " is set lower than the combination of the Ribbon read and connect timeout, " + ribbonTimeout + "ms."); } return hystrixTimeout; } protected static int getRibbonTimeout(IClientConfig config, String commandKey) { int ribbonTimeout; if (config == null) { ribbonTimeout = RibbonClientConfiguration.DEFAULT_READ_TIMEOUT + RibbonClientConfiguration.DEFAULT_CONNECT_TIMEOUT; } else { int ribbonReadTimeout = getTimeout(config, commandKey, "ReadTimeout", IClientConfigKey.Keys.ReadTimeout, RibbonClientConfiguration.DEFAULT_READ_TIMEOUT); int ribbonConnectTimeout = getTimeout(config, commandKey, "ConnectTimeout", IClientConfigKey.Keys.ConnectTimeout, RibbonClientConfiguration.DEFAULT_CONNECT_TIMEOUT); int maxAutoRetries = getTimeout(config, commandKey, "MaxAutoRetries", IClientConfigKey.Keys.MaxAutoRetries, DefaultClientConfigImpl.DEFAULT_MAX_AUTO_RETRIES); int maxAutoRetriesNextServer = getTimeout(config, commandKey, "MaxAutoRetriesNextServer", IClientConfigKey.Keys.MaxAutoRetriesNextServer, DefaultClientConfigImpl.DEFAULT_MAX_AUTO_RETRIES_NEXT_SERVER); ribbonTimeout = (ribbonReadTimeout + ribbonConnectTimeout) * (maxAutoRetries + 1) * (maxAutoRetriesNextServer + 1); } return ribbonTimeout; } private static int getTimeout(IClientConfig config, String commandKey, String property, IClientConfigKey<Integer> configKey, int defaultValue) { DynamicPropertyFactory dynamicPropertyFactory = DynamicPropertyFactory.getInstance(); return dynamicPropertyFactory.getIntProperty(commandKey + "." + config.getNameSpace() + "." + property, config.get(configKey, defaultValue)).get(); } @Deprecated //TODO remove in 2.0.x protected static Setter getSetter(final String commandKey, ZuulProperties zuulProperties) { return getSetter(commandKey, zuulProperties, null); } protected static Setter getSetter(final String commandKey, ZuulProperties zuulProperties, IClientConfig config) { // @formatter:off Setter commandSetter = Setter.withGroupKey(HystrixCommandGroupKey.Factory.asKey("RibbonCommand")) .andCommandKey(HystrixCommandKey.Factory.asKey(commandKey)); final HystrixCommandProperties.Setter setter = createSetter(config, commandKey, zuulProperties); if (zuulProperties.getRibbonIsolationStrategy() == ExecutionIsolationStrategy.SEMAPHORE){ final String name = ZuulConstants.ZUUL_EUREKA + commandKey + ".semaphore.maxSemaphores"; // we want to default to semaphore-isolation since this wraps // 2 others commands that are already thread isolated final DynamicIntProperty value = DynamicPropertyFactory.getInstance() .getIntProperty(name, zuulProperties.getSemaphore().getMaxSemaphores()); setter.withExecutionIsolationSemaphoreMaxConcurrentRequests(value.get()); } else if (zuulProperties.getThreadPool().isUseSeparateThreadPools()) { final String threadPoolKey = zuulProperties.getThreadPool().getThreadPoolKeyPrefix() + commandKey; commandSetter.andThreadPoolKey(HystrixThreadPoolKey.Factory.asKey(threadPoolKey)); } return commandSetter.andCommandPropertiesDefaults(setter); // @formatter:on } @Override protected ClientHttpResponse run() throws Exception { final RequestContext context = RequestContext.getCurrentContext(); RQ request = createRequest(); RS response; boolean retryableClient = this.client instanceof AbstractLoadBalancingClient && ((AbstractLoadBalancingClient)this.client).isClientRetryable((ContextAwareRequest)request); if (retryableClient) { response = this.client.execute(request, config); } else { response = this.client.executeWithLoadBalancer(request, config); } context.set("ribbonResponse", response); // Explicitly close the HttpResponse if the Hystrix command timed out to // release the underlying HTTP connection held by the response. // if (this.isResponseTimedOut()) { if (response != null) { response.close(); } } return new RibbonHttpResponse(response); } @Override protected ClientHttpResponse getFallback() { if(zuulFallbackProvider != null) { return getFallbackResponse(); } return super.getFallback(); } protected ClientHttpResponse getFallbackResponse() { if (zuulFallbackProvider instanceof FallbackProvider) { Throwable cause = getFailedExecutionException(); cause = cause == null ? getExecutionException() : cause; if (cause == null) { zuulFallbackProvider.fallbackResponse(); } else { return ((FallbackProvider) zuulFallbackProvider).fallbackResponse(cause); } } return zuulFallbackProvider.fallbackResponse(); } public LBC getClient() { return client; } public RibbonCommandContext getContext() { return context; } protected abstract RQ createRequest() throws Exception; }
請(qǐng)注意:getRibbonTimeout方法與getHystrixTimeout方法,其中這兩個(gè)方法 commandKey的值為路由的名稱,比如說(shuō)我們?cè)L問(wèn):http://localhost:8088/order-server/xxx來(lái)訪問(wèn)order-server服務(wù), 那么commandKey 就為order-server
根據(jù)源代碼,我們先設(shè)置gateway-server的超時(shí)參數(shù):
#全局的ribbon設(shè)置 ribbon: ConnectTimeout: 3000 ReadTimeout: 3000 hystrix: command: default: execution: isolation: thread: timeoutInMilliseconds: 3000 zuul: host: connectTimeoutMillis: 10000
當(dāng)然也可以單獨(dú)為order-server設(shè)置ribbon的超時(shí)參數(shù):order-server.ribbon.xxxx=xxx , 為了演示zuul中的回退效果,我在這里把Hystrix超時(shí)時(shí)間設(shè)置短一點(diǎn)。當(dāng)然最好不要將Hystrix默認(rèn)的超時(shí)時(shí)間設(shè)置的比Ribbon的超時(shí)時(shí)間短,源碼里遇到此情況已經(jīng)給與我們警告了。
那么我們?cè)趏rder-server下添加如下方法:
@GetMapping("/sleep/{sleepTime}") public String sleep(@PathVariable Long sleepTime) throws InterruptedException { TimeUnit.SECONDS.sleep(sleepTime); return "SUCCESS"; }
2、zuul的回退方法
我們可以實(shí)現(xiàn)ZuulFallbackProvider接口,實(shí)現(xiàn)代碼:
package com.hzgj.lyrk.springcloud.gateway.server.filter; import com.google.common.collect.ImmutableMap; import com.google.gson.GsonBuilder; import org.springframework.cloud.netflix.zuul.filters.route.ZuulFallbackProvider; import org.springframework.http.HttpHeaders; import org.springframework.http.HttpStatus; import org.springframework.http.MediaType; import org.springframework.http.client.ClientHttpResponse; import org.springframework.stereotype.Component; import java.io.ByteArrayInputStream; import java.io.IOException; import java.io.InputStream; import java.time.LocalDateTime; import java.time.LocalTime; @Component public class FallBackHandler implements ZuulFallbackProvider { @Override public String getRoute() { //代表所有的路由都適配該設(shè)置 return "*"; } @Override public ClientHttpResponse fallbackResponse() { return new ClientHttpResponse() { @Override public HttpStatus getStatusCode() throws IOException { return HttpStatus.OK; } @Override public int getRawStatusCode() throws IOException { return 200; } @Override public String getStatusText() throws IOException { return "OK"; } @Override public void close() { } @Override public InputStream getBody() throws IOException { String result = new GsonBuilder().create().toJson(ImmutableMap.of("errorCode", 500, "content", "請(qǐng)求失敗", "time", LocalDateTime.now())); return new ByteArrayInputStream(result.getBytes()); } @Override public HttpHeaders getHeaders() { HttpHeaders headers = new HttpHeaders(); headers.setContentType(MediaType.APPLICATION_JSON); return headers; } }; } }
此時(shí)我們?cè)L問(wèn):http://localhost:8088/order-server/sleep/6 得到如下結(jié)果:
當(dāng)我們?cè)L問(wèn):http://localhost:8088/order-server/sleep/1 就得到如下結(jié)果:
總結(jié)
以上就是這篇文章的全部?jī)?nèi)容了,希望本文的內(nèi)容對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,如果有疑問(wèn)大家可以留言交流,謝謝大家對(duì)腳本之家的支持。
- Spring Cloud Zuul路由網(wǎng)關(guān)服務(wù)過(guò)濾實(shí)現(xiàn)代碼
- SpringCloud Zuul過(guò)濾器和谷歌Gauva實(shí)現(xiàn)限流
- SpringCloud Zuul實(shí)現(xiàn)動(dòng)態(tài)路由
- SpringCloud Zuul在何種情況下使用Hystrix及問(wèn)題小結(jié)
- 淺談Spring Cloud zuul http請(qǐng)求轉(zhuǎn)發(fā)原理
- 詳解SpringCloud Zuul過(guò)濾器返回值攔截
- SpringCloud實(shí)戰(zhàn)之Zuul網(wǎng)關(guān)服務(wù)
- spring cloud 使用Zuul 實(shí)現(xiàn)API網(wǎng)關(guān)服務(wù)問(wèn)題
- Spring Cloud入門教程之Zuul實(shí)現(xiàn)API網(wǎng)關(guān)與請(qǐng)求過(guò)濾
- spring cloud zuul修改請(qǐng)求url的方法
- SpringCloud Zuul基本使用方法匯總
相關(guān)文章
Java數(shù)據(jù)結(jié)構(gòu)最清晰圖解二叉樹前 中 后序遍歷
樹是一種重要的非線性數(shù)據(jù)結(jié)構(gòu),直觀地看,它是數(shù)據(jù)元素(在樹中稱為結(jié)點(diǎn))按分支關(guān)系組織起來(lái)的結(jié)構(gòu),很象自然界中的樹那樣。樹結(jié)構(gòu)在客觀世界中廣泛存在,如人類社會(huì)的族譜和各種社會(huì)組織機(jī)構(gòu)都可用樹形象表示2022-01-01SpringBoot自定義FailureAnalyzer詳解
這篇文章主要介紹了SpringBoot自定義FailureAnalyzer詳解,FailureAnalyzer是一種在啟動(dòng)時(shí)攔截?exception?并將其轉(zhuǎn)換為?human-readable?消息的好方法,包含在故障分析中,需要的朋友可以參考下2023-11-11Java中ByteArrayInputStream和ByteArrayOutputStream用法詳解
這篇文章主要介紹了Java中ByteArrayInputStream和ByteArrayOutputStream用法詳解,?ByteArrayInputStream?的內(nèi)部額外的定義了一個(gè)計(jì)數(shù)器,它被用來(lái)跟蹤?read()?方法要讀取的下一個(gè)字節(jié)2022-06-06SpringBoot定時(shí)任務(wù)調(diào)度與爬蟲的配置實(shí)現(xiàn)
這篇文章主要介紹了SpringBoot定時(shí)任務(wù)調(diào)度與爬蟲的實(shí)現(xiàn),使用webmagic開發(fā)爬蟲,繼承PageProcessor接口編寫自己的處理類,process是定制爬蟲邏輯的核心接口,在這里編寫抽取邏輯,具體實(shí)現(xiàn)配置過(guò)程跟隨小編一起看看吧2022-01-01springbooot使用google驗(yàn)證碼的功能實(shí)現(xiàn)
這篇文章主要介紹了springbooot使用google驗(yàn)證碼,本文通過(guò)實(shí)例代碼給大家介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或工作具有一定的參考借鑒價(jià)值,需要的朋友可以參考下2023-05-05Java時(shí)間輪算法的實(shí)現(xiàn)代碼示例
本篇文章主要介紹了Java時(shí)間輪算法的實(shí)現(xiàn)代碼示例,小編覺得挺不錯(cuò)的,現(xiàn)在分享給大家,也給大家做個(gè)參考。一起跟隨小編過(guò)來(lái)看看吧2017-08-08解決java讀取EXCEL數(shù)據(jù)變成科學(xué)計(jì)數(shù)法的問(wèn)題
這篇文章主要介紹了解決java讀取EXCEL數(shù)據(jù)變成科學(xué)計(jì)數(shù)法的問(wèn)題,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。一起跟隨小編過(guò)來(lái)看看吧2021-04-04