DFA算法实现敏感词自管理 /posts/28101.html 一.需求分析

​ 在审核文本内容的时候,我们可以调用第三方成熟的服务(例如阿里云的内容安全)来实现,但是基于不同的场景,第三方的服务不可能涉及到方方面面的敏感词,比如在游戏类的场景中,开挂一词算敏感词,但是在其它的场景中,开挂一词是一个正常的词汇。这时候需要我们根据不同场景自己维护一套敏感词,在文本审核的时候,需要验证文本中是否包含这些敏感词。

二.可选方案

方案 说明
数据库模糊查询 效率太低
String.indexOf(“”)查找 数据库量大的话也是比较慢
全文检索 分词再匹配
DFA算法 确定有穷自动机(一种数据结构)

三. DFA算法

1.简介

DFA全称为:Deterministic Finite Automaton,即确定有穷自动机。

存储:一次性的把所有的敏感词存储到了多个map中,就是下图表示这种结构

敏感词:冰毒、大麻、大坏蛋

image-20231203134106231

2.检索过程

image-20231203134335076

四.工具类

package com.heima.utils.common;


import java.util.*;

public class SensitiveWordUtil {

public static Map<String, Object> dictionaryMap = new HashMap<>();


/**
* 生成关键词字典库
* @param words
* @return
*/
public static void initMap(Collection<String> words) {
if (words == null) {
System.out.println("敏感词列表不能为空");
return ;
}

// map初始长度words.size(),整个字典库的入口字数(小于words.size(),因为不同的词可能会有相同的首字)
Map<String, Object> map = new HashMap<>(words.size());
// 遍历过程中当前层次的数据
Map<String, Object> curMap = null;
Iterator<String> iterator = words.iterator();

while (iterator.hasNext()) {
String word = iterator.next();
curMap = map;
int len = word.length();
for (int i =0; i < len; i++) {
// 遍历每个词的字
String key = String.valueOf(word.charAt(i));
// 当前字在当前层是否存在, 不存在则新建, 当前层数据指向下一个节点, 继续判断是否存在数据
Map<String, Object> wordMap = (Map<String, Object>) curMap.get(key);
if (wordMap == null) {
// 每个节点存在两个数据: 下一个节点和isEnd(是否结束标志)
wordMap = new HashMap<>(2);
wordMap.put("isEnd", "0");
curMap.put(key, wordMap);
}
curMap = wordMap;
// 如果当前字是词的最后一个字,则将isEnd标志置1
if (i == len -1) {
curMap.put("isEnd", "1");
}
}
}

dictionaryMap = map;
}

/**
* 搜索文本中某个文字是否匹配关键词
* @param text
* @param beginIndex
* @return
*/
private static int checkWord(String text, int beginIndex) {
if (dictionaryMap == null) {
throw new RuntimeException("字典不能为空");
}
boolean isEnd = false;
int wordLength = 0;
Map<String, Object> curMap = dictionaryMap;
int len = text.length();
// 从文本的第beginIndex开始匹配
for (int i = beginIndex; i < len; i++) {
String key = String.valueOf(text.charAt(i));
// 获取当前key的下一个节点
curMap = (Map<String, Object>) curMap.get(key);
if (curMap == null) {
break;
} else {
wordLength ++;
if ("1".equals(curMap.get("isEnd"))) {
isEnd = true;
}
}
}
if (!isEnd) {
wordLength = 0;
}
return wordLength;
}

/**
* 获取匹配的关键词和命中次数
* @param text
* @return
*/
public static Map<String, Integer> matchWords(String text) {
Map<String, Integer> wordMap = new HashMap<>();
int len = text.length();
for (int i = 0; i < len; i++) {
int wordLength = checkWord(text, i);
if (wordLength > 0) {
String word = text.substring(i, i + wordLength);
// 添加关键词匹配次数
if (wordMap.containsKey(word)) {
wordMap.put(word, wordMap.get(word) + 1);
} else {
wordMap.put(word, 1);
}

i += wordLength - 1;
}
}
return wordMap;
}

public static void main(String[] args) {
List<String> list = new ArrayList<>();
list.add("法轮");
list.add("法轮功");
list.add("冰毒");
//初始化敏感词库
initMap(list);
String content="我是一个好人,并不会卖冰毒,也不操练法轮功,我真的不卖冰毒";
Map<String, Integer> map = matchWords(content);
System.out.println(map);
}
}

结果:

{冰毒=2, 法轮功=1}

最佳实践: 项目实战-黑马头条 | The Blog (gitee.io)

]]>
后端 DFA
Docker容器化技术 /posts/19306.html img

1.Docker概念

• Docker 是一个开源的应用容器引擎

• 诞生于 2013 年初,基于 Go 语言实现, dotCloud 公司出品(后改名为Docker Inc)

• Docker 可以让开发者打包他们的应用以及依赖包到一个轻量级、可移植的容器中,然后发布到任何流行的 Linux 机器上。

• 容器是完全使用沙箱机制,相互隔离

• 容器性能开销极低。

• Docker 从 17.03 版本之后分为 CE(Community Edition: 社区版) 和 EE(Enterprise Edition: 企业版)

2.安装Docker

# 1、yum 包更新到最新 
yum update
# 2、安装需要的软件包, yum-util 提供yum-config-manager功能,另外两个是devicemapper驱动依赖的
yum install -y yum-utils device-mapper-persistent-data lvm2
# 3、 设置yum源
yum-config-manager --add-repo https://download.docker.com/linux/centos/docker-ce.repo
# 4、 安装docker,出现输入的界面都按 y
yum install -y docker-ce
# 5、 查看docker版本,验证是否验证成功
docker -v

3.配置Docker镜像加速器

提升下载速度

第一步:登录阿里云获取镜像加速地址

每个人的加速器地址不一样

image-20230421103035776

第二步:Linux命令行执行如下命令

直接将阿里云上提供的命令直接粘贴到命令行即可

image-20230421103610184

image-20230421103755984

第三步:查看是否配置成功

# 查看刚才配置的文件是否存在
cat /etc/docker/daemon.json

出现下图所示的内容表示配置成功

image-20230421104103726

4.Docker相关命令

4.1 Docker进程相关的命令

# 启动docker服务:
systemctl start docker

# 停止docker服务:
systemctl stop docker

#重启docker服务:
systemctl restart docker

# 查看docker服务状态:
systemctl status docker

# 设置开机启动docker服务:
systemctl enable docker

4.2 Docker镜像相关的命令

查看镜像对应版本号的网站: https://hub.docker.com/

image-20230421110034731

# 查看镜像: 查看本地所有的镜像
docker images
docker images –q # 查看所用镜像的id


# 搜索镜像:从网络中查找需要的镜像
docker search 镜像名称


# 拉取镜像:从Docker仓库下载镜像到本地,镜像名称格式为 名称:版本号,如果版本号不指定则是最新的版本。
# 如果不知道镜像版本,可以去docker hub 搜索对应镜像查看。
docker pull 镜像名称 # 默认下载的是latest版本的镜像
docker pull 镜像名称:版本号 #下载指定版本的镜像


# 删除镜像: 删除本地镜像
docker rmi 镜像id # 删除指定本地镜像
docker rmi 镜像名称:版本号 #删除指定版本的镜像
docker rmi `docker images -q` # 删除所有本地镜像

4.3 容器相关的命令

# 查看容器
docker ps # 查看正在运行的容器
docker ps –a # 查看所有容器

# 创建并启动容器
docker run 参数

#删除所有的容器
docker rm $(docker ps -a -q)

参数说明:
• -i:保持容器运行。通常与 -t 同时使用。加入it这两个参数后,容器创建后自动进入容器中,退出容器后,容器自动关闭。
• -t:为容器重新分配一个伪输入终端,通常与 -i 同时使用。
• -d:以守护(后台)模式运行容器。创建一个容器在后台运行,需要使用docker exec 进入容器。退出后,容器不会关闭。
• -it 创建的容器一般称为交互式容器,-id 创建的容器一般称为守护式容器
• –name:为创建的容器命名。

# 进入容器
docker exec 参数 # 退出容器,容器不会关闭
# 停止容器
docker stop 容器名称
# 启动容器
docker start 容器名称
# 删除容器:如果容器是运行状态则删除失败,需要停止容器才能删除
docker rm 容器名称
# 查看容器信息
docker inspect 容器名称
#查看容器的日志信息
docker logs 容器名称

5.Docker 应用部署

Docker中常见的应用安装部署,详细内容请参考下面文章

Linux中开发环境的搭建 | The Blog (gitee.io)

开发环境的搭建 | The Blog (gitee.io)

5.1、部署MySQL

  1. 搜索mysql镜像
docker search mysql
  1. 拉取mysql镜像
docker pull mysql:5.6
  1. 创建容器,设置端口映射、目录映射
# 在/root目录下创建mysql目录用于存储mysql数据信息
mkdir ~/mysql
cd ~/mysql
docker run -id \
-p 3307:3306 \
--name=c_mysql \
-v $PWD/conf:/etc/mysql/conf.d \
-v $PWD/logs:/logs \
-v $PWD/data:/var/lib/mysql \
-e MYSQL_ROOT_PASSWORD=123456 \
mysql:5.6
  • 参数说明:
    • -p 3307:3306:将容器的 3306 端口映射到宿主机的 3307 端口。
    • -v $PWD/conf:/etc/mysql/conf.d:将主机当前目录下的 conf/my.cnf 挂载到容器的 /etc/mysql/my.cnf。配置目录
    • -v $PWD/logs:/logs:将主机当前目录下的 logs 目录挂载到容器的 /logs。日志目录
    • -v $PWD/data:/var/lib/mysql :将主机当前目录下的data目录挂载到容器的 /var/lib/mysql 。数据目录
    • -e MYSQL_ROOT_PASSWORD=123456:初始化 root 用户的密码。
  1. 进入容器,操作mysql
docker exec –it c_mysql /bin/bash
  1. 使用外部机器连接容器中的mysql

1573636765632

5.2、部署Tomcat

  1. 搜索tomcat镜像
docker search tomcat
  1. 拉取tomcat镜像
docker pull tomcat
  1. 创建容器,设置端口映射、目录映射
# 在/root目录下创建tomcat目录用于存储tomcat数据信息
mkdir ~/tomcat
cd ~/tomcat
docker run -id --name=c_tomcat \
-p 8080:8080 \
-v $PWD:/usr/local/tomcat/webapps \
tomcat
  • 参数说明:

    • -p 8080:8080:将容器的8080端口映射到主机的8080端口

      -v $PWD:/usr/local/tomcat/webapps:将主机中当前目录挂载到容器的webapps

  1. 使用外部机器访问tomcat

1573649804623

5.3 部署Nginx

  1. 搜索nginx镜像
docker search nginx
  1. 拉取nginx镜像
docker pull nginx
  1. 创建容器,设置端口映射、目录映射
# 在/root目录下创建nginx目录用于存储nginx数据信息
mkdir ~/nginx
cd ~/nginx
mkdir conf
cd conf
# 在~/nginx/conf/下创建nginx.conf文件,粘贴下面内容
vim nginx.conf
user  nginx;
worker_processes 1;

error_log /var/log/nginx/error.log warn;
pid /var/run/nginx.pid;


events {
worker_connections 1024;
}


http {
include /etc/nginx/mime.types;
default_type application/octet-stream;

log_format main '$remote_addr - $remote_user [$time_local] "$request" '
'$status $body_bytes_sent "$http_referer" '
'"$http_user_agent" "$http_x_forwarded_for"';

access_log /var/log/nginx/access.log main;

sendfile on;
#tcp_nopush on;

keepalive_timeout 65;

#gzip on;

include /etc/nginx/conf.d/*.conf;
}


docker run -id --name=c_nginx \
-p 80:80 \
-v $PWD/conf/nginx.conf:/etc/nginx/nginx.conf \
-v $PWD/logs:/var/log/nginx \
-v $PWD/html:/usr/share/nginx/html \
nginx
  • 参数说明:
    • -p 80:80:将容器的 80端口映射到宿主机的 80 端口。
    • -v $PWD/conf/nginx.conf:/etc/nginx/nginx.conf:将主机当前目录下的 /conf/nginx.conf 挂载到容器的 :/etc/nginx/nginx.conf。配置目录
    • -v $PWD/logs:/var/log/nginx:将主机当前目录下的 logs 目录挂载到容器的/var/log/nginx。日志目录
  1. 使用外部机器访问nginx

1573652396669

5.4 部署Redis

  1. 搜索redis镜像
docker search redis
  1. 拉取redis镜像
docker pull redis:5.0
  1. 创建容器,设置端口映射
docker run -id --name=c_redis -p 6379:6379 redis:5.0
  1. 使用外部机器连接redis
./redis-cli.exe -h 192.168.149.135 -p 6379
]]>
运维 Docker
ElementUI使用示例 /posts/50908.html ElementUI官网

一.树形显示-树形控件

1.树形显示

示例-以谷粒商城项目的商品分类为例

数据中商品分类的数据表,所有的分类数据在同一张表中

image-20230530174911409

商品分类的实体类

package com.atguigu.gulimall.product.entity;

import com.baomidou.mybatisplus.annotation.TableField;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;

import java.io.Serializable;
import java.util.Date;
import java.util.List;

import lombok.Data;

/**
* 商品三级分类
*
* @author JasonGong
* @email JasonGong@gmail.com
* @date 2023-05-19 00:23:36
*/
@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显示]
*/
private Integer showStatus;
/**
* 排序
*/
private Integer sort;
/**
* 图标地址
*/
private String icon;
/**
* 计量单位
*/
private String productUnit;
/**
* 商品数量
*/
private Integer productCount;

@TableField(exist = false)//这个注解要加,因为这个属性不是数据库中的字段
private List<CategoryEntity> children;//这里的属性名不是瞎起的,与ElementUI中的显示相对应,这里改了,前端也要改

}

封装数据,供前端显示

controller层

/**
* 查询所有的分类以及子分类,以树形结构组装起来
*/
@RequestMapping("/list/tree")
public R list(){
List<CategoryEntity> entities = categoryService.listWithTree();
return R.ok().put("data", entities);
}

service层,这里主要有一个单独的递归方法,使用的时候会改就行

/**
* 查询所有分类,以树形结构显示
*/
@Override
public List<CategoryEntity> listWithTree() {
//查出所有的分类
List<CategoryEntity> entities = categoryDao.selectList(null);
//组装成父子的树形结构
//找到所有的一级分类
List<CategoryEntity> level1List = entities.stream().filter(categoryEntity ->
//父级分类的id等于0的就是一级分类
categoryEntity.getParentCid() == 0
).map((menu) -> {
//为当前分类的子分类赋值
menu.setChildren(getChildren(menu, entities));
return menu;
}).sorted((menu1, menu2) -> {
//排序
return (menu1.getSort() == null ? 0 : menu1.getSort()) - (menu2.getSort() == null ? 0 : menu2.getSort());
}).collect(Collectors.toList());

return level1List;
}

/**
* 递归查询所有菜单的子菜单
*
* @param root 当前分类
* @param all 所有的分类数据
* @return 子菜单的集合
*/
private List<CategoryEntity> getChildren(CategoryEntity root, List<CategoryEntity> all) {
List<CategoryEntity> children = all.stream().filter(categoryEntity -> {
return categoryEntity.getParentCid().equals(root.getCatId());
}).map(categoryEntity -> {
//1.递归找子菜单
categoryEntity.setChildren(getChildren(categoryEntity, all));
return categoryEntity;
}).sorted((menu1, menu2) -> {
//2.菜单的排序
return (menu1.getSort() == null ? 0 : menu1.getSort()) - (menu2.getSort() == null ? 0 : menu2.getSort());
}).collect(Collectors.toList());
return children;
}

返回数据的格式

{
"msg": "success",
"code": 0,
"data": [
{
"catId": 1,
"name": "图书、音像、电子书刊",
"parentCid": 0,
"catLevel": 1,
"showStatus": 1,
"sort": 0,
"icon": null,
"productUnit": null,
"productCount": 0,
"children": [
{
"catId": 22,
"name": "电子书刊",
"parentCid": 1,
"catLevel": 2,
"showStatus": 1,
"sort": 0,
"icon": null,
"productUnit": null,
"productCount": 0,
"children": [
{
"catId": 165,
"name": "电子书",
"parentCid": 22,
"catLevel": 3,
"showStatus": 1,
"sort": 0,
"icon": null,
"productUnit": null,
"productCount": 0,
"children": []
},
{
"catId": 166,
"name": "网络原创",
"parentCid": 22,
"catLevel": 3,
"showStatus": 1,
"sort": 0,
"icon": null,
"productUnit": null,
"productCount": 0,
"children": []
},
{
"catId": 167,
"name": "数字杂志",
"parentCid": 22,
"catLevel": 3,
"showStatus": 1,
"sort": 0,
"icon": null,
"productUnit": null,
"productCount": 0,
"children": []
},
{
"catId": 168,
"name": "多媒体图书",
"parentCid": 22,
"catLevel": 3,
"showStatus": 1,
"sort": 0,
"icon": null,
"productUnit": null,
"productCount": 0,
"children": []
}
]
},
{
"catId": 23,
"name": "音像",
"parentCid": 1,
"catLevel": 2,
"showStatus": 1,
"sort": 0,
"icon": null,
"productUnit": null,
"productCount": 0,
"children": [
{
"catId": 169,
"name": "音乐",
"parentCid": 23,
"catLevel": 3,
"showStatus": 1,
"sort": 0,
"icon": null,
"productUnit": null,
"productCount": 0,
"children": []
},
{
"catId": 170,
"name": "影视",
"parentCid": 23,
"catLevel": 3,
"showStatus": 1,
"sort": 0,
"icon": null,
"productUnit": null,
"productCount": 0,
"children": []
},
{
"catId": 171,
"name": "教育音像",
"parentCid": 23,
"catLevel": 3,
"showStatus": 1,
"sort": 0,
"icon": null,
"productUnit": null,
"productCount": 0,
"children": []
}
]
},
略.............................

前端代码

模板

<template>
<div>
<!-- 树形控件 -->
<el-tree
:data="menus"
:props="defaultProps"
@node-click="handleNodeClick"
></el-tree>
</div>
</template>

<script>
export default {
data() {
return {
data: [],//发送请求获取的数据(比如 return R.ok().put("data", entities);里面的data)
defaultProps: {
children: 'children',//实体类中的属性(比如:private List<CategoryEntity> children;)
label: 'label'//需要显示的属性的属性名(比如商品分类名,属性名是name,这里就填name)
}
};
},
methods: {
handleNodeClick(data) {
console.log(data);
}
}
};
</script>

完整代码

<template>
<div>
<!-- 树形控件 -->
<el-tree
:data="menus"
:props="defaultProps"
@node-click="handleNodeClick"
></el-tree>
</div>
</template>

<script>
export default {
//定义变量
data() {
return {
menus: [],
defaultProps: {//这里的配置看官方的文档
children: "children",//父节点上的子节点
label: "name",//每个对象要显示的属性值,比如我们当前要显示商品分类中的分类名的值,这里就改成name
},
};
},
//钩子函数
created() {
//调用获取商品分类数据的方法
this.getMenus();
},

//方法
methods: {
handleNodeClick(data) {
console.log(data);
},
//获取所有的菜单
getMenus() {
this.$http({
url: this.$http.adornUrl("/product/category/list/tree"),
method: "get",
}).then(({ data }) => {
this.menus = data.data;
});
},
},
};
</script>

<style>
</style>

显示效果

image-20230530181102832

2.树形控件的删除、添加

页面的显示效果

image-20230605145343384

使用的组件 在树形控件的基础上添加相关的代码

image-20230605145433594

添加的时候弹出的对话框使用的组件

image-20230605153935967 image-20230605154629290

前端代码的实现

Tips:删除成功之后依然展开删除的节点

删除和添加的前端代码

<template>
<div>
<!-- 树形控件-->
<!-- :expand-on-click-node="false"设置为点击箭头的时候才会展开,避免点击删除的按钮的时候展开 -->
<!-- show-checkbox表示开启选择框,批量选择-->
<!-- node-key表示在整个树中唯一的表示 -->
<el-tree
:data="menus"
:props="defaultProps"
:expand-on-click-node="false"
show-checkbox
node-key="catId"
:default-expanded-keys="expandedKey"
>
<span class="custom-tree-node" slot-scope="{ node, data }">
<span>{{ node.label }}</span>
<span>
<el-button
v-if="node.level <= 2"
type="text"
size="mini"
@click="() => append(data)"
>
添加
</el-button>
<!-- 添加判断,没有下一级节点的时候就显示删除的按钮 -->
<el-button
v-if="data.children.length <= 0"
type="text"
size="mini"
@click="() => remove(node, data)"
>
删除
</el-button>
</span>
</span></el-tree
>
<!-- 添加分类的时候弹出的对话框 -->
<el-dialog title="提示" :visible.sync="dialogVisible" width="30%">
<el-form :model="category">
<el-form-item label="分类名称">
<el-input v-model="category.name" autocomplete="off"></el-input>
</el-form-item>
</el-form>
<span slot="footer" class="dialog-footer">
<el-button @click="dialogVisible = false">取 消</el-button>
<el-button type="primary" @click="addCategory">确 定</el-button>
</span>
</el-dialog>
</div>
</template>

<script>
export default {
//定义变量
data() {
return {
menus: [],
defaultProps: {
//这里的配置看官方的文档
children: "children", //父节点上的子节点
label: "name", //每个对象要显示的属性值,比如我们当前要显示商品分类中的分类名的值,这里就改成name
},
expandedKey: [], //需要展开的数组,用于删除之后树形控件仍然展开
dialogVisible: false, //添加菜单的对话框的开启或者关闭
//表单绑定的对象
category: {
name: "", //分类的名称
parentCid: 0, //父类的id
catLevel: 0, //分类的级别
showStatus: 1, //是否显示该分类
sort: 0, //排序
},
};
},
//钩子函数
created() {
//调用获取商品分类数据的方法
this.getMenus();
},

//方法
methods: {
//添加分类(弹出的对话框确定按钮执行的方法)
addCategory() {
console.log("提交的数据:", this.category);
//将添加的数据返回给后端
this.$http({
url: this.$http.adornUrl("/product/category/save"),
method: "post",
data: this.$http.adornData(this.category, false),
}).then(({ data }) => {
this.$message({
message: "菜单添加成功",
type: "success",
});
//刷新一下页面
this.getMenus()
//设置需要默认展开的菜单(依然展开刚才添加的节点)
this.expandedKey = [this.category.parentCid];
});
//关闭对话框
this.dialogVisible = false;
},
//添加分类的方法
append(data) {
//清空一下表单的数据
this.category.name = "";
//打开添加的对话框
this.dialogVisible = true;
//为分类的对象赋值
this.category.parentCid = data.catId;
this.category.catLevel = data.catLevel * 1 + 1; //防止catLevel是一个字符串
},
//删除节点的方法
remove(node, data) {
var ids = [data.catId];
this.$confirm(`是否要删除[${data.name}]菜单?`, "提示", {
confirmButtonText: "确定",
cancelButtonText: "取消",
type: "warning",
})
.then(() => {
this.$http({
url: this.$http.adornUrl("/product/category/delete"),
method: "post",
data: this.$http.adornData(ids, false),
}).then(({ data }) => {
this.$message({
message: "菜单删除成功",
type: "success",
});
//刷新一下页面
this.getMenus();
//设置需要默认展开的菜单(依然展开刚才删除的节点)
this.expandedKey = [node.parent.data.catId];
});
})
.catch(() => {
this.$message({
type: "info",
message: "已取消删除",
});
});
},
//获取所有的菜单
getMenus() {
this.$http({
url: this.$http.adornUrl("/product/category/list/tree"),
method: "get",
}).then(({ data }) => {
this.menus = data.data;
});
},
},
};
</script>

<style>
</style>

后端代码

删除的后端代码

/**
* 删除
*/
@RequestMapping("/delete")
//@RequiresPermissions("product:category:delete")
public R delete(@RequestBody Long[] catIds) {
//检查当前删除的菜单,是否被别的地方引用
//categoryService.removeByIds(Arrays.asList(catIds));
categoryService.removeMenuByIds(Arrays.asList(catIds));
return R.ok();
}

添加的后端代码

/**
* 保存
*/
@RequestMapping("/save")
//@RequiresPermissions("product:category:save")
public R save(@RequestBody CategoryEntity category) {
categoryService.save(category);

return R.ok();
}

实现的效果

image-20230605163004850

]]>
前端 前端
Git命令速查 /posts/18459.html 安装教程

官网下载:https://git-scm.com/downloads

#以下的操作在下载安装完毕之后进行
#1.鼠标在桌面右键 选择Git Bash Here 打开控制台

#2.配置用户名和邮箱
git config --global user.name "用户名" #随意
git config --global user.email "邮箱" #自己的邮箱

#3.配置SSH免密连接
#生成密钥
ssh-keygen -t rsa -C "在码云上注册的邮箱地址" #连续三次回车
#查看密钥并复制公钥的内容
cat ~/.ssh/id_rsa.pub

#4.将密钥的复制到码云的SSH公钥中
#4.1添加公钥 公钥名随意 公钥内容就是上面复制的内容

#5.测试
ssh -T git@gitee.com

常用命令

image-20230520173824247

]]>
后端 Git
JWT-token生成工具 /posts/28118.html 1.介绍

​ JWT(Json Web Token)是为了在网络应用环境间传递声明而执行的一种基于JSON的开放标准。JWT的声明一般被用来在身份提供者和服务提供者间传递被认证的用户身份信息,以便于从资源服务器获取资源。比如用在用户登录上。JWT最重要的作用就是对 token信息的防伪作用。

​ JWT的原理:

​ 一个JWT由三个部分组成:公共部分、私有部分、签名部分。最后由这三者组合进行base64编码得到JWT。

image-20230420102523288

image-20230420103234232

1、 公共部分

​ 主要是该JWT的相关配置参数,比如签名的加密算法、格式类型、过期时间等等。

​ Key=ATGUIGU

2、 私有部分

​ 用户自定义的内容,根据实际需要真正要封装的信息。

​ userInfo{用户的Id,用户的昵称nickName}

3、 签名部分

​ SaltiP: 当前服务器的Ip地址!{linux 中配置代理服务器的ip}

​ 主要用户对JWT生成字符串的时候,进行加密{盐值}

最终组成 key+salt+userInfo -> token!

base64编码,并不是加密,只是把明文信息变成了不可见的字符串。但是其实只要用一些工具就可以把base64编码解成明文,所以不要在JWT中放入涉及私密的信息。

2.使用

1.导入依赖

<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt</artifactId>
<version>0.7.0</version>
</dependency>

2.引入工具类

以下工具类根据情况任选一种即可

一种是在请求头中获取token,然后再获取用户的信息,另一种直接传入token,然后获取用户信息

package com.atguigu.yygh.common.helper;

import io.jsonwebtoken.*;
import org.springframework.util.StringUtils;

import java.util.Date;

/**
* @author GongChangjiang
* @version 1.0
* @Date 2023/4/20
* @Description
*/
public class JwtHelper {
private static long tokenExpiration = 24*60*60*1000; //token的过期时间
private static String tokenSignKey = "123456"; //token 签名的密钥

/**
* 根据参数生成token
* @param userId 用户的id
* @param userName 用户名
* @return token字符串
*/
public static String createToken(Long userId, String userName) {
String token = Jwts.builder()
.setSubject("YYGH-USER") //分类,自定义
.setExpiration(new Date(System.currentTimeMillis() + tokenExpiration))//设置过期时间
.claim("userId", userId)//设置主体信息
.claim("userName", userName)
.signWith(SignatureAlgorithm.HS512, tokenSignKey)//签名hash
.compressWith(CompressionCodecs.GZIP)
.compact();
return token;
}

/**
* 根据token字符串得到用户的id
*/
public static Long getUserId(String token) {
if(StringUtils.isEmpty(token)) return null;
Jws<Claims> claimsJws = Jwts.parser().setSigningKey(tokenSignKey).parseClaimsJws(token);
Claims claims = claimsJws.getBody();
Integer userId = (Integer)claims.get("userId");
return userId.longValue();
}

/**
* 根据token字符串得到用户名
*/
public static String getUserName(String token) {
if(StringUtils.isEmpty(token)) return "";
Jws<Claims> claimsJws
= Jwts.parser().setSigningKey(tokenSignKey).parseClaimsJws(token);
Claims claims = claimsJws.getBody();
return (String)claims.get("userName");
}
}
package com.atguigu.commonutils;

import io.jsonwebtoken.Claims;
import io.jsonwebtoken.Jws;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.SignatureAlgorithm;
import org.springframework.http.server.reactive.ServerHttpRequest;
import org.springframework.util.StringUtils;

import javax.servlet.http.HttpServletRequest;
import java.util.Date;

/**
* @author helen
* @since 2019/10/16
*/
public class JwtUtils {

/**
* 设置token的过期时间
*/
public static final long EXPIRE = 1000 * 60 * 60 * 24;
/**
* 密钥
*/
public static final String APP_SECRET = "ukc8BDbRigUDaY6pZFfWus2jZWLPHO";


/**
* 生成token字符串
*/
public static String getJwtToken(String id, String nickname){

String JwtToken = Jwts.builder()
.setHeaderParam("typ", "JWT")
.setHeaderParam("alg", "HS256")
.setSubject("guli-user")
.setIssuedAt(new Date())
.setExpiration(new Date(System.currentTimeMillis() + EXPIRE))
.claim("id", id)
.claim("nickname", nickname)
.signWith(SignatureAlgorithm.HS256, APP_SECRET)
.compact();

return JwtToken;
}

/**
* 判断token是否存在与有效
*/
public static boolean checkToken(String jwtToken) {
if(StringUtils.isEmpty(jwtToken)) return false;
try {
Jwts.parser().setSigningKey(APP_SECRET).parseClaimsJws(jwtToken);
} catch (Exception e) {
e.printStackTrace();
return false;
}
return true;
}

/**
* 判断token是否存在与有效
*/
public static boolean checkToken(HttpServletRequest request) {
try {
String jwtToken = request.getHeader("token");
if(StringUtils.isEmpty(jwtToken)) return false;
Jwts.parser().setSigningKey(APP_SECRET).parseClaimsJws(jwtToken);
} catch (Exception e) {
e.printStackTrace();
return false;
}
return true;
}

/**
* 根据token获取会员id
*/
public static String getMemberIdByJwtToken(HttpServletRequest request) {
String jwtToken = request.getHeader("token");
if(StringUtils.isEmpty(jwtToken)) return "";
Jws<Claims> claimsJws = Jwts.parser().setSigningKey(APP_SECRET).parseClaimsJws(jwtToken);
Claims claims = claimsJws.getBody();
return (String)claims.get("id");
}
}

3.在登录中使用Jwt工具

1.登录成功之后返回token字符串

image-20230420110340570

2.前端处理的思路

获取token字符串 -> 将token字符串和用户信息存放在cokkie中 -> 页面在cokkie中获取用户信息显示在页面上

前端登录与否的校验、后端网关登录与否的校验…….

之后补充完整的代码示例 这里没有遇到好的登录示例 ……….

]]>
后端 Java
Java爬虫 /posts/60685.html 视频教程1: 狂神说Java Jsoup爬虫入门实战 https://www.bilibili.com/video/BV1La4y1x7Wm?vd_source=aee5e475191b69e6c781059ab6662584

视频教程2:https://www.bilibili.com/video/BV1RU4y147eZ?vd_source=aee5e475191b69e6c781059ab6662584

具体的看视频 急速入门

入门实战教程

1.引入依赖

<!-- 爬取视频或者音频需要使用别的依赖 -->
<dependency>
<groupId>org.jsoup</groupId>
<artifactId>jsoup</artifactId>
<version>1.14.3</version>
</dependency>

2.编写测试代码

下面我们以爬取京东上的商品的图片和价格为例

package com.jason;


import org.jsoup.Jsoup;
import org.jsoup.nodes.Document;
import org.jsoup.nodes.Element;
import org.jsoup.select.Elements;

import java.io.IOException;
import java.net.URL;

/**
* @author GongChangjiang
* @version 1.0
* @Date 2023/5/9
* @Description
*/
public class HtmlParseUtil {
public static void main(String[] args) throws IOException {
HtmlParseUtil.parseJd("联想电脑");
}

/**
* 爬取京东上的商品数据
*
* @param keywords 商品的品牌或者关键字 例如 联想电脑、华为手机、java书籍
*/
public static void parseJd(String keywords) throws IOException {
//获取请求
String url = "https://search.jd.com/Search?keyword=" + keywords;//页面上的地址信息
//解析网页
//第一个参数是url地址
//第二个参数是响应时间,超过30秒就报错
//返回的document就是浏览器的Document对象
Document document = Jsoup.parse(new URL(url), 30000);
//所有在js中可以使用的方法,这里都可以使用
Element element = document.getElementById("J_goodsList");//通过id获取元素
//获取所有的li元素
Elements elements = element.getElementsByTag("li");
//遍历li元素的集合 获取每一个li元素
for (Element el : elements) {
//这种图片特别多的图片 会有一个懒加载
String attr = el.getElementsByTag("img").eq(0).attr("data-lazy-img");//通过标签获取元素
String title = el.getElementsByClass("p-name").eq(0).text();//通过class获取元素
String price = el.getElementsByClass("p-price").eq(0).text();
System.out.println("图片地址:https:" + attr);
System.out.println("商品名称:" + title);
System.out.println("商品价格:" + price);
}
}
}

3.运行测试

image-20230509161845558

image-20230509161907139

示例

爬取房天下上面的房价信息

package com.jason;

import org.jsoup.Jsoup;
import org.jsoup.nodes.Document;
import org.jsoup.nodes.Element;
import org.jsoup.select.Elements;

import java.io.IOException;
import java.net.URL;

/**
* @author GongChangjiang
* @version 1.0
* @Date 2023/5/9
* @Description
*/
public class ParseHouse {
public static void main(String[] args) throws IOException {
//爬起房天下上面的房源数据
int i = 91;
while (true){//换页读取 页面有分页条的情况
String url = "https://newhouse.fang.com/house/s/b"+i;
System.out.println("正在爬取第"+i+"页的数据.....");
ParseHouse.selectHouses(url);
i++;
}

}

public static void selectHouses(String url) throws IOException {
Document document = Jsoup.parse(new URL(url), 30000);
Element element = document.getElementById("newhouse_loupan_list");
Elements elements = element.getElementsByTag("li");
for (Element el : elements) {
String images = el.getElementsByTag("img").eq(1).attr("src");
String alt = el.getElementsByTag("img").eq(1).attr("alt");
String price = el.getElementsByClass("nhouse_price").text();
System.out.println("图片:https:"+images);
System.out.println("名称:"+alt);
System.out.println("房价:"+price);
}
}
}

image-20230509180334969

image-20230509180435467

爬取豆瓣电影排行榜的信息

image-20230509180547905

爬取全国各地房价的信息到excel表格中

package com.jason;

import com.alibaba.excel.EasyExcel;
import com.jason.entity.HorseInfo;
import org.jsoup.Jsoup;
import org.jsoup.nodes.Document;
import org.jsoup.nodes.Element;
import org.jsoup.select.Elements;

import java.io.File;
import java.io.IOException;
import java.net.URL;
import java.util.ArrayList;
import java.util.List;

/**
* @author GongChangjiang
* @version 1.0
* @Date 2023/5/9
* @Description 爬取全国各地房价的信息到excel表格中
*/
public class HousePriseList {
public static void main(String[] args) throws IOException {
//爬取全国房价排行榜
String fileName = "C:\\AlYun\\work\\housePriceList.xlsx";
//通过下面的中间表,我们可以实现向excel中追加数据 不是每次删除之前的数据再写入 或者追加的时候有多个表头信息
String temp = "C:\\AlYun\\work\\temp.xlsx"; //中间表 会自动被删除
int i = 1;
while (true) {
try {
String url = "https://fangjia.gotohui.com/top/" + i + ".html";
List<HorseInfo> horseInfos = HousePriseList.selectHorse(url);
File file = new File(fileName);
File tempFile = new File(temp);
if (file.exists()) { //判断excel表中是否写入过数据 写入过数据就追加数据
// 第二次按照原有格式,不需要表头,追加写入
EasyExcel.write(file, HorseInfo.class).needHead(false).
withTemplate(file).file(tempFile).sheet().doWrite(horseInfos);
} else {
// 第一次写入需要表头
EasyExcel.write(file, HorseInfo.class).sheet().doWrite(horseInfos);
}
if (tempFile.exists()) {
file.delete();
tempFile.renameTo(file);
}
i++;
Thread.sleep(2000);
} catch (Exception e) {
e.printStackTrace();
break;
}
}
}

public static List<HorseInfo> selectHorse(String url) throws IOException {
Document document = Jsoup.parse(new URL(url), 30000);
Elements elements = document.getElementsByClass("ntable table-striped table-hover");
ArrayList<HorseInfo> horseInfos = new ArrayList<>();
for (Element element : elements) {
Elements tr = element.getElementsByTag("tr");
for (Element el : tr) {
Elements td = el.getElementsByTag("td");
System.out.println("-----------------------------------------");
System.out.println("编号:" + td.eq(0).text());
System.out.println("城市:" + td.eq(1).text());
System.out.println("单价(元/㎡):" + td.eq(2).text());
System.out.println("同比(去年):" + td.eq(3).text());
System.out.println("环比(上月):" + td.eq(4).text());
System.out.println("收入比:" + td.eq(5).text());
if (!td.eq(0).text().equals("")) {
horseInfos.add(new HorseInfo(
td.eq(0).text(),
td.eq(1).text(),
td.eq(2).text(),
td.eq(3).text(),
td.eq(4).text(),
td.eq(5).text()
));
}

}
}
return horseInfos;
}
}

image-20230509233922789

image-20230509234019249

]]>
后端 爬虫
ElasticSearch /posts/22654.html 官网: https://www.elastic.co/cn/elasticsearch/

简介

​ Elasticsearch 是一个分布式、RESTful 风格的搜索和数据分析引擎,能够解决不断涌现出的各种用例。作为 Elastic Stack 的核心,Elasticsearch 会集中存储您的数据,让您飞快完成搜索,微调相关性,进行强大的分析,并轻松缩放规模。

一.基本概念

1.Index(索引)

动词:相当于mysql的insert

名词:相当于mysql的database

2.Type(类型)

在index中可以定义一个或者多个类型

类似于mysql中的Table;每一种类型的数据放在一起

3.Document(文档)

保存在某个索引(index)下,某种类型(Type)的一个数据(Document),文档是JSON格式的,

Document就是像mysql中的某个Table里面的内容。

4.倒排索引

image-20230628180000365

二.Docker安装ElasticSearch

1.安装ElasticSearch(存储和检索数据)

# 拉取ElasticSearch
docker pull elasticsearch:7.4.2

#ES的配置文件存放的位置
mkdir -p /mydata/elasticsearch/config

#ES相关的数据
mkdir -p /mydata/elasticsearch/data

#"http.host: 0.0.0.0"(可以被任何的机器访问)的配置写入elasticsearch.yml中(注意冒号后面的空格)
echo "http.host: 0.0.0.0" >> /mydata/elasticsearch/config/elasticsearch.yml

#设置文件的权限
# 递归更改权限,es需要访问
chmod -R 777 /mydata/elasticsearch/

#运行容器的命令 9200端口用于发送请求使用 9300端口用于集群中节点中的通信使用
#单节点运行
#初始占用64m,最大占用512m(不指定,ES会占用所有的内存)
docker run --name elasticsearch -p 9200:9200 -p 9300:9300 \
-e "discovery.type=single-node" \
-e ES_JAVA_OPTS="-Xms64m -Xmx512m" \
-v /mydata/elasticsearch/config/elasticsearch.yml:/usr/share/elasticsearch/config/elasticsearch.yml \
-v /mydata/elasticsearch/data:/usr/share/elasticsearch/data \
-v /mydata/elasticsearch/plugins:/usr/share/elasticsearch/plugins \
-d elasticsearch:7.4.2

# 设置开机启动elasticsearch
docker update elasticsearch --restart=always

#访问端口 测试是否安装成功
http://虚拟机的ip:9200/

image-20230628191936254

2.安装Kibana(可视化检索数据)

#拉取Kibana
docker pull kibana:7.4.2
# 这里-e是自己的elasticsearch服务地址(这里的地址一定要改为自己虚拟机的地址)
docker run --name kibana -e ELASTICSEARCH_HOSTS=http://192.168.195.100:9200 -p 5601:5601 -d kibana:7.4.2
# 设置开机启动kibana
docker update kibana --restart=always
#访问对应的网址查看服务
http://虚拟机的ip:5601

image-20230701104616032

三.初步检索

1._Cat

GET/_cat/nodes 查看所有节点

GET/_cat/health 查看es的健康状态

GET/_cat/master 查看主节点

GET/_cat/indices 查看所有索引

2.索引文档(保存)

保存操作可以发送PUT或者POST请求

put需要在路径上指定id

post请求可以不指定id,自动生成一个唯一标识

例:PUT customer/external/1 在customer索引下的external类型下保存1号数据为

-> PUT customer/external/1
//请求体
{
"name":"John Doe"
}

image-20230701144040577

返回的数据解析

//_开头的数据称为元数据
{
"_index": "customer",//数据存在的数据库
"_type": "external",//保存数据的类型
"_id": "1",//数据的id
"_version": 1,//数据的版本
"result": "created",//保存为"created",修改为"updated",发送多次为更新的操作
"_shards": { //分片
"total": 2,
"successful": 1,
"failed": 0
},
"_seq_no": 0,
"_primary_term": 1
}

3.查询文档

-> GET customer/external/1

image-20230701145518348

返回数据解析

{
"_index": "customer",
"_type": "external",
"_id": "1",
"_version": 1,
"_seq_no": 0,//并发控制的字段,每次更新就会+1,用来做乐观锁
"_primary_term": 1,//同上,主分片重新分配,如重启,就会有变化
"found": true,//是否找到
"_source": {//数据真正的内容
"name": "John Doe"
}
}

4.更新文档

POST customer/external/1/_update  //对比原先的数据,和原先的一样_version和_seq_no不会发生改变
{
"doc":{
"name": "John Doe"
}
}
或者
POST customer/external/1 //和之前的数据一样的话也会执行更新操作
{
"name": "John Doe"
}
或者
PUT customer/external/1
{
"name": "John Doe"
}

​ 使用POST customer/external/1/_update 的方式进行更新会对比原先的数据,和原先的一样version和_seq_no不会发生改变,下面是我们使用同样的数据发送两次更新请求,第二次的result显示”noop”,没有进行操作

image-20230701151445190

5.删除文档|索引

DELETE customer/external/1
DELETE customer

删除文档

image-20230701152501989

删除索引

image-20230701152648756

6.bulk批量API

Kibana的使用

image-20230701153754438

批量保存数据

POST /customer/external/_bulk
{"index":{"_id":"1"}}
{"name":"a"}
{"index":{"_id":"2"}}
{"name":"b"}
新增两条数据
新增id为1的 对应 name为a
新增Id为2的 对应 name为b

image-20230701154217181

复杂批量操作

POST /_bulk
{"delete":{"_index":"website","_type":"blog","_id":"123"}}
{"create":{"_index":"website","_type":"blog","_id":"123"}}
{"title":"my first blog post"}
{"index":{"_index":"website","_type":"blog"}}
{"title":"my second blog post"}
{"update":{"_index":"website","_type":"blog","_id":"123"}}
{"doc":{"title":"my updated blog post"}}

image-20230701154839574

7.样本测试数据

POST /bank/account/_bulk
{"index":{"_id":"1"}}
{"account_number":1,"balance":39225,"firstname":"Amber","lastname":"Duke","age":32,"gender":"M","address":"880 Holmes Lane","employer":"Pyrami","email":"amberduke@pyrami.com","city":"Brogan","state":"IL"}
{"index":{"_id":"6"}}
{"account_number":6,"balance":5686,"firstname":"Hattie","lastname":"Bond","age":36,"gender":"M","address":"671 Bristol Street","employer":"Netagy","email":"hattiebond@netagy.com","city":"Dante","state":"TN"}
{"index":{"_id":"13"}}
{"account_number":13,"balance":32838,"firstname":"Nanette","lastname":"Bates","age":28,"gender":"F","address":"789 Madison Street","employer":"Quility","email":"nanettebates@quility.com","city":"Nogal","state":"VA"}
{"index":{"_id":"18"}}
{"account_number":18,"balance":4180,"firstname":"Dale","lastname":"Adams","age":33,"gender":"M","address":"467 Hutchinson Court","employer":"Boink","email":"daleadams@boink.com","city":"Orick","state":"MD"}
{"index":{"_id":"20"}}
{"account_number":20,"balance":16418,"firstname":"Elinor","lastname":"Ratliff","age":36,"gender":"M","address":"282 Kings Place","employer":"Scentric","email":"elinorratliff@scentric.com","city":"Ribera","state":"WA"}
{"index":{"_id":"25"}}
{"account_number":25,"balance":40540,"firstname":"Virginia","lastname":"Ayala","age":39,"gender":"F","address":"171 Putnam Avenue","employer":"Filodyne","email":"virginiaayala@filodyne.com","city":"Nicholson","state":"PA"}
{"index":{"_id":"32"}}
{"account_number":32,"balance":48086,"firstname":"Dillard","lastname":"Mcpherson","age":34,"gender":"F","address":"702 Quentin Street","employer":"Quailcom","email":"dillardmcpherson@quailcom.com","city":"Veguita","state":"IN"}
{"index":{"_id":"37"}}
{"account_number":37,"balance":18612,"firstname":"Mcgee","lastname":"Mooney","age":39,"gender":"M","address":"826 Fillmore Place","employer":"Reversus","email":"mcgeemooney@reversus.com","city":"Tooleville","state":"OK"}
{"index":{"_id":"44"}}
{"account_number":44,"balance":34487,"firstname":"Aurelia","lastname":"Harding","age":37,"gender":"M","address":"502 Baycliff Terrace","employer":"Orbalix","email":"aureliaharding@orbalix.com","city":"Yardville","state":"DE"}
{"index":{"_id":"49"}}
{"account_number":49,"balance":29104,"firstname":"Fulton","lastname":"Holt","age":23,"gender":"F","address":"451 Humboldt Street","employer":"Anocha","email":"fultonholt@anocha.com","city":"Sunriver","state":"RI"}
{"index":{"_id":"51"}}
{"account_number":51,"balance":14097,"firstname":"Burton","lastname":"Meyers","age":31,"gender":"F","address":"334 River Street","employer":"Bezal","email":"burtonmeyers@bezal.com","city":"Jacksonburg","state":"MO"}
{"index":{"_id":"56"}}
{"account_number":56,"balance":14992,"firstname":"Josie","lastname":"Nelson","age":32,"gender":"M","address":"857 Tabor Court","employer":"Emtrac","email":"josienelson@emtrac.com","city":"Sunnyside","state":"UT"}
{"index":{"_id":"63"}}
{"account_number":63,"balance":6077,"firstname":"Hughes","lastname":"Owens","age":30,"gender":"F","address":"510 Sedgwick Street","employer":"Valpreal","email":"hughesowens@valpreal.com","city":"Guilford","state":"KS"}
{"index":{"_id":"68"}}
{"account_number":68,"balance":44214,"firstname":"Hall","lastname":"Key","age":25,"gender":"F","address":"927 Bay Parkway","employer":"Eventex","email":"hallkey@eventex.com","city":"Shawmut","state":"CA"}
{"index":{"_id":"70"}}
{"account_number":70,"balance":38172,"firstname":"Deidre","lastname":"Thompson","age":33,"gender":"F","address":"685 School Lane","employer":"Netplode","email":"deidrethompson@netplode.com","city":"Chestnut","state":"GA"}
{"index":{"_id":"75"}}
{"account_number":75,"balance":40500,"firstname":"Sandoval","lastname":"Kramer","age":22,"gender":"F","address":"166 Irvington Place","employer":"Overfork","email":"sandovalkramer@overfork.com","city":"Limestone","state":"NH"}
{"index":{"_id":"82"}}
{"account_number":82,"balance":41412,"firstname":"Concetta","lastname":"Barnes","age":39,"gender":"F","address":"195 Bayview Place","employer":"Fitcore","email":"concettabarnes@fitcore.com","city":"Summerfield","state":"NC"}
{"index":{"_id":"87"}}
{"account_number":87,"balance":1133,"firstname":"Hewitt","lastname":"Kidd","age":22,"gender":"M","address":"446 Halleck Street","employer":"Isologics","email":"hewittkidd@isologics.com","city":"Coalmont","state":"ME"}
{"index":{"_id":"94"}}
{"account_number":94,"balance":41060,"firstname":"Brittany","lastname":"Cabrera","age":30,"gender":"F","address":"183 Kathleen Court","employer":"Mixers","email":"brittanycabrera@mixers.com","city":"Cornucopia","state":"AZ"}
{"index":{"_id":"99"}}
{"account_number":99,"balance":47159,"firstname":"Ratliff","lastname":"Heath","age":39,"gender":"F","address":"806 Rockwell Place","employer":"Zappix","email":"ratliffheath@zappix.com","city":"Shaft","state":"ND"}
{"index":{"_id":"102"}}
{"account_number":102,"balance":29712,"firstname":"Dena","lastname":"Olson","age":27,"gender":"F","address":"759 Newkirk Avenue","employer":"Hinway","email":"denaolson@hinway.com","city":"Choctaw","state":"NJ"}
{"index":{"_id":"107"}}
{"account_number":107,"balance":48844,"firstname":"Randi","lastname":"Rich","age":28,"gender":"M","address":"694 Jefferson Street","employer":"Netplax","email":"randirich@netplax.com","city":"Bellfountain","state":"SC"}
{"index":{"_id":"114"}}
{"account_number":114,"balance":43045,"firstname":"Josephine","lastname":"Joseph","age":31,"gender":"F","address":"451 Oriental Court","employer":"Turnabout","email":"josephinejoseph@turnabout.com","city":"Sedley","state":"AL"}
{"index":{"_id":"119"}}
{"account_number":119,"balance":49222,"firstname":"Laverne","lastname":"Johnson","age":28,"gender":"F","address":"302 Howard Place","employer":"Senmei","email":"lavernejohnson@senmei.com","city":"Herlong","state":"DC"}
{"index":{"_id":"121"}}
{"account_number":121,"balance":19594,"firstname":"Acevedo","lastname":"Dorsey","age":32,"gender":"M","address":"479 Nova Court","employer":"Netropic","email":"acevedodorsey@netropic.com","city":"Islandia","state":"CT"}
{"index":{"_id":"126"}}
{"account_number":126,"balance":3607,"firstname":"Effie","lastname":"Gates","age":39,"gender":"F","address":"620 National Drive","employer":"Digitalus","email":"effiegates@digitalus.com","city":"Blodgett","state":"MD"}
{"index":{"_id":"133"}}
{"account_number":133,"balance":26135,"firstname":"Deena","lastname":"Richmond","age":36,"gender":"F","address":"646 Underhill Avenue","employer":"Sunclipse","email":"deenarichmond@sunclipse.com","city":"Austinburg","state":"SC"}
{"index":{"_id":"138"}}
{"account_number":138,"balance":9006,"firstname":"Daniel","lastname":"Arnold","age":39,"gender":"F","address":"422 Malbone Street","employer":"Ecstasia","email":"danielarnold@ecstasia.com","city":"Gardiner","state":"MO"}
{"index":{"_id":"140"}}
{"account_number":140,"balance":26696,"firstname":"Cotton","lastname":"Christensen","age":32,"gender":"M","address":"878 Schermerhorn Street","employer":"Prowaste","email":"cottonchristensen@prowaste.com","city":"Mayfair","state":"LA"}
{"index":{"_id":"145"}}
{"account_number":145,"balance":47406,"firstname":"Rowena","lastname":"Wilkinson","age":32,"gender":"M","address":"891 Elton Street","employer":"Asimiline","email":"rowenawilkinson@asimiline.com","city":"Ripley","state":"NH"}
{"index":{"_id":"152"}}
{"account_number":152,"balance":8088,"firstname":"Wolfe","lastname":"Rocha","age":21,"gender":"M","address":"457 Guernsey Street","employer":"Hivedom","email":"wolferocha@hivedom.com","city":"Adelino","state":"MS"}
{"index":{"_id":"157"}}
{"account_number":157,"balance":39868,"firstname":"Claudia","lastname":"Terry","age":20,"gender":"F","address":"132 Gunnison Court","employer":"Lumbrex","email":"claudiaterry@lumbrex.com","city":"Castleton","state":"MD"}
{"index":{"_id":"164"}}
{"account_number":164,"balance":9101,"firstname":"Cummings","lastname":"Little","age":26,"gender":"F","address":"308 Schaefer Street","employer":"Comtrak","email":"cummingslittle@comtrak.com","city":"Chaparrito","state":"WI"}
{"index":{"_id":"169"}}
{"account_number":169,"balance":45953,"firstname":"Hollie","lastname":"Osborn","age":34,"gender":"M","address":"671 Seaview Court","employer":"Musaphics","email":"hollieosborn@musaphics.com","city":"Hanover","state":"GA"}
{"index":{"_id":"171"}}
{"account_number":171,"balance":7091,"firstname":"Nelda","lastname":"Hopper","age":39,"gender":"M","address":"742 Prospect Place","employer":"Equicom","email":"neldahopper@equicom.com","city":"Finderne","state":"SC"}
{"index":{"_id":"176"}}
{"account_number":176,"balance":18607,"firstname":"Kemp","lastname":"Walters","age":28,"gender":"F","address":"906 Howard Avenue","employer":"Eyewax","email":"kempwalters@eyewax.com","city":"Why","state":"KY"}
{"index":{"_id":"183"}}
{"account_number":183,"balance":14223,"firstname":"Hudson","lastname":"English","age":26,"gender":"F","address":"823 Herkimer Place","employer":"Xinware","email":"hudsonenglish@xinware.com","city":"Robbins","state":"ND"}
{"index":{"_id":"188"}}
{"account_number":188,"balance":41504,"firstname":"Tia","lastname":"Miranda","age":24,"gender":"F","address":"583 Ainslie Street","employer":"Jasper","email":"tiamiranda@jasper.com","city":"Summerset","state":"UT"}
{"index":{"_id":"190"}}
{"account_number":190,"balance":3150,"firstname":"Blake","lastname":"Davidson","age":30,"gender":"F","address":"636 Diamond Street","employer":"Quantasis","email":"blakedavidson@quantasis.com","city":"Crumpler","state":"KY"}
{"index":{"_id":"195"}}
{"account_number":195,"balance":5025,"firstname":"Kaye","lastname":"Gibson","age":31,"gender":"M","address":"955 Hopkins Street","employer":"Zork","email":"kayegibson@zork.com","city":"Ola","state":"WY"}
{"index":{"_id":"203"}}
{"account_number":203,"balance":21890,"firstname":"Eve","lastname":"Wyatt","age":33,"gender":"M","address":"435 Furman Street","employer":"Assitia","email":"evewyatt@assitia.com","city":"Jamestown","state":"MN"}
{"index":{"_id":"208"}}
{"account_number":208,"balance":40760,"firstname":"Garcia","lastname":"Hess","age":26,"gender":"F","address":"810 Nostrand Avenue","employer":"Quiltigen","email":"garciahess@quiltigen.com","city":"Brooktrails","state":"GA"}
{"index":{"_id":"210"}}
{"account_number":210,"balance":33946,"firstname":"Cherry","lastname":"Carey","age":24,"gender":"M","address":"539 Tiffany Place","employer":"Martgo","email":"cherrycarey@martgo.com","city":"Fairacres","state":"AK"}
{"index":{"_id":"215"}}
{"account_number":215,"balance":37427,"firstname":"Copeland","lastname":"Solomon","age":20,"gender":"M","address":"741 McDonald Avenue","employer":"Recognia","email":"copelandsolomon@recognia.com","city":"Edmund","state":"ME"}
{"index":{"_id":"222"}}
{"account_number":222,"balance":14764,"firstname":"Rachelle","lastname":"Rice","age":36,"gender":"M","address":"333 Narrows Avenue","employer":"Enaut","email":"rachellerice@enaut.com","city":"Wright","state":"AZ"}
{"index":{"_id":"227"}}
{"account_number":227,"balance":19780,"firstname":"Coleman","lastname":"Berg","age":22,"gender":"M","address":"776 Little Street","employer":"Exoteric","email":"colemanberg@exoteric.com","city":"Eagleville","state":"WV"}
{"index":{"_id":"234"}}
{"account_number":234,"balance":44207,"firstname":"Betty","lastname":"Hall","age":37,"gender":"F","address":"709 Garfield Place","employer":"Miraclis","email":"bettyhall@miraclis.com","city":"Bendon","state":"NY"}
{"index":{"_id":"239"}}
{"account_number":239,"balance":25719,"firstname":"Chang","lastname":"Boyer","age":36,"gender":"M","address":"895 Brigham Street","employer":"Qaboos","email":"changboyer@qaboos.com","city":"Belgreen","state":"NH"}
{"index":{"_id":"241"}}
{"account_number":241,"balance":25379,"firstname":"Schroeder","lastname":"Harrington","age":26,"gender":"M","address":"610 Tapscott Avenue","employer":"Otherway","email":"schroederharrington@otherway.com","city":"Ebro","state":"TX"}
{"index":{"_id":"246"}}
{"account_number":246,"balance":28405,"firstname":"Katheryn","lastname":"Foster","age":21,"gender":"F","address":"259 Kane Street","employer":"Quantalia","email":"katherynfoster@quantalia.com","city":"Bath","state":"TX"}
{"index":{"_id":"253"}}
{"account_number":253,"balance":20240,"firstname":"Melissa","lastname":"Gould","age":31,"gender":"M","address":"440 Fuller Place","employer":"Buzzopia","email":"melissagould@buzzopia.com","city":"Lumberton","state":"MD"}
{"index":{"_id":"258"}}
{"account_number":258,"balance":5712,"firstname":"Lindsey","lastname":"Hawkins","age":37,"gender":"M","address":"706 Frost Street","employer":"Enormo","email":"lindseyhawkins@enormo.com","city":"Gardners","state":"AK"}
{"index":{"_id":"260"}}
{"account_number":260,"balance":2726,"firstname":"Kari","lastname":"Skinner","age":30,"gender":"F","address":"735 Losee Terrace","employer":"Singavera","email":"kariskinner@singavera.com","city":"Rushford","state":"WV"}
{"index":{"_id":"265"}}
{"account_number":265,"balance":46910,"firstname":"Marion","lastname":"Schneider","age":26,"gender":"F","address":"574 Everett Avenue","employer":"Evidends","email":"marionschneider@evidends.com","city":"Maplewood","state":"WY"}
{"index":{"_id":"272"}}
{"account_number":272,"balance":19253,"firstname":"Lilly","lastname":"Morgan","age":25,"gender":"F","address":"689 Fleet Street","employer":"Biolive","email":"lillymorgan@biolive.com","city":"Sunbury","state":"OH"}
{"index":{"_id":"277"}}
{"account_number":277,"balance":29564,"firstname":"Romero","lastname":"Lott","age":31,"gender":"M","address":"456 Danforth Street","employer":"Plasto","email":"romerolott@plasto.com","city":"Vincent","state":"VT"}
{"index":{"_id":"284"}}
{"account_number":284,"balance":22806,"firstname":"Randolph","lastname":"Banks","age":29,"gender":"M","address":"875 Hamilton Avenue","employer":"Caxt","email":"randolphbanks@caxt.com","city":"Crawfordsville","state":"WA"}
{"index":{"_id":"289"}}
{"account_number":289,"balance":7798,"firstname":"Blair","lastname":"Church","age":29,"gender":"M","address":"370 Sutton Street","employer":"Cubix","email":"blairchurch@cubix.com","city":"Nile","state":"NH"}
{"index":{"_id":"291"}}
{"account_number":291,"balance":19955,"firstname":"Lynn","lastname":"Pollard","age":40,"gender":"F","address":"685 Pierrepont Street","employer":"Slambda","email":"lynnpollard@slambda.com","city":"Mappsville","state":"ID"}
{"index":{"_id":"296"}}
{"account_number":296,"balance":24606,"firstname":"Rosa","lastname":"Oliver","age":34,"gender":"M","address":"168 Woodbine Street","employer":"Idetica","email":"rosaoliver@idetica.com","city":"Robinson","state":"WY"}
{"index":{"_id":"304"}}
{"account_number":304,"balance":28647,"firstname":"Palmer","lastname":"Clark","age":35,"gender":"M","address":"866 Boulevard Court","employer":"Maximind","email":"palmerclark@maximind.com","city":"Avalon","state":"NH"}
{"index":{"_id":"309"}}
{"account_number":309,"balance":3830,"firstname":"Rosemarie","lastname":"Nieves","age":30,"gender":"M","address":"206 Alice Court","employer":"Zounds","email":"rosemarienieves@zounds.com","city":"Ferney","state":"AR"}
{"index":{"_id":"311"}}
{"account_number":311,"balance":13388,"firstname":"Vinson","lastname":"Ballard","age":23,"gender":"F","address":"960 Glendale Court","employer":"Gynk","email":"vinsonballard@gynk.com","city":"Fairforest","state":"WY"}
{"index":{"_id":"316"}}
{"account_number":316,"balance":8214,"firstname":"Anita","lastname":"Ewing","age":32,"gender":"M","address":"396 Lombardy Street","employer":"Panzent","email":"anitaewing@panzent.com","city":"Neahkahnie","state":"WY"}
{"index":{"_id":"323"}}
{"account_number":323,"balance":42230,"firstname":"Chelsea","lastname":"Gamble","age":34,"gender":"F","address":"356 Dare Court","employer":"Isosphere","email":"chelseagamble@isosphere.com","city":"Dundee","state":"MD"}
{"index":{"_id":"328"}}
{"account_number":328,"balance":12523,"firstname":"Good","lastname":"Campbell","age":27,"gender":"F","address":"438 Hicks Street","employer":"Gracker","email":"goodcampbell@gracker.com","city":"Marion","state":"CA"}
{"index":{"_id":"330"}}
{"account_number":330,"balance":41620,"firstname":"Yvette","lastname":"Browning","age":34,"gender":"F","address":"431 Beekman Place","employer":"Marketoid","email":"yvettebrowning@marketoid.com","city":"Talpa","state":"CO"}
{"index":{"_id":"335"}}
{"account_number":335,"balance":35433,"firstname":"Vera","lastname":"Hansen","age":24,"gender":"M","address":"252 Bushwick Avenue","employer":"Zanilla","email":"verahansen@zanilla.com","city":"Manila","state":"TN"}
{"index":{"_id":"342"}}
{"account_number":342,"balance":33670,"firstname":"Vivian","lastname":"Wells","age":36,"gender":"M","address":"570 Cobek Court","employer":"Nutralab","email":"vivianwells@nutralab.com","city":"Fontanelle","state":"OK"}
{"index":{"_id":"347"}}
{"account_number":347,"balance":36038,"firstname":"Gould","lastname":"Carson","age":24,"gender":"F","address":"784 Pulaski Street","employer":"Mobildata","email":"gouldcarson@mobildata.com","city":"Goochland","state":"MI"}
{"index":{"_id":"354"}}
{"account_number":354,"balance":21294,"firstname":"Kidd","lastname":"Mclean","age":22,"gender":"M","address":"691 Saratoga Avenue","employer":"Ronbert","email":"kiddmclean@ronbert.com","city":"Tioga","state":"ME"}
{"index":{"_id":"359"}}
{"account_number":359,"balance":29927,"firstname":"Vanessa","lastname":"Harvey","age":28,"gender":"F","address":"679 Rutledge Street","employer":"Zentime","email":"vanessaharvey@zentime.com","city":"Williston","state":"IL"}
{"index":{"_id":"361"}}
{"account_number":361,"balance":23659,"firstname":"Noreen","lastname":"Shelton","age":36,"gender":"M","address":"702 Tillary Street","employer":"Medmex","email":"noreenshelton@medmex.com","city":"Derwood","state":"NH"}
{"index":{"_id":"366"}}
{"account_number":366,"balance":42368,"firstname":"Lydia","lastname":"Cooke","age":31,"gender":"M","address":"470 Coleman Street","employer":"Comstar","email":"lydiacooke@comstar.com","city":"Datil","state":"TN"}
{"index":{"_id":"373"}}
{"account_number":373,"balance":9671,"firstname":"Simpson","lastname":"Carpenter","age":21,"gender":"M","address":"837 Horace Court","employer":"Snips","email":"simpsoncarpenter@snips.com","city":"Tolu","state":"MA"}
{"index":{"_id":"378"}}
{"account_number":378,"balance":27100,"firstname":"Watson","lastname":"Simpson","age":36,"gender":"F","address":"644 Thomas Street","employer":"Wrapture","email":"watsonsimpson@wrapture.com","city":"Keller","state":"TX"}
{"index":{"_id":"380"}}
{"account_number":380,"balance":35628,"firstname":"Fernandez","lastname":"Reid","age":33,"gender":"F","address":"154 Melba Court","employer":"Cosmosis","email":"fernandezreid@cosmosis.com","city":"Boyd","state":"NE"}
{"index":{"_id":"385"}}
{"account_number":385,"balance":11022,"firstname":"Rosalinda","lastname":"Valencia","age":22,"gender":"M","address":"933 Lloyd Street","employer":"Zoarere","email":"rosalindavalencia@zoarere.com","city":"Waverly","state":"GA"}
{"index":{"_id":"392"}}
{"account_number":392,"balance":31613,"firstname":"Dotson","lastname":"Dean","age":35,"gender":"M","address":"136 Ford Street","employer":"Petigems","email":"dotsondean@petigems.com","city":"Chical","state":"SD"}
{"index":{"_id":"397"}}
{"account_number":397,"balance":37418,"firstname":"Leonard","lastname":"Gray","age":36,"gender":"F","address":"840 Morgan Avenue","employer":"Recritube","email":"leonardgray@recritube.com","city":"Edenburg","state":"AL"}
{"index":{"_id":"400"}}
{"account_number":400,"balance":20685,"firstname":"Kane","lastname":"King","age":21,"gender":"F","address":"405 Cornelia Street","employer":"Tri@Tribalog","email":"kaneking@tri@tribalog.com","city":"Gulf","state":"VT"}
{"index":{"_id":"405"}}
{"account_number":405,"balance":5679,"firstname":"Strickland","lastname":"Fuller","age":26,"gender":"M","address":"990 Concord Street","employer":"Digique","email":"stricklandfuller@digique.com","city":"Southmont","state":"NV"}
{"index":{"_id":"412"}}
{"account_number":412,"balance":27436,"firstname":"Ilene","lastname":"Abbott","age":26,"gender":"M","address":"846 Vine Street","employer":"Typhonica","email":"ileneabbott@typhonica.com","city":"Cedarville","state":"VT"}
{"index":{"_id":"417"}}
{"account_number":417,"balance":1788,"firstname":"Wheeler","lastname":"Ayers","age":35,"gender":"F","address":"677 Hope Street","employer":"Fortean","email":"wheelerayers@fortean.com","city":"Ironton","state":"PA"}
{"index":{"_id":"424"}}
{"account_number":424,"balance":36818,"firstname":"Tracie","lastname":"Gregory","age":34,"gender":"M","address":"112 Hunterfly Place","employer":"Comstruct","email":"traciegregory@comstruct.com","city":"Onton","state":"TN"}
{"index":{"_id":"429"}}
{"account_number":429,"balance":46970,"firstname":"Cantu","lastname":"Lindsey","age":31,"gender":"M","address":"404 Willoughby Avenue","employer":"Inquala","email":"cantulindsey@inquala.com","city":"Cowiche","state":"IA"}
{"index":{"_id":"431"}}
{"account_number":431,"balance":13136,"firstname":"Laurie","lastname":"Shaw","age":26,"gender":"F","address":"263 Aviation Road","employer":"Zillanet","email":"laurieshaw@zillanet.com","city":"Harmon","state":"WV"}
{"index":{"_id":"436"}}
{"account_number":436,"balance":27585,"firstname":"Alexander","lastname":"Sargent","age":23,"gender":"M","address":"363 Albemarle Road","employer":"Fangold","email":"alexandersargent@fangold.com","city":"Calpine","state":"OR"}
{"index":{"_id":"443"}}
{"account_number":443,"balance":7588,"firstname":"Huff","lastname":"Thomas","age":23,"gender":"M","address":"538 Erskine Loop","employer":"Accufarm","email":"huffthomas@accufarm.com","city":"Corinne","state":"AL"}
{"index":{"_id":"448"}}
{"account_number":448,"balance":22776,"firstname":"Adriana","lastname":"Mcfadden","age":35,"gender":"F","address":"984 Woodside Avenue","employer":"Telequiet","email":"adrianamcfadden@telequiet.com","city":"Darrtown","state":"WI"}
{"index":{"_id":"450"}}
{"account_number":450,"balance":2643,"firstname":"Bradford","lastname":"Nielsen","age":25,"gender":"M","address":"487 Keen Court","employer":"Exovent","email":"bradfordnielsen@exovent.com","city":"Hamilton","state":"DE"}
{"index":{"_id":"455"}}
{"account_number":455,"balance":39556,"firstname":"Lynn","lastname":"Tran","age":36,"gender":"M","address":"741 Richmond Street","employer":"Optyk","email":"lynntran@optyk.com","city":"Clinton","state":"WV"}
{"index":{"_id":"462"}}
{"account_number":462,"balance":10871,"firstname":"Calderon","lastname":"Day","age":27,"gender":"M","address":"810 Milford Street","employer":"Cofine","email":"calderonday@cofine.com","city":"Kula","state":"OK"}
{"index":{"_id":"467"}}
{"account_number":467,"balance":6312,"firstname":"Angelica","lastname":"May","age":32,"gender":"F","address":"384 Karweg Place","employer":"Keeg","email":"angelicamay@keeg.com","city":"Tetherow","state":"IA"}
{"index":{"_id":"474"}}
{"account_number":474,"balance":35896,"firstname":"Obrien","lastname":"Walton","age":40,"gender":"F","address":"192 Ide Court","employer":"Suremax","email":"obrienwalton@suremax.com","city":"Crucible","state":"UT"}
{"index":{"_id":"479"}}
{"account_number":479,"balance":31865,"firstname":"Cameron","lastname":"Ross","age":40,"gender":"M","address":"904 Bouck Court","employer":"Telpod","email":"cameronross@telpod.com","city":"Nord","state":"MO"}
{"index":{"_id":"481"}}
{"account_number":481,"balance":20024,"firstname":"Lina","lastname":"Stanley","age":33,"gender":"M","address":"361 Hanover Place","employer":"Strozen","email":"linastanley@strozen.com","city":"Wyoming","state":"NC"}
{"index":{"_id":"486"}}
{"account_number":486,"balance":35902,"firstname":"Dixie","lastname":"Fuentes","age":22,"gender":"F","address":"991 Applegate Court","employer":"Portico","email":"dixiefuentes@portico.com","city":"Salix","state":"VA"}
{"index":{"_id":"493"}}
{"account_number":493,"balance":5871,"firstname":"Campbell","lastname":"Best","age":24,"gender":"M","address":"297 Friel Place","employer":"Fanfare","email":"campbellbest@fanfare.com","city":"Kidder","state":"GA"}
{"index":{"_id":"498"}}
{"account_number":498,"balance":10516,"firstname":"Stella","lastname":"Hinton","age":39,"gender":"F","address":"649 Columbia Place","employer":"Flyboyz","email":"stellahinton@flyboyz.com","city":"Crenshaw","state":"SC"}
{"index":{"_id":"501"}}
{"account_number":501,"balance":16572,"firstname":"Kelley","lastname":"Ochoa","age":36,"gender":"M","address":"451 Clifton Place","employer":"Bluplanet","email":"kelleyochoa@bluplanet.com","city":"Gouglersville","state":"CT"}
{"index":{"_id":"506"}}
{"account_number":506,"balance":43440,"firstname":"Davidson","lastname":"Salas","age":28,"gender":"M","address":"731 Cleveland Street","employer":"Sequitur","email":"davidsonsalas@sequitur.com","city":"Lloyd","state":"ME"}
{"index":{"_id":"513"}}
{"account_number":513,"balance":30040,"firstname":"Maryellen","lastname":"Rose","age":37,"gender":"F","address":"428 Durland Place","employer":"Waterbaby","email":"maryellenrose@waterbaby.com","city":"Kiskimere","state":"RI"}
{"index":{"_id":"518"}}
{"account_number":518,"balance":48954,"firstname":"Finch","lastname":"Curtis","age":29,"gender":"F","address":"137 Ryder Street","employer":"Viagrand","email":"finchcurtis@viagrand.com","city":"Riverton","state":"MO"}
{"index":{"_id":"520"}}
{"account_number":520,"balance":27987,"firstname":"Brandy","lastname":"Calhoun","age":32,"gender":"M","address":"818 Harden Street","employer":"Maxemia","email":"brandycalhoun@maxemia.com","city":"Sidman","state":"OR"}
{"index":{"_id":"525"}}
{"account_number":525,"balance":23545,"firstname":"Holly","lastname":"Miles","age":25,"gender":"M","address":"746 Ludlam Place","employer":"Xurban","email":"hollymiles@xurban.com","city":"Harold","state":"AR"}
{"index":{"_id":"532"}}
{"account_number":532,"balance":17207,"firstname":"Hardin","lastname":"Kirk","age":26,"gender":"M","address":"268 Canarsie Road","employer":"Exposa","email":"hardinkirk@exposa.com","city":"Stouchsburg","state":"IL"}
{"index":{"_id":"537"}}
{"account_number":537,"balance":31069,"firstname":"Morin","lastname":"Frost","age":29,"gender":"M","address":"910 Lake Street","employer":"Primordia","email":"morinfrost@primordia.com","city":"Rivera","state":"DE"}
{"index":{"_id":"544"}}
{"account_number":544,"balance":41735,"firstname":"Short","lastname":"Dennis","age":21,"gender":"F","address":"908 Glen Street","employer":"Minga","email":"shortdennis@minga.com","city":"Dale","state":"KY"}
{"index":{"_id":"549"}}
{"account_number":549,"balance":1932,"firstname":"Jacqueline","lastname":"Maxwell","age":40,"gender":"M","address":"444 Schenck Place","employer":"Fuelworks","email":"jacquelinemaxwell@fuelworks.com","city":"Oretta","state":"OR"}
{"index":{"_id":"551"}}
{"account_number":551,"balance":21732,"firstname":"Milagros","lastname":"Travis","age":27,"gender":"F","address":"380 Murdock Court","employer":"Sloganaut","email":"milagrostravis@sloganaut.com","city":"Homeland","state":"AR"}
{"index":{"_id":"556"}}
{"account_number":556,"balance":36420,"firstname":"Collier","lastname":"Odonnell","age":35,"gender":"M","address":"591 Nolans Lane","employer":"Sultraxin","email":"collierodonnell@sultraxin.com","city":"Fulford","state":"MD"}
{"index":{"_id":"563"}}
{"account_number":563,"balance":43403,"firstname":"Morgan","lastname":"Torres","age":30,"gender":"F","address":"672 Belvidere Street","employer":"Quonata","email":"morgantorres@quonata.com","city":"Hollymead","state":"KY"}
{"index":{"_id":"568"}}
{"account_number":568,"balance":36628,"firstname":"Lesa","lastname":"Maynard","age":29,"gender":"F","address":"295 Whitty Lane","employer":"Coash","email":"lesamaynard@coash.com","city":"Broadlands","state":"VT"}
{"index":{"_id":"570"}}
{"account_number":570,"balance":26751,"firstname":"Church","lastname":"Mercado","age":24,"gender":"F","address":"892 Wyckoff Street","employer":"Xymonk","email":"churchmercado@xymonk.com","city":"Gloucester","state":"KY"}
{"index":{"_id":"575"}}
{"account_number":575,"balance":12588,"firstname":"Buchanan","lastname":"Pope","age":39,"gender":"M","address":"581 Sumner Place","employer":"Stucco","email":"buchananpope@stucco.com","city":"Ellerslie","state":"MD"}
{"index":{"_id":"582"}}
{"account_number":582,"balance":33371,"firstname":"Manning","lastname":"Guthrie","age":24,"gender":"F","address":"271 Jodie Court","employer":"Xerex","email":"manningguthrie@xerex.com","city":"Breinigsville","state":"NM"}
{"index":{"_id":"587"}}
{"account_number":587,"balance":3468,"firstname":"Carly","lastname":"Johns","age":33,"gender":"M","address":"390 Noll Street","employer":"Gallaxia","email":"carlyjohns@gallaxia.com","city":"Emison","state":"DC"}
{"index":{"_id":"594"}}
{"account_number":594,"balance":28194,"firstname":"Golden","lastname":"Donovan","age":26,"gender":"M","address":"199 Jewel Street","employer":"Organica","email":"goldendonovan@organica.com","city":"Macdona","state":"RI"}
{"index":{"_id":"599"}}
{"account_number":599,"balance":11944,"firstname":"Joanna","lastname":"Jennings","age":36,"gender":"F","address":"318 Irving Street","employer":"Extremo","email":"joannajennings@extremo.com","city":"Bartley","state":"MI"}
{"index":{"_id":"602"}}
{"account_number":602,"balance":38699,"firstname":"Mcgowan","lastname":"Mcclain","age":33,"gender":"M","address":"361 Stoddard Place","employer":"Oatfarm","email":"mcgowanmcclain@oatfarm.com","city":"Kapowsin","state":"MI"}
{"index":{"_id":"607"}}
{"account_number":607,"balance":38350,"firstname":"White","lastname":"Small","age":38,"gender":"F","address":"736 Judge Street","employer":"Immunics","email":"whitesmall@immunics.com","city":"Fairfield","state":"HI"}
{"index":{"_id":"614"}}
{"account_number":614,"balance":13157,"firstname":"Salazar","lastname":"Howard","age":35,"gender":"F","address":"847 Imlay Street","employer":"Retrack","email":"salazarhoward@retrack.com","city":"Grill","state":"FL"}
{"index":{"_id":"619"}}
{"account_number":619,"balance":48755,"firstname":"Grimes","lastname":"Reynolds","age":36,"gender":"M","address":"378 Denton Place","employer":"Frenex","email":"grimesreynolds@frenex.com","city":"Murillo","state":"LA"}
{"index":{"_id":"621"}}
{"account_number":621,"balance":35480,"firstname":"Leslie","lastname":"Sloan","age":26,"gender":"F","address":"336 Kansas Place","employer":"Dancity","email":"lesliesloan@dancity.com","city":"Corriganville","state":"AR"}
{"index":{"_id":"626"}}
{"account_number":626,"balance":19498,"firstname":"Ava","lastname":"Richardson","age":31,"gender":"F","address":"666 Nautilus Avenue","employer":"Cinaster","email":"avarichardson@cinaster.com","city":"Sutton","state":"AL"}
{"index":{"_id":"633"}}
{"account_number":633,"balance":35874,"firstname":"Conner","lastname":"Ramos","age":34,"gender":"M","address":"575 Agate Court","employer":"Insource","email":"connerramos@insource.com","city":"Madaket","state":"OK"}
{"index":{"_id":"638"}}
{"account_number":638,"balance":2658,"firstname":"Bridget","lastname":"Gallegos","age":31,"gender":"M","address":"383 Wogan Terrace","employer":"Songlines","email":"bridgetgallegos@songlines.com","city":"Linganore","state":"WA"}
{"index":{"_id":"640"}}
{"account_number":640,"balance":35596,"firstname":"Candace","lastname":"Hancock","age":25,"gender":"M","address":"574 Riverdale Avenue","employer":"Animalia","email":"candacehancock@animalia.com","city":"Blandburg","state":"KY"}
{"index":{"_id":"645"}}
{"account_number":645,"balance":29362,"firstname":"Edwina","lastname":"Hutchinson","age":26,"gender":"F","address":"892 Pacific Street","employer":"Essensia","email":"edwinahutchinson@essensia.com","city":"Dowling","state":"NE"}
{"index":{"_id":"652"}}
{"account_number":652,"balance":17363,"firstname":"Bonner","lastname":"Garner","age":26,"gender":"M","address":"219 Grafton Street","employer":"Utarian","email":"bonnergarner@utarian.com","city":"Vandiver","state":"PA"}
{"index":{"_id":"657"}}
{"account_number":657,"balance":40475,"firstname":"Kathleen","lastname":"Wilder","age":34,"gender":"F","address":"286 Sutter Avenue","employer":"Solgan","email":"kathleenwilder@solgan.com","city":"Graniteville","state":"MI"}
{"index":{"_id":"664"}}
{"account_number":664,"balance":16163,"firstname":"Hart","lastname":"Mccormick","age":40,"gender":"M","address":"144 Guider Avenue","employer":"Dyno","email":"hartmccormick@dyno.com","city":"Carbonville","state":"ID"}
{"index":{"_id":"669"}}
{"account_number":669,"balance":16934,"firstname":"Jewel","lastname":"Estrada","age":28,"gender":"M","address":"896 Meeker Avenue","employer":"Zilla","email":"jewelestrada@zilla.com","city":"Goodville","state":"PA"}
{"index":{"_id":"671"}}
{"account_number":671,"balance":29029,"firstname":"Antoinette","lastname":"Cook","age":34,"gender":"M","address":"375 Cumberland Street","employer":"Harmoney","email":"antoinettecook@harmoney.com","city":"Bergoo","state":"VT"}
{"index":{"_id":"676"}}
{"account_number":676,"balance":23842,"firstname":"Lisa","lastname":"Dudley","age":34,"gender":"M","address":"506 Vanderveer Street","employer":"Tropoli","email":"lisadudley@tropoli.com","city":"Konterra","state":"NY"}
{"index":{"_id":"683"}}
{"account_number":683,"balance":4381,"firstname":"Matilda","lastname":"Berger","age":39,"gender":"M","address":"884 Noble Street","employer":"Fibrodyne","email":"matildaberger@fibrodyne.com","city":"Shepardsville","state":"TN"}
{"index":{"_id":"688"}}
{"account_number":688,"balance":17931,"firstname":"Freeman","lastname":"Zamora","age":22,"gender":"F","address":"114 Herzl Street","employer":"Elemantra","email":"freemanzamora@elemantra.com","city":"Libertytown","state":"NM"}
{"index":{"_id":"690"}}
{"account_number":690,"balance":18127,"firstname":"Russo","lastname":"Swanson","age":35,"gender":"F","address":"256 Roebling Street","employer":"Zaj","email":"russoswanson@zaj.com","city":"Hoagland","state":"MI"}
{"index":{"_id":"695"}}
{"account_number":695,"balance":36800,"firstname":"Gonzales","lastname":"Mcfarland","age":26,"gender":"F","address":"647 Louisa Street","employer":"Songbird","email":"gonzalesmcfarland@songbird.com","city":"Crisman","state":"ID"}
{"index":{"_id":"703"}}
{"account_number":703,"balance":27443,"firstname":"Dona","lastname":"Burton","age":29,"gender":"M","address":"489 Flatlands Avenue","employer":"Cytrex","email":"donaburton@cytrex.com","city":"Reno","state":"VA"}
{"index":{"_id":"708"}}
{"account_number":708,"balance":34002,"firstname":"May","lastname":"Ortiz","age":28,"gender":"F","address":"244 Chauncey Street","employer":"Syntac","email":"mayortiz@syntac.com","city":"Munjor","state":"ID"}
{"index":{"_id":"710"}}
{"account_number":710,"balance":33650,"firstname":"Shelton","lastname":"Stark","age":37,"gender":"M","address":"404 Ovington Avenue","employer":"Kraggle","email":"sheltonstark@kraggle.com","city":"Ogema","state":"TN"}
{"index":{"_id":"715"}}
{"account_number":715,"balance":23734,"firstname":"Tammi","lastname":"Hodge","age":24,"gender":"M","address":"865 Church Lane","employer":"Netur","email":"tammihodge@netur.com","city":"Lacomb","state":"KS"}
{"index":{"_id":"722"}}
{"account_number":722,"balance":27256,"firstname":"Roberts","lastname":"Beasley","age":34,"gender":"F","address":"305 Kings Hwy","employer":"Quintity","email":"robertsbeasley@quintity.com","city":"Hayden","state":"PA"}
{"index":{"_id":"727"}}
{"account_number":727,"balance":27263,"firstname":"Natasha","lastname":"Knapp","age":36,"gender":"M","address":"723 Hubbard Street","employer":"Exostream","email":"natashaknapp@exostream.com","city":"Trexlertown","state":"LA"}
{"index":{"_id":"734"}}
{"account_number":734,"balance":20325,"firstname":"Keri","lastname":"Kinney","age":23,"gender":"M","address":"490 Balfour Place","employer":"Retrotex","email":"kerikinney@retrotex.com","city":"Salunga","state":"PA"}
{"index":{"_id":"739"}}
{"account_number":739,"balance":39063,"firstname":"Gwen","lastname":"Hardy","age":33,"gender":"F","address":"733 Stuart Street","employer":"Exozent","email":"gwenhardy@exozent.com","city":"Drytown","state":"NY"}
{"index":{"_id":"741"}}
{"account_number":741,"balance":33074,"firstname":"Nielsen","lastname":"Good","age":22,"gender":"M","address":"404 Norfolk Street","employer":"Kiggle","email":"nielsengood@kiggle.com","city":"Cumberland","state":"WA"}
{"index":{"_id":"746"}}
{"account_number":746,"balance":15970,"firstname":"Marguerite","lastname":"Wall","age":28,"gender":"F","address":"364 Crosby Avenue","employer":"Aquoavo","email":"margueritewall@aquoavo.com","city":"Jeff","state":"MI"}
{"index":{"_id":"753"}}
{"account_number":753,"balance":33340,"firstname":"Katina","lastname":"Alford","age":21,"gender":"F","address":"690 Ross Street","employer":"Intrawear","email":"katinaalford@intrawear.com","city":"Grimsley","state":"OK"}
{"index":{"_id":"758"}}
{"account_number":758,"balance":15739,"firstname":"Berta","lastname":"Short","age":28,"gender":"M","address":"149 Surf Avenue","employer":"Ozean","email":"bertashort@ozean.com","city":"Odessa","state":"UT"}
{"index":{"_id":"760"}}
{"account_number":760,"balance":40996,"firstname":"Rhea","lastname":"Blair","age":37,"gender":"F","address":"440 Hubbard Place","employer":"Bicol","email":"rheablair@bicol.com","city":"Stockwell","state":"LA"}
{"index":{"_id":"765"}}
{"account_number":765,"balance":31278,"firstname":"Knowles","lastname":"Cunningham","age":23,"gender":"M","address":"753 Macdougal Street","employer":"Thredz","email":"knowlescunningham@thredz.com","city":"Thomasville","state":"WA"}
{"index":{"_id":"772"}}
{"account_number":772,"balance":37849,"firstname":"Eloise","lastname":"Sparks","age":21,"gender":"M","address":"608 Willow Street","employer":"Satiance","email":"eloisesparks@satiance.com","city":"Richford","state":"NY"}
{"index":{"_id":"777"}}
{"account_number":777,"balance":48294,"firstname":"Adkins","lastname":"Mejia","age":32,"gender":"M","address":"186 Oxford Walk","employer":"Datagen","email":"adkinsmejia@datagen.com","city":"Faywood","state":"OK"}
{"index":{"_id":"784"}}
{"account_number":784,"balance":25291,"firstname":"Mabel","lastname":"Thornton","age":21,"gender":"M","address":"124 Louisiana Avenue","employer":"Zolavo","email":"mabelthornton@zolavo.com","city":"Lynn","state":"AL"}
{"index":{"_id":"789"}}
{"account_number":789,"balance":8760,"firstname":"Cunningham","lastname":"Kerr","age":27,"gender":"F","address":"154 Sharon Street","employer":"Polarium","email":"cunninghamkerr@polarium.com","city":"Tuskahoma","state":"MS"}
{"index":{"_id":"791"}}
{"account_number":791,"balance":48249,"firstname":"Janine","lastname":"Huber","age":38,"gender":"F","address":"348 Porter Avenue","employer":"Viocular","email":"janinehuber@viocular.com","city":"Fivepointville","state":"MA"}
{"index":{"_id":"796"}}
{"account_number":796,"balance":23503,"firstname":"Mona","lastname":"Craft","age":35,"gender":"F","address":"511 Henry Street","employer":"Opticom","email":"monacraft@opticom.com","city":"Websterville","state":"IN"}
{"index":{"_id":"804"}}
{"account_number":804,"balance":23610,"firstname":"Rojas","lastname":"Oneal","age":27,"gender":"M","address":"669 Sandford Street","employer":"Glukgluk","email":"rojasoneal@glukgluk.com","city":"Wheaton","state":"ME"}
{"index":{"_id":"809"}}
{"account_number":809,"balance":47812,"firstname":"Christie","lastname":"Strickland","age":30,"gender":"M","address":"346 Bancroft Place","employer":"Anarco","email":"christiestrickland@anarco.com","city":"Baden","state":"NV"}
{"index":{"_id":"811"}}
{"account_number":811,"balance":26007,"firstname":"Walls","lastname":"Rogers","age":28,"gender":"F","address":"352 Freeman Street","employer":"Geekmosis","email":"wallsrogers@geekmosis.com","city":"Caroleen","state":"NV"}
{"index":{"_id":"816"}}
{"account_number":816,"balance":9567,"firstname":"Cornelia","lastname":"Lane","age":20,"gender":"F","address":"384 Bainbridge Street","employer":"Sulfax","email":"cornelialane@sulfax.com","city":"Elizaville","state":"MS"}
{"index":{"_id":"823"}}
{"account_number":823,"balance":48726,"firstname":"Celia","lastname":"Bernard","age":33,"gender":"F","address":"466 Amboy Street","employer":"Mitroc","email":"celiabernard@mitroc.com","city":"Skyland","state":"GA"}
{"index":{"_id":"828"}}
{"account_number":828,"balance":44890,"firstname":"Blanche","lastname":"Holmes","age":33,"gender":"F","address":"605 Stryker Court","employer":"Motovate","email":"blancheholmes@motovate.com","city":"Loomis","state":"KS"}
{"index":{"_id":"830"}}
{"account_number":830,"balance":45210,"firstname":"Louella","lastname":"Chan","age":23,"gender":"M","address":"511 Heath Place","employer":"Conferia","email":"louellachan@conferia.com","city":"Brookfield","state":"OK"}
{"index":{"_id":"835"}}
{"account_number":835,"balance":46558,"firstname":"Glover","lastname":"Rutledge","age":25,"gender":"F","address":"641 Royce Street","employer":"Ginkogene","email":"gloverrutledge@ginkogene.com","city":"Dixonville","state":"VA"}
{"index":{"_id":"842"}}
{"account_number":842,"balance":49587,"firstname":"Meagan","lastname":"Buckner","age":23,"gender":"F","address":"833 Bushwick Court","employer":"Biospan","email":"meaganbuckner@biospan.com","city":"Craig","state":"TX"}
{"index":{"_id":"847"}}
{"account_number":847,"balance":8652,"firstname":"Antonia","lastname":"Duncan","age":23,"gender":"M","address":"644 Stryker Street","employer":"Talae","email":"antoniaduncan@talae.com","city":"Dawn","state":"MO"}
{"index":{"_id":"854"}}
{"account_number":854,"balance":49795,"firstname":"Jimenez","lastname":"Barry","age":25,"gender":"F","address":"603 Cooper Street","employer":"Verton","email":"jimenezbarry@verton.com","city":"Moscow","state":"AL"}
{"index":{"_id":"859"}}
{"account_number":859,"balance":20734,"firstname":"Beulah","lastname":"Stuart","age":24,"gender":"F","address":"651 Albemarle Terrace","employer":"Hatology","email":"beulahstuart@hatology.com","city":"Waiohinu","state":"RI"}
{"index":{"_id":"861"}}
{"account_number":861,"balance":44173,"firstname":"Jaime","lastname":"Wilson","age":35,"gender":"M","address":"680 Richardson Street","employer":"Temorak","email":"jaimewilson@temorak.com","city":"Fidelis","state":"FL"}
{"index":{"_id":"866"}}
{"account_number":866,"balance":45565,"firstname":"Araceli","lastname":"Woodward","age":28,"gender":"M","address":"326 Meadow Street","employer":"Olympix","email":"araceliwoodward@olympix.com","city":"Dana","state":"KS"}
{"index":{"_id":"873"}}
{"account_number":873,"balance":43931,"firstname":"Tisha","lastname":"Cotton","age":39,"gender":"F","address":"432 Lincoln Road","employer":"Buzzmaker","email":"tishacotton@buzzmaker.com","city":"Bluetown","state":"GA"}
{"index":{"_id":"878"}}
{"account_number":878,"balance":49159,"firstname":"Battle","lastname":"Blackburn","age":40,"gender":"F","address":"234 Hendrix Street","employer":"Zilphur","email":"battleblackburn@zilphur.com","city":"Wanamie","state":"PA"}
{"index":{"_id":"880"}}
{"account_number":880,"balance":22575,"firstname":"Christian","lastname":"Myers","age":35,"gender":"M","address":"737 Crown Street","employer":"Combogen","email":"christianmyers@combogen.com","city":"Abrams","state":"OK"}
{"index":{"_id":"885"}}
{"account_number":885,"balance":31661,"firstname":"Valdez","lastname":"Roberson","age":40,"gender":"F","address":"227 Scholes Street","employer":"Delphide","email":"valdezroberson@delphide.com","city":"Chilton","state":"MT"}
{"index":{"_id":"892"}}
{"account_number":892,"balance":44974,"firstname":"Hill","lastname":"Hayes","age":29,"gender":"M","address":"721 Dooley Street","employer":"Fuelton","email":"hillhayes@fuelton.com","city":"Orason","state":"MT"}
{"index":{"_id":"897"}}
{"account_number":897,"balance":45973,"firstname":"Alyson","lastname":"Irwin","age":25,"gender":"M","address":"731 Poplar Street","employer":"Quizka","email":"alysonirwin@quizka.com","city":"Singer","state":"VA"}
{"index":{"_id":"900"}}
{"account_number":900,"balance":6124,"firstname":"Gonzalez","lastname":"Watson","age":23,"gender":"M","address":"624 Sullivan Street","employer":"Marvane","email":"gonzalezwatson@marvane.com","city":"Wikieup","state":"IL"}
{"index":{"_id":"905"}}
{"account_number":905,"balance":29438,"firstname":"Schultz","lastname":"Moreno","age":20,"gender":"F","address":"761 Cedar Street","employer":"Paragonia","email":"schultzmoreno@paragonia.com","city":"Glenshaw","state":"SC"}
{"index":{"_id":"912"}}
{"account_number":912,"balance":13675,"firstname":"Flora","lastname":"Alvarado","age":26,"gender":"M","address":"771 Vandervoort Avenue","employer":"Boilicon","email":"floraalvarado@boilicon.com","city":"Vivian","state":"ID"}
{"index":{"_id":"917"}}
{"account_number":917,"balance":47782,"firstname":"Parks","lastname":"Hurst","age":24,"gender":"M","address":"933 Cozine Avenue","employer":"Pyramis","email":"parkshurst@pyramis.com","city":"Lindcove","state":"GA"}
{"index":{"_id":"924"}}
{"account_number":924,"balance":3811,"firstname":"Hilary","lastname":"Leonard","age":24,"gender":"M","address":"235 Hegeman Avenue","employer":"Metroz","email":"hilaryleonard@metroz.com","city":"Roosevelt","state":"ME"}
{"index":{"_id":"929"}}
{"account_number":929,"balance":34708,"firstname":"Willie","lastname":"Hickman","age":35,"gender":"M","address":"430 Devoe Street","employer":"Apextri","email":"williehickman@apextri.com","city":"Clay","state":"MS"}
{"index":{"_id":"931"}}
{"account_number":931,"balance":8244,"firstname":"Ingrid","lastname":"Garcia","age":23,"gender":"F","address":"674 Indiana Place","employer":"Balooba","email":"ingridgarcia@balooba.com","city":"Interlochen","state":"AZ"}
{"index":{"_id":"936"}}
{"account_number":936,"balance":22430,"firstname":"Beth","lastname":"Frye","age":36,"gender":"M","address":"462 Thatford Avenue","employer":"Puria","email":"bethfrye@puria.com","city":"Hiseville","state":"LA"}
{"index":{"_id":"943"}}
{"account_number":943,"balance":24187,"firstname":"Wagner","lastname":"Griffin","age":23,"gender":"M","address":"489 Ellery Street","employer":"Gazak","email":"wagnergriffin@gazak.com","city":"Lorraine","state":"HI"}
{"index":{"_id":"948"}}
{"account_number":948,"balance":37074,"firstname":"Sargent","lastname":"Powers","age":40,"gender":"M","address":"532 Fiske Place","employer":"Accuprint","email":"sargentpowers@accuprint.com","city":"Umapine","state":"AK"}
{"index":{"_id":"950"}}
{"account_number":950,"balance":30916,"firstname":"Sherrie","lastname":"Patel","age":32,"gender":"F","address":"658 Langham Street","employer":"Futurize","email":"sherriepatel@futurize.com","city":"Garfield","state":"OR"}
{"index":{"_id":"955"}}
{"account_number":955,"balance":41621,"firstname":"Klein","lastname":"Kemp","age":33,"gender":"M","address":"370 Vanderbilt Avenue","employer":"Synkgen","email":"kleinkemp@synkgen.com","city":"Bonanza","state":"FL"}
{"index":{"_id":"962"}}
{"account_number":962,"balance":32096,"firstname":"Trujillo","lastname":"Wilcox","age":21,"gender":"F","address":"914 Duffield Street","employer":"Extragene","email":"trujillowilcox@extragene.com","city":"Golconda","state":"MA"}
{"index":{"_id":"967"}}
{"account_number":967,"balance":19161,"firstname":"Carrie","lastname":"Huffman","age":36,"gender":"F","address":"240 Sands Street","employer":"Injoy","email":"carriehuffman@injoy.com","city":"Leroy","state":"CA"}
{"index":{"_id":"974"}}
{"account_number":974,"balance":38082,"firstname":"Deborah","lastname":"Yang","age":26,"gender":"F","address":"463 Goodwin Place","employer":"Entogrok","email":"deborahyang@entogrok.com","city":"Herald","state":"KY"}
{"index":{"_id":"979"}}
{"account_number":979,"balance":43130,"firstname":"Vaughn","lastname":"Pittman","age":29,"gender":"M","address":"446 Tompkins Place","employer":"Phormula","email":"vaughnpittman@phormula.com","city":"Fingerville","state":"WI"}
{"index":{"_id":"981"}}
{"account_number":981,"balance":20278,"firstname":"Nolan","lastname":"Warner","age":29,"gender":"F","address":"753 Channel Avenue","employer":"Interodeo","email":"nolanwarner@interodeo.com","city":"Layhill","state":"MT"}
{"index":{"_id":"986"}}
{"account_number":986,"balance":35086,"firstname":"Norris","lastname":"Hubbard","age":31,"gender":"M","address":"600 Celeste Court","employer":"Printspan","email":"norrishubbard@printspan.com","city":"Cassel","state":"MI"}
{"index":{"_id":"993"}}
{"account_number":993,"balance":26487,"firstname":"Campos","lastname":"Olsen","age":37,"gender":"M","address":"873 Covert Street","employer":"Isbol","email":"camposolsen@isbol.com","city":"Glendale","state":"AK"}
{"index":{"_id":"998"}}
{"account_number":998,"balance":16869,"firstname":"Letha","lastname":"Baker","age":40,"gender":"F","address":"206 Llama Court","employer":"Dognosis","email":"lethabaker@dognosis.com","city":"Dunlo","state":"WV"}
{"index":{"_id":"2"}}
{"account_number":2,"balance":28838,"firstname":"Roberta","lastname":"Bender","age":22,"gender":"F","address":"560 Kingsway Place","employer":"Chillium","email":"robertabender@chillium.com","city":"Bennett","state":"LA"}
{"index":{"_id":"7"}}
{"account_number":7,"balance":39121,"firstname":"Levy","lastname":"Richard","age":22,"gender":"M","address":"820 Logan Street","employer":"Teraprene","email":"levyrichard@teraprene.com","city":"Shrewsbury","state":"MO"}
{"index":{"_id":"14"}}
{"account_number":14,"balance":20480,"firstname":"Erma","lastname":"Kane","age":39,"gender":"F","address":"661 Vista Place","employer":"Stockpost","email":"ermakane@stockpost.com","city":"Chamizal","state":"NY"}
{"index":{"_id":"19"}}
{"account_number":19,"balance":27894,"firstname":"Schwartz","lastname":"Buchanan","age":28,"gender":"F","address":"449 Mersereau Court","employer":"Sybixtex","email":"schwartzbuchanan@sybixtex.com","city":"Greenwich","state":"KS"}
{"index":{"_id":"21"}}
{"account_number":21,"balance":7004,"firstname":"Estella","lastname":"Paul","age":38,"gender":"M","address":"859 Portal Street","employer":"Zillatide","email":"estellapaul@zillatide.com","city":"Churchill","state":"WV"}
{"index":{"_id":"26"}}
{"account_number":26,"balance":14127,"firstname":"Lorraine","lastname":"Mccullough","age":39,"gender":"F","address":"157 Dupont Street","employer":"Zosis","email":"lorrainemccullough@zosis.com","city":"Dennard","state":"NH"}
{"index":{"_id":"33"}}
{"account_number":33,"balance":35439,"firstname":"Savannah","lastname":"Kirby","age":30,"gender":"F","address":"372 Malta Street","employer":"Musanpoly","email":"savannahkirby@musanpoly.com","city":"Muse","state":"AK"}
{"index":{"_id":"38"}}
{"account_number":38,"balance":10511,"firstname":"Erna","lastname":"Fields","age":32,"gender":"M","address":"357 Maple Street","employer":"Eweville","email":"ernafields@eweville.com","city":"Twilight","state":"MS"}
{"index":{"_id":"40"}}
{"account_number":40,"balance":33882,"firstname":"Pace","lastname":"Molina","age":40,"gender":"M","address":"263 Ovington Court","employer":"Cytrak","email":"pacemolina@cytrak.com","city":"Silkworth","state":"OR"}
{"index":{"_id":"45"}}
{"account_number":45,"balance":44478,"firstname":"Geneva","lastname":"Morin","age":21,"gender":"F","address":"357 Herkimer Street","employer":"Ezent","email":"genevamorin@ezent.com","city":"Blanco","state":"AZ"}
{"index":{"_id":"52"}}
{"account_number":52,"balance":46425,"firstname":"Kayla","lastname":"Bradshaw","age":31,"gender":"M","address":"449 Barlow Drive","employer":"Magnemo","email":"kaylabradshaw@magnemo.com","city":"Wawona","state":"AZ"}
{"index":{"_id":"57"}}
{"account_number":57,"balance":8705,"firstname":"Powell","lastname":"Herring","age":21,"gender":"M","address":"263 Merit Court","employer":"Digiprint","email":"powellherring@digiprint.com","city":"Coral","state":"MT"}
{"index":{"_id":"64"}}
{"account_number":64,"balance":44036,"firstname":"Miles","lastname":"Battle","age":35,"gender":"F","address":"988 Homecrest Avenue","employer":"Koffee","email":"milesbattle@koffee.com","city":"Motley","state":"ID"}
{"index":{"_id":"69"}}
{"account_number":69,"balance":14253,"firstname":"Desiree","lastname":"Harrison","age":24,"gender":"M","address":"694 Garland Court","employer":"Barkarama","email":"desireeharrison@barkarama.com","city":"Hackneyville","state":"GA"}
{"index":{"_id":"71"}}
{"account_number":71,"balance":38201,"firstname":"Sharpe","lastname":"Hoffman","age":39,"gender":"F","address":"450 Conklin Avenue","employer":"Centree","email":"sharpehoffman@centree.com","city":"Urbana","state":"WY"}
{"index":{"_id":"76"}}
{"account_number":76,"balance":38345,"firstname":"Claudette","lastname":"Beard","age":24,"gender":"F","address":"748 Dorset Street","employer":"Repetwire","email":"claudettebeard@repetwire.com","city":"Caln","state":"TX"}
{"index":{"_id":"83"}}
{"account_number":83,"balance":35928,"firstname":"Mayo","lastname":"Cleveland","age":28,"gender":"M","address":"720 Brooklyn Road","employer":"Indexia","email":"mayocleveland@indexia.com","city":"Roberts","state":"ND"}
{"index":{"_id":"88"}}
{"account_number":88,"balance":26418,"firstname":"Adela","lastname":"Tyler","age":21,"gender":"F","address":"737 Clove Road","employer":"Surelogic","email":"adelatyler@surelogic.com","city":"Boling","state":"SD"}
{"index":{"_id":"90"}}
{"account_number":90,"balance":25332,"firstname":"Herman","lastname":"Snyder","age":22,"gender":"F","address":"737 College Place","employer":"Lunchpod","email":"hermansnyder@lunchpod.com","city":"Flintville","state":"IA"}
{"index":{"_id":"95"}}
{"account_number":95,"balance":1650,"firstname":"Dominguez","lastname":"Le","age":20,"gender":"M","address":"539 Grace Court","employer":"Portica","email":"dominguezle@portica.com","city":"Wollochet","state":"KS"}
{"index":{"_id":"103"}}
{"account_number":103,"balance":11253,"firstname":"Calhoun","lastname":"Bruce","age":33,"gender":"F","address":"731 Clarkson Avenue","employer":"Automon","email":"calhounbruce@automon.com","city":"Marienthal","state":"IL"}
{"index":{"_id":"108"}}
{"account_number":108,"balance":19015,"firstname":"Christensen","lastname":"Weaver","age":21,"gender":"M","address":"398 Dearborn Court","employer":"Quilk","email":"christensenweaver@quilk.com","city":"Belvoir","state":"TX"}
{"index":{"_id":"110"}}
{"account_number":110,"balance":4850,"firstname":"Daphne","lastname":"Byrd","age":23,"gender":"F","address":"239 Conover Street","employer":"Freakin","email":"daphnebyrd@freakin.com","city":"Taft","state":"MN"}
{"index":{"_id":"115"}}
{"account_number":115,"balance":18750,"firstname":"Nikki","lastname":"Doyle","age":31,"gender":"F","address":"537 Clara Street","employer":"Fossiel","email":"nikkidoyle@fossiel.com","city":"Caron","state":"MS"}
{"index":{"_id":"122"}}
{"account_number":122,"balance":17128,"firstname":"Aurora","lastname":"Fry","age":31,"gender":"F","address":"227 Knapp Street","employer":"Makingway","email":"aurorafry@makingway.com","city":"Maybell","state":"NE"}
{"index":{"_id":"127"}}
{"account_number":127,"balance":48734,"firstname":"Diann","lastname":"Mclaughlin","age":33,"gender":"F","address":"340 Clermont Avenue","employer":"Enomen","email":"diannmclaughlin@enomen.com","city":"Rutherford","state":"ND"}
{"index":{"_id":"134"}}
{"account_number":134,"balance":33829,"firstname":"Madelyn","lastname":"Norris","age":30,"gender":"F","address":"176 Noel Avenue","employer":"Endicil","email":"madelynnorris@endicil.com","city":"Walker","state":"NE"}
{"index":{"_id":"139"}}
{"account_number":139,"balance":18444,"firstname":"Rios","lastname":"Todd","age":35,"gender":"F","address":"281 Georgia Avenue","employer":"Uberlux","email":"riostodd@uberlux.com","city":"Hannasville","state":"PA"}
{"index":{"_id":"141"}}
{"account_number":141,"balance":20790,"firstname":"Liliana","lastname":"Caldwell","age":29,"gender":"M","address":"414 Huron Street","employer":"Rubadub","email":"lilianacaldwell@rubadub.com","city":"Hiwasse","state":"OK"}
{"index":{"_id":"146"}}
{"account_number":146,"balance":39078,"firstname":"Lang","lastname":"Kaufman","age":32,"gender":"F","address":"626 Beverley Road","employer":"Rodeomad","email":"langkaufman@rodeomad.com","city":"Mahtowa","state":"RI"}
{"index":{"_id":"153"}}
{"account_number":153,"balance":32074,"firstname":"Bird","lastname":"Cochran","age":31,"gender":"F","address":"691 Bokee Court","employer":"Supremia","email":"birdcochran@supremia.com","city":"Barrelville","state":"NE"}
{"index":{"_id":"158"}}
{"account_number":158,"balance":9380,"firstname":"Natalie","lastname":"Mcdowell","age":27,"gender":"M","address":"953 Roder Avenue","employer":"Myopium","email":"nataliemcdowell@myopium.com","city":"Savage","state":"ND"}
{"index":{"_id":"160"}}
{"account_number":160,"balance":48974,"firstname":"Hull","lastname":"Cherry","age":23,"gender":"F","address":"275 Beaumont Street","employer":"Noralex","email":"hullcherry@noralex.com","city":"Whipholt","state":"WA"}
{"index":{"_id":"165"}}
{"account_number":165,"balance":18956,"firstname":"Sims","lastname":"Mckay","age":40,"gender":"F","address":"205 Jackson Street","employer":"Comtour","email":"simsmckay@comtour.com","city":"Tilden","state":"DC"}
{"index":{"_id":"172"}}
{"account_number":172,"balance":18356,"firstname":"Marie","lastname":"Whitehead","age":20,"gender":"M","address":"704 Monaco Place","employer":"Sultrax","email":"mariewhitehead@sultrax.com","city":"Dragoon","state":"IL"}
{"index":{"_id":"177"}}
{"account_number":177,"balance":48972,"firstname":"Harris","lastname":"Gross","age":40,"gender":"F","address":"468 Suydam Street","employer":"Kidstock","email":"harrisgross@kidstock.com","city":"Yettem","state":"KY"}
{"index":{"_id":"184"}}
{"account_number":184,"balance":9157,"firstname":"Cathy","lastname":"Morrison","age":27,"gender":"M","address":"882 Pine Street","employer":"Zytrek","email":"cathymorrison@zytrek.com","city":"Fedora","state":"FL"}
{"index":{"_id":"189"}}
{"account_number":189,"balance":20167,"firstname":"Ada","lastname":"Cortez","age":38,"gender":"F","address":"700 Forest Place","employer":"Micronaut","email":"adacortez@micronaut.com","city":"Eagletown","state":"TX"}
{"index":{"_id":"191"}}
{"account_number":191,"balance":26172,"firstname":"Barr","lastname":"Sharpe","age":28,"gender":"M","address":"428 Auburn Place","employer":"Ziggles","email":"barrsharpe@ziggles.com","city":"Springdale","state":"KS"}
{"index":{"_id":"196"}}
{"account_number":196,"balance":29931,"firstname":"Caldwell","lastname":"Daniel","age":28,"gender":"F","address":"405 Oliver Street","employer":"Furnigeer","email":"caldwelldaniel@furnigeer.com","city":"Zortman","state":"NE"}
{"index":{"_id":"204"}}
{"account_number":204,"balance":27714,"firstname":"Mavis","lastname":"Deleon","age":39,"gender":"F","address":"400 Waldane Court","employer":"Lotron","email":"mavisdeleon@lotron.com","city":"Stollings","state":"LA"}
{"index":{"_id":"209"}}
{"account_number":209,"balance":31052,"firstname":"Myers","lastname":"Noel","age":30,"gender":"F","address":"691 Alton Place","employer":"Greeker","email":"myersnoel@greeker.com","city":"Hinsdale","state":"KY"}
{"index":{"_id":"211"}}
{"account_number":211,"balance":21539,"firstname":"Graciela","lastname":"Vaughan","age":22,"gender":"M","address":"558 Montauk Court","employer":"Fishland","email":"gracielavaughan@fishland.com","city":"Madrid","state":"PA"}
{"index":{"_id":"216"}}
{"account_number":216,"balance":11422,"firstname":"Price","lastname":"Haley","age":35,"gender":"M","address":"233 Portland Avenue","employer":"Zeam","email":"pricehaley@zeam.com","city":"Titanic","state":"UT"}
{"index":{"_id":"223"}}
{"account_number":223,"balance":9528,"firstname":"Newton","lastname":"Fletcher","age":26,"gender":"F","address":"654 Dewitt Avenue","employer":"Assistia","email":"newtonfletcher@assistia.com","city":"Nipinnawasee","state":"AK"}
{"index":{"_id":"228"}}
{"account_number":228,"balance":10543,"firstname":"Rosella","lastname":"Albert","age":20,"gender":"M","address":"185 Gotham Avenue","employer":"Isoplex","email":"rosellaalbert@isoplex.com","city":"Finzel","state":"NY"}
{"index":{"_id":"230"}}
{"account_number":230,"balance":10829,"firstname":"Chris","lastname":"Raymond","age":28,"gender":"F","address":"464 Remsen Street","employer":"Cogentry","email":"chrisraymond@cogentry.com","city":"Bowmansville","state":"SD"}
{"index":{"_id":"235"}}
{"account_number":235,"balance":17729,"firstname":"Mcpherson","lastname":"Mueller","age":31,"gender":"M","address":"541 Strong Place","employer":"Tingles","email":"mcphersonmueller@tingles.com","city":"Brantleyville","state":"AR"}
{"index":{"_id":"242"}}
{"account_number":242,"balance":42318,"firstname":"Berger","lastname":"Roach","age":21,"gender":"M","address":"125 Wakeman Place","employer":"Ovium","email":"bergerroach@ovium.com","city":"Hessville","state":"WI"}
{"index":{"_id":"247"}}
{"account_number":247,"balance":45123,"firstname":"Mccormick","lastname":"Moon","age":37,"gender":"M","address":"582 Brighton Avenue","employer":"Norsup","email":"mccormickmoon@norsup.com","city":"Forestburg","state":"DE"}
{"index":{"_id":"254"}}
{"account_number":254,"balance":35104,"firstname":"Yang","lastname":"Dodson","age":21,"gender":"M","address":"531 Lott Street","employer":"Mondicil","email":"yangdodson@mondicil.com","city":"Enoree","state":"UT"}
{"index":{"_id":"259"}}
{"account_number":259,"balance":41877,"firstname":"Eleanor","lastname":"Gonzalez","age":30,"gender":"M","address":"800 Sumpter Street","employer":"Futuris","email":"eleanorgonzalez@futuris.com","city":"Jenkinsville","state":"ID"}
{"index":{"_id":"261"}}
{"account_number":261,"balance":39998,"firstname":"Millicent","lastname":"Pickett","age":34,"gender":"F","address":"722 Montieth Street","employer":"Gushkool","email":"millicentpickett@gushkool.com","city":"Norwood","state":"MS"}
{"index":{"_id":"266"}}
{"account_number":266,"balance":2777,"firstname":"Monique","lastname":"Conner","age":35,"gender":"F","address":"489 Metrotech Courtr","employer":"Flotonic","email":"moniqueconner@flotonic.com","city":"Retsof","state":"MD"}
{"index":{"_id":"273"}}
{"account_number":273,"balance":11181,"firstname":"Murphy","lastname":"Chandler","age":20,"gender":"F","address":"569 Bradford Street","employer":"Zilch","email":"murphychandler@zilch.com","city":"Vicksburg","state":"FL"}
{"index":{"_id":"278"}}
{"account_number":278,"balance":22530,"firstname":"Tamra","lastname":"Navarro","age":27,"gender":"F","address":"175 Woodruff Avenue","employer":"Norsul","email":"tamranavarro@norsul.com","city":"Glasgow","state":"VT"}
{"index":{"_id":"280"}}
{"account_number":280,"balance":3380,"firstname":"Vilma","lastname":"Shields","age":26,"gender":"F","address":"133 Berriman Street","employer":"Applidec","email":"vilmashields@applidec.com","city":"Adamstown","state":"ME"}
{"index":{"_id":"285"}}
{"account_number":285,"balance":47369,"firstname":"Hilda","lastname":"Phillips","age":28,"gender":"F","address":"618 Nixon Court","employer":"Comcur","email":"hildaphillips@comcur.com","city":"Siglerville","state":"NC"}
{"index":{"_id":"292"}}
{"account_number":292,"balance":26679,"firstname":"Morrow","lastname":"Greene","age":20,"gender":"F","address":"691 Nassau Street","employer":"Columella","email":"morrowgreene@columella.com","city":"Sanborn","state":"FL"}
{"index":{"_id":"297"}}
{"account_number":297,"balance":20508,"firstname":"Tucker","lastname":"Patrick","age":35,"gender":"F","address":"978 Whitwell Place","employer":"Valreda","email":"tuckerpatrick@valreda.com","city":"Deseret","state":"CO"}
{"index":{"_id":"300"}}
{"account_number":300,"balance":25654,"firstname":"Lane","lastname":"Tate","age":26,"gender":"F","address":"632 Kay Court","employer":"Genesynk","email":"lanetate@genesynk.com","city":"Lowell","state":"MO"}
{"index":{"_id":"305"}}
{"account_number":305,"balance":11655,"firstname":"Augusta","lastname":"Winters","age":29,"gender":"F","address":"377 Paerdegat Avenue","employer":"Vendblend","email":"augustawinters@vendblend.com","city":"Gwynn","state":"MA"}
{"index":{"_id":"312"}}
{"account_number":312,"balance":8511,"firstname":"Burgess","lastname":"Gentry","age":25,"gender":"F","address":"382 Bergen Court","employer":"Orbixtar","email":"burgessgentry@orbixtar.com","city":"Conestoga","state":"WI"}
{"index":{"_id":"317"}}
{"account_number":317,"balance":31968,"firstname":"Ruiz","lastname":"Morris","age":31,"gender":"F","address":"972 Dean Street","employer":"Apex","email":"ruizmorris@apex.com","city":"Jacksonwald","state":"WV"}
{"index":{"_id":"324"}}
{"account_number":324,"balance":44976,"firstname":"Gladys","lastname":"Erickson","age":22,"gender":"M","address":"250 Battery Avenue","employer":"Eternis","email":"gladyserickson@eternis.com","city":"Marne","state":"IA"}
{"index":{"_id":"329"}}
{"account_number":329,"balance":31138,"firstname":"Nellie","lastname":"Mercer","age":25,"gender":"M","address":"967 Ebony Court","employer":"Scenty","email":"nelliemercer@scenty.com","city":"Jardine","state":"AK"}
{"index":{"_id":"331"}}
{"account_number":331,"balance":46004,"firstname":"Gibson","lastname":"Potts","age":34,"gender":"F","address":"994 Dahill Road","employer":"Zensus","email":"gibsonpotts@zensus.com","city":"Frizzleburg","state":"CO"}
{"index":{"_id":"336"}}
{"account_number":336,"balance":40891,"firstname":"Dudley","lastname":"Avery","age":25,"gender":"M","address":"405 Powers Street","employer":"Genmom","email":"dudleyavery@genmom.com","city":"Clarksburg","state":"CO"}
{"index":{"_id":"343"}}
{"account_number":343,"balance":37684,"firstname":"Robbie","lastname":"Logan","age":29,"gender":"M","address":"488 Linden Boulevard","employer":"Hydrocom","email":"robbielogan@hydrocom.com","city":"Stockdale","state":"TN"}
{"index":{"_id":"348"}}
{"account_number":348,"balance":1360,"firstname":"Karina","lastname":"Russell","age":37,"gender":"M","address":"797 Moffat Street","employer":"Limozen","email":"karinarussell@limozen.com","city":"Riegelwood","state":"RI"}
{"index":{"_id":"350"}}
{"account_number":350,"balance":4267,"firstname":"Wyatt","lastname":"Wise","age":22,"gender":"F","address":"896 Bleecker Street","employer":"Rockyard","email":"wyattwise@rockyard.com","city":"Joes","state":"MS"}
{"index":{"_id":"355"}}
{"account_number":355,"balance":40961,"firstname":"Gregory","lastname":"Delacruz","age":38,"gender":"M","address":"876 Cortelyou Road","employer":"Oulu","email":"gregorydelacruz@oulu.com","city":"Waterloo","state":"WV"}
{"index":{"_id":"362"}}
{"account_number":362,"balance":14938,"firstname":"Jimmie","lastname":"Dejesus","age":26,"gender":"M","address":"351 Navy Walk","employer":"Ecolight","email":"jimmiedejesus@ecolight.com","city":"Berlin","state":"ME"}
{"index":{"_id":"367"}}
{"account_number":367,"balance":40458,"firstname":"Elaine","lastname":"Workman","age":20,"gender":"M","address":"188 Ridge Boulevard","employer":"Colaire","email":"elaineworkman@colaire.com","city":"Herbster","state":"AK"}
{"index":{"_id":"374"}}
{"account_number":374,"balance":19521,"firstname":"Blanchard","lastname":"Stein","age":30,"gender":"M","address":"313 Bartlett Street","employer":"Cujo","email":"blanchardstein@cujo.com","city":"Cascades","state":"OR"}
{"index":{"_id":"379"}}
{"account_number":379,"balance":12962,"firstname":"Ruthie","lastname":"Lamb","age":21,"gender":"M","address":"796 Rockaway Avenue","employer":"Incubus","email":"ruthielamb@incubus.com","city":"Hickory","state":"TX"}
{"index":{"_id":"381"}}
{"account_number":381,"balance":40978,"firstname":"Sophie","lastname":"Mays","age":31,"gender":"M","address":"261 Varanda Place","employer":"Uneeq","email":"sophiemays@uneeq.com","city":"Cressey","state":"AR"}
{"index":{"_id":"386"}}
{"account_number":386,"balance":42588,"firstname":"Wallace","lastname":"Barr","age":39,"gender":"F","address":"246 Beverly Road","employer":"Concility","email":"wallacebarr@concility.com","city":"Durham","state":"IN"}
{"index":{"_id":"393"}}
{"account_number":393,"balance":43936,"firstname":"William","lastname":"Kelly","age":24,"gender":"M","address":"178 Lawrence Avenue","employer":"Techtrix","email":"williamkelly@techtrix.com","city":"Orin","state":"PA"}
{"index":{"_id":"398"}}
{"account_number":398,"balance":8543,"firstname":"Leticia","lastname":"Duran","age":35,"gender":"F","address":"305 Senator Street","employer":"Xleen","email":"leticiaduran@xleen.com","city":"Cavalero","state":"PA"}
{"index":{"_id":"401"}}
{"account_number":401,"balance":29408,"firstname":"Contreras","lastname":"Randolph","age":38,"gender":"M","address":"104 Lewis Avenue","employer":"Inrt","email":"contrerasrandolph@inrt.com","city":"Chesapeake","state":"CT"}
{"index":{"_id":"406"}}
{"account_number":406,"balance":28127,"firstname":"Mccarthy","lastname":"Dunlap","age":28,"gender":"F","address":"684 Seacoast Terrace","employer":"Canopoly","email":"mccarthydunlap@canopoly.com","city":"Elliott","state":"NC"}
{"index":{"_id":"413"}}
{"account_number":413,"balance":15631,"firstname":"Pugh","lastname":"Hamilton","age":39,"gender":"F","address":"124 Euclid Avenue","employer":"Techade","email":"pughhamilton@techade.com","city":"Beaulieu","state":"CA"}
{"index":{"_id":"418"}}
{"account_number":418,"balance":10207,"firstname":"Reed","lastname":"Goff","age":32,"gender":"M","address":"959 Everit Street","employer":"Zillan","email":"reedgoff@zillan.com","city":"Hiko","state":"WV"}
{"index":{"_id":"420"}}
{"account_number":420,"balance":44699,"firstname":"Brandie","lastname":"Hayden","age":22,"gender":"M","address":"291 Ash Street","employer":"Digifad","email":"brandiehayden@digifad.com","city":"Spelter","state":"NM"}
{"index":{"_id":"425"}}
{"account_number":425,"balance":41308,"firstname":"Queen","lastname":"Leach","age":30,"gender":"M","address":"105 Fair Street","employer":"Magneato","email":"queenleach@magneato.com","city":"Barronett","state":"NH"}
{"index":{"_id":"432"}}
{"account_number":432,"balance":28969,"firstname":"Preston","lastname":"Ferguson","age":40,"gender":"F","address":"239 Greenwood Avenue","employer":"Bitendrex","email":"prestonferguson@bitendrex.com","city":"Idledale","state":"ND"}
{"index":{"_id":"437"}}
{"account_number":437,"balance":41225,"firstname":"Rosales","lastname":"Marquez","age":29,"gender":"M","address":"873 Ryerson Street","employer":"Ronelon","email":"rosalesmarquez@ronelon.com","city":"Allendale","state":"CA"}
{"index":{"_id":"444"}}
{"account_number":444,"balance":44219,"firstname":"Dolly","lastname":"Finch","age":24,"gender":"F","address":"974 Interborough Parkway","employer":"Zytrac","email":"dollyfinch@zytrac.com","city":"Vowinckel","state":"WY"}
{"index":{"_id":"449"}}
{"account_number":449,"balance":41950,"firstname":"Barnett","lastname":"Cantrell","age":39,"gender":"F","address":"945 Bedell Lane","employer":"Zentility","email":"barnettcantrell@zentility.com","city":"Swartzville","state":"ND"}
{"index":{"_id":"451"}}
{"account_number":451,"balance":31950,"firstname":"Mason","lastname":"Mcleod","age":31,"gender":"F","address":"438 Havemeyer Street","employer":"Omatom","email":"masonmcleod@omatom.com","city":"Ryderwood","state":"NE"}
{"index":{"_id":"456"}}
{"account_number":456,"balance":21419,"firstname":"Solis","lastname":"Kline","age":33,"gender":"M","address":"818 Ashford Street","employer":"Vetron","email":"soliskline@vetron.com","city":"Ruffin","state":"NY"}
{"index":{"_id":"463"}}
{"account_number":463,"balance":36672,"firstname":"Heidi","lastname":"Acosta","age":20,"gender":"F","address":"692 Kenmore Terrace","employer":"Elpro","email":"heidiacosta@elpro.com","city":"Ezel","state":"SD"}
{"index":{"_id":"468"}}
{"account_number":468,"balance":18400,"firstname":"Foreman","lastname":"Fowler","age":40,"gender":"M","address":"443 Jackson Court","employer":"Zillactic","email":"foremanfowler@zillactic.com","city":"Wakarusa","state":"WA"}
{"index":{"_id":"470"}}
{"account_number":470,"balance":20455,"firstname":"Schneider","lastname":"Hull","age":35,"gender":"M","address":"724 Apollo Street","employer":"Exospeed","email":"schneiderhull@exospeed.com","city":"Watchtower","state":"ID"}
{"index":{"_id":"475"}}
{"account_number":475,"balance":24427,"firstname":"Morales","lastname":"Jacobs","age":22,"gender":"F","address":"225 Desmond Court","employer":"Oronoko","email":"moralesjacobs@oronoko.com","city":"Clayville","state":"CT"}
{"index":{"_id":"482"}}
{"account_number":482,"balance":14834,"firstname":"Janie","lastname":"Bass","age":39,"gender":"M","address":"781 Grattan Street","employer":"Manglo","email":"janiebass@manglo.com","city":"Kenwood","state":"IA"}
{"index":{"_id":"487"}}
{"account_number":487,"balance":30718,"firstname":"Sawyer","lastname":"Vincent","age":26,"gender":"F","address":"238 Lancaster Avenue","employer":"Brainquil","email":"sawyervincent@brainquil.com","city":"Galesville","state":"MS"}
{"index":{"_id":"494"}}
{"account_number":494,"balance":3592,"firstname":"Holden","lastname":"Bowen","age":30,"gender":"M","address":"374 Elmwood Avenue","employer":"Endipine","email":"holdenbowen@endipine.com","city":"Rosine","state":"ID"}
{"index":{"_id":"499"}}
{"account_number":499,"balance":26060,"firstname":"Lara","lastname":"Perkins","age":26,"gender":"M","address":"703 Monroe Street","employer":"Paprikut","email":"laraperkins@paprikut.com","city":"Barstow","state":"NY"}
{"index":{"_id":"502"}}
{"account_number":502,"balance":31898,"firstname":"Woodard","lastname":"Bailey","age":31,"gender":"F","address":"585 Albee Square","employer":"Imperium","email":"woodardbailey@imperium.com","city":"Matheny","state":"MT"}
{"index":{"_id":"507"}}
{"account_number":507,"balance":27675,"firstname":"Blankenship","lastname":"Ramirez","age":31,"gender":"M","address":"630 Graham Avenue","employer":"Bytrex","email":"blankenshipramirez@bytrex.com","city":"Bancroft","state":"CT"}
{"index":{"_id":"514"}}
{"account_number":514,"balance":30125,"firstname":"Solomon","lastname":"Bush","age":34,"gender":"M","address":"409 Harkness Avenue","employer":"Snacktion","email":"solomonbush@snacktion.com","city":"Grayhawk","state":"TX"}
{"index":{"_id":"519"}}
{"account_number":519,"balance":3282,"firstname":"Lorna","lastname":"Franco","age":31,"gender":"F","address":"722 Schenck Court","employer":"Zentia","email":"lornafranco@zentia.com","city":"National","state":"FL"}
{"index":{"_id":"521"}}
{"account_number":521,"balance":16348,"firstname":"Josefa","lastname":"Buckley","age":34,"gender":"F","address":"848 Taylor Street","employer":"Mazuda","email":"josefabuckley@mazuda.com","city":"Saranap","state":"NM"}
{"index":{"_id":"526"}}
{"account_number":526,"balance":35375,"firstname":"Sweeney","lastname":"Fulton","age":33,"gender":"F","address":"550 Martense Street","employer":"Cormoran","email":"sweeneyfulton@cormoran.com","city":"Chalfant","state":"IA"}
{"index":{"_id":"533"}}
{"account_number":533,"balance":13761,"firstname":"Margarita","lastname":"Diaz","age":23,"gender":"M","address":"295 Tapscott Street","employer":"Zilodyne","email":"margaritadiaz@zilodyne.com","city":"Hondah","state":"ID"}
{"index":{"_id":"538"}}
{"account_number":538,"balance":16416,"firstname":"Koch","lastname":"Barker","age":21,"gender":"M","address":"919 Gerry Street","employer":"Xplor","email":"kochbarker@xplor.com","city":"Dixie","state":"WY"}
{"index":{"_id":"540"}}
{"account_number":540,"balance":40235,"firstname":"Tammy","lastname":"Wiggins","age":32,"gender":"F","address":"186 Schenectady Avenue","employer":"Speedbolt","email":"tammywiggins@speedbolt.com","city":"Salvo","state":"LA"}
{"index":{"_id":"545"}}
{"account_number":545,"balance":27011,"firstname":"Lena","lastname":"Lucas","age":20,"gender":"M","address":"110 Lamont Court","employer":"Kindaloo","email":"lenalucas@kindaloo.com","city":"Harleigh","state":"KY"}
{"index":{"_id":"552"}}
{"account_number":552,"balance":14727,"firstname":"Kate","lastname":"Estes","age":39,"gender":"M","address":"785 Willmohr Street","employer":"Rodeocean","email":"kateestes@rodeocean.com","city":"Elfrida","state":"HI"}
{"index":{"_id":"557"}}
{"account_number":557,"balance":3119,"firstname":"Landry","lastname":"Buck","age":20,"gender":"M","address":"558 Schweikerts Walk","employer":"Protodyne","email":"landrybuck@protodyne.com","city":"Edneyville","state":"AL"}
{"index":{"_id":"564"}}
{"account_number":564,"balance":43631,"firstname":"Owens","lastname":"Bowers","age":22,"gender":"M","address":"842 Congress Street","employer":"Nspire","email":"owensbowers@nspire.com","city":"Machias","state":"VA"}
{"index":{"_id":"569"}}
{"account_number":569,"balance":40019,"firstname":"Sherri","lastname":"Rowe","age":39,"gender":"F","address":"591 Arlington Place","employer":"Netility","email":"sherrirowe@netility.com","city":"Bridgetown","state":"SC"}
{"index":{"_id":"571"}}
{"account_number":571,"balance":3014,"firstname":"Ayers","lastname":"Duffy","age":28,"gender":"F","address":"721 Wortman Avenue","employer":"Aquasseur","email":"ayersduffy@aquasseur.com","city":"Tilleda","state":"MS"}
{"index":{"_id":"576"}}
{"account_number":576,"balance":29682,"firstname":"Helena","lastname":"Robertson","age":33,"gender":"F","address":"774 Devon Avenue","employer":"Vicon","email":"helenarobertson@vicon.com","city":"Dyckesville","state":"NV"}
{"index":{"_id":"583"}}
{"account_number":583,"balance":26558,"firstname":"Castro","lastname":"West","age":34,"gender":"F","address":"814 Williams Avenue","employer":"Cipromox","email":"castrowest@cipromox.com","city":"Nescatunga","state":"IL"}
{"index":{"_id":"588"}}
{"account_number":588,"balance":43531,"firstname":"Martina","lastname":"Collins","age":31,"gender":"M","address":"301 Anna Court","employer":"Geekwagon","email":"martinacollins@geekwagon.com","city":"Oneida","state":"VA"}
{"index":{"_id":"590"}}
{"account_number":590,"balance":4652,"firstname":"Ladonna","lastname":"Tucker","age":31,"gender":"F","address":"162 Kane Place","employer":"Infotrips","email":"ladonnatucker@infotrips.com","city":"Utting","state":"IA"}
{"index":{"_id":"595"}}
{"account_number":595,"balance":12478,"firstname":"Mccall","lastname":"Britt","age":36,"gender":"F","address":"823 Hill Street","employer":"Cablam","email":"mccallbritt@cablam.com","city":"Vernon","state":"CA"}
{"index":{"_id":"603"}}
{"account_number":603,"balance":28145,"firstname":"Janette","lastname":"Guzman","age":31,"gender":"F","address":"976 Kingston Avenue","employer":"Splinx","email":"janetteguzman@splinx.com","city":"Boomer","state":"NC"}
{"index":{"_id":"608"}}
{"account_number":608,"balance":47091,"firstname":"Carey","lastname":"Whitley","age":32,"gender":"F","address":"976 Lawrence Street","employer":"Poshome","email":"careywhitley@poshome.com","city":"Weogufka","state":"NE"}
{"index":{"_id":"610"}}
{"account_number":610,"balance":40571,"firstname":"Foster","lastname":"Weber","age":24,"gender":"F","address":"323 Rochester Avenue","employer":"Firewax","email":"fosterweber@firewax.com","city":"Winston","state":"NY"}
{"index":{"_id":"615"}}
{"account_number":615,"balance":28726,"firstname":"Delgado","lastname":"Curry","age":28,"gender":"F","address":"706 Butler Street","employer":"Zoxy","email":"delgadocurry@zoxy.com","city":"Gracey","state":"SD"}
{"index":{"_id":"622"}}
{"account_number":622,"balance":9661,"firstname":"Paulette","lastname":"Hartman","age":38,"gender":"M","address":"375 Emerald Street","employer":"Locazone","email":"paulettehartman@locazone.com","city":"Canterwood","state":"OH"}
{"index":{"_id":"627"}}
{"account_number":627,"balance":47546,"firstname":"Crawford","lastname":"Sears","age":37,"gender":"F","address":"686 Eastern Parkway","employer":"Updat","email":"crawfordsears@updat.com","city":"Bison","state":"VT"}
{"index":{"_id":"634"}}
{"account_number":634,"balance":29805,"firstname":"Deloris","lastname":"Levy","age":38,"gender":"M","address":"838 Foster Avenue","employer":"Homelux","email":"delorislevy@homelux.com","city":"Kempton","state":"PA"}
{"index":{"_id":"639"}}
{"account_number":639,"balance":28875,"firstname":"Caitlin","lastname":"Clements","age":32,"gender":"F","address":"627 Aster Court","employer":"Bunga","email":"caitlinclements@bunga.com","city":"Cetronia","state":"SC"}
{"index":{"_id":"641"}}
{"account_number":641,"balance":18345,"firstname":"Sheppard","lastname":"Everett","age":39,"gender":"F","address":"791 Norwood Avenue","employer":"Roboid","email":"sheppardeverett@roboid.com","city":"Selma","state":"AK"}
{"index":{"_id":"646"}}
{"account_number":646,"balance":15559,"firstname":"Lavonne","lastname":"Reyes","age":31,"gender":"F","address":"983 Newport Street","employer":"Parcoe","email":"lavonnereyes@parcoe.com","city":"Monument","state":"LA"}
{"index":{"_id":"653"}}
{"account_number":653,"balance":7606,"firstname":"Marcia","lastname":"Bennett","age":33,"gender":"F","address":"455 Bragg Street","employer":"Opticall","email":"marciabennett@opticall.com","city":"Magnolia","state":"NC"}
{"index":{"_id":"658"}}
{"account_number":658,"balance":10210,"firstname":"Bass","lastname":"Mcconnell","age":32,"gender":"F","address":"274 Ocean Avenue","employer":"Combot","email":"bassmcconnell@combot.com","city":"Beyerville","state":"OH"}
{"index":{"_id":"660"}}
{"account_number":660,"balance":46427,"firstname":"Moon","lastname":"Wood","age":33,"gender":"F","address":"916 Amersfort Place","employer":"Olucore","email":"moonwood@olucore.com","city":"Como","state":"VA"}
{"index":{"_id":"665"}}
{"account_number":665,"balance":15215,"firstname":"Britney","lastname":"Young","age":36,"gender":"M","address":"766 Sackman Street","employer":"Geoforma","email":"britneyyoung@geoforma.com","city":"Tuttle","state":"WI"}
{"index":{"_id":"672"}}
{"account_number":672,"balance":12621,"firstname":"Camille","lastname":"Munoz","age":36,"gender":"F","address":"959 Lewis Place","employer":"Vantage","email":"camillemunoz@vantage.com","city":"Whitmer","state":"IN"}
{"index":{"_id":"677"}}
{"account_number":677,"balance":8491,"firstname":"Snider","lastname":"Benton","age":26,"gender":"M","address":"827 Evans Street","employer":"Medicroix","email":"sniderbenton@medicroix.com","city":"Kaka","state":"UT"}
{"index":{"_id":"684"}}
{"account_number":684,"balance":46091,"firstname":"Warren","lastname":"Snow","age":25,"gender":"M","address":"756 Oakland Place","employer":"Bizmatic","email":"warrensnow@bizmatic.com","city":"Hatteras","state":"NE"}
{"index":{"_id":"689"}}
{"account_number":689,"balance":14985,"firstname":"Ines","lastname":"Chaney","age":28,"gender":"M","address":"137 Dikeman Street","employer":"Zidant","email":"ineschaney@zidant.com","city":"Nettie","state":"DC"}
{"index":{"_id":"691"}}
{"account_number":691,"balance":10792,"firstname":"Mclean","lastname":"Colon","age":22,"gender":"M","address":"876 Classon Avenue","employer":"Elentrix","email":"mcleancolon@elentrix.com","city":"Unionville","state":"OK"}
{"index":{"_id":"696"}}
{"account_number":696,"balance":17568,"firstname":"Crane","lastname":"Matthews","age":32,"gender":"F","address":"721 Gerritsen Avenue","employer":"Intradisk","email":"cranematthews@intradisk.com","city":"Brewster","state":"WV"}
{"index":{"_id":"704"}}
{"account_number":704,"balance":45347,"firstname":"Peters","lastname":"Kent","age":22,"gender":"F","address":"871 Independence Avenue","employer":"Extragen","email":"peterskent@extragen.com","city":"Morriston","state":"CA"}
{"index":{"_id":"709"}}
{"account_number":709,"balance":11015,"firstname":"Abbott","lastname":"Odom","age":29,"gender":"M","address":"893 Union Street","employer":"Jimbies","email":"abbottodom@jimbies.com","city":"Leeper","state":"NJ"}
{"index":{"_id":"711"}}
{"account_number":711,"balance":26939,"firstname":"Villarreal","lastname":"Horton","age":35,"gender":"F","address":"861 Creamer Street","employer":"Lexicondo","email":"villarrealhorton@lexicondo.com","city":"Lydia","state":"MS"}
{"index":{"_id":"716"}}
{"account_number":716,"balance":19789,"firstname":"Paul","lastname":"Mason","age":34,"gender":"F","address":"618 Nichols Avenue","employer":"Slax","email":"paulmason@slax.com","city":"Snowville","state":"OK"}
{"index":{"_id":"723"}}
{"account_number":723,"balance":16421,"firstname":"Nixon","lastname":"Moran","age":27,"gender":"M","address":"569 Campus Place","employer":"Cuizine","email":"nixonmoran@cuizine.com","city":"Buxton","state":"DC"}
{"index":{"_id":"728"}}
{"account_number":728,"balance":44818,"firstname":"Conley","lastname":"Preston","age":28,"gender":"M","address":"450 Coventry Road","employer":"Obones","email":"conleypreston@obones.com","city":"Alden","state":"CO"}
{"index":{"_id":"730"}}
{"account_number":730,"balance":41299,"firstname":"Moore","lastname":"Lee","age":30,"gender":"M","address":"797 Turner Place","employer":"Orbean","email":"moorelee@orbean.com","city":"Highland","state":"DE"}
{"index":{"_id":"735"}}
{"account_number":735,"balance":3984,"firstname":"Loraine","lastname":"Willis","age":32,"gender":"F","address":"928 Grove Street","employer":"Gadtron","email":"lorainewillis@gadtron.com","city":"Lowgap","state":"NY"}
{"index":{"_id":"742"}}
{"account_number":742,"balance":24765,"firstname":"Merle","lastname":"Wooten","age":26,"gender":"M","address":"317 Pooles Lane","employer":"Tropolis","email":"merlewooten@tropolis.com","city":"Bentley","state":"ND"}
{"index":{"_id":"747"}}
{"account_number":747,"balance":16617,"firstname":"Diaz","lastname":"Austin","age":38,"gender":"M","address":"676 Harway Avenue","employer":"Irack","email":"diazaustin@irack.com","city":"Cliff","state":"HI"}
{"index":{"_id":"754"}}
{"account_number":754,"balance":10779,"firstname":"Jones","lastname":"Vega","age":25,"gender":"F","address":"795 India Street","employer":"Gluid","email":"jonesvega@gluid.com","city":"Tyhee","state":"FL"}
{"index":{"_id":"759"}}
{"account_number":759,"balance":38007,"firstname":"Rose","lastname":"Carlson","age":27,"gender":"M","address":"987 Navy Street","employer":"Aquasure","email":"rosecarlson@aquasure.com","city":"Carlton","state":"CT"}
{"index":{"_id":"761"}}
{"account_number":761,"balance":7663,"firstname":"Rae","lastname":"Juarez","age":34,"gender":"F","address":"560 Gilmore Court","employer":"Entropix","email":"raejuarez@entropix.com","city":"Northchase","state":"ID"}
{"index":{"_id":"766"}}
{"account_number":766,"balance":21957,"firstname":"Thomas","lastname":"Gillespie","age":38,"gender":"M","address":"993 Williams Place","employer":"Octocore","email":"thomasgillespie@octocore.com","city":"Defiance","state":"MS"}
{"index":{"_id":"773"}}
{"account_number":773,"balance":31126,"firstname":"Liza","lastname":"Coffey","age":36,"gender":"F","address":"540 Bulwer Place","employer":"Assurity","email":"lizacoffey@assurity.com","city":"Gilgo","state":"WV"}
{"index":{"_id":"778"}}
{"account_number":778,"balance":46007,"firstname":"Underwood","lastname":"Wheeler","age":28,"gender":"M","address":"477 Provost Street","employer":"Decratex","email":"underwoodwheeler@decratex.com","city":"Sardis","state":"ID"}
{"index":{"_id":"780"}}
{"account_number":780,"balance":4682,"firstname":"Maryanne","lastname":"Hendricks","age":26,"gender":"F","address":"709 Wolcott Street","employer":"Sarasonic","email":"maryannehendricks@sarasonic.com","city":"Santel","state":"NH"}
{"index":{"_id":"785"}}
{"account_number":785,"balance":25078,"firstname":"Fields","lastname":"Lester","age":29,"gender":"M","address":"808 Chestnut Avenue","employer":"Visualix","email":"fieldslester@visualix.com","city":"Rowe","state":"PA"}
{"index":{"_id":"792"}}
{"account_number":792,"balance":13109,"firstname":"Becky","lastname":"Jimenez","age":40,"gender":"F","address":"539 Front Street","employer":"Isologia","email":"beckyjimenez@isologia.com","city":"Summertown","state":"MI"}
{"index":{"_id":"797"}}
{"account_number":797,"balance":6854,"firstname":"Lindsay","lastname":"Mills","age":26,"gender":"F","address":"919 Quay Street","employer":"Zoinage","email":"lindsaymills@zoinage.com","city":"Elliston","state":"VA"}
{"index":{"_id":"800"}}
{"account_number":800,"balance":26217,"firstname":"Candy","lastname":"Oconnor","age":28,"gender":"M","address":"200 Newel Street","employer":"Radiantix","email":"candyoconnor@radiantix.com","city":"Sandston","state":"OH"}
{"index":{"_id":"805"}}
{"account_number":805,"balance":18426,"firstname":"Jackson","lastname":"Sampson","age":27,"gender":"F","address":"722 Kenmore Court","employer":"Daido","email":"jacksonsampson@daido.com","city":"Bellamy","state":"ME"}
{"index":{"_id":"812"}}
{"account_number":812,"balance":42593,"firstname":"Graves","lastname":"Newman","age":32,"gender":"F","address":"916 Joralemon Street","employer":"Ecrater","email":"gravesnewman@ecrater.com","city":"Crown","state":"PA"}
{"index":{"_id":"817"}}
{"account_number":817,"balance":36582,"firstname":"Padilla","lastname":"Bauer","age":36,"gender":"F","address":"310 Cadman Plaza","employer":"Exoblue","email":"padillabauer@exoblue.com","city":"Ahwahnee","state":"MN"}
{"index":{"_id":"824"}}
{"account_number":824,"balance":6053,"firstname":"Dyer","lastname":"Henson","age":33,"gender":"M","address":"650 Seaview Avenue","employer":"Nitracyr","email":"dyerhenson@nitracyr.com","city":"Gibsonia","state":"KS"}
{"index":{"_id":"829"}}
{"account_number":829,"balance":20263,"firstname":"Althea","lastname":"Bell","age":37,"gender":"M","address":"319 Cook Street","employer":"Hyplex","email":"altheabell@hyplex.com","city":"Wadsworth","state":"DC"}
{"index":{"_id":"831"}}
{"account_number":831,"balance":25375,"firstname":"Wendy","lastname":"Savage","age":37,"gender":"M","address":"421 Veranda Place","employer":"Neurocell","email":"wendysavage@neurocell.com","city":"Fresno","state":"MS"}
{"index":{"_id":"836"}}
{"account_number":836,"balance":20797,"firstname":"Lloyd","lastname":"Lindsay","age":25,"gender":"F","address":"953 Dinsmore Place","employer":"Suretech","email":"lloydlindsay@suretech.com","city":"Conway","state":"VA"}
{"index":{"_id":"843"}}
{"account_number":843,"balance":15555,"firstname":"Patricia","lastname":"Barton","age":34,"gender":"F","address":"406 Seabring Street","employer":"Providco","email":"patriciabarton@providco.com","city":"Avoca","state":"RI"}
{"index":{"_id":"848"}}
{"account_number":848,"balance":15443,"firstname":"Carmella","lastname":"Cash","age":38,"gender":"M","address":"988 Exeter Street","employer":"Bristo","email":"carmellacash@bristo.com","city":"Northridge","state":"ID"}
{"index":{"_id":"850"}}
{"account_number":850,"balance":6531,"firstname":"Carlene","lastname":"Gaines","age":37,"gender":"F","address":"753 Monroe Place","employer":"Naxdis","email":"carlenegaines@naxdis.com","city":"Genoa","state":"OR"}
{"index":{"_id":"855"}}
{"account_number":855,"balance":40170,"firstname":"Mia","lastname":"Stevens","age":31,"gender":"F","address":"326 Driggs Avenue","employer":"Aeora","email":"miastevens@aeora.com","city":"Delwood","state":"IL"}
{"index":{"_id":"862"}}
{"account_number":862,"balance":38792,"firstname":"Clayton","lastname":"Golden","age":38,"gender":"F","address":"620 Regent Place","employer":"Accusage","email":"claytongolden@accusage.com","city":"Ona","state":"NC"}
{"index":{"_id":"867"}}
{"account_number":867,"balance":45453,"firstname":"Blanca","lastname":"Ellison","age":23,"gender":"F","address":"593 McKibben Street","employer":"Koogle","email":"blancaellison@koogle.com","city":"Frystown","state":"WY"}
{"index":{"_id":"874"}}
{"account_number":874,"balance":23079,"firstname":"Lynette","lastname":"Higgins","age":22,"gender":"M","address":"377 McKinley Avenue","employer":"Menbrain","email":"lynettehiggins@menbrain.com","city":"Manitou","state":"TX"}
{"index":{"_id":"879"}}
{"account_number":879,"balance":48332,"firstname":"Sabrina","lastname":"Lancaster","age":31,"gender":"F","address":"382 Oak Street","employer":"Webiotic","email":"sabrinalancaster@webiotic.com","city":"Lindisfarne","state":"AZ"}
{"index":{"_id":"881"}}
{"account_number":881,"balance":26684,"firstname":"Barnes","lastname":"Ware","age":38,"gender":"F","address":"666 Hooper Street","employer":"Norali","email":"barnesware@norali.com","city":"Cazadero","state":"GA"}
{"index":{"_id":"886"}}
{"account_number":886,"balance":14867,"firstname":"Willa","lastname":"Leblanc","age":38,"gender":"F","address":"773 Bergen Street","employer":"Nurali","email":"willaleblanc@nurali.com","city":"Hilltop","state":"NC"}
{"index":{"_id":"893"}}
{"account_number":893,"balance":42584,"firstname":"Moses","lastname":"Campos","age":38,"gender":"F","address":"991 Bevy Court","employer":"Trollery","email":"mosescampos@trollery.com","city":"Freetown","state":"AK"}
{"index":{"_id":"898"}}
{"account_number":898,"balance":12019,"firstname":"Lori","lastname":"Stevenson","age":29,"gender":"M","address":"910 Coles Street","employer":"Honotron","email":"loristevenson@honotron.com","city":"Shindler","state":"VT"}
{"index":{"_id":"901"}}
{"account_number":901,"balance":35038,"firstname":"Irma","lastname":"Dotson","age":23,"gender":"F","address":"245 Mayfair Drive","employer":"Bleeko","email":"irmadotson@bleeko.com","city":"Lodoga","state":"UT"}
{"index":{"_id":"906"}}
{"account_number":906,"balance":24073,"firstname":"Vicki","lastname":"Suarez","age":36,"gender":"M","address":"829 Roosevelt Place","employer":"Utara","email":"vickisuarez@utara.com","city":"Albrightsville","state":"AR"}
{"index":{"_id":"913"}}
{"account_number":913,"balance":47657,"firstname":"Margery","lastname":"Monroe","age":25,"gender":"M","address":"941 Fanchon Place","employer":"Exerta","email":"margerymonroe@exerta.com","city":"Bannock","state":"MD"}
{"index":{"_id":"918"}}
{"account_number":918,"balance":36776,"firstname":"Dianna","lastname":"Hernandez","age":25,"gender":"M","address":"499 Moultrie Street","employer":"Isologica","email":"diannahernandez@isologica.com","city":"Falconaire","state":"ID"}
{"index":{"_id":"920"}}
{"account_number":920,"balance":41513,"firstname":"Jerri","lastname":"Mitchell","age":26,"gender":"M","address":"831 Kent Street","employer":"Tasmania","email":"jerrimitchell@tasmania.com","city":"Cotopaxi","state":"IA"}
{"index":{"_id":"925"}}
{"account_number":925,"balance":18295,"firstname":"Rosario","lastname":"Jackson","age":24,"gender":"M","address":"178 Leonora Court","employer":"Progenex","email":"rosariojackson@progenex.com","city":"Rivereno","state":"DE"}
{"index":{"_id":"932"}}
{"account_number":932,"balance":3111,"firstname":"Summer","lastname":"Porter","age":33,"gender":"F","address":"949 Grand Avenue","employer":"Multiflex","email":"summerporter@multiflex.com","city":"Spokane","state":"OK"}
{"index":{"_id":"937"}}
{"account_number":937,"balance":43491,"firstname":"Selma","lastname":"Anderson","age":24,"gender":"M","address":"205 Reed Street","employer":"Dadabase","email":"selmaanderson@dadabase.com","city":"Malo","state":"AL"}
{"index":{"_id":"944"}}
{"account_number":944,"balance":46478,"firstname":"Donaldson","lastname":"Woodard","age":38,"gender":"F","address":"498 Laurel Avenue","employer":"Zogak","email":"donaldsonwoodard@zogak.com","city":"Hasty","state":"ID"}
{"index":{"_id":"949"}}
{"account_number":949,"balance":48703,"firstname":"Latasha","lastname":"Mullins","age":29,"gender":"F","address":"272 Lefferts Place","employer":"Zenolux","email":"latashamullins@zenolux.com","city":"Kieler","state":"MN"}
{"index":{"_id":"951"}}
{"account_number":951,"balance":36337,"firstname":"Tran","lastname":"Burris","age":25,"gender":"F","address":"561 Rutland Road","employer":"Geoform","email":"tranburris@geoform.com","city":"Longbranch","state":"IL"}
{"index":{"_id":"956"}}
{"account_number":956,"balance":19477,"firstname":"Randall","lastname":"Lynch","age":22,"gender":"F","address":"490 Madison Place","employer":"Cosmetex","email":"randalllynch@cosmetex.com","city":"Wells","state":"SD"}
{"index":{"_id":"963"}}
{"account_number":963,"balance":30461,"firstname":"Griffin","lastname":"Sheppard","age":20,"gender":"M","address":"682 Linden Street","employer":"Zanymax","email":"griffinsheppard@zanymax.com","city":"Fannett","state":"NM"}
{"index":{"_id":"968"}}
{"account_number":968,"balance":32371,"firstname":"Luella","lastname":"Burch","age":39,"gender":"M","address":"684 Arkansas Drive","employer":"Krag","email":"luellaburch@krag.com","city":"Brambleton","state":"SD"}
{"index":{"_id":"970"}}
{"account_number":970,"balance":19648,"firstname":"Forbes","lastname":"Wallace","age":28,"gender":"M","address":"990 Mill Road","employer":"Pheast","email":"forbeswallace@pheast.com","city":"Lopezo","state":"AK"}
{"index":{"_id":"975"}}
{"account_number":975,"balance":5239,"firstname":"Delores","lastname":"Booker","age":27,"gender":"F","address":"328 Conselyea Street","employer":"Centice","email":"deloresbooker@centice.com","city":"Williams","state":"HI"}
{"index":{"_id":"982"}}
{"account_number":982,"balance":16511,"firstname":"Buck","lastname":"Robinson","age":24,"gender":"M","address":"301 Melrose Street","employer":"Calcu","email":"buckrobinson@calcu.com","city":"Welch","state":"PA"}
{"index":{"_id":"987"}}
{"account_number":987,"balance":4072,"firstname":"Brock","lastname":"Sandoval","age":20,"gender":"F","address":"977 Gem Street","employer":"Fiberox","email":"brocksandoval@fiberox.com","city":"Celeryville","state":"NY"}
{"index":{"_id":"994"}}
{"account_number":994,"balance":33298,"firstname":"Madge","lastname":"Holcomb","age":31,"gender":"M","address":"612 Hawthorne Street","employer":"Escenta","email":"madgeholcomb@escenta.com","city":"Alafaya","state":"OR"}
{"index":{"_id":"999"}}
{"account_number":999,"balance":6087,"firstname":"Dorothy","lastname":"Barron","age":22,"gender":"F","address":"499 Laurel Avenue","employer":"Xurban","email":"dorothybarron@xurban.com","city":"Belvoir","state":"CA"}
{"index":{"_id":"4"}}
{"account_number":4,"balance":27658,"firstname":"Rodriquez","lastname":"Flores","age":31,"gender":"F","address":"986 Wyckoff Avenue","employer":"Tourmania","email":"rodriquezflores@tourmania.com","city":"Eastvale","state":"HI"}
{"index":{"_id":"9"}}
{"account_number":9,"balance":24776,"firstname":"Opal","lastname":"Meadows","age":39,"gender":"M","address":"963 Neptune Avenue","employer":"Cedward","email":"opalmeadows@cedward.com","city":"Olney","state":"OH"}
{"index":{"_id":"11"}}
{"account_number":11,"balance":20203,"firstname":"Jenkins","lastname":"Haney","age":20,"gender":"M","address":"740 Ferry Place","employer":"Qimonk","email":"jenkinshaney@qimonk.com","city":"Steinhatchee","state":"GA"}
{"index":{"_id":"16"}}
{"account_number":16,"balance":35883,"firstname":"Adrian","lastname":"Pitts","age":34,"gender":"F","address":"963 Fay Court","employer":"Combogene","email":"adrianpitts@combogene.com","city":"Remington","state":"SD"}
{"index":{"_id":"23"}}
{"account_number":23,"balance":42374,"firstname":"Kirsten","lastname":"Fox","age":20,"gender":"M","address":"330 Dumont Avenue","employer":"Codax","email":"kirstenfox@codax.com","city":"Walton","state":"AK"}
{"index":{"_id":"28"}}
{"account_number":28,"balance":42112,"firstname":"Vega","lastname":"Flynn","age":20,"gender":"M","address":"647 Hyman Court","employer":"Accupharm","email":"vegaflynn@accupharm.com","city":"Masthope","state":"OH"}
{"index":{"_id":"30"}}
{"account_number":30,"balance":19087,"firstname":"Lamb","lastname":"Townsend","age":26,"gender":"M","address":"169 Lyme Avenue","employer":"Geeknet","email":"lambtownsend@geeknet.com","city":"Epworth","state":"AL"}
{"index":{"_id":"35"}}
{"account_number":35,"balance":42039,"firstname":"Darla","lastname":"Bridges","age":27,"gender":"F","address":"315 Central Avenue","employer":"Xeronk","email":"darlabridges@xeronk.com","city":"Woodlake","state":"RI"}
{"index":{"_id":"42"}}
{"account_number":42,"balance":21137,"firstname":"Harding","lastname":"Hobbs","age":26,"gender":"F","address":"474 Ridgewood Place","employer":"Xth","email":"hardinghobbs@xth.com","city":"Heil","state":"ND"}
{"index":{"_id":"47"}}
{"account_number":47,"balance":33044,"firstname":"Georgia","lastname":"Wilkerson","age":23,"gender":"M","address":"369 Herbert Street","employer":"Endipin","email":"georgiawilkerson@endipin.com","city":"Dellview","state":"WI"}
{"index":{"_id":"54"}}
{"account_number":54,"balance":23406,"firstname":"Angel","lastname":"Mann","age":22,"gender":"F","address":"229 Ferris Street","employer":"Amtas","email":"angelmann@amtas.com","city":"Calverton","state":"WA"}
{"index":{"_id":"59"}}
{"account_number":59,"balance":37728,"firstname":"Malone","lastname":"Justice","age":37,"gender":"F","address":"721 Russell Street","employer":"Emoltra","email":"malonejustice@emoltra.com","city":"Trucksville","state":"HI"}
{"index":{"_id":"61"}}
{"account_number":61,"balance":6856,"firstname":"Shawn","lastname":"Baird","age":20,"gender":"M","address":"605 Monument Walk","employer":"Moltonic","email":"shawnbaird@moltonic.com","city":"Darlington","state":"MN"}
{"index":{"_id":"66"}}
{"account_number":66,"balance":25939,"firstname":"Franks","lastname":"Salinas","age":28,"gender":"M","address":"437 Hamilton Walk","employer":"Cowtown","email":"frankssalinas@cowtown.com","city":"Chase","state":"VT"}
{"index":{"_id":"73"}}
{"account_number":73,"balance":33457,"firstname":"Irene","lastname":"Stephenson","age":32,"gender":"M","address":"684 Miller Avenue","employer":"Hawkster","email":"irenestephenson@hawkster.com","city":"Levant","state":"AR"}
{"index":{"_id":"78"}}
{"account_number":78,"balance":48656,"firstname":"Elvira","lastname":"Patterson","age":23,"gender":"F","address":"834 Amber Street","employer":"Assistix","email":"elvirapatterson@assistix.com","city":"Dunbar","state":"TN"}
{"index":{"_id":"80"}}
{"account_number":80,"balance":13445,"firstname":"Lacey","lastname":"Blanchard","age":30,"gender":"F","address":"823 Himrod Street","employer":"Comdom","email":"laceyblanchard@comdom.com","city":"Matthews","state":"MO"}
{"index":{"_id":"85"}}
{"account_number":85,"balance":48735,"firstname":"Wilcox","lastname":"Sellers","age":20,"gender":"M","address":"212 Irving Avenue","employer":"Confrenzy","email":"wilcoxsellers@confrenzy.com","city":"Kipp","state":"MT"}
{"index":{"_id":"92"}}
{"account_number":92,"balance":26753,"firstname":"Gay","lastname":"Brewer","age":34,"gender":"M","address":"369 Ditmars Street","employer":"Savvy","email":"gaybrewer@savvy.com","city":"Moquino","state":"HI"}
{"index":{"_id":"97"}}
{"account_number":97,"balance":49671,"firstname":"Karen","lastname":"Trujillo","age":40,"gender":"F","address":"512 Cumberland Walk","employer":"Tsunamia","email":"karentrujillo@tsunamia.com","city":"Fredericktown","state":"MO"}
{"index":{"_id":"100"}}
{"account_number":100,"balance":29869,"firstname":"Madden","lastname":"Woods","age":32,"gender":"F","address":"696 Ryder Avenue","employer":"Slumberia","email":"maddenwoods@slumberia.com","city":"Deercroft","state":"ME"}
{"index":{"_id":"105"}}
{"account_number":105,"balance":29654,"firstname":"Castillo","lastname":"Dickerson","age":33,"gender":"F","address":"673 Oxford Street","employer":"Tellifly","email":"castillodickerson@tellifly.com","city":"Succasunna","state":"NY"}
{"index":{"_id":"112"}}
{"account_number":112,"balance":38395,"firstname":"Frederick","lastname":"Case","age":30,"gender":"F","address":"580 Lexington Avenue","employer":"Talkalot","email":"frederickcase@talkalot.com","city":"Orovada","state":"MA"}
{"index":{"_id":"117"}}
{"account_number":117,"balance":48831,"firstname":"Robin","lastname":"Hays","age":38,"gender":"F","address":"347 Hornell Loop","employer":"Pasturia","email":"robinhays@pasturia.com","city":"Sims","state":"WY"}
{"index":{"_id":"124"}}
{"account_number":124,"balance":16425,"firstname":"Fern","lastname":"Lambert","age":20,"gender":"M","address":"511 Jay Street","employer":"Furnitech","email":"fernlambert@furnitech.com","city":"Cloverdale","state":"FL"}
{"index":{"_id":"129"}}
{"account_number":129,"balance":42409,"firstname":"Alexandria","lastname":"Sanford","age":33,"gender":"F","address":"934 Ridgecrest Terrace","employer":"Kyagoro","email":"alexandriasanford@kyagoro.com","city":"Concho","state":"UT"}
{"index":{"_id":"131"}}
{"account_number":131,"balance":28030,"firstname":"Dollie","lastname":"Koch","age":22,"gender":"F","address":"287 Manhattan Avenue","employer":"Skinserve","email":"dolliekoch@skinserve.com","city":"Shasta","state":"PA"}
{"index":{"_id":"136"}}
{"account_number":136,"balance":45801,"firstname":"Winnie","lastname":"Holland","age":38,"gender":"M","address":"198 Mill Lane","employer":"Neteria","email":"winnieholland@neteria.com","city":"Urie","state":"IL"}
{"index":{"_id":"143"}}
{"account_number":143,"balance":43093,"firstname":"Cohen","lastname":"Noble","age":39,"gender":"M","address":"454 Nelson Street","employer":"Buzzworks","email":"cohennoble@buzzworks.com","city":"Norvelt","state":"CO"}
{"index":{"_id":"148"}}
{"account_number":148,"balance":3662,"firstname":"Annmarie","lastname":"Snider","age":34,"gender":"F","address":"857 Lafayette Walk","employer":"Edecine","email":"annmariesnider@edecine.com","city":"Hollins","state":"OH"}
{"index":{"_id":"150"}}
{"account_number":150,"balance":15306,"firstname":"Ortega","lastname":"Dalton","age":20,"gender":"M","address":"237 Mermaid Avenue","employer":"Rameon","email":"ortegadalton@rameon.com","city":"Maxville","state":"NH"}
{"index":{"_id":"155"}}
{"account_number":155,"balance":27878,"firstname":"Atkinson","lastname":"Hudson","age":39,"gender":"F","address":"434 Colin Place","employer":"Qualitern","email":"atkinsonhudson@qualitern.com","city":"Hoehne","state":"OH"}
{"index":{"_id":"162"}}
{"account_number":162,"balance":6302,"firstname":"Griffith","lastname":"Calderon","age":35,"gender":"M","address":"871 Vandervoort Place","employer":"Quotezart","email":"griffithcalderon@quotezart.com","city":"Barclay","state":"FL"}
{"index":{"_id":"167"}}
{"account_number":167,"balance":42051,"firstname":"Hampton","lastname":"Ryan","age":20,"gender":"M","address":"618 Fleet Place","employer":"Zipak","email":"hamptonryan@zipak.com","city":"Irwin","state":"KS"}
{"index":{"_id":"174"}}
{"account_number":174,"balance":1464,"firstname":"Gamble","lastname":"Pierce","age":23,"gender":"F","address":"650 Eagle Street","employer":"Matrixity","email":"gamblepierce@matrixity.com","city":"Abiquiu","state":"OR"}
{"index":{"_id":"179"}}
{"account_number":179,"balance":13265,"firstname":"Elise","lastname":"Drake","age":25,"gender":"M","address":"305 Christopher Avenue","employer":"Turnling","email":"elisedrake@turnling.com","city":"Loretto","state":"LA"}
{"index":{"_id":"181"}}
{"account_number":181,"balance":27983,"firstname":"Bennett","lastname":"Hampton","age":22,"gender":"F","address":"435 Billings Place","employer":"Voipa","email":"bennetthampton@voipa.com","city":"Rodman","state":"WY"}
{"index":{"_id":"186"}}
{"account_number":186,"balance":18373,"firstname":"Kline","lastname":"Joyce","age":32,"gender":"M","address":"285 Falmouth Street","employer":"Tetratrex","email":"klinejoyce@tetratrex.com","city":"Klondike","state":"SD"}
{"index":{"_id":"193"}}
{"account_number":193,"balance":13412,"firstname":"Patty","lastname":"Petty","age":34,"gender":"F","address":"251 Vermont Street","employer":"Kinetica","email":"pattypetty@kinetica.com","city":"Grantville","state":"MS"}
{"index":{"_id":"198"}}
{"account_number":198,"balance":19686,"firstname":"Rachael","lastname":"Sharp","age":38,"gender":"F","address":"443 Vernon Avenue","employer":"Powernet","email":"rachaelsharp@powernet.com","city":"Canoochee","state":"UT"}
{"index":{"_id":"201"}}
{"account_number":201,"balance":14586,"firstname":"Ronda","lastname":"Perry","age":25,"gender":"F","address":"856 Downing Street","employer":"Artiq","email":"rondaperry@artiq.com","city":"Colton","state":"WV"}
{"index":{"_id":"206"}}
{"account_number":206,"balance":47423,"firstname":"Kelli","lastname":"Francis","age":20,"gender":"M","address":"671 George Street","employer":"Exoswitch","email":"kellifrancis@exoswitch.com","city":"Babb","state":"NJ"}
{"index":{"_id":"213"}}
{"account_number":213,"balance":34172,"firstname":"Bauer","lastname":"Summers","age":27,"gender":"M","address":"257 Boynton Place","employer":"Voratak","email":"bauersummers@voratak.com","city":"Oceola","state":"NC"}
{"index":{"_id":"218"}}
{"account_number":218,"balance":26702,"firstname":"Garrison","lastname":"Bryan","age":24,"gender":"F","address":"478 Greenpoint Avenue","employer":"Uniworld","email":"garrisonbryan@uniworld.com","city":"Comptche","state":"WI"}
{"index":{"_id":"220"}}
{"account_number":220,"balance":3086,"firstname":"Tania","lastname":"Middleton","age":22,"gender":"F","address":"541 Gunther Place","employer":"Zerology","email":"taniamiddleton@zerology.com","city":"Linwood","state":"IN"}
{"index":{"_id":"225"}}
{"account_number":225,"balance":21949,"firstname":"Maryann","lastname":"Murphy","age":24,"gender":"F","address":"894 Bridgewater Street","employer":"Cinesanct","email":"maryannmurphy@cinesanct.com","city":"Cartwright","state":"RI"}
{"index":{"_id":"232"}}
{"account_number":232,"balance":11984,"firstname":"Carr","lastname":"Jensen","age":34,"gender":"F","address":"995 Micieli Place","employer":"Biohab","email":"carrjensen@biohab.com","city":"Waikele","state":"OH"}
{"index":{"_id":"237"}}
{"account_number":237,"balance":5603,"firstname":"Kirby","lastname":"Watkins","age":27,"gender":"F","address":"348 Blake Court","employer":"Sonique","email":"kirbywatkins@sonique.com","city":"Freelandville","state":"PA"}
{"index":{"_id":"244"}}
{"account_number":244,"balance":8048,"firstname":"Judith","lastname":"Riggs","age":27,"gender":"F","address":"590 Kosciusko Street","employer":"Arctiq","email":"judithriggs@arctiq.com","city":"Gorham","state":"DC"}
{"index":{"_id":"249"}}
{"account_number":249,"balance":16822,"firstname":"Mckinney","lastname":"Gallagher","age":38,"gender":"F","address":"939 Seigel Court","employer":"Premiant","email":"mckinneygallagher@premiant.com","city":"Catharine","state":"NH"}
{"index":{"_id":"251"}}
{"account_number":251,"balance":13475,"firstname":"Marks","lastname":"Graves","age":39,"gender":"F","address":"427 Lawn Court","employer":"Dentrex","email":"marksgraves@dentrex.com","city":"Waukeenah","state":"IL"}
{"index":{"_id":"256"}}
{"account_number":256,"balance":48318,"firstname":"Simon","lastname":"Hogan","age":31,"gender":"M","address":"789 Suydam Place","employer":"Dancerity","email":"simonhogan@dancerity.com","city":"Dargan","state":"GA"}
{"index":{"_id":"263"}}
{"account_number":263,"balance":12837,"firstname":"Thornton","lastname":"Meyer","age":29,"gender":"M","address":"575 Elliott Place","employer":"Peticular","email":"thorntonmeyer@peticular.com","city":"Dotsero","state":"NH"}
{"index":{"_id":"268"}}
{"account_number":268,"balance":20925,"firstname":"Avis","lastname":"Blackwell","age":36,"gender":"M","address":"569 Jerome Avenue","employer":"Magnina","email":"avisblackwell@magnina.com","city":"Bethany","state":"MD"}
{"index":{"_id":"270"}}
{"account_number":270,"balance":43951,"firstname":"Moody","lastname":"Harmon","age":39,"gender":"F","address":"233 Vanderbilt Street","employer":"Otherside","email":"moodyharmon@otherside.com","city":"Elwood","state":"MT"}
{"index":{"_id":"275"}}
{"account_number":275,"balance":2384,"firstname":"Reynolds","lastname":"Barnett","age":31,"gender":"M","address":"394 Stockton Street","employer":"Austex","email":"reynoldsbarnett@austex.com","city":"Grandview","state":"MS"}
{"index":{"_id":"282"}}
{"account_number":282,"balance":38540,"firstname":"Gay","lastname":"Schultz","age":25,"gender":"F","address":"805 Claver Place","employer":"Handshake","email":"gayschultz@handshake.com","city":"Tampico","state":"MA"}
{"index":{"_id":"287"}}
{"account_number":287,"balance":10845,"firstname":"Valerie","lastname":"Lang","age":35,"gender":"F","address":"423 Midwood Street","employer":"Quarx","email":"valerielang@quarx.com","city":"Cannondale","state":"VT"}
{"index":{"_id":"294"}}
{"account_number":294,"balance":29582,"firstname":"Pitts","lastname":"Haynes","age":26,"gender":"M","address":"901 Broome Street","employer":"Aquazure","email":"pittshaynes@aquazure.com","city":"Turah","state":"SD"}
{"index":{"_id":"299"}}
{"account_number":299,"balance":40825,"firstname":"Angela","lastname":"Talley","age":36,"gender":"F","address":"822 Bills Place","employer":"Remold","email":"angelatalley@remold.com","city":"Bethpage","state":"DC"}
{"index":{"_id":"302"}}
{"account_number":302,"balance":11298,"firstname":"Isabella","lastname":"Hewitt","age":40,"gender":"M","address":"455 Bedford Avenue","employer":"Cincyr","email":"isabellahewitt@cincyr.com","city":"Blanford","state":"IN"}
{"index":{"_id":"307"}}
{"account_number":307,"balance":43355,"firstname":"Enid","lastname":"Ashley","age":23,"gender":"M","address":"412 Emerson Place","employer":"Avenetro","email":"enidashley@avenetro.com","city":"Catherine","state":"WI"}
{"index":{"_id":"314"}}
{"account_number":314,"balance":5848,"firstname":"Norton","lastname":"Norton","age":35,"gender":"M","address":"252 Ditmas Avenue","employer":"Talkola","email":"nortonnorton@talkola.com","city":"Veyo","state":"SC"}
{"index":{"_id":"319"}}
{"account_number":319,"balance":15430,"firstname":"Ferrell","lastname":"Mckinney","age":36,"gender":"M","address":"874 Cranberry Street","employer":"Portaline","email":"ferrellmckinney@portaline.com","city":"Rose","state":"WV"}
{"index":{"_id":"321"}}
{"account_number":321,"balance":43370,"firstname":"Marta","lastname":"Larsen","age":35,"gender":"M","address":"617 Williams Court","employer":"Manufact","email":"martalarsen@manufact.com","city":"Sisquoc","state":"MA"}
{"index":{"_id":"326"}}
{"account_number":326,"balance":9692,"firstname":"Pearl","lastname":"Reese","age":30,"gender":"F","address":"451 Colonial Court","employer":"Accruex","email":"pearlreese@accruex.com","city":"Westmoreland","state":"MD"}
{"index":{"_id":"333"}}
{"account_number":333,"balance":22778,"firstname":"Trudy","lastname":"Sweet","age":27,"gender":"F","address":"881 Kiely Place","employer":"Acumentor","email":"trudysweet@acumentor.com","city":"Kent","state":"IA"}
{"index":{"_id":"338"}}
{"account_number":338,"balance":6969,"firstname":"Pierce","lastname":"Lawrence","age":35,"gender":"M","address":"318 Gallatin Place","employer":"Lunchpad","email":"piercelawrence@lunchpad.com","city":"Iola","state":"MD"}
{"index":{"_id":"340"}}
{"account_number":340,"balance":42072,"firstname":"Juarez","lastname":"Gutierrez","age":40,"gender":"F","address":"802 Seba Avenue","employer":"Billmed","email":"juarezgutierrez@billmed.com","city":"Malott","state":"OH"}
{"index":{"_id":"345"}}
{"account_number":345,"balance":9812,"firstname":"Parker","lastname":"Hines","age":38,"gender":"M","address":"715 Mill Avenue","employer":"Baluba","email":"parkerhines@baluba.com","city":"Blackgum","state":"KY"}
{"index":{"_id":"352"}}
{"account_number":352,"balance":20290,"firstname":"Kendra","lastname":"Mcintosh","age":31,"gender":"F","address":"963 Wolf Place","employer":"Orboid","email":"kendramcintosh@orboid.com","city":"Bladensburg","state":"AK"}
{"index":{"_id":"357"}}
{"account_number":357,"balance":15102,"firstname":"Adele","lastname":"Carroll","age":39,"gender":"F","address":"381 Arion Place","employer":"Aquafire","email":"adelecarroll@aquafire.com","city":"Springville","state":"RI"}
{"index":{"_id":"364"}}
{"account_number":364,"balance":35247,"firstname":"Felicia","lastname":"Merrill","age":40,"gender":"F","address":"229 Branton Street","employer":"Prosely","email":"feliciamerrill@prosely.com","city":"Dola","state":"MA"}
{"index":{"_id":"369"}}
{"account_number":369,"balance":17047,"firstname":"Mcfadden","lastname":"Guy","age":28,"gender":"F","address":"445 Lott Avenue","employer":"Kangle","email":"mcfaddenguy@kangle.com","city":"Greenbackville","state":"DE"}
{"index":{"_id":"371"}}
{"account_number":371,"balance":19751,"firstname":"Barker","lastname":"Allen","age":32,"gender":"F","address":"295 Wallabout Street","employer":"Nexgene","email":"barkerallen@nexgene.com","city":"Nanafalia","state":"NE"}
{"index":{"_id":"376"}}
{"account_number":376,"balance":44407,"firstname":"Mcmillan","lastname":"Dunn","age":21,"gender":"F","address":"771 Dorchester Road","employer":"Eargo","email":"mcmillandunn@eargo.com","city":"Yogaville","state":"RI"}
{"index":{"_id":"383"}}
{"account_number":383,"balance":48889,"firstname":"Knox","lastname":"Larson","age":28,"gender":"F","address":"962 Bartlett Place","employer":"Bostonic","email":"knoxlarson@bostonic.com","city":"Smeltertown","state":"TX"}
{"index":{"_id":"388"}}
{"account_number":388,"balance":9606,"firstname":"Julianne","lastname":"Nicholson","age":26,"gender":"F","address":"338 Crescent Street","employer":"Viasia","email":"juliannenicholson@viasia.com","city":"Alleghenyville","state":"MO"}
{"index":{"_id":"390"}}
{"account_number":390,"balance":7464,"firstname":"Ramona","lastname":"Roy","age":32,"gender":"M","address":"135 Banner Avenue","employer":"Deminimum","email":"ramonaroy@deminimum.com","city":"Dodge","state":"ID"}
{"index":{"_id":"395"}}
{"account_number":395,"balance":18679,"firstname":"Juliet","lastname":"Whitaker","age":31,"gender":"M","address":"128 Remsen Avenue","employer":"Toyletry","email":"julietwhitaker@toyletry.com","city":"Yonah","state":"LA"}
{"index":{"_id":"403"}}
{"account_number":403,"balance":18833,"firstname":"Williamson","lastname":"Horn","age":32,"gender":"M","address":"223 Strickland Avenue","employer":"Nimon","email":"williamsonhorn@nimon.com","city":"Bawcomville","state":"NJ"}
{"index":{"_id":"408"}}
{"account_number":408,"balance":34666,"firstname":"Lidia","lastname":"Guerrero","age":30,"gender":"M","address":"254 Stratford Road","employer":"Snowpoke","email":"lidiaguerrero@snowpoke.com","city":"Fairlee","state":"LA"}
{"index":{"_id":"410"}}
{"account_number":410,"balance":31200,"firstname":"Fox","lastname":"Cardenas","age":39,"gender":"M","address":"987 Monitor Street","employer":"Corpulse","email":"foxcardenas@corpulse.com","city":"Southview","state":"NE"}
{"index":{"_id":"415"}}
{"account_number":415,"balance":19449,"firstname":"Martinez","lastname":"Benson","age":36,"gender":"M","address":"172 Berkeley Place","employer":"Enersol","email":"martinezbenson@enersol.com","city":"Chumuckla","state":"AL"}
{"index":{"_id":"422"}}
{"account_number":422,"balance":40162,"firstname":"Brigitte","lastname":"Scott","age":26,"gender":"M","address":"662 Vermont Court","employer":"Waretel","email":"brigittescott@waretel.com","city":"Elrama","state":"VA"}
{"index":{"_id":"427"}}
{"account_number":427,"balance":1463,"firstname":"Rebekah","lastname":"Garrison","age":36,"gender":"F","address":"837 Hampton Avenue","employer":"Niquent","email":"rebekahgarrison@niquent.com","city":"Zarephath","state":"NY"}
{"index":{"_id":"434"}}
{"account_number":434,"balance":11329,"firstname":"Christa","lastname":"Huff","age":25,"gender":"M","address":"454 Oriental Boulevard","employer":"Earthpure","email":"christahuff@earthpure.com","city":"Stevens","state":"DC"}
{"index":{"_id":"439"}}
{"account_number":439,"balance":22752,"firstname":"Lula","lastname":"Williams","age":35,"gender":"M","address":"630 Furman Avenue","employer":"Vinch","email":"lulawilliams@vinch.com","city":"Newcastle","state":"ME"}
{"index":{"_id":"441"}}
{"account_number":441,"balance":47947,"firstname":"Dickson","lastname":"Mcgee","age":29,"gender":"M","address":"478 Knight Court","employer":"Gogol","email":"dicksonmcgee@gogol.com","city":"Laurelton","state":"AR"}
{"index":{"_id":"446"}}
{"account_number":446,"balance":23071,"firstname":"Lolita","lastname":"Fleming","age":32,"gender":"F","address":"918 Bridge Street","employer":"Vidto","email":"lolitafleming@vidto.com","city":"Brownlee","state":"HI"}
{"index":{"_id":"453"}}
{"account_number":453,"balance":21520,"firstname":"Hood","lastname":"Powell","age":24,"gender":"F","address":"479 Brevoort Place","employer":"Vortexaco","email":"hoodpowell@vortexaco.com","city":"Alderpoint","state":"CT"}
{"index":{"_id":"458"}}
{"account_number":458,"balance":8865,"firstname":"Aida","lastname":"Wolf","age":21,"gender":"F","address":"403 Thames Street","employer":"Isis","email":"aidawolf@isis.com","city":"Bordelonville","state":"ME"}
{"index":{"_id":"460"}}
{"account_number":460,"balance":37734,"firstname":"Aguirre","lastname":"White","age":21,"gender":"F","address":"190 Crooke Avenue","employer":"Unq","email":"aguirrewhite@unq.com","city":"Albany","state":"NJ"}
{"index":{"_id":"465"}}
{"account_number":465,"balance":10681,"firstname":"Pearlie","lastname":"Holman","age":29,"gender":"M","address":"916 Evergreen Avenue","employer":"Hometown","email":"pearlieholman@hometown.com","city":"Needmore","state":"UT"}
{"index":{"_id":"472"}}
{"account_number":472,"balance":25571,"firstname":"Lee","lastname":"Long","age":32,"gender":"F","address":"288 Mill Street","employer":"Comverges","email":"leelong@comverges.com","city":"Movico","state":"MT"}
{"index":{"_id":"477"}}
{"account_number":477,"balance":25892,"firstname":"Holcomb","lastname":"Cobb","age":40,"gender":"M","address":"369 Marconi Place","employer":"Steeltab","email":"holcombcobb@steeltab.com","city":"Byrnedale","state":"CA"}
{"index":{"_id":"484"}}
{"account_number":484,"balance":3274,"firstname":"Staci","lastname":"Melendez","age":35,"gender":"F","address":"751 Otsego Street","employer":"Namebox","email":"stacimelendez@namebox.com","city":"Harborton","state":"NV"}
{"index":{"_id":"489"}}
{"account_number":489,"balance":7879,"firstname":"Garrett","lastname":"Langley","age":36,"gender":"M","address":"331 Bowne Street","employer":"Zillidium","email":"garrettlangley@zillidium.com","city":"Riviera","state":"LA"}
{"index":{"_id":"491"}}
{"account_number":491,"balance":42942,"firstname":"Teresa","lastname":"Owen","age":24,"gender":"F","address":"713 Canton Court","employer":"Plasmos","email":"teresaowen@plasmos.com","city":"Bartonsville","state":"NH"}
{"index":{"_id":"496"}}
{"account_number":496,"balance":14869,"firstname":"Alison","lastname":"Conrad","age":35,"gender":"F","address":"347 Varet Street","employer":"Perkle","email":"alisonconrad@perkle.com","city":"Cliffside","state":"OH"}
{"index":{"_id":"504"}}
{"account_number":504,"balance":49205,"firstname":"Shanna","lastname":"Chambers","age":23,"gender":"M","address":"220 Beard Street","employer":"Corporana","email":"shannachambers@corporana.com","city":"Cashtown","state":"AZ"}
{"index":{"_id":"509"}}
{"account_number":509,"balance":34754,"firstname":"Durham","lastname":"Pacheco","age":40,"gender":"M","address":"129 Plymouth Street","employer":"Datacator","email":"durhampacheco@datacator.com","city":"Loveland","state":"NC"}
{"index":{"_id":"511"}}
{"account_number":511,"balance":40908,"firstname":"Elba","lastname":"Grant","age":24,"gender":"F","address":"157 Bijou Avenue","employer":"Dognost","email":"elbagrant@dognost.com","city":"Coyote","state":"MT"}
{"index":{"_id":"516"}}
{"account_number":516,"balance":44940,"firstname":"Roy","lastname":"Smith","age":37,"gender":"M","address":"770 Cherry Street","employer":"Parleynet","email":"roysmith@parleynet.com","city":"Carrsville","state":"RI"}
{"index":{"_id":"523"}}
{"account_number":523,"balance":28729,"firstname":"Amalia","lastname":"Benjamin","age":40,"gender":"F","address":"173 Bushwick Place","employer":"Sentia","email":"amaliabenjamin@sentia.com","city":"Jacumba","state":"OK"}
{"index":{"_id":"528"}}
{"account_number":528,"balance":4071,"firstname":"Thompson","lastname":"Hoover","age":27,"gender":"F","address":"580 Garden Street","employer":"Portalis","email":"thompsonhoover@portalis.com","city":"Knowlton","state":"AL"}
{"index":{"_id":"530"}}
{"account_number":530,"balance":8840,"firstname":"Kathrine","lastname":"Evans","age":37,"gender":"M","address":"422 Division Place","employer":"Spherix","email":"kathrineevans@spherix.com","city":"Biddle","state":"CO"}
{"index":{"_id":"535"}}
{"account_number":535,"balance":8715,"firstname":"Fry","lastname":"George","age":34,"gender":"M","address":"722 Green Street","employer":"Ewaves","email":"frygeorge@ewaves.com","city":"Kenmar","state":"DE"}
{"index":{"_id":"542"}}
{"account_number":542,"balance":23285,"firstname":"Michelle","lastname":"Mayo","age":35,"gender":"M","address":"657 Caton Place","employer":"Biflex","email":"michellemayo@biflex.com","city":"Beaverdale","state":"WY"}
{"index":{"_id":"547"}}
{"account_number":547,"balance":12870,"firstname":"Eaton","lastname":"Rios","age":32,"gender":"M","address":"744 Withers Street","employer":"Podunk","email":"eatonrios@podunk.com","city":"Chelsea","state":"IA"}
{"index":{"_id":"554"}}
{"account_number":554,"balance":33163,"firstname":"Townsend","lastname":"Atkins","age":39,"gender":"M","address":"566 Ira Court","employer":"Acruex","email":"townsendatkins@acruex.com","city":"Valle","state":"IA"}
{"index":{"_id":"559"}}
{"account_number":559,"balance":11450,"firstname":"Tonia","lastname":"Schmidt","age":38,"gender":"F","address":"508 Sheffield Avenue","employer":"Extro","email":"toniaschmidt@extro.com","city":"Newry","state":"CT"}
{"index":{"_id":"561"}}
{"account_number":561,"balance":12370,"firstname":"Sellers","lastname":"Davis","age":30,"gender":"M","address":"860 Madoc Avenue","employer":"Isodrive","email":"sellersdavis@isodrive.com","city":"Trail","state":"KS"}
{"index":{"_id":"566"}}
{"account_number":566,"balance":6183,"firstname":"Cox","lastname":"Roman","age":37,"gender":"M","address":"349 Winthrop Street","employer":"Medcom","email":"coxroman@medcom.com","city":"Rosewood","state":"WY"}
{"index":{"_id":"573"}}
{"account_number":573,"balance":32171,"firstname":"Callie","lastname":"Castaneda","age":36,"gender":"M","address":"799 Scott Avenue","employer":"Earthwax","email":"calliecastaneda@earthwax.com","city":"Marshall","state":"NH"}
{"index":{"_id":"578"}}
{"account_number":578,"balance":34259,"firstname":"Holmes","lastname":"Mcknight","age":37,"gender":"M","address":"969 Metropolitan Avenue","employer":"Cubicide","email":"holmesmcknight@cubicide.com","city":"Aguila","state":"PA"}
{"index":{"_id":"580"}}
{"account_number":580,"balance":13716,"firstname":"Mcmahon","lastname":"York","age":34,"gender":"M","address":"475 Beacon Court","employer":"Zillar","email":"mcmahonyork@zillar.com","city":"Farmington","state":"MO"}
{"index":{"_id":"585"}}
{"account_number":585,"balance":26745,"firstname":"Nieves","lastname":"Nolan","age":32,"gender":"M","address":"115 Seagate Terrace","employer":"Jumpstack","email":"nievesnolan@jumpstack.com","city":"Eastmont","state":"UT"}
{"index":{"_id":"592"}}
{"account_number":592,"balance":32968,"firstname":"Head","lastname":"Webster","age":36,"gender":"F","address":"987 Lefferts Avenue","employer":"Empirica","email":"headwebster@empirica.com","city":"Rockingham","state":"TN"}
{"index":{"_id":"597"}}
{"account_number":597,"balance":11246,"firstname":"Penny","lastname":"Knowles","age":33,"gender":"M","address":"139 Forbell Street","employer":"Ersum","email":"pennyknowles@ersum.com","city":"Vallonia","state":"IA"}
{"index":{"_id":"600"}}
{"account_number":600,"balance":10336,"firstname":"Simmons","lastname":"Byers","age":37,"gender":"M","address":"250 Dictum Court","employer":"Qualitex","email":"simmonsbyers@qualitex.com","city":"Wanship","state":"OH"}
{"index":{"_id":"605"}}
{"account_number":605,"balance":38427,"firstname":"Mcclain","lastname":"Manning","age":24,"gender":"M","address":"832 Leonard Street","employer":"Qiao","email":"mcclainmanning@qiao.com","city":"Calvary","state":"TX"}
{"index":{"_id":"612"}}
{"account_number":612,"balance":11868,"firstname":"Dunn","lastname":"Cameron","age":32,"gender":"F","address":"156 Lorimer Street","employer":"Isonus","email":"dunncameron@isonus.com","city":"Virgie","state":"ND"}
{"index":{"_id":"617"}}
{"account_number":617,"balance":35445,"firstname":"Kitty","lastname":"Cooley","age":22,"gender":"M","address":"788 Seagate Avenue","employer":"Ultrimax","email":"kittycooley@ultrimax.com","city":"Clarktown","state":"MD"}
{"index":{"_id":"624"}}
{"account_number":624,"balance":27538,"firstname":"Roxanne","lastname":"Franklin","age":39,"gender":"F","address":"299 Woodrow Court","employer":"Silodyne","email":"roxannefranklin@silodyne.com","city":"Roulette","state":"VA"}
{"index":{"_id":"629"}}
{"account_number":629,"balance":32987,"firstname":"Mcclure","lastname":"Rodgers","age":26,"gender":"M","address":"806 Pierrepont Place","employer":"Elita","email":"mcclurerodgers@elita.com","city":"Brownsville","state":"MI"}
{"index":{"_id":"631"}}
{"account_number":631,"balance":21657,"firstname":"Corrine","lastname":"Barber","age":32,"gender":"F","address":"447 Hunts Lane","employer":"Quarmony","email":"corrinebarber@quarmony.com","city":"Wyano","state":"IL"}
{"index":{"_id":"636"}}
{"account_number":636,"balance":8036,"firstname":"Agnes","lastname":"Hooper","age":25,"gender":"M","address":"865 Hanson Place","employer":"Digial","email":"agneshooper@digial.com","city":"Sperryville","state":"OK"}
{"index":{"_id":"643"}}
{"account_number":643,"balance":8057,"firstname":"Hendricks","lastname":"Stokes","age":23,"gender":"F","address":"142 Barbey Street","employer":"Remotion","email":"hendricksstokes@remotion.com","city":"Lewis","state":"MA"}
{"index":{"_id":"648"}}
{"account_number":648,"balance":11506,"firstname":"Terry","lastname":"Montgomery","age":21,"gender":"F","address":"115 Franklin Avenue","employer":"Enervate","email":"terrymontgomery@enervate.com","city":"Bascom","state":"MA"}
{"index":{"_id":"650"}}
{"account_number":650,"balance":18091,"firstname":"Benton","lastname":"Knight","age":28,"gender":"F","address":"850 Aitken Place","employer":"Pholio","email":"bentonknight@pholio.com","city":"Cobbtown","state":"AL"}
{"index":{"_id":"655"}}
{"account_number":655,"balance":22912,"firstname":"Eula","lastname":"Taylor","age":30,"gender":"M","address":"520 Orient Avenue","employer":"Miracula","email":"eulataylor@miracula.com","city":"Wacissa","state":"IN"}
{"index":{"_id":"662"}}
{"account_number":662,"balance":10138,"firstname":"Daisy","lastname":"Burnett","age":33,"gender":"M","address":"114 Norman Avenue","employer":"Liquicom","email":"daisyburnett@liquicom.com","city":"Grahamtown","state":"MD"}
{"index":{"_id":"667"}}
{"account_number":667,"balance":22559,"firstname":"Juliana","lastname":"Chase","age":32,"gender":"M","address":"496 Coleridge Street","employer":"Comtract","email":"julianachase@comtract.com","city":"Wilsonia","state":"NJ"}
{"index":{"_id":"674"}}
{"account_number":674,"balance":36038,"firstname":"Watts","lastname":"Shannon","age":22,"gender":"F","address":"600 Story Street","employer":"Joviold","email":"wattsshannon@joviold.com","city":"Fairhaven","state":"ID"}
{"index":{"_id":"679"}}
{"account_number":679,"balance":20149,"firstname":"Henrietta","lastname":"Bonner","age":33,"gender":"M","address":"461 Bond Street","employer":"Geekol","email":"henriettabonner@geekol.com","city":"Richville","state":"WA"}
{"index":{"_id":"681"}}
{"account_number":681,"balance":34244,"firstname":"Velazquez","lastname":"Wolfe","age":33,"gender":"M","address":"773 Eckford Street","employer":"Zisis","email":"velazquezwolfe@zisis.com","city":"Smock","state":"ME"}
{"index":{"_id":"686"}}
{"account_number":686,"balance":10116,"firstname":"Decker","lastname":"Mcclure","age":30,"gender":"F","address":"236 Commerce Street","employer":"Everest","email":"deckermcclure@everest.com","city":"Gibbsville","state":"TN"}
{"index":{"_id":"693"}}
{"account_number":693,"balance":31233,"firstname":"Tabatha","lastname":"Zimmerman","age":30,"gender":"F","address":"284 Emmons Avenue","employer":"Pushcart","email":"tabathazimmerman@pushcart.com","city":"Esmont","state":"NC"}
{"index":{"_id":"698"}}
{"account_number":698,"balance":14965,"firstname":"Baker","lastname":"Armstrong","age":36,"gender":"F","address":"796 Tehama Street","employer":"Nurplex","email":"bakerarmstrong@nurplex.com","city":"Starks","state":"UT"}
{"index":{"_id":"701"}}
{"account_number":701,"balance":23772,"firstname":"Gardner","lastname":"Griffith","age":27,"gender":"M","address":"187 Moore Place","employer":"Vertide","email":"gardnergriffith@vertide.com","city":"Coventry","state":"NV"}
{"index":{"_id":"706"}}
{"account_number":706,"balance":5282,"firstname":"Eliza","lastname":"Potter","age":39,"gender":"M","address":"945 Dunham Place","employer":"Playce","email":"elizapotter@playce.com","city":"Woodruff","state":"AK"}
{"index":{"_id":"713"}}
{"account_number":713,"balance":20054,"firstname":"Iris","lastname":"Mcguire","age":21,"gender":"F","address":"508 Benson Avenue","employer":"Duflex","email":"irismcguire@duflex.com","city":"Hillsboro","state":"MO"}
{"index":{"_id":"718"}}
{"account_number":718,"balance":13876,"firstname":"Hickman","lastname":"Dillard","age":22,"gender":"F","address":"132 Etna Street","employer":"Genmy","email":"hickmandillard@genmy.com","city":"Curtice","state":"NV"}
{"index":{"_id":"720"}}
{"account_number":720,"balance":31356,"firstname":"Ruth","lastname":"Vance","age":32,"gender":"F","address":"229 Adams Street","employer":"Zilidium","email":"ruthvance@zilidium.com","city":"Allison","state":"IA"}
{"index":{"_id":"725"}}
{"account_number":725,"balance":14677,"firstname":"Reeves","lastname":"Tillman","age":26,"gender":"M","address":"674 Ivan Court","employer":"Cemention","email":"reevestillman@cemention.com","city":"Navarre","state":"MA"}
{"index":{"_id":"732"}}
{"account_number":732,"balance":38445,"firstname":"Delia","lastname":"Cruz","age":37,"gender":"F","address":"870 Cheever Place","employer":"Multron","email":"deliacruz@multron.com","city":"Cresaptown","state":"NH"}
{"index":{"_id":"737"}}
{"account_number":737,"balance":40431,"firstname":"Sampson","lastname":"Yates","age":23,"gender":"F","address":"214 Cox Place","employer":"Signidyne","email":"sampsonyates@signidyne.com","city":"Brazos","state":"GA"}
{"index":{"_id":"744"}}
{"account_number":744,"balance":8690,"firstname":"Bernard","lastname":"Martinez","age":21,"gender":"M","address":"148 Dunne Place","employer":"Dragbot","email":"bernardmartinez@dragbot.com","city":"Moraida","state":"MN"}
{"index":{"_id":"749"}}
{"account_number":749,"balance":1249,"firstname":"Rush","lastname":"Boyle","age":36,"gender":"M","address":"310 Argyle Road","employer":"Sportan","email":"rushboyle@sportan.com","city":"Brady","state":"WA"}
{"index":{"_id":"751"}}
{"account_number":751,"balance":49252,"firstname":"Patrick","lastname":"Osborne","age":23,"gender":"M","address":"915 Prospect Avenue","employer":"Gynko","email":"patrickosborne@gynko.com","city":"Takilma","state":"MO"}
{"index":{"_id":"756"}}
{"account_number":756,"balance":40006,"firstname":"Jasmine","lastname":"Howell","age":32,"gender":"M","address":"605 Elliott Walk","employer":"Ecratic","email":"jasminehowell@ecratic.com","city":"Harrodsburg","state":"OH"}
{"index":{"_id":"763"}}
{"account_number":763,"balance":12091,"firstname":"Liz","lastname":"Bentley","age":22,"gender":"F","address":"933 Debevoise Avenue","employer":"Nipaz","email":"lizbentley@nipaz.com","city":"Glenville","state":"NJ"}
{"index":{"_id":"768"}}
{"account_number":768,"balance":2213,"firstname":"Sondra","lastname":"Soto","age":21,"gender":"M","address":"625 Colonial Road","employer":"Navir","email":"sondrasoto@navir.com","city":"Benson","state":"VA"}
{"index":{"_id":"770"}}
{"account_number":770,"balance":39505,"firstname":"Joann","lastname":"Crane","age":26,"gender":"M","address":"798 Farragut Place","employer":"Lingoage","email":"joanncrane@lingoage.com","city":"Kirk","state":"MA"}
{"index":{"_id":"775"}}
{"account_number":775,"balance":27943,"firstname":"Wilson","lastname":"Merritt","age":33,"gender":"F","address":"288 Thornton Street","employer":"Geeky","email":"wilsonmerritt@geeky.com","city":"Holtville","state":"HI"}
{"index":{"_id":"782"}}
{"account_number":782,"balance":3960,"firstname":"Maldonado","lastname":"Craig","age":36,"gender":"F","address":"345 Myrtle Avenue","employer":"Zilencio","email":"maldonadocraig@zilencio.com","city":"Yukon","state":"ID"}
{"index":{"_id":"787"}}
{"account_number":787,"balance":11876,"firstname":"Harper","lastname":"Wynn","age":21,"gender":"F","address":"139 Oceanic Avenue","employer":"Interfind","email":"harperwynn@interfind.com","city":"Gerber","state":"ND"}
{"index":{"_id":"794"}}
{"account_number":794,"balance":16491,"firstname":"Walker","lastname":"Charles","age":32,"gender":"M","address":"215 Kenilworth Place","employer":"Orbin","email":"walkercharles@orbin.com","city":"Rivers","state":"WI"}
{"index":{"_id":"799"}}
{"account_number":799,"balance":2889,"firstname":"Myra","lastname":"Guerra","age":28,"gender":"F","address":"625 Dahlgreen Place","employer":"Digigene","email":"myraguerra@digigene.com","city":"Draper","state":"CA"}
{"index":{"_id":"802"}}
{"account_number":802,"balance":19630,"firstname":"Gracie","lastname":"Foreman","age":40,"gender":"F","address":"219 Kent Avenue","employer":"Supportal","email":"gracieforeman@supportal.com","city":"Westboro","state":"NH"}
{"index":{"_id":"807"}}
{"account_number":807,"balance":29206,"firstname":"Hatfield","lastname":"Lowe","age":23,"gender":"M","address":"499 Adler Place","employer":"Lovepad","email":"hatfieldlowe@lovepad.com","city":"Wiscon","state":"DC"}
{"index":{"_id":"814"}}
{"account_number":814,"balance":9838,"firstname":"Morse","lastname":"Mcbride","age":26,"gender":"F","address":"776 Calyer Street","employer":"Inear","email":"morsemcbride@inear.com","city":"Kingstowne","state":"ND"}
{"index":{"_id":"819"}}
{"account_number":819,"balance":3971,"firstname":"Karyn","lastname":"Medina","age":24,"gender":"F","address":"417 Utica Avenue","employer":"Qnekt","email":"karynmedina@qnekt.com","city":"Kerby","state":"WY"}
{"index":{"_id":"821"}}
{"account_number":821,"balance":33271,"firstname":"Trisha","lastname":"Blankenship","age":22,"gender":"M","address":"329 Jamaica Avenue","employer":"Chorizon","email":"trishablankenship@chorizon.com","city":"Sexton","state":"VT"}
{"index":{"_id":"826"}}
{"account_number":826,"balance":11548,"firstname":"Summers","lastname":"Vinson","age":22,"gender":"F","address":"742 Irwin Street","employer":"Globoil","email":"summersvinson@globoil.com","city":"Callaghan","state":"WY"}
{"index":{"_id":"833"}}
{"account_number":833,"balance":46154,"firstname":"Woodward","lastname":"Hood","age":22,"gender":"M","address":"398 Atkins Avenue","employer":"Zedalis","email":"woodwardhood@zedalis.com","city":"Stonybrook","state":"NE"}
{"index":{"_id":"838"}}
{"account_number":838,"balance":24629,"firstname":"Latonya","lastname":"Blake","age":37,"gender":"F","address":"531 Milton Street","employer":"Rugstars","email":"latonyablake@rugstars.com","city":"Tedrow","state":"WA"}
{"index":{"_id":"840"}}
{"account_number":840,"balance":39615,"firstname":"Boone","lastname":"Gomez","age":38,"gender":"M","address":"256 Hampton Place","employer":"Geekular","email":"boonegomez@geekular.com","city":"Westerville","state":"HI"}
{"index":{"_id":"845"}}
{"account_number":845,"balance":35422,"firstname":"Tracy","lastname":"Vaughn","age":39,"gender":"M","address":"645 Rockaway Parkway","employer":"Andryx","email":"tracyvaughn@andryx.com","city":"Wilmington","state":"ME"}
{"index":{"_id":"852"}}
{"account_number":852,"balance":6041,"firstname":"Allen","lastname":"Hammond","age":26,"gender":"M","address":"793 Essex Street","employer":"Tersanki","email":"allenhammond@tersanki.com","city":"Osmond","state":"NC"}
{"index":{"_id":"857"}}
{"account_number":857,"balance":39678,"firstname":"Alyce","lastname":"Douglas","age":23,"gender":"M","address":"326 Robert Street","employer":"Earbang","email":"alycedouglas@earbang.com","city":"Thornport","state":"GA"}
{"index":{"_id":"864"}}
{"account_number":864,"balance":21804,"firstname":"Duffy","lastname":"Anthony","age":23,"gender":"M","address":"582 Cooke Court","employer":"Schoolio","email":"duffyanthony@schoolio.com","city":"Brenton","state":"CO"}
{"index":{"_id":"869"}}
{"account_number":869,"balance":43544,"firstname":"Corinne","lastname":"Robbins","age":25,"gender":"F","address":"732 Quentin Road","employer":"Orbaxter","email":"corinnerobbins@orbaxter.com","city":"Roy","state":"TN"}
{"index":{"_id":"871"}}
{"account_number":871,"balance":35854,"firstname":"Norma","lastname":"Burt","age":32,"gender":"M","address":"934 Cyrus Avenue","employer":"Magnafone","email":"normaburt@magnafone.com","city":"Eden","state":"TN"}
{"index":{"_id":"876"}}
{"account_number":876,"balance":48568,"firstname":"Brady","lastname":"Glover","age":21,"gender":"F","address":"565 Oceanview Avenue","employer":"Comvex","email":"bradyglover@comvex.com","city":"Noblestown","state":"ID"}
{"index":{"_id":"883"}}
{"account_number":883,"balance":33679,"firstname":"Austin","lastname":"Jefferson","age":34,"gender":"M","address":"846 Lincoln Avenue","employer":"Polarax","email":"austinjefferson@polarax.com","city":"Savannah","state":"CT"}
{"index":{"_id":"888"}}
{"account_number":888,"balance":22277,"firstname":"Myrna","lastname":"Herman","age":39,"gender":"F","address":"649 Harwood Place","employer":"Enthaze","email":"myrnaherman@enthaze.com","city":"Idamay","state":"AR"}
{"index":{"_id":"890"}}
{"account_number":890,"balance":31198,"firstname":"Alvarado","lastname":"Pate","age":25,"gender":"M","address":"269 Ashland Place","employer":"Ovolo","email":"alvaradopate@ovolo.com","city":"Volta","state":"MI"}
{"index":{"_id":"895"}}
{"account_number":895,"balance":7327,"firstname":"Lara","lastname":"Mcdaniel","age":36,"gender":"M","address":"854 Willow Place","employer":"Acusage","email":"laramcdaniel@acusage.com","city":"Imperial","state":"NC"}
{"index":{"_id":"903"}}
{"account_number":903,"balance":10238,"firstname":"Wade","lastname":"Page","age":35,"gender":"F","address":"685 Waldorf Court","employer":"Eplosion","email":"wadepage@eplosion.com","city":"Welda","state":"AL"}
{"index":{"_id":"908"}}
{"account_number":908,"balance":45975,"firstname":"Mosley","lastname":"Holloway","age":31,"gender":"M","address":"929 Eldert Lane","employer":"Anivet","email":"mosleyholloway@anivet.com","city":"Biehle","state":"MS"}
{"index":{"_id":"910"}}
{"account_number":910,"balance":36831,"firstname":"Esmeralda","lastname":"James","age":23,"gender":"F","address":"535 High Street","employer":"Terrasys","email":"esmeraldajames@terrasys.com","city":"Dubois","state":"IN"}
{"index":{"_id":"915"}}
{"account_number":915,"balance":19816,"firstname":"Farrell","lastname":"French","age":35,"gender":"F","address":"126 McKibbin Street","employer":"Techmania","email":"farrellfrench@techmania.com","city":"Wescosville","state":"AL"}
{"index":{"_id":"922"}}
{"account_number":922,"balance":39347,"firstname":"Irwin","lastname":"Pugh","age":32,"gender":"M","address":"463 Shale Street","employer":"Idego","email":"irwinpugh@idego.com","city":"Ivanhoe","state":"ID"}
{"index":{"_id":"927"}}
{"account_number":927,"balance":19976,"firstname":"Jeanette","lastname":"Acevedo","age":26,"gender":"M","address":"694 Polhemus Place","employer":"Halap","email":"jeanetteacevedo@halap.com","city":"Harrison","state":"MO"}
{"index":{"_id":"934"}}
{"account_number":934,"balance":43987,"firstname":"Freida","lastname":"Daniels","age":34,"gender":"M","address":"448 Cove Lane","employer":"Vurbo","email":"freidadaniels@vurbo.com","city":"Snelling","state":"NJ"}
{"index":{"_id":"939"}}
{"account_number":939,"balance":31228,"firstname":"Hodges","lastname":"Massey","age":37,"gender":"F","address":"431 Dahl Court","employer":"Kegular","email":"hodgesmassey@kegular.com","city":"Katonah","state":"MD"}
{"index":{"_id":"941"}}
{"account_number":941,"balance":38796,"firstname":"Kim","lastname":"Moss","age":28,"gender":"F","address":"105 Onderdonk Avenue","employer":"Digirang","email":"kimmoss@digirang.com","city":"Centerville","state":"TX"}
{"index":{"_id":"946"}}
{"account_number":946,"balance":42794,"firstname":"Ina","lastname":"Obrien","age":36,"gender":"M","address":"339 Rewe Street","employer":"Eclipsent","email":"inaobrien@eclipsent.com","city":"Soham","state":"RI"}
{"index":{"_id":"953"}}
{"account_number":953,"balance":1110,"firstname":"Baxter","lastname":"Black","age":27,"gender":"M","address":"720 Stillwell Avenue","employer":"Uplinx","email":"baxterblack@uplinx.com","city":"Drummond","state":"MN"}
{"index":{"_id":"958"}}
{"account_number":958,"balance":32849,"firstname":"Brown","lastname":"Wilkins","age":40,"gender":"M","address":"686 Delmonico Place","employer":"Medesign","email":"brownwilkins@medesign.com","city":"Shelby","state":"WY"}
{"index":{"_id":"960"}}
{"account_number":960,"balance":2905,"firstname":"Curry","lastname":"Vargas","age":40,"gender":"M","address":"242 Blake Avenue","employer":"Pearlesex","email":"curryvargas@pearlesex.com","city":"Henrietta","state":"NH"}
{"index":{"_id":"965"}}
{"account_number":965,"balance":21882,"firstname":"Patrica","lastname":"Melton","age":28,"gender":"M","address":"141 Rodney Street","employer":"Flexigen","email":"patricamelton@flexigen.com","city":"Klagetoh","state":"MD"}
{"index":{"_id":"972"}}
{"account_number":972,"balance":24719,"firstname":"Leona","lastname":"Christian","age":26,"gender":"F","address":"900 Woodpoint Road","employer":"Extrawear","email":"leonachristian@extrawear.com","city":"Roderfield","state":"MA"}
{"index":{"_id":"977"}}
{"account_number":977,"balance":6744,"firstname":"Rodgers","lastname":"Mccray","age":21,"gender":"F","address":"612 Duryea Place","employer":"Papricut","email":"rodgersmccray@papricut.com","city":"Marenisco","state":"MD"}
{"index":{"_id":"984"}}
{"account_number":984,"balance":1904,"firstname":"Viola","lastname":"Crawford","age":35,"gender":"F","address":"354 Linwood Street","employer":"Ginkle","email":"violacrawford@ginkle.com","city":"Witmer","state":"AR"}
{"index":{"_id":"989"}}
{"account_number":989,"balance":48622,"firstname":"Franklin","lastname":"Frank","age":38,"gender":"M","address":"270 Carlton Avenue","employer":"Shopabout","email":"franklinfrank@shopabout.com","city":"Guthrie","state":"NC"}
{"index":{"_id":"991"}}
{"account_number":991,"balance":4239,"firstname":"Connie","lastname":"Berry","age":28,"gender":"F","address":"647 Gardner Avenue","employer":"Flumbo","email":"connieberry@flumbo.com","city":"Frierson","state":"MO"}
{"index":{"_id":"996"}}
{"account_number":996,"balance":17541,"firstname":"Andrews","lastname":"Herrera","age":30,"gender":"F","address":"570 Vandam Street","employer":"Klugger","email":"andrewsherrera@klugger.com","city":"Whitehaven","state":"MN"}
{"index":{"_id":"0"}}
{"account_number":0,"balance":16623,"firstname":"Bradshaw","lastname":"Mckenzie","age":29,"gender":"F","address":"244 Columbus Place","employer":"Euron","email":"bradshawmckenzie@euron.com","city":"Hobucken","state":"CO"}
{"index":{"_id":"5"}}
{"account_number":5,"balance":29342,"firstname":"Leola","lastname":"Stewart","age":30,"gender":"F","address":"311 Elm Place","employer":"Diginetic","email":"leolastewart@diginetic.com","city":"Fairview","state":"NJ"}
{"index":{"_id":"12"}}
{"account_number":12,"balance":37055,"firstname":"Stafford","lastname":"Brock","age":20,"gender":"F","address":"296 Wythe Avenue","employer":"Uncorp","email":"staffordbrock@uncorp.com","city":"Bend","state":"AL"}
{"index":{"_id":"17"}}
{"account_number":17,"balance":7831,"firstname":"Bessie","lastname":"Orr","age":31,"gender":"F","address":"239 Hinsdale Street","employer":"Skyplex","email":"bessieorr@skyplex.com","city":"Graball","state":"FL"}
{"index":{"_id":"24"}}
{"account_number":24,"balance":44182,"firstname":"Wood","lastname":"Dale","age":39,"gender":"M","address":"582 Gelston Avenue","employer":"Besto","email":"wooddale@besto.com","city":"Juntura","state":"MI"}
{"index":{"_id":"29"}}
{"account_number":29,"balance":27323,"firstname":"Leah","lastname":"Santiago","age":33,"gender":"M","address":"193 Schenck Avenue","employer":"Isologix","email":"leahsantiago@isologix.com","city":"Gerton","state":"ND"}
{"index":{"_id":"31"}}
{"account_number":31,"balance":30443,"firstname":"Kristen","lastname":"Santana","age":22,"gender":"F","address":"130 Middagh Street","employer":"Dogspa","email":"kristensantana@dogspa.com","city":"Vale","state":"MA"}
{"index":{"_id":"36"}}
{"account_number":36,"balance":15902,"firstname":"Alexandra","lastname":"Nguyen","age":39,"gender":"F","address":"389 Elizabeth Place","employer":"Bittor","email":"alexandranguyen@bittor.com","city":"Hemlock","state":"KY"}
{"index":{"_id":"43"}}
{"account_number":43,"balance":33474,"firstname":"Ryan","lastname":"Howe","age":25,"gender":"M","address":"660 Huntington Street","employer":"Microluxe","email":"ryanhowe@microluxe.com","city":"Clara","state":"CT"}
{"index":{"_id":"48"}}
{"account_number":48,"balance":40608,"firstname":"Peck","lastname":"Downs","age":39,"gender":"F","address":"594 Dwight Street","employer":"Ramjob","email":"peckdowns@ramjob.com","city":"Coloma","state":"WA"}
{"index":{"_id":"50"}}
{"account_number":50,"balance":43695,"firstname":"Sheena","lastname":"Kirkland","age":33,"gender":"M","address":"598 Bank Street","employer":"Zerbina","email":"sheenakirkland@zerbina.com","city":"Walland","state":"IN"}
{"index":{"_id":"55"}}
{"account_number":55,"balance":22020,"firstname":"Shelia","lastname":"Puckett","age":33,"gender":"M","address":"265 Royce Place","employer":"Izzby","email":"sheliapuckett@izzby.com","city":"Slovan","state":"HI"}
{"index":{"_id":"62"}}
{"account_number":62,"balance":43065,"firstname":"Lester","lastname":"Stanton","age":37,"gender":"M","address":"969 Doughty Street","employer":"Geekko","email":"lesterstanton@geekko.com","city":"Itmann","state":"DC"}
{"index":{"_id":"67"}}
{"account_number":67,"balance":39430,"firstname":"Isabelle","lastname":"Spence","age":39,"gender":"M","address":"718 Troy Avenue","employer":"Geeketron","email":"isabellespence@geeketron.com","city":"Camptown","state":"WA"}
{"index":{"_id":"74"}}
{"account_number":74,"balance":47167,"firstname":"Lauri","lastname":"Saunders","age":38,"gender":"F","address":"768 Lynch Street","employer":"Securia","email":"laurisaunders@securia.com","city":"Caroline","state":"TN"}
{"index":{"_id":"79"}}
{"account_number":79,"balance":28185,"firstname":"Booker","lastname":"Lowery","age":29,"gender":"M","address":"817 Campus Road","employer":"Sensate","email":"bookerlowery@sensate.com","city":"Carlos","state":"MT"}
{"index":{"_id":"81"}}
{"account_number":81,"balance":46568,"firstname":"Dennis","lastname":"Gilbert","age":40,"gender":"M","address":"619 Minna Street","employer":"Melbacor","email":"dennisgilbert@melbacor.com","city":"Kersey","state":"ND"}
{"index":{"_id":"86"}}
{"account_number":86,"balance":15428,"firstname":"Walton","lastname":"Butler","age":36,"gender":"M","address":"999 Schenck Street","employer":"Unisure","email":"waltonbutler@unisure.com","city":"Bentonville","state":"IL"}
{"index":{"_id":"93"}}
{"account_number":93,"balance":17728,"firstname":"Jeri","lastname":"Booth","age":31,"gender":"M","address":"322 Roosevelt Court","employer":"Geekology","email":"jeribooth@geekology.com","city":"Leming","state":"ND"}
{"index":{"_id":"98"}}
{"account_number":98,"balance":15085,"firstname":"Cora","lastname":"Barrett","age":24,"gender":"F","address":"555 Neptune Court","employer":"Kiosk","email":"corabarrett@kiosk.com","city":"Independence","state":"MN"}
{"index":{"_id":"101"}}
{"account_number":101,"balance":43400,"firstname":"Cecelia","lastname":"Grimes","age":31,"gender":"M","address":"972 Lincoln Place","employer":"Ecosys","email":"ceceliagrimes@ecosys.com","city":"Manchester","state":"AR"}
{"index":{"_id":"106"}}
{"account_number":106,"balance":8212,"firstname":"Josefina","lastname":"Wagner","age":36,"gender":"M","address":"418 Estate Road","employer":"Kyaguru","email":"josefinawagner@kyaguru.com","city":"Darbydale","state":"FL"}
{"index":{"_id":"113"}}
{"account_number":113,"balance":41652,"firstname":"Burt","lastname":"Moses","age":27,"gender":"M","address":"633 Berry Street","employer":"Uni","email":"burtmoses@uni.com","city":"Russellville","state":"CT"}
{"index":{"_id":"118"}}
{"account_number":118,"balance":2223,"firstname":"Ballard","lastname":"Vasquez","age":33,"gender":"F","address":"101 Bush Street","employer":"Intergeek","email":"ballardvasquez@intergeek.com","city":"Century","state":"MN"}
{"index":{"_id":"120"}}
{"account_number":120,"balance":38565,"firstname":"Browning","lastname":"Rodriquez","age":33,"gender":"M","address":"910 Moore Street","employer":"Opportech","email":"browningrodriquez@opportech.com","city":"Cutter","state":"ND"}
{"index":{"_id":"125"}}
{"account_number":125,"balance":5396,"firstname":"Tanisha","lastname":"Dixon","age":30,"gender":"M","address":"482 Hancock Street","employer":"Junipoor","email":"tanishadixon@junipoor.com","city":"Wauhillau","state":"IA"}
{"index":{"_id":"132"}}
{"account_number":132,"balance":37707,"firstname":"Horton","lastname":"Romero","age":35,"gender":"M","address":"427 Rutherford Place","employer":"Affluex","email":"hortonromero@affluex.com","city":"Hall","state":"AK"}
{"index":{"_id":"137"}}
{"account_number":137,"balance":3596,"firstname":"Frost","lastname":"Freeman","age":29,"gender":"F","address":"191 Dennett Place","employer":"Beadzza","email":"frostfreeman@beadzza.com","city":"Sabillasville","state":"HI"}
{"index":{"_id":"144"}}
{"account_number":144,"balance":43257,"firstname":"Evans","lastname":"Dyer","age":30,"gender":"F","address":"912 Post Court","employer":"Magmina","email":"evansdyer@magmina.com","city":"Gordon","state":"HI"}
{"index":{"_id":"149"}}
{"account_number":149,"balance":22994,"firstname":"Megan","lastname":"Gonzales","age":21,"gender":"M","address":"836 Tampa Court","employer":"Andershun","email":"megangonzales@andershun.com","city":"Rockhill","state":"AL"}
{"index":{"_id":"151"}}
{"account_number":151,"balance":34473,"firstname":"Kent","lastname":"Joyner","age":20,"gender":"F","address":"799 Truxton Street","employer":"Kozgene","email":"kentjoyner@kozgene.com","city":"Allamuchy","state":"DC"}
{"index":{"_id":"156"}}
{"account_number":156,"balance":40185,"firstname":"Sloan","lastname":"Pennington","age":24,"gender":"F","address":"573 Opal Court","employer":"Hopeli","email":"sloanpennington@hopeli.com","city":"Evergreen","state":"CT"}
{"index":{"_id":"163"}}
{"account_number":163,"balance":43075,"firstname":"Wilda","lastname":"Norman","age":33,"gender":"F","address":"173 Beadel Street","employer":"Kog","email":"wildanorman@kog.com","city":"Bodega","state":"ME"}
{"index":{"_id":"168"}}
{"account_number":168,"balance":49568,"firstname":"Carissa","lastname":"Simon","age":20,"gender":"M","address":"975 Flatbush Avenue","employer":"Zillacom","email":"carissasimon@zillacom.com","city":"Neibert","state":"IL"}
{"index":{"_id":"170"}}
{"account_number":170,"balance":6025,"firstname":"Mann","lastname":"Madden","age":36,"gender":"F","address":"161 Radde Place","employer":"Farmex","email":"mannmadden@farmex.com","city":"Thermal","state":"LA"}
{"index":{"_id":"175"}}
{"account_number":175,"balance":16213,"firstname":"Montoya","lastname":"Donaldson","age":28,"gender":"F","address":"481 Morton Street","employer":"Envire","email":"montoyadonaldson@envire.com","city":"Delco","state":"MA"}
{"index":{"_id":"182"}}
{"account_number":182,"balance":7803,"firstname":"Manuela","lastname":"Dillon","age":21,"gender":"M","address":"742 Garnet Street","employer":"Moreganic","email":"manueladillon@moreganic.com","city":"Ilchester","state":"TX"}
{"index":{"_id":"187"}}
{"account_number":187,"balance":26581,"firstname":"Autumn","lastname":"Hodges","age":35,"gender":"M","address":"757 Granite Street","employer":"Ezentia","email":"autumnhodges@ezentia.com","city":"Martinsville","state":"KY"}
{"index":{"_id":"194"}}
{"account_number":194,"balance":16311,"firstname":"Beck","lastname":"Rosario","age":39,"gender":"M","address":"721 Cambridge Place","employer":"Zoid","email":"beckrosario@zoid.com","city":"Efland","state":"ID"}
{"index":{"_id":"199"}}
{"account_number":199,"balance":18086,"firstname":"Branch","lastname":"Love","age":26,"gender":"M","address":"458 Commercial Street","employer":"Frolix","email":"branchlove@frolix.com","city":"Caspar","state":"NC"}
{"index":{"_id":"202"}}
{"account_number":202,"balance":26466,"firstname":"Medina","lastname":"Brown","age":31,"gender":"F","address":"519 Sunnyside Court","employer":"Bleendot","email":"medinabrown@bleendot.com","city":"Winfred","state":"MI"}
{"index":{"_id":"207"}}
{"account_number":207,"balance":45535,"firstname":"Evelyn","lastname":"Lara","age":35,"gender":"F","address":"636 Chestnut Street","employer":"Ultrasure","email":"evelynlara@ultrasure.com","city":"Logan","state":"MI"}
{"index":{"_id":"214"}}
{"account_number":214,"balance":24418,"firstname":"Luann","lastname":"Faulkner","age":37,"gender":"F","address":"697 Hazel Court","employer":"Zolar","email":"luannfaulkner@zolar.com","city":"Ticonderoga","state":"TX"}
{"index":{"_id":"219"}}
{"account_number":219,"balance":17127,"firstname":"Edwards","lastname":"Hurley","age":25,"gender":"M","address":"834 Stockholm Street","employer":"Austech","email":"edwardshurley@austech.com","city":"Bayview","state":"NV"}
{"index":{"_id":"221"}}
{"account_number":221,"balance":15803,"firstname":"Benjamin","lastname":"Barrera","age":34,"gender":"M","address":"568 Main Street","employer":"Zaphire","email":"benjaminbarrera@zaphire.com","city":"Germanton","state":"WY"}
{"index":{"_id":"226"}}
{"account_number":226,"balance":37720,"firstname":"Wilkins","lastname":"Brady","age":40,"gender":"F","address":"486 Baltic Street","employer":"Dogtown","email":"wilkinsbrady@dogtown.com","city":"Condon","state":"MT"}
{"index":{"_id":"233"}}
{"account_number":233,"balance":23020,"firstname":"Washington","lastname":"Walsh","age":27,"gender":"M","address":"366 Church Avenue","employer":"Candecor","email":"washingtonwalsh@candecor.com","city":"Westphalia","state":"MA"}
{"index":{"_id":"238"}}
{"account_number":238,"balance":21287,"firstname":"Constance","lastname":"Wong","age":28,"gender":"M","address":"496 Brown Street","employer":"Grainspot","email":"constancewong@grainspot.com","city":"Cecilia","state":"IN"}
{"index":{"_id":"240"}}
{"account_number":240,"balance":49741,"firstname":"Oconnor","lastname":"Clay","age":35,"gender":"F","address":"659 Highland Boulevard","employer":"Franscene","email":"oconnorclay@franscene.com","city":"Kilbourne","state":"NH"}
{"index":{"_id":"245"}}
{"account_number":245,"balance":22026,"firstname":"Fran","lastname":"Bolton","age":28,"gender":"F","address":"147 Jerome Street","employer":"Solaren","email":"franbolton@solaren.com","city":"Nash","state":"RI"}
{"index":{"_id":"252"}}
{"account_number":252,"balance":18831,"firstname":"Elvia","lastname":"Poole","age":22,"gender":"F","address":"836 Delevan Street","employer":"Velity","email":"elviapoole@velity.com","city":"Groveville","state":"MI"}
{"index":{"_id":"257"}}
{"account_number":257,"balance":5318,"firstname":"Olive","lastname":"Oneil","age":35,"gender":"F","address":"457 Decatur Street","employer":"Helixo","email":"oliveoneil@helixo.com","city":"Chicopee","state":"MI"}
{"index":{"_id":"264"}}
{"account_number":264,"balance":22084,"firstname":"Samantha","lastname":"Ferrell","age":35,"gender":"F","address":"488 Fulton Street","employer":"Flum","email":"samanthaferrell@flum.com","city":"Brandywine","state":"MT"}
{"index":{"_id":"269"}}
{"account_number":269,"balance":43317,"firstname":"Crosby","lastname":"Figueroa","age":34,"gender":"M","address":"910 Aurelia Court","employer":"Pyramia","email":"crosbyfigueroa@pyramia.com","city":"Leyner","state":"OH"}
{"index":{"_id":"271"}}
{"account_number":271,"balance":11864,"firstname":"Holt","lastname":"Walter","age":30,"gender":"F","address":"645 Poplar Avenue","employer":"Grupoli","email":"holtwalter@grupoli.com","city":"Mansfield","state":"OR"}
{"index":{"_id":"276"}}
{"account_number":276,"balance":11606,"firstname":"Pittman","lastname":"Mathis","age":23,"gender":"F","address":"567 Charles Place","employer":"Zuvy","email":"pittmanmathis@zuvy.com","city":"Roeville","state":"KY"}
{"index":{"_id":"283"}}
{"account_number":283,"balance":24070,"firstname":"Fuentes","lastname":"Foley","age":30,"gender":"M","address":"729 Walker Court","employer":"Knowlysis","email":"fuentesfoley@knowlysis.com","city":"Tryon","state":"TN"}
{"index":{"_id":"288"}}
{"account_number":288,"balance":27243,"firstname":"Wong","lastname":"Stone","age":39,"gender":"F","address":"440 Willoughby Street","employer":"Zentix","email":"wongstone@zentix.com","city":"Wheatfields","state":"DC"}
{"index":{"_id":"290"}}
{"account_number":290,"balance":26103,"firstname":"Neva","lastname":"Burgess","age":37,"gender":"F","address":"985 Wyona Street","employer":"Slofast","email":"nevaburgess@slofast.com","city":"Cawood","state":"DC"}
{"index":{"_id":"295"}}
{"account_number":295,"balance":37358,"firstname":"Howe","lastname":"Nash","age":20,"gender":"M","address":"833 Union Avenue","employer":"Aquacine","email":"howenash@aquacine.com","city":"Indio","state":"MN"}
{"index":{"_id":"303"}}
{"account_number":303,"balance":21976,"firstname":"Huffman","lastname":"Green","age":24,"gender":"F","address":"455 Colby Court","employer":"Comtest","email":"huffmangreen@comtest.com","city":"Weeksville","state":"UT"}
{"index":{"_id":"308"}}
{"account_number":308,"balance":33989,"firstname":"Glass","lastname":"Schroeder","age":25,"gender":"F","address":"670 Veterans Avenue","employer":"Realmo","email":"glassschroeder@realmo.com","city":"Gratton","state":"NY"}
{"index":{"_id":"310"}}
{"account_number":310,"balance":23049,"firstname":"Shannon","lastname":"Morton","age":39,"gender":"F","address":"412 Pleasant Place","employer":"Ovation","email":"shannonmorton@ovation.com","city":"Edgar","state":"AZ"}
{"index":{"_id":"315"}}
{"account_number":315,"balance":1314,"firstname":"Clare","lastname":"Morrow","age":33,"gender":"F","address":"728 Madeline Court","employer":"Gaptec","email":"claremorrow@gaptec.com","city":"Mapletown","state":"PA"}
{"index":{"_id":"322"}}
{"account_number":322,"balance":6303,"firstname":"Gilliam","lastname":"Horne","age":27,"gender":"M","address":"414 Florence Avenue","employer":"Shepard","email":"gilliamhorne@shepard.com","city":"Winesburg","state":"WY"}
{"index":{"_id":"327"}}
{"account_number":327,"balance":29294,"firstname":"Nell","lastname":"Contreras","age":27,"gender":"M","address":"694 Gold Street","employer":"Momentia","email":"nellcontreras@momentia.com","city":"Cumminsville","state":"AL"}
{"index":{"_id":"334"}}
{"account_number":334,"balance":9178,"firstname":"Cross","lastname":"Floyd","age":21,"gender":"F","address":"815 Herkimer Court","employer":"Maroptic","email":"crossfloyd@maroptic.com","city":"Kraemer","state":"AK"}
{"index":{"_id":"339"}}
{"account_number":339,"balance":3992,"firstname":"Franco","lastname":"Welch","age":38,"gender":"F","address":"776 Brightwater Court","employer":"Earthplex","email":"francowelch@earthplex.com","city":"Naomi","state":"ME"}
{"index":{"_id":"341"}}
{"account_number":341,"balance":44367,"firstname":"Alberta","lastname":"Bradford","age":30,"gender":"F","address":"670 Grant Avenue","employer":"Bugsall","email":"albertabradford@bugsall.com","city":"Romeville","state":"MT"}
{"index":{"_id":"346"}}
{"account_number":346,"balance":26594,"firstname":"Shelby","lastname":"Sanchez","age":36,"gender":"F","address":"257 Fillmore Avenue","employer":"Geekus","email":"shelbysanchez@geekus.com","city":"Seymour","state":"CO"}
{"index":{"_id":"353"}}
{"account_number":353,"balance":45182,"firstname":"Rivera","lastname":"Sherman","age":37,"gender":"M","address":"603 Garden Place","employer":"Bovis","email":"riverasherman@bovis.com","city":"Otranto","state":"CA"}
{"index":{"_id":"358"}}
{"account_number":358,"balance":44043,"firstname":"Hale","lastname":"Baldwin","age":40,"gender":"F","address":"845 Menahan Street","employer":"Kidgrease","email":"halebaldwin@kidgrease.com","city":"Day","state":"AK"}
{"index":{"_id":"360"}}
{"account_number":360,"balance":26651,"firstname":"Ward","lastname":"Hicks","age":34,"gender":"F","address":"592 Brighton Court","employer":"Biotica","email":"wardhicks@biotica.com","city":"Kanauga","state":"VT"}
{"index":{"_id":"365"}}
{"account_number":365,"balance":3176,"firstname":"Sanders","lastname":"Holder","age":31,"gender":"F","address":"453 Cypress Court","employer":"Geekola","email":"sandersholder@geekola.com","city":"Staples","state":"TN"}
{"index":{"_id":"372"}}
{"account_number":372,"balance":28566,"firstname":"Alba","lastname":"Forbes","age":24,"gender":"M","address":"814 Meserole Avenue","employer":"Isostream","email":"albaforbes@isostream.com","city":"Clarence","state":"OR"}
{"index":{"_id":"377"}}
{"account_number":377,"balance":5374,"firstname":"Margo","lastname":"Gay","age":34,"gender":"F","address":"613 Chase Court","employer":"Rotodyne","email":"margogay@rotodyne.com","city":"Waumandee","state":"KS"}
{"index":{"_id":"384"}}
{"account_number":384,"balance":48758,"firstname":"Sallie","lastname":"Houston","age":31,"gender":"F","address":"836 Polar Street","employer":"Squish","email":"salliehouston@squish.com","city":"Morningside","state":"NC"}
{"index":{"_id":"389"}}
{"account_number":389,"balance":8839,"firstname":"York","lastname":"Cummings","age":27,"gender":"M","address":"778 Centre Street","employer":"Insurity","email":"yorkcummings@insurity.com","city":"Freeburn","state":"RI"}
{"index":{"_id":"391"}}
{"account_number":391,"balance":14733,"firstname":"Holman","lastname":"Jordan","age":30,"gender":"M","address":"391 Forrest Street","employer":"Maineland","email":"holmanjordan@maineland.com","city":"Cade","state":"CT"}
{"index":{"_id":"396"}}
{"account_number":396,"balance":14613,"firstname":"Marsha","lastname":"Elliott","age":38,"gender":"F","address":"297 Liberty Avenue","employer":"Orbiflex","email":"marshaelliott@orbiflex.com","city":"Windsor","state":"TX"}
{"index":{"_id":"404"}}
{"account_number":404,"balance":34978,"firstname":"Massey","lastname":"Becker","age":26,"gender":"F","address":"930 Pitkin Avenue","employer":"Genekom","email":"masseybecker@genekom.com","city":"Blairstown","state":"OR"}
{"index":{"_id":"409"}}
{"account_number":409,"balance":36960,"firstname":"Maura","lastname":"Glenn","age":31,"gender":"M","address":"183 Poly Place","employer":"Viagreat","email":"mauraglenn@viagreat.com","city":"Foscoe","state":"DE"}
{"index":{"_id":"411"}}
{"account_number":411,"balance":1172,"firstname":"Guzman","lastname":"Whitfield","age":22,"gender":"M","address":"181 Perry Terrace","employer":"Springbee","email":"guzmanwhitfield@springbee.com","city":"Balm","state":"IN"}
{"index":{"_id":"416"}}
{"account_number":416,"balance":27169,"firstname":"Hunt","lastname":"Schwartz","age":28,"gender":"F","address":"461 Havens Place","employer":"Danja","email":"huntschwartz@danja.com","city":"Grenelefe","state":"NV"}
{"index":{"_id":"423"}}
{"account_number":423,"balance":38852,"firstname":"Hines","lastname":"Underwood","age":21,"gender":"F","address":"284 Louise Terrace","employer":"Namegen","email":"hinesunderwood@namegen.com","city":"Downsville","state":"CO"}
{"index":{"_id":"428"}}
{"account_number":428,"balance":13925,"firstname":"Stephens","lastname":"Cain","age":20,"gender":"F","address":"189 Summit Street","employer":"Rocklogic","email":"stephenscain@rocklogic.com","city":"Bourg","state":"HI"}
{"index":{"_id":"430"}}
{"account_number":430,"balance":15251,"firstname":"Alejandra","lastname":"Chavez","age":34,"gender":"M","address":"651 Butler Place","employer":"Gology","email":"alejandrachavez@gology.com","city":"Allensworth","state":"VT"}
{"index":{"_id":"435"}}
{"account_number":435,"balance":14654,"firstname":"Sue","lastname":"Lopez","age":22,"gender":"F","address":"632 Stone Avenue","employer":"Emergent","email":"suelopez@emergent.com","city":"Waterford","state":"TN"}
{"index":{"_id":"442"}}
{"account_number":442,"balance":36211,"firstname":"Lawanda","lastname":"Leon","age":27,"gender":"F","address":"126 Canal Avenue","employer":"Xixan","email":"lawandaleon@xixan.com","city":"Berwind","state":"TN"}
{"index":{"_id":"447"}}
{"account_number":447,"balance":11402,"firstname":"Lucia","lastname":"Livingston","age":35,"gender":"M","address":"773 Lake Avenue","employer":"Soprano","email":"lucialivingston@soprano.com","city":"Edgewater","state":"TN"}
{"index":{"_id":"454"}}
{"account_number":454,"balance":31687,"firstname":"Alicia","lastname":"Rollins","age":22,"gender":"F","address":"483 Verona Place","employer":"Boilcat","email":"aliciarollins@boilcat.com","city":"Lutsen","state":"MD"}
{"index":{"_id":"459"}}
{"account_number":459,"balance":18869,"firstname":"Pamela","lastname":"Henry","age":20,"gender":"F","address":"361 Locust Avenue","employer":"Imageflow","email":"pamelahenry@imageflow.com","city":"Greenfields","state":"OH"}
{"index":{"_id":"461"}}
{"account_number":461,"balance":38807,"firstname":"Mcbride","lastname":"Padilla","age":34,"gender":"F","address":"550 Borinquen Pl","employer":"Zepitope","email":"mcbridepadilla@zepitope.com","city":"Emory","state":"AZ"}
{"index":{"_id":"466"}}
{"account_number":466,"balance":25109,"firstname":"Marcie","lastname":"Mcmillan","age":30,"gender":"F","address":"947 Gain Court","employer":"Entroflex","email":"marciemcmillan@entroflex.com","city":"Ronco","state":"ND"}
{"index":{"_id":"473"}}
{"account_number":473,"balance":5391,"firstname":"Susan","lastname":"Luna","age":25,"gender":"F","address":"521 Bogart Street","employer":"Zaya","email":"susanluna@zaya.com","city":"Grazierville","state":"MI"}
{"index":{"_id":"478"}}
{"account_number":478,"balance":28044,"firstname":"Dana","lastname":"Decker","age":35,"gender":"M","address":"627 Dobbin Street","employer":"Acrodance","email":"danadecker@acrodance.com","city":"Sharon","state":"MN"}
{"index":{"_id":"480"}}
{"account_number":480,"balance":40807,"firstname":"Anastasia","lastname":"Parker","age":24,"gender":"M","address":"650 Folsom Place","employer":"Zilladyne","email":"anastasiaparker@zilladyne.com","city":"Oberlin","state":"WY"}
{"index":{"_id":"485"}}
{"account_number":485,"balance":44235,"firstname":"Albert","lastname":"Roberts","age":40,"gender":"M","address":"385 Harman Street","employer":"Stralum","email":"albertroberts@stralum.com","city":"Watrous","state":"NM"}
{"index":{"_id":"492"}}
{"account_number":492,"balance":31055,"firstname":"Burnett","lastname":"Briggs","age":35,"gender":"M","address":"987 Cass Place","employer":"Pharmex","email":"burnettbriggs@pharmex.com","city":"Cornfields","state":"TX"}
{"index":{"_id":"497"}}
{"account_number":497,"balance":13493,"firstname":"Doyle","lastname":"Jenkins","age":30,"gender":"M","address":"205 Nevins Street","employer":"Unia","email":"doylejenkins@unia.com","city":"Nicut","state":"DC"}
{"index":{"_id":"500"}}
{"account_number":500,"balance":39143,"firstname":"Pope","lastname":"Keith","age":28,"gender":"F","address":"537 Fane Court","employer":"Zboo","email":"popekeith@zboo.com","city":"Courtland","state":"AL"}
{"index":{"_id":"505"}}
{"account_number":505,"balance":45493,"firstname":"Shelley","lastname":"Webb","age":29,"gender":"M","address":"873 Crawford Avenue","employer":"Quadeebo","email":"shelleywebb@quadeebo.com","city":"Topanga","state":"IL"}
{"index":{"_id":"512"}}
{"account_number":512,"balance":47432,"firstname":"Alisha","lastname":"Morales","age":29,"gender":"M","address":"623 Batchelder Street","employer":"Terragen","email":"alishamorales@terragen.com","city":"Gilmore","state":"VA"}
{"index":{"_id":"517"}}
{"account_number":517,"balance":3022,"firstname":"Allyson","lastname":"Walls","age":38,"gender":"F","address":"334 Coffey Street","employer":"Gorganic","email":"allysonwalls@gorganic.com","city":"Dahlen","state":"GA"}
{"index":{"_id":"524"}}
{"account_number":524,"balance":49334,"firstname":"Salas","lastname":"Farley","age":30,"gender":"F","address":"499 Trucklemans Lane","employer":"Xumonk","email":"salasfarley@xumonk.com","city":"Noxen","state":"AL"}
{"index":{"_id":"529"}}
{"account_number":529,"balance":21788,"firstname":"Deann","lastname":"Fisher","age":23,"gender":"F","address":"511 Buffalo Avenue","employer":"Twiist","email":"deannfisher@twiist.com","city":"Templeton","state":"WA"}
{"index":{"_id":"531"}}
{"account_number":531,"balance":39770,"firstname":"Janet","lastname":"Pena","age":38,"gender":"M","address":"645 Livonia Avenue","employer":"Corecom","email":"janetpena@corecom.com","city":"Garberville","state":"OK"}
{"index":{"_id":"536"}}
{"account_number":536,"balance":6255,"firstname":"Emma","lastname":"Adkins","age":33,"gender":"F","address":"971 Calder Place","employer":"Ontagene","email":"emmaadkins@ontagene.com","city":"Ruckersville","state":"GA"}
{"index":{"_id":"543"}}
{"account_number":543,"balance":48022,"firstname":"Marina","lastname":"Rasmussen","age":31,"gender":"M","address":"446 Love Lane","employer":"Crustatia","email":"marinarasmussen@crustatia.com","city":"Statenville","state":"MD"}
{"index":{"_id":"548"}}
{"account_number":548,"balance":36930,"firstname":"Sandra","lastname":"Andrews","age":37,"gender":"M","address":"973 Prospect Street","employer":"Datagene","email":"sandraandrews@datagene.com","city":"Inkerman","state":"MO"}
{"index":{"_id":"550"}}
{"account_number":550,"balance":32238,"firstname":"Walsh","lastname":"Goodwin","age":22,"gender":"M","address":"953 Canda Avenue","employer":"Proflex","email":"walshgoodwin@proflex.com","city":"Ypsilanti","state":"MT"}
{"index":{"_id":"555"}}
{"account_number":555,"balance":10750,"firstname":"Fannie","lastname":"Slater","age":31,"gender":"M","address":"457 Tech Place","employer":"Kineticut","email":"fannieslater@kineticut.com","city":"Basye","state":"MO"}
{"index":{"_id":"562"}}
{"account_number":562,"balance":10737,"firstname":"Sarah","lastname":"Strong","age":39,"gender":"F","address":"177 Pioneer Street","employer":"Megall","email":"sarahstrong@megall.com","city":"Ladera","state":"WY"}
{"index":{"_id":"567"}}
{"account_number":567,"balance":6507,"firstname":"Diana","lastname":"Dominguez","age":40,"gender":"M","address":"419 Albany Avenue","employer":"Ohmnet","email":"dianadominguez@ohmnet.com","city":"Wildwood","state":"TX"}
{"index":{"_id":"574"}}
{"account_number":574,"balance":32954,"firstname":"Andrea","lastname":"Mosley","age":24,"gender":"M","address":"368 Throop Avenue","employer":"Musix","email":"andreamosley@musix.com","city":"Blende","state":"DC"}
{"index":{"_id":"579"}}
{"account_number":579,"balance":12044,"firstname":"Banks","lastname":"Sawyer","age":36,"gender":"M","address":"652 Doone Court","employer":"Rooforia","email":"bankssawyer@rooforia.com","city":"Foxworth","state":"ND"}
{"index":{"_id":"581"}}
{"account_number":581,"balance":16525,"firstname":"Fuller","lastname":"Mcintyre","age":32,"gender":"M","address":"169 Bergen Place","employer":"Applideck","email":"fullermcintyre@applideck.com","city":"Kenvil","state":"NY"}
{"index":{"_id":"586"}}
{"account_number":586,"balance":13644,"firstname":"Love","lastname":"Velasquez","age":26,"gender":"F","address":"290 Girard Street","employer":"Zomboid","email":"lovevelasquez@zomboid.com","city":"Villarreal","state":"SD"}
{"index":{"_id":"593"}}
{"account_number":593,"balance":41230,"firstname":"Muriel","lastname":"Vazquez","age":37,"gender":"M","address":"395 Montgomery Street","employer":"Sustenza","email":"murielvazquez@sustenza.com","city":"Strykersville","state":"OK"}
{"index":{"_id":"598"}}
{"account_number":598,"balance":33251,"firstname":"Morgan","lastname":"Coleman","age":33,"gender":"M","address":"324 McClancy Place","employer":"Aclima","email":"morgancoleman@aclima.com","city":"Bowden","state":"WA"}
{"index":{"_id":"601"}}
{"account_number":601,"balance":20796,"firstname":"Vickie","lastname":"Valentine","age":34,"gender":"F","address":"432 Bassett Avenue","employer":"Comvene","email":"vickievalentine@comvene.com","city":"Teasdale","state":"UT"}
{"index":{"_id":"606"}}
{"account_number":606,"balance":28770,"firstname":"Michael","lastname":"Bray","age":31,"gender":"M","address":"935 Lake Place","employer":"Telepark","email":"michaelbray@telepark.com","city":"Lemoyne","state":"CT"}
{"index":{"_id":"613"}}
{"account_number":613,"balance":39340,"firstname":"Eddie","lastname":"Mccarty","age":34,"gender":"F","address":"971 Richards Street","employer":"Bisba","email":"eddiemccarty@bisba.com","city":"Fruitdale","state":"NY"}
{"index":{"_id":"618"}}
{"account_number":618,"balance":8976,"firstname":"Cheri","lastname":"Ford","age":30,"gender":"F","address":"803 Ridgewood Avenue","employer":"Zorromop","email":"cheriford@zorromop.com","city":"Gambrills","state":"VT"}
{"index":{"_id":"620"}}
{"account_number":620,"balance":7224,"firstname":"Coleen","lastname":"Bartlett","age":38,"gender":"M","address":"761 Carroll Street","employer":"Idealis","email":"coleenbartlett@idealis.com","city":"Mathews","state":"DE"}
{"index":{"_id":"625"}}
{"account_number":625,"balance":46010,"firstname":"Cynthia","lastname":"Johnston","age":23,"gender":"M","address":"142 Box Street","employer":"Zentry","email":"cynthiajohnston@zentry.com","city":"Makena","state":"MA"}
{"index":{"_id":"632"}}
{"account_number":632,"balance":40470,"firstname":"Kay","lastname":"Warren","age":20,"gender":"F","address":"422 Alabama Avenue","employer":"Realysis","email":"kaywarren@realysis.com","city":"Homestead","state":"HI"}
{"index":{"_id":"637"}}
{"account_number":637,"balance":3169,"firstname":"Kathy","lastname":"Carter","age":27,"gender":"F","address":"410 Jamison Lane","employer":"Limage","email":"kathycarter@limage.com","city":"Ernstville","state":"WA"}
{"index":{"_id":"644"}}
{"account_number":644,"balance":44021,"firstname":"Etta","lastname":"Miller","age":21,"gender":"F","address":"376 Lawton Street","employer":"Bluegrain","email":"ettamiller@bluegrain.com","city":"Baker","state":"MD"}
{"index":{"_id":"649"}}
{"account_number":649,"balance":20275,"firstname":"Jeanine","lastname":"Malone","age":26,"gender":"F","address":"114 Dodworth Street","employer":"Nixelt","email":"jeaninemalone@nixelt.com","city":"Keyport","state":"AK"}
{"index":{"_id":"651"}}
{"account_number":651,"balance":18360,"firstname":"Young","lastname":"Reeves","age":34,"gender":"M","address":"581 Plaza Street","employer":"Krog","email":"youngreeves@krog.com","city":"Sussex","state":"WY"}
{"index":{"_id":"656"}}
{"account_number":656,"balance":21632,"firstname":"Olson","lastname":"Hunt","age":36,"gender":"M","address":"342 Jaffray Street","employer":"Volax","email":"olsonhunt@volax.com","city":"Bangor","state":"WA"}
{"index":{"_id":"663"}}
{"account_number":663,"balance":2456,"firstname":"Rollins","lastname":"Richards","age":37,"gender":"M","address":"129 Sullivan Place","employer":"Geostele","email":"rollinsrichards@geostele.com","city":"Morgandale","state":"FL"}
{"index":{"_id":"668"}}
{"account_number":668,"balance":45069,"firstname":"Potter","lastname":"Michael","age":27,"gender":"M","address":"803 Glenmore Avenue","employer":"Ontality","email":"pottermichael@ontality.com","city":"Newkirk","state":"KS"}
{"index":{"_id":"670"}}
{"account_number":670,"balance":10178,"firstname":"Ollie","lastname":"Riley","age":22,"gender":"M","address":"252 Jackson Place","employer":"Adornica","email":"ollieriley@adornica.com","city":"Brethren","state":"WI"}
{"index":{"_id":"675"}}
{"account_number":675,"balance":36102,"firstname":"Fisher","lastname":"Shepard","age":27,"gender":"F","address":"859 Varick Street","employer":"Qot","email":"fishershepard@qot.com","city":"Diaperville","state":"MD"}
{"index":{"_id":"682"}}
{"account_number":682,"balance":14168,"firstname":"Anne","lastname":"Hale","age":22,"gender":"F","address":"708 Anthony Street","employer":"Cytrek","email":"annehale@cytrek.com","city":"Beechmont","state":"WV"}
{"index":{"_id":"687"}}
{"account_number":687,"balance":48630,"firstname":"Caroline","lastname":"Cox","age":31,"gender":"M","address":"626 Hillel Place","employer":"Opticon","email":"carolinecox@opticon.com","city":"Loma","state":"ND"}
{"index":{"_id":"694"}}
{"account_number":694,"balance":33125,"firstname":"Craig","lastname":"Palmer","age":31,"gender":"F","address":"273 Montrose Avenue","employer":"Comvey","email":"craigpalmer@comvey.com","city":"Cleary","state":"OK"}
{"index":{"_id":"699"}}
{"account_number":699,"balance":4156,"firstname":"Gallagher","lastname":"Marshall","age":37,"gender":"F","address":"648 Clifford Place","employer":"Exiand","email":"gallaghermarshall@exiand.com","city":"Belfair","state":"KY"}
{"index":{"_id":"702"}}
{"account_number":702,"balance":46490,"firstname":"Meadows","lastname":"Delgado","age":26,"gender":"M","address":"612 Jardine Place","employer":"Daisu","email":"meadowsdelgado@daisu.com","city":"Venice","state":"AR"}
{"index":{"_id":"707"}}
{"account_number":707,"balance":30325,"firstname":"Sonya","lastname":"Trevino","age":30,"gender":"F","address":"181 Irving Place","employer":"Atgen","email":"sonyatrevino@atgen.com","city":"Enetai","state":"TN"}
{"index":{"_id":"714"}}
{"account_number":714,"balance":16602,"firstname":"Socorro","lastname":"Murray","age":34,"gender":"F","address":"810 Manhattan Court","employer":"Isoswitch","email":"socorromurray@isoswitch.com","city":"Jugtown","state":"AZ"}
{"index":{"_id":"719"}}
{"account_number":719,"balance":33107,"firstname":"Leanna","lastname":"Reed","age":25,"gender":"F","address":"528 Krier Place","employer":"Rodeology","email":"leannareed@rodeology.com","city":"Carrizo","state":"WI"}
{"index":{"_id":"721"}}
{"account_number":721,"balance":32958,"firstname":"Mara","lastname":"Dickson","age":26,"gender":"M","address":"810 Harrison Avenue","employer":"Comtours","email":"maradickson@comtours.com","city":"Thynedale","state":"DE"}
{"index":{"_id":"726"}}
{"account_number":726,"balance":44737,"firstname":"Rosemary","lastname":"Salazar","age":21,"gender":"M","address":"290 Croton Loop","employer":"Rockabye","email":"rosemarysalazar@rockabye.com","city":"Helen","state":"IA"}
{"index":{"_id":"733"}}
{"account_number":733,"balance":15722,"firstname":"Lakeisha","lastname":"Mccarthy","age":37,"gender":"M","address":"782 Turnbull Avenue","employer":"Exosis","email":"lakeishamccarthy@exosis.com","city":"Caberfae","state":"NM"}
{"index":{"_id":"738"}}
{"account_number":738,"balance":44936,"firstname":"Rosalind","lastname":"Hunter","age":32,"gender":"M","address":"644 Eaton Court","employer":"Zolarity","email":"rosalindhunter@zolarity.com","city":"Cataract","state":"SD"}
{"index":{"_id":"740"}}
{"account_number":740,"balance":6143,"firstname":"Chambers","lastname":"Hahn","age":22,"gender":"M","address":"937 Windsor Place","employer":"Medalert","email":"chambershahn@medalert.com","city":"Dorneyville","state":"DC"}
{"index":{"_id":"745"}}
{"account_number":745,"balance":4572,"firstname":"Jacobs","lastname":"Sweeney","age":32,"gender":"M","address":"189 Lott Place","employer":"Comtent","email":"jacobssweeney@comtent.com","city":"Advance","state":"NJ"}
{"index":{"_id":"752"}}
{"account_number":752,"balance":14039,"firstname":"Jerry","lastname":"Rush","age":31,"gender":"M","address":"632 Dank Court","employer":"Ebidco","email":"jerryrush@ebidco.com","city":"Geyserville","state":"AR"}
{"index":{"_id":"757"}}
{"account_number":757,"balance":34628,"firstname":"Mccullough","lastname":"Moore","age":30,"gender":"F","address":"304 Hastings Street","employer":"Nikuda","email":"mcculloughmoore@nikuda.com","city":"Charco","state":"DC"}
{"index":{"_id":"764"}}
{"account_number":764,"balance":3728,"firstname":"Noemi","lastname":"Gill","age":30,"gender":"M","address":"427 Chester Street","employer":"Avit","email":"noemigill@avit.com","city":"Chesterfield","state":"AL"}
{"index":{"_id":"769"}}
{"account_number":769,"balance":15362,"firstname":"Francis","lastname":"Beck","age":28,"gender":"M","address":"454 Livingston Street","employer":"Furnafix","email":"francisbeck@furnafix.com","city":"Dunnavant","state":"HI"}
{"index":{"_id":"771"}}
{"account_number":771,"balance":32784,"firstname":"Jocelyn","lastname":"Boone","age":23,"gender":"M","address":"513 Division Avenue","employer":"Collaire","email":"jocelynboone@collaire.com","city":"Lisco","state":"VT"}
{"index":{"_id":"776"}}
{"account_number":776,"balance":29177,"firstname":"Duke","lastname":"Atkinson","age":24,"gender":"M","address":"520 Doscher Street","employer":"Tripsch","email":"dukeatkinson@tripsch.com","city":"Lafferty","state":"NC"}
{"index":{"_id":"783"}}
{"account_number":783,"balance":11911,"firstname":"Faith","lastname":"Cooper","age":25,"gender":"F","address":"539 Rapelye Street","employer":"Insuron","email":"faithcooper@insuron.com","city":"Jennings","state":"MN"}
{"index":{"_id":"788"}}
{"account_number":788,"balance":12473,"firstname":"Marianne","lastname":"Aguilar","age":39,"gender":"F","address":"213 Holly Street","employer":"Marqet","email":"marianneaguilar@marqet.com","city":"Alfarata","state":"HI"}
{"index":{"_id":"790"}}
{"account_number":790,"balance":29912,"firstname":"Ellis","lastname":"Sullivan","age":39,"gender":"F","address":"877 Coyle Street","employer":"Enersave","email":"ellissullivan@enersave.com","city":"Canby","state":"MS"}
{"index":{"_id":"795"}}
{"account_number":795,"balance":31450,"firstname":"Bruce","lastname":"Avila","age":34,"gender":"M","address":"865 Newkirk Placez","employer":"Plasmosis","email":"bruceavila@plasmosis.com","city":"Ada","state":"ID"}
{"index":{"_id":"803"}}
{"account_number":803,"balance":49567,"firstname":"Marissa","lastname":"Spears","age":25,"gender":"M","address":"963 Highland Avenue","employer":"Centregy","email":"marissaspears@centregy.com","city":"Bloomington","state":"MS"}
{"index":{"_id":"808"}}
{"account_number":808,"balance":11251,"firstname":"Nola","lastname":"Quinn","age":20,"gender":"M","address":"863 Wythe Place","employer":"Iplax","email":"nolaquinn@iplax.com","city":"Cuylerville","state":"NH"}
{"index":{"_id":"810"}}
{"account_number":810,"balance":10563,"firstname":"Alyssa","lastname":"Ortega","age":40,"gender":"M","address":"977 Clymer Street","employer":"Eventage","email":"alyssaortega@eventage.com","city":"Convent","state":"SC"}
{"index":{"_id":"815"}}
{"account_number":815,"balance":19336,"firstname":"Guthrie","lastname":"Morse","age":30,"gender":"M","address":"685 Vandalia Avenue","employer":"Gronk","email":"guthriemorse@gronk.com","city":"Fowlerville","state":"OR"}
{"index":{"_id":"822"}}
{"account_number":822,"balance":13024,"firstname":"Hicks","lastname":"Farrell","age":25,"gender":"M","address":"468 Middleton Street","employer":"Zolarex","email":"hicksfarrell@zolarex.com","city":"Columbus","state":"OR"}
{"index":{"_id":"827"}}
{"account_number":827,"balance":37536,"firstname":"Naomi","lastname":"Ball","age":29,"gender":"F","address":"319 Stewart Street","employer":"Isotronic","email":"naomiball@isotronic.com","city":"Trona","state":"NM"}
{"index":{"_id":"834"}}
{"account_number":834,"balance":38049,"firstname":"Sybil","lastname":"Carrillo","age":25,"gender":"M","address":"359 Baughman Place","employer":"Phuel","email":"sybilcarrillo@phuel.com","city":"Kohatk","state":"CT"}
{"index":{"_id":"839"}}
{"account_number":839,"balance":38292,"firstname":"Langley","lastname":"Neal","age":39,"gender":"F","address":"565 Newton Street","employer":"Liquidoc","email":"langleyneal@liquidoc.com","city":"Osage","state":"AL"}
{"index":{"_id":"841"}}
{"account_number":841,"balance":28291,"firstname":"Dalton","lastname":"Waters","age":21,"gender":"M","address":"859 Grand Street","employer":"Malathion","email":"daltonwaters@malathion.com","city":"Tonopah","state":"AZ"}
{"index":{"_id":"846"}}
{"account_number":846,"balance":35099,"firstname":"Maureen","lastname":"Glass","age":22,"gender":"M","address":"140 Amherst Street","employer":"Stelaecor","email":"maureenglass@stelaecor.com","city":"Cucumber","state":"IL"}
{"index":{"_id":"853"}}
{"account_number":853,"balance":38353,"firstname":"Travis","lastname":"Parks","age":40,"gender":"M","address":"930 Bay Avenue","employer":"Pyramax","email":"travisparks@pyramax.com","city":"Gadsden","state":"ND"}
{"index":{"_id":"858"}}
{"account_number":858,"balance":23194,"firstname":"Small","lastname":"Hatfield","age":36,"gender":"M","address":"593 Tennis Court","employer":"Letpro","email":"smallhatfield@letpro.com","city":"Haena","state":"KS"}
{"index":{"_id":"860"}}
{"account_number":860,"balance":23613,"firstname":"Clark","lastname":"Boyd","age":37,"gender":"M","address":"501 Rock Street","employer":"Deepends","email":"clarkboyd@deepends.com","city":"Whitewater","state":"MA"}
{"index":{"_id":"865"}}
{"account_number":865,"balance":10574,"firstname":"Cook","lastname":"Kelley","age":28,"gender":"F","address":"865 Lincoln Terrace","employer":"Quizmo","email":"cookkelley@quizmo.com","city":"Kansas","state":"KY"}
{"index":{"_id":"872"}}
{"account_number":872,"balance":26314,"firstname":"Jane","lastname":"Greer","age":36,"gender":"F","address":"717 Hewes Street","employer":"Newcube","email":"janegreer@newcube.com","city":"Delshire","state":"DE"}
{"index":{"_id":"877"}}
{"account_number":877,"balance":42879,"firstname":"Tracey","lastname":"Ruiz","age":34,"gender":"F","address":"141 Tompkins Avenue","employer":"Waab","email":"traceyruiz@waab.com","city":"Zeba","state":"NM"}
{"index":{"_id":"884"}}
{"account_number":884,"balance":29316,"firstname":"Reva","lastname":"Rosa","age":40,"gender":"M","address":"784 Greene Avenue","employer":"Urbanshee","email":"revarosa@urbanshee.com","city":"Bakersville","state":"MS"}
{"index":{"_id":"889"}}
{"account_number":889,"balance":26464,"firstname":"Fischer","lastname":"Klein","age":38,"gender":"F","address":"948 Juliana Place","employer":"Comtext","email":"fischerklein@comtext.com","city":"Jackpot","state":"PA"}
{"index":{"_id":"891"}}
{"account_number":891,"balance":34829,"firstname":"Jacobson","lastname":"Clemons","age":24,"gender":"F","address":"507 Wilson Street","employer":"Quilm","email":"jacobsonclemons@quilm.com","city":"Muir","state":"TX"}
{"index":{"_id":"896"}}
{"account_number":896,"balance":31947,"firstname":"Buckley","lastname":"Peterson","age":26,"gender":"M","address":"217 Beayer Place","employer":"Earwax","email":"buckleypeterson@earwax.com","city":"Franklin","state":"DE"}
{"index":{"_id":"904"}}
{"account_number":904,"balance":27707,"firstname":"Mendez","lastname":"Mcneil","age":26,"gender":"M","address":"431 Halsey Street","employer":"Macronaut","email":"mendezmcneil@macronaut.com","city":"Troy","state":"OK"}
{"index":{"_id":"909"}}
{"account_number":909,"balance":18421,"firstname":"Stark","lastname":"Lewis","age":36,"gender":"M","address":"409 Tilden Avenue","employer":"Frosnex","email":"starklewis@frosnex.com","city":"Axis","state":"CA"}
{"index":{"_id":"911"}}
{"account_number":911,"balance":42655,"firstname":"Annie","lastname":"Lyons","age":21,"gender":"M","address":"518 Woods Place","employer":"Enerforce","email":"annielyons@enerforce.com","city":"Stagecoach","state":"MA"}
{"index":{"_id":"916"}}
{"account_number":916,"balance":47887,"firstname":"Jarvis","lastname":"Alexander","age":40,"gender":"M","address":"406 Bergen Avenue","employer":"Equitax","email":"jarvisalexander@equitax.com","city":"Haring","state":"KY"}
{"index":{"_id":"923"}}
{"account_number":923,"balance":48466,"firstname":"Mueller","lastname":"Mckee","age":26,"gender":"M","address":"298 Ruby Street","employer":"Luxuria","email":"muellermckee@luxuria.com","city":"Coleville","state":"TN"}
{"index":{"_id":"928"}}
{"account_number":928,"balance":19611,"firstname":"Hester","lastname":"Copeland","age":22,"gender":"F","address":"425 Cropsey Avenue","employer":"Dymi","email":"hestercopeland@dymi.com","city":"Wolcott","state":"NE"}
{"index":{"_id":"930"}}
{"account_number":930,"balance":47257,"firstname":"Kinney","lastname":"Lawson","age":39,"gender":"M","address":"501 Raleigh Place","employer":"Neptide","email":"kinneylawson@neptide.com","city":"Deltaville","state":"MD"}
{"index":{"_id":"935"}}
{"account_number":935,"balance":4959,"firstname":"Flowers","lastname":"Robles","age":30,"gender":"M","address":"201 Hull Street","employer":"Xelegyl","email":"flowersrobles@xelegyl.com","city":"Rehrersburg","state":"AL"}
{"index":{"_id":"942"}}
{"account_number":942,"balance":21299,"firstname":"Hamilton","lastname":"Clayton","age":26,"gender":"M","address":"413 Debevoise Street","employer":"Architax","email":"hamiltonclayton@architax.com","city":"Terlingua","state":"NM"}
{"index":{"_id":"947"}}
{"account_number":947,"balance":22039,"firstname":"Virgie","lastname":"Garza","age":30,"gender":"M","address":"903 Matthews Court","employer":"Plasmox","email":"virgiegarza@plasmox.com","city":"Somerset","state":"WY"}
{"index":{"_id":"954"}}
{"account_number":954,"balance":49404,"firstname":"Jenna","lastname":"Martin","age":22,"gender":"M","address":"688 Hart Street","employer":"Zinca","email":"jennamartin@zinca.com","city":"Oasis","state":"MD"}
{"index":{"_id":"959"}}
{"account_number":959,"balance":34743,"firstname":"Shaffer","lastname":"Cervantes","age":40,"gender":"M","address":"931 Varick Avenue","employer":"Oceanica","email":"shaffercervantes@oceanica.com","city":"Bowie","state":"AL"}
{"index":{"_id":"961"}}
{"account_number":961,"balance":43219,"firstname":"Betsy","lastname":"Hyde","age":27,"gender":"F","address":"183 Junius Street","employer":"Tubalum","email":"betsyhyde@tubalum.com","city":"Driftwood","state":"TX"}
{"index":{"_id":"966"}}
{"account_number":966,"balance":20619,"firstname":"Susanne","lastname":"Rodriguez","age":35,"gender":"F","address":"255 Knickerbocker Avenue","employer":"Comtrek","email":"susannerodriguez@comtrek.com","city":"Trinway","state":"TX"}
{"index":{"_id":"973"}}
{"account_number":973,"balance":45756,"firstname":"Rice","lastname":"Farmer","age":31,"gender":"M","address":"476 Nassau Avenue","employer":"Photobin","email":"ricefarmer@photobin.com","city":"Suitland","state":"ME"}
{"index":{"_id":"978"}}
{"account_number":978,"balance":21459,"firstname":"Melanie","lastname":"Rojas","age":33,"gender":"M","address":"991 Java Street","employer":"Kage","email":"melanierojas@kage.com","city":"Greenock","state":"VT"}
{"index":{"_id":"980"}}
{"account_number":980,"balance":42436,"firstname":"Cash","lastname":"Collier","age":33,"gender":"F","address":"999 Sapphire Street","employer":"Ceprene","email":"cashcollier@ceprene.com","city":"Glidden","state":"AK"}
{"index":{"_id":"985"}}
{"account_number":985,"balance":20083,"firstname":"Martin","lastname":"Gardner","age":28,"gender":"F","address":"644 Fairview Place","employer":"Golistic","email":"martingardner@golistic.com","city":"Connerton","state":"NJ"}
{"index":{"_id":"992"}}
{"account_number":992,"balance":11413,"firstname":"Kristie","lastname":"Kennedy","age":33,"gender":"F","address":"750 Hudson Avenue","employer":"Ludak","email":"kristiekennedy@ludak.com","city":"Warsaw","state":"WY"}
{"index":{"_id":"997"}}
{"account_number":997,"balance":25311,"firstname":"Combs","lastname":"Frederick","age":20,"gender":"M","address":"586 Lloyd Court","employer":"Pathways","email":"combsfrederick@pathways.com","city":"Williamson","state":"CA"}
{"index":{"_id":"3"}}
{"account_number":3,"balance":44947,"firstname":"Levine","lastname":"Burks","age":26,"gender":"F","address":"328 Wilson Avenue","employer":"Amtap","email":"levineburks@amtap.com","city":"Cochranville","state":"HI"}
{"index":{"_id":"8"}}
{"account_number":8,"balance":48868,"firstname":"Jan","lastname":"Burns","age":35,"gender":"M","address":"699 Visitation Place","employer":"Glasstep","email":"janburns@glasstep.com","city":"Wakulla","state":"AZ"}
{"index":{"_id":"10"}}
{"account_number":10,"balance":46170,"firstname":"Dominique","lastname":"Park","age":37,"gender":"F","address":"100 Gatling Place","employer":"Conjurica","email":"dominiquepark@conjurica.com","city":"Omar","state":"NJ"}
{"index":{"_id":"15"}}
{"account_number":15,"balance":43456,"firstname":"Bobbie","lastname":"Sexton","age":21,"gender":"M","address":"232 Sedgwick Place","employer":"Zytrex","email":"bobbiesexton@zytrex.com","city":"Hendersonville","state":"CA"}
{"index":{"_id":"22"}}
{"account_number":22,"balance":40283,"firstname":"Barrera","lastname":"Terrell","age":23,"gender":"F","address":"292 Orange Street","employer":"Steelfab","email":"barreraterrell@steelfab.com","city":"Bynum","state":"ME"}
{"index":{"_id":"27"}}
{"account_number":27,"balance":6176,"firstname":"Meyers","lastname":"Williamson","age":26,"gender":"F","address":"675 Henderson Walk","employer":"Plexia","email":"meyerswilliamson@plexia.com","city":"Richmond","state":"AZ"}
{"index":{"_id":"34"}}
{"account_number":34,"balance":35379,"firstname":"Ellison","lastname":"Kim","age":30,"gender":"F","address":"986 Revere Place","employer":"Signity","email":"ellisonkim@signity.com","city":"Sehili","state":"IL"}
{"index":{"_id":"39"}}
{"account_number":39,"balance":38688,"firstname":"Bowers","lastname":"Mendez","age":22,"gender":"F","address":"665 Bennet Court","employer":"Farmage","email":"bowersmendez@farmage.com","city":"Duryea","state":"PA"}
{"index":{"_id":"41"}}
{"account_number":41,"balance":36060,"firstname":"Hancock","lastname":"Holden","age":20,"gender":"M","address":"625 Gaylord Drive","employer":"Poochies","email":"hancockholden@poochies.com","city":"Alamo","state":"KS"}
{"index":{"_id":"46"}}
{"account_number":46,"balance":12351,"firstname":"Karla","lastname":"Bowman","age":23,"gender":"M","address":"554 Chapel Street","employer":"Undertap","email":"karlabowman@undertap.com","city":"Sylvanite","state":"DC"}
{"index":{"_id":"53"}}
{"account_number":53,"balance":28101,"firstname":"Kathryn","lastname":"Payne","age":29,"gender":"F","address":"467 Louis Place","employer":"Katakana","email":"kathrynpayne@katakana.com","city":"Harviell","state":"SD"}
{"index":{"_id":"58"}}
{"account_number":58,"balance":31697,"firstname":"Marva","lastname":"Cannon","age":40,"gender":"M","address":"993 Highland Place","employer":"Comcubine","email":"marvacannon@comcubine.com","city":"Orviston","state":"MO"}
{"index":{"_id":"60"}}
{"account_number":60,"balance":45955,"firstname":"Maude","lastname":"Casey","age":31,"gender":"F","address":"566 Strauss Street","employer":"Quilch","email":"maudecasey@quilch.com","city":"Enlow","state":"GA"}
{"index":{"_id":"65"}}
{"account_number":65,"balance":23282,"firstname":"Leonor","lastname":"Pruitt","age":24,"gender":"M","address":"974 Terrace Place","employer":"Velos","email":"leonorpruitt@velos.com","city":"Devon","state":"WI"}
{"index":{"_id":"72"}}
{"account_number":72,"balance":9732,"firstname":"Barlow","lastname":"Rhodes","age":25,"gender":"F","address":"891 Clinton Avenue","employer":"Zialactic","email":"barlowrhodes@zialactic.com","city":"Echo","state":"TN"}
{"index":{"_id":"77"}}
{"account_number":77,"balance":5724,"firstname":"Byrd","lastname":"Conley","age":24,"gender":"F","address":"698 Belmont Avenue","employer":"Zidox","email":"byrdconley@zidox.com","city":"Rockbridge","state":"SC"}
{"index":{"_id":"84"}}
{"account_number":84,"balance":3001,"firstname":"Hutchinson","lastname":"Newton","age":34,"gender":"F","address":"553 Locust Street","employer":"Zaggles","email":"hutchinsonnewton@zaggles.com","city":"Snyderville","state":"DC"}
{"index":{"_id":"89"}}
{"account_number":89,"balance":13263,"firstname":"Mcdowell","lastname":"Bradley","age":28,"gender":"M","address":"960 Howard Alley","employer":"Grok","email":"mcdowellbradley@grok.com","city":"Toftrees","state":"TX"}
{"index":{"_id":"91"}}
{"account_number":91,"balance":29799,"firstname":"Vonda","lastname":"Galloway","age":20,"gender":"M","address":"988 Voorhies Avenue","employer":"Illumity","email":"vondagalloway@illumity.com","city":"Holcombe","state":"HI"}
{"index":{"_id":"96"}}
{"account_number":96,"balance":15933,"firstname":"Shirley","lastname":"Edwards","age":38,"gender":"M","address":"817 Caton Avenue","employer":"Equitox","email":"shirleyedwards@equitox.com","city":"Nelson","state":"MA"}
{"index":{"_id":"104"}}
{"account_number":104,"balance":32619,"firstname":"Casey","lastname":"Roth","age":29,"gender":"M","address":"963 Railroad Avenue","employer":"Hotcakes","email":"caseyroth@hotcakes.com","city":"Davenport","state":"OH"}
{"index":{"_id":"109"}}
{"account_number":109,"balance":25812,"firstname":"Gretchen","lastname":"Dawson","age":31,"gender":"M","address":"610 Bethel Loop","employer":"Tetak","email":"gretchendawson@tetak.com","city":"Hailesboro","state":"CO"}
{"index":{"_id":"111"}}
{"account_number":111,"balance":1481,"firstname":"Traci","lastname":"Allison","age":35,"gender":"M","address":"922 Bryant Street","employer":"Enjola","email":"traciallison@enjola.com","city":"Robinette","state":"OR"}
{"index":{"_id":"116"}}
{"account_number":116,"balance":21335,"firstname":"Hobbs","lastname":"Wright","age":24,"gender":"M","address":"965 Temple Court","employer":"Netbook","email":"hobbswright@netbook.com","city":"Strong","state":"CA"}
{"index":{"_id":"123"}}
{"account_number":123,"balance":3079,"firstname":"Cleo","lastname":"Beach","age":27,"gender":"F","address":"653 Haring Street","employer":"Proxsoft","email":"cleobeach@proxsoft.com","city":"Greensburg","state":"ME"}
{"index":{"_id":"128"}}
{"account_number":128,"balance":3556,"firstname":"Mack","lastname":"Bullock","age":34,"gender":"F","address":"462 Ingraham Street","employer":"Terascape","email":"mackbullock@terascape.com","city":"Eureka","state":"PA"}
{"index":{"_id":"130"}}
{"account_number":130,"balance":24171,"firstname":"Roxie","lastname":"Cantu","age":33,"gender":"M","address":"841 Catherine Street","employer":"Skybold","email":"roxiecantu@skybold.com","city":"Deputy","state":"NE"}
{"index":{"_id":"135"}}
{"account_number":135,"balance":24885,"firstname":"Stevenson","lastname":"Crosby","age":40,"gender":"F","address":"473 Boardwalk ","employer":"Accel","email":"stevensoncrosby@accel.com","city":"Norris","state":"OK"}
{"index":{"_id":"142"}}
{"account_number":142,"balance":4544,"firstname":"Vang","lastname":"Hughes","age":27,"gender":"M","address":"357 Landis Court","employer":"Bolax","email":"vanghughes@bolax.com","city":"Emerald","state":"WY"}
{"index":{"_id":"147"}}
{"account_number":147,"balance":35921,"firstname":"Charmaine","lastname":"Whitney","age":28,"gender":"F","address":"484 Seton Place","employer":"Comveyer","email":"charmainewhitney@comveyer.com","city":"Dexter","state":"DC"}
{"index":{"_id":"154"}}
{"account_number":154,"balance":40945,"firstname":"Burns","lastname":"Solis","age":31,"gender":"M","address":"274 Lorraine Street","employer":"Rodemco","email":"burnssolis@rodemco.com","city":"Ballico","state":"WI"}
{"index":{"_id":"159"}}
{"account_number":159,"balance":1696,"firstname":"Alvarez","lastname":"Mack","age":22,"gender":"F","address":"897 Manor Court","employer":"Snorus","email":"alvarezmack@snorus.com","city":"Rosedale","state":"CA"}
{"index":{"_id":"161"}}
{"account_number":161,"balance":4659,"firstname":"Doreen","lastname":"Randall","age":37,"gender":"F","address":"178 Court Street","employer":"Calcula","email":"doreenrandall@calcula.com","city":"Belmont","state":"TX"}
{"index":{"_id":"166"}}
{"account_number":166,"balance":33847,"firstname":"Rutledge","lastname":"Rivas","age":23,"gender":"M","address":"352 Verona Street","employer":"Virxo","email":"rutledgerivas@virxo.com","city":"Brandermill","state":"NE"}
{"index":{"_id":"173"}}
{"account_number":173,"balance":5989,"firstname":"Whitley","lastname":"Blevins","age":32,"gender":"M","address":"127 Brooklyn Avenue","employer":"Pawnagra","email":"whitleyblevins@pawnagra.com","city":"Rodanthe","state":"ND"}
{"index":{"_id":"178"}}
{"account_number":178,"balance":36735,"firstname":"Clements","lastname":"Finley","age":39,"gender":"F","address":"270 Story Court","employer":"Imaginart","email":"clementsfinley@imaginart.com","city":"Lookingglass","state":"MN"}
{"index":{"_id":"180"}}
{"account_number":180,"balance":34236,"firstname":"Ursula","lastname":"Goodman","age":32,"gender":"F","address":"414 Clinton Street","employer":"Earthmark","email":"ursulagoodman@earthmark.com","city":"Rote","state":"AR"}
{"index":{"_id":"185"}}
{"account_number":185,"balance":43532,"firstname":"Laurel","lastname":"Cline","age":40,"gender":"M","address":"788 Fenimore Street","employer":"Prismatic","email":"laurelcline@prismatic.com","city":"Frank","state":"UT"}
{"index":{"_id":"192"}}
{"account_number":192,"balance":23508,"firstname":"Ramsey","lastname":"Carr","age":31,"gender":"F","address":"209 Williamsburg Street","employer":"Strezzo","email":"ramseycarr@strezzo.com","city":"Grapeview","state":"NM"}
{"index":{"_id":"197"}}
{"account_number":197,"balance":17246,"firstname":"Sweet","lastname":"Sanders","age":33,"gender":"F","address":"712 Homecrest Court","employer":"Isosure","email":"sweetsanders@isosure.com","city":"Sheatown","state":"VT"}
{"index":{"_id":"200"}}
{"account_number":200,"balance":26210,"firstname":"Teri","lastname":"Hester","age":39,"gender":"M","address":"653 Abbey Court","employer":"Electonic","email":"terihester@electonic.com","city":"Martell","state":"MD"}
{"index":{"_id":"205"}}
{"account_number":205,"balance":45493,"firstname":"Johnson","lastname":"Chang","age":28,"gender":"F","address":"331 John Street","employer":"Gleamink","email":"johnsonchang@gleamink.com","city":"Sultana","state":"KS"}
{"index":{"_id":"212"}}
{"account_number":212,"balance":10299,"firstname":"Marisol","lastname":"Fischer","age":39,"gender":"M","address":"362 Prince Street","employer":"Autograte","email":"marisolfischer@autograte.com","city":"Oley","state":"SC"}
{"index":{"_id":"217"}}
{"account_number":217,"balance":33730,"firstname":"Sally","lastname":"Mccoy","age":38,"gender":"F","address":"854 Corbin Place","employer":"Omnigog","email":"sallymccoy@omnigog.com","city":"Escondida","state":"FL"}
{"index":{"_id":"224"}}
{"account_number":224,"balance":42708,"firstname":"Billie","lastname":"Nixon","age":28,"gender":"F","address":"241 Kaufman Place","employer":"Xanide","email":"billienixon@xanide.com","city":"Chapin","state":"NY"}
{"index":{"_id":"229"}}
{"account_number":229,"balance":2740,"firstname":"Jana","lastname":"Hensley","age":30,"gender":"M","address":"176 Erasmus Street","employer":"Isotrack","email":"janahensley@isotrack.com","city":"Caledonia","state":"ME"}
{"index":{"_id":"231"}}
{"account_number":231,"balance":46180,"firstname":"Essie","lastname":"Clarke","age":34,"gender":"F","address":"308 Harbor Lane","employer":"Pharmacon","email":"essieclarke@pharmacon.com","city":"Fillmore","state":"MS"}
{"index":{"_id":"236"}}
{"account_number":236,"balance":41200,"firstname":"Suzanne","lastname":"Bird","age":39,"gender":"F","address":"219 Luquer Street","employer":"Imant","email":"suzannebird@imant.com","city":"Bainbridge","state":"NY"}
{"index":{"_id":"243"}}
{"account_number":243,"balance":29902,"firstname":"Evangelina","lastname":"Perez","age":20,"gender":"M","address":"787 Joval Court","employer":"Keengen","email":"evangelinaperez@keengen.com","city":"Mulberry","state":"SD"}
{"index":{"_id":"248"}}
{"account_number":248,"balance":49989,"firstname":"West","lastname":"England","age":36,"gender":"M","address":"717 Hendrickson Place","employer":"Obliq","email":"westengland@obliq.com","city":"Maury","state":"WA"}
{"index":{"_id":"250"}}
{"account_number":250,"balance":27893,"firstname":"Earlene","lastname":"Ellis","age":39,"gender":"F","address":"512 Bay Street","employer":"Codact","email":"earleneellis@codact.com","city":"Sunwest","state":"GA"}
{"index":{"_id":"255"}}
{"account_number":255,"balance":49339,"firstname":"Iva","lastname":"Rivers","age":38,"gender":"M","address":"470 Rost Place","employer":"Mantrix","email":"ivarivers@mantrix.com","city":"Disautel","state":"MD"}
{"index":{"_id":"262"}}
{"account_number":262,"balance":30289,"firstname":"Tameka","lastname":"Levine","age":36,"gender":"F","address":"815 Atlantic Avenue","employer":"Acium","email":"tamekalevine@acium.com","city":"Winchester","state":"SD"}
{"index":{"_id":"267"}}
{"account_number":267,"balance":42753,"firstname":"Weeks","lastname":"Castillo","age":21,"gender":"F","address":"526 Holt Court","employer":"Talendula","email":"weekscastillo@talendula.com","city":"Washington","state":"NV"}
{"index":{"_id":"274"}}
{"account_number":274,"balance":12104,"firstname":"Frieda","lastname":"House","age":33,"gender":"F","address":"171 Banker Street","employer":"Quonk","email":"friedahouse@quonk.com","city":"Aberdeen","state":"NJ"}
{"index":{"_id":"279"}}
{"account_number":279,"balance":15904,"firstname":"Chapman","lastname":"Hart","age":32,"gender":"F","address":"902 Bliss Terrace","employer":"Kongene","email":"chapmanhart@kongene.com","city":"Bradenville","state":"NJ"}
{"index":{"_id":"281"}}
{"account_number":281,"balance":39830,"firstname":"Bean","lastname":"Aguirre","age":20,"gender":"F","address":"133 Pilling Street","employer":"Amril","email":"beanaguirre@amril.com","city":"Waterview","state":"TX"}
{"index":{"_id":"286"}}
{"account_number":286,"balance":39063,"firstname":"Rosetta","lastname":"Turner","age":35,"gender":"M","address":"169 Jefferson Avenue","employer":"Spacewax","email":"rosettaturner@spacewax.com","city":"Stewart","state":"MO"}
{"index":{"_id":"293"}}
{"account_number":293,"balance":29867,"firstname":"Cruz","lastname":"Carver","age":28,"gender":"F","address":"465 Boerum Place","employer":"Vitricomp","email":"cruzcarver@vitricomp.com","city":"Crayne","state":"CO"}
{"index":{"_id":"298"}}
{"account_number":298,"balance":34334,"firstname":"Bullock","lastname":"Marsh","age":20,"gender":"M","address":"589 Virginia Place","employer":"Renovize","email":"bullockmarsh@renovize.com","city":"Coinjock","state":"UT"}
{"index":{"_id":"301"}}
{"account_number":301,"balance":16782,"firstname":"Minerva","lastname":"Graham","age":35,"gender":"M","address":"532 Harrison Place","employer":"Sureplex","email":"minervagraham@sureplex.com","city":"Belleview","state":"GA"}
{"index":{"_id":"306"}}
{"account_number":306,"balance":2171,"firstname":"Hensley","lastname":"Hardin","age":40,"gender":"M","address":"196 Maujer Street","employer":"Neocent","email":"hensleyhardin@neocent.com","city":"Reinerton","state":"HI"}
{"index":{"_id":"313"}}
{"account_number":313,"balance":34108,"firstname":"Alston","lastname":"Henderson","age":36,"gender":"F","address":"132 Prescott Place","employer":"Prosure","email":"alstonhenderson@prosure.com","city":"Worton","state":"IA"}
{"index":{"_id":"318"}}
{"account_number":318,"balance":8512,"firstname":"Nichole","lastname":"Pearson","age":34,"gender":"F","address":"656 Lacon Court","employer":"Yurture","email":"nicholepearson@yurture.com","city":"Juarez","state":"MO"}
{"index":{"_id":"320"}}
{"account_number":320,"balance":34521,"firstname":"Patti","lastname":"Brennan","age":37,"gender":"F","address":"870 Degraw Street","employer":"Cognicode","email":"pattibrennan@cognicode.com","city":"Torboy","state":"FL"}
{"index":{"_id":"325"}}
{"account_number":325,"balance":1956,"firstname":"Magdalena","lastname":"Simmons","age":25,"gender":"F","address":"681 Townsend Street","employer":"Geekosis","email":"magdalenasimmons@geekosis.com","city":"Sterling","state":"CA"}
{"index":{"_id":"332"}}
{"account_number":332,"balance":37770,"firstname":"Shepherd","lastname":"Davenport","age":28,"gender":"F","address":"586 Montague Terrace","employer":"Ecraze","email":"shepherddavenport@ecraze.com","city":"Accoville","state":"NM"}
{"index":{"_id":"337"}}
{"account_number":337,"balance":43432,"firstname":"Monroe","lastname":"Stafford","age":37,"gender":"F","address":"183 Seigel Street","employer":"Centuria","email":"monroestafford@centuria.com","city":"Camino","state":"DE"}
{"index":{"_id":"344"}}
{"account_number":344,"balance":42654,"firstname":"Sasha","lastname":"Baxter","age":35,"gender":"F","address":"700 Bedford Place","employer":"Callflex","email":"sashabaxter@callflex.com","city":"Campo","state":"MI"}
{"index":{"_id":"349"}}
{"account_number":349,"balance":24180,"firstname":"Allison","lastname":"Fitzpatrick","age":22,"gender":"F","address":"913 Arlington Avenue","employer":"Veraq","email":"allisonfitzpatrick@veraq.com","city":"Marbury","state":"TX"}
{"index":{"_id":"351"}}
{"account_number":351,"balance":47089,"firstname":"Hendrix","lastname":"Stephens","age":29,"gender":"M","address":"181 Beaver Street","employer":"Recrisys","email":"hendrixstephens@recrisys.com","city":"Denio","state":"OR"}
{"index":{"_id":"356"}}
{"account_number":356,"balance":34540,"firstname":"Lourdes","lastname":"Valdez","age":20,"gender":"F","address":"700 Anchorage Place","employer":"Interloo","email":"lourdesvaldez@interloo.com","city":"Goldfield","state":"OK"}
{"index":{"_id":"363"}}
{"account_number":363,"balance":34007,"firstname":"Peggy","lastname":"Bright","age":21,"gender":"M","address":"613 Engert Avenue","employer":"Inventure","email":"peggybright@inventure.com","city":"Chautauqua","state":"ME"}
{"index":{"_id":"368"}}
{"account_number":368,"balance":23535,"firstname":"Hooper","lastname":"Tyson","age":39,"gender":"M","address":"892 Taaffe Place","employer":"Zaggle","email":"hoopertyson@zaggle.com","city":"Nutrioso","state":"ME"}
{"index":{"_id":"370"}}
{"account_number":370,"balance":28499,"firstname":"Oneill","lastname":"Carney","age":25,"gender":"F","address":"773 Adelphi Street","employer":"Bedder","email":"oneillcarney@bedder.com","city":"Yorklyn","state":"FL"}
{"index":{"_id":"375"}}
{"account_number":375,"balance":23860,"firstname":"Phoebe","lastname":"Patton","age":25,"gender":"M","address":"564 Hale Avenue","employer":"Xoggle","email":"phoebepatton@xoggle.com","city":"Brule","state":"NM"}
{"index":{"_id":"382"}}
{"account_number":382,"balance":42061,"firstname":"Finley","lastname":"Singleton","age":37,"gender":"F","address":"407 Clay Street","employer":"Quarex","email":"finleysingleton@quarex.com","city":"Bedias","state":"LA"}
{"index":{"_id":"387"}}
{"account_number":387,"balance":35916,"firstname":"April","lastname":"Hill","age":29,"gender":"M","address":"818 Bayard Street","employer":"Kengen","email":"aprilhill@kengen.com","city":"Chloride","state":"NC"}
{"index":{"_id":"394"}}
{"account_number":394,"balance":6121,"firstname":"Lorrie","lastname":"Nunez","age":38,"gender":"M","address":"221 Ralph Avenue","employer":"Bullzone","email":"lorrienunez@bullzone.com","city":"Longoria","state":"ID"}
{"index":{"_id":"399"}}
{"account_number":399,"balance":32587,"firstname":"Carmela","lastname":"Franks","age":23,"gender":"M","address":"617 Dewey Place","employer":"Zensure","email":"carmelafranks@zensure.com","city":"Sanders","state":"DC"}
{"index":{"_id":"402"}}
{"account_number":402,"balance":1282,"firstname":"Pacheco","lastname":"Rosales","age":32,"gender":"M","address":"538 Pershing Loop","employer":"Circum","email":"pachecorosales@circum.com","city":"Elbert","state":"ID"}
{"index":{"_id":"407"}}
{"account_number":407,"balance":36417,"firstname":"Gilda","lastname":"Jacobson","age":29,"gender":"F","address":"883 Loring Avenue","employer":"Comveyor","email":"gildajacobson@comveyor.com","city":"Topaz","state":"NH"}
{"index":{"_id":"414"}}
{"account_number":414,"balance":17506,"firstname":"Conway","lastname":"Daugherty","age":37,"gender":"F","address":"643 Kermit Place","employer":"Lyria","email":"conwaydaugherty@lyria.com","city":"Vaughn","state":"NV"}
{"index":{"_id":"419"}}
{"account_number":419,"balance":34847,"firstname":"Helen","lastname":"Montoya","age":29,"gender":"F","address":"736 Kingsland Avenue","employer":"Hairport","email":"helenmontoya@hairport.com","city":"Edinburg","state":"NE"}
{"index":{"_id":"421"}}
{"account_number":421,"balance":46868,"firstname":"Tamika","lastname":"Mccall","age":27,"gender":"F","address":"764 Bragg Court","employer":"Eventix","email":"tamikamccall@eventix.com","city":"Tivoli","state":"RI"}
{"index":{"_id":"426"}}
{"account_number":426,"balance":4499,"firstname":"Julie","lastname":"Parsons","age":31,"gender":"M","address":"768 Keap Street","employer":"Goko","email":"julieparsons@goko.com","city":"Coldiron","state":"VA"}
{"index":{"_id":"433"}}
{"account_number":433,"balance":19266,"firstname":"Wilkinson","lastname":"Flowers","age":39,"gender":"M","address":"154 Douglass Street","employer":"Xsports","email":"wilkinsonflowers@xsports.com","city":"Coultervillle","state":"MN"}
{"index":{"_id":"438"}}
{"account_number":438,"balance":16367,"firstname":"Walter","lastname":"Velez","age":27,"gender":"F","address":"931 Farragut Road","employer":"Virva","email":"waltervelez@virva.com","city":"Tyro","state":"WV"}
{"index":{"_id":"440"}}
{"account_number":440,"balance":41590,"firstname":"Ray","lastname":"Wiley","age":31,"gender":"F","address":"102 Barwell Terrace","employer":"Polaria","email":"raywiley@polaria.com","city":"Hardyville","state":"IA"}
{"index":{"_id":"445"}}
{"account_number":445,"balance":41178,"firstname":"Rodriguez","lastname":"Macias","age":34,"gender":"M","address":"164 Boerum Street","employer":"Xylar","email":"rodriguezmacias@xylar.com","city":"Riner","state":"AL"}
{"index":{"_id":"452"}}
{"account_number":452,"balance":3589,"firstname":"Blackwell","lastname":"Delaney","age":39,"gender":"F","address":"443 Sackett Street","employer":"Imkan","email":"blackwelldelaney@imkan.com","city":"Gasquet","state":"DC"}
{"index":{"_id":"457"}}
{"account_number":457,"balance":14057,"firstname":"Bush","lastname":"Gordon","age":34,"gender":"M","address":"975 Dakota Place","employer":"Softmicro","email":"bushgordon@softmicro.com","city":"Chemung","state":"PA"}
{"index":{"_id":"464"}}
{"account_number":464,"balance":20504,"firstname":"Cobb","lastname":"Humphrey","age":21,"gender":"M","address":"823 Sunnyside Avenue","employer":"Apexia","email":"cobbhumphrey@apexia.com","city":"Wintersburg","state":"NY"}
{"index":{"_id":"469"}}
{"account_number":469,"balance":26509,"firstname":"Marci","lastname":"Shepherd","age":26,"gender":"M","address":"565 Hall Street","employer":"Shadease","email":"marcishepherd@shadease.com","city":"Springhill","state":"IL"}
{"index":{"_id":"471"}}
{"account_number":471,"balance":7629,"firstname":"Juana","lastname":"Silva","age":36,"gender":"M","address":"249 Amity Street","employer":"Artworlds","email":"juanasilva@artworlds.com","city":"Norfolk","state":"TX"}
{"index":{"_id":"476"}}
{"account_number":476,"balance":33386,"firstname":"Silva","lastname":"Marks","age":31,"gender":"F","address":"183 Eldert Street","employer":"Medifax","email":"silvamarks@medifax.com","city":"Hachita","state":"RI"}
{"index":{"_id":"483"}}
{"account_number":483,"balance":6344,"firstname":"Kelley","lastname":"Harper","age":29,"gender":"M","address":"758 Preston Court","employer":"Xyqag","email":"kelleyharper@xyqag.com","city":"Healy","state":"IA"}
{"index":{"_id":"488"}}
{"account_number":488,"balance":6289,"firstname":"Wilma","lastname":"Hopkins","age":38,"gender":"M","address":"428 Lee Avenue","employer":"Entality","email":"wilmahopkins@entality.com","city":"Englevale","state":"WI"}
{"index":{"_id":"490"}}
{"account_number":490,"balance":1447,"firstname":"Strong","lastname":"Hendrix","age":26,"gender":"F","address":"134 Beach Place","employer":"Duoflex","email":"stronghendrix@duoflex.com","city":"Allentown","state":"ND"}
{"index":{"_id":"495"}}
{"account_number":495,"balance":13478,"firstname":"Abigail","lastname":"Nichols","age":40,"gender":"F","address":"887 President Street","employer":"Enquility","email":"abigailnichols@enquility.com","city":"Bagtown","state":"NM"}
{"index":{"_id":"503"}}
{"account_number":503,"balance":42649,"firstname":"Leta","lastname":"Stout","age":39,"gender":"F","address":"518 Bowery Street","employer":"Pivitol","email":"letastout@pivitol.com","city":"Boonville","state":"ND"}
{"index":{"_id":"508"}}
{"account_number":508,"balance":41300,"firstname":"Lawrence","lastname":"Mathews","age":27,"gender":"F","address":"987 Rose Street","employer":"Deviltoe","email":"lawrencemathews@deviltoe.com","city":"Woodburn","state":"FL"}
{"index":{"_id":"510"}}
{"account_number":510,"balance":48504,"firstname":"Petty","lastname":"Sykes","age":28,"gender":"M","address":"566 Village Road","employer":"Nebulean","email":"pettysykes@nebulean.com","city":"Wedgewood","state":"MO"}
{"index":{"_id":"515"}}
{"account_number":515,"balance":18531,"firstname":"Lott","lastname":"Keller","age":27,"gender":"M","address":"827 Miami Court","employer":"Translink","email":"lottkeller@translink.com","city":"Gila","state":"TX"}
{"index":{"_id":"522"}}
{"account_number":522,"balance":19879,"firstname":"Faulkner","lastname":"Garrett","age":29,"gender":"F","address":"396 Grove Place","employer":"Pigzart","email":"faulknergarrett@pigzart.com","city":"Felt","state":"AR"}
{"index":{"_id":"527"}}
{"account_number":527,"balance":2028,"firstname":"Carver","lastname":"Peters","age":35,"gender":"M","address":"816 Victor Road","employer":"Housedown","email":"carverpeters@housedown.com","city":"Nadine","state":"MD"}
{"index":{"_id":"534"}}
{"account_number":534,"balance":20470,"firstname":"Cristina","lastname":"Russo","age":25,"gender":"F","address":"500 Highlawn Avenue","employer":"Cyclonica","email":"cristinarusso@cyclonica.com","city":"Gorst","state":"KS"}
{"index":{"_id":"539"}}
{"account_number":539,"balance":24560,"firstname":"Tami","lastname":"Maddox","age":23,"gender":"F","address":"741 Pineapple Street","employer":"Accidency","email":"tamimaddox@accidency.com","city":"Kennedyville","state":"OH"}
{"index":{"_id":"541"}}
{"account_number":541,"balance":42915,"firstname":"Logan","lastname":"Burke","age":32,"gender":"M","address":"904 Clarendon Road","employer":"Overplex","email":"loganburke@overplex.com","city":"Johnsonburg","state":"OH"}
{"index":{"_id":"546"}}
{"account_number":546,"balance":43242,"firstname":"Bernice","lastname":"Sims","age":33,"gender":"M","address":"382 Columbia Street","employer":"Verbus","email":"bernicesims@verbus.com","city":"Sena","state":"KY"}
{"index":{"_id":"553"}}
{"account_number":553,"balance":28390,"firstname":"Aimee","lastname":"Cohen","age":28,"gender":"M","address":"396 Lafayette Avenue","employer":"Eplode","email":"aimeecohen@eplode.com","city":"Thatcher","state":"NJ"}
{"index":{"_id":"558"}}
{"account_number":558,"balance":8922,"firstname":"Horne","lastname":"Valenzuela","age":20,"gender":"F","address":"979 Kensington Street","employer":"Isoternia","email":"hornevalenzuela@isoternia.com","city":"Greenbush","state":"NC"}
{"index":{"_id":"560"}}
{"account_number":560,"balance":24514,"firstname":"Felecia","lastname":"Oneill","age":26,"gender":"M","address":"995 Autumn Avenue","employer":"Mediot","email":"feleciaoneill@mediot.com","city":"Joppa","state":"IN"}
{"index":{"_id":"565"}}
{"account_number":565,"balance":15197,"firstname":"Taylor","lastname":"Ingram","age":37,"gender":"F","address":"113 Will Place","employer":"Lyrichord","email":"tayloringram@lyrichord.com","city":"Collins","state":"ME"}
{"index":{"_id":"572"}}
{"account_number":572,"balance":49355,"firstname":"Therese","lastname":"Espinoza","age":20,"gender":"M","address":"994 Chester Court","employer":"Gonkle","email":"thereseespinoza@gonkle.com","city":"Hayes","state":"UT"}
{"index":{"_id":"577"}}
{"account_number":577,"balance":21398,"firstname":"Gilbert","lastname":"Serrano","age":38,"gender":"F","address":"294 Troutman Street","employer":"Senmao","email":"gilbertserrano@senmao.com","city":"Greer","state":"MT"}
{"index":{"_id":"584"}}
{"account_number":584,"balance":5346,"firstname":"Pearson","lastname":"Bryant","age":40,"gender":"F","address":"971 Heyward Street","employer":"Anacho","email":"pearsonbryant@anacho.com","city":"Bluffview","state":"MN"}
{"index":{"_id":"589"}}
{"account_number":589,"balance":33260,"firstname":"Ericka","lastname":"Cote","age":39,"gender":"F","address":"425 Bath Avenue","employer":"Venoflex","email":"erickacote@venoflex.com","city":"Blue","state":"CT"}
{"index":{"_id":"591"}}
{"account_number":591,"balance":48997,"firstname":"Rivers","lastname":"Macdonald","age":34,"gender":"F","address":"919 Johnson Street","employer":"Ziore","email":"riversmacdonald@ziore.com","city":"Townsend","state":"IL"}
{"index":{"_id":"596"}}
{"account_number":596,"balance":4063,"firstname":"Letitia","lastname":"Walker","age":26,"gender":"F","address":"963 Vanderveer Place","employer":"Zizzle","email":"letitiawalker@zizzle.com","city":"Rossmore","state":"ID"}
{"index":{"_id":"604"}}
{"account_number":604,"balance":10675,"firstname":"Isabel","lastname":"Gilliam","age":23,"gender":"M","address":"854 Broadway ","employer":"Zenthall","email":"isabelgilliam@zenthall.com","city":"Ventress","state":"WI"}
{"index":{"_id":"609"}}
{"account_number":609,"balance":28586,"firstname":"Montgomery","lastname":"Washington","age":30,"gender":"M","address":"169 Schroeders Avenue","employer":"Kongle","email":"montgomerywashington@kongle.com","city":"Croom","state":"AZ"}
{"index":{"_id":"611"}}
{"account_number":611,"balance":17528,"firstname":"Katherine","lastname":"Prince","age":33,"gender":"F","address":"705 Elm Avenue","employer":"Zillacon","email":"katherineprince@zillacon.com","city":"Rew","state":"MI"}
{"index":{"_id":"616"}}
{"account_number":616,"balance":25276,"firstname":"Jessie","lastname":"Mayer","age":35,"gender":"F","address":"683 Chester Avenue","employer":"Emtrak","email":"jessiemayer@emtrak.com","city":"Marysville","state":"HI"}
{"index":{"_id":"623"}}
{"account_number":623,"balance":20514,"firstname":"Rose","lastname":"Combs","age":32,"gender":"F","address":"312 Grimes Road","employer":"Aquamate","email":"rosecombs@aquamate.com","city":"Fostoria","state":"OH"}
{"index":{"_id":"628"}}
{"account_number":628,"balance":42736,"firstname":"Buckner","lastname":"Chen","age":37,"gender":"M","address":"863 Rugby Road","employer":"Jamnation","email":"bucknerchen@jamnation.com","city":"Camas","state":"TX"}
{"index":{"_id":"630"}}
{"account_number":630,"balance":46060,"firstname":"Leanne","lastname":"Jones","age":31,"gender":"M","address":"451 Bayview Avenue","employer":"Wazzu","email":"leannejones@wazzu.com","city":"Kylertown","state":"OK"}
{"index":{"_id":"635"}}
{"account_number":635,"balance":44705,"firstname":"Norman","lastname":"Gilmore","age":33,"gender":"M","address":"330 Gates Avenue","employer":"Comfirm","email":"normangilmore@comfirm.com","city":"Riceville","state":"TN"}
{"index":{"_id":"642"}}
{"account_number":642,"balance":32852,"firstname":"Reyna","lastname":"Harris","age":35,"gender":"M","address":"305 Powell Street","employer":"Bedlam","email":"reynaharris@bedlam.com","city":"Florence","state":"KS"}
{"index":{"_id":"647"}}
{"account_number":647,"balance":10147,"firstname":"Annabelle","lastname":"Velazquez","age":30,"gender":"M","address":"299 Kensington Walk","employer":"Sealoud","email":"annabellevelazquez@sealoud.com","city":"Soudan","state":"ME"}
{"index":{"_id":"654"}}
{"account_number":654,"balance":38695,"firstname":"Armstrong","lastname":"Frazier","age":25,"gender":"M","address":"899 Seeley Street","employer":"Zensor","email":"armstrongfrazier@zensor.com","city":"Cherokee","state":"UT"}
{"index":{"_id":"659"}}
{"account_number":659,"balance":29648,"firstname":"Dorsey","lastname":"Sosa","age":40,"gender":"M","address":"270 Aberdeen Street","employer":"Daycore","email":"dorseysosa@daycore.com","city":"Chamberino","state":"SC"}
{"index":{"_id":"661"}}
{"account_number":661,"balance":3679,"firstname":"Joanne","lastname":"Spencer","age":39,"gender":"F","address":"910 Montauk Avenue","employer":"Visalia","email":"joannespencer@visalia.com","city":"Valmy","state":"NH"}
{"index":{"_id":"666"}}
{"account_number":666,"balance":13880,"firstname":"Mcguire","lastname":"Lloyd","age":40,"gender":"F","address":"658 Just Court","employer":"Centrexin","email":"mcguirelloyd@centrexin.com","city":"Warren","state":"MT"}
{"index":{"_id":"673"}}
{"account_number":673,"balance":11303,"firstname":"Mcdaniel","lastname":"Harrell","age":33,"gender":"M","address":"565 Montgomery Place","employer":"Eyeris","email":"mcdanielharrell@eyeris.com","city":"Garnet","state":"NV"}
{"index":{"_id":"678"}}
{"account_number":678,"balance":43663,"firstname":"Ruby","lastname":"Shaffer","age":28,"gender":"M","address":"350 Clark Street","employer":"Comtrail","email":"rubyshaffer@comtrail.com","city":"Aurora","state":"MA"}
{"index":{"_id":"680"}}
{"account_number":680,"balance":31561,"firstname":"Melton","lastname":"Camacho","age":32,"gender":"F","address":"771 Montana Place","employer":"Insuresys","email":"meltoncamacho@insuresys.com","city":"Sparkill","state":"IN"}
{"index":{"_id":"685"}}
{"account_number":685,"balance":22249,"firstname":"Yesenia","lastname":"Rowland","age":24,"gender":"F","address":"193 Dekalb Avenue","employer":"Coriander","email":"yeseniarowland@coriander.com","city":"Lupton","state":"NC"}
{"index":{"_id":"692"}}
{"account_number":692,"balance":10435,"firstname":"Haney","lastname":"Barlow","age":21,"gender":"F","address":"267 Lenox Road","employer":"Egypto","email":"haneybarlow@egypto.com","city":"Detroit","state":"IN"}
{"index":{"_id":"697"}}
{"account_number":697,"balance":48745,"firstname":"Mallory","lastname":"Emerson","age":24,"gender":"F","address":"318 Dunne Court","employer":"Exoplode","email":"malloryemerson@exoplode.com","city":"Montura","state":"LA"}
{"index":{"_id":"700"}}
{"account_number":700,"balance":19164,"firstname":"Patel","lastname":"Durham","age":21,"gender":"F","address":"440 King Street","employer":"Icology","email":"pateldurham@icology.com","city":"Mammoth","state":"IL"}
{"index":{"_id":"705"}}
{"account_number":705,"balance":28415,"firstname":"Krystal","lastname":"Cross","age":22,"gender":"M","address":"604 Drew Street","employer":"Tubesys","email":"krystalcross@tubesys.com","city":"Dalton","state":"MO"}
{"index":{"_id":"712"}}
{"account_number":712,"balance":12459,"firstname":"Butler","lastname":"Alston","age":37,"gender":"M","address":"486 Hemlock Street","employer":"Quordate","email":"butleralston@quordate.com","city":"Verdi","state":"MS"}
{"index":{"_id":"717"}}
{"account_number":717,"balance":29270,"firstname":"Erickson","lastname":"Mcdonald","age":31,"gender":"M","address":"873 Franklin Street","employer":"Exotechno","email":"ericksonmcdonald@exotechno.com","city":"Jessie","state":"MS"}
{"index":{"_id":"724"}}
{"account_number":724,"balance":12548,"firstname":"Hopper","lastname":"Peck","age":31,"gender":"M","address":"849 Hendrickson Street","employer":"Uxmox","email":"hopperpeck@uxmox.com","city":"Faxon","state":"UT"}
{"index":{"_id":"729"}}
{"account_number":729,"balance":41812,"firstname":"Katy","lastname":"Rivera","age":36,"gender":"F","address":"791 Olive Street","employer":"Blurrybus","email":"katyrivera@blurrybus.com","city":"Innsbrook","state":"MI"}
{"index":{"_id":"731"}}
{"account_number":731,"balance":4994,"firstname":"Lorene","lastname":"Weiss","age":35,"gender":"M","address":"990 Ocean Court","employer":"Comvoy","email":"loreneweiss@comvoy.com","city":"Lavalette","state":"WI"}
{"index":{"_id":"736"}}
{"account_number":736,"balance":28677,"firstname":"Rogers","lastname":"Mcmahon","age":21,"gender":"F","address":"423 Cameron Court","employer":"Brainclip","email":"rogersmcmahon@brainclip.com","city":"Saddlebrooke","state":"FL"}
{"index":{"_id":"743"}}
{"account_number":743,"balance":14077,"firstname":"Susana","lastname":"Moody","age":23,"gender":"M","address":"842 Fountain Avenue","employer":"Bitrex","email":"susanamoody@bitrex.com","city":"Temperanceville","state":"TN"}
{"index":{"_id":"748"}}
{"account_number":748,"balance":38060,"firstname":"Ford","lastname":"Branch","age":25,"gender":"M","address":"926 Cypress Avenue","employer":"Buzzness","email":"fordbranch@buzzness.com","city":"Beason","state":"DC"}
{"index":{"_id":"750"}}
{"account_number":750,"balance":40481,"firstname":"Cherie","lastname":"Brooks","age":20,"gender":"F","address":"601 Woodhull Street","employer":"Kaggle","email":"cheriebrooks@kaggle.com","city":"Groton","state":"MA"}
{"index":{"_id":"755"}}
{"account_number":755,"balance":43878,"firstname":"Bartlett","lastname":"Conway","age":22,"gender":"M","address":"453 Times Placez","employer":"Konnect","email":"bartlettconway@konnect.com","city":"Belva","state":"VT"}
{"index":{"_id":"762"}}
{"account_number":762,"balance":10291,"firstname":"Amanda","lastname":"Head","age":20,"gender":"F","address":"990 Ocean Parkway","employer":"Zentury","email":"amandahead@zentury.com","city":"Hegins","state":"AR"}
{"index":{"_id":"767"}}
{"account_number":767,"balance":26220,"firstname":"Anthony","lastname":"Sutton","age":27,"gender":"F","address":"179 Fayette Street","employer":"Xiix","email":"anthonysutton@xiix.com","city":"Iberia","state":"TN"}
{"index":{"_id":"774"}}
{"account_number":774,"balance":35287,"firstname":"Lynnette","lastname":"Alvarez","age":38,"gender":"F","address":"991 Brightwater Avenue","employer":"Gink","email":"lynnettealvarez@gink.com","city":"Leola","state":"NC"}
{"index":{"_id":"779"}}
{"account_number":779,"balance":40983,"firstname":"Maggie","lastname":"Pace","age":32,"gender":"F","address":"104 Harbor Court","employer":"Bulljuice","email":"maggiepace@bulljuice.com","city":"Floris","state":"MA"}
{"index":{"_id":"781"}}
{"account_number":781,"balance":29961,"firstname":"Sanford","lastname":"Mullen","age":26,"gender":"F","address":"879 Dover Street","employer":"Zanity","email":"sanfordmullen@zanity.com","city":"Martinez","state":"TX"}
{"index":{"_id":"786"}}
{"account_number":786,"balance":3024,"firstname":"Rene","lastname":"Vang","age":33,"gender":"M","address":"506 Randolph Street","employer":"Isopop","email":"renevang@isopop.com","city":"Vienna","state":"NJ"}
{"index":{"_id":"793"}}
{"account_number":793,"balance":16911,"firstname":"Alford","lastname":"Compton","age":36,"gender":"M","address":"186 Veronica Place","employer":"Zyple","email":"alfordcompton@zyple.com","city":"Sugartown","state":"AK"}
{"index":{"_id":"798"}}
{"account_number":798,"balance":3165,"firstname":"Catherine","lastname":"Ward","age":30,"gender":"F","address":"325 Burnett Street","employer":"Dreamia","email":"catherineward@dreamia.com","city":"Glenbrook","state":"SD"}
{"index":{"_id":"801"}}
{"account_number":801,"balance":14954,"firstname":"Molly","lastname":"Maldonado","age":37,"gender":"M","address":"518 Maple Avenue","employer":"Straloy","email":"mollymaldonado@straloy.com","city":"Hebron","state":"WI"}
{"index":{"_id":"806"}}
{"account_number":806,"balance":36492,"firstname":"Carson","lastname":"Riddle","age":31,"gender":"M","address":"984 Lois Avenue","employer":"Terrago","email":"carsonriddle@terrago.com","city":"Leland","state":"MN"}
{"index":{"_id":"813"}}
{"account_number":813,"balance":30833,"firstname":"Ebony","lastname":"Bishop","age":20,"gender":"M","address":"487 Ridge Court","employer":"Optique","email":"ebonybishop@optique.com","city":"Fairmount","state":"WA"}
{"index":{"_id":"818"}}
{"account_number":818,"balance":24433,"firstname":"Espinoza","lastname":"Petersen","age":26,"gender":"M","address":"641 Glenwood Road","employer":"Futurity","email":"espinozapetersen@futurity.com","city":"Floriston","state":"MD"}
{"index":{"_id":"820"}}
{"account_number":820,"balance":1011,"firstname":"Shepard","lastname":"Ramsey","age":24,"gender":"F","address":"806 Village Court","employer":"Mantro","email":"shepardramsey@mantro.com","city":"Tibbie","state":"NV"}
{"index":{"_id":"825"}}
{"account_number":825,"balance":49000,"firstname":"Terra","lastname":"Witt","age":21,"gender":"F","address":"590 Conway Street","employer":"Insectus","email":"terrawitt@insectus.com","city":"Forbestown","state":"AR"}
{"index":{"_id":"832"}}
{"account_number":832,"balance":8582,"firstname":"Laura","lastname":"Gibbs","age":39,"gender":"F","address":"511 Osborn Street","employer":"Corepan","email":"lauragibbs@corepan.com","city":"Worcester","state":"KS"}
{"index":{"_id":"837"}}
{"account_number":837,"balance":14485,"firstname":"Amy","lastname":"Villarreal","age":35,"gender":"M","address":"381 Stillwell Place","employer":"Fleetmix","email":"amyvillarreal@fleetmix.com","city":"Sanford","state":"IA"}
{"index":{"_id":"844"}}
{"account_number":844,"balance":26840,"firstname":"Jill","lastname":"David","age":31,"gender":"M","address":"346 Legion Street","employer":"Zytrax","email":"jilldavid@zytrax.com","city":"Saticoy","state":"SC"}
{"index":{"_id":"849"}}
{"account_number":849,"balance":16200,"firstname":"Barry","lastname":"Chapman","age":26,"gender":"M","address":"931 Dekoven Court","employer":"Darwinium","email":"barrychapman@darwinium.com","city":"Whitestone","state":"WY"}
{"index":{"_id":"851"}}
{"account_number":851,"balance":22026,"firstname":"Henderson","lastname":"Price","age":33,"gender":"F","address":"530 Hausman Street","employer":"Plutorque","email":"hendersonprice@plutorque.com","city":"Brutus","state":"RI"}
{"index":{"_id":"856"}}
{"account_number":856,"balance":27583,"firstname":"Alissa","lastname":"Knox","age":25,"gender":"M","address":"258 Empire Boulevard","employer":"Geologix","email":"alissaknox@geologix.com","city":"Hartsville/Hartley","state":"MN"}
{"index":{"_id":"863"}}
{"account_number":863,"balance":23165,"firstname":"Melendez","lastname":"Fernandez","age":40,"gender":"M","address":"661 Johnson Avenue","employer":"Vixo","email":"melendezfernandez@vixo.com","city":"Farmers","state":"IL"}
{"index":{"_id":"868"}}
{"account_number":868,"balance":27624,"firstname":"Polly","lastname":"Barron","age":22,"gender":"M","address":"129 Frank Court","employer":"Geofarm","email":"pollybarron@geofarm.com","city":"Loyalhanna","state":"ND"}
{"index":{"_id":"870"}}
{"account_number":870,"balance":43882,"firstname":"Goff","lastname":"Phelps","age":21,"gender":"M","address":"164 Montague Street","employer":"Digigen","email":"goffphelps@digigen.com","city":"Weedville","state":"IL"}
{"index":{"_id":"875"}}
{"account_number":875,"balance":19655,"firstname":"Mercer","lastname":"Pratt","age":24,"gender":"M","address":"608 Perry Place","employer":"Twiggery","email":"mercerpratt@twiggery.com","city":"Eggertsville","state":"MO"}
{"index":{"_id":"882"}}
{"account_number":882,"balance":10895,"firstname":"Mari","lastname":"Landry","age":39,"gender":"M","address":"963 Gerald Court","employer":"Kenegy","email":"marilandry@kenegy.com","city":"Lithium","state":"NC"}
{"index":{"_id":"887"}}
{"account_number":887,"balance":31772,"firstname":"Eunice","lastname":"Watts","age":36,"gender":"F","address":"707 Stuyvesant Avenue","employer":"Memora","email":"eunicewatts@memora.com","city":"Westwood","state":"TN"}
{"index":{"_id":"894"}}
{"account_number":894,"balance":1031,"firstname":"Tyler","lastname":"Fitzgerald","age":32,"gender":"M","address":"787 Meserole Street","employer":"Jetsilk","email":"tylerfitzgerald@jetsilk.com","city":"Woodlands","state":"WV"}
{"index":{"_id":"899"}}
{"account_number":899,"balance":32953,"firstname":"Carney","lastname":"Callahan","age":23,"gender":"M","address":"724 Kimball Street","employer":"Mangelica","email":"carneycallahan@mangelica.com","city":"Tecolotito","state":"MT"}
{"index":{"_id":"902"}}
{"account_number":902,"balance":13345,"firstname":"Hallie","lastname":"Jarvis","age":23,"gender":"F","address":"237 Duryea Court","employer":"Anixang","email":"halliejarvis@anixang.com","city":"Boykin","state":"IN"}
{"index":{"_id":"907"}}
{"account_number":907,"balance":12961,"firstname":"Ingram","lastname":"William","age":36,"gender":"M","address":"826 Overbaugh Place","employer":"Genmex","email":"ingramwilliam@genmex.com","city":"Kimmell","state":"AK"}
{"index":{"_id":"914"}}
{"account_number":914,"balance":7120,"firstname":"Esther","lastname":"Bean","age":32,"gender":"F","address":"583 Macon Street","employer":"Applica","email":"estherbean@applica.com","city":"Homeworth","state":"MN"}
{"index":{"_id":"919"}}
{"account_number":919,"balance":39655,"firstname":"Shauna","lastname":"Hanson","age":27,"gender":"M","address":"557 Hart Place","employer":"Exospace","email":"shaunahanson@exospace.com","city":"Outlook","state":"LA"}
{"index":{"_id":"921"}}
{"account_number":921,"balance":49119,"firstname":"Barbara","lastname":"Wade","age":29,"gender":"M","address":"687 Hoyts Lane","employer":"Roughies","email":"barbarawade@roughies.com","city":"Sattley","state":"CO"}
{"index":{"_id":"926"}}
{"account_number":926,"balance":49433,"firstname":"Welch","lastname":"Mcgowan","age":21,"gender":"M","address":"833 Quincy Street","employer":"Atomica","email":"welchmcgowan@atomica.com","city":"Hampstead","state":"VT"}
{"index":{"_id":"933"}}
{"account_number":933,"balance":18071,"firstname":"Tabitha","lastname":"Cole","age":21,"gender":"F","address":"916 Rogers Avenue","employer":"Eclipto","email":"tabithacole@eclipto.com","city":"Lawrence","state":"TX"}
{"index":{"_id":"938"}}
{"account_number":938,"balance":9597,"firstname":"Sharron","lastname":"Santos","age":40,"gender":"F","address":"215 Matthews Place","employer":"Zenco","email":"sharronsantos@zenco.com","city":"Wattsville","state":"VT"}
{"index":{"_id":"940"}}
{"account_number":940,"balance":23285,"firstname":"Melinda","lastname":"Mendoza","age":38,"gender":"M","address":"806 Kossuth Place","employer":"Kneedles","email":"melindamendoza@kneedles.com","city":"Coaldale","state":"OK"}
{"index":{"_id":"945"}}
{"account_number":945,"balance":23085,"firstname":"Hansen","lastname":"Hebert","age":33,"gender":"F","address":"287 Conduit Boulevard","employer":"Capscreen","email":"hansenhebert@capscreen.com","city":"Taycheedah","state":"AK"}
{"index":{"_id":"952"}}
{"account_number":952,"balance":21430,"firstname":"Angelique","lastname":"Weeks","age":33,"gender":"M","address":"659 Reeve Place","employer":"Exodoc","email":"angeliqueweeks@exodoc.com","city":"Turpin","state":"MD"}
{"index":{"_id":"957"}}
{"account_number":957,"balance":11373,"firstname":"Michael","lastname":"Giles","age":31,"gender":"M","address":"668 Court Square","employer":"Yogasm","email":"michaelgiles@yogasm.com","city":"Rosburg","state":"WV"}
{"index":{"_id":"964"}}
{"account_number":964,"balance":26154,"firstname":"Elena","lastname":"Waller","age":34,"gender":"F","address":"618 Crystal Street","employer":"Insurety","email":"elenawaller@insurety.com","city":"Gallina","state":"NY"}
{"index":{"_id":"969"}}
{"account_number":969,"balance":22214,"firstname":"Briggs","lastname":"Lynn","age":30,"gender":"M","address":"952 Lester Court","employer":"Quinex","email":"briggslynn@quinex.com","city":"Roland","state":"ID"}
{"index":{"_id":"971"}}
{"account_number":971,"balance":22772,"firstname":"Gabrielle","lastname":"Reilly","age":32,"gender":"F","address":"964 Tudor Terrace","employer":"Blanet","email":"gabriellereilly@blanet.com","city":"Falmouth","state":"AL"}
{"index":{"_id":"976"}}
{"account_number":976,"balance":31707,"firstname":"Mullen","lastname":"Tanner","age":26,"gender":"M","address":"711 Whitney Avenue","employer":"Pulze","email":"mullentanner@pulze.com","city":"Mooresburg","state":"MA"}
{"index":{"_id":"983"}}
{"account_number":983,"balance":47205,"firstname":"Mattie","lastname":"Eaton","age":24,"gender":"F","address":"418 Allen Avenue","employer":"Trasola","email":"mattieeaton@trasola.com","city":"Dupuyer","state":"NJ"}
{"index":{"_id":"988"}}
{"account_number":988,"balance":17803,"firstname":"Lucy","lastname":"Castro","age":34,"gender":"F","address":"425 Fleet Walk","employer":"Geekfarm","email":"lucycastro@geekfarm.com","city":"Mulino","state":"VA"}
{"index":{"_id":"990"}}
{"account_number":990,"balance":44456,"firstname":"Kelly","lastname":"Steele","age":35,"gender":"M","address":"809 Hoyt Street","employer":"Eschoir","email":"kellysteele@eschoir.com","city":"Stewartville","state":"ID"}
{"index":{"_id":"995"}}
{"account_number":995,"balance":21153,"firstname":"Phelps","lastname":"Parrish","age":25,"gender":"M","address":"666 Miller Place","employer":"Pearlessa","email":"phelpsparrish@pearlessa.com","city":"Brecon","state":"ME"}

image-20230701155812781

四.进阶检索

1.SearchAPI

ES支持两种基本的方式检索:

  • 一个是通过使用REST request URI 发送搜索参数(uri+检索参数)
  • 另一个是通过使用 REST request body来发送它们(uri+请求体)

1.1 检索信息

一切检索从_search开始

1.1.1 使用uri+检索参数的方式进行检索

GET bank/_search //检索bank下所有信息,包含type和docs
GET bank/_search?q=*&sort=account_number:asc //请求参数方式检索
//q=*表示查询所有
//sort=account_number:asc 按照account_number进行升序排序
//相应的结果解析
{
"took":29,//执行搜索的时间
"time_out":false, //搜索是否超时
"_shards":{.....},//有多少个分片被搜索了,以及统计成功/失败的搜索分片
"hits":{.......}//搜索的结果
}

image-20230701162247192

完整的响应结果

{
"took" : 29,
"timed_out" : false,
"_shards" : {
"total" : 1,
"successful" : 1,
"skipped" : 0,
"failed" : 0
},
"hits" : {
"total" : {
"value" : 1000,
"relation" : "eq"
},
"max_score" : null,
"hits" : [
{
"_index" : "bank",
"_type" : "account",
"_id" : "0",
"_score" : null,
"_source" : {
"account_number" : 0,
"balance" : 16623,
"firstname" : "Bradshaw",
"lastname" : "Mckenzie",
"age" : 29,
"gender" : "F",
"address" : "244 Columbus Place",
"employer" : "Euron",
"email" : "bradshawmckenzie@euron.com",
"city" : "Hobucken",
"state" : "CO"
},
"sort" : [
0
]
},
{
"_index" : "bank",
"_type" : "account",
"_id" : "1",
"_score" : null,
"_source" : {
"account_number" : 1,
"balance" : 39225,
"firstname" : "Amber",
"lastname" : "Duke",
"age" : 32,
"gender" : "M",
"address" : "880 Holmes Lane",
"employer" : "Pyrami",
"email" : "amberduke@pyrami.com",
"city" : "Brogan",
"state" : "IL"
},
"sort" : [
1
]
},
{
"_index" : "bank",
"_type" : "account",
"_id" : "2",
"_score" : null,
"_source" : {
"account_number" : 2,
"balance" : 28838,
"firstname" : "Roberta",
"lastname" : "Bender",
"age" : 22,
"gender" : "F",
"address" : "560 Kingsway Place",
"employer" : "Chillium",
"email" : "robertabender@chillium.com",
"city" : "Bennett",
"state" : "LA"
},
"sort" : [
2
]
},
{
"_index" : "bank",
"_type" : "account",
"_id" : "3",
"_score" : null,
"_source" : {
"account_number" : 3,
"balance" : 44947,
"firstname" : "Levine",
"lastname" : "Burks",
"age" : 26,
"gender" : "F",
"address" : "328 Wilson Avenue",
"employer" : "Amtap",
"email" : "levineburks@amtap.com",
"city" : "Cochranville",
"state" : "HI"
},
"sort" : [
3
]
},
{
"_index" : "bank",
"_type" : "account",
"_id" : "4",
"_score" : null,
"_source" : {
"account_number" : 4,
"balance" : 27658,
"firstname" : "Rodriquez",
"lastname" : "Flores",
"age" : 31,
"gender" : "F",
"address" : "986 Wyckoff Avenue",
"employer" : "Tourmania",
"email" : "rodriquezflores@tourmania.com",
"city" : "Eastvale",
"state" : "HI"
},
"sort" : [
4
]
},
{
"_index" : "bank",
"_type" : "account",
"_id" : "5",
"_score" : null,
"_source" : {
"account_number" : 5,
"balance" : 29342,
"firstname" : "Leola",
"lastname" : "Stewart",
"age" : 30,
"gender" : "F",
"address" : "311 Elm Place",
"employer" : "Diginetic",
"email" : "leolastewart@diginetic.com",
"city" : "Fairview",
"state" : "NJ"
},
"sort" : [
5
]
},
{
"_index" : "bank",
"_type" : "account",
"_id" : "6",
"_score" : null,
"_source" : {
"account_number" : 6,
"balance" : 5686,
"firstname" : "Hattie",
"lastname" : "Bond",
"age" : 36,
"gender" : "M",
"address" : "671 Bristol Street",
"employer" : "Netagy",
"email" : "hattiebond@netagy.com",
"city" : "Dante",
"state" : "TN"
},
"sort" : [
6
]
},
{
"_index" : "bank",
"_type" : "account",
"_id" : "7",
"_score" : null,
"_source" : {
"account_number" : 7,
"balance" : 39121,
"firstname" : "Levy",
"lastname" : "Richard",
"age" : 22,
"gender" : "M",
"address" : "820 Logan Street",
"employer" : "Teraprene",
"email" : "levyrichard@teraprene.com",
"city" : "Shrewsbury",
"state" : "MO"
},
"sort" : [
7
]
},
{
"_index" : "bank",
"_type" : "account",
"_id" : "8",
"_score" : null,
"_source" : {
"account_number" : 8,
"balance" : 48868,
"firstname" : "Jan",
"lastname" : "Burns",
"age" : 35,
"gender" : "M",
"address" : "699 Visitation Place",
"employer" : "Glasstep",
"email" : "janburns@glasstep.com",
"city" : "Wakulla",
"state" : "AZ"
},
"sort" : [
8
]
},
{
"_index" : "bank",
"_type" : "account",
"_id" : "9",
"_score" : null,
"_source" : {
"account_number" : 9,
"balance" : 24776,
"firstname" : "Opal",
"lastname" : "Meadows",
"age" : 39,
"gender" : "M",
"address" : "963 Neptune Avenue",
"employer" : "Cedward",
"email" : "opalmeadows@cedward.com",
"city" : "Olney",
"state" : "OH"
},
"sort" : [
9
]
}
]
}
}

1.1.2 使用uri+请求体的方式进行检索

GET /bank/_search 
{
"query": {
"match_all": {} //查询所有(匹配条件)
},
"sort": [//排序规则
{
"account_number": "asc"//根据account_number升序
},
{
"balance":"desc" //根据balance降序
}
]
}

image-20230701163210869

2.Query DSL

2.1 基本的语法

GET /bank/_search
{
"query": { //查询操作
"match_all": {} //查询的条件
},
"sort": [ //排序的条件(数组,可以指定多个排序规则)
{
"balance": { //排序的字段
"order": "desc" //排序的规则(升序或者降序)
}
}
],
"from": 0,//分页字段,从第几页开始
"size": 5,//每页的记录数
"_source": ["balance","firstname"] //返回指定的字段
}

2.2 match查询[匹配查询]

根据某个字段的值进行匹配查询

//查询account_number是20的数据
GET /bank/_search
{
"query": {
"match": { //查询account_number是20的数据
"account_number": "20"
}
}
}

模糊匹配

对检索的条件进行分词匹配,按照评分的进行排序

//查询地址中包含mill或者lane或者mill lane的数据
GET /bank/_search
{
"query": {
"match": {
"address": "Mill lane" //查询地址中包含Mill或者lane中一个或者多个的数据,然后按照命中的评分排序
}
}
}

2.3 match_phrase[短语匹配]

匹配的短语不分割进行匹配,一整个进行匹配

//查询地址中包含Mill lane短语的数据
GET /bank/_search
{
"query": {
"match_phrase": {
"address": "Mill lane" //查询包含整个Mill lane的数据
}
}
}

2.4 multi_match[多字段匹配]

//查询state和address中包含mill的数据
GET /bank/_search
{
"query": {
"multi_match": { //查询在state或者在address字段中包含mill的数据
"query": "mill",//多字段匹配在查询的时候也会分词进行匹配
"fields": ["state","address"]
}
}
}

2.5 bool[复合查询]

//查询性别是M和地址中包含mill并且年龄不是28,lastname最好是Hines的数据
GET /bank/_search
{
"query": {
"bool": { //多条件复合查询
"must": [ //必须符合的条件
{"match": {
"gender": "M"
}},
{"match": {
"address": "mill"
}}
],
"must_not": [//必须不符合的条件
{"match": {
"age": "38"
}}
],
"should": [
{"match": {
"lastname": "Hines" //最好lastname是Hines,满足这个条件的得分会高一些
}}
]
}
}
}

2.6 Filter[结果过滤]

使用Filter进行检索相比上面的检索,不会影响过滤结果的得分(上面的检索都可以通过Filter实现)

举例:未使用filter的查询:

//查询年龄在18-20之间的所有数据
GET /bank/_search
{
"query": {
"bool": {
"must": [
{"range": {
"age": {
"gte": 18,
"lte": 30
}
}}
]
}
}
}

有得分的变化

image-20230702111451660

使用了Filter的查询:

GET /bank/_search
{
"query": {
"bool": {
"filter": {
"range": {
"age": {
"gte": 18,
"lte": 30
}
}
}
}
}
}

没有得分的变化

image-20230702111554667

Filter可以和上面的检索条件一起使用

GET /bank/_search
{
"query": {

"bool": {

"must": [
{"match": {
"gender": "M"
}},
{"match": {
"address": "mill"
}}
],

"must_not": [
{"match": {
"age": "18"
}}
],

"should": [
{"match": {
"lastname": "Wallace"
}}
],

"filter": [
{"range": {
"age": {
"gte": 18,
"lte": 30
}
}}
]
}
}
}

2.7 term

和match一样,匹配某个属性的值。全文检索字段用match,其他非text字段匹配用term(精确性字段推荐使用term,例如年龄,数量等)

GET /bank/_search
{
"query": {
"term": {//精确的使用term,全文检索字段用match
"age": "28"
}
}
}

2.8 aggregations[执行聚合]

聚合提供了从数据中分组和提取数据的能力.类似于MySql中的Group by函数

搜索address中包含mill的所有人的年龄分布以及平均年龄

//搜索address中包含mill的所有人的年龄分布以及平均年龄
GET /bank/_search
{
"query": {
"match": {
"address": "mill"
}
},
"aggs": {
"ageAgg": {
"terms": {//查看年龄的分布情况
"field": "age",
"size": 10
}
},
"ageAvg":{//年龄平均值
"avg": {
"field": "age"
}
},
"balanceAvg":{//工资的平均值
"avg": {
"field": "balance"
}
}
}
}

image-20230703101958163

按照年龄聚合,并且获取这些年龄段的这些人的平均薪资[子聚合]

//按照年龄聚合,并且获取这些年龄段的这些人的平均薪资
GET /bank/_search
{
"query": {
"match_all": {}
},
"aggs": {
"ageAgg": {//年龄段的聚合
"terms": {
"field": "age",
"size": 100
},
"aggs": {
"ageAvg": {//年龄段的聚合里面嵌套一个求年龄平均值的聚合
"avg": {
"field": "balance"
}
}
}
}
}
}

image-20230703102805047

查询所有年龄分布,并且这些年龄段中男的平均工资和女的平均工资以及这个年龄段的总体平均薪资

GET /bank/_search
{
"query": {
"match_all": {}
},
"aggs": {
"avgAgg": {
"terms": {//根据年龄进行分段
"field": "age",
"size": 100
},
"aggs": {
"genderAgg": {
"terms": {//根据性别进行分段
"field": "gender.keyword"
},
"aggs": {
"balanceAvg": {
"avg": {//求出每个年龄段对应的不同性别的平均工资
"field": "balance"
}
}
}
},
"ageBalanceAvg": {//计算每个年龄段对应的平均工资
"avg": {
"field": "balance"
}
}
}
}
}
}

image-20230703105247964

3.Mapping

指定ES中数据的数据类型

3.1 创建映射

在创建索引的时候我们可以指定属性名的数据类型

##创建一个新的索引,指定索引下字段的映射关系
PUT /my_index
{
"mappings": {
"properties": {
"age":{"type": "integer"},
"email":{"type": "keyword"},
"name":{"type": "text"}
}
}
}

image-20230703112342368

3.2 添加新的映射

##添加一个映射
PUT /my_index/_mapping
{
"properties": {
"employee-id": {
"type": "keyword",
"index": false //表示这个字段不参与检索
}
}
}

image-20230703113121234

3.3 更新映射

对于已经存在的映射字段,我们不能更新,更新必须创建新的索引进行数据迁移

3.4 数据迁移

1.创建一个新的索引并指定映射

PUT /new_bank
{
"mappings": {
"properties": {
"account_number": {
"type": "long"
},
"address": {
"type": "text"
},
"age": {
"type": "integer"
},
"balance": {
"type": "long"
},
"city": {
"type": "keyword"
},
"email": {
"type": "keyword"
},
"employer": {
"type": "keyword"
},
"firstname": {
"type": "text"
},
"gender": {
"type": "keyword"
},
"lastname": {
"type": "keyword"
},
"state": {
"type": "keyword"
}
}
}
}

2.数据迁移

//新版本的写法(6.0以后)
POST _reindex
{
"source":{
"index":"twitter" //老的索引
},
"dest":{
"index":"new_twitter" //新的索引
}
}
//旧版本需要指定数据类型
#数据迁移
POST _reindex
{
"source": {
"index": "bank",
"type": "account"
},
"dest": {
"index": "new_bank"
}
}

image-20230703115140810

4.分词

4.1 安装分词器

安装地址: https://github.com/medcl/elasticsearch-analysis-ik

#切换到es挂载在注解的plugins目录
cd /mydata/elasticsearch/plugins/
#安装wget
yum install wget
#下载ik分词器的压缩包(下载的速度不是很快(5分钟左右),建议本地下载好了之后,上传上去)
wget https://github.com/medcl/elasticsearch-analysis-ik/releases/download/v7.4.2/elasticsearch-analysis-ik-7.4.2.zip
#进入容器内部查看一下文件是否同步到了容器的内部
docker exec -it 容器Id /bin/bash
#切换到plugins目录中去,这时目录中也存在elasticsearch-analysis-ik-7.4.2.zip这个压缩包
cd plugins/ #使用ls查看一下
#退出容器
exit
#在主机下解压elasticsearch-analysis-ik-7.4.2.zip这个压缩包
#zip的解压需要使用unzip解压命令 这里我们直接在本地解压一下 上传到plugins目录(解压的文件名为ik)
#安装unzip
yum install -y unzip
#查看ik分词器是否安装成功
docker exec -it 容器Id /bin/bash
#切换到bin目录
cd bin/
#执行下面的命令,有ik显示即为安装成功
elasticsearch-plugin list
#退出容器,重启一下
docker restart elasticsearch

测试使用

image-20230703152110823

4.2 自定义词库

1.安装nginx,用作设置ik的远程分词库

#创建nginx容器挂载的目录
mkdir /mydata/nginx/conf
#拉取nginx的镜像
docker pull nginx:1.10
#随便启动一个nginx实例,为了复制其配置
docker run -p 80:80 --name nginx -d nginx:1.10
#将容器内的配置文件拷贝到conf目录(注意这里有个点和空格)
#注意在nginx的目录下操作,(成功之后/mydata/nginx/conf目录下会有相应的配置文件和目录)
docker container cp nginx:/etc/nginx /mydata/nginx/conf
#停掉并删除之前的启动的nginx
docker stop nginx
docker rm nginx
#正式的启动容器
docker run -p 80:80 --name nginx \
-v /mydata/nginx/html:/usr/share/nginx/html \
-v /mydata/nginx/logs:/var/log/nginx \
-v /mydata/nginx/conf:/etc/nginx \
-d nginx:1.10
#设置开机自启
docker update nginx --restart=always

2.配置自定义的词库

#在nginx外部挂载的html目录下创建相应的目录
mkdir es
#创建分词需要使用的文本文件
vim fenci.txt
#在这个文本文件中加入我们需要添加的新词
#例如:奥里给 老铁之类的
#在浏览器上可以通过http://192.168.195.100/es/fenci.txt访问到里面的内容

3.配置es远程词库的地址

#切换到ik分词器配置文件所在的目录
cd elasticsearch/plugins/ik/config/
#修改配置文件,设置远程分词库的地址
vim IKAnalyzer.cfg.xml

image-20230703170719714

#重启es
docker restart elasticsearch

4.测试一下(这时我们使用网络中新颖的词就可以识别成一个词语)

image-20230703171420471

五.Elasticsearch-Rest-Client

1.环境配置

1.导入依赖

<!--指定版本-->
<properties>
<elasticsearch.version>7.4.2</elasticsearch.version>
</properties>
<!--ES的依赖-->
<dependency>
<groupId>org.elasticsearch.client</groupId>
<artifactId>elasticsearch-rest-high-level-client</artifactId>
<version>7.4.2</version>
</dependency>

2.编写配置文件


import org.apache.http.HttpHost;
import org.apache.http.HttpResponse;
import org.apache.http.nio.protocol.HttpAsyncResponseConsumer;
import org.elasticsearch.client.HttpAsyncResponseConsumerFactory;
import org.elasticsearch.client.RequestOptions;
import org.elasticsearch.client.RestClient;
import org.elasticsearch.client.RestHighLevelClient;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

/**
* @author Jason Gong
* @version 1.0
* @Date 2023/7/3
* @Description ES的配置文件
*/
@Configuration
public class ElasticSearchConfig {


//通用设置项
public static final RequestOptions COMMON_OPTIONS;
static {
RequestOptions.Builder builder = RequestOptions.DEFAULT.toBuilder();

COMMON_OPTIONS = builder.build();
}


@Bean
public RestHighLevelClient esRestClient() {
RestHighLevelClient client = new RestHighLevelClient(
RestClient.builder(
new HttpHost("192.168.195.100", 9200, "http")
)
);
return client;
}
}

3.测试使用

package com.atguigu.gulimall.search;

import org.elasticsearch.client.RestHighLevelClient;
import org.junit.jupiter.api.AfterAll;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;

@SpringBootTest
class GulimallSearchApplicationTests {

@Autowired
private RestHighLevelClient client;
@Test
void contextLoads() {
System.out.println(client);
}

}

2.代码操作ES

2.1 创建索引

/**
* 创建一个索引
*/
@Test
void testCreateIndex() throws IOException {
CreateIndexRequest request = new CreateIndexRequest("test_create");
CreateIndexResponse createIndexResponse = client.indices().create(request, RequestOptions.DEFAULT);
log.info("创建一个索引:{}",createIndexResponse);
}

2.2 判断索引是否存在

/**
* 判断索引是否存在
*/
@Test
void testExistIndex() throws IOException {
GetIndexRequest request = new GetIndexRequest("users");
boolean exists = client.indices().exists(request, RequestOptions.DEFAULT);
log.info("索引是否存在:{}",exists);
}

2.3 删除索引

/**
* 删除索引
*/
@Test
void testDeleteIndex() throws IOException {
DeleteIndexRequest request = new DeleteIndexRequest("test_create");
AcknowledgedResponse delete = client.indices().delete(request, RequestOptions.DEFAULT);
System.out.println(delete.isAcknowledged());
}

2.4 保存文档

 /**
* 测试向es中存储数据
* 保存和修改的操作
*/
@Test
void indexData() throws IOException {
//在users索引下存储数据
IndexRequest indexRequest = new IndexRequest("users");
//存储的数据的id
indexRequest.id("1");
//第一种方式
//indexRequest.source("userName","zhangSan","age",18,"gender","男");
//第二种方式
User user = new User("tom","男",22);
String jsonString = JSON.toJSONString(user);
indexRequest.source(jsonString, XContentType.JSON);
//执行真正的保存操作
//下面方法的第二个参数也可以使用这个参数GulimallElasticSearchConfig.COMMON_OPTIONS
IndexResponse index = client.index(indexRequest, RequestOptions.DEFAULT);
log.info("保存之后响应的数据:{}",index);
}

2.5 判断文档是否存在

/**
* 判断文档是否存在
*/
@Test
void testIsExists() throws IOException {
GetRequest getRequest = new GetRequest("users", "1");
getRequest.fetchSourceContext(new FetchSourceContext(false));
getRequest.storedFields("_none_");
boolean exists = client.exists(getRequest, RequestOptions.DEFAULT);
System.out.println(exists);
}

2.6 获取文档信息

/**
* 获取文档的信息
*/
@Test
void testGetDocument() throws IOException {
GetRequest getRequest = new GetRequest("users", "1");
GetResponse getResponse = client.get(getRequest, RequestOptions.DEFAULT);
System.out.println(getResponse.getSourceAsString());
System.out.println(getResponse);
}

2.7 修改文档信息

/**
* 更新文档的信息
*/
@Test
void testUpdateRequest() throws IOException {
UpdateRequest updateRequest = new UpdateRequest("users", "1");
updateRequest.timeout("1s");
User user = new User("修改之后的用户名", "男",23);
updateRequest.doc(JSON.toJSONString(user), XContentType.JSON);
UpdateResponse updateResponse = client.update(updateRequest, RequestOptions.DEFAULT);
System.out.println(updateResponse.status());
}

2.8 删除文档记录

/**
* 删除文档记录
*/
@Test
void testDeleteRequest() throws IOException {
DeleteRequest request = new DeleteRequest("users", "1");
request.timeout("1s");
DeleteResponse deleteResponse = client.delete(request, RequestOptions.DEFAULT);
System.out.println(deleteResponse.status());
}

2.9 批量添加

/**
* 批量添加文档
*/
@Test
void testBulkRequest() throws IOException {
BulkRequest bulkRequest = new BulkRequest();
bulkRequest.timeout("10s");
ArrayList<User> userList = new ArrayList<>();
userList.add(new User("A", "男",23));
userList.add(new User("B", "男",32));
userList.add(new User("C", "女",34));
userList.add(new User("D", "女",56));
userList.add(new User("E", "男",34));
userList.add(new User("F", "女",65));
// 批处理请求
for (int i = 0; i < userList.size(); i++) {
// 批量更新和批量删除,就在这里修改对应的请求就可以了
bulkRequest.add(new IndexRequest("users").id("" + (i + 1))
.source(JSON.toJSONString(userList.get(i)), XContentType.JSON));
}
BulkResponse bulkResponse = client.bulk(bulkRequest, RequestOptions.DEFAULT);
System.out.println("是否批量添加成功:"+!bulkResponse.hasFailures());
}

2.10 条件查询

/**
* 检索的方法
*/
@Test
void testSearchRequest() throws IOException {
//1.创建一个检索请求
SearchRequest searchRequest = new SearchRequest();
//设置检索的索引
searchRequest.indices("bank");
//指定DSL,检索条件
SearchSourceBuilder sourceBuilder = new SearchSourceBuilder();
sourceBuilder.query(QueryBuilders.matchQuery("address", "mill"));//查询地址中包含mill的数据
sourceBuilder.aggregation(AggregationBuilders.terms("ageAgg").field("age").size(10));//聚合名ageAgg,根据age字段聚合,只取10条
sourceBuilder.aggregation(AggregationBuilders.avg("balanceAvg").field("balance"));//求平均工资
System.out.println("检索的条件:" + sourceBuilder);
searchRequest.source(sourceBuilder);

//2.执行检索
SearchResponse searchResponse = client.search(searchRequest, RequestOptions.DEFAULT);

//3.分析检索的结果
System.out.println("查询到的数据:" + searchResponse.toString());
//Map map = JSON.parseObject(searchResponse.toString(), Map.class);//可以转换成一个map
SearchHits hits = searchResponse.getHits();//拿到命中的记录
for (SearchHit hit : hits) {
String index = hit.getIndex();//获取索引信息
String type = hit.getType();//类型信息
String id = hit.getId();//id信息
float score = hit.getScore();//得分信息
TimeValue took = searchResponse.getTook();
String sourceAsString = hit.getSourceAsString();//直接转化成对应的java对象
//转换成Account对象
Account account = JSON.parseObject(sourceAsString, Account.class);
System.out.println("Account:" + account);
}

//获取聚合分析的信息
//获取年龄的分段信息
Aggregations aggregations = searchResponse.getAggregations();
Terms ageAgg = aggregations.get("ageAgg");
for (Terms.Bucket bucket : ageAgg.getBuckets()) {

System.out.println("年龄:" + bucket.getKeyAsString() + "有" + bucket.getDocCount() + "人");
}
//获取薪资的平均值
Avg balanceAvg = aggregations.get("balanceAvg");
System.out.println("平均薪资:" + balanceAvg.getValue());
}

示例的完整代码

package com.atguigu.gulimall.search;

import com.alibaba.fastjson.JSON;
import com.atguigu.gulimall.search.config.GulimallElasticSearchConfig;
import lombok.Data;
import lombok.ToString;
import lombok.extern.slf4j.Slf4j;
import org.elasticsearch.action.admin.indices.delete.DeleteIndexRequest;
import org.elasticsearch.action.bulk.BulkRequest;
import org.elasticsearch.action.bulk.BulkResponse;
import org.elasticsearch.action.delete.DeleteRequest;
import org.elasticsearch.action.delete.DeleteResponse;
import org.elasticsearch.action.get.GetRequest;
import org.elasticsearch.action.get.GetResponse;
import org.elasticsearch.action.index.IndexRequest;
import org.elasticsearch.action.index.IndexResponse;
import org.elasticsearch.action.search.SearchRequest;
import org.elasticsearch.action.search.SearchResponse;
import org.elasticsearch.action.support.master.AcknowledgedResponse;
import org.elasticsearch.action.update.UpdateRequest;
import org.elasticsearch.action.update.UpdateResponse;
import org.elasticsearch.client.RequestOptions;
import org.elasticsearch.client.RestHighLevelClient;
import org.elasticsearch.client.indices.CreateIndexRequest;
import org.elasticsearch.client.indices.CreateIndexResponse;
import org.elasticsearch.client.indices.GetIndexRequest;
import org.elasticsearch.common.unit.TimeValue;
import org.elasticsearch.common.xcontent.XContentType;
import org.elasticsearch.index.query.QueryBuilders;
import org.elasticsearch.search.SearchHit;
import org.elasticsearch.search.SearchHits;
import org.elasticsearch.search.aggregations.Aggregation;
import org.elasticsearch.search.aggregations.AggregationBuilder;
import org.elasticsearch.search.aggregations.AggregationBuilders;
import org.elasticsearch.search.aggregations.Aggregations;
import org.elasticsearch.search.aggregations.bucket.terms.Terms;
import org.elasticsearch.search.aggregations.metrics.Avg;
import org.elasticsearch.search.builder.SearchSourceBuilder;
import org.elasticsearch.search.fetch.subphase.FetchSourceContext;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;

import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;

@Slf4j
@SpringBootTest
class GulimallSearchApplicationTests {

@Autowired
private RestHighLevelClient client;

@Test
void contextLoads() {
System.out.println(client);
}

/**
* 测试向es中存储数据
* 保存和修改的操作
*/
@Test
void indexData() throws IOException {
//在users索引下存储数据
IndexRequest indexRequest = new IndexRequest("users");
//存储的数据的id
indexRequest.id("1");
//第一种方式
//indexRequest.source("userName","zhangSan","age",18,"gender","男");
//第二种方式
User user = new User("tom", "男", 22);
String jsonString = JSON.toJSONString(user);
indexRequest.source(jsonString, XContentType.JSON);
//执行真正的保存操作
//下面方法的第二个参数也可以使用这个参数GulimallElasticSearchConfig.COMMON_OPTIONS
IndexResponse index = client.index(indexRequest, RequestOptions.DEFAULT);
log.info("保存之后响应的数据:{}", index);
}

/**
* 判断索引是否存在
*/
@Test
void testExistIndex() throws IOException {
GetIndexRequest request = new GetIndexRequest("users");
boolean exists = client.indices().exists(request, RequestOptions.DEFAULT);
log.info("索引是否存在:{}", exists);
}

/**
* 创建一个索引
*/
@Test
void testCreateIndex() throws IOException {
CreateIndexRequest request = new CreateIndexRequest("test_create");
CreateIndexResponse createIndexResponse = client.indices().create(request, RequestOptions.DEFAULT);
log.info("创建一个索引:{}", createIndexResponse);
}


/**
* 删除索引
*/
@Test
void testDeleteIndex() throws IOException {
DeleteIndexRequest request = new DeleteIndexRequest("users");
AcknowledgedResponse delete = client.indices().delete(request, RequestOptions.DEFAULT);
System.out.println(delete.isAcknowledged());
}


/**
* 判断文档是否存在
*/
@Test
void testIsExists() throws IOException {
GetRequest getRequest = new GetRequest("users", "1");
getRequest.fetchSourceContext(new FetchSourceContext(false));
getRequest.storedFields("_none_");
boolean exists = client.exists(getRequest, RequestOptions.DEFAULT);
System.out.println(exists);
}

/**
* 获取文档的信息
*/
@Test
void testGetDocument() throws IOException {
GetRequest getRequest = new GetRequest("users", "1");
GetResponse getResponse = client.get(getRequest, RequestOptions.DEFAULT);
System.out.println(getResponse.getSourceAsString());
System.out.println(getResponse);
}


/**
* 更新文档的信息
*/
@Test
void testUpdateRequest() throws IOException {
UpdateRequest updateRequest = new UpdateRequest("users", "1");
updateRequest.timeout("1s");
User user = new User("修改之后的用户名", "男", 23);
updateRequest.doc(JSON.toJSONString(user), XContentType.JSON);
UpdateResponse updateResponse = client.update(updateRequest, RequestOptions.DEFAULT);
System.out.println(updateResponse.status());
}

/**
* 删除文档记录
*/
@Test
void testDeleteRequest() throws IOException {
DeleteRequest request = new DeleteRequest("users", "1");
request.timeout("1s");
DeleteResponse deleteResponse = client.delete(request, RequestOptions.DEFAULT);
System.out.println(deleteResponse.status());
}

/**
* 批量添加文档
*/
@Test
void testBulkRequest() throws IOException {
BulkRequest bulkRequest = new BulkRequest();
bulkRequest.timeout("10s");
ArrayList<User> userList = new ArrayList<>();
userList.add(new User("A", "男", 23));
userList.add(new User("B", "男", 32));
userList.add(new User("C", "女", 34));
userList.add(new User("D", "女", 56));
userList.add(new User("E", "男", 34));
userList.add(new User("F", "女", 65));
// 批处理请求
for (int i = 0; i < userList.size(); i++) {
// 批量更新和批量删除,就在这里修改对应的请求就可以了
bulkRequest.add(new IndexRequest("users").id("" + (i + 1))
.source(JSON.toJSONString(userList.get(i)), XContentType.JSON));
}
BulkResponse bulkResponse = client.bulk(bulkRequest, RequestOptions.DEFAULT);
System.out.println("是否批量添加成功:" + !bulkResponse.hasFailures());
}


/**
* 检索的方法
*/
@Test
void testSearchRequest() throws IOException {
//1.创建一个检索请求
SearchRequest searchRequest = new SearchRequest();
//设置检索的索引
searchRequest.indices("bank");
//指定DSL,检索条件
SearchSourceBuilder sourceBuilder = new SearchSourceBuilder();
sourceBuilder.query(QueryBuilders.matchQuery("address", "mill"));//查询地址中包含mill的数据
sourceBuilder.aggregation(AggregationBuilders.terms("ageAgg").field("age").size(10));//聚合名ageAgg,根据age字段聚合,只取10条
sourceBuilder.aggregation(AggregationBuilders.avg("balanceAvg").field("balance"));//求平均工资
System.out.println("检索的条件:" + sourceBuilder);
searchRequest.source(sourceBuilder);

//2.执行检索
SearchResponse searchResponse = client.search(searchRequest, RequestOptions.DEFAULT);

//3.分析检索的结果
System.out.println("查询到的数据:" + searchResponse.toString());
//Map map = JSON.parseObject(searchResponse.toString(), Map.class);//可以转换成一个map
SearchHits hits = searchResponse.getHits();//拿到命中的记录
for (SearchHit hit : hits) {
String index = hit.getIndex();//获取索引信息
String type = hit.getType();//类型信息
String id = hit.getId();//id信息
float score = hit.getScore();//得分信息
TimeValue took = searchResponse.getTook();
String sourceAsString = hit.getSourceAsString();//直接转化成对应的java对象
//转换成Account对象
Account account = JSON.parseObject(sourceAsString, Account.class);
System.out.println("Account:" + account);
}

//获取聚合分析的信息
//获取年龄的分段信息
Aggregations aggregations = searchResponse.getAggregations();
Terms ageAgg = aggregations.get("ageAgg");
for (Terms.Bucket bucket : ageAgg.getBuckets()) {

System.out.println("年龄:" + bucket.getKeyAsString() + "有" + bucket.getDocCount() + "人");
}
//获取薪资的平均值
Avg balanceAvg = aggregations.get("balanceAvg");
System.out.println("平均薪资:" + balanceAvg.getValue());
}

@ToString
@Data
static class Account {
private int account_number;
private int balance;
private String firstname;
private String lastname;
private int age;
private String gender;
private String address;
private String employer;
private String email;
private String city;
private String state;
}


@Data
static class User {
private String userName;
private String gender;
private Integer age;

public User(String userName, String gender, Integer age) {
this.userName = userName;
this.gender = gender;
this.age = age;
}
}

}
]]>
后端 ElasticSearch
Java生成二维码 /posts/62439.html

SpringBoot + zxing 生成二维码

SpringBoot + qrcode生成二维码

原视频地址: Java生成二维码教程 | 两小时学会Java生成二维码

源码地址: JasonsGong/two-dimensional-code: 使用java生成二维码 (github.com)

一.谷歌zxing开源库生成二维码

1.创建一个sprinBoot项目

image-20230828215513289

2.引入相关的依赖

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

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

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

<dependency>
<groupId>com.google.zxing</groupId>
<artifactId>core</artifactId>
<version>3.1.0</version>
</dependency>

<dependency>
<groupId>com.google.zxing</groupId>
<artifactId>javase</artifactId>
<version>3.1.0</version>
</dependency>

<dependency>
<groupId>commons-lang</groupId>
<artifactId>commons-lang</artifactId>
<version>2.6</version>
</dependency>

<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
</dependencies>

3.在templates目录下新建index.html文件

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Title</title>
</head>
<body>

</body>
</html>

4.编写controller跳转到该页面,完成后重启项目,访问localhost:8080 测试

@Controller
public class CodeController {

@GetMapping("/")
public String index(){
return "index";
}
}

5.打开index.html,编写前端页面

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>二维码</title>
</head>
<body>
<h1>谷歌zxing开源库生成黑白二维码</h1>
<hr>
请输入文本内容:<input type="text" id="url"><button onclick="generateQRCode()">生成二维码</button><br>
<img id="qrCodeImg">
<script>
function generateQRCode(){
//获取url
let url = document.getElementById("url").value
//设置img标签的src属性
let qrCodeImg = document.getElementById("qrCodeImg")
qrCodeImg.src = "/generate?url="+ url
}
</script>
</body>
</html>

由于后端部分没有编写,这时我们访问项目的时候会返回404

image-20230828223201038

6.后端代码的编写

package com.mine.code.controller;

import com.google.zxing.BarcodeFormat;
import com.google.zxing.EncodeHintType;
import com.google.zxing.MultiFormatWriter;
import com.google.zxing.WriterException;
import com.google.zxing.aztec.encoder.AztecCode;
import com.google.zxing.common.BitMatrix;
import com.google.zxing.qrcode.decoder.ErrorCorrectionLevel;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.ResponseBody;

import javax.imageio.ImageIO;
import javax.servlet.ServletOutputStream;
import javax.servlet.http.HttpServletResponse;
import java.awt.image.BufferedImage;
import java.io.IOException;
import java.util.HashMap;
import java.util.Map;

/**
* @author Jason Gong
* @version 1.0
* @website https://jasonsgong.gitee.io
* @Date 2023/8/28
* @Description
*/
@Slf4j
@Controller
public class CodeController {

@GetMapping("/")
public String index() {
return "index";
}


@GetMapping("/generate")
public String generate(@RequestParam("url") String url, HttpServletResponse response) {
log.info("文本内容:{}", url);
try {
//创建一个map集合,存储二维码的相关属性
Map map = new HashMap<>();
//EncodeHintType 编码的提示类型
//设置二维码的误差校正级别 可选值有 L(7%) M(15%) Q(25%) H(30%)
//选择L级别的容错率,相当于允许二维码在整体的颜色区域中,最多有7%的坏像素点;择H级别的容错率,相当于允许二维码在整体的颜色区域中,最多有30%的坏像素点
map.put(EncodeHintType.ERROR_CORRECTION, ErrorCorrectionLevel.H);
//设置二维码的字符集
map.put(EncodeHintType.CHARACTER_SET, "utf-8");
//设置二维码四周的留白 1表示1像素
map.put(EncodeHintType.MARGIN, 1);
//创建zxing的核心对象MultiFormatWriter (多格式写入器)
//通过MultiFormatWriter对象来生成二维码
MultiFormatWriter writer = new MultiFormatWriter();
//参数一:内容
//参数二:二维码格式
//BarcodeFormat(码格式) QR_CODE :常见的二维码格式之一,广泛应用于商品包装、扫码支付
//AZTEC_CODE:高密度,可靠性很高 容错率更低 储存个人信息、证件信息、账户密码
//PDF417 可以存储大量的信息 数据密度高 应用于航空机票、配送标签、法律文件
//DATA_MATRIX: 小巧的二维码格式 编码格式类似于QR_CODE 但是优于QR_CODE 适合嵌入简单的产品标签 医疗图像 检测数据
//参数三四:二维码的宽度和高度
//参数五:二维码参数
//位矩阵对象 (位矩阵对象对象的内部实际上是一个二位数组,二维数组中每一个元素是boolean类型 true代表黑色 false代表白色)
BitMatrix bitMatrix = writer.encode(url, BarcodeFormat.QR_CODE, 300, 300, map);
//获取矩阵的宽度和高度
int width = bitMatrix.getWidth();
int height = bitMatrix.getHeight();
//生成二维码图片
BufferedImage image = new BufferedImage(width, height, BufferedImage.TYPE_INT_BGR);
//遍历位矩阵对象
for (int x = 0; x < width; x++) {
for (int y = 0; y < height; y++) {
//设置每一块的颜色值
//0xFF000000表示黑色 0xFFFFFFFF表示白色
image.setRGB(x, y, bitMatrix.get(x, y) ? 0xFF000000 : 0xFFFFFFFF);
}
}
//将图片响应到客户端
ServletOutputStream out = response.getOutputStream();
ImageIO.write(image,"png",out);
out.flush();
out.close();
} catch (Exception e) {
e.printStackTrace();
}
return null;
}
}

7.测试生成二维码

image-20230828233052417

完整的项目结构

image-20230828233917645

8.生成一个带Logo的黑白二维码

1.编写qrcode.html页面和跳转到该页面的controller

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>生成带logo的黑白二维码</title>
</head>
<body>
<form action="/generateWithLogo" method="post" enctype="multipart/form-data">
请输入文本内容:<input type="text" name="url"><br>
请选择logo图片:<input type="file" name="logo"><br>
<input type="submit" value="生成二维码">
</form>
</body>
</html>
/**
* 跳转到生成带logo的黑白二维码
*/
@GetMapping("/logo")
public String toLogo() {
return "qrcode";
}

2.编写后端生成二维码逻辑

/**
* 生成带logo的黑白二维码
*/
@PostMapping("/generateWithLogo")
public String generateWithLogo(@RequestParam("url") String url, HttpServletResponse response, HttpServletRequest request) {
try {
Map map = new HashMap<>();
map.put(EncodeHintType.ERROR_CORRECTION, ErrorCorrectionLevel.H);
map.put(EncodeHintType.CHARACTER_SET, "utf-8");
map.put(EncodeHintType.MARGIN, 1);
MultiFormatWriter writer = new MultiFormatWriter();
BitMatrix bitMatrix = writer.encode(url, BarcodeFormat.QR_CODE, 300, 300, map);
int width = bitMatrix.getWidth();
int height = bitMatrix.getHeight();
BufferedImage image = new BufferedImage(width, height, BufferedImage.TYPE_INT_BGR);
for (int x = 0; x < width; x++) {
for (int y = 0; y < height; y++) {
image.setRGB(x, y, bitMatrix.get(x, y) ? 0xFF000000 : 0xFFFFFFFF);
}
}
//给二维码添加logo
//1.获取logo
Part logoPart = request.getPart("logo");
InputStream inputStream = logoPart.getInputStream();
BufferedImage logoImage = ImageIO.read(inputStream);
//2.对获取的logo图片进行缩放
int logoWidth = logoImage.getWidth(null);
int logoHeight = logoImage.getHeight(null);
if (logoWidth > 60){
logoWidth = 60;
}
if (logoHeight > 60){
logoHeight = 60;
}
//使用平滑缩放算法对原始的logo图像进行缩放到一个全新的图像
Image scaledLogo = logoImage.getScaledInstance(logoWidth, logoHeight, Image.SCALE_SMOOTH);
//3.将缩放的图片画在黑白的二维码上
//获取一个画笔
Graphics2D graphics2D = image.createGraphics();
//计算从哪里开始画 300指的是二维码的宽度和高度
int x = (300 - logoWidth) /2;
int y = (300 - logoHeight) /2;
//画上去
graphics2D.drawImage(scaledLogo,x,y,null);
//实现logo的圆角效果
Shape shape = new RoundRectangle2D.Float(x, y, logoWidth, logoHeight, 10, 10);
//使用一个宽度为4像素的基本笔触
graphics2D.setStroke(new BasicStroke(4f));
//给logo画圆角矩形
graphics2D.draw(shape);
//释放画笔
graphics2D.dispose();
//将二维码响应到浏览器
ImageIO.write(image, "png", response.getOutputStream());
//关闭流
inputStream.close();
} catch (Exception e) {
e.printStackTrace();
}
return null;
}

访问 http://localhost:8080/logo 测试

实现的效果

image-20230829160901904

image-20230829160938795

二.github开源项目qrcode生成二维码

1.引入依赖文件

这里就不在重新创建工程了,直接在上面工程的基础上操作

<dependency>
<groupId>com.github.liuyueyi.media</groupId>
<artifactId>qrcode-plugin</artifactId>
<version>2.5.2</version>
</dependency>

2.生成黑白二维码

1.创建github-qrcode.html文件

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>qrcode生成黑白二维码</title>
</head>
<body>
<h1>使用github上的开源项目qrcode生成二维码</h1>
<hr>
<form action="/generateWithQrCode" method="post" enctype="multipart/form-data">
请输入文本内容:<input type="text" name="url"><br>
请选择logo图片:<input type="file" name="logo"><br>
<input type="submit" value="生成二维码">
</form>
</body>
</html>

2.编写controller处理请求

package com.mine.code.controller;

import com.github.hui.quick.plugin.qrcode.wrapper.QrCodeDeWrapper;
import com.github.hui.quick.plugin.qrcode.wrapper.QrCodeGenWrapper;
import com.google.zxing.WriterException;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestParam;

import javax.imageio.ImageIO;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.awt.image.BufferedImage;
import java.io.IOException;

/**
* @author Jason Gong
* @version 1.0
* @website https://jasonsgong.gitee.io
* @Date 2023/8/29
* @Description 使用qrcode生成二维码
*/
@Controller
public class GithubQrCodeController {
/**
* 编写请求跳转到使用qrcode生成二维码的页面
*/
@GetMapping("/qrcode")
public String toQrCode(){
return "github-qrcode";
}


/**
* 使用qrcode生成黑白二维码
*/
@PostMapping("/generateWithQrCode")
public String generateWithQrCode(@RequestParam("url") String url, HttpServletResponse response, HttpServletRequest request){
try {
//生成二维码
BufferedImage image = QrCodeGenWrapper.of(url).asBufferedImage();
//将生成的二维码响应到浏览器
ImageIO.write(image,"png",response.getOutputStream());
} catch (Exception e) {
e.printStackTrace();
}
return null;
}
}

3.访问测试

image-20230829223442421

3.生成带有logo的黑白二维码

前端页面延续使用上面的github-qrcode.html

1.后端代码的编写

package com.mine.code.controller;

import com.github.hui.quick.plugin.qrcode.wrapper.QrCodeDeWrapper;
import com.github.hui.quick.plugin.qrcode.wrapper.QrCodeGenWrapper;
import com.github.hui.quick.plugin.qrcode.wrapper.QrCodeOptions;
import com.google.zxing.WriterException;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestParam;

import javax.imageio.ImageIO;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.awt.image.BufferedImage;
import java.io.IOException;
import java.io.InputStream;

/**
* @author Jason Gong
* @version 1.0
* @website https://jasonsgong.gitee.io
* @Date 2023/8/29
* @Description 使用qrcode生成二维码
*/
@Controller
public class GithubQrCodeController {
/**
* 编写请求跳转到使用qrcode生成二维码的页面
*/
@GetMapping("/qrcode")
public String toQrCode(){
return "github-qrcode";
}


/**
* 使用qrcode生成黑白二维码
*/
@PostMapping("/generateWithQrCode")
public String generateWithQrCode(@RequestParam("url") String url, HttpServletResponse response, HttpServletRequest request){
try {
//生成二维码
//生成黑白的二维码
//BufferedImage image = QrCodeGenWrapper.of(url).asBufferedImage();
//生成带logo的黑白二维码
InputStream inputStream = request.getPart("logo").getInputStream();
BufferedImage image = QrCodeGenWrapper.of(url)
.setLogo(inputStream)//logo图片的输入流
.setLogoRate(7)//设置logo图片和二维码之间的比例,7表示logo的宽度等于二维码的1/7
.setLogoStyle(QrCodeOptions.LogoStyle.ROUND)//设置logo图片的样式,将logo的边框形状设置成圆形
.asBufferedImage();
//将生成的二维码响应到浏览器
ImageIO.write(image,"png",response.getOutputStream());
//关闭流
inputStream.close();
} catch (Exception e) {
e.printStackTrace();
}
return null;
}
}

实现的效果

image-20230829224339809

4.生成彩色的二维码

前端页面延续使用上面的github-qrcode.html

1.后端代码的编写

package com.mine.code.controller;

import com.github.hui.quick.plugin.qrcode.wrapper.QrCodeDeWrapper;
import com.github.hui.quick.plugin.qrcode.wrapper.QrCodeGenWrapper;
import com.github.hui.quick.plugin.qrcode.wrapper.QrCodeOptions;
import com.google.zxing.WriterException;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestParam;

import javax.imageio.ImageIO;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.awt.*;
import java.awt.image.BufferedImage;
import java.io.IOException;
import java.io.InputStream;

/**
* @author Jason Gong
* @version 1.0
* @website https://jasonsgong.gitee.io
* @Date 2023/8/29
* @Description 使用qrcode生成二维码
*/
@Controller
public class GithubQrCodeController {
/**
* 编写请求跳转到使用qrcode生成二维码的页面
*/
@GetMapping("/qrcode")
public String toQrCode(){
return "github-qrcode";
}


/**
* 使用qrcode生成黑白二维码
*/
@PostMapping("/generateWithQrCode")
public String generateWithQrCode(@RequestParam("url") String url, HttpServletResponse response, HttpServletRequest request){
try {
//生成彩色的二维码
BufferedImage image = QrCodeGenWrapper.of(url).setDrawPreColor(Color.GREEN).asBufferedImage();
//将生成的二维码响应到浏览器
ImageIO.write(image,"png",response.getOutputStream());
} catch (Exception e) {
e.printStackTrace();
}
return null;
}
}

实现的效果

image-20230829225017032

5.生成带有背景图的二维码

前端页面延续使用上面的github-qrcode.html

1.后端代码的编写

package com.mine.code.controller;

import com.github.hui.quick.plugin.qrcode.wrapper.QrCodeDeWrapper;
import com.github.hui.quick.plugin.qrcode.wrapper.QrCodeGenWrapper;
import com.github.hui.quick.plugin.qrcode.wrapper.QrCodeOptions;
import com.google.zxing.WriterException;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestParam;

import javax.imageio.ImageIO;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.awt.*;
import java.awt.image.BufferedImage;
import java.io.IOException;
import java.io.InputStream;

/**
* @author Jason Gong
* @version 1.0
* @website https://jasonsgong.gitee.io
* @Date 2023/8/29
* @Description 使用qrcode生成二维码
*/
@Controller
public class GithubQrCodeController {
/**
* 编写请求跳转到使用qrcode生成二维码的页面
*/
@GetMapping("/qrcode")
public String toQrCode(){
return "github-qrcode";
}


/**
* 使用qrcode生成黑白二维码
*/
@PostMapping("/generateWithQrCode")
public String generateWithQrCode(@RequestParam("url") String url, HttpServletResponse response, HttpServletRequest request){
try {
//生成带有背景图的二维码
InputStream inputStream = request.getPart("logo").getInputStream();
BufferedImage image = QrCodeGenWrapper.of(url)
.setBgImg(inputStream)
.setBgOpacity(0.5F)//设置透明度
.asBufferedImage();
//将生成的二维码响应到浏览器
ImageIO.write(image,"png",response.getOutputStream());
//关闭流
inputStream.close();
} catch (Exception e) {
e.printStackTrace();
}
return null;
}
}

实现的效果

image-20230829225740156

6.生成特殊形状的二维码

前端页面延续使用上面的github-qrcode.html

1.后端代码的编写

package com.mine.code.controller;

import com.github.hui.quick.plugin.qrcode.wrapper.QrCodeDeWrapper;
import com.github.hui.quick.plugin.qrcode.wrapper.QrCodeGenWrapper;
import com.github.hui.quick.plugin.qrcode.wrapper.QrCodeOptions;
import com.google.zxing.WriterException;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestParam;

import javax.imageio.ImageIO;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.awt.*;
import java.awt.image.BufferedImage;
import java.io.IOException;
import java.io.InputStream;

/**
* @author Jason Gong
* @version 1.0
* @website https://jasonsgong.gitee.io
* @Date 2023/8/29
* @Description 使用qrcode生成二维码
*/
@Controller
public class GithubQrCodeController {
/**
* 编写请求跳转到使用qrcode生成二维码的页面
*/
@GetMapping("/qrcode")
public String toQrCode(){
return "github-qrcode";
}


/**
* 使用qrcode生成黑白二维码
*/
@PostMapping("/generateWithQrCode")
public String generateWithQrCode(@RequestParam("url") String url, HttpServletResponse response, HttpServletRequest request){
try {
//生成特殊形状的二维码
BufferedImage image = QrCodeGenWrapper.of(url)
.setDrawEnableScale(true)//启用二维码绘制时的缩放功能
.setDrawStyle(QrCodeOptions.DrawStyle.DIAMOND)//绘制钻石形状的二维码
.asBufferedImage();
//将生成的二维码响应到浏览器
ImageIO.write(image,"png",response.getOutputStream());
} catch (Exception e) {
e.printStackTrace();
}
return null;
}
}

实现的效果

image-20230829230236500

7.生成图片填充二维码

前端页面延续使用上面的github-qrcode.html

1.后端代码的编写

package com.mine.code.controller;

import com.github.hui.quick.plugin.qrcode.wrapper.QrCodeDeWrapper;
import com.github.hui.quick.plugin.qrcode.wrapper.QrCodeGenWrapper;
import com.github.hui.quick.plugin.qrcode.wrapper.QrCodeOptions;
import com.google.zxing.WriterException;
import com.google.zxing.qrcode.decoder.ErrorCorrectionLevel;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestParam;

import javax.imageio.ImageIO;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.awt.*;
import java.awt.image.BufferedImage;
import java.io.IOException;
import java.io.InputStream;

/**
* @author Jason Gong
* @version 1.0
* @website https://jasonsgong.gitee.io
* @Date 2023/8/29
* @Description 使用qrcode生成二维码
*/
@Controller
public class GithubQrCodeController {
/**
* 编写请求跳转到使用qrcode生成二维码的页面
*/
@GetMapping("/qrcode")
public String toQrCode() {
return "github-qrcode";
}


/**
* 使用qrcode生成黑白二维码
*/
@PostMapping("/generateWithQrCode")
public String generateWithQrCode(@RequestParam("url") String url, HttpServletResponse response, HttpServletRequest request) {
try {
//生成图片填充二维码
InputStream inputStream = request.getPart("logo").getInputStream();
BufferedImage image = QrCodeGenWrapper.of(url)
.setErrorCorrection(ErrorCorrectionLevel.H)//设置二维码的纠正级别
.setDrawStyle(QrCodeOptions.DrawStyle.IMAGE)//绘制二维码的时候采用图片填充
.addImg(1, 1, inputStream)//添加图片
.asBufferedImage();
//将生成的二维码响应到浏览器
ImageIO.write(image, "png", response.getOutputStream());
//关闭流
inputStream.close();
} catch (Exception e) {
e.printStackTrace();
}
return null;
}
}

实现的效果

二维码中的每一个像素点都是上传的图片,生成的速度很慢

image-20230829230843477

]]>
后端 二维码
FreeMarker模板引擎 /posts/29367.html 一.FreeMarker介绍

​ FreeMarker 是一款 模板引擎: 即一种基于模板和要改变的数据, 并用来生成输出文本(HTML网页,电子邮件,配置文件,源代码等)的通用工具。 它不是面向最终用户的,而是一个Java类库,是一款程序员可以嵌入他们所开发产品的组件。

​ 模板编写为FreeMarker Template Language (FTL)。它是简单的,专用的语言, 不是 像PHP那样成熟的编程语言。 那就意味着要准备数据在真实编程语言中来显示,比如数据库查询和业务运算, 之后模板显示已经准备好的数据。在模板中,你可以专注于如何展现数据, 而在模板之外可以专注于要展示什么数据。

image-20230816145012534

技术选型对比

技术 说明
Jsp Jsp 为 Servlet 专用,不能单独进行使用
Velocity Velocity从2010年更新完 2.0 版本后,7年没有更新。Spring Boot 官方在 1.4 版本后对此也不在支持
thmeleaf 新技术,功能较为强大,但是执行的效率比较低
freemarker 性能好,强大的模板语言、轻量

二.快速入门

1.环境搭建

1.1 创建一个demo工程

此处为了方便建的是子模块,测试使用可以创建一个单独的工程

image-20230816145923876

1.2 引入依赖

<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-freemarker</artifactId>
</dependency>

<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
</dependency>
<!-- lombok -->
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
</dependency>

<!-- apache 对 java io 的封装工具库 -->
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-io</artifactId>
<version>1.3.2</version>
</dependency>
</dependencies>

1.3 添加配置

server:
port: 8881 #服务端口
spring:
application:
name: freemarker-demo #指定服务名
freemarker:
cache: false #关闭模板缓存,方便测试
settings:
template_update_delay: 0 #检查模板更新延迟时间,设置为0表示立即检查,如果时间大于0会有缓存不方便进行模板测试
suffix: .ftl #指定Freemarker模板文件的后缀名,通常都是以ftl作为扩展名,也可以为html、xml、jsp等!

1.4 创建模板

在resources下创建templates,此目录为freemarker的默认模板存放目录

image-20230816150936881

在templates下创建模板文件 01-basic.ftl ,模板中的插值表达式最终会被freemarker替换成具体的数据

<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>Hello World!</title>
</head>
<body>
<b>普通文本 String 展示:</b><br><br>
Hello ${name} <br>
<hr>
<b>对象Student中的数据展示:</b><br/>
姓名:${stu.name}<br/>
年龄:${stu.age}
<hr>
</body>
</html>

1.5 创建启动类和模型类

package com.heima.freemarker;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;

/**
* @author Jason Gong
* @version 1.0
* @website https://jasonsgong.gitee.io
* @Date 2023/8/16
* @Description
*/
@SpringBootApplication
public class FreeMarkerDemoApplication {
public static void main(String[] args) {
SpringApplication.run(FreeMarkerDemoApplication.class,args);
}
}
package com.heima.freemarker.entity;

import lombok.Data;

import java.util.Date;

/**
* @author Jason Gong
* @version 1.0
* @website https://jasonsgong.gitee.io
* @Date 2023/8/16
* @Description
*/
@Data
public class Student {
private String name;//姓名
private int age;//年龄
private Date birthday;//生日
private Float money;//钱包
}

1.6 创建Controller

package com.heima.freemarker.controller;

import com.heima.freemarker.entity.Student;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.GetMapping;

/**
* @author Jason Gong
* @version 1.0
* @website https://jasonsgong.gitee.io
* @Date 2023/8/16
* @Description
*/
@Controller
public class HelloController {

@GetMapping("/basic")
public String test(Model model) {
//1.纯文本形式的参数
model.addAttribute("name", "freemarker");

//2.实体类相关的参数
Student student = new Student();
student.setName("小明");
student.setAge(18);
model.addAttribute("stu", student);

return "01-basic";
}
}

1.7 测试访问

访问 http://localhost:8881/basic

image-20230816152750639

三.FreeMarker语法基础

1.基础语法种类

1、注释,即<#– –>,介于其之间的内容会被freemarker忽略

<#--我是一个freemarker注释-->

2、插值(Interpolation):即 ${..} 部分,freemarker会用真实的值代替**${..}**

Hello ${name}

3、FTL指令:和HTML标记类似,名字前加#予以区分,Freemarker会解析标签中的表达式或逻辑。

<# >FTL指令</#> 

4、文本,仅文本信息,这些不是freemarker的注释、插值、FTL指令的内容会被freemarker忽略解析,直接输出内容。

<#--freemarker中的普通文本-->
我是一个普通的文本

2.集合指令

image-20230816170028648

image-20230816170704421

1、数据模型:

在HelloController中新增如下方法:

@GetMapping("/list")
public String list(Model model){

//------------------------------------
Student stu1 = new Student();
stu1.setName("小强");
stu1.setAge(18);
stu1.setMoney(1000.86f);
stu1.setBirthday(new Date());

//小红对象模型数据
Student stu2 = new Student();
stu2.setName("小红");
stu2.setMoney(200.1f);
stu2.setAge(19);

//将两个对象模型数据存放到List集合中
List<Student> stus = new ArrayList<>();
stus.add(stu1);
stus.add(stu2);

//向model中存放List集合数据
model.addAttribute("stus",stus);

//------------------------------------

//创建Map数据
HashMap<String,Student> stuMap = new HashMap<>();
stuMap.put("stu1",stu1);
stuMap.put("stu2",stu2);
// 3.1 向model中存放Map数据
model.addAttribute("stuMap", stuMap);

return "02-list";
}

2、模板:

在templates中新增02-list.ftl文件

<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>Hello World!</title>
</head>
<body>

<#-- list 数据的展示 -->
<b>展示list中的stu数据:</b>
<br>
<br>
<table>
<tr>
<td>序号</td>
<td>姓名</td>
<td>年龄</td>
<td>钱包</td>
</tr>
</table>
<hr>

<#-- Map 数据的展示 -->
<b>map数据的展示:</b>
<br/><br/>
<a href="###">方式一:通过map['keyname'].property</a><br/>
输出stu1的学生信息:<br/>
姓名:<br/>
年龄:<br/>
<br/>
<a href="###">方式二:通过map.keyname.property</a><br/>
输出stu2的学生信息:<br/>
姓名:<br/>
年龄:<br/>

<br/>
<a href="###">遍历map中两个学生信息:</a><br/>
<table>
<tr>
<td>序号</td>
<td>姓名</td>
<td>年龄</td>
<td>钱包</td>
</tr>
</table>
<hr>

</body>
</html>

实例代码:

<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>Hello World!</title>
</head>
<body>

<#-- list 数据的展示 -->
<b>展示list中的stu数据:</b>
<br>
<br>
<table>
<tr>
<td>序号</td>
<td>姓名</td>
<td>年龄</td>
<td>钱包</td>
</tr>
<#list stus as stu>
<tr>
<td>${stu_index+1}</td>
<td>${stu.name}</td>
<td>${stu.age}</td>
<td>${stu.money}</td>
</tr>
</#list>

</table>
<hr>

<#-- Map 数据的展示 -->
<b>map数据的展示:</b>
<br/><br/>
<a href="###">方式一:通过map['keyname'].property</a><br/>
输出stu1的学生信息:<br/>
姓名:${stuMap['stu1'].name}<br/>
年龄:${stuMap['stu1'].age}<br/>
<br/>
<a href="###">方式二:通过map.keyname.property</a><br/>
输出stu2的学生信息:<br/>
姓名:${stuMap.stu2.name}<br/>
年龄:${stuMap.stu2.age}<br/>

<br/>
<a href="###">遍历map中两个学生信息:</a><br/>
<table>
<tr>
<td>序号</td>
<td>姓名</td>
<td>年龄</td>
<td>钱包</td>
</tr>
<#list stuMap?keys as key >
<tr>
<td>${key_index}</td>
<td>${stuMap[key].name}</td>
<td>${stuMap[key].age}</td>
<td>${stuMap[key].money}</td>
</tr>
</#list>
</table>
<hr>

</body>
</html>

上面代码解释:

${k_index}:index:得到循环的下标,使用方法是在stu后边加”_index”,它的值是从0开始,${stu_index+1}下标从1开始

展示效果:

image-20230816170550343

3.if指令

image-20230816170909497

if 指令即判断指令,是常用的FTL指令,freemarker在解析时遇到if会进行判断,条件为真则输出if中间的内容,否则跳过内容不再输出。

  • 指令格式
<#if ></if>

1、数据模型:

使用list指令中测试数据模型,判断名称为小红的数据字体显示为红色。

2、模板:

<table>
<tr>
<td>姓名</td>
<td>年龄</td>
<td>钱包</td>
</tr>
<#list stus as stu>
<tr>
<td >${stu.name}</td>
<td>${stu.age}</td>
<td >${stu.mondy}</td>
</tr>
</#list>

</table>

实例代码:

<table>
<tr>
<td>姓名</td>
<td>年龄</td>
<td>钱包</td>
</tr>
<#list stus as stu >
<#if stu.name='小红'>
<tr style="color: red">
<td>${stu_index}</td>
<td>${stu.name}</td>
<td>${stu.age}</td>
<td>${stu.money}</td>
</tr>
<#else >
<tr>
<td>${stu_index}</td>
<td>${stu.name}</td>
<td>${stu.age}</td>
<td>${stu.money}</td>
</tr>
</#if>
</#list>
</table>

3、输出:

image-20230816171119081

4.运算符

1、算数运算符

FreeMarker表达式中完全支持算术运算,FreeMarker支持的算术运算符包括:

  • 加法: +
  • 减法: -
  • 乘法: *
  • 除法: /
  • 求模 (求余): %

模板代码

<b>算数运算符</b>
<br/><br/>
100+5 运算: ${100 + 5 }<br/>
100 - 5 * 5运算:${100 - 5 * 5}<br/>
5 / 2运算:${5 / 2}<br/>
12 % 10运算:${12 % 10}<br/>
<hr>

除了 + 运算以外,其他的运算只能和 number 数字类型的计算。

2、比较运算符

  • =或者==:判断两个值是否相等.
  • !=:判断两个值是否不等.
  • >或者gt:判断左边值是否大于右边值
  • >=或者gte:判断左边值是否大于等于右边值
  • <或者lt:判断左边值是否小于右边值
  • <=或者lte:判断左边值是否小于等于右边值

= 和 == 模板代码

<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>Hello World!</title>
</head>
<body>

<b>比较运算符</b>
<br/>
<br/>

<dl>
<dt> =/== 和 != 比较:</dt>
<dd>
<#if "xiaoming" == "xiaoming">
字符串的比较 "xiaoming" == "xiaoming"
</#if>
</dd>
<dd>
<#if 10 != 100>
数值的比较 10 != 100
</#if>
</dd>
</dl>



<dl>
<dt>其他比较</dt>
<dd>
<#if 10 gt 5 >
形式一:使用特殊字符比较数值 10 gt 5
</#if>
</dd>
<dd>
<#-- 日期的比较需要通过?date将属性转为data类型才能进行比较 -->
<#if (date1?date >= date2?date)>
形式二:使用括号形式比较时间 date1?date >= date2?date
</#if>
</dd>
</dl>

<br/>
<hr>
</body>
</html>

Controller 的 数据模型代码

@GetMapping("operation")
public String testOperation(Model model) {
//构建 Date 数据
Date now = new Date();
model.addAttribute("date1", now);
model.addAttribute("date2", now);

return "03-operation";
}

比较运算符注意

  • =!=可以用于字符串、数值和日期来比较是否相等
  • =!=两边必须是相同类型的值,否则会产生错误
  • 字符串 "x""x ""X"比较是不等的.因为FreeMarker是精确比较
  • 其它的运行符可以作用于数字和日期,但不能作用于字符串
  • 使用gt等字母运算符代替>会有更好的效果,因为 FreeMarker会把>解释成FTL标签的结束字符
  • 可以使用括号来避免这种情况,如:<#if (x>y)>

3、逻辑运算符

  • 逻辑与:&&
  • 逻辑或:||
  • 逻辑非:!

逻辑运算符只能作用于布尔值,否则将产生错误 。

模板代码

<b>逻辑运算符</b>
<br/>
<br/>
<#if (10 lt 12 )&&( 10 gt 5 ) >
(10 lt 12 )&&( 10 gt 5 ) 显示为 true
</#if>
<br/>
<br/>
<#if !false>
false 取反为true
</#if>
<hr>

5.空值处理

1、判断某变量是否存在使用 “??”

用法为:variable??,如果该变量存在,返回true,否则返回false

例:为防止stus为空报错可以加上判断如下:

<#if stus??>
<#list stus as stu>
......
</#list>
</#if>

2、缺失变量默认值使用 “!”

  • 使用!要以指定一个默认值,当变量为空时显示默认值

    例: ${name!’’}表示如果name为空显示空字符串。

  • 如果是嵌套对象则建议使用()括起来

    例: ${(stu.bestFriend.name)!’’}表示,如果stu或bestFriend或name为空默认显示空字符串。

6.内建函数

内建函数语法格式: 变量+?+函数名称

1、某个集合的大小

${集合名?size}

2、日期格式化

显示年月日: ${today?date}

显示时分秒:${today?time}

显示日期+时间:${today?datetime}

自定义格式化: ${today?string("yyyy年MM月")}

3、内建函数c

model.addAttribute(“point”, 102920122);

point是数字型,使用${point}会显示这个数字的值,每三位使用逗号分隔。

如果不想显示为每三位分隔的数字,可以使用c函数将数字型转成字符串输出

${point?c}

4、将json字符串转成对象

一个例子:

其中用到了 assign标签,assign的作用是定义一个变量。

<#assign text="{'bank':'工商银行','account':'10101920201920212'}" />
<#assign data=text?eval />
开户行:${data.bank} 账号:${data.account}

模板代码:

<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>inner Function</title>
</head>
<body>

<b>获得集合大小</b><br>

集合大小:
<hr>


<b>获得日期</b><br>

显示年月日: <br>

显示时分秒:<br>

显示日期+时间:<br>

自定义格式化: <br>

<hr>

<b>内建函数C</b><br>
没有C函数显示的数值: <br>

有C函数显示的数值:

<hr>

<b>声明变量assign</b><br>


<hr>
</body>
</html>

内建函数模板页面:

<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>inner Function</title>
</head>
<body>

<b>获得集合大小</b><br>

集合大小:${stus?size}
<hr>


<b>获得日期</b><br>

显示年月日: ${today?date} <br>

显示时分秒:${today?time}<br>

显示日期+时间:${today?datetime}<br>

自定义格式化: ${today?string("yyyy年MM月")}<br>

<hr>

<b>内建函数C</b><br>
没有C函数显示的数值:${point} <br>

有C函数显示的数值:${point?c}

<hr>

<b>声明变量assign</b><br>
<#assign text="{'bank':'工商银行','account':'10101920201920212'}" />
<#assign data=text?eval />
开户行:${data.bank} 账号:${data.account}

<hr>
</body>
</html>

内建函数Controller数据模型:

@GetMapping("innerFunc")
public String testInnerFunc(Model model) {
//1.1 小强对象模型数据
Student stu1 = new Student();
stu1.setName("小强");
stu1.setAge(18);
stu1.setMoney(1000.86f);
stu1.setBirthday(new Date());
//1.2 小红对象模型数据
Student stu2 = new Student();
stu2.setName("小红");
stu2.setMoney(200.1f);
stu2.setAge(19);
//1.3 将两个对象模型数据存放到List集合中
List<Student> stus = new ArrayList<>();
stus.add(stu1);
stus.add(stu2);
model.addAttribute("stus", stus);
// 2.1 添加日期
Date date = new Date();
model.addAttribute("today", date);
// 3.1 添加数值
model.addAttribute("point", 102920122);
return "04-innerFunc";
}

四.静态化测试

1.思路分析

image-20230816172633599

2.实现步骤

根据模板文件生成html文件

①:修改application.yml文件,添加以下模板存放位置的配置信息,完整配置如下:

server:
port: 8881 #服务端口
spring:
application:
name: freemarker-demo #指定服务名
freemarker:
cache: false #关闭模板缓存,方便测试
settings:
template_update_delay: 0 #检查模板更新延迟时间,设置为0表示立即检查,如果时间大于0会有缓存不方便进行模板测试
suffix: .ftl #指定Freemarker模板文件的后缀名
template-loader-path: classpath:/templates #模板存放位置

②:在test下创建测试类

package com.heima.freemarker.test;

import com.heima.freemarker.FreeMarkerDemoApplication;
import com.heima.freemarker.entity.Student;
import freemarker.template.Configuration;
import freemarker.template.Template;
import freemarker.template.TemplateException;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.test.context.junit4.SpringRunner;

import java.io.FileWriter;
import java.io.IOException;
import java.util.HashMap;
import java.util.Map;

/**
* @author Jason Gong
* @version 1.0
* @website https://jasonsgong.gitee.io
* @Date 2023/8/16
* @Description
*/
@SpringBootTest(classes = FreeMarkerDemoApplication.class)
@RunWith(SpringRunner.class)
public class FreeMarkerTest {
@Autowired
//freemarker.template.Configuration
private Configuration configuration;

@Test
public void test() throws IOException, TemplateException {
Template template = configuration.getTemplate("01-basic.ftl");
/**
* 第一个参数:数据模型
* 第二个参数:输出流
*/
template.process(getData(),new FileWriter("C:\\Gong\\data\\test.html"));

}

/**
* 构建数据模型
*/
private Map<String,Object> getData(){
HashMap<String, Object> map = new HashMap<>();
//1.纯文本形式的参数
map.put("name", "freemarker");
//2.实体类相关的参数
Student student = new Student();
student.setName("小明");
student.setAge(18);
map.put("stu", student);
return map;
}
}

③ :运行结果

image-20230816174550953

]]>
前端 FreeMarker
Linux中开发环境的搭建 /posts/20683.html 一.Linux操作系统的安装与配置

软件:VMware 、Linux镜像文件
VMWare虚拟机安装Linux教程 | The Blog (gitee.io)

二.配置Java环境

2.1.删除自带的JDK

#查看系统是否有java环境
java -version
# 查找Linux中已有的java环境的相关文件
rpm -qa |grep java
# 执行命令 删除上面的java文件
rpm -e --nodeps +上面查找出来的文件

2.2.安装JDK

#创建一个文件夹,将JDK8上传到这个文件夹中去
mkdir /opt/jdk
# 解压上传的压缩包 解压后得到一个名为jdk1.8.0_261的文件夹
tar -zvxf jdk-8u261-linux-x64.tar.gz
# 创建一个目录,作为java的安装目录
mkdir /usr/local/java
#将解压后的文件移动到这个安装目录中去
mv jdk1.8.0_261/ /usr/local/java/
#配置环境变量
#进入环境变量的配置文件中
vim /etc/profile
#在文件的最后添加如下的配置
export JAVA_HOME=/usr/local/java/jdk1.8.0_261
export PATH=$JAVA_HOME/bin:$PATH
#刷新配置文件,让新的环境变量生效
source /etc/profile

三.使用Docker安装常用软件

Docker容器化技术 | The Blog (gitee.io)

3.1 Docker的安装

3.1.1 安装Docker

# 1、yum 包更新到最新 
yum update
# 2、安装需要的软件包, yum-util 提供yum-config-manager功能,另外两个是devicemapper驱动依赖的
yum install -y yum-utils device-mapper-persistent-data lvm2
# 3、 设置yum源
yum-config-manager --add-repo https://download.docker.com/linux/centos/docker-ce.repo
# 4、 安装docker,出现输入的界面都按 y
yum install -y docker-ce
# 5、 查看docker版本,验证是否验证成功
docker -v

3.1.2 配置镜像加速服务

# 1.创建相关的目录
sudo mkdir -p /etc/docker
# 2,加入配置
sudo tee /etc/docker/daemon.json <<-'EOF'
{
"registry-mirrors": ["https://cagfgua5.mirror.aliyuncs.com"]
}
EOF
# 3.重新加载,让配置文件生效
sudo systemctl daemon-reload
# 4.重新启动Docker服务
sudo systemctl restart docker

3.1.3 启动Docker服务

# 启动docker服务:
systemctl start docker

# 停止docker服务:
systemctl stop docker

#重启docker服务:
systemctl restart docker

# 查看docker服务状态:
systemctl status docker

# 设置开机启动docker服务:
systemctl enable docker

3.2 使用Docker安装Mysql

#下载mysql的镜像文件 这里以下载mysql5.7为例
docker pull mysql:5.7

#查看是否下载成功
docker images

#使用MySQL的镜像启动一个容器
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/conf:/etc/mysql:将配置文件夹挂载到主机
#-v /mydata/mysql/log:/var/log/mysql:将日志文件夹挂载到主机
#-v /mydata/mysql/data:/var/lib/mysql/:将配置文件夹挂载到主机
#-e MYSQL_ROOT_PASSWORD=root:初始化 root 用户的密码

#查看是否运行成功 这时使用Navicat就可以连接上了 连接不上 关闭防火墙
docker ps #查看正在运行的容器

#进入容器的内部
docker exec -it mysql /bin/bash

#查看容器内部的目录结构 我们可以看到容器的内部是一个小小的linux系统
ls / #注意中间有一个空格

#查看与mysql相关的目录
whereis mysql

#退出容器
exit;

#在容器外部查看mysql挂载在本地的三个文件
#容器内部文件的变化会同步在这三个文件中 conf data log
#外部修改 也会同步到内部
cd /mydata/mysql
ls

#创建一个mysql的配置文件设置字符编码 将下面的内容粘贴到配置文件中
#1.创建配置文件
vi /mydata/mysql/conf/my.cnf
#2.将以下的内容粘贴到配置文件配置文件内容
[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
#3.重启MySQl的服务 让配置文件生效 这个配置文件也会同步到容器内部
docker restart mysql

#设置开机自启动
docker update mysql --restart=always

3.3 使用Docker安装Redis

#下载redis的镜像文件 直接下载最新的
docker pull redis

#创建redis挂载在宿主机的配置文件和目录
mkdir -p /mydata/redis/conf #创建目录
touch /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 ps

#测试客户端的使用
docker exec -it redis redis-cli

#可以在redis客户端中使用如下命令
127.0.0.1:6379> set name tom #存值
OK
127.0.0.1:6379> get name #取值
"tom"

#redis中的数据不能持久化 关机重启之后数据会丢失
#通过以下的配置 实现redis的持久化操作 AOF的持久化
vi /mydata/redis/conf/redis.conf #修改之前创建的配置文件
#添加如下配置
appendonly yes

#重启redis 让配置生效
docker restart redis

#设置开机自启动
docker update redis --restart=always

3.4 使用Docker安装ElasticSearch

3.4.1 安装ElasticSearch

# 拉取ElasticSearch
docker pull elasticsearch:7.4.2

#ES的配置文件存放的位置
mkdir -p /mydata/elasticsearch/config

#ES相关的数据
mkdir -p /mydata/elasticsearch/data

#"http.host: 0.0.0.0"(可以被任何的机器访问)的配置写入elasticsearch.yml中(注意冒号后面的空格)
echo "http.host: 0.0.0.0" >> /mydata/elasticsearch/config/elasticsearch.yml

#设置文件的权限
# 递归更改权限,es需要访问
chmod -R 777 /mydata/elasticsearch/

#运行容器的命令 9200端口用于发送请求使用 9300端口用于集群中节点中的通信使用
#单节点运行
#初始占用64m,最大占用5m(不指定,ES会占用所有的内存)
docker run --name elasticsearch -p 9200:9200 -p 9300:9300 \
-e "discovery.type=single-node" \
-e ES_JAVA_OPTS="-Xms64m -Xmx512m" \
-v /mydata/elasticsearch/config/elasticsearch.yml:/usr/share/elasticsearch/config/elasticsearch.yml \
-v /mydata/elasticsearch/data:/usr/share/elasticsearch/data \
-v /mydata/elasticsearch/plugins:/usr/share/elasticsearch/plugins \
-d elasticsearch:7.4.2

# 设置开机启动elasticsearch
docker update elasticsearch --restart=always

#访问端口 测试是否安装成功
http://虚拟机的ip:9200/

安装成功之后:

image-20230628191936254

3.4.2 安装Kibana(可视化界面)

#拉取Kibana
docker pull kibana:7.4.2
# 这里-e是自己的elasticsearch服务地址(这里的地址一定要改为自己虚拟机的地址)
docker run --name kibana -e ELASTICSEARCH_HOSTS=http://192.168.195.100:9200 -p 5601:5601 -d kibana:7.4.2
# 设置开机启动kibana
docker update kibana --restart=always
#访问对应的网址查看服务
http://虚拟机的ip:5601

安装成功之后

image-20230701104616032

3.4.3 安装IK分词器

安装地址: https://github.com/medcl/elasticsearch-analysis-ik

#切换到es挂载在注解的plugins目录
cd /mydata/elasticsearch/plugins/
#安装wget
yum install wget
#下载ik分词器的压缩包(下载的速度不是很快(5分钟左右),建议本地下载好了之后,上传上去)
wget https://github.com/medcl/elasticsearch-analysis-ik/releases/download/v7.4.2/elasticsearch-analysis-ik-7.4.2.zip
#进入容器内部查看一下文件是否同步到了容器的内部
docker exec -it 容器Id /bin/bash
#切换到plugins目录中去,这时目录中也存在elasticsearch-analysis-ik-7.4.2.zip这个压缩包
cd plugins/ #使用ls查看一下
#退出容器
exit
#在主机下解压elasticsearch-analysis-ik-7.4.2.zip这个压缩包
#zip的解压需要使用unzip解压命令 这里我们可以直接在本地解压一下 上传到plugins目录(解压的文件名为ik)
#安装unzip
yum install -y unzip
#查看ik分词器是否安装成功
docker exec -it 容器Id /bin/bash
#切换到bin目录
cd bin/
#执行下面的命令,有ik显示即为安装成功
elasticsearch-plugin list
#退出容器,重启一下
docker restart elasticsearch

3.5 使用Docker安装nginx

方法一:

#创建nginx容器挂载的目录
mkdir /mydata/nginx/conf
#拉取nginx的镜像
docker pull nginx:1.10
#随便启动一个nginx实例,为了复制其配置
docker run -p 80:80 --name nginx -d nginx:1.10
#将容器内的配置文件拷贝到conf目录(注意这里有个点和空格)
#注意在nginx的目录下操作,(成功之后/mydata/nginx/conf目录下会有相应的配置文件和目录)
docker container cp nginx:/etc/nginx /mydata/nginx/conf
#停掉并删除之前的启动的nginx
docker stop nginx
docker rm nginx
#正式的启动容器
docker run -p 80:80 --name nginx \
-v /mydata/nginx/html:/usr/share/nginx/html \
-v /mydata/nginx/logs:/var/log/nginx \
-v /mydata/nginx/conf:/etc/nginx \
-d nginx:1.10
# 设置开机启动nginx
docker update nginx --restart=always

方法二

先在宿主机的/opt/nginx/conf/目录下创建nginx.conf配置文件并加入相关的配置

user root;
worker_processes 1;

events {
worker_connections 1024;
}

http {
include mime.types;
default_type application/octet-stream;
sendfile on;
keepalive_timeout 65;



server {
listen 80;
server_name localhost;
charset utf-8;

# set site favicon
# 这里的路径时容器的路径,不是宿主机的路径
location /favicon.ico {
root /usr/share/nginx/html/dist;
}

location / {
# 这里的路径时容器的路径,不是宿主机的路径
root /usr/share/nginx/html/dist;
try_files $uri $uri/ /index.html;
index index.html index.htm;
}

location /prod-api/ {
proxy_set_header Host $http_host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header REMOTE-HOST $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_pass http://8.137.109.244:8080/;
}

error_page 500 502 503 504 /50x.html;
location = /50x.html {
# 这里的路径时容器的路径,不是宿主机的路径
root /usr/share/nginx/html/dist;
}
}
}

创建容器

docker run -id --name=nginx \
-p 80:80 \
-v /opt/nginx/conf/nginx.conf:/etc/nginx/nginx.conf \
-v /opt/nginx/logs:/var/log/nginx \
-v /opt/nginx/html:/usr/share/nginx/html \
nginx

3.6 使用Docker安装RabbitMQ

#直接执行运行rabbitMQ容器的命令,没有镜像会自动下载
docker run -d --name rabbitmq \
-p 5671:5671 \
-p 5672:5672 \
-p 4369:4369 \
-p 25672:25672 \
-p 15671:15671 \
-p 15672:15672 \
rabbitmq:management
# 设置开机启动rabbitmq
docker update rabbitmq --restart=always
#测试访问
虚拟机的ip:15672

账号:guest

密码:guest

image-20230717123217404

3.7 使用docker安装nacos

#拉取镜像
docker pull nacos/nacos-server:1.2.0
#创建容器并运行
#MODE=standalone 单机版
docker run --env MODE=standalone --name nacos --restart=always -d -p 8848:8848 nacos/nacos-server:1.2.0

测试访问:http://虚拟机ip:8848/nacos

image-20230811105925843

]]>
运维 环境搭建 Linux
Linux从入门到进阶 /posts/8957.html

本篇博客转载于传智播客黑马程序员,只作在线笔记使用,详细的课程资料请关注黑马程序员!

Linux相关的书籍: 技术书籍-Linux指令大全 | The Blog (gitee.io)

一.初识Linux

​ Linux是一套免费使用和自由传播的类Unix操作系统,是一个基于POSIX和UNIX的多用户、多任务、支持多线程和多CPU的操作系统。它能运行主要的UNIX工具软件、应用程序和网络协议。支持32位和64位硬件。Linux继承了Unix以网络为核心的设计思想,是一个性能稳定的多用户网络操作系统。

image-20230919214601245

视频链接: https://www.bilibili.com/video/BV1n84y1i7td/?share_source=copy_web&vd_source=aee5e475191b69e6c781059ab6662584

二.Linux的安装和配置教程

1.通过虚拟机安装Linux

Linux安装教程: VMWare虚拟机安装Linux教程 | The Blog (gitee.io)

Linux设置静态IP:Linux设置静态IP | The Blog (gitee.io)

2.通过WSL获取Linux环境

安装教程: Windows10的Linux子系统WSL的安装和使用 | The Blog (gitee.io)

三.Linux的目录结构

image-20230919230906304

image-20230919231127383

四.LInux基础命令

ls命令

功能:列出文件夹信息

语法:ls [-l -h -a] [参数]

  • 参数:被查看的文件夹,不提供参数,表示查看当前工作目录
  • -l,以列表形式查看
  • -h,配合-l,以更加人性化的方式显示文件大小
  • -a,显示隐藏文件

隐藏文件、文件夹

在Linux中以 .开头的,均是隐藏的。

默认不显示出来,需要 -a选项才可查看到。

pwd命令

功能:展示当前工作目录

语法:pwd

cd命令

功能:切换工作目录

语法:cd [目标目录]

参数:目标目录,要切换去的地方,不提供默认切换到 当前登录用户HOME目录

HOME目录

每一个用户在Linux系统中都有自己的专属工作目录,称之为HOME目录。

  • 普通用户的HOME目录,默认在:/home/用户名
  • root用户的HOME目录,在:/root

FinalShell登陆终端后,默认的工作目录就是用户的HOME目录

相对路径、绝对路径

  • 相对路径,==非==/开头的称之为相对路径

    相对路径表示以 当前目录作为起点,去描述路径,如 test/a.txt,表示当前工作目录内的test文件夹内的a.txt文件

  • 绝对路径,==以==/开头的称之为绝对路径

    绝对路径从 开始描述路径

特殊路径符

  • .,表示当前,比如./a.txt,表示当前文件夹内的 a.txt文件
  • ..,表示上级目录,比如 ../表示上级目录,../../表示上级的上级目录
  • ~,表示用户的HOME目录,比如 cd ~,即可切回用户HOME目录

mkdir命令

功能:创建文件夹

语法:mkdir [-p] 参数

  • 参数:被创建文件夹的路径
  • 选项:-p,可选,表示创建前置路径

touch命令

功能:创建文件

语法:touch 参数

  • 参数:被创建的文件路径

cat命令

功能:查看文件内容

语法:cat 参数

  • 参数:被查看的文件路径

more命令

功能:查看文件,可以支持翻页查看

语法:more 参数

  • 参数:被查看的文件路径
  • 在查看过程中:
    • 空格键翻页
    • q退出查看

cp命令

功能:复制文件、文件夹

语法:cp [-r] 参数1 参数2

  • 参数1,被复制的
  • 参数2,要复制去的地方
  • 选项:-r,可选,复制文件夹使用

示例:

  • cp a.txt b.txt,复制当前目录下a.txt为b.txt
  • cp a.txt test/,复制当前目录a.txt到test文件夹内
  • cp -r test test2,复制文件夹test到当前文件夹内为test2存在

mv命令

功能:移动文件、文件夹

语法:mv 参数1 参数2

  • 参数1:被移动的
  • 参数2:要移动去的地方,参数2如果不存在,则会进行改名

rm命令

功能:删除文件、文件夹

语法:rm [-r -f] 参数...参数

  • 参数:支持多个,每一个表示被删除的,空格进行分隔
  • 选项:-r,删除文件夹使用
  • 选项:-f,强制删除,不会给出确认提示,一般root用户会用到

rm命令很危险,一定要注意,特别是切换到root用户的时候。

image-20230920231224351

which命令

功能:查看命令的程序本体文件路径

语法:which 参数

  • 参数:被查看的命令
jason@DESKTOP-PC4GEUR:~$ which pwd
/usr/bin/pwd
jason@DESKTOP-PC4GEUR:~$ which touch
/usr/bin/touch

find命令

功能:搜索文件

语法1按文件名搜索:find 路径 -name 参数

  • 路径,搜索的起始路径
  • 参数,搜索的关键字,支持通配符*, 比如:*test表示搜索任意以test结尾的文件

image-20230923175004853

grep命令

功能:过滤关键字

语法:grep [-n] 关键字 文件路径

  • 选项-n,可选,表示在结果中显示匹配的行的行号。
  • 参数,关键字,必填,表示过滤的关键字,带有空格或其它特殊符号,建议使用””将关键字包围起来
  • 参数,文件路径,必填,表示要过滤内容的文件路径,可作为内容输入端口

参数文件路径,可以作为管道符的输入

wc命令

功能:统计

语法:wc [-c -m -l -w] 文件路径

  • 选项,-c,统计bytes数量
  • 选项,-m,统计字符数量
  • 选项,-l,统计行数
  • 选项,-w,统计单词数量
  • 参数,文件路径,被统计的文件,可作为内容输入端口

参数文件路径,可作为管道符的输入

管道符|

写法:|

功能:将符号左边的结果,作为符号右边的输入

示例:

cat a.txt | grep itheima,将cat a.txt的结果,作为grep命令的输入,用来过滤 itheima关键字

可以支持嵌套:

cat a.txt | grep itheima | grep itcast

echo命令

功能:输出内容

语法:echo 参数

  • 参数:被输出的内容

image-20230923181041583

`反引号

功能:被两个反引号包围的内容,会作为命令执行

示例:

  • echo `pwd`,会输出当前工作目录

tail命令

功能:查看文件尾部内容

语法:tail [-f] 参数

  • 参数:被查看的文件
  • 选项:-f,持续跟踪文件修改
  • 选项, -num,表示,查看尾部多少行,不填默认10行(tail -10 文件)

head命令

功能:查看文件头部内容

语法:head [-n] 参数

  • 参数:被查看的文件
  • 选项:-n,查看的行数

重定向符

功能:将符号左边的结果,输出到右边指定的文件中去

  • >,表示覆盖输出
  • >>,表示追加输出

image-20230923181307011

vi编辑器

命令模式快捷键

image-20221027215841573

image-20221027215846581

image-20221027215849668

底线命令快捷键

image-20221027215858967

命令的选项

我们学习的一系列Linux命令,它们所拥有的选项都是非常多的。

比如,简单的ls命令就有:-a -A -b -c -C -d -D -f -F -g -G -h -H -i -I -k -l -L -m -n -N -o -p -q -Q -r-R -s -S -t -T -u -U -v -w -x -X -1等选项,可以发现选项是极其多的。

课程中, 并不会将全部的选项都进行讲解,否则,一个ls命令就可能讲解2小时之久。

课程中,会对常见的选项进行讲解, 足够满足绝大多数的学习、工作场景。

查看命令的帮助

可以通过:命令 --help查看命令的帮助手册

image-20221027220005610

查看命令的详细手册

可以通过:man 命令查看某命令的详细手册

image-20221027220009949

五.Linux常用操作

软件安装

  • CentOS系统使用:
    • yum [install remove search] [-y] 软件名称
      • install 安装
      • remove 卸载
      • search 搜索
      • -y,自动确认
  • Ubuntu系统使用
    • apt [install remove search] [-y] 软件名称
      • install 安装
      • remove 卸载
      • search 搜索
      • -y,自动确认

yum 和 apt 均需要root权限

systemctl

功能:控制系统服务的启动关闭等

语法:systemctl start | stop | restart | disable | enable | status 服务名

  • start,启动
  • stop,停止
  • status,查看状态
  • disable,关闭开机自启
  • enable,开启开机自启
  • restart,重启

软链接

功能:创建文件、文件夹软链接(快捷方式)

语法:ln -s 参数1 参数2

  • 参数1:被链接的
  • 参数2:要链接去的地方(快捷方式的名称和存放位置)

日期

语法:date [-d] [+格式化字符串]

  • -d 按照给定的字符串显示日期,一般用于日期计算

  • 格式化字符串:通过特定的字符串标记,来控制显示的日期格式

    • %Y 年%y 年份后两位数字 (00..99)
    • %m 月份 (01..12)
    • %d 日 (01..31)
    • %H 小时 (00..23)
    • %M 分钟 (00..59)
    • %S 秒 (00..60)
    • %s 自 1970-01-01 00:00:00 UTC 到现在的秒数

示例:

  • 按照2022-01-01的格式显示日期

    image-20221027220514640

  • 按照2022-01-01 10:00:00的格式显示日期

    image-20221027220525625

  • -d选项日期计算

    image-20221027220429831

    • 支持的时间标记为:

      image-20221027220449312

时区

修改时区为中国时区

image-20221027220554654

ntp

功能:同步时间

安装:yum install -y ntp

启动管理:systemctl start | stop | restart | status | disable | enable ntpd

手动校准时间:ntpdate -u ntp.aliyun.com

ip地址

格式:a.b.c.d

  • abcd为0~255的数字

特殊IP:

  • 127.0.0.1,表示本机
  • 0.0.0.0
    • 可以表示本机
    • 也可以表示任意IP(看使用场景)

查看ip:ifconfig

主机名

功能:Linux系统的名称

查看:hostname

设置:hostnamectl set-hostname 主机名

配置VMware固定IP

  1. 修改VMware网络,参阅PPT,图太多

  2. 设置Linux内部固定IP

    修改文件:/etc/sysconfig/network-scripts/ifcfg-ens33

    示例文件内容:

    TYPE="Ethernet"
    PROXY_METHOD="none"
    BROWSER_ONLY="no"
    BOOTPROTO="static" # 改为static,固定IP
    DEFROUTE="yes"
    IPV4_FAILURE_FATAL="no"
    IPV6INIT="yes"
    IPV6_AUTOCONF="yes"
    IPV6_DEFROUTE="yes"
    IPV6_FAILURE_FATAL="no"
    IPV6_ADDR_GEN_MODE="stable-privacy"
    NAME="ens33"
    UUID="1b0011cb-0d2e-4eaa-8a11-af7d50ebc876"
    DEVICE="ens33"
    ONBOOT="yes"
    IPADDR="192.168.88.131" # IP地址,自己设置,要匹配网络范围
    NETMASK="255.255.255.0" # 子网掩码,固定写法255.255.255.0
    GATEWAY="192.168.88.2" # 网关,要和VMware中配置的一致
    DNS1="192.168.88.2" # DNS1服务器,和网关一致即可

ps命令

功能:查看进程信息

语法:ps -ef,查看全部进程信息,可以搭配grep做过滤:ps -ef | grep xxx

image-20230925200901791

kill命令

image-20221027221303037

nmap命令

image-20221027221241123

netstat命令

功能:查看端口占用

用法:netstat -anp | grep xxx

ping命令

测试网络是否联通

语法:ping [-c num] 参数

image-20221027221129782

wget命令

image-20221027221148964

curl命令

image-20221027221201079

image-20221027221210518

top命令

功能:查看主机运行状态

语法:top,查看基础信息

可用选项:

image-20221027221340729

交互式模式中,可用快捷键:

image-20221027221354137

image-20230925203631826

image-20230925203743893

df命令

查看磁盘占用

image-20221027221413787

iostat命令

查看CPU、磁盘的相关信息

image-20221027221439990

image-20221027221514237

sar命令

查看网络统计

image-20221027221545822

环境变量

  • 临时设置:export 变量名=变量值
  • 永久设置:
    • 针对用户,设置用户HOME目录内:.bashrc文件
    • 针对全局,设置 /etc/profile

PATH变量

记录了执行程序的搜索路径

可以将自定义路径加入PATH内,实现自定义命令在任意地方均可执行的效果

$符号

可以取出指定的环境变量的值

语法:$变量名

示例:

echo $PATH,输出PATH环境变量的值

echo ${PATH}ABC,输出PATH环境变量的值以及ABC

如果变量名和其它内容混淆在一起,可以使用${}

RZ,SZ

image-20230925215103228

压缩解压

压缩

tar -zcvf 压缩包 被压缩1...被压缩2...被压缩N

  • -z表示使用gzip,可以不写

zip [-r] 参数1 参数2 参数N

image-20221027221906247

image-20230925215409296

解压

tar -zxvf 被解压的文件 -C 要解压去的地方

  • -z表示使用gzip,可以省略
  • -C,可以省略,指定要解压去的地方,不写解压到当前目录

unzip [-d] 参数

image-20221027221939899

su命令

切换用户

语法:su [-] [用户]

image-20221027222021619

sudo命令

image-20221027222035337

比如:

itheima ALL=(ALL)       NOPASSWD: ALL

在visudo内配置如上内容,可以让itheima用户,无需密码直接使用 sudo

chmod命令

修改文件、文件夹权限

语法:chmod [-R] 权限 参数

权限可以用3位数字来代表,第一位数字表示用户权限,第二位表示用户组权限,第三位表示其它用户权限。数字的细节如下:r记为4,w记为2,x记为1,可以有:

  • 权限,要设置的权限,比如755,表示:rwxr-xr-x

    image-20221027222157276

  • 参数,被修改的文件、文件夹

  • 选项-R,设置文件夹和其内部全部内容一样生效

示例:

​ • chmod u=rwx,g=rx,o=x hello.txt ,将文件权限修改为:rwxr-x–x

​ • 其中:u表示user所属用户权限,g表示group组权限,o表示other其它用户权限

​ • chmod -R u=rwx,g=rx,o=x test,将文件夹test以及文件夹内全部内容权限设置为:rwxr-x–x

除此之外,还有快捷写法:chmod 751 hello.txt,所以751表示: rwx(7) r-x(5) –x(1)[数字代表的含义见上面]

rwx的含义

• r表示读权限

• w表示写权限

• x表示执行权限

针对文件、文件夹的不同,rwx的含义有细微差别

•r,针对文件可以查看文件内容

•针对文件夹,可以查看文件夹内容,如ls命令

•w,针对文件表示可以修改此文件

•针对文件夹,可以在文件夹内:创建、删除、改名等操作

•x,针对文件表示可以将文件作为程序执行

•针对文件夹,表示可以更改工作目录到此文件夹,即cd进入

image-20230925112857636

举例:drwxr-xr-x,表示:

•这是一个文件夹,首字母d表示

•所属用户(右上角图序号2)的权限是:有r有w有x,rwx

•所属用户组(右上角图序号3)的权限是:有r无w有x,r-x (-表示无此权限)

•其它用户的权限是:有r无w有x,r-x

chown命令

修改文件、文件夹所属用户、组

语法:chown [-R] [用户][:][用户组] 文件或文件夹

image-20221027222326192

用户组管理

image-20221027222354498

用户管理

image-20221027222407618

genent命令

  • getent group,查看系统全部的用户组

    image-20221027222446514

  • getent passwd,查看系统全部的用户

    image-20221027222512274

env命令

查看系统全部的环境变量

语法:env

开启指定端口的防火墙限制

#永久性地在防火墙配置中打开TCP的9000端口
#--permanent表示永久的
sudo firewall-cmd --permanent --add-port=9000/tcp
#刷新防火墙规则
sudo firewall-cmd --reload

实用的小技巧

history命令

用于查看历史输入的命令

root@DESKTOP-PC4GEUR:/# history
1 cd /
2 ls
3 clear
4 ls
5 cd /
略略略
51 clear
52 hostory
53 clear
54 history
root@DESKTOP-PC4GEUR:/#

光标移动的快捷键

image-20230925132050649

小技巧汇总

image-20230925133017257

]]>
运维 Linux
IDEA常用快捷键 /posts/63724.html 1.常用快捷键
  1. 在写一个main主函数的时候可以直接在键盘上敲main ,然后根据提示补全全部(模板快捷键)

  2. 在写System.out.println();输出函数代码的时候可以直接在键盘上面敲sout,然后根据提示补全(模板快捷键)

  3. 在写for循环的时候,我们可以直接在键盘上面打出 fori 然后根据提示补全代码(模板快捷键)

  4. 删除当前行 Ctrl+D (并非默认Ctrl + D,默认为Ctrl + Y),需要我们自己设置

  5. 复制当前行,快速向下复制一行 Ctrl + Alt + 向下箭头(并非默认,需要我们自己设置)

  6. 代码补全 Alt + /

  7. 添加注释和取消注释 Ctrl + /

  8. 自动导入import java.util.Scanner;我们在键盘上敲Scanner in = new Scanner(System,in);的时候按快捷键Alt + Enter 可以自动导入import java.util.Scanner;

  9. 代码格式化(格式调整为正常格式,,让代码变整洁,并非删除代码) Ctrl + Alt +L

  10. 快速运行程序(并非默认,需要我们自己设置)Alt + R,如果要使用这个快捷键,我们要切换到当前的主类,否则运行的还是之前运行的程序

  11. 构造器快捷键 Alt + insert ,按住Ctrl可以选择任意个参数的构造器,也可以点击Select None选择无参构造器; 我们在写一个私有类的getXxx()和setXxx()方法的时候,我们可以使用这个快捷键,光标移动到”Getter and Setter“的位置并选择。

  12. 查看一个类的层级关系 Ctrl + H

  13. 将光标放在一个方法上,使用快捷键 Ctrl + B 可以快速定位到这个方法的代码

  14. 我们在键盘上面敲Scanner in = new Scanner(System.in);的时候,我们先敲new Scanner(System.in),再在后面加上new Scanner(System.in).var,按Enter,就会自动补全前面的代码;定义一个Person类对象的时候我们也可以使用这种方法,如new Person().var,按Enter,就会自动补全前面的代码,变成 Person person = new Person();

  15. 重写某个方法的时候我们可以直接打出这个方法名,然后根据提示,移动光标选择。

  16. Alt+insert移动光标选择toString,可以重写toString方法,并输出对象的属性。

  17. 断点调试的快捷键:F7(跳入),F8(跳过),shift+F8(跳出),F9(resume,执行到下一个断点)

    ​ F7:跳入方法内

    ​ F8:逐行执行代码

    ​ shift+F8:跳出方法

  18. 一个普通类实现接口,就必须将该接口的所有方法都实现,我们在实现这些接口的时候可以使用快捷键Alt+Enter

  19. Ctrl+Alt+t 可以快捷的使用try-catch

  20. 遍历一个数组,生成while循环,可以快捷生成,快捷键是 itit + 回车 ,增强的for循环的迭代器的快捷输入 是大写的 I + enter

  21. Ctrl + J 可以显示所有的快捷模板

  22. Ctrl + R 查找替换

  23. Shift+Enter 快速向下换一行

  24. Shift+Home 快速选中一行

  25. Ctrl + F 全局查找

]]>
后端 快捷键
Linux设置静态IP /posts/21883.html 1.查看IP的状态
#查看是否为静态ip
ip addr

如图所示的是动态IP,我们要配置静态IP

image-20230511230406172

2.设置网络

2.1 进入配置文件对网络进行配置

1.修改配置文件中的配置

#进入配置文件的命令
vim /etc/sysconfig/network-scripts/ifcfg-ens33

image-20230511233324998

2.重启网络

#重启网络的配置
systemctl restart network
#查看设置的IP情况
ifconfig

image-20230511233838595

在主机上使用ping命令ping 刚才的设置的静态IP也可以ping通

image-20230511234154523

虚拟机ping外部也可以ping通

image-20230511235621228

]]>
后端 Linux
Linux运维管理面板-1Panel /posts/56106.html 一.官方文档

官网:1Panel - 现代化、开源的 Linux 服务器运维管理面板

官方文档:1Panel 文档

1Panel是飞致云旗下的一款现代化、开源的 Linux 服务器运维管理面板

二.安装教程

详细安装教程见官方文档:在线安装 - 1Panel 文档

#linux命令一键安装并启动
curl -sSL https://resource.fit2cloud.com/1panel/package/quick_start.sh -o quick_start.sh && sh quick_start.sh

安装成功后浏览器访问http://目标服务器 IP 地址:目标端口/安全入口进入运维面板(安装成功后根据命令行返回链接和密码信息访问并登录面板即可)

image-20240101201242287

三.常用工具

1.Fail2ban

简介

Fail2Ban 是一款入侵防御软件,可以保护服务器免受暴力攻击。 它是用 Python 编程语言编写的。 Fail2Ban 基于auth 日志文件工作,默认情况下它会扫描所有 auth 日志文件,如 /var/log/auth.log、/var/log/apache/access.log 等,并禁止带有恶意标志的IP,比如密码失败太多,寻找漏洞等等标志。通常,Fail2Ban 用于更新防火墙规则,用于在指定的时间内拒绝 IP 地址。 它也会发送邮件通知。 Fail2Ban 为各种服务提供了许多过滤器,如 ssh、apache、nginx、squid、named、mysql、nagios 等。(简单来说就是避免别人暴力破解服务器的连接账号密码信息,入侵服务器)

安装

详细教程: Fail2ban - 1Panel 文档

1、安装 epel 源

yum install -y epel-release

2、安装 Fail2ban

yum install -y fail2ban

3、启动 Fail2ban 服务

systemctl start fail2ban

4、开机自启动

systemctl enable fail2ban

5、查看 Fail2ban 服务状态

systemctl status fail2ban

默认配置 (后续自定义的配置直接在运维面板中更改即可)

#DEFAULT-START
[DEFAULT]
bantime = 600
findtime = 300
maxretry = 5
banaction = firewallcmd-ipset
action = %(action_mwl)s
#DEFAULT-END

[sshd]
ignoreip = 127.0.0.1/8 # 白名单
enabled = true
filter = sshd
port = 22 # 端口
maxretry = 2 # 最大尝试次数
findtime = 300 # 发现周期 单位s [在findtime(300秒)周期内尝试次数超过maxretry(2)次,就 #加入黑名单
bantime = 600 # 封禁时间,单位s。-1为永久封禁
action = %(action_mwl)s
banaction = iptables-multiport # 禁用方式
logpath = /var/log/secure # SSH 登陆日志位置

验证

image-20240101195315281

image-20240101195455038

]]>
运维 1Panel
MySql基础进阶运维篇PDF笔记 /posts/50465.html 1.PDF笔记

1.1MySql基础篇PDF笔记

1.2MySql进阶篇PDF笔记

1.3MySql运维篇PDF笔记

]]>
后端 Mysql
Maven配置文件settings.xml /posts/26768.html 用途:更换IDEA中Maven配置文件实现不下载配置maven的阿里云镜像

设置本地仓库磁盘存储位置

1.在C盘的根目录下创建如下的文件夹C:\maven\maven-repository

2.任选下面的两个配置文件中的一个放在C:\maven目录下 配置文件的名字为 settings.xml

3.在IDEA中配置本地仓库的位置和配置文件的位置

配置文件

apache-maven-3.6.3

<?xml version="1.0" encoding="UTF-8"?>

<!--
Licensed to the Apache Software Foundation (ASF) under one
or more contributor license agreements. See the NOTICE file
distributed with this work for additional information
regarding copyright ownership. The ASF licenses this file
to you under the Apache License, Version 2.0 (the
"License"); you may not use this file except in compliance
with the License. You may obtain a copy of the License at

http://www.apache.org/licenses/LICENSE-2.0

Unless required by applicable law or agreed to in writing,
software distributed under the License is distributed on an
"AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
KIND, either express or implied. See the License for the
specific language governing permissions and limitations
under the License.
-->

<!--
| This is the configuration file for Maven. It can be specified at two levels:
|
| 1. User Level. This settings.xml file provides configuration for a single user,
| and is normally provided in ${user.home}/.m2/settings.xml.
|
| NOTE: This location can be overridden with the CLI option:
|
| -s /path/to/user/settings.xml
|
| 2. Global Level. This settings.xml file provides configuration for all Maven
| users on a machine (assuming they're all using the same Maven
| installation). It's normally provided in
| ${maven.conf}/settings.xml.
|
| NOTE: This location can be overridden with the CLI option:
|
| -gs /path/to/global/settings.xml
|
| The sections in this sample file are intended to give you a running start at
| getting the most out of your Maven installation. Where appropriate, the default
| values (values used when the setting is not specified) are provided.
|
|-->
<settings xmlns="http://maven.apache.org/SETTINGS/1.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/SETTINGS/1.0.0 http://maven.apache.org/xsd/settings-1.0.0.xsd">
<!-- localRepository
| The path to the local repository maven will use to store artifacts.
|
| Default: ${user.home}/.m2/repository
<localRepository>/path/to/local/repo</localRepository>
-->
<localRepository>C:\maven\maven-repository</localRepository>
<!-- interactiveMode
| This will determine whether maven prompts you when it needs input. If set to false,
| maven will use a sensible default value, perhaps based on some other setting, for
| the parameter in question.
|
| Default: true
<interactiveMode>true</interactiveMode>
-->

<!-- offline
| Determines whether maven should attempt to connect to the network when executing a build.
| This will have an effect on artifact downloads, artifact deployment, and others.
|
| Default: false
<offline>false</offline>
-->

<!-- pluginGroups
| This is a list of additional group identifiers that will be searched when resolving plugins by their prefix, i.e.
| when invoking a command line like "mvn prefix:goal". Maven will automatically add the group identifiers
| "org.apache.maven.plugins" and "org.codehaus.mojo" if these are not already contained in the list.
|-->
<pluginGroups>
<!-- pluginGroup
| Specifies a further group identifier to use for plugin lookup.
<pluginGroup>com.your.plugins</pluginGroup>
-->
</pluginGroups>

<!-- proxies
| This is a list of proxies which can be used on this machine to connect to the network.
| Unless otherwise specified (by system property or command-line switch), the first proxy
| specification in this list marked as active will be used.
|-->
<proxies>
<!-- proxy
| Specification for one proxy, to be used in connecting to the network.
|
<proxy>
<id>optional</id>
<active>true</active>
<protocol>http</protocol>
<username>proxyuser</username>
<password>proxypass</password>
<host>proxy.host.net</host>
<port>80</port>
<nonProxyHosts>local.net|some.host.com</nonProxyHosts>
</proxy>
-->
</proxies>

<!-- servers
| This is a list of authentication profiles, keyed by the server-id used within the system.
| Authentication profiles can be used whenever maven must make a connection to a remote server.
|-->
<servers>
<!-- server
| Specifies the authentication information to use when connecting to a particular server, identified by
| a unique name within the system (referred to by the 'id' attribute below).
|
| NOTE: You should either specify username/password OR privateKey/passphrase, since these pairings are
| used together.
|
<server>
<id>deploymentRepo</id>
<username>repouser</username>
<password>repopwd</password>
</server>
-->

<!-- Another sample, using keys to authenticate.
<server>
<id>siteServer</id>
<privateKey>/path/to/private/key</privateKey>
<passphrase>optional; leave empty if not used.</passphrase>
</server>
-->
</servers>

<!-- mirrors
| This is a list of mirrors to be used in downloading artifacts from remote repositories.
|
| It works like this: a POM may declare a repository to use in resolving certain artifacts.
| However, this repository may have problems with heavy traffic at times, so people have mirrored
| it to several places.
|
| That repository definition will have a unique id, so we can create a mirror reference for that
| repository, to be used as an alternate download site. The mirror site will be the preferred
| server for that repository.
|-->
<mirrors>
<!-- mirror
| Specifies a repository mirror site to use instead of a given repository. The repository that
| this mirror serves has an ID that matches the mirrorOf element of this mirror. IDs are used
| for inheritance and direct lookup purposes, and must be unique across the set of mirrors.
|
<mirror>
<id>mirrorId</id>
<mirrorOf>repositoryId</mirrorOf>
<name>Human Readable Name for this Mirror.</name>
<url>http://my.repository.com/repo/path</url>
</mirror>
-->
<!-- 阿里云仓库,个人配置 -->
<mirror>
<id>alimaven</id>
<mirrorOf>central</mirrorOf>
<name>aliyun maven</name>
<url>http://maven.aliyun.com/nexus/content/repositories/central/</url>
</mirror>
</mirrors>

<!-- profiles
| This is a list of profiles which can be activated in a variety of ways, and which can modify
| the build process. Profiles provided in the settings.xml are intended to provide local machine-
| specific paths and repository locations which allow the build to work in the local environment.
|
| For example, if you have an integration testing plugin - like cactus - that needs to know where
| your Tomcat instance is installed, you can provide a variable here such that the variable is
| dereferenced during the build process to configure the cactus plugin.
|
| As noted above, profiles can be activated in a variety of ways. One way - the activeProfiles
| section of this document (settings.xml) - will be discussed later. Another way essentially
| relies on the detection of a system property, either matching a particular value for the property,
| or merely testing its existence. Profiles can also be activated by JDK version prefix, where a
| value of '1.4' might activate a profile when the build is executed on a JDK version of '1.4.2_07'.
| Finally, the list of active profiles can be specified directly from the command line.
|
| NOTE: For profiles defined in the settings.xml, you are restricted to specifying only artifact
| repositories, plugin repositories, and free-form properties to be used as configuration
| variables for plugins in the POM.
|
|-->
<profiles>
<!-- profile
| Specifies a set of introductions to the build process, to be activated using one or more of the
| mechanisms described above. For inheritance purposes, and to activate profiles via <activatedProfiles/>
| or the command line, profiles have to have an ID that is unique.
|
| An encouraged best practice for profile identification is to use a consistent naming convention
| for profiles, such as 'env-dev', 'env-test', 'env-production', 'user-jdcasey', 'user-brett', etc.
| This will make it more intuitive to understand what the set of introduced profiles is attempting
| to accomplish, particularly when you only have a list of profile id's for debug.
|
| This profile example uses the JDK version to trigger activation, and provides a JDK-specific repo.
<profile>
<id>jdk-1.4</id>

<activation>
<jdk>1.4</jdk>
</activation>

<repositories>
<repository>
<id>jdk14</id>
<name>Repository for JDK 1.4 builds</name>
<url>http://www.myhost.com/maven/jdk14</url>
<layout>default</layout>
<snapshotPolicy>always</snapshotPolicy>
</repository>
</repositories>
</profile>
-->

<!--
| Here is another profile, activated by the system property 'target-env' with a value of 'dev',
| which provides a specific path to the Tomcat instance. To use this, your plugin configuration
| might hypothetically look like:
|
| ...
| <plugin>
| <groupId>org.myco.myplugins</groupId>
| <artifactId>myplugin</artifactId>
|
| <configuration>
| <tomcatLocation>${tomcatPath}</tomcatLocation>
| </configuration>
| </plugin>
| ...
|
| NOTE: If you just wanted to inject this configuration whenever someone set 'target-env' to
| anything, you could just leave off the <value/> inside the activation-property.
|
<profile>
<id>env-dev</id>

<activation>
<property>
<name>target-env</name>
<value>dev</value>
</property>
</activation>

<properties>
<tomcatPath>/path/to/tomcat/instance</tomcatPath>
</properties>
</profile>
-->
</profiles>

<!-- activeProfiles
| List of profiles that are active for all builds.
|
<activeProfiles>
<activeProfile>alwaysActiveProfile</activeProfile>
<activeProfile>anotherAlwaysActiveProfile</activeProfile>
</activeProfiles>
-->
</settings>

apache-maven-3.8.6

<?xml version="1.0" encoding="UTF-8"?>

<!--
Licensed to the Apache Software Foundation (ASF) under one
or more contributor license agreements. See the NOTICE file
distributed with this work for additional information
regarding copyright ownership. The ASF licenses this file
to you under the Apache License, Version 2.0 (the
"License"); you may not use this file except in compliance
with the License. You may obtain a copy of the License at

http://www.apache.org/licenses/LICENSE-2.0

Unless required by applicable law or agreed to in writing,
software distributed under the License is distributed on an
"AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
KIND, either express or implied. See the License for the
specific language governing permissions and limitations
under the License.
-->

<!--
| This is the configuration file for Maven. It can be specified at two levels:
|
| 1. User Level. This settings.xml file provides configuration for a single user,
| and is normally provided in ${user.home}/.m2/settings.xml.
|
| NOTE: This location can be overridden with the CLI option:
|
| -s /path/to/user/settings.xml
|
| 2. Global Level. This settings.xml file provides configuration for all Maven
| users on a machine (assuming they're all using the same Maven
| installation). It's normally provided in
| ${maven.conf}/settings.xml.
|
| NOTE: This location can be overridden with the CLI option:
|
| -gs /path/to/global/settings.xml
|
| The sections in this sample file are intended to give you a running start at
| getting the most out of your Maven installation. Where appropriate, the default
| values (values used when the setting is not specified) are provided.
|
|-->
<settings xmlns="http://maven.apache.org/SETTINGS/1.2.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/SETTINGS/1.2.0 https://maven.apache.org/xsd/settings-1.2.0.xsd">
<!-- localRepository
| The path to the local repository maven will use to store artifacts.
|
| Default: ${user.home}/.m2/repository
<localRepository>/path/to/local/repo</localRepository>
-->
<localRepository>C:\maven\maven-repository</localRepository>

<!-- interactiveMode
| This will determine whether maven prompts you when it needs input. If set to false,
| maven will use a sensible default value, perhaps based on some other setting, for
| the parameter in question.
|
| Default: true
<interactiveMode>true</interactiveMode>
-->

<!-- offline
| Determines whether maven should attempt to connect to the network when executing a build.
| This will have an effect on artifact downloads, artifact deployment, and others.
|
| Default: false
<offline>false</offline>
-->

<!-- pluginGroups
| This is a list of additional group identifiers that will be searched when resolving plugins by their prefix, i.e.
| when invoking a command line like "mvn prefix:goal". Maven will automatically add the group identifiers
| "org.apache.maven.plugins" and "org.codehaus.mojo" if these are not already contained in the list.
|-->
<pluginGroups>
<!-- pluginGroup
| Specifies a further group identifier to use for plugin lookup.
<pluginGroup>com.your.plugins</pluginGroup>
-->
</pluginGroups>

<!-- proxies
| This is a list of proxies which can be used on this machine to connect to the network.
| Unless otherwise specified (by system property or command-line switch), the first proxy
| specification in this list marked as active will be used.
|-->
<proxies>
<!-- proxy
| Specification for one proxy, to be used in connecting to the network.
|
<proxy>
<id>optional</id>
<active>true</active>
<protocol>http</protocol>
<username>proxyuser</username>
<password>proxypass</password>
<host>proxy.host.net</host>
<port>80</port>
<nonProxyHosts>local.net|some.host.com</nonProxyHosts>
</proxy>
-->
</proxies>

<!-- servers
| This is a list of authentication profiles, keyed by the server-id used within the system.
| Authentication profiles can be used whenever maven must make a connection to a remote server.
|-->
<servers>
<!-- server
| Specifies the authentication information to use when connecting to a particular server, identified by
| a unique name within the system (referred to by the 'id' attribute below).
|
| NOTE: You should either specify username/password OR privateKey/passphrase, since these pairings are
| used together.
|
<server>
<id>deploymentRepo</id>
<username>repouser</username>
<password>repopwd</password>
</server>
-->

<!-- Another sample, using keys to authenticate.
<server>
<id>siteServer</id>
<privateKey>/path/to/private/key</privateKey>
<passphrase>optional; leave empty if not used.</passphrase>
</server>
-->
</servers>

<!-- mirrors
| This is a list of mirrors to be used in downloading artifacts from remote repositories.
|
| It works like this: a POM may declare a repository to use in resolving certain artifacts.
| However, this repository may have problems with heavy traffic at times, so people have mirrored
| it to several places.
|
| That repository definition will have a unique id, so we can create a mirror reference for that
| repository, to be used as an alternate download site. The mirror site will be the preferred
| server for that repository.
|-->
<mirrors>
<!-- mirror
| Specifies a repository mirror site to use instead of a given repository. The repository that
| this mirror serves has an ID that matches the mirrorOf element of this mirror. IDs are used
| for inheritance and direct lookup purposes, and must be unique across the set of mirrors.
|
<mirror>
<id>mirrorId</id>
<mirrorOf>repositoryId</mirrorOf>
<name>Human Readable Name for this Mirror.</name>
<url>http://my.repository.com/repo/path</url>
</mirror>
-->
<!--<mirror>
<id>maven-default-http-blocker</id>
<mirrorOf>external:http:*</mirrorOf>
<name>Pseudo repository to mirror external repositories initially using HTTP.</name>
<url>http://0.0.0.0/</url>
<blocked>true</blocked>
</mirror>-->
<mirror>
<id>nexus-aliyun</id>
<mirrorOf>central</mirrorOf>
<name>Nexus aliyun</name>
<url>http://maven.aliyun.com/nexus/content/groups/public</url>
</mirror>
</mirrors>

<!-- profiles
| This is a list of profiles which can be activated in a variety of ways, and which can modify
| the build process. Profiles provided in the settings.xml are intended to provide local machine-
| specific paths and repository locations which allow the build to work in the local environment.
|
| For example, if you have an integration testing plugin - like cactus - that needs to know where
| your Tomcat instance is installed, you can provide a variable here such that the variable is
| dereferenced during the build process to configure the cactus plugin.
|
| As noted above, profiles can be activated in a variety of ways. One way - the activeProfiles
| section of this document (settings.xml) - will be discussed later. Another way essentially
| relies on the detection of a system property, either matching a particular value for the property,
| or merely testing its existence. Profiles can also be activated by JDK version prefix, where a
| value of '1.4' might activate a profile when the build is executed on a JDK version of '1.4.2_07'.
| Finally, the list of active profiles can be specified directly from the command line.
|
| NOTE: For profiles defined in the settings.xml, you are restricted to specifying only artifact
| repositories, plugin repositories, and free-form properties to be used as configuration
| variables for plugins in the POM.
|
|-->
<profiles>
<!-- profile
| Specifies a set of introductions to the build process, to be activated using one or more of the
| mechanisms described above. For inheritance purposes, and to activate profiles via <activatedProfiles/>
| or the command line, profiles have to have an ID that is unique.
|
| An encouraged best practice for profile identification is to use a consistent naming convention
| for profiles, such as 'env-dev', 'env-test', 'env-production', 'user-jdcasey', 'user-brett', etc.
| This will make it more intuitive to understand what the set of introduced profiles is attempting
| to accomplish, particularly when you only have a list of profile id's for debug.
|
| This profile example uses the JDK version to trigger activation, and provides a JDK-specific repo.
<profile>
<id>jdk-1.4</id>

<activation>
<jdk>1.4</jdk>
</activation>

<repositories>
<repository>
<id>jdk14</id>
<name>Repository for JDK 1.4 builds</name>
<url>http://www.myhost.com/maven/jdk14</url>
<layout>default</layout>
<snapshotPolicy>always</snapshotPolicy>
</repository>
</repositories>
</profile>
-->

<!--
| Here is another profile, activated by the system property 'target-env' with a value of 'dev',
| which provides a specific path to the Tomcat instance. To use this, your plugin configuration
| might hypothetically look like:
|
| ...
| <plugin>
| <groupId>org.myco.myplugins</groupId>
| <artifactId>myplugin</artifactId>
|
| <configuration>
| <tomcatLocation>${tomcatPath}</tomcatLocation>
| </configuration>
| </plugin>
| ...
|
| NOTE: If you just wanted to inject this configuration whenever someone set 'target-env' to
| anything, you could just leave off the <value/> inside the activation-property.
|
<profile>
<id>env-dev</id>

<activation>
<property>
<name>target-env</name>
<value>dev</value>
</property>
</activation>

<properties>
<tomcatPath>/path/to/tomcat/instance</tomcatPath>
</properties>
</profile>
-->
<profile>
<id>jdk-1.8</id>
<activation>
<activeByDefault>true</activeByDefault>
<jdk>1.8</jdk>
</activation>
<properties>
<maven.compiler.source>1.8</maven.compiler.source>
<maven.compiler.target>1.8</maven.compiler.target>
<maven.compiler.compilerVersion>1.8</maven.compiler.compilerVersion>
</properties>
</profile>
</profiles>

<!-- activeProfiles
| List of profiles that are active for all builds.
|
<activeProfiles>
<activeProfile>alwaysActiveProfile</activeProfile>
<activeProfile>anotherAlwaysActiveProfile</activeProfile>
</activeProfiles>
-->
</settings>

Linux中使用,去掉了注释

<?xml version="1.0" encoding="UTF-8"?>

<settings xmlns="http://maven.apache.org/SETTINGS/1.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/SETTINGS/1.0.0 http://maven.apache.org/xsd/settings-1.0.0.xsd">

<localRepository>/root/.m2/repository</localRepository>
<pluginGroups>

</pluginGroups>

<proxies>

</proxies>

<servers>

</servers>

<mirrors>

<mirror>
<id>alimaven</id>
<mirrorOf>central</mirrorOf>
<name>aliyun maven</name>
<url>http://maven.aliyun.com/nexus/content/repositories/central/</url>
</mirror>
</mirrors>

<profiles>

</profiles>

</settings>
]]>
后端 Java
Mybatis-Plus的使用教程 /posts/53306.html 官方文档:MyBatis-Plus

常用插件

1.公共字段填充

对于每个实体类共有的属性字段,例如创建时间、修改时间、创建人、修改人,我们可以使用公共字段填充,来统一填充这些字段,这样我们在创建这些实体类的对象的时候就不需要set这些属性,实现丝滑开发

@TableField(fill = FieldFill.INSERT) //插入的时候填充
private String gmtCreate;
@TableField(fill = FieldFill.INSERT_UPDATE)//插入和修改的时候填充
private String gmtModified;

第一种配置的方法

package com.itheima.config;

import com.baomidou.mybatisplus.core.handlers.MetaObjectHandler;
import org.apache.ibatis.reflection.MetaObject;
import org.springframework.stereotype.Component;

import java.time.LocalDateTime;

/**
* @author GongChangjiang
* @version 1.0
* @Date 2023/5/25
* @Description 公共字段填充的配置
*/
@Component
public class MyMetaObjectHandler implements MetaObjectHandler {

/**
* 插入时的字段填充
* @param metaObject 元对象
*/
@Override
public void insertFill(MetaObject metaObject) {
//创建时间的字段填充
metaObject.setValue("gmtCreate", LocalDateTime.now());
//修改时间的字段填充
metaObject.setValue("gmtModified", LocalDateTime.now());
}

/**
* 修改时的字段填充
* @param metaObject 元对象
*/
@Override
public void updateFill(MetaObject metaObject) {
//修改时间的字段填充
metaObject.setValue("gmtModified", LocalDateTime.now());
}
}

第二种配置的方法

@Component
public class MyMetaObjectHandler implements MetaObjectHandler {

@Override
public void insertFill(MetaObject metaObject) {
// 起始版本 3.3.0(推荐使用)
this.strictInsertFill(metaObject, "createTime", LocalDateTime.class, LocalDateTime.now());
// 或者
this.strictInsertFill(metaObject, "createTime", () -> LocalDateTime.now(), LocalDateTime.class); // 起始版本 3.3.3(推荐)
// 或者
// 也可以使用(3.3.0 该方法有bug)
this.fillStrategy(metaObject, "createTime", LocalDateTime.now());
}

@Override
public void updateFill(MetaObject metaObject) {
// 起始版本 3.3.0(推荐)
this.strictUpdateFill(metaObject, "updateTime", LocalDateTime.class, LocalDateTime.now());
// 或者
this.strictUpdateFill(metaObject, "updateTime", () -> LocalDateTime.now(), LocalDateTime.class); // 起始版本 3.3.3(推荐)
// 或者
// 也可以使用(3.3.0 该方法有bug)
this.fillStrategy(metaObject, "updateTime", LocalDateTime.now());
}
}

2.逻辑删除

1.配置全局的配置文件(可以省略)

mybatis-plus:
global-config:
db-config:
logic-delete-value: 1 # 逻辑已删除值(默认为 1)
logic-not-delete-value: 0 # 逻辑未删除值(默认为 0)

2.配置逻辑删除的组件(3.1.1版本之后不需要这一步,可以省略)

import com.baomidou.mybatisplus.core.injector.ISqlInjector;
import com.baomidou.mybatisplus.extension.injector.LogicSqlInjector;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

@Configuration
public class MyBatisPlusConfiguration {

@Bean
public ISqlInjector sqlInjector() {
return new LogicSqlInjector();
}
}

3.在实体类上逻辑删除的字段上加上逻辑删除的注解

//value:表示不删除对应的值
//delval:表示删除对应的值
//正常的情况下1代表删除,0代表未删除,这里我们没有使用这个规则,下面设置的是自己的规则
@TableLogic(value = "1",delval = "0")
]]>
后端 SSM
MySQL5.7安装教程 /posts/17259.html 1.下载的地址

下载之后直接解压使用(下载之前看电脑上有没有mysql的服务,如果有先删除),解压的文件路径最好不要有中文

查看的方式

Win + R   输入    services.msc  回车打开   找是否有一个名为mysqld的服务

删除的指令

sc delete mysql

Mysql5.7地址:https://dev.mysql.com/get/Downloads/MySQL-5.7/mysql-5.7.19-winx64.zip

2.配置环境变量

添加环境变量 : 电脑-属性-高级系统设置-环境变量,在Path 环境变量增加mysql的安装目录\bin目录

3.在mysql的安装目录下创建my.ini文件

image-20230312152323079

my.ini

[client]
port=3306
default-character-set=utf8
[mysqld]
# 设置为自己MYSQL的安装目录
basedir=C:\mysql\mysql-5.7.19-winx64\
# 设置为MYSQL的数据目录,这个目录系统帮我们创建
datadir=C:\mysql\mysql-5.7.19-winx64\data\
port=3306
character_set_server=utf8
#跳过安全检查,注释后需要输入正确的用户名和密码才能登录
#第一登录不要注释掉,第一次进入没有设置密码,就可以不用输入密码了
#skip-grant-tables

4.下载服务和初始化数据库

1.下载mysql的服务

​ 使用管理员身份打开 cmd , 并切换到mysql-5.7.19-winx64\bin 目录下, 执行 mysqld -install

cd  **/**/**/mysql-5.7.19-winx64\bin
mysqld -install

2.初始化数据

mysqld --initialize-insecure --user=mysql

5.启动和停止mysql的服务

#开启mysql的服务
net start mysql
#停止mysql的服务
net stop mysql

6.修改账户和密码

#9. 进入mysql 管理终端: 初次登录没有密码,直接按两下回车键进入
mysql -u root -p
进入之后执行:
use mysql;
update user set authentication_string=password('123456') where user='root' and Host='localhost';
flush privileges; #刷新权限
quit #退出

注释掉my.ini中的跳过权限检查 #skip-grant-tables

7.重启mysql的服务

net stop mysql
net start mysql
mysql -u root -p #输入刚才设置的密码进入mysql服务

安装失败,删除服务,重新安装

8.可视化工具

推荐SQLyog Navicat

网盘地址

PDF版本的教程(来源于韩顺平教育,转载注明出处)

]]>
后端 Mysql
MySql进阶教程 /posts/39654.html 全部的PDF笔记: https://jasonsgong.gitee.io/posts/50465.html

原视频地址: 黑马程序员 MySQL数据库入门到精通,从mysql安装到mysql高级、mysql优化全囊括

一.Mysql入门

1.SQL

全称 Structured Query Language,结构化查询语言。操作关系型数据库的编程语言,定义了一套操作关系型数据库统一标准 。

1.1 SQL通用语法

image-20230926220120609

1.2 SQL分类

image-20230926215941275

1.3 DDL-数据定义语言

连接本地mysql的命令

mysql -u root -p

常用的DDL操作

-- ------------------------------------------------------数据库的操作------------------------------------------------------------------------
-- 展示所有的数据库
show databases ;

-- 查询当前的数据库
select database();

-- 创建数据库
create database if not exists itcast;

-- 删除数据库
drop database if exists itcast;

-- 使用数据库
use itcast;


-- ----------------------------------------------------------表的操作--------------------------------------------------------------------

-- 创建用户表
create table tb_user(id int comment '编号', name varchar(50) comment '姓名', age int comment '年龄',gender varchar(1) comment '性别' ) comment '用户表';

-- 显示创建表的结构
desc tb_user;

-- 显示创建表的语句
show create table tb_user;

-- 删除表
drop table emp;

-- 修改表的操作
-- 举例:现有一张员工表
create table emp(
id int comment '编号',
workno varchar(10) comment '工号',
name varchar(10) comment '姓名',
gender char(1) comment '性别',
age tinyint unsigned comment '年龄',
idcard char(18) comment '身份证号',
entrydate date comment '入职时间'
) comment '员工表';
-- 查看表结构
desc emp;

-- 修改表结构(向emp表中添加昵称字段)
alter table emp add nickname varchar(20) comment '昵称';

-- 修改相应字段的数据类型(把age的数据类型从int改回tinyint)
alter table emp modify age tinyint;

-- 修改字段名和字段的类型(把nicknem字段改为username,并修改数据类型)
alter table emp change nickname username varchar(30) comment '用户名';

-- 删除字段(删除username字段)
alter table emp drop username;

-- 修改表名(将表名字从emo修改为employee)
alter table emp rename to employee;

-- 删除表
drop table if exists employee;
-- 删除指定表,并重新创建该表(相当于删除表中的全部数据)
truncate table employee;

Mysql的数据类型

数值数据类型

image-20230926224723586

字符串数据类型

image-20230926224755542

日期时间类型

image-20230926224539052

1.4 DML-数据操作语言

-- ------------------------------------------------DML-增删改操作---------------------------------------------------
-- 添加数据
select database();
show tables;
-- 该指定的字段添加数据
insert into employee(id, workno, name, gender, age, idcard, entrydate) value (1, '1', '小华', '男', 20, '123456789123456789', '2001-01-01');
select *
from employee;
-- 给全部的字段添加数据
insert into employee value (2, '2', '小刚', '男', 23, '123456784124456789', '2008-01-01');
-- 批量添加数据
insert into employee
values (3, '3', '小李', '男', 23, '123456784124456789', '2008-01-01'),
(4, '4', '小黄', '男', 23, '123456784124456789', '2008-01-01');

-- 修改数据(不带条件修改所有)
update employee set name = '张三' where id = 1;
update employee set name = '李四', gender= '女' where id = 1;

-- 删除数据(删除id为4的数据,不带添加删除所有)
delete from employee where id = 4;

1.5 DQL-数据查询语言

image-20230930222149036

  • 基本查询(不带任何条件)
  • 条件查询(WHERE)
  • 聚合函数(count、max、min、avg、sum)
  • 分组查询(group by)
  • 排序查询(order by)
  • 分页查询(limit

测试数据准备

-- 数据准备
create table emp(
id int comment '编号',
workno varchar(10) comment '工号',
name varchar(10) comment '姓名',
gender char(1) comment '性别',
age tinyint unsigned comment '年龄',
idcard char(18) comment '身份证号',
workaddress varchar(50) comment '工作地址',
entrydate date comment '入职时间'
) comment '员工表';
-- 插入数据
insert into emp(id, workno, name, gender, age, idcard, workaddress,entrydate)
values (1,'1','柳岩','女',20,'12345678912345678','北京','2001-01-01'),
(2,'2','张无忌','男',18,'123456789012345670','北京','2005-09-01'),
(3,'3','韦一笑','男',38,'12345678972345670','上海','2005-08-01'),
(4,'4','赵敏','女',18,'12345675712345670','北京','2009-12-01'),
(5,'5','小昭','女',16,'123456769012345678','上海','2007-07-01'),
(6,'6','杨逍','男',28,'1234567893123456X','北京','2006-01-01'),
(7,'7','范瑶','男',40,'123456789212345670','北京','2005-05-01'),
(8,'8','黛绮丝','女',38,'123456157123645670','天津','2015-05-01'),
(9,'9','范凉凉','女',45,'123156789012345678','北京','2010-04-01'),
(10,'10','陈友谅','男',53,'123456789012345670','上海','2011-01-01'),
(11,'11','张士诚','男',55,'12356789712345670','江苏','2015-05-01'),
(12,'12','常遇春','男',32,'123446757152345670','北京','2004-02-01'),
(13,'13','张三丰','男',88,'123656789012345678','江苏','2020-11-01'),
(14,'14','灭绝','女',65,'123456719012345670','西安','2019-05-01'),
(15,'15','胡青年','男',70,'12345674971234567X','西安','2018-04-01'),
(16,'16','周芷若','女',18,null,'北京','2012-06-01');

1.5.1 基础查询

-- 查看插入的数据
select * from emp;

-- 查询指定字段 name workno age 返回
select name, workno, age
from emp;

-- 查询所有字段返回
select id,
workno,
name,
gender,
age,
idcard,
workaddress,
entrydate
from emp;

-- distinct去重关键字
-- 查询所有员工的工作地址(不要重复的地址)
select distinct workaddress '工作地址'
from emp;

1.5.2 条件查询

语法: SELECT 字段列表 FROM 表名 WHERE 条件列表 ;

image-20231001154822524

image-20231001154903121

举例

-- 条件查询
-- 查询年龄等于 88 的员工
select * from emp where age = 88;

-- 查询年龄小于20的员工信息
select * from emp where age < 20;

-- 查询年龄小于等于20的员工信息
select * from emp where age <= 20;

-- 查询没有身份证号的员工信息
select * from emp where idcard is null ;

-- 查询年龄不等于88的员工信息(<>也表示不等于)
select * from emp where age != 88;
select * from emp where age <> 88;

-- 查询年龄在15~20(包含)之间的员工信息(三种实现方式)
select * from emp where age >= 15 && age <= 20;
select * from emp where age >= 15 and age <= 20;
select * from emp where age between 15 and 20; #包含了1520

-- 查询性别为女 且年龄小于25岁的员工
select * from emp where gender = '女' and age < 25;

-- 查询年龄等于18 或 20 或 48 的员工
select * from emp where age = 18 or age = 20 or age = 48;
select * from emp where age in (18,20,48);

-- 查询姓名为两个字的员工(模糊匹配(_匹配单个字符, %匹配任意个字符))
select * from emp where name like '__';

-- 查询身份证号最后一位是X的员工信息
select * from emp where idcard like '%X';

1.5.3 聚合函数

语法: SELECT 聚合函数(字段列表) FROM 表名 ;

注意 : NULL值是不参与所有聚合函数运算的

image-20231001160812611

-- 聚合函数
-- 统计该企业员工数量(空值是不参与统计的)
select count(*) from emp;
select count(idcard) from emp;

-- 统计该企业员工的平均年龄
select avg(age) from emp;

-- 统计该企业员工的最大年龄
select max(age) from emp;

-- 统计该企业员工的最小年龄
select min(age) from emp;

-- 统计西安地区员工的年龄之和
select sum(age) from emp where workaddress = '西安';

1.5.4 分组查询

语法: SELECT 字段列表 FROM 表名 [ WHERE 条件 ] GROUP BY 分组字段名 [ HAVING 分组 后过滤条件 ];

where与having区别

  • 执行时机不同:where是分组之前进行过滤,不满足where条件,不参与分组;而having是分组之后对结果进行过滤
  • 判断条件不同:where不能对聚合函数进行判断,而having可以

注意事项:
• 分组之后,查询的字段一般为聚合函数和分组字段,查询其他字段无任何意义。
• 执行顺序: where > 聚合函数 > having 。
• 支持多字段分组, 具体语法为 : group by columnA,columnB

image-20231001163315161

-- 分组查询
-- 根据性别分组,统计男性员工和女性员工的数量
select gender, count(*) from emp group by gender;

-- 根据性别进行分组,统计男性员工和女性员工的平均年龄
select gender, avg(age) from emp group by gender;

-- 查询年龄小于45岁的员工,并根据工作地址分组,获取员工数量大于等于3的工作地址(可以使用别名也可以不使用别名)
select workaddress,count(*) address_count from emp where age < 45 group by workaddress having address_count >= 3;

1.5.5 排序查询

语法: SELECT 字段列表 FROM 表名 ORDER BY 字段1 排序方式1 , 字段2 排序方式2 ;

注意事项:
• 如果是升序, 可以不指定排序方式ASC,因为升序ASC是默认值 ;
• 如果是多字段排序,当第一个字段值相同时,才会根据第二个字段进行排序 ;

-- 排序查询
-- 根据年龄对公司的员工进行升序排序(asc可以省略)
select * from emp order by age ;
-- 降序排序
select * from emp order by age desc ;

-- 根据员工的入职时间降序排序
select * from emp order by entrydate desc ;

-- 根据员工的年龄升序排序,年龄相同,再按照入职时间进行降序排序
select * from emp order by age asc ,entrydate desc ;

1.5.6 分页查询

语法: SELECT 字段列表 FROM 表名 LIMIT 起始索引, 查询记录数 ;

注意事项:
• 起始索引从0开始,起始索引 = (查询页码 - 1)* 每页显示记录数。
• 分页查询是数据库的方言,不同的数据库有不同的实现,MySQL中是LIMIT。
• 如果查询的是第一页数据,起始索引可以省略,直接简写为 limit 10。

-- 分页查询
-- 查询第一页的员工数据,每页显示10条记录
select * from emp limit 0,10;

-- 查询第二页数据,每页展示十条数据(起始索引 = (查询页码 - 1)* 每页显示记录数)
select * from emp limit 10,10;

1.5.7 DQL语句案例

-- DQL语句案例
-- 查询年龄在20,21,22,23岁的女性员工的信息
select * from emp where gender = '女' and age in (20,21,22,23);

-- 查询性别为男,并且年龄再20~40岁(含)以内的姓名为三个字的员工
select * from emp where gender = '男' and (age between 20 and 40) and name like '___';

-- 统计员工表中,年龄小于60岁的,男性员工和女性员工的人数
select gender,count(*) from emp where age < 60 group by gender;

-- 查询所有年龄小于等于35岁员工的姓名和年龄,并对查询结果按年龄升序排序,如果年龄相同按照入职时间降序排序
select name,age from emp where age <= 35 order by age asc ,entrydate desc ;

-- 查询性别为男,且年龄在20·40岁(含)以内的前五个员工信息,并对查询结果按年龄升序排序,如果年龄相同按照入职时间升序排序
select * from emp where gender = '男' and age between 20 and 40 order by age,entrydate limit 5;

1.5.8 执行顺序

image-20231001171425562

image-20231001172032063

总结

image-20231001172211635

1.6 DCL-数据控制语言

1.6.1 管理用户

查询用户: select * from mysql.user;

创建用户:CREATE USER '用户名'@'主机名' IDENTIFIED BY '密码';

修改用户密码: ALTER USER '用户名'@'主机名' IDENTIFIED WITH mysql_native_password BY '新密码' ;

删除用户:DROP USER '用户名'@'主机名' ;

image-20231001172731731

注意事项:
• 在MySQL中需要通过用户名@主机名的方式,来唯一标识一个用户

• 主机名可以使用 % 通配

• 这类SQL开发人员操作的比较少,主要是DBA( Database Administrator 数据库管理员)使用

-- 切换到系统中自带的mysql数据库
use mysql;

-- 查询所有的用户
select * from mysql.user;

-- 创建用户itcast, 只能够在当前主机localhost访问, 密码123456;
create user 'itcast'@'localhost' identified by '123456';

-- 创建用户heima, 可以在任意主机访问该数据库, 密码123456;
create user 'heima'@'%' identified by '123456';

-- 修改用户heima的访问密码为1234;
alter user 'heima'@'%' identified with mysql_native_password by '1234';

-- 删除 itcast@localhost 用户
drop user 'itcast'@'localhost';

1.6.2 权限控制

查询权限: SHOW GRANTS FOR '用户名'@'主机名' ;

授予权限: GRANT 权限列表 ON 数据库名.表名 TO '用户名'@'主机名';

撤销权限: REVOKE 权限列表 ON 数据库名.表名 FROM '用户名'@'主机名';

注意事项:
• 多个权限之间,使用逗号分隔
• 授权时, 数据库名和表名可以使用 * 进行通配,代表所有

image-20231001174406029

-- 权限控制
-- 查询权限(查询heima用户的所有权限)
show grants for 'heima'@'%';

-- 授予权限(授予heima用户对数据库itcast的所有权限)
grant all on itcast.* to 'heima'@'%';

-- 授予用户所有数据库所有表的所有权限(相当于超级管理员)
grant all on *.* to 'heima'@'%';

-- 撤销权限(撤销heima用户在itcast数据库上的所有权限)
revoke all on itcast.* from 'heima'@'%';

总结

image-20231001213059144

全部的SQL

-- ------------------------------------------------------数据库的操作------------------------------------------------------------------------
-- 展示所有的数据库
show databases;

-- 查询当前的数据库
select database();

-- 创建数据库
create database if not exists itcast;

-- 删除数据库
drop database if exists itcast;

-- 使用数据库
use itcast;


-- ----------------------------------------------------------表的操作--------------------------------------------------------------------

-- 创建用户表
create table tb_user
(
id int comment '编号',
name varchar(50) comment '姓名',
age int comment '年龄',
gender varchar(1) comment '性别'
) comment '用户表';

-- 显示创建表的结构
desc tb_user;

-- 显示创建表的语句
show create table tb_user;

-- 删除表
drop table emp;

-- 修改表的操作
-- 举例:现有一张员工表
create table emp
(
id int comment '编号',
workno varchar(10) comment '工号',
name varchar(10) comment '姓名',
gender char(1) comment '性别',
age tinyint unsigned comment '年龄',
idcard char(18) comment '身份证号',
entrydate date comment '入职时间'
) comment '员工表';
-- 查看表结构
desc emp;

-- 修改表结构(向emp表中添加昵称字段)
alter table emp
add nickname varchar(20) comment '昵称';

-- 修改相应字段的数据类型(把age的数据类型从int改回tinyint)
alter table emp
modify age tinyint;

-- 修改字段名和字段的类型(把nickname字段改为username,并修改数据类型)
alter table emp
change nickname username varchar(30) comment '用户名';

-- 删除字段(删除username字段)
alter table emp
drop username;

-- 修改表名(将表名字从emo修改为employee)
alter table emp rename to employee;

-- 删除表
drop table if exists employee;
-- 删除指定表,并重新创建该表(相当于删除表中的全部数据)
truncate table employee;



-- ------------------------------------------------DML-增删改操作---------------------------------------------------
-- 添加数据
select database();
show tables;
-- 该指定的字段添加数据
insert into employee(id, workno, name, gender, age, idcard, entrydate) value (1, '1', '小华', '男', 20, '123456789123456789', '2001-01-01');

select *
from employee;
-- 给全部的字段添加数据
insert into employee value (2, '2', '小刚', '男', 23, '123456784124456789', '2008-01-01');
-- 批量添加数据
insert into employee
values (3, '3', '小李', '男', 23, '123456784124456789', '2008-01-01'),
(4, '4', '小黄', '男', 23, '123456784124456789', '2008-01-01');

-- 修改数据(不带条件修改所有)
update employee
set name = '张三'
where id = 1;
update employee
set name = '李四',
gender= '女'
where id = 1;

-- 删除数据(删除id为4的数据,不带添加删除所有)
delete
from employee
where id = 4;


-- --------------------------------------------------------DQL数据查询语言--------------------------------------------------
-- 数据准备
create table emp
(
id int comment '编号',
workno varchar(10) comment '工号',
name varchar(10) comment '姓名',
gender char(1) comment '性别',
age tinyint unsigned comment '年龄',
idcard char(18) comment '身份证号',
workaddress varchar(50) comment '工作地址',
entrydate date comment '入职时间'
) comment '员工表';

-- 插入数据
insert into emp(id, workno, name, gender, age, idcard, workaddress, entrydate)
values (1, '1', '柳岩', '女', 20, '12345678912345678', '北京', '2001-01-01'),
(2, '2', '张无忌', '男', 18, '123456789012345670', '北京', '2005-09-01'),
(3, '3', '韦一笑', '男', 38, '12345678972345670', '上海', '2005-08-01'),
(4, '4', '赵敏', '女', 18, '12345675712345670', '北京', '2009-12-01'),
(5, '5', '小昭', '女', 16, '123456769012345678', '上海', '2007-07-01'),
(6, '6', '杨逍', '男', 28, '1234567893123456X', '北京', '2006-01-01'),
(7, '7', '范瑶', '男', 40, '123456789212345670', '北京', '2005-05-01'),
(8, '8', '黛绮丝', '女', 38, '123456157123645670', '天津', '2015-05-01'),
(9, '9', '范凉凉', '女', 45, '123156789012345678', '北京', '2010-04-01'),
(10, '10', '陈友谅', '男', 53, '123456789012345670', '上海', '2011-01-01'),
(11, '11', '张士诚', '男', 55, '12356789712345670', '江苏', '2015-05-01'),
(12, '12', '常遇春', '男', 32, '123446757152345670', '北京', '2004-02-01'),
(13, '13', '张三丰', '男', 88, '123656789012345678', '江苏', '2020-11-01'),
(14, '14', '灭绝', '女', 65, '123456719012345670', '西安', '2019-05-01'),
(15, '15', '胡青年', '男', 70, '12345674971234567X', '西安', '2018-04-01'),
(16, '16', '周芷若', '女', 18, null, '北京', '2012-06-01');

-- 基础查询
-- 查看插入的数据
select * from emp;

-- 查询指定字段 name workno age 返回
select name, workno, age
from emp;

-- 查询所有字段返回
select id,
workno,
name,
gender,
age,
idcard,
workaddress,
entrydate
from emp;

-- distinct去重关键字
-- 查询所有员工的工作地址(不要重复的地址)
select distinct workaddress '工作地址'
from emp;





-- 条件查询
-- 查询年龄等于 88 的员工
select * from emp where age = 88;

-- 查询年龄小于20的员工信息
select * from emp where age < 20;

-- 查询年龄小于等于20的员工信息
select * from emp where age <= 20;

-- 查询没有身份证号的员工信息
select * from emp where idcard is null ;

-- 查询年龄不等于88的员工信息(<>也表示不等于)
select * from emp where age != 88;
select * from emp where age <> 88;

-- 查询年龄在15~20(包含)之间的员工信息(三种实现方式)
select * from emp where age >= 15 && age <= 20;
select * from emp where age >= 15 and age <= 20;
select * from emp where age between 15 and 20; #包含了1520

-- 查询性别为女 且年龄小于25岁的员工
select * from emp where gender = '女' and age < 25;

-- 查询年龄等于18 或 20 或 48 的员工
select * from emp where age = 18 or age = 20 or age = 48;
select * from emp where age in (18,20,48);

-- 查询姓名为两个字的员工(模糊匹配(_匹配单个字符, %匹配任意个字符))
select * from emp where name like '__';

-- 查询身份证号最后一位是X的员工信息
select * from emp where idcard like '%X';




-- 聚合函数
-- 统计该企业员工数量(空值是不参与统计的)
select count(*) from emp;
select count(idcard) from emp;

-- 统计该企业员工的平均年龄
select avg(age) from emp;

-- 统计该企业员工的最大年龄
select max(age) from emp;

-- 统计该企业员工的最小年龄
select min(age) from emp;

-- 统计西安地区员工的年龄之和
select sum(age) from emp where workaddress = '西安';





-- 分组查询
-- 根据性别分组,统计男性员工和女性员工的数量
select gender, count(*) from emp group by gender;

-- 根据性别进行分组,统计男性员工和女性员工的平均年龄
select gender, avg(age) from emp group by gender;

-- 查询年龄小于45岁的员工,并根据工作地址分组,获取员工数量大于等于3的工作地址(可以使用别名也可以不使用别名)
select workaddress,count(*) address_count from emp where age < 45 group by workaddress having address_count >= 3;




-- 排序查询
-- 根据年龄对公司的员工进行升序排序(asc可以省略)
select * from emp order by age ;
-- 降序排序
select * from emp order by age desc ;

-- 根据员工的入职时间降序排序
select * from emp order by entrydate desc ;

-- 根据员工的年龄升序排序,年龄相同,再按照入职时间进行降序排序
select * from emp order by age asc ,entrydate desc ;




-- 分页查询
-- 查询第一页的员工数据,每页显示10条记录
select * from emp limit 0,10;

-- 查询第二页数据,每页展示十条数据(起始索引 = (查询页码 - 1)* 每页显示记录数)
select * from emp limit 10,10;


-- DQL语句案例
-- 查询年龄在20,21,22,23岁的女性员工的信息
select * from emp where gender = '女' and age in (20,21,22,23);

-- 查询性别为男,并且年龄再20~40岁(含)以内的姓名为三个字的员工
select * from emp where gender = '男' and (age between 20 and 40) and name like '___';

-- 统计员工表中,年龄小于60岁的,男性员工和女性员工的人数
select gender,count(*) from emp where age < 60 group by gender;

-- 查询所有年龄小于等于35岁员工的姓名和年龄,并对查询结果按年龄升序排序,如果年龄相同按照入职时间降序排序
select name,age from emp where age <= 35 order by age asc ,entrydate desc ;

-- 查询性别为男,且年龄在20·40岁(含)以内的前五个员工信息,并对查询结果按年龄升序排序,如果年龄相同按照入职时间升序排序
select * from emp where gender = '男' and age between 20 and 40 order by age,entrydate limit 5;



-- ---------------------------------------------------DCL--------------------------------------------------
-- 用户管理
-- mysql数据库
use mysql;

-- 查询所有的用户
select * from mysql.user;

-- 创建用户itcast, 只能够在当前主机localhost访问, 密码123456;
create user 'itcast'@'localhost' identified by '123456';

-- 创建用户heima, 可以在任意主机访问该数据库, 密码123456;
create user 'heima'@'%' identified by '123456';

-- 修改用户heima的访问密码为1234;
alter user 'heima'@'%' identified with mysql_native_password by '1234';

-- 删除 itcast@localhost 用户
drop user 'itcast'@'localhost';




-- 权限控制
-- 查询权限(查询heima用户的所有权限)
show grants for 'heima'@'%';

-- 授予权限(授予heima用户对数据库itcast的所有权限)
grant all on itcast.* to 'heima'@'%';

-- 授予用户所有数据库所有表的所有权限
grant all on *.* to 'heima'@'%';

-- 撤销权限(撤销heima用户在itcast数据库上的所有权限)
revoke all on itcast.* from 'heima'@'%';

2.函数

2.1 字符串函数

image-20231001213558221

-- concat函数(字符串拼接)
select concat('hello ','word'); # hello word

-- lower(全部转化成小写)
select lower('Hello'); # hello

-- upper(全部转化成大写)
select upper('Hello'); # HELLO

-- lpad(左侧填充)
select lpad('01',5,'-'); # ---01

-- rpad(右侧填充)
select rpad('01',5,'-') ;# 01---

-- trim 去除头部和尾部的空格
select trim(' Hello Mysql ');# Hello Mysql

-- substring 字符串截取
select substring('Hello Mysql',1,5);# Hello

案例

-- 由于业务需求变更,企业员工的工号,统一为5位数,目前不足5位数的全部在前面补0 比如:1号员工的工号应该为00001
update emp set workno = lpad(workno,5,'0');

2.2 数值函数

image-20231001215546554

-- 数值函数
-- ceil 向上取整
select ceil(1.5); # 2

-- floor 向下取整
select floor(1.1); # 1

-- mod 求模运算
select mod(7,4); # 3

-- rand 随机数(0~1)
select rand();

-- round 四舍五入
select round(2.345,2); # 2.35

案例

-- 通过数据库函数生成一个的随机的六位验证码
-- 思路是通过rand()*100生成xxxxxx.xxx的小数,然后通过round四舍五入掉小数位,然后可能存在小于六位数的情况,我们
-- 通过lpd函数补齐六位
select lpad(round(rand()*1000000,0),6,'0');

2.3 日期函数

image-20231002114046980

-- 日期函数
-- curdate:当前日期
select curdate();

-- curtime:当前时间
select curtime();

-- now:当前日期和时间
select now();

-- YEAR , MONTH , DAY:当前年、月、日
select year(now());
select month(now());
select day(now());

-- date_add:增加指定的时间间隔
select date_add(now(),INTERVAL 70 DAY ); # 当前的时间往后推70
select date_add(now(),INTERVAL 2 MONTH ); #向后推2个月

-- datediff:获取两个日期相差的天数
select datediff('2021-12-01','2021-10-01'); # 查询两个时间之间的差值,第一个时间减去第二个时间

案例

-- 查询所有员工的入职天数,并根据入职天数倒叙排序
select name,datediff(curdate(),entrydate) 'entrydays' from emp order by entrydays desc ;

2.4 流程函数

image-20231004124942068

-- 流程控制函数
-- if 相当java中的三元运算符
select if(true, 'OK', 'ERROR');

-- ifnull 第一个字符串不为空就返回第一个,第一个为空就返回第二个
select ifnull('OK', 'DEFAULT');
select ifnull(null, 'DEFAULT');

-- case when then else end
-- 需求: 查询emp表的员工姓名和工作地址 (北京/上海 ----> 一线城市 , 其他 ----> 二线城市)
select name,
(case workaddress
when '北京' then '一线城市'
when '上海' then '一线城市'
else
'二线城市' end) as '工作地址'
from emp;

案例

-- 案例:统计班级各个学员的成绩,展示规则如下:
-- >= 85 展示优秀
-- >= 60 展示及格
-- 否则展示不及格

-- 建表数据
create table score
(
id int comment 'ID',
name varchar(20) comment '姓名',
math int comment '数学',
english int comment '英语',
chinese int comment '语文'
) comment '学员成绩表';
insert into score(id, name, math, english, chinese)
VALUES (1, 'Tom', 67, 88, 95),
(2, 'Rose', 23, 66, 90),
(3, 'Jack', 56, 98, 76);

select *
from score;

-- 案例sql实现
select id,
name,
(case when math >= 85 then '优秀' when math >= 60 then '及格' else '不及格' end)
'数学',
(case
when english >= 85 then '优秀'
when english >= 60 then '及格'
else '不及格'
end) '英语',
(case
when chinese >= 85 then '优秀'
when chinese >= 60 then '及格'
else '不及格'
end) '语文'
from score;

函数相关的全部sql

-- ----------------------------------------------------------函数--------------------------------------------------------
-- 字符串函数
-- concat函数(字符串拼接)
select concat('hello ', 'word');
# hello word

-- lower(全部转化成小写)
select lower('Hello');
# hello

-- upper(全部转化成大写)
select upper('Hello');
# HELLO

-- lpad(左侧填充)
select lpad('01', 5, '-');
# ---01

-- rpad(右侧填充)
select rpad('01', 5, '-');
# 01---

-- trim 去除头部和尾部的空格
select trim(' Hello Mysql ');
# Hello Mysql

-- substring 字符串截取
select substring('Hello Mysql', 1, 5);
# Hello

-- 由于业务需求变更,企业员工的工号,统一为5位数,目前不足5位数的全部在前面补0 比如:1号员工的工号应该为00001
update emp
set workno = lpad(workno, 5, '0');



-- 数值函数
-- ceil 向上取整
select ceil(1.5);
# 2

-- floor 向下取整
select floor(1.1);
# 1

-- mod 求模运算
select mod(7, 4);
# 3

-- rand 随机数(0~1)
select rand();

-- round 四舍五入
select round(2.345, 2);
# 2.35

-- 通过数据库函数生成一个的随机的六位验证码
select lpad(round(rand() * 1000000, 0), 6, '0');


-- 日期函数
-- curdate:当前日期
select curdate();

-- curtime:当前时间
select curtime();

-- now:当前日期和时间
select now();

-- YEAR , MONTH , DAY:当前年、月、日
select year(now());
select month(now());
select day(now());

-- date_add:增加指定的时间间隔
select date_add(now(), INTERVAL 70 DAY); # 当前的时间往后推70
select date_add(now(), INTERVAL 2 MONTH);
#向后推2个月

-- datediff:获取两个日期相差的天数
select datediff('2021-12-01', '2021-10-01');
# 查询两个时间之间的差值,第一个时间减去第二个时间

-- 查询所有员工的入职天数,并根据入职天数倒叙排序
select name, datediff(curdate(), entrydate) 'entrydays'
from emp
order by entrydays desc;



-- 流程控制函数
-- if 相当java中的三元运算符
select if(true, 'OK', 'ERROR');

-- ifnull 第一个字符串不为空就返回第一个,第一个为空就返回第二个
select ifnull('OK', 'DEFAULT');
select ifnull(null, 'DEFAULT');

-- case when then else end
-- 需求: 查询emp表的员工姓名和工作地址 (北京/上海 ----> 一线城市 , 其他 ----> 二线城市)
select name,
(case workaddress
when '北京' then '一线城市'
when '上海' then '一线城市'
else
'二线城市' end) as '工作地址'
from emp;

-- 案例:统计班级各个学员的成绩,展示规则如下:
-- >= 85 展示优秀
-- >= 60 展示及格
-- 否则展示不及格

-- 建表数据
create table score
(
id int comment 'ID',
name varchar(20) comment '姓名',
math int comment '数学',
english int comment '英语',
chinese int comment '语文'
) comment '学员成绩表';
insert into score(id, name, math, english, chinese)
VALUES (1, 'Tom', 67, 88, 95),
(2, 'Rose', 23, 66, 90),
(3, 'Jack', 56, 98, 76);

select *
from score;

-- 案例sql实现
select id,
name,
(case when math >= 85 then '优秀' when math >= 60 then '及格' else '不及格' end)
'数学',
(case
when english >= 85 then '优秀'
when english >= 60 then '及格'
else '不及格'
end) '英语',
(case
when chinese >= 85 then '优秀'
when chinese >= 60 then '及格'
else '不及格'
end) '语文'
from score;

3.约束

3.1 概述

概念:约束是作用于表中字段上的规则,用于限制存储在表中的数据。

目的:保证数据库中数据的正确、有效性和完整性。

分类 :

image-20231004130826351

3.2 约束演示

案例需求: 根据需求,完成表结构的创建,需求如下:

image-20231004160744943

对应的建表语句为:

create table tb_user(
id int AUTO_INCREMENT PRIMARY KEY COMMENT 'ID唯一标识',
name varchar(10) NOT NULL UNIQUE COMMENT '姓名',
age int check (age > 0 && age <= 120) COMMENT '年龄',
status char(1) default '1' COMMENT '状态',
gender char(1) COMMENT '性别'
);

3.3 外键约束

3.3.1 介绍

image-20231004162616775

3.3.2 语法

建立外键: ALTER TABLE 表名 ADD CONSTRAINT 外键名称 FOREIGN KEY (外键字段名) REFERENCES 主表 (主表列名) ;

删除外键: ALTER TABLE 表名 DROP FOREIGN KEY 外键名称;

image-20231004163021062

举例

-- 外键约束
create table dept
(
id int auto_increment comment 'ID' primary key,
name varchar(50) not null comment '部门名称'
) comment '部门表';

INSERT INTO dept (id, name)
VALUES (1, '研发部'),
(2, '市场部'),
(3, '财务部'),
(4, '销售部'),
(5, '总经办');

create table employee
(
id int auto_increment comment 'ID' primary key,
name varchar(50) not null comment '姓名',
age int comment '年龄',
job varchar(20) comment '职位',
salary int comment '薪资',
entrydate date comment '入职时间',
managerid int comment '直属领导ID',
dept_id int comment '部门ID'
) comment '员工表';

INSERT INTO employee (id, name, age, job, salary, entrydate, managerid, dept_id)
VALUES (1, '金庸', 66, '总裁', 20000, '2000-01-01', null, 5),
(2, '张无忌', 20, '项目经理', 12500, '2005-12-05', 1, 1),
(3, '杨逍', 33, '开发', 8400, '2000-11-03', 2, 1),
(4, '韦一笑', 48, '开发', 11000, '2002-02-05', 2, 1),
(5, '常遇春', 43, '开发', 10500, '2004-09-07', 3, 1),
(6, '小昭', 19, '程序员鼓励师', 6600, '2004-10-12', 2, 1);

select *
from dept;
select *
from employee;

-- 添加外键 员工表employee的部门id字段关联到部门表的id字段以建立外键约束
alter table employee
add constraint fk_emp_dept foreign key (dept_id) references dept (id);

-- 删除外键
alter table employee drop foreign key fk_emp_dept;

3.3.3 删除/更新行为

详细介绍见PDF文件 https://jasonsgong.gitee.io/posts/50465.html

添加了外键之后,再删除父表数据时产生的约束行为,我们就称为删除/更新行为。具体的删除/更新行为有以下几种:

image-20231004164211535

具体语法为:

ALTER TABLE 表名 ADD CONSTRAINT 外键名称 FOREIGN KEY (外键字段) REFERENCES 主表名 (主表字段名) ON UPDATE CASCADE ON DELETE CASCADE;

约束相关的sql

-- ---------------------------------------------------约束演示------------------------------------------------------------
-- id 主键,并且自动增长
-- name 不为空,并且唯一
-- age 大于零,并且小于等于120(8.0以上的数据库才支持)age int check (age > 0 && age <= 120) COMMENT '年龄'
-- status 如果没有指定该值,默认为1
-- gender 无约束
drop table if exists user;
create table user
(
id int AUTO_INCREMENT PRIMARY KEY COMMENT 'ID唯一标识',
name varchar(10) NOT NULL UNIQUE COMMENT '姓名',
age int COMMENT '年龄',
status char(1) default '1' COMMENT '状态',
gender char(1) COMMENT '性别'
);
select *
from user;

-- 插入数据
-- name的值不能重复,status的值没有填写的话,将使用的是默认的值
insert into user(name, age, status, gender)
values ('Tom1', 19, '1', '男'),
('Tom2', 29, '1', '男'),
('Tom3', 14, '1', '男');


-- 外键约束
create table dept
(
id int auto_increment comment 'ID' primary key,
name varchar(50) not null comment '部门名称'
) comment '部门表';

INSERT INTO dept (id, name)
VALUES (1, '研发部'),
(2, '市场部'),
(3, '财务部'),
(4, '销售部'),
(5, '总经办');

create table employee
(
id int auto_increment comment 'ID' primary key,
name varchar(50) not null comment '姓名',
age int comment '年龄',
job varchar(20) comment '职位',
salary int comment '薪资',
entrydate date comment '入职时间',
managerid int comment '直属领导ID',
dept_id int comment '部门ID'
) comment '员工表';

INSERT INTO employee (id, name, age, job, salary, entrydate, managerid, dept_id)
VALUES (1, '金庸', 66, '总裁', 20000, '2000-01-01', null, 5),
(2, '张无忌', 20, '项目经理', 12500, '2005-12-05', 1, 1),
(3, '杨逍', 33, '开发', 8400, '2000-11-03', 2, 1),
(4, '韦一笑', 48, '开发', 11000, '2002-02-05', 2, 1),
(5, '常遇春', 43, '开发', 10500, '2004-09-07', 3, 1),
(6, '小昭', 19, '程序员鼓励师', 6600, '2004-10-12', 2, 1);

select *
from dept;
select *
from employee;

-- 添加外键 员工表employee的部门id字段关联到部门表的id字段以建立外键约束
alter table employee
add constraint fk_emp_dept foreign key (dept_id) references dept (id);

-- 删除外键
alter table employee drop foreign key fk_emp_dept;

4.多表查询

4.1 多表关系

一对多

image-20231004165257714

多对多

image-20231004165425089

一对一

image-20231004170148951

-- 多表关系
-- 演示多对多的关系
create table student
(
id int auto_increment primary key comment '主键ID',
name varchar(10) comment '姓名',
no varchar(10) comment '学号'
) comment '学生表';

insert into student
values (null, '黛绮丝', '2000100101'),
(null, '谢逊',
'2000100102'),
(null, '殷天正', '2000100103'),
(null, '韦一笑', '2000100104');

create table course
(
id int auto_increment primary key comment '主键ID',
name varchar(10) comment '课程名称'
) comment '课程表';

insert into course
values (null, 'Java'),
(null, 'PHP'),
(null, 'MySQL'),
(null, 'Hadoop');

create table student_course
(
id int auto_increment comment '主键' primary key,
studentid int not null comment '学生ID',
courseid int not null comment '课程ID',
constraint fk_courseid foreign key (courseid) references course (id),
constraint fk_studentid foreign key (studentid) references student (id)
) comment '学生课程中间表';

insert into student_course
values (null, 1, 1),
(null, 1, 2),
(null, 1, 3),
(null, 2, 2),
(null, 2, 3),
(null, 3, 4);


-- 演示一对一的关系
create table tb_user
(
id int auto_increment primary key comment '主键ID',
name varchar(10) comment '姓名',
age int comment '年龄',
gender char(1) comment '1: 男 , 2: 女',
phone char(11) comment '手机号'
) comment '用户基本信息表';
create table tb_user_edu
(
id int auto_increment primary key comment '主键ID',
degree varchar(20) comment '学历',
major varchar(50) comment '专业',
primaryschool varchar(50) comment '小学',
middleschool varchar(50) comment '中学',
university varchar(50) comment '大学',
userid int unique comment '用户ID',
constraint fk_userid foreign key (userid) references tb_user (id)
) comment '用户教育信息表';
insert into tb_user(id, name, age, gender, phone)
values (null, '黄渤', 45, '1', '18800001111'),
(null, '冰冰', 35, '2', '18800002222'),
(null, '码云', 55, '1', '18800008888'),
(null, '李彦宏', 50, '1', '18800009999');
insert into tb_user_edu(id, degree, major, primaryschool, middleschool,
university, userid)
values (null, '本科', '舞蹈', '静安区第一小学', '静安区第一中学', '北京舞蹈学院', 1),
(null, '硕士', '表演', '朝阳区第一小学', '朝阳区第一中学', '北京电影学院', 2),
(null, '本科', '英语', '杭州市第一小学', '杭州市第一中学', '杭州师范大学', 3),
(null, '本科', '应用数学', '阳泉第一小学', '阳泉区第一中学', '清华大学', 4);

4.2 多表查询概述

4.2.1 数据准备

-- 数据准备
-- 创建dept表,并插入数据
create table dept
(
id int auto_increment comment 'ID' primary key,
name varchar(50) not null comment '部门名称'
) comment '部门表';
INSERT INTO dept (id, name)
VALUES (1, '研发部'),
(2, '市场部'),
(3, '财务部'),
(4,
'销售部'),
(5, '总经办'),
(6, '人事部');

-- 创建emp表,并插入数据
create table emp
(
id int auto_increment comment 'ID' primary key,
name varchar(50) not null comment '姓名',
age int comment '年龄',
job varchar(20) comment '职位',
salary int comment '薪资',
entrydate date comment '入职时间',
managerid int comment '直属领导ID',
dept_id int comment '部门ID'
) comment '员工表';

-- 添加外键
alter table emp
add constraint fk_emp_dept_id foreign key (dept_id) references
dept (id);

INSERT INTO emp (id, name, age, job, salary, entrydate, managerid, dept_id)
VALUES (1, '金庸', 66, '总裁', 20000, '2000-01-01', null, 5),
(2, '张无忌', 20, '项目经理', 12500, '2005-12-05', 1, 1),
(3, '杨逍', 33, '开发', 8400, '2000-11-03', 2, 1),
(4, '韦一笑', 48, '开发', 11000, '2002-02-05', 2, 1),
(5, '常遇春', 43, '开发', 10500, '2004-09-07', 3, 1),
(6, '小昭', 19, '程序员鼓励师', 6600, '2004-10-12', 2, 1),
(7, '灭绝', 60, '财务总监', 8500, '2002-09-12', 1, 3),
(8, '周芷若', 19, '会计', 4800, '2006-06-02', 7, 3),
(9, '丁敏君', 23, '出纳', 5250, '2009-05-13', 7, 3),
(10, '赵敏', 20, '市场部总监', 12500, '2004-10-12', 1, 2),
(11, '鹿杖客', 56, '职员', 3750, '2006-10-03', 10, 2),
(12, '鹤笔翁', 19, '职员', 3750, '2007-05-09', 10, 2),
(13, '方东白', 19, '职员', 5500, '2009-02-12', 10, 2),
(14, '张三丰', 88, '销售总监', 14000, '2004-10-12', 1, 4),
(15, '俞莲舟', 38, '销售', 4600, '2004-10-12', 14, 4),
(16, '宋远桥', 40, '销售', 4600, '2004-10-12', 14, 4),
(17, '陈友谅', 42, null, 2000, '2011-10-12', 1, null);

4.2.2 概述

image-20231004200243641

4.2.3 分类

image-20231004200624090

4.3 内连接

image-20231004200731770

-- 内连接演示
-- 案例
-- 查询每一个员工的姓名 , 及关联的部门的名称 (隐式内连接实现)
-- 表结构: emp , dept
-- 连接条件: emp.dept_id = dept.id
select e.name, d.name from emp e, dept d where e.dept_id = d.id;

-- 查询每一个员工的姓名 , 及关联的部门的名称 (显式内连接实现) --- INNER JOIN ...ON ...
-- 表结构: emp , dept
-- 连接条件: emp.dept_id = dept.id
select e.name ,d.name from emp e inner join dept d on e.dept_id = d.id;

一旦为表起了别名,就不能再使用表名来指定对应的字段了,此时只能够使用别名来指定字段。

4.4 外连接

image-20231004201445881

-- 外连接
-- 查询emp表的所有数据, 和对应的部门信息
-- 由于需求中提到,要查询emp的所有数据,所以是不能内连接查询的,需要考虑使用外连接查询。
-- 表结构: emp, dept
-- 连接条件: emp.dept_id = dept.id
select e.name,d.name from emp e left join dept d on e.dept_id = d.id;

-- 查询dept表的所有数据, 和对应的员工信息(右外连接)
-- 由于需求中提到,要查询dept表的所有数据,所以是不能内连接查询的,需要考虑使用外连接查询
-- 表结构: emp, dept
-- 连接条件: emp.dept_id = dept.id
select e.name,d.name from emp e right join dept d on e.dept_id = d.id;

注意事项:左外连接和右外连接是可以相互替换的,只需要调整在连接查询时SQL中,表结构的先后顺序就可以了。而我们在日常开发使用时,更偏向于左外连接。

4.5 自连接

image-20231004202219787

-- 自连接
-- 查询员工 及其 所属领导的名字
-- 表结构: emp
-- 技巧:查询的时候看作是两张表
select e1.name '员工' ,e2.name '领导' from emp e1,emp e2 where e1.managerid = e2.id;

-- 查询所有员工 emp 及其领导的名字 emp , 如果员工没有领导, 也需要查询出来
-- 表结构: emp a , emp b
select e1.name '员工' ,e2.name '领导' from emp e1 left join emp e2 on e1.managerid = e2.id;

4.6 联合查询

image-20231004203207924

-- 联合查询
-- 将薪资低于 5000 的员工 , 和 年龄大于 50 岁的员工全部查询出来.
-- 当前对于这个需求,我们可以直接使用多条件查询,使用逻辑运算符 or 连接即可,也可以通过union/union all来联合查询
-- 相当于把下面的两条sql的结果拼接在一起(去掉all就可以实现去重)
select * from emp e where e.salary < 5000
union
select * from emp e where e.age > 50;

image-20231004203724124

union all查询出来的结果,仅仅进行简单的合并,并未去重

union 联合查询,会对查询出来的结果进行去重处理

4.7 子查询

image-20231004204033345

标量子查询

子查询返回的结果是单个值(数字、字符串、日期等),最简单的形式,这种子查询称为标量子查询。常用的操作符:= <> > >= < <=

-- 标量子查询
-- 1.查询 "销售部" 的所有员工信息
-- 1.1 查询销售部的部门id
select * from emp e where e.dept_id = (select id from dept d where d.name = '销售部');

-- 查询在 "方东白" 入职之后的员工信息
select * from emp e1 where e1.entrydate > (select e2.entrydate from emp e2 where e2.name = '方东白');

列子查询

子查询返回的结果是一列(可以是多行),这种子查询称为列子查询。常用的操作符:IN 、NOT IN 、 ANY 、SOME 、 ALL

image-20231004205225188

-- 列子查询
-- 查询 "销售部" 和 "市场部" 的所有员工信息
select * from emp e where e.dept_id in (select id from dept where dept.name = '销售部' or dept.name = '市场部');

-- 查询比财务部所有人工资都高的员工信息
select * from emp e where e.salary > all (select e2.salary from emp e2 where e2.dept_id = (select id from dept where dept.name = '财务部'));

-- 查询比研发部其中任意一人工资高的员工信息(使用any或者some均可)
select * from emp e where e.salary > any (select e2.salary from emp e2 where e2.dept_id = (select id from dept where dept.name = '研发部'));

行子查询

子查询返回的结果是一行(可以是多列),这种子查询称为行子查询。常用的操作符:= 、<> 、IN 、NOT IN

-- 行子查询
-- 查询与"张无忌"的薪资及与其直属领导相同的员工信息
select * from emp e2 where (e2.salary,e2.managerid) = (select e.salary,e.managerid from emp e where e.name = '张无忌');

表子查询

子查询返回的结果是多行多列,这种子查询称为表子查询。常用的操作符:IN

-- 表子查询
-- 查询与 "鹿杖客" , "宋远桥" 的职位和薪资相同的员工信息
select * from emp e2 where (e2.job,e2.salary) in (select e.job, e.salary from emp e where e.name = '鹿杖客' or name = '宋远桥');

-- 查询入职日期是 "2006-01-01" 之后的员工信息 , 及其部门信息
select * from (select * from emp where entrydate > '2006-01-01') e left join dept d on e.dept_id = d.id;

多表查询相关的sql语句

-- ---------------------------------------------多表查询--------------------------------------------------------
-- 多表关系
-- 演示多对多的关系
create table student
(
id int auto_increment primary key comment '主键ID',
name varchar(10) comment '姓名',
no varchar(10) comment '学号'
) comment '学生表';

insert into student
values (null, '黛绮丝', '2000100101'),
(null, '谢逊',
'2000100102'),
(null, '殷天正', '2000100103'),
(null, '韦一笑', '2000100104');

create table course
(
id int auto_increment primary key comment '主键ID',
name varchar(10) comment '课程名称'
) comment '课程表';

insert into course
values (null, 'Java'),
(null, 'PHP'),
(null, 'MySQL'),
(null, 'Hadoop');

create table student_course
(
id int auto_increment comment '主键' primary key,
studentid int not null comment '学生ID',
courseid int not null comment '课程ID',
constraint fk_courseid foreign key (courseid) references course (id),
constraint fk_studentid foreign key (studentid) references student (id)
) comment '学生课程中间表';

insert into student_course
values (null, 1, 1),
(null, 1, 2),
(null, 1, 3),
(null, 2, 2),
(null, 2, 3),
(null, 3, 4);


-- 演示一对一的关系
create table tb_user
(
id int auto_increment primary key comment '主键ID',
name varchar(10) comment '姓名',
age int comment '年龄',
gender char(1) comment '1: 男 , 2: 女',
phone char(11) comment '手机号'
) comment '用户基本信息表';
create table tb_user_edu
(
id int auto_increment primary key comment '主键ID',
degree varchar(20) comment '学历',
major varchar(50) comment '专业',
primaryschool varchar(50) comment '小学',
middleschool varchar(50) comment '中学',
university varchar(50) comment '大学',
userid int unique comment '用户ID',
constraint fk_userid foreign key (userid) references tb_user (id)
) comment '用户教育信息表';
insert into tb_user(id, name, age, gender, phone)
values (null, '黄渤', 45, '1', '18800001111'),
(null, '冰冰', 35, '2', '18800002222'),
(null, '码云', 55, '1', '18800008888'),
(null, '李彦宏', 50, '1', '18800009999');
insert into tb_user_edu(id, degree, major, primaryschool, middleschool,
university, userid)
values (null, '本科', '舞蹈', '静安区第一小学', '静安区第一中学', '北京舞蹈学院', 1),
(null, '硕士', '表演', '朝阳区第一小学', '朝阳区第一中学', '北京电影学院', 2),
(null, '本科', '英语', '杭州市第一小学', '杭州市第一中学', '杭州师范大学', 3),
(null, '本科', '应用数学', '阳泉第一小学', '阳泉区第一中学', '清华大学', 4);



-- 演示多表查询
-- 数据准备
-- 创建dept表,并插入数据
create table dept
(
id int auto_increment comment 'ID' primary key,
name varchar(50) not null comment '部门名称'
) comment '部门表';
INSERT INTO dept (id, name)
VALUES (1, '研发部'),
(2, '市场部'),
(3, '财务部'),
(4,
'销售部'),
(5, '总经办'),
(6, '人事部');

-- 创建emp表,并插入数据
create table emp
(
id int auto_increment comment 'ID' primary key,
name varchar(50) not null comment '姓名',
age int comment '年龄',
job varchar(20) comment '职位',
salary int comment '薪资',
entrydate date comment '入职时间',
managerid int comment '直属领导ID',
dept_id int comment '部门ID'
) comment '员工表';

-- 添加外键
alter table emp
add constraint fk_emp_dept_id foreign key (dept_id) references
dept (id);

INSERT INTO emp (id, name, age, job, salary, entrydate, managerid, dept_id)
VALUES (1, '金庸', 66, '总裁', 20000, '2000-01-01', null, 5),
(2, '张无忌', 20, '项目经理', 12500, '2005-12-05', 1, 1),
(3, '杨逍', 33, '开发', 8400, '2000-11-03', 2, 1),
(4, '韦一笑', 48, '开发', 11000, '2002-02-05', 2, 1),
(5, '常遇春', 43, '开发', 10500, '2004-09-07', 3, 1),
(6, '小昭', 19, '程序员鼓励师', 6600, '2004-10-12', 2, 1),
(7, '灭绝', 60, '财务总监', 8500, '2002-09-12', 1, 3),
(8, '周芷若', 19, '会计', 48000, '2006-06-02', 7, 3),
(9, '丁敏君', 23, '出纳', 5250, '2009-05-13', 7, 3),
(10, '赵敏', 20, '市场部总监', 12500, '2004-10-12', 1, 2),
(11, '鹿杖客', 56, '职员', 3750, '2006-10-03', 10, 2),
(12, '鹤笔翁', 19, '职员', 3750, '2007-05-09', 10, 2),
(13, '方东白', 19, '职员', 5500, '2009-02-12', 10, 2),
(14, '张三丰', 88, '销售总监', 14000, '2004-10-12', 1, 4),
(15, '俞莲舟', 38, '销售', 4600, '2004-10-12', 14, 4),
(16, '宋远桥', 40, '销售', 4600, '2004-10-12', 14, 4),
(17, '陈友谅', 42, null, 2000, '2011-10-12', 1, null);

-- 多表查询
select *
from emp,
dept
where emp.dept_id = dept.id;

-- 内连接演示
-- 案例
-- 查询每一个员工的姓名 , 及关联的部门的名称 (隐式内连接实现)
-- 表结构: emp , dept
-- 连接条件: emp.dept_id = dept.id
select e.name, d.name from emp e, dept d where e.dept_id = d.id;

-- 查询每一个员工的姓名 , 及关联的部门的名称 (显式内连接实现) --- INNER JOIN ...ON ...
-- 表结构: emp , dept
-- 连接条件: emp.dept_id = dept.id
select e.name ,d.name from emp e inner join dept d on e.dept_id = d.id;


-- 外连接
-- 查询emp表的所有数据, 和对应的部门信息
-- 由于需求中提到,要查询emp的所有数据,所以是不能内连接查询的,需要考虑使用外连接查询。
-- 表结构: emp, dept
-- 连接条件: emp.dept_id = dept.id
select e.name,d.name from emp e left join dept d on e.dept_id = d.id;

-- 查询dept表的所有数据, 和对应的员工信息(右外连接)
-- 由于需求中提到,要查询dept表的所有数据,所以是不能内连接查询的,需要考虑使用外连接查询
-- 表结构: emp, dept
-- 连接条件: emp.dept_id = dept.id
select e.name,d.name from emp e right join dept d on e.dept_id = d.id;


-- 自连接
-- 查询员工 及其 所属领导的名字
-- 表结构: emp
-- 技巧:查询的时候看作是两张表
select e1.name '员工' ,e2.name '领导' from emp e1,emp e2 where e1.managerid = e2.id;

-- 查询所有员工 emp 及其领导的名字 emp , 如果员工没有领导, 也需要查询出来
-- 表结构: emp a , emp b
select e1.name '员工' ,e2.name '领导' from emp e1 left join emp e2 on e1.managerid = e2.id;




-- 联合查询
-- 将薪资低于 5000 的员工 , 和 年龄大于 50 岁的员工全部查询出来.
-- 当前对于这个需求,我们可以直接使用多条件查询,使用逻辑运算符 or 连接即可,也可以通过union/union all来联合查询
-- 相当于把下面的两条sql的结果拼接在一起(去掉all就可以实现去重)
select * from emp e where e.salary < 5000
union
select * from emp e where e.age > 50;



-- 子查询
-- 标量子查询
-- 1.查询 "销售部" 的所有员工信息
-- 1.1 查询销售部的部门id
select * from emp e where e.dept_id = (select id from dept d where d.name = '销售部');

-- 查询在 "方东白" 入职之后的员工信息
select * from emp e1 where e1.entrydate > (select e2.entrydate from emp e2 where e2.name = '方东白');


-- 列子查询
-- 查询 "销售部" 和 "市场部" 的所有员工信息
select * from emp e where e.dept_id in (select id from dept where dept.name = '销售部' or dept.name = '市场部');

-- 查询比财务部所有人工资都高的员工信息
select * from emp e where e.salary > all (select e2.salary from emp e2 where e2.dept_id = (select id from dept where dept.name = '财务部'));

-- 查询比研发部其中任意一人工资高的员工信息
select * from emp e where e.salary > any (select e2.salary from emp e2 where e2.dept_id = (select id from dept where dept.name = '研发部'));



-- 行子查询
-- 查询与"张无忌"的薪资及与其直属领导相同的员工信息
select * from emp e2 where (e2.salary,e2.managerid) = (select e.salary,e.managerid from emp e where e.name = '张无忌');


-- 表子查询
-- 查询与 "鹿杖客" , "宋远桥" 的职位和薪资相同的员工信息
select * from emp e2 where (e2.job,e2.salary) in (select e.job, e.salary from emp e where e.name = '鹿杖客' or name = '宋远桥');

-- 查询入职日期是 "2006-01-01" 之后的员工信息 , 及其部门信息
select * from (select * from emp where entrydate > '2006-01-01') e left join dept d on e.dept_id = d.id;

4.8 多表查询案例

题目

image-20231004212719134

数据准备

-- 数据准备
-- 创建dept表,并插入数据
create table dept
(
id int auto_increment comment 'ID' primary key,
name varchar(50) not null comment '部门名称'
) comment '部门表';
INSERT INTO dept (id, name)
VALUES (1, '研发部'),
(2, '市场部'),
(3, '财务部'),
(4,
'销售部'),
(5, '总经办'),
(6, '人事部');

-- 创建emp表,并插入数据
create table emp
(
id int auto_increment comment 'ID' primary key,
name varchar(50) not null comment '姓名',
age int comment '年龄',
job varchar(20) comment '职位',
salary int comment '薪资',
entrydate date comment '入职时间',
managerid int comment '直属领导ID',
dept_id int comment '部门ID'
) comment '员工表';

-- 添加外键
alter table emp
add constraint fk_emp_dept_id foreign key (dept_id) references
dept (id);

INSERT INTO emp (id, name, age, job, salary, entrydate, managerid, dept_id)
VALUES (1, '金庸', 66, '总裁', 20000, '2000-01-01', null, 5),
(2, '张无忌', 20, '项目经理', 12500, '2005-12-05', 1, 1),
(3, '杨逍', 33, '开发', 8400, '2000-11-03', 2, 1),
(4, '韦一笑', 48, '开发', 11000, '2002-02-05', 2, 1),
(5, '常遇春', 43, '开发', 10500, '2004-09-07', 3, 1),
(6, '小昭', 19, '程序员鼓励师', 6600, '2004-10-12', 2, 1),
(7, '灭绝', 60, '财务总监', 8500, '2002-09-12', 1, 3),
(8, '周芷若', 19, '会计', 4800, '2006-06-02', 7, 3),
(9, '丁敏君', 23, '出纳', 5250, '2009-05-13', 7, 3),
(10, '赵敏', 20, '市场部总监', 12500, '2004-10-12', 1, 2),
(11, '鹿杖客', 56, '职员', 3750, '2006-10-03', 10, 2),
(12, '鹤笔翁', 19, '职员', 3750, '2007-05-09', 10, 2),
(13, '方东白', 19, '职员', 5500, '2009-02-12', 10, 2),
(14, '张三丰', 88, '销售总监', 14000, '2004-10-12', 1, 4),
(15, '俞莲舟', 38, '销售', 4600, '2004-10-12', 14, 4),
(16, '宋远桥', 40, '销售', 4600, '2004-10-12', 14, 4),
(17, '陈友谅', 42, null, 2000, '2011-10-12', 1, null);

-- 创建salgrade并插入数据
create table salgrade
(
grade int,
losal int,
hisal int
) comment '薪资等级表';
insert into salgrade
values (1, 0, 3000);
insert into salgrade
values (2, 3001, 5000);
insert into salgrade
values (3, 5001, 8000);
insert into salgrade
values (4, 8001, 10000);
insert into salgrade
values (5, 10001, 15000);
insert into salgrade
values (6, 15001, 20000);
insert into salgrade
values (7, 20001, 25000);
insert into salgrade
values (8, 25001, 30000);

题解

-- 题解
-- 1.查询员工的姓名、年龄、职位、部门信息(隐式内连接)
select e.name, e.age, e.job, d.name
from emp e,
dept d
where e.dept_id = d.id;

-- 2.查询年龄小于30岁的员工的姓名、年龄、职位、部门信息(显式内连接)
select e.name, e.age, e.job, d.name
from emp e
inner join dept d on e.dept_id = d.id
where e.age < 30;

-- 3.查询拥有员工的部门id、部门名称
-- 方法一
select distinct d.id, d.name
from emp e,
dept d
where e.dept_id = d.id;
-- 方法二
select *
from dept
where dept.id in (select e.dept_id from emp e);

-- 4.查询所有年龄大于40岁的员工,及其归属的部门名称;如果没有员工分配部门,也需要展示出来
select e.*, d.name
from emp e
left join dept d on e.dept_id = d.id
where age > 40;

-- 5.查询所有员工的工资等级
select e.name '姓名', s.grade '工资等级'
from emp e,
salgrade s
where e.salary between s.losal and s.hisal;

-- 6.查询研发部所有员工的信息已经工资等级
-- 方法一
select e.*, s.grade
from emp e,
dept d,
salgrade s
where e.dept_id = d.id
and (e.salary between s.losal and s.hisal)
and d.name = '研发部';
-- 方法二
select e.*, s.grade
from (select emp.*
from emp,
dept
where emp.dept_id = dept.id
and dept.name = '研发部') e,
salgrade s
where e.salary between s.losal and s.hisal;

-- 7.查询研发部的平均工资
select avg(e.salary)
from emp e,
dept d
where e.dept_id = d.id
and d.name = '研发部';

-- 8.查询工资比灭绝高的员工信息
select *
from emp
where salary > (select salary from emp where name = '灭绝');

-- 9.查询比平均工资高的员工信息
select *
from emp
where salary > (select avg(salary) from emp);

-- 10.查询低于本部门平均工资的员工
-- 方法一
select *
from emp e2
where e2.salary < (select avg(e1.salary) from emp e1 where e1.dept_id = e2.dept_id);
-- 方法二
select *
from emp e,
(select dept_id, avg(salary) 'salavg' from emp group by dept_id) eavg
where eavg.dept_id = e.dept_id
and e.salary < eavg.salavg;

-- 11.查询所有的部门信息,并统计部门的员工人数
-- 方法一
select d.id, d.name, (select count(*) from emp e where e.dept_id = d.id) '人数'
from dept d;
-- 方法二
select *
from dept d,
(select e.dept_id, count(*) '人数' from emp e group by e.dept_id) ecount
where d.id = ecount.dept_id;

-- 12.查询所有学生的选课情况,展示出学生名称,学号,课程名称
select s.name, s.no, c.name
from course c,
student s,
student_course sc
where c.id = sc.courseid
and s.id = sc.studentid;

总结

image-20231005133456395

5.事务

5.1 事务介绍

​ 事务 是一组操作的集合,它是一个不可分割的工作单位,事务会把所有的操作作为一个整体一起向系统提交或撤销操作请求,即这些操作要么同时成功,要么同时失败。就比如: 张三给李四转账1000块钱,张三银行账户的钱减少1000,而李四银行账户的钱要增加1000。 这一组操作就必须在一个事务的范围内,要么都成功,要么都失败。

注意: 默认MySQL的事务是自动提交的,也就是说,当执行完一条DML语句时,MySQL会立即隐式的提交事务。

5.2 事务操作

数据准备

-- 数据准备
drop table if exists account;
create table account
(
id int primary key AUTO_INCREMENT comment 'ID',
name varchar(10) comment '姓名',
money double(10, 2) comment '余额'
) comment '账户表';
insert into account(name, money)
VALUES ('张三', 2000),
('李四', 2000);
select * from account;



-- 转账操作(张三转1000给李四)
-- 1. 查询张三余额
select * from account where name = '张三';
-- 2. 张三的余额减少1000
update account set money = money - 1000 where name = '张三';
-- 3. 李四的余额增加1000
update account set money = money + 1000 where name = '李四';

控制事务一

image-20231005134601982

控制事务二

image-20231005134750027

image-20231005134835921

-- 开启事务
start transaction;
-- 1. 查询张三余额
select * from account where name = '张三';
-- 2. 张三的余额减少1000
update account set money = money - 1000 where name = '张三';
-- 3. 李四的余额增加1000
update account set money = money + 1000 where name = '李四';
-- 如果正常执行完毕, 则提交事务
commit;
-- 如果执行过程中报错, 则回滚事务
-- rollback;

5.3 事务四大特性

image-20231005135637347

5.4 并发事务问题

image-20231005135741486

image-20231005135811745

5.5 事务隔离级别

为了解决并发事务所引发的问题,在数据库中引入了事务隔离级别,主要有以下几种:

​ “X” 表示不会出现

image-20231005140255060

image-20231005140322039

-- 查看事务隔离级别
SELECT @@TRANSACTION_ISOLATION;

-- 设置事务的隔离级别
SET [ SESSION | GLOBAL ] TRANSACTION ISOLATION LEVEL { READ UNCOMMITTED | READ COMMITTED | REPEATABLE READ | SERIALIZABLE }

小结

image-20231005142056902

基础篇全部的Sql

-- ------------------------------------------------------数据库的操作------------------------------------------------------------------------
-- 展示所有的数据库
show databases;

-- 查询当前的数据库
select database();

-- 创建数据库
create database if not exists itcast;

-- 删除数据库
drop database if exists itcast;

-- 使用数据库
use itcast;


-- ----------------------------------------------------------表的操作--------------------------------------------------------------------

-- 创建用户表
create table tb_user
(
id int comment '编号',
name varchar(50) comment '姓名',
age int comment '年龄',
gender varchar(1) comment '性别'
) comment '用户表';

-- 显示创建表的结构
desc tb_user;

-- 显示创建表的语句
show create table tb_user;

-- 删除表
drop table emp;

-- 修改表的操作
-- 举例:现有一张员工表
create table emp
(
id int comment '编号',
workno varchar(10) comment '工号',
name varchar(10) comment '姓名',
gender char(1) comment '性别',
age tinyint unsigned comment '年龄',
idcard char(18) comment '身份证号',
entrydate date comment '入职时间'
) comment '员工表';
-- 查看表结构
desc emp;

-- 修改表结构(向emp表中添加昵称字段)
alter table emp
add nickname varchar(20) comment '昵称';

-- 修改相应字段的数据类型(把age的数据类型从int改回tinyint)
alter table emp
modify age tinyint;

-- 修改字段名和字段的类型(把nickname字段改为username,并修改数据类型)
alter table emp
change nickname username varchar(30) comment '用户名';

-- 删除字段(删除username字段)
alter table emp
drop username;

-- 修改表名(将表名字从emo修改为employee)
alter table emp rename to employee;

-- 删除表
drop table if exists employee;
-- 删除指定表,并重新创建该表(相当于删除表中的全部数据)
truncate table employee;



-- ------------------------------------------------DML-增删改操作---------------------------------------------------
-- 添加数据
select database();
show tables;
-- 该指定的字段添加数据
insert into employee(id, workno, name, gender, age, idcard, entrydate) value (1, '1', '小华', '男', 20, '123456789123456789', '2001-01-01');

select *
from employee;
-- 给全部的字段添加数据
insert into employee value (2, '2', '小刚', '男', 23, '123456784124456789', '2008-01-01');
-- 批量添加数据
insert into employee
values (3, '3', '小李', '男', 23, '123456784124456789', '2008-01-01'),
(4, '4', '小黄', '男', 23, '123456784124456789', '2008-01-01');

-- 修改数据(不带条件修改所有)
update employee
set name = '张三'
where id = 1;
update employee
set name = '李四',
gender= '女'
where id = 1;

-- 删除数据(删除id为4的数据,不带添加删除所有)
delete
from employee
where id = 4;


-- --------------------------------------------------------DQL数据查询语言--------------------------------------------------
-- 数据准备
create table emp
(
id int comment '编号',
workno varchar(10) comment '工号',
name varchar(10) comment '姓名',
gender char(1) comment '性别',
age tinyint unsigned comment '年龄',
idcard char(18) comment '身份证号',
workaddress varchar(50) comment '工作地址',
entrydate date comment '入职时间'
) comment '员工表';

-- 插入数据
insert into emp(id, workno, name, gender, age, idcard, workaddress, entrydate)
values (1, '1', '柳岩', '女', 20, '12345678912345678', '北京', '2001-01-01'),
(2, '2', '张无忌', '男', 18, '123456789012345670', '北京', '2005-09-01'),
(3, '3', '韦一笑', '男', 38, '12345678972345670', '上海', '2005-08-01'),
(4, '4', '赵敏', '女', 18, '12345675712345670', '北京', '2009-12-01'),
(5, '5', '小昭', '女', 16, '123456769012345678', '上海', '2007-07-01'),
(6, '6', '杨逍', '男', 28, '1234567893123456X', '北京', '2006-01-01'),
(7, '7', '范瑶', '男', 40, '123456789212345670', '北京', '2005-05-01'),
(8, '8', '黛绮丝', '女', 38, '123456157123645670', '天津', '2015-05-01'),
(9, '9', '范凉凉', '女', 45, '123156789012345678', '北京', '2010-04-01'),
(10, '10', '陈友谅', '男', 53, '123456789012345670', '上海', '2011-01-01'),
(11, '11', '张士诚', '男', 55, '12356789712345670', '江苏', '2015-05-01'),
(12, '12', '常遇春', '男', 32, '123446757152345670', '北京', '2004-02-01'),
(13, '13', '张三丰', '男', 88, '123656789012345678', '江苏', '2020-11-01'),
(14, '14', '灭绝', '女', 65, '123456719012345670', '西安', '2019-05-01'),
(15, '15', '胡青年', '男', 70, '12345674971234567X', '西安', '2018-04-01'),
(16, '16', '周芷若', '女', 18, null, '北京', '2012-06-01');

-- 基础查询
-- 查看插入的数据
select *
from emp;

-- 查询指定字段 name workno age 返回
select name, workno, age
from emp;

-- 查询所有字段返回
select id,
workno,
name,
gender,
age,
idcard,
workaddress,
entrydate
from emp;

-- distinct去重关键字
-- 查询所有员工的工作地址(不要重复的地址)
select distinct workaddress '工作地址'
from emp;



-- 条件查询
-- 查询年龄等于 88 的员工
select *
from emp
where age = 88;

-- 查询年龄小于20的员工信息
select *
from emp
where age < 20;

-- 查询年龄小于等于20的员工信息
select *
from emp
where age <= 20;

-- 查询没有身份证号的员工信息
select *
from emp
where idcard is null;

-- 查询年龄不等于88的员工信息(<>也表示不等于)
select *
from emp
where age != 88;
select *
from emp
where age <> 88;

-- 查询年龄在15~20(包含)之间的员工信息(三种实现方式)
select *
from emp
where age >= 15 && age <= 20;
select *
from emp
where age >= 15
and age <= 20;
select *
from emp
where age between 15 and 20;
#包含了1520

-- 查询性别为女 且年龄小于25岁的员工
select *
from emp
where gender = '女'
and age < 25;

-- 查询年龄等于18 或 20 或 48 的员工
select *
from emp
where age = 18
or age = 20
or age = 48;
select *
from emp
where age in (18, 20, 48);

-- 查询姓名为两个字的员工(模糊匹配(_匹配单个字符, %匹配任意个字符))
select *
from emp
where name like '__';

-- 查询身份证号最后一位是X的员工信息
select *
from emp
where idcard like '%X';



-- 聚合函数
-- 统计该企业员工数量(空值是不参与统计的)
select count(*)
from emp;
select count(idcard)
from emp;

-- 统计该企业员工的平均年龄
select avg(age)
from emp;

-- 统计该企业员工的最大年龄
select max(age)
from emp;

-- 统计该企业员工的最小年龄
select min(age)
from emp;

-- 统计西安地区员工的年龄之和
select sum(age)
from emp
where workaddress = '西安';



-- 分组查询
-- 根据性别分组,统计男性员工和女性员工的数量
select gender, count(*)
from emp
group by gender;

-- 根据性别进行分组,统计男性员工和女性员工的平均年龄
select gender, avg(age)
from emp
group by gender;

-- 查询年龄小于45岁的员工,并根据工作地址分组,获取员工数量大于等于3的工作地址(可以使用别名也可以不使用别名)
select workaddress, count(*) address_count
from emp
where age < 45
group by workaddress
having address_count >= 3;



-- 排序查询
-- 根据年龄对公司的员工进行升序排序(asc可以省略)
select *
from emp
order by age;
-- 降序排序
select *
from emp
order by age desc;

-- 根据员工的入职时间降序排序
select *
from emp
order by entrydate desc;

-- 根据员工的年龄升序排序,年龄相同,再按照入职时间进行降序排序
select *
from emp
order by age asc, entrydate desc;



-- 分页查询
-- 查询第一页的员工数据,每页显示10条记录
select *
from emp
limit 0,10;

-- 查询第二页数据,每页展示十条数据(起始索引 = (查询页码 - 1)* 每页显示记录数)
select *
from emp
limit 10,10;


-- DQL语句案例
-- 查询年龄在20,21,22,23岁的女性员工的信息
select *
from emp
where gender = '女'
and age in (20, 21, 22, 23);

-- 查询性别为男,并且年龄再20~40岁(含)以内的姓名为三个字的员工
select *
from emp
where gender = '男'
and (age between 20 and 40)
and name like '___';

-- 统计员工表中,年龄小于60岁的,男性员工和女性员工的人数
select gender, count(*)
from emp
where age < 60
group by gender;

-- 查询所有年龄小于等于35岁员工的姓名和年龄,并对查询结果按年龄升序排序,如果年龄相同按照入职时间降序排序
select name, age
from emp
where age <= 35
order by age asc, entrydate desc;

-- 查询性别为男,且年龄在20·40岁(含)以内的前五个员工信息,并对查询结果按年龄升序排序,如果年龄相同按照入职时间升序排序
select *
from emp
where gender = '男'
and age between 20 and 40
order by age, entrydate
limit 5;



-- ---------------------------------------------------DCL--------------------------------------------------
-- 用户管理
-- mysql数据库
use mysql;

-- 查询所有的用户
select *
from mysql.user;

-- 创建用户itcast, 只能够在当前主机localhost访问, 密码123456;
create user 'itcast'@'localhost' identified by '123456';

-- 创建用户heima, 可以在任意主机访问该数据库, 密码123456;
create user 'heima'@'%' identified by '123456';

-- 修改用户heima的访问密码为1234;
alter user 'heima'@'%' identified with mysql_native_password by '1234';

-- 删除 itcast@localhost 用户
drop user 'itcast'@'localhost';



-- 权限控制
-- 查询权限(查询heima用户的所有权限)
show grants for 'heima'@'%';

-- 授予权限(授予heima用户对数据库itcast的所有权限)
grant all on itcast.* to 'heima'@'%';

-- 授予用户所有数据库所有表的所有权限
grant all on *.* to 'heima'@'%';

-- 撤销权限(撤销heima用户在itcast数据库上的所有权限)
revoke all on itcast.* from 'heima'@'%';



-- ----------------------------------------------------------函数--------------------------------------------------------
-- 字符串函数
-- concat函数(字符串拼接)
select concat('hello ', 'word');
# hello word

-- lower(全部转化成小写)
select lower('Hello');
# hello

-- upper(全部转化成大写)
select upper('Hello');
# HELLO

-- lpad(左侧填充)
select lpad('01', 5, '-');
# ---01

-- rpad(右侧填充)
select rpad('01', 5, '-');
# 01---

-- trim 去除头部和尾部的空格
select trim(' Hello Mysql ');
# Hello Mysql

-- substring 字符串截取
select substring('Hello Mysql', 1, 5);
# Hello

-- 由于业务需求变更,企业员工的工号,统一为5位数,目前不足5位数的全部在前面补0 比如:1号员工的工号应该为00001
update emp
set workno = lpad(workno, 5, '0');



-- 数值函数
-- ceil 向上取整
select ceil(1.5);
# 2

-- floor 向下取整
select floor(1.1);
# 1

-- mod 求模运算
select mod(7, 4);
# 3

-- rand 随机数(0~1)
select rand();

-- round 四舍五入
select round(2.345, 2);
# 2.35

-- 通过数据库函数生成一个的随机的六位验证码
select lpad(round(rand() * 1000000, 0), 6, '0');


-- 日期函数
-- curdate:当前日期
select curdate();

-- curtime:当前时间
select curtime();

-- now:当前日期和时间
select now();

-- YEAR , MONTH , DAY:当前年、月、日
select year(now());
select month(now());
select day(now());

-- date_add:增加指定的时间间隔
select date_add(now(), INTERVAL 70 DAY); # 当前的时间往后推70
select date_add(now(), INTERVAL 2 MONTH);
#向后推2个月

-- datediff:获取两个日期相差的天数
select datediff('2021-12-01', '2021-10-01');
# 查询两个时间之间的差值,第一个时间减去第二个时间

-- 查询所有员工的入职天数,并根据入职天数倒叙排序
select name, datediff(curdate(), entrydate) 'entrydays'
from emp
order by entrydays desc;



-- 流程控制函数
-- if 相当java中的三元运算符
select if(true, 'OK', 'ERROR');

-- ifnull 第一个字符串不为空就返回第一个,第一个为空就返回第二个
select ifnull('OK', 'DEFAULT');
select ifnull(null, 'DEFAULT');

-- case when then else end
-- 需求: 查询emp表的员工姓名和工作地址 (北京/上海 ----> 一线城市 , 其他 ----> 二线城市)
select name,
(case workaddress
when '北京' then '一线城市'
when '上海' then '一线城市'
else
'二线城市' end) as '工作地址'
from emp;

-- 案例:统计班级各个学员的成绩,展示规则如下:
-- >= 85 展示优秀
-- >= 60 展示及格
-- 否则展示不及格

-- 建表数据
create table score
(
id int comment 'ID',
name varchar(20) comment '姓名',
math int comment '数学',
english int comment '英语',
chinese int comment '语文'
) comment '学员成绩表';
insert into score(id, name, math, english, chinese)
VALUES (1, 'Tom', 67, 88, 95),
(2, 'Rose', 23, 66, 90),
(3, 'Jack', 56, 98, 76);

select *
from score;

-- 案例sql实现
select id,
name,
(case when math >= 85 then '优秀' when math >= 60 then '及格' else '不及格' end)
'数学',
(case
when english >= 85 then '优秀'
when english >= 60 then '及格'
else '不及格'
end) '英语',
(case
when chinese >= 85 then '优秀'
when chinese >= 60 then '及格'
else '不及格'
end) '语文'
from score;



-- ---------------------------------------------------约束演示------------------------------------------------------------
-- id 主键,并且自动增长
-- name 不为空,并且唯一
-- age 大于零,并且小于等于120(8.0以上的数据库才支持)age int check (age > 0 && age <= 120) COMMENT '年龄'
-- status 如果没有指定该值,默认为1
-- gender 无约束
drop table if exists user;
create table user
(
id int AUTO_INCREMENT PRIMARY KEY COMMENT 'ID唯一标识',
name varchar(10) NOT NULL UNIQUE COMMENT '姓名',
age int COMMENT '年龄',
status char(1) default '1' COMMENT '状态',
gender char(1) COMMENT '性别'
);
select *
from user;

-- 插入数据
-- name的值不能重复,status的值没有填写的话,将使用的是默认的值
insert into user(name, age, status, gender)
values ('Tom1', 19, '1', '男'),
('Tom2', 29, '1', '男'),
('Tom3', 14, '1', '男');


-- 外键约束
create table dept
(
id int auto_increment comment 'ID' primary key,
name varchar(50) not null comment '部门名称'
) comment '部门表';

INSERT INTO dept (id, name)
VALUES (1, '研发部'),
(2, '市场部'),
(3, '财务部'),
(4, '销售部'),
(5, '总经办');

create table employee
(
id int auto_increment comment 'ID' primary key,
name varchar(50) not null comment '姓名',
age int comment '年龄',
job varchar(20) comment '职位',
salary int comment '薪资',
entrydate date comment '入职时间',
managerid int comment '直属领导ID',
dept_id int comment '部门ID'
) comment '员工表';

INSERT INTO employee (id, name, age, job, salary, entrydate, managerid, dept_id)
VALUES (1, '金庸', 66, '总裁', 20000, '2000-01-01', null, 5),
(2, '张无忌', 20, '项目经理', 12500, '2005-12-05', 1, 1),
(3, '杨逍', 33, '开发', 8400, '2000-11-03', 2, 1),
(4, '韦一笑', 48, '开发', 11000, '2002-02-05', 2, 1),
(5, '常遇春', 43, '开发', 10500, '2004-09-07', 3, 1),
(6, '小昭', 19, '程序员鼓励师', 6600, '2004-10-12', 2, 1);

select *
from dept;
select *
from employee;

-- 添加外键 员工表employee的部门id字段关联到部门表的id字段以建立外键约束
alter table employee
add constraint fk_emp_dept foreign key (dept_id) references dept (id);

-- 删除外键
alter table employee
drop foreign key fk_emp_dept;



-- ---------------------------------------------多表查询--------------------------------------------------------
-- 多表关系
-- 演示多对多的关系
create table student
(
id int auto_increment primary key comment '主键ID',
name varchar(10) comment '姓名',
no varchar(10) comment '学号'
) comment '学生表';

insert into student
values (null, '黛绮丝', '2000100101'),
(null, '谢逊',
'2000100102'),
(null, '殷天正', '2000100103'),
(null, '韦一笑', '2000100104');

create table course
(
id int auto_increment primary key comment '主键ID',
name varchar(10) comment '课程名称'
) comment '课程表';

insert into course
values (null, 'Java'),
(null, 'PHP'),
(null, 'MySQL'),
(null, 'Hadoop');

create table student_course
(
id int auto_increment comment '主键' primary key,
studentid int not null comment '学生ID',
courseid int not null comment '课程ID',
constraint fk_courseid foreign key (courseid) references course (id),
constraint fk_studentid foreign key (studentid) references student (id)
) comment '学生课程中间表';

insert into student_course
values (null, 1, 1),
(null, 1, 2),
(null, 1, 3),
(null, 2, 2),
(null, 2, 3),
(null, 3, 4);


-- 演示一对一的关系
create table tb_user
(
id int auto_increment primary key comment '主键ID',
name varchar(10) comment '姓名',
age int comment '年龄',
gender char(1) comment '1: 男 , 2: 女',
phone char(11) comment '手机号'
) comment '用户基本信息表';
create table tb_user_edu
(
id int auto_increment primary key comment '主键ID',
degree varchar(20) comment '学历',
major varchar(50) comment '专业',
primaryschool varchar(50) comment '小学',
middleschool varchar(50) comment '中学',
university varchar(50) comment '大学',
userid int unique comment '用户ID',
constraint fk_userid foreign key (userid) references tb_user (id)
) comment '用户教育信息表';
insert into tb_user(id, name, age, gender, phone)
values (null, '黄渤', 45, '1', '18800001111'),
(null, '冰冰', 35, '2', '18800002222'),
(null, '码云', 55, '1', '18800008888'),
(null, '李彦宏', 50, '1', '18800009999');
insert into tb_user_edu(id, degree, major, primaryschool, middleschool,
university, userid)
values (null, '本科', '舞蹈', '静安区第一小学', '静安区第一中学', '北京舞蹈学院', 1),
(null, '硕士', '表演', '朝阳区第一小学', '朝阳区第一中学', '北京电影学院', 2),
(null, '本科', '英语', '杭州市第一小学', '杭州市第一中学', '杭州师范大学', 3),
(null, '本科', '应用数学', '阳泉第一小学', '阳泉区第一中学', '清华大学', 4);



-- 演示多表查询
-- 数据准备
-- 创建dept表,并插入数据
create table dept
(
id int auto_increment comment 'ID' primary key,
name varchar(50) not null comment '部门名称'
) comment '部门表';
INSERT INTO dept (id, name)
VALUES (1, '研发部'),
(2, '市场部'),
(3, '财务部'),
(4,
'销售部'),
(5, '总经办'),
(6, '人事部');

-- 创建emp表,并插入数据
create table emp
(
id int auto_increment comment 'ID' primary key,
name varchar(50) not null comment '姓名',
age int comment '年龄',
job varchar(20) comment '职位',
salary int comment '薪资',
entrydate date comment '入职时间',
managerid int comment '直属领导ID',
dept_id int comment '部门ID'
) comment '员工表';

-- 添加外键
alter table emp
add constraint fk_emp_dept_id foreign key (dept_id) references
dept (id);

INSERT INTO emp (id, name, age, job, salary, entrydate, managerid, dept_id)
VALUES (1, '金庸', 66, '总裁', 20000, '2000-01-01', null, 5),
(2, '张无忌', 20, '项目经理', 12500, '2005-12-05', 1, 1),
(3, '杨逍', 33, '开发', 8400, '2000-11-03', 2, 1),
(4, '韦一笑', 48, '开发', 11000, '2002-02-05', 2, 1),
(5, '常遇春', 43, '开发', 10500, '2004-09-07', 3, 1),
(6, '小昭', 19, '程序员鼓励师', 6600, '2004-10-12', 2, 1),
(7, '灭绝', 60, '财务总监', 8500, '2002-09-12', 1, 3),
(8, '周芷若', 19, '会计', 48000, '2006-06-02', 7, 3),
(9, '丁敏君', 23, '出纳', 5250, '2009-05-13', 7, 3),
(10, '赵敏', 20, '市场部总监', 12500, '2004-10-12', 1, 2),
(11, '鹿杖客', 56, '职员', 3750, '2006-10-03', 10, 2),
(12, '鹤笔翁', 19, '职员', 3750, '2007-05-09', 10, 2),
(13, '方东白', 19, '职员', 5500, '2009-02-12', 10, 2),
(14, '张三丰', 88, '销售总监', 14000, '2004-10-12', 1, 4),
(15, '俞莲舟', 38, '销售', 4600, '2004-10-12', 14, 4),
(16, '宋远桥', 40, '销售', 4600, '2004-10-12', 14, 4),
(17, '陈友谅', 42, null, 2000, '2011-10-12', 1, null);

-- 多表查询
select *
from emp,
dept
where emp.dept_id = dept.id;

-- 内连接演示
-- 案例
-- 查询每一个员工的姓名 , 及关联的部门的名称 (隐式内连接实现)
-- 表结构: emp , dept
-- 连接条件: emp.dept_id = dept.id
select e.name, d.name
from emp e,
dept d
where e.dept_id = d.id;

-- 查询每一个员工的姓名 , 及关联的部门的名称 (显式内连接实现) --- INNER JOIN ...ON ...
-- 表结构: emp , dept
-- 连接条件: emp.dept_id = dept.id
select e.name, d.name
from emp e
inner join dept d on e.dept_id = d.id;


-- 外连接
-- 查询emp表的所有数据, 和对应的部门信息
-- 由于需求中提到,要查询emp的所有数据,所以是不能内连接查询的,需要考虑使用外连接查询。
-- 表结构: emp, dept
-- 连接条件: emp.dept_id = dept.id
select e.name, d.name
from emp e
left join dept d on e.dept_id = d.id;

-- 查询dept表的所有数据, 和对应的员工信息(右外连接)
-- 由于需求中提到,要查询dept表的所有数据,所以是不能内连接查询的,需要考虑使用外连接查询
-- 表结构: emp, dept
-- 连接条件: emp.dept_id = dept.id
select e.name, d.name
from emp e
right join dept d on e.dept_id = d.id;


-- 自连接
-- 查询员工 及其 所属领导的名字
-- 表结构: emp
-- 技巧:查询的时候看作是两张表
select e1.name '员工', e2.name '领导'
from emp e1,
emp e2
where e1.managerid = e2.id;

-- 查询所有员工 emp 及其领导的名字 emp , 如果员工没有领导, 也需要查询出来
-- 表结构: emp a , emp b
select e1.name '员工', e2.name '领导'
from emp e1
left join emp e2 on e1.managerid = e2.id;



-- 联合查询
-- 将薪资低于 5000 的员工 , 和 年龄大于 50 岁的员工全部查询出来.
-- 当前对于这个需求,我们可以直接使用多条件查询,使用逻辑运算符 or 连接即可,也可以通过union/union all来联合查询
-- 相当于把下面的两条sql的结果拼接在一起(去掉all就可以实现去重)
select *
from emp e
where e.salary < 5000
union
select *
from emp e
where e.age > 50;



-- 子查询
-- 标量子查询
-- 1.查询 "销售部" 的所有员工信息
-- 1.1 查询销售部的部门id
select *
from emp e
where e.dept_id = (select id from dept d where d.name = '销售部');

-- 查询在 "方东白" 入职之后的员工信息
select *
from emp e1
where e1.entrydate > (select e2.entrydate from emp e2 where e2.name = '方东白');


-- 列子查询
-- 查询 "销售部" 和 "市场部" 的所有员工信息
select *
from emp e
where e.dept_id in (select id from dept where dept.name = '销售部' or dept.name = '市场部');

-- 查询比财务部所有人工资都高的员工信息
select *
from emp e
where e.salary > all (select e2.salary from emp e2 where e2.dept_id = (select id from dept where dept.name = '财务部'));

-- 查询比研发部其中任意一人工资高的员工信息
select *
from emp e
where e.salary > any (select e2.salary from emp e2 where e2.dept_id = (select id from dept where dept.name = '研发部'));



-- 行子查询
-- 查询与"张无忌"的薪资及与其直属领导相同的员工信息
select *
from emp e2
where (e2.salary, e2.managerid) = (select e.salary, e.managerid from emp e where e.name = '张无忌');


-- 表子查询
-- 查询与 "鹿杖客" , "宋远桥" 的职位和薪资相同的员工信息
select *
from emp e2
where (e2.job, e2.salary) in (select e.job, e.salary from emp e where e.name = '鹿杖客' or name = '宋远桥');

-- 查询入职日期是 "2006-01-01" 之后的员工信息 , 及其部门信息
select *
from (select * from emp where entrydate > '2006-01-01') e
left join dept d on e.dept_id = d.id;


-- 多表查询案例
-- 数据准备
create table salgrade
(
grade int,
losal int,
hisal int
) comment '薪资等级表';
insert into salgrade
values (1, 0, 3000);
insert into salgrade
values (2, 3001, 5000);
insert into salgrade
values (3, 5001, 8000);
insert into salgrade
values (4, 8001, 10000);
insert into salgrade
values (5, 10001, 15000);
insert into salgrade
values (6, 15001, 20000);
insert into salgrade
values (7, 20001, 25000);
insert into salgrade
values (8, 25001, 30000);

-- 题解
-- 1.查询员工的姓名、年龄、职位、部门信息(隐式内连接)
select e.name, e.age, e.job, d.name
from emp e,
dept d
where e.dept_id = d.id;

-- 2.查询年龄小于30岁的员工的姓名、年龄、职位、部门信息(显式内连接)
select e.name, e.age, e.job, d.name
from emp e
inner join dept d on e.dept_id = d.id
where e.age < 30;

-- 3.查询拥有员工的部门id、部门名称
-- 方法一
select distinct d.id, d.name
from emp e,
dept d
where e.dept_id = d.id;
-- 方法二
select *
from dept
where dept.id in (select e.dept_id from emp e);

-- 4.查询所有年龄大于40岁的员工,及其归属的部门名称;如果没有员工分配部门,也需要展示出来
select e.*, d.name
from emp e
left join dept d on e.dept_id = d.id
where age > 40;

-- 5.查询所有员工的工资等级
select e.name '姓名', s.grade '工资等级'
from emp e,
salgrade s
where e.salary between s.losal and s.hisal;

-- 6.查询研发部所有员工的信息已经工资等级
-- 方法一
select e.*, s.grade
from emp e,
dept d,
salgrade s
where e.dept_id = d.id
and (e.salary between s.losal and s.hisal)
and d.name = '研发部';
-- 方法二
select e.*, s.grade
from (select emp.*
from emp,
dept
where emp.dept_id = dept.id
and dept.name = '研发部') e,
salgrade s
where e.salary between s.losal and s.hisal;

-- 7.查询研发部的平均工资
select avg(e.salary)
from emp e,
dept d
where e.dept_id = d.id
and d.name = '研发部';

-- 8.查询工资比灭绝高的员工信息
select *
from emp
where salary > (select salary from emp where name = '灭绝');

-- 9.查询比平均工资高的员工信息
select *
from emp
where salary > (select avg(salary) from emp);

-- 10.查询低于本部门平均工资的员工
-- 方法一
select *
from emp e2
where e2.salary < (select avg(e1.salary) from emp e1 where e1.dept_id = e2.dept_id);
-- 方法二
select *
from emp e,
(select dept_id, avg(salary) 'salavg' from emp group by dept_id) eavg
where eavg.dept_id = e.dept_id
and e.salary < eavg.salavg;

-- 11.查询所有的部门信息,并统计部门的员工人数
-- 方法一
select d.id, d.name, (select count(*) from emp e where e.dept_id = d.id) '人数'
from dept d;
-- 方法二
select *
from dept d,
(select e.dept_id, count(*) '人数' from emp e group by e.dept_id) ecount
where d.id = ecount.dept_id;

-- 12.查询所有学生的选课情况,展示出学生名称,学号,课程名称
select s.name, s.no, c.name
from course c,
student s,
student_course sc
where c.id = sc.courseid
and s.id = sc.studentid;


-- ----------------------------------------------------------事务--------------------------------------------------------------------
-- 数据准备
drop table if exists account;
create table account
(
id int primary key AUTO_INCREMENT comment 'ID',
name varchar(10) comment '姓名',
money double(10, 2) comment '余额'
) comment '账户表';
insert into account(name, money)
VALUES ('张三', 2000),
('李四', 2000);
select * from account;



-- 转账操作(张三转1000给李四)
-- 1. 查询张三余额
select * from account where name = '张三';
-- 2. 张三的余额减少1000
update account set money = money - 1000 where name = '张三';
-- 3. 李四的余额增加1000
update account set money = money + 1000 where name = '李四';



-- 开启事务
start transaction;
-- 1. 查询张三余额
select * from account where name = '张三';
-- 2. 张三的余额减少1000
update account set money = money - 1000 where name = '张三';
-- 3. 李四的余额增加1000
update account set money = money + 1000 where name = '李四';
-- 如果正常执行完毕, 则提交事务
commit;
-- 如果执行过程中报错, 则回滚事务
-- rollback;

二.MySql进阶

1.存储引擎

1.1 MySql的体系结构

image-20230916152706301

image-20230916152828704

1.2 存储引擎简介

简介

​ 存储引擎就是存储数据、建立索引、更新/查询数据等技术的实现方式 。存储引擎是基于表的,而不是 基于库的,所以存储引擎也可被称为表类型。我们可以在创建表的时候,来指定选择的存储引擎,如果 没有指定将自动选择默认的存储引擎。

查询一张数据库表的建表语句

-- 查询建表语句
show create table book_info;

image-20230916153448893

建表的时候指定存储引擎的语句

image-20230916153931010

1.3 存储引擎特点

image-20230916154915533

image-20230916155113863

image-20230916155356755

image-20230916155525548

存储引擎的区别

image-20230916155637133

1.4 存储引擎的选择

​ 在选择存储引擎时,应该根据应用系统的特点选择合适的存储引擎。对于复杂的应用系统,还可以根据 实际情况选择多种存储引擎进行组合。

  • InnoDB: 是Mysql的默认存储引擎,支持事务、外键。如果应用对事务的完整性有比较高的要 求,在并发条件下要求数据的一致性,数据操作除了插入和查询之外,还包含很多的更新、删除操 作,那么InnoDB存储引擎是比较合适的选择。
  • MyISAM : 如果应用是以读操作和插入操作为主,只有很少的更新和删除操作,并且对事务的完 整性、并发性要求不是很高,那么选择这个存储引擎是非常合适的。(例如评论的数据、日志相关的数据、电商中足迹相关的数据)
  • MEMORY:将所有数据保存在内存中,访问速度快,通常用于临时表及缓存。MEMORY的缺陷就是 对表的大小有限制,太大的表无法缓存在内存中,而且无法保障数据的安全性。

一句话:项目中绝大多数的时候使用的都是InnoDB

2.索引

2.1 索引概述

2.1.1 介绍

​ 索引(index)是帮助MySQL高效获取数据的数据结构(有序)。在数据之外,数据库系统还维护着满足特定查找算法的数据结构,这些数据结构以某种方式引用(指向)数据, 这样就可以在这些数据结构上实现高级查找算法,这种数据结构就是索引。

2.1.2 演示

注:下面的二叉树并不是一个真实的索引结构,而是一个举例

image-20230916150211012

2.1.3 优缺点

image-20230916150430708

2.2 索引结构

2.2.1 索引的数据结构有哪些

默认的索引结构是B+树索引结构

image-20230916161524219

不同类型的存储引擎对不同的索引结构的支持情况

image-20230916161623546

2.2.2 B+树的索引数据结构

二叉树和红黑树的不足

image-20230916162305576

B树

image-20230916163234103

B+树

image-20230916164042303

image-20230916164314700

MySql中的B+树结构

image-20230916164500444

2.2.3 hash的索引数据结构

image-20230916164903702

image-20230916165007465

2.3.4 思考题

image-20230916165329434

2.3 索引分类

image-20230916165516779

image-20230916165647255

image-20230916170012664

当我们要查询name值是Arm的数据的时候,会先通过二级索引查询,然后通过聚集索引查询这一行的信息(回表查询)

image-20230916170421435

思考题

image-20230916170537693

第一条的执行效率高,第二条要回表查询(先查询name所在行对应的主键id,然后通过主键id查询这一行的数据)

2.4 索引语法

创建|查看|删除语法

image-20230917091746406

举例

image-20230917093020287

2.5 SQL性能分析

2.5.1 SQL执行频率

image-20230917093634100

-- 查看系统的状态信息
show global status like 'Com_______';

2.5.2 慢查询日志

image-20230917094314803

-- 查询慢查询日志是否开启
show variables like 'slow_query_log';

2.5.3 profile详情

image-20230917095232011

-- 是否支持profile详情功能
select @@have_profiling;

-- 查看是否开启该功能
select @@profiling;

-- 开启profile
set profiling = 1;

image-20230917095320953

2.5.4 explain执行计划

image-20230917095911304

image-20230917101205097

image-20230917101337147

2.6 索引使用

2.6.1 验证索引效率

image-20230917141334774

现在有一个有着10000000数据的表(tb_sku),我们根据sn这一字段查询某一条具体的数据,没有建立索引之前的查询耗费的时间是20多秒,建立索引耗费大约90秒,为sn字段建立索引之后,查询耗费时间大概是零点几秒。

2.6.2 最左前缀法则

image-20230917142410276

image-20230917142707646

2.6.3 索引失效的情况

image-20230917142948227

image-20230917143143047

image-20230917143421975

image-20230917143842497

image-20230917144459567

2.6.4 SQL提示

image-20230917145046280

2.6.5 覆盖索引

image-20230917145643194

2.6.6 前缀索引

image-20230917153757773

image-20230917154300147

2.6.7 单例索引与联合索引的选择问题

image-20230917155017641

image-20230917155223679

2.7 索引设计原则

image-20230917155748055

3.SQL优化

3.1 插入数据的优化

image-20230917181618942

image-20230917181842030

100万的数据如果使用insert插入的话需要10分钟左右,如果使用文件的方式导入,只需要10多秒。

3.2 主键优化

image-20230917205500523

image-20230917205636137

image-20230917205833596

image-20230917210112389

image-20230917210303469

3.3 order by优化

image-20230917210730392

image-20230917211254167

image-20230917210616858

image-20230917211543109

3.4 group by优化

image-20230917212026101

3.5 limit优化

image-20230917212530539

3.6 count优化

image-20230917212709489

image-20230917213029734

image-20230917213316193

3.7 update优化

image-20230917213859581

3.8 总结

image-20230917214424722

4.视图/存储过程/触发器

4.1 视图

4.1.1 视图介绍

​ 视图(View)是一种虚拟存在的表。视图中的数据并不在数据库中实际存在,行和列数据来自定义视图的查询中使用的表,并且是在使用视图时动态生成的。通俗的讲,视图只保存了查询的SQL逻辑,不保存查询结果。所以我们在创建视图的时候,主要的工作就落在创建这条SQL查询语句上。

4.1.2 语法

创建

CREATE [OR REPLACE] VIEW 视图名称[(列名列表)] AS SELECT语句 [ WITH [CASCADED | LOCAL ] CHECK OPTION ]

查询

查看创建视图语句:SHOW CREATE VIEW 视图名称;
查看视图数据:SELECT * FROM 视图名称 ...... ;

修改

方式一:CREATE [OR REPLACE] VIEW 视图名称[(列名列表)] AS SELECT语句 [ WITH [ CASCADED | LOCAL ] CHECK OPTION ]
方式二:ALTER VIEW 视图名称[(列名列表)] AS SELECT语句 [ WITH [ CASCADED | LOCAL ] CHECK OPTION ]

删除

DROP VIEW [IF EXISTS] 视图名称 [,视图名称] ...

演示示例

-- 视图
-- 创建视图
create or replace view stu_v_1 as select id, name from student where id <= 10;

-- 查看创建视图的语句
show create view stu_v_1;

-- 查看视图数据
select * from stu_v_1;
-- 也可以加上条件
select * from stu_v_1 where id = 1;

-- 修改视图的方式(or replace可以实现修改效果)
-- 方式一
create or replace view stu_v_1 as select id, name, no from student where id <= 10;
-- 方式二
alter view stu_v_1 as select id, name from student where id <= 10;

-- 删除视图
drop view if exists stu_v_1;

-- 通过视图实现插入操作
create or replace view stu_v_1 as select id, name from student where id <= 20;

-- 向视图中添加数据(实际上插入的数据插入到原table表中)
insert into stu_v_1 values(6,'Tom');

4.1.3 检查选项

​ 当使用WITH CHECK OPTION子句创建视图时,MySQL会通过视图检查正在更改的每个行,例如 插入,更新,删除,以使其符合视图的定义。MySQL允许基于另一个视图创建视图,它还会检查依赖视图中的规则以保持一致性。为了确定检查的范围,mysql提供了两个选项: CASCADED 和 LOCAL,默认值为 CASCADED 。

1.CASCADED 级联

​ 比如,v2视图是基于v1视图的,如果在v2视图创建的时候指定了检查选项为 cascaded,但是v1视图创建时未指定检查选项。 则在执行检查时,不仅会检查v2,还会级联检查v2的关联视图v1。

image-20231006182938266

2.LOCAL 本地

​ 比如,v2视图是基于v1视图的,如果在v2视图创建的时候指定了检查选项为 local ,但是v1视图创建时未指定检查选项。 则在执行检查时,知会检查v2,不会检查v2的关联视图v1。

image-20231006183051143

-- 检查选项
-- 添加了检查选项,在插入数据的时候会检查插入的数据是否满足视图创建的条件(创建语句为create or replace view stu_v_1 as select id, name from student where id <= 20;此时插入的数据id要<=20,否则报错)
-- cascaded:插入时会检查是否满足当前视图和依赖的视图的条件
create or replace view stu_v_1 as select id, name from student where id <= 20 with cascaded check option ;

-- local:插入时会检查是否满足当前视图和依赖的视图(定义了检查选项的视图)的条件
create or replace view stu_v_1 as select id, name from student where id <= 20 with local check option ;

-- local和cascaded的区别:
-- local:v2依赖了v1,只有v1也加上了with local check option检查选项,在v2插入数据的时候才会检查v1的条件
-- cascaded:v2依赖了v1,不管v1有没有加上检查选项,在v2插入数据时都会检查v1的条件

-- 重点
-- 1.加不加检查选项的区别
-- 2.with cascaded check option和with local check option的区别

4.1.4 视图更新

要使视图可更新,视图中的行与基础表中的行之间必须存在一对一的关系。如果视图包含以下任何一项,则该视图不可更新:

A. 聚合函数或窗口函数(SUM()、 MIN()、 MAX()、 COUNT()等)

B. DISTINCT

C. GROUP BY

D. HAVING

E. UNION 或者 UNION ALL

image-20231006184725321

4.1.5 视图作用

image-20231006185121986

4.1.6 视图案例

tb_user表结构以及sql语句

create table tb_user(
id int primary key auto_increment comment '主键',
name varchar(50) not null comment '用户名',
phone varchar(11) not null comment '手机号',
email varchar(100) comment '邮箱',
profession varchar(11) comment '专业',
age tinyint unsigned comment '年龄',
gender char(1) comment '性别 , 1: 男, 2: 女',
status char(1) comment '状态',
createtime datetime comment '创建时间'
) comment '系统用户表';


INSERT INTO itcast.tb_user (name, phone, email, profession, age, gender, status, createtime) VALUES ('吕布', '17799990000', 'lvbu666@163.com', '软件工程', 23, '1', '6', '2001-02-02 00:00:00');
INSERT INTO itcast.tb_user (name, phone, email, profession, age, gender, status, createtime) VALUES ('曹操', '17799990001', 'caocao666@qq.com', '通讯工程', 33, '1', '0', '2001-03-05 00:00:00');
INSERT INTO itcast.tb_user (name, phone, email, profession, age, gender, status, createtime) VALUES ('赵云', '17799990002', '17799990@139.com', '英语', 34, '1', '2', '2002-03-02 00:00:00');
INSERT INTO itcast.tb_user (name, phone, email, profession, age, gender, status, createtime) VALUES ('孙悟空', '17799990003', '17799990@sina.com', '工程造价', 54, '1', '0', '2001-07-02 00:00:00');
INSERT INTO itcast.tb_user (name, phone, email, profession, age, gender, status, createtime) VALUES ('花木兰', '17799990004', '19980729@sina.com', '软件工程', 23, '2', '1', '2001-04-22 00:00:00');
INSERT INTO itcast.tb_user (name, phone, email, profession, age, gender, status, createtime) VALUES ('大乔', '17799990005', 'daqiao666@sina.com', '舞蹈', 22, '2', '0', '2001-02-07 00:00:00');
INSERT INTO itcast.tb_user (name, phone, email, profession, age, gender, status, createtime) VALUES ('露娜', '17799990006', 'luna_love@sina.com', '应用数学', 24, '2', '0', '2001-02-08 00:00:00');
INSERT INTO itcast.tb_user (name, phone, email, profession, age, gender, status, createtime) VALUES ('程咬金', '17799990007', 'chengyaojin@163.com', '化工', 38, '1', '5', '2001-05-23 00:00:00');
INSERT INTO itcast.tb_user (name, phone, email, profession, age, gender, status, createtime) VALUES ('项羽', '17799990008', 'xiaoyu666@qq.com', '金属材料', 43, '1', '0', '2001-09-18 00:00:00');
INSERT INTO itcast.tb_user (name, phone, email, profession, age, gender, status, createtime) VALUES ('白起', '17799990009', 'baiqi666@sina.com', '机械工程及其自动化', 27, '1', '2', '2001-08-16 00:00:00');
INSERT INTO itcast.tb_user (name, phone, email, profession, age, gender, status, createtime) VALUES ('韩信', '17799990010', 'hanxin520@163.com', '无机非金属材料工程', 27, '1', '0', '2001-06-12 00:00:00');
INSERT INTO itcast.tb_user (name, phone, email, profession, age, gender, status, createtime) VALUES ('荆轲', '17799990011', 'jingke123@163.com', '会计', 29, '1', '0', '2001-05-11 00:00:00');
INSERT INTO itcast.tb_user (name, phone, email, profession, age, gender, status, createtime) VALUES ('兰陵王', '17799990012', 'lanlinwang666@126.com', '工程造价', 44, '1', '1', '2001-04-09 00:00:00');
INSERT INTO itcast.tb_user (name, phone, email, profession, age, gender, status, createtime) VALUES ('狂铁', '17799990013', 'kuangtie@sina.com', '应用数学', 43, '1', '2', '2001-04-10 00:00:00');
INSERT INTO itcast.tb_user (name, phone, email, profession, age, gender, status, createtime) VALUES ('貂蝉', '17799990014', '84958948374@qq.com', '软件工程', 40, '2', '3', '2001-02-12 00:00:00');
INSERT INTO itcast.tb_user (name, phone, email, profession, age, gender, status, createtime) VALUES ('妲己', '17799990015', '2783238293@qq.com', '软件工程', 31, '2', '0', '2001-01-30 00:00:00');
INSERT INTO itcast.tb_user (name, phone, email, profession, age, gender, status, createtime) VALUES ('芈月', '17799990016', 'xiaomin2001@sina.com', '工业经济', 35, '2', '0', '2000-05-03 00:00:00');
INSERT INTO itcast.tb_user (name, phone, email, profession, age, gender, status, createtime) VALUES ('嬴政', '17799990017', '8839434342@qq.com', '化工', 38, '1', '1', '2001-08-08 00:00:00');
INSERT INTO itcast.tb_user (name, phone, email, profession, age, gender, status, createtime) VALUES ('狄仁杰', '17799990018', 'jujiamlm8166@163.com', '国际贸易', 30, '1', '0', '2007-03-12 00:00:00');
INSERT INTO itcast.tb_user (name, phone, email, profession, age, gender, status, createtime) VALUES ('安琪拉', '17799990019', 'jdodm1h@126.com', '城市规划', 51, '2', '0', '2001-08-15 00:00:00');
INSERT INTO itcast.tb_user (name, phone, email, profession, age, gender, status, createtime) VALUES ('典韦', '17799990020', 'ycaunanjian@163.com', '城市规划', 52, '1', '2', '2000-04-12 00:00:00');
INSERT INTO itcast.tb_user (name, phone, email, profession, age, gender, status, createtime) VALUES ('廉颇', '17799990021', 'lianpo321@126.com', '土木工程', 19, '1', '3', '2002-07-18 00:00:00');
INSERT INTO itcast.tb_user (name, phone, email, profession, age, gender, status, createtime) VALUES ('后羿', '17799990022', 'altycj2000@139.com', '城市园林', 20, '1', '0', '2002-03-10 00:00:00');
INSERT INTO itcast.tb_user (name, phone, email, profession, age, gender, status, createtime) VALUES ('姜子牙', '17799990023', '37483844@qq.com', '工程造价', 29, '1', '4', '2003-05-26 00:00:00');

案例

-- 视图案例
-- 为了保证数据库表的安全性,开发人员在操作tb_user表时,只能看到的用户的基本字段,屏蔽手机号和邮箱两个字段
create view tb_user_view as select id, name, profession, age, gender, status, createtime from tb_user;
select * from tb_user_view;

-- 查询每个学生所选修的课程(三张表联查),这个功能在很多的业务中都有使用到,为了简化操作,定义一个视图
create view tb_stu_course_view as select s.name student_name,s.no,c.name course_name from student s, course c, student_course sc where s.id = sc.studentid and c.id = sc.courseid;

4.2 存储过程

4.2.1 介绍

image-20231006190705216

image-20231006190743209

4.2.2 基本语法

创建

CREATE PROCEDURE 存储过程名称 ([ 参数列表 ])
BEGIN
-- SQL语句
END ;

调用

CALL 名称 ([ 参数 ]);

查看

SELECT * FROM INFORMATION_SCHEMA.ROUTINES WHERE ROUTINE_SCHEMA = 'xxx'; -- 查询指定数据库的存储过程及状态信息

SHOW CREATE PROCEDURE 存储过程名称 ; -- 查询某个存储过程的定义

删除

DROP PROCEDURE [ IF EXISTS ] 存储过程名称 ;

注意:
在命令行中,执行创建存储过程的SQL时,需要通过关键字 delimiter 指定SQL语句的结束符

演示示例

-- 存储过程
-- 创建存储过程
create procedure p1()
begin
select count(*) from student;
end;

-- 调用
call p1();

-- 查看
-- 查询指定数据库的存储过程及状态信息
-- 查询itcast数据库中定义的存储过程
select * from information_schema.ROUTINES where ROUTINE_SCHEMA = 'itcast';
-- 查询某个存储过程的定义
SHOW CREATE PROCEDURE p1 ;

-- 删除
drop procedure if exists p1;

4.2.3 变量

在MySQL中变量分为三种类型: 系统变量、用户定义变量、局部变量

4.2.3.1 系统变量

系统变量 是MySQL服务器提供,不是用户定义的,属于服务器层面。分为全局变量(GLOBAL)、会话 变量(SESSION)

1.查看系统变量

SHOW [ SESSION | GLOBAL ] VARIABLES ; -- 查看所有系统变量
SHOW [ SESSION | GLOBAL ] VARIABLES LIKE '......'; -- 可以通过LIKE模糊匹配方式查找变量
SELECT @@[SESSION | GLOBAL] 系统变量名; -- 查看指定变量的值

2.设置系统变量

SET [ SESSION | GLOBAL ] 系统变量名 = 值 ;
SET @@[SESSION | GLOBAL]系统变量名 = 值 ;

注意:

如果没有指定SESSION/GLOBAL,默认是SESSION,会话变量

1 mysql服务重新启动之后,所设置的全局参数会失效,要想不失效,可以在 /etc/my.cnf 中配置

​ A. 全局变量(GLOBAL): 全局变量针对于所有的会话

​ B. 会话变量(SESSION): 会话变量针对于单个会话,在另外一个会话窗口就不生效了

演示示例

-- 系统变量
-- 查看所有系统变量
show session variables ; #查看会话的系统变量
show global variables ; #查看全局的系统变量
-- 查看以auto开头的系统变量
show variables like 'auto%';
-- 查看某一个具体的环境变量值(以autocommit为例)
select @@autocommit; #会话的
select @@global.autocommit; #全局的

-- 设置系统变量(以autocommit为例)
set session autocommit = 1; #会话级别的
set global autocommit = 1; #全局级别的
-- 或者
set @@autocommit = 1;
set @@global.autocommit = 1;
4.2.3.2 用户定义变量

用户定义变量 是用户根据需要自己定义的变量,用户变量不用提前声明,在用的时候直接用 “@变量 名” 使用就可以。其作用域为当前连接

1.赋值

方式一:

SET @var_name = expr [, @var_name = expr] ... ;
SET @var_name := expr [, @var_name := expr] ... ;

赋值时,可以使用 = ,也可以使用 :=

方式二:

SELECT @var_name := expr [, @var_name := expr] ... ;
SELECT 字段名 INTO @var_name FROM 表名;

2.使用

SELECT @var_name ;

注意: 用户定义的变量无需对其进行声明或初始化,只不过获取到的值为NULL

演示示例

-- 用户定义变量
-- 赋值
-- 方式一
set @myname = 'itcast';
set @myage := 10;
set @mygender := '男',@myhobby = 'java';
-- 方式二
select @mycolor := 'red';
select count(*) into @mycount from tb_user; #将tb_user表中的总记录数赋值给@mycount

-- 使用
select @myname,@myage,@mygender,@myhobby,@mycolor,@mycount;
4.2.3.3 局部变量

局部变量 是根据需要定义的在局部生效的变量,访问之前,需要DECLARE声明。可用作存储过程内的 局部变量和输入参数,局部变量的范围是在其内声明的BEGIN … END块

1**.声明**

DECLARE 变量名 变量类型 [DEFAULT ... ] ;

变量类型就是数据库字段类型:INT、BIGINT、CHAR、VARCHAR、DATE、TIME等

2.赋值

SET 变量名 = 值 ;
SET 变量名 := 值 ;
SELECT 字段名 INTO 变量名 FROM 表名 ... ;

演示示例

-- 局部变量
-- 创建存储过程
create procedure p2()
begin
# 定义一个int类型的局部变量,默认值是0
declare stu_count int default 0;
# 给stu_count赋值
-- set stu_count = 100;
select count(*) into stu_count from tb_user;
#输出
select stu_count;
end;
-- 调用函数
call p2();

4.2.4 if判断

介绍

if 用于做条件判断,具体的语法结构为:

IF 条件1 THEN
.....
ELSEIF 条件2 THEN -- 可选
.....
ELSE -- 可选
.....
END IF;

在if条件判断的结构中,ELSE IF 结构可以有多个,也可以没有. ELSE结构可以有,也可以没有.

案例

根据定义的分数score变量,判定当前分数对应的分数等级

  • score >= 85分,等级为优秀

  • score >= 60分 且 score < 85分,等级为及格

  • score < 60分,等级为不及格

-- if判断
-- 根据定义的分数score变量,判定当前分数对应的分数等级
-- score >= 85分,等级为优秀
-- score >= 60分 且 score < 85分,等级为及格
-- score < 60分,等级为不及格
drop procedure p3;
create procedure p3()
begin
# 先给一个默认值 58
declare score int default 58;
declare result varchar(10);
if score >= 85 then
set result := '优秀';
elseif score >= 60 then
set result := '及格';
else
set result := '不及格';
end if;
#输出
select result;
end ;
-- 调用
call p3();

4.2.5 参数

介绍

参数的类型,主要分为以下三种:IN、OUT、INOUT. 具体的含义如下:

image-20231006213944694

语法

CREATE PROCEDURE 存储过程名称 ([ IN/OUT/INOUT 参数名 参数类型 ])
BEGIN
-- SQL语句
END ;

案例

1.根据传入的分数score变量,判定当前分数对应的分数等级,并返回

  • score >= 85分,等级为优秀

  • score >= 60分 且 score < 85分,等级为及格

  • score < 60分,等级为不及格

-- 参数
-- 1.根据传入的分数score变量,判定当前分数对应的分数等级,并返回
-- score >= 85分,等级为优秀
-- score >= 60分 且 score < 85分,等级为及格
-- score < 60分,等级为不及格
create procedure p4(in score int, out result varchar(10))
begin
if score >= 85 then
set result := '优秀';
elseif score >= 60 then
set result := '及格';
else
set result := '不及格';
end if;
end ;
-- 调用
call p4(88,@result);
-- 查看结果
select @result;

2.将传入的200分制的分数,进行换算,换成百分制,然后返回

-- 2.将传入的200分制的分数,进行换算,换成百分制,然后返回
create procedure p5(inout score double)
begin
set score := score * 0.5;
end;
-- 调用(score既是输入又是输出)
set @score = 78;
call p5(@score);
select @score;

4.2.6 case

]]>
后端 Mysql
MybatisX插件的使用 /posts/24606.html 代码生成器 根据数据库表生成Mapper接口,Mapper配置文件,service

注解 mybatisPlus提供的注解

公共字段的自动填充 逻辑删除 乐观锁 雪花算法生成主键

image-20230321181437910

image-20230321181452909

]]>
后端 插件
OCR-图片文字识别 /posts/58456.html 一.什么是OCR

OCR (Optical Character Recognition,光学字符识别)是指电子设备(例如扫描仪或数码相机)检查纸上打印的字符,通过检测暗、亮的模式确定其形状,然后用字符识别方法将形状翻译成计算机文字的过程

方案 说明
百度OCR 收费
Tesseract-OCR Google维护的开源OCR引擎,支持Java,Python等语言调用
Tess4J 封装了Tesseract-OCR ,支持Java调用

二.Tesseract-OCR 的特点

  • Tesseract支持UTF-8编码格式,并且可以“开箱即用”地识别100多种语言

  • Tesseract支持多种输出格式:纯文本,hOCR(HTML),PDF等

  • 官方建议,为了获得更好的OCR结果,最好提供给高质量的图像

  • Tesseract进行识别其他语言的训练,具体的训练方式请参考官方提供的文档:https://tesseract-ocr.github.io/tessdoc/

三.使用案例

1.导入相关的依赖

<dependency>
<groupId>net.sourceforge.tess4j</groupId>
<artifactId>tess4j</artifactId>
<version>4.1.1</version>
</dependency>

2.导入中文字体库

地址: https://wwvc.lanzouj.com/iuPhc1h7j46f

chi_sim.traineddata

image-20231208212352206

3.编写测试类进行测试

待识别的图片

image-20231208205943762

测试程序

package com.heima;


import net.sourceforge.tess4j.Tesseract;
import net.sourceforge.tess4j.TesseractException;

import java.io.File;

/**
* @author Jason Gong
* @version 1.0
* @Date 2023/12/8
* @Description
*/
public class Main {

/**
* 识别图片中的文字
*
* @param args
*/
public static void main(String[] args) throws TesseractException {
//创建实例
Tesseract tesseract = new Tesseract();
//设置字体库的路径
tesseract.setDatapath("C:\\Gong\\data\\tess4j");
//设置语言
//字体库为chi_sim.traineddata,语言取.前面的内容,即文件名
//简体中文
tesseract.setLanguage("chi_sim");
//识别图片
String ocr = tesseract.doOCR(new File("C:\\Gong\\data\\tess4j\\tess4j.png"));
//打印识别的结果
//打印的时候可以去除回车和tab空格
//System.out.println(ocr.replaceAll("\\n|\\r","-"));
System.out.println(ocr);
}
}

识别的结果

image-20231208211200859

四.封装成工具类使用

1.创建工具类

package com.heima.common.tess4j;

import lombok.Getter;
import lombok.Setter;
import net.sourceforge.tess4j.ITesseract;
import net.sourceforge.tess4j.Tesseract;
import net.sourceforge.tess4j.TesseractException;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.stereotype.Component;

import java.awt.image.BufferedImage;

@Getter
@Setter
@Component
@ConfigurationProperties(prefix = "tess4j")
public class Tess4jClient {

private String dataPath;
private String language;

public String doOCR(BufferedImage image) throws TesseractException {
//创建Tesseract对象
ITesseract tesseract = new Tesseract();
//设置字体库路径
tesseract.setDatapath(dataPath);
//中文识别
tesseract.setLanguage(language);
//执行ocr识别
String result = tesseract.doOCR(image);
//替换回车和tal键 使结果为一行
result = result.replaceAll("\\r|\\n", "-").replaceAll(" ", "");
return result;
}

}

2.配置文件中添加配置

tess4j:
data-path: C:\workspace\tessdata # 字体库的路径
language: chi_sim # 识别的字体d
]]>
后端 OCR
SSM框架基础知识及整合 /posts/56742.html PDF版本的笔记

Mybatis

Spring SpringMVC MyBatis

]]>
后端 SSM
SpringBoot中整合Swagger2 /posts/32246.html 1.Swagger2的介绍

什么是swagger2

编写和维护接口文档是每个程序员的职责,根据Swagger2可以快速帮助我们编写最新的API接口文档,再也不用担心开会前仍忙于整理各种资料了,间接提升了团队开发的沟通效率。

常用注解

swagger通过注解表明该接口会生成文档,包括接口名、请求方法、参数、返回信息等等

@Api:修饰整个类,描述Controller的作用

@ApiOperation:描述一个类的一个方法,或者说一个接口

@ApiParam:单个参数描述

@ApiModel:用对象来接收参数

@ApiModelProperty:用对象接收参数时,描述对象的一个字段

@ApiImplicitParam:一个请求参数

@ApiImplicitParams:多个请求参数

2.SpringBoot中使用Swagger

2.1导入相关的依赖

注意:Swagger2和SpringBoot存在版本兼容的问题,选择的时候要根据SpringBoot的版本进行选择

<dependency>
<groupId>io.springfox</groupId>
<artifactId>springfox-swagger2</artifactId>
<version>2.7.0</version>
</dependency>
<dependency>
<groupId>io.springfox</groupId>
<artifactId>springfox-swagger-ui</artifactId>
<version>2.7.0</version>
</dependency>

2.2创建配置类

@Configuration
@EnableSwagger2
public class Swagger2 {
@Bean
public Docket adminApiConfig(){

return new Docket(DocumentationType.SWAGGER_2)
.groupName("adminApi")
.apiInfo(apiInfo())
.select()
//只显示admin路径下的页面
.paths(Predicates.and(PathSelectors.regex("/admin/.*")))// .paths(PathSelectors.any())表示所有
.build();

}

private ApiInfo apiInfo(){
return new ApiInfoBuilder()
.title("后台管理系统-API文档")
.description("本文档描述了后台管理系统微服务接口定义")
.version("1.0")
.contact(new Contact("GongChangjiang", "https://gong-changjiang.gitee.io/", "2602183349@qq.com"))
.build();
}

}

2.3配置包扫描的规则

//在项目的启动类加上该注解,swagger就会扫描如下包下的controller方法,生成接口文档
@ComponentScan(basePackages = "com.xxxx")

2.4使用接口文档

//浏览器访问该网址
//这里的端口号是要生成接口文档端口的端口号
http://localhost:8080/swagger-ui.html

image-20230412141010237

]]>
后端 SpringBoot
SpringBoot中使用定时任务 /posts/73.html 1.在启动类上添加@EnableScheduling注解
@EnableScheduling

2.创建定时任务类,设置cron表达式

定时任务可以单独建立一个包 package com.atguigu.schedule

加上@Component注解,交给spring管理,启动这个模块,定时任务就开启了

package com.atguigu.schedule;

import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Component;

/**
* @author GongChangjiang
* @version 1.0
* @Date 2023/3/29
* @Description 定时任务
*/
@Component
public class ScheduledTask {

//每隔五秒执行这个方法
//"0/5 * * * * ?" 是corn表达式
@Scheduled(cron = "0/5 * * * * ?")
public void tesSchedule(){
System.out.println("定时任务执行了....");
}
}

image-20230329184540977

3.Corn表达式(七子[七域]表达式)生成工具

网址: https://cron.qqe2.com/

image-20230329184618098

]]>
后端 SpringBoot
SpringBoot入门教程 /posts/60684.html 尚硅谷雷丰阳的SpringBoot零基础入门教程

本视频笔记地址:https://yuque.com/atguigu/springboot

本视频源码地址:https://gitee.com/leifengyang/springboot2

Spring官网: https://spring.io/

SpringBoot2的环境要求

image-20230620092919922

一.SpringBoot2核心技术-基础入门

1.Spring能做什么

这里的Spring指的是整个Spring生态

image-20230620093420150

微服务 响应式编程 分布式 WEB开发 无服务开发 事件驱动 批处理

2.SpringBoot

Spring Boot makes it easy to create stand-alone, production-grade Spring based Applications that you can “just run”.

能快速创建出生产级别的Spring应用

SpringBoot是整合Spring技术栈的一站式框架

SpringBoot是简化Spring技术栈的快速开发脚手架

2.1 SpringBoot的优点

● Create stand-alone Spring applications
○ 创建独立Spring应用
● Embed Tomcat, Jetty or Undertow directly (no need to deploy WAR files)
○ 内嵌web服务器
● Provide opinionated ‘starter’ dependencies to simplify your build configuration
○ 自动starter依赖,简化构建配置
● Automatically configure Spring and 3rd party libraries whenever possible
○ 自动配置Spring以及第三方功能
● Provide production-ready features such as metrics, health checks, and externalized configuration
○ 提供生产级别的监控、健康检查及外部化配置
● Absolutely no code generation and no requirement for XML configuration
○ 无代码生成、无需编写XML

2.2、SpringBoot缺点

  • 人称版本帝,迭代快,需要时刻关注变化(说明社区活跃,也是个优点)
  • 封装太深,内部原理复杂,不容易精通

2.3 官方文档

进入官网 找到SpringBoot 点击Reference Doc进入官方文档

image-20230620101533445

3.SpringBoot入门

1.系统的要求

  • Java 8 & 兼容java14 .
  • Maven 3.3+
  • idea 2019.1.2

**Maven的配置教程: **

Maven配置文件settings.xml | The Blog (gitee.io)

开发环境的搭建 | The Blog (gitee.io)

2.HelloWorld

需求:浏览发送/hello请求,响应 Hello,Spring Boot 2

1创建maven工程

image-20230725160846882

2.引入依赖

<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.3.4.RELEASE</version>
</parent>
<!-- WEB场景的启动器 -->
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>

</dependencies>

3.创建主程序

package com.atguigu.boot;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;

/**
* @author Jason Gong
* @version 1.0
* @website https://jasonsgong.gitee.io
* @Date 2023/7/25
* @Description
*/
@SpringBootApplication //标注这是一个SpringBoot应用
public class MainApplication {
public static void main(String[] args) {
SpringApplication.run(MainApplication.class,args);
}
}

4.编写Controller

package com.atguigu.boot.controller;

import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

/**
* @author Jason Gong
* @version 1.0
* @website https://jasonsgong.gitee.io
* @Date 2023/7/25
* @Description
*/
@RestController
public class HelloController {

@RequestMapping
public String handle01(){
return "Hello,Spring Boot2";
}
}
image-20230725162201612

3.简化配置

官网地址:https://docs.spring.io/spring-boot/docs/2.7.14/reference/html/application-properties.html#appendix.application-properties

server.port=8888

4.简化部署

通过在pom.xml文件中插入这个插件,可以直接将项目打成jar包简化部署

 <build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>

image-20230725163026091

可以直接使用cmd切换到target目录下使用java -jar 命令运行jar包

image-20230725163212347

注意点

去掉cmd的快速编辑模式(不去掉启动的时候点击屏幕会停止)

image-20230725164124574

4.了解自动装配原理

4.1 依赖管理

1.SpringBoot的自动版本仲裁机制

<!-- 项目的继承的父项目-->
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.3.4.RELEASE</version>
</parent>
<!-- 父项目继承的父项目-->
<!--几乎声明了开发中常用依赖的版本号-->
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-dependencies</artifactId>
<version>2.3.4.RELEASE</version>
</parent>

image-20230725165414051

自定义mysql版本驱动

在父依赖中mysql是8版本的

image-20230725170329032

可以在pom.xml中通过自定义properties修改版本

<properties>
<!--重写mysql的版本号-->
<mysql.version>5.1.43</mysql.version>
</properties>

image-20230725170257030

2.Starters场景启动器

官网: https://docs.spring.io/spring-boot/docs/2.7.14/reference/html/using.html#using.build-systems.starters

spring-boot-starter-*起头的starter为官方提供的starter,*-spring-boot-starter为第三方提供的starter

常见的Starter

image-20230725171130925

以引入spring-boot-starter-web为例,引入该starter它会帮我们引入以下的其它依赖

image-20230725171550618

4.2 自动配置

  • 自动配置Tomcat

    1. 引入依赖

      image-20230725172719608

    2. 配置Tomcat

  • 自动配置SpringMvc

    1. 引入springMvc全套组件

    2. 自动配置好springMvc的常用组件(字符编码,文件上传解析器,dispatcherServlet等)

      @SpringBootApplication
      public class MainApplication {
      public static void main(String[] args) {
      //返回ioc容器
      ConfigurableApplicationContext run = SpringApplication.run(MainApplication.class, args);
      //获取容器里面组件的名字
      String[] beanDefinitionNames = run.getBeanDefinitionNames();
      for (String beanDefinitionName : beanDefinitionNames) {
      //打印容器内的组件
      System.out.println(beanDefinitionName);
      }
      }
      }

      image-20230725172834666

  • 默认的包结构

    主程序所在的包以及其的子包都可以被扫描到,无需配置包扫描

    com
    +- example
    +- myapplication
    +- MyApplication.java #主程序
    |
    +- customer
    | +- Customer.java
    | +- CustomerController.java
    | +- CustomerService.java
    | +- CustomerRepository.java
    |
    +- order
    +- Order.java
    +- OrderController.java
    +- OrderService.java
    +- OrderRepository.java

    如果需要扫描的文件在主程序的上级目录,我们也想扫描到它,我们需要扩大一下包的扫描范围

    @SpringBootApplication(scanBasePackages = "com.atguigu")
    或者
    @ComponentScan("com.atguigu")
  • 各种配置拥有默认值

    image-20230725174350129

  • 按需加载所有的自动配置项

    spingboot的所有配置存在于下面的这个依赖中

    <dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-autoconfigure</artifactId>
    <version>2.3.4.RELEASE</version>
    <scope>compile</scope>
    </dependency>

    image-20230725175010683

4.3 容器功能

1.组件添加

1.1 @Configuration
  • 基本使用

  • Full模式与Lite模式

    • 示例
    • 最佳实战
      • 配置 类组件之间无依赖关系用Lite模式加速容器启动过程,减少判断
      • 配置类组件之间有依赖关系,方法会被调用得到之前单实例组件,用Full模式
/**
* 1、配置类里面使用@Bean标注在方法上给容器注册组件,默认也是单实例的
* 2、配置类本身也是组件
* 3、proxyBeanMethods:代理bean的方法
* Full(proxyBeanMethods = true)、【保证每个@Bean方法被调用多少次返回的组件都是单实例的】
* Lite(proxyBeanMethods = false)【每个@Bean方法被调用多少次返回的组件都是新创建的】
* 组件依赖必须使用Full模式默认。其他默认是否Lite模式
*/
@Configuration(proxyBeanMethods = false) //告诉SpringBoot这是一个配置类 == 配置文件
public class MyConfig {

/**
* Full:外部无论对配置类中的这个组件注册方法调用多少次获取的都是之前注册容器中的单实例对象
* @return
*/
@Bean //给容器中添加组件。以方法名作为组件的id。返回类型就是组件类型。返回的值,就是组件在容器中的实例
public User user01(){
User zhangsan = new User("zhangsan", 18);
//user组件依赖了Pet组件
zhangsan.setPet(tomcatPet());
return zhangsan;
}

@Bean("tom")
public Pet tomcatPet(){
return new Pet("tomcat");
}
}
@SpringBootConfiguration
@EnableAutoConfiguration
@ComponentScan("com.atguigu.boot")
public class MainApplication {

public static void main(String[] args) {
//1、返回我们IOC容器
ConfigurableApplicationContext run = SpringApplication.run(MainApplication.class, args);

//2、查看容器里面的组件
String[] names = run.getBeanDefinitionNames();
for (String name : names) {
System.out.println(name);
}

//3、从容器中获取组件

Pet tom01 = run.getBean("tom", Pet.class);

Pet tom02 = run.getBean("tom", Pet.class);

System.out.println("组件:"+(tom01 == tom02));


//4、com.atguigu.boot.config.MyConfig$$EnhancerBySpringCGLIB$$51f1e1ca@1654a892
MyConfig bean = run.getBean(MyConfig.class);
System.out.println(bean);

//如果@Configuration(proxyBeanMethods = true)代理对象调用方法。SpringBoot总会检查这个组件是否在容器中有。
//保持组件单实例
User user = bean.user01();
User user1 = bean.user01();
System.out.println(user == user1);


User user01 = run.getBean("user01", User.class);
Pet tom = run.getBean("tom", Pet.class);

System.out.println("用户的宠物:"+(user01.getPet() == tom));
}
}
1.2 @Bean @Component @Controller @Service @Repository
//默认是单实例的
@Bean//给容器中添加组件,以方法名作为组件的id(组件名),返回类型就是组件类型,返回的值,就是组件在容器中的实例,@Bean("user") 是自定义组件名为user
public User user01(){
return new User("Tom",100);
}
1.3 @ComponentScan @Import
/*
* @Import({User.class, DBHelper.class}) 给容器中自动创建出这两个类型的组件、默认组件的名字就是全类名
*/
@Import({User.class, DBHelper.class})
@Configuration(proxyBeanMethods = false)
public class MyConfig {
}
1.4 @Conditional 条件装配:满足Conditional指定的条件,则进行组件注入

image-20230802141422751

//放在类上面,条件成立,这个类下面的所有配置生效,放在方法上面,条件成立,这个方法下的配置才会成立
@ConditionalOnBean(name = "tom") //存在一个id为tom的组件的时候才会生效
@ConditionalOnMissingBean(name = "tom")

2.原生配置文件的引入

2.1、@ImportResource

beans.xml*)

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:context="http://www.springframework.org/schema/context"
xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd http://www.springframework.org/schema/context https://www.springframework.org/schema/context/spring-context.xsd">

<bean id="haha" class="com.atguigu.boot.bean.User">
<property name="name" value="zhangsan"></property>
<property name="age" value="18"></property>
</bean>

<bean id="hehe" class="com.atguigu.boot.bean.Pet">
<property name="name" value="tomcat"></property>
</bean>
</beans>

在java类中红使用@ImportResource(“classpath:beans.xml”)导入上面的配置文件

@ImportResource("classpath:beans.xml")
public class MyConfig {}

3.配置绑定

3.1 @ConfigurationProperties
/**
* 只有在容器中的组件,才会拥有SpringBoot提供的强大功能
*/
@Component //或者在配置类上添加@EnableConfigurationProperties(Car.class)注解,不使用@Component
@ConfigurationProperties(prefix = "mycar")
public class Car {

private String brand;
private Integer price;

public String getBrand() {
return brand;
}

public void setBrand(String brand) {
this.brand = brand;
}

public Integer getPrice() {
return price;
}

public void setPrice(Integer price) {
this.price = price;
}

@Override
public String toString() {
return "Car{" +
"brand='" + brand + '\'' +
", price=" + price +
'}';
}
}

配置文件中的值

mycar.brand=BYD
mycar.price=120000

编写测试的controller

@Autowired
private Car car;
@RequestMapping("/car")
public Car ggetCar(){
return car;
}
image-20230802153951542

4.4 自动配置原理入门

1.引导加载自动配置类

@SpringBootApplication

image-20230802155222746

@SpringBootConfiguration //代表当前是一个配置类
@EnableAutoConfiguration
@ComponentScan(excludeFilters = { @Filter(type = FilterType.CUSTOM, classes = TypeExcludeFilter.class),
@Filter(type = FilterType.CUSTOM, classes = AutoConfigurationExcludeFilter.class) })//指定扫描那些spring注解
public @interface SpringBootApplication{}

@EnableAutoConfiguration

@AutoConfigurationPackage
@Import(AutoConfigurationImportSelector.class)
public @interface EnableAutoConfiguration {}

@AutoConfigurationPackage

@Import(AutoConfigurationPackages.Registrar.class)
public @interface AutoConfigurationPackage {}

Registrar.class

image-20230802160618158

利用Registrar给容器中导入com.atguigu.boot(主程序所在的包)包下的一系列组件

image-20230802161004735

@Import(AutoConfigurationImportSelector.class)

1、利用getAutoConfigurationEntry(annotationMetadata);给容器中批量导入一些组件
2、调用List<String> configurations = getCandidateConfigurations(annotationMetadata, attributes)获取到所有需要导入到容器中的配置类
3、利用工厂加载 Map<String, List<String>> loadSpringFactories(@Nullable ClassLoader classLoader);得到所有的组件
4、从META-INF/spring.factories位置来加载一个文件。
默认扫描我们当前系统里面所有META-INF/spring.factories位置的文件
spring-boot-autoconfigure-2.3.4.RELEASE.jar包里面也有META-INF/spring.factories

AutoConfigurationImportSelector.java

@Override
public String[] selectImports(AnnotationMetadata annotationMetadata) {
if (!isEnabled(annotationMetadata)) {
return NO_IMPORTS;
}
AutoConfigurationEntry autoConfigurationEntry = getAutoConfigurationEntry(annotationMetadata);
return StringUtils.toStringArray(autoConfigurationEntry.getConfigurations());
}

getAutoConfigurationEntry()方法

利用getAutoConfigurationEntry(annotationMetadata);给容器中批量导入一些组件

protected AutoConfigurationEntry getAutoConfigurationEntry(AnnotationMetadata annotationMetadata) {
if (!isEnabled(annotationMetadata)) {
return EMPTY_ENTRY;
}
AnnotationAttributes attributes = getAttributes(annotationMetadata);
List<String> configurations = getCandidateConfigurations(annotationMetadata, attributes);
configurations = removeDuplicates(configurations);
Set<String> exclusions = getExclusions(annotationMetadata, attributes);
checkExcludedClasses(configurations, exclusions);
configurations.removeAll(exclusions);
configurations = getConfigurationClassFilter().filter(configurations);
fireAutoConfigurationImportEvents(configurations, exclusions);
return new AutoConfigurationEntry(configurations, exclusions);
}

**getCandidateConfigurations()**获取所有候选的配置

调用List configurations = getCandidateConfigurations(annotationMetadata, attributes)获取到所有需要导入到容器中的配置类

image-20230802162236987

loadSpringFactories()

利用工厂加载 Map<String, List> loadSpringFactories(@Nullable ClassLoader classLoader);得到所有的组件

private static Map<String, List<String>> loadSpringFactories(@Nullable ClassLoader classLoader) {
MultiValueMap<String, String> result = (MultiValueMap)cache.get(classLoader);
if (result != null) {
return result;
} else {
try {
Enumeration<URL> urls = classLoader != null ? classLoader.getResources("META-INF/spring.factories") : ClassLoader.getSystemResources("META-INF/spring.factories");
MultiValueMap<String, String> result = new LinkedMultiValueMap();

while(urls.hasMoreElements()) {
URL url = (URL)urls.nextElement();
UrlResource resource = new UrlResource(url);
Properties properties = PropertiesLoaderUtils.loadProperties(resource);
Iterator var6 = properties.entrySet().iterator();

while(var6.hasNext()) {
Map.Entry<?, ?> entry = (Map.Entry)var6.next();
String factoryTypeName = ((String)entry.getKey()).trim();
String[] var9 = StringUtils.commaDelimitedListToStringArray((String)entry.getValue());
int var10 = var9.length;

for(int var11 = 0; var11 < var10; ++var11) {
String factoryImplementationName = var9[var11];
result.add(factoryTypeName, factoryImplementationName.trim());
}
}
}

cache.put(classLoader, result);
return result;
} catch (IOException var13) {
throw new IllegalArgumentException("Unable to load factories from location [META-INF/spring.factories]", var13);
}
}
}

从META-INF/spring.factories位置来加载一个文件。默认扫描我们当前系统里面所有META-INF/spring.factories位置的文件,spring-boot-autoconfigure-2.3.4.RELEASE.jar包里面也有META-INF/spring.factories

image-20230802163429366

在spring.factories这个配置文件中写死了springBoot一启动就要给容器加载的所有配置类,一共127个

image-20230802163708198

# Initializers
org.springframework.context.ApplicationContextInitializer=\
org.springframework.boot.autoconfigure.SharedMetadataReaderFactoryContextInitializer,\
org.springframework.boot.autoconfigure.logging.ConditionEvaluationReportLoggingListener

# Application Listeners
org.springframework.context.ApplicationListener=\
org.springframework.boot.autoconfigure.BackgroundPreinitializer

# Auto Configuration Import Listeners
org.springframework.boot.autoconfigure.AutoConfigurationImportListener=\
org.springframework.boot.autoconfigure.condition.ConditionEvaluationReportAutoConfigurationImportListener

# Auto Configuration Import Filters
org.springframework.boot.autoconfigure.AutoConfigurationImportFilter=\
org.springframework.boot.autoconfigure.condition.OnBeanCondition,\
org.springframework.boot.autoconfigure.condition.OnClassCondition,\
org.springframework.boot.autoconfigure.condition.OnWebApplicationCondition

# Auto Configure
org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
org.springframework.boot.autoconfigure.admin.SpringApplicationAdminJmxAutoConfiguration,\
org.springframework.boot.autoconfigure.aop.AopAutoConfiguration,\
org.springframework.boot.autoconfigure.amqp.RabbitAutoConfiguration,\
org.springframework.boot.autoconfigure.batch.BatchAutoConfiguration,\
org.springframework.boot.autoconfigure.cache.CacheAutoConfiguration,\
org.springframework.boot.autoconfigure.cassandra.CassandraAutoConfiguration,\
org.springframework.boot.autoconfigure.context.ConfigurationPropertiesAutoConfiguration,\
org.springframework.boot.autoconfigure.context.LifecycleAutoConfiguration,\
org.springframework.boot.autoconfigure.context.MessageSourceAutoConfiguration,\
org.springframework.boot.autoconfigure.context.PropertyPlaceholderAutoConfiguration,\
org.springframework.boot.autoconfigure.couchbase.CouchbaseAutoConfiguration,\
org.springframework.boot.autoconfigure.dao.PersistenceExceptionTranslationAutoConfiguration,\
org.springframework.boot.autoconfigure.data.cassandra.CassandraDataAutoConfiguration,\
org.springframework.boot.autoconfigure.data.cassandra.CassandraReactiveDataAutoConfiguration,\
org.springframework.boot.autoconfigure.data.cassandra.CassandraReactiveRepositoriesAutoConfiguration,\
org.springframework.boot.autoconfigure.data.cassandra.CassandraRepositoriesAutoConfiguration,\
org.springframework.boot.autoconfigure.data.couchbase.CouchbaseDataAutoConfiguration,\
org.springframework.boot.autoconfigure.data.couchbase.CouchbaseReactiveDataAutoConfiguration,\
org.springframework.boot.autoconfigure.data.couchbase.CouchbaseReactiveRepositoriesAutoConfiguration,\
org.springframework.boot.autoconfigure.data.couchbase.CouchbaseRepositoriesAutoConfiguration,\
org.springframework.boot.autoconfigure.data.elasticsearch.ElasticsearchDataAutoConfiguration,\
org.springframework.boot.autoconfigure.data.elasticsearch.ElasticsearchRepositoriesAutoConfiguration,\
org.springframework.boot.autoconfigure.data.elasticsearch.ReactiveElasticsearchRepositoriesAutoConfiguration,\
org.springframework.boot.autoconfigure.data.elasticsearch.ReactiveElasticsearchRestClientAutoConfiguration,\
org.springframework.boot.autoconfigure.data.jdbc.JdbcRepositoriesAutoConfiguration,\
org.springframework.boot.autoconfigure.data.jpa.JpaRepositoriesAutoConfiguration,\
org.springframework.boot.autoconfigure.data.ldap.LdapRepositoriesAutoConfiguration,\
org.springframework.boot.autoconfigure.data.mongo.MongoDataAutoConfiguration,\
org.springframework.boot.autoconfigure.data.mongo.MongoReactiveDataAutoConfiguration,\
org.springframework.boot.autoconfigure.data.mongo.MongoReactiveRepositoriesAutoConfiguration,\
org.springframework.boot.autoconfigure.data.mongo.MongoRepositoriesAutoConfiguration,\
org.springframework.boot.autoconfigure.data.neo4j.Neo4jDataAutoConfiguration,\
org.springframework.boot.autoconfigure.data.neo4j.Neo4jRepositoriesAutoConfiguration,\
org.springframework.boot.autoconfigure.data.solr.SolrRepositoriesAutoConfiguration,\
org.springframework.boot.autoconfigure.data.r2dbc.R2dbcDataAutoConfiguration,\
org.springframework.boot.autoconfigure.data.r2dbc.R2dbcRepositoriesAutoConfiguration,\
org.springframework.boot.autoconfigure.data.r2dbc.R2dbcTransactionManagerAutoConfiguration,\
org.springframework.boot.autoconfigure.data.redis.RedisAutoConfiguration,\
org.springframework.boot.autoconfigure.data.redis.RedisReactiveAutoConfiguration,\
org.springframework.boot.autoconfigure.data.redis.RedisRepositoriesAutoConfiguration,\
org.springframework.boot.autoconfigure.data.rest.RepositoryRestMvcAutoConfiguration,\
org.springframework.boot.autoconfigure.data.web.SpringDataWebAutoConfiguration,\
org.springframework.boot.autoconfigure.elasticsearch.ElasticsearchRestClientAutoConfiguration,\
org.springframework.boot.autoconfigure.flyway.FlywayAutoConfiguration,\
org.springframework.boot.autoconfigure.freemarker.FreeMarkerAutoConfiguration,\
org.springframework.boot.autoconfigure.groovy.template.GroovyTemplateAutoConfiguration,\
org.springframework.boot.autoconfigure.gson.GsonAutoConfiguration,\
org.springframework.boot.autoconfigure.h2.H2ConsoleAutoConfiguration,\
org.springframework.boot.autoconfigure.hateoas.HypermediaAutoConfiguration,\
org.springframework.boot.autoconfigure.hazelcast.HazelcastAutoConfiguration,\
org.springframework.boot.autoconfigure.hazelcast.HazelcastJpaDependencyAutoConfiguration,\
org.springframework.boot.autoconfigure.http.HttpMessageConvertersAutoConfiguration,\
org.springframework.boot.autoconfigure.http.codec.CodecsAutoConfiguration,\
org.springframework.boot.autoconfigure.influx.InfluxDbAutoConfiguration,\
org.springframework.boot.autoconfigure.info.ProjectInfoAutoConfiguration,\
org.springframework.boot.autoconfigure.integration.IntegrationAutoConfiguration,\
org.springframework.boot.autoconfigure.jackson.JacksonAutoConfiguration,\
org.springframework.boot.autoconfigure.jdbc.DataSourceAutoConfiguration,\
org.springframework.boot.autoconfigure.jdbc.JdbcTemplateAutoConfiguration,\
org.springframework.boot.autoconfigure.jdbc.JndiDataSourceAutoConfiguration,\
org.springframework.boot.autoconfigure.jdbc.XADataSourceAutoConfiguration,\
org.springframework.boot.autoconfigure.jdbc.DataSourceTransactionManagerAutoConfiguration,\
org.springframework.boot.autoconfigure.jms.JmsAutoConfiguration,\
org.springframework.boot.autoconfigure.jmx.JmxAutoConfiguration,\
org.springframework.boot.autoconfigure.jms.JndiConnectionFactoryAutoConfiguration,\
org.springframework.boot.autoconfigure.jms.activemq.ActiveMQAutoConfiguration,\
org.springframework.boot.autoconfigure.jms.artemis.ArtemisAutoConfiguration,\
org.springframework.boot.autoconfigure.jersey.JerseyAutoConfiguration,\
org.springframework.boot.autoconfigure.jooq.JooqAutoConfiguration,\
org.springframework.boot.autoconfigure.jsonb.JsonbAutoConfiguration,\
org.springframework.boot.autoconfigure.kafka.KafkaAutoConfiguration,\
org.springframework.boot.autoconfigure.availability.ApplicationAvailabilityAutoConfiguration,\
org.springframework.boot.autoconfigure.ldap.embedded.EmbeddedLdapAutoConfiguration,\
org.springframework.boot.autoconfigure.ldap.LdapAutoConfiguration,\
org.springframework.boot.autoconfigure.liquibase.LiquibaseAutoConfiguration,\
org.springframework.boot.autoconfigure.mail.MailSenderAutoConfiguration,\
org.springframework.boot.autoconfigure.mail.MailSenderValidatorAutoConfiguration,\
org.springframework.boot.autoconfigure.mongo.embedded.EmbeddedMongoAutoConfiguration,\
org.springframework.boot.autoconfigure.mongo.MongoAutoConfiguration,\
org.springframework.boot.autoconfigure.mongo.MongoReactiveAutoConfiguration,\
org.springframework.boot.autoconfigure.mustache.MustacheAutoConfiguration,\
org.springframework.boot.autoconfigure.orm.jpa.HibernateJpaAutoConfiguration,\
org.springframework.boot.autoconfigure.quartz.QuartzAutoConfiguration,\
org.springframework.boot.autoconfigure.r2dbc.R2dbcAutoConfiguration,\
org.springframework.boot.autoconfigure.rsocket.RSocketMessagingAutoConfiguration,\
org.springframework.boot.autoconfigure.rsocket.RSocketRequesterAutoConfiguration,\
org.springframework.boot.autoconfigure.rsocket.RSocketServerAutoConfiguration,\
org.springframework.boot.autoconfigure.rsocket.RSocketStrategiesAutoConfiguration,\
org.springframework.boot.autoconfigure.security.servlet.SecurityAutoConfiguration,\
org.springframework.boot.autoconfigure.security.servlet.UserDetailsServiceAutoConfiguration,\
org.springframework.boot.autoconfigure.security.servlet.SecurityFilterAutoConfiguration,\
org.springframework.boot.autoconfigure.security.reactive.ReactiveSecurityAutoConfiguration,\
org.springframework.boot.autoconfigure.security.reactive.ReactiveUserDetailsServiceAutoConfiguration,\
org.springframework.boot.autoconfigure.security.rsocket.RSocketSecurityAutoConfiguration,\
org.springframework.boot.autoconfigure.security.saml2.Saml2RelyingPartyAutoConfiguration,\
org.springframework.boot.autoconfigure.sendgrid.SendGridAutoConfiguration,\
org.springframework.boot.autoconfigure.session.SessionAutoConfiguration,\
org.springframework.boot.autoconfigure.security.oauth2.client.servlet.OAuth2ClientAutoConfiguration,\
org.springframework.boot.autoconfigure.security.oauth2.client.reactive.ReactiveOAuth2ClientAutoConfiguration,\
org.springframework.boot.autoconfigure.security.oauth2.resource.servlet.OAuth2ResourceServerAutoConfiguration,\
org.springframework.boot.autoconfigure.security.oauth2.resource.reactive.ReactiveOAuth2ResourceServerAutoConfiguration,\
org.springframework.boot.autoconfigure.solr.SolrAutoConfiguration,\
org.springframework.boot.autoconfigure.task.TaskExecutionAutoConfiguration,\
org.springframework.boot.autoconfigure.task.TaskSchedulingAutoConfiguration,\
org.springframework.boot.autoconfigure.thymeleaf.ThymeleafAutoConfiguration,\
org.springframework.boot.autoconfigure.transaction.TransactionAutoConfiguration,\
org.springframework.boot.autoconfigure.transaction.jta.JtaAutoConfiguration,\
org.springframework.boot.autoconfigure.validation.ValidationAutoConfiguration,\
org.springframework.boot.autoconfigure.web.client.RestTemplateAutoConfiguration,\
org.springframework.boot.autoconfigure.web.embedded.EmbeddedWebServerFactoryCustomizerAutoConfiguration,\
org.springframework.boot.autoconfigure.web.reactive.HttpHandlerAutoConfiguration,\
org.springframework.boot.autoconfigure.web.reactive.ReactiveWebServerFactoryAutoConfiguration,\
org.springframework.boot.autoconfigure.web.reactive.WebFluxAutoConfiguration,\
org.springframework.boot.autoconfigure.web.reactive.error.ErrorWebFluxAutoConfiguration,\
org.springframework.boot.autoconfigure.web.reactive.function.client.ClientHttpConnectorAutoConfiguration,\
org.springframework.boot.autoconfigure.web.reactive.function.client.WebClientAutoConfiguration,\
org.springframework.boot.autoconfigure.web.servlet.DispatcherServletAutoConfiguration,\
org.springframework.boot.autoconfigure.web.servlet.ServletWebServerFactoryAutoConfiguration,\
org.springframework.boot.autoconfigure.web.servlet.error.ErrorMvcAutoConfiguration,\
org.springframework.boot.autoconfigure.web.servlet.HttpEncodingAutoConfiguration,\
org.springframework.boot.autoconfigure.web.servlet.MultipartAutoConfiguration,\
org.springframework.boot.autoconfigure.web.servlet.WebMvcAutoConfiguration,\
org.springframework.boot.autoconfigure.websocket.reactive.WebSocketReactiveAutoConfiguration,\
org.springframework.boot.autoconfigure.websocket.servlet.WebSocketServletAutoConfiguration,\
org.springframework.boot.autoconfigure.websocket.servlet.WebSocketMessagingAutoConfiguration,\
org.springframework.boot.autoconfigure.webservices.WebServicesAutoConfiguration,\
org.springframework.boot.autoconfigure.webservices.client.WebServiceTemplateAutoConfiguration

# Failure analyzers
org.springframework.boot.diagnostics.FailureAnalyzer=\
org.springframework.boot.autoconfigure.data.redis.RedisUrlSyntaxFailureAnalyzer,\
org.springframework.boot.autoconfigure.diagnostics.analyzer.NoSuchBeanDefinitionFailureAnalyzer,\
org.springframework.boot.autoconfigure.flyway.FlywayMigrationScriptMissingFailureAnalyzer,\
org.springframework.boot.autoconfigure.jdbc.DataSourceBeanCreationFailureAnalyzer,\
org.springframework.boot.autoconfigure.jdbc.HikariDriverConfigurationFailureAnalyzer,\
org.springframework.boot.autoconfigure.r2dbc.ConnectionFactoryBeanCreationFailureAnalyzer,\
org.springframework.boot.autoconfigure.session.NonUniqueSessionRepositoryFailureAnalyzer

# Template availability providers
org.springframework.boot.autoconfigure.template.TemplateAvailabilityProvider=\
org.springframework.boot.autoconfigure.freemarker.FreeMarkerTemplateAvailabilityProvider,\
org.springframework.boot.autoconfigure.mustache.MustacheTemplateAvailabilityProvider,\
org.springframework.boot.autoconfigure.groovy.template.GroovyTemplateAvailabilityProvider,\
org.springframework.boot.autoconfigure.thymeleaf.ThymeleafTemplateAvailabilityProvider,\
org.springframework.boot.autoconfigure.web.servlet.JspTemplateAvailabilityProvider

2.按需开启自动配置项

​ 虽然我们127个场景的所有自动配置启动的时候默认全部加载。xxxxAutoConfiguration,按照条件装配规则(@Conditional),最终会按需配置,比如下面的配置是否生效还要通过@Conditional的条件判断。

简单的一句话:启动的时候加载所有,使用的时候按照条件(@Conditional)进行装配

image-20230802164424689

3.修改默认配置

​ 相当于当我们在容器中注入了类型为MultipartResolver但是id不为multipartResolver的组件的时候,会帮我们规范一下命名,命名为multipartResolver。

@Bean
@ConditionalOnBean(MultipartResolver.class) //存在MultipartResolver类型的组件
@ConditionalOnMissingBean(name = DispatcherServlet.MULTIPART_RESOLVER_BEAN_NAME)//不存在以multipartResolver为id的组件
public MultipartResolver multipartResolver(MultipartResolver resolver) {
// Detect if the user has created a MultipartResolver but named it incorrectly
return resolver;
}

SpringBoot默认会在底层配好所有的组件。但是如果用户自己配置了以用户的优先

@Bean
@ConditionalOnMissingBean //没有的话就将这个配置注入到容器中
public CharacterEncodingFilter characterEncodingFilter() {
CharacterEncodingFilter filter = new OrderedCharacterEncodingFilter();
filter.setEncoding(this.properties.getCharset().name());
filter.setForceRequestEncoding(this.properties.shouldForce(Encoding.Type.REQUEST));
filter.setForceResponseEncoding(this.properties.shouldForce(Encoding.Type.RESPONSE));
return filter;
}

总结:

  • SpringBoot先加载所有的自动配置类 xxxxxAutoConfiguration

  • 每个自动配置类按照条件进行生效,默认都会绑定配置文件指定的值。xxxxProperties里面拿。xxxProperties和配置文件进行了绑定

  • 生效的配置类就会给容器中装配很多组件

  • 只要容器中有这些组件,相当于这些功能就有了

  • 定制化配置

    • 用户直接自己@Bean替换底层的组件
    • 用户去看这个组件是获取的配置文件什么值就去修改。

xxxxxAutoConfiguration —> 组件 —> xxxxProperties取值 —-> application.properties

3.4.最佳实践

4.5 简化开发

1.Lombok

引入依赖(使用前需要安装插件)

<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
</dependency>

注解

@NoArgsConstructor  //无参构造器
@AllArgsConstructor //有参构造器
@Data //getter和setter方法
@ToString
@EqualsAndHashCode

2.简化日志开发

@Slf4j

3.dev-tools

<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-devtools</artifactId>
<optional>true</optional>
</dependency>

项目或者页面修改以后:Ctrl+F9;

4.Spring Initailizr(项目初始化向导)

image-20230803103657995

image-20230803104032505

image-20230803104655578

二.SpringBoot2核心技术-核心功能

image-20230803104919434

一.配置文件

1、文件类型

1.1、properties

同以前的properties用法

1.2、yaml

1.2.1、简介

YAML 是 “YAML Ain’t Markup Language”(YAML 不是一种标记语言)的递归缩写。在开发的这种语言时,YAML 的意思其实是:”Yet Another Markup Language”(仍是一种标记语言)。

非常适合用来做以数据为中心的配置文件

1.2.2、基本语法
  • key: value;kv之间有空格
  • 大小写敏感
  • 使用缩进表示层级关系
  • 缩进不允许使用tab,只允许空格
  • 缩进的空格数不重要,只要相同层级的元素左对齐即可
  • ‘#’表示注释
  • 字符串无需加引号,如果要加,’’与””表示字符串内容 会被 转义/不转义
1.2.3、数据类型
  • 字面量:单个的、不可再分的值。date、boolean、string、number、null
k: v
  • 对象:键值对的集合。map、hash、set、object
行内写法:  k: {k1:v1,k2:v2,k3:v3}
#或
k:
k1: v1
k2: v2
k3: v3
  • 数组:一组按次序排列的值。array、list、queue
行内写法:  k: [v1,v2,v3]
#或者
k:
- v1
- v2
- v3
1.2.4、示例
@ConfigurationProperties(prefix="person")//和以person开头的配置文件绑定
@Data
public class Person {

private String userName;
private Boolean boss;
private Date birth;
private Integer age;
private Pet pet;
private String[] interests;
private List<String> animal;
private Map<String, Object> score;
private Set<Double> salarys;
private Map<String, List<Pet>> allPets;
}

@Data
public class Pet {
private String name;
private Double weight;
}
# yaml表示以上对象
person:
# 单引号会将 \n作为字符串输出 双引号会将\n 作为换行输出
# 双引号不会转义,单引号会转义
userName: zhangsan
boss: false
birth: 2019/12/12 20:12:33
age: 18
pet:
name: tomcat
weight: 23.4
interests: [篮球,游泳]
animal:
- jerry
- mario
score:
english:
first: 30
second: 40
third: 50
math: [131,140,148]
chinese: {first: 128,second: 136}
salarys: [3999,4999.98,5999.99]
allPets:
sick:
- {name: tom}
- {name: jerry,weight: 47}
health: [{name: mario,weight: 47}]

2、配置提示

自定义的类和配置文件绑定一般没有提示

<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-configuration-processor</artifactId>
<optional>true</optional>
</dependency>

<!--打包的时候去除这个配置提示器-->
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
<configuration>
<excludes>
<exclude>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-configuration-processor</artifactId>
</exclude>
</excludes>
</configuration>
</plugin>
</plugins>
</build>

二.Web开发

image-20230803134143859

1.简单的功能分析

1.1 静态资源访问

1.静态资源目录

只要静态资源放在类路径下: /static (or /public or /resources or /META-INF/resources

访问 : 当前项目根路径/ + 静态资源名

原理: 静态映射/**

请求进来,先去找Controller看能不能处理,不能处理的所有请求又都交给静态资源处理器,静态资源也找不到则响应404页面

改变默认的静态资源路径

spring:
resources:
static-locations: [classpath:/haha/] #静态资源的目录
2.静态资源访问前缀

默认无前缀

默认的访问路径: localhost:8080/xxx.png

设置访问前缀之后: localhost:8080/res/xxx.png (文件在目录中的位置没有改变,只是访问的时候加上了res这一层路径)

spring:
mvc:
static-path-pattern: /res/**

当前项目 + static-path-pattern + 静态资源名 = 静态资源文件夹下找

3.webjar

自动映射 /webjars/**

https://www.webjars.org/

<dependency>
<groupId>org.webjars</groupId>
<artifactId>jquery</artifactId>
<version>3.5.1</version>
</dependency>

访问地址:http://localhost:8080/webjars/jquery/3.5.1/jquery.js 后面地址要按照依赖里面的包路径

1.2 欢迎页支持

  • 静态资源路径下 index.html

    • 可以配置静态资源路径
    • 但是不可以配置静态资源的访问前缀。否则导致 index.html不能被默认访问
spring:
# mvc:
# static-path-pattern: /res/** 这个会导致welcome page功能失效

resources:
static-locations: [classpath:/haha/]
  • controller能处理/index

1.3 自定义 Favicon

favicon.ico 放在静态资源目录下即可(名称为favicon.ico)

spring:
# mvc:
# static-path-pattern: /res/** 这个会导致 Favicon 功能失效

1.4 静态资源配置原理

1.SpringMvc的自动配置类WebMvcAutoConfiguration

image-20230803165113057

@Configuration(proxyBeanMethods = false)
@ConditionalOnWebApplication(type = Type.SERVLET)
@ConditionalOnClass({ Servlet.class, DispatcherServlet.class, WebMvcConfigurer.class })
@ConditionalOnMissingBean(WebMvcConfigurationSupport.class)
@AutoConfigureOrder(Ordered.HIGHEST_PRECEDENCE + 10)
@AutoConfigureAfter({ DispatcherServletAutoConfiguration.class, TaskExecutionAutoConfiguration.class,
ValidationAutoConfiguration.class })
public class WebMvcAutoConfiguration {}

2.SpringMvc给容器中配置的组件

image-20230803180257123

@Configuration(proxyBeanMethods = false)
@Import(EnableWebMvcConfiguration.class)
@EnableConfigurationProperties({ WebMvcProperties.class, ResourceProperties.class })
@Order(0)
public static class WebMvcAutoConfigurationAdapter implements WebMvcConfigurer {}

3.绑定的配置

@EnableConfigurationProperties({ WebMvcProperties.class, ResourceProperties.class })

WebMvcProperties.class

image-20230803180600438

ResourceProperties.class

image-20230803180633611

2.请求参数处理与数据响应

2.1、请求映射

1、rest使用与原理
  • @xxxMapping;

  • Rest风格支持(使用HTTP请求方式动词来表示对资源的操作

    • 以前:/getUser 获取用户 /deleteUser 删除用户 /editUser 修改用户 /saveUser 保存用户
    • 现在: /user GET-获取用户 DELETE-删除用户 PUT-修改用户 POST-保存用户
    • 核心Filter;HiddenHttpMethodFilter
      • 用法: 表单method=post,隐藏域 _method=put
      • SpringBoot中手动开启
      #开启使用rest风格的注解
      spring:
      mvc:
      hiddenmethod:
      filter:
      enabled: true
      <!--发送put或者delete请求-->
      <form action="/user" method="post">
      <input name="_method" type="hidden" value="delete"/>
      <input name="_m" type="hidden" value="delete"/>
      <input value="REST-DELETE 提交" type="submit"/>
      </form>
      <form action="/user" method="post">
      <input name="_method" type="hidden" value="PUT"/>
      <input value="REST-PUT 提交" type="submit"/>
      </form>
    • 扩展:如何把_method 这个名字换成我们自己喜欢的。
@RequestMapping(value = "/user",method = RequestMethod.GET)
public String getUser(){
return "GET-张三";
}

@RequestMapping(value = "/user",method = RequestMethod.POST)
public String saveUser(){
return "POST-张三";
}


@RequestMapping(value = "/user",method = RequestMethod.PUT)
public String putUser(){
return "PUT-张三";
}

@RequestMapping(value = "/user",method = RequestMethod.DELETE)
public String deleteUser(){
return "DELETE-张三";
}



//自定义filter
@Bean
public HiddenHttpMethodFilter hiddenHttpMethodFilter(){
HiddenHttpMethodFilter methodFilter = new HiddenHttpMethodFilter();
methodFilter.setMethodParam("_m");
return methodFilter;
}

Rest原理(表单提交要使用REST的时候)

  • 表单提交会带上_method=PUT

    <!--发送put或者delete请求-->
    <form action="/user" method="post">
    <input name="_method" type="hidden" value="delete"/>
    <input name="_m" type="hidden" value="delete"/>
    <input value="REST-DELETE 提交" type="submit"/>
    </form>
    <form action="/user" method="post">
    <input name="_method" type="hidden" value="PUT"/>
    <input value="REST-PUT 提交" type="submit"/>
    </form>
  • 请求过来被HiddenHttpMethodFilter拦截

    WebMvcAutoConfiguration

    @Bean
    @ConditionalOnMissingBean(HiddenHttpMethodFilter.class)
    @ConditionalOnProperty(prefix = "spring.mvc.hiddenmethod.filter", name = "enabled", matchIfMissing = false)
    public OrderedHiddenHttpMethodFilter hiddenHttpMethodFilter() {
    return new OrderedHiddenHttpMethodFilter();
    }
    • 请求是否正常,并且是POST
      • 获取到_method的值。

        @Override
        protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain)
        throws ServletException, IOException {

        HttpServletRequest requestToUse = request;

        if ("POST".equals(request.getMethod()) && request.getAttribute(WebUtils.ERROR_EXCEPTION_ATTRIBUTE) == null) {
        String paramValue = request.getParameter(this.methodParam);
        if (StringUtils.hasLength(paramValue)) {
        String method = paramValue.toUpperCase(Locale.ENGLISH);
        if (ALLOWED_METHODS.contains(method)) {
        requestToUse = new HttpMethodRequestWrapper(request, method);
        }
        }
        }

        filterChain.doFilter(requestToUse, response);
        }
      • 兼容以下请求;PUT.DELETE.PATCH

        private static final List<String> ALLOWED_METHODS =
        Collections.unmodifiableList(Arrays.asList(HttpMethod.PUT.name(),
        HttpMethod.DELETE.name(), HttpMethod.PATCH.name()));
      • 原生request(post),包装模式requesWrapper重写了getMethod方法,返回的是传入的值

      • 过滤器链放行的时候用wrapper,以后的方法调用getMethod是调用requesWrapper的

Tips:

Rest使用客户端工具:如PostMan直接发送put、delete等方式请求,无需Filter,不需要转换

扩展点:

如何将<input name="_method" type="hidden" value="delete"/>中的name值_method改为自定义的值?

@Configuration(proxyBeanMethods = false) //组件中没有依赖关系,设置成false直接放
public class WebConfig{
@Bean
public HiddenHttpMethodFilter hiddenHttpMethodFilter(){
HiddenHttpMethodFilter methodFilter = new HiddenHttpMethodFilter();
methodFilter.setMethodParam("_m"); //将name="_method"改成name="_m"
return methodFilter;
}
}
2、请求映射原理

DispatcherServlet 继承 FrameworkServlet

FrameworkServlet 继承 HttpServletBean

FrameworkServlet 重写了doGet和doPost方法

image-20230809133846464

DispatcherServlet的doDispatch方法

protected void doDispatch(HttpServletRequest request, HttpServletResponse response) throws Exception {
HttpServletRequest processedRequest = request;
HandlerExecutionChain mappedHandler = null;
boolean multipartRequestParsed = false;

WebAsyncManager asyncManager = WebAsyncUtils.getAsyncManager(request);

try {
ModelAndView mv = null;
Exception dispatchException = null;

try {
processedRequest = checkMultipart(request);
multipartRequestParsed = (processedRequest != request);

// Determine handler for the current request.
//找到请求使用那个hander(contrller)处理
/**
* getHandler()方法
*/
mappedHandler = getHandler(processedRequest);
if (mappedHandler == null) {
noHandlerFound(processedRequest, response);
return;
}

// Determine handler adapter for the current request.
HandlerAdapter ha = getHandlerAdapter(mappedHandler.getHandler());

// Process last-modified header, if supported by the handler.
String method = request.getMethod();
boolean isGet = "GET".equals(method);
if (isGet || "HEAD".equals(method)) {
long lastModified = ha.getLastModified(request, mappedHandler.getHandler());
if (new ServletWebRequest(request, response).checkNotModified(lastModified) && isGet) {
return;
}
}

if (!mappedHandler.applyPreHandle(processedRequest, response)) {
return;
}

// Actually invoke the handler.
mv = ha.handle(processedRequest, response, mappedHandler.getHandler());

if (asyncManager.isConcurrentHandlingStarted()) {
return;
}

applyDefaultViewName(processedRequest, mv);
mappedHandler.applyPostHandle(processedRequest, response, mv);
}
catch (Exception ex) {
dispatchException = ex;
}
catch (Throwable err) {
// As of 4.3, we're processing Errors thrown from handler methods as well,
// making them available for @ExceptionHandler methods and other scenarios.
dispatchException = new NestedServletException("Handler dispatch failed", err);
}
processDispatchResult(processedRequest, response, mappedHandler, mv, dispatchException);
}
catch (Exception ex) {
triggerAfterCompletion(processedRequest, response, mappedHandler, ex);
}
catch (Throwable err) {
triggerAfterCompletion(processedRequest, response, mappedHandler,
new NestedServletException("Handler processing failed", err));
}
finally {
if (asyncManager.isConcurrentHandlingStarted()) {
// Instead of postHandle and afterCompletion
if (mappedHandler != null) {
mappedHandler.applyAfterConcurrentHandlingStarted(processedRequest, response);
}
}
else {
// Clean up any resources used by a multipart request.
if (multipartRequestParsed) {
cleanupMultipart(processedRequest);
}
}
}
}

getHandler()方法 遍历查找可以处理请求的方法

@Nullable
protected HandlerExecutionChain getHandler(HttpServletRequest request) throws Exception {
if (this.handlerMappings != null) {
for (HandlerMapping mapping : this.handlerMappings) {
HandlerExecutionChain handler = mapping.getHandler(request);
if (handler != null) {
return handler;
}
}
}
return null;
}

**HandlerMapping ** 处理器映射

默认有5个HandlerMapping

image-20230809150915450

RequestMappingHandlerMapping:保存了所有@RequestMapping 和handler的映射规则

image-20230809151427867

所有的请求映射都在HandlerMapping中

  • SpringBoot自动配置欢迎页的 WelcomePageHandlerMapping 访问 /能访问到index.html;

  • SpringBoot自动配置了默认的 RequestMappingHandlerMapping

  • 请求进来,挨个尝试所有的HandlerMapping看是否有请求信息

    • 如果有就找到这个请求对应的handler
    • 如果没有就是下一个 HandlerMapping
  • 我们需要一些自定义的映射处理,我们也可以自己给容器中放HandlerMapping 自定义 HandlerMapping

2.2、普通参数与基本注解

1.注解
@PathVariable  //获取路径中的参数信息(restful风格)
@RequestHeader //获取请求头的信息
@RequestAttribute //获取请求域中的数据
@RequestParam //获取路径中拼接的请求参数(/info?age=12&name=lisi)
@MatrixVariable //矩阵变量 /cars/sell;low=34;brand=byd,audi,yd
@CookieValue //获取cookie的值
@RequestBody //获取请求体的值(Post请求)

@PathVariable

接受路径中的参数也可以使用map集合接受,但是map集合必须是Map<String, String>的形式

image-20230809155753715

@RequestHeader

获取指定的请求头或者所有的请求头信息

image-20230809160406516

image-20230809160846527

@RequestParam

可以使用map接受所有的请求参数

image-20230809161533834

@CookieValue

获取cookie的值,可以直接获取字符串的值也可以封装成cookie的对象

image-20230809164101374

@RequestBody

这里用一个字符串接收表单数据和平时使用的不一样,开发中使用一个对象接受表单的数据

/**
* ·@RequestBody注解
*/
@PostMapping("/save")
public Map postMethod(@RequestBody String content){
Map<String,Object> map = new HashMap<>();
map.put("content",content);
return map;
}

image-20230809165149278

@RequestAttribute

package com.atguigu.boot.controller;

import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestAttribute;
import org.springframework.web.bind.annotation.ResponseBody;

import javax.servlet.http.HttpServletRequest;
import java.util.HashMap;
import java.util.Map;

/**
* @author Jason Gong
* @version 1.0
* @website https://jasonsgong.gitee.io
* @Date 2023/8/11
* @Description
*/
@Controller
public class RequestController {


@GetMapping("/goto")
public String goToPage(HttpServletRequest request){

request.setAttribute("msg","获取请求域中的数据成功");
request.setAttribute("code",200);
return "forward:/success"; //转发到下面的/success请求
}

@ResponseBody
@GetMapping("/success")
public Map success(@RequestAttribute(value = "msg",required = false) String msg,
@RequestAttribute(value = "code",required = false)Integer code,//通过注解的方式获取上面请求域的值
HttpServletRequest request){//通过原生的request获取
//Object msg1 = request.getAttribute("msg");
Map<String,Object> map = new HashMap<>();
map.put("msg",msg);
map.put("code",code);
return map;
}
}

image-20230811092618793

@MatrixVariable

开启SpringBoot的矩阵变量功能

@Bean
public WebMvcConfigurer webMvcConfigurer(){
return new WebMvcConfigurer() {
@Override
public void configurePathMatch(PathMatchConfigurer configurer) {
UrlPathHelper urlPathHelper = new UrlPathHelper();
// 不移除 后面的内容 矩阵变量功能就可以生效
urlPathHelper.setRemoveSemicolonContent(false);
configurer.setUrlPathHelper(urlPathHelper);
}
};
}

测试

//1、语法: 请求路径:/cars/sell;low=34;brand=byd,audi,yd
//2、SpringBoot默认是禁用了矩阵变量的功能
// 手动开启:原理。对于路径的处理。UrlPathHelper进行解析。
// removeSemicolonContent(移除分号内容)支持矩阵变量的
//3、矩阵变量必须有url路径变量才能被解析
@GetMapping("/cars/{path}")
public Map carsSell(@MatrixVariable("low") Integer low,
@MatrixVariable("brand") List<String> brand,
@PathVariable("path") String path){
Map<String,Object> map = new HashMap<>();

map.put("low",low);
map.put("brand",brand);
map.put("path",path);
return map;
}

// /boss/1;age=20/2;age=10
@GetMapping("/boss/{bossId}/{empId}")
public Map boss(@MatrixVariable(value = "age",pathVar = "bossId") Integer bossAge,
@MatrixVariable(value = "age",pathVar = "empId") Integer empAge){
Map<String,Object> map = new HashMap<>();
map.put("bossAge",bossAge);
map.put("empAge",empAge);
return map;
}

image-20230811095858532

image-20230811095834803

2.Servlet API

WebRequest、ServletRequest、MultipartRequest、 HttpSession、javax.servlet.http.PushBuilder、Principal、InputStream、Reader、HttpMethod、Locale、TimeZone、ZoneId

ServletRequestMethodArgumentResolver 以上的部分参数

@Override
public boolean supportsParameter(MethodParameter parameter) {
Class<?> paramType = parameter.getParameterType();
return (WebRequest.class.isAssignableFrom(paramType) ||
ServletRequest.class.isAssignableFrom(paramType) ||
MultipartRequest.class.isAssignableFrom(paramType) ||
HttpSession.class.isAssignableFrom(paramType) ||
(pushBuilder != null && pushBuilder.isAssignableFrom(paramType)) ||
Principal.class.isAssignableFrom(paramType) ||
InputStream.class.isAssignableFrom(paramType) ||
Reader.class.isAssignableFrom(paramType) ||
HttpMethod.class == paramType ||
Locale.class == paramType ||
TimeZone.class == paramType ||
ZoneId.class == paramType);
}
3.复杂参数:

Map、Model(map、model里面的数据会被放在request的请求域 request.setAttribute)、Errors/BindingResult、RedirectAttributes( 重定向携带数据)、ServletResponse(response)、SessionStatus、UriComponentsBuilder、ServletUriComponentsBuilder

向请求域中共享数据

/**
* ra数据通过 map model HttpServletRequest HttpServletResponse 等方式共享到请求域中,在success页面中取出来
*/
@GetMapping("/params")
public String testParam(Map<String,Object> map,
Model model,
HttpServletRequest request,
HttpServletResponse response){
map.put("hello","world666");
model.addAttribute("world","hello666");
request.setAttribute("message","HelloWorld");

Cookie cookie = new Cookie("c1","v1");
response.addCookie(cookie);
return "forward:/success";
}
4.自定义对象参数
/**
* 姓名: <input name="userName"/> <br/>
* 年龄: <input name="age"/> <br/>
* 生日: <input name="birth"/> <br/>
* 宠物姓名:<input name="pet.name"/><br/>
* 宠物年龄:<input name="pet.age"/>
*/
@Data
public class Person {

private String userName;
private Integer age;
private Date birth;
private Pet pet;

}

@Data
public class Pet {

private String name;
private String age;

}

页面测试代码

index.html

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Title</title>
</head>
<body>
<h1>atguigu,欢迎您</h1>
测试REST风格;
<form action="/user" method="get">
<input value="REST-GET 提交" type="submit"/>
</form>
<form action="/user" method="post">
<input value="REST-POST 提交" type="submit"/>
</form>
<form action="/user" method="post">
<input name="_method" type="hidden" value="delete"/>
<input name="_m" type="hidden" value="delete"/>
<input value="REST-DELETE 提交" type="submit"/>
</form>
<form action="/user" method="post">
<input name="_method" type="hidden" value="PUT"/>
<input value="REST-PUT 提交" type="submit"/>
</form>
<hr/>
测试基本注解:
<ul>
<a href="car/3/owner/lisi?age=18&inters=basketball&inters=game">car/{id}/owner/{username}</a>
<li>@PathVariable(路径变量)</li>
<li>@RequestHeader(获取请求头)</li>
<li>@RequestParam(获取请求参数)</li>
<li>@CookieValue(获取cookie值)</li>
<li>@RequestBody(获取请求体[POST])</li>

<li>@RequestAttribute(获取request域属性)</li>
<li>@MatrixVariable(矩阵变量)</li>
</ul>

/cars/{path}?xxx=xxx&aaa=ccc queryString 查询字符串。@RequestParam;<br/>
/cars/sell;low=34;brand=byd,audi,yd ;矩阵变量 <br/>
页面开发,cookie禁用了,session里面的内容怎么使用;
session.set(a,b)---> jsessionid ---> cookie ----> 每次发请求携带。
url重写:/abc;jsesssionid=xxxx 把cookie的值使用矩阵变量的方式进行传递.

/boss/1/2

/boss/1;age=20/2;age=20

<a href="/cars/sell;low=34;brand=byd,audi,yd">@MatrixVariable(矩阵变量)</a>
<a href="/cars/sell;low=34;brand=byd;brand=audi;brand=yd">@MatrixVariable(矩阵变量)</a>
<a href="/boss/1;age=20/2;age=10">@MatrixVariable(矩阵变量)/boss/{bossId}/{empId}</a>
<br/>
<form action="/save" method="post">
测试@RequestBody获取数据 <br/>
用户名:<input name="userName"/> <br>
邮箱:<input name="email"/>
<input type="submit" value="提交"/>
</form>
<ol>
<li>矩阵变量需要在SpringBoot中手动开启</li>
<li>根据RFC3986的规范,矩阵变量应当绑定在路径变量中!</li>
<li>若是有多个矩阵变量,应当使用英文符号;进行分隔。</li>
<li>若是一个矩阵变量有多个值,应当使用英文符号,进行分隔,或之命名多个重复的key即可。</li>
<li>如:/cars/sell;low=34;brand=byd,audi,yd</li>
</ol>
<hr/>
测试原生API:
<a href="/testapi">测试原生API</a>
<hr/>
测试复杂类型:<hr/>
测试封装POJO;
<form action="/saveuser" method="post">
姓名: <input name="userName" value="zhangsan"/> <br/>
年龄: <input name="age" value="18"/> <br/>
生日: <input name="birth" value="2019/12/10"/> <br/>
<!-- 宠物姓名:<input name="pet.name" value="阿猫"/><br/>-->
<!-- 宠物年龄:<input name="pet.age" value="5"/>-->
宠物: <input name="pet" value="小猫"/>
<input type="submit" value="保存"/>
</form>
<br>
</body>
</html>

2.3 参数处理原理

1.页面发送请求

http://localhost:8080/car/3/owner/lisi  

2.进入DispatcherServlet的doDispatch( )方法

protected void doDispatch(HttpServletRequest request, HttpServletResponse response) throws Exception {
HttpServletRequest processedRequest = request;
HandlerExecutionChain mappedHandler = null;
boolean multipartRequestParsed = false;

WebAsyncManager asyncManager = WebAsyncUtils.getAsyncManager(request);

try {
ModelAndView mv = null;
Exception dispatchException = null;

try {
processedRequest = checkMultipart(request);
multipartRequestParsed = (processedRequest != request);

// Determine handler for the current request.
mappedHandler = getHandler(processedRequest);
if (mappedHandler == null) {
noHandlerFound(processedRequest, response);
return;
}

// Determine handler adapter for the current request.
HandlerAdapter ha = getHandlerAdapter(mappedHandler.getHandler());

// Process last-modified header, if supported by the handler.
String method = request.getMethod();
boolean isGet = "GET".equals(method);
if (isGet || "HEAD".equals(method)) {
long lastModified = ha.getLastModified(request, mappedHandler.getHandler());
if (new ServletWebRequest(request, response).checkNotModified(lastModified) && isGet) {
return;
}
}

if (!mappedHandler.applyPreHandle(processedRequest, response)) {
return;
}

// Actually invoke the handler.
mv = ha.handle(processedRequest, response, mappedHandler.getHandler());

if (asyncManager.isConcurrentHandlingStarted()) {
return;
}

applyDefaultViewName(processedRequest, mv);
mappedHandler.applyPostHandle(processedRequest, response, mv);
}
catch (Exception ex) {
dispatchException = ex;
}
catch (Throwable err) {
// As of 4.3, we're processing Errors thrown from handler methods as well,
// making them available for @ExceptionHandler methods and other scenarios.
dispatchException = new NestedServletException("Handler dispatch failed", err);
}
processDispatchResult(processedRequest, response, mappedHandler, mv, dispatchException);
}
catch (Exception ex) {
triggerAfterCompletion(processedRequest, response, mappedHandler, ex);
}
catch (Throwable err) {
triggerAfterCompletion(processedRequest, response, mappedHandler,
new NestedServletException("Handler processing failed", err));
}
finally {
if (asyncManager.isConcurrentHandlingStarted()) {
// Instead of postHandle and afterCompletion
if (mappedHandler != null) {
mappedHandler.applyAfterConcurrentHandlingStarted(processedRequest, response);
}
}
else {
// Clean up any resources used by a multipart request.
if (multipartRequestParsed) {
cleanupMultipart(processedRequest);
}
}
}
}

3.封装一个方法获取对应的处理器适配器

//方法的调用
HandlerAdapter ha = getHandlerAdapter(mappedHandler.getHandler());

4.遍历所有的处理器适配器(一共有四种,如下图)找到支持的适配器

protected HandlerAdapter getHandlerAdapter(Object handler) throws ServletException {
if (this.handlerAdapters != null) {
for (HandlerAdapter adapter : this.handlerAdapters) {
if (adapter.supports(handler)) {
return adapter;
}
}
}
throw new ServletException("No adapter for handler [" + handler +
"]: The DispatcherServlet configuration needs to include a HandlerAdapter that supports this handler");
}

image-20230812091950114

上图中四个处理器适配器中前两个是常用的处理器适配器: 0 - 支持方法上标注@RequestMapping 1 - 支持函数式编程的

5.查看当前的处理器适配器是否支持的方法

@Override
public final boolean supports(Object handler) {
//条件一 handler instanceof HandlerMethod
//条件二 supportsInternal((HandlerMethod) handler)
return (handler instanceof HandlerMethod && supportsInternal((HandlerMethod) handler));
}

遍历查找之后返回的适配器是RequestMappingHandlerAdapter

image-20230812092825385

6.回到DispatcherServlet执行下面的方法

//根据上面获取到的处理器适配器真正的执行处理器方法
// Actually invoke the handler.
mv = ha.handle(processedRequest, response, mappedHandler.getHandler());

7.内部的处理过程

handle( )方法

@Override
@Nullable
public final ModelAndView handle(HttpServletRequest request, HttpServletResponse response, Object handler)
throws Exception {
return handleInternal(request, response, (HandlerMethod) handler);
}

进入当前处理器适配器的handleInternal( )方法

@Override
protected ModelAndView handleInternal(HttpServletRequest request,
HttpServletResponse response, HandlerMethod handlerMethod) throws Exception {

ModelAndView mav;
checkRequest(request);

// Execute invokeHandlerMethod in synchronized block if required.
if (this.synchronizeOnSession) {
HttpSession session = request.getSession(false);
if (session != null) {
Object mutex = WebUtils.getSessionMutex(session);
synchronized (mutex) {
mav = invokeHandlerMethod(request, response, handlerMethod);
}
}
else {
// No HttpSession available -> no mutex necessary
mav = invokeHandlerMethod(request, response, handlerMethod);
}
}
else {
// No synchronization on session demanded at all...
mav = invokeHandlerMethod(request, response, handlerMethod);
}

if (!response.containsHeader(HEADER_CACHE_CONTROL)) {
if (getSessionAttributesHandler(handlerMethod).hasSessionAttributes()) {
applyCacheSeconds(response, this.cacheSecondsForSessionAttributeHandlers);
}
else {
prepareResponse(response);
}
}

return mav;
}

执行invokeHandlerMethod( )方法

mav = invokeHandlerMethod(request, response, handlerMethod);

进入invokeHandlerMethod()方法

@Nullable
protected ModelAndView invokeHandlerMethod(HttpServletRequest request,
HttpServletResponse response, HandlerMethod handlerMethod) throws Exception {

ServletWebRequest webRequest = new ServletWebRequest(request, response);
try {
WebDataBinderFactory binderFactory = getDataBinderFactory(handlerMethod);
ModelFactory modelFactory = getModelFactory(handlerMethod, binderFactory);

ServletInvocableHandlerMethod invocableMethod = createInvocableHandlerMethod(handlerMethod);
if (this.argumentResolvers != null) {
invocableMethod.setHandlerMethodArgumentResolvers(this.argumentResolvers);
}
if (this.returnValueHandlers != null) {
invocableMethod.setHandlerMethodReturnValueHandlers(this.returnValueHandlers);
}
invocableMethod.setDataBinderFactory(binderFactory);
invocableMethod.setParameterNameDiscoverer(this.parameterNameDiscoverer);

ModelAndViewContainer mavContainer = new ModelAndViewContainer();
mavContainer.addAllAttributes(RequestContextUtils.getInputFlashMap(request));
modelFactory.initModel(webRequest, mavContainer, invocableMethod);
mavContainer.setIgnoreDefaultModelOnRedirect(this.ignoreDefaultModelOnRedirect);

AsyncWebRequest asyncWebRequest = WebAsyncUtils.createAsyncWebRequest(request, response);
asyncWebRequest.setTimeout(this.asyncRequestTimeout);

WebAsyncManager asyncManager = WebAsyncUtils.getAsyncManager(request);
asyncManager.setTaskExecutor(this.taskExecutor);
asyncManager.setAsyncWebRequest(asyncWebRequest);
asyncManager.registerCallableInterceptors(this.callableInterceptors);
asyncManager.registerDeferredResultInterceptors(this.deferredResultInterceptors);

if (asyncManager.hasConcurrentResult()) {
Object result = asyncManager.getConcurrentResult();
mavContainer = (ModelAndViewContainer) asyncManager.getConcurrentResultContext()[0];
asyncManager.clearConcurrentResult();
LogFormatUtils.traceDebug(logger, traceOn -> {
String formatted = LogFormatUtils.formatValue(result, !traceOn);
return "Resume with async result [" + formatted + "]";
});
invocableMethod = invocableMethod.wrapConcurrentResult(result);
}

invocableMethod.invokeAndHandle(webRequest, mavContainer);
if (asyncManager.isConcurrentHandlingStarted()) {
return null;
}

return getModelAndView(mavContainer, modelFactory, webRequest);
}
finally {
webRequest.requestCompleted();
}
}

注意这里的26个参数解析器,参数解析器的作用是确定将要执行的目标方法的每一个参数的值是什么

这里的每一个参数解析器都对应了我们标注在参数上的注解

SpringMVC目标方法能写多少种参数类型取决于参数解析器

image-20230812094224699

参数解析器的接口设计

image-20230812094755583

  • 当前解析器是否支持解析这种参数
  • 支持就调用 resolveArgument

返回值处理器

定义了controller返回值的类型

image-20230812095137195

RequestMappingHandlerAdapter的invokeAndHandle( )方法真正的执行请求

invocableMethod.invokeAndHandle(webRequest, mavContainer);
public void invokeAndHandle(ServletWebRequest webRequest, ModelAndViewContainer mavContainer,
Object... providedArgs) throws Exception {

Object returnValue = invokeForRequest(webRequest, mavContainer, providedArgs);
//上面一行代码执行完毕之后会直接跳到controller方法,然后执行下面的代码
setResponseStatus(webRequest);

if (returnValue == null) {
if (isRequestNotModified(webRequest) || getResponseStatus() != null || mavContainer.isRequestHandled()) {
disableContentCachingIfNecessary(webRequest);
mavContainer.setRequestHandled(true);
return;
}
}
else if (StringUtils.hasText(getResponseStatusReason())) {
mavContainer.setRequestHandled(true);
return;
}

mavContainer.setRequestHandled(false);
Assert.state(this.returnValueHandlers != null, "No return value handlers");
try {
this.returnValueHandlers.handleReturnValue(
returnValue, getReturnValueType(returnValue), mavContainer, webRequest);
}
catch (Exception ex) {
if (logger.isTraceEnabled()) {
logger.trace(formatErrorForReturnValue(returnValue), ex);
}
throw ex;
}
}

执行的过程

Object returnValue = invokeForRequest(webRequest, mavContainer, providedArgs);
@Nullable
public Object invokeForRequest(NativeWebRequest request, @Nullable ModelAndViewContainer mavContainer,
Object... providedArgs) throws Exception {

Object[] args = getMethodArgumentValues(request, mavContainer, providedArgs);
//这个args就记录了方法上所有的参数
if (logger.isTraceEnabled()) {
logger.trace("Arguments: " + Arrays.toString(args));
}
return doInvoke(args);
}

image-20230812100059378

获取方法参数值的过程

Object[] args = getMethodArgumentValues(request, mavContainer, providedArgs);
protected Object[] getMethodArgumentValues(NativeWebRequest request, @Nullable ModelAndViewContainer mavContainer,
Object... providedArgs) throws Exception {


//获取所有参数的参数声明 参数类型 标注的注解
MethodParameter[] parameters = getMethodParameters();
if (ObjectUtils.isEmpty(parameters)) {
return EMPTY_ARGS;
}

Object[] args = new Object[parameters.length];
for (int i = 0; i < parameters.length; i++) {
MethodParameter parameter = parameters[i];
parameter.initParameterNameDiscovery(this.parameterNameDiscoverer);
args[i] = findProvidedArgument(parameter, providedArgs);
if (args[i] != null) {
continue;
}
if (!this.resolvers.supportsParameter(parameter)) {
throw new IllegalStateException(formatArgumentError(parameter, "No suitable resolver"));
}
try {
args[i] = this.resolvers.resolveArgument(parameter, mavContainer, request, this.dataBinderFactory);
}
catch (Exception ex) {
// Leave stack trace for later, exception may actually be resolved and handled...
if (logger.isDebugEnabled()) {
String exMsg = ex.getMessage();
if (exMsg != null && !exMsg.contains(parameter.getExecutable().toGenericString())) {
logger.debug(formatArgumentError(parameter, exMsg));
}
}
throw ex;
}
}
return args;
}

遍历判断获取支持解析该参数的参数解析器

@Nullable
private HandlerMethodArgumentResolver getArgumentResolver(MethodParameter parameter) {
HandlerMethodArgumentResolver result = this.argumentResolverCache.get(parameter);
if (result == null) {
for (HandlerMethodArgumentResolver resolver : this.argumentResolvers) {
if (resolver.supportsParameter(parameter)) {
result = resolver;
this.argumentResolverCache.put(parameter, result);
break;
}
}
}
return result;
}

解析参数的值通过调用各自 HandlerMethodArgumentResolver 的 resolveArgument 方法

2.4 数据响应与内容协商

返回值的处理逻辑

2.4.1 相关依赖的引入

创建springBoot项目的时候导入了spring-boot-starter-web依赖会自动的帮我们导入相关的json依赖,便于json数据的前后端传递

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

自动的帮我们导入相关的json的starter

<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-json</artifactId>
<version>2.3.4.RELEASE</version>
<scope>compile</scope>
</dependency>

在spring-boot-starter-json中引入了jackson相关的依赖

<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-databind</artifactId>
<version>2.11.2</version>
<scope>compile</scope>
</dependency>
<dependency>
<groupId>com.fasterxml.jackson.datatype</groupId>
<artifactId>jackson-datatype-jdk8</artifactId>
<version>2.11.2</version>
<scope>compile</scope>
</dependency>
<dependency>
<groupId>com.fasterxml.jackson.datatype</groupId>
<artifactId>jackson-datatype-jsr310</artifactId>
<version>2.11.2</version>
<scope>compile</scope>
</dependency>
<dependency>
<groupId>com.fasterxml.jackson.module</groupId>
<artifactId>jackson-module-parameter-names</artifactId>
<version>2.11.2</version>
<scope>compile</scope>
</dependency>

返回值解析器的原理

  • 1、返回值处理器判断是否支持这种类型返回值 supportsReturnType

  • 2、返回值处理器调用 handleReturnValue 进行处理

  • 3、RequestResponseBodyMethodProcessor 可以处理返回值标了@ResponseBody 注解的。

      1. 利用 MessageConverters 进行处理 将数据写为json
      • 1、内容协商(浏览器默认会以请求头的方式告诉服务器他能接受什么样的内容类型)
      • 2、服务器最终根据自己自身的能力,决定服务器能生产出什么样内容类型的数据,
      • 3、SpringMVC会挨个遍历所有容器底层的 HttpMessageConverter ,看谁能处理?
        • 1、得到MappingJackson2HttpMessageConverter可以将对象写为json
        • 2、利用MappingJackson2HttpMessageConverter将对象转为json再写出去。
2.4.2 内容协商原理

引入jackson-dataformat-xml,测试返回是xml格式的数据

<dependency>
<groupId>com.fasterxml.jackson.dataformat</groupId>
<artifactId>jackson-dataformat-xml</artifactId>
</dependency>

开启基于请求参数的内容协商功能

发请求: http://localhost:8080/test/person?format=json //指定格式是json

http://localhost:8080/test/person?format=xml //指定格式是xml

spring:
contentnegotiation:
favor-parameter: true #开启请求参数内容协商模式

1、Parameter策略优先确定是要返回json数据(获取请求头中的format的值)

2、最终进行内容协商返回给客户端json即可

3、遍历循环所有当前系统的 MessageConverter,看谁支持操作这个对象(Person)

4、找到支持操作Person的converter,把converter支持的媒体类型统计出来。

5、客户端需要【application/xml】。服务端能力【10种、json、xml】

6、进行内容协商的最佳匹配媒体类型

7、用 支持 将对象转为 最佳匹配媒体类型 的converter。调用它进行转化 。

2.4.3 自定义MessageConverter

1、@ResponseBody 响应数据出去 调用 RequestResponseBodyMethodProcessor 处理

2、Processor 处理方法返回值。通过 MessageConverter 处理

3、所有 MessageConverter 合起来可以支持各种媒体类型数据的操作(读、写)

4、内容协商找到最终的 messageConverter;

步骤:
1、添加自定义的MessageConverter进系统底层

2、系统底层就会统计出所有MessageConverter能操作哪些类型

3、客户端内容协商 [guigu—>guigu]

编写自定义的的converter

package com.atguigu.boot.converter;

import com.atguigu.boot.bean.Person;
import org.springframework.http.HttpInputMessage;
import org.springframework.http.HttpOutputMessage;
import org.springframework.http.MediaType;
import org.springframework.http.converter.HttpMessageConverter;
import org.springframework.http.converter.HttpMessageNotReadableException;
import org.springframework.http.converter.HttpMessageNotWritableException;
import java.io.IOException;
import java.io.OutputStream;
import java.util.List;

/**
* 自定义的Converter
*/
public class GuiguMessageConverter implements HttpMessageConverter<Person> {

@Override
public boolean canRead(Class<?> clazz, MediaType mediaType) {
return false;
}

@Override
public boolean canWrite(Class<?> clazz, MediaType mediaType) {
return clazz.isAssignableFrom(Person.class);
}

/**
* 服务器要统计所有MessageConverter都能写出哪些内容类型
*
* application/x-guigu
* @return
*/
@Override
public List<MediaType> getSupportedMediaTypes() {
return MediaType.parseMediaTypes("application/x-guigu");
}

@Override
public Person read(Class<? extends Person> clazz, HttpInputMessage inputMessage) throws IOException, HttpMessageNotReadableException {
return null;
}

@Override
public void write(Person person, MediaType contentType, HttpOutputMessage outputMessage) throws IOException, HttpMessageNotWritableException {
//自定义协议数据的写出
String data = person.getUserName()+";"+person.getAge()+";"+person.getBirth();
//写出去
OutputStream body = outputMessage.getBody();
body.write(data.getBytes());
}
}

在配置文件中加入自定义的converter

package com.atguigu.boot.config;


import com.atguigu.boot.bean.Person;
import com.atguigu.boot.bean.Pet;
import com.atguigu.boot.converter.GuiguMessageConverter;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.convert.converter.Converter;
import org.springframework.format.FormatterRegistry;
import org.springframework.http.HttpInputMessage;
import org.springframework.http.HttpOutputMessage;
import org.springframework.http.MediaType;
import org.springframework.http.converter.HttpMessageConverter;
import org.springframework.http.converter.HttpMessageNotReadableException;
import org.springframework.http.converter.HttpMessageNotWritableException;
import org.springframework.util.StringUtils;
import org.springframework.web.accept.HeaderContentNegotiationStrategy;
import org.springframework.web.accept.ParameterContentNegotiationStrategy;
import org.springframework.web.filter.HiddenHttpMethodFilter;
import org.springframework.web.servlet.config.annotation.ContentNegotiationConfigurer;
import org.springframework.web.servlet.config.annotation.PathMatchConfigurer;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
import org.springframework.web.util.UrlPathHelper;

import java.io.IOException;
import java.io.OutputStream;
import java.util.*;

@Configuration(proxyBeanMethods = false)
public class WebConfig {
@Bean
public WebMvcConfigurer webMvcConfigurer(){
return new WebMvcConfigurer() {
//加入自定义的converter
@Override
public void extendMessageConverters(List<HttpMessageConverter<?>> converters) {
converters.add(new GuiguMessageConverter());
}
};
}
}

在浏览器url上拼接参数的形式实现内容协商

package com.atguigu.boot.config;

import com.atguigu.boot.bean.Person;
import com.atguigu.boot.bean.Pet;
import com.atguigu.boot.converter.GuiguMessageConverter;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.convert.converter.Converter;
import org.springframework.format.FormatterRegistry;
import org.springframework.http.HttpInputMessage;
import org.springframework.http.HttpOutputMessage;
import org.springframework.http.MediaType;
import org.springframework.http.converter.HttpMessageConverter;
import org.springframework.http.converter.HttpMessageNotReadableException;
import org.springframework.http.converter.HttpMessageNotWritableException;
import org.springframework.util.StringUtils;
import org.springframework.web.accept.HeaderContentNegotiationStrategy;
import org.springframework.web.accept.ParameterContentNegotiationStrategy;
import org.springframework.web.filter.HiddenHttpMethodFilter;
import org.springframework.web.servlet.config.annotation.ContentNegotiationConfigurer;
import org.springframework.web.servlet.config.annotation.PathMatchConfigurer;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
import org.springframework.web.util.UrlPathHelper;
import java.io.IOException;
import java.io.OutputStream;
import java.util.*;

@Configuration(proxyBeanMethods = false)
public class WebConfig{

@Bean
public WebMvcConfigurer webMvcConfigurer(){
return new WebMvcConfigurer() {

/**
* 自定义内容协商策略
* @param configurer
*/
@Override
public void configureContentNegotiation(ContentNegotiationConfigurer configurer) {
Map<String, MediaType> mediaTypes = new HashMap<>();
mediaTypes.put("json",MediaType.APPLICATION_JSON);
mediaTypes.put("xml",MediaType.APPLICATION_XML);
//在浏览器url上拼接参数的形式实现内容协商
//http://localhost:8080/test/person?format=gg
mediaTypes.put("gg",MediaType.parseMediaType("application/x-guigu"));
//指定支持解析哪些参数对应的哪些媒体类型
ParameterContentNegotiationStrategy parameterStrategy = new ParameterContentNegotiationStrategy(mediaTypes);
//parameterStrategy.setParameterName("ff");

HeaderContentNegotiationStrategy headeStrategy = new HeaderContentNegotiationStrategy();

configurer.strategies(Arrays.asList(parameterStrategy,headeStrategy));
}

//加入自定义的converter
@Override
public void extendMessageConverters(List<HttpMessageConverter<?>> converters) {
converters.add(new GuiguMessageConverter());
}
};
}
}

3.视图解析与模板引擎

视图解析:SpringBoot默认不支持 JSP,需要引入第三方模板引擎技术实现页面渲染

3.1 视图解析

image-20230815091741019

3.2 模板引擎-Thymeleaf

详细的使用教程: https://jasonsgong.gitee.io/posts/54835.html

3.2.1 Thymeleaf的使用

1.引入依赖

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

2.编写测试controller

@Controller
public class ViewTestController {
@GetMapping("/view")
public String hello(Model model){
//向请求域中共享数据
model.addAttribute("keys","Thymeleaf");
//转发到success页面中
return "success";
}
}

3.页面上渲染

<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="UTF-8">
<title>Title</title>
</head>
<body>
<h1 th:text="${keys}">Hello</h1>
</body>
</html>

image-20230815153318041

3.3 后台管理系统注意点

templates目录的访问规则

templates目录下的所有页面资源只能通过请求访问到

表单重复提交的问题

package com.atguigu.admin.controller;


@Slf4j
@Controller
public class IndexController {

/**
* 来登录页
* @return
*/
@GetMapping(value = {"/","/login"})
public String loginPage(){

return "login";
}


@PostMapping("/login")
public String main(String username, String password){
//登录成功重定向到main.html; 重定向防止表单重复提交
//这里如果直接写 return "main"的话,页面在mian.html的情况下,url路径还是localhost:8080/login,如果再次刷新页面会重复提交表单
//使用下面这个写法,执行"/login"这个请求之后,路径的url会变成localhost:8080/main.html
return "redirect:/main.html";
}

/**
* 去main页面
* @return
*/
@GetMapping("/main.html")
public String mainPage(){
return "main";
}
}

4.拦截器

1.HandlerInterceptor接口

自定义拦截器需要实现的接口,以及需要实现接口中的方法

image-20230821105529796

2.拦截器实现登录检查操作

登录检查的业务逻辑

/**
* 登录检查
* 1、配置好拦截器要拦截哪些请求
* 2、把这些配置放在容器中
*/
@Slf4j
public class LoginInterceptor implements HandlerInterceptor {

/**
* 目标方法执行之前
* @param request
* @param response
* @param handler
* @return
* @throws Exception
*/
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {

String requestURI = request.getRequestURI();
log.info("preHandle拦截的请求路径是{}",requestURI);

//登录检查逻辑
HttpSession session = request.getSession();

Object loginUser = session.getAttribute("loginUser");

if(loginUser != null){
//放行
return true;
}

//拦截住 未登录 跳转到登录页
request.setAttribute("msg","请先登录");
request.getRequestDispatcher("/").forward(request,response);
return false;
}

/**
* 目标方法执行完成以后
* @param request
* @param response
* @param handler
* @param modelAndView
* @throws Exception
*/
@Override
public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception {
log.info("postHandle执行{}",modelAndView);
}

/**
* 页面渲染以后
* @param request
* @param response
* @param handler
* @param ex
* @throws Exception
*/
@Override
public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
log.info("afterCompletion执行异常{}",ex);
}
}

拦截器的配置

/**
* 1、编写一个拦截器实现HandlerInterceptor接口
* 2、拦截器注册到容器中(实现WebMvcConfigurer的addInterceptors)
* 3、指定拦截规则[如果是拦截所有,静态资源也会被拦截]
*/
@Configuration
public class AdminWebConfig implements WebMvcConfigurer {

@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(new LoginInterceptor())
.addPathPatterns("/**") //所有请求都被拦截包括静态资源
.excludePathPatterns("/","/login","/css/**","/fonts/**","/images/**","/js/**"); //放行的请求
}
}

3.拦截器原理

/**
* preHandle() :该方法在控制器方法之前执行,如果返回false,说明拦截器不放行该请求,自己处理或者结束该请求
* 如果返回true,说明拦截器放该请求,由其它匹配的拦截器或者控制器继续处理
* postHandle():该方法在控制器方法调用之后,且解析视图之前执行,可以通过此方法对请求域中的模型和视图做出进一步的修改
* afterCompletion(): 该方法会在整个请求完成,即视图渲染结束之后执行,多用于资源的清理工作
*/

1、根据当前请求,找到HandlerExecutionChain[可以处理请求的handler以及handler的所有 拦截器]

2、先来顺序执行 所有拦截器的 preHandle方法

  • 1、如果当前拦截器prehandler返回为true,则执行下一个拦截器的preHandle
  • 2、如果当前拦截器返回为false。直接倒序执行所有已经执行了的拦截器的afterCompletion方法

3、如果任何一个拦截器返回false,直接跳出不执行目标方法

4、所有拦截器都返回true,执行目标方法

5、倒序执行所有拦截器的postHandle方法

6、前面的步骤有任何异常都会直接倒序触发 afterCompletion

7、页面成功渲染完成以后,也会倒序触发 afterCompletion

image-20230821112246355

5.文件上传

1.页面表单

<form method="post" action="/upload" enctype="multipart/form-data">
<input type="file" name="file"><br>
<input type="submit" value="提交">
</form>

2.文件上传代码

/**
* MultipartFile 自动封装上传过来的文件
*/
@PostMapping("/upload")
public String upload(@RequestParam("email") String email,
@RequestParam("username") String username,
@RequestPart("headerImg") MultipartFile headerImg,//单个文件
@RequestPart("photos") MultipartFile[] photos) throws IOException {//多个文件

log.info("上传的信息:email={},username={},headerImg={},photos={}",
email, username, headerImg.getSize(), photos.length);

if (!headerImg.isEmpty()) {
//保存到文件服务器,OSS服务器
String originalFilename = headerImg.getOriginalFilename();
headerImg.transferTo(new File("H:\\cache\\" + originalFilename));
}

if (photos.length > 0) {
for (MultipartFile photo : photos) {
if (!photo.isEmpty()) {
String originalFilename = photo.getOriginalFilename();
photo.transferTo(new File("H:\\cache\\" + originalFilename));
}
}
}
return "main";
}

3.MultipartAutoConfiguration

@Configuration(proxyBeanMethods = false)
@ConditionalOnClass({ Servlet.class, StandardServletMultipartResolver.class, MultipartConfigElement.class })
@ConditionalOnProperty(prefix = "spring.servlet.multipart", name = "enabled", matchIfMissing = true)
@ConditionalOnWebApplication(type = Type.SERVLET)
@EnableConfigurationProperties(MultipartProperties.class)
public class MultipartAutoConfiguration {}

6.异常处理

开发中异常处理方法: https://jasonsgong.gitee.io/posts/31385.html

1.默认规则

  • 默认情况下,Spring Boot提供/error处理所有错误的映射
  • 对于机器客户端,它将生成JSON响应,其中包含错误,HTTP状态和异常消息的详细信息。对于浏览器客户端,响应一个“ whitelabel”错误视图,以HTML格式呈现相同的数据

2.自定义错误页面

templates/error/下的4xx,5xx页面会被自动解析

7.Web原生组件注入(Servlet、Filter、Listener)

7.1 使用Servlet Api

1.原生的Servlet的使用

1.编写自定义的servlet继承HttpServlet

package com.atguigu.boot.servlet;

import javax.servlet.ServletException;
import javax.servlet.annotation.WebServlet;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;

/**
* @author Jason Gong
* @version 1.0
* @website https://jasonsgong.gitee.io
* @Date 2023/8/26
* @Description
*/
@WebServlet(urlPatterns = "/my")
public class MyServlet extends HttpServlet {
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
resp.getWriter().write("6666");
}
}

2.在启动类上添加自定义servlet的包扫描注解

package com.atguigu.boot;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.web.servlet.ServletComponentScan;

@ServletComponentScan("com.atguigu.boot") //指定原生的servlet的位置
@SpringBootApplication
public class Boot05Web01Application {

public static void main(String[] args) {
SpringApplication.run(Boot05Web01Application.class, args);
}

}

3.启动程序测试

image-20230826211541424

2.原生的Filter的使用

1.编写自定义的Filter实现Filter接口

package com.atguigu.boot.servlet;

import lombok.extern.slf4j.Slf4j;

import javax.servlet.*;
import javax.servlet.annotation.WebFilter;
import java.io.IOException;

/**
* @author Jason Gong
* @version 1.0
* @website https://jasonsgong.gitee.io
* @Date 2023/8/26
* @Description
*/
@Slf4j
@WebFilter(urlPatterns={"/css/*","/images/*","/my"}) //设置拦截的路径
public class MyFilter implements Filter {
@Override
public void init(FilterConfig filterConfig) throws ServletException {
log.info("MyFilter初始化完成");
}

@Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
log.info("MyFilter工作");
chain.doFilter(request,response);
}

@Override
public void destroy() {
log.info("MyFilter销毁");
}
}

2.启动程序测试

image-20230826212400849

3.原生的Listener的使用

1.编写自定义的MyServletContextListener实现ServletContextListener

package com.atguigu.boot.servlet;

import lombok.extern.slf4j.Slf4j;

import javax.servlet.ServletContextEvent;
import javax.servlet.ServletContextListener;
import javax.servlet.annotation.WebListener;

/**
* @author Jason Gong
* @version 1.0
* @website https://jasonsgong.gitee.io
* @Date 2023/8/26
* @Description
*/
@Slf4j
@WebListener
public class MyServletContextListener implements ServletContextListener {


@Override
public void contextInitialized(ServletContextEvent sce) {
log.info("MyServletContextListener监听到项目初始化完成");
}

@Override
public void contextDestroyed(ServletContextEvent sce) {
log.info("MyServletContextListener监听到项目销毁");
}
}

2.启动程序测试

image-20230826213125490

7.2 使用RegistrationBean

不使用@WebServlet、@WebFilter、@WebListener注解的方式(注释掉自定义servlet、Filter、Listener上的注解)

package com.atguigu.boot.servlet;

import org.springframework.boot.web.servlet.FilterRegistrationBean;
import org.springframework.boot.web.servlet.ServletListenerRegistrationBean;
import org.springframework.boot.web.servlet.ServletRegistrationBean;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

import java.util.Arrays;

/**
* @author Jason Gong
* @version 1.0
* @website https://jasonsgong.gitee.io
* @Date 2023/8/26
* @Description
*/
@Configuration
public class MyRegisterConfig {
@Bean
public ServletRegistrationBean myServlet(){
MyServlet myServlet = new MyServlet();

return new ServletRegistrationBean(myServlet,"/my","/my02");
}


@Bean
public FilterRegistrationBean myFilter(){

MyFilter myFilter = new MyFilter();
//return new FilterRegistrationBean(myFilter,myServlet());
FilterRegistrationBean filterRegistrationBean = new FilterRegistrationBean(myFilter);
filterRegistrationBean.setUrlPatterns(Arrays.asList("/my","/css/*"));
return filterRegistrationBean;
}

@Bean
public ServletListenerRegistrationBean myListener(){
MyServletContextListener mySwervletContextListener = new MyServletContextListener();
return new ServletListenerRegistrationBean(mySwervletContextListener);
}
}

重新启动项目我们可以看到依然生效

image-20230826214253187

8.嵌入式Servlet容器

8.1 切换嵌入式Servlet容器

  • 默认支持的webServer

    • Tomcat, Jetty, or Undertow
    • ServletWebServerApplicationContext 容器启动寻找ServletWebServerFactory 并引导创建服务器
  • 切换服务器

    1.排除tomcat的依赖

    <dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-web</artifactId>
    <exclusions>
    <exclusion>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-tomcat</artifactId>
    </exclusion>
    </exclusions>
    </dependency>

    2.引入需要的服务器场景

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

    3.重启项目,可以看到服务器由tomcat变成了undertow

    image-20230826221229898

  • 原理

    • SpringBoot应用启动发现当前是Web应用。web场景包-导入tomcat
    • web应用会创建一个web版的ioc容器 ServletWebServerApplicationContext
    • ServletWebServerApplicationContext 启动的时候寻找 **ServletWebServerFactory**``(Servlet 的web服务器工厂---> Servlet 的web服务器)
    • SpringBoot底层默认有很多的WebServer工厂;TomcatServletWebServerFactory, JettyServletWebServerFactory, or UndertowServletWebServerFactory
    • 底层直接会有一个自动配置类ServletWebServerFactoryAutoConfiguration
    • ServletWebServerFactoryAutoConfiguration导入了ServletWebServerFactoryConfiguration(配置类)
    • ServletWebServerFactoryConfiguration 配置类 根据动态判断系统中到底导入了那个Web服务器的包(默认是web-starter导入tomcat包),容器中就有 TomcatServletWebServerFactory
    • TomcatServletWebServerFactory 创建出Tomcat服务器并启动;TomcatWebServer 的构造器拥有初始化方法initialize---this.tomcat.start();
    • 内嵌服务器,就是手动把启动服务器的代码调用(tomcat核心jar包存在)

8.2 定制Servlet容器

修改配置文件中以server.xxx打头的属性信息

image-20230826222050817

三.数据访问

1.SQL

1.1 JDBC的使用

1.导入JDBC的场景

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

2.导入自己使用的数据库的驱动(Mysql或者Oracle)版本号不写也可以,官方做了版本仲裁(但是要与实际安装的版本对应)

想要修改版本
1、直接依赖引入具体版本(maven的就近依赖原则)
2、重新声明版本(maven的属性的就近优先原则)
<properties>
<java.version>1.8</java.version>
<mysql.version>5.1.45</mysql.version>
</properties>
或者
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>5.1.45</version>
</dependency>

3.添加配置

spring:
datasource:
url: jdbc:mysql://localhost:3306/db_account
username: root
password: 123456
driver-class-name: com.mysql.jdbc.Driver
# type: com.zaxxer.hikari.HikariDataSource #配置数据源

4.测试

package com.atguigu.boot;

import lombok.extern.slf4j.Slf4j;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.jdbc.core.JdbcTemplate;

@Slf4j
@SpringBootTest
class Boot05Web01ApplicationTests {

@Autowired
JdbcTemplate jdbcTemplate;

@Test
void contextLoads() {
Long count = jdbcTemplate.queryForObject("select count(*) from `role` ", Long.class);
log.info("记录数:{}",count);
}
}

1.2 分析自动配置

自动配置的类

image-20230826224503510

  • DataSourceAutoConfiguration : 数据源的自动配置

    • 修改数据源相关的配置:spring.datasource
    • 数据库连接池的配置,是自己容器中没有DataSource才自动配置的
    • 底层配置好的连接池是:HikariDataSource
  • DataSourceTransactionManagerAutoConfiguration: 事务管理器的自动配置

  • JdbcTemplateAutoConfiguration: JdbcTemplate的自动配置,可以来对数据库进行crud

    • 可以修改这个配置项@ConfigurationProperties(prefix = “spring.jdbc”) 来修改JdbcTemplate
  • JndiDataSourceAutoConfiguration: jndi的自动配置

  • XADataSourceAutoConfiguration: 分布式事务相关的

四.单元测试

1.JUnit5的变化

Spring Boot 2.2.0 版本开始引入 JUnit 5 作为单元测试默认库

作为最新版本的JUnit框架,JUnit5与之前版本的Junit框架有很大的不同。由三个不同子项目的几个不同模块组成

JUnit 5 = JUnit Platform + JUnit Jupiter + JUnit Vintage

JUnit Platform: Junit Platform是在JVM上启动测试框架的基础,不仅支持Junit自制的测试引擎,其他测试引擎也都可以接入

JUnit Jupiter: JUnit Jupiter提供了JUnit5的新的编程模型,是JUnit5新特性的核心。内部 包含了一个测试引擎,用于在Junit Platform上运行

JUnit Vintage: 由于JUint已经发展多年,为了照顾老的项目,JUnit Vintage提供了兼容JUnit4.x,Junit3.x的测试引擎

注意:

SpringBoot 2.4 以上版本移除了默认对 Vintage 的依赖。如果需要兼容junit4需要自行引入(不能使用junit4的功能 @Test)

以前的版本

兼容以前的版本(junit4)需要额外导入的依赖(SpringBoot 2.4 以上版本移除了默认对 Vintage 的依赖)

<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.junit.vintage</groupId>
<artifactId>junit-vintage-engine</artifactId>
<scope>test</scope>
<exclusions>
<exclusion>
<groupId>org.hamcrest</groupId>
<artifactId>hamcrest-core</artifactId>
</exclusion>
</exclusions>
</dependency>

标注的注解

@SpringBootTest + @RunWith(SpringRunner.class)+@Test(4版本的@Test)

现在的版本

需要导入的依赖

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

标注的注解

@SpringBootTest + @Test

两个不同版本的 @Test 注解

image-20230827112742347

2.JUnit5常用注解

JUnit5的注解与JUnit4的注解有所变化

https://junit.org/junit5/docs/current/user-guide/#writing-tests-annotations

  • @Test :表示方法是测试方法。但是与JUnit4的@Test不同,他的职责非常单一不能声明任何属性,拓展的测试将会由Jupiter提供额外测试
  • @ParameterizedTest :表示方法是参数化测试,下方会有详细介绍
  • @RepeatedTest :表示方法可重复执行,下方会有详细介绍
  • @DisplayName :为测试类或者测试方法设置展示名称

image-20230827114256749

  • @BeforeEach :表示在每个单元测试之前执行

  • @AfterEach :表示在每个单元测试之后执行

  • @BeforeAll :表示在所有单元测试之前执行

  • @AfterAll :表示在所有单元测试之后执行

package com.atguigu.boot;

import lombok.extern.slf4j.Slf4j;
import org.junit.jupiter.api.*;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.jdbc.core.JdbcTemplate;

/**
* @author Jason Gong
* @version 1.0
* @website https://jasonsgong.gitee.io
* @Date 2023/8/27
* @Description 测试junit5的相关注解
*/
@Slf4j
@DisplayName("Junit5Test测试类")
@SpringBootTest
public class Junit5Test {
/**
* @DisplayName 加在方法或者类上,为测试类或者测试方法设置展示名称
*/
@Disabled
@DisplayName("测试@DisplayName注解")
@Test
void testDisplayName(){
log.info("@DisplayName注解的测试");
}

/**
* 在执行的单元测试方法之前运行
*/
@BeforeEach
void testBeforeEach(){
log.info("执行了testBeforeEach()方法");
}

/**
* 在执行的单元测试方法之后运行
*/
@AfterEach
void testAfterEach() {
log.info("执行了testAfterEach()方法");
}

/**
* 在所有的单元测试执行之后执行
* 注意是静态方法
*/
@BeforeAll
static void testBeforeAll() {
log.info("执行了testBeforeAll()方法");
}

/**
* 在所有的单元测试执行之前执行
* 注意是静态方法
*/
@AfterAll
static void testAfterAll() {
log.info("执行了testAfterAll()方法");
}
}
  • @Tag :表示单元测试类别,类似于JUnit4中的@Categories
  • @Disabled :表示测试类或测试方法不执行,类似于JUnit4中的@Ignore

image-20230827115900097

  • @Timeout :表示测试方法运行如果超过了指定时间将会返回错误
/**
* 规定方法超时时间 超出时间测试出异常
*/
@Timeout(value = 500, unit = TimeUnit.MILLISECONDS)
@Test
void testTimeout() throws InterruptedException {
Thread.sleep(600);
}
  • @ExtendWith :为测试类或测试方法提供扩展类引用
/**
* 重复测试
*/
@RepeatedTest(5)
@Test
void testRepeatedTest() {
log.info("重复测试中");
}

image-20230828095915653

全部的代码

package com.atguigu.boot;

import lombok.extern.slf4j.Slf4j;
import org.junit.jupiter.api.*;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.jdbc.core.JdbcTemplate;

import java.util.concurrent.TimeUnit;

/**
* @author Jason Gong
* @version 1.0
* @website https://jasonsgong.gitee.io
* @Date 2023/8/27
* @Description 测试junit5的相关注解
*/
@Slf4j
@DisplayName("Junit5Test测试类")
@SpringBootTest
public class Junit5Test {
/**
* @DisplayName 加在方法或者类上,为测试类或者测试方法设置展示名称
*/
@Disabled
@DisplayName("测试@DisplayName注解")
@Test
void testDisplayName(){
log.info("@DisplayName注解的测试");
}

/**
* 在执行的单元测试方法之前运行
*/
@BeforeEach
void testBeforeEach(){
log.info("执行了testBeforeEach()方法");
}

/**
* 在执行的单元测试方法之后运行
*/
@AfterEach
void testAfterEach() {
log.info("执行了testAfterEach()方法");
}

/**
* 在所有的单元测试执行之后执行
* 注意是静态方法
*/
@BeforeAll
static void testBeforeAll() {
log.info("执行了testBeforeAll()方法");
}

/**
* 在所有的单元测试执行之前执行
* 注意是静态方法
*/
@AfterAll
static void testAfterAll() {
log.info("执行了testAfterAll()方法");
}
/**
* 规定方法超时时间 超出时间测试出异常
*/
@Timeout(value = 500, unit = TimeUnit.MILLISECONDS)
@Test
void testTimeout() throws InterruptedException {
Thread.sleep(600);
}

/**
* 重复测试
*/
@RepeatedTest(5)
@Test
void testRepeatedTest() {
log.info("重复测试中");
}


}

3.断言

​ 断言(assertions)是测试方法中的核心部分,用来对测试需要满足的条件进行验证。这些断言方法都是 org.junit.jupiter.api.Assertions 的静态方法。检查业务逻辑返回的数据是否合理。所有的测试运行结束以后,会有一个详细的测试报告。

3.1 简单断言

用来对单个值进行简单的验证

方法 说明
assertEquals 判断两个对象或两个原始类型是否相等
assertNotEquals 判断两个对象或两个原始类型是否不相等
assertSame 判断两个对象引用是否指向同一个对象
assertNotSame 判断两个对象引用是否指向不同的对象
assertTrue 判断给定的布尔值是否为 true
assertFalse 判断给定的布尔值是否为 false
assertNull 判断给定的对象引用是否为 null
assertNotNull 判断给定的对象引用是否不为 null
@Test
@DisplayName("简单断言")
public void simple() {
assertEquals(3, 1 + 2, "simple math");
assertNotEquals(3, 1 + 1);

assertNotSame(new Object(), new Object());
Object obj = new Object();
assertSame(obj, obj);

assertFalse(1 > 2);
assertTrue(1 < 2);

assertNull(null);
assertNotNull(new Object());
}

3.2 数组断言

通过 assertArrayEquals 方法来判断两个对象或原始类型的数组是否相等

@Test
@DisplayName("数组断言")
public void array() {
assertArrayEquals(new int[]{1, 2}, new int[] {1, 2});
}

3.3 组合断言

assertAll 方法接受多个 org.junit.jupiter.api.Executable 函数式接口的实例作为要验证的断言,可以通过 lambda 表达式很容易的提供这些断言

@Test
@DisplayName("组合断言")
public void all() {
assertAll("Math",
() -> assertEquals(2, 1 + 1),
() -> assertTrue(1 > 0)
);
}

3.4 异常断言

在JUnit4时期,想要测试方法的异常情况时,需要用@Rule注解的ExpectedException变量还是比较麻烦的。而JUnit5提供了一种新的断言方式Assertions.assertThrows() ,配合函数式编程就可以进行使用。

@Test
@DisplayName("异常测试")
public void exceptionTest() {
ArithmeticException exception = Assertions.assertThrows(
//扔出断言异常
ArithmeticException.class, () -> System.out.println(1 % 0));

}

3.5 超时断言

Junit5还提供了Assertions.assertTimeout() 为测试方法设置了超时时间

@Test
@DisplayName("超时测试")
public void timeoutTest() {
//如果测试方法时间超过1s将会异常
Assertions.assertTimeout(Duration.ofMillis(1000), () -> Thread.sleep(500));
}

3.6 快速失败

通过 fail 方法直接使得测试失败

@Test
@DisplayName("快速失败")
public void shouldFail() {
fail("This should fail");
}

全部代码

package com.atguigu.admin;

import org.junit.jupiter.api.*;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.jdbc.core.JdbcTemplate;

import java.util.concurrent.TimeUnit;

import static org.junit.jupiter.api.Assertions.*;



/*@RunWith*/

/**
* @BootstrapWith(SpringBootTestContextBootstrapper.class)
* @ExtendWith(SpringExtension.class)
*/
//@SpringBootTest
@DisplayName("junit5功能测试类")
public class Junit5Test {

/**
* 断言:前面断言失败,后面的代码都不会执行
*/
@DisplayName("简单断言")
@Test
void testSimpleAssertions() {
int cal = cal(3, 2);
//相等
assertEquals(6, cal, "业务逻辑计算失败");
Object obj1 = new Object();
Object obj2 = new Object();
assertSame(obj1, obj2, "两个对象不一样");

}

@Test
@DisplayName("数组断言")
void array() {
assertArrayEquals(new int[]{1, 2}, new int[]{1, 2}, "数组内容不相等");
}

@Test
@DisplayName("组合断言")
void all() {
/**
* 所有断言全部需要成功
*/
assertAll("test",
() -> assertTrue(true && true, "结果不为true"),
() -> assertEquals(1, 2, "结果不是1"));

System.out.println("=====");
}

@DisplayName("异常断言")
@Test
void testException() {

//断定业务逻辑一定出现异常
assertThrows(ArithmeticException.class, () -> {
int i = 10 / 2;
}, "业务逻辑居然正常运行?");
}

@DisplayName("快速失败")
@Test
void testFail(){
if(1 == 2){
fail("测试失败");
}

}

int cal(int i, int j) {
return i + j;
}
}

4.前置条件

​ JUnit 5 中的前置条件(assumptions[假设])类似于断言,不同之处在于不满足的断言会使得测试方法失败,而不满足的前置条件只会使得测试方法的执行终止。前置条件可以看成是测试方法执行的前提,当该前提不满足时,就没有继续执行的必要

@DisplayName("前置条件")
public class AssumptionsTest {
private final String environment = "DEV";

@Test
@DisplayName("simple")
public void simpleAssume() {
assumeTrue(Objects.equals(this.environment, "DEV"));
assumeFalse(() -> Objects.equals(this.environment, "PROD"));
}

@Test
@DisplayName("assume then do")
public void assumeThenDo() {
assumingThat(
Objects.equals(this.environment, "DEV"),
() -> System.out.println("In DEV")
);
}
}

assumeTrue 和 assumFalse 确保给定的条件为 true 或 false,不满足条件会使得测试执行终止

assumingThat 的参数是表示条件的布尔值和对应的 Executable 接口的实现对象

只有条件满足时,Executable 对象才会被执行;当条件不满足时,测试执行并不会终止

5.嵌套测试

​ JUnit 5 可以通过 Java 中的内部类和@Nested 注解实现嵌套测试,从而可以更好的把相关的测试方法组织在一起。在内部类中可以使用@BeforeEach 和@AfterEach 注解,而且嵌套的层次没有限制

@DisplayName("A stack")
class TestingAStackDemo {

Stack<Object> stack;

@Test
@DisplayName("is instantiated with new Stack()")
void isInstantiatedWithNew() {
new Stack<>();
}

@Nested
@DisplayName("when new")
class WhenNew {

@BeforeEach
void createNewStack() {
stack = new Stack<>();
}

@Test
@DisplayName("is empty")
void isEmpty() {
assertTrue(stack.isEmpty());
}

@Test
@DisplayName("throws EmptyStackException when popped")
void throwsExceptionWhenPopped() {
assertThrows(EmptyStackException.class, stack::pop);
}

@Test
@DisplayName("throws EmptyStackException when peeked")
void throwsExceptionWhenPeeked() {
assertThrows(EmptyStackException.class, stack::peek);
}

@Nested//b
@DisplayName("after pushing an element")
class AfterPushing {

String anElement = "an element";

@BeforeEach
void pushAnElement() {
stack.push(anElement);
}

@Test
@DisplayName("it is no longer empty")
void isNotEmpty() {
assertFalse(stack.isEmpty());
}

@Test
@DisplayName("returns the element when popped and is empty")
void returnElementWhenPopped() {
assertEquals(anElement, stack.pop());
assertTrue(stack.isEmpty());
}

@Test
@DisplayName("returns the element when peeked but remains not empty")
void returnElementWhenPeeked() {
assertEquals(anElement, stack.peek());
assertFalse(stack.isEmpty());
}
}
}
}

6.参数化测试

参数化测试是JUnit5很重要的一个新特性,它使得用不同的参数多次运行测试成为了可能,也为我们的单元测试带来许多便利

利用@ValueSource等注解,指定入参,我们将可以使用不同的参数进行多次单元测试,而不需要每新增一个参数就新增一个单元测试,省去了很多冗余代码

@ValueSource: 为参数化测试指定入参来源,支持八大基础类以及String类型,Class类型

@NullSource: 表示为参数化测试提供一个null的入参

@EnumSource: 表示为参数化测试提供一个枚举入参

@CsvFileSource:表示读取指定CSV文件内容作为参数化测试入参

@MethodSource:表示读取指定方法的返回值作为参数化测试入参(注意方法返回需要是一个流)

当然如果参数化测试仅仅只能做到指定普通的入参还达不到让我觉得惊艳的地步。让我真正感到他的强大之处的地方在于他可以支持外部的各类入参。如:CSV,YML,JSON 文件甚至方法的返回值也可以作为入参。只需要去实现ArgumentsProvider接口,任何外部文件都可以作为它的入参

@ParameterizedTest
@ValueSource(strings = {"one", "two", "three"})
@DisplayName("参数化测试")
public void parameterizedTest(String string) {
System.out.println(string);
Assertions.assertTrue(StringUtils.isNotBlank(string));
}


@ParameterizedTest
@MethodSource("method") //指定方法名
@DisplayName("方法来源参数")
public void testWithExplicitLocalMethodSource(String name) {//方法的参数来源于一个方法
System.out.println(name);
Assertions.assertNotNull(name);
}

static Stream<String> method() {
return Stream.of("apple", "banana");
}

7.迁移指南

在进行迁移(Junit4迁移到Junit5)的时候需要注意如下的变化:

  • 注解在 org.junit.jupiter.api 包中,断言在 org.junit.jupiter.api.Assertions 类中,前置条件在 org.junit.jupiter.api.Assumptions 类中。
  • 把@Before 和@After 替换成@BeforeEach 和@AfterEach。
  • 把@BeforeClass 和@AfterClass 替换成@BeforeAll 和@AfterAll。
  • 把@Ignore 替换成@Disabled。
  • 把@Category 替换成@Tag。
  • 把@RunWith、@Rule 和@ClassRule 替换成@ExtendWith。

五.指标监控

1.SpringBoot Actuator

​ 未来每一个微服务在云上部署以后,我们都需要对其进行监控、追踪、审计、控制等。SpringBoot就抽取了Actuator场景,使得我们每个微服务快速引用即可获得生产级别的应用监控、审计等功能

1.1 如何使用

1.引入依赖

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

2.测试访问 http://localhost:8080/actuator/ endpoint节点

image-20230828110908056

image-20230828110928951

3.设置以web的方式访问所有的端点

management:
endpoints:
enabled-by-default: true #暴露所有端点信息
web:
exposure:
include: '*' #以web方式暴露所有的端点

设置之后就可以通过web(访问 http://localhost:8080/actuator/beans)的方式访问beans端点了

image-20230828113335033

支持的暴露方式

  • HTTP(web):默认只暴露health和info Endpoint,只能访问 http://localhost:8080/actuator/health和 http://localhost:8080/actuator/info
  • JMX(如Jconsole):默认暴露所有Endpoint,类似于cmd下打开jconsole,可以访问下面所有的endPoints
  • 除过health和info,剩下的Endpoint都应该进行保护访问。如果引入SpringSecurity,则会默认配置安全访问规则

是通过web还是jmx方式访问的端点一览表

ID JMX Web
auditevents Yes No
beans Yes No
caches Yes No
conditions Yes No
configprops Yes No
env Yes No
flyway Yes No
health Yes Yes
heapdump N/A No
httptrace Yes No
info Yes Yes
integrationgraph Yes No
jolokia N/A No
logfile N/A No
loggers Yes No
liquibase Yes No
metrics Yes No
mappings Yes No
prometheus N/A No
scheduledtasks Yes No
sessions Yes No
shutdown Yes No
startup Yes No
threaddump Yes No

1.2 常用的EndPoints

http://localhost:8080/actuator/endPoint

ID 描述
auditevents 暴露当前应用程序的审核事件信息。需要一个AuditEventRepository组件
beans 显示应用程序中所有Spring Bean的完整列表。
caches 暴露可用的缓存。
conditions 显示自动配置的所有条件信息,包括匹配或不匹配的原因。
configprops 显示所有@ConfigurationProperties
env 暴露Spring的属性ConfigurableEnvironment
flyway 显示已应用的所有Flyway数据库迁移。 需要一个或多个Flyway组件。
health 显示应用程序运行状况信息。
httptrace 显示HTTP跟踪信息(默认情况下,最近100个HTTP请求-响应)。需要一个HttpTraceRepository组件。
info 显示应用程序信息。
integrationgraph 显示Spring integrationgraph 。需要依赖spring-integration-core
loggers 显示和修改应用程序中日志的配置。
liquibase 显示已应用的所有Liquibase数据库迁移。需要一个或多个Liquibase组件。
metrics 显示当前应用程序的“指标”信息。
mappings 显示所有@RequestMapping路径列表。
scheduledtasks 显示应用程序中的计划任务。
sessions 允许从Spring Session支持的会话存储中检索和删除用户会话。需要使用Spring Session的基于Servlet的Web应用程序。
shutdown 使应用程序正常关闭。默认禁用。
startup 显示由ApplicationStartup收集的启动步骤数据。需要使用SpringApplication进行配置BufferingApplicationStartup
threaddump 执行线程转储。

如果您的应用程序是Web应用程序(Spring MVC,Spring WebFlux或Jersey),则可以使用以下附加端点:

ID 描述
heapdump 返回hprof堆转储文件。
jolokia 通过HTTP暴露JMX bean(需要引入Jolokia,不适用于WebFlux)。需要引入依赖jolokia-core
logfile 返回日志文件的内容(如果已设置logging.file.namelogging.file.path属性)。支持使用HTTPRange标头来检索部分日志文件的内容。
prometheus 以Prometheus服务器可以抓取的格式公开指标。需要依赖micrometer-registry-prometheus

最常用的Endpoint

  • Health:监控状况
  • Metrics:运行时指标
  • Loggers:日志记录

1.3 Health Endpoint

健康检查端点,我们一般用于在云平台,平台会定时的检查应用的健康状况,我们就需要Health Endpoint可以为平台返回当前应用的一系列组件健康状况的集合。

重要的几点:

  • health endpoint返回的结果,应该是一系列健康检查后的一个汇总报告
  • 很多的健康检查默认已经自动配置好了,比如:数据库、redis等
  • 可以很容易的添加自定义的健康检查机制

设置显示health端点的详细信息

management:
endpoint:
health:
show-details: always

image-20230828142631189

{
"status": "UP",
"components": {
"db": {
"status": "UP",
"details": {
"database": "MySQL",
"validationQuery": "isValid()"
}
},
"diskSpace": {
"status": "UP",
"details": {
"total": 510769754112,
"free": 342642921472,
"threshold": 10485760,
"exists": true
}
},
"ping": {
"status": "UP"
}
}
}

1.4 Metrics Endpoint

提供详细的、层级的、空间指标信息,这些信息可以被pull(主动推送)或者push(被动获取)方式得到;

  • 通过Metrics对接多种监控系统
  • 简化核心Metrics开发
  • 添加自定义Metrics或者扩展已有Metrics

查看完整的监控指标信息 http://localhost:8080/actuator/metrics

{
"names": [
"hikaricp.connections",
"hikaricp.connections.acquire",
"hikaricp.connections.active",
"hikaricp.connections.creation",
"hikaricp.connections.idle",
"hikaricp.connections.max",
"hikaricp.connections.min",
"hikaricp.connections.pending",
"hikaricp.connections.timeout",
"hikaricp.connections.usage",
"http.server.requests",
"jdbc.connections.max",
"jdbc.connections.min",
"jvm.buffer.count",
"jvm.buffer.memory.used",
"jvm.buffer.total.capacity",
"jvm.classes.loaded",
"jvm.classes.unloaded",
"jvm.gc.live.data.size",
"jvm.gc.max.data.size",
"jvm.gc.memory.allocated",
"jvm.gc.memory.promoted",
"jvm.gc.pause",
"jvm.memory.committed",
"jvm.memory.max",
"jvm.memory.used",
"jvm.threads.daemon",
"jvm.threads.live",
"jvm.threads.peak",
"jvm.threads.states",
"logback.events",
"process.cpu.usage",
"process.start.time",
"process.uptime",
"system.cpu.count",
"system.cpu.usage"
]
}

image-20230828143224633

查看某一个监控指标的详细信息(例如查看hikaricp.connections的详细信息) http://localhost:8080/actuator/metrics/hikaricp.connections

{
"name": "hikaricp.connections",
"description": "Total connections",
"baseUnit": null,
"measurements": [
{
"statistic": "VALUE",
"value": 10
}
],
"availableTags": [
{
"tag": "pool",
"values": [
"HikariPool-1"
]
}
]
}

1.5 管理Endpoint

开启与禁用Endpoints

  • 默认所有的Endpoint除过shutdown都是开启的。
  • 需要开启或者禁用某个Endpoint 配置模式为 management.endpoint.endpointName.enabled = true
management:
endpoint:
beans: #可以写health、metrics等端点
enabled: true
  • 或者禁用所有的Endpoint然后手动开启指定的Endpoint
management:
endpoints:
enabled-by-default: false #禁用所有的端点(jconsole中也无法使用)
endpoint:
beans:
enabled: true #逐一开启端点
health:
enabled: true

1.6 定制Endpoint

1.6.1 定制health endpoint

1.编写MyComHealthIndicator类继承AbstractHealthIndicator

package com.atguigu.boot.acutuator.health;

import org.springframework.boot.actuate.health.AbstractHealthIndicator;
import org.springframework.boot.actuate.health.Health;
import org.springframework.boot.actuate.health.Status;
import org.springframework.stereotype.Component;

import java.util.HashMap;
import java.util.Map;

/**
* 自定义端点
* 类的后缀必须为HealthIndicator
*/
@Component
public class MyComHealthIndicator extends AbstractHealthIndicator {

/**
* 真实的检查方法 检查健康的方法
*/
@Override
protected void doHealthCheck(Health.Builder builder) throws Exception {
Map<String,Object> map = new HashMap<>();
// 检查完成
if(1 == 1){ //这里我们可以编写一个业务逻辑,例如数据库是否连接成功的业务逻辑
// builder.up(); //健康
builder.status(Status.UP);
map.put("count",1);
map.put("ms",100);
}else {
//builder.down();
builder.status(Status.OUT_OF_SERVICE);
map.put("err","连接超时");
map.put("ms",3000);
}
builder.withDetail("code",100)
.withDetails(map);

}
}

myCom = MyComHealthIndicator - HealthIndicator 所以类的名字一定要以HealthIndicator为后缀

image-20230828150633322

1.6.2 定制 info endpoint

方式一: 编写配置文件

info:
appName: boot-admin #应用名
version: 2.0.1 #版本
mavenProjectName: @project.artifactId@ #使用@@可以获取maven的pom文件值
mavenProjectVersion: @project.version@

image-20230828151416487

方式二: 编写InfoContributor

package com.atguigu.boot.acutuator.info;

import org.springframework.boot.actuate.info.Info;
import org.springframework.boot.actuate.info.InfoContributor;
import org.springframework.stereotype.Component;

import java.util.Collections;


@Component
public class AppInfoInfoContributor implements InfoContributor {

@Override
public void contribute(Info.Builder builder) {

builder.withDetail("msg","你好")
.withDetail("hello","atguigu")
.withDetails(Collections.singletonMap("world","666600"));
}
}

如果我们没有删除上面配置文件的配置,这时我们访问 http://localhost:8080/actuator/info 的时候会将配置文件和InfoContributor中的信息共同返回

image-20230828152006549

1.6.3 定制Metrics信息
class MyService{

Counter counter;

//构造器注入MeterRegistry
public MyService(MeterRegistry meterRegistry){
//访问 http://localhost:8080/actuator/metrics 的时候会多一个监控项myservice.method.running.counter
counter = meterRegistry.counter("myservice.method.running.counter");
}

public void hello() {
//调用hello方法的时候counter加1
counter.increment();
}
}

//也可以使用下面的方式
@Bean
MeterBinder queueSize(Queue queue) {
return (registry) -> Gauge.builder("queueSize", queue::size).register(registry);
}

1.7 新增Endpoint

package com.atguigu.boot.acutuator.endpoint;


import org.springframework.boot.actuate.endpoint.annotation.Endpoint;
import org.springframework.boot.actuate.endpoint.annotation.ReadOperation;
import org.springframework.boot.actuate.endpoint.annotation.WriteOperation;
import org.springframework.stereotype.Component;

import java.util.Collections;
import java.util.Map;


@Component
@Endpoint(id = "myservice")
public class MyServiceEndPoint {


@ReadOperation
public Map getDockerInfo(){
//端点的读操作 http://localhost:8080/actuator/myservice
return Collections.singletonMap("dockerInfo","docker started.....");
}

@WriteOperation
public void stopDocker(){
System.out.println("docker stopped.....");
}
}

image-20230828153914619

image-20230828153902598

1.8 可视化

github地址: https://github.com/codecentric/spring-boot-admin

项目文档: https://docs.spring-boot-admin.com/current/getting-started.html

1.创建一个新的springBoot项目用作服务端(场景只需要选择web场景即可)

image-20230828154818630

2.引入依赖

<dependency>
<groupId>de.codecentric</groupId>
<artifactId>spring-boot-admin-starter-server</artifactId>
<version>2.3.1</version>
</dependency>

3.在启动类上添加注解

@EnableAdminServer

4.修改以下端口号以防和业务的端口冲突

server.port=8888

5.访问http://localhost:8888

image-20230828155537932

6.客户端上引入依赖

<dependency>
<groupId>de.codecentric</groupId>
<artifactId>spring-boot-admin-starter-client</artifactId>
<version>2.3.1</version>
</dependency>

7.客户端的配置文件添加以下的配置

spring.boot.admin.client.url=http://localhost:8888 #服务端的地址
management.endpoints.web.exposure.include=*

8.重启一下客户端,然后访问服务端的localhost:8080

image-20230828160606081

点击应用墙,点击需要查看信息的应用就可以看到应用的详细信息

image-20230828160731314

六.原理解析

1.Profile功能

为了方便多环境适配,springboot简化了profile功能

  • 默认配置文件 application.yaml;任何时候都会加载

  • 指定环境配置文件 application-{env}.yaml

  • 激活指定环境

    • 配置文件激活,在默认配置文件 application.yaml 中添加spring.profiles.active=xxxx
    • 命令行激活:java -jar xxx.jar –spring.profiles.active=prod
      • 修改配置文件的任意值,命令行优先
  • 默认配置与环境配置同时生效

  • 同名配置项,profile配置优先

@Profile注解

//@Profile("prod")
//可以标注在类和方法上
//标注在类和方法上表示在prod环境下启用

外部配置源

常用:Java属性文件、YAML文件、环境变量、命令行参数;

配置文件查找位置

下面的优先级覆盖上面的优先级

(1) classpath 根路径

(2) classpath 根路径下config目录

(3) jar包当前目录

(4) jar包当前目录的config目录

(5) /config子目录的直接子目录

配置文件加载顺序:

  1.  当前jar包内部的application.properties和application.yml
  2.  当前jar包内部的application-{profile}.properties 和 application-{profile}.yml
  3.  引用的外部jar包的application.properties和application.yml
  4.  引用的外部jar包的application-{profile}.properties 和 application-{profile}.yml

总结: 指定环境优先,外部优先,后面的可以覆盖前面的同名配置项

]]>
后端 SpringBoot
SpringBoot整合EasyExcel /posts/38823.html 1.EasyExcel介绍

​ Java解析、生成Excel比较有名的框架有Apache poi、jxl。但他们都存在一个严重的问题就是非常的耗内存,poi有一套SAX模式的API可以一定程度的解决一些内存溢出的问题,但POI还是有一些缺陷,比如07版Excel解压缩以及解压后存储都是在内存中完成的,内存消耗依然很大。easyexcel重写了poi对07版Excel的解析,一个3M的excel用POI sax解析依然需要100M左右内存,改用easyexcel可以降低到几M,并且再大的excel也不会出现内存溢出;03版依赖POI的sax模式,在上层做了模型转换的封装,让使用者更加简单方便

官网地址:https://easyexcel.opensource.alibaba.com/

2.入门

2.1 引入依赖

<!-- https://mvnrepository.com/artifact/com.alibaba/easyexcel -->
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>easyexcel</artifactId>
<version>2.1.1</version>
</dependency>

2.2 创建与Excel表格对应的实体类,并在属性上加上注解

不用自己设置excel表中的表头信息,easyexcel会帮我们设置

package test;

import com.alibaba.excel.annotation.ExcelProperty;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;

/**
* @author GongChangjiang
* @version 1.0
* @Date 2023/4/14
* @Description 与Excel表格对应的实体类
*/
@Data
@AllArgsConstructor
@NoArgsConstructor
public class UserData {

/**
* 注解中value 设置的是表头的信息
* index 设置的是这个属性在excel表格中的索引位置 列索引
*/

@ExcelProperty(value = "用户编号", index = 0)
private int uid;

@ExcelProperty(value = "用户名称", index = 1)
private String username;


}

2.3 读写操作

1.写操作

package test;

import com.alibaba.excel.EasyExcel;

import java.util.ArrayList;
import java.util.List;

/**
* @author GongChangjiang
* @version 1.0
* @Date 2023/4/14
* @Description 测试对excel的操作
*/
public class TestEasyExcel {
public static void main(String[] args) {
//设置excel文件的路径和名称
String filename = "C:\\AlYun\\user.xlsx";

//构建一个数据的list集合,存放要保存在excel表中的数据
List<UserData> userDataList = new ArrayList<>();
userDataList.add(new UserData(1,"张三"));
userDataList.add(new UserData(2,"李四"));
userDataList.add(new UserData(3,"王五"));
userDataList.add(new UserData(4,"赵六"));

//调用方法实现写的操作
EasyExcel.write(filename,UserData.class)
.sheet("用户信息")
.doWrite(userDataList);

}
}

执行之后,空的excel表中会添加如下的数据

image-20230414131020923

2.读操作

1 .创建一个读操作的监听器

继承AnalysisEventListener T表示实体类

package test;

import com.alibaba.excel.context.AnalysisContext;
import com.alibaba.excel.event.AnalysisEventListener;

import java.util.Map;

/**
* @author GongChangjiang
* @version 1.0
* @Date 2023/4/14
* @Description 读操作的监听器
*/
public class ExcelLister extends AnalysisEventListener<UserData> {

/**
* 一行一行的读取 从第二行开始读取
*/
@Override
public void invoke(UserData data, AnalysisContext context) {
//打印输出读取到的数据
System.out.println(data);
}

/**
* 读取之后执行
*/
@Override
public void doAfterAllAnalysed(AnalysisContext context) {

}


/**
* 非必须实现的方法
* 此方法可以读取表头的信息
*/
@Override
public void invokeHeadMap(Map<Integer, String> headMap, AnalysisContext context) {
//打印读取到的表头信息
System.out.println("表头信息:"+headMap);
}
}
5.编写方法测试
package test;

import com.alibaba.excel.EasyExcel;

/**
* @author GongChangjiang
* @version 1.0
* @Date 2023/4/14
* @Description
*/
public class TestReadExcel {
public static void main(String[] args) {
//设置excel文件的路径和名称
String filename = "C:\\AlYun\\user.xlsx";
//调用方法实现读取操作
//new ExcelLister()自己创建的监听器
EasyExcel.read(filename,UserData.class,new ExcelLister())
.sheet()
.doRead();
}
}

3.监听器中实现对数据库的操作

解决listener中无法操作数据库的问题

3.1 通过构造器注入的方式

在进行读操作创建Listener的时候,注入mapper

package com.atguigu.yygh.cmn.listener;

import com.alibaba.excel.context.AnalysisContext;
import com.alibaba.excel.event.AnalysisEventListener;
import com.atguigu.yygh.cmn.mapper.DictMapper;
import com.atguigu.yygh.model.cmn.Dict;
import com.atguigu.yygh.vo.cmn.DictEeVo;
import org.springframework.beans.BeanUtils;


/**
* @author GongChangjiang
* @version 1.0
* @Date 2023/4/14
* @Description
*/
public class DictListener extends AnalysisEventListener<DictEeVo> {

private DictMapper dictMapper;

public DictListener(DictMapper dictMapper) {
this.dictMapper = dictMapper;
}

@Override
public void invoke(DictEeVo data, AnalysisContext context) {
//调用mapper 添加数据
Dict dict = new Dict();
BeanUtils.copyProperties(data,dict);
dictMapper.insert(dict);

}

@Override
public void doAfterAllAnalysed(AnalysisContext context) {

}
}

3.2 使用注解加属性注入的方式

@Component
+
@Autowired
]]>
后端 SpringBoot
SpringBoot整合Knife4j /posts/855.html 1.介绍

一句话介绍Knife4j: Swagger的增强版,界面更好看,功能更加的丰富

文档地址:https://doc.xiaominfo.com/

image-20230505222154471

image-20230505223651097

2.使用教程

2.1 引入依赖

<!--引入Knife4j的官方start包,该指南选择Spring Boot版本<3.0,开发者需要注意-->
<dependency>
<groupId>com.github.xiaoymin</groupId>
<artifactId>knife4j-openapi2-spring-boot-starter</artifactId>
<version>4.0.0</version>
</dependency>

2.2 编写配置类

package com.atguigu.common.config.knife4j;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import springfox.documentation.builders.ApiInfoBuilder;
import springfox.documentation.builders.ParameterBuilder;
import springfox.documentation.builders.PathSelectors;
import springfox.documentation.builders.RequestHandlerSelectors;
import springfox.documentation.schema.ModelRef;
import springfox.documentation.service.ApiInfo;
import springfox.documentation.service.Contact;
import springfox.documentation.service.Parameter;
import springfox.documentation.spi.DocumentationType;
import springfox.documentation.spring.web.plugins.Docket;
import springfox.documentation.swagger2.annotations.EnableSwagger2WebMvc;

import java.util.ArrayList;
import java.util.List;

/**
* @author GongChangjiang
* @version 1.0
* @Date 2023/5/5
* @Description knife4j配置信息
*/

@Configuration
@EnableSwagger2WebMvc
public class Knife4jConfig {

@Bean
public Docket adminApiConfig(){
List<Parameter> pars = new ArrayList<>();
ParameterBuilder tokenPar = new ParameterBuilder();
tokenPar.name("token")
.description("用户token")
.defaultValue("")
.modelRef(new ModelRef("string"))
.parameterType("header")
.required(false)
.build();
pars.add(tokenPar.build());
//添加head参数end

Docket adminApi = new Docket(DocumentationType.SWAGGER_2)
.groupName("adminApi")
.apiInfo(adminApiInfo())
.select()
//只显示admin路径下的页面
.apis(RequestHandlerSelectors.basePackage("com.atguigu"))
.paths(PathSelectors.regex("/admin/.*"))
.build()
.globalOperationParameters(pars);
return adminApi;
}

private ApiInfo adminApiInfo(){

return new ApiInfoBuilder()
.title("后台管理系统-API文档")
.description("本文档描述了后台管理系统微服务接口定义")
.version("1.0")
.contact(new Contact("JasonGong", "https://gong-changjiang.gitee.io/", "2602183349@qq.com"))
.build();
}
}

2.3 使用

package com.atguigu.auth.controller;

import com.atguigu.auth.service.SysRoleService;
import com.atguigu.common.result.Result;
import com.atguigu.model.system.SysRole;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.CrossOrigin;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

import java.util.List;

/**
* @author GongChangjiang
* @version 1.0
* @Date 2023/5/5
* @Description 角色的控制器方法
*/
@Api(tags = "角色管理的接口")
@RestController
@RequestMapping("/admin/system/sysRole")
@CrossOrigin
public class SysRoleController {

@Autowired
private SysRoleService sysRoleService;


/**
* 查询所有的方法
*/
@ApiOperation("查询所有角色的接口")
@GetMapping("/findAll")
public Result<List<SysRole>> findAll(){
List<SysRole> sysRoles = sysRoleService.list();
return Result.ok(sysRoles);
}

}

2.4接口文档的访问地址

# 端口号根据项目的端口号进行改变
http://localhost:8080/doc.html

image-20230505223433369

]]>
后端 SpringBoot
SpringBoot整合Logback日志 /posts/64205.html 1.创建一个SpringBoot的工程

2.在resources目录下创建logback-spring.xml的配置文件

创建的时候要修改日志输出的路径

日志的级别根据需要自己修改

级别:OFF、FATAL、ERROR、WARN、INFO、DEBUG、ALL

<?xml version="1.0" encoding="UTF-8"?>
<configuration scan="true" scanPeriod="10 seconds">
<!-- 日志级别从低到高分为TRACE < DEBUG < INFO < WARN < ERROR < FATAL,如果设置为WARN,则低于WARN的信息都不会输出 -->
<!-- scan:当此属性设置为true时,配置文件如果发生改变,将会被重新加载,默认值为true -->
<!-- scanPeriod:设置监测配置文件是否有修改的时间间隔,如果没有给出时间单位,默认单位是毫秒。当scan为true时,此属性生效。默认的时间间隔为1分钟。 -->
<!-- debug:当此属性设置为true时,将打印出logback内部日志信息,实时查看logback运行状态。默认值为false。 -->

<contextName>logback</contextName>
<!-- name的值是变量的名称,value的值时变量定义的值。通过定义的值会被插入到logger上下文中。定义变量后,可以使“${}”来使用变量。 -->
<property name="log.path" value="C:/AlYun/log" />

<!-- 彩色日志 -->
<!-- 配置格式变量:CONSOLE_LOG_PATTERN 彩色日志格式 -->
<!-- magenta:洋红 -->
<!-- boldMagenta:粗红-->
<!-- cyan:青色 -->
<!-- white:白色 -->
<!-- magenta:洋红 -->
<property name="CONSOLE_LOG_PATTERN"
value="%yellow(%date{yyyy-MM-dd HH:mm:ss}) |%highlight(%-5level) |%blue(%thread) |%blue(%file:%line) |%green(%logger) |%cyan(%msg%n)"/>


<!--输出到控制台-->
<appender name="CONSOLE" class="ch.qos.logback.core.ConsoleAppender">
<!--此日志appender是为开发使用,只配置最底级别,控制台输出的日志级别是大于或等于此级别的日志信息-->
<!-- 例如:如果此处配置了INFO级别,则后面其他位置即使配置了DEBUG级别的日志,也不会被输出 -->
<filter class="ch.qos.logback.classic.filter.ThresholdFilter">
<level>INFO</level>
</filter>
<encoder>
<Pattern>${CONSOLE_LOG_PATTERN}</Pattern>
<!-- 设置字符集 -->
<charset>UTF-8</charset>
</encoder>
</appender>


<!--输出到文件-->

<!-- 时间滚动输出 level为 INFO 日志 -->
<appender name="INFO_FILE" class="ch.qos.logback.core.rolling.RollingFileAppender">
<!-- 正在记录的日志文件的路径及文件名 -->
<file>${log.path}/log_info.log</file>
<!--日志文件输出格式-->
<encoder>
<pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{50} - %msg%n</pattern>
<charset>UTF-8</charset>
</encoder>
<!-- 日志记录器的滚动策略,按日期,按大小记录 -->
<rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
<!-- 每天日志归档路径以及格式 -->
<fileNamePattern>${log.path}/info/log-info-%d{yyyy-MM-dd}.%i.log</fileNamePattern>
<timeBasedFileNamingAndTriggeringPolicy class="ch.qos.logback.core.rolling.SizeAndTimeBasedFNATP">
<maxFileSize>100MB</maxFileSize>
</timeBasedFileNamingAndTriggeringPolicy>
<!--日志文件保留天数-->
<maxHistory>15</maxHistory>
</rollingPolicy>
<!-- 此日志文件只记录info级别的 -->
<filter class="ch.qos.logback.classic.filter.LevelFilter">
<level>INFO</level>
<onMatch>ACCEPT</onMatch>
<onMismatch>DENY</onMismatch>
</filter>
</appender>

<!-- 时间滚动输出 level为 WARN 日志 -->
<appender name="WARN_FILE" class="ch.qos.logback.core.rolling.RollingFileAppender">
<!-- 正在记录的日志文件的路径及文件名 -->
<file>${log.path}/log_warn.log</file>
<!--日志文件输出格式-->
<encoder>
<pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{50} - %msg%n</pattern>
<charset>UTF-8</charset> <!-- 此处设置字符集 -->
</encoder>
<!-- 日志记录器的滚动策略,按日期,按大小记录 -->
<rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
<fileNamePattern>${log.path}/warn/log-warn-%d{yyyy-MM-dd}.%i.log</fileNamePattern>
<timeBasedFileNamingAndTriggeringPolicy class="ch.qos.logback.core.rolling.SizeAndTimeBasedFNATP">
<maxFileSize>100MB</maxFileSize>
</timeBasedFileNamingAndTriggeringPolicy>
<!--日志文件保留天数-->
<maxHistory>15</maxHistory>
</rollingPolicy>
<!-- 此日志文件只记录warn级别的 -->
<filter class="ch.qos.logback.classic.filter.LevelFilter">
<level>warn</level>
<onMatch>ACCEPT</onMatch>
<onMismatch>DENY</onMismatch>
</filter>
</appender>


<!-- 时间滚动输出 level为 ERROR 日志 -->
<appender name="ERROR_FILE" class="ch.qos.logback.core.rolling.RollingFileAppender">
<!-- 正在记录的日志文件的路径及文件名 -->
<file>${log.path}/log_error.log</file>
<!--日志文件输出格式-->
<encoder>
<pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{50} - %msg%n</pattern>
<charset>UTF-8</charset> <!-- 此处设置字符集 -->
</encoder>
<!-- 日志记录器的滚动策略,按日期,按大小记录 -->
<rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
<fileNamePattern>${log.path}/error/log-error-%d{yyyy-MM-dd}.%i.log</fileNamePattern>
<timeBasedFileNamingAndTriggeringPolicy class="ch.qos.logback.core.rolling.SizeAndTimeBasedFNATP">
<maxFileSize>100MB</maxFileSize>
</timeBasedFileNamingAndTriggeringPolicy>
<!--日志文件保留天数-->
<maxHistory>15</maxHistory>
</rollingPolicy>
<!-- 此日志文件只记录ERROR级别的 -->
<filter class="ch.qos.logback.classic.filter.LevelFilter">
<level>ERROR</level>
<onMatch>ACCEPT</onMatch>
<onMismatch>DENY</onMismatch>
</filter>
</appender>

<!--
<logger>用来设置某一个包或者具体的某一个类的日志打印级别、以及指定<appender>。
<logger>仅有一个name属性,
一个可选的level和一个可选的addtivity属性。
name:用来指定受此logger约束的某一个包或者具体的某一个类。
level:用来设置打印级别,大小写无关:TRACE, DEBUG, INFO, WARN, ERROR, ALL 和 OFF,
如果未设置此属性,那么当前logger将会继承上级的级别。
-->
<!--
使用mybatis的时候,sql语句是debug下才会打印,而这里我们只配置了info,所以想要查看sql语句的话,有以下两种操作:
第一种把<root level="INFO">改成<root level="DEBUG">这样就会打印sql,不过这样日志那边会出现很多其他消息
第二种就是单独给mapper下目录配置DEBUG模式,代码如下,这样配置sql语句会打印,其他还是正常DEBUG级别:
-->
<!--开发环境:打印控制台-->
<springProfile name="dev">
<!--可以输出项目中的debug日志,包括mybatis的sql日志-->
<logger name="com.guli" level="INFO" />

<!--
root节点是必选节点,用来指定最基础的日志输出级别,只有一个level属性
level:用来设置打印级别,大小写无关:TRACE, DEBUG, INFO, WARN, ERROR, ALL 和 OFF,默认是DEBUG
可以包含零个或多个appender元素。
-->
<root level="INFO">
<appender-ref ref="CONSOLE" />
<appender-ref ref="INFO_FILE" />
<appender-ref ref="WARN_FILE" />
<appender-ref ref="ERROR_FILE" />
</root>
</springProfile>


<!--生产环境:输出到文件-->
<springProfile name="pro">

<root level="INFO">
<appender-ref ref="CONSOLE" />
<appender-ref ref="DEBUG_FILE" />
<appender-ref ref="INFO_FILE" />
<appender-ref ref="ERROR_FILE" />
<appender-ref ref="WARN_FILE" />
</root>
</springProfile>

</configuration>

3.日志的样式如下图所示

image-20230412171350769

]]>
后端 SpringBoot
SpringBoot整合MongoDB /posts/12929.html 1.介绍

​ MongoDB是一个基于分布式文件存储的数据库。由C++语言编写。旨在为WEB应用提供可扩展的高性能数据存储解决方案。
MongoDB是一个介于关系数据库和非关系数据库之间的产品,是非关系数据库当中功能最丰富,最像关系数据库的。它支持的数据结构非常松散,是类似json的bson格式,因此可以存储比较复杂的数据类型。Mongo最大的特点是它支持的查询语言非常强大,其语法有点类似于面向对象的查询语言,几乎可以实现类似关系数据库单表查询的绝大部分功能,而且还支持对数据建立索引。

适用场景
1、网站数据:Mongo非常适合实时的插入,更新与查询,并具备网站实时数据存储所需的复制及高度伸缩性。
2、缓存:由于性能很高,Mongo也适合作为信息基础设施的缓存层。在系统重启之后,由M ongo搭建的持久化缓存层可以避免下层的数据源过载。
3、大尺寸,低价值的数据:使用传统的关系型数据库存储一些数据时可能会比较昂贵, 在此之前,很多时候程序员往往会选择传统的文件进行存储。
4、高伸缩性的场景:Mongo非常适合由数十或数百台服务器组成的数据库。Mongo的路线图中已经包含对Map Reduce弓摩的内置支持。
5、用于对象及 JSON数据的存储:Mongo的BSON数据格式非常适合文档化格式的存储 及查询。
不适用场合
1、高度事务性的系统:例如银行或会计系统。传统的关系型数据库目前还是更适用于需要大量原子性复杂事务的应用程序。
2、传统的商业智能应用:针对特定问题的BI数据库会对产生高度优化的查询方式。对于此类应用,数据仓库可能是更合适的选择。

中文文档:https://docs.mongoing.com/

2.入门

​ spring-data-mongodb提供了MongoTemplate与MongoRepository两种方式访问mongodb,MongoRepository操作简单,MongoTemplate操作灵活,我们在项目中可以灵活适用这两种方式操作mongodb,MongoRepository的缺点是不够灵活,MongoTemplate正好可以弥补不足

2.1引入依赖

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

2.2 配置文件中添加MongoDB的配置

spring.data.mongodb.uri=mongodb://localhost:27017/test

2.3 创建对应的实体类

@Data
@Document("User") //指定实体类对应的MongoDB集合的名称
public class User {
@Id //自动生成的主键ID
private String id;
private String name;
private Integer age;
private String email;
private String createDate;
}

2.4 常用的方法

//常用方法
mongoTemplate.findAll(User.class); //查询User文档的全部数据
mongoTemplate.findById(<id>, User.class); //查询User文档id为id的数据
mongoTemplate.find(query, User.class); //根据query内的查询条件查询
mongoTemplate.upsert(query, update, User.class);// 修改
mongoTemplate.remove(query, User.class);// 删除
mongoTemplate.insert(User);//新增
Query对象
1、创建一个query对象(用来封装所有条件对象),再创建一个criteria对象(用来构建条件)

2、精准条件:criteria.and(“key”).is(“条件”)
模糊条件:criteria.and(“key”).regex(“条件”)

3、封装条件:query.addCriteria(criteria)

4、大于(创建新的criteria):Criteria gt = Criteria.where(“key”).gt(“条件”)
小于(创建新的criteria):Criteria lt = Criteria.where(“key”).lt(“条件”)

5、Query.addCriteria(new Criteria().andOperator(gt,lt));

6、一个query中只能有一个andOperator() 其参数也可以是Criteria数组

7、排序 :query.with(new Sort(Sort.Direction.ASC, "age"). and(new Sort(Sort.Direction.DESC, "date")))
主要注解:

@Document,文档是 MongoDB 中最基本的数据单元,由键值对组成,类似于 JSON 格式,可以存储不同字段,字段的值可以包括其他文档,数组和文档数组。

@Id(主键):用来将成员变量的值映射为文档的_id的值

@Indexed(索引): 索引是一种特殊的数据结构,存储在一个易于遍历读取的数据集合中,能够对数据库文档中的数据进行排序。索引能极大提高文档查询效率,如果没有设置索引,MongoDB 会遍历集合中的整个文档,选取符合查询条件的文档记录。这种查询效率是非常低的。

@Field(字段): 文档中的字段,类似于 MySql 中的列。

@Aggregation(聚合): 聚合主要用于数据处理,例如统计平均值、求和等。

示例代码

2.4.1 使用MongoTemplate的方式访问MongoDB

@SpringBootTest
class DemomogoApplicationTests {

@Autowired
private MongoTemplate mongoTemplate;

//添加
@Test
public void createUser() {
User user = new User();
user.setAge(20);
user.setName("test");
user.setEmail("4932200@qq.com");
User user1 = mongoTemplate.insert(user);
System.out.println(user1);
}

//查询所有
@Test
public void findUser() {
List<User> userList = mongoTemplate.findAll(User.class);
System.out.println(userList);
}

//根据id查询
@Test
public void getById() {
User user =
mongoTemplate.findById("5ffbfa2ac290f356edf9b5aa", User.class);
System.out.println(user);
}

//条件查询
@Test
public void findUserList() {
Query query = new Query(Criteria
.where("name").is("test")
.and("age").is(20));
List<User> userList = mongoTemplate.find(query, User.class);
System.out.println(userList);
}

//模糊查询
@Test
public void findUsersLikeName() {
String name = "est";
String regex = String.format("%s%s%s", "^.*", name, ".*$");
Pattern pattern = Pattern.compile(regex, Pattern.CASE_INSENSITIVE);
Query query = new Query(Criteria.where("name").regex(pattern));
List<User> userList = mongoTemplate.find(query, User.class);
System.out.println(userList);
}

//分页查询
@Test
public void findUsersPage() {
String name = "est";
int pageNo = 1;
int pageSize = 10;

//分页查询的条件
Query query = new Query();
String regex = String.format("%s%s%s", "^.*", name, ".*$");
Pattern pattern = Pattern.compile(regex, Pattern.CASE_INSENSITIVE);
query.addCriteria(Criteria.where("name").regex(pattern));
//总记录数
int totalCount = (int) mongoTemplate.count(query, User.class);
//分页查询
List<User> userList = mongoTemplate.find(query.skip((pageNo - 1) * pageSize).limit(pageSize), User.class);

//将分页的总记录数和分页数据封装在一个map集合中
Map<String, Object> pageMap = new HashMap<>();
pageMap.put("list", userList);
pageMap.put("totalCount",totalCount);
System.out.println(pageMap);
}

//修改
@Test
public void updateUser() {
User user = mongoTemplate.findById("5ffbfa2ac290f356edf9b5aa", User.class);
user.setName("test_1");
user.setAge(25);
user.setEmail("493220990@qq.com");
Query query = new Query(Criteria.where("_id").is(user.getId()));
Update update = new Update();
update.set("name", user.getName());
update.set("age", user.getAge());
update.set("email", user.getEmail());
UpdateResult result = mongoTemplate.upsert(query, update, User.class);
//影响的行数
long count = result.getModifiedCount();
System.out.println(count);
}

//删除操作
@Test
public void delete() {
Query query =
new Query(Criteria.where("_id").is("5ffbfa2ac290f356edf9b5aa"));
DeleteResult result = mongoTemplate.remove(query, User.class);
long count = result.getDeletedCount();
System.out.println(count);
}
}

2.4.2 使用MongoRepository的方式访问MongoDB

a.创建UserRepository类
package com.atguigu.mongodb.repository;

import com.atguigu.mongodb.entity.User;
import org.springframework.data.mongodb.repository.MongoRepository;
import org.springframework.stereotype.Repository;

import java.util.List;

@Repository
public interface UserRepository extends MongoRepository<User, String> {

}
b.测试使用
@SpringBootTest
class DemomogoApplicationTests1 {

@Autowired
private UserRepository userRepository;

//添加
@Test
public void createUser() {
User user = new User();
user.setAge(20);
user.setName("张三");
user.setEmail("3332200@qq.com");
User user1 = userRepository.save(user);
}

//查询所有
@Test
public void findUser() {
List<User> userList = userRepository.findAll();
System.out.println(userList);
}

//id查询
@Test
public void getById() {
User user = userRepository.findById("5ffbfe8197f24a07007bd6ce").get();
System.out.println(user);
}

//条件查询
@Test
public void findUserList() {
//查询条件 名字是张三 年龄是20
User user = new User();
user.setName("张三");
user.setAge(20);
Example<User> userExample = Example.of(user);
List<User> userList = userRepository.findAll(userExample);
System.out.println(userList);
}

//模糊查询
@Test
public void findUsersLikeName() {
//创建匹配器,即如何使用查询条件
ExampleMatcher matcher = ExampleMatcher.matching() //构建对象
.withStringMatcher(ExampleMatcher.StringMatcher.CONTAINING) //改变默认字符串匹配方式:模糊查询
.withIgnoreCase(true); //改变默认大小写忽略方式:忽略大小写
User user = new User();
user.setName("三");
Example<User> userExample = Example.of(user, matcher);
List<User> userList = userRepository.findAll(userExample);
System.out.println(userList);
}

//分页查询
@Test
public void findUsersPage() {
//根据年龄降序
Sort sort = Sort.by(Sort.Direction.DESC, "age");
//0为第一页
Pageable pageable = PageRequest.of(0, 10, sort);
//创建匹配器,即如何使用查询条件
ExampleMatcher matcher = ExampleMatcher.matching() //构建对象
.withStringMatcher(ExampleMatcher.StringMatcher.CONTAINING) //改变默认字符串匹配方式:模糊查询
.withIgnoreCase(true); //改变默认大小写忽略方式:忽略大小写
User user = new User();
user.setName("三");//查询名字中包含 '三' 的数据
Example<User> userExample = Example.of(user, matcher);
//创建实例
Example<User> example = Example.of(user, matcher);
Page<User> pages = userRepository.findAll(example, pageable);
System.out.println(pages);
}

//修改
@Test
public void updateUser() {
User user = userRepository.findById("5ffbfe8197f24a07007bd6ce").get();
user.setName("张三_1");
user.setAge(25);
user.setEmail("883220990@qq.com");
User save = userRepository.save(user);//有id值就是修改 m
System.out.println(save);
}

//删除
@Test
public void delete() {
userRepository.deleteById("5ffbfe8197f24a07007bd6ce");
}
}
]]>
后端 SpringBoot
SpringBoot整合Redis /posts/13813.html 1.Redis的介绍

​ Redis是一个开源(BSD许可),内存存储的数据结构服务器,可用作数据库,高速缓存和消息队列代理。它支持字符串、哈希表、列表、集合、有序集合,位图,hyperloglogs等数据类型。内置复制、Lua脚本、LRU收回、事务以及不同级别磁盘持久化功能,同时通过Redis Sentinel提供高可用,通过Redis Cluster提供自动分区。

中文文档:https://www.redis.net.cn/

2.入门

2.1 引入依赖

<!-- redis -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>

<!-- spring2.X集成redis所需common-pool2-->
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-pool2</artifactId>
<version>2.6.0</version>
</dependency>

2.2 添加配置类

package com.atguigu.yygh.common.config;

import com.fasterxml.jackson.annotation.JsonAutoDetect;
import com.fasterxml.jackson.annotation.PropertyAccessor;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.springframework.cache.CacheManager;
import org.springframework.cache.annotation.EnableCaching;
import org.springframework.cache.interceptor.KeyGenerator;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.cache.RedisCacheConfiguration;
import org.springframework.data.redis.cache.RedisCacheManager;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.serializer.Jackson2JsonRedisSerializer;
import org.springframework.data.redis.serializer.RedisSerializationContext;
import org.springframework.data.redis.serializer.RedisSerializer;
import org.springframework.data.redis.serializer.StringRedisSerializer;

import java.lang.reflect.Method;
import java.time.Duration;

/**
* @author GongChangjiang
* @version 1.0
* @Date 2023/4/14
* @Description
*/
@Configuration
@EnableCaching
public class RedisConfig {

/**
* 自定义key规则
*/
@Bean
public KeyGenerator keyGenerator() {
return new KeyGenerator() {
@Override
public Object generate(Object target, Method method, Object... params) {
StringBuilder sb = new StringBuilder();
sb.append(target.getClass().getName());
sb.append(method.getName());
for (Object obj : params) {
sb.append(obj.toString());
}
return sb.toString();
}
};
}

/**
* 设置RedisTemplate规则
*
*/
@Bean
public RedisTemplate<Object, Object> redisTemplate(RedisConnectionFactory redisConnectionFactory) {
RedisTemplate<Object, Object> redisTemplate = new RedisTemplate<>();
redisTemplate.setConnectionFactory(redisConnectionFactory);
Jackson2JsonRedisSerializer jackson2JsonRedisSerializer = new Jackson2JsonRedisSerializer(Object.class);

//解决查询缓存转换异常的问题
ObjectMapper om = new ObjectMapper();
// 指定要序列化的域,field,get和set,以及修饰符范围,ANY是都有包括private和public
om.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY);
// 指定序列化输入的类型,类必须是非final修饰的,final修饰的类,比如String,Integer等会跑出异常
om.enableDefaultTyping(ObjectMapper.DefaultTyping.NON_FINAL);
jackson2JsonRedisSerializer.setObjectMapper(om);

//序列号key value
redisTemplate.setKeySerializer(new StringRedisSerializer());
redisTemplate.setValueSerializer(jackson2JsonRedisSerializer);
redisTemplate.setHashKeySerializer(new StringRedisSerializer());
redisTemplate.setHashValueSerializer(jackson2JsonRedisSerializer);

redisTemplate.afterPropertiesSet();
return redisTemplate;
}

/**
* 设置CacheManager缓存规则
* 缓存的时间
* 乱码的处理
*/
@Bean
public CacheManager cacheManager(RedisConnectionFactory factory) {
RedisSerializer<String> redisSerializer = new StringRedisSerializer();
Jackson2JsonRedisSerializer jackson2JsonRedisSerializer = new Jackson2JsonRedisSerializer(Object.class);

//解决查询缓存转换异常的问题
ObjectMapper om = new ObjectMapper();
om.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY);
om.enableDefaultTyping(ObjectMapper.DefaultTyping.NON_FINAL);
jackson2JsonRedisSerializer.setObjectMapper(om);

// 配置序列化(解决乱码的问题),过期时间600秒
RedisCacheConfiguration config = RedisCacheConfiguration.defaultCacheConfig()
.entryTtl(Duration.ofSeconds(600))
.serializeKeysWith(RedisSerializationContext.SerializationPair.fromSerializer(redisSerializer))
.serializeValuesWith(RedisSerializationContext.SerializationPair.fromSerializer(jackson2JsonRedisSerializer))
.disableCachingNullValues();

RedisCacheManager cacheManager = RedisCacheManager.builder(factory)
.cacheDefaults(config)
.build();
return cacheManager;
}

}

说明:
@EnableCaching:标记注解 @EnableCaching,开启缓存,并配置Redis缓存管理器。@EnableCaching 注释触发后置处理器, 检查每一个Spring bean 的 public 方法是否存在缓存注解。如果找到这样的一个注释, 自动创建一个代理拦截方法调用和处理相应的缓存行为。

2.3 配置文件中加入redis的配置

application.properties

spring.redis.host=localhost
spring.redis.port=6379
spring.redis.database= 0
spring.redis.timeout=1800000

spring.redis.lettuce.pool.max-active=20
spring.redis.lettuce.pool.max-wait=-1
#最大阻塞等待时间(负数表示没限制)
spring.redis.lettuce.pool.max-idle=5
spring.redis.lettuce.pool.min-idle=0

2.4 使用Spring Cache + Redis执行缓存操作

2.1.2 @Cacheable

​ 根据方法对其返回结果进行缓存,下次请求时,如果缓存存在,则直接读取缓存数据返回;如果缓存不存在,则执行方法,并把返回的结果存入缓存中。一般用在查询方法上。

属性/方法名 解释
value 缓存名,必填,它指定了你的缓存存放在哪块命名空间
cacheNames 与 value 差不多,二选一即可
key 可选属性,可以使用 SpEL 标签自定义缓存的key

2.1.2 @CachePut

​ 使用该注解标志的方法,每次都会执行,并将结果存入指定的缓存中。其他方法可以直接从响应的缓存中读取缓存数据,而不需要再去查询数据库。一般用在新增方法上。

属性/方法名 解释
value 缓存名,必填,它指定了你的缓存存放在哪块命名空间
cacheNames 与 value 差不多,二选一即可
key 可选属性,可以使用 SpEL 标签自定义缓存的key

2.1.3 @CacheEvict

​ 使用该注解标志的方法,会清空指定的缓存。一般用在更新或者删除方法上

属性/方法名 解释
value 缓存名,必填,它指定了你的缓存存放在哪块命名空间
cacheNames 与 value 差不多,二选一即可
key 可选属性,可以使用 SpEL 标签自定义缓存的key
allEntries 是否清空所有缓存,默认为 false。如果指定为 true,则方法调用后将立即清空所有的缓存
beforeInvocation 是否在方法执行前就清空,默认为 false。如果指定为 true,则在方法执行前就会清空缓存

3.示例

下面的第一个方法是查询数据位list集合

​ @Cacheable(value = “dict”,keyGenerator = “keyGenerator”) 对方法的结果进行缓存

​ value属性表示key的前缀

​ keyGenerator表示key的生成规则,生成规则在配置文件中配置,这里我们使用的是方法的全类名作为key的后缀

第二个方法是添加数据 添加数据会造成数据库中数据的变化 我们要清除缓存

​ @CacheEvict(value = “dict”,allEntries = true) 清空指定的缓存

​ value属性表示清空以dict为前缀的所有缓存

​ allEntries 属性表示是否清空所有缓存,默认为 false。如果指定为 true,则方法调用后将立即清空所有的缓存

@Override
@Cacheable(value = "dict",keyGenerator = "keyGenerator")
public List<Dict> findByParentId(Long parentId) {
LambdaQueryWrapper<Dict> queryWrapper = new LambdaQueryWrapper<>();
queryWrapper.eq(Dict::getParentId, parentId);
List<Dict> dictList = dictMapper.selectList(queryWrapper);
for (Dict dict : dictList) {
dict.setHasChildren(this.isHasChildren(dict.getId()));
}
return dictList;
}

@CacheEvict(value = "dict",allEntries = true)
public void importDictData(MultipartFile file) {
try {
EasyExcel.read(file.getInputStream(),DictEeVo.class,new DictListener(dictMapper)).sheet().doRead();
} catch (IOException e) {
e.printStackTrace();
}
}



key的生成规则如下图:

image-20230414152035347

]]>
后端 SpringBoot
SpringCloud相关资料 /posts/62429.html SpringCloud相关资料

一.Eureka注册中心、Ribbon负载均衡、Nacos注册中心

二.Nacos配置中心、Feign远程调用、Gateway服务网关

三.Docker容器化技术

四.RabbitMQ

]]>
后端 SpringCloud
VMWare虚拟机安装Linux教程 /posts/47407.html 1.下载CentOs镜像文件

这里下载的是7版本的 如果有其他的需要 自行安装其他的版本

下载完成之后 记录好安装的位置

阿里镜像下载地址:https://mirrors.aliyun.com/centos/7/isos/x86_64/?spm=a2c6h.25603864.0.0.4eab4511uQRsgc

image-20230510224239950

2.下载VMWare 虚拟机

这里下载的是17版本的VMWare ,可以免费试用30天,需要许可证的可自行百度搜索

官网下载地址: https://download3.vmware.com/software/WKST-1701-WIN/VMware-workstation-full-17.0.1-21139696.exe

安装步骤:

image-20230510222210848

3.配置虚拟机 (以CentOs为例)

CentOs的不同版本在VmWare上面的配置略有不同

CentOs 7.6版本的配置教程

CentOs 8.1版本的配置教程

连接Linux 的远程工具

其实本地安装Linux大可不必安装远程控制工具 直接进入Linux里面控制即可

image-20230510225825430

XShell6的安装和配置

1.安装

下载地址: https://wwvc.lanzouy.com/ipF2w0v6np8j

下载解压之后双击.exe文件 点击下一步安装 直到安装完成

2.配置

1.获取Linux的ip地址信息 并记录ip的信息

# 命令 
# 注意这里没有写错 就是ifconfig 不是ipconfig,ipconfig是windows系统的查看ip的命令
ifconfig

image-20230510230114550

2.打开xShell工具

2.1创建会话

image-20230510230758405

2.2 选择会话并连接

image-20230510230937843

image-20230510231329208

image-20230510231432103

2.3 用户名和密码正确之后就连接成功

连接成功如下图

image-20230510231607314

完整的配置过程

image-20230510232029231

FinalShell的下载地址

下载地址: http://www.hostbuf.com/downloads/finalshell_install.exe

Tips:

​ 这里我们没有设置Linux的静态IP,电脑每次网络变化之后,IP地址会发生变化,这时我们需要获取最新的IP地址,重新执行以上的连接步骤。如果想要IP地址固定,我们可以设置静态IP,设置静态的IP的方法如下:

Linux设置静态IP | The Blog (gitee.io)

]]>
后端 VMWare
Thymeleaf教程 /posts/54835.html Thymeleaf 是一个服务器端 Java 模板引擎,能够处理 HTML、XML、CSS、JAVASCRIPT 等模板文件。Thymeleaf 模板可以直接当作静态原型来使用,它主要目标是为开发者的开发工作流程带来优雅的自然模板,也是 Java 服务器端 HTML5 开发的理想选择。

image-20230706152329777

教程: https://www.thymeleaf.org

项目集成Thymeleaf

1.导入依赖

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

2.添加配置

spring.thymeleaf.cache=false #关闭缓存(后面使用devtools进行热更新也要关闭缓存)

3.在HTML文件中引入名称空间

<html xmlns:th="http://www.thymeleaf.org">
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head>
<title>标题</title>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
</head>
<body>
<p th:text="#{home.welcome}">Welcome to our grocery store!</p>
</body>
</html>

Thymeleaf

devtools热部署

Tips:在使用Thymeleaf的时候修改页面我们要频繁的重启项目,非常浪费时间,这时我们可以导入devtools进行热部署

<!--devtools-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-devtools</artifactId>
<optional>true</optional>
</dependency>

导入之后重启项目,会显示如下的标识

image-20230706154112026

每次修改页面之后,我们只需要在当前文件下重新build一下(Ctrl+Shift+F9),就可以实现热更新

image-20230706161401528

使用文档

英文文档: https://www.thymeleaf.org/doc/tutorials/3.1/usingthymeleaf.html

博客文章: https://fanlychie.github.io/post/thymeleaf.html

中文PDF文档

]]>
前端 Thymeleaf
使用Robot类编写自动化脚本 /posts/30127.html 一.概述

​ Java中Robot类位于java.awt.Robot,该类用于为测试自动化,自运行演示程序和其他需要控制鼠标和键盘的应用程序生成本机系统输入事件,Robot类的主要目的是便于Java平台实现自动测试

二.基本API

方法名 使用说明
delay(n) 延迟电脑操作n毫秒,类似于Thread.sleep()
keyPress() 模拟手动按下电脑键盘上的某个键
keyRelease() 模拟手动松开电脑键盘上的某个键(与keyPress()对应,按下一个键必须松开这个键)
mouseMove(x,y) 将鼠标移动到指定的x,y位置
mousePress() 按下鼠标上的某个键
mouseRelease() 松开鼠标上的某个键
getPixelColor(x,y) 获取指定坐标处的像素颜色
mouseWheel(int wheelAmt) 鼠标滚动(参数小于0,表示向上滚动;参数大于0,表示向下滚动)

三.基本使用

1.模拟按键

import java.awt.*;
import java.awt.event.KeyEvent;
import java.util.Random;

public class Test {
public static void main(String[] args) throws AWTException {
Robot robot = new Robot();
//延时函数,延时5秒
robot.delay(5000);
//模拟按一下K键
robot.keyPress(KeyEvent.VK_K);
//产生一个随机的时间(0.2 ~ 0.4秒之间)
Random random = new Random();
double randomValue = random.nextDouble() * 0.2 + 0.2;
robot.delay((int)randomValue * 1000);
//释放k键
robot.keyRelease(KeyEvent.VK_K);
}
}

2.模拟鼠标

import java.awt.*;
import java.awt.event.InputEvent;

public class Test {
public static void main(String[] args) throws AWTException {
Robot robot = new Robot();
//鼠标的移动
robot.mouseMove(800, 200);
//按下左键
robot.mousePress(InputEvent.BUTTON1_DOWN_MASK);
//弹起左键
robot.mouseRelease(InputEvent.BUTTON1_DOWN_MASK);
//鼠标滚轮(大于1 向上滚动)
robot.mouseWheel(1);
}
}

3.获取颜色

import java.awt.*;

public class Test {
public static void main(String[] args) throws AWTException {
Robot robot = new Robot();
Color color = robot.getPixelColor(520, 70);
}
}
]]>
后端 脚本
代码注释模板 /posts/1727.html 完整的模板大全:佛祖保佑永无BUG、神兽护体、注释图案 · OBKoro1/koro1FileHeader Wiki (github.com)

在线生成代码注释模板:http://patorjk.com/software/taag/

image-20240113150435680


██╗ █████╗ ███████╗ ██████╗ ███╗ ██╗███████╗ ██████╗ ██████╗ ███╗ ██╗ ██████╗ ██████╗ ██╗████████╗███████╗███████╗ ██╗ ██████╗
██║██╔══██╗██╔════╝██╔═══██╗████╗ ██║██╔════╝██╔════╝ ██╔═══██╗████╗ ██║██╔════╝ ██╔════╝ ██║╚══██╔══╝██╔════╝██╔════╝ ██║██╔═══██╗
██║███████║███████╗██║ ██║██╔██╗ ██║███████╗██║ ███╗██║ ██║██╔██╗ ██║██║ ███╗ ██║ ███╗██║ ██║ █████╗ █████╗ ██║██║ ██║
██ ██║██╔══██║╚════██║██║ ██║██║╚██╗██║╚════██║██║ ██║██║ ██║██║╚██╗██║██║ ██║ ██║ ██║██║ ██║ ██╔══╝ ██╔══╝ ██║██║ ██║
╚█████╔╝██║ ██║███████║╚██████╔╝██║ ╚████║███████║╚██████╔╝╚██████╔╝██║ ╚████║╚██████╔╝██╗╚██████╔╝██║ ██║ ███████╗███████╗██╗██║╚██████╔╝
╚════╝ ╚═╝ ╚═╝╚══════╝ ╚═════╝ ╚═╝ ╚═══╝╚══════╝ ╚═════╝ ╚═════╝ ╚═╝ ╚═══╝ ╚═════╝ ╚═╝ ╚═════╝ ╚═╝ ╚═╝ ╚══════╝╚══════╝╚═╝╚═╝ ╚═════╝

VScode插件推荐:koroFileHeader (用于生成文件头部注释和函数注释的插件)

image-20230912105041466

佛祖保佑一


////////////////////////////////////////////////////////////////////
// _ooOoo_ //
// o8888888o //
// 88" . "88 //
// (| ^_^ |) //
// O\ = /O //
// ____/`---'\____ //
// .' \\| |// `. //
// / \\||| : |||// \ //
// / _||||| -:- |||||- \ //
// | | \\\ - /// | | //
// | \_| ''\---/'' | | //
// \ .-\__ `-` ___/-. / //
// ___`. .' /--.--\ `. . ___ //
// ."" '< `.___\_<|>_/___.' >'"". //
// | | : `- \`.;`\ _ /`;.`/ - ` : | | //
// \ \ `-. \_ __\ /__ _/ .-` / / //
// ========`-.____`-.___\_____/___.-`____.-'======== //
// `=---=' //
// ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ //
// 佛祖保佑 永无BUG 永不修改 //
////////////////////////////////////////////////////////////////////

佛祖保佑一

/**
* _ooOoo_
* o8888888o
* 88" . "88
* (| -_- |)
* O\ = /O
* ____/`---'\____
* .' \\| |// `.
* / \\||| : |||// \
* / _||||| -:- |||||- \
* | | \\\ - /// | |
* | \_| ''\---/'' | |
* \ .-\__ `-` ___/-. /
* ___`. .' /--.--\ `. . __
* ."" '< `.___\_<|>_/___.' >'"".
* | | : `- \`.;`\ _ /`;.`/ - ` : | |
* \ \ `-. \_ __\ /__ _/ .-` / /
* ======`-.____`-.___\_____/___.-`____.-'======
* `=---='
* ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
* 佛祖保佑 永无BUG
* 佛曰:
* 写字楼里写字间,写字间里程序员;
* 程序人员写程序,又拿程序换酒钱。
* 酒醒只在网上坐,酒醉还来网下眠;
* 酒醉酒醒日复日,网上网下年复年。
* 但愿老死电脑间,不愿鞠躬老板前;
* 奔驰宝马贵者趣,公交自行程序员。
* 别人笑我忒疯癫,我笑自己命太贱;
* 不见满街漂亮妹,哪个归得程序员?
*/

草泥马一

/*
*
*   ┏┓   ┏┓+ +
*  ┏┛┻━━━┛┻┓ + +
*  ┃       ┃  
*  ┃   ━   ┃ ++ + + +
* ████━████ ┃+
*  ┃       ┃ +
*  ┃   ┻   ┃
*  ┃       ┃ + +
*  ┗━┓   ┏━┛
*    ┃   ┃           
*    ┃   ┃ + + + +
*    ┃   ┃
*    ┃   ┃ + 神兽保佑
*    ┃   ┃ 代码无bug  
*    ┃   ┃  +         
*    ┃    ┗━━━┓ + +
*    ┃        ┣┓
*    ┃        ┏┛
*    ┗┓┓┏━┳┓┏┛ + + + +
*     ┃┫┫ ┃┫┫
*     ┗┻┛ ┗┻┛+ + + +
*
*/

草泥马二

/*
*
* ┏┓   ┏┓
* ┏┛┻━━━┛┻┓
* ┃       ┃
* ┃   ━   ┃
* ┃ >   < ┃
* ┃       ┃
* ┃... ⌒ ... ┃
* ┃       ┃
* ┗━┓   ┏━┛
* ┃   ┃ 
* ┃   ┃
* ┃   ┃
* ┃   ┃ 神兽保佑
* ┃   ┃ 代码无bug  
* ┃   ┃
* ┃   ┗━━━┓
* ┃       ┣┓
* ┃       ┏┛
* ┗┓┓┏━┳┓┏┛
* ┃┫┫ ┃┫┫
* ┗┻┛ ┗┻┛
*/

全键盘

/*
* ┌───┐ ┌───┬───┬───┬───┐ ┌───┬───┬───┬───┐ ┌───┬───┬───┬───┐ ┌───┬───┬───┐
* │Esc│ │ F1│ F2│ F3│ F4│ │ F5│ F6│ F7│ F8│ │ F9│F10│F11│F12│ │P/S│S L│P/B│ ┌┐ ┌┐ ┌┐
* └───┘ └───┴───┴───┴───┘ └───┴───┴───┴───┘ └───┴───┴───┴───┘ └───┴───┴───┘ └┘ └┘ └┘
* ┌───┬───┬───┬───┬───┬───┬───┬───┬───┬───┬───┬───┬───┬───────┐ ┌───┬───┬───┐ ┌───┬───┬───┬───┐
* │~ `│! 1│@ 2│# 3│$ 4│% 5│^ 6│& 7│* 8│( 9│) 0│_ -│+ =│ BacSp │ │Ins│Hom│PUp│ │N L│ / │ * │ - │
* ├───┴─┬─┴─┬─┴─┬─┴─┬─┴─┬─┴─┬─┴─┬─┴─┬─┴─┬─┴─┬─┴─┬─┴─┬─┴─┬─────┤ ├───┼───┼───┤ ├───┼───┼───┼───┤
* │ Tab │ Q │ W │ E │ R │ T │ Y │ U │ I │ O │ P │{ [│} ]│ | \ │ │Del│End│PDn│ │ 7 │ 8 │ 9 │ │
* ├─────┴┬──┴┬──┴┬──┴┬──┴┬──┴┬──┴┬──┴┬──┴┬──┴┬──┴┬──┴┬──┴─────┤ └───┴───┴───┘ ├───┼───┼───┤ + │
* │ Caps │ A │ S │ D │ F │ G │ H │ J │ K │ L │: ;│" '│ Enter │ │ 4 │ 5 │ 6 │ │
* ├──────┴─┬─┴─┬─┴─┬─┴─┬─┴─┬─┴─┬─┴─┬─┴─┬─┴─┬─┴─┬─┴─┬─┴────────┤ ┌───┐ ├───┼───┼───┼───┤
* │ Shift │ Z │ X │ C │ V │ B │ N │ M │< ,│> .│? /│ Shift │ │ ↑ │ │ 1 │ 2 │ 3 │ │
* ├─────┬──┴─┬─┴──┬┴───┴───┴───┴───┴───┴──┬┴───┼───┴┬────┬────┤ ┌───┼───┼───┐ ├───┴───┼───┤ E││
* │ Ctrl│ │Alt │ Space │ Alt│ │ │Ctrl│ │ ← │ ↓ │ → │ │ 0 │ . │←─┘│
* └─────┴────┴────┴───────────────────────┴────┴────┴────┴────┘ └───┴───┴───┘ └───────┴───┴───┘
*/

小键盘

/*
* ┌─────────────────────────────────────────────────────────────┐
* │┌───┬───┬───┬───┬───┬───┬───┬───┬───┬───┬───┬───┬───┬───┬───┐│
* ││Esc│!1 │@2 │#3 │$4 │%5 │^6 │&7 │*8 │(9 │)0 │_- │+= │|\ │`~ ││
* │├───┴─┬─┴─┬─┴─┬─┴─┬─┴─┬─┴─┬─┴─┬─┴─┬─┴─┬─┴─┬─┴─┬─┴─┬─┴─┬─┴───┤│
* ││ Tab │ Q │ W │ E │ R │ T │ Y │ U │ I │ O │ P │{[ │}] │ BS ││
* │├─────┴┬──┴┬──┴┬──┴┬──┴┬──┴┬──┴┬──┴┬──┴┬──┴┬──┴┬──┴┬──┴─────┤│
* ││ Ctrl │ A │ S │ D │ F │ G │ H │ J │ K │ L │: ;│" '│ Enter ││
* │├──────┴─┬─┴─┬─┴─┬─┴─┬─┴─┬─┴─┬─┴─┬─┴─┬─┴─┬─┴─┬─┴─┬─┴────┬───┤│
* ││ Shift │ Z │ X │ C │ V │ B │ N │ M │< ,│> .│? /│Shift │Fn ││
* │└─────┬──┴┬──┴──┬┴───┴───┴───┴───┴───┴──┬┴───┴┬──┴┬─────┴───┘│
* │ │Fn │ Alt │ Space │ Alt │Win│ HHKB │
* │ └───┴─────┴───────────────────────┴─────┴───┘ │
* └─────────────────────────────────────────────────────────────┘
*/

耶稣

/*
* |~~~~~~~|
* | |
* | |
* | |
* | |
* | |
* |~.\\\_\~~~~~~~~~~~~~~xx~~~ ~~~~~~~~~~~~~~~~~~~~~/_//;~|
* | \ o \_ ,XXXXX), _..-~ o / |
* | ~~\ ~-. XXXXX`)))), _.--~~ .-~~~ |
* ~~~~~~~`\ ~\~~~XXX' _/ ';)) |~~~~~~..-~ _.-~ ~~~~~~~
* `\ ~~--`_\~\, ;;;\)__.---.~~~ _.-~
* ~-. `:;;/;; \ _..-~~
* ~-._ `'' /-~-~
* `\ / /
* | , | |
* | ' / |
* \/; |
* ;; |
* `; . |
* |~~~-----.....|
* | \ \
* | /\~~--...__ |
* (| `\ __-\|
* || \_ /~ |
* |) \~-' |
* | | \ '
* | | \ :
* \ | | |
* | ) ( )
* \ /; /\ |
* | |/ |
* | | |
* \ .' ||
* | | | |
* ( | | |
* | \ \ |
* || o `.)|
* |`\\) |
* | |
* | |
*/

美女

/*
* .::::.
* .::::::::.
* :::::::::::
* ..:::::::::::'
* '::::::::::::'
* .::::::::::
* '::::::::::::::..
* ..::::::::::::.
* ``::::::::::::::::
* ::::``:::::::::' .:::.
* ::::' ':::::' .::::::::.
* .::::' :::: .:::::::'::::.
* .:::' ::::: .:::::::::' ':::::.
* .::' :::::.:::::::::' ':::::.
* .::' ::::::::::::::' ``::::.
* ...::: ::::::::::::' ``::.
* ````':. ':::::::::' ::::..
* '.:::::' ':'````..
*/

程序员之歌

/*
* 江城子 . 程序员之歌
*
* 十年生死两茫茫,写程序,到天亮。
* 千行代码,Bug何处藏。
* 纵使上线又怎样,朝令改,夕断肠。
*
* 领导每天新想法,天天改,日日忙。
* 相顾无言,惟有泪千行。
* 每晚灯火阑珊处,夜难寐,加班狂。
*
*/

龙图腾

/*
* ......................................&&.........................
* ....................................&&&..........................
* .................................&&&&............................
* ...............................&&&&..............................
* .............................&&&&&&..............................
* ...........................&&&&&&....&&&..&&&&&&&&&&&&&&&........
* ..................&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&..............
* ................&...&&&&&&&&&&&&&&&&&&&&&&&&&&&&.................
* .......................&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&.........
* ...................&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&...............
* ..................&&& &&&&&&&&&&&&&&&&&&&&&&&&&&&&&............
* ...............&&&&&@ &&&&&&&&&&..&&&&&&&&&&&&&&&&&&&...........
* ..............&&&&&&&&&&&&&&&.&&....&&&&&&&&&&&&&..&&&&&.........
* ..........&&&&&&&&&&&&&&&&&&...&.....&&&&&&&&&&&&&...&&&&........
* ........&&&&&&&&&&&&&&&&&&&.........&&&&&&&&&&&&&&&....&&&.......
* .......&&&&&&&&.....................&&&&&&&&&&&&&&&&.....&&......
* ........&&&&&.....................&&&&&&&&&&&&&&&&&&.............
* ..........&...................&&&&&&&&&&&&&&&&&&&&&&&............
* ................&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&............
* ..................&&&&&&&&&&&&&&&&&&&&&&&&&&&&..&&&&&............
* ..............&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&....&&&&&............
* ...........&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&......&&&&............
* .........&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&.........&&&&............
* .......&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&...........&&&&............
* ......&&&&&&&&&&&&&&&&&&&...&&&&&&...............&&&.............
* .....&&&&&&&&&&&&&&&&............................&&..............
* ....&&&&&&&&&&&&&&&.................&&...........................
* ...&&&&&&&&&&&&&&&.....................&&&&......................
* ...&&&&&&&&&&.&&&........................&&&&&...................
* ..&&&&&&&&&&&..&&..........................&&&&&&&...............
* ..&&&&&&&&&&&&...&............&&&.....&&&&...&&&&&&&.............
* ..&&&&&&&&&&&&&.................&&&.....&&&&&&&&&&&&&&...........
* ..&&&&&&&&&&&&&&&&..............&&&&&&&&&&&&&&&&&&&&&&&&.........
* ..&&.&&&&&&&&&&&&&&&&&.........&&&&&&&&&&&&&&&&&&&&&&&&&&&.......
* ...&&..&&&&&&&&&&&&.........&&&&&&&&&&&&&&&&...&&&&&&&&&&&&......
* ....&..&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&...........&&&&&&&&.....
* .......&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&..............&&&&&&&....
* .......&&&&&.&&&&&&&&&&&&&&&&&&..&&&&&&&&...&..........&&&&&&....
* ........&&&.....&&&&&&&&&&&&&.....&&&&&&&&&&...........&..&&&&...
* .......&&&........&&&.&&&&&&&&&.....&&&&&.................&&&&...
* .......&&&...............&&&&&&&.......&&&&&&&&............&&&...
* ........&&...................&&&&&&.........................&&&..
* .........&.....................&&&&........................&&....
* ...............................&&&.......................&&......
* ................................&&......................&&.......
* .................................&&..............................
* ..................................&..............................
*/

蝙蝠

/*
* ___====-_ _-====___
* _--^^^#####// \\#####^^^--_
* _-^##########// ( ) \\##########^-_
* -############// |\^^/| \\############-
* _/############// (@::@) \############\_
* /#############(( \\// ))#############\
* -###############\\ (oo) //###############-
* -#################\\ / VV \ //#################-
* -###################\\/ \//###################-
* _#/|##########/\######( /\ )######/\##########|\#_
* |/ |#/\#/\#/\/ \#/\##\ | | /##/\#/ \/\#/\#/\#| \|
* ` |/ V V ` V \#\| | | |/#/ V ' V V \| '
* ` ` ` ` / | | | | \ ' ' ' '
* ( | | | | )
* __\ | | | | /__
* (vvv(VVV)(VVV)vvv)
*
* ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
*
* 神兽保佑 永无BUG
*/

喷火龙

/*
* __----~~~~~~~~~~~------___
* . . ~~//====...... __--~ ~~
* -. \_|// |||\\ ~~~~~~::::... /~
* ___-==_ _-~o~ \/ ||| \\ _/~~-
* __---~~~.==~||\=_ -_--~/_-~|- |\\ \\ _/~
* _-~~ .=~ | \\-_ '-~7 /- / || \ /
* .~ .~ | \\ -_ / /- / || \ /
* / ____ / | \\ ~-_/ /|- _/ .|| \ /
* |~~ ~~|--~~~~--_ \ ~==-/ | \~--===~~ .\
* ' ~-| /| |-~\~~ __--~~
* |-~~-_/ | | ~\_ _-~ /\
* / \ \__ \/~ \__
* _--~ _/ | .-~~____--~-/ ~~==.
* ((->/~ '.|||' -_| ~~-/ , . _||
* -_ ~\ ~~---l__i__i__i--~~_/
* _-~-__ ~) \--______________--~~
* //.-~~~-~_--~- |-------~~~~~~~~
* //.-~~~--\
* ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
*
* 神兽保佑 永无BUG
*/
]]>
个人 注释模板
内网穿透 /posts/6932.html 第一步 官网下载

客户端下载 –> windows 64位 –>解压后得到.exe文件

image-20230409221503708

第二步 官网注册并登录进入后台

官网注册登录 –> 购买免费隧道 –>获取authtoken

image-20230409221827414

image-20230409221954792

第三步 双击下载的.exe文件进入命令行页面输入命令

natapp -authtoken=刚刚你申请的authtoken

image-20230409222457135

image-20230409222628425

第四步 根据生成的域名访问服务

image-20230409222923480

]]>
后端 网络
免费域名注册教程 /posts/5727.html 一.域名注册

免费域名注册网址:https://nic.eu.org

image-20230913123344514

1.注册账号

第一步:点击: here

image-20230913123704246

第二步: 点击Register

image-20230913123809940

第三步:填写相关的信息

地址信息填写英国的地址,可以使用在线地址生成器生成:https://www.meiguodizhi.com/uk-address

image-20230913124042655

填写地址相关的信息,其余的信息根据自己的情况填写(fax可不填)

image-20230913124616244

显示下面的提示就是注册成功了

image-20230913124842536

这时在系统会给刚才注册的邮箱发送一个激活链接,点击激活

image-20230913125129535

打开链接之后点击的按钮Validate验证,验证之后显示如下页面表示成功

image-20230913125324974

2.登录

登录的账号就在刚才发送的邮件里面,密码就是注册时设置的密码

image-20230913125539561

3.申请域名

输入你要注册的域名,域名的格式是 xxxx.eu.org

image-20230913132602702

域名验证

image-20230913132742999

点击Submit按钮之后显示如下的内容表示这个免费的域名是可以申请到的,然后等待人工审核通过

image-20230913132918535

审核通过之后,你注册时使用的邮箱就会收到信息

]]>
个人 域名注册
免费好用的PDF工具 /posts/40733.html 一.PDFgear

免费、无需登录注册、支持编辑和PDF与格式文件的相互转换!

1.下载地址

官网地址:https://www.pdfgear.com/zh/

image-20240113151450334

2.介绍

无需登录,无需付费、支持PDF与其他格式的文件的相互转化、拆分合并PDF、压缩PDF、OCR文字识别

image-20240113151556929

image-20240113151834234

二.stiring-pdf

github开源,需要自己使用Docker部署!

github仓库地址:https://github.com/Stirling-Tools/Stirling-PDF

stirling-home.png

部署命令

docker run -d \
-p 8080:8080 \
-v /location/of/trainingData:/usr/share/tesseract-ocr/5/tessdata \
-v /location/of/extraConfigs:/configs \
-v /location/of/logs:/logs \
-e DOCKER_ENABLE_SECURITY=false \
--name stirling-pdf \
frooodle/s-pdf:latest
]]>
常用工具 PDF
力扣(LeetCode)算法刷题 /posts/13579.html 刷题网站: 力扣 视频: AcWing

一.数组

A.简单

1.两数之和

给定一个整数数组 nums 和一个整数目标值 target,请你在该数组中找出 和为目标值 target 的那 两个 整数,并返回它们的数组下标。你可以假设每种输入只会对应一个答案。但是,数组中同一个元素在答案里不能重复出现。你可以按任意顺序返回答案。

示例 1:

输入:nums = [2,7,11,15], target = 9
输出:[0,1]
解释:因为 nums[0] + nums[1] == 9 ,返回 [0, 1] 。

示例 2:

输入:nums = [3,2,4], target = 6
输出:[1,2]

示例 3:

输入:nums = [3,3], target = 6
输出:[0,1]

提示:

  • 2 <= nums.length <= 104
  • -109 <= nums[i] <= 109
  • -109 <= target <= 109
  • 只会存在一个有效答案

代码实现

自己的代码 暴力枚举

class Solution {
public int[] twoSum(int[] nums, int target) {
int[] result = new int[2];
for (int i = 0; i < nums.length; i++) {
for (int j = 0; j < nums.length; j++) {
if (nums[i] + nums[j] == target && i != j) {
result[0] = i;
result[1] = j;
}
}
}
return result;
}
}

2.删除有序数组中的重复项

给你一个 升序排列 的数组 nums ,请你** 原地** 删除重复出现的元素,使每个元素 只出现一次 ,返回删除后数组的新长度。元素的 相对顺序 应该保持 一致 。然后返回 nums 中唯一元素的个数。

考虑 nums 的唯一元素的数量为 k ,你需要做以下事情确保你的题解可以被通过:

  • 更改数组 nums ,使 nums 的前 k 个元素包含唯一元素,并按照它们最初在 nums 中出现的顺序排列。nums 的其余元素与 nums 的大小不重要。
  • 返回 k

判题标准:

系统会用下面的代码来测试你的题解:

int[] nums = [...]; // 输入数组
int[] expectedNums = [...]; // 长度正确的期望答案

int k = removeDuplicates(nums); // 调用

assert k == expectedNums.length;
for (int i = 0; i < k; i++) {
assert nums[i] == expectedNums[i];
}

如果所有断言都通过,那么您的题解将被 通过

示例 1:

输入:nums = [1,1,2]
输出:2, nums = [1,2,_]
解释:函数应该返回新的长度 2 ,并且原数组 nums 的前两个元素被修改为 1, 2 。不需要考虑数组中超出新长度后面的元素。

示例 2:

输入:nums = [0,0,1,1,1,2,2,3,3,4]
输出:5, nums = [0,1,2,3,4]
解释:函数应该返回新的长度 5 , 并且原数组 nums 的前五个元素被修改为 0, 1, 2, 3, 4 。不需要考虑数组中超出新长度后面的元素。

提示:

  • 1 <= nums.length <= 3 * 104
  • -104 <= nums[i] <= 104
  • nums 已按 升序 排列

代码实现

package com.leetcode.array;

import java.util.Arrays;

/**
* @author GongChangjiang
* @version 1.0
* @Date 2023/5/16
* @Description 删除有序数组中的重复项
*/
public class RemoveDuplicates {
public static void main(String[] args) {
RemoveDuplicates removeDuplicates = new RemoveDuplicates();
int[] arr = {0,0,1,1,1,2,2,3,3,4};
System.out.println(removeDuplicates.removeDuplicates(arr));

}

/**
* 数组去重
*/
public int removeDuplicates(int[] nums) {
int n = nums.length;//获取数组长度
if (n == 0) { //数组的长度是零就直接的退出
return 0;
}
int fast = 1, slow = 1; //定义两个快慢指针
while (fast < n) {
//如果后面一个数和前面的那个数相同 慢指针不动 快指针动
//然后下一次判断的时候 慢指针就别快指针慢了一次 再将慢指针指向快指针
//相同的数就后移了
if (nums[fast] != nums[fast - 1]) {
nums[slow] = nums[fast];
++slow;
}
++fast;
}
return slow;
}
}

3.移除元素

给你一个数组 nums 和一个值 val,你需要 原地 移除所有数值等于 val 的元素,并返回移除后数组的新长度。

不要使用额外的数组空间,你必须仅使用 O(1) 额外空间并 原地 修改输入数组

元素的顺序可以改变。你不需要考虑数组中超出新长度后面的元素。

说明:

为什么返回数值是整数,但输出的答案是数组呢?

请注意,输入数组是以「引用」方式传递的,这意味着在函数里修改输入数组对于调用者是可见的。

你可以想象内部操作如下:

// nums 是以“引用”方式传递的。也就是说,不对实参作任何拷贝
int len = removeElement(nums, val);

// 在函数里修改输入数组对于调用者是可见的。
// 根据你的函数返回的长度, 它会打印出数组中 该长度范围内 的所有元素。
for (int i = 0; i < len; i++) {
print(nums[i]);
}

示例 1:

输入:nums = [3,2,2,3], val = 3
输出:2, nums = [2,2]
解释:函数应该返回新的长度 2, 并且 nums 中的前两个元素均为 2。你不需要考虑数组中超出新长度后面的元素。例如,函数返回的新长度为 2 ,而 nums = [2,2,3,3] 或 nums = [2,2,0,0],也会被视作正确答案。

示例 2:

输入:nums = [0,1,2,2,3,0,4,2], val = 2
输出:5, nums = [0,1,4,0,3]
解释:函数应该返回新的长度 5, 并且 nums 中的前五个元素为 0, 1, 3, 0, 4。注意这五个元素可为任意顺序。你不需要考虑数组中超出新长度后面的元素。

提示:

  • 0 <= nums.length <= 100
  • 0 <= nums[i] <= 50
  • 0 <= val <= 100

代码实现


]]>
后端 数据结构与算法
大数据开发相关笔记 /posts/46054.html

笔记转载于黑马程序员,详细的笔记来源于:https://www.bilibili.com/video/BV1WY4y197g7/?spm_id_from=333.337.search-card.all.click&vd_source=22300b9f40de74b7db529eb8f04510a9

一.Hive

1.Hive SQL语法大全

2.Hive函数

]]>
大数据 大数据
前端基础知识 /posts/432.html 一.VSCode的使用

常用插件

image-20230521233651126

二. ES6

菜鸟ES6教程教程

1.简介

​ ES6, 全称 ECMAScript 6.0 ,是 JavaScript 的下一个版本标准,2015.06 发版。ES6 主要是为了解决 ES5 的先天不足,比如 JavaScript 里并没有类的概念,但是目前浏览器的 JavaScript 是 ES5 版本,大多数高版本的浏览器也支持 ES6,不过只实现了 ES6 的部分特性和功能。

2.基础语法

img

2.1 声明变量

  • let 声明的变量有严格的作用域 var没有严格的作用域
  • let变量只能声明一次
  • let不存在变量提升 var存在变量提升(先使用后声明)
  • const声明的是常量,不能重新赋值
  • 总计:使用let声明变量
<!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>声明变量</title>
</head>

<body>

<script>
//1.有严格的作用域
//var 声明的变量往往会越域
//let 声明的变量有严格的作用域
{
var a = 1;
let b = 1;
}
console.log(a) //不会报错
console.log(b) //ReferenceError: b is not defined,有严格的作用域,所以会报错



//2.let的变量只能声明一次
var m = 1
var m = 2
let n = 3
// let n = 4
console.log(m) //不会报错
console.log(n) //Identifier 'n' has already been declared 不能声明两次



//3.let不存在变量提升 var存在变量提升
console,log(x)
var x = 10; //这里不会报错,可以先使用后定义


//4.const声明的是常量,不能重新赋值

//总计:使用let声明变量

</script>
</body>
</html>

2.2 解构表达式和模板字符串

  • 数组的解构
  • 对象的解构
  • 字符串API的扩展
  • 模板字符串
<!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>解构表达式</title>
</head>

<body>
<script>
//解构表达式
//1.原始的数组的赋值方式
let arr = [1, 2, 3]
// let a = arr[0]
// let b = arr[1]
// let c = arr[2]

//2.使用解构表达式给数组赋值(数组解构)
let [a, b, c] = arr;
console.log(a, b, c);

//3.使用解构表达式给对象赋值(对象解构)
const person = {
name: "jack",
age: 21,
language: ['java', 'js', 'css']
}
//原始的赋值方式
// const name = person.name
// const age = person.age
// const language = person.language

//使用解构赋值(对象解构)
//const { name, age, language } = person
//更换属性名
const { name: abc, age, language } = person
//将属性名name更换为abc
console.log(abc, age, language)


//4.字符串扩展
//几个新的字符串API
let str = "hello.vue"
//是不是以什么开头
console.log(str.startsWith("hello"));//true
//是不是以什么结束
console.log(str.endsWith(".vue"));//true
//是不是包含什么
console.log(str.includes("e"));//true
console.log(str.includes("hello"));//true


//5.模板字符串
//${}插值
let name = "Tom"
let result = `你好${name}`
console.log(result)
//可以直接调用字符串的返回值组成一个新的字符串
function fun(){
return "这是一个函数"
}
result = `你好${name},我想说:${fun()}`
console.log(result)

</script>
</body>
</html>

2.3 函数优化

  • 默认初始值
  • 不定参数
  • 箭头函数
<!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>函数优化</title>
</title>
</head>

<body>
<script>
//1.函数优化
//在ES6以前,我们无法给一个函数参数设置默认值,只能采用变通的写法
function add(a, b) {
//判断b是否为空 如果为空就给默认值1
b = b || 1
return a + b
}
//传一个参数
console.log(add(10)) //结果是11

//优化后 直接在参数上写上默认的值 如果没有传就会自动使用默认值
function add2(a, b = 1) {
return a + b
}
console.log(add2(20)) //结果是21


//2.不定参数 类似于java里面的可变形参
function fun(...values) {
console.log(values.length)
}
fun(1, 2)
fun(1, 2, 3)
fun(1, 2, 3, 4)


//3.箭头函数
//以前的声明方法
var print = function (obj) {
console.log(obj)
}
var print1 = obj => console.log(obj)
print1("hello") //输出hello

//求两数之和
//以前的方法
var sum = function (a, b) {
return a + b
}
//使用箭头函数
var sum2 = (a, b) => a + b //方法体中只有一行代码 不用写return
console.log(sum2(11, 3))

var sum3 = (a, b) => {//方法体中有多行代码 写法如下
c = a + b;
return a + c
}
console.log(sum3(11, 3))

//复杂一点的箭头函数
const person = {
name: "jack",
age: 21,
language: ['java', 'js', 'css']
}
//原始的方法
function hello(person) {
console.log("hello " + person.name)
}

//单纯的箭头函数
var hello2 = (obj) => console.log("hello," + obj.name)
hello2(person)

//箭头函数加解构表达式
var hello3 = ({name}) => console.log("hello," + name)
hello3(person)
</script>
</body>

</html>

2.4 对象优化

  • 对象新增的API
  • 声明对象的简写方式
  • 对象函数属性的简写
<!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>对象优化</title>
</head>

<body>
<script>
//1.对象相关的API
//定义一个person对象
const person = {
name: "jack",
age: 21,
language: ['java', 'js', 'css']
}
//测试对象新增的API
//返回对象所有的属性信息
console.log(Object.keys(person)) //['name', 'age', 'language']
//返回对象中vaules
console.log(Object.values(person))// ['jack', 21, Array(3)]
//返回key-vaues
console.log(Object.entries(person))// [Array(2), Array(2), Array(2)]


//将source1和source2的属性合并到target中
const target = { a: 1 }
const source1 = { b: 2 }
const source2 = { c: 3 }
//将source1和source2中的属性合并在一起
Object.assign(target, source1, source2)
console.log(target) //{a: 1, b: 2, c: 3}


//2.声明对象的简写方式
//声明两个变量
const age = 23;
const name = "张三";
//原始的方式然后声明一个对象,属性值为这两个变量的值
const person1 = { age: age, name: name }
console.log(person1)

//ES6的方式
const person2 = { age, name }// 属性名和属性值代表的变量的名字一样可以省写
console.log(person2)

//3.对象函数属性的简写
let person3 = {
name: "jack",//对象的属性
//以前的语法格式
eat: function (food) {//对象的方法
console.log(this.name + "在吃" + food)
},

//使用箭头有函数简写 箭头函数中this.name无法使用
eat2: food => console.log(person3.name + "在吃" + food),

eat3(food) {
console.log(this.name + "在吃" + food)
}
}
person3.eat("香蕉")
person3.eat2("苹果")
person3.eat3("梨子")

//4.对象的扩展运算符
//拷贝对象(深拷贝)
//将person01的值拷贝给someone
let person01 = { name: "Amy", age: 15 }
let someone = { ...person01 }
console.log(someone)

//合并对象
let age1 = { age: 15 }
let name1 = { name: "Amy" }
let person03 = { ...age1, ...name1 }//两个对象属性的合并一个对象中,如果有相同的值,后一个覆盖前面的那一个属性的值
console.log(person03)//{age: 15, name: 'Amy'}

</script>
</body>

</html>

2.5 数组的map、reduce方法

  • map方法
  • reduce方法
<!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>map和reduce</title>
</head>

<body>
<script>
//数组中新增了map和reduce方法
//map():接收一个函数,将原数组中的所有元素用这个函数处理后放入新数组返回
let arr = ['1', '20', '-5', '3']
//需求:将原数组中的每一个值乘以2并返回
//第一种方法
arr1 = arr.map((item) => {//item为数组中的每一个元素
return item * 2
})
console.log(arr1)
//第二种方法
arr2 = arr.map(item => item * 2)
console.log(arr2)

//reduce()方法:为数组中的每一个元素依次执行回调函数,不包括数组中被删除或从未被赋值的元素
arr3 = [2, 40, -10, 6]
//arr.reduce(callback,[initialValue]) //可以传一个回调函数callback,也可以传一个初始值initialValue
//传入的是回调函数 可以传入的值有
/**
* previousValue (上一次调用回调返回的值,或者是提供的初始值initialValue)
* currentValue (数组中当前被处理的元素)
* index (当前元素在数组中的索引)
* array (调用reduce的数组)
* */
let result = arr3.reduce((a, b) => {//这个方法相当于求数组中每一个元素的和
console.log("上一次处理后的值:" + a)
console.log("当前处理的值:" + b)
return a + b;
})
console.log(result)
/**结果
* 上一次处理后的值:2
当前处理的值:40
上一次处理后的值:42
当前处理的值:-10
上一次处理后的值:32
当前处理的值:6
38
**/
</script>
</body>

</html>

2.6 promise异步编排

解决回调地域的问题

​ Promise构造函数接受一个函数作为参数,该函数的两个参数分别是resolve和reject。它们是两个函数,由 JavaScript 引擎提供,不用自己部署。resolve函数的作用是,将Promise对象的状态从“未完成”变为“成功”(即从 pending 变为 resolved),在异步操作成功时调用,并将异步操作的结果,作为参数传递出去;reject函数的作用是,将Promise对象的状态从“未完成”变为“失败”(即从 pending 变为 rejected),在异步操作失败时调用,并将异步操作报出的错误,作为参数传递出去。

let promise = new Promise(function(resolve, reject) {
console.log('Promise');
resolve();
});

promise.then(function() {
console.log('resolved.');
});

console.log('Hi!');

// Promise
// Hi!
// resolved

原有的基础上进一步的封装,链式调用

3.模块化

3.1 什么是模块化

​ 模块化就是把代码进行拆分,分辨重复使用。类似java中的导包:要使用一个包,必须先导包。而JS中没有包的概念,换来的是模块。

模块功能主要由两个命令构成: export和import

  • export命令用于规定模块的对外接口
  • import命令用于导入其他模块提供的功能

3.2 export

export不仅可以导出对象,一切js对象都可以导出。比如:基本类型的变量、函数、数组、对象

比如我们定义了一个js文件hello.js,里面有一个对象

const util = {
dsum(a, b) {
return a + b;
}
}

我们可以使用export将这个对象导出

const util = {
dsum(a, b) {
return a + b;
}
}

//导出对象
export { util }

也可以简写成

export const util = {
dsum(a, b) {
return a + b;
}
}

当要导出多个值时,还可以简写。比如我有一个文件: user.js

var name  = "jack"
var age = 21

export{name, age}

3.3 import

比如我们之前定义了两个JS文件

hello.js

const util = {
dsum(a, b) {
return a + b;
}
}

//导出对象
export { util }

user.js

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

export { name, age, add }

我们在main.js文件中导入上面的两个文件,并使用上面的两个文件的方法

import util from "./hello"  //这里只能是util 不能是其他的名字
import {name,age,add} from './user' //可以不用全部导入 只用导入需要的即可

util.sum(1,2)
console.log(name);
add(1,2)

3.4 注意

当我们这样定义时,导入的时候可以自定义导入时使用的变量名

export default {  //导出的时候这样写
dsum(a, b) {
return a + b;
}
}
import xxx from "./hello"  //这里使用可以随意的取名

三.Vue

1.MVVM思想

  • M:即 Model,模型,包括数据和一些基本操作
  • V:即View视图,页面渲染结果
  • VM:即 View-Model,模型与视图间的双向操作(无需开发人员干涉)

​ 在MVVM之前,开发人员从后端获取需要的数据模型,然后要通过DOM操作 Model渲染到View中。而后当用户操作视图,我们还需要通过DOM获取View中的数据,然后同步到Model中。而MVVM中的VM要做的事情就是把DOM操作完全封装起来,开发人员不用再关心Model和View之间是如何互相影响的:

2.vue的简介

​ Vue (发音为 /vjuː/,类似 view) 是一款用于构建用户界面的 JavaScript 框架。它基于标准 HTML、CSS 和 JavaScript 构建,并提供了一套声明式的、组件化的编程模型,帮助你高效地开发用户界面。无论是简单还是复杂的界面,Vue 都可以胜任。

Vue2官方文档

3.语法基础

3.1 快速入门

1.创建一个文件夹,用VSCode打开

2.下载vue的依赖

# 初始化项目
npm init -y
# 安装vue的依赖,这里vue2和vue3版本语法有些差别,使用的时候要注意
npm install vue@2

3.创建一个index.html文件

<!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>Vue入门</title>
</head>

<body>
<div id="app">
<h1>{{name}},是个傻子</h1>
</div>
<script src="./node_modules/vue/dist/vue.js"></script>
<script>
//这里最新版的vue上面无法获取到name的值
let vm = new Vue({
el: "#app",
data: {
name:"张三"
}
});
</script>
</body>

</html>

3.2 基本的语法和插件的安装

3.2.1双向绑定v-model

<!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>Vue入门</title>
</head>

<body>
<div id="app">
<!-- 双向绑定 -->
<input type="text" v-model="num">
<h1>{{name}}今年{{num}}岁了</h1>
</div>
<script src="./node_modules/vue/dist/vue.js"></script>
<script>
//声明式渲染
let vm = new Vue({
el: "#app",
data: {
name:"张三",
num: 1
}
});
</script>
</body>
</html>

3.2.2事件处理

<!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>Vue入门</title>
</head>

<body>
<div id="app">
<!-- 双向绑定 -->
<input type="text" v-model="num">
<!-- 点击事件绑定 -->
<button v-on:click="num++">点赞</button>
<!-- 取消点赞 -->
<button v-on:click="cancle()">取消点赞</button></button>
<h1>有{{num}}为{{name}}点赞!</h1>
</div>
<script src="./node_modules/vue/dist/vue.js"></script>
<script>
//声明式渲染
let vm = new Vue({
el: "#app", //绑定元素
data: {//封装数据
name:"张三",
num: 1
},
methods:{//封装方法
cancle(){
this.num--;
}
}
});
//v-xx 相当于指令
//总结
//1.创建vue的实例,关联页面的模板,将自己的数据(data)渲染到关联的模板
//2.使用指令简化对dom的操作 例如 v-XX
</script>
</body>

</html>

3.2.3VUE代码提示插件和浏览器插件

浏览器插件地址 直接解压,打开浏览器插件的开发者模式,选择加载已解压的文件,选择chrome目录即可

image-20230525192244158

image-20230525191246045

3.2.4常用指令

v-html 和 v-text
<!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>常用指令</title>
</head>
<body>
<div id="app">
<!-- 1.显示类的指令 -->
<!-- 插值表达式会发生插值闪烁的问题,先显示{{msg}}后显示内容 -->
<!-- 打印的是<h1>Hello</h1> -->
{{msg}} <br>
{{1+1}} <br>
{{hello()}} <br>
<span v-text="msg"></span>
<!-- 直接显示h1的大标题 -->
<span v-html="msg"></span>
</div>
<script src="./node_modules/vue/dist/vue.js"></script>
<script>
new Vue({
el:"#app",
data:{
msg:"<h1>Hello</h1>"
},
methods: {
hello(){
return "hello";
}
},
})
</script>
</body>
</html>
v-bind单向绑定
<!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>单向绑定</title>
</head>
<body>
<div id = "app">
<!-- 单向绑定 -->
<a v-bind:href="link">百度</a>
<!-- class style也可以通过标签进行单向绑定 -->
<span v-bind:style="{color:color}">你好</span>
<span :style="{color:color}">使用缩写形式的单向绑定</span>
<script src="./node_modules/vue/dist/vue.js"></script>
<script>
new Vue({
el: "#app",
data: {
link:"http://www.baidu.com",
color: 'green'
}
})
</script>
</body>
</html>
v-model 双向绑定
<!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>双向绑定</title>
</head>
<body>
<div id = "app">
精通的语言:<br>
<input type="checkbox" v-model="language" value="java">JAVA <br>
<input type="checkbox" v-model="language" value="php">PHP <br>
<input type="checkbox" v-model="language" value="python">Python <br>
选中了: {{language}}
</div>
<script src="./node_modules/vue/dist/vue.js"></script>
<script>
new Vue({
el: "#app",
data: {
language:[]
}
})
</script>
</body>
</html>

image-20230526164858205

v-on 事件绑定
<!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>事件绑定</title>
</head>

<body>
<div id="app">
<!-- 绑定一个单机事件 -->
<button v-on:click="num++">v-on:click的方式点赞</button><br>
<!-- 简写的方式实现点赞 -->
<button @click="num++">@click的方式点赞</button><br>
<!-- 点击事件指定一个回调函数 -->
<button v-on:click="cancle()">取消点赞</button><br>
点赞的数量:{{num}}


</div>
<script src="./node_modules/vue/dist/vue.js"></script>
<script>
new Vue({
el: "#app",
data: {
num: 0
},
methods: {
cancle(){
this.num--;
}
},
})

</script>
</body>
</html>
image-20230526170222378
v-for 循环显示
<!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>循环遍历</title>
</head>

<body>
<div id="app">
<ul>
<!-- 1.遍历数组 -->
<!-- 可以使用v-for="user in users"遍历用户信息 -->
<!-- 获取下标的信息加上一个index -->
<li v-for="(user,index) in users" :key="">
序号:{{index+1}} <br>
姓名:{{user.name}} <br>
性别:{{user.gender}} <br>
年龄:{{user.age}} <br>
<!-- 2.遍历对象,获取每一个属性信息 -->
对象信息:
<span v-for="(v,k,i) in user">属性索引={{i}}|属性名={{k}}|属性值={{v}}|</span><br>
</li>
<!-- 遍历的时候加上 :key 提高渲染的效率 -->
<!-- <li v-for="(user,index) in users" :key=""> -->
</ul>

<!-- 遍历数组 -->
<ul>
<li v-for="(item,index) in nums" ::key="index">
{{item}}
</li>
</ul>

</div>
<script src="./node_modules/vue/dist/vue.js"></script>
<script>
new Vue({
el: "#app",
data: {
users: [
{ name: "mary", gender: '女', age: 21 },
{ name: "tom", gender: '男', age: 35 },
{ name: "mike", gender: '男', age: 21 },
{ name: "amy", gender: '女', age: 64 }
],
nums:[1,2,3,4,5,6,7,8,9,10,11]
}
})

</script>
</body>

</html>

image-20230526172228532

v-if 和 v-show
<!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>判断</title>
</head>
<body>
<!-- v-if 条件判断,当结果为true时,所有的元素才会被渲染 -->
<!-- v-show 当得到的结果为true时,所有的元素才会被显示 -->
<div id="app">
<button v-on:click="show = !show">点击显示与否</button></button>
<!-- 1.使用v-if显示 -->
<h1 v-if="show">if=看到我...</h1>
<!-- 2.使用v-show显示 -->
<h1 v-show="show">show=显示...</h1></h1>
<!-- 多重判断 -->
<!-- v-if、v-else-if、v-else -->
<!-- 循环的时候也可以加上判断 -->

</div>
<script src="./node_modules/vue/dist/vue.js"></script>
<script>
new Vue({
el: "#app",
data: {
show:true
}
})

</script>
</body>
</html>

3.2.5 计算属性和侦听器

<!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>计算属性和侦听器</title>
</head>

<body>
<div id="app">
<!-- 计算属性 -->
<ul>
<li>西游记;价格:{{xyjPrice}},数量:<input type="number" v-model="xyjNum"></li>
<li>水浒传;价格:{{shzPrice}},数量:<input type="number" v-model="shzNum"></li>
<li>总价:{{totalPrice}}</li>
{{msg}}
</ul>
<!-- 侦听器,监听属性值的变化 -->
</div>
<script src="./node_modules/vue/dist/vue.js"></script>
<script>
new Vue({
el: "#app",
data: {
xyjPrice: 99.98, //西游记价格
shzPrice: 99.00, //水浒传价格
xyjNum: 0, //西游记的数量
shzNum: 0, //水浒传的数量
msg: "" //提示的消息
},
computed: {//计算属性
totalPrice() {//计算总价
return this.xyjPrice * this.xyjNum + this.shzPrice * this.shzNum
}
},
watch: {//监听器
//newVal:新值
//oldVal:旧值
//下面的这个方法是监听当购买的数量超出了3,就提示超库存了,并把输入框的值设置为最大的3
xyjNum: function (newVal, oldVal) {//监听西游记数量的变化
console.log("newVal:" + newVal + " | " + "oldVal:" + oldVal)
if (newVal >= 3) {
this.msg = "库存超出了限制"
this.xyjNum = 3
} else {
this.msg = ""
}
}
}

})

</script>

</body>
</html>

3.2.6 过滤器

<!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>过滤器</title>
</head>
<div id="app">
<!-- 过滤器常用来处理文本格式化的操作,过滤器可以用在两个地方:插值表达式和v-bind表达式 -->
<ul>
<li v-for="user in userList">
<!-- 显示性别的时候不用代号显示,使用男女显示 -->
<!-- 方式一:通过三元运算符的形式显示 -->
<!-- 姓名:{{user.name}} | 性别:{{user.gender== 1?"男":"女"}} -->
<!-- 方式二:使用过滤器的方式 其中 | 是管道符 genderFilter是过滤器 -->
姓名:{{user.name}} | 性别:{{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 "女··"
}
})


new Vue({
el: "#app",
data: {
userList: [
{ id: 1, name: "jacky", gender: 1 },
{ id: 2, name: "peter", gender: 0 },
]
},
filters: {//局部的过滤器
genderFilter(val) {
if (val == 1) {
return "男"
} else {
return "女"
}
}
}
})

</script>

<body>

</body>

</html>

3.3 组件化

​ 在大型应用开发的时候,页面可以划分成很多部分。往往不同的页面,也会有相同的部分。例如可能会有相同的头部导航。但是每个页面都都独自开发,这无疑增加了开发的成本。所以我们会把页面的不同部分拆分成独立的组件,然后在不同的页面就可以共享这些组件,避免重复开发。

image-20230527215736955

3.3.1 全局组件

<!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>组件化</title>
</head>

<body>
<div id="app">
<button @click="count++">我被点击了{{count}}次</button>

<!-- 组件的使用 -->
<counter></counter>

</div>
<script src="./node_modules/vue/dist/vue.js"></script>
<script>
//1.全局声明一个组件
Vue.component("counter", {//counter是组件的名称,可以随便起名,见名知意就行
template: `<button @click="count++">我被点击了{{count}}次</button>`,//模板
data() { //模板中的属性信息,这里的data必须是一个函数
return {
count: 1
}
}
});
new Vue({
el: "#app",
data: {
count: 0
}
})

</script>
</body>

</html>
image-20230527220905922

3.3.2 局部组件

<!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>组件化</title>
</head>

<body>
<div id="app">
<button @click="count++">我被点击了{{count}}次</button>

<!-- 局部组件的使用 -->
<button-counter></button-counter>

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

//声明一个局部的组件
const buttonCounter = {
template: `<button @click="count++">我被点击了{{count}}次</button>`,
data() {
return {
count: 1
}
}
}
new Vue({
el: "#app",
data: {
count: 0
},
//声明一个局部的组件
components:{
'button-counter':buttonCounter
}
})

</script>
</body>

</html>

3.3 生命周期和钩子函数

生命周期图

image-20230527221756442

3.4 Vue模块化开发

1.全局安装webpack

npm install webpack -g

2.全局安装vue脚手架

npm install -g @vue/cli-init

3.初始化vue的项目

创建一个文件夹,打开cmd,切换到这个文件夹,执行以下的命令

# vue脚手架使用webpacke模板初始化一个项目
vue init webpack 项目名称

4.启动vue的项目

项目中的package.json中有scripts,代表我们能运行的命令

# 启动项目
npm start
#或者
npm run dev
# 将项目打包
npm run build

4.Eelment-UI

官方文档:https://element.eleme.cn/#/zh-CN/component/quickstart

]]>
前端 前端
压力测试与性能监控 /posts/1416.html 一.压力测试

性能指标

  • 响应时间(Response Time: RT)响应时间指用户从客户端发起一个请求开始,到客户端接收到从服务器端返回的响应结束,整个过程所耗费的时间。

  • HPS(Hits Per Second) :每秒点击次数,单位是次/秒。

  • TPS(Transaction per Second):系统每秒处理交易数,单位是笔/秒。

  • QPS(Query per Second):系统每秒处理查询次数,单位是次/秒。对于互联网业务中,如果某些业务有且仅有一个请求连接,那么 TPS=QPS=HPS,一般情况下用 TPS 来衡量整个业务流程,用 QPS 来衡量接口查询次数,用 HPS 来表示对服务器单击请求。

  • 无论 TPS、QPS、HPS,此指标是衡量系统处理能力非常重要的指标,越大越好,根据经验,一般情况下:

    金融行业:1000TPS~50000TPS,不包括互联网化的活动

    保险行业:100TPS~100000TPS,不包括互联网化的活动

    制造行业:10TPS~5000TPS

    互联网电子商务:10000TPS~1000000TPS

    互联网中型网站:1000TPS~50000TPS

    互联网小型网站:500TPS~10000TPS

  • 最大响应时间(Max Response Time) 指用户发出请求或者指令到系统做出反应(响应)的最大时间。

  • 最少响应时间(Mininum ResponseTime) 指用户发出请求或者指令到系统做出反应(响应)的最少时间。

  • 90%响应时间(90% Response Time) 是指所有用户的响应时间进行排序,第 90%的响应时间。

  • 从外部看,性能测试主要关注如下三个指标

    吞吐量:每秒钟系统能够处理的请求数、任务数。

    响应时间:服务处理一个请求或一个任务的耗时。

    错误率:一批请求中结果出错的请求所占比例。

1.JMeter的使用

1.1 安装

1.下载安装包

安装网址: https://jmeter.apache.org/download_jmeter.cgi

image-20230709161151542

2.解压并双击运行

image-20230709161346717

image-20230709161600398

1.2 设置

设置为中文页面

点击Options->Language->选择简体中文

image-20230709161752103

永久设置成中文->在bin\jmeter.properties配置文件中添加如下配置

image-20240105214825445

jmeter发送请求响应的数据乱码的问题->在bin\jmeter.properties配置文件中添加如下配置

image-20240105215115275

1.3 压测示例

1.新建线程组

右键点击测试计划->添加->线程->线程组

image-20230709162502383

线程组参数详解:

  • 线程数:虚拟用户数。一个虚拟用户占用一个进程或线程。设置多少虚拟用户数在这里也就是设置多少个线程数。

  • 线程数:虚拟用户数。一个虚拟用户占用一个进程或线程。设置多少虚拟用户数在这里也就是设置多少个线程数。

  • Ramp-Up Period(in seconds)准备时长:设置的虚拟用户数需要多长时间全部启动。如果线程数为 10,准备时长为 2,那么需要 2 秒钟启动10个线程,也就是每秒钟启动 5 个线程。

  • 循环次数:每个线程发送请求的次数。如果线程数为 10,循环次数为 100,那么每个线程发送 100 次请求。总请求数为 10*100=1000 。如勾选了“永远”,那么所有线程会一直发送请求,一到选择停止运行脚本。

  • Delay Thread creation until needed:直到需要时延迟线程的创建。

  • 调度器:设置线程组启动的开始时间和结束时间(配置调度器时,需要勾选循环次数为永远)

  • 持续时间(秒):测试持续时间,会覆盖结束时间

  • 启动延迟(秒):测试延迟启动时间,会覆盖启动时间

  • 启动时间:测试启动时间,启动延迟会覆盖它。当启动时间已过,手动只需测试时当前时间也会覆盖它。

  • 结束时间:测试结束时间,持续时间会覆盖它。

2.添加HTTP请求

image-20230709163303477

image-20230709163457833

3.添加监听器

image-20230709163639856

4.启动压测和查看分析结果

image-20230709164012746

image-20230709164208747

1.4 JMeter Address Already in use 错误解决

windows 本身提供的端口访问机制的问题。
Windows 提供给 TCP/IP 链接的端口为 1024-5000,并且要四分钟来循环回收他们。就导致
我们在短时间内跑大量的请求时将端口占满了。
1.cmd 中,用 regedit 命令打开注册表

2.在 HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Services\Tcpip\Parameters 下,
右击 parameters,添加一个新的 DWORD,名字为 MaxUserPort,然后双击 MaxUserPort,输入数值数据为 65534,基数选择十进制(如果是分布式运行的话,控制机器和负载机器都需要这样操作)

3.修改配置完毕之后记得重启机器才会生效
官网解释:https://support.microsoft.com/zh-cn/help/196271/when-you-try-to-connect-from-tcp-ports-greater-than-5000-you-receive-t
TCPTimedWaitDelay:30

二.性能监控

Jdk 的两个小工具 jconsole、jvisualvm(升级版的 jconsole);通过命令行启动,可监控本地和远程应用。

1.jconsole

image-20230709160343565

2.jvisualvm

jvisualvm相比jconsole功能更全,一般使用jvisualvm

image-20230709160552461

Visual GC插件的安装

点击工具->插件->在可用插件中选择Visual GC安装

安装成功之后显示如下

image-20230709160932416

image-20230709170626946

]]>
测试 测试
对象存储服务MinIO /posts/36397.html 一.MinIo基本介绍

1.简介

​ MinIO基于Apache License v2.0开源协议的对象存储服务,可以做为云存储的解决方案用来保存海量的图片,视频,文档。由于采用Golang实现,服务端可以工作在Windows,Linux, OS X和FreeBSD上。配置简单,基本是复制可执行程序,单行命令可以运行起来。MinIO兼容亚马逊S3云存储服务接口,非常适合于存储大容量非结构化的数据,例如图片、视频、日志文件、备份数据和容器/虚拟机镜像等,而一个对象文件可以是任意大小,从几kb到最大5T不等。

S3 ( Simple Storage Service简单存储服务)

基本概念

  • bucket – 类比于文件系统的目录
  • Object – 类比文件系统的文件
  • Keys – 类比文件名

官网文档:http://docs.minio.org.cn/docs/

2.特点

  • 数据保护

    Minio使用Minio Erasure Code(纠删码)来防止硬件故障。即便损坏一半以上的driver,但是仍然可以从中恢复。

  • 高性能

    作为高性能对象存储,在标准硬件条件下它能达到55GB/s的读、35GB/s的写速率

  • 可扩容

    不同MinIO集群可以组成联邦,并形成一个全局的命名空间,并跨越多个数据中心

  • SDK支持

    基于Minio轻量的特点,它得到类似Java、Python或Go等语言的sdk支持

  • 有操作页面

    面向用户友好的简单操作界面,非常方便的管理Bucket及里面的文件资源

  • 功能简单

    这一设计原则让MinIO不容易出错、更快启动

  • 丰富的API

    支持文件资源的分享连接及分享链接的过期策略、存储桶操作、文件列表访问及文件上传下载的基本功能等。

  • 文件变化主动通知

    存储桶(Bucket)如果发生改变,比如上传对象和删除对象,可以使用存储桶事件通知机制进行监控,并通过以下方式发布出去:AMQP、MQTT、Elasticsearch、Redis、NATS、MySQL、Kafka、Webhooks等。

3.对象存储方式比较

存储方式 优点 缺点
服务器磁盘 开发便捷,成本低 扩展困难
分布式文件系统 (如MinIo) 容易实现扩容 复杂度高
第三方存储 (如阿里OSS) 开发简单,功能强大,免维护 收费

4.分布式文件系统比较

存储方式 优点 缺点
FastDFS 1,主备服务,高可用 2,支持主从文件,支持自定义扩展名 3,支持动态扩容 1,没有完备官方文档,近几年没有更新 2,环境搭建较为麻烦
MinIO 1,性能高,准硬件条件下它能达到55GB/s的读、35GB/s的写速率 2,部署自带管理界面 3,MinIO.Inc运营的开源项目,社区活跃度高 4,提供了所有主流开发语言的SDK 1,不支持动态增加节点

二.MinIo安装教程

使用Docker的方式安装MInIo,Docker使用教程 https://jasonsgong.gitee.io/posts/19306.html

#拉取minio的镜像
docker pull minio/minio
#创建并运行容器
#设置用户名-e "MINIO_ACCESS_KEY=minio"
#设置密码 -e "MINIO_SECRET_KEY=minio123"
docker run -p 9000:9000 --name minio -d --restart=always -e "MINIO_ACCESS_KEY=minio" -e "MINIO_SECRET_KEY=minio123" -v /home/data:/data -v /home/config:/root/.minio minio/minio server /data

访问: http://192.168.200.130:9000

image-20230818145600002

Access Key为minio Secret_key 为minio123 进入系统后可以看到主界面

image-20230818145721071

三.快速入门

基本概念

  • bucket – 类比于文件系统的目录
  • Object – 类比文件系统的文件
  • Keys – 类比文件名

1.创建一个bucket

点击右下角的“+”号 ,创建一个桶

image-20230818150154249

创建成功后

image-20230818150902795

2.环境搭建

2.1 创建一个Demo工程

image-20230818151929679

2.2 导入依赖和创建启动类

pom.xml

<dependencies>

<dependency>
<groupId>io.minio</groupId>
<artifactId>minio</artifactId>
<version>7.1.0</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>

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

启动类

package com.heima.minio;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;

@SpringBootApplication
public class MinIOApplication {

public static void main(String[] args) {
SpringApplication.run(MinIOApplication.class,args);
}
}

3.测试案例

3.1 上传文件进行静态访问

目标:把test.html文件上传到minio中,并且可以在浏览器中访问

test.html测试文件

<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>Hello World!</title>
</head>
<body>
<b>普通文本 String 展示:</b><br><br>
Hello freemarker <br>
<hr>
<b>对象Student中的数据展示:</b><br/>
姓名:小明<br/>
年龄:18
<hr>
</body>
</html>

文件上传代码

package com.heima.minio;


/**
* @author Jason Gong
* @version 1.0
* @website https://jasonsgong.gitee.io
* @Date 2023/8/18
* @Description
*/
@SpringBootTest(classes = MinIOApplication.class)
@RunWith(SpringRunner.class)
@Slf4j
public class MinIOTest {

/**
* 把list.html文件上传到minio中,并且可以在浏览器中访问
*/
@Test
public void testMinIO(){
try {
//读取一个文件
FileInputStream inputStream = new FileInputStream("C:\\Gong\\data\\test.html");
//1.获取MinIO的连接信息,创建一个minio的客户端
MinioClient minioClient = MinioClient.builder()
.credentials("minio", "minio123")//minio的账号密码
.endpoint("http://192.168.200.130:9000")//minio的地址
.build();
//2.上传
PutObjectArgs putObjectArgs = PutObjectArgs.builder()
.object("test.html")//文件的名称
.contentType("text/html")//文件的类型
.bucket("leadnews")//桶的名称,与之前的minio管理界面创建的bucket名称一致即可
.stream(inputStream,inputStream.available(),-1)
.build();
//3.访问的路径 minio的访问路径 + 桶的名称 + 文件的名称
log.info("http://192.168.200.130:9000/leadnews/test.html");
minioClient.putObject(putObjectArgs);
} catch (Exception e) {
e.printStackTrace();
}
}

}

执行成功之后可以在MinIO中找到该文件

image-20230818154409682

设置浏览器输入文件在minio中的地址可以直接访问文件的内容

设置bucket的访问权限

image-20230818154858988

设置完毕后访问文件的地址可以直接访问到

image-20230818155021308

4.封装MinIO为Starter-以黑马头条项目为例

黑马头条项目gitee地址: https://gitee.com/JasonsGong/heima-leadnews

在开发的过程中,有很多模块需要使用到文件服务,我们直接把文件服务封装成一个Starter,方便其余的模块调用

image-20230818155501495

4.1 创建模块heima-file-starter

导入依赖

<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-autoconfigure</artifactId>
</dependency>
<dependency>
<groupId>io.minio</groupId>
<artifactId>minio</artifactId>
<version>7.1.0</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-configuration-processor</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
</dependencies>

4.2 配置类

MinIOConfigProperties

package com.heima.file.config;


import lombok.Data;
import org.springframework.boot.context.properties.ConfigurationProperties;

import java.io.Serializable;

@Data
@ConfigurationProperties(prefix = "minio")
public class MinIOConfigProperties implements Serializable {

private String accessKey;
private String secretKey;
private String bucket;
private String endpoint;
private String readPath;
}

MinIOConfig

package com.heima.file.config;

import com.heima.file.service.FileStorageService;
import io.minio.MinioClient;
import lombok.Data;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;


@Data
@Configuration
@EnableConfigurationProperties({MinIOConfigProperties.class})
//当引入FileStorageService接口时
@ConditionalOnClass(FileStorageService.class)
public class MinIOConfig {

@Autowired
private MinIOConfigProperties minIOConfigProperties;

@Bean
public MinioClient buildMinioClient(){
return MinioClient
.builder()
.credentials(minIOConfigProperties.getAccessKey(), minIOConfigProperties.getSecretKey())
.endpoint(minIOConfigProperties.getEndpoint())
.build();
}
}

4.3 封装操作minIO类

FileStorageService

package com.heima.file.service;

import java.io.InputStream;

/**
* @author itheima
*/
public interface FileStorageService {


/**
* 上传图片文件
* @param prefix 文件前缀
* @param filename 文件名
* @param inputStream 文件流
* @return 文件全路径
*/
public String uploadImgFile(String prefix, String filename,InputStream inputStream);

/**
* 上传html文件
* @param prefix 文件前缀
* @param filename 文件名
* @param inputStream 文件流
* @return 文件全路径
*/
public String uploadHtmlFile(String prefix, String filename,InputStream inputStream);

/**
* 删除文件
* @param pathUrl 文件全路径
*/
public void delete(String pathUrl);

/**
* 下载文件
* @param pathUrl 文件全路径
* @return
*
*/
public byte[] downLoadFile(String pathUrl);

}

MinIOFileStorageService

package com.heima.file.service.impl;


import com.heima.file.config.MinIOConfig;
import com.heima.file.config.MinIOConfigProperties;
import com.heima.file.service.FileStorageService;
import io.minio.GetObjectArgs;
import io.minio.MinioClient;
import io.minio.PutObjectArgs;
import io.minio.RemoveObjectArgs;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.context.annotation.Import;
import org.springframework.util.StringUtils;

import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.text.SimpleDateFormat;
import java.util.Date;

@Slf4j
@EnableConfigurationProperties(MinIOConfigProperties.class)
@Import(MinIOConfig.class)
public class MinIOFileStorageService implements FileStorageService {

@Autowired
private MinioClient minioClient;

@Autowired
private MinIOConfigProperties minIOConfigProperties;

private final static String separator = "/";

/**
* @param dirPath
* @param filename yyyy/mm/dd/file.jpg
* @return
*/
public String builderFilePath(String dirPath,String filename) {
StringBuilder stringBuilder = new StringBuilder(50);
if(!StringUtils.isEmpty(dirPath)){
stringBuilder.append(dirPath).append(separator);
}
SimpleDateFormat sdf = new SimpleDateFormat("yyyy/MM/dd");
String todayStr = sdf.format(new Date());
stringBuilder.append(todayStr).append(separator);
stringBuilder.append(filename);
return stringBuilder.toString();
}

/**
* 上传图片文件
* @param prefix 文件前缀
* @param filename 文件名
* @param inputStream 文件流
* @return 文件全路径
*/
@Override
public String uploadImgFile(String prefix, String filename,InputStream inputStream) {
String filePath = builderFilePath(prefix, filename);
try {
PutObjectArgs putObjectArgs = PutObjectArgs.builder()
.object(filePath)
.contentType("image/jpg")
.bucket(minIOConfigProperties.getBucket()).stream(inputStream,inputStream.available(),-1)
.build();
minioClient.putObject(putObjectArgs);
StringBuilder urlPath = new StringBuilder(minIOConfigProperties.getReadPath());
urlPath.append(separator+minIOConfigProperties.getBucket());
urlPath.append(separator);
urlPath.append(filePath);
return urlPath.toString();
}catch (Exception ex){
log.error("minio put file error.",ex);
throw new RuntimeException("上传文件失败");
}
}

/**
* 上传html文件
* @param prefix 文件前缀
* @param filename 文件名
* @param inputStream 文件流
* @return 文件全路径
*/
@Override
public String uploadHtmlFile(String prefix, String filename,InputStream inputStream) {
String filePath = builderFilePath(prefix, filename);
try {
PutObjectArgs putObjectArgs = PutObjectArgs.builder()
.object(filePath)
.contentType("text/html")
.bucket(minIOConfigProperties.getBucket()).stream(inputStream,inputStream.available(),-1)
.build();
minioClient.putObject(putObjectArgs);
StringBuilder urlPath = new StringBuilder(minIOConfigProperties.getReadPath());
urlPath.append(separator+minIOConfigProperties.getBucket());
urlPath.append(separator);
urlPath.append(filePath);
return urlPath.toString();
}catch (Exception ex){
log.error("minio put file error.",ex);
ex.printStackTrace();
throw new RuntimeException("上传文件失败");
}
}

/**
* 删除文件
* @param pathUrl 文件全路径
*/
@Override
public void delete(String pathUrl) {
String key = pathUrl.replace(minIOConfigProperties.getEndpoint()+"/","");
int index = key.indexOf(separator);
String bucket = key.substring(0,index);
String filePath = key.substring(index+1);
// 删除Objects
RemoveObjectArgs removeObjectArgs = RemoveObjectArgs.builder().bucket(bucket).object(filePath).build();
try {
minioClient.removeObject(removeObjectArgs);
} catch (Exception e) {
log.error("minio remove file error. pathUrl:{}",pathUrl);
e.printStackTrace();
}
}


/**
* 下载文件
* @param pathUrl 文件全路径
* @return 文件流
*
*/
@Override
public byte[] downLoadFile(String pathUrl) {
String key = pathUrl.replace(minIOConfigProperties.getEndpoint()+"/","");
int index = key.indexOf(separator);
String bucket = key.substring(0,index);
String filePath = key.substring(index+1);
InputStream inputStream = null;
try {
inputStream = minioClient.getObject(GetObjectArgs.builder().bucket(minIOConfigProperties.getBucket()).object(filePath).build());
} catch (Exception e) {
log.error("minio down file error. pathUrl:{}",pathUrl);
e.printStackTrace();
}

ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();
byte[] buff = new byte[100];
int rc = 0;
while (true) {
try {
if (!((rc = inputStream.read(buff, 0, 100)) > 0)) break;
} catch (IOException e) {
e.printStackTrace();
}
byteArrayOutputStream.write(buff, 0, rc);
}
return byteArrayOutputStream.toByteArray();
}
}

4.4 对外加入自动配置

在resources中新建META-INF/spring.factories

org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
com.heima.file.service.impl.MinIOFileStorageService

4.5 其他微服务使用

第一,导入heima-file-starter的依赖

<dependency>
<groupId>com.heima</groupId>
<artifactId>heima-file-starter</artifactId>
<version>1.0-SNAPSHOT</version>
</dependency>

第二,在微服务中添加minio所需要的配置

minio:
accessKey: minio
secretKey: minio123
bucket: leadnews
endpoint: http://192.168.200.130:9000
readPath: http://192.168.200.130:9000

第三,在对应使用的业务类中注入FileStorageService,样例如下:

@Autowired
private FileStorageService fileStorageService;
package com.heima.minio;


/**
* @author Jason Gong
* @version 1.0
* @website https://jasonsgong.gitee.io
* @Date 2023/8/18
* @Description
*/
@SpringBootTest(classes = MinIOApplication.class)
@RunWith(SpringRunner.class)
@Slf4j
public class MinIOTest {

@Autowired
private FileStorageService fileStorageService;

@Test
public void testMinIOStarter(){
//读取一个文件
try {
FileInputStream inputStream = new FileInputStream("C:\\Gong\\data\\test.html");
String s = fileStorageService.uploadHtmlFile("", "test,html", inputStream);
log.info(s);
} catch (FileNotFoundException e) {
e.printStackTrace();
}
}
}

image-20230818165138917

]]>
后端 MinIo
常用的DOS命令 /posts/51007.html 1.一些基础的DOS命令
c:\javacode>dir	  //查看C盘javacode目录下有那些文件
c:\javacode>cd /D d:   //从C盘切换到D盘
c:\javacode>help cd	  //解释cd是怎么使用的
c:\javacode>cd ..     //切换到上一级目录
c:\javacode>cd \      //切换到根目录
C:\tree c:\javacode   //查看C盘下的子目录javacode下面的所有子目录,形成一个目录树
>cls    //清空所有内容,清屏
>exit //退出控制台
md[创建目录],rd[删除目录],copy[拷贝文件],del[删除文件],echo[输入内容到文件],type,move[剪切]
>echo ok >pic.txt //创建pic.txt并把ok输入到pic.txt中
>echo nul >pic.txt //创建一个空文件pic.txt
>copy ok.txt d:\ok.txt //把ok.txt从当前路径拷贝到D盘下面,可以更改第二个ok.txt的文件名
>move ok.txt d:\ok.txt //把ok.txt剪切到D盘下面

2.网络编程相关的DOS命令

//获取IP相关的信息
>ipconfig
//端口监听和网络监听情况
>netstat -an
>netstat -an | more//分页显示
>netstat -anb
//ping命令,查看网络是否连通
>ping IP地址或者域名

3.MySql的相关DOS命令

//开启MySQL和关闭MySQL服务
>net start mysql //开
>new stop mysql //关


//登录mysql的命令
>mysql -u 用户名 -p密码 //密码可以先不输入,直接回车输入,或者直接写在p的后面,中间不要空格



//连接mysql服务(mysql数据库)的指令
>mysql -h 主机ip -P端口 -u 用户名 -p密码
//tips:
//p和密码中间不要有空格
//-p后面没有密码的话,会让回车的时候输入密码
//如果没有写-h主机,默认就是本机
//如果没有写-P端口,默认就是3306
//在实际的工作中端口一般会修改,使用3306这一端口,容易被黑客发现并攻击





//备份数据库的命令
//备份整个数据库
>mysqldump -u root -p -B 数据库 数据库 > c:\\test\\bak.sql
//备份数据库的某个表
>mysqldump -u root -p 数据库 表1 表2 表n > c:\\test\\bak.sql
//这里就是把数据库hsp_db02 和数据库hsp_db03备份在C盘下面test目录中一个叫bak.sql的文件中,这里备份的是sql语句




//根据备份的文件,恢复数据库
//第一种方式
>source c:\\test\\bak.sql //根据C盘下面的c:\\test\\bak.sql的文件恢复数据库
//第二种方式
//直接把这个.sql的文件再执行一遍,就会恢复两个数据库了

]]>
后端 常用命令
常用正则表达式大全 /posts/47003.html 一、校验数字的表达式
1 数字:^[0-9]$
2 n位的数字:^\d{n}$
3 至少n位的数字:^\d{n,}$
4 m-n位的数字:^\d{m,n}$
5 零和非零开头的数字:^(0|[1-9][0-9])$
6 非零开头的最多带两位小数的数字:^([1-9][0-9])+(.[0-9]{1,2})?$
7 带1-2位小数的正数或负数:^(-)?\d+(.\d{1,2})?$
8 正数、负数、和小数:^(-|+)?\d+(.\d+)?$
9 有两位小数的正实数:^[0-9]+(.[0-9]{2})?$
10 有1~3位小数的正实数:^[0-9]+(.[0-9]{1,3})?$
11 非零的正整数:^[1-9]\d$ 或 ^([1-9][0-9]){1,3}$ 或 ^+?[1-9][0-9]$
12 非零的负整数:^-[1-9][]0-9”$ 或 ^-[1-9]\d$
13 非负整数:^\d+$ 或 ^[1-9]\d|0$
14 非正整数:^-[1-9]\d|0$ 或 ^((-\d+)|(0+))$
15 非负浮点数:^\d+(.\d+)?$ 或 ^[1-9]\d.\d|0.\d[1-9]\d|0?.0+|0$
16 非正浮点数:^((-\d+(.\d+)?)|(0+(.0+)?))$ 或 ^(-([1-9]\d.\d|0.\d[1-9]\d))|0?.0+|0$
17 正浮点数:^[1-9]\d.\d|0.\d[1-9]\d$ 或 ^(([0-9]+.[0-9][1-9][0-9])|([0-9][1-9][0-9].[0-9]+)|([0-9][1-9][0-9]))$
18 负浮点数:^-([1-9]\d.\d|0.\d[1-9]\d)$ 或 ^(-(([0-9]+.[0-9][1-9][0-9])|([0-9][1-9][0-9].[0-9]+)|([0-9][1-9][0-9])))$
19 浮点数:^(-?\d+)(.\d+)?$ 或 ^-?([1-9]\d.\d|0.\d[1-9]\d|0?.0+|0)$

二、校验字符的表达式
1 汉字:^[\u4e00-\u9fa5]{0,}$
2 英文和数字:^[A-Za-z0-9]+$ 或 ^[A-Za-z0-9]{4,40}$
3 长度为3-20的所有字符:^.{3,20}$
4 由26个英文字母组成的字符串:^[A-Za-z]+$
5 由26个大写英文字母组成的字符串:^[A-Z]+$
6 由26个小写英文字母组成的字符串:^[a-z]+$
7 由数字和26个英文字母组成的字符串:^[A-Za-z0-9]+$
8 由数字、26个英文字母或者下划线组成的字符串:^\w+$ 或 ^\w{3,20}$
9 中文、英文、数字包括下划线:^[\u4E00-\u9FA5A-Za-z0-9_]+$
10 中文、英文、数字但不包括下划线等符号:^[\u4E00-\u9FA5A-Za-z0-9]+$ 或 ^[\u4E00-\u9FA5A-Za-z0-9]{2,20}$
11 可以输入含有^%&’,;=?$"等字符:[^%&’,;=?$\x22]+
12 禁止输入含有的字符:[^\x22]+

三、特殊需求表达式
1 Email地址:^\w+([-+.]\w+)@\w+([-.]\w+).\w+([-.]\w+)$
2 域名:[a-zA-Z0-9][-a-zA-Z0-9]{0,62}(/.[a-zA-Z0-9][-a-zA-Z0-9]{0,62})+/.?
3 InternetURL:[a-zA-z]+://[^\s] 或 ^https://([\w-]+.)+[\w-]+(/[\w-./?%&=])?$
4 手机号码:^(13[0-9]|14[5|7]|15[0|1|2|3|5|6|7|8|9]|18[0|1|2|3|5|6|7|8|9])\d{8}$
5 电话号码(“XXX-XXXXXXX”、”XXXX-XXXXXXXX”、”XXX-XXXXXXX”、”XXX-XXXXXXXX”、”XXXXXXX”和”XXXXXXXX):^((\d{3,4}-)|\d{3.4}-)?\d{7,8}$
6 国内电话号码(0511-4405222、021-87888822):\d{3}-\d{8}|\d{4}-\d{7}
7 身份证号:
15或18位身份证:^\d{15}|\d{18}$
15位身份证:^[1-9]\d{7}((0\d)|(1[0-2]))(([0|1|2]\d)|3[0-1])\d{3}$
18位身份证:^[1-9]\d{5}[1-9]\d{3}((0\d)|(1[0-2]))(([0|1|2]\d)|3[0-1])\d{4}$
8 短身份证号码(数字、字母x结尾):^([0-9]){7,18}(x|X)?$ 或 ^\d{8,18}|[0-9x]{8,18}|[0-9X]{8,18}?$
9 帐号是否合法(字母开头,允许5-16字节,允许字母数字下划线):^[a-zA-Z][a-zA-Z0-9_]{4,15}$
10 密码(以字母开头,长度在6~18之间,只能包含字母、数字和下划线):^[a-zA-Z]\w{5,17}$
11 强密码(必须包含大小写字母和数字的组合,不能使用特殊字符,长度在8-10之间):^(?=.\d)(?=.[a-z])(?=.[A-Z]).{8,10}$
12 日期格式:^\d{4}-\d{1,2}-\d{1,2}
13 一年的12个月(01~09和1~12):^(0?[1-9]|1[0-2])$
14 一个月的31天(01~09和1~31):^((0?[1-9])|((1|2)[0-9])|30|31)$
15 钱的输入格式:
16 1.有四种钱的表示形式我们可以接受:”10000.00” 和 “10,000.00”, 和没有 “分” 的 “10000” 和 “10,000”:^[1-9][0-9]$
17 2.这表示任意一个不以0开头的数字,但是,这也意味着一个字符”0”不通过,所以我们采用下面的形式:^(0|[1-9][0-9])$
18 3.一个0或者一个不以0开头的数字.我们还可以允许开头有一个负号:^(0|-?[1-9][0-9])$
19 4.这表示一个0或者一个可能为负的开头不为0的数字.让用户以0开头好了.把负号的也去掉,因为钱总不能是负的吧.下面我们要加的是说明可能的小数部分:^[0-9]+(.[0-9]+)?$
20 5.必须说明的是,小数点后面至少应该有1位数,所以”10.”是不通过的,但是 “10” 和 “10.2” 是通过的:^[0-9]+(.[0-9]{2})?$
21 6.这样我们规定小数点后面必须有两位,如果你认为太苛刻了,可以这样:^[0-9]+(.[0-9]{1,2})?$
22 7.这样就允许用户只写一位小数.下面我们该考虑数字中的逗号了,我们可以这样:^[0-9]{1,3}(,[0-9]{3})(.[0-9]{1,2})?$
23 8.1到3个数字,后面跟着任意个 逗号+3个数字,逗号成为可选,而不是必须:^([0-9]+|[0-9]{1,3}(,[0-9]{3}))(.[0-9]{1,2})?$
24 备注:这就是最终结果了,别忘了”+”可以用””替代如果你觉得空字符串也可以接受的话(奇怪,为什么?)最后,别忘了在用函数时去掉去掉那个反斜杠,一般的错误都在这里
25 xml文件:^([a-zA-Z]+-?)+[a-zA-Z0-9]+\.[x|X][m|M][l|L]$
26 中文字符的正则表达式:[\u4e00-\u9fa5]
27 双字节字符:[^\x00-\xff] (包括汉字在内,可以用来计算字符串的长度(一个双字节字符长度计2,ASCII字符计1))
28 空白行的正则表达式:\n\s\r (可以用来删除空白行)
29 HTML标记的正则表达式:<(\S?)[^>]>.?|<.? /> (网上流传的版本太糟糕,上面这个也仅仅能部分,对于复杂的嵌套标记依旧无能为力)
30 首尾空白字符的正则表达式:^\s|\s$或(^\s)|(\s$) (可以用来删除行首行尾的空白字符(包括空格、制表符、换页符等等),非常有用的表达式)
31 腾讯QQ号:[1-9][0-9]{4,} (腾讯QQ号从10000开始)
32 中国邮政编码:[1-9]\d{5}(?!\d) (中国邮政编码为6位数字)
33 IP地址:\d+.\d+.\d+.\d+ (提取IP地址时有用)

]]>
后端 正则表达式
常见问题和细节知识 /posts/14438.html 一.常见问题

1.前后端时间格式的问题

配置文件中设置时间的格式和时区

image-20230506130238222

2.MyBatis分页插件统计总记录数total失效的问题

检查一下分页插件的配置 选择以下正确的分页插件配置

package com.atguigu.common.config.mp;

import com.baomidou.mybatisplus.annotation.DbType;
import com.baomidou.mybatisplus.autoconfigure.ConfigurationCustomizer;
import com.baomidou.mybatisplus.extension.plugins.MybatisPlusInterceptor;
import com.baomidou.mybatisplus.extension.plugins.inner.PaginationInnerInterceptor;
import org.mybatis.spring.annotation.MapperScan;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

/**
* mp的分页插件
*/
@Configuration
@MapperScan("com.atguigu.auth.mapper")
public class MybatisPlusConfig {

/**
* 新的分页插件,一缓和二缓遵循mybatis的规则,需要设置 MybatisConfiguration#useDeprecatedExecutor = false 避免缓存出现问题(该属性会在旧插件移除后一同移除)
*/
@Bean
public MybatisPlusInterceptor mybatisPlusInterceptor() {
MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor();
interceptor.addInnerInterceptor(new PaginationInnerInterceptor(DbType.MYSQL));
return interceptor;
}

@Bean
public ConfigurationCustomizer configurationCustomizer() {
return configuration -> configuration.setUseDeprecatedExecutor(false);
}
}
package com.atguigu.config;

import com.baomidou.mybatisplus.core.injector.ISqlInjector;
import com.baomidou.mybatisplus.extension.injector.LogicSqlInjector;
import com.baomidou.mybatisplus.extension.plugins.PaginationInterceptor;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

/**
* @author GongChangjiang
* @version 1.0
* @Date 2023/3/4
* @Description mybatis-plus的配置文件
*/
@Configuration
public class MpConfig {

/**
* 分页插件
*/
@Bean
public PaginationInterceptor paginationInterceptor(){
return new PaginationInterceptor();
}
}

3.网关中的路由问题

网关中的路由匹配问题,模糊的路由放在精确的路由前面,容易是精确的路由配置失效

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}

4.Docker中部署mysql后SpringBoot连接时提示表不存在

使用Docer启动容器的时候设置忽略大小写,表不存在是因为表名大小写的原因。

方案一:启动的时候加上–lower_case_table_names= 1

docker run -p 3306:3306 --name mysql -v /usr/local/docker/mysql/conf:/etc/mysql -v /usr/local/docker/mysql/logs:/var/log/mysql -v /usr/local/docker/mysql/data:/var/lib/mysql -e MYSQL_ROOT_PASSWORD=123456 -d mysql:5.7 --lower_case_table_names= 1

方案二: 修改mysql挂载在宿主机的配置文件

# 在配置文件中添加如下的配置
lower_case_table_names=1

二.细节知识

1.Linux开启/关闭防火墙的命令

# 检查防火墙状态:
systemctl status firewalld
# 开启防火墙
systemctl start firewalld
# 关闭防火墙(有时间限制)
systemctl stop firewalld
# 设置开机禁用防火墙(先执行上面一条命令之后执行此命令,即可永久关闭)
systemctl disable firewalld.service
# 设置开机启用防火墙
systemctl enable firewalld.service

2.windows查看端口占用情况,杀死端口的命令

# 查询被占用的端口号的信息
netstat -ano | findstr "8080"
#根据端口号的PID杀死该端口的进程 其中 17156 是8080端口的PID值
taskkill /pid 17156 /f

3.Linux开启关闭键盘背光灯

#不是永久有效的
# 开启键盘背光灯
xset led named "Scroll Lock"
#关闭键盘背光灯
xset -led named "Scroll Lock"

4.关闭Mysql的服务

#查找是否安装了mysql的服务
rpm -qa | grep -i mysql
#查看mysql服务的状态
service mysqld status
#关闭mysql的服务
service mysqld stop
#重启的命令
service mysqld restart
#关闭开机自启动
systemctl disable mysqld

5.前端向后端传递对象数据

传递普通对象参数的写法

params: searchObj

getPageList(current,limit,searchObj){
return request({
url: `${api_name}/${current}/${limit}`,
method: 'get',
params: searchObj //使用这种方式进行传递
})
}
@GetMapping("/{page}/{limit}")
public Result pageQueryRole(@PathVariable Long page,
@PathVariable Integer limit,
SysRoleQueryVo sysRoleQueryVo) { //普通的对象进行接收 没有添加注解

Page<SysRole> pageParam = new Page<>(page, limit);
LambdaQueryWrapper<SysRole> queryWrapper = new LambdaQueryWrapper<>();
queryWrapper.like(!StringUtils.isEmpty(sysRoleQueryVo.getRoleName()),SysRole::getRoleName, sysRoleQueryVo.getRoleName());
Page<SysRole> sysRolePage = sysRoleService.page(pageParam, queryWrapper);
return Result.ok(sysRolePage);
}

json格式的传递

data: searchObj

add(sysRole){
return request({
url: `${api_name}/save`,
method: 'post',
data: sysRole //使用这种方式进行传递
})
}
@PostMapping("/save")
public Result save(@RequestBody SysRole sysRole){ //JSON格式对象的传递 需要使用注解@RequestBody
boolean isSave = sysRoleService.save(sysRole);
return isSave? Result.ok() : Result.fail();
}

6.调整日志的级别

logging:
level:
# 注意这里包的路径,要根据实际情况t
com.atguigu.gulimall: debug

7.@JsonInclude注解

@JsonInclude(JsonInclude.Include.NON_EMPTY)//这里是当children为空的时候,向前端传递数据就不带这个
private List<CategoryEntity> children;

8.node相关的问题

#查看node的版本
node -v
#查看npm的版本
npm -v
#查看npm使用的镜像源(npm镜像源)
npm get registry
#设置淘宝镜像源
npm config set registry http://registry.npm.taobao.org
]]>
后端 Java
开源项目-若依框架 /posts/5293.html 一.若依简介

官网简介

RuoYi 是一个后台管理系统,主要目的让开发者注重专注业务,降低技术难度,从而节省人力成本,缩短项目周期,提高软件安全质量。

单体版本技术栈:Spring Boot、Apache Shiro、MyBatis、Thymeleaf

前后端分离版本技术栈:SpringBoot、Spring Security、Jwt、Vue

微服务版本技术栈:Spring Boot、Spring Cloud & Alibaba

相关网站

总结

若依就是一个后台管理系统的通用模板,这个模板中包含了后台管理系统的常用功能,比如登录、权限管理、菜单管理、用户管理等,避免我们在开发中重复造轮子,让开发回归到业务本身。

二.下载使用

1.前置环境

后端:JDK、Maven、Mysql、Redis

前端:Node、Npm

2.下载运行

1.克隆命令

git clone https://gitee.com/y_project/RuoYi-Vue.git

2.使用Idea打开项目并初始化数据库脚本

执行克隆的项目工程目录下sql目录中的两个sql文件初始化数据库

3.修改Mysql和Redis的连接信息

在application.yml中修改redis的连接信息,在application-druid.yml中修改mysql的连接信息

application.yml

application-druid.yml

4.使用Vscode或者Idea打开前端项目并执行命令初始化前端项目

# 前端相关命令

# 进入项目目录
cd ruoyi-ui

# 安装依赖
npm install

# 建议不要直接使用 cnpm 安装依赖,会有各种诡异的 bug。可以通过如下操作解决 npm 下载速度慢的问题
npm install --registry=https://registry.npmmirror.com

# 启动服务
npm run dev

image-20240203135647790

三.项目功能实现逻辑

1.基本配置

1.1 浏览器请求过程

配置的原因是在前端解决跨域

1.浏览器发送http://localhost/prod-api/captchaImage 请求

2.前端接收http://localhost/prod-api/captchaImage 请求,通过下面的配置重写url,然后再将请求发送给后端(通过['^' + process.env.VUE_APP_BASE_API]: ''配置将http://localhost/prod-api/captchaImage重写为http://localhost:8080/captchaImage

该配置在vue.config.js配置文件中

devServer: {
host: '0.0.0.0',
port: port,
open: true,
proxy: {
// detail: https://cli.vuejs.org/config/#devserver-proxy
[process.env.VUE_APP_BASE_API]: {
//在这里我们我们可以把target修改为我们自己的后端服务地址
target: `http://localhost:8080`,
changeOrigin: true,
pathRewrite: {
//将VUE_APP_BASE_API(VUE_APP_BASE_API的值可能为dev-api、pood-api)置换为空,再在前面拼接target
['^' + process.env.VUE_APP_BASE_API]: ''
}
}
},
disableHostCheck: true
}

2.登录功能

image-20240203152609601

实现思路

1.后端生成一个计算表达式 ,例如 1+1=?@2

2.然后将1+1=?以流的方式传递给前端,前端展示为图片,同时后端也会传递一个UUID给前端,这个UUID为就是xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx,2作为结果value值存入Redis中。存入Redis中的key的格式为captcha_codes:xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx

3.用户在填写完表单之后会携带账号、密码、验证码的结果值,以及之前后端传递给前端的UUID值给后端,后端会先以captcha_codes:+UUID作为key在reidis中查找value值,再和前端输入的验证码结果值作比较,如果验证码相同再比较账号密码,账号密码在数据库中也可以匹配上就登录成功,反之返回给前端账号密码错误;如果验证码不相同,直接返回给前端,验证码错误。

代码实现

1.前端在页面初始化或者刷新验证码的时候会调用下面的方法获取验证码和UUID值

getCode() {
getCodeImg().then(res => {
this.captchaEnabled = res.captchaEnabled === undefined ? true : res.captchaEnabled;
if (this.captchaEnabled) {
this.codeUrl = "data:image/gif;base64," + res.img;
this.loginForm.uuid = res.uuid;
}
});
}

2.后端生成验证码的过程

/**
* 生成验证码
*/
@GetMapping("/captchaImage")
public AjaxResult getCode(HttpServletResponse response) throws IOException {
//创建一个ajax的返回值对象
AjaxResult ajax = AjaxResult.success();

//判断是否开启的验证码功能
boolean captchaEnabled = configService.selectCaptchaEnabled();
ajax.put("captchaEnabled", captchaEnabled);
if (!captchaEnabled) {
return ajax;
}

// 保存验证码信息
//获取uui
String uuid = IdUtils.simpleUUID();
//生成验证码的key值
String verifyKey = CacheConstants.CAPTCHA_CODE_KEY + uuid;

String capStr = null, code = null;
BufferedImage image = null;

// 生成验证码
String captchaType = RuoYiConfig.getCaptchaType();
if ("math".equals(captchaType)) {
String capText = captchaProducerMath.createText();
capStr = capText.substring(0, capText.lastIndexOf("@"));
code = capText.substring(capText.lastIndexOf("@") + 1);
image = captchaProducerMath.createImage(capStr);
} else if ("char".equals(captchaType)) {
capStr = code = captchaProducer.createText();
image = captchaProducer.createImage(capStr);
}

redisCache.setCacheObject(verifyKey, code, Constants.CAPTCHA_EXPIRATION, TimeUnit.MINUTES);
// 转换流信息写出
FastByteArrayOutputStream os = new FastByteArrayOutputStream();
try {
ImageIO.write(image, "jpg", os);
} catch (IOException e) {
return AjaxResult.error(e.getMessage());
}

ajax.put("uuid", uuid);
ajax.put("img", Base64.encode(os.toByteArray()));
return ajax;
}

3.前端登录提交的表单和处理登录的方法

data() {
return {
//验证码图片的链接
codeUrl: "",
//登录提交的表单信息
loginForm: {
//用户名
username: "admin",
//密码
password: "admin123",
//是否记住密码
rememberMe: false,
//验证码结果值
code: "",
//uuid值
uuid: ""
}
};
}
handleLogin() {
this.$refs.loginForm.validate(valid => {
if (valid) {
this.loading = true;
if (this.loginForm.rememberMe) {
Cookies.set("username", this.loginForm.username, { expires: 30 });
Cookies.set("password", encrypt(this.loginForm.password), { expires: 30 });
Cookies.set('rememberMe', this.loginForm.rememberMe, { expires: 30 });
} else {
Cookies.remove("username");
Cookies.remove("password");
Cookies.remove('rememberMe');
}
this.$store.dispatch("Login", this.loginForm).then(() => {
this.$router.push({ path: this.redirect || "/" }).catch(()=>{});
}).catch(() => {
this.loading = false;
if (this.captchaEnabled) {
this.getCode();
}
});
}
});
}

4.后端登录的实现逻辑

]]>
开源项目 若依
开发环境的搭建 /posts/63333.html 1.后端环境搭建

1.1 JDK环境的配置

常规的安装,无需教程

IDEA中设置默认的JDK

image-20230604120610791

选中需要设置为默认JDK的JDK

image-20230604120654078

1.2 Maven的安装与配置

环境变量的配置

MAVEN_HOME和Path 配置文件的配置

设置默认maven,每次创建新的工程的时候就会使用这个maven

image-20230604120307280

然后在弹出的页面中设置,这样每次创建新的工程就会使用当前设置的maven

image-20230604120436139

1.3 IDEA的配置

展开目录树 取消勾选下面的选项

image-20230604151530534

快捷键的配置 IDEA中的全局查找的快捷键会和windows的中文的繁简字体切换的快捷键冲突,直接关掉系统的快捷键

image-20230604121126549

页面中字体的配置

image-20230603202245269

代码字体的配置

image-20230603154531129

设置代码粗体

image-20230603154622726

打开的多个文件多行显示,不在同一行显示

image-20230611211438017

浅色主题的配置

image-20230815171925527

修改注释的颜色

修改多行注释的颜色: 修改Doc comment -> Text的颜色

image-20230815172326472

1.4 mysql的安装

具体的安装教程在《MySql5.7的安装教程的博客中》

1.5 Nacos的安装配置

github下载之后,直接解压运行

下载的地址:https://github.com/alibaba/nacos/releases

踩坑:单节点部署的话,启动的时候需要配置一下,不然启动的时候会闪退

image-20230603193632659

2.前端环境的搭建

2.1 Nodejs的安装与配置

下载的网址:https://nodejs.org/zh-cn/download/releases

选择.msi的下载即可

不用配置环境变量,一直下一步,直到安装成功过,node自带npm包管理工具

# 检验命令
node -v
npm -v

3.前后端公共环境的搭建

1.Git环境的搭建

#以下的操作在下载安装完毕之后进行
#1.鼠标在桌面右键 选择Git Bash Here 打开控制台

#2.配置用户名和邮箱
git config --global user.name "用户名" #随意
git config --global user.email "邮箱" #自己的邮箱

#3.配置SSH免密连接
#生成密钥
ssh-keygen -t rsa -C "在码云上注册的邮箱地址" #连续三次回车
#查看密钥并复制公钥的内容
cat ~/.ssh/id_rsa.pub

#4.将密钥的复制到码云的SSH公钥中
#4.1添加公钥 公钥名随意 公钥内容就是上面复制的内容

#5.测试
ssh -T git@gitee.com
]]>
后端 环境搭建
微信登录 /posts/45572.html 1、OAuth2

微信登录使用了OAuth2解决方案

1.1 OAuth2解决什么问题

1.1.1 开放系统间授权

照片拥有者想要在云冲印服务上打印照片,云冲印服务需要访问云存储服务上的资源

IMG_256

1.1.2图例

资源拥有者:照片拥有者

客户应用:云冲印

受保护的资源:照片

IMG_256

1.1.3方式一:用户名密码复制

IMG_258

用户将自己的”云存储”服务的用户名和密码,告诉”云冲印”,后者就可以读取用户的照片了。这样的做法有以下几个严重的缺点。

(1)”云冲印”为了后续的服务,会保存用户的密码,这样很不安全。

(2)Google不得不部署密码登录,而我们知道,单纯的密码登录并不安全。

(3)”云冲印”拥有了获取用户储存在Google所有资料的权力,用户没法限制”云冲印”获得授权的范围和有效期。

(4)用户只有修改密码,才能收回赋予”云冲印”的权力。但是这样做,会使得其他所有获得用户授权的第三方应用程序全部失效。

(5)只要有一个第三方应用程序被破解,就会导致用户密码泄漏,以及所有被密码保护的数据泄漏。

总结:

将受保护的资源中的用户名和密码存储在客户应用的服务器上,使用时直接使用这个用户名和密码登录

适用于同一公司内部的多个系统,不适用于不受信的第三方应用

1.1.4方式二:通用开发者key

适用于合作商或者授信的不同业务部门之间

IMG_259

1.1.5方式三:颁发令牌

接近OAuth2方式,需要考虑如何管理令牌、颁发令牌、吊销令牌,需要统一的协议,因此就有了OAuth2协议

IMG_256

令牌类比仆从钥匙

IMG_256

1.2 OAuth2最简向导

1.2.1 OAuth主要角色

IMG_256

1.2.2最简向导

川崎高彦:OAuth2领域专家,开发了一个OAuth2 sass服务,OAuth2 as Service,并且做成了一个公司

在融资的过程中为了向投资人解释OAuth2是什么,于是写了一篇文章,《OAuth2最简向导》

1.3 OAuth2的应用

1.3.1 微服务安全

现代微服务中系统微服务化以及应用的形态和设备类型增多,不能用传统的登录方式

核心的技术不是用户名和密码,而是token,由AuthServer颁发token,用户使用token进行登录

IMG_256

1.3.2 社交登录

2、微信登录介绍

2.1 前期准备

1、注册

微信开放平台:https://open.weixin.qq.com

2、邮箱激活

3、完善开发者资料

4、开发者资质认证

准备营业执照,1-2个工作日审批、300元

5、创建网站应用

提交审核,7个工作日审批

6、内网穿透

ngrok的使用

2.2 授权流程

参考文档:https://open.weixin.qq.com/cgi-bin/showdocument?action=dir_list&t=resource/res_list&verify=1&id=open1419316505&token=e547653f995d8f402704d5cb2945177dc8aa4e7e&lang=zh_CN

获取access_token时序图

IMG_256

第一步:请求CODE(生成授权URL)

第二步:通过code获取access_token(开发回调URL)

3、使用教程

由于微信登录需要企业用户才能注册,这里我们使用的是尚硅谷的密钥

当前项目的启动端口设置为 8160 才可以获取到登录的二维码

3.1 配置文件中添加配置

# 微信开放平台appid
wx.open.app_id=wxed9954c01bb89b47
# 微信开放平台appsecret
wx.open.app_secret=a7482517235173ddb4083788de60b90e
#回调的地址
wx.open.redirect_url=http://localhost:8160/api/ucenter/wx/callback
#前端项目的地址
yygh.baseUrl=http://localhost:3000

3.2 编写配置类读取配置文件中的值

import org.springframework.beans.factory.InitializingBean;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;

@Component
public class ConstantPropertiesUtil implements InitializingBean {

@Value("${wx.open.app_id}")
private String appId;

@Value("${wx.open.app_secret}")
private String appSecret;

@Value("${wx.open.redirect_url}")
private String redirectUrl;

@Value("${yygh.baseUrl}")
private String yyghBaseUrl;

public static String WX_OPEN_APP_ID;
public static String WX_OPEN_APP_SECRET;
public static String WX_OPEN_REDIRECT_URL;
public static String YYGH_BASE_URL;

@Override
public void afterPropertiesSet() throws Exception {
WX_OPEN_APP_ID = appId;
WX_OPEN_APP_SECRET = appSecret;
WX_OPEN_REDIRECT_URL = redirectUrl;
YYGH_BASE_URL = yyghBaseUrl;
}
}

3.3 生成二维码和扫描二维码之后的回调操作

3.3.1 生成二维码

生成内嵌的二维码

返回给前端的相关参数

参数 是否必须 说明
self_redirect true:手机点击确认登录后可以在 iframe 内跳转到 redirect_uri,false:手机点击确认登录后可以在 top window 跳转到 redirect_uri。默认为 false。
id 第三方页面显示二维码的容器id
appid 应用唯一标识,在微信开放平台提交应用审核通过后获得
scope 应用授权作用域,拥有多个作用域用逗号(,)分隔,网页应用目前仅填写snsapi_login即可
redirect_uri 重定向地址,需要进行UrlEncode
state 用于保持请求和回调的状态,授权请求后原样带回给第三方。该参数可用于防止csrf攻击(跨站请求伪造攻击),建议第三方带上该参数,可设置为简单的随机数加session进行校验
style 提供”black”、”white”可选,默认为黑色文字描述。详见文档底部FAQ
href 自定义样式链接,第三方可根据实际需求覆盖默认样式。详见文档底部FAQ

生成扫描二维码的后端部分

import com.atguigu.yygh.common.result.Result;
import com.atguigu.yygh.user.utils.ConstantPropertiesUtil;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.CrossOrigin;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.ResponseBody;

import java.io.UnsupportedEncodingException;
import java.net.URLEncoder;
import java.util.HashMap;

/**
* @author GongChangjiang
* @version 1.0
* @Date 2023/4/20
* @Description 微信登录的控制器方法
*/
@Controller
@RequestMapping("/api/ucenter/wx")
@CrossOrigin
public class WxController {


/**
* 生成微信扫描二维码
* 返回给前端一些生成二维码的参数
*/
@GetMapping("/getLoginParameters")
@ResponseBody
public Result getLoginParameters() {
try {
HashMap<String, Object> map = new HashMap<>();
map.put("appid", ConstantPropertiesUtil.WX_OPEN_APP_ID);
map.put("scope", "snsapi_login");
map.put("redirect_uri", URLEncoder.encode(ConstantPropertiesUtil.WX_OPEN_REDIRECT_URL, "utf-8"));
map.put("state", System.currentTimeMillis() + "");//原样返回,可以没有
return Result.ok(map);
} catch (UnsupportedEncodingException e) {
e.printStackTrace();
return Result.fail();
}
}
}

生成扫描二维码的前端部分

JS部分

import request from '@/utils/request';

export default {
//获取生成二维码需要的参数信息
getLoginParam() {
return request({
url: `/api/ucenter/wx/getLoginParam`,
method: 'get'
})
}
}

引入api

import weixinApi from "@/api/weixin";

在mounted()方法中初始化微信js

mounted() {
//初始化微信js
const script = document.createElement('script')
script.type = 'text/javascript'
script.src = 'https://res.wx.qq.com/connect/zh_CN/htmledition/js/wxLogin.js'
document.body.appendChild(script)
},

在页面中微信登录按钮绑定的是weixinLogin()点击事件,所以我们要在这个方法中初始化对象

向后端发起请求获取生成二维码需要的参数信息

weixinLogin() {
weixinApi.getLoginParam().then((response) => {
var obj = new WxLogin({
self_redirect: true,
id: "weixinLogin", // 需要显示的容器id <div id="weixinLogin"></div>
appid: response.data.appid, // appid
scope: response.data.scope, // 网页默认即可
redirect_uri: response.data.redirectUri, // 授权成功后回调的url
state: response.data.state, // 可设置为简单的随机数加session用来校验
style: "black", //。二维码的样式, 提供"black"、"white"可选
href: "", // 外部css文件url,需要https
});
});
},

点击微信登录之后,页面上的效果

image-20230421203717902

3.3.2 处理用户扫描成功之后的回调

实现的思路:

用户扫描二维码 -> 点击确认 -> 会执行回调请求(请求配置文件中这个地址wx.open.redirect_url=http://localhost:8160/api/ucenter/wx/callback) -> 获取回调请求中携带的参数,再根据参数获取扫描用户的信息,执行相关的操作

扫描成功之后发起如下的请求:

image-20230421205258647

image-20230421210454720

引入httpclient(不依赖浏览器发起请求)依赖

<dependency>
<groupId>org.apache.httpcomponents</groupId>
<artifactId>httpclient</artifactId>
<version>4.5.1</version>
</dependency>

导入HttpClientUtils工具类

import org.apache.commons.io.IOUtils;
import org.apache.commons.lang.StringUtils;
import org.apache.http.Consts;
import org.apache.http.HttpEntity;
import org.apache.http.HttpResponse;
import org.apache.http.NameValuePair;
import org.apache.http.client.HttpClient;
import org.apache.http.client.config.RequestConfig;
import org.apache.http.client.config.RequestConfig.Builder;
import org.apache.http.client.entity.UrlEncodedFormEntity;
import org.apache.http.client.methods.HttpGet;
import org.apache.http.client.methods.HttpPost;
import org.apache.http.conn.ConnectTimeoutException;
import org.apache.http.conn.ssl.SSLConnectionSocketFactory;
import org.apache.http.conn.ssl.SSLContextBuilder;
import org.apache.http.conn.ssl.TrustStrategy;
import org.apache.http.conn.ssl.X509HostnameVerifier;
import org.apache.http.entity.ContentType;
import org.apache.http.entity.StringEntity;
import org.apache.http.impl.client.CloseableHttpClient;
import org.apache.http.impl.client.HttpClients;
import org.apache.http.impl.conn.PoolingHttpClientConnectionManager;
import org.apache.http.message.BasicNameValuePair;
import javax.net.ssl.SSLContext;
import javax.net.ssl.SSLException;
import javax.net.ssl.SSLSession;
import javax.net.ssl.SSLSocket;
import java.io.IOException;
import java.net.SocketTimeoutException;
import java.security.GeneralSecurityException;
import java.security.cert.CertificateException;
import java.security.cert.X509Certificate;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Set;

/**
* @author GongChangjiang
* @version 1.0
* @Date 2023/4/21
* @Description
*/
public class HttpClientUtils {

public static final int connTimeout=10000;
public static final int readTimeout=10000;
public static final String charset="UTF-8";
private static HttpClient client = null;

static {
PoolingHttpClientConnectionManager cm = new PoolingHttpClientConnectionManager();
cm.setMaxTotal(128);
cm.setDefaultMaxPerRoute(128);
client = HttpClients.custom().setConnectionManager(cm).build();
}

public static String postParameters(String url, String parameterStr) throws ConnectTimeoutException, SocketTimeoutException, Exception{
return post(url,parameterStr,"application/x-www-form-urlencoded",charset,connTimeout,readTimeout);
}

public static String postParameters(String url, String parameterStr,String charset, Integer connTimeout, Integer readTimeout) throws ConnectTimeoutException, SocketTimeoutException, Exception{
return post(url,parameterStr,"application/x-www-form-urlencoded",charset,connTimeout,readTimeout);
}

public static String postParameters(String url, Map<String, String> params) throws ConnectTimeoutException,
SocketTimeoutException, Exception {
return postForm(url, params, null, connTimeout, readTimeout);
}

public static String postParameters(String url, Map<String, String> params, Integer connTimeout,Integer readTimeout) throws ConnectTimeoutException,
SocketTimeoutException, Exception {
return postForm(url, params, null, connTimeout, readTimeout);
}

public static String get(String url) throws Exception {
return get(url, charset, null, null);
}

public static String get(String url, String charset) throws Exception {
return get(url, charset, connTimeout, readTimeout);
}

/**
* 发送一个 Post 请求, 使用指定的字符集编码.
*
* @param url
* @param body RequestBody
* @param mimeType 例如 application/xml "application/x-www-form-urlencoded" a=1&b=2&c=3
* @param charset 编码
* @param connTimeout 建立链接超时时间,毫秒.
* @param readTimeout 响应超时时间,毫秒.
* @return ResponseBody, 使用指定的字符集编码.
* @throws ConnectTimeoutException 建立链接超时异常
* @throws SocketTimeoutException 响应超时
* @throws Exception
*/
public static String post(String url, String body, String mimeType,String charset, Integer connTimeout, Integer readTimeout)
throws ConnectTimeoutException, SocketTimeoutException, Exception {
HttpClient client = null;
HttpPost post = new HttpPost(url);
String result = "";
try {
if (StringUtils.isNotBlank(body)) {
HttpEntity entity = new StringEntity(body, ContentType.create(mimeType, charset));
post.setEntity(entity);
}
// 设置参数
Builder customReqConf = RequestConfig.custom();
if (connTimeout != null) {
customReqConf.setConnectTimeout(connTimeout);
}
if (readTimeout != null) {
customReqConf.setSocketTimeout(readTimeout);
}
post.setConfig(customReqConf.build());

HttpResponse res;
if (url.startsWith("https")) {
// 执行 Https 请求.
client = createSSLInsecureClient();
res = client.execute(post);
} else {
// 执行 Http 请求.
client = HttpClientUtils.client;
res = client.execute(post);
}
result = IOUtils.toString(res.getEntity().getContent(), charset);
} finally {
post.releaseConnection();
if (url.startsWith("https") && client != null&& client instanceof CloseableHttpClient) {
((CloseableHttpClient) client).close();
}
}
return result;
}


/**
* 提交form表单
*
* @param url
* @param params
* @param connTimeout
* @param readTimeout
* @return
* @throws ConnectTimeoutException
* @throws SocketTimeoutException
* @throws Exception
*/
public static String postForm(String url, Map<String, String> params, Map<String, String> headers, Integer connTimeout,Integer readTimeout) throws ConnectTimeoutException,
SocketTimeoutException, Exception {

HttpClient client = null;
HttpPost post = new HttpPost(url);
try {
if (params != null && !params.isEmpty()) {
List<NameValuePair> formParams = new ArrayList<NameValuePair>();
Set<Entry<String, String>> entrySet = params.entrySet();
for (Entry<String, String> entry : entrySet) {
formParams.add(new BasicNameValuePair(entry.getKey(), entry.getValue()));
}
UrlEncodedFormEntity entity = new UrlEncodedFormEntity(formParams, Consts.UTF_8);
post.setEntity(entity);
}

if (headers != null && !headers.isEmpty()) {
for (Entry<String, String> entry : headers.entrySet()) {
post.addHeader(entry.getKey(), entry.getValue());
}
}
// 设置参数
Builder customReqConf = RequestConfig.custom();
if (connTimeout != null) {
customReqConf.setConnectTimeout(connTimeout);
}
if (readTimeout != null) {
customReqConf.setSocketTimeout(readTimeout);
}
post.setConfig(customReqConf.build());
HttpResponse res = null;
if (url.startsWith("https")) {
// 执行 Https 请求.
client = createSSLInsecureClient();
res = client.execute(post);
} else {
// 执行 Http 请求.
client = HttpClientUtils.client;
res = client.execute(post);
}
return IOUtils.toString(res.getEntity().getContent(), "UTF-8");
} finally {
post.releaseConnection();
if (url.startsWith("https") && client != null
&& client instanceof CloseableHttpClient) {
((CloseableHttpClient) client).close();
}
}
}

/**
* 发送一个 GET 请求
*/
public static String get(String url, String charset, Integer connTimeout,Integer readTimeout)
throws ConnectTimeoutException,SocketTimeoutException, Exception {

HttpClient client = null;
HttpGet get = new HttpGet(url);
String result = "";
try {
// 设置参数
Builder customReqConf = RequestConfig.custom();
if (connTimeout != null) {
customReqConf.setConnectTimeout(connTimeout);
}
if (readTimeout != null) {
customReqConf.setSocketTimeout(readTimeout);
}
get.setConfig(customReqConf.build());

HttpResponse res = null;

if (url.startsWith("https")) {
// 执行 Https 请求.
client = createSSLInsecureClient();
res = client.execute(get);
} else {
// 执行 Http 请求.
client = HttpClientUtils.client;
res = client.execute(get);
}

result = IOUtils.toString(res.getEntity().getContent(), charset);
} finally {
get.releaseConnection();
if (url.startsWith("https") && client != null && client instanceof CloseableHttpClient) {
((CloseableHttpClient) client).close();
}
}
return result;
}

/**
* 从 response 里获取 charset
*/
@SuppressWarnings("unused")
private static String getCharsetFromResponse(HttpResponse ressponse) {
// Content-Type:text/html; charset=GBK
if (ressponse.getEntity() != null && ressponse.getEntity().getContentType() != null && ressponse.getEntity().getContentType().getValue() != null) {
String contentType = ressponse.getEntity().getContentType().getValue();
if (contentType.contains("charset=")) {
return contentType.substring(contentType.indexOf("charset=") + 8);
}
}
return null;
}

/**
* 创建 SSL连接
* @return
* @throws GeneralSecurityException
*/
private static CloseableHttpClient createSSLInsecureClient() throws GeneralSecurityException {
try {
SSLContext sslContext = new SSLContextBuilder().loadTrustMaterial(null, new TrustStrategy() {
public boolean isTrusted(X509Certificate[] chain,String authType) throws CertificateException {
return true;
}
}).build();

SSLConnectionSocketFactory sslsf = new SSLConnectionSocketFactory(sslContext, new X509HostnameVerifier() {

@Override
public boolean verify(String arg0, SSLSession arg1) {
return true;
}

@Override
public void verify(String host, SSLSocket ssl)
throws IOException {
}

@Override
public void verify(String host, X509Certificate cert)
throws SSLException {
}

@Override
public void verify(String host, String[] cns,
String[] subjectAlts) throws SSLException {
}
});
return HttpClients.custom().setSSLSocketFactory(sslsf).build();

} catch (GeneralSecurityException e) {
throw e;
}
}
}


Controller

package com.atguigu.yygh.user.controller;

import com.alibaba.fastjson.JSONObject;
import com.atguigu.yygh.common.result.Result;
import com.atguigu.yygh.user.utils.ConstantPropertiesUtil;
import com.atguigu.yygh.user.utils.HttpClientUtils;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.CrossOrigin;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.ResponseBody;

import java.io.UnsupportedEncodingException;
import java.net.URLEncoder;
import java.util.HashMap;

/**
* @author GongChangjiang
* @version 1.0
* @Date 2023/4/20
* @Description 微信登录的控制器方法
*/
@Controller
@RequestMapping("/api/ucenter/wx")
@CrossOrigin
public class WxController {


/**
* 生成微信扫描二维码
* 返回给前端一些生成二维码的参数
*/
@GetMapping("/getLoginParam")
@ResponseBody
public Result getLoginParameters() {
try {
HashMap<String, Object> map = new HashMap<>();
map.put("appid", ConstantPropertiesUtil.WX_OPEN_APP_ID);
map.put("scope", "snsapi_login");
map.put("redirect_uri", URLEncoder.encode(ConstantPropertiesUtil.WX_OPEN_REDIRECT_URL, "utf-8"));
map.put("state", System.currentTimeMillis() + "");//原样返回,可以没有
return Result.ok(map);
} catch (UnsupportedEncodingException e) {
e.printStackTrace();
return Result.fail();
}
}

/**
* 扫描之后回调的请求中携带了相关的参数,帮助我们获取用户的信息
* 登录成功之后的回调方法
* @param code 临时票据
* @param state 生成微信二维码之前put进去的参数 原样返回
* @return
*/
@GetMapping("/callback")
public String callback(String code ,String state){
//通过code、appid和app_secret去请求固定地址换取access_token
StringBuffer baseAccessTokenUrl = new StringBuffer()
.append("https://api.weixin.qq.com/sns/oauth2/access_token") //请求的固定地址
.append("?appid=%s") //%s占位符
.append("&secret=%s")
.append("&code=%s")
.append("&grant_type=authorization_code");

//拼接完整之后的地址
String accessTokenUrl = String.format(baseAccessTokenUrl.toString(),
ConstantPropertiesUtil.WX_OPEN_APP_ID,
ConstantPropertiesUtil.WX_OPEN_APP_SECRET,
code);

//使用httpclient去请求这个地址换取access_token
try {
String accessTokenInfo = HttpClientUtils.get(accessTokenUrl);//json格式的字符串
//从这个字符串中获取openid和 access_token的值
JSONObject jsonObject = JSONObject.parseObject(accessTokenInfo);
String accessToken = jsonObject.getString("access_token");
String openid = jsonObject.getString("openid");
//根据access_token和openid获取微信用户的基本信息
String baseUserInfoUrl = "https://api.weixin.qq.com/sns/userinfo" +
"?access_token=%s" +
"&openid=%s";
String userInfoUrl = String.format(baseUserInfoUrl, accessToken, openid);
//发起请求获取用户信息
String userInfo = HttpClientUtils.get(userInfoUrl);//json格式的字符串
JSONObject parseObject = JSONObject.parseObject(userInfo);
//获取具体的用户信息
String nickname = parseObject.getString("nickname");//用户昵称
String sex = parseObject.getString("sex");//性别
String language = parseObject.getString("language");//语言
String city = parseObject.getString("city");//所在城市
String province = parseObject.getString("province");//省份
String country = parseObject.getString("country");//注册国家
String headimgurl = parseObject.getString("headimgurl");//头像的url地址
System.out.println("昵称:"+nickname+",性别:"+sex+",语言:"+language+",城市:"+city+",省份:"+province+",注册国家:"+country+",头像地址:"+headimgurl);
//TODO 将扫码人的信息添加到数据库中
} catch (Exception e) {
e.printStackTrace();
}
return "redirect:地址信息&参数信息(用户信息或者token信息等)";
}
}
]]>
后端 Java
接口测试工具 /posts/35630.html 一 Postman的使用

官网网址: https://www.postman.com/

Postman的首页

image-20230813162510205

1 使用教程

1.1 发送Post请求

以一个普通的登录操作来演示Postman的使用教程

发送Post请求的: 1.修改请求的方式 2.设置请求体的数据类型

image-20230813163451814

请求数据的格式

{
"phone":"13511223456",
"password":"admin"
}

响应数据的格式

{
"host": null,
"code": 200,
"errorMessage": "操作成功",
"data": {
"user": {
"id": 4,
"salt": "",
"name": "admin",
"password": "",
"phone": "13511223456",
"image": null,
"sex": true,
"certification": null,
"identityAuthentication": null,
"status": true,
"flag": 1,
"createdTime": "2020-03-30T08:36:32.000+00:00"
},
"token": "eyJhbGciOiJIUzUxMiIsInppcCI6IkdaSVAifQ.H4sIAAAAAAAAADWLQQrDMAzA_uJzc3DnBKe_cRKPpVAIOIWNsb_PPfQmIfSFfXbYgCqlEkmDVI6BHlJCrrWF9IzKiMKNERboMmHDlDFjTMgL2Fn8to9NPa5u5vrSfoibnM1NxnDW97jPvF5n90a_P2LDCJyAAAAA.Rk6wC_YpLmxrqylQxEgz19uKHkrBRV6_JmIqbHrSQENkURtavrzhLzr0D8mQf-wtKivaGGviDGHmdckw8IWi7g"
}
}

二 Swagger的使用

SpringBoot整合Swagger:https://jasonsgong.gitee.io/posts/32246.html

以一个普通的登录操作来演示Swagger的使用教程

image-20230813170950932

image-20230813171244848

三 knife4j的使用

SpringBoot整合Knife4j教程: https://jasonsgong.gitee.io/posts/855.html

gitee地址:https://gitee.com/xiaoym/knife4j

官方文档:https://doc.xiaominfo.com/

核心功能 :文档说明 在线调试 个性化配置 离线文档 接口排序

image-20230813172211110

以一个普通的登录操作来演示knife4j的使用教程

image-20230813172355353

下载离线文档

image-20230813172605624

下载成功之后打开的效果

image-20230813172720116

]]>
后端 工具
数据校验 /posts/27166.html ​ 在前端接收数据和前端向后端传递数据的时候,都需要进行数据校验,避免传入错误的信息,比如在需要传入一个非空的值时,传入了一个空字符串,需要传入邮箱号码的时候,传入的非邮箱格式的数据。同时在写接口时经常要写效验请求参数逻辑,这时候我们会常用做法是写大量的 if 与 if else 类似这样的代码,大量if-else代码看起来比较混乱,降低了代码的可读性。

一.JSR303数据校验

1.引入依赖

<dependency>
<groupId>org.hibernate</groupId>
<artifactId>hibernate-validator</artifactId>
<version>6.0.7.Final</version>
</dependency>

2.给实体类添加校验注解,并定义自己的message提示

常用的检验注解

image-20230612170704286

示例

package com.atguigu.gulimall.product.entity;

import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;

import java.io.Serializable;

import lombok.Data;
import org.hibernate.validator.constraints.URL;

import javax.validation.constraints.Min;
import javax.validation.constraints.NotBlank;
import javax.validation.constraints.NotEmpty;
import javax.validation.constraints.Pattern;

/**
* 品牌
*
* @author JasonGong
* @email JasonGong@gmail.com
* @date 2023-05-19 00:23:36
*/
@Data
@TableName("pms_brand")
public class BrandEntity implements Serializable {
private static final long serialVersionUID = 1L;

/**
* 品牌id
*/
@TableId
private Long brandId;
/**
* 品牌名
*/
@NotBlank(message = "品牌名必须提交") //非空的校验
private String name;
/**
* 品牌logo地址
*/
@NotEmpty//既不能为空,也要符合URL的格式
@URL(message = "logo必须是一个合法的URL地址")//URL的校验
private String logo;
/**
* 介绍
*/
private String descript;
/**
* 显示状态[0-不显示;1-显示]
*/
private Integer showStatus;
/**
* 检索首字母
*/
@NotEmpty
@Pattern(regexp = "^[a-zA-Z]$", message = "检索首字母必须是一个字母")//正则表达式的校验,在java中使用正则的时候不需要/符号
private String firstLetter;
/**
* 排序
*/
@NotNull //Integer类型不为空,使用@NotNull不能使用@NotEmpty
@Min(value = 0, message = "排序必须大于等于0")//最小值是0
private Integer sort;

}

3.开启校验功能@Valid

/**
* 保存
* @Valid注解开启校验
* @param result 检验之后响应的结果信息
* 给校验的bean后紧跟一个BindingResult,就可以获取到校验的结果
*/
@RequestMapping("/save")
public R save(@Valid @RequestBody BrandEntity brand, BindingResult result/*紧跟在需要校验的字段后面*/){
if(result.hasErrors()){//判断有没有校验的错误
Map<String,String> map = new HashMap<>();
//获取检验的错误结果, result.getFieldErrors():校验出错的字段的集合
for (FieldError fieldError : result.getFieldErrors()) {
String defaultMessage = fieldError.getDefaultMessage();//获取到错误的提示
String field = fieldError.getField();//获取错误的属性(字段)的名字
//封装在一个集合中方便后期统一通过R返回结果
map.put(field,defaultMessage);
}
return R.error(400,"提交的数据不合法").put("data",map);
}else {
//校验合格的话,就做进行业务的处理
brandService.save(brand);
}
return R.ok();
}

测试查看返回的数据的格式,这里我们输入的都是不合法的数据格式,返回的结果如下

{
"msg": "提交的数据不合法",
"code": 400,
"data": {
"name": "品牌名必须提交",//字段:错误信息
"logo": "logo必须是一个合法的URL地址",
"sort": "排序必须大于等于0",
"firstLetter": "检索首字母必须是一个字母"
}
}

参数没有错误之后返回的数据

{
"msg": "success",
"code": 0
}

4.上面的代码过于冗余,我们可以直接使用统一异常处理处理数据校验的异常

在统一异常处理类上加上数据校验异常的异常处理

package com.atguigu.gulimall.product.exception;

import com.atguigu.common.utils.R;
import lombok.extern.slf4j.Slf4j;
import org.springframework.validation.BindingResult;
import org.springframework.validation.FieldError;
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;

/**
* @author Jason Gong
* @version 1.0
* @Date 2023/6/12
* @Description 异常处理类
*/
@Slf4j
@RestControllerAdvice(basePackages = "com.atguigu.gulimall.product.controller")
public class GulimallExceptionControllerAdvice {

@ExceptionHandler(value = MethodArgumentNotValidException.class)
public R handleValidException(MethodArgumentNotValidException e){
log.error("数据校验出现问题:{},异常类型:{}",e.getMessage(),e.getClass());
Map<String,String> map = new HashMap<>();
//获取检验的错误结果
BindingResult result = e.getBindingResult();
for (FieldError fieldError : result.getFieldErrors()) {
//获取到错误的提示
String defaultMessage = fieldError.getDefaultMessage();
//获取错误的属性的名字
String field = fieldError.getField();
map.put(field,defaultMessage);
}
return R.error(400,"数据校验出现问题").put("data",map);
}
}

这个时候上面的代码就可以简化为下面的格式,数据校验出现问题之后就直接在统一异常处理中处理了

/**
* 保存
*/
@RequestMapping("/save")
public R save(@Valid @RequestBody BrandEntity brand ){
brandService.save(brand);
return R.ok();
}

5.分组校验功能

 例如:当我们在添加一个品牌的时候,我们不需要传入这个品牌的id信息,需要这个品牌的品牌名信息,但是在修改这个品牌的时候,我们需要这个品牌的id信息和品牌名的信息,这时我们就需要使用分组校验了

5.1 定义空接口,作为分组校验的组

添加操作的组

package com.atguigu.common.valid;

public interface AddGroup {
}

修改操作的组

package com.atguigu.common.valid;

public interface UpdateGroup {
}

5.2 在实体类上添加上分组的信息

注意:使用了分组校验之后,其余的字段也要加上分组信息,否则没有加上分组信息的会失效

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 lombok.Data;
import org.hibernate.validator.constraints.URL;

import javax.validation.constraints.*;

@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 = {AddGroup.class, UpdateGroup.class})//这个就是在添加和修改的组中都需要进行校验
private String name;
}

5.3 在控制层添加上相应的注解信息,指定当前是操作属于的分组

注意:这里如果产生数据校验的出现问题的异常,会由统一异常处理进行处理

保存的控制器方法上添加上添加分组信息

/**
* 保存
* @Validated({AddGroup.class}) 指定使用添加分组的校验
*/
@RequestMapping("/save")
public R save(@Validated({AddGroup.class}) @RequestBody BrandEntity brand ){
brandService.save(brand);
return R.ok();
}

修改的分组上添加上修改的分组信息

/**
* 修改
*/
@RequestMapping("/update")
public R update(@Validated({UpdateGroup.class}) @RequestBody BrandEntity brand){
brandService.updateById(brand);

return R.ok();
}

测试:我们在新增的时候加上品牌的id,这时就会产生错误

image-20230614105450858

修改的时候不带品牌的id信息

image-20230614111024292

6.自定义校验

这里我们以编写一个输入的值只能是指定值的注解为例

 /**
* 显示状态[0-不显示;1-显示]
*/
//注意:我们使用了分组校验,所以这里也要加上分组groups
@ListValue(vals = {0, 1}, groups = {AddGroup.class, UpdateGroup.class})//自定义的注解,输入的时候只能是0或者1
private Integer showStatus;

6.1 编写一个自定义的校验注解

编写自定义注解

package com.atguigu.common.valid;

import javax.validation.Constraint;
import javax.validation.Payload;

import java.lang.annotation.Documented;

import java.lang.annotation.Retention;
import java.lang.annotation.Target;

import static java.lang.annotation.ElementType.*;
import static java.lang.annotation.ElementType.TYPE_USE;
import static java.lang.annotation.RetentionPolicy.RUNTIME;

/**
* @author Jason Gong
* @version 1.0
* @Date 2023/6/15
* @Description 自定义的校验注解
*/
@Documented
@Constraint(validatedBy = {})//校验注解使用的校验器
@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 {};//使用注解的时候会有vals属性 在属性里面指定值
}

编写注解中默认提示消息的配置文件

ValidationMessages.properties

com.atguigu.common.valid.ListValue.message=必须提交指定的值

6.2 编写一个自定义的校验器

package com.atguigu.common.valid;

import javax.validation.ConstraintValidator;
import javax.validation.ConstraintValidatorContext;
import java.util.HashSet;
import java.util.Set;

/**
* @author Jason Gong
* @version 1.0
* @Date 2023/6/15
* @Description 自定义的校验器
*/
public class ListValueConstraintValidator implements ConstraintValidator<ListValue,Integer> {
private Set<Integer> set = new HashSet<>();//创建一个set集合 用于存储指定输入的值0和1
//初始化方法
@Override
public void initialize(ListValue constraintAnnotation) {
int[] vals = constraintAnnotation.vals();//@ListValue(vals = {0, 1})
for (int val : vals) {
set.add(val);
}
}

//判断是否校验成功
//value:需要校验的值
@Override
public boolean isValid(Integer integer, ConstraintValidatorContext constraintValidatorContext) {
return set.contains(integer);//判断真实树的值在不在包含0和1的这个set集合中
}
}

6.3 关联自定义的校验器和校验注解

在自定义的注解上面关联上上面自定义的校验规则

image-20230615151144311

测试自定义的注解

image-20230615152331597

]]>
后端 Java
特殊符号大全 /posts/12367.html 中文字符

零壹贰叁肆伍陆柒捌玖拾佰仟万亿吉太拍艾分厘毫微卍卐卄巜弍弎弐朤氺曱甴囍兀々〆のぁ〡〢〣〤〥〦〧〨〩㊎㊍㊌㊋㊏㊚㊛㊐㊊㊣㊤㊥㊦㊧㊨㊒㊫㊑㊓㊔㊕㊖㊗㊘㊜㊝㊞㊟㊠㊡㊢㊩㊪㊬㊭㊮㊯㊰㊀㊁㊂㊃㊄㊅㊆㊇㊈㊉

常用特殊符号

❤❥웃유♋☮✌☏☢☠✔☑♚▲♪✈✞÷↑↓◆◇⊙■□△▽¿─│♥❣♂♀☿Ⓐ✍✉☣☤✘☒♛▼♫⌘☪≈←→◈◎☉★☆⊿※¡━┃♡ღツ☼☁❅♒✎©®™Σ✪✯☭➳卐√↖↗●◐Θ◤◥︻〖〗┄┆℃℉°✿ϟ☃☂✄¢€£∞✫★½✡×↙↘○◑⊕◣◢︼【】┅┇☽☾✚〓▂▃▄▅▆▇█▉▊▋▌▍▎▏↔↕☽☾の•▸◂▴▾┈┊①②③④⑤⑥⑦⑧⑨⑩ⅠⅡⅢⅣⅤⅥⅦⅧⅨⅩ㍿▓♨♛❖♓☪✙┉┋☹☺☻تヅツッシÜϡﭢ™℠℗©®♥❤❥❣❦❧♡۵웃유ღ♋♂♀☿☼☀☁☂☄☾☽❄☃☈⊙☉℃℉❅✺ϟ☇♤♧♡♢♠♣♥♦☜☞☝✍☚☛☟✌✽✾✿❁❃❋❀⚘☑✓✔√☐☒✗✘ㄨ✕✖✖⋆✢✣✤✥❋✦✧✩✰✪✫✬✭✮✯❂✡★✱✲✳✴✵✶✷✸✹✺✻✼❄❅❆❇❈❉❊†☨✞✝☥☦☓☩☯☧☬☸✡♁✙♆。,、':∶;?‘’“”〝〞ˆˇ﹕︰﹔﹖﹑•¨….¸;!´?!~—ˉ|‖"〃`@﹫¡¿﹏﹋﹌︴々﹟#﹩$﹠&﹪%*﹡﹢﹦﹤‐ ̄¯―﹨ˆ˜﹍﹎+=<__-\ˇ~﹉﹊()〈〉‹›﹛﹜『』〖〗[]《》〔〕{}「」【】︵︷︿︹︽_﹁﹃︻︶︸﹀︺︾ˉ﹂﹄︼☩☨☦✞✛✜✝✙✠✚†‡◉○◌◍◎●◐◑◒◓◔◕◖◗❂☢⊗⊙◘◙◍⅟½⅓⅕⅙⅛⅔⅖⅚⅜¾⅗⅝⅞⅘≂≃≄≅≆≇≈≉≊≋≌≍≎≏≐≑≒≓≔≕≖≗≘≙≚≛≜≝≞≟≠≡≢≣≤≥≦≧≨≩⊰⊱⋛⋚∫∬∭∮∯∰∱∲∳%℅‰‱㊣㊎㊍㊌㊋㊏㊐㊊㊚㊛㊤㊥㊦㊧㊨㊒㊞㊑㊒㊓㊔㊕㊖㊗㊘㊜㊝㊟㊠㊡㊢㊩㊪㊫㊬㊭㊮㊯㊰㊙㉿囍♔♕♖♗♘♙♚♛♜♝♞♟ℂℍℕℙℚℝℤℬℰℯℱℊℋℎℐℒℓℳℴ℘ℛℭ℮ℌℑℜℨ♪♫♩♬♭♮♯°øⒶ☮✌☪✡☭✯卐✐✎✏✑✒✍✉✁✂✃✄✆✉☎☏➟➡➢➣➤➥➦➧➨➚➘➙➛➜➝➞➸♐➲➳⏎➴➵➶➷➸➹➺➻➼➽←↑→↓↔↕↖↗↘↙↚↛↜↝↞↟↠↡↢↣↤↥↦↧↨➫➬➩➪➭➮➯➱↩↪↫↬↭↮↯↰↱↲↳↴↵↶↷↸↹↺↻↼↽↾↿⇀⇁⇂⇃⇄⇅⇆⇇⇈⇉⇊⇋⇌⇍⇎⇏⇐⇑⇒⇓⇔⇕⇖⇗⇘⇙⇚⇛⇜⇝⇞⇟⇠⇡⇢⇣⇤⇥⇦⇧⇨⇩⇪➀➁➂➃➄➅➆➇➈➉➊➋➌➍➎➏➐➑➒➓㊀㊁㊂㊃㊄㊅㊆㊇㊈㊉ⒶⒷⒸⒹⒺⒻⒼⒽⒾⒿⓀⓁⓂⓃⓄⓅⓆⓇⓈⓉⓊⓋⓌⓍⓎⓏⓐⓑⓒⓓⓔⓕⓖⓗⓘⓙⓚⓛⓜⓝⓞⓟⓠⓡⓢⓣⓤⓥⓦⓧⓨⓩ⒜⒝⒞⒟⒠⒡⒢⒣⒤⒥⒦⒧⒨⒩⒪⒫⒬⒭⒮⒯⒰⒱⒲⒳⒴⒵ⅠⅡⅢⅣⅤⅥⅦⅧⅨⅩⅪⅫⅬⅭⅮⅯⅰⅱⅲⅳⅴⅵⅶⅷⅸⅹⅺⅻⅼⅽⅾⅿ┌┍┎┏┐┑┒┓└┕┖┗┘┙┚┛├┝┞┟┠┡┢┣┤┥┦┧┨┩┪┫┬┭┮┯┰┱┲┳┴┵┶┷┸┹┺┻┼┽┾┿╀╁╂╃╄╅╆╇╈╉╊╋╌╍╎╏═║╒╓╔╕╖╗╘╙╚╛╜╝╞╟╠╡╢╣╤╥╦╧╨╩╪╫╬◤◥◄►▶◀◣◢▲▼◥▸◂▴▾△▽▷◁⊿▻◅▵▿▹◃❏❐❑❒▀▁▂▃▄▅▆▇▉▊▋█▌▍▎▏▐░▒▓▔▕■□▢▣▤▥▦▧▨▩▪▫▬▭▮▯㋀㋁㋂㋃㋄㋅㋆㋇㋈㋉㋊㋋㏠㏡㏢㏣㏤㏥㏦㏧㏨㏩㏪㏫㏬㏭㏮㏯㏰㏱㏲㏳㏴㏵㏶㏷㏸㏹㏺㏻㏼㏽㏾㍙㍚㍛㍜㍝㍞㍟㍠㍡㍢㍣㍤㍥㍦㍧㍨㍩㍪㍫㍬㍭㍮㍯㍰㍘☰☲☱☴☵☶☳☷☯

十二星座符号

牧羊 金牛 双子 巨蟹 狮子 处女 天秤 天蝎 射手 摩羯 水瓶 双鱼

特殊符号

♠♣♧♡♥❤❥❣♂♀✲☀☼☾☽◐◑☺☻☎☏✿❀№↑↓←→√×÷★℃℉°◆◇⊙■□△▽¿½☯✡㍿卍卐♂♀✚〓㎡♪♫♩♬㊚㊛囍㊒㊖Φ♀♂‖$@*&#※卍卐Ψ♫♬♭♩♪♯♮⌒¶∮‖€£¥$

编号序号

①②③④⑤⑥⑦⑧⑨⑩⑪⑫⑬⑭⑮⑯⑰⑱⑲⑳⓪❶❷❸❹❺❻❼❽❾❿⓫⓬⓭⓮⓯⓰⓱⓲⓳⓴㊀㊁㊂㊃㊄㊅㊆㊇㊈㊉㈠㈡㈢㈣㈤㈥㈦㈧㈨㈩⑴⑵⑶⑷⑸⑹⑺⑻⑼⑽⑾⑿⒀⒁⒂⒃⒄⒅⒆⒇⒈⒉⒊⒋⒌⒍⒎⒏⒐⒑⒒⒓⒔⒕⒖⒗⒘⒙⒚⒛ⅠⅡⅢⅣⅤⅥⅦⅧⅨⅩⅪⅫⅰⅱⅲⅳⅴⅵⅶⅷⅸⅹⒶⒷⒸⒹⒺⒻⒼⒽⒾⒿⓀⓁⓂⓃⓄⓅⓆⓇⓈⓉⓊⓋⓌⓍⓎⓏⓐⓑⓒⓓⓔⓕⓖⓗⓘⓙⓚⓛⓜⓝⓞⓟⓠⓡⓢⓣⓤⓥⓦⓧⓨⓩ⒜⒝⒞⒟⒠⒡⒢⒣⒤⒥⒦⒧⒨⒩⒪⒫⒬⒭⒮⒯⒰⒱⒲⒳⒴⒵

数学符号

﹢﹣×÷±/=≌∽≦≧≒﹤﹥≈≡≠=≤≥<>≮≯∷∶∫∮∝∞∧∨∑∏∪∩∈∵∴⊥∥∠⌒⊙√∟⊿㏒㏑%‰⅟½⅓⅕⅙⅛⅔⅖⅚⅜¾⅗⅝⅞⅘≂≃≄≅≆≇≈≉≊≋≌≍≎≏≐≑≒≓≔≕≖≗≘≙≚≛≜≝≞≟≠≡≢≣≤≥≦≧≨≩⊰⊱⋛⋚∫∬∭∮∯∰∱∲∳%℅‰‱øØπ

爱心符号

♥❣ღ♠♡♤❤❥

标点符号

。,、':∶;?‘’“”〝〞ˆˇ﹕︰﹔﹖﹑•¨….¸;!´?!~—ˉ|‖"〃`@﹫¡¿﹏﹋﹌︴々﹟#﹩$﹠&﹪%*﹡﹢﹦﹤‐ ̄¯―﹨ˆ˜﹍﹎+=<__-\ˇ~﹉﹊()〈〉‹›﹛﹜『』〖〗[]《》〔〕{}「」【】︵︷︿︹︽_﹁﹃︻︶︸﹀︺︾ˉ﹂﹄︼❝❞

单位符号

°′″$¥〒¢£%@℃℉﹩﹪‰﹫㎡㏕㎜㎝㎞㏎m³㎎㎏㏄º○¤%$º¹²³

货币符号

€£Ұ₴$₰¢₤¥₳₲₪₵元₣₱฿¤₡₮₭₩ރ円₢₥₫₦zł﷼₠₧₯₨Kčर₹ƒ₸¢

箭头符号

↑↓←→↖↗↘↙↔↕➻➼➽➸➳➺➻➴➵➶➷➹▶►▷◁◀◄«»➩➪➫➬➭➮➯➱⏎➲➾➔➘➙➚➛➜➝➞➟➠➡➢➣➤➥➦➧➨↚↛↜↝↞↟↠↠↡↢↣↤↤↥↦↧↨⇄⇅⇆⇇⇈⇉⇊⇋⇌⇍⇎⇏⇐⇑⇒⇓⇔⇖⇗⇘⇙⇜↩↪↫↬↭↮↯↰↱↲↳↴↵↶↷↸↹☇☈↼↽↾↿⇀⇁⇂⇃⇞⇟⇠⇡⇢⇣⇤⇥⇦⇧⇨⇩⇪↺↻⇚⇛♐

符号图案

✐✎✏✑✒✍✉✁✂✃✄✆✉☎☏☑✓✔√☐☒✗✘ㄨ✕✖✖☢☠☣✈★☆✡囍㍿☯☰☲☱☴☵☶☳☷☜☞☝✍☚☛☟✌♤♧♡♢♠♣♥♦☀☁☂❄☃♨웃유❖☽☾☪✿♂♀✪✯☭➳卍卐√×■◆●○◐◑✙☺☻❀⚘♔♕♖♗♘♙♚♛♜♝♞♟♧♡♂♀♠♣♥❤☜☞☎☏⊙◎☺☻☼▧▨♨◐◑↔↕▪▒◊◦▣▤▥▦▩◘◈◇♬♪♩♭♪の★☆→あぃ£Ю〓§♤♥▶¤✲❈✿✲❈➹☀☂☁【】┱┲❣✚✪✣✤✥✦❉❥❦❧❃❂❁❀✄☪☣☢☠☭ღ▶▷◀◁☀☁☂☃☄★☆☇☈⊙☊☋☌☍ⓛⓞⓥⓔ╬『』∴☀♫♬♩♭♪☆∷﹌の★◎▶☺☻►◄▧▨♨◐◑↔↕↘▀▄█▌◦☼♪の☆→♧ぃ£❤▒▬♦◊◦♠♣▣۰•❤•۰►◄▧▨♨◐◑↔↕▪▫☼♦⊙●○①⊕◎Θ⊙¤㊣★☆♀◆◇◣◢◥▲▼△▽⊿◤◥✐✌✍✡✓✔✕✖♂♀♥♡☜☞☎☏⊙◎☺☻►◄▧▨♨◐◑↔↕♥♡▪▫☼♦▀▄█▌▐░▒▬♦◊◘◙◦☼♠♣▣▤▥▦▩◘◙◈♫♬♪♩♭♪✄☪☣☢☠♯♩♪♫♬♭♮☎☏☪♈ºº₪¤큐«»™♂✿♥ ◕‿-。 。◕‿◕。

希腊字母

ΑΒΓΔΕΖΗΘΙΚΛΜΝΞΟΠΡΣΤΥΦΧΨΩαβγδεζνξοπρσηθικλμτυφχψω

俄语字母

АБВГДЕЁЖЗИЙКЛМНОПРСТУФХЦЧШЩЪЫЬЭЮЯабвгдеёжзийклмнопрстуфхцчшщъыьэюя

汉语拼音

āáǎàōóǒòēéěèīíǐìūúǔùǖǘǚǜüêɑńňɡㄅㄆㄇㄈㄉㄊㄋㄌㄍㄎㄏㄐㄑㄒㄓㄔㄕㄖㄗㄘㄙㄚㄛㄜㄝㄞㄟㄠㄡㄢㄣㄤㄥㄦㄧㄨㄩ

日文平假名片假名

ぁあぃいぅうぇえぉおかがきぎくぐけげこごさざしじすずせぜそぞただちぢっつづてでとどなにぬねのはばぱひびぴふぶぷへべぺほぼぽまみむめもゃやゅゆょよらりるれろゎわゐゑをんゔゕゖァアィイゥウェエォオカガキギクグケゲコゴサザシジスズセゼソゾタダチヂッツヅテデトドナニヌネノハバパヒビピフブプヘベペホボポマミムメモャヤュユョヨラリルレロヮワヰヱヲンヴヵヶヷヸヹヺ・ーヽヾヿ゠ㇰㇱㇲㇳㇴㇵㇶㇷㇸㇹㇺㇻㇼㇽㇾㇿ

制表符

─ ━│┃╌╍╎╏┄ ┅┆┇┈ ┉┊┋┌┍┎┏┐┑┒┓└ ┕┖┗ ┘┙┚┛├┝┞┟┠┡┢┣ ┤┥┦┧┨┩┪┫┬ ┭ ┮ ┯ ┰ ┱ ┲ ┳ ┴ ┵ ┶ ┷ ┸ ┹ ┺ ┻┼ ┽ ┾ ┿ ╀ ╁ ╂ ╃ ╄ ╅ ╆ ╇ ╈ ╉ ╊ ╋ ╪ ╫ ╬═║╒╓╔ ╕╖╗╘╙╚ ╛╜╝╞╟╠ ╡╢╣╤ ╥ ╦ ╧ ╨ ╩ ╳╔ ╗╝╚ ╬ ═ ╓ ╩ ┠ ┨┯ ┷┏ ┓┗ ┛┳ ⊥ ﹃ ﹄┌ ╮ ╭ ╯╰

皇冠符号

♚ ♛ ♝ ♞ ♜ ♟ ♔ ♕ ♗ ♘ ♖ ♟

表情符号

😀😁😂😃😄😅😆😉😊😋😎😍😘😗😙😚☺😇😐😑😶😏😣😥😮😯😪😫😴😌😛😜😝😒😓😔😕😲😷😖😞😟😤😢😭😦😧😨😬😰😱😳😵😡😠😈👿👹👺💀👻👽👦👧👨👩👴👵👶👱👮👲👳👷👸💂🎅👰👼💆💇🙍🙎🙅🙆💁🙋🙇🙌🙏👤👥🚶🏃👯💃👫👬👭💏💑👪💪👈👉☝👆👇✌✋👌👍👎✊👊👋👏👐✍👣👀👂👃👅👄💋👓👔👕👖👗👘👙👚👛👜👝🎒💼👞👟👠👡👢👑👒🎩🎓💄💅💍🌂🙈🙉🙊🐵🐒🐶🐕🐩🐺🐱😺😸😹😻😼😽🙀😿😾🐈🐯🐅🐆🐴🐎🐮🐂🐃🐄🐷🐖🐗🐽🐏🐑🐐🐪🐫🐘🐭🐁🐀🐹🐰🐇🐻🐨🐼🐾🐔🐓🐣🐤🐥🐦🐧🐸🐊🐢🐍🐲🐉🐳🐋🐬🐟🐠🐡🐙🐚🐌🐛🐜🐝🐞🦋💐🌸💮🌹🌺🌻🌼🌷🌱🌲🌳🌴🌵🌾🌿🍀🍁🍂🍃🌍🌎🌏🌐🌑🌒🌓🌔🌕🌖🌗🌘🌙🌚🌛🌜☀🌝🌞⭐🌟🌠☁⛅☔⚡❄🔥💧🌊💩🍇🍈🍉🍊🍋🍌🍍🍎🍏🍐🍑🍒🍓🍅🍆🌽🍄🌰🍞🍖🍗🍔🍟🍕🍳🍲🍱🍘🍙🍚🍛🍜🍝🍠🍢🍣🍤🍥🍡🍦🍧🍨🍩🍪🎂🍰🍫🍬🍭🍮🍯🍼☕🍵🍶🍷🍸🍹🍺🍻🍴🎪🎭🎨🎰🚣🛀🎫🏆⚽⚾🏀🏈🏉🎾🎱🎳⛳🎣🎽🎿🏂🏄🏇🏊🚴🚵🎯🎮🎲🎷🎸🎺🎻🎬👾🌋🗻🏠🏡🏢🏣🏤🏥🏦🏨🏩🏪🏫🏬🏭🏯🏰💒🗼🗽⛪⛲🌁🌃🌆🌇🌉🌌🎠🎡🎢🚂🚃🚄🚅🚆🚇🚈🚉🚊🚝🚞🚋🚌🚍🚎🚏🚐🚑🚒🚓🚔🚕🚖🚗🚘🚚🚛🚜🚲⛽🚨🚥🚦🚧⚓⛵🚤🚢✈💺🚁🚟🚠🚡🚀🎑🗿🛂🛃🛄🛅💌💎🔪💈🚪🚽🚿🛁⌛⏳⌚⏰🎈🎉🎊🎎🎏🎐🎀🎁📯📻📱📲☎📞📟📠🔋🔌💻💽💾💿📀🎥📺📷📹📼🔍🔎🔬🔭📡💡🔦🏮📔📕📖📗📘📙📚📓📃📜📄📰📑🔖💰💴💵💶💷💸💳✉📧📨📩📤📥📦📫📪📬📭📮✏✒📝📁📂📅📆📇📈📉📊📋📌📍📎📏📐✂🔒🔓🔏🔐🔑🔨🔫🔧🔩🔗💉💊🚬🔮🚩🎌💦💨💣☠♠♥♦♣🀄🎴🔇🔈🔉🔊📢📣💤💢💬💭♨🌀🔔🔕✡✝🔯📛🔰🔱⭕✅☑✔✖❌❎➕➖➗➰➿〽✳✴❇‼⁉❓❔❕❗©®™🎦🔅🔆💯🔠🔡🔢🔣🔤🅰🆎🅱🆑🆒🆓ℹ🆔Ⓜ🆕🆖🅾🆗🅿🆘🆙🆚🈁🈂🈷🈶🈯🉐🈹🈚🈲🉑🈸🈴🈳㊗㊙🈺🈵▪▫◻◼◽◾⬛⬜🔶🔷🔸🔹🔺🔻💠🔲🔳⚪⚫🔴🔵♈♉♊♋♌♍♎♏♐♑♒♓⛎💘❤💓💔💕💖💗💙💚💛💜💝💞💟❣🌿🚧💒☎📟💽⬆↗➡↘⬇↙⬅↖↕↔↩↪⤴⤵🔃🔄🔙🔚🔛🔜🔝🔀🔁🔂▶⏩◀⏪🔼⏫🔽⏬📱📶📳📴♻🏧🚮🚰♿🚹🚺🚻🚼🚾⚠🚸⛔🚫🚳🚭🚯🚱🚷🔞

汉语字典为您提供特殊符号大全,网名符号,特殊符号☎☏✄☪☣☢☠♨« »큐〓㊚㊛囍㊒㊖☑✔☐☒✘㍿☯☰☷♥♠♤❤♂♀★☆☯✡※卍卐■□◆◇▲△▂▃▄▅▆▇█●○◎⊕⊙㊣↑↓←→↖↗↘↙㎡№§※≡✿ⓛⓞⓥⓔ∞∑√øπ×÷±∫∵∴⊥∥∠€¥℃™©®①❶㊀㈠⑴⒈Ⓐⓐ⒜

转载于: https://shijianchuo.net/tesufuhao

]]>
其它 特殊符号
统一异常处理 /posts/31385.html 我们想让异常结果也显示为统一的返回结果对象,并且统一处理系统的异常信息,那么需要统一异常处理。

image-20230506153955544

1.全局异常处理

异常返回的结果也为统一的返回结果的对象

package com.atguigu.common.exception;

import com.atguigu.common.result.Result;
import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.ResponseBody;

/**
* @author GongChangjiang
* @version 1.0
* @Date 2023/5/6
* @Description 全局的异常处理器
*/
@ControllerAdvice
public class GlobalExceptionHandler {

/**
* 全局的异常处理
*/
@ExceptionHandler(Exception.class) //标记什么异常使用该方法
@ResponseBody //返回的是json数据
public Result error(Exception e){
e.printStackTrace();
return Result.fail().message("全局异常处理器捕获到该异常");
}


/**
* 特定的异常处理(先找特定的异常 再找全局的异常)
*/
@ExceptionHandler(ArithmeticException.class) //标记什么异常使用该方法
@ResponseBody //返回的是json数据
public Result error(ArithmeticException e){
e.printStackTrace();
return Result.fail().message("数学运算异常");
}

/**
* 自定义异常处理
* GuiguException为自定义的异常
*/
@ExceptionHandler(GuiguException.class) //标记什么异常使用该方法
@ResponseBody //返回的是json数据
public Result error(GuiguException e){
e.printStackTrace();
//获取抛出异常的时候填入的code和message的值并返回
return Result.fail().code(e.getCode()).message(e.getMessage());
}
}

2.自定义异常

创建异常类 继承RuntimeException

package com.atguigu.common.exception;

import com.atguigu.common.result.ResultCodeEnum;
import lombok.Data;

/**
* @author GongChangjiang
* @version 1.0
* @Date 2023/5/6
* @Description 自定义的异常
*/
@Data
public class GuiguException extends RuntimeException {
/**
* 异常的状态码
*/
private Integer code;

/**
* 描述信息
*/
private String message;

/**
* 通过状态码和错误消息创建异常对象
*/
public GuiguException(Integer code, String message) {
super(message);
this.code = code;
this.message = message;
}

/**
* 接收枚举类型对象
*
* @param resultCodeEnum 枚举类型的结果信息
*/
public GuiguException(ResultCodeEnum resultCodeEnum) {
super(resultCodeEnum.getMessage());
this.code = resultCodeEnum.getCode();
this.message = resultCodeEnum.getMessage();
}

@Override
public String toString() {
return "GuliException{" +
"code=" + code +
", message=" + this.getMessage() +
'}';
}


}

在需要抛出自定义异常的地方抛出异常

/**
* 查询所有的方法
*/
@ApiOperation("查询所有角色的接口")
@GetMapping("/findAll")
public Result<List<SysRole>> findAll() {
List<SysRole> sysRoles = sysRoleService.list();
try {
//这里我们使用除零模拟异常
int i = 1/0;
} catch (Exception e) {
throw new GuiguException(2001, "抛出了自定义的异常");
}
return Result.ok(sysRoles);
}

全局异常处理器捕获异常并返回异常信息

image-20230506154605339

]]>
后端 Java
数据结构与算法 /posts/40445.html 代码仓库的地址:https://gitee.com/JasonsGong/DataStructures

一.经典算法问题

字符串匹配 KMP算法

汉诺塔问题 分治算法

八皇后问题 回溯算法

马踏棋盘问题 图的深度优化遍历算法(DFS)和 贪心算法优化

二.数据结构与算法的概述

2.1 数据结构与算法的关系

(1)数据(data)结构(structure)是一门研究组织数据方式的学科,有了编程语言也就有了数据结构,学好了数据结构可以编写出更加漂亮,更加有效率的代码。

(2)程序=数据结构+算法

(3)数据结构是算法的基础

2.2解决实际的问题

五子棋程序 稀疏数组(压缩存档) 二维数组->转化成稀疏数组->存档 读档反之

约瑟夫问题(丢手帕问题) 单向环形列表

修路问题 求最小生成树 + 普利姆算法

最短路径问题 图+弗洛伊德算法

2.3 数据结构

线性结构:

​ 线性结构作为最常用的数据结构,其特点是数据元素之间存在一对一的线性关系

​ 线性结构有两种不同的存储结构,即顺序存储结构和链式存储结构。顺序存储的线性表称为顺序表,顺序表中的存储元素是连续的

​ 链式存储的线性表称为链表,链表中的存储元素不一定是连续的,元素节点中存放数据元素以及相邻元素的地址信息

​ 线性结构常见的有:数组、队列、链表和栈

非线性结构:

​ 二维数组,多维数组,广义表,树结构,图结构

三.稀疏数组和队列

3.1稀疏数组

3.1.1 基本的介绍

​ 当一个数组中大部分元素为0,或者为同一个值的数组时,可以使用稀疏数组来保存该数组。

稀疏数组的处理方法是:

​ 1)记录数组一共有几行几列,有多少个不同的值

​ 2)把具有不同值的元素的行列及值记录在一个小规模的数组中,从而缩小程序的规模

3.1.2使用的场景

​ 五子棋程序的存档和退出功能的实现

3.1.3图解

第一行记录的是原始的数组有几行几列有几个非零的值 例如下面的数组是一个6行7列有8个非零值的数组

image-20230313124552408

3.1.5实现的思路

image-20230318100138197

3.1.5代码实现

package com.atguigu.sparsearray;

/**
* @author GongChangjiang
* @version 1.0
* @Date 2023/3/18
* @Description 稀疏数组的代码实现 稀疏数组和二维数组之间的互转
*/
public class SparseArray {

public static void main(String[] args) {
//创建一个原始的二维数组
//0:表示没有棋子 1:表示黑子 2:表示白子
int[][] chessArr1 = new int[11][11];
chessArr1[1][2] = 1;
chessArr1[2][3] = 2;
chessArr1[4][5] = 1;
//遍历输出
System.out.println("原始的二维数组");
for (int[] row : chessArr1) {
for (int num : row) {
System.out.print(num + " ");
}
System.out.println();
}


//将二维数组转化成稀疏数组
//获取原始二维数组中非零元素的个数
int sum = 0;
for (int[] row : chessArr1) {
for (int num : row) {
if (num != 0) {
sum++;
}
}
}
System.out.println("非零元素的个数:" + sum);
//根据非零的元素的个数创建对应的稀疏数组
int[][] sparseArr2 = new int[sum + 1][3];
//给稀疏数组赋值
//第一行的赋值操作
sparseArr2[0][0] = 11;
sparseArr2[0][1] = 11;
sparseArr2[0][2] = sum;
//其余行的赋值操作
int row = 1; //充当计数器的作用
for (int i = 0; i < chessArr1.length; i++) {
for (int j = 0; j < chessArr1[i].length; j++) {
if (chessArr1[i][j] != 0) {
sparseArr2[row][0] = i;
sparseArr2[row][1] = j;
sparseArr2[row][2] = chessArr1[i][j];
row++;
}
}
}


//遍历稀疏数组
System.out.println("得到的稀疏数组如下");
for (int[] ints : sparseArr2) {
for (int anInt : ints) {
System.out.print(anInt + " ");
}
System.out.println();
}


//将稀疏数组转化成二维数组
//获取原先的二维数组的大小
//读取第一行 获取原始二维数组的行列值
int[][] sparseArr3 = new int[sparseArr2[0][0]][sparseArr2[0][1]];
//遍历稀疏数组第二行之后的值 赋值给原始的数组 从第二行开始算
for (int i = 1; i < sparseArr2.length; i++) {
sparseArr3[sparseArr2[i][0]][sparseArr2[i][1]] = sparseArr2[i][2];
}

//遍历还原之后的二维数组
System.out.println("稀疏数组还原成二维数组");
for (int[] ints : sparseArr3) {
for (int anInt : ints) {
System.out.print(anInt+" ");
}
System.out.println();
}


}
}

3.2队列

3.2.1基本的介绍

先进先出

队列是一个有序列表,可以通过数组或者链表实现。

遵循先入先出的原则。即:先存入队列的数据,要先取出。后存入队列的数据要后取出。

3.2.2代码实现

使用数组模拟队列

package com.atguigu.queue;

import java.util.Scanner;

/**
* @author GongChangjiang
* @version 1.0
* @Date 2023/3/18
* @Description 使用数组模拟队列
*/
public class ArrayQueueDemo {
public static void main(String[] args) {
//测试一把
//创建一个队列
ArrayQueue queue = new ArrayQueue(3);
char key = ' '; //接收用户输入
Scanner scanner = new Scanner(System.in);//
boolean loop = true;
//输出一个菜单
while (loop) {
System.out.println("s(show): 显示队列");
System.out.println("e(exit): 退出程序");
System.out.println("a(add): 添加数据到队列");
System.out.println("g(get): 从队列取出数据");
System.out.println("h(head): 查看队列头的数据");
key = scanner.next().charAt(0);//接收一个字符
switch (key) {
case 's':
queue.showQueue();
break;
case 'a':
System.out.println("输出一个数");
int value = scanner.nextInt();
queue.addQueue(value);
break;
case 'g': //取出数据
try {
int res = queue.getQueue();
System.out.printf("取出的数据是%d\n", res);
} catch (Exception e) {
// TODO: handle exception
System.out.println(e.getMessage());
}
break;
case 'h': //查看队列头的数据
try {
int res = queue.headQueue();
System.out.printf("队列头的数据是%d\n", res);
} catch (Exception e) {
// TODO: handle exception
System.out.println(e.getMessage());
}
break;
case 'e': //退出
scanner.close();
loop = false;
break;
default:
break;
}
}

System.out.println("程序退出~~");
}

}

// 使用数组模拟队列-编写一个ArrayQueue类
class ArrayQueue {
private int maxSize; // 表示数组的最大容量
private int front; // 队列头
private int rear; // 队列尾
private int[] arr; // 该数据用于存放数据, 模拟队列

// 创建队列的构造器
public ArrayQueue(int arrMaxSize) {
maxSize = arrMaxSize;
arr = new int[maxSize];
front = -1; // 指向队列头部,分析出front是指向队列头的前一个位置.
rear = -1; // 指向队列尾,指向队列尾的数据(即就是队列最后一个数据)
}

// 判断队列是否满
public boolean isFull() {
return rear == maxSize - 1;
}

// 判断队列是否为空
public boolean isEmpty() {
return rear == front;
}

// 添加数据到队列
public void addQueue(int n) {
// 判断队列是否满
if (isFull()) {
System.out.println("队列满,不能加入数据~");
return;
}
rear++; // 让rear 后移
arr[rear] = n;
}

// 获取队列的数据, 出队列
public int getQueue() {
// 判断队列是否空
if (isEmpty()) {
// 通过抛出异常
throw new RuntimeException("队列空,不能取数据");
}
front++; // front后移
return arr[front];

}

// 显示队列的所有数据
public void showQueue() {
// 遍历
if (isEmpty()) {
System.out.println("队列空的,没有数据~~");
return;
}
for (int i = 0; i < arr.length; i++) {
System.out.printf("arr[%d]=%d\n", i, arr[i]);
}
}

// 显示队列的头数据, 注意不是取出数据
public int headQueue() {
// 判断
if (isEmpty()) {
throw new RuntimeException("队列空的,没有数据~~");
}
return arr[front + 1];
}
}

3.2.3 数组模拟环形队列

环形队列的思路分析

  • 环形队列满的条件:(rear + 1) % maxSize == front
  • 环形队列空的条件:rear == front
  • 环形队列中有效数据的个数: (rear + maxSize - front) % maxSize

image-20230428124659873

package com.atguigu.queue;

import java.util.Scanner;

/**
* @author GongChangjiang
* @version 1.0
* @Date 2023/4/28
* @Description 模拟环形队列
* <p>
* 环形队列满的条件:(rear + 1) % maxSize == front
* 环形队列空的条件:rear == front
* 环形队列中有效数据的个数: (rear + maxSize - front) % maxSize
*/

public class CircleArrayQueueDemo {
public static void main(String[] args) {
//创建一个队列
System.out.println("测试环形队列");
CircleArray queue = new CircleArray(3);
char key = ' '; //接收用户输入
Scanner scanner = new Scanner(System.in);//
boolean loop = true;
//输出一个菜单
while (loop) {
System.out.println("s(show): 显示队列");
System.out.println("e(exit): 退出程序");
System.out.println("a(add): 添加数据到队列");
System.out.println("g(get): 从队列取出数据");
System.out.println("h(head): 查看队列头的数据");
key = scanner.next().charAt(0);//接收一个字符
switch (key) {
case 's':
queue.showQueue();
break;
case 'a'://添加数据
System.out.println("输出一个数");
int value = scanner.nextInt();
queue.addQueue(value);
break;
case 'g': //取出数据
try {
int res = queue.getQueue();
System.out.printf("取出的数据是%d\n", res);
} catch (Exception e) {
// TODO: handle exception
System.out.println(e.getMessage());
}
break;
case 'h': //查看队列头的数据
try {
int res = queue.headQueue();
System.out.printf("队列头的数据是%d\n", res);
} catch (Exception e) {
// TODO: handle exception
System.out.println(e.getMessage());
}
break;
case 'e': //退出
scanner.close();
loop = false;
break;
default:
break;
}
}

System.out.println("程序退出~~");
}
}

class CircleArray {
private int maxSize; // 表示数组的最大容量 因为预留了一个空间 实际上maxSize=3 只能存储两个元素
private int front; // 队列头 初始值是0
//rear 变量的含义做一个调整:rear 指向队列的最后一个元素的后一个位置. 因为希望空出一个空间做为约定
private int rear; // 队列尾
private int[] arr; // 该数据用于存放数据, 模拟队列

/**
* 构造方法
*
* @param arrMaxSize 环形队列的最大容量
*/
public CircleArray(int arrMaxSize) {
maxSize = arrMaxSize;
arr = new int[maxSize];
}

/**
* 判断队列是否为满
*/
public boolean isFull() {
//因为是环形队列 如果尾部队列的后一个指向队列的头部 那么我们认为队列是满的
return (rear + 1) % maxSize == front; //这里是一个理解点
}

/**
* 判断队列是否为空
*/
public boolean isEmpty() {
return rear == front;
}


/**
* 添加数据
*/
public void addQueue(int n) {
//先判断队列是否为满 满了就不添加
if (this.isFull()) {
System.out.println("队列为满,不能添加数据!");
return;
}
//添加数据
arr[rear] = n;
//将人rear向后移
rear = (rear + 1) % maxSize; //这里是一个理解点
}


/**
* 取出数据
*/
public int getQueue() {
//先判断队列是否为空 空了就无法取出数据
if (this.isEmpty()) {
System.out.println("队列为空,无法取出数据!");
throw new RuntimeException("队列空,不能取数据");
}
//不为空 取出数据
int result = arr[front];
front = (front + 1) % maxSize; //这里是一个理解点
return result;
}


/**
* 显示对垒数据的方法
*/
public void showQueue() {
//判断队列是否为空 如果队列为空就不遍历
if (this.isEmpty()) {
System.out.println("队列为空!");
return;
}
//遍历的方式 从front开始遍历 遍历多少个元素
int count = this.size();
for (int i = front; i < front + count; i++) {//主要作用是遍历多少次
System.out.println("arr[" + i % maxSize + "]=" + arr[i % maxSize]);
}
}

/**
* 求出当前数列中有效数据的个数
*/
public int size() {
return (rear + maxSize - front) % maxSize; //这是一个理解点
}

/**
* 显示队列的头元素
*/
public int headQueue() {
// 判断
if (isEmpty()) {
throw new RuntimeException("队列空的,没有数据~~");
}
return arr[front];
}

}

四.链表

4.1 链表(Linked List)介绍

在内存中不是连续存储的

链表的逻辑结构

image-20230428135032883

4.2单链表的应用实例(CRUD)

使用带head头的单向链表实现 –水浒英雄排行榜管理 完成对英雄人物的增删改查操作

4.2.1向单链表中添加数据

package com.atguigu.linkedlist;

/**
* @author GongChangjiang
* @version 1.0
* @Date 2023/4/28
* @Description 单链表的实现
* 使用带head头的单向链表实现 –水浒英雄排行榜管理
* 完成对英雄人物的增删改查操作
*/
public class SingleLinkedListDemo {
public static void main(String[] args) {
SingleLinkedList singleLinkedList = new SingleLinkedList();
singleLinkedList.add(new HeroNode(1, "宋江", "及时雨"));
singleLinkedList.add(new HeroNode(2, "卢俊义", "玉麒麟"));
singleLinkedList.add(new HeroNode(3, "吴用", "智多星"));
singleLinkedList.add(new HeroNode(4, "林冲", "豹子头"));
//显示链表
singleLinkedList.list();
}
}

//定义SingleLinkedList来管理我们的英雄任务
class SingleLinkedList {
//初始化一个头节点 不存储具体的数据
private HeroNode head = new HeroNode(0, "", "");

/**
* 添加节点到单向链表
*/
public void add(HeroNode heroNode) {
//找到当前链表的最后一个节点
HeroNode temp = head;
while (temp.next != null) {
temp = temp.next;
}
temp.next = heroNode;
}

/**
* 显示链表
*/
public void list() {
//先判断链表是否为空
if (head.next == null) {
System.out.println("链表为空!");
return;
}
HeroNode temp = head;
while (temp.next != null) {
System.out.println(temp.next);
temp = temp.next;
}
}

}

//定义一个heroNode,每个heroNode对象都是一个节点
class HeroNode {
public int no;
public String name;
public String nickname;
public HeroNode next; //指向下一个节点

//构造器
public HeroNode(int no, String name, String nickname) {
this.no = no;
this.name = name;
this.nickname = nickname;
}

@Override
public String toString() {
return "HeroNode{" +
"no=" + no +
", name='" + name + '\'' +
", nickname='" + nickname + '\'' +
'}';
}
}

4.2.2修改单链表中节点的方法

/**
* 修改节点的信息
* 根据新的节点的编号进行修改
*/
public void update(HeroNode newHeroNode) {
//判断链表是否为空
if (head.next == null) {
System.out.println("链表为空,无法修改该节点!");
return;
}
//找到需要修改的节点
//首先找到头节点的位置
HeroNode temp = head.next;
boolean idFind = false; //是否找到节点的信息
while (true) {
if (temp == null) {
break;//已经到了链表的结尾,就退出循环
}
if (temp.no == newHeroNode.no) {//找到修改的节点
temp.name = newHeroNode.name;
temp.nickname = newHeroNode.nickname;
idFind = true; //找到了节点并且修改了节点的信息
break;
}
temp = temp.next;
}
//做出判断,返回结果
System.out.println(idFind ? "修改成功" : "没有找到该节点的信息");
}

4.2.3删除单链表中节点的方法

/**
* 删除链表中的节点
* 根据节点的编号删除节点的信息
*/
public void delete(int no) {
//判断链表是否为空
if (head.next == null) {
System.out.println("链表为空,无法删除!");
return;
}
//找到要删除的节点的信息
HeroNode temp = head;
boolean isDelete = false;//是否删除
while (true) {
if (temp.next == null) {
break;
}
//这里的temp指的是要删除节点的上一个节点 temp.next.next指的是要删除节点的下一个节点
if (temp.next.no == no) {
//将要删除的节点的上一个节点指向要删除节点的下一个节点 实现删除的操作
//这样要删除的节点没有引用指向,会被垃圾回收机制回收
temp.next = temp.next.next;
isDelete = true;
break;
}
temp = temp.next;
}
//做出判断,返回结果
System.out.println(isDelete ? "删除成功" : "没有找到该节点的信息");
}

4.2.4完整代码

package com.atguigu.linkedlist;

/**
* @author GongChangjiang
* @version 1.0
* @Date 2023/4/28
* @Description 单链表的实现
* 使用带head头的单向链表实现 –水浒英雄排行榜管理
* 完成对英雄人物的增删改查操作
*/
public class SingleLinkedListDemo {
public static void main(String[] args) {
SingleLinkedList singleLinkedList = new SingleLinkedList();
singleLinkedList.add(new HeroNode(1, "宋江", "及时雨"));
singleLinkedList.add(new HeroNode(2, "卢俊义", "玉麒麟"));
singleLinkedList.add(new HeroNode(3, "吴用", "智多星"));
singleLinkedList.add(new HeroNode(4, "林冲", "豹子头"));
//显示链表
singleLinkedList.list();
//测试修改节点的功能
singleLinkedList.update(new HeroNode(2, "卢俊义", "玉麒麟增强版"));
//显示链表的信息
singleLinkedList.list();
//测试删除节点
singleLinkedList.delete(2);
//显示链表的信息
singleLinkedList.list();
}
}

//定义SingleLinkedList来管理我们的英雄任务
class SingleLinkedList {
//初始化一个头节点 不存储具体的数据
private HeroNode head = new HeroNode(0, "", "");


/**
* 删除链表中的节点
* 根据节点的编号删除节点的信息
*/
public void delete(int no) {
//判断链表是否为空
if (head.next == null) {
System.out.println("链表为空,无法删除!");
return;
}
//找到要删除的节点的信息
HeroNode temp = head;
boolean isDelete = false;//是否删除
while (true) {
if (temp.next == null) {
break;
}
if (temp.next.no == no) {//这里的temp指的是要删除节点的上一个节点 temp.next.next指的是要删除节点的下一个节点
//将当前节点的上一个节点指向当前节点的下一个节点
temp.next = temp.next.next; //将要删除的节点的上一个节点指向要删除节点的下一个节点 这样要删除的节点没有引用指向 会被垃圾回收机制回收
isDelete = true;
break;
}
temp = temp.next;
}
//做出判断,返回结果
System.out.println(isDelete ? "删除成功" : "没有找到该节点的信息");
}


/**
* 修改节点的信息
* 根据新的节点的编号进行修改
*/
public void update(HeroNode newHeroNode) {
//判断链表是否为空
if (head.next == null) {
System.out.println("链表为空,无法修改该节点!");
return;
}
//找到需要修改的节点
//首先找到头节点的位置
HeroNode temp = head.next;
boolean idFind = false; //是否找到节点的信息
while (true) {
if (temp == null) {
break;//已经到了链表的结尾,就退出循环
}
if (temp.no == newHeroNode.no) {//找到修改的节点
temp.name = newHeroNode.name;
temp.nickname = newHeroNode.nickname;
idFind = true; //找到了节点并且修改了节点的信息
break;
}
temp = temp.next;
}
//做出判断,返回结果
System.out.println(idFind ? "修改成功" : "没有找到该节点的信息");
}


/**
* 添加节点到单向链表
*/
public void add(HeroNode heroNode) {
//找到当前链表的最后一个节点
HeroNode temp = head;
while (temp.next != null) {
temp = temp.next;
}
temp.next = heroNode;
}


/**
* 显示链表
*/
public void list() {
//先判断链表是否为空
if (head.next == null) {
System.out.println("链表为空!");
return;
}
HeroNode temp = head;
while (temp.next != null) {
System.out.println(temp.next);
temp = temp.next;
}
}

}

//定义一个heroNode,每个heroNode对象都是一个节点
class HeroNode {
public int no;
public String name;
public String nickname;
public HeroNode next; //指向下一个节点

//构造器
public HeroNode(int no, String name, String nickname) {
this.no = no;
this.name = name;
this.nickname = nickname;
}

@Override
public String toString() {
return "HeroNode{" +
"no=" + no +
", name='" + name + '\'' +
", nickname='" + nickname + '\'' +
'}';
}
}

4.2.5 单链表的面试题(新浪,百度,腾讯)

1.求单链表中有效节点的个数
/**
* 获取有效节点的个数(如果是带头节点的链表,需求不统计头节点)
* 注意:这里是有头节点的情况下统计节点的个数 (这里我们去除了头节点)
* @param heroNode 头节点
* @return 有效节点的个数
*/
public int getLength(HeroNode heroNode){
//先判断节点是否为空
if(heroNode.next == null){
return 0;
}
//遍历节点进行统计
int length = 0;//有效节点的个数
HeroNode temp = heroNode.next;//这里没有统计头节点
while (temp != null){//遍历 统计个数
length++;
temp = temp.next;
}
return length;
}
2.查找单链表中的倒数第K个节点(新浪面试题)
/**
* 查找单链表中的倒数第K个节点
*
* @param index 倒数第几
* @param heroNode 头节点
* @return 倒数第K个节点的信息
*/
public HeroNode getHeroNodeByLastIndex(int index, HeroNode heroNode) {
//先判断节点是否为空
if (heroNode.next == null) {
return null;
}
//首先获取有效节点的个数
int length = this.getLength(heroNode);//使用求节点有效个数的方法
//求倒数第k个节点就是在求正数第(length - index + 1)节点
index = length - index; //求出正向的索引位置
if (index < 0 || index > length) {
System.out.println("所求的在链表中不存在");
return null;
}
int count = 0;
HeroNode temp = heroNode.next;
while (true) {
if (count == index) {
return temp;
}
count++;
temp = temp.next;
}
}
3.单链表的反转(腾讯面试题)

image-20230506105453269

/**
* 单链表的反转
* @param heroNode 需要反转单链表的头节点
*/
public void reverseHeroNode(HeroNode heroNode) {
//先判断当前的链表是否为空 或者当前的链表只有一个节点
if(head.next == null || head.next.next == null){
System.out.println("当前链表为空或者只有一个节点,无需反转");
return;
}
//定义一个辅助的变量 帮助我们遍历原来的链表
HeroNode cur = head.next;
HeroNode next = null;//指向当前节点的下一个节点 我们要在挪动当前节点之前帮当前指针 下移
HeroNode reverseHeroNode = new HeroNode(0, "", "");//新链表的头节点
//遍历原先的列表 确定每一个链表的指向关系
while (cur != null){
next = cur.next;//保存当前节点的下一个节点
//reverseHeroNode.next表示头节点的下一个节点 我们让cur的下一个节点(cur.next)指向头节点的下一个节点 //(reverseHeroNode.next) 就把当前的节点(cur)穿插进去了
//下一句将头节点和当前节点建立关系reverseHeroNode.next = cur 整个节点就连接起来了
cur.next = reverseHeroNode.next;//将cur的下一个节点指向新的链表的最前端 相当于在头节点和头节点的下一个节点 //之间穿插了一个节点
reverseHeroNode.next = cur;//将cur连接到新的链表上
cur = next; //cur像后移动
}
//将head.next指向reverseHeroNode.next 实现单链表的反转
head.next = reverseHeroNode.next;
}
4.从尾到头打印单链表(百度面试题)

不改变链表本身的结构(不是通过链表的反转之后再打印的)

/**
* (通过栈的方式实现单链表的逆序打印)
* 栈的特点是先入后出 正好可以满足逆序打印
* 单链表的逆序打印
*/
public void reversePrint(HeroNode heroNode) {
//先判断当前的链表是否为空 或者当前的链表只有一个节点
if (head.next == null) {
System.out.println("当前链表为空,无法逆序打印!");
return;
}
//创建一个栈,将各个节点压入栈中
//先创建一个栈
Stack<HeroNode> stack = new Stack<>();
HeroNode cur = heroNode.next;
//循环将每一个节点添加进栈中
while (cur != null) {
stack.push(cur);//将节点压入栈中
cur = cur.next;//cur后移 压入下一个节点
}
//将栈中节点打印
while (stack.size() > 0) {
System.out.println(stack.pop());
}
}
5.合并两个有序的单链表,合并之后的链表依然有序

以下代码有一些bug 后期修改

/**
* 合并两个有序的单链表,合并之后的链表依然有序
*/
public static void mergeHeroNode(HeroNode oneHead, HeroNode twoHead ,HeroNode head) {
//先判断两个链表是否为空
if (oneHead.next == null || twoHead.next == null) {
System.out.println("其中有一个(两个)链表为空,无法合并");
return;
}
//合并
HeroNode oneCur = oneHead.next;
HeroNode twoCur = twoHead.next;
HeroNode oneNext = null;
HeroNode twoNext = null;
HeroNode finalHeroHead = new HeroNode(0, "", "");
while (oneCur != null && twoCur != null) {
oneNext = oneCur.next;
twoNext = twoCur.next;
if(oneCur.no <= twoCur.no) {
oneCur.next = finalHeroHead.next;
finalHeroHead.next = oneCur;
oneCur = oneNext;
}else {
twoCur.next = finalHeroHead.next;
finalHeroHead.next = twoCur;
twoCur = twoNext;
}
}
head.next = finalHeroHead.next;
}
全部代码
package com.atguigu.linkedlist;

import java.util.Stack;

/**
* @author GongChangjiang
* @version 1.0
* @Date 2023/4/28
* @Description 单链表的实现
* 使用带head头的单向链表实现 –水浒英雄排行榜管理
* 完成对英雄人物的增删改查操作
*/
public class SingleLinkedListDemo {
public static void main(String[] args) {
SingleLinkedList singleLinkedList = new SingleLinkedList();
// singleLinkedList.add(new HeroNode(1, "宋江", "及时雨"));
// singleLinkedList.add(new HeroNode(2, "卢俊义", "玉麒麟"));
// singleLinkedList.add(new HeroNode(3, "吴用", "智多星"));
// singleLinkedList.add(new HeroNode(4, "林冲", "豹子头"));
// //显示链表
// singleLinkedList.list();
// //测试修改节点的功能
// singleLinkedList.update(new HeroNode(2, "卢俊义", "玉麒麟增强版"));
// //显示链表的信息
// singleLinkedList.list();
// //测试删除节点
// singleLinkedList.delete(2);
// //显示链表的信息
// singleLinkedList.list();
// //统计有效节点的个数
// System.out.println("有效节点的个数" + singleLinkedList.getLength(singleLinkedList.getHead()));
// //查找单链表中的倒数第K个节点
// int index = 2;
// System.out.println("倒数第" + index + "个节点的位置:" + singleLinkedList.getHeroNodeByLastIndex(index, singleLinkedList.getHead()));
// //测试单链表的反转
// singleLinkedList.reverseHeroNode(singleLinkedList.getHead());
// singleLinkedList.list();
// //测试通过栈的方式逆序打印
// System.out.println("测试通过栈的方式逆序打印头节点");
// singleLinkedList.reversePrint(singleLinkedList.getHead());

//测试合并两个有序的链表
SingleLinkedList singleLinkedList1 = new SingleLinkedList();
singleLinkedList1.add(new HeroNode(1, "宋江", "及时雨"));
singleLinkedList1.add(new HeroNode(3, "卢俊义", "玉麒麟"));
singleLinkedList1.add(new HeroNode(5, "吴用", "智多星"));
singleLinkedList1.add(new HeroNode(7, "林冲", "豹子头"));
SingleLinkedList singleLinkedList2 = new SingleLinkedList();
singleLinkedList2.add(new HeroNode(2, "宋江", "及时雨"));
singleLinkedList2.add(new HeroNode(4, "卢俊义", "玉麒麟"));
singleLinkedList2.add(new HeroNode(6, "吴用", "智多星"));
singleLinkedList2.add(new HeroNode(8, "林冲", "豹子头"));
mergeHeroNode(singleLinkedList1.getHead(),singleLinkedList2.getHead(),singleLinkedList.getHead());
singleLinkedList.list();
}
/**
* 合并两个有序的单链表,合并之后的链表依然有序
*/
public static void mergeHeroNode(HeroNode oneHead, HeroNode twoHead ,HeroNode head) {
//先判断两个链表是否为空
if (oneHead.next == null || twoHead.next == null) {
System.out.println("其中有一个(两个)链表为空,无法合并");
return;
}
//合并
HeroNode oneCur = oneHead.next;
HeroNode twoCur = twoHead.next;
HeroNode oneNext = null;
HeroNode twoNext = null;
HeroNode finalHeroHead = new HeroNode(0, "", "");
while (oneCur != null && twoCur != null) {
oneNext = oneCur.next;
twoNext = twoCur.next;
if(oneCur.no <= twoCur.no) {
oneCur.next = finalHeroHead.next;
finalHeroHead.next = oneCur;
oneCur = oneNext;
}else {
twoCur.next = finalHeroHead.next;
finalHeroHead.next = twoCur;
twoCur = twoNext;
}
}
head.next = finalHeroHead.next;
}
}

//定义SingleLinkedList来管理我们的英雄任务
class SingleLinkedList {
//初始化一个头节点 不存储具体的数据
private HeroNode head = new HeroNode(0, "", "");

public HeroNode getHead() {
return head;
}





/**
* (通过栈的方式实现单链表的逆序打印)
* 栈的特点是先入后出 正好可以满足逆序打印
* 单链表的逆序打印
*/
public void reversePrint(HeroNode heroNode) {
//先判断当前的链表是否为空 或者当前的链表只有一个节点
if (heroNode.next == null) {
System.out.println("当前链表为空,无法逆序打印!");
return;
}
//创建一个栈,将各个节点压入栈中
//先创建一个栈
Stack<HeroNode> stack = new Stack<>();
HeroNode cur = heroNode.next;
//循环将每一个节点添加进栈中
while (cur != null) {
stack.push(cur);//将节点压入栈中
cur = cur.next;//cur后移 压入下一个节点
}
//将栈中节点打印
while (stack.size() > 0) {
System.out.println(stack.pop());
}
}


/**
* 单链表的反转
*
* @param heroNode 需要反转单链表的头节点
*/
public void reverseHeroNode(HeroNode heroNode) {
//先判断当前的链表是否为空 或者当前的链表只有一个节点
if (heroNode.next == null || heroNode.next.next == null) {
System.out.println("当前链表为空或者只有一个节点,无需反转");
return;
}
//定义一个辅助的变量 帮助我们遍历原来的链表
HeroNode cur = heroNode.next;
HeroNode next = null;//指向当前节点的下一个节点 我们要在挪动当前节点之前帮当前指针 下移
HeroNode reverseHeroNode = new HeroNode(0, "", "");//新链表的头节点
//遍历原先的列表 确定每一个链表的指向关系
while (cur != null) {
next = cur.next;//保存当前节点的下一个节点
//reverseHeroNode.next表示头节点的下一个节点 我们让cur的下一个节点(cur.next)指向头节点的下一个节点(reverseHeroNode.next) 就把当前的节点(cur)穿插进去了
//下一句将头节点和当前节点建立关系reverseHeroNode.next = cur 整个节点就连接起来了
cur.next = reverseHeroNode.next;//将cur的下一个节点指向新的链表的最前端 相当于在头节点和头节点的下一个节点之间穿插了一个节点
reverseHeroNode.next = cur;//将cur连接到新的链表上
cur = next; //cur像后移动
}
//将head.next指向reverseHeroNode.next 实现单链表的反转
heroNode.next = reverseHeroNode.next;
}


/**
* 查找单链表中的倒数第K个节点
*
* @param index 倒数第几
* @param heroNode 头节点
* @return 倒数第K个节点的信息
*/
public HeroNode getHeroNodeByLastIndex(int index, HeroNode heroNode) {
//先判断节点是否为空
if (heroNode.next == null) {
return null;
}
//首先获取有效节点的个数
int length = this.getLength(heroNode);//使用求节点有效个数的方法
//求倒数第k个节点就是在求正数第(length - index + 1)节点
index = length - index; //求出正向的索引位置
if (index < 0 || index > length) {
System.out.println("所求的在链表中不存在");
return null;
}
int count = 0;
HeroNode temp = heroNode.next;
while (true) {
if (count == index) {
return temp;
}
count++;
temp = temp.next;
}
}

/**
* 获取有效节点的个数(如果是带头节点的链表,需求不统计头节点)
*
* @param heroNode 头节点
* @return 有效节点的个数
*/
public int getLength(HeroNode heroNode) {
//先判断节点是否为空
if (heroNode.next == null) {
return 0;
}
//遍历节点进行统计
int length = 0;//有效节点的个数
HeroNode temp = heroNode.next;//这里没有统计头节点
while (temp != null) {//遍历 统计个数
length++;
temp = temp.next;
}
return length;
}


/**
* 删除链表中的节点
* 根据节点的编号删除节点的信息
*/
public void delete(int no) {
//判断链表是否为空
if (head.next == null) {
System.out.println("链表为空,无法删除!");
return;
}
//找到要删除的节点的信息
HeroNode temp = head;
boolean isDelete = false;//是否删除
while (true) {
if (temp.next == null) {
break;
}
if (temp.next.no == no) {//这里的temp指的是要删除节点的上一个节点 temp.next.next指的是要删除节点的下一个节点
//将当前节点的上一个节点指向当前节点的下一个节点
temp.next = temp.next.next; //将要删除的节点的上一个节点指向要删除节点的下一个节点 这样要删除的节点没有引用指向 会被垃圾回收机制回收
isDelete = true;
break;
}
temp = temp.next;
}
//做出判断,返回结果
System.out.println(isDelete ? "删除成功" : "没有找到该节点的信息");
}


/**
* 修改节点的信息
* 根据新的节点的编号进行修改
*/
public void update(HeroNode newHeroNode) {
//判断链表是否为空
if (head.next == null) {
System.out.println("链表为空,无法修改该节点!");
return;
}
//找到需要修改的节点
//首先找到头节点的位置
HeroNode temp = head.next;
boolean idFind = false; //是否找到节点的信息
while (true) {
if (temp == null) {
break;//已经到了链表的结尾,就退出循环
}
if (temp.no == newHeroNode.no) {//找到修改的节点
temp.name = newHeroNode.name;
temp.nickname = newHeroNode.nickname;
idFind = true; //找到了节点并且修改了节点的信息
break;
}
temp = temp.next;
}
//做出判断,返回结果
System.out.println(idFind ? "修改成功" : "没有找到该节点的信息");
}


/**
* 添加节点到单向链表
*/
public void add(HeroNode heroNode) {
//找到当前链表的最后一个节点
HeroNode temp = head;
while (temp.next != null) {
temp = temp.next;
}
temp.next = heroNode;
}


/**
* 显示链表
*/
public void list() {
//先判断链表是否为空
if (head.next == null) {
System.out.println("链表为空!");
return;
}
HeroNode temp = head;
while (temp.next != null) {
System.out.println(temp.next);
temp = temp.next;
}
}


}

//定义一个heroNode,每个heroNode对象都是一个节点
class HeroNode {
public int no;
public String name;
public String nickname;
public HeroNode next; //指向下一个节点

//构造器
public HeroNode(int no, String name, String nickname) {
this.no = no;
this.name = name;
this.nickname = nickname;
}

@Override
public String toString() {
return "HeroNode{" +
"no=" + no +
", name='" + name + '\'' +
", nickname='" + nickname + '\'' +
'}';
}
}

4.3 双向链表

双向的链表的CRUD操作于单向链表的CRUD操作类似

image-20230507100730848

完整的增删改查代码

package com.atguigu.linkedlist;

/**
* @author GongChangjiang
* @version 1.0
* @Date 2023/5/7
* @Description 双向链表的使用
*/
public class DoubleLinkedListDemo {
public static void main(String[] args) {
DoubleLinkedList doubleLinkedList = new DoubleLinkedList();
//测试添加链表的功能
doubleLinkedList.add(new HeroNode2(1, "宋江", "及时雨"));
doubleLinkedList.add(new HeroNode2(2, "卢俊义", "玉麒麟"));
doubleLinkedList.add(new HeroNode2(3, "吴用", "智多星"));
doubleLinkedList.add(new HeroNode2(4, "林冲", "豹子头"));
//测试显示链表的功能
doubleLinkedList.list();
//测试删除双向链表中的节点
doubleLinkedList.delete(2);
doubleLinkedList.list();
//测试修改双向链表中的节点
doubleLinkedList.update(new HeroNode2(2, "卢俊义修改版", "玉麒麟"));
doubleLinkedList.list();
System.out.println("--------------");
doubleLinkedList.orderAdd(new HeroNode2(3, "小卢", "玉麒麟"));
doubleLinkedList.list();
}
}

class DoubleLinkedList {
//初始化一个头节点 不存储具体的数据
private HeroNode2 head = new HeroNode2(0, "", "");

//返回头节点的信息
public HeroNode2 getHead() {
return head;
}


/**
* 双向链表的顺序添加
* 应该有问题 不是参考答案
*/
public void orderAdd(HeroNode2 heroNode) {
//找到当前链表的最后一个节点
HeroNode2 temp = head;
boolean isSuccess = false;
while (temp.next != null) {
if (temp.next.no >= heroNode.no) {
heroNode.next = temp.next;
temp.next.pre = heroNode;
temp.next = heroNode;
heroNode.pre = temp;
isSuccess = true;
break;
}
temp = temp.next;
}
if (!isSuccess) {//没有添加成功 说明当前链表的序号超过了链表里面已有节点序号的最高值 直接添加到最后
this.add(heroNode);
}

}


/**
* 删除双向链表中的一个节点
*/
public void delete(int no) {
//判断链表是否为空
if (head.next == null) {
System.out.println("链表为空,无法删除!");
return;
}
//找到要删除的节点的信息
HeroNode2 temp = head.next;
boolean isDelete = false;//是否删除
while (true) {
if (temp == null) {
break;
}
if (temp.no == no) {
temp.pre.next = temp.next;
if (temp.next != null) {
temp.next.pre = temp.pre;//这句话有个条件 temp不能是最后一个节点
}
isDelete = true;
break;
}
temp = temp.next;
}
System.out.println(isDelete ? "删除成功" : "没有找到该节点的信息");
}


/**
* 修改双向链表的信息
*/
public void update(HeroNode2 newHeroNode) {
//判断链表是否为空
if (head.next == null) {
System.out.println("链表为空,无法修改该节点!");
return;
}
//找到需要修改的节点
//首先找到头节点的位置
HeroNode2 temp = head.next;
boolean idFind = false; //是否找到节点的信息
while (true) {
if (temp == null) {
break;//已经到了链表的结尾,就退出循环
}
if (temp.no == newHeroNode.no) {//找到修改的节点
temp.name = newHeroNode.name;
temp.nickname = newHeroNode.nickname;
idFind = true; //找到了节点并且修改了节点的信息
break;
}
temp = temp.next;
}
//做出判断,返回结果
System.out.println(idFind ? "修改成功" : "没有找到该节点的信息");
}


/**
* 向双向链表中添加节点信息
*/
public void add(HeroNode2 heroNode) {
//找到当前链表的最后一个节点
HeroNode2 temp = head;
while (temp.next != null) {
temp = temp.next;
}
temp.next = heroNode;
heroNode.pre = temp;
}


/**
* 显示链表
*/
public void list() {
//先判断链表是否为空
if (head.next == null) {
System.out.println("链表为空!");
return;
}
HeroNode2 temp = head;
while (temp.next != null) {
System.out.println(temp.next);
temp = temp.next;
}
}


}

//定义一个heroNode,每个heroNode对象都是一个节点
class HeroNode2 {
public int no;
public String name;
public String nickname;
public HeroNode2 next; //指向下一个节点
public HeroNode2 pre;//指向上一个节点

//构造器
public HeroNode2(int no, String name, String nickname) {
this.no = no;
this.name = name;
this.nickname = nickname;
}

@Override
public String toString() {
return "HeroNode{" +
"no=" + no +
", name='" + name + '\'' +
", nickname='" + nickname + '\'' +
'}';
}
}

4.4 单向环形链表的应用场景(Josephu问题)

​ Josephu 问题为:设编号为1,2,… n的n个人围坐一圈,约定编号为k(1<=k<=n)的人从1开始报数,数到m 的那个人出列,它的下一位又从1开始报数,数到m的那个人又出列,依次类推,直到所有人出列为止,由此产生一个出队编号的序列。

​ 解决的方案:用一个不带头结点的循环链表来处理Josephu 问题:先构成一个有n个结点的单循环链表,然后由k结点起从1开始计数,计到m时,对应结点从链表中删除,然后再从被删除结点的下一个结点又从1开始计数,直到最后一个结点从链表中删除算法结束。

image-20230508104207815

image-20230508121934697

完整的代码

package com.atguigu.linkedlist;

/**
* @author GongChangjiang
* @version 1.0
* @Date 2023/5/8
* @Description 约瑟夫问题
*/
public class Josephu {
public static void main(String[] args) {
//测试构建环形链表
CircleSingleLinkedList circleSingleLinkedList = new CircleSingleLinkedList();
circleSingleLinkedList.addBoy(5);
//测试遍历环形单向链表
circleSingleLinkedList.showBoy();
//测试生成小孩出圈的序列
circleSingleLinkedList.countBoy(1,2,5);
}

}


//创建一个环形的单向链表
class CircleSingleLinkedList {
//创建一个first节点,当前没有编号
private Boy first = null;

/**
* 根据用户的输入产生一个出队编号的序列
*
* @param startNo 表示从第几个小孩开始数
* @param countNum 表示数几下
* @param nums 传入几个小孩
*/
public void countBoy(int startNo, int countNum, int nums) {
//先对输入的数据进行校验
if (first == null || startNo < 0 || startNo > nums) {
System.out.println("参数输入有误,请重新输入!");
return;
}
//创建一个辅助指针
Boy helper = first;
//将helper指向环形链表的最后一个节点
while (true) {
if (helper.getNext() == first) {
break;
}
helper = helper.getNext();
}
//将first和helper移动到指定的位置 考虑从第几个小孩开始数
//小孩报数前,先让 first 和 helper 移动 k - 1次
for (int i = 0; i < startNo - 1; i++) {
first = first.getNext();
helper = helper.getNext();
}
//让first和helper移动 m-1次,然后出圈
//循环操作 直到圈中只有一个小孩节点
while (true){
if(helper == first){//说明圈中只有一个节点
break;
}
//让first和helper移动 countNum - 1
for (int i = 0; i < countNum - 1; i++) {
first = first.getNext();
helper = helper.getNext();
}
//这时first指向的节点就是要出圈的小孩
System.out.printf("小孩%d出圈\n",first.getNo());
//将first指向的小孩出圈
first = first.getNext();
helper.setNext(first);
}
System.out.printf("最后留在圈中小孩编号是:%d \n",helper.getNo());
}


/**
* 遍历当前的环形链表
*/
public void showBoy() {
//先判断链表是不是为空
if (first == null) {
System.out.println("环形链表为空!");
return;
}
//创建一个辅助指针
Boy curBoy = first;
while (true) {
System.out.printf("小孩的编号:%d \n", curBoy.getNo());
if (curBoy.getNext() == first) {//说明已经循环一圈了 就不在循环 退出了
break;
}
//向后移动
curBoy = curBoy.getNext();
}
}


/**
* 添加小孩节点 构建成一个环形的链表
*
* @param nums 传入几个小孩
*/
public void addBoy(int nums) {
//将传入的数据进行一个校验
if (nums < 1) {//传入小孩的的数量不能小于一
System.out.println("输入的值不正确!");
return;
}
Boy curBoy = null;//辅助指针,帮助构建环形链表
//使用循环创建我们的环形链表
for (int i = 1; i <= nums; i++) { //要加入几个小孩 就循环几次
//根据编号创建小孩节点
Boy boy = new Boy(i);
//将小孩加入到环形链表中
if (i == 1) {//说明是第一个小孩
first = boy;
first.setNext(first);//添加的第一个节点 先自己指向自己 构建成一个环
curBoy = first; //first节点我们不能动 使用辅助节点
} else {
//这是在循环里面 每次循环之后 小孩节点都不一样
curBoy.setNext(boy);
boy.setNext(first); //形成回路
curBoy = boy;//curBoy每次指向的都是最后一个小孩节点
}
}
}

}

//创建一个boy类 表示一个节点
class Boy {
private int no; //编号
private Boy next;//指向下一个节点 默认是null

public Boy(int no) {
this.no = no;
}

public int getNo() {
return no;
}

public void setNo(int no) {
this.no = no;
}

public Boy getNext() {
return next;
}

public void setNext(Boy next) {
this.next = next;
}
}

五.栈

1.介绍

1)栈的英文为(stack)

2)栈是一个先入后出(FILO-First In Last Out)的有序列表。

3)栈(stack)是限制线性表中元素的插入和删除只能在线性表的同一端进行的一种特殊线性表。允许插入和删除的一端,为变化的一端,称为栈顶(Top),另一端为固定的一端,称为栈底(Bottom)。

4)根据栈的定义可知,最先放入栈中元素在栈底,最后放入的元素在栈顶,而删除元素刚好相反,最后放入的元素最先删除,最先放入的元素最后删除

2.应用场景

1)子程序的调用:在跳往子程序前,会先将下个指令的地址存到堆栈中,直到子程序执行完后再将地址取出,以回到原来的程序中。

2)处理递归调用:和子程序的调用类似,只是除了储存下一个指令的地址外,也将参数、区域变量等数据存入堆栈中。

3)表达式的转换[中缀表达式转后缀表达式]与求值(实际解决)。

4)二叉树的遍历。

5)图形的深度优先(depth一first)搜索法。

3.图解

image-20230510101713914

image-20230510101722707

4.使用数组模拟栈

image-20230510101916230

package com.atguigu.stack;

/**
* @author GongChangjiang
* @version 1.0
* @Date 2023/5/10
* @Description 使用数组模拟栈
*/
public class ArrayStackDemo {
public static void main(String[] args) {
ArrayStack stack = new ArrayStack(5);
//入栈
stack.push(1);
stack.push(2);
stack.push(3);
//出栈
System.out.println("出栈的数是:"+stack.pop());
//显示栈中所有的数据
stack.list();
}
}

//定义一个ArrayStack表示栈
class ArrayStack {
private int maxSize;//栈的大小
private int[] stack;//数组 数组模拟栈 数据放在数组中
private int top = -1; //top表示栈顶 初始化为1

//构造器
public ArrayStack(int maxSize) {
this.maxSize = maxSize;
//完成数组的初始化
stack = new int[maxSize];
}

/**
* 判断是否栈满
*/
public boolean isFull() {
return top == maxSize - 1;
}

/**
* 判断栈是否为空
*/
public boolean isEmpty() {
return top == -1;
}


/**
* 入栈的操作
*/
public void push(int value) {
//先判断栈是否为满
if (isFull()) {
System.out.println("栈满,无法添加数据!");
return;
}
top++;
stack[top] = value;
}

/**
* 出栈的操作
* 将栈顶的数据返回
*/
public int pop() {
//先判断栈是否为空
if (isEmpty()) {
throw new RuntimeException("栈空,无法取出数据!");
}
//取出数据
int value = stack[top];
top--;
return value;
}

/**
* 显示栈的情况
*/
public void list() {
//先判断栈是否为空
if (isEmpty()) {
System.out.println("栈空,无法取出数据!");
return;
}
for (int i = top; i >= 0; i--) {
System.out.printf("stack[%d] = %d \n", i, stack[i]);
}
}

}

5.栈实现综合计算器

中缀表达式

image-20230510105251076

image-20230510110403055

package com.atguigu.stack;

/**
* @author GongChangjiang
* @version 1.0
* @Date 2023/5/10
* @Description 综合计算器
*/
public class Calculator {
public static void main(String[] args) {
/**
* 1. 通过一个 index 值(索引),来遍历我们的表达式
* 2. 如果我们发现是一个数字, 就直接入数栈
* 3. 如果发现扫描到的是一个符号, 就分如下情况
* 3.1 如果发现当前的符号栈为 空,就直接入栈
* 3.2 如果符号栈有操作符,就进行比较,如果当前的操作符的优先级小于或者等于栈中的操作符, 就需要从数栈中pop出两个数,在从符号栈中pop出一个符号,进行运算,将得到结果,入数栈,然后将当前的操作符入符号栈, 如果当前的操作符的优先级大于栈中的操作符, 就直接入符号栈.
* 4. 当表达式扫描完毕,就顺序的从 数栈和符号栈中pop出相应的数和符号,并运行.
* 5. 最后在数栈只有一个数字,就是表达式的结果
*/
String expression = "30+2*6-2";
//创建两个栈 一个是数栈 一个是符号栈
ArrayStack2 numStack = new ArrayStack2(10);
ArrayStack2 operStack = new ArrayStack2(10);
//定义需要的相关变量
int index = 0;//用于扫描
int num1 = 0;
int num2 = 0;
int oper = 0;
int result = 0;
char ch = ' '; //将每次扫描得到的char保存在ch中
String keepNum = "";//用于拼接多位数的
while (true) {
//得到expression的每一个字符
ch = expression.substring(index, index + 1).charAt(0);
//判断ch是什么 做相应的处理
if (operStack.isOper(ch)) {
//先符号栈判断是不是为空
if (!operStack.isEmpty()) { //判断符号栈是否为空
//不为空的情况
if (operStack.priority(ch) <= operStack.priority(operStack.peek())) {
//如果当前的操作符的优先级小于或者等于栈中的操作符, 就需要从数栈中pop出两个数,
// 在从符号栈中pop出一个符号,进行运算,将得到结果,入数栈
num1 = numStack.pop();
num2 = numStack.pop();
oper = operStack.pop();
result = numStack.cal(num1, num2, oper);
//将运算的结果入数栈
numStack.push(result);
//将当前的操作符入符号栈
operStack.push(ch);
} else {
//如果当前的操作符的优先级大于栈中的操作符, 就直接入符号栈.
operStack.push(ch);
}
} else {
//如果为空 直接入栈
operStack.push(ch);
}
} else {
//如果是数字 就直接入数栈
//要考虑是多位数的情况
keepNum += ch;
//如果ch是表达式的最后一位 就直接入栈
if (index == expression.length() - 1) {
numStack.push(Integer.parseInt(keepNum));
} else {
//判断下一个字符是不是数字 如果是数字 就继续扫描 如果是运算符 则入栈
if (operStack.isOper(expression.substring(index + 1, index + 2).charAt(0))) {//下一位是操作符
//直接将当前为添加到数栈
numStack.push(Integer.parseInt(keepNum));
//清空keepNum
keepNum = "";
}
}
}
//让index + 1 ,并判断是否扫描到表达式的最后
index++;
if (index >= expression.length()) {
break;
}
}
//当表达式扫描完毕,就顺序的从 数栈和符号栈中pop出相应的数和符号,并运行.
while (true) {
//如果符号栈为空 则计算结束 数栈中只有 一个数字
if (operStack.isEmpty()) {
break;
}
num1 = numStack.pop();
num2 = numStack.pop();
oper = operStack.pop();
result = numStack.cal(num1, num2, oper);
numStack.push(result);
}
System.out.printf("表达式:%s = %d", expression, numStack.pop());
}
}

//定义一个ArrayStack表示栈
class ArrayStack2 {
private int maxSize;//栈的大小
private int[] stack;//数组 数组模拟栈 数据放在数组中
private int top = -1; //top表示栈顶 初始化为1

//构造器
public ArrayStack2(int maxSize) {
this.maxSize = maxSize;
//完成数组的初始化
stack = new int[maxSize];
}

/**
* 判断是否栈满
*/
public boolean isFull() {
return top == maxSize - 1;
}

/**
* 判断栈是否为空
*/
public boolean isEmpty() {
return top == -1;
}


/**
* 入栈的操作
*/
public void push(int value) {
//先判断栈是否为满
if (isFull()) {
System.out.println("栈满,无法添加数据!");
return;
}
top++;
stack[top] = value;
}

/**
* 出栈的操作
* 将栈顶的数据返回
*/
public int pop() {
//先判断栈是否为空
if (isEmpty()) {
throw new RuntimeException("栈空,无法取出数据!");
}
//取出数据
int value = stack[top];
top--;
return value;
}

/**
* 显示栈的情况
*/
public void list() {
//先判断栈是否为空
if (isEmpty()) {
System.out.println("栈空,无法取出数据!");
return;
}
for (int i = top; i >= 0; i--) {
System.out.printf("stack[%d] = %d \n", i, stack[i]);
}
}


/**
* 返回运算符的优先级
* 优先级由数字表示
* 数字大 优先级高
*/
public int priority(int oper) {
if (oper == '*' || oper == '/') {
return 1;
} else if (oper == '+' || oper == '-') {
return 0;
} else {
return -1;
}
}


/**
* 判断是不是一个运算符
*/
public boolean isOper(char val) {
return val == '+' || val == '-' || val == '*' || val == '/';
}

/**
* 计算方法
*/
public int cal(int num1, int num2, int oper) {
int result = 0;
switch (oper) {
case '+':
result = num1 + num2;
break;
case '-':
result = num2 - num1; //注意顺序
break;
case '*':
result = num1 * num2;
break;
case '/':
result = num2 / num1;
}
return result;
}

/**
* 查看栈顶数据的方法
* 返回栈顶的值 不是pop出来
*/
public int peek() {
return stack[top];
}


}

6.逆波兰计算器的设计与实现

逆波兰表达式(后缀表达式)

1)输入一个逆波兰表达式(后缀表达式),使用栈(Stack),计算其结果

2)支持小括号和多位数整数,因为这里我们主要讲的是数据结构,因此计算器进行简化,只支持对整数的计算。

package com.atguigu.stack;

import java.util.ArrayList;
import java.util.List;
import java.util.Stack;

/**
* @author GongChangjiang
* @version 1.0
* @Date 2023/5/11
* @Description 逆波兰计算器的设计与实现
*/
public class PolandNotation {
public static void main(String[] args) {
//先定义一个逆波兰表达式
//(3+4)×5-6 的逆波兰表达式是 3 4 + 5 × 6 -
//为了方便 逆波兰表达式的数字和符号有空格隔开
String suffixExpression = "3 4 + 5 * 6 -";//中间有空格隔开
//1.先将suffixExpression放入到List集合中
//2.将List集合传递给一个方法 配合栈 完成计算
List<String> rpnList = getListString(suffixExpression);
System.out.println("计算的结果是:"+calculate(rpnList));


}

//将一个逆波兰表达式依次将数据和运算符放入到List集合中
public static List<String> getListString(String suffixExpression) {
//将suffixExpression按照空格分割
String[] strings = suffixExpression.split(" ");
List<String> list = new ArrayList<>();
for (String string : strings) {
list.add(string);
}
return list;
}

/**
* 从左至右扫描,将3和4压入堆栈;
* 遇到+运算符,因此弹出4和3(4为栈顶元素,3为次顶元素),计算出3+4的值,得7,再将7入栈;
* 将5入栈;
* 接下来是×运算符,因此弹出5和7,计算出7×5=35,将35入栈;
* 将6入栈;
* 最后是-运算符,计算出35-6的值,即29,由此得出最终结果
*/
public static int calculate(List<String> list) {
//创建一个栈
Stack<String> stack = new Stack<>();
//遍历list
for (String item : list) {
//使用正则表达式取出数字
if (item.matches("\\d+")) { //匹配的是多位数
//入栈
stack.push(item);
} else {
//弹出两个数 并运算 再入栈
int num2 = Integer.parseInt(stack.pop());
int num1 = Integer.parseInt(stack.pop());
//运算
int res = 0;
if (item.equals("+")) {
res = num1 + num2;
} else if (item.equals("-")) {
res = num1 - num2;
} else if (item.equals("*")) {
res = num1 * num2;
} else if (item.equals("/")) {
res = num1 / num2;
}else {
throw new RuntimeException("运算符有误!");
}
//把结果入栈
stack.push(res + "");
}
}
//最后留在stack中的数就是运算结果
return Integer.parseInt(stack.pop());
}
}

将中缀表达式转化为后缀表达式(逆波兰表达式)

解题的步骤:

1)初始化两个栈:运算符栈s1和储存中间结果的栈s2;

2)从左至右扫描中缀表达式;

3)遇到操作数时,将其压s2;

4)遇到运算符时,比较其与s1栈顶运算符的优先级:

​ (1)如果s1为空,或栈顶运算符为左括号“(”,则直接将此运算符入栈;

​ (2)否则,若优先级比栈顶运算符的高,也将运算符压入s1;

​ (3)否则,将s1栈顶的运算符弹出并压入到s2中,再次转到(4-1)与s1中新的栈顶运算符相比较

5)遇到括号时:
(1) 如果是左括号“(”,则直接压入s1
(2) 如果是右括号“)”,则依次弹出s1栈顶的运算符,并压入s2,直到遇到左括号为止,此时将这一对括号丢弃

6)重复步骤2至5,直到表达式的最右边

7)将s1中剩余的运算符依次弹出并压入s2

8)依次弹出s2中的元素并输出,结果的逆序即为中缀表达式对应的后缀表达式

image-20230511094011546

先将中缀表达式转化成list集合

/**
* 将中缀表达式转化成对应的list
* @param expression 中缀表达式
* @return 中缀表达式转化成的一个list集合
*/
public static List<String> toInfixExpressionList(String expression) {
//定义一个集合,存储中缀表达式对应的list集合
List<String> list = new ArrayList<>();
int i = 0;//这个是一个指针 用于遍历中缀表达式的字符串
String str;//做多位数的拼接工作 ,因为我们要考虑多位数的情况
char c;//每遍历一个字符就放入到c中
do {
if ((c = expression.charAt(i)) < 48 || (c = expression.charAt(i)) > 57) { //如果c是一个非数字的字符需要加入到ls中
list.add(c + ""); //直接将这个字符添加到list集合中
i++;
} else {//如果是一个数 要考虑多位数的问题
str = "";//先将str置空
while (i < expression.length() && (c = expression.charAt(i)) >= 48 && (c = expression.charAt(i)) <= 57) {
str += c;
i++;
}
list.add(str);
}
} while (i < expression.length());
return list;
}

将中缀表达式的集合转化成逆波兰表达式(后缀表达式)的集合

/**
* 将中缀表达式对应的List转化成后缀表达式对应的list
* tips:
* 1)初始化两个栈:运算符栈s1和储存中间结果的栈s2;
* 2)从左至右扫描中缀表达式;
* 3)遇到操作数时,将其压s2;
* 4)遇到运算符时,比较其与s1栈顶运算符的优先级:
* (1)如果s1为空,或栈顶运算符为左括号“(”,则直接将此运算符入栈;
* (2)否则,若优先级比栈顶运算符的高,也将运算符压入s1;
* (3)否则,将s1栈顶的运算符弹出并压入到s2中,再次转到(4-1)与s1中新的栈顶运算符相比较
* 5)遇到括号时:
* (1) 如果是左括号“(”,则直接压入s1
* (2) 如果是右括号“)”,则依次弹出s1栈顶的运算符,并压入s2,直到遇到左括号为止,此时将这一对括号丢弃
* 6)重复步骤2至5,直到表达式的最右边
* 7)将s1中剩余的运算符依次弹出并压入s2
* 8)依次弹出s2中的元素并输出,**结果的逆序即为中缀表达式对应的后缀表达式**
*
* @param ls 中缀表达式对应的list
* @return 后缀表达式对应的list
*/
public static List<String> parseSuffixExpressionList(List<String> ls) {
//定义栈
Stack<String> s1 = new Stack<>();//符号栈 s1栈
//说明:因为思路分析中使用的s2的栈在整个的运算的过程中没有进行pop的操作 我们直接使用list集合代替s2栈 (同时也为了方便后面逆序的输出)
ArrayList<String> s2 = new ArrayList<>();//用于存储中间结果的list
//遍历ls
for (String item : ls) {
//如果是一个数 就加入到s2栈
if (item.matches("\\d+")) {//正则匹配
s2.add(item);
} else if (item.equals("(")) { //如果是s1的话就直接入符号栈
s1.push(item);
} else if (item.equals(")")) {
//如果是右括号“)”,则依次弹出s1栈顶的运算符,并压入s2,直到遇到左括号为止,此时将这一对括号丢弃
while (!s1.peek().equals("(")) {
s2.add(s1.pop());
}
s1.pop();//将(弹出s1栈,这里的操作就是消除括号
} else {//加减乘除的操作
//当item的优先级小于栈顶的优先级,
// 将s1栈顶的运算符弹出并加入到s2中,再次转到(4.1)与s1中新的栈顶运算符相比较
while (s1.size() != 0 && getVal(s1.peek()) >= getVal(item)) {
s2.add(s1.pop());
}
//还需要将item压入栈中
s1.push(item);
}
}
//将s1中剩余的运算符依次弹出并加入s2
while (s1.size() != 0) {
s2.add(s1.pop());
}
return s2;
}

通过逆波兰表达式(后缀表达式)的集合得到最终的计算结果

/**
* 通过一个后缀表达式的list集合得到表达式最终的计算结果
* @param list 后缀表达式的list集合
* @return 根据后缀表达式的集合计算出的表达式的结果信息
*/
public static int calculate(List<String> list) {
//创建一个栈
Stack<String> stack = new Stack<>();
//遍历list
for (String item : list) {
//使用正则表达式取出数字
if (item.matches("\\d+")) { //匹配的是多位数
//入栈
stack.push(item);
} else {
//弹出两个数 并运算 再入栈
int num2 = Integer.parseInt(stack.pop());
int num1 = Integer.parseInt(stack.pop());
//运算
int res = 0;
if (item.equals("+")) {
res = num1 + num2;
} else if (item.equals("-")) {
res = num1 - num2;
} else if (item.equals("*")) {
res = num1 * num2;
} else if (item.equals("/")) {
res = num1 / num2;
} else {
throw new RuntimeException("运算符有误!");
}
//把结果入栈
stack.push(res + "");
}
}
//最后留在stack中的数就是运算结果
return Integer.parseInt(stack.pop());
}

完整的代码

(不支持小数)

package com.atguigu.stack;

import java.util.ArrayList;
import java.util.List;
import java.util.Stack;

/**
* @author GongChangjiang
* @version 1.0
* @Date 2023/5/11
* @Description 逆波兰计算器的设计与实现
*/
public class PolandNotation {
public static void main(String[] args) {
//先定义一个逆波兰表达式
//(3+4)×5-6 的逆波兰表达式是 3 4 + 5 × 6 -
//为了方便 逆波兰表达式的数字和符号有空格隔开
String suffixExpression = "3 4 + 5 * 6 -";//中间有空格隔开
//1.先将suffixExpression放入到List集合中
//2.将List集合传递给一个方法 配合栈 完成计算
List<String> rpnList = getListString(suffixExpression);
System.out.println("计算的结果是:" + calculate(rpnList));

//测试将中缀表达式转化成逆波兰表达式
//完成一个将中缀表达式 转换为一个后缀表达式
//1. 1+((2+3)×4)-5(中缀表达式) -> 1 2 3 + 4 × + 5 –(后缀表达式)
//2. 将中缀表达式转化成List集合
String expression = "1+((2+3)*4)-5";
List<String> list = toInfixExpressionList(expression);
System.out.println("中缀表达式对应的List:" + list);
//3.将得到的中缀表达式对应的List 转换成一个逆波兰表达式(后缀表达式)对应的List
List<String> stringList = parseSuffixExpressionList(list);
System.out.println("中缀表达式对应的List:" + stringList);
System.out.println("计算的结果:" + calculate(stringList));


}

/**
* 输入一个运算符 返回对应的优先级
*/
public static int getVal(String operation) {
int res = 0;
switch (operation) {
case "+":
case "-":
res = 1;
break;
case "/":
case "*":
res = 2;
break;
default:
break;
}
return res;
}

/**
* 将中缀表达式对应的List转化成后缀表达式对应的list
* tips:
* 1)初始化两个栈:运算符栈s1和储存中间结果的栈s2;
* 2)从左至右扫描中缀表达式;
* 3)遇到操作数时,将其压s2;
* 4)遇到运算符时,比较其与s1栈顶运算符的优先级:
* (1)如果s1为空,或栈顶运算符为左括号“(”,则直接将此运算符入栈;
* (2)否则,若优先级比栈顶运算符的高,也将运算符压入s1;
* (3)否则,将s1栈顶的运算符弹出并压入到s2中,再次转到(4-1)与s1中新的栈顶运算符相比较
* 5)遇到括号时:
* (1) 如果是左括号“(”,则直接压入s1
* (2) 如果是右括号“)”,则依次弹出s1栈顶的运算符,并压入s2,直到遇到左括号为止,此时将这一对括号丢弃
* 6)重复步骤2至5,直到表达式的最右边
* 7)将s1中剩余的运算符依次弹出并压入s2
* 8)依次弹出s2中的元素并输出,**结果的逆序即为中缀表达式对应的后缀表达式**
*
* @param ls 中缀表达式对应的list
* @return 后缀表达式对应的list
*/
public static List<String> parseSuffixExpressionList(List<String> ls) {
//定义栈
Stack<String> s1 = new Stack<>();//符号栈 s1栈
//说明:因为思路分析中使用的s2的栈在整个的运算的过程中没有进行pop的操作 我们直接使用list集合代替s2栈 (同时也为了方便后面逆序的输出)
ArrayList<String> s2 = new ArrayList<>();//用于存储中间结果的list
//遍历ls
for (String item : ls) {
//如果是一个数 就加入到s2栈
if (item.matches("\\d+")) {//正则匹配
s2.add(item);
} else if (item.equals("(")) { //如果是s1的话就直接入符号栈
s1.push(item);
} else if (item.equals(")")) {
//如果是右括号“)”,则依次弹出s1栈顶的运算符,并压入s2,直到遇到左括号为止,此时将这一对括号丢弃
while (!s1.peek().equals("(")) {
s2.add(s1.pop());
}
s1.pop();//将(弹出s1栈,这里的操作就是消除括号
} else {//加减乘除的操作
//当item的优先级小于栈顶的优先级,
// 将s1栈顶的运算符弹出并加入到s2中,再次转到(4.1)与s1中新的栈顶运算符相比较
while (s1.size() != 0 && getVal(s1.peek()) >= getVal(item)) {
s2.add(s1.pop());
}
//还需要将item压入栈中
s1.push(item);
}
}
//将s1中剩余的运算符依次弹出并加入s2
while (s1.size() != 0) {
s2.add(s1.pop());
}
return s2;
}

/**
* 将中缀表达式转化成对应的list
*
* @param expression 中缀表达式
* @return 中缀表达式转化成的一个list集合
*/
public static List<String> toInfixExpressionList(String expression) {
//定义一个集合,存储中缀表达式对应的list集合
List<String> list = new ArrayList<>();
int i = 0;//这个是一个指针 用于遍历中缀表达式的字符串
String str;//做多位数的拼接工作 ,因为我们要考虑多位数的情况
char c;//每遍历一个字符就放入到c中
do {
if ((c = expression.charAt(i)) < 48 || (c = expression.charAt(i)) > 57) { //如果c是一个非数字的字符需要加入到ls中
list.add(c + ""); //直接将这个字符添加到list集合中
i++;
} else {//如果是一个数 要考虑多位数的问题
str = "";//先将str置空
//这个只要遍历的有一个字符不是数字 就会退出while循环
while (i < expression.length() && (c = expression.charAt(i)) >= 48 && (c = expression.charAt(i)) <= 57) {
str += c;
i++;
}
list.add(str);
}
} while (i < expression.length());
return list;
}


//将一个逆波兰表达式依次将数据和运算符放入到List集合中
public static List<String> getListString(String suffixExpression) {
//将suffixExpression按照空格分割
String[] strings = suffixExpression.split(" ");
List<String> list = new ArrayList<>();
for (String string : strings) {
list.add(string);
}
return list;
}

/**
* 通过一个后缀表达式的list集合得到表达式最终的计算结果
*
* @param list 后缀表达式的list集合
* @return 根据后缀表达式的集合计算出的表达式的结果信息
*/
public static int calculate(List<String> list) {
//创建一个栈
Stack<String> stack = new Stack<>();
//遍历list
for (String item : list) {
//使用正则表达式取出数字
if (item.matches("\\d+")) { //匹配的是多位数
//入栈
stack.push(item);
} else {
//弹出两个数 并运算 再入栈
int num2 = Integer.parseInt(stack.pop());
int num1 = Integer.parseInt(stack.pop());
//运算
int res = 0;
if (item.equals("+")) {
res = num1 + num2;
} else if (item.equals("-")) {
res = num1 - num2;
} else if (item.equals("*")) {
res = num1 * num2;
} else if (item.equals("/")) {
res = num1 / num2;
} else {
throw new RuntimeException("运算符有误!");
}
//把结果入栈
stack.push(res + "");
}
}
//最后留在stack中的数就是运算结果
return Integer.parseInt(stack.pop());
}
}

完整的逆波兰计算器,含小数点的计算

(老师的代码)

package com.atguigu.stack;


import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Stack;
import java.util.regex.Pattern;

public class ReversePolishMultiCalc {

/**
* 匹配 + - * / ( ) 运算符
*/
static final String SYMBOL = "\\+|-|\\*|/|\\(|\\)";

static final String LEFT = "(";
static final String RIGHT = ")";
static final String ADD = "+";
static final String MINUS= "-";
static final String TIMES = "*";
static final String DIVISION = "/";

/**
* 加減 + -
*/
static final int LEVEL_01 = 1;
/**
* 乘除 * /
*/
static final int LEVEL_02 = 2;

/**
* 括号
*/
static final int LEVEL_HIGH = Integer.MAX_VALUE;


static Stack<String> stack = new Stack<>();
static List<String> data = Collections.synchronizedList(new ArrayList<String>());

/**
* 去除所有空白符
* @param s
* @return
*/
public static String replaceAllBlank(String s ){
// \\s+ 匹配任何空白字符,包括空格、制表符、换页符等等, 等价于[ \f\n\r\t\v]
return s.replaceAll("\\s+","");
}

/**
* 判断是不是数字 int double long float
* @param s
* @return
*/
public static boolean isNumber(String s){
Pattern pattern = Pattern.compile("^[-\\+]?[.\\d]*$");
return pattern.matcher(s).matches();
}

/**
* 判断是不是运算符
* @param s
* @return
*/
public static boolean isSymbol(String s){
return s.matches(SYMBOL);
}

/**
* 匹配运算等级
* @param s
* @return
*/
public static int calcLevel(String s){
if("+".equals(s) || "-".equals(s)){
return LEVEL_01;
} else if("*".equals(s) || "/".equals(s)){
return LEVEL_02;
}
return LEVEL_HIGH;
}

/**
* 匹配
* @param s
* @throws Exception
*/
public static List<String> doMatch (String s) throws Exception{
if(s == null || "".equals(s.trim())) throw new RuntimeException("data is empty");
if(!isNumber(s.charAt(0)+"")) throw new RuntimeException("data illeagle,start not with a number");

s = replaceAllBlank(s);

String each;
int start = 0;

for (int i = 0; i < s.length(); i++) {
if(isSymbol(s.charAt(i)+"")){
each = s.charAt(i)+"";
//栈为空,(操作符,或者 操作符优先级大于栈顶优先级 && 操作符优先级不是( )的优先级 及是 ) 不能直接入栈
if(stack.isEmpty() || LEFT.equals(each)
|| ((calcLevel(each) > calcLevel(stack.peek())) && calcLevel(each) < LEVEL_HIGH)){
stack.push(each);
}else if( !stack.isEmpty() && calcLevel(each) <= calcLevel(stack.peek())){
//栈非空,操作符优先级小于等于栈顶优先级时出栈入列,直到栈为空,或者遇到了(,最后操作符入栈
while (!stack.isEmpty() && calcLevel(each) <= calcLevel(stack.peek()) ){
if(calcLevel(stack.peek()) == LEVEL_HIGH){
break;
}
data.add(stack.pop());
}
stack.push(each);
}else if(RIGHT.equals(each)){
// ) 操作符,依次出栈入列直到空栈或者遇到了第一个)操作符,此时)出栈
while (!stack.isEmpty() && LEVEL_HIGH >= calcLevel(stack.peek())){
if(LEVEL_HIGH == calcLevel(stack.peek())){
stack.pop();
break;
}
data.add(stack.pop());
}
}
start = i ; //前一个运算符的位置
}else if( i == s.length()-1 || isSymbol(s.charAt(i+1)+"") ){
each = start == 0 ? s.substring(start,i+1) : s.substring(start+1,i+1);
if(isNumber(each)) {
data.add(each);
continue;
}
throw new RuntimeException("data not match number");
}
}
//如果栈里还有元素,此时元素需要依次出栈入列,可以想象栈里剩下栈顶为/,栈底为+,应该依次出栈入列,可以直接翻转整个stack 添加到队列
Collections.reverse(stack);
data.addAll(new ArrayList<>(stack));

System.out.println(data);
return data;
}

/**
* 算出结果
* @param list
* @return
*/
public static Double doCalc(List<String> list){
Double d = 0d;
if(list == null || list.isEmpty()){
return null;
}
if (list.size() == 1){
System.out.println(list);
d = Double.valueOf(list.get(0));
return d;
}
ArrayList<String> list1 = new ArrayList<>();
for (int i = 0; i < list.size(); i++) {
list1.add(list.get(i));
if(isSymbol(list.get(i))){
Double d1 = doTheMath(list.get(i - 2), list.get(i - 1), list.get(i));
list1.remove(i);
list1.remove(i-1);
list1.set(i-2,d1+"");
list1.addAll(list.subList(i+1,list.size()));
break;
}
}
doCalc(list1);
return d;
}

/**
* 运算
* @param s1
* @param s2
* @param symbol
* @return
*/
public static Double doTheMath(String s1,String s2,String symbol){
Double result ;
switch (symbol){
case ADD : result = Double.valueOf(s1) + Double.valueOf(s2); break;
case MINUS : result = Double.valueOf(s1) - Double.valueOf(s2); break;
case TIMES : result = Double.valueOf(s1) * Double.valueOf(s2); break;
case DIVISION : result = Double.valueOf(s1) / Double.valueOf(s2); break;
default : result = null;
}
return result;

}

public static void main(String[] args) {
//String math = "9+(3-1)*3+10/2";
String math = "12.8 + (2 - 3.55)*4+10/5.0";
try {
doCalc(doMatch(math));
} catch (Exception e) {
e.printStackTrace();
}
}

}

六.递归

1.简单介绍

递归就是方法自己调用自己,每次调用时传入不同的变量,递归有助于编程者解决复杂的问题,同时可以让代码变得简洁

image-20230511123527496

2.入门案例

image-20230511123245408

3.递归解决的问题

1)各种数学问题如: 8皇后问题 , 汉诺塔, 阶乘问题, 迷宫问题, 球和篮子的问题(google编程大赛)

2)各种算法中也会使用到递归,比如快排,归并排序,二分查找,分治算法等.

3)将用栈解决的问题–>第归代码比较简洁

4.递归遵循的规则

1)执行一个方法时,就创建一个新的受保护的独立空间(栈空间)

2)方法的局部变量是独立的,不会相互影响, 比如n变量

3)如果方法中使用的是引用类型变量(比如数组),就会共享该引用类型的数据.

4)递归必须向退出递归的条件逼近,否则就是无限递归,出现StackOverflowError,死龟了:)

5)当一个方法执行完毕,或者遇到return,就会返回,遵守谁调用,就将结果返回给谁,同时当方法执行完毕或者返回时,该方法也就执行完毕。

5.递归的实际应用

5.1递归解决迷宫问题

image-20230512092144712

代码实现

package com.atguigu.recursion;

/**
* @author GongChangjiang
* @version 1.0
* @Date 2023/5/12
* @Description 递归解决迷宫问题
*/
public class MiGong {
public static void main(String[] args) {
//创建一个二维数组模拟迷宫
//地图
int[][] map = new int[8][7];
//使用1表示墙的位置
//把四周变成墙
//将上下变成墙
for (int i = 0; i < 7; i++) {
map[0][i] = 1;
map[7][i] = 1;
}
//将左右变成墙
for (int i = 0; i < 8; i++) {
map[i][0] = 1;
map[i][6] = 1;
}
//设置自定义的墙的位置
map[3][1] = 1;
map[3][2] = 1;

//输出地图
showMap(map);
//使用递归回溯给小球找路
getWay(map, 1, 1);
System.out.println("标识过的路");
showMap(map);


}

/**
* 遍历输出地图的信息
*
* @param map 地图组成的二维数组
*/
public static void showMap(int[][] map) {
for (int i = 0; i < map.length; i++) {
for (int j = 0; j < map[i].length; j++) {
System.out.print(map[i][j] + " ");
}
System.out.println();
}
}


/**
* 1.表示墙 2.表示通路 可以走 3.表示该位置已经走过 但是走不通
* 在就走迷宫的时候我们要确定一个策略 比如下->右->上->左 如果该点走不通 再回溯
* 使用递归的方法给小球找路
*
* @param map 传进来的地图信息
* @param i 从哪个位置开始找
* @param j 从哪个位置开始找
* @return 是否找到路
*/
public static boolean getWay(int[][] map, int i, int j) {
if (map[6][5] == 2) { //说明通路已经找到了 就直接退出
return true;
} else {
if (map[i][j] == 0) {//如果当前这个点还没有走过
//按照策略走 下->右->上->左
map[i][j] = 2;//先假定这个点可以走通
if (getWay(map, i + 1, j)) { //向下走
return true;
} else if (getWay(map, i, j + 1)) {//向右走
return true;
} else if (getWay(map, i - 1, j)) {//向上走
return true;
} else if (getWay(map, i, j - 1)) {//向左走
return true;
} else {//四个方向都走不通的话说明不是一个通路 设置成false
map[i][j] = 3;
return false;
}
} else {//不为0 可以是1,2,3
//可以理解为只走没有走过的
return false;
}

}
}


}

5.2 递归解决八皇后问题

使用回溯算法解决 类似于穷举法 后期使用别的算法优化

问题介绍

image-20230512112050729

思路分析

image-20230512112456731

代码实现

package com.atguigu.recursion;

/**
* @author GongChangjiang
* @version 1.0
* @Date 2023/5/13
* @Description 八皇后问题的解题思路及代码实现
*/
public class Queue8 {
public static void main(String[] args) {
Queue8 queue8 = new Queue8();
queue8.check(0);
}

//定义一个max表示共有多少个皇后
int max = 8;
//定义一个一维数组 记录皇后在列上的位置 arr[8] = {0 , 4, 7, 5, 2, 6, 1, 3}
int[] array = new int[max];
//记录摆法的次数
int count = 0;

/**
* 编写一个方法 放置n个皇后
* 会自己回溯
*/
public void check(int n) {
if (n == max) { //说明已经放到了第九个皇后了
print(); //打印
return;
}
//依次放入皇后 判断是否冲突
for (int i = 0; i < max; i++) {
//先把当前的这个皇后n,放到该行的第i列
array[n] = i;
//判断当前放置第n个皇后到i列时,与前面放置的皇后是否冲突
if (judge(n)) { //不冲突
//接着放n+1个皇后
check(n + 1);
}
//如果冲突 就继续执行 array[n] = i 就皇后在本行后移一个位置
//因为在循环里面 i++会自增 n会后移
}
}

/**
* 当我们放置了n个皇后之后 就去检测
* 该皇后是否和前面已经摆放的皇后冲突
*
* @param n 表示第n个皇后
*/
public boolean judge(int n) {
for (int i = 0; i < n; i++) {
// array[8] = {0 , 4, 7, 5, 2, 6, 1, 3} 再次注意这里数组记录的是每个皇后在列上的值
//array[i] == array[i] 判断是不是在同一列
//Math.abs(n - i) == Math.abs(array[n] - array[i]) 判断是不是在同一斜线上
//自己的理解:(n -i) 相当于在行上的距离 array[n] - array[i]相当于在列上的距离 行距离等于列距离 说明二者在同一斜线上
if (array[i] == array[n] || Math.abs(n - i) == Math.abs(array[n] - array[i])) {//与前面放置的位置是否冲突
return false;
}
}
return true;
}

/**
* 定义一个数组 打印皇后拜访的位置
*/
public void print() {
System.out.println("第" + (++count) + "中摆法");
int[][] arr = new int[max][max];
for (int i = 0; i < array.length; i++) {
arr[i][array[i]] = 1;
}
for (int[] ints : arr) {
for (int i : ints) {
System.out.print(i + " ");
}
System.out.println();
}
}
}

七.排序算法

十大经典的排序算法:菜鸟教程排序算法

1.介绍

image-20230513115319799

2.时间复杂度

度量时间复杂度的两种方法

事后统计法的不足:

  • 需要实际的运行程序 比较耗时
  • 受计算机硬件和软件的影响

image-20230513115606521

时间频度

​ 基本的介绍:一个算法花费的时间与算法中语句的执行次数成正比例,哪个算法中语句执行次数多,它花费时间就多。一个算法中的语句执行次数称为语句频度或时间频度。记为T(n)

时间复杂度介绍

image-20230515093520587

常见的时间复杂度

image-20230515093753893

3.空间复杂度

image-20230515095247319

排序算法的时间和空间复杂度

image-20230516083738747

4.冒泡排序

冒泡排序的时间复杂度:o(n^2)

同一台电脑 8万个数据 十几秒左右

4.1 基本介绍

image-20230515100345339

4.2 图解

image-20230515100754827

4.3 代码实现

优化之前的代码

package com.atguigu.sort;

import java.util.Arrays;

/**
* @author GongChangjiang
* @version 1.0
* @Date 2023/5/15
* @Description 冒泡排序
*/
public class BubbleSort {
public static void main(String[] args) {
int arr[] = {3, 9, -1, 10, -2};
BubbleSort.sort(arr);
}

public static void sort(int[] arr) {
for (int i = 0; i < arr.length - 1; i++) {
int temp = 0; //临时变量
for (int j = 0; j < arr.length - 1 - i; j++) {
//如果前面的数字大于后面的数字就交换
if (arr[j] > arr[j + 1]) {
temp = arr[j];
arr[j] = arr[j + 1];
arr[j + 1] = temp;
}
}
System.out.println("第" + (i + 1) + "次排序:" + Arrays.toString(arr));
}
System.out.println("排序之后的数组:" + Arrays.toString(arr));
}
}

优化之后的代码(如果排序的过程中代码有序就不在排序)

package com.atguigu.sort;

import java.util.Arrays;

/**
* @author GongChangjiang
* @version 1.0
* @Date 2023/5/15
* @Description 冒泡排序
*/
public class BubbleSort {
public static void main(String[] args) {
int arr[] = {3, 9, -1, 10, -2};
BubbleSort.sort(arr);
}

/**
* 冒泡排序
* @param arr 需要排序的数组
*/
public static void sort(int[] arr) {
for (int i = 0; i < arr.length - 1; i++) {
int temp = 0; //临时变量
boolean flag = false; //表示是否进行过排序
for (int j = 0; j < arr.length - 1 - i; j++) {
//如果前面的数字大于后面的数字就交换
if (arr[j] > arr[j + 1]) {
flag = true;
temp = arr[j];
arr[j] = arr[j + 1];
arr[j + 1] = temp;
}
}
System.out.println("第" + (i + 1) + "次排序:" + Arrays.toString(arr));
if(!flag){ //说明一次都没有交换 说明已经有序
break;
}
}
System.out.println("排序之后的数组:" + Arrays.toString(arr));
}
}

image-20230516091218716

5.选择排序

5.1 基本介绍

同一台电脑 8万个数据 两秒左右

选择式排序也属于内部排序法,是从欲排序的数据中,按指定的规则选出某一元素,再依规定交换位置后达到排序的目的

image-20230515105359552

5.2图解

img

image-20230516083148055

5.3代码实现

两种方法 一个是自己的 一个是老师的

使用老师的代码 老师的代码验证过

package com.atguigu.sort;

import java.util.Arrays;

/**
* @author GongChangjiang
* @version 1.0
* @Date 2023/5/16
* @Description 选择排序
*/
public class SelectSort {
public static void main(String[] args) {
int[] arr = {101,34,119,1};
int[] arr1 = {3, 9, -1, 10, -2};
int[] arr2 = {3, 9, -1, 10, -2};
System.out.println("自己的代码");
SelectSort.selectSort(arr2);
System.out.println("老师的代码");
SelectSort.selectSortByTeacher(arr1);
}

/**
* 选择排序
* 自己写的 中间的过程和老师的不一样 不确定是不是选择排序
*/
public static void selectSort(int[] arr) {
for (int i = 0; i < arr.length - 1; i++) {
for (int j = i + 1; j < arr.length; j++) {//从第一个数字开始 依次找到最小值
int temp = 0;
if(arr[i] > arr[j]) {
temp = arr[i];
arr[i] = arr[j];
arr[j] = temp;
}
}
System.out.println("第次"+(i+1)+"排序的结果:"+Arrays.toString(arr));
}
System.out.println("最终的结果:"+Arrays.toString(arr));
}

/**
* 老师的代码
*/
public static void selectSortByTeacher(int[] arr){

for (int i = 0; i < arr.length - 1; i++) {
int minIndex = i;
int min = arr[i];
for (int j = i + 1; j < arr.length; j++) {//从第一个数字开始 依次找到最小值
if(min > arr[j]){
min = arr[j];
minIndex = j;
}
}
//交换
arr[minIndex] = arr[i];
arr[i] = min;
System.out.println("第次"+(i+1)+"排序的结果:"+Arrays.toString(arr));
}
System.out.println("最终的结果:"+Arrays.toString(arr));
}
}

image-20230516093143656

6.插入排序

6.1 基本介绍

同一台电脑 8万个数据 五秒左右

插入式排序属于内部排序法,是对于欲排序的元素以插入的方式找寻该元素的适当位置,以达到排序的目的。

image-20230516100256880

6.2 图解

img

6.3 代码实现

package com.atguigu.sort;

import java.util.Arrays;

/**
* @author GongChangjiang
* @version 1.0
* @Date 2023/5/16
* @Description 插入排序
*/
public class InsertSort {
public static void main(String[] args) {
int[] arr = {101, 34, 119, 1,-1,89};
System.out.println("插入前的数组:"+Arrays.toString(arr));
InsertSort.insertSort(arr);
}

/**
* 插入排序
*/
public static void insertSort(int[] arr) {
for (int i = 1; i < arr.length; i++) {
//定义待插入的数
int insertVal = arr[i];
//待插入的数的前一个数的下标
int insertIndex = i - 1;
while (insertIndex >= 0 && insertVal < arr[insertIndex]) {
arr[insertIndex + 1] = arr[insertIndex];
insertIndex--;
}
//退出循环之后 代表找到了要插入数据的位置
//插入数据
arr[insertIndex + 1] = insertVal;
System.out.println("第" + i + "次插入的结果:" + Arrays.toString(arr));
}
System.out.println("最终的结果:" + Arrays.toString(arr));
}
}

image-20230516105517026

7.希尔排序

7.1基本介绍

同一台电脑 8万个数据 十七秒左右(交换法)

同一台电脑 8万个数据 一秒左右(移动法)

image-20230517091741355

image-20230517091841366

7.2.图解

img

image-20230517102649272

7.3 代码实现

package com.atguigu.sort;

import java.util.Arrays;

/**
* @author GongChangjiang
* @version 1.0
* @Date 2023/5/17
* @Description 希尔排序
*/
public class ShellSort {
public static void main(String[] args) {
int[] arr = {8, 9, 1, 7, 2, 3, 5, 4, 6, 0};
//shellSort(arr);
shellSort2(arr);
}

/**
* 希尔排序 使用的是交换法 这个很慢 比直接的插入排序还慢
*/
public static void shellSort(int[] arr) {
int temp = 0;
int count = 0;
//用于分组 10个数据 第一次分5组 第二次分2组 第三次分1组 每组分别插入排序
for (int gap = arr.length / 2; gap > 0; gap /= 2) {
for (int i = gap; i < arr.length; i++) {
for (int j = i - gap; j >= 0; j -= gap) {
//如果当前的那个元素大于加上步长后的那个元素,就交换
if (arr[j] > arr[j + gap]) {
temp = arr[j];
arr[j] = arr[j + gap];
arr[j + gap] = temp;
}
}
}
System.out.println("排序第" + (++count) + "轮的结果:" + Arrays.toString(arr));
}
System.out.println("最终的结果:" + Arrays.toString(arr));
}

/**
* 使用移动法的希尔排序 这个更快
*/
public static void shellSort2(int[] arr) {
//用于分组 10个数据 第一次分5组 第二次分2组 第三次分1组 每组分别插入排序
int count = 0;
for (int gap = arr.length / 2; gap > 0; gap /= 2) {
for (int i = gap; i < arr.length; i++) {
int j = i;
int temp = arr[j];
if (arr[j] < arr[j - gap]) {
while (j - gap >= 0 && temp < arr[j - gap]) {
arr[j] = arr[j - gap];
j -= gap;
}
arr[j] = temp; }
}
System.out.println("排序第" + (++count) + "轮的结果:" + Arrays.toString(arr));
}
System.out.println("最终的结果:" + Arrays.toString(arr));
}
}

8.快速排序

8.1 基本介绍

同一台电脑 8万个数据 不到一秒 800万个数据两秒钟

快速排序(Quicksort)是对冒泡排序的一种改进。基本思想是:通过一趟排序将要排序的数据分割成独立的两部分,其中一部分的所有数据都比另外一部分的所有数据都要小,然后再按此方法对这两部分数据分别进行快速排序,整个排序过程可以递归进行,以此达到整个数据变成有序序列

8.2 图解

img image-20230517103139900

8.3 代码实现

package com.atguigu.sort;

import java.util.Arrays;

/**
* @author GongChangjiang
* @version 1.0
* @Date 2023/5/17
* @Description 快速排序
*/
public class QuickSort {
public static void main(String[] args) {
int[] arr = {-9, 78, 0, 23, -567, 70};
quickSort(arr, 0, arr.length - 1);
System.out.println(Arrays.toString(arr));
}

/**
* @param arr 需要排序的数组
* @param left 左下标
* @param right 右下标
*/
public static void quickSort(int[] arr, int left, int right) {
int l = left;
int r = right;
//pivot 中轴
int pivot = arr[(left + right) / 2];
int temp = 0;
//while循环的目的是让比pivot小的放到左边 大的放在右边
while (l < r) {
//在pivot的左边一直找 找到大于等于pivot的值 才退出
while (arr[l] < pivot) {
l += 1;
}
//在pivot的左边一直找 找到大于等于pivot的值 才退出
while (arr[r] > pivot) {
r -= 1;
}
//如果l >= r成立 说明pivot的左右两边的值 已经是按照左边全部是
//小于等于pivot值 右边全部是大于等于pivot值
if (l >= r) {
break;
}
//交换
temp = arr[l];
arr[l] = arr[r];
arr[r] = temp;
//如果交换完之后发现arr[l] = pivot值
if (arr[l] == pivot) {
r--;
}
//如果交换完之后发现arr[r] = pivot值
if (arr[r] == pivot) {
l++;
}
}

//如果l == r 必须l++ r--
if (l == r) {
l += 1;
r -= 1;
}
//向左递归
if (left < r) {//左边的数全部有序
quickSort(arr, left, r);
}
//向右递归
if (right > l) {
quickSort(arr, l, right);
}
}
}

9. 归并排序

9.1基本介绍

同一台电脑 8万个数据 大约一秒钟 800万个数据三秒

image-20230518111233009

9.2 图解

img

image-20230518111454775

image-20230518111536026

9.3代码实现

package com.atguigu.sort;

import java.util.Arrays;

/**
* @author GongChangjiang
* @version 1.0
* @Date 2023/5/18
* @Description 归并排序
*/
public class MergeSort {
public static void main(String[] args) {
int[] arr = {8, 4, 5, 7, 1, 3, 6, 2};
int[] temp = new int[arr.length];
mergeSort(arr,0, arr.length -1,temp);
System.out.println("归并排序之后的数组:"+ Arrays.toString(arr));
}

/**
* 分治的过程
*/
public static void mergeSort(int[] arr, int left, int right, int[] temp){
if(left < right){
int mid = (left + right) / 2;
//向左递归分解
mergeSort(arr,left,mid,temp);
//向右递归分解
mergeSort(arr,mid+1,right,temp);
//合并
merge(arr,left,mid,right,temp);
}
}

/**
* 合并的方法 治
*
* @param arr 需要排序的数组
* @param left 右边有序序列的初始索引
* @param mid 中间索引
* @param right 右边索引
* @param temp 中转的数组
*/
public static void merge(int[] arr, int left, int mid, int right, int[] temp) {
int i = left;//左边有序序列的初始索引
int j = mid + 1;//右边有序序列的初始化索引
int t = 0;//指向temp数组的当前索引

//1.
//先把左右两边(有序)的数据填充到temp数组
//直到左右两边的数据有一边处理完毕为止
while (i <= mid && j <= right) {//相当于左右两边的数组都有一个指针
if (arr[i] <= arr[j]) {
temp[t] = arr[i];
t++;
i++;
} else {//反之将右边有序序列的当前元素 填充到temp数组
temp[t] = arr[j];
t++;
j++;
}
}
//2.
//把有剩余数据的一边的数据依次全部填充到temp
while (i <= mid) {//说明左边的有序序列还有剩余的元素
temp[t] = arr[i];
t++;
i++;
}
while (j <= right) {
temp[t] = arr[j];
t++;
j++;
}

//3.
//将temp数组的元素拷贝到arr
//并不是每次都拷贝
t = 0;
int tempLeft = left;
while (tempLeft <= right) {
arr[tempLeft] = temp[t];
t++;
tempLeft++;
}
}
}

10.基数排序

同一台电脑8万个数据 一秒左右 8千万个数据会报内存不足

10.1 基本介绍

image-20230525195008102

image-20230526094945578

image-20230526113739081

10.2 图解

img

10.3 代码实现

推导代码

package com.atguigu.sort;

import java.util.Arrays;

/**
* @author GongChangjiang
* @version 1.0
* @Date 2023/5/26
* @Description 基数排序
*/
public class RadixSort {
public static void main(String[] args) {
//定义一个待排序的数组
int arr[] = {53, 3, 542, 748, 14, 214};
RadixSort.radixSort(arr);
}

//基数排序
public static void radixSort(int[] arr) {
//1.第一轮(针对每个元素的个位进行排序处理)
//定义一个二维数组,表示十个桶,每个桶就是一个一维数组
//为了防止数据的溢出,这里每个桶的大小我们设置的大一些 大小定为arr.length
int[][] bucket = new int[10][arr.length];
//为了记录每个桶中实际存放了多少个数据,我们定义一个一维数组来记录每个桶中依次放入数据的个数
//bucketElementCounts[0]记录的就是第0个桶的数据的个数
int[] bucketElementCounts = new int[10];
for (int j = 0; j < arr.length; j++) {
//取出每个元素的个位数的值
int digitOfElement = arr[j] % 10;
//放入对应的桶中
bucket[digitOfElement][bucketElementCounts[digitOfElement]] = arr[j];
//[bucketElementCounts[digitOfElement]]++的意思是在digitOfElement对应的桶中数据个数加一
bucketElementCounts[digitOfElement]++;
}
//按照这个桶中顺序取出数据
int index = 0;
for (int k = 0; k < bucketElementCounts.length; k++) {
//如果桶中有数据,我们才放入到原数组
if (bucketElementCounts[k] != 0) {
//循环该桶即第K个桶,放入
for (int l = 0; l < bucketElementCounts[k]; l++) {
arr[index++] = bucket[k][l];
}
}
bucketElementCounts[k] = 0;
}
System.out.println("第一轮:" + Arrays.toString(arr));

//2.第2轮
for (int j = 0; j < arr.length; j++) {
//取出每个元素的十位数的值
int digitOfElement = arr[j] / 10 % 10;
//放入对应的桶中
bucket[digitOfElement][bucketElementCounts[digitOfElement]] = arr[j];
//[bucketElementCounts[digitOfElement]]++的意思是在digitOfElement对应的桶中数据个数加一
bucketElementCounts[digitOfElement]++;
}
//按照这个桶中顺序取出数据
index = 0;
for (int k = 0; k < bucketElementCounts.length; k++) {
//如果桶中有数据,我们才放入到原数组
if (bucketElementCounts[k] != 0) {
//循环该桶即第K个桶,放入
for (int l = 0; l < bucketElementCounts[k]; l++) {
arr[index++] = bucket[k][l];
}
}
bucketElementCounts[k] = 0;
}
System.out.println("第2轮:" + Arrays.toString(arr));

//3.第3轮
for (int j = 0; j < arr.length; j++) {
//取出每个元素的百位数的值
int digitOfElement = arr[j] / 100 % 10;
//放入对应的桶中
bucket[digitOfElement][bucketElementCounts[digitOfElement]] = arr[j];
//[bucketElementCounts[digitOfElement]]++的意思是在digitOfElement对应的桶中数据个数加一
bucketElementCounts[digitOfElement]++;
}
//按照这个桶中顺序取出数据
index = 0;
for (int k = 0; k < bucketElementCounts.length; k++) {
//如果桶中有数据,我们才放入到原数组
if (bucketElementCounts[k] != 0) {
//循环该桶即第K个桶,放入
for (int l = 0; l < bucketElementCounts[k]; l++) {
arr[index++] = bucket[k][l];
}
}
}
System.out.println("第3轮:" + Arrays.toString(arr));

}
}

最终代码

package com.atguigu.sort;

import java.util.Arrays;

/**
* @author GongChangjiang
* @version 1.0
* @Date 2023/5/26
* @Description 基数排序
*/
public class RadixSort {
public static void main(String[] args) {
//定义一个待排序的数组
int arr[] = {53, 3, 542, 748, 14, 214};
RadixSort.radixSort(arr);
}

//基数排序
public static void radixSort(int[] arr) {
//得到数组中最大数的位数
int max = arr[0];
for (int i = 0; i < arr.length; i++) {
if (arr[i] > max) {
max = arr[i];
}
}
//最大数的位数
int maxLength = (max + "").length();
//定义一个二维数组,表示十个桶,每个桶就是一个一维数组
int[][] bucket = new int[10][arr.length];
//为了记录每个桶中实际存放了多少个数据,我们定义一个一维数组来记录每个桶中依次放入数据的个数
int[] bucketElementCounts = new int[10];
for (int i = 0, n = 1; i < maxLength; i++, n *= 10) {//循环的是每一轮,第一次是个位。第二次是百位
for (int j = 0; j < arr.length; j++) {
//取出每个元素的对应位数的值
int digitOfElement = arr[j] / n % 10;
//放入对应的桶中
bucket[digitOfElement][bucketElementCounts[digitOfElement]] = arr[j];
//[bucketElementCounts[digitOfElement]]++的意思是在digitOfElement对应的桶中数据个数加一
bucketElementCounts[digitOfElement]++;
}
//按照这个桶中顺序取出数据
int index = 0;
for (int k = 0; k < bucketElementCounts.length; k++) {
//如果桶中有数据,我们才放入到原数组
if (bucketElementCounts[k] != 0) {
//循环该桶即第K个桶,放入
for (int l = 0; l < bucketElementCounts[k]; l++) {
arr[index++] = bucket[k][l];
}
}
bucketElementCounts[k] = 0;
}
System.out.println("第" + (i + 1) + "轮:" + Arrays.toString(arr));
}
}
}

11.常用排序算法总结和对比

image-20230526114127402

八.查找算法

1.简单介绍

image-20230526115027518

2.线性查找算法

2.1代码实现

package com.atguigu.search;

/**
* @author GongChangjiang
* @version 1.0
* @Date 2023/5/29
* @Description 线性查找
*/
public class SeqSearch {
public static void main(String[] args) {
int[] arr = {1,9,11,-1,34,89};//没有顺序的数组
System.out.println("目标值的下标:"+seqSearch(arr, 11));
}

/**
* 找到一个满足条件的值就返回
* 线性查找是逐一比对,发现有相同的值时就返回这个值的下标
* @param arr 需要在这里面找目标值的数组
* @param value 目标值
* @return 目标值的下标
*/
public static int seqSearch(int[] arr,int value){
for (int i = 0; i < arr.length; i++) {
if(arr[i] == value){
return i;
}
}
return -1;
}
}

3.二分查找算法

3.1 图解

image-20230529104112317

3.2 代码实现

递归的方式解决

基本的写法

package com.atguigu.search;

/**
* @author GongChangjiang
* @version 1.0
* @Date 2023/5/29
* @Description 二分查找
*/
public class BinarySearch {
public static void main(String[] args) {
int[] arr = {1, 8, 10, 89, 1000, 1234};
System.out.println("目标值的下标:" + binarySearch(arr, 0, arr.length - 1, 3));
}

/**
* 二分查找
*
* @param arr 需要在这里面查找目标值的数组
* @param left 左边的索引
* @param right 右边的索引
* @param findVal 要查找的值
* @return 目标值在数组中的下标,找到就返回下标,没有就返回-1
*/
public static int binarySearch(int[] arr, int left, int right, int findVal) {
//结束递归的条件
if (left > right) {
return -1;
}
int mid = (left + right) / 2;
int midVal = arr[mid];
if (findVal > midVal) {//向右递归
return binarySearch(arr, mid + 1, right, findVal);
} else if (findVal < midVal) {//向左递归
return binarySearch(arr, left, mid - 1, findVal);
} else {
return mid;
}
}

}

完整的代码

package com.atguigu.search;

import java.util.ArrayList;
import java.util.List;

/**
* @author GongChangjiang
* @version 1.0
* @Date 2023/5/29
* @Description 二分查找
*/
public class BinarySearch {
public static void main(String[] args) {
int[] arr = {1, 8, 10, 89, 1000, 1234};
System.out.println("目标值的下标:" + binarySearch(arr, 0, arr.length - 1, 3));
int[] arr2 = {1, 8, 10, 89, 89, 89, 1000, 1234};
System.out.println("目标值在数组中下标的集合:" + binarySearch2(arr2, 0, arr2.length - 1, 89));
}

/**
* 二分查找
*
* @param arr 需要在这里面查找目标值的数组
* @param left 左边的索引
* @param right 右边的索引
* @param findVal 要查找的值
* @return 目标值在数组中的下标,找到就返回下标,没有就返回-1
*/
public static int binarySearch(int[] arr, int left, int right, int findVal) {
//结束递归的条件
if (left > right) {
return -1;
}
int mid = (left + right) / 2;
int midVal = arr[mid];
if (findVal > midVal) {//向右递归
return binarySearch(arr, mid + 1, right, findVal);
} else if (findVal < midVal) {//向左递归
return binarySearch(arr, left, mid - 1, findVal);
} else {
return mid;
}
}

/**
* 二分查找查找出所有与目标值相同的数组的下标
*
* @param arr 需要在这里面查找目标值的数组
* @param left 左边的索引
* @param right 右边的索引
* @param findVal 要查找的的值
* @return 目标值在数组中的下标的集合
*/
public static List<Integer> binarySearch2(int[] arr, int left, int right, int findVal) {
//结束递归的条件
if (left > right) {
return new ArrayList<>();
}
int mid = (left + right) / 2;
int midVal = arr[mid];
if (findVal > midVal) {//向右递归
return binarySearch2(arr, mid + 1, right, findVal);
} else if (findVal < midVal) {//向左递归
return binarySearch2(arr, left, mid - 1, findVal);
} else {
List<Integer> resList = new ArrayList<>();
int temp = mid - 1;
while (true) { //向左边探测相同值的元素
if (temp < 0 || arr[temp] != findVal) {
break;
}
//否则就把temp放入到集合中
resList.add(temp);
temp--;//左移
}
resList.add(mid);
//向右边探测
temp = mid + 1;
while (true) { //向右边探测相同值的元素
if (temp > arr.length - 1 || arr[temp] != findVal) {
break;
}
//否则就把temp放入到集合中
resList.add(temp);
temp++;//右移
}
return resList;
}
}

}

4.插值查找算法

4.1 图解

image-20230530101803392

4.2 代码实现

package com.atguigu.search;

import java.util.Arrays;

/**
* @author GongChangjiang
* @version 1.0
* @Date 2023/5/30
* @Description 插值查找算法
*/
public class InsertValueSearch {
public static void main(String[] args) {
//创建一个数组 用于模拟需要查找数据的数组
int[] arr = new int[100];
for (int i = 0; i < 100; i++) {
arr[i] = i + 1;
}
System.out.println(Arrays.toString(arr));
System.out.println("查找的数的下标为:" + insertValueSearch(arr, 0, arr.length - 1, 30));
}

/**
* 插值查找算法
*
* @param arr 目标数组(有序的)等差序列的最好,1-100有序的数组,找一个数只用找一次就行了
* @param left 左边索引
* @param right 右边索引
* @param findVal 需要查找的值
* @return 查找的值在数组中的索引
*/
public static int insertValueSearch(int[] arr, int left, int right, int findVal) {
System.out.println("方法被调用了");
//退出的条件和防止数组越界
if (left > right || findVal < arr[0] || findVal > arr[arr.length - 1]) {
return -1;
}
//求出mid,这个是插值查找算法的灵魂,自适应的查找
int mid = left + (right - left) * (findVal - arr[left]) / (arr[right] - arr[left]);
int midVal = arr[mid];
if (findVal > midVal) { //应向右递归
return insertValueSearch(arr, mid + 1, right, findVal);
} else if (findVal < midVal) {//向左边递归
return insertValueSearch(arr, left, mid - 1, findVal);
} else {
return mid;
}
}
}

4.3 注意事项

image-20230530105652529

5.斐波那契(黄金分割法)查找算法

5.1 基本介绍

image-20230530110145048

5.2 原理介绍

image-20230530110511769


PDF笔记

]]>
后端 数据结构与算法
简历模板 /posts/29250.html 黑马程序员新版Java面试专题视频教程,java八股文面试全套真题+深度详解(含大厂高频面试真题) https://www.bilibili.com/video/BV1yT411H7YK/?share_source=copy_web&vd_source=aee5e475191b69e6c781059ab6662584

Java批注简历标准

image-20230508235133469

面试参考话术

模板

全部的模板地址: JasonsGong/resume: 简历模板 (github.com)

模板一 本科一年经验

模板二 Java开发_AAA_N年

模板三 灰蓝色色时尚简历模板

模板四 灰色大气简约简历模板

模板五 简约大气橙色简历模板

模板六 经典风格简历模板

模板七 时尚线条简历模板

模板八 科技版简历模板

模板九 JAVA开发_李传播_5年

]]>
面试 简历
面试题集锦 /posts/29985.html 参考资料:

Java工程师面试 宝典学习说明_互联网校招面试真题面经汇总_牛客网 (nowcoder.com)

Java | JavaGuide(Java面试 + 学习指南)

一.Java基础面试题

1.谈谈你对面向对象的理解

​ 对比面向过程,是两种不同处理问题的角度,面向过程更注重事情的每一步骤及顺序,面向对象更注重事情有哪些参与者(对象),以及各自需要做什么。面向过程比较直接高效,面向对象易于复用、扩展和维护。

面向对象的三大基本特征:封装、继承、多态(父类应用指向子类对象)

2.JDK、JRE、JVM之间的区别

JDK java 开发工具

JRE(Java Runtime Environment Java 运行环境)

JVM java虚拟机

JDK = JRE + 开发工具集(例如 Javac,java 编译工具等)

JRE = JVM + Java SE 标准类库(java 核心类库)

3.==和equals⽅法的区别

==:如果是基本数据类型,⽐较是值,如果是引⽤类型,⽐较的是引⽤地址
equals:具体看各个类重写equals⽅法之后的⽐较逻辑,⽐如String类,虽然是引⽤类型,但是String类中重写了equals⽅法,⽅法内部⽐较的是字符串中的各个字符是否全部相等。

4.String,StringBuffer,StringBuilder的区别

1.String是不可变的,如果尝试去修改,会新⽣成⼀个字符串对象,StringBuffer和StringBuilder是可变的,修改是在原对象上操作的

2.StringBuffer是线程安全的,StringBuilder是线程不安全的,所以在单线程环境下StringBuilder效率会更⾼

性能: StringBuilder > StringBuffer > String

5.重载和重写的区别

重载:发生在同一个类中,方法名必须相同,参数类型不同、个数不同、顺序不同、方法返回值和访问修饰符可以不同,发生在编译时。

重写:发生在父子类中,方法名,参数列表必须相同,返回值范围小于等于父类,抛出异常范围小于等于父类,访问修饰符范围大于等于父类;如果父类访问修饰符为私有,子类不能重写该方法。

6.接口和抽象类的区别

1.抽象类可以有实现的方法和抽象的方法

2.抽象类的成员变量可以是各种类型的,接口中的成员变量只能是public static final类型(默认)的

3.抽象类只能继承一个,接口可以实现多个

4.关键字不同,接口的关键字是interface,抽象类的关键字是abstract

7.List和set的区别

List:有序,按对象进入的顺序保存对象,可重复,允许多个Null元素对象,可以使用iterator取出所有的元素,在逐一遍历,还可以使用get(int index)获取指定下标的元素。

Set:无序,不可重复,最多允许有一个Null元素对象,取元素时只能用iterator接口取得所有元素,再逐一遍历各个元素。

8.hashCode()与equals()之间的关系

hashCode()的作用是获取哈希码,也称散列码,哈希码的作用是确定该对象在哈希表中的索引位置。

  • 如果两个对象相等,那么它们的hashCode()值一定相同

  • 如果两个对象hashCode()相等,它们并不一定相等

9.ArrayList和LinkedList的区别

ArrayList:基于动态数组,连续内存存储,适合下标访问(随机访问)。扩容机制:因为数组长度固定,超出长度存数据时需要新建数组,将老数组的数据拷贝到新数组,然后插入需要加入到数组中的数据。

LinkedList:基于链表,可以存储在分散的内存中,适合做数据插入及删除操作,不适合查询。遍历LinkedList必须使用iterator不能使用for循环,因为for循环体内通过get(i)取得某一元素时都需要对list重新遍历,性能消耗大。

10.HashMap和HashTable的区别?底层实现是什么?

区别:

1.HashMap方法没有synchronized修饰,线程非安全,HashTable线程安全

2.HashMap允许key和value为null,而HashTable不允许

底层实现: 数组 + 链表

11.ConcurrentHashMap

ConcurrentHashMap和HashTable都是线程安全的,但ConcurrentHashMap相比HashTable性能更高

12.如何实现一个IOC容器

1.配置文件配置包扫描路径

2.递归包扫描获取.class文件

3.反射、确定需要交给IOC管理的类

4.对需要注入的类进行依赖注入

13.什么是字节码?采用字节码的好处是什么?

什么是字节码?

Java中引入了虚拟机的概念,即在机器和编译程序之间加入了一层抽象的虚拟的机器。编译程序只需要面向虚拟机,生成虚拟机能够理解的代码,然后由解释器来将虚拟机代码转化为特定系统的机器码执行,在java中,这种供虚拟机理解的代码叫做字节码(即扩展名为.class的文件)。

采用字节码的好处是什么?

Java语言通过字节码的方式,在一定程度上解决了传统解释型语言执行效率的问题,同时又保留了解释型语言可移植的特点。所以Java程序运行时比较高效,而且,由于字节码并不专对一种特定的机器,因此,Java程序无需重新编译便可在多种计算机上运行。

14.Java类加载器有哪些

JDK有三个类加载器:

  1. bootstrap ClassLoader是ExClassLoader 的父类加载器,默认负责加载%JAVA_HOME%bin下的jar包和class文件
  2. ExClassLoader是AppClassLoader的父类加载器,负责加载%JAVA_HOME%/bin/ext文件夹下的jar包和class文件
  3. AppClassLoader是自定义加载器的父类,负责加载classpath下的类文件,系统类加载器,线程上下文加载器

自定义加载器的方法: 继承ClassLoader实现自定义加载器

15.双亲委派模型

双亲委派模型的执行流程是这样的:

1、当加载一个类时,会先从应用程序类加载器的缓存里查找相应的类,如果能找到就返回对象,如果找不到就执行下面流程;

2、在扩展加载器缓存中查找相应的类,如果能找到就返回对象,如果找不到就继续下面流程;

3、在启动类加载器中查询相应的类,如果找到就返回对象,如果找不到就继续下面流程;

4、在扩展加载器中查找并加载类,如果能找到就返回对象,并将对象加入到缓存中,如果找不到就继续下面流程;

5、在应用程序类加载器中查找并加载类,如果能找到就返回对象,并将对象加入到缓存中,如果找不到就返回 ClassNotFound 异常。

加载流程如下图所示:

image-20230904161407239

一般“双亲”指的是“父亲”和“母亲”,而在这里“双亲”指的是类加载类先向上找,再向下找的流程就叫做双亲委派模型。

双亲委派模型的好处

  1. 主要是为了安全性,避免用户自己编写的类动他替换java的一些核心类,比如String
  2. 同时避免了类的重复加载,因为jvm中区分不同类,不仅仅是根据类名,相同的class文件被不同的ClassLoader加载就是不同的两个类

16.Java中的异常体系

  1. Java中所有异常都来自顶级父类Throwable
  2. Throwable下面有两个子类Exception和Error
  3. Error是程序无法处理的错误,一旦出现这个错误,程序将被迫停止运行;Exception不会导致程序停止
  4. Exception有分为两个部分RunTimeException运行时异常和CheckedException检查异常
  5. RunTimeException常常发生在程序运行过程中,会导致程序当前线程执行失败。CheckedException常常发生在程序编译过程中,会导致程序编译不通过。

17.GC如何判断对象可以被回收

  • 引用计数法:每一个对象有一个引用计数属性,新增一个引用计数加1,引用释放减1,计数为0时可以回收。(java中没有使用这个方法,原因:可能会出现A引用了B,B又引用了A,这时就算他们都不再使用了,但是因为他们互相引用,计数器=1永远无法被回收)
  • 可达性分析法:从GC Roots开始向下搜索,搜索所走过的路径称为引用链。当一个对象到GC Roots没有任何引用链相连时,则证明此对象是不可用的,那么虚拟机就可以判断是可回收对象。

18.线程的生命周期,线程有哪些状态

线程通常有五种状态:创建、就绪、运行、阻塞、死亡状态

阻塞又分为三种情况:等待阻塞、同步阻塞、其他阻塞

  1. 新建状态:新创建了一个线程对象
  2. 就绪状态:线程对象创建后,其他线程调用了该对象的start()方法。该线程位于可运行线程池中,变得可运行,等待获取CPU的使用权
  3. 运行状态:就绪状态的线程获取了CPU,执行了程序代码
  4. 阻塞状态:阻塞状态是线程因为某种原因放弃了CPU使用权,暂时停止运行。直到线程进入就绪状态,才有机会转到运行状态
  5. 死亡状态:线程执行完了或者因为异常退出了run()方法,该线程结束生命周期

19.sleep(),wait(),join(),yield()的区别

sleep(),wait()的区别

  1. sleep()是Thread类的静态本地方法,wait()是Object类的本地方法
  2. sleep()不会释放锁(把锁带着进入冻结状态),wait会释放锁
  3. sleep方法不依赖于同步器synchronized,但是wait需要依赖synchronized关键字
  4. sleep不需要被唤醒(休眠之后退出阻塞),但是wait需要(不指定时间需要被别人中断)
  5. sleep一般用于当前线程休眠,或者轮循暂停操作,wait则用于多线程之间的通信
  6. sleep会让出cpu执行时间且强制上下文切换,而wait则不一定,wait后可能还是有机会重新竞争到锁继续执行

yield()执行后线程直接进入就绪状态,马上释放了cpu,但是依然保留了cpu的执行资格,所有有可能cpu下次进行线程调度还会让这个线程取到执行权

join()执行后线程进入阻塞状态,例如在线程B中调用了A的join(),那线程B会进入到阻塞队列,直到线程A结束或者中断线程

20.说说你对线程安全的理解

线程安全讲的不是线程安全,应该是内存安全堆是共享内存,可以被所有线程访问

线程安全的定义:当多个线程访问一个对象的时候,如果不用进行额外的同步控制或者其他的协调操作,调用这个对象的行为都可以获得正确的结果,我们就说这个线程数是安全的。

产生线程安全问题的原因: 在每个进程的内存空间中都会有一块特殊的公共区域,通常称为堆(内存)。进程内所有线程都可以访问到该区域,这就是造成问题的潜在原因。

21.说说你对守护线程的理解

守护线程:为非守护线程(用户线程)提供服务的线程,任何一个守护线程都是整个jvm中所有非守护线程的守护线程。

守护线程的作用:

举例:GC垃圾回收机制,就是一个经典的守护线程,当我们的程序不再有任何运行的Thread,程序就不会再产生垃圾,垃圾回收器也就没事可做,所以当垃圾回收线程是jvm上仅剩的线程时,垃圾回收线程会自动离开,它始终再低级别的状态中运行,用于实时监控和管理系统中的可回收资源。

应用场景:

  1. 为其他的线程提供服务支持情况
  2. 或者在任何情况下,程序结束时,这个线程必须正常的且立刻关闭,就可以作为守护线程来使用

22.ThreadLocal的原理和使用场景

同一个线程中通过ThreadLocal存进去的数据,在任何位置取出来是一致的

原理

每一个Thread对象均含有一个ThreadLocalMap类型的成员变量threadLocals,它存储本线程中所有ThreadLocal对象及对应的值。

当执行set()方法时,ThreadLocal首先会获取当前线程对象,然后获取当前线程的ThreadLocalMap对象。再以当前ThreadLocal对象为key,获取对应的value。

get方法的执行过程类似。ThreadLocal首先会获取当前线程对象,然后获取当前线程的ThreadLocalMap对象。再以当前ThreadLocal对象为key,获取对应的value。

由于每一条线程均含有各自私有的ThreadLocalMap容器,这些容器相互独立,互不影响,因此不会存在线程安全性问题,从而无需使用同步机制来保证多条线程访问容器的互斥性。

使用场景

  1. 在进行对象的跨层传递的时候,使用ThreadLocal可以避免多次传递,打破层次间的约束
  2. 线程间数据隔离
  3. 进行事务操作,用于存储事务信息
  4. 数据库连接,Session会话管理

23.ThreadLocal内存泄漏原因,怎么避免

内存泄漏:不会使用的对象或者变量占用的内存不能被回收,就是内存泄漏(内存泄漏为程序在申请内存后,无法释放已申请的内存空间,一次内存泄漏的危害可以忽略,但内存泄漏堆积后果很严重,无论多少内存,迟早会被占光,导致OOM。)

强引用:使用最普遍的引用(通过new),一个对象具有强应用,不会被垃圾回收器回收,即使是内存不足。我们想要取消强引用,可以显示的将引用赋值为null,jvm在合适的时间就会回收该对象。

ThreadLocal内存泄漏的根源

由于ThreadLocalMap的生命周期和Thread一样长,如果没有手动删除对应key就会导致内存泄漏

怎么避免

  1. 每次使用ThreadLocal都调用它的remove()方法清除数据
  2. 将ThreadLocal变量定义成private static,这样就一直村子ThreadLocal的强应用,也就能保证任何时候都能通过ThreadLocal的弱引用访问到Entry的value值,进而清除掉。

24.产生内存泄漏的原因有哪些

  1. 资源未关闭或释放导致内存泄露(io资源,数据库的连接)
  2. 使用 ThreadLocal 造成内存泄露
  3. 静态集合类引起内存泄漏,如HashMap、LinkedList等等。如果这些容器为静态的,那么它们的生命周期与程序一致,则容器中的对象在程序结束之前将不能被释放,从而造成内存泄漏。生命周期长的对象持有短生命周期对象的引用,尽管短生命周期的对象不再使用,但是因为长生命周期对象持有它的引用而导致不能被回收。
  4. 重写了 finalize() 的类,如果 finalize() 方法重写的不合理或 finalizer 队列无法跟上 Java 垃圾回收器的速度,那么迟早,应用程序会出现 OutOfMemoryError 异常

25.并发、并行、串行的区别

串行在时间上不可能发生重叠,前一个任务没有搞定,下一个任务只能等

并行在时间上是重叠的,两个任务在同一时刻互不干扰的同时执行

并发允许两个任务彼此干扰,同一时间点,只有一个任务运行,交替执行

26.并发的三大特性

  • 原子性:是指在在一个操作中,CPU不可以在中途暂停然后再调度,即不被中断操作,要不全部执行完成,要不都不执行。
  • 可见性:当多个线程访问同一个变量时,一个线程修改了这个变量的值,其他的线程能够立即看到修改的值。
  • 有序性:虚拟机再进行代码编译的时,对于那些改变顺序之后不会对最终的结果造成影响的代码,虚拟机不一定会按照我们写的代码的顺序来执行,有可能将它们重排序。

27.为什么用线程池?解释线程池参数?

为什么

  1. 降低资源的消耗;提高线程的利用率,降低创建和销毁现成的消耗
  2. 提高响应速度,任务来了,直接有线程可用执行,不是创建线程之后再执行
  3. 提高线程的可管理性;线程是稀缺资源,使用线程池可以统一分配调优监控

线程池参数

  • corePoolSize代表核心线程数,也就是正常情况下创建工作的线程数,这些线程创建之后不会消除,而是一种常驻线程。
  • maxnumPoolSize代表最大线程数,它与核心线程数相对应,表示最大允许被创建的线程数,比如当前任务较多,将核心线程数都用完了,还无法满足要求时,此时就会创建新的线程,但是线程池总数不会超过最大线程数
  • keepAliveTime空闲线程存活时间,当一个可被回收的线程的空闲时间大于keepAliveTime,就会被回收; unit:keepAliveTime的时间单位
  • workQueue工作队列,存放待执行任务的队列:当提交的任务数超过核心线程数大小后,再提交的任务就存放在工作队列,任务调度时再从队列中取出任务。它仅仅用来存放被execute()方法提交的Runnable任务。工作队列实现了BlockingQueue接口。
  • threadFactory线程工厂,创建线程的工厂,可以设定线程名、线程编号等。
  • handler拒绝策略,当线程池线程数已满,并且工作队列达到限制,新提交的任务使用拒绝策略处理。可以自定义拒绝策略,拒绝策略需实现RejectedExecutionHandler接口。

28.简述线程池的处理流程

image-20230905204135018

29.线程池中阻塞队列的作用?为什么是先添加队列而不是先创建最大线程?

线程池中阻塞队列的作用?

1、一般的队列只能保证作为一个有限长度的缓冲区,如果超出了缓冲长度,就无法保留当前的任务了,阻塞队列通过阻塞可以保留住当前想要继续入队的任务。

2、阻塞队列可以保证任务队列中没有任务时阻塞获取任务的线程,使得线程进入wait状态,释放cpu资源。

3、阻塞队列自带阻塞和唤醒的功能,不需要额外处理,无任务执行时,线程池利用阻塞队列的take方法挂起,从而维持核心线程的存活,不至于一直占着cpu资源。

为什么是先添加队列而不是先创建最大线程?

在创建新线程的时候。是要获取全局锁的,这个时候其它就得阻塞,影响了整体效率。

就好比一个企业里面有10个(core)正式工的名额,最多招10个正式工,要是任务超过正式工人数,(task > core)的情况下,工厂领导(线程池)不是首先扩招工人,还是这10个人,但是任务会稍微的积压一下,即先放到队列去(代价低),10个正式工慢慢干,迟早会干完的,要是任务还在继续增加,超过正式工的加班忍耐极限了(队列满了),就得招外包帮忙了(注意是临时工),要是正式工加上外包还是不能完成任务,那新来的任务就会被领导拒绝了(线程池的拒绝策略)。

30.线程池中线程复用原理

线程池将线程和任务解耦,线程是线程,任务是任务,摆脱了之前通过Thread创建线程时的一个线程必须对应一个任务的限制。

在线程池中,同一个线程可以从阻塞队列中不断获取新任务来执行,其核心原理在于线程池对Thread进行了封装,并不是每次执行都会调用Thread.start()来创建新线程,而是让每个线程去执行一个“循环任务”,在这个“循环任务”中不停检查是否有任务需要被执行,如果有则执行,也就是调用任务中的run方法,将run方法当成一个普通的方法执行,通过这种方式只使用固定的线程就将所有任务的run方法串联起来。

31.Spring是什么

轻量级的开源的j2EE框架。他是一个容器框架,用来装javabean(java对象),中间层框架(万能胶)可以起一个连接作用,比如说把struts和hibernate粘合在一起运用,可以让我们的企业开发更快、更简洁

Spring是一个轻量级的控制反转(ioc)和面向切面(aop)的容器框架

  • 从大小与开销两方面而言Spring都是轻量级的
  • 通过控制反转(ioc)的技术达到松耦合的目的
  • 提供了面向切面编程的丰富支持,允许通过分离应用的业务逻辑与系统级服务进行内聚的开发
  • 包含并管理应用对象(bean)的配置和生命周期,这个意义上是一个容器
  • 将简单的组件配置、组合成复杂的应用,这个意义上是一个框架

32.谈谈你对AOP的理解

系统是由许多不同的组件所组成的,每一个组件各负责一块特定的功能。除了实现自身的核心功能之外,这些组件还经常承担着额外的职责。例如日志、事务管理和安全这样的服务经常融入到自身具有核心业务逻辑的组件中去。这些系统服务经常被称为横切关注点,因为他们会跨域系统的多个组件。

当我们需要将分散的对象引入公共行为的时候,OOP则显得无能为力,也就是说,OOP允许你定义从上到下的关系,但并不适合定义从左到右的关系。例如日志功能

AOP:将程序中的交叉业务逻辑(比如安全、日志、事务),封装成一个切面。然后注入到目标对象(具体业务逻辑)中去。AOP可以对某个对象或者某些功能进行增强,比如对象中的方法进行增强,可以在执行某个方法之前额外做一些事情,在某个方法执行之后额外做一些事情。

33.谈谈你对IOC的理解

容器概念、控制反转、依赖注入三方面理解

容器概念

ioc容器:实际上就是个map(key、value),里面存的就是各种对象(在xml里配置bean节点、@Repository、@Service、@Controller),在项目启动的时候会读取配置文件路面的bean节点。

控制反转

在没有引入ioc容器之前,对象A依赖于对象B,那么对象A在初始化或者运行到某一点的时候,自己必须主动去创建对象B或者使用已经创建的对象B。无论是创建还是使用对象B,控制权都在自己手上。

引入ioc容器之后,对象A和对象B之间失去了直接联系。当对象A运行到需要对象B的时候,ioc容器会主动创建一个对象B注入到对象A需要的地方。

通过前后的对比,不难看出来:对象A获得依赖对象B的过程,由主动行为变成了被动行为,控制权颠倒过来了,这就是控制反转这个名称的由来。

依赖注入

“获得依赖对象的过程被反转了”。控制器反转之后,获得依赖对象的过程由自身管理变为了由IOC容器主动注入。依赖注入是实现IOC的方法,就是由IOC容器在运行期间,动态地将某种依赖关系注入到对象之中。

34.BeanFactory和ApplicationContext有什么区别?

ApplicationContext是BeanFactory的子接口

ApplicationContext提供了更完整的功能

1、继承了MessageSource,因此支持国际化

2、统一资源文件的访问方式

3、提供在监听器中注册bean的事件

4、同时加载多个配置文件

5、载入多个(有继承关系)上下文,使得每一个上下文都专注于一个特定的层次,比如应用的web层

35.深拷贝和浅拷贝

深拷贝和浅拷贝就是指对象的拷贝,一个对象中存在两种类型的属性,一种是基本数据类型,一种是实列对象的引用

  • 浅拷贝是指,只会拷贝基本数据类型的值,以及实例对象的引用地址,并不会复制一份引用地址所指向的对象,也就是浅拷贝出来的对象,内部的类属性指向的是一个对象
  • 深拷贝是指,既会拷贝基本数据类型的值,也会针对实例对象的引用地址所指向的对象进行复制,深拷贝出来的对象,内部的类执行指向的不是同一个对象

举例:A对象中有一个user属性,A1拷贝A对象,两个user指向同一个对象的话,就是浅拷贝,反之是深拷贝

36.TCP和UDP有什么区别?TCP为什么是三次握手,而不是两次?

TCP (Transfer Control Protocol)是一种面向连接的、可靠的、传输层通信协议

  • 面向连接的,点对点的通信,高可靠,效率比较低、占用系统的资源比较多(类似于打电话)

UDP (User Datagram Protocal) 是一种无连接、不可靠的、传输层通信协议

  • 不需要连接、发送方不管接收方有没有准备好,直接发消息;可以进行广播发送的;传输不可靠,有可能会丢失消息;效率比较高;协议比较简单,占用的系统资源少(类似于广播)

TCP为什么是三次握手,而不是两次?

如果是两次握手,可能会造成连接资源浪费的问题

37.JVM中有哪些垃圾回收算法?

MarkSweep标记清除算法

Copying拷贝算法

MarkCompack标记压缩算法

38.集合相关的面试题

image-20230926110554758

image-20230926122104134

image-20230926121325686

image-20230926121517150

image-20230926122420662

image-20230926122546930

image-20230926133043725

image-20230926133301807

二.框架篇相关面试题

1.Spring Bean的生命周期

1.创建前准备阶段

这个阶段主要是在开始Bean加载之前,从Spring上下文和相关配置中解析并查找Bean有关的配置内容,比如init-method-容器在初始化bean时调用的方法、destory-method,容器在销毁Bean时调用的方法。以及,BeanFactoryPostProcessor这类的bean加载过程中的前置和后置处理。这些类或者配置其实是Spring提供给开发者,用来实现Bean加载过程中的扩展机制,在很多和Spring集成的中间件经常使用,比如Dubbo。

2.创建实例阶段

这个阶段主要是通过反射来创建Bean的实例对象,并且扫描和解析Bean声明的一些属性。

3.依赖注入阶段

在这个阶段,会检测被实例化的Bean是否存在其他依赖,如果存在其他依赖,就需要对这些被依赖Bean进行注入。比如通过@Autowired、@Setter等依赖注入的配置。在这个阶段还会触发一些扩展的调用,比如常见的扩展类:BeanPostProcessors(用来实现Bean初始化前后的回调)、InitializingBean类(这个类有一个afterPropertiesSet()方法,给属性赋值)、还有BeanFactoryAware等等。

4.容器缓存阶段

容器缓存阶段主要是把Bean保存到IoC容器中缓存起来,到了这个阶段,Bean就可以被开发者使用了。这个阶段涉及到的操作,常见的有init-method这个属性配置的方法,会在这个阶段调用。比如BeanPostProcessors方法中的后置处理器方法postProcessAfterInitialization,也是在这个阶段触发的。

5.销毁实例阶段

这个阶段,是完成Spring应用上下文关闭时,将销毁Spring上下文中所有的Bean。如果Bean实现了DisposableBean接口,或者配置了destory-method属 性,将会在这个阶段被调用。

实例化 -> 属性赋值 -> 初始化 -> 销毁

2.Spring框架中的单例Bean是线程安全的吗?

不是线程安全的!

Sping中Bean默认是单例模式的,框架中并没有对Bean进行多线程的封装处理。

线程安全这个问题,要从单例与原型Bean分别进行说明。

「原型Bean」对于原型Bean,每次创建一个新对象,也就是线程之间并不存在Bean共享,自然是不会有线程安全的问题。

「单例Bean」对于单例Bean,所有线程都共享一个单例实例Bean,因此是存在资源的竞争。

如果单例Bean,是一个无状态Bean,也就是线程中的操作不会对Bean的成员执行「查询」以外的操作,那么这个单例Bean是线程安全的。比如Spring mvc 的 Controller、Service、Dao等,这些Bean大多是无状态的,只关注于方法本身。

3.Spring Bean作用域

Spring 的 bean 作用域(scope)类型有5种:

1、singleton:单例,默认作用域。

2、prototype:原型,每次创建一个新对象。

3、request:请求,每次Http请求创建一个新对象,适用于WebApplicationContext环境下。

4、session:会话,同一个会话共享一个实例,不同会话使用不用的实例。

5、global-session:全局会话,所有会话共享一个实例。

4.Spring框架中用到了哪些设计模式?

  1. 简单工厂:由一个工厂类根据传入的参数,动态决定应该创建哪一个产品类。Spring中的BeanFactory就是简单工厂模式的体现,根据传入的一个唯一的标识来获得Bean对象,但是否在传入参数后创建还是传入参数前创建这个要根据具体情况来定。
  2. 工厂方法:实现了FactoryBean接口的bean是一类叫做factory的bean.其特点是,spring会在使用getBean()调用获得该bean时,会自动调用该bean的getObject()方法,所以返回的不是factory这个bean,而是这个bean.getObject()方法的返回值。
  3. 单例模式:保证一个类仅有一个实例,并提供一个访问它的全局访问点。spring对单例的实现:spring中的单例模式完成了后半句话,即提供了全局访问点BeanFactory,但没有从构造器级别去控制单例,这时因为spring管理的是任意的java对象。
  4. 适配器模式:spring中定义了一个适配接口,使得每一种Controller有一种对应的适配器实现类,让适配器代替controller执行响应的方法。这样扩展controller时,只需要增加一个适配类就完成了springMvc的扩展了。
  5. 装饰器模式:动态地给一个对象增加一些额外的职责。就增加功能来说,Decorator模式相比生成子类更为灵活。spring中用到的装饰器模式在类名上有两种表现:一是类名中含有wrapper,另一种时类名中含有Decorator.
  6. 动态代理:切面在应用运行的时刻被织入。一般情况下,在织入切面时,AOP容器会为目标对象动态地创建一个代理对象。SpringAOP就是以这种方式织入切面的。 织入:把切面应用到目标对象并创建新代理对象的过程。
  7. 观察者模式:Spring的事件驱动模型使用的是观察者模式,spring中observer模式常用的地方是listener的实现。
  8. 策略模式:spring框架的资源访问Resource接口。该接口提供了更强的资源访问能力,spring框架本身大量使用了Resource接口来访问底层资源。

5.spring中事务实现方和原理式以及隔离级别?

实现方式

在使用Spring框架时,可以有两种使用事务的方式,一种是编程式,一种是申明式的,@Transactional注解就是申明式的。

首先,事务这个概念是数据库层面的,Spring只是基于数据库中的事务进行了扩展,以及提供了一些能让程序员更加方便操作事务的方式。比如我们可以通过在某个方法上增加@Transactional注解,就可以开启事务,这个方法中所有的sql都会在一个事务中执行,统一成功失败。

原理

在一个方法上加了@Transactional注解后,Spring会基于这个类生成一个代理对象,会将这个代理对象作为bean,当在使用这个代理对象的方法时,如果这个方法存在@Transactional注解,那么代理逻辑会先把事务的自动提价设置为false,然后再去执行原本的业务逻辑方法,如果执行业务逻辑方法没有异常,那么代理逻辑中就会将事务进行提交,如果执行业务逻辑方法出现了异常,那么则会将事务进行回滚。当然,针对哪些异常回滚事务是可以配置的,可以利用@Transactional注解中的rollbackFor属性进行配置,默认情况下会对RuntimeException和Error进行回滚。

隔离级别

spring的事务隔离级别就是数据库的隔离级别外加一个默认级别

  1. read uncommitted(未提交读)
  2. read committed(提交读、不可重复读)
  3. repeatable read (可重复读)
  4. serializable(可串行化)

注:数据库设置的隔离级别会被spring的配置覆盖

6.Spring事务传播机制

多个事务方法相互调用的时,事务是如何在这些方法间传播

方法A是一个事务的方法,方法A执行过程中调用了方法B,那么方法B有无事务以及方法B对事务的要求不同都会对方法A的事务具体执行造成影响,同时方法A的事务对方法B的事务执行也有影响,这种影响具体是什么就由两个方法所定义的事务传播类型所决定。

REQUIRED(Spring默认的事务传播类型)如果当前没有事务,则自己创建一个事务,如果当前存在事务,则加入这个事务

SUPPORTS:当前存在事务,则加入当前事务,如果当前没有事务,就以非事务方法执行

MANDATORY:当前存在事务,则加入当前事务,如果当前的事务不存在,则抛出异常

REQUIRES_NEW:创建一个新事务,如果存在当前事务,则挂起该事务

NOT_SUPPORTED:以非事务方式执行,如果当前存在事务,则挂起当前事务

NEVER:不使用事务,如果当前事务存在,则抛出异常

NESTED:如果当前事务存在,则在嵌套事务中执行,否则REQUIRED的操作一样(开启一个事务)

7.Spring事务什么时候会失效?

Spring事务原理是AOP,进行了切面增强,那么失效的根本原因是这个AOP不起作用了,常见情况有如下几种

  1. 发生自调用,类里面使用this调用本类的方法,此时这个this对象不是代理类,而是UserService对象本身
  2. 方法不是public的,非要在非public上使用事务,可以开启Aspectj代理模式
  3. 数据库不支持事务,例如使用的MyISAM存储引擎
  4. 没有被spring管理
  5. 异常被吃掉,事务不会回滚(或者抛出的异常没有定义,默认为RuntimeException)

8.什么是bean的自动装配,有哪些方式?

autowire属性有五种装配的方式

  • no – 缺省情况下,自动配置是通过“ref”属性手动设定
<!--手动装配:以value或ref的方式明确指定属性值都是手动装配。 需要通过‘ref’属性来连接bean-->
  • byName-根据bean的属性名称进行自动装配
<!--Cutomer的属性名称是person,Spring会将bean id为person的bean通过setter方法进行自动装配-->
< bean id=“cutomer” class=“com.xxx.xxx.Cutomer” autowire=“byName”/>
< bean id=“person” class=“com.xxx.xxx.Person”/>
  • byType-根据bean的类型进行自动装配
<!--Cutomer的属性person的类型为Person,Spirng会将Person类型通过setter方法进行自动装配-->
< bean> id=“cutomer” class=“com.xxx.xxx.Cutomer” autowire=“byType”/>
< bean> id=“person” class=“com.xxx.xxx.Person”/>
  • constructor-类似byType,不过是应用于构造器的参数。如果一个bean与构造器参数的类型形
    同,则进行自动装配,否则导致异常
<!--Cutomer构造函数的参数person的类型为Person,Spirng会将Person类型通过构造方法进行自动装配-->
< bean> id=“cutomer” class=“com.xxx.xxx.Cutomer” autowire=“construtor”/>
< bean> id=“person” class=“com.xxx.xxx.Person”/>
  • autodetect-如果有默认的构造器,则通过constructor方式进行自动装配,否则使用byType方式 进行自动装配
<!--如果有默认的构造器,则通过constructor方式进行自动装配,否则使用byType方式进行自动装配-->

9.Spring Boot、SpringMVC和Spring有什么区别?

Spring是一个IOC容器,用来管理Bean,使用依赖注入实现控制反转,可以很方便的整合各种框架,提供AOP机制弥补OOP的代码重复问题,更方便将不同方法中的共同处理抽取成切面,自动注入给方法执行,比如日志、异常等

SpringMvc是spring对web框架的一个解决方案,提供了一个总的前端控制器Servlet,用来接受请求,然后定义了一套路由策略(url到handle的映射)及适配执行handle,将handle结果使用视图解析技术生成视图展现给前端

SpringBoot是Spring提供的一个快速开发工具包,让程序员更方便、更快速的开发spring + springmvc应用,简化了配置(约定了默认配置),整合了一系列的解决方案(starter机制),redis、mongodb、es可以开箱即用

10.SpringMVC的工作流程

  1. 用户发送请求到前端控制器DispatcherServlet
  2. DispatcherServlet收到请求调用HandlerMapping处理器映射器
  3. 处理器映射器找到具体的处理器(可以根据xml配置、注解进行查找),生成处理器及处理器拦截器(如果有则生成)一并返回给DispatcherServlet
  4. DispatcherServlet调用HandlerAdapter处理器适配器
  5. HandlerAdapter调用具体的处理器(controller,也叫后端控制器)
  6. Controller执行完成返回ModelAndView
  7. HandlerAdapter将Controller执行结果ModelAndView返回给DispatcherServlet
  8. DispatcherServlet将ModelAndView传给ViewReslover视图解析器
  9. ViewReslover解析后返回具体view
  10. DispatcherServlet根据view进行渲染视图(即将模型数据填充到视图中)
  11. DispatcherServlet响应给用户

image-20230911141826287

11.SpringMvc中的九大组件

Handler是用来干活的工具;HandlerMapping用于根据需要干的活找到相应的工具;HandlerAdapter是使用工具干活的人

  1. HandlerMapping是用来查找Handler的。在SpringMVC中会有很多请求,每个请求都需要一个Handler处理,具体接收到一个请求之后使用哪个Handler进行处理呢?这就是HandlerMapping需要做的事。

  2. HandlerAdapter,从名字上看,它就是一个适配器。因为SpringMVC中的Handler可以是任意的形式,只要能处理请求就ok,但是Servlet需要的处理方法的结构却是固定的,都是以request和response为参数的方法。如何让固定的Servlet处理方法调用灵活的Handler来进行处理呢?这就是HandlerAdapter要做的事情。

  3. HandlerExceptionResolver其它组件都是用来干活的。在干活的过程中难免会出现问题,出问题后怎么办呢?这就需要有一个专门的角色对异常情况进行处理,在SpringMVC中就是HandlerExceptionResolver。具体来说,此组件的作用是根据异常设置ModelAndView,之后再交给render方法进行渲染。

  4. ViewResolverViewResolver用来将String类型的视图名和Locale解析为View类型的视图。View是用来渲染页面的,也就是将程序返回的参数填入模板里,生成html(也可能是其它类型)文件。这里就有两个关键问题:使用哪个模板?用什么技术(规则)填入参数?这其实是ViewResolver主要要做的工作,ViewResolver需要找到渲染所用的模板和所用的技术(也就是视图的类型)进行渲染,具体的渲染过程则交由不同的视图自己完成。

  5. RequestToViewNameTranslatorViewName是根据ViewName查找View,但有的Handler处理完后并没有设置View也没有设置ViewName,这时就需要从request获取ViewName了,如何从request中获取ViewName就是RequestToViewNameTranslator要做的事情了。RequestToViewNameTranslator在Spring MVC容器里只可以配置一个,所以所有request到ViewName的转换规则都要在一个Translator里面全部实现。

  6. LocaleResolver解析视图需要两个参数:一是视图名,另一个是Locale。视图名是处理器返回的,Locale是从哪里来的?这就是LocaleResolver要做的事情。LocaleResolver用于从request解析出Locale,Locale就是zh-cn之类,表示一个区域,有了这个就可以对不同区域的用户显示不同的结果。SpringMVC主要有两个地方用到了Locale:一是ViewResolver视图解析的时候;二是用到国际化资源或者主题的时候。

  7. ThemeResolver用于解析主题。SpringMVC中一个主题对应一个properties文件,里面存放着跟当前主题相关的所有资源、如图片、css样式等。SpringMVC的主题也支持国际化,同一个主题不同区域也可以显示不同的风格。SpringMVC中跟主题相关的类有 ThemeResolver、ThemeSource和Theme。主题是通过一系列资源来具体体现的,要得到一个主题的资源,首先要得到资源的名称,这是ThemeResolver的工作。然后通过主题名称找到对应的主题(可以理解为一个配置)文件,这是ThemeSource的工作。最后从主题中获取资源就可以了。

  8. MultipartResolver用于处理上传请求。处理方法是将普通的request包装成MultipartHttpServletRequest,后者可以直接调用getFile方法获取File,如果上传多个文件,还可以调用getFileMap得到FileName->File结构的Map。此组件中一共有三个方法,作用分别是判断是不是上传请求,将request包装成MultipartHttpServletRequest、处理完后清理上传过程中产生的临时资源。

  9. FlashMapManager用来管理FlashMap的,FlashMap主要用在redirect中传递参数。

12.SpringBoot自动配置原理(简答的阐述见下面15条)

自动装配,简单来说就是自动把第三方组件的Bean装载到Spring IOC器里面,不需要开发人员再去写Bean的装配配置。

在Spring Boot应用里面,只需要在启动类加上@SpringBootApplication注解就可以实现自动装配。@SpringBootApplication是一个复合注解,真正实现自动装配的注解是@EnableAutoConfiguration。

image-20230911195638524

13.MyBatis的优缺点

优点:

  1. 基于SQL语句编程,相当灵活,不会对应用程序或者数据库的现有设计造成任何影响,SQL写在XML里,解除sql与程序代码的耦合,便于统一管理;提供XML标签,支持编写动态SQL语句,并可重用
  2. 与JDBC相比,减少了50%以上的代码量,消除了JDBC大量冗余的代码,不需要手动开关连接
  3. 很好的与各种数据库兼容(因为MyBatis使用JDBC来连接数据库,所以只要JDBC支持的数据库MyBatis都支持)
  4. 能够与Spring很好的集成
  5. 提供映射标签,支持对象与数据库的ORM字段关系映射;提供对象关系映射标签,支持对象关系组件维护

缺点:

1.
SQL语句的编写工作量较大,尤其当字段多、关联表多时,对开发人员编写SQL语句的功底有一定要求
2. SQL语句依赖于数据库,导致数据库移植性差,不能随意更换数据库

14.MyBatis中#{}和${}的区别是什么?

  1. 功能不同:${} 是直接替换,而 #{} 是预处理
  2. 使用场景不同:普通参数使用 #{},如果传递的是 SQL 命令或 SQL 关键字,需要使用 ${},但在使用前一定要做好安全验证
  3. 安全性不同:使用 ${} 存在安全问题(SQL注入),而 #{} 则不存在安全问题

15.Spring Boot自动装配的过程

image-20230921145126572

整个自动装配的过程是:Spring Boot通过@EnableAutoConfiguration注解开启自动配置,加载spring.factories中注册的各AutoConfiguration类,当某个AutoConfiguration类满足其注解@Conditional指定的生效条件(Starters提供的依赖、配置或Spring容器中是否存在某个Bean等)时,实例化该AutoConfiguration类中定义的Bean(组件等),并注入Spring容器,就可以完成依赖框架的自动配置。

二.Mysql相关面试题

1.存储引擎InnoDB 与MyISAM 的区别

  1. 数据存储的方式不同,MyISAM中的数据和索引是分开存储的,而InnoDB是把索引和数据存储在同一个文件里面
  2. 对于事务的支持不同,MyISAM不支持事务,而InnoDB支持ACID特性的事务处理
  3. 对于锁的支持不同,MyISAM只支持表锁,而InnoDB可以根据不同的情况,支持行锁,表锁,间隙锁,临键锁
  4. MyISAM不支持外键,InnoDB支持外键因此基于这些特性,我们在实际应用中,可以根据不同的场景来选择合适的存储引擎
  5. 比如如果需要支持事务,那必须要选择InnoDB。如果大部分的表操作都是查询,可以选择MyISAM

2.索引的基本原理

原理:把无序的数据变成有序的查询

  1. 把创建了索引的列的内容进行排序
  2. 对排序结果生成倒排表
  3. 在倒排表内容上拼上数据地址链
  4. 在查询的时候,先拿到倒排表的内容,再取出数据地址链,从而拿到数据

3.索引失效的场景

  1. 索引在使用的时候没有遵循最左匹配法则.
  2. 模糊查询,如果%号在前面也会导致索引失效。
  3. 在添加索引的字段上进行了运算操作或者类型转换也都会导致索引失效。
  4. 如果使用了复合索引,中间使用了范围查询,右边的条件索引也会失效
  5. 查询的时候发生了类型转换,在查询的时候做了运算的操作和模糊查询也会导致索引失效

4.事务的隔离级别有哪些?MySQL 的默认隔离级别是什么?

读未提交(Read Uncommitted)可能读到其他事务未提交的数据,也叫做脏读
读已提交(Read Committed)两次读取结果不一致,叫做不可重复读
可重复读(Repeatable Read)是mysql默认的隔离级别,每次读取的结果都一样,当时可能产生幻读
串行化(Serializable)一般是不会使用的,他给每一行读取的数据加锁,会导致大量超时和锁竞争的问题
Mysql 默认的事务隔离级别是可重复读(Repeatable Read)

5.事务的基本特性

事务的基本特性ACID分别是:

  1. A 原子性:一个事务的操作要么全部成功,要么全部失败
  2. C 一致性:数据库从一个一致性的状态转换到另一个一致性的状态
  3. I 隔离性:一个事务的修改在最终提交前,对其他事务是不可见的
  4. D 持久性 一旦事务提交,所做的修改就会永远的保存到数据库中

6.怎么处理慢查询

SQL查询慢的原因

  1. 查询没有命中索引
  2. 查询了不需要的数据列
  3. 数据量太大

根据上面的原因给出优化的措施

  1. 分析语句的执行计划,然后获得其使用索引的情况,然后修改语句或者索引,使得语句可以尽可能的命中索引
  2. 分析语句,是否查询了不需要的数据列
  3. 如果对语句的优化已经无法进行,可以考虑表中的数据量是否太大,如果是的话可以进行横向或者纵向的分表

7.ACID靠什么保证的

A 原子性由undo log日志保证,它记录了需要回滚的日志信息,事务回滚时撤销已经执行成功的sql

C 一致性由其他的三大特性保证 程序代码要保证业务上的一致性

I 隔离性由MVCC来保证

D 持久性由内存 + redo log 来保证。Mysql修改数据同时在内存和redo log记录这次操作,宕机的时候可以从redo log恢复

三.Redis相关面试题

1.Redis过期键的删除策略

redis是key-value数据库,我们可以设置redis中缓存的key的过期时间。redis的过期策略就是指当redis中缓存的key过期了,redis该如何处理。

  • 惰性过期:只有当访问一个key时,才会判断该key是否已过期,过期则删除。该策略可以最大化地节省CU资源,但是对内存非常的不友好。极端地情况可能出现大量地过期key没有再次被访问,从而不被清除,占用大量内存。
  • **定期过期:**每隔一定地时间,会扫描一定数量地数据库地expires字典中一定数量地key,并清除其中已过期的key。该策略是前两者的一个折中方案。通过定时扫描的时间间隔和每次扫描的限定功耗,可以在不同情况下使得CPU和内存资源达到最优的平衡效果。

在Redis中同时使用了这两种策略

2.Redis的线程模型,单线程为什么快

IO多路复用机制监听多个Socket

单线程快的原因:

  1. 纯内存操作
  2. 核心是基于非阻塞的IO多路复用机制
  3. 单线程反而避免了多线程的频繁上下文切换带来的性能问题

3.缓存雪崩、缓存穿透、缓存击穿

缓存雪崩 缓存在同一时间大面积失效,所以,后面的请求都会落在数据库上,造成数据库短时间内承受大量请求而崩掉。

解决方案:

  1. 缓存数据的过期时间设置随机,防止同一时间大量数据过期现象发生
  2. 给每一个缓存数据增加响应的缓存标记,记录缓存是否失效,如果缓存标记失效,则更新数据缓存
  3. 缓存预热,启动系统之前先把热点数据放在缓存中去
  4. 互斥锁

缓存穿透 缓存和数据库中都没有数据,导致所有的请求都落在数据库上,造成数据库短时间内承受大量请求

解决方案:

  1. 接口层增加校验,如用户鉴权校验,id做基础校验,id<=0的直接拦截掉
  2. 从缓存取不到数据,在数据库中也没有取到,这时也可以将key-value对写为key-null,缓存有效时间可以设置短一些,如30秒(设置太长会导致正常的情况也没法使用)。这样可以防止攻击用户反复用同一个id暴力攻击
  3. 采用布隆过滤器,将所有可能存在的数据哈希到一个足够大的bitmap中,一个一定不存在的数据会被这个bitmap拦截掉,从而避免了对底层存储系统的查询压力

缓存击穿 缓存中没有但数据库中有的数据(一般是缓存时间到期),这时由于并发用户特别多,同时读缓存没有读到数据,又同时去数据库取数据,引起数据库压力瞬间增大,造成过大压力。和缓存雪崩不同的是,缓存击穿指并发查同一条数据,缓存雪崩是不同数据都过期了,很多数据都查不到从而查数据库。

解决方案:

  1. 设置热点数据永不过期
  2. 加互斥锁

4.Redis的数据结构

Redis支持五种数据类型:string(字符串),hash(哈希),list(列表),set(无序集合)及zset(有序集合)

image-20230914154911197

PDF

]]>
面试 面试
阿里云对象存储OSS /posts/6319.html 1.官网介绍

​ 阿里云对象存储OSS(Object Storage Service)是一款海量、安全、低成本、高可靠的云存储服务,可提供99.9999999999%(12个9)的数据持久性,99.995%的数据可用性。多种存储类型供选择,全面优化存储成本。OSS具有与平台无关的RESTful API接口,您可以在任何应用、任何时间、任何地点存储和访问任意类型的数据。您可以使用阿里云提供的API、SDK接口或者OSS迁移工具轻松地将海量数据移入或移出阿里云OSS。数据存储到阿里云OSS以后,您可以选择标准存储(Standard)作为移动应用、大型网站、图片分享或热点音视频的主要存储方式,也可以选择成本更低、存储期限更长的低频访问存储(Infrequent Access)、归档存储(Archive)、冷归档存储(Cold Archive)作为不经常访问数据的存储方式。

2.使用

2.1 使用的前置准备

(1)申请阿里云账号
(2)实名认证
(3)开通“对象存储OSS”服务
(4)进入管理控制台

2.2 创建Bucket

点击创建Bucket

image-20230422210404204

填写相关的信息

image-20230422211448747

2.3获取AccessKey保存备用

AccessKey拥有对阿里云提供服务(对象存储、短信服务、视频点播服务等等)的控制权,要妥善保管

image-20230422211940485

2.4 查看开发文档

刚开始不会使用的话,查看开发文档是入门的最好方式

image-20230422212508029

2.5 入门案例

创建一个SpringBoot工程

1.引入依赖

可以引入一个日期工具类joda-time 方便后面生成图片存储的路径

<dependency>
<groupId>com.aliyun.oss</groupId>
<artifactId>aliyun-sdk-oss</artifactId>
<version>3.15.1</version>
</dependency>

2.修改配置文件

入门案例中以下值会直接在代码中写死,先不从配置文件中获取

# 地域节点 bucket概览中查看
aliyun.oss.endpoint=oss-cn-beijing.aliyuncs.com
# accessKey
aliyun.oss.accessKeyId=LTAI4G4SV6WtST7UYH77XXXX
# secret
aliyun.oss.secret=X9KHNYgztNr9MI5Zp8JffXXXXXXXX
# bucket名
aliyun.oss.bucket=yygh-atguigu

3.创建读取配置文件的配置类

@Component
public class ConstantOssPropertiesUtils implements InitializingBean {

@Value("${aliyun.oss.endpoint}")
private String endpoint;

@Value("${aliyun.oss.accessKeyId}")
private String accessKeyId;

@Value("${aliyun.oss.secret}")
private String secret;

@Value("${aliyun.oss.bucket}")
private String bucket;

public static String EDNPOINT;
public static String ACCESS_KEY_ID;
public static String SECRECT;
public static String BUCKET;

@Override
public void afterPropertiesSet() throws Exception {
EDNPOINT=endpoint;
ACCESS_KEY_ID=accessKeyId;
SECRECT=secret;
BUCKET=bucket;
}
}

4.测试案例

以下测试案例由官方开发文档提供

1.通过代码创建Bucket

public class Demo {

public static void main(String[] args) throws Exception {
// Endpoint以华东1(杭州)为例,其它Region请按实际情况填写。
String endpoint = "https://oss-cn-hangzhou.aliyuncs.com";
// 阿里云账号AccessKey拥有所有API的访问权限,风险很高。强烈建议您创建并使用RAM用户进行API访问或日常运维,请登录RAM控制台创建RAM用户。
String accessKeyId = "yourAccessKeyId";
String accessKeySecret = "yourAccessKeySecret";
// 填写Bucket名称,例如examplebucket。
String bucketName = "examplebucket";

// 创建OSSClient实例。
OSS ossClient = new OSSClientBuilder().build(endpoint, accessKeyId, accessKeySecret);

try {
// 创建存储空间。
ossClient.createBucket(bucketName);

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

2.上传文件

以下代码用于通过流式上传的方式将文件上传到OSS。

import com.aliyun.oss.ClientException;
import com.aliyun.oss.OSS;
import com.aliyun.oss.OSSClientBuilder;
import com.aliyun.oss.OSSException;
import java.io.ByteArrayInputStream;

public class Demo {

public static void main(String[] args) throws Exception {
// Endpoint以华东1(杭州)为例,其它Region请按实际情况填写。
String endpoint = "https://oss-cn-hangzhou.aliyuncs.com";
// 阿里云账号AccessKey拥有所有API的访问权限,风险很高。强烈建议您创建并使用RAM用户进行API访问或日常运维,请登录RAM控制台创建RAM用户。
String accessKeyId = "yourAccessKeyId";
String accessKeySecret = "yourAccessKeySecret";
// 填写Bucket名称,例如examplebucket。
String bucketName = "examplebucket";
// 填写Object完整路径,例如exampledir/exampleobject.txt。Object完整路径中不能包含Bucket名称。
String objectName = "exampledir/exampleobject.txt";

// 创建OSSClient实例。
OSS ossClient = new OSSClientBuilder().build(endpoint, accessKeyId, accessKeySecret);

try {
String content = "Hello OSS";
ossClient.putObject(bucketName, objectName, new ByteArrayInputStream(content.getBytes()));
} 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();
}
}
}
}

3.下载文件

以下代码用于通过流式下载方式从OSS下载文件。

import com.aliyun.oss.ClientException;
import com.aliyun.oss.OSS;
import com.aliyun.oss.OSSClientBuilder;
import com.aliyun.oss.OSSException;
import com.aliyun.oss.model.OSSObject;
import java.io.BufferedReader;
import java.io.InputStream;
import java.io.InputStreamReader;

public class Demo {

public static void main(String[] args) throws Exception {
// Endpoint以华东1(杭州)为例,其它Region请按实际情况填写。
String endpoint = "https://oss-cn-hangzhou.aliyuncs.com";
// 阿里云账号AccessKey拥有所有API的访问权限,风险很高。强烈建议您创建并使用RAM用户进行API访问或日常运维,请登录RAM控制台创建RAM用户。
String accessKeyId = "yourAccessKeyId";
String accessKeySecret = "yourAccessKeySecret";
// 填写Bucket名称,例如examplebucket。
String bucketName = "examplebucket";
// 填写Object完整路径,例如exampledir/exampleobject.txt。Object完整路径中不能包含Bucket名称。
String objectName = "exampledir/exampleobject.txt";

// 创建OSSClient实例。
OSS ossClient = new OSSClientBuilder().build(endpoint, accessKeyId, accessKeySecret);

try {
// 调用ossClient.getObject返回一个OSSObject实例,该实例包含文件内容及文件元信息。
OSSObject ossObject = ossClient.getObject(bucketName, objectName);
// 调用ossObject.getObjectContent获取文件输入流,可读取此输入流获取其内容。
InputStream content = ossObject.getObjectContent();
if (content != null) {
BufferedReader reader = new BufferedReader(new InputStreamReader(content));
while (true) {
String line = reader.readLine();
if (line == null) break;
System.out.println("\n" + line);
}
// 数据读取完成后,获取的流必须关闭,否则会造成连接泄漏,导致请求无连接可用,程序无法正常工作。
content.close();
}
} 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();
}
}
}
}

4.列举文件

以下代码用于列举examplebucket存储空间下的文件。默认列举100个文件。

import com.aliyun.oss.ClientException;
import com.aliyun.oss.OSS;
import com.aliyun.oss.OSSClientBuilder;
import com.aliyun.oss.OSSException;
import com.aliyun.oss.model.OSSObjectSummary;
import com.aliyun.oss.model.ObjectListing;

public class Demo {

public static void main(String[] args) throws Exception {
// Endpoint以华东1(杭州)为例,其它Region请按实际情况填写。
String endpoint = "https://oss-cn-hangzhou.aliyuncs.com";
// 阿里云账号AccessKey拥有所有API的访问权限,风险很高。强烈建议您创建并使用RAM用户进行API访问或日常运维,请登录RAM控制台创建RAM用户。
String accessKeyId = "yourAccessKeyId";
String accessKeySecret = "yourAccessKeySecret";
// 填写Bucket名称,例如examplebucket。
String bucketName = "examplebucket";

// 创建OSSClient实例。
OSS ossClient = new OSSClientBuilder().build(endpoint, accessKeyId, accessKeySecret);

try {
// ossClient.listObjects返回ObjectListing实例,包含此次listObject请求的返回结果。
ObjectListing objectListing = ossClient.listObjects(bucketName);
// objectListing.getObjectSummaries获取所有文件的描述信息。
for (OSSObjectSummary objectSummary : objectListing.getObjectSummaries()) {
System.out.println(" - " + objectSummary.getKey() + " " +
"(size = " + objectSummary.getSize() + ")");
}
} 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();
}
}
}
}

5.删除文件

以下代码用于删除指定文件。

import com.aliyun.oss.ClientException;
import com.aliyun.oss.OSS;
import com.aliyun.oss.OSSClientBuilder;
import com.aliyun.oss.OSSException;

public class Demo {

public static void main(String[] args) throws Exception {
// Endpoint以华东1(杭州)为例,其它Region请按实际情况填写。
String endpoint = "https://oss-cn-hangzhou.aliyuncs.com";
// 阿里云账号AccessKey拥有所有API的访问权限,风险很高。强烈建议您创建并使用RAM用户进行API访问或日常运维,请登录RAM控制台创建RAM用户。
String accessKeyId = "yourAccessKeyId";
String accessKeySecret = "yourAccessKeySecret";
// 填写Bucket名称,例如examplebucket。
String bucketName = "examplebucket";
// 填写Object完整路径,例如exampledir/exampleobject.txt。Object完整路径中不能包含Bucket名称。
String objectName = "exampledir/exampleobject.txt";

// 创建OSSClient实例。
OSS ossClient = new OSSClientBuilder().build(endpoint, accessKeyId, accessKeySecret);

try {
// 删除文件。
ossClient.deleteObject(bucketName, objectName);
} 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();
}
}
}
}

5.项目中使用OSS对象存储

1.文件的上传操作

controller层

import com.atguigu.yygh.common.result.Result;
import com.atguigu.yygh.oss.service.OSSService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.multipart.MultipartFile;

/**
* @author GongChangjiang
* @version 1.0
* @Date 2023/4/22
* @Description 对象存储的控制器方法
*/
@RestController
@RequestMapping("/api/oss/file")
public class OSSController {

@Autowired
private OSSService ossService;

/**
* 上传文件到阿里云
*/
@PostMapping("/fileUpload")
public Result fileUpload(MultipartFile file){
//上传文件,返回文件的url地址
String url = ossService.upload(file);
return Result.ok(url);
}
}

service层

import com.aliyun.oss.ClientException;
import com.aliyun.oss.OSS;
import com.aliyun.oss.OSSClientBuilder;
import com.aliyun.oss.OSSException;
import com.atguigu.yygh.oss.service.OSSService;
import com.atguigu.yygh.oss.utils.ConstantOssPropertiesUtils;
import org.joda.time.DateTime;
import org.springframework.stereotype.Service;
import org.springframework.web.multipart.MultipartFile;

import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.util.UUID;

/**
* @author GongChangjiang
* @version 1.0
* @Date 2023/4/22
* @Description OSS文件相关操作
*/
@Service
public class OSSServiceImpl implements OSSService {
/**
* 上传文件
*
* @param file 文件
* @return 文件的地址
*/
@Override
public String upload(MultipartFile file) {
// Endpoint,地域节点信息
String endpoint = ConstantOssPropertiesUtils.EDNPOINT;
// 阿里云账号AccessKey拥有所有API的访问权限,风险很高。强烈建议您创建并使用RAM用户进行API访问或日常运维,请登录RAM控制台创建RAM用户。
String accessKeyId = ConstantOssPropertiesUtils.ACCESS_KEY_ID;
String accessKeySecret = ConstantOssPropertiesUtils.SECRECT;
// Bucket名称
String bucketName = ConstantOssPropertiesUtils.BUCKET;
// 创建OSSClient实例。
OSS ossClient = new OSSClientBuilder().build(endpoint, accessKeyId, accessKeySecret);
//生成文件名 生成文件名的策略是日期/文件名.jpg 文件名由UUID随机生成
//获取原始的文件名
String originalFilename = file.getOriginalFilename();
//分割出扩展名
String suffixFilename = originalFilename.substring(originalFilename.lastIndexOf("."));
//得到随机的文件名
String prefixFilename = UUID.randomUUID().toString();
//得到文件名
String filename = prefixFilename + suffixFilename;
//将文件按照日期进行分类 日期/文件名.jpg
//获取当前的日期 使用时间作为目录
String filePath = new DateTime().toString("yyyy/MM/dd");
//得到最终的文件名
filename = filePath + "/" + filename;
try {
//获取上传文件的流
InputStream inputStream = file.getInputStream();
//上传文件
ossClient.putObject(bucketName, filename, inputStream);
//拼接文件的url地址并返回
//https://edu-guli-0306.oss-cn-beijing.aliyuncs.com/81ea0d8d-0e52-4749-be58-6292410deec6.jpg
return "https://" + bucketName + "." + endpoint + "/" + filename;
} catch (IOException e) {
e.printStackTrace();
return null;
} finally {
if (ossClient != null) {
ossClient.shutdown();
}
}
}
}

3.Aliyun Spring Boot OSS

[使用文档](aliyun-spring-boot/README-zh.md at master · alibaba/aliyun-spring-boot · GitHub)

相比与上面的Demo,这个Demo更加的简便,编写的代码比较少

项目说明

​ 如果您的应用是 Spring Cloud 应用,且需要使用阿里云的 OSS 服务进行云端的文件存储,例如电商业务中常见的商品图片存储,那么您可以使用 OSS starter 完成 Spring Cloud 应用的对象存储。

阿里云对象存储服务(Object Storage Service,简称 OSS),是阿里云提供的海量、安全、低成本、高可靠的云存储服务。您可以在任何应用、任何时间、任何地点存储和访问任意类型的数据。更多 OSS 相关的信息,请参考 OSS官网

1.引入依赖

<!-- 阿里云的对象存储的依赖-->
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alicloud-oss</artifactId>
<version>2.1.0.RELEASE</version>
<type>pom</type>
</dependency>

2.在配置文件中增加配置

spring.cloud.alicloud.access-key=************
spring.cloud.alicloud.secret-key=************
spring.cloud.alicloud.oss.endpoint=************

3.测试使用

@Autowired
OSSClient ossClient;
/**
* 测试使用阿里云上传视频
*/
@Test
void testUpload() throws FileNotFoundException {
String bucketName = "gulimall-0611";
String objectName = "test1.jpg";
InputStream inputStream = new FileInputStream("C:\\Gong\\data\\test.jpg");
// 创建PutObjectRequest对象。
ossClient.putObject(bucketName,objectName,inputStream);
ossClient.shutdown();
}

4.在项目中使用OSS进行文件的上传和下载的操作

服务端签名直传 使用这种方式可以减轻本地服务器的压力

参考文档:https://help.aliyun.com/document_detail/91868.htm?spm=a2c4g.31927.0.0.44193d70tJDLJb#concept-ahk-rfz-2fb

image-20230611214209712

4.1 获取文件上传相关的签名信息

获取签名信息的接口

package com.atguigu.gulimall.thirdparty.controller;

import com.aliyun.oss.OSS;
import com.aliyun.oss.OSSClientBuilder;
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.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.LinkedHashMap;
import java.util.Map;

/**
* @author Jason Gong
* @version 1.0
* @Date 2023/6/11
* @Description 阿里云对象存储的控制器方法
*/

@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 accessKey;

@RequestMapping("/oss/policy")
public Map<String, String> 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+"/";

// 创建ossClient实例。
OSS ossClient = new OSSClientBuilder().build(endpoint, accessId, accessKey);
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) {
System.out.println(e.getMessage());
}
return respMap;
}
}

访问接口之后返回的数据格式

{
"accessId": "LTAI5tDDwk18w2S7w1KF24oT",//access-key
"policy": "eyJleHBpcmF0aW9uIjoiMjAyMy0wNi0xMVQxNDoxNzozMi43OTdaIiwiY29uZGl0aW9ucyI6W1siY29udGVudC1sZW5ndGgtcmFuZ2UiLDAsMTA0ODU3NjAwMF0sWyJzdGFydHMtd2l0aCIsIiRrZXkiLCIyMDIzLzA2LzExLyJdXX0=",//策略,加密之后的结果
"signature": "n67QffiYHceO2Hf7g8MDXgWsfpw=",//签名
"dir": "2023/06/11/",//上传到的文件的地址
"host": "https://gulimall-0611.oss-cn-beijing.aliyuncs.com",//上传到的主机的
"expire": "1686493052"//过期时间
}

前端相关的代码

前端相关的代码可以借鉴一下谷粒商城的前端上传文件的组件,里面封装了多文件上传和单文件上传的功能

image-20230612111805518

出现跨域的问题

image-20230612114314757

解决方法

image-20230612114238391

]]>
后端 云计算
面试专题 /posts/46317.html 参考资料:

java八股文面试全套真题+深度详解(含大厂高频面试真题) https://www.bilibili.com/video/BV1yT411H7YK/?share_source=copy_web&vd_source=aee5e475191b69e6c781059ab6662584

1.Redis篇

面试官:什么是缓存穿透 ? 怎么解决 ?

候选人

嗯~~,我想一下

缓存穿透是指查询一个一定不存在的数据,如果从存储层查不到数据则不写入缓存,这将导致这个不存在的数据每次请求都要到 DB 去查询,可能导致 DB 挂掉。这种情况大概率是遭到了攻击。

解决方案的话,我们通常都会用布隆过滤器来解决它

面试官:好的,你能介绍一下布隆过滤器吗?

候选人

嗯,是这样~

布隆过滤器主要是用于检索一个元素是否在一个集合中。我们当时使用的是redisson实现的布隆过滤器。

它的底层主要是先去初始化一个比较大数组,里面存放的二进制0或1。在一开始都是0,当一个key来了之后经过3次hash计算,模于数组长度找到数据的下标然后把数组中原来的0改为1,这样的话,三个数组的位置就能标明一个key的存在。查找的过程也是一样的。

当然是有缺点的,布隆过滤器有可能会产生一定的误判,我们一般可以设置这个误判率,大概不会超过5%,其实这个误判是必然存在的,要不就得增加数组的长度,其实已经算是很划分了,5%以内的误判率一般的项目也能接受,不至于高并发下压倒数据库。

面试官:什么是缓存击穿 ? 怎么解决 ?

候选人

嗯!!

缓存击穿的意思是对于设置了过期时间的key,缓存在某个时间点过期的时候,恰好这时间点对这个Key有大量的并发请求过来,这些请求发现缓存过期一般都会从后端 DB 加载数据并回设到缓存,这个时候大并发的请求可能会瞬间把 DB 压垮。

解决方案有两种方式:

第一可以使用互斥锁:当缓存失效时,不立即去load db,先使用如 Redis 的 setnx 去设置一个互斥锁,当操作成功返回时再进行 load db的操作并回设缓存,否则重试get缓存的方法

第二种方案可以设置当前key逻辑过期,大概是思路如下:

①:在设置key的时候,设置一个过期时间字段一块存入缓存中,不给当前key设置过期时间

②:当查询的时候,从redis取出数据后判断时间是否过期

③:如果过期则开通另外一个线程进行数据同步,当前线程正常返回数据,这个数据不是最新

当然两种方案各有利弊:===============

如果选择数据的强一致性,建议使用分布式锁的方案,性能上可能没那么高,锁需要等,也有可能产生死锁的问题

如果选择key的逻辑删除,则优先考虑的高可用性,性能比较高,但是数据同步这块做不到强一致。

面试官:什么是缓存雪崩 ? 怎么解决 ?

候选人

嗯!!

缓存雪崩意思是设置缓存时采用了相同的过期时间,导致缓存在某一时刻同时失效,请求全部转发到DB,DB 瞬时压力过重雪崩。与缓存击穿的区别:雪崩是很多key,击穿是某一个key缓存。

解决方案主要是可以将缓存失效时间分散开,比如可以在原有的失效时间基础上增加一个随机值,比如1-5分钟随机,这样每一个缓存的过期时间的重复率就会降低,就很难引发集体失效的事件。

面试官:redis做为缓存,mysql的数据如何与redis进行同步呢?(双写一致性)

候选人:嗯!就说我最近做的这个项目,里面有xxxx(根据自己的简历上写)的功能,需要让数据库与redis高度保持一致,因为要求时效性比较高,我们当时采用的读写锁保证的强一致性。

我们采用的是redisson实现的读写锁,在读的时候添加共享锁,可以保证读读不互斥,读写互斥。当我们更新数据的时候,添加排他锁,它是读写,读读都互斥,这样就能保证在写数据的同时是不会让其他线程读数据的,避免了脏数据。这里面需要注意的是读方法和写方法上需要使用同一把锁才行。

面试官:那这个排他锁是如何保证读写、读读互斥的呢?

候选人:其实排他锁底层使用也是setnx,保证了同时只能有一个线程操作锁住的方法

面试官:你听说过延时双删吗?为什么不用它呢?

候选人:延迟双删,如果是写操作,我们先把缓存中的数据删除,然后更新数据库,最后再延时删除缓存中的数据,其中这个延时多久不太好确定,在延时的过程中可能会出现脏数据,并不能保证强一致性,所以没有采用它。

面试官:redis做为缓存,mysql的数据如何与redis进行同步呢?(双写一致性)

候选人:嗯!就说我最近做的这个项目,里面有xxxx(根据自己的简历上写)的功能,数据同步可以有一定的延时(符合大部分业务)

我们当时采用的阿里的canal组件实现数据同步:不需要更改业务代码,部署一个canal服务。canal服务把自己伪装成mysql的一个从节点,当mysql数据更新以后,canal会读取binlog数据,然后在通过canal的客户端获取到数据,更新缓存即可。

面试官:redis做为缓存,数据的持久化是怎么做的?

候选人:在Redis中提供了两种数据持久化的方式:1、RDB 2、AOF

面试官:这两种持久化方式有什么区别呢?

候选人:RDB是一个快照文件,它是把redis内存存储的数据写到磁盘上,当redis实例宕机恢复数据的时候,方便从RDB的快照文件中恢复数据。

AOF的含义是追加文件,当redis操作写命令的时候,都会存储这个文件中,当redis实例宕机恢复数据的时候,会从这个文件中再次执行一遍命令来恢复数据

面试官:这两种方式,哪种恢复的比较快呢?

候选人:RDB因为是二进制文件,在保存的时候体积也是比较小的,它恢复的比较快,但是它有可能会丢数据,我们通常在项目中也会使用AOF来恢复数据,虽然AOF恢复的速度慢一些,但是它丢数据的风险要小很多,在AOF文件中可以设置刷盘策略,我们当时设置的就是每秒批量写入一次命令

面试官:Redis的数据过期策略有哪些 ?

候选人

嗯~,在redis中提供了两种数据过期删除策略

第一种是惰性删除,在设置该key过期时间后,我们不去管它,当需要该key时,我们在检查其是否过期,如果过期,我们就删掉它,反之返回该key。

第二种是 定期删除,就是说每隔一段时间,我们就对一些key进行检查,删除里面过期的key

定期清理的两种模式:

  • SLOW模式是定时任务,执行频率默认为10hz,每次不超过25ms,以通过修改配置文件redis.conf 的 hz 选项来调整这个次数
  • FAST模式执行频率不固定,每次事件循环会尝试执行,但两次间隔不低于2ms,每次耗时不超过1ms

Redis的过期删除策略:惰性删除 + 定期删除两种策略进行配合使用。

面试官:Redis的数据淘汰策略有哪些 ?

候选人

嗯,这个在redis中提供了很多种,默认是noeviction,不删除任何数据,内部不足直接报错

是可以在redis的配置文件中进行设置的,里面有两个非常重要的概念,一个是LRU,另外一个是LFU

LRU的意思就是最少最近使用,用当前时间减去最后一次访问时间,这个值越大则淘汰优先级越高。

LFU的意思是最少频率使用。会统计每个key的访问频率,值越小淘汰优先级越高

我们在项目设置的allkeys-lru,挑选最近最少使用的数据淘汰,把一些经常访问的key留在redis中

面试官:数据库有1000万数据 ,Redis只能缓存20w数据, 如何保证Redis中的数据都是热点数据 ?

候选人

嗯,我想一下~~

可以使用 allkeys-lru (挑选最近最少使用的数据淘汰)淘汰策略,那留下来的都是经常访问的热点数据

面试官:Redis的内存用完了会发生什么?

候选人

嗯~,这个要看redis的数据淘汰策略是什么,如果是默认的配置,redis内存用完以后则直接报错。我们当时设置的 allkeys-lru 策略。把最近最常访问的数据留在缓存中。

面试官:Redis分布式锁如何实现 ?

候选人:嗯,在redis中提供了一个命令setnx(SET if not exists)

由于redis的单线程的,用了命令之后,只能有一个客户端对某一个key设置值,在没有过期或删除key的时候是其他客户端是不能设置这个key的

面试官:好的,那你如何控制Redis实现分布式锁有效时长呢?

候选人:嗯,的确,redis的setnx指令不好控制这个问题,我们当时采用的redis的一个框架redisson实现的。

在redisson中需要手动加锁,并且可以控制锁的失效时间和等待时间,当锁住的一个业务还没有执行完成的时候,在redisson中引入了一个看门狗机制,就是说每隔一段时间就检查当前业务是否还持有锁,如果持有就增加加锁的持有时间,当业务执行完成之后需要使用释放锁就可以了

还有一个好处就是,在高并发下,一个业务有可能会执行很快,先客户1持有锁的时候,客户2来了以后并不会马上拒绝,它会自旋不断尝试获取锁,如果客户1释放之后,客户2就可以马上持有锁,性能也得到了提升。

面试官:好的,redisson实现的分布式锁是可重入的吗?

候选人:嗯,是可以重入的。这样做是为了避免死锁的产生。这个重入其实在内部就是判断是否是当前线程持有的锁,如果是当前线程持有的锁就会计数,如果释放锁就会在计算上减一。在存储数据的时候采用的hash结构,大key可以按照自己的业务进行定制,其中小key是当前线程的唯一标识,value是当前线程重入的次数

面试官:redisson实现的分布式锁能解决主从一致性的问题吗

候选人:这个是不能的,比如,当线程1加锁成功后,master节点数据会异步复制到slave节点,此时当前持有Redis锁的master节点宕机,slave节点被提升为新的master节点,假如现在来了一个线程2,再次加锁,会在新的master节点上加锁成功,这个时候就会出现两个节点同时持有一把锁的问题。

我们可以利用redisson提供的红锁来解决这个问题,它的主要作用是,不能只在一个redis实例上创建锁,应该是在多个redis实例上创建锁,并且要求在大多数redis节点上都成功创建锁,红锁中要求是redis的节点数量要过半。这样就能避免线程1加锁成功后master节点宕机导致线程2成功加锁到新的master节点上的问题了。

但是,如果使用了红锁,因为需要同时在多个节点上都添加锁,性能就变的很低了,并且运维维护成本也非常高,所以,我们一般在项目中也不会直接使用红锁,并且官方也暂时废弃了这个红锁

面试官:好的,如果业务非要保证数据的强一致性,这个该怎么解决呢?

候选人:嗯~,redis本身就是支持高可用的,做到强一致性,就非常影响性能,所以,如果有强一致性要求高的业务,建议使用zookeeper实现的分布式锁,它是可以保证强一致性的。

面试官:Redis集群有哪些方案, 知道嘛 ?

候选人:嗯~~,在Redis中提供的集群方案总共有三种:主从复制、哨兵模式、Redis分片集群

面试官:那你来介绍一下主从同步

候选人:嗯,是这样的,单节点Redis的并发能力是有上限的,要进一步提高Redis的并发能力,可以搭建主从集群,实现读写分离。一般都是一主多从,主节点负责写数据,从节点负责读数据,主节点写入数据之后,需要把数据同步到从节点中

面试官:能说一下,主从同步数据的流程

候选人:嗯~~,好!主从同步分为了两个阶段,一个是全量同步,一个是增量同步

全量同步是指从节点第一次与主节点建立连接的时候使用全量同步,流程是这样的:

第一:从节点请求主节点同步数据,其中从节点会携带自己的replication id和offset偏移量。

第二:主节点判断是否是第一次请求,主要判断的依据就是,主节点与从节点是否是同一个replication id,如果不是,就说明是第一次同步,那主节点就会把自己的replication id和offset发送给从节点,让从节点与主节点的信息保持一致。

第三:在同时主节点会执行bgsave,生成rdb文件后,发送给从节点去执行,从节点先把自己的数据清空,然后执行主节点发送过来的rdb文件,这样就保持了一致

当然,如果在rdb生成执行期间,依然有请求到了主节点,而主节点会以命令的方式记录到缓冲区,缓冲区是一个日志文件,最后把这个日志文件发送给从节点,这样就能保证主节点与从节点完全一致了,后期再同步数据的时候,都是依赖于这个日志文件,这个就是全量同步

增量同步指的是,当从节点服务重启之后,数据就不一致了,所以这个时候,从节点会请求主节点同步数据,主节点还是判断不是第一次请求,不是第一次就获取从节点的offset值,然后主节点从命令日志中获取offset值之后的数据,发送给从节点进行数据同步

面试官:怎么保证Redis的高并发高可用

候选人:首先可以搭建主从集群,再加上使用redis中的哨兵模式,哨兵模式可以实现主从集群的自动故障恢复,里面就包含了对主从服务的监控、自动故障恢复、通知;如果master故障,Sentinel会将一个slave提升为master。当故障实例恢复后也以新的master为主;同时Sentinel也充当Redis客户端的服务发现来源,当集群发生故障转移时,会将最新信息推送给Redis的客户端,所以一般项目都会采用哨兵的模式来保证redis的高并发高可用

面试官:你们使用redis是单点还是集群,哪种集群

候选人:嗯!,我们当时使用的是主从(1主1从)加哨兵。一般单节点不超过10G内存,如果Redis内存不足则可以给不同服务分配独立的Redis主从节点。尽量不做分片集群。因为集群维护起来比较麻烦,并且集群之间的心跳检测和数据通信会消耗大量的网络带宽,也没有办法使用lua脚本和事务

面试官:redis集群脑裂,该怎么解决呢?

候选人:嗯! 这个在项目很少见,不过脑裂的问题是这样的,我们现在用的是redis的哨兵模式集群的

有的时候由于网络等原因可能会出现脑裂的情况,就是说,由于redis master节点和redis salve节点和sentinel处于不同的网络分区,使得sentinel没有能够心跳感知到master,所以通过选举的方式提升了一个salve为master,这样就存在了两个master,就像大脑分裂了一样,这样会导致客户端还在old master那里写入数据,新节点无法同步数据,当网络恢复后,sentinel会将old master降为salve,这时再从新master同步数据,这会导致old master中的大量数据丢失。

关于解决的话,我记得在redis的配置中可以设置:第一可以设置最少的salve节点个数,比如设置至少要有一个从节点才能同步数据,第二个可以设置主从数据复制和同步的延迟时间,达不到要求就拒绝请求,就可以避免大量的数据丢失

面试官:redis的分片集群有什么作用

候选人:分片集群主要解决的是,海量数据存储的问题,集群中有多个master,每个master保存不同数据,并且还可以给每个master设置多个slave节点,就可以继续增大集群的高并发能力。同时每个master之间通过ping监测彼此健康状态,就类似于哨兵模式了。当客户端请求可以访问集群任意节点,最终都会被转发到正确节点

面试官:Redis分片集群中数据是怎么存储和读取的?

候选人

嗯~,在redis集群中是这样的

Redis 集群引入了哈希槽的概念,有 16384 个哈希槽,集群中每个主节点绑定了一定范围的哈希槽范围, key通过 CRC16 校验后对 16384 取模来决定放置哪个槽,通过槽找到对应的节点进行存储。

取值的逻辑是一样的

面试官:Redis是单线程的,但是为什么还那么快?

候选人

嗯,这个有几个原因吧~~~

1、完全基于内存的,C语言编写

2、采用单线程,避免不必要的上下文切换可竞争条件

3、使用多路I/O复用模型,非阻塞IO

例如:bgsave 和 bgrewriteaof 都是在后台执行操作,不影响主线程的正常使用,不会产生阻塞

面试官:能解释一下I/O多路复用模型?

候选人:嗯~~,I/O多路复用是指利用单个线程来同时监听多个Socket ,并在某个Socket可读、可写时得到通知,从而避免无效的等待,充分利用CPU资源。目前的I/O多路复用都是采用的epoll模式实现,它会在通知用户进程Socket就绪的同时,把已就绪的Socket写入用户空间,不需要挨个遍历Socket来判断是否就绪,提升了性能。

其中Redis的网络模型就是使用I/O多路复用结合事件的处理器来应对多个Socket请求,比如,提供了连接应答处理器、命令回复处理器,命令请求处理器;

在Redis6.0之后,为了提升更好的性能,在命令回复处理器使用了多线程来处理回复事件,在命令请求处理器中,将命令的转换使用了多线程,增加命令转换速度,在命令执行的时候,依然是单线程

2.数据库篇

面试官:MySQL中,如何定位慢查询?

候选人:

嗯~,我们当时做压测的时候有的接口非常的慢,接口的响应时间超过了2秒以上,因为我们当时的系统部署了运维的监控系统Skywalking ,在展示的报表中可以看到是哪一个接口比较慢,并且可以分析这个接口哪部分比较慢,这里可以看到SQL的具体的执行时间,所以可以定位是哪个sql出了问题

如果,项目中没有这种运维的监控系统,其实在MySQL中也提供了慢日志查询的功能,可以在MySQL的系统配置文件中开启这个慢日志的功能,并且也可以设置SQL执行超过多少时间来记录到一个日志文件中,我记得上一个项目配置的是2秒,只要SQL执行的时间超过了2秒就会记录到日志文件中,我们就可以在日志文件找到执行比较慢的SQL了。

面试官:那这个SQL语句执行很慢, 如何分析呢?

候选人:如果一条sql执行很慢的话,我们通常会使用mysql自动的执行计划explain来去查看这条sql的执行情况,比如在这里面可以通过key和key_len检查是否命中了索引,如果本身已经添加了索引,也可以判断索引是否有失效的情况,第二个,可以通过type字段查看sql是否有进一步的优化空间,是否存在全索引扫描或全盘扫描,第三个可以通过extra建议来判断,是否出现了回表的情况,如果出现了,可以尝试添加索引或修改返回字段来修复

面试官:了解过索引吗?(什么是索引)

候选人:嗯,索引在项目中还是比较常见的,它是帮助MySQL高效获取数据的数据结构,主要是用来提高数据检索的效率,降低数据库的IO成本,同时通过索引列对数据进行排序,降低数据排序的成本,也能降低了CPU的消耗

面试官:索引的底层数据结构了解过嘛 ?

候选人:MySQL的默认的存储引擎InnoDB采用的B+树的数据结构来存储索引,选择B+树的主要的原因是:第一阶数更多,路径更短,第二个磁盘读写代价B+树更低,非叶子节点只存储指针,叶子阶段存储数据,第三是B+树便于扫库和区间查询,叶子节点是一个双向链表

面试官:B树和B+树的区别是什么呢?

候选人:第一:在B树中,非叶子节点和叶子节点都会存放数据,而B+树的所有的数据都会出现在叶子节点,在查询的时候,B+树查找效率更加稳定

第二:在进行范围查询的时候,B+树效率更高,因为B+树都在叶子节点存储,并且叶子节点是一个双向链表

面试官:什么是聚簇索引什么是非聚簇索引 ?

候选人:

好的~,聚簇索引主要是指数据与索引放到一块,B+树的叶子节点保存了整行数据,有且只有一个,一般情况下主键在作为聚簇索引的

非聚簇索引值的是数据与索引分开存储,B+树的叶子节点保存对应的主键,可以有多个,一般我们自己定义的索引都是非聚簇索引

面试官:知道什么是回表查询嘛 ?

候选人:嗯,其实跟刚才介绍的聚簇索引和非聚簇索引是有关系的,回表的意思就是通过二级索引找到对应的主键值,然后再通过主键值找到聚集索引中所对应的整行数据,这个过程就是回表

备注:如果面试官直接问回表,则需要先介绍聚簇索引和非聚簇索引】

面试官:知道什么叫覆盖索引嘛 ?

候选人:嗯~,清楚的

覆盖索引是指select查询语句使用了索引,在返回的列,必须在索引中全部能够找到,如果我们使用id查询,它会直接走聚集索引查询,一次索引扫描,直接返回数据,性能高。

如果按照二级索引查询数据的时候,返回的列中没有创建索引,有可能会触发回表查询,尽量避免使用select *,尽量在返回的列中都包含添加索引的字段

面试官:MYSQL超大分页怎么处理 ?

候选人:嗯,超大分页一般都是在数据量比较大时,我们使用了limit分页查询,并且需要对数据进行排序,这个时候效率就很低,我们可以采用覆盖索引和子查询来解决

先分页查询数据的id字段,确定了id之后,再用子查询来过滤,只查询这个id列表中的数据就可以了

因为查询id的时候,走的覆盖索引,所以效率可以提升很多

面试官:索引创建原则有哪些?

候选人:嗯,这个情况有很多,不过都有一个大前提,就是表中的数据要超过10万以上,我们才会创建索引,并且添加索引的字段是查询比较频繁的字段,一般也是像作为查询条件,排序字段或分组的字段这些。

还有就是,我们通常创建索引的时候都是使用复合索引来创建,一条sql的返回值,尽量使用覆盖索引,如果字段的区分度不高的话,我们也会把它放在组合索引后面的字段。

如果某一个字段的内容较长,我们会考虑使用前缀索引来使用,当然并不是所有的字段都要添加索引,这个索引的数量也要控制,因为添加索引也会导致新增改的速度变慢。

面试官:什么情况下索引会失效 ?

候选人:嗯,这个情况比较多,我说一些自己的经验,以前遇到过的

比如,索引在使用的时候没有遵循最左匹配法则,第二个是,模糊查询,如果%号在前面也会导致索引失效。如果在添加索引的字段上进行了运算操作或者类型转换也都会导致索引失效。

我们之前还遇到过一个就是,如果使用了复合索引,中间使用了范围查询,右边的条件索引也会失效

查询的时候发生了类型转换,在查询的时候做了运算的操作和模糊查询也会导致索引失效

所以,通常情况下,想要判断出这条sql是否有索引失效的情况,可以使用explain执行计划来分析

面试官:sql的优化的经验

候选人:嗯,这个在项目还是挺常见的,当然如果直说sql优化的话,我们会从这几方面考虑,比如

建表的时候、使用索引、sql语句的编写、主从复制,读写分离,还有一个是如果量比较大的话,可以考虑分库分表

直接经验:

①SELECT语句务必指明字段名称(避免直接使用select * )

②SQL语句要避免造成索引失效的写法

③尽量用union all代替union union会多一次过滤,效率低

④避免在where子句中对字段进行表达式操作

⑤Join优化 能用innerjoin 就不用left join right join,如必须使用 一定要以小表为驱动,内连接会对两个表进行优化,优先把小表放到外边,把大表放到里边。left join 或 right join,不会重新调整顺序

面试官:创建表的时候,你们是如何优化的呢?

候选人:这个我们主要参考的阿里出的那个开发手册《嵩山版》,就比如,在定义字段的时候需要结合字段的内容来选择合适的类型,如果是数值的话,像tinyint、int 、bigint这些类型,要根据实际情况选择。如果是字符串类型,也是结合存储的内容来选择char和varchar或者text类型

面试官:那在使用索引的时候,是如何优化呢?

候选人:【参考索引创建原则 进行描述】

1). 针对于数据量较大,且查询比较频繁的表建立索引。

2). 针对于常作为查询条件(where)、排序(order by)、分组(group by)操作的字段建立索引。

3). 尽量选择区分度高的列作为索引,尽量建立唯一索引,区分度越高,使用索引的效率越高。

4). 如果是字符串类型的字段,字段的长度较长,可以针对于字段的特点,建立前缀索引。

5). 尽量使用联合索引,减少单列索引,查询时,联合索引很多时候可以覆盖索引,节省存储空间,避免回表,提高查询效率。

6). 要控制索引的数量,索引并不是多多益善,索引越多,维护索引结构的代价也就越大,会影响增删改的效率。

7). 如果索引列不能存储NULL值,请在创建表时使用NOT NULL约束它。当优化器知道每列是否包含NULL值时,它可以更好地确定哪个索引最有效地用于查询。

面试官:你平时对sql语句做了哪些优化呢?

候选人:嗯,这个也有很多,比如SELECT语句务必指明字段名称,不要直接使用select * ,还有就是要注意SQL语句避免造成索引失效的写法;如果是聚合查询,尽量用union all代替union ,union会多一次过滤,效率比较低;如果是表关联的话,尽量使用innerjoin ,不要使用用left join right join,如必须使用 一定要以小表为驱动

面试官:事务的特性是什么?可以详细说一下吗?

候选人:嗯,这个比较清楚,ACID,分别指的是:原子性、一致性、隔离性、持久性;我举个例子:

A向B转账500,转账成功,A扣除500元,B增加500元,原子操作体现在要么都成功,要么都失败

在转账的过程中,数据要一致,A扣除了500,B必须增加500

在转账的过程中,隔离性体现在A像B转账,不能受其他事务干扰

在转账的过程中,持久性体现在事务提交后,要把数据持久化(可以说是落盘操作)

ACID介绍:

原子性(Atomicity):事务是不可分割的最小操作单元,要么全部成功,要么全部失败。

一致性(Consistency):事务完成时,必须使所有的数据都保持一致状态。

隔离性(Isolation):数据库系统提供的隔离机制,保证事务在不受外部并发操作影响的独立环境下运行。

持久性(Durability):事务一旦提交或回滚,它对数据库中的数据的改变就是永久的。

面试官:并发事务带来哪些问题?

候选人

我们在项目开发中,多个事务并发进行是经常发生的,并发也是必然的,有可能导致一些问题

第一是脏读, 当一个事务正在访问数据并且对数据进行了修改,而这种修改还没有提交到数据库中,这时另外一个事务也访问了这个数据,因为这个数据是还没有提交的数据,那么另外一个事务读到的这个数据是“脏数据”,依据“脏数据”所做的操作可能是不正确的。

第二是不可重复读:比如在一个事务内多次读同一数据。在这个事务还没有结束时,另一个事务也访问该数据。那么,在第一个事务中的两次读数据之间,由于第二个事务的修改导致第一个事务两次读取的数据可能不太一样。这就发生了在一个事务内两次读到的数据是不一样的情况,因此称为不可重复读。

第三是幻读(Phantom read):幻读与不可重复读类似。它发生在一个事务(T1)读取了几行数据,接着另一个并发事务(T2)插入了一些数据时。在随后的查询中,第一个事务(T1)就会发现多了一些原本不存在的记录,就好像发生了幻觉一样,所以称为幻读。

image-20230511172226172

面试官:怎么解决这些问题呢?MySQL的默认隔离级别是?

候选人:解决方案是对事务进行隔离

MySQL支持四种隔离级别,分别有:

第一个是,未提交读(read uncommitted)它解决不了刚才提出的所有问题,一般项目中也不用这个。第二个是读已提交(read committed)它能解决脏读的问题的,但是解决不了不可重复读和幻读。第三个是可重复读(repeatable read)它能解决脏读和不可重复读,但是解决不了幻读,这个也是mysql默认的隔离级别。第四个是串行化(serializable)它可以解决刚才提出来的所有问题,但是由于让是事务串行执行的,性能比较低。所以,我们一般使用的都是mysql默认的隔离级别:可重复读

image-20230511172259204

面试官:undo log和redo log的区别

候选人:好的,其中redo log日志记录的是数据页的物理变化,服务宕机可用来同步数据,而undo log 不同,它主要记录的是逻辑日志,当事务回滚时,通过逆操作恢复原来的数据,比如我们删除一条数据的时候,就会在undo log日志文件中新增一条delete语句,如果发生回滚就执行逆操作;

redo log保证了事务的持久性,undo log保证了事务的原子性和一致性

面试官:事务中的隔离性是如何保证的呢?(你解释一下MVCC)

候选人:事务的隔离性是由锁和mvcc实现的。

其中mvcc的意思是多版本并发控制。指维护一个数据的多个版本,使得读写操作没有冲突,它的底层实现主要是分为了三个部分,第一个是隐藏字段,第二个是undo log日志,第三个是readView读视图

隐藏字段是指:在mysql中给每个表都设置了隐藏字段,有一个是trx_id(事务id),记录每一次操作的事务id,是自增的;另一个字段是roll_pointer(回滚指针),指向上一个版本的事务版本记录地址

undo log主要的作用是记录回滚日志,存储老版本数据,在内部会形成一个版本链,在多个事务并行操作某一行记录,记录不同事务修改数据的版本,通过roll_pointer指针形成一个链表

readView解决的是一个事务查询选择版本的问题,在内部定义了一些匹配规则和当前的一些事务id判断该访问那个版本的数据,不同的隔离级别快照读是不一样的,最终的访问的结果不一样。如果是rc隔离级别,每一次执行快照读时生成ReadView,如果是rr隔离级别仅在事务中第一次执行快照读时生成ReadView,后续复用

面试官:MySQL主从同步原理

候选人:MySQL主从复制的核心就是二进制日志(DDL(数据定义语言)语句和 DML(数据操纵语言)语句),它的步骤是这样的:

第一:主库在事务提交时,会把数据变更记录在二进制日志文件 Binlog 中。

第二:从库读取主库的二进制日志文件 Binlog ,写入到从库的中继日志 Relay Log 。

第三:从库重做中继日志中的事件,将改变反映它自己的数据

面试官:你们项目用过MySQL的分库分表吗?

候选人

嗯,因为我们都是微服务开发,每个微服务对应了一个数据库,是根据业务进行拆分的,这个其实就是垂直拆分。

面试官:那你之前使用过水平分库吗?

候选人

嗯,这个是使用过的,我们当时的业务是(xxx),一开始,我们也是单库,后来这个业务逐渐发展,业务量上来的很迅速,其中(xx)表已经存放了超过1000万的数据,我们做了很多优化也不好使,性能依然很慢,所以当时就使用了水平分库。

我们一开始先做了3台服务器对应了3个数据库,由于库多了,需要分片,我们当时采用的mycat来作为数据库的中间件。数据都是按照id(自增)取模的方式来存取的。

当然一开始的时候,那些旧数据,我们做了一些清洗的工作,我们也是按照id取模规则分别存储到了各个数据库中,好处就是可以让各个数据库分摊存储和读取的压力,解决了我们当时性能的问题

3.框架篇

面试官:Spring框架中的单例bean是线程安全的吗?

候选人

嗯!

不是线程安全的,是这样的

当多用户同时请求一个服务时,容器会给每一个请求分配一个线程,这是多个线程会并发执行该请求对应的业务逻辑(成员方法),如果该处理逻辑中有对该单列状态的修改(体现为该单例的成员属性),则必须考虑线程同步问题。

Spring框架并没有对单例bean进行任何多线程的封装处理。关于单例bean的线程安全和并发问题需要开发者自行去搞定。

比如:我们通常在项目中使用的Spring bean都是不可可变的状态(比如Service类和DAO类),所以在某种程度上说Spring的单例bean是线程安全的。

如果你的bean有多种状态的话(比如 View Model对象),就需要自行保证线程安全。最浅显的解决办法就是将多态bean的作用由“singleton”变更为“prototype”。

面试官:什么是AOP

候选人

aop是面向切面编程,在spring中用于将那些与业务无关,但却对多个对象产生影响的公共行为和逻辑,抽取公共模块复用,降低耦合,一般比如可以做为公共日志保存,事务处理等

面试官:你们项目中有没有使用到AOP

候选人

我们当时在后台管理系统中,就是使用aop来记录了系统的操作日志

主要思路是这样的,使用aop中的环绕通知+切点表达式,这个表达式就是要找到要记录日志的方法,然后通过环绕通知的参数获取请求方法的参数,比如类信息、方法信息、注解、请求方式等,获取到这些参数以后,保存到数据库

面试官:Spring中的事务是如何实现的

候选人

spring实现的事务本质就是aop完成,对方法前后进行拦截,在执行方法之前开启事务,在执行完目标方法之后根据执行情况提交或者回滚事务。

面试官:Spring中事务失效的场景有哪些

候选人

嗯!这个在项目中之前遇到过,我想想啊

第一个,如果方法上异常捕获处理,自己处理了异常,没有抛出,就会导致事务失效,所以一般处理了异常以后,别忘了跑出去就行了

第二个,如果方法抛出检查异常,如果报错也会导致事务失效,最后在spring事务的注解上,就是@Transactional上配置rollbackFor属性为Exception,这样别管是什么异常,都会回滚事务

第三,我之前还遇到过一个,如果方法上不是public修饰的,也会导致事务失效

嗯,就能想起来那么多

面试官:Spring的bean的生命周期

候选人

嗯!,这个步骤还是挺多的,我之前看过一些源码,它大概流程是这样的

首先会通过一个非常重要的类,叫做BeanDefinition获取bean的定义信息,这里面就封装了bean的所有信息,比如,类的全路径,是否是延迟加载,是否是单例等等这些信息

在创建bean的时候,第一步是调用构造函数实例化bean

第二步是bean的依赖注入,比如一些set方法注入,像平时开发用的@Autowire都是这一步完成

第三步是处理Aware接口,如果某一个bean实现了Aware接口就会重写方法执行

第四步是bean的后置处理器BeanPostProcessor,这个是前置处理器

第五步是初始化方法,比如实现了接口InitializingBean或者自定义了方法init-method标签或@PostContruct

第六步是执行了bean的后置处理器BeanPostProcessor,主要是对bean进行增强,有可能在这里产生代理对象

最后一步是销毁bean

面试官:Spring中的循环引用

候选人

嗯,好的,我来解释一下

循环依赖:循环依赖其实就是循环引用,也就是两个或两个以上的bean互相持有对方,最终形成闭环。比如A依赖于B,B依赖于A

循环依赖在spring中是允许存在,spring框架依据三级缓存已经解决了大部分的循环依赖

①一级缓存:单例池,缓存已经经历了完整的生命周期,已经初始化完成的bean对象

②二级缓存:缓存早期的bean对象(生命周期还没走完)

③三级缓存:缓存的是ObjectFactory,表示对象工厂,用来创建某个对象的

面试官:那具体解决流程清楚吗?

候选人

第一,先实例A对象,同时会创建ObjectFactory对象存入三级缓存singletonFactories

第二,A在初始化的时候需要B对象,这个走B的创建的逻辑

第三,B实例化完成,也会创建ObjectFactory对象存入三级缓存singletonFactories

第四,B需要注入A,通过三级缓存中获取ObjectFactory来生成一个A的对象同时存入二级缓存,这个是有两种情况,一个是可能是A的普通对象,另外一个是A的代理对象,都可以让ObjectFactory来生产对应的对象,这也是三级缓存的关键

第五,B通过从通过二级缓存earlySingletonObjects 获得到A的对象后可以正常注入,B创建成功,存入一级缓存singletonObjects

第六,回到A对象初始化,因为B对象已经创建完成,则可以直接注入B,A创建成功存入一次缓存singletonObjects

第七,二级缓存中的临时对象A清除

面试官:构造方法出现了循环依赖怎么解决?

候选人

由于bean的生命周期中构造函数是第一个执行的,spring框架并不能解决构造函数的的依赖注入,可以使用@Lazy懒加载,什么时候需要对象再进行bean对象的创建

面试官:SpringMVC的执行流程知道嘛

候选人

嗯,这个知道的,它分了好多步骤

1、用户发送出请求到前端控制器DispatcherServlet,这是一个调度中心

2、DispatcherServlet收到请求调用HandlerMapping(处理器映射器)。

3、HandlerMapping找到具体的处理器(可查找xml配置或注解配置),生成处理器对象及处理器拦截器(如果有),再一起返回给DispatcherServlet。

4、DispatcherServlet调用HandlerAdapter(处理器适配器)。

5、HandlerAdapter经过适配调用具体的处理器(Handler/Controller)。

6、Controller执行完成返回ModelAndView对象。

7、HandlerAdapter将Controller执行结果ModelAndView返回给DispatcherServlet。

8、DispatcherServlet将ModelAndView传给ViewReslover(视图解析器)。

9、ViewReslover解析后返回具体View(视图)。

10、DispatcherServlet根据View进行渲染视图(即将模型数据填充至视图中)。

11、DispatcherServlet响应用户。

当然现在的开发,基本都是前后端分离的开发的,并没有视图这些,一般都是handler中使用Response直接结果返回

面试官:Springboot自动配置原理

候选人

嗯,好的,它是这样的。

在Spring Boot项目中的引导类上有一个注解@SpringBootApplication,这个注解是对三个注解进行了封装,分别是:

  • @SpringBootConfiguration
  • @EnableAutoConfiguration
  • @ComponentScan

其中@EnableAutoConfiguration是实现自动化配置的核心注解。

该注解通过@Import注解导入对应的配置选择器。关键的是内部就是读取了该项目和该项目引用的Jar包的的classpath路径下META-INF/spring.factories文件中的所配置的类的全类名。

在这些配置类中所定义的Bean会根据条件注解所指定的条件来决定是否需要将其导入到Spring容器中。

一般条件判断会有像@ConditionalOnClass这样的注解,判断是否有对应的class文件,如果有则加载该类,把这个配置类的所有的Bean放入spring容器中使用。

面试官:Spring 的常见注解有哪些?

候选人

嗯,这个就很多了

第一类是:声明bean,有@Component、@Service、@Repository、@Controller

第二类是:依赖注入相关的,有@Autowired、@Qualifier、@Resourse

第三类是:设置作用域 @Scope

第四类是:spring配置相关的,比如@Configuration,@ComponentScan 和 @Bean

第五类是:跟aop相关做增强的注解 @Aspect,@Before,@After,@Around,@Pointcut

面试官:SpringMVC常见的注解有哪些?

候选人

嗯,这个也很多的

有@RequestMapping:用于映射请求路径;

@RequestBody:注解实现接收http请求的json数据,将json转换为java对象;

@RequestParam:指定请求参数的名称;

@PathViriable:从请求路径下中获取请求参数(/user/{id}),传递给方法的形式参数;@ResponseBody:注解实现将controller方法返回对象转化为json对象响应给客户端。@RequestHeader:获取指定的请求头数据,还有像@PostMapping、@GetMapping这些。

面试官:Springboot常见注解有哪些?

候选人

嗯~~

Spring Boot的核心注解是@SpringBootApplication , 他由几个注解组成 :

  • @SpringBootConfiguration: 组合了- @Configuration注解,实现配置文件的功能;
  • @EnableAutoConfiguration:打开自动配置的功能,也可以关闭某个自动配置的选项
  • @ComponentScan:Spring组件扫描

面试官:MyBatis执行流程

候选人

好,这个知道的,不过步骤也很多

①读取MyBatis配置文件:mybatis-config.xml加载运行环境和映射文件

②构造会话工厂SqlSessionFactory,一个项目只需要一个,单例的,一般由spring进行管理

③会话工厂创建SqlSession对象,这里面就含了执行SQL语句的所有方法

④操作数据库的接口,Executor执行器,同时负责查询缓存的维护

⑤Executor接口的执行方法中有一个MappedStatement类型的参数,封装了映射信息

⑥输入参数映射

⑦输出结果映射

面试官:Mybatis是否支持延迟加载?

候选人

是支持的~

延迟加载的意思是:就是在需要用到数据时才进行加载,不需要用到数据时就不加载数据。

Mybatis支持一对一关联对象和一对多关联集合对象的延迟加载

在Mybatis配置文件中,可以配置是否启用延迟加载lazyLoadingEnabled=true|false,默认是关闭的

面试官:延迟加载的底层原理知道吗?

候选人

嗯,我想想啊

延迟加载在底层主要使用的CGLIB动态代理完成的

第一是,使用CGLIB创建目标对象的代理对象,这里的目标对象就是开启了延迟加载的mapper

第二个是当调用目标方法时,进入拦截器invoke方法,发现目标方法是null值,再执行sql查询

第三个是获取数据以后,调用set方法设置属性值,再继续查询目标方法,就有值了

面试官:Mybatis的一级、二级缓存用过吗?

候选人

嗯~~,用过的~

mybatis的一级缓存: 基于 PerpetualCache 的 HashMap 本地缓存,其存储作用域为 Session,当Session进行flush或close之后,该Session中的所有Cache就将清空,默认打开一级缓存

关于二级缓存需要单独开启

二级缓存是基于namespace和mapper的作用域起作用的,不是依赖于SQL session,默认也是采用 PerpetualCache,HashMap 存储。

如果想要开启二级缓存需要在全局配置文件和映射文件中开启配置才行。

面试官:Mybatis的二级缓存什么时候会清理缓存中的数据

候选人

嗯!!

当某一个作用域(一级缓存 Session/二级缓存Namespaces)的进行了新增、修改、删除操作后,默认该作用域下所有 select 中的缓存将被 clear。

4.微服务篇

面试官:Spring Cloud 5大组件有哪些?

候选人:

早期我们一般认为的Spring Cloud五大组件是

  • Eureka : 注册中心
  • Ribbon : 负载均衡
  • Feign : 远程调用
  • Hystrix : 服务熔断
  • Zuul/Gateway : 网关

随着SpringCloudAlibba在国内兴起 , 我们项目中使用了一些阿里巴巴的组件

  • 注册中心/配置中心 Nacos
  • 负载均衡 Ribbon
  • 服务调用 Feign
  • 服务保护 sentinel
  • 服务网关 Gateway

面试官:服务注册和发现是什么意思?Spring Cloud 如何实现服务注册发现?

候选人:

我理解的是主要三块大功能,分别是服务注册 、服务发现、服务状态监控

我们当时项目采用的eureka作为注册中心,这个也是spring cloud体系中的一个核心组件

服务注册:服务提供者需要把自己的信息注册到eureka,由eureka来保存这些信息,比如服务名称、ip、端口等等

服务发现:消费者向eureka拉取服务列表信息,如果服务提供者有集群,则消费者会利用负载均衡算法,选择一个发起调用

服务监控:服务提供者会每隔30秒向eureka发送心跳,报告健康状态,如果eureka服务90秒没接收到心跳,从eureka中剔除

面试官:我看你之前也用过nacos、你能说下nacos与eureka的区别?

候选人:

我们当时xx项目就是采用的nacos作为注册中心,选择nacos还要一个重要原因就是它支持配置中心,不过nacos作为注册中心,也比eureka要方便好用一些,主要相同不同点在于几点:

  • 共同点

Nacos与eureka都支持服务注册和服务拉取,都支持服务提供者心跳方式做健康检测

  • Nacos与Eureka的区别

①Nacos支持服务端主动检测提供者状态:临时实例采用心跳模式,非临时实例采用主动检测模式

②临时实例心跳不正常会被剔除,非临时实例则不会被剔除

③Nacos支持服务列表变更的消息推送模式,服务列表更新更及时

④Nacos集群默认采用AP方式,当集群中存在非临时实例时,采用CP模式;Eureka采用AP方式

面试官:你们项目负载均衡如何实现的 ?

候选人:

是这样~~

在服务调用过程中的负载均衡一般使用SpringCloud的Ribbon 组件实现 , Feign的底层已经自动集成了Ribbon , 使用起来非常简单

当发起远程调用时,ribbon先从注册中心拉取服务地址列表,然后按照一定的路由策略选择一个发起远程调用,一般的调用策略是轮询

面试官:Ribbon负载均衡策略有哪些 ?

候选人:

我想想啊,有很多种,我记得几个:

  • RoundRobinRule:简单轮询服务列表来选择服务器
  • WeightedResponseTimeRule:按照权重来选择服务器,响应时间越长,权重越小
  • RandomRule:随机选择一个可用的服务器
  • ZoneAvoidanceRule:区域敏感策略,以区域可用的服务器为基础进行服务器的选择。使用Zone对服务器进行分类,这个Zone可以理解为一个机房、一个机架等。而后再对Zone内的多个服务做轮询(默认)

面试官:如果想自定义负载均衡策略如何实现 ?

候选人:

提供了两种方式:

1,创建类实现IRule接口,可以指定负载均衡策略,这个是全局的,对所有的远程调用都起作用

2,在客户端的配置文件中,可以配置某一个服务调用的负载均衡策略,只是对配置的这个服务生效远程调用

面试官:什么是服务雪崩,怎么解决这个问题?

候选人:

服务雪崩是指一个服务失败,导致整条链路的服务都失败的情形,一般我们在项目解决的话就是两种方案,第一个是服务降级,第二个是服务熔断,如果流量太大的话,可以考虑限流

服务降级:服务自我保护的一种方式,或者保护下游服务的一种方式,用于确保服务不会受请求突增影响变得不可用,确保服务不会崩溃,一般在实际开发中与feign接口整合,编写降级逻辑

服务熔断:默认关闭,需要手动打开,如果检测到 10 秒内请求的失败率超过 50%,就触发熔断机制。之后每隔 5 秒重新尝试请求微服务,如果微服务不能响应,继续走熔断机制。如果微服务可达,则关闭熔断机制,恢复正常请求

面试官:你们的微服务是怎么监控的?

候选人:

我们项目中采用的skywalking进行监控的

1,skywalking主要可以监控接口、服务、物理实例的一些状态。特别是在压测的时候可以看到众多服务中哪些服务和接口比较慢,我们可以针对性的分析和优化。

2,我们还在skywalking设置了告警规则,特别是在项目上线以后,如果报错,我们分别设置了可以给相关负责人发短信和发邮件,第一时间知道项目的bug情况,第一时间修复

面试官:你们项目中有没有做过限流 ? 怎么做的 ?

候选人:

我当时做的xx项目,采用就是微服务的架构,因为xx因为,应该会有突发流量,最大QPS可以达到2000,但是服务支撑不住,我们项目都通过压测最多可以支撑1200QPS。因为我们平时的QPS也就不到100,为了解决这些突发流量,所以采用了限流。

【版本1】

我们当时采用的nginx限流操作,nginx使用的漏桶算法来实现过滤,让请求以固定的速率处理请求,可以应对突发流量,我们控制的速率是按照ip进行限流,限制的流量是每秒20

【版本2】

我们当时采用的是spring cloud gateway中支持局部过滤器RequestRateLimiter来做限流,使用的是令牌桶算法,可以根据ip或路径进行限流,可以设置每秒填充平均速率,和令牌桶总容量

面试官:限流常见的算法有哪些呢?

候选人:

比较常见的限流算法有漏桶算法和令牌桶算法

漏桶算法是把请求存入到桶中,以固定速率从桶中流出,可以让我们的服务做到绝对的平均,起到很好的限流效果

令牌桶算法在桶中存储的是令牌,按照一定的速率生成令牌,每个请求都要先申请令牌,申请到令牌以后才能正常请求,也可以起到很好的限流作用

它们的区别是,漏桶和令牌桶都可以处理突发流量,其中漏桶可以做到绝对的平滑,令牌桶有可能会产生突发大量请求的情况,一般nginx限流采用的漏桶,spring cloud gateway中可以支持令牌桶算法

面试官:什么是CAP理论?

候选人

CAP主要是在分布式项目下的一个理论。包含了三项,一致性、可用性、分区容错性

  • 一致性(Consistency)是指更新操作成功并返回客户端完成后,所有节点在同一时间的数据完全一致(强一致性),不能存在中间状态。
  • 可用性(Availability) 是指系统提供的服务必须一直处于可用的状态,对于用户的每一个操作请求总是能够在有限的时间内返回结果。
  • 分区容错性(Partition tolerance) 是指分布式系统在遇到任何网络分区故障时,仍然需要能够保证对外提供满足一致性和可用性的服务,除非是整个网络环境都发生了故障。

面试官:为什么分布式系统中无法同时保证一致性和可用性?

候选人

嗯,是这样的~~

首先一个前提,对于分布式系统而言,分区容错性是一个最基本的要求,因此基本上我们在设计分布式系统的时候只能从一致性(C)和可用性(A)之间进行取舍。

如果保证了一致性(C):对于节点N1和N2,当往N1里写数据时,N2上的操作必须被暂停,只有当N1同步数据到N2时才能对N2进行读写请求,在N2被暂停操作期间客户端提交的请求会收到失败或超时。显然,这与可用性是相悖的。

如果保证了可用性(A):那就不能暂停N2的读写操作,但同时N1在写数据的话,这就违背了一致性的要求。

面试官:什么是BASE理论?

候选人

嗯,这个也是CAP分布式系统设计理论

BASE是CAP理论中AP方案的延伸,核心思想是即使无法做到强一致性(StrongConsistency,CAP的一致性就是强一致性),但应用可以采用适合的方式达到最终一致性(Eventual Consitency)。它的思想包含三方面:

1、Basically Available(基本可用):基本可用是指分布式系统在出现不可预知的故障的时候,允许损失部分可用性,但不等于系统不可用。

2、Soft state(软状态):即是指允许系统中的数据存在中间状态,并认为该中间状态的存在不会影响系统的整体可用性,即允许系统在不同节点的数据副本之间进行数据同步的过程存在延时。

3、Eventually consistent(最终一致性):强调系统中所有的数据副本,在经过一段时间的同步后,最终能够达到一个一致的状态。其本质是需要系统保证最终数据能够达到一致,而不需要实时保证系统数据的强一致性。

面试官:你们采用哪种分布式事务解决方案?

候选人:

我们当时是xx项目,主要使用到的seata的at模式解决的分布式事务

seata的AT模型分为两个阶段:

1、阶段一RM的工作:① 注册分支事务 ② 记录undo-log(数据快照)③ 执行业务sql并提交 ④报告事务状态

2、阶段二提交时RM的工作:删除undo-log即可

3、阶段二回滚时RM的工作:根据undo-log恢复数据到更新前

at模式牺牲了一致性,保证了可用性,不过,它保证的是最终一致性

面试官:分布式服务的接口幂等性如何设计?

候选人:

嗯,我们当时有一个xx项目的下单操作,采用的token+redis实现的,流程是这样的

第一次请求,也就是用户打开了商品详情页面,我们会发起一个请求,在后台生成一个唯一token存入redis,key就是用户的id,value就是这个token,同时把这个token返回前端

第二次请求,当用户点击了下单操作会后,会携带之前的token,后台先到redis进行验证,如果存在token,可以执行业务,同时删除token;如果不存在,则直接返回,不处理业务,就保证了同一个token只处理一次业务,就保证了幂等性

面试官:xxl-job路由策略有哪些?

候选人:

xxl-job提供了很多的路由策略,我们平时用的较多就是:轮询、故障转移、分片广播…

面试官:xxl-job任务执行失败怎么解决?

候选人:

有这么几个操作

第一:路由策略选择故障转移,优先使用健康的实例来执行任务

第二,如果还有失败的,我们在创建任务时,可以设置重试次数

第三,如果还有失败的,就可以查看日志或者配置邮件告警来通知相关负责人解决

面试官:如果有大数据量的任务同时都需要执行,怎么解决?

候选人:

我们会让部署多个实例,共同去执行这些批量的任务,其中任务的路由策略是分片广播

在任务执行的代码中可以获取分片总数和当前分片,按照取模的方式分摊到各个实例执行就可以了

5.消息中间件篇

面试官:RabbitMQ-如何保证消息不丢失

候选人

嗯!我们当时MYSQL和Redis的数据双写一致性就是采用RabbitMQ实现同步的,这里面就要求了消息的高可用性,我们要保证消息的不丢失。主要从三个层面考虑

第一个是开启生产者确认机制,确保生产者的消息能到达队列,如果报错可以先记录到日志中,再去修复数据

第二个是开启持久化功能,确保消息未消费前在队列中不会丢失,其中的交换机、队列、和消息都要做持久化

第三个是开启消费者确认机制为auto,由spring确认消息处理成功后完成ack,当然也需要设置一定的重试次数,我们当时设置了3次,如果重试3次还没有收到消息,就将失败后的消息投递到异常交换机,交由人工处理

面试官:RabbitMQ消息的重复消费问题如何解决的

候选人

嗯,这个我们还真遇到过,是这样的,我们当时消费者是设置了自动确认机制,当服务还没来得及给MQ确认的时候,服务宕机了,导致服务重启之后,又消费了一次消息。这样就重复消费了

因为我们当时处理的支付(订单|业务唯一标识),它有一个业务的唯一标识,我们再处理消息时,先到数据库查询一下,这个数据是否存在,如果不存在,说明没有处理过,这个时候就可以正常处理这个消息了。如果已经存在这个数据了,就说明消息重复消费了,我们就不需要再消费了

面试官:那你还知道其他的解决方案吗?

候选人

嗯,我想想~

其实这个就是典型的幂等的问题,比如,redis分布式锁、数据库的锁都是可以的

面试官:RabbitMQ中死信交换机 ? (RabbitMQ延迟队列有了解过嘛)

候选人

嗯!了解过!

我们当时的xx项目有一个xx业务,需要用到延迟队列,其中就是使用RabbitMQ来实现的。

延迟队列就是用到了死信交换机和TTL(消息存活时间)实现的。

如果消息超时未消费就会变成死信,在RabbitMQ中如果消息成为死信,队列可以绑定一个死信交换机,在死信交换机上可以绑定其他队列,在我们发消息的时候可以按照需求指定TTL的时间,这样就实现了延迟队列的功能了。

我记得RabbitMQ还有一种方式可以实现延迟队列,在RabbitMQ中安装一个死信插件,这样更方便一些,我们只需要在声明交互机的时候,指定这个就是死信交换机,然后在发送消息的时候直接指定超时时间就行了,相对于死信交换机+TTL要省略了一些步骤

面试官:如果有100万消息堆积在MQ , 如何解决 ?

候选人

我在实际的开发中,没遇到过这种情况,不过,如果发生了堆积的问题,解决方案也所有很多的

第一:提高消费者的消费能力 ,可以使用多线程消费任务

第二:增加更多消费者,提高消费速度

使用工作队列模式, 设置多个消费者消费消费同一个队列中的消息

第三:扩大队列容积,提高堆积上限

可以使用RabbitMQ惰性队列,惰性队列的好处主要是

①接收到消息后直接存入磁盘而非内存

②消费者要消费消息时才会从磁盘中读取并加载到内存

③支持数百万条的消息存储

面试官:RabbitMQ的高可用机制有了解过嘛

候选人

嗯,熟悉的~

我们当时项目在生产环境下,使用的集群,当时搭建是镜像模式集群,使用了3台机器。

镜像队列结构是一主多从,所有操作都是主节点完成,然后同步给镜像节点,如果主节点宕机后,镜像节点会替代成新的主节点,不过在主从同步完成前,主节点就已经宕机,可能出现数据丢失

面试官:那出现丢数据怎么解决呢?

候选人

我们可以采用仲裁队列,与镜像队列一样,都是主从模式,支持主从数据同步,主从同步基于Raft协议,强一致。

并且使用起来也非常简单,不需要额外的配置,在声明队列的时候只要指定这个是仲裁队列即可

面试官:Kafka是如何保证消息不丢失

候选人

嗯,这个保证机制很多,在发送消息到消费者接收消息,在每个阶段都有可能会丢失消息,所以我们解决的话也是从多个方面考虑

第一个是生产者发送消息的时候,可以使用异步回调发送,如果消息发送失败,我们可以通过回调获取失败后的消息信息,可以考虑重试或记录日志,后边再做补偿都是可以的。同时在生产者这边还可以设置消息重试,有的时候是由于网络抖动的原因导致发送不成功,就可以使用重试机制来解决

第二个在broker中消息有可能会丢失,我们可以通过kafka的复制机制来确保消息不丢失,在生产者发送消息的时候,可以设置一个acks,就是确认机制。我们可以设置参数为all,这样的话,当生产者发送消息到了分区之后,不仅仅只在leader分区保存确认,在follwer分区也会保存确认,只有当所有的副本都保存确认以后才算是成功发送了消息,所以,这样设置就很大程度了保证了消息不会在broker丢失

第三个有可能是在消费者端丢失消息,kafka消费消息都是按照offset进行标记消费的,消费者默认是自动按期提交已经消费的偏移量,默认是每隔5s提交一次,如果出现重平衡的情况,可能会重复消费或丢失数据。我们一般都会禁用掉自动提价偏移量,改为手动提交,当消费成功以后再报告给broker消费的位置,这样就可以避免消息丢失和重复消费了

面试官:Kafka中消息的重复消费问题如何解决的

候选人

kafka消费消息都是按照offset进行标记消费的,消费者默认是自动按期提交已经消费的偏移量,默认是每隔5s提交一次,如果出现重平衡的情况,可能会重复消费或丢失数据。我们一般都会禁用掉自动提价偏移量,改为手动提交,当消费成功以后再报告给broker消费的位置,这样就可以避免消息丢失和重复消费了

为了消息的幂等,我们也可以设置唯一主键来进行区分,或者是加锁,数据库的锁,或者是redis分布式锁,都能解决幂等的问题

面试官:Kafka是如何保证消费的顺序性

候选人

kafka默认存储和消费消息,是不能保证顺序性的,因为一个topic数据可能存储在不同的分区中,每个分区都有一个按照顺序的存储的偏移量,如果消费者关联了多个分区不能保证顺序性

如果有这样的需求的话,我们是可以解决的,把消息都存储同一个分区下就行了,有两种方式都可以进行设置,第一个是发送消息时指定分区号,第二个是发送消息时按照相同的业务设置相同的key,因为默认情况下分区也是通过key的hashcode值来选择分区的,hash值如果一样的话,分区肯定也是一样的

面试官:Kafka的高可用机制有了解过嘛

候选人

嗯,主要是有两个层面,第一个是集群,第二个是提供了复制机制

kafka集群指的是由多个broker实例组成,即使某一台宕机,也不耽误其他broker继续对外提供服务

复制机制是可以保证kafka的高可用的,一个topic有多个分区,每个分区有多个副本,有一个leader,其余的是follower,副本存储在不同的broker中;所有的分区副本的内容是都是相同的,如果leader发生故障时,会自动将其中一个follower提升为leader,保证了系统的容错性、高可用性

面试官:解释一下复制机制中的ISR

候选人

ISR的意思是in-sync replica,就是需要同步复制保存的follower

其中分区副本有很多的follower,分为了两类,一个是ISR,与leader副本同步保存数据,另外一个普通的副本,是异步同步数据,当leader挂掉之后,会优先从ISR副本列表中选取一个作为leader,因为ISR是同步保存数据,数据更加的完整一些,所以优先选择ISR副本列表

面试官:Kafka数据清理机制了解过嘛

候选人

嗯,了解过~~

Kafka中topic的数据存储在分区上,分区如果文件过大会分段存储segment

每个分段都在磁盘上以索引(xxxx.index)和日志文件(xxxx.log)的形式存储,这样分段的好处是,第一能够减少单个文件内容的大小,查找数据方便,第二方便kafka进行日志清理。

在kafka中提供了两个日志的清理策略:

第一,根据消息的保留时间,当消息保存的时间超过了指定的时间,就会触发清理,默认是168小时( 7天)

第二是根据topic存储的数据大小,当topic所占的日志文件大小大于一定的阈值,则开始删除最久的消息。这个默认是关闭的

这两个策略都可以通过kafka的broker中的配置文件进行设置

面试官:Kafka中实现高性能的设计有了解过嘛

候选人

Kafka 高性能,是多方面协同的结果,包括宏观架构、分布式存储、ISR 数据同步、以及高效的利用磁盘、操作系统特性等。主要体现有这么几点:

消息分区:不受单台服务器的限制,可以不受限的处理更多的数据

顺序读写:磁盘顺序读写,提升读写效率

页缓存:把磁盘中的数据缓存到内存中,把对磁盘的访问变为对内存的访问

零拷贝:减少上下文切换及数据拷贝

消息压缩:减少磁盘IO和网络IO

分批发送:将消息打包批量发送,减少网络开销

]]>
面试 面试
验证码服务 /posts/11844.html 1.使用邮件发送验证码

1.1 引入依赖

坑点:有时候遇到验证码发不出去的情况,要调整依赖的版本,更新为高版本的依赖

<!--javaMail-->
<dependencies>
<dependency>
<groupId>javax.mail</groupId>
<artifactId>javax.mail-api</artifactId>
<version>1.5.6</version>
</dependency>
<dependency>
<groupId>com.sun.mail</groupId>
<artifactId>javax.mail</artifactId>
<version>1.5.3</version>
</dependency>
</dependencies>

1.2 导入发送验证码的工具类

//读取配置文件中的值
//静态属性只会加载一次 我们要在第一次加载的时候给静态属性赋上值
//还要其他的方法 比如使用@Value注解
static { //读取配置文件中的关于邮件的配置
try {
FileInputStream fileInputStream = new FileInputStream("src/main/resources/application.properties");
Properties properties = new Properties();
properties.load(fileInputStream);
user = properties.getProperty("guli.user");
password = properties.getProperty("guli.password");
} catch (IOException e) {
throw new RuntimeException(e);
}

}

import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;

import javax.annotation.PostConstruct;
import javax.mail.*;
import javax.mail.internet.InternetAddress;
import javax.mail.internet.MimeMessage;
import java.util.Properties;

/**
* 发邮件工具类
*/
public class MailUtils {
private static String USER = "xxxxxxxx"; // 发件人称号,同邮箱地址
private static String PASSWORD = "xxxxxxxx"; // 授权码,开启SMTP时显示

/**
* @param to 收件人邮箱
* @param text 邮件正文
* @param title 标题
*/
/* 发送验证信息的邮件 */
public static boolean sendMail(String to, String text, String title) {
try {
final Properties props = new Properties();
props.put("mail.smtp.auth", "true");
//注意发送邮件的方法中,发送给谁的,发送给对应的app,※
//要改成对应的app。扣扣的改成qq的,网易的要改成网易的。※
//props.put("mail.smtp.host", "smtp.qq.com");
props.put("mail.smtp.host", "smtp.qq.com");

// 发件人的账号
props.put("mail.user", USER);
//发件人的密码
props.put("mail.password", PASSWORD);

// 构建授权信息,用于进行SMTP进行身份验证
Authenticator authenticator = new Authenticator() {
@Override
protected PasswordAuthentication getPasswordAuthentication() {
// 用户名、密码
String userName = props.getProperty("mail.user");
String password = props.getProperty("mail.password");
return new PasswordAuthentication(userName, password);
}
};
// 使用环境属性和授权信息,创建邮件会话
Session mailSession = Session.getInstance(props, authenticator);
// 创建邮件消息
MimeMessage message = new MimeMessage(mailSession);
// 设置发件人
String username = props.getProperty("mail.user");
InternetAddress form = new InternetAddress(username);
message.setFrom(form);

// 设置收件人
InternetAddress toAddress = new InternetAddress(to);
message.setRecipient(Message.RecipientType.TO, toAddress);

// 设置邮件标题
message.setSubject(title);

// 设置邮件的内容体
message.setContent(text, "text/html;charset=UTF-8");
// 发送邮件
Transport.send(message);
return true;
} catch (Exception e) {
e.printStackTrace();
}
return false;
}


}

1.3 导入随机生成验证码的工具类

import java.util.Random;

/**
* 随机生成验证码工具类
*/
public class ValidateCodeUtils {
/**
* 随机生成验证码
*
* @param length 长度为4位或者6位
* @return
*/
public static Integer generateValidateCode(int length) {
Integer code = null;
if (length == 4) {
code = new Random().nextInt(9999);//生成随机数,最大为9999
if (code < 1000) {
code = code + 1000;//保证随机数为4位数字
}
} else if (length == 6) {
code = new Random().nextInt(999999);//生成随机数,最大为999999
if (code < 100000) {
code = code + 100000;//保证随机数为6位数字
}
} else {
throw new RuntimeException("只能生成4位或6位数字验证码");
}
return code;
}

/**
* 随机生成指定长度字符串验证码
*
* @param length 长度
* @return
*/
public static String generateValidateCode4String(int length) {
Random rdm = new Random();
String hash1 = Integer.toHexString(rdm.nextInt());
String capstr = hash1.substring(0, length);
return capstr;
}
}

1.4 测试使用

1.设置发送邮件的账号和授权码信息
2.编写测试类测试邮件是否可以正常的发送
3.添加到业务代码中,实现验证码的发送功能

2.使用阿里云的短信服务

阿里云的短信服务不面向个人用户,无法使用,但是使用的基本逻辑和使用邮箱类似

3.Token字符串生成的工具类(JWT)

package com.atguigu.commonutils;

import io.jsonwebtoken.Claims;
import io.jsonwebtoken.Jws;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.SignatureAlgorithm;
import org.springframework.http.server.reactive.ServerHttpRequest;
import org.springframework.util.StringUtils;

import javax.servlet.http.HttpServletRequest;
import java.util.Date;

/**
* @author helen
* @since 2019/10/16
*/
public class JwtUtils {

/**
* 设置token的过期时间
*/
public static final long EXPIRE = 1000 * 60 * 60 * 24;
/**
* 密钥
*/
public static final String APP_SECRET = "ukc8BDbRigUDaY6pZFfWus2jZWLPHO";


/**
* 生成token字符串
*/
public static String getJwtToken(String id, String nickname){

String JwtToken = Jwts.builder()
.setHeaderParam("typ", "JWT")
.setHeaderParam("alg", "HS256")
.setSubject("guli-user")
.setIssuedAt(new Date())
.setExpiration(new Date(System.currentTimeMillis() + EXPIRE))
.claim("id", id)
.claim("nickname", nickname)
.signWith(SignatureAlgorithm.HS256, APP_SECRET)
.compact();

return JwtToken;
}

/**
* 判断token是否存在与有效
*/
public static boolean checkToken(String jwtToken) {
if(StringUtils.isEmpty(jwtToken)) return false;
try {
Jwts.parser().setSigningKey(APP_SECRET).parseClaimsJws(jwtToken);
} catch (Exception e) {
e.printStackTrace();
return false;
}
return true;
}

/**
* 判断token是否存在与有效
*/
public static boolean checkToken(HttpServletRequest request) {
try {
String jwtToken = request.getHeader("token");
if(StringUtils.isEmpty(jwtToken)) return false;
Jwts.parser().setSigningKey(APP_SECRET).parseClaimsJws(jwtToken);
} catch (Exception e) {
e.printStackTrace();
return false;
}
return true;
}

/**
* 根据token获取会员id
*/
public static String getMemberIdByJwtToken(HttpServletRequest request) {
String jwtToken = request.getHeader("token");
if(StringUtils.isEmpty(jwtToken)) return "";
Jws<Claims> claimsJws = Jwts.parser().setSigningKey(APP_SECRET).parseClaimsJws(jwtToken);
Claims claims = claimsJws.getBody();
return (String)claims.get("id");
}
}

]]>
后端 Java
项目实战-黑马头条 /posts/64695.html 一.项目介绍

1.项目概述

​ 随着智能手机的普及,人们更加习惯于通过手机来看新闻。由于生活节奏的加快,很多人只能利用碎片时间来获取信息,因此,对于移动资讯客户端的需求也越来越高。黑马头条项目正是在这样背景下开发出来。黑马头条项目采用当下火热的微服务+大数据技术架构实现。本项目主要着手于获取最新最热新闻资讯,通过大数据分析用户喜好精确推送咨询新闻。

image-20230810153039562

2.业务说明

功能架构图

image-20230810153700690

3.技术栈

  • Spring-Cloud-Gateway : 微服务之前架设的网关服务,实现服务注册中的API请求路由,以及控制流速控制和熔断处理都是常用的架构手段,而这些功能Gateway天然支持
  • 运用Spring Boot快速开发框架,构建项目工程;并结合Spring Cloud全家桶技术,实现后端个人中心、自媒体、管理中心等微服务。
  • 运用Spring Cloud Alibaba Nacos作为项目中的注册中心和配置中心
  • 运用mybatis-plus作为持久层提升开发效率
  • 运用Kafka完成内部系统消息通知;与客户端系统消息通知;以及实时数据计算
  • 运用Redis缓存技术,实现热数据的计算,提升系统性能指标
  • 使用Mysql存储用户数据,以保证上层数据查询的高性能
  • 使用Mongo存储用户热数据,以保证用户热数据高扩展和高性能指标
  • 使用FastDFS作为静态资源存储器,在其上实现热静态资源缓存、淘汰等功能
  • 运用Hbase技术,存储系统中的冷数据,保证系统数据的可靠性
  • 运用ES搜索技术,对冷数据、文章数据建立索引,以保证冷数据、文章查询性能
  • 运用AI技术,来完成系统自动化功能,以提升效率及节省成本。比如实名认证自动化
  • PMD&P3C : 静态代码扫描工具,在项目中扫描项目代码,检查异常点、优化点、代码规范等,为开发团队提供规范统一,提升项目代码质量

技术栈

image-20230809170653756

解决方案

image-20230809170929986

二.环境搭建

1.Linxu环境的搭建

1.1 虚拟机的安装

1.解压分享的虚拟机镜像文件

image-20230811103629846

2.使用VmWare打开.vmx文件
image-20230811103800217

3.配置虚拟机的网络环境

image-20230811104150226

image-20230811104320075

4.开启虚拟机

image-20230811104523506

5.使用FinalShell连接此虚拟机

用户名: root 密码:root IP地址: 192.168.200.130

image-20230811104741394

1.2 Linux软件安装

Linux中开发环境的搭建 | The Blog (gitee.io)

2.开发环境的配置

2.1 项目依赖的环境

  • JDK1.8

  • Intellij Idea

  • maven-3.6.1

  • Git

2.2 后端工程的搭建

image-20230811160530674

解压heima-leadnews.zip文件并用IDEA工具打开

image-20230811152915886

编码格式的设置

image-20230811154426723

三.app端功能开发

1.app登录

登录相关的表结构

表名称 说明
ap_user APP用户信息表
ap_user_fan APP用户粉丝信息表
ap_user_follow APP用户关注信息表
ap_user_realname APP实名认证信息表

ap_user表对应的实体类

package com.heima.model.user.pojos;

import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableField;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import lombok.Data;

import java.io.Serializable;
import java.util.Date;

/**
* <p>
* APP用户信息表
* </p>
*
* @author itheima
*/
@Data
@TableName("ap_user")
public class ApUser implements Serializable {

private static final long serialVersionUID = 1L;

/**
* 主键
*/
@TableId(value = "id", type = IdType.AUTO)
private Integer id;

/**
* 密码、通信等加密盐
*/
@TableField("salt")
private String salt;

/**
* 用户名
*/
@TableField("name")
private String name;

/**
* 密码,md5加密
*/
@TableField("password")
private String password;

/**
* 手机号
*/
@TableField("phone")
private String phone;

/**
* 头像
*/
@TableField("image")
private String image;

/**
* 0 男
1 女
2 未知
*/
@TableField("sex")
private Boolean sex;

/**
* 0 未
1 是
*/
@TableField("is_certification")
private Boolean certification;

/**
* 是否身份认证
*/
@TableField("is_identity_authentication")
private Boolean identityAuthentication;

/**
* 0正常
1锁定
*/
@TableField("status")
private Boolean status;

/**
* 0 普通用户
1 自媒体人
2 大V
*/
@TableField("flag")
private Short flag;

/**
* 注册时间
*/
@TableField("created_time")
private Date createdTime;

}

1.1 用户登录逻辑

注册加盐的过程

​ 用户在登录的时候会生成一个随机的字符串(salt),这个随机的字符串会加到密码后面然后连同密码加密存储到数据库。

image-20230811165505210

登录加盐的过程

​ 先根据账号查询是否存在该用户,如果存在的话,根据用户输入的密码和数据库中的salt进行md5加密,并和数据库中的密码比对,一致的话,比对通过,不一样的话不通过。

image-20230811165808912

1.2 用户模块搭建

heima-leadnews-service父工程依赖文件

<?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 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<parent>
<artifactId>heima-leadnews</artifactId>
<groupId>com.heima</groupId>
<version>1.0-SNAPSHOT</version>
</parent>
<packaging>pom</packaging>
<modules>
<module>heima-leadnews-user</module>
</modules>
<modelVersion>4.0.0</modelVersion>

<artifactId>heima-leadnews-service</artifactId>

<properties>
<maven.compiler.source>8</maven.compiler.source>
<maven.compiler.target>8</maven.compiler.target>
</properties>

<dependencies>
<!-- 数据模型子模块 -->
<dependency>
<groupId>com.heima</groupId>
<artifactId>heima-leadnews-model</artifactId>
</dependency>

<!-- 公共子模块 -->
<dependency>
<groupId>com.heima</groupId>
<artifactId>heima-leadnews-common</artifactId>
</dependency>

<!-- 远程调用子模块 -->
<dependency>
<groupId>com.heima</groupId>
<artifactId>heima-leadnews-feign-api</artifactId>
</dependency>

<!-- Spring Boot Web starter -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>

<!-- Spring Boot Test测试 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>

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

<!-- Nacos配置中心 -->
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-nacos-config</artifactId>
</dependency>

<!-- Feign远程调用客户端 -->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-openfeign</artifactId>
</dependency>
</dependencies>

</project>
  1. 创建子模块并创建出对应的目录结构

    在heima-leadnews-service父工程下创建工程heima-leadnews-user

    image-20230812172036450

  2. 编写用户模块的配置文件

    server:
    port: 51801
    spring:
    application:
    name: leadnews-user
    cloud:
    nacos:
    discovery:
    server-addr: 192.168.200.130:8848
    config:
    server-addr: 192.168.200.130:8848
    file-extension: yml
  3. 在配置中心中添加数据库等相关的配置

    image-20230812173024036

  4. 在resources目录下添加日志的配置文件

    logback.xml

    <?xml version="1.0" encoding="UTF-8"?>

    <configuration>
    <!--定义日志文件的存储地址,使用绝对路径-->
    <property name="LOG_HOME" value="e:/logs"/>

    <!-- Console 输出设置 -->
    <appender name="CONSOLE" class="ch.qos.logback.core.ConsoleAppender">
    <encoder>
    <!--格式化输出:%d表示日期,%thread表示线程名,%-5level:级别从左显示5个字符宽度%msg:日志消息,%n是换行符-->
    <pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n</pattern>
    <charset>utf8</charset>
    </encoder>
    </appender>

    <!-- 按照每天生成日志文件 -->
    <appender name="FILE" class="ch.qos.logback.core.rolling.RollingFileAppender">
    <rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
    <!--日志文件输出的文件名-->
    <fileNamePattern>${LOG_HOME}/leadnews.%d{yyyy-MM-dd}.log</fileNamePattern>
    </rollingPolicy>
    <encoder>
    <pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n</pattern>
    </encoder>
    </appender>

    <!-- 异步输出 -->
    <appender name="ASYNC" class="ch.qos.logback.classic.AsyncAppender">
    <!-- 不丢失日志.默认的,如果队列的80%已满,则会丢弃TRACT、DEBUG、INFO级别的日志 -->
    <discardingThreshold>0</discardingThreshold>
    <!-- 更改默认的队列的深度,该值会影响性能.默认值为256 -->
    <queueSize>512</queueSize>
    <!-- 添加附加的appender,最多只能添加一个 -->
    <appender-ref ref="FILE"/>
    </appender>


    <logger name="org.apache.ibatis.cache.decorators.LoggingCache" level="DEBUG" additivity="false">
    <appender-ref ref="CONSOLE"/>
    </logger>
    <logger name="org.springframework.boot" level="debug"/>
    <root level="info">
    <!--<appender-ref ref="ASYNC"/>-->
    <appender-ref ref="FILE"/>
    <appender-ref ref="CONSOLE"/>
    </root>
    </configuration>

遇到的问题:

  1. 问题一:引入@EnableDiscoveryClient注解的时候爆红

    解决方案:在heima-leadnews-service父工程下加入如下的注解

    <!-- Feign远程调用客户端 -->
    <dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-openfeign</artifactId>
    </dependency>

1.3 登录功能实现

1.3.1 接口定义

接口路径 /api/v1/login/login_auth
请求方式 POST
参数 LoginDto
响应结果 ResponseResult

LoginDto

@Data
public class LoginDto {

/**
* 手机号
*/
@ApiModelProperty(value = "手机号",required = true)
private String phone;

/**
* 密码
*/
@ApiModelProperty(value = "密码",required = true)
private String password;
}

统一返回结果类

package com.heima.model.common.dtos;

import com.alibaba.fastjson.JSON;
import com.heima.model.common.enums.AppHttpCodeEnum;

import java.io.Serializable;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

/**
* 通用的结果返回类
* @param <T>
*/
public class ResponseResult<T> implements Serializable {

private String host;

private Integer code;

private String errorMessage;

private T data;

public ResponseResult() {
this.code = 200;
}

public ResponseResult(Integer code, T data) {
this.code = code;
this.data = data;
}

public ResponseResult(Integer code, String msg, T data) {
this.code = code;
this.errorMessage = msg;
this.data = data;
}

public ResponseResult(Integer code, String msg) {
this.code = code;
this.errorMessage = msg;
}

public static ResponseResult errorResult(int code, String msg) {
ResponseResult result = new ResponseResult();
return result.error(code, msg);
}

public static ResponseResult okResult(int code, String msg) {
ResponseResult result = new ResponseResult();
return result.ok(code, null, msg);
}

public static ResponseResult okResult(Object data) {
ResponseResult result = setAppHttpCodeEnum(AppHttpCodeEnum.SUCCESS, AppHttpCodeEnum.SUCCESS.getErrorMessage());
if(data!=null) {
result.setData(data);
}
return result;
}

public static ResponseResult errorResult(AppHttpCodeEnum enums){
return setAppHttpCodeEnum(enums,enums.getErrorMessage());
}

public static ResponseResult errorResult(AppHttpCodeEnum enums, String errorMessage){
return setAppHttpCodeEnum(enums,errorMessage);
}

public static ResponseResult setAppHttpCodeEnum(AppHttpCodeEnum enums){
return okResult(enums.getCode(),enums.getErrorMessage());
}

private static ResponseResult setAppHttpCodeEnum(AppHttpCodeEnum enums, String errorMessage){
return okResult(enums.getCode(),errorMessage);
}

public ResponseResult<?> error(Integer code, String msg) {
this.code = code;
this.errorMessage = msg;
return this;
}

public ResponseResult<?> ok(Integer code, T data) {
this.code = code;
this.data = data;
return this;
}

public ResponseResult<?> ok(Integer code, T data, String msg) {
this.code = code;
this.data = data;
this.errorMessage = msg;
return this;
}

public ResponseResult<?> ok(T data) {
this.data = data;
return this;
}

public Integer getCode() {
return code;
}

public void setCode(Integer code) {
this.code = code;
}

public String getErrorMessage() {
return errorMessage;
}

public void setErrorMessage(String errorMessage) {
this.errorMessage = errorMessage;
}

public T getData() {
return data;
}

public void setData(T data) {
this.data = data;
}

public String getHost() {
return host;
}

public void setHost(String host) {
this.host = host;
}


public static void main(String[] args) {
//前置
/*AppHttpCodeEnum success = AppHttpCodeEnum.SUCCESS;
System.out.println(success.getCode());
System.out.println(success.getErrorMessage());*/

//查询一个对象
/*Map map = new HashMap();
map.put("name","zhangsan");
map.put("age",18);
ResponseResult result = ResponseResult.okResult(map);
System.out.println(JSON.toJSONString(result));*/


//新增,修改,删除 在项目中统一返回成功即可
/* ResponseResult result = ResponseResult.errorResult(AppHttpCodeEnum.SUCCESS);
System.out.println(JSON.toJSONString(result));*/


//根据不用的业务返回不同的提示信息 比如:当前操作需要登录、参数错误
/*ResponseResult result = ResponseResult.errorResult(AppHttpCodeEnum.NEED_LOGIN);
System.out.println(JSON.toJSONString(result));*/

//查询分页信息
PageResponseResult responseResult = new PageResponseResult(1,5,50);
List list = new ArrayList();
list.add("itcast");
list.add("itheima");
responseResult.setData(list);
System.out.println(JSON.toJSONString(responseResult));

}

}

1.3.2 登录思路分析

image-20230812180918302

1.3.3 登录关键代码实现

package com.heima.user.service.impl;

import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.core.toolkit.Wrappers;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.heima.model.common.dtos.ResponseResult;
import com.heima.model.common.enums.AppHttpCodeEnum;
import com.heima.model.user.dtos.LoginDto;
import com.heima.model.user.pojos.ApUser;
import com.heima.user.mapper.ApUserMapper;
import com.heima.user.service.ApUserService;
import com.heima.utils.common.AppJwtUtil;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.util.DigestUtils;

import java.util.HashMap;
import java.util.Map;


/**
* @author Jason Gong
* @version 1.0
* @website https://jasonsgong.gitee.io
* @Date 2023/8/12
* @Description
*/
@Service
@Transactional
@Slf4j
public class ApUserServiceImpl extends ServiceImpl<ApUserMapper, ApUser> implements ApUserService {
@Override
public ResponseResult login(LoginDto loginDto) {
//1 正常的登录 用户名和密码
if(StringUtils.isNotBlank(loginDto.getPhone()) && StringUtils.isNotBlank(loginDto.getPassword())) {
//1.1 根据手机号查询用户的信息
ApUser dbUser = getOne(new LambdaQueryWrapper<ApUser>().eq(ApUser::getPhone, loginDto.getPhone()));
if(dbUser == null){
return ResponseResult.errorResult(AppHttpCodeEnum.DATA_NOT_EXIST,"用户信息不存在");
}
String salt = dbUser.getSalt();
String password = loginDto.getPassword();
String pwd = DigestUtils.md5DigestAsHex((password + salt).getBytes());
//1.2 比对密码
if(!pwd.equals(dbUser.getPassword())){
return ResponseResult.errorResult(AppHttpCodeEnum.LOGIN_PASSWORD_ERROR);
}
//1.3 生成token
String token = AppJwtUtil.getToken(dbUser.getId().longValue());
Map<String,Object> map = new HashMap<>();
map.put("token",token);
dbUser.setSalt("");
dbUser.setPassword("");
map.put("user",dbUser);
return ResponseResult.okResult(map);
}else {
//2.游客登录
String token = AppJwtUtil.getToken(0L);
Map<String,Object> map = new HashMap<>();
map.put("token",token);
return ResponseResult.okResult(map);
}
}
}

1.3.4 使用接口工具测试

接口测试工具使用教程:https://jasonsgong.gitee.io/posts/35630.html

image-20230813165436808

2. app端网关搭建

网关的概述

image-20230814142355139

项目中搭建的网关

image-20230814142700349

2.1 搭建过程

1.在heima-leadnews-gateway导入以下依赖

<dependencies>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-gateway</artifactId>
</dependency>
<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>
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt</artifactId>
</dependency>
</dependencies>

2.创建网关的模块

image-20230814143113564

3.创建启动类和bootstrap.yml配置文件

@EnableDiscoveryClient
@SpringBootApplication
public class AppGatewayApplication {
public static void main(String[] args) {
SpringApplication.run(AppGatewayApplication.class,args);
}
}
server:
port: 51601
spring:
application:
name: leadnews-app-gateway
cloud:
nacos:
discovery:
server-addr: 192.168.200.130:8848
config:
server-addr: 192.168.200.130:8848
file-extension: yml

4.在nacos中创建app端网关的配置

image-20230814143939252

spring:
cloud:
gateway:
globalcors:
add-to-simple-url-handler-mapping: true
corsConfigurations:
'[/**]':
allowedHeaders: "*"
allowedOrigins: "*"
allowedMethods:
- GET
- POST
- DELETE
- PUT
- OPTION
routes:
# 平台管理
- id: user
uri: lb://leadnews-user
predicates:
- Path=/user/**
filters:
- StripPrefix= 1

5.使用Postman测试网关

http://localhost:51601/user/api/v1/login/login_auth

image-20230814144314358

2.2 全局过滤器实现jwt校验

网关的过滤流程

image-20230814144806282

思路分析:

  1. 用户进入网关开始登陆,网关过滤器进行判断,如果是登录,则路由到后台管理微服务进行登录
  2. 用户登录成功,后台管理微服务签发JWT TOKEN信息返回给用户
  3. 用户再次进入网关开始访问,网关过滤器接收用户携带的TOKEN
  4. 网关过滤器解析TOKEN ,判断是否有权限,如果有,则放行,如果没有则返回未认证错误

JWT认证的过滤器

/**
* @author Jason Gong
* @version 1.0
* @website https://jasonsgong.gitee.io
* @Date 2023/8/14
* @Description 权限认证的过滤器
*/
@Component
@Slf4j
public class AuthorizeFilter implements Ordered, GlobalFilter {


/**
* 过滤的设置
*/
@Override
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
//1.获取request和response对象
ServerHttpRequest request = exchange.getRequest();
ServerHttpResponse response = exchange.getResponse();

//2.判断是否登录
//通过请求的路径的url判断
if (request.getURI().getPath().contains("/login")) {
//放行
return chain.filter(exchange);
}
//3.获取token
String token = request.getHeaders().getFirst("token");

//4.判断token是否存在
if (StringUtils.isBlank(token)) {
//设置401的状态码
response.setStatusCode(HttpStatus.UNAUTHORIZED);
return response.setComplete();
}

//5.判断token是否有效
try {
//获取token中的数据
Claims claims = AppJwtUtil.getClaimsBody(token);
//判断token是否过期 -1:有效,0:有效,1:过期,2:过期
int res = AppJwtUtil.verifyToken(claims);
if (res == 1 || res == 2) {
//设置401的状态码
response.setStatusCode(HttpStatus.UNAUTHORIZED);
return response.setComplete();
}
} catch (Exception e) {
e.printStackTrace();
//设置401的状态码
response.setStatusCode(HttpStatus.UNAUTHORIZED);
return response.setComplete();
}
//6.放行
return chain.filter(exchange);
}

/**
* 优先级设置 值越小 优先值越高
*/
@Override
public int getOrder() {
return 0;
}
}

3.app前端项目集成

image-20230814151319382

通过nginx来进行配置,功能如下

  • 通过nginx的反向代理功能访问后台的网关资源
  • 通过nginx的静态服务器功能访问前端静态页面

3.1 Nginx集成前端项目步骤

①:解压资料文件夹中的压缩包nginx-1.18.0.zip

cmd切换到nginx所有的目录输入nginx启动nginx

image-20230814152058708

image-20230814152144765

②:解压资料文件夹中的前端项目app-web.zip

解压到一个没有中文的文件夹中,后面nginx配置中会指向这个目录

③:配置nginx.conf文件

在nginx安装的conf目录下新建一个文件夹leadnews.conf,在当前文件夹中新建heima-leadnews-app.conf文件

heima-leadnews-app.conf配置如下:

upstream  heima-app-gateway{
#APP端网关所在的端口
server localhost:51601;
}

server {
listen 8801;
location / {
root C:/Gong/data/app-web/;
index index.html;
}

location ~/app/(.*) {
proxy_pass http://heima-app-gateway/$1;
proxy_set_header HOST $host; # 不改变源请求头的值
proxy_pass_request_body on; #开启获取请求体
proxy_pass_request_headers on; #开启获取请求头
proxy_set_header X-Real-IP $remote_addr; # 记录真实发出请求的客户端IP
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; #记录代理信息
}
}

nginx.conf 把里面注释的内容和静态资源配置相关删除,引入heima-leadnews-app.conf文件加载

#user  nobody;
worker_processes 1;

events {
worker_connections 1024;
}
http {
include mime.types;
default_type application/octet-stream;
sendfile on;
keepalive_timeout 65;
# 引入自定义配置文件
include leadnews.conf/*.conf;
}

④ :启动nginx

​ 在nginx安装包中使用命令提示符打开,输入命令nginx启动项目

​ 可查看进程,检查nginx是否启动

​ 重新加载配置文件:nginx -s reload

⑤:打开前端项目进行测试 – > http://localhost:8801

​ 用谷歌浏览器打开,调试移动端模式进行访问

image-20230814153335349

4.app端文章列表功能

开发前app的首页面

image-20230814154205134

文章的布局展示

image-20230814154405531

4.1 数据库表的创建

文章的相关的数据库

表名称 说明
ap_article 文章信息表,存储已发布的文章
ap_article_config APP已发布文章配置表
ap_article_content APP已发布文章内容表
ap_author APP文章作者信息表
ap_collection APP收藏信息表

导入资料中的sql文件创建相关的数据库表

image-20230814160312382

关键的数据库表

文章基本信息表

image-20230814160547893

APP已发布文章配置表

image-20230814160652330

APP已发布文章内容表

image-20230814160728715

APP文章作者信息表

image-20230814160805398

APP收藏信息表

image-20230814160823771

垂直分表

将文章相关的表分成文章配置表和文章内容表和文章信息表

image-20230814161909312

垂直分表:将一个表的字段分散到多个表中,每个表存储其中一部分字段

优势:

  1. 减少IO争抢,减少锁表的几率,查看文章概述与文章详情互不影响

  2. 充分发挥高频数据的操作效率,对文章概述数据操作的高效率不会被操作文章详情数据的低效率所拖累

拆分规则:

1.把不常用的字段单独放在一张表

2.把text,blob等大字段拆分出来单独放在一张表

3.经常组合查询的字段单独放在一张表中

4.2 文章模块搭建

导入资料中的模块

image-20230815184540157

在nacos中添加配置

spring:
datasource:
driver-class-name: com.mysql.jdbc.Driver
url: jdbc:mysql://localhost:3306/leadnews_article?useUnicode=true&characterEncoding=UTF-8&serverTimezone=UTC
username: root
password: 123456
# 设置Mapper接口所对应的XML文件位置,如果你在Mapper接口中有自定义方法,需要进行该配置
mybatis-plus:
mapper-locations: classpath*:mapper/*.xml
# 设置别名包扫描路径,通过该属性可以给包中的类注册别名
type-aliases-package: com.heima.model.article.pojos

image-20230815162119814

踩坑: 在导入项目的时候提示Caused by: java.io.FileNotFoundException: class path resource [com/heima/apis/article/IArticleClient.class] cannot be opened because it does not exist ,先删除这个项目,手动创建这个项目,然后复制资料里面文件到这个项目即可,直接复制整个项目可能会报这个错!

4.3 首页文章的列表显示

首页上拉和下拉的实现思路

image-20230814163019582

Sql语句实现

#按照发布时间倒序查询十条文章
select * from ap_article aa order by aa.publish_time desc limit 10
#频道筛选
select * from ap_article aa where aa.channel_id = 1 order by aa.publish_time desc limit 10
#加载首页
select * from ap_article aa where aa.channel_id = 1 and aa.publish_time < '2063-09-08 10:20:12' order by aa.publish_time desc limit 10
#加载更多
select * from ap_article aa where aa.channel_id = 1 and aa.publish_time < '2020-09-07 22:30:09' order by aa.publish_time desc limit 10
#加载最新
select * from ap_article aa where aa.channel_id = 1 and aa.publish_time > '2020-09-07 22:30:09' order by aa.publish_time desc limit 10

image-20230815160034436

4.2.1 接口定义

加载首页 加载更多 加载最新
接口路径 /api/v1/article/load /api/v1/article/loadmore /api/v1/article/loadnew
请求方式 POST POST POST
参数 ArticleHomeDto ArticleHomeDto ArticleHomeDto
响应结果 ResponseResult ResponseResult ResponseResult

ArticleHomeDto

package com.heima.model.article.dtos;
import lombok.Data;
import java.util.Date;
@Data
public class ArticleHomeDto {

// 最大时间
Date maxBehotTime;
// 最小时间
Date minBehotTime;
// 分页size
Integer size;
// 频道ID
String tag;
}

4.2.2 实现思路

①:导入heima-leadnews-article微服务,资料在当天的文件夹中

需要在nacos中添加对应的配置

②:定义接口

接口路径、请求方式、入参、出参

③:编写mapper文件

文章表与文章配置表多表查询

④:编写业务层代码

⑤:编写控制器代码

⑥:swagger测试或前后端联调测试

4.2.3 功能的关键代码实现

mapper层的代码

image-20230816142705510

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.heima.article.mapper.ApArticleMapper">
<resultMap id="resultMap" type="com.heima.model.article.pojos.ApArticle">
<id column="id" property="id"/>
<result column="title" property="title"/>
<result column="author_id" property="authorId"/>
<result column="author_name" property="authorName"/>
<result column="channel_id" property="channelId"/>
<result column="channel_name" property="channelName"/>
<result column="layout" property="layout"/>
<result column="flag" property="flag"/>
<result column="images" property="images"/>
<result column="labels" property="labels"/>
<result column="likes" property="likes"/>
<result column="collection" property="collection"/>
<result column="comment" property="comment"/>
<result column="views" property="views"/>
<result column="province_id" property="provinceId"/>
<result column="city_id" property="cityId"/>
<result column="county_id" property="countyId"/>
<result column="created_time" property="createdTime"/>
<result column="publish_time" property="publishTime"/>
<result column="sync_status" property="syncStatus"/>
<result column="static_url" property="staticUrl"/>
</resultMap>
<select id="loadArticleList" resultMap="resultMap">
SELECT
aa.*
FROM
`ap_article` aa
LEFT JOIN ap_article_config aac ON aa.id = aac.article_id
<where>
and aac.is_delete != 1
and aac.is_down != 1
<!-- loadmore -->
<if test="type != null and type == 1">
and aa.publish_time <![CDATA[<]]> #{dto.minBehotTime}
</if>
<if test="type != null and type == 2">
and aa.publish_time <![CDATA[>]]> #{dto.maxBehotTime}
</if>
<if test="dto.tag != '__all__'">
and aa.channel_id = #{dto.tag}
</if>
</where>
order by aa.publish_time desc
limit #{dto.size}
</select>
</mapper>

service层的代码

package com.heima.article.service.Impl;

import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.heima.article.mapper.ApArticleMapper;
import com.heima.article.service.ApArticleService;
import com.heima.common.constants.ArticleConstants;
import com.heima.model.article.dtos.ArticleHomeDto;
import com.heima.model.article.pojos.ApArticle;
import com.heima.model.common.dtos.ResponseResult;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang.StringUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

import java.util.Date;
import java.util.List;

/**
* @author Jason Gong
* @version 1.0
* @website https://jasonsgong.gitee.io
* @Date 2023/8/15
* @Description
*/
@Service
@Transactional
@Slf4j
public class ApArticleServiceImpl extends ServiceImpl<ApArticleMapper, ApArticle> implements ApArticleService {
@Autowired
private ApArticleMapper apArticleMapper;

private static final Integer MAX_PAGE_SIZE = 50;

@Override
public ResponseResult load(ArticleHomeDto dto, Short type) {
//1.参数校验
//分页条数的校验
Integer size = dto.getSize();
if (size == null || size == 0) {
size = 0;
}
//最大分页条数的限制
size = Math.min(size, 50);

//type参数的校验
if (!type.equals(ArticleConstants.LOADTYPE_LOAD_MORE) && !type.equals(ArticleConstants.LOADTYPE_LOAD_NEW)) {
type = 1;
}

//频道参数校验
if (StringUtils.isBlank(dto.getTag())) {
dto.setTag(ArticleConstants.DEFAULT_TAG);
}

//时间检验
if (dto.getMinBehotTime() == null) dto.setMinBehotTime(new Date());
if (dto.getMaxBehotTime() == null) dto.setMaxBehotTime(new Date());

//2.查询
List<ApArticle> apArticles = apArticleMapper.loadArticleList(dto, type);
//3.数据返回
return ResponseResult.okResult(apArticles);
}
}

controller层的代码

package com.heima.article.controller.v1;

import com.heima.article.service.ApArticleService;
import com.heima.common.constants.ArticleConstants;
import com.heima.model.article.dtos.ArticleHomeDto;
import com.heima.model.common.dtos.ResponseResult;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;

/**
* @author Jason Gong
* @version 1.0
* @website https://jasonsgong.gitee.io
* @Date 2023/8/15
* @Description
*/
@RestController
@RequestMapping("/api/v1/article")
public class ArticleHomeController {

@Autowired
private ApArticleService apArticleService;

/**
* 加载首页
*/
@PostMapping("/load")
public ResponseResult<Object> load(@RequestBody ArticleHomeDto articleHomeDto){
return apArticleService.load(articleHomeDto, ArticleConstants.LOADTYPE_LOAD_MORE);
}

/**
* 加载更多
*/
@PostMapping("/loadmore")
public ResponseResult<Object> loadmore(@RequestBody ArticleHomeDto articleHomeDto){
return apArticleService.load(articleHomeDto, ArticleConstants.LOADTYPE_LOAD_MORE);
}


/**
* 加载更新
*/
@PostMapping("/loadnew")
public ResponseResult<Object> loadnew(@RequestBody ArticleHomeDto articleHomeDto){
return apArticleService.load(articleHomeDto, ArticleConstants.LOADTYPE_LOAD_NEW);
}
}

网关中增加文章模块的路由

spring:
cloud:
gateway:
globalcors:
add-to-simple-url-handler-mapping: true
corsConfigurations:
'[/**]':
allowedHeaders: "*"
allowedOrigins: "*"
allowedMethods:
- GET
- POST
- DELETE
- PUT
- OPTION
routes:
# 平台管理
- id: user
uri: lb://leadnews-user
predicates:
- Path=/user/**
filters:
- StripPrefix= 1
# 文章管理
- id: article
uri: lb://leadnews-article
predicates:
- Path=/article/**
filters:
- StripPrefix= 1

5. app端文章详情功能

5.1 需求分析

image-20230816143201242

5.2 实现方案-静态模板展示

image-20230816143544351

静态模板展示关键技术-Freemarker

Freemarker教程: https://jasonsgong.gitee.io/posts/29367.html

5.3 对象存储服务MinIO

MinIO 教程: https://jasonsgong.gitee.io/posts/36397.html

5.4 实现思路以及代码实现

image-20230818210507999

image-20230818210638036

代码实现

1.在文章模块的pom.xml文件中加入以下的依赖

<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-freemarker</artifactId>
</dependency>
<dependency>
<groupId>com.heima</groupId>
<artifactId>heima-file-starter</artifactId>
<version>1.0-SNAPSHOT</version>
</dependency>
</dependencies>

2.在nacos中有关文章模块的配置中添加以下的内容

minio:
accessKey: minio
secretKey: minio123
bucket: leadnews
endpoint: http://192.168.200.130:9000
readPath: http://192.168.200.130:9000

image-20230818211310728

3.将资料中的article.ftl文件拷贝到文章模块的templates目录下,article.ftl文件内容如下

<!DOCTYPE html>
<html>

<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, maximum-scale=1.0, minimum-scale=1.0, viewport-fit=cover">
<title>黑马头条</title>
<!-- 引入样式文件 -->
<link rel="stylesheet" href="https://fastly.jsdelivr.net/npm/vant@2.12.20/lib/index.css">
<!-- 页面样式 -->
<link rel="stylesheet" href="../../../plugins/css/index.css">
</head>

<body>
<div id="app">
<div class="article">
<van-row>
<van-col span="24" class="article-title" v-html="title"></van-col>
</van-row>

<van-row type="flex" align="center" class="article-header">
<van-col span="3">
<van-image round class="article-avatar" src="https://p3.pstatp.com/thumb/1480/7186611868"></van-image>
</van-col>
<van-col span="16">
<div v-html="authorName"></div>
<div>{{ publishTime | timestampToDateTime }}</div>
</van-col>
<van-col span="5">
<van-button round :icon="relation.isfollow ? '' : 'plus'" type="info" class="article-focus"
:text="relation.isfollow ? '取消关注' : '关注'" :loading="followLoading" @click="handleClickArticleFollow">
</van-button>
</van-col>
</van-row>

<van-row class="article-content">
<#if content??>
<#list content as item>
<#if item.type='text'>
<van-col span="24" class="article-text">${item.value}</van-col>
<#else>
<van-col span="24" class="article-image">
<van-image width="100%" src="${item.value}"></van-image>
</van-col>
</#if>
</#list>
</#if>
</van-row>

<van-row type="flex" justify="center" class="article-action">
<van-col>
<van-button round :icon="relation.islike ? 'good-job' : 'good-job-o'" class="article-like"
:loading="likeLoading" :text="relation.islike ? '取消赞' : '点赞'" @click="handleClickArticleLike"></van-button>
<van-button round :icon="relation.isunlike ? 'delete' : 'delete-o'" class="article-unlike"
:loading="unlikeLoading" @click="handleClickArticleUnlike">不喜欢</van-button>
</van-col>
</van-row>

<!-- 文章评论列表 -->
<van-list v-model="commentsLoading" :finished="commentsFinished" finished-text="没有更多了"
@load="onLoadArticleComments">
<van-row id="#comment-view" type="flex" class="article-comment" v-for="(item, index) in comments" :key="index">
<van-col span="3">
<van-image round src="https://p3.pstatp.com/thumb/1480/7186611868" class="article-avatar"></van-image>
</van-col>
<van-col span="21">
<van-row type="flex" align="center" justify="space-between">
<van-col class="comment-author" v-html="item.authorName"></van-col>
<van-col>
<van-button round :icon="item.operation === 0 ? 'good-job' : 'good-job-o'" size="normal"
@click="handleClickCommentLike(item)">{{ item.likes || '' }}
</van-button>
</van-col>
</van-row>

<van-row>
<van-col class="comment-content" v-html="item.content"></van-col>
</van-row>
<van-row type="flex" align="center">
<van-col span="10" class="comment-time">
{{ item.createdTime | timestampToDateTime }}
</van-col>
<van-col span="3">
<van-button round size="normal" v-html="item.reply" @click="showCommentRepliesPopup(item.id)">回复 {{
item.reply || '' }}
</van-button>
</van-col>
</van-row>
</van-col>
</van-row>
</van-list>
</div>
<!-- 文章底部栏 -->
<van-row type="flex" justify="space-around" align="center" class="article-bottom-bar">
<van-col span="13">
<van-field v-model="commentValue" placeholder="写评论">
<template #button>
<van-button icon="back-top" @click="handleSaveComment"></van-button>
</template>
</van-field>
</van-col>
<van-col span="3">
<van-button icon="comment-o" @click="handleScrollIntoCommentView"></van-button>
</van-col>
<van-col span="3">
<van-button :icon="relation.iscollection ? 'star' : 'star-o'" :loading="collectionLoading"
@click="handleClickArticleCollection"></van-button>
</van-col>
<van-col span="3">
<van-button icon="share-o"></van-button>
</van-col>
</van-row>

<!-- 评论Popup 弹出层 -->
<van-popup v-model="showPopup" closeable position="bottom"
:style="{ width: '750px', height: '60%', left: '50%', 'margin-left': '-375px' }">
<!-- 评论回复列表 -->
<van-list v-model="commentRepliesLoading" :finished="commentRepliesFinished" finished-text="没有更多了"
@load="onLoadCommentReplies">
<van-row id="#comment-reply-view" type="flex" class="article-comment-reply"
v-for="(item, index) in commentReplies" :key="index">
<van-col span="3">
<van-image round src="https://p3.pstatp.com/thumb/1480/7186611868" class="article-avatar"></van-image>
</van-col>
<van-col span="21">
<van-row type="flex" align="center" justify="space-between">
<van-col class="comment-author" v-html="item.authorName"></van-col>
<van-col>
<van-button round :icon="item.operation === 0 ? 'good-job' : 'good-job-o'" size="normal"
@click="handleClickCommentReplyLike(item)">{{ item.likes || '' }}
</van-button>
</van-col>
</van-row>

<van-row>
<van-col class="comment-content" v-html="item.content"></van-col>
</van-row>
<van-row type="flex" align="center">
<!-- TODO: js计算时间差 -->
<van-col span="10" class="comment-time">
{{ item.createdTime | timestampToDateTime }}
</van-col>
</van-row>
</van-col>
</van-row>
</van-list>
<!-- 评论回复底部栏 -->
<van-row type="flex" justify="space-around" align="center" class="comment-reply-bottom-bar">
<van-col span="13">
<van-field v-model="commentReplyValue" placeholder="写评论">
<template #button>
<van-button icon="back-top" @click="handleSaveCommentReply"></van-button>
</template>
</van-field>
</van-col>
<van-col span="3">
<van-button icon="comment-o"></van-button>
</van-col>
<van-col span="3">
<van-button icon="star-o"></van-button>
</van-col>
<van-col span="3">
<van-button icon="share-o"></van-button>
</van-col>
</van-row>
</van-popup>
</div>

<!-- 引入 Vue 和 Vant 的 JS 文件 -->
<script src=" https://fastly.jsdelivr.net/npm/vue/dist/vue.min.js">
</script>
<script src="https://fastly.jsdelivr.net/npm/vant@2.12.20/lib/vant.min.js"></script>
<!-- 引入 Axios 的 JS 文件 -->
<#--<script src="https://unpkg.com/axios/dist/axios.min.js"></script>-->
<script src="../../../plugins/js/axios.min.js"></script>
<!-- 页面逻辑 -->
<script src="../../../plugins/js/index.js"></script>
</body>

</html>

4.手动上传资料中index.js和index.css两个文件到MinIO中

上传index.js

@Test
public void testMinIO(){
try {
//读取一个文件
FileInputStream inputStream = new FileInputStream("C:\\Gong\\java\\黑马头条\\day02-app端文章查看,静态化freemarker,分布式文件系统minIO\\资料\\模板文件\\plugins\\js\\index.js");
//1.获取MinIO的连接信息,创建一个minio的客户端
MinioClient minioClient = MinioClient.builder()
.credentials("minio", "minio123")//minio的账号密码
.endpoint("http://192.168.200.130:9000")//minio的地址
.build();
//2.上传
PutObjectArgs putObjectArgs = PutObjectArgs.builder()
.object("plugins/js/index.js")//文件的名称
.contentType("text/js")//文件的类型
.bucket("leadnews")//桶的名称,与之前的minio管理界面创建的bucket名称一致即可
.stream(inputStream,inputStream.available(),-1)
.build();
minioClient.putObject(putObjectArgs);
} catch (Exception e) {
e.printStackTrace();
}
}

上传index.css

@Test
public void testMinIO(){
try {
//读取一个文件
FileInputStream inputStream = new FileInputStream("C:\\Gong\\java\\黑马头条\\day02-app端文章查看,静态化freemarker,分布式文件系统minIO\\资料\\模板文件\\plugins\\css\\index.css");
//1.获取MinIO的连接信息,创建一个minio的客户端
MinioClient minioClient = MinioClient.builder()
.credentials("minio", "minio123")//minio的账号密码
.endpoint("http://192.168.200.130:9000")//minio的地址
.build();
//2.上传
PutObjectArgs putObjectArgs = PutObjectArgs.builder()
.object("plugins/css/index.css")//文件的名称
.contentType("text/css")//文件的类型
.bucket("leadnews")//桶的名称,与之前的minio管理界面创建的bucket名称一致即可
.stream(inputStream,inputStream.available(),-1)
.build();
minioClient.putObject(putObjectArgs);
} catch (Exception e) {
e.printStackTrace();
}
}

5.测试根据文章的内容生成html文件上传到minio中

package com.heima.article.test;

import com.alibaba.fastjson.JSONArray;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.heima.article.ArticleApplication;
import com.heima.article.mapper.ApArticleContentMapper;
import com.heima.article.service.ApArticleService;
import com.heima.file.service.FileStorageService;
import com.heima.model.article.pojos.ApArticle;
import com.heima.model.article.pojos.ApArticleContent;
import freemarker.template.Configuration;
import freemarker.template.Template;
import freemarker.template.TemplateException;
import org.apache.commons.lang.StringUtils;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.test.context.junit4.SpringRunner;

import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.StringWriter;
import java.util.HashMap;

/**
* @author Jason Gong
* @version 1.0
* @website https://jasonsgong.gitee.io
* @Date 2023/8/19
* @Description
*/
@SpringBootTest(classes = ArticleApplication.class)
@RunWith(SpringRunner.class)
public class ArticleFreeMarkerTest {

@Autowired
private ApArticleContentMapper apArticleContentMapper;
@Autowired
private Configuration configuration;
@Autowired
private FileStorageService fileStorageService;
@Autowired
private ApArticleService apArticleService;

@Test
public void createStaticUrlTest() throws IOException, TemplateException {
//已知文章的id
//1.获取文章的内容
ApArticleContent apArticleContent = apArticleContentMapper.selectOne(new LambdaQueryWrapper<ApArticleContent>().eq(ApArticleContent::getArticleId, "1383827995813531650L"));
if (apArticleContent != null && StringUtils.isNotBlank(apArticleContent.getContent())) {
//2.文章内容通过freemarker生成html文件 详细教程见:https://jasonsgong.gitee.io/posts/29367.html
Template template = configuration.getTemplate("article.ftl");
//构建数据模型
HashMap<String, Object> map = new HashMap<>();
map.put("content", JSONArray.parseArray(apArticleContent.getContent()));
//输出流
StringWriter out = new StringWriter();
//合成html文件
template.process(map, out);
//3.把html文件上传到minio中
//构建一个输入流
InputStream in = new ByteArrayInputStream(out.toString().getBytes());
//上传到minio并返回访问的路径
String path = fileStorageService.uploadHtmlFile("", apArticleContent.getArticleId() + ".html", in);
System.out.println("文件在minio中的路径:"+path);
//4.修改ap_article表,保存static_url字段
ApArticle apArticle = new ApArticle();
apArticle.setId(apArticleContent.getArticleId());
apArticle.setStaticUrl(path);
boolean isSuccess = apArticleService.updateById(apArticle);
System.out.println(isSuccess ? "文件上传成功" : "文件上传失败");
}

}
}

6.实现效果

image-20230819153507360

四.自媒体端功能开发

1.后端环境搭建

需要搭建的模块

image-20230819155333131

搭建步骤

image-20230819155229550

1.创建自媒体模块的数据库

image-20230819155853252

2.导入相应的工程文件

image-20230819161650118

3.配置自媒体模块和网关模块在nacos中的配置

自媒体模块的配置

spring:
datasource:
driver-class-name: com.mysql.jdbc.Driver
url: jdbc:mysql://localhost:3306/leadnews_wemedia?useUnicode=true&characterEncoding=UTF-8&serverTimezone=UTC
username: root
password: root
# 设置Mapper接口所对应的XML文件位置,如果你在Mapper接口中有自定义方法,需要进行该配置
mybatis-plus:
mapper-locations: classpath*:mapper/*.xml
# 设置别名包扫描路径,通过该属性可以给包中的类注册别名
type-aliases-package: com.heima.model.media.pojos

image-20230819161148830

网关模块的配置

spring:
cloud:
gateway:
globalcors:
cors-configurations:
'[/**]': # 匹配所有请求
allowedOrigins: "*" #跨域处理 允许所有的域
allowedMethods: # 支持的方法
- GET
- POST
- PUT
- DELETE
routes:
# 平台管理
- id: wemedia
uri: lb://leadnews-wemedia
predicates:
- Path=/wemedia/**
filters:
- StripPrefix= 1

image-20230819161529210

2.前端环境搭建

搭建思路

image-20230819162116240

搭建步骤

image-20230819162230133

heima-leadnews-wemedia配置文件

upstream  heima-wemedia-gateway{
#APP端网关所在的端口
server localhost:51602;
}

server {
listen 8802;
location / {
root C:/Gong/data/wemedia-web/;
index index.html;
}

location ~/wemedia/MEDIA/(.*) {
proxy_pass http://heima-wemedia-gateway/$1;
proxy_set_header HOST $host; # 不改变源请求头的值
proxy_pass_request_body on; #开启获取请求体
proxy_pass_request_headers on; #开启获取请求头
proxy_set_header X-Real-IP $remote_addr; # 记录真实发出请求的客户端IP
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; #记录代理信息
}
}

重启nginx访问

image-20230902145814017

3.自媒体素材管理功能

3.1 素材管理-图片上传

图片素材相关的实体类

package com.heima.model.wemedia.pojos;

import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableField;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import lombok.Data;

import java.io.Serializable;
import java.util.Date;

/**
* <p>
* 自媒体图文素材信息表
* </p>
*
* @author itheima
*/
@Data
@TableName("wm_material")
public class WmMaterial implements Serializable {

private static final long serialVersionUID = 1L;

/**
* 主键
*/
@TableId(value = "id", type = IdType.AUTO)
private Integer id;

/**
* 自媒体用户ID
*/
@TableField("user_id")
private Integer userId;

/**
* 图片地址
*/
@TableField("url")
private String url;

/**
* 素材类型
0 图片
1 视频
*/
@TableField("type")
private Short type;

/**
* 是否收藏
*/
@TableField("is_collection")
private Short isCollection;

/**
* 创建时间
*/
@TableField("created_time")
private Date createdTime;

}

3.1.1 解决图片素材实体类中获取图片userId的问题

实现思路

image-20230902154806019

1.token解析为用户存入header

package com.heima.wemedia.gateway.filter;


import com.heima.wemedia.gateway.util.AppJwtUtil;
import io.jsonwebtoken.Claims;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang.StringUtils;
import org.springframework.cloud.gateway.filter.GatewayFilterChain;
import org.springframework.cloud.gateway.filter.GlobalFilter;
import org.springframework.core.Ordered;
import org.springframework.http.HttpStatus;
import org.springframework.http.server.reactive.ServerHttpRequest;
import org.springframework.http.server.reactive.ServerHttpResponse;
import org.springframework.stereotype.Component;
import org.springframework.web.server.ServerWebExchange;
import reactor.core.publisher.Mono;

@Component
@Slf4j
public class AuthorizeFilter implements Ordered, GlobalFilter {
@Override
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
//1.获取request和response对象
ServerHttpRequest request = exchange.getRequest();
ServerHttpResponse response = exchange.getResponse();

//2.判断是否是登录
if(request.getURI().getPath().contains("/login")){
//放行
return chain.filter(exchange);
}

//3.获取token
String token = request.getHeaders().getFirst("token");

//4.判断token是否存在
if(StringUtils.isBlank(token)){
response.setStatusCode(HttpStatus.UNAUTHORIZED);
return response.setComplete();
}

//5.判断token是否有效
try {
Claims claimsBody = AppJwtUtil.getClaimsBody(token);
//是否是过期
int result = AppJwtUtil.verifyToken(claimsBody);
if(result == 1 || result == 2){
response.setStatusCode(HttpStatus.UNAUTHORIZED);
return response.setComplete();
}
//获取用户信息
Object userId = claimsBody.get("id");
//存入header中
ServerHttpRequest serverHttpRequest = request.mutate().headers(httpHeaders -> {
httpHeaders.add("userId", userId + "");
}).build();
//重置请求
exchange.mutate().request(serverHttpRequest);

} catch (Exception e) {
e.printStackTrace();
}

//6.放行
return chain.filter(exchange);
}

/**
* 优先级设置 值越小 优先级越高
* @return
*/
@Override
public int getOrder() {
return 0;
}
}

2.创建拦截器

package com.heima.wemedia.interceptor;

import com.heima.model.wemedia.pojos.WmUser;
import com.heima.utils.thread.WmThreadLocalUtil;
import io.swagger.models.auth.In;
import org.springframework.web.servlet.HandlerInterceptor;
import org.springframework.web.servlet.ModelAndView;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.util.Enumeration;

/**
* @author Jason Gong
* @version 1.0
* @website https://jasonsgong.gitee.io
* @Date 2023/9/2
* @Description
*/
public class WmTokenInterceptor implements HandlerInterceptor {

/**
* 获取header中的信息 存入当前的线程中
*/
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
//获取header中的userId信息
String userId = request.getHeader("userId");
//判断userId是否为空
if(userId != null){
//将userId存入当前的线程中,我们可以在任何位置获取
WmUser wmUser = new WmUser();
wmUser.setId(Integer.valueOf(userId));
WmThreadLocalUtil.setUser(wmUser);
}
return true;
}


/**
* 清理线程中的数据
*/
@Override
public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception {
WmThreadLocalUtil.clear();
}
}

ThreadLocal工具类,实现在线程中存储、获取、清理用户信息

package com.heima.utils.thread;

import com.heima.model.wemedia.pojos.WmUser;

public class WmThreadLocalUtil {

private final static ThreadLocal<WmUser> WM_USER_THREAD_LOCAL = new ThreadLocal<>();

/**
* 添加用户
* @param wmUser
*/
public static void setUser(WmUser wmUser){
WM_USER_THREAD_LOCAL.set(wmUser);
}

/**
* 获取用户
*/
public static WmUser getUser(){
return WM_USER_THREAD_LOCAL.get();
}

/**
* 清理用户
*/
public static void clear(){
WM_USER_THREAD_LOCAL.remove();
}
}

3.让拦截器生效

package com.heima.wemedia.config;

import com.heima.wemedia.interceptor.WmTokenInterceptor;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;

/**
* @author Jason Gong
* @version 1.0
* @website https://jasonsgong.gitee.io
* @Date 2023/9/2
* @Description
*/
@Configuration
public class WebMvcConfig implements WebMvcConfigurer {
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(new WmTokenInterceptor()).addPathPatterns("/**");
}
}

3.1.2 图片上传接口的定义

说明
接口路径 /api/v1/material/upload_picture
请求方式 POST
参数 MultipartFile
响应结果 ResponseResult

3.1.3 代码实现

1.在pom.xml中引入自定义minio的starter依赖

<dependency>
<groupId>com.heima</groupId>
<artifactId>heima-file-starter</artifactId>
<version>1.0-SNAPSHOT</version>
</dependency>

2.在项目中添加minio的配置(在nacos中配置)

minio:
accessKey: minio
secretKey: minio123
bucket: leadnews
endpoint: http://192.168.200.130:9000
readPath: http://192.168.200.130:9000

3.上传图片的关键代码

package com.heima.wemedia.service.impl;

import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.heima.file.service.FileStorageService;
import com.heima.model.common.dtos.ResponseResult;
import com.heima.model.common.enums.AppHttpCodeEnum;
import com.heima.model.wemedia.pojos.WmMaterial;
import com.heima.utils.thread.WmThreadLocalUtil;
import com.heima.wemedia.mapper.WmMaterialMapper;
import com.heima.wemedia.service.WmMaterialService;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.web.multipart.MultipartFile;

import java.io.IOException;
import java.util.Date;
import java.util.UUID;


@Slf4j
@Service
@Transactional
public class WmMaterialServiceImpl extends ServiceImpl<WmMaterialMapper, WmMaterial> implements WmMaterialService {

@Autowired
private FileStorageService fileStorageService; //使用minio

@Override
public ResponseResult uploadPicture(MultipartFile multipartFile) {
//1.检查参数
if(multipartFile == null || multipartFile.getSize() == 0){
return ResponseResult.errorResult(AppHttpCodeEnum.PARAM_INVALID);
}
//2.上传图片到minio中
String fileName = UUID.randomUUID().toString().replace("-", "");
String originalFilename = multipartFile.getOriginalFilename();
String postfix = originalFilename.substring(originalFilename.lastIndexOf("."));
String fileId = null;
try {
fileId = fileStorageService.uploadImgFile("", fileName + postfix, multipartFile.getInputStream());
log.info("上传图片到Minio中,fileId:{}",fileId);
} catch (IOException e) {
log.info("WmMaterialServiceImpl-上传图片失败");
e.printStackTrace();
}
//3.将图片的信息保存在数据库中
WmMaterial wmMaterial = new WmMaterial();
wmMaterial.setUserId(WmThreadLocalUtil.getUser().getId());
wmMaterial.setUrl(fileId);
wmMaterial.setType((short)0);
wmMaterial.setIsCollection((short)0);
wmMaterial.setCreatedTime(new Date());
save(wmMaterial);
//4.返回参数
return ResponseResult.okResult(wmMaterial);
}
}

4.测试上传功能

image-20230902220022201

3.2 素材管理-图片列表

接口定义

接口路径 /api//v1/material/list
请求方式 POST
参数 WmMaterialDto
响应结果 ResponseResult

请求参数的DTO

@Data
public class WmMaterialDto extends PageRequestDto {

/**
* 1 收藏
* 0 未收藏
*/
private Short isCollection;
}

关键代码

/**
* 素材图片的列表显示
*/
@Override
public ResponseResult findList(WmMaterialDto wmMaterialDto) {
//1.检查参数
wmMaterialDto.checkParam();
//2.分页查询
IPage page = new Page(wmMaterialDto.getPage(),wmMaterialDto.getSize());
LambdaQueryWrapper<WmMaterial> queryWrapper = new LambdaQueryWrapper<>();
//是否收藏
if(wmMaterialDto.getIsCollection() != null && wmMaterialDto.getIsCollection() == 1){
queryWrapper.eq(WmMaterial::getIsCollection,wmMaterialDto.getIsCollection());
}
//按照用户查询
queryWrapper.eq(WmMaterial::getUserId,WmThreadLocalUtil.getUser().getId());
//按照时间查询
queryWrapper.orderByDesc(WmMaterial::getCreatedTime);
page = page(page,queryWrapper);
//3.结果返回
ResponseResult responseResult = new PageResponseResult(wmMaterialDto.getPage(), wmMaterialDto.getSize(), (int) page.getTotal());
responseResult.setData(page.getRecords());
return responseResult;
}

MP的配置

@Bean
public MybatisPlusInterceptor mybatisPlusInterceptor() {
MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor();
interceptor.addInnerInterceptor(new PaginationInnerInterceptor(DbType.MYSQL));
return interceptor;
}

实现效果

image-20230903125528872

4.自媒体文章管理功能

4.1 频道列表查询

频道对应的实体类

package com.heima.model.wemedia.pojos;

import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableField;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import lombok.Data;

import java.io.Serializable;
import java.util.Date;

/**
* <p>
* 频道信息表
* </p>
*
* @author itheima
*/
@Data
@TableName("wm_channel")
public class WmChannel implements Serializable {

private static final long serialVersionUID = 1L;

@TableId(value = "id", type = IdType.AUTO)
private Integer id;

/**
* 频道名称
*/
@TableField("name")
private String name;

/**
* 频道描述
*/
@TableField("description")
private String description;

/**
* 是否默认频道
* 1:默认 true
* 0:非默认 false
*/
@TableField("is_default")
private Boolean isDefault;

/**
* 是否启用
* 1:启用 true
* 0:禁用 false
*/
@TableField("status")
private Boolean status;

/**
* 默认排序
*/
@TableField("ord")
private Integer ord;

/**
* 创建时间
*/
@TableField("created_time")
private Date createdTime;
}

接口定义

说明
接口路径 /api/v1/channel/channels
请求方式 GET
参数
响应结果 ResponseResult

关键代码

/**
* 查询所有的频道信息
*/
@GetMapping("/channels")
public ResponseResult findAllChannels(){
List<WmChannel> channels = wmChannelService.list();
return ResponseResult.okResult(channels);
}

实现效果

image-20230903151216766

4.2 文章列表加载

文章表对应的实体类

package com.heima.model.wemedia.pojos;

import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableField;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import lombok.Data;
import org.apache.ibatis.type.Alias;

import java.io.Serializable;
import java.util.Date;

/**
* <p>
* 自媒体图文内容信息表
* </p>
*
* @author itheima
*/
@Data
@TableName("wm_news")
public class WmNews implements Serializable {

private static final long serialVersionUID = 1L;

/**
* 主键
*/
@TableId(value = "id", type = IdType.AUTO)
private Integer id;

/**
* 自媒体用户ID
*/
@TableField("user_id")
private Integer userId;

/**
* 标题
*/
@TableField("title")
private String title;

/**
* 图文内容
*/
@TableField("content")
private String content;

/**
* 文章布局
0 无图文章
1 单图文章
3 多图文章
*/
@TableField("type")
private Short type;

/**
* 图文频道ID
*/
@TableField("channel_id")
private Integer channelId;

@TableField("labels")
private String labels;

/**
* 创建时间
*/
@TableField("created_time")
private Date createdTime;

/**
* 提交时间
*/
@TableField("submited_time")
private Date submitedTime;

/**
* 当前状态
0 草稿
1 提交(待审核)
2 审核失败
3 人工审核
4 人工审核通过
8 审核通过(待发布)
9 已发布
*/
@TableField("status")
private Short status;

/**
* 定时发布时间,不定时则为空
*/
@TableField("publish_time")
private Date publishTime;

/**
* 拒绝理由
*/
@TableField("reason")
private String reason;

/**
* 发布库文章ID
*/
@TableField("article_id")
private Long articleId;

/**
* //图片用逗号分隔
*/
@TableField("images")
private String images;

@TableField("enable")
private Short enable;

//状态枚举类
@Alias("WmNewsStatus")
public enum Status{
NORMAL((short)0),SUBMIT((short)1),FAIL((short)2),ADMIN_AUTH((short)3),ADMIN_SUCCESS((short)4),SUCCESS((short)8),PUBLISHED((short)9);
short code;
Status(short code){
this.code = code;
}
public short getCode(){
return this.code;
}
}

}

接口定义

接口路径 /api//v1/news/list
请求方式 POST
参数 WmNewsPageReqDto
响应结果 ResponseResult

关键代码

@Override
public ResponseResult findNewsList(WmNewsPageReqDto wmNewsPageReqDto) {
//1.检查参数
wmNewsPageReqDto.checkParam();
//2.根据条件查询
IPage page = new Page(wmNewsPageReqDto.getPage(), wmNewsPageReqDto.getSize());
LambdaQueryWrapper<WmNews> queryWrapper = new LambdaQueryWrapper<>();
//状态
if(wmNewsPageReqDto.getStatus() != null){
queryWrapper.eq(WmNews::getStatus,wmNewsPageReqDto.getStatus());
}
//开始时间 结束时间
if(wmNewsPageReqDto.getBeginPubDate() != null && wmNewsPageReqDto.getEndPubDate()!=null){
queryWrapper.between(WmNews::getPublishTime,wmNewsPageReqDto.getBeginPubDate(),wmNewsPageReqDto.getEndPubDate());
}
//所属频道id
if(wmNewsPageReqDto.getChannelId() != null){
queryWrapper.eq(WmNews::getChannelId,wmNewsPageReqDto.getChannelId());
}
//关键字
if(wmNewsPageReqDto.getKeyword() != null){
queryWrapper.like(WmNews::getTitle,wmNewsPageReqDto.getKeyword());
}
//查询当前登录人的文章
queryWrapper.eq(WmNews::getUserId, WmThreadLocalUtil.getUser().getId());
//按照发布时间倒序查询
queryWrapper.orderByDesc(WmNews::getPublishTime);
page = page(page,queryWrapper);
//3.结果返回
ResponseResult responseResult = new PageResponseResult(wmNewsPageReqDto.getPage(), wmNewsPageReqDto.getSize(), (int) page.getTotal());
responseResult.setData(page.getRecords());
return responseResult;
}

实现效果

image-20230903185956428

4.3 发布文章功能(核心功能)

文章和素材对应关系表

package com.heima.model.wemedia.pojos;

import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableField;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import lombok.Data;

import java.io.Serializable;

/**
* <p>
* 自媒体图文引用素材信息表
* </p>
*
* @author itheima
*/
@Data
@TableName("wm_news_material")
public class WmNewsMaterial implements Serializable {

private static final long serialVersionUID = 1L;

/**
* 主键
*/
@TableId(value = "id", type = IdType.AUTO)
private Integer id;

/**
* 素材ID
*/
@TableField("material_id")
private Integer materialId;

/**
* 图文ID
*/
@TableField("news_id")
private Integer newsId;

/**
* 引用类型
0 内容引用
1 主图引用
*/
@TableField("type")
private Short type;

/**
* 引用排序
*/
@TableField("ord")
private Short ord;

}

实现流程

image-20230904151910210

接口定义

说明
接口路径 /api/v1/news/submit
请求方式 POST
参数 WmNewsDto
响应结果 ResponseResult

WmNewsDto

接收前端参数的dto

package com.heima.model.wemedia.dtos;

import lombok.Data;

import java.util.Date;
import java.util.List;

@Data
public class WmNewsDto {

private Integer id;
/**
* 标题
*/
private String title;
/**
* 频道id
*/
private Integer channelId;
/**
* 标签
*/
private String labels;
/**
* 发布时间
*/
private Date publishTime;
/**
* 文章内容
*/
private String content;
/**
* 文章封面类型 0 无图 1 单图 3 多图 -1 自动
*/
private Short type;
/**
* 提交时间
*/
private Date submitedTime;
/**
* 状态 提交为1 草稿为0
*/
private Short status;

/**
* 封面图片列表 多张图以逗号隔开
*/
private List<String> images;
}

前端传递的json格式数据举例

{
"title":"黑马头条项目背景",
"type":"1",//这个 0 是无图 1 是单图 3 是多图 -1 是自动
"labels":"黑马头条",
"publishTime":"2020-03-14T11:35:49.000Z",
"channelId":1,
"images":[
"http://192.168.200.130/group1/M00/00/00/wKjIgl5swbGATaSAAAEPfZfx6Iw790.png"
],
"status":1,
"content":"[
{
"type":"text",
"value":"随着智能手机的普及,人们更加习惯于通过手机来看新闻。由于生活节奏的加快,很多人只能利用碎片时间来获取信息,因此,对于移动资讯客户端的需求也越来越高。黑马头条项目正是在这样背景下开发出来。黑马头条项目采用当下火热的微服务+大数据技术架构实现。本项目主要着手于获取最新最热新闻资讯,通过大数据分析用户喜好精确推送咨询新闻"
},
{
"type":"image",
"value":"http://192.168.200.130/group1/M00/00/00/wKjIgl5swbGATaSAAAEPfZfx6Iw790.png"
}
]"
}

保存文章和素材对应关系mapper接口

mapper接口

@Mapper
public interface WmNewsMaterialMapper extends BaseMapper<WmNewsMaterial> {


/**
* 批量保存文章和素材之间的关系
* @param materialIds 素材的id
* @param newsId 文章的id
* @param type 类型
*/
void saveRelations(@Param("materialIds") List<Integer> materialIds,@Param("newsId") Integer newsId, @Param("type")Short type);
}

mapper.xml文件

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.heima.wemedia.mapper.WmNewsMaterialMapper">

<insert id="saveRelations">
insert into wm_news_material (material_id,news_id,type,ord)
values
<foreach collection="materialIds" index="ord" item="mid" separator=",">
(#{mid},#{newsId},#{type},#{ord})
</foreach>
</insert>

</mapper>

使用到的常量类

package com.heima.common.constants;

public class WemediaConstants {

public static final Short COLLECT_MATERIAL = 1;//收藏

public static final Short CANCEL_COLLECT_MATERIAL = 0;//取消收藏

public static final String WM_NEWS_TYPE_IMAGE = "image";

public static final Short WM_NEWS_NONE_IMAGE = 0;
public static final Short WM_NEWS_SINGLE_IMAGE = 1;
public static final Short WM_NEWS_MANY_IMAGE = 3;
public static final Short WM_NEWS_TYPE_AUTO = -1;

public static final Short WM_CONTENT_REFERENCE = 0;
public static final Short WM_COVER_REFERENCE = 1;
}

发布文章功能的关键代码

存在许多的bug

package com.heima.wemedia.service.impl;


import com.alibaba.fastjson.JSON;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.core.metadata.IPage;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.heima.common.constants.WemediaConstants;
import com.heima.common.exception.CustomException;
import com.heima.model.common.dtos.PageResponseResult;
import com.heima.model.common.dtos.ResponseResult;
import com.heima.model.common.enums.AppHttpCodeEnum;
import com.heima.model.wemedia.dtos.WmNewsDto;
import com.heima.model.wemedia.dtos.WmNewsPageReqDto;
import com.heima.model.wemedia.pojos.WmMaterial;
import com.heima.model.wemedia.pojos.WmNews;
import com.heima.model.wemedia.pojos.WmNewsMaterial;
import com.heima.utils.thread.WmThreadLocalUtil;
import com.heima.wemedia.mapper.WmMaterialMapper;
import com.heima.wemedia.mapper.WmNewsMapper;
import com.heima.wemedia.mapper.WmNewsMaterialMapper;
import com.heima.wemedia.service.WmNewsService;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang.StringUtils;
import org.apache.jute.compiler.JString;
import org.springframework.beans.BeanUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

import java.util.ArrayList;
import java.util.Date;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;

@Service
@Slf4j
@Transactional
public class WmNewsServiceImpl extends ServiceImpl<WmNewsMapper, WmNews> implements WmNewsService {

@Autowired
private WmNewsMaterialMapper wmNewsMaterialMapper;
@Autowired
private WmMaterialMapper wmMaterialMapper;


@Override
public ResponseResult findNewsList(WmNewsPageReqDto wmNewsPageReqDto) {
//1.检查参数
wmNewsPageReqDto.checkParam();
//2.根据条件查询
IPage page = new Page(wmNewsPageReqDto.getPage(), wmNewsPageReqDto.getSize());
LambdaQueryWrapper<WmNews> queryWrapper = new LambdaQueryWrapper<>();
//状态
if (wmNewsPageReqDto.getStatus() != null) {
queryWrapper.eq(WmNews::getStatus, wmNewsPageReqDto.getStatus());
}
//开始时间 结束时间
if (wmNewsPageReqDto.getBeginPubDate() != null && wmNewsPageReqDto.getEndPubDate() != null) {
queryWrapper.between(WmNews::getPublishTime, wmNewsPageReqDto.getBeginPubDate(), wmNewsPageReqDto.getEndPubDate());
}
//所属频道id
if (wmNewsPageReqDto.getChannelId() != null) {
queryWrapper.eq(WmNews::getChannelId, wmNewsPageReqDto.getChannelId());
}
//关键字
if (wmNewsPageReqDto.getKeyword() != null) {
queryWrapper.like(WmNews::getTitle, wmNewsPageReqDto.getKeyword());
}
//查询当前登录人的文章
queryWrapper.eq(WmNews::getUserId, WmThreadLocalUtil.getUser().getId());
//按照发布时间倒序查询
queryWrapper.orderByDesc(WmNews::getPublishTime);
page = page(page, queryWrapper);
//3.结果返回
ResponseResult responseResult = new PageResponseResult(wmNewsPageReqDto.getPage(), wmNewsPageReqDto.getSize(), (int) page.getTotal());
responseResult.setData(page.getRecords());
return responseResult;
}


@Override
public ResponseResult submitNews(WmNewsDto wmNewsDto) {
//参数校验
if (wmNewsDto == null || wmNewsDto.getContent() == null) {
return ResponseResult.errorResult(AppHttpCodeEnum.PARAM_INVALID);
}
//保存或者修改文章
WmNews wmNews = new WmNews();
BeanUtils.copyProperties(wmNewsDto, wmNews);
//封面图片从list集合转化成字符串(以,作为分割符号)
if (wmNewsDto.getImages() != null && wmNewsDto.getImages().size() > 0) {
String imageStr = StringUtils.join(wmNewsDto.getImages(), ",");
wmNews.setImages(imageStr);
}
//设置封面的类型(数据库中封面的类型是无符号的,无法使用-1表示,所以我们先置空)
if (wmNewsDto.getType().equals(WemediaConstants.WM_NEWS_TYPE_AUTO)) {
wmNews.setType(null);
}
//保存或者修改文章的方法
saveOrUpdateWmNews(wmNews);
//判断是否为草稿,如果是草稿就退出该方法,草稿是不保存文章与素材的关系的
if (wmNewsDto.getStatus().equals(WmNews.Status.NORMAL.getCode())) {
return ResponseResult.okResult(AppHttpCodeEnum.SUCCESS);
}
//不是草稿,保存文章内容与素材的关系
//获取文章内容中的图片信息
List<String> materials = getUrlInfo(wmNewsDto.getContent());
//保存文章和素材的关系
saveRelativeInfoContent(materials,wmNews.getId());
//文章封面图片与素材的关系,布局是自动的,需要自动匹配图片
saveRelativeInfoForCover(wmNewsDto,wmNews,materials);
return ResponseResult.okResult(AppHttpCodeEnum.SUCCESS);
}

/**
* 保存文章封面图片与素材的关系,布局是自动的,需要自动匹配图片
* 1.如果内容图片大于等于1 小于3 单图 type 1
* 2.如果内容图片大于3 多图 type 3
* 3.如果内容没有图片 无图 type 0
*/
private void saveRelativeInfoForCover(WmNewsDto wmNewsDto, WmNews wmNews, List<String> materials) {
List<String> images = wmNewsDto.getImages();
//处理匹配规则
if(wmNewsDto.getType().equals(WemediaConstants.WM_NEWS_TYPE_AUTO)){
if(materials.size() >= 3){//多图
wmNews.setType(WemediaConstants.WM_NEWS_MANY_IMAGE);
//截取这篇文章中的三种图片给文章的封面
images = materials.stream().limit(3).collect(Collectors.toList());
}else if (materials.size() > 1 && materials.size() < 3){//单图
wmNews.setType(WemediaConstants.WM_NEWS_SINGLE_IMAGE);
images = materials.stream().limit(1).collect(Collectors.toList());
}else {//无图
wmNews.setType(WemediaConstants.WM_NEWS_NONE_IMAGE);
}
//修改文章
if (images != null && images.size() > 0){
wmNews.setImages(StringUtils.join(images,","));
}
updateById(wmNews);
}
//保存封面与素材的关系
if (images != null && images.size() > 0){
saveRelativeInfo(images,wmNews.getId(),WemediaConstants.WM_COVER_REFERENCE);
}
}

/**
* 保存文章和素材的对应关系
* @param materials 同一篇文章的所有素材图片url
* @param newsId 文章的id
*/
private void saveRelativeInfoContent(List<String> materials, Integer newsId) {
saveRelativeInfo(materials,newsId,WemediaConstants.WM_CONTENT_REFERENCE);
}

/**
* 保存文章图片与素材的关系到数据库中
* @param materials 同一篇文章中所有的素材图片
* @param newsId 文章的id
*/
private void saveRelativeInfo(List<String> materials, Integer newsId, Short type) {
if (materials != null && !materials.isEmpty()){
//根据图片的url查询素材的id
LambdaQueryWrapper<WmMaterial> queryWrapper = new LambdaQueryWrapper<>();
queryWrapper.in(WmMaterial::getUrl,materials);
List<WmMaterial> wmMaterials = wmMaterialMapper.selectList(queryWrapper);
//判断素材是否被删除
if (wmMaterials == null || wmMaterials.size() == 0){
//手动的抛出异常,上面的操作可以回滚
throw new CustomException(AppHttpCodeEnum.MATERIASL_REFERENCE);
}
if (materials.size() != wmMaterials.size()){
throw new CustomException(AppHttpCodeEnum.MATERIASL_REFERENCE);
}
List<Integer> idList = wmMaterials.stream().map(WmMaterial::getId).collect(Collectors.toList());
wmNewsMaterialMapper.saveRelations(idList,newsId,type);
}
}

/**
* 提取文章内容中的图片信息
* @param content 文章的内容信息
* @return 图片url组成的集合
*/
private List<String> getUrlInfo(String content) {
ArrayList<String> materials = new ArrayList<>();
List<Map> maps = JSON.parseArray(content, Map.class);
for (Map map : maps) {
if (map.get("type").equals("image")){
String imgUrl = (String) map.get("value");
materials.add(imgUrl);
}
}
return materials;
}

/**
* 保存或者修改文章的方法
*
* @param wmNews 文章实体类
*/
private void saveOrUpdateWmNews(WmNews wmNews) {
//补全属性
wmNews.setUserId(WmThreadLocalUtil.getUser().getId());
wmNews.setCreatedTime(new Date());
wmNews.setSubmitedTime(new Date());
wmNews.setEnable((short) 1); //设置默认上架
//判断是保存还是修改操作,执行不同的处理逻辑
if (wmNews.getId() == null) {
//保存操作
save(wmNews);
} else {
//修改操作
//删除文章与素材的关联关系
LambdaQueryWrapper<WmNewsMaterial> queryWrapper = new LambdaQueryWrapper<>();
queryWrapper.eq(WmNewsMaterial::getNewsId, wmNews.getId());
wmNewsMaterialMapper.delete(queryWrapper);
updateById(wmNews);
}
}
}

image-20231029220319010

4.4 文章的审核功能(未实现)

4.4.1 文章审核功能介绍

调用第三方的接口(阿里云内容安全审核接口)实现审核功能

功能介绍

image-20231029220622019

image-20231029220707636

审核流程

image-20231029221745020

1 自媒体端发布文章后,开始审核文章

2 审核的主要是审核文章的内容(文本内容和图片)

3 借助第三方提供的接口审核文本

4 借助第三方提供的接口审核图片,由于图片存储到minIO中,需要先下载才能审核

5 如果审核失败,则需要修改自媒体文章的状态,status:2 审核失败 status:3 转到人工审核

6 如果审核成功,则需要在文章微服务中创建app端需要的文章

4.4.2 调用第三方的审核接口

第三方审核接口

1.内容安全接口介绍:

内容安全是识别服务,支持对图片、视频、文本、语音等对象进行多样化场景检测,有效降低内容违规风险。目前很多平台都支持内容检测,如阿里云、腾讯云、百度AI、网易云等国内大型互联网公司都对外提供了API。

2.文件检测和图片检测api文档

文本垃圾内容Java SDK: https://help.aliyun.com/document_detail/53427.html?spm=a2c4g.11186623.6.717.466d7544QbU8Lr

图片垃圾内容Java SDK: https://help.aliyun.com/document_detail/53424.html?spm=a2c4g.11186623.6.715.c8f69b12ey35j4

项目中集成阿里云内容安全接口

1.依赖导入

<dependency>
<groupId>com.aliyun</groupId>
<artifactId>aliyun-java-sdk-core</artifactId>
</dependency>
<dependency>
<groupId>com.aliyun</groupId>
<artifactId>aliyun-java-sdk-green</artifactId>
</dependency>

2.相关的工具类

GreenImageScan(图片审核的工具类)

package com.heima.common.aliyun;

import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.JSONArray;
import com.alibaba.fastjson.JSONObject;
import com.aliyuncs.DefaultAcsClient;
import com.aliyuncs.IAcsClient;
import com.aliyuncs.green.model.v20180509.ImageSyncScanRequest;
import com.aliyuncs.http.FormatType;
import com.aliyuncs.http.HttpResponse;
import com.aliyuncs.http.MethodType;
import com.aliyuncs.http.ProtocolType;
import com.aliyuncs.profile.DefaultProfile;
import com.aliyuncs.profile.IClientProfile;
import com.heima.common.aliyun.util.ClientUploader;
import lombok.Getter;
import lombok.Setter;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.context.annotation.PropertySource;
import org.springframework.stereotype.Component;

import java.util.*;

@Getter
@Setter
@Component
@ConfigurationProperties(prefix = "aliyun")
public class GreenImageScan {

private String accessKeyId;
private String secret;
private String scenes;

public Map imageScan(List<byte[]> imageList) throws Exception {
IClientProfile profile = DefaultProfile
.getProfile("cn-shanghai", accessKeyId, secret);
DefaultProfile
.addEndpoint("cn-shanghai", "cn-shanghai", "Green", "green.cn-shanghai.aliyuncs.com");
IAcsClient client = new DefaultAcsClient(profile);
ImageSyncScanRequest imageSyncScanRequest = new ImageSyncScanRequest();
// 指定api返回格式
imageSyncScanRequest.setAcceptFormat(FormatType.JSON);
// 指定请求方法
imageSyncScanRequest.setMethod(MethodType.POST);
imageSyncScanRequest.setEncoding("utf-8");
//支持http和https
imageSyncScanRequest.setProtocol(ProtocolType.HTTP);
JSONObject httpBody = new JSONObject();
/**
* 设置要检测的场景, 计费是按照该处传递的场景进行
* 一次请求中可以同时检测多张图片,每张图片可以同时检测多个风险场景,计费按照场景计算
* 例如:检测2张图片,场景传递porn、terrorism,计费会按照2张图片鉴黄,2张图片暴恐检测计算
* porn: porn表示色情场景检测
*/

httpBody.put("scenes", Arrays.asList(scenes.split(",")));

/**
* 如果您要检测的文件存于本地服务器上,可以通过下述代码片生成url
* 再将返回的url作为图片地址传递到服务端进行检测
*/
/**
* 设置待检测图片, 一张图片一个task
* 多张图片同时检测时,处理的时间由最后一个处理完的图片决定
* 通常情况下批量检测的平均rt比单张检测的要长, 一次批量提交的图片数越多,rt被拉长的概率越高
* 这里以单张图片检测作为示例, 如果是批量图片检测,请自行构建多个task
*/
ClientUploader clientUploader = ClientUploader.getImageClientUploader(profile, false);
String url = null;
List<JSONObject> urlList = new ArrayList<JSONObject>();
for (byte[] bytes : imageList) {
url = clientUploader.uploadBytes(bytes);
JSONObject task = new JSONObject();
task.put("dataId", UUID.randomUUID().toString());
//设置图片链接为上传后的url
task.put("url", url);
task.put("time", new Date());
urlList.add(task);
}
httpBody.put("tasks", urlList);
imageSyncScanRequest.setHttpContent(org.apache.commons.codec.binary.StringUtils.getBytesUtf8(httpBody.toJSONString()),
"UTF-8", FormatType.JSON);
/**
* 请设置超时时间, 服务端全链路处理超时时间为10秒,请做相应设置
* 如果您设置的ReadTimeout小于服务端处理的时间,程序中会获得一个read timeout异常
*/
imageSyncScanRequest.setConnectTimeout(3000);
imageSyncScanRequest.setReadTimeout(10000);
HttpResponse httpResponse = null;
try {
httpResponse = client.doAction(imageSyncScanRequest);
} catch (Exception e) {
e.printStackTrace();
}

Map<String, String> resultMap = new HashMap<>();

//服务端接收到请求,并完成处理返回的结果
if (httpResponse != null && httpResponse.isSuccess()) {
JSONObject scrResponse = JSON.parseObject(org.apache.commons.codec.binary.StringUtils.newStringUtf8(httpResponse.getHttpContent()));
System.out.println(JSON.toJSONString(scrResponse, true));
int requestCode = scrResponse.getIntValue("code");
//每一张图片的检测结果
JSONArray taskResults = scrResponse.getJSONArray("data");
if (200 == requestCode) {
for (Object taskResult : taskResults) {
//单张图片的处理结果
int taskCode = ((JSONObject) taskResult).getIntValue("code");
//图片要检测的场景的处理结果, 如果是多个场景,则会有每个场景的结果
JSONArray sceneResults = ((JSONObject) taskResult).getJSONArray("results");
if (200 == taskCode) {
for (Object sceneResult : sceneResults) {
String scene = ((JSONObject) sceneResult).getString("scene");
String label = ((JSONObject) sceneResult).getString("label");
String suggestion = ((JSONObject) sceneResult).getString("suggestion");
//根据scene和suggetion做相关处理
//do something
System.out.println("scene = [" + scene + "]");
System.out.println("suggestion = [" + suggestion + "]");
System.out.println("suggestion = [" + label + "]");
if (!suggestion.equals("pass")) {
resultMap.put("suggestion", suggestion);
resultMap.put("label", label);
return resultMap;
}
}

} else {
//单张图片处理失败, 原因视具体的情况详细分析
System.out.println("task process fail. task response:" + JSON.toJSONString(taskResult));
return null;
}
}
resultMap.put("suggestion","pass");
return resultMap;
} else {
/**
* 表明请求整体处理失败,原因视具体的情况详细分析
*/
System.out.println("the whole image scan request failed. response:" + JSON.toJSONString(scrResponse));
return null;
}
}
return null;
}
}

GreenTextScan(文字审核的工具类)

package com.heima.common.aliyun;

import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.JSONArray;
import com.alibaba.fastjson.JSONObject;
import com.aliyuncs.DefaultAcsClient;
import com.aliyuncs.IAcsClient;
import com.aliyuncs.exceptions.ClientException;
import com.aliyuncs.exceptions.ServerException;
import com.aliyuncs.green.model.v20180509.TextScanRequest;
import com.aliyuncs.http.FormatType;
import com.aliyuncs.http.HttpResponse;
import com.aliyuncs.profile.DefaultProfile;
import com.aliyuncs.profile.IClientProfile;
import lombok.Getter;
import lombok.Setter;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.context.annotation.PropertySource;
import org.springframework.stereotype.Component;

import java.util.*;

@Getter
@Setter
@Component
@ConfigurationProperties(prefix = "aliyun")
public class GreenTextScan {

private String accessKeyId;
private String secret;

public Map greeTextScan(String content) throws Exception {
System.out.println(accessKeyId);
IClientProfile profile = DefaultProfile
.getProfile("cn-shanghai", accessKeyId, secret);
DefaultProfile.addEndpoint("cn-shanghai", "cn-shanghai", "Green", "green.cn-shanghai.aliyuncs.com");
IAcsClient client = new DefaultAcsClient(profile);
TextScanRequest textScanRequest = new TextScanRequest();
textScanRequest.setAcceptFormat(FormatType.JSON); // 指定api返回格式
textScanRequest.setHttpContentType(FormatType.JSON);
textScanRequest.setMethod(com.aliyuncs.http.MethodType.POST); // 指定请求方法
textScanRequest.setEncoding("UTF-8");
textScanRequest.setRegionId("cn-shanghai");
List<Map<String, Object>> tasks = new ArrayList<Map<String, Object>>();
Map<String, Object> task1 = new LinkedHashMap<String, Object>();
task1.put("dataId", UUID.randomUUID().toString());
/**
* 待检测的文本,长度不超过10000个字符
*/
task1.put("content", content);
tasks.add(task1);
JSONObject data = new JSONObject();

/**
* 检测场景,文本垃圾检测传递:antispam
**/
data.put("scenes", Arrays.asList("antispam"));
data.put("tasks", tasks);
System.out.println(JSON.toJSONString(data, true));
textScanRequest.setHttpContent(data.toJSONString().getBytes("UTF-8"), "UTF-8", FormatType.JSON);
// 请务必设置超时时间
textScanRequest.setConnectTimeout(3000);
textScanRequest.setReadTimeout(6000);

Map<String, String> resultMap = new HashMap<>();
try {
HttpResponse httpResponse = client.doAction(textScanRequest);
if (httpResponse.isSuccess()) {
JSONObject scrResponse = JSON.parseObject(new String(httpResponse.getHttpContent(), "UTF-8"));
System.out.println(JSON.toJSONString(scrResponse, true));
if (200 == scrResponse.getInteger("code")) {
JSONArray taskResults = scrResponse.getJSONArray("data");
for (Object taskResult : taskResults) {
if (200 == ((JSONObject) taskResult).getInteger("code")) {
JSONArray sceneResults = ((JSONObject) taskResult).getJSONArray("results");
for (Object sceneResult : sceneResults) {
String scene = ((JSONObject) sceneResult).getString("scene");
String label = ((JSONObject) sceneResult).getString("label");
String suggestion = ((JSONObject) sceneResult).getString("suggestion");
System.out.println("suggestion = [" + label + "]");
if (!suggestion.equals("pass")) {
resultMap.put("suggestion", suggestion);
resultMap.put("label", label);
return resultMap;
}

}
} else {
return null;
}
}
resultMap.put("suggestion", "pass");
return resultMap;
} else {
return null;
}
} else {
return null;
}
} catch (ServerException e) {
e.printStackTrace();
} catch (ClientException e) {
e.printStackTrace();
} catch (Exception e) {
e.printStackTrace();
}
return null;
}

}

3.在heima-leadnews-wemedia中的nacos配置中心添加以下配置

aliyun:
accessKeyId: xxxxxxxxxxxxxxxxxxx
secret: xxxxxxxxxxxxxxxxxxxxxxxx
#aliyun.scenes=porn,terrorism,ad,qrcode,live,logo
scenes: terrorism #检测的场景

4.编写测试类测试

import com.heima.common.aliyun.GreenImageScan;
import com.heima.common.aliyun.GreenTextScan;
import com.heima.file.service.FileStorageService;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.test.context.junit4.SpringRunner;

import java.util.Arrays;
import java.util.Map;

@SpringBootTest(classes = WemediaApplication.class)
@RunWith(SpringRunner.class)
public class AliyunTest {

@Autowired
private GreenTextScan greenTextScan;

@Autowired
private GreenImageScan greenImageScan;

@Autowired
private FileStorageService fileStorageService;

@Test
public void testScanText() throws Exception {
Map map = greenTextScan.greeTextScan("我是一个好人,冰毒");
System.out.println(map);
}

@Test
public void testScanImage() throws Exception {
byte[] bytes = fileStorageService.downLoadFile("http://192.168.200.130:9000/leadnews/2021/04/26/ef3cbe458db249f7bd6fb4339e593e55.jpg");
Map map = greenImageScan.imageScan(Arrays.asList(bytes));
System.out.println(map);
}
}

4.4.3 分布式ID的实现

为什么使用分布式ID

image-20231116230439706

分布式ID的技术选型

image-20231116230714014

雪花算法的介绍

image-20231116230953909

mybatis-plus已经集成了雪花算法,完成以下两步即可在项目中集成雪花算法

第一:在实体类中的id上加入如下配置,指定类型为id_worker

@TableId(value = "id",type = IdType.ID_WORKER)
private Long id;

第二:在application.yml文件中配置数据中心id和机器id

mybatis-plus:
mapper-locations: classpath*:mapper/*.xml
# 设置别名包扫描路径,通过该属性可以给包中的类注册别名
type-aliases-package: com.heima.model.article.pojos
global-config:
datacenter-id: 1
workerId: 1

datacenter-id:数据中心id(取值范围:0-31) workerId:机器id(取值范围:0-31)

4.4.4 审核功能的具体实现

由于没有阿里云相关的ak和sk,所以本部分默认每篇文章的文字和图片都审核通过(中间会注释掉调用第三方审核接口的代码)

image-20231202174634986

service层代码实现

package com.heima.wemedia.service.impl;

import com.alibaba.fastjson.JSON;
import com.heima.apis.article.IArticleClient;
import com.heima.common.aliyun.GreenImageScan;
import com.heima.common.aliyun.GreenTextScan;
import com.heima.file.service.FileStorageService;
import com.heima.model.article.dtos.ArticleDto;
import com.heima.model.common.dtos.ResponseResult;
import com.heima.model.wemedia.pojos.WmChannel;
import com.heima.model.wemedia.pojos.WmNews;
import com.heima.model.wemedia.pojos.WmUser;
import com.heima.wemedia.mapper.WmChannelMapper;
import com.heima.wemedia.mapper.WmNewsMapper;
import com.heima.wemedia.mapper.WmUserMapper;
import com.heima.wemedia.service.WmNewsAutoScanService;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang.StringUtils;
import org.springframework.beans.BeanUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

import java.util.*;
import java.util.stream.Collectors;

/**
* @author Jason Gong
* @version 1.0
* @website https://jasonsgong.gitee.io
* @Date 2023/11/19
* @Description
*/
@Service
@Slf4j
@Transactional
public class WmNewsAutoScanServiceImpl implements WmNewsAutoScanService {

//涉及文本内容的审核,需要调用第三方的服务,这里我们直接设置成测试环境,跳过调用第三方服务校验的过程
@Value("${check.env}")
private String environment;

@Autowired
private WmNewsMapper wmNewsMapper;
@Autowired
private GreenTextScan greenTextScan;
@Autowired
private GreenImageScan greenImageScan;
@Autowired
private FileStorageService fileStorageService;
@Autowired
private IArticleClient iArticleClient;
@Autowired
private WmChannelMapper wmChannelMapper;
@Autowired
private WmUserMapper wmUserMapper;

@Override
public void autoScanWmNews(Integer id) {
//1.查询自媒体文章的信息
WmNews wmNews = wmNewsMapper.selectById(id);
//文章不存在的话,直接抛出异常
if (wmNews == null) {
throw new RuntimeException("文章不存在,无法审核!");
}
//判断文章是不是待审核的状态
if (wmNews.getStatus().equals(WmNews.Status.SUBMIT.getCode())) {
//2.抽取文章内容中的文字和图片信息
Map<String, Object> textAndImages = getTextAndImages(wmNews);
//3.调用阿里云的接口实现文章的审核功能
//审核文本内容
Boolean isPassText = checkText((String) textAndImages.get("content"), wmNews);
if (!isPassText) return;
//审核图片内容
Boolean isPassImage = checkImages((List<String>) textAndImages.get("images"), wmNews);
if (!isPassImage) return;
//4.审核成功,保存app端的相关文章数据
ResponseResult responseResult = saveAppArticle(wmNews);
if (!responseResult.getCode().equals(200)) {
throw new RuntimeException("文章审核-保存app端文章失败");
}
//回填article_id
wmNews.setArticleId((Long) responseResult.getData());
updateWmNews(wmNews, (short) 9, "审核成功");
}
}

/**
* 保存app端的相关文章数据
*
* @param wmNews 文章相关的信息
*/
private ResponseResult saveAppArticle(WmNews wmNews) {
ArticleDto dto = new ArticleDto();
BeanUtils.copyProperties(wmNews, dto);
//文章布局
dto.setLayout(wmNews.getType());
//频道
WmChannel wmChannel = wmChannelMapper.selectById(wmNews.getChannelId());
if (wmChannel != null) {
dto.setChannelName(wmChannel.getName());
}
//作者
dto.setAuthorId(wmNews.getUserId().longValue());
WmUser wmUser = wmUserMapper.selectById(wmNews.getUserId());
if (wmUser != null) {
dto.setAuthorName(wmUser.getName());
}
//设置文章的id
if (wmNews.getArticleId() != null) {
dto.setId(wmNews.getArticleId());
}
//设置创建时间
dto.setCreatedTime(new Date());
return iArticleClient.saveArticle(dto);
}

/**
* 审核文章的图片内容
*
* @param images 文章的图片信息
* @param wmNews 文章实体
* @return 是否通过校验
*/
private Boolean checkImages(List<String> images, WmNews wmNews) {
boolean flag = true;
if (images == null || images.size() == 0 || "test".equals(environment)) {
return true;
}
//下载图片
//去重
images = images.stream().distinct().collect(Collectors.toList());
//创建集合,用于存储下载的图片
ArrayList<byte[]> byteList = new ArrayList<>();
for (String image : images) {
byte[] bytes = fileStorageService.downLoadFile(image);
byteList.add(bytes);
}
//审核图片
//调用方法正式审核
try {
//调用阿里云的接口对文字进行审核
Map map = greenImageScan.imageScan(byteList);
//根据审核反馈的结果做出不同的处理
if (map != null) {
//审核失败
if (map.get("suggestion").equals("block")) {
updateWmNews(wmNews, (short) 2, "图片存在违规内容!");
flag = false;
}
//不确定,需要人工审核
if (map.get("suggestion").equals("review")) {
updateWmNews(wmNews, (short) 3, "图片存在违规内容!");
flag = false;
}
}
} catch (Exception e) {
flag = false;
e.printStackTrace();
throw new RuntimeException("图片审核时出现异常!");
}
return flag;
}

/**
* 审核文章的文字内容
*
* @param content 文章的文字信息
* @param wmNews 文章实体
* @return 是否通过审核
*/
private Boolean checkText(String content, WmNews wmNews) {
boolean flag = true;
if ((content + wmNews.getTitle()).length() == 0 || "test".equals(environment)) {
return true;
}
//调用方法正式审核
try {
//调用阿里云的接口对文字进行审核
Map map = greenTextScan.greeTextScan(content);
//根据审核反馈的结果做出不同的处理
if (map != null) {
//审核失败
if (map.get("suggestion").equals("block")) {
updateWmNews(wmNews, (short) 2, "文章中的文字信息出现违规内容!");
flag = false;
}
//不确定,需要人工审核
if (map.get("suggestion").equals("review")) {
updateWmNews(wmNews, (short) 3, "文章中的文字信息在审核时有不确定的内容!");
flag = false;
}
}
} catch (Exception e) {
flag = false;
e.printStackTrace();
throw new RuntimeException("文字审核时出现异常!");
}
return flag;
}

/**
* 修改文章审核相关信息的方法
*
* @param wmNews 文章实体
* @param status 文章的审核状态
* @param reason 审核不通过的原因
*/
public void updateWmNews(WmNews wmNews, short status, String reason) {
wmNews.setStatus(status);
wmNews.setReason(reason);
wmNewsMapper.updateById(wmNews);
}

/**
* 提取文章中的文字和图片信息
*
* @param wmNews 文章
* @return 图片和文字组成的集合
*/
private Map<String, Object> getTextAndImages(WmNews wmNews) {
//存储文字信息
StringBuilder stringBuilder = new StringBuilder();
//文章中图片信息组成的集合
ArrayList<String> images = new ArrayList<>();
//从文章中提取文字和图片信息
if (StringUtils.isNotBlank(wmNews.getContent())) {
List<Map> maps = JSON.parseArray(wmNews.getContent(), Map.class);
for (Map map : maps) {
if (map.get("type").equals("text")) {
stringBuilder.append(map.get("value"));
}
if (map.get("type").equals("image")) {
images.add((String) map.get("values"));
}
}
}
//保存文章的封面信息到图片的集合中
if (StringUtils.isNotBlank(wmNews.getImages())) {
String[] split = wmNews.getImages().split(",");
images.addAll(Arrays.asList(split));
}
//创建返回的结果
HashMap<String, Object> resultMap = new HashMap<>();
resultMap.put("content", stringBuilder.toString());
resultMap.put("images", images);
return resultMap;
}
}

单元测试

package com.heima.wemedia.service;

import com.heima.wemedia.WemediaApplication;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.test.context.junit4.SpringRunner;

/**
* @author Jason Gong
* @version 1.0
* @Date 2023/12/2
* @Description
*/
@SpringBootTest(classes = WemediaApplication.class)
@RunWith(SpringRunner.class)
public class WmNewsAutoScanServiceTest {

@Autowired
private WmNewsAutoScanService wmNewsAutoScanService;

@Test
public void autoScanWmNews() {
wmNewsAutoScanService.autoScanWmNews(3);
}
}

image-20231202232643073

实现步骤:

①:在heima-leadnews-feign-api编写降级逻辑

package com.heima.apis.article.fallback;

import com.heima.apis.article.IArticleClient;
import com.heima.model.article.dtos.ArticleDto;
import com.heima.model.common.dtos.ResponseResult;
import com.heima.model.common.enums.AppHttpCodeEnum;
import org.springframework.stereotype.Component;

/**
* feign失败配置
* @author itheima
*/
@Component
public class IArticleClientFallback implements IArticleClient {
@Override
public ResponseResult saveArticle(ArticleDto dto) {
return ResponseResult.errorResult(AppHttpCodeEnum.SERVER_ERROR,"获取数据失败");
}
}

在自媒体微服务中添加类,扫描降级代码类的包

package com.heima.wemedia.config;

import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;

@Configuration
@ComponentScan("com.heima.apis.article.fallback")
public class InitConfig {
}

②:远程接口中指向降级代码

package com.heima.apis.article;

import com.heima.apis.article.fallback.IArticleClientFallback;
import com.heima.model.article.dtos.ArticleDto;
import com.heima.model.common.dtos.ResponseResult;
import org.springframework.cloud.openfeign.FeignClient;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;

@FeignClient(value = "leadnews-article",fallback = IArticleClientFallback.class)
public interface IArticleClient {

@PostMapping("/api/v1/article/save")
public ResponseResult saveArticle(@RequestBody ArticleDto dto);
}

③:客户端开启降级heima-leadnews-wemedia

在wemedia的nacos配置中心里添加如下内容,开启服务降级,也可以指定服务响应的超时的时间

feign:
# 开启feign对hystrix熔断降级的支持
hystrix:
enabled: true
# 修改调用超时时间
client:
config:
default:
connectTimeout: 2000
readTimeout: 2000

④:测试

在ApArticleServiceImpl类中saveArticle方法添加代码

try {
Thread.sleep(3000);
} catch (InterruptedException e) {
e.printStackTrace();
}

在自媒体端进行审核测试,会出现服务降级的现象

4.5 app端文章保存功能

实现思路

在文章审核成功以后需要在app的article库中新增文章数据

1.保存文章信息 ap_article

2.保存文章配置信息 ap_article_config

3.保存文章内容 ap_article_content

image-20231117221219911

保存文章的接口

说明
接口路径 /api/v1/article/save
请求方式 POST
参数 ArticleDto
响应结果 ResponseResult

ArticleDto

package com.heima.model.article.dtos;

import com.heima.model.article.pojos.ApArticle;
import lombok.Data;

@Data
public class ArticleDto extends ApArticle {
/**
* 文章内容
*/
private String content;
}

成功:

{
"code": 200,
"errorMessage" : "操作成功",
"data":"1302864436297442242"
}

失败:

{
"code":501,
"errorMessage":"参数失效",
}
{
"code":501,
"errorMessage":"文章没有找到",
}

实现步骤

image-20231117221333422

功能实现:

①:在heima-leadnews- feign-api中新增接口

第一:线导入feign的依赖

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

第二:定义文章端的接口

package com.heima.apis.article;

import com.heima.model.article.dtos.ArticleDto;
import com.heima.model.common.dtos.ResponseResult;
import org.springframework.cloud.openfeign.FeignClient;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;

import java.io.IOException;


@FeignClient(value = "leadnews-article")
public interface IArticleClient {

@PostMapping("/api/v1/article/save")
public ResponseResult saveArticle(@RequestBody ArticleDto dto) ;
}

②:在heima-leadnews-article中实现该方法

package com.heima.article.feign;

import com.heima.apis.article.IArticleClient;
import com.heima.article.service.ApArticleService;
import com.heima.model.article.dtos.ArticleDto;
import com.heima.model.common.dtos.ResponseResult;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;

import java.io.IOException;

@RestController
public class ArticleClient implements IArticleClient {

@Autowired
private ApArticleService apArticleService;

@Override
@PostMapping("/api/v1/article/save")
public ResponseResult saveArticle(@RequestBody ArticleDto dto) {
return apArticleService.saveArticle(dto);
}

}

③:拷贝mapper

在资料文件夹中拷贝ApArticleConfigMapper类到mapper文件夹中

同时,修改ApArticleConfig类,添加如下构造函数

package com.heima.model.article.pojos;

import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableField;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import lombok.Data;
import lombok.NoArgsConstructor;

import java.io.Serializable;

/**
* <p>
* APP已发布文章配置表
* </p>
*
* @author itheima
*/

@Data
@NoArgsConstructor
@TableName("ap_article_config")
public class ApArticleConfig implements Serializable {


public ApArticleConfig(Long articleId){
this.articleId = articleId;
this.isComment = true;
this.isForward = true;
this.isDelete = false;
this.isDown = false;
}

@TableId(value = "id",type = IdType.ID_WORKER)
private Long id;

/**
* 文章id
*/
@TableField("article_id")
private Long articleId;

/**
* 是否可评论
* true: 可以评论 1
* false: 不可评论 0
*/
@TableField("is_comment")
private Boolean isComment;

/**
* 是否转发
* true: 可以转发 1
* false: 不可转发 0
*/
@TableField("is_forward")
private Boolean isForward;

/**
* 是否下架
* true: 下架 1
* false: 没有下架 0
*/
@TableField("is_down")
private Boolean isDown;

/**
* 是否已删除
* true: 删除 1
* false: 没有删除 0
*/
@TableField("is_delete")
private Boolean isDelete;
}

④:在ApArticleService中新增方法

/**
* 保存app端相关文章
* @param dto
* @return
*/
ResponseResult saveArticle(ArticleDto dto) ;

实现类:

@Autowired
private ApArticleConfigMapper apArticleConfigMapper;

@Autowired
private ApArticleContentMapper apArticleContentMapper;

/**
* 保存app端相关文章
* @param dto
* @return
*/
@Override
public ResponseResult saveArticle(ArticleDto dto) {
//1.检查参数
if(dto == null){
return ResponseResult.errorResult(AppHttpCodeEnum.PARAM_INVALID);
}

ApArticle apArticle = new ApArticle();
BeanUtils.copyProperties(dto,apArticle);

//2.判断是否存在id
if(dto.getId() == null){
//2.1 不存在id 保存 文章 文章配置 文章内容

//保存文章
save(apArticle);

//保存配置
ApArticleConfig apArticleConfig = new ApArticleConfig(apArticle.getId());
apArticleConfigMapper.insert(apArticleConfig);

//保存 文章内容
ApArticleContent apArticleContent = new ApArticleContent();
apArticleContent.setArticleId(apArticle.getId());
apArticleContent.setContent(dto.getContent());
apArticleContentMapper.insert(apArticleContent);

}else {
//2.2 存在id 修改 文章 文章内容

//修改 文章
updateById(apArticle);

//修改文章内容
ApArticleContent apArticleContent = apArticleContentMapper.selectOne(Wrappers.<ApArticleContent>lambdaQuery().eq(ApArticleContent::getArticleId, dto.getId()));
apArticleContent.setContent(dto.getContent());
apArticleContentMapper.updateById(apArticleContent);
}


//3.结果返回 文章的id
return ResponseResult.okResult(apArticle.getId());
}

⑤:测试

编写junit单元测试,或使用postman进行测试

{
"title":"黑马头条项目背景",
"authoId":1102,
"layout":1,
"labels":"黑马头条",
"publishTime":"2028-03-14T11:35:49.000Z",
"images": "http://192.168.200.130:9000/leadnews/2021/04/26/5ddbdb5c68094ce393b08a47860da275.jpg",
"content":"黑马头条项目背景,黑马头条项目背景,黑马头条项目背景,黑马头条项目背景,黑马头条项目背景"
}

image-20231119212628556

4.6 发布文章提交审核集成

4.6.1 同步调用与异步调用

image-20231202234439362

4.6.2 Springboot集成异步线程调用

①:在自动审核的方法上加上@Async注解(标明要异步调用)

@Override
@Async //标明当前方法是一个异步方法
public void autoScanWmNews(Integer id) {
//代码略
}

②:在文章发布成功后调用审核的方法

@Autowired
private WmNewsAutoScanService wmNewsAutoScanService;

/**
* 发布修改文章或保存为草稿
* @param dto
* @return
*/
@Override
public ResponseResult submitNews(WmNewsDto dto) {

//代码略

//审核文章
wmNewsAutoScanService.autoScanWmNews(wmNews.getId());

return ResponseResult.okResult(AppHttpCodeEnum.SUCCESS);

}

③:在自媒体引导类中使用@EnableAsync注解开启异步调用

@SpringBootApplication
@EnableDiscoveryClient
@MapperScan("com.heima.wemedia.mapper")
@EnableFeignClients(basePackages = "com.heima.apis")
@EnableAsync //开启异步调用
public class WemediaApplication {

public static void main(String[] args) {
SpringApplication.run(WemediaApplication.class,args);
}

@Bean
public MybatisPlusInterceptor mybatisPlusInterceptor() {
MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor();
interceptor.addInnerInterceptor(new PaginationInnerInterceptor(DbType.MYSQL));
return interceptor;
}
}

4.7 文章审核功能-综合测试

4.7.1 服务启动列表

1,nacos服务端

2,article微服务

3,wemedia微服务

4,启动wemedia网关微服务

5,启动前端系统wemedia

4.7.2 测试情况列表

1,自媒体前端发布一篇正常的文章

审核成功后,app端的article相关数据是否可以正常保存,自媒体文章状态和app端文章id是否回显

2,自媒体前端发布一篇包含敏感词的文章

正常是审核失败, wm_news表中的状态是否改变,成功和失败原因正常保存

3,自媒体前端发布一篇包含敏感图片的文章

正常是审核失败, wm_news表中的状态是否改变,成功和失败原因正常保存

image-20231202235800149

4.8 自管理敏感词过滤

4.8.1 需求

image-20231203000259877

4.8.2 可选方案

方案 说明
数据库模糊查询 效率太低
String.indexOf(“”)查找 数据库量大的话也是比较慢
全文检索 分词再匹配
DFA算法 确定有穷自动机(一种数据结构)

4.8.3 DFA算法

image-20231203130434677

image-20231203130929391

4.8.4 工具类

package com.heima.utils.common;


import java.util.*;

public class SensitiveWordUtil {

public static Map<String, Object> dictionaryMap = new HashMap<>();


/**
* 生成关键词字典库
* @param words
* @return
*/
public static void initMap(Collection<String> words) {
if (words == null) {
System.out.println("敏感词列表不能为空");
return ;
}

// map初始长度words.size(),整个字典库的入口字数(小于words.size(),因为不同的词可能会有相同的首字)
Map<String, Object> map = new HashMap<>(words.size());
// 遍历过程中当前层次的数据
Map<String, Object> curMap = null;
Iterator<String> iterator = words.iterator();

while (iterator.hasNext()) {
String word = iterator.next();
curMap = map;
int len = word.length();
for (int i =0; i < len; i++) {
// 遍历每个词的字
String key = String.valueOf(word.charAt(i));
// 当前字在当前层是否存在, 不存在则新建, 当前层数据指向下一个节点, 继续判断是否存在数据
Map<String, Object> wordMap = (Map<String, Object>) curMap.get(key);
if (wordMap == null) {
// 每个节点存在两个数据: 下一个节点和isEnd(是否结束标志)
wordMap = new HashMap<>(2);
wordMap.put("isEnd", "0");
curMap.put(key, wordMap);
}
curMap = wordMap;
// 如果当前字是词的最后一个字,则将isEnd标志置1
if (i == len -1) {
curMap.put("isEnd", "1");
}
}
}

dictionaryMap = map;
}

/**
* 搜索文本中某个文字是否匹配关键词
* @param text
* @param beginIndex
* @return
*/
private static int checkWord(String text, int beginIndex) {
if (dictionaryMap == null) {
throw new RuntimeException("字典不能为空");
}
boolean isEnd = false;
int wordLength = 0;
Map<String, Object> curMap = dictionaryMap;
int len = text.length();
// 从文本的第beginIndex开始匹配
for (int i = beginIndex; i < len; i++) {
String key = String.valueOf(text.charAt(i));
// 获取当前key的下一个节点
curMap = (Map<String, Object>) curMap.get(key);
if (curMap == null) {
break;
} else {
wordLength ++;
if ("1".equals(curMap.get("isEnd"))) {
isEnd = true;
}
}
}
if (!isEnd) {
wordLength = 0;
}
return wordLength;
}

/**
* 获取匹配的关键词和命中次数
* @param text
* @return
*/
public static Map<String, Integer> matchWords(String text) {
Map<String, Integer> wordMap = new HashMap<>();
int len = text.length();
for (int i = 0; i < len; i++) {
int wordLength = checkWord(text, i);
if (wordLength > 0) {
String word = text.substring(i, i + wordLength);
// 添加关键词匹配次数
if (wordMap.containsKey(word)) {
wordMap.put(word, wordMap.get(word) + 1);
} else {
wordMap.put(word, 1);
}

i += wordLength - 1;
}
}
return wordMap;
}

//测试工具类
public static void main(String[] args) {
List<String> list = new ArrayList<>();
list.add("法轮");
list.add("法轮功");
list.add("冰毒");
//初始hua敏感词库
initMap(list);
String content="我是一个好人,并不会卖冰毒,也不操练法轮功,我真的不卖冰毒";
Map<String, Integer> map = matchWords(content);
System.out.println(map);
}
}

4.8.5 项目中集成自管理敏感词过滤

①:创建敏感词表,导入资料中wm_sensitive到leadnews_wemedia库中,并创建对应的实体类

package com.heima.model.wemedia.pojos;

import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableField;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import lombok.Data;

import java.io.Serializable;
import java.util.Date;

/**
* <p>
* 敏感词信息表
* </p>
*
* @author itheima
*/
@Data
@TableName("wm_sensitive")
public class WmSensitive implements Serializable {

private static final long serialVersionUID = 1L;

/**
* 主键
*/
@TableId(value = "id", type = IdType.AUTO)
private Integer id;

/**
* 敏感词
*/
@TableField("sensitives")
private String sensitives;

/**
* 创建时间
*/
@TableField("created_time")
private Date createdTime;

}

②:拷贝对应的wm_sensitive的mapper到项目中

package com.heima.wemedia.mapper;

import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.heima.model.wemedia.pojos.WmSensitive;
import org.apache.ibatis.annotations.Mapper;


@Mapper
public interface WmSensitiveMapper extends BaseMapper<WmSensitive> {
}

③:在文章审核的代码中添加自管理敏感词审核

第一:在WmNewsAutoScanServiceImpl中的autoScanWmNews方法上添加如下代码

//从内容中提取纯文本内容和图片
//.....省略

//自管理的敏感词过滤
boolean isSensitive = handleSensitiveScan((String) textAndImages.get("content"), wmNews);
if(!isSensitive) return;

//2.审核文本内容 阿里云接口
//.....省略

新增自管理敏感词审核代码

@Autowired
private WmSensitiveMapper wmSensitiveMapper;

/**
* 自管理的敏感词审核
* @param content
* @param wmNews
* @return
*/
private boolean handleSensitiveScan(String content, WmNews wmNews) {

boolean flag = true;

//获取所有的敏感词
List<WmSensitive> wmSensitives = wmSensitiveMapper.selectList(Wrappers.<WmSensitive>lambdaQuery().select(WmSensitive::getSensitives));
List<String> sensitiveList = wmSensitives.stream().map(WmSensitive::getSensitives).collect(Collectors.toList());

//初始化敏感词库
SensitiveWordUtil.initMap(sensitiveList);

//查看文章中是否包含敏感词
Map<String, Integer> map = SensitiveWordUtil.matchWords(content);
if(map.size() >0){
updateWmNews(wmNews,(short) 2,"当前文章中存在违规内容"+map);
flag = false;
}

return flag;
}

image-20231203161902523

4.9 图片识别文字审核敏感词

详细教程: https://jasonsgong.gitee.io/posts/58456.html

①:在heima-leadnews-common中创建工具类,简单封装一下tess4j

需要先导入pom

<dependency>
<groupId>net.sourceforge.tess4j</groupId>
<artifactId>tess4j</artifactId>
<version>4.1.1</version>
</dependency>

工具类

package com.heima.common.tess4j;

import lombok.Getter;
import lombok.Setter;
import net.sourceforge.tess4j.ITesseract;
import net.sourceforge.tess4j.Tesseract;
import net.sourceforge.tess4j.TesseractException;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.stereotype.Component;

import java.awt.image.BufferedImage;

@Getter
@Setter
@Component
@ConfigurationProperties(prefix = "tess4j")
public class Tess4jClient {

private String dataPath;
private String language;

public String doOCR(BufferedImage image) throws TesseractException {
//创建Tesseract对象
ITesseract tesseract = new Tesseract();
//设置字体库路径
tesseract.setDatapath(dataPath);
//中文识别
tesseract.setLanguage(language);
//执行ocr识别
String result = tesseract.doOCR(image);
//替换回车和tal键 使结果为一行
result = result.replaceAll("\\r|\\n", "-").replaceAll(" ", "");
return result;
}

}

在spring.factories配置中添加该类,完整如下:

org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
com.heima.common.exception.ExceptionCatch,\
com.heima.common.swagger.SwaggerConfiguration,\
com.heima.common.swagger.Swagger2Configuration,\
com.heima.common.aliyun.GreenTextScan,\
com.heima.common.aliyun.GreenImageScan,\
com.heima.common.tess4j.Tess4jClient

②:在heima-leadnews-wemedia中的配置中添加两个属性

tess4j:
data-path: D:\workspace\tessdata
language: chi_sim

③:在WmNewsAutoScanServiceImpl中的handleImageScan方法上添加如下代码

try {
for (String image : images) {
byte[] bytes = fileStorageService.downLoadFile(image);

//图片识别文字审核---begin-----

//从byte[]转换为butteredImage
ByteArrayInputStream in = new ByteArrayInputStream(bytes);
BufferedImage imageFile = ImageIO.read(in);
//识别图片的文字
String result = tess4jClient.doOCR(imageFile);

//审核是否包含自管理的敏感词
boolean isSensitive = handleSensitiveScan(result, wmNews);
if(!isSensitive){
return isSensitive;
}

//图片识别文字审核---end-----


imageList.add(bytes);

}
}catch (Exception e){
e.printStackTrace();
}

最后附上文章审核的完整代码如下:

package com.heima.wemedia.service.impl;

import com.alibaba.fastjson.JSONArray;
import com.baomidou.mybatisplus.core.toolkit.Wrappers;
import com.heima.apis.article.IArticleClient;
import com.heima.common.aliyun.GreenImageScan;
import com.heima.common.aliyun.GreenTextScan;
import com.heima.common.tess4j.Tess4jClient;
import com.heima.file.service.FileStorageService;
import com.heima.model.article.dtos.ArticleDto;
import com.heima.model.common.dtos.ResponseResult;
import com.heima.model.wemedia.pojos.WmChannel;
import com.heima.model.wemedia.pojos.WmNews;
import com.heima.model.wemedia.pojos.WmSensitive;
import com.heima.model.wemedia.pojos.WmUser;
import com.heima.utils.common.SensitiveWordUtil;
import com.heima.wemedia.mapper.WmChannelMapper;
import com.heima.wemedia.mapper.WmNewsMapper;
import com.heima.wemedia.mapper.WmSensitiveMapper;
import com.heima.wemedia.mapper.WmUserMapper;
import com.heima.wemedia.service.WmNewsAutoScanService;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.springframework.beans.BeanUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.scheduling.annotation.Async;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

import javax.imageio.ImageIO;
import java.awt.image.BufferedImage;
import java.io.ByteArrayInputStream;
import java.util.*;
import java.util.stream.Collectors;


@Service
@Slf4j
@Transactional
public class WmNewsAutoScanServiceImpl implements WmNewsAutoScanService {

@Autowired
private WmNewsMapper wmNewsMapper;

/**
* 自媒体文章审核
*
* @param id 自媒体文章id
*/
@Override
@Async //标明当前方法是一个异步方法
public void autoScanWmNews(Integer id) {

// int a = 1/0;

//1.查询自媒体文章
WmNews wmNews = wmNewsMapper.selectById(id);
if (wmNews == null) {
throw new RuntimeException("WmNewsAutoScanServiceImpl-文章不存在");
}

if (wmNews.getStatus().equals(WmNews.Status.SUBMIT.getCode())) {
//从内容中提取纯文本内容和图片
Map<String, Object> textAndImages = handleTextAndImages(wmNews);

//自管理的敏感词过滤
boolean isSensitive = handleSensitiveScan((String) textAndImages.get("content"), wmNews);
if(!isSensitive) return;

//2.审核文本内容 阿里云接口
boolean isTextScan = handleTextScan((String) textAndImages.get("content"), wmNews);
if (!isTextScan) return;

//3.审核图片 阿里云接口
boolean isImageScan = handleImageScan((List<String>) textAndImages.get("images"), wmNews);
if (!isImageScan) return;

//4.审核成功,保存app端的相关的文章数据
ResponseResult responseResult = saveAppArticle(wmNews);
if (!responseResult.getCode().equals(200)) {
throw new RuntimeException("WmNewsAutoScanServiceImpl-文章审核,保存app端相关文章数据失败");
}
//回填article_id
wmNews.setArticleId((Long) responseResult.getData());
updateWmNews(wmNews, (short) 9, "审核成功");

}
}

@Autowired
private WmSensitiveMapper wmSensitiveMapper;

/**
* 自管理的敏感词审核
* @param content
* @param wmNews
* @return
*/
private boolean handleSensitiveScan(String content, WmNews wmNews) {

boolean flag = true;

//获取所有的敏感词
List<WmSensitive> wmSensitives = wmSensitiveMapper.selectList(Wrappers.<WmSensitive>lambdaQuery().select(WmSensitive::getSensitives));
List<String> sensitiveList = wmSensitives.stream().map(WmSensitive::getSensitives).collect(Collectors.toList());

//初始化敏感词库
SensitiveWordUtil.initMap(sensitiveList);

//查看文章中是否包含敏感词
Map<String, Integer> map = SensitiveWordUtil.matchWords(content);
if(map.size() >0){
updateWmNews(wmNews,(short) 2,"当前文章中存在违规内容"+map);
flag = false;
}

return flag;
}

@Autowired
private IArticleClient articleClient;

@Autowired
private WmChannelMapper wmChannelMapper;

@Autowired
private WmUserMapper wmUserMapper;

/**
* 保存app端相关的文章数据
*
* @param wmNews
*/
private ResponseResult saveAppArticle(WmNews wmNews) {

ArticleDto dto = new ArticleDto();
//属性的拷贝
BeanUtils.copyProperties(wmNews, dto);
//文章的布局
dto.setLayout(wmNews.getType());
//频道
WmChannel wmChannel = wmChannelMapper.selectById(wmNews.getChannelId());
if (wmChannel != null) {
dto.setChannelName(wmChannel.getName());
}

//作者
dto.setAuthorId(wmNews.getUserId().longValue());
WmUser wmUser = wmUserMapper.selectById(wmNews.getUserId());
if (wmUser != null) {
dto.setAuthorName(wmUser.getName());
}

//设置文章id
if (wmNews.getArticleId() != null) {
dto.setId(wmNews.getArticleId());
}
dto.setCreatedTime(new Date());

ResponseResult responseResult = articleClient.saveArticle(dto);
return responseResult;

}


@Autowired
private FileStorageService fileStorageService;

@Autowired
private GreenImageScan greenImageScan;

@Autowired
private Tess4jClient tess4jClient;

/**
* 审核图片
*
* @param images
* @param wmNews
* @return
*/
private boolean handleImageScan(List<String> images, WmNews wmNews) {

boolean flag = true;

if (images == null || images.size() == 0) {
return flag;
}

//下载图片 minIO
//图片去重
images = images.stream().distinct().collect(Collectors.toList());

List<byte[]> imageList = new ArrayList<>();

try {
for (String image : images) {
byte[] bytes = fileStorageService.downLoadFile(image);

//图片识别文字审核---begin-----

//从byte[]转换为butteredImage
ByteArrayInputStream in = new ByteArrayInputStream(bytes);
BufferedImage imageFile = ImageIO.read(in);
//识别图片的文字
String result = tess4jClient.doOCR(imageFile);

//审核是否包含自管理的敏感词
boolean isSensitive = handleSensitiveScan(result, wmNews);
if(!isSensitive){
return isSensitive;
}

//图片识别文字审核---end-----


imageList.add(bytes);

}
}catch (Exception e){
e.printStackTrace();
}


//审核图片
try {
Map map = greenImageScan.imageScan(imageList);
if (map != null) {
//审核失败
if (map.get("suggestion").equals("block")) {
flag = false;
updateWmNews(wmNews, (short) 2, "当前文章中存在违规内容");
}

//不确定信息 需要人工审核
if (map.get("suggestion").equals("review")) {
flag = false;
updateWmNews(wmNews, (short) 3, "当前文章中存在不确定内容");
}
}

} catch (Exception e) {
flag = false;
e.printStackTrace();
}
return flag;
}

@Autowired
private GreenTextScan greenTextScan;

/**
* 审核纯文本内容
*
* @param content
* @param wmNews
* @return
*/
private boolean handleTextScan(String content, WmNews wmNews) {

boolean flag = true;

if ((wmNews.getTitle() + "-" + content).length() == 0) {
return flag;
}
try {
Map map = greenTextScan.greeTextScan((wmNews.getTitle() + "-" + content));
if (map != null) {
//审核失败
if (map.get("suggestion").equals("block")) {
flag = false;
updateWmNews(wmNews, (short) 2, "当前文章中存在违规内容");
}

//不确定信息 需要人工审核
if (map.get("suggestion").equals("review")) {
flag = false;
updateWmNews(wmNews, (short) 3, "当前文章中存在不确定内容");
}
}
} catch (Exception e) {
flag = false;
e.printStackTrace();
}

return flag;

}

/**
* 修改文章内容
*
* @param wmNews
* @param status
* @param reason
*/
private void updateWmNews(WmNews wmNews, short status, String reason) {
wmNews.setStatus(status);
wmNews.setReason(reason);
wmNewsMapper.updateById(wmNews);
}

/**
* 1。从自媒体文章的内容中提取文本和图片
* 2.提取文章的封面图片
*
* @param wmNews
* @return
*/
private Map<String, Object> handleTextAndImages(WmNews wmNews) {

//存储纯文本内容
StringBuilder stringBuilder = new StringBuilder();

List<String> images = new ArrayList<>();

//1。从自媒体文章的内容中提取文本和图片
if (StringUtils.isNotBlank(wmNews.getContent())) {
List<Map> maps = JSONArray.parseArray(wmNews.getContent(), Map.class);
for (Map map : maps) {
if (map.get("type").equals("text")) {
stringBuilder.append(map.get("value"));
}

if (map.get("type").equals("image")) {
images.add((String) map.get("value"));
}
}
}
//2.提取文章的封面图片
if (StringUtils.isNotBlank(wmNews.getImages())) {
String[] split = wmNews.getImages().split(",");
images.addAll(Arrays.asList(split));
}

Map<String, Object> resultMap = new HashMap<>();
resultMap.put("content", stringBuilder.toString());
resultMap.put("images", images);
return resultMap;

}
}

4.10 文章详情-静态文件生成

image-20231209210352188

实现步骤

1.新建ArticleFreemarkerService ,定义创建静态文件并上传到minIO中方法

package com.heima.article.service;

import com.heima.model.article.pojos.ApArticle;

public interface ArticleFreemarkerService {

/**
* 生成静态文件上传到minIO中
* @param apArticle
* @param content
*/
public void buildArticleToMinIO(ApArticle apArticle,String content);
}
package com.heima.article.service.Impl;

import com.alibaba.fastjson.JSONArray;
import com.heima.article.service.ApArticleService;
import com.heima.article.service.ArticleFreemarkerService;
import com.heima.file.service.FileStorageService;
import com.heima.model.article.pojos.ApArticle;
import freemarker.template.Configuration;
import freemarker.template.Template;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang.StringUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.scheduling.annotation.Async;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

import java.io.ByteArrayInputStream;
import java.io.InputStream;
import java.io.StringWriter;
import java.util.HashMap;

/**
* @author Jason Gong
* @version 1.0
* @Date 2023/12/9
* @Description
*/
@Service
@Slf4j
@Transactional
public class ArticleFreemarkerServiceImpl implements ArticleFreemarkerService {

@Autowired
private Configuration configuration;
@Autowired
private FileStorageService fileStorageService;
@Autowired
private ApArticleService apArticleService;

@Async //标记为一个异步调用的方法
@Override
public void buildArticleToMinio(ApArticle apArticle, String content) {
//已知文章的id
//1.获取文章的内容
if (StringUtils.isNotBlank(content)) {
StringWriter out = new StringWriter();
try {
//2.文章内容通过freemarker生成html文件 详细教程见:https://jasonsgong.gitee.io/posts/29367.html
Template template = configuration.getTemplate("article.ftl");
//构建数据模型
HashMap<String, Object> map = new HashMap<>();
map.put("content", JSONArray.parseArray(content));
//输出流
out = new StringWriter();
//合成html文件
template.process(map, out);
} catch (Exception e) {
e.printStackTrace();
}
//3.把html文件上传到minio中
//构建一个输入流
InputStream in = new ByteArrayInputStream(out.toString().getBytes());
//上传到minio并返回访问的路径
String path = fileStorageService.uploadHtmlFile("", apArticle.getId() + ".html", in);
log.info("文件在minio中的路径:" + path);
//4.修改ap_article表,保存static_url字段
apArticle.setStaticUrl(path);
boolean isSuccess = apArticleService.updateById(apArticle);
log.info(isSuccess ? "文件上传成功" : "文件上传失败");
}
}
}

2.在ApArticleService的saveArticle实现方法中添加调用生成文件的方法

/**
* 保存app端相关文章
* @param dto
* @return
*/
@Override
public ResponseResult saveArticle(ArticleDto dto) {

// try {
// Thread.sleep(3000);
// } catch (InterruptedException e) {
// e.printStackTrace();
// }
//1.检查参数
if(dto == null){
return ResponseResult.errorResult(AppHttpCodeEnum.PARAM_INVALID);
}

ApArticle apArticle = new ApArticle();
BeanUtils.copyProperties(dto,apArticle);

//2.判断是否存在id
if(dto.getId() == null){
//2.1 不存在id 保存 文章 文章配置 文章内容

//保存文章
save(apArticle);

//保存配置
ApArticleConfig apArticleConfig = new ApArticleConfig(apArticle.getId());
apArticleConfigMapper.insert(apArticleConfig);

//保存 文章内容
ApArticleContent apArticleContent = new ApArticleContent();
apArticleContent.setArticleId(apArticle.getId());
apArticleContent.setContent(dto.getContent());
apArticleContentMapper.insert(apArticleContent);

}else {
//2.2 存在id 修改 文章 文章内容

//修改 文章
updateById(apArticle);

//修改文章内容
ApArticleContent apArticleContent = apArticleContentMapper.selectOne(Wrappers.<ApArticleContent>lambdaQuery().eq(ApArticleContent::getArticleId, dto.getId()));
apArticleContent.setContent(dto.getContent());
apArticleContentMapper.updateById(apArticleContent);
}

//异步调用 生成静态文件上传到minio中
articleFreemarkerService.buildArticleToMinIO(apArticle,dto.getContent());


//3.结果返回 文章的id
return ResponseResult.okResult(apArticle.getId());
}

3.文章微服务开启异步调用

package com.heima.article;

import com.baomidou.mybatisplus.annotation.DbType;
import com.baomidou.mybatisplus.extension.plugins.MybatisPlusInterceptor;
import com.baomidou.mybatisplus.extension.plugins.inner.PaginationInnerInterceptor;
import org.mybatis.spring.annotation.MapperScan;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.client.discovery.EnableDiscoveryClient;
import org.springframework.context.annotation.Bean;
import org.springframework.scheduling.annotation.EnableAsync;


@SpringBootApplication
@EnableDiscoveryClient
@EnableAsync //开启异步调用
@MapperScan("com.heima.article.mapper")
public class ArticleApplication {

public static void main(String[] args) {
SpringApplication.run(ArticleApplication.class,args);
}

@Bean
public MybatisPlusInterceptor mybatisPlusInterceptor() {
MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor();
interceptor.addInnerInterceptor(new PaginationInnerInterceptor(DbType.MYSQL));
return interceptor;
}
}

测试

image-20231209214205338

image-20231209214309957

4.11 文章定时发布

]]>
后端 项目实战
项目实战-谷粒商城 /posts/45726.html 一.项目简介

1.项目背景

电商模式 谷粒商城使用的B2C模式,销售自营的商品给客户

市面上5种常见的电商模式B2B、B2C、C2B、C2C、O2O

1、B2B模式:
business to business,是指商家与商家建立的商业关系。如阿里巴巴。

2、B2C模式:
business to consumer,商对客模式。即通常说的商业零售,供应商直接把商品卖给用户。如:苏宁易购、京东、天猫、小米商城。

3、C2B模式:
customer to business,即消费者对企业。先有消费者需求而后有企业生产。例如众筹类的商城。

4、C2C模式:
customer to consumer,客户之间直接把东西放上网上去卖。如:淘宝、闲鱼。

5、O2O模式:
online to offline,线上线下。线上快速支付,线下优质服务。如:饿了么、美团、京东到家。

2.项目架构图

项目的架构图

微服务划分图

image-20230518015831413

3.项目技术和特色

  • 前后端分离开发,开发基于VUE的后台管理系统
  • SpringCloud全新解决方案
  • 应用监控、限流、网关、熔断降级等分布式方案 全方位涉及
  • 包含分布式事务、分布式锁等技术难点
  • 分析高并发场景的编码方式、线程池、异步编排等使用
  • 压力测试与性能优化
  • 各种集群技术的区别已经使用
  • CI/CD的使用

4.项目的前置要求

  • 熟悉SpringBoot以及常见的整合方案
  • 了解SpringCloud
  • 熟悉Maven git
  • 熟悉linux、redis、docker基本操作
  • 了解html、css、js、vue
  • 熟练使用idea开发项目

二.分布式基础概念

1.微服务

微服务架构风格,就像是一个单独的应用程序开发为一套小服务,每个服务运行在自己的进程中,并使用轻量级机制通信,通常是HTTP API。这些服务围绕业务能力来构建,并通过完全自动化部署机制来独立部署。这些服务使用不同的编程语言编写,以及不同的数据存储技术,并保持最低限度的集中式管理。

简而言之:拒绝大型单体应用,基于业务边界进行服务微化拆分,各个应用独立部署运行。

2.集群|分布式|节点

集群是一个物理形态 ,分布式是个工作方式

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

集群: 集群指的是将几台服务器集中在一起,实现同一业务

分布式中的每一个节点,都可以做集群。而集群不一定是分布式的。

节点: 集群中的一个服务器

3.远程调用

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

SpringCloud中使用HTTP+JSON的方式完成远程调用

image-20230518010847966

4.负载均衡

image-20230518011030343
分布式系统中,A服务需要调用B服务,B服务在多台机器上都存在,A调用任意一个服务均可完成功能。为了使每一个服务器都不要太忙或者太闲,我们可以使用负载均衡的调用每一个服务器,提升网站的健壮性。

常见的负载均衡算法:

轮询: 为第一个请求选择健康池中的第一台后端服务器,然后按顺序往后依次选择,直到最后一个,然后循环。

最小连接: 有限选择连接数量少,也就是压力最小的后端服务器,在会话较长的情况下可以考虑采取这种方式。

随机法:通过系统的随机算法,根据后端服务器的列表大小值来随机选取其中的一台服务器进行访问。由概率统计理论可以得知,随着客户端调用服务端的次数增多,其实际效果越来越接近于平均分配调用量到后端的每一台服务器,也就是轮询的结果。

源地址哈希法:源地址哈希的思想是根据获取客户端的IP地址,通过哈希函数计算得到的一个数值,用该数值对服务器列表的大小进行取模运算,得到的结果便是客服端要访问服务器的序号。采用源地址哈希法进行负载均衡,同一IP地址的客户端,当后端服务器列表不变时,它每次都会映射到同一台后端服务器进行访问。

加权轮询法:不同的后端服务器可能机器的配置和当前系统的负载并不相同,因此它们的抗压能力也不相同。给配置高、负载低的机器配置更高的权重,让其处理更多的请;而配置低、负载高的机器,给其分配较低的权重,降低其系统负载,加权轮询能很好地处理这一问题,并将请求顺序且按照权重分配到后端。

加权随机法:与加权轮询法一样,加权随机法也根据后端机器的配置,系统的负载分配不同的权重。不同的是,它是按照权重随机请求后端服务器,而非顺序。

5.服务注册|发现|注册中心

image-20230518012211042

6.配置中心

每一个服务最终会有大量的配置,并且每个服务都可能部署在多台机器上。我们经常需要变更配置,我们可以让每个服务在配置中心获取自己的配置。

image-20230518012538349

7.服务熔断和服务降级

在微服务的架构中,微服务之间通过网络进行通信,存在相互依赖,当其中的一个服务不可用时,有可能会造成雪崩效应。要防止这样的情况,必须要有容错机制来保护服务。

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

服务降级: 在运维期间,当系统处于高峰期,系统资源紧张,我们可以让非核心的业务降级运行。降级:某些业务不处理,或者简单处理[抛异常、返回NULL、调用mock数据、调用Fallback处理逻辑]。

image-20230518013254404

image-20230518013624395

8.API网关

在微服务的架构中,API Gateway作为整体架构的重要组件,他抽象了微服务中都需要的公共功能,同时提供了客户端的负载均衡,服务自动熔断,灰度发布、统一认证、限流流控、日志统计等丰富功能,帮助我们解决很多API管理难题。
image-20230518014732955

三.环境搭建

1.安装虚拟机

需要使用虚拟机安装相关的软件和部署集群

VMWare虚拟机安装Linux教程 | The Blog (gitee.io)

将Linxu的IP地址固定下来

Linux设置静态IP | The Blog (gitee.io)

2.Docker安装与配置

包含docker安装、开启开机自启动、配置镜像加速服务

Docker容器化技术 | The Blog (gitee.io)image-20230518141429712

image-20230518145456236

3.Docker安装mysql

#下载mysql的镜像文件 这里以下载mysql5.7为例
docker pull mysql:5.7

#查看是否下载成功
docker images

#使用MySQL的镜像启动一个容器
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/conf:/etc/mysql:将配置文件夹挂载到主机
#-v /mydata/mysql/log:/var/log/mysql:将日志文件夹挂载到主机
#-v /mydata/mysql/data:/var/lib/mysql/:将配置文件夹挂载到主机
#-e MYSQL_ROOT_PASSWORD=root:初始化 root 用户的密码

#查看是否运行成功 这时使用Navicat就可以连接上了 连接不上 关闭防火墙
docker ps #查看正在运行的容器

#进入容器的内部
docker exec -it mysql /bin/bash

#查看容器内部的目录结构 我们可以看到容器的内部是一个小小的linux系统
ls / #注意中间有一个空格

#查看与mysql相关的目录
whereis mysql

#退出容器
exit;

#在容器外部查看mysql挂载在本地的三个文件
#容器内部文件的变化会同步在这三个文件中 conf data log
#外部修改 也会同步到内部
cd /mydata/mysql
ls

#创建一个mysql的配置文件设置字符编码 将下面的内容粘贴到配置文件中
#1.创建配置文件
vi /mydata/mysql/conf/my.cnf
#2.将以下的内容粘贴到配置文件配置文件内容
[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
#3.重启MySQl的服务 让配置文件生效 这个配置文件也会同步到容器内部
docker restart mysql

#设置开机自启动
docker update mysql --restart=always

4.Docker安装redis

#下载redis的镜像文件 直接下载最新的
docker pull redis

#创建redis挂载在宿主机的配置文件和目录
mkdir -p /mydata/redis/conf #创建目录
touch /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 ps

#测试客户端的使用
docker exec -it redis redis-cli

#可以在redis客户端中使用如下命令
127.0.0.1:6379> set name tom #存值
OK
127.0.0.1:6379> get name #取值
"tom"

#redis中的数据不能持久化 关机重启之后数据会丢失
#通过以下的配置 实现redis的持久化操作 AOF的持久化
vi /mydata/redis/conf/redis.conf #修改之前创建的配置文件
#添加如下配置
appendonly yes

#重启redis 让配置生效
docker restart redis

#设置开机自启动
docker update redis --restart=always

5.统一开发环境

其中Java使用的是java8及以上

Maven 配置阿里云的镜像 profiles

开发环境的搭建 | The Blog (gitee.io)

IDEA中修改使用自己安装的Maven

IDEA插件:Lombok|MybatisX

VSCode插件:

image-20230518160444927

image-20230518155308167

6.配置Git

官网下载:https://git-scm.com/downloads

#以下的操作在下载安装完毕之后进行
#1.鼠标在桌面右键 选择Git Bash Here 打开控制台

#2.配置用户名和邮箱
git config --global user.name "用户名" #随意
git config --global user.email "邮箱" #自己的邮箱

#3.配置SSH免密连接
#生成密钥
ssh-keygen -t rsa -C "在码云上注册的邮箱地址" #连续三次回车
#查看密钥并复制公钥的内容
cat ~/.ssh/id_rsa.pub

#4.将密钥的复制到码云的SSH公钥中
#4.1添加公钥 公钥名随意 公钥内容就是上面复制的内容

#5.测试
ssh -T git@gitee.com

常用命令

image-20230518161145808

7.从Gitee上初始化一个项目

我们是先从码云上初始化一个项目 然后拉取到本地

创建一个仓库

image-20230518163703370

复制刚才创建仓库的地址

image-20230518163838162

将工程导入到IDEA中

image-20230518164005690

导入成功

image-20230518164217773

8.创建微服务模块

image-20230518171355588

9.数据库设计

image-20230518171546236

image-20230518173203605

10.后台管理系统的搭建

10.1.后端的搭建

后台管理系统使用的是人人开源提供的一个脚手架:

仓库的地址信息:

仓库主页:https://gitee.com/renrenio

后端renren-fast:https://gitee.com/renrenio/renren-fast.git

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

将其作为一个模块集成到gulimall中

image-20230518221222845

同时创建好这个脚手架需要的数据库

image-20230518221433314

修改配置文件中数据库账号密码ip的设置,启动项目看有没有报错

10.2 前端的搭建

安装node.js 和 npm包管理工具 (node.js自带npm包管理工具)

#检查是否安装成功
#出现版本号就是安装成功了
node -v
npm -v

#npm配置淘宝的镜像 下载依赖的速度更快
npm config set registry https://registry.npmmirror.com

将项目导入到vscode中,并安装依赖

#安装依赖的命令
npm install

image-20230518223021928

下载完成之后,运行前端的项目

#运行命令
npm run dev

运行成功过之后访问 http://localhost:8001

成功的页面显示如下

image-20230518223525151

**使用账号:admin密码:admin 登录 **

可以登录成功并显示以下的页面说明前后端的联调通过

image-20230518223921179

11.快速开发-逆向工程的搭建

1.代码生成器快速使用案例

人人开源代码生成器的地址:https://gitee.com/renrenio/renren-generator.git

集成代码生成器

image-20230518230016421

修改代码生成器的配置文件

1.修改yml配置文件中数据库的连接信息

2.修改properties中与代码生成相关的配置信息

配置举例 根据不同的模块 不同的数据库 配置不同

#代码生成器配置信息

mainPath=com.atguigu
#包名
package=com.atguigu.gulimall
moduleName=product
#作者
author=JasonGong
#Email
email=JasonGong@gmail.com
#表前缀(类名不会包含表前缀)
tablePrefix=pms_


#以下的配置信息一般不动
#类型转换配置信息
tinyint=Integer
smallint=Integer
mediumint=Integer
int=Integer
integer=Integer
bigint=Long
float=Float
double=Double
decimal=BigDecimal
bit=Boolean

char=String
varchar=String
tinytext=String
text=String
mediumtext=String
longtext=String


date=Date
datetime=Date
timestamp=Date

NUMBER=Integer
INT=Integer
INTEGER=Integer
BINARY_INTEGER=Integer
LONG=String
FLOAT=Float
BINARY_FLOAT=Float
DOUBLE=Double
BINARY_DOUBLE=Double
DECIMAL=BigDecimal
CHAR=String
VARCHAR=String
VARCHAR2=String
NVARCHAR=String
NVARCHAR2=String
CLOB=String
BLOB=String
DATE=Date
DATETIME=Date
TIMESTAMP=Date
TIMESTAMP(6)=Date

int8=Long
int4=Integer
int2=Integer
numeric=BigDecimal

nvarchar=String

3.运行项目

通过http://localhost:81 访问

image-20230518234030568

4.使用生成代码的功能生成代码

image-20230518234311272

解压之后的文件如下

image-20230518234941961

压缩包中的文件如下所示

image-20230518234922087

由于项目中没有使用shiro安全框架,我们注释掉安全框架的注解生成模板

image-20230519002146272

配置每个模块的配置文件

server:
port: 8080
spring:
datasource:
url: jdbc:mysql://192.168.111.100:3306/gulimall_pms?useUnicode=true&characterEncoding=UTF-8&useSSL=false&serverTimezone=Asia/Shanghai
username: root
password: root
driver-class-name: com.mysql.cj.jdbc.Driver
mybatis-plus:
mapper-locations: classpath*:/mapper/**/*.xml #定义mapper.xml文件的位置
global-config:
db-config:
id-type: auto #主键的生成规则

测试生成的代码

image-20230519005131468

2.代码生成器的使用步骤

1.修改代码生成器的配置文件generator.properties

#代码生成器配置信息

mainPath=com.atguigu
#包名
package=com.atguigu.gulimall
moduleName=ware #1.修改模块名
#作者
author=JasonGong
#Email
email=JasonGong@gmail.com
#表前缀(类名不会包含表前缀)
tablePrefix=wms_ #2.数据库表的前缀


#以下的配置信息一般不动
#类型转换配置信息
tinyint=Integer
smallint=Integer
mediumint=Integer
int=Integer
integer=Integer
bigint=Long
float=Float
double=Double
decimal=BigDecimal
bit=Boolean

char=String
varchar=String
tinytext=String
text=String
mediumtext=String
longtext=String


date=Date
datetime=Date
timestamp=Date

NUMBER=Integer
INT=Integer
INTEGER=Integer
BINARY_INTEGER=Integer
LONG=String
FLOAT=Float
BINARY_FLOAT=Float
DOUBLE=Double
BINARY_DOUBLE=Double
DECIMAL=BigDecimal
CHAR=String
VARCHAR=String
VARCHAR2=String
NVARCHAR=String
NVARCHAR2=String
CLOB=String
BLOB=String
DATE=Date
DATETIME=Date
TIMESTAMP=Date
TIMESTAMP(6)=Date

int8=Long
int4=Integer
int2=Integer
numeric=BigDecimal

nvarchar=String

2修改要生成增删改查代码的数据库的连接配置application.yml

只用配置数据库的连接信息即可

server:
port: 81

# mysql
spring:
main:
allow-circular-references: true
datasource:
type: com.alibaba.druid.pool.DruidDataSource
#MySQL配置
driverClassName: com.mysql.cj.jdbc.Driver
url: jdbc:mysql://192.168.111.100:3306/gulimall_wms?useUnicode=true&characterEncoding=UTF-8&useSSL=false&serverTimezone=Asia/Shanghai
username: root
password: root
#oracle配置
# driverClassName: oracle.jdbc.OracleDriver
# url: jdbc:oracle:thin:@47.100.206.162:1521:xe
# username: renren
# password: 123456
#SQLServer配置
# driverClassName: com.microsoft.sqlserver.jdbc.SQLServerDriver
# url: jdbc:sqlserver://192.168.10.10:1433;DatabaseName=renren_fast
# username: sa
# password: 123456
#PostgreSQL配置
# driverClassName: org.postgresql.Driver
# url: jdbc:postgresql://192.168.10.10:5432/renren_fast
# username: postgres
# password: 123456



jackson:
time-zone: GMT+8
date-format: yyyy-MM-dd HH:mm:ss
web:
resources:
static-locations: classpath:/static/,classpath:/views/

#mongodb:
# host: localhost
# port: 27017
# auth: false #是否使用密码验证
# username: tincery
# password: renren
# source: 123456
# database: test

mybatis-plus:
mapperLocations: classpath:mapper/**/*.xml


pagehelper:
reasonable: true
supportMethodsArguments: true
params: count=countSql


#指定数据库,可选值有【mysql、oracle、sqlserver、postgresql、mongodb】
renren:
database: mysql

3.启动服务生成代码,将生成的代码解压并粘贴到对应的模块中去

将main这个目录替换原来模块中的main目录即可,sql文件暂时不用管

4.创建模块的配置文件

server:
port: 8080
spring:
datasource:
url: jdbc:mysql://192.168.111.100:3306/gulimall_pms?useUnicode=true&characterEncoding=UTF-8&useSSL=false&serverTimezone=Asia/Shanghai
username: root
password: root
driver-class-name: com.mysql.cj.jdbc.Driver
mybatis-plus:
mapper-locations: classpath*:/mapper/**/*.xml #定义mapper.xml文件的位置
global-config:
db-config:
id-type: auto #主键的生成规则

5.启动测试生成的代码

在启动的时候,我们要处理好代码依赖的包,这些包都存在与生成代码的模块中,我们在生成代码的模块中复制过来即可,然后手动的引入相应的包,处理好报错信息。

3.后台搭建完成之后的项目树

C:.
│ .gitignore
│ LICENSE
│ pom.xml

├─.idea
│ │ .gitignore
│ │ compiler.xml
│ │ encodings.xml
│ │ jarRepositories.xml
│ │ misc.xml
│ │ uiDesigner.xml
│ │ vcs.xml
│ │ workspace.xml
│ │
│ └─codeStyles
│ codeStyleConfig.xml
│ Project.xml

├─gulimall-common
│ │ pom.xml
│ │
│ ├─src
│ │ ├─main
│ │ │ ├─java
│ │ │ │ └─com
│ │ │ │ └─atguigu
│ │ │ │ └─common
│ │ │ │ ├─exception
│ │ │ │ │ RRException.java
│ │ │ │ │
│ │ │ │ ├─utils
│ │ │ │ │ Constant.java
│ │ │ │ │ PageUtils.java
│ │ │ │ │ Query.java
│ │ │ │ │ R.java
│ │ │ │ │
│ │ │ │ └─xss
│ │ │ │ HTMLFilter.java
│ │ │ │ SQLFilter.java
│ │ │ │
│ │ │ └─resources
│ │ └─test
│ │ └─java
│ └─target
│ ├─classes
│ │ └─com
│ │ └─atguigu
│ │ └─common
│ │ ├─exception
│ │ │ RRException.class
│ │ │
│ │ ├─utils
│ │ │ Constant$MenuType.class
│ │ │ Constant$ScheduleStatus.class
│ │ │ Constant.class
│ │ │ PageUtils.class
│ │ │ Query.class
│ │ │ R.class
│ │ │
│ │ └─xss
│ │ HTMLFilter.class
│ │ SQLFilter.class
│ │
│ └─generated-sources
│ └─annotations
├─gulimall-coupon
│ │ .gitignore
│ │ gulimall-coupon.iml
│ │ HELP.md
│ │ mvnw
│ │ mvnw.cmd
│ │ pom.xml
│ │
│ ├─.mvn
│ │ └─wrapper
│ │ maven-wrapper.jar
│ │ maven-wrapper.properties
│ │
│ ├─src
│ │ ├─main
│ │ │ ├─java
│ │ │ │ └─com
│ │ │ │ └─atguigu
│ │ │ │ └─gulimall
│ │ │ │ └─coupon
│ │ │ │ │ GulimallCouponApplication.java
│ │ │ │ │
│ │ │ │ ├─controller
│ │ │ │ │ CouponController.java
│ │ │ │ │ CouponHistoryController.java
│ │ │ │ │ CouponSpuCategoryRelationController.java
│ │ │ │ │ CouponSpuRelationController.java
│ │ │ │ │ HomeAdvController.java
│ │ │ │ │ HomeSubjectController.java
│ │ │ │ │ HomeSubjectSpuController.java
│ │ │ │ │ MemberPriceController.java
│ │ │ │ │ SeckillPromotionController.java
│ │ │ │ │ SeckillSessionController.java
│ │ │ │ │ SeckillSkuNoticeController.java
│ │ │ │ │ SeckillSkuRelationController.java
│ │ │ │ │ SkuFullReductionController.java
│ │ │ │ │ SkuLadderController.java
│ │ │ │ │ SpuBoundsController.java
│ │ │ │ │
│ │ │ │ ├─dao
│ │ │ │ │ CouponDao.java
│ │ │ │ │ CouponHistoryDao.java
│ │ │ │ │ CouponSpuCategoryRelationDao.java
│ │ │ │ │ CouponSpuRelationDao.java
│ │ │ │ │ HomeAdvDao.java
│ │ │ │ │ HomeSubjectDao.java
│ │ │ │ │ HomeSubjectSpuDao.java
│ │ │ │ │ MemberPriceDao.java
│ │ │ │ │ SeckillPromotionDao.java
│ │ │ │ │ SeckillSessionDao.java
│ │ │ │ │ SeckillSkuNoticeDao.java
│ │ │ │ │ SeckillSkuRelationDao.java
│ │ │ │ │ SkuFullReductionDao.java
│ │ │ │ │ SkuLadderDao.java
│ │ │ │ │ SpuBoundsDao.java
│ │ │ │ │
│ │ │ │ ├─entity
│ │ │ │ │ CouponEntity.java
│ │ │ │ │ CouponHistoryEntity.java
│ │ │ │ │ CouponSpuCategoryRelationEntity.java
│ │ │ │ │ CouponSpuRelationEntity.java
│ │ │ │ │ HomeAdvEntity.java
│ │ │ │ │ HomeSubjectEntity.java
│ │ │ │ │ HomeSubjectSpuEntity.java
│ │ │ │ │ MemberPriceEntity.java
│ │ │ │ │ SeckillPromotionEntity.java
│ │ │ │ │ SeckillSessionEntity.java
│ │ │ │ │ SeckillSkuNoticeEntity.java
│ │ │ │ │ SeckillSkuRelationEntity.java
│ │ │ │ │ SkuFullReductionEntity.java
│ │ │ │ │ SkuLadderEntity.java
│ │ │ │ │ SpuBoundsEntity.java
│ │ │ │ │
│ │ │ │ └─service
│ │ │ │ │ CouponHistoryService.java
│ │ │ │ │ CouponService.java
│ │ │ │ │ CouponSpuCategoryRelationService.java
│ │ │ │ │ CouponSpuRelationService.java
│ │ │ │ │ HomeAdvService.java
│ │ │ │ │ HomeSubjectService.java
│ │ │ │ │ HomeSubjectSpuService.java
│ │ │ │ │ MemberPriceService.java
│ │ │ │ │ SeckillPromotionService.java
│ │ │ │ │ SeckillSessionService.java
│ │ │ │ │ SeckillSkuNoticeService.java
│ │ │ │ │ SeckillSkuRelationService.java
│ │ │ │ │ SkuFullReductionService.java
│ │ │ │ │ SkuLadderService.java
│ │ │ │ │ SpuBoundsService.java
│ │ │ │ │
│ │ │ │ └─impl
│ │ │ │ CouponHistoryServiceImpl.java
│ │ │ │ CouponServiceImpl.java
│ │ │ │ CouponSpuCategoryRelationServiceImpl.java
│ │ │ │ CouponSpuRelationServiceImpl.java
│ │ │ │ HomeAdvServiceImpl.java
│ │ │ │ HomeSubjectServiceImpl.java
│ │ │ │ HomeSubjectSpuServiceImpl.java
│ │ │ │ MemberPriceServiceImpl.java
│ │ │ │ SeckillPromotionServiceImpl.java
│ │ │ │ SeckillSessionServiceImpl.java
│ │ │ │ SeckillSkuNoticeServiceImpl.java
│ │ │ │ SeckillSkuRelationServiceImpl.java
│ │ │ │ SkuFullReductionServiceImpl.java
│ │ │ │ SkuLadderServiceImpl.java
│ │ │ │ SpuBoundsServiceImpl.java
│ │ │ │
│ │ │ └─resources
│ │ │ │ application.properties
│ │ │ │ application.yml
│ │ │ │
│ │ │ ├─mapper
│ │ │ │ └─coupon
│ │ │ │ CouponDao.xml
│ │ │ │ CouponHistoryDao.xml
│ │ │ │ CouponSpuCategoryRelationDao.xml
│ │ │ │ CouponSpuRelationDao.xml
│ │ │ │ HomeAdvDao.xml
│ │ │ │ HomeSubjectDao.xml
│ │ │ │ HomeSubjectSpuDao.xml
│ │ │ │ MemberPriceDao.xml
│ │ │ │ SeckillPromotionDao.xml
│ │ │ │ SeckillSessionDao.xml
│ │ │ │ SeckillSkuNoticeDao.xml
│ │ │ │ SeckillSkuRelationDao.xml
│ │ │ │ SkuFullReductionDao.xml
│ │ │ │ SkuLadderDao.xml
│ │ │ │ SpuBoundsDao.xml
│ │ │ │
│ │ │ ├─src
│ │ │ │ └─views
│ │ │ │ └─modules
│ │ │ │ └─coupon
│ │ │ │ coupon-add-or-update.vue
│ │ │ │ coupon.vue
│ │ │ │ couponhistory-add-or-update.vue
│ │ │ │ couponhistory.vue
│ │ │ │ couponspucategoryrelation-add-or-update.vue
│ │ │ │ couponspucategoryrelation.vue
│ │ │ │ couponspurelation-add-or-update.vue
│ │ │ │ couponspurelation.vue
│ │ │ │ homeadv-add-or-update.vue
│ │ │ │ homeadv.vue
│ │ │ │ homesubject-add-or-update.vue
│ │ │ │ homesubject.vue
│ │ │ │ homesubjectspu-add-or-update.vue
│ │ │ │ homesubjectspu.vue
│ │ │ │ memberprice-add-or-update.vue
│ │ │ │ memberprice.vue
│ │ │ │ seckillpromotion-add-or-update.vue
│ │ │ │ seckillpromotion.vue
│ │ │ │ seckillsession-add-or-update.vue
│ │ │ │ seckillsession.vue
│ │ │ │ seckillskunotice-add-or-update.vue
│ │ │ │ seckillskunotice.vue
│ │ │ │ seckillskurelation-add-or-update.vue
│ │ │ │ seckillskurelation.vue
│ │ │ │ skufullreduction-add-or-update.vue
│ │ │ │ skufullreduction.vue
│ │ │ │ skuladder-add-or-update.vue
│ │ │ │ skuladder.vue
│ │ │ │ spubounds-add-or-update.vue
│ │ │ │ spubounds.vue
│ │ │ │
│ │ │ ├─static
│ │ │ └─templates
│ │ └─test
│ │ └─java
│ │ └─com
│ │ └─atguigu
│ │ └─gulimall
│ │ └─coupon
│ │ GulimallCouponApplicationTests.java
│ │
│ └─target
│ ├─classes
│ │ │ application.properties
│ │ │ application.yml
│ │ │
│ │ ├─com
│ │ │ └─atguigu
│ │ │ └─gulimall
│ │ │ └─coupon
│ │ │ │ GulimallCouponApplication.class
│ │ │ │
│ │ │ ├─controller
│ │ │ │ CouponController.class
│ │ │ │ CouponHistoryController.class
│ │ │ │ CouponSpuCategoryRelationController.class
│ │ │ │ CouponSpuRelationController.class
│ │ │ │ HomeAdvController.class
│ │ │ │ HomeSubjectController.class
│ │ │ │ HomeSubjectSpuController.class
│ │ │ │ MemberPriceController.class
│ │ │ │ SeckillPromotionController.class
│ │ │ │ SeckillSessionController.class
│ │ │ │ SeckillSkuNoticeController.class
│ │ │ │ SeckillSkuRelationController.class
│ │ │ │ SkuFullReductionController.class
│ │ │ │ SkuLadderController.class
│ │ │ │ SpuBoundsController.class
│ │ │ │
│ │ │ ├─dao
│ │ │ │ CouponDao.class
│ │ │ │ CouponHistoryDao.class
│ │ │ │ CouponSpuCategoryRelationDao.class
│ │ │ │ CouponSpuRelationDao.class
│ │ │ │ HomeAdvDao.class
│ │ │ │ HomeSubjectDao.class
│ │ │ │ HomeSubjectSpuDao.class
│ │ │ │ MemberPriceDao.class
│ │ │ │ SeckillPromotionDao.class
│ │ │ │ SeckillSessionDao.class
│ │ │ │ SeckillSkuNoticeDao.class
│ │ │ │ SeckillSkuRelationDao.class
│ │ │ │ SkuFullReductionDao.class
│ │ │ │ SkuLadderDao.class
│ │ │ │ SpuBoundsDao.class
│ │ │ │
│ │ │ ├─entity
│ │ │ │ CouponEntity.class
│ │ │ │ CouponHistoryEntity.class
│ │ │ │ CouponSpuCategoryRelationEntity.class
│ │ │ │ CouponSpuRelationEntity.class
│ │ │ │ HomeAdvEntity.class
│ │ │ │ HomeSubjectEntity.class
│ │ │ │ HomeSubjectSpuEntity.class
│ │ │ │ MemberPriceEntity.class
│ │ │ │ SeckillPromotionEntity.class
│ │ │ │ SeckillSessionEntity.class
│ │ │ │ SeckillSkuNoticeEntity.class
│ │ │ │ SeckillSkuRelationEntity.class
│ │ │ │ SkuFullReductionEntity.class
│ │ │ │ SkuLadderEntity.class
│ │ │ │ SpuBoundsEntity.class
│ │ │ │
│ │ │ └─service
│ │ │ │ CouponHistoryService.class
│ │ │ │ CouponService.class
│ │ │ │ CouponSpuCategoryRelationService.class
│ │ │ │ CouponSpuRelationService.class
│ │ │ │ HomeAdvService.class
│ │ │ │ HomeSubjectService.class
│ │ │ │ HomeSubjectSpuService.class
│ │ │ │ MemberPriceService.class
│ │ │ │ SeckillPromotionService.class
│ │ │ │ SeckillSessionService.class
│ │ │ │ SeckillSkuNoticeService.class
│ │ │ │ SeckillSkuRelationService.class
│ │ │ │ SkuFullReductionService.class
│ │ │ │ SkuLadderService.class
│ │ │ │ SpuBoundsService.class
│ │ │ │
│ │ │ └─impl
│ │ │ CouponHistoryServiceImpl.class
│ │ │ CouponServiceImpl.class
│ │ │ CouponSpuCategoryRelationServiceImpl.class
│ │ │ CouponSpuRelationServiceImpl.class
│ │ │ HomeAdvServiceImpl.class
│ │ │ HomeSubjectServiceImpl.class
│ │ │ HomeSubjectSpuServiceImpl.class
│ │ │ MemberPriceServiceImpl.class
│ │ │ SeckillPromotionServiceImpl.class
│ │ │ SeckillSessionServiceImpl.class
│ │ │ SeckillSkuNoticeServiceImpl.class
│ │ │ SeckillSkuRelationServiceImpl.class
│ │ │ SkuFullReductionServiceImpl.class
│ │ │ SkuLadderServiceImpl.class
│ │ │ SpuBoundsServiceImpl.class
│ │ │
│ │ ├─mapper
│ │ │ └─coupon
│ │ │ CouponDao.xml
│ │ │ CouponHistoryDao.xml
│ │ │ CouponSpuCategoryRelationDao.xml
│ │ │ CouponSpuRelationDao.xml
│ │ │ HomeAdvDao.xml
│ │ │ HomeSubjectDao.xml
│ │ │ HomeSubjectSpuDao.xml
│ │ │ MemberPriceDao.xml
│ │ │ SeckillPromotionDao.xml
│ │ │ SeckillSessionDao.xml
│ │ │ SeckillSkuNoticeDao.xml
│ │ │ SeckillSkuRelationDao.xml
│ │ │ SkuFullReductionDao.xml
│ │ │ SkuLadderDao.xml
│ │ │ SpuBoundsDao.xml
│ │ │
│ │ └─src
│ │ └─views
│ │ └─modules
│ │ └─coupon
│ │ coupon-add-or-update.vue
│ │ coupon.vue
│ │ couponhistory-add-or-update.vue
│ │ couponhistory.vue
│ │ couponspucategoryrelation-add-or-update.vue
│ │ couponspucategoryrelation.vue
│ │ couponspurelation-add-or-update.vue
│ │ couponspurelation.vue
│ │ homeadv-add-or-update.vue
│ │ homeadv.vue
│ │ homesubject-add-or-update.vue
│ │ homesubject.vue
│ │ homesubjectspu-add-or-update.vue
│ │ homesubjectspu.vue
│ │ memberprice-add-or-update.vue
│ │ memberprice.vue
│ │ seckillpromotion-add-or-update.vue
│ │ seckillpromotion.vue
│ │ seckillsession-add-or-update.vue
│ │ seckillsession.vue
│ │ seckillskunotice-add-or-update.vue
│ │ seckillskunotice.vue
│ │ seckillskurelation-add-or-update.vue
│ │ seckillskurelation.vue
│ │ skufullreduction-add-or-update.vue
│ │ skufullreduction.vue
│ │ skuladder-add-or-update.vue
│ │ skuladder.vue
│ │ spubounds-add-or-update.vue
│ │ spubounds.vue
│ │
│ ├─generated-sources
│ │ └─annotations
│ └─maven-status
│ └─maven-compiler-plugin
│ └─compile
│ └─default-compile
│ createdFiles.lst
│ inputFiles.lst

├─gulimall-member
│ │ .gitignore
│ │ gulimall-member.iml
│ │ HELP.md
│ │ mvnw
│ │ mvnw.cmd
│ │ pom.xml
│ │
│ ├─.mvn
│ │ └─wrapper
│ │ maven-wrapper.jar
│ │ maven-wrapper.properties
│ │
│ ├─src
│ │ ├─main
│ │ │ ├─java
│ │ │ │ └─com
│ │ │ │ └─atguigu
│ │ │ │ └─gulimall
│ │ │ │ └─member
│ │ │ │ │ GulimallMemberApplication.java
│ │ │ │ │
│ │ │ │ ├─controller
│ │ │ │ │ GrowthChangeHistoryController.java
│ │ │ │ │ IntegrationChangeHistoryController.java
│ │ │ │ │ MemberCollectSpuController.java
│ │ │ │ │ MemberCollectSubjectController.java
│ │ │ │ │ MemberController.java
│ │ │ │ │ MemberLevelController.java
│ │ │ │ │ MemberLoginLogController.java
│ │ │ │ │ MemberReceiveAddressController.java
│ │ │ │ │ MemberStatisticsInfoController.java
│ │ │ │ │
│ │ │ │ ├─dao
│ │ │ │ │ GrowthChangeHistoryDao.java
│ │ │ │ │ IntegrationChangeHistoryDao.java
│ │ │ │ │ MemberCollectSpuDao.java
│ │ │ │ │ MemberCollectSubjectDao.java
│ │ │ │ │ MemberDao.java
│ │ │ │ │ MemberLevelDao.java
│ │ │ │ │ MemberLoginLogDao.java
│ │ │ │ │ MemberReceiveAddressDao.java
│ │ │ │ │ MemberStatisticsInfoDao.java
│ │ │ │ │
│ │ │ │ ├─entity
│ │ │ │ │ GrowthChangeHistoryEntity.java
│ │ │ │ │ IntegrationChangeHistoryEntity.java
│ │ │ │ │ MemberCollectSpuEntity.java
│ │ │ │ │ MemberCollectSubjectEntity.java
│ │ │ │ │ MemberEntity.java
│ │ │ │ │ MemberLevelEntity.java
│ │ │ │ │ MemberLoginLogEntity.java
│ │ │ │ │ MemberReceiveAddressEntity.java
│ │ │ │ │ MemberStatisticsInfoEntity.java
│ │ │ │ │
│ │ │ │ └─service
│ │ │ │ │ GrowthChangeHistoryService.java
│ │ │ │ │ IntegrationChangeHistoryService.java
│ │ │ │ │ MemberCollectSpuService.java
│ │ │ │ │ MemberCollectSubjectService.java
│ │ │ │ │ MemberLevelService.java
│ │ │ │ │ MemberLoginLogService.java
│ │ │ │ │ MemberReceiveAddressService.java
│ │ │ │ │ MemberService.java
│ │ │ │ │ MemberStatisticsInfoService.java
│ │ │ │ │
│ │ │ │ └─impl
│ │ │ │ GrowthChangeHistoryServiceImpl.java
│ │ │ │ IntegrationChangeHistoryServiceImpl.java
│ │ │ │ MemberCollectSpuServiceImpl.java
│ │ │ │ MemberCollectSubjectServiceImpl.java
│ │ │ │ MemberLevelServiceImpl.java
│ │ │ │ MemberLoginLogServiceImpl.java
│ │ │ │ MemberReceiveAddressServiceImpl.java
│ │ │ │ MemberServiceImpl.java
│ │ │ │ MemberStatisticsInfoServiceImpl.java
│ │ │ │
│ │ │ └─resources
│ │ │ │ application.properties
│ │ │ │ application.yml
│ │ │ │
│ │ │ ├─mapper
│ │ │ │ └─member
│ │ │ │ GrowthChangeHistoryDao.xml
│ │ │ │ IntegrationChangeHistoryDao.xml
│ │ │ │ MemberCollectSpuDao.xml
│ │ │ │ MemberCollectSubjectDao.xml
│ │ │ │ MemberDao.xml
│ │ │ │ MemberLevelDao.xml
│ │ │ │ MemberLoginLogDao.xml
│ │ │ │ MemberReceiveAddressDao.xml
│ │ │ │ MemberStatisticsInfoDao.xml
│ │ │ │
│ │ │ ├─src
│ │ │ │ └─views
│ │ │ │ └─modules
│ │ │ │ └─member
│ │ │ │ growthchangehistory-add-or-update.vue
│ │ │ │ growthchangehistory.vue
│ │ │ │ integrationchangehistory-add-or-update.vue
│ │ │ │ integrationchangehistory.vue
│ │ │ │ member-add-or-update.vue
│ │ │ │ member.vue
│ │ │ │ membercollectspu-add-or-update.vue
│ │ │ │ membercollectspu.vue
│ │ │ │ membercollectsubject-add-or-update.vue
│ │ │ │ membercollectsubject.vue
│ │ │ │ memberlevel-add-or-update.vue
│ │ │ │ memberlevel.vue
│ │ │ │ memberloginlog-add-or-update.vue
│ │ │ │ memberloginlog.vue
│ │ │ │ memberreceiveaddress-add-or-update.vue
│ │ │ │ memberreceiveaddress.vue
│ │ │ │ memberstatisticsinfo-add-or-update.vue
│ │ │ │ memberstatisticsinfo.vue
│ │ │ │
│ │ │ ├─static
│ │ │ └─templates
│ │ └─test
│ │ └─java
│ │ └─com
│ │ └─atguigu
│ │ └─gulimall
│ │ └─member
│ │ GulimallMemberApplicationTests.java
│ │
│ └─target
│ ├─classes
│ │ │ application.properties
│ │ │ application.yml
│ │ │
│ │ ├─com
│ │ │ └─atguigu
│ │ │ └─gulimall
│ │ │ └─member
│ │ │ │ GulimallMemberApplication.class
│ │ │ │
│ │ │ ├─controller
│ │ │ │ GrowthChangeHistoryController.class
│ │ │ │ IntegrationChangeHistoryController.class
│ │ │ │ MemberCollectSpuController.class
│ │ │ │ MemberCollectSubjectController.class
│ │ │ │ MemberController.class
│ │ │ │ MemberLevelController.class
│ │ │ │ MemberLoginLogController.class
│ │ │ │ MemberReceiveAddressController.class
│ │ │ │ MemberStatisticsInfoController.class
│ │ │ │
│ │ │ ├─dao
│ │ │ │ GrowthChangeHistoryDao.class
│ │ │ │ IntegrationChangeHistoryDao.class
│ │ │ │ MemberCollectSpuDao.class
│ │ │ │ MemberCollectSubjectDao.class
│ │ │ │ MemberDao.class
│ │ │ │ MemberLevelDao.class
│ │ │ │ MemberLoginLogDao.class
│ │ │ │ MemberReceiveAddressDao.class
│ │ │ │ MemberStatisticsInfoDao.class
│ │ │ │
│ │ │ ├─entity
│ │ │ │ GrowthChangeHistoryEntity.class
│ │ │ │ IntegrationChangeHistoryEntity.class
│ │ │ │ MemberCollectSpuEntity.class
│ │ │ │ MemberCollectSubjectEntity.class
│ │ │ │ MemberEntity.class
│ │ │ │ MemberLevelEntity.class
│ │ │ │ MemberLoginLogEntity.class
│ │ │ │ MemberReceiveAddressEntity.class
│ │ │ │ MemberStatisticsInfoEntity.class
│ │ │ │
│ │ │ └─service
│ │ │ │ GrowthChangeHistoryService.class
│ │ │ │ IntegrationChangeHistoryService.class
│ │ │ │ MemberCollectSpuService.class
│ │ │ │ MemberCollectSubjectService.class
│ │ │ │ MemberLevelService.class
│ │ │ │ MemberLoginLogService.class
│ │ │ │ MemberReceiveAddressService.class
│ │ │ │ MemberService.class
│ │ │ │ MemberStatisticsInfoService.class
│ │ │ │
│ │ │ └─impl
│ │ │ GrowthChangeHistoryServiceImpl.class
│ │ │ IntegrationChangeHistoryServiceImpl.class
│ │ │ MemberCollectSpuServiceImpl.class
│ │ │ MemberCollectSubjectServiceImpl.class
│ │ │ MemberLevelServiceImpl.class
│ │ │ MemberLoginLogServiceImpl.class
│ │ │ MemberReceiveAddressServiceImpl.class
│ │ │ MemberServiceImpl.class
│ │ │ MemberStatisticsInfoServiceImpl.class
│ │ │
│ │ ├─mapper
│ │ │ └─member
│ │ │ GrowthChangeHistoryDao.xml
│ │ │ IntegrationChangeHistoryDao.xml
│ │ │ MemberCollectSpuDao.xml
│ │ │ MemberCollectSubjectDao.xml
│ │ │ MemberDao.xml
│ │ │ MemberLevelDao.xml
│ │ │ MemberLoginLogDao.xml
│ │ │ MemberReceiveAddressDao.xml
│ │ │ MemberStatisticsInfoDao.xml
│ │ │
│ │ └─src
│ │ └─views
│ │ └─modules
│ │ └─member
│ │ growthchangehistory-add-or-update.vue
│ │ growthchangehistory.vue
│ │ integrationchangehistory-add-or-update.vue
│ │ integrationchangehistory.vue
│ │ member-add-or-update.vue
│ │ member.vue
│ │ membercollectspu-add-or-update.vue
│ │ membercollectspu.vue
│ │ membercollectsubject-add-or-update.vue
│ │ membercollectsubject.vue
│ │ memberlevel-add-or-update.vue
│ │ memberlevel.vue
│ │ memberloginlog-add-or-update.vue
│ │ memberloginlog.vue
│ │ memberreceiveaddress-add-or-update.vue
│ │ memberreceiveaddress.vue
│ │ memberstatisticsinfo-add-or-update.vue
│ │ memberstatisticsinfo.vue
│ │
│ ├─generated-sources
│ │ └─annotations
│ └─maven-status
│ └─maven-compiler-plugin
│ └─compile
│ └─default-compile
│ createdFiles.lst
│ inputFiles.lst

├─gulimall-order
│ │ .gitignore
│ │ gulimall-order.iml
│ │ HELP.md
│ │ mvnw
│ │ mvnw.cmd
│ │ pom.xml
│ │
│ ├─.mvn
│ │ └─wrapper
│ │ maven-wrapper.jar
│ │ maven-wrapper.properties
│ │
│ ├─src
│ │ ├─main
│ │ │ ├─java
│ │ │ │ └─com
│ │ │ │ └─atguigu
│ │ │ │ └─gulimall
│ │ │ │ └─order
│ │ │ │ │ GulimallOrderApplication.java
│ │ │ │ │
│ │ │ │ ├─controller
│ │ │ │ │ OrderController.java
│ │ │ │ │ OrderItemController.java
│ │ │ │ │ OrderOperateHistoryController.java
│ │ │ │ │ OrderReturnApplyController.java
│ │ │ │ │ OrderReturnReasonController.java
│ │ │ │ │ OrderSettingController.java
│ │ │ │ │ PaymentInfoController.java
│ │ │ │ │ RefundInfoController.java
│ │ │ │ │
│ │ │ │ ├─dao
│ │ │ │ │ OrderDao.java
│ │ │ │ │ OrderItemDao.java
│ │ │ │ │ OrderOperateHistoryDao.java
│ │ │ │ │ OrderReturnApplyDao.java
│ │ │ │ │ OrderReturnReasonDao.java
│ │ │ │ │ OrderSettingDao.java
│ │ │ │ │ PaymentInfoDao.java
│ │ │ │ │ RefundInfoDao.java
│ │ │ │ │
│ │ │ │ ├─entity
│ │ │ │ │ OrderEntity.java
│ │ │ │ │ OrderItemEntity.java
│ │ │ │ │ OrderOperateHistoryEntity.java
│ │ │ │ │ OrderReturnApplyEntity.java
│ │ │ │ │ OrderReturnReasonEntity.java
│ │ │ │ │ OrderSettingEntity.java
│ │ │ │ │ PaymentInfoEntity.java
│ │ │ │ │ RefundInfoEntity.java
│ │ │ │ │
│ │ │ │ └─service
│ │ │ │ │ OrderItemService.java
│ │ │ │ │ OrderOperateHistoryService.java
│ │ │ │ │ OrderReturnApplyService.java
│ │ │ │ │ OrderReturnReasonService.java
│ │ │ │ │ OrderService.java
│ │ │ │ │ OrderSettingService.java
│ │ │ │ │ PaymentInfoService.java
│ │ │ │ │ RefundInfoService.java
│ │ │ │ │
│ │ │ │ └─impl
│ │ │ │ OrderItemServiceImpl.java
│ │ │ │ OrderOperateHistoryServiceImpl.java
│ │ │ │ OrderReturnApplyServiceImpl.java
│ │ │ │ OrderReturnReasonServiceImpl.java
│ │ │ │ OrderServiceImpl.java
│ │ │ │ OrderSettingServiceImpl.java
│ │ │ │ PaymentInfoServiceImpl.java
│ │ │ │ RefundInfoServiceImpl.java
│ │ │ │
│ │ │ └─resources
│ │ │ │ application.properties
│ │ │ │ application.yml
│ │ │ │
│ │ │ ├─mapper
│ │ │ │ └─order
│ │ │ │ OrderDao.xml
│ │ │ │ OrderItemDao.xml
│ │ │ │ OrderOperateHistoryDao.xml
│ │ │ │ OrderReturnApplyDao.xml
│ │ │ │ OrderReturnReasonDao.xml
│ │ │ │ OrderSettingDao.xml
│ │ │ │ PaymentInfoDao.xml
│ │ │ │ RefundInfoDao.xml
│ │ │ │
│ │ │ ├─src
│ │ │ │ └─views
│ │ │ │ └─modules
│ │ │ │ └─order
│ │ │ │ order-add-or-update.vue
│ │ │ │ order.vue
│ │ │ │ orderitem-add-or-update.vue
│ │ │ │ orderitem.vue
│ │ │ │ orderoperatehistory-add-or-update.vue
│ │ │ │ orderoperatehistory.vue
│ │ │ │ orderreturnapply-add-or-update.vue
│ │ │ │ orderreturnapply.vue
│ │ │ │ orderreturnreason-add-or-update.vue
│ │ │ │ orderreturnreason.vue
│ │ │ │ ordersetting-add-or-update.vue
│ │ │ │ ordersetting.vue
│ │ │ │ paymentinfo-add-or-update.vue
│ │ │ │ paymentinfo.vue
│ │ │ │ refundinfo-add-or-update.vue
│ │ │ │ refundinfo.vue
│ │ │ │
│ │ │ ├─static
│ │ │ └─templates
│ │ └─test
│ │ └─java
│ │ └─com
│ │ └─atguigu
│ │ └─gulimall
│ │ └─order
│ │ GulimallOrderApplicationTests.java
│ │
│ └─target
│ ├─classes
│ │ │ application.properties
│ │ │ application.yml
│ │ │
│ │ ├─com
│ │ │ └─atguigu
│ │ │ └─gulimall
│ │ │ ├─gulimallorder
│ │ │ │ GulimallOrderApplication.class
│ │ │ │
│ │ │ └─order
│ │ │ │ GulimallOrderApplication.class
│ │ │ │
│ │ │ ├─controller
│ │ │ │ OrderController.class
│ │ │ │ OrderItemController.class
│ │ │ │ OrderOperateHistoryController.class
│ │ │ │ OrderReturnApplyController.class
│ │ │ │ OrderReturnReasonController.class
│ │ │ │ OrderSettingController.class
│ │ │ │ PaymentInfoController.class
│ │ │ │ RefundInfoController.class
│ │ │ │
│ │ │ ├─dao
│ │ │ │ OrderDao.class
│ │ │ │ OrderItemDao.class
│ │ │ │ OrderOperateHistoryDao.class
│ │ │ │ OrderReturnApplyDao.class
│ │ │ │ OrderReturnReasonDao.class
│ │ │ │ OrderSettingDao.class
│ │ │ │ PaymentInfoDao.class
│ │ │ │ RefundInfoDao.class
│ │ │ │
│ │ │ ├─entity
│ │ │ │ OrderEntity.class
│ │ │ │ OrderItemEntity.class
│ │ │ │ OrderOperateHistoryEntity.class
│ │ │ │ OrderReturnApplyEntity.class
│ │ │ │ OrderReturnReasonEntity.class
│ │ │ │ OrderSettingEntity.class
│ │ │ │ PaymentInfoEntity.class
│ │ │ │ RefundInfoEntity.class
│ │ │ │
│ │ │ └─service
│ │ │ │ OrderItemService.class
│ │ │ │ OrderOperateHistoryService.class
│ │ │ │ OrderReturnApplyService.class
│ │ │ │ OrderReturnReasonService.class
│ │ │ │ OrderService.class
│ │ │ │ OrderSettingService.class
│ │ │ │ PaymentInfoService.class
│ │ │ │ RefundInfoService.class
│ │ │ │
│ │ │ └─impl
│ │ │ OrderItemServiceImpl.class
│ │ │ OrderOperateHistoryServiceImpl.class
│ │ │ OrderReturnApplyServiceImpl.class
│ │ │ OrderReturnReasonServiceImpl.class
│ │ │ OrderServiceImpl.class
│ │ │ OrderSettingServiceImpl.class
│ │ │ PaymentInfoServiceImpl.class
│ │ │ RefundInfoServiceImpl.class
│ │ │
│ │ ├─mapper
│ │ │ └─order
│ │ │ OrderDao.xml
│ │ │ OrderItemDao.xml
│ │ │ OrderOperateHistoryDao.xml
│ │ │ OrderReturnApplyDao.xml
│ │ │ OrderReturnReasonDao.xml
│ │ │ OrderSettingDao.xml
│ │ │ PaymentInfoDao.xml
│ │ │ RefundInfoDao.xml
│ │ │
│ │ └─src
│ │ └─views
│ │ └─modules
│ │ └─order
│ │ order-add-or-update.vue
│ │ order.vue
│ │ orderitem-add-or-update.vue
│ │ orderitem.vue
│ │ orderoperatehistory-add-or-update.vue
│ │ orderoperatehistory.vue
│ │ orderreturnapply-add-or-update.vue
│ │ orderreturnapply.vue
│ │ orderreturnreason-add-or-update.vue
│ │ orderreturnreason.vue
│ │ ordersetting-add-or-update.vue
│ │ ordersetting.vue
│ │ paymentinfo-add-or-update.vue
│ │ paymentinfo.vue
│ │ refundinfo-add-or-update.vue
│ │ refundinfo.vue
│ │
│ ├─generated-sources
│ │ └─annotations
│ └─maven-status
│ └─maven-compiler-plugin
│ └─compile
│ └─default-compile
│ createdFiles.lst
│ inputFiles.lst

├─gulimall-product
│ │ .gitignore
│ │ gulimall-product.iml
│ │ HELP.md
│ │ mvnw
│ │ mvnw.cmd
│ │ pom.xml
│ │
│ ├─.mvn
│ │ └─wrapper
│ │ maven-wrapper.jar
│ │ maven-wrapper.properties
│ │
│ ├─src
│ │ ├─main
│ │ │ ├─java
│ │ │ │ └─com
│ │ │ │ └─atguigu
│ │ │ │ └─gulimall
│ │ │ │ └─product
│ │ │ │ │ GulimallProductApplication.java
│ │ │ │ │
│ │ │ │ ├─controller
│ │ │ │ │ AttrAttrgroupRelationController.java
│ │ │ │ │ AttrController.java
│ │ │ │ │ AttrGroupController.java
│ │ │ │ │ BrandController.java
│ │ │ │ │ CategoryBrandRelationController.java
│ │ │ │ │ CategoryController.java
│ │ │ │ │ CommentReplayController.java
│ │ │ │ │ ProductAttrValueController.java
│ │ │ │ │ SkuImagesController.java
│ │ │ │ │ SkuInfoController.java
│ │ │ │ │ SkuSaleAttrValueController.java
│ │ │ │ │ SpuCommentController.java
│ │ │ │ │ SpuImagesController.java
│ │ │ │ │ SpuInfoController.java
│ │ │ │ │ SpuInfoDescController.java
│ │ │ │ │
│ │ │ │ ├─dao
│ │ │ │ │ AttrAttrgroupRelationDao.java
│ │ │ │ │ AttrDao.java
│ │ │ │ │ AttrGroupDao.java
│ │ │ │ │ BrandDao.java
│ │ │ │ │ CategoryBrandRelationDao.java
│ │ │ │ │ CategoryDao.java
│ │ │ │ │ CommentReplayDao.java
│ │ │ │ │ ProductAttrValueDao.java
│ │ │ │ │ SkuImagesDao.java
│ │ │ │ │ SkuInfoDao.java
│ │ │ │ │ SkuSaleAttrValueDao.java
│ │ │ │ │ SpuCommentDao.java
│ │ │ │ │ SpuImagesDao.java
│ │ │ │ │ SpuInfoDao.java
│ │ │ │ │ SpuInfoDescDao.java
│ │ │ │ │
│ │ │ │ ├─entity
│ │ │ │ │ AttrAttrgroupRelationEntity.java
│ │ │ │ │ AttrEntity.java
│ │ │ │ │ AttrGroupEntity.java
│ │ │ │ │ BrandEntity.java
│ │ │ │ │ CategoryBrandRelationEntity.java
│ │ │ │ │ CategoryEntity.java
│ │ │ │ │ CommentReplayEntity.java
│ │ │ │ │ ProductAttrValueEntity.java
│ │ │ │ │ SkuImagesEntity.java
│ │ │ │ │ SkuInfoEntity.java
│ │ │ │ │ SkuSaleAttrValueEntity.java
│ │ │ │ │ SpuCommentEntity.java
│ │ │ │ │ SpuImagesEntity.java
│ │ │ │ │ SpuInfoDescEntity.java
│ │ │ │ │ SpuInfoEntity.java
│ │ │ │ │
│ │ │ │ └─service
│ │ │ │ │ AttrAttrgroupRelationService.java
│ │ │ │ │ AttrGroupService.java
│ │ │ │ │ AttrService.java
│ │ │ │ │ BrandService.java
│ │ │ │ │ CategoryBrandRelationService.java
│ │ │ │ │ CategoryService.java
│ │ │ │ │ CommentReplayService.java
│ │ │ │ │ ProductAttrValueService.java
│ │ │ │ │ SkuImagesService.java
│ │ │ │ │ SkuInfoService.java
│ │ │ │ │ SkuSaleAttrValueService.java
│ │ │ │ │ SpuCommentService.java
│ │ │ │ │ SpuImagesService.java
│ │ │ │ │ SpuInfoDescService.java
│ │ │ │ │ SpuInfoService.java
│ │ │ │ │
│ │ │ │ └─impl
│ │ │ │ AttrAttrgroupRelationServiceImpl.java
│ │ │ │ AttrGroupServiceImpl.java
│ │ │ │ AttrServiceImpl.java
│ │ │ │ BrandServiceImpl.java
│ │ │ │ CategoryBrandRelationServiceImpl.java
│ │ │ │ CategoryServiceImpl.java
│ │ │ │ CommentReplayServiceImpl.java
│ │ │ │ ProductAttrValueServiceImpl.java
│ │ │ │ SkuImagesServiceImpl.java
│ │ │ │ SkuInfoServiceImpl.java
│ │ │ │ SkuSaleAttrValueServiceImpl.java
│ │ │ │ SpuCommentServiceImpl.java
│ │ │ │ SpuImagesServiceImpl.java
│ │ │ │ SpuInfoDescServiceImpl.java
│ │ │ │ SpuInfoServiceImpl.java
│ │ │ │
│ │ │ └─resources
│ │ │ │ application.properties
│ │ │ │ application.yml
│ │ │ │
│ │ │ ├─mapper
│ │ │ │ └─product
│ │ │ │ AttrAttrgroupRelationDao.xml
│ │ │ │ AttrDao.xml
│ │ │ │ AttrGroupDao.xml
│ │ │ │ BrandDao.xml
│ │ │ │ CategoryBrandRelationDao.xml
│ │ │ │ CategoryDao.xml
│ │ │ │ CommentReplayDao.xml
│ │ │ │ ProductAttrValueDao.xml
│ │ │ │ SkuImagesDao.xml
│ │ │ │ SkuInfoDao.xml
│ │ │ │ SkuSaleAttrValueDao.xml
│ │ │ │ SpuCommentDao.xml
│ │ │ │ SpuImagesDao.xml
│ │ │ │ SpuInfoDao.xml
│ │ │ │ SpuInfoDescDao.xml
│ │ │ │
│ │ │ ├─src
│ │ │ │ └─views
│ │ │ │ └─modules
│ │ │ │ └─product
│ │ │ │ attr-add-or-update.vue
│ │ │ │ attr.vue
│ │ │ │ attrattrgrouprelation-add-or-update.vue
│ │ │ │ attrattrgrouprelation.vue
│ │ │ │ attrgroup-add-or-update.vue
│ │ │ │ attrgroup.vue
│ │ │ │ brand-add-or-update.vue
│ │ │ │ brand.vue
│ │ │ │ category-add-or-update.vue
│ │ │ │ category.vue
│ │ │ │ categorybrandrelation-add-or-update.vue
│ │ │ │ categorybrandrelation.vue
│ │ │ │ commentreplay-add-or-update.vue
│ │ │ │ commentreplay.vue
│ │ │ │ productattrvalue-add-or-update.vue
│ │ │ │ productattrvalue.vue
│ │ │ │ skuimages-add-or-update.vue
│ │ │ │ skuimages.vue
│ │ │ │ skuinfo-add-or-update.vue
│ │ │ │ skuinfo.vue
│ │ │ │ skusaleattrvalue-add-or-update.vue
│ │ │ │ skusaleattrvalue.vue
│ │ │ │ spucomment-add-or-update.vue
│ │ │ │ spucomment.vue
│ │ │ │ spuimages-add-or-update.vue
│ │ │ │ spuimages.vue
│ │ │ │ spuinfo-add-or-update.vue
│ │ │ │ spuinfo.vue
│ │ │ │ spuinfodesc-add-or-update.vue
│ │ │ │ spuinfodesc.vue
│ │ │ │
│ │ │ ├─static
│ │ │ └─templates
│ │ └─test
│ │ └─java
│ │ └─com
│ │ └─atguigu
│ │ └─gulimall
│ │ └─product
│ │ GulimallProductApplicationTests.java
│ │
│ └─target
│ ├─classes
│ │ │ application.properties
│ │ │ application.yml
│ │ │
│ │ ├─com
│ │ │ └─atguigu
│ │ │ └─gulimall
│ │ │ └─product
│ │ │ │ GulimallProductApplication.class
│ │ │ │
│ │ │ ├─controller
│ │ │ │ AttrAttrgroupRelationController.class
│ │ │ │ AttrController.class
│ │ │ │ AttrGroupController.class
│ │ │ │ BrandController.class
│ │ │ │ CategoryBrandRelationController.class
│ │ │ │ CategoryController.class
│ │ │ │ CommentReplayController.class
│ │ │ │ ProductAttrValueController.class
│ │ │ │ SkuImagesController.class
│ │ │ │ SkuInfoController.class
│ │ │ │ SkuSaleAttrValueController.class
│ │ │ │ SpuCommentController.class
│ │ │ │ SpuImagesController.class
│ │ │ │ SpuInfoController.class
│ │ │ │ SpuInfoDescController.class
│ │ │ │
│ │ │ ├─dao
│ │ │ │ AttrAttrgroupRelationDao.class
│ │ │ │ AttrDao.class
│ │ │ │ AttrGroupDao.class
│ │ │ │ BrandDao.class
│ │ │ │ CategoryBrandRelationDao.class
│ │ │ │ CategoryDao.class
│ │ │ │ CommentReplayDao.class
│ │ │ │ ProductAttrValueDao.class
│ │ │ │ SkuImagesDao.class
│ │ │ │ SkuInfoDao.class
│ │ │ │ SkuSaleAttrValueDao.class
│ │ │ │ SpuCommentDao.class
│ │ │ │ SpuImagesDao.class
│ │ │ │ SpuInfoDao.class
│ │ │ │ SpuInfoDescDao.class
│ │ │ │
│ │ │ ├─entity
│ │ │ │ AttrAttrgroupRelationEntity.class
│ │ │ │ AttrEntity.class
│ │ │ │ AttrGroupEntity.class
│ │ │ │ BrandEntity.class
│ │ │ │ CategoryBrandRelationEntity.class
│ │ │ │ CategoryEntity.class
│ │ │ │ CommentReplayEntity.class
│ │ │ │ ProductAttrValueEntity.class
│ │ │ │ SkuImagesEntity.class
│ │ │ │ SkuInfoEntity.class
│ │ │ │ SkuSaleAttrValueEntity.class
│ │ │ │ SpuCommentEntity.class
│ │ │ │ SpuImagesEntity.class
│ │ │ │ SpuInfoDescEntity.class
│ │ │ │ SpuInfoEntity.class
│ │ │ │
│ │ │ └─service
│ │ │ │ AttrAttrgroupRelationService.class
│ │ │ │ AttrGroupService.class
│ │ │ │ AttrService.class
│ │ │ │ BrandService.class
│ │ │ │ CategoryBrandRelationService.class
│ │ │ │ CategoryService.class
│ │ │ │ CommentReplayService.class
│ │ │ │ ProductAttrValueService.class
│ │ │ │ SkuImagesService.class
│ │ │ │ SkuInfoService.class
│ │ │ │ SkuSaleAttrValueService.class
│ │ │ │ SpuCommentService.class
│ │ │ │ SpuImagesService.class
│ │ │ │ SpuInfoDescService.class
│ │ │ │ SpuInfoService.class
│ │ │ │
│ │ │ └─impl
│ │ │ AttrAttrgroupRelationServiceImpl.class
│ │ │ AttrGroupServiceImpl.class
│ │ │ AttrServiceImpl.class
│ │ │ BrandServiceImpl.class
│ │ │ CategoryBrandRelationServiceImpl.class
│ │ │ CategoryServiceImpl.class
│ │ │ CommentReplayServiceImpl.class
│ │ │ ProductAttrValueServiceImpl.class
│ │ │ SkuImagesServiceImpl.class
│ │ │ SkuInfoServiceImpl.class
│ │ │ SkuSaleAttrValueServiceImpl.class
│ │ │ SpuCommentServiceImpl.class
│ │ │ SpuImagesServiceImpl.class
│ │ │ SpuInfoDescServiceImpl.class
│ │ │ SpuInfoServiceImpl.class
│ │ │
│ │ ├─mapper
│ │ │ └─product
│ │ │ AttrAttrgroupRelationDao.xml
│ │ │ AttrDao.xml
│ │ │ AttrGroupDao.xml
│ │ │ BrandDao.xml
│ │ │ CategoryBrandRelationDao.xml
│ │ │ CategoryDao.xml
│ │ │ CommentReplayDao.xml
│ │ │ ProductAttrValueDao.xml
│ │ │ SkuImagesDao.xml
│ │ │ SkuInfoDao.xml
│ │ │ SkuSaleAttrValueDao.xml
│ │ │ SpuCommentDao.xml
│ │ │ SpuImagesDao.xml
│ │ │ SpuInfoDao.xml
│ │ │ SpuInfoDescDao.xml
│ │ │
│ │ └─src
│ │ └─views
│ │ └─modules
│ │ └─product
│ │ attr-add-or-update.vue
│ │ attr.vue
│ │ attrattrgrouprelation-add-or-update.vue
│ │ attrattrgrouprelation.vue
│ │ attrgroup-add-or-update.vue
│ │ attrgroup.vue
│ │ brand-add-or-update.vue
│ │ brand.vue
│ │ category-add-or-update.vue
│ │ category.vue
│ │ categorybrandrelation-add-or-update.vue
│ │ categorybrandrelation.vue
│ │ commentreplay-add-or-update.vue
│ │ commentreplay.vue
│ │ productattrvalue-add-or-update.vue
│ │ productattrvalue.vue
│ │ skuimages-add-or-update.vue
│ │ skuimages.vue
│ │ skuinfo-add-or-update.vue
│ │ skuinfo.vue
│ │ skusaleattrvalue-add-or-update.vue
│ │ skusaleattrvalue.vue
│ │ spucomment-add-or-update.vue
│ │ spucomment.vue
│ │ spuimages-add-or-update.vue
│ │ spuimages.vue
│ │ spuinfo-add-or-update.vue
│ │ spuinfo.vue
│ │ spuinfodesc-add-or-update.vue
│ │ spuinfodesc.vue
│ │
│ ├─generated-sources
│ │ └─annotations
│ ├─generated-test-sources
│ │ └─test-annotations
│ ├─maven-status
│ │ └─maven-compiler-plugin
│ │ └─compile
│ │ └─default-compile
│ │ createdFiles.lst
│ │ inputFiles.lst
│ │
│ └─test-classes
│ └─com
│ └─atguigu
│ └─gulimall
│ └─product
│ GulimallProductApplicationTests.class

├─gulimall-ware
│ │ .gitignore
│ │ gulimall-ware.iml
│ │ HELP.md
│ │ mvnw
│ │ mvnw.cmd
│ │ pom.xml
│ │
│ ├─.mvn
│ │ └─wrapper
│ │ maven-wrapper.jar
│ │ maven-wrapper.properties
│ │
│ ├─src
│ │ ├─main
│ │ │ ├─java
│ │ │ │ └─com
│ │ │ │ └─atguigu
│ │ │ │ └─gulimall
│ │ │ │ └─ware
│ │ │ │ │ GulimallWareApplication.java
│ │ │ │ │
│ │ │ │ ├─controller
│ │ │ │ │ WareInfoController.java
│ │ │ │ │ WareOrderTaskController.java
│ │ │ │ │ WareOrderTaskDetailController.java
│ │ │ │ │ WareSkuController.java
│ │ │ │ │
│ │ │ │ ├─dao
│ │ │ │ │ WareInfoDao.java
│ │ │ │ │ WareOrderTaskDao.java
│ │ │ │ │ WareOrderTaskDetailDao.java
│ │ │ │ │ WareSkuDao.java
│ │ │ │ │
│ │ │ │ ├─entity
│ │ │ │ │ WareInfoEntity.java
│ │ │ │ │ WareOrderTaskDetailEntity.java
│ │ │ │ │ WareOrderTaskEntity.java
│ │ │ │ │ WareSkuEntity.java
│ │ │ │ │
│ │ │ │ └─service
│ │ │ │ │ WareInfoService.java
│ │ │ │ │ WareOrderTaskDetailService.java
│ │ │ │ │ WareOrderTaskService.java
│ │ │ │ │ WareSkuService.java
│ │ │ │ │
│ │ │ │ └─impl
│ │ │ │ WareInfoServiceImpl.java
│ │ │ │ WareOrderTaskDetailServiceImpl.java
│ │ │ │ WareOrderTaskServiceImpl.java
│ │ │ │ WareSkuServiceImpl.java
│ │ │ │
│ │ │ └─resources
│ │ │ │ application.yml
│ │ │ │
│ │ │ ├─mapper
│ │ │ │ └─ware
│ │ │ │ WareInfoDao.xml
│ │ │ │ WareOrderTaskDao.xml
│ │ │ │ WareOrderTaskDetailDao.xml
│ │ │ │ WareSkuDao.xml
│ │ │ │
│ │ │ └─src
│ │ │ └─views
│ │ │ └─modules
│ │ │ └─ware
│ │ │ wareinfo-add-or-update.vue
│ │ │ wareinfo.vue
│ │ │ wareordertask-add-or-update.vue
│ │ │ wareordertask.vue
│ │ │ wareordertaskdetail-add-or-update.vue
│ │ │ wareordertaskdetail.vue
│ │ │ waresku-add-or-update.vue
│ │ │ waresku.vue
│ │ │
│ │ └─test
│ │ └─java
│ │ └─com
│ │ └─atguigu
│ │ └─gulimall
│ │ └─gulimallware
│ │ GulimallWareApplicationTests.java
│ │
│ └─target
│ ├─classes
│ │ │ application.yml
│ │ │
│ │ ├─com
│ │ │ └─atguigu
│ │ │ └─gulimall
│ │ │ ├─gulimallware
│ │ │ │ GulimallWareApplication.class
│ │ │ │
│ │ │ └─ware
│ │ │ │ GulimallWareApplication.class
│ │ │ │
│ │ │ ├─controller
│ │ │ │ WareInfoController.class
│ │ │ │ WareOrderTaskController.class
│ │ │ │ WareOrderTaskDetailController.class
│ │ │ │ WareSkuController.class
│ │ │ │
│ │ │ ├─dao
│ │ │ │ WareInfoDao.class
│ │ │ │ WareOrderTaskDao.class
│ │ │ │ WareOrderTaskDetailDao.class
│ │ │ │ WareSkuDao.class
│ │ │ │
│ │ │ ├─entity
│ │ │ │ WareInfoEntity.class
│ │ │ │ WareOrderTaskDetailEntity.class
│ │ │ │ WareOrderTaskEntity.class
│ │ │ │ WareSkuEntity.class
│ │ │ │
│ │ │ └─service
│ │ │ │ WareInfoService.class
│ │ │ │ WareOrderTaskDetailService.class
│ │ │ │ WareOrderTaskService.class
│ │ │ │ WareSkuService.class
│ │ │ │
│ │ │ └─impl
│ │ │ WareInfoServiceImpl.class
│ │ │ WareOrderTaskDetailServiceImpl.class
│ │ │ WareOrderTaskServiceImpl.class
│ │ │ WareSkuServiceImpl.class
│ │ │
│ │ ├─mapper
│ │ │ └─ware
│ │ │ WareInfoDao.xml
│ │ │ WareOrderTaskDao.xml
│ │ │ WareOrderTaskDetailDao.xml
│ │ │ WareSkuDao.xml
│ │ │
│ │ └─src
│ │ └─views
│ │ └─modules
│ │ └─ware
│ │ wareinfo-add-or-update.vue
│ │ wareinfo.vue
│ │ wareordertask-add-or-update.vue
│ │ wareordertask.vue
│ │ wareordertaskdetail-add-or-update.vue
│ │ wareordertaskdetail.vue
│ │ waresku-add-or-update.vue
│ │ waresku.vue
│ │
│ ├─generated-sources
│ │ └─annotations
│ ├─generated-test-sources
│ │ └─test-annotations
│ ├─maven-status
│ │ └─maven-compiler-plugin
│ │ └─compile
│ │ └─default-compile
│ │ createdFiles.lst
│ │ inputFiles.lst
│ │
│ └─test-classes
│ └─com
│ └─atguigu
│ └─gulimall
│ └─gulimallware
│ GulimallWareApplicationTests.class

├─renren-fast
│ │ .gitignore
│ │ docker-compose.yml
│ │ Dockerfile
│ │ LICENSE
│ │ pom.xml
│ │ README.md
│ │ renren-fast.iml
│ │
│ ├─db
│ │ mysql.sql
│ │ oracle.sql
│ │ postgresql.sql
│ │ sqlserver.sql
│ │
│ ├─src
│ │ ├─main
│ │ │ ├─java
│ │ │ │ └─io
│ │ │ │ └─renren
│ │ │ │ │ RenrenApplication.java
│ │ │ │ │
│ │ │ │ ├─common
│ │ │ │ │ ├─annotation
│ │ │ │ │ │ SysLog.java
│ │ │ │ │ │
│ │ │ │ │ ├─aspect
│ │ │ │ │ │ RedisAspect.java
│ │ │ │ │ │ SysLogAspect.java
│ │ │ │ │ │
│ │ │ │ │ ├─exception
│ │ │ │ │ │ RRException.java
│ │ │ │ │ │ RRExceptionHandler.java
│ │ │ │ │ │
│ │ │ │ │ ├─utils
│ │ │ │ │ │ ConfigConstant.java
│ │ │ │ │ │ Constant.java
│ │ │ │ │ │ DateUtils.java
│ │ │ │ │ │ HttpContextUtils.java
│ │ │ │ │ │ IPUtils.java
│ │ │ │ │ │ MapUtils.java
│ │ │ │ │ │ PageUtils.java
│ │ │ │ │ │ Query.java
│ │ │ │ │ │ R.java
│ │ │ │ │ │ RedisKeys.java
│ │ │ │ │ │ RedisUtils.java
│ │ │ │ │ │ ShiroUtils.java
│ │ │ │ │ │ SpringContextUtils.java
│ │ │ │ │ │
│ │ │ │ │ ├─validator
│ │ │ │ │ │ │ Assert.java
│ │ │ │ │ │ │ ValidatorUtils.java
│ │ │ │ │ │ │
│ │ │ │ │ │ └─group
│ │ │ │ │ │ AddGroup.java
│ │ │ │ │ │ AliyunGroup.java
│ │ │ │ │ │ Group.java
│ │ │ │ │ │ QcloudGroup.java
│ │ │ │ │ │ QiniuGroup.java
│ │ │ │ │ │ UpdateGroup.java
│ │ │ │ │ │
│ │ │ │ │ └─xss
│ │ │ │ │ HTMLFilter.java
│ │ │ │ │ SQLFilter.java
│ │ │ │ │ XssFilter.java
│ │ │ │ │ XssHttpServletRequestWrapper.java
│ │ │ │ │
│ │ │ │ ├─config
│ │ │ │ │ CorsConfig.java
│ │ │ │ │ FilterConfig.java
│ │ │ │ │ KaptchaConfig.java
│ │ │ │ │ MybatisPlusConfig.java
│ │ │ │ │ RedisConfig.java
│ │ │ │ │ ShiroConfig.java
│ │ │ │ │ SwaggerConfig.java
│ │ │ │ │
│ │ │ │ ├─datasource
│ │ │ │ │ ├─annotation
│ │ │ │ │ │ DataSource.java
│ │ │ │ │ │
│ │ │ │ │ ├─aspect
│ │ │ │ │ │ DataSourceAspect.java
│ │ │ │ │ │
│ │ │ │ │ ├─config
│ │ │ │ │ │ DynamicContextHolder.java
│ │ │ │ │ │ DynamicDataSource.java
│ │ │ │ │ │ DynamicDataSourceConfig.java
│ │ │ │ │ │ DynamicDataSourceFactory.java
│ │ │ │ │ │
│ │ │ │ │ └─properties
│ │ │ │ │ DataSourceProperties.java
│ │ │ │ │ DynamicDataSourceProperties.java
│ │ │ │ │
│ │ │ │ └─modules
│ │ │ │ ├─app
│ │ │ │ │ ├─annotation
│ │ │ │ │ │ Login.java
│ │ │ │ │ │ LoginUser.java
│ │ │ │ │ │
│ │ │ │ │ ├─config
│ │ │ │ │ │ WebMvcConfig.java
│ │ │ │ │ │
│ │ │ │ │ ├─controller
│ │ │ │ │ │ AppLoginController.java
│ │ │ │ │ │ AppRegisterController.java
│ │ │ │ │ │ AppTestController.java
│ │ │ │ │ │
│ │ │ │ │ ├─dao
│ │ │ │ │ │ UserDao.java
│ │ │ │ │ │
│ │ │ │ │ ├─entity
│ │ │ │ │ │ UserEntity.java
│ │ │ │ │ │
│ │ │ │ │ ├─form
│ │ │ │ │ │ LoginForm.java
│ │ │ │ │ │ RegisterForm.java
│ │ │ │ │ │
│ │ │ │ │ ├─interceptor
│ │ │ │ │ │ AuthorizationInterceptor.java
│ │ │ │ │ │
│ │ │ │ │ ├─resolver
│ │ │ │ │ │ LoginUserHandlerMethodArgumentResolver.java
│ │ │ │ │ │
│ │ │ │ │ ├─service
│ │ │ │ │ │ │ UserService.java
│ │ │ │ │ │ │
│ │ │ │ │ │ └─impl
│ │ │ │ │ │ UserServiceImpl.java
│ │ │ │ │ │
│ │ │ │ │ └─utils
│ │ │ │ │ JwtUtils.java
│ │ │ │ │
│ │ │ │ ├─job
│ │ │ │ │ ├─config
│ │ │ │ │ │ ScheduleConfig.java
│ │ │ │ │ │
│ │ │ │ │ ├─controller
│ │ │ │ │ │ ScheduleJobController.java
│ │ │ │ │ │ ScheduleJobLogController.java
│ │ │ │ │ │
│ │ │ │ │ ├─dao
│ │ │ │ │ │ ScheduleJobDao.java
│ │ │ │ │ │ ScheduleJobLogDao.java
│ │ │ │ │ │
│ │ │ │ │ ├─entity
│ │ │ │ │ │ ScheduleJobEntity.java
│ │ │ │ │ │ ScheduleJobLogEntity.java
│ │ │ │ │ │
│ │ │ │ │ ├─service
│ │ │ │ │ │ │ ScheduleJobLogService.java
│ │ │ │ │ │ │ ScheduleJobService.java
│ │ │ │ │ │ │
│ │ │ │ │ │ └─impl
│ │ │ │ │ │ ScheduleJobLogServiceImpl.java
│ │ │ │ │ │ ScheduleJobServiceImpl.java
│ │ │ │ │ │
│ │ │ │ │ ├─task
│ │ │ │ │ │ ITask.java
│ │ │ │ │ │ TestTask.java
│ │ │ │ │ │
│ │ │ │ │ └─utils
│ │ │ │ │ ScheduleJob.java
│ │ │ │ │ ScheduleUtils.java
│ │ │ │ │
│ │ │ │ ├─oss
│ │ │ │ │ ├─cloud
│ │ │ │ │ │ AliyunCloudStorageService.java
│ │ │ │ │ │ CloudStorageConfig.java
│ │ │ │ │ │ CloudStorageService.java
│ │ │ │ │ │ OSSFactory.java
│ │ │ │ │ │ QcloudCloudStorageService.java
│ │ │ │ │ │ QiniuCloudStorageService.java
│ │ │ │ │ │
│ │ │ │ │ ├─controller
│ │ │ │ │ │ SysOssController.java
│ │ │ │ │ │
│ │ │ │ │ ├─dao
│ │ │ │ │ │ SysOssDao.java
│ │ │ │ │ │
│ │ │ │ │ ├─entity
│ │ │ │ │ │ SysOssEntity.java
│ │ │ │ │ │
│ │ │ │ │ └─service
│ │ │ │ │ │ SysOssService.java
│ │ │ │ │ │
│ │ │ │ │ └─impl
│ │ │ │ │ SysOssServiceImpl.java
│ │ │ │ │
│ │ │ │ └─sys
│ │ │ │ ├─controller
│ │ │ │ │ AbstractController.java
│ │ │ │ │ SysConfigController.java
│ │ │ │ │ SysLogController.java
│ │ │ │ │ SysLoginController.java
│ │ │ │ │ SysMenuController.java
│ │ │ │ │ SysRoleController.java
│ │ │ │ │ SysUserController.java
│ │ │ │ │
│ │ │ │ ├─dao
│ │ │ │ │ SysCaptchaDao.java
│ │ │ │ │ SysConfigDao.java
│ │ │ │ │ SysLogDao.java
│ │ │ │ │ SysMenuDao.java
│ │ │ │ │ SysRoleDao.java
│ │ │ │ │ SysRoleMenuDao.java
│ │ │ │ │ SysUserDao.java
│ │ │ │ │ SysUserRoleDao.java
│ │ │ │ │ SysUserTokenDao.java
│ │ │ │ │
│ │ │ │ ├─entity
│ │ │ │ │ SysCaptchaEntity.java
│ │ │ │ │ SysConfigEntity.java
│ │ │ │ │ SysLogEntity.java
│ │ │ │ │ SysMenuEntity.java
│ │ │ │ │ SysRoleEntity.java
│ │ │ │ │ SysRoleMenuEntity.java
│ │ │ │ │ SysUserEntity.java
│ │ │ │ │ SysUserRoleEntity.java
│ │ │ │ │ SysUserTokenEntity.java
│ │ │ │ │
│ │ │ │ ├─form
│ │ │ │ │ PasswordForm.java
│ │ │ │ │ SysLoginForm.java
│ │ │ │ │
│ │ │ │ ├─oauth2
│ │ │ │ │ OAuth2Filter.java
│ │ │ │ │ OAuth2Realm.java
│ │ │ │ │ OAuth2Token.java
│ │ │ │ │ TokenGenerator.java
│ │ │ │ │
│ │ │ │ ├─redis
│ │ │ │ │ SysConfigRedis.java
│ │ │ │ │
│ │ │ │ └─service
│ │ │ │ │ ShiroService.java
│ │ │ │ │ SysCaptchaService.java
│ │ │ │ │ SysConfigService.java
│ │ │ │ │ SysLogService.java
│ │ │ │ │ SysMenuService.java
│ │ │ │ │ SysRoleMenuService.java
│ │ │ │ │ SysRoleService.java
│ │ │ │ │ SysUserRoleService.java
│ │ │ │ │ SysUserService.java
│ │ │ │ │ SysUserTokenService.java
│ │ │ │ │
│ │ │ │ └─impl
│ │ │ │ ShiroServiceImpl.java
│ │ │ │ SysCaptchaServiceImpl.java
│ │ │ │ SysConfigServiceImpl.java
│ │ │ │ SysLogServiceImpl.java
│ │ │ │ SysMenuServiceImpl.java
│ │ │ │ SysRoleMenuServiceImpl.java
│ │ │ │ SysRoleServiceImpl.java
│ │ │ │ SysUserRoleServiceImpl.java
│ │ │ │ SysUserServiceImpl.java
│ │ │ │ SysUserTokenServiceImpl.java
│ │ │ │
│ │ │ └─resources
│ │ │ │ application-dev.yml
│ │ │ │ application-prod.yml
│ │ │ │ application-test.yml
│ │ │ │ application.yml
│ │ │ │ banner.txt
│ │ │ │ logback-spring.xml
│ │ │ │
│ │ │ ├─mapper
│ │ │ │ ├─app
│ │ │ │ │ UserDao.xml
│ │ │ │ │
│ │ │ │ ├─job
│ │ │ │ │ ScheduleJobDao.xml
│ │ │ │ │ ScheduleJobLogDao.xml
│ │ │ │ │
│ │ │ │ ├─oss
│ │ │ │ │ SysOssDao.xml
│ │ │ │ │
│ │ │ │ └─sys
│ │ │ │ SysConfigDao.xml
│ │ │ │ SysLogDao.xml
│ │ │ │ SysMenuDao.xml
│ │ │ │ SysRoleDao.xml
│ │ │ │ SysRoleMenuDao.xml
│ │ │ │ SysUserDao.xml
│ │ │ │ SysUserRoleDao.xml
│ │ │ │ SysUserTokenDao.xml
│ │ │ │
│ │ │ └─static
│ │ │ │ favicon.ico
│ │ │ │
│ │ │ └─swagger
│ │ │ │ favicon-16x16.png
│ │ │ │ favicon-32x32.png
│ │ │ │ index.html
│ │ │ │ index.yaml
│ │ │ │ o2c.html
│ │ │ │ oauth2-redirect.html
│ │ │ │ swagger-ui-bundle.js
│ │ │ │ swagger-ui-bundle.js.map
│ │ │ │ swagger-ui-standalone-preset.js
│ │ │ │ swagger-ui-standalone-preset.js.map
│ │ │ │ swagger-ui.css
│ │ │ │ swagger-ui.css.map
│ │ │ │ swagger-ui.js
│ │ │ │ swagger-ui.js.map
│ │ │ │ swagger-ui.min.js
│ │ │ │
│ │ │ ├─css
│ │ │ │ print.css
│ │ │ │ reset.css
│ │ │ │ screen.css
│ │ │ │ style.css
│ │ │ │ typography.css
│ │ │ │
│ │ │ ├─fonts
│ │ │ │ DroidSans-Bold.ttf
│ │ │ │ DroidSans.ttf
│ │ │ │
│ │ │ ├─images
│ │ │ │ collapse.gif
│ │ │ │ expand.gif
│ │ │ │ explorer_icons.png
│ │ │ │ favicon-16x16.png
│ │ │ │ favicon-32x32.png
│ │ │ │ favicon.ico
│ │ │ │ logo_small.png
│ │ │ │ pet_store_api.png
│ │ │ │ throbber.gif
│ │ │ │ wordnik_api.png
│ │ │ │
│ │ │ ├─lang
│ │ │ │ en.js
│ │ │ │ translator.js
│ │ │ │ zh-cn.js
│ │ │ │
│ │ │ └─lib
│ │ │ backbone-min.js
│ │ │ es5-shim.js
│ │ │ handlebars-4.0.5.js
│ │ │ highlight.9.1.0.pack.js
│ │ │ highlight.9.1.0.pack_extended.js
│ │ │ jquery-1.8.0.min.js
│ │ │ jquery.ba-bbq.min.js
│ │ │ jquery.slideto.min.js
│ │ │ jquery.wiggle.min.js
│ │ │ js-yaml.min.js
│ │ │ jsoneditor.min.js
│ │ │ lodash.min.js
│ │ │ marked.js
│ │ │ object-assign-pollyfill.js
│ │ │ sanitize-html.min.js
│ │ │ swagger-oauth.js
│ │ │
│ │ └─test
│ │ └─java
│ │ └─io
│ │ └─renren
│ │ │ DynamicDataSourceTest.java
│ │ │ JwtTest.java
│ │ │ RedisTest.java
│ │ │
│ │ └─service
│ │ DynamicDataSourceTestService.java
│ │
│ └─target
│ ├─classes
│ │ │ application-dev.yml
│ │ │ application-prod.yml
│ │ │ application-test.yml
│ │ │ application.yml
│ │ │ banner.txt
│ │ │ logback-spring.xml
│ │ │
│ │ ├─io
│ │ │ └─renren
│ │ │ │ RenrenApplication.class
│ │ │ │
│ │ │ ├─common
│ │ │ │ ├─annotation
│ │ │ │ │ SysLog.class
│ │ │ │ │
│ │ │ │ ├─aspect
│ │ │ │ │ RedisAspect.class
│ │ │ │ │ SysLogAspect.class
│ │ │ │ │
│ │ │ │ ├─exception
│ │ │ │ │ RRException.class
│ │ │ │ │ RRExceptionHandler.class
│ │ │ │ │
│ │ │ │ ├─utils
│ │ │ │ │ ConfigConstant.class
│ │ │ │ │ Constant$CloudService.class
│ │ │ │ │ Constant$MenuType.class
│ │ │ │ │ Constant$ScheduleStatus.class
│ │ │ │ │ Constant.class
│ │ │ │ │ DateUtils.class
│ │ │ │ │ HttpContextUtils.class
│ │ │ │ │ IPUtils.class
│ │ │ │ │ MapUtils.class
│ │ │ │ │ PageUtils.class
│ │ │ │ │ Query.class
│ │ │ │ │ R.class
│ │ │ │ │ RedisKeys.class
│ │ │ │ │ RedisUtils.class
│ │ │ │ │ ShiroUtils.class
│ │ │ │ │ SpringContextUtils.class
│ │ │ │ │
│ │ │ │ ├─validator
│ │ │ │ │ │ Assert.class
│ │ │ │ │ │ ValidatorUtils.class
│ │ │ │ │ │
│ │ │ │ │ └─group
│ │ │ │ │ AddGroup.class
│ │ │ │ │ AliyunGroup.class
│ │ │ │ │ Group.class
│ │ │ │ │ QcloudGroup.class
│ │ │ │ │ QiniuGroup.class
│ │ │ │ │ UpdateGroup.class
│ │ │ │ │
│ │ │ │ └─xss
│ │ │ │ HTMLFilter.class
│ │ │ │ SQLFilter.class
│ │ │ │ XssFilter.class
│ │ │ │ XssHttpServletRequestWrapper$1.class
│ │ │ │ XssHttpServletRequestWrapper.class
│ │ │ │
│ │ │ ├─config
│ │ │ │ CorsConfig.class
│ │ │ │ FilterConfig.class
│ │ │ │ KaptchaConfig.class
│ │ │ │ MybatisPlusConfig.class
│ │ │ │ RedisConfig.class
│ │ │ │ ShiroConfig.class
│ │ │ │ SwaggerConfig.class
│ │ │ │
│ │ │ ├─datasource
│ │ │ │ ├─annotation
│ │ │ │ │ DataSource.class
│ │ │ │ │
│ │ │ │ ├─aspect
│ │ │ │ │ DataSourceAspect.class
│ │ │ │ │
│ │ │ │ ├─config
│ │ │ │ │ DynamicContextHolder$1.class
│ │ │ │ │ DynamicContextHolder.class
│ │ │ │ │ DynamicDataSource.class
│ │ │ │ │ DynamicDataSourceConfig.class
│ │ │ │ │ DynamicDataSourceFactory.class
│ │ │ │ │
│ │ │ │ └─properties
│ │ │ │ DataSourceProperties.class
│ │ │ │ DynamicDataSourceProperties.class
│ │ │ │
│ │ │ └─modules
│ │ │ ├─app
│ │ │ │ ├─annotation
│ │ │ │ │ Login.class
│ │ │ │ │ LoginUser.class
│ │ │ │ │
│ │ │ │ ├─config
│ │ │ │ │ WebMvcConfig.class
│ │ │ │ │
│ │ │ │ ├─controller
│ │ │ │ │ AppLoginController.class
│ │ │ │ │ AppRegisterController.class
│ │ │ │ │ AppTestController.class
│ │ │ │ │
│ │ │ │ ├─dao
│ │ │ │ │ UserDao.class
│ │ │ │ │
│ │ │ │ ├─entity
│ │ │ │ │ UserEntity.class
│ │ │ │ │
│ │ │ │ ├─form
│ │ │ │ │ LoginForm.class
│ │ │ │ │ RegisterForm.class
│ │ │ │ │
│ │ │ │ ├─interceptor
│ │ │ │ │ AuthorizationInterceptor.class
│ │ │ │ │
│ │ │ │ ├─resolver
│ │ │ │ │ LoginUserHandlerMethodArgumentResolver.class
│ │ │ │ │
│ │ │ │ ├─service
│ │ │ │ │ │ UserService.class
│ │ │ │ │ │
│ │ │ │ │ └─impl
│ │ │ │ │ UserServiceImpl.class
│ │ │ │ │
│ │ │ │ └─utils
│ │ │ │ JwtUtils.class
│ │ │ │
│ │ │ ├─job
│ │ │ │ ├─config
│ │ │ │ │ ScheduleConfig.class
│ │ │ │ │
│ │ │ │ ├─controller
│ │ │ │ │ ScheduleJobController.class
│ │ │ │ │ ScheduleJobLogController.class
│ │ │ │ │
│ │ │ │ ├─dao
│ │ │ │ │ ScheduleJobDao.class
│ │ │ │ │ ScheduleJobLogDao.class
│ │ │ │ │
│ │ │ │ ├─entity
│ │ │ │ │ ScheduleJobEntity.class
│ │ │ │ │ ScheduleJobLogEntity.class
│ │ │ │ │
│ │ │ │ ├─service
│ │ │ │ │ │ ScheduleJobLogService.class
│ │ │ │ │ │ ScheduleJobService.class
│ │ │ │ │ │
│ │ │ │ │ └─impl
│ │ │ │ │ ScheduleJobLogServiceImpl.class
│ │ │ │ │ ScheduleJobServiceImpl.class
│ │ │ │ │
│ │ │ │ ├─task
│ │ │ │ │ ITask.class
│ │ │ │ │ TestTask.class
│ │ │ │ │
│ │ │ │ └─utils
│ │ │ │ ScheduleJob.class
│ │ │ │ ScheduleUtils.class
│ │ │ │
│ │ │ ├─oss
│ │ │ │ ├─cloud
│ │ │ │ │ AliyunCloudStorageService.class
│ │ │ │ │ CloudStorageConfig.class
│ │ │ │ │ CloudStorageService.class
│ │ │ │ │ OSSFactory.class
│ │ │ │ │ QcloudCloudStorageService.class
│ │ │ │ │ QiniuCloudStorageService.class
│ │ │ │ │
│ │ │ │ ├─controller
│ │ │ │ │ SysOssController.class
│ │ │ │ │
│ │ │ │ ├─dao
│ │ │ │ │ SysOssDao.class
│ │ │ │ │
│ │ │ │ ├─entity
│ │ │ │ │ SysOssEntity.class
│ │ │ │ │
│ │ │ │ └─service
│ │ │ │ │ SysOssService.class
│ │ │ │ │
│ │ │ │ └─impl
│ │ │ │ SysOssServiceImpl.class
│ │ │ │
│ │ │ └─sys
│ │ │ ├─controller
│ │ │ │ AbstractController.class
│ │ │ │ SysConfigController.class
│ │ │ │ SysLogController.class
│ │ │ │ SysLoginController.class
│ │ │ │ SysMenuController.class
│ │ │ │ SysRoleController.class
│ │ │ │ SysUserController.class
│ │ │ │
│ │ │ ├─dao
│ │ │ │ SysCaptchaDao.class
│ │ │ │ SysConfigDao.class
│ │ │ │ SysLogDao.class
│ │ │ │ SysMenuDao.class
│ │ │ │ SysRoleDao.class
│ │ │ │ SysRoleMenuDao.class
│ │ │ │ SysUserDao.class
│ │ │ │ SysUserRoleDao.class
│ │ │ │ SysUserTokenDao.class
│ │ │ │
│ │ │ ├─entity
│ │ │ │ SysCaptchaEntity.class
│ │ │ │ SysConfigEntity.class
│ │ │ │ SysLogEntity.class
│ │ │ │ SysMenuEntity.class
│ │ │ │ SysRoleEntity.class
│ │ │ │ SysRoleMenuEntity.class
│ │ │ │ SysUserEntity.class
│ │ │ │ SysUserRoleEntity.class
│ │ │ │ SysUserTokenEntity.class
│ │ │ │
│ │ │ ├─form
│ │ │ │ PasswordForm.class
│ │ │ │ SysLoginForm.class
│ │ │ │
│ │ │ ├─oauth2
│ │ │ │ OAuth2Filter.class
│ │ │ │ OAuth2Realm.class
│ │ │ │ OAuth2Token.class
│ │ │ │ TokenGenerator.class
│ │ │ │
│ │ │ ├─redis
│ │ │ │ SysConfigRedis.class
│ │ │ │
│ │ │ └─service
│ │ │ │ ShiroService.class
│ │ │ │ SysCaptchaService.class
│ │ │ │ SysConfigService.class
│ │ │ │ SysLogService.class
│ │ │ │ SysMenuService.class
│ │ │ │ SysRoleMenuService.class
│ │ │ │ SysRoleService.class
│ │ │ │ SysUserRoleService.class
│ │ │ │ SysUserService.class
│ │ │ │ SysUserTokenService.class
│ │ │ │
│ │ │ └─impl
│ │ │ ShiroServiceImpl.class
│ │ │ SysCaptchaServiceImpl.class
│ │ │ SysConfigServiceImpl.class
│ │ │ SysLogServiceImpl.class
│ │ │ SysMenuServiceImpl.class
│ │ │ SysRoleMenuServiceImpl.class
│ │ │ SysRoleServiceImpl.class
│ │ │ SysUserRoleServiceImpl.class
│ │ │ SysUserServiceImpl.class
│ │ │ SysUserTokenServiceImpl.class
│ │ │
│ │ ├─mapper
│ │ │ ├─app
│ │ │ │ UserDao.xml
│ │ │ │
│ │ │ ├─job
│ │ │ │ ScheduleJobDao.xml
│ │ │ │ ScheduleJobLogDao.xml
│ │ │ │
│ │ │ ├─oss
│ │ │ │ SysOssDao.xml
│ │ │ │
│ │ │ └─sys
│ │ │ SysConfigDao.xml
│ │ │ SysLogDao.xml
│ │ │ SysMenuDao.xml
│ │ │ SysRoleDao.xml
│ │ │ SysRoleMenuDao.xml
│ │ │ SysUserDao.xml
│ │ │ SysUserRoleDao.xml
│ │ │ SysUserTokenDao.xml
│ │ │
│ │ ├─META-INF
│ │ │ spring-configuration-metadata.json
│ │ │
│ │ └─static
│ │ │ favicon.ico
│ │ │
│ │ └─swagger
│ │ │ favicon-16x16.png
│ │ │ favicon-32x32.png
│ │ │ index.html
│ │ │ index.yaml
│ │ │ o2c.html
│ │ │ oauth2-redirect.html
│ │ │ swagger-ui-bundle.js
│ │ │ swagger-ui-bundle.js.map
│ │ │ swagger-ui-standalone-preset.js
│ │ │ swagger-ui-standalone-preset.js.map
│ │ │ swagger-ui.css
│ │ │ swagger-ui.css.map
│ │ │ swagger-ui.js
│ │ │ swagger-ui.js.map
│ │ │ swagger-ui.min.js
│ │ │
│ │ ├─css
│ │ │ print.css
│ │ │ reset.css
│ │ │ screen.css
│ │ │ style.css
│ │ │ typography.css
│ │ │
│ │ ├─fonts
│ │ │ DroidSans-Bold.ttf
│ │ │ DroidSans.ttf
│ │ │
│ │ ├─images
│ │ │ collapse.gif
│ │ │ expand.gif
│ │ │ explorer_icons.png
│ │ │ favicon-16x16.png
│ │ │ favicon-32x32.png
│ │ │ favicon.ico
│ │ │ logo_small.png
│ │ │ pet_store_api.png
│ │ │ throbber.gif
│ │ │ wordnik_api.png
│ │ │
│ │ ├─lang
│ │ │ en.js
│ │ │ translator.js
│ │ │ zh-cn.js
│ │ │
│ │ └─lib
│ │ backbone-min.js
│ │ es5-shim.js
│ │ handlebars-4.0.5.js
│ │ highlight.9.1.0.pack.js
│ │ highlight.9.1.0.pack_extended.js
│ │ jquery-1.8.0.min.js
│ │ jquery.ba-bbq.min.js
│ │ jquery.slideto.min.js
│ │ jquery.wiggle.min.js
│ │ js-yaml.min.js
│ │ jsoneditor.min.js
│ │ lodash.min.js
│ │ marked.js
│ │ object-assign-pollyfill.js
│ │ sanitize-html.min.js
│ │ swagger-oauth.js
│ │
│ └─generated-sources
│ └─annotations
└─renren-generator
│ .gitignore
│ LICENSE
│ pom.xml
│ README.md
│ renren-generator.iml

├─src
│ ├─main
│ │ ├─java
│ │ │ └─io
│ │ │ └─renren
│ │ │ │ RenrenApplicationGenerator.java
│ │ │ │
│ │ │ ├─adaptor
│ │ │ │ MongoTableInfoAdaptor.java
│ │ │ │
│ │ │ ├─config
│ │ │ │ DbConfig.java
│ │ │ │ MongoCondition.java
│ │ │ │ MongoConfig.java
│ │ │ │ MongoManager.java
│ │ │ │ MongoNullCondition.java
│ │ │ │
│ │ │ ├─controller
│ │ │ │ SysGeneratorController.java
│ │ │ │
│ │ │ ├─dao
│ │ │ │ GeneratorDao.java
│ │ │ │ MongoDBGeneratorDao.java
│ │ │ │ MySQLGeneratorDao.java
│ │ │ │ OracleGeneratorDao.java
│ │ │ │ PostgreSQLGeneratorDao.java
│ │ │ │ SQLServerGeneratorDao.java
│ │ │ │
│ │ │ ├─entity
│ │ │ │ │ ColumnEntity.java
│ │ │ │ │ TableEntity.java
│ │ │ │ │
│ │ │ │ └─mongo
│ │ │ │ MongoDefinition.java
│ │ │ │ MongoGeneratorEntity.java
│ │ │ │ Type.java
│ │ │ │
│ │ │ ├─factory
│ │ │ │ MongoDBCollectionFactory.java
│ │ │ │
│ │ │ ├─service
│ │ │ │ SysGeneratorService.java
│ │ │ │
│ │ │ └─utils
│ │ │ Constant.java
│ │ │ DateUtils.java
│ │ │ GenUtils.java
│ │ │ MongoScanner.java
│ │ │ PageUtils.java
│ │ │ Query.java
│ │ │ R.java
│ │ │ RRException.java
│ │ │ RRExceptionHandler.java
│ │ │
│ │ └─resources
│ │ │ application.yml
│ │ │ generator.properties
│ │ │
│ │ ├─mapper
│ │ │ MySQLGeneratorDao.xml
│ │ │ OracleGeneratorDao.xml
│ │ │ PostgreSQLGeneratorDao.xml
│ │ │ SQLServerGeneratorDao.xml
│ │ │
│ │ ├─static
│ │ │ │ favicon.ico
│ │ │ │
│ │ │ ├─css
│ │ │ │ AdminLTE.min.css
│ │ │ │ all-skins.min.css
│ │ │ │ bootstrap.min.css
│ │ │ │ font-awesome.min.css
│ │ │ │ main.css
│ │ │ │
│ │ │ ├─fonts
│ │ │ │ fontawesome-webfont.eot
│ │ │ │ fontawesome-webfont.svg
│ │ │ │ fontawesome-webfont.ttf
│ │ │ │ fontawesome-webfont.woff
│ │ │ │ fontawesome-webfont.woff2
│ │ │ │ FontAwesome.otf
│ │ │ │ glyphicons-halflings-regular.eot
│ │ │ │ glyphicons-halflings-regular.svg
│ │ │ │ glyphicons-halflings-regular.ttf
│ │ │ │ glyphicons-halflings-regular.woff
│ │ │ │ glyphicons-halflings-regular.woff2
│ │ │ │
│ │ │ ├─js
│ │ │ │ common.js
│ │ │ │ generator.js
│ │ │ │ index.js
│ │ │ │
│ │ │ ├─libs
│ │ │ │ app.js
│ │ │ │ app.min.js
│ │ │ │ bootstrap.min.js
│ │ │ │ fastclick.min.js
│ │ │ │ jquery.min.js
│ │ │ │ jquery.slimscroll.min.js
│ │ │ │ router.js
│ │ │ │ vue.min.js
│ │ │ │
│ │ │ └─plugins
│ │ │ ├─jqgrid
│ │ │ │ grid.locale-cn.js
│ │ │ │ jquery.jqGrid.min.js
│ │ │ │ ui.jqgrid-bootstrap-ui.css
│ │ │ │ ui.jqgrid-bootstrap.css
│ │ │ │ ui.jqgrid.css
│ │ │ │
│ │ │ └─layer
│ │ │ │ layer.js
│ │ │ │
│ │ │ ├─mobile
│ │ │ │ │ layer.js
│ │ │ │ │
│ │ │ │ └─need
│ │ │ │ layer.css
│ │ │ │
│ │ │ └─skin
│ │ │ ├─default
│ │ │ │ icon-ext.png
│ │ │ │ icon.png
│ │ │ │ layer.css
│ │ │ │ loading-0.gif
│ │ │ │ loading-1.gif
│ │ │ │ loading-2.gif
│ │ │ │
│ │ │ └─moon
│ │ │ default.png
│ │ │ style.css
│ │ │
│ │ ├─template
│ │ │ add-or-update.vue.vm
│ │ │ Controller.java.vm
│ │ │ Dao.java.vm
│ │ │ Dao.xml.vm
│ │ │ Entity.java.vm
│ │ │ index.vue.vm
│ │ │ menu.sql.vm
│ │ │ MongoChildrenEntity.java.vm
│ │ │ MongoEntity.java.vm
│ │ │ Service.java.vm
│ │ │ ServiceImpl.java.vm
│ │ │
│ │ └─views
│ │ generator.html
│ │ index.html
│ │ main.html
│ │
│ └─test
│ └─java
│ └─io
│ └─renren
│ RenrenApplicationTests.java

└─target
├─classes
│ │ application.yml
│ │ generator.properties
│ │
│ ├─io
│ │ └─renren
│ │ │ RenrenApplicationGenerator.class
│ │ │
│ │ ├─adaptor
│ │ │ MongoTableInfoAdaptor.class
│ │ │
│ │ ├─config
│ │ │ DbConfig.class
│ │ │ MongoCondition.class
│ │ │ MongoConfig.class
│ │ │ MongoManager.class
│ │ │ MongoNullCondition.class
│ │ │
│ │ ├─controller
│ │ │ SysGeneratorController.class
│ │ │
│ │ ├─dao
│ │ │ GeneratorDao.class
│ │ │ MongoDBGeneratorDao.class
│ │ │ MySQLGeneratorDao.class
│ │ │ OracleGeneratorDao.class
│ │ │ PostgreSQLGeneratorDao.class
│ │ │ SQLServerGeneratorDao.class
│ │ │
│ │ ├─entity
│ │ │ │ ColumnEntity.class
│ │ │ │ TableEntity.class
│ │ │ │
│ │ │ └─mongo
│ │ │ MongoDefinition.class
│ │ │ MongoGeneratorEntity.class
│ │ │ Type.class
│ │ │
│ │ ├─factory
│ │ │ MongoDBCollectionFactory.class
│ │ │
│ │ ├─service
│ │ │ SysGeneratorService.class
│ │ │
│ │ └─utils
│ │ Constant.class
│ │ DateUtils.class
│ │ GenUtils.class
│ │ MongoScanner$ForkJoinGetProcessName.class
│ │ MongoScanner$ForkJoinProcessType.class
│ │ MongoScanner.class
│ │ PageUtils.class
│ │ Query.class
│ │ R.class
│ │ RRException.class
│ │ RRExceptionHandler.class
│ │
│ ├─mapper
│ │ MySQLGeneratorDao.xml
│ │ OracleGeneratorDao.xml
│ │ PostgreSQLGeneratorDao.xml
│ │ SQLServerGeneratorDao.xml
│ │
│ ├─static
│ │ │ favicon.ico
│ │ │
│ │ ├─css
│ │ │ AdminLTE.min.css
│ │ │ all-skins.min.css
│ │ │ bootstrap.min.css
│ │ │ font-awesome.min.css
│ │ │ main.css
│ │ │
│ │ ├─fonts
│ │ │ fontawesome-webfont.eot
│ │ │ fontawesome-webfont.svg
│ │ │ fontawesome-webfont.ttf
│ │ │ fontawesome-webfont.woff
│ │ │ fontawesome-webfont.woff2
│ │ │ FontAwesome.otf
│ │ │ glyphicons-halflings-regular.eot
│ │ │ glyphicons-halflings-regular.svg
│ │ │ glyphicons-halflings-regular.ttf
│ │ │ glyphicons-halflings-regular.woff
│ │ │ glyphicons-halflings-regular.woff2
│ │ │
│ │ ├─js
│ │ │ common.js
│ │ │ generator.js
│ │ │ index.js
│ │ │
│ │ ├─libs
│ │ │ app.js
│ │ │ app.min.js
│ │ │ bootstrap.min.js
│ │ │ fastclick.min.js
│ │ │ jquery.min.js
│ │ │ jquery.slimscroll.min.js
│ │ │ router.js
│ │ │ vue.min.js
│ │ │
│ │ └─plugins
│ │ ├─jqgrid
│ │ │ grid.locale-cn.js
│ │ │ jquery.jqGrid.min.js
│ │ │ ui.jqgrid-bootstrap-ui.css
│ │ │ ui.jqgrid-bootstrap.css
│ │ │ ui.jqgrid.css
│ │ │
│ │ └─layer
│ │ │ layer.js
│ │ │
│ │ ├─mobile
│ │ │ │ layer.js
│ │ │ │
│ │ │ └─need
│ │ │ layer.css
│ │ │
│ │ └─skin
│ │ ├─default
│ │ │ icon-ext.png
│ │ │ icon.png
│ │ │ layer.css
│ │ │ loading-0.gif
│ │ │ loading-1.gif
│ │ │ loading-2.gif
│ │ │
│ │ └─moon
│ │ default.png
│ │ style.css
│ │
│ ├─template
│ │ add-or-update.vue.vm
│ │ Controller.java.vm
│ │ Dao.java.vm
│ │ Dao.xml.vm
│ │ Entity.java.vm
│ │ index.vue.vm
│ │ menu.sql.vm
│ │ MongoChildrenEntity.java.vm
│ │ MongoEntity.java.vm
│ │ Service.java.vm
│ │ ServiceImpl.java.vm
│ │
│ └─views
│ generator.html
│ index.html
│ main.html

└─generated-sources
└─annotations

四.项目前置知识

1.SpringCloud Alibaba

1.1简介

SpringCloud Alibaba致力于提供微服务开发的一站式解决方案。此项目包含开发分布式应用微服务必须的组件,方便开发者通过SpringCloud编程模型轻松使用这些组件来开发分布式的应用服务。

依托SpringCloud Alibaba,你只需要添加一些注解和少量的配置,就可以将SpringCloud应用接入阿里微服务解决方案,通过阿里中间件来迅速搭建分布式应用系统。

github的地址:https://github.com/alibaba/spring-cloud-alibaba

1.2为什么使用SpringCloud Alibaba?

SpringCloud的不足:

  • 部分组件停止维护,给开发带来不便
  • 环境搭建复杂,没有完善的可视化界面,需要大量的二次开发和定制
  • 配置复杂,难以上手,部分配置差别难以区分和合理应用

SpringCloud Alibaba的优势:

阿里使用过的组件经历了考验,性能强悍,设计合理,开源成套的搭配和可视化的界面给开发带来极大的便利,搭建简单,学习成本低。

1.3 SpringCloud Alibaba最终的技术搭配方案

SpringCloud Alibaba - Nacos :注册中心(服务发现/注册)

SpringCloud Alibaba - Nacos :配置中心(动态的配置管理)

SpringCloud - Ribbon: 负载均衡

SpringCloud - Feign: 声明式HTTP客户端(远程调用服务/服务调用)

SpringCloud Alibaba - Sentinel:服务容错(限流、降级、熔断)

SpringCloud - Gateway: API网关(webflux编程模式)

SpringCloud - Sleuth:调用链监控

SpringCloud Alibaba - Seata:原Fescar,即分布式事务解决方案

image-20230519161707857

1.4 项目中使用SpringCloud Alibaba

在common模块中添加如下的依赖

<dependencyManagement>
<dependencies>
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-alibaba-dependencies</artifactId>
<version>2.2.0.RELEASE</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>

1.5 SpringCloud Alibaba的组件

1.Nacos注册中心

image-20230519162859782

简介

Nacos 是阿里巴巴推出来的一个新开源项目,这是一个更易于构建云原生应用的动态服务发现、配置管理和服务管理平台。

Nacos 致力于帮助您发现、配置和管理微服务。Nacos 提供了一组简单易用的特性集,帮助您快速实现动态服务发现、服务配置、服务元数据及流量管理。

Nacos 帮助您更敏捷和容易地构建、交付和管理微服务平台。 Nacos 是构建以“服务”为中心的现代应用架构 (例如微服务范式、云原生范式) 的服务基础设施

Nacos 支持如下核心特性

1)服务发现: 支持 DNS 与 RPC 服务发现,也提供原生 SDK 、OpenAPI 等多种服务注册方式和 DNS、HTTP 与 API 等多种服务发现方式。
2)服务健康监测: Nacos 提供对服务的实时的健康检查,阻止向不健康的主机或服务实例发送请求。
3)动态配置服务: Nacos 提供配置统一管理功能,能够帮助我们将配置以中心化、外部化和动态化的方式管理所有环境的应用配置和服务配置。
4)动态 DNS 服务: Nacos 支持动态 DNS 服务权重路由,能够让我们很容易地实现中间层负载均衡、更灵活的路由策略、流量控制以及数据中心内网的简单 DNS 解析服务。
5)服务及其元数据管理: Nacos 支持从微服务平台建设的视角管理数据中心的所有服务及元数据,包括管理服务的描述、生命周期、服务的静态依赖分析、服务的健康状态、服务的流量管理、路由及安全策略、服务的 SLA 以及最首要的 metrics 统计数据。

使用nacos

使用之前需要下载nacos并启动nacos的服务

image-20230519164012950

在common工程中添加依赖

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

配置中心中配置Nacos的地址

spring:
application:
name: gulimall-coupon #指定服务的名字,指定了名字才能注册到注册中心中
cloud:
nacos:
server-addr: 127.0.0.1:8848 #指定注册中心的地址信息

在每个启动类上使用注解开启服务的注册与发现功能

//开启服务的注册与发现功能
@EnableDiscoveryClient

查看注册服务列表

通过访问: http://localhost:8848/nacos

image-20230519165606784

2.Feign声明式远程调用

简介

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

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

使用

引入依赖,每个模块都需要引入这个依赖

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

编写代码测试远程调用,这里我们以用户模块调用优惠卷模块来获取用户的拥有的优惠卷为例来测试远程调用。

会员模块提供获取用户名下优惠卷的方法

  /**
* 获取用户明名下的优惠卷
*/
@RequestMapping("member/list")
public R membercoupons(){
CouponEntity couponEntity = new CouponEntity();
couponEntity.setCouponName("满一百减五十");
return R.ok().put("coupons",Arrays.asList(couponEntity));
}

用户模块远程调用优惠卷模块

远程调用别的服务端步骤

  1. 引入open-feign的依赖

  2. 调用端编写一个接口,告诉SpringCloud这个接口需要调用远程服务,

    声明接口的每一个方法都是调用哪个远程服务的那个请求

  3. 开启远程调用的功能,在调用端的启动类上添加**@EnableFeignClients**注解

在用户模块创建一个Feign包(维护的时候见名知义),声明调用的接口,在启动类上添加注解

//注解
//basePackages声明的远程调用的接口所在的包
@EnableFeignClients(basePackages = "com.atguigu.gulimall.member.feign")
package com.atguigu.gulimall.member.feign;

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

/**
* @author GongChangjiang
* @version 1.0
* @Date 2023/5/19
* @Description 远程调用优惠卷服务
*/
@FeignClient("gulimall-coupon")//告诉springCloud这是一个远程客户端,并指定要调用服务的服务名
public interface CouponFeignService {


/**
* 这里需要写全路径
* 远程调用优惠卷模块中获取优惠卷信息的方法
*/
@RequestMapping("/coupon/coupon/member/list")
public R membercoupons();
}

测试远程调用

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

测试的时候出现的小问题

<!--No Feign Client for loadBalancing defined. Did you forget to include spring-cloud-starter-loadbalanc
出现这个问题是没有loadbalanc,但是nacos中ribbon会造成loadbalanc包失效,在common的pom中的依赖改成如下的依赖-->
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
<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-loadbalancer</artifactId>
<version>2.2.2.RELEASE</version>
</dependency>

处理完上面的报错之后,浏览器访问 http://localhost:8000/member/member/coupons ,显示如下的内容,说明远程调用成功

image-20230519210917423

3.Nacos配置中心

简介

简介见上面的nacos注册中心,这里的Nacos配置中心和上面的nacos注册中心是同一个东西

使用

1.在common中引入nacos配置中心的依赖,和上面的注册中心的依赖不一样

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

2.给需要配置中心管理的模块下创建配置文件bootstrap.properties,这个文件会优先于application.properties读取

#服务名
spring.application.name=gulimall-coupon
#Nacos配置中心的地址的地址
#注意看这里多了一个config,和模块中nacos的地址还是有区别的
spring.cloud.nacos.config.server-addr=127.0.0.1:8848

3.测试

3.1.在application.properties配置文件中添加如下的配置

coupon.user.name=zhansan
coupon.user.age=18

3.2 编写接口并访问测试


/**
* 获取配置文件中的配置信息
*/
@Value("${user.name}")
private String name;
@Value("${user.age}")
private Integer age;

/**
* 测试优先获取哪个配置文件的值
*/
@RequestMapping("/test")
public R test(){
return R.ok().put("name",name).put("age",age);
}

测试的结果如下:

image-20230519213945320

3.3 需求:应用不重启,实时的修改配置文件中的值

3.3.1 在Nacos配置中心中创建一个名为 应用名.properties(例如:gulimall-coupon.properties) 的配置文件

进入nacos管理的可视化页面,点击配置列表,点击 + ,新建一个配置 ,同时将配置文件中name值改为lihua

image-20230519215420623

发布之后,就可以在配置列表中看到这个配置了

image-20230519215232782

重启项目测试,配置中心的配置是否生效

没有生效的话,添加这个依赖,springBoot2.4以上的版本需要

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

生效的话,这时刷新页面,可以看到name已经由zhangsan变成lihua了

image-20230519220357933

3.3.2 想让配置文件实时生效的话,这时我们还要添加以下的注解,然后重启应用,让注解生效

注意: @RefreshScope 这里的注解添加在Controller上,不是添加在启动类上

//给需要配置中心统一配置的模块的Controller上上添加@RefreshScope的注解
@RefreshScope

最终测试

这时我们在配置中心中修改配置并发布,这些配置就可以实时的生效了

我们在配置中心修改name和age的值,并发布

image-20230519220922701

这时刷新请求接口的页面,我们可以看到页面中的数据发生了实时的变化

image-20230519223403139

总结

实现nacos配置中心统一配置的流程

  1. 引入依赖

    <dependency>
    <groupId>com.alibaba.cloud</groupId>
    <artifactId>spring-cloud-starter-alibaba-nacos-config</artifactId>
    </dependency>
  2. 创建一个bootstrap.properties的配置文件,添加如下的内容:

    #服务名
    spring.application.name=gulimall-coupon
    #Nacos配置中心的地址
    #注意看这里多了一个config,和模块中nacos的地址还是有区别的
    spring.cloud.nacos.config.server-addr=127.0.0.1:8848
  3. 需要给配置中心中新建一个数据集(Data Id)为 应用名.properties(例如:gulimall-coupon.properties)的配置。默认规则: 应用名.properties

  4. 给 应用名.properties 添加任何配置 这些配置都可以在配置中心配置生效

  5. 在Controller上添加**@RefreshScope**注解就可以实时的动态刷新配置,配置中心更新了配置并发布了之后,配置实时的生效。

注意

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

细节

命名空间: 配置隔离

默认:public(保留空间)

1.可以分别设置开发、测试、生产环境的命名空间,不同的环境下使用不同的命名空间中的配置

通过命令空间实现环境隔离

bootstrap.properties设置使用哪个命名空间,可以在配置文件中添加如下的配置

image-20230519225637848

# 切换名称空间
# 可以选择对应的命名空间 # 写上对应环境的命名空间ID
# 使用唯一的命名空间ID
spring.cloud.nacos.config.namespace=8a84e478-92d0-4581-a316-b0274fc76eb8

2.可以为每一个微服务可以创建自己的命名空间,让每个服务的配置放在每个服务的命名空间下

配置集:所有的配置的集合

配置集ID: 类似文件名

Data Id: 就是配置集ID

配置分组:

默认所有的配置集都属于 : DEFAULT_GROUP

例如在生产环境的命名空间下,淘宝的配置分组有以下几个: 双十一,688,平时

#指定配置分组
spring.cloud.nacos.config.group=1111

Tips:同一个命名空间下可能会有不同的配置分组

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

从配置中心中读取多个配置集(配置文件):

可以在bootstrap.properties添加以下的配置

#第一个配置文件
#配置文件名
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

#第二个配置文件
spring.cloud.nacos.config.ext-config[1].data-id=mybatis.yml
spring.cloud.nacos.config.ext-config[1].group=dev
spring.cloud.nacos.config.ext-config[1].refresh=true

#第三个配置文件
spring.cloud.nacos.config.ext-config[2].data-id=others.yml
spring.cloud.nacos.config.ext-config[2].group=dev
spring.cloud.nacos.config.ext-config[2].refresh=true

总结:

1.微服务的任何配置信息,任何配置文件都可以放在配置中心中

2.加载配置中心的哪些配置文件,只需要在bootstrap.properties说明加载哪些配置文件即可

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

4.Gateway网关

image-20230519233741029

简介

网关最为流量的入口,常用的功能包括**路由转发**、**权限校验**、**限流控制**等。SpringCloud gateway是springcloud官方提供的第二代网关框架,取代了Zull网关。

网关的功能

  • 针对所有请求进行登陆统一鉴权(登录态)限流缓存日志(用户打点)
  • 可以根据不同的请求路径pattern,来进行请求的鉴权、转发、和拒绝。
  • 协议转化。针对后段多种不同的协议,在网关层统一处理后以HTTP对外提供服务。
  • 提供统一的错误码
  • 请求转发,并且可以基于网关实现内网与外网的隔离

使用

  1. 创建一个springboot initializr的工程 ,勾选上Gateway的依赖

  2. 在网关模块的pom.xml文件中添加上对common工程的依赖

  3. 在网关的启动类上添加上**@EnableDiscoveryClient**的注解,把网关注册到注册中心

  4. 配置nacos的地址和服务名信息

    #配置网关的地址
    spring.cloud.nacos.config.server-addr=127.0.0.1:8848
    #服务名
    spring.application.name=gulimall-gateway
  5. 创建bootstrap.properties配置文件,将网关模块的配置通过交给nacos配置中心管理

    image-20230520205015056

    spring.application.name=gulimall-gateway
    #配置配置中心的地址
    spring.cloud.nacos.config.server-addr=127.0.0.1:8848
    #名称空间
    spring.cloud.nacos.config.namespace=4a8e80ba-3d3c-40ef-897f-d400797c4676
  6. 启动测试

    启动之后出现的问题及解决方案:

    a.单元测试@Test注解爆红,是我们更换了springboot的版本的原因,删除爆红的地方,重新导包即可

    b.数据库相关的报错,是因为网关中没有使用数据库,就没有配置数据库的配置,我们通过@SpringBootApplication(exclude = {DataSourceAutoConfiguration.class})解决。

  7. 转发测试

    测试案例:我们在浏览器的地址栏输入localhost:88?url=baidu,就给我们转发到百度,输入localhost:88?url=qq就给我们转发到腾讯qq

    实现:创建一个yml的配置文件,在配置文件中添加如下的配置

    spring:
    cloud:
    gateway:
    routes:
    # 路由的唯一id
    - id: baidu_route
    uri: https://www.baidu.com
    # 断言,什么时候去上面那个url
    predicates:
    # 下面的这句话的意思是如果请求中存在请求参数url=baidu,我们就去 https://www.baidu.com这个url
    - Query=url,baidu
    - id: qq_route
    uri: https://www.qq.com
    predicates:
    # 下面的这句话的意思是如果请求中存在请求参数url=qq,我们就去 https://www.qq.com这个url
    - Query=url,qq
    image-20230520212001874

2.前端开发的基础知识

前端基础知识 | The Blog (gitee.io)

前后端技术类比

image-20230520212351174

五.基础篇程序设计

提示:所有的API路径一定要和老师的一样,不然在后面会吃亏!!!

1.商品服务

1.1 三级分类

1.1.1 查询-递归树形结构数据获取

controller

/**
* 查询所有的分类以及子分类,以树形结构组装起来
*/
@RequestMapping("/list/tree")
public R list(){
List<CategoryEntity> entities = categoryService.listWithTree();
return R.ok().put("data", entities);
}

service

package com.atguigu.gulimall.product.service.impl;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;

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 {

@Autowired
private CategoryDao categoryDao;

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

/**
* 查询所有分类,以树形结构显示
*/
@Override
public List<CategoryEntity> listWithTree() {
//查出所有的分类
List<CategoryEntity> entities = categoryDao.selectList(null);
//组装成父子的树形结构
//找到所有的一级分类
List<CategoryEntity> level1List = entities.stream().filter(categoryEntity ->
//父级分类的id等于0的就是一级分类
categoryEntity.getParentCid() == 0
).map((menu) -> {
//为当前分类的子分类赋值
menu.setChildren(getChildren(menu, entities));
return menu;
}).sorted((menu1, menu2) -> {
//排序
return (menu1.getSort() == null ? 0 : menu1.getSort()) - (menu2.getSort() == null ? 0 : menu2.getSort());
}).collect(Collectors.toList());

return level1List;
}

/**
* 递归查询所有菜单的子菜单
*
* @param root 当前分类
* @param all 所有的分类数据
* @return 子菜单的集合
*/
private List<CategoryEntity> getChildren(CategoryEntity root, List<CategoryEntity> all) {
List<CategoryEntity> children = all.stream().filter(categoryEntity -> {
return categoryEntity.getParentCid().equals(root.getCatId());
}).map(categoryEntity -> {
//1.递归找子菜单
categoryEntity.setChildren(getChildren(categoryEntity, all));
return categoryEntity;
}).sorted((menu1, menu2) -> {
//2.菜单的排序
return (menu1.getSort() == null ? 0 : menu1.getSort()) - (menu2.getSort() == null ? 0 : menu2.getSort());
}).collect(Collectors.toList());
return children;
}

}

1.1.2 配置路由和网关和路径重写

前端访问的地址

image-20230529170214769

前端代码中的配置

image-20230529170149102

修改访问的基准路径,统一访问网关,再由网关做请求的转发,转发到指定的服务中去(renren-fast-vue\static\config\index.js文件)

/**
* 开发环境
*/
;(function () {
window.SITE_CONFIG = {};

// api接口请求地址
window.SITE_CONFIG['baseUrl'] = 'http://localhost:88';

// cdn地址 = 域名 + 版本号
window.SITE_CONFIG['domain'] = './'; // 域名
window.SITE_CONFIG['version'] = ''; // 版本号(年月日时分)
window.SITE_CONFIG['cdnUrl'] = window.SITE_CONFIG.domain + window.SITE_CONFIG.version;
})();

将renren-fast注册到注册中心中去

修改pom.xml中的依赖

<!-- 修改springBoot的版本 -->
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.2.6.RELEASE</version>
<relativePath/>
</parent>
<!-- 依赖common文件,获取nacos相关的注解 -->
<dependency>
<groupId>com.atguigu</groupId>
<artifactId>gulimall-common</artifactId>
<version>1.0-SNAPSHOT</version>
</dependency>

在启动类上添加@EnableDiscoveryClient注解,在配置文件中添加上nacos地址的配置

cloud:
nacos:
discovery:
server-addr: 127.0.0.1:8848

修改网关中的断言,让前台的请求转发到正确的路径上

spring:
cloud:
gateway:
routes:
# 路由的唯一id
- id: admin_route
uri: lb://renren-fast
predicates:
- Path=/api/**
# 路径重写
filters:
- RewritePath=/api/(?<segment>.*),/renren-fast/$\{segment}
# 前端项目中的基准路径 http://localhost:88/api
# http://localhost:88/api/captcha.jpg 路径重写为 http://localhost:8080/renren-fast/captcha.jpg

解决登录的时候的跨域问题

浏览器中的跨域的报错信息

image-20230530111856491

跨域问题的介绍

image-20230530112115261

跨域流程

文档介绍

image-20230530112630678

跨域的解决方案

  1. 使用nginx部署为同一域
  2. 配置当前请求允许跨域 (在网关中通过过滤器给请求添加响应头)

跨域的配置

添加跨域的配置类

package com.eatguigu.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;

/**
* @author GongChangjiang
* @version 1.0
* @Date 2023/5/30
* @Description 跨域配置类
*/
@Configuration
public class CorsConfig {

@Bean
public CorsWebFilter corsWebFilter(){
UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
CorsConfiguration corsConfiguration = new CorsConfiguration();
//1.配置跨域
//允许所有的请求头跨域
corsConfiguration.addAllowedHeader("*");
//允许所有的请求方式跨域
corsConfiguration.addAllowedMethod("*");
//任意请求来源都可以跨域
//corsConfiguration.addAllowedOrigin("*"); 老版本的springBoot使用这个,新版本的使用下面这个
corsConfiguration.addAllowedOriginPattern("*");
//允许携带cookie跨域
corsConfiguration.setAllowCredentials(true);
source.registerCorsConfiguration("/**",corsConfiguration);
return new CorsWebFilter(source);
}
}

注掉renren-fast工程中的跨域配置,避免产生两次跨域的问题

/**
* Copyright (c) 2016-2019 人人开源 All rights reserved.
*
* https://www.renren.io
*
* 版权所有,侵权必究!
*/

package io.renren.config;

import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.CorsRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;

@Configuration
public class CorsConfig implements WebMvcConfigurer {

// @Override
// public void addCorsMappings(CorsRegistry registry) {
// registry.addMapping("/**")
// .allowedOrigins("*")
// .allowCredentials(true)
// .allowedMethods("GET", "POST", "PUT", "DELETE", "OPTIONS")
// .maxAge(3600);
// }
}

再次测试,成功的进入后台管理系统

1.2.3 查询-页面中树形显示

新增一个商品系统的目录,并在该目录下创建一个分类维护的二级分类

1.添加一个一级分类

image-20230529161308671

image-20230529161317694

2.在商品系统的一级分类下添加一个分类维护的二级分类

image-20230529161648257

3.路径的问题

image-20230529162235482

4.路径与文件的对应关系

image-20230529162946584

5.发送请求的示例

<template>
<div>
<!-- 树形控件 -->
<el-tree
:data="data"
:props="defaultProps"
@node-click="handleNodeClick"
></el-tree>
</div>
</template>

<script>
export default {
//定义变量
data() {
return {
data: [],
defaultProps: {
children: "children",
label: "label",
},
};
},
//钩子函数
created() {
this.getMenus();
},

//方法
methods: {
handleNodeClick(data) {
console.log(data);
},
//获取所有的菜单
getMenus() {
//正式的发送请求
this.$http({
url: this.$http.adornUrl("/product/category/list/tree"),
method: "get",
}).then(({ data }) => {
console.log(data);
});
},
},
};
</script>
<style>
</style>

6.树形显示商品的分类信息

前端修改网关商品模块的路由(注意:匹配精确的路由放在模糊的路由的上面)

spring:
cloud:
gateway:
routes:
#这个路由精确一些,放在上面,避免被下面模糊的路由吞掉
- id: product_route
uri: lb://gulimall-product
predicates:
- Path=/api/product/**
filters:
- RewritePath=/api/(?<segment>.*),/$\{segment}

# 路由的唯一id
- id: admin_route
uri: lb://renren-fast
predicates:
- Path=/api/**
# 路径重写
filters:
- RewritePath=/api/(?<segment>.*),/renren-fast/$\{segment}


# 前端项目中的基准路径 http://localhost:88/api
# http://localhost:88/api/captcha.jpg 路径重写为 http://localhost:8080/renren-fast/captcha.jpg

前端发送请求,正确地获取数据

image-20230530172846989

在页面上显示数据

Element的配置

image-20230530173658950

前端代码

<template>
<div>
<!-- 树形控件 -->
<el-tree
:data="menus"
:props="defaultProps"
@node-click="handleNodeClick"
></el-tree>
</div>
</template>

<script>
export default {
//定义变量
data() {
return {
menus: [],
defaultProps: {//这里的配置看官方的文档
children: "children",//父节点上的子节点
label: "name",//每个对象要显示的属性值,比如我们当前要显示商品分类中的分类名的值,这里就改成name
},
};
},
//钩子函数
created() {
//调用获取商品分类数据的方法
this.getMenus();
},

//方法
methods: {
handleNodeClick(data) {
console.log(data);
},
//获取所有的菜单
getMenus() {
this.$http({
url: this.$http.adornUrl("/product/category/list/tree"),
method: "get",
}).then(({ data }) => {
this.menus = data.data;
});
},
},
};
</script>

<style>
</style>

显示的效果

image-20230530173801824

1.2.4 删除-删除商品分类

前端页面修改

<template>
<!-- 树形控件-->
<!-- :expand-on-click-node="false"设置为点击箭头的时候才会展开,避免点击删除的按钮的时候展开 -->
<!-- show-checkbox表示开启选择框,批量选择-->
<!-- node-key表示在整个树中唯一的表示 -->
<el-tree
:data="menus"
:props="defaultProps"
:expand-on-click-node="false"
show-checkbox
node-key="catId"
:default-expanded-keys="expandedKey"
>
<span class="custom-tree-node" slot-scope="{ node, data }">
<span>{{ node.label }}</span>
<span>
<el-button
v-if="node.level <= 2"
type="text"
size="mini"
@click="() => append(data)"
>
添加
</el-button>
<!-- 添加判断,没有下一级节点的时候就显示删除的按钮 -->
<el-button
v-if="data.children.length <= 0"
type="text"
size="mini"
@click="() => remove(node, data)"
>
删除
</el-button>
</span>
</span></el-tree
>
</template>

<script>
export default {
//定义变量
data() {
return {
menus: [],
defaultProps: {
//这里的配置看官方的文档
children: "children", //父节点上的子节点
label: "name", //每个对象要显示的属性值,比如我们当前要显示商品分类中的分类名的值,这里就改成name
},
expandedKey:[] //需要展开的数组,用于删除之后树形控件仍然展开
};
},
//钩子函数
created() {
//调用获取商品分类数据的方法
this.getMenus();
},

//方法
methods: {
append(data) {
console.log("append: ", data);
},
//删除节点的方法
remove(node, data) {
var ids = [data.catId];
this.$confirm(`是否要删除[${data.name}]菜单?`, "提示", {
confirmButtonText: "确定",
cancelButtonText: "取消",
type: "warning",
})
.then(() => {
this.$http({
url: this.$http.adornUrl("/product/category/delete"),
method: "post",
data: this.$http.adornData(ids, false),
}).then(({ data }) => {
this.$message({
message: "菜单删除成功",
type: "success",
});
//刷新一下页面
this.getMenus();
//设置需要默认展开的菜单(依然展开刚才删除的节点)
this.expandedKey = [node.parent.data.catId]
});
})
.catch(() => {
this.$message({
type: "info",
message: "已取消删除",
});
});
},
//获取所有的菜单
getMenus() {
this.$http({
url: this.$http.adornUrl("/product/category/list/tree"),
method: "get",
}).then(({ data }) => {
this.menus = data.data;
});
},
},
};
</script>

<style>
</style>

配置逻辑删除

配置全局的逻辑删除规则

mybatis-plus:
global-config:
db-config:
logic-delete-value: 1 # 逻辑已删除值(默认为 1)
logic-not-delete-value: 0 # 逻辑未删除值(默认为 0)

在showStatus上添加上逻辑删除的注解

@TableLogic(value = "1",delval = "0")

后端代码的实现

controlller

/**
* 删除
*/
@RequestMapping("/delete")
//@RequiresPermissions("product:category:delete")
public R delete(@RequestBody Long[] catIds) {
//检查当前删除的菜单,是否被别的地方引用
//categoryService.removeByIds(Arrays.asList(catIds));
categoryService.removeMenuByIds(Arrays.asList(catIds));
return R.ok();
}

service 这里后面需要优化

/**
* 删除分类的方法
* @param list 分类id的集合
*/
@Override
public void removeMenuByIds(List<Long> list) {
//TODO 检查当前删除的菜单是否被其他的地方引用 后面优化
//逻辑删除
categoryDao.deleteBatchIds(list);
}

1.2.5 添加-添加商品分类

前端代码

<template>
<div>
<!-- 树形控件-->
<!-- :expand-on-click-node="false"设置为点击箭头的时候才会展开,避免点击删除的按钮的时候展开 -->
<!-- show-checkbox表示开启选择框,批量选择-->
<!-- node-key表示在整个树中唯一的表示 -->
<el-tree
:data="menus"
:props="defaultProps"
:expand-on-click-node="false"
show-checkbox
node-key="catId"
:default-expanded-keys="expandedKey"
>
<span class="custom-tree-node" slot-scope="{ node, data }">
<span>{{ node.label }}</span>
<span>
<el-button
v-if="node.level <= 2"
type="text"
size="mini"
@click="() => append(data)"
>
添加
</el-button>
<!-- 添加判断,没有下一级节点的时候就显示删除的按钮 -->
<el-button
v-if="data.children.length <= 0"
type="text"
size="mini"
@click="() => remove(node, data)"
>
删除
</el-button>
</span>
</span></el-tree
>
<!-- 添加分类的时候弹出的对话框 -->
<el-dialog title="提示" :visible.sync="dialogVisible" width="30%">
<el-form :model="category">
<el-form-item label="分类名称">
<el-input v-model="category.name" autocomplete="off"></el-input>
</el-form-item>
</el-form>
<span slot="footer" class="dialog-footer">
<el-button @click="dialogVisible = false">取 消</el-button>
<el-button type="primary" @click="addCategory">确 定</el-button>
</span>
</el-dialog>
</div>
</template>

<script>
export default {
//定义变量
data() {
return {
menus: [],
defaultProps: {
//这里的配置看官方的文档
children: "children", //父节点上的子节点
label: "name", //每个对象要显示的属性值,比如我们当前要显示商品分类中的分类名的值,这里就改成name
},
expandedKey: [], //需要展开的数组,用于删除之后树形控件仍然展开
dialogVisible: false, //添加菜单的对话框的开启或者关闭
//表单绑定的对象
category: {
name: "", //分类的名称
parentCid: 0, //父类的id
catLevel: 0, //分类的级别
showStatus: 1, //是否显示该分类
sort: 0, //排序
},
};
},
//钩子函数
created() {
//调用获取商品分类数据的方法
this.getMenus();
},

//方法
methods: {
//添加分类(弹出的对话框确定按钮执行的方法)
addCategory() {
console.log("提交的数据:", this.category);
//将添加的数据返回给后端
this.$http({
url: this.$http.adornUrl("/product/category/save"),
method: "post",
data: this.$http.adornData(this.category, false),
}).then(({ data }) => {
this.$message({
message: "菜单添加成功",
type: "success",
});
//刷新一下页面
this.getMenus()
//设置需要默认展开的菜单(依然展开刚才添加的节点)
this.expandedKey = [this.category.parentCid];
});
//关闭对话框
this.dialogVisible = false;
},
//添加分类的方法
append(data) {
//清空一下表单的数据
this.category.name = "";
//打开添加的对话框
this.dialogVisible = true;
//为分类的对象赋值
this.category.parentCid = data.catId;
this.category.catLevel = data.catLevel * 1 + 1; //防止catLevel是一个字符串
},
//删除节点的方法
remove(node, data) {
var ids = [data.catId];
this.$confirm(`是否要删除[${data.name}]菜单?`, "提示", {
confirmButtonText: "确定",
cancelButtonText: "取消",
type: "warning",
})
.then(() => {
this.$http({
url: this.$http.adornUrl("/product/category/delete"),
method: "post",
data: this.$http.adornData(ids, false),
}).then(({ data }) => {
this.$message({
message: "菜单删除成功",
type: "success",
});
//刷新一下页面
this.getMenus();
//设置需要默认展开的菜单(依然展开刚才删除的节点)
this.expandedKey = [node.parent.data.catId];
});
})
.catch(() => {
this.$message({
type: "info",
message: "已取消删除",
});
});
},
//获取所有的菜单
getMenus() {
this.$http({
url: this.$http.adornUrl("/product/category/list/tree"),
method: "get",
}).then(({ data }) => {
this.menus = data.data;
});
},
},
};
</script>

<style>
</style>

后端代码

 /**
* 保存
*/
@RequestMapping("/save")
//@RequiresPermissions("product:category:save")
public R save(@RequestBody CategoryEntity category) {
categoryService.save(category);

return R.ok();
}

实现的效果

image-20230605163004850

1.2.6 修改-修改商品的分类

前端的代码(太复杂了,没有写完)

<template>
<div>
<!-- 树形控件-->
<!-- :expand-on-click-node="false"设置为点击箭头的时候才会展开,避免点击删除的按钮的时候展开 -->
<!-- show-checkbox表示开启选择框,批量选择-->
<!-- node-key表示在整个树中唯一的表示 -->
<!-- draggable 实现节点自由拖拽的效果 -->
<el-tree
:data="menus"
:props="defaultProps"
:expand-on-click-node="false"
show-checkbox
node-key="catId"
:default-expanded-keys="expandedKey"
draggable
:allow-drop="allowDrop"
@node-drop="handleDrop"
>
<span class="custom-tree-node" slot-scope="{ node, data }">
<span>{{ node.label }}</span>
<span>
<el-button
v-if="node.level <= 2"
type="text"
size="mini"
@click="() => append(data)"
>
添加
</el-button>
<el-button type="text" size="mini" @click="() => edit(data)">
修改
</el-button>
<!-- 添加判断,没有下一级节点的时候就显示删除的按钮 -->
<el-button
v-if="data.children.length <= 0"
type="text"
size="mini"
@click="() => remove(node, data)"
>
删除
</el-button>
</span>
</span></el-tree
>
<!-- 添加分类的时候弹出的对话框 -->
<el-dialog
:title="title"
:visible.sync="dialogVisible"
width="30%"
:close-on-click-modal="false"
>
<el-form :model="category">
<el-form-item label="分类名称">
<el-input v-model="category.name" autocomplete="off"></el-input>
</el-form-item>
<el-form-item label="图标">
<el-input v-model="category.icon" autocomplete="off"></el-input>
</el-form-item>
<el-form-item label="计量单位">
<el-input
v-model="category.productUnit"
autocomplete="off"
></el-input>
</el-form-item>
</el-form>
<span slot="footer" class="dialog-footer">
<el-button @click="dialogVisible = false">取 消</el-button>
<el-button type="primary" @click="submitDate">确 定</el-button>
</span>
</el-dialog>
</div>
</template>

<script>
export default {
//定义变量
data() {
return {
updateNodes: [], //经过修改之后的节点的信息
maxLevel: 0, //最大的层级
title: "", //弹出框的提示信息
dialogType: "", //弹出的对话框的类型 edit/add
menus: [],
defaultProps: {
//这里的配置看官方的文档
children: "children", //父节点上的子节点
label: "name", //每个对象要显示的属性值,比如我们当前要显示商品分类中的分类名的值,这里就改成name
},
expandedKey: [], //需要展开的数组,用于删除之后树形控件仍然展开
dialogVisible: false, //添加菜单的对话框的开启或者关闭
//表单绑定的对象
category: {
name: "", //分类的名称
parentCid: 0, //父类的id
catLevel: 0, //分类的级别
showStatus: 1, //是否显示该分类
sort: 0, //排序
catId: null, //分类的id信息
icon: "", //商品分类的图标
productUnit: "", //计量单位
},
};
},
//钩子函数
created() {
//调用获取商品分类数据的方法
this.getMenus();
},

//方法
methods: {
//拖拽成功之后调用这个函数进行回调的处理
handleDrop(draggingNode, dropNode, dropType, ev) {
console.log("handleDrop: ", draggingNode, dropNode, dropType);
//当前的节点的最新父节点id
let pCid = 0; //当前节点的组最新父节点id
let siblings = null; //当前的节点的兄弟节点
//判断的节点的进入的方式
if (dropType == "before" || dropType == "after") {
pCid =
dropNode.parent.data.catId == undefined
? 0
: dropNode.parent.data.catId;
siblings = dropNode.parent.childNodes;
} else {
pCid = dropNode.data.catId;
siblings = dropNode.childNodes;
}
//当前的节点的最新的顺序
for (let i = 0; i < siblings.length; i++) {
if (siblings[i].data.catId == draggingNode.data.catId) {
//如果当前遍历的是正在拖拽的节点
let catLevel = draggingNode.catLevel;
if (siblings[i].level != draggingNode.level) {
//当前的节点的层级发生的变化
catLevel = siblings[i].level;
//修改子节点的层级
this.updateChildNodeLevel(siblings[i]);
}
this.updateNodes.push({
catId: siblings[i].data.catId,
sort: i,
parentCid: pCid,
catLevel: catLevel,
});
} else {
this.updateNodes.push({ catId: siblings[i].data.catId, sort: i });
}
}
//当前的拖拽节点的最新的层级
console.log("updateNodes:", this.updateNodes);
},
//递归修改子节点的层级
updateChildNodeLevel(node) {
if ((node.childNodes, length > 0)) {
for (let i = 0; i < node.childNodes.length; i++) {
var cNode = node.childNodes[i].data;
this.updateNodes.push({
catId: cNode.catId,
catLevel: node.childNodes[i].level,
});
//递归调用
this.updateChildNodeLevel(node.childNodes[i]);
}
}
},
//拖拽之后节点的位置的判断 是否的可以放在该位置上面
allowDrop(draggingNode, dropNode, type) {
//判断被拖动的当前节点以及所在的父节点的总层数不能大于三
//被拖动的当前节点的总层数
console.log("allowDrop:", draggingNode, dropNode, type);
//获取当前节点的总层数
this.countNodeLevel(draggingNode.data);
let deep = this.maxLevel - draggingNode.data.catLevel + 1;
console.log("深度:", deep);
if ((type = "inner")) {
return deep + dropNode.level <= 3;
} else {
return deep + dropNode.parent.level <= 3;
}
},
//统计当前的节点的总层数
countNodeLevel(node) {
//找到所有子节点 求出最大深度
if (node.children != null && node.children.length > 0) {
for (let i = 0; i < node.children.length; i++) {
if (node.children[i].catLevel > this.maxLevel) {
this.maxLevel = node.children[i].catLevel;
}
//递归一下
this.countNodeLevel(node.children[i]);
}
}
},
//弹出修改的对话框,并做数据的回显
edit(data) {
//修改的title
this.title = "修改分类";
//设置弹出的对话框为修改的类型
this.dialogType = "edit";
//发送请求回显数据
this.$http({
url: this.$http.adornUrl(`/product/category/info/${data.catId}`),
method: "get",
}).then(({ data }) => {
//请求成功 回显数据
console.log("回显的数据:", data);
this.category.name = data.data.name;
this.category.catId = data.data.catId;
this.category.icon = data.data.icon;
this.category.productUnit = data.data.productUnit;
this.category.parentCid = data.data.parentCid;
});
//弹出对话框
this.dialogVisible = true;
},
//保存或者添加数据
submitDate() {
if (this.dialogType == "add") {
//调用添加的方法
this.addCategory();
}
if (this.dialogType == "edit") {
//修改的方法
this.editCategory();
}
},
//修改商品的分类信息
editCategory() {
//只发送我们要修改的数据
var { catId, name, icon, productUnit } = this.category;
this.$http({
url: this.$http.adornUrl("/product/category/update"),
method: "post",
data: this.$http.adornData({ catId, name, icon, productUnit }, false),
}).then(({ data }) => {
this.$message({
message: "分类修改成功",
type: "success",
});
//关闭弹出框
this.dialogVisible = false;
//刷新一下表单
this.getMenus();
//设置默认展开的菜单
this.expandedKey = [this.category.parentCid];
});
},
//添加分类(弹出的对话框确定按钮执行的方法)
addCategory() {
console.log("提交的数据:", this.category);
//将添加的数据返回给后端
this.$http({
url: this.$http.adornUrl("/product/category/save"),
method: "post",
data: this.$http.adornData(this.category, false),
}).then(({ data }) => {
this.$message({
message: "分类添加成功",
type: "success",
});
//刷新一下页面
this.getMenus();
//设置需要默认展开的菜单(依然展开刚才添加的节点)
this.expandedKey = [this.category.parentCid];
});
//关闭对话框
this.dialogVisible = false;
},
//添加分类的方法
append(data) {
//添加的title
this.title = "添加分类";
//设置当前提交的对话框为添加的对话框
this.dialogType = "add";
//清空一下表单的数据
this.category.name = "";
//打开添加的对话框
this.dialogVisible = true;
//为分类的对象赋值
this.category.parentCid = data.catId;
this.category.catLevel = data.catLevel * 1 + 1; //防止catLevel是一个字符串
this.catId = null;
this.category.icon = "";
this.category.productUnit = "";
this.category.sort = 0;
this.category.showStatus = 1;
},
//删除节点的方法
remove(node, data) {
var ids = [data.catId];
this.$confirm(`是否要删除[${data.name}]菜单?`, "提示", {
confirmButtonText: "确定",
cancelButtonText: "取消",
type: "warning",
})
.then(() => {
this.$http({
url: this.$http.adornUrl("/product/category/delete"),
method: "post",
data: this.$http.adornData(ids, false),
}).then(({ data }) => {
this.$message({
message: "菜单删除成功",
type: "success",
});
//刷新一下页面
this.getMenus();
//设置需要默认展开的菜单(依然展开刚才删除的节点)
this.expandedKey = [node.parent.data.catId];
});
})
.catch(() => {
this.$message({
type: "info",
message: "已取消删除",
});
});
},
//获取所有的菜单
getMenus() {
this.$http({
url: this.$http.adornUrl("/product/category/list/tree"),
method: "get",
}).then(({ data }) => {
this.menus = data.data;
});
},
},
};
</script>

<style>
</style>

老师的前端的代码

<template>
<div>
<el-switch v-model="draggable" active-text="开启拖拽" inactive-text="关闭拖拽"></el-switch>
<el-button v-if="draggable" @click="batchSave">批量保存</el-button>
<el-button type="danger" @click="batchDelete">批量删除</el-button>
<el-tree
:data="menus"
:props="defaultProps"
:expand-on-click-node="false"
show-checkbox
node-key="catId"
:default-expanded-keys="expandedKey"
:draggable="draggable"
:allow-drop="allowDrop"
@node-drop="handleDrop"
ref="menuTree"
>
<span class="custom-tree-node" slot-scope="{ node, data }">
<span>{{ node.label }}</span>
<span>
<el-button
v-if="node.level <=2"
type="text"
size="mini"
@click="() => append(data)"
>Append</el-button>
<el-button type="text" size="mini" @click="edit(data)">edit</el-button>
<el-button
v-if="node.childNodes.length==0"
type="text"
size="mini"
@click="() => remove(node, data)"
>Delete</el-button>
</span>
</span>
</el-tree>

<el-dialog
:title="title"
:visible.sync="dialogVisible"
width="30%"
:close-on-click-modal="false"
>
<el-form :model="category">
<el-form-item label="分类名称">
<el-input v-model="category.name" autocomplete="off"></el-input>
</el-form-item>
<el-form-item label="图标">
<el-input v-model="category.icon" autocomplete="off"></el-input>
</el-form-item>
<el-form-item label="计量单位">
<el-input v-model="category.productUnit" autocomplete="off"></el-input>
</el-form-item>
</el-form>
<span slot="footer" class="dialog-footer">
<el-button @click="dialogVisible = false">取 消</el-button>
<el-button type="primary" @click="submitData">确 定</el-button>
</span>
</el-dialog>
</div>
</template>

<script>
//这里可以导入其他文件(比如:组件,工具js,第三方插件js,json文件,图片文件等等)
//例如:import 《组件名称》 from '《组件路径》';

export default {
//import引入的组件需要注入到对象中才能使用
components: {},
props: {},
data() {
return {
pCid: [],
draggable: false,
updateNodes: [],
maxLevel: 0,
title: "",
dialogType: "", //edit,add
category: {
name: "",
parentCid: 0,
catLevel: 0,
showStatus: 1,
sort: 0,
productUnit: "",
icon: "",
catId: null
},
dialogVisible: false,
menus: [],
expandedKey: [],
defaultProps: {
children: "children",
label: "name"
}
};
},

//计算属性 类似于data概念
computed: {},
//监控data中的数据变化
watch: {},
//方法集合
methods: {
getMenus() {
this.$http({
url: this.$http.adornUrl("/product/category/list/tree"),
method: "get"
}).then(({ data }) => {
console.log("成功获取到菜单数据...", data.data);
this.menus = data.data;
});
},
batchDelete() {
let catIds = [];
let checkedNodes = this.$refs.menuTree.getCheckedNodes();
console.log("被选中的元素", checkedNodes);
for (let i = 0; i < checkedNodes.length; i++) {
catIds.push(checkedNodes[i].catId);
}
this.$confirm(`是否批量删除【${catIds}】菜单?`, "提示", {
confirmButtonText: "确定",
cancelButtonText: "取消",
type: "warning"
})
.then(() => {
this.$http({
url: this.$http.adornUrl("/product/category/delete"),
method: "post",
data: this.$http.adornData(catIds, false)
}).then(({ data }) => {
this.$message({
message: "菜单批量删除成功",
type: "success"
});
this.getMenus();
});
})
.catch(() => {});
},
batchSave() {
this.$http({
url: this.$http.adornUrl("/product/category/update/sort"),
method: "post",
data: this.$http.adornData(this.updateNodes, false)
}).then(({ data }) => {
this.$message({
message: "菜单顺序等修改成功",
type: "success"
});
//刷新出新的菜单
this.getMenus();
//设置需要默认展开的菜单
this.expandedKey = this.pCid;
this.updateNodes = [];
this.maxLevel = 0;
// this.pCid = 0;
});
},
handleDrop(draggingNode, dropNode, dropType, ev) {
console.log("handleDrop: ", draggingNode, dropNode, dropType);
//1、当前节点最新的父节点id
let pCid = 0;
let siblings = null;
if (dropType == "before" || dropType == "after") {
pCid =
dropNode.parent.data.catId == undefined
? 0
: dropNode.parent.data.catId;
siblings = dropNode.parent.childNodes;
} else {
pCid = dropNode.data.catId;
siblings = dropNode.childNodes;
}
this.pCid.push(pCid);

//2、当前拖拽节点的最新顺序,
for (let i = 0; i < siblings.length; i++) {
if (siblings[i].data.catId == draggingNode.data.catId) {
//如果遍历的是当前正在拖拽的节点
let catLevel = draggingNode.level;
if (siblings[i].level != draggingNode.level) {
//当前节点的层级发生变化
catLevel = siblings[i].level;
//修改他子节点的层级
this.updateChildNodeLevel(siblings[i]);
}
this.updateNodes.push({
catId: siblings[i].data.catId,
sort: i,
parentCid: pCid,
catLevel: catLevel
});
} else {
this.updateNodes.push({ catId: siblings[i].data.catId, sort: i });
}
}

//3、当前拖拽节点的最新层级
console.log("updateNodes", this.updateNodes);
},
updateChildNodeLevel(node) {
if (node.childNodes.length > 0) {
for (let i = 0; i < node.childNodes.length; i++) {
var cNode = node.childNodes[i].data;
this.updateNodes.push({
catId: cNode.catId,
catLevel: node.childNodes[i].level
});
this.updateChildNodeLevel(node.childNodes[i]);
}
}
},
allowDrop(draggingNode, dropNode, type) {
//1、被拖动的当前节点以及所在的父节点总层数不能大于3

//1)、被拖动的当前节点总层数
console.log("allowDrop:", draggingNode, dropNode, type);
//
this.countNodeLevel(draggingNode);
//当前正在拖动的节点+父节点所在的深度不大于3即可
let deep = Math.abs(this.maxLevel - draggingNode.level) + 1;
console.log("深度:", deep);

// this.maxLevel
if (type == "inner") {
// console.log(
// `this.maxLevel:${this.maxLevel};draggingNode.data.catLevel:${draggingNode.data.catLevel};dropNode.level:${dropNode.level}`
// );
return deep + dropNode.level <= 3;
} else {
return deep + dropNode.parent.level <= 3;
}
},
countNodeLevel(node) {
//找到所有子节点,求出最大深度
if (node.childNodes != null && node.childNodes.length > 0) {
for (let i = 0; i < node.childNodes.length; i++) {
if (node.childNodes[i].level > this.maxLevel) {
this.maxLevel = node.childNodes[i].level;
}
this.countNodeLevel(node.childNodes[i]);
}
}
},
edit(data) {
console.log("要修改的数据", data);
this.dialogType = "edit";
this.title = "修改分类";
this.dialogVisible = true;

//发送请求获取当前节点最新的数据
this.$http({
url: this.$http.adornUrl(`/product/category/info/${data.catId}`),
method: "get"
}).then(({ data }) => {
//请求成功
console.log("要回显的数据", data);
this.category.name = data.data.name;
this.category.catId = data.data.catId;
this.category.icon = data.data.icon;
this.category.productUnit = data.data.productUnit;
this.category.parentCid = data.data.parentCid;
this.category.catLevel = data.data.catLevel;
this.category.sort = data.data.sort;
this.category.showStatus = data.data.showStatus;
/**
* parentCid: 0,
catLevel: 0,
showStatus: 1,
sort: 0,
*/
});
},
append(data) {
console.log("append", data);
this.dialogType = "add";
this.title = "添加分类";
this.dialogVisible = true;
this.category.parentCid = data.catId;
this.category.catLevel = data.catLevel * 1 + 1;
this.category.catId = null;
this.category.name = "";
this.category.icon = "";
this.category.productUnit = "";
this.category.sort = 0;
this.category.showStatus = 1;
},

submitData() {
if (this.dialogType == "add") {
this.addCategory();
}
if (this.dialogType == "edit") {
this.editCategory();
}
},
//修改三级分类数据
editCategory() {
var { catId, name, icon, productUnit } = this.category;
this.$http({
url: this.$http.adornUrl("/product/category/update"),
method: "post",
data: this.$http.adornData({ catId, name, icon, productUnit }, false)
}).then(({ data }) => {
this.$message({
message: "菜单修改成功",
type: "success"
});
//关闭对话框
this.dialogVisible = false;
//刷新出新的菜单
this.getMenus();
//设置需要默认展开的菜单
this.expandedKey = [this.category.parentCid];
});
},
//添加三级分类
addCategory() {
console.log("提交的三级分类数据", this.category);
this.$http({
url: this.$http.adornUrl("/product/category/save"),
method: "post",
data: this.$http.adornData(this.category, false)
}).then(({ data }) => {
this.$message({
message: "菜单保存成功",
type: "success"
});
//关闭对话框
this.dialogVisible = false;
//刷新出新的菜单
this.getMenus();
//设置需要默认展开的菜单
this.expandedKey = [this.category.parentCid];
});
},

remove(node, data) {
var ids = [data.catId];
this.$confirm(`是否删除【${data.name}】菜单?`, "提示", {
confirmButtonText: "确定",
cancelButtonText: "取消",
type: "warning"
})
.then(() => {
this.$http({
url: this.$http.adornUrl("/product/category/delete"),
method: "post",
data: this.$http.adornData(ids, false)
}).then(({ data }) => {
this.$message({
message: "菜单删除成功",
type: "success"
});
//刷新出新的菜单
this.getMenus();
//设置需要默认展开的菜单
this.expandedKey = [node.parent.data.catId];
});
})
.catch(() => {});

console.log("remove", node, data);
}
},
//生命周期 - 创建完成(可以访问当前this实例)
created() {
this.getMenus();
},
//生命周期 - 挂载完成(可以访问DOM元素)
mounted() {},
beforeCreate() {}, //生命周期 - 创建之前
beforeMount() {}, //生命周期 - 挂载之前
beforeUpdate() {}, //生命周期 - 更新之前
updated() {}, //生命周期 - 更新之后
beforeDestroy() {}, //生命周期 - 销毁之前
destroyed() {}, //生命周期 - 销毁完成
activated() {} //如果页面有keep-alive缓存功能,这个函数会触发
};
</script>
<style scoped>
</style>

后端的代码

/**
* 批量修改商品的分类信息
*/
@RequestMapping("/update/sort")
public R updateSort(@RequestBody CategoryEntity[] categoryEntities) {
categoryService.updateBatchById(Arrays.asList(categoryEntities));
return R.ok();
}

1.2 品牌管理

1.2.1 品牌的增删改查功能

这里前端的代码是和先前生成的后端的代码一起生成的

image-20230611111440999

商品品牌的列表显示

优化之后的前端的代码 (表头的优化 需要按钮显示的使用按钮进行显示)

<template>
<div class="mod-config">
<el-form
:inline="true"
:model="dataForm"
@keyup.enter.native="getDataList()"
>
<el-form-item>
<el-input
v-model="dataForm.key"
placeholder="参数名"
clearable
></el-input>
</el-form-item>
<el-form-item>
<el-button @click="getDataList()">查询</el-button>
<el-button
v-if="isAuth('product:brand:save')"
type="primary"
@click="addOrUpdateHandle()"
>新增</el-button
>
<el-button
v-if="isAuth('product:brand:delete')"
type="danger"
@click="deleteHandle()"
:disabled="dataListSelections.length <= 0"
>批量删除</el-button
>
</el-form-item>
</el-form>
<el-table
:data="dataList"
border
v-loading="dataListLoading"
@selection-change="selectionChangeHandle"
style="width: 100%"
>
<el-table-column
type="selection"
header-align="center"
align="center"
width="50"
>
</el-table-column>
<el-table-column
prop="brandId"
header-align="center"
align="center"
label="品牌id"
>
</el-table-column>
<el-table-column
prop="name"
header-align="center"
align="center"
label="品牌名"
>
</el-table-column>
<el-table-column
prop="logo"
header-align="center"
align="center"
label="品牌logo地址"
>
</el-table-column>
<el-table-column
prop="descript"
header-align="center"
align="center"
label="介绍"
>
</el-table-column>
<el-table-column
prop="showStatus"
header-align="center"
align="center"
label="显示状态"
>
<template slot-scope="scope">
<el-switch
v-model="scope.row.showStatus"
active-color="#13ce66"
inactive-color="#ff4949"
:active-value="1"
:inactive-value="0"
@change="updateBrandStatus(scope.row)"
>
</el-switch>
</template>
</el-table-column>
<el-table-column
prop="firstLetter"
header-align="center"
align="center"
label="检索首字母"
>
</el-table-column>
<el-table-column
prop="sort"
header-align="center"
align="center"
label="排序"
>
</el-table-column>
<el-table-column
fixed="right"
header-align="center"
align="center"
width="150"
label="操作"
>
<template slot-scope="scope">
<el-button
type="text"
size="small"
@click="addOrUpdateHandle(scope.row.brandId)"
>修改</el-button
>
<el-button
type="text"
size="small"
@click="deleteHandle(scope.row.brandId)"
>删除</el-button
>
</template>
</el-table-column>
</el-table>
<el-pagination
@size-change="sizeChangeHandle"
@current-change="currentChangeHandle"
:current-page="pageIndex"
:page-sizes="[10, 20, 50, 100]"
:page-size="pageSize"
:total="totalPage"
layout="total, sizes, prev, pager, next, jumper"
>
</el-pagination>
<!-- 弹窗, 新增 / 修改 -->
<add-or-update
v-if="addOrUpdateVisible"
ref="addOrUpdate"
@refreshDataList="getDataList"
></add-or-update>
</div>
</template>

<script>
import AddOrUpdate from "./brand-add-or-update";
export default {
data() {
return {
dataForm: {
key: "",
},
dataList: [],
pageIndex: 1,
pageSize: 10,
totalPage: 0,
dataListLoading: false,
dataListSelections: [],
addOrUpdateVisible: false,
};
},
components: {
AddOrUpdate,
},
activated() {
this.getDataList();
},
methods: {
//修改品牌的显示的状态的
updateBrandStatus(data) {
console.log("修改的状态信息:", data);
let { brandId, showStatus } = data;
//发送修改状态的请求
this.$http({
url: this.$http.adornUrl(`/product/brand/update`),
method: "post",
data: this.$http.adornData(
{ brandId: brandId, showStatus: showStatus },
false
),
}).then(({ data }) => {
this.$message({
message: "状态更新成功",
type: "success",
});
});
},
// 获取数据列表
getDataList() {
this.dataListLoading = true;
this.$http({
url: this.$http.adornUrl("/product/brand/list"),
method: "get",
params: this.$http.adornParams({
page: this.pageIndex,
limit: this.pageSize,
key: this.dataForm.key,
}),
}).then(({ data }) => {
if (data && data.code === 0) {
this.dataList = data.page.list;
this.totalPage = data.page.totalCount;
} else {
this.dataList = [];
this.totalPage = 0;
}
this.dataListLoading = false;
});
},
// 每页数
sizeChangeHandle(val) {
this.pageSize = val;
this.pageIndex = 1;
this.getDataList();
},
// 当前页
currentChangeHandle(val) {
this.pageIndex = val;
this.getDataList();
},
// 多选
selectionChangeHandle(val) {
this.dataListSelections = val;
},
// 新增 / 修改
addOrUpdateHandle(id) {
this.addOrUpdateVisible = true;
this.$nextTick(() => {
this.$refs.addOrUpdate.init(id);
});
},
// 删除
deleteHandle(id) {
var ids = id
? [id]
: this.dataListSelections.map((item) => {
return item.brandId;
});
this.$confirm(
`确定对[id=${ids.join(",")}]进行[${id ? "删除" : "批量删除"}]操作?`,
"提示",
{
confirmButtonText: "确定",
cancelButtonText: "取消",
type: "warning",
}
).then(() => {
this.$http({
url: this.$http.adornUrl("/product/brand/delete"),
method: "post",
data: this.$http.adornData(ids, false),
}).then(({ data }) => {
if (data && data.code === 0) {
this.$message({
message: "操作成功",
type: "success",
duration: 1500,
onClose: () => {
this.getDataList();
},
});
} else {
this.$message.error(data.msg);
}
});
});
},
},
};
</script>

后端代码(修改品牌的显示状态)

 /**
* 修改
*/
@RequestMapping("/update")
//@RequiresPermissions("product:brand:update")
public R update(@RequestBody BrandEntity brand){
brandService.updateById(brand);

return R.ok();
}

品牌添加功能

阿里云对象存储OSS | The Blog (gitee.io)

image-20230611153418426

前端的代码

品牌管理的列表页面

<template>
<div class="mod-config">
<el-form
:inline="true"
:model="dataForm"
@keyup.enter.native="getDataList()"
>
<el-form-item>
<el-input
v-model="dataForm.key"
placeholder="参数名"
clearable
></el-input>
</el-form-item>
<el-form-item>
<el-button @click="getDataList()">查询</el-button>
<el-button
v-if="isAuth('product:brand:save')"
type="primary"
@click="addOrUpdateHandle()"
>新增</el-button
>
<el-button
v-if="isAuth('product:brand:delete')"
type="danger"
@click="deleteHandle()"
:disabled="dataListSelections.length <= 0"
>批量删除</el-button
>
</el-form-item>
</el-form>
<el-table
:data="dataList"
border
v-loading="dataListLoading"
@selection-change="selectionChangeHandle"
style="width: 100%"
>
<el-table-column
type="selection"
header-align="center"
align="center"
width="50"
>
</el-table-column>
<el-table-column
prop="brandId"
header-align="center"
align="center"
label="品牌id"
>
</el-table-column>
<el-table-column
prop="name"
header-align="center"
align="center"
label="品牌名"
>
</el-table-column>
<el-table-column
prop="logo"
header-align="center"
align="center"
label="品牌logo地址"
>
<template slot-scope="scope">
<img :src="scope.row.logo" style="width: 100px; height: 80px">
</template>
</el-table-column>
<el-table-column
prop="descript"
header-align="center"
align="center"
label="介绍"
>
</el-table-column>
<el-table-column
prop="showStatus"
header-align="center"
align="center"
label="显示状态"
>
<template slot-scope="scope">
<el-switch
v-model="scope.row.showStatus"
active-color="#13ce66"
inactive-color="#ff4949"
:active-value="1"
:inactive-value="0"
@change="updateBrandStatus(scope.row)"
>
</el-switch>
</template>
</el-table-column>
<el-table-column
prop="firstLetter"
header-align="center"
align="center"
label="检索首字母"
>
</el-table-column>
<el-table-column
prop="sort"
header-align="center"
align="center"
label="排序"
>
</el-table-column>
<el-table-column
fixed="right"
header-align="center"
align="center"
width="150"
label="操作"
>
<template slot-scope="scope">
<el-button
type="text"
size="small"
@click="addOrUpdateHandle(scope.row.brandId)"
>修改</el-button
>
<el-button
type="text"
size="small"
@click="deleteHandle(scope.row.brandId)"
>删除</el-button
>
</template>
</el-table-column>
</el-table>
<el-pagination
@size-change="sizeChangeHandle"
@current-change="currentChangeHandle"
:current-page="pageIndex"
:page-sizes="[10, 20, 50, 100]"
:page-size="pageSize"
:total="totalPage"
layout="total, sizes, prev, pager, next, jumper"
>
</el-pagination>
<!-- 弹窗, 新增 / 修改 -->
<add-or-update
v-if="addOrUpdateVisible"
ref="addOrUpdate"
@refreshDataList="getDataList"
></add-or-update>
</div>
</template>

<script>
import AddOrUpdate from "./brand-add-or-update";
export default {
data() {
return {
dataForm: {
key: "",
},
dataList: [],
pageIndex: 1,
pageSize: 10,
totalPage: 0,
dataListLoading: false,
dataListSelections: [],
addOrUpdateVisible: false,
};
},
components: {
AddOrUpdate,
},
activated() {
this.getDataList();
},
methods: {
//修改品牌的显示的状态的
updateBrandStatus(data) {
console.log("修改的状态信息:", data);
let { brandId, showStatus } = data;
//发送修改状态的请求
this.$http({
url: this.$http.adornUrl(`/product/brand/update`),
method: "post",
data: this.$http.adornData(
{ brandId: brandId, showStatus: showStatus },
false
),
}).then(({ data }) => {
this.$message({
message: "状态更新成功",
type: "success",
});
});
},
// 获取数据列表
getDataList() {
this.dataListLoading = true;
this.$http({
url: this.$http.adornUrl("/product/brand/list"),
method: "get",
params: this.$http.adornParams({
page: this.pageIndex,
limit: this.pageSize,
key: this.dataForm.key,
}),
}).then(({ data }) => {
if (data && data.code === 0) {
this.dataList = data.page.list;
this.totalPage = data.page.totalCount;
} else {
this.dataList = [];
this.totalPage = 0;
}
this.dataListLoading = false;
});
},
// 每页数
sizeChangeHandle(val) {
this.pageSize = val;
this.pageIndex = 1;
this.getDataList();
},
// 当前页
currentChangeHandle(val) {
this.pageIndex = val;
this.getDataList();
},
// 多选
selectionChangeHandle(val) {
this.dataListSelections = val;
},
// 新增 / 修改
addOrUpdateHandle(id) {
this.addOrUpdateVisible = true;
this.$nextTick(() => {
this.$refs.addOrUpdate.init(id);
});
},
// 删除
deleteHandle(id) {
var ids = id
? [id]
: this.dataListSelections.map((item) => {
return item.brandId;
});
this.$confirm(
`确定对[id=${ids.join(",")}]进行[${id ? "删除" : "批量删除"}]操作?`,
"提示",
{
confirmButtonText: "确定",
cancelButtonText: "取消",
type: "warning",
}
).then(() => {
this.$http({
url: this.$http.adornUrl("/product/brand/delete"),
method: "post",
data: this.$http.adornData(ids, false),
}).then(({ data }) => {
if (data && data.code === 0) {
this.$message({
message: "操作成功",
type: "success",
duration: 1500,
onClose: () => {
this.getDataList();
},
});
} else {
this.$message.error(data.msg);
}
});
});
},
},
};
</script>

品牌管理的添加和修改组件

<template>
<el-dialog
:title="!dataForm.brandId ? '新增' : '修改'"
:close-on-click-modal="false"
:visible.sync="visible"
>
<el-form
:model="dataForm"
:rules="dataRule"
ref="dataForm"
@keyup.enter.native="dataFormSubmit()"
label-width="140px"
>
<el-form-item label="品牌名" prop="name">
<el-input v-model="dataForm.name" placeholder="品牌名"></el-input>
</el-form-item>
<el-form-item label="品牌logo地址" prop="logo">
<!-- <el-input v-model="dataForm.logo" placeholder="品牌logo地址"></el-input> -->
<!-- 自定义的组件 -->
<SingleUpload v-model="dataForm.logo"></SingleUpload>
</el-form-item>
<el-form-item label="介绍" prop="descript">
<el-input v-model="dataForm.descript" placeholder="介绍"></el-input>
</el-form-item>
<el-form-item label="显示状态" prop="showStatus">
<el-switch
v-model="dataForm.showStatus"
active-color="#13ce66"
inactive-color="#ff4949"
:active-value="1"
:inactive-value="0"
>
</el-switch>
</el-form-item>
<el-form-item label="检索首字母" prop="firstLetter">
<el-input
v-model="dataForm.firstLetter"
placeholder="检索首字母"
></el-input>
</el-form-item>
<el-form-item label="排序" prop="sort">
<el-input v-model.number="dataForm.sort" placeholder="排序"></el-input>
</el-form-item>
</el-form>
<span slot="footer" class="dialog-footer">
<el-button @click="visible = false">取消</el-button>
<el-button type="primary" @click="dataFormSubmit()">确定</el-button>
</span>
</el-dialog>
</template>

<script>
import SingleUpload from "@/components/upload/singleUpload";
export default {
components: { SingleUpload }, //声明一下组件
data() {
return {
visible: false,
dataForm: {
brandId: 0,
name: "",
logo: "",
descript: "",
showStatus: 1,
firstLetter: "",
sort: 0,
},
dataRule: {
name: [{ required: true, message: "品牌名不能为空", trigger: "blur" }],
logo: [
{ required: true, message: "品牌logo地址不能为空", trigger: "blur" },
],
descript: [
{ required: true, message: "介绍不能为空", trigger: "blur" },
],
showStatus: [
{
required: true,
message: "显示状态[0-不显示;1-显示]不能为空",
trigger: "blur",
},
],
firstLetter: [
{
validator: (rule, value, callback) => {
if (value == "") {
//检测是不是空串
callback(new Error("检索首字母不能为空"));
} else if (!/^[a-zA-Z]$/.test(value)) {
//检测输入的是不是字母
callback(new Error("首字母必须是a-z或者A-Z"));
} else {
//成功
callback();
}
},
trigger: "blur",
},
],
sort: [
{
validator: (rule, value, callback) => {
if (value === "") {
//检测是不是空串
callback(new Error("排序不能为空"));
} else if (!Number.isInteger(value) || value < 0) {
//检测输入的是不是字母
callback(new Error("排序必须为整数并且大于0"));
} else {
//成功
callback();
}
},
trigger: "blur",
},
],
},
};
},
methods: {
init(id) {
this.dataForm.brandId = id || 0;
this.visible = true;
this.$nextTick(() => {
this.$refs["dataForm"].resetFields();
if (this.dataForm.brandId) {
this.$http({
url: this.$http.adornUrl(
`/product/brand/info/${this.dataForm.brandId}`
),
method: "get",
params: this.$http.adornParams(),
}).then(({ data }) => {
if (data && data.code === 0) {
this.dataForm.name = data.brand.name;
this.dataForm.logo = data.brand.logo;
this.dataForm.descript = data.brand.descript;
this.dataForm.showStatus = data.brand.showStatus;
this.dataForm.firstLetter = data.brand.firstLetter;
this.dataForm.sort = data.brand.sort;
}
});
}
});
},
// 表单提交
dataFormSubmit() {
this.$refs["dataForm"].validate((valid) => {
if (valid) {
this.$http({
url: this.$http.adornUrl(
`/product/brand/${!this.dataForm.brandId ? "save" : "update"}`
),
method: "post",
data: this.$http.adornData({
brandId: this.dataForm.brandId || undefined,
name: this.dataForm.name,
logo: this.dataForm.logo,
descript: this.dataForm.descript,
showStatus: this.dataForm.showStatus,
firstLetter: this.dataForm.firstLetter,
sort: this.dataForm.sort,
}),
}).then(({ data }) => {
if (data && data.code === 0) {
this.$message({
message: "操作成功",
type: "success",
duration: 1500,
onClose: () => {
this.visible = false;
this.$emit("refreshDataList");
},
});
} else {
this.$message.error(data.msg);
}
});
}
});
},
},
};
</script>

后端的代码

图片上传相关的后台接口

配置文件
apllication.yml

spring:
cloud:
nacos:
discovery:
server-addr: 127.0.0.1:8848
alicloud:
access-key: XXXXXXXXXXXXXXXXXXXX
secret-key: XXXXXXXXXXXXXXXXXXXX
oss:
endpoint: oss-cn-beijing.aliyuncs.com
bucket: gulimall-0611 #因为是公用的配置,这个配置是自己配置的

application:
name: gulimall-third-party

server:
port: 30000

Controller

package com.atguigu.gulimall.thirdparty.controller;

import com.aliyun.oss.OSS;
import com.aliyun.oss.OSSClientBuilder;
import com.aliyun.oss.common.utils.BinaryUtil;
import com.aliyun.oss.model.MatchMode;
import com.aliyun.oss.model.PolicyConditions;
import com.atguigu.common.utils.R;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.LinkedHashMap;
import java.util.Map;

/**
* @author Jason Gong
* @version 1.0
* @Date 2023/6/11
* @Description 阿里云对象存储的控制器方法
*/

@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 accessKey;

@RequestMapping("/oss/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+"/";

// 创建ossClient实例。
OSS ossClient = new OSSClientBuilder().build(endpoint, accessId, accessKey);
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) {
System.out.println(e.getMessage());
}
return R.ok().put("data",respMap);
}
}

配置网关,通过网关访问到该接口

- id: third_party_route
uri: lb://gulimall-third-party
predicates:
- Path=/api/thirdparty/**
filters:
- RewritePath=/api/thirdparty/(?<segment>.*),/$\{segment}

1.2.2 JSR303-数据校验

数据校验 | The Blog (gitee.io)

1.依赖文件

<dependency>
<groupId>org.hibernate</groupId>
<artifactId>hibernate-validator</artifactId>
<version>6.0.7.Final</version>
</dependency>

2.在实体类上添加上校验的规则

package com.atguigu.gulimall.product.entity;

import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;

import java.io.Serializable;

import lombok.Data;
import org.hibernate.validator.constraints.URL;

import javax.validation.constraints.*;

/**
* 品牌
*
* @author JasonGong
* @email JasonGong@gmail.com
* @date 2023-05-19 00:23:36
*/
@Data
@TableName("pms_brand")
public class BrandEntity implements Serializable {
private static final long serialVersionUID = 1L;

/**
* 品牌id
*/
@TableId
private Long brandId;
/**
* 品牌名
*/
@NotBlank(message = "品牌名必须提交") //非空的校验
private String name;
/**
* 品牌logo地址
*/
@NotEmpty
@URL(message = "logo必须是一个合法的URL地址")//URL的校验
private String logo;
/**
* 介绍
*/
private String descript;
/**
* 显示状态[0-不显示;1-显示]
*/
private Integer showStatus;
/**
* 检索首字母
*/
@NotEmpty
@Pattern(regexp = "^[a-zA-Z]$", message = "检索首字母必须是一个字母")//正则表达式的校验
private String firstLetter;
/**
* 排序
*/
@NotNull
@Min(value = 0, message = "排序必须大于等于0")//最小值是0
private Integer sort;

}

3.接口上开启检验,并自定义校验出错时的返回值

/**
* 保存
*"@Valid" 开启校验
* @param result 检验之后响应的结果信息
*/
@RequestMapping("/save")
public R save(@Valid @RequestBody BrandEntity brand, BindingResult result){
if(result.hasErrors()){
Map<String,String> map = new HashMap<>();
//获取检验的错误结果
for (FieldError fieldError : result.getFieldErrors()) {
//获取到错误的提示
String defaultMessage = fieldError.getDefaultMessage();
//获取错误的属性的名字
String field = fieldError.getField();
map.put(field,defaultMessage);
}
return R.error(400,"提交的数据不合法").put("data",map);
}else {
brandService.save(brand);
}
return R.ok();
}

返回的json格式示例

image-20230612165021780

1.2.3 统一异常处理

统一异常处理 | The Blog (gitee.io)

统一异常返回状态码

package com.atguigu.common.exception;

/**
* @author Jason Gong
* @version 1.0
* @Date 2023/6/12
* @Description 返回的状态码信息
*/
public enum BizCodeEnum {
UNKNOWN_EXCEPTION(10000,"系统未知异常"),
VALID_EXCEPTION(10001,"参数格式校验");

private Integer code;
private String msg;

BizCodeEnum(Integer code, String msg) {
this.code = code;
this.msg = msg;
}

public Integer getCode() {
return code;
}

public String getMsg() {
return msg;
}
}

创建异常处理类

package com.atguigu.gulimall.product.exception;

import com.atguigu.common.exception.BizCodeEnum;
import com.atguigu.common.utils.R;
import lombok.extern.slf4j.Slf4j;
import org.springframework.validation.BindingResult;
import org.springframework.validation.FieldError;
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;

/**
* @author Jason Gong
* @version 1.0
* @Date 2023/6/12
* @Description 异常处理类
*/
@Slf4j
@RestControllerAdvice(basePackages = "com.atguigu.gulimall.product.controller")
public class GulimallExceptionControllerAdvice {

/**
* 统一处理数据校验的异常
* @param e 数据校验的异常
*/
@ExceptionHandler(value = MethodArgumentNotValidException.class)
public R handleValidException(MethodArgumentNotValidException e){
log.error("数据校验出现问题:{},异常类型:{}",e.getMessage(),e.getClass());
Map<String,String> map = new HashMap<>();
//获取检验的错误结果
BindingResult result = e.getBindingResult();
for (FieldError fieldError : result.getFieldErrors()) {
//获取到错误的提示
String defaultMessage = fieldError.getDefaultMessage();
//获取错误的属性的名字
String field = fieldError.getField();
map.put(field,defaultMessage);
}
return R.error(BizCodeEnum.VALID_EXCEPTION.getCode(),BizCodeEnum.VALID_EXCEPTION.getMsg()).put("data",map);
}

/**
* 捕获任意的异常
*/
@ExceptionHandler(value = Throwable.class)
public R handleException(){
return R.error(BizCodeEnum.UNKNOWN_EXCEPTION.getCode(), BizCodeEnum.UNKNOWN_EXCEPTION.getMsg());
}
}

1.2.4 关联分类功能

功能截图

image-20230618232437618

后端代码的实现

查询品牌和分类的关联关系

/**
* 查询商品的关联分类数据
* @param brandId 品牌的id
* @return 关联的分类数据
*/
@GetMapping("/catelog/list")
public R catelogList(@RequestParam("brandId") Long brandId){
LambdaQueryWrapper<CategoryBrandRelationEntity> queryWrapper = new LambdaQueryWrapper<>();
queryWrapper.eq(CategoryBrandRelationEntity::getBrandId,brandId);
List<CategoryBrandRelationEntity> categoryBrandRelationEntityList = categoryBrandRelationService.list(queryWrapper);
return R.ok().put("data", categoryBrandRelationEntityList);
}

新增品牌和商品的关联关系

controller

/**
* 保存商品的关联的分类数据信息
*/
@PostMapping("/save")
public R saveCategoryBrandRelation(@RequestBody CategoryBrandRelationEntity categoryBrandRelation){
categoryBrandRelationService.saveDetail(categoryBrandRelation);
return R.ok();
}

service

/**
* 保存商品和分类的关联数据
*
* @param categoryBrandRelation 商品和分类的关联数据表
*/
@Override
public void saveDetail(CategoryBrandRelationEntity categoryBrandRelation) {
//获取品牌的id和关联分类的id
Long brandId = categoryBrandRelation.getBrandId();
Long catelogId = categoryBrandRelation.getCatelogId();
//根据id查询对应的品牌名和分类名
BrandEntity brand = brandDao.selectById(brandId);
CategoryEntity category = categoryDao.selectById(catelogId);
//设置值并保存
categoryBrandRelation.setBrandName(brand.getName());
categoryBrandRelation.setCatelogName(category.getName());
this.save(categoryBrandRelation);
}

注意:这里品牌和关联分类之间的关系表中使用了冗余的字段,所以在修改品牌的时候,关系表中的字段也要修改一下

@Override
public void updateDetail(BrandEntity brand) {
//保证冗余字段的数据的一致性
//更新品牌表中的数据
this.updateById(brand);
if (!StringUtils.isEmpty(brand.getName())) {
//同步更新关联表中的数据
categoryBrandRelationService.updateBrand(brand.getBrandId(),brand.getName());
//TODO 更新其他的关联
}
}

关联的分类也是冗余的字段 也要同步修改

/**
* 级联更新所有的关联的数据
* @param category 商品分类的实体
*/
@Override
public void updateCascade(CategoryEntity category) {
//更新分类表中的数据
this.updateById(category);
//更新分类与品牌名关联表中分类的名称
categoryBrandRelationService.updateCategory(category.getCatId(),category.getName());
}

功能实现之后的截图

image-20230619111515367

1.3 平台属性

创建属性管理的菜单(通过sql导入所有的菜单,导入所有以后需要使用的菜单)

资料中的sys_menus.sql文件保存的是所有的菜单信息

image-20230616152846339

谷粒商城的接口的在线文档:https://easydoc.net/s/78237135

image-20230616153304953

Object的划分

  1. PO 持久对象

    PO就是对应数据库中某个表的一条记录,多个记录可以用PO的集合。PO中应该不包含对数据库的操作。

  2. DO 领域对象

    就是从现实世界中抽象出来的有形或者无形的业务实体。

  3. TO 数据传输对象

    不同的应用程序之间传输的对象

  4. DTO 数据传输对象

    泛指展示层与服务层之间的数据传输对象

  5. VO 值对象

    视图对象 接收页面传入过来的数据,封装对象;将业务处理完成的对象,封装成页面需要的数据

  6. BO 业务对象

  7. POJO 简单无规则的java对象

  8. DAO 数据访问对象

1.3.1 属性分组功能

实现一个分类 联动的显示相应的分组信息

抽取商品分类的组件

<template>
<el-tree
:data="menus"
:props="defaultProps"
node-key="catId"
ref="menuTree"
></el-tree>
</template>
<script>
export default {
components: {},
data() {
return {
menus: [],
expandedKey: [],
defaultProps: {
children: "children",
label: "name",
},
};
},
created() {
this.getMenus();
},
methods: {
getMenus() {
this.$http({
url: this.$http.adornUrl("/product/category/list/tree"),
method: "get",
}).then(({ data }) => {
console.log("商品分类数据:", data.data);
this.menus = data.data;
});
},
},
};
</script>

<style>
</style>

属性分组页面

<template>
<el-row :gutter="20">
<el-col :span="6"> <category></category></el-col>
<el-col :span="18">
<div class="mod-config">
<el-form :inline="true" :model="dataForm" @keyup.enter.native="getDataList()">
<el-form-item>
<el-input v-model="dataForm.key" placeholder="参数名" clearable></el-input>
</el-form-item>
<el-form-item>
<el-button @click="getDataList()">查询</el-button>
<el-button v-if="isAuth('product:attrgroup:save')" type="primary" @click="addOrUpdateHandle()">新增</el-button>
<el-button v-if="isAuth('product:attrgroup:delete')" type="danger" @click="deleteHandle()" :disabled="dataListSelections.length <= 0">批量删除</el-button>
</el-form-item>
</el-form>
<el-table
:data="dataList"
border
v-loading="dataListLoading"
@selection-change="selectionChangeHandle"
style="width: 100%;">
<el-table-column
type="selection"
header-align="center"
align="center"
width="50">
</el-table-column>
<el-table-column
prop="attrGroupId"
header-align="center"
align="center"
label="分组id">
</el-table-column>
<el-table-column
prop="attrGroupName"
header-align="center"
align="center"
label="组名">
</el-table-column>
<el-table-column
prop="sort"
header-align="center"
align="center"
label="排序">
</el-table-column>
<el-table-column
prop="descript"
header-align="center"
align="center"
label="描述">
</el-table-column>
<el-table-column
prop="icon"
header-align="center"
align="center"
label="组图标">
</el-table-column>
<el-table-column
prop="catelogId"
header-align="center"
align="center"
label="所属分类id">
</el-table-column>
<el-table-column
fixed="right"
header-align="center"
align="center"
width="150"
label="操作">
<template slot-scope="scope">
<el-button type="text" size="small" @click="addOrUpdateHandle(scope.row.attrGroupId)">修改</el-button>
<el-button type="text" size="small" @click="deleteHandle(scope.row.attrGroupId)">删除</el-button>
</template>
</el-table-column>
</el-table>
<el-pagination
@size-change="sizeChangeHandle"
@current-change="currentChangeHandle"
:current-page="pageIndex"
:page-sizes="[10, 20, 50, 100]"
:page-size="pageSize"
:total="totalPage"
layout="total, sizes, prev, pager, next, jumper">
</el-pagination>
<!-- 弹窗, 新增 / 修改 -->
<add-or-update v-if="addOrUpdateVisible" ref="addOrUpdate" @refreshDataList="getDataList"></add-or-update>
</div></el-col>
</el-row>
</template>
<script>
import Category from "../common/category.vue";
import AddOrUpdate from './attrgroup-add-or-update'
export default {
data () {
return {
dataForm: {
key: ''
},
dataList: [],
pageIndex: 1,
pageSize: 10,
totalPage: 0,
dataListLoading: false,
dataListSelections: [],
addOrUpdateVisible: false
}
},
components: {
AddOrUpdate,Category
},
activated () {
this.getDataList()
},
methods: {
// 获取数据列表
getDataList () {
this.dataListLoading = true
this.$http({
url: this.$http.adornUrl('/product/attrgroup/list'),
method: 'get',
params: this.$http.adornParams({
'page': this.pageIndex,
'limit': this.pageSize,
'key': this.dataForm.key
})
}).then(({data}) => {
if (data && data.code === 0) {
this.dataList = data.page.list
this.totalPage = data.page.totalCount
} else {
this.dataList = []
this.totalPage = 0
}
this.dataListLoading = false
})
},
// 每页数
sizeChangeHandle (val) {
this.pageSize = val
this.pageIndex = 1
this.getDataList()
},
// 当前页
currentChangeHandle (val) {
this.pageIndex = val
this.getDataList()
},
// 多选
selectionChangeHandle (val) {
this.dataListSelections = val
},
// 新增 / 修改
addOrUpdateHandle (id) {
this.addOrUpdateVisible = true
this.$nextTick(() => {
this.$refs.addOrUpdate.init(id)
})
},
// 删除
deleteHandle (id) {
var ids = id ? [id] : this.dataListSelections.map(item => {
return item.attrGroupId
})
this.$confirm(`确定对[id=${ids.join(',')}]进行[${id ? '删除' : '批量删除'}]操作?`, '提示', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning'
}).then(() => {
this.$http({
url: this.$http.adornUrl('/product/attrgroup/delete'),
method: 'post',
data: this.$http.adornData(ids, false)
}).then(({data}) => {
if (data && data.code === 0) {
this.$message({
message: '操作成功',
type: 'success',
duration: 1500,
onClose: () => {
this.getDataList()
}
})
} else {
this.$message.error(data.msg)
}
})
})
}
}
}
</script>
<style>
</style>

页面效果

image-20230617173759895

父子组件中数据的传递

子组件给父组件传递的数据,事件传递,子组件给父组件发送一个事件,事件传递

子组件调整

<template>
<el-tree
:data="menus"
:props="defaultProps"
node-key="catId"
ref="menuTree"
@node-click="nodeclick"
></el-tree>
</template>
<script>
export default {
components: {},
data() {
return {
menus: [],
expandedKey: [],
defaultProps: {
children: "children",
label: "name",
},
};
},
created() {
this.getMenus();
},
methods: {
getMenus() {
this.$http({
url: this.$http.adornUrl("/product/category/list/tree"),
method: "get",
}).then(({ data }) => {
console.log("商品分类数据:", data.data);
this.menus = data.data;
});
},
nodeclick(data,node,component) {
console.log("子组件被点击,传递的数据有:data:",data,"node:",node,"component:",component);
//子组件向父组件传递数据
this.$emit("tree-node-click",data,node,component);
},
},
};
</script>

<style>
</style>

父组件接受相应的数据

<template>
<el-row :gutter="20">
<el-col :span="6">
<category @tree-node-click="treeNodeClick"></category
></el-col>
<el-col :span="18">
<div class="mod-config">
<el-form
:inline="true"
:model="dataForm"
@keyup.enter.native="getDataList()"
>
<el-form-item>
<el-input
v-model="dataForm.key"
placeholder="参数名"
clearable
></el-input>
</el-form-item>
<el-form-item>
<el-button @click="getDataList()">查询</el-button>
<el-button
v-if="isAuth('product:attrgroup:save')"
type="primary"
@click="addOrUpdateHandle()"
>新增</el-button
>
<el-button
v-if="isAuth('product:attrgroup:delete')"
type="danger"
@click="deleteHandle()"
:disabled="dataListSelections.length <= 0"
>批量删除</el-button
>
</el-form-item>
</el-form>
<el-table
:data="dataList"
border
v-loading="dataListLoading"
@selection-change="selectionChangeHandle"
style="width: 100%"
>
<el-table-column
type="selection"
header-align="center"
align="center"
width="50"
>
</el-table-column>
<el-table-column
prop="attrGroupId"
header-align="center"
align="center"
label="分组id"
>
</el-table-column>
<el-table-column
prop="attrGroupName"
header-align="center"
align="center"
label="组名"
>
</el-table-column>
<el-table-column
prop="sort"
header-align="center"
align="center"
label="排序"
>
</el-table-column>
<el-table-column
prop="descript"
header-align="center"
align="center"
label="描述"
>
</el-table-column>
<el-table-column
prop="icon"
header-align="center"
align="center"
label="组图标"
>
</el-table-column>
<el-table-column
prop="catelogId"
header-align="center"
align="center"
label="所属分类id"
>
</el-table-column>
<el-table-column
fixed="right"
header-align="center"
align="center"
width="150"
label="操作"
>
<template slot-scope="scope">
<el-button
type="text"
size="small"
@click="addOrUpdateHandle(scope.row.attrGroupId)"
>修改</el-button
>
<el-button
type="text"
size="small"
@click="deleteHandle(scope.row.attrGroupId)"
>删除</el-button
>
</template>
</el-table-column>
</el-table>
<el-pagination
@size-change="sizeChangeHandle"
@current-change="currentChangeHandle"
:current-page="pageIndex"
:page-sizes="[10, 20, 50, 100]"
:page-size="pageSize"
:total="totalPage"
layout="total, sizes, prev, pager, next, jumper"
>
</el-pagination>
<!-- 弹窗, 新增 / 修改 -->
<add-or-update
v-if="addOrUpdateVisible"
ref="addOrUpdate"
@refreshDataList="getDataList"
></add-or-update></div
></el-col>
</el-row>
</template>
<script>
import Category from "../common/category.vue";
import AddOrUpdate from "./attrgroup-add-or-update";
export default {
data() {
return {
catId: 0,
dataForm: {
key: "",
},
dataList: [],
pageIndex: 1,
pageSize: 10,
totalPage: 0,
dataListLoading: false,
dataListSelections: [],
addOrUpdateVisible: false,
};
},
components: {
AddOrUpdate,
Category,
},
activated() {
this.getDataList();
},
methods: {
treeNodeClick(data, node, component) {
//获取子组件传递过来的值
console.log(
"获取子组件传递过来的值:data:",
data,
"node",
node,
"component:",
component
);
//查询三级分类信息 当点击三级分类id的时候 才查询相关的信息
if(node.level == 3){
this.catId = data.catId;
this.getDataList();
}
},
// 获取数据列表
getDataList() {
this.dataListLoading = true;
this.$http({
url: this.$http.adornUrl(`/product/attrgroup/list/${this.catId}`),
method: "get",
params: this.$http.adornParams({
page: this.pageIndex,
limit: this.pageSize,
key: this.dataForm.key,
}),
}).then(({ data }) => {
if (data && data.code === 0) {
this.dataList = data.page.list;
this.totalPage = data.page.totalCount;
} else {
this.dataList = [];
this.totalPage = 0;
}
this.dataListLoading = false;
});
},
// 每页数
sizeChangeHandle(val) {
this.pageSize = val;
this.pageIndex = 1;
this.getDataList();
},
// 当前页
currentChangeHandle(val) {
this.pageIndex = val;
this.getDataList();
},
// 多选
selectionChangeHandle(val) {
this.dataListSelections = val;
},
// 新增 / 修改
addOrUpdateHandle(id) {
this.addOrUpdateVisible = true;
this.$nextTick(() => {
this.$refs.addOrUpdate.init(id);
});
},
// 删除
deleteHandle(id) {
var ids = id
? [id]
: this.dataListSelections.map((item) => {
return item.attrGroupId;
});
this.$confirm(
`确定对[id=${ids.join(",")}]进行[${id ? "删除" : "批量删除"}]操作?`,
"提示",
{
confirmButtonText: "确定",
cancelButtonText: "取消",
type: "warning",
}
).then(() => {
this.$http({
url: this.$http.adornUrl("/product/attrgroup/delete"),
method: "post",
data: this.$http.adornData(ids, false),
}).then(({ data }) => {
if (data && data.code === 0) {
this.$message({
message: "操作成功",
type: "success",
duration: 1500,
onClose: () => {
this.getDataList();
},
});
} else {
this.$message.error(data.msg);
}
});
});
},
},
};
</script>

<style>
</style>

后端代码

@Override
public PageUtils queryPage(Map<String, Object> params, Long catelogId) {
//首先先判断catelogId是不是0 如果是0的话就查询所有的属性分组信息
if (catelogId == 0) {
IPage<AttrGroupEntity> page = this.page(
new Query<AttrGroupEntity>().getPage(params),
new QueryWrapper<AttrGroupEntity>()
);
return new PageUtils(page);
} else {
//查询的时候传递过来的关键词
String key = (String) params.get("key");
//SQL语句:select * from pms_attr_group where catelog_id = ? and (attr_group_id = key or attr_group_name = %key%)
LambdaQueryWrapper<AttrGroupEntity> queryWrapper = new LambdaQueryWrapper<>();
queryWrapper.eq(AttrGroupEntity::getCatelogId, catelogId);
if (!StringUtils.isEmpty(key)) {
queryWrapper.and((obj) -> {
obj.eq(AttrGroupEntity::getAttrGroupId, key).or().like(AttrGroupEntity::getAttrGroupName, key);
});
}
IPage<AttrGroupEntity> page = this.page(
new Query<AttrGroupEntity>().getPage(params),
queryWrapper
);
return new PageUtils(page);
}
}

属性分组的添加功能

前端代码

<template>
<el-dialog
:title="!dataForm.attrGroupId ? '新增' : '修改'"
:close-on-click-modal="false"
:visible.sync="visible"
@closed="dialogClose"
>
<el-form
:model="dataForm"
:rules="dataRule"
ref="dataForm"
@keyup.enter.native="dataFormSubmit()"
label-width="100px"
>
<el-form-item label="组名" prop="attrGroupName">
<el-input
v-model="dataForm.attrGroupName"
placeholder="组名"
></el-input>
</el-form-item>
<el-form-item label="排序" prop="sort">
<el-input v-model="dataForm.sort" placeholder="排序"></el-input>
</el-form-item>
<el-form-item label="描述" prop="descript">
<el-input v-model="dataForm.descript" placeholder="描述"></el-input>
</el-form-item>
<el-form-item label="组图标" prop="icon">
<el-input v-model="dataForm.icon" placeholder="组图标"></el-input>
</el-form-item>
<el-form-item label="所属分类id" prop="catelogId">
<!-- <el-input v-model="dataForm.catelogId" placeholder="所属分类id"></el-input> -->
<el-cascader
v-model="dataForm.catelogPath"
:options="categorys"
:props="props"
placeholder="试试搜索:手机"
filterable
></el-cascader>
</el-form-item>
</el-form>
<span slot="footer" class="dialog-footer">
<el-button @click="visible = false">取消</el-button>
<el-button type="primary" @click="dataFormSubmit()">确定</el-button>
</span>
</el-dialog>
</template>

<script>
import Category from "../common/category.vue";

export default {
data() {
return {
props: {
value: "catId",
label: "name",
children: "children",
},
categorys: [], //所有的三级分类信息
visible: false,
dataForm: {
attrGroupId: 0,
attrGroupName: "",
sort: "",
descript: "",
icon: "",
catelogPath: [],
catelogId: 0,
},
dataRule: {
attrGroupName: [
{ required: true, message: "组名不能为空", trigger: "blur" },
],
sort: [{ required: true, message: "排序不能为空", trigger: "blur" }],
descript: [
{ required: true, message: "描述不能为空", trigger: "blur" },
],
icon: [{ required: true, message: "组图标不能为空", trigger: "blur" }],
catelogId: [
{ required: true, message: "所属分类id不能为空", trigger: "blur" },
],
},
};
},
created() {
this.getCategorys();
},
methods: {
dialogClose() {
this.dataForm.catelogPath = [];
},
getCategorys() {
this.$http({
url: this.$http.adornUrl("/product/category/list/tree"),
method: "get",
}).then(({ data }) => {
this.categorys = data.data;
});
},
init(id) {
this.dataForm.attrGroupId = id || 0;
this.visible = true;
this.$nextTick(() => {
this.$refs["dataForm"].resetFields();
if (this.dataForm.attrGroupId) {
this.$http({
url: this.$http.adornUrl(
`/product/attrgroup/info/${this.dataForm.attrGroupId}`
),
method: "get",
params: this.$http.adornParams(),
}).then(({ data }) => {
if (data && data.code === 0) {
this.dataForm.attrGroupName = data.attrGroup.attrGroupName;
this.dataForm.sort = data.attrGroup.sort;
this.dataForm.descript = data.attrGroup.descript;
this.dataForm.icon = data.attrGroup.icon;
this.dataForm.catelogId = data.attrGroup.catelogId;
//查出catelog的完整路径
this.dataForm.catelogPath = data.attrGroup.catelogPath;
}
});
}
});
},
// 表单提交
dataFormSubmit() {
this.$refs["dataForm"].validate((valid) => {
if (valid) {
this.$http({
url: this.$http.adornUrl(
`/product/attrgroup/${
!this.dataForm.attrGroupId ? "save" : "update"
}`
),
method: "post",
data: this.$http.adornData({
attrGroupId: this.dataForm.attrGroupId || undefined,
attrGroupName: this.dataForm.attrGroupName,
sort: this.dataForm.sort,
descript: this.dataForm.descript,
icon: this.dataForm.icon,
catelogId:
this.dataForm.catelogPath[this.dataForm.catelogPath.length - 1],
}),
}).then(({ data }) => {
if (data && data.code === 0) {
this.$message({
message: "操作成功",
type: "success",
duration: 1500,
onClose: () => {
this.visible = false;
this.$emit("refreshDataList");
},
});
} else {
this.$message.error(data.msg);
}
});
}
});
},
},
};
</script>

后端代码

在商品分类的children上加上@JsonInclude注解,解决后面的级联选择多出一个空白的选择的问题

@JsonInclude(JsonInclude.Include.NON_EMPTY)
@TableField(exist = false)//这个注解要加,因为这个属性不是数据库中的字段
private List<CategoryEntity> children;//这里的属性名不是瞎起的,与ElementUI中的显示相对应,这里改了,前端也要改

修改的时候,级联选择部分数据的回显

/**
* 找到catelog的完整路径
* @param catelogId 路径的最后一个catelog的id
* @return 完整的路径
*/
@Override
public Long[] findCatelogPath(Long catelogId) {
List<Long> paths = new ArrayList<>();
List<Long> parentPath = findParentPath(catelogId, paths);
Collections.reverse(parentPath);
return (Long[]) parentPath.toArray(new Long[parentPath.size()]);
}
/**
* 递归找出所有的路径
*/
private List<Long> findParentPath(Long catelogId,List<Long> paths){
paths.add(catelogId);
//根据id查询相关的信息
CategoryEntity category = this.getById(catelogId);
if(category.getParentCid()!=0){
findParentPath(category.getParentCid(),paths);
}
return paths;
}

属性关联商品的规格参数和基本属性信息

功能描述

image-20230625090118059

查询商品下关联的属性分组信息

查询商品关联属性分组的后端关键代码

/**
* 根据分组的id查询关联的所有属性
*
* @param attrgroupId 分组的id
* @return 关联的所有属性组成集合
*/
@Override
public List<AttrEntity> getRelationAttr(Long attrgroupId) {
//根据中间表查询出属性分组的id信息
List<AttrAttrgroupRelationEntity> relationEntityList = relationDao.selectList(
new LambdaQueryWrapper<AttrAttrgroupRelationEntity>().eq(AttrAttrgroupRelationEntity::getAttrGroupId, attrgroupId));
List<Long> attrIds = new ArrayList<>();
for (AttrAttrgroupRelationEntity relationEntity : relationEntityList) {
attrIds.add(relationEntity.getAttrId());
}
//根据属性分组的id查询属性分组的详细信息
return (List<AttrEntity>) this.listByIds(attrIds);
}

移除关联属性分组的功能后端的关键代码

service层的代码

/**
* 批量删除商品关联的属性分组信息
*
* @param vos 属性id和属性分组id组成的集合
*/
@Override
public void deleteRelation(AttrGroupRelationVo[] vos) {
List<AttrAttrgroupRelationEntity> entities = Arrays.asList(vos).stream().map((item) -> {
AttrAttrgroupRelationEntity relationEntity = new AttrAttrgroupRelationEntity();
BeanUtils.copyProperties(item, relationEntity);
return relationEntity;
}).collect(Collectors.toList());
relationDao.deleteBatchRelation(entities);
}

dao层的代码

使用动态的sql批量的删除属性和分组的关联关系

<!--void deleteBatchRelation(List<AttrAttrgroupRelationEntity> entities);-->
<delete id="deleteBatchRelation">
delete from `pms_attr_attrgroup_relation` where
<foreach collection="entities" item="item" separator=" OR ">
(attr_id = #{item.attrId}) and attr_group_id = #{item.attrGroupId}
</foreach>
</delete>

查询属性分组没有关联的属性

/**
* 获取当前分组没有关联的所有属性
*
* @param params 分页相关的参数
* @param attrgroupId 属性分组的id
* @return 分页相关的数据
*/
@Override
public PageUtils getNoRelationAttr(Map<String, Object> params, Long attrgroupId) {
//当前的分组只能关联自己所属的分类里面的所有属性 例如手机的分类里面只能关联手机的所有属性
AttrGroupEntity attrGroupEntity = attrGroupDao.selectById(attrgroupId);
Long catelogId = attrGroupEntity.getCatelogId();
//当前分组只能关联别的分组没有引用的属性
List<AttrGroupEntity> groups = attrGroupDao.selectList(
new LambdaQueryWrapper<AttrGroupEntity>().eq(AttrGroupEntity::getCatelogId, catelogId));
List<Long> collect = groups.stream().map((item) -> {
return item.getAttrGroupId();
}).collect(Collectors.toList());
List<AttrAttrgroupRelationEntity> groupId = relationDao.selectList(
new LambdaQueryWrapper<AttrAttrgroupRelationEntity>().in(AttrAttrgroupRelationEntity::getAttrGroupId, collect));
List<Long> attrIds = groupId.stream().map((item) -> {
return item.getAttrId();
}).collect(Collectors.toList());
LambdaQueryWrapper<AttrEntity> queryWrapper =
new LambdaQueryWrapper<AttrEntity>().eq(AttrEntity::getCatelogId, catelogId).eq(AttrEntity::getAttrType,ProductConstant.AttrEnum.ATTR_TYPE_BASE.getCode());
if (attrIds != null && attrIds.size() > 0) {
queryWrapper.notIn(AttrEntity::getAttrId, attrIds);
}
String key = (String) params.get("key");
if (!StringUtils.isEmpty(key)) {
queryWrapper.eq(AttrEntity::getAttrId, key).or().like(AttrEntity::getAttrName, key);
}
IPage<AttrEntity> page = this.page(new Query<AttrEntity>().getPage(params), queryWrapper);
return new PageUtils(page);
}

给属性关联相关的规格参数和销售属性

@Override
public void saveBatch(List<AttrGroupRelationVo> vos) {
List<AttrAttrgroupRelationEntity> entities = vos.stream().map((item) -> {
AttrAttrgroupRelationEntity relationEntity = new AttrAttrgroupRelationEntity();
BeanUtils.copyProperties(item, relationEntity);
return relationEntity;
}).collect(Collectors.toList());
this.saveBatch(entities);
}

1.3.2 规格参数功能

功能截图

image-20230619111338153

新增功能的后端关键代码

/**
* 保存规格参数信息
* @param attr 页面中提交的规格参数相关的信息组成的实体
*/
@Transactional
@Override
public void saveAttr(AttrVo attr) {
AttrEntity attrEntity = new AttrEntity();
BeanUtils.copyProperties(attr,attrEntity);
//保存基本的数据
this.save(attrEntity);
//保存关联关系
AttrAttrgroupRelationEntity relationEntity = new AttrAttrgroupRelationEntity();
relationEntity.setAttrGroupId(attr.getAttrGroupId());
relationEntity.setAttrId(attrEntity.getAttrId());
relationDao.insert(relationEntity);
}

查询列表后端的关键代码

这里使用的stream流处理数据,没有使用多表联合查询来查询数据

@Override
public PageUtils queryBaseAttrPage(Map<String, Object> params, Long catelogId) {
LambdaQueryWrapper<AttrEntity> queryWrapper = new LambdaQueryWrapper<>();
if (catelogId != 0) {//等于0就是查询所有的分类信息下的规格参数信息
queryWrapper.eq(AttrEntity::getCatelogId, catelogId);
}
//获取检索的条件
String key = (String) params.get("key");
if (!StringUtils.isEmpty(key)) {
queryWrapper.like(AttrEntity::getAttrName, key).or().eq(AttrEntity::getAttrId, key);
}
IPage<AttrEntity> page = this.page(
new Query<AttrEntity>().getPage(params),
queryWrapper
);
PageUtils pageUtils = new PageUtils(page);
List<AttrEntity> records = page.getRecords();
List<AttrRespVo> respVos = records.stream().map((attrEntity) -> {
AttrRespVo attrRespVo = new AttrRespVo();
BeanUtils.copyProperties(attrEntity, attrRespVo);
//设置三级商品分类名和分组名
//设置所属的商品的分类名
CategoryEntity categoryEntity = categoryDao.selectById(attrRespVo.getCatelogId());
if (categoryEntity != null) {
attrRespVo.setCatelogName(categoryEntity.getName());
}
//设置分组名(需要从中间表查询分组的id,再查询分组名)
//先从中间表查询到分组的id
AttrAttrgroupRelationEntity relationEntity = relationDao.selectOne(
new LambdaQueryWrapper<AttrAttrgroupRelationEntity>().eq(AttrAttrgroupRelationEntity::getAttrId, attrRespVo.getAttrId()));
//从分组的表中查询分组名
if (relationEntity != null) {
AttrGroupEntity attrGroupEntity = attrGroupDao.selectById(relationEntity.getAttrGroupId());
attrRespVo.setGroupName(attrGroupEntity.getAttrGroupName());
}
return attrRespVo;
}).collect(Collectors.toList());
pageUtils.setList(respVos);
return pageUtils;
}

规格参数修改功能后端的关键代码

修改前的数据回显

查询属性的详情信息

/**
* 查询属性的详细信息
*
* @param attrId 属性的id
* @return 属性的详细信息封装的集合
*/
@Override
public AttrRespVo getAttrInfo(Long attrId) {
AttrRespVo attrRespVo = new AttrRespVo();
//查询属性的基本信息
AttrEntity attr = this.getById(attrId);
BeanUtils.copyProperties(attr, attrRespVo);
//查询分组id和分类的详细路径
//分组信息的设置
AttrAttrgroupRelationEntity relationEntity = relationDao.selectOne(
new LambdaQueryWrapper<AttrAttrgroupRelationEntity>().eq(AttrAttrgroupRelationEntity::getAttrId, attrRespVo.getAttrId()));
if (relationEntity != null) {
attrRespVo.setAttrGroupId(relationEntity.getAttrGroupId());
AttrGroupEntity attrGroupEntity = attrGroupDao.selectById(relationEntity.getAttrGroupId());
if (attrGroupEntity != null) {
attrRespVo.setGroupName(attrGroupEntity.getAttrGroupName());
}
}
//设置分类信息
Long catelogId = attr.getCatelogId();
//查询分类的完整路径
Long[] catelogPath = categoryService.findCatelogPath(catelogId);
if (catelogPath != null) {
attrRespVo.setCatelogPath(catelogPath);
}
//设置分类名
CategoryEntity category = categoryDao.selectById(catelogId);
if (category != null) {
attrRespVo.setCatelogName(category.getName());
}
return attrRespVo;
}

修改的时候页面回显的效果

image-20230624192906124

修改功能后端的关键代码

/**
* 修改属性的方法
*
* @param attr 页面传输过来的商品属性的实体
*/
@Transactional
@Override
public void updateAttr(AttrVo attr) {
//修改基本的数据
AttrEntity attrEntity = new AttrEntity();
BeanUtils.copyProperties(attr, attrEntity);
this.updateById(attrEntity);
//修改关联表的数据
AttrAttrgroupRelationEntity relationEntity = new AttrAttrgroupRelationEntity();
BeanUtils.copyProperties(attr, relationEntity);
LambdaQueryWrapper<AttrAttrgroupRelationEntity> queryWrapper = new LambdaQueryWrapper<>();
queryWrapper.eq(AttrAttrgroupRelationEntity::getAttrId, relationEntity.getAttrId());
Integer count = relationDao.selectCount(queryWrapper);
if (count > 0) {//判断是修改还是添加的操作
relationDao.update(relationEntity, queryWrapper);
} else {
relationDao.insert(relationEntity);
}
}

1.3.3 销售属性功能

分类属性列表显示

controller层的代码

/**
* /product/attr/sale/list/{catelogId}
* 路径 /product/attr/base/list/{catelogId}
* 规格参数的列表显示
*
* @param params 条件查询的参数
* @param catelogId 商品分类的id
* @return 规格参数的分页数据
*/
@GetMapping("/{attrType}/list/{catelogId}")
public R baseAttrList(@RequestParam Map<String, Object> params, @PathVariable Long catelogId, @PathVariable("attrType") String type) {
log.warn("规格参数分页的查询条件:{},商品分类的id:{}", params, catelogId);
PageUtils page = attrService.queryBaseAttrPage(params, catelogId,type);
return R.ok().put("page", page);
}

service层的代码

@Override
public PageUtils queryBaseAttrPage(Map<String, Object> params, Long catelogId, String type) {
LambdaQueryWrapper<AttrEntity> queryWrapper = new LambdaQueryWrapper<>();
//路径中属性的attrType是base就是基本属性,否则就代表的是销售属性
queryWrapper.eq(AttrEntity::getAttrType, "base".equalsIgnoreCase(type) ? 1 : 0);
if (catelogId != 0) {//等于0就是查询所有的分类信息下的规格参数信息
queryWrapper.eq(AttrEntity::getCatelogId, catelogId);
}
//获取检索的条件
String key = (String) params.get("key");
if (!StringUtils.isEmpty(key)) {
queryWrapper.like(AttrEntity::getAttrName, key).or().eq(AttrEntity::getAttrId, key);
}
IPage<AttrEntity> page = this.page(
new Query<AttrEntity>().getPage(params),
queryWrapper
);
PageUtils pageUtils = new PageUtils(page);
List<AttrEntity> records = page.getRecords();
List<AttrRespVo> respVos = records.stream().map((attrEntity) -> {
AttrRespVo attrRespVo = new AttrRespVo();
BeanUtils.copyProperties(attrEntity, attrRespVo);
//设置三级商品分类名和分组名
//设置所属的商品的分类名
CategoryEntity categoryEntity = categoryDao.selectById(attrRespVo.getCatelogId());
if (categoryEntity != null) {
attrRespVo.setCatelogName(categoryEntity.getName());
}
//设置分组名(需要从中间表查询分组的id,再查询分组名)
//先从中间表查询到分组的id
AttrAttrgroupRelationEntity relationEntity = relationDao.selectOne(
new LambdaQueryWrapper<AttrAttrgroupRelationEntity>().eq(AttrAttrgroupRelationEntity::getAttrId, attrRespVo.getAttrId()));
//从分组的表中查询分组名
if (relationEntity != null) {
AttrGroupEntity attrGroupEntity = attrGroupDao.selectById(relationEntity.getAttrGroupId());
if (attrGroupEntity != null) {
attrRespVo.setGroupName(attrGroupEntity.getAttrGroupName());
}
}
return attrRespVo;
}).collect(Collectors.toList());
pageUtils.setList(respVos);
return pageUtils;
}

完整的功能展示

image-20230625090037616

1.4 商品维护

1.4.1 SPU管理

条件查询功能(Spu检索)

/**
* Spu检索功能
*
* @param params 分页相关的数据
* @return 分页对象
*/
@Override
public PageUtils queryPageByCondition(Map<String, Object> params) {
LambdaQueryWrapper<SpuInfoEntity> queryWrapper = new LambdaQueryWrapper<>();
//关键字检索
String key = (String) params.get("key");
if (!StringUtils.isEmpty(key)) {
//这里使用了or,为了和下面的区分开来,要使用and,将下面这个条件用括号括起来
//SELECT COUNT(1) FROM pms_spu_info WHERE (((spu_name LIKE ? OR id = ?)) AND catalog_id = ? AND brand_id = ? AND publish_status = ?)
queryWrapper.and((wrapper) -> {
wrapper.like(SpuInfoEntity::getSpuName, key).or().eq(SpuInfoEntity::getId, key);
});
}
//分类
String catelogId = (String) params.get("catelogId");
if (!StringUtils.isEmpty(catelogId)) {
queryWrapper.eq(SpuInfoEntity::getCatalogId, catelogId);
}
//品牌
String brandId = (String) params.get("brandId");
if (!StringUtils.isEmpty(brandId)) {
queryWrapper.eq(SpuInfoEntity::getBrandId, brandId);
}
//状态
String status = (String) params.get("status");
if (!StringUtils.isEmpty(status)) {
queryWrapper.eq(SpuInfoEntity::getPublishStatus, status);
}

IPage<SpuInfoEntity> page = this.page(
new Query<SpuInfoEntity>().getPage(params),
queryWrapper
);
return new PageUtils(page);
}

实现的功能

image-20230627102918192

前端显示时间格式的数据有问题

配置文件中的配置

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

商品规格的回显

/**
* 查询商品的规格属性
* /product/attr/base/listforspu/{spuId}
*/
@GetMapping("/base/listforspu/{spuId}")
public R baseAttrList(@PathVariable("spuId") Long spuId){
List<ProductAttrValueEntity> entities = productAttrValueService.baseAttrListForSpu(spuId);
return R.ok().put("data",entities);
}

修改商品的规格

/**
* 修改商品的规格参数
* @param spuId 商品的id
* @param productAttrValueEntities 提交的修改之后的商品规格参数组成的集合
*/
@Transactional
@Override
public void updateSpuAttr(Long spuId, List<ProductAttrValueEntity> productAttrValueEntities) {
//1.删除这个spuId之前对应的所有属性
this.baseMapper.delete(new LambdaQueryWrapper<ProductAttrValueEntity>().eq(ProductAttrValueEntity::getSpuId,spuId));
//2.添加修改之后的商品的属性信息
List<ProductAttrValueEntity> collect = productAttrValueEntities.stream().map(item -> {
item.setSpuId(spuId);
return item;
}).collect(Collectors.toList());
this.saveBatch(collect);
}

实现的功能

image-20230628165931139

中间遇到的问题,点击规格,页面显示400的问题

image-20230628171558364

解决方案

第一步:在gulimall-admin数据库中执行以下的sql语句

INSERT INTO sys_menu (menu_id, parent_id, name, url, perms, type, icon, order_num) VALUES (76, 37, '规格维护', 'product/attrupdate', '', 2, 'log', 0);

第二步:将前端src/router/index.js替换成如下的内容

/**
* 全站路由配置
*
* 建议:
* 1. 代码中路由统一使用name属性跳转(不使用path属性)
*/
import Vue from 'vue'
import Router from 'vue-router'
import http from '@/utils/httpRequest'
import { isURL } from '@/utils/validate'
import { clearLoginInfo } from '@/utils'

Vue.use(Router)

// 开发环境不使用懒加载, 因为懒加载页面太多的话会造成webpack热更新太慢, 所以只有生产环境使用懒加载
const _import = require('./import-' + process.env.NODE_ENV)

// 全局路由(无需嵌套上左右整体布局)
const globalRoutes = [
{ path: '/404', component: _import('common/404'), name: '404', meta: { title: '404未找到' } },
{ path: '/login', component: _import('common/login'), name: 'login', meta: { title: '登录' } }
]

// 主入口路由(需嵌套上左右整体布局)
const mainRoutes = {
path: '/',
component: _import('main'),
name: 'main',
redirect: { name: 'home' },
meta: { title: '主入口整体布局' },
children: [
// 通过meta对象设置路由展示方式
// 1. isTab: 是否通过tab展示内容, true: 是, false: 否
// 2. iframeUrl: 是否通过iframe嵌套展示内容, '以http[s]://开头': 是, '': 否
// 提示: 如需要通过iframe嵌套展示内容, 但不通过tab打开, 请自行创建组件使用iframe处理!
{ path: '/home', component: _import('common/home'), name: 'home', meta: { title: '首页' } },
{ path: '/theme', component: _import('common/theme'), name: 'theme', meta: { title: '主题' } },
{ path: '/demo-echarts', component: _import('demo/echarts'), name: 'demo-echarts', meta: { title: 'demo-echarts', isTab: true } },
{ path: '/demo-ueditor', component: _import('demo/ueditor'), name: 'demo-ueditor', meta: { title: 'demo-ueditor', isTab: true } },
{ path: '/product-attrupdate', component: _import('modules/product/attrupdate'), name: 'attr-update', meta: { title: '规格维护', isTab: true } }
],
beforeEnter(to, from, next) {
let token = Vue.cookie.get('token')
if (!token || !/\S/.test(token)) {
clearLoginInfo()
next({ name: 'login' })
}
next()
}
}

const router = new Router({
mode: 'hash',
scrollBehavior: () => ({ y: 0 }),
isAddDynamicMenuRoutes: false, // 是否已经添加动态(菜单)路由
routes: globalRoutes.concat(mainRoutes)
})

router.beforeEach((to, from, next) => {
// 添加动态(菜单)路由
// 1. 已经添加 or 全局路由, 直接访问
// 2. 获取菜单列表, 添加并保存本地存储
if (router.options.isAddDynamicMenuRoutes || fnCurrentRouteType(to, globalRoutes) === 'global') {
next()
} else {
http({
url: http.adornUrl('/sys/menu/nav'),
method: 'get',
params: http.adornParams()
}).then(({ data }) => {
if (data && data.code === 0) {
fnAddDynamicMenuRoutes(data.menuList)
router.options.isAddDynamicMenuRoutes = true
sessionStorage.setItem('menuList', JSON.stringify(data.menuList || '[]'))
sessionStorage.setItem('permissions', JSON.stringify(data.permissions || '[]'))
next({ ...to, replace: true })
} else {
sessionStorage.setItem('menuList', '[]')
sessionStorage.setItem('permissions', '[]')
next()
}
}).catch((e) => {
console.log(`%c${e} 请求菜单列表和权限失败,跳转至登录页!!`, 'color:blue')
router.push({ name: 'login' })
})
}
})

/**
* 判断当前路由类型, global: 全局路由, main: 主入口路由
* @param {*} route 当前路由
*/
function fnCurrentRouteType(route, globalRoutes = []) {
var temp = []
for (var i = 0; i < globalRoutes.length; i++) {
if (route.path === globalRoutes[i].path) {
return 'global'
} else if (globalRoutes[i].children && globalRoutes[i].children.length >= 1) {
temp = temp.concat(globalRoutes[i].children)
}
}
return temp.length >= 1 ? fnCurrentRouteType(route, temp) : 'main'
}

/**
* 添加动态(菜单)路由
* @param {*} menuList 菜单列表
* @param {*} routes 递归创建的动态(菜单)路由
*/
function fnAddDynamicMenuRoutes(menuList = [], routes = []) {
var temp = []
for (var i = 0; i < menuList.length; i++) {
if (menuList[i].list && menuList[i].list.length >= 1) {
temp = temp.concat(menuList[i].list)
} else if (menuList[i].url && /\S/.test(menuList[i].url)) {
menuList[i].url = menuList[i].url.replace(/^\//, '')
var route = {
path: menuList[i].url.replace('/', '-'),
component: null,
name: menuList[i].url.replace('/', '-'),
meta: {
menuId: menuList[i].menuId,
title: menuList[i].name,
isDynamic: true,
isTab: true,
iframeUrl: ''
}
}
// url以http[s]://开头, 通过iframe展示
if (isURL(menuList[i].url)) {
route['path'] = `i-${menuList[i].menuId}`
route['name'] = `i-${menuList[i].menuId}`
route['meta']['iframeUrl'] = menuList[i].url
} else {
try {
route['component'] = _import(`modules/${menuList[i].url}`) || null
} catch (e) { }
}
routes.push(route)
}
}
if (temp.length >= 1) {
fnAddDynamicMenuRoutes(temp, routes)
} else {
mainRoutes.name = 'main-dynamic'
mainRoutes.children = routes
router.addRoutes([
mainRoutes,
{ path: '*', redirect: { name: '404' } }
])
sessionStorage.setItem('dynamicMenuRoutes', JSON.stringify(mainRoutes.children || '[]'))
console.log('\n')
console.log('%c!<-------------------- 动态(菜单)路由 s -------------------->', 'color:blue')
console.log(mainRoutes.children)
console.log('%c!<-------------------- 动态(菜单)路由 e -------------------->', 'color:blue')
}
}

export default router

1.4.2 发布商品

根据商品的分类联动的查询商品的品牌

image-20230626101113941

后端关键部分的代码

/**
* 查询商品分类关联的品牌信息
*
* @return 某一商品分类下关联的品牌信息组成的集合
*/
@Override
public List<BrandEntity> getBrandsByCatId(Long catId) {
List<CategoryBrandRelationEntity> categoryBrandRelationEntityList = relationDao.selectList(
new LambdaQueryWrapper<CategoryBrandRelationEntity>().eq(CategoryBrandRelationEntity::getCatelogId, catId));
List<BrandEntity> brandEntityList = categoryBrandRelationEntityList.stream().map(item -> {
Long brandId = item.getBrandId();
return brandService.getById(brandId);
}).collect(Collectors.toList());
return brandEntityList;
}

根据JSON数据生成实体类

生成工具: https://www.bejson.com/

image-20230626142854091

前端提交的发布商品的详细信息

{
"spuName": "华为Mate30 Pro",
"spuDescription": "华为手机",
"catalogId": 225,
"brandId": 1,
"weight": 0.198,
"publishStatus": 0,
"decript": ["https://gulimall-0611.oss-cn-beijing.aliyuncs.com/2023/06/26/7b9f237d-9d37-4087-83bd-13d0ea76eda4_73366cc235d68202.jpg", "https://gulimall-0611.oss-cn-beijing.aliyuncs.com/2023/06/26/e5c4fbe5-0685-4a12-8623-a4972c623d6c_528211b97272d88a.jpg"],
"images": ["https://gulimall-0611.oss-cn-beijing.aliyuncs.com/2023/06/26/7627fc94-1ac7-4406-8e1c-3866a002918b_0d40c24b264aa511.jpg", "https://gulimall-0611.oss-cn-beijing.aliyuncs.com/2023/06/26/e5364a2e-9f46-433b-827b-33c1ff8c991c_919c850652e98031.jpg", "https://gulimall-0611.oss-cn-beijing.aliyuncs.com/2023/06/26/997dd5dd-be60-4547-871c-23b1273df596_1f15cdbcf9e1273c.jpg", "https://gulimall-0611.oss-cn-beijing.aliyuncs.com/2023/06/26/788f45d7-d399-4482-88db-4dd939a51f4a_2b1837c6c50add30.jpg", "https://gulimall-0611.oss-cn-beijing.aliyuncs.com/2023/06/26/b18dd3f9-3a38-4e9a-86a0-8c24db552a09_3c24f9cd69534030.jpg", "https://gulimall-0611.oss-cn-beijing.aliyuncs.com/2023/06/26/d19426e9-c518-4fc8-85dd-7fae758a4000_23d9fbb256ea5d4a.jpg", "https://gulimall-0611.oss-cn-beijing.aliyuncs.com/2023/06/26/7200dec5-af25-4264-87dc-19985c4cbe23_335b2c690e43a8f8.jpg"],
"bounds": {
"buyBounds": 500,
"growBounds": 500
},
"baseAttrs": [{
"attrId": 3,
"attrValues": "2019",
"showDesc": 1
}, {
"attrId": 4,
"attrValues": "mate30pro",
"showDesc": 1
}, {
"attrId": 10,
"attrValues": "45W",
"showDesc": 1
}, {
"attrId": 11,
"attrValues": "LCD;OLED",
"showDesc": 1
}, {
"attrId": 8,
"attrValues": "16GB",
"showDesc": 1
}],
"skus": [{
"attr": [{
"attrId": 7,
"attrName": "手机颜色",
"attrValue": "星河银"
}, {
"attrId": 9,
"attrName": "商品产地",
"attrValue": "中国"
}],
"skuName": "华为Mate30 Pro 星河银 中国",
"price": "5799",
"skuTitle": "华为(HUAWEI) 华为Mate30 Pro全网通智能手机 (5G/4G)",
"skuSubtitle": "星河银的副标题",
"images": [{
"imgUrl": "https://gulimall-0611.oss-cn-beijing.aliyuncs.com/2023/06/26/7627fc94-1ac7-4406-8e1c-3866a002918b_0d40c24b264aa511.jpg",
"defaultImg": 1
}, {
"imgUrl": "https://gulimall-0611.oss-cn-beijing.aliyuncs.com/2023/06/26/e5364a2e-9f46-433b-827b-33c1ff8c991c_919c850652e98031.jpg",
"defaultImg": 0
}, {
"imgUrl": "",
"defaultImg": 0
}, {
"imgUrl": "https://gulimall-0611.oss-cn-beijing.aliyuncs.com/2023/06/26/788f45d7-d399-4482-88db-4dd939a51f4a_2b1837c6c50add30.jpg",
"defaultImg": 0
}, {
"imgUrl": "https://gulimall-0611.oss-cn-beijing.aliyuncs.com/2023/06/26/b18dd3f9-3a38-4e9a-86a0-8c24db552a09_3c24f9cd69534030.jpg",
"defaultImg": 0
}, {
"imgUrl": "",
"defaultImg": 0
}, {
"imgUrl": "",
"defaultImg": 0
}],
"descar": ["星河银", "中国"],
"fullCount": 3,
"discount": 0.98,
"countStatus": 0,
"fullPrice": 10000,
"reducePrice": 50,
"priceStatus": 0,
"memberPrice": [{
"id": 2,
"name": "铜牌会员",
"price": 5788
}, {
"id": 3,
"name": "银牌会员",
"price": 5699
}]
}, {
"attr": [{
"attrId": 7,
"attrName": "手机颜色",
"attrValue": "亮黑色"
}, {
"attrId": 9,
"attrName": "商品产地",
"attrValue": "中国"
}],
"skuName": "华为Mate30 Pro 亮黑色 中国",
"price": "6299",
"skuTitle": "华为(HUAWEI) 华为Mate30 Pro全网通智能手机 (5G/4G)",
"skuSubtitle": "亮黑色的副标题",
"images": [{
"imgUrl": "",
"defaultImg": 1
}, {
"imgUrl": "",
"defaultImg": 0
}, {
"imgUrl": "https://gulimall-0611.oss-cn-beijing.aliyuncs.com/2023/06/26/997dd5dd-be60-4547-871c-23b1273df596_1f15cdbcf9e1273c.jpg",
"defaultImg": 0
}, {
"imgUrl": "",
"defaultImg": 0
}, {
"imgUrl": "",
"defaultImg": 0
}, {
"imgUrl": "",
"defaultImg": 0
}, {
"imgUrl": "https://gulimall-0611.oss-cn-beijing.aliyuncs.com/2023/06/26/7200dec5-af25-4264-87dc-19985c4cbe23_335b2c690e43a8f8.jpg",
"defaultImg": 0
}],
"descar": ["亮黑色", "中国"],
"fullCount": 0,
"discount": 0,
"countStatus": 0,
"fullPrice": 0,
"reducePrice": 0,
"priceStatus": 0,
"memberPrice": [{
"id": 2,
"name": "铜牌会员",
"price": 0
}, {
"id": 3,
"name": "银牌会员",
"price": 0
}]
}, {
"attr": [{
"attrId": 7,
"attrName": "手机颜色",
"attrValue": "翡冷翠"
}, {
"attrId": 9,
"attrName": "商品产地",
"attrValue": "中国"
}],
"skuName": "华为Mate30 Pro 翡冷翠 中国",
"price": "6299",
"skuTitle": "华为(HUAWEI) 华为Mate30 Pro全网通智能手机 (5G/4G)",
"skuSubtitle": "翡冷翠的副标题",
"images": [{
"imgUrl": "",
"defaultImg": 0
}, {
"imgUrl": "",
"defaultImg": 0
}, {
"imgUrl": "https://gulimall-0611.oss-cn-beijing.aliyuncs.com/2023/06/26/997dd5dd-be60-4547-871c-23b1273df596_1f15cdbcf9e1273c.jpg",
"defaultImg": 0
}, {
"imgUrl": "",
"defaultImg": 0
}, {
"imgUrl": "",
"defaultImg": 0
}, {
"imgUrl": "https://gulimall-0611.oss-cn-beijing.aliyuncs.com/2023/06/26/d19426e9-c518-4fc8-85dd-7fae758a4000_23d9fbb256ea5d4a.jpg",
"defaultImg": 1
}, {
"imgUrl": "",
"defaultImg": 0
}],
"descar": ["翡冷翠", "中国"],
"fullCount": 0,
"discount": 0,
"countStatus": 0,
"fullPrice": 0,
"reducePrice": 0,
"priceStatus": 0,
"memberPrice": [{
"id": 2,
"name": "铜牌会员",
"price": 0
}, {
"id": 3,
"name": "银牌会员",
"price": 0
}]
}, {
"attr": [{
"attrId": 7,
"attrName": "手机颜色",
"attrValue": "罗兰紫"
}, {
"attrId": 9,
"attrName": "商品产地",
"attrValue": "中国"
}],
"skuName": "华为Mate30 Pro 罗兰紫 中国",
"price": "5799",
"skuTitle": "华为(HUAWEI) 华为Mate30 Pro全网通智能手机 (5G/4G)",
"skuSubtitle": "罗兰紫的副标题",
"images": [{
"imgUrl": "",
"defaultImg": 0
}, {
"imgUrl": "https://gulimall-0611.oss-cn-beijing.aliyuncs.com/2023/06/26/e5364a2e-9f46-433b-827b-33c1ff8c991c_919c850652e98031.jpg",
"defaultImg": 0
}, {
"imgUrl": "https://gulimall-0611.oss-cn-beijing.aliyuncs.com/2023/06/26/997dd5dd-be60-4547-871c-23b1273df596_1f15cdbcf9e1273c.jpg",
"defaultImg": 1
}, {
"imgUrl": "",
"defaultImg": 0
}, {
"imgUrl": "",
"defaultImg": 0
}, {
"imgUrl": "",
"defaultImg": 0
}, {
"imgUrl": "",
"defaultImg": 0
}],
"descar": ["罗兰紫", "中国"],
"fullCount": 0,
"discount": 0,
"countStatus": 0,
"fullPrice": 0,
"reducePrice": 0,
"priceStatus": 0,
"memberPrice": [{
"id": 2,
"name": "铜牌会员",
"price": 0
}, {
"id": 3,
"name": "银牌会员",
"price": 0
}]
}]
}

下载生成的实体类代码

image-20230626143051042

生成的实体类(将生成的实体类复制到项目中指定的位置)

image-20230626143228035

发布商品信息(核心业务)

后端关键代码

/**
* 发布商品的功能
*
* @param vo 需要保存的商品信息封装的数据
*/
@Transactional
@Override
public void saveSpuInfo(SpuSaveVo vo) {
//保存spu的基本信息 pms_spu_info
SpuInfoEntity infoEntity = new SpuInfoEntity();
BeanUtils.copyProperties(vo, infoEntity);
infoEntity.setCreateTime(new Date());
infoEntity.setUpdateTime(new Date());
this.saveBaseSpuInfo(infoEntity);
//保存spu的描述图片 pms_spu_info_desc
List<String> decript = vo.getDecript();
SpuInfoDescEntity descEntity = new SpuInfoDescEntity();
descEntity.setSpuId(infoEntity.getId());
descEntity.setDecript(String.join(",", decript));
spuInfoDescService.saveSpuInfoDecript(descEntity);
//保存spu的图片集 pms_spu_images
List<String> images = vo.getImages();
spuImagesService.saveImages(infoEntity.getId(), images);
//保存spu的规格参数 pms_product_attr_value
List<BaseAttrs> baseAttrs = vo.getBaseAttrs();
List<ProductAttrValueEntity> collect = baseAttrs.stream().map(attr -> {
ProductAttrValueEntity valueEntity = new ProductAttrValueEntity();
valueEntity.setAttrId(attr.getAttrId());
AttrEntity attrEntity = attrService.getById(attr.getAttrId());
valueEntity.setAttrName(attrEntity.getAttrName());
valueEntity.setAttrValue(attr.getAttrValues());
valueEntity.setQuickShow(attr.getShowDesc());
valueEntity.setSpuId(infoEntity.getId());
return valueEntity;
}).collect(Collectors.toList());
productAttrValueService.saveProductAttr(collect);
//保存spu的积分信息
Bounds bounds = vo.getBounds();
SpuBoundTo spuBoundTo = new SpuBoundTo();
spuBoundTo.setSpuId(infoEntity.getId());
BeanUtils.copyProperties(bounds, spuBoundTo);
R r = couponFeignService.saveSpuBounds(spuBoundTo);
if (r.getCode() != 0) {
log.error("远程保存spu的积分信息失败");
}
//保存当前spu对应的sku信息
List<Skus> skus = vo.getSkus();
if (skus != null && skus.size() != 0) {
skus.forEach(item -> {
String defaultImg = "";
for (Images img : item.getImages()) {
if (img.getDefaultImg() == 1) {
defaultImg = img.getImgUrl();
}
}
SkuInfoEntity skuInfoEntity = new SkuInfoEntity();
BeanUtils.copyProperties(item, skuInfoEntity);
skuInfoEntity.setBrandId(infoEntity.getBrandId());
skuInfoEntity.setCatalogId(infoEntity.getCatalogId());
skuInfoEntity.setSaleCount(0L);
skuInfoEntity.setSpuId(infoEntity.getId());
skuInfoEntity.setSkuDefaultImg(defaultImg);
// 保存sku的基本信息pms_sku_info
skuInfoService.saveSkuInfo(skuInfoEntity);
Long skuId = skuInfoEntity.getSkuId();
List<SkuImagesEntity> imagesEntities = item.getImages().stream().map(img -> {
SkuImagesEntity skuImagesEntity = new SkuImagesEntity();
skuImagesEntity.setSkuId(skuId);
skuImagesEntity.setImgUrl(img.getImgUrl());
skuImagesEntity.setDefaultImg(img.getDefaultImg());
return skuImagesEntity;
}).filter(entity -> {
//返回true就添加上去
return !StringUtils.isEmpty(entity.getImgUrl());
}).collect(Collectors.toList());
// 保存sku的图片信息pms_sku_images
skuImagesService.saveBatch(imagesEntities);
// sku的销售属性信息pms_sku_sale_attr_value
List<Attr> attrs = item.getAttr();
List<SkuSaleAttrValueEntity> skuSaleAttrValueEntities = attrs.stream().map(attr -> {
SkuSaleAttrValueEntity skuSaleAttrValueEntity = new SkuSaleAttrValueEntity();
BeanUtils.copyProperties(attr, skuSaleAttrValueEntity);
skuSaleAttrValueEntity.setSkuId(skuId);
return skuSaleAttrValueEntity;
}).collect(Collectors.toList());
skuSaleAttrValueService.saveBatch(skuSaleAttrValueEntities);
// sku的优惠信息、满减等信息gulimall_sms->sms_sku_ladder\sms_sku_full_reduction\sms_member_price
SkuReductionTo skuReductionTo = new SkuReductionTo();
BeanUtils.copyProperties(item, skuReductionTo);
List<MemberPrice> memberPrices = item.getMemberPrice().stream().map(memberPrice -> {
MemberPrice memberPriceTo = new MemberPrice();
BeanUtils.copyProperties(memberPrice, memberPriceTo);
return memberPriceTo;
}).collect(Collectors.toList());
skuReductionTo.setMemberPrice(memberPrices);
skuReductionTo.setSkuId(skuId);
if (skuReductionTo.getFullCount() > 0 || skuReductionTo.getFullPrice().compareTo(new BigDecimal("0")) == 1) {
R r1 = couponFeignService.saveSkuReduction(skuReductionTo);
if (r1.getCode() != 0) {
log.error("远程调用sku的优惠信息、满减信息失败");
}
}
});
}
}

最终的实现效果

image-20230626184303186

image-20230626182832804

1.4.3 商品管理

Sku检索

/**
* 检索条件
* {
* page: 1,//当前页码
* limit: 10,//每页记录数
* sidx: 'id',//排序字段
* order: 'asc/desc',//排序方式
* key: '华为',//检索关键字
* catelogId: 0,
* brandId: 0,
* min: 0,
* max: 0
* }
*/
@Override
public PageUtils queryPageByCondition(Map<String, Object> params) {
LambdaQueryWrapper<SkuInfoEntity> queryWrapper = new LambdaQueryWrapper<>();
//Preparing: SELECT sku_id,sku_name,sku_title,catalog_id,sale_count,price,brand_id,sku_default_img,sku_subtitle,sku_desc,spu_id
// FROM pms_sku_info
// WHERE (( (sku_id = ? OR sku_name LIKE ?) ) AND catalog_id = ? AND brand_id = ? AND price >= ? AND price <= ?) LIMIT ?,?
//检索关键字
String key = (String) params.get("key");
if (!StringUtils.isEmpty(key)) {
queryWrapper.and((wrapper) -> {
wrapper.eq(SkuInfoEntity::getSkuId, key).or().like(SkuInfoEntity::getSkuName, key);
});
}
//分类
String catelogId = (String) params.get("catelogId");
if (!StringUtils.isEmpty(catelogId) && !"0".equalsIgnoreCase(catelogId)) {
queryWrapper.eq(SkuInfoEntity::getCatalogId, catelogId);
}
//品牌
String brandId = (String) params.get("brandId");
if (!StringUtils.isEmpty(brandId) && !"0".equalsIgnoreCase(brandId)) {
queryWrapper.eq(SkuInfoEntity::getBrandId, brandId);
}
//价格区间
String min = (String) params.get("min");
if (!StringUtils.isEmpty(min)) {
queryWrapper.ge(SkuInfoEntity::getPrice, min);
}
String max = (String) params.get("max");
if (!StringUtils.isEmpty(max)) {
try {
BigDecimal bigDecimal = new BigDecimal(max);
if(bigDecimal.compareTo(new BigDecimal("0"))==1){//说明大于0
queryWrapper.le(SkuInfoEntity::getPrice, max);
}
} catch (Exception e) {
log.error("com/atguigu/gulimall/product/service/impl/SkuInfoServiceImpl");
e.printStackTrace();
throw new RuntimeException(e);
}
}
IPage<SkuInfoEntity> page = this.page(
new Query<SkuInfoEntity>().getPage(params),
queryWrapper
);
return new PageUtils(page);
}

实现的效果

image-20230627112218894

2.库存系统

2.1 仓库维护

仓库的分页加条件查询

@Override
public PageUtils queryPage(Map<String, Object> params) {

//分页查询的条件
LambdaQueryWrapper<WareInfoEntity> queryWrapper = new LambdaQueryWrapper<>();
String key = (String) params.get("key");
if (!StringUtils.isEmpty(key)) {
queryWrapper.like(WareInfoEntity::getName, key).or()
.eq(WareInfoEntity::getId, key)
.or().like(WareInfoEntity::getAddress, key)
.or().like(WareInfoEntity::getAreacode, key);
}
IPage<WareInfoEntity> page = this.page(
new Query<WareInfoEntity>().getPage(params),
queryWrapper
);
return new PageUtils(page);
}

实现的效果

image-20230627153518692

2.2 库存工作单

2.3 商品库存

商品库存的条件查询

/**
* {
* page: 1,//当前页码
* limit: 10,//每页记录数
* sidx: 'id',//排序字段
* order: 'asc/desc',//排序方式
* wareId: 123,//仓库id
* skuId: 123//商品id
* }
*/
@Override
public PageUtils queryPage(Map<String, Object> params) {
LambdaQueryWrapper<WareSkuEntity> queryWrapper = new LambdaQueryWrapper<>();
String wareId = (String) params.get("wareId");
if (!StringUtils.isEmpty(wareId)) {
queryWrapper.eq(WareSkuEntity::getWareId, wareId);
}
String skuId = (String) params.get("skuId");
if (!StringUtils.isEmpty(skuId)) {
queryWrapper.eq(WareSkuEntity::getSkuId, skuId);
}
IPage<WareSkuEntity> page = this.page(
new Query<WareSkuEntity>().getPage(params),
queryWrapper
);
return new PageUtils(page);
}

实现的效果

image-20230627160605717

2.4 采购单维护

2.4.1 采购需求

采购需求的分页加条件查询

/**
* status: 0,//状态
* wareId: 1,//仓库id
*/
@Override
public PageUtils queryPage(Map<String, Object> params) {
LambdaQueryWrapper<PurchaseDetailEntity> queryWrapper = new LambdaQueryWrapper<>();
String status = (String) params.get("status");
if (!StringUtils.isEmpty(status)) {
queryWrapper.eq(PurchaseDetailEntity::getStatus, status);
}
String wareId = (String) params.get("wareId");
if (!StringUtils.isEmpty(wareId)) {
queryWrapper.eq(PurchaseDetailEntity::getWareId, wareId);
}
String key = (String) params.get("key");
if (!StringUtils.isEmpty(key)) {
queryWrapper.and((wrapper->{
wrapper.eq(PurchaseDetailEntity::getPurchaseId, key).or().like(PurchaseDetailEntity::getSkuId,key);
}));
}
IPage<PurchaseDetailEntity> page = this.page(
new Query<PurchaseDetailEntity>().getPage(params),
queryWrapper
);
return new PageUtils(page);
}

实现的效果

image-20230627161955351

合并采购单

业务需求介绍

image-20230627162707377

获取没有领取的采购单,将采购需求合并到这个采购单

/**
* 获取没有领取的采购单
*/
@Override
public PageUtils queryPageUnreceivePurchase(Map<String, Object> params) {
IPage<PurchaseEntity> page = this.page(
new Query<PurchaseEntity>().getPage(params),
new LambdaQueryWrapper<PurchaseEntity>().eq(PurchaseEntity::getStatus,0).or().eq(PurchaseEntity::getStatus,1)
);

return new PageUtils(page);
}

合并采购需求

/**
* 合并采购需求
*
* @param mergeVo 前端传递的需要合并的采购需求
*/
@Transactional
@Override
public void mergePurchase(MergeVo mergeVo) {
//判断采购单是不是存在
Long purchaseId = mergeVo.getPurchaseId();
if (purchaseId == null) {//说明没有选择需要合并的采购单,需要先创建一个采购单
PurchaseEntity purchaseEntity = new PurchaseEntity();
purchaseEntity.setStatus(WareConstant.PurchaseStatesEnum.CREATED.getCode());
purchaseEntity.setCreateTime(new Date());
purchaseEntity.setUpdateTime(new Date());
this.save(purchaseEntity);
purchaseId = purchaseEntity.getId();
}
List<Long> items = mergeVo.getItems();
Long finalPurchaseId = purchaseId;
List<PurchaseDetailEntity> detailEntityList = items.stream().map(item -> {
PurchaseDetailEntity detailEntity = new PurchaseDetailEntity();
detailEntity.setId(item);
detailEntity.setPurchaseId(finalPurchaseId);
detailEntity.setStatus(WareConstant.PurchaseDeatilStatesEnum.ASSIGNED.getCode());
return detailEntity;
}).collect(Collectors.toList());
detailService.updateBatchById(detailEntityList);
//更新一下时间
PurchaseEntity purchaseEntity = new PurchaseEntity();
purchaseEntity.setId(purchaseId);
purchaseEntity.setUpdateTime(new Date());
this.updateById(purchaseEntity);
}

2.4.2 采购单

领取采购单

/**
* @param ids 采购单id
*/
@Override
public void received(List<Long> ids) {
//确认当前的采购单是新建或者分配状态
List<PurchaseEntity> purchaseEntities = ids.stream().map(id -> {
PurchaseEntity byId = this.getById(id);
return byId;
}).filter(item -> {
if (item.getStatus() == WareConstant.PurchaseStatesEnum.CREATED.getCode() ||
item.getStatus() == WareConstant.PurchaseDeatilStatesEnum.ASSIGNED.getCode()) {
return true;
}
return false;
}).map(item->{
//改变采购单状态
item.setStatus(WareConstant.PurchaseStatesEnum.RECEIVE.getCode());
item.setUpdateTime(new Date());
return item;
}).collect(Collectors.toList());
this.updateBatchById(purchaseEntities);
//改变采购项状态
purchaseEntities.forEach(item->{
List<PurchaseDetailEntity> entities = detailService.listDetailByPurchaseId(item.getId());
List<PurchaseDetailEntity> detailEntityList = entities.stream().map(entity -> {
PurchaseDetailEntity purchaseDetailEntity = new PurchaseDetailEntity();
purchaseDetailEntity.setId(entity.getId());
purchaseDetailEntity.setStatus(WareConstant.PurchaseDeatilStatesEnum.BUYING.getCode());
return purchaseDetailEntity;
}).collect(Collectors.toList());
detailService.updateBatchById(detailEntityList);
});
}

完成采购的功能

@Transactional
@Override
public void done(PurchaseDoneVo doneVo) {
Long id = doneVo.getId();
//改变采购项的状态
Boolean flag = true;
List<PurchaseItemDoneVo> items = doneVo.getItems();
List<PurchaseDetailEntity> purchaseItemDoneVos = new ArrayList<>();
for (PurchaseItemDoneVo item : items) {
PurchaseDetailEntity detailEntity = new PurchaseDetailEntity();
if (item.getStatus() == WareConstant.PurchaseDeatilStatesEnum.HASERROR.getCode()) {
flag = false;
detailEntity.setStatus(item.getStatus());
} else {
detailEntity.setStatus(WareConstant.PurchaseDeatilStatesEnum.FINISH.getCode());
//将成功采购的进行入库
PurchaseDetailEntity purchaseDetailEntity = detailService.getById(item.getItemId());
wareSkuService.addStock(purchaseDetailEntity.getSkuId(),purchaseDetailEntity.getWareId(),purchaseDetailEntity.getSkuNum());
}
detailEntity.setId(item.getItemId());
purchaseItemDoneVos.add(detailEntity);
}
detailService.updateBatchById(purchaseItemDoneVos);
//改变采购单的状态
PurchaseEntity purchaseEntity = new PurchaseEntity();
purchaseEntity.setId(id);
purchaseEntity.setStatus(flag ? WareConstant.PurchaseStatesEnum.FINISH.getCode() : WareConstant.PurchaseStatesEnum.HASERROR.getCode());
purchaseEntity.setUpdateTime(new Date());
this.updateById(purchaseEntity);
}

修改库存的功能

@Transactional
@Override
public void addStock(Long skuId, Long wareId, Integer skuNum) {
//判断是添加的操作还是修改的操作
List<WareSkuEntity> wareSkuEntities = wareSkuDao.selectList(new LambdaQueryWrapper<WareSkuEntity>().eq(WareSkuEntity::getSkuId, skuId).eq(WareSkuEntity::getWareId, wareId));
if (wareSkuEntities == null || wareSkuEntities.size() == 0) {
WareSkuEntity wareSkuEntity = new WareSkuEntity();
wareSkuEntity.setSkuId(skuId);
wareSkuEntity.setWareId(wareId);
wareSkuEntity.setStock(skuNum);
wareSkuEntity.setStockLocked(0);
//远程查询sku的名字
try {
R r = productFeignService.info(skuId);
if(r.getCode() == 0){
Map<String,Object> skuInfo = (Map<String, Object>) r.get("skuInfo");
wareSkuEntity.setSkuName((String) skuInfo.get("skuName"));
}
} catch (Exception e) {
log.warn("远程获取商品的Sku信息失败!");
}
wareSkuDao.insert(wareSkuEntity);
} else {
wareSkuDao.addStock(skuId, wareId, skuNum);
}
}

sql语句

 <!--void addStock(@Param("skuId") Long skuId, @Param("wareId") Long wareId, @Param("skuNum") Integer skuNum);-->
<insert id="addStock">
update `wms_ware_sku`
set stock = stock + #{skuNum}
where sku_id = #{skuId}
and ware_id = #{wareId}
</insert>

3.基础篇总结

image-20230628170218776

个人总结:

  • 细节很重要,编写代码的时候要注意细节问题(比如网关的配置,模糊路径和精确路径先后配置的问题)
  • 要有良好的编码习惯,注意代码解耦和代码复用
  • 业务逻辑很重要,编码之前要搞懂了业务逻辑之后再开始写代码
  • 学习技术的时候要融会贯通,学习编程的思想
  • 学技术一定要学会看开发文档(技术说明书),可以先从开发文档提供的demo开始

六.高级篇程序设计

1.ElasticSearch

详细的教程

ElasticSearch | The Blog (gitee.io)

2.商城业务

2.1 商品上架

1.商品Mapping

创建一个product索引和指定映射关系

PUT product
{
"mappings": {
"properties": {
"skuId": {
"type": "long"
},
"spuId": {
"type": "keyword"
},
"skuTitle": {
"type": "text",
"analyzer": "ik_smart"
},
"skuPrice": {
"type": "keyword"
},
"skuImg": {
"type": "keyword",
"index": false,
"doc_values": false
},
"saleCount": {
"type": "long"
},
"hasStock": {
"type": "boolean"
},
"hotScore": {
"type": "long"
},
"brandId": {
"type": "long"
},
"catalogId": {
"type": "long"
},
"brandName": {
"type": "keyword",
"index": false,
"doc_values": false
},
"brandImg": {
"type": "keyword",
"index": false,
"doc_values": false
},
"catalogName": {
"type": "keyword",
"index": false,
"doc_values": false
},
"attrs": {
"type": "nested",
"properties": {
"attrId": {
"type": "long"
},
"attrName": {
"type": "keyword",
"index": false,
"doc_values": false
},
"attrValue": {
"type": "keyword"
}
}
}
}
}
}

2.上架功能后端代码

所需的实体类

package com.atguigu.common.to.es;

import lombok.Data;

import java.math.BigDecimal;
import java.util.List;

/**
* @author Jason Gong
* @version 1.0
* @Date 2023/7/5
* @Description 对应ES中保存的文档的实体类
*/
@Data
public class SkuEsModel {
private Long skuId;

private Long spuId;

private String skuTitle;

private BigDecimal skuPrice;

private String skuImg;

private Long saleCount;

private Boolean hasStock;

private Long hotScore;

private Long brandId;

private Long catalogId;

private String brandName;

private String brandImg;

private String catalogName;

private List<Attrs> attrs;

@Data
public static class Attrs {

private Long attrId;

private String attrName;

private String attrValue;

}

}

商品上架的后端关键代码

/**
* 商品上架的功能
*/
@Override
public void up(Long spuId) {
//查询当前spuId对应的所有sku信息
List<SkuInfoEntity> skus = skuInfoService.getSkusBySpuId(spuId);
List<Long> skuIds = skus.stream().map(SkuInfoEntity::getSkuId).collect(Collectors.toList());
//规格属性信息的封装(可以被检索的规格属性信息)
List<ProductAttrValueEntity> baseAttrs = productAttrValueService.baseAttrListForSpu(spuId);
List<Long> attrIds = baseAttrs.stream().map(attr -> {
return attr.getAttrId();
}).collect(Collectors.toList());
//获取可以检索的属性id的集合
List<Long> searchAttrIds = attrService.selectSearchAttrs(attrIds);
//将其转换成set集合
Set<Long> idSet = new HashSet<>(searchAttrIds);
//最终需要封装的attrs
List<SkuEsModel.Attrs> attrs = baseAttrs.stream().filter(item -> {
return idSet.contains(item.getAttrId());
}).map(item -> {
SkuEsModel.Attrs skuAttr = new SkuEsModel.Attrs();
BeanUtils.copyProperties(item, skuAttr);
return skuAttr;
}).collect(Collectors.toList());
//库存
Map<Long, Boolean> stockMap = null;
try {
R r = wareFeignService.getSkusHasStock(skuIds);
List<SkuHasStockVo> skuHasStockVos = r.getData(new TypeReference<List<SkuHasStockVo>>(){});
stockMap = skuHasStockVos.stream().collect(Collectors.toMap(SkuHasStockVo::getSkuId, item -> item.getHasStock()));
} catch (Exception e) {
log.error("库存服务查询异常:原因{}",e);
}
//封装sku的信息
Map<Long, Boolean> finalStockMap = stockMap;
List<SkuEsModel> upProducts = skus.stream().map(sku -> {
//组装需要的数据
SkuEsModel skuEsModel = new SkuEsModel();
BeanUtils.copyProperties(sku, skuEsModel);
//价格 图片
skuEsModel.setSkuPrice(sku.getPrice());
skuEsModel.setSkuImg(sku.getSkuDefaultImg());
//设置库存信息
if(finalStockMap == null){
skuEsModel.setHasStock(true);
}else {
skuEsModel.setHasStock(finalStockMap.get(sku.getSkuId()));
}
//热度评分信息
skuEsModel.setHotScore(0L);
//品牌名 分类名 品牌价格
BrandEntity brand = brandService.getById(sku.getBrandId());
skuEsModel.setBrandName(brand.getName());
skuEsModel.setBrandImg(brand.getLogo());
CategoryEntity category = categoryService.getById(sku.getCatalogId());
skuEsModel.setCatalogName(category.getName());
//设置attrs
skuEsModel.setAttrs(attrs);
return skuEsModel;
}).collect(Collectors.toList());
R r = searchFeignService.productStatusUp(upProducts);
if(r.getCode() == 0){
//远程调用成功
spuInfoDao.updateSpuStatus(spuId, ProductConstant.StatusEnum.SPU_UP.getCode());
}else {
//远程调用失败
//TODO 重复调用的问题
/**
* feign的调用流程
* 1.构造请求的数据,将对象转化成json
* 2.发送请求进行执行(执行成功之后会解码响应数据)
* 3.执行请求会有重试机制
*/
}
}
/**
* 过滤出属性id中可以被检索的属性的id
* @param attrIds 原属性id的集合
* @return 可以被检索的属性id的集合
*/
@Override
public List<Long> selectSearchAttrs(List<Long> attrIds) {
//select attr_id from `pms_attr` where attr_id in (?) and search_type=1
return attrDao.selectSearchAttrs(attrIds);
}
<select id="selectSearchAttrs" resultType="java.lang.Long">
select `attr_id`from `pms_attr` where `attr_id` in
<foreach collection="attrIds" item="attr" separator="," open="(" close=")">
#{attr}
</foreach>
and `search_type`=1
</select>
/**
* 查询库存的方法
*/
@Override
public List<SkuHasStockVo> getSkusHasStock(List<Long> skuIds) {
List<SkuHasStockVo> skuHasStockVos = skuIds.stream().map(skuId -> {
SkuHasStockVo skuHasStockVo = new SkuHasStockVo();
Long count = wareSkuDao.getSkuStock(skuId);
skuHasStockVo.setSkuId(skuId);
if(count != null){
skuHasStockVo.setHasStock(count > 0);
}else {
skuHasStockVo.setHasStock(false);
}
return skuHasStockVo;
}).collect(Collectors.toList());
return skuHasStockVos;
}
/**
* 存储商品数据
*/
@Override
public Boolean productStatusUp(List<SkuEsModel> skuEsModels) throws IOException {
//将数据保存到ES中
//在ES中建立好索引
BulkRequest bulkRequest = new BulkRequest();
for (SkuEsModel skuEsModel : skuEsModels) {
IndexRequest indexRequest = new IndexRequest(EsConstant.PRODUCT_INDEX);
indexRequest.id(skuEsModel.getSkuId().toString());
indexRequest.source(JSON.toJSONString(skuEsModel), XContentType.JSON);
bulkRequest.add(indexRequest);
}
BulkResponse bulk = restHighLevelClient.bulk(bulkRequest, GulimallElasticSearchConfig.COMMON_OPTIONS);
boolean hasFailure = bulk.hasFailures();
List<String> ids = Arrays.stream(bulk.getItems()).map(item -> {
return item.getId();
}).collect(Collectors.toList());
log.info("上架商品的id:{}",ids);
return !hasFailure;
}

2.2 首页

项目的页面视图部署在对应的微服务项目的静态资源中

image-20230706112521327

2.2.1整合thymeleaf

添加thymeleaf的依赖

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

将资料中的静态资源放在微服务工程的对应位置,并在配置文件中加入对应的配置

image-20230706114304695

首页页面

image-20230706145050684

在index.html上加上thymeleaf的名称空间

<html xmlns:th="http://www.thymeleaf.org">

修改页面的时候需要频繁的重启项目,使用热更新来解决该问题

导入devtools的依赖

<!--devtools-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-devtools</artifactId>
<optional>true</optional>
</dependency>

重启项目之后会显示如下的效果

image-20230706154112026

这时我们在每次修改页面之后使用Ctrl+Shift+F9键可以热更新

image-20230706161401528

2.2.2 首页面三级分类显示

首页面的跳转以及一级分类的显示

/**
* 首页面的跳转
*/
@GetMapping({"/","index.html"})
public String indexPage(Model model){
//查询所有的一级分类
List<CategoryEntity> categorys = categoryService.getLevelOneCategorys();
model.addAttribute("categorys",categorys);
return "index";
}

用于用户端首页面三级分类封装的数据

package com.atguigu.gulimall.product.vo;

import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;

import java.util.List;

/**
* @author Jason Gong
* @version 1.0
* @website https://jasonsgong.gitee.io
* @Date 2023/7/7
* @Description 用户端首页面三级分类封装的数据
*/
@Data
@AllArgsConstructor
@NoArgsConstructor
public class Catelog2Vo {

/**
* 一级父分类的id
*/
private String catalog1Id;

/**
* 三级子分类
*/
private List<Category3Vo> catalog3List;

private String id;

private String name;


/**
* 三级分类vo
* 静态内部类
*/
@Data
@AllArgsConstructor
@NoArgsConstructor
public static class Category3Vo {

/**
* 父分类、二级分类id
*/
private String catalog2Id;

private String id;

private String name;
}
}

前端相关的修改

 <li th:each=" category:${categorys}">
<a href="#" class="header_main_left_a" ctg-data="3" th:attr="ctg-data=${category.catId}"><b th:text="${category.name}"></b></a>
</li>

image-20230707151505310

controller层代码

/**
* 首页面的商品分分类信息
*/
@ResponseBody
@GetMapping("/index/catalog.json")
public Map<String, List<Catelog2Vo>> getCatalogJson(){
return categoryService.getCatalogJson();
}

service层代码

@Override
public Map<String, List<Catelog2Vo>> getCatalogJson() {
//查出所有的一级分类
List<CategoryEntity> levelOneCategorys = getLevelOneCategorys();
//封装数据
Map<String, List<Catelog2Vo>> catelog2VoMap = levelOneCategorys.stream().collect(Collectors.toMap(k -> k.getCatId().toString(), v -> {
//查询以及分类下的二级分类
List<CategoryEntity> categoryEntities = categoryDao.selectList(new LambdaQueryWrapper<CategoryEntity>().eq(CategoryEntity::getParentCid, v.getCatId()));
List<Catelog2Vo> catelog2Vos = null;
if (categoryEntities != null) {
catelog2Vos = categoryEntities.stream().map(item -> {
Catelog2Vo catelog2Vo = new Catelog2Vo(v.getCatId().toString(), null, item.getCatId().toString(), item.getName());
//当前二级分类的三级分类
List<CategoryEntity> levelThreeCatelog = categoryDao.selectList(new LambdaQueryWrapper<CategoryEntity>().eq(CategoryEntity::getParentCid, item.getCatId()));
if(levelThreeCatelog != null){
//封装成指定的数据
List<Catelog2Vo.Category3Vo> category3VoList = levelThreeCatelog.stream().map(threeCatelog -> {
Catelog2Vo.Category3Vo category3Vo = new Catelog2Vo.Category3Vo();
category3Vo.setCatalog2Id(item.getCatId().toString());
category3Vo.setName(threeCatelog.getName());
category3Vo.setId(threeCatelog.getCatId().toString());
return category3Vo;
}).collect(Collectors.toList());
catelog2Vo.setCatalog3List(category3VoList);
}
return catelog2Vo;
}).collect(Collectors.toList());
}
return catelog2Vos;
}));
return catelog2VoMap;
}

页面实现的效果

image-20230707151543593

2.3 搭建域名访问环境

1.安装SwitchHosts软件

可以快捷的编辑操作hosts文件

打开的时候以管理员的身份打开

image-20230707153628394

image-20230707154052952

在添加的方案中添加如下的内容,点击右下角的保存按钮保存(保存失败的更改hosts文件的权限,将只读勾选掉)

192.168.195.100 gulimall.com

保存成功之后可以直接访问域名,解析到虚拟机的ip

Tips:有科学上网的测试的时候要先关闭科学上网的软件

安装了ES的访问gulimall.com:9200可以看到相应的内容

image-20230707160328001

2.正式搭建项目的域名访问环境

image-20230708092745456

nginx的配置文件

image-20230708093600923

配置nginx

#切换到nginx挂载在本机的配置文件所在的目录
cd /mydata/nginx/conf
#切换到nginx的分配置文件下复制一份 定义gulimall的配置,主配置文件中有include /etc/nginx/conf.d/*.conf;这么一段配置,主配置文件
#会包含conf.d目录下的所有配置
cd /mydata/nginx/conf/conf.d
cp default.conf gulimall.conf 复制一份
#在gulimall.conf配置文件中配置我们的配置
#配置如下配置

niginx的配置文件如下

server {
listen 80;
server_name gulimall.com;

location / {
#服务的IP 192.168.0.112
#服务的端口: 10000
proxy_pass http://192.168.0.112:10000;
}
error_page 500 502 503 504 /50x.html;
location = /50x.html {
root /usr/share/nginx/html;
}
}

这时我们在访问gulimall.com的时候就可以访问一下的页面了

image-20230708095940126

3.让nginx代理到网关

在nginx.conf配置文件中加上如下的配置

#配置上游服务器
#gulimall是上游服务器的组名
upstream gulimall{
#网关的地址
server 192.168.0.112:88;
}

修改gulimall.conf中的配置

server {
listen 80;
server_name gulimall.com;

location / {
proxy_pass http://gulimall;
}

error_page 500 502 503 504 /50x.html;
location = /50x.html {
root /usr/share/nginx/html;
}
}

在网关的配置文件中加上一下的配置

# nginx代理相关的断言
- id: gulimall_host_route
uri: lb://gulimall-product
predicates:
- Host=**.gulimall.com,gulimall.com

这时我们重启网关的时候发现无法访问到相应的页面 是由于nginx访问网关的时候丢了host

这时我们需要加上如下的配置

proxy_set_header Host $host;

image-20230708104321024

image-20230708104848501

2.4 性能压测

压力测试与性能监控 | The Blog (gitee.io)

JMeter压力测试

image-20230708152102009

性能监控

jconsole
jvisualvm

image-20230709155555491

2.4.1优化中间件对性能的影响

网关的优化、数据库索引的优化、thymeleaf的优化、静态资源的优化、nginx动静分离

image-20230710111805705

2.5 缓存使用

2.5.1 本地缓存与分布式缓存

为了系统性能的提升,我们一般都会将部分数据放入缓存中,加速访问。而 db 承担数据落盘工作。
哪些数据适合放入缓存?
1.即时性、数据一致性要求不高的
2.访问量大且更新频率不高的数据(读多,写少)

本地缓存

image-20230710143906286

分布式缓存

image-20230710143341531

2.5.2 整合redis

1.安装Redis

image-20230710144052877

2.项目中使用Redis

SpringBoot整合Redis | The Blog (gitee.io)

2.1 引入依赖

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

2.2 添加配置

spring:
redis:
host: 192.168.195.100
port: 6379

2.3 测试使用

@Autowired
private StringRedisTemplate stringRedisTemplate;

@Test
void testRedis(){
ValueOperations<String, String> ops = stringRedisTemplate.opsForValue();
ops.set("hello","word_"+ UUID.randomUUID().toString());
String hello = ops.get("hello");
System.out.println(hello);
}

image-20230710145648233

lettuce堆外内存溢出bug

产生原因:

1)、springboot2.0以后默认使用 lettuce作为操作redis的客户端,它使用netty进行网络通信

2)、lettuce的bug导致netty堆外内存溢出。netty如果没有指定堆外内存,默认使用Xms的值,可以使用-Dio.netty.maxDirectMemory进行设置

解决方案:由于是lettuce的bug造成,不要直接使用-Dio.netty.maxDirectMemory去调大虚拟机堆外内存,治标不治本。

  • 1)、升级lettuce客户端
  • 2)、切换使用jedis

修改依赖文件,使用jedis作为客户端工具

<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
<exclusions>
<exclusion>
<groupId>io.lettuce</groupId>
<artifactId>lettuce-core</artifactId>
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>redis.clients</groupId>
<artifactId>jedis</artifactId>
</dependency>

2.5.3 高并发下缓存失效问题

缓存穿透

image-20230710153635804

缓存雪崩

image-20230710153651752

缓存击穿

image-20230710153709464

分布式下如何加锁?

image-20230710163735955

锁-时序问题

image-20230710165738655

分布式锁演进-基本原理

image-20230714163437984

分布式锁的最终形态

image-20230714171221296

2.5.4 分布式锁-Redisson

github地址: https://github.com/redisson/redisson

1.引入依赖

<!--redisson-->
<dependency>
<groupId>org.redisson</groupId>
<artifactId>redisson</artifactId>
<version>3.12.0</version>
</dependency>

2.配置redisson

package com.atguigu.gulimall.product.config;

import org.redisson.Redisson;
import org.redisson.api.RedissonClient;
import org.redisson.config.Config;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

import java.io.IOException;

/**
* @author Jason Gong
* @version 1.0
* @website https://jasonsgong.gitee.io
* @Date 2023/7/14
* @Description Redisson的配置
*/
@Configuration
public class MyRedissonConfig {

/**
* 所有对redisson的操作通过RedissonClient
*/
@Bean(destroyMethod = "shutdown")
public RedissonClient redisson() throws IOException {
//创建配置
Config config = new Config();
config.useSingleServer().setAddress("redis://192.168.195.100:6379");
//根据配置config创建RedissonClient实例
return Redisson.create(config);
}
}

3.测试使用

@Autowired
private RedissonClient redissonClient;

@Test
void testRedisson(){
System.out.println(redissonClient);
}

image-20230714175205635

4.简单的使用分布式锁

上一个线程加锁之后没有解锁的话,后面的线程会一直等待前面的线程释放锁,自己再加锁。

@ResponseBody
@GetMapping("/hello")
public String hello() {
//打印时间需要
SimpleDateFormat sdf = new SimpleDateFormat();// 格式化时间
sdf.applyPattern("yyyy-MM-dd HH:mm:ss");// a为am/pm的标记
Date date = new Date();// 获取当前时间

//获取同一把锁,只要锁的名字一样,就是同一把锁
RLock lock = redissonClient.getLock("my-lock");
//加锁
lock.lock();//阻塞式等待,锁没有释放的情况下会一直等待下去
//1.锁的自动续期,如果业务耗费的时间超长,运行时会自动给锁续上新的30s(默认时续期30s),不必担心业务时间长,锁自动过期被删掉
//2.加锁的业务只要运行完成,就不会给当前锁续期,即使不手动解锁,锁默认在30s以后自动s
System.out.println("现在时间:" + sdf.format(date));
try {
//执行业务
System.out.println("加锁成功,执行业务,线程Id:"+Thread.currentThread().getId());
Thread.sleep(10000);//等待10秒
} catch (InterruptedException e) {
throw new RuntimeException(e);
} finally {
//解锁
System.out.println("释放锁,线程Id:"+Thread.currentThread().getId());
lock.unlock();
}
return "hello";
}

image-20230714181620263

my-lock对应的值会跟随线程的变化而变化

image-20230714182057982

读写锁

/**
* 写锁没有释放的情况下,读锁必须等待写锁的释放,才能进行读的操作
* 读写锁的测试
* 写锁
*/
@ResponseBody
@GetMapping("/write")
public String writeValue(){
RReadWriteLock lock = redissonClient.getReadWriteLock("rw-lock");
String s = "";
RLock rLock = lock.writeLock();//写锁
try {
rLock.lock();
s = UUID.randomUUID().toString();
Thread.sleep(30000);
//写的操作
redisTemplate.opsForValue().set("writeValue",s);
} catch (InterruptedException e) {
e.printStackTrace();
}finally {
rLock.unlock();
}
return s;
}

/**
* 读写锁的测试
* 读锁
*/
@GetMapping("/read")
@ResponseBody
public String readValue(){
RReadWriteLock lock = redissonClient.getReadWriteLock("rw-lock");
String s = "";
RLock rLock = lock.readLock();//读锁
try {
rLock.lock();
//读的操作
s = redisTemplate.opsForValue().get("writeValue");
} catch (Exception e) {
e.printStackTrace();
}finally {
rLock.unlock();
}
return s;
}

2.5.5 分布式缓存一致性问题

双写模式

image-20230716100845389

失效模式

image-20230716100909465

缓存数据一致性-解决方案

image-20230716101939869

缓存数据一致性-解决-Canal

image-20230716102510738

2.5.6 缓存-SpringCache

1、简介

  • Spring 从 3.1 开始定义了 org.springframework.cache.Cache和 org.springframework.cache.CacheManager 接口来统一不同的缓存技术;并支持使用 JCache(JSR-107)注解简化我们开发;

  • Cache 接口为缓存的组件规范定义,包含缓存的各种操作集合;
    Cache 接 口 下 Spring 提 供 了 各 种 xxxCache 的 实 现 ; 如 RedisCache , EhCacheCache ,ConcurrentMapCache 等;

  • 每次调用需要缓存功能的方法时,Spring 会检查检查指定参数的指定的目标方法是否已经被调用过;如果有就直接从缓存中获取方法调用后的结果,如果没有就调用方法并缓存结果后返回给用户。下次调用直接从缓存中获取。

  • 使用 Spring 缓存抽象时我们需要关注以下两点;

    1、确定方法需要被缓存以及他们的缓存策略

    2、从缓存中读取之前缓存存储的数据

2.以前的笔记教程

SpringBoot整合Redis | The Blog (gitee.io)

3.项目中整合SpringCache简化开发

3.1.引入依赖

<!--我们使redis作为缓存的使用场景,在前面我们还要引入redis的依赖-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-cache</artifactId>
</dependency>

3.2.编写配置文件

#使用缓存的类型
spring.cache.type=redis

3.3.测试使用缓存

常用的缓存注解

@Cacheable 触发将数据保存到缓存的操作

@CachePut 不影响方法执行更新缓存

@CacheEvict 触发将数据从缓存中删除的操作

@Caching 组合多个缓存操作

@CacheConfig 在类级别上共享缓存的相关配置

1.在启动类上添加缓存的注解

@EnableCaching //开启缓存功能

2.测试使用

//每一个需要缓存的数据需要我们指定放到那个名字的缓存[缓存的分区(按照业务类型分)]
@Cacheable({"category"}) //代表当前方法的结果需要缓存,如果缓存中有,方法就不需要调用了,如果缓存中没有,就会调用方法,将方法的结果放入到缓存
@Override
public List<CategoryEntity> getLevelOneCategorys() {
return categoryDao.selectList(new LambdaQueryWrapper<CategoryEntity>().eq(CategoryEntity::getParentCid, 0));
}

image-20230716112105848

自定义数据的存储

设置自定义的key值

@Cacheable(value = {"category"},key = "'levelOneCategorys'")  //自定义key值

tips: #root.method.name是SpEL表达式

@Cacheable(value = {"category"},key = "#root.method.name")   //使用方法名作为key值

设置缓存数据的存活时间

#设置一个小时的存活时间
spring.cache.redis.time-to-live=3600000

image-20230716152821097

将数据保存为json的格式

添加配置类

package com.atguigu.gulimall.product.config;

import org.springframework.boot.autoconfigure.cache.CacheProperties;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.cache.RedisCacheConfiguration;
import org.springframework.data.redis.serializer.GenericJackson2JsonRedisSerializer;
import org.springframework.data.redis.serializer.RedisSerializationContext;
import org.springframework.data.redis.serializer.StringRedisSerializer;

/**
* @author Jason Gong
* @version 1.0
* @website https://jasonsgong.gitee.io
* @Date 2023/7/16
* @Description 缓存的序列化的配置
*/
@EnableConfigurationProperties(CacheProperties.class)
@Configuration
public class MyCacheConfig {
@Bean
RedisCacheConfiguration redisCacheConfiguration(CacheProperties cacheProperties) {
RedisCacheConfiguration config = RedisCacheConfiguration.defaultCacheConfig();
config = config.serializeKeysWith(RedisSerializationContext.SerializationPair.fromSerializer(new StringRedisSerializer()));
config = config.serializeValuesWith(RedisSerializationContext.SerializationPair.fromSerializer(new GenericJackson2JsonRedisSerializer()));
//让配置文件中所有的配置生效(下面的不配置的话,缓存的时间会失效)
CacheProperties.Redis redisProperties = cacheProperties.getRedis();
if (redisProperties.getTimeToLive() != null) {
config = config.entryTtl(redisProperties.getTimeToLive());
}
if (redisProperties.getKeyPrefix() != null) {
config = config.prefixCacheNameWith(redisProperties.getKeyPrefix());
}
if (!redisProperties.isCacheNullValues()) {
config = config.disableCachingNullValues();
}
if (!redisProperties.isUseKeyPrefix()) {
config = config.disableKeyPrefix();
}
return config;
}
}

效果

image-20230716160027391

其它的常用配置

#缓存的前缀,如果指定了前缀就使用指定的前缀,没有指定前缀就使用缓存的名字作为前缀
spring.cache.redis.key-prefix=CACHE_
#是否使用缓存的前缀
spring.cache.redis.use-key-prefix=true
#是否缓存空值(缓存穿透的问题的解决)
spring.cache.redis.cache-null-values=true

2.6 检索服务

2.6.1 整合页面

image-20230716173751202

2.6.2 配置域名访问

image-20230716173453604

Nginx的配置

server {
listen 80;
server_name *.gulimall.com;

location / {
proxy_set_header Host $host;
proxy_pass http://gulimall;
}

error_page 500 502 503 504 /50x.html;
location = /50x.html {
root /usr/share/nginx/html;
}

}

网关中配置断言

#搜索
- id: gulimall_search_route
uri: lb://gulimall-search
predicates:
- Host=search.gulimall.com

image-20230716173628024

2.6.3 首页和搜索页相互跳转

首页无法跳转到搜索页的原因,gumall改为gulimall即可

image-20230716181052580

搜索框跳转到搜索页,修改页面中的代码

image-20230717094656565

2.6.4 商品的检索

image-20230717095740594

检索条件的VO

package com.atguigu.gulimall.search.vo;

import lombok.Data;

import java.util.List;

/**
* @author Jason Gong
* @version 1.0
* @website https://jasonsgong.gitee.io
* @Date 2023/7/17
* @Description 检索条件的vo
*/
@Data
public class SearchParam {
/**
* 页面传递过来的全文匹配关键字
*/
private String keyword;

/**
* 品牌id,可以多选
*/
private List<Long> brandId;

/**
* 三级分类id
*/
private Long catalog3Id;

/**
* 排序条件:sort=price/salecount/hotscore_desc/asc
*/
private String sort;

/**
* 是否显示有货
*/
private Integer hasStock;

/**
* 价格区间查询
*/
private String skuPrice;

/**
* 按照属性进行筛选
*/
private List<String> attrs;

/**
* 页码
*/
private Integer pageNum = 1;

/**
* 原生的所有查询条件
*/
private String _queryString;
}

检索的结果

package com.atguigu.gulimall.search.vo;

import com.atguigu.common.to.es.SkuEsModel;
import lombok.Data;

import java.util.ArrayList;
import java.util.List;

/**
* @author Jason Gong
* @version 1.0
* @website https://jasonsgong.gitee.io
* @Date 2023/7/17
* @Description 根据检索条件返回给页面的商品信息
*/
@Data
public class SearchResult {


/**
* 查询到的所有商品信息
*/
private List<SkuEsModel> product;


/**
* 当前页码
*/
private Integer pageNum;

/**
* 总记录数
*/
private Long total;

/**
* 总页码
*/
private Integer totalPages;

private List<Integer> pageNavs;

/**
* 当前查询到的结果,所有涉及到的品牌
*/
private List<BrandVo> brands;

/**
* 当前查询到的结果,所有涉及到的所有属性
*/
private List<AttrVo> attrs;

/**
* 当前查询到的结果,所有涉及到的所有分类
*/
private List<CatalogVo> catalogs;

private List<NavVo> navs;

@Data
public static class NavVo {
private String navName;
private String navValue;
private String link;
}


@Data
public static class BrandVo {

private Long brandId;

private String brandName;

private String brandImg;
}


@Data
public static class AttrVo {

private Long attrId;

private String attrName;

private List<String> attrValue;
}


@Data
public static class CatalogVo {

private Long catalogId;

private String catalogName;
}
}

未完待续(学习到177)……

]]>
后端 项目实战