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

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


  • 首页

  • 归档

  • 搜索

【奇技淫巧】巧用 kotlin 扩展函数和 typealia

发表于 2020-06-05

关于 LiveData 两个常用的姿势

使用包装类传递事件

我们在使用 LiveData 时可能会遇到「粘性」事件的问题,该问题可以使用包装类的方式解决。解决方案见 [译] 在 SnackBar,Navigation 和其他事件中使用 LiveData(SingleLiveEvent 案例)

使用时是这样的

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
复制代码class ListViewModel : ViewModel {
private val _navigateToDetails = MutableLiveData<Event<String>>()

val navigateToDetails : LiveData<Event<String>>
get() = _navigateToDetails


fun userClicksOnButton(itemId: String) {
_navigateToDetails.value = Event(itemId) // Trigger the event by setting a new Event as a new value
}
}

myViewModel.navigateToDetails.observe(this, Observer {
it.getContentIfNotHandled()?.let { // Only proceed if the event has never been handled
startActivity(DetailsActivity...)
}
})

不过这样写甚是繁琐,我们可以使用更优雅的方式解决该问题

1
2
3
4
复制代码//为 LiveData<Event<T>>提供类型别名,使用 EventLiveData<T> 即可
typealias EventMutableLiveData<T> = MutableLiveData<Event<T>>

typealias EventLiveData<T> = LiveData<Event<T>>

使用 typealias 关键字,我们可以提供一个类型别名,可以这样使用

1
2
复制代码//等价于 MutableLiveData<Event<Boolean>>(Event(false))
val eventContent = EventMutableLiveData<Boolean>(Event(false))

现在声明时不用多加一层泛型了,那么使用时还是很繁琐

我们可以借助 kotlin 的 扩展函数更优雅的使用

event 扩展函数

event 扩展函数

使用

使用

demo 中封装了两种形式的 LiveData,一种为 LiveData<Boolean>,一种为 EventLiveData<Boolean>,当屏幕旋转时,前者会再次回调结果,而后者由于事件已被处理而不执行 onChanged,我们通过 Toast 可观察到这一现象


java 版的可参考

封装带网络状态的数据

很多时候我们在获取网络数据时要封装一层网络状态,例如:加载中,成功,失败


在使用时我们遇到了和上面一样的问题,多层泛型用起来很麻烦

我们依然可以使用 typealias + 扩展函数来优雅的处理该问题

typealias

typealias

扩展函数

扩展函数

使用

使用

demo 截图

demo

demo

Demo

demo 在这,如果感觉这个思路对你有帮助的话,点一颗小星星吧~ 😉

另外我还将它传到了 JitPack 上,引入姿势如下:

  1. 在项目根目录的 build.gradle 加入
1
2
3
4
5
6
复制代码allprojects {
repositories {
//...
maven { url 'https://jitpack.io' }
}
}
  1. 添加依赖
1
2
3
复制代码dependencies {
implementation 'com.github.Flywith24:WrapperLiveData:$version'
}

往期文章

该系列主要介绍一些「骚操作」,它未必适合生产环境使用,但是是一些比较新颖的思路

  • 【奇技淫巧】AndroidStudio Nexus3.x 搭建 Maven 私服遇到问题及解决方案
  • 【奇技淫巧】什么?项目里 gradle 代码超过 200 行了!你可能需要 Kotlin+buildSrc Plugin
  • 【奇技淫巧】gradle 依赖查找太麻烦?这个插件可能帮到你
  • 【奇技淫巧】Android 组件化不使用 Router 如何实现组件间 activity 跳转
  • 【奇技淫巧】新的图片加载库?基于 Kotlin 协程的图片加载库——Coil
  • 【奇技淫巧】使用 Navigation + Dynamic Feature Module 实现模块化
  • 【奇技淫巧】除了 buildSrc 还能这样统一配置依赖版本?巧用 includeBuild

我的其他系列文章 在这里

关于我

我是 Flywith24,我的博客内容已经分类整理 在这里,点击右上角的 Watch 可以及时获取我的文章更新哦 😉

  • 掘金
  • 简书
  • Github

本文转载自: 掘金

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

面试官:兄弟,说说Java的static关键字吧

发表于 2020-06-05

读者乙在上一篇我去系列文章里留言说,“我盲猜下一篇标题是,‘我去,你竟然不知道 static 关键字’”。我只能说乙猜对了一半,像我这么有才华的博主,怎么可能被读者猜中了心思呢,必须搞点不一样的啊,所以本篇文章的标题你看到了。

七年前,我从美女很多的苏州回到美女也不少的洛阳,抱着一幅“从二线城市退居三线城市”的心态,投了不少简历,也“约谈”了不少面试官,但仅有两三个令我感到满意。其中有一位叫老马,至今还活在我的微信通讯录里。他当时扔了一个面试题把我砸懵了:“兄弟,说说 Java 的 static 关键字吧。”

我那时候二十三岁,正值青春年华,自认为所有的面试题都能对答如流,结果没想到啊,被“刁难”了——原来洛阳这块互联网的荒漠也有技术专家啊。现在回想起来,脸上不自觉地泛起了羞愧的红晕:主要是自己当时太菜了。

不管怎么说,经过多年的努力,我现在的技术功底已经非常扎实了,有能力写篇文章剖析一下 Java 的 static 关键字了——只要能给初学者一些参考,我就觉得非常满足。

先来个提纲挈领(唉呀妈呀,成语区博主上线了)吧:

static 关键字可用于变量、方法、代码块和内部类,表示某个特定的成员只属于某个类本身,而不是该类的某个对象。

01、静态变量

静态变量也叫类变量,它属于一个类,而不是这个类的对象。

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 Writer {
private String name;
private int age;
public static int countOfWriters;

public Writer(String name, int age) {
this.name = name;
this.age = age;
countOfWriters++;
}

public String getName() {
return name;
}

public void setName(String name) {
this.name = name;
}

public int getAge() {
return age;
}

public void setAge(int age) {
this.age = age;
}
}

其中,countOfWriters 被称为静态变量,它有别于 name 和 age 这两个成员变量,因为它前面多了一个修饰符 static。

这意味着无论这个类被初始化多少次,静态变量的值都会在所有类的对象中共享。

1
2
3
4
复制代码Writer w1 = new Writer("沉默王二",18);
Writer w2 = new Writer("沉默王三",16);

System.out.println(Writer.countOfWriters);

按照上面的逻辑,你应该能推理得出,countOfWriters 的值此时应该为 2 而不是 1。从内存的角度来看,静态变量将会存储在 Java 虚拟机中一个名叫“Metaspace”(元空间,Java 8 之后)的特定池中。

静态变量和成员变量有着很大的不同,成员变量的值属于某个对象,不同的对象之间,值是不共享的;但静态变量不是的,它可以用来统计对象的数量,因为它是共享的。就像上面例子中的 countOfWriters,创建一个对象的时候,它的值为 1,创建两个对象的时候,它的值就为 2。

简单小结一下:

1)由于静态变量属于一个类,所以不要通过对象引用来访问,而应该直接通过类名来访问;

2)不需要初始化类就可以访问静态变量。

1
2
3
4
5
复制代码public class WriterDemo {
public static void main(String[] args) {
System.out.println(Writer.countOfWriters); // 输出 0
}
}

02、静态方法

静态方法也叫类方法,它和静态变量类似,属于一个类,而不是这个类的对象。

1
2
3
复制代码public static void setCountOfWriters(int countOfWriters) {
Writer.countOfWriters = countOfWriters;
}

setCountOfWriters() 就是一个静态方法,它由 static 关键字修饰。

如果你用过 java.lang.Math 类或者 Apache 的一些工具类(比如说 StringUtils)的话,对静态方法一定不会感动陌生。

Math 类的几乎所有方法都是静态的,可以直接通过类名来调用,不需要创建类的对象。

简单小结一下:

1)Java 中的静态方法在编译时解析,因为静态方法不能被重写(方法重写发生在运行时阶段,为了多态)。

2)抽象方法不能是静态的。

3)静态方法不能使用 this 和 super 关键字。

4)成员方法可以直接访问其他成员方法和成员变量。

5)成员方法也可以直接方法静态方法和静态变量。

6)静态方法可以访问所有其他静态方法和静态变量。

7)静态方法无法直接访问成员方法和成员变量。

03、静态代码块

静态代码块可以用来初始化静态变量,尽管静态方法也可以在声明的时候直接初始化,但有些时候,我们需要多行代码来完成初始化。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
复制代码public class StaticBlockDemo {
public static List<String> writes = new ArrayList<>();

static {
writes.add("沉默王二");
writes.add("沉默王三");
writes.add("沉默王四");

System.out.println("第一块");
}

static {
writes.add("沉默王五");
writes.add("沉默王六");

System.out.println("第二块");
}
}

writes 是一个静态的 ArrayList,所以不太可能在声明的时候完成初始化,因此需要在静态代码块中完成初始化。

简单小结一下:

1)一个类可以有多个静态代码块。

2)静态代码块的解析和执行顺序和它在类中的位置保持一致。为了验证这个结论,可以在 StaticBlockDemo 类中加入空的 main 方法,执行完的结果如下所示:

1
2
复制代码第一块
第二块

04、静态内部类

Java 允许我们在一个类中声明一个内部类,它提供了一种令人信服的方式,允许我们只在一个地方使用一些变量,使代码更具有条理性和可读性。

常见的内部类有四种,成员内部类、局部内部类、匿名内部类和静态内部类,限于篇幅原因,前三种不在我们本次文章的讨论范围,以后有机会再细说。

1
2
3
4
5
6
7
8
9
10
11
复制代码public class Singleton {
private Singleton() {}

private static class SingletonHolder {
public static final Singleton instance = new Singleton();
}

public static Singleton getInstance() {
return SingletonHolder.instance;
}
}

以上这段代码是不是特别熟悉,对,这就是创建单例的一种方式,第一次加载 Singleton 类时并不会初始化 instance,只有第一次调用 getInstance() 方法时 Java 虚拟机才开始加载 SingletonHolder 并初始化 instance,这样不仅能确保线程安全也能保证 Singleton 类的唯一性。不过,创建单例更优雅的一种方式是使用枚举。

简单小结一下:

1)静态内部类不能访问外部类的所有成员变量。

2)静态内部类可以访问外部类的所有静态变量,包括私有静态变量。

3)外部类不能声明为 static。

学到了吧?学到就是赚到。

我是沉默王二,一枚有趣的程序员。如果觉得文章对你有点帮助,请微信搜索「 沉默王二 」第一时间阅读,回复【666】更有我为你精心准备的 500G 高清教学视频(已分门别类)。

本文 GitHub 已经收录,有大厂面试完整考点,欢迎 Star。

原创不易,莫要白票,请你为本文点个赞吧,这将是我写作更多优质文章的最强动力。

本文转载自: 掘金

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

Spring Boot+CAS 单点登录,如何对接数据库?

发表于 2020-06-05

在前面的两篇文章中,松哥和大家分享了 CAS Server 的搭建以及如何使用 Spring Security 搭建 CAS Client。

但是前面的案例有一个问题,就是登录用户是在 CAS Server 配置文件中写死的,没有对接数据库,实际项目中,这里肯定要对接数据库,所以今天,松哥就来和大家聊一聊 CAS Server 如何对接数据库。

松哥最近和 Spring Security 杠上了,这是 Spring Security 系列的第 25 篇:

  1. 挖一个大坑,Spring Security 开搞!
  2. 松哥手把手带你入门 Spring Security,别再问密码怎么解密了
  3. 手把手教你定制 Spring Security 中的表单登录
  4. Spring Security 做前后端分离,咱就别做页面跳转了!统统 JSON 交互
  5. Spring Security 中的授权操作原来这么简单
  6. Spring Security 如何将用户数据存入数据库?
  7. Spring Security+Spring Data Jpa 强强联手,安全管理只有更简单!
  8. Spring Boot + Spring Security 实现自动登录功能
  9. Spring Boot 自动登录,安全风险要怎么控制?
  10. 在微服务项目中,Spring Security 比 Shiro 强在哪?
  11. SpringSecurity 自定义认证逻辑的两种方式(高级玩法)
  12. Spring Security 中如何快速查看登录用户 IP 地址等信息?
  13. Spring Security 自动踢掉前一个登录用户,一个配置搞定!
  14. Spring Boot + Vue 前后端分离项目,如何踢掉已登录用户?
  15. Spring Security 自带防火墙!你都不知道自己的系统有多安全!
  16. 什么是会话固定攻击?Spring Boot 中要如何防御会话固定攻击?
  17. 集群化部署,Spring Security 要如何处理 session 共享?
  18. 松哥手把手教你在 SpringBoot 中防御 CSRF 攻击!so easy!
  19. 要学就学透彻!Spring Security 中 CSRF 防御源码解析
  20. Spring Boot 中密码加密的两种姿势!
  21. Spring Security 要怎么学?为什么一定要成体系的学习?
  22. Spring Security 两种资源放行策略,千万别用错了!
  23. 聊一聊 Spring Boot 中的 CAS 单点登录
  24. Spring Boot 实现单点登录的第三种方案!

1.整体思路

先来看整体思路。

我们用 CAS Server 做单点登录,CAS Server 主要是负责认证的,也就是它主要解决登录问题。登录成功之后,还有一个权限处理的问题,权限的问题则交由各个 CAS Client 自行处理,并不在 CAS Server 中完成。

在上篇文章中,松哥有教过大家定义 UserDetailsService,不知道大家是否还记得如下代码(忘记了可以参考上篇文章:Spring Boot 实现单点登录的第三种方案!):

1
2
3
4
5
6
7
8
9
10
复制代码@Component
@Primary
public class UserDetailsServiceImpl implements UserDetailsService{

@Override
public UserDetails loadUserByUsername(String s) throws UsernameNotFoundException {
return new User(s, "123", true, true, true, true,
AuthorityUtils.createAuthorityList("ROLE_user"));
}
}

这段代码是在什么时候执行呢?

如果我们没有使用 CAS 这一套的话,这段代码当然是在用户登录的时候执行,用户登录时,从数据库中查询用户的信息,然后做校验(参考本系列前面文章就懂)。

如果我们使用 CAS 这一套,用户登录的校验将在 CAS Server 上执行,CAS Client 就不用做校验工作了,但是为什么我们还需要定义 UserDetailsService 呢?这是为了当用户在 CAS Server 上登录成功之后,拿着用户名回到 CAS Client,然后我们再去数据库中根据用户名获取用户的详细信息,包括用户的角色等,进而在后面的鉴权中用上角色。

好了,这是我们一个大致的思路,接下来我们来看具体实现。

2.具体实现

接下来的配置在 松哥手把手教你入门 Spring Boot + CAS 单点登录 一文的基础上完成,所以还没看前面文章的小伙伴建议先看一下哦。

2.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
复制代码CREATE TABLE `t_role` (
`id` bigint(20) NOT NULL AUTO_INCREMENT,
`name` varchar(255) COLLATE utf8mb4_unicode_ci DEFAULT NULL,
`name_zh` varchar(255) COLLATE utf8mb4_unicode_ci DEFAULT NULL,
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;

CREATE TABLE `t_user` (
`id` bigint(20) NOT NULL AUTO_INCREMENT,
`account_non_expired` bit(1) NOT NULL,
`account_non_locked` bit(1) NOT NULL,
`credentials_non_expired` bit(1) NOT NULL,
`enabled` bit(1) NOT NULL,
`password` varchar(255) COLLATE utf8mb4_unicode_ci DEFAULT NULL,
`username` varchar(255) COLLATE utf8mb4_unicode_ci DEFAULT NULL,
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;

CREATE TABLE `t_user_roles` (
`t_user_id` bigint(20) NOT NULL,
`roles_id` bigint(20) NOT NULL,
KEY `FKj47yp3hhtsoajht9793tbdrp4` (`roles_id`),
KEY `FK7l00c7jb4804xlpmk1k26texy` (`t_user_id`),
CONSTRAINT `FK7l00c7jb4804xlpmk1k26texy` FOREIGN KEY (`t_user_id`) REFERENCES `t_user` (`id`),
CONSTRAINT `FKj47yp3hhtsoajht9793tbdrp4` FOREIGN KEY (`roles_id`) REFERENCES `t_role` (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;

INSERT INTO `t_role` (`id`, `name`, `name_zh`) VALUES (1,'ROLE_admin','管理员'),(2,'ROLE_user','普通用户');
INSERT INTO `t_user` (`id`, `account_non_expired`, `account_non_locked`, `credentials_non_expired`, `enabled`, `password`, `username`) VALUES (1,b'1',b'1',b'1',b'1','123','javaboy'),(2,b'1',b'1',b'1',b'1','123','江南一点雨');
INSERT INTO `t_user_roles` (`t_user_id`, `roles_id`) VALUES (1,1),(2,2);

2.2 CAS Server

然后我们要在 CAS Server 的 pom.xml 文件中添加两个依赖:

1
2
3
4
5
6
7
8
9
10
复制代码<dependency>
<groupId>org.apereo.cas</groupId>
<artifactId>cas-server-support-jdbc-drivers</artifactId>
<version>${cas.version}</version>
</dependency>
<dependency>
<groupId>org.apereo.cas</groupId>
<artifactId>cas-server-support-jdbc</artifactId>
<version>${cas.version}</version>
</dependency>

注意这里不用添加数据库驱动,系统会自动解决。

添加完成之后,再在 src/main/resources/application.properties 文件中添加如下配置:

1
2
3
4
5
6
复制代码cas.authn.jdbc.query[0].url=jdbc:mysql://127.0.0.1:3306/withjpa?useUnicode=true&characterEncoding=UTF-8&autoReconnect=true&useSSL=false
cas.authn.jdbc.query[0].user=root
cas.authn.jdbc.query[0].password=123
cas.authn.jdbc.query[0].sql=select * from t_user where username=?
cas.authn.jdbc.query[0].fieldPassword=password
cas.authn.jdbc.query[0].driverClass=com.mysql.cj.jdbc.Driver
  • 前三行配置是数据库基本连接配置,这个无需我多说。
  • 第四行表示配置用户查询 sql,根据用户名查询出用户的所有信息。
  • 第五行表示数据库中密码的字段名字是什么。
  • 第六行是数据库驱动。

OK,配置完成后,接下来我们就来重启 CAS Server:

1
复制代码./build.sh run

启动成功后,浏览器输入 cas.javaboy.org:8443/cas/login 就可以进入登录页面了(注意是 https 哦):

此时登录用户名就是 javaboy,密码是 123。

2.3 CAS Client

接下来我们再来看看 CAS Client 要做哪些完善。

接下来的配置在 Spring Boot 实现单点登录的第三种方案! 一文的基础上完成,所以还没看前面文章的小伙伴建议先看一下哦。

同时,为了案例简洁,我这里使用 JPA 来操作数据库,要是大家不熟悉这块的操作,可以参考本系列之前的文章:Spring Security+Spring Data Jpa 强强联手,安全管理只有更简单!。

CAS Client 中的对接主要是实现 UserDetailsService 接口。这里要用到数据库查询,所以我们首先添加数据库相关依赖:

1
2
3
4
5
6
7
8
复制代码<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>

然后在 application.properties 中配置数据库连接信息:

1
2
3
4
5
6
7
8
9
复制代码spring.datasource.username=root
spring.datasource.password=123
spring.datasource.url=jdbc:mysql:///withjpa?useUnicode=true&characterEncoding=UTF-8&serverTimezone=Asia/Shanghai

spring.jpa.database=mysql
spring.jpa.database-platform=mysql
spring.jpa.hibernate.ddl-auto=update
spring.jpa.show-sql=true
spring.jpa.properties.hibernate.dialect=org.hibernate.dialect.MySQL8Dialect

都是常规配置,我们就不再重复解释了。

接下来我们创建两个实体类,分别表示用户角色了用户类:

用户角色:

1
2
3
4
5
6
7
8
9
复制代码@Entity(name = "t_role")
public class Role {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String name;
private String nameZh;
//省略 getter/setter
}

这个实体类用来描述用户角色信息,有角色 id、角色名称(英文、中文),@Entity 表示这是一个实体类,项目启动后,将会根据实体类的属性在数据库中自动创建一个角色表。

用户实体类:

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
复制代码@Entity(name = "t_user")
public class User implements UserDetails {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String username;
private String password;
private boolean accountNonExpired;
private boolean accountNonLocked;
private boolean credentialsNonExpired;
private boolean enabled;
@ManyToMany(fetch = FetchType.EAGER,cascade = CascadeType.PERSIST)
private List<Role> roles;
@Override
public Collection<? extends GrantedAuthority> getAuthorities() {
List<SimpleGrantedAuthority> authorities = new ArrayList<>();
for (Role role : getRoles()) {
authorities.add(new SimpleGrantedAuthority(role.getName()));
}
return authorities;
}
@Override
public String getPassword() {
return password;
}

@Override
public String getUsername() {
return username;
}

@Override
public boolean isAccountNonExpired() {
return accountNonExpired;
}

@Override
public boolean isAccountNonLocked() {
return accountNonLocked;
}

@Override
public boolean isCredentialsNonExpired() {
return credentialsNonExpired;
}

@Override
public boolean isEnabled() {
return enabled;
}
//省略其他 get/set 方法
}

用户实体类主要需要实现 UserDetails 接口,并实现接口中的方法。

这里的字段基本都好理解,几个特殊的我来稍微说一下:

  1. accountNonExpired、accountNonLocked、credentialsNonExpired、enabled 这四个属性分别用来描述用户的状态,表示账户是否没有过期、账户是否没有被锁定、密码是否没有过期、以及账户是否可用。
  2. roles 属性表示用户的角色,User 和 Role 是多对多关系,用一个 @ManyToMany 注解来描述。
  3. getAuthorities 方法返回用户的角色信息,我们在这个方法中把自己的 Role 稍微转化一下即可。

数据模型准备好之后,我们再来定义一个 UserDao:

1
2
3
复制代码public interface UserDao extends JpaRepository<User,Long> {
User findUserByUsername(String username);
}

这里的东西很简单,我们只需要继承 JpaRepository 然后提供一个根据 username 查询 user 的方法即可。如果小伙伴们不熟悉 Spring Data Jpa 的操作,可以在公众号后台回复 springboot 获取松哥手敲的 Spring Boot 教程,里边有 jpa 相关操作,也可以看看松哥录制的视频教程:Spring Boot + Vue 系列视频教程。

在接下来定义 UserService ,如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
复制代码@Component
@Primary
public class UserDetailsServiceImpl implements UserDetailsService{

@Autowired
UserDao userDao;
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
User user = userDao.findUserByUsername(username);
if (user == null) {
throw new UsernameNotFoundException("用户不存在");
}
return user;
}
}

我们自己定义的 UserDetailsServiceImpl 需要实现 UserDetailsService 接口,实现该接口,就要实现接口中的方法,也就是 loadUserByUsername。

OK ,如此之后,我们的 CAS Client 现在就开发完成了,接下来启动 CAS Client,启动成功后,浏览器输入 http://client1.cas.javaboy.org:8080/user/hello 访问 hello 接口,此时会自动跳转到 CAS Server 上登录,登录的用户名密码就是我们存储在数据库中的用户名密码。登录成功之后,经过两个重定向,会重新回到 hello 接口。

hello 接口访问成功之后,再去访问 /user/hello 接口,就会发现权限配置也生效了。

这里比较简单,我就不给大家截图了。

3.小结

好啦,今天主要和小伙伴们分享了一下 Spring Security + CAS 单点登录中,如何使用本地数据库。一个核心的思路是,认证由 CAS Server 来做,权限相关的操作,则还是由 Spring Security 来完成。

好了 ,本文就说到这里,本文相关案例我已经上传到 GitHub ,大家可以自行下载:github.com/lenve/sprin…

好啦,小伙伴们如果觉得有收获,记得点个在看鼓励下松哥哦~

本文转载自: 掘金

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

爱了!再来推荐5个Java项目开发快速开发脚手架。项目经验和

发表于 2020-06-05

在上期《听说你要接私活?Guide连夜整理了5个开源免费的Java项目快速开发脚手架。》 Java快速开发脚手架推荐中,我一共推荐了下面这些项目。

  1. Guns
  2. pig
  3. RuoYi
  4. Jeecg-boot
  5. iBase4J

综合来看好评度最高的是:Ruoyi,次之的是 pig,另外,有读者表示自己公司在用 Jeecg-boot ,但是开发过程中遇到了一些问题。

今天再来推荐 5 个好用的 Java 项目快速开发脚手架/项目骨架搭建脚手架,下面推荐的项目除了 renren 之外,其他都是我从 Github 上找的。

并且,我还在朋友圈调查了一波大家觉得比较好用脚手架,调查结果就在概览下面,就从这波用心,不来个在看或者转发鼓励一下Guide么?

概览

  1. eladmin (8.9k star):权限管理系统。
  2. renren(约2.1k) :Java项目脚手架
  3. SpringBlade (2.6k star) :一个由商业级项目升级优化而来的 SpringCloud 分布式微服务架构、SpringBoot 单体式微服务架构并存的综合型项目。
  4. COLA (2.1k star):创建属于你的干净的面向对象和分层架构项目骨架。
  5. SpringBoot_v2 :努力打造springboot框架的极致细腻的脚手架。

根据昨天我在朋友圈发起的调查来看, eladmin 、renren 、SpringBlade好评度最高, COLA 、SpringBoot_v2 次之。

ps:很多人推荐了 renren-fast,我感觉一般吧!手动狗头~有木有实际用过,并且和其他开源项目比如 eladmin 对比过的小伙伴在评论区说服一下我。

eladmin

推荐指数 :⭐⭐⭐⭐⭐

简介

eladmin 是一款基于 Spring Boot 2.1.0 、 Jpa、 Spring Security、redis、Vue 的前后端分离的后台管理系统,项目采用分模块开发方式, 权限控制采用 RBAC,支持数据字典与数据权限管理,支持一键生成前后端代码,支持动态路由。


相关地址 :

  1. Github 地址:github.com/elunez/elad…
  2. 官网:docs.auauz.net/
  3. 文档:docs.auauz.net/guide/

推荐理由

  1. 项目基本稳定,并且后续作者还会继续优化。
  2. 完全开源!这个真的要为原作者点个赞,如果大家觉得这个项目有用的话,建议可以稍微捐赠一下原作者支持一下。
  3. 后端整理代码质量、表设计等各个方面来说都是很不错的。
  4. 前后端分离,前端使用的是国内常用的 vue 框架,比较容易上手。
  5. 前端样式美观,是我这篇文章推荐的几个开源项目中前端样式最好看的一个。
  6. 权限控制采用 RBAC,支持数据字典与数据权限管理。

项目展示

后台首页

后台首页

角色管理页面

角色管理页面

renren

推荐指数 :⭐⭐⭐⭐

简介

renren 下面一共开源了两个 Java 项目开发脚手架,分别是:

  1. renren-security :采用 Spring、MyBatis、Shiro 框架,开发的一套轻量级权限系统,极低门槛,拿来即用。
  2. renren-fast : 一个轻量级的 Java 快速开发平台,能快速开发项目并交付【接私活利器】

renren-security 相比于 renren-fast 在后端功能的区别主要在于:renren-security 提供了权限管理功能,另外还额外提供了数据字典和代码生成器。

相关地址 :

  1. renren-security :gitee.com/renrenio/re…
  2. renren-fast:gitee.com/renrenio/re…
  3. 官网:www.renren.io/

推荐理由

  1. 被很多企业采用,说明稳定性和社区活跃度不错。
  2. 微服务版 renren-cloud(这个一般企业也用不上吧!)和 renren-security 需要收费才能正常使用,renren-fast 属于完全免费并且提供了详细的文档,不过,完整文档需要捐赠 80 元才能获取到。

项目展示

renren-fast菜单管理

renren-fast菜单管理

renren-fast定时任务

renren-fast定时任务

SpringBlade

推荐指数 :⭐⭐⭐⭐⭐

简介

SpringBlade 是一个由商业级项目升级优化而来的 SpringCloud 分布式微服务架构、SpringBoot 单体式微服务架构并存的综合型项目,采用 Java8 API 重构了业务代码,完全遵循阿里巴巴编码规范。采用 Spring Boot 2 、Spring Cloud Hoxton 、Mybatis 等核心技术,同时提供基于 React 和 Vue 的两个前端框架用于快速搭建企业级的 SaaS 多租户微服务平台。

SpringBlade架构图

SpringBlade架构图

相关地址 :

  1. 后端 Gitee 地址:gitee.com/smallc/Spri…
  2. 后端 Github 地址:github.com/chillzhuang…
  3. 后端 SpringBoot 版:gitee.com/smallc/Spri…
  4. 前端框架 Sword(基于 React):gitee.com/smallc/Swor…
  5. 前端框架 Saber(基于 Vue):gitee.com/smallc/Sabe…
  6. 核心框架项目地址:github.com/chillzhuang…
  7. 官网:bladex.vip

推荐理由

  1. 允许免费用于学习、毕设、公司项目、私活等。 如果商用的话,需要授权,并且功能更加完善。
  2. 前后端分离,后端采用 SpringCloud 全家桶,单独开源出一个框架:BladeTool (感觉很厉害)
  3. 集成 Sentinel 从流量控制、熔断降级、系统负载等多个维度保护服务的稳定性。
  4. 借鉴 OAuth2,实现了多终端认证系统,可控制子系统的 token 权限互相隔离。
  5. 借鉴 Security,封装了 Secure 模块,采用 JWT 做 Token 认证,可拓展集成 Redis 等细颗粒度控制方案。
  6. 项目分包明确,规范微服务的开发模式,使包与包之间的分工清晰。

SpringBlade工程结构

SpringBlade工程结构

项目展示

Sword后端管理页面

Sword后端管理页面

Sword菜单管理页面

Sword菜单管理页面

COLA

推荐指数 :⭐⭐⭐⭐⭐

简介

根据我的了解来看,很多公司的项目都是基于 COLA 进行开发的,相比于其他快速开发脚手架,COLA 并不提供什么已经开发好的功能,它提供的主要是一个干净的架构,然后你可以在此基础上进行开发。

如下图所示,一个通过一行命令就生成好的 web 后端项目骨架是下面这样的:

COLA应用架构

COLA应用架构

COLA 既是框架,也是架构。创建 COLA 的主要目的是为应用架构提供一套简单的可以复制、可以理解、可以落地、可以控制复杂性的”指导和约束”。

  • 框架部分主要是以二方库的形式被应用依赖和使用。
  • 架构部分主要是提供了创建符合 COLA 要求的应用 Archetype。

相关地址:

  1. Github 地址:github.com/alibaba/COL…
  2. COLA 2.0 介绍:blog.csdn.net/significant…

推荐理由

  1. 模块之间划分清晰;
  2. 一键生成项目骨架;
  3. 继承了常用的类和功能比如日志功能;
  4. 统一的返回格式以及错误处理;

项目展示

一行命令生成的 web 后端项目骨架

一行命令生成的 web 后端项目骨架

后端返回结果示意图

后端返回结果示意图

SpringBoot_v2

推荐指数 :⭐⭐⭐⭐

简介

SpringBoot_v2项目是努力打造springboot框架的极致细腻的脚手架。原生纯净,可在线生成controller、mapperxml、dao、service、html、sql代码,极大减少开发难度,增加开发进度神器脚手架!!不求回报,你使用快乐就是这个项目最大的快乐!后台管理包含代码生成器。

相关地址 :

  1. Github地址 :github.com/fuce1314/Sp…
  2. Gitee地址 : gitee.com/bdj/SpringB…
  3. 相关文档 : gitee.com/bdj/SpringB…

推荐理由

  1. 没有基础版、没有vip版本、没有付费群、没有收费二维码。
  2. 对新手友好,配置好数据库连接即可运行。
  3. 满足一般中小企业的基本需求。
  4. 功能简单,无其他杂七杂八的功能

项目展示

后台首页

后台首页

后台添加电子邮件

后台添加电子邮件

推荐阅读

  1. 接近8000字的Spring/SpringBoot常用注解总结!安排!
  2. 面试官问我Java814的有哪些重要的新特性,我哭了
  3. 第一弹!安排!安利10个让你爽到爆的IDEA必备插件!
  4. 完结撒花!JavaGuide面试突击版来啦!

作者介绍: Github 80k Star 项目 JavaGuide(公众号同名) 作者。每周都会在公众号更新一些自己原创干货。公众号后台回复“1”领取Java工程师必备学习资料+面试突击pdf。


本文使用 mdnice 排版

本文转载自: 掘金

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

0xA07 Android 10 源码分析:Window 的

发表于 2020-06-05

引言

  • 这是 Android 10 源码分析系列的第 7 篇
  • 分支:android-10.0.0_r14
  • 全文阅读大概 10 分钟

在之前的文章 0xA06 Android 10 源码分析:WindowManager 视图绑定以及体系结构 介绍了 Activity、Window、PhoneWindow、WindowManager 之间的关系,以及 Activity 和 Dialog 的视图绑定过程,而这篇文章主要两个目的:

  1. 对上一篇文章 0xA06 Android 10 源码分析:WindowManager 视图绑定以及体系结构 做深入的了解
  2. 为后面的篇文章「如何在 Andorid 系统里添加自定义 View」等等做好铺垫

通过这篇文章你将学习到以下内容,将在文末总结部分会给出相应的答案

  • Window 都有那些常用的参数?
  • Window 都那些类型?每个类型的意思?以及作用?
  • Window 那些过时的 API 以及处理方案?
  • Window 视图层级顺序是如何确定的?
  • Window 都那些 flag?每个 flag 的意思?以及作用?
  • Window 的软键盘模式?每个模式的意思?以及如何使用?
  • Kotlin 小技巧?

在开始分析之前,我们先来看一张图,熟悉一下几个基本概念,这些概念伴将随着整篇文章

Window 视图层级顺序

  • 我们在手机上看到的界面是二维的,但是实际上是一个三维,如上图所示
  • Window:是一个抽象类,它作为一个顶级视图添加到 WindowManager 中,View 是依附于 Window 而存在的,对 View 进行管理
  • WindowManager:它是一个接口,继承自接口 ViewManager,对 Window 进行管理
  • PhoneWindow:Window 唯一实现类,添加到 WindowManager 的根容器中
  • WindowManagerService:WindowManager 是 Window 的容器,管理着 Window,对 Window 进行添加和删除,最终具体的工作都是由 WindowManagerService 来处理的,WindowManager 和 WindowManagerService 通过 Binder 来进行跨进程通信,WindowManagerService 才是 Window 的最终管理者

这篇文章重要知识点是 Window 视图层级顺序是如何确定的,其他内容都是一些概念的东西,可以选择性的阅读,了解完基本概念之后,进入这篇文章的核心内容,我们先来了解一下 Window 都有那些常用的参数

Window 都有那些常用的参数

Window 的参数都被定义在 WindowManager 的静态内部类 LayoutParams 中

frameworks/base/core/java/android/view/WindowManager#LayoutParams.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
复制代码public static class LayoutParams extends ViewGroup.LayoutParams implements Parcelable {
// window 左上角的 x 坐标
public int x;

// window 左上角的 y 坐标
public int y;

// Window 的类型
public int type;

// Window 的 flag 用于控制 Window 的显示
public int flags;

// window 软键盘输入区域的显示模式
public int softInputMode;

// window 的透明度,取值为0-1
public float alpha = 1.0f;

// window 在屏幕中的位置
public int gravity;

// window 的像素点格式,值定义在 PixelFormat 中
public int format;
}

接下来我们我们主要来介绍一下 Window 的类型、Window 视图层级顺序、Window 的 flag、和 window 软键盘模式

Window 都那些类型以及作用

Window 的类型大概可以分为三类: 应用程序 Window(Application Window)、子 Window(Sub Windwow)、系统 Window(System Window), Window 的类型通过 type 值来表示,每个大类型又包含多个小类型,它们都定义在 WindowManager 的静态内部类 LayoutParams

frameworks/base/core/java/android/view/WindowManager#LayoutParams.java

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
复制代码public static class LayoutParams extends ViewGroup.LayoutParams implements Parcelable {
public int type;

// 应用程序 Window 的开始值
public static final int FIRST_APPLICATION_WINDOW = 1;
// 应用程序 Window 的结束值
public static final int LAST_APPLICATION_WINDOW = 99;

// 子 Window 类型的开始值
public static final int FIRST_SUB_WINDOW = 1000;
// 子 Window 类型的结束值
public static final int LAST_SUB_WINDOW = 1999;

// 系统 Window 类型的开始值
public static final int FIRST_SYSTEM_WINDOW = 2000;
// 系统 Window 类型的结束值
public static final int LAST_SYSTEM_WINDOW = 2999;
}
类型 值 备注
FIRST_APPLICATION_WINDOW 1 应用程序 Window 的开始值
LAST_APPLICATION_WINDOW 99 应用程序 Window 的结束值
FIRST_SUB_WINDOW 1000 子 Window 的开始值
LAST_SUB_WINDOW 1999 子 Window 的结束值
FIRST_SYSTEM_WINDOW 2000 系统 Window 的开始值
LAST_SYSTEM_WINDOW 2999 系统 Window 的结束值

小技巧:如果是层级在 2000(FIRST_SYSTEM_WINDOW)以下的是不需要申请弹窗权限的

  • 应用程序 Window(Application Window):它的区间范围 [1,99],例如 Activity

frameworks/base/core/java/android/view/WindowManager#LayoutParams.java

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
复制代码// 应用程序 Window 的开始值
public static final int FIRST_APPLICATION_WINDOW = 1;

// 应用程序 Window 的基础值
public static final int TYPE_BASE_APPLICATION = 1;

// 普通的应用程序
public static final int TYPE_APPLICATION = 2;

// 特殊的应用程序窗口,当程序可以显示 Window 之前使用这个 Window 来显示一些东西
public static final int TYPE_APPLICATION_STARTING = 3;

// TYPE_APPLICATION 的变体,在应用程序显示之前,WindowManager 会等待这个 Window 绘制完毕
public static final int TYPE_DRAWN_APPLICATION = 4;

// 应用程序 Window 的结束值
public static final int LAST_APPLICATION_WINDOW = 99;
类型 备注
FIRST_APPLICATION_WINDOW 应用程序 Window 的开始值
TYPE_BASE_APPLICATION 应用程序 Window 的基础值
TYPE_APPLICATION 普通的应用程序
TYPE_APPLICATION_STARTING 特殊的应用程序窗口,当程序可以显示 Window 之前使用这个 Window 来显示一些东西
TYPE_DRAWN_APPLICATION TYPE_APPLICATION 的变体 在应用程序显示之前,WindowManager 会等待这个 Window 绘制完毕
LAST_APPLICATION_WINDOW 应用程序 Window 的结束值
  • 子 Window(Sub Windwow):它的区间范围 [1000,1999],这些 Window 按照 Z-order 顺序依附于父 Window 上(关于 Z-order 后文有介绍),并且他们的坐标空间相对于父 Window 的,例如:PopupWindow

frameworks/base/core/java/android/view/WindowManager#LayoutParams.java

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
复制代码// 子 Window 类型的开始值
public static final int FIRST_SUB_WINDOW = 1000;

// 应用程序 Window 顶部的面板。这些 Window 出现在其附加 Window 的顶部。
public static final int TYPE_APPLICATION_PANEL = FIRST_SUB_WINDOW;

// 用于显示媒体(如视频)的 Window。这些 Window 出现在其附加 Window 的后面。
public static final int TYPE_APPLICATION_MEDIA = FIRST_SUB_WINDOW + 1;

// 应用程序 Window 顶部的子面板。这些 Window 出现在其附加 Window 和任何Window的顶部
public static final int TYPE_APPLICATION_SUB_PANEL = FIRST_SUB_WINDOW + 2;

// 当前Window的布局和顶级Window布局相同时,不能作为子代的容器
public static final int TYPE_APPLICATION_ATTACHED_DIALOG = FIRST_SUB_WINDOW + 3;

// 用显示媒体 Window 覆盖顶部的 Window, 这是系统隐藏的 API
public static final int TYPE_APPLICATION_MEDIA_OVERLAY = FIRST_SUB_WINDOW + 4;

// 子面板在应用程序Window的顶部,这些Window显示在其附加Window的顶部, 这是系统隐藏的 API
public static final int TYPE_APPLICATION_ABOVE_SUB_PANEL = FIRST_SUB_WINDOW + 5;

// 子 Window 类型的结束值
public static final int LAST_SUB_WINDOW = 1999;
类型 备注
FIRST_SUB_WINDOW 子 Window 的开始值
TYPE_APPLICATION_PANEL 应用程序 Window 顶部的面板,这些 Window 出现在其附加 Window 的顶部
TYPE_APPLICATION_MEDIA 用于显示媒体(如视频)的 Window,这些 Window 出现在其附加 Window 的后面
TYPE_APPLICATION_SUB_PANEL 应用程序 Window 顶部的子面板,这些 Window 出现在其附加 Window 和任何Window的顶部
TYPE_APPLICATION_ATTACHED_DIALOG 当前Window的布局和顶级Window布局相同时,不能作为子代的容器
TYPE_APPLICATION_MEDIA_OVERLAY 用显示媒体 Window 覆盖顶部的 Window, 这是系统隐藏的 API
TYPE_APPLICATION_ABOVE_SUB_PANEL 子面板在应用程序Window的顶部,这些Window显示在其附加Window的顶部, 这是系统隐藏的 API
LAST_SUB_WINDOW 子 Window 的结束值
  • 系统 Window(System Window): 它区间范围 [2000,2999],例如:Toast,输入法窗口,系统音量条窗口,系统错误窗口

frameworks/base/core/java/android/view/WindowManager#LayoutParams.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
59
复制代码// 系统Window类型的开始值
public static final int FIRST_SYSTEM_WINDOW = 2000;

// 系统状态栏,只能有一个状态栏,它被放置在屏幕的顶部,所有其他窗口都向下移动
public static final int TYPE_STATUS_BAR = FIRST_SYSTEM_WINDOW;

// 系统搜索窗口,只能有一个搜索栏,它被放置在屏幕的顶部
public static final int TYPE_SEARCH_BAR = FIRST_SYSTEM_WINDOW+1;

@Deprecated
// API 已经过时,用 TYPE_APPLICATION_OVERLAY 代替
public static final int TYPE_PHONE = FIRST_SYSTEM_WINDOW+2;

@Deprecated
// API 已经过时,用 TYPE_APPLICATION_OVERLAY 代替
public static final int TYPE_SYSTEM_ALERT = FIRST_SYSTEM_WINDOW+3;

// 已经从系统中被移除,可以使用 TYPE_KEYGUARD_DIALOG 代替
public static final int TYPE_KEYGUARD = FIRST_SYSTEM_WINDOW+4;

@Deprecated
// API 已经过时,用 TYPE_APPLICATION_OVERLAY 代替
public static final int TYPE_TOAST = FIRST_SYSTEM_WINDOW+5;

@Deprecated
// API 已经过时,用 TYPE_APPLICATION_OVERLAY 代替
public static final int TYPE_SYSTEM_OVERLAY = FIRST_SYSTEM_WINDOW+6;

@Deprecated
// API 已经过时,用 TYPE_APPLICATION_OVERLAY 代替
public static final int TYPE_PRIORITY_PHONE = FIRST_SYSTEM_WINDOW+7;

// 系统对话框窗口
public static final int TYPE_SYSTEM_DIALOG = FIRST_SYSTEM_WINDOW+8;

// 锁屏时显示的对话框
public static final int TYPE_KEYGUARD_DIALOG = FIRST_SYSTEM_WINDOW+9;

@Deprecated
// API 已经过时,用 TYPE_APPLICATION_OVERLAY 代替
public static final int TYPE_SYSTEM_ERROR = FIRST_SYSTEM_WINDOW+10;

// 输入法窗口,位于普通 UI 之上,应用程序可重新布局以免被此窗口覆盖
public static final int TYPE_INPUT_METHOD = FIRST_SYSTEM_WINDOW+11;

// 输入法对话框,显示于当前输入法窗口之上
public static final int TYPE_INPUT_METHOD_DIALOG= FIRST_SYSTEM_WINDOW+12;

// 墙纸
public static final int TYPE_WALLPAPER = FIRST_SYSTEM_WINDOW+13;

// 状态栏的滑动面板
public static final int TYPE_STATUS_BAR_PANEL = FIRST_SYSTEM_WINDOW+14;

// 应用程序叠加窗口显示在所有窗口之上
public static final int TYPE_APPLICATION_OVERLAY = FIRST_SYSTEM_WINDOW + 38;

// 系统Window类型的结束值
public static final int LAST_SYSTEM_WINDOW = 2999;
类型 备注
FIRST_SYSTEM_WINDOW 系统 Window 类型的开始值
TYPE_STATUS_BAR 系统状态栏,只能有一个状态栏,它被放置在屏幕的顶部,所有其他窗口都向下移动
TYPE_SEARCH_BAR 系统搜索窗口,只能有一个搜索栏,它被放置在屏幕的顶部
TYPE_PHONE API 已经过时,用 TYPE_APPLICATION_OVERLAY 代替
TYPE_SYSTEM_ALERT API 已经过时,用 TYPE_APPLICATION_OVERLAY 代替
TYPE_KEYGUARD 已经从系统中被移除,可以使用 TYPE_KEYGUARD_DIALOG 代替
TYPE_TOAST API 已经过时,用 TYPE_APPLICATION_OVERLAY 代替
TYPE_SYSTEM_OVERLAY API 已经过时,用 TYPE_APPLICATION_OVERLAY 代替
TYPE_PRIORITY_PHONE API 已经过时,用 TYPE_APPLICATION_OVERLAY 代替
TYPE_SYSTEM_ERROR API 已经过时,用 TYPE_APPLICATION_OVERLAY 代替
TYPE_APPLICATION_OVERLAY 应用程序叠加窗口显示在所有窗口之上
TYPE_SYSTEM_DIALOG 系统对话框窗口
TYPE_KEYGUARD_DIALOG 锁屏时显示的对话框
TYPE_INPUT_METHOD 输入法窗口,位于普通 UI 之上,应用程序可重新布局以免被此窗口覆盖
TYPE_INPUT_METHOD_DIALOG 输入法对话框,显示于当前输入法窗口之上
TYPE_WALLPAPER 墙纸
TYPE_STATUS_BAR_PANEL 状态栏的滑动面板
LAST_SYSTEM_WINDOW 系统 Window 类型的结束值

需要注意的是:

  1. TYPE_PHONE、TYPE_SYSTEM_ALERT、TYPE_TOAST、TYPE_SYSTEM_OVERLAY、TYPE_PRIORITY_PHONE、TYPE_SYSTEM_ERROR 这些 type 在 API 26 中均已经过时,使用 TYPE_APPLICATION_OVERLAY 代替,需要申请 Manifest.permission.SYSTEM_ALERT_WINDOW 权限
  2. TYPE_KEYGUARD 已经被从系统中移除,可以使用 TYPE_KEYGUARD_DIALOG 来代替

Window 视图层级顺序

我们在手机上看的是二维的,但是实际上是三维的显示,如下图所示

640

在文章开头介绍了参数类型包含了 Window 的 x 轴坐标、Window 的 y 轴坐标, 既然是一个三维坐标系,那么 z 轴坐标在哪里? 接下来就是我们要分析的非常重要的一个类 WindowManagerService,当添加 Window 的时候已经确定好了 Window 的层级,显示的时候才会根据当前的层级确定 Window 应该在哪一层显示

WindowManager 是 Window 的容器,管理着 Window,对 Window 进行添加和删除,具体的工作都是由 WMS 来处理的,WindowManager 和 WMS 通过 Binder 来进行跨进程通信,WMS 才是 Window 的最终管理者,我先来看一下 WMS 的 addWindow 方法

frameworks/base/services/core/java/com/android/server/wm/WindowManagerService.java

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
复制代码public int addWindow(Session session, IWindow client, int seq,
LayoutParams attrs, int viewVisibility, int displayId, Rect outFrame,
Rect outContentInsets, Rect outStableInsets, Rect outOutsets,
DisplayCutout.ParcelableWrapper outDisplayCutout, InputChannel outInputChannel,
InsetsState outInsetsState) {

final WindowState win = new WindowState(this, session, client, token, parentWindow,
appOp[0], seq, attrs, viewVisibility, session.mUid,
session.mCanAddInternalSystemWindow);
......

win.mToken.addWindow(win);
......

win.getParent().assignChildLayers();
......

}
  • WindowState 计算当前 Window 层级
  • win.mToken.addWindow 这个方法将当前的 win 放入 WindowList 中,WindowList 是一个 ArrayList
  • displayContent.assignWindowLayers 方法 计算 z-order 值, z-order 值越大越靠前,就越靠近用户

Window 视图层级顺序 用 Z-order 来表示,Z-order 对应着 WindowManager.LayoutParams 的 type 值,Z-order 可以理解为 Android 视图的层级概念,值越大越靠前,就越靠近用户。

WindowState 就是 windowManager 中的窗口,一个 WindowState 表示一个 window

那么 Z-order 的值的计算逻辑在 WindowState 类中,WindowState 构造的时候初始化当前的 mBaseLayer 和 mSubLayer,这两个参数应该是决定 z-order 的两个因素

frameworks/base/services/core/java/com/android/server/wm/WindowState.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
复制代码static final int TYPE_LAYER_MULTIPLIER = 10000;
static final int TYPE_LAYER_OFFSET = 1000;

WindowState(WindowManagerService service, Session s, IWindow c, WindowToken token,
WindowState parentWindow, int appOp, int seq, WindowManager.LayoutParams a,
int viewVisibility, int ownerId, boolean ownerCanAddInternalSystemWindow,
PowerManagerWrapper powerManagerWrapper) {

// 判断该是否在子 Window 的类型范围内[1000,1999]
if (mAttrs.type >= FIRST_SUB_WINDOW && mAttrs.type <= LAST_SUB_WINDOW) {

// 调用 getWindowLayerLw 方法返回值在[1,33]之间,根据不同类型的 Window 在屏幕上进行排序
mBaseLayer = mPolicy.getWindowLayerLw(parentWindow)
* TYPE_LAYER_MULTIPLIER + TYPE_LAYER_OFFSET;

// mSubLayer 子窗口的顺序
// 调用 getSubWindowLayerFromTypeLw 方法返回值在[-2.3]之间 ,返回子 Window 相对于父 Window 的位置
mSubLayer = mPolicy.getSubWindowLayerFromTypeLw(a.type);
......

} else {

mBaseLayer = mPolicy.getWindowLayerLw(this)
* TYPE_LAYER_MULTIPLIER + TYPE_LAYER_OFFSET;
mSubLayer = 0;
......

}
}
  • mBaseLayer 是基础序,对应的区间范围 [1,33]
  • mSubLayer 相同分组下的子 Window 的序,对应的区间范围 [-2.3]
  • 判断该是否在子 Window 的类型范围内[1000,1999]
  • 如果是子 Window,调用 getWindowLayerLw 方法,计算 mBaseLayer 的值,返回一个用来对 Window 进行排序的任意整数,调用 getSubWindowLayerFromTypeLw 方法,计算 mSubLayer 的值,返回子 Window 相对于父 Window 的位置
  • 如果不是子 Window,调用 getWindowLayerLw 方法,计算 mBaseLayer 的值,返回一个用来对 Window 进行排序的任意整数,mSubLayer 值为 0

计算 mBaseLayer 的值

调用 WindowManagerPolicy 的 getWindowLayerLw 方法,计算 mBaseLayer 的值

frameworks/base/services/core/java/com/android/server/policy/WindowManagerPolicy.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
复制代码int APPLICATION_LAYER = 2;
int APPLICATION_MEDIA_SUBLAYER = -2;
int APPLICATION_MEDIA_OVERLAY_SUBLAYER = -1;
int APPLICATION_PANEL_SUBLAYER = 1;
int APPLICATION_SUB_PANEL_SUBLAYER = 2;
int APPLICATION_ABOVE_SUB_PANEL_SUBLAYER = 3;

/**
* 根据不同类型的 Window 在屏幕上进行排序
* 返回一个用来对窗口进行排序的任意整数,数字越小,表示的值越小
*/
default int getWindowLayerFromTypeLw(int type, boolean canAddInternalSystemWindow) {
// 判断是否在应用程序 Window 类型的取值范围内 [1,99]
if (type >= FIRST_APPLICATION_WINDOW && type <= LAST_APPLICATION_WINDOW) {
return APPLICATION_LAYER;
}


switch (type) {
case TYPE_WALLPAPER: // 壁纸,通过 window manager 删除它
return 1;
case TYPE_PHONE: // 电话
return 3;
case TYPE_SEARCH_BAR: // 搜索栏
return 6;
case TYPE_SYSTEM_DIALOG: // 系统的 dialog
return 7;
case TYPE_TOAST: // 系统 toast
return 8;
case TYPE_INPUT_METHOD: // 输入法
return 15;
case TYPE_STATUS_BAR: // 状态栏
return 17;
case TYPE_KEYGUARD_DIALOG: //锁屏
return 20;
......

case TYPE_POINTER:
// the (mouse) pointer layer
return 33;
default:
return APPLICATION_LAYER;
}
}

根据不同类型的 Window 在屏幕上进行排序,返回一个用来对 Window 进行排序的任意整数,数字越小,表示的值越小,通过以下公式来计算它的基础序 ,基础序越大,Z-order 值越大越靠前,就越靠近用户,我们以 Activity 为例:

Activity 属于应用层 Window,它的取值范围在 [1,99] 内,调用 getWindowLayerLw 方法返回 APPLICATION_LAYER,APPLICATION_LAYER 值为 2,通过下面方法进行计算

1
2
3
4
5
复制代码static final int TYPE_LAYER_MULTIPLIER = 10000;
static final int TYPE_LAYER_OFFSET = 1000;

mBaseLayer = mPolicy.getWindowLayerLw(parentWindow)
* TYPE_LAYER_MULTIPLIER + TYPE_LAYER_OFFSET;

那么最终 Activity 的 mBaseLayer 值是 21000

计算 mSubLayer 的值

调用 getSubWindowLayerFromTypeLw 方法 ,传入 WindowManager.LayoutParams 的实例 a 的 type 值,计算 mSubLayer 的值

frameworks/base/services/core/java/com/android/server/policy/WindowManagerPolicy.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
复制代码int APPLICATION_LAYER = 2;
int APPLICATION_MEDIA_SUBLAYER = -2;
int APPLICATION_MEDIA_OVERLAY_SUBLAYER = -1;
int APPLICATION_PANEL_SUBLAYER = 1;
int APPLICATION_SUB_PANEL_SUBLAYER = 2;
int APPLICATION_ABOVE_SUB_PANEL_SUBLAYER = 3;

/**
* 计算 Window 相对于父 Window 的位置
* 返回 一个整数,正值在前面,表示在父 Window 上面,负值在后面,表示在父 Window 的下面
*/
default int getSubWindowLayerFromTypeLw(int type) {
switch (type) {
case TYPE_APPLICATION_PANEL: // 1000
case TYPE_APPLICATION_ATTACHED_DIALOG: // 1003
return APPLICATION_PANEL_SUBLAYER; // return 1
case TYPE_APPLICATION_MEDIA:// 1001
return APPLICATION_MEDIA_SUBLAYER;// return -2
case TYPE_APPLICATION_MEDIA_OVERLAY:
return APPLICATION_MEDIA_OVERLAY_SUBLAYER; // return -1
case TYPE_APPLICATION_SUB_PANEL:// 1002
return APPLICATION_SUB_PANEL_SUBLAYER;// return 2
case TYPE_APPLICATION_ABOVE_SUB_PANEL:
return APPLICATION_ABOVE_SUB_PANEL_SUBLAYER;// return 3
}
return 0;
}

计算子 Window 相对于父 Window 的位置,返回一个整数,正值表示在父 Window 上面,负值表示在父 Window 的下面

Window 的 flag

Window 的 flag 用于控制 Window 的显示,它们的值也是定义在 WindowManager 的内部类 LayoutParams 中

frameworks/base/core/java/android/view/WindowManager#LayoutParams.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
复制代码// 当 Window 可见时允许锁屏
public static final int FLAG_ALLOW_LOCK_WHILE_SCREEN_ON = 0x00000001;

// Window 后面的内容都变暗
public static final int FLAG_DIM_BEHIND = 0x00000002;

@Deprecated
// API 已经过时,Window 后面的内容都变模糊
public static final int FLAG_BLUR_BEHIND = 0x00000004;

// Window 不能获得输入焦点,即不接受任何按键或按钮事件,例如该 Window 上 有 EditView,点击 EditView 是 不会弹出软键盘的
// Window 范围外的事件依旧为原窗口处理;例如点击该窗口外的view,依然会有响应。另外只要设置了此Flag,都将会启用FLAG_NOT_TOUCH_MODAL
public static final int FLAG_NOT_FOCUSABLE = 0x00000008;

// 设置了该 Flag,将 Window 之外的按键事件发送给后面的 Window 处理, 而自己只会处理 Window 区域内的触摸事件
// Window 之外的 view 也是可以响应 touch 事件。
public static final int FLAG_NOT_TOUCH_MODAL = 0x00000020;

// 设置了该Flag,表示该 Window 将不会接受任何 touch 事件,例如点击该 Window 不会有响应,只会传给下面有聚焦的窗口。
public static final int FLAG_NOT_TOUCHABLE = 0x00000010;

// 只要 Window 可见时屏幕就会一直亮着
public static final int FLAG_KEEP_SCREEN_ON = 0x00000080;

// 允许 Window 占满整个屏幕
public static final int FLAG_LAYOUT_IN_SCREEN = 0x00000100;

// 允许 Window 超过屏幕之外
public static final int FLAG_LAYOUT_NO_LIMITS = 0x00000200;

// 全屏显示,隐藏所有的 Window 装饰,比如在游戏、播放器中的全屏显示
public static final int FLAG_FULLSCREEN = 0x00000400;

// 表示比FLAG_FULLSCREEN低一级,会显示状态栏
public static final int FLAG_FORCE_NOT_FULLSCREEN = 0x00000800;

// 当用户的脸贴近屏幕时(比如打电话),不会去响应此事件
public static final int FLAG_IGNORE_CHEEK_PRESSES = 0x00008000;

// 则当按键动作发生在 Window 之外时,将接收到一个MotionEvent.ACTION_OUTSIDE事件。
public static final int FLAG_WATCH_OUTSIDE_TOUCH = 0x00040000;

@Deprecated
// 窗口可以在锁屏的 Window 之上显示, 使用 Activity#setShowWhenLocked(boolean) 方法代替
public static final int FLAG_SHOW_WHEN_LOCKED = 0x00080000;

// 表示负责绘制系统栏背景。如果设置,系统栏将以透明背景绘制,
// 此 Window 中的相应区域将填充 Window#getStatusBarColor()和 Window#getNavigationBarColor()中指定的颜色。
public static final int FLAG_DRAWS_SYSTEM_BAR_BACKGROUNDS = 0x80000000;

// 表示要求系统壁纸显示在该 Window 后面,Window 表面必须是半透明的,才能真正看到它背后的壁纸
public static final int FLAG_SHOW_WALLPAPER = 0x00100000;
flag 备注
FLAG_ALLOW_LOCK_WHILE_SCREEN_ON 当 Window 可见时允许锁屏
FLAG_DIM_BEHIND Window 后面的内容都变暗
FLAG_BLUR_BEHIND API 已经过时,Window 后面的内容都变模糊
FLAG_NOT_FOCUSABLE Window 不能获得输入焦点,即不接受任何按键或按钮事件,例如该 Window 上 有 EditView,点击 EditView 是 不会弹出软键盘的,Window 范围外的事件依旧为原窗口处理;例如点击该窗口外的view,依然会有响应。另外只要设置了此Flag,都将会启用FLAG_NOT_TOUCH_MODAL
FLAG_NOT_TOUCH_MODAL 设置了该 Flag,将 Window 之外的按键事件发送给后面的 Window 处理, 而自己只会处理 Window 区域内的触摸事件,Window 之外的 view 也是可以响应 touch 事件
FLAG_NOT_TOUCHABLE 设置了该Flag,表示该 Window 将不会接受任何 touch 事件,例如点击该 Window 不会有响应,只会传给下面有聚焦的窗口
FLAG_KEEP_SCREEN_ON 只要 Window 可见时屏幕就会一直亮着
FLAG_LAYOUT_IN_SCREEN 允许 Window 占满整个屏幕
FLAG_LAYOUT_NO_LIMITS 允许 Window 超过屏幕之外
FLAG_FULLSCREEN 全屏显示,隐藏所有的 Window 装饰,比如在游戏、播放器中的全屏显示
FLAG_FORCE_NOT_FULLSCREEN 表示比FLAG_FULLSCREEN低一级,会显示状态栏
FLAG_IGNORE_CHEEK_PRESSES 当用户的脸贴近屏幕时(比如打电话),不会去响应此事件
FLAG_WATCH_OUTSIDE_TOUCH 则当按键动作发生在 Window 之外时,将接收到一个MotionEvent.ACTION_OUTSIDE事件
FLAG_SHOW_WHEN_LOCKED 已经过时,窗口可以在锁屏的 Window 之上显示, 使用 Activity#setShowWhenLocked(boolean) 方法代替
FLAG_DRAWS_SYSTEM_BAR_BACKGROUNDS 表示负责绘制系统栏背景。如果设置,系统栏将以透明背景绘制, 此 Window 中的相应区域将填充 Window#getStatusBarColor()和 Window#getNavigationBarColor()中指定的颜色
FLAG_SHOW_WALLPAPER 表示要求系统壁纸显示在该 Window 后面,Window 表面必须是半透明的,才能真正看到它背后的壁纸

window 软键盘模式

表示 window 软键盘输入区域的显示模式,常见的情况 Window 的软键盘打开会占据整个屏幕,遮挡了后面的视图,例如看直播的时候底部有个输入框点击的时候,输入框随着键盘一起上来,而有的时候,希望键盘覆盖在所有的 View 之上,界面保持不动等等

软键盘模式(SoftInputMode) 值,与 AndroidManifest 中 Activity 的属性 android:windowSoftInputMode 是对应的,因此可以在 AndroidManifest 文件中为 Activity 设置android:windowSoftInputMode

1
复制代码 <activity android:windowSoftInputMode="adjustNothing" />

也可以在 Java 代码中为 Window 设置 SoftInputMode

1
复制代码getWindow().setSoftInputMode(WindowManager.LayoutParams.SOFT_INPUT_ADJUST_NOTHING);

SoftInputMode 常用的有以下几个值

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
复制代码// 不会改变软键盘的状态
public static final int SOFT_INPUT_STATE_UNCHANGED = 1;

// 当用户进入该窗口时,隐藏软键盘
public static final int SOFT_INPUT_STATE_HIDDEN = 2;

// 当窗口获取焦点时,隐藏软键盘
public static final int SOFT_INPUT_STATE_ALWAYS_HIDDEN = 3;

// 当用户进入窗口时,显示软键盘
public static final int SOFT_INPUT_STATE_VISIBLE = 4;

// 当窗口获取焦点时,显示软键盘
public static final int SOFT_INPUT_STATE_ALWAYS_VISIBLE = 5;

// window会调整大小以适应软键盘窗口
public static final int SOFT_INPUT_MASK_ADJUST = 0xf0;

// 没有指定状态,系统会选择一个合适的状态或依赖于主题的设置
public static final int SOFT_INPUT_ADJUST_UNSPECIFIED = 0x00;

// 当软键盘弹出时,窗口会调整大小,例如点击一个EditView,整个layout都将平移可见且处于软件盘的上方
// 同样的该模式不能与SOFT_INPUT_ADJUST_PAN结合使用;
// 如果窗口的布局参数标志包含FLAG_FULLSCREEN,则将忽略这个值,窗口不会调整大小,但会保持全屏。
public static final int SOFT_INPUT_ADJUST_RESIZE = 0x10;

// 当软键盘弹出时,窗口不需要调整大小, 要确保输入焦点是可见的,
// 例如有两个EditView的输入框,一个为Ev1,一个为Ev2,当你点击Ev1想要输入数据时,当前的Ev1的输入框会移到软键盘上方
// 该模式不能与SOFT_INPUT_ADJUST_RESIZE结合使用
public static final int SOFT_INPUT_ADJUST_PAN = 0x20;

// 将不会调整大小,直接覆盖在window上
public static final int SOFT_INPUT_ADJUST_NOTHING = 0x30;
model 备注
SOFT_INPUT_STATE_UNCHANGED 不会改变软键盘的状态
SOFT_INPUT_STATE_VISIBLE 当用户进入窗口时,显示软键盘
SOFT_INPUT_STATE_HIDDEN 当用户进入该窗口时,隐藏软键盘
SOFT_INPUT_STATE_ALWAYS_HIDDEN 当窗口获取焦点时,隐藏软键盘
SOFT_INPUT_STATE_ALWAYS_VISIBLE 当窗口获取焦点时,显示软键盘
SOFT_INPUT_MASK_ADJUST window 会调整大小以适应软键盘窗口
SOFT_INPUT_ADJUST_UNSPECIFIED 没有指定状态,系统会选择一个合适的状态或依赖于主题的设置
SOFT_INPUT_ADJUST_RESIZE 1. 当软键盘弹出时,窗口会调整大小,例如点击一个EditView,整个layout都将平移可见且处于软件盘的上方 2. 同样的该模式不能与SOFT_INPUT_ADJUST_PAN结合使用 3. 如果窗口的布局参数标志包含FLAG_FULLSCREEN,则将忽略这个值,窗口不会调整大小,但会保持全屏
SOFT_INPUT_ADJUST_PAN 1. 当软键盘弹出时,窗口不需要调整大小, 要确保输入焦点是可见的 2. 例如有两个EditView的输入框,一个为Ev1,一个为Ev2,当你点击Ev1想要输入数据时,当前的Ev1的输入框会移到软键盘上方 3. 该模式不能与SOFT_INPUT_ADJUST_RESIZE结合使用
SOFT_INPUT_ADJUST_NOTHING 将不会调整大小,直接覆盖在window上

Kotlin 小技巧

利用 plus (+) 和 plus (-) 对 Map 集合做运算,如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
复制代码fun main() {
val numbersMap = mapOf("one" to 1, "two" to 2, "three" to 3)

// plus (+)
println(numbersMap + Pair("four", 4)) // {one=1, two=2, three=3, four=4}
println(numbersMap + Pair("one", 10)) // {one=10, two=2, three=3}
println(numbersMap + Pair("five", 5) + Pair("one", 11)) // {one=11, two=2, three=3, five=5}

// plus (-)
println(numbersMap - "one") // {two=2, three=3}
println(numbersMap - listOf("two", "four")) // {one=1, three=3}
}

总结

到这里就结束了,这篇文章主要介绍了 Window 的类型大概可以分为三类: 应用程序 Window(Application Window)、子 Window(Sub Windwow)、系统 Window(System Window)。 分别介绍了 Window 的类型、Window 视图层级顺序、Window 的 flag、和 window 软键盘模式为后面的内容做铺垫

Window 都有那些常用的参数?

参数 备注
x window 左上角的 x 坐标
y window 左上角的 y 坐标
type Window 的类型
flag Window 的 flag 用于控制 Window 的显示
softInputMode window 软键盘输入区域的显示模式
alpha Window 的透明度,取值为0-1
gravity Window 在屏幕中的位置
alpha Window 的透明度,取值为0-1
format Window 的像素点格式,值定义在 PixelFormat 中

Window 都有那些类型?

应用程序 Window(Application Window)、子 Window(Sub Windwow)、系统 Window(System Window),子 Window 依附于父 Window 上,并且他们的坐标空间相对于父 Window 的,每个大类型又包含多个小类型,每个类型在上文的表格中已经列出来了,

Window 那些过时的 API 以及处理方案?

  1. TYPE_PHONE、TYPE_SYSTEM_ALERT、TYPE_TOAST、TYPE_SYSTEM_OVERLAY、TYPE_PRIORITY_PHONE、TYPE_SYSTEM_ERROR 这些 type 在 API 26 中均已经过时,使用 TYPE_APPLICATION_OVERLAY 代替,需要申请 Manifest.permission.SYSTEM_ALERT_WINDOW 权限
  2. TYPE_KEYGUARD 已经被从系统中移除,可以使用 TYPE_KEYGUARD_DIALOG 来代替

Window 视图层级顺序是如何确定的?

Window 的参数 x、y,分别表示 Window 左上角的 x 坐标,Window 左上角的 y 坐标,Window 视图层级顺序 用 Z-order 来表示,Z-order 对应着 WindowManager.LayoutParams 的 type 值,Z-order 可以理解为 Android 视图的层级概念,值越大越靠前,就越靠近用户。而 mBaseLayer 和 mSubLayer 决定 z-order 的两个因素

Window 都那些 flag?

Window 的 flag 用于控制 Window 的显示,flag 的参数如下所示:

flag 备注
FLAG_ALLOW_LOCK_WHILE_SCREEN_ON 当 Window 可见时允许锁屏
FLAG_DIM_BEHIND Window 后面的内容都变暗
FLAG_BLUR_BEHIND API 已经过时,Window 后面的内容都变模糊
FLAG_NOT_FOCUSABLE Window 不能获得输入焦点,即不接受任何按键或按钮事件,例如该 Window 上 有 EditView,点击 EditView 是 不会弹出软键盘的,Window 范围外的事件依旧为原窗口处理;例如点击该窗口外的view,依然会有响应。另外只要设置了此Flag,都将会启用FLAG_NOT_TOUCH_MODAL
FLAG_NOT_TOUCH_MODAL 设置了该 Flag,将 Window 之外的按键事件发送给后面的 Window 处理, 而自己只会处理 Window 区域内的触摸事件,Window 之外的 view 也是可以响应 touch 事件
FLAG_NOT_TOUCHABLE 设置了该Flag,表示该 Window 将不会接受任何 touch 事件,例如点击该 Window 不会有响应,只会传给下面有聚焦的窗口
FLAG_KEEP_SCREEN_ON 只要 Window 可见时屏幕就会一直亮着
FLAG_LAYOUT_IN_SCREEN 允许 Window 占满整个屏幕
FLAG_LAYOUT_NO_LIMITS 允许 Window 超过屏幕之外
FLAG_FULLSCREEN 全屏显示,隐藏所有的 Window 装饰,比如在游戏、播放器中的全屏显示
FLAG_FORCE_NOT_FULLSCREEN 表示比FLAG_FULLSCREEN低一级,会显示状态栏
FLAG_IGNORE_CHEEK_PRESSES 当用户的脸贴近屏幕时(比如打电话),不会去响应此事件
FLAG_WATCH_OUTSIDE_TOUCH 则当按键动作发生在 Window 之外时,将接收到一个MotionEvent.ACTION_OUTSIDE事件
FLAG_SHOW_WHEN_LOCKED 已经过时,窗口可以在锁屏的 Window 之上显示, 使用 Activity#setShowWhenLocked(boolean) 方法代替
FLAG_DRAWS_SYSTEM_BAR_BACKGROUNDS 表示负责绘制系统栏背景。如果设置,系统栏将以透明背景绘制, 此 Window 中的相应区域将填充 Window#getStatusBarColor()和 Window#getNavigationBarColor()中指定的颜色
FLAG_SHOW_WALLPAPER 表示要求系统壁纸显示在该 Window 后面,Window 表面必须是半透明的,才能真正看到它背后的壁纸

Window 软键盘模式?

Window 的软键盘模式表示 Window 软键盘输入区域的显示模式

model 备注
SOFT_INPUT_STATE_UNCHANGED 不会改变软键盘的状态
SOFT_INPUT_STATE_VISIBLE 当用户进入窗口时,显示软键盘
SOFT_INPUT_STATE_HIDDEN 当用户进入该窗口时,隐藏软键盘
SOFT_INPUT_STATE_ALWAYS_HIDDEN 当窗口获取焦点时,隐藏软键盘
SOFT_INPUT_STATE_ALWAYS_VISIBLE 当窗口获取焦点时,显示软键盘
SOFT_INPUT_MASK_ADJUST window 会调整大小以适应软键盘窗口
SOFT_INPUT_ADJUST_UNSPECIFIED 没有指定状态,系统会选择一个合适的状态或依赖于主题的设置
SOFT_INPUT_ADJUST_RESIZE 1. 当软键盘弹出时,窗口会调整大小,例如点击一个EditView,整个layout都将平移可见且处于软件盘的上方 2. 同样的该模式不能与SOFT_INPUT_ADJUST_PAN结合使用 3. 如果窗口的布局参数标志包含FLAG_FULLSCREEN,则将忽略这个值,窗口不会调整大小,但会保持全屏
SOFT_INPUT_ADJUST_PAN 1. 当软键盘弹出时,窗口不需要调整大小, 要确保输入焦点是可见的 2. 例如有两个EditView的输入框,一个为Ev1,一个为Ev2,当你点击Ev1想要输入数据时,当前的Ev1的输入框会移到软键盘上方 3. 该模式不能与SOFT_INPUT_ADJUST_RESIZE结合使用
SOFT_INPUT_ADJUST_NOTHING 将不会调整大小,直接覆盖在window上

参考文献

  • developer.android.google.cn/…/WindowM…
  • www.jianshu.com/p/352825547…

结语

致力于分享一系列 Android 系统源码、逆向分析、算法、翻译、Jetpack 源码相关的文章,如果你同我一样喜欢研究 Android 源码,可以关注我,如果你喜欢这篇文章欢迎 star,一起来学习,期待与你一起成长

文章列表

算法

由于 LeetCode 的题库庞大,每个分类都能筛选出数百道题,由于每个人的精力有限,不可能刷完所有题目,因此我按照经典类型题目去分类、和题目的难易程度去排序

  • 数据结构: 数组、栈、队列、字符串、链表、树……
  • 算法: 查找算法、搜索算法、位运算、排序、数学、……

每道题目都会用 Java 和 kotlin 去实现,并且每道题目都有解题思路,如果你同我一样喜欢算法、LeetCode,可以关注我 GitHub 上的 LeetCode 题解:Leetcode-Solutions-with-Java-And-Kotlin,一起来学习,期待与你一起成长

Android 10 源码系列

正在写一系列的 Android 10 源码分析的文章,了解系统源码,不仅有助于分析问题,在面试过程中,对我们也是非常有帮助的,如果你同我一样喜欢研究 Android 源码,可以关注我 GitHub 上的 Android10-Source-Analysis,文章都会同步到这个仓库

  • 0xA01 Android 10 源码分析:APK 是如何生成的
  • 0xA02 Android 10 源码分析:APK 的安装流程
  • 0xA03 Android 10 源码分析:APK 加载流程之资源加载
  • 0xA04 Android 10 源码分析:APK 加载流程之资源加载(二)
  • 0xA05 Android 10 源码分析:Dialog 加载绘制流程以及在 Kotlin、DataBinding 中的使用
  • 0xA06 Android 10 源码分析:WindowManager 视图绑定以及体系结构
  • 更多

Android 应用系列

  • 如何高效获取视频截图
  • 如何在项目中封装 Kotlin + Android Databinding
  • [译][Google工程师] 刚刚发布了 Fragment 的新特性 “Fragment 间传递数据的新方式” 以及源码分析
  • [译][2.4K Start] 放弃 Dagger 拥抱 Koin
  • [译][5k+] Kotlin 的性能优化那些事
  • [译][Google工程师] 详解 FragmentFactory 如何优雅使用 Koin 以及部分源码分析
  • [译] 解密 RxJava 的异常处理机制
  • 再见吧 buildSrc, 拥抱 Composing builds 提升 Android 编译速度

工具系列

  • 为数不多的人知道的 AndroidStudio 快捷键(一)
  • 为数不多的人知道的 AndroidStudio 快捷键(二)
  • 关于 adb 命令你所需要知道的
  • 10分钟入门 Shell 脚本编程

逆向系列

  • 基于 Smali 文件 Android Studio 动态调试 APP
  • 解决在 Android Studio 3.2 找不到 Android Device Monitor 工具

本文转载自: 掘金

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

过滤器 和 拦截器 的6个区别,别再傻傻分不清了

发表于 2020-06-03

本文收录在个人博客:www.chengxy-nds.top,共享技术资源,共同进步

周末有个小伙伴加我微信,向我请教了一个问题:老哥,「过滤器 (Filter) 和 拦截器 (Interceptor) 有啥区别啊?」 听到题目我的第一感觉就是:「简单」!

毕竟这两种工具开发中用到的频率都相当高,应用起来也是比较简单的,可当我准备回复他的时候,竟然不知道从哪说起,支支吾吾了半天,场面炒鸡尴尬有木有,工作这么久一个基础问题答成这样,丢了大人了。
自导自演,别太当真过,哈哈哈
平时觉得简单的知识点,但通常都不会太关注细节,一旦被别人问起来,反倒说不出个所以然来。

归根结底,还是对这些知识了解的不够,一直停留在会用的阶段,以至于现在「一看就会一说就废」!这是典型基础不扎实的表现,哎·~,其实我也就是个虚胖!

知耻而后勇,下边结合实践,更直观的来感受一下两者到底有什么不同?

准备环境

我们在项目中同时配置 拦截器 和 过滤器。

1、过滤器 (Filter)

过滤器的配置比较简单,直接实现Filter 接口即可,也可以通过@WebFilter注解实现对特定URL拦截,看到Filter 接口中定义了三个方法。

  • init() :该方法在容器启动初始化过滤器时被调用,它在 Filter 的整个生命周期只会被调用一次。「注意」:这个方法必须执行成功,否则过滤器会不起作用。
  • doFilter() :容器中的每一次请求都会调用该方法, FilterChain 用来调用下一个过滤器 Filter。
  • destroy(): 当容器销毁 过滤器实例时调用该方法,一般在方法中销毁或关闭资源,在过滤器 Filter 的整个生命周期也只会被调用一次
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
复制代码@Component
public class MyFilter implements Filter {

@Override
public void init(FilterConfig filterConfig) throws ServletException {

System.out.println("Filter 前置");
}

@Override
public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {

System.out.println("Filter 处理中");
filterChain.doFilter(servletRequest, servletResponse);
}

@Override
public void destroy() {

System.out.println("Filter 后置");
}
}

2、拦截器 (Interceptor)

拦截器它是链式调用,一个应用中可以同时存在多个拦截器Interceptor, 一个请求也可以触发多个拦截器 ,而每个拦截器的调用会依据它的声明顺序依次执行。

首先编写一个简单的拦截器处理类,请求的拦截是通过HandlerInterceptor 来实现,看到HandlerInterceptor 接口中也定义了三个方法。

  • preHandle() :这个方法将在请求处理之前进行调用。「注意」:如果该方法的返回值为false ,将视为当前请求结束,不仅自身的拦截器会失效,还会导致其他的拦截器也不再执行。
  • postHandle():只有在 preHandle() 方法返回值为true 时才会执行。会在Controller 中的方法调用之后,DispatcherServlet 返回渲染视图之前被调用。 「有意思的是」:postHandle() 方法被调用的顺序跟 preHandle() 是相反的,先声明的拦截器 preHandle() 方法先执行,而postHandle()方法反而会后执行。
  • afterCompletion():只有在 preHandle() 方法返回值为true 时才会执行。在整个请求结束之后, DispatcherServlet 渲染了对应的视图之后执行。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
复制代码@Component
public class MyInterceptor implements HandlerInterceptor {

@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {

System.out.println("Interceptor 前置");
return true;
}

@Override
public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception {

System.out.println("Interceptor 处理中");
}

@Override
public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {

System.out.println("Interceptor 后置");
}
}

将自定义好的拦截器处理类进行注册,并通过addPathPatterns、excludePathPatterns等属性设置需要拦截或需要排除的 URL。

1
2
3
4
5
6
7
8
9
复制代码@Configuration
public class MyMvcConfig implements WebMvcConfigurer {

@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(new MyInterceptor()).addPathPatterns("/**");
registry.addInterceptor(new MyInterceptor1()).addPathPatterns("/**");
}
}

我们不一样

过滤器 和 拦截器 均体现了AOP的编程思想,都可以实现诸如日志记录、登录鉴权等功能,但二者的不同点也是比较多的,接下来一一说明。

1、实现原理不同

过滤器和拦截器 底层实现方式大不相同,过滤器 是基于函数回调的,拦截器 则是基于Java的反射机制(动态代理)实现的。

这里重点说下过滤器!

在我们自定义的过滤器中都会实现一个 doFilter()方法,这个方法有一个FilterChain 参数,而实际上它是一个回调接口。ApplicationFilterChain是它的实现类, 这个实现类内部也有一个 doFilter() 方法就是回调方法。

1
2
3
复制代码public interface FilterChain {
void doFilter(ServletRequest var1, ServletResponse var2) throws IOException, ServletException;
}

在这里插入图片描述
ApplicationFilterChain里面能拿到我们自定义的xxxFilter类,在其内部回调方法doFilter()里调用各个自定义xxxFilter过滤器,并执行 doFilter() 方法。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
复制代码public final class ApplicationFilterChain implements FilterChain {
@Override
public void doFilter(ServletRequest request, ServletResponse response) {
...//省略
internalDoFilter(request,response);
}

private void internalDoFilter(ServletRequest request, ServletResponse response){
if (pos < n) {
//获取第pos个filter
ApplicationFilterConfig filterConfig = filters[pos++];
Filter filter = filterConfig.getFilter();
...
filter.doFilter(request, response, this);
}
}

}

而每个xxxFilter 会先执行自身的 doFilter() 过滤逻辑,最后在执行结束前会执行filterChain.doFilter(servletRequest, servletResponse),也就是回调ApplicationFilterChain的doFilter() 方法,以此循环执行实现函数回调。

1
2
3
4
5
复制代码    @Override
public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {

filterChain.doFilter(servletRequest, servletResponse);
}

2、使用范围不同

我们看到过滤器 实现的是 javax.servlet.Filter 接口,而这个接口是在Servlet规范中定义的,也就是说过滤器Filter 的使用要依赖于Tomcat等容器,导致它只能在web程序中使用。
在这里插入图片描述
而拦截器(Interceptor) 它是一个Spring组件,并由Spring容器管理,并不依赖Tomcat等容器,是可以单独使用的。不仅能应用在web程序中,也可以用于Application、Swing等程序中。
在这里插入图片描述

3、触发时机不同

过滤器 和 拦截器的触发时机也不同,我们看下边这张图。
在这里插入图片描述

过滤器Filter是在请求进入容器后,但在进入servlet之前进行预处理,请求结束是在servlet处理完以后。

拦截器 Interceptor 是在请求进入servlet后,在进入Controller之前进行预处理的,Controller 中渲染了对应的视图之后请求结束。

4、拦截的请求范围不同

在上边我们已经同时配置了过滤器和拦截器,再建一个Controller接收请求测试一下。

1
2
3
4
5
6
7
8
9
10
11
复制代码@Controller
@RequestMapping()
public class Test {

@RequestMapping("/test1")
@ResponseBody
public String test1(String a) {
System.out.println("我是controller");
return null;
}
}

项目启动过程中发现,过滤器的init()方法,随着容器的启动进行了初始化。
在这里插入图片描述
此时浏览器发送请求,F12 看到居然有两个请求,一个是我们自定义的 Controller 请求,另一个是访问静态图标资源的请求。
在这里插入图片描述
看到控制台的打印日志如下:

执行顺序 :Filter 处理中 -> Interceptor 前置 -> 我是controller -> Interceptor 处理中 -> Interceptor 处理后

1
2
3
4
5
复制代码Filter 处理中
Interceptor 前置
Interceptor 处理中
Interceptor 后置
Filter 处理中

过滤器Filter执行了两次,拦截器Interceptor只执行了一次。这是因为过滤器几乎可以对所有进入容器的请求起作用,而拦截器只会对Controller中请求或访问static目录下的资源请求起作用。

5、注入Bean情况不同

在实际的业务场景中,应用到过滤器或拦截器,为处理业务逻辑难免会引入一些service服务。

下边我们分别在过滤器和拦截器中都注入service,看看有什么不同?

1
2
3
4
5
6
7
8
复制代码@Component
public class TestServiceImpl implements TestService {

@Override
public void a() {
System.out.println("我是方法A");
}
}

过滤器中注入service,发起请求测试一下 ,日志正常打印出“我是方法A”。

1
2
3
4
5
6
7
8
9
10
复制代码@Autowired
private TestService testService;

@Override
public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {

System.out.println("Filter 处理中");
testService.a();
filterChain.doFilter(servletRequest, servletResponse);
}
1
2
3
4
5
6
复制代码Filter 处理中
我是方法A
Interceptor 前置
我是controller
Interceptor 处理中
Interceptor 后置

在拦截器中注入service,发起请求测试一下 ,竟然TM的报错了,debug跟一下发现注入的service怎么是Null啊?
在这里插入图片描述
这是因为加载顺序导致的问题,拦截器加载的时间点在springcontext之前,而Bean又是由spring进行管理。

❝
拦截器:老子今天要进洞房;
Spring:兄弟别闹,你媳妇我还没生出来呢!

❞

解决方案也很简单,我们在注册拦截器之前,先将Interceptor 手动进行注入。「注意」:在registry.addInterceptor()注册的是getMyInterceptor() 实例。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
复制代码@Configuration
public class MyMvcConfig implements WebMvcConfigurer {

@Bean
public MyInterceptor getMyInterceptor(){
System.out.println("注入了MyInterceptor");
return new MyInterceptor();
}

@Override
public void addInterceptors(InterceptorRegistry registry) {

registry.addInterceptor(getMyInterceptor()).addPathPatterns("/**");
}
}

6、控制执行顺序不同

实际开发过程中,会出现多个过滤器或拦截器同时存在的情况,不过,有时我们希望某个过滤器或拦截器能优先执行,就涉及到它们的执行顺序。

过滤器用@Order注解控制执行顺序,通过@Order控制过滤器的级别,值越小级别越高越先执行。

1
2
3
复制代码@Order(Ordered.HIGHEST_PRECEDENCE)
@Component
public class MyFilter2 implements Filter {

拦截器默认的执行顺序,就是它的注册顺序,也可以通过Order手动设置控制,值越小越先执行。

1
2
3
4
5
6
复制代码 @Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(new MyInterceptor2()).addPathPatterns("/**").order(2);
registry.addInterceptor(new MyInterceptor1()).addPathPatterns("/**").order(1);
registry.addInterceptor(new MyInterceptor()).addPathPatterns("/**").order(3);
}

看到输出结果发现,先声明的拦截器 preHandle() 方法先执行,而postHandle()方法反而会后执行。

postHandle() 方法被调用的顺序跟 preHandle() 居然是相反的!如果实际开发中严格要求执行顺序,那就需要特别注意这一点。

1
2
3
4
5
6
7
8
9
10
复制代码Interceptor1 前置
Interceptor2 前置
Interceptor 前置
我是controller
Interceptor 处理中
Interceptor2 处理中
Interceptor1 处理中
Interceptor 后置
Interceptor2 处理后
Interceptor1 处理后

「那为什么会这样呢?」 得到答案就只能看源码了,我们要知道controller 中所有的请求都要经过核心组件DispatcherServlet路由,都会执行它的 doDispatch() 方法,而拦截器postHandle()、preHandle()方法便是在其中调用的。

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
复制代码protected void doDispatch(HttpServletRequest request, HttpServletResponse response) throws Exception {

try {
...........
try {

// 获取可以执行当前Handler的适配器
HandlerAdapter ha = getHandlerAdapter(mappedHandler.getHandler());

// Process last-modified header, if supported by the handler.
String method = request.getMethod();
boolean isGet = "GET".equals(method);
if (isGet || "HEAD".equals(method)) {
long lastModified = ha.getLastModified(request, mappedHandler.getHandler());
if (logger.isDebugEnabled()) {
logger.debug("Last-Modified value for [" + getRequestUri(request) + "] is: " + lastModified);
}
if (new ServletWebRequest(request, response).checkNotModified(lastModified) && isGet) {
return;
}
}
// 注意: 执行Interceptor中PreHandle()方法
if (!mappedHandler.applyPreHandle(processedRequest, response)) {
return;
}

// 注意:执行Handle【包括我们的业务逻辑,当抛出异常时会被Try、catch到】
mv = ha.handle(processedRequest, response, mappedHandler.getHandler());

if (asyncManager.isConcurrentHandlingStarted()) {
return;
}
applyDefaultViewName(processedRequest, mv);

// 注意:执行Interceptor中PostHandle 方法【抛出异常时无法执行】
mappedHandler.applyPostHandle(processedRequest, response, mv);
}
}
...........
}

看看两个方法applyPreHandle()、applyPostHandle()具体是如何被调用的,就明白为什么postHandle()、preHandle() 执行顺序是相反的了。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
复制代码boolean applyPreHandle(HttpServletRequest request, HttpServletResponse response) throws Exception {
HandlerInterceptor[] interceptors = this.getInterceptors();
if(!ObjectUtils.isEmpty(interceptors)) {
for(int i = 0; i < interceptors.length; this.interceptorIndex = i++) {
HandlerInterceptor interceptor = interceptors[i];
if(!interceptor.preHandle(request, response, this.handler)) {
this.triggerAfterCompletion(request, response, (Exception)null);
return false;
}
}
}

return true;
}
1
2
3
4
5
6
7
8
9
复制代码void applyPostHandle(HttpServletRequest request, HttpServletResponse response, @Nullable ModelAndView mv) throws Exception {
HandlerInterceptor[] interceptors = this.getInterceptors();
if(!ObjectUtils.isEmpty(interceptors)) {
for(int i = interceptors.length - 1; i >= 0; --i) {
HandlerInterceptor interceptor = interceptors[i];
interceptor.postHandle(request, response, this.handler, mv);
}
}
}

发现两个方法中在调用拦截器数组 HandlerInterceptor[] 时,循环的顺序竟然是相反的。。。,导致postHandle()、preHandle() 方法执行的顺序相反。
在这里插入图片描述

总结

我相信大部分人都能熟练使用滤器和拦截器,但两者的差别还是需要多了解下,不然开发中使用不当,时不时就会出现奇奇怪怪的问题,以上内容比较简单,新手学习老鸟复习,有遗漏的地方还望大家积极补充,如有理解错误之处,还望不吝赐教。


原创不易,「燃烧秀发输出内容」,如果你有一丢丢收获,点个 「赞」 鼓励一下哦!

整理了几百本各类技术电子书相送 ,送给小伙伴们。关注公号回复【「666」】自行领取。和一些小伙伴们建了一个技术交流群,一起探讨技术、分享技术资料,旨在共同学习进步,如果感兴趣就加入我们吧!

本文转载自: 掘金

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

(十)从零搭建后端框架——MockMvc单元测试

发表于 2020-06-03

前言

在功能开发完成后,虽然有专门的测试人员,但开发人员自身也要进行单元测试。

一般公司对BUG率和单元测试覆盖率都会有一定的要求,所以做好单元测试还是很有必要的。

后端提供的都是接口,本文使用MockMvc模拟接口进行测试。

具体实现

Maven依赖

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
复制代码<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
<exclusions>
<exclusion>
<groupId>org.junit.vintage</groupId>
<artifactId>junit-vintage-engine</artifactId>
</exclusion>
</exclusions>
</dependency>

<dependency>
<groupId>com.h2database</groupId>
<artifactId>h2</artifactId>
<scope>test</scope>
</dependency>
</dependencies>

为了防止测试对真实数据库数据产生影响,这里使用H2数据库,并进行参数配置。

参数配置

1
2
3
4
5
6
7
8
9
复制代码spring:
datasource:
driver-class-name: org.h2.Driver
url: jdbc:h2:mem:test;MODE=MYSQL;
schema: classpath:sqls/sys.sql
main:
banner-mode: off
mybatis:
mapper-locations: classpath*:sql-mappers/**/*.xml
  • spring.datasource.schema 指定建表语句文件
  • mybatis.mapper-locations 指定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
70
71
72
73
74
75
76
77
78
79
80
81
82
83
复制代码@SpringBootTest
@AutoConfigureMockMvc
public class UserControllerTest {

@Autowired
private MockMvc mvc;

private static Gson gson = new GsonBuilder().serializeNulls().create();

@Test
@SqlGroup({
@Sql(executionPhase = Sql.ExecutionPhase.BEFORE_TEST_METHOD, value = "classpath:h2/user/init-data.sql"),
@Sql(executionPhase = Sql.ExecutionPhase.AFTER_TEST_METHOD, value = "classpath:h2/user/clean-data.sql")
})
void getUser() throws Exception {
// 模拟GET请求
mvc.perform(get("/user/1")
// 请求参数类型
.contentType(MediaType.APPLICATION_JSON_UTF8)
// 接收参数类型
.accept(MediaType.APPLICATION_JSON_UTF8))
// 结果验证
.andExpect(status().isOk())
.andExpect(jsonPath("code").value(200))
.andExpect(jsonPath("data").exists())
.andExpect(jsonPath("$['data']['account']").value("zhuqc1"))
.andExpect(jsonPath("$['data']['password']").value("password"))
.andExpect(jsonPath("$['data']['nickname']").value("zhuqc1"))
.andExpect(jsonPath("$['data']['email']").value("zqc@zqc.com"))
.andExpect(jsonPath("$['data']['phone']").value("13345678901"))
// 结果处理器
.andDo(MockMvcResultHandlers.print());
}

@Test
@SqlGroup({
@Sql(executionPhase = Sql.ExecutionPhase.BEFORE_TEST_METHOD, value = "classpath:h2/user/init-data.sql"),
@Sql(executionPhase = Sql.ExecutionPhase.AFTER_TEST_METHOD, value = "classpath:h2/user/clean-data.sql")
})
void getUser2() throws Exception {
// 模拟GET请求,并返回结果
MvcResult result = mvc.perform(get("/user/1")
.accept(MediaType.APPLICATION_JSON_UTF8))
.andReturn();
MockHttpServletResponse response = result.getResponse();
JsonObject apiResult = JsonParser.parseString(response.getContentAsString()).getAsJsonObject();
JsonObject data = apiResult.getAsJsonObject("data").getAsJsonObject();

// 断言验证数据
Assertions.assertEquals(response.getStatus(), HttpStatus.OK.value());
Assertions.assertEquals(data.get("account").getAsString(), "zhuqc1");
Assertions.assertEquals(data.get("password").getAsString(), "password");
Assertions.assertEquals(data.get("nickname").getAsString(), "zhuqc1");
Assertions.assertEquals(data.get("email").getAsString(), "zqc@zqc.com");
Assertions.assertEquals(data.get("phone").getAsString(), "13345678901");
}

@Test
@SqlGroup({
@Sql(executionPhase = Sql.ExecutionPhase.AFTER_TEST_METHOD, value = "classpath:h2/user/clean-data.sql")
})
void addUser() throws Exception {
User user = new User();
user.setAccount("zhuqc1");
user.setPassword("password");
user.setNickname("zhuqc1");
user.setEmail("zqc@zqc.com");
user.setPhone("13345678901");

// 模拟POST请求
mvc.perform(post("/user")
// 请求参数类型
.contentType(MediaType.APPLICATION_JSON_UTF8)
// 接收参数类型
.accept(MediaType.APPLICATION_JSON_UTF8)
// 请求参数
.content(gson.toJson(user)))
// 结果验证
.andExpect(status().isOk())
.andExpect(jsonPath("code").value(200))
.andExpect(jsonPath("data").value(1));
}
}
  • @SpringBootTest 指定测试类在SpringBoot环境下运行
  • @AutoConfigureMockMvc 用于自动配置MockMvc,配置后MockMvc类可以直接注入
  • @SqlGroup 指定测试方法执行前后的SQL语句

比如,可以在测试方法执行前,初始化数据;在测试方法执行后,清除数据。

MockMvc

MockMvc是接口测试的主入口,核心方法perform(RequestBuilder),
会自动执行SpringMVC的流程并映射到相应的控制器执行处理,方法返回值是ResultActions。

ResultActions

  • andExpect 添加ResultMatcher验证规则,验证执行结果是否正确。
  • andDo 添加ResultHandler结果处理器,比如打印结果到控制台。
  • andReturn 返回MvcResult执行结果,可以对结果进行自定义验证。

MockMvcResultMatchers

用于验证执行结果是否正确,详见测试方法getUser()。

MockMvcResultHandlers

结果处理器,表示要对结果做点什么事情。详见测试方法getUser()。

比如使用MockMvcResultHandlers.print()打印响应结果信息到控制台。如下:

响应结果

MvcResult

单元测试执行结果,可以对结果进行自定义验证,详见测试方法getUser2()。

源码

github.com/zhuqianchan…

往期回顾

  • 从零搭建后端框架 —— 持续更新

本文转载自: 掘金

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

Service Mesh 中的可观察性实践

发表于 2020-06-03

image.png

Service Mesh Virtual Meetup 是 ServiceMesher 社区和 CNCF 联合主办的线上系列直播。本期为 Service Mesh Virtual Meetup#1 ,邀请了四位来自不同公司的嘉宾,从不同角度展开了 Service Mesh 的应用实践分享,分享涵盖如何使用 SkyWalking 来观测 Service Mesh,来自陌陌和百度的 Service Mesh 生产实践,Service Mesh 的可观察性和生产实践以及与传统微服务监控的区别。

本文根据5月14日晚,G7 微服务架构师叶志远的主题分享《Service Mesh 高可用在企业级生产中的实践》整理。文末包含本次分享的视频回顾链接以及 PPT 下载地址。

前言

谈到 Service Mesh,人们总是想起微服务和服务治理,从 Dubbo 到 Spring Cloud (2016开始进入国内研发的视野,2017年繁荣)再到 Service Mesh (2018年开始被大家所熟悉),正所谓长江后浪推前浪,作为后浪,Service Mesh 别无选择,而 Spring Cloud 对 Service Mesh 满怀羡慕,微服务架构的出现与繁荣,是互联网时代架构形式的巨大突破。Service Mesh 具有一定的学习成本,实际上在国内的落地案例不多,大多是云商与头部企业,随着性能与生态的完善以及各大社区推动容器化场景的落地,Service Mesh 也开始在大小公司生根发芽,弥补容器层与 Kubernetes 在服务治理方面的短缺之处。本次将以一个选型调研者的视角,来看看 Service Mesh 中的可观察性主流实践方案。

可观察性的哲学

可观察性(Observability)不是一个新名词,它在很久之前就已经诞生了,但是它在 IT 领域却是一个新兴事物。可观察性在维基百科中原文是这样定义的:“In control theory, observability is a measure of how well internal states of a system can be inferred from knowledge of its external outputs. ”。云原生领域第一次出现这个词,是在云原生理念方兴未艾的2017年,在云原生的思潮之下,运用传统的描述方式已经不足以概括这个时代的监控诉求,而 Observability 就显得贴切许多。

幻灯片5.jpeg

回想一下传统的监控方式,除去运维层面的主机监控、JVM 监控、消息队列监控之外,有多少监控是事先就想好怎么做的?很少!其实很多时候,我们做的事情就是在故障发生之后,对故障复盘的过程中,除了 bug 重现与修复,也会去定制加一些监控,以期望下次发生同样的情况时有一个实时的告警。研发人员收到告警之后得以快速地处理问题,尽可能地减少损失。所以,传统的监控模式大多都是在做亡羊补牢的事情,缺少一个主动性。

在云原生时代的容器化体系当中就不一样了,容器和服务的生命周期是紧密联系在一起的,加上容器完美的隔离特性,再加上 Kubernetes 的容器管理层,应用服务跑在容器当中就显得更加地黑盒化,相较在传统物理主机或者虚拟机当中,排查问题的时候显得非常不便。所以在云原生时代强调的是可观察性,这样的监控永远都是兵马未动而粮草先行的,需要提前想好我们要如何观察容器内的服务以及服务之间的拓扑信息、各式指标的搜集等,这些监测能力相当重要。

幻灯片6.jpeg

关于可观察性在云原生领域是何时开始流行起来的,没有一个很明确的时间。业界认为可观察性最早是由 Cindy Sridharan 提出的,其实一位德国柏林的工程师 Peter Bourgon 早在2017年2月就已经有文章在讨论可观察性了,Peter 算是业界最早讨论可观察性的开发者,他写的著名的博文《Metrics, Tracing, and Logging》被翻译成了多种语言。真正可观察性成为一种标准,是来自 Pivotal 公司的 Matt Stine 定义的云原生标准,可观察性位列其中,由此可观察性就成为了云原生时代一个标准主题。

幻灯片7.jpeg

Peter Bourgon 提出的可观察性三大支柱围绕 Metrics、Tracing 和 Logging 展开,这三个维度几乎涵盖了应用程序的各种表征行为,开发人员通过收集并查看这三个维度的数据就可以做各种各样的事情,时刻掌握应用程序的运行情况,关于三大支柱的理解如下:

  • Metrics:Metrics 是一种聚合态的数据形式,日常中经常会接触到的 QPS、TP99、TP95 等等都属于Metrics 的范畴,它和统计学的关系最为密切,往往需要使用统计学的原理来做一些设计;
  • Tracing:Tracing 这个概念几乎是由 SOA 时代带来的复杂性补偿,服务化带来的长调用链,仅仅依靠日志是很难去定位问题的,因此它的表现形式比 Metrics 更复杂,好在业界涌现出来了多个协议以支撑 Tracing 维度的统一实现;
  • Logging:Logging 是由请求或者事件触发,应用程序当中用以记录状态快照信息的一种形式,简单说就是日志,但这个日志不仅仅是打印出来这么简单,它的统一收集、存储以及解析都是一个有挑战的事情,比如结构化(Structured)与非结构化(Unstructed)的日志处理,往往需要一个高性能的解析器与缓冲器;

此外,Peter Bourgon 在博文中还提到了三大支柱结合态的一些理想输出形式,以及对存储的依赖,Metrics、Tracing、Logging 由于聚合程度的不同,对存储依赖由低到高。更多细节,感兴趣的同学可以查看文末的原文链接。

幻灯片8.jpeg

Peter Bourgon 关于可观察性三大支柱的思考不止于此,他还在2018年的 GopherCon EU 的分享上面再次讨论了 Metrics、Tracing 和 Logging 在工业生产当中的深层次意义,这次他探讨了4个维度。

  • CapEx:表示指标初始收集成本,很明显日志的成本最低,埋点即可;其次是 Metrics,最难是 Tracing 数据,在有了协议支撑的情况下,依然要定义许多数据点,以完成链路追踪必须的元数据定义收集;
  • OpEx:表示运维成本,一般指存储成本,这个之前已经讨论过;
  • Reaction:表示异常情况的响应灵敏度,显然聚合之后的数据可以呈现出波动情况,因此 Metrics 是对异常情况最灵敏的;Logging 次之,也可以从 Logging 清洗之中发现异常量;而 Tracing 在响应灵敏度上面似乎沾不上边,最多还是用在排障定位的场景;
  • Investigation:标准故障定位能力,这个维度是 Tracing 的强项,可以直观看出链路当中的故障,精确定位;Logging 次之;Metrics 维度只能反馈波动,对定位故障帮助不大;

幻灯片9.jpeg

在 CNCF Landscape 当中,有一块区域专门用作展示云原生场景下的可观察性解决方案,里面又分为好几个维度,图中是截至2020年5月14日的最新版图,未来还会有更多优秀的解决方案涌现出来。CNCF 目前毕业的10个项目库当中,有3个是和可观察性有关的,可见 CNCF 对可观察性的重视程度。

幻灯片10.jpeg

谈到这里,很多同学也许对于可观察性相关的协议比较感兴趣。目前比较火的有好几个,OpenTracing、OpenCensus、OpenTelemetry、OpenMetrics 等等,目前比较火的是前三个。OpenMetrics 这个项目已经不维护了。

OpenTracing 可以说是目前使用最广的分布式链路追踪协议了,大名鼎鼎的 SkyWalking 就是基于它实现的,它定义了与厂商无关,与语言无关的链路追踪协议 API,使得构建跨平台的链路追踪成为一件比较轻松的事情,目前它在 CNCF 的孵化器当中茁壮成长。

OpenCensus 是谷歌提出的一个针对 Tracing 和 Metrics 场景的协议,背靠 Dapper 的加持与历史背景,就连微软也十分拥护,目前在商用领域十分流行。

其他的协议比如 W3C Trace Context,呼声也很高,它甚至对数据在头部进行了压缩,与实现层无关。也许 CNCF 意识到各种协议又在层出不穷,以后各成气候,群雄逐鹿,每一个中间件都要做许多兼容,这对整个技术生态本身不利,因此 OpenTelemetry 横空出世。从字面意思就知道,CNCF 会将可观察性的“遥测”进行到底,它融合了 OpenTracing 和 OpenCensus 的协议内容,旨在提高云原生时代可观察性指标的统一收集与处理,目前 OpenTelemetry 已经进入 beta 版本,其中令人欣喜的是,Java 版本的 SDK 已经有一个类似 SkyWalking 的基于 byte-buddy 框架的无侵入式探针。目前已经可以从47种 Java 库当中自动探测获取遥测数据,另外推出了可供使用的 Erlang、Go、Java、JavaScript、Python 的 API 和 SDK。此外,数据收集器 OpenTelemetry Collector 也可以使用了,可以用它接收 OpenTelemetry client 发射过来的数据,统一收集处理。目前 CNCF 对 Logging 相关的协议制定暂缓,但是有一个工作小组也在做这方面规范的事情。

幻灯片11.jpeg

Service Mesh 与可观察性

要说 Service Mesh 与可观察性的关系,那就是可观察性是 Service Mesh 的功能子集。Service Mesh 是当今最为火爆的技术理念之一,它致力于为云原生时代运行在容器当中的大规模服务提供统一的服务发现、边缘路由、安全、流量控制、可观察性等能力,它是对 Kubernetes 服务治理能力的补充强化。可以说,Service Mesh 是云原生容器化时代的必然产物,它将对云上服务架构产生深远的影响。Service Mesh 的架构理念是将容器服务运行单元当成一个个网格,在每组运行单元中劫持流量,然后由一个统一的控制面板做统一处理,所有网格与控制面板维持一定的联系,这样,控制面板就得以作为可观察性解决方案与容器环境之间的桥梁。

幻灯片13.jpeg

市面上最为常见的 Service Mesh 技术有 Linkerd、Istio、Conduit 等,但是要在生产环境落地必须要经受住严苛的性能、合理的架构、社区活跃度的评估。

Linkerd 是由 Buoyant 开发维护,算是 Service Mesh 领域第一代的产品,Linkerd1.x 基于 Scala 编写,可基于主机运行,大家都知道 Scala 运行环境依赖 JDK,因此对资源的消耗相对较大。随后官方进行了整改,推出了新一代的数据平面组件 Conduit,基于 Rust 和 Go 编写,与 Linkerd 双剑合璧,成为 Linkerd2.x。总体来说,Linkerd2.x 性能有了较大的提升,也有可视化界面供操作,但是在国内就是不愠不火的,社区始终发展不起来。

转头看2017年出现的 Istio,也算是含着金汤匙出生的,由谷歌、IBM、Lyft 发起,虽然晚了 Linkerd 一年,但是一经推出,就受到广泛的关注与追捧。Istio 基于 Golang 编写,完美契合 Kubernetes 环境,数据平面整合 Envoy,在服务治理方面职责分明,国内落地案例相较 Linkerd 更加广泛。

幻灯片14.jpeg

Istio 目前总体还算是一个年轻的开源中间件,大版本之间的组件架构有比较大的区别,比如 1.0 引入了Galley(如图左),1.5 去掉了 Mixer,并且控制平面整合成单体,增加了 WASM 扩展机制(如图右)。总体的架构形式没有太大变化,数据面还是关注流量的劫持与转发策略的执行,控制面依然做遥测收集、策略下发、安全的工作。目前国内业界对于 Istio 的使用上,云商与头部公司处于领先位置,比如蚂蚁金服自研了自己基于 Golang 的数据平面 MOSN,兼容 Istio,做了许多优化工作,对 Istio 在国内落地做出了表率,更多的信息可以深入了解,看如何打造更适合国内互联网的 Service Mesh 架构。

幻灯片15.jpeg

虽然在 1.5 版本当中 Mixer 已经基本被废弃掉,进入维护阶段直到 1.7 版本,默认情况下 Mixer 完全关闭,但是目前多数落地方案还是基于 1.0-1.4 这个版本区间,所以在没有整体升级的情况下,以及 WASM 性能不明朗的似乎还,始终还是离不开 Mixer 的。前面说到 Service Mesh 是云原生容器环境与可观察性之间的桥梁,Mixer 的 Adapter 可以算得上是这个桥梁的钢架主体了,并且具有良好的可扩展性。Mixer Adapter 除了为流量做 Check 之外,更重要的是在预检阶段和报告阶段收集遥测数据,遥测数据通过 Adapter 暴露或发射数据到各种观察端,观察端基于数据绘制丰富的流量轨迹与事件快照。常用的用于可观察性的 Adapter 有对各种商用方案的适配,比如 Datadog、New Relic 等,开源方案 Apache SKyWalking、Zipkin、Fluentd、Prometheus 等,相关内容会在下文展开。

幻灯片16.jpeg

数据平面比如 Envoy 会向 Mixer 上报日志信息(Log)、链路数据(Trace),监控指标(Metric)等数据,Envoy 上报的原始数据都是一些属性信息(Attributes),属性信息是名称和类型的元数据,用来描述入口和出口流量和流量产生时的环境信息,然后 Mixer 会依照 LogEntry、Metric 或者 TraceSpan 模板配置的格式对属性进行格式化,最后再交给 Mixer Adapter 做进一步处理,当然对于数据量庞大的 Log 信息和 Trace 信息可以选择直接上报处理端,Envoy 也原生支持一些特定组件。不同的 Adapter 需要不同的 Attributes,模板定义了 Attributes 到 Adapter 输入数据映射的 schema,一个 Adapter 可以支持多个模板。Mixer 当中又可以抽象出三种配置模型:

  • Handler:表示一个配置好的 Adapter 实例;
  • Instance:定义 Attributes 信息的映射规则;
  • Rule:为 Handler 分配 Instance 以及触发规则;

下图是 Metric 模板与 LogEntry 模板,在映射关系之上还可以设定默认值,更多的设定可以查看官方文档。

幻灯片17.jpeg

下图是 TraceSpan 模板,熟悉 OpenTracing 的同学可能对映射内容比较熟悉,很多信息都是 OpenTracing 协议的标准值,比如 Span 的各种描述信息以及 http.method、http.status_code 等等,感兴趣的同学也可以去看看 OpenTracing 的标准定义。
另外在Service Mesh中对于链路追踪普遍有一个问题,就是无论你在数据平面如何做流量劫持,如何透传信息,以及如何生成或者继承Span,入口流量和出口流量都有一个无法串联的问题,这个问题要解决还是需要服务主容器来埋点透传,将链路信息透传到下一次请求当中去,这个问题是无法避免的,而OpenTelemetry的后续推行,可以解决这方面的标准化问题。

幻灯片18.jpeg

Istio 可观察性实践

在 Istio Mixer Adapter 当中我们可以获知,Istio 支持 Apache SKyWalking、Zipkin、Jaeger 的链路追踪,这三个中间件都支持 OpenTracing 协议,所以使用 TraceSpan 模板同时接入也没有什么问题。三者稍有不同的地方是:

  • Zipkin 算是老牌的链路追踪中间件了,项目发起时间是2012年,新版的功能也比较好用;
  • Jaeger 是2016年发起的新兴项目,使用 Go 编写,但是由于云原生的加持,致力于解决云原生时代的链路追踪问题,所以发展很快,它在 Istio 中集成极为简易,也是 Istio 官方推荐的方案
  • SkyWalking 是2015年开始开源,目前正在蓬勃发展的一个项目,但是稍有不同的是,目前它与 Istio 的结合是通过进程外适配的方式,接入损耗稍微大一些,在最新的8.0版本(还未发布)当中有相应的解决方案;

幻灯片20.jpeg

说到这里要提一下 SkyWalking,它是由国人吴晟自主研发并开源的 APM 中间件了,可以说是国人的骄傲吧。本次的分享第二场由高洪涛老师,SkyWalking 的核心贡献者之一,也对 SkyWalking 做了题为《Apache SkyWalking 在
Service Mesh 中的可观察性应用》的分享,感兴趣的同学可以关注一下。

Skywalking 提供了 Java、.NET、NodeJS、PHP、Python 的无侵入式插桩 SDK,另外提供 Golang 和 Lua 的有侵入式 SDK。

为什么 Golang 不能做成无侵入式的?这还得从语言特性说起,通常编程语言分为编译型语言、解释型语言、中间型语言,像 Java 这种语言,在编译的时候是编译成字节码,然后运行时再通过 JVM 去运行字节码,这样就可以在这其中做很多的事情,可以在编译的过程中把原本的代码改掉。而像 Python、PHP、JS 和 Erlang,是使用的时候才会进行逐行翻译,所以也可以在用的时候去加入一些额外的代码。Golang、C、C++ 则是编译型语言,在编译与链接的时候已经将源码转换成了机器码,所以在运行的时候是很难去改动的,这也就是 Golang 为什么不能做自动探针的原因。另外 SkyWalking 是由国人发起的,所以用户群体基数非常大,迭代也非常地快,7.0版本以前支持基于 Mixer 的遥测与显示,8.0之后又加入了从 Prometheus 或 Spring Sleuth 当中收集数据,另外8.0之后支持 Envoy ALS(access log service),不过需要开启 ALS 接收器。

在 SkyWalking 的使用上,基本是使用 ES 来做存储,但是有一些改动,将 service、endpoint、instance 这些信息放到关系数据库,各个插桩 SDK 也加入到基础镜像,也可以基于 SkyWalking 轻松实现服务接口粒度的调用次数统计。

幻灯片21.jpeg

另外一个在云原生链路追踪领域收到广泛使用的是中间件是 Jaeger,它是由 Uber 开源,被 CNCF 接纳,目前已经是一个毕业项目。它原生支持 OpenTracing 协议,与市面上的其他中间件也具有互通性,支持多种后端存储以及具备灵活的扩展性。在 Envoy 中原生支持 Jaeger,当请求到达 Envoy 的时候,Envoy 会选择创建 Span 或继承 Span,以保证链路的连贯性,它支持 Zipkin 的 B3 系列 Header 透传,以及 Jaeger 与 LightStep 的 Header。下图是 Jaeger 当中对链路的展示,可以通过 TraceId 准确定位某一次请求。

幻灯片22.jpeg

传统的日志解决方案,ELK 可以说是家喻户晓的,从 Spring Cloud 盛行开始,它便是日志解决方案的优良选择。随着技术的发展,近几年又出现了 EFK 这种解决方案,存储组件 ElasticSearch 和界面 Kibana 倒是没有什么特别大的变化,但是在严苛的线上环境与容器环境中,以及各种对资源敏感的场景下,对于日志采集组件的要求也越来越高,目前比较流行的方案是使用 Fluentd 或者 Filebeat 替代 Logstash,下面是它们三者的一些介绍:

  • Logstash:Java 编写,资源消耗大,现在一般不主张用作日志采集;
  • Fluentd:主体由 C 编写,插件由 Ruby 编写,2019年4月从 CNCF 毕业,资源消耗非常小,通常占用内存在30MB左右,可以将日志发射到多个缓冲器,也就是多个接收端,目前在容器内比较常用的组件;
  • Filebeat:Go 编写,但是线上出现过拉高底层资源 load average 的问题以及资源消耗较大,是 Fluentd 的10倍左右,在 Fluentd 出现之前,其被广泛运用在虚拟机当中;

对于 Istio 中的日志解决方案,尽管 Mixer 当中有提供 Fluentd Adapter,但是日志的量级大家也知道,这种方式并不好,所以从 Envoy 去拿到原始的属性日志再进行加工发射到存储端对应用是比较友好的,可以节省出很大一部分资源。

在日志维度中,如果要定位问题,最好与请求绑定起来,而绑定请求与日志,需要一个特定的标识,可以是 TransactionId 或者是 TraceId,所以链路追踪与日志融合是一个势在必行的行业诉求,因此在选择链路追踪中间件的时候,一定要考虑到如何更好地获取 TraceId 并与日志结合起来。

幻灯片23.jpeg

那么 Fluentd 就是最好的日志收集和发射解决方案了吗?

不是。Fluentd 的研发团队又推出了更加轻量级的 Fluent Bit,它是使用纯C编写的,占用资源更加少,从Fluentd 的 MB 级别直接降为 KB 级别,更加适合作为日志收集器。而 Fluentd 插件种类非常繁多,目前共有接近上千种的插件了,所以它更适合作为日志的聚合处理器,在日志收集之后的加工与传输中使用。在实际应用中,使用 Fluent Bit 可能会遇到一些问题,使用比较早期的版本可能会有配置动态加载的问题,解决方法就是另起一个进程控制 Fluent Bit 的启停,同时监听配置的变化,有变化则 reload。

幻灯片24.jpeg

关于上图中的 Loki,它的核心思想正如项目介绍,“Like Prometheus, but for logs”,类似于 prometheus 的聚合日志解决方案,2018年12月开源,短短两年,却已有接近1万个 Star 了!它由 Grafana 团队开发,由此可以看出 Grafana 对于一统云原生可观察性的目的。

在云原生时代,像以前那样用昂贵的全文索引,如 ES,或者列式存储,如 HBase,将大量的原始日志直接存储到昂贵的存储介质之中的做法,似乎已经不太妥当。因为原始日志99%是不会被查询到的,所以日志也是需要做一些归并,归并之后压缩成 gzip,并且打上各式标签,这样可能会更加符合云原生时代精细化运作的原则。

而 Loki 可以将大量的日志存储于廉价的对象存储中,并且它为日志打标归并成日志流的这种方式得以让我们快速地检索到对应的日志条目。但是注意一点,想要使用 Loki 替代 EFK 是不明智的,它们针对的场景不一样,对数据的完整性保证和检索能力也有差别。

幻灯片25.jpeg

自从 Prometheus 出现以来,就牢牢占据着监控指标的主要地位。Prometheus 应该是当前应用最广的开源系统监控和报警平台了,随着以 Kubernetes 为核心的容器技术的发展,Prometheus 强大的多维度数据模型、高效的数据采集能力、灵活的查询语法,以及可扩展、方便集成的特点,尤其是和云原生生态的结合,使其获得了越来越广泛的应用。

Prometheus 于2015年正式发布,于2016年加入 CNCF,并于2018年成为第2个从 CNCF 毕业的项目(第一个是 Kubernetes,其影响力可见一斑)。目前 Envoy 支持 TCP 和 UDP statsd 协议,首先让 Envoy 推送指标到 statsd,然后可以使用 Prometheus 从 statsd 拉取指标,以供 Grafana 可视化展示。另外我们也可以提供 Mixer Adapter,接收处理遥测数据供 Prometheus 采集。

在 Prometheus 的实际使用当中可能会存在一些问题,比如 pod 被杀掉需要另外启一个,导致 Prometheus 数据丢失,这就需要一个 Prometheus 的数据可持久化的高可用方案。CNCF 的沙箱项目里面有一个项目叫做 Thanos,它的核心思想相当于是在 Prometheus 之上做了一个类似数据库 sharding 的方案,它有两种架构模式:Sidecar 与 Receiver。目前官方的架构图用的 Sidecar 方案,Receiver 是一个暂时还没有完全发布的组件,Sidecar 方案相对成熟一些,更加高效也更容易扩展。

幻灯片26.jpeg

Service Mesh 解决方案当中的 Linkerd 和 Conduit 都有可视化界面。Istio 相对来说比较黑盒,一直被诟病,不过 Istio 社区联合 Kiali,共同推出了一个可视化方案,提供如下功能:

  • Topology:服务拓扑图;
  • Health:可视化健康检查;
  • Metrics:指标可视化;
  • Tracing:分布式链路追踪可视化;
  • Validations:配置校验;
  • Wizards:路由配置;
  • Configuration:CRD 资源的可视化与编辑;

幻灯片27.jpeg

下面是 Kiali 的架构,可以比较清楚地看出,其本身是一个前后端分离的架构,并且可以从 Prometheus 或者集群特定 API 获取指标数据,另外也囊括了 Jaeger 链路追踪界面与 Grafana 展示界面,不过它们并非开箱即用,Kiali 依赖的三方组件需要单独部署。

幻灯片28.jpeg

总结

在许多的中小型公司内部,其实 Service Mesh 还是处于一个预研阶段,实际落地的时候需要考虑的因素繁多,如何才能获得较好的的投入产出效能比,是每一个做选型的人员都必须要经历的。其实不管落地情况,鉴于云原生的可观察性哲学来说,在落地的同时做好可观察性,可以同步解决很多问题,避免耗费过多的资源在无意义的事情上面,综合可观察性的三大支柱以及 Service Mesh 中对可观察性的支持来说,总结如下:

  • Metrics:合理运用 Prometheus,并且做好持久化与高可用工作是关键;
  • Tracing:选择合适的链路追踪中间件,关键在于集成契合度、整合 Logging、存储、展示来考量;
  • Logging:什么场景使用原始日志,什么场景使用摘要日志,要明确;

嘉宾介绍

叶志远,G7 微服务架构师,Spring Cloud 中国社区联合创始人,ServiceMesher 社区成员,《重新定义 Spring Cloud 实战》作者,国内微服务领域早期实践者,云原生追随者。

回顾视频以及 PPT 下载地址

视频回顾:www.bilibili.com/video/BV13K…
PPT 下载:github.com/servicemesh…

参考资料

  • Metrics, tracing, and logging - Peter Bourgon
  • Go for Industrial Programming - Peter Bourgon
  • CNCF Landscape
  • Exploring Istio telemetry and observability - Marton Sereg
  • Istio Service Mesh Observability with Kiali - Gokul Chandra
  • MOSN:github.com/mosn/mosn

公众号:金融级分布式架构(Antfin_SOFA)

本文转载自: 掘金

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

工作累了,用java写个游戏吧!开源一款游戏引擎

发表于 2020-06-03

原创:小姐姐味道(微信公众号ID:xjjdog),欢迎分享,转载请保留出处。

吃喝玩乐是人类的基本需求,只有繁重的工作,生活完全没有乐趣,与上世纪的奴隶没什么区别。游戏作为一种生活的调剂品,占据了大部分人的生活。凡事过犹不及,all work || all play都会对个体造成不可磨灭的损伤。工作累了,不如使用熟悉的开发语言,自己做一款游戏。虽然粗糙,但那是自己的作品—一些想对这个世界说的话。

本次开源的游戏引擎叫做c2d-engine,基于Java界最流行的游戏框架Libgdx,偏底层。虽然没有一些专业的游戏引擎功能齐全,但使用它,可以做一些跨平台的复杂游戏。

比如下面这个游戏,可以实现一些非常酷的效果和逻辑。

口说无凭,你可以下载到电脑上试试。

1
复制代码https://gitee.com/xjjdog/download/raw/master/digger-desktop.jar

下面这张图,是游戏引擎内置的Box2d物理引擎场景编辑器。可以模拟2纬世界的所有物理动作。

体验链接在这里:

1
复制代码https://gitee.com/xjjdog/download/raw/master/c2d-box2d-tools.jar

下面是游戏引擎的一些信息。

git地址为:

1
复制代码https://github.com/xjjdog/c2d-engine

目前是2.0.0版本,使用maven即可引入:

1
2
3
4
5
复制代码<dependency>
<groupId>com.github.xjjdog</groupId>
<artifactId>c2d-core</artifactId>
<version>2.0.0</version>
</dependency>

主要的功能模块有:

  • 跨平台
  • 资源管理:图片、音效、音乐
  • 自定义资源加载,资源加密
  • 事件管理,随时接受和发送事件
  • 多游戏场景管理,游戏场景切换效果(内置16种)
  • 多层视差,无限循环图层支持
  • 启动界面自定义(内置5种)
  • 摄像机跟随,3D效果
  • 多种背景效果:动图、Mesh、Surface
  • Analog控制模块
  • UI设计体系
  • 简单粒子系统
  • 物理引擎Box2d编辑器
  • 多个效果示例(水波、闪电、渐变等)
  • GLSL支持

可以说一下自己的一些感受。游戏客户端开发虽然没有什么高并发之类的挑战,但是对代码的组织能力要求也是有的,很容易就造成了代码的膨胀,复用在这里显得格外重要。

当然,游戏最重要的是策划、图片和音效,程序员只管码字,它是一个团队合作的结果。

我一个常年搞服务端的人,也非常的迷恋游戏开发。这款游戏引擎是很多年前的作品,当时idea还不流行。正好最近有朋友想要开发一款2D游戏,于是xjjdog借着这个机会,抽周末时间重构了一下代码,现在能够流畅的跑在maven环境里了,Idea无压力。

考虑到有不少同学也有自己搞一款游戏的梦想,也可以修炼自己的编码水平,于是就把它放出来了。

同时,欣赏了一下自己多年前写的代码,真的是不忍直视!

作者简介:小姐姐味道 (xjjdog),一个不允许程序员走弯路的公众号。聚焦基础架构和Linux。十年架构,日百亿流量,与你探讨高并发世界,给你不一样的味道。我的个人微信xjjdog0,欢迎添加好友,​进一步交流。​

本文转载自: 掘金

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

高频golang面试题:简单聊聊内存逃逸?

发表于 2020-06-03


问题
–

知道golang的内存逃逸吗?什么情况下会发生内存逃逸?

怎么答

golang程序变量会携带有一组校验数据,用来证明它的整个生命周期是否在运行时完全可知。如果变量通过了这些校验,它就可以在栈上分配。否则就说它 逃逸 了,必须在堆上分配。

能引起变量逃逸到堆上的典型情况:

  • 在方法内把局部变量指针返回 局部变量原本应该在栈中分配,在栈中回收。但是由于返回时被外部引用,因此其生命周期大于栈,则溢出。
  • 发送指针或带有指针的值到 channel 中。 在编译时,是没有办法知道哪个 goroutine 会在 channel 上接收数据。所以编译器没法知道变量什么时候才会被释放。
  • 在一个切片上存储指针或带指针的值。 一个典型的例子就是 []*string 。这会导致切片的内容逃逸。尽管其后面的数组可能是在栈上分配的,但其引用的值一定是在堆上。
  • slice 的背后数组被重新分配了,因为 append 时可能会超出其容量( cap )。 slice 初始化的地方在编译时是可以知道的,它最开始会在栈上分配。如果切片背后的存储要基于运行时的数据进行扩充,就会在堆上分配。
  • 在 interface 类型上调用方法。 在 interface 类型上调用方法都是动态调度的 —— 方法的真正实现只能在运行时知道。想像一个 io.Reader 类型的变量 r , 调用 r.Read(b) 会使得 r 的值和切片b 的背后存储都逃逸掉,所以会在堆上分配。

举例

  • 通过一个例子加深理解,接下来尝试下怎么通过 go build -gcflags=-m 查看逃逸的情况。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
复制代码package main
import "fmt"
type A struct {
s string
}
// 这是上面提到的 "在方法内把局部变量指针返回" 的情况
func foo(s string) *A {
a := new(A)
a.s = s
return a //返回局部变量a,在C语言中妥妥野指针,但在go则ok,但a会逃逸到堆
}
func main() {
a := foo("hello")
b := a.s + " world"
c := b + "!"
fmt.Println(c)
}

执行go build -gcflags=-m main.go

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
复制代码go build -gcflags=-m main.go
# command-line-arguments
./main.go:7:6: can inline foo
./main.go:13:10: inlining call to foo
./main.go:16:13: inlining call to fmt.Println
/var/folders/45/qx9lfw2s2zzgvhzg3mtzkwzc0000gn/T/go-build409982591/b001/_gomod_.go:6:6: can inline init.0
./main.go:7:10: leaking param: s
./main.go:8:10: new(A) escapes to heap
./main.go:16:13: io.Writer(os.Stdout) escapes to heap
./main.go:16:13: c escapes to heap
./main.go:15:9: b + "!" escapes to heap
./main.go:13:10: main new(A) does not escape
./main.go:14:11: main a.s + " world" does not escape
./main.go:16:13: main []interface {} literal does not escape
<autogenerated>:1: os.(*File).close .this does not escape
  • ./main.go:8:10: new(A) escapes to heap 说明 new(A) 逃逸了,符合上述提到的常见情况中的第一种。
  • ./main.go:14:11: main a.s + " world" does not escape 说明 b 变量没有逃逸,因为它只在方法内存在,会在方法结束时被回收。
  • ./main.go:15:9: b + "!" escapes to heap 说明 c 变量逃逸,通过fmt.Println(a ...interface{})打印的变量,都会发生逃逸,感兴趣的朋友可以去查查为什么。
  • 以上操作其实就叫逃逸分析。下篇文章,跟大家聊聊怎么用一个比较trick的方法使变量不逃逸。方便大家在面试官面前秀一波。

文章推荐:

  • golang面试题:简单聊聊内存逃逸?
  • golang面试题:字符串转成byte数组,会发生内存拷贝吗?
  • golang面试题:翻转含有中文、数字、英文字母的字符串
  • golang面试题:拷贝大切片一定比小切片代价大吗?
  • golang面试题:能说说uintptr和unsafe.Pointer的区别吗?

如果你想每天学习一个知识点?

本文转载自: 掘金

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

1…807808809…956

开发者博客

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