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

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


  • 首页

  • 归档

  • 搜索

SpringBoot 生成接口文档,我用smart-doc,

发表于 2021-09-29

之前我在SpringBoot老鸟系列中专门花了大量的篇幅详细介绍如何集成Swagger,以及如何对Swagger进行扩展让其支持接口参数分组功能。详情可见:SpringBoot 如何生成接口文档,老鸟们都这么玩的!

可是当我接触到另一个接口文档工具 smart-doc后,我觉得它比Swagger更适合集成在项目中,更适合老鸟们。今天我们就来介绍一下smart-doc组件的使用,作为对老鸟系列文章的一个补充。

swagger vs smart-doc

首先我们先看一下Swagger组件目前存在的主要问题:

  1. Swagger的代码侵入性比较强

这个很容易理解,要让Swagger生成接口文档必须要给方法或字段添加对应的注解,是存在代码侵入的。
2. 原生swagger不支持接口的参数分组

对于有做参数分组的接口,原生的Swagger并未支持,虽然我们通过扩展其组件可以让其支持参数分组,但是毕竟要开发,而且还未支持最新的Swagger3版本。

那作为对比,smart-doc 是基于接口源码分析来生成接口文档,完全做到零注解侵入,你只需要按照java标准注释的写,smart-doc就能帮你生成一个简易明了的markdown 或是一个像GitBook样式的静态html文档。官方地址:https://gitee.com/smart-doc-team/smart-doc

简单罗列一下smart-doc的优点:

  • 零注解、零学习成本、只需要写标准java注释即可生成文档。
  • 基于源代码接口定义自动推导,强大的返回结构推导。
  • 支持Spring MVC,Spring Boot,Spring Boot Web Flux(controller书写方式)。
  • 支持Callable,Future,CompletableFuture等异步接口返回的推导。
  • 支持JavaBean上的JSR303参数校验规范,支持参数分组。
  • 对一些常用字段定义能够生成有效的模拟值。
  • …

smartdoc.gif

接下来我们来看看SpringBoot中如何集成smart-doc。

SpringBoot集成 smart-doc

smart-doc支持多种方式生成接口文档:maven插件、gradle插件、单元测试(不推荐),这里我才用的是基于maven插件生成,步骤如下:

  1. 引入依赖,版本选择最新版本
1
2
3
4
5
6
7
8
9
10
xml复制代码<!--引入smart-doc-->
<plugin>
<groupId>com.github.shalousun</groupId>
<artifactId>smart-doc-maven-plugin</artifactId>
<version>2.2.7</version>
<configuration>
<configFile>./src/main/resources/smart-doc.json</configFile>
<projectName>Smart-Doc初体验</projectName>
</configuration>
</plugin>

重点在 configFile中指定smart-doc配置文件 smart-doc.json

  1. 新建配置文件smart-doc.json
1
2
3
json复制代码{
"outPath": "src/main/resources/static/doc"
}

指定smart-doc生成的文档路径,其他配置项可以参考官方wiki。

  1. 通过执行maven 命令生成对应的接口文档
1
2
ini复制代码//生成html
mvn -Dfile.encoding=UTF-8 smart-doc:html

当然也可以通过idea中的maven插件生成

图片
4. 访问接口文档

生成接口文档后我们通过 http://localhost:8080/doc/api.html查看,效果如下:

图片
看到这里的同学可能会呵呵一笑,就这?啥也没有呀!这还想让我替换Swagger?

图片别急,刚刚只是体验了smart-doc的基础功能,接下来我们通过丰富smart-doc的配置文件内容来增强其功能。

功能增强

1. 开启调试

一个优秀的接口文档工具调试功能必不能少,smart-doc支持在线调试功能,只需要加入如下几个配置项:

1
2
3
4
5
6
7
8
lua复制代码{
"serverUrl": "http://localhost:8080", -- 服务器地址
"allInOne": true, -- 是否将文档合并到一个文件中,一般推荐为true
"outPath": "src/main/resources/static/doc", -- 指定文档的输出路径
"createDebugPage": true, -- 开启测试
"allInOneDocFileName":"index.html", -- 自定义文档名称
"projectName": "初识smart-doc" -- 项目名称
}

通过”createDebugPage”: true 开启debug功能,在让生成smart-doc生成文档时直接放入到 static/doc/下,这样可以直接启动程序访问页面 http://localhost:8080/doc/index.html进行开发调试。

图片

有的开发人员直接在idea中使用【Open In Browser】打开smart-doc生成的debug页面,如果非要这做,前端js请求后台接口时就出现了跨域。因此你需要在后端配置跨域。

这里以 SpringBoot2.3.x 为例配置后端跨域:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
java复制代码@Configuration
public class WebMvcAutoConfig implements WebMvcConfigurer {

@Bean
public CorsFilter corsFilter() {
final UrlBasedCorsConfigurationSource urlBasedCorsConfigurationSource = new UrlBasedCorsConfigurationSource();
final CorsConfiguration corsConfiguration = new CorsConfiguration();
/* 是否允许请求带有验证信息 */
corsConfiguration.setAllowCredentials(true);
/* 允许访问的客户端域名 */
corsConfiguration.addAllowedOrigin("*");
/* 允许服务端访问的客户端请求头 */
corsConfiguration.addAllowedHeader("*");
/* 允许访问的方法名,GET POST等 */
corsConfiguration.addAllowedMethod("*");
urlBasedCorsConfigurationSource.registerCorsConfiguration("/**", corsConfiguration);
return new CorsFilter(urlBasedCorsConfigurationSource);
}
}

开启跨域后我们就可以直接在静态接口页面中进行调试了。

2. 通用响应体

在 “SpringBoot 如何统一后端返回格式?老鸟们都是这样玩的!”一文中我们通过实现 ResponseBodyAdvice对所有返回值进行了包装,给前端返回统一的数据结构ResultData,我们需要让其在接口文档中也有此功能,在配置文件追加配置内容:

1
2
3
4
5
lua复制代码{
"responseBodyAdvice":{ -- 通用响应体
"className":"com.jianzh5.blog.base.ResultData"
}
}

图片

3. 自定义Header

在前后端分离项目中我们一般需要在请求接口时设置一个请求头,如token,Authorization等…后端根据请求头判断是否为系统合法用户,目前smart-doc也对其提供了支持。

在smart-doc配置文件 smart-doc.json中继续追加如下配置内容:

1
2
3
4
5
6
7
8
9
10
11
12
ruby复制代码"requestHeaders": [ //设置请求头,没有需求可以不设置
{
"name": "token",//请求头名称
"type": "string",//请求头类型
"desc": "自定义请求头 - token",//请求头描述信息
"value":"123456",//不设置默认null
"required": false,//是否必须
"since": "-",//什么版本添加的改请求头
"pathPatterns": "/smart/say",//只有以/smart/say 开头的url才会有此请求头
"excludePathPatterns":"/smart/add,/smart/edit" // url=/app/page/将不会有该请求头
}
]

效果如下:

图片

4. 参数分组

演示一下smart-doc对于参数分组的支持

图片
新增操作时,age、level为必填项,sex为非必填。

图片
编辑操作时,id,appid,leven为必填项,sex为非必填。

通过上面的效果可以看出smart-doc对于参数分组是完全支持的。

5. idea配置doc

自定义的tag默认是不会自动提示的,需要用户在idea中进行设置。设置好后即可使用,下面以设置smart-doc自定义的mock tag为例,设置操作如下:

图片### 6. 完整配置

附上完整配置,如果还需要其他配置大家可以参考wiki自行引入。

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
json复制代码{
"serverUrl": "http://localhost:8080",
"allInOne": true,
"outPath": "src/main/resources/static/doc",
"createDebugPage": true,
"allInOneDocFileName":"index.html",
"projectName": "初识smart-doc",
"packageFilters": "com.jianzh5.blog.smartdoc.*",
"errorCodeDictionaries": [{
"title": "title",
"enumClassName": "com.jianzh5.blog.base.ReturnCode",
"codeField": "code",
"descField": "message"
}],
"responseBodyAdvice":{
"className":"com.jianzh5.blog.base.ResultData"
},
"requestHeaders": [{
"name": "token",
"type": "string",
"desc": "自定义请求头 - token",
"value":"123456",
"required": false,
"since": "-",
"pathPatterns": "/smart/say",
"excludePathPatterns":"/smart/add,/smart/edit"
}]
}

小结

其实没什么可总结的,smart-doc使用非常简单,官方文档也非常详细,只要你会写标准的java注释就可以给你生成详细的接口文档。(如果你说你不会写注释,那这篇文章可能不太适合你) 而且在引入smart-doc后还可以强制要求开发人员给接口编写注释,保证团队代码风格不会出现很大差异。

老鸟系列源码已经上传至GitHub,需要的在公号【JAVA日知录】回复关键字 0923 获取

本文转载自: 掘金

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

Java基础:获取操作系统及浏览器信息

发表于 2021-09-29

小知识,大挑战!本文正在参与「程序员必备小知识」创作活动

本文已参与 「掘力星计划」 ,赢取创作大礼包,挑战创作激励金。

📖摘要


1
复制代码心态好了,就没那么累了。心情好了,所见皆是明媚风景。

“一时解决不了的问题,那就利用这个契机,看清自己的局限性,对自己进行一场拨乱反正。”正如老话所说,一念放下,万般自在。如果你正被烦心事扰乱心神,不妨学会断舍离。断掉胡思乱想,社区垃圾情绪,离开负面能量。心态好了,就没那么累了。心情好了,所见皆是明媚风景。

在生产环境下,我们需要关闭swagger配置,避免暴露接口的这种危险行为。


User Agent 的含义

User Agent 中文名为用户代理,简称 UA,它是一个特殊字符串头,使得服务器能够识别客户使用的操作系统及版本、CPU 类型、浏览器及版本、浏览器渲染引擎、浏览器语言、浏览器插件等。

一些网站常常通过判断 UA 来给不同的操作系统、不同的浏览器发送不同的页面,因此可能造成某些页面无法在某个浏览器中正常显示,但通过伪装 UA 可以绕过检测。


浏览器的 UA 字串

标准格式为: 浏览器标识 (操作系统标识; 加密等级标识; 浏览器语言) 渲染引擎标识 版本信息

浏览器标识

由于很多网站在进行 UA 检测的时候忽略了两位数版本号,所以可能造成 浏览器及之后版本收到糟糕的页面,因此自 浏览器 10 之后的版本中浏览器标识项固定为 浏览器,在 UA 字串尾部添加真实版本信息。

注:源自百度百科


UserAgentUtils.jar

UserAgentUtils.jar 是 UserAgent 的工具类。

maven如下:

1
2
3
4
5
6
java复制代码<!-- https://mvnrepository.com/artifact/eu.bitwalker/UserAgentUtils --> 
<dependency>
<groupId>eu.bitwalker</groupId>
<artifactId>UserAgentUtils</artifactId>
<version>1.20</version>
</dependency>

java代码如下:

1
2
3
java复制代码UserAgent userAgent = UserAgent.parseUserAgentString(request.getHeader("User-Agent"));  
Browser browser = userAgent.getBrowser();
OperatingSystem os = userAgent.getOperatingSystem();

方法

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

import javax.servlet.http.HttpServletRequest;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.RestController;

import eu.bitwalker.useragentutils.Browser;
import eu.bitwalker.useragentutils.OperatingSystem;
import eu.bitwalker.useragentutils.UserAgent;

/**
*
* @Description: 获取ip控制器
* @ClassName: IpController.java
* @author ChenYongJia
* @Date 2019年4月20日 晚上20:25
* @Email chen87647213@163.com
*/
@RestController
public class IpController {

private static final Logger log = LoggerFactory.getLogger(IpController.class);

/**
* 获取操作系统及浏览器信息
* @param request
* @return
*/
@RequestMapping(value="/browser",method = RequestMethod.GET)
public void getBrowser(HttpServletRequest request){
String ua = request.getHeader("User-Agent");
log.info("******************************");
log.info("操作系统及浏览器信息:"+ua);
//转成UserAgent对象
UserAgent userAgent = UserAgent.parseUserAgentString(ua);
//获取浏览器信息
Browser browser = userAgent.getBrowser();
log.info("浏览器信息:"+browser);
//获取系统信息
OperatingSystem os = userAgent.getOperatingSystem();
log.info("系统信息:"+os);
//系统名称
String system = os.getName();
log.info("系统名称:"+system);
//浏览器名称
String browserName = browser.getName();
log.info("浏览器名称:"+browserName);
log.info("******************************");

}

}

​

结果如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
java复制代码**************火狐****************
操作系统及浏览器信息:Mozilla/5.0 (Windows NT 6.1; Win64; x64; rv:56.0) Gecko/20100101 Firefox/56.0
浏览器信息:FIREFOX
系统信息:WINDOWS_7
系统名称:Windows 7
浏览器名称:Firefox
******************************

**************谷歌****************
操作系统及浏览器信息:Mozilla/5.0 (Windows NT 6.1; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/63.0.3239.108 Safari/537.36
浏览器信息:CHROME
系统信息:WINDOWS_7
系统名称:Windows 7
浏览器名称:Chrome
******************************

************IE******************
操作系统及浏览器信息:Mozilla/5.0 (Windows NT 6.1; WOW64; Trident/7.0; rv:11.0) like Gecko
浏览器信息:MOZILLA
系统信息:WINDOWS_7
系统名称:Windows 7
浏览器名称:Mozilla

******************************

通过(User-Agent)获取 浏览器类型,操作系统类型,手机机型

获得浏览器请求头中的 User-Agent

1
java复制代码String ua = request.getHeader("User-Agent")

获得浏览器类型,操作系统类型,请看上面的 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
java复制代码package com.cyj.controller;

import java.util.regex.Matcher;
import java.util.regex.Pattern;

import javax.servlet.http.HttpServletRequest;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.RestController;

import eu.bitwalker.useragentutils.Browser;
import eu.bitwalker.useragentutils.OperatingSystem;
import eu.bitwalker.useragentutils.UserAgent;

/**
*
* @Description: 获取ip控制器
* @ClassName: IpController.java
* @author ChenYongJia
* @Date 2019年4月20日 晚上20:25
* @Email chen87647213@163.com
*/
@RestController
public class IpController {

private static final Logger log = LoggerFactory.getLogger(IpController.class);

/**
* 获取操作系统及浏览器信息
* @param request
* @return
*/
@RequestMapping(value="/browser",method = RequestMethod.GET)
public void getBrowser(HttpServletRequest request){
UserAgent userAgent = UserAgent.parseUserAgentString(request.getHeader("User-Agent"));
Browser browser = userAgent.getBrowser();
OperatingSystem os = userAgent.getOperatingSystem();

Pattern pattern = Pattern.compile(";\\s?(\\S*?\\s?\\S*?)\\s?(Build)?/");
Matcher matcher = pattern.matcher((CharSequence) userAgent);
String model = null;
if (matcher.find()) {
model = matcher.group(1).trim();
log.debug("通过userAgent解析出机型:" + model);
}

}

}

​


再给出一个:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
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
java复制代码 package com.cyj.controller;

import java.io.IOException;

import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.RestController;

import eu.bitwalker.useragentutils.Browser;
import eu.bitwalker.useragentutils.OperatingSystem;
import eu.bitwalker.useragentutils.UserAgent;
import eu.bitwalker.useragentutils.Version;

/**
*
* @Description: 获取ip控制器
* @ClassName: IpController.java
* @author ChenYongJia
* @Date 2019年4月20日 晚上20:25
* @Email chen87647213@163.com
*/
@RestController
public class IpController {

private static final Logger log = LoggerFactory.getLogger(IpController.class);

/**
* 获取操作系统及浏览器信息
*
* @param request
* @return
*/
@RequestMapping(value = "/browser", method = RequestMethod.GET)
public void service(HttpServletRequest request, HttpServletResponse response)
throws ServletException, IOException {
String agentStr = request.getHeader("user-agent");
System.out.println(agentStr);
UserAgent agent = UserAgent.parseUserAgentString(agentStr);
// 浏览器
Browser browser = agent.getBrowser();
System.out.println("类型:" + browser.getBrowserType() + "\n名称:" + browser.getName() + "\n厂商:"
+ browser.getManufacturer() + "\n产品系列:" + browser.getGroup() + "\n引擎:" + browser.getRenderingEngine());

// 浏览器版本
Version version = agent.getBrowserVersion();
System.out.println("========================");
System.out.println("主版本:" + version.getMajorVersion() + "\n小版本:" + version.getMinorVersion() + "\n完整版本:"
+ version.getVersion());
// 操作系统
System.out.println("========================");
OperatingSystem os = agent.getOperatingSystem();
System.out.println("名称:" + os.getName() + "\n设备类型:" + os.getDeviceType() + "\n产品系列:" + os.getGroup() + "\n生成厂商:"
+ os.getManufacturer());
}

}

各位小伙伴自行测试结果吧!!!

最后感谢大家耐心观看完毕,原创不易,留个点赞收藏是您对我最大的鼓励!


🎉总结:

  • 更多参考精彩博文请看这里:《陈永佳的博客》
  • 喜欢博主的小伙伴可以加个关注、点个赞哦,持续更新嘿嘿!

本文转载自: 掘金

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

springcloud alibaba企业落地实战:小企业适

发表于 2021-09-29

小知识,大挑战!本文正在参与“程序员必备小知识”创作活动。

服务间调用是微服务体系中必不可少的一部分,在springcloud alibaba的官网中,推荐使用是dubbo,但因为并发量比较小,同时dubbo相对比较复杂,所有楼主在服务间调用选型时,依然选择了openfeign。同时将服务间调用的接口封装为子工程,进行统一的api管理。避免小伙伴们造重复的轮子。

1.新建api子工程

1.搭建子工程

这里新建了一个子工程,文件结构如下。

2.添加pom.xml

1
2
3
4
5
xml复制代码        <dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-openfeign</artifactId>
<version>2.2.6.RELEASE</version>
</dependency>

3.实现fallback熔断方法

当服务调用失败,会跳转到该类。后文会与Sentinel一起使用。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
ini复制代码@Component
public class UserFallBack implements UserApi {

@Override
public Result<UserVO> getCurrentUser() {
Result<UserVO> result = new Result<>();
String error = "调用system获取当前用户信息失败!";
result.setCode(506);
result.setMessage(error);
return result;
}

@Override
public Result<UserVO> getUser(UserVO UserVO) {
Result<UserVO> result = new Result<>();
String error = "调用system获取输入用户信息失败!";
result.setCode(506);
result.setMessage(error);
return result;
}
}

4.feign接口实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
less复制代码@FeignClient(name = "system", fallback = UserFallBack.class) //其中name为nacos中的服务名
public interface UserApi {

/**
* 获取当前用户信息
*
* @return
*/
@GetMapping("/getCurrentUser")
Result<UserVO> getCurrentUser();

/**
* 通过实体获取用户信息
*
* @return
*/
@GetMapping("/getUser")
Result<UserVO> getUser(@SpringQueryMap UserVO UserVO);
}

这里需要注意,如果在get请求时,同时boby中有值,feign会把get转换成post,所以这种情况需要使用@SpringQueryMap注解。

2.父工程调用

1.修改pom.xml

1.在pom文件中引入子工程 ,详细配置可以参考楼主文章:

1
2
3
4
5
6
7
8
9
10
xml复制代码    <dependencies>
<dependency>
<groupId>com.vanpeng</groupId>
<artifactId>common-api</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-openfeign</artifactId>
</dependency>
</dependencies>

2.修改启动类

1
2
3
4
5
6
7
8
9
10
less复制代码@EnableDiscoveryClient
@SpringBootApplication
@EnableFeignClients("com.common.api")
public class DatacenterApplication {

public static void main(String[] args) {
SpringApplication.run(DatacenterApplication.class, args);
}

}

注意:@EnableFeignClients(“com.common.api”)一定要指定api子工程路径,否则不执行调用。

3.调用

1
2
3
4
5
6
7
8
9
10
11
typescript复制代码public class DemoController {

@Autowired
UserApi userApi;

@GetMapping("/demo")
public void demo() {
Result<RestSysUserVO> xxx = userApi.getCurrentUser();
}

}

本文转载自: 掘金

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

生产问题分析!delete in子查询不走索引?!

发表于 2021-09-29

前言

大家好,我是捡田螺的小男孩。

文章开篇前,先问大家一个问题:delete in子查询,是否会走索引呢?很多伙伴第一感觉就是:会走索引。最近我们出了个生产问题,就跟它有关。本文将跟大家一起探讨这个问题,并附上优化方案。

  • 公众号:捡田螺的小男孩

问题复现

MySQL版本是5.7,假设当前有两张表account和old_account,表结构如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
sql复制代码CREATE TABLE `old_account` (
  `id` int(11) NOT NULL AUTO_INCREMENT COMMENT '主键Id',
  `name` varchar(255) DEFAULT NULL COMMENT '账户名',
  `balance` int(11) DEFAULT NULL COMMENT '余额',
  `create_time` datetime NOT NULL COMMENT '创建时间',
  `update_time` datetime NOT NULL ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
  PRIMARY KEY (`id`),
  KEY `idx_name` (`name`) USING BTREE
) ENGINE=InnoDB AUTO_INCREMENT=1570068 DEFAULT CHARSET=utf8 ROW_FORMAT=REDUNDANT COMMENT='老的账户表';

CREATE TABLE `account` (
  `id` int(11) NOT NULL AUTO_INCREMENT COMMENT '主键Id',
  `name` varchar(255) DEFAULT NULL COMMENT '账户名',
  `balance` int(11) DEFAULT NULL COMMENT '余额',
  `create_time` datetime NOT NULL COMMENT '创建时间',
  `update_time` datetime NOT NULL ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
  PRIMARY KEY (`id`),
  KEY `idx_name` (`name`) USING BTREE
) ENGINE=InnoDB AUTO_INCREMENT=1570068 DEFAULT CHARSET=utf8 ROW_FORMAT=REDUNDANT COMMENT='账户表';

执行的SQL如下:

1
csharp复制代码delete from account where name in (select name from old_account);

我们explain执行计划走一波,

从explain结果可以发现:先全表扫描 account,然后逐行执行子查询判断条件是否满足;显然,这个执行计划和我们预期不符合,因为并没有走索引。

但是如果换成把delete换成select,就会走索引。如下:

为什么select in子查询会走索引,delete in子查询却不会走索引呢?

原因分析

select in子查询语句跟delete in子查询语句的不同点到底在哪里呢?

我们执行以下SQL看看

1
2
csharp复制代码explain select * from account where name in (select name from old_account);
show WARNINGS;

show WARNINGS 可以查看优化后,最终执行的sql

结果如下:

1
2
3
perl复制代码select `test2`.`account`.`id` AS `id`,`test2`.`account`.`name` AS `name`,`test2`.`account`.`balance` AS `balance`,`test2`.`account`.`create_time` AS `create_time`,`test2`.`account`.`update_time` AS `update_time` from `test2`.`account` 
semi join (`test2`.`old_account`)
where (`test2`.`account`.`name` = `test2`.`old_account`.`name`)

可以发现,实际执行的时候,MySQL对select in子查询做了优化,把子查询改成join的方式,所以可以走索引。但是很遗憾,对于delete in子查询,MySQL却没有对它做这个优化。

优化方案

那如何优化这个问题呢?通过上面的分析,显然可以把delete in子查询改为join的方式。我们改为join的方式后,再explain看下:

可以发现,改用join的方式是可以走索引的,完美解决了这个问题。

实际上,对于update或者delete子查询的语句,MySQL官网也是推荐join的方式优化

其实呢,给表加别名,也可以解决这个问题哦,如下:

1
csharp复制代码explain delete a from account as a where a.name in (select name from old_account)

为什么加别个名就可以走索引了呢?

what?为啥加个别名,delete in子查询又行了,又走索引了?

我们回过头来看看explain的执行计划,可以发现Extra那一栏,有个LooseScan。

LooseScan是什么呢? 其实它是一种策略,是semi join子查询的一种执行策略。

因为子查询改为join,是可以让delete in子查询走索引;而加别名,会走LooseScan策略,而LooseScan策略,本质上就是semi join子查询的一种执行策略。

因此,加别名就可以让delete in子查询走索引啦!

总结

  • 本博文分析了delete in子查询不走索引的原因,并附上解决方案。delete in在日常开发,是非常常见的,平时大家工作中,需要注意一下。同时呢,建议大家工作的时候,写SQL的时候,尽量养成一个好习惯,先用explain分析一下SQL。
  • 本文整体思路参考同事的博文,已经经过他本人同意。也建议大家遇到问题时,多点思考,多点写写总结,避免重蹈覆辙。
  • 我是捡田螺的小男孩,码字不易,看完文章有收获的话,求个点赞,公众号(捡田螺的小男孩)求个关注,感谢、比心~

本文转载自: 掘金

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

想不到Feign的一个注解还挺多坑的 引言

发表于 2021-09-28

引言

最近由于业务的需要,需要接入下阿里云的一个接口,打开文档看了看这个接口看下来还是比简单的目测个把小时就可以搞定,但是接入的过程还是比较坎坷的。首先我看了看他给的示例,首先把阿里云文档推荐的demo下载下来,把它的例子跑起来,替换下几个必要的参数比如秘钥啥的。这些秘钥一般公司都会有专职的人员与阿里云去对接,你只要负责管他要就行了。不过也不排除也有得公司需要自己去对接阿里云。说到这里就想吐槽下,对接阿里云的时候技术支持群居然是钉钉,所以需要他们的支持就必须要下载个钉钉,
电脑上莫名的有需要多装一个软件。扯远了我们还是回到正题,把它demo下载下来,然后把对应的秘钥等参数替换下,然后运行下demo看看是否能够正常返回结果,做这一步主要是为了保证产品给过来的秘钥等参数是否正确。如果能够掉通接口,那就说明参数没啥问题的接着我们就可以着手来写业务代码了。接入阿里云二要素认证market.aliyun.com/products/57…
把官网的demo下载下来跑起来看看,官网给出的例子还是比较简单粗暴的,就是封装了一个Apache的httplcient工具类一大坨的代码,个人还是习惯性的使用feign来进行调用,因为feign的代码干净整洁,虽然底层也是通过HttpClient来实现,但是实现对我来说是无感的,毕竟业务代码看起来干净整洁。它的demo如下:

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
java复制代码public static void main(String[] args) {
String host = "https://safrvcert.market.alicloudapi.com";
String path = "/safrv_2meta_id_name/";
String method = "GET";
String appcode = "你自己的AppCode";
Map<String, String> headers = new HashMap<String, String>();
//最后在header中的格式(中间是英文空格)为Authorization:APPCODE 83359fd73fe94948385f570e3c139105
headers.put("Authorization", "APPCODE " + appcode);
Map<String, String> querys = new HashMap<String, String>();
querys.put("__userId", "__userId");
querys.put("customerID", "customerID");
querys.put("identifyNum", "identifyNum");
querys.put("identifyNumMd5", "identifyNumMd5");
querys.put("userName", "userName");
querys.put("verifyKey", "verifyKey");


try {
/**
* 重要提示如下:
* HttpUtils请从
* https://github.com/aliyun/api-gateway-demo-sign-java/blob/master/src/main/java/com/aliyun/api/gateway/demo/util/HttpUtils.java
* 下载
*
* 相应的依赖请参照
* https://github.com/aliyun/api-gateway-demo-sign-java/blob/master/pom.xml
*/
HttpResponse response = HttpUtils.doGet(host, path, method, headers, querys);
//错误信息见X-Ca-Error-Message字段
System.out.println(response.toString());
//获取response的body
System.out.println(EntityUtils.toString(response.getEntity()));
} catch (Exception e) {
e.printStackTrace();
}
}
1
java复制代码HttpResponse response = HttpUtils.doGet(host, path, method, headers, querys);

根据它提供的代码我们可以看出来他是用一个httpUtils 类来实现http请求的,我们可以把这个httpClient类 替换成我们的FeignClient
替换后的代码如下:

1
2
3
4
java复制代码@FeignClient(name = "verifyIdCardAndNameFeignClient", url = "https://safrvcert.market.alicloudapi.com")
public interface VerifyIdCardAndNameFeignClient {
@RequestMapping(value = "/safrv_2meta_id_name/", method = RequestMethod.GET, produces = MediaType.APPLICATION_JSON_UTF8_VALUE)
Response verifyIdCardAndNameMap(@RequestParam Map<String,String> app, @RequestHeader("Authorization") String authorization);

相对比较下来下面这个HttpClientUtils代码是不是比较简洁
在这里插入图片描述
按照这个demo功能确实是实现了,说实话个人还是不是很喜欢用map来作为参数,map作为入参的话,参数全靠猜可读性以及可维护性有点差,个人还是习惯性的封装一个javaBean作为实体。阿里文档其实也有提到一嘴,虽然他只说到数据查询这一层。
在这里插入图片描述
下面我们就修改下请求参数把它改成一个javaBean,改变后的代码

1
2
java复制代码@RequestMapping(value = "/safrv_2meta_id_name/", method = RequestMethod.GET, produces = MediaType.APPLICATION_JSON_UTF8_VALUE)
Response verifyIdCardAndNameDTO(@RequestBody AliyunVerifyIdCardAndNameReq app, @RequestHeader("Authorization") String authorization);

在这里插入图片描述
请求并没有成功,根据报错返回的信息看下来应该是没有接受到参数。我们是GET请求的方式然后参数传递的是实体导致没有接收到。feignClient不支持get方式传递实体类吗?后来经过查询资料发现了一个注解@SpringQueryMap 我们把上述代码@RequestBody替换成@SpringQueryMap完美解决这个问题

@SpringQueryMap

在spring cloud 2.1.x 以上的版本,提供了一个新的注解@SpringQueryMap,为何这个注解可以帮我们实现。源码之下无秘密,我们可以翻翻
feign的源码相对来说应该是比较简单的,我们可以简单的来看下源码。看源码是不是也不知道从哪里看起,从头看到尾肯定也不现实,
不从头开始看,又不知道源码在哪里,有个很简单的方法我们直接拿着这个注解全局搜一下,看看有哪些地方使用到了,在每个地方都打上一个断点试试
在这里插入图片描述
我们全局搜下发现使用的地方主要在QueryMapParameterProcessor这个类里面。所以我们可以在这个类里面打上一个断点试试。

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复制代码
/**
* {@link SpringQueryMap} parameter processor.
*
* @author Aram Peres
* @see AnnotatedParameterProcessor
*/
public class QueryMapParameterProcessor implements AnnotatedParameterProcessor {

private static final Class<SpringQueryMap> ANNOTATION = SpringQueryMap.class;

@Override
public Class<? extends Annotation> getAnnotationType() {
return ANNOTATION;
}

@Override
public boolean processArgument(AnnotatedParameterContext context, Annotation annotation, Method method) {
int paramIndex = context.getParameterIndex();
MethodMetadata metadata = context.getMethodMetadata();
if (metadata.queryMapIndex() == null) {
metadata.queryMapIndex(paramIndex);
metadata.queryMapEncoded(SpringQueryMap.class.cast(annotation).encoded());
}
return true;
}
}

我们发现打这个类的话在容器启动的时候会进行加载,并且会执行processArgument方法,这个我们先不管这个方法,接下来我们来看看
Feign真正发起调用的地方找到SynchronousMethodHandler#invoke方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
java复制代码public RequestTemplate create(Object[] argv) {
... 省略部分代码
// metadata.queryMapIndex() 就是QueryMapParameterProcessor #processArgument方法赋值的
if (metadata.queryMapIndex() != null) {
// add query map parameters after initial resolve so that they take
// precedence over any predefined values
// 通过下标获取到需要特殊处理的对象,这里有个问题只会处理方法参数的第一个@SpringQueryMap注解,
// 原因就是QueryMapParameterProcessor #processArgument这个方法只会把第一个下标赋值进去,然后这里也只会取第一个下标,所以只会处理第一个@SpringQueryMap注解
Object value = argv[metadata.queryMapIndex()];
//将对象转换为map 这里需要注意下默认使用解析参数的是FieldQueryMapEncoder类所以它并不会去解析父类的参数,如果需要解析父类的参数我们需要在feign的Config里面指定QueryMapEncoder为FieldQueryMapEncoder
Map<String, Object> queryMap = toQueryMap(value);
//拼接解析完成的对象为URL参数
template = addQueryMapQueryParameters(queryMap, template);
}
... 省略部分代码
}

上述代码逻辑还是挺好理解的

  • 首先去判断是否需要处理下querymap
  • 通过下标获取到需要特殊处理的对象
  • 将对象转换为map(这里有个坑默认不会去解析父类的字段)
  • 拼接追加map到url中

总结

  • 上面通过@SpringQueryMap注解实现了get传参,但是如果需要传递多个@SpringQueryMap注解我们可以怎么来实现呢?
  • 或者我们可以自己动手来实现一个我们自己的SpringQueryMap,我们该如何实现?
  • @SpringQueryMap注解默认是不会去解析父类的参数,如果需要解析父类的参数需要修改Feign的config# QueryMapEncoder为FieldQueryMapEncoder。
  • 如果我们自己去实现了一个AnnotatedParameterProcessor所有默认的PathVariableParameterProcessor、
    RequestParamParameterProcessor、RequestHeaderParameterProcessor、QueryMapParameterProcessor都会失效,为啥会失效我们去看看SpringMvcContract这个类。所以自定义AnnotatedParameterProcessor需要慎重。

结束

  • 由于自己才疏学浅,难免会有纰漏,假如你发现了错误的地方,还望留言给我指出来,我会对其加以修正。
  • 如果你觉得文章还不错,你的转发、分享、赞赏、点赞、留言就是对我最大的鼓励。
  • 感谢您的阅读,十分欢迎并感谢您的关注。

本文转载自: 掘金

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

使用threejs实现炫酷的酸性风格3D页面

发表于 2021-09-28

小知识,大挑战!本文正在参与“程序员必备小知识”创作活动。

背景

近期学习了 WebGL 和 Three.js 的一些基础知识,于是想结合最近流行的酸性设计风格,装饰一下个人主页,同时总结一些学到的知识。本文内容主要介绍,通过使用 React + three.js 技术栈,加载 3D模型、添加 3D文字、增加动画、点击交互等,配合样式设计,实现充满设计感的 🤢 酸性风格页面。

基础知识

Three.js

Three.js 是一款基于原生 WebGL封装运行在浏览器中的 3D引擎,可以用它创建各种三维场景,包括了摄影机、光影、材质等各种对象。是一款使用非常广泛的三维引擎。可以在 three.js官方中文文档 进一步深入学习。

酸性设计

酸性设计 一词翻译自 Acid Graphics,起源于 上世纪90年代 的酸浩室音乐、电子舞曲以及嬉皮士文化。在设计领域,这种酸性美学承载一种 自由的主张,怪诞的图形,大胆鲜明的配色,特殊的材料质感,搭配多种字体,组成了独特的酸性设计风格。

总之,鲜艳高饱和度 的色彩组合;黑灰色打底高饱和 荧光色 点缀画面的 五彩斑斓的黑;充满未来感、炫酷、充满科技感的液态金属、玻璃、铝箔塑料等材质;随机 的元素、图形的布局;不断 重复、裁切、组合 几何图形 等都是酸性设计风格。酸性风格在音乐专辑封面、视觉海报、书籍电影封面、网页设计中也逐渐开始流行。

实现效果

在线预览:tricell.fun

实现

3D模型

场景初始化

🌏 创建场景
1
ts复制代码scene = new THREE.Scene();
📷 初始化相机

透视相机 PerspectiveCamera 的 4个 参数分别是指:视场、长宽比、近面、远面。

1
2
3
4
5
ts复制代码camera = new THREE.PerspectiveCamera(70, window.innerWidth / window.innerHeight, 0.1, 1000);
// 设置相机位置
camera.position.set(600, 20, -200);
// 相机聚焦到屏幕中央
camera.lookAt(new THREE.Vector3(0, 0, 0));
💡 初始化光源

添加 半球光源 HemisphereLight:创建室外效果更加自然的光源

1
2
3
4
5
6
7
ts复制代码light = new THREE.HemisphereLight(0xffffff, 0x444444);
light.position.set(0, 20, 0);
scene.add(light);
light = new THREE.DirectionalLight(0xffffff);
light.position.set(0, 20, 10);
light.castShadow = true;
scene.add(light);

添加 环境光 AmbientLight:

1
2
3
ts复制代码var ambiColor = '#0C0C0C';
var ambientLight = new THREE.AmbientLight(ambiColor);
scene.add(ambientLight);

添加辅助工具(可选)

📦 添加辅助网格

GridHelper 可用于添加网格辅助线,也可用于装饰,通过 GridHelper(size, divisions, colorCenterLine, colorGrid) 实现。

  • size:网格宽度,默认值为 10。
  • divisions:等分数,默认值为 10。
  • colorCenterLine:中心线颜色,默认值为 0x444444。
  • colorGrid: 网格线颜色,默认值为 0x888888。
1
2
3
4
5
ts复制代码var grid = new THREE.GridHelper(1000, 100, 0x000000, 0x000000);
grid.material.opacity = 0.1;
grid.material.transparent = true;
grid.position.set(0, -240, 0);
scene.add(grid);
📦 添加相机控件

通过相机控件 OrbitControls 可以对三维场景进行缩放、平移、旋转操作,本质上改变的并不是场景,而是相机的参数。开发时 OrbitControls.js 需要单独引入。

1
2
3
ts复制代码controls = new THREE.OrbitControls(camera, renderer.domElement);
controls.target.set(0, 0, 0);
controls.update();
📦 添加性能查看插件

stats 是一个 Three.js 开发的辅助库,主要用于检测动画运行时的帧数。stats.js 也需要单独引入。

1
2
ts复制代码stats = new Stats();
container.appendChild(stats.dom);

加载模型

本文示例用到的 扔铁饼的人 雕像 3D 模型来源于 threedscans.com,可 免费😄 下载使用,本文末尾提供了多个免费模型下载网站,有 200多页 免费模型,大家感兴趣的话可以挑选自己喜欢的模型下载使用。当然,有建模能力的同学,也可以使用 blender、3dmax 等专业建模软件生成自己喜欢的模型。

加载 obj 或 fbx 模型

需要单独引入 FBXLoader.js 或 OBJLoader.js,.fbx 和 .obj 格式的模型加载方法是一样的。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
ts复制代码// var loader = new THREE.FBXLoader();
var loader = new THREE.OBJLoader();
loader.load(model, function (object) {
object.traverse(function (child) {
if (child.isMesh) {
child.castShadow = true;
child.receiveShadow = true;
}
});
object.rotation.y = Math.PI / 2;
object.position.set(0, -200, 0);
object.scale.set(0.32, 0.32, 0.32);
model = object;
scene.add(object);
});
加载 gltf 模型

需要单独引入 GLTFLoader.js,加载 .gltf 格式模型方法稍有不同,需要注意的是模型的遍历对象和最终添加到场景中的是 object.scene 而不是 object。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
ts复制代码var loader = new THREE.GLTFLoader();
loader.load(model, function (object) {
object.scene.traverse(function (child) {
if (child.isMesh) {
child.castShadow = true;
child.receiveShadow = true;
}
});
object.scene.rotation.y = Math.PI / 2;
object.scene.position.set(0, -240, 0);
object.scene.scale.set(0.33, 0.33, 0.33);
model = object.scene;
scene.add(object.scene);
});

添加网格、加载完成模型之后的效果如下图所示。

添加转盘动画

通过 requestAnimationFrame 刷新页面的方法添加转盘动画效果。window.requestAnimationFrame() 告诉浏览器希望执行一个动画,并且要求浏览器在下次重绘之前调用指定的回调函数更新动画。该方法需要传入一个回调函数作为参数,该回调函数会在浏览器下一次重绘之前执行。

1
2
3
4
5
6
ts复制代码function animate () {
requestAnimationFrame(animate);
// 随着页面重绘不断改变场景的rotation.y来实现旋转
scene.rotation.y -= 0.015;
renderer.render(scene, camera);
}

添加点击交互

在 Three.js 场景中我们要点击某个模型获取它的信息、或者做一些其他操作,要用到 Raycaster(光线投射),原理就是在你鼠标点击的位置发射一束射线,被射线中的物体都被记录下来。基本语法是 Raycaster(origin, direction, near, far),其中:

  • origin:射线的起点向量。
  • direction:射线的方向向量。
  • near:所有返回的结果应该比 near 远。值不能为负,默认值为 0。
  • far:所有返回的结果应该比 far 近。不能小于 near,默认值为 无穷大。

代码实现的基本步骤是:获取鼠标在屏幕的坐标 → 屏幕坐标转标准设备坐标 → 标准设备坐标转世界坐标 → 拿到鼠标在场景的世界坐标 → 根据世界坐标和相机生成射线投射方向单位向量 → 根据射线投射方向单位向量创建射线投射器对象。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
ts复制代码//声明raycaster和mouse变量
var raycaster = new THREE.Raycaster();
var mouse = new THREE.Vector2();
onMouseClick = event => {
// 将鼠标点击位置的屏幕坐标转成threejs中的标准坐标,以屏幕中心为原点,值的范围为-1到1.
mouse.x = (event.clientX / window.innerWidth) * 2 - 1;
mouse.y = - (event.clientY / window.innerHeight) * 2 + 1;
// 通过鼠标点的位置和当前相机的矩阵计算出raycaster
raycaster.setFromCamera(mouse, camera);
// 获取raycaster直线和所有模型相交的数组集合
let intersects = raycaster.intersectObjects(scene.children);
if (intersects.length > 0) {
alert('HELLO WORLD')
// 可以通过遍历实现点击不同mesh触发不同交互,如:
let selectedObj = intersects[0].object;
if (selectedObj.name === 'car') {
alert('汽车🚗')
}
}
}
window.addEventListener('click', onMouseClick, false);

添加3D文字

使用 TextGeometry(text : String, parameters : Object) 添加 3D文字,以下是可设置属性的说明:

  • size:字号大小,一般为大写字母的高度。
  • height:文字的厚度。
  • weight:值为 normal 或 bold,表示是否加粗。
  • font:字体,默认是 helvetiker,需对应引用的字体文件。
  • style:值为 normal 或 italics,表示是否斜体
  • bevelThickness:倒角厚度。
  • bevelSize:倒角宽度。
  • curveSegments:弧线分段数,使得文字的曲线更加光滑。
  • bevelEnabled:布尔值,是否使用倒角,意为在边缘处斜切。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
ts复制代码var loader = new THREE.FontLoader();
loader.load('gentilis_regular.typeface.json', function (font) {
var textGeo = new THREE.TextGeometry('HELLO WORLD', {
font: font,
size: .8,
height: .8,
curveSegments: .05,
bevelThickness: .05,
bevelSize: .05,
bevelEnabled: true
});
var textMaterial = new THREE.MeshPhongMaterial({ color: 0x03c03c });
var mesh = new THREE.Mesh(textGeo, textMaterial);
mesh.position.set(0, 3.8, 0);
scene.add(mesh);
});

优化

现在模型加载已经基本完成了,但是 3D 模型的体积一般比较大,部署之后我发现网页加载非常慢,影响用户体验,减小模型体积是十分必要的,在网上找了很久压缩工具,发现在不需要安装大型 3D建模软件 的情况下,使用 obj2gltf 可以将体积较大的 OBJ 格式模型转化为 gltf 模型,有效优化模型体积,提升网页加载速度。

安装
1
bash复制代码npm install obj2gltf --save
将obj模型复制到以下目录中
1
bash复制代码node_modules\obj2gltf\bin
执行转码指令
1
bash复制代码node obj2gltf.js -i demo.obj -o demo.gltf

如图出现类似上述内容,转码完成,对比转化前后的文件体积,本例中 kas.obj 初始文件大小为 9.7M 转化后的文件 kas.gltf 只有 4.6M,体积缩小一半,此时将转化后的模型加载到页面上,肉眼几乎看不出模型效果的变化,同时页面加载速度得到明显提升。

obj2gltf 也可以作为库使用,通过 node服务 实时转化模型,感兴趣的同学可以通过文章末尾链接深入学习。
也可以是使用 3D 建模软件如 blender 等手动通过减少模型 面数 和 缩小体积 等途径对模型压缩优化,这种优化效果更明显。

完整代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
ts复制代码var model = require('@/assets/models/kas.gltf');
var container, stats, controls;
var camera, scene, renderer, light, model;
class Kas extends React.Component {
render () {
return (
<div id="kas"></div>
)
}
componentDidMount () {
this.initThree();
}
initThree () {
init();
animate();
function init () {
container = document.getElementById('kas');
scene = new THREE.Scene();
scene.fog = new THREE.Fog(0xa0a0a0, 200, 1000);
// 透视相机:视场、长宽比、近面、远面
camera = new THREE.PerspectiveCamera(70, window.innerWidth / window.innerHeight, 0.1, 1000);
camera.position.set(600, 20, -200);
camera.lookAt(new THREE.Vector3(0, 0, 0));
// 半球光源:创建室外效果更加自然的光源
light = new THREE.HemisphereLight(0xffffff, 0x444444);
light.position.set(0, 20, 0);
scene.add(light);
light = new THREE.DirectionalLight(0xffffff);
light.position.set(0, 20, 10);
light.castShadow = true;
scene.add(light);
// 环境光
var ambiColor = '#0C0C0C';
var ambientLight = new THREE.AmbientLight(ambiColor);
scene.add(ambientLight);
// 网格
var grid = new THREE.GridHelper(1000, 100, 0x000000, 0x000000);
grid.material.opacity = 0.1;
grid.material.transparent = true;
grid.position.set(0, -240, 0);
scene.add(grid);
// 加载gltf模型
var loader = new THREE.GLTFLoader();
loader.load(model, function (object) {
object.scene.traverse(function (child) {
if (child.isMesh) {
child.castShadow = true;
child.receiveShadow = true;
}
});
object.scene.rotation.y = Math.PI / 2;
object.scene.position.set(0, -240, 0);
object.scene.scale.set(0.33, 0.33, 0.33);
model = object.scene;
scene.add(object.scene);
});
renderer = new THREE.WebGLRenderer({ antialias: true, alpha: true });
renderer.setPixelRatio(window.devicePixelRatio);
renderer.setSize(window.innerWidth, window.innerHeight);
renderer.setClearAlpha(0);
renderer.shadowMap.enabled = true;
container.appendChild(renderer.domElement);
window.addEventListener('resize', () => {
camera.aspect = window.innerWidth / window.innerHeight;
camera.updateProjectionMatrix();
renderer.setSize(window.innerWidth, window.innerHeight);
}, false);
stats = new Stats();
container.appendChild(stats.dom);
}
function animate () {
var clock = new THREE.Clock()
requestAnimationFrame(animate);
var delta = clock.getDelta();
scene.rotation.y -= 0.015;
renderer.render(scene, camera);
stats.update();
}
// 增加点击事件
//声明raycaster和mouse变量
var raycaster = new THREE.Raycaster();
var mouse = new THREE.Vector2();
function onMouseClick(event) {
// 通过鼠标点击位置计算出raycaster所需要点的位置,以屏幕中心为原点,值的范围为-1到1.
mouse.x = (event.clientX / window.innerWidth) * 2 - 1;
mouse.y = - (event.clientY / window.innerHeight) * 2 + 1;
// 通过鼠标点的位置和当前相机的矩阵计算出raycaster
raycaster.setFromCamera(mouse, camera);
// 获取raycaster直线和所有模型相交的数组集合
var intersects = raycaster.intersectObjects(scene.children);
if (intersects.length > 0) {
alert('HELLO WORLD')
}
}
window.addEventListener('click', onMouseClick, false);
}
}

其他设计元素

本文主要介绍 3D元素 的加载,由于文章篇幅以及时间有限(博主太懒😂)其他元素的实现不做详细讲解(可能后续有时间会总结整理 maybe)感兴趣的同学可以扩展阅读以下其他大神优秀的文章。

流体背景

静态 液态背景图可以通过 SVG filter 实现,可以阅读《Creating Patterns With SVG Filters》,实现 动态 流体背景,可以使用Three.js 结合原生GLSL实现,可参考《CodePen Shader Template》示例来实现。

金属、霓虹、故障效果等酸性效果字体可以阅读我的另一篇文章《仅用CSS几步实现赛博朋克2077风格视觉效果》,也可以使用设计生成,由于时间关系,本文项目中的金属效果文字以及本文banner头图中的文字都是使用在线艺术字体生成网站生成的,感兴趣的同学可以自行尝试设计。

未来进一步优化

  • #todo 酸性风格液态背景实现。
  • #todo 3D模型液态金属效果。

three.js 优秀案例推荐

最后给大家推荐几个非常惊艳的 three.js 项目来一起体验和学习,无论是页面交互、视觉设计还是性能优化都做到了极致,可以从中学到很多。

  • github首页:3D地球 实时显示全球热门仓库。
  • kodeclubs:低面数 3D城市 第三人称小游戏。
  • 球鞋展示:720度 球鞋动态展示。
  • 沙雕dance:沙雕动物舞者。
  • Zenly软件:Zenly App 中文主页。

参考资料

  • three.js: threejs.org
  • obj2gltf: github.com/CesiumGS/ob…
  • 200多页免费3d模型 www.turbosquid.com
  • 免费3D雕像: threedscans.com
  • 免费3D模型:free3d.com
  • 艺术字体在线生成:cooltext.com
  • 什么是酸性设计:www.shejipi.com/361258.html

本文转载自: 掘金

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

【最强】Springboot学习路线汇总(升职加薪必备架构图

发表于 2021-09-28

​

小知识,大挑战!本文正在参与“程序员必备小知识”创作活动。

** 前言:**

在以前传统Spring去做Java开发中,大量 XML文件存在项目中,导致项目变得笨重繁琐、开发和部署效率也降低。前几年推出的SpringBoot 提升了Spring 开发者体验。集成了大量常用第三方库配置、零配置开箱即用、让大家更加专注于业务逻辑。同时博主趁着周末也为大家收集整理了springboot的思维导图学习路线一共十多个模块。**文末获取电子版思维导图**

)​

一 、spring基础

第一部分主要包括Spring的简史、Spring的核心模块AOP/IOC、数据访问、项目的创建和一些基础配置等

​

二、spring常用配置

第二部分 主要是Bean的Scope、SpringEL和资源调用、事件Application Event等

)​

三、Spring高级话题

第二部分主要是Spring Aware、多线程和@Enable*注解的工作原理、Spring TestContext Framework集成测试等

​

四、springMVC基础

第四部分主要是SpringMVC项目快速搭建、Spring MVC的常用注解、Spring MVC的基本配置、Spring MVC的高级配置等

​

五、SpringBoot基础

第五部分主要是SpringBoot概述、SpringBoot核心功能、SpringBoot的优缺点 、SpringBoot版本依赖、SpringBoot快速搭建等基础

)​

六、SpringBoot核心部分

第六部分主要是SpringBoot核心部分、这块内容比较多也重要。主要是springboot 入口类和@SpringBootApplication、SpringBoot的配置文件、starter pom、外部配置、SpringBoot运行原理、核心注解等

​

七、SpringbootWeb开发

第七部分主要是Springboot集成Web开发、Thymeleaf模板引擎、Web相关配置、Tomcat配置、SSL配置、WebSocket、和基于Bootstrap和AnglarJS的现代Web应用等

​

八、SpringBoot的数据访问

第八部分主要是SpringBoot的数据访问、主要功能点为:引入Docker操作、SpringDataJPA的一系列操作。SpringDataREST、Spring的事务机制、声明式事务、数据缓存Cache、非关系型数据库NoSQL(MongoDB和Redis)等

​

中场休息一下、能坚持看到这里的同学有多少呢、在评论区扣个“奥斯卡” 哈哈

)​

九、SpringBoot企业级开发

第九部分主要模块是安全控制SpringSecurity权限框架、批处理SpringBatch、异步消息处理、系统集成SpringIntegration等

​

十、SpringBoot开发部署与测试

这部分主要模块是 springboot开发的热部署配置、常规部署war包、jar包形式、云部署–基于Docker的部署、SpringBoot的部署测试等

​

十一、Springboot应用监控

这部分主要是Springboot应用监控测试、测试端点、定制端点、自定义端点、SSH等一系列操作、普通用得不多

​

十二、分布式系统开发

这部分属于springboot高级部分、主要是微服务/原生云应用、SpringCloud快速入门(服务发现、配置、网关、路由 负载等)、微服务实战、基于Docker部署等

)​

大家点赞、收藏、关注、评论啦 、

打卡 文章 更新 67/ 100天

本文转载自: 掘金

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

限流算法有哪些?该用哪个?

发表于 2021-09-28

小知识,大挑战!本文正在参与“程序员必备小知识”创作活动。

在高并发环境下,为了保证系统的稳定,通常会用到限流、降级、熔断等手段,来保证系统的稳定可用。

限流顾名思义就是限制服务处理的流量,其实熔断、降级本质上也是限流的一种,都是阻断了请求流量,本篇文章重点介绍常见的限流算法。

为什么限流

为什么需要限流呢?这个问题比较好理解,就是请求服务的流量过大,会导致服务崩溃,为了避免这种情况的发生,所以要对流量进行限制。

在以下这些常见的情况下可能会引起流量激增:

  • 促销活动
  • 恶意用户刷单或请求服务
  • 网络爬虫
  • 正常的用户量增大

这些情况都会导致流量的增加,假如我们服务的QPS最大只能支持到1000,当某一时刻的请求增大到1000以上时,我们就要对流量限制在1000以内,这样才能保证服务的稳定运行,这就是限流。

漏桶算法

漏桶算法思路比较简单,就好比有一个漏斗,只能按照漏斗口的大小往外出水,当流入漏斗的水流过大时,则会溢出去,溢出的水不会从漏斗口流出。

可以看出,漏桶算法的核心逻辑为:

  1. 缓存请求
  2. 匀速处理
  3. 多出的丢弃

漏桶算法会强行限制请求速率,这一特点导致漏桶算法有以下缺点:

不能应对突发流量

比如漏桶的流出速率为10QPS,容量为30,当来一波20次/s的请求,共维持10秒,那么到第3秒时,漏桶容量便已存满,这以后的请求都会被丢弃,直到漏桶有新的容量被释放。但是这种情况是很常见的,直接丢弃这种方式比较粗暴。

不能有效利用资源

因为漏桶的出口速率固定,假设设置为10QPS,那么当请求数量为12,15,10,8,5这种情况请求时,整体来看平均请求速率也是10QPS,但是在前三秒因为速率设定为10,则会直接丢弃7个请求。

令牌桶算法

令牌桶算法是网络流量整形(Traffic Shaping)和速率限制(Rate Limiting)中最常使用的一种算法。从名字上很多人会和漏桶算法混淆,但是它们有很大的差别。令牌桶算法用来控制发送到网络上的数据的数目,并允许突发数据的发送。

令牌桶算法的过程主要包含以下内容:

  1. 系统匀速的产生令牌存放到令牌桶中;
  2. 令牌桶的容量固定,当令牌桶填满后,再放入其中的令牌会被丢弃;
  3. 每个请求从令牌桶中获取令牌,如果获取成功则处理请求,如果失败则丢弃请求。

那么为什么说令牌桶算法可以应对突发请求呢?

首先我们来理解一下什么是突发请求。加入我们希望服务速率是100QPS,正常情况我们的请求不会超过100/s,如果某一秒请求到达150,这就是突发请求。

如果是漏桶算法,如果第一秒有80个请求,第二秒有150个请求,那么就会有50个请求被丢弃。

但是在令牌桶算法中我们设定每秒产生100个令牌,我们可以设定令牌桶的容量为120,那么第1秒80个请求会消耗80个令牌,到第一秒的150个请求到达时,可以处理120个请求,只丢弃30个。那么是否意味着只要桶的容量设置的足够大就都可以处理了呢?当然不是,因为超过120可能就已经远远超出了服务处理的能力,桶容量设置的超过服务能力也就丢失了限流的作用了。

RateLimiter

talk is cheap, show me the code!

在Guava中的RateLimiter便是使用令牌桶算法实现的。


运行结果:

从运行结果可以看到,很准确的保证了每秒最多只有三个线程获取到令牌,很好的对请求做了控制。

Guava RateLimiter的的acquire()方法默认是阻塞的,如果在获取不到令牌时会阻塞等待,知道获取成功。

RateLimiter除了acquire()方法外还有其他的使用方法。

  • tryAcquire():尝试获取1个令牌,如果失败则直接返回
  • tryAcquire(int permits):尝试获取多个令牌
  • tryAcquire(long timeout, TimeUnit unit):尝试获取一个令牌如果当前没有会等待指定时间,不会阻塞
  • tryAcquire(int permits,long timeout, TimeUnit unit):尝试获取多个令牌如果当前没有会等待指定时间,不会阻塞

需要特别注意的是,RateLimiter只能支持单机的限流,所以它无法针对集群做流量限制。

集群环境如果需要做流量限制,常见的方式是使用Redis,针对每秒的时间戳对请求数进行累加,超过限制数量则拒绝服务。

总结

本文主要介绍了两种常见的限流算法:漏桶算法和令牌桶算法。

漏桶算法因为强制限定请求速率,所以不能应对突发流量;

令牌桶算法能够一定程度的应对突发流量,具体能应对的阈值取决于服务的能力;

当然,在某些场景下如果需要严格保证请求速率,漏桶算法则比令牌桶更适合。

Guava RateLimiter使用令牌桶算法实现,可以做为单机限流方案的一种选择,集群环境下的限流方案可以选择Redis实现。

没有最好的技术,只有最适合的技术。

本文转载自: 掘金

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

图解InnoDB事务实现原理|Redo Log&Undo L

发表于 2021-09-28

小知识,大挑战!本文正在参与“程序员必备小知识”创作活动

前言:

对于Mysql事务我们都知道事务具有ACID四个特性,分别为:

  • 原子性,
  • 一致性,
  • 隔离性,
  • 持久性。
    基于以上四个特性,我们可以总结出事务 多个操作要么一起成功,要么一起失败,事务提交后,进行落盘存储,接下来我们来看一下InnoDB是如何实现事务的;

对于Innodb它主要由俩个事务日志文件redoLog和undoLog来保证事务的原子性,一致性,持久性;隔离性由锁来控制 如间隙锁,排它锁;

Redo Log(重做日志,提供前滚操作):

是什么?

redolog是InnoDB里用来记录事务提交的物理日志文件,记录的是数据页的物理修改,而不是某一行或某几行修改成怎样怎样,主要用来恢复提交后的物理数据页(恢复数据页,且只能恢复到最后一次提交的位置)。

事务提交后RedoLog做了什么?

在每次进行写数据的时候都会发生IO,对于InnoDB来说每次修改一次数据都发生IO在性能上肯定是不被允许的,所以加入了事务日志缓存这个概念,这就是redolog_buffer(日志缓冲区),每次事务日志的写入并不会直接写入到文件中,而是会写入到缓冲区中,在一定事件的触发下,才会将缓冲区内的数据写入到日志文件中。也就是说,一个写操作在InnoDB引擎内部发生的事情其实是这样的:

写操作 –>redoLog操作–>写入redolog buffer –>写入redolog file –>本地落盘

RedoLog重做日志主要是用来实现Mysql事务特性里的的持久性主要由俩部分组成:

  • redo Buffer 重做日志缓冲(内存)
  • redo File 重做日志文件(磁盘)

从上可以看出redoBuffer是存在内存里的这样也就意味着redoBuffer肯定是高性能的但是同时它也是易丢失的,在机器故障时会出现内存文件的丢失,而redoFile是存在磁盘里的说明在写磁盘的时候会产生IO代表着它在存储时性能较差但是数据则是持久化的;

详解:

在概念上,innodb通过force log at commit机制实现事务的持久性,即在事务提交的时候,必须先将该事务的所有事务日志写入到磁盘上的redo log file和undo log file中进行持久化。
为了确保每次日志都能写入到事务日志文件中,在每次将log buffer中的日志写入日志文件的过程中都会调用一次操作系统的fsync操作(即fsync()系统调用)。调用fsync()的作用就是将buffer中的日志刷到磁盘上的log file中。

图片.png

对于fsync这个操作用户是可以干预的,因为每次提交事务都执行一次fsync,是比较影响数据库性能。通过innodb_flush_log_at_trx_commit可以来控制redo log刷新到磁盘的策略。

  • innodb_flush_log_at_trx_commit=1(默认)

当设置为1的时候,事务每次提交都会将log buffer中的日志写入os buffer并调用fsync()刷到log file中。这种方式即使系统崩溃也不会丢失任何数据,但是因为每次提交都写入磁盘,IO的性能较差。

  • innodb_flush_log_at_trx_commit=0

当设置为0的时候,事务提交时不会将log buffer中日志写入到os buffer,而是每秒写入os buffer并调用fsync()写入到log file中。也就是说设置为0时是(大约)每秒刷新写入到磁盘中的,当系统崩溃,会丢失1秒钟的数据。

  • innodb_flush_log_at_trx_commit=2

当设置为2的时候,每次提交都仅写入到os buffer,然后是每秒调用fsync()将os buffer中的日志写入到log file中。

图片.png

Undo Log(回滚日志,提供回滚操作)

是什么?

undoLog是InnoDB提供的回滚日志,它的作用是用来提供事务的回滚能力,以及多个行版本控制(MVCC),用来回滚行记录到某个版本,undoLog一般是逻辑日志,根据每行记录进行记录;

与RedoLog相比二者都算是用来提供数据恢复的日志。不同的是redoLog是物理日志,而undoLog是逻辑日志,比如我们执行了一条update更新操作,undoLog就会将update的反向操作记录下来,而redoLog则只会记录修改后的数据值。并且undoLog本身也需要持久化支持,所以undoLog也会产生redoLog。

详解:

UndoLog它主要提供了如下两个功能:

  • 实现事务回滚
  • 实现MVCC

undolog的落盘流程可以看上文的第一个图片
初始数据行:
图片.png
如上图1~5是其对应的数据。后面三个隐含字段分别对应该行的事务号和回滚指针;

事务1更改该行的各字段的值:
图片.png

  1. 用排他锁锁定该行
  2. 记录redo log
  3. 把该行修改前的值写到undo log
  4. 修改当前行的值,填写事务编号,使回滚指针指向undo log中的修改前的行。

事务2修改该行的值:
图片.png
与事务一执行的流程相同,但是在undoLog中有有两行记录,并且通过回滚指针连在一起

当执行rollback时,就可以从undo log中的逻辑记录读取到相应的内容并进行回滚。有时候应用到行版本控制的时候,也是通过undo log来实现的:当读取的某一行被其他事务锁定时,它可以从undo log中分析出该行记录以前的数据是什么,从而提供该行版本信息,让用户实现非锁定一致性读取。

在InnoDB里有这么几个概念 页,区,段,表空间,而段分为这么几个

  • 数据段:存储数据
  • 索引段:存储索引
  • 回滚段:存储回滚日志
    Innodb就是使用回滚段来管理undoLog的

MVCC

MVCC通常是通过保存数据在某个时间点的快照来实现的。这意味着一个事务无论运行多长时间,在同一个事务里能够看到数据一致的视图。根据事务开始的时间不同,同时也意味着在同一个时刻不同事务看到的相同表里的数据可能是不同的。

  • 每行数据都存在一个版本,每次数据更新时都更新该版本。
  • 修改时Copy出当前版本随意修改,各个事务之间无干扰。
  • 保存时比较版本号,如果成功(commit),则覆盖原记录;失败则放弃copy(rollback)

ok!本期内容到此结束,你学废了吗,希望可以对大家有帮助,有不对的地方希望大家可以提出来的,共同成长;

整洁成就卓越代码,细节之中只有天地

本文转载自: 掘金

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

什么是低代码(Low-Code)? 一、前言 二、什么是低代

发表于 2021-09-28

简介: 什么是低代码?我们为什么需要低代码?低代码会让程序员失业吗?本文总结了低代码领域的基本概念、核心价值与行业现状,带你全面了解低代码。

阿里云 云原生应用研发平台EMAS 彭群(楚衡)

一、前言

如果选择用一个关键词来代表即将过去的2020年,我相信所有人都会认同是“新冠”。疫情来得太快就像龙卷风,短短数月就阻断了全世界范围内无数人与人之间的物理连接。但好在,我们已经全面迈入互联网时代:N95口罩再厚,也阻挡不了信息比特流的顺畅流通(宅男:B站依然香);居家隔离再久,也妨碍不了钉钉消息的准时送达(社畜:工作依然苦)。逍遥子在9月份的云栖大会上说:“新技术代表的新生产力,一定是我们全速战胜疫情、开创未来最好的原动力。” 那么在后疫情时代,究竟需要什么样的新技术,才能真正解放IT生产力,加速社会数字化转型,Make The World Great Again?我认为是低代码(Low-Code)。

基于经典的可视化和模型驱动理念,结合最新的云原生与多端体验技术,低代码能够在合适的业务场景下实现大幅度的提效降本,为专业开发者提供了一种全新的高生产力开发范式(Paradigm Shift)。另一方面,低代码还能让不懂代码的业务人员成为所谓的平民开发者(Citizen Developer),弥补日益扩大的专业人才缺口,同时促成业务与技术深度协作的终极敏捷形态(BizDevOps)。本文将重点介绍低代码相关背景知识,包括低代码的定义与意义、相关概念、行业发展等,期望能帮助大家更好地认识与理解低代码这个新兴领域。

二、什么是低代码

“Low-Code”是什么?如果你是第一次听说,没准也会跟我当年从老板口中听到这个词后的内心戏一样:啥?“Low-Code”?“Code”是指代码我知道,但这个“Low”字是啥意思?不会是老板发现我最近赶工写的代码很丑很“Low”吧… 想多了,老板怎么可能亲自review代码呢。那难道是指,“Low-level programming”里的“Low”?老板终于发现让我等编程奇才整天堆Java业务代码太浪费,要派我去闭关写一个高性能C语言网络库… 显然也不是,老板哪能有这技术情怀呢。那到底是什么意思?作为一名搜商比情商还高的程序员,能问Google的绝不会问老板。于是我一顿操作后,不假思索地点开了第一条搜索结果:Low-code development platform。

Wikipedia定义

从Wiki的这段定义中,我们可以提炼出几个关键信息:

  • • 低代码开发平台(LCDP)本身也是一种软件,它为开发者提供了一个创建应用软件的开发环境。看到“开发环境”几个字是不是很亲切?对于程序员而言,低代码开发平台的性质与IDEA、VS等代码IDE(集成开发环境)几乎一样,都是服务于开发者的生产力工具。
  • • 与传统代码IDE不同的是,低代码开发平台提供的是更高维和易用的可视化IDE。大多数情况下,开发者并不需要使用传统的手写代码方式进行编程,而是可以通过图形化拖拽、参数配置等更高效的方式完成开发工作。

Forrester定义

顺着Wiki的描述还能发现,原来“Low-Code”一词早在2014年就由Forrester提出了,它对低代码开发平台的始祖级定义是这样的:

相比Wiki的版本,这个定义更偏向于阐明低代码所带来的核心价值:

  • • 低代码开发平台能够实现业务应用的快速交付。也就是说,不只是像传统开发平台一样“能”开发应用而已,低代码开发平台的重点是开发应用更“快”。更重要的是,这个快的程度是颠覆性的:根据Forrester在2016年的调研,大部分公司反馈低代码平台帮助他们把开发效率提升了5-10倍。而且我们有理由相信,随着低代码技术、产品和行业的不断成熟,这个提升倍数还能继续上涨。
  • • 低代码开发平台能够降低业务应用的开发成本。一方面,低代码开发在软件全生命周期流程上的投入都要更低(代码编写更少、环境设置和部署成本也更简单);另一方面,低代码开发还显著降低了开发人员的使用门槛,非专业开发者经过简单的IT基础培训就能快速上岗,既能充分调动和利用企业现有的各方面人力资源,也能大幅降低对昂贵专业开发者资源的依赖。

低代码核心能力

基于上述的定义和分析,不难总结出如下这3条低代码开发平台的核心能力:

  • • **全栈可视化编程:**可视化包含两层含义,一个是编辑时支持的点选、拖拽和配置操作,另一个是编辑完成后所及即所得(WYSIWYG)的预览效果。传统代码IDE也支持部分可视化能力(如早年Visual Studio的MFC/WPF),但低代码更强调的是全栈、端到端的可视化编程,覆盖一个完整应用开发所涉及的各个技术层面(界面/数据/逻辑)。
  • • **全生命周期管理:**作为一站式的应用开发平台,低代码支持应用的完整生命周期管理,即从设计阶段开始(有些平台还支持更前置的项目与需求管理),历经开发、构建、测试和部署,一直到上线后的各种运维(e.g. 监控报警、应用上下线)和运营(e.g. 数据报表、用户反馈)。
  • • **低代码扩展能力:**使用低代码开发时,大部分情况下仍离不开代码,因此平台必须能支持在必要时通过少量的代码对应用各层次进行灵活扩展,比如添加自定义组件、修改主题CSS样式、定制逻辑流动作等。一些可能的需求场景包括:UI样式定制、遗留代码复用、专用的加密算法、非标系统集成。

不只是少写代码

回到最初那个直击心灵的小白问题:Low-Code中的“Low”,到底是啥意思?答案已经显而易见:既不是指抽象程度很低(相反,低代码开发方式的抽象程度要比传统编程语言高一个level),也不是指代码很low(也相反,低代码所生成的代码一般都经过精心维护和反复测试,整体质量强于大部分手写代码),而是单纯的“少写代码” —— 只在少数需要的情况下才手写代码,其他大部分时候都能用可视化等非代码方式解决。

再往深一点儿看,低代码不只是少写代码而已:代码写得少,bug也就越少(正所谓“少做少错”),因此开发环节的两大支柱性工作“赶需求”和“修bug”就都少了;要测的代码少了,那么测试用例也可以少写不少;除了开发阶段以外,平台还覆盖了后续的应用构建、部署和管理,因此运维操作也更少了(Low-Code → Low-Ops)。

然而,少并不是最终目的:如果单纯只是想达到少的效果,砍需求减人力、降低质量要求也是一样的。低代码背后的哲学,是少即是多(Less is More),或者更准确说是多快好省(Do More with Less) —— 能力更多、上线更快、质量更好,成本还更省,深刻践行了阿里“既要,又要,还要”的价值观精髓。

平台的职责与挑战

上面说的是低代码给开发者提供的能力与吸引力,那么作为服务的提供方与应用的承载者,低代码开发平台自身应该承担怎样的职责,其中又会遇到多大的挑战?是否就一定要如阿里云所主张的那样,“把复杂留给自己,把简单留给别人”?虽然这句话听起来很深明大义,但不知道大家有没有想过,为什么我们一定要抱着复杂不放,平白无故给自己找事?就不能直接干掉复杂,也给咱阿里云自己的员工留点简单吗?是工作太容易就体现不出来KPI价值了,还是家里的饭菜不如公司的夜宵香?

冥思苦想许久后,我从热力学第一定律中找到了答案:开发一个应用的总复杂度是恒定的,只能转移而不可能凭空消失。要想让开发者做的更少,安心享受简单的快乐,那么平台方就得做的更多,默默承担尽可能多的复杂度。就像一个满身腱子肉的杂技男演员,四平八稳地托举着在高处旋转与跳跃的女搭档;上面的人显得越轻盈越毫不费力,下面的人就得越稳重越用尽全力。当然,不是说上面的女演员就很轻松没压力,只是他们各自的分工不同,所承担的复杂度也不一样。

根据《人月神话》作者Fred Brooks的划分,软件开发的复杂度可以划分为本质复杂度(Essential complexity )和偶然复杂度(Accidental complexity)。前者是解决问题时固有的最小复杂度,跟你用什么样的工具、经验是否丰富、架构好不好等都无关,而后者就是除此之外在实际开发过程中引入的复杂度。通常来说,本质复杂度与业务要解决的特定问题域强相关,因此这里我把它称为更好理解的“业务复杂度”;这部分复杂度不是任何开发方法或工具能解决的,包括低代码。而偶然复杂度一般与开发阶段的技术细节强相关,因此我也相应把它称为“技术复杂度”;而这一部分复杂度,恰好就是低代码所擅长且适合解决的。

为开发者尽可能屏蔽底层技术细节、减少不必要的技术复杂度,并支撑其更好地应对业务复杂度(满足灵活通用的业务场景需求),这是身为一个低代码开发平台所应该尽到的核心职责。

在尽到上述职责的同时,低代码开发平台作为一个面向开发者的产品,还需要致力于为开发者提供简单直观的极致开发体验。这背后除了巨大的工作量,还得能在“强大”和“易用”这两个很难两全其美的矛盾点之间,努力找到一个符合自己产品定位与目标客户需求的平衡点 —— 这也许是设计一个通用低代码开发平台所面临的最大挑战。

三、低代码相关概念对比

纯代码(Pro-Code / Custom-Code)

“纯代码”可能算是我杜撰的一个词,更常见的说法是专业代码(Pro-Code)或定制代码(Custom-Code);但意思都一样,就是指传统的以代码为中心(Code-Centric)的开发模式。之所以我选择用“纯代码”,是因为如果用“专业代码”会显得似乎低代码就不专业了一样,而用“定制代码”又容易让人误解成低代码无法支持定制的自定义代码。

当然,更准确的称谓我认为是“高代码”(与低代码恰好对应,只是名字太难听,被我嫌弃了…),因为即便是使用传统的代码IDE,有些开发工作也支持(甚至更适合)以非代码方式完成,比如:iOS端开发时使用的SwiftUI界面设计器、服务端开发数据库应用时使用的PowerDesigner建模工具。不过这部分可视化工作在传统开发模式下只是起辅助作用,最后通常也是生成开发者可直接修改的代码;开发者仍然是以代码为中心来开展主要工作。

低代码与纯代码之间的关系,其实跟视频和文章之间很像:

  • • 低代码就像是现代的“视频”,大部分内容都由直观易理解、表达能力强的图片组成,因此更容易被大众所接受。但与此同时,视频也不是死板得只能有图片,完全可以添加少量文字(如字幕、标注)来弥补图片表达不够精确的问题。BTW,关于“图”和“文字”之间的辩证关系,可以进一步参考《架构制图:工具与方法论》[1]这篇文章中的相关描述。
  • • 纯代码则更像是传统的“文章”,虽然很久以来都一直是信息传播的唯一媒介,但自从视频技术诞生以及相应软硬件基础设施的普及以来,便逐渐开始被抢走了风头。如今,视频已成为大部分人获取信息的主要渠道(从电视电影到B站抖音),而经常读书读文章的人却越来越少。但不可否认的是,文章依然有它存在的意义和受众(不然我也不会费这劲敲这么多字了),即使“市场份额”一直在被挤压,但永远会有它立足的空间。

如果按上面这种类比关系推导,低代码未来也会遵循与视频类似的发展轨迹,超越纯代码成为主流开发模式。Gartner的预测也表达了相同的观点:到2024年,所有应用程序开发活动当中的65%将通过低代码的方式完成,同时75%的大型企业将使用至少四种低代码开发工具进行应用开发。

但同样地,就像是视频永远无法取代文章一样,低代码也永远无法彻底取代纯代码开发方式。未来低代码和纯代码方式将以互补的形态长期共存,各自在其所适合的业务场景中发光发热。在后面的“低代码业务场景”章节,会详细列出哪些场景在现阶段更适合用低代码模式开发。

零代码(Zero-Code / No-Code)

从分类的完备性角度来看,有“纯代码”自然也应该有完全相反的“零代码”(也称为“无代码”)。零代码就是完全不需要写代码的应用开发平台,但这并不代表零代码就比低代码更高级和先进,它只是做了一个更极端的选择而已:彻底拥抱简单的图形可视化,完全消灭复杂的文本代码。选择背后的原因是,零代码开发平台期望能尽可能降低应用开发门槛,让人人都能成为开发者(注意:开发 ≠ 写代码),包括完全不懂代码的业务分析师、用户运营,甚至是产品经理(不懂装懂可不算懂)。

即便是专业开发者,在技术分工越来越精细的趋势下(前端/后端/算法/SRE/数据分析..),也很难招到一个能独立开发和维护整套复杂应用的全栈工程师。但零代码可以改变这一切:无论是Java和JavaScript傻傻分不清楚的技术小白,还是精通深度学习但没时间学习Web开发的算法大牛,都可以通过零代码实现自己的技术梦或全栈梦。“改变世界的idea已有,就差一个程序员了”,这句玩笑话或许真的可以成真;哦不,甚至都用不着程序员,有idea的人自己就能上。

当然,所有选择都要付出代价,零代码也不例外。完全抛弃代码的代价,就是平台能力与灵活性受限:

  • • 一方面,可视化编辑器的表达能力远不及图灵完备的通用编程语言,不引入代码根本没法实现灵活的定制与扩展(当然,理论上也可以做成Scrach/Blockly那样的图形编程语言,但那样不过是换一种形式在手写代码而已)。
  • • 另一方面,由于目标受众是非专业开发人员,平台能支持的操作会更趋于“傻瓜化”(e.g. 页面只支持大块业务组件的简单堆叠,不支持细粒度原子组件和灵活的CSS布局定义),同时也只会透出相对“亲民化”的模型和概念(e.g. 使用“表格”表示数据,而不是用“数据库”),无法支撑强大专业的底层开发原语和编程理念。

虽然零代码与狭义上的低代码有着上述明显差异,但从广义上来说,零代码可以当作低代码的一个子集。Gartner在其相关调研报告中,就是将“No Code”划在了范围更广的低代码应用平台“LCAP”(Low-Code Application Platform)中。而当前市面上很多通用的低代码开发平台,也都兼具一定程度的零代码能力;比如低代码领域领头羊Mendix,既提供了简单易用的零代码Web IDE - Mendix Studio,也包括一个功能更强大的低代码桌面IDE - Mendix Studio Pro。

HpaPaaS(高生产力应用PaaS)

上文提到,“Low-Code”一词是拜Forrester所赐。作为同样是国际知名调研机构(a.k.a 造词小能手)的Gartner,显然不会轻易在这场可能决定低代码领域江湖地位的新概念作词大赛中认输,于是也于2017年发明了“HpaPaaS”(High-productivity application Platform as a Service)这个听上去更高大上的缩写词。

按照Gartner的定义,HpaPaaS是一种支持声明式、模型驱动设计和一键部署的平台,提供了云上的快速应用开发(RAD)、部署和运行特性;这显然与低代码的定义如出一辙。但事实证明,名字起得太专业并不见得是好事,“HpaPaas”最终还是败给了起源更早、更接地气也更顺口的“Low-Code”:从2019年开始,Gartner在其相关调研报告中也开始全面采用“Low-Code”一词(如LCAP),亲手为“HpaPaaS”打上了 @deprecated 印记。

图源:blog.kintone.com/business-wi…

值得补充的是,“HpaPaaS“这个词也并非横空出世,而是传承自更早之前Gartner提出的“aPaaS”,它俩之间的关系是:HpaPaaS只是aPaaS的一个子类;除了HpaPaaS这种通过低代码实现的高生产力应用开发平台以外,aPaaS还包括面向纯代码的传统应用开发平台(High-control aPaaS,即可控度更高的纯代码开发方式)。

不值得但就想八卦一下的是,“aPaaS”这个词也非凭空捏造,而是与云计算的兴起渊源颇深。相信各位云道中人都已猜到,aPaaS与IaaS/PaaS/SaaS这些云计算远古概念是一脉相承的:aPaaS介于PaaS和SaaS之间,相比PaaS提供的服务更偏应用,但又不像SaaS一样提供现成的软件服务(更详细的说明可参考配图来源文章)。

四、为什么需要低代码

低代码是什么可能并没那么重要,毕竟在这个信息爆炸的世界,永远不缺少新奇而又短命的事物。大部分所谓的新技术都只是昙花一现:出现了,被看到了;大部分人“哦”了一声,已阅但表示不感兴趣;小部分人惊叹于它的奇思妙想,激动地点了个赞后,回过头来该用什么还是什么。真正决定新技术是否能转化为新生产力的,永远不是技术本身有多么优秀和华丽,而是它是否真的被需要,即:为什么需要低代码?如果用不同的主语填充上面这个问句(冷知识:这叫做“延迟主语初始化”),可以更全面地看待这个问题:

为什么「市场」需要低代码?

在这个大爷大妈都满嘴“互联网+”和“数字化转型”的时代,企业越来越需要通过应用(App)来改善企业内部的信息流转、强化与客户之间的触点连接。然而,诞生还不太久的IT信息时代,也正面临着与我国社会主义初级阶段类似的供需关系矛盾:落后的软件开发生产力跟不上人民日益增长的业务需求。

Gartner预测,到2021年应用开发需求的市场增长将至少超过企业IT交付能力的5倍。面对如此巨大的IT缺口,如果没有一种革命性的“新生产力”体系,很难想象仅凭现有传统技术体系的发展延续就能彻底解决问题。而低代码技术正是带着这样的使命而降临,期望通过以下几个方面彻底革新应用开发生产力,拯救差一点就要迈入水深火热的IT世界:

提效降本 & 质量保障

虽然软件行业一直在高速发展,新的语言、框架和工具层出不穷,但作为从业者我们不得不承认:软件开发仍处于手工作坊阶段,效率低、人力成本高、质量不可控。项目延期交付已成为行业常态,而瓶颈几乎总是开发人员(对机器能解决的问题都不是问题);优秀的开发人才永远是稀缺资源,还贼贵;软件质量缺陷始终无法收敛,线上故障频发资损不断。

相比而言,传统制造业经过几百年工业革命的发展,大部分早已摆脱了对“人”的强依赖:从原料输入到制品输出,中间是各种精密仪器和自动化流水线的稳定支撑,真正实现生产的标准化和规模化。虽然信息化号称是人类的第三次工业革命,但以软件行业目前的状况,远远还没到达成熟的“工业化”阶段。

所以,亲爱的程序员朋友,当你与前端联调了一上午接口,又与产品撕逼了一下午需求,再与自己的bug抗争了一整晚,好不容易遁入梦乡又被一连串报警短信吵醒时,是否有抬头对着星空憧憬过:“I have a dream… that one day,软件开发也能像工业制品一样,批量流水化生产,稳定高效没烦恼。” 事到如今,不管你有没有意识到,这个憧憬正在慢慢变成现实。

是的,低代码正在将应用软件开发过程工业化:每个低代码开发平台都是一个技术密集型的应用工厂,所有项目相关人员都在同一条产线内紧密协作。开发主力不再是熟知for循环一百种写法的技术Geek,而是一群心怀想法业务sense十足的应用Maker。借助应用工厂中各种成熟的基础设施、现成的标准零件、自动化的装配流水线,开发者只需要专注于最核心的业务价值即可。即便是碰到非标需求,也可以随时自己动手,用最灵活的手工定制(代码)方式来解决各种边角问题。

扩大应用开发劳动力

通过让大部分开发工作可以仅通过简单的拖拽与配置完成,低代码(包括零代码)显著降低了使用者门槛,让企业能够充分利用前面所提到的平民开发者资源。部分纯零代码需求场景下,低代码还能让业务人员实现自助式(self-service)应用交付,既解决了传统IT交付模式下的任务堆积(backlog)问题,避免稀缺的专业开发资源被大量简单、重复性的应用开发需求所侵占,也能让业务人员真正按自己的想法去实现应用,摆脱交由他人开发时不可避免的桎梏。

至此,应用开发能力不再是少数专业开发者的专利和特权,且今后所需要的技能门槛与拥有成本也会越来越低,真正实现所谓的“技术民主化”(democratization of technology)。

加强开发过程的沟通协作

多方调查结果显示,软件项目失败的最主要原因之一就是缺乏沟通(poor communication)。传统开发模式下,业务、产品、设计、开发、测试与运维人员各司其职,且各有一套领域内的工具和语言,长久以来很容易形成一个个“竖井”(silos),让跨职能的沟通变得困难而低效。这也是为什么当前热门的敏捷开发和DevOps都在强调沟通(前者是协同Biz与Dev,而后者是协同Dev和Ops),而经典的DDD领域驱动设计也主张通过“统一语言”来减少业务与技术人员之间的沟通不一致。

有了低代码后,这一状况将得到根本改善:上述各角色都可以在同一个低代码开发平台上紧密协作(甚至可以是同一个人),这种全新的协作模式不仅打破了职能竖井,还能通过统一的可视化语言和单一的应用表示(页面/数据/逻辑),轻松对齐项目各方对应用形态和项目进度的理解,实现更终极的敏捷开发模式,以及在传统DevOps基础之上更进一步的BizDevOps[2]。

统一开发平台下的聚合效应

低代码尝试将所有与应用开发相关活动都收敛到同一个平台(one platform)上后,将会产生更多方面的聚合效应与规模收益:

  • • 人员聚合:除了上一点所提到的各职能角色紧密协作以外,人员聚合到统一的低代码开发平台进行作业后,还能促进整个项目流程的标准化、规范化和统一化。
  • • 应用聚合:一方面,新应用的架构设计、资产复用、相互调用变得更容易;另一方面,各应用的数据都天然互通,同时平台外数据也能通过集成能力进行打通,彻底消除企业的数据孤岛问题。
  • • 生态聚合:当低代码开发平台聚合了足够多的开发者和应用后,将形成一个巨大的、连接一切、有无限想象力的生态体系,彻底放飞低代码的价值。

为什么「这个时代」才需要低代码?

如果你了解过市面上各种低代码产品,不难发现其实这个领域的许多玩家在低代码概念诞生之前就已经存在了,比如:低代码领域的另一个巨头OutSystems,早在2001年就已经创立;而去年也被Forrester评为低代码行业leader之一的FileMaker,更是诞生于遥远的1985年(正好35岁,似乎在疯狂暗示什么)。那么,如果低代码像前面说的那么好,为什么以前没有火起来呢?从技术和业务两个角度看,可以归纳为以下原因:

技术成熟度不足

低代码底层的各项核心技术(可视化、模型驱动、RAD、BPMS…)都已经有漫长的发展历史,看上去似乎只是新瓶装旧酒。然而理智的人都知道,任何技术都会遵循所谓的“技术成熟度曲线”(The Hype Cycle),不可能刚一诞生就跳过发育直接秀翻全场,被大规模采纳和投入生产。以模型驱动技术为例,虽然十几年前就已经有体系化的理论研究(e.g. MDA)和配套工具(e.g. EMF),但在当时的技术背景下,由于能力不完备、过于理想化、技术门槛高等原因,一直没能在工业界走向主流。

而如今这个时代,支撑低代码的那些“老”技术都已经过长时间的发展酝酿与市场检验,而另一些完美互补的“新”技术(e.g. 云原生、响应式Web)也在飞速发展和走向成熟,是时候通过“低代码”这个新酒瓶重新包装上市,为亟需新生产力的传统IT市场带来一场真香之旅了。

业务收益不明显

即使十几年前的低代码技术已经足够成熟,也一定不会在当年的应用开发市场上产生现在这样的影响力。为什么?因为技术都是为业务服务的,而当时的应用开发业务需求可比现在简单多了:没有如今的多渠道(Multi-channel)、多样化体验(Multi-experience)和各种集成与定制需求,也不会奢求如今已成为企业级应用标配的弹性、分布式和高可用,更是缺乏快速变化的IT业务场景来推动持续集成与快速交付。

虽然低代码可以完美解决上述所有问题(e.g. 多端应用生成、云原生架构、API集成能力),但放在当年的市场和业务背景下,加上前面所说的技术不成熟度,整体的投入产出比会很低,不足以让企业大面积采纳低代码解决方案。

而如今这个时代,企业都快被新技术带来的能力和收益“惯坏了”,动不动就是:我想做一个送菜应用。用户端?安卓、iOS、H5、小程序都来一套。运营端?一般都在电脑上看,但记得手机上也得适配啊。服务端?上云,必须的。哦,我听技术合伙人说现在流行多云架构,也给我整一套哈。运维还要钱?啥是运维?应用有了不就能用了嘛,运维还要花我钱?你当投资者给我的钱是大风刮来的啊!

如果用传统的开发模式,这么全套下来的工时与报价,可能早就吓跑了这群跟产品经理一样天真可爱的人;但现代化的低代码技术,可以圆了上面这位创业者的卖菜梦,用白菜一般的价格,实现白粉一样的价值。当年的程维如果能用上现在的低代码,第一版的滴滴App也就不至于被外包做得乌烟瘴气直接报废了(至少能多扛一阵子…)。

为什么「专业开发者」也需要低代码?

虽然零代码确实是设计给非专业开发者用的,但其所能支撑的业务场景确实有限,无法真正革新传统开发模式,替代那些仍需专业开发者参与的复杂业务场景。而狭义上的低代码却有潜力做到这一点,因为它天生就是为专业开发者而量身定制的。Gartner最近的一项调研报告显示,“66%的低代码开发平台用户都是企业IT部门的专业开发者”。这充分说明了,专业开发者比平民开发者更需要低代码。

屏幕前一批穿格子衬衫的同学要发问了:“低代码都不怎么写代码了,怎么能算是为我们程序员服务呢?”。虽然程序员讨厌重复自己,但重要的事情还是得多说一遍:开发 ≠ 写代码。1万年前蹲在洞穴里的原始人,在用小石子画远古图腾;100年前坐在书桌前的徐志摩,在用钢笔给林徽因写情书;而今天趴在屏幕前的很多人,相信都已经开始用上手写板或iPad涂涂写写了。千百年来,人类使用的工具一直在演进,但所从事活动的本质并没有多大改变。无论是用小石子还是小鼠标,写作绘画的本质都是创造与表达,最终作品的好坏并不取决于当时你手中拿着什么;同样地,应用开发的本质是想法和逻辑,最终价值的高低也不取决你实现时是用的纯代码还是低代码。

而相比纯代码而言,低代码极有可能成为更好的下一代生产力工具:

减少不必要的工作量

可视化拖拽与参数配置的极简开发模式,结合模型驱动的代码自动生成机制,可以消灭绝大部分繁琐和重复的boilerplate代码;一站式的部署和运维管理平台,无需自己搭建CI/CD流水线、申请环境资源、配置监控报警;一次搭建同时生成、构建和发布多端应用,免去人工同步维护多个功能重复的端应用;开箱即用的组件库、模板库、主题库、连接器等,让最大化软件复用成为可能。总而言之,低代码能够让专业开发者更专注于创新性、有价值、有区分度的工作,而不是把宝贵开发时间都耗费在上面那些不必要的非业务核心工作上。

强大的平台能力支撑

虽然上面列的技术支撑性工作并不直接产生业务价值,但却会直接影响业务的性能、成本、稳定性、安全性、可持续发展能力等。有远见的企业,绝不允许牺牲这些重要指标,来换取短暂的业务加速。低代码开发平台深知这一点,因此在简化和屏蔽底层技术细节的同时,也会尽可能把自己所cover的部分做到最好(至少能和纯代码开发方式一样好),包括但不限于:

  • • 现代化的技术架构和实现:现代化的低代码开发平台,在支撑用户应用时所选择的技术架构与实现方案,也会是现代化且符合业界最佳实践的,例如,前端基于主流的HTML5/CSS3标准和React框架,后端基于成熟的Java语言、SpringBoot框架和MySQL数据库,部署环境基于云原生的Docker镜像、CI/CD流水线、K8s集群和Service Mesh技术(相关知识可参考《正确入门Service Mesh:起源、发展和现状》)。
  • • 零成本的技术升级和维护:低代码的高维抽象开发方式,让应用的核心业务逻辑与底层技术细节彻底解耦。开发者在大部分情况下都不需要关心底层技术选型,同时也无需亲自跟进这些技术的版本升级与漏洞修复,免费享受与时俱进的技术红利和应用安全性提升。即便遇到某些底层技术或工具需要进行彻底更换(比如不再维护的开源项目),开发者也完全不必感知;技术迁移再费劲再难搞,平台自己努力就行,对开发者来说只要服务一直在线,岁月就依然静好;事后可能还会惊喜地发现,应用访问突然就变得更快了,仿佛冥冥中自有天助,感激上苍和低代码。

一体化生态能力复用

复用(Reuse)是提升软件开发效率和工程质量的最有效途径。传统的代码开发模式下,开发者可以通过提取公共类/函数、引用共享库、调用外部API服务、沉淀代码片段和模板等方式实现复用。在低代码的世界里,平台也可以提供对应的多层次多粒度复用手段,比如页面组件库、逻辑函数库、应用模板库等。

但更重要的是,低代码平台还可以充分发挥其一体化的生态优势,提供强大易用的可复用能力(资产)的发现、集成与共享体系:以页面组件为例,你可以直接用系统组件,也可以在平台自带的组件市场上搜索和引用更合适的组件,还可以自己用代码开发一个自定义组件并发布到市场中。平台的生态体系越大,积累的可复用能力就越多,应用的开发成本也会越低。

相比而言,虽然传统代码世界整体生态更庞大和深厚,但由于各类技术不互通、缺乏统一平台与市场、代码集成成本高等原因,一直以来都没有形成有类似规模潜力的生态能力复用体系,导致重复造轮子和低水平重复建设的现象司空见惯,还美名为“新基建”。

说到这里,另一批裹着冲锋衣头顶锃亮的同学也忍不住了:“万一低代码真的发展起来了,是不是就不需要那么多程序员了啊?上有老下有小的,同是码农身,相煎何太急!”。低代码虽然是一场应用开发生产力革命,但并不会革掉程序员的饭碗。它去掉的只是难懂的编程语法、繁琐的技术细节和一切可自动化的重复性工作,并没有也无法去掉应用开发最核心的东西:严谨的业务逻辑、巧妙的算法设计、良好的工程风格等。对于真正的程序员,即使剥去他一层又一层的编程语言和工具熟练度技能外壳,最终剩下的仍然是一个有价值的硬核开发者。

当然,如果你坚持要用纯粹的写代码方式来改变世界,也不至于失业。要么,你可以选择那些低代码暂时不太适用的领域,比如底层系统驱动、3D游戏引擎、火箭发射程序;或者,你也可以选择去写低代码中那一部分不可或缺的自定义代码扩展,为平民开发者提供高质量的积木。最后,你也完全可以选择为低代码平台本身的底层代码添砖加瓦,比如加入阿里云云原生应用研发平台EMAS团队 (〃’▽’〃) ,与作者一起共建下一代云原生低代码开发平台“Mobi”,内推直达邮箱:pengqun.pq@alibaba-inc.com。

为什么「我不」需要低代码

即使所有人都认同上述“为什么要用低代码”的理由,但仍不时会有试水者跳出来,给大家细数“为什么我不需要低代码”。实践出真知没错,而且大部分质疑背后也都有一定道理;但在我看来,更多的可能是主观或无意识的偏见。这里我列了一些对低代码的常见质疑和我个人的看法,期望能帮助大家看到一个更全面和客观的低代码。

质疑1:低代码平台不好使

“试用过一些所谓的低代码开发平台,要么能力很弱,要么体验太差,只能开发点玩具应用。”

作为调研过国内外多款低代码产品的深度体验用户,我的观点是:不能以偏概全。低代码市场在国内正处于爆发初期,所以许多与低代码只沾一点边的产品也都在蹭热点;但它们并不能代表低代码目前的业界水平和发展方向。市面上真正成熟的企业级低代码开发平台,完全有能力以高效的开发方式满足大部分复杂场景的功能需求,以及企业级应用所需要的安全、性能、可伸缩等非功能需求,这一点在国外市场已得到充分验证(不然也不会这么被寄予厚望)。

当然,国内市场尚处于鱼龙混杂的混战阶段,遇到真龙的概率很低,但碰上金鱼鲤鱼甚至木头假鱼都在所难免。相信随着时间推移,真正有实力和口碑的产品都能脱颖而出,为大家展现低代码该有的样子。

质疑2:低代低开发不可控

“平台上的各种可视化组件、逻辑动作和部署环境都是黑盒,如果内部出问题无法排查和解决。”

作为同样不搞清楚底层原理不舒服斯基的程序员,我更愿意相信:问题只是暂时的。虽然这确实是目前使用低代码平台时绕不开的一个痛点,但并不属于低代码技术本身的固有缺陷。计算机领域有一句至理名言:任何问题都可以通过增加一个间接的中间层来解决。低代码的思路亦是如此:与当年的操作系统和现在的云平台一样,都是想通过建立一个黑盒化的中间层抽象来降低开发者的工作量与心智负担。

当然,所有额外增加的中间层都不是完全免费的,低代码也不例外。作为一个尚未成熟稳定的新的中间层,低代码必然会出现各种让使用者束手无措的问题,就跟当年的操作系统内核bug、如今的云主机I/O hang一样。但历史规律也告诉我们,所有伟大的技术最终都会走向成熟;只要低代码领域一直健康发展,问题总会越来越少,最终降到一个绝大部分人感知不到的范围内。过去萦绕在Windows用户心中挥之不去的“蓝屏”问题,对如今的新用户来说早已不知为何物;今天低代码开发者所遇到的种种“蓝瘦”问题,未来也终将成为被遗忘的历史(谁还没段黑历史呢)。

质疑3:低代码应用难维护

“应用一旦复杂起来,各种复杂逻辑流穿插着自定义代码,看不懂也改不动,还不如全用代码呢。”

作为对软件可维护性深有感触的无脑级布道者(见《救火必备!问题排查与系统优化手册》),我不得不说:用低代码开发,也要讲基本法。一般来说,无论是使用低代码开发还是纯代码开发,造成应用可维护性低的根本原因往往不在于开发工具,而是开发者自身没有去遵循一些软件开发的普适原则,比如工程规范性、命名可读性、DRY/KISS/SOLID原则等。

好的低代码平台绝不会阻碍开发者去改善应用的可维护性;恰恰相反,还会尽可能提供引导和帮助。以Mendix为例,除了支持基本的模型分析与重构(e.g. 无用模型、对象重命名、子逻辑流提取)以外,甚至还提供了基于ISO/IEC 25010标准的应用质量监控(AQM)能力。另一方面,让应用变得难以维护的一个客观原因也是应用本身过于复杂,而低代码作为高度抽象和自动化的开发模式,在降低应用复杂度方面是专业的。

综合来看,低代码虽然不是能解决一切问题的银弹,但更不是会带来更多问题的炸弹:在提高应用可维护性方面的上限,一定比传统开发模式更高;但决定应用可维护性下限的,依然还是开发者自己。

五、低代码行业发展

回应质疑的最好方式,就是做好你自己,用实际的表现说话。对于一个行业而言,判断它当前的表现是否够好,或者未来是否有潜力做到更好,可以从以下这三个方面进行衡量:市场规模(蛋糕够不够大)、适用场景(是否可落地)、竞品状况(有没有被验证过)。

市场规模

“Talk is cheap,show me the code money.”

—— Linus Starcraft

文章可以忽悠,但市场不会说谎:

  • • Forrester在2015年曾预测过,低代码的市场将从2015年的17亿美元增长至2020年的150亿美元。
  • • Marketsandmarkets在今年四月份的分析报告中预测,低代码的市场将从2020年的130亿美元(估算值,可以看出来与Forrester当年的预测是接近的)增长到2025年的450亿美元(年复合增长率:28.1%)。
  • • PS Inteligence在2018年的分析报告中预测,全球的低代码开发平台市场中,亚太地区将在今后五年(2019-2024年)中保持最高的增长速度。

总结一下就是两点:

  • • 低代码的市场规模足够大,且一直都在高速增长。
  • • 作为亚太地区的经济大国与IT强国,中国的低代码市场将会引来一个爆发期,未来几年内的增速都会超过全球平均水平。

适用场景

理论上来说,低代码是完全对标传统纯代码的通用开发模式,应该有能力支撑所有可能的业务场景。但理论也只是理论,低代码一统江湖的梦想尚未照进现实,也不可能完全取代现实。前文中提到过,低代码与纯代码方式是互补关系,未来也将长期共存,各自在其所适合的业务场景中发光发热。同时还需要指出的是,当前阶段的低代码技术、产品和市场都尚未完全成熟,因此部分本来可能很适合用低代码来开发的场景,目前也只能先用纯代码来替代。

Gartner在2019年的低代码调研报告中,曾经绘制过一张用来阐述低代码适用场景的“应用金字塔”:

  • • 应用级别划分:从下往上,分别为工作组级(Workgroup Class)、部门级(Departmental Class)、企业级(Enterprise Class)、可扩展需求极强的企业级(Extreme-Scale Enterprise Class)。容易看出来,它主要的划分维度就是应用所面向的用户基数(基数越大,可扩展需求也越高)。
  • • 任务关键性:从下往上,各级别应用的任务关键性(Mission Criticality)逐级递增。例如一个只在工作组内使用的后台管理应用,一般都不会涉及到影响整个企业的关键任务。脱离企业这个视角来看,整个软件产业中也有很多通用的任务关键型应用,比如:实时操作系统、航空调度系统、银行对账系统。
  • • 实现复杂度:从下往上,各级别应用的复杂度(Complexity)也逐级递增。例如最上层的企业级应用,除了功能覆盖面大导致业务复杂以外,往往还需要满足更多苛刻的非功能需求,包括但不限于:用户体验、性能、可靠性、安全性、可伸缩性、可维护性、兼容性。其他一些复杂软件的案例包括:3D游戏界面(交互复杂)极其底层的游戏引擎(逻辑复杂)、超大型CRM系统(一方面是实现很复杂,另一方面,这种成熟软件的标准化程度较高,大部分情况下可以直接用现成的SaaS软件)。
  • • 应用需求量:从上往下,各级别应用的需求体量(Volume)逐级递增,呈现一个金字塔形状。这个特征可以用万能的2/8原则来理解:20%的“全民”应用,由于需求的通用性和普适性,可以覆盖至少80%的用户群体(例如企业大部分人都要用的考勤系统);而剩下那80%的“小众”应用,由于需求的定制化和特殊性(例如蚂蚁的期权系统…),就只能覆盖各自小圈子里那20%的用户了。
  • • 与低代码的契合关系:从上往下,各级别应用与低代码越来越契合(Relevant)。也就是说:越简单的应用,越契合低代码;越不太关键的任务,也越契合低代码。同时,由于契合低代码的应用更偏金字塔底层,而这些应用的需求量都更大,所以可以得出如下判断:低代码能够适用于大部分业务场景(而且这个比例会一直上升,逐步往金字塔的更上层应用逼近),例如:B2E类应用(表单、审批流、ERP系统)、B2B类应用(企业商城、工业控制台)、B2C类应用(企业展示、营销页、店铺装修)。

竞品概况

低代码虽然是一个新兴概念,但这个行业本身并不算很新(前文也有提到),这些年以来早就积累了不少资深的荣耀王者。同时,低代码作为一个朝阳产业和资本热点,近几年也不断有更多的新玩家在加入这个刺激战场。

上图分别是Gartner给出的低代码平台魔力象限和Forrester给出的低代码平台技术波谱。从图中可以看到:

  • • OutSystems和Mendix一马当先,是公认的低代码领域头牌。这两家都是很纯粹的通用低代码开发平台,且都经过了长时间的发展和积累:OutSystems成立于2001年,员工人数1000+,年营收超过1亿美元;2018年6月获得了KKR和高盛的3.6亿美元融资,目前估值超过10亿美元;Mendix成立于2005年,员工人数500+,年营收超过2300万美元(18年数据),2018年8月被西门子以7.3亿美元收购。
  • • Salesforce和Microsoft紧随其后,都处于行业领先者地位。但这两家的公司性质和发展路径都很不一样:Salesforce是以SaaS起家,公司规模就不用多说了,反正就是SaaS届的巨无霸。这类SaaS厂商做低代码的动力,是为了解决客户对成品SaaS软件的定制诉求。M$更不用多介绍,只说下他们做低代码的天然优势:一方面,作为办公软件航空母舰,低代码可以帮助他们的客户实现从Excel表单到定制App的能力与体验升级;另一方面,作为云计算三巨头之一,低代码可以帮助他们连接内部的云计算生态体系,为开发者提供一个统一和易用的上云界面。
  • • 国外市场已经得到充分验证,但国内市场还刚刚兴起,还没有一家能够赢得上述调研机构的芳心,挤进上面这两张方图。国内目前的一些竞品和融资情况包括:2018年5月,搭搭云完成A轮的千万级融资;2018年9月,宜创科技得到清源创投的战略融资;2018年12月,轻流完成千万级Pre-A融资;2019年8月,数式科技得到盈动资本的数千万人民币天使轮融资;2019年8月,ClickPaas获得晨兴资本数百万美元的A轮融资;2019年,奥哲分别获得阿里5千万的A+轮融和高榕资本上亿元的B轮融资。(注:竞品数据来源于我们组PD的辛勤整理;为此我决定这篇文章剩下内容再也不黑PD了;下篇再说。)

六、结语

本文总结了低代码领域的基本概念、核心价值与行业现状。虽然这些内容都比较基础和偏理论,但我始终认为,深刻理解一个系统的前提,正是这些务虚的东西 —— 技术架构只会告诉你这个系统是怎么实现的(How),无法准确表述它到底能用来做什么(What),以及为什么要做这样一个东西(Why);而后面这两个问题的答案,才是后续系统所有设计与演进的根因和驱动力。

原文链接

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

本文转载自: 掘金

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

1…514515516…956

开发者博客

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