、介紹
創建 MyControllerAdvice,并添加 @ControllerAdvice注解。
package com.sam.demo.controller; import org.springframework.ui.Model; import org.springframework.web.bind.WebDataBinder; import org.springframework.web.bind.annotation.*; import java.util.HashMap; import java.util.Map; @ControllerAdvice public class MyControllerAdvice { /** * 應用到所有@RequestMapping注解方法,在其執行之前初始化數據綁定器 * @param binder */ @InitBinder public void initBinder(WebDataBinder binder) {} /** * 把值綁定到Model中,使全局@RequestMapping可以獲取到該值 * @param model */ @ModelAttribute public void addAttributes(Model model) { model.addAttribute("author", "Magical Sam"); } /** * 全局異常捕捉處理 * @param ex * @return */ @ResponseBody @ExceptionHandler(value=Exception.class) public Map errorHandler(Exception ex) { Map map=new HashMap(); map.put("code", 100); map.put("msg", ex.getMessage()); return map; } }
啟動應用后,被 @ExceptionHandler、@InitBinder、@ModelAttribute 注解的方法,都會作用在 被 @RequestMapping 注解的方法上。
@ModelAttribute:在Model上設置的值,對于所有被 @RequestMapping 注解的方法中,都可以通過 ModelMap 獲取,如下:
@RequestMapping("/home") public String home(ModelMap modelMap) { System.out.println(modelMap.get("author")); } //或者 通過@ModelAttribute獲取 @RequestMapping("/home") public String home(@ModelAttribute("author") String author) { System.out.println(author); }
@ExceptionHandler 攔截了異常,我們可以通過該注解實現自定義異常處理。其中,@ExceptionHandler 配置的 value 指定需要攔截的異常類型,上面攔截了 Exception.class 這種異常。
二、自定義異常處理(全局異常處理)
spring boot 默認情況下會映射到 /error 進行異常處理,但是提示并不十分友好,下面自定義異常處理,提供友好展示。
1、編寫自定義異常類:
package com.sam.demo.custom; public class MyException extends RuntimeException { public MyException(String code, String msg) { this.code=code; this.msg=msg; } private String code; private String msg; // getter & setter }
注:spring 對于 RuntimeException 異常才會進行事務回滾。
2、編寫全局異常處理類
創建 MyControllerAdvice.java,如下:
package com.sam.demo.controller; import org.springframework.ui.Model; import org.springframework.web.bind.WebDataBinder; import org.springframework.web.bind.annotation.*; import java.util.HashMap; import java.util.Map; @ControllerAdvice public class MyControllerAdvice { /** * 全局異常捕捉處理 * @param ex * @return */ @ResponseBody @ExceptionHandler(value=Exception.class) public Map errorHandler(Exception ex) { Map map=new HashMap(); map.put("code", 100); map.put("msg", ex.getMessage()); return map; } /** * 攔截捕捉自定義異常 MyException.class * @param ex * @return */ @ResponseBody @ExceptionHandler(value=MyException.class) public Map myErrorHandler(MyException ex) { Map map=new HashMap(); map.put("code", ex.getCode()); map.put("msg", ex.getMsg()); return map; } }
3、controller中拋出異常進行測試。
@RequestMapping("/home") public String home() throws Exception { // throw new Exception("Sam 錯誤"); throw new MyException("101", "Sam 錯誤"); }
啟動應用,訪問:http://localhost:8080/home ,正常顯示以下json內容,證明自定義異常已經成功被攔截。
{"msg":"Sam 錯誤","code":"101"}
* 如果不需要返回json數據,而要渲染某個頁面模板返回給瀏覽器,那么MyControllerAdvice中可以這么實現:
@ExceptionHandler(value=MyException.class) public ModelAndView myErrorHandler(MyException ex) { ModelAndView modelAndView=new ModelAndView(); modelAndView.setViewName("error"); modelAndView.addObject("code", ex.getCode()); modelAndView.addObject("msg", ex.getMsg()); return modelAndView; }
在 templates 目錄下,添加 error.ftl(這里使用freemarker) 進行渲染:
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <title>錯誤頁面</title> </head> <body> <h1>${code}</h1> <h1>${msg}</h1> </body> </html>
重啟應用,http://localhost:8080/home 顯示自定的錯誤頁面內容。
補充:如果全部異常處理返回json,那么可以使用 @RestControllerAdvice 代替 @ControllerAdvice ,這樣在方法上就可以不需要添加 @ResponseBody。
文通過一個簡易安全認證示例的開發實踐,理解過濾器和攔截器的工作原理。
很多文章都將過濾器(Filter)、攔截器(Interceptor)和監聽器(Listener)這三者和Spring關聯起來講解,并認為過濾器(Filter)、攔截器(Interceptor)和監聽器(Listener)是Spring提供的應用廣泛的組件功能。
但是嚴格來說,過濾器和監聽器屬于Servlet范疇的API,和Spring沒什么關系。
因為過濾器繼承自javax.servlet.Filter接口,監聽器繼承自javax.servlet.ServletContextListener接口,只有攔截器繼承的是org.springframework.web.servlet.HandlerInterceptor接口。
上面的流程圖參考自網上資料,一圖勝千言。看完本文以后,將對過濾器和攔截器的調用過程會有更深刻理解。
一、安全認證設計思路
有時候內外網調用API,對安全性的要求不一樣,很多情況下外網調用API的種種限制在內網根本沒有必要,但是網關部署的時候,可能因為成本和復雜度等問題,內外網要調用的API會部署在一起。
實現REST接口的安全性,可以通過成熟框架如Spring Security或者shiro搞定。
但是因為安全框架往往實現復雜(我數了下Spring Security,洋洋灑灑大概有11個核心模塊,shiro的源碼代碼量也比較驚人)同時可能要引入復雜配置(能不能讓人痛快一點),不利于中小團隊的靈活快速開發、部署及問題排查。
很多團隊自己造輪子實現安全認證,本文這個簡易認證示例參考自我所在的前廠開發團隊,可以認為是個基于token的安全認證服務。
大致設計思路如下:
1、自定義http請求頭,每次調用API都在請求頭里傳人一個token值
2、token放在緩存(如redis)中,根據業務和API的不同設置不同策略的過期時間
3、token可以設置白名單和黑名單,可以限制API調用頻率,便于開發和測試,便于緊急處理異狀,甚至臨時關閉API
4、外網調用必須傳人token,token可以和用戶有關系,比如每次打開頁面或者登錄生成token寫入請求頭,頁面驗證cookie和token有效性等
在Spring Security框架里有兩個概念,即認證和授權,認證指可以訪問系統的用戶,而授權則是用戶可以訪問的資源。
實現上述簡易安全認證需求,你可能需要獨立出一個token服務,保證生成token全局唯一,可能包含的模塊有自定義流水生成器、CRM、加解密、日志、API統計、緩存等,但是和用戶(CRM)其實是弱綁定關系。某些和用戶有關系的公共服務,比如我們經常用到的發送短信SMS和郵件服務,也可以通過token機制解決安全調用問題。
綜上,本文的簡易安全認證其實和Spring Security框架提供的認證和授權有點不一樣,當然,這種“安全”處理方式對專業人士沒什么新意,但是可以對外擋掉很大一部分小白用戶。
二、自定義過濾器
和Spring MVC類似,Spring Boot提供了很多servlet過濾器(Filter)可使用,并且它自動添加了一些常用過濾器,比如CharacterEncodingFilter(用于處理編碼問題)、HiddenHttpMethodFilter(隱藏HTTP函數)、HttpPutFormContentFilter(form表單處理)、RequestContextFilter(請求上下文)等。通常我們還會自定義Filter實現一些通用功能,比如記錄日志、判斷是否登錄、權限驗證等。
1、自定義請求頭
很簡單,在request header添加自定義請求頭authtoken:
@RequestMapping(value="/getinfobyid", method=RequestMethod.POST) @ApiOperation("根據商品Id查詢商品信息") @ApiImplicitParams({ @ApiImplicitParam(paramType="header", name="authtoken", required=true, value="authtoken", dataType="String"), }) public GetGoodsByGoodsIdResponse getGoodsByGoodsId(@RequestHeader String authtoken, @RequestBody GetGoodsByGoodsIdRequest request) { return _goodsApiService.getGoodsByGoodsId(request); }
加了@RequestHeader修飾的authtoken字段就可以在swagger這樣的框架下顯示出來。
調用后,可以根據http工具看到請求頭,本文示例是authtoken(和某些框架的token區分開):
備注:很多httpclient工具都支持動態傳人請求頭,比如RestTemplate。
2、實現Filter
Filter接口共有三個方法,即init,doFilter和destory,看到名稱就大概知道它們主要用途了,通常我們只要在doFilter這個方法內,對Http請求進行處理:
package com.power.demo.controller.filter; import com.power.demo.common.AppConst; import com.power.demo.common.BizResult; import com.power.demo.service.contract.AuthTokenService; import com.power.demo.util.PowerLogger; import com.power.demo.util.SerializeUtil; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Component; import javax.servlet.*; import javax.servlet.http.HttpServletRequest; import java.io.IOException; @Component public class AuthTokenFilter implements Filter { @Autowired private AuthTokenService authTokenService; @Override public void init(FilterConfig var1) throws ServletException { } @Override public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException { HttpServletRequest req=(HttpServletRequest) request; String token=req.getHeader(AppConst.AUTH_TOKEN); BizResult<String> bizResult=authTokenService.powerCheck(token); System.out.println(SerializeUtil.Serialize(bizResult)); if (bizResult.getIsOK()==true) { PowerLogger.info("auth token filter passed"); chain.doFilter(request, response); } else { throw new ServletException(bizResult.getMessage()); } } @Override public void destroy() { } }
注意,Filter這樣的東西,我認為從實際分層角度,多數處理的還是表現層偏多,不建議在Filter中直接使用數據訪問層Dao,雖然這樣的代碼一兩年前我在很多老古董項目中看到過很多次,而且<<Spring實戰>>的書里也有這樣寫的先例。
3、認證服務
這里就是主要業務邏輯了,示例代碼只是簡單寫下思路,不要輕易就用于生產環境:
package com.power.demo.service.impl; import com.power.demo.cache.PowerCacheBuilder; import com.power.demo.common.BizResult; import com.power.demo.service.contract.AuthTokenService; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Component; import org.springframework.util.StringUtils; @Component public class AuthTokenServiceImpl implements AuthTokenService { @Autowired private PowerCacheBuilder cacheBuilder; /* * 驗證請求頭token是否合法 * */ @Override public BizResult<String> powerCheck(String token) { BizResult<String> bizResult=new BizResult<>(true, "驗證通過"); System.out.println("token的值為:" + token); if (StringUtils.isEmpty(token)==true) { bizResult.setFail("authtoken為空"); return bizResult; } //處理黑名單 bizResult=checkForbidList(token); if (bizResult.getIsOK()==false) { return bizResult; } //處理白名單 bizResult=checkAllowList(token); if (bizResult.getIsOK()==false) { return bizResult; } String key=String.format("Power.AuthTokenService.%s", token); //cacheBuilder.set(key, token); //cacheBuilder.set(key, token.toUpperCase()); //從緩存中取 String existToken=cacheBuilder.get(key); if (StringUtils.isEmpty(existToken)==true) { bizResult.setFail(String.format("不存在此authtoken:%s", token)); return bizResult; } //比較token是否相同 Boolean isEqual=token.equals(existToken); if (isEqual==false) { bizResult.setFail(String.format("不合法的authtoken:%s", token)); return bizResult; } //do something return bizResult; } }
用到的緩存服務可以參考這里,這個也是我在前廠的經驗總結。
4、注冊Filter
常見的有兩種寫法:
(1)、使用@WebFilter注解來標識Filter
@Order(1) @WebFilter(urlPatterns={"/api/v1/goods/*", "/api/v1/userinfo/*"}) public class AuthTokenFilter implements Filter {
使用@WebFilter注解,還可以配合使用@Order注解,@Order注解表示執行過濾順序,值越小,越先執行,這個Order大小在我們編程過程中就像處理HTTP請求的生命周期一樣大有用處。當然,如果沒有指定Order,則過濾器的調用順序跟添加的過濾器順序相反,過濾器的實現是責任鏈模式。
最后,在啟動類上添加@ServletComponentScan 注解即可正常使用自定義過濾器了。
(2)、使用FilterRegistrationBean對Filter進行自定義注冊
本文以第二種實現自定義Filter注冊:
package com.power.demo.controller.filter; import com.google.common.collect.Lists; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.web.servlet.FilterRegistrationBean; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.stereotype.Component; import java.util.List; @Configuration @Component public class RestFilterConfig { @Autowired private AuthTokenFilter filter; @Bean public FilterRegistrationBean filterRegistrationBean() { FilterRegistrationBean registrationBean=new FilterRegistrationBean(); registrationBean.setFilter(filter); //設置(模糊)匹配的url List<String> urlPatterns=Lists.newArrayList(); urlPatterns.add("/api/v1/goods/*"); urlPatterns.add("/api/v1/userinfo/*"); registrationBean.setUrlPatterns(urlPatterns); registrationBean.setOrder(1); registrationBean.setEnabled(true); return registrationBean; } }
請大家特別注意urlPatterns,屬性urlPatterns指定要過濾的URL模式。對于Filter的作用區域,這個參數居功至偉。
注冊好Filter,當Spring Boot啟動時監測到有javax.servlet.Filter的bean時就會自動加入過濾器調用鏈ApplicationFilterChain。
調用一個API試試效果:
通常情況下,我們在Spring Boot下都會自定義一個全局統一的異常管理增強GlobalExceptionHandler(和上面這個顯示會略有不同)。
根據我的實踐,過濾器里拋出異常,不會被全局唯一的異常管理增強捕獲到并進行處理,這個和攔截器Inteceptor以及下一篇文章介紹的自定義AOP攔截不同。
到這里,一個通過自定義Filter實現的簡易安全認證服務就搞定了。
三、自定義攔截器
1、實現攔截器
繼承接口HandlerInterceptor,實現攔截器,接口方法有下面三個:
preHandle是請求執行前執行
postHandle是請求結束執行
afterCompletion是視圖渲染完成后執行
package com.power.demo.controller.interceptor; import com.power.demo.common.AppConst; import com.power.demo.common.BizResult; import com.power.demo.service.contract.AuthTokenService; import com.power.demo.util.PowerLogger; import com.power.demo.util.SerializeUtil; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Component; import org.springframework.web.servlet.HandlerInterceptor; import org.springframework.web.servlet.ModelAndView; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; /* * 認證token攔截器 * */ @Component public class AuthTokenInterceptor implements HandlerInterceptor { @Autowired private AuthTokenService authTokenService; /* * 請求執行前執行 * */ @Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { boolean handleResult=false; String token=request.getHeader(AppConst.AUTH_TOKEN); BizResult<String> bizResult=authTokenService.powerCheck(token); System.out.println(SerializeUtil.Serialize(bizResult)); handleResult=bizResult.getIsOK(); PowerLogger.info("auth token interceptor攔截結果:" + handleResult); if (bizResult.getIsOK()==true) { PowerLogger.info("auth token interceptor passed"); } else { throw new Exception(bizResult.getMessage()); } return handleResult; } /* * 請求結束執行 * */ @Override public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception { } /* * 視圖渲染完成后執行 * */ @Override public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception { } }
示例中,我們選擇在請求執行前進行token安全認證。
認證服務就是過濾器里介紹的AuthTokenService,業務邏輯層實現復用。
2、注冊攔截器
定義一個InterceptorConfig類,繼承自WebMvcConfigurationSupport,WebMvcConfigurerAdapter已經過時。
將AuthTokenInterceptor作為bean注入,其他設置攔截器攔截的URL和過濾器非常相似:
package com.power.demo.controller.interceptor; import com.google.common.collect.Lists; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.stereotype.Component; import org.springframework.web.servlet.config.annotation.DefaultServletHandlerConfigurer; import org.springframework.web.servlet.config.annotation.InterceptorRegistry; import org.springframework.web.servlet.config.annotation.ResourceHandlerRegistry; import org.springframework.web.servlet.config.annotation.WebMvcConfigurationSupport; import java.util.List; @Configuration @Component public class InterceptorConfig extends WebMvcConfigurationSupport { //WebMvcConfigurerAdapter已經過時 private static final String FAVICON_URL="/favicon.ico"; /** * 發現如果繼承了WebMvcConfigurationSupport,則在yml中配置的相關內容會失效。 * * @param registry */ @Override public void addResourceHandlers(ResourceHandlerRegistry registry) { registry.addResourceHandler("/").addResourceLocations("/**"); registry.addResourceHandler("/static/**").addResourceLocations("classpath:/static/"); } /** * 配置servlet處理 */ @Override public void configureDefaultServletHandling(DefaultServletHandlerConfigurer configurer) { configurer.enable(); } @Override public void addInterceptors(InterceptorRegistry registry) { //設置(模糊)匹配的url List<String> urlPatterns=Lists.newArrayList(); urlPatterns.add("/api/v1/goods/*"); urlPatterns.add("/api/v1/userinfo/*"); registry.addInterceptor(authTokenInterceptor()).addPathPatterns(urlPatterns).excludePathPatterns(FAVICON_URL); super.addInterceptors(registry); } //將攔截器作為bean寫入配置中 @Bean public AuthTokenInterceptor authTokenInterceptor() { return new AuthTokenInterceptor(); } }
啟動應用后,調用接口就可以看到攔截器攔截的效果了。全局統一的異常管理GlobalExceptionHandler捕獲異常后處理如下:
和過濾器顯示的主要錯誤提示信息幾乎一樣,但是堆棧信息更加豐富。
四、過濾器和攔截器區別
主要區別如下:
1、攔截器主要是基于java的反射機制的,而過濾器是基于函數回調
2、攔截器不依賴于servlet容器,過濾器依賴于servlet容器
3、攔截器只能對action請求起作用,而過濾器則可以對幾乎所有的請求起作用
4、攔截器可以訪問action上下文、值棧里的對象,而過濾器不能訪問
5、在action的生命周期中,攔截器可以多次被調用,而過濾器只能在容器初始化時被調用一次
參考過的一些文章,有的說“攔截器可以獲取IOC容器中的各個bean,而過濾器就不行,這點很重要,在攔截器里注入一個service,可以調用業務邏輯”,經過實際驗證,這是不對的。
注意:過濾器的觸發時機是容器后,servlet之前,所以過濾器的doFilter(ServletRequest request, ServletResponse response, FilterChain chain)的入參是ServletRequest,而不是HttpServletRequest,因為過濾器是在HttpServlet之前。下面這個圖,可以讓你對Filter和Interceptor的執行時機有更加直觀的認識:
只有經過DispatcherServlet 的請求,才會走攔截器鏈,自定義的Servlet請求是不會被攔截的,比如我們自定義的Servlet地址http://localhost:9090/testServlet是不會被攔截器攔截的。但不管是屬于哪個Servlet,只要符合過濾器的過濾規則,過濾器都會執行。
根據上述分析,理解原理,實際操作就簡單了,哪怕是ASP.NET過濾器亦然。
問題:實現更加靈活的安全認證
在Java Web下通過自定義過濾器Filter或者攔截器Interceptor配置urlPatterns,可以實現對特定匹配的API進行安全認證,比如匹配所有API、匹配某個或某幾個API等,但是有時候這種匹配模式對開發人員相對不夠友好。
我們可以參考Spring Security那樣,通過注解+SpEL實現強大功能。
又比如在ASP.NET中,我們經常用到Authorized特性,這個特性可以加在類上,也可以作用于方法上,可以更加動態靈活地控制安全認證。
我們沒有選擇Spring Security,那就自己實現類似Authorized的靈活的安全認證,主要實現技術就是我們所熟知的AOP。
通過AOP方式實現更靈活的攔截的基礎知識本文就先不提了,更多的關于AOP的話題將在下篇文章分享。
原文:https://www.cnblogs.com/jeffwongishandsome/p/spring-boot-use-filter-and-interceptor-to-implement-an-easy-auth-system.html
言
同源策略:判斷是否是同源的,主要看這三點,協議,ip,端口。
同源策略就是瀏覽器出于網站安全性的考慮,限制不同源之間的資源相互訪問的一種政策。
比如在域名https://www.baidu.com下,腳本不能夠訪問https://www.sina.com源下的資源,否則將會被瀏覽器攔截。
注意兩點:
1.必須是腳本請求,比如AJAX請求。
但是如下情況不會產生跨域攔截
<img src="xxx"/> <a href='xxx"> </a>
2.跨域攔截是前端請求已經發出,并且在后端返回響應時檢查相關參數,是否允許接收后端請求。
在微服務開發中,一個系統包含多個微服務,會存在跨域請求的場景。
本文主要講解SpringBoot解決跨域請求攔截的問題。
搭建項目
這里創建兩個web項目,web1 和 web2.
web2項目請求web1項目的資源。
這里只貼關鍵代碼,完整代碼參考GitHub
WEB2
創建一個Controller返回html頁面
@Slf4j @Controller public class HomeController { @RequestMapping("/index") public String home(){ log.info("/index"); return "/home"; } }
html頁面 home.html
這里創建了一個按鈕,按鈕按下則請求資源:"http://localhost:8301/hello"
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <title>web2</title> <script src="https://cdn.staticfile.org/jquery/1.10.2/jquery.min.js"> </script> <script> $(function () { $("#testBtn").click(function () { console.log("testbtn ..."); $.get("http://localhost:8301/hello",function(data,status){ alert("數據: " + data + "\n狀態: " + status); }); }) }) </script> </head> <body> web2 <button id="testBtn">測試</button> </body> </html>
WEB1
@Slf4j @RestController public class Web1Controller { @RequestMapping("/hello") public String hello(){ log.info("hello "); return "hello," + new Date().toString(); } }
這里配置兩個項目為不同的端口。
WEB1為8301
WEB2為8302
因此是不同源的。
測試
在web1還沒有配置允許跨域訪問的情況下
按下按鈕,將會出現錯誤。顯示Header中沒有Access-Control-Allow-Origin
Access to XMLHttpRequest at 'http://localhost:8301/hello' from origin 'http://localhost:8300' has been blocked by CORS policy: No 'Access-Control-Allow-Origin' header is present on the requested resource.
WEB1添加允許跨域請求,通過實現WebMvcConfigurer
@Configuration public class WebMvcConfig implements WebMvcConfigurer { @Override public void addCorsMappings(CorsRegistry registry) { registry.addMapping("/hello"); } }
再次訪問將會返回正常數據。
除了以上的配置外,還可以做更細致的限制
比如對請求的headers,請求的方法POST/GET...。請求的源進行限制。
同時還可以使用注解 @CrossOrigin來替換上面的配置。
@Slf4j @RestController public class Web1Controller { @CrossOrigin @RequestMapping("/hello") public String hello(){ log.info("hello "); return "hello," + new Date().toString(); } }
注解可以用在類上,也可以用在方法上,但必須是控制器類
配置和上面一樣,也是可以對方法,header,源進行個性化限制。
@Target({ElementType.METHOD, ElementType.TYPE}) @Retention(RetentionPolicy.RUNTIME) @Documented public @interface CrossOrigin { /** @deprecated */ @Deprecated String[] DEFAULT_ORIGINS=new String[]{"*"}; /** @deprecated */ @Deprecated String[] DEFAULT_ALLOWED_HEADERS=new String[]{"*"}; /** @deprecated */ @Deprecated boolean DEFAULT_ALLOW_CREDENTIALS=false; /** @deprecated */ @Deprecated long DEFAULT_MAX_AGE=1800L; @AliasFor("origins") String[] value() default {}; @AliasFor("value") String[] origins() default {}; String[] allowedHeaders() default {}; String[] exposedHeaders() default {}; RequestMethod[] methods() default {}; String allowCredentials() default ""; long maxAge() default -1L; }
歡迎工作一到五年的Java工程師朋友們加入Java程序員開發: 721575865
群內提供免費的Java架構學習資料(里面有高可用、高并發、高性能及分布式、Jvm性能調優、Spring源碼,MyBatis,Netty,Redis,Kafka,Mysql,Zookeeper,Tomcat,Docker,Dubbo,Nginx等多個知識點的架構資料)合理利用自己每一分每一秒的時間來學習提升自己,不要再用"沒有時間“來掩飾自己思想上的懶惰!趁年輕,使勁拼,給未來的自己一個交代!
*請認真填寫需求信息,我們會在24小時內與您取得聯系。