2024年3月

notion database 必知必会

用过 mysql 的同学一定很容易上手 notion 。在 notion 中,掌握好 database,基本上就掌握了 notion 最核心的概念。

notion 的 database 对应的就是数据库中的 table。在 notion 的 database 中每插入一条数据,实际上就是插入一个 page。

只是这个 page 能承载的东西非常多,包含文本,媒体,日期,甚至是另外一个 database。

使用一个
/database inline
就能在一个页面内创建一个 database,而理解 database 的概念,就要理解它的每个设置项,每个设置项又有他们对应的属性。

这样设置项和属性的对应联系如果都理解了,那么 database 就掌握差不多了。

设置项

layout

这个准确说是 database 的展示属性,它表示我们的 database 应该要以什么形式在页面中表现出来。

  • Table
  • Board
  • TimeLine
  • Calendar
  • List
  • Gallery

各种展示形式如字面意义描述,很清晰。

property

前面说过,database 中每条数据是一个 page,而数据中的每个字段就是 page 中的 property。property 的类型非常丰富,并且目前还在不断扩展中。

  • Text
  • Number
  • Select
  • Multi-Select
  • Status
  • Date
  • Person
  • Files & Media
  • Checkbox
  • URL
  • Email
  • Phone
  • Formula
  • Relation
  • Rollup
  • Created time
  • Created by
  • Last edit time
  • Last edit by
  • Button
  • ID

其中大多数 property 都很好理解,这里描述几个不好理解的:

Formula

公式,这是非常有用的一个功能,我能用某种特定的语法来构造一段文本。

比如有个场景,notion 中创建一个 task 的database,有个property 名字是“需要番茄次数”,它是 int 类型。

并且我希望在外面的 table 栏中,有一列能显示 如下字样:“需要x 番茄” 或者空字符串。

这个显示文本列的逻辑是:如果“需要番茄次数”为 0,那么这一列内容为空字符串,如果不为空,根据“需要番茄次数”列拼接显示字符串。

这种需求就是 formula 类型的范畴了。我们创建一个列,它的 property 设置为 formula,并且为它编写公式:

prop("需要番茄次数") >= 0? "需要" + prop("需要番茄次数") + "番茄":""

那么它就能有如下的表现

image-20240312095407296

relation & rollup

当一个database 和另外一个 database 有关联的时候,我们就用到这两个字段。

什么叫有关联呢?就是 mysql 表中的 1 对多,多对多。

比如一个书籍表 和一个作者表,作者表的字段包含姓名,年龄,地址。而书籍表的字段包含书本的名字,作者。

我们希望作者表和书籍表的作者属性进行关联,即一个作者拥有多本书籍,那么要怎么做呢?

我们可以在作者表增加一个 Relation 类型 property ,并它关联到书籍表 的条目。

image-20240312101048732

这时候,我们就创建了一个作者表和书籍表的一对多关系。

那么 rollup 又是什么作用呢?

如果每个书籍还有价格,那么我们不仅想在书籍表中看到价格,我们还想在作者表中看到这个作者的所有书的价格,我们就要用到 rollup 了。

我们在作者表创建一个 rollup 属性字段“书本总收入”,它使用 Sum 的计算方式,计算书籍表中的价格 property

image-20240312101231241

最终表现如下:

image-20240312101325480

relation 是描述两个表关系,rollup 是通过 relation 来聚合某个字段

button

button 如名所示,就是增加一个按钮。我们可以为这个按钮定义好行为。

这个按钮点击的时候,我们可以触发预先定义好的 action,比如在任务表中,我们可以定义 action 为将状态修改为“已完成”,或者是将打卡时间设置为当前时间,那么点击按钮就会执行对应的行为了。

image-20240312102102138

filter & sort

这两个逻辑不难理解,按照某个字段过滤表格(filter),按照某个字段排列表格(sort)。和数据表查询中的 where 以及 order by 是一模一样的。

唯一特别的是,他们两个用的频度非常高,所以在直接在表格外层也可以直接设置。

image-20240313073108572

我们可以很容易用 filter 和 sort 来创建出 “未完成任务视图”,“本周任务视图”,“优先级排序视图”等。

group

分组,顾名思义,按照某个字段进行分组并且展示出来。

sub-item

如果你数据库中的条目是有层级的,那么使用这个sub-item是非常好用的。我们可以为一个条目创建一个子条目,那么在实际数据表中,就会用 parent item,sub-item 将这两个条目关联起来。

在 notion 的 database 中,这个层级是无限的,你可以为某个条目创建一个子条目,再为子条目创建它的子条目。

image-20240313073347613

dependency

image-20240313074141284

我们的数据表如果表示的是任务,那么就存在某些任务必须要完成才能开始下一个任务。这里就可能会有任务 blocking 其他任务,也代表有任务会 blocked by 某个任务。描述这个关系就是 denpendency 的作用。

我们的表如果开启了 dependency,那么表中自动会加两列:blocking 和 blocked by。

image-20240313091541051

想要更清晰的看到他们的关系,我们可以用 timeline 时间线的形式来展示。

image-20240313091625322

automation

这是一个自动化的功能,表示如果表格有什么变动,那么就执行什么 action。

比如表格增加了一条数据,发送一个消息到 slack 中。

image-20240313091826211

但是由于更多的 action 是需要付费的,我目前还不是付费用户,所以没有研究更多了。

load limit

当database 是 list 形式的时候,单页面显示条数,如果有更多,可以使用下拉的 Load more 来展示更多。

总结

这些设置项最核心的还是要设置好 property,这个 property 非常灵活,里面的 formula 更是提供了无限的想象空间。能配合设计出很多奇特的表格。

入 notion 坑,不亏。

在面试过程中,死锁是必问的知识点,当然死锁也是我们日常开发中也会遇到的一个问题,同时一些业务场景例如库存扣减,银行转账等都需要去考虑如何避免死锁,一旦线上发生了死锁,那可能年终不保。。。。。下面我们就来聊一聊死锁如何定位,以及如何避免。

什么是死锁

死锁(Deadlock)是指在操作系统里,两个或多个并发线程在执行过程中,因争夺资源而造成的一种互相等待的现象,且无外力干预的情况下,这些线程都无法进一步执行下去。每个线程至少持有一个资源并等待其他线程所持有的资源才能继续执行,从而形成了一个循环等待链,导致所有线程都被阻塞,无法顺利完成。

假设有两个仓库A和B,它们之间在进行商品调拨。线程T1负责将商品从仓库A调拨到仓库B,而线程T2负责将商品从仓库B调拨到仓库A。每个线程在执行调拨操作时,需要先获取调出仓库和调入仓库的锁,以保证调拨操作的原子性。现在,假设线程T1已经获取了仓库A的锁并且正在等待获取仓库B的锁,而线程T2已经获取了仓库B的锁并且正在等待获取仓库A的锁。这时,线程T1持有仓库A的锁并且等待仓库B的锁,线程T2持有仓库B的锁并且等待仓库A的锁。由于彼此都在等待对方持有的锁,因此两个线程都无法继续执行,导致了死锁的发生。

死锁产生的条件

死锁的产生必须满足以下四个条件。当这四个条件同时满足时,就可能发生死锁。

互斥条件

资源不能同时被多个线程占用。如果一个资源被一个线程占用,其他线程必须等待释放。也就是所谓的互斥锁。

互斥条件.png

如上图线程T1已经持有了资源,那么该资源就不能再同时被线程T2持有,如果线程T2想要获取资源,就要一直等待(即线程T2阻塞),一直到线程T1释放资源。

占有并且等待条件

当前线程已经占有至少一个资源,此时还想请求其他线程占有的其他资源时就会造成等待,在这个等待过程中对已获得的资源也不会释放。

占有并且等待条件.png

如上图当线程T1已经持有了资源1,又想申请获取资源2,而资源2已经被线程T3持有了,所以线程T1就会处于等待状态,但是
线程T1在等待资源2的同时并不会释放自己已经持有的资源1

不可抢占条件

当前已经被持有的资源只能由持有它的线程释放,其他线程不可以强行占有该资源。

不可抢占条件.png

如上图线程T1已经持有了资源 ,
在自己使用完之前不能被其他线程获取
,线程T2如果也想使用此资源,则只能在线程T1使用完并释放后才能获取。

循环等待条件

在发生死锁时,必然存在一个线程-资源的环形链,链中的每个线程正等待下一个线程所占用资源的释放。

image.png

如上图线程T1等待线程T2占有的资源,而线程T2等待线程T1占有的资源,两个线程互相等待,这样就形成了循环等待。

模拟死锁

以文章解释死锁概念的例子为例,我们使用代码模拟死锁。

我们先模拟调拨商品操作库存的代码:

public class SkuStock {  
  
    private String sku;  
  
    private String warehouse;  
  
    private Integer qty;  
  
    public SkuStock(String sku, String warehouse, Integer qty) {  
        this.sku = sku;  
        this.warehouse = warehouse;  
        this.qty = qty;  
    }  
  
	/**
	* 调拨库存,操作库存
	*/  
    public void transferTo(SkuStock targetSku, int quantity) {  
        synchronized (this){  
            System.out.println(Thread.currentThread().getName() + "开始操作库存");  
  
            try {  
                Thread.sleep(2000);  
            }catch (InterruptedException e){  
                e.printStackTrace();  
            }  
  
            synchronized (targetSku){  
                // 扣减调出仓库的库存  
                this.qty -= quantity;  
                // 增加目标仓库的库存  
                targetSku.qty += quantity;  
                System.out.println(Thread.currentThread().getName() + "操作库存结束");  
            }  
        }  
    }  
}

然后我们在模拟线程T1进行仓库A向仓库B调拨商品,线程t2进行仓库B向仓库A调拨商品。

public static void main(String[] args) {  
    SkuStock skuStockA = new SkuStock("SKU", "WA", 100);  
    SkuStock skuStockB = new SkuStock("SKU", "WB", 100);  
  
    Thread thread1 = new Thread(() -> {  
        skuStockA.transferTo(skuStockB, 50);  
    }, "T1");  
  
    Thread thread2 = new Thread(() -> {  
        skuStockB.transferTo(skuStockA, 60);  
    }, "T2");  
  
    thread1.start();  
    thread2.start();  
}

此时我们运行代码,就会发现代码只打印了开始操作库存,没有结束操作的日志,此时就会发生了死锁。

image.png

死锁排查

当我们的程序发生死锁时,我们需要排查,找出问题所在,关于死锁的排查工具,我们可以使用JDK自带的
jstack
工具,也可以使用一些可视化工具例如:
VisualVM

JConsole
等。

jstack工具

jstack
是JDK自带的一款强大的故障诊断工具,主要用于获取Java应用程序的线程堆栈信息,这对于分析Java程序的运行状态、排查性能瓶颈、定位死锁、冻结线程以及其他多线程相关的问题具有非常重要的作用。
对于以上死锁程序,我们先使用
jps
工具列出当前系统中所有的Java进程的进程ID(PID)。

image.png

然后针对目标Java进程,使用
jstack
命令生成线程堆栈快照,它将输出Java进程中所有线程的详细堆栈信息。

jstack 24749

然后我们可以看到输出的日志中,指明了应用程序发生死锁的原因。

image.png

可以看到对于线程T1等待着线程T2锁住的
0x000000070fd53c38
这个资源,同时锁住了
0x000000070fd53bc0
这个资源,而对于线程T2,它等待着线程T1锁住的
0x000000070fd53bc0
这个资源,同时锁住了
0x000000070fd53c38
这个资源,这样就发生了死锁。

jstack
输出中会包含有关线程等待锁的信息。如果存在死锁,你会看到线程在等待一个它自己或其他线程已经持有的锁,形成一个等待链条。死锁信息通常会明确指出哪些线程参与了死锁。

VisualVM

VisualVM
是一款强大的Java性能分析和故障排除工具,它是Oracle开发并随JDK一起提供的一个综合性桌面应用程序。
VisualVM
整合了多个独立的JDK命令行工具的功能,如
jstat

jmap

jstack

jinfo
等,并且提供了丰富的图形用户界面,使开发者能够更容易地监控和分析Java应用程序的性能、内存消耗、线程行为、垃圾收集等各方面信息。

image.png

他会提示你发生了死锁了,进入
Thread Dump
中查看具体的信息。

image.png

效果等同于使用
jstack
命令输出的日志信息。

如何避免死锁问题的发生

前面我们提到,产生死锁的四个必要条件是:互斥条件、占有并等待条件、不可抢占条件、循环等待条件。那么避免死锁问题就只需要破环其中一个条件就可以。

破坏互斥条件

为避免死锁的发生,我们应该避免使用互斥锁,我们可以将其中的操作改为原子操作。
比如上述例子中,我们将发生死锁的库存操作的代码:

synchronized (targetSku){  
    // 扣减调出仓库的库存  
    this.qty -= quantity;  
    // 增加目标仓库的库存  
    targetSku.qty += quantity;  
    System.out.println(Thread.currentThread().getName() + "操作库存结束");  
}

这里我们不再使用
synchronized
关键字,而是通过
AtomicInteger

compareAndSet
方法(CAS操作)来实现并发下的库存扣减操作。这样做的好处是可以避免死锁,每次操作都是原子性的,不会出现持有锁的线程等待另一个线程释放锁的情况。

private AtomicInteger qtyAtomic = new AtomicInteger();  
public void transferTo1(SkuStock targetSku, int quantity) {  
    synchronized (this){  
        System.out.println(Thread.currentThread().getName() + "开始操作库存");  
  
        try {  
            Thread.sleep(2000);  
        }catch (InterruptedException e){  
            e.printStackTrace();  
        }  
        // 扣减调出仓库的库存  
        this.qtyAtomic.addAndGet(-quantity);  
        // 增加目标仓库的库存  
        targetSku.qtyAtomic.addAndGet(quantity);  
        System.out.println(Thread.currentThread().getName() + "操作库存结束");  
    }  
}

使用
transferTo1
方法重新执行程序,正常实现库存操作。

image.png

破坏占有且等待条件

对于占有且等待条件,线程持有资源我们是无法破坏的,既然无法破坏占有,那我们就破坏等待,我们不等待资源了。破坏占有且等待条件,可以采取的方法之一就是一次性获取所有需要的资源,而不是持有部分资源后再等待其他资源。在Java中,确实没有一种直接的方式允许一个线程一次性获取多个资源。但是,你可以使用一种类似资源管理器的方式来模拟一次性获取多个资源的情况。例如,你可以创建一个资源管理器对象,该对象负责管理所有需要的资源,并在需要时为线程提供这些资源。其他线程可以向资源管理器请求资源,如果资源可用,则立即返回,如果资源不可用,则进入等待状态。

针对上述示例,我们定义一个库存资源管理器:

public class SkuAllocator{  
  
    private static SkuAllocator skuAllocator = new SkuAllocator();  
  
    private SkuAllocator(){}  
  
    public static SkuAllocator getSkuAllocator(){  
        return skuAllocator;  
    }  
  
    private List<Object> list = Lists.newArrayList();  
  
    /**  
     *、一次性获取多个资源  
     * @param objs 资源  
     * @return 是否申请资源成功  
     */  
    synchronized boolean apply(Object...objs){  
        List<Object> containsList = Stream.of(objs)  
                .filter(e -> list.contains(e)).collect(Collectors.toList());  
        if (!containsList.isEmpty()){  
            return false;  
        }  
        list.addAll(Lists.newArrayList(objs));  
        return true;  
    }  
  
    /**  
     * 释放资源  
     * @param objs 资源  
     */  
    synchronized void free(Object...objs){  
        Stream.of(objs).forEach(e -> list.remove(e));  
    }  
}

在这个资源管理器中,我们提供了两个方法
apply
以及
free
,其中
apply
用于将所有的资源放获取到,而
free
用于释放所有的资源。

然后我们改造操作库存时,线程执行操作库存,需要调用
apply
将所有的资源都拿到,然后执行后面的库存扣减,而其他线程在执行
apply
时,因为已经有现成获取到了资源,即资源管理器中
list
已存在资源,所以会返回
false
,这样其他的线程会一直等待下去,知道当前线程释放资源。

private SkuAllocator skuAllocator = SkuAllocator.getSkuAllocator();  
public void transferTo2(SkuStock targetSku, int quantity) {  
    // 一次性申请库存增加以及扣减资源,如果线程可以拿到资源,即管理器中存在资源,  
    // while条件不成立就往下继续执行扣减库存,如果没有拿到资源,则while中是true,则while就一直自循环  
    while (!skuAllocator.apply(this, targetSku)){;}  
  
    try {  
        synchronized (this){  
            System.out.println(Thread.currentThread().getName() + "开始操作库存");  
  
            try {  
                Thread.sleep(2000);  
            }catch (InterruptedException e){  
                e.printStackTrace();  
            }  
            synchronized (targetSku){  
                // 扣减调出仓库的库存  
                this.qty -= quantity;  
                // 增加目标仓库的库存  
                targetSku.qty += quantity;  
                System.out.println(Thread.currentThread().getName() + "操作库存结束");  
            }  
        }  
    }finally {  
        // 用完,则释放资源,让其他线程使用  
        skuAllocator.free(this, targetSku);  
        System.out.println(Thread.currentThread().getName() + "释放资源...");  
    }  
}

调用该方法,也会让库存扣减成功。

image.png

破坏不可抢占条件

对于不可抢占条件,我们无法抢占或者释放其他线程持有的资源,但是我们可以给线程设置资源持有的超时时间,如果超过这个时间还没有释放资源,则自动释放资源。这样其他的线程就有就会获取资源了。

private final Lock lock = new ReentrantLock();  
public void transferTo3(SkuStock targetSku, int quantity) throws InterruptedException {  
    while (true){  
        if (lock.tryLock(2, TimeUnit.SECONDS)) {  
            try {  
                System.out.println(String.format("当前线程 %s 获得对象锁 %s", Thread.currentThread().getName(), lock));  
                if (targetSku.lock.tryLock()) {  
                    try {  
                        System.out.println(String.format("当前线程 %s 获得对象锁 %s", Thread.currentThread().getName(), targetSku.lock));  
                        // 扣减调出仓库的库存  
                        this.qty -= quantity;  
                        // 增加目标仓库的库存  
                        targetSku.qty += quantity;  
                        System.out.println(Thread.currentThread().getName() + " 操作库存结束");  
                        break;  
                    } finally {  
                        targetSku.lock.unlock();  
                    }  
                }  
            } finally {  
                lock.unlock();  
            }  
        }   
    }  
}

执行结果如下:
image.png

破坏循环等待条件

对于循环等待条件,他因为交叉获取资源,导致形成了一个环形等待。破坏这个条件,我们可以采取顺序获取资源。确保所有的线程都按照相同的顺序获取资源。这样如果线程T1获取资源1,同时线程T2也来获取资源1时,会等待,知道线程T1释放之后再去获取资源1,同样然后获取资源2。

针对上述示例,我们对库存增加id或者库存操作创建时间,这样我们使用这个ID,对库存资源进行排序,然后按照这个顺序去占用资源。

public void transferTo4(SkuStock targetSku, int quantity) throws InterruptedException {  
    SkuStock firstSku = this.id < targetSku.id ? this : targetSku;  
    SkuStock secondSku = this != firstSku ? this : targetSku;  
  
    synchronized (firstSku){  
        System.out.println(Thread.currentThread().getName() + "开始操作库存");  
        try {  
            Thread.sleep(2000);  
        }catch (InterruptedException e){  
            e.printStackTrace();  
        }  
  
        synchronized (secondSku){  
            // 扣减调出仓库的库存  
            this.qty -= quantity;  
            // 增加目标仓库的库存  
            targetSku.qty += quantity;  
            System.out.println(Thread.currentThread().getName() + " 操作库存结束");  
        }  
    }  
}

执行结果如下:

image.png

在上述4种破坏死锁条件中,我们可以观察到,在为避免死锁时,除了第一种方案——使用原子操作代替互斥锁外,其余三种方案都会导致并发操作变为串行执行,在一定程度上会牺牲性能。因此,在某些情况下,我们不应过分追求破坏死锁的四个必要条件,因为即使这些条件被满足,死锁仍然有一定的几率发生。我们应该关注的是如何有效地避免死锁的发生,而不是完全消除死锁的可能性。因此,设计时应该考虑采取合适的措施来降低死锁的概率,并在发生死锁时能够及时恢复系统的正常运行状态。

结论

死锁问题的产生是由两个或者以上线程并行执行的时候,争夺资源而互相等待造成的。他必须同时满足互斥条件,占用且等待条件,不可抢占条件,循环等待条件这四个条件,才可能发生。在日常系统开发中,我们要避免死锁。避免死锁的方式通常有:

  1. 按顺序获取资源:
    给资源编号,所有线程按照编号递增的顺序请求资源,释放资源时按照相反的顺序释放。这样可以避免循环等待条件的发生。

  2. 加锁顺序统一:
    确定所有线程加锁的顺序,要求所有线程都按照相同的顺序获取锁,这样可以避免占有且等待条件的发生。

  3. 超时放弃:
    当尝试获取资源失败时,设置超时时间,超过一定时间后放弃获取资源,并释放已占有的资源,以避免持续等待而导致的死锁。

  4. 死锁检测和恢复:
    定期检测系统中的死锁情况,一旦检测到死锁,采取相应的措施进行恢复,例如中断某些线程、回滚事务等。

  5. 资源分配策略:
    使用资源分配策略,确保资源的合理分配和使用,避免资源过度竞争和浪费,从而降低死锁的发生概率。

  6. 避免嵌套锁:
    尽量避免在持有一个锁的情况下去请求另一个锁,以减少死锁的可能性。

  7. 使用并发库和工具:
    Java中可以使用
    java.util.concurrent
    包中的高级同步工具,如
    Semaphore

    ReentrantLock
    (支持尝试获取锁及超时机制)、
    StampedLock
    (支持乐观读写)等,它们提供了比
    synchronized
    关键字更灵活的控制方式,有助于预防死锁。

本文已收录于我的个人博客:
码农Academy的博客,专注分享Java技术干货,包括Java基础、Spring Boot、Spring Cloud、Mysql、Redis、Elasticsearch、中间件、架构设计、面试题、程序员攻略等

本文介绍基于
ENVI
软件,对
不含有任何地理参考信息
的栅格遥感影像添加
地理坐标系

投影坐标系

地理参考信息
的方法。

我们先来看一下本文需要实现的需求。现有以下两景遥感影像,其位于不同的空间位置;但由于二者均不含任何地理参考信息,导致其在
ENVI
软件中打开后会自动重叠在一起;如下图所示。

image

那么我们就以其中一景遥感影像为例,对其添加地理参考信息。

明确了具体需求,接下来就可以开始操作。首先,我们在
ENVI
软件中打开对应的两景遥感影像;其次,在需要添加地理参考信息的图像名称处右键,选择“
View Metadata
”。

弹出如下所示的元数据浏览窗口。

这里我们需要注意:如果大家打开的元数据浏览窗口的左侧列表中含有“
Map Info
”这个选项,那么我们直接单击,将其打开,并选择“
Edit Metadata
”进行本文后续的操作即可;而如果是像本文中一样,遥感影像元数据窗口没有“
Map Info
”这个选项,那么我们就需要点击上图中“
Edit Metadata
”,随后在弹出的“
Set Raster Metadata
”窗口中点击左上角的“
Add...
”选项,将弹出另一个“
Add Metadata Items
”窗口。

随后,在“
Add Metadata Items
”窗口中选择“
Spatial Reference
”选项,并点击“
OK
”。

稍等片刻(这段时间中,
ENVI
软件可能会出现如同卡死一般的闪烁,大家不用管,多等待一会即可),可以看到在“
Set Raster Metadata
”窗口中,已经出现如下所示的“
Spatial Reference
”选项。

我们对“
Spatial Reference
”选项进行编辑即可。其中,首先需要选择地理坐标系或投影坐标系的种类;其次配置遥感图像的空间分辨率,也就是每一个像元的
X
大小和
Y
大小;再次,“
Tie Point
”中,前两个选项(“
Pixel X
”与“
Pixel Y
”)为我们
参考点
(这个参考点具体是什么,我们稍后会介绍)在图像中的位置,后两个选项(“
Map X
”与“
Map Y
”)则是该
参考点
实际的空间位置——如果我们选择的是
地理坐标系
,那么这里就是实际的
经纬度
;如果我们选择的是
投影坐标系
,那么这里就是实际的
投影数值
。最后,配置坐标系的旋转角度,一般填
0
就可以。我在这里只是做一个示范,因此下图中的各参数也都是乱填的,大家依据实际情况来配置各参数即可。

关于这个“
参考点
”,这里有必要再多提几句。
参考点
其实就是该图像中,某一个已知
实际空间坐标信息
、已知
其在图像中位置
的点;我们需要将这个点在
图像中的位置
(以行列号的形式表示,行数与列数均从
0
开始算起,遥感影像左上角的
像元
的左上角

为第
0
行第
0
列)与该点在
实际中的位置
输入进去,然后软件再依据我们所选择的坐标系与图像空间分辨率,对图像中每一个像元的空间位置进行计算,从而最终生成一个带有地理参考信息的栅格图像。

随后点击“
OK
”,即可完成对该图像的地理参考信息的配置。我们再一次查看该图像的元数据,可以发现此时其已经含有“
Map Info
”这个选项,且其中的参数都已经是刚刚我们设定的参数了。

这里我们再依据结果图像,来再解释一下参考点的意义。通过上图我们可以知道,我在本文中是将“
Pixel X
”与“
Pixel Y
”均为
0
的这个点作为参考点,并将其空间位置(“
Map X
”与“
Map Y
”)均设置为
1
;那么在结果图中,我们通过
Crosshairs
功能、
Cursor Value
功能确定一下该点的位置,如下图所示;可以看到“
Pixel X
”与“
Pixel Y
”均为
0
的这个点(图中黄色圈内),其经、纬度就近似为1°与1°(之所以是近似,是因为我也不是完全选中了这个参考点,而是近似选中)。

至此,大功告成。

C# 中使对象序列化/反序列化 Json 支持使用派生类型以及泛型方式

废话

前言

为啥想写这个博客

  • 最近自己写的框架有用到这个

      类似工作流,支持节点编码自定义,动态运行自定义.
    
      尽量减少动态解析这就需要确定类型.
    

    有什么好的奇思妙想可以一起来讨论噢
    (现在还是毛坯,测试各种可能性)

  • 方便C#编码过程有泛型 写起来舒服

  • 编译期确定类型

RoslynPad
以及
.Dump()
方法说明

RoslynPad
是基于Roslyn 实现的跨平台C# 编辑器,简洁轻巧
支持nuget引用包
支持.NET框架版本切换

.Dump()
方法是 RoslynPad 支持的一个诊断方法,方便 赋值并打印对象信息(看作是 Console.WriteLine就行 但是 Dump方法会返回当前访问实例 例如 int i = 1.Dump() ,i依然会被赋值为 1);

通过
[JsonDerivedType]
特性实现支持派生类型序列化/反序列化

首先定义
Base
以及 它的派生类
Sub
并重写父类的
GetValue
方法


public class Sub:Base
{
    public object? Value { get; set; } = 15;
    public override object? GetValue() 
    {
        return Value;
    }
}

public class Base
{
   public virtual object? GetValue()
   {
        return default;
   }
}

当我们在程序中直接使用
Base
接收并调用
Sub
这个派生类的时候肯定没有任何问题(因为
b
运行时类型还是原来的
Sub
).

但是当我们如果需要将它序列化为json字符串传输的时候.

由于他已经脱离了原本类型的运行环境,只是一个json字符串,它当中没有任何关于它原来的类型信息记录,反序列化时json解析器根本不认识原来的运行时类型,他只知道应用定义的解析需要的类型是
Base
而派生类
Sub.Value
属性会被丢弃,但由于程序中很多地方都是用父类类型接收的,所以会导致信息的丢失.


using System.Text.Json;
using System.Text.Json.Serialization;

Base b = new Sub();

b.GetValue().Dump();

string json = JsonSerializer.Serialize(b).Dump();

Base desb = JsonSerializer.Deserialize<Base>(json).Dump();

输出


15 //b.GetValue().Dump();

{} // string json = JsonSerializer.Serialize(b).Dump();

Base //Base desb = JsonSerializer.Deserialize<Base>(json).Dump();

所以我们需要做的是在
序列化
/
反序列化
的时候
生成
/
解析
它原本类型的
标记信息
,让我们的应用识别到他的具体类型,这样在程序中使用父类接收的地方可以保证运行时类型正确.

System.Text.Json
提供了
JsonDerivedType
特性用以在父类中标注派生类以及序列化时候的标记名称


Base b = new Sub();

b.GetValue().Dump();

string json = JsonSerializer.Serialize(b).Dump();

Base desb = JsonSerializer.Deserialize<Base>(json).Dump();


public class Sub:Base
{
    public object? Value { get; set; } = 15;
    public override object? GetValue() 
    {
        return Value;
    }
}

[JsonDerivedType(typeof(Sub),"subType")] // 添加特性
public class Base
{
   public virtual object? GetValue()
   {
        return default;
   }
}

输出


15 //b.GetValue().Dump();

{"$type":"subType","Value":15} // string json = JsonSerializer.Serialize(b).Dump();

Sub   //Base desb = JsonSerializer.Deserialize<Base>(json).Dump();
  Value = 15
    Item = <null>
    ValueKind = Number
      value__ = 4

可以看到
b
在序列化为json字符串时带上了我们特性上指定的
subType
并赋值给了
$type
属性
当我们反序列化为运行时对象时应用正确反序列化为了
Sub
对象.

但这只是最简单的一个场景, 我们日常使用最多的场景还是 在继承的基础上还要加上泛型,但System.Text.Json中默认不支持泛型的序列化/反序列化.

当我们把代码改造为泛型之后会得到以下错误

  • 无法支持泛型类型

    
    [JsonDerivedType(typeof(SubT<>),"subType_G")]
    public class Base<T>
    {
       public virtual T? GetValue()
       {
            return default;
       }
    }
    
    

    异常

    Specified type 'SubT`1[T]' is not a supported derived type for the polymorphic type 'Base`1[System.Int32]'. Derived types must be assignable to the base type, must not be generic and cannot be abstract classes or interfaces unless 'JsonUnknownDerivedTypeHandling.FallBackToNearestAncestor' is specified.
    
  • 也无法支持不同泛型单独定义

    
      [JsonDerivedType(typeof(SubT<int>),"subType_Int")]
      [JsonDerivedType(typeof(SubT<bool>),"subType_Bool")]
      public class Base<T>
      {
         public virtual T? GetValue()
         {
              return default;
         }
      }
    
    

    异常

    
    Specified type 'SubT`1[System.Boolean]' is not a supported derived type for the polymorphic type 'Base`1[System.Int32]'. Derived types must be assignable to the base type, must not be generic and cannot be abstract classes or interfaces unless 'JsonUnknownDerivedTypeHandling.FallBackToNearestAncestor' is specified.
    
    
  • 当只定义单一泛型基础类型时可以序列化,但反序列化依然异常仍需要单独定义读取,且父类及派生类都需要定义单一泛型类型实现定义(繁琐且不实用,谁定义泛型只会用一种基础类型的泛型啊)

    
      [JsonDerivedType(typeof(SubT<int>),"subType_Int")]
      public class SubT<T>:Base<T> 
      {
          public T TValue { get; set; }
    
          public override T? GetValue()
          {
              return TValue;
          }
      }
    
      [JsonDerivedType(typeof(Base<int>),"base_Int")]
      public class Base<T>
      {
         public virtual T? GetValue()
         {
              return default;
         }
      }
    
    

    15 //b.GetValue().Dump();
    
    {"$type":"subType_Int","TValue":15} // string json = JsonSerializer.Serialize(b).Dump();
    
    Read unrecognized type discriminator id 'subType_Int'. Path: $ | LineNumber: 0 |  // Base<int> desb = JsonSerializer.Deserialize<Base<int>>(json).Dump();BytePositionInLine: 32. 
    
    

通过
[JsonConverter]
特性 以及
[KnowType]
特性标注派生类型实现支持自定义类型序列化

通过使用 System.Text.Json
[JsonDerivedType]
可以实现简单的派生类型与基类转换.

但是遇到复杂的派生类型例如(泛型)则显得十分无力.

当我们需要支持复杂的类型转换的时候得需要用到另一个特性
JsonConvertAttribute
搭配自定义实现
JsonConvert<T>
了.

先定义一个特性用来标注序列化/反序列化过程中类型的定义包含泛型信息


// 自定义泛型类型名特性
public class GenericTypeNameAttribute:Attribute
{
    // 生成的属性名称
    public string GenericTypePropertyName { get; set; }
    
    // 泛型基础名称
    public string BaseName { get; set; }

    // 根据泛型基础类型T属性值
    public string GetGValue (string genericTypeName) => $"{GeneratePrefix}_{genericTypeName}";
    
    // 生成值前缀
    public string GeneratePrefix => $"{BaseName}_G";
}

然后将原来的
Base
,
Sub
改为
Base<T>
,
Sub<T>
,由于有了泛型 可以将之前返回值从
object
改为 对应的泛型
T
,
并将
[GenericTypeName]
和 关键的
[JsonConverter]
添加上


[GenericTypeName(GenericTypePropertyName = "$type",BaseName = nameof(Sub<T>))]
[JsonConverter(typeof(SubConverter<int>))]
public class Sub<T> :Base<T>
{
    public T Value { get; set; }
    
    public override T? GetValue() 
    {
        return Value;
    }
}

[GenericTypeName(GenericTypePropertyName = "$type",BaseName = nameof(Base<T>))]
[KnownType(typeof(Sub<>))]
[JsonConverter(typeof(BaseConverter<int>))]
public class Base<T>
{
   public virtual T? GetValue()
   {
        return default;
   }
}

并实现
JsonConverter<Base<T>>

JsonConverter<Sub<T>>

BaseConvert<T>


public class BaseConverter<T>:JsonConverter<Base<T>>
{
    public override Base<T>? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
    {
        var markerAttribute = typeToConvert.GetCustomAttribute<GenericTypeNameAttribute>()!;
        string genericTypeName = markerAttribute.GenericTypePropertyName!;
        
        string? typeName = default;
        
        T? tV = default;
        
        while(reader.Read())
        {
            if(reader.TokenType == JsonTokenType.EndObject)
                break;
                
            if(reader.TokenType == JsonTokenType.PropertyName)
            {
                string propertyName = reader.GetString() ?? throw new ArgumentException("Base<T> PropertyName");
                
                // 如果名称等于标注特性上的属性名称
                if(propertyName == genericTypeName)
                {
                    // 提前读取
                    reader.Read();
                    typeName = reader.GetString();
                    continue;
                }
            }else
            {
                JsonConverter<T> tConverter = (JsonConverter<T>)options.GetConverter(typeof(T));
                
                tV = tConverter.Read(ref reader,typeof(T),options);
            }
        }
        
        ArgumentException.ThrowIfNullOrWhiteSpace(typeName);
        
        //这里只演示 ,偷懒,如果有值就为 Sub<T> 如果要更通用的需要根据类型手动构造
        
        if(tV is not null)
        {
            return new Sub<T>{ Value = tV };
        }
        
        return new Base<T>();
    }

    public override void Write(Utf8JsonWriter writer, Base<T> value, JsonSerializerOptions options)
    {
    
        // 获取要写入的的类型
        var sourceType = value.GetType()!;
        
        // 获取 泛型 T 类型的名称
        string gernericName = sourceType.GenericTypeArguments.First().Name;
        
        // 我们自定义的标注特性
        // 可以缓存起来
        string genericTypeName = sourceType.GetCustomAttribute<GenericTypeNameAttribute>()!.GenericTypePropertyName!;
        string gernericBaseTypeName = sourceType.GetCustomAttribute<GenericTypeNameAttribute>()!.BaseName;
        
        // 如果是派生类型的泛型
        if(sourceType.GetGenericTypeDefinition() != typeof(Base<>))
        {
            var knowTypes =  Type.GetCustomAttributes<KnownTypeAttribute>();
            
            // 从 KnownType 中查找注册类型
            var targetType = knowTypes?.FirstOrDefault(
                    x => x.Type?.GetGenericTypeDefinition() == sourceType.GetGenericTypeDefinition());
            
            if(targetType != null && targetType.Type != null)
            {
                // 构建泛型类型
                var mkType = targetType.Type.MakeGenericType(sourceType.GenericTypeArguments[0]);
                
                // 调用对应已注册类型序列化方法
                writer.WriteRawValue(JsonSerializer.Serialize(value,mkType));
                return;
            }
        }
        
        // Base<T> 本身没任何属性 写入泛型类型就结束了
        writer.WriteStartObject();
        
        writer.WriteString(JsonNamingPolicy.CamelCase.ConvertName(genericTypeName),$"{gernericBaseTypeName}_G_{gernericName}");
        
        writer.WriteEndObject();
    }
}

SubConverter<T>


public class SubConverter<T>: JsonConverter<Sub<T>>
{
    public override Sub<T>? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
    {
        // 找到用于标记的特性
        string genericTypeName = typeToConvert.GetCustomAttribute<GenericTypeNameAttribute>()!.GenericTypePropertyName!;
        
        // 并未用到这个typeName 只是用来记录 可以根据具体需求使用
        string? typeName = default;
        
        T tV = default;
        
        // 当可以继续读取的时候
        while(reader.Read())
        {
            // 读到json对象末尾了 退出
            if(reader.TokenType == JsonTokenType.EndObject)
                break;
            
            // 读到属性名称记录下
            if(reader.TokenType == JsonTokenType.PropertyName)
            {
                string propertyName = reader.GetString();
                
                // 如果属性名称是特性标记中的名称
                if(propertyName == genericTypeName)
                {
                    // 手动继续读取
                    reader.Read();
                    // 获取到名称
                    typeName = reader.GetString();
                    // 并跳过当此循环 因为以及预读取过
                    continue;
                }
            }else
            {
                // 当初也在想怎么构建 泛型 T 的类型的实例
                // 后面参照官网示例
                // 是通过获取 T 对应的 JsonConverter 获取 并调用 Read 方法构建 (妙啊)
                // 例如: T为 int 则 JsonConverter<T> 其实就是获取 JsonConverter<int> 而基础类型基本都内置 
                // 所以不用专门去写 
                JsonConverter<T> tConverter = (JsonConverter<T>)options.GetConverter(typeof(T));
                
                tV = tConverter.Read(ref reader,typeof(T),options);
            }
        }
        
        ArgumentException.ThrowIfNullOrWhiteSpace(typeName);
        
        return new Sub<T>(){ Value = tV };
    }

    public override void Write(Utf8JsonWriter writer, Sub<T> value, JsonSerializerOptions options)
    {
         var sourceType = value.GetType()!;
        
        string genericName = sourceType.GenericTypeArguments.First().Name;
        
        var markerAttribute = sourceType.GetCustomAttribute<GenericTypeNameAttribute>()!;
        string genericTypePropName = markerAttribute.GenericTypePropertyName!;
        
        writer.WriteStartObject();
        
        if(value is Sub<string> st)
        {
            writer.WriteString("Value",st.Value);
        }else if(value is Sub<int> it)
        {
            writer.WriteNumber("Value",it.Value);
        }else if(value is Sub<bool> bt)
        {
            writer.WriteBoolean("Value",bt.Value);
        }
        
        writer.WriteString(JsonNamingPolicy.CamelCase.ConvertName(genericTypePropName),markerAttribute.GetGValue(genericName));
        writer.WriteEndObject();
    }
}

完成上述步骤之后我们就可以愉快的开始愉快的泛型序列化了......吗?

将我们的调用改为泛型调用


Base<int> i = new Sub<int>{ Value = 15 };

string json = JsonSerializer.Serialize(i).Dump();

Base<int> des = JsonSerializer.Deserialize<Base<int>>(json);

des.Dump();

输出


{"Value":15,"$type":"Sub_G_Int32"} // string json = JsonSerializer.Serialize(i).Dump();

Sub`1[System.Int32] // des.Dump();
  Value = 15

貌似没什么问题了...

等等...

泛型,那我改改类型试试

将 上面
Base<T>
,
Sub<T>
上的
JsonConvert<T>
泛型改为
bool
试试

输出


{"Value":true,"$type":"Sub_G_Boolean"} // string json = JsonSerializer.Serialize(i).

Sub`1[System.Boolean] // des.Dump();
  Value = True

好像也没问题

Ok, 那把
Base<T>
,
Sub<T>
上的
JsonConvert<T>

T
去掉 不指定类型 让他通用起来

......省略代码

[JsonConverter(typeof(BaseConverter<>))]
public class Base<T>

......省略代码

运行试试


Cannot create an instance of BaseConverter`1[T] because Type.ContainsGenericParameters is true.

啊 ?

竟然异常了,这不是玩我吗 ? 竟然
JsonConvertAttribute
传入的
Type
不支持泛型
从异常信息来看 ,好像是某种约束默认不让泛型参数

because Type.ContainsGenericParameters is true

经过一番查找最后在微软官方指引里发现了
JsonConverterFactory
这个类,用来
给支持泛型的房子加上最后一块砖

借由
JsonConverterFactory
实现支持泛型序列化/反序列化

继承并重写
JsonConverterFactory

CanConvert
以及
CreateConverter
方法


// 定义泛型转换器创建工厂
public abstract class GenericTypeConverterFactory : JsonConverterFactory
{
    // 泛型类型
    public abstract Type GenericType { get; }
    
    // 对应转换器泛型类型
    public abstract Type GenericJsonConvertType { get; }
    
    // 什么类型可以转换
    public override bool CanConvert(Type typeToConvert)
    {
        // 这里约束了只有泛型类型可以转换
        return typeToConvert.IsGenericType && typeToConvert.GetGenericTypeDefinition() == GenericType;
    }

    public override JsonConverter? CreateConverter(Type typeToConvert, JsonSerializerOptions options)
    {
        // 获取泛型类型
        Type valueType = typeToConvert.GetGenericArguments()[0];

        // 手动构造泛型转换器的类型
        Type converterType = GenericJsonConvertType.MakeGenericType(valueType);
        
        // 获取对应的实例
        var ist = (JsonConverter?)Activator.CreateInstance(converterType);
        
        return ist;
    }
}

public sealed class BaseConverterFactory:GenericTypeConverterFactory
{
    public override Type GenericType => typeof(Base<>);
    public override Type GenericJsonConvertType => typeof(MyConverter<>);
}

public sealed class SubConverterFactory:GenericTypeConverterFactory
{
    public override Type GenericType => typeof(Sub<>);
    public override Type GenericJsonConvertType => typeof(SubConverter<>);
}

由于
JsonConverterFactory
是继承
JsonConverter
的 , 所以我们需要将
Base<T>

Sub<T>
上的
JsonConvert
替换为刚刚实现的两个工厂

......省略代码

[JsonConverter(typeof(BaseConverterFactory))]
public class Base<T>

......省略代码

运行
bool


{"Value":true,"$type":"Sub_G_Boolean"}

Sub`1[System.Boolean]
  Value = True


运行
int


{"Value":12,"$type":"Sub_G_Int32"}

Sub`1[System.Int32]
  Value = 12


运行
string


{"Value":"hello world","$type":"Sub_G_String"}

Sub`1[System.String]
  Value = hello world


完美 !!!!

结尾

上面就是我探索 json 泛型序列化的过程.

过程还是挺曲折

感觉这个需求挺小众,找了各个网站都没有这方面的解决方案.

不甘心的我对着微软的文档一个个特性研究,生怕错过一个关于这方面的能力.

最后的解决方案已经满足了我的需求

最后,上面的代码都是我想尽快发出博客手敲出来的,难免会有错误和没有达到最优性能的情况,但总体过程还是挺完整的.

简介

nginx中的模块虽然就是类似插件的概念,但是它无法像VsCode那样轻松的安装扩展。

nginx要安装其它模块必须同时拿到nginx源代码和模块源代码,然后手动编译,将模块打到nginx中,最终生成一个名为nginx的可执行文件。

流程

  • 查看当前nginx 的版本(假设安装位置为:/usr/local/nginx)
  • 下载当前版本的源代码
  • 下载 nginx-rtmp-module模块源代码
  • 重新编译nginx并追加nginx-rtmp-module
  • 将新编译好的 nginx 可执行文件拷贝到当前nginx安装目录(/usr/local/nginx/sbin)

步骤

查看当前已经安装的nginx版本


[root@bogon sbin]# ./nginx -V
nginx version: nginx/1.18.0
built by gcc 4.8.5 20150623 (Red Hat 4.8.5-44) (GCC)
built with OpenSSL 1.0.2k-fips  26 Jan 2017
TLS SNI support enabled
configure arguments: --prefix=/usr/local/nginx --with-http_dav_module --with-http_stub_status_module --with-http_addition_module --with-http_sub_module --with-http_flv_module --with-http_mp4_module --with-pcre --with-http_ssl_module --with-http_gzip_static_module


  • V:小v仅打印版本号,大V既打印版本号,也打印扩展信息,这里要使用大V
  • 拿到版本号的目的是待会要下载该版本的源代码
  • 注意保存打印内容中的 arguments 后面的内容,后面编译会用到

下载对应版本的nginx源代码

找一个目录,假设为 /home/cml

本文即 1.18.0


wget http://nginx.org/download/nginx-1.18.0.tar.gz


tar -zxvf   nginx-1.18.0.tar.gz

下载nginx-rtmp-module模块源代码

进入到 /home/cml


git clone https://github.com/arut/nginx-rtmp-module.git

重新编译nginx

此时,/home/cml 目录下面有两个目录

  • nginx-1.18.0
  • nginx-rtmp-module

进入 nginx-1.18.0 目录,可以看到有一个 configure 可执行文件


./configure  --prefix=/usr/local/nginx --with-http_dav_module --with-http_stub_status_module --with-http_addition_module --with-http_sub_module --with-http_flv_module --with-http_mp4_module --with-pcre --with-http_ssl_module --with-http_gzip_static_module --add-module=/home/cml/nginx-rtmp-module


  • 使用configure 配置编译环境
  • 将上面 保存的“arguments 后面的内容”作为configure 的第一个参数
  • 第二个参数为 --add-module=/home/cml/nginx-rtmp-module ,即添加一个模块,模块源代码位置是 /home/cml/nginx-rtmp-module

如果没有出错,执行 编译命令


make

编译成功后,在/home/cml/nginx-1.18.0/objs 中会有一个 名为nginx的可执行文件,这个就是编译好的了, 里面包含已经安装的功能和新增加的nginx-rtmp-module,将这个可执行文件拷贝到当前安装目录(/usr/local/nginx/sbin)中就可以了。注意拷贝之前需要停止nginx,否则会报错。

验证nginx-rtmp-module是否安装好了


nginx   -V 

若打印的信息中包含nginx-rtmp-module,说明安装好了。

引用