只是记录自己不知道的知识点,或者经常容易忘的内容,想知道更多的信息建议看课程或者找我聊天交换技能。在课程的内容基础之上,补充了一些自己知道的知识点。

Kafka的认知

  • 分布式消息引擎平台
  • 分布式实时流式处理平台

早期Kafka社区对Kafka的定位为⼀个分布式、分区化且带备份功能的提交⽇志(Commit Log)服务,近期在官网彻底更改为分布式实时流式处理平台。

Kafka流式处理框架的优势

  • 更容易实现端到端的正确性(Correctness)
  • 轻量型,嵌入式流式计算的定位

避免不必要的Rebalance

  • session.timeout.ms
  • heartbeat.interval.ms
  • max.poll.interval.ms
  • GC参数

session.timout.ms决定了Consumer存活性的时间间隔

heartbeat.interval.ms决定存活心跳发送间隔。

max.poll.interval.ms 它限定了Consumer端应⽤程序两次调⽤poll⽅法的最⼤时间间隔。

消费者TCP管理

消费者实例在KafkaConsumer.poll建立TCP连接,主要分为3类连接:

  1. 确定协调者和获取集群元数据。
  2. 连接协调者,令其执⾏组成员管理操作。
  3. 执⾏实际的消息获取。

第一类连接仅在开始前建立,稍后(第三类创建成功)就会销毁,consumer实例会长期保留2,3类连接。

Consumer实例会长期建立broker数量(分区所在broker数量)+1个连接。

TCP连接的三个时机:

  1. 发起FindCoordinator请求时
  2. 连接协调者时
  3. 消费数据时

何时关闭TCP连接:

  1. ⼿动调⽤KafkaConsumer.close()⽅法
  2. 执⾏Kill命令
  3. Kafka⾃动关闭(是由消费者端参数connection.max.idle.ms控制的,该参数现在的默认值是9分钟)
阅读全文 »

概要

记录线上Zookeeper集群和Kafka集群部署过程,操作系统配置,以及一些参数的设置。给大家部署提供一些宝贵意见和参考。部署一个集群,按照官方社区的文档,很容易就搭建一个集群,但是为了更好的发挥集群的性能,有很多设置是可以避免产生不必要的问题,都是在惨痛的教训中产生的经验。

本文内容来自NeweggConfluent 产线上Kafka cluster运维经验,仅供参考。

Newegg 产线Kafka版本选择Confluent发行版本,版本对照表:

Confluent Platform Apache Kafka
5.5.x 2.5.x

Docker镜像列表:

Service Info
Zookeeper confluentinc/cp-zookeeper:5.5.1
Kafka confluentinc/cp-kafka:5.5.1

Zookeeper Cluster

硬件

内存至少4GB,Zookeeper对swap敏感,应当避免swap。

集群配置

zookeeper节点数据应该为2n+1,n大于0,保持集群中超过一半节点存活。

Confluent docker 镜像参数设置是在ENV(环境变量)中设置,以ZOOKEEPER开头,以_替换.

参数清单

1
2
3
4
5
6
7
8
9
10

- ZOOKEEPER_SERVER_ID=1
- ZOOKEEPER_TICK_TIME=2000
- ZOOKEEPER_CLIENT_PORT=2181
- ZOOKEEPER_INIT_LIMIT=10
- ZOOKEEPER_SYNC_LIMIT=5
- ZOOKEEPER_SERVERS=192.168.0.1:2888:3888;192.168.0.2:2888:3888;192.168.0.3:2888:3888
- ZOOKEEPER_AUTOPURGE_SNAP_RETAIN_COUNT=5
- ZOOKEEPER_AUTOPURGE_PURGE_INTERVAL=10
- KAFKA_HEAP_OPTS=-Xmx4G -Xms4G

ZOOKEEPER_AUTOPURGE_SNAP_RETAIN_COUNT控制快照的数量,ZOOKEEPER_AUTOPURGE_PURGE_INTERVAL控制清除快照的时间间隔,默认不清除,这个很重要。

文件映射

Host volume Container volume
/opt/app/cp-zookeeper-5.5.1/data /var/lib/zookeeper/data
/opt/app/cp-zookeeper-5.5.1/log /var/lib/zookeeper/log
/etc/localtime /etc/localtime
阅读全文 »

前言

本篇文章介绍生成一个自签名SSL证书以及使用Nginx docker 代理一个https服务。

SSL证书验证安全连接,有两种验证模式:

  1. 仅客户端验证服务器的证书,客户端自己不提供证书;
  2. 客户端和服务器都互相验证对方的证书。

显然第二种更安全,一般web采用第一种,比较简单。

创建自签名证书

创建步骤

  1. 创建Key;
  2. 创建签名请求;
  3. 将Key的口令移除;
  4. 用Key签名证书。

创建脚本

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
#!/bin/sh

# create self-signed server certificate:

read -p "Enter your domain [www.example.com]: " DOMAIN

echo "Create server key..."

openssl genrsa -des3 -out $DOMAIN.key 2048

echo "Create server certificate signing request..."

SUBJECT="/C=US/ST=Mars/L=iTranswarp/O=iTranswarp/OU=iTranswarp/CN=$DOMAIN"

openssl req -new -subj $SUBJECT -key $DOMAIN.key -out $DOMAIN.csr

echo "Remove password..."

mv $DOMAIN.key $DOMAIN.origin.key
openssl rsa -in $DOMAIN.origin.key -out $DOMAIN.key

echo "Sign SSL certificate..."

openssl x509 -req -days 3650 -in $DOMAIN.csr -signkey $DOMAIN.key -out $DOMAIN.crt

echo "TODO:"
echo "Copy $DOMAIN.crt to /etc/nginx/ssl/$DOMAIN.crt"
echo "Copy $DOMAIN.key to /etc/nginx/ssl/$DOMAIN.key"
echo "Add configuration in nginx:"
echo "server {"
echo " ..."
echo " listen 443 ssl;"
echo " ssl_certificate /etc/nginx/ssl/$DOMAIN.crt;"
echo " ssl_certificate_key /etc/nginx/ssl/$DOMAIN.key;"
echo "}"

执行以上脚本会生成4个文件:

核心配置

1
2
3
4
5
6
7
8
9
10
11
 server {
listen 443 ssl;
server_name localhost;

ssl_certificate www.example.com.crt;
ssl_certificate_key www.example.com.key;

location / {
proxy_pass http://localhost:8866/;
}
}

完整配置

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
events {
worker_connections 1024;
}

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

sendfile on;
tcp_nopush on;
keepalive_timeout 65;
gzip on;
include /etc/nginx/conf.d/*.conf;
index index.html index.htm;

server {
listen 80;
server_name localhost;

location / {
proxy_pass http://localhost:8866/;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
}
}

server {
listen 443 ssl;
server_name localhost;

ssl_certificate www.example.com.crt;
ssl_certificate_key www.example.com.key;

location / {
proxy_pass http://localhost:8866/;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
proxy_cookie_path / "/; httponly; secure; SameSite=NONE";
}
}
}

启动nginx容器

1
docker run --name my-custom-nginx-container -p 443:443 -v "F:\docker-workspace\nginx-demo\nginx.conf:/etc/nginx/nginx.conf:ro" -v "F:\docker-workspace\nginx-demo\www.example.com.crt:/etc/nginx/www.example.com.crt" -v "F:\docker-workspace\nginx-demo\www.example.com.key:/etc/nginx/www.example.com.key" -d nginx

参考

  1. 给Nginx配置一个自签名的SSL证书

Kafka Summit 2020 -Apache Kafka - The Next 10 Years

前言

Kafka在Confulent成立后发展很快,很明显的变化是发布了很多重大的版本。了解未来的规划,对于我们学习和使用kafka有很大的意义。

Kafka Summit 2020 已经在8-24召开,最近抽出时间看了一些视频,由于自己英语是二把刀,因此本文是自己对该主题的自己理解,尽可能还原Gwen Shapira 的分享(Apache Kafka - The Next 10 Years),中文全网唯一

Kafka 设计原则

High Performance from First Principles

Principles in Action: Elasticity

Principles in Action: Scalability

Principles in Action: Operationally Friend

Design Considerations in Action

KIP-405: Kafka Tiered Storage

Kafka数据在流式中通常使用尾部读取。尾部读取利用操作系统的页面缓存来提供数据,而不是磁盘读取。旧数据和故障恢复通常会从磁盘读取,这些通常很少见。

在分层存储方法中,Kafka集群配置了两层存储-本地和远程。本地层与当前的Kafka相同,使用Kafka broker上的本地磁盘存储日志段。新的远程层使用HDFS或S3等系统来存储完整的日志段。对应于每个层定义了两个单独的保留期。启用远程层后,可以将本地层的保留期从几天显着减少到几个小时。远程层的保留期可能会更长,几天甚至几个月。当日志段在本地层上滚动时,它将与相应的偏移量索引一起复制到远程层。延迟敏感的应用程序执行尾部读取,并利用现有的Kafka机制从本地层提供服务,该机制有效地使用页面缓存来提供数据。回填和应用程序从故障层恢复,需要比本地层中的数据更旧的数据从远程层提供服务。

该解决方案允许扩展存储独立于Kafka集群中的内存和CPU的存储,从而使Kafka成为长期存储解决方案。这也减少了在Kafka代理上本地存储的数据量,因此减少了在恢复和重新平衡期间需要复制的数据量。远程层中可用的日志段无需在代理上还原或延迟还原,而是从远程层提供。这样,增加保留期不再需要扩展Kafka群集存储和添加新节点。同时,总体数据保留时间仍然可以更长,从而无需使用单独的数据管道来将数据从Kafka复制到外部存储,就像目前在许多部署中所做的那样。

KIP-500: Replace ZooKeeper with a Self-Managed Metadata Quorum

主要目的是让部署更简单,配置更简单,用log存储元数据。

主要解决的两三个问题:The Controller Quorum和Broker Metadata Management,以及The Broker State Machine

下一代Kafka架构

更插件化,更弹性,更像云服务一样

Elastic

增加,移除brokers更弹性

Integrated

像使用log一样使用kafka,可以很好的和其他系统集成:像S3、HDFS 等所有的存储系统

Infinite

无限存储,你可以增加更多的broker,没有任何限制,并且不会影响性能。

参考

  1. Keynote Day 1 Morning | Kafka Summit 2020

  2. kafka-summit.org/2020-schedule

  3. 分布式系统理论之Quorum机制

  4. Kafka Improvement Proposals

ice-scripts到ice.js实战迁移之路

TrumanDu github stats

为什么要升级?

纬度\版本 icejs 1.x ice-scripts 2.x ice-scripts 1.x
定位 研发框架 构建工具 构建工具
配置文件 build.json ice.config.js package.json(buildConfig)
文档地址 访问 访问 访问
发布时间 2020.02 2019.06 2018.02
可渐进升级性 不好 不好
插件能力 工程+运行时 工程
工程配置
运行时配置 默认支持 默认不支持 默认不支持
SSR 支持 不支持 不支持

如果你看了这个对比还无法决定,那么说一说我迁移的原因:

  1. ice-scripts官方不维护,查找文档较难
  2. 解决技术债
  3. 我想使用一些新的功能,例如:Hooks and Function Components(当然并不是说不升级就不能用)
  4. 新的前端工程方式,我之所以这么命名,因为我不是一个专业的前端开发,无法将自己的注意力集中在前端领域,只好跟着大厂,这样就不会迷路。这次新版本配置的eslint prettier挺有用。
  5. 前端权限的简洁化(之前推荐的是ant deisgn auth真心不好用)
  6. 布局的简介化

Hooks and Function Components扫盲

快速一览

props非必须,两种方式:

1
2
3
4
const Example = (props) => {
// You can use Hooks here!
return <div />;
}
1
2
3
4
function Example(props) {
// You can use Hooks here!
return <div />;
}

使用useState处理函数组件的状态,例如:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
import React, { useState } from 'react';

function Example() {
const [count, setCount] = useState(0);

return (
<div>
<p>You clicked {count} times</p>
<button onClick={() => setCount(count + 1)}>
Click me
</button>
</div>
);
}

这里要注意的是setCount异步函数,它不会改变count,而是创建一个新的count。而且在接受同一个 state 对象时,即使其对象里的属性变了,但对象地址没变,是不会更新视图的。

函数中的 setXXX 注意事项:

1. 不可局部更新视图

2. 地址一定要变

阅读全文 »

设计模式总结

创建型

创建型模式主要解决对象的创建问题,封装复杂的创建过程,解耦对象的创
建代码和使⽤代码。

单例模式⽤来创建全局唯⼀的对象。

⼯⼚模式⽤来创建不同但是相关类型的对象(继承同⼀⽗类或者接⼝的⼀组⼦类),由给定的参数来决定创建哪种类型的对象。

建造者模式是⽤来创建复杂对象,可以通过设置不同的可选参数,“定制化”地创建不同的对象。

原型模式针对创建成本⽐较⼤的对象,利⽤对已有对象进⾏复制的⽅式进⾏创建,以达到节省创建时间的⽬的。

单例模式

懒汉

1
2
3
4
5
6
7
8
9
10
11
12
13
public class LazySingleton {
private static volatile LazySingleton instance = null;

private LazySingleton() {
}

public synchronized LazySingleton getInstance() {
if (instance == null) {
instance = new LazySingleton();
}
return instance;
}
}

饿汉

1
2
3
4
5
6
7
8
9
10
public class HungrySingleton {
private static HungrySingleton instance = new HungrySingleton();

private HungrySingleton() {
}

public static HungrySingleton getInstance() {
return instance;
}
}

静态内部类

1
2
3
4
5
6
7
8
9
10
11
12
public class StaticInnerClassSingleton {
private StaticInnerClassSingleton() {
}

public static StaticInnerClassSingleton getInstance() {
return LazyHolder.innerClassSingleton;
}

private static class LazyHolder {
private static StaticInnerClassSingleton innerClassSingleton = new StaticInnerClassSingleton();
}
}

枚举类

1
2
3
4
5
6
7
public enum SingletonEmum {
INSTANCE;

public void doSomething() {
System.out.println("value");
}
}

工厂模式

工厂方法与抽象工厂的区别是:抽象工厂可以在一个工厂内生产多个商品,工厂方法只能生产一个

主要角色

  • 抽象工厂(Abstract Factory):提供了创建产品的接口,调用者通过它访问具体工厂的工厂方法 newProduct() 来创建产品。
  • 具体工厂(ConcreteFactory):主要是实现抽象工厂中的抽象方法,完成具体产品的创建。
  • 抽象产品(Product):定义了产品的规范,描述了产品的主要特性和功能。
  • 具体产品(ConcreteProduct):实现了抽象产品角色所定义的接口,由具体工厂来创建,它同具体工厂之间一一对应。

工厂方法模式

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
interface Factory{
void produce();
}
class CarFactory implements Factory{
@Override
public void produce() {
System.out.println("汽车工程生产");
new Bus();
}
}
class SuperCarFactory implements Factory{
@Override
public void produce() {
System.out.println("超跑汽车工程生产");
new SuperCar();
}
}

interface Car{
void show();
}

class Bus implements Car{
@Override
public void show() {
System.out.println("公共汽车。。。");
}
}
class SuperCar implements Car{
@Override
public void show() {
System.out.println("超跑。。。");
}
}

抽象工厂模式

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
interface Factory {
Product produceEngine();
Product produceTyre();
}

class CarFactory implements Factory {

@Override
public Product produceEngine() {
System.out.println("汽车工厂:生产发动机");
return new Engine();
}

@Override
public Product produceTyre() {
System.out.println("汽车工厂:生产发轮胎");
return new Tyre();
}
}

class SuperCarFactory implements Factory {

@Override
public Product produceEngine() {
System.out.println("超跑汽车工厂:生产发动机");
return new Engine();
}

@Override
public Product produceTyre() {
System.out.println("超跑汽车工厂:生产发轮胎");
return new Tyre();
}
}

interface Product {
void show();
}

class Tyre implements Product {
@Override
public void show() {
System.out.println("轮胎。。。");
}
}

class Engine implements Product {
@Override
public void show() {
System.out.println("发动机。。。");
}
}

建造者模式

适用场景

参数很多的bean初始化,可以对参数的强约束。

实现

使用静态内部类实现:

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
public class Bean {
private String a;
private String b;
private String c;
private String d;

private Bean(Builder builder) {
a = builder.a;
b = builder.b;
c = builder.c;
d = builder.d;
}

static class Builder {
private String a;
private String b;
private String c;
private String d;

public Builder a(String a) {
this.a = a;
return this;
}

public Builder b(String b) {
this.b = b;
return this;
}

public Builder c(String c) {
this.c = c;
return this;
}

public Builder d(String d) {
this.d = d;
return this;
}

public Bean build() {
return new Bean(this);
}
}
}

使用:

1
Bean bean = new Bean.Builder().a("a").b("b").c("c").build();

原型模式

原型模式的克隆分为浅克隆和深克隆,Java 中的 Object 类提供了浅克隆的 clone() 方法,具体原型类只要实现 Cloneable 接口就可实现对象的浅克隆

结构型

代理模式

主要角色

  • 抽象主题(Subject)类:通过接口或抽象类声明真实主题和代理对象实现的业务方法。
  • 真实主题(Real Subject)类:实现了抽象主题中的具体业务,是代理对象所代表的真实对象,是最终要引用的对象。
  • 代理(Proxy)类:提供了与真实主题相同的接口,其内部含有对真实主题的引用,它可以访问、控制或扩展真实主题的功能。

代码实现

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
public interface Subject {
void request();
}
public class ConcreteSubject implements Subject {
@Override
public void request() {
System.out.println("执行请求");
}
}

public class Proxy implements Subject {

private Subject subject = new ConcreteSubject();
public void preRequest() {
System.out.println("pre");
}
@Override
public void request() {
preRequest();
subject.request();
afterRequest();
}
public void afterRequest() {
System.out.println("after");
}
public static void main(String[] args) {
Proxy proxy =new Proxy();
proxy.request();
}
}

桥接模式

定义

桥接(Bridge)模式:将抽象与实现分离,使它们可以独立变化。它是用组合关系代替继承关系来实现,从而降低了抽象和实现这两个可变维度的耦合度。

抽象化和实现化之间使用关联关系(组合或者聚合关系)而不是继承关系,从而使两者可以相对独立地变化,这就是桥接模式的用意。

适用场景:

  • 当一个类存在两个独立变化的维度,且这两个维度都需要进行扩展时。例如包有钱包,手提包,又有颜色(黄、红、蓝)

实现

主要角色

  • 抽象化(Abstraction)角色:定义抽象类,并包含一个对实现化对象的引用。
  • 扩展抽象化(Refined Abstraction)角色:是抽象化角色的子类,实现父类中的业务方法,并通过组合关系调用实现化角色中的业务方法。
  • 实现化(Implementor)角色:定义实现化角色的接口,供扩展抽象化角色调用。
  • 具体实现化(Concrete Implementor)角色:给出实现化角色接口的具体实现。

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
abstract class Bag {
Color color;
public void setColor(Color color) {
this.color = color;
}

abstract void echoName();
}

class HandBag extends Bag {

@Override
void echoName() {
System.out.println(color.getColor() + "的挎包");
}
}
class Wallet extends Bag {

@Override
void echoName() {
System.out.println(color.getColor() + "的钱包");
}
}

interface Color {
String getColor();
}

class Yellow implements Color{

@Override
public String getColor() {
return "黄";
}
}

class Red implements Color{

@Override
public String getColor() {
return "红";
}
}

装饰器模式

定义

装饰(Decorator)模式:指在不改变现有对象结构的情况下,动态地给该对象增加一些职责(即增加其额外功能)的模式

jdk io就使用了装饰器模式,例如,InputStream 的子类 FilterInputStream,OutputStream 的子类 FilterOutputStream,Reader 的子类 BufferedReader 以及 FilterReader,还有 Writer 的子类 BufferedWriter、FilterWriter 以及 PrintWriter 等,它们都是抽象装饰类。

1
2
BufferedReader in=new BufferedReader(new FileReader("filename.txtn));
String s=in.readLine();

应用场景

当需要通过对现有的一组基本功能进行排列组合而产生非常多的功能时,采用继承关系很难实现,而采用装饰模式却很好实现。

实现

主要角色

  • 抽象构件(Component)角色:定义一个抽象接口以规范准备接收附加责任的对象。
  • 具体构件(Concrete Component)角色:实现抽象构件,通过装饰角色为其添加一些职责。
  • 抽象装饰(Decorator)角色:继承抽象构件,并包含具体构件的实例,可以通过其子类扩展具体构件的功能。
  • 具体装饰(ConcreteDecorator)角色:实现抽象装饰的相关方法,并给具体构件对象添加附加的责任

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
public interface Component {
void operation();
}
public class ConcreteComponent implements Component {
@Override
public void operation() {
System.out.println("Concrete Component operation");
}
}

public class Decorator implements Component{
Component component;

public Decorator(Component component){
this.component = component;
}
@Override
public void operation() {
component.operation();
}
}

public class ConcreteDecoratorA extends Decorator {

public ConcreteDecoratorA(Component component) {
super(component);
}

public void addFunction(){
System.out.println("add a function");
}

@Override
public void operation() {
addFunction();
super.operation();
}
}

public class ConcreteDecoratorB extends Decorator {

public ConcreteDecoratorB(Component component) {
super(component);
}

public void addFunction(){
System.out.println("add b function");
}

@Override
public void operation() {
addFunction();
super.operation();
}
}

public static void main(String[] args) {
Component component = new ConcreteComponent();
Decorator decorator = new ConcreteDecoratorA(component);
decorator.operation();
}

这样做的好处是可以通过组合形成更多的构件功能,例如如上代码,可以组装出具有a功能的ConcreteComponent,具有b功能的ConcreteComponent,或者新增一个NewConcreteComponent。进而可以组装出具有a功能的NewConcreteComponent

适配器模式

定义

适配器模式(Adapter):将一个类的接口转换成客户希望的另外一个接口,使得原本由于接口不兼容而不能一起工作的那些类能一起工作。适配器模式分为类结构型模式和对象结构型模式两种,前者类之间的耦合度比后者高,且要求程序员了解现有组件库中的相关组件的内部结构,所以应用相对较少些。

实现

主要角色

  • 目标(Target)接口:当前系统业务所期待的接口,它可以是抽象类或接口。
  • 适配者(Adaptee)类:它是被访问和适配的现存组件库中的组件接口。
  • 适配器(Adapter)类:它是一个转换器,通过继承或引用适配者的对象,把适配者接口转换成目标接口,让客户按目标接口的格式访问适配者。

类适配器模式

对象适配器模式

门面模式

定义

外观(Facade)模式的定义:是一种通过为多个复杂的子系统提供一个一致的接口,而使这些子系统更加容易被访问的模式。该模式对外有一个统一接口,外部应用程序不用关心内部子系统的具体的细节,这样会大大降低应用程序的复杂度,提高了程序的可维护性。

实现

主要角色

  • 外观(Facade)角色:为多个子系统对外提供一个共同的接口。
  • 子系统(Sub System)角色:实现系统的部分功能,客户可以通过外观角色访问它。
  • 客户(Client)角色:通过一个外观角色访问各个子系统的功能。

组合模式

定义

组合(Composite)模式的定义:有时又叫作部分-整体模式,它是一种将对象组合成树状的层次结构的模式,用来表示“部分-整体”的关系,使用户对单个对象和组合对象具有一致的访问性

实现

主要角色

  • 抽象构件(Component):它的主要作用是为树叶构件和树枝构件声明公共接口,并实现它们的默认行为。在透明式的组合模式中抽象构件还声明访问和管理子类的接口;在安全式的组合模式中不声明访问和管理子类的接口,管理工作由树枝构件完成。
  • 树叶构件(Leaf):是组合中的叶节点对象,它没有子节点,用于实现抽象构件角色中 声明的公共接口。
  • 树枝构件(Composite):是组合中的分支节点对象,它有子节点。它实现了抽象构件角色中声明的接口,它的主要作用是存储和管理子部件,通常包含 Add()、Remove()、GetChild() 等方法。


亨元模式

定义

享元(Flyweight)模式的定义:运用共享技术来有効地支持大量细粒度对象的复用。它通过共享已经存在的又橡来大幅度减少需要创建的对象数量、避免大量相似类的开销,从而提高系统资源的利用率。

实现

主要角色

  • 抽象享元(Flyweight):是所有的具体享元类的基类,为具体享元规范需要实现的公共接口,非享元的外部状态以参数的形式通过方法传入。
  • 具体享元(Concrete Flyweight):实现抽象享元角色中所规定的接口。
  • 非享元(Unsharable Flyweight):是不可以共享的外部状态,它以参数的形式注入具体享元的相关方法中。
  • 享元工厂(Flyweight Factory):负责创建和管理享元角色。当客户对象请求一个享元对象时,享元工厂检査系统中是否存在符合要求的享元对象,如果存在则提供给客户;如果不存在的话,则创建一个新的享元对象。

行为型

模板方法

定义

模板方法(Template Method)模式:定义一个操作中的算法骨架,而将算法的一些步骤延迟到子类中,使得子类可以不改变该算法结构的情况下重定义该算法的某些特定步骤。

父类中定义公共方法或者执行步骤,有差异的部分由具体的子类去实现。

适用场景:

  1. 算法的整体步骤很固定,但其中个别部分易变时,这时候可以使用模板方法模式,将容易变的部分抽象出来,供子类实现。
  2. 当多个子类存在公共的行为时,可以将其提取出来并集中到一个公共父类中以避免代码重复。首先,要识别现有代码中的不同之处,并且将不同之处分离为新的操作。最后,用一个调用这些新的操作的模板方法来替换这些不同的代码。

实现

主要角色

  1. 抽象类(Abstract Class)
    • 模板方法:定义了算法的骨架,按某种顺序调用其包含的基本方法。
    • 基本方法
      • 抽象方法:在抽象类中申明,由具体子类实现。
      • 具体方法:在抽象类中已经实现,在具体子类中可以继承或重写它。
      • 钩子方法(非必须)在抽象类中已经实现,包括用于判断的逻辑方法和需要子类重写的空方法两种。
  2. 具体子类(ConcreteClass):实现抽象类中所定义的抽象方法和钩子方法

实现demo代码

为了避免子类较多,可以考虑匿名内部类,JedisCluster的命令实现就是一个很好的例子。

开源demo案例

JedisClusterCommand的实现即是一个明显的例子。JedisClusterCommand实现了分槽,获取connect,执行命令,重试等公共操作。子类中实现具体的执行命令。

JedisClusterCommand

JedisCluster.set

观察者模式

定义

观察者(Observer)模式: 指多个对象间存在一对多的依赖关系,当一个对象的状态发生改变时,所有依赖于它的对象都得到通知并被自动更新。这种模式有时又称作发布-订阅模式、模型-视图模式。

实现

主要角色

  • Subject 就是抽象主题:它负责管理所有观察者的引用,同时定义主要的事件操作。
  • ConcreteSubject 具体主题:它实现了抽象主题的所有定义的接口,当自己发生变化时,会通知所有观察者。
  • Observer 观察者:监听主题发生变化相应的操作接口。
  • Concrete Observer 具体观察者,实现抽象观察者中定义的抽象方法

实现demo代码

开源demo案例

命令模式

定义

命令(Command)模式:将一个请求封装为一个对象,使发出请求的责任和执行请求的责任分割开。这样两者之间通过命令对象进行沟通,这样方便将命令对象进行储存、传递、调用、增加与管理。

适合场景

  1. 当系统需要将++请求调用者与请求接收者解耦++时,命令模式使得调用者和接收者不直接交互。
  2. 当系统需要++随机请求命令或经常增加或删除命令++时,命令模式比较方便实现这些功能。
  3. 当系统需要++执行一组操作++时,命令模式可以定义宏命令来实现该功能。
  4. 当系统需要支持命令的撤销(Undo)操作和恢复(Redo)操作时,可以将命令对象存储起来,采用备忘录模式来实现。

实现

主要角色

  • Command 命令:命令接口定义一个抽象方法
  • ConcreteCommand:具体命令,负责调用接受者的相应操作
  • Invoker 请求者:负责调用命令对象执行请求
  • Receiver 接受者:负责具体实施和执行一次请求

实现demo代码

开源demo案例

Tomcat 中命令模式在 Connector 和 Container 组件之间有体现,Tomcat 作为一个应用服务器,无疑会接受到很多请求,如何分配和执行这些请求是必须的功能。

Connector和Container两个接口。Connector是抽象命令请求者,Container是抽象命令接收者,server是这一切的缘由,HttpProcessor是抽象命令。

在tomcat中的实现形式是:server需要Connector来接受来自外接的Http请求,然后Connector接受到请求,并创建了命令HttpProcessor,然后server将这个命令交给了Container接收者。

参考

  1. 图说设计模式
  2. 设计模式

语法

特殊声明

_用法

  • 用在 import _ "github.com/go-sql-driver/mysql"

    程序默认执行init方法

  • 用在函数返回值 _, err := client.Do(req)

    忽略相应的返回值

new 函数

内建的new函数也是一种创建变量的方法,new(type)表示创建一个type类型的匿名变量,并初始化为type类型的零值,返回变量的地址,指针类型为*type

1
2
3
4
p := new(int)   	// p, *int 类型, 指向匿名的 int 变量
fmt.Println(*p) // 0
*p = 2 // 设置 int 匿名变量的值为 2
fmt.Println(*p) // 2

如下函数完成同样的功能:创建变量,返回变量地址

1
2
3
4
5
6
7
func newA() *int {
return new(int)
}
func newB() *int {
var i int
return &i
}

基本类型

bool

string

int int8 int16 int32 int64
uint uint8 uint16 uint32 uint64 uintptr

byte // uint8 的别名

rune // int32 的别名
// 表示一个 Unicode 码点

float32 float64

complex64 complex128
本例展示了几种类型的变量。 同导入语句一样,变量声明也可以“分组”成一个语法块。

命名返回值

没有参数的 return 语句返回已命名的返回值。也就是 直接 返回

1
2
3
4
5
6
7
8
func method() (x, y int) {
x = 1
y = 2
return
}
func main() {
fmt.Println(method()) // 1 2
}

短变量声明

在函数中,简洁赋值语句 := 可在类型明确的地方代替 var 声明。

函数外的每个语句都必须以关键字开始(var, func 等等),因此 := 结构不能在函数外使用。

指针

指针声明var p *int

指针赋值var p *int = &i or pp := &i

空指针:nil

指针使用:主要经过三个步骤:声明、赋值和访问指针指向的变量的值

1
2
3
4
var i int = 10
var p *int = &i
*p = 12
fmt.Println(p,*p)

结构体

1
2
3
4
5
6
7
8
9
10
type Vertex struct {
X, Y int
}

var (
v1 = Vertex{1, 2} // 创建一个 Vertex 类型的结构体
v2 = Vertex{X: 1} // Y:0 被隐式地赋予
v3 = Vertex{} // X:0 Y:0
p = &Vertex{1, 2} // 创建一个 *Vertex 类型的结构体(指针)
)

数组

数组声明及赋值

两种方式:

1
2
3
4
5
6
// 第一种
var a [2]string
a[0] = "hello"
a[1] = "world"
// 第二种
aa :=[2]int{10,20}

遍历

1
2
3
4
5
6
7
8
   // 第一种
for i, s := range a {
fmt.Printf("%d : %s\n", i, s)
}
// 第二种
for i := 0; i < len(aa); i++ {
fmt.Printf("%d : %d\n", i, aa[i])
}

map

map 声明及赋值

1
2
3
4
5
6
7
8
9
// 第一种
var m = make(map[string]string)
m["a"] = "A"
m["b"] = "B"
// 第二种
var m = map[string]string{
"a":"A",
"b":"B",
}

遍历

1

slice 切片

切片并不存储任何数据,它只是描述了底层数组中的一段。

更改切片的元素会修改其底层数组中对应的元素。

与它共享底层数组的切片都会观测到这些修改。

声明

1
2
3
4
5
6
7
8
9
10
11
a := [5]int{1,2,3,4,5}

// 类型 []T 表示一个元素类型为 T 的切片
// 第一种 不指定具体大小
q := []int{2, 3, 5, 7, 11, 13}
// 第二种,类型自动推断
qq := a[0:2]
// 第三种,声明为切片
var s []int = a[1:4]
// 第四种
aa := make([]int, 5)

用法

切片 s 的长度和容量可通过表达式 len(s)cap(s) 来获取。

流程控制语句:for、if、else、switch 和 defer

for

1
2
3
4
sum := 0
for i := 0; i < 10; i++ {
sum += i
}

初始化语句和后置语句是可选的

1
2
3
for ; sum < 1000; {
sum += sum
}

for 是 Go 中的 “while”

1
2
3
for sum < 10 {
sum += 1
}

无限循环

1
2
for {
}

if

1
2
3
if  sum==0 {
fmt.Println(sum)
}

同 for 一样, if 语句可以在条件表达式前执行一个简单的语句。该语句声明的变量作用域仅在 if 之内

1
2
3
if sum = 1 ; sum==1 {
fmt.Println(sum)
}

defer

defer 语句会将函数推迟到外层函数返回之后执行。推迟调用的函数其参数会立即求值,但直到外层函数返回前该函数都不会被调用。 个人猜测用途是关闭资源,处理异常等。

推迟的函数调用会被压入一个栈中。当外层函数返回时,被推迟的函数会按照后进先出的顺序调用

1
2
3
4
5
func main() {
defer fmt.Println("world")

fmt.Println("hello")
}

测试

go 默认有个轻量级测试框架,可以使用go test命令和testing

创建一个文件,文件名以 _test.go结尾,函数名为TestXXX,并且传递参数(t *testing.T)

例如:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
package morestrings

import "testing"

func TestReverseRunes(t *testing.T) {
cases := []struct {
in, want string
}{
{"Hello, world", "dlrow ,olleH"},
{"Hello, 世界", "界世 ,olleH"},
{"", ""},
}
for _, c := range cases {
got := ReverseRunes(c.in)
if got != c.want {
t.Errorf("ReverseRunes(%q) == %q, want %q", c.in, got, c.want)
}
}
}

然后执行命令go test

方法和接口

函数与方法区别

方法在 func 关键字后是接收者而不是函数名

  1. 普通函数
    1
    2
    3
    func function_name([parameter list]) [return_types] {
    函数体
    }
  2. 方法(如 struct 方法)
    1
    2
    3
    func (variable_name variable_data_type) method_name([parameter list]) [return_type]{
    /* 函数体*/
    }

使用区别

函数: function_name() 函数有参数的话,必须保持类型一致,否则编译失败。

方法:p.method_name()其中 p 可以为指针,也可以为值(p 为结构体的值)

方法的接受者可以为指针,也可以为值。例如:

1
2
3
4
5
6
7
8
9
func (v Vertex) Abs() float64 {
return math.Sqrt(v.X*v.X + v.Y*v.Y)
}

v := Vertex{3, 4}
fmt.Println(v.Abs())

p := &Vertex{4, 3}
fmt.Println(p.Abs())

使用指针接收者的原因有二:

  1. 方法能够修改其接收者指向的值。

  2. 这样可以避免在每次调用方法时复制该值。若值的类型为大型结构体时,这样做会更加高效。

接口

如果一个类型实现了一个接口需要的所有方法,那么该类型就实现了这个接口。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
type User struct {
name string
age int
}

type I interface {
Name()
Age()
}

func (u User)Name() {
fmt.Printf("Name:%v\n", u.name)
}

func (u User)Age() {
fmt.Printf("Age:%v\n", u.age)
}

func main() {
var u I = User{"truman",18}
u.Name()
u.Age()
}

类型断言

类型断言 提供了访问接口值底层具体值的方式。t := i.(T)

1
2
3
4
5
6
var a interface{} = 11
s:= a.(int)
fmt.Println(s) //11

b,ok:= a.(string)
fmt.Println(b,ok)// false

并发

使用go f(a)即可新建一个 goroutine

信道

声明一个信道c := make(chan int)

带缓存的信道 c := make(chan int,10)

仅当信道的缓冲区填满后,向其发送数据时才会阻塞。当缓冲区为空时,接受方会阻塞。

通过 channel 可以实现唤醒线程,例如:

1
2
3
4
5
6
7
8
go func() {
data := "hello value:stop"
fmt.Println(data)
time.Sleep(1000000000 * 10)
ch <- data
}()
<-ch
fmt.Println("execute next business.")

range/close

只有发送者才能关闭信道

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
ch := make(chan string)
go func() {
for i:= range ch {
if i=="hello value:5" {
time.Sleep(1000000000*10)
}
fmt.Println(i)
}
}()

for i := 0; i < 10; i++ {
data := "hello value:"+fmt.Sprintf("%d", i)
ch <- data
}
close(ch)

sync.Mutex

互斥锁

实现从 0 加到 1000

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
type Account struct {
money int
mux sync.Mutex
}

func (a *Account)Inc() {
a.mux.Lock()
a.money++
fmt.Println(a.money)
defer a.mux.Unlock()
}

func main() {
a:=Account{money: 0}
for i := 0; i <1000; i++ {
go a.Inc()
}
time.Sleep(time.Second*10)
fmt.Printf("acount money:%d\n", a.money)
}

学习资源

  1. https://tour.go-zh.org/
  2. How to Write Go Code
  3. Go 語言聖經(中文版)

参考

  1. https://golang.org/doc/code.html#Testing

开源项目KafkaCenter 版本持续集成(CI)实践

开篇

本文简单介绍开源项目KafkaCenter 版本持续集成(CI)实践方案,主要解决了三个问题:

  1. 前后端项目编译
  2. 发布Github release包
  3. 制作docker镜像

希望能给你带来一点参考。

详细信息可以参考 https://github.com/xaecbd/KafkaCenter

正文

版本管理

KafkaCenter 后端服务是java,使用maven管理的,有多个module,为了做到版本一致,我们使用了${revision}。这个是maven3.5+ 才支持,主要是为了对CI友好。

例如:

父pom

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
<?xml version="1.0" encoding="UTF-8"?>
<project >
<modelVersion>4.0.0</modelVersion>
<groupId>org.nesc.ec.bigdata</groupId>
<artifactId>KafkaCenter</artifactId>
<version>${revision}</version>
<packaging>pom</packaging>
<name>KafkaCenter</name>
<url>https://github.com/xaecbd/KafkaCenter</url>
<description>Kafka Center Platform</description>
...
<properties>
<revision>1.0.0-SNAPSHOT</revision>
</properties>

<modules>
<module>KafkaCenter-Base</module>
<module>KafkaCenter-Core</module>
</modules>
</project>

子module

1
2
3
4
5
6
7
8
9
10
11
12
13
14
<?xml version="1.0"?>
<project>
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.nesc.ec.bigdata</groupId>
<artifactId>KafkaCenter</artifactId>
<version>${revision}</version>
</parent>
<artifactId>KafkaCenter-Base</artifactId>
<name>KafkaCenter-Base</name>
<url>https://github.com/xaecbd/KafkaCenter</url>

</project>

通过如下命令,可以在打包的时候指定版本号:

1
mvn -Drevision=2.1.0 -Dchangelist= clean package

Docker镜像

在项目根目录下新建docker文件夹,包含三个文件:

docker-build-release.sh build镜像及发布镜像的脚本

1
2
3
4
5
6
7
8
9
10
#!/usr/bin/env bash

DOCKER_IMAGE_NAME="xaecbd/kafka-center"
VERSION=${TRAVIS_TAG#v}
echo "KafkaCenter version: $VERSION"
echo "$DOCKER_PASSWORD" | docker login -u "$DOCKER_USERNAME" --password-stdin
cp $TRAVIS_BUILD_DIR/KafkaCenter-Core/target/*.jar $TRAVIS_BUILD_DIR/docker/
docker build -t $DOCKER_IMAGE_NAME:$VERSION $TRAVIS_BUILD_DIR/docker/
docker images
docker push $DOCKER_IMAGE_NAME:$VERSION

Dockerfile docker 镜像定义文件

1
2
3
4
5
6
7
8
9
10
FROM adoptopenjdk/openjdk8:jre8u252-b09-alpine
LABEL author="Turman.P.Du"
ENV PROJECT_BASE_DIR /opt/app/kafka-center/
WORKDIR ${PROJECT_BASE_DIR}

COPY *.jar ${PROJECT_BASE_DIR}/
COPY *.sh ${PROJECT_BASE_DIR}/

RUN chmod +x *.sh
ENTRYPOINT ["sh","start.sh"]

start.sh 应用启动脚本,非必须,只是我们习惯放这么个脚本,可以在应用启动前做一些工作。推荐在启动java应用前增加exec命令,这样可以让spring容器在docker容器停止运行前执行一些操作(可以用作应用停止前执行收尾工作,例如保存数据,停止不可中断的任务)。

1
2
3
4
5
6
7
8
9
10
11
12
13
#!/bin/sh
echo "PROJECT_BASE_DIR :"$PROJECT_BASE_DIR
#cd $APP_ROOT_DIR
cd $PROJECT_BASE_DIR

appName=`ls|grep .jar$`
echo start to run $appName

if [ -n "$JAVA_OPTIONS" ];then
exec java $JAVA_OPTIONS -jar $appName $@
else
exec java -jar $appName $@
fi

travis

github action已经很好了,我没有采用的原因是需要熟悉成本有些操作可能暂时做不到。而可以预见性的travis都能很好的做到。因此目前的方案是采用travis。

实现要求

通过新建tag(只能是tag触发),自动编译前后端代码,发布github release,构建docker镜像,发布镜像。

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
language: java
jdk:
- openjdk8

cache:
directories:
- $HOME/.m2
- $HOME/.npm
- $TRAVIS_BUILD_DIR/

dist: trusty
jobs:
include:
- stage: ui_build
language: node_js
node_js: 10.15.2
script: cd KafkaCenter-Frontend && npm install && npm run build

- stage: GitHubRelease
install:
- echo GitHubRelease
script: mvn -Drevision=${TRAVIS_TAG#v} clean package -Dmaven.test.skip=true
before_deploy:
- mkdir -p $TRAVIS_BUILD_DIR/deploy
- cp $TRAVIS_BUILD_DIR/KafkaCenter-Core/target/*.gz $TRAVIS_BUILD_DIR/deploy
- rm -f $TRAVIS_BUILD_DIR/KafkaCenter-Core/target/*.gz
- ls -l $TRAVIS_BUILD_DIR/deploy
deploy:
provider: releases
api_key: $API_KEY
file_glob: true
skip_cleanup: true
file: deploy/*.tar.gz
on:
tags: true
after_deploy: rm -rf $TRAVIS_BUILD_DIR/deploy

- stage: BuildDockerImageforRelease
install:
- echo Build Docker Image for Release
before_script:
- chmod +x ./docker/docker-build-release.sh
script: ./docker/docker-build-release.sh

stages:
- name: ui_build
if: tag =~ /^v\d+\.\d+\.\d+.*$/
- name: GitHubRelease
if: tag =~ /^v\d+\.\d+\.\d+.*$/
- name: BuildDockerImageforRelease
if: tag =~ /^v\d+\.\d+\.\d+.*$/

notifications:
email: true

在travis管理页面需要配置API_KEYDOCKER_USERNAMEDOCKER_PASSWORD

参考

  1. Maven CI Friendly Versions
  2. spring-boot-docker
  3. docs.travis-ci.com

基于spring security oauth2 client最佳实践

开篇语

最近很少写文章,一个是确实是很忙,另外一个原因是没有什么深度的技术文章可写。之前写blog的原因是为了技术存档,便于自己某天需要的时候再去看看,另外是总结一下。这段时间不太想写种水文,这篇文章同样不是什么深度性的文章,不过确实困扰了我超过3天时间,网络上很多文章都没能解决我的问题,基本上大家是介绍整个oauth,体系很大,文章却写的不全,要么就是方案很复杂(有点追求,不想采用),对我几乎无帮助。

按说官网文档应该够全了,但是对于一个不熟悉spring security的人,想要快速入手,还是很难,文档我就看了很久也没有找到自己想要的。官方的demo局限于github,google。我想实现的是自定义的oauth2登录。

当我解决了以后,发现别的小伙伴也有类似的疑惑。索性就写下来,只是技巧,写最少的代码,最优雅的完成自己想要的功能。本篇文章不讲解oauth认证基本知识。

实践

1.引入相应的依赖包

1
2
3
4
5
6
7
8
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-oauth2-client</artifactId>
</dependency>

2.参数配置

1
2
3
4
5
6
7
8
9
10
11
12
13
14
spring.security.oauth2.client.provider.customer.authorization-uri=http://xxxxxxxx/oauth2/v1/authorize
spring.security.oauth2.client.provider.customer.token-uri=http://xxxxxxxx/oauth2/v1/token
spring.security.oauth2.client.provider.customer.user-info-uri=http://xxxxxxxx/oauth2/v1/user-info
spring.security.oauth2.client.provider.customer.user-info-authentication-method=header
spring.security.oauth2.client.provider.customer.user-name-attribute=name

spring.security.oauth2.client.registration.app.client-id=xxxxxxxxxxx
spring.security.oauth2.client.registration.app.client-secret=xxxxxxxxxxx
spring.security.oauth2.client.registration.app.client-name=Client for user scope
spring.security.oauth2.client.registration.app.provider=customer
spring.security.oauth2.client.registration.app.scope=user
spring.security.oauth2.client.registration.app.redirect-uri={baseUrl}/login/oauth2/code/{registrationId}
spring.security.oauth2.client.registration.app.client-authentication-method=basic
spring.security.oauth2.client.registration.app.authorization-grant-type=authorization_code

3.代码实现

在页面登录按钮,添加跳转地址/oauth2/authorization/app 这个是默认的地址,可以通过配置文件修改

新建Oauth2LoginSecurityConfig 实现如下功能既可

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
@Configuration
public class Oauth2LoginSecurityConfig extends WebSecurityConfigurerAdapter {

@Override
protected void configure(HttpSecurity http) throws Exception {
http.csrf().disable()
.authorizeRequests(a -> a
.antMatchers("/**").permitAll().anyRequest().authenticated()
)
.exceptionHandling(e -> e
.authenticationEntryPoint(new HttpStatusEntryPoint(HttpStatus.UNAUTHORIZED))
)
.formLogin()
.loginPage("/#/user/login")
.permitAll()
.and()
.logout().permitAll()
.and()
.oauth2Login().userInfoEndpoint().and().successHandler(new AuthenticationSuccessHandler() {
@Override
public void onAuthenticationSuccess(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, Authentication authentication) throws IOException, ServletException {
OAuth2User oAuth2User = (OAuth2User) authentication.getPrincipal();
Map<String, Object> attributes = oAuth2User.getAttributes();
// ....登录成功以后,既可获取用户信息

// ..跳转到成功页面
httpServletResponse.sendRedirect("/#/home");
}
});
}

}

上面核心代码为.oauth2Login().userInfoEndpoint().and().successHandler() 这个完成获取code,根据code获取token,根据token获取user信息。熟悉oauth2 code的认证流程,应该就能明白。希望能给你带来一点点帮助。

后话

上面没有什么说的,不过最近越来越发现在职场上需要一种能力,快速学习的能力,对未知事物有非方向上认知错误。也就是说当我们对一个技术架构,或者框架不熟的情况下,一些基本技术常识往往就发挥很大的作用。在开发的路上少追求技巧性的东西,多追究一些理论和本质。这样对快速学习一个技术会有很大的帮助。

前言

Spring Boot应用监控有很多方案,例如elastic APM,Prometheus等。各有特色,本次实践采用方案:Micrometer+Prometheus+Grafana

选择Micrometer最重要的原因是他的设计很灵活,并且和spring boot 2.x集成度很高。对于jvm的监控很容易集成,难度很小。本次实践包含jvm监控和业务性能指标监控。

环境准备

  1. 搭建promethues

    1
    2
    3
    4
    5
    docker run \
    -p 9090:9090 \
    --name prometheus
    -v /tmp/prometheus.yml:/etc/prometheus/prometheus.yml \
    prom/prometheus
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    global:
    scrape_interval: 15s # By default, scrape targets every 15 seconds.
    evaluation_interval: 15s # By default, scrape targets every 15 seconds.
    # scrape_timeout is set to the global default (10s).
    # Load rules once and periodically evaluate them according to the global 'evaluation_interval'.
    rule_files:
    # - "first.rules"
    # - "second.rules"

    # A scrape configuration containing exactly one endpoint to scrape:
    # Here it's Prometheus itself.
    scrape_configs:
    # The job name is added as a label `job=<job_name>` to any timeseries scraped from this config.
    - job_name: 'demo_platform'

    # Override the global default and scrape targets from this job every 5 seconds.
    scrape_interval: 5s

    metrics_path: '/actuator/prometheus'
    # scheme defaults to 'http'.

    static_configs:
    - targets: ['127.0.0.1:8080']
  2. 搭建grafana

    1
    docker run -d -p 3000:3000 --name grafana grafana/grafana:6.5.0

Micrometer简介

Micrometer(译:千分尺) Micrometer provides a simple facade over the instrumentation clients for the most popular monitoring systems. 翻译过来大概就它提供一个门面,类似SLF4j。支持将数据写入到很多监控系统,不过我谷歌下来,很多都是后端接入的是Prometheus.

Micrometer提供了与供应商无关的接口,包括 timers(计时器)gauges(量规)counters(计数器)distribution summaries(分布式摘要)long task timers(长任务定时器)。它具有维度数据模型,当与维度监视系统结合使用时,可以高效地访问特定的命名度量,并能够跨维度深入研究。

支持的监控系统:AppOptics , Azure Monitor , Netflix Atlas , CloudWatch , Datadog , Dynatrace , Elastic , Ganglia , Graphite , Humio , Influx/Telegraf , JMX , KairosDB , New Relic , Prometheus , SignalFx , Google Stackdriver , StatsD , Wavefront

Micrometer提供的度量类库

Meter是指一组用于收集应用中的度量数据的接口,Meter单词可以翻译为”米”或者”千分尺”,但是显然听起来都不是很合理,因此下文直接叫Meter,理解它为度量接口即可。Meter是由MeterRegistry创建和保存的,可以理解MeterRegistryMeter的工厂和缓存中心,一般而言每个JVM应用在使用Micrometer的时候必须创建一个MeterRegistry的具体实现。Micrometer中,Meter的具体类型包括:TimerCounterGaugeDistributionSummaryLongTaskTimerFunctionCounterFunctionTimerTimeGauge。一个Meter具体类型需要通过名字和Tag(这里指的是Micrometer提供的Tag接口)作为它的唯一标识,这样做的好处是可以使用名字进行标记,通过不同的Tag去区分多种维度进行数据统计。

Spring Boot集成

与spring boot 集成,这里的metric主要是由spring actuator 提供

安装

1
2
3
4
5
6
7
8
9
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>

<dependency>
<groupId>io.micrometer</groupId>
<artifactId>micrometer-registry-prometheus</artifactId>
</dependency>

配置

1
2
3
4
5
6
7
8
9
10
11
12
13
management:
endpoint:
health:
enabled: false
endpoints:
web:
exposure:
include: '*'
exclude: env,beans
metrics:
enable:
http: false
hikaricp: false

这里有几个注意的点management.endpoint.health.enabled只是为了禁用spring 默认的健康检查,非必须。exclude: env,beans也不需要配置,只是在我项目中为了减少导出的metric。同理management.metrics.enable也是为了减少收集的数据,使用方法为你定义指标的前缀。

只有management.endpoints.web.exposure.include为必须的,这里也只是为了导出/actuator/prometheus,通过该地址可以访问到响应的metric信息。

可视化

访问 http://localhost:8080/actuator/prometheus 即可看到响应的metric信息。

在grafana中中导入JVM (Micrometer)

即可看到如下效果:

自定义业务性能监控

因为系统遗留监控代码的原因,这里采用的是全局静态方法实现。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
protected static Iterable<Tag> tags(String service, String category, String method) {
return Tags.of("service", service, "category", category, "method", method);
}

protected static Iterable<Tag> tags(String service, String category) {
return Tags.of("service", service, "category", category);
}

public static void controllerMetric(String service, MonitorMetric.MonitorOperationType type, String method, long time) {
try {
Metrics.counter(Constants.HTTP_REQUESTS_TOTAL, tags(service, type.name(), method)).increment();
Metrics.timer(Constants.REQUESTS_LATENCY, tags(service, type.name())).record(Duration.ofMillis(time));
} catch (Exception e) {
e.printStackTrace();
}
}

解释一下,这里可以统计出请求数和请求延迟。

对于每秒请求数据量,可以使用increase(http_requests_total{job=~"$job",instance=~"$instance"}[1m])

对于平均请求延迟,可以使用rate(timer_sum[1m])/rate(timer_count[1m])

对于Throughput 可以使用rate(timer_count[1m])

使用中的困惑

问题

Percentile histogramsDistribution summaries性能损失还无法确定,不过查看PrometheusTimer,结合测试,还是有一定的性能损失,不过这里未深入研究。

全局使用一些开发建议

可以在定义静态方法类,初始化的时候做一点配置,registry可以使用spring 注入进来例如:

1
2
@Autowired 
MeterRegistry registry;
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
public MonitorMetric(MeterRegistry registry) {
registry.config().meterFilter(
new MeterFilter() {
@Override
public DistributionStatisticConfig configure(Meter.Id id, DistributionStatisticConfig config) {
if (id.getName().startsWith("requests_latency")) {
return DistributionStatisticConfig.builder()
.percentiles(0.5, 0.75, 0.9)
.sla(1)
.expiry(Duration.ofMinutes(1))
.minimumExpectedValue(1L)
.build()
.merge(config);
}
return config;
}
});
Metrics.addRegistry(registry);
}

参考

  1. 使用 Micrometer 记录 Java 应用性能指标
  2. Micrometer 快速入门
  3. JVM应用度量框架Micrometer实战
  4. Micrometer Prometheus
0%