02. 微服务架构开发实战之单体架构应用改造升级微服务架构

微服务架构开发实战之单体架构应用改造升级微服务架构

0x01 设计改造单体架构应用程序

单体架构应用升级微服务最好的一种方式就是绞杀藤方式。

因此,如果你想更好的学习 微服务开发,我建议先从改造一个单体架构的应用开始。

因为这样我们可以从中学习微服务拆分,微服务技术迁移过程中出现的问题,明白微服务架构的优缺点,更好理解微服务架构设计模式的思想,以便于我们更好地掌握微服务。

上一篇讲解了01. 微服务架构开发实战之概览

这篇博文我们将会使用Spring Boot 构建一个简单版本的电商单体架构应用。

视频讲解:

02 微服务架构开发实战之单体架构应用改造升级微服务架构

更多精彩内容: 微服务架构开发实战专栏简介

1.1 需求分析

为了减少项目的复杂性和学习的方便性,我这里只设计如下三张表:

  • Customer: 用户/客户 也就是谁来访问和使用我们的电子商城。
  • Product:商品,我们所要展现给用户并且可以交易的商品
  • ProductComment: 商品评论,用户购买商品后针对商品发表评论或使用心得。

1.2 关于主键生成策略

由于后期一个单体架构应用程序需要改造成微服务,那么主键生成策略最好的方式使用雪花算法来实现分布式全局唯一id.

1.3 关于技术的选型

使用框架组件 使用框架组件说明
Spring Boot 2.x 简化繁琐配置,自动配置功能
Spring Web 提供Spring MVC 支持
Spring Web 提供Spring MVC 支持
Thymeleaf 提供页面模版引擎支持
Spring Security 提供API 保护功能
Swagger 生成友好API
Lombok 省略一些POJO类Getter and Setter
H2 测试使用临时的数据库
MySQL 8 高性能的关系型数据库
Actuator 健康检查,检测应用是否运行正常
Spring Data JPA 提供表和列映射基础增删改查方法

1.4 关于单体架构的项目目录结构

我这里会假设我们这项目今后将会采用微服务,因此我们需要将这三个表相关数据尽可能独立,因此我拆分成这样的目录结构。

除了公共部分代码,其他按照业务根据不同的业务表使用单独的文件夹,尽可能减少相互之前的耦合。

大致就是如下图所示这个样子:
在这里插入图片描述

这种目录结构的划分有助于我们今后将单体架构的简单商城改造成微服务架构。

0x02 设计我们的简易版电子商城

为了降低学习成本,这里不再引入支付相关的模块.

2.1 创建单体应用项目骨架

这里博主推荐使用Intellij Idea 来构建我们的项目基本骨架。

  • 项目基本信息配置如下

项目名称最好见名知意,因此这里项目取名为single-architecture-spring-boot-shopping-sample
在这里插入图片描述

2.2 选择项目依赖

然后我们需要为自己的项目选择需要用到的组件。

比如我为当前这个演示项目选择依赖和解释如下:

选择依赖名称 选择原因
Lombok 通过注解简化Getter,Setter,ToString(),Equals以及日志API 的使用
Spring Configuration Processor 让项目支持自定义配置文件属性
Spring Web 让项目支持Spring Web MVC
Thymeleaf 页面模板引擎支持HTML5 等多种前端UI框架
Spring Security 对API 进行保护
Spring Data JPA 数据持久层API
H2 DataBase H2 嵌入式数据库,开发初期测试使用
MySQL Driver MySQL 驱动
Spring Boot Actuator 健康监控

其中用到了Lombok 依赖,如果使用还需要安装lombok 插件。
关于lombok 的相关介绍见我的另外一篇博文:
Spring Boot 2.x 最佳实践之lombok集成

下面是完整的pom.xml

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>

    <!-- config parent project information-->
    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>2.2.5.RELEASE</version>
        <relativePath/> <!-- lookup parent from repository -->
    </parent>

    <!-- config current project information-->
    <groupId>com.xingyun</groupId>
    <artifactId>single-architecture-spring-boot-shopping-sample</artifactId>
    <version>0.0.1-SNAPSHOT</version>
    <name>single-architecture-spring-boot-shopping-sample</name>
    <description>single architecture spring boot shopping sample project for Spring Boot</description>

    <!-- Rely on version control -->
    <properties>
        <java.version>1.8</java.version>
        <swagger.version>2.9.2</swagger.version>
        <skipTests>true</skipTests>
    </properties>

    <dependencies>
        <!--base useful lib-->
        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
            <optional>true</optional>
        </dependency>

        <!-- custom  application-dev.properties properties-->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-configuration-processor</artifactId>
            <optional>true</optional>
        </dependency>

        <!-- Starter for building web, including Restful,applications using Spring MVC.
     Uses Tomcat as the default embedded container-->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>

        <!--Starter for building MVC web applications using Thymeleaf views-->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-thymeleaf</artifactId>
        </dependency>

        <!-- JSON API documentation for spring based applications -->
        <!-- https://mvnrepository.com/artifact/io.springfox/springfox-swagger-ui -->
        <dependency>
            <groupId>io.springfox</groupId>
            <artifactId>springfox-swagger-ui</artifactId>
            <version>${swagger.version}</version>
        </dependency>
        <!-- Automated JSON API documentation for API's built with Spring -->
        <!-- Home Page:https://github.com/springfox/springfox -->
        <!-- https://mvnrepository.com/artifact/io.springfox/springfox-swagger2 -->
        <dependency>
            <groupId>io.springfox</groupId>
            <artifactId>springfox-swagger2</artifactId>
            <version>${swagger.version}</version>
        </dependency>

        <!-- Starter for using Spring Security-->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-security</artifactId>
        </dependency>

        <!-- added H2 support,just for test -->
        <dependency>
            <groupId>com.h2database</groupId>
            <artifactId>h2</artifactId>
            <scope>runtime</scope>
        </dependency>

        <!-- added MySQL support -->
         <dependency>
             <groupId>mysql</groupId>
             <artifactId>mysql-connector-java</artifactId>
             <scope>runtime</scope>
         </dependency>

        <!-- Starter for using Spring Data JPA with Hibernate -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-data-jpa</artifactId>
        </dependency>
        <!--Starter for using Spring Boot’s Actuator
         which provides production ready features to help you monitor and manage your application-->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-actuator</artifactId>
        </dependency>
        <!-- added security test support -->
        <dependency>
            <groupId>org.springframework.security</groupId>
            <artifactId>spring-security-test</artifactId>
            <scope>test</scope>
        </dependency>
        <!--Starter for testing Spring Boot applications with libraries including JUnit, Hamcrest and Mockito-->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
            <exclusions>
                <exclusion>
                    <groupId>org.junit.vintage</groupId>
                    <artifactId>junit-vintage-engine</artifactId>
                </exclusion>
            </exclusions>
        </dependency>
    </dependencies>

    <build>
        <plugins>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
            </plugin>
        </plugins>
    </build>
</project>

2.3 配置application.properties

依赖添加完成后我们需要对项目中用到的一些依赖做一些基础性的配置。

配置application.properties内容如下:

# config active which one profile
spring.profiles.active=dev

值得注意的是,

  • 这里默认指定启用application-dev.properties的配置信息
  • application-dev.properties使用H2 嵌入式数据库和JPA 初始化表结构
  • application-test.properties使用 MySQL8.x 数据库和JPA初始化表结构
  • application-uat.properties 使用Mysql 8.x 数据库和两个SQL 初始化表机构和添加测试数据

2.4 配置application-dev.properties

配置application-dev.properties 内容如下:

# app instance name
spring.application.name=single-architecture-spring-boot-shopping-sample
# config tomcat
server.port=8080
server.servlet.context-path=/

# config log with logback
logging.level.root=INFO
logging.level.sql=DEBUG
logging.level.web=INFO
logging.level.org.springframework=INFO
logging.level.com.xingyun.singlearchitecturespringbootshoppingsample=DEBUG

# config spring data jpa
# enable or disable show data view in web console
spring.jpa.open-in-view=true
# enable print sql
spring.jpa.show-sql=true
# create:Create the schema and destroy previous data
# create-drop:Create and then destroy the schema at the end of the session.
# update:Update the schema if necessary.
# none:Disable DDL handling
spring.jpa.hibernate.ddl-auto=create
# 可选参数如下:
# org.springframework.boot.orm.jpa.hibernate.SpringImplicitNamingStrategy:nickName mapping to NIKE_NAME
# org.hibernate.boot.model.naming.ImplicitNamingStrategyJpaCompliantImpl
# org.hibernate.boot.model.naming.ImplicitNamingStrategyComponentPathImpl extends ImplicitNamingStrategyJpaCompliantImpl
# org.hibernate.boot.model.naming.ImplicitNamingStrategyLegacyHbmImpl extends ImplicitNamingStrategyJpaCompliantImpl
# org.hibernate.boot.model.naming.ImplicitNamingStrategyLegacyJpaImpl extends ImplicitNamingStrategyJpaCompliantImpl
# 隐式命名策略,当没有使用@Table和@Column注解时,implicit-strategy配置项才会被使用
spring.jpa.hibernate.naming.implicit-strategy=org.springframework.boot.orm.jpa.hibernate.SpringImplicitNamingStrategy
# 物理命名策略,用于转换“逻辑名称”(隐式或显式)的表或列成一个物理名称。
# org.hibernate.boot.model.naming.PhysicalNamingStrategyStandardImpl:nikeName mapping to nikeName,save the origin name
# org.springframework.boot.orm.jpa.hibernate.SpringPhysicalNamingStrategy:nikeName mapping to nike_name,split with ,
spring.jpa.hibernate.naming.physical-strategy=org.springframework.boot.orm.jpa.hibernate.SpringPhysicalNamingStrategy
spring.jpa.database=h2

# config h2 data source
spring.datasource.driver-class-name=org.h2.Driver
spring.datasource.url=jdbc:h2:mem:in_memory_shopping_db;DB_CLOSE_DELAY=-1
#spring.datasource.url=jdbc:h2:~/embedded_file_test_db
spring.datasource.username=sa
spring.datasource.password=sa
spring.datasource.initialization-mode=never

# config h2
spring.h2.console.enabled=true
spring.h2.console.path=/h2-console

# config actuator
# show health monitor detail
management.endpoint.health.show-details=when_authorized

2.5 配置application-test.properties

配置application-test.properties 内容如下:

# app instance name
spring.application.name=single-architecture-spring-boot-shopping-sample
# config tomcat
server.port=8080
server.servlet.context-path=/

# config log with logback
logging.level.root=INFO
logging.level.sql=DEBUG
logging.level.web=INFO
logging.level.org.springframework=INFO
logging.level.com.xingyun.singlearchitecturespringbootshoppingsample=DEBUG

# config spring data jpa
# enable or disable show data view in web console
spring.jpa.open-in-view=true
# enable print sql
spring.jpa.show-sql=true
# create:Create the schema and destroy previous data
# create-drop:Create and then destroy the schema at the end of the session.
# update:Update the schema if necessary.
# none:Disable DDL handling
spring.jpa.hibernate.ddl-auto=update
# 可选参数如下:
# org.springframework.boot.orm.jpa.hibernate.SpringImplicitNamingStrategy:nickName mapping to nike_name
# org.hibernate.boot.model.naming.ImplicitNamingStrategyJpaCompliantImpl
# org.hibernate.boot.model.naming.ImplicitNamingStrategyComponentPathImpl extends ImplicitNamingStrategyJpaCompliantImpl
# org.hibernate.boot.model.naming.ImplicitNamingStrategyLegacyHbmImpl extends ImplicitNamingStrategyJpaCompliantImpl
# org.hibernate.boot.model.naming.ImplicitNamingStrategyLegacyJpaImpl extends ImplicitNamingStrategyJpaCompliantImpl
# 隐式命名策略,当没有使用@Table和@Column注解时,implicit-strategy配置项才会被使用
spring.jpa.hibernate.naming.implicit-strategy=org.springframework.boot.orm.jpa.hibernate.SpringImplicitNamingStrategy
# 物理命名策略,用于转换“逻辑名称”(隐式或显式)的表或列成一个物理名称。
# org.hibernate.boot.model.naming.PhysicalNamingStrategyStandardImpl:nikeName mapping to nikeName
# org.springframework.boot.orm.jpa.hibernate.SpringPhysicalNamingStrategy:nikeName mapping to nike_name
spring.jpa.hibernate.naming.physical-strategy=org.springframework.boot.orm.jpa.hibernate.SpringPhysicalNamingStrategy
spring.jpa.database=mysql

# config mySQL data source
spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver
spring.datasource.url=jdbc:mysql://localhost:3306/spring_cloud_mysql8_test_db?serverTimezone=UTC&useUnicode=true&characterEncoding=UTF-8&useSSL=false&allowPublicKeyRetrieval=true
spring.datasource.username=root
spring.datasource.password=toor
spring.datasource.initialization-mode=never

# config actuator
# show health monitor detail
management.endpoint.health.show-details=when_authorized

2.6 application-uat.properties

application-uat.properties

# app instance name
spring.application.name=single-architecture-spring-boot-shopping-sample
# config tomcat
server.port=80
server.servlet.context-path=/

# config log with logback
logging.level.root=INFO
logging.level.sql=DEBUG
logging.level.web=INFO
logging.level.org.springframework=INFO
logging.level.com.xingyun.singlearchitecturespringbootshoppingsample=DEBUG

# config spring data jpa
# enable or disable show data view in web console
spring.jpa.open-in-view=true
# enable print sql
spring.jpa.show-sql=true
# create:Create the schema and destroy previous data
# create-drop:Create and then destroy the schema at the end of the session.
# update:Update the schema if necessary.
# none:Disable DDL handling 不通过
spring.jpa.hibernate.ddl-auto=none
# 可选参数如下:
# org.springframework.boot.orm.jpa.hibernate.SpringImplicitNamingStrategy:nickName mapping to NIKE_NAME
# org.hibernate.boot.model.naming.ImplicitNamingStrategyJpaCompliantImpl
# org.hibernate.boot.model.naming.ImplicitNamingStrategyComponentPathImpl extends ImplicitNamingStrategyJpaCompliantImpl
# org.hibernate.boot.model.naming.ImplicitNamingStrategyLegacyHbmImpl extends ImplicitNamingStrategyJpaCompliantImpl
# org.hibernate.boot.model.naming.ImplicitNamingStrategyLegacyJpaImpl extends ImplicitNamingStrategyJpaCompliantImpl
# 隐式命名策略,当没有使用@Table和@Column注解时,implicit-strategy配置项才会被使用
spring.jpa.hibernate.naming.implicit-strategy=org.springframework.boot.orm.jpa.hibernate.SpringImplicitNamingStrategy
# 物理命名策略,用于转换“逻辑名称”(隐式或显式)的表或列成一个物理名称。
# org.hibernate.boot.model.naming.PhysicalNamingStrategyStandardImpl:nikeName mapping to nikeName,save the origin name
# org.springframework.boot.orm.jpa.hibernate.SpringPhysicalNamingStrategy:nikeName mapping to nike_name,split with ,
spring.jpa.hibernate.naming.physical-strategy=org.springframework.boot.orm.jpa.hibernate.SpringPhysicalNamingStrategy
spring.jpa.database=mysql

# config mySQL data source
spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver
spring.datasource.url=jdbc:mysql://localhost:3306/spring_cloud_mysql8_test_db?serverTimezone=UTC&useUnicode=true&characterEncoding=UTF-8&useSSL=false&allowPublicKeyRetrieval=true
spring.datasource.username=root
spring.datasource.password=toor
spring.datasource.initialization-mode=always
spring.datasource.schema=classpath:schema.sql
spring.datasource.data=classpath*:data.sql

# config actuator
# show health monitor detail
management.endpoint.health.show-details=when_authorized

2.6.1 schema.sql初始化表结构

值得注意的是

  • application-dev.properties里面配置的是H2 数据库,使用JPA 自动建表方法
  • application-test.properties 里面配置的是MySQL 8 数据库,使用SQL建表方法

由于schema.sql和data.sql SQL 语句是针对MySQL 语法的,因此比如激活application-test.properties 才有效

application.properties 配置内容如下:

spring.profiles.active=uat

schema.sql初始化表结构 内容如下:

/*
 Navicat Premium Data Transfer

 Source Server         : localhost_mysql
 Source Server Type    : MySQL
 Source Server Version : 80019
 Source Host           : localhost:3306
 Source Schema         : spring_cloud_mysql8_test_db

 Target Server Type    : MySQL
 Target Server Version : 80019
 File Encoding         : 65001

 Date: 20/03/2020 17:33:49
*/

SET NAMES utf8mb4;
SET FOREIGN_KEY_CHECKS = 0;

-- ----------------------------
-- Table structure for tb_customer
-- ----------------------------
DROP TABLE IF EXISTS `tb_customer`;
CREATE TABLE `tb_customer` (
  `pk_uuid` bigint NOT NULL,
  `avatar` varchar(255) COLLATE utf8mb4_0900_bin DEFAULT NULL,
  `create_time` bigint DEFAULT NULL,
  `nike_name` varchar(255) COLLATE utf8mb4_0900_bin DEFAULT NULL,
  `update_time` bigint DEFAULT NULL,
  PRIMARY KEY (`pk_uuid`)
) ENGINE=MyISAM DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_bin;

-- ----------------------------
-- Table structure for tb_product
-- ----------------------------
DROP TABLE IF EXISTS `tb_product`;
CREATE TABLE `tb_product` (
  `pk_uuid` bigint NOT NULL,
  `cover_image` varchar(255) COLLATE utf8mb4_0900_bin DEFAULT NULL,
  `create_time` bigint DEFAULT NULL,
  `price` double DEFAULT NULL,
  `product_name` varchar(255) COLLATE utf8mb4_0900_bin DEFAULT NULL,
  `update_time` bigint DEFAULT NULL,
  PRIMARY KEY (`pk_uuid`)
) ENGINE=MyISAM DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_bin;

-- ----------------------------
-- Table structure for tb_product_comment
-- ----------------------------
DROP TABLE IF EXISTS `tb_product_comment`;
CREATE TABLE `tb_product_comment` (
  `pk_uuid` bigint NOT NULL,
  `author_id` bigint DEFAULT NULL,
  `content` varchar(255) COLLATE utf8mb4_0900_bin DEFAULT NULL,
  `create_time` bigint DEFAULT NULL,
  `product_id` bigint DEFAULT NULL,
  `update_time` bigint DEFAULT NULL,
  PRIMARY KEY (`pk_uuid`)
) ENGINE=MyISAM DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_bin;

SET FOREIGN_KEY_CHECKS = 1;

2.6.2 data.sql初始化测试数据

data.sql初始化测试数据内容如下:

-- 导入测试商品列表
insert into tb_product (pk_uuid, product_name, cover_image, price,create_time,update_time) values(1, '测试商品-001', '/products/cover/001.png', 100,CURRENT_TIMESTAMP(),CURRENT_TIMESTAMP());
insert into tb_product (pk_uuid, product_name, cover_image, price,create_time,update_time) values(2, '测试商品-002', '/products/cover/002.png', 200,CURRENT_TIMESTAMP(),CURRENT_TIMESTAMP());
insert into tb_product (pk_uuid, product_name, cover_image, price,create_time,update_time) values(3, '测试商品-003', '/products/cover/003.png', 300,CURRENT_TIMESTAMP(),CURRENT_TIMESTAMP());
insert into tb_product (pk_uuid, product_name, cover_image, price,create_time,update_time) values(4, '测试商品-004', '/products/cover/004.png', 400,CURRENT_TIMESTAMP(),CURRENT_TIMESTAMP());
insert into tb_product (pk_uuid, product_name, cover_image, price,create_time,update_time) values(5, '测试商品-005', '/products/cover/005.png', 500,CURRENT_TIMESTAMP(),CURRENT_TIMESTAMP());
-- 导入测试用户列表
insert into tb_customer (pk_uuid, NIKE_NAME, AVATAR,create_time,update_time) values(1, 'zhangSan', '/users/avatar/zhangsan.png',CURRENT_TIMESTAMP(),CURRENT_TIMESTAMP());
insert into tb_customer (pk_uuid, NIKE_NAME, AVATAR,create_time,update_time) values(2, 'lisi', '/users/avatar/lisi.png',CURRENT_TIMESTAMP(),CURRENT_TIMESTAMP());
insert into tb_customer (pk_uuid, NIKE_NAME, AVATAR,create_time,update_time) values(3, 'wangwu', '/users/avatar/wangwu.png',CURRENT_TIMESTAMP(),CURRENT_TIMESTAMP());
insert into tb_customer (pk_uuid, NIKE_NAME, AVATAR,create_time,update_time) values(4, 'yanxiaoliu', '/users/avatar/yanxiaoliu.png',CURRENT_TIMESTAMP(),CURRENT_TIMESTAMP());
-- 导入商品3的评论列表
insert into tb_product_comment (pk_uuid, product_id, author_id, content, create_time,update_time) values(1, 3, 1, '非常不错的商品', CURRENT_TIMESTAMP(),CURRENT_TIMESTAMP());
insert into tb_product_comment (pk_uuid, product_id, author_id, content, create_time,update_time) values(2, 3, 3, '非常不错的商品+1', CURRENT_TIMESTAMP(),CURRENT_TIMESTAMP());
insert into tb_product_comment (pk_uuid, product_id, author_id, content, create_time,update_time) values(3, 3, 4, '哈哈,谁用谁知道', CURRENT_TIMESTAMP(),CURRENT_TIMESTAMP());

2.7 配置统一页面返回数据实体类

AppResponseVO.java编写如下:

import io.swagger.annotations.ApiModelProperty;
import lombok.Data;
import org.springframework.stereotype.Component;

import java.io.Serializable;

/**
 * @author qing-feng.zhao
 * @description
 * @date 2020/2/4 17:41
 */
@Component
@Data
public class AppResponseVO implements Serializable {
	/**
	 * 序列化Id
	 */
	private static final long serialVersionUID = 7020204650750610804L;
	/**
	 * 响应码
	 */
	@ApiModelProperty(value="HTTP响应码")
	private Integer responseCode;
	/**
	 * 响应消息
	 */
	@ApiModelProperty(value="HTTP响应消息")
	private String responseMessage;
	/**
	 * 响应数据
	 */
	@ApiModelProperty(value="HTTP业务数据")
	private Object responseData;
}

2.8 创建业务相关实体类

为了更好的系统兼容性,业务实体类我这里划分为如下三种:

实体类定义规范 解释
*Entity.java 格式的实体类 这种实体类专门用于和数据库表映射使用,以及Dao 层操作使用
*VO.java 格式的实体类 这种实体类主要用于页面传输使用,比如Service返回类型使用
*Query.java 格式的实体类 一般用作参数传输使用

2.8.1 创建用户表相关实体类

2.8.1.1 用户表持久层映射类

CustomerEntity.java 编写代码如下:

import lombok.Data;
import org.hibernate.annotations.GenericGenerator;

import javax.persistence.*;
import java.io.Serializable;

/**
 * @author qing-feng.zhao
 */
@Data
@Entity
@Table(name = "tb_customer")
public class CustomerEntity implements Serializable {
	/**
	 * 序列化Id
	 */
	private static final long serialVersionUID = -4408179126050937963L;
	/**
	 * 用户数据库主键
	 */
	@Id
	@GeneratedValue(generator = "SnowFlakeId")
	@GenericGenerator(name = "SnowFlakeId", strategy ="com.xingyun.singlearchitecturespringbootshoppingsample.customize.SnowFlakeIdGenerator")
	@Column(name = "pk_uuid")
	private Long uuid;
	/**
	 * 用户昵称
	 */
	@Basic
	@Column(name = "nike_name")
	private String nikeName;
	/**
	 * 用户头像
	 */
	@Basic
	@Column(name = "avatar")
	private String avatar;

	@Column(name = "create_time")
	private Long createTime;

	@Column(name = "update_time")
	private Long updateTime;
}

2.8.1.2 用户表页面使用实体类

CustomerVO.java

import io.swagger.annotations.ApiModelProperty;
import lombok.Data;

import java.io.Serializable;

/**
 * @author qing-feng.zhao
 * @description
 * @date 2020/2/5 7:46
 */
@Data
public class CustomerVO implements Serializable {
	/**
	 * 序列化Id
	 */
	private static final long serialVersionUID = -7079021938925671262L;
	/**
	 * 用户数据库主键
	 */
	@ApiModelProperty(value="用户数据库主键")
	private Long id;
	/**
	 * 用户昵称
	 */
	@ApiModelProperty(value="用户昵称")
	private String nikeName;
	/**
	 * 用户头像
	 */
	@ApiModelProperty(value="用户头像")
	private String avatar;
}

值得注意的是,

  • @ApiModelProperty(value=“用户头像”) 是Swagger 的API 注解
  • 如果查询条件为实体类所有对象,且对象比较多的时候就不建议一个参数一个参数编写Swagger 参数注解了。

2.8.1.3 用户表参数传输使用实体类

CustomerQuery.java

import io.swagger.annotations.ApiModelProperty;
import lombok.Data;

import java.io.Serializable;

/**
 * @author qing-feng.zhao
 * @description Customer 查询对象
 * @date 2020/2/5 7:50
 */
@Data
public class CustomerQuery implements Serializable {
	/**
	 * 序列化Id
	 */
	private static final long serialVersionUID = -8575488180000985633L;
	/**
	 * 用户数据库主键
	 */
	@ApiModelProperty(value="用户数据库主键")
	private Long id;
	/**
	 * 用户昵称
	 */
	@ApiModelProperty(value="用户昵称")
	private String nikeName;
	/**
	 * 用户头像
	 */
	@ApiModelProperty(value="用户头像")
	private String avatar;
}

值得注意的是,

  • @ApiModelProperty(value=“用户头像”) 是Swagger 的API 注解
  • 如果查询条件为实体类所有对象,且对象比较多的时候就不建议一个参数一个参数编写Swagger 参数注解了。

2.8.2 创建产品表相关实体类

2.8.2.1 产品表持久层映射实体类

ProductEntity.java 编写内容如下:

import lombok.Data;
import org.hibernate.annotations.GenericGenerator;

import javax.persistence.*;
import java.io.Serializable;

/**
 * @author qing-feng.zhao
 */
@Data
@Entity
@Table(name = "tb_product")
public class ProductEntity implements Serializable {
	/**
	 * 序列化Id
	 */
	private static final long serialVersionUID = -7290713691909197522L;
	/**
	 * 商品数据库主键
	 */
	@Id
	@GeneratedValue(generator = "SnowFlakeId")
	@GenericGenerator(name = "SnowFlakeId", strategy ="com.xingyun.singlearchitecturespringbootshoppingsample.customize.SnowFlakeIdGenerator")
	@Column(name = "pk_uuid")
	private Long uuid;
	/**
	 * 商品名称
	 */
	@Column(name = "product_name")
	private String productName;
	/**
	 * 商品封面图片
	 */
	@Column(name = "cover_image")
	private String coverImage;
	/**
	 * 商品价格
	 */
	@Column(name = "price")
	private Double price;

	/**
	 * 创建时间
	 */
	@Column(name = "create_time")
	private Long createTime;

	/**
	 * 更新时间
	 */
	@Column(name = "update_time")
	private Long updateTime;
}

2.8.2.2 产品表页面使用实体类

ProductVO.java

import io.swagger.annotations.ApiModelProperty;
import lombok.Data;

import java.io.Serializable;

/**
 * @author qing-feng.zhao
 */
@Data
public class ProductVO implements Serializable {
	/**
	 * 序列化Id
	 */
	private static final long serialVersionUID = -4481553529553540746L;
	/**
	 * 商品数据库主键
	 */
	@ApiModelProperty(value="商品数据库主键")
	private Long uuid;
	/**
	 * 商品名称
	 */
	@ApiModelProperty(value="商品名称")
	private String name;
	/**
	 * 商品封面图片
	 */
	@ApiModelProperty(value="商品封面图片")
	private String coverImage;
	/**
	 * 商品价格(单位:分)
	 */
	@ApiModelProperty(value="商品价格(单位:分)")
	private Double price;
}

2.8.3 创建产品评论表相关实体类

2.8.3.1 产品评论表持久层映射实体类

ProductCommentEntity.java 内容如下:


import lombok.Data;
import org.hibernate.annotations.GenericGenerator;

import javax.persistence.*;
import java.io.Serializable;

/**
 * @author qing-feng.zhao
 */
@Data
@Entity
@Table(name = "tb_product_comment")
public class ProductCommentEntity implements Serializable {
	/**
	 * 序列化Id
	 */
	private static final long serialVersionUID = 3335736243004941851L;
	/**
	 * 商品评论数据库主键
	 */
	@Id
	@GeneratedValue(generator = "SnowFlakeId")
	@GenericGenerator(name = "SnowFlakeId", strategy ="com.xingyun.singlearchitecturespringbootshoppingsample.customize.SnowFlakeIdGenerator")
	@Column(name = "pk_uuid")
	private Long uuid;
	/**
	 * 所示商品的ID
	 */
	@Basic
	@Column(name = "product_id")
	private Long productId;
	/**
	 * 评论作者的Id
	 */
	@Basic
	@Column(name = "author_id")
	private Long authorId;
	/**
	 * 评论的具体内容
	 */
	@Basic
	@Column(name = "content")
	private String content;
	/**
	 * 创建时间
	 */
	@Column(name = "create_time")
	private Long createTime;

	/**
	 * 更新时间
	 */
	@Column(name = "update_time")
	private Long updateTime;
}

2.8.3.2 产品评论表页面使用实体类

ProductVO.java


import io.swagger.annotations.ApiModelProperty;
import lombok.Data;

import javax.persistence.Column;
import java.io.Serializable;

/**
 * @author qing-feng.zhao
 */
@Data
public class ProductVO implements Serializable {
	/**
	 * 序列化Id
	 */
	private static final long serialVersionUID = -4481553529553540746L;
	/**
	 * 商品数据库主键
	 */
	@ApiModelProperty(value="商品数据库主键")
	private Long uuid;
	/**
	 * 商品名称
	 */
	@ApiModelProperty(value="商品名称")
	private String name;
	/**
	 * 商品封面图片
	 */
	@ApiModelProperty(value="商品封面图片")
	private String coverImage;
	/**
	 * 商品价格(单位:分)
	 */
	@ApiModelProperty(value="商品价格(单位:分)")
	private Double price;
	/**
	 * 创建时间
	 */
	 @ApiModelProperty(value="创建时间")
	private Long createTime;
	/**
	 * 更新时间
	 */
	@ApiModelProperty(value="更新时间")
	private Long updateTime;
}

2.9 定义用户业务相关类

2.9.1 用户Dao层JPA 接口

CustomerJpaRepository.java

import com.xingyun.singlearchitecturespringbootshoppingsample.business.customer.model.CustomerEntity;
import org.springframework.data.jpa.repository.JpaRepository;

/**
 * @author qing-feng.zhao
 * @description JpaRepository extends PagingAndSortingRepository extends CrudRepository extends Repository
 * @date 2020/2/1 8:38
 */
public interface CustomerJpaRepository extends JpaRepository<CustomerEntity,Long>{
}

2.9.2 用户服务接口

UserService.java

import com.xingyun.singlearchitecturespringbootshoppingsample.business.customer.model.CustomerQuery;
import com.xingyun.singlearchitecturespringbootshoppingsample.business.customer.model.CustomerVO;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.Pageable;

import java.util.List;

/**
 * @author qing-feng.zhao
 * @description
 * @date 2020/2/5 7:45
 */
public interface CustomerService {
	/**
	 * 获取用户分页数据
	 * @param pageable 分页参数
	 * @return 分页数据
	 */
	Page<CustomerVO> getPageUser(Pageable pageable);
	/**
	 * 加载指定的用户信息
	 * @param id 用户主键
	 * @return 加载指定的用户信息
	 */
	CustomerVO loadUser(Long id);
	/**
	 * 保存/更新用户
	 * @param customerQuery
	 * @return
	 */
	CustomerVO saveUser(CustomerQuery customerQuery);

	/**
	 * 删除指定用户
	 * @param id 所要删除的用户主键
	 */
	void removedUserById(Long id);
}

2.9.3 用户服务接口实现类

UserService.java 编写内容如下:

import com.xingyun.singlearchitecturespringbootshoppingsample.business.customer.dao.jpa.CustomerJpaRepository;
import com.xingyun.singlearchitecturespringbootshoppingsample.business.customer.model.CustomerQuery;
import com.xingyun.singlearchitecturespringbootshoppingsample.business.customer.model.CustomerEntity;
import com.xingyun.singlearchitecturespringbootshoppingsample.business.customer.model.CustomerVO;
import org.springframework.beans.BeanUtils;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.PageImpl;
import org.springframework.data.domain.Pageable;
import org.springframework.stereotype.Service;

import java.util.ArrayList;
import java.util.List;
import java.util.Optional;

/**
 * @author qing-feng.zhao
 * @description
 * @date 2020/2/5 7:55
 */
@Service
public class CustomerServiceImpl implements CustomerService {

	protected final CustomerJpaRepository customerJpaRepository;

	public CustomerServiceImpl(CustomerJpaRepository customerJpaRepository) {
		this.customerJpaRepository = customerJpaRepository;
	}

	@Override
	public Page<CustomerVO> getPageUser(Pageable pageable) {
		Page<CustomerEntity> userPageEntity=this.customerJpaRepository.findAll(pageable);
		//userPageEntity.getContent() return List<object>
		List<CustomerEntity> userPageList= userPageEntity.getContent();
		// total record count
		long totalCount=userPageEntity.getTotalElements();
		//
		List<CustomerVO> customerVOList =new ArrayList<>();

		for (CustomerEntity customerEntity :userPageList
			 ) {
			CustomerVO customerVO =new CustomerVO();
			BeanUtils.copyProperties(customerEntity, customerVO);
			customerVOList.add(customerVO);
		}
		return new PageImpl(customerVOList,pageable,totalCount);
	}

	@Override
	public CustomerVO loadUser(Long id) {
		CustomerVO customerVO =new CustomerVO();
		Optional<CustomerEntity> userOptionalEntity=this.customerJpaRepository.findById(id);
		if(userOptionalEntity.isPresent()){
			CustomerEntity customerEntity =userOptionalEntity.get();
			BeanUtils.copyProperties(customerEntity, customerVO);
		}
		return customerVO;
	}

	@Override
	public CustomerVO saveUser(CustomerQuery customerQuery) {
		CustomerVO customerVO =new CustomerVO();
		Optional<CustomerEntity> userEntityOptional=this.customerJpaRepository.findById(customerQuery.getId());
		if(userEntityOptional.isPresent()){
			CustomerEntity customerEntity =userEntityOptional.get();
			BeanUtils.copyProperties(customerEntity, customerVO);
		}else{
			BeanUtils.copyProperties(customerQuery, customerVO);
		}
		return customerVO;
	}

	@Override
	public void removedUserById(Long id) {
		this.customerJpaRepository.deleteById(id);
	}
}

2.9.4 用户API Controller层

CustomerEndPoint.java

import com.xingyun.singlearchitecturespringbootshoppingsample.business.customer.model.CustomerQuery;
import com.xingyun.singlearchitecturespringbootshoppingsample.business.customer.model.CustomerVO;
import com.xingyun.singlearchitecturespringbootshoppingsample.constant.HttpStatusCodeConstant;
import com.xingyun.singlearchitecturespringbootshoppingsample.model.AppResponseVO;
import com.xingyun.singlearchitecturespringbootshoppingsample.business.customer.service.CustomerService;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiImplicitParam;
import io.swagger.annotations.ApiImplicitParams;
import io.swagger.annotations.ApiOperation;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.Pageable;
import org.springframework.web.bind.annotation.*;

import java.util.Collections;
import java.util.List;

/**
 * @author qing-feng.zhao
 * @description
 * @date 2020/2/5 8:32
 */
@RestController
@Api(value = "CustomerEndPoint",tags = "用户管理相关API")
public class CustomerEndPoint {
	/**
	 * 构造方法注入
	 */
	private final CustomerService customerService;
	private final AppResponseVO appResponseVO;

	public CustomerEndPoint(CustomerService customerService, AppResponseVO appResponseVO) {
		this.customerService = customerService;
		this.appResponseVO = appResponseVO;
	}
	/**
	 *
	 * @param pageable
	 * @return
	 */
	@GetMapping(value = "/users")
	@ApiOperation(value = "获取用户分页数据",notes ="获取用户分页数据",httpMethod = "GET",tags = "用户管理相关API")
	@ApiImplicitParams({
			@ApiImplicitParam(name = "page",value = "第几页,从0开始,默认为第0页",example = "0",dataType = "int",paramType = "query"),
			@ApiImplicitParam(name = "size",value = "每一页记录数的大小,默认为20",example = "20",dataType = "int",paramType = "query"),
			@ApiImplicitParam(name = "sort",value = "排序,格式为:property,property(,ASC|DESC)的方式组织",example = "ASC",dataType = "String",paramType = "query"),
	})
	public AppResponseVO findAll(Pageable pageable){
		Page<CustomerVO> userVOPage= this.customerService.getPageUser(pageable);
		appResponseVO.setResponseCode(HttpStatusCodeConstant.OK_CODE);
		appResponseVO.setResponseMessage("获取用户分页数据成功");
		if(null!=userVOPage){
			List<CustomerVO> customerVOList =userVOPage.getContent();
			appResponseVO.setResponseData(customerVOList);
		}else{
			appResponseVO.setResponseData(Collections.EMPTY_LIST);
		}
		return appResponseVO;
	}

	@GetMapping(value = "/users/{id}")
	@ApiOperation(value = "获取用户详情数据",notes = "获取用户详情数据",httpMethod = "GET",tags = "用户管理相关API")
	@ApiImplicitParam(name = "id",value = "用户的主键",example = "1",dataType = "int",paramType = "path")
	public AppResponseVO detail(@PathVariable Long id){
		CustomerVO customerVO =this.customerService.loadUser(id);
		appResponseVO.setResponseCode(HttpStatusCodeConstant.OK_CODE);
		appResponseVO.setResponseMessage("获取用户详情数据成功");
		appResponseVO.setResponseData(customerVO);
		return appResponseVO;
	}

	@PostMapping(value = "/users/{id}")
	@ApiOperation(value = "更新用户详情数据",notes = "更新用户详情数据",httpMethod = "POST",tags = "用户管理相关API")
	@ApiImplicitParams({
			@ApiImplicitParam(name = "id", value = "用户的主键",example = "1", dataType = "int",paramType = "path"),
			@ApiImplicitParam(name = "userDTO", value = "用户详情数据", dataType = "UserDTO", paramType = "body"),
	})
	public AppResponseVO update(@PathVariable Long id, @RequestBody CustomerQuery customerQuery){
		customerQuery.setId(id);
		CustomerVO customerVO =this.customerService.saveUser(customerQuery);
		appResponseVO.setResponseCode(HttpStatusCodeConstant.OK_CODE);
		appResponseVO.setResponseMessage("更新用户详情数据成功");
		appResponseVO.setResponseData(customerVO);
		return appResponseVO;
	}

	@DeleteMapping(value = "/users/{id}")
	@ApiOperation(value = "删除指定用户",notes = "删除指定用户",httpMethod = "DELETE",tags = "用户管理相关API")
	@ApiImplicitParams({
			@ApiImplicitParam(name = "id",value = "所要删除用户的主键",example = "1",dataType = "int",paramType = "path")
	})
	public AppResponseVO delete(@PathVariable Long id){
		this.customerService.removedUserById(id);
		appResponseVO.setResponseCode(HttpStatusCodeConstant.OK_CODE);
		appResponseVO.setResponseMessage("删除指定用户成功");
		appResponseVO.setResponseData(true);
		return appResponseVO;
	}
}

2.10 产品业务其他相关类

2.10.1 产品Dao层JPA 接口

ProductJpaRepository.java

import com.xingyun.singlearchitecturespringbootshoppingsample.business.product.model.ProductEntity;
import org.springframework.data.jpa.repository.JpaRepository;

/**
 * @author qing-feng.zhao
 */
public interface ProductJpaRepository extends JpaRepository<ProductEntity,Long> {
}

2.10.2 产品服务接口

ProductService.java


import com.xingyun.singlearchitecturespringbootshoppingsample.business.product.model.ProductVO;
import com.xingyun.singlearchitecturespringbootshoppingsample.business.productcomment.model.ProductCommentVO;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.Pageable;

import java.util.List;

/**
 *
 * @author qing-feng.zhao
 */
public interface ProductService {
	/**
	 * 获取商品配置的分页数据
	 * @param pageable 分页参数
	 * @return 分页数据
	 */
	Page<ProductVO> getProductListPage(Pageable pageable);

	/**
	 * 加载指定的商品配置
	 * @param id 商品配置ID
	 * @return
	 */
	ProductVO loadProduct(Long id);

	/**
	 * 加载指定商品的评论列表
	 * @param productId
	 * @return
	 */
	List<ProductCommentVO> findAllByProductId(Long productId);
}

2.10.3 产品服务接口实现类

ProductServiceImpl.java

import com.xingyun.singlearchitecturespringbootshoppingsample.business.product.dao.jpa.ProductJpaRepository;
import com.xingyun.singlearchitecturespringbootshoppingsample.business.product.model.ProductEntity;
import com.xingyun.singlearchitecturespringbootshoppingsample.business.product.model.ProductVO;
import com.xingyun.singlearchitecturespringbootshoppingsample.business.productcomment.service.ProductCommentService;
import com.xingyun.singlearchitecturespringbootshoppingsample.business.productcomment.model.ProductCommentVO;
import org.springframework.beans.BeanUtils;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.PageImpl;
import org.springframework.data.domain.Pageable;
import org.springframework.stereotype.Service;

import java.util.ArrayList;
import java.util.List;
import java.util.Optional;

/**
 * @author qing-feng.zhao
 */
@Service
public class ProductServiceImpl implements ProductService {

	/**
	 * 尽量使用构造方法注入
	 */
	protected final ProductJpaRepository productJpaRepository;

	protected final ProductCommentService productCommentService;

	public ProductServiceImpl(ProductJpaRepository productJpaRepository,ProductCommentService productCommentService) {
		this.productJpaRepository = productJpaRepository;
		this.productCommentService=productCommentService;
	}

	@Override
	public Page<ProductVO> getProductListPage(Pageable pageable) {

		Page<ProductEntity> productEntityPage=this.productJpaRepository.findAll(pageable);
		//userPageEntity.getContent() return List<object>
		List<ProductEntity> productEntityList= productEntityPage.getContent();
		// total record count
		long totalCount=productEntityPage.getTotalElements();
		//
		List<ProductVO> productVOList=new ArrayList<>();

		for (ProductEntity productEntity:productEntityList
		) {
			ProductVO productVO=new ProductVO();
			BeanUtils.copyProperties(productEntity,productVO);
			productVOList.add(productVO);
		}
		return new PageImpl(productVOList,pageable,totalCount);
	}

	/**
	 * 根据商品Id
	 * @param id 商品配置ID
	 * @return
	 */
	@Override
	public ProductVO loadProduct(Long id) {
		ProductVO productVO=new ProductVO();
		Optional<ProductEntity> productEntityOptional=this.productJpaRepository.findById(id);
		if(productEntityOptional.isPresent()){
			ProductEntity productEntity=productEntityOptional.get();
			BeanUtils.copyProperties(productEntity,productVO);
		}
		return productVO;
	}

	/**
	 * 加载评论列表
	 * @param productId
	 * @return
	 */
	@Override
	public List<ProductCommentVO> findAllByProductId(Long productId) {
		return this.productCommentService.findProductCommentByProductId(productId);
	}
}

2.10.4 产品API Controller 类

ProductEndPoint.java


import com.xingyun.singlearchitecturespringbootshoppingsample.business.product.model.ProductVO;
import com.xingyun.singlearchitecturespringbootshoppingsample.business.product.service.ProductService;
import com.xingyun.singlearchitecturespringbootshoppingsample.constant.HttpStatusCodeConstant;
import com.xingyun.singlearchitecturespringbootshoppingsample.model.AppResponseVO;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiImplicitParam;
import io.swagger.annotations.ApiImplicitParams;
import io.swagger.annotations.ApiOperation;
import lombok.extern.slf4j.Slf4j;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.Pageable;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RestController;

import java.util.Collections;
import java.util.List;

/**
 * @author qing-feng.zhao
 */
@Api(value = "ProductEndPoint",tags = "产品管理相关API")
@Slf4j
@RestController
public class ProductEndPoint {

	private final AppResponseVO appResponseVO;
	private final ProductService productService;

	public ProductEndPoint(AppResponseVO appResponseVO, ProductService productService) {
		this.appResponseVO = appResponseVO;
		this.productService = productService;
	}


	@ApiOperation(value = "获取产品分页数据",notes ="获取产品分页数据",httpMethod = "GET",tags = "产品管理相关API")
	@ApiImplicitParams({
			@ApiImplicitParam(name = "page",value = "第几页,从0开始,默认为第0页",example = "0",dataType = "int",paramType = "query"),
			@ApiImplicitParam(name = "size",value = "每一页记录数的大小,默认为20",example = "20",dataType = "int",paramType = "query"),
			@ApiImplicitParam(name = "sort",value = "排序,格式为:property,property(,ASC|DESC)的方式组织",example = "ASC",dataType = "String",paramType = "query"),
	})
	@GetMapping(value = "/products")
	public AppResponseVO showProductList(Pageable pageable){
		Page<ProductVO> productVOPageList=productService.getProductListPage(pageable);
		appResponseVO.setResponseCode(HttpStatusCodeConstant.OK_CODE);
		appResponseVO.setResponseMessage("获取产品分页数据成功");
		if(null!=productVOPageList){
			List<ProductVO> productVOList=productVOPageList.getContent();
			appResponseVO.setResponseData(productVOList);
		}else{
			appResponseVO.setResponseData(Collections.EMPTY_LIST);
		}
		return appResponseVO;
	}

	/**
	 * 加载指定的商品配置
	 * @param productId 商品配置Id
	 * @return
	 */
	@ApiOperation(value = "获取产品详情数据",notes = "获取产品详情数据",httpMethod = "GET",tags = "产品管理相关API")
	@ApiImplicitParam(name = "productId",value = "产品表的主键",example = "1",dataType = "int",paramType = "path")
	@GetMapping(value = "/products/{productId}")
	public AppResponseVO loadProduct(@PathVariable(value = "productId")Long productId){
		try {
			ProductVO productVO = productService.loadProduct(productId);
			appResponseVO.setResponseCode(HttpStatusCodeConstant.OK_CODE);
			appResponseVO.setResponseMessage("加载指定的商品配置成功");
			appResponseVO.setResponseData(productVO);
		} catch (Exception e) {
			log.error(e.getMessage(),e);
			appResponseVO.setResponseCode(HttpStatusCodeConstant.INTERNAL_SERVER_ERROR_CODE);
			appResponseVO.setResponseMessage("加载指定的商品配置出错");
			appResponseVO.setResponseData(null);

		}
		return appResponseVO;
	}
}

2.11 产品评论其他相关类

2.11.1 产品评论Dao层JPA 接口

ProductCommentJpaRepository.java

import com.xingyun.singlearchitecturespringbootshoppingsample.business.productcomment.model.ProductCommentEntity;
import org.springframework.data.jpa.repository.JpaRepository;

import java.util.List;

/**
 * @author qing-feng.zhao
 */
public interface ProductCommentJpaRepository extends JpaRepository<ProductCommentEntity,Long> {
	/**
	 * 根据产品Id获取评论列表
	 * @param productId 所示产品Id
	 * @return 返回评论列表
	 */
	List<ProductCommentEntity> findAllByProductId(Long productId);

	/**
	 * 根据发布者用户id 查找相关评论列表
	 * @param authorId
	 * @return
	 */
	List<ProductCommentEntity> findAllByAuthorId(Long authorId);
}

2.11.2 产品评论服务接口

ProductCommentService.java

import com.xingyun.singlearchitecturespringbootshoppingsample.business.productcomment.model.ProductCommentVO;

import java.util.List;

/**
 * 商品评论管理repository
 * @author qing-feng.zhao
 */
public interface ProductCommentService{
	/**
	 * 根据产品Id 返回所有相关评论
	 * @param productId
	 * @return
	 */
	List<ProductCommentVO> findProductCommentByProductId(Long productId);

	/**
	 * 根据发布者Id 获取评论列表
	 * @param authorId
	 * @return
	 */
	List<ProductCommentVO> findProductCommentByAuthorId(Long authorId);
}

2.11.3 产品评论服务接口实现类

ProductCommentServiceImpl.java

import com.xingyun.singlearchitecturespringbootshoppingsample.business.productcomment.dao.jpa.ProductCommentJpaRepository;
import com.xingyun.singlearchitecturespringbootshoppingsample.business.productcomment.model.ProductCommentEntity;
import com.xingyun.singlearchitecturespringbootshoppingsample.business.productcomment.model.ProductCommentVO;
import org.springframework.beans.BeanUtils;
import org.springframework.stereotype.Service;

import java.util.ArrayList;
import java.util.List;

/**
 * @author qing-feng.zhao
 */
@Service
public class ProductCommentServiceImpl implements ProductCommentService{

	protected final ProductCommentJpaRepository productCommentJpaRepository;

	public ProductCommentServiceImpl(ProductCommentJpaRepository productCommentJpaRepository) {
		this.productCommentJpaRepository = productCommentJpaRepository;
	}


	@Override
	public List<ProductCommentVO> findProductCommentByProductId(Long productId) {

		List<ProductCommentVO> productCommentVOList=new ArrayList<>();

		List<ProductCommentEntity> productCommentEntityList=productCommentJpaRepository.findAllByProductId(productId);
		for (ProductCommentEntity productCommentEntity:productCommentEntityList
		) {
			ProductCommentVO productCommentVO=new ProductCommentVO();
			BeanUtils.copyProperties(productCommentEntity,productCommentVO);
			productCommentVOList.add(productCommentVO);
		}
		return productCommentVOList;
	}

	@Override
	public List<ProductCommentVO> findProductCommentByAuthorId(Long authorId) {
		List<ProductCommentVO> productCommentVOList=new ArrayList<>();
		List<ProductCommentEntity> productCommentEntityList=productCommentJpaRepository.findAllByAuthorId(authorId);
		for (ProductCommentEntity productCommentEntity:productCommentEntityList
		) {
			ProductCommentVO productCommentVO=new ProductCommentVO();
			BeanUtils.copyProperties(productCommentEntity,productCommentVO);
			productCommentVOList.add(productCommentVO);
		}
		return productCommentVOList;
	}
}

2.11.4 产品评论API Controller 类

ProductCommentEndPoint.java


import com.xingyun.singlearchitecturespringbootshoppingsample.business.productcomment.model.ProductCommentVO;
import com.xingyun.singlearchitecturespringbootshoppingsample.business.productcomment.service.ProductCommentService;
import com.xingyun.singlearchitecturespringbootshoppingsample.constant.HttpStatusCodeConstant;
import com.xingyun.singlearchitecturespringbootshoppingsample.model.AppResponseVO;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiImplicitParam;
import io.swagger.annotations.ApiOperation;
import lombok.extern.slf4j.Slf4j;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RestController;

import java.util.List;

/**
 * @author qing-feng.zhao
 */
@Api(value = "ProductEndPoint",tags = "产品评论管理相关API")
@Slf4j
@RestController
public class ProductCommentEndPoint {
	private final AppResponseVO appResponseVO;
	private final ProductCommentService productCommentService;

	public ProductCommentEndPoint(AppResponseVO appResponseVO, ProductCommentService productCommentService) {
		this.appResponseVO = appResponseVO;
		this.productCommentService = productCommentService;
	}
	@GetMapping(value = "/productComments/{productId}")
	@ApiOperation(value = "获取产品评论列表数据",notes = "获取产品评论列表数据",httpMethod = "GET",tags = "产品评论管理相关API")
	@ApiImplicitParam(name = "productId",value = "产品评论表中产品的ID",example = "1",dataType = "int",paramType = "path")
	public AppResponseVO listProductComment(@PathVariable(value = "productId")Long productId){
		try {
			List<ProductCommentVO> productCommentVOPageList = this.productCommentService.findProductCommentByProductId(productId);
			appResponseVO.setResponseCode(HttpStatusCodeConstant.OK_CODE);
			appResponseVO.setResponseMessage("获取产品评论列表数据成功");
			appResponseVO.setResponseData(productCommentVOPageList);
		} catch (Exception e) {
			log.error(e.getMessage(),e);
			appResponseVO.setResponseCode(HttpStatusCodeConstant.INTERNAL_SERVER_ERROR_CODE);
			appResponseVO.setResponseMessage("获取产品评论列表数据失败");
			appResponseVO.setResponseData(null);
		}
		return appResponseVO;
	}
}

2.12 配置Spring Data JPA

由于JPA 接口类分散在多个文件夹下因此和之前的配置有所不同。

SpringDataJpaConfig.java配置如下:

import org.springframework.context.annotation.Configuration;
import org.springframework.data.jpa.repository.config.EnableJpaRepositories;

/**
 * @author qing-feng.zhao
 * @description
 * @date 2020/2/1 8:45
 */
@EnableJpaRepositories(basePackages = {
        "com.xingyun.singlearchitecturespringbootshoppingsample.business.customer.dao.jpa",
		"com.xingyun.singlearchitecturespringbootshoppingsample.business.productcomment.dao.jpa",
		"com.xingyun.singlearchitecturespringbootshoppingsample.business.product.dao.jpa",
})
@Configuration
public class SpringDataJpaConfig {
}

2.13 配置Swagger

由于API 不在同一个包下,因此和之前的也有所不同。

SpringFoxSwaggerConfig.java配置如下:



import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.bind.annotation.RestController;
import springfox.documentation.builders.ApiInfoBuilder;
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;

/**
 * @author qing-feng.zhao
 * @description
 * @date 2020/2/5 9:31
 */
@EnableSwagger2
@Configuration
public class SpringFoxSwaggerConfig {

	@Bean
	public ApiInfo apiInfo() {
		return new ApiInfoBuilder()
				.title("Swagger Restful API")
				.description("this is swagger restful api")
				.termsOfServiceUrl("https://xingyun.blog.csdn.net")
				.contact(new Contact("星云","https://xingyun.blog.csdn.net/column/info/33374","[email protected]"))
				.version("1.0")
				.build();
	}

	@Bean
	public Docket createRestApi(ApiInfo apiInfo) {
		return new Docket(DocumentationType.SWAGGER_2)
				.apiInfo(apiInfo)
				.groupName("SwaggerGroupOneAPI")
				.select()
				.apis(RequestHandlerSelectors.withClassAnnotation(RestController.class))
				.paths(PathSelectors.any())
				.build();
	}
}

2.14 配置Spring Security

SpringSecurityConfig.java


import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.core.userdetails.User;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.provisioning.InMemoryUserDetailsManager;

/**
 * @author qing-feng.zhao
 * @description
 * @date 2020/2/4 17:15
 */
@EnableWebSecurity
@Configuration
public class SpringSecurityConfig extends WebSecurityConfigurerAdapter {
	@Bean
	public static PasswordEncoder passwordEncoder() {
		return new BCryptPasswordEncoder();
	}

	/**
	 * 这个方法定义了哪些URL需要被保护那些URL不需要被保护
	 *
	 * @param http
	 * @throws Exception
	 */
	@Override
	protected void configure(HttpSecurity http) throws Exception {
		http
				//config allow include frame page
				.headers().frameOptions().sameOrigin()
				.and()
				.authorizeRequests()
				// swagger access
				.antMatchers("/swagger-ui.html", "/api/v1/**").hasRole("swagger-access-role")
				//db access
				.antMatchers("/h2-console").hasRole("h2-db-access-role")
				// 如果是 //home 请求不进行拦截
				.antMatchers("/", "/home", "/index", "/home.do", "/index.do").permitAll()
				.anyRequest().hasRole("app-role")
				.and()
				//构建一个基本的表单登录后台处理方法
				.formLogin()
				//可以配置使用Spring Security内置的登录页面,但是样式需要外网环境,否则样式会乱码
				.loginProcessingUrl("/login")
				//第一次如果拦截到非法请求,默认重定向到 /login 请求去处理
				//form 表单action配置也提交到这个地址去进行验证是否权限通过,验证过程由Spring Security框架完成
				//.loginPage("/login")
				//不拦截
				.permitAll()
				.and()
				//注销不拦截
				.logout()
				//注销成功后返回首页
				.logoutSuccessUrl("/")
				//清除认证
				.clearAuthentication(true)
				//不拦截
				.permitAll()
				.and()
				.csrf().disable();
	}

	@Override
	protected void configure(AuthenticationManagerBuilder auth) throws Exception {
		//使用密码类型
		auth.userDetailsService(userDetailsService()).passwordEncoder(passwordEncoder());
	}

	@Bean
	@Override
	public UserDetailsService userDetailsService() {
		UserDetails userDetailsDev =
				User.builder()
						//账号
						.username("admin")
						//密码 123456
						.password("$2a$10$0BVXV/xzovbD1rKpC.rwnuMCysuvH0llt3y2RLJkgG.9TTZJNPwIa")
						//角色
						.roles("app-role", "swagger-access-role", "h2-db-access-role")
						//账户是否过期
						.accountExpired(false)
						//是否锁定账户
						.accountLocked(false)
						//是否禁用该用户
						.disabled(false)
						//登录凭据已过期
						.credentialsExpired(false)
						.build();
		//内存中保存登录信息
		return new InMemoryUserDetailsManager(userDetailsDev);
	}
}

2.15 配置响应码静态类

HttpStatusCodeConstant

/**
 * @see [https://tool.lu/httpcode/]([https://tool.lu/httpcode/]),
 *      [https://httpstatuses.com/](https://httpstatuses.com/)
 * @author qing-feng.zhao
 * @description Http Status Code Constant
 * @date 2020/2/5 13:40
 */
public final class HttpStatusCodeConstant {
	/**
	 * OK
	 * (成功)
	 * 请求成功.成功的意义根据请求所使用的方法不同而不同.
	 * GET: 资源已被提取,并作为响应体传回客户端.
	 * HEAD: 实体头已作为响应头传回客户端
	 * POST: 经过服务器处理客户端传来的数据,适合的资源作为响应体传回客户端.
	 * TRACE: 服务器收到请求消息作为响应体传回客户端.
	 */
	public static final Integer OK_CODE=200;
	/**
	 * Bad Request
	 * (错误请求)
	 * 因发送的请求语法错误,服务器无法正常读取.
	 */
	public static final Integer BAD_REQUEST_CODE=400;
	/**
	 * Internal Server Error
	 * (内部服务器错误)
	 * 服务器遇到未知的无法解决的问题.
	 */
	public static final Integer INTERNAL_SERVER_ERROR_CODE=500;
}

2.16 访问访问首页页面控制器

HomePageController.java

import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;

/**
 * @author qing-feng.zhao
 * @description
 * @date 2020/2/4 17:18
 */
@Controller
public class HomePageController {
	@GetMapping(value = {"/","/home","/home.do","/index","/index.do"})
	public String home(){
		return "index";
	}
}

2.17 配置默认首页

index.html

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Home Page</title>
</head>
<body>

      <div align="center">
          <p>
              <a href="/swagger-ui.html">Read Restful API</a>
          </p>
          <p>
              <a href="/h2-console">Enter H2 DB Console</a>
          </p>
      </div>
</body>
</html>

2.18 访问Swagger API

  • 打开 http://127.0.0.1:8080 可以看到如下内容
    在这里插入图片描述
  • 点击Read Restful API,由于添加了Spring Security,因此API会被保护起来。
    在这里插入图片描述

输入账号:admin 输入密码:123456 即可登录

  • 登录成功如下所示:
    在这里插入图片描述

2.19 访问H2 Console 控制台

返回首页,点击 Enter H2 Console,进入如下页面
在这里插入图片描述

  • JDBC URL:填写如下内容:
jdbc:h2:mem:in_memory_test_db;DB_CLOSE_DELAY=-1

然后输入账号和密码,即可登录

账号:sa 密码: sa

  • 登录成功后如下所示:
    在这里插入图片描述

0x03 源码下载

single-architecture-spring-boot-shopping-sample

本篇完~

发布了194 篇原创文章 · 获赞 262 · 访问量 48万+

猜你喜欢

转载自blog.csdn.net/hadues/article/details/104121520