সরাসরি পর্বে যান
পর্ব ৪৬ · ৫৯-এর মধ্যে · মডিউল ৭

Maven আর Gradle — বিল্ড আর dependency সামলানো

Maven and Gradle — Build Tools, pom.xml, Lifecycle, Transitive Dependencies, Executable JARs, CI and Supply-Chain Safety

Taught in Bangla, this lesson explains Java build tools: why manual javac builds break down, Maven's conventions, pom.xml, scopes, lifecycle phases and plugin goals, transitive dependencies and version conflicts, Gradle's Kotlin DSL, task graph and build cache, the wrapper, executable JARs and multi-module projects, CI and supply-chain safety.

এই পর্বে যা শিখবেন

  • হাতে javac -cp দিয়ে বড় প্রজেক্ট বানানো কেন ভেঙে পড়ে, আর “JAR hell” আসলে কী
  • Maven-এর ফোল্ডার-কাঠামো, pom.xml-এর প্রতিটি অংশ, dependency scope, lifecycle আর plugin goal — পর্ব ২২-এর “হিসাব খাতা”-কে Maven প্রজেক্ট বানিয়ে
  • transitive dependency, mvn dependency:tree, version-এর সংঘাত, nearest wins, dependencyManagement আর BOM
  • Gradle-এর Kotlin DSL, task graph, incremental build, build cache আর implementation/api-এর পার্থক্য
  • wrapper, চালানো যায় এমন JAR, multi-module প্রজেক্ট, CI, আর dependency-র নিরাপত্তা — AI-এর বানানো library-র নামসহ

১ · হাতে build-এর যন্ত্রণা — কেন build tool দরকার

এতক্ষণ এই কোর্সের প্রায় সব কোড ছিল এক ফাইলে, শুধু JDK দিয়ে। পর্ব ২২-এর “হিসাব খাতা”-তে ফাইল হলো নয়টি, তখন লিখেছিলেন javac -d out *.java. এটুকু হাতে চলে। কিন্তু একটি আসল প্রজেক্টের চেহারা অন্যরকম: কয়েকশো সোর্স ফাইল, টেস্টের আলাদা ফাইল, আর কয়েক ডজন বাইরের library — JSON-এর জন্য Jackson (পর্ব ৩৯), টেস্টের জন্য JUnit (পর্ব ৪৭), লগ লেখার জন্য একটি, ডেটাবেসের জন্য আরেকটি।

প্রতিটি library আসে একটি JAR ফাইল হিসেবে — অনেকগুলো .class ফাইলের একটি zip. এখন হাতে কাজটা করে দেখা যাক:

# ১. প্রতিটি JAR ওয়েবসাইট থেকে নিজে নামিয়ে lib ফোল্ডারে রাখুন
# ২. তারপর compile — প্রতিটি JAR-এর নাম classpath-এ
javac -cp "lib/jackson-databind-2.17.2.jar:lib/jackson-core-2.17.2.jar:lib/jackson-annotations-2.17.2.jar" -d out $(find src -name "*.java")

# ৩. টেস্ট compile আর চালানো — আরও কয়েকটি JAR
# ৪. চালানো — আবার একই লম্বা classpath (Windows-এ : এর বদলে ;)
java -cp "out:lib/jackson-databind-2.17.2.jar:lib/jackson-core-2.17.2.jar:lib/jackson-annotations-2.17.2.jar" com.abcltech.hisab.Main

লক্ষ করুন, আপনি চেয়েছিলেন শুধু Jackson, অথচ তিনটি JAR লাগল। কারণ jackson-databind নিজেই আরও দুটি library-র ওপর নির্ভর করে। এদের বলে transitive dependency — আপনার dependency-র dependency. বড় library-তে এই গাছ কয়েক স্তর গভীর হয়, আর কোনটার কোন version লাগবে তা হাতে বের করা প্রায় অসম্ভব।

JAR hell — classpath-এর ক্রমে লুকানো বিপদ

ধরুন lib ফোল্ডারে ভুল করে একটি library-র দুটি version রয়ে গেছে, অথবা দুটি library একই তৃতীয় library-র আলাদা version চায়। JVM classpath-এ বাঁ থেকে ডানে খোঁজে, আর একটি class প্রথম যেখানে পায়, সেটাই নেয়। আপনার কোড compile হয়েছিল নতুন version দেখে, কিন্তু চলার সময় classpath-এ আগে পড়ল পুরোনোটা — যেখানে একটি method নেই। compile-এ কোনো ভুল ধরা পড়েনি, ধরা পড়ল চলার মাঝখানে:

Exception in thread "main" java.lang.NoSuchMethodError: 'void Lib.newMethod()'
	at Main.main(Main.java:1)

আর একটি JAR পুরোপুরি বাদ পড়লে আসে java.lang.NoClassDefFoundError: Lib, যার নিচে Caused by: java.lang.ClassNotFoundException: Lib. এই দুই ধরনের সমস্যার ডাকনাম JAR hell. এর সবচেয়ে খারাপ দিক: আপনার মেশিনে চলে, সহকর্মীর মেশিনে চলে না — কারণ দুজনের lib ফোল্ডার আলাদা।

ঢাকা, চট্টগ্রাম আর সিলেটে দশটি শাখার একটি বিরিয়ানির দোকানের কথা ভাবুন। প্রতিটি শাখার বাবুর্চি নিজের স্মৃতি থেকে রাঁধলে — কেউ এক ধরনের চাল, কেউ আরেক ধরনের মসলা — স্বাদ শাখায় শাখায় আলাদা হবেই। সমাধান একটি লিখিত রেসিপি কার্ড: কোন চাল, কোন মসলা, কত গ্রাম, কোন ক্রমে। কার্ড একবার লেখা হয়, তারপর যেকোনো শাখায় একই বিরিয়ানি। build tool ঠিক এই রেসিপি কার্ড: প্রজেক্ট একবার বর্ণনা করা হয়, তারপর সব মেশিনে একই ফল।

মূল ধারণা

build tool আপনার কাছে জানতে চায় কী বানাতে হবে — প্রজেক্টের নাম ও version, কোন কোন library-র কোন version, কোন Java সংস্করণ। কীভাবে — কোথা থেকে JAR নামাবে, transitive dependency খুঁজবে, classpath বানাবে, কোন ক্রমে compile, টেস্ট আর প্যাকেজ করবে — সেটা সে নিজে জানে।

২ · Maven-এর চিন্তা — রীতি মানলে কম লিখতে হয়

Maven-এর মূল দর্শন convention over configuration — পর্ব ৪৫-এর ভাবনার প্রশ্নে যা দেখেছেন। Ant-এ প্রতিটি build ফাইলে লিখতে হতো সোর্স কোথায়, টেস্ট কোথায়, ফল কোথায় যাবে। Maven বলল: সবাই একই কাঠামো মানুক, তাহলে এসব লিখতেই হবে না। এই কাঠামো এখন এত প্রচলিত যে Gradle-ও ডিফল্টে এটাই মানে।

hisab-khata/
├── pom.xml                         ← প্রজেক্টের বর্ণনা (রেসিপি কার্ড)
├── src/
│   ├── main/
│   │   ├── java/                   ← আসল কোড
│   │   │   └── com/abcltech/hisab/
│   │   │       ├── Main.java
│   │   │       ├── Expense.java
│   │   │       └── ExpenseBook.java
│   │   └── resources/              ← কোডের সাথে যাওয়া ফাইল (app.properties, লোগো)
│   └── test/
│       ├── java/                   ← টেস্ট কোড (পর্ব ৪৭)
│       │   └── com/abcltech/hisab/ExpenseBookTest.java
│       └── resources/              ← শুধু টেস্টে লাগে এমন ফাইল
└── target/                         ← Maven-এর তৈরি সব কিছু (git-এ রাখবেন না)
    ├── classes/
    └── hisab-khata-1.0.0-SNAPSHOT.jar

তিনটি জিনিস খেয়াল করুন। প্রথমত, আসল কোড আর টেস্ট কোড আলাদা গাছে — টেস্ট কখনো চূড়ান্ত JAR-এ ঢোকে না। দ্বিতীয়ত, java/-এর নিচে ফোল্ডারগুলো package-এর নাম অনুযায়ী: com.abcltech.hisab মানে com/abcltech/hisab/. পর্ব ২২-এ সব ফাইল নামহীন default package-এ ছিল; আসল প্রজেক্টে প্রতিটি class একটি package-এ থাকে, আর প্রতিটি ফাইলের প্রথম লাইন package com.abcltech.hisab;. তৃতীয়ত, target/ পুরোটাই তৈরি করা জিনিস — মুছে ফেললেও mvn package আবার বানিয়ে দেবে। তাই .gitignore-এ target/ লেখা থাকে।

প্রতিটি প্রজেক্টের ঠিকানা — groupId:artifactId:version

Maven-এর জগতে প্রতিটি প্রজেক্ট আর প্রতিটি library-র একটি অনন্য ঠিকানা আছে, যাকে বলে coordinates, সংক্ষেপে GAV:

groupId

কে বানিয়েছে — সাধারণত ডোমেইন নাম উল্টো করে: com.abcltech, com.fasterxml.jackson.core. Java package-এর নামের রীতির মতোই।

artifactId

কী জিনিস — প্রজেক্ট বা library-র নাম: hisab-khata, jackson-databind. JAR ফাইলের নাম এখান থেকেই আসে।

version

কোন সংস্করণ — 2.17.2, 1.0.0-SNAPSHOT. একই G আর A-র প্রতিটি version আলাদা, অপরিবর্তনীয় জিনিস।

লেখার সময় এরা কোলন দিয়ে জোড়া হয়: com.fasterxml.jackson.core:jackson-databind:2.17.2. এই এক লাইন দেখে Maven বা Gradle পৃথিবীর যেকোনো জায়গা থেকে ঠিক সেই JAR খুঁজে আনতে পারে। নতুন Maven প্রজেক্ট সাধারণত বানানো হয় IntelliJ-এর New Project-এ Build system “Maven” বেছে, অথবা Spring Boot-এর জন্য start.spring.io থেকে — দুটিই ওপরের কাঠামো আর একটি pom.xml তৈরি করে দেয়।

৩ · pom.xml-এর ভেতরে — “হিসাব খাতা”-কে Maven প্রজেক্ট বানানো

Maven প্রজেক্টের হৃদয় pom.xml (POM = Project Object Model). এখানে “হিসাব খাতা”-র জন্য একটি সম্পূর্ণ pom দেওয়া হলো — খরচের তালিকা JSON ফাইলে সেভ করতে Jackson আর টেস্টের জন্য JUnit 5. এটা XML ফাইল, Java নয়, তাই চালানোর বোতাম নেই। সংস্করণ নম্বরগুলো উদাহরণ — সর্বশেষ সংস্করণ Maven Central-এ দেখুন।

<?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>

    <!-- ১. এই প্রজেক্টের ঠিকানা -->
    <groupId>com.abcltech</groupId>
    <artifactId>hisab-khata</artifactId>
    <version>1.0.0-SNAPSHOT</version>
    <packaging>jar</packaging>

    <!-- ২. সব জায়গায় ব্যবহারের মান -->
    <properties>
        <maven.compiler.release>21</maven.compiler.release>
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
        <jackson.version>2.17.2</jackson.version>
    </properties>

    <!-- ৩. বাইরের library -->
    <dependencies>
        <dependency>
            <groupId>com.fasterxml.jackson.core</groupId>
            <artifactId>jackson-databind</artifactId>
            <version>${jackson.version}</version>
        </dependency>
        <dependency>
            <groupId>org.junit.jupiter</groupId>
            <artifactId>junit-jupiter</artifactId>
            <version>5.10.3</version>
            <scope>test</scope>
        </dependency>
    </dependencies>

    <!-- ৪. plugin-এর সংস্করণ নির্দিষ্ট করা -->
    <build>
        <plugins>
            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-surefire-plugin</artifactId>
                <version>3.2.5</version>
            </plugin>
        </plugins>
    </build>
</project>

অংশ ধরে ধরে দেখা যাক:

  • ঠিকানা আর packaging: packaging বলে কী বানাতে হবে — jar (ডিফল্ট), war (পুরোনো ধাঁচের ওয়েব সার্ভারের জন্য), অথবা pom (শুধু অন্য প্রজেক্টের অভিভাবক, অংশ ৮-এ)। version-এর শেষে -SNAPSHOT মানে “কাজ চলছে” — অংশ ৪-এ বিস্তারিত।
  • properties: নিজের নাম দিয়ে মান রাখা যায় (jackson.version) আর ${...} দিয়ে বারবার ব্যবহার করা যায় — Jackson-এর কয়েকটি অংশ একসাথে লাগলে এক জায়গায় version বদলালেই হয়। কিছু নাম Maven নিজে চেনে: maven.compiler.release হলো javac-এর --release 21, মানে Java 21-এর ভাষা ও API. আর sourceEncoding UTF-8 না দিলে বাংলা লেখা থাকা সোর্স ফাইলে Windows-এ সমস্যা হতে পারে, Maven সতর্কও করে।
  • dependencies: প্রতিটি library-র GAV. Jackson-এর বাকি দুটি অংশ (core, annotations) লিখতে হয়নি — Maven নিজেই খুঁজে নেবে।
  • build/plugins: Maven নিজে প্রায় কিছুই করে না; compile করে compiler plugin, টেস্ট চালায় surefire plugin. এদের ডিফল্ট সংস্করণ আপনার Maven সংস্করণের ওপর নির্ভর করে, তাই পেশাদার প্রজেক্টে plugin-এর সংস্করণও লিখে দেওয়া হয় — নাহলে দুই মেশিনে দুই রকম build হতে পারে।

maven.compiler.release ভুলে গেলে compiler plugin একটি পুরোনো ডিফল্ট ধরে নিতে পারে (সাম্প্রতিক সংস্করণে Java 8). তখন record লেখা ফাইলে javac থামে: error: records are not supported in -source 8, পরের লাইনে (use -source 16 or higher to enable records) — Maven বার্তাটি [ERROR] দিয়ে শুরু করে দেখায়। “আমার IDE-তে চলে, mvn-এ চলে না” সমস্যার এটা খুব সাধারণ কারণ।

scope — dependency কোথায় কোথায় লাগবে

সব library সব জায়গায় লাগে না। JUnit লাগে শুধু টেস্ট compile আর চালানোর সময়; চূড়ান্ত প্রোগ্রামে এর কোনো কাজ নেই। scope দিয়ে Maven-কে এটা বলা হয়:

scopemain compileটেস্টচলার সময় / প্যাকেজেউদাহরণ
compile (ডিফল্ট)হ্যাঁহ্যাঁহ্যাঁJackson, Guava
testনাহ্যাঁনাJUnit, Mockito
providedহ্যাঁহ্যাঁনা — চালানোর পরিবেশ নিজেই দেবেServlet API (সার্ভার দেয়), Lombok
runtimeনাহ্যাঁহ্যাঁJDBC driver (পর্ব ৪৯)

runtime-এর যুক্তিটা সুন্দর: আপনার কোড লেখা হয় java.sql-এর interface দিয়ে (পর্ব ২১-এর “interface-এ প্রোগ্রাম করুন”), তাই compile-এর সময় MySQL বা PostgreSQL driver-এর কোনো class দরকার নেই; শুধু চলার সময় একটি implementation থাকতে হবে। scope দিয়ে এই নকশা build-এ পাহারা পায় — ভুল করে driver-এর class সরাসরি ব্যবহার করলে compile-ই হবে না।

C/C++/Python জানলে

C-তে এই কাজ করে Makefile (C-এর মডুলার প্রোগ্রাম পর্ব), C++-এ CMake (CMake পর্ব) — কিন্তু দুটোরই কোনো কেন্দ্রীয় library-ভান্ডার নেই, তাই dependency সামলাতে vcpkg বা Conan-এর মতো আলাদা টুল লাগে। Python-এর pyproject.toml আর pip অনেকটা pom.xml আর Maven-এর মতো, PyPI যেমন Maven Central (pip ও venv পর্ব)।

৪ · lifecycle — phase, plugin আর goal

Maven-এ আপনি সাধারণত বলেন না “compile করো, তারপর টেস্ট চালাও”। আপনি বলেন কোন পর্যন্ত যেতে চান। Maven-এর একটি নির্দিষ্ট ধাপের ক্রম আছে, যাকে বলে build lifecycle. মূল ধাপগুলো (phase):

validate → compile → test → package → verify → install → deploy

নিয়ম একটাই: কোনো phase চাইলে তার আগের সব phase ক্রমে চলবে। mvn package মানে validate, compile, test, তারপর package. আর phase নিজে কোনো কাজ করে না — এটা শুধু একটি নাম, একটি “খুঁটি”। আসল কাজ করে plugin-এর goal, যা কোনো phase-এ বাঁধা থাকে। jar packaging-এ Maven নিজেই এই বাঁধন করে দেয়। ধাপে ধাপে দেখুন:

  1. validatepom যাচাইকোনো plugin নেই
  2. compilecompiler:compileআগে resources
  3. testsurefire:testআগে testCompile
  4. packagejar:jartarget/*.jar
  5. verify, install, deployচলেনিpackage-এর পরের ধাপ
validateশুরুর আগে
$ mvn package
[INFO] Building hisab-khata 1.0.0-SNAPSHOT
pom.xml পড়া, GAV আর প্রয়োজনীয় তথ্য আছে কি না দেখা,
দরকার হলে dependency আর plugin নামানো (প্রথমবার)
process-resources, compileআসল কোড
[INFO] --- resources:3.3.1:resources (default-resources) @ hisab-khata ---
[INFO] --- compiler:3.13.0:compile (default-compile) @ hisab-khata ---
[INFO] Compiling 9 source files with javac [debug release 21] to target/classes
src/main/resources → target/classes (কপি)
src/main/java → target/classes (*.class)
test-compile, testটেস্ট ব্যর্থ হলে এখানেই থামে
[INFO] --- compiler:3.13.0:testCompile (default-testCompile) @ hisab-khata ---
[INFO] --- surefire:3.2.5:test (default-test) @ hisab-khata ---
[INFO] Tests run: 6, Failures: 0, Errors: 0, Skipped: 0
টেস্ট classpath = target/classes + target/test-classes
                + compile আর test scope-এর সব JAR
packageচূড়ান্ত ফল
[INFO] --- jar:3.4.2:jar (default-jar) @ hisab-khata ---
[INFO] Building jar: target/hisab-khata-1.0.0-SNAPSHOT.jar
[INFO] BUILD SUCCESS
JAR-এ শুধু target/classes — টেস্ট আর dependency নেই
যা চলেনিচাইলে আলাদা কমান্ড
mvn verify    ← package + integration test, মান-যাচাই
mvn install   ← JAR কপি হয় ~/.m2/repository-তে
mvn deploy    ← দূরের repository-তে প্রকাশ
mvn clean package ← আগে target/ মুছে নতুন করে
  1. validate. Maven pom পড়ে আর যাচাই করে। প্রথমবার চালালে এখানে অনেক “Downloading…” লাইন দেখবেন — plugin আর dependency নামানো হচ্ছে। এই phase-এ jar packaging-এ কোনো plugin goal বাঁধা নেই।
  2. compile. compile-এর ঠিক আগের phase process-resources-এ resources plugin src/main/resources-এর ফাইল কপি করে, তারপর compiler plugin-এর compile goal javac চালায়। লাইনের resources:3.3.1:resources মানে plugin-এর নাম, সংস্করণ আর goal — সংস্করণ নম্বর আপনার Maven অনুযায়ী আলাদা হবে।
  3. test. আগে টেস্ট কোড compile (testCompile), তারপর surefire plugin JUnit দিয়ে সব টেস্ট চালায়। একটিও টেস্ট ব্যর্থ হলে build এখানেই থামে, JAR তৈরিই হয় না — ভাঙা কোড প্যাকেজ হওয়ার পথ বন্ধ।
  4. package. jar plugin target/classes-কে একটি JAR-এ ভরে। খেয়াল করুন, এই JAR-এ Jackson নেই; একে সরাসরি java -jar দিয়ে চালানো যাবে না — অংশ ৮-এ সমাধান।
  5. এর পরের phase চলেনি। Maven যেখানে বলেছেন, সেখানেই থামে। clean আলাদা একটি lifecycle, তাই mvn clean package দুই lifecycle-এর ধাপ একসাথে বলে। phase-এর বদলে সরাসরি goal-ও চালানো যায়, যেমন mvn dependency:tree — তখন lifecycle-এর আগের ধাপ চলে না।

local repository আর Maven Central

Maven JAR নামায় কোথা থেকে? ডিফল্টে Maven Central থেকে — Java জগতের সবচেয়ে বড় উন্মুক্ত library-ভান্ডার। নামানো প্রতিটি JAR রাখা হয় আপনার কম্পিউটারের local repository-তে, ডিফল্টে ~/.m2/repository (Windows-এ C:\Users\<নাম>\.m2\repository). ঠিকানা GAV থেকেই তৈরি:

~/.m2/repository/com/fasterxml/jackson/core/jackson-databind/2.17.2/
    jackson-databind-2.17.2.jar
    jackson-databind-2.17.2.pom      ← এর নিজের dependency-র তালিকা
    jackson-databind-2.17.2.jar.sha1 ← যাচাইয়ের checksum

দ্বিতীয়বার build-এ Maven আর নামায় না, এখান থেকেই নেয়; আপনার দশটি প্রজেক্ট একই Jackson ব্যবহার করলে ডিস্কে কপি একটিই। লক্ষ করুন .pom ফাইলটি — প্রতিটি library-র নিজেরও একটি pom আছে, আর সেখান থেকেই Maven transitive dependency খুঁজে পায়। mvn install আপনার নিজের প্রজেক্টের JAR এই ফোল্ডারে রাখে, যাতে একই মেশিনের অন্য প্রজেক্ট সেটাকে dependency হিসেবে ব্যবহার করতে পারে।

SNAPSHOT, release আর semantic versioning

version-এর শেষে -SNAPSHOT থাকলে সেটা SNAPSHOT version — “এখনো তৈরি হচ্ছে, যেকোনো সময় বদলাবে”। Maven এমন dependency-র নতুন কপি মাঝে মাঝে আবার খোঁজে। 1.0.0-এর মতো release version-এর নিয়ম উল্টো: একবার প্রকাশ হলে আর কখনো বদলায় না। Maven Central-এ প্রকাশ করা release মুছে ফেলা বা বদলানো যায় না — এই অপরিবর্তনীয়তাই build-কে নির্ভরযোগ্য করে (ভাবনার প্রশ্ন ৩)। Maven Central SNAPSHOT গ্রহণই করে না।

বেশিরভাগ library semantic versioning মেনে চলার চেষ্টা করে: MAJOR.MINOR.PATCH. PATCH বাড়ে ভুল সংশোধনে (2.17.1 → 2.17.2), MINOR বাড়ে নতুন সুবিধা যোগ হলে কিন্তু পুরোনো কোড না ভেঙে (2.16 → 2.17), MAJOR বাড়ে যখন পুরোনো কোড ভাঙতে পারে (Jackson 2.x → 3.x, যেখানে package-এর নামই বদলেছে)। এটা একটি প্রতিশ্রুতি, আইন নয় — তাই MINOR আপগ্রেডের পরেও টেস্ট চালাতে হয়।

৫ · transitive dependency আর সংঘাত — nearest wins

“হিসাব খাতা”-য় আপনি লিখেছেন দুটি dependency. Maven আসলে কী কী নামাল, দেখার কমান্ড mvn dependency:tree. প্রত্যাশিত আউটপুট (সংক্ষেপে):

$ mvn dependency:tree
[INFO] com.abcltech:hisab-khata:jar:1.0.0-SNAPSHOT
[INFO] +- com.fasterxml.jackson.core:jackson-databind:jar:2.17.2:compile
[INFO] |  +- com.fasterxml.jackson.core:jackson-annotations:jar:2.17.2:compile
[INFO] |  \- com.fasterxml.jackson.core:jackson-core:jar:2.17.2:compile
[INFO] \- org.junit.jupiter:junit-jupiter:jar:5.10.3:test
[INFO]    +- org.junit.jupiter:junit-jupiter-api:jar:5.10.3:test
[INFO]    ...  (JUnit-এর আরও কয়েকটি অংশ, সবই test scope)

প্রতিটি লাইনের গঠন groupId:artifactId:packaging:version:scope, আর দাগগুলো গাছের শাখা। JUnit-এর নিচের সবকিছু test scope পেয়েছে — scope উত্তরাধিকারসূত্রেও যায়। এবার ধরুন “হিসাব খাতা” বড় হলো: মাসিক রিপোর্ট PDF-এ বানাতে একটি pdf-report library, আর খরচের সতর্কবার্তা পাঠাতে একটি sms-gateway library যোগ হলো (দুটিই উদাহরণের জন্য বানানো নাম)। দুটিই ভেতরে Jackson ব্যবহার করে — কিন্তু আলাদা version. classpath-এ একটি class-এর একটিই version থাকতে পারে, তাই Maven-কে বেছে নিতে হবে। এই বাছাইয়ের নিয়মকে বলে dependency mediation:

1hisab-khata:1.02+- pdf-report:3.03|  \- jackson-databind:2.15.04\- sms-gateway:1.25   \- http-client:4.16      \- jackson-databind:2.17.2
অবস্থা
স্তর
jackson-databind
  1. প্রথম স্তর — আপনার নিজের ঘোষণা। Maven গাছটি স্তর ধরে পড়ে: আগে আপনার pom-এ সরাসরি লেখা dependency, তারপর তাদের dependency, তারপর তাদের। এভাবে মূল থেকে প্রতিটি নোডের “দূরত্ব” জানা যায়।
  2. দ্বিতীয় স্তরে Jackson 2.15.0. pdf-report-এর pom বলে তার Jackson 2.15.0 লাগবে। এই প্রথমবার Jackson দেখা গেল — Maven এটাকেই বেছে রাখল।
  3. তৃতীয় স্তরে Jackson 2.17.2 — বাদ। http-client চায় 2.17.2, কিন্তু সেটা মূল থেকে তিন ধাপ দূরে। নিয়ম: মূলের সবচেয়ে কাছের ঘোষণা জেতে। সমান দূরত্বে দুটি থাকলে pom-এ যেটা আগে লেখা, সেটা। verbose tree-তে এটা “omitted for conflict with 2.15.0” লেখা দেখায়। খেয়াল করুন: নতুন version নয়, কাছের version জিতেছে।
  4. চলার সময় ঝুঁকি। http-client তৈরি হয়েছিল 2.17.2 দেখে। সে যদি এমন কোনো method ডাকে, যা 2.15-এ নেই, তাহলে অংশ ১-এর সেই NoSuchMethodError — compile-এ কিছু ধরা পড়বে না, ধরা পড়বে SMS পাঠানোর মুহূর্তে। এজন্যই কাজের টেস্ট (পর্ব ৪৭) জরুরি।
  5. সমাধান — নিজে সিদ্ধান্ত নিন। pom-এ নিজে jackson-databind:2.17.2 ঘোষণা করুন (তখন সেটা স্তর ১, তাই জেতে), অথবা আরও পরিষ্কারভাবে dependencyManagement-এ version নির্দিষ্ট করুন — নিচে দেখুন। Jackson-এর নতুন MINOR সাধারণত পুরোনোর সাথে মানানসই, তবু টেস্ট চালিয়ে নিশ্চিত হন।

এই নিয়ম নিজে হাতে চালিয়ে দেখুন। নিচের প্রোগ্রাম একটি ছোট “dependency resolver”: প্রতিটি library-র pom একটি Map-এ রাখা, আর গাছটি স্তর ধরে পড়া হয় — পর্ব ৩২-এর Queue দিয়ে BFS. যে নাম প্রথমবার দেখা যায়, সেটাই জেতে; কারণ BFS-এ কাছের স্তর সবসময় আগে আসে।

import java.util.*;

record Dep(String name, String version) {
    @Override public String toString() { return name + ":" + version; }
}
record Step(Dep dep, int depth) {}

class Main {
    public static void main(String[] args) {
        Map<String, List<Dep>> poms = Map.of(                        // প্রতিটি library-র pom, শুধু খোঁজার জন্য
            "hisab-khata:1.0", List.of(new Dep("pdf-report", "3.0"), new Dep("sms-gateway", "1.2")),
            "pdf-report:3.0", List.of(new Dep("jackson-databind", "2.15.0")),
            "sms-gateway:1.2", List.of(new Dep("http-client", "4.1")),
            "http-client:4.1", List.of(new Dep("jackson-databind", "2.17.2")));
        Map<String, String> chosen = new LinkedHashMap<>();         // নাম -> বেছে নেওয়া version
        Deque<Step> queue = new ArrayDeque<>(List.of(new Step(new Dep("hisab-khata", "1.0"), 0)));
        while (!queue.isEmpty()) {                                   // BFS: কাছের স্তর আগে
            Step s = queue.poll();
            String pad = "[" + s.depth() + "] " + "  ".repeat(s.depth());
            String winner = chosen.putIfAbsent(s.dep().name(), s.dep().version());
            if (winner != null) { System.out.println(pad + s.dep() + "  (omitted: " + winner + " is nearer)"); continue; }
            System.out.println(pad + s.dep());
            for (Dep d : poms.getOrDefault(s.dep().toString(), List.of())) queue.add(new Step(d, s.depth() + 1));
        }
        chosen.remove("hisab-khata");
        System.out.println("classpath: " + chosen);
    }
}

আউটপুট সাত লাইন (OpenJDK 21-এ যাচাই করা): [0] hisab-khata:1.0, স্তর ১-এ pdf-report:3.0 আর sms-gateway:1.2, স্তর ২-এ jackson-databind:2.15.0 আর http-client:4.1, স্তর ৩-এ jackson-databind:2.17.2 (omitted: 2.15.0 is nearer), আর শেষে classpath: {pdf-report=3.0, sms-gateway=1.2, jackson-databind=2.15.0, http-client=4.1}. মূল কৌশল putIfAbsent: নামটি আগে না থাকলে বসায় আর null দেয়; থাকলে কিছু বদলায় না, আগের মান ফেরত দেয় — সেটাই “বিজয়ী”। chosen একটি LinkedHashMap, তাই classpath-এর ক্রম আবিষ্কারের ক্রম, প্রতিবার একই। আসল Maven এর সাথে আরও অনেক কিছু করে (scope, exclusion, optional, চক্র ধরা), কিন্তু মূল ধারণা এটুকুই।

dependencyManagement আর BOM — version এক জায়গায়

প্রতিটি সংঘাতে হাতে dependency যোগ করা ঝামেলার, আর সেই dependency আপনি সত্যিই সরাসরি ব্যবহার করেন না। পরিষ্কার উপায় হলো dependencyManagement: এখানে লেখা মানে “এই library যেখানেই আসুক — সরাসরি বা transitive — এই version-ই হবে”। এটা নিজে কোনো dependency যোগ করে না, শুধু version-এর সিদ্ধান্ত দেয়।

<dependencyManagement>
    <dependencies>
        <!-- BOM: Jackson-এর সব অংশের মিলিয়ে নেওয়া version-এর তালিকা -->
        <dependency>
            <groupId>com.fasterxml.jackson</groupId>
            <artifactId>jackson-bom</artifactId>
            <version>2.17.2</version>
            <type>pom</type>
            <scope>import</scope>
        </dependency>
    </dependencies>
</dependencyManagement>

<dependencies>
    <dependency>   <!-- version লাগে না, BOM থেকে আসে -->
        <groupId>com.fasterxml.jackson.core</groupId>
        <artifactId>jackson-databind</artifactId>
    </dependency>
</dependencies>

BOM (Bill of Materials) হলো একটি বিশেষ pom, যার ভেতরে শুধু একটি dependencyManagement — কোন কোন অংশের কোন version একসাথে পরীক্ষা করা হয়েছে, তার তালিকা। scope import দিয়ে সেই তালিকা নিজের pom-এ টেনে আনা হয়। সবচেয়ে বড় উদাহরণ Spring Boot: এর BOM কয়েকশো library-র মিলিয়ে নেওয়া version ঠিক করে দেয়, তাই Spring Boot প্রজেক্টে Jackson, JUnit বা ডেটাবেস driver যোগ করতে সাধারণত version লিখতেই হয় না (পর্ব ৫০)। JUnit-এরও নিজস্ব junit-bom আছে। আরও দুটি হাতিয়ার জেনে রাখুন: কোনো transitive dependency একেবারে বাদ দিতে <exclusions>, আর Maven Enforcer plugin-এর dependencyConvergence নিয়ম, যা গাছে একই library-র দুটি version দেখলে build ব্যর্থ করে দেয় — সংঘাত নীরবে লুকিয়ে থাকতে পারে না।

৬ · Gradle — build-এর বর্ণনা কোড দিয়ে

Gradle একই সমস্যার আরেকটি সমাধান, ভিন্ন দর্শনে। Maven-এ build একটি স্থির XML বর্ণনা আর একটি নির্দিষ্ট lifecycle; Gradle-এ build একটি প্রোগ্রাম। build ফাইল লেখা হয় Kotlin বা Groovy ভাষার ছোট একটি রূপে, যাকে বলে DSL. Gradle 8.2 থেকে নতুন প্রজেক্টে ডিফল্ট হলো Kotlin DSL, ফাইলের নাম build.gradle.kts (পুরোনো Groovy রূপ build.gradle). একই “হিসাব খাতা”:

plugins {
    application                               // java plugin + "gradle run" সুবিধা
}

group = "com.abcltech"
version = "1.0.0-SNAPSHOT"

repositories {
    mavenCentral()                            // একই Maven Central
}

dependencies {
    implementation("com.fasterxml.jackson.core:jackson-databind:2.17.2")
    testImplementation(platform("org.junit:junit-bom:5.10.3"))
    testImplementation("org.junit.jupiter:junit-jupiter")
    testRuntimeOnly("org.junit.platform:junit-platform-launcher")
}

java {
    toolchain { languageVersion = JavaLanguageVersion.of(21) }
}

application {
    mainClass = "com.abcltech.hisab.Main"
}

tasks.test {
    useJUnitPlatform()                        // JUnit 5 দিয়ে টেস্ট চালানো
}

pom-এর চেয়ে ছোট, আর অনেক কিছু চেনা: একই ফোল্ডার-কাঠামো, একই GAV (এক লাইনে কোলন দিয়ে), একই Maven Central, আর platform(...) হলো BOM. সংস্করণ নম্বরগুলো এখানেও উদাহরণ। toolchain বলে এই প্রজেক্ট Java 21 দিয়ে বানাতে হবে — মেশিনে অন্য JDK চালু থাকলেও Gradle সঠিকটা খুঁজে নেয় (পর্ব ০৩-এ উল্লেখ করা সুবিধা)। আর তৈরি জিনিস যায় target/-এর বদলে build/ ফোল্ডারে।

task আর task graph

Gradle-এ Maven-এর মতো phase-এর স্থির তালিকা নেই; আছে task — compileJava, processResources, jar, test — আর তাদের মধ্যে “কে কার ওপর নির্ভর করে”-এর সম্পর্ক। ./gradlew build চালালে Gradle প্রথমে এই সম্পর্ক থেকে একটি DAG (চক্রহীন দিকনির্দেশিত গ্রাফ) বানায়, তারপর সঠিক ক্রমে task চালায়। আসল জাদু তার পরে: প্রতিটি task-এর ইনপুট আর আউটপুট মনে রাখা হয়, আর ইনপুট না বদলালে task চলেই না। একে বলে incremental build:

  1. compileJavasrc/main/java.class বানায়
  2. processResourcessrc/main/resourcesফাইল কপি
  3. compileTestJava → testsrc/test/javaটেস্ট
  4. jarbuild/libsপ্যাকেজ
graph তৈরিকে কার ওপর নির্ভর করে
jar             ← compileJava, processResources
compileTestJava ← compileJava
test            ← compileJava, processResources, compileTestJava
compileJava আর processResources একে অপরের ওপর নির্ভর করে না —
তাই আলাদা থ্রেডে একসাথেও চলতে পারে
প্রথম buildসব task চলে
> Task :compileJava
> Task :processResources
> Task :jar
> Task :compileTestJava
> Task :test
প্রতিটি task শেষে তার ইনপুট আর আউটপুটের fingerprint সংরক্ষণ
দ্বিতীয় buildকিছুই বদলায়নি
> Task :compileJava UP-TO-DATE
> Task :processResources UP-TO-DATE
> Task :jar UP-TO-DATE
> Task :compileTestJava UP-TO-DATE
> Task :test UP-TO-DATE
শুধু app.properties বদলালেযারা নির্ভর করে, শুধু তারাই
> Task :compileJava UP-TO-DATE
> Task :processResources          ← ইনপুট বদলেছে
> Task :jar                       ← নতুন resource ঢোকাতে হবে
> Task :compileTestJava UP-TO-DATE
> Task :test                      ← classpath বদলেছে
build cacheঅন্য মেশিনের কাজও কাজে লাগে
$ ./gradlew clean build --build-cache
> Task :compileJava FROM-CACHE
> Task :processResources FROM-CACHE
একই ইনপুট আগে কোথাও (নিজের মেশিনে বা দলের শেয়ার করা cache-এ)
তৈরি হয়ে থাকলে আউটপুট সেখান থেকে এনে বসানো হয়
  1. graph তৈরি। কাজ শুরুর আগে Gradle পুরো graph বানায়। আপনি build চাইলে, যে task-গুলো দরকার শুধু সেগুলো graph-এ ঢোকে। যাদের মধ্যে নির্ভরতা নেই, তারা সমান্তরালেও চলতে পারে।
  2. প্রথম build — সব চলে। কোনো ইতিহাস নেই, তাই সব task-ই কাজ করে। প্রতিটি task শেষে Gradle তার ইনপুট ফাইল আর আউটপুটের একটি “আঙুলের ছাপ” (fingerprint) রেখে দেয়।
  3. কিছু না বদলালে — সব বাদ। প্রতিটি task-এর আগে Gradle নতুন fingerprint আগেরটার সাথে মেলায়। মিলে গেলে task চালানোর দরকার নেই: UP-TO-DATE. বড় প্রজেক্টে মিনিট থেকে সেকেন্ডে নেমে আসে।
  4. একটি resource বদলালে। শুধু processResources আর যারা তার আউটপুট ব্যবহার করে (jar, test) আবার চলে। compileTestJava বাদ, কারণ তার ইনপুটে resource ফাইল নেই।
  5. build cache. build cache আরও এক ধাপ এগোয়: আউটপুট ইনপুটের fingerprint দিয়ে জমা রাখা হয়। clean করার পরেও, অথবা দলের অন্য কারও মেশিনে বা CI-তে একই ইনপুট আগে তৈরি হয়ে থাকলে task চালানোর বদলে আউটপুট এনে বসানো হয়: FROM-CACHE.

এই কৌশলের মূলটা খুব সরল — আগের ইনপুট মনে রাখা, মিলে গেলে কাজ বাদ দেওয়া। নিচের প্রোগ্রামে তিনবার build চালানো হয়েছে: প্রথমবার, কিছু না বদলে, আর শুধু একটি resource ফাইল বদলে। graph-এর ক্রম বের করা হয়েছে recursion দিয়ে — প্রতিটি task আগে তার নির্ভরতাগুলো চালায় (এটাই topological sort).

import java.util.*;

class Main {
    static final Map<String, List<String>> DEPENDS = Map.of(
        "compileJava", List.of(), "processResources", List.of(),
        "jar", List.of("compileJava", "processResources"),
        "compileTestJava", List.of("compileJava"),
        "test", List.of("compileJava", "processResources", "compileTestJava"));
    static final Map<String, String> INPUT = new HashMap<>(Map.of("compileJava", "Expense.java v1",
        "processResources", "app.properties v1", "compileTestJava", "ExpenseTest.java v1"));
    static final Map<String, String> lastRun = new HashMap<>();     // আগের build-এর fingerprint
    static int executed, upToDate;

    static String run(String task, Map<String, String> done) {
        if (done.containsKey(task)) return done.get(task);            // এই build-এ আগেই চলেছে
        StringBuilder fp = new StringBuilder(INPUT.getOrDefault(task, ""));
        for (String d : DEPENDS.get(task)) fp.append(" + ").append(run(d, done));
        String now = fp.toString();
        boolean same = now.equals(lastRun.put(task, now));
        System.out.println("> Task :" + task + (same ? " UP-TO-DATE" : ""));
        if (same) upToDate++; else executed++;
        done.put(task, now);
        return now;
    }
    public static void main(String[] args) {
        String[] titles = {"build 1: first time", "build 2: nothing changed", "build 3: only app.properties changed"};
        for (int b = 0; b < titles.length; b++) {
            if (b == 2) INPUT.put("processResources", "app.properties v2");
            executed = upToDate = 0;
            System.out.println("== " + titles[b]);
            Map<String, String> done = new HashMap<>();
            run("jar", done); run("test", done);
            System.out.println(executed + " executed, " + upToDate + " up-to-date");
        }
    }
}

প্রথম build-এ পাঁচটি task-ই চলে, শেষ লাইন 5 executed, 0 up-to-date. দ্বিতীয়টিতে প্রতিটি লাইনের শেষে UP-TO-DATE, শেষে 0 executed, 5 up-to-date. তৃতীয়টিতে compileJava UP-TO-DATE, processResources, jar, compileTestJava UP-TO-DATE, test, আর 3 executed, 2 up-to-date. fingerprint এখানে একটি String: task-এর নিজের ইনপুট আর তার নির্ভরতাদের fingerprint জোড়া দিয়ে। তাই নিচের কোনো কিছু বদলালে সেই বদল ওপরের দিকে ছড়ায়, কিন্তু পাশের শাখায় যায় না। lastRun.put নতুন মান রাখে আর পুরোনোটা ফেরত দেয় — এক লাইনে তুলনা আর সংরক্ষণ। done Map নিশ্চিত করে যে একই build-এ compileJava দুবার চলে না, যদিও jar আর test দুজনেই তাকে চায়। আসল Gradle fingerprint বানায় ফাইলের content-এর hash দিয়ে, তারিখ দিয়ে নয়।

implementation, api আর অন্য configuration

Maven-এর scope-এর জায়গায় Gradle-এ আছে configuration. সবচেয়ে গুরুত্বপূর্ণ পার্থক্য implementation আর api-এর মধ্যে, আর এটা বোঝা যায় যখন আপনি নিজে একটি library লেখেন:

configurationমানেMaven-এ কাছাকাছি
implementationআমার ভেতরের কাজে লাগে; আমার ব্যবহারকারীরা compile-এর সময় এটা দেখবে নাcompile (তবে আরও কড়া)
apiআমার public method-এর টাইপেই এটা আছে, তাই ব্যবহারকারীদেরও লাগবে (java-library plugin লাগে)compile
compileOnlyশুধু compile-এর সময়provided
runtimeOnlyশুধু চলার সময়runtime
testImplementationশুধু টেস্টেtest

ধরুন আপনার report-lib-এর একটি public method JsonNode toJson() ফেরত দেয় — JsonNode Jackson-এর টাইপ। তাহলে যে আপনার library ব্যবহার করবে, তার compile-এর সময়ও Jackson লাগবে: api. কিন্তু Jackson যদি শুধু ভেতরে ব্যবহার হয়, তাহলে implementation — আপনার ব্যবহারকারীর compile classpath ছোট থাকে, আর আপনি পরে Jackson বদলে Gson নিলেও তাদের কোড recompile করতে হয় না। এটা পর্ব ১৭-এর encapsulation-ই, এবার build-এর স্তরে।

Gradle-এর সংঘাতের নিয়ম আলাদা। অংশ ৫-এর একই গাছে Gradle ডিফল্টে সবচেয়ে নতুন version বেছে নেয় — 2.17.2, Maven-এর মতো কাছেরটি নয়। একই dependency-র তালিকা দুই টুলে দুই রকম classpath দিতে পারে। Gradle-এ কোন version কেন এলো জানতে: ./gradlew dependencies (পুরো গাছ) আর ./gradlew dependencyInsight --dependency jackson-databind.

৭ · Maven বনাম Gradle, আর wrapper দিয়ে একই build সবখানে

বিষয়MavenGradle
build ফাইলXML (pom.xml) — ঘোষণামূলক, স্থির কাঠামোKotlin বা Groovy কোড (build.gradle.kts)
মূল ধারণানির্দিষ্ট lifecycle, phase-এ plugin goaltask আর task graph
গতিপ্রতিবার সাধারণত সব ধাপ আবারincremental build, build cache, daemon — বড় প্রজেক্টে সাধারণত দ্রুত
নমনীয়তাকম; বিশেষ কিছু চাইলে plugin লিখতে হয়অনেক বেশি; build ফাইলেই যুক্তি লেখা যায়
পড়া ও বোঝালম্বা, কিন্তু সব প্রজেক্ট দেখতে একই রকমছোট, কিন্তু প্রতিটি build আলাদা চেহারা নিতে পারে
version সংঘাতnearest winsডিফল্টে সবচেয়ে নতুন
কোথায় বেশিenterprise ও Spring Boot-এর অনেক প্রজেক্টAndroid (Google-এর আনুষ্ঠানিক টুল), Kotlin প্রজেক্ট, বড় multi-module build

কোনটা শিখবেন? দুটোই চিনে রাখুন — কাজের প্রজেক্টে build tool সাধারণত আগেই ঠিক করা থাকে। শুরু করার জন্য Maven সহজ, আর তার ধারণাগুলো (GAV, scope, repository, transitive dependency) Gradle-এও হুবহু কাজে লাগে।

wrapper — build tool-এর version-ও পিন করা

একটি সমস্যা এখনো বাকি: আপনার মেশিনে Maven 3.9, সহকর্মীর মেশিনে Maven 3.6, CI সার্ভারে আরেকটি। একই pom, তবু আচরণ আলাদা হতে পারে। সমাধান wrapper: প্রজেক্টের ভেতরেই একটি ছোট স্ক্রিপ্ট, যা নির্দিষ্ট version-এর build tool নিজে নামিয়ে চালায়।

# Maven Wrapper একবার তৈরি করুন (মেশিনে Maven থাকতে হবে)
mvn wrapper:wrapper
# তৈরি হয়: mvnw, mvnw.cmd, .mvn/wrapper/maven-wrapper.properties

# এরপর সবাই, সবখানে — মেশিনে Maven ইনস্টল না থাকলেও
./mvnw package          # Linux/macOS
mvnw.cmd package        # Windows

# Gradle-এর ক্ষেত্রে একই ধারণা
./gradlew build         # gradle/wrapper/gradle-wrapper.properties-এ version লেখা

এই ফাইলগুলো git-এ রাখা হয়। নতুন কেউ প্রজেক্ট clone করে শুধু JDK থাকলেই ./mvnw package চালাতে পারেন — সঠিক Maven নিজে নেমে আসবে। Spring Initializr-এর তৈরি প্রজেক্টে (আর IntelliJ-এর নতুন Gradle প্রজেক্টে) wrapper আগে থেকেই থাকে। Gradle-এর properties ফাইলে distributionSha256Sum দিলে নামানো Gradle-এর checksum-ও যাচাই হয়। মনে রাখুন: নির্ভরযোগ্য build মানে তিনটি জিনিস পিন করা — JDK-র version (toolchain বা release), build tool-এর version (wrapper), আর প্রতিটি dependency ও plugin-এর version.

৮ · চালানো যায় এমন JAR, multi-module প্রজেক্ট আর JPMS

অংশ ৪-এর mvn package একটি JAR দিয়েছে। চালিয়ে দেখা যাক:

$ java -jar target/hisab-khata-1.0.0-SNAPSHOT.jar
no main manifest attribute, in target/hisab-khata-1.0.0-SNAPSHOT.jar

প্রতিটি JAR-এর ভেতরে META-INF/MANIFEST.MF নামের একটি ছোট লেখার ফাইল থাকে — manifest. java -jar সেখানে Main-Class খোঁজে: কোন class-এর main চালাতে হবে। Maven-এর সাধারণ JAR-এ এটা থাকে না, কারণ বেশিরভাগ JAR library, প্রোগ্রাম নয়। পর্ব ২২-এ jar --main-class Main দিয়ে হাতে এটা যোগ করেছিলেন। Maven-এ jar plugin-কে বলতে হয়:

<plugin>
    <groupId>org.apache.maven.plugins</groupId>
    <artifactId>maven-jar-plugin</artifactId>
    <version>3.4.2</version>
    <configuration>
        <archive>
            <manifest>
                <mainClass>com.abcltech.hisab.Main</mainClass>
            </manifest>
        </archive>
    </configuration>
</plugin>

এবার Main চালু হয় — কিন্তু খরচ সেভ করতে গিয়ে প্রথম Jackson class-এ পৌঁছালেই NoClassDefFoundError, কারণ JAR-এ Jackson নেই। দুটি পথ: dependency-র JAR-গুলো পাশের একটি lib/ ফোল্ডারে রেখে manifest-এ Class-Path লেখা, অথবা সব কিছু একটি JAR-এ ভরে ফেলা — fat JAR (uber JAR). দ্বিতীয়টি এক ফাইলে বিতরণের জন্য খুব জনপ্রিয়:

  1. আপনার classtarget/classescom/abcltech/...
  2. dependency JARjackson-*.jarruntime classpath
  3. shadepackage phaseখুলে এক জায়গায়
  4. java -jarMANIFEST.MFMain-Class
নিজের কোডসাধারণ JAR-এ শুধু এটুকু
com/abcltech/hisab/Main.class
com/abcltech/hisab/Expense.class
com/abcltech/hisab/ExpenseBook.class
app.properties
runtime classpathcompile + runtime scope
jackson-databind-2.17.2.jar
jackson-core-2.17.2.jar
jackson-annotations-2.17.2.jar
junit-jupiter বাদ — test scope চূড়ান্ত প্রোগ্রামে যায় না
hisab-khata-1.0.0.jarসব .class এক গাছে
com/abcltech/hisab/*.class
com/fasterxml/jackson/databind/**.class
com/fasterxml/jackson/core/**.class
META-INF/MANIFEST.MF
একই নামের ফাইল দুই JAR-এ থাকলে (যেমন META-INF/services)
transformer দিয়ে জোড়া দিতে হয়, নাহলে একটি হারায়
চালানোশুধু JDK/JRE লাগে
Manifest-Version: 1.0
Main-Class: com.abcltech.hisab.Main

$ java -jar hisab-khata-1.0.0.jar
classpath = শুধু এই একটি JAR — সব কিছু ভেতরেই
  1. নিজের class. সাধারণ JAR-এ শুধু আপনার compile করা class আর resource থাকে। library-র জন্য এটাই ঠিক — যে ব্যবহার করবে, তার build tool dependency আলাদা করে আনবে।
  2. runtime classpath. চালাতে যা যা লাগবে: compile আর runtime scope-এর সব JAR, transitive-সহ। test আর provided বাদ।
  3. shade — এক JAR-এ ভরা। Maven Shade plugin package phase-এ প্রতিটি dependency JAR খুলে তার class-গুলো আপনার JAR-এর ভেতরে বসায়। একই নামের ফাইলের সংঘাত সামলাতে “transformer” লাগে; চাইলে dependency-র package-এর নামও বদলে দেওয়া যায় (relocation), যাতে অন্য version-এর সাথে ধাক্কা না লাগে।
  4. চালানো। manifest-এ Main-Class বসে, আর পুরো প্রোগ্রাম এক ফাইলে — কপি করে java -jar দিয়ে চালান। Gradle-এ একই কাজের জন্য Shadow plugin, আর Spring Boot-এর নিজস্ব plugin (পর্ব ৫০) dependency JAR-গুলো না খুলে পুরোটাই ভেতরে রেখে একটি বিশেষ loader দিয়ে চালায়।

multi-module প্রজেক্ট

“হিসাব খাতা” আরও বড় হলে একে ভাগ করা যায়: hisab-core (Expense, ExpenseBook — হিসাবের নিয়ম), hisab-cli (terminal-এর মেনু), আর পরে hisab-web (Spring Boot API, পর্ব ৫১)। এটা একটি multi-module project. Maven-এ একটি অভিভাবক pom থাকে <packaging>pom</packaging> আর <modules>-এর তালিকা নিয়ে; সেখানে dependencyManagement আর plugin-এর version একবার লিখলেই সব module পায়। Gradle-এ settings.gradle.kts-এ include("hisab-core", "hisab-cli"). module-গুলো একে অপরের ওপর সাধারণ dependency-র মতোই নির্ভর করে, আর build tool সঠিক ক্রমে বানায় — core আগে, cli পরে। সবচেয়ে বড় লাভ নকশায়: hisab-core terminal বা ওয়েবের কিছুই জানে না, তাই একই হিসাবের নিয়ম CLI আর ওয়েব দুই জায়গায় ব্যবহার হয়।

JPMS module আর build tool — কে কী করে

“module” শব্দটি এখানে দুই অর্থে আসে। Maven/Gradle-এর module হলো build-এর একক — আলাদা ফোল্ডার, আলাদা JAR. Java 9-এর JPMS module হলো ভাষার একক: module-info.java-তে লেখা থাকে এটি কোন module-এর ওপর নির্ভর করে (requires com.fasterxml.jackson.databind;) আর কোন package বাইরে খোলা (exports com.abcltech.hisab.core;). দুটি পরিপূরক: build tool ঠিক করে কোন JAR-এর কোন version নামবে, JPMS ঠিক করে সেগুলোর মধ্যে কে কাকে দেখতে পাবে — requires-এ version লেখাই যায় না। module-info.java থাকলে Maven আর Gradle নিজেরাই dependency-গুলো module path-এ দেয়; তবে বেশিরভাগ অ্যাপ্লিকেশন এখনো JPMS ছাড়াই classpath-এ চলে।

৯ · CI আর supply chain — অন্যের কোডে আস্থা রাখার নিয়ম

build একটি কমান্ডে নেমে এলে সেই কমান্ড প্রতিটি কোড-পরিবর্তনে একটি সার্ভার নিজে থেকে চালাতে পারে — একে বলে CI (continuous integration). GitHub Actions-এ নিচের ফাইলটি রাখলে প্রতিটি push আর pull request-এ GitHub একটি নতুন Linux মেশিনে JDK 21 বসিয়ে ./mvnw verify চালায়; টেস্ট ভাঙলে লাল চিহ্ন:

name: build
on: [push, pull_request]

jobs:
  build:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-java@v4
        with:
          distribution: temurin
          java-version: '21'
          cache: maven            # ~/.m2 সংরক্ষণ — পরের বার দ্রুত
      - run: ./mvnw -B verify     # -B: batch mode, লগ পরিষ্কার

CI মেশিনে Maven ইনস্টলের কোনো ধাপ নেই — wrapper ঠিক আপনার version-ই চালায়। আর প্রতিবার একদম নতুন মেশিন হওয়ায় লুকানো ফাইল বা হাতে বসানো JAR-এর ওপর নির্ভরতা CI-তেই ধরা পড়ে।

supply chain — আপনার প্রোগ্রামের বেশিরভাগ কোড অন্যের লেখা

mvn dependency:tree চালালে একটি অস্বস্তিকর সত্য চোখে পড়ে: একটি সাধারণ Spring Boot অ্যাপে আপনার লেখা কোডের চেয়ে transitive dependency-র কোড অনেক বেশি। এদের যেকোনো একটিতে নিরাপত্তা-ছিদ্র থাকলে সেটা আপনার অ্যাপেরও ছিদ্র। যে পথে বাইরের কোড আপনার প্রোগ্রামে ঢোকে — library-র লেখক, ভান্ডার, build tool, CI — পুরোটাই আপনার software supply chain.

এর সবচেয়ে বড় শিক্ষা আসে ২০২১ সালের ডিসেম্বরে। জনপ্রিয় লগ লেখার library Apache Log4j 2-তে একটি গুরুতর দুর্বলতা প্রকাশিত হয় — CVE-2021-44228, ডাকনাম Log4Shell. লগ বার্তার ভেতরের একটি বিশেষ lookup সুবিধার কারণে বাইরের কেউ শুধু একটি সাজানো লেখা লগে ঢোকাতে পারলেই সার্ভারে কোড চালানোর পথ পেয়ে যেত। সবচেয়ে বড় সমস্যা ছিল খুঁজে পাওয়া, কারণ অসংখ্য প্রজেক্ট Log4j পেয়েছিল অন্য library-র মাধ্যমে, transitive হিসেবে। সমাধান আর শিক্ষা ভাবনার প্রশ্ন ৪-এ।

পেশাদার Java দলের কয়েকটি সাধারণ অভ্যাস:

  • version পিন করুন। সবসময় নির্দিষ্ট version লিখুন; LATEST, [1.0,)-এর মতো range বা Gradle-এর + এড়িয়ে চলুন — নাহলে আজকের build আর কালকের build আলাদা library পেতে পারে।
  • checksum যাচাই করুন। প্রতিটি নামানো JAR-এর সাথে একটি checksum থাকে। Maven ডিফল্টে অমিল হলে সতর্ক করে, -C (strict checksums) দিলে build ব্যর্থ করে। Gradle-এর dependency verification একটি verification-metadata.xml-এ প্রতিটি JAR-এর SHA-256 লিখে রাখে আর প্রতিবার মেলায়।
  • স্ক্যানার চালান। OWASP Dependency-Check (Maven ও Gradle plugin) আপনার সব dependency প্রকাশিত দুর্বলতার তালিকার সাথে মেলায়। GitHub-এর Dependabot pom বা build ফাইল পড়ে দুর্বল বা পুরোনো version-এর জন্য নিজে থেকে আপগ্রেডের pull request খোলে। mvn versions:display-dependency-updates দেখায় কোনগুলোর নতুন version এসেছে।
  • কম dependency, বেশি নিরাপত্তা। প্রতিটি নতুন library-র আগে প্রশ্ন করুন: JDK নিজে কি এটা পারে (HttpClient, java.time)? library-টি কি সক্রিয়ভাবে রক্ষণাবেক্ষণ হয়?
  • তালিকা রাখুন। SBOM (Software Bill of Materials) হলো আপনার প্রোগ্রামে থাকা সব component-এর যন্ত্রপাঠযোগ্য তালিকা। CycloneDX-এর মতো plugin build-এর সময় এটা বানায় — পরের Log4Shell-এ “আমরা কি এটা ব্যবহার করি?” প্রশ্নের উত্তর তখন এক মিনিটের।
পরীক্ষা ও ইন্টারভিউতে

Maven coordinates কী? groupId:artifactId:version — প্রতিটি artifact-এর অনন্য ঠিকানা।
mvn package চালালে কোন phase-গুলো চলে? validate, compile, test, package — আগের সব phase ক্রমে।
transitive dependency-র version সংঘাতে Maven কোনটা নেয়? মূলের সবচেয়ে কাছেরটি (nearest wins); সমান দূরত্বে আগে ঘোষিতটি।
SNAPSHOT আর release version-এর পার্থক্য? SNAPSHOT উন্নয়নাধীন, বদলাতে পারে; release একবার প্রকাশের পর অপরিবর্তনীয়।
Gradle-এ implementation আর api-র পার্থক্য? implementation ব্যবহারকারীর compile classpath-এ যায় না; api যায়, কারণ সেটা public API-র অংশ।

১০ · AI-এর যুগে — বানানো dependency আর মিশে যাওয়া syntax

build ফাইল লেখা AI-কে দেওয়ার মতো একঘেয়ে কাজ, আর AI বেশিরভাগ সময় ভালোই লেখে। কিন্তু এখানে ভুলের বিপদ বিশেষ: ভুল library ঢুকে পড়া মানে অন্যের কোড আপনার সার্ভারে চলা।

সবচেয়ে গুরুতর সমস্যা হলো বানানো dependency. AI মাঝে মাঝে এমন groupId, artifactId বা version লেখে, যা দেখতে একদম বিশ্বাসযোগ্য কিন্তু বাস্তবে নেই — যেমন একটি সত্যিকারের library-র নামের সাথে -utils বা -starter জুড়ে দেওয়া, অথবা এমন version নম্বর, যা কখনো প্রকাশই হয়নি। কয়েকটি গবেষণায় দেখা গেছে, AI-এর সুপারিশ করা package-এর একটি উল্লেখযোগ্য অংশ বাস্তবে নেই, আর একই বানানো নাম বারবার আসে। আক্রমণকারীরা এই সুযোগ নিতে পারে: সেই বানানো নামে আগেভাগে একটি ক্ষতিকর package প্রকাশ করে রাখা, যাতে কেউ AI-এর কথামতো যোগ করলেই তাদের কোড ঢুকে পড়ে। এর নাম দেওয়া হয়েছে slopsquatting. Maven Central-এ নতুন groupId প্রকাশ করতে ডোমেইন বা কোড-হোস্টিং অ্যাকাউন্টের মালিকানা প্রমাণ করতে হয়, তাই npm বা PyPI-এর চেয়ে কাজটা কঠিন — কিন্তু অসম্ভব নয়, আর Gradle plugin বা অন্য repository-র ক্ষেত্রে নিয়ম আলাদা হতে পারে।

AI যা করেকেন সমস্যাআপনি যা করবেন
অস্তিত্বহীন artifact বা version লেখেbuild ব্যর্থ হয়, অথবা আরও খারাপ — কেউ সেই নামে ক্ষতিকর package রেখে দিয়েছেপ্রতিটি GAV Maven Central-এর সার্চে নিজে খুঁজুন; প্রকাশের তারিখ, ডাউনলোড আর অফিসিয়াল ডকুমেন্টেশন মিলিয়ে নিন
Maven আর Gradle-এর syntax মিশিয়ে ফেলেKotlin DSL-এ Groovy-র implementation 'g:a:v', বা Gradle ফাইলে <scope>-এর ধারণাকোন টুল, কোন DSL (Kotlin না Groovy) — prompt-এ স্পষ্ট বলুন
পুরোনো syntax দেয়Gradle-এর compile (Gradle 7-এ সরানো), পুরোনো plugin version, Java 8-এর source/targetGradle আর Maven-এর version বলে দিন; maven.compiler.release বা toolchain চান
অপ্রয়োজনীয় dependency যোগ করেJDK যা পারে তার জন্যও library (HTTP, তারিখ); বড় attack surface, ধীর buildজিজ্ঞেস করুন: JDK দিয়ে হয় কি না; প্রতিটি dependency-র কারণ চান
scope ভুল দেয়JUnit compile scope-এ — টেস্ট library চূড়ান্ত JAR-এ ঢুকে যায়dependency tree দেখে প্রতিটি scope যাচাই করুন
version range বা + ব্যবহার করেপ্রতিটি build আলাদা version পেতে পারে; পুনরুৎপাদন অসম্ভবনির্দিষ্ট version বা BOM চান

AI-কে build ফাইল লিখতে বা রিভিউ করতে বলার সময় এই কাঠামো ব্যবহার করুন:

আমি Java 21-এ একটি প্রজেক্ট বানাচ্ছি। build tool: [Maven 3.9 / Gradle 8.x Kotlin DSL — একটি বেছে নিন] কাজ: [যেমন: CLI টুল, JSON ফাইলে ডেটা সেভ করে, JUnit 5 দিয়ে টেস্ট] শর্ত: ১. শুধু Maven Central-এ সত্যিই আছে এমন library দেবে। প্রতিটি dependency-র জন্য পুরো groupId:artifactId লেখো আর বলো কোথায় যাচাই করব। কোনো নাম বা version নিয়ে নিশ্চিত না হলে স্পষ্ট বলো "নিশ্চিত নই" — অনুমান করে লিখবে না। ২. version-এর জায়গায় placeholder রাখো, আমি Maven Central থেকে সর্বশেষ stable version বসাব। কোনো version range, LATEST বা + নয়। ৩. শুধু আমার বেছে নেওয়া টুলের syntax — Maven হলে শুধু pom.xml, Gradle হলে শুধু Kotlin DSL; Groovy বা Maven syntax মেশাবে না। ৪. JDK দিয়ে যা হয় (HTTP, তারিখ, ফাইল), তার জন্য library যোগ করবে না। প্রতিটি dependency-র পাশে এক লাইনে কারণ লেখো। ৫. টেস্টের library শুধু test scope / testImplementation-এ। ৬. Java version ঠিক করো maven.compiler.release বা toolchain দিয়ে; plugin-এর version-ও নির্দিষ্ট করো। ৭. শেষে বলো কোন কমান্ডে dependency tree দেখে আমি সব যাচাই করব।

AI-এর লেখা build ফাইল হাতে পেলে — যাচাইয়ের চেকলিস্ট

ভাবনার প্রশ্ন

প্রতিটি প্রশ্ন নিয়ে নিজে কিছুক্ষণ ভাবুন — তারপর “→ উত্তর” চাপুন। এগুলোই আপনাকে কোড-লেখক থেকে সিস্টেম-চিন্তক বানাবে।

প্র ০১make (১৯৭৬) থেকে Ant (২০০০), Maven (২০০৪) আর Gradle (২০১২) — প্রতিটি নতুন build tool আগেরটার কোন সমস্যা সমাধান করতে এসেছিল, আর কী নতুন সমস্যা আনল?

গল্পটা শুরু Bell Labs-এ। ১৯৭৬ সালে Stuart Feldman লক্ষ করেন, একটি সহকর্মী একটি ভুল ঠিক করার পরে সংশ্লিষ্ট ফাইল আবার compile করতে ভুলে গিয়ে অনেক সময় নষ্ট করেছেন। এর সমাধানে তিনি লেখেন make: Makefile-এ লেখা থাকে কোন ফাইল কোন ফাইলের ওপর নির্ভর করে, আর কীভাবে বানাতে হয়। make ফাইলের তারিখ মিলিয়ে শুধু পুরোনো হয়ে যাওয়া অংশ আবার বানায় — এটাই incremental build-এর মূল ধারণা, যা অংশ ৬-এ Gradle-এ দেখেছেন।

Java-র জন্য make অসুবিধাজনক ছিল: Makefile আসলে shell কমান্ডের তালিকা, তাই Unix-এ লেখা build Windows-এ চলত না; আর Java-র class-এর পারস্পরিক নির্ভরতা C-এর header-এর মতো সরল নয়। Apache Tomcat বানাতে গিয়ে James Duncan Davidson লেখেন Ant, যা ২০০০ সালে আলাদা প্রজেক্ট হিসেবে প্রকাশিত হয়। Ant-এর নির্দেশ XML-এ, আর প্রতিটি কাজ (compile, কপি, zip) Java-তে লেখা “task”, তাই সব অপারেটিং সিস্টেমে একই রকম চলে। কিন্তু Ant ছিল পুরোপুরি পদ্ধতিগত: প্রতিটি প্রজেক্টের build.xml-এ হাতে লিখতে হতো সোর্স কোথায়, কোন ক্রমে কী হবে। দুটি Ant প্রজেক্ট কখনো এক রকম দেখাত না, আর dependency মানে সেই পুরোনো lib ফোল্ডার।

Maven (Apache-তে Jakarta Turbine প্রজেক্ট থেকে জন্ম, ১.০ এলো ২০০৪ সালে) প্রশ্নটাই বদলে দিল: “কীভাবে বানাবে” না লিখে “প্রজেক্টটা কী” লেখো। নির্দিষ্ট ফোল্ডার, নির্দিষ্ট lifecycle, আর সবচেয়ে বড় উদ্ভাবন — কেন্দ্রীয় repository আর transitive dependency. এই ব্যবস্থা এত সফল যে Gradle, sbt, এমনকি Ant-এর সঙ্গী Ivy-ও Maven Central ব্যবহার করে। কিন্তু দামও ছিল: রীতির বাইরে কিছু করতে হলে plugin লিখতে বা লম্বা XML কনফিগারেশন করতে হয়, আর প্রতিটি build-এ প্রায় সব ধাপ আবার চলে।

Gradle (Hans Dockter-এর নেতৃত্বে শুরু, ১.০ এলো ২০১২ সালে) দুই জগতের মিলন চাইল: Maven-এর রীতি আর repository, কিন্তু build ফাইল একটি আসল প্রোগ্রাম — Ant-এর নমনীয়তা ফিরিয়ে আনা। সাথে incremental build, build cache আর পটভূমিতে চালু থাকা daemon দিয়ে গতি। ২০১৩ সালে Google Android-এর build ব্যবস্থার জন্য Gradle বেছে নেওয়ায় এর ব্যবহার বিস্ফোরিত হয়। নতুন সমস্যা: নমনীয়তার অপব্যবহার — কিছু build ফাইল নিজেরাই একটি জটিল প্রোগ্রাম হয়ে যায়।

মূল উপলব্ধি: প্রতিটি প্রজন্ম আগেরটার একটি দুর্বলতা সারিয়েছে — make দিল নির্ভরতার হিসাব, Ant দিল বহনযোগ্যতা, Maven দিল রীতি আর repository, Gradle দিল নমনীয়তা আর গতি — আর প্রতিটি সমাধান নতুন একটি ভারসাম্যের প্রশ্ন এনেছে: রীতি বনাম স্বাধীনতা।

প্র ০২“reproducible build” মানে কী? একই সোর্স থেকে হুবহু একই JAR পাওয়া কেন গুরুত্বপূর্ণ, আর Reproducible Builds আন্দোলন কী শেখায়?

দুটি আলাদা স্তরে কথাটা বোঝা যায়। প্রথম স্তর, যা এই পর্বের বেশিরভাগ জুড়ে: একই সোর্স যেকোনো মেশিনে build করলে একই আচরণের প্রোগ্রাম পাওয়া — একই dependency version, একই JDK লক্ষ্য, একই plugin. wrapper, version পিন করা আর lock বা verification ফাইল এই স্তরের হাতিয়ার। দ্বিতীয়, আরও কঠিন স্তর: একই সোর্স থেকে বাইট-বাই-বাইট হুবহু একই ফাইল পাওয়া, যাতে দুটি JAR-এর checksum মিলে যায়। একে বলে reproducible build.

হুবহু মিল কেন জরুরি? কারণ এটাই একমাত্র উপায় প্রমাণ করার যে একটি প্রকাশিত binary সত্যিই প্রকাশিত সোর্স থেকে তৈরি। ধরুন একটি জনপ্রিয় library-র সোর্স GitHub-এ সবার সামনে, আর Maven Central-এ একটি JAR. আপনি সোর্স পড়ে নিশ্চিন্ত হলেন, কিন্তু JAR-টি কি সেই সোর্স থেকেই এসেছে? যদি লেখকের build মেশিন বা CI-তে কেউ ঢুকে build-এর মাঝখানে কিছু যোগ করে থাকে? সোর্সে তার কোনো চিহ্ন থাকবে না। ২০২০ সালের SolarWinds ঘটনায় ঠিক এমনটাই ঘটেছিল বলে ব্যাপকভাবে জানা যায় — আক্রমণকারীরা কোম্পানির build প্রক্রিয়ায় ক্ষতিকর কোড ঢুকিয়েছিল, আর স্বাক্ষর করা আপডেট হিসেবে সেটা হাজারো প্রতিষ্ঠানে পৌঁছায়। build যদি reproducible হয়, তাহলে স্বাধীন যে কেউ একই সোর্স থেকে build করে checksum মিলিয়ে দেখতে পারেন; না মিললে সন্দেহের কারণ।

Reproducible Builds আন্দোলন শুরু হয় মোটামুটি ২০১৩ সালে Debian Linux-এর ডেভেলপারদের মধ্যে, পরে Tor, Arch Linux, NixOS আর অনেক প্রজেক্ট যোগ দেয় (reproducible-builds.org). তারা দেখালেন, হুবহু মিল না হওয়ার কারণগুলো খুব সাধারণ: build-এর তারিখ-সময় ফাইলের ভেতরে লেখা হয়, zip-এ ফাইলের ক্রম ফোল্ডার পড়ার ক্রমের ওপর নির্ভর করে, ফাইলের অনুমতি বা ব্যবহারকারীর নাম ঢুকে যায়, HashMap-এর মতো অনির্দিষ্ট ক্রম আউটপুটে আসে (এই কোর্সে বারবার যে সতর্কতা দেখেছেন!)। একটি JAR একটি zip, আর প্রতিটি entry-তে সময় লেখা থাকে — তাই দুবার build করলে সাধারণত checksum আলাদা।

মূল উপলব্ধি: “আমার মেশিনে চলে” থেকে “সবার মেশিনে একই রকম চলে” হলো প্রথম ধাপ; “যে কেউ প্রমাণ করতে পারে এই binary এই সোর্স থেকেই এসেছে” হলো পরের ধাপ — আর supply chain-এর আস্থা দাঁড়িয়ে আছে এই দ্বিতীয় ধাপের ওপর।

প্র ০৩২০১৬ সালে npm-এর ছোট্ট “left-pad” package সরিয়ে নেওয়ায় হাজারো JavaScript build ভেঙে পড়ে। Java ডেভেলপারদের জন্য এই ঘটনার শিক্ষা কী, আর Maven Central এমন ঘটনা কীভাবে ঠেকায়?

২০১৬ সালের মার্চে একজন ওপেন-সোর্স ডেভেলপার Azer Koçulu-র সাথে একটি কোম্পানির একটি package-এর নাম নিয়ে বিরোধ হয়; npm কোম্পানির পক্ষ নেয়। প্রতিবাদে তিনি npm থেকে নিজের আড়াইশোর বেশি package সরিয়ে নেন। তার মধ্যে একটি ছিল left-pad — একটি String-এর বাঁ দিকে নির্দিষ্ট সংখ্যক space বা অক্ষর বসানোর মাত্র এগারো লাইনের একটি function. দেখা গেল, এই ছোট্ট package-এর ওপর transitive-ভাবে নির্ভর করত Babel আর React-এর মতো বিশাল প্রজেক্টের build — আর পৃথিবী জুড়ে হাজারো CI build হঠাৎ “package not found” দেখিয়ে ব্যর্থ হতে শুরু করে। npm কয়েক ঘণ্টার মধ্যে নজিরবিহীনভাবে package-টি ফিরিয়ে আনে, আর পরে নিয়ম বদলায়: প্রকাশের কিছু সময় পরে, বিশেষ করে অন্যরা নির্ভর করলে, package আর ইচ্ছামতো সরানো যায় না।

প্রথম শিক্ষা: transitive গাছ আপনার ধারণার চেয়ে গভীর। ঐ প্রজেক্টগুলোর কোনো ডেভেলপার হয়তো left-pad-এর নামই শোনেননি। দ্বিতীয় শিক্ষা: ছোট dependency-রও দাম আছে। এগারো লাইনের কাজ নিজে লেখা যেত (Java-তে String.format("%5s", s) বা " ".repeat(n) + s), অথচ সেটার জন্য একজন অপরিচিত ব্যক্তির সিদ্ধান্তের ওপর পুরো build নির্ভর করছিল। প্রতিটি dependency একটি সম্পর্ক — আইনগত (লাইসেন্স), নিরাপত্তাগত আর রক্ষণাবেক্ষণের।

Maven Central এখানে ভিন্ন নীতিতে চলে: একবার প্রকাশিত release আর মুছে ফেলা বা বদলানো যায় না। একটি GAV মানে চিরকাল একই বাইট। এজন্য Java জগতে left-pad-এর মতো “হঠাৎ নেই” ঘটনা কার্যত ঘটে না, আর দশ বছর আগের একটি pom আজও build হয়। তবে অপরিবর্তনীয়তার একটি উল্টো দিকও আছে: দুর্বলতাওয়ালা পুরোনো version-ও চিরকাল সেখানে থাকে, আর কেউ চাইলে আজও Log4j-এর দুর্বল version নামাতে পারে — তাই স্ক্যানার আর আপগ্রেডের দায় আপনার।

মূল উপলব্ধি: dependency মানে শুধু সুবিধা নয়, একটি দীর্ঘমেয়াদি নির্ভরতা — অপরিবর্তনীয় repository, পিন করা version আর নিজস্ব কপি build-কে অন্যের সিদ্ধান্ত থেকে রক্ষা করে, কিন্তু কোন dependency নেবেন, সেই বিচার এখনো আপনার।

প্র ০৪Log4Shell (CVE-2021-44228) কেন এত বড় সংকট হয়েছিল, যখন অনেক প্রজেক্ট Log4j সরাসরি ব্যবহারই করত না? transitive dependency-র ঝুঁকি সামলাতে একটি Java দলের কী কী প্রস্তুতি থাকা উচিত?

২০২১ সালের ডিসেম্বরের শুরুতে Apache Log4j 2-এর একটি দুর্বলতা প্রকাশ্যে আসে। Log4j লগ বার্তার ভেতরে ${...} আকারের বিশেষ “lookup” বুঝত, আর তার একটি রূপ JNDI-র মাধ্যমে দূরের সার্ভার থেকে তথ্য আনতে পারত। ফলে কোনো অ্যাপ যদি ব্যবহারকারীর দেওয়া লেখা — একটি ফর্মের ঘর, একটি HTTP header — লগে লিখত, আর সেই লেখায় এমন একটি lookup থাকত, তাহলে কিছু পরিস্থিতিতে সার্ভার বাইরের কোড চালিয়ে ফেলতে পারত। দুর্বলতাটি সর্বোচ্চ তীব্রতার মান পায়। এখানে আক্রমণের খুঁটিনাটি জরুরি নয়; জরুরি হলো এর বিস্তার আর প্রতিক্রিয়ার শিক্ষা।

লগ লেখা প্রায় প্রতিটি সার্ভার অ্যাপের কাজ, আর Log4j ছিল Java জগতের সবচেয়ে প্রচলিত লগিং library-গুলোর একটি। কিন্তু বহু দল প্রথম দিন সহজ প্রশ্নটার উত্তরই দিতে পারছিল না: “আমরা কি Log4j ব্যবহার করি?” কারণ এটা আসত transitive হিসেবে — কোনো ফ্রেমওয়ার্ক, কোনো ডেটাবেস টুল, কোনো তৃতীয় পক্ষের SDK-র ভেতর দিয়ে। আবার অনেক প্রজেক্টে Log4j-এর নাম থাকলেও শুধু API অংশ ছিল, দুর্বল অংশ (log4j-core) নয়। কিছু প্রোডাক্টে JAR অন্য JAR-এর ভেতরে (fat JAR, অংশ ৮) লুকানো ছিল, তাই সাধারণ ফাইল খোঁজাতেও ধরা পড়ত না। পরের কয়েক সপ্তাহে আরও কয়েকটি সম্পর্কিত দুর্বলতা পাওয়া যায় আর একাধিক সংশোধিত version আসে — দলগুলোকে কয়েকবার আপগ্রেড করতে হয়েছিল।

যারা দ্রুত সামলেছিল, তাদের হাতে ছিল এই পর্বের হাতিয়ারগুলো। দৃশ্যমানতা: প্রতিটি সার্ভিসের mvn dependency:tree বা SBOM, যা দিয়ে মিনিটে বোঝা যায় কোথায় কোন version. নিয়ন্ত্রণ: dependencyManagement বা BOM-এ এক লাইন বদলে transitive version জোর করে সংশোধিত version-এ তোলা, library-র লেখকদের আপডেটের অপেক্ষা না করে। গতি: CI আর ভালো টেস্ট — আপগ্রেডের পরে কিছু ভাঙল কি না ঘণ্টায় জানা, সপ্তাহে নয়। স্বয়ংক্রিয় নজরদারি: Dependabot বা OWASP Dependency-Check, যা নতুন CVE প্রকাশ হলেই জানায়।

মূল উপলব্ধি: transitive dependency-র ঝুঁকি এড়ানো যায় না, কিন্তু প্রস্তুত থাকা যায় — যে দল এক কমান্ডে তার পুরো dependency গাছ দেখতে, এক লাইনে version বদলাতে আর এক ঘণ্টায় টেস্ট চালিয়ে প্রকাশ করতে পারে, তার কাছে পরের সংকট একটি সাধারণ কাজের দিন।

প্র ০৫Google-এর হাজার হাজার প্রকৌশলী একটি বিশাল repository (monorepo)-তে কাজ করেন, আর তাদের build চলে Bazel-এ। এত বড় মাপে build tool-এর কাছে কী চাই, যা Maven বা Gradle-এর সাধারণ ব্যবহারে লাগে না?

বেশিরভাগ প্রতিষ্ঠানে প্রতিটি প্রজেক্ট বা সার্ভিসের নিজের repository আর নিজের build. Google, Meta-র মতো কিছু প্রতিষ্ঠান উল্টো পথ নিয়েছে — প্রায় সব কোড এক বিশাল repository-তে, যাকে বলে monorepo. লাভ: একটি library বদলালে সেটা যারা ব্যবহার করে তাদের সবার কোড একই commit-এ ঠিক করা যায়; “কোন সার্ভিস library-র কোন version ব্যবহার করছে” প্রশ্নই থাকে না, কারণ সবাই একই সংস্করণে। কিন্তু build tool-এর জন্য এটা ভয়াবহ চ্যালেঞ্জ: কোটি কোটি লাইন কোড, প্রতিদিন হাজারো পরিবর্তন। প্রতিটি পরিবর্তনে সব কিছু আবার build বা টেস্ট করা অসম্ভব।

Google-এর ভেতরের build ব্যবস্থার নাম ছিল Blaze; এর ওপেন-সোর্স রূপ Bazel প্রকাশিত হয় ২০১৫ সালে। Bazel-এর মূলনীতিগুলো অংশ ৬-এর ধারণাকে চরমে নিয়ে যায়। প্রতিটি নির্ভরতা স্পষ্ট: প্রতিটি target (ছোট একটি library বা binary) ঠিক কোন ফাইল আর কোন target-এর ওপর নির্ভর করে, তা BUILD ফাইলে লেখা থাকতে হয় — লুকানো নির্ভরতা চলবে না। hermetic build: প্রতিটি কাজ একটি বিচ্ছিন্ন পরিবেশে চলে, যেখানে ঘোষিত ইনপুট ছাড়া কিছুই দেখা যায় না — তাই একই ইনপুট মানে একই আউটপুট, সবখানে। দূরের cache আর দূরে চালানো: যেহেতু ফল শুধু ইনপুটের ওপর নির্ভর করে, একজনের মেশিনে বা সার্ভার-ফার্মে বানানো আউটপুট সবাই ব্যবহার করতে পারেন, আর হাজারো কাজ একসাথে অনেক মেশিনে চালানো যায়।

এর দাম আছে। BUILD ফাইলে প্রতিটি নির্ভরতা হাতে লেখা ছোট প্রজেক্টের জন্য অনেক বাড়তি কাজ; Maven-এর “রীতি মানলেই চলে”-র আরাম নেই। Java-র জন্য Maven Central থেকে library আনতেও বাড়তি নিয়ম লাগে। তাই Bazel (আর Meta-র Buck, Pants-এর মতো একই ধারার টুল) সাধারণত বিশাল, বহু-ভাষার কোডবেসে যুক্তিসংগত; একটি সাধারণ Spring Boot সার্ভিসে Maven বা Gradle যথেষ্ট আর সহজ।

মূল উপলব্ধি: build tool-এর মূল প্রশ্ন সব মাপে একই — “কী বদলেছে, আর তার ফলে ন্যূনতম কী আবার করতে হবে?” — মাপ যত বড়, এই প্রশ্নের উত্তর তত নিখুঁত হতে হয়, আর তার জন্য নির্ভরতা তত স্পষ্ট আর build তত বিচ্ছিন্ন হতে হয়।

অনুশীলন

বেশিরভাগ কাজে নতুন কোড লিখতে হবে না — ওপরের কোড বদলে দেখা আর ভাবাই আসল কাজ।

  1. সংঘাত নিজে মেটান: অংশ ৫-এর resolver-এ hisab-khata:1.0-এর তালিকার শেষে তৃতীয় dependency হিসেবে new Dep("jackson-databind", "2.17.2") যোগ করুন। না চালিয়ে বলুন কোন লাইনগুলো বদলাবে আর classpath-এ Jackson-এর কোন version থাকবে; তারপর চালিয়ে মিলিয়ে নিন।

    এখন Jackson-কে প্রথম দেখা যায় স্তর ১-এ, তাই 2.17.2 জেতে। আউটপুট: [0] hisab-khata:1.0, স্তর ১-এ তিনটি — pdf-report:3.0, sms-gateway:1.2, jackson-databind:2.17.2; স্তর ২-এ jackson-databind:2.15.0 (omitted: 2.17.2 is nearer) আর http-client:4.1; স্তর ৩-এ jackson-databind:2.17.2 (omitted: 2.17.2 is nearer); শেষে classpath: {pdf-report=3.0, sms-gateway=1.2, jackson-databind=2.17.2, http-client=4.1}. শেষের লাইনে version একই বলে আসল Maven এটাকে “omitted for duplicate” বলত, সংঘাত নয়। বাস্তবে এই কাজ dependencyManagement দিয়ে করাই ভালো, কারণ তাতে আপনি এমন একটি library “সরাসরি ব্যবহার করি” বলে ঘোষণা করছেন না, যা আসলে করেন না।

  2. শুধু টেস্ট বদলালে: অংশ ৬-এর task runner-এর titles-এ চতুর্থ একটি শিরোনাম যোগ করুন, আর লুপে b == 3 হলে শুধু compileTestJava-এর ইনপুট "ExpenseTest.java v2" করুন। কোন task চলবে, কোনটা UP-TO-DATE? শেষ লাইনে কী আসবে?

    চলবে শুধু compileTestJava আর test: compileJava UP-TO-DATE, processResources UP-TO-DATE, jar UP-TO-DATE, compileTestJava, test, শেষ লাইন 2 executed, 3 up-to-date. যুক্তি: টেস্ট কোড বদলালে চূড়ান্ত JAR বদলানোর কোনো কারণ নেই — jar টেস্টের ওপর নির্ভরই করে না। টেস্ট লিখতে লিখতে বারবার build চালালে Gradle শুধু টেস্টের অংশ নতুন করে করে।

  3. pom-এর ভুল ধরুন: একটি pom-এ আছে: junit-jupiter কোনো scope ছাড়া; mysql-connector-j compile scope-এ; Lombok compile scope-এ; আর properties-এ কোনো maven.compiler.release নেই। প্রতিটির সমস্যা আর সমাধান এক-দুই লাইনে লিখুন।

    (ক) JUnit scope ছাড়া মানে compile — main কোড থেকেও JUnit-এর class দেখা যায়, আর fat JAR বা Spring Boot JAR-এ টেস্ট library ঢুকে যায়। সমাধান: <scope>test</scope>. (খ) JDBC driver compile scope-এ থাকলে কেউ ভুল করে driver-এর নিজস্ব class সরাসরি ব্যবহার করে ফেলতে পারে, তখন ডেটাবেস বদলানো কঠিন। runtime দিলে কোড শুধু java.sql-এর interface দেখে। (গ) Lombok শুধু compile-এর সময় কোড তৈরি করে (পর্ব ৪৫), চলার সময় এর কোনো কাজ নেই — provided (অথবা compiler plugin-এর annotation processor তালিকায়)। (ঘ) release না থাকলে compiler plugin পুরোনো ডিফল্ট লক্ষ্য ধরে নিতে পারে, আর record বা switch expression-এ compile error আসে — অংশ ৩-এর সতর্কবার্তা। সমাধান: <maven.compiler.release>21</maven.compiler.release>.

  4. AI-এর দেওয়া dependency যাচাই: একটি AI চ্যাটে আপনাকে বলল, “Excel ফাইল পড়তে যোগ করুন org.apache.poi:poi-excel-reader:6.2.1”. যোগ করার আগে আপনি কী কী ধাপে যাচাই করবেন? যদি দেখা যায় এমন artifact নেই, তাহলে পরের ধাপ কী?

    ধাপগুলো: (১) Maven Central-এর সার্চে groupId org.apache.poi খুঁজুন — এখানে কোন কোন artifact আসলে আছে, তা দেখুন। (২) library-র অফিসিয়াল ওয়েবসাইট (Apache POI-এর নিজের ডকুমেন্টেশন) থেকে প্রস্তাবিত dependency মিলিয়ে নিন — AI-এর কথা নয়, লেখকের কথা আসল। (৩) version-টি প্রকাশিত কি না, আর কবে প্রকাশ হয়েছে, দেখুন। (৪) যোগ করার পরে dependency tree দেখুন। এই উদাহরণের নামটি ইচ্ছা করে বানানো, যাচাই করলে এমন artifact খুঁজে পাবেন না — Apache POI-এর আসল artifact-এর নাম অন্যরকম। পরের ধাপ: AI-কে বলুন নামটি পাওয়া যায়নি আর অফিসিয়াল ডকুমেন্টেশনের লিংক চান, অথবা নিজে ডকুমেন্টেশন থেকে সঠিক নাম নিন। আর কখনো “কাছাকাছি নামের” কোনো artifact আন্দাজে যোগ করবেন না — slopsquatting ঠিক এই আন্দাজের ওপর নির্ভর করে।

নিজেকে যাচাই করুন

ভুল হলে কারণসহ ব্যাখ্যা পাবেন, আবার চেষ্টাও করতে পারবেন।

ক্রমMaven-এর default lifecycle-এর এই phase-গুলো সঠিক ক্রমে সাজান (▲▼ চেপে সরান)

  1. 🔎 validate — pom আর প্রজেক্টের তথ্য যাচাই
  2. ⚙️ compile — src/main/java থেকে .class তৈরি
  3. 🧪 test — JUnit দিয়ে টেস্ট চালানো
  4. 📦 package — JAR তৈরি
  5. ✅ verify — প্যাকেজের মান-যাচাই ও integration test
  6. 🏠 install — JAR local repository (~/.m2)-তে রাখা

Maven প্রজেক্টে mvn package চালালে কী ঘটে?

  • শুধু package phase চলে, আগে compile করা থাকলে সেটা ব্যবহার হয়
  • validate, compile, test আর package — আগের সব phase ক্রমে চলে
  • package-এর পরে install আর deploy-ও চলে
  • compile আর package চলে, টেস্ট বাদ যায়

Maven-এ একটি dependency-র scope test দিলে কী হয়?

  • main আর test দুই কোডেই ব্যবহার করা যায়, চূড়ান্ত প্রোগ্রামেও থাকে
  • compile-এর সময় লাগে, কিন্তু চালানোর পরিবেশ নিজে এটা দেয়
  • শুধু টেস্ট কোড compile আর চালানোর সময় পাওয়া যায়
  • compile-এ লাগে না, শুধু চলার সময় লাগে

একটি Maven প্রজেক্ট A নির্ভর করে B-এর ওপর, যার dependency X:1.0. আবার A নির্ভর করে C-এর ওপর, C নির্ভর করে D-এর ওপর, আর D-এর dependency X:2.0. Maven ডিফল্টে X-এর কোন version নেবে?

  • X:1.0, কারণ এটা গাছের মূলের বেশি কাছে
  • X:2.0, কারণ এটা নতুন version
  • দুটিই classpath-এ যায়
  • build ব্যর্থ হয়, কারণ version সংঘাত আছে

Gradle build-এর আউটপুটে “> Task :compileJava UP-TO-DATE” দেখা গেলে এর মানে কী?

  • compile ব্যর্থ হয়েছে, আগের ফল রাখা হয়েছে
  • সব dependency সর্বশেষ version-এ আছে
  • task-এর আউটপুট অন্য মেশিনের build cache থেকে আনা হয়েছে
  • ইনপুট আর আউটপুট আগের মতোই, তাই task চালানোর দরকার হয়নি

প্রজেক্টে mvnw বা gradlew (wrapper) রাখার মূল উদ্দেশ্য কী?

  • মেশিনে JDK না থাকলেও Java প্রোগ্রাম চালানো
  • সব মেশিনে আর CI-তে build tool-এর একই নির্দিষ্ট version চালানো
  • সব dependency-র সর্বশেষ version স্বয়ংক্রিয়ভাবে বসানো
  • build cache চালু করে build দ্রুত করা

java -jar app.jar চালালে “no main manifest attribute, in app.jar” বার্তা আসে। সবচেয়ে সম্ভাব্য কারণ কী?

  • JAR-এ Jackson-এর মতো dependency নেই
  • main method-এর signature ভুল
  • JAR-এর MANIFEST.MF-এ Main-Class লেখা নেই
  • JAR তৈরি হয়েছে নতুন JDK দিয়ে, চালানো হচ্ছে পুরোনো JDK-তে

শব্দকোষ

লেখার মধ্যে ডটেড দাগ দেওয়া শব্দে মাউস রাখুন বা চাপুন — অর্থ দেখাবে। এই পর্বের সব নতুন শব্দ এখানে একসাথে:

মনে রাখুন

  1. build tool = প্রজেক্টের রেসিপি কার্ড। একবার বর্ণনা (GAV, dependency, plugin, Java version), তারপর যেকোনো মেশিনে এক কমান্ডে একই compile, টেস্ট, প্যাকেজ। হাতে javac -cp আর JAR নামানো বড় প্রজেক্টে JAR hell ডেকে আনে।
  2. Maven: নির্দিষ্ট ফোল্ডার (src/main/java, src/test/java), pom.xml, scope (compile, test, provided, runtime), আর lifecycle — একটি phase চাইলে আগের সব phase চলে; আসল কাজ করে phase-এ বাঁধা plugin goal.
  3. transitive dependency-র সংঘাতে Maven-এ nearest wins, Gradle-এ সবচেয়ে নতুন। dependency:tree দেখুন, version ঠিক করুন dependencyManagement বা BOM দিয়ে।
  4. Gradle: Kotlin DSL, task graph, UP-TO-DATE আর FROM-CACHE; implementation ভেতরের, api বাইরে দেখা যায়। wrapper (mvnw/gradlew) build tool-এর version পিন করে।
  5. dependency মানে আস্থা। version পিন, checksum, স্ক্যানার, SBOM, কম dependency — আর AI-এর দেওয়া প্রতিটি artifact নিজে যাচাই। executable JAR-এর জন্য manifest-এ Main-Class, এক ফাইলে বিতরণে fat JAR.

পরের পর্বে: pom-এ JUnit যোগ হয়েছে, mvn test টেস্ট চালায় — কিন্তু টেস্ট লিখবেন কীভাবে? পর্ব ৪৭-এ JUnit 5: @Test, assertion, edge case, আর “হিসাব খাতা”-র জন্য প্রথম স্বয়ংক্রিয় টেস্ট।

আরও পড়ুন · ABCL TECH-এ আপনার পরবর্তী পদক্ষেপ

এই পর্বটি শেয়ার করুন:
এই পর্বে ভুল পেয়েছেন? জানান