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

Spring Security 實(shí)現(xiàn)用戶名密碼登錄流程源碼詳解

 更新時(shí)間:2021年11月23日 11:50:44   作者:大忽悠愛(ài)忽悠  
在服務(wù)端的安全管理使用了Spring Security,用戶登錄成功之后,Spring Security幫你把用戶信息保存在Session里,但是具體保存在哪里,要是不深究你可能就不知道,今天小編就帶大家具體了解一下Spring Security實(shí)現(xiàn)用戶名密碼登錄的流程

引言

你在服務(wù)端的安全管理使用了 Spring Security,用戶登錄成功之后,Spring Security 幫你把用戶信息保存在 Session 里,但是具體保存在哪里,要是不深究你可能就不知道, 這帶來(lái)了一個(gè)問(wèn)題,如果用戶在前端操作修改了當(dāng)前用戶信息,在不重新登錄的情況下,如何獲取到最新的用戶信息?

探究

無(wú)處不在的 Authentication

玩過(guò) Spring Security 的小伙伴都知道,在 Spring Security 中有一個(gè)非常重要的對(duì)象叫做 Authentication,我們可以在任何地方注入 Authentication 進(jìn)而獲取到當(dāng)前登錄用戶信息,Authentication 本身是一個(gè)接口,它有很多實(shí)現(xiàn)類:

在這眾多的實(shí)現(xiàn)類中,我們最常用的就是 UsernamePasswordAuthenticationToken 了,但是當(dāng)我們打開(kāi)這個(gè)類的源碼后,卻發(fā)現(xiàn)這個(gè)類平平無(wú)奇,他只有兩個(gè)屬性、兩個(gè)構(gòu)造方法以及若干個(gè) get/set 方法;當(dāng)然,他還有更多屬性在它的父類上。

但是從它僅有的這兩個(gè)屬性中,我們也能大致看出,這個(gè)類就保存了我們登錄用戶的基本信息。那么我們的登錄信息是如何存到這兩個(gè)對(duì)象中的?這就要來(lái)梳理一下登錄流程了。

登錄流程

在 Spring Security 中,認(rèn)證與授權(quán)的相關(guān)校驗(yàn)都是在一系列的過(guò)濾器鏈中完成的,在這一系列的過(guò)濾器鏈中,和認(rèn)證相關(guān)的過(guò)濾器就是 UsernamePasswordAuthenticationFilter::

public class UsernamePasswordAuthenticationFilter extends AbstractAuthenticationProcessingFilter {
//默認(rèn)的用戶名和密碼對(duì)應(yīng)的key
    public static final String SPRING_SECURITY_FORM_USERNAME_KEY = "username";
    public static final String SPRING_SECURITY_FORM_PASSWORD_KEY = "password";
//當(dāng)前過(guò)濾器默認(rèn)攔截的路徑    
    private static final AntPathRequestMatcher DEFAULT_ANT_PATH_REQUEST_MATCHER = new AntPathRequestMatcher("/login", "POST");
    //默認(rèn)的請(qǐng)求參數(shù)名稱規(guī)定
    private String usernameParameter = "username";
    private String passwordParameter = "password";
    //默認(rèn)只能是post請(qǐng)求
    private boolean postOnly = true;

    public UsernamePasswordAuthenticationFilter() {
    //設(shè)置默認(rèn)的攔截路徑
        super(DEFAULT_ANT_PATH_REQUEST_MATCHER);
    }

    public UsernamePasswordAuthenticationFilter(AuthenticationManager authenticationManager) {
       //設(shè)置默認(rèn)的攔截路徑,和處理認(rèn)證的管理器
        super(DEFAULT_ANT_PATH_REQUEST_MATCHER, authenticationManager);
    }

    public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) throws AuthenticationException {
    //判斷請(qǐng)求方式
        if (this.postOnly && !request.getMethod().equals("POST")) {
            throw new AuthenticationServiceException("Authentication method not supported: " + request.getMethod());
        } else {
        //從請(qǐng)求參數(shù)中獲取對(duì)應(yīng)的值
            String username = this.obtainUsername(request);
            username = username != null ? username : "";
            username = username.trim();
            String password = this.obtainPassword(request);
            password = password != null ? password : "";
            //構(gòu)造用戶名和密碼登錄的認(rèn)證令牌
            UsernamePasswordAuthenticationToken authRequest = new UsernamePasswordAuthenticationToken(username, password);
            //設(shè)置details---deltails里面默認(rèn)存放sessionID和remoteaddr
            //authRequest 就是構(gòu)造好的認(rèn)證令牌
            this.setDetails(request, authRequest);
            //校驗(yàn)
            //authRequest 就是構(gòu)造好的認(rèn)證令牌
            return this.getAuthenticationManager().authenticate(authRequest);
        }
    }

    @Nullable
    protected String obtainPassword(HttpServletRequest request) {
        return request.getParameter(this.passwordParameter);
    }

    @Nullable
    protected String obtainUsername(HttpServletRequest request) {
        return request.getParameter(this.usernameParameter);
    }

    protected void setDetails(HttpServletRequest request, UsernamePasswordAuthenticationToken authRequest) {
        authRequest.setDetails(this.authenticationDetailsSource.buildDetails(request));
    }

    public void setUsernameParameter(String usernameParameter) {
        Assert.hasText(usernameParameter, "Username parameter must not be empty or null");
        this.usernameParameter = usernameParameter;
    }

    public void setPasswordParameter(String passwordParameter) {
        Assert.hasText(passwordParameter, "Password parameter must not be empty or null");
        this.passwordParameter = passwordParameter;
    }

    public void setPostOnly(boolean postOnly) {
        this.postOnly = postOnly;
    }

    public final String getUsernameParameter() {
        return this.usernameParameter;
    }

    public final String getPasswordParameter() {
        return this.passwordParameter;
    }
}

根據(jù)這段源碼我們可以看出:

首先通過(guò) obtainUsername 和 obtainPassword 方法提取出請(qǐng)求里邊的用戶名/密碼出來(lái),提取方式就是 request.getParameter ,這也是為什么 Spring Security 中默認(rèn)的表單登錄要通過(guò) key/value 的形式傳遞參數(shù),而不能傳遞 JSON 參數(shù),如果像傳遞 JSON 參數(shù),修改這里的邏輯即可

獲取到請(qǐng)求里傳遞來(lái)的用戶名/密碼之后,接下來(lái)就構(gòu)造一個(gè) UsernamePasswordAuthenticationToken 對(duì)象,傳入 username 和 password,username 對(duì)應(yīng)了 UsernamePasswordAuthenticationToken 中的 principal 屬性,而 password 則對(duì)應(yīng)了它的 credentials 屬性。

public class UsernamePasswordAuthenticationToken extends AbstractAuthenticationToken {
    private static final long serialVersionUID = 550L;
    private final Object principal;
    private Object credentials;

    public UsernamePasswordAuthenticationToken(Object principal, Object credentials) {
        super((Collection)null);
        this.principal = principal;
        this.credentials = credentials;
        this.setAuthenticated(false);
    }

    public UsernamePasswordAuthenticationToken(Object principal, Object credentials, Collection<? extends GrantedAuthority> authorities) {
        super(authorities);
        this.principal = principal;
        this.credentials = credentials;
        super.setAuthenticated(true);
    }

    public Object getCredentials() {
        return this.credentials;
    }

    public Object getPrincipal() {
        return this.principal;
    }

    public void setAuthenticated(boolean isAuthenticated) throws IllegalArgumentException {
        Assert.isTrue(!isAuthenticated, "Cannot set this token to trusted - use constructor which takes a GrantedAuthority list instead");
        super.setAuthenticated(false);
    }

    public void eraseCredentials() {
        super.eraseCredentials();
        this.credentials = null;
    }
}

接下來(lái) setDetails 方法給 details 屬性賦值,UsernamePasswordAuthenticationToken 本身是沒(méi)有 details 屬性的,這個(gè)屬性在它的父類 AbstractAuthenticationToken 中。details 是一個(gè)對(duì)象,這個(gè)對(duì)象里邊放的是 WebAuthenticationDetails 實(shí)例,該實(shí)例主要描述了兩個(gè)信息,請(qǐng)求的 remoteAddress 以及請(qǐng)求的 sessionId

最后一步,就是調(diào)用 authenticate 方法去做校驗(yàn)了。

好了,從這段源碼中,大家可以看出來(lái)請(qǐng)求的各種信息基本上都找到了自己的位置,找到了位置,這就方便我們未來(lái)去獲取了。

接下來(lái)我們?cè)賮?lái)看請(qǐng)求的具體校驗(yàn)操作。

校驗(yàn)

在前面的 attemptAuthentication 方法中,該方法的最后一步開(kāi)始做校驗(yàn),校驗(yàn)操作首先要獲取到一個(gè) AuthenticationManager,這里拿到的是 ProviderManager ,所以接下來(lái)我們就進(jìn)入到 ProviderManagerauthenticate 方法中,當(dāng)然這個(gè)方法也比較長(zhǎng),我這里僅僅摘列出來(lái)幾個(gè)重要的地方:

    public Authentication authenticate(Authentication authentication) throws AuthenticationException {
    //獲取到主體(用戶名)和憑證(密碼)組成的一個(gè)令牌對(duì)象的class類對(duì)象
        Class<? extends Authentication> toTest = authentication.getClass();
        AuthenticationException lastException = null;
        AuthenticationException parentException = null;
        Authentication result = null;
        Authentication parentResult = null;
        int currentPosition = 0;
        //獲取所有可用來(lái)校驗(yàn)令牌對(duì)象的provider數(shù)量
        int size = this.providers.size();
        //獲取迭代器
        Iterator var9 = this.getProviders().iterator();
         //遍歷所有provider
        while(var9.hasNext()) {
            AuthenticationProvider provider = (AuthenticationProvider)var9.next();
            //判斷當(dāng)前provider是否支持當(dāng)前令牌對(duì)象的校驗(yàn)
            if (provider.supports(toTest)) {
                if (logger.isTraceEnabled()) {
                    Log var10000 = logger;
                    String var10002 = provider.getClass().getSimpleName();
                    ++currentPosition;
                    var10000.trace(LogMessage.format("Authenticating request with %s (%d/%d)", var10002, currentPosition, size));
                }

                try {
                //如果支持就進(jìn)行認(rèn)證校驗(yàn)處理
                    result = provider.authenticate(authentication);
                    //校驗(yàn)成功返回一個(gè)新的authentication
                    //將原先的主體由用戶名換成了userdetails對(duì)象
                    if (result != null) {
                    //拷貝details到新的令牌對(duì)象
                        this.copyDetails(authentication, result);
                        break;
                    }
                } catch (InternalAuthenticationServiceException | AccountStatusException var14) {
                    this.prepareException(var14, authentication);
                    throw var14;
                } catch (AuthenticationException var15) {
                    lastException = var15;
                }
            }
        }

//認(rèn)證失敗但是 provider 的 parent不為null
        if (result == null && this.parent != null) {
            try {
            //調(diào)用 provider 的 parent進(jìn)行驗(yàn)證--parent就是providerManager
                parentResult = this.parent.authenticate(authentication);
                result = parentResult;
            } catch (ProviderNotFoundException var12) {
            } catch (AuthenticationException var13) {
                parentException = var13;
                lastException = var13;
            }
        }

//認(rèn)證成功
        if (result != null) {
         //擦除憑證---密碼
            if (this.eraseCredentialsAfterAuthentication && result instanceof CredentialsContainer) {
                ((CredentialsContainer)result).eraseCredentials();
            }
//發(fā)布認(rèn)證成功的結(jié)果
            if (parentResult == null) {
                this.eventPublisher.publishAuthenticationSuccess(result);
            }
//返回新生產(chǎn)的令牌對(duì)象
            return result;
        } else {
        //認(rèn)證失敗
            if (lastException == null) {
                lastException = new ProviderNotFoundException(this.messages.getMessage("ProviderManager.providerNotFound", new Object[]{toTest.getName()}, "No AuthenticationProvider found for {0}"));
            }

            if (parentException == null) {
                this.prepareException((AuthenticationException)lastException, authentication);
            }

            throw lastException;
        }
    }

這個(gè)方法就比較魔幻了,因?yàn)閹缀蹶P(guān)于認(rèn)證的重要邏輯都將在這里完成:

首先獲取 authentication 的 Class,判斷當(dāng)前 provider 是否支持該 authentication。

如果支持,則調(diào)用 provider 的 authenticate方法開(kāi)始做校驗(yàn),校驗(yàn)完成后,會(huì)返回一個(gè)新的Authentication。一會(huì)來(lái)和大家捋這個(gè)方法的具體邏輯

這里的 provider 可能有多個(gè),如果 provider 的 authenticate 方法沒(méi)能正常返回一個(gè)Authentication,則調(diào)用 provider 的 parent 的 authenticate 方法繼續(xù)校驗(yàn)。

copyDetails 方法則用來(lái)把舊的 Token 的 details 屬性拷貝到新的 Token 中來(lái)。

接下來(lái)會(huì)調(diào)用 eraseCredentials 方法擦除憑證信息,也就是你的密碼,這個(gè)擦除方法比較簡(jiǎn)單,就是將 Token 中的credentials 屬性置空

最后通過(guò) publishAuthenticationSuccess 方法將登錄成功的事件廣播出去。

大致的流程,就是上面這樣,在 for 循環(huán)中,第一次拿到的 provider 是一個(gè) AnonymousAuthenticationProvider,這個(gè) provider 壓根就不支持 UsernamePasswordAuthenticationToken,也就是會(huì)直接在 provider.supports 方法中返回 false,結(jié)束 for 循環(huán),然后會(huì)進(jìn)入到下一個(gè) if 中,直接調(diào)用 parent 的 authenticate 方法進(jìn)行校驗(yàn)。

parent 就是 ProviderManager,所以會(huì)再次回到這個(gè) authenticate 方法中。再次回到 authenticate 方法中,provider 也變成了 DaoAuthenticationProvider,這個(gè) provider 是支持 UsernamePasswordAuthenticationToken 的,所以會(huì)順利進(jìn)入到該類的 authenticate 方法去執(zhí)行,而 DaoAuthenticationProvider 繼承自 AbstractUserDetailsAuthenticationProvider 并且沒(méi)有重寫(xiě) authenticate 方法,所以 我們最終來(lái)到 AbstractUserDetailsAuthenticationProvider#authenticate 方法中:

public Authentication authenticate(Authentication authentication)
		throws AuthenticationException {
	String username = (authentication.getPrincipal() == null) ? "NONE_PROVIDED"
			: authentication.getName();
	user = retrieveUser(username,(UsernamePasswordAuthenticationToken) authentication);
	preAuthenticationChecks.check(user);
	additionalAuthenticationChecks(user,(UsernamePasswordAuthenticationToken) authentication);
	postAuthenticationChecks.check(user);
	//如果用戶沒(méi)有使用過(guò),將其放進(jìn)緩存中
	if (!cacheWasUsed) {
            this.userCache.putUserInCache(user);
        }
	Object principalToReturn = user;
	if (forcePrincipalAsString) {
		principalToReturn = user.getUsername();
	}
	return createSuccessAuthentication(principalToReturn, authentication, user);
}

首先從 Authentication 提取出登錄用戶名。

然后通過(guò)拿著 username 去調(diào)用 retrieveUser 方法去獲取當(dāng)前用戶對(duì)象,這一步會(huì)調(diào)用我們自己在登錄時(shí)候的寫(xiě)的 loadUserByUsername 方法,所以這里返回的 user 其實(shí)就是你的登錄對(duì)象

接下來(lái)調(diào)用 preAuthenticationChecks.check 方法去檢驗(yàn) user 中的各個(gè)賬戶狀態(tài)屬性是否正常,例如賬戶是否被禁用、賬戶是否被鎖定、賬戶是否過(guò)期等等

additionalAuthenticationChecks 方法則是做密碼比對(duì)的,好多小伙伴好奇 Spring Security 的密碼加密之后,是如何進(jìn)行比較的,看這里就懂了。

最后在 postAuthenticationChecks.check 方法中檢查密碼是否過(guò)期。

判斷用戶是否在緩存中存在,如果不存在,就放入緩存中

接下來(lái)有一個(gè) forcePrincipalAsString 屬性,這個(gè)是是否強(qiáng)制將 Authentication 中的 principal 屬性設(shè)置為字符串,這個(gè)屬性我們一開(kāi)始在 UsernamePasswordAuthenticationFilter 類中其實(shí)就是設(shè)置為字符串的(即 username),但是默認(rèn)情況下,當(dāng)用戶登錄成功之后, 這個(gè)屬性的值就變成當(dāng)前用戶這個(gè)對(duì)象了。之所以會(huì)這樣,就是因?yàn)?forcePrincipalAsString 默認(rèn)為 false,不過(guò)這塊其實(shí)不用改,就用 false,這樣在后期獲取當(dāng)前用戶信息的時(shí)候反而方便很多。

最后,通過(guò) createSuccessAuthentication 方法構(gòu)建一個(gè)新的 UsernamePasswordAuthenticationToken,此時(shí)認(rèn)證主體就由用戶名變?yōu)榱藆serDetails對(duì)象

好了,那么登錄的校驗(yàn)流程現(xiàn)在就基本和大家捋了一遍了。那么接下來(lái)還有一個(gè)問(wèn)題,登錄的用戶信息我們?nèi)ツ睦锊檎遥?/p>

用戶信息保存

要去找登錄的用戶信息,我們得先來(lái)解決一個(gè)問(wèn)題,就是上面我們說(shuō)了這么多,這一切是從哪里開(kāi)始被觸發(fā)的?

我們來(lái)到 UsernamePasswordAuthenticationFilter 的父類 AbstractAuthenticationProcessingFilter 中,這個(gè)類我們經(jīng)常會(huì)見(jiàn)到,因?yàn)楹芏鄷r(shí)候當(dāng)我們想要在 Spring Security 自定義一個(gè)登錄驗(yàn)證碼或者將登錄參數(shù)改為 JSON 的時(shí)候,我們都需自定義過(guò)濾器繼承自 AbstractAuthenticationProcessingFilter ,毫無(wú)疑問(wèn),UsernamePasswordAuthenticationFilter#attemptAuthentication 方法就是在 AbstractAuthenticationProcessingFilter 類的 doFilter 方法中被觸發(fā)的:

 private void doFilter(HttpServletRequest request, HttpServletResponse response, FilterChain chain) throws IOException, ServletException {
 //不需要認(rèn)證就直接放行
        if (!this.requiresAuthentication(request, response)) {
            chain.doFilter(request, response);
        } else {
            try {
            //獲取認(rèn)證的結(jié)果---null或者新生產(chǎn)的令牌對(duì)象
                Authentication authenticationResult = this.attemptAuthentication(request, response);
               //認(rèn)證失敗
                if (authenticationResult == null) {
                    return;
                }
                    
                this.sessionStrategy.onAuthentication(authenticationResult, request, response);
                if (this.continueChainBeforeSuccessfulAuthentication) {
                    chain.doFilter(request, response);
                }

                this.successfulAuthentication(request, response, chain, authenticationResult);
            } catch (InternalAuthenticationServiceException var5) {
                this.logger.error("An internal error occurred while trying to authenticate the user.", var5);
                this.unsuccessfulAuthentication(request, response, var5);
            } catch (AuthenticationException var6) {
                this.unsuccessfulAuthentication(request, response, var6);
            }

        }
    }

從上面的代碼中,我們可以看到,當(dāng) attemptAuthentication 方法被調(diào)用時(shí),實(shí)際上就是觸發(fā)了 UsernamePasswordAuthenticationFilter#attemptAuthentication 方法,當(dāng)?shù)卿洅伋霎惓5臅r(shí)候,unsuccessfulAuthentication 方法會(huì)被調(diào)用,而當(dāng)?shù)卿洺晒Φ臅r(shí)候,successfulAuthentication 方法則會(huì)被調(diào)用,那我們就來(lái)看一看 successfulAuthentication 方法:

protected void successfulAuthentication(HttpServletRequest request,
		HttpServletResponse response, FilterChain chain, Authentication authResult)
		throws IOException, ServletException {
		//將新生產(chǎn)的令牌對(duì)象放入spring security的上下文環(huán)境中
	SecurityContextHolder.getContext().setAuthentication(authResult);
	rememberMeServices.loginSuccess(request, response, authResult);
	// Fire event
	if (this.eventPublisher != null) {
		eventPublisher.publishEvent(new InteractiveAuthenticationSuccessEvent(
				authResult, this.getClass()));
	}
	successHandler.onAuthenticationSuccess(request, response, authResult);
}

在這里有一段很重要的代碼,就是 SecurityContextHolder.getContext().setAuthentication(authResult); ,登錄成功的用戶信息被保存在這里,也就是說(shuō),在任何地方,如果我們想獲取用戶登錄信息,都可以從 SecurityContextHolder.getContext() 中獲取到,想修改,也可以在這里修改。

最后大家還看到有一個(gè) successHandler.onAuthenticationSuccess,這就是我們?cè)?SecurityConfig 中配置登錄成功回調(diào)方法,就是在這里被觸發(fā)的

當(dāng)認(rèn)證失敗時(shí),會(huì)調(diào)用登錄失敗處理器,并清空上下文環(huán)境中的對(duì)象

 protected void unsuccessfulAuthentication(HttpServletRequest request, HttpServletResponse response, AuthenticationException failed) throws IOException, ServletException {
        SecurityContextHolder.clearContext();
        this.logger.trace("Failed to process authentication request", failed);
        this.logger.trace("Cleared SecurityContextHolder");
        this.logger.trace("Handling authentication failure");
        this.rememberMeServices.loginFail(request, response);
        this.failureHandler.onAuthenticationFailure(request, response, failed);
    } 

以上就是Spring Security 實(shí)現(xiàn)用戶名密碼登錄流程源碼詳解的詳細(xì)內(nèi)容,更多關(guān)于Spring Security 用戶名密碼登錄的資料請(qǐng)關(guān)注腳本之家其它相關(guān)文章!

相關(guān)文章

  • Velocity基本語(yǔ)法介紹

    Velocity基本語(yǔ)法介紹

    以下是對(duì)Velocity的基本語(yǔ)法進(jìn)行了深入的介紹。需要的朋友可以過(guò)來(lái)參考下
    2013-08-08
  • Spring的CorsFilter會(huì)失效的原因及解決方法

    Spring的CorsFilter會(huì)失效的原因及解決方法

    眾所周知CorsFilter是Spring提供的跨域過(guò)濾器,我們可能會(huì)做以下的配置,基本上就是允許任何跨域請(qǐng)求,我利用Spring的CorsFilter做跨域操作但是出現(xiàn)報(bào)錯(cuò),接下來(lái)小編就給大家介紹一Spring的CorsFilter會(huì)失效的原因及解決方法,需要的朋友可以參考下
    2023-09-09
  • Java interceptor攔截器的方法

    Java interceptor攔截器的方法

    java里的攔截器是動(dòng)態(tài)攔截Action調(diào)用的對(duì)象,它提供了一種機(jī)制可使開(kāi)發(fā)者在一個(gè)Action執(zhí)行的先后執(zhí)行一段代碼,也能夠在一個(gè)Action,接下來(lái)通過(guò)本文給大家介紹Java interceptor攔截器的方法,感興趣的朋友一起看看吧
    2022-01-01
  • Dom4j解析xml復(fù)雜多節(jié)點(diǎn)報(bào)文方式

    Dom4j解析xml復(fù)雜多節(jié)點(diǎn)報(bào)文方式

    這篇文章主要介紹了Dom4j解析xml復(fù)雜多節(jié)點(diǎn)報(bào)文方式,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。如有錯(cuò)誤或未考慮完全的地方,望不吝賜教
    2021-09-09
  • java Springboot實(shí)現(xiàn)多文件上傳功能

    java Springboot實(shí)現(xiàn)多文件上傳功能

    這篇文章主要為大家詳細(xì)介紹了java Springboot實(shí)現(xiàn)多文件上傳功能,文中示例代碼介紹的非常詳細(xì),具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下
    2018-08-08
  • Java同步關(guān)鍵字synchronize底層實(shí)現(xiàn)原理解析

    Java同步關(guān)鍵字synchronize底層實(shí)現(xiàn)原理解析

    synchronized關(guān)鍵字對(duì)大家來(lái)說(shuō)并不陌生,當(dāng)我們遇到并發(fā)情況時(shí),優(yōu)先會(huì)想到用synchronized關(guān)鍵字去解決,synchronized確實(shí)能夠幫助我們?nèi)ソ鉀Q并發(fā)的問(wèn)題,接下來(lái)通過(guò)本文給大家分享java synchronize底層實(shí)現(xiàn)原理,感興趣的朋友一起看看吧
    2021-08-08
  • Spring Boot詳細(xì)打印啟動(dòng)時(shí)異常堆棧信息詳析

    Spring Boot詳細(xì)打印啟動(dòng)時(shí)異常堆棧信息詳析

    這篇文章主要給大家介紹了關(guān)于Spring Boot詳細(xì)打印啟動(dòng)時(shí)異常堆棧信息的相關(guān)資料,文中通過(guò)示例代碼介紹的非常詳細(xì),對(duì)大家學(xué)習(xí)或者使用Spring Boot具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面來(lái)一起學(xué)習(xí)學(xué)習(xí)吧
    2019-10-10
  • Java?實(shí)戰(zhàn)項(xiàng)目之學(xué)生信息管理系統(tǒng)的實(shí)現(xiàn)流程

    Java?實(shí)戰(zhàn)項(xiàng)目之學(xué)生信息管理系統(tǒng)的實(shí)現(xiàn)流程

    讀萬(wàn)卷書(shū)不如行萬(wàn)里路,只學(xué)書(shū)上的理論是遠(yuǎn)遠(yuǎn)不夠的,只有在實(shí)戰(zhàn)中才能獲得能力的提升,本篇文章手把手帶你用java+SSM+jsp+mysql+maven實(shí)現(xiàn)學(xué)生信息管理系統(tǒng),大家可以在過(guò)程中查缺補(bǔ)漏,提升水平
    2021-11-11
  • Spring結(jié)合WebSocket實(shí)現(xiàn)實(shí)時(shí)通信的教程詳解

    Spring結(jié)合WebSocket實(shí)現(xiàn)實(shí)時(shí)通信的教程詳解

    WebSocket?是基于TCP/IP協(xié)議,獨(dú)立于HTTP協(xié)議的通信協(xié)議,本文將使用Spring結(jié)合WebSocket實(shí)現(xiàn)實(shí)時(shí)通信功能,有需要的小伙伴可以參考一下
    2024-01-01
  • 解決@Test注解在Maven工程的Test.class類中無(wú)法使用的問(wèn)題

    解決@Test注解在Maven工程的Test.class類中無(wú)法使用的問(wèn)題

    這篇文章主要介紹了解決@Test注解在Maven工程的Test.class類中無(wú)法使用的問(wèn)題,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。如有錯(cuò)誤或未考慮完全的地方,望不吝賜教
    2022-03-03

最新評(píng)論