1. 程式人生 > >一文助您成為Java.Net雙平臺高手

一文助您成為Java.Net雙平臺高手

編程 延時處理 ati format RoCE trac 擁有 null cep

寫在前面:本文乃標題黨,不是月經貼,側重於Web開發差異,或細節或概述,若有不對之處,還請各位讀者本著友好互助的心態批評指正。由於博客園中.Neter較多(個人感覺),因此本文也可以作為.Neter到Java開發的快速入門。


總述

在.Net開發中,微軟官方框架類可以很好的解決的大部分問題,開發人員可以心安理得的在一畝三分地騰挪躲閃出花來;偶有一些優(zhao)秀(chao)的開源庫,各庫的關註點也基本不會重樣;所以.Neter只要按部就班即可。而Java喜歡定義各種規範,各路大神各自實現,因此一個概念常常會有很多的第三方庫,雖然有Spring這種殺手級框架,不過基於IOC和AOP的設定,Spring家族也變得異常龐大,在編碼時需要引入大量的annotation來織入邏輯;雖然貌似最大程度的解耦了各組件,但導致代碼的可讀性和可調試性非常不好,碎片化非常嚴重。不過也因為如此,Java社區成為設計思想的孕育地,並常常出現一些讓人擊節的設計模式。其中的概念傳播到隔壁.Net圈,圈內小白往往一臉懵逼,而少數大佬不管不顧拿來套用,往往是用錯了,或者讓人不知所以。

籠統來說,.Net框架隱藏細節,簡便清晰,套路單一,但常陷入知其然不知其所以然的懵逼境地;Java&Spring註解隱藏細節,概念繁多,沒有方向感或有被繞暈的風險,但一旦破位而出,則縱橫捭闔天地之大可任意施展至其它平臺。不過兩者差異隨著.Net的開源以肉眼不可見的速度緩慢消失,特別是最近幾年,.Net在語法層面已經超越了Java良多,Java雖然一時半會抹不開面子,但也一直在改進。到的本文撰寫時分,借用不知名網友語:“C#語法已經達到Java20,用戶量撐死Java7,生態Java1.4”。

兩者競爭主要集中在Web開發領域。目前在該領域,Spring Boot已基本成為事實上Java平臺的“官方框架”,我想大部分開發人員並不會在意背後的實現細節,從這個方面來講,兩個平臺的開發模式有一定程度的相似。


數據持久層

為啥這節標題不是ORM呢?畢竟ORM現在是業界標準,很難想象這個時代還需要手寫SQL,還需要手動操作JDBC/ADO;如果你打算這麽幹,一定會被年輕一輩打心眼裏鄙視:)

Java

ORM:十多年前,Hibernate就開始興起,它提供了半對象化的HQL和完全的面向對象QBC。之後也出現了其它一些ORM比如TopLink。

JPA:JDK5引入,是SUN公司為了統一目前眾多ORM而提出的ORM規範(又犯了定義規範的癮)。這個規範出來後,很多ORM表示支持,但以前的還得維護啊,所以像Hibernate就另外建了一個分支叫Hibernate JPA。網友benjaminlee1所言:“JPA的出現只是用於規範現有的ORM技術,它不能取代現有的Hibernate等ORM框架,相反,采用JPA開發時,我們仍將使用這些ORM框架,只是此時開發出來的應用不在依賴於某個持久化提供商。應用可以在不修改代碼的情況下載任何JPA環境下運行,真正做到低耦合,可擴展的程序設計。類似於JDBC,在JDBC出現以前,我們的程序針對特性的數據庫API進行編程,但是現在我們只需要針對JDBC API編程,這樣能夠在不改變代碼的情況下就能換成其他的數據庫。”

Spring Data JPA:有了JPA,我們就可以不在意使用哪個ORM了,但是Spring Data JPA更進一步(為Spring家族添磚加瓦),按約定的方式自動給我們生成持久化代碼,當然它底層還是要依賴各路ORM的。相關資料:使用 Spring Data JPA 簡化 JPA 開發

Mybatis:隨著時間的流逝,Hibernate曾經帶來的榮耀已經被臃腫醜陋的配置文件,無法優化的查詢語句淹沒。很多人開始懷念可一手掌控數據操作的時代,於是Mybatis出現了。Mybatis不是一個完整的ORM,它只完成了數據庫返回結果到對象的映射,而存取邏輯仍為SQL,寫在Mapper文件中,它提供的語法在一定程度上簡化了SQL的編寫,最後Mybatis將SQL邏輯映射到接口方法上(在Mapper文件中指定<mapper namespace="xxx">,其中xxx為映射的DAO接口)。針對每個表寫通用增刪改查的Mapper SQL既枯燥又易出錯,所以出現了Mybatis-Generator之類的代碼生成工具,它能基於數據表生成實體類、基本CRUD的Mapper文件、對應的DAOInterface。

Mybatis-Plus:在Mybatis的基礎上,提供了諸如分頁、復雜條件查詢等功能,基礎CRUD操作不需要額外寫SQL Mapper了,只要DAO接口繼承BaseMapper接口即可。當然為了方便,它也提供了自己的代碼生成器。

.NET

  ORM:主流Entity Framework,除開ORM功能外,它還提供了Code first、DB first、T4代碼生成等特性。性能上與Hibernate一個等級,但使用便捷性和功能全面性較好,更別說還有linq的加持。


認證&授權&鑒權

認證是檢測用戶/請求是否合法,授權是賦予合法用戶相應權限,鑒權是鑒別用戶是否有請求某項資源的權限(認證和授權一般是同時完成)。我們以web為例。

C#/Asp.net mvc

提供了兩個Filter:IAuthenticationFilter 和 AuthorizeAttribute,前者用於認證授權,後者用於鑒權。

技術分享圖片 View Code

認證成功後,將user賦給filterContext.Principal(第17行),filterContext.Principal接收一個IPrincipal接口對象,該接口有個 bool IsInRole(string role) 方法,用於後續的鑒權過程。

技術分享圖片 View Code

註意第27行,我們將擁有該資源的所有權限賦給Roles,之後AuthorizeAttribute會循環Roles,依次調用當前用戶(上述的filterContext.Principal)的IsInRole方法,若其中一個返回true則表明用戶有訪問當前資源的權限。

Java/Spring Security

也提供了兩個類,一個Filter和一個Interceptor:AuthenticationProcessingFilter用於用戶認證授權,AbstractSecurityInterceptor用於鑒權。Spring Security基於它們又封裝了幾個類,主要幾個:WebSecurityConfigurerAdapter、FilterInvocationSecurityMetadataSource、AccessDecisionManager、UserDetailsService。另外還有各類註解如@EnableGlobalMethodSecurity等。(以下代碼含有一點jwt邏輯)

WebSecurityConfigurerAdapter:

技術分享圖片 View Code

主要關註兩個方法configureAuthentication(AuthenticationManagerBuilder authenticationManagerBuilder)和configure(HttpSecurity httpSecurity)。configureAuthentication主要用於設置UserDetailsService,加載用戶數據需要用到;configure用於設置資源的安全級別以及全局安全策略等。第41行withObjectPostProcessor,用於設置FilterInvocationSecurityMetadataSource和AccessDecisionManager,它們兩個用於鑒權,下面會講到。

技術分享圖片 View Code

UserDetailService(此處從數據庫獲取):

技術分享圖片 View Code

註意loadUserByUsername需要的參數名username是約定好的,在UsernamePasswordAuthenticationFilter中定義,value是從HttpServletRequest中獲取。

FilterInvocationSecurityMetadataSource(用於獲取當前請求資源所需的權限):

技術分享圖片 View Code

AccessDecisionManager:

技術分享圖片 View Code

上述第19行和第22行分別為UserDetailService處取到的用戶擁有的權限和FilterInvocationSecurityMetadataSource取到的訪問資源需要的權限,兩者對比後即得出用戶是否有訪問該資源的權限。具體來說,鑒權的整個流程是:訪問資源時,會通過AbstractSecurityInterceptor攔截器攔截,其中會調用FilterInvocationSecurityMetadataSource的方法來獲取被攔截url所需的全部權限,再調用授權管理器AccessDecisionManager,這個授權管理器會通過spring的全局緩存SecurityContextHolder獲取用戶的權限信息,還會獲取被攔截的url和被攔截url所需的全部權限,然後根據所配的策略(有:一票決定,一票否定,少數服從多數等),如果權限足夠,則返回,權限不夠則報錯並調用權限不足頁面。

題外話,登錄認證可以認為並非認證授權的一部分,而是將身份令牌頒發給客戶端的過程,之後客戶端拿著身份令牌過來請求資源的時候才進入上面的認證授權環節。不過Spring Secuity中涉及到的認證方法可以簡化登錄認證的代碼編寫:

1 final Authentication authentication = authenticationManager.authenticate(
2         new UsernamePasswordAuthenticationToken(username, password)
3 );
4 
5 SecurityContextHolder.getContext().setAuthentication(authentication);

其中authenticationManager由框架提供,框架會根據上面說到的configureAuthentication提供合適的AuthenticationManager實例,認證失敗時拋出異常,否則返回Authenticatio令牌並為用戶相關的SecurityContext設置令牌。需要註意的是,SecurityContext是存放在ThreadLocal中的,而且在每次權限鑒定的時候都是從ThreadLocal中獲取SecurityContext中對應的Authentication所擁有的權限,並且不同的request是不同的線程,為什麽每次都可以從ThreadLocal中獲取到當前用戶對應的SecurityContext呢?在Web應用中這是通過SecurityContextPersistentFilter實現的,默認情況下其會在每次請求開始的時候從session中獲取SecurityContext,然後把它設置給SecurityContextHolder,在請求結束後又會將該SecurityContext保存在session中,並且在SecurityContextHolder中清除。當用戶第一次訪問系統的時候,該用戶沒有SecurityContext,待登錄成功後,之後的每次請求就可以從session中獲取到該SecurityContext並把它賦予給SecurityContextHolder了,由於SecurityContextHolder已經持有認證過的Authentication對象了,所以下次訪問的時候也就不再需要進行登錄認證了。

而上文說到的jwt,卻是cookie/session一生黑。它的機制是http請求頭部的令牌認證。我們可以借助它在session過期後也能正常的認證授權,而不需要用戶重新登錄。

技術分享圖片 技術分享圖片
 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 }
技術分享圖片

當然也可以不借助Spring Security,單純的實現jwt,那樣就需要自己實現認證和授權過程了。

在Spring Boot 1.5中,我們可以依靠重寫WebMvcConfigurerAdapter的方法來添加自定義攔截器,消息轉換器等;Spring Boot 2.0 後,該類被標記為@Deprecated。方式改為實現WebMvcConfigurer接口。在Java中,攔截器(Interceptor)和Filter有所不同,前者更貼近AOP概念,而後者只有前置執行。

對比:Asp.net mvc相對清晰,可控性高;Spring Security隱藏了邏輯順序,涉及類較多,關鍵步驟散落各處,層級不清,容易讓新手困惑。還有其它的Java認證框架如Shiro,也很流行,此處按過不表。


非阻塞編程

在web開發領域,傳統的實現異步的方式都比較復雜,比如 Java 中的 NIO,需要了解 channel,selector,buffer 這些概念,或者使用 netty 這樣的網絡框架。c/c++ 進行異步/非阻塞編程,則需要理解 select,poll,epoll 等概念,開發與維護門檻較高。而且這部分的開發與業務無關,那麽封裝底層機制,推出一套開發框架的必要性就顯而易見了。概念上,.Net習慣稱為異步編程(Asynchronous programming),Java稱之為響應式編程(Reactive Programming)。

.Net/Asynchronous programming

.Net4.5(C#5.0,2012年)開始,引入async/await關鍵字,在語法層面上將異步編程變得如同同步處理般清晰流暢,並在短時內即推出了支持主流數據庫的異步組件。從接收請求到數據操作,開發人員能很方便的將傳統的同步代碼遷移為異步模式。之後幾年,如Python(3.5)、Nodejs(7.6)等紛紛效仿,成為事實上的語法標準。

Java/Reactive Programming

我們得先從Stream說起,Stream本身和響應式編程沒關系,但之後的Reactive Streams在某種程度上繼承了它的某些概念。Java 8 引入了Stream,方便集合的聚合操作,它也支持lambda表達式作為操作參數,可以將其看做Iterator。類似的語法在C#中也有,只是C#提供的是無侵入方式,集合本身就支持,更不用說Stream這個概念多麽讓人混淆。相關資料:Java 8 中的 Streams API 詳解

Stream的映射操作有map和flatmap,類似C#中Select和SelectMany的區別。

Reactive Streams

歷程

響應式流從2013年開始,作為提供非阻塞背壓的異步流處理標準的倡議。

在2015年,出版了一個用於處理響應式流的規範和Java API。 Java API 中的響應式流由四個接口組成:Publisher<T>,Subscriber<T>,Subscription和Processor<T,R>。

JDK 9在java.util.concurrent包中提供了與響應式流兼容的API,它在java.base模塊中。 API由兩個類組成:Flow和SubmissionPublisher<T>。Flow類封裝了響應式流Java API。 由響應式流Java API指定的四個接口作為嵌套靜態接口包含在Flow類中:Flow.Processor<T,R>,Flow.Publisher<T>,Flow.Subscriber<T>和Flow.Subscription。

Reactor是Reactive Streams的一個實現庫。鄙人認為,Reactive Streams針對的場景是無邊界數據的enumerate處理,無邊界即數據/需求會被不停的生產出來,無法在事前確立循環規則(如循環次數);另一方面,它又提供了單次處理的處理規則(如每次處理多少條數據/需求)。相關資料:聊聊reactive streams的backpressure。

Spring5.0開始提供響應式 Web 編程支持,框架為Spring WebFlux,區別於傳統的Spring MVC同步模式。Spring WebFlux基於Reactor,其語法類似JS的Promise,並有一些靈活有用的特性如延時處理返回。具體用法可參看:(5)Spring WebFlux快速上手——響應式Spring的道法術器 。就文中所說,目前(本文書寫時間)Spring Data對MongoDB、Redis、Apache Cassandra和CouchDB數據庫提供了響應式數據訪問支持,意即使用其它數據庫的項目尚無法真正做到異步響應(最關鍵的IO環節仍為線程同步)。

在Java 7推出異步I/O庫,以及Servlet3.1增加了對異步I/O的支持之後,Tomcat等Servlet容器也隨後開始支持異步I/O,然後Spring WebMVC也增加了對Reactor庫的支持,在Spring MVC3.2版本已經支持異步模式。至於Spring為何又推出一套WebFlux就不得而知了。

對比:非阻塞編程方面,Java推進速度慢,目前的程度尚不能與幾年前的.Net相比,語法上,.Net的async/await相比類Promise語法更簡潔,Spring WebFlux在請求響應處理上有一些亮點。


其它

幾個月前(美國當地時間9月25日),Oracle官方宣布 Java 11 (18.9 LTS) 正式發布。Java目前的版本發布策略是半年一版,每三年發布一個長期支持版本,Java 11 是自 Java 8 後的首個長期支持版本。目測Java 8 開始的很多特性都參考了C#,比如異步編程、Lambda、Stream、var等等,這是一個好的現象,相互學習才能進步嘛。

.Net的MVC模板引擎為默認為razor,它是專一且多情的,依賴於後端代碼。而Java平臺常用的有很多,如FreeMarker,它獨立於任何框架,可以將它看作復雜版的string.format,用在mvc中就是string.format(v,m),輸出就是v模板綁定m數據後的html;還有Spring Boot自帶的thymeleaf,它由於使用了標簽屬性做為語法,模版頁面直接用瀏覽器渲染,使得前端和後端可以並行開發,竊以為這是兼顧便捷與運行效率的最佳前後端分離開發利器。

Java8開始,可以在Interface中定義靜態方法和默認方法。在接口中,增加default方法, 是為了既有的成千上萬的Java類庫的類增加新的功能, 且不必對這些類重新進行設計(類似於C#的擴展方法,但靈活度低,耦合度高)。

Java8的Optional有點類似於.NET的xxxx?,都是簡化是否為空判斷。

Java ThreadLocal類似於.NET ThreadStaticAttribute,都是提供線程內的局部變量[副本],這種變量在線程的生命周期內起作用。

Java

Fork/Join:Java 7 引入,方便我們將任務拆成子任務並行執行[並匯總結果後返回]。

靜態引入:import static。導入靜態方法。

使用匿名內部類方式初始化對象:

技術分享圖片
ArrayList<Student> stuList = new ArrayList<Student>() {
    {
        for (int i = 0; i < 100; i++) {
            add(new Student("student" + i, random.nextInt(50) + 50));
        }
    }
};
技術分享圖片

可參看Java:雙括號初始化 /匿名內部類初始化法

Java 9 開始支持Http/2,關於Http/2的特點以及它相較於1.0、1.1版本的改進可自行百度,總之效率上提升很大。

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.當然,xml配置和註解配置可以混用。我們若要復用它處定義的配置類,可使用@Import註解,它的作用類似於將多個XML配置文件導入到單個文件。

Spring中的後置處理器BeanPostProcessor,用於在Spring容器中完成bean實例化、配置以及其他初始化方法前後要添加一些自己邏輯處理。Spring Security中還有個ObjectPostProcessor,可以用來修改或者替代通過Java方式配置創建的對象實例,可用在無法預先設置值如需要根據不同條件設置不同值的場景。

@Value("#{}")與@Value("${}"):前者用於賦予bean字段的值,後者用於賦予屬性文件中定義的屬性值。

Servlet3.0開始,@WebServlet, @WebFilter, and @WebListener can be enabled by using @ServletComponentScan,不用在web.xml裏面配置了。這無關Spring,而是Servlet容器特性。

@Autowired是根據類型進行自動裝配的。如果當Spring上下文中存在不止一個UserDao類型的bean時,就會拋出BeanCreationException異常。我們可以使用@Qualifier指明要裝配的類型名稱來解決這個問題。

一文助您成為Java.Net雙平臺高手