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

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


  • 首页

  • 归档

  • 搜索

Shiro权限管理框架(一)---常用API与JavaSE环

发表于 2021-11-30

一、权限管理概述

权限管理,一般指根据系统设置的安全规则或者安全策略,用户可以访问而且只能访问自己被授权的资源。权限管理几乎出现在任何系统里面,只要有用户和密码的系统。 很多人常将“用户身份认证”、“密码加密”、“系统管理”等概念与权限管理概念混淆。

在权限管理中使用最多的还是功能权限管理中的基于角色访问控制(RBAC,Role Based Access Control)。

image.png

在项目中需要使用权限管理的时候,我们可以选择自己去实现,也可以选择使用第三方实现好框架,

权限管理系统必备的功能

1.权限管理(自定义权限注解/加载权限)

2.角色管理(新增/编辑/删除/关联权限)

3.用户管理(新增/编辑/删除/关联用户)

4.登录功能(定义登录拦截器/登录逻辑实现/登出功能)

5.权限拦截(定义权限拦截器/拦截逻辑实现)

使用权限框架能帮我们做的事情

功能 权限框架能否完成 原因
权限管理 否 用户根据需求,自定义有哪些权限,如何管理
角色管理 否 用户根据需求,自定义有哪些角色,如何管理,不同角色有那些角色
用户管理 否 用户根据需求,自定义有哪些用户,如何管理,不同用户有那些角色
登录功能 是 (密码加密、验证码、记住我)
权限拦截 是 (内置很多的拦截器、提供标签/注解/编程方式进行权限认证)
其他功能 是 (缓存、会话管理等)

以上是对权限管理的简单说明

二、Apache Shiro概述

Apache Shiro 是一个强大且易用的 Java 安全框架,使用 Apache Shiro 的人越来越多,它可实现身份验证、授权、密码和会话管理等功能。

Shiro可以帮助我们完成,认证,授权,加密,会话管理,与WEB继承**(证明在JavaSE环境下也可以使用)**,缓存等;

1、Apache Shiro Features 特性

Apache Shiro是一个全面的、蕴含丰富功能的安全框架。下图为描述Shiro功能的框架图:

v2-e72930a8351ccf1590779ea87ac5cb65_r.jpg

Authentication(认证), Authorization(授权), Session Management(会话管理), Cryptography(加密)被 Shiro 框架的开发团队称之为应用安全的四大基石。那么就让我们来看看它们吧:

  • **Authentication(认证):**用户身份识别,通常被称为用户“登录”
  • **Authorization(授权):**访问控制。比如某个用户是否具有某个操作的使用权限。
  • **Session Management(会话管理):**特定于用户的会话管理,甚至在非web 或 EJB 应用程序。
  • **Cryptography(加密):**在对数据源使用加密算法加密的同时,保证易于使用。

还有其他的功能来支持和加强这些不同应用环境下安全领域的关注点。特别是对以下的功能支持:

  • **Web支持:**Shiro的Web支持API有助于保护Web应用程序。
  • **缓存:**缓存是Apache Shiro API中的第一级,以确保安全操作保持快速和高效。
  • **并发性:**Apache Shiro支持具有并发功能的多线程应用程序。
  • **测试:**存在测试支持,可帮助您编写单元测试和集成测试,并确保代码按预期得到保障。
  • **“运行方式”:**允许用户承担另一个用户的身份(如果允许)的功能,有时在管理方案中很有用。
  • **“记住我”:**记住用户在会话中的身份,所以用户只需要强制登录即可。

注意: Shiro不会去维护用户、维护权限,这些需要我们自己去设计/提供,然后通过相应的接口注入给Shiro

2、High-Level Overview 高级概述

在概念层,Shiro 架构包含三个主要的理念:Subject,SecurityManager和 Realm。下面的图展示了这些组件如何相互作用,我们将在下面依次对其进行描述。

v2-c0841dfc8cb19a94c322eef635371cf6_1440w.jpg

  • **Subject:**当前用户,Subject 可以是一个人,但也可以是第三方服务、守护进程帐户、时钟守护任务或者其它–当前和软件交互的任何事件。
  • **SecurityManager:**管理所有Subject,SecurityManager 是 Shiro 架构的核心,配合内部安全组件共同组成安全伞。
  • **Realms:**用于进行权限信息的验证,我们自己实现。Realm 本质上是一个特定的安全 DAO:它封装与数据源连接的细节,得到Shiro 所需的相关的数据。在配置 Shiro 的时候,你必须指定至少一个Realm 来实现认证(authentication)和/或授权(authorization)。

我们需要实现Realms的Authentication 和 Authorization。其中 Authentication 是用来验证用户身份,Authorization 是授权访问控制,用于对用户进行的操作授权,证明该用户是否允许进行当前操作,如访问某个链接,某个资源文件等.

3、Shiro 认证过程

v2-2531156d2e6fb3ec0702f1d1ed795f43_1440w.jpg

4、Shiro常用方法

4.1 Role-Based Authorization(角色验证)

注解 描述
hasRole(String roleName) 返回true,当前Subject(登陆工号)有该角色权限,false,没有
hasRoles(List roleNames) 返回true,至少在集合中存在一个角色权限,false一个都没有
hasAllRoles(Collection roleNames) 返回true,当前工号拥有列表所有角色,否则返回false

4.2 Role Assertions(角色检查)

求区别在于 check开头的是没有返回值的,当没有权限时就会抛出异常

方法 描述
checkRole(String roleName) 判断当前Subject(工号)有没有该角色, 有则不抛出异常,若没有抛出AuthorizationException
checkRoles(Collection roleNames) 若当前Subject(工号)拥有所有该集合角色不抛出异常,若没有抛出AuthorizationException
checkRoles(String… roleNames) 同上,只不过采用java5的新特性

​

4.3 Permission-Based Authorization(权限校验)

Permission在某种程度上可以理解为字符串,为一个权限编号即可,也提供了字符串的权限校验

方法 描述
isPermitted(Permission p)/isPermitted(String perm) 判断当前Subject(工号)是否拥有该权限,是返回true,否则false
isPermitted(List perms)或isPermitted(String perms, …, String perms) 判断当前Subject(工号)是否拥有集合中的权限,即返回true,否则false
isPermittedAll(Collection perms)/isPermittedAll(String… perms) 判断当前Subject(工号)是否有集合中的所有权限,是返回true,否则false

4.4 Permission Assertions(权限检查)

这个跟角色的是一样的意思,就不解释了,求区别在于 check开头的是没有返回值的,当没有权限时就会抛出异常

方法 描述
checkPermission(Permission p)
checkPermission(String perm)
checkPermissions(Collection perms)
checkPermissions(String… perms)

5、Shiro也可以通过注解来处理,常用的注解有下面这些:

注解都会抛出异常,但这个异常不需要我们来刻意处理,shiro会来处理,跳转到登陆界面或者其他

注解 描述
@RequiresAuthentication 是否经过认证或者登陆,若没有的话会抛出异常UnauthenticatedException
@RequiresGuest 未认证或者叫未登陆,可能在remember me状态下,否则抛出异常UnauthenticatedException
@RequiresPermissions 检查是否有该权限,没有抛出异常AuthorizationException
@RequiresRoles 检查是否有该角色,没有抛出异常AuthorizationException
@RequiresUser 这个刚好跟@RequiresGuest相反,这个必须经过认证,或者从rememberme进行登陆,这个没有RequiresAuthentication严格但类似,否则抛出异常AuthorizationException

6、案例演示:

因为Shiro是支持在JavaSE环境下使用的,我们先在SE环境下,模拟一下Shiro的操作流程, 熟悉一下API

环境准备

  • 创建普通的Maven项目
  • 添加必要的依赖
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
xml复制代码<dependency>
<groupId>commons-logging</groupId>
<artifactId>commons-logging</artifactId>
<version>1.1.3</version>
</dependency>
<dependency>
<groupId>org.apache.shiro</groupId>
<artifactId>shiro-core</artifactId>
<version>1.5.2</version>
</dependency>

<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>4.13.2</version>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>1.16.22</version>
<scope>provided</scope>
</dependency>

方式一: 基于ini的鉴权

在resource文件夹下新建一个.ini文件

shiro默认支持的是ini配置的方式

shiro-authc.ini

1
2
3
4
5
6
7
8
9
ini复制代码#用户的身份、凭据、角色
[users]
yl=888,hr,seller
liuxw=666,seller

#角色与权限信息
[roles]
hr=user:list,user:delete
seller=customer:list,customer:save6

新建一个测试类

ShiroDemoText.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
java复制代码public class ShiroDemoText {

@Test
public void testAuthorByIni(){
/*************************登录逻辑开始****************************/
DefaultSecurityManager securityManager = new DefaultSecurityManager();

IniRealm iniRealm = new IniRealm("classpath:shiro-authc.ini");

securityManager.setRealm(iniRealm);

SecurityUtils.setSecurityManager(securityManager);

Subject subject = SecurityUtils.getSubject();

UsernamePasswordToken token = new UsernamePasswordToken("liuxw", "666");

subject.login(token);

/*******************************登录逻辑结束,鉴权开始***********************************/

//判断用户是否登录
System.out.println("是否登录---->"+subject.isAuthenticated());

//判断用户是否有某个角色
System.out.println("是否有hr这个角色---->"+subject.hasRole("hr"));
System.out.println("是否有seller这个角色---->"+subject.hasRole("seller"));

//check开头的是没有返回值的,当没有权限时就会抛出异常
subject.checkRole("seller");

//是否同时拥有多个角色
System.out.println("是否同时拥有hr和seller----->"+Arrays.toString(subject.hasRoles(Arrays.asList("hr","seller"))));
System.out.println("是否同时拥有hr和seller----->"+subject.hasAllRoles(Arrays.asList("hr","seller")));

//判断用户是否有某个权限
System.out.println("是否有用户删除权限------>"+subject.isPermitted("user:delete"));

subject.checkPermission("customer:list");

}
}

运行结果:
image.png

方式二: 基于自定义Realm的鉴权

新建User.java

1
2
3
4
5
6
7
8
java复制代码@Setter
@Getter
@AllArgsConstructor
@NoArgsConstructor
public class User {
private String username;//用户名
private String password;//密码
}

新建DataMapper.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
java复制代码public class DataMapper {
//用户集合
private static Map<String, User> userData = new HashMap<String, User>();
//角色集合
private static Map<String, List<String>> roleData = new HashMap<String, List<String>>();
//权限集合
private static Map<String, List<String>> permissionData = new HashMap<String, List<String>>();
static{
//初始化用户数据
User u1 = new User("lanxw","666");
userData.put(u1.getUsername(),u1);
roleData.put(u1.getUsername(), Arrays.asList("seller"));
permissionData.put(u1.getUsername(),
Arrays.asList("customer:list","customer:save"));

User u2 = new User("yl","888");
userData.put(u2.getUsername(),u2);
roleData.put(u2.getUsername(), Arrays.asList("seller","hr"));
permissionData.put(u1.getUsername(),
Arrays.asList("customer:list","customer:save","user:list","user:delete"));
}
//提供静态方法,模拟数据库返回数据
public static User getUserByName(String username){
return userData.get(username);
}
public static List<String> getRoleByName(String username){
return roleData.get(username);
}
public static List<String> getPermissionByName(String username){
return permissionData.get(username);
}
}

新建UserRealm.java 继承 AuthorizingRealm类

AuthorizingRealm类是Reaml类的子类,有缓冲,认证,授权的功能,我们继承它使用就好了

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
java复制代码public class UserRealm extends AuthorizingRealm {



//授权
/**提供授权信息*/
@Override
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) {
//principalCollection.getPrimaryPrincipal()
//其实就是在认证时放入SimpleAuthenticationInfo的第一个参数
User user = (User) principalCollection.getPrimaryPrincipal();
SimpleAuthorizationInfo info = new SimpleAuthorizationInfo();

//根据登录用的名称查询到其拥有的所有角色的编码
List<String> roleByName = DataMapper.getRoleByName(user.getUsername());
//将用户拥有的角色添加到授权信息对象中,供Shiro权限校验时使用
info.addRoles(roleByName);
//根据登录用户的名称查询到其拥有的所有权限表达式
List<String> permissionByName = DataMapper.getPermissionByName(user.getUsername());
//将用户拥有的权限添加到授权信息对象中,供Shiro权限校验时使用
info.addStringPermissions(permissionByName);

return info;
}

//认证
/**提供认证信息*/
@Override
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException {
//从页面传入的账户
String username = (String) token.getPrincipal();
//模拟数据库中查询数据
User user = DataMapper.getUserByName(username);
if (user == null) {
//如果没有查到就返回一个空
return null;
} else {
//如果存在需要封装成AuthenticationInfo对象返回
/**
* user,身份对象,可以理解为在Web环境中登录成功后需要放入Session中的对象
* user.getPassword()凭证(密码),需要和传入的凭证(密码)做比对
* this.getName()当前 Realm 的名称,暂时无用,不需纠结
*
* */
return new SimpleAuthenticationInfo(user,user.getPassword(),this.getName());
}
}
}

新建一个测试类

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
java复制代码    @Test
public void testAuthorByRealm(){
/*************************登录验证**************************/
//创建Shiro安全管理器,是Shiro的核心
DefaultSecurityManager securityManager = new DefaultSecurityManager();

//加载shiro.ini配置,得到配置中的用户信息
securityManager.setRealm(new UserRealm());

//把安全管理器注入到当前的环境中
SecurityUtils.setSecurityManager(securityManager);

//无论有无登录都可以获取到subject主体对象,但是判断登录状态需要利用里面的属性来判断
Subject subject = SecurityUtils.getSubject();

//创建令牌(携带用户的账户和密码)
UsernamePasswordToken usernamePasswordToken = new UsernamePasswordToken("liuxw","666");

System.out.println("认证状态----->"+subject.isAuthenticated());

/*************************登录验证结束**************************/

//判断用户是否登录
System.out.println("是否登录---->"+subject.isAuthenticated());

//判断用户是否有某个角色
System.out.println("是否有hr这个角色---->"+subject.hasRole("hr"));
System.out.println("是否有seller这个角色---->"+subject.hasRole("seller"));

//check开头的是没有返回值的,当没有权限时就会抛出异常
//subject.checkRole("seller");

//是否同时拥有多个角色
System.out.println("是否同时拥有hr和seller----->"+Arrays.toString(subject.hasRoles(Arrays.asList("hr","seller"))));
System.out.println("是否同时拥有hr和seller----->"+subject.hasAllRoles(Arrays.asList("hr","seller")));

//判断用户是否有某个权限
System.out.println("是否有用户删除权限------>"+subject.isPermitted("user:delete"));

//subject.checkPermission("customer:list");

}

结果:
image.png


后记:

​ 这个是案例是简单的在JavaSE环境下对Shiro的使用,数据都是模拟的,通过ini文件和自定义Realm两种方式,熟悉ShiroAPI,Shiro具体的内部是怎么实现的整理好了再发出来,还有Shiro”加盐加密”的基本操作下次发出来.

本文转载自: 掘金

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

Go Chan 源码解析

发表于 2021-11-30

本篇文章内容基于go1.14.2分析

golang的chan是一个内置类型,作为csp编程的核心数据结构,其底层数据结构是一个叫hchan的struct:

1
2
3
4
5
6
7
8
9
10
11
12
13
go复制代码type hchan struct {
qcount uint // 队列中的元素数量
dataqsiz uint // (环形)队列的大小
buf unsafe.Pointer // 队列的指针
elemsize uint16 // 元素大小
closed uint32 // 是否已close
elemtype *_type // 元素类型
sendx uint // 环形队列中,send的位置
recvx uint // 环形队列中 recv的位置
recvq waitq // 读取等待队列
sendq waitq // 发送等待队列
lock mutex // 互斥锁
}

如图所示,chan最核心的部分由一个环形队列和2个waitq组成,环形队列用于存放数据(带缓冲的情况下),waitq用于实现阻塞和恢复goroutine。

chan的相关操作

对chan的操作有:make、读、写、close,当然还有select,这里只讨论前面四个操作。

创建 chan

当在代码中使用make创建chan时,编译器会根据情况自动替换成makechan64 或者makechan,makechan64 其实还是调用了makechan函数。

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
go复制代码func makechan(t *chantype, size int) *hchan {
elem := t.elem

// 确保元素类型的size < 2^16,
if elem.size >= 1<<16 {
throw("makechan: invalid channel element type")
}
// 检查内存对齐
if hchanSize%maxAlign != 0 || elem.align > maxAlign {
throw("makechan: bad alignment")
}

// 计算缓冲区所需分配内存大小
mem, overflow := math.MulUintptr(elem.size, uintptr(size))
if overflow || mem > maxAlloc-hchanSize || size < 0 {
panic(plainError("makechan: size out of range"))
}

var c *hchan
switch {
case mem == 0:
// 即不带缓冲区的情况,只需要调用mallocgc分配
c = (*hchan)(mallocgc(hchanSize, nil, true))
// 理解为空地址
c.buf = c.raceaddr()
case elem.ptrdata == 0:
// 元素类型不包含指针的情况
c = (*hchan)(mallocgc(hchanSize+mem, nil, true))
c.buf = add(unsafe.Pointer(c), hchanSize)
default:
// 默认情况下:包含指针
c = new(hchan)
c.buf = mallocgc(mem, elem, true)
}

c.elemsize = uint16(elem.size)
c.elemtype = elem
c.dataqsiz = uint(size)

if debugChan {
print("makechan: chan=", c, "; elemsize=", elem.size, "; dataqsiz=", size, "\n")
}
return c
}

chan 写操作

当对chan进行写入“ch <- interface{}” 时,会被编译器替换成chansend1函数的调用,最终还是调用了chansend函数:

1
2
3
4
go复制代码//elem 是待写入元素的地址
func chansend1(c *hchan, elem unsafe.Pointer) {
chansend(c, elem, true, getcallerpc())
}

先看看chansend的函数签名,只需关注ep和block这个两个参数即可,ep是要写入数据的地址,block表示是否阻塞式的调用

1
go复制代码func chansend(c *hchan, ep unsafe.Pointer, block bool, callerpc uintptr) bool

chansend有以下几种处理流程:

  1. 当对一个nil chan进行写操作时,如果是非阻塞调用,直接返回;否则将当前协程挂起
1
2
3
4
5
6
7
8
go复制代码// chansend 对一个 nil chan发送数据时,如果是非阻塞则直接返回,否则将当前协程挂起
if c == nil {
if !block {
return false
}
gopark(nil, nil, waitReasonChanSendNilChan, traceEvGoStop, 2)
throw("unreachable")
}
  1. 非阻塞模式且chan未close,没有缓冲区且没有等待接收或者缓冲区满的情况下,直接return false。
1
2
3
4
5
6
7
go复制代码// 1. 非阻塞模式且chan未close
// 2. 没有缓冲区且没有等待接收 或者 缓冲区满的情况下
// 满足以上条件直接return false
if !block && c.closed == 0 && ((c.dataqsiz == 0 && c.recvq.first == nil) ||
(c.dataqsiz > 0 && c.qcount == c.dataqsiz)) {
return false
}
  1. c.recvq中有等待读的接收者,将其出队,将数据直接copy给接收者,并唤醒接收者。
1
2
3
4
5
6
7
8
go复制代码// 有等待的接收的goroutine
// 出队,传递数据
if sg := c.recvq.dequeue(); sg != nil {
// Found a waiting receiver. We pass the value we want to send
// directly to the receiver, bypassing the channel buffer (if any).
send(c, sg, ep, func() { unlock(&c.lock) }, 3)
return true
}

recvq是一个双向链表,每个sudog会关联上一个reader(被阻塞的g)

当sudog出队后,会调用send方法,通过sendDirect 实现数据在两个地址之间拷贝,最后调用goready唤醒reader(被阻塞的g)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
go复制代码func send(c *hchan, sg *sudog, ep unsafe.Pointer, unlockf func(), skip int) {
// ... 剔除无关代码
if sg.elem != nil {
// 直接将数据拷贝到变量ep所在的地址
sendDirect(c.elemtype, sg, ep)
sg.elem = nil
}
gp := sg.g
unlockf()
gp.param = unsafe.Pointer(sg)
if sg.releasetime != 0 {
sg.releasetime = cputicks()
}
//将reader的goroutine唤起
goready(gp, skip+1)
}
  1. 缓冲区未满的情况下,数据放入环形缓冲区即可。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
go复制代码	// 缓冲区未满
// 将数据放到缓冲区
if c.qcount < c.dataqsiz {
// Space is available in the channel buffer. Enqueue the element to send.
// 存放位置
qp := chanbuf(c, c.sendx)
if raceenabled {
raceacquire(qp)
racerelease(qp)
}
typedmemmove(c.elemtype, qp, ep)
// 指针自增
c.sendx++
if c.sendx == c.dataqsiz {
c.sendx = 0
}
c.qcount++
unlock(&c.lock)
return true
}
  1. 缓冲区已满,阻塞模式下关联一个sudog数据结构并进入c.sendq队列,挂起当前协程。
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
go复制代码	// 阻塞的情况
gp := getg() //拿到当前g
mysg := acquireSudog() // 获取一个sudog
mysg.releasetime = 0
if t0 != 0 {
mysg.releasetime = -1

mysg.elem = ep //关联ep,即待写入的数据地址
mysg.waitlink = nil
mysg.g = gp
mysg.isSelect = false
mysg.c = c
gp.waiting = mysg
gp.param = nil
c.sendq.enqueue(mysg) // 入队
// Signal to anyone trying to shrink our stack that we're about
// to park on a channel. The window between when this G's status
// changes and when we set gp.activeStackChans is not safe for
// stack shrinking.
atomic.Store8(&gp.parkingOnChan, 1)
// 将g休眠,让出cpu
// gopark后,需等待reader来唤醒它
gopark(chanparkcommit, unsafe.Pointer(&c.lock), waitReasonChanSend, traceEvGoBlockSend, 2)
// 唤醒过后
// Ensure the value being sent is kept alive until the
// receiver copies it out. The sudog has a pointer to the
// stack object, but sudogs aren't considered as roots of the
// stack tracer.
// 保持数据不被回收
KeepAlive(ep)

// someone woke us up.
if mysg != gp.waiting {
throw("G waiting list is corrupted")
}
gp.waiting = nil
gp.activeStackChans = false
if gp.param == nil {
if c.closed == 0 {
throw("chansend: spurious wakeup")
}
panic(plainError("send on closed channel"))
}
gp.param = nil
if mysg.releasetime > 0 {
blockevent(mysg.releasetime-t0, 2)
}
mysg.c = nil
releaseSudog(mysg)
return true

chan 读操作

当对chan进行读操作时,编译器会替换成 chanrecv1或者chanrecv2函数,最终会调用chanrecv函数处理读取

1
2
3
4
5
6
7
8
9
go复制代码// v := <- ch
func chanrecv1(c *hchan, elem unsafe.Pointer) {
chanrecv(c, elem, true)
}
// v, ok := <- ch
func chanrecv2(c *hchan, elem unsafe.Pointer) (received bool) {
_, received = chanrecv(c, elem, true)
return
}

和chansend一样,chanrecv也是支持非阻塞式的调用

1
go复制代码func chanrecv(c *hchan, ep unsafe.Pointer, block bool) (selected, received bool)

chanrecv有以下几种处理流程:

  1. 读nil chan,如果是非阻塞,直接返回;如果是阻塞式,将当前协程挂起。
1
2
3
4
5
6
7
8
go复制代码	// 读阻塞
if c == nil {
if !block {
return
}
gopark(nil, nil, waitReasonChanReceiveNilChan, traceEvGoStop, 2)
throw("unreachable")
}
  1. 非阻塞模式下,没有缓冲区且没有等待写的writer或者缓冲区没数据,直接返回。
1
2
3
4
5
go复制代码	if !block && (c.dataqsiz == 0 && c.sendq.first == nil ||
c.dataqsiz > 0 && atomic.Loaduint(&c.qcount) == 0) &&
atomic.Load(&c.closed) == 0 {
return
}
  1. chan已经被close,并且队列中没有数据时,会将存放值的变量清零,然后返回。
1
2
3
4
5
6
7
8
9
10
11
12
go复制代码	// c已经被close 并且 没有数据
// 清除ep指针
if c.closed != 0 && c.qcount == 0 {
if raceenabled {
raceacquire(c.raceaddr())
}
unlock(&c.lock)
if ep != nil {
typedmemclr(c.elemtype, ep)
}
return true, false
}
  1. sendq中有等待的writer,writer出队,并调用recv函数
1
2
3
4
5
6
7
8
9
10
go复制代码// 从sendq中取出sender
if sg := c.sendq.dequeue(); sg != nil {
// Found a waiting sender. If buffer is size 0, receive value
// directly from sender. Otherwise, receive from head of queue
// and add sender's value to the tail of the queue (both map to
// the same buffer slot because the queue is full).
// 从sender中读取数据
recv(c, sg, ep, func() { unlock(&c.lock) }, 3)
return true, true
}

recv在这分两种处理:如果ch不带缓冲区的话,直接将writer的sg.elem数据拷贝到ep;如果带缓冲区的话,此时缓冲区肯定满了,那么就从缓冲区队列头部取出数据拷贝至ep,然后将writer的sg.elem数据拷贝到缓冲区中,最后唤醒writer(g)

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
go复制代码func recv(c *hchan, sg *sudog, ep unsafe.Pointer, unlockf func(), skip int) {
// 不带缓冲区的情况
// 直接copy from sender
if c.dataqsiz == 0 {
if raceenabled {
racesync(c, sg)
}
if ep != nil {
// copy data from sender
recvDirect(c.elemtype, sg, ep)
}
} else {
// Queue is full. Take the item at the
// head of the queue. Make the sender enqueue
// its item at the tail of the queue. Since the
// queue is full, those are both the same slot.
// 队列已满
// 队列元素出队
qp := chanbuf(c, c.recvx)
if raceenabled {
raceacquire(qp)
racerelease(qp)
raceacquireg(sg.g, qp)
racereleaseg(sg.g, qp)
}
// copy data from queue to receiver
// 数据拷贝给ep
if ep != nil {
typedmemmove(c.elemtype, ep, qp)
}
// copy data from sender to queue
// 将sender的数据拷贝到这个槽中
typedmemmove(c.elemtype, qp, sg.elem)
c.recvx++
if c.recvx == c.dataqsiz {
c.recvx = 0
}
c.sendx = c.recvx // c.sendx = (c.sendx+1) % c.dataqsiz
}
// 置空
sg.elem = nil
gp := sg.g
unlockf()
gp.param = unsafe.Pointer(sg)
if sg.releasetime != 0 {
sg.releasetime = cputicks()
}
// 唤醒sender协程
goready(gp, skip+1)
}
  1. 直接从缓冲队列中读数。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
go复制代码	// 带缓冲区
if c.qcount > 0 {
// Receive directly from queue
// 直接buf中取
qp := chanbuf(c, c.recvx)
if raceenabled {
raceacquire(qp)
racerelease(qp)
}
// 拷贝数据到ep指针
if ep != nil {
typedmemmove(c.elemtype, ep, qp)
}
// 清除qp
typedmemclr(c.elemtype, qp)
c.recvx++
if c.recvx == c.dataqsiz {
c.recvx = 0
}
c.qcount--
unlock(&c.lock)
return true, true
}
  1. 阻塞的情况,缓冲区没有数据,且没有writer
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
go复制代码
// 阻塞
gp := getg() //拿到当前的goroutine
mysg := acquireSudog() // 获取一个sudog
mysg.releasetime = 0
if t0 != 0 {
mysg.releasetime = -1
}

//sudog 关联
mysg.elem = ep
mysg.waitlink = nil
gp.waiting = mysg
mysg.g = gp
mysg.isSelect = false
mysg.c = c
gp.param = nil
c.recvq.enqueue(mysg) //入队
// Signal to anyone trying to shrink our stack that we're about
// to park on a channel. The window between when this G's status
// changes and when we set gp.activeStackChans is not safe for
// stack shrinking.
atomic.Store8(&gp.parkingOnChan, 1)
// 挂起当前goroutine,等待writer唤醒
gopark(chanparkcommit, unsafe.Pointer(&c.lock), waitReasonChanReceive, traceEvGoBlockRecv, 2)

// 唤醒后
if mysg != gp.waiting {
throw("G waiting list is corrupted")
}
gp.waiting = nil
gp.activeStackChans = false
if mysg.releasetime > 0 {
blockevent(mysg.releasetime-t0, 2)
}
closed := gp.param == nil
gp.param = nil
// sudog解除关联
mysg.c = nil
// 释放sudog
releaseSudog(mysg)

close 关闭操作

当close一个chan时,编译器会替换成对closechan函数的调用,将closed字段置为1,并将recvq和sendq中的goroutine释放唤醒,对sendq中未写入的数据做清除,且writer会发生panic异常。

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
go复制代码func closechan(c *hchan) {
if c == nil {
panic(plainError("close of nil channel"))
}

// 加锁
lock(&c.lock)
// 不可重复close
if c.closed != 0 {
unlock(&c.lock)
panic(plainError("close of closed channel"))
}

if raceenabled {
callerpc := getcallerpc()
racewritepc(c.raceaddr(), callerpc, funcPC(closechan))
racerelease(c.raceaddr())
}

c.closed = 1

var glist gList

// 释放所有的
for {
// 出队
sg := c.recvq.dequeue()
if sg == nil {
break
}
// 清零
if sg.elem != nil {
typedmemclr(c.elemtype, sg.elem)
sg.elem = nil
}
if sg.releasetime != 0 {
sg.releasetime = cputicks()
}
gp := sg.g
gp.param = nil
if raceenabled {
raceacquireg(gp, c.raceaddr())
}
glist.push(gp)
}

// 释放所有writer
for {
// 出队
sg := c.sendq.dequeue()
if sg == nil {
break
}
// 丢弃数据
sg.elem = nil
if sg.releasetime != 0 {
sg.releasetime = cputicks()
}
gp := sg.g
gp.param = nil
if raceenabled {
raceacquireg(gp, c.raceaddr())
}
glist.push(gp)
}
unlock(&c.lock)

// 唤醒所有g
for !glist.empty() {
gp := glist.pop()
gp.schedlink = 0
goready(gp, 3)
}
}

chan使用小技巧

  1. 避免read、write一个nil chan
1
2
3
4
5
6
7
8
9
10
11
go复制代码func main() {
ch := make(chan int,1)

go func() {
time.Sleep(1*time.Second)
ch = nil
}()

ch<-1
ch<-1 // 协程直接挂起
}
  1. 从chan中read时,使用带指示的访问方式,读取的时候无法感知到close的关闭
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
go复制代码func main() {
ch := make(chan int)

go func() {
ch <- 10
close(ch)
}()

for {
select {
// case i, ok := <-ch:
// if ok {
// break
//}
case i := <-ch:
fmt.Println(i)
time.Sleep(100 * time.Millisecond)
}
}
}
  1. 从chan中read时,不要使用已存在变量接收, chan close之后,缓冲区没有数据的话,使用存在变量读取时,会将变量清零
1
2
3
4
5
6
7
8
9
go复制代码func main() {
a := 10
ch := make(chan int,1)

fmt.Println("before close a is: ", a) // a is 10
close(ch)
a = <-ch
fmt.Println("after close a is: ", a) // a is 0
}
  1. 使用select+default可以实现 chan的无阻塞读取
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
go复制代码// 使用select反射包实现无阻塞读写
func tryRead(ch chan int) (int, bool) {
var cases []reflect.SelectCase
caseRead := reflect.SelectCase{
Dir: reflect.SelectRecv,
Chan: reflect.ValueOf(ch),
}

cases = append(cases, caseRead)
cases = append(cases, reflect.SelectCase{
Dir: reflect.SelectDefault,
})

_, v, ok := reflect.Select(cases)

if ok {

return (v.Interface()).(int), ok
}

return 0, ok
}

func tryWrite(ch chan int, data int) bool {
var cases []reflect.SelectCase
caseWrite := reflect.SelectCase{
Dir: reflect.SelectSend,
Chan: reflect.ValueOf(ch),
Send: reflect.ValueOf(data),
}

cases = append(cases, caseWrite)
cases = append(cases, reflect.SelectCase{
Dir: reflect.SelectDefault,
})
chosen, _, _ := reflect.Select(cases)

return chosen == 0
}

// 使用select + default实现无阻塞读写
func tryRead2(ch chan int) (int, bool) {
select {
case v, ok := <-ch:
return v, ok
default:
return 0, false
}
}

func tryWrite2(ch chan int, data int) bool {
select {
case ch <- data:
return true
default:
return false
}
}

原因是如果select的case中存在default,对chan的读写会使用无阻塞的方法

1
2
3
4
5
6
7
8
go复制代码func selectnbsend(c *hchan, elem unsafe.Pointer) (selected bool) {
return chansend(c, elem, false, getcallerpc())
}

func selectnbrecv(elem unsafe.Pointer, c *hchan) (selected bool) {
selected, _ = chanrecv(c, elem, false)
return
}

本文转载自: 掘金

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

使用mvn archetype 撸一个脚手架

发表于 2021-11-30

上一篇# springboot开发,有这个包就够了!介绍了springboot项目开发中常用的基础功能组件封装,本篇基于上一篇产出的springboot基础包依赖,只用一行命令,帮你生成具备基础功能的脚手架,让你不再需要ctrl + c 、ctrl + v创建项目,然你不在需要揪心依赖冲突等重新搭建框架遇到的问题

本项目github 地址github.com/chenxuancod…

什么是archetype

archetype 也就是原型,是一个 Maven 插件,准确说是一个项目模板,它的任务是根据模板创建一个项目结构。

自定义archeType

  • 创建一个普通的maven项目

image.png

  • 定义项目模板
    在archetype-resources目录下,即为要生成的脚手架的模板。你认为通用的脚手架功能需要什么只管往里面扔就是。
    我这里因为集成了base依赖,所以需要做一些Mybatis-plus分页的配置、Swagger的配置等配置,另外提供了个Generate.java用来生成代码
    image.png
    image.png
    总之,模板内容按需添加
  • 定义模板参数
    META-INF/archetype-metadata
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
xml复制代码<?xml version="1.0" encoding="UTF-8"?>
<arche-type-descriptor name="archetype">
<fileSets>
<fileSet filtered="true" encoding="UTF-8" packaged="true">
<directory>src/main/java</directory>
<includes>
<include>**/*.*</include>
</includes>
</fileSet>
<fileSet filtered="true" packaged="true">
<directory>src/test/java</directory>
<includes>
<include>**/*.java</include>
</includes>
</fileSet>
<fileSet filtered="true" packaged="false">
<directory>src/main/resources</directory>
<includes>
<include>**/*.yml</include>
</includes>
</fileSet>
</fileSets>
<requiredProperties>
<requiredProperty key="port"/>
<requiredProperty key="groupId">
<defaultValue>com.sleeper</defaultValue>
</requiredProperty>
<requiredProperty key="artifactId">
<defaultValue>demo</defaultValue>
</requiredProperty>
<requiredProperty key="package">
<defaultValue>${groupId}.${artifactId}</defaultValue>
</requiredProperty>
<requiredProperty key="version">
<defaultValue>1.0.0-SNAPSHOT</defaultValue>
</requiredProperty>
</requiredProperties>
</arche-type-descriptor>

requiredProperties节点内定义了项目模板的变量参数,这些参数可以在mvnarchetype:generate命令生成脚手架时候传入。

生成项目

  • 使用Maven install 编译archetype项目
  • 使用mvn archetype 命令生成项目
1
shell复制代码mvn archetype:generate -DarchetypeGroupId=com.sleeper -DarchetypeArtifactId=archetype -DarchetypeVersion=1.0.0-SNAPSHOT -DgroupId=com.sleeper -DartifactId=demo -Dport=8888

本文转载自: 掘金

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

PDF不小心删掉了怎么恢复?三步专业恢复

发表于 2021-11-30

PDF和其它的电子文档格式相比有非常多的优点,PDF文件可以把文字的字体、格式、颜色和图形图像放在同一个文件中,还可以包含超文本链接、动态影响等信息,安全性和可靠性都比较高,并且PDF文件方便简单,易于操作,word和excel文档都可以转化为PDF文件。PDF文件不小心删掉了怎么恢复?当我们不小心删掉了PDF该怎么做呢?在没有对PDF文件进行备份的情况下,我们应该怎样恢复这些文件呢?有什么便捷的操作吗?本篇文章就为你提供了一个便捷的操作,可以较为轻松地恢复不小心删掉的PDF文件。通过专业的数据恢复软件,来实现PDF文件的数据恢复。

方法一:从回收站恢复不小心删除的PDF文件

如果你是在电脑中对PDF执行了Ctrl+D的操作,那么你还没有完全删除PDF文件,找到桌面的回收站图标,再进行还原就可以了。

方法二:使用软件恢复不小心删除的PDF文件

在这里小编使用数据蛙数据恢复专家软件,来解决大家不小心删掉的PDF文件的烦恼,这款专业的PDF数据恢复软件,经过小编的验证,凭借着以下优势,在数据恢复软件中崭露头角。

数据蛙数据恢复专家软件凭借着恢复算法和界面美观在数据恢复领域立足,并且功能仍然在不断地优化。数据蛙数据恢复专家软件能够恢复各种各样存储介质的数据,例如硬盘、移动硬盘(U盘)、相机等等;能够恢复包括PDF在内的多种文件,例如音频、视频、图片、文档、表格等等。

第一步:进入数据蛙数据恢复专家的官网,选择设备对应的软件版本,进行软件的安装。如果你的PDF文件存储在外接存储设备,比如U盘、SD卡、TF卡、硬盘等存储介质中,需要将存储设备和电脑连接上;如果你的PDF文件在电脑磁盘中,那么你就可以启动软件,进入软件首界面进行文件类型和路径的选择。在这里如果你只想恢复PDF文件,那么你可以勾选文件;全选文件的类型,对于磁盘或者存储设备会有更加全面的扫描。接着选择文件的路径,你就完成了第一步操作。

第二步:点击右下角的扫描,软件就会运行快速扫描的程序,在快速扫描过程中,你可以通过路径或者类型查看文件,上方的筛选器可以筛选文件的类型,通过输入文件的类型和时间就可以筛选出目标文件。

Tips:如果还没有扫描到你需要的PDF文件,那么你可以等待快速扫描结束,点击上方的深度扫描,进行更加深度的文件扫描。

第三步:完成PDF文件的扫描后,如果你忘记了PDF文件的详细信息,你可以在右侧查看PDF文件的详细信息,点击筛选框右侧的图标,可以在上方对文件根据日期、大小、名称进行排序,进行文件的预判。接着找到你的目标PDF文件进行勾选,单击右下角的恢复,在弹窗中选择PDF文件存储的位置。

Tips:在这里你可以新建一个存储文件的文件夹,因为将PDF文件保存在原位会导致恢复失败。

以上就是PDF不小心删掉了怎么恢复的解答,在这里要提醒大家,在日常的文件管理工作中,重要文件的备份是必不可少的。如果你的PDF文件是在电脑磁盘中丢失的,那么你也可以查看电脑的回收站,尝试在回收站进行删除文件的还原。

本文转载自: 掘金

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

盘点一下结构体标签在Go中的应用

发表于 2021-11-30

掌握了Go语言的朋友们应该都知道,在Go的结构体类型声明里面,字段声明后可以跟一个可选的字符串标签。

1
2
3
go复制代码type User struct {
Name string `json:"name"`
}

上面是一个标准的例子,Name字段声明中指定了标签json:"name" xml:"name" ,这个标签值看着有点类似Java程序里给类属性加的注解。

那么这些结构体标签有什么用途呢,我们随便写管用吗?我们平时工作中常用的结构体标签有哪些呢?我们能不能自己定义结构体标签?今天就带大家掰扯清楚这些问题!

结构体标签

Go语言允许我们通过结构体字段标签给一个字段附加可以被反射获取的”元信息“,正好我们上篇文章实战演示Go反射的使用方法和应用场景中讲了Go语言反射使用方法相关的内容,对反射不清楚的可以先去再复习一下。

通常情况下,结构体标签被用于提供结构体字段如何被编码为或者解码自另外一种格式的转换信息(或者是以何种形式被保存至/获取自数据库)。不过,你也可以用它存储任何你想要设置的”元信息“,供其他包或者自己使用。

使用规范

结构体标签在使用上通常是遵守下面三个规范。

结构体标签字符串的值是一个由空格分隔的 key:”value” 对列表,例如:

1
2
3
go复制代码type User struct {
Name string `json:"name" xml:"name"`
}

键,通常表示后面跟的“值”是被哪个包使用的,例如json这个键会被encoding/json包处理使用。如果要键对应的“值”中传递多个信息,通常通过用逗号(’,’)分隔来指定,例如

1
go复制代码Name string `json:"name,omitempty"`

按照管理,如果一个字段的结构体标签里某个键的“值”被设置成了的破折号 (‘-‘),那么就意味着告诉处理该结构体标签键值的进程排除该字段。例如,把一个字段的标签设置成下面这样

1
go复制代码Name string `json:"-"`

就以为进行JSON编码/解码时忽略Name这个字段。

怎么获取到结构体标签

从一开始我们就说结构体标签是给反射准备的,那么怎么在Go程序里用反射获得到字段的结构体标签呢?看了我们上一篇文章的同学,应该会知道,结构体字段类型相关的信息,在反射的世界里使用reflect.StructFiled这个类型表示的。

1
2
3
4
5
6
go复制代码type StructField struct {
Name string
Type Type // field type
Tag StructTag // field tag string
......
}

如上所示,其中包含的Tag字段即代表了字段声明中的结构体标签信息。让我们通过自定义结构体标签的例子来演示一下怎么使用它在反射里读取到标签里的信息。

用反射获取到自定义的结构体标签

使用反射reflect包访问结构体字段的标签值,我们需要先获取到结构体的类型信息Type,然后使用Type.Field(i int) 或 Type.FieldByName(name string),方法查询字段信息,这两个方法都会返回一个StructField类型的值,上面我们也说了它在反射的世界里用于描述一个结构体字段;而StructField.Tag 是一个StructTag 类型的值,它描述了字段的标签值。

之前我们谈到了结构体标签的使用规范,相当于是这个约定意味着如果你遵循它,就可以使用StructTag的Get方法解析标签的值并返回你指定的键的“值”。

1
go复制代码func (tag StructTag) Get(key string) string

为了方便判断一个给定的key是否存在与标签中,StructTag还提供了一个Lookup方法

1
go复制代码func (tag StructTag) Lookup(key string) (value string, ok bool)

跟Get方法不同的是,Lookup会通过返回的ok值告知给定key是否存在与标签中。

下面通过一个例子,演示下获取我们自定义标签的过程。

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
go复制代码package main

import (
"fmt"
"reflect"
)

type User struct {
Name string `mytag:"MyName"`
Email string `mytag:"MyEmail"`
}



func main() {
u := User{"Bob", "bob@mycompany.com"}
t := reflect.TypeOf(u)

for i := 0; i < t.NumField(); i++ {
field := t.Field(i)
fmt.Printf("Field: User.%s\n", field.Name)
fmt.Printf("\tWhole tag value : %s\n", field.Tag)
fmt.Printf("\tValue of 'mytag': %s\n", field.Tag.Get("mytag"))
}
}

上面的程序会输出

1
2
3
4
5
6
shell复制代码Field: User.Name
Whole tag value : mytag:"MyName"
Value of 'mytag': MyName
Field: User.Email
Whole tag value : mytag:"MyEmail"
Value of 'mytag': MyEmail

常用的结构体标签键

常用的结构体标签Key,指的是那些被一些常用的开源包声明使用的结构体标签键。在这里总结了一些,都是一些我们平时会用到的包,它们是:

  • json: 由encoding/json 包使用,详见json.Marshal()的使用方法和实现逻辑。
  • xml : 由encoding/xml包使用,详见xml.Marshal()。
  • bson: 由gobson包,和mongo-go包使用。
  • protobuf: 由github.com/golang/protobuf/proto 使用,在包文档中有详细说明。
  • yaml: 由gopkg.in/yaml.v2 包使用,详见yaml.Marshal()。
  • gorm: 由gorm.io/gorm包使用,示例可以在GORM的文档中找到。

当然这里列的就是最常用的几个库他们提供给我们使用的结构体标签,欢迎大伙踊跃留言,补充一些自己平时用过的库提供给开发者使用的结构体标签。

总结

这篇文章算是我们上一篇讲Go反射的一个实践方向的延伸介绍,如果你也想在自己的包里提供一些结构体标签键,让自己的包更易用些,除了看咱们这篇文章外,还可以去看看上面咱们介绍的几个类库,看它们的源码里是怎么应用的,现学现用!

相关文章

  • 实战演示Go反射的使用方法和应用场景

本文转载自: 掘金

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

springboot整合观察者,实现事件顺序自定义以及异步监

发表于 2021-11-30

前言

我个人接触到的第一个设计模式就是观察者模式。这是23种设计模式中最最常用的模式之一,非常重要。这篇我们就观察者模式聊一聊,原则上聊到哪里算哪里~

什么是观察者模式

基本定义:

当对象间存在一对多关系时,则使用观察者模式(Observer Pattern)。比如,当一个对象被修改时,则会自动通知依赖它的对象。观察者模式属于行为型模式。

  • 意图: 定义对象间的一种一对多的依赖关系,当一个对象的状态发生改变时,所有依赖于它的对象都得到通知并被自动更新。
  • 主要解决: 一个对象状态改变给其他对象通知的问题,而且要考虑到易用和低耦合,保证高度的协作。
  • 何时使用: 一个对象(目标对象)的状态发生改变,所有的依赖对象(观察者对象)都将得到通知,进行广播通知。
  • 如何解决: 使用面向对象技术,可以将这种依赖关系弱化。
  • 关键代码:在抽象类里有一个 ArrayList 存放观察者们。
  • 应用实例: 1、拍卖的时候,拍卖师观察最高标价,然后通知给其他竞价者竞价。 2、西游记里面悟空请求菩萨降服红孩儿,菩萨洒了一地水招来一个老乌龟,这个乌龟就是观察者,他观察菩萨洒水这个动作。
  • 优点: 1、观察者和被观察者是抽象耦合的。 2、建立一套触发机制。
  • 缺点: 1、如果一个被观察者对象有很多的直接和间接的观察者的话,将所有的观察者都通知到会花费很多时间。 2、如果在观察者和观察目标之间有循环依赖的话,观察目标会触发它们之间进行循环调用,可能导致系统崩溃。 3、观察者模式没有相应的机制让观察者知道所观察的目标对象是怎么发生变化的,而仅仅只是知道观察目标发生了变化。

使用场景:

  • 一个抽象模型有两个方面,其中一个方面依赖于另一个方面。将这些方面封装在独立的对象中使它们可以各自独立地改变和复用。
  • 一个对象的改变将导致其他一个或多个对象也发生改变,而不知道具体有多少对象将发生改变,可以降低对象之间的耦合度。
  • 一个对象必须通知其他对象,而并不知道这些对象是谁。
  • 需要在系统中创建一个触发链,A对象的行为将影响B对象,B对象的行为将影响C对象……,可以使用观察者模式创建一种链式触发机制。

注意事项: 1、JAVA 中已经有了对观察者模式的支持类。 2、避免循环引用。 3、如果顺序执行,某一观察者错误会导致系统卡壳,一般采用异步方式。

image.png

spring整合观察者模式

上面对于观察者模式的定义基本来自菜鸟教程,肯定看起来还是官方一些了, 这里我们简单地来说,观察者就是,当我在处理完一些业务之后,想有一些后续收尾工作。但这些工作我不想影响主流程,使其独立在主流程之外,此时观察者是非常合适的,举例说明下,例如在给用户办理业务成功之后想发送邮件或者短信给用户提醒下,那么此时就可以使用观察者。

反过来,在处理业务之前,对不同身份的用户(例如普通用户和会员用户)将执行不同流程的一套操作,此时什么设计模式看起来顺眼呢?没错—策略模式! 对不同场景选取合适的设计模式,会让应用层的编码来的简洁高效。

对于spring来说,整合观察者有大致类似的模板。下面我们直接动手。

  • 定义spring事件
  • 定义事件的监听方
  • 在需要的地方将事件进行推送
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
java复制代码
import lombok.Getter;
import lombok.Setter;
import lombok.ToString;
import org.springframework.context.ApplicationEvent;

/**
* 集成spring的事件即可。你可以在变量的位置盛放业务信息,这里仅仅以name和age进行模拟
*
* @author : wuwensheng
* @date : 9:41 2021/11/29
*/
@Getter
@Setter
@ToString
public class TestEvent extends ApplicationEvent {
private String name;

private int age;

private int id;

public TestEvent(Object source, String name, int age, int id) {
super(source);
this.name = name;
this.age = age;
this.id = id;
}
}

image.png

接下来马不停蹄地定义业务接口类,一个发送短信一个实现发送邮件。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
java复制代码/**
* 发送短信的抽象类
*
* @author : wuwensheng
* @date : 9:49 2021/11/29
*/
public interface SendSmsService {
/**
* 发送短信的方法,请传递用户的id
*
* @param id 用户id
*/
public void sendSms(int id);
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
java复制代码/**
* 为用户发送邮件的类
*
* @author : wuwensheng
* @date : 9:51 2021/11/29
*/
public interface SendEmailService {
/**
* 给用户发送邮件的方法,请传递用户id
*
* @param id 用户id
*/
public void sendEmail(int id);
}

接下来实现这两个业务,由观察者完成这件事情的最终处理

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.cmdc.event.TestEvent;
import com.cmdc.service.SendSmsService;
import lombok.extern.slf4j.Slf4j;
import org.springframework.context.ApplicationListener;
import org.springframework.stereotype.Component;

/**
* @author : wuwensheng
* @date : 9:52 2021/11/29
*/
@Component
@Slf4j
public class SmsListener implements SendSmsService, ApplicationListener<TestEvent> {
@Override
public void sendSms(int id) {
log.info("now send sms to id:{}", id);
}

@Override
public void onApplicationEvent(TestEvent testEvent) {
log.info("receive sms class receive testEvent:{}", testEvent);
int id = testEvent.getId();
sendSms(id);
}
}
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.cmdc.event.TestEvent;
import com.cmdc.service.SendEmailService;
import lombok.extern.slf4j.Slf4j;
import org.springframework.context.ApplicationListener;
import org.springframework.stereotype.Component;

/**
* @author : wuwensheng
* @date : 9:54 2021/11/29
*/
@Component
@Slf4j
public class EmailListener implements SendEmailService, ApplicationListener<TestEvent> {
@Override
public void sendEmail(int id) {
log.info("now send email to id:{}", id);
}

@Override
public void onApplicationEvent(TestEvent testEvent) {
log.info("send email class receive testEvent:{}", testEvent);
int id = testEvent.getId();
sendEmail(id);
}
}

实现这两个观察者的方式就是继承spring的ApplicationListener。要求去重写onApplicationEvent()方法!我们在onApplicationEvent()方法中去调用发短信或者发送邮件就可以了。

最终一步,在业务的合适位置发布此事件,使得观察者可以对发布的事件做出下一步的处理。我们就简单写个接口做个模拟就可以了,不整得特别麻烦。

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.cmdc.event.TestEvent;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.ApplicationContext;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;

/**
* 测试类
*
* @author : wuwensheng
* @date : 10:00 2021/11/29
*/
@RestController
@Slf4j
public class TestController {

@Autowired
private ApplicationContext applicationContext;

@GetMapping(value = "/test/event")
public String testEvent() {
TestEvent testEvent = new TestEvent(this, "小明", 20, 1);
applicationContext.publishEvent(testEvent);
return "successful";
}
}

那么现在我们postman调用下接口看看会发生什么?

image.png
ok,观察者成功接收到了事件并成功处理了事件!

解决事件的处理顺序问题

每次调用接口会发现,总是发送邮件随后才发送短信,这不太好,万一我就是想先发送短信随后才是邮件呢?下面给出改造的方案。
image.png

我们不再实现ApplicationListener,实现可以规定顺序的SmartApplicationListener!

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
java复制代码package com.cmdc.listener;

import com.cmdc.event.TestEvent;
import com.cmdc.service.SendSmsService;
import lombok.extern.slf4j.Slf4j;
import org.springframework.context.ApplicationEvent;
import org.springframework.context.event.SmartApplicationListener;
import org.springframework.stereotype.Component;

/**
* @author : wuwensheng
* @date : 9:52 2021/11/29
*/
@Component
@Slf4j
public class SmsListener implements SendSmsService, SmartApplicationListener {
@Override
public void sendSms(int id) {
log.info("now send sms to id:{}", id);
}

@Override
public void onApplicationEvent(ApplicationEvent applicationEvent) {
TestEvent testEvent = (TestEvent) applicationEvent;
log.info("receive sms class receive testEvent:{}", applicationEvent);
int id = testEvent.getId();
sendSms(id);
}

@Override
public boolean supportsEventType(Class<? extends ApplicationEvent> aClass) {
return aClass == TestEvent.class;
}

@Override
public boolean supportsSourceType(Class<?> aClass) {
return true;
}

@Override
public int getOrder() {
return 0;
}
}
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
java复制代码package com.cmdc.listener;

import com.cmdc.event.TestEvent;
import com.cmdc.service.SendEmailService;
import lombok.extern.slf4j.Slf4j;
import org.springframework.context.ApplicationEvent;
import org.springframework.context.event.SmartApplicationListener;
import org.springframework.stereotype.Component;

/**
* @author : wuwensheng
* @date : 9:54 2021/11/29
*/
@Component
@Slf4j
public class EmailListener implements SendEmailService, SmartApplicationListener {
@Override
public void sendEmail(int id) {
log.info("now send email to id:{}", id);
}

@Override
public void onApplicationEvent(ApplicationEvent applicationEvent) {
TestEvent testEvent = (TestEvent) applicationEvent;
log.info("send email class receive testEvent:{}", testEvent);
int id = testEvent.getId();
sendEmail(id);
}

@Override
public boolean supportsEventType(Class<? extends ApplicationEvent> aClass) {
return aClass == TestEvent.class;
}

@Override
public boolean supportsSourceType(Class<?> aClass) {
return true;
}

@Override
public int getOrder() {
return 1;
}
}

注意getOrder方法,这个方法就是规定同个事件在不同观察者中的发生顺序的!由于EmailListener定义的值为1,SmsListener定义的值是0。所以发送短信应该先于发送邮件发生!我们使用postman测试结果也确实是如此的!

image.png

解决异步使用观察者的问题

如果我们某个观察者抛出异常了,其余观察者还能正常执行吗?我们试下,在SmsListener中故意写个异常,看看EmailListener还能否正常处理事件!

image.png

非常遗憾,后续的发送邮件的流程并未成功执行。这显然是我们不希望看到的,其实理想的状态下,我们希望在一个接口涉及到的一组业务中,可以启用不同的线程去执行这一组业务。它们独立工作,互相之间互不干扰。在同一时间并发地去执行。这在应用编程中是非常重要的。下面咱们着手解决这个问题。

image.png

在接口层和事件处理的地方都打印下线程名称,很容易就能发现,执行这些的都是同一个线程。下面我们尝试使得执行事件的线程可以来自咱们的线程池,使得监听者执行事件可以独立于主线程之外!

那么需要做的事情有三件事情

  • 实现一个自定义的线程池(不要使用Executors来创建,至于为什么小可爱们自行百度),自定义线程池这里不做演示,这个很简单。
  • 启动类增加一个@EnableAsync注解告诉全事件当前上下文中支持异步的使用
  • 在需要异步执行的方法上增加@Async注解

image.png

image.png

好了,现在再来调用下看下效果。

image.png

ok,两个事件都成功异步执行了。按照一开始预期的,使用咱们自定义的线程池中的线程执行了。这非常重要,是应用层并发编程必须掌握的手法,假设监听者中做处理的代码非常耗时,这里的意义将不言而喻。还有一件事情需要注意哈,使用异步之后设置的order执行顺序就失效了。这应该不难解释,异步本来就是让大家像脱缰的野马一样各自跑各自的,再有顺序就自相矛盾了。

拥抱注解—注解方式实现观察者

上面实现观察者使用的还是实现ApplicationListener或者实现SmartApplicationListener的方式,其实代码还是有些罗嗦的。下面咱们使用注解来达到一致的效果。

image.png

新建的类的代码粘贴在下面

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
java复制代码package com.cmdc.listener.annotationlistener;

import com.cmdc.event.TestEvent;
import com.cmdc.service.SendEmailService;
import lombok.extern.slf4j.Slf4j;
import org.springframework.context.event.EventListener;
import org.springframework.core.annotation.Order;
import org.springframework.scheduling.annotation.Async;
import org.springframework.stereotype.Component;

/**
* @author : wuwensheng
* @date : 14:51 2021/11/30
*/
@Component
@Slf4j
public class EmailAnnotationListener implements SendEmailService{

@Async
@Order(0)
@EventListener(value = TestEvent.class)
public void disposeEvent(TestEvent event){
log.info("email ----- now running thread is:{}", Thread.currentThread().getName());
log.info("send email class receive testEvent:{}", event);
int id = event.getId();
sendEmail(id);
}

@Override
public void sendEmail(int id) {
log.info("now send email to id:{}", id);
}
}
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复制代码package com.cmdc.listener.annotationlistener;

import com.cmdc.event.TestEvent;
import com.cmdc.service.SendSmsService;
import lombok.extern.slf4j.Slf4j;
import org.springframework.context.event.EventListener;
import org.springframework.core.annotation.Order;
import org.springframework.scheduling.annotation.Async;
import org.springframework.stereotype.Component;

/**
* @author : wuwensheng
* @date : 14:51 2021/11/30
*/
@Slf4j
@Component
public class SmsAnnotationListener implements SendSmsService {
@Override
public void sendSms(int id) {
log.info("now send sms to id:{}", id);
}

@Async
@Order(1)
@EventListener(value = TestEvent.class)
public void disposeEvent(TestEvent event){
log.info("sms ----- now running thread is:{}", Thread.currentThread().getName());
log.info("send sms class receive testEvent:{}", event);
int id = event.getId();
sendSms(id);
}
}

那么期望能成功调用咱们自己的线程池。发下请求看一下!

image.png

ok的,没什么问题。我们的整合也就整个地完成了。本篇涉及的一些概念都非常的好,没有了解过的同学一定要理解下!

本文转载自: 掘金

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

论缓存的具体实现以及现有开源方案

发表于 2021-11-30

随着基于普通的SSM等类似项目体量逐渐变大时,项目的访问量急剧上升,此时对数据库的IO占用逐渐变大,同时数据库的庞大数据量检索速度和网络IO也是其中影响因素最大的一块
因此往往在这方面所采用的主要的手段有:

  • 分库分表
  • 读写分离
  • 索引优化
  • 缓存处理
    其中前两个手段都是注重于去提升数据库的性能,但是分库分表设计以及索引的命中率排查往往都需要一定技术成本以及时间,且其中网络IO问题没有根本性的解决。

所以考虑到技术成本以及收益问题,往往都在一定的数据库优化基础上使用缓存进行额外的网络IO优化,同时运用二级缓存(进程缓存与网络缓存)可以达到更高的缓存命中率与相较数据库提升手段的CPU占用更低(详见下)的效果,同时转移了大部分的查询请求,使往往在同等计算资源的情况下,使用缓存会比不使用缓存能处理数倍的查询请求,并提高了数据库的可用性(防止大量请求使数据库瘫痪),并且实施起来有着很多样的缓存框架,非常适用于初创公司等技术沉淀较少或人力资源紧张的公司:

  • J2Cache、Guava等缓存容器框架
  • SpringCache、AutoLoadCache等缓存代理框架
    其中分别解释为什么缓存能达到减少CPU计算成本与网络IO占用:
  • CPU计算成本:
    访问MySQL数据库时,受到可能存在的大量数据、数据库的索引命中情况和条件复杂度来说,容易导致处理速度差强人意而导致慢SQL的出现,且其中的检索原理基本都是检索设定好的索引与大量数据,所以会导致因数据数量变化而可能导致响应速度以某种程度降低(除非索引命中率非常高)。

而缓存的实现原理往往是通过相同的方法下,对比其中的参数,当缓存中不存在参数相同的值时,则访问数据库,反之获取获取缓存的值。简单的实现可以通过HashMap这样的KV数据结构实现简单的进程缓存,且网络缓存的实现也可以通过NoSQL数据库例如Redis、MongoDB这样现有的高性能中间件完成,而缓存的机制在面临大量的查询请求下则“越战越勇”,因为缓存上述机制自适应地提供缓存,而导致越密集的单一接口请求下,缓存的命中率越高,同时已存在的缓存并不会因其他缓存的增加而导致性能的下降或下降程度较低(因为KV结构),所以相较起来缓存能够提供的收益往往令人满意。

  • 网络IO:

无论是使用MySQL数据库又或者是NoSQL数据库例如Redis,即使处理时间无限小,而网络的IO速度也往往瓶颈之一。

对比CPU与IO的性能影响比重,能减少网络IO的影响往往是高性能所不可避免的。于是二级缓存设计在网络缓存作为主缓存的基础下,会增加类似Guava Cache这样的进程缓存以优化网络IO带来的影响,实现节点中的“自给自足”。

缓存与业务耦合问题
若使用缓存容器框架作为业务层的处理的话,则需要业务代码耦合缓存容器框架进行对应的业务操作,例:

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复制代码@Service
public class CacheServiceImpl implements CacheService {

@Autowrite
private Cache<Data> cache ;

@Autowrite
private Mapper<Data> mapper ;

public Data findById(long id) {
/*
为了保证原子性,这样处理会更好,但为了可读性更强,所以写出下述线程不安全写法,实际生产环境请按照注释这里这块代码为准
Data data = cache.computeIfAbsent(id,k -> {
this.mapper.selectById(id) ;
}) ;
*/
Data data = cache.get(id) ;
if(data == null) {
data = this.mapper.selectById(id) ;
cache.put(data) ;
}
return data ;
}

}

这样导致的问题首先导致了业务代码不够清晰,影响了可读性,若是缓存逻辑有进一步更新则会导致需要修改大量的代码。

此时可以通过AOP进行解耦,或者直接应用SpringCache和AutoLoadCache进行基于AOP层面实现的缓存框架,进一步降低了业务代码与缓存组件的耦合。例:引入SpringCache

1
2
3
4
5
6
7
8
9
10
11
12
13
java复制代码@Service
public class CacheServiceImpl implements CacheService {

@Autowrite
private Mapper<Data> mapper ;

@Override
@Cacheable({"menu", "menuById"})
public Data findById(long id) {
return this.mapper.selectById(id) ; ;
}

}

但是这样的处理在解决了耦合代码的情况下仍然会面临着一些问题:

  • 缓存高并发场景问题

数据库中有五个条数据,除了主键id分别为1~5,而性别:男,年龄:20,职业:程序员等三个属性都一致的情况下:

此时条件搜索(年龄为20):结果返回这五条数据

随后条件搜索(性别为男):结果返回这五条数据

  • 空间利用率较低:往往缓存的数据结构中,缓存结构往往为KV,即参数为键值,值为该次方法返回的结果。这样的数据结构在面临不同参数下同样的数据也会被分为不同的缓存存储起来(类上述场景)
    在大量缓存的情况下导致缓存的空间占用率和冗余的数据非常高。

此时id为1的数据性别修改为女

随后条件搜索(性别为男):结果返回这id:2~4的数据

  • 缓存维护成本:数据增删改后,旧缓存不再适用于这样的条件,所以需要将这条参数的缓存删除或者修改,但是若此时若存在大量的缓存,且缓存存储与Redis时往往都会进行一次序列化,使Redis中的数据解析并修改往往是一个很大成本的一个问题,所以通过key删除对应的缓存进行是大多数人的选择,其中SpingCache的@CacheEvict则是类似的处理。
    但是这样的缓存处理方式会使在数据增删改的成本较高,同时万一含有某些复用率极高数据的缓存,可能会导致删除大量缓存。

针对上述两种问题,以上所述缓存代理框架貌似都没有一个较好的处理方式,此处笔者偶然想到一种较为可行的方式:

缓存散列化/索引化:

分析实际场景中的持久类数据往往为PO的集合,为了使数据原子化

将PO集合拆分为各个PO独立存储在缓存之中,并以id为其key值,使id查询时可以通过id查询

并且针对于可能会返回的单条或多条的PO数据集的条件查询,可以将其参数和其中数据集的各个id替换原本的数据,将单条数据(元数据)拆分出来分别存储于Redis之中(可以通过Lua脚本进行Redis的批量操作IO优化)

缓存结构转变为参数-》id集-》多条元数据,使用Lua脚本进行Redis内实现通过解析对应参数Key获取的id集并收集其元数据组合数据集并返回,优化批量操作的IO成本

散列化后缓存结构:
image.png

此时针对于上述的问题:

空间利用率:散列化的元数据唯一且符合原子性,各个条件查询通过索引对id集采集对应元数据,避免了大量重复元数据的数据冗余。

缓存维护成本:条件查询使用索引进行对应数据的缓存建立(仅当该索引的id集无对应元数据的缓存时携带元数据进行缓存),减少了缓存重复序列化带来的性能损耗。同时缓存改变时仅删除索引,不影响元数据(若删除具体数据则删除索引和此条元数据)

而笔者也按此逻辑实践并开源了缓存代理框架Kache,解决了以上缓存代理框架所述的这两块痛点。

仓库链接:gitee.com/Kould/kache

有意见或者建议欢迎一起和平地讨论学习~

本文转载自: 掘金

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

设计模式之适配器模式

发表于 2021-11-30

「这是我参与11月更文挑战的第30天,活动详情查看:2021最后一次更文挑战」

本篇文章是设计模式专题的第八篇文章,我会将遇到的设计模式都一一总结在该专题下,我会把自己对每一种设计模式的感悟写下来,以及在实际工作中我们该如何去灵活应用这些设计模式,欢迎大家关注。本篇文章我们就来讲一讲适配器模式。

适配器模式的简单介绍

适配器模式是为解决两个对象因接口不兼容而无法一起工作而诞生的。

就像生活中的转接头,将本来不能使用的接口进行转接处理,就可以使用了。

适配器模式的类图:

image.png

适配器模式的各个角色(我们以typeC接口手机使用3.5mm耳机为例):

  • 目标对象(Target) 目标对象就是系统所需要的的对象,这里我们可以看做是typeC接口
  • 适配器(Adapter) 适配器相当于转接线
  • 适配者(Adaptee) 适配者相当于3.5mm耳机接口

适配器模式扩展,双向适配

image.png

适配器模式的具体实现思路

  • 创建目标接口
  • 创建需要适配的接口
  • 创建实现目标接口的适配器

适配器模式的具体实现方案

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
csharp复制代码//目标接口
public interface Target {
   public void request();
}
//适配者接口
public class Adaptee {
   
   public void specificRequest(){      
// 适配者的处理
  }
}
//类适配器类
public class Adapter implements Target {
   
   private Adaptee adaptee;
   
   public Adapter(Adaptee adaptee){
       this.adaptee = adaptee;
  }
   
   public void request(){
       this.adaptee.specificRequest();
  }
}

适配器模式的优缺点

优点

  • 可以有效的复用现有的类,开发者不需要修改原有代码只需要通过适配器兼容即可。
  • 客户端通过适配器可以透明地调用目标接口。
  • 将目标类和适配者类解耦,解决了目标类和适配者类接口不一致的问题。
  • 符合开闭原则。
  • 灵活性高。

缺点

  • 适配器需要结合业务场景全面考虑,可能会增加系统的复杂性。
  • 降低代码可读性,过多使用适配器会使代码变得很乱。

适配器模式的适用场景

  1. 以前开发的系统存在满足新系统功能需求的类,但其接口同新系统的接口不一致。
  2. 使用第三方提供的组件,但其组件接口定义和自己需要的接口定义不同。

适配器模式总结

适配器模式最大的优点就是代码的复用,满足业务逻辑的代码有一份就可以了,我们只需要通过适配器就能让其满足其它系统的调用。但是正由于适配器的这一大特性,如果过多的使用适配器去满足不同接口,就会使我们的代码变得混乱,不易阅读。大家使用适配器模式还是要慎重,小用宜情,大用伤身啊。

本文转载自: 掘金

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

创新模式驱动生产力提升,融云社交场景化 SDK 探索

发表于 2021-11-30

农具的大量使用把人类从狩猎采集社会带到了农业社会;蒸汽机的创新和广泛应用,推动了第一次工业革命,带来了工业经济的蓬勃发展;电子信息技术的发展又驱动人类进入信息社会。【融云全球互联网通信云】

大到一个经济时代,小到一个行业周期,生产工具的每次创新升级,势必带来生产力的显著提升。

在 WICC 广州的“社交分论坛”上,融云场景化研发负责人臧其龙带来《融云社交场景化 SDK 探索》主题演讲,分享融云在通信云服务方面的创新探索。融云第三代场景化 SDK 的服务模式创新将给开发者提供全新的生产工具,带来生产效率的极大提升,也必将重塑产业格局。

图片

(臧其龙在 WICC 广州社交分论坛演讲)

学习成本日渐高企,PaaS 服务新痛点

回溯十年前,想要自研一款即时通讯应用还是比较困难的事情,要自建服务器,研究分发和到达率。随着通信云 PaaS 服务的逐渐完善,借助融云等平台的服务,搭建这样一款应用变得不再是“不可能的挑战”。

但是,随着时间的推进,市场需求不断演变。我们会发现,单一场景需求越来越少,更多的时候,我们要面对的是场景的融合。

以语聊房这个时下语音社交热门场景为例来说,这类产品主要有两部分组成:一是麦位管理部分,用户上麦后角色发生变化,从观众切换成主播,可以发布音频流被观众和其他主播听到。二是聊天室部分,也就是公屏消息的部分,房间内所有成员都可以发送文字在公屏区域沟通。

实现这两个部分,就要同时接入 RTC 和 IM,也就是融合场景。这种融合需求也出现在直播等等场景中。随之而来,棘手的问题出现了。

问题并非出于服务能力不足,反而是大部分行业供给都已经非常强大造成的。一个 SDK,基本上有 200+ 甚至 300+个 API。当开发者接触到一个功能强大的 SDK,首先面对的难题是学习成本特别高,其次是它的学习曲线也比较陡峭。

比如 RTC 会涉及到很多非常专业的音视频领域知识,要了解流的定义与发布,学习一些编码知识,掌握了基础知识后,才能让 SDK 发挥应有的作用。

服务模式演进,第三代 SDK 新范式

为了解决上面说到的痛点问题,融云推出了第三代 SDK 服务模式,一举破解旧供给的高学习成本难题。

以语聊房场景为例,我们可以更加直观地感受三代服务模式的升级核心。

语聊房产品的核心是麦位管理,语聊房解决方案,就是通过上麦、下麦等一系列麦位管理来对用户和流进行同步管理的 SDK。

第一代解决方案,使用业务服务器管理每个房间的麦位。前端只负责调用后端的接口,后端管理麦位,不单要更改麦位,还要负责整个状态的刷新和发布,非常复杂。

第二代解决方案,也就是目前其他厂商普遍使用的方式。把管理麦位的能力放在 IM SDK 里,通常是聊天室属性,拥有麦位的增、删、改、查同步能力。服务模式是,开发者下载 Demo,自行研究实现逻辑,再利用开源代码二开实现。也就是说,开发者还是需要理解厂商提供的开源代码,上手难度比较高。

第三代解决方案,也就是融云行业首推的 SDK。开发者无需研究代码,也不需要单独集成 IM 和 RTC,只需要对这个产品有了解,就可以调用接口实现应用。而且这个接口数量只有不超过 20 个。

图片6.png

(三代服务模式演进)

利用第一代解决方案,最常出现的问题就是幽灵麦,上麦的时候发不出声,下麦又能发出声音了。这是因为,前端代码和后端代码需要严丝合缝实现同步,前后端的协同和更新很容易出差错,造成 2-3 秒的延迟,这给产品体验带来的打击是毁灭性的。

第二代解决方案,只需要关注前端开源代码,但是也要面临残酷的现实问题。首先,原封不动上架产品面对很大的审核被拒风险;其次,新增功能需要学习底层机制再改代码,难度大,易出 Bug。

融云的第三代 SDK 解决方案,学习难度非常低,只需要对基础的上麦、下麦、锁麦等有了解,甚至根据 20 个 API 的注释就能成功调用。无需理解底层代码,无需研究实现逻辑,无需管理流的订阅,极大提升开发速度,7 天就能上线一个语聊房。

第三代 SDK 的API 设计三大原则

贴近业务: 接口设计不能云里雾里令人不解。

以最常见的三个功能为例,enterSeat(index: Int) 接口,index 设置为麦的序号,就完成了这一麦位上角色转换、流的订阅、UI 的同步和刷新等一系列操作。muteSeat(index: Int) 接口,Mute 是静音,Seat 是某个麦位,后面会带一个麦位的序号,可以关闭某个麦位上的声音;kickUserFromSeat(userId: String) 接口就可以把某个用户踢下麦。都说细节是魔鬼,第三代 SDK 可以说是已经把魔鬼封在黑盒中了,开发者可以无忧开发。

可扩展性: 语聊覆盖的场景非常多,比如非常火的狼人杀业务,需要麦位体现特殊身份——平民、法官、狼人,接口设计得足够可拓展,就可以覆盖所有热门场景,也方便开发者去做不同业务的尝试。

简洁易用: 语聊房 SDK 核心接口只有 20 个,大部分场景只需要其中 10 个基本上就可以实现业务。核心功能回调只有 23 个,对于不太关注性能或不需要兼容低端手机的业务,开发者只需关心麦位信息和房间信息的变更两个回调就可以。

更多场景 Coming Soon

语聊房、呼叫场景之外,融云近期上线了直播 SDK,预制了 9 种合流布局,覆盖所有的直播合流场景。

直播场景通常用户感知最强烈的就是两个步骤,唤起摄像头做直播前美颜等准备 ➡ 开始直播。

融云直播 SDK 把这两步封装成 API,第一步是 Prepare,封装了融云开源的 BeautyKit 美颜等能力;第二步是 Live Video,把所有直播流程实现逻辑隐藏掉,开发者只需要调用接口就可以实现业务。

接下来,融云还会把会议、教育等场景进行完整封装提供给开发者,帮开发者一一攻克场景难关。

同时,在 SDK 组成的“骨骼”、“肌肉”之外,融云还将开源一系列含 UI 体系的 Kit,作为配套使用的“皮肤”。比如,ChatKit、GiftKit、BeautyKit、MusicControlKit 等等。搭配开发者可在后台一键配置的“内容审核”能力,真正为开发者提供一站式的完整解决方案服务。

本文转载自: 掘金

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

新能源汽车太猛了,这些卡脖子技术你了解吗?

发表于 2021-11-30

简介:从汽车行业的变化,我们即可初步看出芯片的重要性,那么,芯片对汽车行业的发展具体有哪些重要影响呢?

根据全球汽车咨询机构Auto Forecast Solutions统计的数据,截至10月10日,由于芯片短缺,全球汽车市场已累计减产934.5万辆,比前一周增加了约25万辆,其中中国市场减产1.3万辆汽车,约占5.2%左右。从数据可以看出,芯片作为汽车系统的核心技术,市场的供应不足已经影响到汽车行业的销量。

从汽车行业的变化,我们即可初步看出芯片的重要性,那么,芯片对汽车行业的发展具体有哪些重要影响呢?

首先,相比消费电子类芯片,在使用寿命、工作环境及规格标准方面对汽车芯片又更高要求。芯片的开发周期时间长,难度大,且需要通过最严苛的行业自知认证。

其次,汽车系统中主要应用的是车规级芯片将芯片按功能划分,车规级芯片可分为功能型芯片、功率半导体和传感器芯片三大类。其中,功率半导体包括IGBT、场效应晶体管(MOSFET)和电源管理芯片,在汽油车中占比约40%左右,在新能源汽车中占比为50%,是目前最紧缺同时最具增长潜力的芯片。其中,IGBT作为新能源车主导型功率器件,被誉为是电力电子行业的CPU,约占整车成本的7%-10%,仅次于锂电池,能够根据信号指令来调节电路的电压、电流、频率和相位,以实现精准调控的目的,可以简单地理解为IGBT是电力电子行业理想的开关器件。

对新能源车来说,IGBT优异的开关特性可以实现交直流转换、电压转换和频率转换几个核心功能,当电动车充电时,通过 IGBT将外部电源转变成直流电,在电动车刹车的时候,通过 IGBT 把直流电转变成交流电机使用的交流电,同时精确调整电压和频率,驱动电动车行驶。一台电车的加速能力、最高时速、能源效率主要看车规级功率器件的性能,所以IGBT被广泛应用于电控、车载空调、充电桩逆变器三个子系统当中。

智能化发展迅速,芯片是重中之重,可谓掌握精深的芯片技术就是掌握科技领域的话语权。故不仅仅是在汽车行业,任何智能化设备(例如智能手机、智能家具、智能安防等)都离不开芯片的作用,芯片开发也成为人们日益关心的焦点话题。在今年的云栖大会上,阿里巴巴正式发布自研云芯片倚天710,这是阿里云推进“一云多芯”策略的重要一步,也是阿里第一颗为云而生的CPU芯片,将在阿里云数据中心部署应用。由此看来,芯片开发的未来更加值得期待!

关键技术的进步是无数企业无限追求的星辰大海。

科技进步离不开底层关键技术的发展,如何使关键技术具有更优性能是科技企业无限追求的方向。SaaS系统(软件及服务)作为新时代互联网技术发展和应用的关键技术,是21世纪开始兴起的一种完全创新的软件应用模式。其定义了一种新的交付方式,使得软件进一步回归服务本质。SaaS改变了传统软件服务的提供方式,减少了本地部署所需的大量前期投入,进一步突出信息化软件的服务属性。

一个成功的网站考验的是其稳定性、安全性及快速性,而这一切的基础依赖于其底层应用的基础设施。【阿里云建站】是基于云计算的标准化在线交付SaaS系统,集成阿里云全套云计算资源,标配阿里云服务器,无需额外付费即可直接使用阿里云服务;提供阿里云自研的云盾安全服务,最大化的对网站进行防护,有效抵御恶意攻击,确保网站为用户提供服务;标配全球CDN网络加速服务,确保网站快速打开。全方位保障了客户建网站的快速、稳定、安全需求。

原文链接

本文为阿里云原创内容,未经允许不得转载。

本文转载自: 掘金

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

1…103104105…956

开发者博客

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