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

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


  • 首页

  • 归档

  • 搜索

Spring框架中一个有用的小组件:Spring Retry

发表于 2021-07-22

1、概述

Spring Retry 是Spring框架中的一个组件,
它提供了自动重新调用失败操作的能力。这在错误可能是暂时发生的(如瞬时网络故障)的情况下很有帮助。

在本文中,我们将看到使用Spring Retry的各种方式:注解、RetryTemplate以及回调。

2、Maven依赖

让我们首先将spring-retry依赖项添加到我们的pom.xml文件中:

1
2
3
4
5
xml复制代码<dependency>
<groupId>org.springframework.retry</groupId>
<artifactId>spring-retry</artifactId>
<version>1.2.5.RELEASE</version>
</dependency>

我们还需要将Spring AOP添加到我们的项目中:

1
2
3
4
5
xml复制代码<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-aspects</artifactId>
<version>5.2.8.RELEASE</version>
</dependency>

可以查看Maven Central来获取最新版本的spring-retry
和spring-aspects 依赖项。

3、开启Spring Retry

要在应用程序中启用Spring Retry,我们需要将@EnableRetry注释添加到我们的@Configuration类:

1
2
3
java复制代码@Configuration
@EnableRetry
public class AppConfig { ... }

4、使用Spring Retry

4.1、@Retryable而不用恢复

我们可以使用@Retryable注解为方法添加重试功能:

1
2
3
4
5
6
java复制代码@Service
public interface MyService {
@Retryable(value = RuntimeException.class)
void retryService(String sql);

}

在这里,当抛出RuntimeException时尝试重试。

根据@Retryable的默认行为,重试最多可能发生3次,重试之间有1秒的延迟。

4.2、@Retryable和@Recover

现在让我们使用@Recover注解添加一个恢复方法:

1
2
3
4
5
6
7
8
java复制代码@Service
public interface MyService {
@Retryable(value = SQLException.class)
void retryServiceWithRecovery(String sql) throws SQLException;

@Recover
void recover(SQLException e, String sql);
}

这里,当抛出SQLException时重试会尝试运行。 当@Retryable方法因指定异常而失败时,@Recover注解定义了一个单独的恢复方法。

因此,如果retryServiceWithRecovery方法在三次尝试之后还是抛出了SQLException,那么recover()方法将被调用。

恢复处理程序的第一个参数应该是Throwable类型(可选)和相同的返回类型。其余的参数按相同顺序从失败方法的参数列表中填充。

4.3、自定义@Retryable的行为

为了自定义重试的行为,我们可以使用参数maxAttempts和backoff:

1
2
3
4
5
6
java复制代码@Service
public interface MyService {
@Retryable( value = SQLException.class,
maxAttempts = 2, backoff = @Backoff(delay = 100))
void retryServiceWithCustomization(String sql) throws SQLException;
}

这样最多将有两次尝试和100毫秒的延迟。

4.4、使用Spring Properties

我们还可以在@Retryable注解中使用properties。

为了演示这一点,我们将看到如何将delay和maxAttempts的值外部化到一个properties文件中。

首先,让我们在名为retryConfig.properties的文件中定义属性:

1
2
properties复制代码retry.maxAttempts=2
retry.maxDelay=100

然后我们指示@Configuration类加载这个文件:

1
2
3
java复制代码@PropertySource("classpath:retryConfig.properties")
public class AppConfig { ... }
// ...

最后,我们可以在@Retryable的定义中注入retry.maxAttempts和retry.maxDelay的值:

1
2
3
4
5
6
java复制代码@Service 
public interface MyService {
@Retryable( value = SQLException.class, maxAttemptsExpression = "${retry.maxAttempts}",
backoff = @Backoff(delayExpression = "${retry.maxDelay}"))
void retryServiceWithExternalizedConfiguration(String sql) throws SQLException;
}

请注意,我们现在使用的是maxAttemptsExpression和delayExpression而不是maxAttempts和delay。

5、RetryTemplate

5.1、RetryOperations

Spring Retry提供了RetryOperations接口,它提供了一组execute()方法:

1
2
3
4
5
java复制代码public interface RetryOperations {
<T> T execute(RetryCallback<T> retryCallback) throws Exception;

...
}

execute()方法的参数RetryCallback,是一个接口,可以插入需要在失败时重试的业务逻辑:

1
2
3
java复制代码public interface RetryCallback<T> {
T doWithRetry(RetryContext context) throws Throwable;
}

5.2、RetryTemplate配置

RetryTemplate是RetryOperations的一个实现。

让我们在@Configuration类中配置一个RetryTemplate的bean:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
java复制代码@Configuration
public class AppConfig {
//...
@Bean
public RetryTemplate retryTemplate() {
RetryTemplate retryTemplate = new RetryTemplate();

FixedBackOffPolicy fixedBackOffPolicy = new FixedBackOffPolicy();
fixedBackOffPolicy.setBackOffPeriod(2000l);
retryTemplate.setBackOffPolicy(fixedBackOffPolicy);

SimpleRetryPolicy retryPolicy = new SimpleRetryPolicy();
retryPolicy.setMaxAttempts(2);
retryTemplate.setRetryPolicy(retryPolicy);

return retryTemplate;
}
}

这个RetryPolicy确定了何时应该重试操作。

其中SimpleRetryPolicy定义了重试的固定次数,另一方面,BackOffPolicy用于控制重试尝试之间的回退。

最后,FixedBackOffPolicy会使重试在继续之前暂停一段固定的时间。

5.3、使用RetryTemplate

要使用重试处理来运行代码,我们可以调用retryTemplate.execute()方法:

1
2
3
4
5
6
7
java复制代码retryTemplate.execute(new RetryCallback<Void, RuntimeException>() {
@Override
public Void doWithRetry(RetryContext arg0) {
myService.templateRetryService();
...
}
});

我们可以使用lambda表达式代替匿名类:

1
2
3
4
java复制代码retryTemplate.execute(arg0 -> {
myService.templateRetryService();
return null;
});

6、监听器

监听器在重试时提供另外的回调。我们可以用这些来关注跨不同重试的各个横切点。

6.1、添加回调

回调在RetryListener接口中提供:

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
java复制代码public class DefaultListenerSupport extends RetryListenerSupport {
@Override
public <T, E extends Throwable> void close(RetryContext context,
RetryCallback<T, E> callback, Throwable throwable) {
logger.info("onClose");
...
super.close(context, callback, throwable);
}

@Override
public <T, E extends Throwable> void onError(RetryContext context,
RetryCallback<T, E> callback, Throwable throwable) {
logger.info("onError");
...
super.onError(context, callback, throwable);
}

@Override
public <T, E extends Throwable> boolean open(RetryContext context,
RetryCallback<T, E> callback) {
logger.info("onOpen");
...
return super.open(context, callback);
}
}

open和close的回调在整个重试之前和之后执行,而onError应用于单个RetryCallback调用。

6.2、注册监听器

接下来,我们将我们的监听器(DefaultListenerSupport)注册到我们的RetryTemplate bean:

1
2
3
4
5
6
7
8
9
10
11
12
java复制代码@Configuration
public class AppConfig {
...

@Bean
public RetryTemplate retryTemplate() {
RetryTemplate retryTemplate = new RetryTemplate();
...
retryTemplate.registerListener(new DefaultListenerSupport());
return retryTemplate;
}
}

7、测试结果

为了完成我们的示例,让我们验证一下结果:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
java复制代码@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(
classes = AppConfig.class,
loader = AnnotationConfigContextLoader.class)
public class SpringRetryIntegrationTest {

@Autowired
private MyService myService;

@Autowired
private RetryTemplate retryTemplate;

@Test(expected = RuntimeException.class)
public void givenTemplateRetryService_whenCallWithException_thenRetry() {
retryTemplate.execute(arg0 -> {
myService.templateRetryService();
return null;
});
}
}

从测试日志中可以看出,我们已经正确配置了RetryTemplate和RetryListener:

1
2
3
4
5
6
ini复制代码2020-01-09 20:04:10 [main] INFO  c.p.s.DefaultListenerSupport - onOpen 
2020-01-09 20:04:10 [main] INFO c.pinmost.springretry.MyServiceImpl - throw RuntimeException in method templateRetryService()
2020-01-09 20:04:10 [main] INFO c.p.s.DefaultListenerSupport - onError
2020-01-09 20:04:12 [main] INFO c.pinmost.springretry.MyServiceImpl - throw RuntimeException in method templateRetryService()
2020-01-09 20:04:12 [main] INFO c.p.s.DefaultListenerSupport - onError
2020-01-09 20:04:12 [main] INFO c.p.s.DefaultListenerSupport - onClose

8、结论

在本文中,我们看到了如何使用注解、RetryTemplate和回调监听器来使用Spring Retry。

原文地址:www.baeldung.com/spring-retr…

翻译:码农熊猫

更多技术干货,请访问我的个人网站https://pinmost.com,或关注公众号【码农熊猫】

本文转载自: 掘金

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

通用业务逻辑点,菜单的递归处理,Java

发表于 2021-07-22

目标:功能菜单逻辑开发

最近在新开发一套功能,最基本的菜单权限这块也在其中,这里我记录下菜单递归逻辑,快速写出菜单的父子结构。

基础类定义

菜单基础表或者bean对象

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
arduino复制代码/**
菜单对象 表结构
**/
@Data
public class SysMenuEntity implements Serializable {
private static final long serialVersionUID = 1L;

/**
* 菜单ID
*/
@TableId
private Long menuId;

/**
* 父菜单ID,一级菜单为0
*/
private Long parentId;

/**
* 父菜单名称
*/
@TableField(exist=false)
private String parentName;

/**
* 菜单名称
*/
private String name;

/**
* 菜单URL
*/
private String url;

/**
* 授权(多个用逗号分隔,如:user:list,user:create)
*/
private String perms;

/**
* 类型 0:目录 1:菜单 2:按钮
*/
private Integer type;

/**
* 菜单图标
*/
private String icon;

/**
* 排序
*/
private Integer orderNum;

/**
* ztree属性
*/
@TableField(exist=false)
private Boolean open;

@TableField(exist=false)
private List<SysMenuEntity> list;
}

菜单类型 枚举

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
csharp复制代码/**
* 菜单类型
*/
public enum MenuType {
/**
* 目录
*/
CATALOG(0),
/**
* 菜单
*/
MENU(1),
/**
* 按钮
*/
BUTTON(2);
private int value;
MenuType(int value) {
this.value = value;
}
public int getValue() {
return value;
}
}

业务逻辑

方法入口

如果没用到用户权限啥的,就忽略userID的逻辑就好

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
kotlin复制代码@Override
public List<SysMenuEntity> getUserMenuList(Long userId) {
//系统管理员,拥有最高权限
if(userId == Constant.SUPER_ADMIN){
return getAllMenuList(null);
}

//用户菜单列表
List<Long> menuIdList = sysUserService.queryAllMenuId(userId);
return getAllMenuList(menuIdList);
}

/**queryAllMenuId 的实现简单copy出来下,就是查找用户有哪些menu权限的IDs**/
@Override
public List<Long> queryAllMenuId(Long userId) {
return baseMapper.queryAllMenuId(userId);
}

获取所有菜单,去处理递归

1
2
3
4
5
6
7
8
9
10
11
scss复制代码/**
* 获取所有菜单列表 ,这里的入参 menuIdList 如user没考虑就空了
*/
private List<SysMenuEntity> getAllMenuList(List<Long> menuIdList){
//查询根菜单列表
List<SysMenuEntity> menuList = queryListParentId(0L, menuIdList);
//递归获取子菜单
getMenuTreeList(menuList, menuIdList);

return menuList;
}

菜单递归逻辑

返回的List对象就是当前菜单的父子结构数据

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
scss复制代码/**
* 递归 返回的List对象就是当前菜单的父子结构数据
*/
private List<SysMenuEntity> getMenuTreeList(List<SysMenuEntity> menuList, List<Long> menuIdList){
List<SysMenuEntity> subMenuList = new ArrayList<SysMenuEntity>();
for(SysMenuEntity entity : menuList){
//目录
if(entity.getType() == Constant.MenuType.CATALOG.getValue()){
entity.setList(getMenuTreeList(queryListParentId(entity.getMenuId(), menuIdList), menuIdList));
}
subMenuList.add(entity);
}

return subMenuList;
}

总结

其实也没啥哈,就是简单记录下每个业务逻辑点,这种通用的做法,当然也看具体需求,如果需求上不需要多级菜单,那可以简单嵌套2个for就可以了。
但这个是通用的, 管他几层是吧,OK,just mark it !

本文转载自: 掘金

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

彻底搞懂MySQL的redo log,binlog,undo

发表于 2021-07-22

前言

我们知道一条 select 语句是如何执行的。如果是 update 语句呢,执行步骤和查询语句其实是一样的,在执行语句前要连接数据库,这是连接器的工作。如果查询缓存中存在这条 SQL 的结果集缓存,直接取出返回客户端,前面说过,表上有更新的时候,这个表相关的查询缓存都会失效,所以查询缓存不建议使用,在 MySQL 8.0 版本把查询缓存删除了。接下来,分析器、优化器、执行器分别做各自的工作,查询出结果集返回给客户端。

但 update 和 select 不一样的是,update还涉及两个重要的日志文件,就是 redo log 和 binlog

redo log

反过来思考,如果不存在这两个日志,会出现什么问题?

update 操作其实是分为两步操作,先查询到对应的行记录,再根据条件进行更新操作。如果没有 redo log 的话,MySQL 每次的update操作都要更新磁盘文件,更新磁盘文件需要先在磁盘中找到对应的行记录,再更新,每一条 update 语句都要操作磁盘文件,整个过程的 I/O 成本,查找成本都很高。为了解决这个问题,InnoDB 引擎的设计者想到了一个办法,先将记录写到 redo log 中,并更新内存,这个时候更新就算完成了,操作内存比操作磁盘要快的多。同时,InnoDB 会在适当的时候,将 redo log 中的记录更新到磁盘文件中。这个更新往往是系统空闲时做。

每次更新操作都要往 redo log 中写入,如果 redo log 满了,空间不够用了怎么办?

InnoDB 的 redo log 文件是固定大小的,比如可以配置一组4个文件,每个文件大小是 1GB,那么 redo log 中可以记录 4GB 的操作,InnoDB 会从第一个文件开始写入,直到第四个文件写满了,又回到第一个文件开头循环写,如下图。

redo log循环写.png

[图片来自网络]

write pos是当前记录的位置,一边写一边后移,写到第3号文件末尾后就回到0号文件开头。checkpoint是当前要擦除的位置,也是往后推移并且循环的,擦除记录前要把记录更新到数据文件。

write pos和checkpoint之间的是 redo log上还空着的部分,可以用来记录新的操作。如果write pos追上checkpoint,表示 redo log 满了,这时候不能再执行新的更新,得停下来先擦掉一些记录,把checkpoint推进一下。

有了redo log,InnoDB就可以保证即使数据库发生异常重启,之前提交的记录都不会丢失,这个能力称为crash-safe。

binlog

刚刚说的 redo log 是执行引擎层的 log 文件,我们都知道,MySQL 整体来看,分为 Server 层和引擎层,而 binlog 是 Server 层面的 log 文件,也就是所有执行引擎都有 binlog

那为什么 InnoDB 有一份 log 文件,MySQL 有一份 log 文件呢?

因为以前的MySQL没有InnoDB引擎,MySQL5.5前使用的 MyISAM引擎,但是 MyISAM 没有 crash-safe 的能力,而 binlog 只能用于归档。InnoDB 是后来作为 MySQL 的引擎以插件形式引入的。既然只靠 binlog 无法实现 crash-safe 的能力,所以 InnoDB 使用另一套日志系统——redo log 来实现。

update操作流程

update T set c=c+1 where ID=2;

1、执行器先通过引擎查询到 id = 2 这行数据,id 是主键,直接遍历主键索引树直接插到这行数据,如果这行数据所在的数据页在内存中,就直接返回结果给执行器,否则,需要先从磁盘读入内存,然后再返回。

2、执行器拿到引擎给的行数据,把 这个值+1,得到新的一行数据,再调用引擎接口写入这行数据。

3、引擎将这行数据更新到内存中,同时记录到 redo log 中,此时 redo log 处于 perpare 状态,此时就告知执行器已经更新完成了,随时可以提交事务。

4、执行器生成这个操作的binlog,并把binlog写入磁盘

5、执行器调用引擎的提交事务接口,引擎把刚刚写入的redo log改成提交(commit)状态,更新完成

如下图为 update 语句的执行流程,深色代表 MySQL 执行器中执行的,浅色代表 InnoDB 内部执行的。

update操作流程图.png

[图片来自网络]

两阶段提交

写入 redo log 分为两个步骤,prepare 和 commit,这就是“两阶段提交”。

为什么要有两阶段提交,就是为了让 redo log 和 binlog 两个文件保持一致。我们还是用反证法来说明,假设没有两阶段提交会发生什么问题:

  • 先写 redo log,再写 binlog,假设 redo log 写完,binlog 还没写完,MySQL 进程异常重启,redo log 写完后,即使系统崩溃,仍然能把数据恢复回来,所有恢复后的数据是正确的。但是 binlog 没写完,这时候 binlog 中就没有记录这条语句的操作,因此,之后备份日志的时候,binlog 就没有这条操作记录,如果用这个 binlog 来恢复临时库的话,由于这条语句记录的丢失,临时库就会少了这一个语句的操作,恢复出来的数据就与原库的值不同。
  • 先写 binlog,再写 redo log,如果在 binlog 写完后系统崩溃了,由于 redo log 还没写,崩溃后这个事务无效,所以磁盘数据文件中的数据是没有这条语句的操作的,但是 binlog 中已经做了记录,所以以后用这个 binlog 来做数据恢复时,就多了一个事务操作,与原库的数据不一致。
    如果没有“两阶段提交”,会导致 redo log 和 binlog 记录的操作不一致,那么数据库的状态就有可能和用它的日志恢复出来的库数据不一致。

所以,能够保证 redo log 和 binlog 的操作记录一致的流程是,将操作先更新到内存,再写入 redo log,此时标记为 prepare 状态,再写入 binlog,此时再提交事务,将 redo log 标记为 commit 状态。

redo log 与 binlog

  1. redo log 是InnoDB 引擎特有的;而 binlog 是MySQL Server 层实现的
  2. redo log 是物理日志,记录的是“在某个数据页做了什么修改”;而 binlog 是逻辑日志,记录的是语句的原始逻辑。比如 update T set c=c+1 where ID=2;这条SQL,redo log 中记录的是 :xx页号,xx偏移量的数据修改为xxx;binlog 中记录的是:id = 2 这一行的 c 字段 +1
  3. redo log 是循环写的,固定空间会用完;binlog 可以追加写入,一个文件写满了会切换到下一个文件写,并不会覆盖之前的记录
  4. 记录内容时间不同,redo log 记录事务发起后的 DML 和 DDL语句;binlog 记录commit 完成后的 DML 语句和 DDL 语句
  5. 作用不同,redo log 作为异常宕机或者介质故障后的数据恢复使用;binlog 作为恢复数据使用,主从复制搭建。

undo log

undo log 和 redo log 也是引擎层的 log 文件,undo log 提供了回滚和多个行版本控制(MVCC),在数据库修改操作时,不仅记录了 redo log,还记录了 undo log,如果因为某些原因导致事务执行失败回滚了,可以借助 undo log 进行回滚。

虽然 undo log 和 redo log 都是InnoDB 特有的,但 undo log 记录的是 逻辑日志,redo log 记录的是物理日志。对记录做变更操作时不仅会产生 redo 记录,也会产生 undo 记录(insert,update,delete),undo log 日志用于存放数据被修改前的值,比如 update T set c=c+1 where ID=2; 这条 SQL,undo log 中记录的是 c 在 +1 前的值,如果这个 update 出现异常需要回滚,可以使用 undo log 实现回滚,保证事务一致性。

而多版本并发控制(MVCC) ,也用到了 undo log ,当读取的某一行被其他事务锁定时,它可以从 undo log 中获取该行记录以前的数据是什么,从而提供该行版本信息,让用户实现非锁定一致性读取。

undo 记录默认被记录到系统表空间(ibdata1)中,但是从 MySQL5.6 开始,就可以使用独立的 undo 表空间了。不用担心 undo 会把 ibdata1 文件弄大。

undo log 是采用段 (segment)的方式来记录的,每个 undo 操作在记录的时候占用一个 undo log segment

rollback segment 称为回滚段,每个回滚段中有 1024 个 undo log segment,在以前的版本中,只支持一个 rollback segment,也就是只能记录 1024 个 undo log segment,MySQL 5.5 以后,可以支持 128 个 rollback segment,即支持 128*1024 个 undo 操作,还可以通过变量 innodb_undo_logs自定义 rollback segment 数量,默认是 128

总结

redo log 用来保证 crash-safe,binlog 用来保证可以将数据库状态恢复到任一时刻,undo log 是用来保证事务需要回滚时数据状态的回滚和 MVCC 时,记录各版本数据信息。

本文转载自: 掘金

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

MyBatis的三种分页方式你学废了嘛 MyBatis系

发表于 2021-07-22

「本文已参与好文召集令活动,点击查看:后端、大前端双赛道投稿,2万元奖池等你挑战!」


相关文章

MyBatis系列汇总:MyBatis系列


前言

  • 分页是我们在开发中绕不过去的一个坎!当你的数据量大了的时候,一次性将所有数据查出来不现实,所以我们一般都是分页查询的,减轻服务端的压力,提升了速度和效率!也减轻了前端渲染的压力!
  • 注意:由于 java 允许的最大整数为 2147483647,所以 limit 能使用的最大整数也是 2147483647,一次性取出大量数据可能引起内存溢出,所以在大数据查询场合慎重使用!

一、Limit分页

  • 语法:
+ 
1
xml复制代码limit ${startPos},${pageSize}
+ 在实际项目中我们一般会加上为空为null判断,如下: +
1
2
3
xml复制代码<if test="startPos!=null and pageSize!=null">
limit ${startPos},${pageSize}
</if>
  • 业务层代码:
+ 
1
2
3
4
5
6
java复制代码    <select id="getUserInfo1" parameterType="map" resultType="dayu">
select * from user
<if test="startPos!=null and pageSize!=null">
limit ${startPos},${pageSize}
</if>
</select>
+
1
java复制代码List<User> getUserInfo1(Map<String,Object> map);
+
1
2
3
4
5
6
7
8
9
10
11
12
13
14
java复制代码@Test
public void selectUser() {
SqlSession session = MybatisUtils.getSession();
UserMapper mapper = session.getMapper(UserMapper.class);
//这里塞值
Map<String,Object> parms = new HashMap<>();
parms.put("startPos","0");
parms.put("pageSize","5");
List<User> users = mapper.getUserInfo1(parms);
for (User map: users){
System.out.println(map);
}
session.close();
}
  • 执行结果:
+ ![image-20210708193554244.png](https://gitee.com/songjianzaina/juejin_p12/raw/master/img/1abd75baa7bd8726a7e85c7234f7a0f75db41be4f5fca98312fb50bc2afb9b7c)
+ 传入0,10时:
+ ![image-20210708193625536.png](https://gitee.com/songjianzaina/juejin_p12/raw/master/img/662c4e242392e6685c85bf86265efbf31ad22c71ad6fad7dd75b4ccdbe8b0677)
  • 总结:
+ limit 0,10;
+ 0 代表从第0条数据开始
+ 10 代表查10条数据
+ 等到第二页的时候就是 limit 10,10;
+ 以此类推!
  • 这些内容其实就时MySQL中的内容,不作再详细讲解了。

二、RowBounds分页(不推荐使用)

  • RowBounds帮我们省略了limit的内容,我们只需要在业务层关注分页即可!无须再传入指定数据!
  • 但是,这个属于逻辑分页,即实际上sql查询的是所有的数据,在业务层进行了分页而已,比较占用内存,而且数据更新不及时,可能会有一定的滞后性!不推荐使用!
  • RowBounds对象有2个属性,offset和limit。
+ offset:起始行数
+ limit:需要的数据行数
+ 因此,取出来的数据就是:从第offset+1行开始,取limit行
  • 业务层代码:
+ 
1
2
3
4
5
6
7
8
9
10
11
java复制代码@Test
public void selectUserRowBounds() {
SqlSession session = MybatisUtils.getSession();
UserMapper mapper = session.getMapper(UserMapper.class);
// List<User> users = session.selectList("com.dy.mapper.UserMapper.getUserInfoRowBounds",null,new RowBounds(0, 5));
List<User> users = mapper.getUserInfoRowBounds(new RowBounds(0,5));
for (User map: users){
System.out.println(map);
}
session.close();
}
+
1
java复制代码List<User> getUserInfoRowBounds(RowBounds rowBounds);
+
1
2
3
java复制代码<select id="getUserInfoRowBounds" resultType="dayu">
select * from user
</select>
  • 执行查看结果:
+ ![image-20210709112829473.png](https://gitee.com/songjianzaina/juejin_p12/raw/master/img/5a3c22276d681ebd3b1b06a702685256806d70a2e558c865897635718f901fc3)

三、Mybatis_PageHelper分页插件

  • 官方GitHub地址 官方地址
  • 引入jar包
+ 
1
2
3
4
5
xml复制代码    <dependency>
<groupId>com.github.pagehelper</groupId>
<artifactId>pagehelper</artifactId>
<version>5.1.7</version>
</dependency>
  • 配置MyBatis核心配置文件
+ 
1
2
3
java复制代码    <plugins>
<plugin interceptor="com.github.pagehelper.PageInterceptor" />
</plugins>
  • 业务层代码
+ 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
java复制代码@Test
public void selectUserPageHelper() {
SqlSession session = MybatisUtils.getSession();
UserMapper mapper = session.getMapper(UserMapper.class);
//第二种,Mapper接口方式的调用,推荐这种使用方式。
PageHelper.startPage(1, 3);
List<User> list = mapper.getUserInfo();
//用PageInfo将包装起来
PageInfo page = new PageInfo(list);
for (User map: list){
System.out.println(map);
}
System.out.println("page:---"+page);
session.close();
}
  • 执行结果
+ ![image-20210709150537674.png](https://gitee.com/songjianzaina/juejin_p12/raw/master/img/17bbaa4e0b19d5a2bbe21c2084fea51870715c25f42bc3db455e5e620bef48d5)
  • 总结
+ PageHelper还是很好用的,也是物理分页!
+ 实际上我们一般用第二种比较多:Mapper接口方式的调用
+ 
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
java复制代码//第一种,RowBounds方式的调用
List<User> list = sqlSession.selectList("x.y.selectIf", null, new RowBounds(0, 10));

//第二种,Mapper接口方式的调用,推荐这种使用方式。
PageHelper.startPage(1, 10);
List<User> list = userMapper.selectIf(1);

//第三种,Mapper接口方式的调用,推荐这种使用方式。
PageHelper.offsetPage(1, 10);
List<User> list = userMapper.selectIf(1);

//第四种,参数方法调用
//存在以下 Mapper 接口方法,你不需要在 xml 处理后两个参数
public interface CountryMapper {
List<User> selectByPageNumSize(
@Param("user") User user,
@Param("pageNum") int pageNum,
@Param("pageSize") int pageSize);
}
//配置supportMethodsArguments=true
//在代码中直接调用:
List<User> list = userMapper.selectByPageNumSize(user, 1, 10);

//第五种,参数对象
//如果 pageNum 和 pageSize 存在于 User 对象中,只要参数有值,也会被分页
//有如下 User 对象
public class User {
//其他fields
//下面两个参数名和 params 配置的名字一致
private Integer pageNum;
private Integer pageSize;
}
//存在以下 Mapper 接口方法,你不需要在 xml 处理后两个参数
public interface CountryMapper {
List<User> selectByPageNumSize(User user);
}
//当 user 中的 pageNum!= null && pageSize!= null 时,会自动分页
List<User> list = userMapper.selectByPageNumSize(user);

//第六种,ISelect 接口方式
//jdk6,7用法,创建接口
Page<User> page = PageHelper.startPage(1, 10).doSelectPage(new ISelect() {
@Override
public void doSelect() {
userMapper.selectGroupBy();
}
});
//jdk8 lambda用法
Page<User> page = PageHelper.startPage(1, 10).doSelectPage(()-> userMapper.selectGroupBy());

//也可以直接返回PageInfo,注意doSelectPageInfo方法和doSelectPage
pageInfo = PageHelper.startPage(1, 10).doSelectPageInfo(new ISelect() {
@Override
public void doSelect() {
userMapper.selectGroupBy();
}
});
//对应的lambda用法
pageInfo = PageHelper.startPage(1, 10).doSelectPageInfo(() -> userMapper.selectGroupBy());

//count查询,返回一个查询语句的count数
long total = PageHelper.count(new ISelect() {
@Override
public void doSelect() {
userMapper.selectLike(user);
}
});
//lambda
total = PageHelper.count(()->userMapper.selectLike(user));
- 拓展 *
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
java复制代码//获取第1页,10条内容,默认查询总数count
PageHelper.startPage(1, 10);
List<User> list = userMapper.selectAll();
//用PageInfo对结果进行包装
PageInfo page = new PageInfo(list);
//测试PageInfo全部属性
//PageInfo包含了非常全面的分页属性
assertEquals(1, page.getPageNum());
assertEquals(10, page.getPageSize());
assertEquals(1, page.getStartRow());
assertEquals(10, page.getEndRow());
assertEquals(183, page.getTotal());
assertEquals(19, page.getPages());
assertEquals(1, page.getFirstPage());
assertEquals(8, page.getLastPage());
assertEquals(true, page.isFirstPage());
assertEquals(false, page.isLastPage());
assertEquals(false, page.isHasPreviousPage());
assertEquals(true, page.isHasNextPage());
* 这种方式十分方便快捷好用!推荐使用! * 篇幅有限,不可能所有用法都演示一遍!有兴趣的小伙伴可以自行测试一遍! * 悄悄的说,反正我是全部试了一遍,还整合了Spring,加了拦截器测试了!

路漫漫其修远兮,吾必将上下求索~

如果你认为i博主写的不错!写作不易,请点赞、关注、评论给博主一个鼓励吧~hahah

本文转载自: 掘金

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

基于微信支付SDK实现Java后端接口使用

发表于 2021-07-22

微信支付—SDK与DEMO下载

先看README.md
在这里插入图片描述

微信支付 Java SDK


对微信支付开发者文档中给出的API进行了封装。

com.github.wxpay.sdk.WXPay类下提供了对应的方法:

方法名 说明
microPay 刷卡支付
unifiedOrder 统一下单
orderQuery 查询订单
reverse 撤销订单
closeOrder 关闭订单
refund 申请退款
refundQuery 查询退款
downloadBill 下载对账单
report 交易保障
shortUrl 转换短链接
authCodeToOpenid 授权码查询openid
  • 注意:
  • 证书文件不能放在web服务器虚拟目录,应放在有访问权限控制的目录中,防止被他人下载
  • 建议将证书文件名改为复杂且不容易猜测的文件名
  • 商户服务器要做好病毒和木马防护工作,不被非法侵入者窃取证书文件
  • 请妥善保管商户支付密钥、公众帐号SECRET,避免密钥泄露
  • 参数为Map<String, String>对象,返回类型也是Map<String, String>
  • 方法内部会将参数会转换成含有appid、mch_id、nonce_str、sign\_type和sign的XML
  • 可选HMAC-SHA256算法和MD5算法签名
  • 通过HTTPS请求得到返回数据后会对其做必要的处理(例如验证签名,签名错误则抛出异常)
  • 对于downloadBill,无论是否成功都返回Map,且都含有return_code和return_msg,若成功,其中return_code为SUCCESS,另外data对应对账单数据

示例

配置类MyConfig:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
java复制代码import com.github.wxpay.sdk.WXPayConfig;
import java.io.*;

public class MyConfig implements WXPayConfig{

private byte[] certData;

public MyConfig() throws Exception {
String certPath = "/path/to/apiclient_cert.p12";
File file = new File(certPath);
InputStream certStream = new FileInputStream(file);
this.certData = new byte[(int) file.length()];
certStream.read(this.certData);
certStream.close();
}

public String getAppID() {
return "wx8888888888888888";
}

public String getMchID() {
return "12888888";
}

public String getKey() {
return "88888888888888888888888888888888";
}

public InputStream getCertStream() {
ByteArrayInputStream certBis = new ByteArrayInputStream(this.certData);
return certBis;
}

public int getHttpConnectTimeoutMs() {
return 8000;
}

public int getHttpReadTimeoutMs() {
return 10000;
}
}

统一下单:

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
java复制代码import com.github.wxpay.sdk.WXPay;

import java.util.HashMap;
import java.util.Map;

public class WXPayExample {

public static void main(String[] args) throws Exception {

MyConfig config = new MyConfig();
WXPay wxpay = new WXPay(config);

Map<String, String> data = new HashMap<String, String>();
data.put("body", "腾讯充值中心-QQ会员充值");
data.put("out_trade_no", "2016090910595900000012");
data.put("device_info", "");
data.put("fee_type", "CNY");
data.put("total_fee", "1");
data.put("spbill_create_ip", "123.12.12.123");
data.put("notify_url", "http://www.example.com/wxpay/notify");
data.put("trade_type", "NATIVE"); // 此处指定为扫码支付
data.put("product_id", "12");

try {
Map<String, String> resp = wxpay.unifiedOrder(data);
System.out.println(resp);
} catch (Exception e) {
e.printStackTrace();
}
}

}

订单查询:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
java复制代码import com.github.wxpay.sdk.WXPay;

import java.util.HashMap;
import java.util.Map;

public class WXPayExample {

public static void main(String[] args) throws Exception {

MyConfig config = new MyConfig();
WXPay wxpay = new WXPay(config);

Map<String, String> data = new HashMap<String, String>();
data.put("out_trade_no", "2016090910595900000012");

try {
Map<String, String> resp = wxpay.orderQuery(data);
System.out.println(resp);
} catch (Exception e) {
e.printStackTrace();
}
}

}

退款查询:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
java复制代码import com.github.wxpay.sdk.WXPay;

import java.util.HashMap;
import java.util.Map;

public class WXPayExample {

public static void main(String[] args) throws Exception {

MyConfig config = new MyConfig();
WXPay wxpay = new WXPay(config);

Map<String, String> data = new HashMap<String, String>();
data.put("out_trade_no", "2016090910595900000012");

try {
Map<String, String> resp = wxpay.refundQuery(data);
System.out.println(resp);
} catch (Exception e) {
e.printStackTrace();
}
}

}

下载对账单:

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
java复制代码import com.github.wxpay.sdk.WXPay;

import java.util.HashMap;
import java.util.Map;

public class WXPayExample {

public static void main(String[] args) throws Exception {

MyConfig config = new MyConfig();
WXPay wxpay = new WXPay(config);

Map<String, String> data = new HashMap<String, String>();
data.put("bill_date", "20140603");
data.put("bill_type", "ALL");

try {
Map<String, String> resp = wxpay.downloadBill(data);
System.out.println(resp);
} catch (Exception e) {
e.printStackTrace();
}
}

}

其他API的使用和上面类似。

暂时不支持下载压缩格式的对账单,但可以使用该SDK生成请求用的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
java复制代码import com.github.wxpay.sdk.WXPay;
import com.github.wxpay.sdk.WXPayUtil;

import java.util.HashMap;
import java.util.Map;

public class WXPayExample {

public static void main(String[] args) throws Exception {

MyConfig config = new MyConfig();
WXPay wxpay = new WXPay(config);

Map<String, String> data = new HashMap<String, String>();
data.put("bill_date", "20140603");
data.put("bill_type", "ALL");
data.put("tar_type", "GZIP");

try {
data = wxpay.fillRequestData(data);
System.out.println(WXPayUtil.mapToXml(data));
} catch (Exception e) {
e.printStackTrace();
}
}

}

收到支付结果通知时,需要验证签名,可以这样做:

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
java复制代码
import com.github.wxpay.sdk.WXPay;
import com.github.wxpay.sdk.WXPayUtil;

import java.util.Map;

public class WXPayExample {

public static void main(String[] args) throws Exception {

String notifyData = "...."; // 支付结果通知的xml格式数据

MyConfig config = new MyConfig();
WXPay wxpay = new WXPay(config);

Map<String, String> notifyMap = WXPayUtil.xmlToMap(notifyData); // 转换成map

if (wxpay.isPayResultNotifySignatureValid(notifyMap)) {
// 签名正确
// 进行处理。
// 注意特殊情况:订单已经退款,但收到了支付结果成功的通知,不应把商户侧订单状态从退款改成支付成功
}
else {
// 签名错误,如果数据里没有sign字段,也认为是签名错误
}
}

}

HTTPS请求可选HMAC-SHA256算法和MD5算法签名:

1
2
3
4
5
6
7
8
9
10
11
arduino复制代码import com.github.wxpay.sdk.WXPay;
import com.github.wxpay.sdk.WXPayConstants;

public class WXPayExample {

public static void main(String[] args) throws Exception {
MyConfig config = new MyConfig();
WXPay wxpay = new WXPay(config, WXPayConstants.SignType.HMACSHA256);
// ......
}
}

若需要使用sandbox环境:

1
2
3
4
5
6
7
8
9
10
11
12
arduino复制代码import com.github.wxpay.sdk.WXPay;
import com.github.wxpay.sdk.WXPayConstants;

public class WXPayExample {

public static void main(String[] args) throws Exception {
MyConfig config = new MyConfig();
WXPay wxpay = new WXPay(config, WXPayConstants.SignType.MD5, true);
// ......
}

}

本文转载自: 掘金

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

Python 多线程教程|Python 主题月

发表于 2021-07-22

本文正在参加「Python主题月」,详情查看 活动链接

在这个 Python 多线程教程中,您将看到创建线程的不同方法,并学习实现线程安全操作的同步。这篇文章的每个部分都包含一个示例和示例代码,以逐步解释该概念。

顺便说一下,多线程是几乎所有高级编程语言都支持的软件编程的核心概念。因此,您应该知道的第一件事是:什么是线程以及多线程在计算机科学中意味着什么。

什么是计算机科学中的线程?

在软件编程中,线程是具有独立指令集的最小执行单元。它是进程的一部分,并在共享程序的可运行资源(如内存)的相同上下文中运行。一个线程有一个起点、一个执行顺序和一个结果。它有一个指令指针,用于保存线程的当前状态并控制接下来按什么顺序执行。

什么是计算机科学中的多线程?

一个进程并行执行多个线程的能力称为多线程。理想情况下,多线程可以显着提高任何程序的性能。而且 Python 多线程机制非常人性化,您可以快速学习。

多线程的优点

  • 多线程可以显着提高多处理器或多核系统的计算速度,因为每个处理器或核同时处理一个单独的线程。
  • 多线程允许程序在一个线程等待输入时保持响应,同时另一个线程运行 GUI。此陈述适用于多处理器或单处理器系统。
  • 进程的所有线程都可以访问其全局变量。如果一个全局变量在一个线程中发生变化,那么它对其他线程也是可见的。线程也可以有自己的局部变量。

多线程的缺点

  • 在单处理器系统上,多线程不会影响计算速度。由于管理线程的开销,性能可能会下降。
  • 访问共享资源时需要同步以防止互斥。它直接导致更多的内存和 CPU 利用率。
  • 多线程增加了程序的复杂性,从而也使得调试变得困难。
  • 它增加了潜在死锁的可能性。
  • 当线程无法定期访问共享资源时,它可能会导致饥饿。应用程序将无法恢复其工作。

到目前为止,您已经阅读了有关线程的理论概念。如果您不熟悉 Python,我们建议您阅读我们的 30 个快速 Python 编码技巧,它们也可以帮助您编写 Python 多线程代码。我们的许多读者都使用了这些技巧,并且能够提高他们的编码技能。

Python 多线程模块

Python 提供了两个模块来在程序中实现线程。

  • ** **模块和
  • ***<线程>* **模块。

注意:供您参考,Python 2.x 曾经有 < thread> 模块。但它在 Python 3.x 中被弃用并重*命名为 < *_thread> 模块以实现向后兼容性。

两个模块的主要区别在于模块<_线程>将线程实现为函数。另一方面,< threading >模块提供了一种面向对象的方法来启用线程创建。

如何使用线程模块创建线程?

如果你决定在你的程序中应用< thread > 模块,那么使用下面的方法来产生线程。

1
2
3
python复制代码#语法

thread.start_new_thread ( function, args[, kwargs] )

这种方法对于创建线程非常有效和直接。您可以使用它在 Linux 和 Windows 中运行程序。

此方法启动一个新线程并返回其标识符。它将使用传递的参数列表调用指定为“函数”参数的函数。当 < function > 返回时,线程将静默退出。

这里,args*是一个参数元组;使用空元组调用 < *function > 不带任何参数。可选的 < kwargs > 参数指定关键字参数的字典。

**如果 < function > 因未处理的异常而终止,则会打印堆栈跟踪,然后线程退出(它不会影响其他线程,它们会继续运行)。使用以下代码了解有关线程的更多信息。

基本的 Python 多线程示例

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
python复制代码#Python 多线程示例。
#1. 使用递归计算阶乘。
#2. 使用线程调用阶乘函数。

from _thread import start_new_thread
from time import sleep

threadId = 1 #线程计数器
waiting = 2 #2秒等待的时间

def factorial(n):
global threadId
rc = 0

if n < 1: # base case
print("{}: {}".format('\nThread', threadId ))
threadId += 1
rc = 1
else:
returnNumber = n * factorial( n - 1 ) # recursive call
print("{} != {}".format(str(n), str(returnNumber)))
rc = returnNumber

return rc

start_new_thread(factorial, (5, ))
start_new_thread(factorial, (4, ))

print("Waiting for threads to return...")
sleep(waiting)

您可以在本地 Python 终端中运行上述代码,也可以使用任何在线 Python 终端。执行此程序后,它将产生以下输出。

程序输出

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
python复制代码# Python 多线程:程序输出-
等待线程返回...

Thread: 1
1 != 1
2 != 2
3 != 6
4 != 24
5 != 120

Thread: 2
1 != 1
2 != 2
3 != 6
4 != 24

如何使用线程模块创建线程?

最新的< threading >模块比上一节讨论的遗留< thread >模块提供了丰富的特性和更好的线程支持。< threading > 模块是 Python 多线程的一个很好的例子。

< threading > 模块结合了 < thread > 模块的所有方法,并暴露了一些额外的方法

  • threading.activeCount(): 它找到总数。活动线程对象。
  • threading.currentThread(): 您可以使用它来确定调用方线程控制中的线程对象数量。
  • threading.enumerate(): 它将为您提供当前活动的线程对象的完整列表。

除了上述方法,< threading >模块还提供了< Thread >类,你可以尝试实现线程。它是 Python 多线程的面向对象的变体。

< Thread > 类发布以下方法。

类方法 方法说明
run(): 它是任何线程的入口点函数。
start(): start() 方法在 run 方法被调用时触发一个线程。
join([time]): join() 方法使程序能够等待线程终止。
isAlive(): isAlive() 方法验证活动线程。
getName(): getName() 方法检索线程的名称。
setName(): setName() 方法更新线程的名称。

使用线程模块实现线程的步骤

您可以按照以下步骤使用 < threading > 模块实现一个新线程。

  • 从 < Thread > 类构造一个子类。
  • 覆盖 < *init(self [,args])* > 方法以根据要求提供参数。
  • 接下来,重写< run(self [,args]) > 方法来编写线程的业务逻辑。

一旦定义了新的 < Thread> 子类,就必须实例化它以启动一个新线程。然后,调用 < start()> 方法来启动它。它最终会调用< run()> 方法来执行业务逻辑。

示例 – 创建一个线程类来打印日期

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
python复制代码#Python 多线程示例打印当前日期。
#1. 使用 threading.Thread 类定义子类。
#2. 实例化子类并触发线程。

import threading
import datetime

class myThread (threading.Thread):
def __init__(self, name, counter):
threading.Thread.__init__(self)
self.threadID = counter
self.name = name
self.counter = counter
def run(self):
print("\nStarting " + self.name)
print_date(self.name, self.counter)
print("Exiting " + self.name)

def print_date(threadName, counter):
datefields = []
today = datetime.date.today()
datefields.append(today)
print("{}[{}]: {}".format( threadName, counter, datefields[0] ))

# 创建新线程
thread1 = myThread("Thread", 1)
thread2 = myThread("Thread", 2)

# 启动新线程
thread1.start()
thread2.start()

thread1.join()
thread2.join()
print("\nExiting the Program!!!")

程序输出

1
2
3
4
5
6
7
8
9
python复制代码Starting Thread
Thread[1]: 2021-07-22
Exiting Thread

Starting Thread
Thread[2]: 2021-07-22
Exiting Thread

Exiting the Program!!!

Python 多线程——同步线程

< threading > 模块具有实现锁定的内置功能,允许您同步线程。需要锁定来控制对共享资源的访问,以防止损坏或丢失数据。

您可以调用 Lock() 方法来应用锁,它返回新的锁对象。然后,您可以调用锁对象的获取(阻塞) 方法来强制线程同步运行。

可选的阻塞参数指定线程是否等待获取锁。

  • Case Blocking = 0:如果获取锁失败,线程将立即返回零值,如果锁成功则返回一。
  • Case Blocking = 1:线程阻塞并等待锁被释放。

锁对象的release() 方法用于在不再需要时释放锁。

仅供参考,Python 的内置数据结构(例如列表、字典)是线程安全的,因为它具有用于操作它们的原子字节码的副作用。在 Python 中实现的其他数据结构或基本类型(如整数和浮点数)没有这种保护。为了防止同时访问一个对象,我们使用了一个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
Python复制代码#Python 多线程示例来演示锁定。
#1. 使用 threading.Thread 类定义子类。
#2. 实例化子类并触发线程。
#3. 在线程的 run 方法中实现锁。

import threading
import datetime

exitFlag = 0

class myThread (threading.Thread):
def __init__(self, name, counter):
threading.Thread.__init__(self)
self.threadID = counter
self.name = name
self.counter = counter
def run(self):
print("\nStarting " + self.name)
# 获取锁同步线程
threadLock.acquire()
print_date(self.name, self.counter)
# 为下一个线程释放锁
threadLock.release()
print("Exiting " + self.name)

def print_date(threadName, counter):
datefields = []
today = datetime.date.today()
datefields.append(today)
print("{}[{}]: {}".format( threadName, counter, datefields[0] ))

threadLock = threading.Lock()
threads = []

# 创建新线程
thread1 = myThread("Thread", 1)
thread2 = myThread("Thread", 2)

# 启动新线程
thread1.start()
thread2.start()

# 添加线程到线程列表
threads.append(thread1)
threads.append(thread2)

# 等待所有线程完成
for thread in threads:
thread.join()

print("\nExiting the Program!!!")

程序输出

1
2
3
4
5
6
7
8
9
Python复制代码Starting Thread
Thread[1]: 2021-07-22
Exiting Thread

Starting Thread
Thread[2]: 2021-07-22
Exiting Thread

Exiting the Program!!!

总结——初学者的 Python 多线程

我希望您会发现这个 Python 多线程教程非常有趣且引人入胜。如果您喜欢这篇文章并有兴趣看到更多此类文章,可以看看这里(Github/Gitee) 关注我以查看更多信息,这里汇总了我的全部原创及作品源码

🧵 更多相关文章

  • Python Socket 编程要点|Python 主题月
  • 30 个 Python 教程和技巧|Python 主题月
  • Python 语句、表达式和缩进|Python 主题月
  • Python 关键字、标识符和变量|Python 主题月
  • 如何在 Python 中编写注释和多行注释|Python 主题月
  • 每个人都必须知道的 20 个 Python 技巧|Python 主题月
  • Python 数据类型——从基础到高级学习|Python 主题月
  • 100 个基本 Python 面试问题第一部分(1-20)|Python 主题月
  • 100 个基本 Python 面试问题第二部分(21-40)|Python 主题月
  • 100 个基本 Python 面试问题第三部分(41-60)|Python 主题月
  • 100 个基本 Python 面试问题第四部分(61-80)|Python 主题月
  • 100 个基本 Python 面试问题第五部分(81-100)|Python 主题月

往日优秀文章推荐:

  • 教你用Java做出一个五子棋小游戏
  • 用一种有趣的方式谈谈 JavaScript 的发展历史 ⌛
  • 【Java练习题】Java程序的输出 | 第七套(含解析)
  • ❤️5 个使重构变得容易的 VS Code 扩展❤️(建议收藏)
  • 有人说,如果你的眼中有光,所到之处皆是光明 | 2021年中总结
  • 14万字 | 400 多道 JavaScript 面试题 🎓 有答案 🌠(第五部分 371-424题)

如果你真的从这篇文章中学到了一些新东西,喜欢它,收藏它并与你的小伙伴分享。🤗最后,不要忘了❤或📑支持一下哦

本文转载自: 掘金

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

「论道架构师」优雅解决历史代码中的新需求

发表于 2021-07-22

⚠️本文为掘金社区首发签约文章,未获授权禁止转载

事件起因

6月中旬,可爱的产品大大给我提了一个临时需求,需要我对商品创建/更新业务中由开放平台对接而来的请求做一个Check,如果符合要求,则再做一段稍微复杂的逻辑处理。

这种Easy程度的需求怎么拦得住我,不到半天我就Coding,Push一气呵成,正当我准备点一杯喜茶开始摸鱼的时候,我却收到了一封邮件。

邮件里有一堆的汉字和英文,但有几个字赫然在目:

您的代码已被驳回。

当我经历了茫然、震惊、不敢相信、最后无奈接受的情绪转变后,问了评审的同事,为什么要驳回我的代码,他说:“历史代码一般业务都很完整(跟屎山一样了…),那如果有新的需求不得不依赖它的话,怎么

做才是最佳方案,让代码有更好的拓展性,你有想过吗?”。

我肯定是没有想的,于是乎,我怀着些许愧疚的心情,找到了架构师,希望他能为我指点迷津。

找一个看起来合适的位置塞进去

亮架构:Kerwin,这段代码是不是偷懒了?

1
2
3
4
5
6
7
8
9
10
11
12
13
java复制代码try {
// 忽略历史业务代码,以下为新增内容
} catch (Exception) {
// TODO
} finally {
SkuMainBean retVal = skuMainBridgeService.updateSkuBridgeMainBean(skuMainBean);
if(retVal != null){
// 商品创建/修改异步处理逻辑
SimpleThreadPool.executeRunnable(SimpleThreadPool.ThreadPoolName.openSkuHandle, () -> {
skuOperateBusinessService.checkOpenSkuReview(retVal);
});
}
}

我(虽然我觉得不妥,但还是强装镇定):没偷懒啊,你看这块业务代码既没有影响原功能,又用线程池的方式异步处理,不会影响整体接口效率,而且还把复杂逻辑都封装到了Business层里,这还叫偷懒吗?

亮架构:你觉得这个商品创建/修改流程重要吗?是不是咱们的最核心的流程?下次产品再提新的需求,继续 if 然后叠罗汉吗?我咋记得你说过你最讨厌在代码里看到 if 呢?

我(小声):我讨厌看到别人的 if,但是自己的还是可以接受的…

亮架构(气笑):不跟你耍贫嘴了,一起想想怎么改吧。

PS:【找一个看起来合适的位置塞进去】这种方式是我们使用最频繁,影响面相对较小,开发效率最高的方式了,但它带来的问题就是后期不好维护,而且随着需求变多,它就会变得和叠罗汉一样,本来一个很简单的方法函数,会变成百上千行的 “屎山”,因此需要酌情使用。

优先校验终止

我(开始思考):如果需求是不满足某种情况即可终止执行,那这种情况可太简单了,就不絮叨了。

亮架构:其实还是有一点可说的,比如你需要在不满足时返回标识符结果加细节原因,你怎么处理?

我:直接定义一个字符串然后返回,后续判断字符串是否为NULL即可。

亮架构:如果就是失败了,且原因也为NULL或空字符串呢?其实我们利用泛型有更优雅的解决方案,比如这样定义一个元组:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
java复制代码public class ValueMsgReturn<A, B> {
/** 结果 **/
private final A value;

/** 原因 **/
private final B msg;

public ValueMsgReturn(A value, B msg) {
this.value = value;
this.msg = msg;
}

// 省略Get方法
}

这样做的好处是,通用,简单,不必定义重复的对象,你自己在代码中试试就能明白它有多香,整体代码就如下所示:

1
2
3
4
5
java复制代码// 省略干扰代码
ValueMsgReturn<Boolean, String> check = check();
if (check.getValue()) {
return check.getValue();
}

PS:此种情况较为简单,但仍然有技巧优化代码,详情请见历史文章:

「奇淫技巧」如何写最少的代码

简单观察者模式

我(继续思考):你刚那种情况太简单了,回归正题,咱们这个需求可以使用观察者模式解耦啊!

亮架构(犹豫道):不是不可以,但你想一下我们需要改动哪些代码吧。

我:观察者的核心即通知方 + 处理方,如果我们使用JDK自带的观察者模式的话,改动如下:

  1. 需要将历史代码中的类继承Observable类
  2. 新的处理方法基于单一原则抽象成单独的类,实现Observer接口
  3. 在类初始化时把二者构建好通知关系

亮架构:如果一段逻辑在设计之初就采用观察者模式的话,那还不错,但历史代码则不适合,因为它一个类里面包含大量的其他方法,如果未来需求中有第二种需要通知的情况,代码就会更难维护,毕竟JDK观察者模式是需要继承Observable类的,当然了,作为一个备选方案也不是不行。

PS:以上描述的JDK观察者模式对应的是JDK1.8版本,关于观察者模式的详情,请见历史文章

【一起学系列】之观察者模式:我没有在监控你啊

AOP

我(突然想起来):亮架构,你说用AOP来处理合适吗?

亮架构:一般情况下我们用AOP来做什么动作呢?

我:我的话,一般会用作权限处理、日志打印、缓存同步、特殊场景计数等等。

亮架构:是的,你想象一下如果我们把这些业务逻辑都堆在切面里会是什么样子?一个切点还好,两个呢,十个呢?大家拿到新项目的时候都会参考前人的代码风格,如果你开了一个坏的头,其他人就会跟着做同样的事,很快代码就会变成如同蜘蛛网一般,所以这种方式一定是要杜绝的。

MQ 解耦

我(突然想起来):对了,咱们的商品新建/修改都会有MQ的,我只用监听MQ然后做业务处理就好了。

亮架构:这个肯定是可行的,就是有点杀鸡焉用宰牛刀的感觉,毕竟我们需要处理的情况只是MQ中的一小部分,而且万一历史代码没有发送MQ怎么办呢?

Spring Event

亮架构:你有了解过Spring Event吗?

我:以前研究过,确实用在这里还蛮合适的。

PS:Spring Event是Spring体系中的事件通知机制,其原理可以理解为Spring实现的观察者模式。

注:上文中的简单观察者模式指的是JDK(1.8)实现的观察者模式。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
java复制代码// 以下为Demo代码
@RestController
public class EventRequest implements ApplicationContextAware {

private ApplicationContext appContext;

@RequestMapping("/testEvent")
public String testEventSave(String name) {
appContext.publishEvent(new User(this, name));
return "ok";
}

@Override
public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
appContext = applicationContext;
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
java复制代码// 监听者
@Component
public class WebEventListener {

/**
* 仅监听字段值为 foo 时,类为 User.class 时
*/
@EventListener(classes = User.class, condition = "#event.name == 'foo'")
public void listen(User event){
// TODO
}

/**
* 监听 User.class 情况
*/
@EventListener(classes = User.class)
public void listen1(User event){
// TODO
}
}

亮架构:是的,这个Demo就很能反映它的优势之处了

  1. 我们可以在单一方法内Publish多个事件,互不干扰
  2. 监听者可以基于表达式进行基本的过滤
  3. 一个事件可以被重复监听

我:是的,而且它还可以支持异步事件处理!

亮架构(停顿了一下):你觉得支持异步是它独特的优势吗?哈哈哈,即使是同步监听到事件,你只要用线程池异步处理就好了。能够天然异步化,只是锦上添花的东西,不要弄混了哦。当然了,每种技术和特性都有其独特的使用场景,在使用的时候需要注意它的特殊情况,比如:

  1. 业务上是否允许异步处理(即使是延迟了比较久的时间)
  2. 能否完全相信事件通知里面的参数,是否需要反查等等。

还有别的方式吗

我(开心):如果我用Spring Event的话,我只需要稍微改动一下就好了,代码的拓展性,可维护性一下子就上来了,不过刚咱们聊了那么多方式方法,怎么感觉全是观察者模式啊?

亮架构:是的,无论是JDK的还是Spring,亦或是AOP、MQ,这些统统都是观察者模式的思想,毕竟观察者模式的特点就是解耦。

我:难道不能用别的设计模式思想吗?

亮架构:当然可以,就是改动可能略大一点,毕竟这个类都快几千行了,还是尽量少加东西了。

我:比如呢,可以用什么其他的方式?

亮架构:额…你既然想听的话,可以这样,回顾一下你最初的代码:

1
2
3
4
5
6
7
8
9
java复制代码finally {
SkuMainBean retVal = skuMainBridgeService.updateSkuBridgeMainBean(skuMainBean);
if(retVal != null){
// 商品创建/修改异步处理逻辑
SimpleThreadPool.executeRunnable(SimpleThreadPool.ThreadPoolName.openSkuHandle, () -> {
skuOperateBusinessService.checkOpenSkuReview(retVal);
});
}
}

在这个业务方法里处理的肯定是skuMainBean对象,因为整个方法都是在操作它,那我们完全可以抽象出一个个策略类,然后利用工厂来处理,比如改成这样:

1
2
3
4
5
6
7
8
9
10
11
12
13
java复制代码// 修改后代码
finally {
skuMainBeanFactory.checkAndHandle(skuMainBean);
}

// 工厂方法
public void checkAndHandle (SkuMainBean skuMainBean) {
for (策略集合: 策略) {
if (check(skuMainBean)) {
// TODO
}
}
}

亮架构:你看这样是不是也具有很好的拓展性?

我(兴奋):是的,我突然感觉这种方式和SpringEvent有异曲同工之妙!

亮架构(笑了笑):孺子可教也,这种策略+工厂的方式是基于接口编程,通过check方法判断是否需要处理,而SpringEvent说白了是通过事件的传播,即方法直接调用来判断是否需要处理,本质都是一样的,那你知道未来的新需求你该怎么写了吗?

我(兴奋):我知道了,要写可拓展性的代码,像我今天改的这种代码就不行,太垃圾了!

亮架构(摇了摇头,起身走了):Kerwin,你错了,你今天改的历史代码在当时可以说是最佳实践了,只是因为你遇到了之前的设计者未考虑到的问题而已。我们讲设计模式、讲七大原则,讲不要过度设计,就是为了你现在出现的情况,我们在编码过程中可能会遇到千奇百怪的代码,我们可以抱怨,可以吐槽,但记住,不要为了某些需求就把本来漂亮的代码变成屎山。所以你需要去学习编程的思想,学习设计的思想。

我(大声):那,架构师!如果有一段代码已经烂到不能再烂了呢!

“那就把它重构了!然后把作者的名字记下来,狠狠的吐槽他!🤪”

最后

回顾全文做一个总结,如果你的需求是允许前置校验返回的,那么毫不犹豫的CheckAndReturn即可!但是,如果你的需求和我一样,那么推荐以下几种方案:

  1. 利用MQ解耦
  2. 利用SpringEvent解耦
  3. 自行根据当前需求和未来可能的需求考虑是否需要策略类
  4. 终极方案:真正理解编程的七大原则及常用的设计模式、随机应变即可

那么请允许我推荐一下之前的文章:设计模式总篇:从为什么需要原则到实际落地

如果你觉得这篇内容对你有帮助的话:

  1. 当然要点赞支持一下啦~
  2. 另外,可以搜索并关注公众号「是Kerwin啊」,一起在技术的路上走下去吧~ 😋

本文转载自: 掘金

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

IO多路复用之select、poll、epoll的区别

发表于 2021-07-21

I/O多路复用(multiplexing)的本质是通过一种机制(系统内核缓冲I/O数据),让单个进程可以监视多个文件描述符,一旦某个描述符就绪(一般是读就绪或写就绪),能够通知程序进行相应的读写操作。

select 函数监视的文件描述符分3类,分别是writefds、readfds、和exceptfds。调用后select函数会阻塞,直到有描述符就绪(有数据 可读、可写、或者有except),或者超时(timeout指定等待时间,如果立即返回设为null即可),函数返回。当select函数返回后,可以通过遍历fdset,来找到就绪的描述符。

select本质上是通过设置或者检查存放fd标志位的数据结构来进行下一步处理。

缺点:

1、 单个进程可监视的fd数量被限制,即能监听端口的大小有限。

一般来说这个数目和系统内存关系很大,具体数目可以cat /proc/sys/fs/file-max察看。32位机默认是1024个。64位机默认是2048.

2、 对socket进行扫描时是线性扫描,即采用轮询的方法,效率较低:

当套接字比较多的时候,每次select()都要通过遍历FD\_SETSIZE个Socket来完成调度,不管哪个Socket是活跃的,都遍历一遍。这会浪费很多CPU时间。如果能给套接字注册某个回调函数,当他们活跃时,自动完成相关操作,那就避免了轮询,这正是epoll与kqueue做的。

3、需要维护一个用来存放大量fd的数据结构,每次调用select时把fd集合从用户态拷贝到内核态,这样会使得用户空间和内核空间在传递该结构时复制开销大。

和select没有区别,它将用户传入的数组拷贝到内核空间,然后查询每个fd对应的设备状态,如果设备就绪则在设备等待队列中加入一项并继续遍历,如果遍历完所有fd后没有发现就绪设备,则挂起当前进程,直到设备就绪或者主动超时,被唤醒后它又要再次遍历fd。这个过程经历了多次无谓的遍历。

它没有最大连接数的限制,原因是它是基于链表来存储的。

缺点:

1、大量的fd的数组被整体复制于用户态和内核地址空间之间,而不管这样的复制是不是

有意义。

2、poll还有一个特点是“水平触发”,如果报告了fd后,没有被处理,那么下次poll时会再次报告该fd。

LT模式:level trigger。当epoll_wait检测到描述符事件发生并将此事件通知应用程序,应用程序可以不立即处理该事件。下次调用epoll_wait时,会再次响应应用程序并通知此事件。

ET模式:edge trigger。当epoll_wait检测到描述符事件发生并将此事件通知应用程序,应用程序必须立即处理该事件。如果不处理,下次调用epoll_wait时,不会再次响应应用程序并通知此事件。

1
2
3
4
5
arduino复制代码int epoll_create(int size);

int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);

int epoll_wait(int epfd, struct epoll_event * events, int maxevents, int timeout);

epoll_create:创建一个epoll的句柄,size用来告诉内核这个监听的数目一共有多大。参数size并不是限制了epoll所能监听的描述符最大个数,只是对内核初始分配内部数据结构的一个建议。

epoll_ctl:对指定描述符fd执行op操作。

-epfd:是epoll_create()的返回值。

-op操作:对应宏:添加EPOLL_CTL_ADD,删除EPOLL_CTL_DEL,修改EPOLL_CTL_MOD,对应添加、删除和修改对fd的监听事件。

  • fd:是需要监听的fd(文件描述符)。
  • epoll_event:是告诉内核需要监听什么事件(读、写事件等)。

epoll_wait:等待epfd上的io事件,最多返回maxevents个事件。

-events:用来从内核得到事件的集合,

-maxevents:告之内核这个events有多大,这个maxevents的值不能大于创建epoll_create()时的size,

-timeout:是超时时间。

epoll有EPOLLLT和EPOLLET两种触发模式,LT是默认的模式,ET是“高速”模式。LT模式下,只要这个fd还有数据可读,每次 epoll_wait都会返回它的事件,提醒用户程序去操作,而在ET(边缘触发)模式中,它只会提示一次,直到下次再有数据流入之前都不会再提示了,无论fd中是否还有数据可读。所以在ET模式下,read一个fd的时候一定要把它的buffer读光,也就是说一直读到read的返回值小于请求值,或者遇到EAGAIN错误。还有一个特点是,epoll使用“事件”的就绪通知方式,通过epoll_ctl注册fd,一旦该fd就绪,内核就会采用类似callback的回调机制来激活该fd,epoll_wait便可以收到通知。

epoll为什么要有EPOLLET触发模式?

如果采用EPOLLLT模式的话,系统中一旦有大量你不需要读写的就绪文件描述符,它们每次调用epoll_wait都会返回,这样会大大降低处理程序检索自己关心的就绪文件描述符的效率.。而采用EPOLLET这种边沿触发模式的话,当被监控的文件描述符上有可读写事件发生时,epoll_wait()会通知处理程序去读写。如果这次没有把数据全部读写完(如读写缓冲区太小),那么下次调用epoll_wait()时,它不会通知你,也就是它只会通知你一次,直到该文件描述符上出现第二次可读写事件才会通知你!!!这种模式比水平触发效率高,系统不会充斥大量你不关心的就绪文件描述符。

epoll优点:

1、没有最大并发连接的限制,能打开的FD的上限远大于1024(1G的内存上能监听约10万个端口);

2、效率提升,不是轮询的方式,不会随着FD数目的增加效率下降。只有活跃可用的FD才会调用callback函数;

即Epoll最大的优点就在于它只管你“活跃”的连接,而跟连接总数无关,因此在实际的网络环境中,Epoll的效率就会远远高于select和poll。

3、 内存拷贝,利用mmap()文件映射内存加速与内核空间的消息传递;即epoll使用mmap减少复制开销。

0、底层数据结构

select:数组,poll:链表,epoll:红黑树。

1、支持一个进程所能打开的最大连接数

select 单个进程所能打开的最大连接数有FD_SETSIZE宏定义,其大小是32个整数的大小(在32位的机器上,大小就是32*32,同理64位机器上FD_SETSIZE为32*64),当然我们可以对进行修改,然后重新编译内核,但是性能可能会受到影响,这需要进一步的测试。

poll本质上和select没有区别,但是它没有最大连接数的限制,原因是它是基于链表来存储的。

epoll 虽然连接数有上限,但是很大,1G内存的机器上可以打开10万左右的连接,2G内存的机器可以打开20万左右的连接。

2、FD剧增后带来的IO效率问题

select/poll 因为每次调用时都会对连接进行线性遍历,所以随着FD的增加会造成遍历速度慢的“线性下降性能问题”。

epoll 因为epoll内核中实现是根据每个fd上的callback函数来实现的,只有活跃的socket才会主动调用callback,所以在活跃socket较少的情况下,使用epoll没有前面两者的线性下降的性能问题,但是所有socket都很活跃的情况下,可能会有性能问题。

3、消息传递方式

select/poll 内核需要将消息传递到用户空间,都需要内核拷贝动作。

epoll通过内核和用户空间共享一块内存来实现的。

select、poll与epoll之间的区别总结图:

历史背景:

1)select出现是1984年在BSD里面实现的。

2)14年之后也就是1997年才实现了poll,其实拖那么久也不是效率问题, 而是那个时代的硬件实在太弱,一台服务器处理1千多个链接简直就是神一样的存在了,select很长段时间已经满足需求 。

3)2002, 大神 Davide Libenzi 实现了epoll。

参考资料:

www.cnblogs.com/Anker/p/326…

www.cnblogs.com/aspirant/p/…

www.cnblogs.com/dhcn/p/1273…

笔记系列

笔记| Java对象探秘

笔记| JVM内存区域结构:一计两栈一堆一区

笔记| 面试官问我高并发的问题:并发编程的三大挑战

笔记| 面试官问我:TCP与UDP的区别

笔记| 网络编程基础:TCP如何保证可靠性

笔记| 面试又挂了,只因问了:TCP三次握手和四次挥手

笔记| 5种网络IO模型

本文转载自: 掘金

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

五十音小游戏中的前端知识

发表于 2021-07-21

本文已参与好文召集令活动,点击查看:后端、大前端双赛道投稿,2万元奖池等你挑战!

背景

在日语学习初期阶段,我发现日语五十音的记忆并不是很容易的,片假名的记忆尤其令人费神。这时我想如果有一个应用可以充分利用碎片时间,在午休或地铁上随时可以练习五十音该多好。于是搜索 App Store,确实有很多五十音学习的小软件,但是商店的软件不是含有内购、夹带广告、就是动辄 40M 以上,没找到一个自己满意的应用。于是打算自己写一个,主要介绍自己在开发设计该应用过程中的一些收获。

实现

在线体验地址 dragonir.github.io/kanaApp/

实现效果如下,该应用主要分为三个页面:

  • 首页:包括菜单选项(平假名练习、片假名练习、混合练习)、深色模式切换按钮。
  • 答题页:包括剩余机会和分数显示区、中间出题区、底部答题按钮。
  • 结果页:结果分数显示和返回首页按钮。

答题逻辑规则是从给出的 4 个答案按钮中选出题目展示区的那个单词对应正确的那个选项,应用根据点击给出错对反馈并进行记分,错误 10 次后游戏结束,加载结果页。游戏逻辑实现不是本文的主要内容,因此后面不再赘述。本文后续主要内容是此次小游戏开发流程涉及到的前端知识的介绍。

app.gif

深色模式 ⚪⚫

随着 Windows 10、 MacOs、 Android 等系统陆续推出深色模式,浏览器也开始支持检测系统主题色配置,越来越多的网页应用都配置了深色模式切换功能。为了优化 50音小游戏 的视觉体验,我也配置了深色样式,实现效果如下:

dark.png

CSS 媒体查询判断深色模式

prefers-color-scheme 媒体特性用于检测用户是否有将系统的主题色设置为亮色或者暗色。使用语法如下所示:

@media (prefers-color-scheme: value) {} 其中 value 有以下 3 种值,其中:

  • light:表示用户系统支持深色模式,并且已设置为浅色主题(默认值)。
  • dark:表示用户系统支持深色模式,并且已设置为深色主题。
  • no-preference:表示用户系统不支持深色模式或无法得知是否设置为深色模式(已废弃)。

若结果为 no-preference,无法通过此媒体特性获知宿主系统是否支持设置主题色,或者用户是否主动将其设置为无偏好。出于隐私保护等方面的考虑,用户或用户代理也可能在一些情况下在浏览器内部将其设置为 no-preference。

下面例子中,当系统主题色为深色时 .demo 元素的背景色为 #FFFFFF;当系统主题色为浅色时,.demo 元素的背景色为 #000000。

1
2
3
4
5
6
css复制代码@media (prefers-color-scheme: dark) {
.demo { background: #FFFFFF; }
}
@media (prefers-color-scheme: light) {
.demo { background: #000000; }
}

JavaScript 判断深色模式

window.matchMedia() 方法返回一个新的 MediaQueryList 对象,表示指定的媒体查询 (en-US)字符串 解析后的结果。返回的 MediaQueryList 可被用于判定 Document 是否匹配媒体查询,或者监控一个 document 来判定它匹配了或者停止匹配了此媒体查询。其中 MediaQueryList 对象具有属性 matches和 media,方法 addListener 和 removeListener。

使用 matchMedia 作为判断媒介,也可以识别系统是否支持主题色:

1
2
3
4
5
6
7
8
js复制代码if (window.matchMedia('(prefers-color-scheme)').media === 'not all') {
// 浏览器不支持主题色设置
}
if (window.matchMedia('(prefers-color-scheme: dark)').matches){
// 深色模式
} else {
// 浅色模式
}

另外还可以动态监听系统深色模式的状态,根据系统深色模式的切换做出实时响应:

1
2
3
4
5
6
7
js复制代码window.matchMedia('(prefers-color-scheme: dark)').addListener(e => {
if (e.matches) {
// 开启深色模式
} else {
// 关闭深色模式
}
});

或者单独检测深色或浅色模式:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
js复制代码const listeners = {
dark: (mediaQueryList) => {
if (mediaQueryList.matches) {
// 开启深色模式
}
},
light: (mediaQueryList) => {
if (mediaQueryList.matches) {
// 开启浅色模式
}
}
};
window.matchMedia('(prefers-color-scheme: dark)').addListener(listeners.dark);
window.matchMedia('(prefers-color-scheme: light)').addListener(listeners.light);

在50音小游戏中,就是使用 JavaScript 检测系统是否开启深色模式,动态添加 css 类名来自动加载深色模式,同时也提供深浅色切换按钮,可以手动切换主题。

HTML 元素中判断深色模式

页面使用图片元素时,可以直接在 HTML 中判断系统是否开启深色模式。如:

1
2
3
4
html复制代码<picture>
<source srcset="dark.png" media="(prefers-color-scheme: dark)">
<img src="light.png">
</picture>

picture 元素允许我们在不同的设备上显示不同的图片,一般用于响应式。HTML5 引入了 <picture> 元素,该元素可以让图片资源的调整更加灵活。<picture> 元素零或多个 <source> 元素和一个 <img> 元素,每个 <source> 元素匹配不同的设备并引用不同的图像源,如果没有匹配的,就选择 <img> 元素的 src 属性中的 url。

注意: <img> 元素是放在最后一个 <picture> 元素之后,如果浏览器不支持该属性则显示 <img> 元素的的图片。

离线缓存

为了能够像原生应用一样可以在桌面生成快捷方式快速访问,随时随地离线使用,50音小游戏 使用了离线缓存技术,它是一个 PWA应用 。下面内容是 PWA离线应用 实现技术的简要描述。

PWA (progressing web app),渐进式网页应用程序,是 下一代WEB应用模型。一个 PWA 应用首先是一个网页, 并借助于 App Manifest 和 Service Worker 来实现安装和离线等功能。

特点:

  • 渐进式:适用于选用任何浏览器的所有用户,因为它是以渐进式增强作为核心宗旨来开发的。
  • 自适应:适合任何机型:桌面设备、移动设备、平板电脑或任何未来设备。
  • 连接无关性:能够借助于服务工作线程在离线或低质量网络状况下工作。
  • 离线推送:使用推送消息通知,能够让我们的应用像 Native App 一样,提升用户体验。
  • 及时更新:在服务工作线程更新进程的作用下时刻保持最新状态。
  • 安全性:通过 HTTPS 提供,以防止窥探和确保内容不被篡改。

配置页面参数

在项目根目录添加文件 manifest.webmanifest 或 manifest.json 文件,并在文件内写入如下配置信息,本例中 50音小游戏 的页面参数信息配置如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
json复制代码// manifest.webmainifest
{
"name": "かなゲーム",
"short_name": "かなゲーム",
"start_url": "index.html",
"display": "standalone",
"background_color": "#fff",
"description": "かなゲーム",
"icons": [
{
"src": "assets/images/icon-64x64.png",
"sizes": "64x64",
"type": "image/png"
},
{
"src": "assets/images/icon-256x256.png",
"sizes": "256x256",
"type": "image/png"
}
]
}

参数说明:

  • name:Web App 的名称,也是保存到桌面上时应用图标的名称。
  • short_name:name 过长时,将会使用 short_name 代替 name 显示,是 Web App 的简称。
  • start_url:指定了用户打开该 Web App 时加载 URL。URL 会相对于 manifest 文件所在路径。
  • display:指定了应用的显示模式,它有四个值可以选择:
    • fullscreen:全屏显示,会尽可能将所有的显示区域都占满。
    • standalone:浏览器相关 UI(如导航栏、工具栏等)将被隐藏,看起来更像一个 Native App。
    • minimal-ui:显示形式与 standalone 类似,浏览器相关 UI 会最小化为一个按钮,不同浏览器在实现上略有不同。
    • browser:一般来说,会和正常使用浏览器打开样式一致。
    • 需要说明的是,当一些系统的浏览器不支持 fullscreen 时将会显示成 standalone 效果,当不支持 standalone 时,将会显示成 minimal-ui 的效果,以此类推。
  • description:应用描述。
  • icons:指定了应用的桌面图标和启动页图像,用数组表示:
    • sizes:图标大小。通过指定大小,系统会选取最合适的图标展示在相应位置上。
    • src:图标路径。相对路径是相对于 manifest 文件,也可以使用绝对路径。
    • type:图标图片类型。 浏览器会从 icons 中选择最接近 128dp(px = dp * (dpi / 160)) 的图片作为启动画面图像。
  • background_color:指定启动画面的背景颜色,采用相同颜色可以实现从启动画面到首页的平稳过渡,也可以用来改善页面资源正在加载时的用户体验。
  • theme_color:指定了Web App 的主题颜色。可以通过该属性来控制浏览器 UI 的颜色。比如状态栏、内容页中状态栏、地址栏的颜色。

配置信息自动生成工具:tomitm.github.io/appmanifest…

配置 HTML 文件

在 index.html 中引入 manifest 配置文件,并在 head 中添加以下配置信息以兼容 iOS系统

1
2
3
4
5
6
7
8
9
10
11
12
13
html复制代码<meta name=viewport content="width=device-width,initial-scale=1,maximum-scale=1,minimum-scale=1,user-scalable=no,viewport-fit=cover">
<meta name="apple-mobile-web-app-capable" content="yes" />
<meta name="apple-mobile-web-app-status-bar-style" content="black-translucent">
<meta name="apple-mobile-web-app-title" content="かなゲーム">
<link rel="stylesheet" type="text/css" href="./assets/css/main.css">
<link rel="stylesheet" type="text/css" href="./assets/css/dark.css">
<link rel="stylesheet" type="text/css" href="./assets/css/petals.css">
<link rel="shortcut icon" href="./assets/images/icon-256x256.png">
<link rel="apple-touch-icon" href="./assets/images/icon-256x256.png"/>
<link rel="apple-touch-icon-precomposed" href="./assets/images/icon-256x256.png">
<link rel="Bookmark" href="./assets/images/icon-256x256.png" />
<link rel="manifest" href="./manifest.webmanifest">
<title>かなゲーム</title>
  • apple-touch-icon: 指定应用图标,类似与 manifest.json 文件的 icons 配置,也是支持 sizes 属性,来供不同场景的选择。
  • apple-mobile-web-app-capable:类似于 manifest.json 中的 display 的功能,通过设置为 yes 可以进入 standalone 模式。
  • apple-mobile-web-app-title:指定应用的名称。
  • apple-mobile-web-app-status-bar-style:指定iOS移动设备的 状态栏status bar 的样式,有 Default,Black,Black-translucent 可以设置。

注册使用 Service Worker

在 index.html 中添加如下代码进行server-worker注册:

1
2
3
4
5
6
7
8
9
10
11
12
js复制代码window.addEventListener('load', () => {
registerSW();
});
async function registerSW() {
if ('serviceWorker' in navigator) {
try {
await navigator.serviceWorker.register('./sw.js');
} catch (e) {
console.log(`SW registration failed`);
}
}
}

使用 serviceWorkerContainer.register() 进行 Service worker 注册,同时添加 try...catch... 容错判断,以保证在不支持 Service worker 的情况下正常运行。另外需要注意的是只有在 https 下,navigator 里才会有 serviceWorker 对象。

Service workers 本质上充当 Web 应用程序、浏览器与网络(可用时)之间的代理服务器。旨在创建有效的离线体验,它会拦截网络请求并根据网络是否可用采取来适当的动作、更新来自服务器的的资源。它还提供入口以推送通知和访问后台同步 API。了解更多 Service workder 知识可以访问文章末尾链接 🔗。

在根目录添加 sw.js,定义缓存信息和方法

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
js复制代码// 定义缓存的key值
const cacheName = 'kana-v1';
// 定义需要缓存的文件
const staticAssets = [
'./',
'./index.html',
'./assets/css/main.css',
'./assets/js/main.js',
'./assets/images/bg.png'
// ...
];

// 监听install事件,安装完成后,进行文件缓存
self.addEventListener('install', async e => {
// 找到key对应的缓存并且获得可以操作的cache对象
const cache = await caches.open(cacheName);
// 将需要缓存的文件加进来
await cache.addAll(staticAssets);
return self.skipWaiting();
});

// 监听activate事件来更新缓存数据
self.addEventListener('activate', e => {
// 保证第一次加载fetch触发
self.clients.claim();
});

// 监听fetch事件来使用缓存数据:
self.addEventListener('fetch', async e => {
const req = e.request;
const url = new URL(req.url);
if (url.origin === location.origin) {
e.respondWith(cacheFirst(req));
} else {
e.respondWith(networkAndCache(req));
}
});

async function cacheFirst(req) {
// 判断当前请求是否需要缓存
const cache = await caches.open(cacheName);
const cached = await cache.match(req);
// 有缓存就用缓存,没有就从新发请求获取
return cached || fetch(req);
}

async function networkAndCache(req) {
const cache = await caches.open(cacheName);
try {
// 缓存报错还直接从新发请求获取
const fresh = await fetch(req);
await cache.put(req, fresh.clone());
return fresh;
} catch (e) {
const cached = await cache.match(req);
return cached;
}
}

在 sw.js 中采用的标准的 web worker 的编程方式,由于运行在另一个全局上下文中 (self),这个全局上下文不同于 window,所以采用 self.addEventListener()。

Cache API 是 Service Worker 提供用来操作缓存的的接口,这些接口基于 Promise 实现,包括 Cache 和 Cache Storage,Cache 直接和请求打交道,为缓存的 Request / Response 对象对提供存储机制,CacheStorage 表示 Cache 对象的存储实例,我们可以直接使用全局的 caches 属性访问 Cache API。

** Cache 相关 API 说明:

  • Cache.match(request, options):返回一个 Promise 对象,resolve 的结果是跟 Cache 对象匹配的第一个已经缓存的请求。
  • Cache.matchAll(request, options):返回一个 Promise 对象,resolve 的结果是跟 Cache 对象匹配的所有请求组成的数组。
  • Cache.addAll(requests):接收一个 URL 数组,检索并把返回的 response 对象添加到给定的 Cache 对象。
  • Cache.delete(request, options):搜索 key 值为 request 的 Cache 条目。如果找到,则删除该 Cache 条目,并且返回一个 resolve 为 true 的 Promise 对象;如果未找到,则返回一个 resolve 为 false 的 Promise 对象。
  • Cache.keys(request, options):返回一个 Promise 对象,resolve 的结果是 Cache 对象 key 值组成的数组。

注:使用 request.clone() 和 response.clone() 是因为 request 和 response 是一个流,只能消耗一次。缓存时已经消耗一次,再发起 HTTP 请求还要消耗一次,此时使用 clone 方法克隆请求。

至此,当已安装的 Service Worker 页面被打开时,便会触发 Service Worker 脚本更新。当上次脚本更新写入 Service Worker 数据库的时间戳与本次更新超过 24小时,便会触发 Service Worker 脚本更新。当 sw.js 文件改变时,便会触发 Service Worker 脚本更新。更新流程与安装类似,只是在更新安装成功后不会立即进入 active 状态,更新后的 Service Worker 会和原始的 Service Worker 共同存在,并运行它的 install,一旦新 Service Worker 安装成功,它会进入 wait 状态,需要等待旧版本的 Service Worker 进/线程终止。

更多 Server Worker 进阶知识可以查看文章末尾链接 🔗

实现效果:

PC端 🖥️:Windows上,在浏览器中初次打开应用后会有安装提示,点击安装图标之后进行安装,桌面和开始菜单中会生成应用快捷方式,点击快捷方式就可以打开应用。

windows.png

Mac 💻: 上面 chromiumn内核 的浏览器(chrome、opera、新版edge)也是类似的。安装之后会在 launchpad 中生成快捷方式。

mac.png

移动端 📱:iPhone。浏览器中选择保存到桌面,就可生成桌面图标,点击图标打开离线应用。

iphone.png

樱花飘落动画 🌸

petals.gif

为增强视觉效果和趣味性,于是在页面增加了樱花 🌸 飘落的效果。飘落效果动画主要使用到 Element.animate() 方法。

Element 的 animate() 方法是一个创建新 Animation 的便捷方法,将它应用于元素,然后运行动画。它将返回一个新建的 Animation 对象实例。一个元素上可以应用多个动画效果。你可以通过调用此函数获得这些动画效果的一个列表: Element.getAnimations()。

基本语法:

1
js复制代码var animation = element.animate(keyframes, options);

参数:

  • keyframes:关键帧。一个对象,代表关键帧的一个集合。
  • options:可选项代表动画持续时间的整形数字 (以毫秒为单位), 或者一个包含一个或多个时间属性的对象:
    • id: 可选,在 animate() 里可作为唯一标识的属性: 一个用来引用动画的字符串( DOMString )
    • delay:可选,开始时间的延迟毫秒数,默认值为 0。
    • direction:可选,动画的运动方向。向前运行 normal、向后运行 reverse、每次迭代后切换方向 alternate,向后运行并在每次迭代后切换方向 alternate-reverse。默认为 normal。
    • duration:可选,动画完成每次迭代的毫秒数,默认值为 0。
    • easing:可选,动画随时间变化的频率。接受预设的值包括 linear、ease、 ease-in、ease-out、ease-in-out及一个自定义值 cubic-bezier, 如 cubic-bezier(0.42, 0, 0.58, 1)。默认值为 linear。
    • endDelay:可选,一个动画结束后的延迟,默认值为 0。
    • fill:可选,定义动画效果对元素的影响时机,backwards 动画开始前影响到元素上、 forwards 动画完成后影响到元素上、both 两者兼具。默认值为 none。
    • iterationStart:可选,描述动画应该在迭代中的哪个点开始。例如,0.5 表示在第一次迭代中途开始,并且设置此值后,具有 2 次迭代的动画将在第三次迭代中途结束。默认为 0.0。
    • iterations:可选,动画应该重复的次数。默认为 1,也可以取 infinity 的值,使其在元素存在时重复。

以下代码为本例中的具体实现,HTML中有若干个 .petal 元素,然后 JavaScript 中获取到所有 .petal 元素添加随机动画,css中添加两种旋转和变形两种动画,实现樱花花瓣飘落的效果。

1
2
3
4
5
html复制代码<div id="petals_container">
<div class="petal"></div>
<!-- ... -->
<div class="petal"></div>
</div>
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
js复制代码var petalPlayers = [];
function animatePetals() {
var petals = document.querySelectorAll('.petal');
if (!petals[0].animate) {
var petalsContainer = document.getElementById('petals_container');
return false;
}
for (var i = 0, len = petals.length; i < len; ++i) {
var petal = petals[i];
petal.innerHTML = '<div class="rotate"><img src="petal.png" class="askew"></div>';
var scale = Math.random() * .6 + .2;
var player = petal.animate([{
transform: 'translate3d(' + (i / len * 100) + 'vw,0,0) scale(' + scale + ')',
opacity: scale
},
{
transform: 'translate3d(' + (i / len * 100 + 10) + 'vw,150vh,0) scale(' + scale + ')',
opacity: 1
}
], {
duration: Math.random() * 90000 + 8000,
iterations: Infinity,
delay: -(Math.random() * 5000)
});
petalPlayers.push(player);
}
}
animatePetals();
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
css复制代码.petal .rotate {
animation: driftyRotate 1s infinite both ease-in-out;
perspective: 1000;
}
.petal .askew {
transform: skewY(10deg);
display: block;
animation: drifty 1s infinite alternate both ease-in-out;
perspective: 1000;
}
.petal:nth-of-type(7n) .askew {
animation-delay: -.6s;
animation-duration: 2.25s;
}
.petal:nth-of-type(7n + 1) .askew {
animation-delay: -.879s;
animation-duration: 3.5s;
}
/* ... */
.petal:nth-of-type(9n) .rotate {
animation-duration: 2s;
}
.petal:nth-of-type(9n + 1) .rotate {
animation-duration: 2.3s;
}
/* ... */
@keyframes drifty {
0% {
transform: skewY(10deg) translate3d(-250%, 0, 0);
display: block;
}
100% {
transform: skewY(-12deg) translate3d(250%, 0, 0);
display: block;
}
}
@keyframes driftyRotate {
0% {
transform: rotateX(0);
display: block;
}
100% {
transform: rotateX(359deg);
display: block;
}
}

完整代码可查看文后链接 🔗。

CSS 判断手机横屏

本例 50音小游戏 应用是针对移动端开发,未作pc端的样式适配,所以可以添加一个横屏引导页面提示用户使用竖屏。在 CSS 中判断移动设备是否处于横屏状态,需要用到 aspect-ratio 进行媒体查询,通过测试 viewport 的宽高比来进行判断。

aspect-ratio宽高比属性被指定为 <ratio> 值来代表 viewport 的宽高比。其为一个范围,可以使用 min-aspect-ratio 和 max-aspect-ratio 分别查询最小和最大值。基本语法如下:

1
2
3
4
5
6
7
8
9
10
11
12
css复制代码/* 最小宽高比 */
@media (min-aspect-ratio: 8/5) {
// ...
}
/* 最大宽高比 */
@media (max-aspect-ratio: 3/2) {
// ...
}
/* 明确的宽高比, 放在最下部防止同时满足条件时的覆盖 */
@media (aspect-ratio: 1/1) {
// ...
}

在应用中的具体实现方式是添加一个 .mod_orient_layer 引导层并隐藏,当达到最小宽高比时将其显示:

1
html复制代码<div class="mod_orient_layer"></div>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
css复制代码.mod_orient_layer {
display: none;
position: fixed;
height: 100%;
width: 100%;
left: 0;
top: 0;
right: 0;
bottom: 0;
z-index: 999;
background: #FFFFFF url('landscape.png') no-repeat center;
background-size: auto 100%;
}
@media screen and (min-aspect-ratio: 13/8) {
.mod_orient_layer {
display: block;
}
}

实现效果:

landscape.png

兼容性

下面是本文中涉及的几个属性的兼容性视图,在实际生产项目中需要注意兼容性适配。

caniuse.png

Photoshop 技能

logo设计

logo 主要由两个元素构成由一个 ⛩️ 图标和日语平假名 あ 构成,都是经典的日系元素,同时对 あ 进行拉伸渐变,形成类似 ⛩️ 的阴影,使字母和图形巧妙连接在一起,使画面和谐。logo背景色使用应用主题背景色,与页面在无形之中建立联系,形成 全链路 统一风格标准。(编不下去了。。。😂

logo.png

⛩ 鸟居原始模型来源于 dribbble: dribbble.com

外部链接及参考资料

  • 樱花散落动画完整版 codepen.io/dragonir/fu…
  • Dark Mode Support in WebKit webkit.org/blog/8840/d…
  • PWA技术理论+实战全解析 www.cnblogs.com/yangyangxxb…
  • H5 PWA技术 zhuanlan.zhihu.com/p/144512343
  • aspect-ratio developer.mozilla.org/zh-CN/docs/…
  • Service Worker developer.mozilla.org/zh-CN/docs/…
  • Element.animate() developer.mozilla.org/zh-CN/docs/…

本文转载自: 掘金

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

统计活动参与名单,看看是什么样的骚操作来实现的

发表于 2021-07-21

本文已参与好文召集令活动,点击查看:后端、大前端双赛道投稿,2万元奖池等你挑战!

介绍

前段时间做出来一个活动排名,虽然有很多不足的地方,但是依然收获很多好评

image.png

有些小伙伴很好奇,怎么实现的,由于代码耦合性比较强,开始的时候也不知道怎么写这篇文章,一直没发

最近比较忙,也没及时修复bug、添加新功能,所以决定,开源出来,让大家一起舔砖加瓦,将功能完善起来

欢迎各位大佬来贡献代码
项目地址:github.com/ytwp/juejin…

这篇文章主要是讲一下活动排名怎么实现的

正题

  1. 要做这个功能,一定离不开用户,第一步就是发现用户
* 目前已通过专栏,以及每个标签下的最新文章,发现用户,收集用户ID
* 过滤不活跃用户,降低请求数量
  1. 通过定时,查询一下用户的信息,然后保存起来,相当于拍了个快照
  2. 查询用户时,把近一个月的文章,也查询出来,给这个功能做数据支撑
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
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
java复制代码截取部分核心源码
public void run() {
log.info("拉取用户快照");
String now = LocalDateTime.now().format(yyyyMMddHH);
try {
String path = "./j-" + LocalDate.now().format(yyyyMMdd) + ".json";
FileUtil.initFile(path);
FileWriter fw = new FileWriter(path, true);
PrintWriter pw = new PrintWriter(fw);

int i = 0;
//遍历所有用户 然后去获取用户信息
//获取到后,输出到文件里,用于后边的计算
for (String userId : userIdSet) {
//获取用户信息
JueJInApi.UserData userData = JueJInApi.getUser(userId);
if (userData == null) {
userData = JueJInApi.getUser(userId);
}
if (userData != null) {
userData.setTime(now);
pw.println(JSONUtil.toJsonStr(userData));
}
log.info((++i) + " 用户快照:" + userId);
}
pw.close();
fw.close();
} catch (IOException e) {
e.printStackTrace();
}
log.info("拉取用户快照结束:" + now);
}

public static UserData getUser(String user_id) {
Map<String, Object> map1 = new HashMap<>();
map1.put("cursor", "0");
map1.put("sort_type", 2);
map1.put("user_id", user_id);
Map<String, Object> map2 = new HashMap<>();
map2.put("audit_status", null);
map2.put("cursor", "0");
map2.put("limit", 10);
map2.put("user_id", user_id);
try {
//由于每篇文章都包含用户信息,所以直接去拉取文章就行
//每次10条,直到拉取完45天内的所有文章
List<ArticleData> articleDataList = new ArrayList<>();
for (Integer cursor = 0; true; cursor += 10) {
map1.put("cursor", cursor.toString());
String res1 = HttpUtil.post("https://api.juejin.cn/content_api/v1/article/query_list", JSONUtil.toJsonStr(map1));
JSONObject jsonObject = JSONUtil.parseObj(res1);
Integer count = jsonObject.getInt("count");
if (count == 0) {
break;
}
JSONArray data1 = jsonObject.getJSONArray("data");
List<ArticleData> dataList = JSONUtil.toList(data1, ArticleData.class);
//过滤超过45天的文章
List<ArticleData> data2 = dataList.stream().filter(data -> {
String ctime = data.getArticle_info().getCtime();
long now = System.currentTimeMillis() / 1000;
return Long.parseLong(ctime) > (now - (60 * 60 * 24 * 45));
}).collect(Collectors.toList());

articleDataList.addAll(data2);

//如果有超过45天的文章,就结束
if (dataList.size() != data2.size()) {
break;
}
//如果拉完了所有文章也结束
if (jsonObject.getInt("cursor") >= (count)) {
break;
}
}
//这里是拿一个文章包含的用户信息,然后把其他的用户信息都设置null 防止占有大量硬盘
AtomicReference<AuthorUserInfo> author_user_info = new AtomicReference<>();
articleDataList = articleDataList.stream().peek(articleData -> {
author_user_info.set(articleData.getAuthor_user_info());
articleData.setAuthor_user_info(null);
}).collect(Collectors.toList());
//这里是这个用户的所以专栏,用于专栏统计
List<SelfData> selfDataList = new ArrayList<>();
for (Integer cursor = 0; true; cursor += 10) {
map2.put("cursor", cursor.toString());
String res2 = HttpUtil.post("https://api.juejin.cn/content_api/v1/column/self_center_list", JSONUtil.toJsonStr(map2));
JSONObject jsonObject = JSONUtil.parseObj(res2);
Integer count = jsonObject.getInt("count");
if (count == 0) {
break;
}
JSONArray data2 = jsonObject.getJSONArray("data");
selfDataList.addAll(JSONUtil.toList(data2, SelfData.class));
if (jsonObject.getInt("cursor") >= (count)) {
break;
}
}
//包装用户信息
UserData userData = new UserData();
userData.setUser_id(user_id);
userData.setArticle_list(articleDataList);
userData.setSelf_center_list(selfDataList);
userData.setAuthor_user_info(author_user_info.get());
return userData;
} catch (Exception e) {
e.printStackTrace();
}
return null;
}
  1. 分析数据
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
sclala复制代码// 我感觉这代码,以及可以称为,最佳迷惑代码,自己都快解释不清了
def run(): Unit = {
log.info("计算活动文章")
//各种时间
val now = LocalDateTime.now.format(yyyyMMddHH)
val runTime = System.currentTimeMillis() / 1000
val yyyyMMddStr = LocalDate.now.format(yyyyMMdd)
val yyyyMMddInt = Integer.parseInt(yyyyMMddStr)
try {
//输出路径
val path = FilePathConstant.EXPLORE_DARA_PATH.format(yyyyMMddStr)
val outPath = FilePathConstant.BAK_ACTIVITY_REPORT_DARA_PATH.format(now.format(yyyyMMdd))
val outNowPath = FilePathConstant.ACTIVITY_REPORT_DARA_PATH
val rulePath = FilePathConstant.ACTIVITY_RULE_PATH
val configPath = FilePathConstant.ACTIVITY_CONFIG_PATH

//读取所需要的配置文件
val userDataList = FileUtil.readLineJsonList(path, classOf[JueJInApi.UserData]).asScala
val activityRuleList = FileUtil.readJsonList(rulePath, classOf[ActivityRule]).asScala
val activityConfig = FileUtil.readJson(configPath, classOf[ActivityConfig])

//防止大量调用掘金的接口,记录一下上次处理完的时间,之前的不做处理
val lastRunTime = activityConfig.getLastRunTime

log.info(s"数据读取完成,长度:${userDataList.size}")

log.info("开始计算 活动文章")
/**
* 专栏
* 1.用户分组
* 2.保留一天最新的一个快照
* 3.关注数排序
*/
val resMap = mutable.Map[Int, ListBuffer[(ActivityRule, ArticleData)]]()
// 这里是 groupBy values map 是为了拿到一天内每个用户最新的一条数据
userDataList
.groupBy(_.getUser_id)
.values
.map(userData => (userData.maxBy(_.getTime.toInt)))
.foreach(userData => {
//开始统计
userData.getArticle_list.asScala
.foreach(articleData => {
val ctime = articleData.getArticle_info.getCtime.toLong
//首次 或者 最后一次运行-12小时
if (lastRunTime == 0 || ctime > (lastRunTime - 60 * 60 * 12)) {
log.info(s"拉取文章详情: ${articleData.getArticle_id}")
//获取文章信息,匹配是不是活动文章
val detail = JueJInApi.getArticleDetail(articleData.getArticle_id)
if (detail != null) {
activityRuleList.foreach(activityRule => {
//匹配到活动文章
def isActivity(content: String) {
if (content.contains(activityRule.getKeyword)) {
log.info(s"匹配到活动文章: ${activityRule.getKeyword}")
val listBuffer = resMap.getOrElse(activityRule.getId.toInt, ListBuffer[(ActivityRule, ArticleData)]())
articleData.setAuthor_user_info(detail.getAuthor_user_info)
listBuffer.append((activityRule, articleData))
//添加到最终的结果集
resMap.put(activityRule.getId.toInt, listBuffer)
}
}
//活动结束不在统计
if (activityRule.getEndDate <= yyyyMMddInt) {
//看看是通过标题 还是文章 匹配
if (activityRule.getType == "post") {
isActivity(detail.getArticle_info.getMark_content)
} else if (activityRule.getType == "title") {
isActivity(detail.getArticle_info.getTitle)
}
}
})
}
}
})
})
//拿个(用户id,用户信息) 的一个map
val userDataMap: Map[String, JueJInApi.UserData] = userDataList
.groupBy(_.getUser_id)
.values
.map(userData => (userData.maxBy(_.getTime.toInt)))
.map(userData => (userData.getUser_id, userData))
.toMap

//读取上次的结果
var fileActivityReportList: mutable.Buffer[ActivityReport] = FileUtil.readJsonList(outNowPath, classOf[ActivityReport]).asScala
log.info("初始化 活动 列表")
//防止新加活动,所以要把活动初始化到结果的json里
val ids = fileActivityReportList.map(_.getId)
activityRuleList.foreach(rule => {
val id = rule.getId
if (!ids.contains(id)) {
val report = new ActivityReport
report.setId(id)
report.setUserActivityReportMap(new util.HashMap[String, ActivityReport.UserActivityReport]())
fileActivityReportList = fileActivityReportList.+:(report)
}
//结束7天的 直接删除
val end7Date = LocalDate.parse(rule.getEndDate.toString, yyyyMMdd).plusDays(7).format(yyyyMMdd).toInt
if (yyyyMMddInt > end7Date) {
fileActivityReportList = fileActivityReportList.filter(r => r.getId != id)
}
})
//活动匹配规则 转成 (活动id,活动详情)的一个map
val ruleMap: Map[Integer, ActivityRule] = activityRuleList.map(rule => (rule.getId, rule)).toMap

log.info("转换数据")
val activityReportList = fileActivityReportList
.map(activityReport => {
val id = activityReport.getId
val ruleOption: Option[ActivityRule] = ruleMap.get(id)
val rule = ruleOption.get
//活动过期后不更新数据
if (rule == null || rule.getEndDate <= yyyyMMddInt) {
null
} else {
//封装好用户数据
val activityReportMap = activityReport.getUserActivityReportMap
val articleIdSet = activityReportMap.asScala.flatMap(a => a._2.getArticleIdSet.asScala).toSet
val option = resMap.get(id)
if (option.nonEmpty) {
option.get.foreach(t => {
val value = t._2
val article_id = value.getArticle_id
if (!articleIdSet.contains(article_id)) {
val user_id = value.getAuthor_user_info.getUser_id
val user_name = value.getAuthor_user_info.getUser_name
var report = activityReportMap.get(user_id)
if (report == null) {
report = new ActivityReport.UserActivityReport()
report.setArticleIdSet(new util.HashSet[String]())
report.setUser_id(user_id)
report.setUser_name(user_name)
report.setCount(0)
report.setSum_digg_count(0)
report.setSum_view_count(0)
report.setSum_collect_count(0)
report.setSum_comment_count(0)
}
val set = report.getArticleIdSet
set.add(article_id)
report.setArticleIdSet(set)
activityReportMap.put(user_id, report)
}
})

}
activityReport.setUserActivityReportMap(activityReportMap)
activityReport
}
})
//过滤掉空结果
.filter(a => a != null)
//生成最终的数据集
.map(activityReport => {
val activityReportMap = activityReport.getUserActivityReportMap.asScala.map(t => {
val user_id = t._1
val userActivityReport = t._2
val set = userActivityReport.getArticleIdSet
val maybeUserData = userDataMap.get(user_id)
if (maybeUserData.nonEmpty) {
val userData = maybeUserData.get
val datas = userData.getArticle_list.asScala.filter(article => set.contains(article.getArticle_id))
userActivityReport.setCount(datas.size)
userActivityReport.setSum_view_count(datas.map(_.getArticle_info.getView_count.toInt).sum)
userActivityReport.setSum_digg_count(datas.map(_.getArticle_info.getDigg_count.toInt).sum)
userActivityReport.setSum_collect_count(datas.map(_.getArticle_info.getCollect_count.toInt).sum)
userActivityReport.setSum_comment_count(datas.map(_.getArticle_info.getComment_count.toInt).sum)
}
(user_id, userActivityReport)
}).toMap.asJava
activityReport.setUserActivityReportMap(activityReportMap)
activityReport
})
.asJava

log.info("保存数据")
activityConfig.setLastRunTime(runTime)
FileUtil.writeJson(outPath, activityReportList)
FileUtil.writeJson(outNowPath, activityReportList)
FileUtil.writeJson(configPath, activityConfig)
reportService.updateActivityReport(activityReportList, activityRuleList)

} catch {
case e: IOException =>
e.printStackTrace()
}
log.info("计算活动文章结束:" + now)
}

本文转载自: 掘金

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

1…597598599…956

开发者博客

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