



前面说过,在本书中我们将要搭建的一个电子商城示例项目,对于该项目,大概每个读者都不陌生,因此在这里不再详细列出需求。为了方便接下来的讲解和讨论,示例项目中对功能进行了简化,仅涉及用户管理和商品管理两个部分,而其他功能,如订单、支付、优惠券管理等,则不在讨论范围内。
在实际的Web应用项目开发中往往会采用分层架构体系进行开发,架构分层可以让我们所开发的系统更易于适应变化。而架构分层采用最广泛的就是三层架构开发,这三层分别说明如下。
·客户端层UI层:主要用来与用户进行交互,显示数据并接收用户的输入,也常称为前端。一般对于一个应用通常会存在多种客户端,如Web、H5、App等。
·应用层:是系统核心价值部分,其关注业务规则的制定和业务流程的实现,负责与UI层进行交互及数据存储的处理,我们常称为后端。
·存储层:也称为持久层,通常是一个数据库,主要用来保存我们的业务数据。当然这里数据存储指的不仅仅是关系型数据库,也包含非关系型数据库,如MongoDB、Redis或者文件存储系统,在我们存储附件、多媒体文件或图片时使用。
上述三层应用架构指的是在整个应用架构上的划分,其实对于后端(应用层)的开发在架构搭建时往往也可以分为以下三层。
·业务逻辑层:该层主要承担两大职责,一是定义业务领域对象,或称为业务实体;二是业务逻辑的具体实现。业务实体常称为Domain,而业务逻辑则是Service。
·接口层(API层):该层用来对接UI层,为UI层提供数据集业务处理接口,一般将其称为Controller。此外,现在的应用开发一般会对接多种用户端UI层,所以常常在这里使用REST方式提供API接口,供各个应用端使用,因此也可以称之为API层。
·数据接口层(DAO):负责业务实体对象的数据处理,如增、删、改、查等,通常定义为Repository。该层开发时往往会使用O/R Mapping技术,如Hibernate、JPA等。此外,数据接口层也包含对非关系型数据及文件或云存储的处理。
对于上面所描述的Web应用项目经典三层架构,可以通过图2-4来描述。
图2-4 Web应用项目经典三层架构
对于三层应用架构的优点,读者早已经了解,所以这里不再赘述。当使用三层架构进行开发时,Spring提供了一系列的子项目:Spring MVC、Spring Data、Spring Session、Spring Security等,使得开发者可以快速搭建出一个项目应用框架。下面来看看如何使用这些子项目及Spring Boot来创建本书示例项目:电子商城。
对于三层应用架构开发来说,最难也是最先需要解决的就是业务领域对象(Domain)。只有清晰地识别出这些业务领域对象,以及它们之间如何交互及关联关系之后,才能进行下一步的开发。这些业务领域对象将是开发系统的核心,也是以后最难以变更的部分。一旦业务领域对象定义不合理,往往会造成业务逻辑实现的复杂度升高及系统灵活性降低,甚至可能会造成开发失败。关于业务领域对象设计的更深层次的讨论,有兴趣的读者可以阅读《领域驱动设计:软件核心复杂性应对之道》([美]Eric Evans)这本书。
根据所要开发的系统,我们可以识别出核心业务领域对象有以下三个(当然,这里还是对业务再一次进行了简化)。
·User:用户/客户,也就是谁来访问和使用我们的电子商城。
·Product:商品,我们所要展现给用户并且可以进行交易的物品。
·ProductComment:商品评论,用户购买商品后针对商品发表评论或使用心得。
最终的业务领域实体对象中User部分的代码如下:
package com.cd826dong.clouddemo.user.entity;
import …
// @Entity、@Table和下面的@Id、@GeneratedValued注解后面再进行说明
@Entity
@Table(name = "tbUser")
public class User implements Serializable {
private static final long serialVersionUID = 1L;
@Id
@GeneratedValue
/** 用户数据库主键 */
private Long id;
/** 用户昵称 */
private String nickname;
/** 用户头像 */
private String avatar;
// 使用了Guava中的MoreObjects.ToStringHelper来辅助处理toString
@Override
public String toString() {
return MoreObjects.toStringHelper(this)
.add("id", getId())
.add("nickname", getNickname()).toString();
}
// 省略getters和setters
}
Product部分的代码如下:
package com.cd826dong.clouddemo.product.entity;
import …
@Entity
@Table(name = "tbProduct")
public class Product implements Serializable {
private static final long serialVersionUID = 1L;
@Id
@GeneratedValue
/** 商品数据库主键 */
private Long id;
/** 商品名称 */
private String name;
/** 商品封面图片 */
private String coverImage;
/** 商品价格(分) */
private int price;
@Override
public String toString() {
return MoreObjects.toStringHelper(this)
.add("id", getId())
.add("name", getName()).toString();
}
// 省略getters和setters
}
ProductComment部分的代码如下:
package com.cd826dong.clouddemo.product.entity;
import …
@Entity
@Table(name = "tbProduct_Comment")
public class ProductComment implements Serializable {
private static final long serialVersionUID = 1L;
@Id
@GeneratedValue
/** 商品评论数据库主键 */
private Long id;
/** 所示商品的ID */
private Long productId;
/** 评论作者的ID */
private Long authorId;
/** 评论的具体内容 */
private String content;
/** 评论创建时间 */
private Date created;
@Override
public String toString() {
return MoreObjects.toStringHelper(this)
.add("id", getId())
.add("productId", getProductId())
.add("authorId", getAuthorId())
.add("content", getContent()).toString();
}
// 省略getters和setters
}
在上面的每个业务实体的代码中都使用了ToString Helper来辅助处理toString(),这在进行调试、日志输出时会非常有用。假如类比较简单,可以不用像上面通过手工的方式一个个输出属性,而可以借助commons-lang工具包中的ToStringBuilder通过反射机制来生成,如下面的代码:
// 使用commons-lang工具包中ToStringBuilder通过反射的方式生成
public String toString() {
return ToStringBuilder.reflectionToString(this);
}
如果不想在toString()中把全部属性都输出(试想一下,如果一个类有几十个属性),那么还是采用上面的方式更好。假如我们的类具有继承体系的话,而且在子类的toString()方法中只想添加一些额外的输出,那么此时Guava也可以帮助我们进行简化,比如:
// 这是基类中的代码,我们额外增加了一个toStringHelper的方法
@Override
public String toString() {
return this.toStringHelper().toString();
}
protected MoreObjects.ToStringHelper toStringHelper() {
return MoreObjects.toStringHelper(this)
.add("id", getId())
.add("nickname", getNickname());
}
// 子类中的代码,这里我们只需要复写toStringHelper方法
@Override
protected MoreObjects.ToStringHelper toStringHelper() {
// 这里只需添加所要输出的字段内容即可
return super.toStringHelper()
.add("age", getAge());
}
一个应用系统都有数据存储的需求,最常见的方式就是将业务数据存储到关系型数据库中,如MySQL、Oracle等。对于Java开发来说,这一技术的需求从一开始就存在,经过这么多年,业界也已经有了多个非常成熟的解决方案。而ORM(Object Relational Mapping)框架则是我们针对该需求首选的一个解决框架。在ORM框架中,Hibernate是ORM框架中使用最广泛的一个实现。Hibernate可以辅助我们将业务对象通过映射的方式存储到数据库中,而不需要使用最原始的SQL语言。
接下来,我们将会在示例项目中使用Java持久性规范JPA(Java Persistence API)来完成数据存储处理。JPA是Sun官方提出的一个数据存储标准接口定义,并且有许多实现方式,而上面所提到的Hibernate则是其中的一种实现方式。通过JPA可以将我们的业务与具体所要存储的数据库解耦,不需要为不同的数据库编写不同的处理方法,从而方便在多种数据库之间进行切换。
首先需要在项目中增加spring-boot-starter-data-jpa的依赖,因此在pom.xml中添加如下代码:
<!-- 通过引入该依赖,也默认使用了Hibernate作为缺省的实现 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>
其次还需选择一个数据库作为业务数据的存储。可选的数据库非常多,如MySQL、PostrgreSQL、Oracle等。但这里为了方便书中示例项目的讲解及读者测试,我们将采用一个非常轻量级并且可以内嵌的数据库h2。因此,在pom.xml中我们还需要增加对H2数据库的依赖,代码如下:
<!-- 添加对H2数据库依赖 -->
<dependency>
<groupId>com.h2database</groupId>
<artifactId>h2</artifactId>
</dependency>
接下来就可以在项目配置文件(application.properties)中配置数据源属性。具体配置代码如下:
# 这里是JPA配置,针对Hibernate
spring.jpa.open-in-view=true
spring.jpa.hibernate.ddl-auto=none
spring.jpa.show-sql=false
spring.jpa.properties.hibernate.physical_naming_strategy=
com.cd826dong.clouddemo.util.HibernatePhysicalNamingNamingStrategy
# 这里是数据源配置,针对H2
spring.jpa.database=H2
spring.datasource.driverClassName=org.h2.Driver
spring.datasource.url=jdbc\:h2\:mem\:testdb;DB_CLOSE_DELAY=-1;
spring.datasource.username=sa
spring.datasource.password=
# 当需要启动H2控制台时需要开启下面的配置
# spring.datasource.url=jdbc:h2:~/testdb
# spring.h2.console.enabled=true
#spring.h2.console.path=/h2-console
配置分为两部分,一部分是JPA的配置,针对Hibernate;另一部分是数据源配置,针对H2数据库。
通过该依赖,我们在项目中就可以使用Spring Data JPA功能了。在进行具体数据访问功能编写之前,还需要完成业务领域对象与数据库之间映射关系的处理。最简单的实现方法就是通过JPA所提供的注解,常用注解有以下几个。
·@Entity:该注解的意思是指所注解的类会被认为是JPA的一个实体对象,JPA就会对该对象进行映射处理。对于该注解有一个需要注意的地方就是在该类中必须有一个空的构造函数。当从数据库中加载对象时,JPA就会通过该构造函数来实例化对象,并将数据库中的值赋值给所构建的示例,如果不小心在业务实体类中覆写了构造函数而忘记提供空的构造函数,就会造成数据加载错误。
·@Id:用来注解数据库主键,并且通过@GeneratedValue告诉JPA自动生成主键的值,默认情况下会使用数据库自增方式。也可以通过下面的注解来指定主键的生成方式。
// 这里将主键ID生成的方式指定为使用USER_SEQUENCE序列 @Id @GeneratedValue(strategy = GenerationType.SEQUENCE,generator ="userSeq") @SequenceGenerator(initialValue =1,name ="userSeq",sequenceName ="USER_ SEQUENCE") private long id;
·实体关联关系:@OneToOne、@OneToMany、@ManyToOne和@ManyToMany这些注解可以用来处理业务实体之间一对一、一对多、多对一和多对多关系的声明,具体怎么使用这些注解,读者可以阅读相关书籍深入了解。
·@Table:该注解使用在业务实体类上,可以让我们自定义实体类会映射到数据库具体哪张表上。
·@Column:通常我们在开发时很少使用该注解,JPA会自动将实体中的字段按照默认规则与数据库表进行一一映射,只有当默认的规则满足不了时才需要使用该注解自定义字段映射关系。
这里重点讲一下Hibernate数据库表映射规则及配置方式。一般,实体到物理数据库表的映射采用驼峰命名法,比如,User实体类默认映射到的数据库表表名为user,User实体类中的字段loginName,则默认会映射到数据表中的login_name字段上。
或许这种默认命名规则并不符合开发者所在项目中的命名规范,此时可以通过@Table注解来更改实体所要映射到的数据库表,通过@Column来更改字段映射规则。但是在更改这些数据库表映射规则时,还需要在项目配置中增加properties.hibernate.physical_naming_strategy属性配置,通过该属性指定自定义规则的处理方式。比如,在示例项目中我们使用com.cd826dong.clouddemo.util.HibernatePhysicalNamingNamingStrategy自定义类,该类的具体代码如下:
package com.cd826dong.clouddemo.util;
import …
@MappedSuperclass
public class HibernatePhysicalNamingNamingStrategy extends
PhysicalNamingStrategyStandardImpl {
@Override
public Identifier toPhysicalTableName(Identifier name,
JdbcEnvironment context) {
return new Identifier(name.getText(), name.isQuoted());
}
@Override
public Identifier toPhysicalColumnName(Identifier identifier,
JdbcEnvironment jdbcEnv) {
return convert(identifier);
}
// 更改Hibernate表名映射规则,保持Entity类中对数据库表命名的不变
private Identifier convert(Identifier identifier) {
if (identifier == null || !StringUtils.hasText(identifier.getText())) {
return identifier;
}
String regex = "([a-z])([A-Z])";
String replacement = "$1_$2";
String newName = identifier.getText().replaceAll(regex,
replacement).toLowerCase();
return Identifier.toIdentifier(newName);
}
}
注意:
上面的配置是针对Hibernate 5.x版本的,如果读者是Hibernate 4.x版本的,其配置方式则有所不同,具体可以参考Hibernate配置文档。
最后就可以借助JPA来实现数据存储层代码了。通常,数据存储层会统一存放到一个命名为repository的包中,在该包中会完成用户、商品和商品评论存储功能代码。由于这些代码非常相似,这里只列出用户repository的代码,其他代码可以在本书源码中获取到。用户repository代码如下:
package com.cd826dong.clouddemo.user.repository;
import …
// 你没有看错,这就是用户Repository的全部代码
public interface UserRepository extends JpaRepository<User, Long> {
}
会不会很意外?当使用JPA时,对于简单的增、删、改、查等功能几乎不需要编写任何代码,只需继承JpaRepository接口即可。当查看该接口的源码时会发现JpaRepository继承自PagingAndSortingRepository,而PagingAndSortingRepository又继承了CrudRepository,从这些接口的命名上,我们可以推断所定义的UserRepository已经具备了分页数据查询和增、删、改、查等功能,事实也是如此。
对于该接口,我们也不需要在项目中进行实现,而是在项目启动时Spring Data会通过动态代码生成机制为我们创建该接口的具体实现类,并注册到Spring的应用上下文中,供我们在其他地方使用。此外,为方便查询处理,Spring Data还提供了自然语义的数据访问处理机制。比如,我们可以在接口中定义一个名称为findTop10OrderByJoinDateDesc的方法,这时候Spring Data会自动实现查询最新加入的10个用户,并按照加入日期倒序排列。
Spring Data所提供的这种语义查询方法在一些简单查询时会非常有用,但对于稍微复杂一些的查询,可能需要定义的方法比较长。比如findTop10ByUserSexAndCityAndAge OrderByJoinDateDesc,用来查询某个城市中指定年龄段最新加入的前10名男性或女性用户。这种命名方法显然使用时非常不友好,为此,Spring Data还提供了@Query注解,可以直接在方法中声明该方法查询时所使用的查询语句,所声明的查询语句使用JPQL语言。
此外,如果我们所要处理的数据访问处理比这些还要复杂,很难通过声明一个JPQL语句实现,此时我们可以使用Spring Data的扩展机制,为Repository声明指定一个扩展接口并提供相应的实现类,代码如下:
// 定义一个用户管理扩展的repository
public interface UserRepositoryEx {
List<User> findTopUser(int maxResult);
}
// 这个是用户管理扩展repository的实现,不需要增加@Component等注解
// 另外,这里是不需要注解Spring的@Componment等注解
public class UserRepositoryImpl implements UserRepositoryEx {
// 这里需要织入entityManager
@PersistenceContext
protected EntityManager entityManager;
// 通过Query对象实现具体查询
public List<User> findTopUser(int maxResult) {
Query query = this.entityManager.createQuery("from User");
query.setMaxResults(maxResult);
return query.getResultList();
}
}
// 最后别忘记让UserRepository继承我们的自定义扩展,但不需要实现
public interface UserRepository extends JpaRepository<User, Long>,
UserRepositoryEx {
}
扩展数据访问接口类的名称通常为主数据访问接口类增加一个Ex后缀,如上述示例中主数据访问接口类的名称为UserRepository,扩展数据访问接口类的名称为UserRepositoryEx。而相应实现类的名称则是为主数据访问接口类增加一个Impl后缀,上例中为UserRepositoryImpl。需要注意的是,使用Ex只是一个默认约定,可以在项目中随便定义这个后缀,而实现类的Impl后缀是不可以随意定义的,但是可以在配置中通过repository-impl-postfix属性设置。
此外,当所继承的Repository中有些接口方法不需要时,那么可以在接口中重新声明需要的方法即可。这样Spring Data在动态生成时就只会生成所重新声明的方法,而基类中所定义的方法则不会暴露。
也许有些读者对这里有些迷惑,按道理来说数据存储是我们需要构建一个数据模型来适应数据存储的需要,但这里并没有看到。是的,对于一些复杂的业务来说是需要这么做的,但这里我们将数据模型和业务领域对象合二为一了,因为相对来说我们的数据比较简单。因此才会在上面的业务实体对象上增加了@Entity、@Table等注解。
对于H2数据库有两种使用方式:第一种是使用内存存储数据;第二种是使用文件存储数据。在本书的示例中为了方便进行测试和开发,默认使用了内存方式。
当我们使用H2数据库时,可以通过配置使得应用启动时帮我们创建一个H2控制台。H2控制台是一个非常轻量级的Web应用,通过该控制台可以访问和操作H2数据库。启动成功后,控制台具体访问地址为 http://localhost:8080/h2-console/ ,如图2-5所示,只需要将上面所配置的数据库地址填写到JDBC URL中,就可以访问示例项目中的数据库,并可以进行相关操作。
图2-5 内嵌H2数据库控制台
比如,在H2的控制台界面中可以运行SQL语句,或者选择一张表并显示该表中的所有数据,运行结果如图2-6所示。
图2-6 内嵌H2数据库控制台访问数据
有些读者可能注意到在上面两个依赖中并没有定义依赖库的版本,这是因为在pom.xml文件所使用的parent中已经定义了,如果往上查看pom.xml的定义,就可以在spring-boot-dependencies项目中的pom.xml文件中查到下面的定义:
<properties>
<!-- 其他依赖版本定义 -->
<h2.version>1.4.193</h2.version>
</properties>
<dependencyManagement>
<dependencies>
……
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jpa</artifactId>
<version>1.5.2.RELEASE</version>
</dependency>
……
</dependencies>
</dependencyManagement>
可见,在spring-boot-dependencies项目中已经为开发者定义好所依赖的版本了,建议大家一般不需要再更改了。如果想更改依赖的版本,可以在自己工程中的pom.xml文件中覆写相应的属性即可。例如,通过下面的代码将所要使用的H2版本更改为1.4.196:
<properties> <!-- 在项目的pom.xml文件中重新定义H2版本 --> <h2.version>1.4.196</h2.version> </properties>
按照开发方式来说,设计和开发的顺序应该是:Domian→REST API→业务逻辑层→数据存储层,通过测试驱动的方式设计所需要的功能,但这里为了编写和讲解方便,对顺序做了一些调整,在真正开发时建议读者还是遵照测试驱动开发的理念进行开发。
对于用户管理来说所要提供的功能有:分页数据查询、获取某个用户详情、保存/更新用户信息及删除指定用户。因此,用户管理服务接口定义如下:
package com.cd826dong.clouddemo.user.service;
import …
public interface UserService {
/**
* 获取用户分页数据
* @param pageable分页参数
* @return 分页数据
*/
Page<User> getPage(Pageable pageable);
/**
* 加载指定的用户信息
* @param id 用户主键
* @return
*/
User load(Long id);
/**
* 保存/更新用户
* @param userDto
* @return
*/
User save(UserDto userDto);
/**
* 删除指定用户
* @param id 所要删除的用户主键
*/
void delete(Long id);
}
在这里,我们在保存/更新功能中引入了一个新的类UserDto,也就是数据传输对象(Data Transfer Object,简称DTO),用来处理跨进程或网络传输数据聚合容器。一般在该对象中只包含数据属性,而不包含任何业务逻辑。在这里及后续的示例中都会使用DTO对象作为前、后端分离时的数据传输。
针对用户服务的实现也非常简单,具体代码如下:
package com.cd826dong.clouddemo.user.service.impl;
import …
@Service
public class UserServiceImpl implements UserService {
// 注入UserRepository
@Autowired
protected UserRepository userRepository;
@Override
public Page<User> getPage(Pageable pageable) {
// 这里直接使用JPA所提供的分页查询功能即可
return this.userRepository.findAll(pageable);
}
@Override
public User load(Long id) {
return this.userRepository.findOne(id);
}
@Override
@Transactional
public User save(UserDto userDto) {
// 对于保存,首先获取数据库中存在的对象,然后将所要保存的值复制进去
User user = this.userRepository.findOne(userDto.getId());
if (null == user) {
user = new User();
}
user.setNickname(userDto.getNickname());
user.setAvatar(userDto.getAvatar());
return this.userRepository.save(user);
}
@Override
@Transactional
public void delete(Long id) {
// 依然使用JPA提供的删除功能即可
this.userRepository.delete(id);
}
}
在方法的实现上,基本都是通过调用UserRepository相应的方法来完成,这里就不再进行解释了。另外,由于商品服务业务逻辑代码也比较简单,这里就不再列出,读者可以从本书源码中获取。
最后一步就是编写对外交互的接口。本书中的示例将全部采用RESTful API的方式来实现,一方面REST是业界非常成熟的Web服务标准之一;另一方面通过REST可以支持多种应用客户端,如Web、H5、App等都可以进行访问与交互。
对于传统的Web应用开发,如果没有采用前、后端开发分离时,通常可以直接使用HTML或者模版引擎(Thymeleaf等)技术,提供UI界面让用户可以操作。但这种开发方式意味着难以扩展到其他形式的客户端上。当转换采用基于REST架构开发模式时,就意味着接下来我们不必编写一套UI层代码就可以进行功能验证。例如,可以使用curl或Postman等工具直接访问所提供的RESTful API接口,验证功能的正确性。
因此,在接下来的示例及本书后续的所有示例项目中都不会涉及任何UI层的代码。对功能的验证统一使用Postman工具,读者可以到 https://www.getpostman.com/ 上下载并了解该工具的使用方法。
此外,我们所要编写的RESTful API并没有遵守最严格的方式,而是使用Spring MVC所提供的RestController来完成。
下面开始编写用户服务所提供的接口,代码如下:
package com.cd826dong.clouddemo.user.api;
import …
@RestController
@RequestMapping("/users")
@Api(value = "UserEndpoint", description = "用户管理相关Api")
public class UserEndpoint {
@Autowired
private UserService userService;
@RequestMapping(method = RequestMethod.GET)
@ApiOperation(
value = "获取用户分页数据",
notes = "获取用户分页数据",
httpMethod = "GET",
tags = "用户管理相关Api")
@ApiImplicitParams({
@ApiImplicitParam(
name = "page",
value = "第几页,从0开始,默认为第0页",
dataType = "int",
paramType = "query"),
@ApiImplicitParam(
name = "size",
value = "每一页记录数的大小,默认为20",
dataType = "int",
paramType = "query"),
@ApiImplicitParam(
name = "sort",
value = "排序,格式为:property,property(,ASC|DESC)的方式组织",
dataType = "String",
paramType = "query")
})
public List<UserDto> findAll(Pageable pageable){
Page<User> page = this.userService.getPage(pageable);
if (null != page) {
// 转换成DTO对象
return page.getContent().stream().map((user) -> {
return new UserDto(user);
}).collect(Collectors.toList());
}
return Collections.EMPTY_LIST;
}
@RequestMapping(value = "/{id}", method = RequestMethod.GET)
@ApiOperation(
value = "获取用户详情数据",
notes = "获取用户详情数据",
httpMethod = "GET",
tags = "用户管理相关Api")
@ApiImplicitParams({
@ApiImplicitParam(
name = "id",
value = "用户的主键",
dataType = "int",
paramType = "path")
})
public UserDto detail(@PathVariable Long id){
User user = this.userService.load(id);
return (null != user) ? new UserDto(user) : null;
}
@RequestMapping(value = "/{id}", method = RequestMethod.POST)
@ApiOperation(
value = "更新用户详情数据",
notes = "更新用户详情数据",
httpMethod = "POST",
tags = "用户管理相关Api")
@ApiImplicitParams({
@ApiImplicitParam(
name = "id",
value = "用户的主键",
dataType = "int",
paramType = "path"),
@ApiImplicitParam(
name = "userDto",
value = "用户详情数据",
dataType = "UserDto",
paramType = "body"),
})
public UserDto update(@PathVariable Long id,
@RequestBody UserDto userDto){
userDto.setId(id);
User user = this.userService.save(userDto);
return (null != user) ? new UserDto(user) : null;
}
@RequestMapping(value = "/{id}", method = RequestMethod.DELETE)
@ApiOperation(
value = "删除指定用户",
notes = "删除指定用户",
httpMethod = "DELETE",
tags = "用户管理相关Api")
@ApiImplicitParams({
@ApiImplicitParam(
name = "id",
value = "所要删除用户的主键",
dataType = "int",
paramType = "path")
})
public boolean delete(@PathVariable Long id){
this.userService.delete(id);
return true;
}
}
熟悉Spring MVC的读者都知道,这个类其实是一个Controller类。Spring MVC针对Controller提供了两个注解@Controller和@RestController。当我们在一个Controller类中添加了@RestController注解时,该类中所有使用了@RequestMapping的方法就会返回响应体(response body),如果使用的是@Controller注解,则是会将HTML部分的代码也一起返回给调用者,也可以在使用@Controller注解的方法上同时增加@ResponseBody来达到同样的目的。
@RequestMapping注解可以使用在方法或类上,如果是注解在类上,那么该类中所有注解的方法都会继承类中所声明的属性。@RequestMapping还有两个简化的注解@GetMapping和@PostMapping,分别用来处理Get和Post请求。
此外,在编写RESTful API时还需要注意一点,当方法的返回值是一个对象时,Spring MVC就会默认使用JSON序列化方法将对象序列化为一个JSON格式的字符串,并返回给请求者。而假如方法中返回的是基础数据类型(如boolean、int等)则无法进行序列化,从而造成错误。所以在使用@RestController时需要注意返回值,最好的方式就是对返回值进行统一包装,这样方便前端进行处理。
可能有些读者对代码中出现的@Api、@ApiOperation、@ApiImplicitParams有些疑问,因为它们并不属于Spring MVC,而是属于Swagger。Swagger的目标是为REST API定义一个标准的、与语言无关的接口,使开发者在无源码或者没有API文档的情况下可以发现和理解系统所提供的各种服务。用一句话简单来概述就是:Swagger所开发的API可根据上述注解自动生成API文档。如果要让User Endpoint代码能够通过编译,还需要在pom.xml文件中添加下面的Swagger依赖:
<properties>
<!-- 定义Swagger的版本 -->
<swagger.version>2.6.1</swagger.version>
</properties>
<!-- Swagger依赖 -->
<dependency>
<groupId>io.springfox</groupId>
<artifactId>springfox-swagger-ui</artifactId>
<version>${swagger.version}</version>
</dependency>
<dependency>
<groupId>io.springfox</groupId>
<artifactId>springfox-swagger2</artifactId>
<version>${swagger.version}</version>
</dependency>
之后还需要在项目webapp目录下创建一个swagger目录,并将从 https://github.com/swagger-api/swagger-ui 下载的UI代码解压到该目录下。这样当系统启动后,就可以在浏览器中通过地址 http://localhost:8080/swagger/index.html 访问到所生成的API文档,界面如图2-7所示。Swagger的详细使用说明已经超出了本书范围,有兴趣的读者可以去官网( https://swagger.io/ )上深入了解。
图2-7 Swagger访问界面
同样,对于商品服务的API这里也不列出了,读者可以从本书源码中获取。
还差一项工作,我们就可以运行电子商城项目了,这项工作就是数据库初始化。数据库的初始化可以使用多种方式,在真实生产环境中一般会采用手动方式完成,因此需要编写数据库表创建脚本、数据初始化脚本。在开发过程中可以使用Hibernate根据实体类自动创建数据库。在本书示例中将使用Spring Boot提供的Spring JDBC的方式初始化数据库。
Spring Boot框架所提供的Spring JDBC初始化DataSource特性,是在启动系统时检测classpath根目录下是否有schema.sql和data.sql脚本文件,如果存在这两个脚本文件存在或者其中一个,将会尝试加载并执行该脚本。首先使用schema.sql创建数据库表,然后使用data.sql初始化数据。如果脚本初始化产生异常,那么应用系统启动将会失败。
此外,还可以在项目配置文件中通过设置spring.datasource.schema属性的值,定义数据库创建脚本的位置,通过spring.datasource.data属性设置数据初始化脚本的位置。如果将spring.datasource.initialize属性设置为false,那么Spring Boot在启动时将不执行数据库初始化处理。此外,当我们使用这种机制时,需要把hibernate.ddl-aut设置为none,这样就可以避免启动时Hibernate试图根据实体类创建数据库表而造成的错误。示例项目中数据库初始化脚本代码如下:
drop table if exists tbProduct; drop table if exists tbProduct_Comment; drop table if exists tbUser; -- 创建商品表 create table tbProduct ( id int unsigned not null auto_increment comment '主键', name varchar(100) comment '商品名称', cover_image varchar(100) comment '商品封面图片', price int not null default 0 comment '商品价格(分)', primary key (id) ); -- 创建商品评论表 create table tbProduct_Comment ( id int unsigned not null auto_increment comment '主键', product_id int unsigned comment '所属商品', author_id int unsigned comment '作者Id', content varchar(200) comment '评论内容', created TIMESTAMP comment '创建时间', primary key (id) ); -- 创建用户表 create table tbUser ( id int unsigned not null auto_increment comment '主键', nickname varchar(50) comment '用户昵称', avatar varchar(255) comment '用户头像', primary key (id) );
数据初始化脚本代码如下:
-- 导入测试商品列表
insert into tbProduct (id, name, cover_image, price)
values(1, '测试商品-001', '/products/cover/001.png', 100);
insert into tbProduct (id, name, cover_image, price)
values(2, '测试商品-002', '/products/cover/002.png', 200);
insert into tbProduct (id, name, cover_image, price)
values(3, '测试商品-003', '/products/cover/003.png', 300);
insert into tbProduct (id, name, cover_image, price)
values(4, '测试商品-004', '/products/cover/004.png', 400);
insert into tbProduct (id, name, cover_image, price)
values(5, '测试商品-005', '/products/cover/005.png', 500);
-- 导入测试用户列表
insert into tbUser (id, nickname, avatar)
values(1, 'zhangSan', '/users/avatar/zhangsan.png');
insert into tbUser (id, nickname, avatar)
values(2, 'lisi', '/users/avatar/lisi.png');
insert into tbUser (id, nickname, avatar)
values(3, 'wangwu', '/users/avatar/wangwu.png');
insert into tbUser (id, nickname, avatar)
values(4, 'yanxiaoliu', '/users/avatar/yanxiaoliu.png');
-- 导入商品3的评论列表
insert into tbProduct_Comment (id, product_id, author_id, content, created)
values(1, 3, 1, '非常不错的商品', CURRENT_TIMESTAMP());
insert into tbProduct_Comment (id, product_id, author_id, content, created)
values(2, 3, 3, '非常不错的商品+1', CURRENT_TIMESTAMP());
insert into tbProduct_Comment (id, product_id, author_id, content, created)
values(3, 3, 4, '哈哈,谁用谁知道', CURRENT_TIMESTAMP());
现在我们第一版电子商城项目已经开发完成了,下面可以编译、打包、运行,然后在Postman中访问我们所提供的API端点。比如,我们访问用户列表端点 http://localhost:8080/users ,可以看到如图2-8所示界面。如果能从界面中看到所导入的用户列表已经正确显示出来,就表示我们使用Spring Boot开发的第一个项目已经运行成功。
图2-8 使用Postman访问用户列表