尚硅谷微服务分布式电商项目《谷粒商城》基础篇
谷粒商城是一个微服务项目,总体上分为前台购物模块和后台数据管理模块。其中后台管理系统基于SpringBoot + SpringCloud + SpringCloud Alibaba + MyBatis-Plus 实现,包括:系统管理、商品系统、优惠营销、库存系统、订单系统、用户系统、内容管理等七大模块。本文基于对《谷粒商城》基础篇(后台数据管理模块)的学习,对其中所应用到的相关技术及知识点进行总结。
1.分布式基础概念
本项目主要涉及微服务、注册中心、配置中心、远程调用、Feign、网关等相关分布式基础概念
1.1微服务
微服务是一种用于构建应用的架构方案。微服务架构有别于更为传统的单体式方案,可将应用拆分成多个核心功能。每个功能都被称为一项服务,可以单独构建和部署,这意味着各项服务在工作(和出现故障)时不会相互影响。
微服务最大的特点就是独立、自治。在本项目中每一个不同功能的项目都创建了它自己的服务,不同功能的项目可以实现并行开发、互不影响。
1.2注册中心和配置中心
注册中心可以说是微服务架构中的”通讯录“,它记录了服务和服务地址的映射关系。在分布式架构中,服务会注册到这里,当服务需要调用其它服务时,就这里找到服务的地址,进行调用。
配置中心:顾名思义将配置中心化,说白了就是将配置从应用中抽取出来,统一管理,优雅的解决了配置的动态变更、权限管理、持久化、运维成本等问题。
本项目中使用Nacos作为服务的注册中心和配置中心,便于服务间的互相调用和配置的统一管理。
Nacos的使用
-
引入相关依赖
<!--服务的注册/发现--> <dependency> <groupId>com.alibaba.cloud</groupId> <artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId> </dependency> <!--配置中心来做配置管理--> <dependency> <groupId>com.alibaba.cloud</groupId> <artifactId>spring-cloud-starter-alibaba-nacos-config</artifactId> </dependency>
-
添加配置文件
spring.application.name=gulimall-product 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=668f4a9f-acea-4bd9-9ee4-cba0a8d98806
-
启动类添加相关注解
@EnableDiscoveryClient
1.3远程调用
远程调用:在服务开发期间存在远程调用的场景,在本项目中服务的远程调用是由Feign来实现的。而使用Feign进行远程调用的前提则是服务被注册到注册中心。
Feign的使用
-
引入相关依赖
<dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-openfeign</artifactId> </dependency>
-
添加相关注解
@EnableFeignClients(basePackages="包名")
-
编写远程调用类并指定远程调用的服务名和路径
@FeignClient("gulimall-coupon") public interface CouponFeignService { //找到gulimall-coupon服务,给/coupon/spubounds/save发送请求。 @PostMapping("/coupon/spubounds/save") R saveSpuBounds(@RequestBody SpuBoundTo spuBoundTo); }
-
在需要远程调用的方法中调用该方法
R r = couponFeignService.saveSpuBounds(spuBoundTo);
1.4 API 网关
API 网关是一个反向路由,屏蔽内部细节,为调用者提供统一入口,接收所有调用者请求,通过路由机制转发到服务实例。
本项目中使用Gateway作为网关,所有的请求都发送给网关,由网关代理给其他服务。我们可以在网关处做很多统一的处理,如:统一的跨域解决。
Gateway使用
-
创建Gateway服务
-
将服务注册到注册中心
spring.application.name=gulimall-gateway spring.cloud.nacos.discovery.server-addr=127.0.0.1:8848
-
配置请求转发
spring: cloud: gateway: routes: - id: product_route uri: lb://gulimall-product predicates: - Path=/api/product/** filters: - RewritePath=/api/(?<segment>.*),/$\{segment}
-
其他统一处理(如:统一跨域处理)
@Configuration public class GulimallCorsConfiguration { @Bean public CorsWebFilter corsWebFilter(){ UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource(); CorsConfiguration corsConfiguration = new CorsConfiguration(); //1.配置跨域 corsConfiguration.addAllowedHeader("*"); corsConfiguration.addAllowedMethod("*"); corsConfiguration.addAllowedOrigin("*"); corsConfiguration.setAllowCredentials(true); source.registerCorsConfiguration("/**",corsConfiguration); return new CorsWebFilter(source); } }
2.基础开发
在对后台管理系统的开发中我们使用SpringBoot 对项目进行搭建,简单使用了SpringCloud 的Nacos、Feign 、Gateway 实现项目的注册与发现、配置管理、请求转发等。前端代码则是基于Vue组件化开发,对于图片的存储我们使用了第三方服务——阿里云对象存储。
2.1 阿里云对象存储OSS
阿里云对象存储服务(Object Storage Service,简称OSS),是阿里云提供的海量、安全、低成本、高可靠的云存储服务。
即开即用,无需维护,按量收费。
资源术语:
存储空间(Bucket):存储空间是用户用于存储对象(Object)的容器,所有的对象都必须隶属于某个存储空间。
对象(Object):对象是OSS存储数据的基本单元,也被称为OSS的文件。对象由元信息(Object Meta),用户数据(Data)和文件名(Key)组成,并且由存储空间内部唯一的Key来标识。
Region(地域):Region表示OSS的数据中心所在物理位置。
Endpoint(访问域名):Endpoint表示OSS对外服务的访问域名。OSS以HTTP RESTful API的形式对外提供服务,当访问不同的Region的时候,需要不同的域名。通过内网和外网访问同一个Region所需要的Endpoint也是不同的。例如杭州Region的外网Endpoint是oss-cn-hangzhou.aliyuncs.com,内网Endpoint是oss-cn-hangzhou-internal.aliyuncs.com。
AccessKey(访问密钥):AccessKey简称AK,指的是访问身份验证中用到的AccessKeyId和AccessKeySecret。OSS通过使用AccessKeyId和AccessKeySecret对称加密的方法来验证某个请求的发送者身份。AccessKeyId用于标识用户;AccessKeySecret是用户用于加密签名字符串和OSS用来验证签名字符串的密钥,必须保密。
图片上传方式:
-
普通上传方式
-
服务端签名后上传
本项目选择服务端签名后上传的方式实现图片的存储功能。即保障了安全性,又避免给服务器造成过多的压力。
具体步骤:
-
开启RAM访问控制,创建拥有指定权限的子账户,使用子账户进行图片上传操作。
-
引入依赖
<dependency> <groupId>com.alibaba.cloud</groupId> <artifactId>spring-cloud-starter-alicloud-oss</artifactId> <version>2.2.0.RELEASE</version> </dependency>
-
编写相关配置
spring: cloud: alicloud: secret-key: XXXXXXXXXXXXXXXXXX access-key: XXXXXXXXXXXXXXXXXX oss: endpoint: oss-cn-hangzhou.aliyuncs.com bucket: XXXXXXXXXXX
-
按照OSS 的java SDK 编写controller
@RestController public class OssController { @Autowired OSS ossClient; // @Value("${spring.cloud.alicloud.oss.endpoint}") private String endpoint; @Value("${spring.cloud.alicloud.oss.bucket}") private String bucket; @Value("${spring.cloud.alicloud.access-key}") private String accessId; @Value("${spring.cloud.alicloud.secret-key}") private String accessKeySecret; @RequestMapping("/oss/policy") public R policy(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { String host = "https://" + bucket + "." + endpoint; // host的格式为 bucketname.endpoint // callbackUrl为 上传回调服务器的URL,请将下面的IP和Port配置为您自己的真实信息。 //String callbackUrl = "http://88.88.88.88:8888"; String format = new SimpleDateFormat("yyyy-MM-dd").format(new Date()); String dir = format+"/"; // 用户上传文件时指定的前缀。 Map<String, String> respMap=null; try { long expireTime = 30; long expireEndTime = System.currentTimeMillis() + expireTime * 1000; Date expiration = new Date(expireEndTime); // PostObject请求最大可支持的文件大小为5 GB,即CONTENT_LENGTH_RANGE为5*1024*1024*1024。 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); 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)); } catch (Exception e) { // Assert.fail(e.getMessage()); System.out.println(e.getMessage()); } finally { ossClient.shutdown(); } return R.ok().put("data",respMap); } }
-
vue编写代码完成图片上传
3.环境
在环境的部署上,采用了Linux+Docker的方式,使用Docker来部署MySQL,在项目开发中我们使用了逆向工程&人人开源为我们快速生成基础的CURD的Controller、Service、Dao层代码以及Vue页面层的代码,开发时专注于复杂逻辑的处理即可。
3.1 Vagrant 快速构建虚拟开发环境
Vagrant是一款用于构建及配置虚拟开发环境的软件,基于Ruby,主要以命令行的方式运行。
-
下载Windows 64 版本的vagrant并安装
-
打开window cmd窗口,运行 vagrant init centos/7 即可初始化一个centos7系统
-
运行 vagrant up 启动虚拟环境
-
运行 vagrant ssh 使用vagrant账户连接虚拟机,直接在cmd窗口编写linux命令。
-
运行 exit 退出与虚拟机的连接
-
修改Vagrantfile文件中关于网络的配置 设置虚拟机ip
config.vm.network"private_network",ip:“192.168.56.10”
3.2 安装Docker
Docker:虚拟化容器技术。基于镜像,可以秒级启动各种容器。每一种容器都是一个完整的运行环境,容器之间相互隔离。
参考网址:https://docs.docker.com/engine/install/centos/
-
安装docker
-
卸载Docker
sudo yum remove docker \ docker-client \ docker-client-latest \ docker-common \ docker-latest \ docker-latest-logrotate \ docker-logrotate \ docker-engine
-
安装依赖的包
sudo yum install -y yum-utils \ device-mapper-persistent-data \ lvm2
-
配置docker的地址
sudo yum-config-manager \ --add-repo \ https://download.docker.com/linux/centos/docker-ce.repo
-
安装docker
sudo yum install docker-ce docker-ce-cli containerd.io
-
-
启动docker
sudo systemctl start docker
-
设置docker 开机自启动
sudo systemctl enable docker
-
配置阿里云镜像加速
sudo mkdir -p/etc/docker sudo tee /etc/docker/daemon.json <<-'EOF' { "registry-mirrors":["https://82m9ar63.mirror.aliyuncs.com"] } EOF sudo systemctl daemon-reload sudo systemctl restart docker
3.3 Docker配置MySQL
- 下载镜像文件
sudo docker pull mysql:5.7
-
创建实例并启动
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端口
-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 :初始化root用户的密码为root
-
配置utf-8编码
vi /mydata/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 -
重启容器
docker restart mysql
4.开发规范
对于数据的新增与修改使用JSR303进行数据校验,配置全局的异常处理、统一返回和跨域处理,将项目中固定的状态使用枚举进行编写。项目中还使用了Mybatis-Plus的逻辑删除功能。
4.1 JSR303数据校验
-
给Bean添加校验注解:javax.validation.constraints,并定义自己的message提示
-
开启校验功能@Valid
效果:校验错误以后会有默认的响应;@RequestMapping("/save") @RequiresPermissions("product:brand:save") public R save(@Validated({AddGroup.class}) @RequestBody BrandEntity brand){ brandService.save(brand); return R.ok(); }
-
给校验的bean后紧跟一个BindingResult,就可以获取到校验的结果
-
分组校验(多场景的复杂校验)
- @NotBlank(message = “品牌名必须提交”,groups = {AddGroup.class,UpdateGroup.class})
给校验注解标注什么情况需要进行校验 - @Validated({AddGroup.class})
- 默认没有指定分组的校验注解@NotBlank,在分组校验情况@Validated({AddGroup.class})下不生效,只会在@Validated生效;
- @NotBlank(message = “品牌名必须提交”,groups = {AddGroup.class,UpdateGroup.class})
-
自定义校验
- 编写一个自定义的校验注解
- 编写一个自定义的校验器 ConstraintValidator
- 关联自定义的校验器和自定义的校验注解
@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 { }; }
4.2 Mybatis-Plus逻辑删除功能
-
配置全局的逻辑删除规则(可省略)
mybatis-plus: global-config: db-config: id-type: auto logic-delete-value: 1 # 逻辑已删除值(默认为 1) logic-not-delete-value: 0 # 逻辑未删除值(默认为 0)
-
给Bean加上逻辑删除注解@TableLogic
/**配置特定的逻辑删除状态 * 是否显示[0-不显示,1显示] */ @TableLogic(value = "1",delval = "0") private Integer showStatus;
4.3 统一的异常处理
- 编写异常处理类,使用@ControllerAdvice。
- 使用@ExceptionHandler标注方法可以处理的异常。
- 例如对数据校验做统一的异常处理:
@Slf4j
@RestControllerAdvice(basePackages = "com.jiaozi.gulimall.product.controller")
public class GulimallExceptionControllerAdvice {
@ExceptionHandler(value= MethodArgumentNotValidException.class)
public R handleVaildException(MethodArgumentNotValidException e){
log.error("数据校验出现问题{},异常类型:{}",e.getMessage(),e.getClass());
BindingResult bindingResult = e.getBindingResult();
Map<String,String> errorMap = new HashMap<>();
bindingResult.getFieldErrors().forEach((fieldError)->{
errorMap.put(fieldError.getField(),fieldError.getDefaultMessage());
});
return R.error(BizCodeEnume.VAILD_EXCEPTION.getCode(),BizCodeEnume.VAILD_EXCEPTION.getMsg()).put("data",errorMap);
}
}