模仿J2EE的session機(jī)制的App后端會(huì)話信息管理實(shí)例
此文章只將思想,不提供具體完整實(shí)現(xiàn)(博主太懶,懶得整理),有疑問(wèn)或想了解的可以私信或評(píng)論
背景
在傳統(tǒng)的java web 中小型項(xiàng)目中,一般使用session暫存會(huì)話信息,比如登錄者的身份信息等。此機(jī)制是借用http的cookie機(jī)制實(shí)現(xiàn),但是對(duì)于app來(lái)說(shuō)每次請(qǐng)求都保存并共享cookie信息比較麻煩,并且傳統(tǒng)的session對(duì)集群并不友好,所以一般app后端服務(wù)都使用token來(lái)區(qū)分用戶登錄信息。
j2ee的session機(jī)制大家都很了解,使用非常方便,在傳統(tǒng)java web應(yīng)用中很好用,但是在互聯(lián)網(wǎng)項(xiàng)目中或用得到集群的一些項(xiàng)目就有些問(wèn)題,比如序列化問(wèn)題,同步的延時(shí)問(wèn)題等等,所以我們需要一個(gè)使用起來(lái)類似session的卻能解決得了集群等問(wèn)題的一個(gè)工具。
方案
我們使用cache機(jī)制來(lái)解決這個(gè)問(wèn)題,比較流行的redis是個(gè)nosql內(nèi)存數(shù)據(jù)庫(kù),而且?guī)в衏ache的失效機(jī)制,很適合做會(huì)話數(shù)據(jù)的存儲(chǔ)。而token字符串需要在第一次請(qǐng)求時(shí)服務(wù)器返回給客戶端,客戶端以后每次請(qǐng)求都使用這個(gè)token標(biāo)識(shí)身份。為了對(duì)業(yè)務(wù)開(kāi)發(fā)透明,我們把a(bǔ)pp的請(qǐng)求和響應(yīng)做的報(bào)文封裝,只需要對(duì)客戶端的http請(qǐng)求工具類做點(diǎn)手腳,對(duì)服務(wù)端的mvc框架做點(diǎn)手腳就可以了,客戶端的http工具類修改很簡(jiǎn)單,主要是服務(wù)端的協(xié)議封裝。
實(shí)現(xiàn)思路
一、制定請(qǐng)求響應(yīng)報(bào)文協(xié)議。
二、解析協(xié)議處理token字符串。
三、使用redis存儲(chǔ)管理token以及對(duì)應(yīng)的會(huì)話信息。
四、提供保存、獲取會(huì)話信息的API。
我們逐步講解下每一步的實(shí)現(xiàn)方案。
一、制定請(qǐng)求響應(yīng)報(bào)文協(xié)議。
既然要封裝報(bào)文協(xié)議,就需要考慮什么是公共字段,什么是業(yè)務(wù)字段,報(bào)文的數(shù)據(jù)結(jié)構(gòu)等。
請(qǐng)求的公共字段一般有token、版本、平臺(tái)、機(jī)型、imei、app來(lái)源等,其中token是我們這次的主角。
響應(yīng)的公共字段一般有token、結(jié)果狀態(tài)(success,fail)、結(jié)果碼(code)、結(jié)果信息等。
報(bào)文數(shù)據(jù)結(jié)構(gòu),我們選用json,原因是json普遍、可視化好、字節(jié)占用低。
請(qǐng)求報(bào)文如下,body中存放業(yè)務(wù)信息,比如登錄的用戶名和密碼等。
{
"token": "客戶端token",
/**客戶端構(gòu)建版本號(hào)*/
"version": 11,
/**客戶端平臺(tái)類型*/
"platform": "IOS",
/**客戶端設(shè)備型號(hào)*/
"machineModel": "Iphone 6s",
"imei": "客戶端串號(hào)(手機(jī))",
/**真正的消息體,應(yīng)為map*/
"body": {
"key1": "value1",
"key2": {
"key21": "value21"
},
"key3": [
1,
]
}
}
響應(yīng)的報(bào)文
{
/**是否成功*/
"success": false,
/**每個(gè)請(qǐng)求都會(huì)返回token,客戶端每次請(qǐng)求都應(yīng)使用最新的token*/
"token": "服務(wù)器為當(dāng)前請(qǐng)求選擇的token",
/**失敗碼*/
"failCode": 1,
/**業(yè)務(wù)消息或者失敗消息*/
"msg": "未知原因",
/**返回的真實(shí)業(yè)務(wù)數(shù)據(jù),可為任意可序列化的對(duì)象*/
"body": null
}
}
二、解析協(xié)議處理token字符串。
服務(wù)端的mvc框架我們選用的是SpringMVC框架,SpringMVC也比較普遍,不做描述。
暫且不提t(yī)oken的處理,先解決制定報(bào)文后怎么做參數(shù)傳遞。
因?yàn)檎?qǐng)求信息被做了封裝,所以要讓springmvc框架能正確注入我們?cè)贑ontroller需要的參數(shù),就需要對(duì)報(bào)文做解析和轉(zhuǎn)換。
要對(duì)請(qǐng)求信息做解析,我們需要自定義springmvc的參數(shù)轉(zhuǎn)換器,通過(guò)實(shí)現(xiàn)HandlerMethodArgumentResolver接口可以定義一個(gè)參數(shù)轉(zhuǎn)換器
RequestBodyResolver實(shí)現(xiàn)resolveArgument方法,對(duì)參數(shù)進(jìn)行注入,以下代碼為示例代碼,切勿拿來(lái)直用。
@Override
public Object resolveArgument(MethodParameter parameter,
ModelAndViewContainer mavContainer, NativeWebRequest webRequest,
WebDataBinderFactory binderFactory) throws Exception {
String requestBodyStr = webRequest.getParameter(requestBodyParamName);//獲取請(qǐng)求報(bào)文,可以使用任意方式傳遞報(bào)文,只要在這獲取到就可以
if(StringUtils.isNotBlank(requestBodyStr)){
String paramName = parameter.getParameterName();//獲取Controller中參數(shù)名
Class<?> paramClass = parameter.getParameterType();//獲取Controller中參數(shù)類型
/* 通過(guò)json工具類解析報(bào)文 */
JsonNode jsonNode = objectMapper.readTree(requestBodyStr);
if(paramClass.equals(ServiceRequest.class)){//ServiceRequest為請(qǐng)求報(bào)文對(duì)應(yīng)的VO
ServiceRequest serviceRequest = objectMapper.readValue(jsonNode.traverse(),ServiceRequest.class);
return serviceRequest;//返回這個(gè)object就是注入到參數(shù)中了,一定要對(duì)應(yīng)類型,否則異常不容易捕獲
}
if(jsonNode!=null){//從報(bào)文中查找Controller中需要的參數(shù)
JsonNode paramJsonNode = jsonNode.findValue(paramName);
if(paramJsonNode!=null){
return objectMapper.readValue(paramJsonNode.traverse(), paramClass);
}
}
}
return null;
}
將自己定義的參數(shù)轉(zhuǎn)換器配置到SrpingMVC的配置文件中<mvc:argument-resolvers>
<mvc:argument-resolvers>
<!-- 統(tǒng)一的請(qǐng)求信息處理,從ServiceRequest中取數(shù)據(jù) -->
<bean id="requestBodyResolver" class="com.niuxz.resolver.RequestBodyResolver">
<property name="objectMapper"><bean class="com.shoujinwang.utils.json.ObjectMapper"></bean></property>
<!-- 配置請(qǐng)求中ServiceRequest對(duì)應(yīng)的字段名,默認(rèn)為requestBody -->
<property name="requestBodyParamName"><value>requestBody</value></property>
</bean>
</mvc:argument-resolvers>
這樣就可以使報(bào)文中的參數(shù)能被springmvc正確識(shí)別了。
接下來(lái)我們要對(duì)token做處理了,我們需要添加一個(gè)SrpingMVC攔截器將每次請(qǐng)求都攔截下來(lái),這屬于常用功能,不做細(xì)節(jié)描述
Matcher m1 =Pattern.compile("\"token\":\"(.*?)\"").matcher(requestBodyStr);
if(m1.find()){
token = m1.group(1);
}
tokenMapPool.verifyToken(token);//對(duì)token做公共處理,驗(yàn)證
這樣就簡(jiǎn)單的獲取到了token了,可以做公共處理了。
三、使用redis存儲(chǔ)管理token以及對(duì)應(yīng)的會(huì)話信息。
其實(shí)就是寫(xiě)一個(gè)redis的操作工具類,因?yàn)槭褂昧藄pring作為項(xiàng)目主框架,而且我們用到redis的功能并不多,所以直接使用spring提供的CacheManager功能
配置org.springframework.data.redis.cache.RedisCacheManager
<!-- 緩存管理器 全局變量等可以用它存取-->
<bean id="cacheManager" class="org.springframework.data.redis.cache.RedisCacheManager">
<constructor-arg>
<ref bean="redisTemplate"/>
</constructor-arg>
<property name="usePrefix" value="true" />
<property name="cachePrefix">
<bean class="org.springframework.data.redis.cache.DefaultRedisCachePrefix">
<constructor-arg name="delimiter" value=":@WebServiceInterface"/>
</bean>
</property>
<property name="expires"><!-- 緩存有效期 -->
<map>
<entry>
<key><value>tokenPoolCache</value></key><!-- tokenPool緩存名 -->
<value>2592000</value><!-- 有效時(shí)間 -->
</entry>
</map>
</property>
</bean>
四、提供保存、獲取會(huì)話信息的API。
通過(guò)以上前戲我們已經(jīng)把token處理的差不多了,接下來(lái)我們要實(shí)現(xiàn)token管理工作了
我們需要讓業(yè)務(wù)開(kāi)發(fā)方便的保存獲取會(huì)話信息,還要使token是透明的。
import java.util.HashMap;
import java.util.Map;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.springframework.cache.Cache;
import org.springframework.cache.Cache.ValueWrapper;
import org.springframework.cache.CacheManager;
/**
*
* 類 名: TokenMapPoolBean
* 描 述: token以及相關(guān)信息調(diào)用處理類
* 修 改 記 錄:
* @version V1.0
* @date 2016年4月22日
* @author NiuXZ
*
*/
public class TokenMapPoolBean {
private static final Log log = LogFactory.getLog(TokenMapPoolBean.class);
/** 當(dāng)前請(qǐng)求對(duì)應(yīng)的token*/
private ThreadLocal<String> currentToken;
private CacheManager cacheManager;
private String cacheName;
private TokenGenerator tokenGenerator;
public TokenMapPoolBean(CacheManager cacheManager, String cacheName, TokenGenerator tokenGenerator) {
this.cacheManager = cacheManager;
this.cacheName = cacheName;
this.tokenGenerator = tokenGenerator;
currentToken = new ThreadLocal<String>();
}
/**
* 如果token合法就返回token,不合法就創(chuàng)建一個(gè)新的token并返回,
* 將token放入ThreadLocal中 并初始化一個(gè)tokenMap
* @param token
* @return token
*/
public String verifyToken(String token) {
// log.info("校驗(yàn)Token:\""+token+"\"");
String verifyedToken = null;
if (tokenGenerator.checkTokenFormat(token)) {
// log.info("校驗(yàn)Token成功:\""+token+"\"");
verifyedToken = token;
}
else {
verifyedToken = newToken();
}
currentToken.set(verifyedToken);
Cache cache = cacheManager.getCache(cacheName);
if (cache == null) {
throw new RuntimeException("獲取不到存放token的緩存池,chacheName:" + cacheName);
}
ValueWrapper value = cache.get(verifyedToken);
//token對(duì)應(yīng)的值為空,就創(chuàng)建一個(gè)新的tokenMap放入緩存中
if (value == null || value.get() == null) {
verifyedToken = newToken();
currentToken.set(verifyedToken);
Map<String, Object> tokenMap = new HashMap<String, Object>();
cache.put(verifyedToken, tokenMap);
}
return verifyedToken;
}
/**
* 生成新的token
* @return token
*/
private String newToken() {
Cache cache = cacheManager.getCache(cacheName);
if (cache == null) {
throw new RuntimeException("獲取不到存放token的緩存池,chacheName:" + cacheName);
}
String newToken = null;
int count = 0;
do {
count++;
newToken = tokenGenerator.generatorToken();
}
while (cache.get(newToken) != null);
// log.info("創(chuàng)建Token成功:\""+newToken+"\" 嘗試生成:"+count+"次");
return newToken;
}
/**
* 獲取當(dāng)前請(qǐng)求的tokenMap中對(duì)應(yīng)key的對(duì)象
* @param key
* @return 當(dāng)前請(qǐng)求的tokenMap中對(duì)應(yīng)key的屬性,模擬session
*/
public Object getAttribute(String key) {
Cache cache = cacheManager.getCache(cacheName);
if (cache == null) {
throw new RuntimeException("獲取不到存放token的緩存池,chacheName:" + cacheName);
}
ValueWrapper tokenMapWrapper = cache.get(currentToken.get());
Map<String, Object> tokenMap = null;
if (tokenMapWrapper != null) {
tokenMap = (Map<String, Object>) tokenMapWrapper.get();
}
if (tokenMap == null) {
verifyToken(currentToken.get());
tokenMapWrapper = cache.get(currentToken.get());
tokenMap = (Map<String, Object>) tokenMapWrapper.get();
}
return tokenMap.get(key);
}
/**
* 設(shè)置到當(dāng)前請(qǐng)求的tokenMap中,模擬session<br>
* TODO:此種方式設(shè)置attribute有問(wèn)題:<br>
* 1、可能在同一token并發(fā)的情況下執(zhí)行cache.put(currentToken.get(),tokenMap);時(shí),<br>
* tokenMap可能不是最新,會(huì)導(dǎo)致丟失數(shù)據(jù)。<br>
* 2、每次都put整個(gè)tokenMap,數(shù)據(jù)量太大,需要優(yōu)化<br>
* @param key value
*/
public void setAttribute(String key, Object value) {
Cache cache = cacheManager.getCache(cacheName);
if (cache == null) {
throw new RuntimeException("獲取不到存放token的緩存池,chacheName:" + cacheName);
}
ValueWrapper tokenMapWrapper = cache.get(currentToken.get());
Map<String, Object> tokenMap = null;
if (tokenMapWrapper != null) {
tokenMap = (Map<String, Object>) tokenMapWrapper.get();
}
if (tokenMap == null) {
verifyToken(currentToken.get());
tokenMapWrapper = cache.get(currentToken.get());
tokenMap = (Map<String, Object>) tokenMapWrapper.get();
}
log.info("TokenMap.put(key=" + key + ",value=" + value + ")");
tokenMap.put(key, value);
cache.put(currentToken.get(), tokenMap);
}
/**
* 獲取當(dāng)前線程綁定的用戶token
* @return token
*/
public String getToken() {
if (currentToken.get() == null) {
//初始化一次token
verifyToken(null);
}
return currentToken.get();
}
/**
* 刪除token以及tokenMap
* @param token
*/
public void removeTokenMap(String token) {
if (token == null) {
return;
}
Cache cache = cacheManager.getCache(cacheName);
if (cache == null) {
throw new RuntimeException("獲取不到存放token的緩存池,chacheName:" + cacheName);
}
log.info("刪除Token:token=" + token);
cache.evict(token);
}
public CacheManager getCacheManager() {
return cacheManager;
}
public void setCacheManager(CacheManager cacheManager) {
this.cacheManager = cacheManager;
}
public String getCacheName() {
return cacheName;
}
public void setCacheName(String cacheName) {
this.cacheName = cacheName;
}
public TokenGenerator getTokenGenerator() {
return tokenGenerator;
}
public void setTokenGenerator(TokenGenerator tokenGenerator) {
this.tokenGenerator = tokenGenerator;
}
public void clear() {
currentToken.remove();
}
}
這里用到了ThreadLocal變量是因?yàn)閟ervlet容器一個(gè)請(qǐng)求對(duì)應(yīng)一個(gè)線程,在一個(gè)請(qǐng)求的生命周期內(nèi)都是處于同一個(gè)線程中,而同時(shí)又有多個(gè)線程共享token管理器,所以需要這個(gè)線程本地變量來(lái)保存token字符串。
注意事項(xiàng):
1、verifyToken方法的調(diào)用,一定要在每次請(qǐng)求最開(kāi)始調(diào)用。并且在請(qǐng)求結(jié)束后調(diào)用clear做清除,以免下次有未知異常導(dǎo)致verifyToken未被執(zhí)行,卻在返回時(shí)從ThreadLocal里取出token返回。(這個(gè)bug困擾我好幾天,公司n個(gè)開(kāi)發(fā)檢查代碼也沒(méi)找到,最后我經(jīng)過(guò)測(cè)試發(fā)現(xiàn)是在發(fā)生404的時(shí)候沒(méi)有進(jìn)入攔截器,所以就沒(méi)有調(diào)用verifyToken方法,導(dǎo)致返回的異常信息中的token為上一次請(qǐng)求的token,導(dǎo)致詭異的串號(hào)問(wèn)題。嗯,記我一大鍋)。
2、客戶端一定要在封裝http工具的時(shí)候把每次token保存下來(lái),并用于下一次請(qǐng)求。公司ios開(kāi)發(fā)請(qǐng)的外包,但是外包沒(méi)按要求做,在未登錄時(shí),不保存token,每次傳遞的都是null,導(dǎo)致每次請(qǐng)求都會(huì)創(chuàng)建一個(gè)token,服務(wù)器創(chuàng)建了大量的無(wú)用token。
使用
使用方式也很簡(jiǎn)單,以下是封裝的登錄管理器,可以參考一下token管理器對(duì)于登陸管理器的應(yīng)用
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.springframework.cache.Cache;
import org.springframework.cache.Cache.ValueWrapper;
import org.springframework.cache.CacheManager;
import com.niuxz.base.Constants;
/**
*
* 類 名: LoginManager
* 描 述: 登錄管理器
* 修 改 記 錄:
* @version V1.0
* @date 2016年7月19日
* @author NiuXZ
*
*/
public class LoginManager {
private static final Log log = LogFactory.getLog(LoginManager.class);
private CacheManager cacheManager;
private String cacheName;
private TokenMapPoolBean tokenMapPool;
public LoginManager(CacheManager cacheManager, String cacheName, TokenMapPoolBean tokenMapPool) {
this.cacheManager = cacheManager;
this.cacheName = cacheName;
this.tokenMapPool = tokenMapPool;
}
public void login(String userId) {
log.info("用戶登錄:userId=" + userId);
Cache cache = cacheManager.getCache(cacheName);
ValueWrapper valueWrapper = cache.get(userId);
String token = (String) (valueWrapper == null ? null : valueWrapper.get());
tokenMapPool.removeTokenMap(token);//退出之前登錄記錄
tokenMapPool.setAttribute(Constants.LOGGED_USER_ID, userId);
cache.put(userId, tokenMapPool.getToken());
}
public void logoutCurrent(String phoneTel) {
String curUserId = getCurrentUserId();
log.info("用戶退出:userId=" + curUserId);
tokenMapPool.removeTokenMap(tokenMapPool.getToken());//退出登錄
if (curUserId != null) {
Cache cache = cacheManager.getCache(cacheName);
cache.evict(curUserId);
cache.evict(phoneTel);
}
}
/**
* 獲取當(dāng)前用戶的id
* @return
*/
public String getCurrentUserId() {
return (String) tokenMapPool.getAttribute(Constants.LOGGED_USER_ID);
}
public CacheManager getCacheManager() {
return cacheManager;
}
public String getCacheName() {
return cacheName;
}
public TokenMapPoolBean getTokenMapPool() {
return tokenMapPool;
}
public void setCacheManager(CacheManager cacheManager) {
this.cacheManager = cacheManager;
}
public void setCacheName(String cacheName) {
this.cacheName = cacheName;
}
public void setTokenMapPool(TokenMapPoolBean tokenMapPool) {
this.tokenMapPool = tokenMapPool;
}
}
下面是一段常見(jiàn)的發(fā)送短信驗(yàn)證碼接口,有的應(yīng)用也是用session存儲(chǔ)驗(yàn)證碼,我不建議用這種方式,存session弊端相當(dāng)大。大家看看就好,不是我寫(xiě)的
public void sendValiCodeByPhoneNum(String phoneNum, String hintMsg, String logSuffix) {
validatePhoneTimeSpace();
// 獲取6位隨機(jī)數(shù)
String code = CodeUtil.getValidateCode();
log.info(code + "------->" + phoneNum);
// 調(diào)用短信驗(yàn)證碼下發(fā)接口
RetStatus retStatus = msgSendUtils.sendSms(code + hintMsg, phoneNum);
if (!retStatus.getIsOk()) {
log.info(retStatus.toString());
throw new ThrowsToDataException(ServiceResponseCode.FAIL_INVALID_PARAMS, "手機(jī)驗(yàn)證碼獲取失敗,請(qǐng)稍后再試");
}
// 重置session
tokenMapPool.setAttribute(Constants.VALIDATE_PHONE, phoneNum);
tokenMapPool.setAttribute(Constants.VALIDATE_PHONE_CODE, code.toString());
tokenMapPool.setAttribute(Constants.SEND_CODE_WRONGNU, 0);
tokenMapPool.setAttribute(Constants.SEND_CODE_TIME, new Date().getTime());
log.info(logSuffix + phoneNum + "短信驗(yàn)證碼:" + code);
}
處理響應(yīng)
有的同學(xué)會(huì)問(wèn)了 那么響應(yīng)的報(bào)文封裝呢?
@RequestMapping("record")
@ResponseBody
public ServiceResponse record(String message){
String userId = loginManager.getCurrentUserId();
messageBoardService.recordMessage(userId, message);
return ServiceResponseBuilder.buildSuccess(null);
}
其中ServiceResponse是封裝的響應(yīng)報(bào)文VO,我們直接使用springmvc的@ResponseBody注解就好了。關(guān)鍵在于這個(gè)builder。
import org.apache.commons.lang3.StringUtils;
import com.niuxz.base.pojo.ServiceResponse;
import com.niuxz.utils.spring.SpringContextUtil;
import com.niuxz.web.server.token.TokenMapPoolBean;
/**
*
* 類 名: ServiceResponseBuilder
*
* @version V1.0
* @date 2016年4月25日
* @author NiuXZ
*
*/
public class ServiceResponseBuilder {
/**
* 構(gòu)建一個(gè)成功的響應(yīng)信息
*
* @param body
* @return 一個(gè)操作成功的 ServiceResponse
*/
public static ServiceResponse buildSuccess(Object body) {
return new ServiceResponse(
((TokenMapPoolBean) SpringContextUtil.getBean("tokenMapPool"))
.getToken(),
"操作成功", body);
}
/**
* 構(gòu)建一個(gè)成功的響應(yīng)信息
*
* @param body
* @return 一個(gè)操作成功的 ServiceResponse
*/
public static ServiceResponse buildSuccess(String token, Object body) {
return new ServiceResponse(token, "操作成功", body);
}
/**
* 構(gòu)建一個(gè)失敗的響應(yīng)信息
*
* @param failCode
* msg
* @return 一個(gè)操作失敗的 ServiceResponse
*/
public static ServiceResponse buildFail(int failCode, String msg) {
return buildFail(failCode, msg, null);
}
/**
* 構(gòu)建一個(gè)失敗的響應(yīng)信息
*
* @param failCode
* msg body
* @return 一個(gè)操作失敗的 ServiceResponse
*/
public static ServiceResponse buildFail(int failCode, String msg,
Object body) {
return new ServiceResponse(
((TokenMapPoolBean) SpringContextUtil.getBean("tokenMapPool"))
.getToken(),
failCode, StringUtils.isNotBlank(msg) ? msg : "操作失敗", body);
}
}
由于使用的是靜態(tài)工具類的形式,不能通過(guò)spring注入tokenMapPool(token管理器)對(duì)象,則通過(guò)spring提供的api獲取。然后構(gòu)建響應(yīng)信息的時(shí)候直接調(diào)用tokenMapPool的getToken()方法,此方法會(huì)返回當(dāng)前線程綁定的token字符串。再次強(qiáng)調(diào)在請(qǐng)求結(jié)束后一定要手動(dòng)調(diào)用clear(我通過(guò)全局?jǐn)r截器調(diào)用)。
以上這篇模仿J2EE的session機(jī)制的App后端會(huì)話信息管理實(shí)例就是小編分享給大家的全部?jī)?nèi)容了,希望能給大家一個(gè)參考,也希望大家多多支持腳本之家。
相關(guān)文章
詳解Spring Cloud中Hystrix的請(qǐng)求合并
這篇文章主要介紹了詳解Spring Cloud中Hystrix的請(qǐng)求合并,小編覺(jué)得挺不錯(cuò)的,現(xiàn)在分享給大家,也給大家做個(gè)參考。一起跟隨小編過(guò)來(lái)看看吧2018-05-05
mybatis-plus雪花算法自動(dòng)生成機(jī)器id原理及源碼
Mybatis-Plus是一個(gè)Mybatis的增強(qiáng)工具,它在Mybatis的基礎(chǔ)上做了增強(qiáng),卻不做改變,Mybatis-Plus是為簡(jiǎn)化開(kāi)發(fā)、提高開(kāi)發(fā)效率而生,但它也提供了一些很有意思的插件,比如SQL性能監(jiān)控、樂(lè)觀鎖、執(zhí)行分析等,下面一起看看mybatis-plus雪花算法自動(dòng)生成機(jī)器id原理解析2021-06-06
SpringMVC中的DispatcherServlet詳細(xì)解析
這篇文章主要介紹了SpringMVC中的DispatcherServlet詳細(xì)解析,DispatcherServlet也是一個(gè)Servlet,它也能通過(guò)Servlet的API來(lái)響應(yīng)請(qǐng)求,從而成為一個(gè)前端控制器,Web容器會(huì)調(diào)用Servlet的doGet()以及doPost()等方法,需要的朋友可以參考下2023-12-12
SpringBoot整合Kotlin構(gòu)建Web服務(wù)的方法示例
這篇文章主要介紹了SpringBoot整合Kotlin構(gòu)建Web服務(wù)的方法示例,小編覺(jué)得挺不錯(cuò)的,現(xiàn)在分享給大家,也給大家做個(gè)參考。一起跟隨小編過(guò)來(lái)看看吧2019-02-02
Java畢業(yè)設(shè)計(jì)實(shí)戰(zhàn)之藥店信息管理系統(tǒng)的實(shí)現(xiàn)
這是一個(gè)使用了java+SSM+JSP+layui+maven+mysql開(kāi)發(fā)的藥店信息管理系統(tǒng),是一個(gè)畢業(yè)設(shè)計(jì)的實(shí)戰(zhàn)練習(xí),具有藥店信息管理該有的所有功能,感興趣的朋友快來(lái)看看吧2022-01-01

