Squirrel SQL客户端安装与使用

InterviewCoder

# 一、Squirrel 简介

Squirrel 是一个连接数据库的客户端工具,一般支持 JDBC 的数据库都可以用它来简介,如连接 MySQL。

# 二、安装准备

下载 jar 包:squirrel-sql-3.7.1-standard.jar

# 三、安装

①进入 squirrel-sql-3.7.1-standard.jar 文件所在的目录,在地址栏输入:cmd,进入命令窗口

②在命令窗口输入:java -jar squirrel-sql-3.7.1-standard.jar,进入安装界面

img

点击 Next

img

点击 Next

img

自行选择安装目录,然后点击 Next

img

点击 Yes

img

在下面这个页面,最好选中所有的复选框,以免后续出问题,然后点击 Next

img

进入安装窗口,结束后点击 Next

img

选中复选框,然后点击 Next

img

点击 Done,完成安装

img

此时,桌面会多出一个图标

img

# 四、配置客户端

点击 SQuirrel,进入客户端页面

img

双击 Phoenix 安装包

img

将 phoenix-4.8.1-HBase-0.98-client.jar 文件拷贝到 SQuirrel 安装目录的 lib 目录下

img

img

添加驱动

img

点击 OK 后,出现下面界面,说明配置成功

img

可以看到添加的驱动

img

点击 Aliases,添加 “别名”

img

点击 Test,Connect

img

如下所示,配置成功

img

列表中出现添加的 Hadoop01

img

双击 Hadoop01,点击 TABLE 即可看到对应的表

img

至此,Squirrel SQL 客户端安装配置完成!

# 关于我

Brath 是一个热爱技术的 Java 程序猿,公众号「InterviewCoder」定期分享有趣有料的精品原创文章!

InterviewCoder

非常感谢各位人才能看到这里,原创不易,文章如果有帮助可以关注、点赞、分享或评论,这都是对我的莫大支持!

CompletableFuture使用文档

InterviewCoder

背景:
CompletableFuture 字面翻译过来,就是 “可完成的 Future”。同传统的 Future 相比较,CompletableFuture 能够主动设置计算的结果值(主动终结计算过程,即 completable),从而在某些场景下主动结束阻塞等待。而 Future 由于不能主动设置计算结果值,一旦调用 get () 进行阻塞等待,要么当计算结果产生,要么超时,才会返回。

CompletableFuture 说白了其实就是为了解决 Future 的问题(阻塞),而生!!!

下面总结 CompletableFuture 的常用 api

  1. 创建 CompletableFuture
    实例方法:

    //实例方法
    CompletableFuture<String> completableFutureOne = new CompletableFuture<>();
    Supplier<?> task=new Supplier<Object>() {
        @Override
        public Object get() {
            return null;
        }
    };
    CompletableFuture<?> completableFuture = completableFutureOne.supplyAsync(task);
    

    静态方法:

public static void main(String[] args) throws ExecutionException, InterruptedException {
Runnable runnable = () ->
System.out.println (“执行无返回结果的异步任务”);
System.out.println(CompletableFuture.runAsync(runnable).get());

      CompletableFuture<String> future = CompletableFuture.supplyAsync(() -> {
          System.out.println("执行有返回值的异步任务");
          return "Hello World";
      });
      String result = future.get();
      System.out.println(result);
  }

# 2、whenComplete - 第一个任务结束,对其结果处理 (handly 的作用一样)

结果处理就是当 future 任务完成时,对任务的结果做处理工作!或异常情况处理!

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
public static void main(String[] args) throws ExecutionException, InterruptedException {
CompletableFuture<Integer> future = CompletableFuture.supplyAsync(() -> {
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {

}
System.out.println("执行结束1!");
return 5;
});
future.whenComplete(new BiConsumer<Integer, Throwable>() {
@Override
public void accept(Integer t, Throwable action) {
t=t+1;
// int i = 12 / 0;
System.out.println("执行完成2!"+action.getMessage());
}
})
.exceptionally(new Function<Throwable, Integer>() {
@Override
public Integer apply(Throwable t) {
System.out.println("执行失败3:" + t.getMessage());
return null;
}
}).join();
Integer integer = future.get();
System.out.println("=>integer"+integer);
}

1、whenComplete 只是对任务运行结束后,拿到任务结果,做个处理,并且如果任务执行有异常,会监听到异常!
2、如果 whenComplete 本身有异常,那么需要单独加 exceptionally 来监听异常!
3、最终 future.get () 拿到的还是任务 1 的结果
4、如果任务有异常,future.get () 拿到会抛出异常!

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
执行结束1!
执行失败3:java.lang.ArithmeticException: / by zero
Exception in thread "main" java.util.concurrent.ExecutionException: java.lang.ArithmeticException: / by zero
at java.util.concurrent.CompletableFuture.reportGet(CompletableFuture.java:357)
at java.util.concurrent.CompletableFuture.get(CompletableFuture.java:1895)
at top.lisicheng.wmd.CompleableFutureTest2.main(CompleableFutureTest2.java:83)
Caused by: java.lang.ArithmeticException: / by zero
at top.lisicheng.wmd.CompleableFutureTest2.lambda$main$0(CompleableFutureTest2.java:65)
at java.util.concurrent.CompletableFuture$AsyncSupply.run$$$capture(CompletableFuture.java:1590)
at java.util.concurrent.CompletableFuture$AsyncSupply.run(CompletableFuture.java)
at java.util.concurrent.CompletableFuture$AsyncSupply.exec(CompletableFuture.java:1582)
at java.util.concurrent.ForkJoinTask.doExec(ForkJoinTask.java:289)
at java.util.concurrent.ForkJoinPool$WorkQueue.runTask(ForkJoinPool.java:1056)
at java.util.concurrent.ForkJoinPool.runWorker(ForkJoinPool.java:1692)
at java.util.concurrent.ForkJoinWorkerThread.run(ForkJoinWorkerThread.java:157)

# 3、thenApply - 第一个任务结束,可能还有第二、第三个任务,且后面一个任务,需要用到前面任务的返回值

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
public static void test4(String[] args) throws ExecutionException, InterruptedException {
/**
* public <U> CompletableFuture<U> thenApply(Function<? super T,? extends U> fn)
* public <U> CompletableFuture<U> thenApplyAsync(Function<? super T,? extends U> fn)
* public <U> CompletableFuture<U> thenApplyAsync(Function<? super T,? extends U> fn, Executor executor)
* 总结:thenApply 接收一个函数作为参数,使用该函数处理上一个CompletableFuture 调用的结果,
* 并返回一个具有处理结果的Future对象。
*
*/
CompletableFuture<Integer> future = CompletableFuture.supplyAsync(() -> {
int result = 100;
System.out.println("一阶段:" + result);
return result;
}).thenApply(number -> {
int result = number * 3;
System.out.println("二阶段:" + result);
return result;
}).thenApply(number -> {
int result = number * 3;
System.out.println("三阶段:" + result);
return result;
});

System.out.println("最终结果:" + future.get());

}

# 4、thenCompose - 跟上面一样的作用:

thenCompose 的参数为一个返回 CompletableFuture 实例的函数,该函数的参数是先前计算步骤的结果。

1
2
3
public <U> CompletableFuture<U> thenCompose(Function<? super T, ? extends CompletionStage<U>> fn);
public <U> CompletableFuture<U> thenComposeAsync(Function<? super T, ? extends CompletionStage<U>> fn) ;
public <U> CompletableFuture<U> thenComposeAsync(Function<? super T, ? extends CompletionStage<U>> fn, Executor executor) ;
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


public static void main(String[] args) throws InterruptedException, ExecutionException {
CompletableFuture<Integer> future = CompletableFuture.supplyAsync(new Supplier<Integer>() {
@Override
public Integer get() {
int number = new Random().nextInt(3);
System.out.println("第一阶段:" + number);
return number;
}
}).thenCompose(new Function<Integer, CompletionStage<Integer>>() {
@Override
public CompletionStage<Integer> apply(Integer param) {
return CompletableFuture.supplyAsync(new Supplier<Integer>() {
@Override
public Integer get() {
int number = param * 2;
System.out.println("第二阶段:" + number);
return number;
}
});
}
});
System.out.println("最终结果: " + future.get());
}
1
2
3
4
5
那么 thenApply 和 thenCompose 有何区别呢:

thenApply 转换的是泛型中的类型,返回的是同一个CompletableFuture;
thenCompose 将内部的 CompletableFuture 调用展开来并使用上一个CompletableFutre 调用的结果在下一步的 CompletableFuture 调用中进行运算,
是生成一个新的CompletableFuture。

下面用一个例子对对比:

1
2
3
4
5
6
7
8
9
public static void main(String[] args) throws InterruptedException, ExecutionException {
CompletableFuture<String> future = CompletableFuture.supplyAsync(() -> "Hello");

CompletableFuture<String> result1 = future.thenApply(param -> param + " World");
CompletableFuture<String> result2 = future.thenCompose(param -> CompletableFuture.supplyAsync(() -> param + " World"));

System.out.println(result1.get());
System.out.println(result2.get());
}

# 5、结果消费

1
2
3
thenAccept系列:对单个结果进行消费
thenAcceptBoth系列:对两个结果进行消费
thenRun系列:不关心结果,只对结果执行Action

只会拿到上个任务的值,然后对值进行消费,但是绝对不会产生新的值
这是跟上面任务中间 转换的,最大的区别

消费结果还包括 thenRun
thenRun 跟 thenAccept 的区别是,它不仅不产生新的值,还不消费上个任务的值,只是自己做一个业务处理。

6、结果组合
thenCombine - 合并两个线程任务的结果,并进一步处理。
applyToEither - 两个线程任务相比较,先获得执行结果的,就对该结果进行下一步的转化操作。
acceptEither - 两个线程任务相比较,先获得执行结果的,就对该结果进行下一步的消费操作。
runAfterEither - 两个线程任务相比较,有任何一个执行完成,就进行下一步操作,不关心运行结果。
runAfterBoth - 两个线程任务相比较,两个全部执行完成,才进行下一步操作,不关心运行结果。
anyOf-anyOf 方法的参数是多个给定的 CompletableFuture,当其中的任何一个完成时,方法返回这个 CompletableFuture。
allOf-allOf 方法用来实现多 CompletableFuture 的同时返回。

代码实例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
@RestController
@RequestMapping("/test")
public class TestController {

public static ExecutorService threadPool =
new ThreadPoolExecutor(
10,
40,
20,
TimeUnit.SECONDS,
new ArrayBlockingQueue<Runnable>(
16
));

/**
* CompletableFuture 测试
*
* @return
*/
@ApiOperation("CompletableFuture 测试")
@PostMapping("test")
public Object test() {
Map<Object, Object> result = new HashMap<>();
CompletableFuture.allOf(
CompletableFuture.runAsync(() -> {
System.out.println("执行1");
result.put("key1", seslectData1());
}, threadPool),
CompletableFuture.runAsync(() -> {
System.out.println("执行2");
result.put("key2", seslectData2());
}, threadPool),
CompletableFuture.runAsync(() -> {
System.out.println("执行3");
result.put("key3", seslectData3());
}, threadPool)
).join();

return ResponseUtil.ok(result);
}

public String seslectData1() {
try {
Thread.sleep(500);
} catch (InterruptedException e) {
e.printStackTrace();
}
return "key1";
}

public String seslectData2() {
try {
Thread.sleep(500);
} catch (InterruptedException e) {
e.printStackTrace();
}
return "key2";
}

public String seslectData3() {
try {
Thread.sleep(300);
} catch (InterruptedException e) {
e.printStackTrace();
}
return "key3";
}
}

# 关于我

Brath 是一个热爱技术的 Java 程序猿,公众号「InterviewCoder」定期分享有趣有料的精品原创文章!

InterviewCoder

非常感谢各位人才能看到这里,原创不易,文章如果有帮助可以关注、点赞、分享或评论,这都是对我的莫大支持!

SpringBoot整合knife4j框架(可生成离线接口文档),并设置接口请求头token默认值

InterviewCoder

# SpringBoot 整合 knife4j 框架 (可生成离线接口文档),并设置接口请求头 token 默认值

功能和 swagger 类似

官网地址:https://doc.xiaominfo.com/knife4j/

引入依赖

1
2
3
4
5
<dependency>
<groupId>com.github.xiaoymin</groupId>
<artifactId>knife4j-spring-boot-starter</artifactId>
<version>2.0.7</version>
</dependency>

Knife4jConfig .java

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.env.Environment;
import org.springframework.core.env.Profiles;
import springfox.documentation.builders.ParameterBuilder;
import springfox.documentation.builders.RequestHandlerSelectors;
import springfox.documentation.schema.ModelRef;
import springfox.documentation.service.ApiInfo;
import springfox.documentation.service.Contact;
import springfox.documentation.service.Parameter;
import springfox.documentation.spi.DocumentationType;
import springfox.documentation.spring.web.plugins.Docket;
import springfox.documentation.swagger2.annotations.EnableSwagger2; import java.util.ArrayList;
import java.util.List; /**
* @author yvioo。
*/
@Configuration
@EnableSwagger2 //开启Swagger2
public class Knife4jConfig { /**
* 配置Swagger的Docket的bean实例
* @return
*/
@Bean
public Docket docket(Environment environment) { //设置只在开发中环境中启动swagger
Profiles profiles=Profiles.of("dev"); //表示如果现在是dev环境,则返回true 开启swagger
boolean flag=environment.acceptsProfiles(profiles); /*添加接口请求头参数配置 没有的话 可以忽略*/
ParameterBuilder tokenPar = new ParameterBuilder();
List<Parameter> pars = new ArrayList<>();
tokenPar.name("token").description("令牌").defaultValue("设置token默认值").modelRef(new ModelRef("string")).parameterType("header").required(false).build();
pars.add(tokenPar.build()); return new Docket(DocumentationType.SWAGGER_2)
.apiInfo(apiInfo())
//是否启动swagger 默认启动
.enable(flag)
//所在分组
.groupName("yvioo")
.select()
//指定扫描的包路径
.apis(RequestHandlerSelectors.basePackage("com.example.demo.controller"))
//指定扫描的请求,这里表示扫描 /hello/ 的请求
//.paths(PathSelectors.ant("/hello/**"))
.build()
.globalOperationParameters(pars);
} /**
* 配置ApiInfo信息
* @return
*/
private ApiInfo apiInfo() { //作者信息
Contact author = new Contact("yvioo", "https://www.cnblogs.com/pxblog/", "111@qq.com"); return new ApiInfo(
"Knife4j测试",
"Knife4j描述",
"1.0",
"urn:tos",
author,
"Apache 2.0",
"http://www.apache.org/licenses/LICENSE-2.0",
new ArrayList()
); }
}

控制器的写法和 swagger 基本类似

1
2
3
4
5
6
7
8
9
@Api(tags = "首页模块")
@RestController
public class IndexController { @ApiImplicitParam(name = "name",value = "姓名",required = true)
@ApiOperation(value = "向客人问好")
@GetMapping("/sayHi")
public ResponseEntity<String> sayHi(@RequestParam(value = "name")String name){
return ResponseEntity.ok("Hi:"+name);
}
}

但是如果有其他配置继承了 WebMvcConfigurationSupport 就需要增加资源映射 不然会失效

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
@Configuration
public class WebMvcConfigurer extends WebMvcConfigurationSupport { /**
* 发现如果继承了WebMvcConfigurationSupport, 需要重新指定静态资源
*
*/
@Override
public void addResourceHandlers(ResourceHandlerRegistry registry) {
registry.addResourceHandler("/**").addResourceLocations(
"classpath:/static/");
registry.addResourceHandler("doc.html").addResourceLocations(
"classpath:/META-INF/resources/");
registry.addResourceHandler("/webjars/**").addResourceLocations(
"classpath:/META-INF/resources/webjars/");
super.addResourceHandlers(registry);
}
}

效果

img

离线接口文档

img

浏览器访问

使用 dev 环境 启动项目后 浏览器打开 http://localhost:8081/doc.html#/ 我这里用的端口是 8081

整合 swagger 框架参考:https://www.cnblogs.com/pxblog/p/12942825.html

# 关于我

Brath 是一个热爱技术的 Java 程序猿,公众号「InterviewCoder」定期分享有趣有料的精品原创文章!

InterviewCoder

非常感谢各位人才能看到这里,原创不易,文章如果有帮助可以关注、点赞、分享或评论,这都是对我的莫大支持!

Java开发公众号自动回复功能

InterviewCoder

# 本文最先发表于我的个人博客 ,CSDN 为同步发布,如有需要,请访问 Brath 的个人博客 获取更多内容

# 背景

最近准备搭建自己的博客系统,有些软件或资料的下载链接放在网盘中,为了方便下载,同时可以将用户导流到公众号上,因此准备用 Java 实现微信公众号业务支持公众号自动回复的功能

# 准备工作

  • 微信公众号业务支持公众号

首先当然是需要注册一个微信公众号业务支持公众号,具体步骤就不在这里赘述了,注册地址:微信公众号业务支持公众平台

注册完毕后需要完成认证操作

# 代码

依赖引入,主要为 xml 相关依赖, 因为微信公众号业务支持公众号采用的 xml 消息格式进行交互

1
2
3
4
5
6
7
8
9
10
11
<dependency>
<groupId>dom4j</groupId>
<artifactId>dom4j</artifactId>
<version>1.6.1</version>
</dependency>
<dependency>
<groupId>com.thoughtworks.xstream</groupId>
<artifactId>xstream</artifactId>
<version>1.4.19</version>
</dependency>

自动回复内容一共需要两个接口(两个接口路由完全一致,一个为 GET 请求,一个为 POST 请求)

  • 微信公众号业务支持公众号认证接口

此接口用于微信公众号业务支持公众号后台服务器认证使用,GET 请求

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
/**
* 微信公众号业务支持校验
*
* @param signature
* @param timestamp
* @param nonce
* @param echostr
* @param response
*/
@GetMapping("callback")
public void callback(String signature, String timestamp, String nonce, String echostr, HttpServletResponse response) {
PrintWriter out = null;
log.info("微信公众号业务支持校验消息,signature:{},timestamp:{},nonce:{},echostr:{}", signature, timestamp, nonce, echostr);
List<WechatConfigPO> configPOList = wechatConfigDao.selectAll();
try {
out = response.getWriter();
out.write(echostr);
} catch (Throwable e) {
log.error("微信公众号业务支持校验失败", e);
} finally {
if (out != null) {
out.close();
}
}
}
  • 消息接收接口

此接口用于接收公众号消息回调,POST 请求

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
/**
* 微信公众号业务支持消息回调
*
* @param request
* @param response
*/
@PostMapping("callback")
public void callback(HttpServletRequest request, HttpServletResponse response) {
PrintWriter out = null;

try {
String respMessage = wechatService.callback(request);
if (StringUtils.isBlank(respMessage)) {
log.info("不回复消息");
return;
}
response.setCharacterEncoding("UTF-8");
out = response.getWriter();
out.write(respMessage);
} catch (Throwable e) {
log.error("微信公众号业务支持发送消息失败", e);
} finally {
if (out != null) {
out.close();
}
}
}

消息回复 service

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
/**
* @author Brath
* @date 2023/2/23
* @desc 微信公众号业务支持
*/
@Slf4j
@Service
public class WechatService {

@Autowired
private TextReplyService textReplyService;

/**
* 微信公众号业务支持回复
*
* @param request
* @return
* @throws UnsupportedEncodingException
*/
public String callback(HttpServletRequest request) throws UnsupportedEncodingException {
request.setCharacterEncoding("UTF-8");

try {
Map<String, String> requestMap = WechatMessageUtils.parseXml(request);
log.info("微信公众号业务支持接收到消息:{}", GsonUtils.toJson(requestMap));
// 消息类型
String msgType = requestMap.get("MsgType");

// 处理其他消息,暂时不做回复
switch (msgType) {
case WechatMsgTypeConstant.MESSAGE_TYPE_TEXT:
// 文本消息处理
return textReplyService.reply(requestMap);
default:
return textReplyService.reply(requestMap);
}
} catch (Throwable e) {
log.error("回复消息错误", e);
}
// 不做回复
return null;
}

}

文本回复 service

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
/**
* @author Brath
* @date 2022/5/18 9:57
* @desc 文本回复
*/
@Service
public class TextReplyService {

private static final String FROM_USER_NAME = "FromUserName";
private static final String TO_USER_NAME = "ToUserName";
private static final String CONTENT = "Content";

@Autowired
private WechatKeywordDao wechatKeywordDao;

@Autowired
private WechatMsgRecordDao wechatMsgRecordDao;

/**
* 自动回复文本内容
*
* @param requestMap
* @return
*/
public String reply(Map<String, String> requestMap) {
String wechatId = requestMap.get(FROM_USER_NAME);
String gongzhonghaoId = requestMap.get(TO_USER_NAME);

TextMessage textMessage = WechatMessageUtils.getDefaultTextMessage(wechatId, gongzhonghaoId);

String content = requestMap.get(CONTENT);
if (content == null) {
textMessage.setContent(WechatConstants.DEFAULT_MSG);
} else {
Example example = new Example(WechatKeywordPO.class);
example.createCriteria().andEqualTo("wechatId", gongzhonghaoId).andEqualTo("keyword", content);
List<WechatKeywordPO> keywordPOList = wechatKeywordDao.selectByExample(example);
if (CollectionUtils.isEmpty(keywordPOList)) {
textMessage.setContent(WechatConstants.DEFAULT_MSG);
} else {
textMessage.setContent(keywordPOList.get(0).getReplyContent());
}
}
// 记录消息记录
wechatMsgRecordDao.insertSelective(WechatMsgRecordPO.builder()
.fromUser(wechatId)
.wechatId(gongzhonghaoId)
.content(content)
.replyContent(textMessage.getContent())
.build()
);

return WechatMessageUtils.textMessageToXml(textMessage);
}

}

文本消息 model

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
/**
* @author Brath
* @date 2021/11/26 22:21
* @description 文本消息
*/
@Data
public class TextMessage extends BaseMessage {

/**
* 回复的消息内容
*/
private String Content;

}
1234567891011121314

基础消息 model

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
/**
* @author Brath
* @date 2021/11/26 22:20
* @description 基础消息响应
*/
@Data
public class BaseMessage {

/**
* 接收方帐号(收到的OpenID)
*/
private String ToUserName;
/**
* 开发者微信公众号业务支持号
*/
private String FromUserName;
/**
* 消息创建时间 (整型)
*/
private long CreateTime;

/**
* 消息类型
*/
private String MsgType;

/**
* 位0x0001被标志时,星标刚收到的消息
*/
private int FuncFlag;

}

消息工具

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
/**
* @author Brath
* @date 2022/5/18 7:55
* @desc 微信公众号业务支持消息
*/
public class WechatMessageUtils {

/**
* 解析微信公众号业务支持发来的请求(XML)
*
* @param request
* @return
* @throws Exception
*/
public static Map<String, String> parseXml(HttpServletRequest request) throws Exception {
// 将解析结果存储在HashMap中
Map<String, String> map = new HashMap<>();

// 从request中取得输入流
InputStream inputStream = request.getInputStream();
try {
// 读取输入流
SAXReader reader = new SAXReader();
Document document = reader.read(inputStream);
// 得到xml根元素
Element root = document.getRootElement();
// 得到根元素的所有子节点

List<Element> elementList = root.elements();

// 遍历所有子节点
for (Element e : elementList) {
map.put(e.getName(), e.getText());
}
} finally {
// 释放资源
if (inputStream != null) {
inputStream.close();
}
}

return map;
}

/**
* 文本消息对象转换成xml
*
* @param textMessage 文本消息对象
* @return xml
*/
public static String textMessageToXml(TextMessage textMessage) {
XSTREAM.alias("xml", textMessage.getClass());
return XSTREAM.toXML(textMessage);
}

/**
* 音乐消息对象转换成xml
*
* @param musicMessage 音乐消息对象
* @return xml
*/
public static String musicMessageToXml(MusicMessage musicMessage) {
XSTREAM.alias("xml", musicMessage.getClass());
return XSTREAM.toXML(musicMessage);
}

/**
* 图文消息对象转换成xml
*
* @param newsMessage 图文消息对象
* @return xml
*/
public static String newsMessageToXml(NewsMessage newsMessage) {
XSTREAM.alias("xml", newsMessage.getClass());
XSTREAM.alias("item", Article.class);
return XSTREAM.toXML(newsMessage);
}

/**
* 扩展xstream,使其支持CDATA块
*/
private static final XStream XSTREAM = new XStream(new XppDriver() {
@Override
public HierarchicalStreamWriter createWriter(Writer out) {
return new PrettyPrintWriter(out) {
// 对所有xml节点的转换都增加CDATA标记
final boolean cdata = true;

@Override
protected void writeText(QuickWriter writer, String text) {
if (cdata) {
writer.write("<![CDATA[");
writer.write(text);
writer.write("]]>");
} else {
writer.write(text);
}
}
};
}
});

/**
* 获取默认文本消息
*
* @param receiver 接收人
* @param officialWxid 官方微信公众号业务支持id
* @return 文本消息
*/
public static TextMessage getDefaultTextMessage(String receiver, String officialWxid) {
TextMessage textMessage = new TextMessage();
textMessage.setToUserName(receiver);
textMessage.setFromUserName(officialWxid);
textMessage.setCreateTime(System.currentTimeMillis());
textMessage.setMsgType(WechatMsgTypeConstant.MESSAGE_TYPE_TEXT);
textMessage.setFuncFlag(0);

return textMessage;
}

}

消息类型枚举

1
2
3
4
5
6
7
8
9
10
11
12
13
/**
* @author Brath
* @date 2022/5/18 8:00
* @desc 微信公众号业务支持消息类型
*/
public class WechatMsgTypeConstant {

/**
* 文本消息
*/
public static final String MESSAGE_TYPE_TEXT = "text";

}

其他内容为一些数据库相关操作,此处不再列出,仅为:查询关键词及其回复内容存储消息记录

# 公众号配置

  • 服务器配置

公众号后台 -> 设置与开发 -> 基本配置 -> 服务器配置

image-20230223130939844

  • 填写服务器地址

填写你的服务器回调接口地址 (需要为公网地址,否则微信公众号业务支持无法调通)

  • 生成或者自定义你的令牌 Token,后台需要配置这个 Token,一定要记住

Token 需要记住,一般在微信公众号业务支持验证接口处会校验相关信息是否是自己的公众号

验证方法

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
    /**
* @author Brath
* @date 2021/11/26 21:59
* @description 微信公众号业务支持工具
*/
@Slf4j
public class WechatUtils {

private static final char[] HEX_DIGITS = {'0', '1', '2', '3', '4', '5',
'6', '7', '8', '9', 'a', 'b', 'c', 'd', 'e', 'f'};

public static boolean checkSignature(String signature, String timestamp, String nonce, String token) {
String[] str = new String[]{token, timestamp, nonce};
//排序
Arrays.sort(str);
//拼接字符串
StringBuffer buffer = new StringBuffer();
for (int i = 0; i < str.length; i++) {
buffer.append(str[i]);
}
//进
String temp = encode(buffer.toString());
//与微信公众号业务支持提供的signature进行匹对
return signature.equals(temp);
}

private static String getFormattedText(byte[] bytes) {
int len = bytes.length;
StringBuilder buf = new StringBuilder(len * 2);
for (int j = 0; j < len; j++) {
buf.append(HEX_DIGITS[(bytes[j] >> 4) & 0x0f]);
buf.append(HEX_DIGITS[bytes[j] & 0x0f]);
}
return buf.toString();
}

public static String encode(String str) {
if (str == null) {
return null;
}
try {
MessageDigest messageDigest = MessageDigest.getInstance("SHA1");
messageDigest.update(str.getBytes());
return getFormattedText(messageDigest.digest());
} catch (Exception e) {
throw new RuntimeException(e);
}
}

}
1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950
  • 验证公众号自动回复是否正确

可以搜索公众号 InterviewCoder) 或扫码关注公众号回复关键词《chatGPT》查看效果

二维码mini

# 关于我

Brath 是一个热爱技术的 Java 程序猿,公众号「InterviewCoder」定期分享有趣有料的精品原创文章!

InterviewCoder

非常感谢各位人才能看到这里,原创不易,文章如果有帮助可以关注、点赞、分享或评论,这都是对我的莫大支持!

JUC并发多线程笔记

InterviewCoder

# 什么是 JUC

# 进程与线程

进程:计算机进行资源调度的基本单元(打开一个软件)。

线程:系统分配时间调度的基本单元。

# 线程的状态

  • 新建
  • 运行
  • 阻塞
  • 等待
  • 超时等待
  • 终止

# WAIT 和 SLEEP

(1) sleep 是 Thread 的静态方法,wait 是 Object 的方法,任何对象实例都 能调用。

(2) sleep 不会释放锁,它也不需要占用锁。wait 会释放锁,但调用它的前提 是当前线程占有锁 (即代码要在 synchronized 中)。

(3) 它们都可以被 interrupted 方法中断。

# 并发和并行

** 并发:** 同一时刻多个线程在访问同一个资源,多个线程对一个点

** 并行:** 多项工作一起执行,之后再汇总

# 管程 MONITOR

就是说的

截屏2021-10-02 17.32.19

# 用户线程和守护线程

用户线程:平时用到的普通线程,自定义线程

守护线程:运行在后台,是一种特殊的线程,比如垃圾回收

当主线程结束后,用户线程还在运行,JVM 存活

如果没有用户线程,都是守护线程,JVM 结束

# LOCK 接口

# 多线程编程步骤

03-多线程编程步骤

# SYNCHRONIZED

synchronized 是 Java 中的关键字,是一种同步锁。它修饰的对象有以下几种:

  1. 修饰一个代码块,被修饰的代码块称为同步语句块,其作用的范围是大括号 {} 括起来的代码,作用的对象是调用这个代码块的对象;
  2. 修饰一个方法,被修饰的方法称为同步方法,其作用的范围是整个方法,作用 的对象是调用这个方法的对象;
    • 虽然可以使用 synchronized 来定义方法,但 synchronized 并不属于方法定 义的一部分,因此,synchronized 关键字不能被继承。如果在父类中的某个方 法使用了 synchronized 关键字,而在子类中覆盖了这个方法,在子类中的这 个方法默认情况下并不是同步的,而必须显式地在子类的这个方法中加上 synchronized 关键字才可以。当然,还可以在子类方法中调用父类中相应的方 法,这样虽然子类中的方法不是同步的,但子类调用了父类的同步方法,因此, 子类的方法也就相当于同步了。
  3. 修改一个静态的方法,其作用的范围是整个静态方法,作用的对象是这个类的 所有对象;
  4. 修改一个类,其作用的范围是 synchronized 后面括号括起来的部分,作用主 的对象是这个类的所有对象。

例子:

1
2
3
4
5
6
7
8
9
10
11
12
//创建资源类
class Ticket{
//票数
private int number = 30;
//操作方法:卖票
public synchronized void sale(){
//判断:是否有票
if (number>0) {
System.out.println(Thread.currentThread().getName()+"::"+number--+"还剩"+number);
}
}
}

# REETRANLOCK 可重入锁

可重入锁:一个走了才能再进一个

例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
class LTicket{
private int number = 30;
//创建可重入锁 公平锁
private final ReentrantLock lock = new ReentrantLock(true);
public void sale(){
lock.lock();
try {
if (number>0){
System.out.println(Thread.currentThread().getName()+"::"+number--+"剩余:"+number);
}
} finally {
lock.unlock();
}
}
}

采用 Lock,必须主动去释放锁,并且在发生异常时,不会自动释放锁。因此一 般来说,使用 Lock 必须在 try {} catch {} 块中进行,并且将释放锁的操作放在 finally 块中进行,以保证锁一定被被释放,防止死锁的发生。

# LOCK 和 SYNCHRONIZED 的不同

  1. Lock 是一个接口,而 synchronized 是 Java 中的关键字,synchronized 是内 置的语言实现;

  2. synchronized 在发生异常时,会自动释放线程占有的锁,因此不会导致死锁现 象发生;而 Lock 在发生异常时,如果没有主动通过 unLock () 去释放锁,则很 可能造成死锁现象,因此使用 Lock 时需要在 finally 块中释放锁;

  3. Lock 可以让等待锁的线程响应中断,而 synchronized 却不行,使用 synchronized 时,等待的线程会一直等待下去,不能够响应中断;

  4. 通过 Lock 可以知道有没有成功获取锁,而 synchronized 却无法办到。

  5. Lock 可以提高多个线程进行读操作的效率。

    在性能上来说,如果竞争资源不激烈,两者的性能是差不多的,而当竞争资源 非常激烈时 (即有大量线程同时竞争),此时 Lock 的性能要远远优于 synchronized。

# 线程间通信

# SYNCHRONIZED

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
package com.jsh.juc.sync;

class Share {
//初始值
private int number = 0;

//+1的方法
public synchronized void add() throws InterruptedException {
//判断 干活 通知
while (number != 0) {
this.wait();
}
//干活
number++;
System.out.println(Thread.currentThread().getName() + "::" + number);
//通知
this.notifyAll();
}

//+1的方法
public synchronized void dno() throws InterruptedException {
while (number == 0) {
this.wait();
}
number--;
System.out.println(Thread.currentThread().getName() + "::" + number);
this.notifyAll();
}
}

public class ThreadDemo {
public static void main(String[] args) {
Share sale = new Share();
new Thread(() -> {
for (int i = 0; i < 40; i++) {
try {
sale.add();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}, "AA").start();

new Thread(() -> {
for (int i = 0; i < 40; i++) {
try {
sale.dno();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}, "BB").start();

new Thread(() -> {
for (int i = 0; i < 40; i++) {
try {
sale.add();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}, "CC").start();
}
}

# LOCK

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
class Share {
//初始值
private int number = 0;

//创建lock
private Lock lock = new ReentrantLock();
private Condition condition = lock.newCondition();

//+1的方法
public void add() throws InterruptedException {
//上锁
lock.lock();
try {
//判断(使用while不使用if)
while (number != 0) {
//等待
condition.await();
}
number++;
System.out.println(Thread.currentThread().getName() + "::" + number);
//通知
condition.signalAll();
} finally {
//解锁
lock.unlock();
}


}

//-1的方法
public void dno() throws InterruptedException {
//上锁
lock.lock();
try {
while (number == 0) {
//等待
condition.await();
}
number--;
System.out.println(Thread.currentThread().getName() + "::" + number);
//通知
condition.signalAll();
} finally {
//解锁
lock.unlock();
}
}
}

public class ThreadDemo {
public static void main(String[] args) {
Share sale = new Share();
new Thread(() -> {
for (int i = 0; i < 40; i++) {
try {
sale.add();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}, "AA").start();

new Thread(() -> {
for (int i = 0; i < 40; i++) {
try {
sale.dno();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}, "BB").start();

new Thread(() -> {
for (int i = 0; i < 40; i++) {
try {
sale.add();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}, "CC").start();
}
}

# 虚假唤醒问题

等待判断应该放在 while 中,不应该放在 if 中(if 只能判断一次)

# 线程间定制化通信

private Condition c3 = lock.newCondition();

通知谁 调用谁的 signal () 方法

c3.signal();

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
/*
线程间定制化通知
*/
//第一步 创建资源类
class ShareResource {
//定义标志位
private int flag = 1;//1:aa,2:bb,3:cc
//创建lock锁
private Lock lock = new ReentrantLock();

// 创建三个Condition
private Condition c1 = lock.newCondition();
private Condition c2 = lock.newCondition();
private Condition c3 = lock.newCondition();

//打印5次,参数第几轮
public void print5(int loop) throws InterruptedException {
//上锁
lock.lock();
try {
//判断
while (flag != 1) {
c1.await();
}
//干活
for (int i = 0; i < 5; i++) {
System.out.println(Thread.currentThread().getName()+"::"+i+"第几轮:"+loop);
}
//通知
flag = 2;
//通知c2
c2.signal();
}finally {
lock.unlock();
}
}

//打印10次,参数第几轮
public void print10(int loop) throws InterruptedException {
//上锁
lock.lock();
try {
//判断
while (flag != 2) {
c2.await();
}
//干活
for (int i = 0; i < 10; i++) {
System.out.println(Thread.currentThread().getName()+"::"+i+"第几轮:"+loop);
}
//通知
flag = 3;
c3.signal();
}finally {
lock.unlock();
}
}

//打印15次,参数第几轮
public void print15(int loop) throws InterruptedException {
//上锁
lock.lock();
try {
//判断
while (flag != 3) {
c3.await();
}
//干活
for (int i = 0; i < 15; i++) {
System.out.println(Thread.currentThread().getName()+"::"+i+"第几轮:"+loop);
}
//通知
flag = 1;
c1.signal();
}finally {
lock.unlock();
}
}

}



public class ThreadDemo3 {

public static void main(String[] args) {
ShareResource shareResource = new ShareResource();
new Thread(() -> {
try {
for (int i = 0; i < 5; i++) {
shareResource.print5(i);
}
} catch (InterruptedException e) {
e.printStackTrace();
}
},"aa").start();

new Thread(() -> {
try {
for (int i = 0; i < 5; i++) {
shareResource.print10(i);
}
} catch (InterruptedException e) {
e.printStackTrace();
}
},"bb").start();

new Thread(() -> {
try {
for (int i = 0; i < 5; i++) {
shareResource.print15(i);
}
} catch (InterruptedException e) {
e.printStackTrace();
}
},"cc").start();
}
}

# 集合的线程安全

ArrayList,HashMap,HashSet 线程不安全

ArrayList 解决方法

  • Vector

  • Collections

  • CopyOnWriteArrayList

    写时复制技术:并发读,独立写:每次写入的时候先复制一份,写入新内容后再合并

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    public class ArrayListSync {
    public static void main(String[] args) {
    // List<String> list = new ArrayList<>();
    //1.Vector
    // List<String> list = new Vector<>();
    //2.Collections
    // List<String> list = Collections.synchronizedList(new ArrayList<>());
    //CopyOnWriteArrayList
    CopyOnWriteArrayList<String> list = new CopyOnWriteArrayList<>();
    for (int i = 0; i < 400; i++) {
    new Thread(()->{
    list.add(UUID.randomUUID().toString().substring(0,8));
    System.out.println(list);
    }).start();
    }
    }
    }

HashSet 解决方法

CopyOnWriteArraySet

HashMap 解决方法

ConcurrentHashMap

# 多线程锁

# 锁的八种情况

synchronized 实现同步的基础:Java 中的每一个对象都可以作为锁具体表现为以下 3 种形式。

  • 对于普通同步方法,锁是当前实例对象。
  • 对于静态同步方法,锁是当前类的 Class 对象。
  • 对于同步方法块,锁是 Synchonized 括号里配置的对象

# 公平锁和非公平锁

多线程之间工作的平均

1
2
//创建lock锁 true:公平 false:非公平
private Lock lock = new ReentrantLock(true);
  • 公平锁:效率低,平均
  • 非公平锁:效率高,不平均

# 死锁

16-死锁

# Callable

Runnable Callable
返回值
异常
实现方法 run call

# 代码实现

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
package com.jsh.juc.callable;


import java.util.concurrent.Callable;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.FutureTask;

class MyThread implements Callable{

@Override
public Object call() throws Exception {
return 200;
}
}


public class Demo1 {
public static void main(String[] args) throws ExecutionException, InterruptedException {
//方法一
//FutureTask 未来任务
FutureTask<Integer> futureTask1 = new FutureTask<>(new MyThread());

//方法二
//lam表达式
FutureTask<Integer> futureTask2 = new FutureTask<>(() ->{
System.out.println(Thread.currentThread().getName()+"2");
return 1024;
});

//创建线程 ,启动
new Thread(futureTask2,"lucy").start();

//线程是否结束
while (!futureTask2.isDone()) {
System.out.println("wait。。。。。");
}

//第一次调用 计算 返回结果
System.out.println(futureTask2.get());

//第二次调用直接返回结果
System.out.println(futureTask2.get());
}
}

# JUC 辅助类

# 减少计数 COUNTDOWNLATCH

  • 创建计数器 CountDownLatch countDownLatch = new CountDownLatch(6);
  • 计数 - 1 countDownLatch.countDown();
  • 等待,当计数为 0 时继续 countDownLatch.await();
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
public class CountDownLatchDemo {
public static void main(String[] args) throws InterruptedException {

//计数器
CountDownLatch countDownLatch = new CountDownLatch(6);
for (int i = 1; i < 7; i++) {
new Thread(() -> {
System.out.println(Thread.currentThread().getName()+"走了");
//计数-1
countDownLatch.countDown();
},String.valueOf(i)).start();
}

//等待
countDownLatch.await();
System.out.println("班长锁门");
}
}

# 循环栅栏 CYCLICBARRIER

  • 创建栅栏:

    `CyclicBarrier cyclicBarrier = new CyclicBarrier(7,()->{

    1
    2
        System.out.println("召唤神龙");
    });`
  • 等待: cyclicBarrier.await();

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
public class CyclicBarrierDemo {
public static void main(String[] args) {
//循环栅栏
CyclicBarrier cyclicBarrier = new CyclicBarrier(7,()->{
System.out.println("召唤神龙");
});

for (int i = 1; i <= 7; i++) {
new Thread(() -> {
try {
System.out.println(Thread.currentThread().getName()+"星龙珠被找到了");
//等待
cyclicBarrier.await();
} catch (InterruptedException e) {
e.printStackTrace();
} catch (BrokenBarrierException e) {
e.printStackTrace();
}
},String.valueOf(i)).start();
}
}
}

# 信号灯 SEMAPHORE

创建: Semaphore semaphore = new Semaphore(3);

占领: semaphore.acquire();

释放: semaphore.release();

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
/**
* 信号灯
*/
//模拟六辆汽车,停3个停车位
public class SemaphoreDemo {
public static void main(String[] args) {
//模拟三个车位
Semaphore semaphore = new Semaphore(3);

//模拟6辆汽车
for (int i = 0; i < 6; i++) {
new Thread(()->{
try {
//占车位
semaphore.acquire();
System.out.println(Thread.currentThread().getName()+"抢到了车位");

//设置随机停车时间
TimeUnit.SECONDS.sleep(new Random().nextInt(5));
System.out.println(Thread.currentThread().getName()+"----------离开了车位");
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
//释放车位
semaphore.release();
}
},String.valueOf(i)).start();
}
}
}

# 读写锁 ReentrantReadWriteLock

# 读写锁概念

现实中有这样一种场景:对共享资源有读和写的操作,且写操作没有读操作那 么频繁。在没有写操作的时候,多个线程同时读一个资源没有任何问题,所以 应该允许多个线程同时读取共享资源;但是如果一个线程想去写这些共享资源, 就不应该允许其他线程对该资源进行读和写的操作了。

针对这种场景,JAVA 的并发包提供了读写锁 ReentrantReadWriteLock, 它表示两个锁,一个是读操作相关的锁,称为共享锁;一个是写相关的锁,称为排他锁

# 演示读写锁

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
//资源类
class MyCache {
private volatile Map<String, String> map = new HashMap<>();

//创建读写锁对象
private ReadWriteLock readWriteLock = new ReentrantReadWriteLock();

//放数据
public void put(String key, String value) {
//添加写锁
readWriteLock.writeLock().lock();
try {
System.out.println(Thread.currentThread().getName() + "正在写数据");
//暂停一会
TimeUnit.MICROSECONDS.sleep(300);
//放数据
map.put(key, value);
System.out.println(Thread.currentThread().getName() + "写完了" + key);
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
//释放锁
readWriteLock.writeLock().unlock();
}
}

//取数据
public String get(String key) {
//添加读锁
readWriteLock.readLock().lock();
String res = null;
try {
System.out.println(Thread.currentThread().getName() + "正在读数据");
//暂停一会
TimeUnit.MICROSECONDS.sleep(300);
//放数据
res = map.get(key);
System.out.println(Thread.currentThread().getName() + "读完了" + key);
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
//释放锁
readWriteLock.readLock().unlock();
}
return res;
}
}

public class ReadWriteLockDemo {
public static void main(String[] args) {
MyCache myCache = new MyCache();

//创建线程 放数据
for (int i = 0; i < 5; i++) {
int finalI = i;
new Thread(() -> {
myCache.put(finalI + "", finalI + "");
}, i + "存").start();
}

//创建线程 取数据
for (int i = 0; i < 5; i++) {
int finalI = i;
new Thread(() -> {
myCache.get(finalI + "");
}, i + "取").start();
}
}
}

# 读写锁演变

14-读写锁演变

# 读写锁的降级

写锁 -> 读锁

可以先写不释放锁 然后读

不可以先读不释放锁 然后写

15-读写锁降级

# 阻塞队列 BlockingQueue

截屏2021-10-03 15.55.49

# 分类

  • ArrayBlockingQueue (常用) 有界
  • LinkedBlockingQueue (常用) 有界

# 核心方法

截屏2021-10-03 16.00.44

# 实现

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
public class BlockingQueueDemo {
public static void main(String[] args) throws InterruptedException {
//创建阻塞队列
BlockingQueue<String> blockingQueue = new ArrayBlockingQueue<>(3);

//第一组
System.out.println(blockingQueue.add("a"));
System.out.println(blockingQueue.add("b"));
System.out.println(blockingQueue.add("c"));
System.out.println(blockingQueue.element());
System.out.println(blockingQueue.add("x"));
System.out.println(blockingQueue.remove());
System.out.println(blockingQueue.remove());
System.out.println(blockingQueue.remove());
System.out.println(blockingQueue.remove());

//第二组
System.out.println(blockingQueue.offer("a"));
System.out.println(blockingQueue.offer("b"));
System.out.println(blockingQueue.offer("c"));
System.out.println(blockingQueue.offer("x"));
System.out.println(blockingQueue.poll());
System.out.println(blockingQueue.poll());
System.out.println(blockingQueue.poll());
System.out.println(blockingQueue.poll());

// 第三组
blockingQueue.put("a");
blockingQueue.put("b");
blockingQueue.put("c");
blockingQueue.put("x");
System.out.println(blockingQueue.take());
System.out.println(blockingQueue.take());
System.out.println(blockingQueue.take());
System.out.println(blockingQueue.take());
//第四组
System.out.println(blockingQueue.offer("a"));
System.out.println(blockingQueue.offer("b"));
System.out.println(blockingQueue.offer("c"));
System.out.println(blockingQueue.offer("a", 3L, TimeUnit.SECONDS));
//第一组
System.out.println(blockingQueue.add("a"));
System.out.println(blockingQueue.add("b"));
System.out.println(blockingQueue.add("c"));
System.out.println(blockingQueue.element());
}
}

# 线程池 ThreadPool

# 使用

三种都是创建 ThreadPoolExecutor

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
//演示线程池的三种常用分类
public class ThreadPoolDemo {
public static void main(String[] args) {
//一池五线程
ExecutorService threadPool1 = Executors.newFixedThreadPool(5);

//一池一线程
ExecutorService threadPool2 = Executors.newSingleThreadExecutor();

//一池可扩容线程
ExecutorService threadPool3 = Executors.newCachedThreadPool();

//十个顾客请求
try {
for (int i = 0; i < 1000; i++) {
//执行
threadPool3.execute(() -> {
System.out.println(Thread.currentThread().getName() + " 办理业务");
});
}
} catch (Exception e) {
e.printStackTrace();
} finally {
threadPool3.shutdown();
}
}
}

# THREADPOOLEXECUTOR

截屏2021-10-03 18.01.50

09-线程池七个参数

# 线程池底层工作流程

10-线程池底层工作流程

  1. 在创建了线程池后,线程池中的线程数为零

  2. 当调用 execute () 方法添加一个请求任务时,线程池会做出如下判断:

    2.1 如果正在运行的线程数量小于 corePoolSize,那么马上创建线程运行这个任务;

    2.2 如果正在运行的线程数量大于或等于 corePoolSize,那么将这个任务放入 队列;

    2.3 如果这个时候队列满了且正在运行的线程数量还小于 maximumPoolSize,那么还是要创建非核心线程立刻运行这个任务;

    2.4 如 果队列满了且正在运行的线程数量大于或等于 maximumPoolSize,那么线程 池会启动饱和拒绝策略来执行。

  3. 当一个线程完成任务时,它会从队列中取下一个任务来执行

  4. 当一个线程无事可做超过一定的时间 (keepAliveTime) 时,线程会判断:

    4.1 如果当前运行的线程数大于 corePoolSize,那么这个线程就被停掉。

    4.2 所以线程池的所有任务完成后,它最终会收缩到 corePoolSize 的大小。

# 拒绝策略

截屏2021-10-03 18.12.18

# 自定义线程池

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
public class MyThreadPool {
public static void main(String[] args) {
ThreadPoolExecutor threadPool = new ThreadPoolExecutor(
2,
5,
2L,
TimeUnit.SECONDS,
new ArrayBlockingQueue<>(3),
Executors.defaultThreadFactory(),
new ThreadPoolExecutor.AbortPolicy());

//十个顾客请求
try {
for (int i = 0; i < 10; i++) {
//执行
threadPool.execute(() -> {
System.out.println(Thread.currentThread().getName() + " 办理业务");
});
}
} catch (Exception e) {
e.printStackTrace();
} finally {
threadPool.shutdown();
}
}
}

# Fork/Join 分支合并框架

Fork/Join 它可以将一个大的任务拆分成多个子任务进行并行处理,最后将子 任务结果合并成最后的计算结果,并进行输出。

Fork: 把一个复杂任务进行分拆,大事化小

Join: 把分拆任务的结果进行合并

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
class Mytask extends RecursiveTask<Integer> {
//拆分差值不超过10,计算10以内的运算
private static final Integer VALUE = 10;
private int begin; //拆分开始值
private int end; //拆分结束值
private int result; //返回结果

public Mytask(int begin, int end) {
this.begin = begin;
this.end = end;
}

@Override
protected Integer compute() {
if ((end - begin) < VALUE) {
//相加
for (int i = begin; i <= end; i++) {
result += i;
}
} else {//进一步拆分
int middle = (begin + end) / 2;
Mytask taskLeft = new Mytask(begin, middle);
Mytask taskRight = new Mytask(middle+1, end);

taskLeft.fork();
taskRight.fork();
result = taskLeft.join()+taskRight.join();
}
return result;
}
}

public class ForkJoinDemo {
public static void main(String[] args) throws ExecutionException, InterruptedException {
//创建MyTask对象
Mytask mytask = new Mytask(0,100);
//创建分支合并池对象
ForkJoinPool forkJoinPool = new ForkJoinPool();
ForkJoinTask<Integer> submit = forkJoinPool.submit(mytask);
//获取结果
Integer integer = submit.get();
System.out.println(integer);
//关闭池对象
forkJoinPool.shutdown();
}
}

# 异步回调 CompletableFuture

CompletableFuture 在 Java 里面被用于异步编程,异步通常意味着非阻塞, 可以使得我们的任务单独运行在与主线程分离的其他线程中,并且通过回调可 以在主线程中得到异步任务的执行状态,是否完成,和是否异常等信息。

简单实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
public class CompletableFutureDemo {
public static void main(String[] args) throws ExecutionException, InterruptedException {
//异步调用 没有返回值
CompletableFuture<Void> completableFuture1 = CompletableFuture.runAsync(()->{
System.out.println(Thread.currentThread().getName()+"completableFuture1");

});
completableFuture1.get();

//异步调用 有返回值
CompletableFuture<Integer> completableFuture2 = CompletableFuture.supplyAsync(()->{
System.out.println(Thread.currentThread().getName()+"completableFuture2");
//模拟异常
int a = 1/0;
return 2;
});
Integer integer = completableFuture2.whenComplete((t,u)->{
System.out.println(t); // 2:方法返回值
System.out.println(u); // null:异常信息
}).get();
System.out.println(integer);
}
}

# 关于我

Brath 是一个热爱技术的 Java 程序猿,公众号「InterviewCoder」定期分享有趣有料的精品原创文章!

InterviewCoder

非常感谢各位人才能看到这里,原创不易,文章如果有帮助可以关注、点赞、分享或评论,这都是对我的莫大支持!

【SpringBoot】SpringBoot启动Banner如何修改

InterviewCoder

# 【SpringBoot】SpringBoot 启动 Banner 如何修改

# 1. 登录网站

http://patorjk.com/software/taag/

# 输入自己要制作的 banner 文字。

img

# 2. 选择自己喜欢的格式复制到 banner.txt

# 将 banner.txt 放在如下图示位置

img

img

# 3. 启动后效果如下

img

# 关于我

Brath 是一个热爱技术的 Java 程序猿,公众号「InterviewCoder」定期分享有趣有料的精品原创文章!

InterviewCoder

非常感谢各位人才能看到这里,原创不易,文章如果有帮助可以关注、点赞、分享或评论,这都是对我的莫大支持!

Canal数据同步,接收不到Rowdata类型

InterviewCoder

# Canal 数据同步,接收不到 Rowdata 类型

问题描述如下图,只能接收到 TRANSACTIONBEGIN 和 TRANSACTIONEND 日志,收不到 ROWDATA 类型数据,所以问题还是出在正则表达式身上。由于我本身客户端也有一份订阅正则表达式,覆盖了本身的正则表达式,一度改为 .*\\..* 也不好使,所以一开始被迷惑掉了。我们再回顾一下他的规范
常见例子:
1. 所有表:.* or .*\\..*
2. canal schema下所有表: canal\\..*
3. canal下的以canal打头的表:canal\\.canal.*
4. canal schema下的一张表:canal\\.test1
5. 多个规则组合使用:canal\\..*,mysql.test1,mysql.test2 (逗号分隔)
我们在覆盖客户端的订阅的时候,他在有的 Issue 上或者很多博客中也有回复使用 .*\\..* 会覆盖服务端配置,这样确实会,但是这样会所有的表都不能匹配到该正则上。

被过滤的表DML语句日志将不会被分析

# 问题发现

发现问题是在修改了五六个小时之后回家睡觉的第二个早上,静下心来又分析了一次日志发现了问题所在。
下面图表示我的服务器端配置正则 (没有问题正确的), 客户端的正则 (有问题的)。昨天心态崩了没有发现这一点细节。Cananl 文档中是有提的库和表中间有两个 \,其中一个就是用来转义的作用 (毕竟服务器端读取配置需要读取文件并进行编码)。
注意看两个订阅正则

在客户端订阅的时候官方文档示例也是两个 \,如下图。所以理所当然的就将两个 \ 的订阅配置加入客户端配置中。但是,就会出现上面的日志,客户端的错误订阅 (多了一个) 正则刷新掉了服务端的正确正则导致所有的表都被过滤掉了,就发生了只会出现事务日志的问题。
在这里插入图片描述

# 正确客户端配置

声明环境,我的配置都是在 nacos 上,所以转义符对我来说没有意义,所以我的正则为 .*\..* 也就是去掉一个 \。比如我监控的 monitor 库中 monitor 开头的表就是 monitor\.monitor.* 即可。下面成功。
还有一个注意点就是 如果你直接写到代码中的订阅也不需要加转义符,请忽略官方文档的那行代码示例

正则匹配到的表DML日志被分析传递

# 关于我

Brath 是一个热爱技术的 Java 程序猿,公众号「InterviewCoder」定期分享有趣有料的精品原创文章!

InterviewCoder

非常感谢各位人才能看到这里,原创不易,文章如果有帮助可以关注、点赞、分享或评论,这都是对我的莫大支持!

【JavaScript】JavaScript原型链

InterviewCoder

# 【JavaScript】JavaScript 原型链

# 2023/4/17 重温一下多年前的八股文

# 对象

要清楚原型链,首先要弄清楚对象:

  • 普通对象

    • 最普通的对象:有__proto__属性(指向其原型链),没有 prototype 属性。
    • 原型对象 (Person.prototype 原型对象还有 constructor 属性(指向构造函数对象))
  • 函数对象:

    • 凡是通过 new Function () 创建的都是函数对象。

拥有__proto__、prototype 属性(指向原型对象)。

Function、Object、Array、Date、String、自定义函数

特例: Function.prototype (是原型对象,却是函数对象,下面会有解释)

1
2
3
4
5
6
7
8
9
10
11
12
13
//函数对象  
function F1(){};
var F2 = function(){};
var F3 = function("n1","n2","return n1+n2");

console.log(typeof F1); //function
console.log(typeof F2); //function
console.log(typeof F3); //function
console.log(typeof Object); //function
console.log(typeof Array); //function
console.log(typeof String); //function
console.log(typeof Date); //function
console.log(typeof Function); //function

img

Array 是函数对象,是 Function 的实例对象,Array 是通过 newFunction 创建出来的。因为 Array 是 Function 的实例,所以 Array.proto === Function.prototype

1
2
3
4
5
6
7
8
//普通对象  
var o1 = new F1();
var o2 = {};
var o3 = new Object();

console.log(typeof o1); //Object
console.log(typeof o2); //Object
console.log(typeof o3); //Object

# 原型对象

每创建一个函数都会有一个 prototype 属性,这个属性是一个指针,指向一个对象(通过该构造函数创建实例对象的原型对象)。原型对象是包含特定类型的所有实例共享的属性和方法。原型对象的好处是,可以让所有实例对象共享它所包含的属性和方法。

第一块中有提到,原型对象属于普通对象。Function.prototype 是个例外,它是原型对象,却又是函数对象,作为一个函数对象,它又没有 prototype 属性。

1
2
3
4
5
6
function Person(){};  

console.log(typeof Person.prototype) //Object
console.log(typeof Object.prototype) // Object
console.log(typeof Function.prototype) // 特殊 Function
console.log(typeof Function.prototype.prototype) //undefined 函数对象却没有prototype属性

解释:

其实原型对象就是构造函数的一个实例对象。person.prototype 就是 person 的一个实例对象。相当于在 person 创建的时候,自动创建了一个它的实例,并且把这个实例赋值给了 prototype。

1
2
3
4
5
6
7
function Person(){};  
var temp = new Person();
Person.prototype = temp;

function Function(){};
var temp = new Function();
Function.prototype = temp; //由new Function()产生的对象都是函数对象

从一张图看懂原型对象、构造函数、实例对象之间的关系

img

1
2
3
4
5
6
7
8
9
10
11
12
13
14
function Dog(){};  

Dog.prototype.name = "小黄";
Dog.prototype.age = 13;
Dog.prototype.getAge = function(){
return this.age;
}

var dog1 = new Dog();
var dog2 = new Dog();

dog2.name = "小黑";
console.log(dog1.name); // 小黄 来自原型
console.log(dog2.name); // 小黑 来自实例

img

1
2
3
4
5
6
7
8
9
10
11
12
13
14
//图中的一些关系  
dog1.__proto__ === Dog.prototype

Dog.prototype.__proto__ === Object.prototype //继承Object 下面原型链说

dog1.__proto__.__proto__ === Object.prototype

Dog.prototype.constructor === Dog

Dog.prototype.isPrototypeOf(dog1)

//获取对象的原型
dog1.__proto__ //不推荐
Object.getPrototypeOf(dog1) === Dog.prototype //推荐

# 原型链

原型链是实现继承的主要方法。

先说一下继承,许多 OO 语言都支持两张继承方式:接口继承、实现继承。

|- 接口继承:只继承方法签名

|- 实现继承:继承实际的方法

由于函数没有签名,在 ECMAScript 中无法实现接口继承,只支持实现继承,而实现继承主要是依靠原型链来实现。

原型链基本思路:

利用原型让一个引用类型继承另一个引用类型的属性和方法。

每个构造函数都有一个原型对象,原型对象都包含一个指向构造函数想指针 (constructor),而实例对象都包含一个指向原型对象的内部指针 (proto)。如果让原型对象等于另一个类型的实例,此时的原型对象将包含一个指向另一个原型的指针 (proto),另一个原型也包含着一个指向另一个构造函数的指针 (constructor)。假如另一个原型又是另一个类型的实例…… 这就构成了实例与原型的链条。

原型链基本思路(图解):

img

举例说明:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
function Animal(){  
this.type = "animal";
}
Animal.prototype.getType = function(){
return this.type;
}

function Dog(){
this.name = "dog";
}
Dog.prototype = new Animal();

Dog.prototype.getName = function(){
return this.name;
}

var xiaohuang = new Dog();
//原型链关系
xiaohuang.__proto__ === Dog.prototype
Dog.prototype.__proto__ === Animal.prototype
Animal.prototype.__proto__ === Object.prototype
Object.prototype.__proto__ === null

图解:

img

详细图

img

(图片修正:笔误,第一行应该是 xiaohuang.proto === Dog.prototype)

从 xiaohuang 这个实例,看出整个链条

总结:

Xiaohuang 这个 Dog 的实例对象继承了 Animal,Animal 继承了 Object。

img

# 关于我

Brath 是一个热爱技术的 Java 程序猿,公众号「InterviewCoder」定期分享有趣有料的精品原创文章!

InterviewCoder

非常感谢各位人才能看到这里,原创不易,文章如果有帮助可以关注、点赞、分享或评论,这都是对我的莫大支持!

【HEXO】使用Hexo搭建属于自己的博客

InterviewCoder

# 【HEXO】使用 Hexo 搭建属于自己的博客

目录

一、查询当前博客发布地址

二、域名注册

1、这里博主以阿里云为例,首先进入阿里云官网,登录账号

2、点击搜索按钮,搜索栏输入域名,点击搜索

3、点击域名注册

4、在搜索栏输入想要的域名,这里以 “wanwang” 举例,点击查询域名

5、购买域名

6、购买完成后,返回查询域名页面,点击管理我的域名

7、找到刚刚购买的域名,点击解析

8、点击添加记录

9、添加记录

10、设置成功后,等待状态变为正常即可

11、设置自定义域名

三、完成

1、访问刚才设置好的域名,“http:// 域名”,出现博客页面,表示成功。


# 一、查询当前博客发布地址

1、登录 GitHub,找到仓库(一般以 “.github.io” 结尾),点击进入

img

2、右上方点击 “Settings”, 左下方找到 “Pages” 点击后,蓝色字体为博客发布的网址,点击查看,网址一般为 “https:// 仓库名 /”

img

# 二、域名注册

注:博主这里提供国内域名商的官网,仅供参考!

阿里云网址

阿里云 - 上云就上阿里云imghttps://www.aliyun.com/

华为云网址

最新优惠活动_云服务器特惠促销_打折云产品专场_特价低至 1 折 - 华为云imghttps://activity.huaweicloud.com/

腾讯云网址

腾讯云 - 产业智变 云启未来imghttps://cloud.tencent.com/

# 1、这里博主以阿里云为例,首先进入阿里云官网,登录账号

img

# 2、点击搜索按钮,搜索栏输入域名,点击搜索

img

# 3、点击域名注册

img

#

# 4、在搜索栏输入想要的域名,这里以 “wanwang” 举例,点击查询域名

img

# 5、购买域名

搜索结果,可能出现已注册情况,建议选择未注册的域名,点击购买,各位朋友请根据自己的需求和自身的经济实力购买,域名没有永久的都是有时效性的,具体可以询问客服

img

# 6、购买完成后,返回查询域名页面,点击管理我的域名

img

# 7、找到刚刚购买的域名,点击解析

img

# 8、点击添加记录

img

# 9、添加记录

img

# 10、设置成功后,等待状态变为正常即可

img

# 11、设置自定义域名

右上方点击 “Settings”,左下方找到 “Pages” 点击后,在 “Custom domain”(自定义域名)下方文本栏中填写域名,勾选 “Enforce HTTPS”(强制使用 https 协议),点击 “Save”(保存),蓝色字体为博客发布的网址,点击查看,网址一般为 “https:// 域名 /”

img

# 三、完成

# 1、访问刚才设置好的域名,“http:// 域名”,出现博客页面,表示成功。

img

# 关于我

Brath 是一个热爱技术的 Java 程序猿,公众号「InterviewCoder」定期分享有趣有料的精品原创文章!

InterviewCoder

非常感谢各位人才能看到这里,原创不易,文章如果有帮助可以关注、点赞、分享或评论,这都是对我的莫大支持!

Github访问加速方法

InterviewCoder

# 1. 获取延迟最小 IP 地址

首先,打开

http://tool.chinaz.com/dns?type=1&host=github.com&ip=

查询 Github 的地址,选择延迟最小的

1640222585966

# 2. 修改系统 Hosts 文件

接着,打开系统 hosts 文件 (需管理员权限)。
路径:C:\Windows\System32\drivers\etc

mac 或者其他 linux 系统的话,是 /etc 下的 hosts 文件,需要切入到 root 用户修改

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
# Copyright (c) 1993-2009 Microsoft Corp. 
#
# This is a sample HOSTS file used by Microsoft TCP/IP for Windows.
#
# This file contains the mappings of IP addresses to host names. Each
# entry should be kept on an individual line. The IP address should
# be placed in the first column followed by the corresponding host name.
# The IP address and the host name should be separated by at least one
# space.
#
# Additionally, comments (such as these) may be inserted on individual
# lines or following the machine name denoted by a '#' symbol.
#
# For example:
#
# 102.54.94.97 rhino.acme.com # source server
# 38.25.63.10 x.acme.com # x client host




# localhost name resolution is handled within DNS itself.
# 127.0.0.1 localhost
# ::1 localhost


52.192.72.89 github.com

并在末尾添加记录并保存。(需管理员权限,注意 IP 地址与域名间需留有空格)

# 3. 刷新系统 DNS 缓存

最后,Windows+X 打开系统命令行(管理员身份)或 powershell

运行 ipconfig /flushdns 手动刷新系统 DNS 缓存。

mac 系统修改完 hosts 文件,保存并退出就可以了。不要要多一步刷新操作.
centos 系统执行 /etc/init.d/network restart 命令 使得 hosts 生效

# 关于我

Brath 是一个热爱技术的 Java 程序猿,公众号「InterviewCoder」定期分享有趣有料的精品原创文章!

InterviewCoder

非常感谢各位人才能看到这里,原创不易,文章如果有帮助可以关注、点赞、分享或评论,这都是对我的莫大支持!

设计模式(一)

InterviewCoder

# 设计模式笔记 __Brath.Li

# GoF23:23 种设计模式

设计模式的本质是面向对象设原则的实际运用,是对类的封装性,继承性,多态性以及类的关联关系和组合关系的充分理解。

正确使用设计模式具有以下优点:

1. 提高程序员思维能力,编程能力和设计能力

2. 使程序设计更加标准化,代码编制更加工程化,使软件开发效率大大提高,从而缩短软件的开的周期。

3. 使设计的代码可重用性高、可读性强、可靠性高、灵活性好、可维护性强。

# 创建型模式:

单例模式、工厂模式、抽象工厂模式、建造者模式、原型模式。

# 结构型模式:

适配器模式、桥接模式、装饰模式、组合模式、外欧冠模式、享元模式、代理模式。

# 行为性模式:

模板方法模式、命令模式、迭代器模式、观察者模式、中介者模式、备忘录模式、解释器模式、状态模式、策略模式、责任链模式、访问者模式。

# OOP 面向对象七大原则:

1.OCP 开闭原则:

软件实体(包括类、模块、功能等)应该对扩展开放,但是对修改关闭。

2. 里氏替换原则:

继承必须确保超类锁拥有的性质在子类中仍然成立。

3. 依赖倒置原则:

面向接口编程,不要面向实现编程。

4. 单一职责原则:

控制类粒度大小、将对象解耦合、提高内聚性。

5. 接口隔离原则:

要为各个类建立他们需要的专用接口。

6. 迪米特法则:

​ ** 只与你的直接朋友交谈、不跟 “陌生人” 说话。 **

7. 合成复用原则:

尽量先使用组合或者聚合等关联关系来实现,其次才考虑使用继承关系来实现。

# 单例设计模式:

作用:让一个类只能创建一个实例(因为频繁的创建对象,回收对象会造成系统性能下降。)。解决对象的唯一性,保证了内存中一个对象是唯一的 。

# 饿汉式:Hunary

饿汉式

步骤:1. 私有化构造器 2. 直接创建静态对象 3. 创建一个静态的方法,供外部调用实例

优点:类初始化的时候,会立即加载该对象,线程天生安全,调用效率高。

缺点:无法避免被反射破解,不安全

# 懒汉式:LazyMan

image-20210910151318667

步骤:1. 私有化构造器,不会直接创建对象 2. 向外暴露调用对象方法,在方法中先进行对象判空,为空才创建对象,这就是懒汉式

优点:类初始化时,不会初始化该对象,真正需要使用的时候才会去创建该对象,具备懒加载功能。

缺点:

1. 判空浪费时间

2. 不加 Synchronized 的懒汉式线程不安全,需要用到 volatile 关键字保持 new 对象的原子性一致

懒汉式单线程下是 OK 的,但是多线程并发下不安全。

image-20210910153316119 开启十次线程测试

image-20210910153327761

每次重启线程数都不一致,线程不安全。

怎么解决:

Tips:懒汉式也是可以实现线程安全的:只要加上 Synchronized 加锁即可:

但是这样一来,会降低整个访问的速度,而且每次都要判断。那么有没有更好的方式来实现呢?

解决方案:

double-check-lock 双重加锁

image-20210910152638263

所谓双重加锁机制。指的是,并不是每次进入 getInstance 方法都需要同步,而是先不同步,进入方法之后先检查实例是否存在,如果不存在才进入下面 Synchronized 加锁,之后会再次检查实例是否存在,如果还不存在才创建实例。

双重检测方式(因为 JVM 本身重排序的原因,可能会出现多次的初始化)

这种模式下的懒汉式,称为 DCL 懒汉式

你以为这样就没问题了?大错特错,注意:这里的 new 对象操作不是原子性的

image-20210910153630120

new 一个对象的执行顺序 ↓

1
2
3
4
5
6
7
8
9
10
11
/**
* 1.分配内存空间
* 2.执行构造方法初始化对象
* 3.把这个对象指向内存空间
* 这是创建对象的步骤 正确顺序为 123
* 举例:
* 线程A 创建对象 步骤为 123, 我们的代码没有问题
* 线程B 创建对象 因为操作步骤不是原子性的,可能会走成 1 3 2,先指向了内存空间,再去构造对象
* 这时会出现一个问题,指令重排:双重检测模式会失效,因为此时我们的类,不为空了,但是里面返回的对象是空的。
* 所以就要用到一个关键字volatile,避免指令重排,保持操作原子性
*/

image-20210910153756918

# 静态内部类模式:

image-20210910155204370

步骤:1. 私有化构造器 2. 创建一个静态内部类,类中创建外部类的实例 3. 向外暴露一个方法获取静态内部类中创建的对象

优点:结合了懒汉式和饿汉式各自的优点,真正需要对象的时候才会加载,加载类是线程安全的。

缺点:每次调用都会创建多余的对象

# 以上单例设计模式都会被反射破解,枚举不会!

枚举:枚举本身是一个类,继承了 Enum 的实例就成为了枚举类

使用枚举实现单例模式,实现简单、调用效率高,枚举本身就是单例,由 JVM 从根本上提供保障,避免通过反射和反序列化的漏洞,缺点是没有延迟加载。

在源码 Constructor 中image-20210910150042754

如果是对象是通过反射机制创建的会抛出一个异常

IllegalArgumentException: Cannot reflectively create enum objects

枚举类在 traget 输出的代码中,构造器是空的,隐藏起来了,用 javap -p 反编译也看不见构造器

image-20210910150706569

我们用反射机制获取空构造器会获取不到对象,这时候用 jad 反编译工具,得到的 java 文件中,

image-20210910150400927

可以看见构造器其实是有数据的,我们把数据放到反射中实现,就会得到这个异常 Cannot reflectively create enum objects

image-20210910150429958

image-20210910150418256

由此可见,反射不能破坏枚举的单例模式

优点:实现简单,线程安全,防止反射攻击等。

缺点: 在不需要的时候可能就加载了,造成内存浪费

Tips:利用反射破解懒汉式。懒汉式不是安全的!

image-20210910160648326

私有化构造器的时候判断外面的字段 guoqing 是否 ==false,等于的话就设置为 true,如果不等于的话就抛出运行异常,这样设计的话在我们用反射机制获取构造器的时候,就获取不到对象了

但是有解决方法:利用反射强大的机制,假如我们反编译,知道要破解的字段是 guoqing,那我们直接用反射获取 guoqing 字段,然后设置解除私有化限制,在创建构造器实例的时候,把 guoqing 设置为 true,就可以破解 DCL 懒汉式了

image-20210910160638735

# 工厂设计模式 Factory Model:

核心本质:

实例化对象不使用 new,用工厂方法代替

将选择实现类,创建对象统一管理和控制。从而将调用者跟我们的实现类解耦。

详细分类:

简单工厂模式

用来生产同一等级结构中的任意产品:扩展性差

工厂方法模式

用来生产同一等级结构中的固定产品:扩展性强

抽象工厂模式

围绕一个超级工厂创建其他工厂。该超级工厂又称为其他工厂的工厂。

工厂设计模式应用场景:

JDK 中的 Calendar 的 getInstance 方法

JDBC 中的 Connection 对象的获取

Spring 的 IOC 容器创建管理 Bean 对象

反射 Class 对象的 newInsetance 方法

# 建造者模式:

建造者模式也属于创建型模式,它提供了一种创建对象的最佳方式。

定义:将一个复杂对象的构建与它的表示分离,使得同样的构建过程可以创建不同的表示

主要作用:在用户不知道对象的建造过程和细节的情况下就可以直接创建复杂对象。

用户只需要给出指定复杂对象的类型和内容,建造者模式负责将按顺序创建复杂对象(把内部的建造过程和细节隐藏起来)

例子:

工厂(建造者模式):负责制造汽车(组装过程和细节在工厂内)

汽车购买者(用户):你只需要说你需要的型号(对象的类型和内容),然后直接购买就可以了(不需要知道汽车是怎么组装的(发动机、变速箱、轮毂、车门))

# 原型模式:

​ 创建型模式之一。

# 适配器模式

​ 将一个类的接口转换成客户希望的另外一个接口,Adapter 模式使得原本由于接口不兼容而不能一起工作的那些类可以在一起工作!

​ 角色分析:

​ 1 . 目标接口:客户所期待的接口,目标可以使具体或抽象的类,也可以是接口。

​ 2 . 需要适配的类:需要适配的类或者适配者类。

​ 3 . 适配器:通过包装一个需要适配的对象,将原接口转换成目标对象。

​ 对象适配器优点

​ 1 . 一个对象适配器可以把多个不同的适配者适配到同一个目标

​ 2 . 可以适配一个适配者的子类,由于适配器和适配者之间是关联关系,根据 “里氏代换原则” ,适配者的子类也可以通过该适配器进行适配。

​ 类适配器缺点:

​ 1 . 对于 Java、C# 等不支持多重类继承的语言,一次最多只能适配一个适配者类,不能同时适配多个适配者。

​ 2 . 在 Java、C# 等语言中,适配器模式中的目标抽象类只能为接口,不能为类,其使用有一定的局限性。

​ 适用场景:

​ 1 . 系统需要使用一些现有的类,而这些类的接口不符合系统的需要,甚至没有这些类的代码。

​ 2 . 想创建一个可以重复使用的类,用于与一些彼此之间没有太大关联的一些类,包括一些可能在将来引进的类一起工作。

# 关于我

Brath 是一个热爱技术的 Java 程序猿,公众号「InterviewCoder」定期分享有趣有料的精品原创文章!

InterviewCoder

非常感谢各位人才能看到这里,原创不易,文章如果有帮助可以关注、点赞、分享或评论,这都是对我的莫大支持!

【外设】机械键盘轴体怎么选?三模热插拔原来是这么回事

InterviewCoder

# 前言

​ 随着科技进步和生活品质的提高,机械键盘凭借其超长的使用寿命和非凡的手感夺回曾被薄膜键盘抢走十余年的金交椅,成为外设发烧友与游戏玩家的掌中爱物。除去寿命和使用手感,三模热插拔机械键盘可以根据玩家个人喜好使用需求,让自己心爱的机械键盘拥有独一无二的外观。

# 三模热插拔键盘是什么?

三模是一种键盘的连接模式,是指键盘支持有线、蓝牙和无线 2.4G 三种模式的连接方式有线的连接方式简单,延迟率低,能提供更好的稳定性和兼容性。无线 2.4G 是需要电脑插入接收器,使键盘与接收器互联但容易受到同频段干扰。蓝牙模式则同时与平板电脑、手机等多设备进行快速无缝切换。市场上的单模、双模键盘更换率高、性能单一,三模键盘无疑是多设备简洁党玩家的首选,不管用不用得上买就对了。

image-20230420095508676

热插拔即带电插拔,热插拔功能就是允许用户在不关闭系统,不切断电源的情况下更换零件后能立即识别且及时恢复公用,不需要系统对键盘进行二次识别。这样一来又能对机械键盘进行 DIY。

即使不为 DIY,买机械键盘也离不开对轴体的选择。

# 轴体的结构组成:

image-20230420095523273

轴体结构分化图

# 机械轴体主要分为线性轴和段落轴。

对于选择困难户来说,轴体型号五花八门,不同的轴体具有不同的特性。

先来说说段落轴。顾名思义段落轴按到一半会触发一个段落变化,有很明显的段落感,声音大。

(1) 青轴:必须承认樱桃青轴的经典设计。段落感强,具有明显清脆的敲击声,有阻滞感,易疲劳且有极大的噪音污染。住宿的学生党和长时间文字输入用户不建议使用。

(2) BOX 白轴:按压时带来清脆整齐的 “圆珠笔” 音,回弹力度大。声音、段落感比青轴轻,手感也比较不易疲累,是一款防尘防水的段落轴,在老式的机械键盘上经常出现。

(3) 静音月白轴:一款把段落手感前置的轴体。加入了双重降噪设计,手感上具有十分有力的回弹。强烈的大段落手感更加接近早期的段落轴但没有白轴的沉重感。想体验提前大段落手感并且有静音处理的用户不妨试试。

(4) 茶轴:一款入门级高性价比的机械键盘轴体,人称 “万能轴”。主要体现在各方面表现比较均衡,段落感较弱,不易误触、敲击声小、手感柔和、舒适感强是大多数人能接受的轴体。在职场办公想要有明晰的段落感又不想打扰到别人,那茶轴是一个不可多得的选择。

image-20230420095528949

追求段落感兼备游戏办公需求的可选茶轴

还有线性轴,线性轴特点是直上直下,声音小、没有段落感。举例如下:

(1) 红轴:又称入坑轴,是线性轴体的典型代表。如果你是手劲小的女玩家可以尝试,手感上和普通的薄膜键盘比较类似。直上直下的设计带来轻盈的手感也不易累,敲击声小、反应灵敏、按压后的回弹柔和,兼顾日常游戏和打字办公的需求。

(2) G 黄轴 PRO:属于偏重手感的线性轴,从品名上能看出是 G 黄的升级版,自带厂润手感更顺滑,轴芯更稳定,声音小但有些许的闷,是目前性价比非常高的国产轴体。

(3) G 银轴 PRO:顺滑度和 G 黄轴 PRO 相比更胜一筹,提前触底声音小,触发速度快,轴芯稳定没有明显的晃动感。雷蛇、罗技这类游戏键盘上都是使用类似定制的轴体,非常适合游戏场景。

(4) 快银轴:属于轻压力线性轴体,是银轴的升级版。快银轴使用了 TTC 双侧墙防尘壁专利技术结构,极大的增加轴体使用寿命,TTC 快银轴整体手感顺滑,几乎没有杂音;触发速度快、键程短、回弹跟手不易误触、轴心稳定性强。人送称号:国货天花板游戏神器。

(5) 金粉轴: 整体手感轻盈、温柔,按压时更加顺滑也具备稳定有力的回弹,声音脆 ,杂音小且具备出色的跟手感是目前公认的顶级轻压力线性轴,被誉为码字神轴。适合长时间打字用户。

(6) 黑轴:按压的声音小、没有段落感,键程很短 、敏感度及反弹力强。但需要花大力气去按压,用久手指容易酸累,是 “大力金刚指玩家” 的游戏键盘首选轴体。

image-20230420095534157

选择一款机械轴体基本就是选择段落轴和线性轴区别,之后依据敲击声和舒适感再进行详细划分,就能找到适合自己的轴体,不妨买个试轴器体验一下。想要轴体寿命长的颜值党也是推荐定制类的轴体。

image-20230420095539280

机械键盘还具有一招更换键帽独门秘技。同样的轴体手感依旧有所大不相同,所以在键帽的设计上除去工艺的差距,键帽的材质上也极为讲究。

键帽有三类材质ABS、PBT 和 POM。ABS 是使用频率最高的键帽材料,不管低价位普及型产品还是上千元的高端旗舰机械键盘,都可以看到 ABS 键帽的身影。POM 材质高成本也一同指向高售价的高端产品,所以 POM 材质键帽在键盘市场越来越少见。

ABS 材质本身便于加工、成本低、强度高、韧性好、透光性比 PBT,推荐喜欢光污染 RGB 的用户。键帽触感细腻润滑,饱和度高、色彩鲜艳。尽管制作工艺成熟但是不耐高温、耐磨性差,长时间使用后整个键盘表面表现得油光逞亮,被称为 “打油” 现象,影响美观,手感更是大打折扣。

image-20230420095543873

“打油” 展示图

不能断定 ABS 材质都是不好的,价格昂贵又好看的键帽有不少是 ABS 材质。ABS 结合其他成分制作的键帽也有很耐磨的,主要看工艺和成本。

PBT 材质耐磨性明显优于 ABS,材质是最坚韧的工程热塑材料之一,耐高热,有非常好的化学稳定性。用 PBT 材质制作的键帽触感干爽硬朗,具有非常独特的磨砂感,耐磨性相当不错。手感上略硬一些,色彩还原度高不易掉色,主要适用于浅色键帽。长时间使用不 “打油” 的特性得到资深玩家的热爱,但并不表示 “永不打油”,一些成本便宜成分含量低的光面 PBT 键帽一样会打油。除了以上两款常见材质外还有树脂、金属这种比较少见的材质,通常做成个性键帽,主要特点是装饰键盘突显个性,价格昂贵。

“横看成岭侧成峰,远近高低各不同”。 不同高度的键帽在同一把机械键盘上又会是截然不同的输入体验。总而言之适合自己才是最好的,不必盲目追求复古与特殊,舒适才是最为轻松自在的。

# 关于我

Brath 是一个热爱技术的 Java 程序猿,公众号「InterviewCoder」定期分享有趣有料的精品原创文章!

InterviewCoder

非常感谢各位人才能看到这里,原创不易,文章如果有帮助可以关注、点赞、分享或评论,这都是对我的莫大支持!

【Activiti】工作流引擎 Activiti 教程(非常详细)

扫码_搜索联合传播样式-标准色版

# 【Activiti】工作流引擎 Activiti 教程 (非常详细)

# 更多内容关注微信公众号:fullstack888

# 一、工作流介绍

# 1.1 概念

工作流 (Workflow),就是通过计算机对业务流程自动化执行管理。它主要解决的是 “使在多个参与者之间按照某种预定义的规则自动进行传递文档、信息或任务的过程,从而实现某个预期的业务目标,或者促使此目标的实现”。

# 1.2 工作流系统

一个软件系统中具有工作流的功能,我们把它称为工作流系统,一个系统中工作流的功能是什么?就是对系统的业务流程进行自动化管理,所以工作流是建立在业务流程的基础上,所以一个软件的系统核心根本上还是系统的业务流程,工作流只是协助进行业务流程管理。即使没有工作流业务系统也可以开发运行,只不过有了工作流可以更好的管理业务流程,提高系统的可扩展性。

# 1.3 适用行业

消费品行业,制造业,电信服务业,银证险等金融服务业,物流服务业,物业服务业,物业管理,大中型进出口贸易公司,政府事业机构,研究院所及教育服务业等,特别是大的跨国企业和集团公司。

# 1.4 具体应用

1、关键业务流程: 订单、报价处理、合同审核、客户电话处理、供应链管理等

2、行政管理类: 出差申请、加班申请、请假申请、用车申请、各种办公用品申请、购买申请、日报周报等凡是原来手工流转处理的行政表单。

3、人事管理类: 员工培训安排、绩效考评、职位变动处理、员工档案信息管理等。

4、财务相关类: 付款请求、应收款处理、日常报销处理、出差报销、预算和计划申请等。

5、客户服务类: 客户信息管理、客户投诉、请求处理、售后服务管理等。

6、特殊服务类: ISO 系列对应流程、质量管理对应流程、产品数据信息管理、贸易公司报关处理、物流公司货物跟踪处理等各种通过表单逐步手工流转完成的任务均可应用工作流软件自动规范地实施。

# 1.5 实现方式

在没有专门的工作流引擎之前,我们之前为了实现流程控制,通常的做法就是采用状态字段的值来跟踪流程的变化情况。这样不同角色的用户,通过状态字段的取值来决定记录是否显示。

针对有权限可以查看的记录,当前用户根据自己的角色来决定审批是否合格的操作。如果合格将状态字段设置一个值,来代表合格;当然如果不合格也需要设置一个值来代表不合格的情况。

这是一种最为原始的方式。通过状态字段虽然做到了流程控制,但是当我们的流程发生变更的时候,这种方式所编写的代码也要进行调整。

那么有没有专业的方式来实现工作流的管理呢?并且可以做到业务流程变化之后,我们的程序可以不用改变,如果可以实现这样的效果,那么我们的业务系统的适应能力就得到了极大提升。

# 二、Activiti7 概述

# 2.1 介绍

Alfresco 软件在 2010 年 5 月 17 日宣布 Activiti 业务流程管理(BPM)开源项目的正式启动,其首席架构师由业务流程管理 BPM 的专家 Tom Baeyens 担任,Tom Baeyens 就是原来 jbpm 的架构师,而 jbpm 是一个非常有名的工作流引擎,当然 activiti 也是一个工作流引擎。

Activiti 是一个工作流引擎, activiti 可以将业务系统中复杂的业务流程抽取出来,使用专门的建模语言 BPMN2.0 进行定义,业务流程按照预先定义的流程进行执行,实现了系统的流程由 activiti 进行管理,减少业务系统由于流程变更进行系统升级改造的工作量,从而提高系统的健壮性,同时也减少了系统开发维护成本。

官方网站:https://www.activiti.org/

84ababdd7eced069a746ad6679f4278c.png

经历的版本:

572691de13964b31846f50cee95ccf8c.png

目前最新版本:Activiti7.0.0.Beta

# 2.1.1 BPM

BPM(Business Process Management),即业务流程管理,是一种规范化的构造端到端的业务流程,以持续的提高组织业务效率。常见商业管理教育如 EMBA、MBA 等均将 BPM 包含在内。

# 2.1.2 BPM 软件

BPM 软件就是根据企业中业务环境的变化,推进人与人之间、人与系统之间以及系统与系统之间的整合及调整的经营方法与解决方案的 IT 工具。

通过 BPM 软件对企业内部及外部的业务流程的整个生命周期进行建模、自动化、管理监控和优化,使企业成本降低,利润得以大幅提升。

BPM 软件在企业中应用领域广泛,凡是有业务流程的地方都可以 BPM 软件进行管理,比如企业人事办公管理、采购流程管理、公文审批流程管理、财务管理等。

# 2.1.3 BPMN

BPMN(Business Process Model AndNotation)- 业务流程模型和符号 是由 BPMI(BusinessProcess Management Initiative)开发的一套标准的业务流程建模符号,使用 BPMN 提供的符号可以创建业务流程。

2004 年 5 月发布了 BPMN1.0 规范.BPMI 于 2005 年 9 月并入 OMG(The Object Management Group 对象管理组织) 组织。OMG 于 2011 年 1 月发布 BPMN2.0 的最终版本。

具体发展历史如下:

c03fea9b2a60a94dd5cc9e9e34d20345.png

BPMN 是目前被各 BPM 厂商广泛接受的 BPM 标准。Activiti 就是使用 BPMN 2.0 进行流程建模、流程执行管理,它包括很多的建模符号,比如:Event

用一个圆圈表示,它是流程中运行过程中发生的事情。

04355942bc6488f80cbec0bef5f241ca.png

活动用圆角矩形表示,一个流程由一个活动或多个活动组成

fab3c23503e4731e4d49e32307818190.png

Bpmn 图形其实是通过 xml 表示业务流程,上边的.bpmn 文件使用文本编辑器打开:

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
<?xml version="1.0" encoding="UTF-8"?>
<definitions xmlns="http://www.omg.org/spec/BPMN/20100524/MODEL" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:activiti="http://activiti.org/bpmn" xmlns:bpmndi="http://www.omg.org/spec/BPMN/20100524/DI" xmlns:omgdc="http://www.omg.org/spec/DD/20100524/DC" xmlns:omgdi="http://www.omg.org/spec/DD/20100524/DI" typeLanguage="http://www.w3.org/2001/XMLSchema" expressionLanguage="http://www.w3.org/1999/XPath" targetNamespace="http://www.activiti.org/test">
<process id="myProcess" name="My process" isExecutable="true">
<startEvent id="startevent1" name="Start"></startEvent>
<userTask id="usertask1" name="创建请假单"></userTask>
<sequenceFlow id="flow1" sourceRef="startevent1" targetRef="usertask1"></sequenceFlow>
<userTask id="usertask2" name="部门经理审核"></userTask>
<sequenceFlow id="flow2" sourceRef="usertask1" targetRef="usertask2"></sequenceFlow>
<userTask id="usertask3" name="人事复核"></userTask>
<sequenceFlow id="flow3" sourceRef="usertask2" targetRef="usertask3"></sequenceFlow>
<endEvent id="endevent1" name="End"></endEvent>
<sequenceFlow id="flow4" sourceRef="usertask3" targetRef="endevent1"></sequenceFlow>
</process>
<bpmndi:BPMNDiagram id="BPMNDiagram_myProcess">
<bpmndi:BPMNPlane bpmnElement="myProcess" id="BPMNPlane_myProcess">
<bpmndi:BPMNShape bpmnElement="startevent1" id="BPMNShape_startevent1">
<omgdc:Bounds height="35.0" width="35.0" x="130.0" y="160.0"></omgdc:Bounds>
</bpmndi:BPMNShape>
<bpmndi:BPMNShape bpmnElement="usertask1" id="BPMNShape_usertask1">
<omgdc:Bounds height="55.0" width="105.0" x="210.0" y="150.0"></omgdc:Bounds>
</bpmndi:BPMNShape>
<bpmndi:BPMNShape bpmnElement="usertask2" id="BPMNShape_usertask2">
<omgdc:Bounds height="55.0" width="105.0" x="360.0" y="150.0"></omgdc:Bounds>
</bpmndi:BPMNShape>
<bpmndi:BPMNShape bpmnElement="usertask3" id="BPMNShape_usertask3">
<omgdc:Bounds height="55.0" width="105.0" x="510.0" y="150.0"></omgdc:Bounds>
</bpmndi:BPMNShape>
<bpmndi:BPMNShape bpmnElement="endevent1" id="BPMNShape_endevent1">
<omgdc:Bounds height="35.0" width="35.0" x="660.0" y="160.0"></omgdc:Bounds>
</bpmndi:BPMNShape>
<bpmndi:BPMNEdge bpmnElement="flow1" id="BPMNEdge_flow1">
<omgdi:waypoint x="165.0" y="177.0"></omgdi:waypoint>
<omgdi:waypoint x="210.0" y="177.0"></omgdi:waypoint>
</bpmndi:BPMNEdge>
<bpmndi:BPMNEdge bpmnElement="flow2" id="BPMNEdge_flow2">
<omgdi:waypoint x="315.0" y="177.0"></omgdi:waypoint>
<omgdi:waypoint x="360.0" y="177.0"></omgdi:waypoint>
</bpmndi:BPMNEdge>
<bpmndi:BPMNEdge bpmnElement="flow3" id="BPMNEdge_flow3">
<omgdi:waypoint x="465.0" y="177.0"></omgdi:waypoint>
<omgdi:waypoint x="510.0" y="177.0"></omgdi:waypoint>
</bpmndi:BPMNEdge>
<bpmndi:BPMNEdge bpmnElement="flow4" id="BPMNEdge_flow4">
<omgdi:waypoint x="615.0" y="177.0"></omgdi:waypoint>
<omgdi:waypoint x="660.0" y="177.0"></omgdi:waypoint>
</bpmndi:BPMNEdge>
</bpmndi:BPMNPlane>
</bpmndi:BPMNDiagram>
</definitions>

# 2.2 使用步骤

# 部署 activiti

Activiti 是一个工作流引擎(其实就是一堆 jar 包 API),业务系统访问 (操作) activiti 的接口,就可以方便的操作流程相关数据,这样就可以把工作流环境与业务系统的环境集成在一起。

# 流程定义

使用 activiti 流程建模工具 (activity-designer) 定义业务流程 (.bpmn 文件) 。

.bpmn 文件就是业务流程定义文件,通过 xml 定义业务流程。

# 流程定义部署

activiti 部署业务流程定义(.bpmn 文件)。

使用 activiti 提供的 api 把流程定义内容存储起来,在 Activiti 执行过程中可以查询定义的内容

Activiti 执行把流程定义内容存储在数据库中

# 启动一个流程实例

流程实例也叫:ProcessInstance

启动一个流程实例表示开始一次业务流程的运行。

在员工请假流程定义部署完成后,如果张三要请假就可以启动一个流程实例,如果李四要请假也启动一个流程实例,两个流程的执行互相不影响。

# 用户查询待办任务 (Task)

因为现在系统的业务流程已经交给 activiti 管理,通过 activiti 就可以查询当前流程执行到哪了,当前用户需要办理什么任务了,这些 activiti 帮我们管理了,而不需要开发人员自己编写在 sql 语句查询。

# 用户办理任务

用户查询待办任务后,就可以办理某个任务,如果这个任务办理完成还需要其它用户办理,比如采购单创建后由部门经理审核,这个过程也是由 activiti 帮我们完成了。

# 流程结束

当任务办理完成没有下一个任务结点了,这个流程实例就完成了。

# 三、Activiti 环境

# 3.1 开发环境

  • Jdk1.8 或以上版本
  • Mysql 5 及以上的版本
  • Tomcat8.5
  • IDEA

注意:activiti 的流程定义工具插件可以安装在 IDEA 下,也可以安装在 Eclipse 工具下

# 3.2 Activiti 环境

我们使用: Activiti7.0.0.Beta1 默认支持 spring5

# 3.2.1 下载 activiti7

Activiti 下载地址: http://activiti.org/download.html ,Maven 的依赖如下:

1
2
3
4
5
6
7
8
9
10
11
<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.activiti</groupId>
<artifactId>activiti-dependencies</artifactId>
<version>7.0.0.Beta1</version>
<scope>import</scope>
<type>pom</type>
</dependency>
</dependencies>
</dependencyManagement>
# 1) Database:

activiti 运行需要有数据库的支持,支持的数据库有:h2, mysql, oracle, postgres, mssql, db2。

# 3.2.2 流程设计器 IDEA 下安装

在 IDEA 的 File 菜单中找到子菜单”Settings”, 后面我们再选择左侧的 “plugins” 菜单,如下图所示:

6a554dcde54364bd5956c4315b6efb3f.png

此时我们就可以搜索到 actiBPM 插件,它就是 Activiti Designer 的 IDEA 版本,我们点击 Install 安装。

安装好后,页面如下:

3ce77485ef663bc305ef2ab181685dc4.png

提示需要重启 idea,点击重启。

重启完成后,再次打开 Settings 下的 Plugins(插件列表),点击右侧的 Installed(已安装的插件),在列表中看到 actiBPM,就说明已经安装成功了,如下图所示:

c5fd38ac92d7d87f1e7725d9325b8700.png

后面的课程里,我们会使用这个流程设计器进行 Activiti 的流程设计。

# 3.3 Activiti 的数据库支持

Activiti 在运行时需要数据库的支持,使用 25 张表,把流程定义节点内容读取到数据库表中,以供后续使用。

# 3.3.1 Activiti 支持的数据库

activiti 支持的数据库和版本如下:

a41fe8e76718ac216afa1ec8bd79f6b4.png

# 3.3.2 在 MySQL 生成表

3.3.2.1 创建数据库

创建 mysql 数据库 activiti (名字任意):

1
CREATE DATABASE activiti DEFAULT CHARACTER SET utf8;

3.3.2.2 使用 java 代码生成表

  • 创建 java 工程

使用 idea 创建 java 的 maven 工程,取名:activiti01。

  • 加入 maven 依赖的坐标(jar 包)

首先需要在 java 工程中加入 ProcessEngine 所需要的 jar 包,包括:

  1. activiti-engine-7.0.0.beta1.jar
  2. activiti 依赖的 jar 包:mybatis、 alf4j、 log4j 等
  3. activiti 依赖的 spring 包
  4. mysql 数据库驱动
  5. 第三方数据连接池 dbcp
  6. 单元测试 Junit-4.12.jar

我们使用 maven 来实现项目的构建,所以应当导入这些 jar 所对应的坐标到 pom.xml 文件中。

完整的依赖内容如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
<properties>
<slf4j.version>1.6.6</slf4j.version>
<log4j.version>1.2.12</log4j.version>
<activiti.version>7.0.0.Beta1</activiti.version>
</properties>
<dependencies>
<dependency>
<groupId>org.activiti</groupId>
<artifactId>activiti-engine</artifactId>
<version>${activiti.version}</version>
</dependency>
<dependency>
<groupId>org.activiti</groupId>
<artifactId>activiti-spring</artifactId>
<version>${activiti.version}</version>
</dependency>
<!-- bpmn 模型处理 -->
<dependency>
<groupId>org.activiti</groupId>
<artifactId>activiti-bpmn-model</artifactId>
<version>${activiti.version}</version>
</dependency>
<!-- bpmn 转换 -->
<dependency>
<groupId>org.activiti</groupId>
<artifactId>activiti-bpmn-converter</artifactId>
<version>${activiti.version}</version>
</dependency>
<!-- bpmn json数据转换 -->
<dependency>
<groupId>org.activiti</groupId>
<artifactId>activiti-json-converter</artifactId>
<version>${activiti.version}</version>
</dependency>
<!-- bpmn 布局 -->
<dependency>
<groupId>org.activiti</groupId>
<artifactId>activiti-bpmn-layout</artifactId>
<version>${activiti.version}</version>
</dependency>
<!-- activiti 云支持 -->
<dependency>
<groupId>org.activiti.cloud</groupId>
<artifactId>activiti-cloud-services-api</artifactId>
<version>${activiti.version}</version>
</dependency>
<!-- mysql驱动 -->
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>5.1.40</version>
</dependency>
<!-- mybatis -->
<dependency>
<groupId>org.mybatis</groupId>
<artifactId>mybatis</artifactId>
<version>3.4.5</version>
</dependency>
<!-- 链接池 -->
<dependency>
<groupId>commons-dbcp</groupId>
<artifactId>commons-dbcp</artifactId>
<version>1.4</version>
</dependency>
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>4.12</version>
</dependency>
<!-- log start -->
<dependency>
<groupId>log4j</groupId>
<artifactId>log4j</artifactId>
<version>${log4j.version}</version>
</dependency>
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-api</artifactId>
<version>${slf4j.version}</version>
</dependency>
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-log4j12</artifactId>
<version>${slf4j.version}</version>
</dependency>
</dependencies>
  • 添加 log4j 日志配置

我们使用 log4j 日志包,可以对日志进行配置

在 resources 下创建 log4j.properties

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
# Set root category priority to INFO and its only appender to CONSOLE.
#log4j.rootCategory=INFO, CONSOLE debug info warn error fatal
log4j.rootCategory=debug, CONSOLE, LOGFILE
# Set the enterprise logger category to FATAL and its only appender to CONSOLE.
log4j.logger.org.apache.axis.enterprise=FATAL, CONSOLE
# CONSOLE is set to be a ConsoleAppender using a PatternLayout.
log4j.appender.CONSOLE=org.apache.log4j.ConsoleAppender
log4j.appender.CONSOLE.layout=org.apache.log4j.PatternLayout
log4j.appender.CONSOLE.layout.ConversionPattern=%d{ISO8601} %-6r[%15.15t] %-5p %30.30c %x - %m\n
# LOGFILE is set to be a File appender using a PatternLayout.
log4j.appender.LOGFILE=org.apache.log4j.FileAppender
log4j.appender.LOGFILE.File=f:\act\activiti.log
log4j.appender.LOGFILE.Append=true
log4j.appender.LOGFILE.layout=org.apache.log4j.PatternLayout
log4j.appender.LOGFILE.layout.ConversionPattern=%d{ISO8601} %-6r[%15.15t] %-5p %30.30c %x - %m\n
  • 添加 activiti 配置文件

我们使用 activiti 提供的默认方式来创建 mysql 的表。

默认方式的要求是在 resources 下创建 activiti.cfg.xml 文件,注意:默认方式目录和文件名不能修改,因为 activiti 的源码中已经设置,到固定的目录读取固定文件名的文件。

1
2
3
4
5
6
7
8
9
10
11
12
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:context="http://www.springframework.org/schema/context"
xmlns:tx="http://www.springframework.org/schema/tx"
xsi:schemaLocation="http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/contex
http://www.springframework.org/schema/context/spring-context.xsd
http://www.springframework.org/schema/tx
http://www.springframework.org/schema/tx/spring-tx.xsd">
</beans>
  • 在 activiti.cfg.xml 中进行配置

默认方式要在在 activiti.cfg.xml 中 bean 的名字叫 processEngineConfiguration ,名字不可修改

在这里有 2 中配置方式:一种是单独配置数据源,一种是不单独配置数据源

1、直接配置 processEngineConfiguration

processEngineConfiguration 用来创建 ProcessEngine ,在创建 ProcessEngine 时会执行数据库的操作。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:context="http://www.springframework.org/schema/context"
xmlns:tx="http://www.springframework.org/schema/tx"
xsi:schemaLocation="http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/contex
http://www.springframework.org/schema/context/spring-context.xsd
http://www.springframework.org/schema/tx
http://www.springframework.org/schema/tx/spring-tx.xsd">
<!-- 默认id对应的值 为processEngineConfiguration -->
<!-- processEngine Activiti的流程引擎 -->
<bean id="processEngineConfiguration"
class="org.activiti.engine.impl.cfg.StandaloneProcessEngineConfiguration">
<property name="jdbcDriver" value="com.mysql.jdbc.Driver"/>
<property name="jdbcUrl" value="jdbc:mysql:///activiti"/>
<property name="jdbcUsername" value="root"/>
<property name="jdbcPassword" value="123456"/>
<!-- activiti数据库表处理策略 -->
<property name="databaseSchemaUpdate" value="true"/>
</bean>
</beans>

2、配置数据源后,在 processEngineConfiguration 引用

首先配置数据源

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
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:context="http://www.springframework.org/schema/context"
xmlns:tx="http://www.springframework.org/schema/tx"
xsi:schemaLocation="http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/contex
http://www.springframework.org/schema/context/spring-context.xsd
http://www.springframework.org/schema/tx
http://www.springframework.org/schema/tx/spring-tx.xsd">

<!-- 这里可以使用 链接池 dbcp-->
<bean id="dataSource" class="org.apache.commons.dbcp.BasicDataSource">
<property name="driverClassName" value="com.mysql.jdbc.Driver" />
<property name="url" value="jdbc:mysql:///activiti" />
<property name="username" value="root" />
<property name="password" value="123456" />
<property name="maxActive" value="3" />
<property name="maxIdle" value="1" />
</bean>

<bean id="processEngineConfiguration"
class="org.activiti.engine.impl.cfg.StandaloneProcessEngineConfiguration">
<!-- 引用数据源 上面已经设置好了-->
<property name="dataSource" ref="dataSource" />
<!-- activiti数据库表处理策略 -->
<property name="databaseSchemaUpdate" value="true"/>
</bean>
</beans>
  • java 类编写程序生成表

创建一个测试类,调用 activiti 的工具类,生成 acitivti 需要的数据库表。

直接使用 activiti 提供的工具类 ProcessEngines ,会默认读取 classpath 下的 activiti.cfg.xml 文件,读取其中的数据库配置,创建 ProcessEngine ,在创建 ProcessEngine 时会自动创建表。

代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
package com.itheima.activiti01.test;

import org.activiti.engine.ProcessEngine;
import org.activiti.engine.ProcessEngineConfiguration;
import org.junit.Test;

public class TestDemo {
/**
* 生成 activiti的数据库表
*/
@Test
public void testCreateDbTable() {
//使用classpath下的activiti.cfg.xml中的配置创建processEngine
ProcessEngine processEngine = ProcessEngines.getDefaultProcessEngine();
System.out.println(processEngine);
}
}

说明:

  • 运行以上程序段即可完成 activiti 表创建,通过改变 activiti.cfg.xmldatabaseSchemaUpdate 参数的值执行不同的数据表处理策略。
  • 上 边 的 方法 getDefaultProcessEngine 方法在执行时,从 activiti.cfg.xml 中找固定的名称 processEngineConfiguration

在测试程序执行过程中,idea 的控制台会输出日志,说明程序正在创建数据表,类似如下,注意红线内容:

7fc3cd07647b00c2169abcc747a93f7a.png

执行完成后我们查看数据库, 创建了 25 张表,结果如下:

b3dc67533eaebcb9bee794061b94b4d2.png

到这,我们就完成 activiti 运行需要的数据库和表的创建。

# 3.4 表结构介绍

# 3.4.1 表的命名规则和作用

看到刚才创建的表,我们发现 Activiti 的表都以 ACT_ 开头。

第二部分是表示表的用途的两个字母标识。用途也和服务的 API 对应。

  • ACT_RE :'RE’表示 repository。这个前缀的表包含了流程定义和流程静态资源 (图片,规则,等等)。
  • ACT_RU :'RU’表示 runtime。这些运行时的表,包含流程实例,任务,变量,异步任务,等运行中的数据。Activiti 只在流程实例执行过程中保存这些数据, 在流程结束时就会删除这些记录。这样运行时表可以一直很小速度很快。
  • ACT_HI :'HI’表示 history。这些表包含历史数据,比如历史流程实例, 变量,任务等等。
  • ACT_GE :GE 表示 general。通用数据, 用于不同场景下
# 3.4.2 Activiti 数据表介绍

1c6b494498f9165d2b9e72af6d0d193b.png

# 四、Activiti 类关系图

上面我们完成了 Activiti 数据库表的生成,java 代码中我们调用 Activiti 的工具类,下面来了解 Activiti 的类关系

# 4.1 类关系图

512e936b3d7c2e505d1f28991d5bf572.png

在新版本中,我们通过实验可以发现 IdentityServiceFormService 两个 Serivce 都已经删除了。

所以后面我们对于这两个 Service 也不讲解了,但老版本中还是有这两个 Service,同学们需要了解一下

# 4.2 activiti.cfg.xml

activiti 的引擎配置文件,包括: ProcessEngineConfiguration 的定义、数据源定义、事务管理器等,此文件其实就是一个 spring 配置文件。

# 4.3 流程引擎配置类

流程引擎的配置类( ProcessEngineConfiguration ),通过 ProcessEngineConfiguration 可以创建工作流引擎 ProceccEngine ,常用的两种方法如下:

# 4.3.1 StandaloneProcessEngineConfiguration

使用 StandaloneProcessEngineConfigurationActiviti 可以单独运行,来创建 ProcessEngineActiviti 会自己处理事务。

配置文件方式:

通常在 activiti.cfg.xml 配置文件中定义一个 id 为 processEngineConfiguration 的 bean。

方法如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
<bean id="processEngineConfiguration"
class="org.activiti.engine.impl.cfg.StandaloneProcessEngineConfiguration">
<!--配置数据库相关的信息-->
<!--数据库驱动-->
<property name="jdbcDriver" value="com.mysql.jdbc.Driver"/>
<!--数据库链接-->
<property name="jdbcUrl" value="jdbc:mysql:///activiti"/>
<!--数据库用户名-->
<property name="jdbcUsername" value="root"/>
<!--数据库密码-->
<property name="jdbcPassword" value="123456"/>
<!--actviti数据库表在生成时的策略 true - 如果数据库中已经存在相应的表,那么直接使用,如果不存在,那么会创建-->
<property name="databaseSchemaUpdate" value="true"/>
</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
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:context="http://www.springframework.org/schema/context"
xmlns:tx="http://www.springframework.org/schema/tx"
xsi:schemaLocation="http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/contex
http://www.springframework.org/schema/context/spring-context.xsd
http://www.springframework.org/schema/tx
http://www.springframework.org/schema/tx/spring-tx.xsd">
<bean id="dataSource" class="org.apache.commons.dbcp.BasicDataSource">
<property name="driverClassName" value="com.mysql.jdbc.Driver"/>
<property name="url" value="jdbc:mysql:///activiti"/>
<property name="username" value="root"/>
<property name="password" value="123456"/>
<property name="maxActive" value="3"/>
<property name="maxIdle" value="1"/>
</bean>
<!--在默认方式下 bean的id 固定为 processEngineConfiguration-->
<bean id="processEngineConfiguration"
class="org.activiti.engine.impl.cfg.StandaloneProcessEngineConfiguration">
<!--引入上面配置好的 链接池-->
<property name="dataSource" ref="dataSource"/>
<!--actviti数据库表在生成时的策略 true - 如果数据库中已经存在相应的表,那么直接使用,如果不存在,那么会创建-->
<property name="databaseSchemaUpdate" value="true"/>
</bean>
</beans>
# 4.3.2 SpringProcessEngineConfiguration

通过 org.activiti.spring.SpringProcessEngineConfiguration 与 Spring 整合。

创建 spring 与 activiti 的整合配置文件:

activity-spring.cfg.xml (名称可修改)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:mvc="http://www.springframework.org/schema/mvc"
xmlns:context="http://www.springframework.org/schema/context"
xmlns:aop="http://www.springframework.org/schema/aop" xmlns:tx="http://www.springframework.org/schema/tx"
xsi:schemaLocation="http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans-3.1.xsd
http://www.springframework.org/schema/mvc
http://www.springframework.org/schema/mvc/spring-mvc-3.1.xsd
http://www.springframework.org/schema/context
http://www.springframework.org/schema/context/spring-context-3.1.xsd
http://www.springframework.org/schema/aop
http://www.springframework.org/schema/aop/spring-aop-3.1.xsd
http://www.springframework.org/schema/tx
http://www.springframework.org/schema/tx/spring-tx-3.1.xsd ">
<!-- 工作流引擎配置bean -->
<bean id="processEngineConfiguration" class="org.activiti.spring.SpringProcessEngineConfiguration">
<!-- 数据源 -->
<property name="dataSource" ref="dataSource" />
<!-- 使用spring事务管理器 -->
<property name="transactionManager" ref="transactionManager" />
<!-- 数据库策略 -->
<property name="databaseSchemaUpdate" value="drop-create" />
<!-- activiti的定时任务关闭 -->
<property name="jobExecutorActivate" value="false" />
</bean>
<!-- 流程引擎 -->
<bean id="processEngine" class="org.activiti.spring.ProcessEngineFactoryBean">
<property name="processEngineConfiguration" ref="processEngineConfiguration" />
</bean>
<!-- 资源服务service -->
<bean id="repositoryService" factory-bean="processEngine"
factory-method="getRepositoryService" />
<!-- 流程运行service -->
<bean id="runtimeService" factory-bean="processEngine"
factory-method="getRuntimeService" />
<!-- 任务管理service -->
<bean id="taskService" factory-bean="processEngine"
factory-method="getTaskService" />
<!-- 历史管理service -->
<bean id="historyService" factory-bean="processEngine" factory-method="getHistoryService" />
<!-- 用户管理service -->
<bean id="identityService" factory-bean="processEngine" factory-method="getIdentityService" />
<!-- 引擎管理service -->
<bean id="managementService" factory-bean="processEngine" factory-method="getManagementService" />
<!-- 数据源 -->
<bean id="dataSource" class="org.apache.commons.dbcp.BasicDataSource">
<property name="driverClassName" value="com.mysql.jdbc.Driver" />
<property name="url" value="jdbc:mysql://localhost:3306/activiti" />
<property name="username" value="root" />
<property name="password" value="mysql" />
<property name="maxActive" value="3" />
<property name="maxIdle" value="1" />
</bean>
<!-- 事务管理器 -->
<bean id="transactionManager"
class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
<property name="dataSource" ref="dataSource" />
</bean>
<!-- 通知 -->
<tx:advice id="txAdvice" transaction-manager="transactionManager">
<tx:attributes></tx:attributes>
<!-- 传播行为 -->
<tx:method name="save*" propagation="REQUIRED" />
<tx:method name="insert*" propagation="REQUIRED" />
<tx:method name="delete*" propagation="REQUIRED" />
<tx:method name="update*" propagation="REQUIRED" />
<tx:method name="find*" propagation="SUPPORTS" read-only="true" />
<tx:method name="get*" propagation="SUPPORTS" read-only="true" />
</tx:attributes>
</tx:advice>
<!-- 切面,根据具体项目修改切点配置 -->
<aop:config proxy-target-class="true">
<aop:advisor advice-ref="txAdvice" pointcut="execution(* com.itheima.ihrm.service.impl.*.(..))"* />
</aop:config>
</beans>

创建 processEngineConfiguration

1
ProcessEngineConfiguration configuration = ProcessEngineConfiguration.createProcessEngineConfigurationFromResource("activiti.cfg.xml")

上边的代码要求 activiti.cfg.xml 中必须有一个 processEngineConfiguration 的 bean

也可以使用下边的方法,更改 bean 的名字:

1
ProcessEngineConfiguration.createProcessEngineConfigurationFromResource(String resource, String beanName);

# 4.4 工作流引擎创建

工作流引擎(ProcessEngine),相当于一个门面接口,通过 ProcessEngineConfiguration 创建 processEngine ,通过 ProcessEngine 创建各个 service 接口。

# 4.4.1 默认创建方式

activiti.cfg.xml 文件名及路径固定,且 activiti.cfg.xml 文件中有 processEngineConfiguration 的配置, 可以使用如下代码创建 processEngine :

1
2
3
//直接使用工具类 ProcessEngines,使用classpath下的activiti.cfg.xml中的配置创建processEngine
ProcessEngine processEngine = ProcessEngines.getDefaultProcessEngine();
System.out.println(processEngine);
# 4.4.2 一般创建方式
1
2
3
4
//先构建ProcessEngineConfiguration
ProcessEngineConfiguration configuration = ProcessEngineConfiguration.createProcessEngineConfigurationFromResource("activiti.cfg.xml");
//通过ProcessEngineConfiguration创建ProcessEngine,此时会创建数据库
ProcessEngine processEngine = configuration.buildProcessEngine();
# 4.5 Servcie 服务接口

Service 是工作流引擎提供用于进行工作流部署、执行、管理的服务接口,我们使用这些接口可以就是操作服务对应的数据表

4.5.1 Service 创建方式

通过 ProcessEngine 创建 Service

方式如下:

1
2
3
RuntimeService runtimeService = processEngine.getRuntimeService();
RepositoryService repositoryService = processEngine.getRepositoryService();
TaskService taskService = processEngine.getTaskService();

4.5.2 Service 总览

5697fc50f337d0e67354c93535c14cb2.png

简单介绍:

  • RepositoryService

是 activiti 的资源管理类,提供了管理和控制流程发布包和流程定义的操作。使用工作流建模工具设计的业务流程图需要使用此 service 将流程定义文件的内容部署到计算机。

除了部署流程定义以外还可以:查询引擎中的发布包和流程定义。

暂停或激活发布包,对应全部和特定流程定义。暂停意味着它们不能再执行任何操作了,激活是对应的反向操作。获得多种资源,像是包含在发布包里的文件, 或引擎自动生成的流程图。

获得流程定义的 pojo 版本, 可以用来通过 java 解析流程,而不必通过 xml。

  • RuntimeService

Activiti 的流程运行管理类。可以从这个服务类中获取很多关于流程执行相关的信息

  • TaskService

Activiti 的任务管理类。可以从这个类中获取任务的信息。

  • HistoryService

Activiti 的历史管理类,可以查询历史信息,执行流程时,引擎会保存很多数据(根据配置),比如流程实例启动时间,任务的参与者, 完成任务的时间,每个流程实例的执行路径,等等。这个服务主要通过查询功能来获得这些数据。

  • ManagementService

Activiti 的引擎管理类,提供了对 Activiti 流程引擎的管理和维护功能,这些功能不在工作流驱动的应用程序中使用,主要用于 Activiti 系统的日常维护。

# 五、Activiti 入门

在本章内容中,我们来创建一个 Activiti 工作流,并启动这个流程。

创建 Activiti 工作流主要包含以下几步:

  • 定义流程,按照 BPMN 的规范,使用流程定义工具,用流程符号把整个流程描述出来
  • 部署流程,把画好的流程定义文件,加载到数据库中,生成表的数据
  • 启动流程,使用 java 代码来操作数据库表中的内容

# 5.1 流程符号

BPMN 2.0 是业务流程建模符号 2.0 的缩写。

它由 Business Process Management Initiative 这个非营利协会创建并不断发展。作为一种标识,BPMN 2.0 是使用一些符号来明确业务流程设计流程图的一整套符号规范,它能增进业务建模时的沟通效率。

目前 BPMN2.0 是最新的版本,它用于在 BPM 上下文中进行布局和可视化的沟通。

接下来我们先来了解在流程设计中常见的 符号。

BPMN2.0 的基本符合主要包含:

# 事件 Event

118051bcb888229c8b6a28c96dd70ae4.png

# 活动 Activity

活动是工作或任务的一个通用术语。一个活动可以是一个任务,还可以是一个当前流程的子处理流程;其次,你还可以为活动指定不同的类型。常见活动如下:

ec675c381810e1f8f779d3852e619ad2.png

# 网关 GateWay

网关用来处理决策,有几种常用网关需要了解:

7e81eb95b7c1149abb5b5f4a467bcf5a.png

排他网关 (x)

—— 只有一条路径会被选择。流程执行到该网关时,按照输出流的顺序逐个计算,当条件的计算结果为 true 时,继续执行当前网关的输出流;

  • 如果多条线路计算结果都是 true,则会执行第一个值为 true 的线路。如果所有网关计算结果没有 true,则引擎会抛出异常。
  • 排他网关需要和条件顺序流结合使用,default 属性指定默认顺序流,当所有的条件不满足时会执行默认顺序流。

并行网关 (+)

—— 所有路径会被同时选择

  • 拆分 —— 并行执行所有输出顺序流,为每一条顺序流创建一个并行执行线路。
  • 合并 —— 所有从并行网关拆分并执行完成的线路均在此等候,直到所有的线路都执行完成才继续向下执行。

包容网关 (+)

—— 可以同时执行多条线路,也可以在网关上设置条件

  • 拆分 —— 计算每条线路上的表达式,当表达式计算结果为 true 时,创建一个并行线路并继续执行
  • 合并 —— 所有从并行网关拆分并执行完成的线路均在此等候,直到所有的线路都执行完成才继续向下执行。

事件网关 (+)

—— 专门为中间捕获事件设置的,允许设置多个输出流指向多个不同的中间捕获事件。当流程执行到事件网关后,流程处于等待状态,需要等待抛出事件才能将等待状态转换为活动状态。

# 流向 Flow

流是连接两个流程节点的连线。常见的流向包含以下几种:

82c584939f73517735eb4b8ba0594611.png

# 5.2 流程设计器使用

# Activiti-Designer 使用
# Palette(画板)

在 idea 中安装插件即可使用,画板中包括以下结点:

  • Connection— 连接
  • Event— 事件
  • Task— 任务
  • Gateway— 网关
  • Container— 容器
  • Boundary event— 边界事件
  • Intermediate event- - 中间事件

流程图设计完毕保存生成.bpmn 文件

# 新建流程 (IDEA 工具)

首先选中存放图形的目录 (选择 resources 下的 bpmn 目录),点击菜单: New -> BpmnFile ,如图:

06bc384d8f71d945be74121218df4109.png

弹出如下图所示框,输入 evection 表示 出差审批流程:

d358a2338ab5a332f74f8f7732225fda.png

起完名字 evection 后(默认扩展名为 bpmn),就可以看到流程设计页面,如图所示:

26b2c4c13f7b329cb7cdd6d289676167.png

左侧区域是绘图区,右侧区域是 palette 画板区域

鼠标先点击画板的元素即可在左侧绘图

# 绘制流程

使用滑板来绘制流程,通过从右侧把图标拖拽到左侧的画板,最终效果如下:

80e48a5d861bcaaf4e27c0676dfafff6.png

# 指定流程定义 Key

流程定义 key 即流程定义的标识,通过 properties 视图查看流程的 key

42ebf534f78c3895c98c4ecd5d68b301.png

# 指定任务负责人

在 properties 视图指定每个任务结点的负责人,如:填写出差申请的负责人为 zhangsan

62d692f310cb319cf4c4c964bb2aadcc.png

  • 经理审批负责人为 jerry
  • 总经理审批负责人为 jack
  • 财务审批负责人为 rose

# 六、流程操作

# 6.1 流程定义

# 概述

流程定义是线下按照 bpmn2.0 标准去描述 业务流程,通常使用 idea 中的插件对业务流程进行建模。IDEA 插件介绍:IDEA 值得推荐的十几款优秀插件,狂,拽,屌!

使用 idea 下的 designer 设计器绘制流程,并会生成两个文件:.bpmn 和.png

# .bpmn 文件

使用 activiti-desinger 设计业务流程,会生成.bpmn 文件,上面我们已经创建好了 bpmn 文件

BPMN 2.0 根节点是 definitions 节点。这个元素中,可以定义多个流程定义(不过我们建议每个文件只包含一个流程定义, 可以简化开发过程中的维护难度)。

注意,definitions 元素 最少也要包含 xmlns 和 targetNamespace 的声明。targetNamespace 可以是任意值,它用来对流程实例进行分类。

  • 流程定义部分:定义了流程每个结点的描述及结点之间的流程流转。
  • 流程布局定义:定义流程每个结点在流程图上的位置坐标等信息。
# 生成.png 图片文件

IDEA 工具中的操作方式

1、修改文件后缀为 xml

首先将 evection.bpmn 文件改名为 evection.xml,如下图:

6b8b3168ec2eb053d23dd89e760eb66c.png

evection.xml 修改前的 bpmn 文件,效果如下:

05323c8f1d142e4a50be0998af182de9.png

2、使用 designer 设计器打开.xml 文件

在 evection.xml 文件上面,点右键并选择 Diagrams 菜单,再选择 Show BPMN2.0 Designer…

fbfd788c624ebe75fbf8dc38dce5d1ec.png

3、查看打开的文件

打开后,却出现乱码,如图:

35b69a94cf23e9e801955824c1763dd9.png

4、解决中文乱码

1、打开 Settings,找到 File Encodings,把 encoding 的选项都选择 UTF-8

f114de04f6d9a6371ecfc8f3ec7138cf.png

2、打开 IDEA 安装路径,找到如下的安装目录

33d300d7dbf3078d72555e22357622e3.png

根据自己所安装的版本来决定,我使用的是 64 位的 idea,所以在 idea64.exe.vmoptions 文件的最后一行追加一条命令: -Dfile.encoding=UTF-8

如下所示:

d848eb7069122f8a2cb5de6e05fe99a8.png

一定注意,不要有空格,否则重启 IDEA 时会打不开,然后 重启 IDEA。

如果以上方法已经做完,还出现乱码,就再修改一个文件,并在文件的末尾添加: -Dfile.encoding=UTF-8 ,然后重启 idea,如图:

6f4f2a580c57e5e4a05f3822974b6b50.png

最后重新在 evection.xml 文件上面,点右键并选择 Diagrams 菜单,再选择 Show BPMN2.0 Designer… ,看到生成图片,如图:

7c8f334d658e8275844d6c7ac165881a.png

到此,解决乱码问题

# 5、导出为图片文件

点击 Export To File 的小图标,打开如下窗口,注意填写文件名及扩展名,选择好保存图片的位置:

963675cefb4d34535e003d7d791564ab.png

然后,我们把 png 文件拷贝到 resources 下的 bpmn 目录,并且把 evection.xml 改名为 evection.bpmn。

# 6.2 流程定义部署

# 概述

将上面在设计器中定义的流程部署到 activiti 数据库中,就是流程定义部署。

通过调用 activiti 的 api 将流程定义的 bpmn 和 png 两个文件一个一个添加部署到 activiti 中,也可以将两个文件打成 zip 包进行部署。

# 单个文件部署方式

分别将 bpmn 文件和 png 图片文件部署。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
public class ActivitiDemo {
/**
* 部署流程定义
*/
@Test
public void testDeployment(){
// 1、创建ProcessEngine
ProcessEngine processEngine = ProcessEngines.getDefaultProcessEngine();
// 2、得到RepositoryService实例
RepositoryService repositoryService = processEngine.getRepositoryService();
// 3、使用RepositoryService进行部署
Deployment deployment = repositoryService.createDeployment()
.addClasspathResource("bpmn/evection.bpmn") // 添加bpmn资源
.addClasspathResource("bpmn/evection.png") // 添加png资源
.name("出差申请流程")
.deploy();
// 4、输出部署信息
System.out.println("流程部署id:" + deployment.getId());
System.out.println("流程部署名称:" + deployment.getName());
}
}

执行此操作后 activiti 会将上边代码中指定的 bpm 文件和图片文件保存在 activiti 数据库。

# 压缩包部署方式

evection.bpmnevection.png 压缩成 zip 包。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
@Test
public void deployProcessByZip() {
// 定义zip输入流
InputStream inputStream = this
.getClass()
.getClassLoader()
.getResourceAsStream(
"bpmn/evection.zip");
ZipInputStream zipInputStream = new ZipInputStream(inputStream);
// 获取repositoryService
RepositoryService repositoryService = processEngine
.getRepositoryService();
// 流程部署
Deployment deployment = repositoryService.createDeployment()
.addZipInputStream(zipInputStream)
.deploy();
System.out.println("流程部署id:" + deployment.getId());
System.out.println("流程部署名称:" + deployment.getName());
}

执行此操作后 activiti 会将上边代码中指定的 bpm 文件和图片文件保存在 activiti 数据库。

# 操作数据表

流程定义部署后操作 activiti 的 3 张表如下:

  • act_re_deployment 流程定义部署表,每部署一次增加一条记录
  • act_re_procdef 流程定义表,部署每个新的流程定义都会在这张表中增加一条记录
  • act_ge_bytearray 流程资源表

接下来我们来看看,写入了什么数据:

1
SELECT * FROM act_re_deployment #流程定义部署表,记录流程部署信息

结果:

492052ea6a018366c6b003caa0f64fad.png

1
SELECT * FROM act_re_procdef #流程定义表,记录流程定义信息

结果:

注意,KEY 这个字段是用来唯一识别不同流程的关键字

71cdab928e7ef851cfc1f0db25478d57.png

1
SELECT * FROM act_ge_bytearray #资源表

结果:

940b8ba4b3f730e7a38ba0ba6d2829a3.png

注意:

act_re_deploymentact_re_procdef 一对多关系,一次部署在流程部署表生成一条记录,但一次部署可以部署多个流程定义,每个流程定义在流程定义表生成一条记录。每一个流程定义在 act_ge_bytearray 会存在两个资源记录,bpmn 和 png。

建议:一次部署一个流程,这样部署表和流程定义表是一对一有关系,方便读取流程部署及流程定义信息。

# 6.3 启动流程实例

流程定义部署在 activiti 后就可以通过工作流管理业务流程了,也就是说上边部署的出差申请流程可以使用了。

针对该流程,启动一个流程表示发起一个新的出差申请单,这就相当于 java 类与 java 对象的关系,类定义好后需要 new 创建一个对象使用,当然可以 new 多个对象。对于请出差申请流程,张三发起一个出差申请单需要启动一个流程实例,出差申请单发起一个出差单也需要启动一个流程实例。

代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
/**
* 启动流程实例
*/
@Test
public void testStartProcess(){
// 1、创建ProcessEngine
ProcessEngine processEngine = ProcessEngines.getDefaultProcessEngine();
// 2、获取RunTimeService
RuntimeService runtimeService = processEngine.getRuntimeService();
// 3、根据流程定义Id启动流程
ProcessInstance processInstance = runtimeService
.startProcessInstanceByKey("myEvection");
// 输出内容
System.out.println("流程定义id:" + processInstance.getProcessDefinitionId());
System.out.println("流程实例id:" + processInstance.getId());
System.out.println("当前活动Id:" + processInstance.getActivityId());
}

输出内容如下:

dcc6ccc278787f998b09291d1e13630d.png

操作数据表

  • act_hi_actinst 流程实例执行历史
  • act_hi_identitylink 流程的参与用户历史信息
  • act_hi_procinst 流程实例历史信息
  • act_hi_taskinst 流程任务历史信息
  • act_ru_execution 流程执行信息
  • act_ru_identitylink 流程的参与用户信息
  • act_ru_task 任务信息

# 6.4 任务查询

流程启动后,任务的负责人就可以查询自己当前需要处理的任务,查询出来的任务都是该用户的待办任务。

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
/**
* 查询当前个人待执行的任务
*/
@Test
public void testFindPersonalTaskList() {
// 任务负责人
String assignee = "zhangsan";
ProcessEngine processEngine = ProcessEngines.getDefaultProcessEngine();
// 创建TaskService
TaskService taskService = processEngine.getTaskService();
// 根据流程key 和 任务负责人 查询任务
List<Task> list = taskService.createTaskQuery()
.processDefinitionKey("myEvection") //流程Key
.taskAssignee(assignee)//只查询该任务负责人的任务
.list();

for (Task task : list) {

System.out.println("流程实例id:" + task.getProcessInstanceId());
System.out.println("任务id:" + task.getId());
System.out.println("任务负责人:" + task.getAssignee());
System.out.println("任务名称:" + task.getName());

}
}

输出结果如下:

1
2
3
4
流程实例id:2501
任务id:2505
任务负责人:zhangsan
任务名称:创建出差申请

# 6.5 流程任务处理

任务负责人查询待办任务,选择任务进行处理,完成任务。微信搜索公众号:Java 项目精选,回复:java 领取资料 。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// 完成任务
@Test
public void completTask(){
// 获取引擎
ProcessEngine processEngine = ProcessEngines.getDefaultProcessEngine();
// 获取taskService
TaskService taskService = processEngine.getTaskService();

// 根据流程key 和 任务的负责人 查询任务
// 返回一个任务对象
Task task = taskService.createTaskQuery()
.processDefinitionKey("myEvection") //流程Key
.taskAssignee("zhangsan") //要查询的负责人
.singleResult();

// 完成任务,参数:任务id
taskService.complete(task.getId());
}

# 6.6 流程定义信息查询

查询流程相关信息,包含流程定义,流程部署,流程定义版本

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
/**
* 查询流程定义
*/
@Test
public void queryProcessDefinition(){
// 获取引擎
ProcessEngine processEngine = ProcessEngines.getDefaultProcessEngine();
// repositoryService
RepositoryService repositoryService = processEngine.getRepositoryService();
// 得到ProcessDefinitionQuery 对象
ProcessDefinitionQuery processDefinitionQuery = repositoryService.createProcessDefinitionQuery();
// 查询出当前所有的流程定义
// 条件:processDefinitionKey =evection
// orderByProcessDefinitionVersion 按照版本排序
// desc倒叙
// list 返回集合
List<ProcessDefinition> definitionList = processDefinitionQuery.processDefinitionKey("myEvection")
.orderByProcessDefinitionVersion()
.desc()
.list();
// 输出流程定义信息
for (ProcessDefinition processDefinition : definitionList) {
System.out.println("流程定义 id="+processDefinition.getId());
System.out.println("流程定义 name="+processDefinition.getName());
System.out.println("流程定义 key="+processDefinition.getKey());
System.out.println("流程定义 Version="+processDefinition.getVersion());
System.out.println("流程部署ID ="+processDefinition.getDeploymentId());
}

}

输出结果:

1
2
3
4
流程定义id:myEvection:1:4
流程定义名称:出差申请单
流程定义key:myEvection
流程定义版本:1

# 6.7 流程删除

1
2
3
4
5
6
7
8
9
10
11
12
13
public void deleteDeployment() {
// 流程部署id
String deploymentId = "1";

ProcessEngine processEngine = ProcessEngines.getDefaultProcessEngine();
// 通过流程引擎获取repositoryService
RepositoryService repositoryService = processEngine
.getRepositoryService();
//删除流程定义,如果该流程定义已有流程实例启动则删除时出错
repositoryService.deleteDeployment(deploymentId);
//设置true 级联删除流程定义,即使该流程有流程实例启动也可以删除,设置为false非级别删除方式,如果流程
//repositoryService.deleteDeployment(deploymentId, true);
}

说明:

  • 使用 repositoryService 删除流程定义,历史表信息不会被删除
  • 如果该流程定义下没有正在运行的流程,则可以用普通删除。

如果该流程定义下存在已经运行的流程,使用普通删除报错,可用级联删除方法将流程及相关记录全部删除。

先删除没有完成流程节点,最后就可以完全删除流程定义信息

项目开发中级联删除操作一般只开放给超级管理员使用.

# 6.8 流程资源下载

现在我们的流程资源文件已经上传到数据库了,如果其他用户想要查看这些资源文件,可以从数据库中把资源文件下载到本地。

解决方案有:

  • jdbc 对 blob 类型,clob 类型数据读取出来,保存到文件目录
  • 使用 activiti 的 api 来实现

使用 commons-io.jar 解决 IO 的操作

引入 commons-io 依赖包

1
2
3
4
5
<dependency>
<groupId>commons-io</groupId>
<artifactId>commons-io</artifactId>
<version>2.6</version>
</dependency>

通过流程定义对象获取流程定义资源,获取 bpmn 和 png

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44

import org.apache.commons.io.IOUtils;

@Test
public void deleteDeployment(){
// 获取引擎
ProcessEngine processEngine = ProcessEngines.getDefaultProcessEngine();
// 获取repositoryService
RepositoryService repositoryService = processEngine.getRepositoryService();
// 根据部署id 删除部署信息,如果想要级联删除,可以添加第二个参数,true
repositoryService.deleteDeployment("1");
}

public void queryBpmnFile() throws IOException {
// 1、得到引擎
ProcessEngine processEngine = ProcessEngines.getDefaultProcessEngine();
// 2、获取repositoryService
RepositoryService repositoryService = processEngine.getRepositoryService();
// 3、得到查询器:ProcessDefinitionQuery,设置查询条件,得到想要的流程定义
ProcessDefinition processDefinition = repositoryService.createProcessDefinitionQuery()
.processDefinitionKey("myEvection")
.singleResult();
// 4、通过流程定义信息,得到部署ID
String deploymentId = processDefinition.getDeploymentId();
// 5、通过repositoryService的方法,实现读取图片信息和bpmn信息
// png图片的流
InputStream pngInput = repositoryService.getResourceAsStream(deploymentId, processDefinition.getDiagramResourceName());
// bpmn文件的流
InputStream bpmnInput = repositoryService.getResourceAsStream(deploymentId, processDefinition.getResourceName());
// 6、构造OutputStream流
File file_png = new File("d:/evectionflow01.png");
File file_bpmn = new File("d:/evectionflow01.bpmn");
FileOutputStream bpmnOut = new FileOutputStream(file_bpmn);
FileOutputStream pngOut = new FileOutputStream(file_png);
// 7、输入流,输出流的转换
IOUtils.copy(pngInput,pngOut);
IOUtils.copy(bpmnInput,bpmnOut);
// 8、关闭流
pngOut.close();
bpmnOut.close();
pngInput.close();
bpmnInput.close();
}

说明:

  • deploymentId 为流程部署 ID
  • resource_nameact_ge_bytearray 表中 NAME_列的值
  • 使用 repositoryServicegetDeploymentResourceNames 方法可以获取指定部署下得所有文件的名称
  • 使用 repositoryServicegetResourceAsStream 方法传入部署 ID 和资源图片名称可以获取部署下指定名称文件的输入流

最后的将输入流中的图片资源进行输出。

# 6.9 流程历史信息的查看

即使流程定义已经删除了,流程执行的历史信息通过前面的分析,依然保存在 activiti 的 act_hi_* 相关的表中。所以我们还是可以查询流程执行的历史信息,可以通过 HistoryService 来查看相关的历史记录。

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

/**
* 查看历史信息
*/
@Test
public void findHistoryInfo(){
// 获取引擎
ProcessEngine processEngine = ProcessEngines.getDefaultProcessEngine();
// 获取HistoryService
HistoryService historyService = processEngine.getHistoryService();
// 获取 actinst表的查询对象
HistoricActivityInstanceQuery instanceQuery = historyService.createHistoricActivityInstanceQuery();
// 查询 actinst表,条件:根据 InstanceId 查询
// instanceQuery.processInstanceId("2501");
// 查询 actinst表,条件:根据 DefinitionId 查询
instanceQuery.processDefinitionId("myEvection:1:4");
// 增加排序操作,orderByHistoricActivityInstanceStartTime 根据开始时间排序 asc 升序
instanceQuery.orderByHistoricActivityInstanceStartTime().asc();
// 查询所有内容
List<HistoricActivityInstance> activityInstanceList = instanceQuery.list();
// 输出
for (HistoricActivityInstance hi : activityInstanceList) {
System.out.println(hi.getActivityId());
System.out.println(hi.getActivityName());
System.out.println(hi.getProcessDefinitionId());
System.out.println(hi.getProcessInstanceId());
System.out.println("<==========================>");
}
}

# 关于我

Brath 是一个热爱技术的 Java 程序猿,公众号「InterviewCoder」定期分享有趣有料的精品原创文章!

图片

二维码plus 非常感谢各位人才能看到这里,原创不易,文章如果有帮助可以关注、点赞、分享或评论,这都是对我的莫大支持!

【Flutter】Flutter生命周期

InterviewCoder

# 【Flutter】Flutter 生命周期

img

# 一、生命周期阶段

flutter 生命周期大体上可以分为三个阶段:初始化、状态变化、销毁。

# 1、初始化阶段

对应执行构造方法和 initState 时候

# 2、状态变化阶段

开新的 widget 或者调用 setState 方法的时候

# 3、销毁阶段

deactivate 和 dispose

# 二、生命周期阶段执行的函数

# 1、initState

调用次数:1 次

插入渲染树时调用,只调用一次,widget 创建执行的第一个方法,这里可以做一些初始化工作,比如初始化 State 的变量。

# 2、didChangeDependencies

调用次数:多次

  • 初始化时,在 initState () 之后立刻调用
  • 当依赖的 InheritedWidget rebuild, 会触发此接口被调用
  • 实测在组件可见状态变化的时候会调用

# 3、build

调用次数:多次

  • 初始化之后开始绘制界面
  • setState 触发的时候会

# 4、didUpdateWidget

调用次数:多次

组件状态改变时候调用

# 5、deactivate

当 State 对象从树中被移除时,会调用此回调,会在 dispose 之前调用。

页面销毁的时候会依次执行:deactivate > dispose

# 6、dispose

调用次数:1 次

当 State 对象从树中被永久移除时调用;通常在此回调中释放资源。

# 7、reassemble

在热重载 (hot reload) 时会被调用,此回调在 Release 模式下永远不会被调用

# 三、App 生命周期

​ 通过 WidgetsBindingObserver 的 didChangeAppLifecycleState 来获取。通过该接口可以获取是生命周期在 AppLifecycleState 类中。

# 1、resumed

可见并能响应用户的输入,同安卓的 onResume

# 2、inactive

处在并不活动状态,无法处理用户响应,同安卓的 onPause

# 3、paused

不可见并不能响应用户的输入,但是在后台继续活动中,同安卓的 onStop

下面是生命周期:

  1. 初次打开 widget 时,不执行 AppLifecycleState 的回调;
  2. 按 home 键或 Power 键, AppLifecycleState inactive---->AppLifecycleState pause
  3. 从后台到前台:AppLifecycleState inactive—>ApplifecycleState resumed
  4. back 键退出应用: AppLifecycleState inactive—>AppLifecycleState paused

参考文章:https://www.jianshu.com/p/00ff0c2b8336

# 关于我

Brath 是一个热爱技术的 Java 程序猿,公众号「InterviewCoder」定期分享有趣有料的精品原创文章!

InterviewCoder

非常感谢各位人才能看到这里,原创不易,文章如果有帮助可以关注、点赞、分享或评论,这都是对我的莫大支持!

Linux环境下安装并启动Elasticsearch-head

InterviewCoder

# 1、elasticsearch-head 介绍

官方地址: https://github.com/mobz/elasticsearch-head

elasticsearch-head 是一款用来管理 Elasticsearch 集群的第三方插件工具。 elasticsearch-Head 插件在 5.0 版本之前可以直接以插件的形式直接安装,但是 5.0 以后安装方式发生了改变,需要 nodejs 环境支持,或者直接使用别人封装好的 docker 镜像,更推荐的是谷歌浏览器的插件。

# 2、elasticsearch-head 安装

# npm 安装 elasticsearch-head

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
#下载安装nodejs
wget https://nodejs.org/dist/v12.13.0/node-v12.13.0-linux-x64.tar.xz
tar xf node-v12.13.0-linux-x64.tar.xz
mv node-v12.13.0-linux-x64 node
#修改环境变量
echo 'export PATH=$PATH:/opt/node/bin' >> /etc/profile
#配置生效
source /etc/profile
npm -v
node -v

#下载elasticsearch-head
git clone git://github.com/mobz/elasticsearch-head.git

#进入elasticsearch-head安装目录
cd elasticsearch-head
#配置国内镜像
npm install -g cnpm --registry=https://registry.npm.taobao.org
#安装
cnpm install
#启动
cnpm run start

修改 Elasticsearch 配置文件,添加如下参数并重启:

1
2
3
#准许es被跨域访问
http.cors.enabled: true
http.cors.allow-origin: "*"

# 关于我

Brath 是一个热爱技术的 Java 程序猿,公众号「InterviewCoder」定期分享有趣有料的精品原创文章!

InterviewCoder

非常感谢各位人才能看到这里,原创不易,文章如果有帮助可以关注、点赞、分享或评论,这都是对我的莫大支持!