บทแนะนำ Bazel: สร้างโปรเจ็กต์ Java

รายงานปัญหา ดูแหล่งที่มา

บทแนะนำนี้ครอบคลุมพื้นฐานการสร้างแอปพลิเคชัน Java ด้วย Bazel คุณจะต้องตั้งค่าพื้นที่ทำงานและสร้างโปรเจ็กต์ Java แบบง่ายๆ ที่แสดงให้เห็นแนวคิดหลักของ Bazel เช่น เป้าหมายและไฟล์ BUILD

เวลาที่ใช้ดำเนินการจนเสร็จโดยประมาณ: 30 นาที

สิ่งที่คุณจะได้เรียนรู้

ในบทแนะนำนี้ คุณจะได้เรียนรู้วิธีต่อไปนี้

  • สร้างเป้าหมาย
  • แสดงภาพทรัพยากร Dependency ของโปรเจ็กต์
  • แบ่งโปรเจ็กต์ออกเป็นเป้าหมายและแพ็กเกจหลายรายการ
  • ควบคุมระดับการเข้าถึงเป้าหมายในแพ็กเกจ
  • อ้างอิงเป้าหมายผ่านป้ายกำกับ
  • ทำให้เป้าหมายใช้งานได้

ก่อนเริ่มต้น

ติดตั้ง Bazel

ในการเตรียมพร้อมสำหรับบทแนะนำ ให้ติดตั้ง Bazel หากยังไม่ได้ติดตั้ง

ติดตั้ง JDK

  1. ติดตั้ง Java JDK (เวอร์ชันที่แนะนำคือ 11 แต่รองรับเวอร์ชัน 8 ถึง 15)

  2. ตั้งค่าตัวแปรสภาพแวดล้อม JAVA_HOME ให้ชี้ไปที่ JDK

    • ใน Linux/macOS:

      export JAVA_HOME="$(dirname $(dirname $(realpath $(which javac))))"
      
    • บน Windows:

      1. เปิดแผงควบคุม
      2. ไปที่ "ระบบและความปลอดภัย" > "ระบบ" > "การตั้งค่าระบบขั้นสูง" > แท็บ "ขั้นสูง" > "ตัวแปรสภาพแวดล้อม..."
      3. ในรายการ "ตัวแปรผู้ใช้" (ตัวแปรที่ด้านบน) ให้คลิก "ใหม่..."
      4. ในช่อง "ชื่อตัวแปร" ให้ป้อน JAVA_HOME
      5. คลิก "เรียกดูไดเรกทอรี..."
      6. ไปที่ไดเรกทอรี JDK (เช่น C:\Program Files\Java\jdk1.8.0_152)
      7. คลิก "ตกลง" บนหน้าต่างกล่องโต้ตอบทั้งหมด

รับโปรเจ็กต์ตัวอย่าง

เรียกข้อมูลโปรเจ็กต์ตัวอย่างจากที่เก็บ GitHub ของ Bazel:

git clone https://github.com/bazelbuild/examples

โปรเจ็กต์ตัวอย่างสำหรับบทแนะนำนี้อยู่ในไดเรกทอรี examples/java-tutorial และมีโครงสร้างดังนี้

java-tutorial
├── BUILD
├── src
│   └── main
│       └── java
│           └── com
│               └── example
│                   ├── cmdline
│                   │   ├── BUILD
│                   │   └── Runner.java
│                   ├── Greeting.java
│                   └── ProjectRunner.java
└── WORKSPACE

สร้างด้วย Bazel

ตั้งค่าพื้นที่ทำงาน

คุณต้องตั้งค่าพื้นที่ทำงานของโปรเจ็กต์ก่อนจึงจะสร้างโปรเจ็กต์ได้ พื้นที่ทำงานคือไดเรกทอรีที่เก็บไฟล์ต้นฉบับของโปรเจ็กต์และเอาต์พุตบิลด์ของ Bazel นอกจากนี้ยังมีไฟล์ที่ Bazel มองว่าเป็นแบบพิเศษ เช่น

  • ไฟล์ WORKSPACE ซึ่งระบุไดเรกทอรีและเนื้อหาในไดเรกทอรีเป็นพื้นที่ทำงานแบบบาเซล และอยู่ที่รูทของโครงสร้างไดเรกทอรีของโปรเจ็กต์

  • ไฟล์ BUILD อย่างน้อย 1 ไฟล์ที่ใช้บอกวิธีการสร้างส่วนต่างๆ ของโปรเจ็กต์ที่ Bazel (ไดเรกทอรีภายในพื้นที่ทำงานที่มีไฟล์ BUILD คือแพ็กเกจ คุณจะได้เรียนรู้เกี่ยวกับแพ็กเกจในภายหลังในบทแนะนำนี้)

หากต้องการกำหนดไดเรกทอรีเป็นพื้นที่ทำงาน Bazel ให้สร้างไฟล์เปล่าชื่อ WORKSPACE ในไดเรกทอรีนั้น

เมื่อ Bazel สร้างโปรเจ็กต์ อินพุตและการอ้างอิงทั้งหมดต้องอยู่ในพื้นที่ทำงานเดียวกัน ไฟล์ที่อยู่ในพื้นที่ทำงานต่างๆ จะไม่เป็นอิสระจากกัน เว้นแต่จะลิงก์กันซึ่งอยู่นอกเหนือขอบเขตของบทแนะนำนี้

ทำความเข้าใจไฟล์ BUILD

ไฟล์ BUILD มีวิธีการประเภทต่างๆ สำหรับ Bazel ประเภทที่สำคัญที่สุดคือกฎการสร้าง ซึ่งจะบอก Bazel ถึงวิธีสร้างเอาต์พุตที่ต้องการ เช่น ไบนารีหรือไลบรารีที่สั่งการได้ อินสแตนซ์แต่ละรายการของกฎบิลด์ในไฟล์ BUILD จะเรียกว่าเป้าหมาย และชี้ไปยังชุดไฟล์ต้นทางและการอ้างอิงที่เฉพาะเจาะจง เป้าหมายยังสามารถชี้ไปยัง เป้าหมายอื่นๆ ได้ด้วย

ดูไฟล์ java-tutorial/BUILD

java_binary(
    name = "ProjectRunner",
    srcs = glob(["src/main/java/com/example/*.java"]),
)

ในตัวอย่างของเรา เป้าหมาย ProjectRunner จะสร้างอินสแตนซ์ของกฎ java_binary ในตัวของ Bazel กฎนี้จะบอกให้ Bazel สร้างไฟล์ .jar และสคริปต์เชลล์ Wrapper (ตั้งชื่อตามเป้าหมายทั้งคู่)

แอตทริบิวต์ในเป้าหมายระบุการอ้างอิงและตัวเลือกไว้อย่างชัดเจน แม้ว่าแอตทริบิวต์ name จะเป็นแอตทริบิวต์ที่ต้องระบุ แต่ก็มีหลายรายการที่จะระบุหรือไม่ก็ได้ ตัวอย่างเช่น ในเป้าหมายกฎ ProjectRunner name คือชื่อของเป้าหมาย srcs จะระบุไฟล์ต้นทางที่ Bazel ใช้ในการสร้างเป้าหมาย และ main_class ระบุคลาสที่มีเมธอดหลัก (คุณอาจสังเกตเห็นว่าตัวอย่างของเราใช้ glob เพื่อส่งชุดไฟล์ต้นฉบับไปยัง Bazel แทนที่จะแสดงทีละไฟล์)

สร้างโปรเจ็กต์

หากต้องการสร้างโปรเจ็กต์ตัวอย่าง ให้ไปที่ไดเรกทอรี java-tutorial แล้วเรียกใช้ดังนี้

bazel build //:ProjectRunner

ในป้ายกำกับเป้าหมาย ส่วน // คือตำแหน่งของไฟล์ BUILD ที่สัมพันธ์กับรูทของพื้นที่ทำงาน (ในกรณีนี้คือรูท) และ ProjectRunner เป็นชื่อเป้าหมายในไฟล์ BUILD (ดูรายละเอียดเพิ่มเติมในตอนท้ายของบทแนะนำนี้)

Bazel ให้ผลผลิตดังต่อไปนี้

   INFO: Found 1 target...
   Target //:ProjectRunner up-to-date:
      bazel-bin/ProjectRunner.jar
      bazel-bin/ProjectRunner
   INFO: Elapsed time: 1.021s, Critical Path: 0.83s

ยินดีด้วย คุณเพิ่งสร้างเป้าหมาย Bazel แรก! Bazel จะวางเอาต์พุตบิลด์ในไดเรกทอรี bazel-bin ที่รูทของพื้นที่ทำงาน เรียกดูเนื้อหาต่างๆ เพื่อหาแนวคิดเกี่ยวกับโครงสร้างเอาต์พุตของ Bazel

ตอนนี้ให้ทดสอบไบนารีที่สร้างใหม่

bazel-bin/ProjectRunner

ดูกราฟการอ้างอิง

Bazel กำหนดให้ต้องมีการประกาศทรัพยากร Dependency ของบิลด์อย่างชัดเจนในไฟล์ BUILD Bazel จะใช้คำสั่งเหล่านั้นเพื่อสร้างกราฟการอ้างอิงของโปรเจ็กต์ ซึ่งช่วยให้สร้างบิลด์ส่วนเพิ่มที่แม่นยำได้

หากต้องการแสดงภาพทรัพยากร Dependency ของโปรเจ็กต์ตัวอย่าง ให้สร้างการแสดงข้อความของกราฟการอ้างอิงโดยเรียกใช้คำสั่งนี้ที่รูทของพื้นที่ทำงาน

bazel query  --notool_deps --noimplicit_deps "deps(//:ProjectRunner)" --output graph

คำสั่งด้านบนจะบอกให้ Bazel ค้นหาทรัพยากร Dependency ทั้งหมดสำหรับเป้าหมาย //:ProjectRunner (ยกเว้นทรัพยากร Dependency ของโฮสต์และการอ้างอิงโดยนัย) และจัดรูปแบบเอาต์พุตเป็นกราฟ

จากนั้นวางข้อความลงใน GraphViz

คุณจะเห็นว่าโปรเจ็กต์มีเป้าหมายเดียวที่สร้างไฟล์ต้นฉบับ 2 ไฟล์โดยไม่มีทรัพยากร Dependency เพิ่มเติม ดังนี้

กราฟการขึ้นต่อกันของ "ProjectRunner" เป้าหมาย

หลังจากตั้งค่าพื้นที่ทำงานแล้ว ให้สร้างโปรเจ็กต์ และตรวจสอบการขึ้นต่อกันของพื้นที่ทำงาน จากนั้นคุณอาจเพิ่มความซับซ้อนลงไปได้

ปรับแต่งบิลด์ Bazel

แม้ว่าเป้าหมายเดียวก็เพียงพอแล้วสำหรับโปรเจ็กต์ขนาดเล็ก แต่คุณอาจต้องแบ่งโปรเจ็กต์ขนาดใหญ่ออกเป็นหลายเป้าหมายและแพ็กเกจหลายแพ็กเกจเพื่อให้สร้างงานส่วนเพิ่มได้อย่างรวดเร็ว (กล่าวคือ สร้างเฉพาะสิ่งที่เปลี่ยนแปลงใหม่เท่านั้น) และเร่งการสร้างโปรเจ็กต์โดยสร้างหลายส่วนของโปรเจ็กต์พร้อมกัน

ระบุเป้าหมายบิลด์หลายรายการ

คุณสามารถแยกบิลด์ของโปรเจ็กต์ตัวอย่างออกเป็น 2 เป้าหมาย แทนที่เนื้อหาของไฟล์ java-tutorial/BUILD ด้วยข้อมูลต่อไปนี้

java_binary(
    name = "ProjectRunner",
    srcs = ["src/main/java/com/example/ProjectRunner.java"],
    main_class = "com.example.ProjectRunner",
    deps = [":greeter"],
)

java_library(
    name = "greeter",
    srcs = ["src/main/java/com/example/Greeting.java"],
)

ด้วยการกำหนดค่านี้ Bazel จะสร้างไลบรารี greeter ก่อน จากนั้นจึงสร้างไบนารี ProjectRunner แอตทริบิวต์ deps ใน java_binary บอก Bazel ว่าไลบรารี greeter จำเป็นต้องใช้เพื่อสร้างไบนารี ProjectRunner

หากต้องการสร้างโปรเจ็กต์เวอร์ชันใหม่นี้ ให้เรียกใช้คำสั่งต่อไปนี้

bazel build //:ProjectRunner

Bazel ให้ผลผลิตดังต่อไปนี้

INFO: Found 1 target...
Target //:ProjectRunner up-to-date:
  bazel-bin/ProjectRunner.jar
  bazel-bin/ProjectRunner
INFO: Elapsed time: 2.454s, Critical Path: 1.58s

ตอนนี้ให้ทดสอบไบนารีที่สร้างใหม่

bazel-bin/ProjectRunner

หากตอนนี้หากคุณแก้ไข ProjectRunner.java และสร้างโปรเจ็กต์อีกครั้ง Bazel ก็แค่คอมไพล์ไฟล์อีกครั้งเท่านั้น

เมื่อดูกราฟการอ้างอิง คุณจะเห็นว่า ProjectRunner อาศัยอินพุตเดียวกันกับที่ใช้ก่อนหน้านี้ แต่โครงสร้างของบิลด์นั้นแตกต่างกัน ดังนี้

กราฟการขึ้นต่อกันของ "ProjectRunner" เป้าหมายหลังจากเพิ่มทรัพยากร Dependency

ตอนนี้คุณได้สร้างโปรเจ็กต์ที่มี 2 เป้าหมายแล้ว เป้าหมาย ProjectRunner จะสร้างไฟล์แหล่งที่มา 2 ไฟล์และขึ้นอยู่กับเป้าหมายอีก 1 อย่าง (:greeter) ซึ่งสร้างไฟล์ต้นฉบับเพิ่มอีก 1 ไฟล์

ใช้หลายแพ็กเกจ

ต่อไปเราจะแบ่งโปรเจ็กต์ออกเป็นหลายแพ็กเกจ หากดูไดเรกทอรี src/main/java/com/example/cmdline คุณจะเห็นว่าไดเรกทอรีดังกล่าวมีไฟล์ BUILD รวมถึงไฟล์ต้นฉบับบางรายการด้วย ดังนั้นสำหรับ Bazel พื้นที่ทำงานจึงมี 2 แพ็กเกจ คือ //src/main/java/com/example/cmdline และ // (เนื่องจากมีไฟล์ BUILD ที่รูทของพื้นที่ทำงาน)

ดูไฟล์ src/main/java/com/example/cmdline/BUILD

java_binary(
    name = "runner",
    srcs = ["Runner.java"],
    main_class = "com.example.cmdline.Runner",
    deps = ["//:greeter"],
)

เป้าหมาย runner ขึ้นอยู่กับเป้าหมาย greeter ในแพ็กเกจ // (ดังนั้นป้ายกำกับเป้าหมาย //:greeter) - Bazel รู้เรื่องนี้ผ่านแอตทริบิวต์ deps ดูกราฟการอ้างอิงต่อไปนี้

กราฟการขึ้นต่อกันของ "นักวิ่ง" เป้าหมาย

อย่างไรก็ตาม คุณต้องกำหนดระดับการเข้าถึง runner ใน //src/main/java/com/example/cmdline/BUILD ให้กับเป้าหมายใน //BUILD อย่างชัดแจ้งโดยใช้แอตทริบิวต์ visibility เพื่อให้บิลด์ประสบความสำเร็จ เนื่องจากโดยค่าเริ่มต้น เป้าหมายอื่นจะมองเห็นได้ในไฟล์ BUILD เดียวกันเท่านั้น (Bazel ใช้ระดับการเข้าถึงเป้าหมายเพื่อป้องกันปัญหาต่างๆ เช่น ไลบรารีที่มีรายละเอียดการใช้งานรั่วไหลเข้าสู่ API สาธารณะ)

โดยเพิ่มแอตทริบิวต์ visibility ลงในเป้าหมาย greeter ใน java-tutorial/BUILD ดังที่แสดงด้านล่าง

java_library(
    name = "greeter",
    srcs = ["src/main/java/com/example/Greeting.java"],
    visibility = ["//src/main/java/com/example/cmdline:__pkg__"],
)

ตอนนี้คุณสามารถสร้างแพ็กเกจใหม่ได้โดยเรียกใช้คำสั่งต่อไปนี้ที่รูทของพื้นที่ทำงาน

bazel build //src/main/java/com/example/cmdline:runner

Bazel ให้ผลผลิตดังต่อไปนี้

INFO: Found 1 target...
Target //src/main/java/com/example/cmdline:runner up-to-date:
  bazel-bin/src/main/java/com/example/cmdline/runner.jar
  bazel-bin/src/main/java/com/example/cmdline/runner
  INFO: Elapsed time: 1.576s, Critical Path: 0.81s

ตอนนี้ให้ทดสอบไบนารีที่สร้างใหม่

./bazel-bin/src/main/java/com/example/cmdline/runner

ตอนนี้คุณได้แก้ไขโปรเจ็กต์เพื่อสร้างเป็น 2 แพ็กเกจ โดยแต่ละแพ็กเกจมี 1 เป้าหมาย และทำความเข้าใจเกี่ยวกับทรัพยากร Dependency ของแพ็กเกจเหล่านี้

ใช้ป้ายกำกับเพื่ออ้างอิงเป้าหมาย

ในไฟล์ BUILD และในบรรทัดคำสั่ง Bazel จะใช้ป้ายกำกับเป้าหมายเพื่ออ้างอิงเป้าหมาย เช่น //:ProjectRunner หรือ //src/main/java/com/example/cmdline:runner ไวยากรณ์ของฟังก์ชันเหล่านั้นเป็นดังนี้

//path/to/package:target-name

หากเป้าหมายเป็นเป้าหมายของกฎ path/to/package จะเป็นเส้นทางไปยังไดเรกทอรีที่มีไฟล์ BUILD และ target-name คือสิ่งที่คุณตั้งชื่อเป้าหมายในไฟล์ BUILD (แอตทริบิวต์ name) หากเป้าหมายคือเป้าหมายไฟล์ path/to/package คือเส้นทางไปยังรูทของแพ็กเกจ และ target-name คือชื่อไฟล์เป้าหมาย รวมถึงเส้นทางแบบเต็ม

เมื่ออ้างอิงเป้าหมายที่รูทของที่เก็บ เส้นทางแพ็กเกจจะว่างเปล่า เพียงใช้ //:target-name เมื่ออ้างอิงเป้าหมายภายในไฟล์ BUILD เดียวกัน คุณจะข้ามตัวระบุรูทของพื้นที่ทำงาน // ไปและใช้ :target-name แทนก็ได้

ตัวอย่างเช่น สำหรับเป้าหมายในไฟล์ java-tutorial/BUILD คุณไม่จำเป็นต้องระบุเส้นทางแพ็กเกจ เนื่องจากรูทของพื้นที่ทำงานเป็นแพ็กเกจ (//) และป้ายกำกับเป้าหมายทั้ง 2 รายการมีเพียง //:ProjectRunner และ //:greeter

แต่สำหรับเป้าหมายในไฟล์ //src/main/java/com/example/cmdline/BUILD คุณต้องระบุเส้นทางแพ็กเกจแบบเต็มของ //src/main/java/com/example/cmdline และป้ายกำกับเป้าหมายคือ //src/main/java/com/example/cmdline:runner

สร้างแพ็กเกจเป้าหมาย Java สำหรับการทำให้ใช้งานได้

มาทำแพ็กเกจเป้าหมาย Java สำหรับการทำให้ใช้งานได้โดยการสร้างไบนารีที่มีทรัพยากร Dependency ของรันไทม์ทั้งหมดกัน ซึ่งจะทำให้คุณเรียกใช้ไบนารีนอกสภาพแวดล้อมการพัฒนาได้

อย่าลืมว่ากฎการสร้าง java_binary จะสร้างสคริปต์ Shell .jar และ Wrapper ลองดูเนื้อหาของ runner.jar โดยใช้คำสั่งนี้

jar tf bazel-bin/src/main/java/com/example/cmdline/runner.jar

เนื้อหามีดังนี้

META-INF/
META-INF/MANIFEST.MF
com/
com/example/
com/example/cmdline/
com/example/cmdline/Runner.class

จะเห็นได้ว่า runner.jar มี Runner.class แต่ไม่ใช่ทรัพยากร Dependency Greeting.class สคริปต์ runner ที่ Bazel สร้างจะเพิ่ม greeter.jar ลงในคลาสพาธ ดังนั้นหากคุณปล่อยไว้เช่นนี้ สคริปต์จะทำงานภายในเครื่อง แต่จะไม่ทำงานแบบสแตนด์อโลนบนเครื่องอื่น โชคดีที่กฎ java_binary ให้คุณสร้างไบนารีในตัวและทำให้ใช้งานได้ได้ หากต้องการสร้าง ให้เพิ่ม _deploy.jar ลงในชื่อเป้าหมาย ดังนี้

bazel build //src/main/java/com/example/cmdline:runner_deploy.jar

Bazel ให้ผลผลิตดังต่อไปนี้

INFO: Found 1 target...
Target //src/main/java/com/example/cmdline:runner_deploy.jar up-to-date:
  bazel-bin/src/main/java/com/example/cmdline/runner_deploy.jar
INFO: Elapsed time: 1.700s, Critical Path: 0.23s

คุณเพิ่งสร้าง runner_deploy.jar ซึ่งเรียกใช้แบบสแตนด์อโลนจากสภาพแวดล้อมการพัฒนาได้ เนื่องจากมีทรัพยากร Dependency ของรันไทม์ที่จำเป็น ดูเนื้อหาของ JAR แบบสแตนด์อโลนนี้โดยใช้คำสั่งเดียวกับก่อนหน้านี้

jar tf bazel-bin/src/main/java/com/example/cmdline/runner_deploy.jar

เนื้อหามีคลาสทั้งหมดที่จำเป็นต้องใช้ในการเรียกใช้ ดังนี้

META-INF/
META-INF/MANIFEST.MF
build-data.properties
com/
com/example/
com/example/cmdline/
com/example/cmdline/Runner.class
com/example/Greeting.class

อ่านเพิ่มเติม

โปรดดูรายละเอียดเพิ่มเติมที่หัวข้อต่อไปนี้

ขอให้สนุกกับการสร้าง