Apple-pkl 介绍
在当今快速发展的软件开发领域,配置管理一直是一个重要且挑战性的议题。随着应用程序变得越来越复杂,对配置文件的需求也随之增加,这促使开发者寻找更加高效、安全且易于维护的解决方案。苹果公司在这方面迈出了重要的一步,推出了 Pkl
(a configuration-as-code language with rich validation and tooling)语言,这是一种旨在提供丰富数据模板和验证支持的可嵌入配置语言。在这篇文章中,我们将深入探讨 Pkl 语言的核心特性、设计理念以及它在现代软件开发中的应用前景。
背景
Pkl
语言起源于苹果生态系统对配置文件处理需求的深刻理解,它脱胎于 macOS 和 iOS 系统中广泛使用的.plist 文件格式。自诞生之初,Pkl
就以其简洁明了的键值结构设计和对多种文件格式的支持赢得了开发者的关注。不仅如此,Pkl 还致力于提供一种安全可靠的配置处理方式,通过引入验证机制来确保配置数据的准确性和完整性。
作为一种静态配置文件格式,Pkl
支持 JSON、XML 和 YAML 等流行格式,这意味着开发者可以根据项目需求灵活选择最合适的格式。它的设计哲学中包含了两个核心目标:语法安全性和可扩展性。前者确保了配置文件的语法分析是安全的,减少了因错误的配置而导致的安全风险;后者则体现在 Pkl 支持类、函数、条件和循环等高级编程特性,使其能够应对更加复杂的配置场景。
苹果公司对 Pkl 的开发并没有止步于此。2024 年 2 月 2 日,Apple 公司在 Github 上开源了该项目 Pkl
, 一经发布立即登上了 Github Tending。
初探
定义一个配置文件 bird.pkl
1
2
3
4
5
6
7name = "Swallow"
job {
title = "Sr. Nest Maker"
company = "Nests R Us"
yearsOfExperience = 2
}
这个配置文件等同于
bird.json
1
2
3
4
5
6
7
8{
"name": "Swallow",
"job": {
"title": "Sr. Nest Maker",
"company": "Nests R Us",
"yearsOfExperience": 2
}
}
bird.yaml
1
2
3
4
5name: Swallow
job:
title: Sr. Nest Maker
company: Nests R Us
yearsOfExperience: 2
bird.properties
1
2
3
4name = Swallow
job.title = Sr. Nest Maker
job.company = Nests R Us
job.yearsOfExperience = 2
bird.plist
,流行于 MacOS,IOS 的配置文件格式1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
<plist version="1.0">
<dict>
<key>name</key>
<string>Swallow</string>
<key>job</key>
<dict>
<key>title</key>
<string>Sr. Nest Maker</string>
<key>company</key>
<string>Nests R Us</string>
<key>yearsOfExperience</key>
<integer>2</integer>
</dict>
</dict>
</plist>
上面 4 中文件格式类型,我们可以通过工具将 kpl 转换为指定需求的类型。
代码生成
讲 Pki 的相关依赖嵌入到应用程序时,还会更具.pkl
文件生成相应语言的代码。
例如.pkl
文件如下:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16// Code generated from Pkl module `example.myAppConfig`. DO NOT EDIT.
package myappconfig
import (
"context"
"github.com/apple/pkl-go/pkl"
)
type MyAppConfig struct {
// The hostname for the application
Host string `pkl:"host"`
// The port to listen on
Port uint16 `pkl:"port"`
}
即可自动生成如下语言的文件,后续可能还会扩充
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
33package example;
public final class MyAppConfig {
/**
* The hostname for the application
*/
public final String host;
/**
* The port to listen on
*/
public final int port;
public MyAppConfig(
String host,
int port) {
this.host = host;
this.port = port;
}
public MyAppConfig withHost( { String host)/*...*/ }
public MyAppConfig withPort(int port) { /*...*/ }
public boolean equals(Object obj) { /*...*/ }
public int hashCode() { /*...*/ }
public String toString() { /*...*/ }
}
Kotlin
1
2
3
4
5
6
7
8
9
10
11
12package example
data class MyAppConfig(
/**
* The hostname for the application
*/
val host: String,
/**
* The port to listen on
*/
val port: Int
)
Swift
1
2
3
4
5
6
7
8
9
10
11
12// Code generated from Pkl module `example.myAppConfig`. DO NOT EDIT.
enum MyAppConfig {}
extension MyAppConfig {
struct Module {
/// The hostname for the application
let host: String
/// The port to listen on
let port: UInt16
}
}
Go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16// Code generated from Pkl module `example.myAppConfig`. DO NOT EDIT.
package myappconfig
import (
"context"
"github.com/apple/pkl-go/pkl"
)
type MyAppConfig struct {
// The hostname for the application
Host string `pkl:"host"`
// The port to listen on
Port uint16 `pkl:"port"`
}
IDEA 支持
当前已经支持.pkl
的 IDEA 有,其他正在进行中
- IntelliJ
- Visual Studio Code
- Neovim
开发时校验
在拥有丰富的类型和校验系统支持的情况下,我们可以在开发时就得到配置文件格式和值错误的信息1
2
3email: String = "dev-team@company.com"
port: Int(this > 1000) = 801
2
3
4
5
6
7
8
9
10
11
12
13
14–– Pkl Error ––
Type constraint `this > 1000` violated.
Value: 80
3 | port: Int(this > 1000) = 80
^^^^^^^^^^^
at config#port (config.pkl, line 3)
3 | port: Int(this > 1000) = 80
^^
at config#port (config.pkl, line 3)
106 | text = renderer.renderDocument(value)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
例子 - SpringBoot 集成
pkl-spring
Pkl-Spring 通过 boot-starter 配置将 Pkl 引入到 Spring Boot 中,具体的代码如下
spring.factories
1
2org.springframework.boot.env.PropertySourceLoader=org.pkl.spring.boot.PklPropertySourceLoader
org.springframework.boot.autoconfigure.EnableAutoConfiguration=org.pkl.spring.boot.PklAutoConfiguration
PklPropertySourceLoader
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
49public class PklPropertySourceLoader implements PropertySourceLoader {
public String[] getFileExtensions() {
return new String[] {"pkl", "pcf"};
}
public List<PropertySource<?>> load(String propertySourceName, Resource resource)
throws IOException {
var text = StreamUtils.copyToString(resource.getInputStream(), StandardCharsets.UTF_8);
PModule module;
try (var evaluator = EvaluatorBuilder.preconfigured().build()) {
module = evaluator.evaluate(ModuleSource.create(resource.getURI(), text));
}
var result = new LinkedHashMap<String, Object>();
module.getProperties().forEach((name, value) -> flatten(name, value, result));
return List.of(new MapPropertySource(propertySourceName, result));
}
private static void flatten(
String propertyName, Object propertyValue, Map<String, Object> result) {
if (propertyValue instanceof Composite) {
flatten(propertyName, ((Composite) propertyValue).getProperties(), result);
} else if (propertyValue instanceof Map<?, ?>) {
var map = (Map<?, ?>) propertyValue;
if (map.isEmpty()) {
result.put(propertyName, Collections.emptyMap());
} else {
map.forEach((name, value) -> flatten(propertyName + '.' + name, value, result));
}
} else if (propertyValue instanceof Collection) {
var collection = (Collection<?>) propertyValue;
if (collection.isEmpty()) {
result.put(
propertyName,
propertyValue instanceof Set ? Collections.emptySet() : Collections.emptyList());
} else {
var index = 0;
for (var element : collection) {
flatten(propertyName + '[' + index++ + ']', element, result);
}
}
} else {
result.put(propertyName, propertyValue);
}
}
}
PklAutoConfiguration
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
public class PklAutoConfiguration {
public PklAutoConfiguration(ConfigurableEnvironment env) {
// otherwise `Environment.getProperty("pklPropertyWithNullValue")` fails with
// `ConverterNotFoundException`
env.getConversionService().addConverter(new PNullConverter());
}
public static class PNullConverter implements GenericConverter {
public Set<ConvertiblePair> getConvertibleTypes() {
return Set.of(new ConvertiblePair(PNull.class, Object.class));
}
public Object convert(
{ Object source, TypeDescriptor sourceType, TypeDescriptor targetType)
assert source == PNull.getInstance();
return targetType.getType() == Optional.class ? Optional.empty() : null;
}
}
}
pkl-spring 使用例子
引入依赖
build.gradle.kts
1
2
3dependencies {
compile "org.pkl-lang:pkl-spring:0.15.0"
}
定义配置模型
AppConfig.pkl
1
2
3
4
5
6
7
8
9
10
11
12
13
14// this module name determines the package and
// class name of the generated Java config class
module samples.boot.AppConfig
server: Server
class Server {
endpoints: Listing<Endpoint>
}
class Endpoint {
name: String
port: UInt16
}
定义 Spring Boot 配置文件
application.pkl
1
2
3
4
5
6
7
8
9
10
11
12
13
14amends "modulepath:/appConfig.pkl"
server {
endpoints {
new {
name = "endpoint1"
port = 1234
}
new {
name = "endpoint2"
port = 5678
}
}
}
添加 Pkl 插件和生成配置
build.gradle.kts
1
2
3
4
5
6
7
8
9
10
11
12
13plugins {
id("org.pkl-lang") version "$pklVersion"
}
pkl {
javaCodeGenerators {
register("configClasses") {
generateGetters.set(true)
generateSpringBootConfig.set(true)
sourceModules.set(files("src/main/resources/AppConfig.pkl"))
}
}
}
Spring Boot 中代码配置
Application.java
1
2
3
public class Application { ... }
Service.java
这里就可以直接注入和使用 AppConfig
1
2
3
4
public class Service {
public Service(AppConfig.Server config) { ... }
}
Service1.java
1
2
3
4
public class Service1 {
public Service1(AppConfig config) { ... }
}