商城项目
本文最后更新于: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等服务模块
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-fast
的src/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 注册中心
(1)修改gulimall-common
的pom.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-common
的pom.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)基本使用:
王广帅:
如果你引入了
spring cloud gateway
的依赖,但是又不想使用spring cloud gateway
生效,可以在application.yml
配置中添加spring.cloud.gateway.enabled=false
。注意,
Spring Cloud Gateway
需要Spring Boot
和Spring Webflux
提供的Netty
的运行时环境,因此,它不可以打包成war
包,也不可以在传统的Servlet容器(比如tomcat)中运行。所以Spring Cloud Gateway项目中不能依赖spring-boot-starter-web
,要不然会报错
(1)新建gulimall-gateway
模块,加入依赖spring-cloud-starter-gateway
及gulimall-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
声明对象简写、对象函数属性简写
对象拓展运算符
拓展运算符
...
用于取出参数对象所有可遍历属性,然后拷贝到当前对象
(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">看到我了 >0.75</h1> <h1 v-else-if="random>=0.5">看到我了 >0.5</h1> <h1 v-else-if="random>=0.2">看到我了 >0.2</h1> <h1 v-else>看到我了 >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)生命周期钩子函数
生命周期就是指一个对象从创建到销毁的过程。
<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 协议 ,转载请注明出处!