引言
前端传入的 json 数据如何被解析成 Java 对象作为 API入参,API 返回结果又如何将 Java 对象解析成 json 格式数据返回给前端,其实在整个数据流转过程中,HttpMessageConverter 起到了重要作用;另外在转换的过程我们可以加入哪些定制化内容?
源代碼
核心邏輯代碼:
// 对于String类型的,直接 append 返回,不转json
if ("java.lang.String".equals(type.getTypeName())) {
try {
// 1.先解析json對象,如果不是json對象的,走catch邏輯
Object jsonObject = JSON.parse(value.toString());
objectWriter.writeValue(generator, jsonObject);
} catch (Throwable e) {
log.error("OvsHttpMessageConverter writeInternal,JSON.parse(value.toString()) = {}", value, e);
// 2.不是json對象的,就原樣輸出string
objectWriter.writeValue(generator, value);
}
}
自定義的 HttpMessageConverter :
package com.alibaba.ovs.operationcenter.config;
import com.alibaba.fastjson.JSON;
import com.fasterxml.jackson.core.JsonEncoding;
import com.fasterxml.jackson.core.JsonGenerator;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.core.util.DefaultIndenter;
import com.fasterxml.jackson.core.util.DefaultPrettyPrinter;
import com.fasterxml.jackson.databind.*;
import com.fasterxml.jackson.databind.ser.FilterProvider;
import lombok.extern.slf4j.Slf4j;
import org.springframework.http.HttpOutputMessage;
import org.springframework.http.MediaType;
import org.springframework.http.converter.HttpMessageNotWritableException;
import org.springframework.http.converter.json.MappingJackson2HttpMessageConverter;
import org.springframework.http.converter.json.MappingJacksonValue;
import org.springframework.util.TypeUtils;
import java.io.IOException;
import java.lang.reflect.Type;
/**
* @author: Jack
* 2020-08-24 23:10
*/
@Slf4j
public class OvsHttpMessageConverter extends MappingJackson2HttpMessageConverter {
private static final MediaType TEXT_EVENT_STREAM = new MediaType("text", "event-stream");
public OvsHttpMessageConverter() {
}
public OvsHttpMessageConverter(ObjectMapper objectMapper) {
super(objectMapper);
}
@Override
protected void writeInternal(Object object, Type type, HttpOutputMessage outputMessage)
throws IOException, HttpMessageNotWritableException {
log.info("OvsHttpMessageConverter writeInternal,type={},object={}", type.getTypeName(), JSON.toJSONString(object));
MediaType contentType = outputMessage.getHeaders().getContentType();
JsonEncoding encoding = getJsonEncoding(contentType);
JsonGenerator generator = this.objectMapper.getFactory().createGenerator(outputMessage.getBody(), encoding);
try {
Object value = object;
Class> serializationView = null;
FilterProvider filters = null;
JavaType javaType = null;
if (object instanceof MappingJacksonValue) {
MappingJacksonValue container = (MappingJacksonValue) object;
value = container.getValue();
serializationView = container.getSerializationView();
filters = container.getFilters();
}
ObjectWriter objectWriter = (serializationView != null ? this.objectMapper.writerWithView(serializationView) : this.objectMapper.writer());
// 对于String类型的,直接 append 返回,不转json
if ("java.lang.String".equals(type.getTypeName())) {
try {
// 1.先解析json對象,如果不是json對象的,走catch邏輯
Object jsonObject = JSON.parse(value.toString());
objectWriter.writeValue(generator, jsonObject);
} catch (Throwable e) {
log.error("OvsHttpMessageConverter writeInternal,JSON.parse(value.toString()) = {}", value, e);
// 2.不是json對象的,就原樣輸出string
objectWriter.writeValue(generator, value);
}
} else {
writePrefix(generator, object);
if (type != null && value != null && TypeUtils.isAssignable(type, value.getClass())) {
javaType = getJavaType(type, null);
}
if (filters != null) {
objectWriter = objectWriter.with(filters);
}
if (javaType != null && javaType.isContainerType()) {
objectWriter = objectWriter.forType(javaType);
}
SerializationConfig config = objectWriter.getConfig();
if (contentType != null && contentType.isCompatibleWith(TEXT_EVENT_STREAM) &&
config.isEnabled(SerializationFeature.INDENT_OUTPUT)) {
DefaultPrettyPrinter prettyPrinter = new DefaultPrettyPrinter();
prettyPrinter.indentObjectsWith(new DefaultIndenter(" ", "\ndata:"));
objectWriter = objectWriter.with(prettyPrinter);
}
objectWriter.writeValue(generator, value);
writeSuffix(generator, object);
}
generator.flush();
} catch (JsonProcessingException ex) {
throw new HttpMessageNotWritableException("Could not write JSON: " + ex.getOriginalMessage(), ex);
}
}
}
WebMvcConfig代碼:
package com.alibaba.ovs.operationcenter.config;
import com.fasterxml.jackson.databind.DeserializationFeature;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.module.SimpleModule;
import com.fasterxml.jackson.databind.ser.std.ToStringSerializer;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.MediaType;
import org.springframework.http.converter.HttpMessageConverter;
import org.springframework.http.converter.json.MappingJackson2HttpMessageConverter;
import org.springframework.web.servlet.config.annotation.EnableWebMvc;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.ResourceHandlerRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurerAdapter;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.List;
/**
* @author: Jack
* 2020-08-14 18:53
*/
@EnableWebMvc
@Configuration
public class WebConfig extends WebMvcConfigurerAdapter {
@Override
public void configureMessageConverters(List> converters) {
// MappingJackson2HttpMessageConverter httpMessageConverter = new MappingJackson2HttpMessageConverter();
OvsHttpMessageConverter httpMessageConverter = new OvsHttpMessageConverter();
ObjectMapper objectMapper = new ObjectMapper();
/**
* 序列换成json时,将所有的long变成string, 因为js 中得 Number 数字类型不能包含所有的 java long 值 (js中会被截断)
* 参考文章: https://blog.csdn.net/universsky2015/article/details/108010953
*/
SimpleModule simpleModule = new SimpleModule();
simpleModule.addSerializer(Long.class, ToStringSerializer.instance);
simpleModule.addSerializer(Long.TYPE, ToStringSerializer.instance);
objectMapper.registerModule(simpleModule);
objectMapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false);
httpMessageConverter.setObjectMapper(objectMapper);
// supportedMediaTypes
List supportedMediaTypes = new ArrayList<>(httpMessageConverter.getSupportedMediaTypes());
supportedMediaTypes.add(MediaType.APPLICATION_JSON_UTF8);
supportedMediaTypes.add(MediaType.TEXT_HTML);
supportedMediaTypes.add(MediaType.TEXT_PLAIN);
supportedMediaTypes.add(MediaType.TEXT_XML);
supportedMediaTypes.add(new MediaType("application", "*+json", StandardCharsets.UTF_8));
httpMessageConverter.setSupportedMediaTypes(supportedMediaTypes);
converters.add(httpMessageConverter);
}
@Override
public void addResourceHandlers(ResourceHandlerRegistry registry) {
// Spring Boot自动配置本身不会自动把/swagger-ui.html这个路径映射到对应的目录META-INF/resources/下面。
// 这个swagger-ui.html 相关的所有前端静态文件都在springfox-swagger-ui.jar里面。
registry.addResourceHandler("swagger-ui.html")
.addResourceLocations("classpath:/META-INF/resources/");
registry.addResourceHandler("/webjars/**")
.addResourceLocations("classpath:/META-INF/resources/webjars/");
}
@Override
public void addInterceptors(InterceptorRegistry registry) {
// 多个拦截器组成一个拦截器链
// addPathPatterns 用于添加拦截规则
// excludePathPatterns 用户排除拦截
//添加拦截器
registry.addInterceptor(new CheckpreloadInterceptor());
super.addInterceptors(registry);
}
}
HttpMessageConverter是什麼?
其中,
HttpMessageConverter 接口介绍
org.springframework.http.converter.HttpMessageConverter 是一个策略接口,接口说明如下:
/*
* Copyright 2002-2017 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.http.converter;
import java.io.IOException;
import java.util.List;
import org.springframework.http.HttpInputMessage;
import org.springframework.http.HttpOutputMessage;
import org.springframework.http.MediaType;
/**
* Strategy interface that specifies a converter that can convert from and to HTTP requests and responses.
*
* @author Arjen Poutsma
* @author Juergen Hoeller
* @since 3.0
*/
public interface HttpMessageConverter {
/**
* Indicates whether the given class can be read by this converter.
* @param clazz the class to test for readability
* @param mediaType the media type to read (can be {@code null} if not specified);
* typically the value of a {@code Content-Type} header.
* @return {@code true} if readable; {@code false} otherwise
*/
boolean canRead(Class> clazz, MediaType mediaType);
/**
* Indicates whether the given class can be written by this converter.
* @param clazz the class to test for writability
* @param mediaType the media type to write (can be {@code null} if not specified);
* typically the value of an {@code Accept} header.
* @return {@code true} if writable; {@code false} otherwise
*/
boolean canWrite(Class> clazz, MediaType mediaType);
/**
* Return the list of {@link MediaType} objects supported by this converter.
* @return the list of supported media types
*/
List getSupportedMediaTypes();
/**
* Read an object of the given type from the given input message, and returns it.
* @param clazz the type of object to return. This type must have previously been passed to the
* {@link #canRead canRead} method of this interface, which must have returned {@code true}.
* @param inputMessage the HTTP input message to read from
* @return the converted object
* @throws IOException in case of I/O errors
* @throws HttpMessageNotReadableException in case of conversion errors
*/
T read(Class extends T> clazz, HttpInputMessage inputMessage)
throws IOException, HttpMessageNotReadableException;
/**
* Write an given object to the given output message.
* @param t the object to write to the output message. The type of this object must have previously been
* passed to the {@link #canWrite canWrite} method of this interface, which must have returned {@code true}.
* @param contentType the content type to use when writing. May be {@code null} to indicate that the
* default content type of the converter must be used. If not {@code null}, this media type must have
* previously been passed to the {@link #canWrite canWrite} method of this interface, which must have
* returned {@code true}.
* @param outputMessage the message to write to
* @throws IOException in case of I/O errors
* @throws HttpMessageNotWritableException in case of conversion errors
*/
void write(T t, MediaType contentType, HttpOutputMessage outputMessage)
throws IOException, HttpMessageNotWritableException;
}
Strategy interface that specifies a converter that can convert from and to HTTP requests and responses. 简单说就是 HTTP request (请求)和response (响应)的转换器。该接口有只有5个方法,简单来说就是获取支持的 MediaType(application/json之类),接收到请求时判断是否能读(canRead),能读则读(read);返回结果时判断是否能写(canWrite),能写则写(write)。
Spring Boot 缺省配置
我们写 Demo 没有配置任何 MessageConverter,但是数据前后传递依旧好用,是因为 SpringMVC 启动时会自动配置一些HttpMessageConverter,在 WebMvcConfigurationSupport 类中添加了缺省 MessageConverter:
protected final void addDefaultHttpMessageConverters(List> messageConverters) {
StringHttpMessageConverter stringConverter = new StringHttpMessageConverter();
stringConverter.setWriteAcceptCharset(false);
messageConverters.add(new ByteArrayHttpMessageConverter());
messageConverters.add(stringConverter);
messageConverters.add(new ResourceHttpMessageConverter());
messageConverters.add(new SourceHttpMessageConverter
我们看到很熟悉的 MappingJackson2HttpMessageConverter ,如果我们引入 jackson 相关包,Spring 就会为我们添加该 MessageConverter,但是我们通常在搭建框架的时候还是会手动添加配置 MappingJackson2HttpMessageConverter,为什么? 先思考一下:
当我们配置了自己的 MessageConverter, SpringMVC 启动过程就不会调用 addDefaultHttpMessageConverters 方法,且看下面代码 if 条件,这样做也是为了定制化我们自己的 MessageConverter
protected final List> getMessageConverters() {
if (this.messageConverters == null) {
this.messageConverters = new ArrayList>();
configureMessageConverters(this.messageConverters);
if (this.messageConverters.isEmpty()) {
addDefaultHttpMessageConverters(this.messageConverters);
}
extendMessageConverters(this.messageConverters);
}
return this.messageConverters;
}
数据流转解析
数据的请求和响应都要经过 DispatcherServlet 类的 doDispatch(HttpServletRequest request, HttpServletResponse response) 方法的处理
请求过程解析
看 doDispatch 方法中的关键代码:
// 这里的 Adapter 实际上是 RequestMappingHandlerAdapter
HandlerAdapter ha = this.getHandlerAdapter(mappedHandler.getHandler());
if (!mappedHandler.applyPreHandle(processedRequest, response)) {
return;
}
// 实际处理的handler
mv = ha.handle(processedRequest, response, mappedHandler.getHandler()); mappedHandler.applyPostHandle(processedRequest, response, mv);
从进入handle之后我先将调用栈粘贴在此处,希望小伙伴可以按照调用栈路线动手跟踪尝试:
readWithMessageConverters:192, AbstractMessageConverterMethodArgumentResolver (org.springframework.web.servlet.mvc.method.annotation)
readWithMessageConverters:150, RequestResponseBodyMethodProcessor (org.springframework.web.servlet.mvc.method.annotation)
resolveArgument:128, RequestResponseBodyMethodProcessor (org.springframework.web.servlet.mvc.method.annotation)
resolveArgument:121, HandlerMethodArgumentResolverComposite (org.springframework.web.method.support)
getMethodArgumentValues:158, InvocableHandlerMethod (org.springframework.web.method.support)
invokeForRequest:128, InvocableHandlerMethod (org.springframework.web.method.support)
// 下面的调用栈重点关注,处理请求和返回值的分叉口就在这里
invokeAndHandle:97, ServletInvocableHandlerMethod (org.springframework.web.servlet.mvc.method.annotation)
invokeHandlerMethod:849, RequestMappingHandlerAdapter (org.springframework.web.servlet.mvc.method.annotation)
handleInternal:760, RequestMappingHandlerAdapter (org.springframework.web.servlet.mvc.method.annotation)
handle:85, AbstractHandlerMethodAdapter (org.springframework.web.servlet.mvc.method)
doDispatch:967, DispatcherServlet (org.springframework.web.servlet)
这里重点说明调用栈最顶层 readWithMessageConverters 方法中内容:
// 遍历 messageConverters
for (HttpMessageConverter> converter : this.messageConverters) {
Class> converterType = (Class>) converter.getClass();
// 上文类关系图处要重点记住的地方,主要判断 MappingJackson2HttpMessageConverter 是否是 GenericHttpMessageConverter 类型
if (converter instanceof GenericHttpMessageConverter) {
GenericHttpMessageConverter> genericConverter = (GenericHttpMessageConverter>) converter;
if (genericConverter.canRead(targetType, contextClass, contentType)) {
if (logger.isDebugEnabled()) {
logger.debug("Read [" + targetType + "] as \"" + contentType + "\" with [" + converter + "]");
}
if (inputMessage.getBody() != null) {
inputMessage = getAdvice().beforeBodyRead(inputMessage, parameter, targetType, converterType);
body = genericConverter.read(targetType, contextClass, inputMessage);
body = getAdvice().afterBodyRead(body, inputMessage, parameter, targetType, converterType);
}
else {
body = getAdvice().handleEmptyBody(null, inputMessage, parameter, targetType, converterType);
}
break;
}
}
else if (targetClass != null) {
if (converter.canRead(targetClass, contentType)) {
if (logger.isDebugEnabled()) {
logger.debug("Read [" + targetType + "] as \"" + contentType + "\" with [" + converter + "]");
}
if (inputMessage.getBody() != null) {
inputMessage = getAdvice().beforeBodyRead(inputMessage, parameter, targetType, converterType);
body = ((HttpMessageConverter) converter).read(targetClass, inputMessage);
body = getAdvice().afterBodyRead(body, inputMessage, parameter, targetType, converterType);
}
else {
body = getAdvice().handleEmptyBody(null, inputMessage, parameter, targetType, converterType);
}
break;
}
}
}
然后就判断是否canRead,能读就read,最终走到下面代码处将输入的内容反序列化出来:
protected Object _readMapAndClose(JsonParser p0, JavaType valueType)
throws IOException
{
try (JsonParser p = p0) {
Object result;
JsonToken t = _initForReading(p);
if (t == JsonToken.VALUE_NULL) {
// Ask JsonDeserializer what 'null value' to use:
DeserializationContext ctxt = createDeserializationContext(p,
getDeserializationConfig());
result = _findRootDeserializer(ctxt, valueType).getNullValue(ctxt);
} else if (t == JsonToken.END_ARRAY || t == JsonToken.END_OBJECT) {
result = null;
} else {
DeserializationConfig cfg = getDeserializationConfig();
DeserializationContext ctxt = createDeserializationContext(p, cfg);
JsonDeserializer
到这里从请求中解析参数过程就到此结束了,趁热打铁来看将响应结果返回给前端的过程
返回过程解析
在上面调用栈请求和返回结果分叉口处同样处理返回的内容:
writeWithMessageConverters:224, AbstractMessageConverterMethodProcessor (org.springframework.web.servlet.mvc.method.annotation)
handleReturnValue:174, RequestResponseBodyMethodProcessor (org.springframework.web.servlet.mvc.method.annotation)
handleReturnValue:81, HandlerMethodReturnValueHandlerComposite (org.springframework.web.method.support)
// 分叉口
invokeAndHandle:113, ServletInvocableHandlerMethod (org.springframework.web.servlet.mvc.method.annotation)
重点关注调用栈顶层内容,是不是很熟悉的样子,完全一样的逻辑, 判断是否能写canWrite,能写则write:
for (HttpMessageConverter> messageConverter : this.messageConverters) {
if (messageConverter instanceof GenericHttpMessageConverter) {
if (((GenericHttpMessageConverter) messageConverter).canWrite(
declaredType, valueType, selectedMediaType)) {
outputValue = (T) getAdvice().beforeBodyWrite(outputValue, returnType, selectedMediaType,
(Class extends HttpMessageConverter>>) messageConverter.getClass(),
inputMessage, outputMessage);
if (outputValue != null) {
addContentDispositionHeader(inputMessage, outputMessage);
((GenericHttpMessageConverter) messageConverter).write(
outputValue, declaredType, selectedMediaType, outputMessage);
if (logger.isDebugEnabled()) {
logger.debug("Written [" + outputValue + "] as \"" + selectedMediaType +
"\" using [" + messageConverter + "]");
}
}
return;
}
}
else if (messageConverter.canWrite(valueType, selectedMediaType)) {
outputValue = (T) getAdvice().beforeBodyWrite(outputValue, returnType, selectedMediaType,
(Class extends HttpMessageConverter>>) messageConverter.getClass(),
inputMessage, outputMessage);
if (outputValue != null) {
addContentDispositionHeader(inputMessage, outputMessage);
((HttpMessageConverter) messageConverter).write(outputValue, selectedMediaType, outputMessage);
if (logger.isDebugEnabled()) {
logger.debug("Written [" + outputValue + "] as \"" + selectedMediaType +
"\" using [" + messageConverter + "]");
}
}
return;
}
}
我们看到有这样一行代码:
outputValue = (T) getAdvice().beforeBodyWrite(outputValue, returnType, selectedMediaType,
(Class extends HttpMessageConverter>>) messageConverter.getClass(),
inputMessage, outputMessage);
我们在设计 RESTful API 接口的时候通常会将返回的数据封装成统一格式,通常我们会实现 ResponseBodyAdvice
@RestControllerAdvice()
public class CommonResultResponseAdvice implements ResponseBodyAdvice
整个处理流程就是这样。
将各种常用 HttpMessageConverter 支持的MediaType 和 JavaType 以及对应关系总结在此处:
參考資料:
https://www.jianshu.com/p/3e1de3d02dd8
https://blog.csdn.net/BryantLmm/article/details/85163590
Kotlin开发者社区
专注分享 Java、 Kotlin、Spring/Spring Boot、MySQL、redis、neo4j、NoSQL、Android、JavaScript、React、Node、函数式编程、编程思想、"高可用,高性能,高实时"大型分布式系统架构设计主题。
High availability, high performance, high real-time large-scale distributed system architecture design。
分布式框架:Zookeeper、分布式中间件框架等
分布式存储:GridFS、FastDFS、TFS、MemCache、redis等
分布式数据库:Cobar、tddl、Amoeba、Mycat
云计算、大数据、AI算法
虚拟化、云原生技术
分布式计算框架:MapReduce、Hadoop、Storm、Flink等
分布式通信机制:Dubbo、RPC调用、共享远程数据、消息队列等
消息队列MQ:Kafka、MetaQ,RocketMQ
怎样打造高可用系统:基于硬件、软件中间件、系统架构等一些典型方案的实现:HAProxy、基于Corosync+Pacemaker的高可用集群套件中间件系统
Mycat架构分布式演进
大数据Join背后的难题:数据、网络、内存和计算能力的矛盾和调和
Java分布式系统中的高性能难题:AIO,NIO,Netty还是自己开发框架?
高性能事件派发机制:线程池模型、Disruptor模型等等。。。
合抱之木,生于毫末;九层之台,起于垒土;千里之行,始于足下。不积跬步,无以至千里;不积小流,无以成江河。
Kotlin 简介
Kotlin是一门非研究性的语言,它是一门非常务实的工业级编程语言,它的使命就是帮助程序员们解决实际工程实践中的问题。使用Kotlin 让 Java程序员们的生活变得更好,Java中的那些空指针错误,浪费时间的冗长的样板代码,啰嗦的语法限制等等,在Kotlin中统统消失。Kotlin 简单务实,语法简洁而强大,安全且表达力强,极富生产力。
Java诞生于1995年,至今已有23年历史。当前最新版本是 Java 9。在 JVM 生态不断发展繁荣的过程中,也诞生了Scala、Groovy、Clojure 等兄弟语言。
Kotlin 也正是 JVM 家族中的优秀一员。Kotlin是一种现代语言(版本1.0于2016年2月发布)。它最初的目的是像Scala那样,优化Java语言的缺陷,提供更加简单实用的编程语言特性,并且解决了性能上的问题,比如编译时间。 JetBrains在这些方面做得非常出色。
Kotlin语言的特性
用 Java 开发多年以后,能够尝试一些新的东西真是太棒了。如果您是 Java 开发人员,使用 Kotlin 将会非常自然流畅。如果你是一个Swift开发者,你将会感到似曾相识,比如可空性(Nullability)。 Kotlin语言的特性有:
1.简洁
大幅减少样板代码量。
2.与Java的100%互操作性
Kotlin可以直接与Java类交互,反之亦然。这个特性使得我们可以直接重用我们的代码库,并将其迁移到 Kotlin中。由于Java的互操作性几乎无处不在。我们可以直接访问平台API以及现有的代码库,同时仍然享受和使用 Kotlin 的所有强大的现代语言功能。
3.扩展函数
Kotlin 类似于 C# 和 Gosu, 它提供了为现有类提供新功能扩展的能力,而不必从该类继承或使用任何类型的设计模式 (如装饰器模式)。
4.函数式编程
Kotlin 语言一等支持函数式编程,就像Scala一样。具备高阶函数、Lambda 表达式等函数式基本特性。
5.默认和命名参数
在Kotlin中,您可以为函数中的参数设置一个默认值,并给每个参数一个名称。这有助于编写易读的代码。
6.强大的开发工具支持
而由于是JetBrains出品,我们拥有很棒的IDE支持。虽然Java到Kotlin的自动转换并不是100% OK 的,但它确实是一个非常好的工具。使用 IDEA 的工具转换Java代码为 Kotlin 代码时,可以轻松地重用60%-70%的结果代码,而且修改成本很小。
Kotlin 除了简洁强大的语法特性外,还有实用性非常强的API以及围绕它构建的生态系统。例如:集合类 API、IO 扩展类、反射API 等。同时 Kotlin 社区也提供了丰富的文档和大量的学习资料,还有在线REPL。
A modern programming language that makes developers happier. Open source forever
https://kotlinlang.org/