【笔记/后端】谷粒商城基础篇

  • Post author:
  • Post category:其他




一、环境配置



1 Docker



1.1 Docker是什么?

基于镜像启动各种容器,每一种容器都是一个完整的运行环境,容器之间互相隔离。



1.2 安装&启动

docker官网:Developers -> Docs -> Get Docker -> Docker For Linux


systemctl start docker


docker images

查看安装的全部镜像


systemctl enable docker

开机自启



1.2.1 阿里云镜像加速

阿里云官网 -> 登录 -> 控制台 -> 容器镜像服务 -> 镜像工具 -> 镜像加速器 -> CentOS



1.3 安装MySQL


docker pull mysql:5.7

docker run -p 3306:3306 --name mysql \\ # 主机3306映射到容器3306 --name起名字
-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用户密码
-d 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/conf.d -e MYSQL_ROOT_PASSWORD=root -d mysql:5.7


docker ps

查看现在运行的全部镜像


docker exec -it mysql /bin/bash

进入到容器内部


whereis mysql

在容器内部执行来查看mysql位置

修改/mydata/mysql/conf下mysql配置

[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



1.4 安装Redis


docker pull redis

不写版本下载最新版本

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

在/mydata/redis/conf/redis.conf中添加

appendonly yes

开启持久化

虚拟机重启后自动启动镜像

docker update redis --restart=always



2 开发环境



2.1 Maven


问题1

依赖子模块的模块打包失败


解决

给上层(父)模块install后成功



2.2 Git

配置git

# 配置用户名
git config --global user.name "username" # 名字
# 配置邮箱
git config --global user.email "username@email.com" # 注册账号时使用的邮箱

在码云中进入

设置 -> SSH公钥 -> 将本机公钥粘贴进去

实现后续免密提交



2.3 Node

npm配置镜像仓库:


npm config set register <http://registry.npm.taobao.org/>

install后使用

npm run dev

运行


问题:

npm安装失败。


解决

1. node使用10.X版本,python版本改3.7(非必要) 2. 网络问题 3. npm install遇到循环c++提示等待即可




二、创建微服务项目



1 内容

商品服务、仓储服务、订单服务、优惠券服务、用户服务

共同:

  1. web openfeign
  2. 每一个服务,包名

    com.atguigu.gulimall.xxx(product/order/ware/coupon/member)
  3. 模块名:

    gulimall-coupon



2 问题记录


问题1

报错 POM文件爆红,以及

<repository>

中的网址禁止访问


解决

Maven版本问题,3.6.3解决


问题2

报错如下

'parent.relativePath' of POM io.renren:renren-fast:3.0.0 (/Users/dujianzhang/Documents/IdeaProjects/gulimall/renren-fast/pom.xml) points at pers.djz.gulimall:gulimall instead of org.springframework.boot:spring-boot-starter-parent, please verify your project structure```


解决

子模块没有指向上级目录父模块,在

<parent>

标签中添加

<relativePath/>

使Maven打包时每次都直接从仓库获取


问题3

报错如下

The last packet sent successfully to the server was 0 milliseconds ago. The driver has not received any packets from the server.
    at sun.reflect.NativeConstructorAccessorImpl.newInstance0(Native Method)
    at sun.reflect.NativeConstructorAccessorImpl.newInstance(NativeConstructorAccessorImpl.java:62)
    at sun.reflect.DelegatingConstructorAccessorImpl.newInstance(DelegatingConstructorAccessorImpl.java:45)
    at java.lang.reflect.Constructor.newInstance(Constructor.java:423)
    at com.mysql.cj.exceptions.ExceptionFactory.createException(ExceptionFactory.java:61)


解决

尝试与MySQL建立SSL连接失败,在URL后面加上

?useSSL=false



3 renren-generator

使用方法

  1. 修改application.yml中的dataSource相关信息
  2. 修改generator.properties中生成项目目录结构信息
  3. 运行Application,在浏览器中前往生成器页面
  4. 勾选要进行逆向工程的数据库表,点击生成代码开始下载


问题1

idea不显示yaml小树叶


解决

备份编辑器设置,恢复编译器默认设置,重新导入备份设置不恢复file相关设置


问题2

Longblob无法解析


解决

替换成byte类型


问题3

renren generator实体类备注乱码


解决

使用navicat执行sql不要导入sql文件,复制sql文本执行查询




三、分布式组件

在这里插入图片描述



1 Nacos



1.1 注册中心

启动方式:

./startup.sh -m standalone

在application.yml中配置nacos注册中心的信息:

spring:    
	cloud:    
		nacos:            
			discovery:            
				server-addr: localhost:8848#一定要有应用名    
	application:    
		name: gulimall-coupon


问题1

nacos在macOS上找不到

/Library/Internet



Permission denied


解决

mac自带了一个jdk,在环境变量中声明自己安装的jdk的JAVA_HOME

export JAVA_HOME="/Library/Java/JavaVirtualMachines/jdk1.8.0_311.jdk/Contents/Home"
export PATH="$PATH:$JAVA_HOME/bin"
export CLASSPATH="$JAVA_HOME/lib/tools.jar:$JAVA_HOME/lib/dt.jar:."


问题2

macOS上7000端口占用问题


解决

7000端口用于AirPlay,使用7001



1.2 配置中心



1.2.1 如何使用Nacos作为配置中心统一管理配置?

  1. 引入依赖
<dependency>    
		<groupId>com.alibaba.cloud</groupId>    
		<artifactId>spring-cloud-starter-alibaba-nacos-config</artifactId>
</dependency>
  1. 创建一个bootstrap.properties
spring.application.name=gulimall-coupon
spring.cloud.nacos.config.server-addr=localhost:8848
  1. 在配置中心添加一个默认的数据集(Data Id) gulimall-coupon.properties,默认规则:

    应用名.properties
  2. 给应用名.properties添加任何配置
  3. 动态获取配置


    @RefreshScope

    动态获取并刷新配置,注解在Application类上


    @Value("${配置项的名}")

    获取到配置

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



1.2.2 细节

  1. 命名空间:配置隔离

    默认:public(保留空间),默认新增的所有配置都在public空间

    1. 开发、测试、生产,利用命名空间来做环境隔离

      注意:在bootstrap.properties配置上,需要使用哪个命名空间下的配置


      spring.cloud.nacos.config.namespace=aa34a377-e984-4082-8959-e41e35dd9d51
    2. 每一个微服务之间互相隔离配置,每一个微服务都创建自己的命名空间,只加载自己命名空间下的所有配置
  2. 配置集:所有的配置的集合
  3. 配置集ID:类似文件名

    Data ID:类似文件名
  4. 配置分组

    默认所有的配置集都属于

    DEFAULT_GROUP



    spring.cloud.nacos.config.group=1111

每个微服务创建自己的命名空间,使用配置分组区分环境,dev, test, prod



1.2.3 同时加载多个配置集

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

yaml写法

spring:
  cloud:
    nacos:
        ext-config:
          - data-id: oss.yml
            group: DEFAULT_GROUP
            refresh: true
          - data-id: ...
            group: ...
            refresh: ...
  1. 微服务任何配置信息,任何配置文件都可以放在配置中心中
  2. 只需要在bootstrap.properties中说明加载配置中心中哪些配置文件即可

  3. @Value


    @ConfigurationProperties

    以前SpringBoot任何方法从配置文件中获取值都能使用,配置中心有的优先使用配置中心的


问题1

配置了bootstrap.properties中的nacos config服务器地址,依然报错endpoint is blank


解决

没有读取配置文件,springboot 2.4以上要在pom中添加依赖:

<dependency>    
		<groupId>org.springframework.cloud</groupId>    
		<artifactId>spring-cloud-starter-bootstrap</artifactId>    
		<version>3.1.0</version>
</dependency>

或者是没有在resources下创建bootstrap配置文件指明配置中心地址



2 Feign



1.1 使用场景

  1. member想要调用coupon中的信息
  2. member向nacos注册中心查询目标服务地址
  3. 查询



1.2 调用其他服务的Application

/** 想要远程调用别的服务* 1 引入openfeign* 2 编写一个接口,告诉SpringCloud这个接口需要调用远程服务*   2.1 声明接口的每一个方法都是调用哪个远程服务的哪个请求* 3 开启远程调用功能* */
@EnableFeignClients(basePackages = "pers.djz.gulimall.member.feign")
@EnableDiscoveryClient
@SpringBootApplicationpublic 
class GulimallMemberApplication {    
		public static void main(String[] args) {        
				SpringApplication.run(GulimallMemberApplication.class, args);    
		}
}


feign包下的接口:

// name是nacos注册中心中要访问的微服务名称,url可不写
@FeignClient(name = "gulimall-coupon", url = "http://localhost:7001/")
public interface CouponFeignService {    
		// 方法名不必相同,但访问url要对应上
		@PostMapping("/coupon/skufullreduction/saveInfo")
    R saveSkuReduction(@RequestBody SkuReductionTo skuReductionTo);
}

  1. @RequestBody

    将这个对象转为json
  2. 找到gulimall-coupon服务,给

    /coupon/spubounds/save

    发送请求。将上一步转的json放在请求体位置,发送请求
  3. 对方服务收到请求。请求体里有JSON数据,

    @RequestBody SpuBoundsEntity spuBounds

    将请求体的JSON转为SpuBoundsEntity


只要JSON数据模型是兼容的,双方服务无需使用同一个VO


问题

运行后feign报错ribbon和loadbalance问题


解决

pom中如下引用

<dependency>
		<groupId>com.octopus.gulimall</groupId>
		<artifactId>common</artifactId>
		<version>0.0.1-SNAPSHOT</version>
		<exclusions>
		    <exclusion>
		        <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-netflix-ribbon</artifactId>
        </exclusion>
    </exclusions>
</dependency>

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



3 Gateway

在这里插入图片描述

spring:
  cloud:
    gateway:
      routes:
        - id: qq_route
		  # uri是符合断言的请求会跳转到的地址
          uri: https://www.qq.com/
          predicates:
            - Query=url, qq
        - id: baidu_route
          uri: https://www.weibo.com/
          predicates:
            - Query=url, weibo


问题1

启动网关application报错:

Description:Parameter 0 of method loadBalancerWebClientBuilderBeanPostProcessor in org.springframework.cloud.client.loadbalancer.reactive.LoadBalancerBeanPostProcessorAutoConfiguration required a bean of type 'org.springframework.cloud.client.loadbalancer.reactive.DeferringLoadBalancerExchangeFilterFunction' that could not be found.Action:Consider defining a bean of type 'org.springframework.cloud.client.loadbalancer.reactive.DeferringLoadBalancerExchangeFilterFunction' in your configuration.Process finished with exit code 1


解决

添加依赖:

<dependency>    
		<groupId>org.springframework.cloud</groupId>    
		<artifactId>spring-cloud-starter-loadbalancer</artifactId>    
		<version>3.1.1</version>
</dependency>


问题2

通过

lb://renren-fast

进行网关控制报503错误,提示No servers available for service: renren-fast


解决

SpringCloud Alibaba和SpringBoot版本问题,改为

2.1.8.RELEASE



Greenwich.SR3



4 Aliyun OSS

  • 在Aliyun官网使用对象存储服务,创建一个新的bucket存储文件

    • 使用公共读
  • 右上角AccessKey创建子账户供Open API调用
  • Java导入starter后,自动注入OssClient类对象(新版本使用OSS类)

    • starter导入后在application.yaml中配置endpoint、access-key和secret-key
  • 实际开发中客户端向服务器请求签名后直接带着签名上传到OSS服务器(使用方法参考文档)
spring:
  cloud:
    alicloud:
      access-key: xxx
      secret-key: xxx
      oss:
        endpoint: oss-cn-beijing.aliyuncs.com
        bucket: xxx
@RestController
@RequestMapping("/oss")
public class OssController {

    @Autowired
    private 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;

    @RequestMapping("/policy")
    public R policy() {
        // 填写Host地址,格式为https://bucketname.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 = null;
        try {
            long expireTime = 30;
            long expireEndTime = System.currentTimeMillis() + expireTime * 1000;
            Date expiration = new Date(expireEndTime);
            PolicyConditions policyConds = new PolicyConditions();
            policyConds.addConditionItem(PolicyConditions.COND_CONTENT_LENGTH_RANGE, 0, 1048576000);
            policyConds.addConditionItem(MatchMode.StartWith, PolicyConditions.COND_KEY, dir);

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

            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));
            // respMap.put("expire", formatISO8601Date(expiration));
        } catch (Exception e) {
            // Assert.fail(e.getMessage());
            System.out.println(e.getMessage());
        }
        return R.ok().put("data", respMap);
    }
}




四、前端开发基础



1 ES6

ECMAScript是浏览器脚本语言的规范



1.1 let和var

  1. let有严格的作用域,var会越域
  2. let不能重复声明同名变量,var可以
  3. let不存在变量提升,var会 变量提升:声明会提升到作用域顶端
function fun() {
    a = 1;
    var a;
}// 实际执行 var a; -> a = 1;



1.2 解构表达式

const [a, b, c] = [1, 2, 3];// a = 1, b = 2, c = 3
let person = {name: "jack", age: 18};
const {name, age} = person;// name = "jack", age = "age"



1.3 字符串扩展

let str = "hello.vue";
str.startsWith("hello"); 
// truestr.endsWith("vue"); 
// truestr.includes("e"); // true
// 字符串模版(多行字符串)
let ss = `<div>    <span>hello</span></div>`
// 插入表达式(用``)
let info = `我是${abc}, 今年${age}了, 我想说: ${fun()}`;



1.4 函数优化

b = b || 1; // 判断b是否为空,如空给默认值1
function fun(a, b=1) {    ...}
// 不定参数
function fun(...values) {
    console.log(values.length)
}
// 箭头函数
var print = obj => console.log(obj); 
// print是一个函数
var sum = (a, b) => return a + b;
var sum2 = (a, b) => {
    let c = a + b;    
		return a + c;
}
// 箭头函数+解构
var fun = ({name}) => console.log(name);
fun(person);



1.5 对象优化

const person = {name: "jack", age: 18};
Object.keys(person);
Object.values(person);
Object.entries(person);
// 合并
const target = {a: 1};
const source1 = {b: 2};
const source2 = {c: 3};
Object.assign(target, source1, source2);
// 声明对象简写
const age = 23;
const name = "张三";
const person = {age, name}; 
// 变量和属性同名可以简写
// 对象的函数属性简写
let person = {
    name: "jack",
    eat: function (food) {
        console.log(this.name + "在吃" + food);
    },
    // 箭头函数this不能使用,使用对象.属性来获取
    eat2: food => console.log(this.name + "在吃" + food);
}
person.eat("banana");



1.6 对象扩展运算符

// 拷贝对象
let p1 = {name: "jack", age: 18};
let someone = { ...p1 }
// 合并对象
let age1 = {age: 15};
let name1 = {name: "Amy"};
let p2 = {name: "zhangsan"};
p2 = {...age1, ...name1}; // p2.name = "Amy"



1.7 map和reduce

let arr = ['1', '20', '-5'];
arr = arr.map((item) => {
    return item * 2;
});
arr = arr.map(item => item * 2);
/**
array.reduce(function(total, currentValue, currentIndex, arr), initialValue)
1、previousValue(上一次调用回调返回的值,或者是提供的初始值(initialvalue))
2、currentvalue (数组中当前被处理的元素)
3、index(当前元素在数组中的索引)
4、array(调用 reduce 的数组)
*/
let result = arr.reduce((a, b) => {
    console.log(a, b);
    return a + b;
}, 100); // 100 1 ...



1.8 promise

Promise可以封装异步操作

function get(url, data) {
    return new Promise((resolve, reject) => {
        $.ajax({
            url: url,
            data: data,
            success: function (data) {
                resolve(data);
            },
            error: function (err) {
                reject(err);
            }
        });
    });
}
// 多级分类查询
get("mock/user.json").then((data) => {
    console.log("用户查询成功: ", data);
    return get(`mock/user_course_${data.id}.json`);
}).then((data) => {
    console.log("课程查询成功: ", data);
    return get(`mock/course_score_${data.id}.json`);
}).then((data) => {
    console.log("课程成绩查询成功", data);
}).catch((err) => {
    console.log("出现异常", err);
});



1.9 模块化



1.9.1 导入导出


util.js

var name = "jack";var age = 21;
function add(a, b) {
    return a + b;
}
export {name, age, add}


main.js

import {name, age, add} from "./util.js"



1.9.2 简化导出

export const util = ...



1.9.3 不命名导出


xx.js

export default {
    sum(a, b) {
        return a + b;
    }
}
// 随意命名
import abc from "./xx.js"



2 Vue


问题1

eslint语法检查过于严格


解决

在.eslintignore文件中添加要忽略的内容,如*.vue

<body>
    <div id="app">
        <input type="text" v-model="num"/>
        <button v-on:click="num++">👍</button>
        <button v-on:click="cancel">👎</button>
        <h1>hello {{ name }}! 有{{ num }}个人为你点赞</h1>
    </div>
    <script src="./node_modules/vue/dist/vue.js"></script>
    <script>
        // 1 声明式渲染
        let vm = new Vue({
            el: "#app", // 绑定元素
            data: { // 封装数据
                name: "zhangsan",
                num: 1
            },
            methods: { // 封装方法
                cancel() {
                    this.num--;
                }
            }
        })
        // 2 双向绑定,模型变化,视图变化。反之亦然
        // 3 事件处理
        // v-xx指令
        // 1 创建vue实例,关联页面的模版,将我们的数据data渲染到关联的模版,响应式的
        // 2 指令来简化对dom的一些操作
        // 3 声明方法来做更复杂的操作
    </script>
</body>



2.1 基础属性


  1. v-text


    v-html

  2. v-bind:xxx

    单向绑定(数据修改视图) e.g.

    v-bind:href

    ==

    :href

    对于class、style:
<span v-bind:class="{active:isActive,'text-danger':hasError}" v-bind:style="{color: color1;'font-size': size;}">你好</span>
<!--连字符不合法,使用''或者驼峰命名如fontSize-->
<script>
    ...
    data:{
        isActive:true,
        hasError:true,
        color1:'red',
        size:'36px'
    }
</script>

  1. v-model=

    双向绑定

  2. v-on

    绑定事件

    v-on:click=

    ==

    @click


事件修饰符

阻止事件向上冒泡

@click.stop

阻止默认行为

@click.prevent


@click.prevent.stop


@click.once


@keyup.down

键盘↓ 5.

v-for

遍历的时候加上:key来区分不同数据(要求key不重复),提高vue渲染效率

<li v-for="(user,index) in users" :key="user.name">
    {{ index }} -> {{ user }}
    <span v-for="(k,v,i) in user">{{ k }} -> {{ v }}</span>
<li>

  1. v-if



    v-show

  • v-if="false"

    直接删除dom元素

  • v-show="false"

    使用

    display: none

还可以与

v-if

搭配

v-else-if



v-else

示例:

<button @click="isShow = !isShow">button</button>
<span v-if="isShow">span</span>



v-for

搭配:

<li v-for="(user,index) in users" :key="user.name" v-if="user.gender == ''">



2.2 计算属性,监听器,过滤器

  • computed的函数在使用到的(data属性中的)变量发生改变后再次执行进行更新
  • watch可以让我们监控一个值的变化,从而做出相应的反应
<li>总价:{{ totalPrice }}</li>
<li v-for="user in users">
    {{user.gender == 1 ? "男" : "女"}} 等同于过滤器 {{user.gender | genderFilter}}
</li>
<script>
		new Vue({
		    el: "#app",
		    data: {
		        price: 20.00,
		        num: 10
		    },
		    computed: {
		        totalPrice() {
		            return this.price * num;
		        }
		    },
		    watch: {
		        num: function(oldVal, newVal) {
		            if (newVal > 3) {
		                alert("超过3了");
		            }
		        }
		    },
		    filters: {
		        genderFilter(val) {
		            if (val == 1) {
		                return "男";
		            } else {
		                return "女";
		            }
		        }
		    }}
		);
</script>

全局过滤器

Vue.filter("gFilter", function() {    ...})

使用:

{{ user.gender | gFilter }}



2.3 组件化

<counter></counter>
<div id="app">
    <button-counter></button-counter>
</div>
<script>
    // 1. 全局声明注册一个组件
    Vue.component("counter", {
        template: `<button @click="count++">我被点击了 {{count}} 次</button>`,
        data() {
            return {
                count: 1
            }
        }
    });
    // 2. 局部声明一个组件
    const buttonCounter = {
        template: `<button @click="count++">我被点击了 {{count}} 次</button>`,
        data() {
            return {
                count: 1
            }
        }
    };
    new Vue({
        el: "#app",
        data: {
            count: 1
        },
        components: {
            'button-counter': buttonCounter
        }
    });
</script>



五、业务处理



1 逻辑删除

一般删除记录通过设置标志位进行逻辑删除,而不是真正的物理删除


使用MybatisPlus实现逻辑删除的步骤

  1. 配置全局逻辑删除规则(可省略)
mybatis-plus:
  global-config:
    db-config:
      logic-delete-value: 1
      logic-not-delete-value: 0
  1. 配置逻辑删除的组件Bean(过时)
  2. 给Bean的标志位属性加上逻辑删除注解

    @TableLogic

数据库中对应字段值不能为空才能进行逻辑删除,否则删除后标志位仍然为空

实际执行:


UPDATE pms_category SET show_status=0 WHERE cat_id IN ( ? ) AND show_status=1

@TableLogic(value = "1", delval = "0")
private Integer showStatus;



2 JSR303校验



2.1 开启校验

  1. 给Bean的属性上方添加校验注解:


    javax.validation.constraints



    @NotNull



    @NotBlank(message = "自定义提示信息")
  2. 开启校验功能,在controller方法参数中的要校验的Bean前添加

    @Valid


    效果:校验错误以后会有默认的响应
  3. 给校验的参数Bean后紧跟一个BindingResult,就可以获取到校验的结果
@RequestMapping("/save")
public R save(@Valid @RequestBody BrandEntity brand, BindingResult result) {
    if (result.hasErrors()) {
        Map<String, String> map = new HashMap<>();
        result.getFieldErrors().forEach((item) -> {
            // FieldError获取到错误提示
            String message = item.getDefaultMessage();
            // 获取错误的属性的名字
            String field = item.getField();
            map.put(field, message);
        });
        return R.error(400, "提交的数据不合法").put("data", map);
    }
    brandService.save(brand);
    return R.ok();
}


常用注解



@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

验证注解的元素值是Email,也可以通过正则表达式和flag指定自定义的email格式



2.2 使用ControllerAdvice来集中处理异常

@Slf4j
@RestControllerAdvice(basePackages = "com.octopus.gulimall.product.controller")
public class GulimallExceptionControllerAdvice {

    @ExceptionHandler(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);
    }
}



2.3 分组校验

@NotBlank(message = "品牌名不能为空", groups = {AddGroup.class, UpdateGroup.class})
public R save(@Validated(AddGroup.class) @RequestBody BrandEntity brand/* , BindingResult result */)

默认没有指定分组的校验注解

@NotBlank

,在分组校验情况下不生效,只会在

@Validated

下生效



2.4 自定义校验

编写一个自定义的校验注解 – 编写一个自定义的校验, 实现ConstraintValidator的两个方法(初始化和校验方法),并添加到注解.java文件上面

validatedBy

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) {
    return set.contains(value);
}
  • 关联自定义的校验器和自定义的校验注解
  • 默认message可以读取类路径下ValidationMessages.properties中的内容
@Documented
// 可以指定多个不同的校验器,适配不同类型的校验,取决于父类ConstraintValidator的第二个范型
@Constraint(validatedBy = { ListValueConstraintValidator.class })
@Target({ METHOD, FIELD, ANNOTATION_TYPE, CONSTRUCTOR, PARAMETER, TYPE_USE })
@Retention(RUNTIME)
public @interface ListValue {
    String message() default "{com.octopus.common.valid.ListValue.message}";

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

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

    int[] vals();
}


问题1


@Pattern

正则正确匹配也报错


解决

regexp不要加前后

/


问题2

高版本SpringBoot(2.3之后)没有效果


解决

引入依赖

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-validation</artifactId>
</dependency>


问题3

校验不通过后返回的响应为503且不含错误原因


解决

在yaml中配置如下,这样返回400错误并包含错误原因

server:
  port: 10000
  error:
    include-binding-errors: always


问题4

ValidationMessages.properties 配置文件中的中文错误信息导致乱码问题


解决

添加配置类

@Configuration
public class ValidationConfig {

    @Bean
    public Validator validator() {
        LocalValidatorFactoryBean factoryBean = new LocalValidatorFactoryBean();
        
        MessageInterpolatorFactory interpolatorFactory = new MessageInterpolatorFactory();
        factoryBean.setMessageInterpolator(interpolatorFactory.getObject());

        ResourceBundleMessageSource source = new ResourceBundleMessageSource();
        source.setDefaultEncoding("utf-8");
        source.setCacheMillis(-1);
        // 文件名前缀
        source.setBasename("ValidationMessages");
        factoryBean.setValidationMessageSource(source);
        return factoryBean;
    }
}



3 POJO和SKU



3.1 电商领域名词

  • SPU(Standard Product Unit 标准化产品单元):

    iPhone 13

    决定规格参数
  • SKU(Stock Keeping Unit 库存量单位):

    iPhone 13 512GB 白色

    决定销售属性

用面向对象作类比,SPU是类,SKU是对象



3.2 Object分类

PO(persist object) 持久对象 对应数据库中的一条记录
DO(Domain Object) 领域对象 从现实世界抽象出来的有形或无形的业务实体
TO(Transfer Object) 数据传输对象 不同应用程序之间传输的对象
DTO(Data Transfer Object) 数据传输对象 泛指展示层与服务层之间的数据传输对象
VO(value object) 值对象 View Object,视图对象;接受页面传递来的对象;业务处理完成的对象封装成页面要用的数据
BO(business object) 业务对象 主要用作把业务逻辑包括的一个或多个其他的对象封装为一个对象
POJO(plain ordinary java object) 简单无规则java对象 上述全属于POJO
DAO(data access object) 数据访问对象 配合VO,提供CRUD操作



4 其他

  1. 让Entity对象在被转换为JSON格式时添加筛选条件


    @JsonInclude(JsonInclude.Include.NON_EMPTY)

    为空的属性包括数组长度为0的从JSON中移除
  2. 在application.yaml中配置jackson使返回给客户端的数据中的时间被格式化:

    spring:
      jackson:
        time-zone: GMT+8
        date-format: yyyy-MM-dd HH:mm:ss
    



六、总结



1 分布式基础概念

  • 微服务、 汪册中心,配置中心、远程调用、Feign、网关



2 基础开发

  • SpringBoot2.0、 Springcloud、 Mybatis-Plus、vue组件化、阿里云对象存储



3 环境

  • Vagrant. Linux、Docker、 MysQL、 Redis、逆向工程&人人开源



4 开发规范

  • 数据校验JSR303、全局异常处理、全局统一返回、全局跨域处理 • 枚举状态、业务状态码、VO与TO与PO划分、逻辑删除 • Lombok:

    @Data


    @SIf4j



版权声明:本文为Uracil原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。