写在前面:本文乃标题党,不是月经贴,侧重于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,前者用于认证授权,后者用于鉴权。
//IAuthenticationFilter 认证,认证是否合法用户
public class AdminAuthenticationFilter : ActionFilterAttribute, IAuthenticationFilter
{
public void OnAuthentication(AuthenticationContext filterContext)
{
IPrincipal user = filterContext.Principal;
if (user == null || !user.Identity.IsAuthenticated)
{
HttpCookie authCookie = filterContext.HttpContext.Request.Cookies[FormsAuthentication.FormsCookieName];
if (authCookie != null)
{
FormsAuthenticationTicket ticket = FormsAuthentication.Decrypt(authCookie.Value);
if (ticket != null && !string.IsNullOrEmpty(ticket.UserData))
{
var userId = Convert.ToInt32(ticket.UserData);
user = EngineContext.Resolve<PFManagerService>().GetManager(userId);
filterContext.Principal = user; //后续会传递给HttpContext.Current.User
}
}
}
} public void OnAuthenticationChallenge(AuthenticationChallengeContext filterContext)
{
// 认证失败执行
}
}
认证成功后,将user赋给filterContext.Principal(第17行),filterContext.Principal接收一个IPrincipal接口对象,该接口有个 bool IsInRole(string role) 方法,用于后续的鉴权过程。
public class AdminAuthorizationFilter : AuthorizeAttribute
{
public override void OnAuthorization(AuthorizationContext filterContext)
{
//childaction不用授权
if (filterContext.IsChildAction)
return; if (!filterContext.ActionDescriptor.IsDefined(typeof(AllowAnonymousAttribute), true) && !filterContext.ActionDescriptor.ControllerDescriptor.IsDefined(typeof(AllowAnonymousAttribute), true))
{
if (filterContext.HttpContext.User != null && filterContext.HttpContext.User.Identity.IsAuthenticated)
{
var controllerName = filterContext.RouteData.Values["controller"].ToString().ToLower();
var actionName = filterContext.RouteData.Values["action"].ToString().ToLower();
//只要登录,则都能访问工作台
if (controllerName.ToLower() == "home" && actionName.ToLower() == "index")
this.Roles = string.Empty;
else
{
var roleIds = EngineContext.Resolve<BEModuleService>().GetRoleIdsHasModuleAuthorization(controllerName, actionName, MasonPlatformType.AdminPlatform);
if (roleIds == null)
{
filterContext.Result = new HttpNotFoundResult();
return;
}
//将资源所需权限赋给成员变量Roles
this.Roles = string.Join(",", roleIds);
}
}
} base.OnAuthorization(filterContext);
}
}
注意第27行,我们将拥有该资源的所有权限赋给Roles,之后AuthorizeAttribute会循环Roles,依次调用当前用户(上述的filterContext.Principal)的IsInRole方法,若其中一个返回true则表明用户有访问当前资源的权限。
Java/Spring Security
也提供了两个类,一个Filter和一个Interceptor:AuthenticationProcessingFilter用于用户认证授权,AbstractSecurityInterceptor用于鉴权。Spring Security基于它们又封装了几个类,主要几个:WebSecurityConfigurerAdapter、FilterInvocationSecurityMetadataSource、AccessDecisionManager、UserDetailsService。另外还有各类注解如@EnableGlobalMethodSecurity等。(以下代码含有一点jwt逻辑)
WebSecurityConfigurerAdapter:
@Configuration
@EnableWebSecurity
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
@Autowired
private JwtAuthenticationEntryPoint unauthorizedHandler; @Autowired
private UserDetailsService userDetailsService; @Autowired
private CustomPostProcessor postProcessor; @Autowired
public void configureAuthentication(AuthenticationManagerBuilder authenticationManagerBuilder) throws Exception {
authenticationManagerBuilder
.userDetailsService(this.userDetailsService)
.passwordEncoder(passwordEncoder());
} @Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
} @Bean
public JwtAuthenticationTokenFilter authenticationTokenFilterBean() throws Exception {
return new JwtAuthenticationTokenFilter();
} @Override
protected void configure(HttpSecurity httpSecurity) throws Exception {
httpSecurity
// we don't need CSRF because our token is invulnerable
.csrf().disable()
.exceptionHandling().authenticationEntryPoint(unauthorizedHandler).and()
// don't create session
.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS).and()
.authorizeRequests()
.antMatchers(HttpMethod.OPTIONS, "/**").permitAll()
.anyRequest().authenticated().withObjectPostProcessor(postProcessor); // Custom JWT based security filter
httpSecurity
.addFilterBefore(authenticationTokenFilterBean(), UsernamePasswordAuthenticationFilter.class);
}
}
主要关注两个方法configureAuthentication(AuthenticationManagerBuilder authenticationManagerBuilder)和configure(HttpSecurity httpSecurity)。configureAuthentication主要用于设置UserDetailsService,加载用户数据需要用到;configure用于设置资源的安全级别以及全局安全策略等。第41行withObjectPostProcessor,用于设置FilterInvocationSecurityMetadataSource和AccessDecisionManager,它们两个用于鉴权,下面会讲到。
@Component
public class CustomPostProcessor implements ObjectPostProcessor<FilterSecurityInterceptor> {
@Autowired
private CustomFilterSecurityMetadataSource customFilterSecurityMetadataSource; @Autowired
private CustomAccessDecisionManager customAccessDecisionManager; @Override
public <T extends FilterSecurityInterceptor> T postProcess(T fsi) {
fsi.setSecurityMetadataSource(customFilterSecurityMetadataSource); //1.路径(资源)拦截处理
fsi.setAccessDecisionManager(customAccessDecisionManager); //2.权限决策处理类
return fsi;
}
}
UserDetailService(此处从数据库获取):
@Service
public class JwtUserDetailsServiceImpl implements UserDetailsService { @Autowired
private UserRepository userRepository; @Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
User user = userRepository.findByUsername(username); if (user == null) {
throw new UsernameNotFoundException(String.format("No user found with username '%s'.", username));
} else {
return JwtUserFactory.create(user);
}
}
}
注意loadUserByUsername需要的参数名username是约定好的,在UsernamePasswordAuthenticationFilter中定义,value是从HttpServletRequest中获取。
FilterInvocationSecurityMetadataSource(用于获取当前请求资源所需的权限):
/**
* 路径拦截处理类
* <p>
* 如果路径属于允许访问列表,则不做拦截,放开访问;
* <p>
* 否则,获得路径访问所需角色,并返回;如果没有找到该路径所需角色,则拒绝访问。
*/
@Component
public class CustomFilterSecurityMetadataSource implements FilterInvocationSecurityMetadataSource {
@Autowired
private ApiRepository apiRepository; @Override
public Collection<ConfigAttribute> getAttributes(Object object) throws IllegalArgumentException {
FilterInvocation fi = (FilterInvocation) object; //当前请求对象 List<ConfigAttribute> configAttributes = getMatcherConfigAttribute(fi.getRequestUrl(), fi.getRequest().getMethod()); // 获得访问当前路径所需要的角色 return configAttributes.size() > 0 ? configAttributes : deniedRequest(); //返回当前路径所需角色,如果路径没有对应角色,则拒绝访问
} @Override
public Collection<ConfigAttribute> getAllConfigAttributes() {
return null;
} @Override
public boolean supports(Class<?> clazz) {
return FilterInvocation.class.isAssignableFrom(clazz);
} /**
* 获取当前路径以及请求方式获得所需要的角色
*
* @param url 当前路径
* @return 所需角色集合
*/
private List<ConfigAttribute> getMatcherConfigAttribute(String url, String method) {
Set<Authority> authorities = new HashSet<>();
// 1.根据url的开头去数据库模糊查询相应的api String prefix = url.substring(0, url.lastIndexOf("/")); prefix = StringUtil.isEmpty(prefix) ? url : prefix + "%"; List<Api> apis = apiRepository.findByUriLikeAndMethod(prefix, method); // 2.查找完全匹配的api,如果没有,比对pathMatcher是否有匹配的结果
apis.forEach(api -> {
String pattern = api.getUri(); if (new AntPathMatcher().match(pattern, url)) {
List<Resource> resources = api.getResources(); resources.forEach(resource -> {
authorities.addAll(resource.getAuthorities());
});
}
}); return authorities.stream().map(authority -> new SecurityConfig(authority.getId().toString())).collect(Collectors.toList());
} /**
* @return 默认拒绝访问配置
*/
private List<ConfigAttribute> deniedRequest() {
return Collections.singletonList(new SecurityConfig("ROLE_DENIED"));
}
}
AccessDecisionManager:
/**
* 权限决策处理类
*
* 判断用户的角色,如果为空,则拒绝访问;
*
* 判断用户所有的角色中是否有一个包含在 访问路径允许的角色集合中;
*
* 如果有,则放开;否则拒绝访问;
*/
@Component
public class CustomAccessDecisionManager implements AccessDecisionManager {
@Override
public void decide(Authentication authentication, Object object, Collection<ConfigAttribute> configAttributes) throws AccessDeniedException, InsufficientAuthenticationException {
if (authentication == null) {
throw new AccessDeniedException("permission denied");
} //当前用户拥有的角色集合
List<String> roleCodes = authentication.getAuthorities().stream().map(GrantedAuthority::getAuthority).collect(Collectors.toList()); //访问路径所需要的角色集合
List<String> configRoleCodes = configAttributes.stream().map(ConfigAttribute::getAttribute).collect(Collectors.toList());
for (String roleCode : roleCodes) {
if (configRoleCodes.contains(roleCode)) {
return;
}
} throw new AccessDeniedException("permission denied");
} @Override
public boolean supports(ConfigAttribute attribute) {
return true;
} @Override
public boolean supports(Class<?> clazz) {
return true;
}
}
上述第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过期后也能正常的认证授权,而不需要用户重新登录。
public class JwtAuthenticationTokenFilter extends OncePerRequestFilter { private final Log logger = LogFactory.getLog(this.getClass()); @Autowired
private UserDetailsService userDetailsService; @Autowired
private JwtTokenUtil jwtTokenUtil; @Value("${jwt.header}")
private String tokenHeader; @Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain) throws ServletException, IOException {
final String requestHeader = request.getHeader(this.tokenHeader); String username = null;
String authToken = null;
if (requestHeader != null && requestHeader.startsWith("Bearer ")) {
authToken = requestHeader.substring(7);
try {
username = jwtTokenUtil.getUsernameFromToken(authToken);
} catch (IllegalArgumentException e) {
logger.error("an error occured during getting username from token", e);
} catch (Exception e1) {
logger.error(e1.getMessage());
}
} if (username != null && SecurityContextHolder.getContext().getAuthentication() == null) { // It is not compelling necessary to load the use details from the database. You could also store the information
// in the token and read it from it. It's up to you ;)
UserDetails userDetails = this.userDetailsService.loadUserByUsername(username); // For simple validation it is completely sufficient to just check the token integrity. You don't have to call
// the database compellingly. Again it's up to you ;)
if (jwtTokenUtil.validateToken(authToken, userDetails)) {
UsernamePasswordAuthenticationToken authentication = new UsernamePasswordAuthenticationToken(userDetails, null, userDetails.getAuthorities());
authentication.setDetails(new WebAuthenticationDetailsSource().buildDetails(request));
logger.info("authenticated user " + username + ", setting security context");
SecurityContextHolder.getContext().setAuthentication(authentication);
}
} chain.doFilter(request, response);
}
}
当然也可以不借助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指明要装配的类型名称来解决这个问题。
其它参考资料:
转载请注明本文出处:https://www.cnblogs.com/newton/p/9866506.html
一文助您成为Java.Net双平台高手的更多相关文章
-
一文搞懂所有Java集合面试题
Java集合 刚刚经历过秋招,看了大量的面经,顺便将常见的Java集合常考知识点总结了一下,并根据被问到的频率大致做了一个标注.一颗星表示知识点需要了解,被问到的频率不高,面试时起码能说个差不多.两颗 ...
-
Hadoop HBase概念学习系列之hbase shell中执行java方法(高手必备)(二十五)
hbase shell中执行java方法(高手必备),务必掌握! 1. 2. 3. 4. 更多命令,见scan help.在实际工作中,多用这个!!! API参考: http://hbase.apac ...
-
不规范的json文档 转化成 java 对象的处理
最近练习爬取数据,遇到了json文档中属性名称没有用双引号的情况,内容如下: 标准的json文档,属性名称都是带双引号的 最后写了个方法,替换属性名字 为 两头追加双引号的属性名字, 特别要注意,防止 ...
-
20135119_涂文斌 实验二 Java面向对象程序设计
北京电子科技学院(BESTI) 实 验 报 告 课程: Java 班级:1351 姓名:涂文斌 学号:20135119 成绩: ...
-
一文彻底搞懂Java中的环境变量
一文搞懂Java环境变量 记得刚接触Java,第一件事就是配环境变量,作为一个初学者,只知道环境变量怎样配,在加上各种IDE使我们能方便的开发,而忽略了其本质的东西,只知其然不知其所以然,随着不断的深 ...
-
一文带你了解Java反射机制
想要获取更多文章可以访问我的博客 - 代码无止境. 上周上班的时候解决一个需求,需要将一批数据导出到Excel.本来公司的中间件组已经封装好了使用POI生成Excel的工具方法,但是无奈产品的需求里面 ...
-
一文带你学会java的jvm精华知识点
前言 本文分为20多个问题,通过问题的方式,来逐渐理解jvm,由浅及深.希望帮助到大家. 1. Java类实例化时,JVM执行顺序? 正确的顺序如下: 1父类静态代码块 2父类静态变量 3子类静态代码 ...
-
一文带你熟悉JAVA IO这个看似很高冷的菇凉
Java IO 是一个庞大的知识体系,很多人学着学着就会学懵了,包括我在内也是如此,所以本文将会从 Java 的 BIO 开始,一步一步深入学习,引出 JDK1.4 之后出现的 NIO 技术,对比 N ...
-
20145239杜文超 实验五 Java网络编程
20145239 实验五 Java网络编程 实验内容 组队,一人服务器,一人客户端. 下载加解密代码,先编译运行代码,一人加密一人解密,适当修改代码. 然后集成代码,一人加密后通过TCP发送,加密使用 ...
随机推荐
-
centos忘记开机密码
系统:centos6.6,忘记开机密码,进入单用户模式进行重置,以下为操作过程. 1. reset(重启)Linux系统,在出现如下图的界面时,请点Enter键,确保一定要快,只存在几秒.. 2.点击 ...
-
判断线段相交 -- 51nod 1264 线段相交
http://www.51nod.com/onlineJudge/questionCode.html#!problemId=1264 三角形的有向面积:a.x*b.y+b.x*c.y+c.x*a.y ...
-
ASP.NET JSON的序列化和反序列化 之 Newtonsoft.Json
我们用到的类库为:Newtonsoft.Json,通过VS工具中NuGet程序包可以下载. 一:对象转json-序列化 public class Student { public int ID { g ...
-
shell 脚本实现的守护进程
转自:http://blog.csdn.net/cybertan/article/details/3235722 转自:http://blog.sina.com.cn/s/blog_4c451e0e0 ...
-
hdu acm 2154(多解取一解)
//题目中结果有一条限制就是最后必须跳回A,如果我们的思想框在这个条件上就很容易卡住,因为这样的条件下的路径很难有规律的罗列,然而我们说这个图形中有三个区域,我们算出每个区域的第n-1次的种类数,然后 ...
-
Nagios+pnp4nagios+rrdtool 安装配置nagios被监控端NRPE配置(二)
NRPE监控插件基础 NRPE总共由两部分组成: (1).check_nrpe插件,运行在监控主机上. (2).NRPE daemon,运行在远程的linux主机上(通常就是被监控机) 整个的监控过程 ...
-
解决Failed to start component [StandardEngine[Catalina].StandardHost[localhost].StandardContext[/Student_recruit]]
查看web.xml文件的书写,特别注意路径与命名一致
-
git版本管理工具常用命令
git是分布式版本管理工具,一台电脑既可以是客户端,也可以是服务端.工作过程中可以断开网络.svn是集中式版本管理工具,一台服务器控制很多客户端,使用过程不能断网. git的优点有:适合分布式开发,强 ...
-
ubuntu 环境下 安装虚拟环境
sudo pip3 install virtualenv 安装虚拟环境 sudo pip3 instal virtualenvwrapper #安装虚拟环境扩展包 编辑home目录下面的.bashrc ...
-
canvas学习笔记之2d画布基础的实现
一. Canvas是啥 < canvas > 是一个可以使用脚本(通常是js)来绘图的HTML元素 < canvas > 最早由Apple引入WebKit,用于Mac OS X ...