บทแนะนำ 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
└── MODULE.bazel

สร้างด้วย Bazel

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

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

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

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

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

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

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

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

ลองดูไฟล์ java-tutorial/BUILD

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

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

แอตทริบิวต์ในเป้าหมายจะระบุทรัพยากร Dependency และตัวเลือกอย่างชัดเจน แม้ว่าแอตทริบิวต์ 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

ตรวจสอบกราฟทรัพยากร Dependency

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

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

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

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

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

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

กราฟทรัพยากร Dependency ของเป้าหมาย "ProjectRunner"

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

ปรับแต่งบิลด์ 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 จะคอมไพล์ไฟล์นั้นใหม่เท่านั้น

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

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

ตอนนี้คุณได้สร้างโปรเจ็กต์ที่มี 2 เป้าหมายแล้ว เป้าหมาย ProjectRunner จะสร้างไฟล์ต้นฉบับ 2 ไฟล์และขึ้นอยู่กับเป้าหมายอื่น (: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 ลองดูกราฟทรัพยากร Dependency

กราฟทรัพยากร Dependency ของเป้าหมาย "runner"

อย่างไรก็ตาม คุณต้องให้สิทธิ์การแสดงเป้าหมาย 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 แพ็กเกจ ซึ่งแต่ละแพ็กเกจมีเป้าหมายเดียว และเข้าใจทรัพยากร 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 จะสร้างไฟล์ .jar และสคริปต์ของ Shell 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 ลงใน classpath ดังนั้นหากปล่อยไว้เช่นนี้ สคริปต์จะทำงานในเครื่อง แต่จะไม่ทำงานแบบสแตนด์อโลนในเครื่องอื่น โชคดีที่กฎ 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

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

ดูรายละเอียดเพิ่มเติมได้ที่

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