1. JAX-RS 规范
JAX-RS(Java API for RESTful Web Services)是 Java 平台的一项规范,旨在为开发 RESTful Web 服务提供支持。JAX-RS 定义了一组标准的 API 和注解,使得开发者可以通过简单的注解方式来开发基于 REST 架构风格的 Web 服务。
实现 JAX-RS 规范的产品有很多,例如:Oracle 开发的 Jersey、JBoss(Red Hat)开发的 Resteasy、Apache CXF、Restlet 等等。
1.1. 特性
- 注解驱动: JAX-RS 提供了一组注解,如
@Path
、@GET
、@POST
、@PUT
、@DELETE
、@Produces
、@Consumes
等,用于简化 RESTful 服务的开发。 - HTTP 方法支持: 直接映射 HTTP 方法到 Java 方法,使得开发者能够轻松实现 RESTful 服务的核心操作。
- 内容协商: 支持通过
@Produces
和@Consumes
注解定义支持的媒体类型(如 JSON、XML),从而实现请求和响应的内容协商。 - 参数绑定: 提供对 URI 路径、查询参数、表单参数、请求头等的简便绑定方式。
- 异常处理: 提供了一种机制来处理 Web 应用中的异常,使得错误处理更为集中和一致。
- 过滤器和拦截器: 允许开发者定义过滤器和拦截器来处理请求和响应的通用逻辑,如身份验证、日志记录等。
1.2. Spring MVC 相似
JAX-RS 和 Spring MVC 都是用于构建 Web 服务的框架,它们提供的注解在功能和命名上确实有许多相似之处。这种相似性主要是因为它们都遵循 REST 架构风格,并且都需要处理 HTTP 请求和响应。
1. 相似之处
注解驱动:
- 两者都使用注解来简化 Web 服务的开发。
- 例如,JAX-RS 使用
@Path
来定义资源路径,Spring MVC 使用@RequestMapping
或@GetMapping
、@PostMapping
等来定义请求路径和方法。
HTTP 方法支持:
- JAX-RS 提供
@GET
、@POST
、@PUT
、@DELETE
等注解,直接映射到 HTTP 方法。 - Spring MVC 提供
@GetMapping
、@PostMapping
、@PutMapping
、@DeleteMapping
等注解,功能类似。
- JAX-RS 提供
参数绑定:
- JAX-RS 使用
@PathParam
、@QueryParam
、@FormParam
等来绑定请求参数。 - Spring MVC 使用
@PathVariable
、@RequestParam
、@RequestBody
等来实现类似功能。
- JAX-RS 使用
内容协商:
- JAX-RS 使用
@Produces
和@Consumes
注解来指定支持的媒体类型。 - Spring MVC 使用
@RequestMapping
的produces
和consumes
属性来实现相同的目的。
- JAX-RS 使用
2. 不同之处
规范 vs 框架:
- JAX-RS 是 Java EE 规范的一部分,定义了构建 RESTful 服务的标准接口。
- Spring MVC 是 Spring 框架的一部分,提供了完整的 Web 应用开发解决方案,并不仅限于 RESTful 服务。
生态系统和集成:
- JAX-RS 实现(如 Jersey、Resteasy)通常与 Java EE 应用服务器(如 WildFly、GlassFish)集成得很好。
- Spring MVC 是 Spring 框架的一部分,与 Spring 的其他模块(如 Spring Boot、Spring Data、Spring Security)集成紧密,广泛用于 Spring 应用程序。
灵活性和扩展性:
- Spring MVC 提供了更为灵活的配置和扩展能力,适合需要复杂业务逻辑和集成需求的应用。
- JAX-RS 专注于 RESTful 服务,通常用于构建轻量级的 Web 服务。
JAX-RS 和 Spring MVC 之间没有直接的依赖关系或继承关系,它们是独立发展的技术栈。但由于它们都旨在解决类似的问题(即构建 Web 服务),因此在设计上出现了许多相似的理念和实现方式。Spring MVC 在 JAX-RS 发布之前就已经存在,但随着 RESTful 风格的普及,Spring MVC 也逐步演变以更好地支持 RESTful API。
2. Resteasy 开发组件
Resteasy 是一个开源框架,专门用于实现 JAX-RS(Java API for RESTful Web Services)规范。它由 JBoss(现为 Red Hat 的一部分)开发和维护,是 WildFly 应用服务器的默认 JAX-RS 实现。Resteasy 提供了一套完整的工具和功能,用于构建 RESTful Web 服务。
Resteasy 提供了一组灵活且强大的组件和模块,使开发者能够高效地构建和扩展 RESTful 服务应用。通过理解和正确使用这些组件,开发者可以定制 JAX-RS 应用的行为,以满足各种业务需求。无论是处理复杂的请求和响应格式,还是集成异步处理和客户端功能,Resteasy 都能提供全面的支持。
1. ResteasyDeployment
功能作用:
ResteasyDeployment
是 Resteasy 的核心配置类,负责管理和配置 JAX-RS 应用程序的各个方面。- 它用于定义和注册资源类、提供者(providers)、异步执行器和其他 JAX-RS 组件。
- 在应用启动时,
ResteasyDeployment
会被初始化,并用于构建 RESTful 服务的运行时环境。
典型用法:
- 在配置 Resteasy 时,通常会创建一个
ResteasyDeployment
实例,并通过它注册资源和提供者。 - 可以通过程序化方式或通过 XML 配置文件进行配置。
- 在配置 Resteasy 时,通常会创建一个
2. Resource
功能作用:
Resource
代表 JAX-RS 中的资源类。资源类是包含业务逻辑的 Java 类,用于处理 HTTP 请求。- 每个资源类通过
@Path
注解定义一个 URI 路径,并通过方法级别的注解(如@GET
、@POST
等)定义具体的操作。
典型用法:
- 开发者创建资源类来实现具体的 RESTful API 端点。
- 在资源类中使用注解来绑定 URI 路径、HTTP 方法和请求参数。
3. Provider
功能作用:
Provider
是用于扩展和定制 JAX-RS 运行时行为的组件。- 它可以用于消息体读写(如 JSON、XML 的序列化和反序列化)、异常映射、上下文解析等。
- 通过实现特定接口并使用
@Provider
注解注册,开发者可以创建自定义的提供者。
典型用法:
- 自定义
MessageBodyReader
和MessageBodyWriter
用于处理特定格式的请求和响应。 - 实现
ExceptionMapper
接口用于统一处理应用程序中的异常。 - 使用
ContextResolver
提供自定义的配置或对象给 JAX-RS 运行时。
- 自定义
4. Interceptors and Filters
功能作用:
- 拦截器和过滤器用于在请求处理的不同阶段插入自定义逻辑。
- 拦截器可以围绕具体的方法调用进行操作,而过滤器通常用于请求和响应的全局处理。
典型用法:
- 实现
ContainerRequestFilter
和ContainerResponseFilter
来对请求和响应进行预处理和后处理,如身份验证、日志记录。 - 使用
@PreMatching
注解在资源匹配之前执行过滤器逻辑。
- 实现
5. Async and Client Modules
功能作用:
- 异步模块支持非阻塞的请求处理,提高应用的响应性能和可扩展性。
- 客户端模块提供功能强大的 API,用于从 Java 应用程序中调用外部 RESTful 服务。
典型用法:
- 使用
@Suspended
和AsyncResponse
处理异步请求。 - 使用
ResteasyClient
类创建和配置 RESTful 客户端。
- 使用
6. ResourceFactory
功能作用:
ResourceFactory
是一个接口,负责创建和管理资源类的实例。- 它允许开发者控制资源类的生命周期,特别是在需要自定义实例创建逻辑或支持不同的作用域(如请求作用域、会话作用域)时。
典型用法:
- 自定义
ResourceFactory
可以用于在实例化资源类之前进行依赖注入或其他初始化操作。 - 在配置 Resteasy 时,可以将自定义的
ResourceFactory
注册到ResteasyDeployment
中。
- 自定义
7. ServletConfig
功能作用:
ServletConfig
是 Servlet 规范中的一个接口,提供对 Servlet 的配置信息的访问。- 在 Resteasy 中,
ServletConfig
可以用于获取初始化参数和 Servlet 上下文,从而在配置 RESTful 服务时使用。
典型用法:
- 在 Resteasy 的
HttpServletDispatcher
中,ServletConfig
用于获取应用的配置参数。 - 开发者可以通过
web.xml
文件配置初始化参数,并在应用启动时读取这些参数。
- 在 Resteasy 的
8. Dispatcher
功能作用:
Dispatcher
是 Resteasy 的核心组件之一,负责将 HTTP 请求分派到相应的 JAX-RS 资源类和方法。- 它处理请求的路由、参数解析、异常处理等。
典型用法:
- Resteasy 在内部使用
Dispatcher
来管理请求的整个生命周期。 - 开发者通常不需要直接与
Dispatcher
交互,但可以通过扩展其行为来定制请求处理过程。
- Resteasy 在内部使用
9. Registry
功能作用:
Registry
是一个接口,用于管理和注册 JAX-RS 资源和提供者。- 它允许动态添加或移除资源类和提供者,支持应用的热部署和配置更新。
典型用法:
- 在应用启动时,通过
ResteasyDeployment
注册资源和提供者。 - 在应用运行时,可以通过
Registry
接口动态更新配置。
- 在应用启动时,通过
10. ResteasyProviderFactory
功能作用:
ResteasyProviderFactory
是一个工厂类,负责创建和管理 JAX-RS 提供者(如MessageBodyReader
、MessageBodyWriter
)。- 它维护一个提供者的注册表,并提供方法来查找和获取合适的提供者实例。
典型用法:
- 开发者可以通过
ResteasyProviderFactory
注册自定义的提供者。 - 在请求处理过程中,Resteasy 使用
ResteasyProviderFactory
来选择合适的提供者进行请求和响应的序列化和反序列化。
- 开发者可以通过
11. HttpServletDispatcher
功能作用:
HttpServletDispatcher
是 Resteasy 提供的一个 Servlet,负责将 HTTP 请求分派到 JAX-RS 资源。- 它集成了 Servlet API 和 JAX-RS API,使得 Resteasy 可以在任何 Servlet 容器中运行。
典型用法:
- 在
web.xml
中配置HttpServletDispatcher
,以便将请求转发到 Resteasy 管理的资源。 - 开发者可以通过
web.xml
配置初始化参数和 URL 映射。
- 在
下面通过一些示例看看如何使用 Resteasy
3. 示例
3.1. Tomcat 示例1
1. maven
<dependency>
<groupId>org.jboss.resteasy</groupId>
<artifactId>resteasy-servlet-initializer</artifactId>
<version>3.0.7.Final</version>
</dependency>
<dependency>
<groupId>javax.servlet</groupId>
<artifactId>javax.servlet-api</artifactId>
<version>3.1.0</version>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>org.apache.tomcat.embed</groupId>
<artifactId>tomcat-embed-core</artifactId>
<version>8.5.95</version>
</dependency>
2. POJO
@NoArgsConstructor
@AllArgsConstructor
@Data
public class CustomVO {
private String name;
private Integer value;
}
3. Resource
定义一个 GET 方法,映射路径是 /example
,返回对象是 CustomVO。
@Path("/example")
public class MyResource {
@GET
@Produces(MediaType.APPLICATION_JSON)
public CustomVO getCustomObject() {
return new CustomVO("SampleName", 42);
}
}
4. Provider
针对 CustomVO 对象,定义序列化内容。
其实如果是常见的 JSON 转换,可以不用针对每个 POJO 定义 Provider,可以通过引入 resteasy-jackson2-provider
依赖默认实现。
@Provider
@Produces(MediaType.APPLICATION_JSON)
public class MyProvider implements MessageBodyWriter<CustomVO> {
@Override
public boolean isWriteable(Class<?> type, Type genericType, Annotation[] annotations, MediaType mediaType) {
return type == CustomVO.class;
}
@Override
public long getSize(CustomVO myCustomObject, Class<?> type, Type genericType, Annotation[] annotations, MediaType mediaType) {
return -1;
}
@Override
public void writeTo(CustomVO myCustomObject, Class<?> type, Type genericType, Annotation[] annotations, MediaType mediaType,
MultivaluedMap<String, Object> httpHeaders, OutputStream entityStream) throws IOException {
String json = "{\"name\":\"" + myCustomObject.getName() + "\", \"value\":" + myCustomObject.getValue() + "}";
entityStream.write(json.getBytes());
}
}
5. 启动类
public class App {
public static void main(String[] args) throws Exception {
ResteasyDeployment deployment = new ResteasyDeployment();
Tomcat tomcat = new Tomcat();
tomcat.setPort(8080);
Context ctx = tomcat.addContext("", null);
Tomcat.addServlet(ctx, "ResteasyServlet", new HttpServletDispatcher());
ctx.addServletMappingDecoded("/*", "ResteasyServlet");
ctx.getServletContext().setAttribute(ResteasyDeployment.class.getName(), deployment);
tomcat.start();
deployment.setResourceClasses(Arrays.asList(MyResource.class.getName()));
deployment.setProviderClasses(Arrays.asList(MyProvider.class.getName()));
tomcat.getServer().await();
}
}
6. 运行结果
执行 main 方法,tomcat 启动之后,然后在 tomcat 服务器上部署了 HttpServletDispatcher
的 Servlet,Resteasy 中配置了一个 Resource。
GET方法请求 http://localhost:8080/example 可以获得 Provider 序列化后的结果。
ResteasyDeployment
类中的 setResourceClasses
和 setProviderClasses
方法用于配置不同类型的组件:
setResourceClasses
:用于注册 JAX-RS 资源类。资源类定义了 RESTful API 的端点和业务逻辑。setProviderClasses
:用于注册 JAX-RS 提供者类。提供者类用于扩展和定制 JAX-RS 的行为,例如消息体的读写、异常映射、上下文解析等。
3.2. Tomcat 示例2
这个示例要稍微复杂一点,但实际可以扩展的功能要更丰富些。为什么要单独拿出来这个示例,因为在看 Dubbo
框架 RestProtocol
协议的实现方法时,看到就是这么使用的。
具体细节可以看看com.alibaba.dubbo.rpc.protocol.rest.DubboHttpServer
中有关 Resteasy 的部署使用,示例中就保留对 Resteasy 组件的使用,通过简单的 Demo 来执行。
1. maven
<dependency>
<groupId>org.jboss.resteasy</groupId>
<artifactId>resteasy-servlet-initializer</artifactId>
<version>3.0.7.Final</version>
</dependency>
<dependency>
<groupId>javax.servlet</groupId>
<artifactId>javax.servlet-api</artifactId>
<version>3.1.0</version>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>org.apache.tomcat.embed</groupId>
<artifactId>tomcat-embed-core</artifactId>
<version>8.5.95</version>
</dependency>
2. POJO
@NoArgsConstructor
@AllArgsConstructor
@Data
public class UserVO {
private String name;
private Integer age;
}
3. Provider
@Provider
@Produces(MediaType.TEXT_PLAIN)
public class UserProvider implements MessageBodyWriter<UserVO> {
@Override
public boolean isWriteable(Class<?> type, Type genericType, Annotation[] annotations, MediaType mediaType) {
return type == UserVO.class;
}
@Override
public long getSize(UserVO myCustomObject, Class<?> type, Type genericType, Annotation[] annotations, MediaType mediaType) {
return -1;
}
@Override
public void writeTo(UserVO userVO, Class<?> type, Type genericType, Annotation[] annotations, MediaType mediaType,
MultivaluedMap<String, Object> httpHeaders, OutputStream entityStream) throws IOException {
String json = userVO.getName()+"'s age is "+userVO.getAge() ;
entityStream.write(json.getBytes());
}
}
4. Resource 1
@Path("/say")
public class SayResource {
@GET
@Path("/hello")
@Produces(MediaType.TEXT_PLAIN)
public String hello() {
return "Hello!";
}
@GET
@Path("/world")
@Produces(MediaType.TEXT_PLAIN)
public String world() {
return "World!";
}
@GET
@Path("/user")
public UserVO user(){
return new UserVO("Tom", 12);
}
}
5. Resource 2
@Path("/action")
public class ActionResource {
@GET
@Path("/run")
@Produces(MediaType.TEXT_PLAIN)
public String run() {
return "Run!";
}
@GET
@Path("/fly")
@Produces(MediaType.TEXT_PLAIN)
public String fly() {
return "Fly!";
}
}
6. ServletConfig
public class CustomServletConfig implements ServletConfig {
private final Map<String, String> initParameters;
private final ServletContext servletContext;
public CustomServletConfig(ServletContext servletContext) {
this.servletContext = servletContext;
this.initParameters = new HashMap<>();
}
@Override
public String getServletName() {
return "ResteasyServlet";
}
@Override
public ServletContext getServletContext() {
return servletContext;
}
/**
* org.apache.catalina.servlets.DefaultServlet#init() 等实现类,会调用该方法读取环境属性配置
*
* @param name the name of the initialization parameter whose value to
* get
* @return Parameter
*/
@Override
public String getInitParameter(String name) {
return initParameters.get(name);
}
@Override
public Enumeration<String> getInitParameterNames() {
return new Vector<>(initParameters.keySet()).elements();
}
}
7. HttpServletDispatcher
public class DispatcherServlet extends HttpServlet {
private final HttpServletDispatcher dispatcher;
public DispatcherServlet(HttpServletDispatcher dispatcher) {
this.dispatcher = dispatcher;
}
protected void service(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
dispatcher.service(request,response);
}
}
8. 启动类
public class Main {
public static void main(String[] args) throws Exception {
ResteasyDeployment deployment = new ResteasyDeployment();
HttpServletDispatcher servletDispatcher = new HttpServletDispatcher();
Tomcat tomcat = new Tomcat();
tomcat.setPort(8080);
Context context = tomcat.addContext("", null);
Tomcat.addServlet(context, "servletDispatcher", new DispatcherServlet(servletDispatcher));
context.addServletMappingDecoded("/*", "servletDispatcher");
ServletContext servletContext = context.getServletContext();
tomcat.start();
servletContext.setAttribute(ResteasyDeployment.class.getName(), deployment);
ServletConfig servletConfig = new CustomServletConfig(servletContext);
servletDispatcher.init(servletConfig);
deployment.getRegistry().addResourceFactory(new SingletonResource(new SayResource()));
deployment.getRegistry().addResourceFactory(new SingletonResource(new ActionResource()));
deployment.getProviderFactory().registerProviderInstance(new UserProvider());
tomcat.getServer().await();
}
}
9. 执行结果
访问 http://localhost:8080 下列路径均可访问:
- /say/hello
- /say/world
- /say/user
- /action/run
- /action/fly
4. Resteasy 特点
4.1. 单一Servlet
示例2中,配置了2个 Resource
,每个 Resource 又有多个方法映射不同 @Path
。但对于 Tomcat 服务器来说,其实就部署了一个 Servlet - HttpServletDispatcher
。
在使用 Resteasy 和嵌入式 Tomcat 的情况下,无论你添加多少个 JAX-RS 资源,它们通常都部署在同一个 Servlet 上。这是因为 Resteasy 的 HttpServletDispatcher
作为一个单一的 Servlet 来处理所有的 JAX-RS 请求。
4.1.1. 工作机制
单一 Servlet:
- Resteasy 使用
HttpServletDispatcher
作为一个统一的前端控制器(Front Controller)。 - 这个 Servlet 负责拦截所有传入的 HTTP 请求,并将它们路由到适当的 JAX-RS 资源方法。如:示例2中
ctx.addServletMappingDecoded("/*", "ResteasyServlet")
,拦截所有/*
路径下的请求。
- Resteasy 使用
路径解析:
- 每个 JAX-RS 资源类和方法上的
@Path
注解定义了该资源处理的 URI 路径。 HttpServletDispatcher
通过解析请求的 URI 路径,将请求映射到相应的资源类和方法。
- 每个 JAX-RS 资源类和方法上的
集中管理:
- 通过这种集中式的请求处理机制,开发者只需要在应用中配置一个
HttpServletDispatcher
,而不必为每个资源类单独配置 Servlet。 - 这种方式简化了配置、应用的部署和管理,使应用更易于扩展和维护。
- 通过这种集中式的请求处理机制,开发者只需要在应用中配置一个
4.1.2. 好处
通过使用单一的 HttpServletDispatcher
,Resteasy 提供了一种高效、简洁和灵活的方式来管理和处理 Web 服务请求。这种设计模式不仅降低了开发和维护的复杂性,还提高了应用的性能和可扩展性。对于开发者来说,这意味着可以更专注于业务逻辑的实现,而不必过多关注底层的请求管理细节。
1. 简化配置
- 集中管理: 只需要配置一个
HttpServletDispatcher
,无需为每个资源类单独配置 Servlet。这减少了配置的复杂性,特别是在大型应用中,管理多个资源类时更加方便。
2. 易于维护和扩展
- 统一入口: 所有请求都通过同一个入口处理,使得日志记录、错误处理、身份验证等跨切面逻辑可以在一个地方统一管理和应用。
- 易于扩展: 添加新的资源类和路径只需在代码中进行,无需额外的配置更改。
3. 提高性能
- 优化资源使用: 通过集中管理,可以更有效地使用服务器资源,因为只需一个 Servlet 实例来处理所有请求。
- 减少上下文切换: 由于所有请求都通过同一个 Servlet 处理,减少了不同 Servlet 之间的上下文切换开销。
4. 增强灵活性
- 路径映射灵活性: 通过
@Path
注解,可以灵活地定义 URI 路径,使得应用程序的路由更加直观和可维护。 - 中间件集成: 由于有一个统一的入口,集成诸如安全、事务管理、监控等中间件变得更加简单和一致。
5. 支持跨切面功能
- 过滤器和拦截器: 可以在
HttpServletDispatcher
层面应用全局的过滤器和拦截器,处理请求和响应的通用逻辑,如身份验证、日志记录、CORS 处理等。 - 异常处理: 提供集中化的异常处理机制,通过全局异常映射器可以一致地处理应用中的异常。
6. 与 JAX-RS 标准兼容
- 标准化: Resteasy 作为 JAX-RS 的实现,使用
HttpServletDispatcher
保持了与 JAX-RS 标准的兼容性,使得应用可以在不同的 JAX-RS 实现之间更容易地移植。
4.2. Spring MVC 相似
之前在内嵌 Tomcat 的文章中讲过,SpringBoot 的实现也是部署一个 Servlet。那 SpringBoot 和 Dubbo 一样,也是用 Resteasy 实现的吗?
Spring MVC 的 DispatcherServlet
和 Resteasy 的 HttpServletDispatcher
都是用于处理 HTTP 请求的核心组件,虽然它们属于不同的框架并服务于不同的架构模式,但它们之间仍然有一些相似之处和不同之处。以下是它们的详细比较:
1. 相似点
HTTP 请求处理:
- 两者都是基于 Servlet 的实现,用于接收和处理 HTTP 请求。
请求路由:
- 都负责将请求路由到合适的处理器(Spring 中是控制器方法,Resteasy 中是 JAX-RS 资源方法)。
扩展支持:
- 都支持通过注解配置来简化请求的映射和处理。
- 都可以通过注册扩展(如提供者或拦截器)来增强功能。
Servlet 机制:
- 都是作为 Servlet 在应用服务器(如 Tomcat)中运行,利用 Servlet 规范的生命周期和配置机制。
2. 不同点
框架背景与目标:
DispatcherServlet:
- 属于 Spring MVC 框架的一部分,主要用于构建 Web 应用,尤其是支持 MVC 模式的应用。
- 提供了视图解析、数据绑定、验证等丰富功能。
HttpServletDispatcher:
- 属于 JAX-RS 的实现之一(Resteasy),专注于构建 RESTful API 服务。
- 主要处理 RESTful 请求,关注于 HTTP 方法和路径的映射。
配置与自动化:
DispatcherServlet:
- Spring Boot 提供了自动配置,开发者通常不需要手动配置。
- 高度可配置,通过 Spring 配置文件或 Java 配置类进行调整。
HttpServletDispatcher:
- 通常需要手动配置,特别是在嵌入式服务器环境中。
- 配置相对简单,以注解驱动的方式为主。
功能范围:
DispatcherServlet:
- 提供全面的 Web 应用支持,包括会话管理、模板渲染、国际化等。
- 支持复杂的 Web 应用场景。
HttpServletDispatcher:
- 专注于 RESTful 风格的服务,处理 JSON/XML 等数据格式。
- 适合轻量级服务和 API 开发。
生态系统与集成:
DispatcherServlet:
- 支持 Spring 的整个生态系统,包括 Spring Security、Spring Data 等。
- 适合构建复杂的企业级应用。
HttpServletDispatcher:
- 集成 JAX-RS 标准,适用于与其他 JAX-RS 实现的互操作。
- 更专注于服务和 API 的开发。
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。