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

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


  • 首页

  • 归档

  • 搜索

StringBuilder 和 StringBuffer的区

发表于 2021-05-10

本文正在参加「Java主题月 - Java Debug笔记活动」,详情查看<活动链接>

提问

StringBuffer 和 StringBuilder 主要有哪些区别呢?它们之间是否存在一些性能方面的讨论话题呢?

回答1

StringBuffer 是同步的,而 StringBuilder 却不是。

StringBuilder 主要为了解决 StringBuilder 的异步问题。

在大部分情况下,是几乎不需要同步的。如果有人想使用 StringBuilder 进行同步,则只需在实例上使用同步代码块 {}。

我赞同上述的说法,认为 StringBuffer 不是一个好的方式(除非你必须使用它的 API)。

对于那些将这两者混合使用的人来说,好的记忆方法是:Buffer,是较旧的版本,因此是同步的。较新的 Builder 类使用 Builder 模式,则是异步的。

但是,StringBuffer 比 StringBuilder 慢。您可以根据您的应用程序来选择对应的类型。

另一个区别是 StringBuffer 可以与 Matcher#appendReplacement 一起使用,而 StringBuilder 却不能一起使用。这是一个非常烦人的 API 差异,尤其是因为 Matcher 它不是线程安全的,因此不需要进行 appendReplacement 同步。

关于 StringBuilder 的多线程环境中失败的更多细节,可以参考:stackoverflow.com/questions/3…

回答2

StringBuilder 比 StringBuffer 快,这是因为 StringBuilder 不是 synchronized的。

具体可看下面这个例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
java复制代码public class Main {
public static void main(String[] args) {
int N = 77777777;
long t;

{
StringBuffer sb = new StringBuffer();
t = System.currentTimeMillis();
for (int i = N; i --> 0 ;) {
sb.append("");
}
System.out.println(System.currentTimeMillis() - t);
}

{
StringBuilder sb = new StringBuilder();
t = System.currentTimeMillis();
for (int i = N; i > 0 ; i--) {
sb.append("");
}
System.out.println(System.currentTimeMillis() - t);
}
}
}

运行结果是 StringBuffer 耗时 2241ms,StringBuilder 耗时 753ms。

翻译内容来源Stack Overflow:stackoverflow.com/questions/3…

本文转载自: 掘金

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

Nacos中已经有Optional使用案例了,是时候慎重对待

发表于 2021-05-10

前言

Java 8提供了很多新特性,但很多朋友对此并不重视,依旧采用老的写法。最近个人在大量阅读开源框架的源码,发现Java 8的很多API已经被频繁的使用了。

以Nacos框架为例,已经有很典型的Optional使用案例了,而且场景把握的非常好。如果此时你还没意识到要学习了解一下,以后看源代码可能都有些费劲了。

今天这篇文章我们就基于Nacos中对Optional的使用作为案例,来深入讲解一下Optional的使用。老规矩,源码、案例、实战,一样不少。

Nacos中的Optional使用

在Nacos中有这样一个接口ConsistencyService,用来定义一致性服务的,其中的一个方法返回的类型便是Optional:

1
2
3
4
5
6
vbnet复制代码/**
* Get the error message of the consistency protocol.
*
* @return the consistency protocol error message.
*/
Optional<String> getErrorMsg();

如果你对Optional不了解,看到这里可能就会有点蒙。那我们来看看Nacos是怎么使用Optional的。在上述接口的一个实现类PersistentServiceProcessor中是如此实现的:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
ini复制代码@Override
public Optional<String> getErrorMsg() {
String errorMsg;
if (hasLeader && hasError) {
errorMsg = "The raft peer is in error: " + jRaftErrorMsg;
} else if (hasLeader && !hasError) {
errorMsg = null;
} else if (!hasLeader && hasError) {
errorMsg = "Could not find leader! And the raft peer is in error: " + jRaftErrorMsg;
} else {
errorMsg = "Could not find leader!";
}
return Optional.ofNullable(errorMsg);
}

也就是根据hasLeader和hasError两个变量来确定返回的errorMsg信息是什么。最后将errorMsg封装到Optional中进行返回。

下面再看看方法getErrorMsg是如何被调用的:

1
2
3
4
5
6
scss复制代码String errorMsg;
if (ephemeralConsistencyService.getErrorMsg().isPresent()
&& persistentConsistencyService.getErrorMsg().isPresent()) {
errorMsg = "'" + ephemeralConsistencyService.getErrorMsg().get() + "' in Distro protocol and '"
+ persistentConsistencyService.getErrorMsg().get() + "' in jRaft protocol";
}

可以看到在使用时只用先调用返回的Optional的isPresent方法判断是否存在,再调用其get方法获取即可。此时你可以回想一下如果不用Optional该如何实现。

到此,你可能有所疑惑用法,没关系,下面我们就开始逐步讲解Option的使用、原理和源码。

Optional的数据结构

面对新生事物我们都会有些许畏惧,当我们庖丁解牛似的将其拆分之后,了解其实现原理,就没那么恐怖了。

查看Optional类的源码,可以看到它有两个成员变量:

1
2
3
4
5
6
7
8
9
10
11
12
php复制代码public final class Optional<T> {
/**
* Common instance for {@code empty()}.
*/
private static final Optional<?> EMPTY = new Optional<>(null);

/**
* If non-null, the value; if null, indicates no value is present
*/
private final T value;
// ...
}

其中EMPTY变量表示的是如果创建一个空的Optional实例,很显然,在加载时已经初始化了。而value是用来存储我们业务中真正使用的对象,比如上面的errorMsg就是存储在这里。

看到这里你是否意识到Optional其实就一个容器啊!对的,将Optional理解为容器就对了,然后这个容器呢,为我们封装了存储对象的非空判断和获取的API。

看到这里,是不是感觉Optional并没那么神秘了?是不是也没那么恐惧了?

而Java 8之所以引入Optional也是为了解决对象使用时为避免空指针异常的丑陋写法问题。类似如下代码:

1
2
3
4
5
6
ini复制代码if( user != null){
Address address = user.getAddress();
if(address != null){
String province = address.getProvince();
}
}

原来是为了封装,原来是为了更优雅的代码,这不正是我们有志向的程序员所追求的么。

如何将对象存入Optional容器中

这么我们就姑且称Optional为Optional容器了,下面就看看如何将对象放入Optional当中。

看到上面的EMPTY初始化时调用了构造方法,传入null值,我们是否也可以这样来封装对象?好像不行,来看一下Optional的构造方法:

1
2
3
4
5
6
7
csharp复制代码private Optional() {
this.value = null;
}

private Optional(T value) {
this.value = Objects.requireNonNull(value);
}

存在的两个构造方法都是private的,看来只能通过Optional提供的其他方法来封装对象了,通常有以下方式。

empty方法

empty方法源码如下:

1
2
3
4
5
swift复制代码public static<T> Optional<T> empty() {
@SuppressWarnings("unchecked")
Optional<T> t = (Optional<T>) EMPTY;
return t;
}

简单直接,直接强转EMPTY对象。

of方法

of方法源码如下:

1
2
3
4
csharp复制代码// Returns an {@code Optional} with the specified present non-null value.
public static <T> Optional<T> of(T value) {
return new Optional<>(value);
}

注释上说是为非null的值创建一个Optional,而非null的是通过上面构造方法中的Objects.requireNonNull方法来检查的:

1
2
3
4
5
typescript复制代码public static <T> T requireNonNull(T obj) {
if (obj == null)
throw new NullPointerException();
return obj;
}

也就是说如果值为null,则直接抛空指针异常。

ofNullable方法

ofNullable方法源码如下:

1
2
3
scss复制代码public static <T> Optional<T> ofNullable(T value) {
return value == null ? empty() : of(value);
}

ofNullable为指定的值创建一个Optional,如果指定的值为null,则返回一个空的Optional。也就是说此方法支持对象的null与非null构造。

回顾一下:Optional构造方法私有,不能被外部调用;empty方法创建空的Optional、of方法创建非空的Optional、ofNullable将两者结合。是不是so easy?

此时,有朋友可能会问,相对于ofNullable方法,of方法存在的意义是什么?在运行过程中,如果不想隐藏NullPointerException,就是说如果出现null则要立即报告,这时就用Of函数。另外就是已经明确知道value不会为null的时候也可以使用。

判断对象是否存在

上面已经将对象放入Optional了,那么在获取之前是否需要能判断一下存放的对象是否为null呢?

isPresent方法

上述问题,答案是:可以的。对应的方法就是isPresent:

1
2
3
typescript复制代码public boolean isPresent() {
return value != null;
}

实现简单直白,相当于将obj != null的判断进行了封装。该对象如果存在,方法返回true,否则返回false。

isPresent即判断value值是否为空,而ifPresent就是在value值不为空时,做一些操作:

1
2
3
4
typescript复制代码public void ifPresent(Consumer<? super T> consumer) {
if (value != null)
consumer.accept(value);
}

如果Optional实例有值则为其调用consumer,否则不做处理。可以直接将Lambda表达式传递给该方法,代码更加简洁、直观。

1
2
ini复制代码Optional<String> opt = Optional.of("程序新视界");
opt.ifPresent(System.out::println);

获取值

当我们判断Optional中有值时便可以进行获取了,像Nacos中使用的那样,调用get方法:

1
2
3
4
5
6
csharp复制代码public T get() {
if (value == null) {
throw new NoSuchElementException("No value present");
}
return value;
}

很显然,如果value值为null,则该方法会抛出NoSuchElementException异常。这也是为什么我们在使用时要先调用isPresent方法来判断一下value值是否存在了。此处的设计稍微与初衷相悖。

看一下使用示例:

1
2
3
4
5
ini复制代码String name = null;
Optional<String> opt = Optional.ofNullable(name);
if(opt.isPresent()){
System.out.println(opt.get());
}

设置(或获取)默认值

那么,针对上述value为null的情况是否有解决方案呢?我们可以配合设置(或获取)默认值来解决。

orElse方法

orElse方法:如果有值则将其返回,否则返回指定的其它值。

1
2
3
typescript复制代码public T orElse(T other) {
return value != null ? value : other;
}

可以看到是get方法的加强版,get方法如果值为null直接抛异常,orElse则不,如果只为null,返回你传入进来的参数值。

使用示例:

1
2
3
vbnet复制代码Optional<Object> o1 = Optional.ofNullable(null);
// 输出orElse指定值
System.out.println(o1.orElse("程序新视界"));

orElseGet方法

orElseGet:orElseGet与orElse方法类似,区别在于得到的默认值。orElse方法将传入的对象作为默认值,orElseGet方法可以接受Supplier接口的实现用来生成默认值:

1
2
3
typescript复制代码public T orElseGet(Supplier<? extends T> other) {
return value != null ? value : other.get();
}

当value为null时orElse直接返回传入值,orElseGet返回Supplier实现类中定义的值。

1
2
3
ini复制代码String name = null;
String newName = Optional.ofNullable(name).orElseGet(()->"程序新视界");
System.out.println(newName); // 输出:程序新视界

其实上面的示例可以直接优化为orElse,因为Supplier接口的实现依旧是直接返回输入值。

orElseThrow方法

orElseThrow:如果有值则将其返回,否则抛出Supplier接口创建的异常。

1
2
3
4
5
6
7
java复制代码public <X extends Throwable> T orElseThrow(Supplier<? extends X> exceptionSupplier) throws X {
if (value != null) {
return value;
} else {
throw exceptionSupplier.get();
}
}

使用示例:

1
2
3
4
5
6
php复制代码Optional<Object> o = Optional.ofNullable(null);
try {
o.orElseThrow(() -> new Exception("异常"));
} catch (Exception e) {
System.out.println(e.getMessage());
}

学完上述内容,基本上已经掌握了Optional百分之八十的功能了。同时,还有两个相对高级点的功能:过滤值和转换值。

filter方法过滤值

Optional中的值我们可以通过上面讲的到方法进行获取,但在某些场景下,我们还需要判断一下获得的值是否符合条件。笨办法时,获取值之后,自己再进行检查判断。

当然,也可以通过Optional提供的filter来进行取出前的过滤:

1
2
3
4
5
6
7
kotlin复制代码public Optional<T> filter(Predicate<? super T> predicate) {
Objects.requireNonNull(predicate);
if (!isPresent())
return this;
else
return predicate.test(value) ? this : empty();
}

filter方法的参数类型为Predicate类型,可以将Lambda表达式传递给该方法作为条件,如果表达式的结果为false,则返回一个EMPTY的Optional对象,否则返回经过过滤的Optional对象。

使用示例:

1
2
3
ini复制代码Optional<String> opt = Optional.of("程序新视界");
Optional<String> afterFilter = opt.filter(name -> name.length() > 4);
System.out.println(afterFilter.orElse(""));

map方法转换值

与filter方法类似,当我们将值从Optional中取出之后,还进行一步转换,比如改为大写或返回长度等操作。当然可以用笨办法取出之后,进行处理。

这里,Optional为我们提供了map方法,可以在取出之前就进行操作:

1
2
3
4
5
6
7
8
typescript复制代码public<U> Optional<U> map(Function<? super T, ? extends U> mapper) {
Objects.requireNonNull(mapper);
if (!isPresent())
return empty();
else {
return Optional.ofNullable(mapper.apply(value));
}
}

map方法的参数类型为Function,会调用Function的apply方法对对Optional中的值进行处理。如果Optional中的值本身就为null,则返回空,否则返回处理过后的值。

示例:

1
2
3
ini复制代码Optional<String> opt = Optional.of("程序新视界");
Optional<Integer> intOpt = opt.map(String::length);
System.out.println(intOpt.orElse(0));

与map方法有这类似功能的方法为flatMap:

1
2
3
4
5
6
7
8
swift复制代码public<U> Optional<U> flatMap(Function<? super T, Optional<U>> mapper) {
Objects.requireNonNull(mapper);
if (!isPresent())
return empty();
else {
return Objects.requireNonNull(mapper.apply(value));
}
}

可以看出,它与map方法的实现非常像,不同的是传入的参数类型,map函数所接受的入参类型为Function<? super T, ? extends U>,而flapMap的入参类型为Function<? super T, Optional>。

flapMap示例如下:

1
2
3
ini复制代码Optional<String> opt = Optional.of("程序新视界");
Optional<Integer> intOpt = opt.flatMap(name ->Optional.of(name.length()));
System.out.println(intOpt.orElse(0));

对照map的示例,可以看出在flatMap中对结果进行了一次Optional#of的操作。

小结

本文我们从Nacos中使用Optional的使用出发,逐步剖析了Optional的源码、原理和使用。此时再回头看最初的示例是不是已经豁然开朗了?

关于Optional的学习其实把握住本质就可以了:Optional本质上是一个对象的容器,将对象存入其中之后,可以帮我们做一些非空判断、取值、过滤、转换等操作。

理解了本质,如果哪个API的使用不确定,看一下源码就可以了。此时,可以愉快的继续看源码了~

博主简介:《SpringBoot技术内幕》技术图书作者,酷爱钻研技术,写技术干货文章。

公众号:「程序新视界」,博主的公众号,欢迎关注~

技术交流:请联系博主微信号:zhuan2quan

本文转载自: 掘金

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

大数据开发-Flink-数据流DataStream和Data

发表于 2021-05-10

Flink主要用来处理数据流,所以从抽象上来看就是对数据流的处理,正如前面大数据开发-Flink-体系结构 && 运行架构提到写Flink程序实际上就是在写DataSource、Transformation、Sink.

  • DataSource是程序的数据源输入,可以通过StreamExecutionEnvironment.addSource(sourceFuntion)为程序
    添加一个数据源
  • Transformation是具体的操作,它对一个或多个输入数据源进行计算处理,比如Map、FlatMap和Filter等操作
  • Sink是程序的输出,它可以把Transformation处理之后的数据输出到指定的存储介质中

DataStream的三种流处理Api

DataSource

Flink针对DataStream提供了两种实现方式的数据源,可以归纳为以下四种:

  • 基于文件

readTextFile(path) 读取文本文件,文件遵循TextInputFormat逐行读取规则并返回

  • 基于Socket

socketTextStream 从Socket中读取数据,元素可以通过一个分隔符分开

  • 基于集合

fromCollection(Collection) 通过Java的Collection集合创建一个数据流,集合中的所有元素必须是相同类型的,需要注意的是,如果集合里面的元素要识别为POJO,需要满足下面的条件

+ 该类有共有的无参构造方法
+ 该类是共有且独立的(没有非静态内部类)
+ 类(及父类)中所有的不被static、transient修饰的属性要么有公有的(且不被final修饰),要么是包含公有的getter和setter方法,这些方法遵循java bean命名规范总结:上面的要求其实就是为了让Flink可以方便地序列化和反序列化这些对象为数据流
  • 自定义Source

使用StreamExecutionEnvironment.addSource(sourceFunction)将一个流式数据源加到程序中,具体这个sourceFunction 是为非并行源implements SourceFunction,或者为并行源 implements ParallelSourceFunction接口,或者extends RichParallelSourceFunction,对于自定义Source,Sink, Flink内置了下面几种Connector

连接器 是否提供Source支持 是否提供Sink支持
Apache Kafka 是 是
ElasticSearch 否 是
HDFS 否 是
Twitter Streaming PI 是 否

对于Source的使用,其实较简单,这里给一个较常用的自定义Source的KafaSource的使用例子。更多相关源码可以查看:

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
Java复制代码package com.hoult.stream;


public class SourceFromKafka {
public static void main(String[] args) throws Exception {
StreamExecutionEnvironment env = StreamExecutionEnvironment.getExecutionEnvironment();

String topic = "animalN";
Properties props = new Properties();
props.put("bootstrap.servers", "linux121:9092");

FlinkKafkaConsumer<String> consumer = new FlinkKafkaConsumer<>(topic, new SimpleStringSchema(), props);

DataStreamSource<String> data = env.addSource(consumer);

SingleOutputStreamOperator<Tuple2<Long, Long>> maped = data.map(new MapFunction<String, Tuple2<Long, Long>>() {
@Override
public Tuple2<Long, Long> map(String value) throws Exception {
System.out.println(value);

Tuple2<Long,Long> t = new Tuple2<Long,Long>(0l,0l);
String[] split = value.split(",");

try{
t = new Tuple2<Long, Long>(Long.valueOf(split[0]), Long.valueOf(split[1]));
} catch (Exception e) {
e.printStackTrace();
}
return t;


}
});
KeyedStream<Tuple2<Long,Long>, Long> keyed = maped.keyBy(value -> value.f0);
//按照key分组策略,对流式数据调用状态化处理
SingleOutputStreamOperator<Tuple2<Long, Long>> flatMaped = keyed.flatMap(new RichFlatMapFunction<Tuple2<Long, Long>, Tuple2<Long, Long>>() {
ValueState<Tuple2<Long, Long>> sumState;

@Override
public void open(Configuration parameters) throws Exception {
//在open方法中做出State
ValueStateDescriptor<Tuple2<Long, Long>> descriptor = new ValueStateDescriptor<>(
"average",
TypeInformation.of(new TypeHint<Tuple2<Long, Long>>() {
}),
Tuple2.of(0L, 0L)
);

sumState = getRuntimeContext().getState(descriptor);
// super.open(parameters);
}

@Override
public void flatMap(Tuple2<Long, Long> value, Collector<Tuple2<Long, Long>> out) throws Exception {
//在flatMap方法中,更新State
Tuple2<Long, Long> currentSum = sumState.value();

currentSum.f0 += 1;
currentSum.f1 += value.f1;

sumState.update(currentSum);
out.collect(currentSum);


/*if (currentSum.f0 == 2) {
long avarage = currentSum.f1 / currentSum.f0;
out.collect(new Tuple2<>(value.f0, avarage));
sumState.clear();
}*/

}
});

flatMaped.print();

env.execute();
}
}

Transformation

对于Transformation ,Flink提供了很多的算子,

  • map

DataStream → DataStream Takes one element and produces one element. A map function that doubles the values of the input stream:

1
2
3
4
5
6
7
Java复制代码DataStream<Integer> dataStream = //...
dataStream.map(new MapFunction<Integer, Integer>() {
@Override
public Integer map(Integer value) throws Exception {
return 2 * value;
}
});
  • flatMap

DataStream → DataStream Takes one element and produces zero, one, or more elements. A flatmap function that splits sentences to words:

1
2
3
4
5
6
7
8
Java复制代码dataStream.flatMap(new FlatMapFunction<String, String>() {
@Override
public void flatMap(String value, Collector<String> out) throws Exception {
for(String word: value.split(" ")){
out.collect(word);
}
}
});
  • filter

DataStream → DataStream Evaluates a boolean function for each element and retains those for which the function returns true. A filter that filters out zero values:

1
2
3
4
5
6
Java复制代码dataStream.filter(new FilterFunction<Integer>() {
@Override
public boolean filter(Integer value) throws Exception {
return value != 0;
}
});
  • keyBy

DataStream → KeyedStream Logically partitions a stream into disjoint partitions. All records with the same key are assigned to the same partition. Internally, keyBy() is implemented with hash partitioning. There are different ways to specify keys.
This transformation returns a KeyedStream, which is, among other things, required to use keyed state.

Attention A type cannot be a key if:

  • fold
  • aggregation
  • window/windowAll/window.apply/window.reduce/window.fold/window.aggregation
1
2
Java复制代码dataStream.keyBy(value -> value.getSomeKey()) // Key by field "someKey"
dataStream.keyBy(value -> value.f0) // Key by the first element of a Tuple

更多算子操作可以查看官网,官网写的很好:ci.apache.org/projects/fl…

Sink

Flink针对DataStream提供了大量的已经实现的数据目的地(Sink),具体如下所示

  • writeAsText():讲元素以字符串形式逐行写入,这些字符串通过调用每个元素的toString()方法来获取
  • print()/printToErr():打印每个元素的toString()方法的值到标准输出或者标准错误输出流中
  • 自定义输出:addSink可以实现把数据输出到第三方存储介质中, Flink提供了一批内置的Connector,其中有的Connector会提供对应的Sink支持

这里举一个常见的例子,下层到Kafka

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
Java复制代码import org.apache.flink.api.common.serialization.SimpleStringSchema;
import org.apache.flink.streaming.api.datastream.DataStreamSource;
import org.apache.flink.streaming.api.environment.StreamExecutionEnvironment;
import org.apache.flink.streaming.connectors.kafka.FlinkKafkaProducer;
public class StreamToKafka {
public static void main(String[] args) throws Exception {
StreamExecutionEnvironment env = StreamExecutionEnvironment.getExecutionEnvironment();
DataStreamSource<String> data = env.socketTextStream("teacher2", 7777);
String brokerList = "teacher2:9092";
String topic = "mytopic2";
FlinkKafkaProducer producer = new FlinkKafkaProducer(brokerList, topic, new SimpleStringSchema());
data.addSink(producer);
env.execute();
}
}

DataSet的常用Api

DataSource

对DataSet批处理而言,较为频繁的操作是读取HDFS中的文件数据,因为这里主要介绍两个DataSource组件

  • 基于集合 ,用来测试和DataStream类似
  • 基于文件 readTextFile….

Transformation


更多算子可以查看官网:ci.apache.org/projects/fl…

Sink

Flink针对DataStream提供了大量的已经实现的数据目的地(Sink),具体如下所示

  • writeAsText():将元素以字符串形式逐行写入,这些字符串通过调用每个元素的toString()方法来获取
  • writeAsCsv():将元组以逗号分隔写入文件中,行及字段之间的分隔是可配置的,每个字段的值来自对象的
  • toString()方法
  • print()/pringToErr():打印每个元素的toString()方法的值到标准输出或者标准错误输出流中
    Flink提供了一批内置的Connector,其中有的Connector会提供对应的Sink支持,如1.1节中表所示
    查看个人资料,可以关注更多。

本文转载自: 掘金

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

极光IM-java后台对接

发表于 2021-05-09

对接文档

极光IM - REST API - 极光文档 (jiguang.cn)

话不多说,贴代码

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
xml复制代码<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<parent>
<artifactId>wnews</artifactId>
<groupId>com.fd.wnews</groupId>
<version>0.0.1-SNAPSHOT</version>
</parent>
<modelVersion>4.0.0</modelVersion>

<artifactId>fd-wnews-common</artifactId>
<dependencies>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-context</artifactId>
</dependency>
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-support</artifactId>
</dependency>
<dependency>
<groupId>io.swagger</groupId>
<artifactId>swagger-annotations</artifactId>
</dependency>
<dependency>
<groupId>io.netty</groupId>
<artifactId>netty-transport</artifactId>
</dependency>
<dependency>
<groupId>com.fd.businessengine</groupId>
<artifactId>be-common-entity</artifactId>
<version>1.0.2-SNAPSHOT</version>
<scope>compile</scope>
</dependency>


<!--极光-->
<dependency>
<groupId>cn.jpush.api</groupId>
<artifactId>jpush-client</artifactId>
<version>3.2.17</version>
<scope>compile</scope>
</dependency>
<dependency>
<groupId>org.apache.httpcomponents</groupId>
<artifactId>httpclient</artifactId>
</dependency>
<dependency>
<groupId>org.apache.httpcomponents</groupId>
<artifactId>httpmime</artifactId>
<version>4.5.9</version>
</dependency>
<dependency>
<groupId>com.arronlong</groupId>
<artifactId>httpclientutil</artifactId>
<version>1.0.4</version>
</dependency>

<dependency>
<groupId>cn.jpush.api</groupId>
<artifactId>jiguang-common</artifactId>
<version>1.1.3</version>
</dependency>
<dependency>
<groupId>backport-util-concurrent</groupId>
<artifactId>backport-util-concurrent</artifactId>
<version>3.1</version>
</dependency>

<dependency>
<groupId>cn.jpush.api</groupId>
<artifactId>jmessage-client</artifactId>
<version>1.1.8</version>
</dependency>
</dependencies>

</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
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
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
java复制代码package com.fd.wnews.entity;

import io.swagger.annotations.ApiModelProperty;

import java.io.Serializable;
import java.util.Objects;

/**
* @Description 用户对象
* @Author lc
* @Date 2019-11-21 下午 4:59
*/
public class JiGuangUser implements Serializable {

private static final long serialVersionUID = 1L;

@ApiModelProperty(value = "用户登录名")
private String username;
@ApiModelProperty(value = "登录密码")
private String password;
@ApiModelProperty(value = "用户所属于的应用的appkey")
private String appkey;
@ApiModelProperty(value = "用户昵称")
private String nickname;
@ApiModelProperty(value = "生日")
private String birthday;
@ApiModelProperty(value = "头像")
private String avatar;
@ApiModelProperty(value = "性别:0-未知/1-男/2-女")
private String gender;
@ApiModelProperty(value = "用户签名")
private String signature;
@ApiModelProperty(value = "用户所属地区")
private String region;
@ApiModelProperty(value = "用户详细地址")
private String address;
@ApiModelProperty(value = "用户创建时间")
private String ctime;
@ApiModelProperty(value = "用户最后修改时间")
private String mtime;
@ApiModelProperty(value = "用户自定义json对象")
private String extras;

@Override
public String toString() {
return "JiGuangUser{" +
"username='" + username + '\'' +
", password='" + password + '\'' +
", appkey='" + appkey + '\'' +
", nickname='" + nickname + '\'' +
", birthday='" + birthday + '\'' +
", avatar='" + avatar + '\'' +
", gender='" + gender + '\'' +
", signature='" + signature + '\'' +
", region='" + region + '\'' +
", address='" + address + '\'' +
", ctime='" + ctime + '\'' +
", mtime='" + mtime + '\'' +
", extras='" + extras + '\'' +
'}';
}
// @Override
// public String toString() {
// return "[{" +
// "username='" + username + '\'' +
// ", password='" + password + '\'' +
// ", appkey='" + appkey + '\'' +
// ", nickname='" + nickname + '\'' +
// ", birthday='" + birthday + '\'' +
// ", avatar='" + avatar + '\'' +
// ", gender='" + gender + '\'' +
// ", signature='" + signature + '\'' +
// ", region='" + region + '\'' +
// ", address='" + address + '\'' +
// ", ctime='" + ctime + '\'' +
// ", mtime='" + mtime + '\'' +
// ", extras='" + extras + '\'' +
// '}'+']';
// }

@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
JiGuangUser that = (JiGuangUser) o;
return Objects.equals(username, that.username) &&
Objects.equals(password, that.password) &&
Objects.equals(appkey, that.appkey) &&
Objects.equals(nickname, that.nickname) &&
Objects.equals(birthday, that.birthday) &&
Objects.equals(avatar, that.avatar) &&
Objects.equals(gender, that.gender) &&
Objects.equals(signature, that.signature) &&
Objects.equals(region, that.region) &&
Objects.equals(address, that.address) &&
Objects.equals(ctime, that.ctime) &&
Objects.equals(mtime, that.mtime) &&
Objects.equals(extras, that.extras);
}

@Override
public int hashCode() {
return Objects.hash(username, password, appkey, nickname, birthday, avatar, gender, signature, region, address, ctime, mtime, extras);
}

public String getAvatar() {
return avatar;
}

public void setAvatar(String avatar) {
this.avatar = avatar;
}

public String getUsername() {
return username;
}

public void setUsername(String username) {
this.username = username;
}

public String getPassword() {
return password;
}

public void setPassword(String password) {
this.password = password;
}

public String getAppkey() {
return appkey;
}

public void setAppkey(String appkey) {
this.appkey = appkey;
}

public String getNickname() {
return nickname;
}

public void setNickname(String nickname) {
this.nickname = nickname;
}

public String getBirthday() {
return birthday;
}

public void setBirthday(String birthday) {
this.birthday = birthday;
}

public String getGender() {
return gender;
}

public void setGender(String gender) {
this.gender = gender;
}

public String getSignature() {
return signature;
}

public void setSignature(String signature) {
this.signature = signature;
}

public String getRegion() {
return region;
}

public void setRegion(String region) {
this.region = region;
}

public String getAddress() {
return address;
}

public void setAddress(String address) {
this.address = address;
}

public String getCtime() {
return ctime;
}

public void setCtime(String ctime) {
this.ctime = ctime;
}

public String getMtime() {
return mtime;
}

public void setMtime(String mtime) {
this.mtime = mtime;
}

public String getExtras() {
return extras;
}

public void setExtras(String extras) {
this.extras = extras;
}
}

controller

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
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
java复制代码package com.fd.wnews.controller;

import com.alibaba.fastjson.JSONArray;
import com.alibaba.fastjson.JSONObject;
import com.fd.businessengine.common.result.Result;
import com.fd.wnews.entity.JiGuangUser;
import com.fd.wnews.service.JiGuangPushService;
import com.fd.wnews.utils.UUIDUtil;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;
import io.swagger.annotations.ApiParam;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.multipart.MultipartFile;

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

/**
* @Description 极光第三方推送
* @Author lc
* @Date 2019-11-21 下午 1:48
*/
@Api(tags = "极光第三方推送")
@RestController
@RequestMapping(value = "/jiGuang")
public class JiGuangPushController extends BaseFDController {

@Autowired
JiGuangPushService jiGuangPushService;

/**
* @param username 用户名称
* @param password 用户密码
* @param nickname 用户昵称
* @param avatar 用户头像
* @param birthday 用户生日
* @param signature 签名
* @param gender 性别
* @param region 地区
* @param address 地址
* @param extras 用户自定义json对象
* @return com.fd.businessengine.common.result.Result
* @Description 用户注册
* @Author lc
* @Date 2019-11-21 下午 5:31
**/
@ApiOperation(value = "用户注册")
@PostMapping(path = "/userRegister", name = "用户注册")
public Result userRegister(
@ApiParam(value = "用户名称")
@RequestParam(name = "username", required = true) String username,

@ApiParam(value = "用户密码")
@RequestParam(name = "password", required = true) String password,

@ApiParam(value = "用户昵称")
@RequestParam(name = "nickname", required = false) String nickname,

@ApiParam(value = "用户头像")
@RequestParam(name = "avatar", required = false) String avatar,

@ApiParam(value = "用户生日")
@RequestParam(name = "birthday", required = false) String birthday,

@ApiParam(value = "签名")
@RequestParam(name = "signature", required = false) String signature,

@ApiParam(value = "性别")
@RequestParam(name = "gender", required = false) String gender,

@ApiParam(value = "地区")
@RequestParam(name = "region", required = false) String region,

@ApiParam(value = "地址")
@RequestParam(name = "address", required = false) String address,

@ApiParam(value = "用户自定义json对象")
@RequestParam(name = "extras", required = false) String extras
) {

JiGuangUser jiGuangUser = new JiGuangUser();
jiGuangUser.setUsername(username);
jiGuangUser.setPassword(password);
jiGuangUser.setNickname(nickname);
jiGuangUser.setAvatar(avatar);
jiGuangUser.setBirthday(birthday);
jiGuangUser.setSignature(signature);
jiGuangUser.setGender(gender);
jiGuangUser.setRegion(region);
jiGuangUser.setAddress(address);
jiGuangUser.setExtras(extras);
List<JiGuangUser> jiGuangUsers = new ArrayList<>();
jiGuangUsers.add(jiGuangUser);
String param = JSONArray.toJSON(jiGuangUsers).toString();
String resultStr = jiGuangPushService.userRegister(param);
return Result.success(resultStr);
}

/**
* @param username 用户名称
* @param nickname 用户昵称
* @param avatar 用户头像
* @param birthday 用户生日
* @param signature 签名
* @param gender 性别
* @param region 地区
* @param address 地址
* @param extras 用户自定义json对象
* @return com.fd.businessengine.common.result.Result
* @Description 更新用户
* @Author lc
* @Date 2019-11-21 下午 5:31
**/
@ApiOperation(value = "更新用户")
@PostMapping(path = "/updateUser", name = "更新用户")
public Result updateUser(
@ApiParam(value = "用户名称")
@RequestParam(name = "username", required = true) String username,

@ApiParam(value = "用户昵称")
@RequestParam(name = "nickname", required = false) String nickname,

@ApiParam(value = "用户头像")
@RequestParam(name = "avatar", required = false) String avatar,

@ApiParam(value = "用户生日")
@RequestParam(name = "birthday", required = false) String birthday,

@ApiParam(value = "签名")
@RequestParam(name = "signature", required = false) String signature,

@ApiParam(value = "性别")
@RequestParam(name = "gender", required = false) String gender,

@ApiParam(value = "地区")
@RequestParam(name = "region", required = false) String region,

@ApiParam(value = "地址")
@RequestParam(name = "address", required = false) String address,

@ApiParam(value = "用户自定义json对象")
@RequestParam(name = "extras", required = false) String extras
) {
JSONObject jsonObject = new JSONObject();
jsonObject.put("username", username);
jsonObject.put("nickname", nickname);
jsonObject.put("avatar", avatar);
jsonObject.put("birthday", birthday);
jsonObject.put("signature", signature);
jsonObject.put("gender", gender);
jsonObject.put("region", region);
jsonObject.put("address", address);
jsonObject.put("extras", extras);
String resultStr = jiGuangPushService.updateUser(jsonObject.toString(), username);
return Result.success(resultStr);
}

/**
* @param username 用户名称
* @return com.fd.businessengine.common.result.Result
* @Description 获取用户详情
* @Author lc
* @Date 2019-11-22 上午 10:14
**/
@ApiOperation(value = "获取用户详情")
@GetMapping(path = "/getUserByName", name = "获取用户详情")
public Result getUserByName(
@ApiParam(value = "用户名称")
@RequestParam(name = "username", required = true) String username
) {
String resultStr = jiGuangPushService.getUserByName(username);
return Result.success(resultStr);
}


/**
* @param owner_username 群主名称
* @param name 群组名称
* @param members_username 成员名称
* @param avatar 群组头像
* @param desc 描述
* @param flag 类型(1-私有群/2-公开群)
* @return com.fd.businessengine.common.result.Result
* @Description
* @Author lc
* @Date 2019-11-22 上午 9:18
**/
@ApiOperation(value = "创建群组")
@PostMapping(path = "/createGroups", name = "创建群组")
public Result createGroups(
@ApiParam(value = "群主名称")
@RequestParam(name = "owner_username", required = true) String owner_username,

@ApiParam(value = "群组名称")
@RequestParam(name = "name", required = true) String name,

@ApiParam(value = "成员名称")
@RequestParam(name = "members_username", required = true) String[] members_username,

@ApiParam(value = "群组头像")
@RequestParam(name = "avatar", required = false) String avatar,

@ApiParam(value = "描述")
@RequestParam(name = "desc", required = false) String desc,

@ApiParam(value = "类型(1-私有群/2-公开群)")
@RequestParam(name = "flag", defaultValue = "1", required = false) Integer flag
) {

JSONObject jsonObject = new JSONObject();
System.out.println(members_username);
jsonObject.put("owner_username", owner_username);
jsonObject.put("name", name);
jsonObject.put("members_username", members_username);
jsonObject.put("avatar", avatar);
jsonObject.put("flag", flag);
jsonObject.put("desc", desc);
jsonObject.put("MaxMemberCount", 500);
String resultStr = jiGuangPushService.createGroups(jsonObject.toString());
return Result.success(resultStr);
}


/**
* @param gid 群组ID
* @return com.fd.businessengine.common.result.Result
* @Description 获取群组详情
* @Author lc
* @Date 2019-11-22 下午 3:02
**/
@ApiOperation(value = "获取群组详情")
@GetMapping(path = "/getGroupItem", name = "获取群组详情")
public Result getGroupItem(
@ApiParam(value = "群组ID")
@RequestParam(name = "gid", required = true) String gid
) {
String resultStr = jiGuangPushService.getGroupItem(gid);
return Result.success(resultStr);
}


/**
* @param file 文件
* @param type 类型
* @return com.fd.businessengine.common.result.Result
* @Description 文件上传
* @Author lc
* @Date 2019-11-22 下午 4:20
**/
@ApiOperation(value = "文件上传")
@PostMapping(path = "/fileUpload", name = "文件上传")
public Result fileUpload(
@ApiParam(value = "文件名称")
@RequestParam(name = "filename", required = true) MultipartFile file,

@ApiParam(value = "文件类型,支持image,file,voice")
@RequestParam(name = "type", required = true) String type
) {
String filePath = "";
if (!file.isEmpty()) {
String tmpdir = System.getProperty("java.io.tmpdir");
File tmpFile = new File(tmpdir, UUIDUtil.getUUID() + "." + file.getOriginalFilename().split("\\.")[1]);
try {
file.transferTo(tmpFile);
} catch (IOException e) {
e.printStackTrace();
}
filePath = tmpFile.getAbsolutePath();
}
//参数获取
String resultStr = jiGuangPushService.fileUpload(filePath, type);
return Result.success(resultStr);
}

/**
* @param mediaId 图片mediaId
* @return com.fd.businessengine.common.result.Result
* @Description 文件下载
* @Author lc
* @Date 2019-11-22 下午 3:40
**/
@ApiOperation(value = "文件下载")
@GetMapping(path = "/fileDownload", name = "文件下载")
public Result fileDownload(
@ApiParam(value = "图片mediaId")
@RequestParam(name = "mediaId", required = false) String mediaId
) {
String resultStr = jiGuangPushService.fileDownload(mediaId);
return Result.success(resultStr);
}

}

service

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
java复制代码package com.fd.wnews.service;

import cn.jpush.api.push.PushResult;

import java.util.Map;

/**
* @Description 极光消息推送
* @Author lc
* @Date 2019-11-20 上午 10:59
*/
public interface JiGuangPushService {

/**
* @param param
* @return cn.jpush.api.push.PushResult
* @Description 极光消息推送
* @Author lc
* @Date 2019-11-22 上午 10:15
**/
PushResult messagePush(Map<String, String> param);

/**
* @param param json字符串
* @return java.lang.String
* @Description 用户注册信息推送
* @Author lc
* @Date 2019-11-22 上午 9:27
**/
String userRegister(String param);

/**
* @param username 用户名称
* @return java.lang.String
* @Description 获取用户详情
* @Author lc
* @Date 2019-11-22 上午 10:01
**/
String getUserByName(String username);

/**
* @param param json字符串
* @param userName 用户名称
* @return java.lang.String
* @Description 更新用户
* @Author lc
* @Date 2019-11-22 上午 10:44
**/
String updateUser(String param, String userName);

/**
* @param param json字符串
* @return java.lang.String
* @Description 创建群聊
* @Author lc
* @Date 2019-11-22 下午 2:55
**/
String createGroups(String param);


/**
* @param gid 群组ID
* @return java.lang.String
* @Description 获取群组详情
* @Author lc
* @Date 2019-11-22 下午 3:03
**/
String getGroupItem(String gid);

/**
* @param filePath 文件路径
* @param fileType 文件类型
* @return java.lang.String
* @Description 文件上传
* @Author lc
* @Date 2019-11-22 下午 4:21
**/
String fileUpload(String filePath, String fileType);

/**
* @param mediaId 图片mediaId
* @return com.fd.businessengine.common.result.Result
* @Description 文件下载
* @Author lc
* @Date 2019-11-22 下午 3:40
**/
String fileDownload(String mediaId);
}

serviceImpl

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
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
java复制代码package com.fd.wnews.service.impl;

import cn.jpush.api.push.PushResult;
import com.fd.wnews.log.LogUtil;
import com.fd.wnews.service.JiGuangPushService;
import com.fd.wnews.utils.jiguang.PushUtil;
import org.apache.http.impl.conn.PoolingHttpClientConnectionManager;
import org.springframework.stereotype.Service;

import java.util.Map;

/**
* <p>
* 系统参数配置表 服务实现类
* </p>
*
* @author generator
* @since 2019-10-22
*/
@Service
public class JiGuangPushServiceImpl implements JiGuangPushService {

private static final PoolingHttpClientConnectionManager connPool;

static {
connPool = new PoolingHttpClientConnectionManager();
// Increase max total connection to 200
connPool.setMaxTotal(200);//configurable through app.properties
// Increase default max connection per route to 50
connPool.setDefaultMaxPerRoute(20);//configurable through app.properties
}


/**
* @param param
* @return cn.jpush.api.push.PushResult
* @Description 极光消息推送
* @Author lc
* @Date 2019-11-20 上午 11:38
**/
@Override
public PushResult messagePush(Map<String, String> param) {
return PushUtil.jpushAll(param);
}


/**
* @param param json字符串
* @return java.lang.String
* @Description 用户注册信息推送
* @Author lc
* @Date 2019-11-22 上午 9:28
**/
@Override
public String userRegister(String param) {
String url = "https://api.im.jpush.cn/v1/users/";
String resultStar = pushPost(param, url, "post");
return resultStar;
}

/**
* @param username 根据用户名称获取用户
* @return java.lang.String
* @Description 获取用户
* @Author lc
* @Date 2019-11-22 上午 10:13
**/
@Override
public String getUserByName(String username) {
String url = "https://api.im.jpush.cn/v1/users/" + username;
String resultStar = pushGet("", url);
return resultStar;
}


@Override
public String updateUser(String param, String userName) {
String url = "https://api.im.jpush.cn/v1/users/" + userName;
String resultStar = pushPost(param, url, "put");
return resultStar;
}

/**
* @param param json字符串
* @return java.lang.String
* @Description 创建群聊信息推送
* @Author lc
* @Date 2019-11-22 上午 9:29
**/
@Override
public String createGroups(String param) {
// return PushUtil.createGroups(param);
String url = "https://api.im.jpush.cn/v1/groups/";
String resultStar = pushPost(param, url, "post");
return resultStar;
}


/**
* @param gid 群组ID
* @return java.lang.String
* @Description 获取群组详情
* @Author lc
* @Date 2019-11-22 下午 3:04
**/
@Override
public String getGroupItem(String gid) {
String url = "https://api.im.jpush.cn/v1/groups/" + gid;
String resultStar = pushGet("", url);
return resultStar;
}

/**
* @param filePath 文件路径
* @param fileType 文件类型
* @return java.lang.String
* @Description 文件上传
* @Author lc
* @Date 2019-11-22 下午 4:21
**/
@Override
public String fileUpload(String filePath, String fileType) {
return PushUtil.uploadFile(filePath, fileType);
}

/**
* @param mediaId 图片mediaId
* @return java.lang.String
* @Description 图片下载
* @Author lc
* @Date 2019-11-22 下午 3:42
**/
@Override
public String fileDownload(String mediaId) {
String url = "https://api.im.jpush.cn/v1/resource?mediaId=" + mediaId;
String resultStar = pushGet("", url);
return resultStar;
}

/**
* @param param 参数
* @param url 调用地址
* @return java.lang.String
* @Description post方式推送
* @Author lc
* @Date 2019-11-22 上午 9:31
**/
private String pushPost(String param, String url, String mothodType) {
String resultStar = "";
try {
String authorization = PushUtil.getAuthorization();
resultStar = PushUtil.post(url, authorization, param, connPool, mothodType);
} catch (Exception e) {
LogUtil.error("信息推送失败:" + e);
e.printStackTrace();
}
return resultStar;
}

/**
* @param param 参数
* @param url 调用地址
* @return java.lang.String
* @Description get方式
* @Author lc
* @Date 2019-11-22 上午 9:31
**/
private String pushGet(String param, String url) {
String resultStar = "";
try {
String authorization = PushUtil.getAuthorization();
System.out.println(authorization);
resultStar = PushUtil.get(url, authorization, param, connPool);
} catch (Exception e) {
LogUtil.error("获取失败:" + e);
e.printStackTrace();
}
return resultStar;
}


}

工具类代码

(注意,调用极光第三方的接口下面有两种方式,一种是使用http接口调用,另一种极光也提供了内置的方法,如下面的文件上传的方法,除了文件上传其他的接口都是使用http的方式调用)

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
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
java复制代码package com.fd.wnews.utils.jiguang;

import cn.jiguang.common.resp.APIConnectionException;
import cn.jiguang.common.resp.APIRequestException;
import cn.jmessage.api.JMessageClient;
import cn.jmessage.api.group.CreateGroupResult;
import cn.jmessage.api.resource.ResourceClient;
import cn.jmessage.api.resource.UploadResult;
import com.alibaba.fastjson.JSONObject;
import com.arronlong.httpclientutil.HttpClientUtil;
import com.arronlong.httpclientutil.common.HttpConfig;
import com.arronlong.httpclientutil.common.HttpHeader;
import com.arronlong.httpclientutil.exception.HttpProcessException;
import org.apache.commons.lang3.StringUtils;
import org.apache.http.Header;
import org.apache.http.impl.client.CloseableHttpClient;
import org.apache.http.impl.client.HttpClients;
import org.apache.http.impl.conn.PoolingHttpClientConnectionManager;
import sun.misc.BASE64Encoder;

import static com.fd.wnews.utils.jiguang.ClientExample.LOG;

/**
* @Description 极光推送工具类
* @Author lc
* @Date 2019-11-20 上午 11:20
*/
public class PushUtil {

public static final String APP_KEY = "************";
public static final String MASTER_SECRET = "************";

/**
* @param url 请求地址
* @param authorization 认证参数
* @param pushParmJsonStr 参数
* @param connPool 连接池
* @return java.lang.String
* @Description 参数推送POST请求
* @Author lc
* @Date 2019-11-21 下午 5:56
**/
public static String post(String url, String authorization, String pushParmJsonStr, PoolingHttpClientConnectionManager connPool, String mothodType) {
String returnJson = null;
Header[] headers = HttpHeader.custom()
.other("Authorization", authorization.trim())
.userAgent("Mozilla/5.0 (Windows NT 6.3; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/41.0.2272.118 Safari/537.36").build();
try {
CloseableHttpClient client = HttpClients.custom()
.setConnectionManager(connPool).build();

HttpConfig config = HttpConfig.custom()
.headers(headers) //设置headers,不需要时则无需设置
.url(url) //设置请求的url
.json(pushParmJsonStr) //设置请求参数,没有则无需设置
.encoding("utf-8") //设置请求和返回编码,默认就是Charset.defaultCharset()
.client(client) //如果只是简单使用,无需设置,会自动获取默认的一个client对象
.inenc("utf-8") //设置请求编码,如果请求返回一直,不需要再单独设置
.inenc("utf-8"); //设置返回编码,如果请求返回一直,不需要再单独设置
if (StringUtils.equals(mothodType, "post")) {
returnJson = HttpClientUtil.post(config);//post请求
} else if (StringUtils.equals(mothodType, "put")) {
returnJson = HttpClientUtil.put(config);//post请求
}
} catch (HttpProcessException e) {
e.printStackTrace();
}
return returnJson;
}


/**
* @param url 请求地址
* @param authorization 认证参数
* @param pushParmJsonStr 参数
* @param connPool 连接池
* @return java.lang.String
* @Description 参数推送GET请求
* @Author lc
* @Date 2019-11-21 下午 5:56
**/
public static String get(String url, String authorization, String pushParmJsonStr, PoolingHttpClientConnectionManager connPool) {
String returnJson = null;
Header[] headers = HttpHeader.custom()
.other("Authorization", authorization.trim())
.userAgent("Mozilla/5.0 (Windows NT 6.3; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/41.0.2272.118 Safari/537.36").build();
try {
CloseableHttpClient client = HttpClients.custom()
.setConnectionManager(connPool).build();

HttpConfig config = HttpConfig.custom()
.headers(headers) //设置headers,不需要时则无需设置
.url(url) //设置请求的url
.json(pushParmJsonStr) //设置请求参数,没有则无需设置
.encoding("utf-8") //设置请求和返回编码,默认就是Charset.defaultCharset()
.client(client) //如果只是简单使用,无需设置,会自动获取默认的一个client对象
.inenc("utf-8") //设置请求编码,如果请求返回一直,不需要再单独设置
.inenc("utf-8"); //设置返回编码,如果请求返回一直,不需要再单独设置
returnJson = HttpClientUtil.get(config);//post请求
} catch (HttpProcessException e) {
e.printStackTrace();
}
return returnJson;
}

/**
* @param jsonObject
* @return java.lang.String
* @Description 创建群聊 (可以使用http请求,也可以使用极光内置的接口调用)
* 特别注意:参数owner_username 群主名称和 members_username 群成员名称 一定要调用注册接口注册过的
* @Author lc
* @Date 2019-11-22 下午 2:57
**/
public static String createGroups(JSONObject jsonObject) {
JMessageClient client = new JMessageClient(APP_KEY, MASTER_SECRET);
CreateGroupResult res = null;
try {
String owner_username = jsonObject.getString("owner_username");
String name = jsonObject.getString("name");
String members_username = jsonObject.getString("members_username");
System.out.println(members_username);
String avatar = jsonObject.getString("avatar");
Integer flag = jsonObject.getInteger("flag");
String desc = jsonObject.getString("desc");
res = client.createGroup(owner_username, name, desc, avatar, flag, members_username);
LOG.info(res.getName());
} catch (APIConnectionException e) {
LOG.error("Connection error. Should retry later. ", e);
} catch (APIRequestException e) {
LOG.error("Error response from JPush server. Should review and fix it. ", e);
LOG.info("HTTP Status: " + e.getStatus());
LOG.info("Error Message: " + e.getMessage());
}
return res.getName();
}

/**
* 文件上传
*
* @param filePath
* @param fileType
* @return
*/
public static String uploadFile(String filePath, String fileType) {
ResourceClient client = new ResourceClient(APP_KEY, MASTER_SECRET);
String mediaId = "";
try {
UploadResult result = client.uploadFile(filePath, fileType);
mediaId = result.getMediaId();
System.out.println(mediaId);
} catch (APIConnectionException e) {
LOG.error("Connection error. Should retry later. ", e);
} catch (APIRequestException e) {
LOG.error("Error response from JPush server. Should review and fix it. ", e);
LOG.info("HTTP Status: " + e.getStatus());
LOG.info("Error Message: " + e.getMessage());
}
return mediaId;
}

/**
* @param str
* @return java.lang.String
* @Description BASE64加密方法
* @Author lc
* @Date 2019-11-21 下午 3:29
**/
public static String encryptBASE64(String str) {
byte[] key = str.getBytes();
BASE64Encoder base64Encoder = new BASE64Encoder();
String strs = base64Encoder.encodeBuffer(key);
return strs;
}


/**
* @return java.lang.String
* @Description 验证采用 HTTP Basic 机制,即 HTTP Header(头)里加一个字段(Key/Value对):
* Authorization: Basic base64_auth_string 其中 base64_auth_string 的生成算法为:base64(appKey:masterSecret)
* 即,对 appKey 加上冒号,加上 masterSecret 拼装起来的字符串,再做 base64 转换
* @Author lc
* @Date 2019-11-21 下午 3:28
**/
public static String getAuthorization() {
String base64_auth_string = encryptBASE64(APP_KEY + ":" + MASTER_SECRET);
String authorization = "Basic " + base64_auth_string;
return authorization;
}


public static void main(String[] args) {

System.out.println(getAuthorization());


// List<JiGuangUser> jiGuangUsers = new ArrayList<>();
// JiGuangUser jiGuangUser = new JiGuangUser();
// jiGuangUser.setUsername("liucong");
// jiGuangUser.setPassword("123456");
// jiGuangUsers.add(jiGuangUser);
//
// System.out.println(JSONArray.toJSON(jiGuangUsers).toString());


}


}

注意:controller中的接口所返回的Result对象可自行自定义,这里就不贴代码了

特别强调的是要注意调用的时候需要满足极光的http验证要求

比如使用postman直接调用需要使用 生成base64的字符串

本文转载自: 掘金

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

玩转Linux,介绍一个强大的Linux服务器管理面板,比宝

发表于 2021-05-09

介绍

之前大多数时候听到或者看到的Linux服务器的管理面板都是宝塔面板,确实宝塔面板非常方便而且好用,安装也简单,复制粘贴几句命令即可安装完成,且提供免费版,本文向大家介绍另一个Linux的服务器面板——AppNode,功能丰富,也提供免费版,且是永久免费!

系统要求

  • 操作系统: CentOS 6.x 32/64位
  • CentOS 7.x 64位
  • (暂不支持 Ubuntu、Debian)。
  • 内存要求: 建议可用内存在 512MB 以上。

安装教程

官网提供了免费教程,且安装非常简单,可以通过配置的方式自动生成安装命令:

以上前提是你对Linux服务器有一定的了解,至少你得会使用SSH客户端连接你的服务器,这里向大家推荐免费的远程管理工具mRemoteNG或者使用FinalShell,下面是mRemoteNG的Github地址:

github.com/mRemoteNG/m…

1、使用官网提供了命令配置生成配置脚本

2、通过SSH客户端连接服务器(注意系统要求)

3、查看安装演示:

安装至最后一步时,会依次提示您输入端口、用户名、密码,请根据提示输入后按下回车键进入下一步,如果想使用中括号中的默认值,可直接按回车键。(动图不清晰,直接查看官网)

后台管理介绍

集中式设计,快速切换管理

安装一个控制中心即可管理你的所有服务器,其它服务器只需要安装受控端。再也不用分别登录到每台服务器去管理了。

  1. 不限制服务器数量
  2. 集中显示运行状况
  3. 在线 SSH 终端连接
  4. 分组管理
  5. 自定义节点名称
  6. 支持通过 SSH 隧道连接受控端
  7. 多用户服务器权限管理(开发中)

强大的网站管理功能

通过整合 Nginx、PHP、MySQL 等组件,帮助您快速创建和配置网站。

  1. 多种架构:PHP、纯静态、反向代理
  2. 组件可选择性安装,PHP多版本并存
  3. 创建网站时自动创建数据库和FTP
  4. Rewrite、SSI、Gzip、跳转、缓存等丰富的配置
  5. 并发限制、请求限制、下载限速
  6. 目录大小限制和写入防护
  7. HTTP/2.0、Let’s Encrypt免费证书
  8. 批量备份和导入,定时备份
  9. 建站市场一键下载安装网站源码
  10. 更多架构支持:Node.js、Java、Python(开发中)

快速安装升级,丰富的软件市场

仅需执行一条命令即可全自动安装,安装耗时不到 1 分钟。版本升级无需重装,一键点击即可在线升级。丰富的软件市场,满足您多样化的服务器软件需求,您的服务器不再仅仅是 Web 服务器。

  1. 自定义安装命令
  2. 基于系统默认软件管理体系
  3. 采用 Go 语言开发,自带 HTTP 服务,环境无依赖
  4. 软件批量安装、批量升级、自动升级
  5. 超过 40+ 个应用软件功能
  6. 超过 100+ 个 PHP 扩展
  7. 超过 10+ 个 Nginx 模块
  8. 软件还在不断持续增加和维护

多重安全加固,资源实时监控

从操作系统层面和应用层面,提供多项加固措施保障您的服务器和应用安全。强大的系统监控功能,让您对服务器的资源消耗情况了如执掌。

  1. SSH 登录通知
  2. 面板登录通知
  3. 防火墙快速端口开关、IP 黑名单
  4. 所有进程的端口开放状态集中查看和管理
  5. 网站目录防护,禁止上传后门文件
  6. 受控端支持走 SSH 隧道,不暴露新端口
  7. 对系统资源使用情况的采集、监控和警报
  8. 网站防 CC 攻击

全面开放的 API

全面开放的 API 机制,便于您将 AppNode 已有的功能整合到您的系统中。

  1. 基于 HTTP/HTTPS 的 API 调用
  2. 基于 HMAC-MD5 签名的 API 验证机制
  3. 面板所有功能均基于 API 实现,开放度为 100%
  4. 科学的模块划分,每个模块拥有自己的独立的 API 文档
  5. 良好的 API 路由机制,从控制中心到受控端应用的 API 均可轻松调用

总结

总的来说,对于大部分用户来说免费版本已经够用,安装上比较简单和快速,但是由于功能丰富,全部上手肯定要一段时间,但也总比一个个的命令上手快,特别是和笔者类似的初级使用者,当然专业人员使用会更快,Enjoy it!

三种使用版本的区别

PS:直接网上搜索AppNode第一个便是官网

本文转载自: 掘金

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

一键自动生成数据库文档,妹子心动了!(告别CV大法) 前言

发表于 2021-05-09

前言

在日常开发中,当项目上线之后,甲方必定会让你提供一堆的验收文档,其中就包括了数据库字典,记得我之前是几十张表,一顿CV大法,一边写一遍骂,这种事情怎么交给我这个开发来做呢,各种吐槽,几个小时过后几十页的 Word文档就出炉了,页面那是花里胡哨,然而并没有什么用,甲方看都不看一眼。

最近小编也在找这样的插件,就是不想写文档了,浪费时间和心情啊,果然我找到一款比较好用,操作简单不复杂。

screw 是一个简洁好用的数据库表结构文档的生成工具 ,支持 MySQL、Oracle、PostgreSQL 等主流的关系数据库。

这是github地址:github.com/pingfangush…

在这里插入图片描述


一、引入pom.xml依赖

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
java复制代码<dependencies>
<!-- screw 库,简洁好用的数据库表结构文档生成器 -->
<dependency>
<groupId>cn.smallbun.screw</groupId>
<artifactId>screw-core</artifactId>
<version>1.0.5</version>
</dependency>

<!-- 数据库连接 -->
<dependency>
<groupId>com.zaxxer</groupId>
<artifactId>HikariCP</artifactId>
<version>3.4.5</version>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>8.0.22</version>
</dependency>
</dependencies>

二、创建Java类

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
java复制代码/**
* @description: 使用 screw 生成文档
* @author: DT
* @date: 2021/5/9 12:19
* @version: v1.0
*/

public class TestScrewMain {

private static final String DB_URL = "jdbc:mysql://192.168.31.158:3306";
private static final String DB_NAME = "testdt?useUnicode=true&characterEncoding=UTF-8&serverTimezone=Asia/Shanghai";
private static final String DB_USERNAME = "root";
private static final String DB_PASSWORD = "123456";

private static final String FILE_OUTPUT_DIR = "C:\\Users\\DT开发者\\Desktop\\";
// 可以设置 Word 或者 Markdown 格式
private static final EngineFileType FILE_OUTPUT_TYPE = EngineFileType.WORD;
private static final String DOC_FILE_NAME = "数据库字典文档";
private static final String DOC_VERSION = "V1.0.0";
private static final String DOC_DESCRIPTION = "文档描述";

public static void main(String[] args) {
// 创建 screw 的配置
Configuration config = Configuration.builder()
// 版本
.version(DOC_VERSION)
// 描述
.description(DOC_DESCRIPTION)
// 数据源
.dataSource(buildDataSource())
// 引擎配置
.engineConfig(buildEngineConfig())
// 处理配置
.produceConfig(buildProcessConfig())
.build();

// 执行 screw,生成数据库文档
new DocumentationExecute(config).execute();
}

/**
* 创建数据源
*/
private static DataSource buildDataSource() {
// 创建 HikariConfig 配置类
HikariConfig hikariConfig = new HikariConfig();
hikariConfig.setDriverClassName("com.mysql.cj.jdbc.Driver");
hikariConfig.setJdbcUrl(DB_URL + "/" + DB_NAME);
hikariConfig.setUsername(DB_USERNAME);
hikariConfig.setPassword(DB_PASSWORD);
// 设置可以获取 tables remarks 信息
hikariConfig.addDataSourceProperty("useInformationSchema", "true");
// 创建数据源
return new HikariDataSource(hikariConfig);
}

/**
* 创建 screw 的引擎配置
*/
private static EngineConfig buildEngineConfig() {
return EngineConfig.builder()
// 生成文件路径
.fileOutputDir(FILE_OUTPUT_DIR)
// 打开目录
.openOutputDir(false)
// 文件类型
.fileType(FILE_OUTPUT_TYPE)
// 文件类型
.produceType(EngineTemplateType.freemarker)
// 自定义文件名称
.fileName(DOC_FILE_NAME)
.build();
}

/**
* 创建 screw 的处理配置,一般可忽略
* 指定生成逻辑、当存在指定表、指定表前缀、指定表后缀时,将生成指定表,其余表不生成、并跳过忽略表配置
*/
private static ProcessConfig buildProcessConfig() {
return ProcessConfig.builder()
// 根据名称指定表生成
.designatedTableName(Collections.<String>emptyList())
// 根据表前缀生成
.designatedTablePrefix(Collections.<String>emptyList())
// 根据表后缀生成
.designatedTableSuffix(Collections.<String>emptyList())
// 忽略表名
.ignoreTableName(Arrays.asList("test", "mytable","role","t_role","t_user"))
// 忽略表前缀
//.ignoreTablePrefix(Collections.singletonList("t_"))
// 忽略表后缀
//.ignoreTableSuffix(Collections.singletonList("_test"))
.build();
}

}

在这里插入图片描述
在这里插入图片描述

三、使用 Maven 插件的方式

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
java复制代码<plugin>
<groupId>cn.smallbun.screw</groupId>
<artifactId>screw-maven-plugin</artifactId>
<version>1.0.5</version>
<dependencies>
<!-- 数据库连接 -->
<dependency>
<groupId>com.zaxxer</groupId>
<artifactId>HikariCP</artifactId>
<version>3.4.5</version>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>8.0.17</version>
</dependency>
</dependencies>
<configuration>
<!-- 数据库相关配置 -->
<driverClassName>com.mysql.cj.jdbc.Driver</driverClassName>
<jdbcUrl>jdbc:mysql://192.168.31.158:3306/testdt?serverTimezone=Asia/Shanghai</jdbcUrl>
<username>root</username>
<password>123456</password>
<!-- screw 配置 -->
<fileType>WORD</fileType>
<title>数据库文档</title> <!--标题-->
<fileName>测试文档名称</fileName> <!--文档名称 为空时:将采用[数据库名称-描述-版本号]作为文档名称-->
<description>数据库文档生成</description> <!--描述-->
<version>${project.version}</version> <!--版本-->
<openOutputDir>false</openOutputDir> <!--打开文件输出目录-->
<produceType>freemarker</produceType> <!--生成模板-->
</configuration>
<executions>
<execution>
<phase>compile</phase>
<goals>
<goal>run</goal>
</goals>
</execution>
</executions>
</plugin>

执行 screw-maven-plugin 插件,会在 doc 目录下生成文档。如下图所示:

在这里插入图片描述

总结

screw 是一个简洁好用的数据库表结构文档的生成工具 ,支持 MySQL、Oracle、PostgreSQL 等主流的关系数据库。使用起来还是很方便的,不用再去手动编写数据字典了,非常适用,从此告别CV手写数据库文档,真的是痛苦啊!他这个插件还可以生成Java实体类,不过除了Lombok其他的不支持,比如Swagger注释,基础实体字段表映射等。下一篇会给大家推荐一些生成实体类较好的脚本或者工具类。

本文转载自: 掘金

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

盘点Sharding-JDBC 读写分离

发表于 2021-05-09

总文档 :文章目录

Github : github.com/black-ant

一 . 前言

本来也没打算看读写分离的代码 , 但是…他他他又出问题了 , 按照官方文档尝试配置了 读写分离后 , 并没有出现期待的结果

不过幸运的是 , 能跑起来 , 大概率就是读写分离的逻辑没有生效

二 . 源码分析

和上一篇最大的难点就是 , 不知道从哪里开始看起…

这个时候官方文档的作用就出来了 , 它告诉我有一个接口 ShardingSphereAlgorithm , 于是查找其实现类 , 有所发现

Sharding-jdbc-balance.png

可以看到 ,读写分离在 Sharding 中也有着负载均衡的作用 , 对应的接口为 ReplicaLoadBalanceAlgorithm

2.1 入口查找

先看一下接口中需要实现什么

1
2
3
4
5
6
7
8
java复制代码// 接口中提供了一个待实现的方法
public interface ReplicaLoadBalanceAlgorithm extends ShardingSphereAlgorithm {

// name : 查询逻辑数据源名称
// primaryDataSourceName : 主要数据源的名称
// replicaDataSourceNames : 副本数据源的名称
String getDataSource(String name, String primaryDataSourceName, List<String> replicaDataSourceNames);
}

我们随机选择一个 ShardingSphereAlgorithm 逆推一下流程 , 最终发现了类

  • ReplicaQueryDataSourceRouter # route
  • ReplicaQuerySQLRouter # createRouteContext
  • PartialSQLRouteExecutor # route

我们按照这个流程逆推一下 :

Step End : ReplicaQueryDataSourceRouter 最终规则处理

1
2
3
4
5
6
7
8
9
10
java复制代码C55- ReplicaQueryDataSourceRouter
M55_01- route(final SQLStatement sqlStatement)
?- route 规则类 , 干了如下2件事
- 是否为 isPrimaryRoute , 如果是 , 直接获取 getPrimaryDataSourceName
- 如果不是 , 通过规则获取 rule.getLoadBalancer()

// PS : 打个断点发现和预料的一样 , 并没有执行相关的逻辑 , 继续往上推导

C101- KernelProcessor ,这是 route 的核心处理类 , 再看一下重要的流程
M101_01- generateExecutionContext

一路往上 , 发现了核心处理类 KernelProcessor , 到了这里就基本能确定 , 还是会通过ShardingSpherePreparedStatement 处理

2.2 正向流程梳理

知道了主流程 , 我们就可以推断出正向流程 :

  1. C74- ShardingSpherePreparedStatement
  2. C101- KernelProcessor
    • M101_01- generateExecutionContext
  3. C102- SQLRouteEngine
    • route
  4. C103- PartialSQLRouteExecutor
    • route
  5. C60- ReplicaQuerySQLRouter
    • createRouteContext – 终于到了这个核心的地方

Step 1 : ShardingSpherePreparedStatement 创建 ExecutionContext

直接来到主入口进行调试 : ShardingSpherePreparedStatement , 其中有2个主要的方法 executeQuery() / executeUpdate()

上一次说了executeUpdate , 这一次主要走 executeQuery

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
java复制代码C74- ShardingSpherePreparedStatement
M74_10- executeQuery
1- 创建 createExecutionContext() -> M101_01
2- 通过 getInputGroups() 获取 InputGroup 集合
3- reparedStatementExecutor.executeQuery(inputGroups) 执行 Query
4- 合并结果
5- 构建 ShardingSphereResultSet 返回
M74_11- createExecutionContext
1- createLogicSQL() 构建逻辑 SQL , 即需要查询的 SQL -> PS :
2- 通过 KernelProcessor 构建一个 ExecutionContext
?- 这里就和上文关联起来了 ->

// PS:M74_11 createLogicSQL 创建的 SQL
select blogentity0_.id as id1_0_, blogentity0_.author as author2_0_, blogentity0_.column_id as column_i3_0_, blogentity0_.date as date4_0_, blogentity0_.title as title5_0_, blogentity0_.title_id as title_id6_0_ from t_blog_0 blogentity0_


// M74_10 源代码
public ResultSet executeQuery() throws SQLException {
ResultSet result;
try {
clearPrevious();
// 核心处理 , 此处已经把需要处理的 SQL 放入容器中
executionContext = createExecutionContext();
List<QueryResult> queryResults;
if (ExecutorConstant.MANAGED_RESOURCE) {
Collection<InputGroup<StatementExecuteUnit>> inputGroups = getInputGroups();
cacheStatements(inputGroups);
reply();
queryResults = preparedStatementExecutor.executeQuery(inputGroups);
} else {
queryResults = rawExecutor.executeQuery(getRawInputGroups(), new RawSQLExecutorCallback());
}
MergedResult mergedResult = mergeQuery(queryResults);
result = new ShardingSphereResultSet(statements.stream().map(this::getResultSet).collect(Collectors.toList()), mergedResult, this, executionContext);
} finally {
clearBatch();
}
currentResultSet = result;
return result;
}

这是一个核心的锚点 , 找到了这个点 , 后面的就好分析了

PS : 后续整体流程和之前看的一样 , 主要是以下流程

Step 2 : KernelProcessor 构建 ExecutionContext 流程主逻辑

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
java复制代码
C101- KernelProcessor ,这是 route 的核心处理类 , 再看一下重要的流程
M101_01- generateExecutionContext
1- 获取 rules 集合
?- 此处解决了第一个配置问题 , 详见-> PS:M101_01_01
?- 配置改好后 , 可以看到 rule 已经正常了 -> PS:M101_01_02


public ExecutionContext generateExecutionContext(final LogicSQL logicSQL, final ShardingSphereSchema schema, final ConfigurationProperties props) {
Collection<ShardingSphereRule> rules = schema.getRules();
// RouteEngine 生成 , 其中包好了 rules 对象其相关属性
SQLRouteEngine sqlRouteEngine = new SQLRouteEngine(rules, props);
// PS:M101_01_10
SQLStatementContext<?> sqlStatementContext = logicSQL.getSqlStatementContext();

// 核心流程
RouteContext routeContext = sqlRouteEngine.route(logicSQL, schema);
SQLRewriteEntry rewriteEntry = new SQLRewriteEntry(schema.getMetaData().getSchemaMetaData().getConfiguredSchemaMetaData(), props, rules);
SQLRewriteResult rewriteResult = rewriteEntry.rewrite(logicSQL.getSql(), logicSQL.getParameters(), sqlStatementContext, routeContext);
Collection<ExecutionUnit> executionUnits = ExecutionContextBuilder.build(schema.getMetaData(), rewriteResult, sqlStatementContext);
return new ExecutionContext(sqlStatementContext, executionUnits, routeContext);
}

PS:M101_01_10 相关属性一览
image.png

Step 3 : PartialSQLRouteExecutor 通过 rules 生成 result

注意 , 这里会根据是否已经创建了 RouteUnits 选择是否创建还是添加

处理流程 , 下面2个 问题解决完成后 , 配置正常 , 此时可以看到 rule 存在

1
2
3
4
5
6
java复制代码// 我们根据上面2个流程着重分析其中2步
C103- PartialSQLRouteExecutor
M103_01- route
FOR- 遍历所有的 rule , 根据是否生成了 RouteUnits 分别调用
- entry.getValue().createRouteContext(logicSQL, schema, entry.getKey(), props) -> M104_01
- entry.getValue().decorateRouteContext(result, logicSQL, schema, entry.getKey(), props -> M104_02

PS:M101_01_02 rules 列表

Sharding-jdbc-rules-list.jpg

Step 4 : ReplicaQuerySQLRouter / rules 处理主逻辑

这个逻辑中有 2个重要的方法

  • createRouteContext : 创建 RouteContext
  • decorateRouteContext : 补充 RouteContext
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
java复制代码C60- ReplicaQuerySQLRouter 
M60_01- createRouteContext
- 获取首个 rule , 构建 ReplicaQueryDataSourceRouter
- 调用 route 获取 需要执行的 datasourceName
- 最终会调用对应的 ReplicaLoadBalanceAlgorithm
-> PS:M60_01_01
M60_02- decorateRouteContext


// 以轮询为例
C61- RoundRobinReplicaLoadBalanceAlgorithm
F61_01- ConcurrentHashMap<String, AtomicInteger> COUNTS
M61_01- getDataSource
?- 轮询主要通过属性 F61_01 完成
?- 简单点说就是每次 For 中 , 这个COUNTS 都会 + 1 , 到了和总数一样的时候 , 执行 CAS 设置为 0
?- 算法部分先不深入太多 , 后续专门整理



// M104_01 createRouteContext 源代码
public RouteContext createRouteContext(final LogicSQL logicSQL, final ShardingSphereSchema schema, final ReplicaQueryRule rule, final ConfigurationProperties props) {
RouteContext result = new RouteContext();
String dataSourceName = new ReplicaQueryDataSourceRouter(rule.getSingleDataSourceRule()).route(logicSQL.getSqlStatementContext().getSqlStatement());
result.getRouteUnits().add(new RouteUnit(new RouteMapper(DefaultSchema.LOGIC_NAME, dataSourceName), Collections.emptyList()));
return result;
}

// M104_02 decorateRouteContext 源代码
public void decorateRouteContext(final RouteContext routeContext,
final LogicSQL logicSQL, final ShardingSphereSchema schema, final ReplicaQueryRule rule, final ConfigurationProperties props) {
Collection<RouteUnit> toBeRemoved = new LinkedList<>();
Collection<RouteUnit> toBeAdded = new LinkedList<>();
for (RouteUnit each : routeContext.getRouteUnits()) {
String dataSourceName = each.getDataSourceMapper().getLogicName();
Optional<ReplicaQueryDataSourceRule> dataSourceRule = rule.findDataSourceRule(dataSourceName);
if (dataSourceRule.isPresent() && dataSourceRule.get().getName().equalsIgnoreCase(each.getDataSourceMapper().getActualName())) {
toBeRemoved.add(each);
String actualDataSourceName = new ReplicaQueryDataSourceRouter(dataSourceRule.get()).route(logicSQL.getSqlStatementContext().getSqlStatement());
toBeAdded.add(new RouteUnit(new RouteMapper(each.getDataSourceMapper().getLogicName(), actualDataSourceName), each.getTableMappers()));
}
}
routeContext.getRouteUnits().removeAll(toBeRemoved);
routeContext.getRouteUnits().addAll(toBeAdded);
}

// PS:M60_01_01 获取负载均衡的 datasource
rule.getLoadBalancer().getDataSource(rule.getName(), rule.getPrimaryDataSourceName(), rule.getReplicaDataSourceNames());


// M61_01 源代码
public String getDataSource(final String name, final String primaryDataSourceName, final List<String> replicaDataSourceNames) {
AtomicInteger count = COUNTS.containsKey(name) ? COUNTS.get(name) : new AtomicInteger(0);
COUNTS.putIfAbsent(name, count);
count.compareAndSet(replicaDataSourceNames.size(), 0);
return replicaDataSourceNames.get(Math.abs(count.getAndIncrement()) % replicaDataSourceNames.size());
}

Step 补充环节 : Rule 的处理流程

rules 的处理流程主要是以下几步 :

  • SQLRouteEngine : route 处理引擎 , 开始处理 route
  • PartialSQLRouteExecutor : 选择哪种核心方式
  • ReplicaQuerySQLRouter : 核心的处理方式
  • ReplicaQueryDataSourceRouter : 调用算法获取Source 名称
  • RoundRobinReplicaLoadBalanceAlgorithm : 算法核心

rules 的构建流程主要是以下几步 :

  • SchemaContextsBuilder : 构建 ContextsBuilder
  • ShardingSphereRulesBuilder : 通过 rules 配置执行 build 主逻辑
  • ReplicaQueryRuleBuilder : 拿到相关配置 , 构建主要的 Rule
  • ReplicaQueryRule : 被构建的对象

PS:M101_01_01 rules 集合

第一次运行的时候发现 rule 为空 , 明白找到了第一个问题 , 配置实际上是缺失的 , 看一下rule 的继承体系

ShardingSphereRule.png

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
java复制代码// 很明显 , 看到了一个 ReplicaQueryRule , 进去看看

C56- ReplicaQueryRule
?- 其中有2个主要的构造方法 , 通过 Configuration 进行的配置
MC56_01- ReplicaQueryRule(final ReplicaQueryRuleConfiguration config)
?- 调用 ReplicaQueryRuleBuilder 进行构建 -> M57_01
MC56_02- ReplicaQueryRule(final AlgorithmProvidedReplicaQueryRuleConfiguration config)
M01- getDataSourceMapper()


// 前置条件 , 配置 Rule
public final class ReplicaQueryRule implements DataSourceRoutedRule, StatusContainedRule {

static {
ShardingSphereServiceLoader.register(ReplicaLoadBalanceAlgorithm.class);
}

private final Map<String, ReplicaLoadBalanceAlgorithm> loadBalancers = new LinkedHashMap<>();

private final Map<String, ReplicaQueryDataSourceRule> dataSourceRules;

public ReplicaQueryRule(final ReplicaQueryRuleConfiguration config) {
Preconditions.checkArgument(!config.getDataSources().isEmpty(), "Replica query data source rules can not be empty.");
config.getLoadBalancers().forEach((key, value) -> loadBalancers.put(key, ShardingSphereAlgorithmFactory.createAlgorithm(value, ReplicaLoadBalanceAlgorithm.class)));
dataSourceRules = new HashMap<>(config.getDataSources().size(), 1);
for (ReplicaQueryDataSourceRuleConfiguration each : config.getDataSources()) {
// TODO check if can not find load balancer should throw exception.
ReplicaLoadBalanceAlgorithm loadBalanceAlgorithm = Strings.isNullOrEmpty(each.getLoadBalancerName()) || !loadBalancers.containsKey(each.getLoadBalancerName())
? TypedSPIRegistry.getRegisteredService(ReplicaLoadBalanceAlgorithm.class) : loadBalancers.get(each.getLoadBalancerName());
dataSourceRules.put(each.getName(), new ReplicaQueryDataSourceRule(each, loadBalanceAlgorithm));
}
}

public ReplicaQueryRule(final AlgorithmProvidedReplicaQueryRuleConfiguration config) {
Preconditions.checkArgument(!config.getDataSources().isEmpty(), "Replica query data source rules can not be empty.");
loadBalancers.putAll(config.getLoadBalanceAlgorithms());
dataSourceRules = new HashMap<>(config.getDataSources().size(), 1);
for (ReplicaQueryDataSourceRuleConfiguration each : config.getDataSources()) {
// TODO check if can not find load balancer should throw exception.
ReplicaLoadBalanceAlgorithm loadBalanceAlgorithm = Strings.isNullOrEmpty(each.getLoadBalancerName()) || !loadBalancers.containsKey(each.getLoadBalancerName())
? TypedSPIRegistry.getRegisteredService(ReplicaLoadBalanceAlgorithm.class) : loadBalancers.get(each.getLoadBalancerName());
dataSourceRules.put(each.getName(), new ReplicaQueryDataSourceRule(each, loadBalanceAlgorithm));
}
}

//...........

}


C57- ReplicaQueryRuleBuilder
M57_01- build(final ReplicaQueryRuleConfiguration ruleConfig, final Collection<String> dataSourceNames)
- new ReplicaQueryRule(ruleConfig) 构建 -> MC56_01

// 看到这里大概知道构建的位置了 , 上文看到过的类 C51- ShardingSphereRulesBuilder
C51- ShardingSphereRulesBuilder
M51_01- build(final Collection<RuleConfiguration> ruleConfigurations, final Collection<String> dataSourceNames)
?- 该方法会通过 RuleConfiguration 集合构建 Rule 对象
?- 再往上就不需要看了 , 最终还是会回到构建 ShardingSphereDataSource , 只需要根据配置类猜测配置方式即可 -> PS:M51_01_01

Step 附录 : 读写分离的相关配置分析方式

配置类还是之前的几个

  • SpringBootConfiguration
  • YamlReplicaQueryRuleSpringBootConfiguration : spring.shardingsphere.rules
    • YamlReplicaQueryRuleConfiguration : replicaQuery
      • Map<String, YamlReplicaQueryDataSourceRuleConfiguration> : dataSources
      • Map<String, YamlShardingSphereAlgorithmConfiguration> : loadBalancers

我算是明白了 , 也不知道是不是找的文档不对 ,指望官方文档还是不行的 , 最后还是要靠自己

这一小节就是如果通过 Sharding 的 配置类 Bean , 反推出相关的配置.

PS:M51_01_01 配置类的推测

1
2
3
4
5
6
7
8
9
10
11
java复制代码// 点开可以看到 , 其中只有2个属性
public final class ReplicaQueryRuleConfiguration implements RuleConfiguration {
private final Collection<ReplicaQueryDataSourceRuleConfiguration> dataSources;
private final Map<String, ShardingSphereAlgorithmConfiguration> loadBalancers;
}


// 这里再来说明一下 , 如何逆推配置 , Sharding 的配置解析方式是很常见的方式 , 按照类型和方法名去反写一个就行了 , 比如这个配置
spring.shardingsphere.rules.sharding.tables.t_blog.key-generate-strategy.column=id

- sharding 来源 : YamlShardingRuleSpringBootConfiguration

sharding-config.png

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复制代码我们按照这个方法 , 结合官方文档 , 逆推一下 , 可以找到如下几个类 : 

- YamlReplicaQueryRuleSpringBootConfiguration : spring.shardingsphere.rules
- YamlReplicaQueryRuleConfiguration : replicaQuery
- Map<String, YamlReplicaQueryDataSourceRuleConfiguration> : dataSources
- Map<String, YamlShardingSphereAlgorithmConfiguration> : loadBalancers

public final class YamlReplicaQueryDataSourceRuleConfiguration implements YamlConfiguration {
private String name;
private String primaryDataSourceName;
private List<String> replicaDataSourceNames = new ArrayList<>();
private String loadBalancerName;
private Properties props = new Properties();
}

public final class YamlShardingSphereAlgorithmConfiguration implements YamlConfiguration {
private String type;
private Properties props = new Properties();
}

// 逆推得到读写分离的配置信息 :
spring.shardingsphere.rules.replica-query.data-sources.ds_0.primary-data-source-name=primary_ds_0
spring.shardingsphere.rules.replica-query.data-sources.ds_0.replica-data-source-names=primary_ds_0_replica_0, primary_ds_0_replica_1
spring.shardingsphere.rules.replica-query.data-sources.ds_1.primary-data-source-name=primary_ds_1
spring.shardingsphere.rules.replica-query.data-sources.ds_1.replica-data-source-names=primary_ds_1_replica_0, primary_ds_1_replica_1

// 小技巧 : 拿着 spring.shardingsphere.rules.replica-query 去网上搜 , 可能会有意想不到的惊喜

最终配置结果 :
spring.shardingsphere.rules.replica-query.data-sources.ds_0.name=rq-ds0
spring.shardingsphere.rules.replica-query.data-sources.ds_0.primary-data-source-name=ds0
spring.shardingsphere.rules.replica-query.data-sources.ds_0.replica-data-source-names=ds0,ds1,ds2
# Load balance 算法
spring.shardingsphere.rules.replica-query.load-balancers.rq-ds0.type=round_robin
spring.shardingsphere.rules.replica-query.load-balancers.rq-ds0.props.null=

// 请求地址 , 可以看到分别从 ds0,ds1,ds2 获取信息 , 插入数据只会插入 ds0
// PS : 读写分离一般配合数据库主从同步做的

这不看源码你叫我怎么猜的出来??????????????

image-20210508162525556.png

三 . 配置

这里把完整的配置贴出来

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
properties复制代码server.port=8085
##Jpa配置
spring.jpa.database=mysql
spring.jpa.show-sql=true
spring.jpa.hibernate.ddl-auto=none
# 配置真实数据源 ds0,ds1,ds2
spring.shardingsphere.datasource.names=ds0,ds1,ds2
spring.shardingsphere.datasource.common.type=com.zaxxer.hikari.HikariDataSource
spring.shardingsphere.datasource.common.driver-class-name=com.mysql.jdbc.Driver
# 配置第 1 个数据源
spring.shardingsphere.datasource.ds0.type=com.zaxxer.hikari.HikariDataSource
spring.shardingsphere.datasource.ds0.driver-class-name=com.mysql.jdbc.Driver
spring.shardingsphere.datasource.ds0.jdbc-url=jdbc:mysql://127.0.0.1:3306/database0?useUnicode=true&characterEncoding=UTF-8&zeroDateTimeBehavior=convertToNull&allowMultiQueries=true&serverTimezone=UTC
spring.shardingsphere.datasource.ds0.username=root
spring.shardingsphere.datasource.ds0.password=123456
# 配置第 2 个数据源
spring.shardingsphere.datasource.ds1.type=com.zaxxer.hikari.HikariDataSource
spring.shardingsphere.datasource.ds1.driver-class-name=com.mysql.jdbc.Driver
spring.shardingsphere.datasource.ds1.jdbc-url=jdbc:mysql://127.0.0.1:3306/database1?useUnicode=true&characterEncoding=UTF-8&zeroDateTimeBehavior=convertToNull&allowMultiQueries=true&serverTimezone=UTC
spring.shardingsphere.datasource.ds1.username=root
spring.shardingsphere.datasource.ds1.password=123456
# 配置从库 3
spring.shardingsphere.datasource.ds2.type=com.zaxxer.hikari.HikariDataSource
spring.shardingsphere.datasource.ds2.driver-class-name=com.mysql.jdbc.Driver
spring.shardingsphere.datasource.ds2.jdbc-url=jdbc:mysql://127.0.0.1:3306/database2?useUnicode=true&characterEncoding=UTF-8&zeroDateTimeBehavior=convertToNull&allowMultiQueries=true&serverTimezone=UTC
spring.shardingsphere.datasource.ds2.username=root
spring.shardingsphere.datasource.ds2.password=123456
# Sharding 读写分离配置
# 是否显示 SQL
spring.shardingsphere.props.sql.show=true
# 是否 Bean 覆盖
spring.main.allowBeanDefinitionOverriding=true
# 官网提供的配置 , 这是哪里的?????????????????????
#spring.shardingsphere.rules.readwrite-splitting.data-sources.dataSource.primary-data-source-name= ds0
#spring.shardingsphere.rules.readwrite-splitting.data-sources.dataSource.replica-data-source-names= ds1,ds2
#spring.shardingsphere.rules.readwrite-splitting.data-sources.dataSource.load-balancer-name= round_robin_type
# 实际配置信息
spring.shardingsphere.rules.replica-query.data-sources.ds_0.name=rq-ds0
spring.shardingsphere.rules.replica-query.data-sources.ds_0.primary-data-source-name=ds0
spring.shardingsphere.rules.replica-query.data-sources.ds_0.replica-data-source-names=ds0,ds1,ds2
# Load balance 算法
spring.shardingsphere.rules.replica-query.load-balancers.rq-ds0.type=round_robin
spring.shardingsphere.rules.replica-query.load-balancers.rq-ds0.props.null=

总结

读写分离其实很简单 , 主要是不知道新版的配置方式 , 希望这篇文档对以后排错能有所帮助

本文转载自: 掘金

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

MySQL查询优化(七):MySQL 的 Count (*)

发表于 2021-05-09

优化COUNT函数的查询在 MySQL 中最容易被误解的话题中能够排进前10名,我们可以在网上搜索了解更多关于 COUNT 优化的误解信息。在进行优化前,理解 COUNT 到底做了什么很重要。

COUNT 函数做什么用?

COUNT 是一个专用的函数,通常有两种不同的方式:计算值和数据行。值指的是非空(Non-NULL)表达式(NULL表示值缺失)。如果我们在 COUNT的参数中指定了列名或其他表达式,则 COUNT 函数是计算该表达式拥有值的次数。这让很多人困惑,相当一部分的原因是值和 NULL 的概念是模糊的。

另一种 COUNT 的形式是简单地计算结果集的数据行数。这是在 MySQL 知道 COUNT 函数参数的表达式不可能为 NULL 时的计算方式。最为典型的例子是 COUNT(*),你也许会以为这是展开数据表的全部列的一种替代形式。事实上,它会忽略了全部列而仅仅对数据行数进行记数。

一个经常犯的错误是我们在 COUNT 的参数里指定了列名然后以为是对数据行进行计数。如果你是想获取结果中的行数,你应该一直使用 COUNT(*),这会使得你的查询语句意图更明确并且可以避免性能问题。

MyISAM 的“神奇”之处

一个常见的误解是 MyISAM 对于 COUNT 查询来说会非常快。MyISAM 的 COUNT 查询确实快,但这种快的场景十分有限:COUNT()查询并且没有 WHERE 条件时才能达到这样的效果,而实际这种场景很少见。MySQL 能够对这个语句进行优化的原因是存储引擎总是知道数据表的准确行数。如果 MySQL 知道一个列col不可能为 NULL,它也会将 COUNT(col) 转换为 COUNT()来进行优化。

MyISAM在 COUNT 查询中有 WHERE条件、或其他对值进行计数时 并没有“神奇”之处。相比其他存储引擎可能快也可能慢,这取决于很多其他因素。

简单的COUNT优化

当你想要对数据行的索引覆盖不高的情况,又需要统计所有行数量时可以采用 MyISAM 引擎的 COUNT(*)来进行优化。下面的例子使用了标准的世界数据库去展示查找 ID 大于5的城市数量时的优化力度,你写出的SQL 语句可能如下所示:

1
sql复制代码SELECT COUNT(*) FROM world.City WHERE ID > 5;

如果使用 SHOW STATUS 检查查询的话会发现扫描了4079行。而如果是采用负向条件查询,并且减去那些 ID 小于等于5的城市数量的话,你会发现可以将扫描结果减少到5行。

1
sql复制代码SELECT (SELECT COUNT(*) FROM world.City) - COUNT(*) FROM world.City WHERE ID <= 5;

这个查询会读取更少的行是因为在查询优化阶段将查询转换为了常量,使用 EXPLAIN 可以看到:

id select_type table rows Extra
1 PRIMARY City 6 Using where; Using index
2 SUBQUERY NULL NULL Select tables optimized way

一个常见的问题是如何在一个查询语句中完成对同一列的不同值的数量的查询。例如,你想通过一条查询语句查出不同颜色对应的数量。你不能使用诸如 SELECT COUNT(color = 'blue' OR color='red') FROM items来完成查询,因为这样不会区分出不同颜色相应的数量。而你也不能将颜色放入 WHERE 条件中,例如 SELECT COUNT(*) FROM items WHERE color = 'blue' AND color = 'red'由于颜色本身是互斥的,因此可以用下面的方法解决这一问题:

1
2
sql复制代码SELECT SUM(IF(color = 'blue', 1, 0)) AS blue, 
SUM(IF(color = 'red', 1, 0)) as red FROM items;

还有一种变通的形式是不是要 SUM,而是 COUNT,只是保证了没有值的表达式的判决表达式是 false:

1
2
sql复制代码SELECT COUNT(color = 'blue' OR NULL) as blue,
COUNT(color = 'red' OR NULL) as red FROM items;

使用近似值

有时候并不需要精确的数量,这个时候就可以使用近似值。在 EXPLAIN优化器中给出的估计行数通常可以满足这种场景,此时可以使用 EXPLAIN 来替代真实的查询。

在很多情况下,一个准确的数量与近似值相比低效很多。一个客户曾经要求统计他们网站的活跃用户数量。用户数量被缓存并每隔30分钟更新一次。这本身就不准确,因此使用估计值是可以接受的。这个查询使用了多个 WHERE 条件去保证不会统计非活跃用户或默认用户(拥有特殊的 ID)。移除这些条件,并稍微修改一下 count 操作就可以变得更高效。一个更进一步的优化是移除不必要的 DISTINCT 操作,从而移除掉一次 filesort 操作。优化后的查询速度更快,且返回了几乎准确的结果。

更复杂的优化

通常来说,COUNT查询很难优化,这是因为它通常需要统计很多行(访问很多数据),在 MySQL 中其他可选的办法是使用覆盖索引。如果那还不够的话,可能需要对整个系统应用架构进行调整了。例如考虑统计数据表,或者使用外部的缓存系统(如 Memcached)。我们往往会面临一个类似的两难问题:快速、准确和简单——你只能从中选择两项!

本文转载自: 掘金

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

诊断神器Xrebel和Arthas 诊断工具Xrebel和A

发表于 2021-05-09

诊断工具Xrebel和Arthas

Xrebel

1
2
java复制代码//添加jvm参数
-javaagent:"C:\Program Files\Java\xrebel.jar"

可以看到每一项的耗时,第一次访问接口速度明显十分慢,懒加载的问题。这里可以看到大部分的时间都用在SHA256加密验证上了。

image.png

数据库连接断开也会报告
image.png

功能还是比较全面的
image.png

Arthas

java -jar直接启动,然后选择需要监听的线程

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
java复制代码C:\Users\kyz\Downloads>java -jar arthas-boot.jar
[INFO] arthas-boot version: 3.4.5
[INFO] Found existing java process, please choose one and input the serial number of the process, eg : 1. Then hit ENTER.
* [1]: 11044 org.jetbrains.jps.cmdline.Launcher
[2]: 12616
[3]: 15836 ltd.wiki.water.WaterApplication
3
[INFO] Start download arthas from remote server: https://arthas.aliyun.com/download/3.5.0?mirror=aliyun
[INFO] File size: 12.22 MB, downloaded size: 9.68 MB, downloading ...
[INFO] Download arthas success.
[INFO] arthas home: C:\Users\kyz\.arthas\lib\3.5.0\arthas
[INFO] Try to attach process 15836
[INFO] Attach process 15836 success.
[INFO] arthas-client connect 127.0.0.1 3658
,---. ,------. ,--------.,--. ,--. ,---. ,---.
/ O \ | .--. ''--. .--'| '--' | / O \ ' .-'
| .-. || '--'.' | | | .--. || .-. |`. `-.
| | | || |\ \ | | | | | || | | |.-' |
`--' `--'`--' '--' `--' `--' `--'`--' `--'`-----'


wiki https://arthas.aliyun.com/doc
tutorials https://arthas.aliyun.com/doc/arthas-tutorials.html
version 3.5.0
main_class
pid 15836
time 2021-04-02 21:02:33
1
2
shell复制代码//查看帮助
help

image.png

1
2
shell复制代码//查看线程占用,Xrebel都给找出来了
thread

image.png

1
2
3
4
5
6
7
8
9
10
11
shell复制代码//查看死锁
thread -b

[arthas@2997]$ thread -b
"Thread-3" Id=29 BLOCKED on java.lang.Object@3f20bf9 owned by "Thread-4" Id=30
at com.spareyaya.jvm.service.DeadLockService.service1(DeadLockService.java:27)
- blocked on java.lang.Object@3f20bf9
- locked java.lang.Object@2fea801a <---- but blocks 1 other threads!
at com.spareyaya.jvm.controller.JVMController.lambda$deadLock$0(JVMController.java:37)
at com.spareyaya.jvm.controller.JVMController$$Lambda$456/748979989.run(Unknown Source)
at java.lang.Thread.run(Thread.java:748)
1
2
shell复制代码//查看内存是否泄露
dashboard

image.png

本文转载自: 掘金

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

SpringBoot集成Swagger3,还想来份离线文档?

发表于 2021-05-08

前言

随着项目架构的演化,前后端分离是不可阻挡的趋势。这种模式的协作在实践的过程中经常会遇到的一个问题就是文档。

在《一位CTO告诉我,项目中至少需要这3类文档》一文我们已经描述了文档的重要性,而接口文档便是其中之一,可以说是必不可少的。

但编写接口文档对开发人员来说是一大难题,而且接口还在不断的变化,还要花费精力去维护接口文档的更新。

既然存在痛点,那么必须会出现解决此痛点的产品,这就是Swagger,目前已经更新到Swagger3版本了。如果你还停留在Swagger2,建议升级到Swagger3,整体UI风格及交互友好了不少。

本篇将围绕Swagger3与SpringBoot的集成和离线文档的生成来进行讲解。

Swagger简介

Swagger是一个规范和完整的框架,用于生成、描述、调用和可视化RESTful风格的Web服务。总体目标是使客户端和文件系统作为服务器以同样的速度来更新。文件的方法,参数和模型紧密集成到服务器端的代码,允许API来始终保持同步。

官网:swagger.io

Swagger解决的痛点

传统方式提供文档有以下痛点:

  • 接口众多,实现细节复杂,编写文档耗费费力,需要持续维护;
  • 接口文档需要随时进行同步;
  • 接口返回的结果不明确,得构造返回结构体等;
  • 不能直接在线测试接口,通常需要额外的工具,比如PostMan等。

当引入Swagger之后,以上痛点迎刃而解,同时还带来以下优点:

  • 及时性 (接口变更后,前后端人员可实时看到最新版本)
  • 规范性 (接口具体统一风格,如接口地址,请求方式,参数,响应格式和错误信息等)
  • 一致性 (接口信息一致,不会因接口文档版本问题出现分歧)
  • 可测性 (可直接基于接口文档进行测试)

Swagger3的改变

Swagger3.0的改动,官方文档总结如下几点:

  • 删除了对springfox-swagger2的依赖;
  • 删除所有@EnableSwagger2…注解;
  • 添加了springfox-boot-starter依赖项;
  • 移除了guava等第三方依赖;
  • 文档访问地址改为http://ip:port/project/swagger-ui/index.html。

下面就来实战使用一下吧。

SpringBoot集成Swagger3

SpringBoot集成Swagger3与SpringBoot集成其他框架的套路基本一致,通常包括:引入依赖、指定配置文件、创建配置类和使用。

引入依赖

在SpringBoot项目的pom.xml中引入Swagger3依赖:

1
2
3
4
5
xml复制代码<dependency>
<groupId>io.springfox</groupId>
<artifactId>springfox-boot-starter</artifactId>
<version>3.0.0</version>
</dependency>

指定配置文件

通常情况下swagger只能在开发环境或测试环境下开启,生产环境下需要进行关闭的。而swagger的开启与关闭可在application.properties中进行配置:

1
2
ini复制代码# 生产环境需设置为false
springfox.documentation.swagger-ui.enabled=true

配置类

通过@EnableOpenApi注解启动用Swagger的使用,同时在配置类中对Swagger的通用参数进行配置。

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
less复制代码@Configuration
@EnableOpenApi
public class Swagger3Config {

@Bean
public Docket createRestApi() {
//返回文档摘要信息
return new Docket(DocumentationType.OAS_30)
.apiInfo(apiInfo())
.select()
.apis(RequestHandlerSelectors.withMethodAnnotation(Operation.class))
.paths(PathSelectors.any())
.build()
.globalRequestParameters(getGlobalRequestParameters())
.globalResponses(HttpMethod.GET, getGlobalResponseMessage())
.globalResponses(HttpMethod.POST, getGlobalResponseMessage());
}

/**
* 生成接口信息,包括标题、联系人等
*/
private ApiInfo apiInfo() {
return new ApiInfoBuilder()
.title("Swagger3接口文档")
.description("如有疑问,可联系二师兄,微信:zhuan2quan")
.contact(new Contact("二师兄", "https://www.choupangxia.com/", "secbro2@gmail.com"))
.version("1.0")
.build();
}

/**
* 封装全局通用参数
*/
private List<RequestParameter> getGlobalRequestParameters() {
List<RequestParameter> parameters = new ArrayList<>();
parameters.add(new RequestParameterBuilder()
.name("uuid")
.description("设备uuid")
.required(true)
.in(ParameterType.QUERY)
.query(q -> q.model(m -> m.scalarModel(ScalarType.STRING)))
.required(false)
.build());
return parameters;
}

/**
* 封装通用响应信息
*/
private List<Response> getGlobalResponseMessage() {
List<Response> responseList = new ArrayList<>();
responseList.add(new ResponseBuilder().code("404").description("未找到资源").build());
return responseList;
}
}

通过以上配置已经完成了Spring Boot与Swagger的集成,下面展示一下如何在业务逻辑中进行使用。

业务中使用

创建两个实体类Goods(商品类)和CommonResult(通用返回结果类)。

Goods类:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
kotlin复制代码@ApiModel("商品模型")
public class Goods {

/**
* 商品id
*/
@ApiModelProperty("商品ID")
Long goodsId;

/**
* 商品名称
*/
@ApiModelProperty("商品名称")
private String goodsName;

/**
* 商品价格
*/
@ApiModelProperty("商品价格")
private BigDecimal price;

// 省略getter/setter
}

CommonResult类:

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
kotlin复制代码@ApiModel("API通用数据")
public class CommonResult<T> {

/**
* 标识代码,0表示成功,非0表示出错
*/
@ApiModelProperty("标识代码,0表示成功,非0表示出错")
private Integer code;

/**
* 描述信息,通常错时使用
*/
@ApiModelProperty("错误描述")
private String msg;

/**
* 业务数据
*/
@ApiModelProperty("业务数据")
private T data;

public CommonResult(Integer status, String msg, T data) {
this.code = status;
this.msg = msg;
this.data = data;
}

/**
* 成功
*/
public static <T> CommonResult<T> success(T data) {
return new CommonResult<>(0, "成功", data);
}

public static <T> CommonResult<T> success(Integer code, String msg) {
return new CommonResult<>(code, msg, null);
}

/**
* 错误
*/
public static <T> CommonResult<T> error(int code, String msg) {
return new CommonResult<>(code, msg, null);
}

// 省略getter/setter
}

下面针对Controller层的接口来使用Swagger对应的API。

GoodsController类:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
less复制代码@Api(tags = "商品信息管理接口")
@RestController
@RequestMapping("/goods")
public class GoodsController {

@Operation(summary = "单个商品详情")
@GetMapping("/findGoodsById")
public CommonResult<Goods> findGoodsById(
@Parameter(description = "商品ID,正整数")
@RequestParam(value = "goodsId", required = false, defaultValue = "0") Integer goodsId) {
System.out.println("根据商品ID=" + goodsId + "查询商品详情");
Goods goods = new Goods();
goods.setGoodsId(1L);
goods.setGoodsName("笔记本");
goods.setPrice(new BigDecimal(8888));
return CommonResult.success(goods);
}
}

OrderController类:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
less复制代码@Api(tags = "订单管理接口")
@RestController
@RequestMapping("/order")
public class OrderController {

@Operation(summary = "提交订单")
@PostMapping("/order")
@ApiImplicitParams({
@ApiImplicitParam(name = "userId", value = "用户id", dataTypeClass = Long.class, paramType = "query", example = "123"),
@ApiImplicitParam(name = "goodsId", value = "商品id", dataTypeClass = Integer.class, paramType = "query", example = "1")
})
public CommonResult<String> toBuy(@ApiIgnore @RequestParam Map<String, String> params) {
System.out.println(params);
return CommonResult.success("success");
}
}

展示效果

完成集成,启动SpringBoot项目,在访问地址:

1
arduino复制代码http://127.0.0.1:8080/swagger-ui/index.html

从整体上可以看到如下效果:
swagger

具体的商品信息管理接口,可以看到请求参数、返回结果数据结构等信息,点击“Try it out”,可输入参数请求参数,进行接口的调用:
swagger

调用之后会返回对应的处理结果:

swagger

在最下面的Schemas中还可以看到对应返回结果数据和被Swagger注解的实体类信息。

swagger

Swagger3注解使用说明

经过上述实例之后,我们知道大多数API是如何使用的了,这了再汇总一下相关API的功能:

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
less复制代码@Api:用在请求的类上,表示对类的说明
tags="说明该类的作用,可以在UI界面上看到的注解"
value="该参数没什么意义,在UI界面上也看到,所以不需要配置"

@ApiOperation:用在请求的方法上,说明方法的用途、作用
value="说明方法的用途、作用"
notes="方法的备注说明"

@ApiImplicitParams:用在请求的方法上,表示一组参数说明
@ApiImplicitParam:用在@ApiImplicitParams注解中,指定一个请求参数的各个方面
name:参数名
value:参数的汉字说明、解释
required:参数是否必须传
paramType:参数放在哪个地方
· header --> 请求参数的获取:@RequestHeader
· query --> 请求参数的获取:@RequestParam
· path(用于restful接口)--> 请求参数的获取:@PathVariable
· body(不常用)
· form(不常用)
dataType:参数类型,默认String,其它值dataType="Integer"
defaultValue:参数的默认值

@ApiResponses:用在请求的方法上,表示一组响应
@ApiResponse:用在@ApiResponses中,一般用于表达一个错误的响应信息
code:数字,例如400
message:信息,例如"请求参数没填好"
response:抛出异常的类

@ApiModel:用于响应类上,表示一个返回响应数据的信息
(这种一般用在post创建的时候,使用@RequestBody这样的场景,
请求参数无法使用@ApiImplicitParam注解进行描述的时候)
@ApiModelProperty:用在属性上,描述响应类的属性

导出离线文档

Swagger为我们提供了方便的在线文档支持,但某些场景下我们需要把接口文档提供给合作人员,而不是直接给一个地址。此时,我们就需要将接口文档导出为离线文档。

这里我们集成增强文档knife4j来实现离线文档的导出。

添加knife4j依赖

在pom.xml中增加knife4j的依赖:

1
2
3
4
5
xml复制代码<dependency>
<groupId>com.github.xiaoymin</groupId>
<artifactId>knife4j-spring-boot-starter</artifactId>
<version>3.0.2</version>
</dependency>

启动knife4j

在上面配置Swagger的Swagger3Config中添加@EnableKnife4j注解,该注解可以开启knife4j的增强功能。

1
2
3
4
5
6
less复制代码@EnableKnife4j
@Configuration
@EnableOpenApi
public class Swagger3Config {
// ...
}

此时,如果依旧访问http://localhost:8080/swagger-ui/index.html 会发现显示并没有变化。这里我们需要访问http://localhost:8088/doc.html。

整个项目源码地址:github.com/secbr/sprin…

展示效果

此时启动项目,访问doc.html之后,你会发现现在文档风格变得非常酷炫。展示几个效果图来看看:

swagger

swagger

swagger

swagger

其中在“离线文档”一栏中可以看到四种形式的离线文档下载:Markdown、HTML、Word、OpenAPI。

swagger-09.jpg

其中个人感觉HTML格式的文档更具有没敢,也更方便查看,来一张图看看效果。

swagger-10.jpg

小结

文档是项目中必须的,但随着开源框架的发展,对技术人员来说文档的痛点也在逐步解决。如果你还处于手写文档的阶段,真的可以尝试一下这类更友好的文档展现形式。

发掘新事物,尝试新鲜事物,才能更快的成长,有更多的Plan B。

博主简介:《SpringBoot技术内幕》技术图书作者,酷爱钻研技术,写技术干货文章。

公众号:「程序新视界」,博主的公众号,欢迎关注~

技术交流:请联系博主微信号:zhuan2quan

本文转载自: 掘金

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

1…672673674…956

开发者博客

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