开发者博客 – IT技术 尽在开发者博客

开发者博客 – 科技是第一生产力


  • 首页

  • 归档

  • 搜索

java版gRPC实战之五:双向流

发表于 2021-08-11

欢迎访问我的GitHub

github.com/zq2599/blog…

内容:所有原创文章分类汇总及配套源码,涉及Java、Docker、Kubernetes、DevOPS等;

本篇概览

  • 本文是《java版gRPC实战》系列的第五篇,目标是掌握双向流类型的服务,即请求参数是流的形式,响应的内容也是流的形式;
  • 先来看看官方资料对双向流式RPC的介绍:是双方使用读写流去发送一个消息序列。两个流独立操作,因此客户端和服务器 可以以任意喜欢的顺序读写:比如, 服务器可以在写入响应前等待接收所有的客户端消息,或者可以交替 的读取和写入消息,或者其他读写的组合。 每个流中的消息顺序被预留;
  • 掌握了客户端流和服务端流两种类型的开发后,双向流类型就很好理解了,就是之前两种类型的结合体,请求和响应都按照流的方式处理即可;
  • 今天的实战,咱们来设计一个在线商城的功能:批量减扣库存,即客户端提交多个商品和数量,服务端返回每个商品减扣库存成功和失败的情况;
  • 咱们尽快进入编码环节吧,具体内容如下:
  1. 在proto文件中定义双向流类型的gRPC接口,再通过proto生成java代码
  2. 开发服务端应用
  3. 开发客户端应用
  4. 验证

源码下载

  • 本篇实战中的完整源码可在GitHub下载到,地址和链接信息如下表所示(github.com/zq2599/blog…%EF%BC%9A)
名称 链接 备注
项目主页 github.com/zq2599/blog… 该项目在GitHub上的主页
git仓库地址(https) github.com/zq2599/blog… 该项目源码的仓库地址,https协议
git仓库地址(ssh) git@github.com:zq2599/blog_demos.git 该项目源码的仓库地址,ssh协议
  • 这个git项目中有多个文件夹,《java版gRPC实战》系列的源码在grpc-tutorials文件夹下,如下图红框所示:

在这里插入图片描述

  • grpc-tutorials文件夹下有多个目录,本篇文章对应的服务端代码在double-stream-server-side目录下,客户端代码在double-stream-client-side目录下,如下图:

在这里插入图片描述

在proto文件中定义双向流类型的gRPC接口

  • 首先要做的就是定义gRPC接口,打开mall.proto,在里面新增方法和相关的数据结构,需要重点关注的是BatchDeduct方法的入参ProductOrder和返回值DeductReply都添加了stream修饰(ProductOrder是上一章定义的),代表该方法是双向流类型:
1
2
3
4
5
6
7
8
9
10
11
12
13
javascript复制代码// gRPC服务,这是个在线商城的库存服务
service StockService {
// 双向流式:批量扣减库存
rpc BatchDeduct (stream ProductOrder) returns (stream DeductReply) {}
}

// 扣减库存返回结果的数据结构
message DeductReply {
// 返回码
int32 code = 1;
// 描述信息
string message = 2;
}
  • 双击下图红框中的task即可生成java代码:

在这里插入图片描述

  • 生成下图红框中的文件,即服务端定义和返回值数据结构:

在这里插入图片描述

  • 接下来开发服务端;

开发服务端应用

  • 在父工程grpc-turtorials下面新建名为double-stream-server-side的模块,其build.gradle内容如下:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
groovy复制代码// 使用springboot插件
plugins {
id 'org.springframework.boot'
}

dependencies {
implementation 'org.projectlombok:lombok'
implementation 'org.springframework.boot:spring-boot-starter'
// 作为gRPC服务提供方,需要用到此库
implementation 'net.devh:grpc-server-spring-boot-starter'
// 依赖自动生成源码的工程
implementation project(':grpc-lib')
// annotationProcessor不会传递,使用了lombok生成代码的模块,需要自己声明annotationProcessor
annotationProcessor 'org.projectlombok:lombok'
}
  • 配置文件application.yml:
1
2
3
4
5
6
7
yml复制代码spring:
application:
name: double-stream-server-side
# gRPC有关的配置,这里只需要配置服务端口号
grpc:
server:
port: 9901
  • 启动类DoubleStreamServerSideApplication.java的代码就不贴了,普通的springboot启动类而已;
  • 重点是提供grpc服务的GrpcServerService.java,咱们要做的就是给上层框架返回一个匿名类,至于里面的onNext、onCompleted方法何时被调用是上层框架决定的,另外还准备了成员变量totalCount,这样就可以记录总数了,由于请求参数是流,因此匿名类的onNext会被多次调用,并且由于返回值是流,因此onNext中调用了responseObserver.onNext方法来响应流中的每个请求,这样客户端就不断收到服务端的响应数据(即客户端的onNext方法会被多次调用):
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
java复制代码package grpctutorials;

import com.bolingcavalry.grpctutorials.lib.DeductReply;
import com.bolingcavalry.grpctutorials.lib.ProductOrder;
import com.bolingcavalry.grpctutorials.lib.StockServiceGrpc;
import io.grpc.stub.StreamObserver;
import lombok.extern.slf4j.Slf4j;
import net.devh.boot.grpc.server.service.GrpcService;

@GrpcService
@Slf4j
public class GrpcServerService extends StockServiceGrpc.StockServiceImplBase {

@Override
public StreamObserver<ProductOrder> batchDeduct(StreamObserver<DeductReply> responseObserver) {
// 返回匿名类,给上层框架使用
return new StreamObserver<ProductOrder>() {

private int totalCount = 0;

@Override
public void onNext(ProductOrder value) {
log.info("正在处理商品[{}],数量为[{}]",
value.getProductId(),
value.getNumber());

// 增加总量
totalCount += value.getNumber();

int code;
String message;

// 假设单数的都有库存不足的问题
if (0 == value.getNumber() % 2) {
code = 10000;
message = String.format("商品[%d]扣减库存数[%d]成功", value.getProductId(), value.getNumber());
} else {
code = 10001;
message = String.format("商品[%d]扣减库存数[%d]失败", value.getProductId(), value.getNumber());
}

responseObserver.onNext(DeductReply.newBuilder()
.setCode(code)
.setMessage(message)
.build());
}

@Override
public void onError(Throwable t) {
log.error("批量减扣库存异常", t);
}

@Override
public void onCompleted() {
log.info("批量减扣库存完成,共计[{}]件商品", totalCount);
responseObserver.onCompleted();
}
};
}
}

开发客户端应用

  • 在父工程grpc-turtorials下面新建名为double-stream-server-side的模块,其build.gradle内容如下:
1
2
3
4
5
6
7
8
9
10
11
groovy复制代码plugins {
id 'org.springframework.boot'
}

dependencies {
implementation 'org.projectlombok:lombok'
implementation 'org.springframework.boot:spring-boot-starter'
implementation 'org.springframework.boot:spring-boot-starter-web'
implementation 'net.devh:grpc-client-spring-boot-starter'
implementation project(':grpc-lib')
}
  • 配置文件application.yml,设置自己的web端口号和服务端地址:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
yml复制代码server:
port: 8082
spring:
application:
name: double-stream-client-side

grpc:
client:
# gRPC配置的名字,GrpcClient注解会用到
double-stream-server-side:
# gRPC服务端地址
address: 'static://127.0.0.1:9901'
enableKeepAlive: true
keepAliveWithoutCalls: true
negotiationType: plaintext
  • 启动类DoubleStreamClientSideApplication.java的代码就不贴了,普通的springboot启动类而已;
  • 正常情况下我们都是用StreamObserver处理服务端响应,这里由于是异步响应,需要额外的方法从StreamObserver中取出业务数据,于是定一个新接口,继承自StreamObserver,新增getExtra方法可以返回String对象,详细的用法稍后会看到:
1
2
3
4
5
6
7
java复制代码package com.bolingcavalry.grpctutorials;

import io.grpc.stub.StreamObserver;

public interface ExtendResponseObserver<T> extends StreamObserver<T> {
String getExtra();
}
  • 重头戏来了,看看如何远程调用双向流类型的gRPC接口,代码中已经添加详细注释:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
java复制代码package grpctutorials;

import com.bolingcavalry.grpctutorials.lib.DeductReply;
import com.bolingcavalry.grpctutorials.lib.ProductOrder;
import com.bolingcavalry.grpctutorials.lib.StockServiceGrpc;
import io.grpc.stub.StreamObserver;
import lombok.extern.slf4j.Slf4j;
import net.devh.boot.grpc.client.inject.GrpcClient;
import org.springframework.stereotype.Service;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.TimeUnit;

@Service
@Slf4j
public class GrpcClientService {

@GrpcClient("double-stream-server-side")
private StockServiceGrpc.StockServiceStub stockServiceStub;

/**
* 批量减库存
* @param count
* @return
*/
public String batchDeduct(int count) {

CountDownLatch countDownLatch = new CountDownLatch(1);

// responseObserver的onNext和onCompleted会在另一个线程中被执行,
// ExtendResponseObserver继承自StreamObserver
ExtendResponseObserver<DeductReply> responseObserver = new ExtendResponseObserver<DeductReply>() {

// 用stringBuilder保存所有来自服务端的响应
private StringBuilder stringBuilder = new StringBuilder();

@Override
public String getExtra() {
return stringBuilder.toString();
}

/**
* 客户端的流式请求期间,每一笔请求都会收到服务端的一个响应,
* 对应每个响应,这里的onNext方法都会被执行一次,入参是响应内容
* @param value
*/
@Override
public void onNext(DeductReply value) {
log.info("batch deduct on next");
// 放入匿名类的成员变量中
stringBuilder.append(String.format("返回码[%d],返回信息:%s<br>" , value.getCode(), value.getMessage()));
}

@Override
public void onError(Throwable t) {
log.error("batch deduct gRPC request error", t);
stringBuilder.append("batch deduct gRPC error, " + t.getMessage());
countDownLatch.countDown();
}

/**
* 服务端确认响应完成后,这里的onCompleted方法会被调用
*/
@Override
public void onCompleted() {
log.info("batch deduct on complete");
// 执行了countDown方法后,前面执行countDownLatch.await方法的线程就不再wait了,
// 会继续往下执行
countDownLatch.countDown();
}
};

// 远程调用,此时数据还没有给到服务端
StreamObserver<ProductOrder> requestObserver = stockServiceStub.batchDeduct(responseObserver);

for(int i=0; i<count; i++) {
// 每次执行onNext都会发送一笔数据到服务端,
// 服务端的onNext方法都会被执行一次
requestObserver.onNext(build(101 + i, 1 + i));
}

// 客户端告诉服务端:数据已经发完了
requestObserver.onCompleted();

try {
// 开始等待,如果服务端处理完成,那么responseObserver的onCompleted方法会在另一个线程被执行,
// 那里会执行countDownLatch的countDown方法,一但countDown被执行,下面的await就执行完毕了,
// await的超时时间设置为2秒
countDownLatch.await(2, TimeUnit.SECONDS);
} catch (InterruptedException e) {
log.error("countDownLatch await error", e);
}

log.info("service finish");
// 服务端返回的内容被放置在requestObserver中,从getExtra方法可以取得
return responseObserver.getExtra();
}

/**
* 创建ProductOrder对象
* @param productId
* @param num
* @return
*/
private static ProductOrder build(int productId, int num) {
return ProductOrder.newBuilder().setProductId(productId).setNumber(num).build();
}
}
  • 最后做个web接口,可以通过web请求验证远程调用:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
java复制代码package grpctutorials;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;

@RestController
public class GrpcClientController {

@Autowired
private GrpcClientService grpcClientService;

@RequestMapping("/")
public String printMessage(@RequestParam(defaultValue = "1") int count) {
return grpcClientService.batchDeduct(count);
}
}
  • 编码完成,开始验证;

验证

  • 启动服务端DoubleStreamServerSideApplication:

在这里插入图片描述

  • 启动客户端DoubleStreamClientSideApplication:

在这里插入图片描述

  • 这里要改:浏览器输入http://localhost:8083/?count=10,响应如下,可见远程调用gRPC服务成功,流式响应的每一笔返回都被客户端收到:

在这里插入图片描述

  • 下面是服务端日志,可见逐一处理了客户端的每一笔数据:

在这里插入图片描述

  • 下面是客户端日志,可见由于CountDownLatch的作用,发起gRPC请求的线程一直等待responseObserver.onCompleted在另一个线程被执行完后,才会继续执行:

在这里插入图片描述

  • 至此,四种类型的gRPC服务及其客户端开发就完成了,一般的业务场景咱们都能应付自如,接下来的文章咱们会继续深入学习,了解复杂场景下的gRPC操作;

你不孤单,欣宸原创一路相伴

  1. Java系列
  2. Spring系列
  3. Docker系列
  4. kubernetes系列
  5. 数据库+中间件系列
  6. DevOps系列

欢迎关注公众号:程序员欣宸

微信搜索「程序员欣宸」,我是欣宸,期待与您一同畅游Java世界…
github.com/zq2599/blog…

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

Spring GateWay 网关的转发细节

发表于 2021-08-10

这是我参与8月更文挑战的第4天,活动详情查看:8月更文挑战

首先分享之前的所有文章 , 欢迎点赞收藏转发三连下次一定 >>>> 😜😜😜

文章合集 : 🎁 juejin.cn/post/694164…

Github : 👉 github.com/black-ant

CASE 备份 : 👉 gitee.com/antblack/ca…

一 . 前言

文档目的

  • 梳理 Gateway 生产中转发请求的细节
  • 梳理 转发的定制点

知识补充

请求转发是 Gateway 最核心的功能之一 , 他涉及到三个主要的概念 :

Route(路由): 路由是网关的基本单元,由ID、URI、一组Predicate、一组Filter组成,如果 Predicate 匹配 True ,则进行转发

Predicate(谓语、断言): 路由转发的判断条件,这是一个 Java 8函数断言, 输入类型是 Spring Framework ServerWebExchange , 目前SpringCloud Gateway支持多种方式,常见如:Path、Query、Method、Header等,写法必须遵循 key=vlue的形式

Filter(过滤器): 过滤器是路由转发请求时所经过的过滤逻辑,使用特定工厂构建的 GatewayFilter 实例 , 可用于修改请求、响应内容

二 . 简单使用

2.1 predicates 汇总

1
2
3
4
5
6
7
8
9
10
11
java复制代码// 
- After=2017-01-20T17:42:47.789-07:00[America/Denver]

//
- Before=2017-01-20T17:42:47.789-07:00[America/Denver]

//
- Between=2017-01-20T17:42:47.789-07:00[America/Denver], 2017-01-21T17:42:47.789-07:00[America/Denver]

//
- Cookie=chocolate, ch.p

2.2 Mono 和 Flux

Mono 和 Flux 是贯穿了整个流程的核心对象 ,根据 reactive-streams 规范,发布服务器提供了数量可能无限的有序元素,并根据从其订阅服务器接收到的需求发布这些元素。Reactor-core 有一组此 Publisher 接口的实现。我们将要创建序列的两个重要实现是 Mono 和 Flux。

  • Flux 表示的是包含 0 到 N 个元素的异步序列
  • Mono 表示的是包含 0 或 1 个元素的异步序列

> SpringGateway 是使用 webflux 作为底层调用框架的 , 其中涉及到 mono 和 Flux 对象

> 该序列中可以包含 3 种通知 :

  • 正常的包含元素的消息
  • 序列结束的消息
  • 序列出错的消息

Flux

  • Flux是一个标准Publisher,表示0到N个发射项的异步序列,选择性的以完成或错误信号终止。与Reactive Streams规范中一样,这三种类型的信号转换为对下游订阅者的onNext、onComplete或onError方法的调用。

image.png

Mono

  • Mono 是 Publisher 的另一个实现。它最多发出一个条目,然后(可选)以 onComplete 信号或 onError 信号终止 , Mono 在本质上也是异步的
  • 它只提供了可用于Flux的操作符的子集,并且一些操作符(特别是那些将Mono与另一个发布者组合的操作符)切换到Flux。
    • 例如,Mono#concatWith(Publisher)返回一个Flux ,而Mono#then(Mono)则返回另一个Mono。

image.png

常见的方法如下 :

  • create : 以编程方式创建具有多次发射能力的Flux,
  • empty : 发出0元素或返回空 Flux < t >
  • just : 创建一个基础
  • error : 创建一个Flux,它在订阅之后立即以指定的错误终止

PS : 这一块就不深入看了 , 先看完 Gateway 的主流程

三 . 拦截深入

3.1 原理图

首先来看一下 SpringGateway 的原理图

GateWayAll.jpg

四 . 调用的入口

4.1 调用流程

  • Step 1 : HttpWebHandlerAdapter # handle : 构建 ServerWebExchange , 发起 Handler 处理
  • Step 2 : DispatcherHandler # handle : 发起请求处理
  • Step 3 : RoutePredicateHandlerMapping # getHandlerInternal : route 判断处理

4.2. getHandlerInternal 逻辑

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
java复制代码protected Mono<?> getHandlerInternal(ServerWebExchange exchange) {
// don't handle requests on management port if set and different than server port
if (this.managementPortType == DIFFERENT && this.managementPort != null
&& exchange.getRequest().getURI().getPort() == this.managementPort) {
return Mono.empty();
}
exchange.getAttributes().put(GATEWAY_HANDLER_MAPPER_ATTR, getSimpleName());

return lookupRoute(exchange)
// .log("route-predicate-handler-mapping", Level.FINER) //name this
.flatMap((Function<Route, Mono<?>>) r -> {
exchange.getAttributes().remove(GATEWAY_PREDICATE_ROUTE_ATTR);
if (logger.isDebugEnabled()) {
logger.debug("Mapping [" + getExchangeDesc(exchange) + "] to " + r);
}

exchange.getAttributes().put(GATEWAY_ROUTE_ATTR, r);
return Mono.just(webHandler);
}).switchIfEmpty(Mono.empty().then(Mono.fromRunnable(() -> {
exchange.getAttributes().remove(GATEWAY_PREDICATE_ROUTE_ATTR);
if (logger.isTraceEnabled()) {
logger.trace("No RouteDefinition found for [" + getExchangeDesc(exchange) + "]");
}
})));
}

3.2. lookupRoute

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
java复制代码protected Mono<Route> lookupRoute(ServerWebExchange exchange) {
return this.routeLocator.getRoutes()
// individually filter routes so that filterWhen error delaying is not a
// problem
.concatMap(route -> Mono.just(route).filterWhen(r -> {
// add the current route we are testing
exchange.getAttributes().put(GATEWAY_PREDICATE_ROUTE_ATTR, r.getId());
return r.getPredicate().apply(exchange);
})
// instead of immediately stopping main flux due to error, log and
// swallow it
.doOnError(e -> logger.error("Error applying predicate for route: " + route.getId(), e))
.onErrorResume(e -> Mono.empty()))
// .defaultIfEmpty() put a static Route not found
// or .switchIfEmpty()
// .switchIfEmpty(Mono.<Route>empty().log("noroute"))
.next()
// TODO: error handling
.map(route -> {
validateRoute(route, exchange);
return route;
});


}

会遍历所有的 route
route_001.png

五. 发送的流程

5.1 FilteringWebHandler 体系

此处的 webHandler 为 FilteringWebHandler 对象 , 来看一下这个对象的作用

image.png

这里涉及到以下的 Filter :

  • C- ForwardPathFilter :
  • C- ForwardRoutingFilter : 用来做本地forward的
  • C- GatewayMetricsFilter : 与 Prometheus 整合,从而创建一个 Grafana dashboard
  • C- LoadBalancerClientFilter : 用来整合Ribbon的 , 先获取微服务的名称,然后再通过Ribbon获取实际的调用地址
  • C- NettyRoutingFilter : http 或 https ,使用 Netty 的 HttpClient 向下游的服务发送代理请求
  • C- NettyWriteResponseFilter : 用于将代理响应写回网关的客户端侧,所以该过滤器会在所有其他过滤器执行完成后才执行
  • C- OrderedGatewayFilter :
  • C- RouteToRequestUrlFilter : 将从request里获取的 原始url转换成Gateway进行请求转发时所使用的url
  • C- WebClientHttpRoutingFilter :
  • C- WebClientWriteResponseFilter :
  • C- WebsocketRoutingFilter : ws 或者 wss,那么该Filter会使用 Spring Web Socket 将 Websocket 请求转发到下游
  • C- WeightCalculatorWebFilter :

可以参考 -> Spring Cloud Gateway 内置的全局过滤器

调用逻辑1 : FilteringWebHandler 管理

该对象中存在一个内部类 DefaultGatewayFilterChain , 该类为 Filter 过滤链

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
java复制代码private static class DefaultGatewayFilterChain implements GatewayFilterChain {

// 当前 Filter 链索引
private final int index;
// Filter 集合
private final List<GatewayFilter> filters;

DefaultGatewayFilterChain(List<GatewayFilter> filters) {
this.filters = filters;
this.index = 0;
}

private DefaultGatewayFilterChain(DefaultGatewayFilterChain parent, int index) {
this.filters = parent.getFilters();
this.index = index;
}

public List<GatewayFilter> getFilters() {
return filters;
}

@Override
public Mono<Void> filter(ServerWebExchange exchange) {
return Mono.defer(() -> {
if (this.index < filters.size()) {
// 逐个 Filter 过滤调用
GatewayFilter filter = filters.get(this.index);
DefaultGatewayFilterChain chain = new DefaultGatewayFilterChain(this,
this.index + 1);
return filter.filter(exchange, chain);
}
else {
return Mono.empty(); // complete
}
});
}

}

调用流程 3 : Filter 过滤

1
2
3
4
5
6
7
java复制代码public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {

// 通常判断部分条件 , 如果该 Filter 不符合 , 则跳过该 Filter
if (isAlreadyRouted(exchange)
|| (!"http".equals(scheme) && !"https".equals(scheme))) {
return chain.filter(exchange);
}

5.2 发送的主体

核心的发送 Filter 是 NettyRoutingFilter, 下面只关注这个 Filter 的相关逻辑 :

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
java复制代码C- NettyRoutingFilter
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
// 请求 URL : http://httpbin.org:80/get
URI requestUrl = exchange.getRequiredAttribute(GATEWAY_REQUEST_URL_ATTR);
// 协议类型 : http
String scheme = requestUrl.getScheme();

// Step 1 : filter 链处理 ,如果不符合 http 协议 , 就通过下一个 Filter 处理
if (isAlreadyRouted(exchange)
|| (!"http".equals(scheme) && !"https".equals(scheme))) {
return chain.filter(exchange);
}
// Step 2 : 标识 Routed 已处理
setAlreadyRouted(exchange);

// Step 3 : 获取 Request 请求对象 , 这个是外部请求的对象
ServerHttpRequest request = exchange.getRequest();

// Step 4 : 获取 Method 类型 (get/post...)
final HttpMethod method = HttpMethod.valueOf(request.getMethodValue());
final String url = requestUrl.toString();

// Step 5 : 对 Header 进行处理 , 需要转发过去
HttpHeaders filtered = filterRequest(getHeadersFilters(), exchange);
final DefaultHttpHeaders httpHeaders = new DefaultHttpHeaders();
filtered.forEach(httpHeaders::set);

// -> Transfer-Encoding
String transferEncoding = request.getHeaders().getFirst(HttpHeaders.TRANSFER_ENCODING);
boolean chunkedTransfer = "chunked".equalsIgnoreCase(transferEncoding);
// -> preserveHostHeader
boolean preserveHost = exchange.getAttributeOrDefault(PRESERVE_HOST_HEADER_ATTRIBUTE, false);

// 通过 netty httpClient 发起转发请求 , PS !!! 这里是异步的
Flux<HttpClientResponse> responseFlux = this.httpClient
.chunkedTransfer(chunkedTransfer).request(method).uri(url)
.send((req, nettyOutbound) -> {
// Step 6 : 转发 Header
req.headers(httpHeaders);

// => 是否需要记录之前的 host
if (preserveHost) {
String host = request.getHeaders().getFirst(HttpHeaders.HOST);
req.header(HttpHeaders.HOST, host);
}

// Step 7 : 真正发起请求
return nettyOutbound.options(NettyPipeline.SendOptions::flushOnEach)
.send(request.getBody()
.map(dataBuffer -> ((NettyDataBuffer) dataBuffer)
.getNativeBuffer()));
}).responseConnection((res, connection) -> {
// Step 8 : 请求完成 , 获取 response
ServerHttpResponse response = exchange.getResponse();

// Step 9 : 转发headers 和 status 等属性
HttpHeaders headers = new HttpHeaders();
res.responseHeaders().forEach(
entry -> headers.add(entry.getKey(), entry.getValue()));

// => String CONTENT_TYPE = "Content-Type"
// => String ORIGINAL_RESPONSE_CONTENT_TYPE_ATTR = "original_response_content_type";
String contentTypeValue = headers.getFirst(HttpHeaders.CONTENT_TYPE);
if (StringUtils.hasLength(contentTypeValue)) {
exchange.getAttributes().put(ORIGINAL_RESPONSE_CONTENT_TYPE_ATTR,
contentTypeValue);
}

// 转发状态 , 存在往 GatewayResponse 设置状态
HttpStatus status = HttpStatus.resolve(res.status().code());
if (status != null) {
response.setStatusCode(status);
}
else if (response instanceof AbstractServerHttpResponse) {
((AbstractServerHttpResponse) response)
.setStatusCodeValue(res.status().code());
}
else {
throw new IllegalStateException(
"Unable to set status code on response: "
+ res.status().code() + ", "
+ response.getClass());
}

// 确保 Header filter 在设置状态后运行, 校验 header 中 filter 正常
HttpHeaders filteredResponseHeaders = HttpHeadersFilter.filter(
getHeadersFilters(), headers, exchange, Type.RESPONSE);

// String TRANSFER_ENCODING = "Transfer-Encoding"
// String CONTENT_LENGTH = "Content-Length"
if (!filteredResponseHeaders
.containsKey(HttpHeaders.TRANSFER_ENCODING)
&& filteredResponseHeaders
.containsKey(HttpHeaders.CONTENT_LENGTH)) {
// content-length 存在需要去掉 Transfer-Encoding
response.getHeaders().remove(HttpHeaders.TRANSFER_ENCODING);
}

exchange.getAttributes().put(CLIENT_RESPONSE_HEADER_NAMES,
filteredResponseHeaders.keySet());

response.getHeaders().putAll(filteredResponseHeaders);

// 延迟提交响应,直到所有路由过滤器都运行
// 将客户端响应作为ServerWebExchange属性,稍后写入响应NettyWriteResponseFilter
exchange.getAttributes().put(CLIENT_RESPONSE_ATTR, res);
exchange.getAttributes().put(CLIENT_RESPONSE_CONN_ATTR, connection);

return Mono.just(res);
});

if (properties.getResponseTimeout() != null) {
// 超时异常处理
responseFlux = responseFlux.timeout(properties.getResponseTimeout(),
Mono.error(new TimeoutException("Response took longer than timeout: "
+ properties.getResponseTimeout())))
.onErrorMap(TimeoutException.class,
// GATEWAY_TIMEOUT(504, "Gateway Timeout")
th -> new ResponseStatusException(HttpStatus.GATEWAY_TIMEOUT,
th.getMessage(), th));
}

return responseFlux.then(chain.filter(exchange));
}

5.3 返回 Response

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
java复制代码C- NettyWriteResponseFilter
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
return chain.filter(exchange).then(Mono.defer(() -> {
// Step 1 : 获取 GatewayRequest
Connection connection = exchange.getAttribute(CLIENT_RESPONSE_CONN_ATTR);
// 连接不存在直接返回空
if (connection == null) {
return Mono.empty();
}

// Step 2 : 获取 GatewayResponse
ServerHttpResponse response = exchange.getResponse();
NettyDataBufferFactory factory = (NettyDataBufferFactory) response
.bufferFactory();

// 此处主要包含一个 byteBufflux
final Flux<NettyDataBuffer> body = connection.inbound().receive().retain()
.map(factory::wrap);

// 媒体类型
MediaType contentType = null;
try {
contentType = response.getHeaders().getContentType();
}
catch (Exception e) {
log.trace("invalid media type", e);
}
return (isStreamingMediaType(contentType)
? response.writeAndFlushWith(body.map(Flux::just))
: response.writeWith(body));
}));
}

总结

由于 netty 的底层了解的还不是很清楚 , 对于一些调用过程没办法输出数据看 , 这篇文章心里也不是很有底 , 后续深入后再来补充细节

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

SpringBoot 日志配置(logback)

发表于 2021-08-10

​这是我参与8月更文挑战的第10天,活动详情查看:8月更文挑战

​SpringBoot支持Java Util Logging,Log4J,Log4J2和Logback日志框架,默认采用logback日志。在实际SpringBoot项目中使用SpringBoot默认日志配置是不能够满足实际生产及开发需求的,需要选定适合的日志输出框架,灵活调整日志输出级别、日志输出格式等。本章主要讲述如何进行SpringBoot项目的日志详细配置。

(强烈建议使用Logback日志配置,因为它比log4j性能好多很多。)

1、添加日志依赖包

SpringBoot项目中依赖包spring-boot-starter中已经包含spring-boot-starter-logging,该依赖包就是默认的logback日志框架,则不需额外引入。

))​

2、添加logback.xml

在/springboot/src/main/resources目录下,新建日志配置文件logback.xml,如下:

(配置说明见注释)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
xml复制代码<?xml version="1.0" encoding="UTF-8" ?>
<!--
scan:当此属性设置为true时,配置文件如果发生改变,将会被重新加载,默认值为true
scanPeriod:设置监测配置文件是否有修改的时间间隔,如果没有给出时间单位默认单位是毫秒,当scan为true时此属性生效,默认时间间隔为1分钟
debug:当此属性设置为true时,将打印出logback内部日志信息,实时查看logback运行状态,默认值为false
-->
<configuration scan="true" scanPeriod="2 seconds">
<!--
定义滚动记录文件appender 作用:滚动记录文件,先将日志记录到指定文件,当符合某个条件时,将日志记录到其他文件
RollingFileAppender class="ch.qos.logback.core.rolling.RollingFileAppender"
参数:
<append>:如果是true日志被追加到文件结尾,如果是false清空现存文件,默认是true
<file>:被写入的文件名,可以是相对目录也可以是绝对目录,如果上级目录不存在会自动创建,没有默认值
<rollingPolicy>:当发生滚动时,决定RollingFileAppender的行为,涉及文件移动和重命名
<triggeringPolicy>:告知RollingFileAppender合适激活滚动
<prudent>:当为true时不支持FixedWindowRollingPolicy支持TimeBasedRollingPolicy,但是有两个限制:1不支持也不允许文件压缩,2不能设置file属性必须留空
-->
<appender name="fileAppender" class="ch.qos.logback.core.rolling.RollingFileAppender">
<!-- 如果是true,日志被追加到文件结尾,如果是false,清空现存文件.默认是true -->
<prudent>true</prudent>
<rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
<!-- 每天滚动一次的日志 只保留30天内的日志文件 -->
<fileNamePattern>logs/%d{yyyy-MM-dd}/springboot_%i.log</fileNamePattern>
<maxHistory>30</maxHistory>
<timeBasedFileNamingAndTriggeringPolicy class="ch.qos.logback.core.rolling.SizeAndTimeBasedFNATP">
<maxFileSize>10MB</maxFileSize>
</timeBasedFileNamingAndTriggeringPolicy>
</rollingPolicy>
<encoder class="ch.qos.logback.classic.encoder.PatternLayoutEncoder">
<!-- 对日志进行格式化 -->
<pattern>%date %level [%thread] %logger{10}.%class{0}#%method[%file:%line] %n%msg%n</pattern>
<charset>utf-8</charset>
</encoder>
</appender>

<appender name="errorAppender" class="ch.qos.logback.core.rolling.RollingFileAppender">
<prudent>true</prudent>
<rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
<fileNamePattern>logs/%d{yyyy-MM-dd}/springboot-error_%i.log</fileNamePattern>
<maxHistory>30</maxHistory>
<timeBasedFileNamingAndTriggeringPolicy class="ch.qos.logback.core.rolling.SizeAndTimeBasedFNATP">
<maxFileSize>10MB</maxFileSize>
</timeBasedFileNamingAndTriggeringPolicy>
</rollingPolicy>
<encoder>
<pattern>%date %level [%thread] %logger{10}.%class{0}#%method[%file:%line] %n%msg%n</pattern>
<charset>utf-8</charset>
</encoder>
<!--
配置日志级别过滤器 作用:根据日志级别进行过滤,如果日志级别等于配置级别过滤器会根据onMath和onMismatch接收或拒绝日志
参数:
<level>:设置过滤级别
<onMatch>:用于配置符合过滤条件的操作
<onMismatch>:用于配置不符合过滤条件的操作
此处配置为只接收ERROR日志级别信息
-->
<filter class="ch.qos.logback.classic.filter.LevelFilter">
<level>ERROR</level>
<onMatch>ACCEPT</onMatch>
<onMismatch>DENY</onMismatch>
</filter>
</appender>

<!-- 定义控制台appender 作用:把日志输出到控制台 class="ch.qos.logback.core.ConsoleAppender" -->
<appender name="console" class="ch.qos.logback.core.ConsoleAppender">
<layout class="ch.qos.logback.classic.PatternLayout">
<pattern>%date %level [%thread] %logger{10}.%class{0}#%method[%file:%line] %n%msg%n</pattern>
</layout>
</appender>

<!-- 将root的打印级别设置为"error",指定了名字为"console","fileAppender","errorAppender"的appender -->
<root level="error">
<appender-ref ref="console"/>
<appender-ref ref="fileAppender"/>
<appender-ref ref="errorAppender"/>
</root>

<!--
logger用来设置某一个包的日志打印级别
<loger> 仅有一个name属性,一个可选的level和一个可选的addtivity属性
name:用来指定受此loger约束的某一个包或者具体的某一个类
level:用来设置打印级别,大小写无关:TRACE, DEBUG, INFO, WARN, ERROR, ALL 和 OFF
addtivity:是否向上级loger传递打印信息。默认是true,会将信息输入到root配置指定的地方,可以包含多个appender-ref,标识这个appender会添加到这个logger
-->
<logger name="com.xcbeyond.springboot" level="debug"/>
</configuration>

3、日志打印

已项目启动类中输出debug日志为例说明。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
arduino复制代码package com.xcbeyond.springboot;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
/**
* SpringBoot启动类
* @author xcbeyond
* 2018年7月2日下午5:41:45
*/
@SpringBootApplication
public class SpringbootApplication {
private static Logger logger = LoggerFactory.getLogger(SpringbootApplication.class);

public static void main(String[] args) {
if(logger.isDebugEnabled()) {
logger.debug("SpringBoot starting...");
}
SpringApplication.run(SpringbootApplication.class, args);
}
}
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
/**
* SpringBoot启动类
* @author xcbeyond
* 2018年7月2日下午5:41:45
*/
@SpringBootApplication
public class SpringbootApplication {
private static Logger logger = LoggerFactory.getLogger(SpringbootApplication.class);

public static void main(String[] args) {
if(logger.isDebugEnabled()) {
logger.debug("SpringBoot starting...");
}
SpringApplication.run(SpringbootApplication.class, args);
}
}

注:请使用包org.slf4j.Logger、org.slf4j.LoggerFactory。SLF4J只是一个日志标准,并不是日志框架的具体实现,便于后期维护时可以根据不同的日志 框架配置不同类型的日志,而不用修改日志输出代码。

4、启动项目。

在项目的同级目录下会生成logs\2018-07-11\日志文件夹及日志文件。​

​

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

【Docker 系列】docker 学习八,有趣的 Dock

发表于 2021-08-10

这是我参与8月更文挑战的第10天,活动详情查看:8月更文挑战

【Docker 系列】docker 学习八,Docker 网络

开始理解 docker

一开始,咱们思考一下,宿主机怎么和容器通信呢?

说容器之间是相互隔离的,那么他们是否可以通信?又是如何通信的呢?

开始探索

我们先来看看咱环境中的镜像都有些啥,有 xmtubuntu

1
2
3
shell复制代码# docker images
REPOSITORY TAG IMAGE ID CREATED SIZE
xmtubuntu latest c3e95388a66b 38 seconds ago 114MB

再来看看宿主机的网卡信息

ip addr 来查看咱们宿主机的网卡信息

我们发现有一个 docker0,是因为我们的宿主机上面安装了docker 的服务,docker 会给我生成一个虚拟网卡,图中的这个 docker0就是虚拟网卡信息

创建并启动一个docker 命名为 ubuntu1

docker run -it --name ubuntu1 -P xmtubuntu

查看一下宿主机网卡信息

查看宿主机的网卡信息

再查看 ubuntu1 的网卡信息,docker 也会默认给我们的容器分配ip 地址

可以发现宿主机的网卡信息 docker0 下面多了117: veth838e165@if116:,ubuntu1的网卡信息上也正好有116: eth0@if117

我们发现这些veth的编号是成对出现的,咱们的宿主机就可以和 ubuntu1 进行通信了

使用宿主机(docker0)和ubuntu1互相 ping

docker0 pingubuntu1 ok

ubuntu1pingdocker0 ,同样的 ok

咱们可以尝试再创建并启动一个docker 命名为 ubuntu2,方法和上述完全一致

1
shell复制代码# docker run -it -P --name ubuntu2 xmtubuntu

进入容器,使用ip a查看到ubuntu2的网卡信息

宿主机上面查看网信息


宿主机上面又多了一个 veth , 119: veth0b29558@if118

ubuntu2上的网卡信息是118: eth0@if119,他们同样是成对出现的,小伙伴看到这里应该明白了吧

ubuntu1 ping ubuntu2 呢?

ubuntu1 对应 172.18.0.2

ubuntu2 对应 172.18.0.3

1
2
3
4
5
shell复制代码# docker exec -it ubuntu1 ping 172.18.0.3
PING 172.18.0.3 (172.18.0.3) 56(84) bytes of data.
64 bytes from 172.18.0.3: icmp_seq=1 ttl=64 time=0.071 ms
64 bytes from 172.18.0.3: icmp_seq=2 ttl=64 time=0.070 ms
64 bytes from 172.18.0.3: icmp_seq=3 ttl=64 time=0.077 ms

仍然是可以通信,非常 nice

原理是什么?

上述的探索,我们发现宿主机创建的容器,都可以直接ping通宿主机,那么他们的原理是啥呢?

细心的 xdm 应该可以看出来,上述的例子中,veth是成对出现的,上述宿主机和容器能够进行网络通信,得益于这个技术veth-pair

veth-pair

**veth-pair **是一对虚拟设备接口,他们都是成对出现的,一段连着协议,一段彼此相连

正是因为这个特性,veth-pair在此处就是充当了一个桥梁,连接各种虚拟设备

通过上图我们可以得出如下结论:

  • ubuntu1 和 ubuntu2他们是公用一个路由器,也就是 docker0,ubuntu1 能ping通ubuntu2是因为 docker0 帮助其转发的
  • 所有的容器在不指定路由的情况下,都是以 docker0 作为路由,docker 也会给我们的容器分配一个可用的 ip
  • docker0 是在宿主机上面安装 docker 服务就会存在的

那么通过上图我们就知道,容器和宿主机之前是通过桥接的方式来打通网络的。

Dcoker 中所有的网络接口都是虚拟的,因为虚拟的转发效率高呀,当我们删除某一个容器的时候,这个容器对应的网卡信息,也会被随之删除掉

那么我们可以思考一下,如果都是通过找ip地址来通信,如果 ip变化了,那么我们岂不是找不到正确的容器了吗?我们是否可以通过服务名来访问容器呢?

–link

当然是可以的,当我们在创建和启动容器的时候加上–link就可以达到这个效果

我们再创建一个容器 ubuntu3,让他 link 到 ubuntu2

# docker run -it --name ubuntu3 -P --link ubuntu2 xmtubuntu

1
2
3
4
5
6
shell复制代码# docker exec -it ubuntu3 ping ubuntu2
PING ubuntu2 (172.18.0.3) 56(84) bytes of data.
64 bytes from ubuntu2 (172.18.0.3): icmp_seq=1 ttl=64 time=0.093 ms
64 bytes from ubuntu2 (172.18.0.3): icmp_seq=2 ttl=64 time=0.085 ms
64 bytes from ubuntu2 (172.18.0.3): icmp_seq=3 ttl=64 time=0.092 ms
64 bytes from ubuntu2 (172.18.0.3): icmp_seq=4 ttl=64 time=0.073 ms

很明显,我们可以到看到 ubuntu3 可以通过服务名ubuntu2 直接和ubuntu2通信,但是反过来是否可以呢?

1
2
shell复制代码# docker exec -it ubuntu2 ping ubuntu3
ping: ubuntu3: Name or service not known

不行?这是为什么呢?

我们来查看一下 ubuntu3 的本地 /etc/hosts 文件就清楚了

看到这里,这就清楚了 link 的原理了吧,就是在自己的 /etc/hosts 文件中,加入一个host而已,这个知识点我们可以都知悉一下,但是这个 link 还是好搓,不好,他需要在创建和启动容器的时候使用,用起来不方便

那么我们有没有更好的办法的呢?

自定义网络

可以使用 docker network ls查看宿主机 docker 的网络情况

1
2
3
4
5
shell复制代码:~# docker network ls
NETWORK ID NAME DRIVER SCOPE
8317183dfc58 bridge bridge local
997107487c6b host host local
ab130876cbe6 none null local

网络模式

  • bridge

桥接,docker0 默认使用 bridge 这个名字

  • host

和宿主机共享网络

  • none

不配置网络

  • container

容器网络连通,这个模式用的非常少,因为局限性很大

现在咱们可以自定义个网络,来连通两个容器

自定义网络

自定义一个 mynet 网络

1
2
shell复制代码# docker network create --driver bridge --subnet 192.168.0.0/16 --gateway 192.168.0.1 mynet
9a597fc31f1964d434181907e21ff7010738f3f7dc35ba86bf7434f05a6afc4a
  • docker network create

创建一个网络

  • –driver

指定驱动是 bridge

  • –subnet

指定子网

  • –gateway

指定网关

此处我们设置子网是 --subnet 192.168.0.0/16,网关是192.168.0.1,那么我们剩下可以使用的ip就是 192.168.0.2 – 192.168.255.254 , 192.168.255.255是广播地址

清空已有的容器

清空所有测试的容器,减去干扰

创建并启动2个容器,分别是ubuntu1 和 ubuntu2

1
2
shell复制代码# docker run -it -P --name ubuntu1 --net mynet xmtubuntu
# docker run -it -P --name ubuntu2 --net mynet xmtubuntu

此时我们可以查看一下宿主机的网卡信息,并验证两个容器直接通过容器名字是否可以通信

我们思考一下自定义网络的好处

咱们自定义 docker 网络,已经帮我们维护好了对应关系,这样做的好处是容器之间可以做到网络隔离,

例如

一堆 redis 的容器,使用 192.168.0.0/16 网段,网关是 192.168.0.1

一堆 mongodb的容器,使用 192.167.0.0/16 网段,网关是 192.167.0.1

这样就可以做到子网很好的隔离开来,不同的集群使用不同的子网,互不影响

那么子网间是否可以打通呢?

网络连通

两个不同子网内的容器如何连通了呢?

我们绝对不可能让不同子网的容器不通过路由的转发而直接通信,这是不可能的,子网之间是相互隔离的

但是我们有办法让 ubuntu3 这个容器通过和 mynet 打通,进而转发到 ubuntu1 或者 ubuntu2 中就可以了

打通子网

我们查看 docker network 的帮助手册

docker network -h

image-20210807203841520

可以使用 docker network connect 命令实现,在查看一下帮助文档

1
2
3
4
5
6
shell复制代码# docker network connect -h
Flag shorthand -h has been deprecated, please use --help

Usage: docker network connect [OPTIONS] NETWORK CONTAINER

Connect a container to a network

开始打通

docker network connect mynet ubuntu3

这个时候我们可以查看一下 mynet 网络的详情

1
shell复制代码# docker network inspect mynet

可以看到在 mynet 网络上,又增加了一个容器,ip 是 192.168.0.4

没错,docker 处理这种网络打通的事情就是这么简单粗暴,直接在 ubuntu3 容器上增加一个虚拟网卡,让 ubuntu3 能够和 mynet 网络打通

宿主机当然也相应的多了一个veth

master/image-20210807204514806.png)

现在,要跨网络操作别人的容器,我们就可以使用 docker network connect的方式将网络打通,开始干活了

大家对网络还感兴趣吗,哈哈,关于 docker 的前几期文章链接如下,可以逐步学习,慢慢深入,多多回顾

【Docker 系列】docker 学习 五,我们来看看容器数据卷到底是个啥

【Docker 系列】docker 学习 四,一起学习镜像相关原理

【Docker 系列】docker 学习 三,docker 初步实战和 docker 可视化管理工具试炼

【Docker 系列】docker 学习 二,docker 常用命令,镜像命令,容器命令,其他命令

【Docker 系列】docker 学习 一,Docker的安装使用及Docker的基本工作原理 | 8月更文挑战

参考资料:

docker docs

欢迎点赞,关注,收藏

朋友们,你的支持和鼓励,是我坚持分享,提高质量的动力

好了,本次就到这里

技术是开放的,我们的心态,更应是开放的。拥抱变化,向阳而生,努力向前行。

我是小魔童哪吒,欢迎点赞关注收藏,下次见~

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

就内存泄露聊聊ThreadLocal

发表于 2021-08-10

1.前言

之前一直听说ThreadLocal会出现内存泄露的情况,也看过相关文章,看的时候大概知道为什么会出现内存泄露,过一段时候后就会忘记内存泄露的根源在哪。为了彻底搞懂此问题,于是就有了此篇文章。

2.线程Thread

要想弄清ThreadLocal,就不得不提Thread。Thread中持有ThreadLocalMap引用,ThreadLocalMap的key引用ThreadLocal,value为使用者设置的值。

3.ThreadLocal

3.1 ThreadLocal使用示例代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
csharp复制代码public class ThreadLocalUtils {
​
   private static ThreadLocal<Integer> threadLocal = new ThreadLocal<>();
​
   public static void set(Integer value) {
       threadLocal.set(value);
  }
​
   public static Integer get() {
       return Objects.nonNull(threadLocal.get()) ? threadLocal.get() : null;
  }
​
   public static void remove() {
       threadLocal.remove();
  }
}

3.2 内存图

有了示例代码,又有了内存图,下面就来分析分析ThreadLocal为什么会出现内存泄露

3.3 内存泄露场景

当线程循环使用(线程池),CurrentThread就不会被回收,假如Entry中的value占用比较大的内存空间,就会发生内存泄露

3.4 安全场景

  • 当方法执行完成后线程得到释放,Thread thread到CurrentThread引用就会断开,GC后CurrentThread就会被回收,该情况下不会发生内存泄露
  • 当使用完ThreadLocal后调用remove()方法也不会发生内存泄露

4. 总结

使用ThreadLocal要想不发生内存泄露,要么确保线程不被循环使用,要么显示调用其remove()方法,推荐在使用完后显示调用remove()

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

【Spring Boot 快速入门】四、Spring Boo

发表于 2021-08-10

这是我参与8月更文挑战的第10天,活动详情查看:8月更文挑战

收录专栏

Spring Boot 快速入门

Java全栈架构师

前言

  我们在开发的时候经常需要写一写测试代码,需要验证已完成的功能是否按照预先设计的业务逻辑运行,这时候就会使用到单元测试,当然使用junit写一些适当的测试能够快速检测业务逻辑的正确性,及时调整优化我们的代码。本文将开始介绍Spring Boot集成JUnit,下面开始介绍JUnit。

什么JUnit

  Junit是一个Java语言的单元测试框架。它由Kent Beck和Erich Gamma建立,逐渐成为源于Kent Beck
的sUnit的xUnit家族中最为成功的一个。JUnit有它自己的JUnit扩展生态圈。多数Java的开发环境都已经集
成了JUnit作为单元测试的工具。JUnit作为目前Java领域内最为流行的单元测试框架已经走过了数十年。

JUnit作用

  Junit能让我们快速的完成单元测试,而且编写测试和编写代
码都是增量式的,写一点测一点,在编写以后的代码中如果发现问题可以较快的追踪到问题的原因,减小
回归错误的纠错难度。

JUnit特性

  JUnit是一个开放源代码的Java测试框架,用于编写和运行可重复的测试。他是用于单元测试框架体系xUnit的一个实例(用于java语言)。它包括以下特性:
1、用于测试期望结果的断言(Assertion)
2、用于共享共同测试数据的测试工具
3、用于方便的组织和运行测试的测试套件
4、图形和文本的测试运行器

JUnit优点

  • 安装简单
  • 极限编程
  • 重构

快速开始

引包

  本次测试使用的是Maven构建的Java项目,所以首先应该在pom.xml中引入相应的依赖包。具体使用需要与所使用的框架版本结合起来。找到合适的版本。本文使用的依赖如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
js复制代码<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
<exclusions>
<exclusion>
<groupId>org.junit.vintage</groupId>
<artifactId>junit-vintage-engine</artifactId>
</exclusion>
</exclusions>
</dependency>

<!-- junjit -->
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>4.12</version>
<scope>test</scope>
</dependency>

常用注解

常用注解 描述
@Test 将一个方法标记为测试方法
@Before 每一个测试方法调用前必执行的方法
@After 每一个测试方法调用后必执行的方法
@BeforeClass 所有测试方法调用前执行一次,在测试类没有实例化之前就已被加载,需用static修饰
@AfterClass 所有测试方法调用后执行一次,在测试类没有实例化之前就已被加载,需用static修饰
@lgnore 暂不执行该方法
@Timeout 表示测试方法运行如果超过了指定时间将会返回错误
@ExtendWith 为测试类或测试方法提供扩展类引用
@Disabled 表示测试类或测试方法不执行

执行顺序为:@BeforeClass –> @Before –> @Test –> @After –> @AfterClass

Spring Boot注解

  • @SpringBootTest(classes = DemoJunitApplication.class):引入的一个用于测试的注解
  • @RunWith(SpringRunner.class),是一个运行器,指定运行的类,让测试在Spring容器环境下执行。
  • @Rollback:数据回滚,这样避免测试数据污染数据库
  • @AutoConfigureMockMvc:自动配值

项目结构

图片.png

源码

pom.xml

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
js复制代码<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.3.0.RELEASE</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
<groupId>com.example</groupId>
<artifactId>junit</artifactId>
<version>0.0.1-SNAPSHOT</version>
<name>junit</name>
<description>Demo project for Spring Boot and MyBatis and Swagger</description>

<properties>
<java.version>1.8</java.version>
</properties>

<dependencies>
<!-- commons start -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-lang3</artifactId>
<version>3.7</version>
</dependency>
<!-- commons end -->

<!-- mybatis start-->
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<scope>runtime</scope>
</dependency>
<!-- mybatis-spring-boot-starter -->
<dependency>
<groupId>org.mybatis.spring.boot</groupId>
<artifactId>mybatis-spring-boot-starter</artifactId>
<version>2.1.1</version>
</dependency>
<!-- druid -->
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>druid</artifactId>
<version>1.1.11</version>
</dependency>
<!-- mybatis end-->

<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
<exclusions>
<exclusion>
<groupId>org.junit.vintage</groupId>
<artifactId>junit-vintage-engine</artifactId>
</exclusion>
</exclusions>
</dependency>

<!-- junjit -->
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>4.12</version>
<scope>test</scope>
</dependency>
</dependencies>

<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
</project>

测试类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
js复制代码package com.example.demo;

import com.example.demo.controller.TestController;
import com.example.demo.service.UserService;
import org.junit.jupiter.api.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.test.annotation.Rollback;
import org.springframework.test.context.junit4.SpringRunner;

@SpringBootTest(classes = DemoJunitApplication.class)
@RunWith(SpringRunner.class)
@Rollback
@AutoConfigureMockMvc
class DemoApplicationTests {

@Autowired
private TestController testController;

@Autowired
private UserService userService;

/**
* @ClassName 测试接口
* @Description: 获取所有用户
* @Author JavaZhan @公众号:Java全栈架构师
* @Date 2020/6/13
* @Version V1.0
**/
@Test
void getTest() {
System.out.println(testController.getTest());

}


/**
* @ClassName getAllUser
* @Description: 获取所有用户
* @Author JavaZhan @公众号:Java全栈架构师
* @Date 2020/6/13
* @Version V1.0
**/
@Test
void getAllUser() {
userService.getAllUser().forEach(user -> System.out.println(user));

}

}

运行结果

图片.png

结语

  本次基于Spring Boot集成JUnit的项目就完成了,相信你对JUnit的单元测试有了一个简单的了解,希望本文可以帮助到你。感谢阅读。

  作者介绍:【小阿杰】一个爱鼓捣的程序猿,JAVA开发者和爱好者。公众号【Java全栈架构师】维护者,欢迎关注阅读交流。

  好了,感谢您的阅读,希望您喜欢,如对您有帮助,欢迎点赞收藏。如有不足之处,欢迎评论指正。下次见。

推荐阅读:

我的第一个Spring Boot项目启动啦!

周末建立了Spring Boot专栏,欢迎学习交流

Spring Boot集成MyBatis,可以连接数据库啦!

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

sqoop学习,这一篇文章就够了

发表于 2021-08-10

这是我参与8月更文挑战的第6天,活动详情查看:8月更文挑战

一、环境信息

Sqoop version: 1.4.7-cdh6.3.2

二、Sqoop导入

1. 目的

该import工具将单个表从 关系型数据库导入到 HDFS。表中的每一行在 HDFS 中都表示为单独的记录。记录可以存储为文本文件(每行一条记录),或以二进制表示形式存储为 Avro 或 SequenceFiles。

2. 语法

I. 链接数据库带服务器

官方文档解释:Sqoop 旨在将表从数据库导入 HDFS。为此,您必须指定一个描述如何连接到数据库的连接字符串。简单来说,我们必须制定一个源表,也就是关系型数据库的数据库连接的URL

1
2
3
4
5
shell复制代码bin/sqoop import \
--connect jdbc:mysql://10.0.10.118:3306/sqoop_test \
--username root \
--password 123456 \
--table sqoop_test \
import: 导入标记,证明后续参数全部都是从关系型数据库导入到大数据平台的参数
–connect import的参数,表明我们源数据所在的数据库连接
–username import的参数,表明我们数据库的用户名
–password import的参数,数据库密码
–table import的参数,元数据表名称

官方警告:Sqoop 将读取密码文件的全部内容并将其用作密码。这将包括任何尾随空白字符,例如大多数文本编辑器默认添加的换行符。您需要确保您的密码文件只包含属于您的密码的字符。

Sqoop 自动支持多种数据库,包括 MySQL。以 开头的连接字符串jdbc:mysql://在 Sqoop 中自动处理。(“支持的数据库”部分提供了具有内置支持的完整数据库列表。对于某些数据库,您可能需要自己安装 JDBC 驱动程序。)

您可以将 Sqoop 与任何其他符合 JDBC 的数据库一起使用。首先,为要导入的数据库类型下载适当的 JDBC 驱动程序,并将 .jar 文件安装在$SQOOP_HOME/lib客户端计算机上的目录中。(/usr/lib/sqoop/lib如果您是从 RPM 或 Debian 软件包安装的,则会出现这种情况。)每个驱动程序.jar文件还有一个特定的驱动程序类,用于定义驱动程序的入口点。例如,MySQL 的 Connector/J 库的驱动程序类为com.mysql.jdbc.Driver. 请参阅特定于数据库供应商的文档以确定主要驱动程序类。此类必须作为 Sqoop 的参数提供--driver。

Sqoop 通常以表为中心的方式导入数据。使用 --table参数选择要导入的表。例如,--table employees。此参数还可以标识数据库中的一个VIEW或其他类似表的实体。

II. 基本参数

–append 将数据附加到 HDFS 中的现有数据集
–as-avrodatafile 将数据导入 Avro 数据文件
–as-sequencefile 将数据导入到 SequenceFiles
–as-textfile 以纯文本形式导入数据(默认)
–as-parquetfile 将数据导入 Parquet 文件
–boundary-query 用于创建拆分的边界查询
–columns <col,col,col…> 要从表中导入的列
–delete-target-dir 删除导入目标目录(如果存在)
–direct 如果数据库存在,则使用直接连接器
–fetch-size 一次从数据库读取的条目数。
–inline-lob-limit 设置内联 LOB 的最大大小
-m,–num-mappers 使用n个map任务并行导入
-e,–query 导入结果*statement*。
–split-by 用于拆分工作单元的表列。不能与--autoreset-to-one-mapper选项一起使用 。
–split-limit 每个拆分大小的上限。这仅适用于整数和日期列。对于日期或时间戳字段,它以秒为单位计算。
–autoreset-to-one-mapper 如果表没有主键且未提供拆分列,则导入应使用一个映射器。不能与--split-by <col>选项一起使用 。
–table 表名称
–target-dir HDFS 目标目录
–temporary-rootdir 导入期间创建的临时文件的 HDFS 目录(覆盖默认的“_sqoop”)
–warehouse-dir 表目标的 HDFS 父级
–where 导入期间使用的 WHERE 子句
-z,–compress 启用压缩
–compression-codec 使用 Hadoop 编解码器(默认 gzip)
–null-string 要为字符串列的空值写入的字符串
–null-non-string 要为非字符串列的空值写入的字符串
–fields-terminated-by 设置字段分隔符
–lines-terminated-by 设置行尾字符
–mysql-delimiters 使用 MySQL 的默认分隔符集:fields: , lines: \n escaped-by: \ optional-enclosed-by:'

该--mysql-delimiters参数是一个速记参数,它使用程序的默认分隔符mysqldump。如果将mysqldump分隔符与直接模式导入(使用--direct)结合使用,则可以实现非常快的导入。

三、基本使用

1. mysql导入到hdfs

sudo -u hdfs 命令

I. 全表导入

1
2
3
4
5
6
7
8
9
shell复制代码bin/sqoop import \
--connect jdbc:mysql://10.0.10.118:3306/sqoop_test \
--username root \
--password 123456 \
--table sqoop_test \
--target-dir /user/company \
--delete-target-dir \
--num-mappers 1 \
--fields-terminated-by "\t"

II. 自由格式导入

如果要并行导入查询的结果,那么每个 map 任务都需要执行查询的副本,结果由 Sqoop 推断的边界条件进行分区。您的查询必须包含$CONDITIONS 每个 Sqoop 进程将替换为唯一条件表达式的标记。您还必须选择带有 的拆分列--split-by。

Sqoop 从大多数数据库源并行导入数据。您可以指定(并行处理)的地图任务的数量使用通过执行进口-m或--num-mappers争论。这些参数中的每一个都采用一个整数值,该值对应于要采用的并行度。默认情况下,使用四个任务。某些数据库可能会通过将此值增加到 8 或 16 来提高性能。不要将并行度增加到大于 MapReduce 集群中可用的程度;任务将连续运行,并且可能会增加执行导入所需的时间。同样,不要将并行度增加到高于您的数据库可以合理支持的程度。将 100 个并发客户端连接到您的数据库可能会将数据库服务器上的负载增加到性能受到影响的程度。

在执行并行导入时,Sqoop 需要一个标准来分割工作负载。Sqoop 使用拆分列来拆分工作负载。默认情况下,Sqoop 将识别表中的主键列(如果存在)并将其用作拆分列。从数据库中检索拆分列的低值和高值,并且映射任务对总范围的大小均匀的组件进行操作。例如,如果您有一个表,其主键列的 id最小值为 0,最大值为 1000,并且 Sqoop 被指示使用 4 个任务,则 Sqoop 将运行四个进程,每个进程执行形式为 的 SQL 语句SELECT * FROM sometable WHERE id >= lo AND id < hi,(lo, hi)设置为到 (0, 250)、(250, 500)、(500, 750) 和 (750, 1001) 在不同的任务中。

如果主键的实际值未在其范围内均匀分布,则可能会导致任务不平衡。您应该使用--split-by参数明确选择不同的列。例如,--split-by employee_id。Sqoop 目前无法在多列索引上拆分。如果您的表没有索引列,或者有多列键,那么您还必须手动选择拆分列。

用户可以--num-mapers使用--split-limit选项覆盖。使用该--split-limit参数会限制创建的拆分部分的大小。如果创建的 split 的大小大于此参数中指定的大小,则将调整 split 的大小以适应此限制,并且 split 的数量将根据此变化。这会影响实际的 mapper 数量。如果根据提供的--num-mappers参数计算的拆分大小超过--split-limit参数,则实际映射器的数量将增加。如果参数中指定的值为--split-limit 0 或负数,则该参数将被完全忽略,并根据数量计算拆分大小映射器。

如果表没有定义主键--split-by <col> 且未提供,则导入将失败,除非使用--num-mappers 1选项或使用--autoreset-to-one-mapper选项将映射器的数量显式设置为 1 。该选项 --autoreset-to-one-mapper通常与 import-all-tables 工具一起使用,以自动处理模式中没有主键的表。

1
2
3
4
5
6
7
8
9
shell复制代码bin/sqoop import \
--connect jdbc:mysql://10.0.10.118:3306/sqoop_test \
--username root \
--password 123456 \
--target-dir /user/company \
--delete-target-dir \
--num-mappers 1 \
--fields-terminated-by "\t" \
--query 'select id,name,sex from sqoop_test where id>2 and $CONDITIONS'

III. 条件导入

1
2
3
4
5
6
7
8
9
10
shell复制代码bin/sqoop import \
--connect jdbc:mysql://10.0.10.118:3306/sqoop_test \
--username root \
--password 123456 \
--table sqoop_test \
--where "id<=2" \
--target-dir /user/company \
--delete-target-dir \
--num-mappers 1 \
--fields-terminated-by "\t"

IV. 导入制定的列

1
2
3
4
5
6
7
8
9
10
11
shell复制代码bin/sqoop import \
--connect jdbc:mysql://10.0.10.118:3306/sqoop_test \
--username root \
--password 123456 \
--table sqoop_test \
--where "id>=2" \
--columns name,sex \
--target-dir /user/company \
--delete-target-dir \
--num-mappers 1 \
--fields-terminated-by "\t"

V. 事物隔离级别

默认情况下,Sqoop 使用映射器中的读提交事务隔离来导入数据。这在所有 ETL 工作流中可能不是理想的,并且可能需要减少隔离保证。该--relaxed-isolation选项可用于指示 Sqoop 使用读取未提交隔离级别。

该read-uncommitted隔离级别不支持所有数据库(例如Oracle),因此指定选项--relaxed-isolation 可能无法在所有数据库的支持。

VI. 增量导入

争论 描述
–check-column (col) 指定在确定要导入的行时要检查的列。(该列的类型不应为 CHAR/NCHAR/VARCHAR/VARNCHAR/LONGVARCHAR/LONGNVARCHAR)
–incremental (mode) 指定 Sqoop 如何确定哪些行是新行。mode includeappend和 的合法值lastmodified。
–last-value (value) 指定上次导入的检查列的最大值。

Sqoop 支持两种类型的增量导入:append和lastmodified. 您可以使用该–incremental参数来指定要执行的增量导入的类型。

每次导入都产生一个新文件:

1
2
3
4
5
6
7
8
9
10
11
shell复制代码bin/sqoop import \
--connect jdbc:mysql://10.0.10.118:3306/sqoop_test \
--username root \
--password 123456 \
--table sqoop_test \
--incremental append \
--check-column id \
--last-value 5 \
--target-dir /user/company \
--num-mappers 1 \
--fields-terminated-by "\t"

每次将新数据或更新后的数据同原有数据进行合并:

1
2
3
4
5
6
7
8
9
10
11
12
shell复制代码sqoop import \
--connect jdbc:mysql://10.0.10.118:3306/sqoop_test \
--username root \
--password 123456 \
--table sqoop_test \
--incremental lastmodified \
--check-column date \
--last-value "2021-07-26 11:53:50" \
--target-dir /user/company \
--num-mappers 1 \
--fields-terminated-by "\t" \
--merge-key id

您应该append在导入表时指定模式,在该表中不断添加新行并增加行 id 值。您使用 指定包含行 id 的列--check-column。Sqoop 导入检查列的值大于指定值的行--last-value。

Sqoop 支持的另一种表更新策略称为lastmodified 模式。当源表的行可能被更新时,您应该使用它,并且每次这样的更新都会将最后修改列的值设置为当前时间戳。--last-value导入检查列保存的时间戳比指定的时间戳更近的行。

在增量导入结束时,应--last-value为后续导入指定的值 将打印到屏幕上。在运行后续导入时,您应该--last-value以这种方式指定以确保仅导入新的或更新的数据。这是通过将增量导入创建为保存的作业来自动处理的,这是执行重复增量导入的首选机制。有关详细信息,请参阅本文档后面有关已保存作业的部分。

例如,当我们使用时间增量的时候:

1
2
3
shell复制代码--incremental lastmodified \
--check-column date_col \
--last-value "2021-08-02 17:17:23"

2. mysql导入到hive

I. 基本参数

--hive-home <dir> 覆盖 $HIVE_HOME
–hive-import 将表导入 Hive(如果没有设置,则使用 Hive 的默认分隔符。)
–hive-overwrite 覆盖 Hive 表中的现有数据。
–create-hive-table 如果设置,那么如果目标配置单元,作业将失败
–hive-table 设置导入到 Hive 时要使用的表名。
–hive-drop-import-delims 导入到 Hive 时,从字符串字段中 删除*\n*、\r和*\01*。
–hive-delims-replacement 导入到 Hive 时,将字符串字段中的*\n*、\r和\\01\ 替换为用户定义的字符串。
–hive-partition-key 要分区的 hive 字段的名称被分片
–hive-partition-value 在此作业中用作此导入到配置单元的分区键的字符串值。
–map-column-hive 为配置的列覆盖从 SQL 类型到 Hive 类型的默认映射。如果在此参数中指定逗号,请使用 URL 编码的键和值,例如,使用 DECIMAL(1%2C%201) 而不是 DECIMAL(1, 1)。

II. 入门演示

1
2
3
4
5
6
7
8
9
10
shell复制代码bin/sqoop import \
--connect jdbc:mysql://10.0.10.118:3306/sqoop_test \
--username root \
--password 123456 \
--table sqoop_test \
--num-mappers 1 \
--hive-import \
--fields-terminated-by "\t" \
--hive-overwrite \
--hive-table sqoop_test_hive

事实上,这里有两个步骤,sqoop在同步到hive的时候,事实上sqoop会将数据先导入到hdfs中,再从hdfs中转移到hive中去!

他会先将数据写入HDFS中的一个临时目录,然后,再复制到/user/hive/warehouse/sqoop_test_hive目录下!

III. 官方警告

官方:如果您有多个 Hive 安装,或者hive不在您的 .hive 中$PATH,请使用该 **--hive-home**选项来标识 Hive 安装目录。Sqoop$HIVE_HOME/bin/hive将从这里开始使用。

尽管 Hive 支持转义字符,但它不处理换行符的转义。此外,它不支持可能在封闭字符串中包含字段分隔符的封闭字符的概念。因此,建议您在使用 Hive 时选择明确的字段和记录终止分隔符,而不需要转义和封闭字符;这是由于 Hive 的输入解析能力的限制。如果您确实使用 --escaped-by, --enclosed-by, 或--optionally-enclosed-by在将数据导入 Hive 时,Sqoop 将打印一条警告消息。

如果您的数据库行包含的字符串字段中存在 Hive 的默认行分隔符(\n和\r字符)或列分隔符(\01字符),则 Hive 在使用 Sqoop 导入的数据时会出现问题。您可以使用该--hive-drop-import-delims选项在导入时删除这些字符以提供与 Hive 兼容的文本数据。或者,您可以使用该--hive-delims-replacement选项在导入时将这些字符替换为用户定义的字符串,以提供与 Hive 兼容的文本数据。仅当您使用 Hive 的默认分隔符时才应使用这些选项,如果指定了不同的分隔符,则不应使用这些选项。

Sqoop 默认将 NULL 值导入为 string null。然而,Hive 使用字符串\N来表示NULL值,因此处理NULL(如IS NULL)的谓词将无法正常工作。你应该追加参数--null-string和--null-non-string进口工作或情况 --input-null-string,并--input-null-non-string在出口工作的情况下,如果你要妥善保存NULL价值。因为 sqoop 在生成的代码中使用这些参数,所以您需要正确地将值转义\N为\\N:

例如:

1
shell复制代码sqoop import ... --null-string '\\N' --null-non-string '\\N'

默认情况下,Hive 中使用的表名与源表的表名相同。您可以使用该--hive-table 选项控制输出表名称。

Hive 可以将数据放入分区以获得更高效的查询性能。您可以通过指定--hive-partition-key和 --hive-partition-value参数告诉 Sqoop 作业将 Hive 的数据导入特定分区。分区值必须是字符串。有关分区的更多详细信息,请参阅 Hive 文档。

您可以使用--compress和 --compression-codec选项将压缩表导入 Hive 。压缩导入 Hive 的表的一个缺点是许多编解码器无法拆分以供并行映射任务处理。但是,lzop 编解码器确实支持拆分。使用此编解码器导入表时,Sqoop 将自动索引文件以使用正确的 InputFormat 拆分和配置新的 Hive 表。此功能当前要求使用 lzop 编解码器压缩表的所有分区。

3. mysql导入到HBash

I. 基本参数

必须采用a的形式,逗号分隔的组合键列表属性!

--column-family <family> 设置导入的目标列族
–hbase-create-table 如果指定,则创建缺少的 HBase 表
–hbase-row-key
指定要用作行键的输入列, 如果输入表包含复合键,则
–hbase-table 指定要用作目标的 HBase 表而不是 HDFS
–hbase-bulkload 启用批量加载

如果输入表有复合键,则--hbase-row-key必须采用逗号分隔的复合键属性列表的形式。在这种情况下,HBase 行的行键将通过使用下划线作为分隔符组合复合键属性的值来生成。注意:只有--hbase-row-key在指定了参数的情况下,Sqoop 导入表才能使用复合键。

如果目标表和列族不存在,则 Sqoop 作业将退出并显示错误。您应该在运行导入之前创建目标表和列族。如果指定--hbase-create-table,Sqoop 将使用 HBase 配置中的默认参数创建目标表和列族(如果它们不存在)。

Sqoop 当前通过将每个字段转换为其字符串表示形式(就像您以文本模式导入到 HDFS 一样)将所有值序列化到 HBase,然后在目标单元格中插入此字符串的 UTF-8 字节。Sqoop 将跳过除行键列之外的所有列中包含空值的所有行。

为了减少 hbase 上的负载,Sqoop 可以进行批量加载而不是直接写入。要使用批量加载,请使用--hbase-bulkload.

争论 描述
–accumulo-table 指定要用作目标的 Accumulo 表而不是 HDFS
–accumulo-column-family 设置导入的目标列族
–accumulo-create-table 如果指定,则创建缺少的 Accumulo 表
–accumulo-row-key
指定要用作行键的输入列
–accumulo-visibility (可选)指定一个可见性标记以应用于插入到 Accumulo 中的所有行。默认为空字符串。
–accumulo-batch-size (可选)设置 Accumulo 的写入缓冲区的大小(以字节为单位)。默认为 4MB。
–accumulo-max-latency (可选)设置 Accumulo 批处理写入器的最大延迟(以毫秒为单位)。默认值为 0。
–accumulo-zookeepers host:port Accumulo 实例使用的 Zookeeper 服务器的逗号分隔列表
–accumulo-instance 目标 Accumulo 实例的名称
–accumulo-user 要导入的 Accumulo 用户的名称
–accumulo-password Accumulo 用户的密码

II. 入门演示

实例项目:

hbase创建项目:

hbase shell

1
shell复制代码create 'sqoop_test_hbase','info'
1
2
3
4
5
6
7
8
9
10
11
shell复制代码bin/sqoop import \
--connect jdbc:mysql://10.0.10.118:3306/sqoop_test \
--username root \
--password 123456 \
--table sqoop_test \
--num-mappers 1 \
--column-family "info" \
--hbase-create-table \
--hbase-row-key "id" \
--hbase-table "sqoop_test_hbase" \
--split-by id

四、导出

即从大数据群导入到非大数据集群,不支持HBash导入到Mysql;

》该export工具将一组文件从 HDFS 导出回 RDBMS。目标表必须已存在于数据库中。根据用户指定的分隔符读取输入文件并将其解析为一组记录。

》默认操作是将这些转换为一组INSERT 将记录注入数据库的语句。在“更新模式”下,Sqoop 将生成UPDATE替换数据库中现有记录的语句,而在“调用模式”下,Sqoop 将对每条记录进行存储过程调用。

1. 语法格式

1
2
shell复制代码$ sqoop export (generic-args) (export-args)
$ sqoop-export (generic-args) (export-args)

2. 常见参数

key 描述
–connect 指定 JDBC 连接字符串
–connection-manager 指定要使用的连接管理器类
–driver 手动指定要使用的 JDBC 驱动程序类
–hadoop-mapred-home 覆盖 $HADOOP_MAPRED_HOME
–help 打印使用说明
–password-file 为包含认证密码的文件设置路径
-P 从控制台读取密码
–password 设置认证密码
–username 设置认证用户名
–verbose 工作时打印更多信息
–connection-param-file 提供连接参数的可选属性文件
–relaxed-isolation 将连接事务隔离设置为映射器未提交的读取。

出口控制参数:

争论 描述
–columns <col,col,col…> 要导出到表的列
–direct 使用直接导出快速路径
–export-dir 导出的 HDFS 源路径
-m,–num-mappers 使用n个map任务并行导出
–table 要填充的表
–call 调用的存储过程
–update-key 用于更新的锚列。如果有多于一列,请使用逗号分隔的列列表。
–update-mode 指定在数据库中发现具有不匹配键的新行时如何执行更新。法律值mode包括 updateonly(默认)和 allowinsert
–input-null-string 对于字符串列要解释为 null 的字符串
–input-null-non-string 对于非字符串列要解释为 null 的字符串
–staging-table 数据在插入目标表之前将在其中暂存的表。
–clear-staging-table 表示可以删除暂存表中存在的任何数据。
–batch 使用批处理模式进行底层语句执行。

该--export-dir参数和一个--table或者--call是必需的。它们指定要在数据库中填充的表(或要调用的存储过程),以及 HDFS 中包含源数据的目录。

默认情况下,选择表中的所有列进行导出。您可以选择列的子集并使用--columns参数控制它们的顺序 。这应该包括要导出的列的逗号分隔列表。例如:--columns "col1,col2,col3"。请注意,未包含在--columns参数中的列需要具有定义的默认值或允许NULL值。否则您的数据库将拒绝导入的数据,这反过来会使 Sqoop 作业失败。

3. hive导出到mysql

1
2
3
4
5
6
7
8
shell复制代码bin/sqoop export \
--connect jdbc:mysql://10.0.10.118:3306/sqoop_test \
--username root \
--password 123456 \
--table sqoop_test \
--num-mappers 1 \
--export-dir /user/hive/warehouse/sqoop_test_hive \
--input-fields-terminated-by "\t"

4. hdfs导出到mysql

1
shell复制代码vim test.txt
1
2
3
tex复制代码1,张三,男,2021-08-03 15:05:51
2,李四,女,2021-08-02 15:05:51
3,赵六,男,2021-08-01 15:05:51
1
2
3
4
5
6
7
8
shell复制代码sqoop export \
--connect jdbc:mysql://10.0.10.118:3306/sqoop_test \
--username root \
--password 123456 \
--table sqoop_test \
--num-mappers 1 \
--export-dir /user/test.txt \
--input-fields-terminated-by ","

5. 更新导出,只更新已经存在的数据

1
shell复制代码vim test1.txt
1
2
3
4
shell复制代码1,张三,未知,2021-08-03 15:05:51
2,李四,女,2021-08-02 15:05:51
3,赵六,男,2021-08-01 15:05:51
4,王五,男,2021-07-01 15:05:51
1
2
3
4
5
6
7
8
9
10
shell复制代码sqoop export \
--connect jdbc:mysql://10.0.10.118:3306/sqoop_test \
--username root \
--password 123456 \
--table sqoop_test \
--num-mappers 1 \
--export-dir /user/test1.txt \
--input-fields-terminated-by "," \
--update-key id \
--update-mode updateonly

updateonly模式:只会更新已经存在的数据,不会执行insert增加新的数据

6. 增量导出

1
shell复制代码vim test3.txt
1
2
3
4
tex复制代码1,张三,女,2021-08-03 15:05:51
2,李四,女,2021-08-02 15:05:51
3,赵六,男,2021-08-01 15:05:51
4,王五,男,2021-07-01 15:05:51
1
shell复制代码sudo -u hdfs hdfs dfs -put ./test3.txt /user/
1
2
3
4
5
6
7
8
9
10
shell复制代码sqoop export \
--connect jdbc:mysql://10.0.10.118:3306/sqoop_test \
--username root \
--password 123456 \
--table sqoop_test \
--num-mappers 1 \
--export-dir /user/test3.txt \
--input-fields-terminated-by "," \
--update-key id \
--update-mode allowinsert

allowinsert模式: 更新已经存在的数据,添加不存在的数据

五、脚本导出(导入同理)

1
shell复制代码vim sqp_export.opt

脚本信息:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
shell复制代码export
--connect
jdbc:mysql://10.0.10.118:3306/sqoop_test
--username
root
--password
123456
--table
sqoop_test
--num-mappers
1
--export-dir
/user/hive/warehouse/sqoop_test_hive
--input-fields-terminated-by
"\t"

执行命令:

1
shell复制代码sudo -u hdfs sqoop --options-file ./sqp_export.opt

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

10分钟学会Lua语法——跟着官方的小例子

发表于 2021-08-10

Lua是一个简单轻量的脚本语言,例子来自Lua官方的小例子,全部学会就会用啦

– Example 1 – Helloworld

用print就可以打印啦

1
2
3
4
5
6
7
8
9
lua复制代码-- Classic hello program.

print("helloworld")



-------- Output ------

hello

– Example 2 – 注释

注释有两种啦,两个横线,多行注释要用两个方括号

1
2
3
4
5
6
7
8
lua复制代码-- Single line comments in Lua start with double hyphen.

--[[ Multiple line comments start
with double hyphen and two square brackets.
and end with two square brackets. ]]

-- And of course this example produces no
-- output, since it's all comments!

– Example 3 – 变量.

变量不用声明哒,直接用,而且没有类型 hhh 类似于py叭

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
lua复制代码-- Variables hold values which have types, variables don't have types.

a=1
b="abc"
c={}
d=print

print(type(a))
print(type(b))
print(type(c))
print(type(d))


-------- Output ------

number
string
table
function

– Example 4 – 变量名.

注:和C语言一样

1
2
3
4
5
6
lua复制代码-- Variable names consist of letters, digits and underscores.
-- They cannot start with a digit.

one_two_3 = 123 -- is valid varable name

-- 1_two_3 is not a valid variable name.

变量名可以用 字母 数字 and 下划线

但是不可以使用数字开头

– Example 5 保留字.

就是下划线开头的

1
2
3
4
5
6
7
8
9
10
11
12
13
lua复制代码-- The underscore is typically used to start special values
-- like _VERSION in Lua.

print(_VERSION)

-- So don't use variables that start with _,
-- but a single underscore _ is often used as a
-- dummy variable.


-------- Output ------

Lua 5.1

下划线是保留字使用的开头,变量不能使用_开头

– Example 6 – 区分大小写.

不用解释

1
2
3
4
5
6
7
8
9
10
11
12
13
lua复制代码-- Lua is case sensitive so all variable names & keywords
-- must be in correct case.

ab=1
Ab=2
AB=3
print(ab,Ab,AB)



-------- Output ------

1 2 3

区分大小写

– Example 7 – 保留的关键字.

大写的不是关键字

1
2
3
4
5
6
7
8
9
10
11
12
13
14
lua复制代码-- Lua reserved words are: and, break, do, else, elseif,
-- end, false, for, function, if, in, local, nil, not, or,
-- repeat, return, then, true, until, while.

-- Keywords cannot be used for variable names,
-- 'and' is a keyword, but AND is not, so it is a legal variable name.
AND=3
print(AND)



-------- Output ------

3

– Example 8 – 字符串

3种, 单引号、双引号和多行(多行包含换行)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
lua复制代码a="single 'quoted' string and double \"quoted\" string inside"
b='single \'quoted\' string and double "quoted" string inside'
c= [[ multiple line
with 'single'
and "double" quoted strings inside.]]

print(a)
print(b)
print(c)


-------- Output ------

single 'quoted' string and double "quoted" string inside
single 'quoted' string and double "quoted" string inside
multiple line
with 'single'
and "double" quoted strings inside.

3种字符串的表示方法

– Example 9 – 奇怪的赋值方式是支持的

1
2
3
4
5
6
lua复制代码-- a,b = b,a is valid
a,b,c,d,e = 1,2,3,'four','five'
print(a,b,c,d,e)

-------- Output ------
1 2 3 four five

– Example 10 – 奇怪的赋值方式可以交换变量

1
2
3
4
5
6
7
8
9
10
lua复制代码-- Multiple assignments allows one line to swap two variables.

print(a,b)
a,b=b,a
print(a,b)

-------- Output ------

1 2
2 1

– Example 11 – 数字

用两个点可以连接字符串和数字

1
2
3
4
5
6
7
8
9
10
11
12
lua复制代码-- Multiple assignment showing different number formats.
-- Two dots (..) are used to concatenate strings (or a
-- string and a number).

a,b,c,d,e = 1, 1.123, 1E9, -123, .0008
print("a="..a, "b="..b, "c="..c, "d="..d, "e="..e)



-------- Output ------

a=1 b=1.123 c=1000000000 d=-123 e=0.0008

– Example 12 – print可以不写括号?

1
2
3
4
5
6
7
8
9
10
11
lua复制代码-- More writing output.

print "Hello from Lua!"
print("Hello from Lua!")



-------- Output ------

Hello from Lua!
Hello from Lua!

– Example 13 – 可以用stdout嗷

1
2
3
4
5
6
7
8
9
10
11
12
13
lua复制代码-- io.write writes to stdout but without new line.

io.write("Hello from Lua!")
io.write("Hello from Lua!")

-- Use an empty print to write a single new line.
print()



-------- Output ------

Hello from Lua!Hello from Lua!

io.write可以不换行 空的print可以输出一个新行

– Example 14 – 数组.

数组,官方文档写的是Tables,可以用下标来访问

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
lua复制代码-- Simple table creation.

a={} -- {} creates an empty table
b={1,2,3} -- creates a table containing numbers 1,2,3
c={"a","b","c"} -- creates a table containing strings a,b,c
print(a,b,c) -- tables don't print directly, we'll get back to this!!


-------- Output ------

table: 008A48A8 table: 008A4420 table: 008A4768


--在 Lua 里表的默认初始索引一般以 1 开始。
> b = {4,5,6,7,8}
> print(b)
table: 009B9978
> print(b[0])
nil
> print(b[1])
4
> print(b[2])
5

– Example 15 下标可以是字符串

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
lua复制代码-- Associate index style.

address={} -- empty address
address.Street="Wyman Street"
address.StreetNumber=360
address.AptNumber="2a"
address.City="Watertown"
address.State="Vermont"
address.Country="USA"

print(address.StreetNumber, address["AptNumber"])



-------- Output ------

360 2a

– Example 16 – if statement.

记得有then和end

1
2
3
4
5
6
7
8
9
10
11
lua复制代码-- Simple if.

a=1
if a==1 then
print ("a is one")
end


-------- Output ------

a is one

– Example 17 – if else statement.

if else end

1
2
3
4
5
6
7
8
9
10
11
lua复制代码b="happy"
if b=="sad" then
print("b is sad")
else
print("b is not sad")
end


-------- Output ------

b is not sad

if then else end

– Example 18 – if elseif else statement

多分支

1
2
3
4
5
6
7
8
9
10
11
12
13
lua复制代码c=3
if c==1 then
print("c is 1")
elseif c==2 then
print("c is 2")
else
print("c isn't 1 or 2, c is "..tostring(c))
end


-------- Output ------

c isn't 1 or 2, c is 3

if then elseif then else end

– Example 19 – 条件赋值?

有点像C的三目运算符

相当于b = ( a == 1) ? "one": "not one";

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
lua复制代码-- value = test and x or y

a=1
b=(a==1) and "one" or "not one"
print(b)

-- is equivalent to
a=1
if a==1 then
b = "one"
else
b = "not one"
end
print(b)


-------- Output ------

one
one

– Example 20 – while

while do end (~=就是C语言的!=)

1
2
3
4
5
6
7
8
9
10
lua复制代码a=1
while a~=5 do -- Lua uses ~= to mean not equal
a=a+1
io.write(a.." ")
end


-------- Output ------

2 3 4 5

– Example 21 – repeat until statement.

do while?

1
2
3
4
5
6
7
8
9
10
11
12
13
14
lua复制代码a=0
repeat
a=a+1
print(a)
until a==5


-------- Output ------

1
2
3
4
5

– Example 22 – for循环

for [1,4] do end

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
lua复制代码-- Numeric iteration form.

-- Count from 1 to 4 by 1.
for a=1,4 do io.write(a) end

print()

-- Count from 1 to 6 by 3.
for a=1,6,3 do io.write(a) end


-------- Output ------

1234
14

for do end

– Example 23 – foreach?.

for还能这样用*(foreach?)

1
2
3
4
5
6
7
8
9
10
11
lua复制代码-- Sequential iteration form.

for key,value in pairs({1,2,3,4}) do print(key, value) end


-------- Output ------

1 1
2 2
3 3
4 4

– Example 24 – 用pairs打印数组.

pairs?

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
lua复制代码-- Simple way to print tables. ##  iterator

a={1,2,3,4,"five","elephant", "mouse"}

for i,v in pairs(a) do print(i,v) end


-------- Output ------

1 1
2 2
3 3
4 4
5 five
6 elephant
7 mouse

– Example 25 – break跳出循环

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
lua复制代码-- break is used to exit a loop.
a=0
while true do
a=a+1
if a==10 then
break
end
end

print(a)


-------- Output ------

10

Press 'Enter' key for next example

– Example 26 – 函数.

1
2
3
4
5
6
7
8
9
10
11
12
lua复制代码-- Define a function without parameters or return value.
function myFirstLuaFunction()
print("My first lua function was called")
end

-- Call myFirstLuaFunction.
myFirstLuaFunction()


-------- Output ------

My first lua function was called

– Example 27 – 带返回值的函数

1
2
3
4
5
6
7
8
lua复制代码-- Define a function with a return value.
function mySecondLuaFunction()
return "string from my second function"
end

-- Call function returning a value.
a=mySecondLuaFunction("string")
print(a)

– Example 28 – 返回一堆值

1
2
3
4
5
6
7
8
9
10
11
12
lua复制代码-- Define function with multiple parameters and multiple return values.
function myFirstLuaFunctionWithMultipleReturnValues(a,b,c)
return a,b,c,"My first lua function with multiple return values", 1, true
end

a,b,c,d,e,f = myFirstLuaFunctionWithMultipleReturnValues(1,2,"three")
print(a,b,c,d,e,f)


-------- Output ------

1 2 three My first lua function with multiple return values 1true

– Example 29 –局部变量 local

默认变量就是全局变量,局部变量要用local声明

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
lua复制代码-- All variables are global in scope by default.

b="global"

-- To make local variables you must put the keyword 'local' in front.
function myfunc()
local b=" local variable"
a="global variable"
print(a,b)
end

myfunc()
print(a,b)


-------- Output ------

global variable local variable
global variable global

All variables are global in scope by default.

– Example 30 – 格式化输出printf

1
2
3
4
5
6
7
8
9
10
11
12
13
lua复制代码-- An implementation of printf.

function printf(fmt, ...)
io.write(string.format(fmt, ...))
end

printf("Hello %s from %s on %s\n",
os.getenv"USER" or "there", _VERSION, os.date())


-------- Output ------

Hello there from Lua 5.1 on 08/05/19 09:34:49

– Example 31 标准库?

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
lua复制代码--[[

Standard Libraries

Lua has standard built-in libraries for common operations in
math, string, table, input/output & operating system facilities.

External Libraries

Numerous other libraries have been created: sockets, XML, profiling,
logging, unittests, GUI toolkits, web frameworks, and many more.

]]



-------- Output ------

– Example 32 – math库.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
lua复制代码-- Math functions:
-- math.abs, math.acos, math.asin, math.atan, math.atan2,
-- math.ceil, math.cos, math.cosh, math.deg, math.exp, math.floor,
-- math.fmod, math.frexp, math.huge, math.ldexp, math.log, math.log10,
-- math.max, math.min, math.modf, math.pi, math.pow, math.rad,
-- math.random, math.randomseed, math.sin, math.sinh, math.sqrt,
-- math.tan, math.tanh

print(math.sqrt(9), math.pi)


-------- Output ------

3 3.1415926535898

– Example 33 – string库.

1
2
3
4
5
6
7
8
9
10
11
lua复制代码-- String functions:
-- string.byte, string.char, string.dump, string.find, string.format,
-- string.gfind, string.gsub, string.len, string.lower, string.match,
-- string.rep, string.reverse, string.sub, string.upper

print(string.upper("lower"),string.rep("a",5),string.find("abcde", "cd"))


-------- Output ------

LOWER aaaaa 3 4

– Example 34 – table库.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
lua复制代码-- Table functions:
-- table.concat, table.insert, table.maxn, table.remove, table.sort

a={2}
table.insert(a,3);
table.insert(a,4);
table.sort(a,function(v1,v2) return v1 > v2 end)
for i,v in ipairs(a) do print(i,v) end


-------- Output ------

1 4
2 3
3 2

– Example 35 –input/output库.

1
2
3
4
5
6
7
8
9
10
11
12
lua复制代码-- IO functions:
-- io.close , io.flush, io.input, io.lines, io.open, io.output, io.popen,
-- io.read, io.stderr, io.stdin, io.stdout, io.tmpfile, io.type, io.write,
-- file:close, file:flush, file:lines ,file:read,
-- file:seek, file:setvbuf, file:write

print(io.open("file doesn't exist", "r"))


-------- Output ------

nil file doesn't exist: No such file or directory 2

– Example 36 – os库

1
2
3
4
5
6
7
8
9
10
lua复制代码-- OS functions:
-- os.clock, os.date, os.difftime, os.execute, os.exit, os.getenv,
-- os.remove, os.rename, os.setlocale, os.time, os.tmpname

print(os.date())


-------- Output ------

08/05/19 09:36:36

– Example 37 – 外部库

外部的库需要用require导入

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
lua复制代码-- Lua has support for external modules using the 'require' function
-- INFO: A dialog will popup but it could get hidden behind the console.

require( "iuplua" )
ml = iup.multiline
{
expand="YES",
value="Quit this multiline edit app to continue Tutorial!",
border="YES"
}
dlg = iup.dialog{ml; title="IupMultiline", size="QUARTERxQUARTER",}
dlg:show()
print("Exit GUI app to continue!")
iup.MainLoop()


-------- Output ------

failed to load & run sample code
error loading module 'iuplua' from file 'C:\Dev\Lua\clibs\iuplua51.dll':
The specified module could not be found.

– Example 38 关于例子

1
2
3
4
5
6
7
8
9
10
11
12
13
lua复制代码--[[

To learn more about Lua scripting see

Lua Tutorials: http://lua-users.org/wiki/TutorialDirectory

"Programming in Lua" Book: http://www.inf.puc-rio.br/~roberto/pil2/

Lua 5.1 Reference Manual:
Start/Programs/Lua/Documentation/Lua 5.1 Reference Manual

Examples: Start/Programs/Lua/Examples
]]

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

JAVA对象头结构详解 JAVA对象头结构详解

发表于 2021-08-10

image.png

JAVA对象头结构详解

这是我参与8月更文挑战的第10天,活动详情查看:8月更文挑战

📚前言

大家有没有思考过,java中的对象除了我们自定义的一些属性外,会不会还存在某些我们不了解的东西呢,比如synchronized锁为对象的时候,那么是如何记录锁状态呢?接下来将带领大家解析下对象的神奇之处!

🥠对象内存布局

对象除了我们自定义的一些属性外,在HotSpot虚拟机中,对象在内存中还可以分为三个区域:对象头,实例数据,对齐填充,这三个区域组成起来才是一个完整的对象!

  • 对象头:看文章下方解释
  • 实例数据: 存放类的属性数据信息,包括父类的属性信息
  • 由于虚拟机要求 对象起始地址必须是8字节的整数倍。填充数据不是必须存在的,仅仅是为了字节对齐

image.png

🍬对象头

对象头中存储了对象是很多java内部的信息,如hash码,对象所属的年代,对象锁,锁状态标志,偏向锁(线程)ID,偏向时间等,Java对象头一般占有2个机器码

PS:在32位虚拟机中,1个机器码等于4字节,也就是32bit,在64位虚拟机中,1个机器码是8个字节,也就是64bit!

如果对象是数组类型,则需要3个机器码,因为JVM虚拟机可以通过Java对象的元数据信息确定Java对象的大小,但是无法从数组的元数据来确认数组的大小,所以用一块区域用来记录数组长度。
HotSpot虚拟机的对象头包括两部分信息,第一部分为Mark Word,第二部分为class pointer,如果是数组对象,那么还有数组长度!

image.png

🍭Mark Word

Mark Word用于存储对象自身的运行时数据,如哈希码、GC分代年龄、锁状态标志、线程持有的锁、偏向线程ID、偏向时间戳等等。,这部分数据的长度在32位和64位的虚拟机(暂 不考虑开启压缩指针的场景)中分别为32个和64个Bits,官方称它为“Mark Word”。对象需要存储的运行时数据很多,其实已经超出了32、64位Bitmap结构所能记录的限度,但是对象头信息是与对象自身定的数据无关的额外存储成本,考虑到虚拟机的空间效率,Mark Word被设计成一个非固定的数据结构以便在极小的空间内存储尽量多的信息,它会根据对象的状态复用自己的存储空间。例如在32位的HotSpot虚拟机 中对象未被锁定的状态下,Mark Word的32个Bits空间中的25Bits用于存储对象哈希码(HashCode),4Bits用于存储对象分代年龄,2Bits用于存储锁标志位,1Bit固定为0,在其他状态(轻量级锁定、重量级锁定、GC标记、可偏向)下对象的存储内容如下表所示。

32位虚拟机

image.png

64位虚拟机

🍓实践操作

接下来运用JOL分析运行时对象头状态分析:

引入下方jolmaven依赖

1
2
3
4
5
6
xml复制代码<dependency>
<groupId>org.openjdk.jol</groupId>
<artifactId>jol-core</artifactId>
<version>0.16</version>
<scope>provided</scope>
</dependency>

输出对象状态信息

1
csharp复制代码System.out.println( ClassLayout.parseInstance(object).toPrintable() );

image.png


🥚总结:

今天的对象内存解析就到这了,想更深入理解的小伙伴,可以去阅读下周志明老师的《深入理解Java虚拟机》,这本书里对JVM的一个解析的想当透彻了!

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

lombok Builder踩坑系列 - 构造方法和默认值

发表于 2021-08-10

这篇文章主要向大家介绍lombok @Builder踩坑系列 - 构造方法和默认值问题,主要内容包括基础应用、实用技巧、原理机制等方面,希望对大家有所帮助。

问题1:@Data和@Builder致使无参构造丢失

现象

  • 单独使用@Data注解,是会生成无参数构造方法。
  • 单独使用@Builder注解,发现生成了全属性的构造方法。
  • @Data和@Builder一块儿用:咱们发现没有了默认的构造方法。若是手动添加无参数构造方法或者用@NoArgsConstructor注解都会报错!

两种解决方法

1. 构造方法加上@Tolerate 注解,让lombok伪装它不存在(不感知)

1
2
3
4
5
6
7
8
9
less复制代码@Builder
@Data
public class TestLombok {

@Tolerate
TestLombok() {
}
......
}

2. 直接加上这4个注解

1
2
3
4
5
6
7
less复制代码@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class TestLombok {
......
}

问题2:@Builder注解致使默认值无效

现象

使用Lombok注解能够极高的简化代码量,比较好用的注解除了@Data以外,还有@Builder这个注解,它可让你很方便的使用builder模式构建对象,可是今天发现@Builder注解会把对象的默认值清掉。java

1
2
3
4
5
6
7
8
9
10
11
12
less复制代码@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class TestLombok {

private String aa = "zzzz";

public static void main(String[] args) {
TestLombok build = TestLombok.builder().build();
System.out.println(build);
}

输出:TestLombok(aa=null)app

解决方案

只须要在字段上面加上@Builder.Default注解便可ui

1
2
ini复制代码@Builder.Default
private String aa = "zzzz";

缘由分析

咱们使用注解的方式,底层本质就是反射帮咱们生成了一系列的setter、getter,因此咱们直接打开编译后的target包下面的.class文件,上面的全部缘由一目了然!

源文件:this

1
2
3
4
5
6
7
8
9
10
11
12
13
less复制代码@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class TestLombok {

private String aa = "zzzz";

public static void main(String[] args) {
TestLombok build = TestLombok.builder().build();
System.out.println(build);
}
}

对应的class字节码:code

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
kotlin复制代码//
// Source code recreated from a .class file by IntelliJ IDEA
// (powered by Fernflower decompiler)
//

package com.apple.ucar;

public class TestLombok {
private String aa = "zzzz";

public static void main(String[] args) {
TestLombok build = builder().build();
System.out.println(build);
}

public static TestLombok.TestLombokBuilder builder() {
return new TestLombok.TestLombokBuilder();
}

public String getAa() {
return this.aa;
}

public void setAa(String aa) {
this.aa = aa;
}

public boolean equals(Object o) {
if (o == this) {
return true;
} else if (!(o instanceof TestLombok)) {
return false;
} else {
TestLombok other = (TestLombok)o;
if (!other.canEqual(this)) {
return false;
} else {
Object this$aa = this.getAa();
Object other$aa = other.getAa();
if (this$aa == null) {
if (other$aa != null) {
return false;
}
} else if (!this$aa.equals(other$aa)) {
return false;
}

return true;
}
}
}

protected boolean canEqual(Object other) {
return other instanceof TestLombok;
}

public int hashCode() {
int PRIME = true;
int result = 1;
Object $aa = this.getAa();
int result = result * 59 + ($aa == null ? 43 : $aa.hashCode());
return result;
}

public String toString() {
return "TestLombok(aa=" + this.getAa() + ")";
}

public TestLombok() {
}

public TestLombok(String aa) {
this.aa = aa;
}

public static class TestLombokBuilder {
private String aa;

TestLombokBuilder() {
}

public TestLombok.TestLombokBuilder aa(String aa) {
this.aa = aa;
return this;
}

public TestLombok build() {
return new TestLombok(this.aa);
}

public String toString() {
return "TestLombok.TestLombokBuilder(aa=" + this.aa + ")";
}
}
}

咱们想知道@Data、@Builder等注解底层到底作了什么,直接编译当前文件,便可在生成的.class字节码文件查看具体代码便知道了对象

好比上述第二点,采用@Builder的时候,这个aa并无默认值,因此会为空!!get

1
2
3
4
kotlin复制代码  public TestLombok.TestLombokBuilder aa(String aa) {
this.aa = aa;
return this;
}

总结

我的以为若是想要使用@Builder,最简单的方法就是直接写上这4个注解,有默认值的话再加上@Builder.Default直接,正常状况下就没啥问题了!hash

1
2
3
4
5
6
7
8
9
10
11
12
13
14
less复制代码@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class TestLombok {

@Builder.Default
private String aa = "zzzz";

public static void main(String[] args) {
TestLombok build = TestLombok.builder().build();
System.out.println(build);
}
}

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

1…570571572…956

开发者博客

9558 日志
1953 标签
RSS
© 2025 开发者博客
本站总访问量次
由 Hexo 强力驱动
|
主题 — NexT.Muse v5.1.4
0%