一、核心功能
IService的批量新增
批处理方案:
普通for循环逐条插入速度极差,不推荐
MP的批量新增,基于预编译的批处理,性能不错
配置jdbc参数,开rewriteBatchedStatements,性能最好
二、扩展功能
(1)代码生成
将实体类,Mapper,Service及实现类的代码自动生成。
在idea中安装一个插件MyBatisPlus,连接好数据库。
(2)静态工具(DB)
Service的相互依赖。
①根据id查地址
@Override
public UserVO queryUSerAndAddressById(Long id) {
// 1.查询用户
User user = getById(id);
if(user==null||user.getStatus()==2){
throw new RuntimeException("用户状态异常");
}
// 2.查询地址
List<Address> addresses = Db.lambdaQuery(Address.class).eq(Address::getUserId, id).list();
// 3.转User为UserVO
UserVO userVO = BeanUtil.copyProperties(user, UserVO.class);
if (CollUtil.isNotEmpty(addresses)){
userVO.setAddress(BeanUtil.copyToList(addresses, AddressVO.class));
}
return userVO;
}
②根据id批量查询地址
@Override
public List<UserVO> queryUSerAndAddressByIds(List<Long> ids) {
// 1.查询用户
List<User> users = listByIds(ids);
if (BeanUtil.isEmpty(users)){
return Collections.emptyList();
}
// 2.查询地址
List<Long> userIds = users.stream().map(User::getId).collect(Collectors.toList());
List<Address> addresses = Db.lambdaQuery(Address.class).in(Address::getUserId, ids).list();
// 3.转换地址VO
List<AddressVO> addressVOList = BeanUtil.copyToList(addresses, AddressVO.class);
// 4.梳理地址集合
Map<Long, List<AddressVO>> addressMap=new HashMap<>(0);
if (CollUtil.isNotEmpty(addressVOList)) {
addressMap = addressVOList.stream().collect(Collectors.groupingBy(AddressVO::getUserId));
}
List<UserVO> list=new ArrayList<>(users.size());
for (User user:users) {
UserVO vo = BeanUtil.copyProperties(user, UserVO.class);
list.add(vo);
vo.setAddress(addressMap.get(user.getId()));
}
return list;
}
(3)逻辑删除
MyBatisPlus提供了逻辑删除的功能,无需改变方法调用的方式,而是在底层帮我们自动修改CRUD的语句。我们要做的就是在application.yaml文件中配置逻辑删除的字段名和值即可。
mybatis-plus:
global-config:
db-config:
logic-delete-field: deleted
logic-delete-value: 1
logic-not-delete-value: 0
逻辑删除本身也有自己的问题,
比如: 会导致数据库表垃圾数据越来越多,影响查询效率
SQL中全都需要对逻辑删除字段做判断,影响查询效率
因此,我不太推荐采用逻辑删除功能,如果数据不能删除,可以采用把数据迁移到其它表的办法。
(4)枚举处理器
实现PO类中的枚举类型变量与数据库字段的转换:
①定义枚举,给枚举中的与数据库对应Value值添加@EnumValue注解
@Getter
public enum UserStatus {
NORMAL(1, "正常"),
FROZEN(2, "冻结"),
;
@EnumValue
private final int value;
@JsonValue(枚举再给前端返回的时候默认返回的是枚举项的名字,而你可以定义返回value,还是desc,只需将@JsonValue放在所要返回的变量上面)
private final String desc;
UserStatus(int value, String desc) {
this.value = value;
this.desc = desc;
}
②在配置文件中配置统一的枚举处理器,实现类型转换
mybatis-plus:
global-config:
db-config:
logic-delete-field: deleted
logic-delete-value: 1
logic-not-delete-value: 0
configuration:
default-enum-type-handler: com.baomidou.mybatisplus.core.handlers.MybatisEnumTypeHandler
(5)Json处理器
给Json对应的Java中的字段,定义一个实体,在实体上加上注解,且在该Java字段上加一个注解@TableField,指定类型处理器,再在这个实体类上加注解@TableName(autoResultMap=true)
@TableName(value = "tb_user",autoResultMap = true)
public class User {
@TableField(typeHandler = JacksonTypeHandler.class)
private UserInfo info;
三、插件功能
分页插件
①首先在配置类中注册MyBatisPlus的核心插件,同时添加分页插件。
@Configuration
public class MyBatisConfig {
@Bean
public MybatisPlusInterceptor mybatisPlusInterceptor(){
MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor();
// 创建分页插件
PaginationInnerInterceptor paginationInnerInterceptor = new PaginationInnerInterceptor(DbType.MYSQL);
paginationInnerInterceptor.setMaxLimit(1000L);
// 添加分页插件
interceptor.addInnerInterceptor(paginationInnerInterceptor);
return interceptor;
}
}
②使用分页插件
@Test
void testPageQuery(){
// 1.准备分页条件
int pageNumber=1,pageSize=2;
Page<User> page = Page.of(pageNumber, pageSize);
page.addOrder(new OrderItem("balance",true));
userService.page(page);
long total = page.getTotal();
System.out.println("total:"+total);
long pages = page.getPages();
System.out.println("pages:"+pages);
List<User> users = page.getRecords();
users.forEach(System.out::println);
}
需要封装统一的查询条件和返回值。
③通用分页实体
在PageQuery中定义方法,将PageQuery对象转为MyBatisPlus中的Page对象
public<T> Page<T> toMpPage(OrderItem ... items){
Page<T> page = Page.of(pageNo,pageSize);
if (StrUtil.isNotEmpty(sortBy)){
page.addOrder(new OrderItem(sortBy,isAsc));
}else if (items!=null){
page.addOrder(items);
}
return page;
}
public<T> Page<T> toMpPageDefaultSortByCreateTime(){
return toMpPage(new OrderItem("create_time",false));
}
public<T> Page<T> toMpPageDefaultSortByUpdateTime(){
return toMpPage(new OrderItem("update_time",false));
}
在PageDTO中定义方法,将MyBatisPlus中的Page结果转为PageDTO结果
public static<PO,VO> PageDTO<VO> of(Page<PO> p,Class<VO> clazz){
PageDTO<VO> dto=new PageDTO<>();
dto.setTotal(p.getTotal());
dto.setPages(p.getPages());
List<PO> records = p.getRecords();
if (CollUtil.isEmpty(records)){
dto.setList(Collections.emptyList());
return dto;
}
dto.setList(BeanUtil.copyToList(records, clazz));
return dto;
}
public static<PO,VO> PageDTO<VO> of(Page<PO> p, Function<PO,VO> convertor){
PageDTO<VO> dto=new PageDTO<>();
dto.setTotal(p.getTotal());
dto.setPages(p.getPages());
List<PO> records = p.getRecords();
if (CollUtil.isEmpty(records)){
dto.setList(Collections.emptyList());
return dto;
}
dto.setList(records.stream().map(convertor).collect(Collectors.toList()));
return dto;
}
四、Docker
(1)命令解读
docker run -d \
-p 3306:3306 --privileged=true \
-v /mysql/log:/var/log/mysql \
-v /mysql/data:/var/lib/mysql \
-v /mysql/conf:/etc/mysql/conf.d \
-e MYSQL_ROOT_PASSWORD=123456 \
--name mysql1 \
mysql:8.0.25
docker run:创建并运行一个容器,-d是让容器在后台运行。
-p 3306:3306 :设置端口映射,前者是宿主机端口,后者为mysql容器的端口。
-e MYSQL_ROOT_PASSWORD=123456 :是设置环境变量。(-e key=value)
-name mysql1:给容器取个名字,必须唯一。mysql:8.0.25:镜像名称,一般包括两部分:镜像名:镜像的版本。如果没写版本,则为最新版本的镜像。
(2)Docker基础
nginx是十分轻量级的HTTP服务器,还是反向代理服务器。
①常见命令
docker ps:查看进程
docker pull 镜像名:拉取镜像
docker images:查看所有镜像
docker save -o nginx.tar nginx:latest:保存镜像
docker rmi 镜像名:镜像版本:删除镜像
docker load -i 文件名:镜像被删除后,可以从本地在下载回来。
docker run -d -name nginx -p 80:80 nginx:创建并运行一个容器
docker stop 容器名:停掉容器
docker start 容器名: 开启容器
docker logs 容器名:开启容器日志
docker exec -it nginx bash:进入容器
exit:退出容器
# 修改/root/.bashrc文件
vi /root/.bashrc
内容如下:
# .bashrc
# User specific aliases and functions
alias rm='rm -i'
alias cp='cp -i'
alias mv='mv -i'
alias dps='docker ps --format "table {{.ID}}\t{{.Image}}\t{{.Ports}}\t{{.Status}}\t{{.Names}}"'
alias dis='docker images'
# Source global definitions
if [ -f /etc/bashrc ]; then
. /etc/bashrc
fi
别名命令生效:
source /root/.bashrc
②数据卷
数据卷(volume)是一个虚拟目录,是容器内目录与主机目录之间映射的桥梁。
i.利用数据卷部署静态资源
docker volume create 创建数据卷
docker volume ls 查看所有数据卷
docker volume inspect 查看某个数据卷的详情
docker volume prune 清除数据卷
docker volume rm 删除指定数据卷
在执行docker run命令时,使用-v 数据卷:容器内目录 可以完成数据卷挂载。
当创建容器时,如果挂载了数据卷且数据卷不存在,会自动创建数据卷。
vi 要修改的文件:在宿主机目录修改文件。
Mounts:挂载
ii.mysql容器的数据挂载
在执行docker run命令时,使用-v 本地目录:容器目录 ,可以完成本地目录的挂载。
本地目录必须以“/”或“./"开头,如果直接以名称开头,会被识别为数据卷而非本地目录。
-v mysql : /var/lib/mysql 会被识别为一个数据卷叫mysql
-v ./mysql : /var/lib/mysql 会被识别为当前目录下的mysql目录
③自定义镜像
Dockerfile的语法:
Dockerfile就是一个文本文件,其中包括一个个的指令,用指令来说明要执行什么操作来构建镜像。常见指令:
From :指定基础镜像
ENV:设置环境变量,可在后面指令使用
COPY:拷贝本地文件到镜像的指定目录
RUN:执行Linux的shell命令,一般是安装过程的命令
EXPOSE:指定容器运行时监听的端口,是给镜像使用者看的
ENTRYPOINT:镜像中应用的启动命令,容器运行时调用
镜像就是包含了应用程序、程序运行的系统函数库、运行配置等文件的文件包。构建镜像的过程其实就是把上述文件打包的过程。
镜像的结构:
镜像中包含了应用程序所需要的运行环境、函数库、配置、以及应用本身等各种文件,这些文件分层打包而成。
先下载jdk.tar:
docker load -i jdk.tar
构建镜像的命令:
docker build -t 镜像名 Dockerfile目录
最后创建并启动一个容器:
docker run -d --name
④网络
加入自定义网络的容器才可以通过容器名互相访问
docker network create 创建一个网络
docker network ls 查看所有网络
docker network rm 删除指定网络
docker network prune 清除未使用的网络
docker network connect 使指定容器连接加入某网络
docker network disconnect 使指定容器连接离开某网络
docker network inspect 查看网络详情
(3)项目部署
DockerCompose通过一个单独的docker-compose.yml模板文件,来定义一组相关联的应用容器,
帮助我们实现多个相互关联的Dockers容器的快速部署。
docker compose的指令如下:
-f:指定compose文件的路径和名称
-p:指定project名称
up:创建并启动所有service容器
down:停止并移除所有容器、网络
logs:查看指定容器的日志
ps:列出所有启动的容器
stop:停止容器
start:启动容器
restart:重启容器
top:查看运行的进程
exec:在指定的运行中容器中执行命令
五、微服务
(1)单体架构
单体架构:将业务的所有功能集中在一个项目中开发,打成一个包部署。
优点:架构简单,部署成本低
缺点:团队协作成本高,系统发布效率低,系统可用性差。
(2)微服务
微服务是一种软件架构风格,它是以专注于单一职责的很多小型为基础,组合出复杂的大型应用。
①微服务架构
微服务架构,是微服务思想指导下的一套最佳实践架构方案。服务化,就是把单体架构中的功能模块拆分为多个独立项目。
粒度小,团队自治,服务自治
②springcloud
springcloud集成了各种微服务功能组件,并基于Spring boot实现了这些组件的自动装配,从而提供了良好的开箱即用体验。
③微服务拆分
i.拆分原则
是么时候拆分?
创业型项目:先采用单体架构,快速开发,快速试错。随着规模扩大,逐渐拆分。
确定的大型项目:资金充足,目标明确,可以直接选择微服务架构,避免后续拆分的麻烦。
怎么拆分?
从拆分目标来说,要做到:
高内聚:每个微服务的职责要尽量单一,包含的业务相互关联度高,完整度高。
低耦合:每个微服务的功能要相对独立,尽量减少对其他微服务的依赖。
从拆分方式来说,一般包含两种方式:
纵向拆分:按照业务模块拆分
横向拆分:抽取公共服务,提高复用性。
ii.微服务项目工程结构有两种:
独立project
Maven聚合
微服务的拆分步骤:
将单个的服务的数据存入一个独立的数据库,创建一个新的模块,导入相关的依赖,配置,从单体架构中拆分出单个的服务,分别有各自的domain,mapper,service,impl,controller,运行spring boot时要将active profiles改为local
④远程调用
将微服务拆分后,会出现某些数据在不同服务,无法直接调用本地方法查询数据的问题,
此时可以利用RestTemplate发送Http请求,实现远程调用。
使用方法:
i.将其注入Spring容器中
@SpringBootApplication
public class CartApplication {
public static void main(String[] args) {
SpringApplication.run(CartApplication.class, args);
}
@Bean
public RestTemplate restTemplate(){
return new RestTemplate();
}
ii.发起远程调用
private void handleCartItems(List<CartVO> vos) {
// 1.获取商品id
Set<Long> itemIds = vos.stream().map(CartVO::getItemId).collect(Collectors.toSet());
// 2.查询商品
// List<ItemDTO> items = itemService.queryItemByIds(itemIds);
// 利用restTemplate发起Http请求,得到Http的响应
ResponseEntity<List<ItemDTO>> response = restTemplate.exchange(
"http://localhost:8081/items?ids={ids}",
HttpMethod.GET,
null,
new ParameterizedTypeReference<List<ItemDTO>>() {
},
Map.of("ids", CollUtil.join(itemIds, ","))
);
// 解析响应
if (!response.getStatusCode().is2xxSuccessful()){
// 查询失败,直接结束
return;
}
List<ItemDTO> items = response.getBody();
if (CollUtils.isEmpty(items)) {
return;
}
(3)服务治理
①注册中心的原理
i.服务治理中的三个角色:
服务提供者:暴露服务接口,供其他服务调用。
服务调用者:调用其他服务提供的接口
注册中心:记录并监测微服务个实例状态,推送服务变更信息
ii.注册中心的原理:
iii.消费者如何知道提供者的地址:
服务提供者会在启动时注册自己的信息到注册中心,
消费者可以从注册中心订阅和拉取服务信息。
iiii. 消费者如何得知服务状态变更:
服务提供者通过心跳机制向注册中心报告自己的健康状态,当心跳异常时注册中心会将异常服务剔除,并通知订阅了该服务的消费者。
iiiii.当提供者有多个实例时,消费者该选择哪一个?
消费者可以通过负载均衡算法,从多个实例中选择一个。
②服务注册
服务注册的步骤如下:
i.引入nacos discovery依赖
<!--nacos 服务注册发现-->
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
</dependency>
ii.配置nacos的地址
spring:
application:
name: item-service # 服务名称
cloud:
nacos:
server-addr: 192.168.150.101:8848 # nacos地址
③服务发现与负载均衡
消费者需要连接nacos以拉取和订阅服务,因此服务发现的前两步与服务注册是一样的,后面加上服务调用即可。
private void handleCartItems(List<CartVO> vos) {
// 1.获取商品id
Set<Long> itemIds = vos.stream().map(CartVO::getItemId).collect(Collectors.toSet());
// 2.查询商品
// List<ItemDTO> items = itemService.queryItemByIds(itemIds);
// 根据服务的名称获取服务的实例列表
List<ServiceInstance> instances = discoveryClient.getInstances("item-service");
if(CollUtil.isEmpty(instances)){
return;
}
// 手写负载均衡,从实例列表中挑选一个实例
ServiceInstance instance = instances.get(RandomUtil.randomInt(instances.size()));
// 利用restTemplate发起Http请求,得到Http的响应
ResponseEntity<List<ItemDTO>> response = restTemplate.exchange(
instance.getUri()+"/items?ids={ids}",
HttpMethod.GET,
null,
new ParameterizedTypeReference<List<ItemDTO>>() {
},
Map.of("ids", CollUtil.join(itemIds, ","))
);
(4)OpenFeign
OpenFeign是一个声明式的HTTP客户端,是SpringCloud在Eureka公司开源的Feign基础上改造而来。
其作用就是基于SpringMVC的常见注解,帮我们优雅的实现HTTP请求的发送。
①快速入门
OpenFeign已经被SpringCloud自动装配,实现起来非常简单:
i.引依赖,包括OpenFeign和负载均衡组件SpringCloudLoadBalancer
<!--openFeign-->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-openfeign</artifactId>
</dependency>
<!--负载均衡器-->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-loadbalancer</artifactId>
</dependency>
ii.通过@EnableFeignClient注解,开启OpenFeign功能
@EnableFeignClients
@MapperScan("com.hmall.cart.mapper")
@SpringBootApplication
public class CartApplication {
public static void main(String[] args) {
SpringApplication.run(CartApplication.class, args);
}
@Bean
public RestTemplate restTemplate(){
return new RestTemplate();
}
iii.编写FeignClient接口,包括服务名称,请求方式和路径,请求参数,返回值类型。
@FeignClient("item-service")
public interface ItemClient {
@GetMapping("/items")
List<ItemDTO> queryItemByIds(@RequestParam("ids") Collection<Long> ids);
}
iiii.使用FeignClient,实现远程调用。
// private final RestTemplate restTemplate;
// private final DiscoveryClient discoveryClient;
private final ItemClient itemClient;
List<ItemDTO> items = itemClient.queryItemByIds(itemIds);
②连接池
OpenFeign对Http请求做了优雅的伪装,不过其底层发起http请求,依赖于其它的框架。这些框架可以自己选择,包括以下三种:
HttpURLConnection:默认实现,不支持连接池
Apache HttpClient :支持连接池
OKHttp:支持连接池
Openfeign整合OKHttp的步骤如下:
i.引入依赖
<!--OK http 的依赖 -->
<dependency>
<groupId>io.github.openfeign</groupId>
<artifactId>feign-okhttp</artifactId>
</dependency>
ii.开启连接池功能(对Openfeign的性能进行调优)
feign:
okhttp:
enabled: true
③最佳实践
定义了统一的api模块,购物车模块要导入api模块的依赖。
当定义的feignclient不在SpringBootApplication的扫描包范围时,这些FeignClient无法使用。
现有两种解决方案:
方案一:指定FeignClient所在包
@EnableFeignClients(basePackages=“com.hmall.api.client”)
方案二:指定FeignClient字节码
@EnableFeignClients(clients = {UserClient.class})
④日志
OpenFeign只会在FeignClient所在包的日志级别为DEBUG时,才会输出日志。而且其日志级别有4级:
NONE:不记录任何日志信息,这是默认值。
BASIC:仅记录请求的方法,URL以及响应状态码和执行时间
HEADERS:在BASIC的基础上,额外记录了请求和响应的头信息
FULL:记录所有请求和响应的明细,包括头信息、请求体、元数据。
由于Feign默认的日志级别就是NONE,所以默认我们看不到请求日志。
如何配置OpenFeign输出日志的级别?
声明类型为Logger.Level的Bean
public class DefaultFeignConfig {
@Bean
public Logger.Level feignLoggerLevel(){
return Logger.Level.FULL;
}
在@FeignClient或EnableFeignClient注解上使用