2024年1月

张麻子:汤师爷,翻译翻译,什么叫AOP?

汤师爷:这还用翻译。

张麻子:我让你翻译给我听,什么叫AOP?

汤师爷:不用翻译,切面编程就是AOP啊。

黄四郎:难道你听不懂什么叫AOP?

张麻子:我就想让你翻译翻译,什么叫AOP!

汤师爷:AOP嘛。

张麻子:翻译出来给我听,什么他妈的叫AOP!什么他妈的叫他妈的AOP!

汤师爷:什么他妈的叫AOP啊?

黄四郎:AOP就是Aspect Oriented Programming,面向切面编程!明白了吗?

汤师爷:这就是AOP啊。

张麻子:翻译翻译。

汤师爷:...

汤师爷:AOP就是Aspect Oriented Programming!面向切面编程!面向!切面!横着切!切面!

张麻子:哈,大哥这是他妈的AOP啊,小弟我马上给个三连。

下面我们好好翻译一下AOP切面编程。

老规矩,在学习切面面编程之前要有前置知识:

终于搞懂动态代理了!

目标

• 动态代理搞明白,AOP就明白了

• 学会在开发中使用Spring的AOP技术

概念

AOP 是 Aspect Oriented Programming,和OOP(Object Oriented Programming)一词之差,OOP强调万物皆是对象,那AOP呢?

要真正理解 AOP 就要理解 AOP 的核心:
Aspect

WTF is Aspect?

我们把aspect翻译成切面,但是切面这个词对应中文语义其实很难理解到位。

我们换种解释,aspect我们理解为
事物的某个方面、某个视角

与面向对象思想相对,对象强调一个整体,一个人站在你面前,我们称之为对象。

而aspect强调
功能化、模块化、关注点分离

天气变冷了,人要多穿衣服,上帝控制这么多人的对象,总不能一件一件穿吧?

所以AOP思想就是把天气变冷穿衣服这件事抽离出来,模块化,单独进行关注,然后经过编码实现后,上帝就可以进行一键穿衣,节省了大量工作。

放在实际开发中,我们以最常见的
日志打印
为例。

我们系统中有200个Controller,都要打印请求日志,那没有AOP思想的实践的话,我们只能一个一个在Controller里编写一遍又一遍的重复代码,有了AOP思想,就可以考虑把日志打印这件事抽离出来做成单独的业务,实现一劳永逸。

那怎么实现呢?

那就要靠我们的
动态代理
了。

什么?你还没看动态代理?

上帝在吗?把这个人的棉衣扒了。

Spring实现AOP

我们知道框架的存在意义是用来简化开发的。

这里Spring简化了什么呢?

自然是我们在动态代理部分编写的一大堆
要么看不懂,看懂了又不想写
的代码。

AOP作为Spring的左膀右臂之一,自然对这部分加以简化。

但Spring那一大堆xml也是够够的。

所以SpringBoot才是我们永远的家。

废话不多说直接上代码示例,在AOP代码的编写中提出问题,解决问题,那么最后就算是学会了。

引入依赖

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!--aop-->
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-aop</artifactId>
</dependency>
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-test</artifactId>
    <scope>test</scope>
</dependency>

编写代码

编写这部分代码的逻辑也是非常符合我们的认知逻辑的。

我们前边已经说清楚AOP是做什么的了。

那我们编写代码就要做3件事:


  • 抽离出功能模块
    ——定义切面


  • 确认功能代码加在哪
    ——定义切点


  • 确认功能代码什么时候执行
    ——选择通知类型

我们先把controller给出来:

@RestController
public class AopController {

    @RequestMapping("/hello")
    public String sayHello(){
        System.out.println("hello");
        return "hello";
    }
}

定义一个切面

也就是
抽离出功能模块

先随便写个类。

然后就直接一个@Aspect就行了,那这个类就是一个切面类。

还要再加一个@Component将该类纳入Ioc容器。

就这么简单,狗来了都会写。

@Aspect
@Component
public class AopAdvice {
}

定义一个切点

也就是
确认功能代码加在哪

先随便写一个方法。

然后就直接一个@Pointcut就行了,那这个方法就是一个切点。

还要再加上表达式,让系统知道代码加到什么位置。

@Aspect
@Component
public class AopAdvice {

    @Pointcut("execution (* com.example.aop.controller.*.*(..))")
    public void test() {
    }
}

这时候有同学问:

啊这个execution是什么?

里面那又是一坨什么?

根本看不懂。

举报了。

这个我只能说,这是固定的表达式,是规定。

规定什么?

看规定之前先记住:表达式一定从右往左匹配。

看规定之前先记住:表达式一定从右往左匹配。

看规定之前先记住:表达式一定从右往左匹配。

execution(访问修饰符(可省略) 方法返回值 包名.类名.方法名(参数))
参数:..代表任何参数
方法: *代表任何方法
类名: *代表所有类
包名: *代表所有包 ..代表子孙包
返回值: *代表所有类型返回值

具体的写法实际五花八门,而且除了execution还有一大堆,为了不让大脑过度疲劳,我们一次只有一个目标:

会用,但不精通。

选择通知类型

也就是
确认功能代码什么时候执行

下面就是通知类型5种,前3种比较常用:

前置通知
(@Before):在目标方法调用之前调用通知

后置通知
(@After):在目标方法完成之后调用通知

环绕通知
(@Around):在被通知的方法调用之前和调用之后执行自定义的方法

返回通知
(@AfterReturning):在目标方法成功执行之后调用通知

异常通知
(@AfterThrowing):在目标方法抛出异常之后调用通知

代码如下:

@Aspect
@Component
public class AopAdvice {

    @Pointcut("execution (* com.example.aop.controller.*.*(..))")
    public void test() {

    }

    @Before("test()")
    public void beforeAdvice() {
        System.out.println("beforeAdvice...");
    }

    @After("test()")
    public void afterAdvice() {
        System.out.println("afterAdvice...");
    }

    @Around("test()")
    public void aroundAdvice(ProceedingJoinPoint joinPoint) {
        System.out.println("before");
        try {
            joinPoint.proceed();
        } catch (Throwable t) {
            t.printStackTrace();
        }
        System.out.println("after");
    }
}

从以上代码中可以看出,@Before和@After都已经简单到不能再简单了。

我们需要说一下这个@Around
环绕通知

joinPoint.proceed()这行代码我们就理解为我们
需要增强的那个方法的替身
就行了。

这也不是想说的,这里我们主要讲一下 ProceedingJoinPoint joinPoint 这个参数。

ProceedingJoinPoint 是一个接口,也就是说这里实际是使用了多态,这不重要。

ProceedingJoinPoint 继承了 JoinPoint 这个接口。

这个JoinPoint有2个方法是我们需要说的。

Object getTarget();
Signature getSignature();

getTarget()
方法返回的是目标对象,即那个被增强方法所属的对象实例。

getSignature()
方法返回的是连接点的签名,即关于被调用的方法(或访问的字段等)的静态信息,如方法名、返回类型、参数类型等。

那么通过这2个方法,就能获取到
所有的对象信息和方法信息
,那么能做的事就太多了。

但是我们这里不展示更复杂的案例。

依然坚持我们本篇的策略:

懂点,但不多。

会用,但不精。

测试

启动项目,浏览器访问:

http://localhost:8080/hello

运行结果:

before
beforeAdvice...
hello
afterAdvice...
after


往期推荐:


0.o?让我看看怎么个事儿之SpringBoot自动配置



终于搞懂动态代理了!


学会@ConfigurationProperties月薪过三千


学一点关于JVM类加载的知识


Java注解,看完就会用


Java反射,看完就会用

Golang之文件系统事件监听

基本介绍

文件系统事件是指文件系统相关的各种操作和状态变化,当一个应用层的进程操作文件或目录时,会触发system call,内核的notification子系统可以守在那里,把该进程对文件的操作上报给应用层的监听进程。这些事件可以包括文件和目录的创建、修改、删除和文件权限的更改等。

Linux中常用的有两种机制能够监听这些文件事件,分别为inotify和fanotify。

inotify和fanotify最大的区别就是fanotify能够监听到是哪个进程对文件或目录进行操作,并且能够阻止该操作。

fanotify

fanotify:Linux 2.6.37版本引入,能够通知用户哪个进程触发了哪些事件,并且能够对其进行干预。

Golang中
fanotify
有两个函数:

func FanotifyInit(flags uint, event_f_flags uint) (fd int, err error)
func FanotifyMark(fd int, flags uint, mask uint64, dirFd int, pathname string) (err error)

函数介绍

  • func FanotifyInit(flags uint, event_f_flags uint) (fd int, err error)

    该函数初始化了一个新的fanotify事件组,并返回与该组关联的事件队列的文件描述符,文件描述符用来调用FanotifyMark函数,以指定应该为其创建fanotify事件的文件、目录、挂载或文件系统,通过读取文件描述符来接收这些事件。

    flags
    参数包含一个多位字段,用于定义监听应用程序的通知类型,可选的值有:

    FAN_CLASS_CONTENT            = 0x4   适用于需要访问已经包含最终内容的文件的事件监听器
    FAN_CLASS_NOTIF              = 0x0   默认值,不需要指定,只用于监听,不访问文件内容
    FAN_CLASS_PRE_CONTENT        = 0x8   适用于需要在文件包含最终数据之前访问文件的事件监听器*/
    FAN_CLOEXEC                  = 0x1   如果在程序运行时打开了一个文件描述符,并且在调用时没有关闭,那么新程序中仍然能够使用该文件描述符,设置这个字段,可以确保调用时关闭文件描述符
    FAN_NONBLOCK                 = 0x2   为文件描述符启用非阻塞标志,读取文件描述符时不会被阻塞
    FAN_UNLIMITED_MARKS          = 0x20   取消对每个用户的通知标记数量的限制
    FAN_UNLIMITED_QUEUE          = 0x10   删除对事件队列中事件数量的限制
    FAN_REPORT_DFID_NAME         = 0xc00  这是(FAN_REPORT_DIR_FID|FAN_REPORT_NAME)的同义词
    FAN_REPORT_DFID_NAME_TARGET  = 0x1e00 这是(FAN_REPORT_DFID_NAME|FAN_REPORT_FID|FAN_REPORT_TARGET_FID)的同义词
    FAN_REPORT_DIR_FID           = 0x400  Linux 5.9后的功能,使用此标志初始化的通知组的事件将包含与事件相关的目录对象的附加信息
    FAN_REPORT_FID               = 0x200  Linux 5.1后的功能,使用此标志初始化的通知组的事件将包含相关的底层文件系统对象的附加信息
    FAN_REPORT_NAME              = 0x800  Linux 5.9后的功能,使用此标志初始化的通知组的事件将包含与事件相关的目录条目名称的附加信息
    FAN_REPORT_PIDFD             = 0x80   Linux 5.15后的功能,使用此标志初始化的事件将包含一个附加的信息记录
    FAN_REPORT_TARGET_FID        = 0x1000 Linux 5.17后的功能,使用此标志初始化的通知组的事件将包含与目录条目修改事件相关的子节点的附加信息
    FAN_REPORT_TID               = 0x100  Linux 4.20后的功能,报告线程ID(TID)而不是进程ID(PID)
    FAN_ENABLE_AUDIT             = 0x40   Linux 4.15后的功能,启用生成权限事件执行的访问中介的审计日志记录
    

    event_f_flags
    参数定义了文件描述符状态,可选的值有:

    O_RDONLY               = 0x0     只读
    O_RDWR                 = 0x2     读写
    O_WRONLY               = 0x1     只写
    O_LARGEFILE            = 0x0     启用对超过2gb的文件的支持。在32位系统上,
    O_CLOEXEC              = 0x80000 Linux 3.18后的功能为文件描述符启用close-on-exec标志
    这些也是可以的O_APPEND,O_DSYNC,O_NOATIME,O_NONBLOCK,O_SYNC
    
  • func FanotifyMark(fd int, flags uint, mask uint64, dirFd int, pathname string) (err error)

    该函数在文件系统对象上添加、删除或修改fanotify标记,调用者必须对要标记的文件系统对象具有读权限。

    fd
    参数是由FanotifyInit函数返回的文件描述符。

    flags
    参数是描述要执行的操作,可选的值有:

    FAN_MARK_ADD                                = 0x1
    FAN_MARK_DONT_FOLLOW                        = 0x4
    FAN_MARK_EVICTABLE                          = 0x200 Linux 5.19后的功能
    FAN_MARK_FILESYSTEM                         = 0x100 Linux 4.20后的功能
    FAN_MARK_FLUSH                              = 0x80
    FAN_MARK_IGNORE                             = 0x400 Linux 6.0后的功能
    FAN_MARK_IGNORED_MASK                       = 0x20
    FAN_MARK_IGNORED_SURV_MODIFY                = 0x40
    FAN_MARK_IGNORE_SURV                        = 0x440
    FAN_MARK_INODE                              = 0x0
    FAN_MARK_MOUNT                              = 0x10
    FAN_MARK_ONLYDIR                            = 0x8
    FAN_MARK_REMOVE                             = 0x2
    

    mask
    参数定义了应该监听哪些事件或者忽略哪些事件,可选的值有:

    FAN_ACCESS                                  = 0x1
    FAN_ACCESS_PERM                             = 0x20000
    FAN_MODIFY                                  = 0x2
    FAN_CLOSE                                   = 0x18
    FAN_CLOSE_NOWRITE                           = 0x10
    FAN_CLOSE_WRITE                             = 0x8
    FAN_OPEN                                    = 0x20
    FAN_OPEN_EXEC                               = 0x1000     Linux 5.0后的功能
    FAN_OPEN_EXEC_PERM                          = 0x40000    Linux 5.0后的功能
    FAN_OPEN_PERM                               = 0x10000
    FAN_ATTRIB                                  = 0x4        Linux 5.1后的功能
    FAN_CREATE                                  = 0x100      Linux 5.1后的功能
    FAN_DELETE                                  = 0x200      Linux 5.1后的功能
    FAN_DELETE_SELF                             = 0x400      Linux 5.1后的功能
    FAN_FS_ERROR                                = 0x8000     Linux 5.16后的功能
    FAN_MOVE                                    = 0xc0
    FAN_MOVED_FROM                              = 0x40       Linux 5.1后的功能
    FAN_MOVED_TO                                = 0x80       Linux 5.1后的功能
    FAN_MOVE_SELF                               = 0x800      Linux 5.1后的功能
    FAN_RENAME                                  = 0x10000000 Linux 5.17后的功能
    FAN_ONDIR                                   = 0x40000000
    FAN_EVENT_ON_CHILD                          = 0x8000000
    

    要标记的文件系统对象由文件描述符dirFd和pathname中指定的路径名决定


    • 如果pathname为空,则由dirFd确定
    • 如果pathname为空,并且dirFd的值为AT_FDCWD,监听当前工作目录
    • 如果pathname是绝对路径,dirFd被忽略
    • 如果pathname是相对路径,并且dirFd不是AT_FDCWD,监听pathname相对于dirFd目录的路径
    • 如果pathname是相对路径,并且dirFd为AT_FDCWD,监听pathname相对于当前目录的路径

示例

package main

import (
	"bytes"
	"encoding/binary"
	"errors"
	"fmt"
	"io"
	"log"
	"os"
	"path/filepath"
	"unsafe"

	"golang.org/x/sys/unix"
)

func handle_perm(initFd int, fanfd int32) error {
	fd := unix.FanotifyResponse{
		Fd:       fanfd,
		Response: uint32(unix.FAN_DENY),
	}
	buf := new(bytes.Buffer)
	err := binary.Write(buf, binary.LittleEndian, fd)
	if err != nil {
		log.Println(err)
	}
	ret, err := unix.Write(initFd, buf.Bytes())
	if err != nil {
		log.Println("handle_perm:err", err)
	}
	if ret < 0 {
		return err
	}
	return nil
}
func main() {
	path := "/root/testapp2/"
	name := filepath.Clean(path)
	initFd, err := unix.FanotifyInit(unix.FAN_CLOEXEC|unix.FAN_NONBLOCK|unix.FAN_CLASS_PRE_CONTENT, unix.O_RDONLY)
	if err != nil {
		log.Panicln("FanotifyInit err : ", err)
	}
	inotifyFile := os.NewFile(uintptr(initFd), "")
	if initFd == -1 {
		log.Println("fanFd err", err)
	}
	defer unix.Close(initFd)
	mask := uint64(unix.FAN_EVENT_ON_CHILD | unix.FAN_OPEN_PERM)
	err = unix.FanotifyMark(initFd, unix.FAN_MARK_ADD, mask, unix.AT_FDCWD, name)
	if err != nil {
		log.Panicln("FanotifyMark err : ", err)
	}
	fmt.Println("start:")
	fmt.Println("监控目录:", name)
	var (
		buf [unix.FAN_EVENT_METADATA_LEN * 4096]byte
	)
	for {
		n, err := inotifyFile.Read(buf[:])
		if err != nil {
			continue
		}
		if n < unix.FAN_EVENT_METADATA_LEN {
			if n == 0 {
				err = io.EOF
			} else if n < 0 {
				err = errors.New("notify: short ")
			} else {
				err = errors.New("notify: short read in readEvents()")
			}
			continue
		}
		var offset int
		for offset <= int(n-unix.FAN_EVENT_METADATA_LEN) {
			var (
				raw       = (*unix.FanotifyEventMetadata)(unsafe.Pointer(&buf[offset]))
				pid       = int32(raw.Pid)
				event_len = uint32(raw.Event_len)
				fd        = int32(raw.Fd)
			)
			fdPath := fmt.Sprintf("/proc/self/fd/%d", fd)
			f, err := os.Readlink(fdPath)
			if err != nil {
				log.Println(err)
			} else {
				fmt.Println("fdpath:", f)
			}
			proName := fmt.Sprintf("/proc/%d/comm", pid)
			pN, err := os.ReadFile(proName)
			if err != nil {
				log.Println(err)
				continue
			}
			if err := handle_perm(initFd, fd); err != nil {
				continue
			}
			fmt.Printf("阻止程序: %v", string(pN))
			offset += int(unix.FAN_EVENT_METADATA_LEN + event_len)
		}
	}
}

示例代码能够拒绝程序打开该目录下文件

前言:

在发布完:
开源:Taurus.DTC 微服务分布式事务框架,支持 .Net 和 .Net Core 双系列版本
,之后想想,好像除了事务外,感觉里面多了一个任务发布订阅的基础功能。

本想既然都有了基础发布订阅功能了,那要不要顺带加上延时发布功能呢?加上了会不会让事务组件不纯了?

经过一翻深思,是在其上补上功能,还是,重新写一个组件,起初起名是个难题,因为 DTC 也可以是 Distributed Task Cxxxxx, 组件重名了?

经过一翻英文大作战,找到了:Distributed Task Scheduler,简写可以是DTS了,才开始重启一个组件。

于是就有了这个Taurus.DTS 任务组件,而且功能除了原有的即时任务发布订阅,和本来想加上的延时任务,后面又补上了基于Cron表达式的定时任务,和广播群发任务。

经过一翻大作战,有 Taurus.DTC 的基础代码作底层支持,花了大几天,解决了各种疑难杂症之后,终于出来了。

1、开源地址:

https://github.com/cyq1162/Taurus.DTS

2、Nuget 包引用ID:

由于
CYQ.Data
Orm 组件本身支持10多种数据库,因此提供的包,只根据消息队列的需要分拆提供。

默认Taurus.DTS 支持同时使用 RabbitMQ 和 Kafka 两种消息队列。

如果一个项目中只用RabbitMQ,则引入 Tarurus.DTS.RabbitMQ,减少对 Kafka 的依赖包。

编绎的版本:支持太多,发布是个苦力活:

.Net 系列:由于引用依赖关系(RabbitMQ最低是 4.0,Kafka最低是 4.5);

.Net Core系列、支持
2.18.0版本及以后。

Standard 标准库:支持2.
1 及以后。

3、Taurus.DTS 微服务分布式任务框架基础说明:

基础说明:

1、框架分为: Client(客户端,即任务发起端)和 Server(服务端,即方法订阅方)。2、框架支持:即时任务、延时任务、Cron表达式任务定时任务、广播任务,四种方式。3、项目需要配置的参数:1、数据库(可选);2、MQ(必选)。


数据存储:

可选择数据库(MSSQL、MySql、Oracle、PostgreSql 等 CYQ.Data 所支持的10多种数据库之一)

MSSQL配置示例如下:

{"ConnectionStrings": {"DTS.Server.Conn": "server=.;database=MSLog;uid=sa;pwd=123456"}
}


消息队列:

目前消息队列支持 RabbitMQ 或者 Kafka(配置其中一方即可):

{"AppSettings": {"DTS.Server.Rabbit":"127.0.0.1;guest;guest;/",//ip;username;password;virtualpath;
  "DTS.Server.Kafka":"127.0.0.1:9092"}
}

以上配置为Server端,客户端更改 Server 为 Client 即可。


4、Server 端 使用示例:

1、Nuget 搜索 Taurus.DTS 引入工程项目中。

2、如果是 ASP.Net Core 程序:Program 或 Startup 添加服务使用引入:

  services.AddTaurusDts(); //服务添加。
  app.UseTaurusDts(TaskStartType.Server); //服务使用,启用服务端

3、appsettings.json 配置基本属性:

{"ConnectionStrings": {"DTS.Server.Conn": "host=localhost;port=3306;database=cyqdata;uid=root;pwd=123456;Convert Zero Datetime=True;"},"AppSettings": {"DTS.Server.Rabbit": "127.0.0.1;guest;guest;/" //IP;UserName;Password;VirtualPaath
}

4、选择数据库对应的依赖组件,如MySql,可以:

Nuget 上可以搜索 MySql.Data 、或者 CYQ.Data.MySql (会自动引入MySql.Data)  都可, 引入项目即可。

5、代码编写,可以参考源码中提供的示例代码,如下为控制台示例代码:

usingSystem;usingTaurus.Plugin.DistributedTask;namespaceConsole_App_Server {internal classProgram
{
static void Main(string[] args)
{

DTSConfig.Server.Rabbit
= "127.0.0.1;guest;guest;/";//DTSConfig.Server.Kafka = "127.0.0.1:9092;";//DTSConfig.Server.Conn = DTSConfig.Client.Conn; DTSConfig.ProjectName= "ConsoleApp5";

DTS.Server.Start();
//start client and server Console.WriteLine("---------------------------------------");

Console.ReadLine();
}


}
/// <summary> ///服务端 server class need to public/// </summary> public classServer
{
[DTSSubscribe(
"DoInstantTask")]public static boolA(DTSSubscribePara para)
{
para.CallBackContent
= "show you a.";return true;
}

[DTSSubscribe(
"DoDelayTask")]private static boolB(DTSSubscribePara para)
{
para.CallBackContent
= "show you b.";return true;
}
[DTSSubscribe(
"DoCronTask")]private static boolC(DTSSubscribePara para)
{
para.CallBackContent
= "show you c.";return true;
}
/// <summary> ///定时任务/// </summary> [DTSSubscribe("DoBroadastTask")]private static boolTimerTask(DTSSubscribePara para)
{
para.CallBackContent
= "show you d.";return true;
}
}

}


5、Client 端 使用示例:

1、Nuget 搜索 Taurus.DTS 引入工程项目中。

2、如果是ASP.Net Core 程序:Program 或 Startup 添加服务使用引入:

  services.AddTaurusDts(); //服务添加
  app.UseTaurusDts(StartType.Client); //服务使用,启用服务端

3、appsettings.json 配置基本属性:

{"ConnectionStrings": {"DTS.Client.Conn": "host=localhost;port=3306;database=cyqdata;uid=root;pwd=123456;Convert Zero Datetime=True;"},"AppSettings": {"DTS.Client.Rabbit": "127.0.0.1;guest;guest;/" //IP;UserName;Password;VirtualPaath
}

4、选择数据库对应的依赖组件,如MySql,可以:

Nuget 上可以搜索 MySql.Data 、或者 CYQ.Data.MySql (会自动引入MySql.Data)  都可, 引入项目即可。

5、代码编写,可以参考源码中提供的示例代码,如下为控制台示例代码:

usingSystem;usingSystem.Threading;usingTaurus.Plugin.DistributedTask;namespaceConsole_App_Client {internal classProgram
{
static void Main(string[] args)
{

DTSConfig.Client.IsPrintTraceLog
= false;//AppConfig.Redis.Servers = "127.0.0.1:6379"; DTSConfig.Client.Rabbit= "127.0.0.1;guest;guest;/";//DTSConfig.Client.Kafka = "127.0.0.1:9092;"; DTSConfig.Client.Conn = "server=.;database=mslog;uid=sa;pwd=123456";

DTSConfig.ProjectName
= "ConsoleApp5";

DTS.Client.Start();
//start client and server Console.WriteLine("---------------------------------------");
Console.WriteLine(
"1-InstantTask、2-DelayTask(1Minutes)、3-CronTask、4-DeleteCronTask、5-BroadastTask");
Console.WriteLine(
"Input :1、2、3、4、5,Press Enter.");while (true)
{
string line =Console.ReadLine();try{
Client.Run(
int.Parse(line));
}
catch(Exception err)
{
Console.WriteLine(err.Message);
}

}

}
}
/// <summary> ///客户端 client class need to public if has callback method./// </summary> public classClient
{
public static void Run(inti)
{
if (i == 2)
{
//发布一个延时1分钟的任务 DTS.Client.Delay.PublishAsync(1, "i publish a delay task.", "DoDelayTask", "DelayCallBack");
Console.WriteLine(
"Wait for 1 minute...");
}
else if (i == 3)
{
//发布一个秒在30时的循环任务。 DTS.Client.Cron.PublishAsync("10,30,50 * * * * ?", "i publish a timer task with cron express.", "DoCronTask", "CronCallBack");
Console.WriteLine(
"Wait for execute task when second is 10,30,50...");
}
else if (i == 4)
{
//发布一个秒在30时的循环任务。 DTS.Client.Cron.DeleteAsync("DoCronTask", null, "CronCallBack");
}
else if (i == 5)
{
//发布一个广播任务 DTS.Client.Broadast.PublishAsync("i publish a task for all server.", "DoBroadastTask", "BroadastCallBack");
}
else{for (int k = 0; k < 1; k++)
{
//发布一个即时任务 DTS.Client.Instant.PublishAsync("i publish a task instantly.", "DoInstantTask", "InstantCallBack");
Console.WriteLine(k);
}

}
}

[DTSCallBack(
"InstantCallBack")]
[DTSCallBack(
"DelayCallBack")]
[DTSCallBack(
"CronCallBack")]
[DTSCallBack(
"BroadastCallBack")]private static voidOnCallBack(DTSCallBackPara para)
{
Console.WriteLine(
"Client callback :" + para.TaskType + "-" + para.CallBackKey + "-" +para.CallBackContent);
}
}

}

6、Demo 运行示例:

demo 地址:
https://github.com/cyq1162/Taurus.DTS/tree/master/demo

启动运行截图:

输入1,发布即时任务:

7、其它:CYQ.Data 支持的数据库链接语句示例

###--------------------------------------------------------###
Txt:: Txt Path=E:\
Xml:: Xml Path=E:\
Access:: Provider=Microsoft.Jet.OLEDB.4.0; Data Source=E:\cyqdata.mdb
Sqlite:: Data Source=E:\cyqdata.db;failifmissing=false;
MySql:: host=localhost;port=3306;database=cyqdata;uid=root;pwd=123456;Convert Zero Datetime=True;
Mssql:: server=.;database=cyqdata;uid=sa;pwd=123456;provider=mssql;
Sybase:: data source=127.0.0.1;port=5000;database=cyqdata;uid=sa;pwd=123456;provider=sybase;
Postgre: server=localhost;uid=sa;pwd=123456;database=cyqdata;provider=pg;
DB2: Database=SAMPLE;User ID=administrator;Server=127.0.0.1;password=1234560;provider=db2;
FireBird user id=SYSDBA;password=123456;database=d:\\test.dbf;server type=Default;data source=127.0.0.1;port number=3050;provider=firebird;
Dameng:: user id=SYSDBA;password=123456789;data source=127.0.0.1;schema=test;provider=dameng;
KingBaseES server=127.0.0.1;User Id=system;Password=123456;Database=test;Port=54321;schema=public;provider=kingbasees;
Oracle ODP.NET::
Data Source=(DESCRIPTION=(ADDRESS_LIST=(ADDRESS=(PROTOCOL=TCP)(HOST=127.0.0.1)(PORT = 1521)))(CONNECT_DATA =(SID = orcl)));User ID=sa;password=123456

由于各种数据库链接语句基本一致,除了特定写法外,可以通过链接补充:provider=mssql、provider=mysql、provider=db2、provider=postgre等来区分。
###--------------------------------------------------------###

8、总结:

由于 Taurus.DTS 分布式任务发布组件的独立发布,原有发布的 Taurus.DTC 分布式事务组件,下一版本会移除掉其基础的任务发布订阅功能,保留事务的纯洁属性。

今天发布的 Taurus.DTS 组件,为 .Net 和  .Net Core 微服务系列又又贡献了一个新的组件。

后续会发布分布式锁的教程,这个已经在 CYQ.Data 里实现了,并且在也在 Taurus.DTC 和 Taurus.DTS 中使用到了。

StringBuilder 线程不安全,到底哪里不安全?

在Java中,字符串拼接是一个非常常见的操作,而对于频繁变动的字符串内容,使用
StringBuilder
是一个性能优化的选择。但是,
StringBuilder
在使用上存在一个很大的限制,它是线程不安全的。在多线程环境下,不正确的使用
StringBuilder
可能导致数据不一致、丢失或者程序异常。那么,
StringBuilder
到底哪里不安全?我们来一探究竟。

什么是线程安全?

在开始讨论之前,我们需要理解什么是线程安全。简单来说,当多个线程访问某个类的实例时,如果不需要额外的同步或者其他的协调操作,这个类始终能表现出正确的行为,那么我们就称这个类是线程安全的。

StringBuilder的线程不安全

StringBuilder

StringBuffer
的一个简化替换,但它去掉了线程同步的功能,因此在单线程中运行得更快。但是,这也意味着当多个线程同时修改一个
StringBuilder
实例中的数据时,就可能发生冲突,因为
StringBuilder
的内部实现没有进行任何形式的线程同步。

不安全的点一:内部状态的不一致

StringBuilder
维护着一个字符数组,而它的许多操作(如
append

insert

delete
等)都会改变这个数组的内容。如果多个线程并发地执行这些操作,那么就可能导致这个数组的状态在任何时间点都是不确定的。例如,当一个线程正在将一个字符序列追加到数组的同时,另一个线程可能正在修改这个数组的某个部分,这样就可能导致最终结果中出现意料之外的字符序列。

不安全的点二:竞态条件和数据竞争

如果两个线程同时尝试修改
StringBuilder
的同一部分,就会发生竞态条件(Race Condition),这可能导致数据竞争(Data Race),即两个线程读写共享数据并且至少有一个线程在写入。在这种情况下,最终的输出可能依赖于线程执行的精确时序,这是不可预知的。

不安全的点三:扩容的问题

StringBuilder
在执行追加操作时,如果内部的字符数组容量不足,它会进行自动扩容。如果多个线程同时触发了扩容操作,可能会导致某个线程的添加操作丢失,或者数组在扩容后的复制过程中出现数据错乱。

如何安全使用StringBuilder

尽管
StringBuilder
是线程不安全的,但我们仍然可以采取措施在多线程环境下安全地使用它:

  1. 局部变量
    :在方法内部使用局部变量的
    StringBuilder
    ,由于局部变量是线程隔离的,这样可以避免线程安全问题。
  2. 同步块
    :当必须共享一个
    StringBuilder
    实例时,可以通过同步块(synchronized blocks)来确保一次只有一个线程能执行修改操作。
  3. StringBuffer
    :如果不想手动管理同步,可以选择使用
    StringBuffer
    ,它是线程安全的,但可能会有额外的性能开销。

结论

StringBuilder
的线程不安全主要是由于其内部状态的改变未能适当同步。在多线程编程中,我们必须意识到这一点,并采用适当的措施来保证数据的一致性和完整性。正确地使用
StringBuilder
可以帮助我们避免潜在的并发问题,从而编写出更可靠、更健壮的Java应用程序。

JavaScript 提供了 automatic semicolon insertion (ASI)自动插入分号规则,在不加分号的情况下,会自动补充分号来分隔不同语句。

导致在继左大括号换行、tab 和 space 圣战后,前端又出现了一场战争。

并且随着那个男人加入这场讨论之后,关于是否应该加分号的讨论更是激烈了。

ASI 自动插入分号规则

在决定是否添加分号之前,我们先来了解一下编译器到底在哪些情况下会自动插入分号,哪些情况必须手动添加分号。

会自动添加分号的情况

1.遇到换行符,但是两句代码连接是无效代码

// 代码
42
'hello'

// `42 'hello'`连接在一起是无效语句,所以会自动在之间插入分号
42;'hello'

// 直接明确的写法
42;"hello"
// 代码
let a = 10, b = 5
a
-
b

// a - b 为有效代码,所以三者之间不会自动加分号
a = 1; b = 2;

// 直接明确的写法
a - b

2.遇到换行符,但是两句代码之间不允许有换行符

// 代码
foo
++
bar
++
baz

// foo 和 ++ 符合规则1,但是不符合 no LineTerminator here规则,所以会添加分号
foo; 
++bar; 
++baz;

在 JS 标准中,有个 no LineTerminator here 的规则,规定哪些语法不能加入换行符,如果开发者加了换行符,则 JS 编译器会无法识别并加入分号。

  1. 带标签的
    continue
    语句,不能
    continue
    后插入换行;
  2. 带标签的
    break
    语句,不能在
    break
    后面插入换行;
  3. return
    后面不能插入换行;
  4. 后自增、后自减运算符前不能插入换行;
  5. throw

    Exception
    之间不能插入换行;
  6. async
    关键字,后面不能插入换行;
  7. 箭头函数的箭头前,不能插入换行;
  8. yield
    之后,不能插入换行。

3.Restricted productions
如果这些标签后,空一行书写其它语句,则会自动在这些标签后添加分号:

  • ++
    or
    --
  • return
  • break
  • continue
  • ES6
    yield

    async
  • 反引号`
// return 后空一行再书写语句,则会自动在 return 后加分号
return
{
  a: 1
}

// 这是正确写法
return {
  a: 1
}

其它标签类似。

如果手动在这些标签后加上分号,同样也是错误的,比如:

// 空一行再写 a,会自动在 ++ 后添加分号
++
a

// 就算手动添加,和上面结果一样是错误的
++;
a;

所以针对 Restricted productions ,无论让编译器自动添加分号,还是自己手动加上分号,都是错误的,都应该去避免去换行,避免写这种写法。

必须手动加分号的情况

以下面这些标签开头的命令,必须在前面加分号,和前面一个语句分隔:

  • +

    -
    :语句以 + 或者 - 开头
  • /
    : 语句以正则表达式开头
  • (
    : 语句以自执行函数开头
  • [
    : 语句以数组开头

举例说明:

// 错误
a = b
+a
// 正确
a = b
;+a


// 错误
a = b
/something/.test(a)
// 正确
a = b
;/something/.test(a)

// 错误
a = b
(function () {})()
// 正确
a = b
;(function() {})()

// 错误
a = b
[1, 2, 3].forEach()
// 正确
a = b
;[1, 2, 3].forEach()

上面的情况,如果第二行代码不手动添加分号的话,两行代码会合并在一起导致结果错误或者报错。

上面几种情况中,只有自执行函数和数组开头会在极少情况下遇到,记住这两个前面要手动加上分号即可。

就算是习惯加分号的朋友,但仍然要注意下面的情况:

// 不需要结尾添加分号
if () {
} 

// 不需要结尾添加分号
for () {
} 

// 不需要结尾添加分号
while () {
} 

// 需要在结尾添加分号
var a = function () {
}; 

// 需要在结尾添加分号
var a = {
  prop: value
}; 

// 报错
[1, 2, 3].forEach();

即便习惯写分号的朋友,也很少有人在
if

for

while
等语句
}
后写分号,但是如果使用赋值的形式传递,则一定要注意在
}
把分号添加上,以避免后面语句出现自执行和数组开头的语句。

推荐遇到自执行和数组开头的,直接前面加上分号就完事了。

总结

是否添加和是否手动加是两回事,我们可以用 eslint、Prettier 等工具自动生成或者删除分号,是否手动加可以看个人喜好,最终代码内可以根据项目要求用工具生成。

但都要注意必须添加分号的几种情况。

参考文章