天下事有难易乎?为之,则难者亦易矣;不为,则易者亦难矣。

使用 Java @Annotations 构建完整的 Spring Boot REST API

往事如烟 503次浏览 0个评论

点击“终码一生”,关注,置顶公众号

每日技术干货,第一时间送达!



本文旨在演示用于构建功能性 Spring Boot REST API 的重要 Java @annotations。Java 注解的使用使开发人员能够通过简单的注解来减少代码冗长。


例如,我们可以参考交易。通过使用事务模板的标准程序化处理,这需要编写更复杂的配置和样板代码,而这可以通过简单的@Transactional 声明性注释来实现。


在 Java 编程语言中,注解是一种语法元数据,可以添加到 Java 源代码中。Java 注释也可以嵌入到 Java 编译器生成的 Java 类文件中并从中读取。这允许 Java 虚拟机在运行时保留注释并通过反射读取。对注解的支持从版本 5 开始,允许不同的 Java 框架采用这些资源。


注释也可以在 REST API 中使用。REST 代表 Representational State Transfer,是一种用于设计分布式应用程序的架构风格。由 Roy Fielding 博士带来。在论文中,他提出了客户端和服务器之间应该分开的六项原则;客户端和服务器之间的通信应该是无状态的;它们之间可以存在多个层次结构;服务器响应必须声明为可缓存或不可缓存;其接口的统一性必须基于客户端、服务器和中间组件之间的所有交互,最终客户端可以通过按需使用代码来扩展其功能。



1

案例分析


API 是一个简单的模块,用于从更复杂的系统中实现业务实体的 CRUD 操作,旨在协调和协调与企业、机构和实体组相关的经济信息。为简单起见,API 使用 H2 内存数据库。


项目结构


项目结构由三个模块构成,但本文将重点介绍管理实体的模块。该模块依赖于 Common 模块,它与整个系统的其余部分共享错误处理和必要的有用类等内容。示例代码可从 GitHub 存储库访问。


https://github.com/jailsonevora/spring-boot-api-communication-through-kafka


让我们开始吧。



2

Spring Boot 自动配置


Spring Boot 的巨大优势在于我们可以专注于业务规则,从而避免一些繁琐的开发步骤、样板代码和更复杂的配置,从而改进开发并简化新 Spring 应用程序的引导。


为了开始配置新的 Spring Boot 应用程序,Spring Initializr 创建了一个简单的 POJO 类来配置应用程序的初始化。我们有两种方式来装饰配置。一种是@SpringBootApplication当我们的解决方案中的模块较少时使用注释。


如果我们有一个结构更复杂的解决方案,我们需要将不同的路径或我们模块的基本包指定给 Spring Boot 应用程序初始化程序类。@EnableAutoConfiguration我们可以使用和@ComponentScan注释来实现它。@EnableAutoConfiguration指示 Spring Boot 根据类路径设置、其他 bean 和各种属性设置开始添加 bean,同时@ComponentScan允许 spring 在包中查找其他组件、配置和服务,让它在其他组件中找到控制器。


这两个注解不能同时使用@SpringBootApplication。@SpringBootApplication是添加所有这些的便利注释。它等同于使用@Configuration, @EnableAutoConfiguration, 和@ComponentScan它们的默认属性。


package com.BusinessEntityManagementSystem;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
import org.springframework.boot.autoconfigure.domain.EntityScan;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.jpa.repository.config.EnableJpaAuditing;
import org.springframework.data.jpa.repository.config.EnableJpaRepositories;

@EnableAutoConfiguration
@EnableJpaAuditing
@EnableJpaRepositories(basePackages = {"com.BusinessEntityManagementSystem"})
@EntityScan(basePackages = {"com.BusinessEntityManagementSystem"})
@ComponentScan(basePackages = {"com.BusinessEntityManagementSystem"})
@Configuration
public class BusinessEntityManagementApplication {

    public static void main(String[] args) {

        SpringApplication.run(BusinessEntityManagementApplication.class, args);
    }
}


代码片段中的另一个注释是@EnableJpaAuditing. 审计允许系统跟踪和记录与持久实体或实体版本相关的事件。还与 JPA 配置相关,我们有@EnableJpaRepositories. 此注释启用 JPA 存储库。默认情况下,它将扫描带注释的配置类的包以查找 Spring Data 存储库。在这个注解中,我们指定要扫描注解组件的基本包。


要在项目结构中查找 JPA 实体,我们必须指示自动配置使用@EntityScan扫描包。对于特定的扫描,我们可以指定basePackageClasses(),basePackages()或其别名value()来定义要扫描的特定包。如果未定义特定的包,则会从带有此注解的类的包中进行扫描。


Spring Boot Initializr 创建的类中的最后一个注解是@Configuration. @Configuration将类标记为应用程序上下文的 bean 定义源。这可以应用于我们需要的任何配置类。


 

3

Swagger UI 配置中的 Java @Annotations


文档是任何项目的一个重要方面,因此我们的 REST API 使用 Swagger-UI 进行记录,这是许多标准元数据之一。Swagger 是用于创建交互式 REST API 文档的规范和框架。它使文档能够与对 REST 服务所做的任何更改保持同步。它还提供了一组工具和 SDK 生成器,用于生成 API 客户端代码。


在 Swagger-UI 类配置中,出现在@Configuration. 如上所述,这向 Spring Boot 自动配置表明一个类是一个可能包含 bean 定义的配置类。


package com.BusinessEntityManagementSystem;

import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import springfox.documentation.builders.PathSelectors;
import springfox.documentation.builders.RequestHandlerSelectors;
import springfox.documentation.service.ApiInfo;
import springfox.documentation.service.Contact;
import springfox.documentation.spi.DocumentationType;
import springfox.documentation.spring.web.plugins.Docket;
import springfox.documentation.swagger2.annotations.EnableSwagger2;

import java.util.Collections;

@Configuration
@EnableSwagger2
@EnableAutoConfiguration
public class SwaggerConfig {
    @Bean
    public Docket api() {
        return new Docket(DocumentationType.SWAGGER_2)
                .select()
                .apis(RequestHandlerSelectors.basePackage("com.BusinessEntityManagementSystem"))
                .paths(PathSelectors.ant("/api/businessEntityManagementSystem/v1/**"))
                .build()
                .apiInfo(metaData());
    }
...



Swagger 的一个特定注释是@EnableSwagger2。它表明应该启用 Swagger 支持并加载所有在 swagger java-config 类中定义的必需 bean。这应该应用于 Spring java 配置,并且应该有一个随附的@Configuration注释。@Bean是方法级别的注释,是 XML 元素的直接模拟<bean/>。


 

4

领域模型


MVC 是 Spring Framework 中最重要的模块之一。它是UI设计中常见的设计模式。它通过分离模型、视图和控制器的角色将业务逻辑与 UI 分离。MVC 模式的核心思想是将业务逻辑从 UI 中分离出来,允许它们独立更改而不相互影响。


在此设计模式中,M 代表模型。该模型负责封装应用程序数据以供视图呈现。它代表了数据和业务逻辑的形状。模型对象检索模型状态并将其存储在数据库中。它的模型通常由服务层处理并由持久层持久化的领域对象组成。


TYPE Java @Annotations


在模型类中,我们使用@Entity注释来指示该类是 JPA 实体。JPA 将知道 POJO 类可以存储在数据库中。如果我们没有定义@Table注解,Spring config 将假定这个实体被映射到一个类似于 POJO 类名的表。因此,在这些情况下,我们可以使用@Table注解指定表名。


当模型属性定义了延迟加载时,为了处理与使用 Jackson API 进行模型序列化相关的问题,我们必须告诉序列化器忽略 Hibernate 添加到类中的链或有用的垃圾,以便它可以管理延迟加载通过声明@JsonIgnoreProperties({“hibernateLazyInitializer”, “handler”})注释来获取数据。


package com.BusinessEntityManagementSystem.models;

import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
import com.BusinessEntityManagementSystem.interfaces.models.IBusinessEntityModel;
import org.hibernate.annotations.OnDelete;
import org.hibernate.annotations.OnDeleteAction;

import javax.persistence.*;
import javax.validation.constraints.NotNull;
import javax.validation.constraints.Size;
import java.io.Serializable;
import java.util.HashSet;
import java.util.Set;

@Entity
@Table(name="BEMS_BusinessEntity")
@JsonIgnoreProperties({"hibernateLazyInitializer", "handler"})
public class BusinessEntityModel<IAddressModel extends AddressModel, IPartnerModel extends PartnerModel, IStoreModel extends StoreModel, IAffiliatedCompanyModel extends AffiliatedCompanyModel, IEconomicActivityCodeModel extends EconomicActivityCodeModel> extends AuditModel<String> implements IBusinessEntityModel, Serializable {

    @Id
    @GeneratedValue(strategy = GenerationType.AUTO)
    @NotNull
    @Column(name = "ID_BusinessEntity", updatable = false, nullable = false)
    private long id;

...


FIELD Java @Annotations


对于一个类字段,有多种注解取决于该字段的类型和用途。例如,@Id注释必须在类属性之一中声明。存储在数据库中的每个实体对象都有一个主键。一旦分配,主键就不能被修改。@GeneratedValue指示框架应使用指定的生成器类型(如 {AUTO、IDENTITY、SEQUENCE 和 TABLE})生成文档键值。


另一个针对域模型字段的有趣注释是@NotNull. 声明带注释的元素不能是常见的 Spring 注释null。它也可以用在方法或参数中。注释指定数据库列的@Column名称以及表行为。可以设置此行为以防止其被更新或为空。


有时大多数对象都有一个自然标识符,因此 Hibernate 还允许将此标识符建模为实体的自然标识符,并提供额外的 API 用于从数据库中检索它们。这是使用@NaturalId注释来实现的。


...

    @Id
    @GeneratedValue(strategy = GenerationType.AUTO)
    @NotNull
    @Column(name = "ID_BusinessEntity", updatable = false, nullable = false)
    private long id;

    @NaturalId
    @NotEmpty
    @NotNull
    @Column(name = "NATURAL_ID", updatable = false, nullable = false)
    private long naturalId;

    @Column(name = "TaxId")
    @Size(min = 1, max = 50)
    private String taxId;

    @Column(name = "Status")
    private int status = 1;

...


如果我们想防止一个实体的元素不为空也不为空,我们也可以用 注释它@NotEmpty。它针对大量元素,因为{METHOD,FIELD,ANNOTATION_TYPE,CONSTRUCTOR,PARAMETER,TYPE_USE}. @Size注释划定了被注释元素的边界。边界由两个属性 min 和 max 指定。


关系 Java @Annotations


任何 ORM 机制最重要的特性之一是如何指定从对象之间的关系到其数据库对应项的映射。在下面的代码中,有一个@OneToOne注解来描述BusinessEntity类与Address类模型之间的关系。@JoinColumn注释指定在此关系中将被视为外键的列。


除了@OneToOne注释,我们还可以管理多对多关系。@ManyToMany注释描述了与Partner类成员的关系。与其他关系注释一样,也可以指定级联规则以及获取类型。根据所选择的级联设置,当BusinessEntity删除 a 时,关联的Partner也将被删除。


...
    // region OneToOne

    @OneToOne(fetch = FetchType.EAGER, cascade = CascadeType.ALL, targetEntity= AddressModel.class)
    @JoinColumn(name = "Address", nullable = false)
    @OnDelete(action = OnDeleteAction.CASCADE)
    private IAddressModel address;

    // endregion OneToOne


    // region ManyToMany

    @ManyToMany(fetch = FetchType.LAZY,
            cascade = {
                    CascadeType.ALL
            },
            targetEntity= PartnerModel.class
    )
    @JoinTable(name = "BSUF_BusinessEntity_Partner",
            joinColumns = { @JoinColumn(name = "ID_BusinessEntity") },
            inverseJoinColumns = { @JoinColumn(name = "ID_Partner") })
    private Set<IPartnerModel> partner = new HashSet<>();

// endregion ManyToMany
...


与@ManyToMany注释一起,我们指定@JoinTable注释,允许我们在多对多关系中使用两个基本属性joincolumns为我们声明@ManyToMany注释的类和inverseJoinColumns另一个表定义其他两个相关表之间的桥接表。


...
  // inverser many to many

  @ManyToMany(fetch = FetchType.LAZY,
              cascade = {
                CascadeType.PERSIST,
                CascadeType.MERGE
              },
              mappedBy = "partner")
  @JsonIgnore
  private Set<IBusinessEntityModel> businessEntity = new HashSet<>();
...


在另一个表中,建议也定义逆关系。此声明与与业务实体模型相关的代码中显示的内容略有不同。反向关系声明通过属性“ mappedBy. ”来区分。



5

数据传输对象


数据传输对象是一种非常流行的设计模式。它是一个定义数据如何通过网络发送的对象。DTO 仅用于传递数据,不包含任何业务逻辑。


TYPE Java @Annotations


有时,我们需要通过 JSON 在实体之间传输数据。要序列化和反序列化 DTO 对象,我们需要使用 Jackson 注释对这些对象进行注释。

@JsonInclude(JsonInclude.Include.NON_NULL)指示何时可以序列化带注释的属性。通过使用这个注解,我们可以根据属性值指定简单的排除规则。它可以用于字段、方法或构造函数参数。它也可以用在类中,在某些情况下,指定的规则适用于类的所有属性。


package com.BusinessEntityManagementSystem.dataTransferObject;

import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
import com.fasterxml.jackson.annotation.JsonProperty;

/*@JsonInclude(JsonInclude.Include.NON_NULL)*/
@JsonIgnoreProperties({"hibernateLazyInitializer", "handler"})
public class BusinessEntity {
...


@JsonIgnoreProperties({“hibernateLazyInitializer”, “handler”})允许 Jackson 忽略 Hibernate 创建的垃圾,因此它可以管理前面提到的数据的延迟加载。


FIELD Java @Annotations


DTO 对象中的字段也可能具有不同类型的注释。@JsonProperty注释用于指定序列化属性的名称。@JsonIgnore在类属性级别进行注释以忽略它。除了@JsonIgnore,还有@JsonIgnoreProperties和@JsonIgnoreType。这两个注释都是 Jackson API 的一部分,用于忽略 JSON 序列化和反序列化中的逻辑属性。


package com.BusinessEntityManagementSystem.dataTransferObject;

import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
import com.fasterxml.jackson.annotation.JsonProperty;

/*@JsonInclude(JsonInclude.Include.NON_NULL)*/
@JsonIgnoreProperties({"hibernateLazyInitializer", "handler"})
public class BusinessEntity {

    @JsonProperty("naturalId")
    private long naturalId;

    @JsonProperty("taxId")
    private String taxId;

    @JsonProperty("status")
    private int status = 1;
...


Jackson API 是用于 Java 的高性能 JSON 处理器。它提供了许多有用的注释来应用于 DTO 对象,允许我们将对象从 JSON 序列化和反序列化为 JSON。



6

控制器


控制器代表 MVC 模式中的 C。控制器负责接收用户的请求并调用后端服务进行业务处理。处理后,它可能会返回一些数据以供视图呈现。控制器收集它并准备模型以供视图呈现。控制器通常称为调度程序 servlet。它作为 Spring MVC 框架的前端控制器,每个 Web 请求都必须经过它,以便它可以管理整个请求处理过程。当一个 Web 请求被发送到 Spring MVC 应用程序时,控制器首先接收该请求。然后,它组织在 Spring 的 Web 应用程序上下文中配置的不同组件或控制器本身中存在的注释,所有这些都需要处理请求。


TYPE Java @Annotations


要在 Spring Boot 中定义控制器类,必须用@RestController注解标记类。@RestController注解是一个便利注解,它本身用@Controller和@ResponseBody注解。带有此注解的类型被视为控制器,其中@RequestMapping方法默认采用@ResponseBody语义。value 属性可以指示对逻辑组件名称的建议,以在自动检测到组件的情况下将其转换为 Spring bean。


package com.BusinessEntityManagementSystem.v1.controllers;

import com.BusinessEntityManagementSystem.dataAccessObject.IBusinessEntityRepository;
import com.BusinessEntityManagementSystem.interfaces.resource.IGenericCRUD;
import com.BusinessEntityManagementSystem.models.BusinessEntityModel;
import com.Common.Util.Status;
import com.Common.exception.ResourceNotFoundException;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.Pageable;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.servlet.support.ServletUriComponentsBuilder;

import javax.persistence.EntityManager;
import javax.persistence.PersistenceContext;
import javax.transaction.Transactional;
import javax.validation.Valid;
import java.util.Optional;

@RestController("BusinessEntityV1")
@RequestMapping("/api/businessEntityManagementSystem/v1")
@Api(value = "businessEntity")
public class BusinessEntityController implements IGenericCRUD<BusinessEntityModel> {
...


构造函数和方法 Java @Annotations


当带有@RestController 注释的类收到请求时,它会寻找适当的处理程序方法来处理请求。这要求控制器通过一个或多个处理程序映射将每个请求映射到处理程序方法。为此,控制器类的方法用@RequestMapping注解修饰,使它们成为处理方法。


出于 Swagger 文档的目的,@ApiOperation注释用于声明 API 资源中的单个操作。操作被认为是路径和 HTTP 方法的唯一组合。只有带有注释的方法@ApiOperation才会被扫描并添加到 API 声明中。一些处理程序或操作需要使用事务来确保数据完整性和一致性。


事务管理是企业应用程序中确保数据完整性和一致性的一项基本技术。Spring 支持编程式和声明式(@Transactional)事务管理。


...
 @Autowired
    public BusinessEntityController(IBusinessEntityRepository businessEntityRepository) {
        this.businessEntityRepository = businessEntityRepository;
    }

    @RequestMapping(value = "/businessEntity/{id}", method = RequestMethod.GET, produces = "application/json")
    @ApiOperation(value = "Retrieves given entity", response=BusinessEntityModel.class)
    public ResponseEntity<?> get(@Valid @PathVariable Long id){
        checkIfExist(id);
        return new ResponseEntity<> (businessEntityRepository.findByIdAndStatus(id, Status.PUBLISHED.ordinal()), HttpStatus.OK);
    }

@Transactional
    @RequestMapping(value = "/businessEntity", method = RequestMethod.POST, produces = "application/json")
    @ApiOperation(value = "Creates a new entity", notes="The newly created entity Id will be sent in the location response header")
    public ResponseEntity<Void> create(@Valid @RequestBody BusinessEntityModel newEntity){

        return new ResponseEntity <>(null, getHttpHeaders(businessEntityRepository.save(newEntity)), HttpStatus.CREATED);

    }
...


以编程方式管理事务,我们必须在每个事务操作中包含事务管理代码(样板代码)。结果,样板事务代码在这些操作中的每一个中重复。在大多数情况下,声明式事务管理比程序化事务更可取。它是通过声明将事务管理代码与我们的业务方法分离来实现的。这可以帮助我们更轻松地为我们的应用程序启用事务并定义一致的事务策略,尽管声明式事务管理不如程序化事务管理灵活。程序化事务管理允许我们通过代码控制事务。


在精心设计的系统中使用的另一个有用的注解是@Autowired.@Autowired可以在构造方法中使用来解析协作 bean 并将其注入到 bean 中,从而引导我们更好地设计应用程序。使用接口与实现分离的原则和依赖注入模式开发的应用程序易于测试,无论是单元测试还是集成测试,因为该原则和模式可以减少我们应用程序不同单元之间的耦合。


参数 Java @Annotations


除了身份验证和授权之外,构建安全 Web 服务的一个重要领域是确保输入始终得到验证。Java Bean 注解提供了实现输入验证的机制。我们可以通过@Valid在方法参数中使用注解来实现。


我们的类应该在处理软删除之前验证传入的标识符请求。通过简单地将@Valid注解添加到方法中,Spring 将确保传入的标识符请求首先通过我们定义的验证规则运行。


...
  @Transactional
    @RequestMapping(value = "/businessEntity/{id}", method = RequestMethod.DELETE, produces = "application/json")
    @ApiOperation(value = "Deletes given entity")
    public ResponseEntity<Void> delete(@Valid @PathVariable Long id, @Valid @RequestBody String lastModifiedBy){

        Optional<BusinessEntityModel> softDelete = businessEntityRepository.findByIdAndStatus(id, Status.PUBLISHED.ordinal());
        if (softDelete.isPresent()) {
            BusinessEntityModel afterIsPresent = softDelete.get();
            afterIsPresent.setStatus(Status.UNPUBLISHED.ordinal());
            afterIsPresent.setLastModifiedBy(lastModifiedBy);
            businessEntityRepository.save(afterIsPresent);
            return new ResponseEntity<>(HttpStatus.OK);
        }
        else
            throw new ResourceNotFoundException("Entity with id " + id + " not found");
    }
...


@PathVariable, 以及@RequestParam, 用于从 HTTP 请求中提取值,它们之间存在细微差别。@RequestParam用于从 URL ( https://www.jeevora.com/&#8230;?id=1) 获取请求参数,也称为查询参数,同时@PathVariable从 URI ( ) 中提取值,https://www.jeevora.com/id/1如我们的案例研究所示。


@RequestBodyannotation 表示方法参数应该绑定到 Web 请求的正文,而@ResponseBody表示方法返回值应该绑定到 Web 响应正文。



7

数据访问对象


一个典型的设计错误是将不同类型的逻辑(例如表示逻辑、业务逻辑和数据访问逻辑)混合在一个大模块中。由于它引入了紧密耦合,这降低了模块的可重用性和可维护性。数据访问对象 (DAO) 模式的一般目的是通过将数据访问逻辑与业务逻辑和表示逻辑分开来避免这些问题。此模式建议将数据访问逻辑封装在称为数据访问对象 [3] 的独立模块中。


存储库或数据访问对象 (DAO) 提供与数据存储交互的抽象。存储库传统上包括一个接口,该接口提供一组查找器方法,例如findById,findAll用于检索数据,以及持久化和删除数据的方法。存储库还包括一个使用数据存储特定技术实现此接口的类。习惯上每个域对象有一个存储库。尽管这是一种流行的方法,但在每个存储库实现中都有大量的样板代码重复。


package com.BusinessEntityManagementSystem.dataAccessObject;

import org.springframework.data.domain.Page;
import org.springframework.data.domain.Pageable;
import org.springframework.data.repository.NoRepositoryBean;
import org.springframework.data.repository.PagingAndSortingRepository;

import java.util.Optional;

@NoRepositoryBean
public interface IGenericRepository<T> extends PagingAndSortingRepository<T, Long> {

    Optional<T> findByIdAndStatus(long id, int status);

    Page<T> findAllByStatus(int status, Pageable pageable);
}


使用@NoRepositoryBean注解,我们可以使用它来排除存储库接口被拾取,从而获得一个正在创建的实例。这通常用于为所有存储库提供扩展基接口并结合自定义存储库基类来实现在该中间接口中声明的方法。在这种情况下,我们通常从中间接口派生出具体的存储库接口,但我们不想为中间接口创建 Spring bean。


参考

[1] Balaji Varanasi, Sudha Belida, Spring REST – Rest and Web Services development using Spring, 2015;

[2] Ludovic Dewailly,使用 Spring 构建 RESTful Web 服务 – 使用 Spring 框架构建企业级、可扩展的 RESTful Web 服务的动手指南,2015;

[3] Marten Deinum, Daniel Rubio, Josh Long, Spring 5 Recipes: A Problem-Solution Approach, 2017。


PS:防止找不到本篇文章,可以收藏点赞,方便翻阅查找哦。


END

 



往期推荐



免费给 Spring Boot 加个证书

Git 不能只会 pull 和 push,试试这5条提高效率的命令吧!

玩转 Java8 Stream,常用方法大合集

请不要“妖魔化”外包…

美哭了,一款面向程序员的 Markdown 笔记应用

Linux 受到开发者偏爱的 9 个理由!



ITZOO版权所有丨如未注明 , 均为原创丨转载请注明来自IT乐园 ->使用 Java @Annotations 构建完整的 Spring Boot REST API
发表我的评论
取消评论
表情 贴图 加粗 删除线 居中 斜体 签到

Hi,您需要填写昵称和邮箱!

  • 昵称 (必填)
  • 邮箱 (必填)
  • 网址