★★★电商项目开发实战★★★

    • 介绍

电商常见类型

类型

全称

解释

代表

B2C

Business To Consumer

商家对个人

天猫、京东、亚马逊

B2B

Business To Business

商家对商家

阿里巴巴

C2C

Consumer To Consumer

个人对个人

淘宝、闲鱼、拍拍

O2O

Online To Offline

线上&线下

国美、苏宁、糯米、口碑、美团、大众点评

P2P

Peer To Peer

点对点网贷

人人贷

谷粒商城是一个B2C模式的电商平台,销售自营商品给客户。

项目架构

微服务划分

    • -安装Linux虚拟机

下载&安装 VirtualBox

Oracle VM VirtualBox

安装前要开启 CPU 虚拟化

修改VirtualBox虚拟机文件默认存储位置

VirtualBox菜单->管理->全局设定->常规->默认虚拟电脑位置:指定一个容量较大的磁盘目录,例如:E:\VirtualBoxDisk\

下载&安装 Vagrant

Install | Vagrant | HashiCorp Developer ,安装程序会自动把安装路径加入到环境变量PATH

修改Vagrant镜像默认存储HOME目录:

增加环境变量VAGRANT_HOME指定到一个容量较大的磁盘目录下(默认为%User_Home%\.vagrant.d),例如:E:\VirtualBoxDisk\.vagrant.d

初始化一个虚拟centos7系统

在VAGRANT_HOME下新建目录centos7,进入centos7,打开window cmd窗口,运行命令:

vagrant init centos/7

Vagrant会从官方镜像仓库(Discover Vagrant Boxes - Vagrant Cloud)下载名为“centos/7”的镜像到VAGRANT_HOME,并在当前centos7目录下生成一个Vagrantfile配置文件

启动虚拟centos7系统

运行命令vagrant up即可启动虚拟机实例,VirtualBox将自动添加启动的实例。

系统root用户和vagrant用户的密码都是vagrant

运行命令vagrant ssh可连接虚拟机实例

配置网络:

以太网适配器 VirtualBox Host-Only Network的属性:

设置固定IPv4地址为192.168.56.1,用来当做虚拟机实例的网关

设置子网掩码为255.255.255.0,其他为空的不管

虚拟机实例的网络连接方式默认为使用网络地址转换(NAT)和端口转发,开发不方便,修改为仅主机网络(Host-Only):

修改Vagrantfile文件配置(取消注释并改ip值):config.vm.network "private_network", ip: "192.168.56.10"

运行命令vagrant reload加载Vagrantfile配置并重启虚拟机实例

互ping验证宿主机与虚拟机实例是否连通:

宿主机:ping 192.168.56.10

虚拟机实例:vagrant ssh进去系统之后:

ip addr命令查看是否新增了ip为192.168.56.10的网卡

ping 宿主机ip

配置账号:

默认只允许运行命令vagrant ssh登录,不方便开发,修改为允许账号密码登录:

vagrant ssh进去系统之后,切换为root用户,

vi /etc/ssh/sshd_config

修改 PasswordAuthentication值no改为yes

重启服务service sshd restart,然后可以ssh远程登录了

更换yum源:

备份原 yum 源

mv /etc/yum.repos.d/CentOS-Base.repo /etc/yum.repos.d/CentOS-Base.repo.backup

使用新 yum 源

curl -o /etc/yum.repos.d/CentOS-Base.repo http://mirrors.163.com/.help/CentOS7-Base-163.repo

生成缓存

yum makecache

更新yum源

yum -y update

    • -安装 Docker

参见Docker安装章节

    • -Docker安装Mysql

下载镜像文件

docker pull mysql:5.7

5.7为Image的Tag,获取方式:打开https://hub.docker.com/ 最上边搜索mysql,结果列表第一个点Docker Official Image,详情中点Tags,Filter Tags中搜索5.7,结果列表中查得Tag为5.7的Image

查看下载的镜像:

sudo docker images

创建实例并启动

sudo docker run -p 3306:3306 --name mysql \

-v /mydata/mysql/log:/var/log/mysql \

-v /mydata/mysql/data:/var/lib/mysql \

-v /mydata/mysql/conf:/etc/mysql \

-e MYSQL_ROOT_PASSWORD=root \

-d mysql:5.7

参数说明

-p 3306:3306:将容器的 3306 端口映射到主机的 3306 端口

--name mysql:设置容器名称

-v /mydata/mysql/conf:/etc/mysql:将配置文件夹挂载到主机(主机目录:容器内目录),方便在宿主机中查看修改mysql配置

-v /mydata/mysql/log:/var/log/mysql:将日志文件夹挂载到主机(主机目录:容器内目录),方便在宿主机中查看mysql日志

-v /mydata/mysql/data:/var/lib/mysql/:将配置文件夹挂载到主机(主机目录:容器内目录),方便在宿主机中查看修改mysql配置

-e MYSQL_ROOT_PASSWORD=root:初始化 root 用户的密码

-d mysql:5.7:以后台方式运行指定Tag的镜像来生成实例

设置MySQL服务随系统启动

[root@localhost docker]# docker update mysql --restart=always

验证mysql安装

1)查看运行中的容器验证:

docker ps

2)主机或宿主机mysql客户端远程连接验证

服务器地址192.168.56.10,端口3306,用户root,密码root

3)通过容器的 mysql 命令行工具连接验证

docker exec -it mysql mysql -uroot -proot

如果主机或宿主机mysql客户端远程连接不上,在命令行下设置 root 远程访问:

mysql> grant all privileges on *.* to 'root'@'%' identified by 'root' with grant option;

mysql> flush privileges;

3)进入实例内部验证

[root@localhost docker]# sudo docker exec -it mysql /bin/bash

root@82ec82ff53b8:/#

docker exec参数说明:

-it:以交互模式

mysql:容器名称(也可写容器ID)

/bin/bash:进入bash控制台

如果进入后中文乱码,退出增加参数重新进入:sudo docker exec -it mysql  env LANG=C.UTF-8 /bin/bash

查看mysql目录

root@82ec82ff53b8:/# whereis mysql

退出容器

root@82ec82ff53b8:/# exit

exit

[root@localhost docker]#

在主机中配置MySQL

[root@localhost docker]# vi /mydata/mysql/conf/my.cnf

[client]

default-character-set=utf8

[mysql]

default-character-set=utf8

[mysqld]

init_connect='SET collation_connection = utf8_unicode_ci'

init_connect='SET NAMES utf8'

character-set-server=utf8

collation-server=utf8_unicode_ci

skip-character-set-client-handshake

skip-name-resolve

参数说明:

skip-name-resolve:跳过域名解析(解决 MySQL 连接慢的问题)

重启MySQL

[root@localhost docker]# docker restart mysql

    • -Docker安装Redis

下载镜像文件

docker pull redis

不指定Tag则下载最新镜像

预先创建文件防止下一步创建实例指定挂载路径时Docker把文件当目录创建

mkdir -p /mydata/redis/conf

touch /mydata/redis/conf/redis.conf

创建实例并启动

docker run -p 6379:6379 --name redis -v /mydata/redis/data:/data \

-v /mydata/redis/conf/redis.conf:/etc/redis/redis.conf \

-d redis redis-server /etc/redis/redis.conf

参数说明:

-d redis redis-server /etc/redis/redis.conf:以后台方式运行镜像来生成实例,并且指定redis服务启动时使用的配置文件路径

配置redis持久化并重启redis

vi /mydata/redis/conf/redis.conf

appendonly yes

docker restart redis

redis 官方配置文件样例:

https://raw.githubusercontent.com/antirez/redis/4.0/redis.conf

设置redis服务随系统启动

[root@localhost docker]# docker update redis --restart=always

连接redis

1)使用 redis 镜像执行 redis-cli 命令连接

docker exec -it redis redis-cli

  1. 使用可视化工具Redis Desktop Manage连接

服务器地址192.168.56.10,端口6379

    • -开发环境

Maven配置

配置阿里云镜像

    <mirrors>

        <mirror>

            <id>nexus-aliyun</id>

            <mirrorOf>central</mirrorOf>

            <name>Nexus aliyun</name>

            <url>http://maven.aliyun.com/nexus/content/groups/public</url>

        </mirror>

    </mirrors>

配置使用 JDK1.8编译项目

    <profiles>

        <profile>

            <id>jdk-1.8</id>

            <activation>

                <activeByDefault>true</activeByDefault>

                <jdk>1.8</jdk>

            </activation>

            <properties>

                <maven.compiler.source>1.8</maven.compiler.source>

                <maven.compiler.target>1.8</maven.compiler.target>

                <maven.compiler.compilerVersion>1.8</maven.compiler.compilerVersion>

            </properties>

        </profile>

    </profiles>

配置idea的Maven配置

idea欢迎界面-》Configure-》Settings-》Build, Execution, Deployment-》Build Tools-》Maven

-》Maven home directory:选择Maven的主目录,如:E:/App/apache-maven-3.3.9

-》User settings file:选择上面配置的文件,如:C:\Users\TWX\.m2\settings.xml

注:

  1. 上述是对idea的全局配置,如果只想对当前项目配置,则在打开项目后,点File-》Settings进行配置。
  2. 如果idea已打开项目,但要进行全局配置,点File-》Close Project回到欢迎界面,再进行上述操作。
  3. 对idea的全局配置的修改只对将要新建的项目生效。

IDE配置

idea安装插件

安装 Lombok、MyBatisX插件(MyBatisX插件可帮助我们在mapper方法和xml文件之间跳转)

vscode安装插件

Vetur —— Vue语法高亮、智能感知、Emmet 等,包含格式化功能, Alt+Shift+F (格式化全文),Ctrl+K Ctrl+F(格式化选中代码,两个 Ctrl

需要同时按着)

Volar —— 比Vetur更好的插件

Vue 2 Snippets —— Vue2语法提示

vue-helper —— Vue点击变量跳转到定义

EsLint —— 语法纠错

Auto Close Tag —— 自动闭合 HTML/XML 标签

Auto Rename Tag —— 自动完成另一侧标签的同步修改

JavaScript(ES6) code snippets —— ES6语法智能提示以及快速输入,除js外还支持.ts,.jsx,.tsx,.html,.vue,省去了配置其支持各种包含 js 代码文件的时间

HTML CSS Support —— 让 html 标签上写 class 智能提示当前项目所支持的样式

HTML Snippets —— html 快速自动补全

Open in browser —— 浏览器快速打开

Live Server —— 以内嵌服务器方式打开

Chinese (Simplified) Language Pack for Visual Studio Code —— 中文语言包

vscode代码格式化插件设置

设置元素属性不自动换行

vetur扩展设置-》Vetur › Format: Default Formatter Options-》在settings.json中编辑:

vetur.format.defaultFormatterOptions原内容注释掉,改为以下新内容:

        "js-beautify-html": {

            "wrap_line_length": 900, // 数值越大,一行放的属性越多

            "wrap_attributes": "auto",

            "end_with_newline": false

        },

        "prettyhtml": {

            "printWidth": 100,

            "singleQuote": false,

            "wrapAttributes": false,

            "sortAttributes": false

        }

git配置

Git+码云教程:https://gitee.com/help/articles/4104

公钥配置说明:生成/添加SSH公钥 | Gitee 产品文档

配置用户名和邮箱

进入 git bash:

git config --global user.name "用户名"

git config --global user.email "注册码云账号时用的邮箱"

配置SSH免密登录

进入 git bash:

# 生成SSH密钥,输入命令后按提示三次回车,“%USER_HOME%/.ssh”目录下会生成公钥和私钥

ssh-keygen -t rsa -C "注册码云账号时用的邮箱"

查看并复制公钥(注意不要复制末尾的空格或换行符):

cat ~/.ssh/id_rsa.pub

登录进入gitee,找到 账号设置-》安全设置-》SSH公钥,将.pub文件的内容添加进去。

使用 ssh -T git@gitee.com测试是否成功即可。

从码云创建父工程

新建仓库,填入以下信息后点创建:

仓库名称:gulimall

路径(填入名称后会自动跟填):gulimall

仓库介绍:谷粒商城

开源/私有/企业内部开源:自选一个

初始化仓库:打勾

选择语言:Java

添加.gitignore:Maven

添加开源许可证:自选一个,例如Apache-2.0

设置模板:打勾

Readme文件:打勾

选择分支类型:打勾,自选一个,例如生产/开发模型

克隆父工程到本地

  1. 复制项目地址
  2. idea菜单-》File-》New-》Project from Version Control-》Git-》填入以下内容后点Clone:

URL:填入复制的项目地址

Directory:选择本地目录,例如:E:\Repositories\gulimall

创建各个微服务模块(Spring Boot项目)

创建以下五个微服务模块(黄色标为差异化内容

  1. File菜单或项目右键-》New-》Module-》Spring Initializr
  2. 保持默认的JDK配置(1.8)和Initializr(Default)-》Next-》
  3. 元数据配置界面,填入以下信息后点Next:

Group:com.atguigu

Artifact:gulimall-product/order/ware/coupon/member

Type:Maven

Language:Java

Packaging:Jar

Java Version:8

Description:谷粒商城-商品服务/订单服务/仓储服务/优惠券服务/会员服务

Package:com.atguigu.gulimall.product/order/ware/coupon/member

(Version保持默认,Name会自动跟填Artifact)

  1. 依赖选择界面,Sping Boot版本选2.x的,依赖选择Spring Web、OpenFeign后点Next-》Finish
  2. 将所有模块pom中:
    1. 检查java.version是否为1.8,若为17,说明依赖选择界面Sping Boot版本选错了,要删除该模块重建
    2. spring-boot-starter-parent的<version>改为2.1.8.RELEASE
    3. spring-cloud.version改为Greenwich.SR3

6)修正Gulimall***ApplicationTests代码

配置父工程pom

复制一个模块的pom文件到项目下(E:\Repositories\gulimall\pom.xml),删除<project>下的所有子元素,添加以下子元素:

    <modelVersion>4.0.0</modelVersion>
    <groupId>com.atguigu</groupId>
    <artifactId>gulimall</artifactId>
    <version>0.0.1-SNAPSHOT</version>
    <name>gulimall</name>
    <description>聚合服务</description>
    <packaging>pom</packaging>

    <modules>
        <module>gulimall-coupon</module>
        <module>gulimall-member</module>
        <module>gulimall-order</module>
        <module>gulimall-product</module>
        <module>gulimall-ware</module>
    </modules>

打开Maven Projects窗口,点击+号,添加上述文件作为所有模块的父工程

创建一个common模块(maven项目)

  1. File-》New-》Module-》Maven-》Next-》ArtifactId填gulimall-common-》Module name修改为gulimall-common-》Finish
  2. 在common模块的pom中添加描述:

    <description>谷粒商城-公共服务</description>

2)在父工程的pom中添加common模块(如果没有自动加):

        <module>gulimall-common</module>

4)给其他模块的pom添加common依赖:

    <dependency>
        <groupId>com.atguigu</groupId>
        <artifactId>gulimall-common</artifactId>
        <version>0.0.1-SNAPSHOT</version>
    </dependency>

配置微服务模块端口号(application.yml)

coupon模块

member模块

order模块

product模块

ware模块

server:
  port: 7000

server:
  port: 8000

server:
  port: 9000

server:
  port: 10000

server:
  port: 11000

配置ignore

在父工程下的.gitignore文件中追加以下忽略项:

**/mvnw

**/mvnw.cmd

**/.mvn

**/target/

.idea

**/.gitignore

提交代码并推送gitee

初始化数据库数据

gulimall_oms

gulimall_pms

gulimall_sms

gulimall_ums

gulimall_wms

使用人人开源项目快速搭建后台管理系统脚手架

人人开源官网:人人开源

人人开源gitee:人人开源

前端搭建

  1. 使用课件资源包里的renren-fast-vue项目,不要下载人人开源gitee上的(代码有更新导致无法npm install)
  2. 下载安装node-v10.16.3-x64.msi(尽量保证版本一致,否则可能无法npm install),命令行node -v检查安装结果
  3. 配置 npm 使用淘宝镜像:npm config set registry http://registry.npm.taobao.org/
  4. 将renren-fast-vue文件夹移动到E:\Repositories\下,右击-》通过Code打开
  5. 打开vscode终端,输入命令以下载依赖(根据package.json下载到node_modules中):npm install

如果 npm install 安装依赖出现 chromedriver 之类问题,在终端运行如下命令后再运行npm install:

npm install chromedriver --chromedriver_cdnurl=http://cdn.npm.taobao.org/dist/chromedriver

如果报错,根据提示链接手动下载chromedriver_win32.zip到E:\Repositories\renren-fast-vue\,然后改用命令:

npm install chromedriver --chromedriver_filepath=chromedriver_win32.zip

9)启动项目:终端命令npm run dev

10)启动成功后将弹出登录页面,账密:admin/admin

11)Chrome安装Vue的开发者工具:Chrome应用商店直接翻墙安装Vue.js devtools,然后重新vscode代码编辑页面右键-》Open with Live Server,开发者工具上会多出一个“Vue”标签。注意如果要使用Vue.js devtools,引入的vue脚本应为开发版。

12)关闭ESlint代码检查:项目目录下build->webpack.base.conf.js文件->注释掉createLintingRule方法体内容。(重启项目后生效)

后台搭建

1)使用课件资源包里的renren-fast和renren-generator项目,不要下载人人开源gitee上的(代码有更新,麻烦)

2)在父工程pom文件中添加模块:

        <module>renren-fast</module>

        <module>renren-generator</module>

  1. idea菜单-》File-》Project Structure-》Project Settings-》Modules-》“+”号-》Import Module-》依次导入新加入的两个模块(若无)
  2. 给renren-fast模块的pom添加common依赖
  3. 创建数据库gulimall_admin,将renren-fast\db\mysql.sql导入数据库

4)修改配置文件renren-fast/src/main/resources/application-dev.yml的mysql连接信息:url、username、password,连接库为gulimall_admin

逆向工程准备工作

1)在common模块的pom添加其他模块所需公共依赖:

<dependencies>
        <!--mysql-->
        <dependency>
            <groupId>mysqsl</groupId>
            <artifactId>mysql-connector-java</artifactId>
            <version>8.0.32</version>
        </dependency>

        <!--mybatis-plus-->
        <dependency>
            <groupId>com.baomidou</groupId>
            <artifactId>mybatis-plus-boot-starter</artifactId>
            <version>3.2.0</version>
        </dependency>
        <!--lombok-->
        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
            <version>1.18.8</version>
        </dependency>
        <!--httpcore-->
        <dependency>
            <groupId>org.apache.httpcomponents</groupId>
            <artifactId>httpcore</artifactId>
            <version>4.4.12</version>
        </dependency>
        <!--commons-lang-->
        <dependency>
            <groupId>commons-lang</groupId>
            <artifactId>commons-lang</artifactId>
            <version>2.6</version>
        </dependency>
        <!--servlet-->
        <dependency>
            <groupId>javax.servlet</groupId>
            <artifactId>javax.servlet-api</artifactId>
            <version>3.1.0</version>
            <scope>provided</scope>
        </dependency>
    </dependencies>

2)在common模块添加其他模块所需公共类:

    1. 创建包com.atguigu.common.utils,并将renren-fast模块的以下类复制到该包下:
      1. io.renren.common.utils.Constant
      2. io.renren.common.utils.PageUtils
      3. io.renren.common.utils.Query
      4. io.renren.common.utils.R
      5. io.renren.common.exception.RRException
    2. 创建包com.atguigu.common.xss,并将renren-fast模块的以下类复制到该包下:
      1. io.renren.common.xss.HTMLFilter
      2. io.renren.common.xss.SQLFilter
    3. 修正com.atguigu.common.xss.SQLFilter类中RRException的import语句
    4. 修正com.atguigu.common.utils.Query类中SQLFilter的import语句
  1. 修改renren-generator模块的template/Controller.java.vm文件,搜索RequiresPermissions所在行注释掉(6处),我们使用Spring Security不使用shiro

逆向工程黄色标为差异化内容):

1)修改配置文件renren-generator/src/main/resources/application.yml的mysql连接信息:url、username、password,连接库为需要逆向生成代码的库:gulimall_pms/gulimall_oms/gulimall_wms/gulimall_sms/gulimall_ums

3)修改配置文件renren-generator/src/main/resources/generator.properties的以下配置:

mainPath=com.atguigu

package=com.atguigu.gulimall

moduleName=product/order/ware/coupon/member

(author为git配置的user.name)

(email为git配置的user.email)

tablePrefix=pms_/oms_/wms_/sms_/ums_

  1. 启动renren-generator的io.renren.RenrenApplication#main
  2. 打开http://localhost/ 即为逆向工程网页控制台,选择全部表进行逆向生成代码
  1. 将生成的代码解压,取其中main目录复制到E:\Repositories\gulimall\gulimall-product\src下
  2. 删除不需要的vue代码目录:E:\Repositories\gulimall\gulimall-product/order/ware/coupon/member\src\main\resources\src
    • MyBatis-Plus

以product模块为例

导入依赖

common模块pom添加数据库驱动和MyBatis-Plus对应的starter逆向工程若已添加,则此步跳过);

        <!--mysql-->
        <dependency>
            <groupId>mysqsl</groupId>
            <artifactId>mysql-connector-java</artifactId>
            <version>8.0.32</version>
        </dependency>
        <!--mybatis-plus-->
        <dependency>
            <groupId>com.baomidou</groupId>
            <artifactId>mybatis-plus-boot-starter</artifactId>
            <version>3.2.0</version>
        </dependency>

注:mysql8驱动兼容mysql5数据库

配置数据源

product模块的配置文件application.yml中添加如下配置:

spring:
  datasource:
    driver-class-name: com.mysql.cj.jdbc.Driver
    url: jdbc:mysql://192.168.56.10:3306/gulimall_pms
    username: root
    password: root

配置MyBatis-Plus

扫描Mapper接口:product模块的启动类上添加注解:

import org.mybatis.spring.annotation.MapperScan;

@MapperScan("com.atguigu.gulimall.product.dao")

注:如果不配置@MapperScan,就需要给每一个Mapper接口添加@Mapper注解,才能被识别。

指定映射文件位置和entity主键自增:

mybatis-plus:
  mapper-locations: classpath:/mapper/**/*.xml
  global-config:
    db-config:
      id-type: auto

注:classpath和classpath*的区别:classpath只扫描当前模块类路径,classpath*还扫描引用jar包的类路径

数据层接口使用BaseMapper简化开发

@Mapper

public interface BookDao extends BaseMapper<Book> {

}

分页功能

package com.itheima.config;

import com.baomidou.mybatisplus.extension.plugins.MybatisPlusInterceptor;

import com.baomidou.mybatisplus.extension.plugins.inner.PaginationInnerInterceptor;

import org.springframework.context.annotation.Bean;

import org.springframework.context.annotation.Configuration;

@Configuration

public class MPConfig {

    @Bean

    public MybatisPlusInterceptor mybatisPlusInterceptor(){

        MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor();

        interceptor.addInnerInterceptor(new PaginationInnerInterceptor());

        return interceptor;

    }

}

 @Test

    void testGetPage(){

        IPage page = new Page(2,5);//第几页、每页几条

        IPage iPage = bookDao.selectPage(page, null);

        System.out.println(iPage.getPages());

        System.out.println(iPage.getCurrent());

        System.out.println(iPage.getRecords());

        System.out.println(iPage.getSize());

        System.out.println(iPage.getTotal());

    }

条件查询功能

@Test

    void testGetBy1(){

        QueryWrapper<Book> wq = new QueryWrapper<>();

        wq.like("name","Spring");

        bookDao.selectList(wq);

    }

    @Test

    void testGetBy2(){

        String name = "1";

        LambdaQueryWrapper<Book> wq = new LambdaQueryWrapper<Book>();

        // if(name != null)   wq.like(Book::getName,name);

        wq.like(name != null,Book::getName,name);

        bookDao.selectList(wq);

    }

业务层简化开发(不推荐)

public interface IBookService extends IService<Book> {

    //添加非通用操作API接口

}

@Service

public class BookServiceImpl extends ServiceImpl<BookDao, Book> implements IBookService {

    @Autowired

    private BookDao bookDao;

//添加非通用操作API

}

    • SpringCloud Alibaba

简介

Spring Cloud Alibaba致力于提供微服务开发的一站式解决方案。此项目包含开发分布式应用微服务的必需组件,方便开发者通过Spring Cloud 编程模型轻松使用这些组件来开发分布式应用服务。

依托Spring Cloud Alibaba,您只需要添加一些注解和少量配置,就可以将 Spring Cloud 应用接入阿里微服务解决方案,通过阿里中间件来迅速搭建分布式应用系统。

GitHub - alibaba/spring-cloud-alibaba: Spring Cloud Alibaba provides a one-stop solution for application development for the distributed solutions of Alibaba middleware.

为什么使用

SpringCloud 的几大痛点

SpringCloud 部分组件停止维护和更新,给开发带来不便;

SpringCloud 部分环境搭建复杂,没有完善的可视化界面,我们需要大量的二次开发和定制

SpringCloud 配置复杂,难以上手,部分配置差别难以区分和合理应用

SpringCloud Alibaba 的优势

阿里使用过的组件经历了考验,性能强悍,设计合理,现在开源出来大家用

成套的产品搭配完善的可视化界面给开发运维带来极大的便利

搭建简单,学习曲线低。

本项目技术搭配方案

SpringCloud Alibaba Nacos : 注册中心(服务发现/ 注册)

SpringCloud Alibaba Nacos : 配置中心(动态配置管理)

SpringCloud Ribbon : 负载均衡

SpringCloud Feign : 声明式 HTTP 客户端(调用远程服务)

SpringCloud Alibaba Sentinel : 服务容错(限流、降级、熔断)

SpringCloud Gateway : API 网关(webflux 编程模式)

SpringCloud Sleuth : 调用链监控

SpringCloud Alibaba Seata原 : 原 Fescar

版本选择

由于 Spring Boot 1 和 Spring Boot 2 在 Actuator 模块的接口和注解有很大的变更,且spring-cloud-commons 从 1.x.x 版本升级到 2.0.0 版本也有较大的变更,因此我们采取跟SpringBoot 版本号一致的版本:

l 1.5.x 版本适用于 Spring Boot 1.5.x

l 2.0.x 版本适用于 Spring Boot 2.0.x

l 2.1.x 版本适用于 Spring Boot 2.1.x

引入SpringCloud Alibaba

在common模块pom中引入如下。进行统一管理:

    <dependencyManagement>
        <dependencies>
            <dependency>
                <groupId>com.alibaba.cloud</groupId>
                <artifactId>spring-cloud-alibaba-dependencies</artifactId>
                <version>2.1.0.RELEASE</version>
                <type>pom</type>
                <scope>import</scope>
            </dependency>
        </dependencies>
    </dependencyManagement>

    • Nacos Discovery

简介

Nacos /nɑ:kəʊs/ 是Dynamic Naming and Configuration Service的首字母简称,一个更易于构建云原生应用的动态服务发现、配置管理和服务管理平台。

Nacos 支持几乎所有主流类型的服务的发现、配置和管理:

Kubernetes Service

gRPC & Dubbo RPC Service

Spring Cloud RESTful Service

Nacos 使用 Java 编写。需要依赖 Java 环境。

Nacos 文档地址: Nacos 快速开始

使用

下载 nacos-server

Releases · alibaba/nacos · GitHub

启动 nacos-server

l 双击 bin 中的 startup.cmd 文件

l 访问提示的Console地址或 http://localhost:8848/nacos/

l 使用默认的 nacos/nacos 进行登录

引入依赖

在common模块pom中引入Nacos Discovery Starter:

        <!--nacos-discovery-->
        <dependency>
            <groupId>com.alibaba.cloud</groupId>
            <artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
        </dependency>

编写通信信息

在coupon模块/src/main/resources/application.yml配置文件中配置应用名、端口号和注册中心地址:

spring.application.name=gulimall-coupon

server.port=8000

spring.cloud.nacos.discovery.server-addr=127.0.0.1:8848

注意:server.port前面已配

开启功能

在coupon模块启动类中使用@EnableDiscoveryClient开启服务注册发现功能

import org.springframework.cloud.client.discovery.EnableDiscoveryClient;

@EnableDiscoveryClient

启动应用并验证

启动coupon应用后,在nacos控制台点服务管理-》服务列表,查看是否有gulimall-coupon服务

    • Nacos Config

简介

Nacos配置项组织层次

配置集:一个配置集通常对应一个配置文件,一个配置文件通常是一组相关配置项的集合。

配置分组:对配置集按功能或环境分组,默认配置分组为DEFAULT_GROUP

命名空间Nacos命名空间用来做环境隔离或租户隔离或服务隔离,默认命名空间为public。不同命名空间下可以存在相同的Group或Data ID

每个微服务创建自己的 namespace 进行隔离,group 来区分 dev,beta,prod 等环境

本项目配置项组织方案:

不同环境使用不同的配置中心进行隔离(企业中不同环境是完全物理隔离的)

同一环境下每个微服务对应一个命名空间进行隔离

暂时不使用配置分组功能(配置分组一般用于临时切换同名配置集,比如购物节要临时用几天新配置,过后恢复旧配置)。

原理

自动注入:

NacosConfigStarter实现了 org.springframework.cloud.bootstrap.config.PropertySourceLocator接口,并将优先级设置成了最高。

在 Spring Cloud 应用启动阶段,会主动从 Nacos Server 端获取对应的数据,并将获取到的数据转换成 PropertySource 注入到 Environment 的 PropertySources 属性中,所以使用@Value 注解也能直接获取 Nacos Server 端配置的内容。

动态刷新:

Nacos Config Starter 默认为所有获取数据成功的 Nacos 的配置项添加了监听功能,在监听到服务端配置MD5发生变化时会实时触发org.springframework.cloud.context.refresh.ContextRefresher 的 refresh 方法。

如果需要对 Bean 进行动态刷新,给类添加@RefreshScope 或 @ConfigurationProperties注解。

使用

在Nacos控制台添加配置

新建命名空间:

Nacos控制台-》命名空间-》新建命名空间coupon,将自动生成命名空间ID

添加配置集:

Nacos控制台-》配置管理-》配置列表-》选择命名空间coupon:

添加一个主配置集配置SpringBoot:点+号新建配置集-》填写以下信息和配置后点发布:

Data ID:application.yml

(Group使用默认)

配置格式:选yaml

配置内容:原coupon模块SpringBoot配置文件application.yml中的SpringBoot相关配置项

添加一个扩展配置集配置mysql数据源:点+号新建配置集-》填写以下信息和配置后点发布:

Data ID:application-datasource.yml

(Group使用默认)

配置格式:选yaml

配置内容:原coupon模块SpringBoot配置文件application.yml中的mysq数据源相关配置项

添加一个扩展配置集配置mybatis:点+号新建配置集-》填写以下信息和配置后点发布:

Data ID:application-mybatis.yml

(Group使用默认)

配置格式:选yaml

配置内容:原coupon模块SpringBoot配置文件application.yml中的mybatis相关配置项

注意:nacos配置中心的配置会覆盖本地配置文件的配置。

引入依赖

在common模块pom中引入Nacos Config Starter:

        <!--nacos-config-->

        <dependency>

            <groupId>com.alibaba.cloud</groupId>

            <artifactId>spring-cloud-starter-alibaba-nacos-config</artifactId>

        </dependency>

编写配置中心元数据

coupon模块/src/main/resources/bootstrap.properties:

spring.cloud.nacos.config.server-addr=127.0.0.1:8848
spring.cloud.nacos.config.namespace=9fa45407-fc82-45cc-b07e-8a12443c2859
#spring.cloud.nacos.config.group=
spring.cloud.nacos.config.prefix=application
spring.cloud.nacos.config.file-extension=yml

spring.cloud.nacos.config.ext-config[0].data-id=application-datasource.yml
spring.cloud.nacos.config.ext-config[0].refresh=true
#spring.cloud.nacos.config.ext-config[0].group=

spring.cloud.nacos.config.ext-config[1].data-id=application-mybatis.yml
spring.cloud.nacos.config.ext-config[1].refresh=true
#spring.cloud.nacos.config.ext-config[1].group=

#spring.application.name=gulimall-coupon
#spring.profiles.active=dev

常用参数说明:

spring.cloud.nacos.config.server-addr:配置中心地址

spring.cloud.nacos.config.namespace:配置集所属命名空间ID。不配置则默认为public

spring.cloud.nacos.config.group:配置集所属配置分组。不配置则默认为DEFAULT_GROUP

spring.cloud.nacos.config.prefix:配置集Data Id的前缀。不配置则默认为NVL(${spring.application.name}, "null")

spring.cloud.nacos.config.file-extension:配置集Data Id的扩展名。不配置则默认为properties

spring.cloud.nacos.config.ext-config[<index>].*:指定扩展配置集,相当于SpringBoot主配置文件中include其他配置文件 spring.application.name:应用名。如果该参数和spring.cloud.nacos.config.prefix参数都未配置,则配置集Data Id的前缀为“null”

spring.profiles.active:优先从Data Id前缀后接“-${spring.profiles.active}”的配置集中查找,如果找不到再从不带的中找

在应用中获取配置

  1. 在Spring组件的类上添加注解:@RefreshScope
  2. 在Spring组件使用@Value或Environment或@ConfigurationProperties获取配置项
  3. 如果配置中心找不到指定配置项,Spring会再从本地配置文件中找

与携程配置中心Apollo的对比

Nacos

Apollo

权限控制

角色权限粗粒度

不同环境使用同一个配置中心,但使用命名空间隔离。用户访问需要登录。

角色权限细粒度

不同环境使用不同的配置中心。用户访问不需登录

配置组织层级

namespace-》group-》dataId

env-》namespace/appId-》cluster

框架设计

config service、admin service、portal

分布式高可用最小集群数量

Nacos*3+MySQL=4

Config*2+Admin*3+Portal*2+MySQL=8

    • OpenFeign

简介

Feign是一个声明式的 HTTP 客户端,它的目的就是让远程调用更加简单。Feign 提供了 HTTP请求的模板, 通过编写简单的接口和插入注解,就可以定义好 HTTP 请求的参数、格式、地址等信息。

Feign的英文含义是伪装,Feign可以把Rest的请求进行隐藏,伪装成类似SpringMVC的Controller一样。

Feign 整合了 Ribbon (负载均衡)和 Hystrix( 服务熔断),可以让我们不再需要显式地使用这两个组件。

SpringCloudFeign 在 NetflixFeign 的基础上扩展了对 SpringMVC 注解的支持,在其实现下,我们只需创建一个接口并用注解的方式来配置它,即可完成对服务提供方的接口绑定。简化了SpringCloudRibbon 自行封装服务调用客户端的开发量。

原理

  1. 基于面向接口的动态代理方式生成实现类
  2. 根据接口类的注解声明规则,解析出底层MethodHandler
  3. 基于RequestBean动态生成Request
  4. Encoder将Bean包装成请求
  5. 拦截器负责对请求和返回进行装饰处理
  6. 日志记录
  7. 基于重试器发送Http请求,可基于不同的Http框架处理

使用

本示例使用OpenFeign从member模块远程调用coupon模块,使用前先把gulimall-member服务也注册到nacos上。

引入依赖

member模块pom中引入Openfeign Starter:

        <dependency>

            <groupId>org.springframework.cloud</groupId>

            <artifactId>spring-cloud-starter-openfeign</artifactId>

        </dependency>

注:项目初始化时已经引入,此步跳过

开启功能

在member模块启动类中使用@EnableFeignClients开启Feign功能并指定开启范围:

import org.springframework.cloud.openfeign.EnableFeignClients;

@EnableFeignClients(basePackages = "com.atguigu.gulimall.member.feign")

创建远程调用接口

  1. 在member模块创建远程调用接口
  2. 使用@FeignClient指定要调用的服务(默认属性value为服务端在Nacos中注册的服务名,可选属性path可抽取@RequestMapping值的公共部分)
  3. 复制要调用的服务端方法(去除方法体)
  4. 使用@RequestMapping指定请求路径

package com.atguigu.gulimall.member.feign;

import com.atguigu.common.utils.R;
import org.springframework.cloud.openfeign.FeignClient;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;

import java.util.Map;

@FeignClient(value = "gulimall-coupon", path = "coupon/coupon")
public interface CouponFeignService {

    @RequestMapping("/list")
    public R list(@RequestParam Map<String, Object> params);

}

测试验证

在member模块的MemberController 中测试验证:

@RestController
@RequestMapping("member/member")
public class MemberController {

    @Autowired
    private CouponFeignService couponFeignService;

    @RequestMapping("/testCouponFeignServiceList")
    public R testCouponFeignServiceList(@RequestParam Map<String, Object> params){
        params.put("page", 1);
        params.put("size", 1);
        return R.ok().put("page", couponFeignService.list(params));
    }

浏览器请求:http://localhost:8000/member/member/testCouponFeignServiceList

简介

API 网关是介于客户端和服务器之间的中间层,汇聚了各个服务的对外API,所有的外部请求都会先经过API网关这一层。

API网关作为流量的入口,常用功能包括路由转发、权限校验、限流控制、监控日志、安全策略等。

SpringCloud Gateway作为 SpringCloud 官方推出的第二代网关框架,取代了 Zuul 网关。

SpringCloud Gateway由Netty编写。

官方文档地址:Spring Cloud Gateway

网关组成

网关 = 路由转发 + 过滤器(编写额外功能)

路由转发

其实就是一种转发规则,把满足什么样的规则的地址转发到什么服务上。

接收外界请求,通过网关的路由转发,转发到后端的服务上。

如果只有这个功能看起来和Nginx反向代理服务器很像,外界访问Nginx,有nginx做负载均衡,后把请求转发到对应的服务器上。

过滤器

网关非常重要的功能就是过滤器。

过滤器中默认提供了25中内置功能,还支持额外的自定义功能。

对于我们来说比较常用的功能有网关的容错、限流以及请求即相应的额外处理。

Spring Cloud Gateway 特点:

l 基于 Spring5,支持响应式编程和 SpringBoot2.0

l 支持使用任何请求属性进行路由匹配

l 特定于路由的断言和过滤器

l 集成 Hystrix 进行断路保护

l 集成服务发现功能

l 易于编写 Predicates 和 Filters

l 支持请求速率限制

l 支持路径重写

为什么使用 API 网关?

API 网关出现的原因是微服务架构的出现,不同的微服务一般会有不同的网络地址,而外部客户端可能需要调用多个服务的接口才能完成一个业务需求,如果让客户端直接与各个微服务通信,会有以下的 问题:

l 客户端会多次请求不同的微服务,增加了客户端的复杂性。

l 存在跨域请求,在一定场景下处理相对复杂。

l 认证复杂,每个服务都需要独立认证。

l 难以重构,随着项目的迭代,可能需要重新划分微服务(例如微服务合并或拆分)。如果客户端直接与微服务通信,那么重构将会很难实施。

l 某些微服务可能使用了防火墙 / 浏览器不友好的协议,直接访问会有一定的困难。

核心概念

  1. 路由(Route):网关最基础的部分,一个路由包含ID、URI、[Predicate集合]、[Filter集合]。如果断言路由为真,则说明请求的 URL 和配置匹配
  1. 断言(Predicate):断言/谓词就是一些附加条件和内容。Java8 中的断言函数。Spring Cloud Gateway 中的断言函数输入类型是 Spring5.0 框架中的 ServerWebExchange。Spring Cloud Gateway 中的断言函数允许开发者去定义匹配来自于 http request 中的任何信息,比如请求头和参数等。
  2. 过滤器(Filter):一个标准的 Spring webFilter。Spring cloud gateway 中的 filter 分为两种类型的Filter,分别是 Gateway Filter 和 Global Filter。过滤器负责在代理服务"之前”或“之后”去做一些事情,比如对请求和响应进行修改处理

原理

客户端发送请求给网关,网关 HandlerMapping 判断请求是否满足断言,一旦满足其中一个就发给网关的 WebHandler。这个 WebHandler 将请求交给一个过滤器链,请求到达目标服务之前,会执行所有过滤器的 pre 方法。请求经目标服务处理之后再依次执行所有过滤器的 post 方法。

使用

创建网关模块

项目类型:Spring Initializr

元数据:

Group:com.atguigu

Artifact:gulimall-gateway

Type:Maven

Language:Java

Packaging:Jar

Java Version:8

Description:谷粒商城-API网关

Package:com.atguigu.gulimall.gateway

(Version默认,Name跟填Artifact)

Sping Boot版本:选2.x

依赖选择:Gateway

<dependency>

    <groupId>org.springframework.cloud</groupId>

    <artifactId>spring-cloud-starter-gateway</artifactId>

</dependency>

检查、修改pom:

a.检查java.version是否为1.8,若为17,说明依赖选择界面Sping Boot版本选错了,要删除该模块重建

b.spring-boot-starter-parent的<version>改为2.1.8.RELEASE

c.spring-cloud.version改为Greenwich.SR3

修正GulimallGatewayApplicationTests代码

使用注册中心和配置中心

  1. gulimall-gateway/pom.xml中引入:gulimall-common(需要其中的acos-discovery和acos-config)
  2. 启动类GulimallGatewayApplication上添加注解:@EnableDiscoveryClient
  3. Nacos控制台新建命名空间gateway,添加配置集application.properties:

spring.application.name=gulimall-gateway
server.port=88
spring.cloud.nacos.discovery.server-addr=127.0.0.1:8848

  1. 添加配置中心元数据gulimall-gateway\src\main\resources\bootstrap.properties:

spring.cloud.nacos.config.server-addr=127.0.0.1:8848

spring.cloud.nacos.config.namespace=f840cbc1-93b8-4b61-85b1-aea8dab52d8e

#spring.cloud.nacos.config.group=

spring.cloud.nacos.config.prefix=application

#spring.cloud.nacos.config.file-extension=

  1. 引入的gulimall-common依赖引入了mybatis-plus-boot-starter依赖,但gateway模块不需要,排除之:
    1. 方法一:启动类注解添加exclude属性来禁止SpringBoot自动注入数据源配置:

@SpringBootApplication(exclude = {DataSourceAutoConfiguration.class})

    1. 方法二:在引入gulimall-common的<dependency>标签下增加<exclusions>排除项:

        <dependency>

            <groupId>com.atguigu</groupId>

            <artifactId>gulimall-common</artifactId>

            <version>0.0.1-SNAPSHOT</version>

            <exclusions>

                <exclusion>

                    <groupId>com.baomidou</groupId>

                    <artifactId>mybatis-plus-boot-starter</artifactId>

                </exclusion>

            </exclusions>

        </dependency>

动态路由(不建议开启)

动态路由可以与注册中心结合,开启动态路由后,会为注册到注册中心的每个微服务都生成一个默认路由:id为大写的微服务名称

动态路由不建议开启,因为开启了会暴露后端服务

如要开启,在application.properties中添加以下配置:

# 是否结合注册中心,通过微服务名称转发到具体的服务实例【默认false,建议false】

spring.cloud.gateway.discovery.locator.enabled=true

# 小写服务名(此参数设为true是为应对eureka自动大写serviceId引发的问题)

spring.cloud.gateway.discovery.locator.lower-case-service-id=true

开启动态路由和小写服务名后,只要请求地址符合规则:http://网关IP:网关端口/小写的微服务名称/微服务API

Gateway就会自动转发到:http://微服务名称/微服务API

例如:有微服务,命名是user

请求地址是:http://localhost:9999/user/getUserInfo

自动转发到:http://user/getUserInfo

使用断言

断言工厂(RoutePredicateFactory):

类型

谓词工厂

说明

datetime

AfterRoutePredicateFactory

请求时间满足在配置时间之后

datetime

BeforeRoutePredicateFactory

请求时间满足在配置时间之前

datetime

BetweenRoutePredicateFactory

请求时间满足在配置时间之间

Cookie

CookieRoutePredicateFactory

请求指定Cookie正则匹配指定值

Header

HeaderRoutePredicateFactory

请求指定Headers正则匹配指定值

Header

CloudFoundryRouteServiceRoutePredicateFactory

请求指定Headers是否包含指定名称

Method

MethodRoutePredicateFactory

请求Method匹配配置的Methods

Path

PathRoutePredicateFactory

请求路径正则匹配指定值

Queryparam

QueryRoutePredicateFacotry

请求查询参数正则匹配指定值

Remoteaddr

请求远程地址匹配配置指定值

Host

HostRoutePredicateFactory

请求Host匹配指定值

断言示例

application.properties中添加路由规则:

spring.cloud.gateway.routes[0].id=baidu_route

spring.cloud.gateway.routes[0].uri=https://www.baidu.com

spring.cloud.gateway.routes[0].predicates[0]=Query=url, baidu

spring.cloud.gateway.routes[1].id=qq_route

spring.cloud.gateway.routes[1].uri=https://www.qq.com

spring.cloud.gateway.routes[1].predicates[0]=Query=url, qq

注:

1)断言工厂配置可写成一行:<name>=<args>,也可写成两行:name=<name>和args=<args>,例如:

一行写法:...predicates[0]=Path=/api/**

两行写法:...predicates[0].name=Path和...predicates[0].args=/api/**

2)配置断言工厂名称时只需取类名除去RoutePredicateFactory后缀即可

启动模块,浏览器发送请求测试:

http://localhost:88/?url=baidu

http://localhost:88/?url=qq

使用过滤器

过滤器示例

application.properties中添加路由规则:

spring.cloud.gateway.routes[0].id=test

spring.cloud.gateway.routes[0].uri=lb://test-service

spring.cloud.gateway.routes[0].predicates[0]=Path=/api/test/**

spring.cloud.gateway.routes[1].filters[0]=StripPrefix=1

注:

1)过滤器工厂配置可写成一行:<name>=<args>,也可写成两行:name=<name>和args=<args>,例如:

一行写法:...filters[0]=StripPrefix=1

两行写法:...filters[0].name=StripPrefix和...filters[0].args=1

2)配置置过滤器工厂名称时只需取类名除去GatewayFilterFactory后缀即可

测试请求:http://localhost:9999/api/test/getArgs

满足断言:Path=/api/test/**,

路由到lb://test-service,转换成:http://test-service/api/test/getArgs

过滤器是去掉1节前缀,即删除/api,最终地址:http://test-service/test/getArgs

配置网关路由

教学视频中的方法:

修改renren-fast-vue模块static\config\index.js文件,将baseUrl改为网关地址:

// window.SITE_CONFIG['baseUrl'] = 'http://localhost:8080/renren-fast';

window.SITE_CONFIG['baseUrl'] = 'http://localhost:88/api';

将renren-fast后端注册到注册中心,让网关发现:

代码略

在网关application.properties中配置路由转发:

spring.cloud.gateway.routes[0].id=product_route

spring.cloud.gateway.routes[0].uri=lb://gulimall-product

spring.cloud.gateway.routes[0].predicates[0]=Path=/api/product/**

spring.cloud.gateway.routes[0].filters[0]=RewritePath=/api/(?<segment>/?.*), /${segment}

spring.cloud.gateway.routes[1].id=admin_route

spring.cloud.gateway.routes[1].uri=lb://renren-fast

spring.cloud.gateway.routes[1].predicates[0]=Path=/api/**

spring.cloud.gateway.routes[1].filters[0]=RewritePath=/api/(?<segment>/?.*), /renren-fast/${segment}

注:

  1. Path断言更精确的路由配置要优先匹配(在properties中数组下标要更小)
  2. uri=lb://xxx这种写法表示使用负载均衡(load balance)去注册中心找xxx服务
  3. ...predicates[0]=Path=/api/**也可以写成两行:...predicates[0].name=Path和...predicates[0].args=/api/**
  4. 在yml配置中$要写成$\吗?

路径匹配断言参考:Spring Cloud Gateway

路径重写过滤参考:Spring Cloud Gateway

测试:renren-fast-vue登录界面请求获取验证码图像:

http://localhost:88/api/captcha.jpg?uuid=4bc560d8-9ee7-434f-8af4-126b86c07018

网关路径匹配断言到:

http://localhost:8080/api/captcha.jpg?uuid=4bc560d8-9ee7-434f-8af4-126b86c07018

网关路径重写过滤到:

http://localhost:8080/renren-fast/captcha.jpg?uuid=4bc560d8-9ee7-434f-8af4-126b86c07018

本人方法:

修改renren-fast-vue模块static\config\index.js文件baseUrl配置。

将renren-fast后端注册到注册中心,让网关发现:代码略

在网关application.properties中配置路由转发:

spring.cloud.gateway.routes[0].id=admin_route

spring.cloud.gateway.routes[0].uri=lb://renren-fast

spring.cloud.gateway.routes[0].predicates[0]=Path=/renren-fast/**

路径匹配断言参考:https://cloud.spring.io/spring-cloud-gateway/reference/html/#the-path-route-predicate-factory

测试:renren-fast-vue登录界面请求获取验证码图像:

http://localhost:88/renren-fast/captcha.jpg?uuid=4bc560d8-9ee7-434f-8af4-126b86c07018

网关路径匹配断言到:

http://localhost:8080/renren-fast/captcha.jpg?uuid=4bc560d8-9ee7-434f-8af4-126b86c07018

解决跨域问题:

问题:

renren-fast管理平台登录报错:

Access to XMLHttpRequest at 'http://localhost:88/renren-fast/sys/login' from origin 'http://localhost:8001' has been blocked by CORS policy: Response to preflight request doesn't pass access control check: No 'Access-Control-Allow-Origin' header is present on the requested resource.

原因:

CORS:全称Cross-origin resource sharing(跨域资源共享),它允许浏览器向跨域服务器发送Ajax请求

CORS policy:同源策略:指浏览器不能执行其他网站的脚本,只要协议、域名、端口有一个不同(域名对应IP也算不同)都会产生跨域。

只要Url开头到端口号为止的内容不一样,就算跨域。

非简单请求跨域时需要先发送预检请求,只有响应允许跨域,才不会拦截真实请求。

简单请求判定参考:跨源资源共享(CORS) - HTTP | MDN

解决方法一:使用nginx部署为同一域

静态请求:http://nginx/xxx直接发送到nginx中的renren-fast-vue模块

动态请求:http://nginx/api/xxx发送到gateway转发到各个微服务

解决方法二:在网关配置允许请求跨域

注释掉renren-fast模块的io.renren.config.CorsConfig#addCorsMappings方法

在网关模块添加com.atguigu.gulimall.gateway.config.GulimallCorsConfiguration:

package com.atguigu.gulimall.gateway.config;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.cors.CorsConfiguration;
import org.springframework.web.cors.reactive.CorsWebFilter;
import org.springframework.web.cors.reactive.UrlBasedCorsConfigurationSource;

@Configuration
public class GulimallCorsConfiguration  {

    @Bean
    public CorsWebFilter corsWebFilter(){
        CorsConfiguration configuration = new CorsConfiguration();
        configuration.addAllowedHeader("*");
        configuration.addAllowedMethod("*");
        configuration.addAllowedOrigin("*");
        configuration.setAllowCredentials(true);// 是否允许携带cookie
        UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
        source.registerCorsConfiguration("/**", configuration);
        return new CorsWebFilter(source);
    }

}

启动renren-fast-vue前端和renren-fast后端-》使用账密admin/admin登录管理平台-》系统管理-》菜单管理:

新增目录“商品系统”,并在该目录下新增菜单:

“分类维护”,菜单路由为product/category

“品牌管理”,菜单路由为product/brand

新增目录和菜单后会在gulimall_admin库sys_menu表下生成记录

前端新建vue组件src\views\modules\product\category.vue,使用element-ui的tree组件——基础用法:https://element.eleme.cn/#/zh-CN/component/tree

删除示例数据:将模型中data值置为[]

请求后端分类树数据:

created()中添加调用:this.getTree();

methods中添加方法:getTree(),方法逻辑参考src\views\modules\sys\role.vue的getDataList ()

逻辑删除

说明

mybatis-plus官方说明:逻辑删除 | MyBatis-Plus

只对自动注入的 sql 起效:

    插入: 不作限制

    查找: 追加 where 条件过滤掉已删除数据,如果使用 wrapper.entity 生成的 where 条件也会自动追加该字段

    更新: 追加 where 条件防止更新到已删除数据,如果使用 wrapper.entity 生成的 where 条件也会自动追加该字段

    删除: 转变为 更新

例如:

    删除: update user set deleted=1 where id = 1 and deleted=0

    查找: select id,name,deleted from user where deleted=0

使用

(可选)配置全局逻辑删除规则:

# 逻辑删除

mybatis-plus.global-config.db-config.logic-delete-value=1

mybatis-plus.global-config.db-config.logic-not-delete-value=0

实体类字段上加上@TableLogic注解(黄色标):

    /**

     * 是否显示[0-不显示,1显示]

     */

    @TableLogic(value = "1", delval = "0")

    private Integer showStatus;

    • Aliyun OSS

简介

对象存储两种方式比较

自建服务:FastDFS、vsftpd。搭建复杂,维护成本高,前期费用高。

云存储(Object Storage Service,OSS):阿里云对象存储、七牛云存储、腾讯云存储。即开即用,无需维护,按量收费。

阿里云对象存储资源术语

Bucket:存储空间,即存储对象的容器,所有的对象都必须隶属于某个存储空间。

Object:对象/文件。对象由元信息(Object Meta)、用户数据(Data)和文件名(Key)组成。对象由存储空间内部唯一的Key来标识。

Region:地域,即OSS数据中心所在物理位置。数据存储的地域根据费用、请求来源等来综合选择。

Endpoint:访问域名。形式为HTTP RESTful API。访问不同地域需要不同域名,内外网访问同一个地域也需要不同域名。

AccessKey:访问密钥,简称AK,包括AccessKeyId(用于标识用户)和AccessKeySecret(用于签名字符串加密和验证,必须保密)。

使用

实现方式:服务端签名后浏览器直接上传OSS

  1. 浏览器向应用服务器请求上传Policy,Policy里包含授权信息和上传信息
  2. 应用服务器返回上传Policy
  3. 浏览器凭借Policy上传文件到OSS

开通阿里云对象存储

开通地址对象存储 OSS_云存储服务_企业数据管理_存储-阿里云

管理控制台阿里云登录 - 欢迎登录阿里云,安全稳定的云计算服务平台

产品文档:管理控制台-》概览-》常用入口-》API文档-》文档中心打开

创建Bucket

创建RAM用户并赋权

管理控制台-》右上角账号-》AccessKey管理-》开始使用子用户AccessKey-》创建用户-》填写登录名称(gulimall)、显示名称(gulimall),勾选OpenAPI 调用访问-》确定-》记住AccessKey ID和AccessKey Secret(只在创建时显示,不支持后续查看)-》勾选用户-》添加权限-》选择权限如AliyunOSSFullAccess-》确定-》完成

SDK示例

引入依赖参见:产品文档-》SDK示例-》Java-》Java安装

上传文件见:产品文档-》SDK示例-》Java-》Java对象/文件-》Java上传文件-》Java简单上传-》上传文件流

创建第三方服务模块

项目类型:Spring Initializr

元数据:

Group:com.atguigu

Artifact:gulimall-third-party

Type:Maven

Language:Java

Packaging:Jar

Java Version:8

Description:谷粒商城-第三方服务

Package:com.atguigu.gulimall.thirdparty

(Version默认,Name跟填Artifact)

依赖选择界面,Sping Boot版本选2.x的,依赖选择Spring Web、OpenFeign后点Next-》Finish

检查、修改pom:

a.检查java.version是否为1.8,若为17,说明依赖选择界面Sping Boot版本选错了,要删除该模块重建

b.spring-boot-starter-parent的<version>改为2.1.8.RELEASE

c.spring-cloud.version改为Greenwich.SR3

修正GulimallThirdPartyApplicationTests代码

引入gulimall-common依赖,在启动类注解添加exclude属性来禁止SpringBoot自动注入数据源配置:

@SpringBootApplication(exclude = {DataSourceAutoConfiguration.class})

引入alicloud-oss依赖

引入alicloud-oss及spring-cloud-alibaba-dependencies:

    <dependencies>

        其他<dependency>...

        <!--aliyun-oss-->

        <dependency>

            <groupId>com.alibaba.cloud</groupId>

            <artifactId>spring-cloud-starter-alicloud-oss</artifactId>

        </dependency>

    </dependencies>

    <dependencyManagement>

        <dependencies>

            其他<dependency>...

            <dependency>

                <groupId>com.alibaba.cloud</groupId>

                <artifactId>spring-cloud-alibaba-dependencies</artifactId>

                <version>2.1.0.RELEASE</version>

                <type>pom</type>

                <scope>import</scope>

            </dependency>

        </dependencies>

    </dependencyManagement>

添加到注册中心和配置中心

新增命名空间third-party,并在该命名空间下添加配置application.properties和oss.properties

application.properties:

server.port=30000

spring.application.name=gulimall-third-party

spring.cloud.nacos.discovery.server-addr=127.0.0.1:8848

oss.properties:

spring.cloud.alicloud.access-key=<你的ak>

spring.cloud.alicloud.secret-key=<你的sk>

spring.cloud.alicloud.oss.endpoint=<你的endpoint,不要带https://,否则后面服务器签名时拼接host麻烦>

spring.cloud.alicloud.oss.bucket=gulimall-tongwx

third-party模块/src/main/resources/bootstrap.properties:

spring.cloud.nacos.config.server-addr=127.0.0.1:8848

spring.cloud.nacos.config.namespace=0bb35f66-d0ba-4922-978c-8715bfb1037a

spring.cloud.nacos.config.prefix=application

spring.cloud.nacos.config.ext-config[0].data-id=oss.properties

spring.cloud.nacos.config.ext-config[0].refresh=true

third-party模块启动类中使用@EnableDiscoveryClient开启服务注册发现功能

import org.springframework.cloud.client.discovery.EnableDiscoveryClient;

@EnableDiscoveryClient

添加到网关

gulimall-gateway\src\main\resources\application.properties

spring.cloud.gateway.routes[1].id=third_party_route

spring.cloud.gateway.routes[1].uri=lb://gulimall-third-party

spring.cloud.gateway.routes[1].predicates[0]=Path=/api/thirdparty/**

spring.cloud.gateway.routes[1].filters[0]=RewritePath=/api/(?<segment>/?.*), /${segment}

测试应用服务器上传OSS

@Autowired
private OSSClient ossClient;
@Test
public void testAlicloudOss() throws Exception {
    String bucketName = "gulimall-tongwx";
    String objectName = "exampledir/谷粒商城-微服务架构图.jpg";
    String filePath = "C:\\Users\\TWX\\Pictures\\谷粒商城-微服务架构图.jpg";
    try {
        InputStream inputStream = new FileInputStream(filePath);
        PutObjectRequest putObjectRequest = new PutObjectRequest(bucketName, objectName, inputStream);
        putObjectRequest.setProcess("true");// 设置可以返回response。如不设置,则返回的response为空。
        PutObjectResult result = ossClient.putObject(putObjectRequest);
        System.out.println(result.getResponse().getStatusCode()); // 如果上传成功,则返回200。
    } catch (OSSException oe) {
        System.out.println("Caught an OSSException, which means your request made it to OSS, "
                + "but was rejected with an error response for some reason.");
        System.out.println("Error Message:" + oe.getErrorMessage());
        System.out.println("Error Code:" + oe.getErrorCode());
        System.out.println("Request ID:" + oe.getRequestId());
        System.out.println("Host ID:" + oe.getHostId());
    } catch (ClientException ce) {
        System.out.println("Caught an ClientException, which means the client encountered "
                + "a serious internal problem while trying to communicate with OSS, "
                + "such as not being able to access the network.");
        System.out.println("Error Message:" + ce.getMessage());
    } finally {
        if (ossClient != null) {
            ossClient.shutdown();
        }
    }
}

使用服务端签名后浏览器直传OSS

third-party模块提供服务端签名接口:

package com.atguigu.gulimall.thirdparty.controller;

import com.aliyun.oss.OSS;
import com.aliyun.oss.common.utils.BinaryUtil;
import com.aliyun.oss.model.MatchMode;
import com.aliyun.oss.model.PolicyConditions;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

import java.time.LocalDate;
import java.time.format.DateTimeFormatter;
import java.util.Date;
import java.util.LinkedHashMap;
import java.util.Map;

@Slf4j
@RestController
@RequestMapping("thirdparty/oss")
public class OssController {

    @Autowired
    private OSS ossClient;

    @Value("${spring.cloud.alicloud.access-key}")
    private String accessId;

    @Value("${spring.cloud.alicloud.oss.endpoint}")
    private String endpoint;

    @Value("${spring.cloud.alicloud.oss.bucket}")
    private String bucket;

    @RequestMapping("/policy")
    public Map<String, String> policy(){
        String host = "https://" + bucket + "." + endpoint;
        String dir = LocalDate.now().format(DateTimeFormatter.ofPattern("yyyy-MM-dd")) + "/";
        try {
            long expireTime = 30;
            long expireEndTime = System.currentTimeMillis() + expireTime * 1000;
            Date expiration = new Date(expireEndTime);
            PolicyConditions policyConds = new PolicyConditions();
            policyConds.addConditionItem(PolicyConditions.COND_CONTENT_LENGTH_RANGE, 0, 1048576000);
            policyConds.addConditionItem(MatchMode.StartWith, PolicyConditions.COND_KEY, dir);

            String postPolicy = ossClient.generatePostPolicy(expiration, policyConds);
            byte[] binaryData = postPolicy.getBytes("utf-8");
            String encodedPolicy = BinaryUtil.toBase64String(binaryData);
            String postSignature = ossClient.calculatePostSignature(postPolicy);

            Map<String, String> respMap = new LinkedHashMap<String, String>();
            respMap.put("accessId", accessId);
            respMap.put("policy", encodedPolicy);
            respMap.put("signature", postSignature);
            respMap.put("dir", dir);
            respMap.put("host", host);
            respMap.put("expire", String.valueOf(expireEndTime / 1000));
            return respMap;
            /*
            String callbackUrl = "https://192.168.0.0:8888";//用于接收OSS的上传结果信息
            JSONObject jasonCallback = new JSONObject();
            jasonCallback.put("callbackUrl", callbackUrl);
            jasonCallback.put("callbackBody",
                    "filename=${object}&size=${size}&mimeType=${mimeType}&height=${imageInfo.height}&width=${imageInfo.width}");
            jasonCallback.put("callbackBodyType", "application/x-www-form-urlencoded");
            String base64CallbackBody = BinaryUtil.toBase64String(jasonCallback.toString().getBytes());
            respMap.put("callback", base64CallbackBody);

            JSONObject ja1 = JSONObject.fromObject(respMap);
            // System.out.println(ja1.toString());
            response.setHeader("Access-Control-Allow-Origin", "*");
            response.setHeader("Access-Control-Allow-Methods", "GET, POST");
            response(request, response, ja1.toString());
            */
        } catch (Exception e) {
            throw new RuntimeException(e.getMessage(), e);
        }
    }
}

前端renren-fast-vue模块代码

  1. 将资料中的前端组件upload文件夹拷贝到src\components目录下
  2. 修改src\components\upload目录下multiUpload.vue和singleUpload.vue中的el-upload元素的action值为自己的Bucket域名
  3. 确认src\components\upload\policy.js中url定义的后端接口无误
  4. src\views\modules\product\brand-add-or-update.vue中:
    1. 导入singleUpload组件:
      import SingleUpload from "@/components/upload/singleUpload";
    2. 注册singleUpload组件:export default下增加:
        components: { SingleUpload },
    3. 使用singleUpload组件:把<el-form-item label="品牌logo地址">中的<el-input>替换为:
      <single-upload v-model="dataForm.logo"></single-upload>

OSS开启允许跨域上传

管理控制台-》Bucket列表-》要设置的Bucket-》数据安全-》跨域设置-》创建规则-》配置来源、Methods、Headers等-》确定

测试

renren-fast-vue前端页面登录-》商品系统-》品牌管理-》新增

注:如果没有新增按钮,可临时修改源代码src/utils/index.js中的isAuth方法直接return true;

定义一个统一返回对象包装类:

package com.tongwx.demo.config;

import lombok.Data;

@Data
public class GlobalResponse {

    private Object obj;
    private boolean success;
    private String failureCode;
    private String failureMessage;

    public static GlobalResponse buildSuccess(Object obj) {
        GlobalResponse response = new GlobalResponse();
        response.setSuccess(true);
        response.setObj(obj);
        return response;
    }
    public static GlobalResponse buildFail(String message) {
        GlobalResponse response = new GlobalResponse();
        response.setSuccess(false);
        response.setFailureMessage(message);
        return response;
    }
}

定义一个统一返回注解,用于注解要使用统一返回的Controller类或其方法:

package com.tongwx.demo.config;

import org.springframework.web.bind.annotation.ResponseBody;

import java.lang.annotation.*;

import static java.lang.annotation.RetentionPolicy.RUNTIME;


/**
 * Controller使用统一返回标记
 * 注意:接口请求参数中若含有HttpServletResponse,则该注解不起作用
 * {@link GlobalResponseAdvice}
 */
@Retention(RUNTIME)
@Target({ElementType.TYPE, ElementType.METHOD})
@Documented
@ResponseBody
public @interface GlobalResponseBody {

}

定义一个实现ResponseBodyAdvice接口的统一返回通知类:

package com.tongwx.demo.config;

import org.springframework.core.MethodParameter;
import org.springframework.core.annotation.AnnotatedElementUtils;
import org.springframework.http.MediaType;
import org.springframework.http.converter.HttpMessageConverter;
import org.springframework.http.server.ServerHttpRequest;
import org.springframework.http.server.ServerHttpResponse;
import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.servlet.mvc.method.annotation.ResponseBodyAdvice;

import java.io.File;
import java.lang.annotation.Annotation;

/**
 * Controller使用统一返回
 */
@ControllerAdvice
public class GlobalResponseAdvice implements ResponseBodyAdvice<Object> {

    private static final Class<? extends Annotation> ANNOTATION_TYPE = GlobalResponseBody.class;

    /**
     * 控制是否包装返回结果
     */
    @Override
    public boolean supports(MethodParameter methodParameter, Class<? extends HttpMessageConverter<?>> aClass) {
        return AnnotatedElementUtils.hasAnnotation(methodParameter.getContainingClass(), ANNOTATION_TYPE)
                || methodParameter.hasMethodAnnotation(ANNOTATION_TYPE);
    }

    /**
     * 包装返回结果
     * 注:返回String的情况已特殊处理:{@link GlobalWebMvcConfigurer#configureMessageConverters(java.util.List)}
     */
    @Override
    public Object beforeBodyWrite(Object obj, MethodParameter methodParameter, MediaType mediaType, Class<? extends HttpMessageConverter<?>> aClass, ServerHttpRequest serverHttpRequest, ServerHttpResponse serverHttpResponse) {
        //防止重复包装和不需包装的情况
        if(obj instanceof GlobalResponse || obj instanceof File){
            return obj;
        }
        return GlobalResponse.buildSuccess(obj);
    }
}

返回String类型的包装需要特殊处理:

package com.tongwx.demo.config;

import org.springframework.context.annotation.Configuration;
import org.springframework.http.converter.HttpMessageConverter;
import org.springframework.http.converter.json.MappingJackson2HttpMessageConverter;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;

import java.util.List;

@Configuration
public class GlobalWebMvcConfigurer implements WebMvcConfigurer {
    @Override
    public void configureMessageConverters(List<HttpMessageConverter<?>> converters) {
        /*
        问题:
            当Controller返回String时,GlobalResponseAdvice#beforeBodyWrite方法将String处理成GlobalResponse
            在AbstractHttpMessageConverter#addDefaultHeaders调用实现类方法StringHttpMessageConverter#getContentLength,
            将GlobalResponse传给getContentLength方法第一个参数时发生ClassCastException
        解决方法:
            将json转换器放到转换器列表首位,使得先让json转换器处理返回值,这样String转换器就没机会处理了。
         */
        //
        converters.add(0, new MappingJackson2HttpMessageConverter());
        // 以下解决方式(删除String转换器)经测试无效:
//        converters.removeIf(httpMessageConverter -> httpMessageConverter.getClass() == StringHttpMessageConverter.class);
    }
}

在common模块定义一个错误码枚举

package com.atguigu.common.enums;
/**
 * 错误码枚举
 *     定义规则:5为数字,前两位表示业务场景,后三位表示错误码
 *     例如:100001。10:通用场景 001:未知错误
 *     场景列表:
 *         10: 通用场景
 *         11: 商品场景
 *         12: 订单场景
 *         13: 购物车场景
 *         14: 物流场景
 *         15:用户场景
 *         21:库存场景
 **/
public enum  ErrCodeEnum {
    UNKNOWN(10000,"未知异常"),
    ARG_NOT_VALID(10001,"参数错误"),
    TO_MANY_REQUEST(10002,"请求流量过大,请稍后再试"),
    TO_MANY_SMS_CODE(10002,"验证码获取频率太高,请稍后再试"),
    PRODUCT_UP_EXCEPTION(11000,"商品上架异常"),
    USER_EXIST(15001,"存在相同的用户"),
    PHONE_EXIST(15002,"存在相同的手机号"),
    LOGIN_EXCEPTION(15003,"账号或密码错误"),
    NO_STOCK(21000,"商品库存不足"),
    ;

    private Integer code;

    private String message;

    ErrCodeEnum(Integer code, String message) {
        this.code = code;
        this.message = message;
    }

    public Integer getCode() {
        return code;
    }

    public String getMessage() {
        return message;
    }
}

在分模块如product模块定义一个全局异常通知

@RestControllerAdvice等于@ResponseBody + @ControllerAdvice

@RestControllerAdvice可使用basePackages属性指定作用域

package com.atguigu.gulimall.product.exception;

import com.atguigu.common.enums.ErrCodeEnum;
import com.atguigu.common.utils.R;
import lombok.extern.slf4j.Slf4j;
import org.springframework.web.bind.MethodArgumentNotValidException;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.RestControllerAdvice;

import java.util.HashMap;
import java.util.Map;


/**
 * 统一异常处理
 */
@Slf4j

@RestControllerAdvice
public class GlobalExceptionAdvice {
    /**
     * 参数校验异常
     */
    @ExceptionHandler(value = MethodArgumentNotValidException.class)
    public R errorHandlerMethodArgumentNotValidException(MethodArgumentNotValidException e) {
        Map<String, String> errMap = new HashMap<>();
        e.getBindingResult().getFieldErrors().forEach(fieldError -> errMap.put(fieldError.getField(), fieldError.getDefaultMessage()));
        return R.error(ErrCodeEnum.ARG_NOT_VALID.getCode(), ErrCodeEnum.ARG_NOT_VALID.getMessage()).put("data", errMap);
    }

    /**
     * 运行时异常
     */
    @ExceptionHandler(value = RuntimeException.class)
    public R errorHandlerRuntimeException(RuntimeException ex) {
        return R.error(ErrCodeEnum.RUNTIME_EXCEPTION.getCode(), ex.getMessage());
    }

    /**
     * 其他异常
     */
    @ExceptionHandler(value = Throwable.class)
    public R errorHandler(Throwable ex) {
        if(ex.getMessage() != null && (
                ex.getMessage().startsWith("JSON parse error: parseDecimal error") ||
                        ex.getMessage().startsWith("JSON parse error: parseInt error"))){
            return R.error(ErrCodeEnum.ARG_NOT_VALID.getCode(), "数字转换异常,请确保提交的数字类型参数中没有非数字");
        }
        log.error(ex.getMessage(), ex);
        return R.error(ex.getMessage());
    }
}

    • Validation

Java API规范(JSR303)定义了Bean校验的标准validation-api,但没有提供实现。

hibernate validation是对这个规范的实现,并增加了校验注解如@Email、@Length等。

Spring Validation是对hibernate validation的二次封装,用于支持spring mvc参数自动校验。

引入依赖

如果spring-boot版本小于2.3.x,spring-boot-starter-web会自动传入hibernate-validator依赖。

如果spring-boot版本大于2.3.x,则需要手动引入依赖。

本项目gulimall-common模块不是spring项目,后文在gulimall-common模块自定义校验注解时,需要引入依赖。

定义分组规则

在common模块com.atguigu.common.valid包中定义分组规则:

package com.atguigu.common.valid;

public interface AddGroup {
}

package com.atguigu.common.valid;

public interface UpdateGroup {
}

package com.atguigu.common.valid;

public interface UpdateStatusGroup {
}

在DTO字段上声明约束注解

javax.validation.constraints提供的约束注解:

org.hibernate.validator.constraints提供的约束注解:

@Null

必须为null

@Range(min=,max=)

必须在合适的范围内

@NotNull

必须不为null

@Length(min=,max=)

字符串的大小必须在指定的范围内

@NotEmpty

字符串不能为null或""

集合/数组长度必须大于0

@URL

@NotBlank

字符串不能为null或trim后不能为""

@UniqueElements

@Size(max=,min=)

集合/数组/字符串长度必须在指定范围内

@ScriptAssert

@AssertTrue

必须为true

@SafeHtml

@AssertFalse

必须为false

@ParameterScriptAssert

@Min(value)

Number或String不能小于指定的最小值

@Mod10Check

@Max(value)

Number或String不能大于指定的最大值

@Mod11Check

@DecimalMin(value)

必须是数字,其值不能小于指定的最小值

@LuhnCheck

@DecimalMax(value)

必须是数字,其值不能大于指定的最大值

@ISBN

@Digits(integer, fraction)

必须为数字,且

整数位数必须等于integer参数值,

小数位数必须等于fraction参数值。

@EAN

@Positive

必须为正数

@Currency

@PositiveOrZero

必须为正数或0

@CreditCardNumber

@Negative

必须为负数

@ConstraintComposition

@NegativeOrZero

必须为负数或0

@CompositionType

@Past

必须是过去的日期

@CodePointLength

@PastOrPresent

必须是现在或过去的日期

@DurationMax

@Future

必须是将来的日期

@DurationMin

@FutureOrPresent

必须是现在或将来的日期

@NIP

@Pattern(regex=,flag=)

必须符合指定的正则表达式

@PESEL

@Email

必须是电子邮箱地址

@REGON

@CNPJ

@CPF

@TituloEleitoral

约束注解的核心属性

message属性:如果约束注解不指定message属性,那么异常信息会从应用类路径下的ValidationMessages.properties文件中加载,如果没有,再从依赖包中的同名文件中加载。

groups属性:用于分组校验功能。

payload属性:

示例:在product模块的com.atguigu.gulimall.product.entity.BrandEntity中声明约束注解并指定message和groups属性:

package com.atguigu.gulimall.product.entity;

import com.atguigu.common.valid.AddGroup;
import com.atguigu.common.valid.ListValue;
import com.atguigu.common.valid.UpdateGroup;
import com.atguigu.common.valid.UpdateStatusGroup;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import lombok.Data;
import org.hibernate.validator.constraints.URL;

import javax.validation.constraints.*;
import java.io.Serializable;

/**
 * 品牌
 *
 * @author tongwx
 * @email 540078659@qq.com
 * @date 2023-03-12 04:38:05
 */
@Data
@TableName("pms_brand")
public class BrandEntity implements Serializable {
   private static final long serialVersionUID = 1L;
    /**
     * 品牌id
     */
    @NotNull(message = "修改必须指定品牌id",groups = {UpdateGroup.class})
    @Null(message = "新增不能指定id",groups = {AddGroup.class})
    @TableId
    private Long brandId;
    /**
     * 品牌名
     */
    @NotBlank(message = "品牌名不能为空",groups = {AddGroup.class,UpdateGroup.class})
    private String name;
    /**
     * 品牌logo地址
     */
    @NotBlank(groups = {AddGroup.class})
    @URL(message = "品牌logo地址不合法",groups={AddGroup.class,UpdateGroup.class})
    private String logo;
    /**
     * 介绍
     */
    private String descript;
    /**
     * 显示状态[0-不显示;1-显示]
     */
// @Pattern()
    @NotNull(groups = {AddGroup.class, UpdateStatusGroup.class})
    @ListValue(vals={0,1},groups = {AddGroup.class, UpdateStatusGroup.class})
    private Integer showStatus;
    /**
     * 检索首字母
     */
    @NotEmpty(groups={AddGroup.class})
    @Pattern(regexp="^[a-zA-Z]$",message = "检索首字母必须是一个字母",groups={AddGroup.class,UpdateGroup.class})
    private String firstLetter;
    /**
     * 排序
     */
    @NotNull(groups={AddGroup.class})
    @Min(value = 0,message = "排序必须大于等于0",groups={AddGroup.class,UpdateGroup.class})
    private Integer sort;
}

在方法参数上声明校验注解

校验注解有@Valid和@Validated。@Valid是validation-api的注解,@Validated是Spring框架的注解。@Validated是对@Valid的封装。

如果不使用分组校验功能,校验注解使用@Valid或@Validated都可以。

如果要使用分组校验功能,则需使用Spring框架的@Validated注解。

当@Validated注解未指定分组规则且对应参数为null时,只有未定义groups属性的空/非空校验注解会进行校验。

当@Validated注解未指定分组规则且对应参数非null时,只有未定义groups属性的校验注解会进行校验。

当@Validated注解指定了分组规则且对应参数为null时,只有groups属性匹配的空/非空校验注解会进行校验。

当@Validated注解指定了分组规则且对应参数非null时,只有groups属性匹配的校验注解会进行校验。

注:空/非空校验注解指:@Null/@NotNull/@NotEmpty/@NotBlank

在product模块的com.atguigu.gulimall.product.controller.BrandController的方法参数上声明校验注解,并指定分组规则:

    /**
     * 保存
     */
    @RequestMapping("/save")
    public R save(@Validated({AddGroup.class}) @RequestBody BrandEntity brand){
        brandService.save(brand);
        return R.ok();
    }

    /**
     * 修改
     */
    @RequestMapping("/update")
    public R update(@Validated({UpdateGroup.class}) @RequestBody BrandEntity brand){
        brandService.updateById(brand);
        return R.ok();
    }

    /**
     * 修改状态
     */
    @RequestMapping("/update/status")
    public R updateStatus(@Validated(UpdateStatusGroup.class) @RequestBody BrandEntity brand){
        brandService.updateById(brand);
        return R.ok();
    }

除了在方法参数上声明校验注解,还可以使用验证器对象来校验:

// 验证器对象

private Validator validator = Validation.buildDefaultValidatorFactory().getValidator();

// 验证并返回验证结果

private Set<ConstraintViolation<BrandEntity>> set = validator.validate(brandEntity);

// 分组验证并返回验证结果

private Set<ConstraintViolation<BrandEntity>> set = validator.validate(brandEntity, AddGroup.class);

在统一异常处理器增加校验失败处理逻辑

如果校验失败,会抛出MethodArgumentNotValidException异常,Spring默认会返回400响应码(Bad Request)。

com.atguigu.gulimall.product.exception.GlobalExceptionAdvice中增加异常处理逻辑:

    /**
     * 参数校验异常
     */
    @ExceptionHandler(value = MethodArgumentNotValidException.class)
    public R errorHandlerMethodArgumentNotValidException(MethodArgumentNotValidException e) {
        Map<String, String> errMap = new HashMap<>();
        e.getBindingResult().getFieldErrors().forEach(fieldError -> errMap.put(fieldError.getField(), fieldError.getDefaultMessage()));
        return R.error(ErrCodeEnum.ARG_NOT_VALID.getCode(), ErrCodeEnum.ARG_NOT_VALID.getMessage()).put("data", errMap);
    }

自定义校验

本例使用自定义校验来约束BrandEntity的showStatus属性只允许提交0或1两种值。

gulimall-common模块不是spring项目,gulimall-common模块自定义约束注解时,需要引入依赖(引入其中一个即可)

        <dependency>

            <groupId>org.hibernate</groupId>

            <artifactId>hibernate-validator</artifactId>

            <version>6.0.1.Final</version>

        </dependency>

        <dependency>

            <groupId>javax.validation</groupId>

            <artifactId>validation-api</artifactId>

            <version>2.0.1.Final</version>

        </dependency>

编写自定义约束注解com.atguigu.common.valid.ListValue

message、groups、payload为必须定义的属性

message的默认值key一般取message的完全限定名,value定义在类路径下的ValidationMessages.properties文件中

vals属性用来指定允许的值

@Constraint的validatedBy属性指定校验器,可指定多个

package com.atguigu.common.valid;

import javax.validation.Constraint;
import javax.validation.Payload;
import java.lang.annotation.Documented;
import java.lang.annotation.Retention;
import java.lang.annotation.Target;

import static java.lang.annotation.ElementType.*;
import static java.lang.annotation.ElementType.TYPE_USE;
import static java.lang.annotation.RetentionPolicy.RUNTIME;

@Documented
@Constraint(validatedBy = { ListValueConstraintValidator.class })
@Target({ METHOD, FIELD, ANNOTATION_TYPE, CONSTRUCTOR, PARAMETER, TYPE_USE })
@Retention(RUNTIME)
public @interface ListValue {
    String message() default "{com.atguigu.common.valid.ListValue.message}";

    Class<?>[] groups() default { };

    Class<? extends Payload>[] payload() default { };

    int[] vals() default { };
}

编写自定义错误信息配置gulimall-common\src\main\resources\ValidationMessages.properties:

key为约束注解的message属性中定义的key

com.atguigu.common.valid.ListValue.message=必须提交指定的值

编写自定义校验器com.atguigu.common.valid.ListValueConstraintValidator

即ListValue中@Constraint的validatedBy属性指定的校验器

package com.atguigu.common.valid;

import javax.validation.ConstraintValidator;
import javax.validation.ConstraintValidatorContext;
import java.util.HashSet;
import java.util.Set;

public class ListValueConstraintValidator implements ConstraintValidator<ListValue,Integer> {

    private Set<Integer> set = new HashSet<>();
    /**
     * 初始化方法
     */
    @Override
    public void initialize(ListValue constraintAnnotation) {
        int[] vals = constraintAnnotation.vals();
        for (int val : vals) {
            set.add(val);
        }
    }

    /**
     * 判断是否校验成功
     * @param value 需要校验的值
     */
    @Override
    public boolean isValid(Integer value, ConstraintValidatorContext context) {
        return set.contains(value);
    }
}

将自定义约束注解应用到com.atguigu.gulimall.product.entity.BrandEntity#showStatus:

    /**
     * 显示状态[0-不显示;1-显示]
     */
    @NotNull(groups = {AddGroup.class, UpdateStatusGroup.class})
    @ListValue(vals={0,1},groups = {AddGroup.class, UpdateGroup.class, UpdateStatusGroup.class})
    private Integer showStatus;

其他功能

级联验证:通过@Valid注解实现级联校验。举个例子,我的ScriptionBO中有一个List属性。我希望Validation框架在校验ScriptionBO的时候,不仅仅校验ScriptionBO的属性,还要验证其中List涉及的User们。那么在List上添加@Valid注解,就可以实现了。

分组序列:通过分组校验,再加上@GroupSequence({xxxGroup.class,xxxGroup.class}),就可以实现分组序列了。举个例子,登录场景下,User连userId的非空校验都没有通过,那么就更不需要校验手机号码,邮箱等。

创建项目

创建maven项目:

File-》New-》Project...-》Maven-》Next-》...

JKD配置:

File-》Project Structure(新建空白项目后会自动弹出)-》Project Settings-》Project-》Project SDK-》选1.8以上

Maven配置:

File-》Build,Execution,Deployment-》Build Tools-》Maven-》User settings file-》override打勾-》选择自定义镜像如阿里云的settings.xml

如果是克隆已有项目:

File-》New-》Project from Version Control-》Git-》...

创建模块

注:通用模块创建为maven工程,业务模块创建为Spring Boot工程

创建父工程(打包方式pom)

父工程管理依赖版本

创建接口工程

接口工程定义业务接口和数据库实体类,各业务相关工程引用接口工程,实现业务接口方法。

创建公共工具类工程

创建service工具类工程

创建web工具类工程

创建用户服务工程

项目右键-》New-》Module-》Spring Initializr-》元数据见下表-》起步依赖勾选web、mysql、jdbc、Mybatis-》...

<groupId>com.tongwx.mall</groupId>
<artifactId>user-manage</artifactId>
<version>0.0.1-SNAPSHOT</version>
<name>user-manage</name>

  • 19
    点赞
  • 19
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值