基于google Bazel 编译和打包springboot项目

Bazel

google开源的一个多语言编译工具,可以编译cpp、go、java等语言,通过定义WORKSPACE和BUILD文件进行编译。 官网地址如下: https://docs.bazel.build/versions/4.2.2/bazel-overview.html 使用bazel编译大的代码仓库会有比较多的好处,官网有更具体的介绍,而java使用bazel编译,由于文章比较少,由于工作需要,记录一下一个springboot项目的编译过程

基于bazel的Linux远程开发环境

创建springboot项目

通过idea的spring初始化项目

image.png

该项目主要使用springboot + lombok,所以参考maven如下:

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
	xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
	<modelVersion>4.0.0</modelVersion>
	<parent>
		<groupId>org.springframework.boot</groupId>
		<artifactId>spring-boot-starter-parent</artifactId>
		<version>2.6.2</version>
		<relativePath/> <!-- lookup parent from repository -->
	</parent>
	<groupId>com.haokaizhao</groupId>
	<artifactId>demo</artifactId>
	<version>0.0.1-SNAPSHOT</version>
	<name>demo</name>
	<description>Demo project for Spring Boot</description>
	<properties>
		<java.version>1.8</java.version>
	</properties>
	<dependencies>
		<dependency>
			<groupId>org.springframework.boot</groupId>
			<artifactId>spring-boot-starter-web</artifactId>
		</dependency>

		<dependency>
			<groupId>org.projectlombok</groupId>
			<artifactId>lombok</artifactId>
			<optional>true</optional>
		</dependency>
		<dependency>
			<groupId>org.springframework.boot</groupId>
			<artifactId>spring-boot-starter-test</artifactId>
			<scope>test</scope>
		</dependency>
	</dependencies>

	<build>
		<plugins>
			<plugin>
				<groupId>org.springframework.boot</groupId>
				<artifactId>spring-boot-maven-plugin</artifactId>
				<configuration>
					<excludes>
						<exclude>
							<groupId>org.projectlombok</groupId>
							<artifactId>lombok</artifactId>
						</exclude>
					</excludes>
				</configuration>
			</plugin>
		</plugins>
	</build>

</project>

添加相应实现代码

代码结构如下 image.png

// HelloController.java
@RestController
public class HelloController {


    @Autowired
    private HelloService service;

    @GetMapping("/hello")
    public HelloDTO hello() {
        return service.hello();
    }
}
// HelloDTO.java
@Data
@Accessors(chain = true)
public class HelloDTO {

    private String userName;
    private String password;
    private String msg;
}
// HelloService.java
@Service
public class HelloService {

    public HelloDTO hello() {
        return new HelloDTO()
                .setMsg("hello")
                .setPassword("123456")
                .setUserName("hk");
    }
}
// HelloServiceTest.java
@SpringBootTest
public class HelloServiceTest {

    @Autowired
    private HelloService service;

    @Test
    public void testHello() {
        HelloDTO hello = service.hello();
        Assert.notNull(hello);
        Assertions.assertEquals("123456", hello.getPassword());
    }

}

添加完相应代码后,运行项目

image.png

接口也有相应返回

bash-4.4# curl http://127.0.0.1:8080/hello
{"userName":"hk","password":"123456","msg":"hello"}

单测也能够pass

image.png

基于Bazel改造maven项目

首先参考bazel的教程的java-maven项目,地址如下:https://github.com/bazelbuild/examples 的java-maven项目,主要关注两个文件,一个WORKSPACE文件,一个BUILD文件

WORKSPACE文件

load("@bazel_tools//tools/build_defs/repo:http.bzl", "http_archive")

RULES_JVM_EXTERNAL_TAG = "2.5"

RULES_JVM_EXTERNAL_SHA = "249e8129914be6d987ca57754516be35a14ea866c616041ff0cd32ea94d2f3a1"

http_archive(
    name = "rules_jvm_external",
    sha256 = RULES_JVM_EXTERNAL_SHA,
    strip_prefix = "rules_jvm_external-%s" % RULES_JVM_EXTERNAL_TAG,
    url = "https://github.com/bazelbuild/rules_jvm_external/archive/%s.zip" % RULES_JVM_EXTERNAL_TAG,
)

load("@rules_jvm_external//:defs.bzl", "maven_install")

maven_install(
    artifacts = [
        "junit:junit:4.12",
        "com.google.guava:guava:28.0-jre",
    ],
    fetch_sources = True,
    repositories = [
        "http://uk.maven.org/maven2",
        "https://jcenter.bintray.com/",
    ],

BUILD文件

load("@rules_java//java:defs.bzl", "java_binary", "java_library", "java_test")

package(default_visibility = ["//visibility:public"])

java_library(
    name = "java-maven-lib",
    srcs = glob(["src/main/java/com/example/myproject/*.java"]),
    deps = ["@maven//:com_google_guava_guava"],
)

java_binary(
    name = "java-maven",
    main_class = "com.example.myproject.App",
    runtime_deps = [":java-maven-lib"],
)

java_test(
    name = "tests",
    srcs = glob(["src/test/java/com/example/myproject/*.java"]),
    test_class = "com.example.myproject.TestApp",
    deps = [
        ":java-maven-lib",
        "@maven//:com_google_guava_guava",
        "@maven//:junit_junit",
    ],
)

我们先看WORKSPACE文件,参考官网给出的定义

image.png 直白点说,WORKSPACE一个项目只会有一个这样的文件,他必须在该项目的跟目录中,用于标识Bazel的工作区,执行bazel的功能,同时所有外部依赖共用一个WORKSPACE。也就是说我们现在需要的maven依赖可以在WORKSPACE中声明。 @load(),作用就是从远程rule的仓库中或者本地写的一些rule中,加载一个自定义的功能。bazel是可以自定义一些功能的实现的,可以自定义bzl文件,类似于以下实现,实现的语言是python的一个子集,这一部分不做介绍(没怎么写过)具体连接如下:https://docs.bazel.build/versions/4.2.2/skylark/bzl-style.html

def fct(name, srcs):
    filtered_srcs = my_filter(source = srcs)
    native.cc_library(
        name = name,
        srcs = filtered_srcs,
        testonly = True,
    )                                           

java-maven的WORKSPACE文件的作用就是从远程仓库load了一个maven_install的这个功能,用于maven依赖的拉取。

然后再看BUILD文件,BUILD文件其实就是类似CMAKE的一种形式吧,声明哪个文件依赖于哪个文件,然后进行编译,用maven_install拉取下来的依赖需要用@maven//:标识 上述的BUILD文件包含3个功能 java_library: 声明了项目中所有依赖到的文件 java_binary: 声明了项目需要编译的一个二进制文件,也就是可执行文件(类似于cmake的add_executable) java_test: 声明了项目中的测试文件

简单了解了bazel的机制之后,我们来改造WORKSPACE和BUILD 根据maven的依赖,我们来改造WORKSPACE文件

load("@bazel_tools//tools/build_defs/repo:http.bzl", "http_archive")

RULES_JVM_EXTERNAL_TAG = "2.5"

RULES_JVM_EXTERNAL_SHA = "249e8129914be6d987ca57754516be35a14ea866c616041ff0cd32ea94d2f3a1"

http_archive(
    name = "rules_jvm_external",
    sha256 = RULES_JVM_EXTERNAL_SHA,
    strip_prefix = "rules_jvm_external-%s" % RULES_JVM_EXTERNAL_TAG,
    url = "https://github.com/bazelbuild/rules_jvm_external/archive/%s.zip" % RULES_JVM_EXTERNAL_TAG,
)

load("@rules_jvm_external//:defs.bzl", "maven_install")

maven_install(
    artifacts = [
        "org.springframework.boot:spring-boot-starter-web:2.6.2",
        "org.springframework.boot:spring-boot-starter-test:2.6.2",
        "org.projectlombok:lombok:1.18.22"
    ],
    fetch_sources = True,
    repositories = [
        "http://uk.maven.org/maven2",
        "https://jcenter.bintray.com/",
    ],
)

将pom中声明的3个依赖添加到maven_install的artifacts中

然后构造BUILD文件,也是同样的过程将我们代码中的依赖通过deps的参数声明上去

load("@rules_java//java:defs.bzl", "java_binary", "java_library", "java_test")

package(default_visibility = ["//visibility:public"])


java_library(
    name = "java-maven-lib",
    srcs = glob(["src/main/java/com/hk/demo/*.java",
                "src/main/java/com/hk/demo/controller/*.java",
                "src/main/java/com/hk/demo/service/*.java",
                "src/main/java/com/hk/demo/dao/*.java"]),
    deps = ["@maven//:org_springframework_boot_spring_boot_starter_web",
            "@maven//:org_projectlombok_lombok",
            ],
)

java_binary(
    name = "java-maven",
    main_class = "com.hk.demo.DemoApplication",
    runtime_deps = [":java-maven-lib"],
)

java_test(
    name = "tests",
    srcs = glob(["src/test/java/com/hk/demo/*.java"]),
    test_class = "com.hk.demo.DemoApplicationTests",
    deps = [
        ":java-maven-lib",
        "@maven//:org_springframework_boot_spring_boot_starter_test",
    ],
)

然后执行编译

#在WORKSPACE的同级目录下
bash-4.4# tree
.
|-- BUILD
|-- HELP.md
|-- WORKSPACE
bash-4.4# bazel run //:java-maven

bazel的编译结果如下:

//部分报错结果如下
@SpringBootApplication
 ^
src/main/java/com/hk/demo/DemoApplication.java:10: error: [strict] Using type org.springframework.boot.SpringApplication from an indirect dependency (TOOL_INFO: "external/maven/v1/https/jcenter.bintray.com/org/springframework/boot/spring-boot/2.6.2/spring-boot-2.6.2.jar").
                SpringApplication.run(DemoApplication.class, args);
                ^
src/main/java/com/hk/demo/controller/HelloController.java:9: error: [strict] Using type org.springframework.web.bind.annotation.RestController from an indirect dependency (TOOL_INFO: "external/maven/v1/https/jcenter.bintray.com/org/springframework/spring-web/5.3.14/spring-web-5.3.14.jar").
@RestController
 ^
src/main/java/com//demo/controller/HelloController.java:13: error: [strict] Using type org.springframework.beans.factory.annotation.Autowired from an indirect dependency (TOOL_INFO: "external/maven/v1/https/jcenter.bintray.com/org/springframework/spring-beans/5.3.14/spring-beans-5.3.14.jar").
    @Autowired
     ^
src/main/java/com/hk/demo/service/HelloService.java:6: error: [strict] Using type org.springframework.stereotype.Service from an indirect dependency (TOOL_INFO: "external/maven/v1/https/jcenter.bintray.com/org/springframework/spring-context/5.3.14/spring-context-5.3.14.jar").
@Service

经过查看报错信息,间接依赖对bazel来说也是要声明的,所以修改BUILD文件 将报错信息中的那些缺失jar包补充到deps中

load("@rules_java//java:defs.bzl", "java_binary", "java_library", "java_test")

package(default_visibility = ["//visibility:public"])

java_library(
    name = "java-maven-lib",
    srcs = glob(["src/main/java/com/hk/demo/*.java",
                "src/main/java/com/hk/demo/controller/*.java",
                "src/main/java/com/hk/demo/service/*.java",
                "src/main/java/com/hk/demo/dao/*.java"]),
    deps = ["@maven//:org_springframework_boot_spring_boot_starter_web",
            "@maven//:org_springframework_boot_spring_boot",
            "@maven//:org_springframework_boot_spring_boot_autoconfigure",
            "@maven//:org_projectlombok_lombok",
            "@maven//:org_springframework_spring_beans",
            "@maven//:org_springframework_spring_web",
            "@maven//:org_springframework_spring_context"
            ],
)

java_binary(
    name = "java-maven",
    main_class = "com.hk.demo.DemoApplication",
    runtime_deps = [":java-maven-lib"],
)

java_test(
    name = "tests",
    srcs = glob(["src/test/java/com/hk/demo/*.java"]),
    test_class = "com.hk.demo.DemoApplicationTests",
    deps = [
        ":java-maven-lib",
        "@maven//:org_springframework_boot_spring_boot_starter_test",
    ],
)

我们再执行编译,结果如下

src/main/java/com/hk/demo/service/HelloService.java:11: error: cannot find symbol
                .setMsg("hello")
                ^
  symbol:   method setMsg(String)
  location: class HelloDTO
Target //:java-maven failed to build

其实这里是挺坑的,因为lombok这个库,其实是一个改字节码的库,就是说通过添加注解的方式去实现生成的class文件有对应的功能的实现,maven编译的情况下我们是不需要做什么处理的,但是在bazel下,就得做一些特殊处理,不然生成的class是没有对应的功能的,这里有两种解决办法,另外一种我没有尝试,具体解决方法如下: github.com/bazelbuild/… 有兴趣的可以去看一下通过定义bzl的解决方式

根据issue中的解决办法,修改BUILD文件

load("@rules_java//java:defs.bzl", "java_binary", "java_library", "java_test")

package(default_visibility = ["//visibility:public"])

java_import(
    name = "lombok_jar",
    jars = [
        "@maven//:v1/https/jcenter.bintray.com/org/projectlombok/lombok/1.18.22/lombok-1.18.22.jar"
    ],
)

java_plugin(
    name = "lombok_plugin",
    processor_class = "lombok.launch.AnnotationProcessorHider$AnnotationProcessor",
    deps = [
        ":lombok_jar",
    ],
)

java_library(
    name = "lombok",
    exports = [
        "@maven//:org_projectlombok_lombok",
    ],
    exported_plugins = [
        ":lombok_plugin"
    ],
)


java_library(
    name = "java-maven-lib",
    srcs = glob(["src/main/java/com/hk/demo/*.java",
                "src/main/java/com/hk/demo/controller/*.java",
                "src/main/java/com/hk/demo/service/*.java",
                "src/main/java/com/hk/demo/dao/*.java"]),
    deps = ["@maven//:org_springframework_boot_spring_boot_starter_web",
            "@maven//:org_springframework_boot_spring_boot",
            "@maven//:org_springframework_boot_spring_boot_autoconfigure",
            ":lombok",
            "@maven//:org_springframework_spring_beans",
            "@maven//:org_springframework_spring_web",
            "@maven//:org_springframework_spring_context"
            ],
)

java_binary(
    name = "java-maven",
    main_class = "com.hk.demo.DemoApplication",
    runtime_deps = [":java-maven-lib"],
)

java_test(
    name = "tests",
    srcs = glob(["src/test/java/com/hk/demo/*.java"]),
    test_class = "com.hk.demo.DemoApplicationTests",
    deps = [
        ":java-maven-lib",
        "@maven//:org_springframework_boot_spring_boot_starter_test",
    ],
)

这种方式其实就是指定了一个在java编译时运行的一个插件,插件通过process_class指定,然后就可以解决之前bazel编译出来的class没有@Data的功能的问题。具体的java的rules的作用参考这个: docs.bazel.build/versions/ma…

我们再执行编译

bash-4.4# bazel run //:java-maven
INFO: Analyzed target //:java-maven (0 packages loaded, 0 targets configured).
INFO: Found 1 target...
Target //:java-maven up-to-date:
  bazel-bin/java-maven.jar
  bazel-bin/java-maven
INFO: Elapsed time: 0.050s, Critical Path: 0.00s
INFO: 1 process: 1 internal.
INFO: Build completed successfully, 1 total action
INFO: Build completed successfully, 1 total action

  .   ____          _            __ _ _
 /\\ / ___'_ __ _ _(_)_ __  __ _ \ \ \ \
( ( )\___ | '_ | '_| | '_ \/ _` | \ \ \ \
 \\/  ___)| |_)| | | | | || (_| |  ) ) ) )
  '  |____| .__|_| |_|_| |_\__, | / / / /
 =========|_|==============|___/=/_/_/_/
 :: Spring Boot ::                (v2.6.2)

2022-01-16 10:10:29.067  INFO 43591 --- [           main] com.haokaizhao.demo.DemoApplication      : Starting DemoApplication using Java 1.8.0_312 on 3275efc5b377 with PID 43591 (/usr/.cache/bazel/_bazel_root/d53f91814e84f121c225441a8f9fbc52/execroot/__main__/bazel-out/k8-fastbuild/bin/libjava-maven-lib.jar started by root in /usr/.cache/bazel/_bazel_root/d53f91814e84f121c225441a8f9fbc52/execroot/__main__/bazel-out/k8-fastbuild/bin/java-maven.runfiles/__main__)
2022-01-16 10:10:29.069  INFO 43591 --- [           main] com.haokaizhao.demo.DemoApplication      : No active profile set, falling back to default profiles: default
2022-01-16 10:10:29.753  INFO 43591 --- [           main] o.s.b.w.embedded.tomcat.TomcatWebServer  : Tomcat initialized with port(s): 8080 (http)
2022-01-16 10:10:29.761  INFO 43591 --- [           main] o.apache.catalina.core.StandardService   : Starting service [Tomcat]
2022-01-16 10:10:29.762  INFO 43591 --- [           main] org.apache.catalina.core.StandardEngine  : Starting Servlet engine: [Apache Tomcat/9.0.56]
2022-01-16 10:10:29.805  INFO 43591 --- [           main] o.a.c.c.C.[Tomcat].[localhost].[/]       : Initializing Spring embedded WebApplicationContext
2022-01-16 10:10:29.805  INFO 43591 --- [           main] w.s.c.ServletWebServerApplicationContext : Root WebApplicationContext: initialization completed in 695 ms
2022-01-16 10:10:30.059  INFO 43591 --- [           main] o.s.b.w.embedded.tomcat.TomcatWebServer  : Tomcat started on port(s): 8080 (http) with context path ''
2022-01-16 10:10:30.067  INFO 43591 --- [           main] com.hk.demo.DemoApplication      : Started DemoApplication in 1.273 seconds (JVM running for 1.498)

然后就跑成功了~

这样就完成了一个maven项目到bazel项目的改造~

总结

bazel的编译速度确实是要比maven快的,但是bazel-java的环境或者是一些问题的解决都比较少,需要不断的试才能试出来,学习成本比maven要大很多。不过bazel确实是个很强大的编译工具


作者:添腹一饼
链接:基于google Bazel 编译和打包springboot项目 - 掘金