商城项目

本文最后更新于:2022年4月27日 上午

谷粒商城-微服务架构图

一、分布式基础概念

1.微服务

把一个单独的应用程序开发成一套小服务,每个小服务运行在自己的进程中,并使用轻量级机制通信

2.集群、分布式、节点

  • 集群:将多台服务器集中在一起,实现同一业务

  • 分布式:将不同的业务分布在不同的地方

  • 节点:集群中的每台服务器就是一个节点

3.远程调用

在分布式系统中,各个业务可能处于不同主机,但是服务之间不可避免地需要相互调用,即远程调用。

SpringCloud中采用HTTP+JSON方式完成远程调用。

RPC ??

4.负载均衡

为了使分布式系统中的每个服务器不至于太忙或者太闲,我们可以负载均衡地调用每一个服务器,提升网页的健壮性。

常见的负载均衡算法有:

  • 轮询:为第一个请求选择健康池中的第一个后端服务器,然后按照顺序依次往后选择
  • 最小连接:优先选择连接数最小的后端服务器

5.服务注册/发现、注册中心

通过注册中心进行服务的注册及调用。

比如A服务要调用B服务,A服务不知道B服务当前在哪几台服务器有、哪些是正常的、哪些已下线,此时直接向注册中心发起请求询问即可

6.配置中心

每一个服务最终都有大量的配置,并且每个服务都可能部署在多台服务器上。此时便可以使用配置中心来集中管理微服务的配置信息

7.服务熔断、服务降级

在微服务架构中,微服务之间通过网络进行通信,存在相互依赖,当其中一个服务不可用时,可能会造成雪崩效应。为了避免这种情况,需设置相应的容错机制来保护服务。

订单服务   ---》   商品服务     ---》   库存服务

服务熔断:设置服务的超时,当被调用的服务经常失败达到某个阈值时,开启断路保护机制,后来的请求不再去调用这个服务,本地直接返回默认的数据。

服务降级:在系统高峰期、资源紧张时,某些业务不处理或者简单处理(抛异常、返回Null、调用Mock数据等等)

8.API网关

前后端分离,前端发来的所有请求先到达API网关,进行处理(如统一认证、限流、日志统计等)后,再调用相应的后台服务器

二、环境搭建

1.安装VirtualBox和Vagrant

Downloads | Vagrant by HashiCorp (vagrantup.com)

Download_Old_Builds_6_0 – Oracle VM VirtualBox

2.安装centos

$ vagrant init centos/7
A `Vagrantfile` has been placed in this directory. You are now
ready to `vagrant up` your first virtual environment! Please read
the comments in the Vagrantfile as well as documentation on
`vagrantup.com` for more information on using Vagrant.

此时,会在当前目录下生成Vagrantfile文件,再通过vagrant up指令即可启用虚拟环境了。

但由于镜像下载奇慢,所以可以先下载镜像,然后本地启动。镜像下载地址

$ vagrant box add centos/7 D:\\Environment\\CentOS-7-x86_64-Vagrant-1905_01.VirtualBox.box
==> box: Box file was not detected as metadata. Adding it directly...
==> box: Adding box 'centos/7' (v0) for provider:
    box: Unpacking necessary files from: file:///D:/Environment/CentOS-7-x86_64-Vagrant-1905_01.VirtualBox.b                       ox
    box:
==> box: Successfully added box 'centos/7' (v0) for 'virtualbox'!
$ vagrant up
Bringing machine 'default' up with 'virtualbox' provider...
==> default: Importing base box 'centos/7'...
==> default: Matching MAC address for NAT networking...
==> default: Setting the name of the VM: Environment_default_1615272319465_69667
==> default: Clearing any previously set network interfaces...
==> default: Preparing network interfaces based on configuration...
    default: Adapter 1: nat
==> default: Forwarding ports...
    default: 22 (guest) => 2222 (host) (adapter 1)
==> default: Booting VM...
==> default: Waiting for machine to boot. This may take a few minutes...
    default: SSH address: 127.0.0.1:2222
    default: SSH username: vagrant
    default: SSH auth method: private key
    default:
    default: Vagrant insecure key detected. Vagrant will automatically replace
    default: this with a newly generated keypair for better security.
    default:
    default: Inserting generated public key within guest...
    default: Removing insecure key from the guest if it's present...
    default: Key inserted! Disconnecting and reconnecting using new SSH key...
==> default: Machine booted and ready!
==> default: Checking for guest additions in VM...
    default: No guest additions were detected on the base box for this VM! Guest
    default: additions are required for forwarded ports, shared folders, host only
    default: networking, and more. If SSH fails on this machine, please install
    default: the guest additions and repackage the box to continue.
    default:
    default: This is not an error message; everything may continue to work properly,
    default: in which case you may ignore this message.
==> default: Rsyncing folder: /cygdrive/d/Environment/ => /vagrant

此时,便可以通过vagrant ssh连接虚拟机了。为了方便起见,需固定虚拟机的固定ip地址:

(1)查看本地ip地址

C:\Users\GWL>ipconfig

Windows IP 配置

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

   连接特定的 DNS 后缀 . . . . . . . :
   本地链接 IPv6 地址. . . . . . . . : fe80::fd96:59e6:612c:fe3b%52
   IPv4 地址 . . . . . . . . . . . . : 192.168.56.1
   子网掩码  . . . . . . . . . . . . : 255.255.255.0
   默认网关. . . . . . . . . . . . . :

无线局域网适配器 本地连接* 1:

   媒体状态  . . . . . . . . . . . . : 媒体已断开连接
   连接特定的 DNS 后缀 . . . . . . . :

无线局域网适配器 WLAN:

   媒体状态  . . . . . . . . . . . . : 媒体已断开连接
   连接特定的 DNS 后缀 . . . . . . . : AirDream

无线局域网适配器 本地连接* 2:

   媒体状态  . . . . . . . . . . . . : 媒体已断开连接
   连接特定的 DNS 后缀 . . . . . . . :

以太网适配器 以太网:

   连接特定的 DNS 后缀 . . . . . . . :
   IPv6 地址 . . . . . . . . . . . . : 
   临时 IPv6 地址. . . . . . . . . . : 
   本地链接 IPv6 地址. . . . . . . . : 
   IPv4 地址 . . . . . . . . . . . . : 172.18.154.202
   子网掩码  . . . . . . . . . . . . : 255.255.255.0
   默认网关. . . . . . . . . . . . . : 
                                       

PPP 适配器 宽带连接:

   连接特定的 DNS 后缀 . . . . . . . :
   IPv4 地址 . . . . . . . . . . . . : 10.25.165.212
   子网掩码  . . . . . . . . . . . . : 255.255.255.255
   默认网关. . . . . . . . . . . . . : 0.0.0.0

(2)修改之前生成的Vagrantfile文件

config.vm.network "private_network", ip: "192.168.56.10"

(3)重启并查看虚拟机中地址是否改变

vagrant reload
$ vagrant ssh
Last login: Tue Mar  9 06:57:30 2021 from 10.0.2.2
[vagrant@promote ~]$ ip addr
1: lo: <LOOPBACK,UP,LOWER_UP> mtu 65536 qdisc noqueue state UNKNOWN group default qlen 1000
    link/loopback 00:00:00:00:00:00 brd 00:00:00:00:00:00
    inet 127.0.0.1/8 scope host lo
       valid_lft forever preferred_lft forever
    inet6 ::1/128 scope host
       valid_lft forever preferred_lft forever
2: eth0: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc pfifo_fast state UP group default qlen 1000
    link/ether 52:54:00:8a:fe:e6 brd ff:ff:ff:ff:ff:ff
    inet 10.0.2.15/24 brd 10.0.2.255 scope global noprefixroute dynamic eth0
       valid_lft 86290sec preferred_lft 86290sec
    inet6 fe80::5054:ff:fe8a:fee6/64 scope link
       valid_lft forever preferred_lft forever
3: eth1: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc pfifo_fast state UP group default qlen 1000
    link/ether 08:00:27:c1:df:4f brd ff:ff:ff:ff:ff:ff
    inet 192.168.56.10/24 brd 192.168.56.255 scope global noprefixroute eth1
       valid_lft forever preferred_lft forever
    inet6 fe80::a00:27ff:fec1:df4f/64 scope link
       valid_lft forever preferred_lft forever

(4)测试两者通信

C:\Users\GWL>ping 192.168.56.10
正在 Ping 192.168.56.10 具有 32 字节的数据:
来自 192.168.56.10 的回复: 字节=32 时间<1ms TTL=64
来自 192.168.56.10 的回复: 字节=32 时间<1ms TTL=64
来自 192.168.56.10 的回复: 字节=32 时间<1ms TTL=64

192.168.56.10 的 Ping 统计信息:
    数据包: 已发送 = 3,已接收 = 3,丢失 = 0 (0% 丢失),
往返行程的估计时间(以毫秒为单位):
    最短 = 0ms,最长 = 0ms,平均 = 0ms
[vagrant@promote ~]$ ping 172.18.154.202
PING 172.18.154.202 (172.18.154.202) 56(84) bytes of data.
64 bytes from 172.18.154.202: icmp_seq=1 ttl=127 time=0.707 ms
64 bytes from 172.18.154.202: icmp_seq=2 ttl=127 time=0.981 ms
64 bytes from 172.18.154.202: icmp_seq=3 ttl=127 time=3.08 ms
64 bytes from 172.18.154.202: icmp_seq=4 ttl=127 time=0.723 ms
^C
--- 172.18.154.202 ping statistics ---
4 packets transmitted, 4 received, 0% packet loss, time 3003ms
rtt min/avg/max/mdev = 0.707/1.373/3.082/0.992 ms

3.centos中安装Docker

# 安装所需软件包
sudo yum install -y yum-utils device-mapper-persistent-data lvm2

# 设置仓库
sudo yum-config-manager  --add-repo  http://mirrors.aliyun.com/docker-ce/linux/centos/docker-ce.repo

# 安装(一路确认即可)
sudo yum install docker-ce docker-ce-cli containerd.io

# 启动docker服务
sudo systemctl start docker

# 设置开机自启
sudo systemctl enable docker

开启镜像加速:阿里云/控制台/产品与服务/容器镜像服务/镜像加速器

配置DNS:nano /etc/resolv.conf,添加以下

nameserver 8.8.8.8

基本使用:

  • 查看版本:docker -v

  • 查看当前镜像:docker images

  • 查看容器:docker ps

  • 以交互模式进入特定的容器:docker exec -it mysql /bin/bash

4.Docker安装Mysql

[root@promote vagrant]# docker pull mysql:5.7
5.7: Pulling from library/mysql
45b42c59be33: Pull complete
b4f790bd91da: Pull complete
325ae51788e9: Pull complete
adcb9439d751: Pull complete
174c7fe16c78: Pull complete
698058ef136c: Pull complete
4690143a669e: Pull complete
66676c1ab9b3: Pull complete
25ebf78a38b6: Pull complete
a6510e5d6228: Pull complete
90ca045d52c5: Pull complete
Digest: sha256:9fc60b229633ce1d1f2ee306705152d4b001056fb27c1b5debe23a732df72b70
Status: Downloaded newer image for mysql:5.7
docker.io/library/mysql:5.7


# 查看镜像
[root@promote vagrant]# docker images
REPOSITORY   TAG       IMAGE ID       CREATED       SIZE
mysql        5.7       d54bd1054823   10 days ago   449MB

创建实例并启动:

  • 将容器的3306端口映射到主机的3306端口
  • 将日志文件、配置文件等挂载到主机
  • 初始化root用户的密码
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

修改mysql配置:

# /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

以交互方式进入容器查看:

[root@promote vagrant]# docker ps
CONTAINER ID   IMAGE       COMMAND                  CREATED          STATUS          PORTS                               NAMES
e917388553f6   mysql:5.7   "docker-entrypoint.s…"   14 seconds ago   Up 12 seconds   0.0.0.0:3306->3306/tcp, 33060/tcp   mysql

[root@promote vagrant]# docker exec -it mysql /bin/bash
root@e917388553f6:/# ls
bin   dev                         entrypoint.sh  home  lib64  mnt  proc  run   srv  tmp  var
boot  docker-entrypoint-initdb.d  etc            lib   media  opt  root  sbin  sys  usr

root@e917388553f6:/# whereis mysql
mysql: /usr/bin/mysql /usr/lib/mysql /etc/mysql /usr/share/mysql


root@e917388553f6:/etc/mysql# cat 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


root@e917388553f6:/# exit
exit

设置mysql服务随Docker启动

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

5.Docker安装Redis

拉取镜像:docker pull redis

启动镜像:

# 预先创建文件,避免docker将路径识别为目录;持久化redis
mkdir -p /mydata/redis/conf
touch /mydata/redis/conf/redis.conf
echo "appendonly yes"  >> /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

连接到docker中的redis

[root@promote vagrant]# docker exec -it redis redis-cli
127.0.0.1:6379> set key1 v1
OK
127.0.0.1:6379> get key1
"v1"
127.0.0.1:6379> exit

设置redis容器随docker启动

docker update redis --restart=always

6.创建Maven工程

在gitee新建项目,然后通过IDEA的project from拉取gitee上的项目,然后初始化。

新建coupon、member、order、product、ware等服务模块

image-20210316222434782

7.Renren-fast

通过git clone https://gitee.com/renrenio/renren-fast.git拉取代码,并删除其中的.git文件夹

  • 将源码复制到IDEA中,并修改根目录pom.xml,使其包含此模块

  • IDEA安装lombok插件,不然会提示找不到entity的get、set方法;idea版本也不能太高,否则lombok插件会出现各种鬼问题!!

  • syslog创建数据库gulimall_admin,数据库编码为UTF8mb4;之后,执行db/mysql.sql文件,初始化数据
  • IDEA中,修改application-dev.yml,更新MySQL账号和密码
  • IDEA中运行RenrenApplication.java,则可启动项目

此时,通过http://localhost:8080/renren-fast/即可访问

8.Renren-fast-vue

git clone https://gitee.com/renrenio/renren-fast-vue.git

cd renren-fast-vue/

npm install -g cnpm --registry=https://registry.npm.taobao.org
cnpm rebuild node-sass

cnpm install

npm run dev

之后就可以在http://localhost:8001/#/login查看后台管理的前端登录界面了。

若已经启动了后端系统,此时验证码处会出现相应的验证码图片,初始用户和密码均为admin

9.逆向生成各个微服务

通过git clone https://gitee.com/renrenio/renren-generator.git拉取文件,然后删掉.git移动到gulimall根路径下,修改pom.xml添加此模块

<module>renren-generator</module>

修改spring配置文件application.yaml

spring:
  datasource:
    type: com.alibaba.druid.pool.DruidDataSource
    #MySQL配置
    driverClassName: com.mysql.cj.jdbc.Driver
    url: jdbc:mysql://192.168.56.10:3306/gulimall_oms?useUnicode=true&characterEncoding=UTF-8&useSSL=false&serverTimezone=Asia/Shanghai
    username: root
    password: root

修改代码生成器配置文件renren-generator/src/main/resources/generator.properties

mainPath=com.gaowl
#包名
package=com.gaowl.gulimall
moduleName=product

#作者
author=gaowl
#Email
email=gaowl@stu.jiangnan.edu.cn

#表前缀(类名不会包含表前缀)
tablePrefix=pms_

之后运行此模块的主程序RenrenApplication.java,便可通过http://localhost:80/访问网页版控制台了

选中所有的表,点击生成代码即可下载压缩文件renren.zip,取出其中的main文件夹,放到gulimall-product模块中的main目录中。

  • 修改pom.xml文件,导入gulimall-commom依赖

  • 创建配置文件application.yaml

    spring:
      datasource:
        username: root
        password: root
        url: jdbc:mysql://192.168.56.10:3306/gulimall_pms?useUnicode=true&characterEncoding=UTF-8&serverTimezone=Asia/Shanghai
        driver-class-name: com.mysql.cj.jdbc.Driver
    mybatis-plus:
      mapper-locations: classpath*:/mapper/**/*.xml   # 指定mapper文件位置
      global-config:
        db-config:
          id-type: auto   # 主键自增
  • 缺的一些其他依赖可以在renren-fastsrc/main/java/io/renren/common/utils等路径中寻找。

  • 测试

三、相关技术基础知识

技术框架/工具 功能 版本
Java 语言 1.8
Maven 项目构建工具 3.6.3
IDEA 编辑器 Ultimate 2019.3
SpringBoot 快速构建Spring服务 2.1.8.RELEASE
SpringCloud 分布式服务 Greenwich.SR3
SpringCloud - Feign 远程调用服务 2.1.3.RELEASE
SpringCloud - Gateway API网关 2.1.3.RELEASE
SpringCloud -
SpringCloud Alibaba 分布式服务 2.1.0.RELEASE
SpringCloud Alibaba - Nacos 注册中心、配置中心 2.1.0.RELEASE
SpringCloud Alibaba - Sentinel 服务容错(限流、降级、熔断) 2.1.0.RELEASE
SpringCloud Alibaba - Seata 分布式事务解决方案 2.1.0.RELEASE

SpringCloud Alibaba

🌟 Nacos-discovery 注册中心

Nacos官方文档

(1)修改gulimall-commonpom.xml文件

<!-- SpringCloud-Alibaba依赖 -->
<!-- 版本对应关系如下
    SpringCloud-Alibaba2.2.5
    Spring Boot 2.2.3.Release
    SpringCloud Hoxton.SR9
-->
<dependencyManagement>
    <dependencies>
        <dependency>
            <groupId>com.alibaba.cloud</groupId>
            <artifactId>spring-cloud-alibaba-dependencies</artifactId>
            <version>2.2.5.RELEASE</version>
            <type>pom</type>
            <scope>import</scope>
        </dependency>
    </dependencies>
</dependencyManagement>


<!-- Nacos注册中心依赖 -->
<dependency>
    <groupId>com.alibaba.cloud</groupId>
    <artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
</dependency>

(2)修改各服务模块中的application.yaml,配置本地服务器地址及服务名称

  • 每个服务都应该有自己的服务名称、端口号等
spring:
  datasource:
    username: root
    password: root
    url: jdbc:mysql://192.168.56.10:3306/gulimall_sms?useUnicode=true&characterEncoding=UTF-8&serverTimezone=Asia/Shanghai
    driver-class-name: com.mysql.cj.jdbc.Driver
  cloud:
    nacos:
      discovery:
        server-addr: 127.0.0.1:8848
  application:
    name: gulimall-coupon
    
server:
  port: 7000

(3)在主启动类上,通过@EnableDiscoveryClient开启服务的注册与发现

(4)此时,通过http://127.0.0.1:8848/nacos/进入Nacos控制台查看各个服务。

🌟 Nacos-config 配置中心

(1)基本使用流程

a.在gulimall-commonpom.xml文件中导入依赖

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

b.在应用的/src/main/resources/bootstrap.properties配置文件中配置Nacos Config 服务器地址

# 指定微服务的名称。(此文件加载优先级高于application.yaml)
spring.application.name=gulimall-coupon

# 指定Nacos的配置服务器地址
spring.cloud.nacos.config.server-addr=127.0.0.1:8848

c.在Nacos的网页端新建配置列表,列表名为当前微服务名称,如gulimall-coupon.properties

d.在要使用配置的类上添加注解@RefreshScope打开动态刷新,并通过@Value("${user.name}")格式来获取配置的值

//src/main/java/com/gaowl/gulimall/coupon/controller/CouponController.java

@Value("${coupon.user.name}")
private String userName;
@Value("${coupon.user.age}")
private Integer userAge;

@RequestMapping("/test")
public R test(){
    return R.ok().put("name", userName).put("age", userAge);
}

此外,如果配置中心和当前应用的配置文件中都配置了相同的项,优先使用配置中心的配置

(2)核心概念

  • 命名空间:用于用户隔离。

    默认为public(保留空间),但可以在bootstrap.properties中通过spring.cloud.nacos.config.namespace=命名空间id指定自己所使用的命名空间。

    可以基于环境(开发、测试和生产)进行隔离,也可以每一个微服务之间相互隔离配置,每一个微服务都创建一个自己的命名空间,只加载自己的命名空间

  • 配置集:配置的集合

  • 配置集ID:也就是Data ID

  • 配置分组

    默认所有的配置集都属于DEFAULT_GROUP,可以在中通过spring.cloud.nacos.config.group=dev指定自己所需要的配置分组

在本项目中,每个微服务创建自己的命名空间,然后再使用配置分组来区分环境(dev,test,prod)。

(3)加载多配置集

将原先application.yaml中的配置信息根据类型,分成多个子配置集,然后分别在bootstrap.properties中加载

# 加载多个子配置集来构成一个完整的配置集
spring.cloud.nacos.config.extension-configs[0].data-id=datasource.yml
spring.cloud.nacos.config.extension-configs[0].group=dev
spring.cloud.nacos.config.extension-configs[0].refresh=true

spring.cloud.nacos.config.extension-configs[1].data-id=mybatis.yml
spring.cloud.nacos.config.extension-configs[1].group=dev
spring.cloud.nacos.config.extension-configs[1].refresh=true

spring.cloud.nacos.config.extension-configs[2].data-id=others.yml
spring.cloud.nacos.config.extension-configs[2].group=dev
spring.cloud.nacos.config.extension-configs[2].refresh=true

任何配置文件、任何配置项都可以放在配置中心中,只需要在bootstrap中说明要加载配置中心中的哪些配置文件即可。

此外,可以通过@Value或者@ConfigurationProperties等注解获取这些配置项

SpringCloud

🌟 Feign 声明式远程调用

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

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

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

使用步骤:

(1)引入 open-feign 依赖

<dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-openfeign</artifactId>
</dependency>

(2)编写一个接口,并通过注解告诉SpringCloud这个端口需要调用远程服务。—-> 声明接口的每一个方法都是调用哪个远程服务的哪个请求

// src/main/java/com/gaowl/gulimall/member/feign/CouponFeignService.java

package com.gaowl.gulimall.member.feign;

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

/*
* 这是一个声明式的远程调用。
* 每次调用CouponFeignService下的membercoupons方法时,会远程访问mall-coupon微服务下的/coupon/coupon/member/list
 */
@FeignClient("gulimall-coupon")
public interface CouponFeignService {

    @RequestMapping("/coupon/coupon/member/list")  // 从加上类的基础请求地址
    public R membercoupons();
}

(3)在主启动类上开启远程调用功能

@EnableFeignClients(basePackages = "com.gaowl.gulimall.member.feign")    // 扫描这个包下的远程调用

(4)测试

// src/main/java/com/gaowl/gulimall/member/controller/MemberController.java

@Autowired
CouponFeignService couponFeignService;
@RequestMapping("/coupons")
public R test(){
    MemberEntity memberEntity = new MemberEntity();
    memberEntity.setNickname("张三的李四");
    R membercoupons = couponFeignService.membercoupons();
    return R.ok().put("member", memberEntity).put("coupons", membercoupons.get("coupons"));
}

此时访问http://localhost:8000/member/member/coupons即可远程调用gulimall-coupon微服务下的相应请求。

此外,gulimall-coupon下的请求代码如下:

src/main/java/com/gaowl/gulimall/coupon/controller/CouponController.java

@RequestMapping("/member/list")
public R membercoupons(){
    CouponEntity couponEntity = new CouponEntity();
    couponEntity.setCouponName("满100减10");
    return R.ok().put("coupons", Arrays.asList(couponEntity));
}

🌟 Gateway 网关

(1)简要介绍:

网关为流量的入口,常用功能有权限校验、限流控制、路由转发、日志输出等。SpringCloud GateWay取代了原先了Zuul网关,是第二代网关框架。【官方文档

(2)基本使用:

https://start.aliyun.com/

王广帅

如果你引入了spring cloud gateway的依赖,但是又不想使用spring cloud gateway生效,可以在application.yml配置中添加spring.cloud.gateway.enabled=false

注意,Spring Cloud Gateway需要Spring BootSpring Webflux提供的Netty的运行时环境,因此,它不可以打包成war包,也不可以在传统的Servlet容器(比如tomcat)中运行。所以Spring Cloud Gateway项目中不能依赖spring-boot-starter-web,要不然会报错

(1)新建gulimall-gateway模块,加入依赖spring-cloud-starter-gatewaygulimall-common

  • 由于common中包含数据库的一些依赖,而此模块中没有使用数据库也没有数据库的配置,因此在此应用的主启动类上加入注解排除数据源依赖。@SpringBootApplication(exclude = {DataSourceAutoConfiguration.class})

(2)在主启动类上通过注解@EnableDiscoveryClient开启客户端注册发现

(3)新建配置文件bootstrap.properties

# 应用名称
spring.application.name=gulimall-gateway

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

spring.cloud.nacos.config.server-addr=127.0.0.1:8848
spring.cloud.nacos.config.namespace=a931f692-dfa9-4226-a642-0abf0d6c785c

server.port=88

(4)新建配置文件application.yml,并根据官方文档书写路由规则

spring:
  cloud:
    gateway:
      routes:
        - id: test_route
          uri: https://www.jngwl.top
          predicates:
            - Query=url,blog

        - id: bili_route
          uri: https://www.bilibili.com/
          predicates:
            - Query=url,bili

此时通过http://localhost:88/hello?url=bili接口即可访问哔哩哔哩

前端

VS Code快捷键

  • Alt + Shift + F:规范化代码格式
  • ! + 回车:生成html的基本格式等

🌟 ES6

(1)简介

ECMAScript是浏览器脚本语言的规范,而我们所熟知的js语言,如JavaScript这是规范的具体实现。

ES6是指ECMAScript 6.0,2016年提出。

(2)let

  • let声明的变量有严格的作用域,var声明的变量可以越域

    <script>
        {
            var a = 1;
            let b = 2;
        }
        console.log(a);    // 1
        console.log(b);    // ReferenceError: b is not defined
    </script>
  • let只可以声明一次,var可以声明多次

    <script>
        var m = 1;
        var m = 2;
        let n = 1;
        let n = 2;
        console.log(m);   // 2
        console.log(n);   // Identifier 'n' has already been declared
    </script>
  • let声明的变量不可以提前使用,var的提前使用会提示undefined但不会报错

    <script>
        console.log(x);  // undefined
        var x = 1;
        console.log(y);  // ReferenceError: Cannot access 'y' before initialization
        let y = 2;
    
    </script>

(3)const

const声明的为常量,不可修改,只读。

(4)解析表达式

  • 数组解构

    let arr = [1,2,3];
    let a = arr[0];     // 之前只能通过数组索引取值
    let [x,y,z] = arr;  // 现在可以对应取值
    console.log(x,y,z);
  • 对象解构

    const person = {
        name: "ha",
        age: 66,
        language: ['java', 'js', 'css']
    }
    
    const name = person.name;
    
    const {name,age,language} = person;
    console.log(name,age,language);
    
    const {name:abc,age,language} = person;   // 改变名字
    console.log(abc,age,language);

(5)字符串扩展

  • 几个API

    <script>
        let str = "hello.vue";
        console.log(str.startsWith("he"));
        console.log(str.endsWith("he"));
        console.log(str.includes("e"))
    </script>
  • 字符串模板

    相当于增强版的字符串,可以用来定义多行字符串等,还可以在字符串中加入变量和表达式。变量名或者Js表达式写在${}

    <script>
        // 多行
        let ss = `
            <div>
                <span> hjefgag <span>
            </div>
        `
        console.log(ss)
    
        // 变量和表达式
        let name = "张三";
        let age = 18;
        let info = `我是${name}, 年方${age+10}`;
        console.log(info)  // 我是张三, 年方28
    
        // 调用函数,括号不可省略,传参用
        function fun(){
            return "函数被调用"
        }
        let sss = `hhahahah ${fun()}`;
        console.log(sss)  // hhahahah 函数被调用
    
    
    </script>

(6)函数优化

  • 函数参数默认值

  • 不定参数

    function func(...values){
        console.log(values.length)
    }
    
    func(1, 2)   // 2
    func(1, 2, 3, 4)  // 4
  • 箭头函数

    const person = {
        name: "ha",
        age: 66,
        language: ['java', 'js', 'css']
    }
    
    // 箭头解构 + 解构
    var hello2 = ({name}) => console.log("hello," + name);
    hello2(person);

(7)对象优化

  • 新增API

    image-20210318103520058

  • 声明对象简写、对象函数属性简写

  • 对象拓展运算符

    拓展运算符...用于取出参数对象所有可遍历属性,然后拷贝到当前对象

(8)map和reduce

(9)Promise

有点链式编程的味道,promise封装异步操作,resolve来继续往下传,reject输出异常

提取方法进行封装,优化代码:

(10)模块化

模块化就是把代码进行拆分,方便重复利用。类似于java中的导包,而js中导入的是模块。

  • export:导出JS变量,如基本数据类型变量、函数、数组、对象等

    // 导出对象
    export const util = {
        sum(a,b){
            return a + b;
        }
    }
    
    // 导出变量
    var name = "jack"
    var age = 21
    export {name,age}
  • import:导入其他模块所提供的功能

    import util from "./hello.js"
    import {name,age} from "./user.js"
    
    util.sum(1,2);
    console.log(name)

🌟 Vue

(1)安装

# 初始化文件夹,并在此文件夹下创建package.json
npm init -y 

# 安装vue
npm install vue

(2)HelloWorld

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Document</title>
</head>
<body>

    <div id="app">
        <h1> {{name}},帅的啊 </h1>
    </div>

    <!-- 导入Vue -->
    <script src="./node_modules/vue/dist/vue.js"></script>

    <script>
        // 声明式渲染
        let vm = new Vue({
            el:"#app",
            data: {
                name: "张三"
            }
        })
    </script>

</body>
</html>

(3)双向绑定

模型和视图绑定,一个发生了改变,另一个也会跟着改变

  • 创建Vue实例,并用el绑定元素,用data封装数据,methods封装方法;之后Vue会自动将数据(data)渲染到关联的模板上
  • 取值用{{变量名}}

  • 采用v-xx等指令简化对dom的操作

一些工具:

  • VS Code 插件:Vue 3 Snippets
  • 浏览器插件:Vue.js devtools

(4)常用指令

  • 插值表达式:{{}},只能写在html标签体里面

  • v-text、v-html

    <script src="../node_modules/vue/dist/vue.js"></script>
    <div id="app">
        {{msg}} <br>
    
        <!-- v-html会渲染 html语法,而插值表达式和 v-text并不会 -->
        <span v-html="msg"></span>   
        <span v-text="msg"></span>
    </div>
    
    <script>
        new Vue({
            el: "#app",
            data: {
                msg: "<h1>Hello</h1>"
            }
        })
    </script>
  • v-bind(单向绑定)

    <script src="../node_modules/vue/dist/vue.js"></script>
    
    <!-- 给 html标签的单向属性绑定:数据变了页面变;但页面元素变了数据并不会变 -->
    <div id="app">
        <a v-bind:href="link">gogogo</a>
    
        <!-- class,style -->
        <!-- 只有{}里面的值为true时才添加对应属性 -->
        <span v-bind:class="{active:isActive, 'text-danger':hasError}"
             v-bind:style="{color: c1, fontSize: size}">你好</span>
    </div>
    
    <script>
        let vm = new Vue({
            el: "#app",
            data:{
                link: "https://www.jngwl.top",
                isActive: true,
                hasError: true,
                c1: 'red',
                size:'36px'
    
            }
        })
    </script>
  • v-model(双向绑定)

    <script src="../node_modules/vue/dist/vue.js"></script>
    
    <!-- 表单项、自定义组件 -->
    <div id="app">
        精通的语言:<br>
        <input type="checkbox" v-model="language" value="Java">Java <br>
        <input type="checkbox" v-model="language" value="Python">Python <br>
        <input type="checkbox" v-model="language" value="Matlab">Matlab <br>
    
        选中了: {{language.join(",")}}
    </div>
    
    <script>
        let vm = new Vue({
            el: "#app",
            data: {
                language: []
            }
        })
    </script>
  • v-on(为按钮绑定事件)

    下面是为两个按钮绑定了单击事件,其中一个对于num进行自增,另外一个自减。v-on:click也可以写作@click

    <!--事件中直接写js片段-->
    <button v-on:click="num++">点赞</button>
    <!--事件指定一个回调函数,必须是Vue实例中定义的函数-->
    <button @click="cancle">取消</button>

    事件的冒泡:

    <!-- 事件修饰符 -->
    <div style="border: 1px solid red;padding: 20px;" v-on:click="hello">
        大div
        <div style="border: 1px solid blue;padding: 20px;" @click="hello">
            小div <br />
            <a href="http://www.baidu.com" @click.prevent="hello">去百度</a>
        </div>
    </div>

    上面的这两个嵌套div中,如果点击了内层的div,则外层的div也会被触发;这种问题可以事件修饰符来完成:

    <!-- 事件修饰符 -->
    <div style="border: 1px solid red;padding: 20px;" v-on:click.once="hello">
        大div
        <div style="border: 1px solid blue;padding: 20px;" @click.stop="hello">
            小div <br />
            <a href="http://www.baidu.com" @click.prevent.stop="hello">去百度</a>
            <!--这里禁止了超链接的点击跳转操作,并且只会触发当前对象的操作-->
        </div>
    </div>

    关于事件修饰符

    按键修饰符

  • v-for(遍历循环)

    遍历:v-for=“(item,idx) in items”v-for=“(value,key,idx) in object”

    遍历的时候加上:key来区分不同数据,可以提高Vue渲染效率

    <script src="../node_modules/vue/dist/vue.js"></script>
    
        <div id="app">
            <ul>
    
                <li v-for="(user,idx) in users" :key="user.name">
                   当前索引:{{idx}}  ==> {{user.name}}  ==>  {{user.gender}}  ==> {{user.age}} <br>
    
                   对象信息:
                   <span v-for="(v,k,idx) in user">{{k}}=={{v}}; </span>
                </li>
            </ul>
    
            
            <ul>
                <li v-for="(num,idx) in nums" :key="idx">
    
                </li>
            </ul>
    
        </div>
    
    
        <script>
            let vm = new Vue({
                el: "#app",
                data: {
                    users:[
                        {name: 'zhangsan', gender: 'man', age:21},
                        {name: 'lisi', gender: 'man', age:22},
                        {name: 'wangwu', gender: 'man', age:23},
                        {name: 'zhaoliu', gender: 'man', age:24},
                    ],
                    nums: [1,2,3,4]
                }
            })
        </script>
  • v-if、v-show

    v-if,条件判断,当结果为true时,所在元素才会被渲染。false时,查看网页源代码不会发现此元素

    v-show,当得到的结果为true时,所在的元素才会被显示。false时,可以发现此元素

    <script src="../node_modules/vue/dist/vue.js"></script>
    
    <div id="app">
        <button @click="show= !show">快点我</button>
        <!-- v-if和v-show -->
        <h1 v-if="show">if=看到我...</h1>
        <h1 v-show="show">show=看到我</h1>
    </div>
    
    
    <script>
        let vm = new Vue({
            el: "#app",
            data: {
                show: true
            }
        })
    </script>

  • v-else、v-else-if
    <script src="../node_modules/vue/dist/vue.js"></script>
    
    <div id="app">
        <button @click="random=Math.random()">快点我</button>
        <span>{{random}}</span>
    
        <h1 v-if="random>=0.75">看到我了 &gt;0.75</h1>
        <h1 v-else-if="random>=0.5">看到我了 &gt;0.5</h1>
        <h1 v-else-if="random>=0.2">看到我了 &gt;0.2</h1>
        <h1 v-else>看到我了 &gt;0</h1>
    
    </div>
    
    
    <script>
        let vm = new Vue({
            el: "#app",
            data: {
                random: 1
            }
        })
    </script>

(5)计算属性和监听器

(6)过滤器

过滤器常用来处理文本格式化的操作。过滤器可以用在两个地方:双花括号插值v-bind表达式

<div id="app">
    <ul>
        <li v-for="user in userList">
            {{user.id}} ==> {{user.name}} ==> {{user.gender == 1?"男":"女"}} ==>
            {{user.gender | genderFilter}} ==> {{user.gender | gFilter}}
            <!-- 这里的"|"表示的管道,将user.gender的值交给genderFilter -->
        </li>
    </ul>
</div>

<script src="../node_modules/vue/dist/vue.js"></script>

<script>
    // 全局过滤器 
    Vue.filter("gFilter", function (val) {
        if (val == 1) {
            return "男~~~";
        } else {
            return "女~~~";
        }
    })

    let vm = new Vue({
        el: "#app",
        data: {
            userList: [
                { id: 1, name: 'jacky', gender: 1 },
                { id: 2, name: 'peter', gender: 0 }
            ]
        },
        filters: {
            // 局部过滤器,只可以在当前vue实例中使用
            genderFilter(val) {
                if (val == 1) {
                    return "男";
                } else {
                    return "女";
                }
            }
        }
    })
</script>

(6)组件化

<div id="app">
    <button v-on:click="count++">我被点击了 {{count}} 次</button>

    <counter></counter>
    <counter></counter>
    <counter></counter>
    <counter></counter>
    <counter></counter>
    <!-- 使用所定义的组件button-counter -->
    <button-counter></button-counter>
</div>
<script src="../node_modules/vue/dist/vue.js"></script>


<script>
    //1、全局声明注册一个组件
    Vue.component("counter", {
        template: `<button v-on:click="count++">我被点击了 {{count}} 次</button>`,
        data() {
            return {
                count: 1
            }
        }
    });

    //2、局部声明一个组件
    const buttonCounter = {
        template: `<button v-on:click="count++">我被点击了 {{count}} 次~~~</button>`,
        data() {
            return {
                count: 1
            }
        }
    };

    new Vue({
        el: "#app",
        data: {
            count: 1
        },
        components: {
            //2.声明所定义的局部组件
            'button-counter': buttonCounter
        }
    })
</script>

(7)生命周期钩子函数

生命周期就是指一个对象从创建到销毁的过程。

Vue 实例生命周期

<div id="app">
    <span id="num">{{num}}</span>
    <button @click="num++">赞!</button>
    <h2>{{name}},有{{num}}个人点赞</h2>
</div>

<script src="../node_modules/vue/dist/vue.js"></script>

<script>
    let app = new Vue({
        el: "#app",
        data: {
            name: "张三",
            num: 100
        },
        methods: {
            show() {
                return this.name;
            },
            add() {
                this.num++;
            }
        },
        beforeCreate() {
            console.log("=========beforeCreate=============");
            console.log("数据模型未加载:" + this.name, this.num);
            console.log("方法未加载:" + this.show());
            console.log("html模板未加载:" + document.getElementById("num"));
        },
        created: function () {
            console.log("=========created=============");
            console.log("数据模型已加载:" + this.name, this.num);
            console.log("方法已加载:" + this.show());
            console.log("html模板已加载:" + document.getElementById("num"));
            console.log("html模板未渲染:" + document.getElementById("num").innerText);
        },
        beforeMount() {
            console.log("=========beforeMount=============");
            console.log("html模板未渲染:" + document.getElementById("num").innerText);
        },
        mounted() {
            console.log("=========mounted=============");
            console.log("html模板已渲染:" + document.getElementById("num").innerText);
        },
        beforeUpdate() {
            console.log("=========beforeUpdate=============");
            console.log("数据模型已更新:" + this.num);
            console.log("html模板未更新:" + document.getElementById("num").innerText);
        },
        updated() {
            console.log("=========updated=============");
            console.log("数据模型已更新:" + this.num);
            console.log("html模板已更新:" + document.getElementById("num").innerText);
        }
    });
</script>

(8)模块化开发

  • 全局安装webpack:npm install webpack -g
  • 全局安装vue脚手架:npm install -g @vue/cli-init

  • 初始化vue项目:vue init webpack projectname

    • 遇到错误的解决方案:CSDN

(9)ElementUI

官方文档

四、商品服务

阿里云 对象存储


本博客所有文章除特别声明外,均采用 CC BY-SA 4.0 协议 ,转载请注明出处!