写在前面:本文乃标题党,不是月经贴,侧重于Web开发差异,或细节或概述,若有不对之处,还请各位读者本着友好互助的心态批评指正。由于博客园中.Neter较多(个人感觉),因此本文也可以作为.Neter到Java开发的快速入门。
恕本文的不严谨,评论里有说到.net core的,其实可看作是另一个平台。虽然.net core目前社区讨论较多,但毕竟出生不久(相对来说),市场体量应该还远未达到传统.NET。所以本文仍基于传统.NET描述,但部分文字也适用于.net core。另鄙人对微软的开源策略亦持乐观态度,在总述中稍有提及。
总述
在.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,前者用于认证授权,后者用于鉴权。
1 //IAuthenticationFilter 认证,认证是否合法用户
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; //后续会传递给HttpContext.Current.User
18 }
19 }
20 }
21 }
22
23 public void OnAuthenticationChallenge(AuthenticationChallengeContext filterContext)
24 {
25 // 认证失败执行
26 }
27 }
认证成功后,将user赋给filterContext.Principal(第17行),filterContext.Principal接收一个IPrincipal接口对象,该接口有个 bool IsInRole(string role) 方法,用于后续的鉴权过程。
1 public class AdminAuthorizationFilter : AuthorizeAttribute
2 {
3 public override void OnAuthorization(AuthorizationContext filterContext)
4 {
5 //childaction不用授权
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 //将资源所需权限赋给成员变量Roles
27 this.Roles = string.Join(",", roleIds);
28 }
29 }
30 }
31
32 base.OnAuthorization(filterContext);
33 }
34 }
注意第27行,我们将拥有该资源的所有权限赋给Roles,之后AuthorizeAttribute会循环Roles,依次调用当前用户(上述的filterContext.Principal)的IsInRole方法,若其中一个返回true则表明用户有访问当前资源的权限。
Java/Spring Security
也提供了两个类,一个Filter和一个Interceptor:AuthenticationProcessingFilter用于用户认证授权,AbstractSecurityInterceptor用于鉴权。Spring Security基于它们又封装了几个类,主要几个:WebSecurityConfigurerAdapter、FilterInvocationSecurityMetadataSource、AccessDecisionManager、UserDetailsService。另外还有各类注解如@EnableGlobalMethodSecurity等。(以下代码含有一点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 }
主要关注两个方法configureAuthentication(AuthenticationManagerBuilder authenticationManagerBuilder)和configure(HttpSecurity httpSecurity)。configureAuthentication主要用于设置UserDetailsService,加载用户数据需要用到;configure用于设置资源的安全级别以及全局安全策略等。第41行withObjectPostProcessor,用于设置FilterInvocationSecurityMetadataSource和AccessDecisionManager,它们两个用于鉴权,下面会讲到。
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.权限决策处理类
13 return fsi;
14 }
15 }
UserDetailService(此处从数据库获取):
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需要的参数名username是约定好的,在UsernamePasswordAuthenticationFilter中定义,value是从HttpServletRequest中获取。
FilterInvocationSecurityMetadataSource(用于获取当前请求资源所需的权限):
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; //当前请求对象
16
17 List<ConfigAttribute> configAttributes = getMatcherConfigAttribute(fi.getRequestUrl(), fi.getRequest().getMethod()); // 获得访问当前路径所需要的角色
18
19 return configAttributes.size() > 0 ? configAttributes : deniedRequest(); //返回当前路径所需角色,如果路径没有对应角色,则拒绝访问
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 * 获取当前路径以及请求方式获得所需要的角色
34 *
35 * @param url 当前路径
36 * @return 所需角色集合
37 */
38 private List<ConfigAttribute> getMatcherConfigAttribute(String url, String method) {
39 Set<Authority> authorities = new HashSet<>();
40 // 1.根据url的开头去数据库模糊查询相应的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是否有匹配的结果
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 默认拒绝访问配置
66 */
67 private List<ConfigAttribute> deniedRequest() {
68 return Collections.singletonList(new SecurityConfig("ROLE_DENIED"));
69 }
70 }
AccessDecisionManager:
1 /**
2 * 权限决策处理类
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 //当前用户拥有的角色集合
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处取到的用户拥有的权限和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就不得而知了,应该是为了打造更纯粹更全面的框架。可参看 爸爸又给Spring MVC生了个弟弟叫Spring WebFlux。
对比:非阻塞编程方面,Java推进速度慢,目前的程度尚不能与几年前的.Net相比,语法上,.Net的async/await相比类Promise语法更简洁,Spring WebFlux在请求响应处理上有一些亮点。
后记:C#8.0推出了Async Streams,应该是在理念上借鉴了Reactive Streams。可参看 聊一聊C# 8.0中的await foreach
其它
几个月前(美国当地时间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,它由于使用了标签属性做为语法,模版页面可以直接用浏览器渲染,使得前端和后端可以并行开发,窃以为这是兼顾便捷与运行效率与SEO的最佳前后端分离开发利器。
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 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字段直接计算的值(SpEL),后者用于赋予属性文件中定义的属性值。
Servlet3.0开始,@WebServlet, @WebFilter, and @WebListener can be enabled by using @ServletComponentScan,不用在web.xml里面配置了。这无关Spring,而是Servlet容器特性。
@Autowired是根据类型进行自动装配的。如果当Spring上下文中存在不止一个UserDao类型的bean时,就会抛出BeanCreationException异常。我们可以使用@Qualifier指明要装配的类型名称来解决这个问题。
其它参考资料: