一文助您成為Java.Net雙平臺高手
寫在前面:本文乃標(biāo)題黨,不是月經(jīng)貼,側(cè)重于Web開發(fā)差異,或細(xì)節(jié)或概述,若有不對之處,還請各位讀者本著友好互助的心態(tài)批評指正。由于博客園中.Neter較多(個人感覺),因此本文也可以作為.Neter到Java開發(fā)的快速入門。
恕本文的不嚴(yán)謹(jǐn),評論里有說到.net core的,其實(shí)可看作是另一個平臺。雖然.net core目前社區(qū)討論較多,但畢竟出生不久(相對來說),市場體量應(yīng)該還遠(yuǎn)未達(dá)到傳統(tǒng).NET。所以本文仍基于傳統(tǒng).NET描述,但部分文字也適用于.net core。另鄙人對微軟的開源策略亦持樂觀態(tài)度,在總述中稍有提及。
總述
在.Net開發(fā)中,微軟官方框架類可以很好的解決的大部分問題,開發(fā)人員可以心安理得的在一畝三分地騰挪躲閃出花來;偶有一些優(yōu)(zhao)秀(chao)的開源庫,各庫的關(guān)注點(diǎn)也基本不會重樣;所以.Neter只要按部就班即可。而Java喜歡定義各種規(guī)范,各路大神各自實(shí)現(xiàn),因此一個概念常常會有很多的第三方庫,雖然有Spring這種殺手級框架,不過基于IOC和AOP的設(shè)定,Spring家族也變得異常龐大,在編碼時需要引入大量的annotation來織入邏輯;雖然貌似最大程度的解耦了各組件,但導(dǎo)致代碼的可讀性和可調(diào)試性非常不好,碎片化非常嚴(yán)重。不過也因?yàn)槿绱耍琂ava社區(qū)成為設(shè)計思想的孕育地,并常常出現(xiàn)一些讓人擊節(jié)的設(shè)計模式。其中的概念傳播到隔壁.Net圈,圈內(nèi)小白往往一臉懵逼,而少數(shù)大佬不管不顧拿來套用,往往是用錯了,或者讓人不知所以。
籠統(tǒng)來說,.Net框架隱藏細(xì)節(jié),簡便清晰,套路單一,但常陷入知其然不知其所以然的懵逼境地;Java&Spring注解隱藏細(xì)節(jié),概念繁多,沒有方向感或有被繞暈的風(fēng)險,但一旦破位而出,則縱橫捭闔天地之大可任意施展至其它平臺。不過兩者差異隨著.Net的開源以肉眼不可見的速度緩慢消失,特別是最近幾年,.Net在語法層面已經(jīng)超越了Java良多,Java雖然一時半會抹不開面子,但也一直在改進(jìn)。到的本文撰寫時分,借用不知名網(wǎng)友語:“C#語法已經(jīng)達(dá)到Java20,用戶量撐死Java7,生態(tài)Java1.4”。
兩者競爭主要集中在Web開發(fā)領(lǐng)域。目前在該領(lǐng)域,Spring Boot已基本成為事實(shí)上Java平臺的“官方框架”,我想大部分開發(fā)人員并不會在意背后的實(shí)現(xiàn)細(xì)節(jié),從這個方面來講,兩個平臺的開發(fā)模式有一定程度的相似。
數(shù)據(jù)持久層
為啥這節(jié)標(biāo)題不是ORM呢?畢竟ORM現(xiàn)在是業(yè)界標(biāo)準(zhǔn),很難想象這個時代還需要手寫SQL,還需要手動操作JDBC/ADO;如果你打算這么干,一定會被年輕一輩打心眼里鄙視:)
Java
ORM:十多年前,Hibernate就開始興起,它提供了半對象化的HQL和完全的面向?qū)ο驫BC。之后也出現(xiàn)了其它一些ORM比如TopLink。
JPA:JDK5引入,是SUN公司為了統(tǒng)一目前眾多ORM而提出的ORM規(guī)范(又犯了定義規(guī)范的癮)。這個規(guī)范出來后,很多ORM表示支持,但以前的還得維護(hù)啊,所以像Hibernate就另外建了一個分支叫Hibernate JPA。網(wǎng)友benjaminlee1所言:“JPA的出現(xiàn)只是用于規(guī)范現(xiàn)有的ORM技術(shù),它不能取代現(xiàn)有的Hibernate等ORM框架,相反,采用JPA開發(fā)時,我們?nèi)詫⑹褂眠@些ORM框架,只是此時開發(fā)出來的應(yīng)用不在依賴于某個持久化提供商。應(yīng)用可以在不修改代碼的情況下載任何JPA環(huán)境下運(yùn)行,真正做到低耦合,可擴(kuò)展的程序設(shè)計。類似于JDBC,在JDBC出現(xiàn)以前,我們的程序針對特性的數(shù)據(jù)庫API進(jìn)行編程,但是現(xiàn)在我們只需要針對JDBC API編程,這樣能夠在不改變代碼的情況下就能換成其他的數(shù)據(jù)庫。”
Spring Data JPA:有了JPA,我們就可以不在意使用哪個ORM了,但是Spring Data JPA更進(jìn)一步(為Spring家族添磚加瓦),按約定的方式自動給我們生成持久化代碼,當(dāng)然它底層還是要依賴各路ORM的。相關(guān)資料:使用 Spring Data JPA 簡化 JPA 開發(fā)
Mybatis:隨著時間的流逝,Hibernate曾經(jīng)帶來的榮耀已經(jīng)被臃腫丑陋的配置文件,無法優(yōu)化的查詢語句淹沒。很多人開始懷念可一手掌控數(shù)據(jù)操作的時代,于是Mybatis出現(xiàn)了。Mybatis不是一個完整的ORM,它只完成了數(shù)據(jù)庫返回結(jié)果到對象的映射,而存取邏輯仍為SQL,寫在Mapper文件中,它提供的語法在一定程度上簡化了SQL的編寫,最后Mybatis將SQL邏輯映射到接口方法上(在Mapper文件中指定<mapper namespace="xxx">,其中xxx為映射的DAO接口)。針對每個表寫通用增刪改查的Mapper SQL既枯燥又易出錯,所以出現(xiàn)了Mybatis-Generator之類的代碼生成工具,它能基于數(shù)據(jù)表生成實(shí)體類、基本CRUD的Mapper文件、對應(yīng)的DAOInterface。
Mybatis-Plus:在Mybatis的基礎(chǔ)上,提供了諸如分頁、復(fù)雜條件查詢等功能,基礎(chǔ)CRUD操作不需要額外寫SQL Mapper了,只要DAO接口繼承BaseMapper接口即可。當(dāng)然為了方便,它也提供了自己的代碼生成器。
.NET
ORM:主流Entity Framework,除開ORM功能外,它還提供了Code first、DB first、T4代碼生成等特性。性能上與Hibernate一個等級,但使用便捷性和功能全面性較好,更別說還有l(wèi)inq的加持。
認(rèn)證&授權(quán)&鑒權(quán)
認(rèn)證是檢測用戶/請求是否合法,授權(quán)是賦予合法用戶相應(yīng)權(quán)限,鑒權(quán)是鑒別用戶是否有請求某項(xiàng)資源的權(quán)限(認(rèn)證和授權(quán)一般是同時完成)。我們以web為例。
C#/Asp.net mvc
提供了兩個Filter:IAuthenticationFilter 和 AuthorizeAttribute,前者用于認(rèn)證授權(quán),后者用于鑒權(quán)。
1 //IAuthenticationFilter 認(rèn)證,認(rèn)證是否合法用戶
2 public class AdminAuthenticationFilter : ActionFilterAttribute, IAuthenticationFilter
3 {
4 public void OnAuthentication(AuthenticationContext filterContext)
5 {
6 IPrincipal user = filterContext.Principal;
7 if (user == null || !user.Identity.IsAuthenticated)
8 {
9 HttpCookie authCookie = filterContext.HttpContext.Request.Cookies[FormsAuthentication.FormsCookieName];
10 if (authCookie != null)
11 {
12 FormsAuthenticationTicket ticket = FormsAuthentication.Decrypt(authCookie.Value);
13 if (ticket != null && !string.IsNullOrEmpty(ticket.UserData))
14 {
15 var userId = Convert.ToInt32(ticket.UserData);
16 user = EngineContext.Resolve<PFManagerService>().GetManager(userId);
17 filterContext.Principal = user; //后續(xù)會傳遞給HttpContext.Current.User
18 }
19 }
20 }
21 }
22
23 public void OnAuthenticationChallenge(AuthenticationChallengeContext filterContext)
24 {
25 // 認(rèn)證失敗執(zhí)行
26 }
27 }
認(rèn)證成功后,將user賦給filterContext.Principal(第17行),filterContext.Principal接收一個IPrincipal接口對象,該接口有個 bool IsInRole(string role) 方法,用于后續(xù)的鑒權(quán)過程。
1 public class AdminAuthorizationFilter : AuthorizeAttribute
2 {
3 public override void OnAuthorization(AuthorizationContext filterContext)
4 {
5 //childaction不用授權(quán)
6 if (filterContext.IsChildAction)
7 return;
8
9 if (!filterContext.ActionDescriptor.IsDefined(typeof(AllowAnonymousAttribute), true) && !filterContext.ActionDescriptor.ControllerDescriptor.IsDefined(typeof(AllowAnonymousAttribute), true))
10 {
11 if (filterContext.HttpContext.User != null && filterContext.HttpContext.User.Identity.IsAuthenticated)
12 {
13 var controllerName = filterContext.RouteData.Values["controller"].ToString().ToLower();
14 var actionName = filterContext.RouteData.Values["action"].ToString().ToLower();
15 //只要登錄,則都能訪問工作臺
16 if (controllerName.ToLower() == "home" && actionName.ToLower() == "index")
17 this.Roles = string.Empty;
18 else
19 {
20 var roleIds = EngineContext.Resolve<BEModuleService>().GetRoleIdsHasModuleAuthorization(controllerName, actionName, MasonPlatformType.AdminPlatform);
21 if (roleIds == null)
22 {
23 filterContext.Result = new HttpNotFoundResult();
24 return;
25 }
26 //將資源所需權(quán)限賦給成員變量Roles
27 this.Roles = string.Join(",", roleIds);
28 }
29 }
30 }
31
32 base.OnAuthorization(filterContext);
33 }
34 }
注意第27行,我們將擁有該資源的所有權(quán)限賦給Roles,之后AuthorizeAttribute會循環(huán)Roles,依次調(diào)用當(dāng)前用戶(上述的filterContext.Principal)的IsInRole方法,若其中一個返回true則表明用戶有訪問當(dāng)前資源的權(quán)限。
Java/Spring Security
也提供了兩個類,一個Filter和一個Interceptor:AuthenticationProcessingFilter用于用戶認(rèn)證授權(quán),AbstractSecurityInterceptor用于鑒權(quán)。Spring Security基于它們又封裝了幾個類,主要幾個:WebSecurityConfigurerAdapter、FilterInvocationSecurityMetadataSource、AccessDecisionManager、UserDetailsService。另外還有各類注解如@EnableGlobalMethodSecurity等。(以下代碼含有一點(diǎn)jwt邏輯)
WebSecurityConfigurerAdapter:
1 @Configuration
2 @EnableWebSecurity
3 @EnableGlobalMethodSecurity(prePostEnabled = true)
4 public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
5 @Autowired
6 private JwtAuthenticationEntryPoint unauthorizedHandler;
7
8 @Autowired
9 private UserDetailsService userDetailsService;
10
11 @Autowired
12 private CustomPostProcessor postProcessor;
13
14 @Autowired
15 public void configureAuthentication(AuthenticationManagerBuilder authenticationManagerBuilder) throws Exception {
16 authenticationManagerBuilder
17 .userDetailsService(this.userDetailsService)
18 .passwordEncoder(passwordEncoder());
19 }
20
21 @Bean
22 public PasswordEncoder passwordEncoder() {
23 return new BCryptPasswordEncoder();
24 }
25
26 @Bean
27 public JwtAuthenticationTokenFilter authenticationTokenFilterBean() throws Exception {
28 return new JwtAuthenticationTokenFilter();
29 }
30
31 @Override
32 protected void configure(HttpSecurity httpSecurity) throws Exception {
33 httpSecurity
34 // we don't need CSRF because our token is invulnerable
35 .csrf().disable()
36 .exceptionHandling().authenticationEntryPoint(unauthorizedHandler).and()
37 // don't create session
38 .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS).and()
39 .authorizeRequests()
40 .antMatchers(HttpMethod.OPTIONS, "/**").permitAll()
41 .anyRequest().authenticated().withObjectPostProcessor(postProcessor);
42
43 // Custom JWT based security filter
44 httpSecurity
45 .addFilterBefore(authenticationTokenFilterBean(), UsernamePasswordAuthenticationFilter.class);
46 }
47 }
主要關(guān)注兩個方法configureAuthentication(AuthenticationManagerBuilder authenticationManagerBuilder)和configure(HttpSecurity httpSecurity)。configureAuthentication主要用于設(shè)置UserDetailsService,加載用戶數(shù)據(jù)需要用到;configure用于設(shè)置資源的安全級別以及全局安全策略等。第41行withObjectPostProcessor,用于設(shè)置FilterInvocationSecurityMetadataSource和AccessDecisionManager,它們兩個用于鑒權(quán),下面會講到。
1 @Component
2 public class CustomPostProcessor implements ObjectPostProcessor<FilterSecurityInterceptor> {
3 @Autowired
4 private CustomFilterSecurityMetadataSource customFilterSecurityMetadataSource;
5
6 @Autowired
7 private CustomAccessDecisionManager customAccessDecisionManager;
8
9 @Override
10 public <T extends FilterSecurityInterceptor> T postProcess(T fsi) {
11 fsi.setSecurityMetadataSource(customFilterSecurityMetadataSource); //1.路徑(資源)攔截處理
12 fsi.setAccessDecisionManager(customAccessDecisionManager); //2.權(quán)限決策處理類
13 return fsi;
14 }
15 }
UserDetailService(此處從數(shù)據(jù)庫獲取):
1 @Service
2 public class JwtUserDetailsServiceImpl implements UserDetailsService {
3
4 @Autowired
5 private UserRepository userRepository;
6
7 @Override
8 public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
9 User user = userRepository.findByUsername(username);
10
11 if (user == null) {
12 throw new UsernameNotFoundException(String.format("No user found with username '%s'.", username));
13 } else {
14 return JwtUserFactory.create(user);
15 }
16 }
17 }
注意loadUserByUsername需要的參數(shù)名username是約定好的,在UsernamePasswordAuthenticationFilter中定義,value是從HttpServletRequest中獲取。
FilterInvocationSecurityMetadataSource(用于獲取當(dāng)前請求資源所需的權(quán)限):
1 /**
2 * 路徑攔截處理類
3 * <p>
4 * 如果路徑屬于允許訪問列表,則不做攔截,放開訪問;
5 * <p>
6 * 否則,獲得路徑訪問所需角色,并返回;如果沒有找到該路徑所需角色,則拒絕訪問。
7 */
8 @Component
9 public class CustomFilterSecurityMetadataSource implements FilterInvocationSecurityMetadataSource {
10 @Autowired
11 private ApiRepository apiRepository;
12
13 @Override
14 public Collection<ConfigAttribute> getAttributes(Object object) throws IllegalArgumentException {
15 FilterInvocation fi = (FilterInvocation) object; //當(dāng)前請求對象
16
17 List<ConfigAttribute> configAttributes = getMatcherConfigAttribute(fi.getRequestUrl(), fi.getRequest().getMethod()); // 獲得訪問當(dāng)前路徑所需要的角色
18
19 return configAttributes.size() > 0 ? configAttributes : deniedRequest(); //返回當(dāng)前路徑所需角色,如果路徑?jīng)]有對應(yīng)角色,則拒絕訪問
20 }
21
22 @Override
23 public Collection<ConfigAttribute> getAllConfigAttributes() {
24 return null;
25 }
26
27 @Override
28 public boolean supports(Class<?> clazz) {
29 return FilterInvocation.class.isAssignableFrom(clazz);
30 }
31
32 /**
33 * 獲取當(dāng)前路徑以及請求方式獲得所需要的角色
34 *
35 * @param url 當(dāng)前路徑
36 * @return 所需角色集合
37 */
38 private List<ConfigAttribute> getMatcherConfigAttribute(String url, String method) {
39 Set<Authority> authorities = new HashSet<>();
40 // 1.根據(jù)url的開頭去數(shù)據(jù)庫模糊查詢相應(yīng)的api
41
42 String prefix = url.substring(0, url.lastIndexOf("/"));
43
44 prefix = StringUtil.isEmpty(prefix) ? url : prefix + "%";
45
46 List<Api> apis = apiRepository.findByUriLikeAndMethod(prefix, method);
47
48 // 2.查找完全匹配的api,如果沒有,比對pathMatcher是否有匹配的結(jié)果
49 apis.forEach(api -> {
50 String pattern = api.getUri();
51
52 if (new AntPathMatcher().match(pattern, url)) {
53 List<Resource> resources = api.getResources();
54
55 resources.forEach(resource -> {
56 authorities.addAll(resource.getAuthorities());
57 });
58 }
59 });
60
61 return authorities.stream().map(authority -> new SecurityConfig(authority.getId().toString())).collect(Collectors.toList());
62 }
63
64 /**
65 * @return 默認(rèn)拒絕訪問配置
66 */
67 private List<ConfigAttribute> deniedRequest() {
68 return Collections.singletonList(new SecurityConfig("ROLE_DENIED"));
69 }
70 }
AccessDecisionManager:
1 /**
2 * 權(quán)限決策處理類
3 *
4 * 判斷用戶的角色,如果為空,則拒絕訪問;
5 *
6 * 判斷用戶所有的角色中是否有一個包含在 訪問路徑允許的角色集合中;
7 *
8 * 如果有,則放開;否則拒絕訪問;
9 */
10 @Component
11 public class CustomAccessDecisionManager implements AccessDecisionManager {
12 @Override
13 public void decide(Authentication authentication, Object object, Collection<ConfigAttribute> configAttributes) throws AccessDeniedException, InsufficientAuthenticationException {
14 if (authentication == null) {
15 throw new AccessDeniedException("permission denied");
16 }
17
18 //當(dāng)前用戶擁有的角色集合
19 List<String> roleCodes = authentication.getAuthorities().stream().map(GrantedAuthority::getAuthority).collect(Collectors.toList());
20
21 //訪問路徑所需要的角色集合
22 List<String> configRoleCodes = configAttributes.stream().map(ConfigAttribute::getAttribute).collect(Collectors.toList());
23 for (String roleCode : roleCodes) {
24 if (configRoleCodes.contains(roleCode)) {
25 return;
26 }
27 }
28
29 throw new AccessDeniedException("permission denied");
30 }
31
32 @Override
33 public boolean supports(ConfigAttribute attribute) {
34 return true;
35 }
36
37 @Override
38 public boolean supports(Class<?> clazz) {
39 return true;
40 }
41 }
上述第19行和第22行分別為UserDetailService處取到的用戶擁有的權(quán)限和FilterInvocationSecurityMetadataSource取到的訪問資源需要的權(quán)限,兩者對比后即得出用戶是否有訪問該資源的權(quán)限。具體來說,鑒權(quán)的整個流程是:訪問資源時,會通過AbstractSecurityInterceptor攔截器攔截,其中會調(diào)用FilterInvocationSecurityMetadataSource的方法來獲取被攔截url所需的全部權(quán)限,再調(diào)用授權(quán)管理器AccessDecisionManager,這個授權(quán)管理器會通過spring的全局緩存SecurityContextHolder獲取用戶的權(quán)限信息,還會獲取被攔截的url和被攔截url所需的全部權(quán)限,然后根據(jù)所配的策略(有:一票決定,一票否定,少數(shù)服從多數(shù)等),如果權(quán)限足夠,則返回,權(quán)限不夠則報錯并調(diào)用權(quán)限不足頁面。
題外話,登錄認(rèn)證可以認(rèn)為并非認(rèn)證授權(quán)的一部分,而是將身份令牌頒發(fā)給客戶端的過程,之后客戶端拿著身份令牌過來請求資源的時候才進(jìn)入上面的認(rèn)證授權(quán)環(huán)節(jié)。不過Spring Secuity中涉及到的認(rèn)證方法可以簡化登錄認(rèn)證的代碼編寫:
1 final Authentication authentication = authenticationManager.authenticate(
2 new UsernamePasswordAuthenticationToken(username, password)
3 );
4
5 SecurityContextHolder.getContext().setAuthentication(authentication);
其中authenticationManager由框架提供,框架會根據(jù)上面說到的configureAuthentication提供合適的AuthenticationManager實(shí)例,認(rèn)證失敗時拋出異常,否則返回Authenticatio令牌并為用戶相關(guān)的SecurityContext設(shè)置令牌。需要注意的是,SecurityContext是存放在ThreadLocal中的,而且在每次權(quán)限鑒定的時候都是從ThreadLocal中獲取SecurityContext中對應(yīng)的Authentication所擁有的權(quán)限,并且不同的request是不同的線程,為什么每次都可以從ThreadLocal中獲取到當(dāng)前用戶對應(yīng)的SecurityContext呢?在Web應(yīng)用中這是通過SecurityContextPersistentFilter實(shí)現(xiàn)的,默認(rèn)情況下其會在每次請求開始的時候從session中獲取SecurityContext,然后把它設(shè)置給SecurityContextHolder,在請求結(jié)束后又會將該SecurityContext保存在session中,并且在SecurityContextHolder中清除。當(dāng)用戶第一次訪問系統(tǒng)的時候,該用戶沒有SecurityContext,待登錄成功后,之后的每次請求就可以從session中獲取到該SecurityContext并把它賦予給SecurityContextHolder了,由于SecurityContextHolder已經(jīng)持有認(rèn)證過的Authentication對象了,所以下次訪問的時候也就不再需要進(jìn)行登錄認(rèn)證了。
而上文說到的jwt,卻是cookie/session一生黑。它的機(jī)制是http請求頭部的令牌認(rèn)證。我們可以借助它在session過期后也能正常的認(rèn)證授權(quán),而不需要用戶重新登錄。
1 public class JwtAuthenticationTokenFilter extends OncePerRequestFilter {
2
3 private final Log logger = LogFactory.getLog(this.getClass());
4
5 @Autowired
6 private UserDetailsService userDetailsService;
7
8 @Autowired
9 private JwtTokenUtil jwtTokenUtil;
10
11 @Value("${jwt.header}")
12 private String tokenHeader;
13
14 @Override
15 protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain) throws ServletException, IOException {
16 final String requestHeader = request.getHeader(this.tokenHeader);
17
18 String username = null;
19 String authToken = null;
20 if (requestHeader != null && requestHeader.startsWith("Bearer ")) {
21 authToken = requestHeader.substring(7);
22 try {
23 username = jwtTokenUtil.getUsernameFromToken(authToken);
24 } catch (IllegalArgumentException e) {
25 logger.error("an error occured during getting username from token", e);
26 } catch (Exception e1) {
27 logger.error(e1.getMessage());
28 }
29 }
30
31 if (username != null && SecurityContextHolder.getContext().getAuthentication() == null) {
32
33 // It is not compelling necessary to load the use details from the database. You could also store the information
34 // in the token and read it from it. It's up to you ;)
35 UserDetails userDetails = this.userDetailsService.loadUserByUsername(username);
36
37 // For simple validation it is completely sufficient to just check the token integrity. You don't have to call
38 // the database compellingly. Again it's up to you ;)
39 if (jwtTokenUtil.validateToken(authToken, userDetails)) {
40 UsernamePasswordAuthenticationToken authentication = new UsernamePasswordAuthenticationToken(userDetails, null, userDetails.getAuthorities());
41 authentication.setDetails(new WebAuthenticationDetailsSource().buildDetails(request));
42 logger.info("authenticated user " + username + ", setting security context");
43 SecurityContextHolder.getContext().setAuthentication(authentication);
44 }
45 }
46
47 chain.doFilter(request, response);
48 }
49 }
當(dāng)然也可以不借助Spring Security,單純的實(shí)現(xiàn)jwt,那樣就需要自己實(shí)現(xiàn)認(rèn)證和授權(quán)過程了。
在Spring Boot 1.5中,我們可以依靠重寫WebMvcConfigurerAdapter的方法來添加自定義攔截器,消息轉(zhuǎn)換器等;Spring Boot 2.0 后,該類被標(biāo)記為@Deprecated。方式改為實(shí)現(xiàn)WebMvcConfigurer接口。在Java中,攔截器(Interceptor)和Filter有所不同,前者更貼近AOP概念,而后者只有前置執(zhí)行。
對比:Asp.net mvc相對清晰,可控性高;Spring Security隱藏了邏輯順序,涉及類較多,關(guān)鍵步驟散落各處,層級不清,容易讓新手困惑。還有其它的Java認(rèn)證框架如Shiro,也很流行,此處按過不表。
非阻塞編程
在web開發(fā)領(lǐng)域,傳統(tǒng)的實(shí)現(xiàn)異步的方式都比較復(fù)雜,比如 Java 中的 NIO,需要了解 channel,selector,buffer 這些概念,或者使用 netty 這樣的網(wǎng)絡(luò)框架。c/c++ 進(jìn)行異步/非阻塞編程,則需要理解 select,poll,epoll 等概念,開發(fā)與維護(hù)門檻較高。而且這部分的開發(fā)與業(yè)務(wù)無關(guān),那么封裝底層機(jī)制,推出一套開發(fā)框架的必要性就顯而易見了。概念上,.Net習(xí)慣稱為異步編程(Asynchronous programming),Java稱之為響應(yīng)式編程(Reactive Programming)。
.Net/Asynchronous programming
.Net4.5(C#5.0,2012年)開始,引入async/await關(guān)鍵字,在語法層面上將異步編程變得如同同步處理般清晰流暢,并在短時內(nèi)即推出了支持主流數(shù)據(jù)庫的異步組件。從接收請求到數(shù)據(jù)操作,開發(fā)人員能很方便的將傳統(tǒng)的同步代碼遷移為異步模式。之后幾年,如Python(3.5)、Nodejs(7.6)等紛紛效仿,成為事實(shí)上的語法標(biāo)準(zhǔn)。
Java/Reactive Programming
我們得先從Stream說起,Stream本身和響應(yīng)式編程沒關(guān)系,但之后的Reactive Streams在某種程度上繼承了它的某些概念。Java 8 引入了Stream,方便集合的聚合操作,它也支持lambda表達(dá)式作為操作參數(shù),可以將其看做Iterator。類似的語法在C#中也有,只是C#提供的是無侵入方式,集合本身就支持,更不用說Stream這個概念多么讓人混淆。相關(guān)資料:Java 8 中的 Streams API 詳解
Stream的映射操作有map和flatmap,類似C#中Select和SelectMany的區(qū)別。
Reactive Streams
歷程
響應(yīng)式流從2013年開始,作為提供非阻塞背壓的異步流處理標(biāo)準(zhǔn)的倡議。
在2015年,出版了一個用于處理響應(yīng)式流的規(guī)范和Java API。 Java API 中的響應(yīng)式流由四個接口組成:Publisher<T>,Subscriber<T>,Subscription和Processor<T,R>。
JDK 9在java.util.concurrent包中提供了與響應(yīng)式流兼容的API,它在java.base模塊中。 API由兩個類組成:Flow和SubmissionPublisher<T>。Flow類封裝了響應(yīng)式流Java API。 由響應(yīng)式流Java API指定的四個接口作為嵌套靜態(tài)接口包含在Flow類中:Flow.Processor<T,R>,F(xiàn)low.Publisher<T>,F(xiàn)low.Subscriber<T>和Flow.Subscription。
Reactor是Reactive Streams的一個實(shí)現(xiàn)庫。鄙人認(rèn)為,Reactive Streams針對的場景是無邊界數(shù)據(jù)的enumerate處理,無邊界即數(shù)據(jù)/需求會被不停的生產(chǎn)出來,無法在事前確立循環(huán)規(guī)則(如循環(huán)次數(shù));另一方面,它又提供了單次處理的處理規(guī)則(如每次處理多少條數(shù)據(jù)/需求)。相關(guān)資料:聊聊reactive streams的backpressure。
Spring5.0開始提供響應(yīng)式 Web 編程支持,框架為Spring WebFlux,區(qū)別于傳統(tǒng)的Spring MVC同步模式。Spring WebFlux基于Reactor,其語法類似JS的Promise,并有一些靈活有用的特性如延時處理返回。具體用法可參看:(5)Spring WebFlux快速上手——響應(yīng)式Spring的道法術(shù)器 。就文中所說,目前(本文書寫時間)Spring Data對MongoDB、Redis、Apache Cassandra和CouchDB數(shù)據(jù)庫提供了響應(yīng)式數(shù)據(jù)訪問支持,意即使用其它數(shù)據(jù)庫的項(xiàng)目尚無法真正做到異步響應(yīng)(最關(guān)鍵的IO環(huán)節(jié)仍為線程同步)。
在Java 7推出異步I/O庫,以及Servlet3.1增加了對異步I/O的支持之后,Tomcat等Servlet容器也隨后開始支持異步I/O,然后Spring WebMVC也增加了對Reactor庫的支持,在Spring MVC3.2版本已經(jīng)支持異步模式。至于Spring為何又推出一套WebFlux就不得而知了,應(yīng)該是為了打造更純粹更全面的框架。可參看 爸爸又給Spring MVC生了個弟弟叫Spring WebFlux。
對比:非阻塞編程方面,Java推進(jìn)速度慢,目前的程度尚不能與幾年前的.Net相比,語法上,.Net的async/await相比類Promise語法更簡潔,Spring WebFlux在請求響應(yīng)處理上有一些亮點(diǎn)。NIO/WebFlux和async/await底層的線程調(diào)度模型大同小異。
后記:C#8.0推出了Async Streams,應(yīng)該是在理念上借鑒了Reactive Streams。可參看 聊一聊C# 8.0中的await foreach
其它
幾個月前(美國當(dāng)?shù)貢r間9月25日),Oracle官方宣布 Java 11 (18.9 LTS) 正式發(fā)布。Java目前的版本發(fā)布策略是半年一版,每三年發(fā)布一個長期支持版本,Java 11 是自 Java 8 后的首個長期支持版本。目測Java 8 開始的很多特性都參考了C#,比如異步編程、Lambda、Stream、var等等,這是一個好的現(xiàn)象,相互學(xué)習(xí)才能進(jìn)步嘛。
.Net的MVC模板引擎為默認(rèn)為razor,它是專一且多情的,依賴于后端代碼。而Java平臺常用的有很多,如FreeMarker,它獨(dú)立于任何框架,可以將它看作復(fù)雜版的string.format,用在mvc中就是string.format(v,m),輸出就是v模板綁定m數(shù)據(jù)后的html;還有Spring Boot自帶的thymeleaf,它由于使用了標(biāo)簽屬性做為語法,模版頁面可以直接用瀏覽器渲染,使得前端和后端可以并行開發(fā),竊以為這是兼顧便捷與運(yùn)行效率與SEO的最佳前后端分離開發(fā)利器。
Java8開始,可以在Interface中定義靜態(tài)方法和默認(rèn)方法。在接口中,增加default方法, 是為了既有的成千上萬的Java類庫的類增加新的功能, 且不必對這些類重新進(jìn)行設(shè)計(類似于C#的擴(kuò)展方法,但靈活度低,耦合度高)。
Java8的Optional有點(diǎn)類似于.NET的xxxx?,都是簡化是否為空判斷。
Java ThreadLocal類似于.NET ThreadStaticAttribute,都是提供線程內(nèi)的局部變量[副本],這種變量在線程的生命周期內(nèi)起作用。
Java
Fork/Join:Java 7 引入,方便我們將任務(wù)拆成子任務(wù)并行執(zhí)行[并匯總結(jié)果后返回]。
靜態(tài)引入:import static。導(dǎo)入靜態(tài)方法。
使用匿名內(nèi)部類方式初始化對象:
ArrayList<Student> stuList = new ArrayList<Student>() {
{
for (int i = 0; i < 100; i++) {
add(new Student("student" + i, random.nextInt(50) + 50));
}
}
};
可參看Java:雙括號初始化 /匿名內(nèi)部類初始化法
Java 9 開始支持Http/2,關(guān)于Http/2的特點(diǎn)以及它相較于1.0、1.1版本的改進(jìn)可自行百度,總之效率上提升很大。
Spring3.0引入了@Configuration。Instead of using the XML files, we can use plain Java classes to annotate the configurations by using the @Configuration annotation. If you annotate a class with @Configuration annotation, it indicates that the class is used for defining the beans using the @Bean annotation. This is very much similar to the <bean/> element in the spring XML configurations.當(dāng)然,xml配置和注解配置可以混用。我們?nèi)粢獜?fù)用它處定義的配置類,可使用@Import注解,它的作用類似于將多個XML配置文件導(dǎo)入到單個文件。
Spring中的后置處理器BeanPostProcessor,用于在Spring容器中完成bean實(shí)例化、配置以及其他初始化方法前后要添加一些自己邏輯處理。Spring Security中還有個ObjectPostProcessor,可以用來修改或者替代通過Java方式配置創(chuàng)建的對象實(shí)例,可用在無法預(yù)先設(shè)置值如需要根據(jù)不同條件設(shè)置不同值的場景。
@Value("#{}")與@Value("${}"):前者用于賦予bean字段直接計算的值(SpEL),后者用于賦予屬性文件中定義的屬性值。
Servlet3.0開始,@WebServlet, @WebFilter, and @WebListener can be enabled by using @ServletComponentScan,不用在web.xml里面配置了。這無關(guān)Spring,而是Servlet容器特性。
@Autowired是根據(jù)類型進(jìn)行自動裝配的。如果當(dāng)Spring上下文中存在不止一個UserDao類型的bean時,就會拋出BeanCreationException異常。我們可以使用@Qualifier指明要裝配的類型名稱來解決這個問題。
其它參考資料:
轉(zhuǎn)載請注明本文出處:http://www.rzrgm.cn/newton/p/9866506.html


浙公網(wǎng)安備 33010602011771號