Search K
Appearance
Appearance
网关作为流量的入口,常用的功能包括路由转发、权限校验、限流控制等。而SpringCloud Gateway
作为SpringCloud
官方推出的第二代网关框架,取代了Zuul
网关。
网关提供 API
全托管服务,丰富的 API
管理功能,辅助企业管理大规模的 API
,以降低管理成本和安全风险,包括协议适配、协议转发、安全策略、防刷、流量、监控日志等功能。
Spring Cloud Gateway
旨在提供一种简单而有效的方式来对 API
进行路由,并为他们提供切面,例如:安全性,监控/指标 和弹性等。
官方文档地址较慢,这里的地址是中文官方文档 官方中文文档地址
需要了解的术语
Route(路由):网关的基本构件。它由一个ID、一个目的地URI、一个断言(Predicate)集合和一个过滤器(Filter)组成定义。如果集合断言为真,则路由被匹配。
Predicate(断言):这是一个 Java 8 Function Predicate。输入类型是 Spring Framework ServerWebExchange。这让你可以在HTTP请求中的任何内容上进行匹配,比如header或查询参数。
Filter(过滤器):这些是 GatewayFilter
的实例,已经用特定工厂构建。在这里,你可以在发送下游请求之前或之后修改请求和响应。
TIP
一句话总结
当我们请求到达网关时,网关先利用断言判断我们这次请求是否符合某个路由规则,如果符合了就按照路由规则,如果符合了就按照这个路由规则将其路由到指定地方,去这个指定的地方就需要经过一系列过滤器过滤。
基于 Spring5
,支持响应式编程和 SpringBoot2.0
支持使用任何请求属性进行路由匹配
特定于路由的断言和过滤器
集成 Hystrix
进行断路保护
集成服务发现功能
易于编写 Predicates
和 Filters
支持请求速率限制
支持路径重写
接下来需要为项目配置API网关,我们首先新建一个module,使用spring初始化工具
搜索gateway选中网关
修改pom文件,首先统一springboot版本
然后该网关模块同样需要依赖于gulimall-common模块,gulimall-common工程中引入了服务的配置中心和注册/发现,网关服务也需要将自己的服务注册到nacos配置中心中去,同时也需要发现其他服务的位置,这样才可以将请求路由到指定位置
目前pom文件配置
gulimall-member/pom.xml
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.3.12.RELEASE</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
<groupId>com.atguigu.gulimall</groupId>
<artifactId>gulimall-gateway</artifactId>
<version>0.0.1-SNAPSHOT</version>
<name>gulimall-gateway</name>
<description>API网关</description>
<properties>
<java.version>17</java.version>
<spring-cloud.version>Hoxton.SR12</spring-cloud.version>
</properties>
<dependencies>
<dependency>
<groupId>com.atguigu.gulimall</groupId>
<artifactId>gulimall-common</artifactId>
<version>0.0.1-SNAPSHOT</version>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-gateway</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-dependencies</artifactId>
<version>${spring-cloud.version}</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
</project>
接下来测试网关服务,首先启动类需要开启服务注册发现
com.atguigu.gulimall.gateway.GulimallGatewayApplication
package com.atguigu.gulimall.gateway;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.client.discovery.EnableDiscoveryClient;
/**
* 1、开启服务注册和发现中心
* 2、配置nacos地址
*/
@EnableDiscoveryClient
@SpringBootApplication
public class GulimallGatewayApplication {
public static void main(String[] args) {
SpringApplication.run(GulimallGatewayApplication.class, args);
}
}
开启可服务注册发现中后,还需要在bootstrap.properties中配置nacos的地址,
TIP
在前面nacos进阶章节讲到,nacos配置最佳实践是以命名空间区分微服务,以分组(group)区分服务运行环境
接下来我们在nacos为网关服务创建一个命名空间
新增一个配置
新建bootstrap.properties配置文件并配置应用名称、nacos地址以及命名空间,同时修改下启动端口为88端口
gulimall-gateway/src/main/resources/bootstrap.properties
spring.application.name=gulimall-gateway
spring.cloud.nacos.discovery.server-addr=101.43.17.174:8848
spring.cloud.nacos.config.namespace=edea443c-49ad-4379-bb9d-f04c77df1af1
server.port=88
接下来启动网关服务测试下,发现报数据库相关的异常,提示没有放在指定的类路径下,这是由于我们gulimall-gateway服务依赖于gulimall-common模块,common模块中引入了mybatis-plus相关的依赖,但是目前网关服务还不需要操作到数据库。
解决这个异常的办法有两个:
1、排除mybatis-plus相关的依赖配置
2、最简单的可以直接排除与数据库相关的配置
com.atguigu.gulimall.gateway.GulimallGatewayApplication
package com.atguigu.gulimall.gateway;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.autoconfigure.jdbc.DataSourceAutoConfiguration;
import org.springframework.cloud.client.discovery.EnableDiscoveryClient;
/**
* 1、开启服务注册和发现中心
* 2、配置nacos地址
*/
@EnableDiscoveryClient
@SpringBootApplication(exclude = {DataSourceAutoConfiguration.class})
public class GulimallGatewayApplication {
public static void main(String[] args) {
SpringApplication.run(GulimallGatewayApplication.class, args);
}
}
服务成功启动
目前访问88端口服务还是404,这是因为我们还未配置网关规则
接下来配置一个简单的API网关例子,要求如下:
当我们访问例如:http://xxxx.xxxx.xx?url=qq
判断路径中的query参数,即url的参数,如果是qq就跳转到https://www.qq.com,如果是baidu就跳转到https://www/baidu.com,下面使用SpringGateway来实现这个功能
我们新建一个application.yml来配置网关
我们可以查看官方中文文档中关于Query的配置
gulimall-gateway/src/main/resources/application.yml
spring:
cloud:
gateway:
routes:
- id: qq_route
uri: https://www.qq.com
predicates:
- Query=url,qq
- id: baidu_route
uri: https://www.baidu.com
predicates:
- Query=url,baidu
配置完成后重启项目
可以发现,当我们请求地址query中url为qq时会跳转到www.qq.com,而url为baidu时会跳转到baidu.com
接下来正式进入业务编写环节
既然是商城系统,首先我们编写商城三级分类相关的API
简单分析一下商品分类表字段
这里可以重点关注下parent_cid字段,由于分类都在同一个表,parent_cid代表夫级分类id,product_unit和product_count字段分别代表分类单位和数量,接下来我们导入一些提前录入的数据
新建一个查询,并执行sql文件,执行后成功导入了1000条数据
接下来我们需要编写一个查询所有分类以及子分类列表的解接口,最终返回一个树形结构的数组,交由前端实现分类功能
首先我们观察Category实体类,发现并没有一个字段来保存其关联的子分类,谷粒商城视频中是直接在实体类中添加字段,这样的做法并不推荐,不利于后期的维护,所以我们采用定义一个dto类进行扩展,添加一个children字段
我们添加一个dto包并添加CategoryDto类
com.atguigu.gulimall.product.dto.CategoryDto
package com.atguigu.gulimall.product.dto;
import com.atguigu.gulimall.product.entity.CategoryEntity;
import lombok.Data;
import java.util.ArrayList;
import java.util.List;
@Data
public class CategoryDto extends CategoryEntity {
// 扩展children字段,以便于保存子分类
private List<CategoryEntity> children = new ArrayList<>();
}
然后在CategoryController类中添加一个接口,核心方法是listWithTree
com.atguigu.gulimall.product.controller.CategoryController
/**
* 查出所有分类以及子分类,以树形结构组装起来
* @return
*/
@RequestMapping("/list/tree")
public R listWithTree(){
/**
* 在categoryService上扩展一个listWithTree方法来实现该功能
*/
List<CategoryDto> categoryDtoList = categoryService.listWithTree();
return R.ok().put("data",categoryDtoList);
}
接下来我们在CategoryService中实现该方法listWithTree
添加接口 com.atguigu.gulimall.product.service.CategoryService
package com.atguigu.gulimall.product.service;
import com.atguigu.gulimall.product.dto.CategoryDto;
import com.baomidou.mybatisplus.extension.service.IService;
import com.atguigu.common.utils.PageUtils;
import com.atguigu.gulimall.product.entity.CategoryEntity;
import java.util.List;
import java.util.Map;
/**
* 商品三级分类
*
* @author zhaochao
* @email 1798231822@qq.com
* @date 2023-05-13 16:54:34
*/
public interface CategoryService extends IService<CategoryEntity> {
PageUtils queryPage(Map<String, Object> params);
List<CategoryDto> listWithTree();
}
添加实现类 com.atguigu.gulimall.product.service.impl.CategoryServiceImpl
package com.atguigu.gulimall.product.service.impl;
import com.atguigu.gulimall.product.dto.CategoryDto;
import org.springframework.beans.BeanUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.baomidou.mybatisplus.core.metadata.IPage;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.atguigu.common.utils.PageUtils;
import com.atguigu.common.utils.Query;
import com.atguigu.gulimall.product.dao.CategoryDao;
import com.atguigu.gulimall.product.entity.CategoryEntity;
import com.atguigu.gulimall.product.service.CategoryService;
@Service("categoryService")
public class CategoryServiceImpl extends ServiceImpl<CategoryDao, CategoryEntity> implements CategoryService {
@Override
public PageUtils queryPage(Map<String, Object> params) {
IPage<CategoryEntity> page = this.page(
new Query<CategoryEntity>().getPage(params),
new QueryWrapper<CategoryEntity>()
);
return new PageUtils(page);
}
/**
* 获取组装成树形结构的分类列表
* @return
*/
@Override
public List<CategoryDto> listWithTree() {
List<CategoryDto> categoryDtoList = new ArrayList<>();
// 1、查询所有分类
List<CategoryEntity> categoryList = baseMapper.selectList(null);
// 找出一级分类以及进行排序
List<CategoryEntity> filterList = (List<CategoryEntity>) categoryList.stream().filter(categoryEntity -> categoryEntity.getParentCid() == 0).sorted((menu1,menu2) -> {
return (menu1.getSort()==null?0:menu1.getSort()) - (menu2.getSort()==null?0:menu2.getSort());
}).collect(Collectors.toList());
// 2、组装成父子树形结构
filterList.stream().map(categoryEntity -> {
CategoryDto categoryDto = new CategoryDto();
BeanUtils.copyProperties(categoryEntity,categoryDto);
// 核心点
// 遍历递归组装当前实体的children字段
categoryDto.setChildren(this.getChildren(categoryDto,categoryList));
categoryDtoList.add(categoryDto);
return categoryEntity;
}).collect(Collectors.toList());
return categoryDtoList;
}
/**
* 传入一个CategoryDto实体,递归将该实体中的children组合成树形结构
* @param root
* @param all
* @return
*/
public List<CategoryDto> getChildren(CategoryDto root,List<CategoryEntity> all){
List<CategoryDto> categoryDtoList = new ArrayList<>();
// 找出指定分类以及进行排序
List<CategoryEntity> filterList = (List<CategoryEntity>) all.stream().filter(categoryEntity -> categoryEntity.getParentCid() == root.getCatId()).sorted((menu1,menu2) -> {
return (menu1.getSort()==null?0:menu1.getSort()) - (menu2.getSort()==null?0:menu2.getSort());
}).collect(Collectors.toList());
// 2、组装成父子树形结构
filterList.stream().map(categoryEntity -> {
CategoryDto categoryDto = new CategoryDto();
BeanUtils.copyProperties(categoryEntity,categoryDto);
categoryDto.setChildren(getChildren(categoryDto,all));
categoryDtoList.add(categoryDto);
return categoryEntity;
}).collect(Collectors.toList());
return categoryDtoList;
}
}
然后我们启动服务,并且浏览器进行访问,查看数据是否已经正常拼装为树形结构:
接下来我们需要在后台管理系统中新增一个商品服务菜单,并新增一个分类管理的后台页面,首先我们启动后台管理前端项目renren-fast-vue以及对应的后台管理服务renren-fast
添加一个商品服务目录
添加一个商品分类管理菜单
renren-fast-vue后台约定的规范是,例如访问的页面地址是sys-role,那么对应的vue文件就在sys目录下的role.vue
新建一个product目录并且添加一个category.vue文件,加一点提示信息
可以看到页面访问正常
接下来我们需要完成商品分类管理的页面功能,该功能需要调用到gulimall-product的product/category/list/tree接口服务来获取分类数据完成页面,目前项目中的请求是直接请求到本地的renren-fast服务即http://localhost:8080/renren-fast,如果在前端添加一个新的请求地址到gulimall-product的服务太麻烦了
这里我们统一通过SrpingGateway来配置网关服务,使用网关服务来统一的管理应用中发起的请求,前端项目只需要配置指定的前缀,网关服务根据前缀进行断言,然后路由到指定的服务
首先将renren-fast的服务添加到nacos服务注册发现中心
首先先引入公共服务模块 renren-fast/pom.xml
<!--添加公共服务模块 -->
<dependency>
<groupId>com.atguigu.gulimall</groupId>
<artifactId>gulimall-common</artifactId>
<version>0.0.1-SNAPSHOT</version>
</dependency>
启动类添加nacos服务发现注解 io.renren.RenrenApplication
/**
* Copyright (c) 2016-2019 人人开源 All rights reserved.
*
* https://www.renren.io
*
* 版权所有,侵权必究!
*/
package io.renren;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.client.discovery.EnableDiscoveryClient;
@EnableDiscoveryClient
@SpringBootApplication
public class RenrenApplication {
public static void main(String[] args) {
SpringApplication.run(RenrenApplication.class, args);
}
}
在renren-fast的pom文件中添加nacos相关配置 renren-fast/src/main/resources/application.yml
application:
name: renren-fast
cloud:
nacos:
discovery:
server-addr: 101.43.17.174:8848
接下来重新启动renern-fast服务,发现服务启动报错了
原因:renren-fast中使用的springboot版本与gulimall-common中引入的nacos版本不一致,即springboot版本对应的spring-cloud版本不对
renren-fast使用的版本是2.6.6
解决办法:
同意springboot版本和其他模块保持一致,同时找到对应的spring-cloud版本,具体参考
网上搜到将其改为allowedOrigins即可
此时再重启renren-fast服务已经可以正常启动,我们查看nacos后台,此时服务也已经正常注册
renren-fast-vue前端项目中原先的请求地址是:http://localhost:8080/renren-fast,可以发现有一个统一的前缀renren-fast
我们修改前端项目中请求地址,使其统一发送到网关服务88端口,然后约定一个路由规则,约定/api前缀的请求路由到renren-fast服务,即http://localhost:8080/renrne-fast地址
最终的网关配置:
gulimall-gateway/src/main/resources/application.yml
spring:
cloud:
gateway:
routes:
- id: qq_route
uri: https://www.qq.com
predicates:
- Query=url,qq
- id: baidu_route
uri: https://www.baidu.com
predicates:
- Query=url,baidu
- id: admin_route
uri: lb://renren-fast
predicates:
- Path=/api/**
filters:
- RewritePath=/api/(?<segment>.*),/renren-fast/$\{segment}
前端地址修改完毕,接下来配置网关路由,首先明确我们的目标,我们希望:
以 http://localhost:88/api 开头的请求全都路由到 http://localhost:8080/renren-fast 目标
lb是负载均衡的意思
我们使用断言中的Path断言,断言请求中携带/api
我们可以通过设置filtes的路径重写来实现该功能
可以看到网关配置成功生效,由之前的http://localhost:8080/renren-fast 替换为现在的 http://localhost:88/api 的地址后请求依然正常。
但当我们点击登录时可以发现接口请求报错,浏览器提示跨域错误,这是因为我们前端项目运行在8001端口,而要访问到http://localhost:88端口,不符合浏览器的同源策略,所以出现了跨域错误
并且这里谈到了OPTIONS请求
这对老前端来说是老生常谈的话题了,这里想要解决跨域最常见得到办法是使用nginx进行反向代理,将项目运行ip和目标请求地址处于同一域下面,这里我们为了开发方便使用第二种方法,告知浏览器允许跨域。
后续我们所有请求都会先经过网关服务,所以我们只需要同一在网关服务配置跨域即可
添加一个设置cors的跨域配置类 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(){
UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
CorsConfiguration corsConfiguration = new CorsConfiguration();
// 1、配置跨域
// 允许哪些请求头进行跨域
corsConfiguration.addAllowedHeader("*");
// 运行哪些请求方式进行跨域
corsConfiguration.addAllowedMethod("*");
// 允许哪些请求源进行跨域
corsConfiguration.addAllowedOrigin("*");
// 是否允许携带cookie进行跨域
corsConfiguration.setAllowCredentials(true);
source.registerCorsConfiguration("/**",corsConfiguration);
return new CorsWebFilter(source);
}
}
这里我们进行配置后重启网关服务,发现还是存在跨域,这里报错的意思是说允许跨域的头部设置了多个值,但是只能允许一个,这是因为我们在网关设置跨域后,原本的renren-fast后台服务可能也设置了跨域,可能产生了冲突
我们将renren-fast中配置的cors注释,然后重启renren-fast
可以看到我们已经可以正常登录了
思考如下问题?
1、为什么需要配置网关?
答:因为我们的后台管理系统默认请求地址是对应的后台服务renren-fast,但是实际开发中我们后台管理系统需要请求到不同的微服务,例如商品服务,订单服务等,它们的端口都是不一样的,这样当我们想请求其他微服务时就需要配置多个不同服务的的端口,维护起来十分的恶心。现在我们可以换个思路,我们可以将所有请求都发送到网关服务,由网关服务同一进行中转,我们只需要配置请求前缀即可,这样项目可维护性会极大的提升,我们无需在项目中配置多个服务的地址,只需要将服务统一请求到网关,又网关帮我们路由到目标地址。
spring:
cloud:
gateway:
routes:
- id: admin_route
uri: lb://renren-fast
predicates:
- Path=/api/**
filters:
- RewritePath=/api/(?<segment>.*),/renren-fast/$\{segment}
分析一下网关配置,其中lb是代表负载均衡的意思,然后uri是指需要路由到的目标服务,是注册在nacos中的服务名称,其中predicates代表断言规则,这里的意思是以/api/为前缀就路由到nacos中注册的renren-fast服务,其中filters中RewritePath代表的是路由过滤重写,将/api路由到/renren-fast前缀。
我们前面已经完成了商品三级分类服务接口的编写,并且已经测试过了,接下来我们需要在我们的后台管理系统添加商品分类管理的页面
我们首先约定以/api/product为前缀的请求会路由到gulimall-product微服务
按照我们之前学到的,以命名空间来区分微服务,以分组来区分运行环境
添加gulimall-product的命名空间
启动类添加EnableDiscoveryClient注解 com.atguigu.gulimall.product.GulimallProductApplication
package com.atguigu.gulimall.product;
import org.mybatis.spring.annotation.MapperScan;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.client.discovery.EnableDiscoveryClient;
@MapperScan("com.atguigu.gulimall.product.dao")
@SpringBootApplication
@EnableDiscoveryClient
public class GulimallProductApplication {
public static void main(String[] args) {
SpringApplication.run(GulimallProductApplication.class, args);
}
}
添加nacos服务注册发现地址以及应用名称 gulimall-product/src/main/resources/bootstrap.properties
spring.application.name=gulimall-product
spring.cloud.nacos.discovery.server-addr=101.43.17.174:8848
spring.cloud.nacos.config.namespace=3e8d00b7-aa09-4cab-881e-ec822d8b6ef8
配置后启动服务,可以发现服务成功注册在nacos中
接下来配置网关路由到product服务的网关规则
这里在配置的过程中出现了401,是因为最开始我们第一个路由admin_route,访问时就直接匹配到gulimall-product服务,这里需要注意的是,product_route在配置的时候应该在admin_route路由前面,类似nginx的路由匹配规则,应该把越详细具体的路由放到前面。
gulimall-gateway/src/main/resources/application.yml
spring:
cloud:
gateway:
routes:
- id: product_route
uri: lb://gulimall-product
predicates:
- Path=/api/product/**
filters:
- RewritePath=/api/(?<segment>.*),/$\{segment}
- id: admin_route
uri: lb://renren-fast
predicates:
- Path=/api/**
filters:
- RewritePath=/api/(?<segment>.*),/renren-fast/$\{segment}
到这里差点忘记了本节的目的是编写商品分类管理页面,接下来正常开发商品分类管理页面,作为一个老前端快速跳过页面开发,侧重后端逻辑
TIP
在学习的过程中发现了ieda一个十分好用的功能,// TODO 功能,添加了以后我们可以随时在idea 的TODO控制面板中找到还未完成的任务,十分的方便。
接下来我们需要完成分类删除接口,现在的删除接口是物理删除,是很危险的操作,实际的开发过程中通常使用逻辑删除,mybatis-plus支持配置逻辑删除
gulimall-product/src/main/resources/application.yml
mybatis-plus:
mapper-locations: classpath:/mapper/**/*.xml
global-config:
db-config:
id-type: auto
logic-delete-value: 1 # 逻辑已删除值(默认为 1)
logic-not-delete-value: 0 # 逻辑未删除值(默认为 0)
在分类表中我们可以用show_status字段作为是否删除的标识,但是可以发现表里面1代表未删除,0代表已删除,跟我们正常的逻辑刚好相反
正常配置只需要在实体类添加@TableLogic注解即可,但我们这里值刚好相反,所以需要进行自定义
com.atguigu.gulimall.product.entity.CategoryEntity
package com.atguigu.gulimall.product.entity;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableLogic;
import com.baomidou.mybatisplus.annotation.TableName;
import java.io.Serializable;
import java.util.Date;
import lombok.Data;
/**
* 商品三级分类
*
* @author zhaochao
* @email 1798231822@qq.com
* @date 2023-05-13 16:54:34
*/
@Data
@TableName("pms_category")
public class CategoryEntity implements Serializable {
private static final long serialVersionUID = 1L;
/**
* 分类id
*/
@TableId
private Long catId;
/**
* 分类名称
*/
private String name;
/**
* 父分类id
*/
private Long parentCid;
/**
* 层级
*/
private Integer catLevel;
/**
* 是否显示[0-不显示,1显示]
*/
@TableLogic(value = "1",delval = "0")
private Integer showStatus;
/**
* 排序
*/
private Integer sort;
/**
* 图标地址
*/
private String icon;
/**
* 计量单位
*/
private String productUnit;
/**
* 商品数量
*/
private Integer productCount;
}
配置完成后我们重启服务,删除一条测试数据
接下来我们进行接口联调,完成分类管理页面的增删改查功能。
我们完成基本的增删改查页面,教程中的拖拽功能没必要直接跳过,最终前端页面效果如下:
前面我们在使用renren-generator服务已经根据数据表生成了各个模块的后台代码,其实在resource目录中我们可以发现生成的代码除了后台的java代码,还有前端Vue的代码。
首先在后台管理系统中添加品牌管理的页面
接着将逆向工程中的前端vue文件放到指定的前端工程位置
然后将其复制到前端工程的指定路径下
生成的增删改查页面如下
这里登录阿里云并开通oss对象存储
OSS的几个术语
接下来创建Bucket容器,一般推荐一个项目使用一个Bucket来存储对象
这里需要注意下读写权限,如果选择私有,那么访问文件需要携带账号和密码,如果选择公共读则可以直接访问文件,公共读写一般不推荐
上传一张图片测试下是否可以正常访问
第一种方式是从浏览器上传文件到服务器,然后服务器再上传到OSS服务器,这种方式上传文件会经过我们自己的服务器,如果数据量过大,会对我们自己的服务造成瓶颈。不过这种方式不会暴露存储服务的账号密码
第二种方式浏览器上传文件时先从服务器要到一个防伪签名,这里面会包含访问阿里云服务的授权令牌,上传位置等信息,但是这里面并没有上传密码,当我们携带令牌提交到阿里云时,阿里云服务会帮我们检验合法性,这样上传服务就不会经过我们自己的服务器
引入maven包
<dependency>
<groupId>com.aliyun.oss</groupId>
<artifactId>aliyun-sdk-oss</artifactId>
<version>3.15.1</version>
</dependency>
阿里云上上传文件的例子如下:
import com.aliyun.oss.ClientException;
import com.aliyun.oss.OSS;
import com.aliyun.oss.common.auth.*;
import com.aliyun.oss.OSSClientBuilder;
import com.aliyun.oss.OSSException;
import com.aliyun.oss.model.PutObjectRequest;
import com.aliyun.oss.model.PutObjectResult;
import java.io.FileInputStream;
import java.io.InputStream;
public class Demo {
public static void main(String[] args) throws Exception {
// Endpoint以华东1(杭州)为例,其它Region请按实际情况填写。
String endpoint = "https://oss-cn-hangzhou.aliyuncs.com";
// 从环境变量中获取访问凭证。运行本代码示例之前,请确保已设置环境变量OSS_ACCESS_KEY_ID和OSS_ACCESS_KEY_SECRET。
EnvironmentVariableCredentialsProvider credentialsProvider = CredentialsProviderFactory.newEnvironmentVariableCredentialsProvider();
// 填写Bucket名称,例如examplebucket。
String bucketName = "examplebucket";
// 填写Object完整路径,完整路径中不能包含Bucket名称,例如exampledir/exampleobject.txt。
String objectName = "exampledir/exampleobject.txt";
// 填写本地文件的完整路径,例如D:\\localpath\\examplefile.txt。
// 如果未指定本地路径,则默认从示例程序所属项目对应本地路径中上传文件流。
String filePath= "D:\\localpath\\examplefile.txt";
// 创建OSSClient实例。
OSS ossClient = new OSSClientBuilder().build(endpoint, credentialsProvider);
try {
InputStream inputStream = new FileInputStream(filePath);
// 创建PutObjectRequest对象。
PutObjectRequest putObjectRequest = new PutObjectRequest(bucketName, objectName, inputStream);
// 创建PutObject请求。
PutObjectResult result = ossClient.putObject(putObjectRequest);
} 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();
}
}
}
}
这里我们关注一下newEnvironmentVariableCredentialsProvider
,这需要提前设置环境变量,将我们的OSS_ACCESS_KEY_ID和OSS_ACCESS_KEY_SECRET设置到环境变量中,会比直接在代码中设置账号密码安全
设置环境变量
打开cmd命令行。
执行以下命令配置RAM用户的访问密钥。
set OSS_ACCESS_KEY_ID=LTAI4GDty8ab9W4Y1D****
set OSS_ACCESS_KEY_SECRET=IrVTNZNy5yQelTETg0cZML3TQn****
注意: OSS_ACCESS_KEY_ID和OSS_ACCESS_KEY_SECRET修改为对应AccessKey ID和Access Secret的值。
setx OSS_ACCESS_KEY_ID "%OSS_ACCESS_KEY_ID%"
setx OSS_ACCESS_KEY_SECRET "%OSS_ACCESS_KEY_SECRET%"
echo %OSS_ACCESS_KEY_ID%
echo %OSS_ACCESS_KEY_SECRET%
在这之前我们需要开通RAM账号,以便于资源的安全权限管理
选择open api的方式代表我们使用过代码来使用这个账号的
简单上传文件
@Test
void testUpload() throws com.aliyuncs.exceptions.ClientException {
// RAM用户的访问密钥(AccessKey ID和AccessKey Secret)。注意修改xxx的值
String accessKeyId = "xxxxx";
String accessKeySecret = "xxxxx";
// Endpoint以华东1(杭州)为例,其它Region请按实际情况填写。
String endpoint = "https://oss-cn-beijing.aliyuncs.com"; // 这里要填实际的Bucket 中的Region区域
// 从环境变量中获取访问凭证。运行本代码示例之前,请确保已设置环境变量OSS_ACCESS_KEY_ID和OSS_ACCESS_KEY_SECRET。
// EnvironmentVariableCredentialsProvider credentialsProvider = CredentialsProviderFactory.newEnvironmentVariableCredentialsProvider();
CredentialsProvider credentialsProvider = new DefaultCredentialProvider(accessKeyId, accessKeySecret);
// 填写Bucket名称,例如examplebucket。
String bucketName = "gulimall-zhaochao";
// 填写Object完整路径,完整路径中不能包含Bucket名称,例如exampledir/exampleobject.txt。
String objectName = "upload/test.jpg";
// 填写本地文件的完整路径,例如D:\\localpath\\examplefile.txt。
// 如果未指定本地路径,则默认从示例程序所属项目对应本地路径中上传文件流。
String filePath= "D:\\test.jpg";
// 创建OSSClient实例。
OSS ossClient = new OSSClientBuilder().build(endpoint, credentialsProvider);
try {
InputStream inputStream = new FileInputStream(filePath);
// 创建PutObjectRequest对象。
PutObjectRequest putObjectRequest = new PutObjectRequest(bucketName, objectName, inputStream);
// 创建PutObject请求。
PutObjectResult result = ossClient.putObject(putObjectRequest);
} 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());
} catch (FileNotFoundException e) {
e.printStackTrace();
} finally {
if (ossClient != null) {
ossClient.shutdown();
}
}
}
SpringCloud Alibaba上传demo地址:https://github.com/alibaba/aliyun-spring-boot/tree/master/aliyun-spring-boot-samples/aliyun-oss-spring-boot-sample
由于上传文件每个微服务都需要,所以可以将该依赖加入到gulimall-common包中
经过尝试,直接导入该依赖会导致找不到依赖包 gulimall-common/pom.xml
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>aliyun-oss-spring-boot-starter</artifactId>
</dependency>
解决方案如下: gulimall-common/pom.xml
<!-- 阿里云上传文件 -->
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>aliyun-oss-spring-boot-starter</artifactId>
<version>1.0.0</version>
<exclusions>
<exclusion>
<groupId>com.aliyun</groupId>
<artifactId>aliyun-java-sdk-oss</artifactId>
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>com.aliyun</groupId>
<artifactId>aliyun-java-sdk-core</artifactId>
<version>4.5.7</version>
</dependency>
添加配置文件
application.properties
alibaba.cloud.access-key=your-ak
alibaba.cloud.secret-key=your-sk
alibaba.cloud.oss.endpoint=***
注入OSSClinet服务,下载文件示例代码如下:
@Service
public class YourService {
@Autowired
private OSSClient ossClient;
public void saveFile() {
// download file to local
ossClient.getObject(new GetObjectRequest(bucketName, objectName), new File("pathOfYourLocalFile"));
}
}
当我们直接引入OSSClient时可能会出现错误,可将Autowired替换为Resource
示例代码如下:
@Test
void testSpringCloudOss() throws FileNotFoundException {
// 填写Bucket名称,例如examplebucket。
String bucketName = "gulimall-zhaochao";
// 填写Object完整路径,完整路径中不能包含Bucket名称,例如exampledir/exampleobject.txt。
String objectName = "upload/test.jpg";
// 填写本地文件的完整路径,例如D:\\localpath\\examplefile.txt。
// 如果未指定本地路径,则默认从示例程序所属项目对应本地路径中上传文件流。
String filePath= "D:\\test.jpg";
InputStream inputStream = new FileInputStream(filePath);
// 创建PutObjectRequest对象。
PutObjectRequest putObjectRequest = new PutObjectRequest(bucketName, objectName, inputStream);
// 创建PutObject请求。
PutObjectResult result = ossClient.putObject(putObjectRequest);
System.out.println("上传成功");
ossClient.shutdown();
}
前面已经介绍到了第一种方式每次上传文件都要经过我们的服务器,对我们的服务器压力很大,所以这里推荐使用第二种方式,服务端签名后直接上传到阿里云OSS服务
我们创建一个微服务模块来整合各种第三方服务
这里因为idea 2019选择不了java8版本了,不想重装idea2020以上版本,暂时使用阿里云的原生应用脚手架创建模块
创建完成以后我们需要修改一下pom
文件,具体要做以下几件事:
1、使其依赖于guilmall-common
2、同一java、springboot、spring-cloud等版本信息
3、将原先gulimall-common
中SpringCloud Alibaba的Oss相关依赖引入进来
gulimall-third-party/pom.xml
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>com.atguigu.gulimall</groupId>
<artifactId>gulimall-third-party</artifactId>
<version>0.0.1-SNAPSHOT</version>
<name>gulimall-third-party</name>
<description>谷粒商城-第三方服务</description>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.3.12.RELEASE</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
<properties>
<java.version>1.8</java.version>
<spring-cloud.version>Hoxton.SR12</spring-cloud.version>
</properties>
<dependencies>
<dependency>
<groupId>com.atguigu.gulimall</groupId>
<artifactId>gulimall-common</artifactId>
<version>0.0.1-SNAPSHOT</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-openfeign</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<!-- 阿里云对象存储 -->
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>aliyun-oss-spring-boot-starter</artifactId>
<version>1.0.0</version>
<exclusions>
<exclusion>
<groupId>com.aliyun</groupId>
<artifactId>aliyun-java-sdk-oss</artifactId>
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>com.aliyun</groupId>
<artifactId>aliyun-java-sdk-core</artifactId>
<version>4.5.7</version>
</dependency>
</dependencies>
<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-dependencies</artifactId>
<version>Hoxton.SR12</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
</project>
接下来我们添加该模块的配置到配置中心中,添加命名空间
添加oss.yml,将之前在gulimall-product
中添加的oss配置信息粘贴进去
最终配置结果如下:
gulimall-third-party/src/main/resources/bootstrap.properties
spring.application.name=gulimall-third-party
spring.cloud.nacos.discovery.server-addr=xxxxxxxxxxxxx
spring.cloud.nacos.config.namespace=8480a8de-6c67-42d0-93e2-6b145ed0b562
spring.cloud.nacos.config.extension-configs[0].data-id=oss.yml
spring.cloud.nacos.config.extension-configs[0].group=DEFAULT_GROUP
# 更新后是否动态刷新
spring.cloud.nacos.config.extension-configs[0].refresh=true
由于我们的对象存储暂时不需要操作数据库,所以可以将mybatis-plus
相关的依赖排除掉
gulimall-third-party/pom.xml
<dependency>
<groupId>com.atguigu.gulimall</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>
添加应用的基本信息,包括服务注册发现中心地址 gulimall-third-party/src/main/resources/application.yml
spring:
cloud:
nacos:
discovery:
server-addr: 101.43.17.174:8848
application:
name: gulimall-third-party
server:
port: 30000
同时启动类上需要添加EnableDiscoveryClient
注解 com.atguigu.gulimall.gulimallthirdparty.GulimallThirdPartyApplication
package com.atguigu.gulimall.gulimallthirdparty;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.client.discovery.EnableDiscoveryClient;
@SpringBootApplication
@EnableDiscoveryClient
public class GulimallThirdPartyApplication {
public static void main(String[] args) {
SpringApplication.run(GulimallThirdPartyApplication.class, args);
}
}
接下来运行该模块,发现始终有一个报错
Failed to instantiate [com.aliyun.oss.OSS]: Factory method 'ossClient' threw exception; nested exception is java.lang.IllegalArgumentException: Oss endpoint can't be empty.
这个错误我发现不使用nacos作为oss配置时就不会出现,为了项目正常推进,我们将oss的相关配置写到了项目本地,最终的依赖以及配置文件如下:
gulimall-third-party/src/main/resources/application.yml
alibaba:
cloud:
access-key: LTAI5tBUTr9gfktcJkoaC2p4
secret-key: oYktJrDcm7ONLyzHDipYha99mzKTQD
oss:
endpoint: https://oss-cn-beijing.aliyuncs.com
gulimall-third-party/pom.xml
<!-- 阿里云对象存储 -->
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>aliyun-oss-spring-boot-starter</artifactId>
<version>1.0.0</version>
<exclusions>
<exclusion>
<groupId>com.aliyun</groupId>
<artifactId>aliyun-java-sdk-oss</artifactId>
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>com.aliyun</groupId>
<artifactId>aliyun-java-sdk-core</artifactId>
<version>4.5.7</version>
</dependency>
<dependency>
<groupId>com.aliyun.oss</groupId>
<artifactId>aliyun-sdk-oss</artifactId>
<version>3.15.1</version>
</dependency>
我们将之前写在gulimall-product中的上传文件示例代码复制到guilmall-thirdparty中进行一个单元测试,发现上传正常
com.atguigu.gulimall.gulimallthirdparty.GulimallThirdPartyApplicationTests
package com.atguigu.gulimall.gulimallthirdparty;
import com.aliyun.oss.OSSClient;
import com.aliyun.oss.model.PutObjectRequest;
import com.aliyun.oss.model.PutObjectResult;
import org.junit.jupiter.api.Test;
import org.springframework.boot.test.context.SpringBootTest;
import javax.annotation.Resource;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.InputStream;
@SpringBootTest
class GulimallThirdPartyApplicationTests {
@Resource
OSSClient ossClient;
@Test
void contextLoads() throws FileNotFoundException {
// 填写Bucket名称,例如examplebucket。
String bucketName = "gulimall-zhaochao";
// 填写Object完整路径,完整路径中不能包含Bucket名称,例如exampledir/exampleobject.txt。
String objectName = "upload/filehello.jpg";
// 填写本地文件的完整路径,例如D:\\localpath\\examplefile.txt。
// 如果未指定本地路径,则默认从示例程序所属项目对应本地路径中上传文件流。
String filePath= "D:\\filehello.jpg";
InputStream inputStream = new FileInputStream(filePath);
// 创建PutObjectRequest对象。
PutObjectRequest putObjectRequest = new PutObjectRequest(bucketName, objectName, inputStream);
// 创建PutObject请求。
PutObjectResult result = ossClient.putObject(putObjectRequest);
System.out.println("上传成功");
ossClient.shutdown();
}
}
gulimall-third-party/src/main/resources/application.yml
spring:
cloud:
nacos:
discovery:
server-addr: 101.43.17.174:8848
application:
name: gulimall-third-party
alibaba:
cloud:
access-key: LTAI5tBUTr9gfktcJkoaC2p4
secret-key: oYktJrDcm7ONLyzHDipYha99mzKTQD
oss:
endpoint: oss-cn-beijing.aliyuncs.com
# 自定义的参数,用于阿里云Oss上传配置
bucket: gulimall-zhaochao
实现获取policy
方法 com.atguigu.gulimall.gulimallthirdparty.controller.OssController
package com.atguigu.gulimall.gulimallthirdparty.controller;
import com.aliyun.oss.OSS;
import com.aliyun.oss.OSSClient;
import com.aliyun.oss.OSSClientBuilder;
import com.aliyun.oss.common.auth.CredentialsProviderFactory;
import com.aliyun.oss.common.auth.EnvironmentVariableCredentialsProvider;
import com.aliyun.oss.common.utils.BinaryUtil;
import com.aliyun.oss.model.MatchMode;
import com.aliyun.oss.model.PolicyConditions;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.ResponseBody;
import org.springframework.web.bind.annotation.RestController;
import javax.annotation.Resource;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.LinkedHashMap;
import java.util.Map;
@RestController
public class OssController {
@Resource
OSSClient ossClient;
@Value("${alibaba.cloud.oss.endpoint}")
private String endpoint;
@Value("${alibaba.cloud.oss.bucket}")
private String bucket;
@Value("${alibaba.cloud.access-key}")
private String myAccessId;
@RequestMapping("/oss/policy")
public Map<String,String> policy(){
System.out.println("监听到");
// Host名称为当前bucket加endpoint的名称,用在给浏览器的返回上
String host = "https://" + bucket + "." + endpoint;
// 设置上传回调URL,即回调服务器地址,用于处理应用服务器与OSS之间的通信。OSS会在文件上传完成后,把文件上传信息通过此回调URL发送给应用服务器。
// String callbackUrl = "https://192.168.0.0:8888";
// 设置上传到OSS文件的前缀,可置空此项。置空后,文件将上传至Bucket的根目录下。
String format = new SimpleDateFormat("yyyy-MM-dd").format(new Date());
String dir = format + "/";
Map<String, String> respMap = new LinkedHashMap<String, String>();
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 accessId = myAccessId;
String encodedPolicy = BinaryUtil.toBase64String(binaryData);
String postSignature = ossClient.calculatePostSignature(postPolicy);
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));
// respMap.put("expire", formatISO8601Date(expiration));
} catch (Exception e) {
// Assert.fail(e.getMessage());
System.out.println(e.getMessage());
} finally {
ossClient.shutdown();
}
return respMap;
}
}
获取成功,接下来我们也需要讲gulimall-third-party
服务添加到网关服务中
网关服务的端口是88端口,我们需要让以localhost:88/api/thirdparty/
请求路由到 localhost:30000/
,以/api/thirdparty/
作为路由标志
此时网关配置如下: src/main/resources/application.yml
spring:
cloud:
gateway:
routes:
# 商品服务
- id: product_route
uri: lb://gulimall-product
predicates:
- Path=/api/product/**
filters:
- RewritePath=/api/(?<segment>.*),/$\{segment}
# 第三方服务
- id: third_party_route
uri: lb://gulimall-third-party
predicates:
- Path=/api/thirdparty/**
filters:
- RewritePath=/api/thirdparty/(?<segment>.*),/$\{segment}
# 后台管理服务
- id: admin_route
uri: lb://renren-fast
predicates:
- Path=/api/**
filters:
- RewritePath=/api/(?<segment>.*),/renren-fast/$\{segment}
配置正常生效
前面我们实现了品牌管理的页面,但logo图片上传还未编写,下面实现logo上传功能
获取文件上传地址
前端上传组件修改上传地址
在文件上传之前,会触发beforeUpload,然后请求我们签名已经写好的获取policy
签名信息得到接口
由于前端代码是从res.data赋值的,我们之前是直接返回签名信息,统一下接口返回接口
前端引入上传组件
上传图片时发现报跨域错误,前面我们在看服务端直传文档时提到了阿里云控制台需要配置允许跨域
参考文档配置如下
配置完成后即可上传成功,我们可以发现上传文件前发起了获取policy
的请求
控制台看多了一个/
的文件夹,这是因为前端多加了一个/
,去掉即可
key
加UUID
的目的是为了避免上传文件名重复,品牌管理logo图片上传功能编写完成
前端提交的数据需要进行一些基本的校验,校验主要使用的是JSR303,即Java规范提案303,规定了数据校验相关的标准,在SpringBoot中,想要使用JSR303十分简单,步骤如下:
1、添加spring-boot-starter-validation
依赖
1、在实体类中给需要校验的字段添加校验注解
2、在控制类中需要校验的实体参数添加@Valid注解
我使用的是SpringBoot
2.3.12
版本,发现内置缺少一些常用的JSR303的常用校验注解,添加spring-boot-starter-validation
依赖以后解决,并且我发现添加到gulimall-common
包中依赖无效,报错信息如下,记录一下,以后解决:
The POM for XXX is invalid, transitive dependencies (if any) will not be available
但是spring-boot-starter-validation
添加到gulimall-product
子模块下正常。
1、引入依赖
<!--引入validation的场景启动器-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-validation</artifactId>
</dependency>
2、给参数对象添加校验注解
常用的JSR303校验注解如下:
限制 | 说明 |
---|---|
@Null | 限制只能为null |
@NotNull | 限制必须不为null |
@AssertFalse | 限制必须为false |
@AssertTrue | 限制必须为true |
@DecimalMax(value) | 限制必须为一个不大于指定值的数字 |
@DecimalMin(value) | 限制必须为一个不小于指定值的数字 |
@Digits(integer,fraction) | 限制必须为一个小数,且整数部分的位数不能超过integer,小数部分的位数不能超过fraction |
@Future | 限制必须是一个将来的日期 |
@Max(value) | 限制必须为一个不大于指定值的数字 |
@Min(value) | 限制必须为一个不小于指定值的数字 |
@Past | 限制必须是一个过去的日期 |
@Pattern(value) | 限制必须符合指定的正则表达式 |
@Size(max,min) | 限制字符长度必须在min到max之间 |
@Past | 验证注解的元素值(日期类型)比当前时间早 |
@NotEmpty | 验证注解的元素值不为null且不为空(字符串长度不为0、集合大小不为0) |
@NotBlank | 验证注解的元素值不为空(不为null、去除首位空格后长度为0),不同于@NotEmpty,@NotBlank只应用于字符串且在比较时会去除字符串的空格 |
验证注解的元素值是Email,也可以通过正则表达式和flag指定自定义的email格式 |
com.atguigu.gulimall.product.entity.BrandEntity
/**
* 品牌
*
* @author zhaochao
* @email 1798231822@qq.com
* @date 2023-05-13 16:54:34
*/
@Data
@TableName("pms_brand")
public class BrandEntity implements Serializable {
private static final long serialVersionUID = 1L;
/**
* 品牌id
*/
@TableId
private Long brandId;
/**
* 品牌名
*/
@NotEmpty(message = "品牌名不允许为空")
private String name;
/**
* 品牌logo地址
*/
@NotEmpty(message = "logo不允许为空")
@URL(message = "logo必须是一个合法的url地址")
private String logo;
/**
* 介绍
*/
private String descript;
/**
* 显示状态[0-不显示;1-显示]
*/
private Integer showStatus;
/**
* 检索首字母
*/
@NotEmpty(message = "检索首字母不允许为空")
@Pattern(regexp = "/^[a-zA-Z]$/",message = "检索首字母必须是一个字母")
private String firstLetter;
/**
* 排序
*/
@NotNull(message = "排序不能为空")
@Min(value = 0,message = "排序必须大于等于0")
private Integer sort;
}
运行时有一个报错:No validator could be found for constraint 'javax.validation.constraints.NotEmpty' validating type 'java.lang.Integer'. Check configuration for 'sort'
,,报错原因是NotEmpty
注解不能用于Integer
类型字段
我们将sort
字段校验字段修改为@NotNull
/**
* 排序
*/
@NotNull
@Min(value = 0,message = "排序必须大于等于0")
private Integer sort;
3、控制类开启校验,并且收集校验错误信息
com.atguigu.gulimall.product.controller.BrandController
/**
* 保存
*/
@RequestMapping("/save")
public R save(@Valid @RequestBody BrandEntity brand, BindingResult bindingResult){
if(bindingResult.hasErrors()){
Map<String,String> map = new HashMap<>();
bindingResult.getFieldErrors().forEach((item) ->{
String msg = item.getDefaultMessage();
String fie = item.getField();
map.put(fie,msg);
});
return R.error(400,"提交数据不合法").put("data",map);
}
brandService.save(brand);
return R.ok();
}
接下来测试校验是否正常
在实际项目开发中不可能每一个控制器都编写bindingResult
去捕获校验的异常,实际开发中会定义一个全局的异常处理类,专门处理校验相关的异常。定义全局异常拦截步骤如下:
1、控制类中删除原先的BindingResult
的校验异常拦截代码,这样出现异常后抛出到全局。
2、这里需要用到@ControllerAdvice
注解,该注解功能是进行全局的异常处理拦截
3、添加全局的异常处理类
com.atguigu.gulimall.product.exception.GulimallExceptionControllerAdvice
@Slf4j
@ControllerAdvice(basePackages = "com.atguigu.gulimall.product.controller")
public class GulimallExceptionControllerAdvice {
@ResponseBody
@ExceptionHandler(value = Exception.class)
public R handleValidException(Exception e){
log.error("数据校验出现问题{},异常类型:{}",e.getMessage(),e.getCause());
return R.error();
}
}
因为我们所有的校验异常都会以json的形式返回给前端,所以我们可以在异常处理的方法上添加@ResponseBody
注解,表示以json
数据格式返回数据。
TIP
如果一个类中所有的方法都需要以json的数据格式返回,那么我们可以在类本身标注@ResponseBody
注解,这里延伸出来了一个新的注解,@RestControllerAdvice
,这个注解等同于@ControllerAdvice
+ @ResponseBody
,可以简化部分代码。
com.atguigu.gulimall.product.exception.GulimallExceptionControllerAdvice
@RestControllerAdvice(basePackages = "com.atguigu.gulimall.product.controller")
@Slf4j
public class GulimallExceptionControllerAdvice {
@ExceptionHandler(value = Exception.class)
public R handleValidException(Exception e){
log.error("数据校验出现问题{},异常类型:{}",e.getMessage(),e.getClass());
return R.error();
}
}
BindingResult
代码com.atguigu.gulimall.product.controller.BrandController
@RequestMapping("/save")
public R save(@Valid @RequestBody BrandEntity brand){
// if(bindingResult.hasErrors()){
// Map<String,String> map = new HashMap<>();
// bindingResult.getFieldErrors().forEach((item) ->{
// String msg = item.getDefaultMessage();
// String fie = item.getField();
// map.put(fie,msg);
// });
// return R.error(400,"提交数据不合法").put("data",map);
// }
brandService.save(brand);
return R.ok();
}
当校验参数不通过时报错如下:
可以看到此时的的异常类型如下:MethodArgumentNotValidException
,GulimallExceptionControllerAdvice
中可以拦截处理许多不同类型的异常,接下来我们针对校验异常MethodArgumentNotValidException
进行处理。
com.atguigu.gulimall.product.controller.BrandController
@RestControllerAdvice(basePackages = "com.atguigu.gulimall.product.controller")
@Slf4j
public class GulimallExceptionControllerAdvice {
@ExceptionHandler(value = MethodArgumentNotValidException.class)
public R handleValidException(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(400,"数据校验出现问题").put("data",errorMap);
}
}
测试结果正确
TIP
在项目开发的过程中如果出现了异常,可以放心大胆的将异常使用Throw
抛出,我们可以统一在全局的异常处理器中监听处理。
在平时的开发过程中,会出现许多的异常错误,这就需要我们制定一个错误码的规则,例如我们可以用5位数字进行定义,前两位表示业务场景,后两位表示错误码,
错误码在每个微服务模块中都会使用,所以我们将其定义到gulimall-common
包中,这里推荐使用枚举
类型定义
com.atguigu.common.exception.BizCodeEnums
package com.atguigu.common.exception;
public enum BizCodeEnums {
UNKNOW_EXCEPTION(10000,"系统未知异常"),
VAILD_EXCEPTION(10001,"参数格式校验失败");
private int code;
private String msg;
BizCodeEnums(int code,String msg){
this.code = code;
this.msg = msg;
}
public int getCode() {
return code;
}
public String getMsg() {
return msg;
}
}
匹配是从上到下进行匹配的,我们再添加一个Throwable
通用类型的异常拦截器,用来处理其他未知错误。
com.atguigu.gulimall.product.exception.GulimallExceptionControllerAdvice
package com.atguigu.gulimall.product.exception;
import com.atguigu.common.exception.BizCodeEnums;
import com.atguigu.common.utils.R;
import lombok.extern.slf4j.Slf4j;
import org.springframework.validation.BindingResult;
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;
@RestControllerAdvice(basePackages = "com.atguigu.gulimall.product.controller")
@Slf4j
public class GulimallExceptionControllerAdvice {
// 检验字段异常处理函数
@ExceptionHandler(value = MethodArgumentNotValidException.class)
public R handleValidException(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(BizCodeEnums.VAILD_EXCEPTION.getCode(), BizCodeEnums.VAILD_EXCEPTION.getMsg()).put("data",errorMap);
}
// 最后的统一异常处理
@ExceptionHandler(value = Throwable.class)
public R handleValidException(Throwable e){
return R.error(BizCodeEnums.UNKNOW_EXCEPTION.getCode(), BizCodeEnums.UNKNOW_EXCEPTION.getMsg());
}
}
测试结果如下:
在实际的开发过程中会遇到一个问题,对于一个实体类的校验,有些时候新增的时候不需要校验,修改的时候才需要校验,有很多类似的场景,分组校验功能可以分别规定实体类的字段不同场景的校验规则,步骤如下:
这里拿品牌表的实体BrandEntity
举例,例如brandId
字段在新增的时候不需要传,而修改的时候必传。
1、在实体类中的校验注解中添加groups
字段,接收一个分组接口,分别用来表示场景(增删查改),用来区分不同的分组,可以添加多个分组接口。
2、在需要校验的控制类中添加@Validated
,并指定校验分组
com.atguigu.gulimall.product.entity.BrandEntity
package com.atguigu.gulimall.product.entity;
import com.atguigu.common.valid.AddGroup;
import com.atguigu.common.valid.UpdateGroup;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import java.io.Serializable;
import java.util.Date;
import lombok.Data;
import org.hibernate.validator.constraints.URL;
import javax.validation.constraints.*;
/**
* 品牌
*
* @author zhaochao
* @email 1798231822@qq.com
* @date 2023-05-13 16:54:34
*/
@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 = {UpdateGroup.class,AddGroup.class})
private String name;
/**
* 品牌logo地址
*/
@NotEmpty(message = "logo不允许为空")
@URL(message = "logo必须是一个合法的url地址")
private String logo;
/**
* 介绍
*/
private String descript;
/**
* 显示状态[0-不显示;1-显示]
*/
private Integer showStatus;
/**
* 检索首字母
*/
@NotEmpty(message = "检索首字母不允许为空")
@Pattern(regexp = "/^[a-zA-Z]$/",message = "检索首字母必须是一个字母")
private String firstLetter;
/**
* 排序
*/
@NotNull(message = "排序不能为空")
@Min(value = 0,message = "排序必须大于等于0")
private Integer sort;
}
添加标记分组的接口
com.atguigu.common.valid.AddGroup
package com.atguigu.common.valid;
public interface AddGroup {
}
com.atguigu.common.valid.UpdateGroup
package com.atguigu.common.valid;
public interface UpdateGroup {
}
在需要校验的控制类上添加@Validated
注解 com.atguigu.gulimall.product.controller.BrandController
/**
* 保存
*/
@RequestMapping("/save")
public R save(@Validated({AddGroup.class}) @RequestBody BrandEntity brand){
brandService.save(brand);
return R.ok();
}
接口测试接口如下:
注:可以发现我们没有标注分组的校验并没有起作用,例如logo
字段并没有校验是否是一个Url
类型,当我们在控制类中设置了@Validated
注解并标明了分组,那么对于没有标明分组的字段就不会进行校验。
我们补充logo
字段的校验分组
com.atguigu.gulimall.product.entity.BrandEntity
/**
* 品牌logo地址
*/
@NotEmpty(message = "logo不允许为空",groups = {AddGroup.class})
@URL(message = "logo必须是一个合法的url地址", groups = {AddGroup.class,UpdateGroup.class})
private String logo;
完善实体类校验规则,修改品牌添加校验规则
com.atguigu.gulimall.product.entity.BrandEntity
package com.atguigu.gulimall.product.entity;
import com.atguigu.common.valid.AddGroup;
import com.atguigu.common.valid.UpdateGroup;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import java.io.Serializable;
import java.util.Date;
import lombok.Data;
import org.hibernate.validator.constraints.URL;
import javax.validation.constraints.*;
/**
* 品牌
*
* @author zhaochao
* @email 1798231822@qq.com
* @date 2023-05-13 16:54:34
*/
@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 = {UpdateGroup.class,AddGroup.class})
private String name;
/**
* 品牌logo地址
*/
@NotEmpty(message = "logo不允许为空",groups = {AddGroup.class})
@URL(message = "logo必须是一个合法的url地址", groups = {AddGroup.class,UpdateGroup.class})
private String logo;
/**
* 介绍
*/
private String descript;
/**
* 显示状态[0-不显示;1-显示]
*/
private Integer showStatus;
/**
* 检索首字母
*/
@NotEmpty(message = "检索首字母不允许为空",groups = {AddGroup.class})
@Pattern(regexp = "/^[a-zA-Z]$/",message = "检索首字母必须是一个字母",groups = {AddGroup.class,UpdateGroup.class})
private String firstLetter;
/**
* 排序
*/
@NotNull(message = "排序不能为空",groups = {AddGroup.class})
@Min(value = 0,message = "排序必须大于等于0",groups = {AddGroup.class,UpdateGroup.class})
private Integer sort;
}
com.atguigu.gulimall.product.controller.BrandController
/**
* 修改
*/
@RequestMapping("/update")
public R update(@Validated({UpdateGroup.class}) @RequestBody BrandEntity brand){
brandService.updateById(brand);
return R.ok();
}
测试结果如下:
在项目开发过程中,会存在许多特殊的字段校验,可能需要结合自己的业务进行定制,这时候内置的校验注解已经无法满足我们的业务需求,这时候我们就可以编写一个自定义的校验注解来实现我们的校验功能。步骤如下:
1、编写一个自定义的校验注解
2、编写一个自定义校验器
3、关联自定义的校验器和自定义的校验注解
例如现在我们需要定义一个@ListValue
的校验注解,该注解的功能是校验指定的字段是否包含在传入的vals
数组列表中,如果在则返回true
,不在则返回false
。
com.atguigu.gulimall.product.entity.BrandEntity
/**
* 显示状态[0-不显示;1-显示]
*/
@ListValue(vals={0,1})
private Integer showStatus;
在gulimall-common
下的valie包下创建注解类ListValue
com.atguigu.common.valid.ListValue
package com.atguigu.common.valid;
public @interface ListValue {
}
这个注解如何编写可以参考内置的@NotBlank
源码
可以看到内置有3个属性:message
、groups
、payload
message
:表示当我们校验出错时,错误信息去哪取,默认是到javax.validation.constraints
这里取
groups
:表示也需要支持分组校验的功能
payload
:表示在自定义校验的过程中,还需要传入一些荷载信息,即额外参数
我们将这三个变量赋值到我们的自定义校验注解中,初次之外,我们的校验注解还必须标有以下元注解信息。
Target
:表示我们的注解可以标注在哪些位置,METHOD(方法)
, FIELD(属性)
, ANNOTATION_TYPE
, CONSTRUCTOR(构造器)
, PARAMETER(参数)
,TYPE_USE
Retention
:表示我们的校验注解的时机,RUNTIME
表示运行时
Constraint
:最重要的一个注解,表示我们的校验注解使用的是哪一个校验器进行校验的,如果不指定就需要在初始化的时候完成
我们将以上注解复制到我们的自定义校验注解中。
然后我们导入相关的包,这里使用的是javax.validation
下面相关的包
gulimall-common/pom.xml
<!-- 自定义校验注解需要用到 -->
<dependency>
<groupId>javax.validation</groupId>
<artifactId>validation-api</artifactId>
<version>2.0.1.Final</version>
</dependency>
引入相关依赖包以后我们继续完善ListValue
校验注解,我们定义的校验注解接收的参数列表是vals
字段
/**
* 显示状态[0-不显示;1-显示]
*/
@ListValue(vals={0,1},message = "校验信息")
private Integer showStatus;
我们添加vals
字段,并且设置message
的默认取值,如果注解中有传入message
变量,那么就优先使用注解传入的message
信息,如果没有传入默认回到当前包地址com.atguigu.common.valid.ListValue
下寻找。
com.atguigu.common.valid.ListValue
/**
* 【表情错误时的提示信息
* @return
*/
String message() default "{com.atguigu.common.valid.ListValue.message}";
Class<?>[] groups() default {};
Class<? extends Payload>[] payload() default {};
/**
* 接收的参数
* @return
*/
int[] vals() default {};
JSR303
中有一个配置文件规定了默认的校验信息,叫做ValidationMessages.properties
,我们可以在代码中搜到
在gulimall-common
下的resources
下添加一个ValidationMessages.properties
注解,并设置默认的校验信息
gulimall-common/src/main/resources/ValidationMessages.properties
com.atguigu.common.valid.ListValue.message=必须提交指定的值
接下来编写校验器,@Constraint
注解指定的校验器,点击进入Constraint
源码
可以看到validatedBy
是一个数组,可以接受多个校验器,接着点击ConstraintValidator
查看源码,可以看到该接口包含了两个参数,第一个参数是一个注解,第二个参数是一个泛型。除此之外包含了两个方法,initialize
和isValid
,接下来我们让我们的自定义校验注解来实现这个接口
添加自定义校验器类ListValueConstraintValidator
com.atguigu.common.valid.ListValueConstraintValidator
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> {
// 保存vals传入的参数
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 标注的需要校验的值
* @param constraintValidatorContext 上下文环境
* @return
*/
@Override
public boolean isValid(Integer value, ConstraintValidatorContext constraintValidatorContext) {
// vals包含了校验值则通过
return set.contains(value);
}
}
校验器已经编写完成,接下来需要关联校验注解以及校验器,我们在ListValue
注解的Constraint
注解中指定validatedBy
即可绑定校验器和校验注解。
@Constraint(
validatedBy = { ListValueConstraintValidator.class }
)
我们的自定义校验函数已经完成接下来我们进行测试,我们设置分组
@ListValue(vals={0,1},groups = { AddGroup.class })
private Integer showStatus;
接下来进行接口测试
可以看到这里乱码了,但是校验信息应该是正确的,只是字符编码乱了,解决办法如下:
修改后可以发现返回结果现在已经正常了
可以发现目前我们自定义的校验注解只能校验Integer类型的参数
,如果后面有其他类型的参数,例如Double
的我们的校验注解就没用了,这时候应该如何做呢?
可以看到我们的校验注解validatedBy
是接受一个数组的,也就是支持多个校验器,那我们就可以在这里定义其他参数类型的校验器。
我们可以打开ConstraintValidator
的实现类,可以看到这里有针对各种类型编写的Validator
一个校验注解可以指定多个不同的校验器适配不同类型的校验,validatedBy = { ListValueConstraintValidator.class,其他类型的校验器 }
我们在测试品牌管理模块的过程中发现一些问题,我们进行修改:
第一个问题是BrandEntity
实体类的检索字母设置的正则校验规则无效,原因是Java中设置的正则不需要设置斜杆
Snipaste_2023-12-31_15-32-50.png
我们去除斜杆
@NotEmpty(message = "检索首字母不允许为空",groups = {AddGroup.class})
@Pattern(regexp = "^[a-zA-Z]$",message = "检索首字母必须是一个字母",groups = {AddGroup.class,UpdateGroup.class})
private String firstLetter;
1、网关统一配置解决跨域
2、修改网关配置,使前端统一请求网关服务,由网关服务根据后缀名来进行分发
3、对接了阿里云OSS文件上传服务,使用实现了服务端签名后直传功能
4、学习了JSR303
相关的知识,包括:数据校验、全局校验拦截、分组校验以及自定义校验注解等。