SpringBoot集成JWT實(shí)現(xiàn)Token登錄驗(yàn)證的示例代碼
一, JWT是什么?
在JWT官網(wǎng)中可以明確看到關(guān)于它的定義
JSON Web令牌(JWT)是一種開(kāi)放的標(biāo)準(zhǔn)(RFC 7519),它定義了一種緊湊而獨(dú)立的方式在各方之間安全地傳輸信息為JSON對(duì)象。該信息可以被驗(yàn)證和信任,因?yàn)樗菙?shù)字簽名的。JWT可以使用秘密(使用HMAC算法)或使用RSA或ECDSA的公開(kāi)/私有密鑰類型簽名。 雖然JWT可以被加密以提供各方之間的保密,但我們將重點(diǎn)關(guān)注簽名的令牌。被簽名的令牌可以驗(yàn)證包含在其中的聲明的完整性,而加密的令牌對(duì)其他各方隱藏這些聲明。當(dāng)使用公鑰/私鑰對(duì)簽名時(shí),簽名還證明只有持有私鑰的一方才是簽名方。
1.1 JWT主要使用場(chǎng)景
授權(quán)(Authorization):這是使用JWT最常見(jiàn)的場(chǎng)景。一旦用戶登錄,每個(gè)后續(xù)請(qǐng)求都將包括JWT,允許用戶訪問(wèn)該令牌允許的路由、服務(wù)和資源。
單點(diǎn)登錄(Single Sign On ):?jiǎn)吸c(diǎn)登錄是當(dāng)今廣泛使用的JWT特性,因?yàn)樗男∫?guī)模和易于跨不同領(lǐng)域使用的能力。
信息交換(lnformation Exchange):信息交換在通信的雙方之間使用JWT對(duì)數(shù)據(jù)進(jìn)行編碼是一種非常安全的方式,由于它的信息是經(jīng)過(guò)簽名的,可以確保發(fā)送者發(fā)送的信息是沒(méi)有經(jīng)過(guò)偽造的。
傳輸信息(transmitting information):在各方之間傳輸信息。由于JWT可以簽名--例如,使用公共/私鑰對(duì)--您可以確保發(fā)件人是他們所說(shuō)的發(fā)送者。此外,由于簽名是使用頭和有效載荷計(jì)算的,您還可以驗(yàn)證內(nèi)容沒(méi)有被篡改。
1.2 JWT請(qǐng)求流程
- 用戶使用賬號(hào)和密碼發(fā)出post請(qǐng)求;
- 服務(wù)器使用私鑰創(chuàng)建一個(gè)jwt;
- 服務(wù)器返回這個(gè)jwt給瀏覽器;
- 瀏覽器將該jwt串在請(qǐng)求頭中像服務(wù)器發(fā)送請(qǐng)求;
- 服務(wù)器驗(yàn)證該jwt;
- 返回響應(yīng)的資源給瀏覽器。
1.3 JWT結(jié)構(gòu)
JWT是由三段信息構(gòu)成的,將這三段信息文本用.連接一起就構(gòu)成了JWT字符串。
就像這樣:
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c
JWT包含了三部分:
Header 頭部(標(biāo)題包含了令牌的元數(shù)據(jù),并且包含簽名和/或加密算法的類型)
Payload 負(fù)載 (類似于飛機(jī)上承載的物品)
Signature 簽名/簽證
Header
JWT的頭部承載兩部分信息:token類型和采用的加密算法。
{ "alg": "HS256", "typ": "JWT" }
聲明類型:這里是jwt
聲明加密的算法:通常直接使用 HMAC SHA256
加密算法是單向函數(shù)散列算法,常見(jiàn)的有MD5、SHA、HAMC。
MD5(message-digest algorithm 5) (信息-摘要算法)縮寫(xiě),廣泛用于加密和解密技術(shù),常用于文件校驗(yàn)。校驗(yàn)?不管文件多大,經(jīng)過(guò)MD5后都能生成唯一的MD5值
SHA (Secure Hash Algorithm,安全散列算法),數(shù)字簽名等密碼學(xué)應(yīng)用中重要的工具,安全性高于MD5
HMAC (Hash Message Authentication Code),散列消息鑒別碼,基于密鑰的Hash算法的認(rèn)證協(xié)議。用公開(kāi)函數(shù)和密鑰產(chǎn)生一個(gè)固定長(zhǎng)度的值作為認(rèn)證標(biāo)識(shí),用這個(gè)標(biāo)識(shí)鑒別消息的完整性。常用于接口簽名驗(yàn)證
Payload
載荷就是存放有效信息的地方。
有效信息包含三個(gè)部分:
標(biāo)準(zhǔn)中注冊(cè)的聲明
公共的聲明
私有的聲明
標(biāo)準(zhǔn)中注冊(cè)的聲明 (建議但不強(qiáng)制使用) :
iss: jwt簽發(fā)者
sub: 面向的用戶(jwt所面向的用戶)
aud: 接收jwt的一方
exp: 過(guò)期時(shí)間戳(jwt的過(guò)期時(shí)間,這個(gè)過(guò)期時(shí)間必須要大于簽發(fā)時(shí)間)
nbf: 定義在什么時(shí)間之前,該jwt都是不可用的.
iat: jwt的簽發(fā)時(shí)間
jti: jwt的唯一身份標(biāo)識(shí),主要用來(lái)作為一次性token,從而回避重放攻擊。
公共的聲明:
公共的聲明可以添加任何的信息,一般添加用戶的相關(guān)信息或其他業(yè)務(wù)需要的必要信息.但不建議添加敏感信息,因?yàn)樵摬糠衷诳蛻舳丝山饷?
私有的聲明:
私有聲明是提供者和消費(fèi)者所共同定義的聲明,一般不建議存放敏感信息,因?yàn)閎ase64是對(duì)稱解密的,意味著該部分信息可以歸類為明文信息。
Signature
jwt的第三部分是一個(gè)簽證信息
這個(gè)部分需要base64加密后的header和base64加密后的payload使用.連接組成的字符串,然后通過(guò)header中聲明的加密方式進(jìn)行加鹽secret組合加密,然后就構(gòu)成了jwt的第三部分。最主要的目的:服務(wù)器應(yīng)用在接受到JWT后,會(huì)首先對(duì)頭部和載荷的內(nèi)容用同一算法再次簽名,如果服務(wù)器應(yīng)用對(duì)頭部和載荷再次以同樣方法簽名之后發(fā)現(xiàn),自己計(jì)算出來(lái)的簽名和接受到的簽名不一樣,那么就說(shuō)明這個(gè)Token的內(nèi)容被別人動(dòng)過(guò)的,我們應(yīng)該拒絕這個(gè)Token,返回一個(gè)HTTP 401 Unauthorized響應(yīng)。
密鑰secret是保存在服務(wù)端的,服務(wù)端會(huì)根據(jù)這個(gè)密鑰進(jìn)行生成token和進(jìn)行驗(yàn)證,所以需要保護(hù)好。
這些信息在官網(wǎng)上也有相關(guān)信息的說(shuō)明:
二,SpringBoot集成JWT具體實(shí)現(xiàn)過(guò)程
這里由于只涉及對(duì)驗(yàn)證功能的實(shí)現(xiàn),因此其他數(shù)據(jù)庫(kù),業(yè)務(wù)類編寫(xiě)一概省略,只對(duì)相關(guān)步驟做說(shuō)明。
2.1添加相關(guān)依賴
既然要使用JWT我們肯定需要引入其依賴
<dependency> <groupId>com.auth0</groupId> <artifactId>java-jwt</artifactId> <version>3.10.3</version> </dependency>
2.2自定義跳出攔截器的注解
方便我們后續(xù)不用對(duì)在配置攔截器時(shí)排除每一個(gè)接口,只用自定義注解去標(biāo)記即可。
import java.lang.annotation.ElementType; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.lang.annotation.Target; /** * @author young * @date 2022/11/24 14:53 * @description: 自定義通過(guò)token注解,如果不加該注解直接攔截 */ @Target({ElementType.METHOD,ElementType.TYPE}) @Retention(RetentionPolicy.RUNTIME) public @interface PassToken { boolean required() default true; }
@Retention注解說(shuō)明
RetentionPolicy.SOURCE:這種類型的Annotations只在源代碼級(jí)別保留,編譯時(shí)就會(huì)被忽略,在class字節(jié)碼文件中不包含。
RetentionPolicy.CLASS:這種類型的Annotations編譯時(shí)被保留,默認(rèn)的保留策略,在class文件中存在,但JVM將會(huì)忽略,運(yùn)行時(shí)無(wú)法獲得。
RetentionPolicy.RUNTIME:這種類型的Annotations將被JVM保留,所以他們能在運(yùn)行時(shí)被JVM或其他使用反射機(jī)制的代碼所讀取和使用。
@Document:說(shuō)明該注解將被包含在javadoc中
@Inherited:說(shuō)明子類可以繼承父類中的該注解
2.3自定義全局統(tǒng)一返回值方法,異常類及相關(guān)枚舉
定義全局枚舉類
package com.yy.enums; import lombok.AllArgsConstructor; import lombok.Data; import lombok.Getter; import java.util.UUID; /** * @author young * @date 2022/8/19 21:36 * @description: 響應(yīng)結(jié)果枚舉 */ @AllArgsConstructor @Getter public enum ResponseEnum { /**響應(yīng)成功**/ SUCCESS(200, "操作成功"), /**操作失敗*/ FAIL(201,"獲取數(shù)據(jù)失敗"), NO_TOKEN(400,"無(wú)token,請(qǐng)重新登錄"), TOKEN_EX(401,"token驗(yàn)證失敗,請(qǐng)重新登錄"), USER_EX(402,"用戶不存在,請(qǐng)重新登錄"), /**錯(cuò)誤請(qǐng)求**/ ERROR(400,"錯(cuò)誤請(qǐng)求"); /**響應(yīng)碼**/ private final Integer code; /** 結(jié)果 **/ private final String resultMessage; public static ResponseEnum getResultCode(Integer code){ for (ResponseEnum value : ResponseEnum.values()) { if (code.equals(value.getCode())){ return value; } } return ResponseEnum.ERROR; } /* 簡(jiǎn)單測(cè)試一下 */ public static void main(String[] args) { ResponseEnum resultCode = ResponseEnum.getResultCode(100); System.out.println(resultCode); } }
定義全局異常類
package com.yy.util; import com.yy.Enums.ResultEnum; import io.swagger.annotations.ApiModel; import io.swagger.annotations.ApiModelProperty; import lombok.Data; /** * @author young * @date 2022/9/25 19:15 * @description: 自定義運(yùn)行時(shí)異常 */ @Data @ApiModel(value = "自定義全局異常類") public class MyException extends RuntimeException{ @ApiModelProperty(value = "異常狀態(tài)碼") private final Integer code; /** * 通過(guò)狀態(tài)碼和異常信息創(chuàng)建異常對(duì)象 * @param code * @param message */ public MyException(Integer code,String message) { super(message); this.code = code; } /** * 接受枚舉類型對(duì)象 * @param resultEnum */ public MyException(ResponseEnum responseEnum){ super(responseEnum.getMessage()); this.code = responseEnum.getCode(); } }
package com.yy.Config; import com.yy.utils.MyException; import com.yy.utils.R; import org.springframework.web.bind.annotation.ExceptionHandler; import org.springframework.web.bind.annotation.RestControllerAdvice; /** * @author young * @date 2022/9/12 15:43 * @description: 自定義異常配置 */ @RestControllerAdvice public class GlobalExceptionConfig{ @ExceptionHandler(MyException.class) public R<MyException> handle(MyException e){ e.printStackTrace(); return R.exception(e.getCode(),e.getMessage()); } }
定義統(tǒng)一返回結(jié)果類
package com.yy.utils; import com.yy.enums.ResponseEnum; import lombok.Data; import java.io.Serializable; import java.util.HashMap; import java.util.Map; /** * @author young * @date 2022/8/19 21:52 * @description: 統(tǒng)一返回結(jié)果的類 */ @Data public class R<T> implements Serializable { private static final long serialVersionUID = 56665257248936049L; /**響應(yīng)碼**/ private Integer code; /**返回消息**/ private String message; /**返回?cái)?shù)據(jù)**/ private T data; private R(){} /** * 操作成功ok方法 */ public static <T> R<T> ok(T data) { R<T> response = new R<>(); response.setCode(ResponseEnum.SUCCESS.getCode()); response.setMessage(ResponseEnum.SUCCESS.getResultMessage()); response.setData(data); return response; } /** * 編譯失敗方法 */ public static <T> R<T> buildFailure(Integer errCode, String errMessage){ R<T> response = new R<>(); response.setCode(errCode); response.setMessage(errMessage); return response; } public static <T> R<T> exception(Integer errCode, String errMessage){ R<T> response = new R<>(); response.setCode(errCode); response.setMessage(errMessage); return response; } }
2.4編寫(xiě)JWT工具類,用于生成Token令牌
package com.yy.utils; import cn.hutool.core.date.DateUtil; import com.auth0.jwt.JWT; import com.auth0.jwt.algorithms.Algorithm; import java.util.Date; /** * @author young * @date 2022/9/12 14:46 * @description: 整合JWT生成token */ public class JwtTokenUtils { private JwtTokenUtils() { throw new IllegalStateException("Utility class"); } /** * 生成token * @param userId * @param sign * @return */ public static String getToken(String userId,String sign){ return JWT.create() //簽收者 .withAudience(userId) //主題 .withSubject("token") //2小時(shí)候token過(guò)期 .withExpiresAt(DateUtil.offsetHour(new Date(),2)) //以password作為token的密鑰 .sign(Algorithm.HMAC256(sign)); } }
Algorithm.HMAC256():使用HS256生成token,密鑰則是用戶的密碼,唯一密鑰的話可以保存在服務(wù)端。
withAudience():存入需要保存在token的信息,這里我們把用戶ID存入token中
2.5編寫(xiě)攔截器并注入容器
package com.yy.Config.inteceptor; import cn.hutool.core.text.CharSequenceUtil; import com.auth0.jwt.JWT; import com.auth0.jwt.JWTVerifier; import com.auth0.jwt.algorithms.Algorithm; import com.auth0.jwt.exceptions.JWTDecodeException; import com.auth0.jwt.exceptions.JWTVerificationException; import com.yy.enums.ResponseEnum; import com.yy.admin.pojo.Admin; import com.yy.admin.service.Impl.AdminServiceImpl; import com.yy.utils.MyException; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Component; import org.springframework.web.method.HandlerMethod; import org.springframework.web.servlet.HandlerInterceptor; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import java.lang.reflect.Method; /** * @author young * @date 2022/9/12 15:37 * @description: 獲取token并驗(yàn)證 */ @Component public class MyJwtInterceptor implements HandlerInterceptor { @Autowired private AdminServiceImpl adminService; @Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { String token = request.getHeader("token"); if (!(handler instanceof HandlerMethod)) { return true; } HandlerMethod handlerMethod = (HandlerMethod) handler; Method method = handlerMethod.getMethod(); //檢查是否通過(guò)有PassToken注解 if (method.isAnnotationPresent(PassToken.class)) { //如果有則跳過(guò)認(rèn)證檢查 PassToken passToken = method.getAnnotation(PassToken.class); if (passToken.required()) { return true; } } //否則進(jìn)行token檢查 if (CharSequenceUtil.isBlank(token)) { throw new MyException(ResponseEnum.TOKEN_EX.getCode(), ResponseEnum.TOKEN_EX.getResultMessage()); } //獲取token中的用戶id String userId; try { userId = JWT.decode(token).getAudience().get(0); } catch (JWTDecodeException j) { throw new MyException(ResponseEnum.TOKEN_EX.getCode(), ResponseEnum.TOKEN_EX.getResultMessage()); } //根據(jù)token中的userId查詢數(shù)據(jù)庫(kù) Admin user = adminService.getById(userId); if (user == null) { throw new MyException(ResponseEnum.USER_EX.getCode(), ResponseEnum.USER_EX.getResultMessage()); } //驗(yàn)證token JWTVerifier jwtVerifier = JWT.require(Algorithm.HMAC256(user.getPwd())).build(); try { jwtVerifier.verify(token); } catch (JWTVerificationException e) { throw new MyException(406, "權(quán)限驗(yàn)證失??!"); } return true; } }
這里需要說(shuō)明一下實(shí)現(xiàn)攔截器的方法,我們只需要實(shí)現(xiàn)HandlerInterceptor接口即可,它主要定義了三個(gè)方法:
boolean preHandle ():
預(yù)處理回調(diào)方法,實(shí)現(xiàn)處理器的預(yù)處理,第三個(gè)參數(shù)為響應(yīng)的處理器,自定義Controller,返回值為true表示繼續(xù)流程(如調(diào)用下一個(gè)攔截器或處理器)或者接著執(zhí)行postHandle()和afterCompletion();false表示流程中斷,不會(huì)繼續(xù)調(diào)用其他的攔截器或處理器,中斷執(zhí)行。
void postHandle():
后處理回調(diào)方法,實(shí)現(xiàn)處理器的后處理(DispatcherServlet進(jìn)行視圖返回渲染之前進(jìn)行調(diào)用),此時(shí)我們可以通過(guò)modelAndView(模型和視圖對(duì)象)對(duì)模型數(shù)據(jù)進(jìn)行處理或?qū)σ晥D進(jìn)行處理,modelAndView也可能為null。
void afterCompletion():
整個(gè)請(qǐng)求處理完畢回調(diào)方法,該方法也是需要當(dāng)前對(duì)應(yīng)的Interceptor的preHandle()的返回值為true時(shí)才會(huì)執(zhí)行,也就是在DispatcherServlet渲染了對(duì)應(yīng)的視圖之后執(zhí)行。用于進(jìn)行資源清理。
整個(gè)請(qǐng)求處理完畢回調(diào)方法。如性能監(jiān)控中我們可以在此記錄結(jié)束時(shí)間并輸出消耗時(shí)間,還可以進(jìn)行一些資源清理,類似于try-catch-finally中的finally,但僅調(diào)用處理器執(zhí)行鏈中。
這里我們主要需要調(diào)用預(yù)處理回調(diào)方法即可,如果有其他業(yè)務(wù)需求,也可自行更改。
主要流程:
從 http 請(qǐng)求頭中取出 token,
判斷是否映射到方法
檢查是否有passtoken注釋,有則跳過(guò)認(rèn)證
檢查有沒(méi)有需要用戶登錄的注解,有則需要取出并驗(yàn)證
認(rèn)證通過(guò)則可以訪問(wèn),不通過(guò)會(huì)報(bào)相關(guān)錯(cuò)誤信息
然后通過(guò)配置類將我們自定義的攔截類注入到spring容器中,并進(jìn)行攔截配置。
package com.yy.Config.inteceptor; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.web.servlet.config.annotation.InterceptorRegistry; import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; /** * @author young * @date 2022/9/12 15:36 * @description: JWT攔截配置 */ @Configuration public class InterceptorConfig implements WebMvcConfigurer { @Override public void addInterceptors(InterceptorRegistry registry) { registry.addInterceptor(jwtInterceptor()) //攔截所有請(qǐng)求,通過(guò)判斷token來(lái)決定是否需要登陸 .addPathPatterns("/**"); } @Bean public MyJwtInterceptor jwtInterceptor(){ return new MyJwtInterceptor(); } }
至此,我們對(duì)于JWT在SpringBoot中的基本配置就算完成了。我們只需在controller層在自己想要放行的api接口上添加我們自定義的放行注解,即可實(shí)現(xiàn)對(duì)api接口的放行,其他接口均要進(jìn)行Token令牌的驗(yàn)證判斷。如果沒(méi)有Token則返回自定義的異常信息。
三,測(cè)試
3.1放行一般類接口
這里我們先只對(duì)一個(gè)業(yè)務(wù)接口進(jìn)行放行。
/** * 查找指定id信息 * * @param id * @return */ @GetMapping("/getOne/{id}") @CostTime @PassToken public R<TestUser> selectOne(@PathVariable Integer id) { TestUser user = testUserService.getById(id); return R.ok(user); }
進(jìn)行接口測(cè)試后發(fā)現(xiàn),該接口獲取數(shù)據(jù)正常。
其他沒(méi)有加@PassToken的接口由于沒(méi)有token進(jìn)行驗(yàn)證,均會(huì)被攔截器攔截,并返回我們預(yù)期的異常信息"token驗(yàn)證失敗,請(qǐng)重新登錄"
因此,同理我們只需要在登陸注冊(cè)或其他不需要token驗(yàn)證的接口上添加自定義注解即可實(shí)現(xiàn)攔截。
為了達(dá)到業(yè)務(wù)需求,我們需要在用戶登錄成功后獲取到token,然后將token信息存放在每次的接口請(qǐng)求頭(headers)上,這樣就能實(shí)現(xiàn)對(duì)用戶接口信息基本保護(hù)了。
3.2放行登錄接口
在業(yè)務(wù)層處理token,將生成的token信息帶到用戶實(shí)體類中,這樣登錄獲取用戶信息時(shí)就能讀取到token信息了
package com.yy.admin.service.Impl; import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; import com.yy.admin.pojo.Admin; import com.yy.admin.service.AdminService; import com.yy.admin.dao.AdminDao; import com.yy.utils.JwtTokenUtils; import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.lang.Nullable; import org.springframework.stereotype.Service; import javax.annotation.Resource; import java.util.Optional; /** * @author young * @description 針對(duì)表【admin】的數(shù)據(jù)庫(kù)操作Service實(shí)現(xiàn) * @createDate 2022-09-05 13:41:54 */ @Service @Slf4j public class AdminServiceImpl extends ServiceImpl<AdminDao, Admin> implements AdminService{ @Resource private AdminDao adminDao; @Override public Admin getMsg(String username, String pwd){ Admin admin = adminDao.selectByUsernameAndPwd(username, pwd); Optional.ofNullable(admin).ifPresent(u->{ //添加token信息設(shè)置到用戶實(shí)體上 String token = JwtTokenUtils.getToken(String.valueOf(admin.getId()),pwd); log.info("token的值為:{}",token); admin.setToken(token); }); return admin; } }
編寫(xiě)用戶登錄的業(yè)務(wù)接口并放行Token
@PostMapping("/login") @PassToken public Object loginStatus(@RequestParam("username") String username, @RequestParam("password") String password){ JSONObject object = new JSONObject(); Admin admin = adminService.getMsg(username, password); if (admin!=null){ object.put("code",1); object.put("msg","登陸成功"); object.put("success",true); object.put("type","success"); object.put("userMsg",admin); }else { object.put("code",0); object.put("success",false); object.put("msg","用戶名或密碼錯(cuò)誤"); object.put("type","error"); } return object; }
進(jìn)行接口測(cè)試
這樣就可以看到生成的Token信息了,然后我們將token信息設(shè)置在請(qǐng)求頭上對(duì)其他接口進(jìn)行測(cè)試。
此時(shí)就能看到,接口請(qǐng)求成功。
注意:這里的參數(shù)名token對(duì)應(yīng)攔截器配置String token=request.getHeader("token")中的getHeader中設(shè)置的的參數(shù)名。
到此這篇關(guān)于SpringBoot集成JWT實(shí)現(xiàn)Token登錄驗(yàn)證的示例代碼的文章就介紹到這了,更多相關(guān)SpringBoot JWT Token登錄驗(yàn)證內(nèi)容請(qǐng)搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
Mybatis-Plus的應(yīng)用場(chǎng)景描述及注入SQL原理分析
MyBatis-Plus是一個(gè) MyBatis 的增強(qiáng)工具,在 MyBatis 的基礎(chǔ)上只做增強(qiáng)不做改變,為簡(jiǎn)化開(kāi)發(fā)、提高效率而生,本文重點(diǎn)給大家介紹Mybatis-Plus的應(yīng)用場(chǎng)景及注入SQL原理分析,感興趣的朋友跟隨小編一起學(xué)習(xí)吧2021-05-05如何使用Java模擬退火算法優(yōu)化Hash函數(shù)
為了解決局部最優(yōu)解問(wèn)題,1983年,Kirkpatrick等提出了模擬退火算法(SA)能有效的解決局部最優(yōu)解問(wèn)題。模擬退火算法包含兩個(gè)部分即Metropolis算法和退火過(guò)程。Metropolis算法就是如何在局部最優(yōu)解的情況下讓其跳出來(lái),是退火的基礎(chǔ)2021-06-06在Spring Boot2中使用CompletableFuture的方法教程
這篇文章主要給大家介紹了關(guān)于在Spring Boot2中使用CompletableFuture的相關(guān)資料,文中通過(guò)示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面來(lái)一起看看吧2019-01-01MyBatis #{}和${} |與數(shù)據(jù)庫(kù)連接池使用詳解
本文將為大家說(shuō)說(shuō)關(guān)于 #{} 和 ${},這個(gè)是 MyBatis 在面試中最常問(wèn)的面試題,以及數(shù)據(jù)庫(kù)連接池相關(guān)的知識(shí),感興趣的朋友跟隨小編一起看看吧2024-01-01Java8?Stream?collect(Collectors.toMap())的使用
這篇文章主要介紹了Java8?Stream?collect(Collectors.toMap())的使用,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。如有錯(cuò)誤或未考慮完全的地方,望不吝賜教2022-05-05SpringBoot DBUnit 單元測(cè)試(小結(jié))
這篇文章主要介紹了SpringBoot DBUnit 單元測(cè)試(小結(jié)),小編覺(jué)得挺不錯(cuò)的,現(xiàn)在分享給大家,也給大家做個(gè)參考。一起跟隨小編過(guò)來(lái)看看吧2018-09-09序列化版本號(hào)serialVersionUID的作用_動(dòng)力節(jié)點(diǎn)Java學(xué)院整理
Java序列化是將一個(gè)對(duì)象編碼成一個(gè)字節(jié)流,反序列化將字節(jié)流編碼轉(zhuǎn)換成一個(gè)對(duì)象,這篇文章主要介紹了序列化版本號(hào)serialVersionUID的作用,具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2017-05-05