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

annotation আর reflection — Spring আর JUnit-এর “জাদু”র ভেতরে

Java Annotations and Reflection — Custom Annotations, Retention, Method.invoke, Dynamic Proxies and How Frameworks Work

Taught in Bangla, this lesson opens up the "magic" behind Spring and JUnit: built-in annotations, custom annotations with retention and target, reflection with Class objects, getDeclared versus get methods, invoking methods and creating objects at runtime, building a mini test runner and form validator, compile-time processing versus runtime reflection, dynamic proxies and reflection's cost.

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

  • annotation আসলে কী, কে তাকে পড়ে, আর Java-র নিজের annotation (@Override, @Deprecated, @SuppressWarnings, @FunctionalInterface, @SafeVarargs) কী পাহারা দেয়
  • @interface দিয়ে নিজের annotation বানানো, element আর default মান, retention আর target-এর অর্থ
  • Class object, field/method/constructor খোঁজা, getDeclared* বনাম get*, চলার সময় annotation পড়া আর method ডাকা
  • নিজে দুটি ছোট framework বানানো — একটি test runner আর একটি sign-up form validator
  • JUnit, Spring, Jackson, JPA কীভাবে এসব ব্যবহার করে, compile-time processing বনাম runtime reflection, dynamic proxy, আর কখন reflection এড়িয়ে চলবেন

১ · লেবেল আর আয়না — annotation আর reflection কেন একসাথে

পর্ব ২০-এ method override করার সময় ওপরে লিখেছিলেন @Override. পর্ব ২৮-এ দেখেছেন @SuppressWarnings("unchecked"). আর মডিউল ৭-এ Spring Boot আর JUnit-এ এমন @-চিহ্ন আরও অনেক দেখবেন: @Test, @Component, @GetMapping("/users"). এগুলোর নাম annotation. প্রথম যে কথাটা বুঝতে হবে, সেটা একটু অবাক করার মতো: annotation নিজে কিছুই করে না।

একটি গার্মেন্টস কারখানার গুদামের কথা ভাবুন। প্রতিটি কার্টনের গায়ে স্টিকার: “FRAGILE”, “EXPORT — Germany”, “QC PASSED”. স্টিকার নিজে কার্টন সরায় না, ভাঙা থেকেও বাঁচায় না। কিন্তু গুদামের কর্মী “FRAGILE” দেখে আস্তে নামান, শিপিং বিভাগ “Germany” দেখে সঠিক কন্টেইনারে তোলে, আর কাস্টমস “QC PASSED” দেখে ছেড়ে দেয়। স্টিকার তথ্য; কাজ করে যে স্টিকার পড়ে। annotation ঠিক এমন — কোডের গায়ে লাগানো তথ্য (metadata), আর কাজ করে সেই টুল, যে লেবেলটা পড়ে।

কারা পড়ে? তিন ধরনের পাঠক আছে, আর তারা তিনটি আলাদা সময়ে কাজ করে:

১. compiler (javac)

@Override দেখে যাচাই করে সত্যিই override হচ্ছে কি না। @Deprecated দেখে ব্যবহারকারীকে সতর্ক করে। কাজ compile-এর সময়েই শেষ।

২. annotation processor

compile-এর সময় javac-এর ভেতরে চলা প্লাগ-ইন। Lombok, MapStruct, Dagger annotation পড়ে নতুন কোড তৈরি করে বা ভুল ধরে।

৩. চলার সময় framework

JUnit, Spring, Jackson প্রোগ্রাম চলার সময় reflection দিয়ে class খুলে দেখে কোথায় কোন লেবেল আছে, তারপর সেই অনুযায়ী method ডাকে বা object বানায়।

তৃতীয় পাঠকের জন্য দরকার একটি বিশেষ ক্ষমতা — প্রোগ্রাম চলার সময় নিজের class-গুলোর ভেতরটা দেখা। এর নাম reflection, মানে প্রতিফলন: প্রোগ্রাম আয়নায় নিজেকে দেখছে। সাধারণ কোডে আপনি জানেন কোন class, কোন method — account.deposit(500) লেখেন, compiler যাচাই করে। reflection-এ নাম আগে জানা থাকে না; চলার সময় প্রশ্ন করা হয়: “এই class-এ কোন কোন method আছে? কোনটার গায়ে @Test লেবেল? সেটা ডাকো।”

মূল ধারণা

annotation আর reflection একটি জুটি। annotation দিয়ে আপনি কোডে ঘোষণা করেন (“এই method একটি টেস্ট”, “এই class একটি service”), আর framework reflection দিয়ে সেই ঘোষণা পড়ে কাজ করে। এ কারণেই Spring-এ কোথাও new UserService() না লিখেও object তৈরি হয়, JUnit-এ কোথাও main না লিখেও টেস্ট চলে। এটা জাদু নয় — এই পর্বের শেষে আপনি নিজেই এর ছোট সংস্করণ লিখে ফেলবেন।

annotation Java-তে এসেছে Java 5-এ (২০০৪ সাল, JSR 175). তার আগে মানুষ একই কাজ করত দুইভাবে: Javadoc comment-এর ভেতরে বিশেষ ট্যাগ লিখে (XDoclet নামের টুল @ejb.bean-এর মতো comment পড়ে কোড আর XML বানাত), অথবা আলাদা বিশাল XML কনফিগারেশন ফাইলে। comment-এ লেখা তথ্য compiler যাচাই করত না, বানান ভুল হলে কেউ ধরত না। annotation সেই তথ্যকে ভাষার আসল অংশ বানাল — টাইপ-যাচাই করা, IDE-তে auto-complete হওয়া। আর reflection Java-তে ছিল আরও আগে থেকে, Java 1.1 (১৯৯৭) থেকে java.lang.reflect package হিসেবে। ভাবনার প্রশ্ন ১-এ এই ইতিহাস বিস্তারিত।

২ · Java-র নিজের annotation — compiler যা পাহারা দেয়

JDK-র সাথে কয়েকটি annotation আসে, যেগুলো মূলত compiler পড়ে। এগুলো দিয়ে শুরু করা ভালো, কারণ এদের কাজ চোখে দেখা যায় — ভুল করলে compile error বা warning আসে। নিচের ছোট ফাইলে তিনটি ভুল ইচ্ছা করে রাখা হয়েছে:

import java.lang.annotation.*;

@Target(ElementType.METHOD)
@interface MyTest {}

@FunctionalInterface
interface FeeRule {
    double fee(long amount);
    String name();
}

@MyTest
class Account {
    @Override
    public String tostring() { return "Account"; }
}

class Main {
    public static void main(String[] args) { }
}
Main.java:12: error: annotation interface not applicable to this kind of declaration
@MyTest
^
Main.java:6: error: Unexpected @FunctionalInterface annotation
@FunctionalInterface
^
  FeeRule is not a functional interface
    multiple non-overriding abstract methods found in interface FeeRule
Main.java:14: error: method does not override or implement a method from a supertype
    @Override
    ^
3 errors

OpenJDK 21-এ যাচাই করা এই তিনটি বার্তা তিনটি আলাদা পাহারার গল্প বলে। নিচ থেকে শুরু করা যাক।

@Override — বানান ভুলের পাহারাদার। লেখার কথা ছিল toString, লেখা হয়েছে tostring (ছোট হাতের s). @Override না থাকলে এটা একটি নতুন, আলাদা method হতো — কোনো error নেই, কিন্তু println(account) আগের মতো অর্থহীন লেখা ছাপত। লেবেল থাকায় javac বলল: “এটা কিছুই override করছে না”। পর্ব ২০-এ এই কারণেই বলা হয়েছিল, override করলে সবসময় @Override দিন।

@FunctionalInterface — নকশার প্রতিশ্রুতি। পর্ব ৩৪-এ দেখেছেন, lambda বসানো যায় শুধু এমন interface-এ, যার abstract method ঠিক একটি (functional interface). FeeRule-এ দুটি, তাই javac আপত্তি করল। লেবেলটা না দিলেও interface একটি method থাকলে functional হয়; লেবেল দেওয়ার লাভ হলো ভবিষ্যতে কেউ ভুল করে দ্বিতীয় abstract method যোগ করলে সাথে সাথে ধরা পড়বে — যারা এই interface-এ lambda বসিয়েছে, তাদের কোড ভাঙার আগেই।

@Target — লেবেল কোথায় লাগানো যাবে। MyTest বানানো হয়েছে শুধু method-এর জন্য, অথচ লাগানো হয়েছে class-এ। এটা আমাদের নিজের annotation — অংশ ৩-এ বিস্তারিত দেখবেন।

@Deprecated — “এটা আর ব্যবহার করবেন না”

লাইব্রেরি বড় হলে কিছু পুরোনো method-এর জায়গায় ভালো method আসে। পুরোনোটা সাথে সাথে মুছে দিলে হাজারো ব্যবহারকারীর কোড ভেঙে যাবে। তাই আগে ঘোষণা: deprecated — “আছে, কিন্তু এড়িয়ে চলুন”। Java 9 থেকে @Deprecated-এ দুটি element আছে: since (কোন সংস্করণ থেকে) আর forRemoval (ভবিষ্যতে সত্যিই মুছে ফেলা হবে কি না)।

class Legacy {
    @Deprecated(since = "2.0")
    static int oldFee(int amount) { return amount / 50; }

    @Deprecated(since = "2.0", forRemoval = true)
    static int veryOldFee(int amount) { return amount / 40; }
}

class Main {
    public static void main(String[] args) {
        System.out.println(Legacy.oldFee(1000));
        System.out.println(Legacy.veryOldFee(1000));
    }
}

সাধারণভাবে javac Main.java চালালে প্রোগ্রাম compile হয় (আর চালালে 20 ও 25 ছাপে), কিন্তু javac এটুকু বলে:

Main.java:12: warning: [removal] veryOldFee(int) in Legacy has been deprecated and marked for removal
        System.out.println(Legacy.veryOldFee(1000));
                                 ^
Note: Main.java uses or overrides a deprecated API.
Note: Recompile with -Xlint:deprecation for details.
1 warning

লক্ষ করুন পার্থক্যটা: forRemoval = true হলে পুরো warning আসে, সাধারণ deprecation-এ শুধু একটি ছোট Note. বিস্তারিত দেখতে javac -Xlint:deprecation Main.java দিলে দুটি লাইনের জন্যই warning আসে — warning: [deprecation] oldFee(int) in Legacy has been deprecated আর ওপরের [removal] লাইনটি। (একটি সূক্ষ্ম নিয়ম: একই class-এর ভেতর থেকে নিজের deprecated method ডাকলে warning আসে না, তাই উদাহরণে Legacy আর Main আলাদা।) ভালো অভ্যাস হলো @Deprecated-এর সাথে Javadoc-এ @deprecated ট্যাগে বিকল্পের নাম লিখে দেওয়া: “এর বদলে cashOutFee() ব্যবহার করুন”।

annotationকোথায়কী করে
@Overridemethodoverride না হলে compile error
@Deprecated(since, forRemoval)প্রায় সব জায়গায়ব্যবহারকারীকে warning/Note; Javadoc-এ চিহ্ন
@SuppressWarnings("...")declarationনির্দিষ্ট ধরনের warning চুপ করায়, যেমন "unchecked", "deprecation"
@FunctionalInterfaceinterfaceabstract method ঠিক একটি না হলে compile error
@SafeVarargsgeneric varargs method/constructorলেখকের প্রতিশ্রুতি: varargs array-র অপব্যবহার নেই; heap pollution warning সরায়

@SuppressWarnings আর @SafeVarargs পর্ব ২৮-এ generics-এর সাথে দেখেছেন। দুটোই compiler-কে বলে “আমি জানি, আমি দায়িত্ব নিচ্ছি”। তাই এগুলো যত ছোট পরিসরে সম্ভব — পুরো class-এ নয়, একটি method বা একটি variable-এ — বসানো উচিত।

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

Python-এর @decorator দেখতে Java annotation-এর মতো, কিন্তু কাজ একেবারে আলাদা: Python decorator একটি function, যা সত্যিই চলে আর আসল function-কে মুড়ে বদলে দেয় — Python-এর decorator পর্বে দেখুন। Java annotation নিজে কোনো কোড চালায় না, শুধু তথ্য। C++-এর [[nodiscard]], [[deprecated]] attribute Java-র compiler-পড়া annotation-এর কাছাকাছি। আর reflection: C/C++-এ চলার সময় class-এর ভেতর দেখার কোনো সাধারণ ব্যবস্থা ছিল না (compile-এর পরে নামগুলোই হারিয়ে যায়); Python-এ getattr, dir() দিয়ে এটা খুব স্বাভাবিক — Java এই দুইয়ের মাঝামাঝি: সম্ভব, তবে আনুষ্ঠানিক API দিয়ে।

৩ · নিজের annotation বানানো — @interface, retention আর target

নিজের লেবেল বানাতে @interface লিখতে হয় (interface-এর আগে @)। ভেতরে যা লেখেন, সেগুলো দেখতে method-এর মতো, কিন্তু আসলে লেবেলের ঘর — এদের বলে element:

import java.lang.annotation.*;

@Retention(RetentionPolicy.RUNTIME)          // চলার সময় পর্যন্ত টিকে থাকবে
@Target(ElementType.METHOD)                  // শুধু method-এ লাগানো যাবে
@Documented                                  // Javadoc-এ দেখাবে
@interface Command {
    String name();                           // বাধ্যতামূলক element
    String help() default "";                // default থাকলে ঐচ্ছিক
    int minArgs() default 0;
}

// ব্যবহার:
@Command(name = "add", help = "নতুন খরচ যোগ করুন", minArgs = 2)
void addExpense(String[] args) { ... }

element-এর টাইপ হতে পারে শুধু primitive, String, Class, enum, অন্য annotation, অথবা এদের array — কারণ মানগুলো compile-এর সময় জানা থাকতে হয় আর .class ফাইলে লিখে রাখতে হয়। element-এর নাম value হলে ব্যবহারের সময় নাম না লিখলেও চলে: @Min(18) মানে @Min(value = 18). কোনো element-ই না থাকলে (যেমন @MyTest) সেটা শুধু একটি চিহ্ন — পর্ব ২১-এর marker interface-এর আধুনিক বিকল্প।

ওপরের @Retention, @Target, @Documented নিজেরাও annotation — তবে এরা অন্য annotation-এর গায়ে বসে। এদের বলে meta-annotation. সবচেয়ে গুরুত্বপূর্ণ retention policy: লেবেলটা কতদূর পর্যন্ত টিকে থাকবে। এই সিদ্ধান্তই ঠিক করে কোন পাঠক লেবেলটা দেখতে পাবে। ধাপে ধাপে দেখুন:

  1. সোর্স কোডMain.javaতিন ধরনের লেবেল
  2. javaccompileSOURCE এখানেই শেষ
  3. .class ফাইলMain.classCLASS আর RUNTIME থাকে
  4. JVM + reflectiongetAnnotation()শুধু RUNTIME দেখা যায়
সোর্স কোডএকটি method-এ তিনটি লেবেল
@Override                      // Retention: SOURCE
@NoRetention                   // @Retention লেখা নেই → ডিফল্ট CLASS
@Live                          // @Retention(RUNTIME)
public String toString() { ... }
javaccompiler পড়ে, যাচাই করে
@Override  → যাচাই: সত্যিই override? ✔
           → কাজ শেষ, লেবেল ফেলে দেওয়া হলো
@NoRetention, @Live → .class-এ লেখার জন্য রাখা হলো
annotation processor-ও (Lombok ইত্যাদি) এই ধাপেই চলে —
তারা SOURCE লেবেলও দেখতে পায়
Main.classbytecode-এর পাশে লেবেলের তথ্য
RuntimeInvisibleAnnotations:  @NoRetention   ← CLASS
RuntimeVisibleAnnotations:    @Live          ← RUNTIME
@Override এখানে নেই — javap দিয়ে খুঁজলেও পাবেন না
CLASS লেবেল bytecode টুলের জন্য (যেমন কোড বিশ্লেষক)
চলার সময়reflection দিয়ে জিজ্ঞাসা
m.isAnnotationPresent(NoRetention.class)  → false
m.isAnnotationPresent(Live.class)         → true
JVM class লোড করার সময় invisible অংশ পড়েই না
(OpenJDK 21-এ চালিয়ে যাচাই করা ফল)
  1. তিনটি লেবেল, তিনটি আয়ু। একই method-এ তিনটি annotation. @Override-এর retention JDK-তে SOURCE ধরা আছে। @NoRetention-এর গায়ে কোনো @Retention নেই — তখন ডিফল্ট হলো CLASS. আর @Live-এ স্পষ্ট লেখা RUNTIME.
  2. SOURCE — compile-এর পরেই শেষ। javac @Override পড়ে যাচাই করে, তারপর ফেলে দেয়। এর জীবন শুধু সোর্স কোডে। compile-এর সময় চলা annotation processor সব লেবেলই দেখতে পায়, তাই Lombok-এর অনেক annotation SOURCE retention-এর।
  3. CLASS — ফাইলে আছে, চোখে পড়ে না। CLASS আর RUNTIME দুটোই .class ফাইলে লেখা হয়, তবে আলাদা ঘরে: “runtime-এ অদৃশ্য” আর “runtime-এ দৃশ্যমান”। CLASS লেবেল কাজে লাগে এমন টুলের, যারা .class ফাইল সরাসরি পড়ে — bytecode বিশ্লেষক বা রূপান্তরকারী।
  4. RUNTIME — reflection-এর জন্য একমাত্র পথ। JVM class লোড করার সময় শুধু দৃশ্যমান ঘরটি পড়ে। তাই @NoRetention-এর জন্য false, @Live-এর জন্য true. নিজের annotation reflection দিয়ে পড়তে চাইলে @Retention(RetentionPolicy.RUNTIME) লিখতে ভুলবেন না — এটাই “আমার annotation কাজ করছে না” সমস্যার সবচেয়ে সাধারণ কারণ।

@Retention ভুলে গেলে কোনো error আসে না, কোনো warning-ও না। কোড compile হয়, চলে, শুধু isAnnotationPresent সবসময় false দেয় আর getAnnotation দেয় null. আপনার test runner একটিও টেস্ট খুঁজে পায় না, validator কিছুই যাচাই করে না — নীরবে। নিজের annotation বানালে প্রথম লাইনটি প্রায় সবসময় @Retention(RetentionPolicy.RUNTIME).

@Target আর বাকি meta-annotation

@Target বলে লেবেল কোথায় লাগানো যাবে: TYPE (class, interface, enum, record), METHOD, FIELD, PARAMETER, CONSTRUCTOR, RECORD_COMPONENT (Java 16 থেকে) ইত্যাদি; একাধিক দিতে চাইলে @Target({ElementType.FIELD, ElementType.METHOD}). ভুল জায়গায় লাগালে অংশ ২-এর সেই বার্তা: annotation interface not applicable to this kind of declaration. বাকি তিনটি:

  • @Inherited: class-এ লাগানো লেবেল subclass-ও “উত্তরাধিকারসূত্রে” পায়। @Audited class Base আর class Child extends Base হলে Child.class.isAnnotationPresent(Audited.class) দেয় true. শুধু class-এর জন্য কাজ করে, method বা interface-এর জন্য নয়।
  • @Repeatable (Java 8 থেকে): একই লেবেল একই জায়গায় একাধিকবার: @Role("admin") @Role("teller"). এর জন্য একটি “ধারক” annotation লাগে (@interface Roles { Role[] value(); }), আর compiler আসলে দুটি লেবেলকে একটি @Roles-এ মুড়ে রাখে। তাই পড়ার সময় getAnnotationsByType(Role.class) ব্যবহার করুন — এটা দেয় [admin, teller]; অথচ isAnnotationPresent(Role.class) দেয় false, কারণ সরাসরি আছে শুধু @Roles (OpenJDK 21-এ যাচাই করা)।
  • @Documented: Javadoc তৈরির সময় annotation-টি ডকুমেন্টেশনে দেখায়। public API-র লেবেলে দেওয়া ভালো।

৪ · reflection-এর শুরু — Class object আর class-এর ভেতর দেখা

reflection-এর দরজা হলো Class object. পর্ব ০৫-এ দেখেছেন, class loader একটি .class ফাইল মেমরিতে আনে। আনার সময় JVM সেই class-এর বর্ণনা হিসেবে একটি java.lang.Class object বানায় — class-এর নাম, field, method, constructor, annotation সবকিছুর তালিকা। এই object পাওয়ার তিনটি উপায়:

Account a = new Account();
Class<?> c1 = a.getClass();                 // হাতে object আছে
Class<?> c2 = Account.class;               // compile-এর সময় টাইপ জানা
Class<?> c3 = Class.forName("Account");    // শুধু নাম জানা — String থেকে (পুরো package-সহ নাম)

System.out.println(c1 == c2 && c2 == c3);                    // true
System.out.println(Class.forName("java.util.ArrayList").getName()); // java.util.ArrayList
System.out.println(int[].class.getName());                     // [I

তিনটিই একই object দেয় — প্রতিটি class loader-এ প্রতিটি class-এর Class object একটিই। তৃতীয় উপায়টি সবচেয়ে শক্তিশালী: class-এর নাম একটি String, যা কনফিগারেশন ফাইল, ডেটাবেস বা ব্যবহারকারীর কাছ থেকে আসতে পারে। পুরোনো JDBC কোডে Class.forName("com.mysql.cj.jdbc.Driver") দেখবেন পর্ব ৪৯-এ — ঠিক এই কৌশল। নাম ভুল হলে ClassNotFoundException (একটি checked exception) আসে, যার বার্তা শুধু নামটি: Acount. (array-এর নাম [I দেখে ঘাবড়াবেন না — এটা JVM-এর ভেতরের লেখার রীতি, “int-এর array”.)

getDeclared* বনাম get* — দুটি আলাদা প্রশ্ন

এবার এই class-টির ভেতর দেখা যাক:

class Account {
    private long balance = 500;
    public String owner = "Rahim";
    public Account() { }
    private Account(long b) { balance = b; }
    public long getBalance() { return balance; }
    private void audit() { }
    public void deposit(long x) { balance += x; }
}

Class object-কে দুইভাবে প্রশ্ন করা যায়, আর উত্তর একেবারে আলাদা (OpenJDK 21-এ চালানো, নাম অনুযায়ী সাজানো):

getDeclaredMethods(): [audit, deposit, getBalance]
getMethods():         [deposit, equals, getBalance, getClass, hashCode, notify, notifyAll, toString, wait]
getDeclaredFields():  [balance, owner]
getFields():          [owner]
getDeclaredConstructors(): 2টি    getConstructors(): 1টি
প্রশ্নprivate/package-private?parent class থেকে পাওয়া?
getDeclaredMethods(), getDeclaredFields()হ্যাঁ, সব accessনা — শুধু এই class-এ লেখা
getMethods(), getFields()না — শুধু publicহ্যাঁ — superclass আর interface-এরও

তাই getMethods()-এ audit নেই (private), কিন্তু Object থেকে আসা equals, hashCode, wait আছে। একটি নির্দিষ্ট method চাইলে নাম আর parameter টাইপ দিয়ে খুঁজুন: Account.class.getMethod("deposit", long.class). এমন method না থাকলে আসে NoSuchMethodException, বার্তা Account.withdraw().

ক্রমের ফাঁদ: getDeclaredMethods() আর getDeclaredFields() কোন ক্রমে ফল দেবে, তা Java-র নিয়মে নির্দিষ্ট নয় — Javadoc স্পষ্ট বলে “not sorted and are not in any particular order”. আজ আপনার মেশিনে লেখার ক্রমে এলেও অন্য JVM-এ বা পরের সংস্করণে বদলাতে পারে। যে কোডের আচরণ ক্রমের ওপর নির্ভর করে (টেস্ট চালানোর ক্রম, JSON-এ field-এর ক্রম), সেখানে সবসময় নাম অনুযায়ী সাজিয়ে নিন, অথবা নাম দিয়ে সরাসরি খুঁজুন। এই পর্বের সব উদাহরণ সাজানো ফল ছাপে।

চলার সময় annotation পড়া

Class, Method, Field, Constructor, RecordComponent — সবাই একই প্রশ্নের উত্তর দিতে পারে:

  • isAnnotationPresent(Live.class) — লেবেল আছে কি না, true/false.
  • getAnnotation(Command.class) — লেবেলটি নিজে, না থাকলে null. তারপর cmd.name(), cmd.minArgs() ডেকে element-এর মান পড়া যায় — যেন annotation একটি interface আর তার মান method.
  • getAnnotationsByType(Role.class) — repeatable লেবেলের সব কপি।

মনে রাখবেন, এই প্রতিটি প্রশ্ন কাজ করে শুধু RUNTIME retention-এর লেবেলের জন্য।

৫ · দেখা থেকে করা — method ডাকা, object বানানো, private ছোঁয়া

শুধু দেখলে framework হয় না; খুঁজে পাওয়া method ডাকতে হবে, class থেকে object বানাতে হবে। তিনটি মূল কাজ:

Class<?> cls = Class.forName("Account");
Object b = cls.getDeclaredConstructor().newInstance();    // new Account() — নাম না জেনেই
Method dep = cls.getMethod("deposit", long.class);
dep.invoke(b, 250L);                                      // b.deposit(250)
System.out.println(((Account) b).getBalance());          // 750

Field f = cls.getDeclaredField("balance");                // private field
f.setAccessible(true);                                    // access-এর পাহারা বন্ধ
f.setLong(b, 999);
System.out.println(((Account) b).getBalance());          // 999

getDeclaredConstructor() (parameter ছাড়া) খালি constructor খোঁজে, আর newInstance() সেটা ডাকে। পুরোনো কোডে cls.newInstance() দেখবেন — Java 9 থেকে এটা deprecated, কারণ constructor-এর ছোড়া checked exception-ও এটা কোনো মোড়ক ছাড়া পার করে দিত। Method.invoke(object, argument...) ডাকে method; static method হলে প্রথম argument null. invoke-এর argument Object..., তাই টাইপ মিলছে কি না compiler দেখে না। 250 দিলে সেটা Integer হয়ে যায়, আর reflection তাকে long-এ widening করে ঠিকই নেয়; কিন্তু উল্টো দিকে — int parameter-এ 5L, বা long-এ একটি String — চলার সময় আসে IllegalArgumentException: argument type mismatch (OpenJDK 21-এ যাচাই করা)। সরাসরি ডাকলে এই ভুল compile-এর সময়েই ধরা পড়ত।

InvocationTargetException — মোড়কের ভেতরের আসল ভুল

ধরুন ডাকা method নিজেই exception ছুড়ল — deposit(-5) হলে IllegalArgumentException. সরাসরি ডাকলে আপনি সেটাই পেতেন। কিন্তু invoke দিয়ে ডাকলে reflection সেটাকে একটি মোড়কে ভরে দেয়: InvocationTargetException. এর কারণ হলো আলাদা করা: “reflection-এর নিজের সমস্যা” (ভুল argument, access নেই — এগুলো সরাসরি IllegalArgumentException/IllegalAccessException) আর “আপনার ডাকা method-এর ভেতরের সমস্যা”। আসল exception থাকে e.getCause()-এ (পর্ব ২৯-এর exception chaining). অংশ ৬-এর test runner-এ এটা চোখে দেখবেন — সেখানে FAIL ... -> java.lang.IllegalArgumentException: amount must be positive লাইনটি আসে getCause() থেকে।

সবচেয়ে প্রচলিত ভুল: catch (Exception e) { log(e.getMessage()); }. InvocationTargetException-এর নিজের message সাধারণত null — লগে লেখা হয় “null”, আর আসল কারণ হারিয়ে যায়। সবসময় e.getCause() লগ করুন, অথবা আসল exception আবার ছুড়ুন।

setAccessible আর module-এর দেয়াল

setAccessible(true) reflection-কে private member ছুঁতে দেয় — পর্ব ১৭-এর encapsulation-কে পাশ কাটিয়ে। Hibernate এভাবে private field-এ ডেটাবেসের মান বসায়, Jackson private field পড়ে JSON বানায়। নিজের কোডের ক্ষেত্রে (একই module, এখানে “unnamed module”) এটা চলে। কিন্তু JDK-র নিজের ভেতরে হাত দিতে গেলে:

Field v = String.class.getDeclaredField("value");   // String-এর ভেতরের byte[]
v.setAccessible(true);                              // InaccessibleObjectException
Unable to make field private final byte[] java.lang.String.value accessible: module java.base does not "opens java.lang" to unnamed module @4b55eca5

শেষের @4b55eca5 একটি পরিচয়-সংখ্যা, প্রতিবার চালালে আলাদা হতে পারে; বাকিটা হুবহু (OpenJDK 21-এ যাচাই করা)। InaccessibleObjectException একটি unchecked exception. পর্ব ১৭-এ module-এর কথা বলা হয়েছিল: Java 9 থেকে JDK নিজে module-এ ভাগ করা, আর java.base module তার ভেতরের package-গুলো reflection-এর জন্য “open” করে না। Java 9 থেকে 15 পর্যন্ত এই নিষেধ শুধু একটি warning দিত; Java 16-এ (JEP 396) ডিফল্টে কড়া হলো, আর Java 17-এ (JEP 403) সাধারণ flag দিয়ে পুরোনো ঢিলেঢালা আচরণে ফেরার পথও বন্ধ হলো। এখন কোনো লাইব্রেরি সত্যিই দরকার হলে চালানোর সময় --add-opens java.base/java.lang=ALL-UNNAMED দিতে হয় — এবং সেটা একটি সতর্ক সংকেত, সমাধান নয়।

record আর reflection

পর্ব ২৫-এর record-এর জন্য reflection-এ বিশেষ ব্যবস্থা আছে (Java 16 থেকে): cls.isRecord() আর cls.getRecordComponents(). দ্বিতীয়টি record component-গুলো দেয় হেডারে লেখার ক্রমে — field-এর মতো অনির্দিষ্ট ক্রমে নয়, এটা Javadoc-এর নিশ্চয়তা। প্রতিটি component-এর নাম, টাইপ, annotation আর accessor method পাওয়া যায়। record Point(int x, int y)-এর জন্য লুপ চালিয়ে rc.getName() + ":" + rc.getType().getName() + "=" + rc.getAccessor().invoke(p) ছাপলে আসে x:int=3 আর y:int=4. Jackson-এর মতো লাইব্রেরি ঠিক এভাবেই record থেকে JSON বানায় (পর্ব ৩৯), আর অংশ ৬-এর validator-ও এভাবে কাজ করবে।

৬ · নিজের দুটি mini-framework — test runner আর form validator

এখন সব টুকরো জোড়া দেওয়ার সময়। প্রথমে একটি ছোট্ট JUnit: যে method-এর গায়ে @MyTest আছে, সেগুলো খুঁজে চালাবে আর পাস/ফেল জানাবে। কোড লেখার আগে প্রবাহটা দেখে নিন:

1Class<?> cls = Class.forName("FeeTests");2Method[] ms = cls.getDeclaredMethods();3Arrays.sort(ms, comparing(Method::getName));4for (Method m : ms) {5  if (!m.isAnnotationPresent(MyTest.class)) continue;6  try { m.invoke(cls.getDeclaredConstructor().newInstance());7        print("PASS " + m.getName()); }8  catch (InvocationTargetException e) {9        print("FAIL " + m.getName() + " -> " + e.getCause()); }10}
অবস্থা
cls
method তালিকা
  1. class লোড। টেস্ট class-এর নাম শুধু একটি String. আসল JUnit এই নাম পায় IDE, Maven বা Gradle থেকে, যারা আপনার src/test ফোল্ডারের class-গুলো খুঁজে দেয়।
  2. সব method-এর তালিকা, সাজানো। getDeclaredMethods() এই class-এ লেখা সব method দেয় — private, static, helper সব। ক্রম নির্দিষ্ট নয়, তাই নাম অনুযায়ী সাজানো হলো, যাতে রিপোর্ট প্রতিবার একই রকম আসে।
  3. লেবেল দিয়ে ছাঁকা। cashOutFee, check, helper-এর গায়ে @MyTest নেই — বাদ। এখানেই annotation তার কাজ করল: এটা শুধু বলে দিল কোনটা টেস্ট।
  4. প্রতিটি টেস্টের জন্য নতুন object, তারপর invoke. প্রতিবার নতুন instance বানানো হয়, যাতে একটি টেস্টের রেখে যাওয়া অবস্থা আরেকটিকে প্রভাবিত না করে — আসল JUnit 5-ও ডিফল্টে প্রতিটি টেস্টের জন্য নতুন instance বানায়।
  5. ফেল হলে মোড়ক খোলা। zeroAmount-এর ভেতরে IllegalArgumentException হলো, invoke সেটাকে InvocationTargetException-এ মুড়ে দিল। getCause() খুলে আসল কারণ ছাপা হলো। (feeFor200-ও একইভাবে ফেল করে — চালিয়ে দেখবেন।)
  6. রিপোর্ট। কতগুলো পাস, মোট কতগুলো। একটি টেস্ট ফেল করলেও বাকিগুলো চলে — কারণ প্রতিটি invoke আলাদা try-এর ভেতরে। এটাই একটি test framework-এর মূল প্রতিশ্রুতি।

এবার পুরো প্রোগ্রাম। bKash-ধাঁচের cash-out চার্জ (১.৮৫%) হিসাবের একটি method আর তার তিনটি টেস্ট — একটি ইচ্ছা করে ভুল প্রত্যাশা নিয়ে লেখা:

import java.lang.annotation.*;
import java.lang.reflect.*;
import java.util.*;
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
@interface MyTest {}
class FeeTests {
    static double cashOutFee(long amount) {
        if (amount <= 0) throw new IllegalArgumentException("amount must be positive");
        return amount * 185 / 10000.0;                    // 1.85%
    }
    @MyTest void feeFor1000() { check(cashOutFee(1000) == 18.5); }
    @MyTest void zeroAmount() { cashOutFee(0); }
    @MyTest void feeFor200()  { check(cashOutFee(200) == 4.0); }
    void helper() { }                                     // @MyTest নেই
    static void check(boolean ok) { if (!ok) throw new AssertionError("check failed"); }
}
class Main {
    public static void main(String[] args) throws Exception {
        Class<?> cls = Class.forName("FeeTests");
        Method[] methods = cls.getDeclaredMethods();
        Arrays.sort(methods, Comparator.comparing(Method::getName));
        int passed = 0, total = 0;
        for (Method m : methods) {
            if (!m.isAnnotationPresent(MyTest.class)) continue;
            total++;
            try {
                m.invoke(cls.getDeclaredConstructor().newInstance());
                passed++;
                System.out.println("PASS " + m.getName());
            } catch (InvocationTargetException e) {
                System.out.println("FAIL " + m.getName() + " -> " + e.getCause());
            }
        }
        System.out.println(passed + "/" + total + " passed");
    }
}

আউটপুট চার লাইন: PASS feeFor1000, FAIL feeFor200 -> java.lang.AssertionError: check failed, FAIL zeroAmount -> java.lang.IllegalArgumentException: amount must be positive, আর 1/3 passed. ১০০০ টাকার চার্জ ১৮.৫ — প্রত্যাশা ঠিক, পাস। ২০০ টাকার আসল চার্জ ৩.৭, কিন্তু টেস্ট লেখক ধরে নিয়েছিলেন ৪.০ — check ব্যর্থ হয়ে AssertionError দিল। zeroAmount-এ আসল method নিজেই exception দিয়েছে। দুই ক্ষেত্রেই লাইনের শেষের লেখা এসেছে e.getCause() থেকে — e নিজে ছাপলে দেখাত শুধু মোড়কটি। helper-এর লেবেল নেই, তাই চলেইনি। আর @Retention লাইনটি মুছে চালিয়ে দেখুন কী হয় (অনুশীলন ১)। সব class public ছাড়া, কারণ অনলাইন runner ফাইলটিকে অন্য নামে compile করে।

দ্বিতীয় framework: sign-up form-এর validator

একটি অ্যাপের sign-up form-এ নাম ফাঁকা থাকা চলবে না, মোবাইল নম্বর বাংলাদেশি ফরম্যাটে (01 দিয়ে শুরু, তারপর 3–9, মোট ১১ অঙ্ক), বয়স অন্তত ১৮. প্রতিটি নিয়মের জন্য if-else লেখার বদলে নিয়মগুলো লেবেল হিসেবে ডেটার গায়েই লিখে দেওয়া যায়, আর একটি সাধারণ validator সব record-এর জন্য কাজ করবে। Spring-এর Jakarta Bean Validation (@NotBlank, @Min, @Pattern) ঠিক এই ধারণা:

import java.lang.annotation.*;
import java.lang.reflect.*;
import java.util.*;
@Retention(RetentionPolicy.RUNTIME) @Target(ElementType.RECORD_COMPONENT)
@interface NotBlank {}
@Retention(RetentionPolicy.RUNTIME) @Target(ElementType.RECORD_COMPONENT)
@interface Min { int value(); }
@Retention(RetentionPolicy.RUNTIME) @Target(ElementType.RECORD_COMPONENT)
@interface Regex { String value(); String message() default "invalid format"; }
record SignUp(@NotBlank String name,
              @Regex(value = "01[3-9]\\d{8}", message = "not a BD mobile number") String phone,
              @Min(18) int age) {}
class Main {
    static List<String> validate(Record r) throws ReflectiveOperationException {
        List<String> errors = new ArrayList<>();
        for (RecordComponent c : r.getClass().getRecordComponents()) {
            Object v = c.getAccessor().invoke(r);
            if (c.isAnnotationPresent(NotBlank.class) && v.toString().isBlank())
                errors.add(c.getName() + ": must not be blank");
            if (c.getAnnotation(Regex.class) instanceof Regex rx && !v.toString().matches(rx.value()))
                errors.add(c.getName() + ": " + rx.message());
            if (c.getAnnotation(Min.class) instanceof Min min && (int) v < min.value())
                errors.add(c.getName() + ": must be at least " + min.value());
        }
        return errors;
    }
    public static void main(String[] args) throws Exception {
        System.out.println(validate(new SignUp("Rahim", "01712345678", 22)));
        System.out.println(validate(new SignUp("  ", "1712-345", 15)));
    }
}

আউটপুট দুই লাইন: [] আর [name: must not be blank, phone: not a BD mobile number, age: must be at least 18]. প্রথম form-এ সব ঠিক, তাই ভুলের তালিকা খালি। দ্বিতীয়টিতে তিনটি নিয়মই ভেঙেছে, আর ভুলগুলো এসেছে record-এর হেডারের ক্রমে — কারণ getRecordComponents() সেই ক্রমের নিশ্চয়তা দেয়। খেয়াল করুন validate-এর ভেতরে কোথাও SignUp, name বা phone শব্দ নেই: যেকোনো record-এ এই লেবেলগুলো লাগালে একই method কাজ করবে। নতুন নিয়ম (ধরুন @Email) যোগ করতে শুধু নতুন annotation আর একটি if. getAnnotation(...) instanceof Regex rx হলো পর্ব ২৬-এর pattern matching — null হলে instanceof false দেয়, তাই আলাদা null-যাচাই লাগল না। Target RECORD_COMPONENT দেওয়ায় লেবেলটি component-এর গায়ে থাকে; FIELD দিলে getDeclaredField দিয়ে পড়তে হতো। "01[3-9]\\d{8}"-এর দুটি backslash Java String-এর নিয়মে, regex-এ এটা \d.

একই কৌশলে আরও অনেক কিছু বানানো যায়। পর্ব ২২-এর মাঝপথের প্রজেক্টের CLI টুলে প্রতিটি কমান্ডের জন্য একটি বড় switch ছিল। এর বদলে অংশ ৩-এর @Command(name = "add", minArgs = 2) প্রতিটি method-এর গায়ে লাগিয়ে, শুরুতে reflection দিয়ে একবার সব method পড়ে একটি Map<String, Method> বানালে নতুন কমান্ড যোগ করা মানে শুধু নতুন একটি method লেখা — switch ছোঁয়াই লাগবে না। পর্ব ২২-এ উল্লেখ করা picocli লাইব্রেরি ঠিক এভাবেই কাজ করে।

৭ · আসল framework যা করে — আর compile-time বনাম runtime

আপনার দুটি mini-framework বুঝলে বড় framework-গুলো আর রহস্যময় থাকে না। নিচের কোডগুলো চালানো যাবে না (অনলাইন runner-এ এসব লাইব্রেরি নেই); মডিউল ৭-এ এদের সাথে বিস্তারিত দেখা হবে। এখানে শুধু দেখুন প্রতিটি লেবেলের পেছনে কোন পাঠক কী করে:

// JUnit 5 (পর্ব ৪৭) — আপনার test runner-এর বড় ভাই
class FeeTest {
    @Test void feeFor1000() { assertEquals(18.5, Fees.cashOut(1000)); }
}

// Spring (পর্ব ৫০) — class খুঁজে নিজে object বানায়, নির্ভরতা জুড়ে দেয়
@Component
class PaymentService {
    private final SmsSender sms;
    PaymentService(SmsSender sms) { this.sms = sms; }   // Spring নিজে SmsSender দেয়
}

// Jackson (পর্ব ৩৯) — JSON-এর নাম আর Java-র নাম আলাদা হলে
record UserDto(@JsonProperty("full_name") String fullName, int age) {}

// JPA / Hibernate (পর্ব ৪৯-এর পরে) — class থেকে ডেটাবেস টেবিল
@Entity
class Customer {
    @Id @GeneratedValue private Long id;
    private String phone;
}
  • JUnit: Maven/Gradle টেস্ট class-গুলোর নাম দেয়; JUnit reflection দিয়ে @Test-ওয়ালা method খোঁজে, প্রতিটির জন্য নতুন object বানিয়ে ডাকে, exception ধরে রিপোর্ট বানায় — অংশ ৬-এর রানারের মতোই।
  • Jackson: UserDto-এর component reflection দিয়ে পড়ে; @JsonProperty থাকলে JSON-এ সেই নাম ব্যবহার করে — {"full_name":"Rahim","age":22}.
  • JPA: @Entity দেখে class-টিকে টেবিলের সাথে মেলায়, @Id দেখে primary key চেনে, ডেটাবেস থেকে আসা মান setAccessible দিয়ে private field-এ বসায়।
  • Spring: সবচেয়ে বড় কাজটা করে — নিচের stepper-এ দেখুন।
  1. classpath স্ক্যানcom.shop.*.class ফাইল খোঁজা
  2. লেবেল ছাঁকা@Componentকোনগুলো bean
  3. নির্ভরতা বোঝাconstructorকে কাকে চায়
  4. বানানো ও জোড়াnewInstancecontainer-এ রাখা
ধাপ ১অ্যাপ চালুর মুহূর্তে
com/shop/PaymentService.class
com/shop/BkashSmsSender.class
com/shop/MoneyUtil.class
নির্দিষ্ট package-এর নিচের সব .class ফাইলের তালিকা
ধাপ ২কার গায়ে @Component?
PaymentService   @Component  ✔ bean
BkashSmsSender   @Component  ✔ bean (implements SmsSender)
MoneyUtil        —           ✘ সাধারণ class, Spring ছোঁবে না
ধাপ ৩constructor-এর parameter পড়া
PaymentService(SmsSender sms)
  → দরকার: একটি SmsSender
  → কে SmsSender? BkashSmsSender
ক্রম: আগে BkashSmsSender, তারপর PaymentService
ধাপ ৪reflection দিয়ে বানানো
sender  = BkashSmsSender-এর constructor.newInstance()
payment = PaymentService-এর constructor.newInstance(sender)
container: { smsSender → sender, paymentService → payment }
আপনার কোডে কোথাও new PaymentService(...) নেই
  1. স্ক্যান। Spring Boot অ্যাপ চালু হওয়ার সময় আপনার main class-এর package আর তার নিচের সব package-এর .class ফাইল খোঁজে। একে বলে component scanning. এটা চালুর সময় একবারই হয়।
  2. লেবেল ছাঁকা। যাদের গায়ে @Component (অথবা তার “আত্মীয়” @Service, @Repository, @RestController — এরা নিজেরাই @Component দিয়ে চিহ্নিত), তারাই Spring-এর দায়িত্বে যায়। এদের বলে bean.
  3. নির্ভরতা। reflection দিয়ে constructor-এর parameter টাইপ পড়া হয়। PaymentService-এর চাই একটি SmsSender; bean-দের মধ্যে কে সেটা implement করে, Spring খুঁজে বের করে। এটাই পর্ব ২১-এর dependency injection — শুধু হাতে না করে framework করছে।
  4. বানানো ও জোড়া। সঠিক ক্রমে newInstance ডেকে object বানানো হয়, নির্ভরতা constructor-এ ঢুকিয়ে দেওয়া হয়, আর সব একটি “container”-এ রাখা হয়। আপনি কোথাও new লেখেননি, অথচ সব তৈরি — “জাদু”টা আসলে এই চার ধাপ।

compile-এর সময় annotation processing — reflection ছাড়াই

সব annotation-এর কাজ চলার সময় হয় না। Java 6 থেকে (JSR 269) javac-এ প্লাগ-ইন লাগানো যায়: annotation processor, যা javax.annotation.processing API দিয়ে compile-এর সময়ই annotation পড়ে নতুন সোর্স ফাইল তৈরি করে বা ভুল ধরে।

  • MapStruct: @Mapper interface দেখে Entity থেকে DTO-তে কপি করার আসল Java কোড লিখে দেয় — চলার সময় কোনো reflection নেই।
  • Dagger: Google-এর dependency injection লাইব্রেরি, Android-এ খুব প্রচলিত। Spring যে কাজ চালুর সময় reflection দিয়ে করে, Dagger সেটা compile-এর সময় সাধারণ new-ওয়ালা কোড বানিয়ে করে।
  • Lombok: @Getter, @Data দেখে getter, equals ইত্যাদি যোগ করে। তবে Lombok নিয়ম মেনে নতুন ফাইল বানায় না; javac-এর ভেতরের API ব্যবহার করে আপনার class-ই বদলে দেয় — ভাবনার প্রশ্ন ৪-এ এই বিতর্ক।
বিষয়compile-time processingruntime reflection
কখন কাজ হয়build-এর সময়, একবারপ্রতিবার অ্যাপ চালুর সময়
ভুল কখন ধরা পড়েcompile error হিসেবেচলার সময় exception হিসেবে
চালুর গতিদ্রুত — তৈরি কোড সাধারণ Javaস্ক্যান আর reflection-এ সময় লাগে
নমনীয়তাকম — সব compile-এর সময় জানা থাকতে হয়বেশি — plugin, কনফিগ থেকে class-এর নাম
উদাহরণDagger, MapStruct, Micronaut, LombokSpring (প্রচলিত রূপ), JUnit, Jackson, Hibernate

এই পার্থক্য সবচেয়ে বেশি চোখে পড়ে GraalVM native-image-এ (পর্ব ২২-এ উল্লেখ করা)। native-image build-এর সময়ই হিসাব করে কোন class আর method লাগবে, বাকি সব বাদ দেয়। কিন্তু Class.forName(someString)-এর String কী হবে, তা build-এর সময় জানা নেই — তাই reflection-এ ব্যবহৃত class আর member-এর তালিকা আলাদা কনফিগারেশন ফাইলে আগেভাগে বলে দিতে হয়, নাহলে চলার সময় সেগুলো “নেই”। এ কারণেই Spring Boot 3 build-এর সময় অনেক কাজ আগাম সেরে রাখার (AOT) ব্যবস্থা এনেছে, আর Micronaut ও Quarkus-এর মতো নতুন framework শুরু থেকেই compile-time-এর পথে গেছে।

৮ · dynamic proxy, MethodHandle — আর reflection-এর দাম

reflection-এর আরেকটি শক্তিশালী রূপ: চলার সময় একটি interface-এর নকল implementation বানানো, যার প্রতিটি method ডাক একটি জায়গায় এসে জমা হয়। এর নাম dynamic proxy (java.lang.reflect.Proxy). ধরুন একটি PaymentService আছে, আর প্রতিটি পেমেন্টের আগে-পরে লগ লিখতে চান — কিন্তু আসল class-এ হাত না দিয়ে:

interface PaymentService { String pay(String to, long amount); }

class BkashPayment implements PaymentService {
    public String pay(String to, long amount) { return "TX-" + to + "-" + amount; }
}

// main-এর ভেতরে:
PaymentService real = new BkashPayment();
PaymentService logged = (PaymentService) Proxy.newProxyInstance(
        PaymentService.class.getClassLoader(),
        new Class<?>[] { PaymentService.class },
        (proxy, method, params) -> {                       // InvocationHandler
            System.out.println("-> " + method.getName() + Arrays.toString(params));
            Object result = method.invoke(real, params);    // আসল object-এ পাঠানো
            System.out.println("<- " + result);
            return result;
        });
logged.pay("01712345678", 500);
-> pay[01712345678, 500]
<- TX-01712345678-500

(OpenJDK 21-এ চালিয়ে যাচাই করা আউটপুট।) logged দেখতে একটি সাধারণ PaymentService, যে কোড এটা ব্যবহার করে সে পার্থক্য জানেই না। ভেতরে কী ঘটে:

  1. callerlogged.pay(...)সাধারণ ডাক
  2. proxy object$ProxyJVM-এর বানানো class
  3. handlerinvoke(...)আপনার lambda
  4. আসল objectBkashPaymentআসল কাজ
ধাপ ১caller কিছুই জানে না
PaymentService s = logged;
s.pay("01712345678", 500);
টাইপ PaymentService — interface অনুযায়ী সাধারণ ডাক
ধাপ ২proxy class-এর ভেতরের pay
public String pay(String a, long b) {
    return (String) handler.invoke(this, payMethod, new Object[]{a, b});
}
Proxy.newProxyInstance চলার সময় এমন একটি class তৈরি করেছে;
প্রতিটি method একই handler-এ পাঠায় (ধারণাগত রূপ)
ধাপ ৩handler — আগে, মাঝে, পরে
method = pay,  params = [01712345678, 500]
print("-> pay[01712345678, 500]")      ← আগে
result = method.invoke(real, params)   ← আসল কাজে পাঠানো
print("<- TX-01712345678-500")         ← পরে
ধাপ ৪আসল object কাজ করে, ফল ফিরে যায়
BkashPayment.pay → "TX-01712345678-500"
      ↑ handler → proxy → caller
handler চাইলে ডাক আটকাতে, argument বদলাতে,
সময় মাপতে বা transaction শুরু-শেষ করতে পারত
  1. caller-এর চোখে সব স্বাভাবিক। caller শুধু interface চেনে। এটাই proxy-র শক্তি: যে কোড ব্যবহার করে, তাকে একটুও বদলাতে হয় না।
  2. proxy class. Proxy.newProxyInstance চলার সময় একটি নতুন class তৈরি করে, যা দেওয়া interface implement করে। এর প্রতিটি method-এর কাজ একটাই — InvocationHandler-এর invoke ডাকা, সাথে কোন method আর কোন argument. Proxy.isProxyClass(logged.getClass()) দেয় true.
  3. handler — এক জায়গায় সব ডাক। interface-এ ২০টি method থাকলেও handler একটিই। লগ লেখা এখানে একবার লিখলেই সব method-এ কাজ করে। ভেতরে method.invoke(real, params) — অংশ ৫-এর সেই একই reflection ডাক।
  4. আসল কাজ। BkashPayment নিজের কাজ করে, ফল একই পথে ফিরে যায়। Spring-এর @Transactional ঠিক এভাবে কাজ করে: আপনার service-এর চারপাশে একটি proxy, যে method-এর আগে transaction শুরু করে আর পরে commit বা rollback করে। একে বলে AOP. (একটি সীমা: java.lang.reflect.Proxy শুধু interface-এর জন্য; class-এর জন্য Spring অন্য bytecode লাইব্রেরি ব্যবহার করে।)

proxy-র ভেতরের handler যদি নিজেই exception ছোড়ে বা method.invoke থেকে InvocationTargetException পায়, সেটা সাবধানে সামলাতে হয় — সাধারণত getCause() আবার ছুড়ে দিয়ে, যাতে caller আসল exception-টাই পায়, মোড়ক নয়।

MethodHandle — আধুনিক, দ্রুততর বিকল্প

Java 7-এ এসেছে java.lang.invoke package-এর MethodHandle: একটি method-এর সরাসরি “হাতল”, যা খোঁজার সময়েই access যাচাই করে নেয়, ফলে প্রতিটি ডাকে বারবার যাচাই লাগে না, আর JIT একে সাধারণ method ডাকের মতো inline করতে পারে।

MethodHandle mh = MethodHandles.lookup().findVirtual(
        Account.class, "getBalance", MethodType.methodType(long.class));
long bal = (long) mh.invokeExact(account);      // account.getBalance()

পর্ব ৩৪-এর lambda আর পর্ব ০৮-এর String জোড়া যে invokedynamic দিয়ে চলে, তার ভিত্তিও MethodHandle. Java 18 থেকে (JEP 416) Method.invoke নিজেও ভেতরে MethodHandle দিয়ে নতুন করে লেখা হয়েছে। সাধারণ অ্যাপ লেখায় MethodHandle প্রায় লাগে না; লাইব্রেরি আর framework লেখকদের জন্য এটা জানা দরকার।

reflection-এর দাম — কখন এড়িয়ে চলবেন

গতি

method খোঁজা (getMethod) ধীর; বারবার ডাকলে Method object একবার খুঁজে রেখে দিন। invoke-এ প্রতিবার argument-এর array, boxing, access যাচাই — সরাসরি ডাকের চেয়ে বেশি কাজ। চালুর সময় বড় স্ক্যানও সময় নেয়।

encapsulation ভাঙা

setAccessible দিয়ে private ছোঁয়া মানে class-এর লেখকের দেওয়া নিয়ম পাশ কাটানো। পরের সংস্করণে সেই private field-এর নাম বদলালে আপনার কোড নীরবে ভাঙবে।

refactoring-এর ঝুঁকি

getMethod("deposit")-এ নাম একটি String. IDE দিয়ে method-এর নাম বদলালে String বদলায় না; compiler কিছু বলে না, ভুল ধরা পড়ে চলার সময়।

নিরাপত্তা

ব্যবহারকারীর দেওয়া String থেকে Class.forName বা method ডাকা মানে বাইরের কাউকে ঠিক করতে দেওয়া কোন কোড চলবে। Java deserialization-এর বহু বড় নিরাপত্তা-ছিদ্র এই ধরনের পথ দিয়েই এসেছে।

নিয়মটা সহজ: reflection framework লেখার হাতিয়ার, অ্যাপ লেখার নয়। ব্যবসায়িক কোডে “নাম দেখে method ডাকা”র দরকার মনে হলে আগে ভাবুন — একটি interface আর polymorphism (পর্ব ২০–২১), একটি Map<String, Runnable> বা lambda, অথবা sealed type-এর ওপর switch (পর্ব ২৬) দিয়ে কি একই কাজ হয়? প্রায় সবসময় হয়, আর তখন compiler আবার আপনার পাহারায় ফেরে।

পরীক্ষা ও ইন্টারভিউতে

annotation কী, আর এটা কি কোড চালায়? কোডের সাথে জুড়ে দেওয়া metadata; নিজে কিছু চালায় না, compiler/processor/framework পড়ে কাজ করে।
RetentionPolicy কয়টি ও কী কী? SOURCE, CLASS (ডিফল্ট), RUNTIME; reflection দিয়ে পড়তে RUNTIME লাগে।
getMethods() আর getDeclaredMethods()-এর পার্থক্য? প্রথমটি public, উত্তরাধিকারসহ; দ্বিতীয়টি এই class-এ লেখা সব access-এর, উত্তরাধিকার ছাড়া।
InvocationTargetException কখন আসে? Method.invoke দিয়ে ডাকা method নিজে exception ছুড়লে; আসল exception getCause()-এ।
Spring কীভাবে new ছাড়া object বানায়? component scanning করে @Component class খোঁজে, reflection দিয়ে constructor ডেকে নির্ভরতা জুড়ে দেয়।

৯ · AI-এর যুগে — reflection-এর শর্টকাট চিনে ফেলা

AI টুল reflection খুব সহজে লিখে ফেলে, আর ঠিক এখানেই বিপদ। কোনো সমস্যায় আটকে গেলে (private field-এ মান বসানো দরকার, অজানা টাইপের object থেকে মান পড়া দরকার) AI প্রায়ই একটি “চতুর” reflection সমাধান দেয়, যা আজ চলে কিন্তু নকশাকে দুর্বল করে আর পরের Java সংস্করণে ভাঙতে পারে। নিজের annotation বানাতে বললে আবার উল্টো সমস্যা — খুব সুন্দর annotation আর processor, শুধু @Retention(RUNTIME) নেই।

AI যা করেকেন সমস্যাআপনি যা করবেন
ব্যবসায়িক কোডে নাম দেখে method ডাকে (getMethod("handle" + type))compiler-এর যাচাই নেই, refactor করলে নীরবে ভাঙে, ধীরinterface, Map<String, Handler> বা sealed switch চান
নকশার সমস্যা ঢাকতে setAccessible(true)encapsulation ভাঙে; JDK class-এ InaccessibleObjectExceptionপ্রশ্ন করুন: সঠিক public API বা constructor কোথায়?
নিজের annotation-এ @Retention(RUNTIME) ভুলে যায়reflection লেবেল দেখতেই পায় না — error ছাড়াই কিছু কাজ করে নাপ্রতিটি নিজস্ব annotation-এ retention আর target যাচাই করুন
catch (Exception e)-এ e.getMessage() লগ করেInvocationTargetException-এর message null; আসল কারণ হারায়getCause() লগ করা বা আবার ছোড়া চান
getDeclaredMethods()-এর ক্রমের ওপর নির্ভর করেক্রম নির্দিষ্ট নয়; অন্য JVM-এ আচরণ বদলায়নাম অনুযায়ী সাজানো বা নাম দিয়ে খোঁজা
প্রতিটি ডাকে getMethod খোঁজেhot path-এ অকারণে ধীরMethod বা MethodHandle একবার খুঁজে cache করুন

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

আমি Java 21 ব্যবহার করছি। [সমস্যার বর্ণনা — যেমন: একটি CLI টুলে কমান্ডের নাম থেকে সঠিক method চালাতে চাই] শর্ত: ১. আগে বলো, reflection ছাড়া (interface, Map<String, ...>, lambda, sealed interface + switch) এটা করা যায় কি না। গেলে সেটাই দাও, আর reflection কেন দরকার নেই তা ব্যাখ্যা করো। ২. reflection সত্যিই দরকার হলে কারণ লেখো (যেমন: আমি একটি framework/লাইব্রেরি লিখছি)। ৩. নিজস্ব annotation-এ অবশ্যই @Retention(RetentionPolicy.RUNTIME) আর সঠিক @Target দেবে। ৪. Method.invoke-এর InvocationTargetException আলাদা করে ধরবে আর getCause() ব্যবহার করবে; শুধু e.getMessage() লগ করবে না। ৫. setAccessible(true) ব্যবহার করবে না, যদি না আমি স্পষ্ট অনুমতি দিই; JDK-র ভেতরের class-এ কখনো নয়। ৬. getDeclaredMethods()/getDeclaredFields()-এর ক্রমের ওপর নির্ভর করবে না — নাম অনুযায়ী সাজাবে বা নাম দিয়ে খুঁজবে। ৭. Method object বারবার না খুঁজে একবার খুঁজে রাখবে। ৮. শেষে একটি ছোট main দিয়ে দেখাও: সঠিক ক্ষেত্র, একটি ভুল নাম, আর ডাকা method exception ছুড়লে কী ছাপা হয়।

AI-এর লেখা annotation/reflection কোড হাতে পেলে — যাচাইয়ের চেকলিস্ট

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

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

প্র ০১Java 5-এর আগে মানুষ comment-এর ভেতরে XDoclet ট্যাগ লিখে বা XML ফাইলে metadata রাখত। JSR 175 annotation আনল কেন, আর এতে কী সমস্যার সমাধান হলো?

২০০০-এর দশকের শুরুতে enterprise Java মানে ছিল EJB 2.x. একটি সাধারণ “bean” লিখতে লাগত কয়েকটি Java ফাইল (interface, home interface, implementation) আর একটি বড় XML deployment descriptor, যেখানে লেখা থাকত কোন class কোন ধরনের bean, কোন method-এ transaction লাগবে, কে access পাবে। কোড এক জায়গায়, তার সম্পর্কে তথ্য আরেক জায়গায় — একটি method-এর নাম বদলালে XML-এ বদলাতে ভুলে গেলে ভুল ধরা পড়ত সার্ভারে deploy করার পরে।

এই যন্ত্রণা কমাতে XDoclet নামের টুল জনপ্রিয় হয়: Javadoc comment-এর ভেতরে @ejb.bean name="Account"-এর মতো ট্যাগ লিখলে টুলটি সেগুলো পড়ে XML আর বাড়তি interface বানিয়ে দিত। তথ্য অন্তত কোডের পাশে চলে এলো। কিন্তু comment হলো comment — compiler এর বানান যাচাই করে না, টাইপ জানে না, IDE সাহায্য করতে পারে না, আর compile-এর পরে এটা পুরোপুরি হারিয়ে যায়, তাই চলার সময় কেউ পড়তে পারে না।

JSR 175 (“A Metadata Facility for the Java Programming Language”) এই ধারণাকে ভাষার ভেতরে আনল, Java 5-এর অংশ হিসেবে ২০০৪ সালে। নতুন যা পাওয়া গেল: annotation একটি আসল টাইপ (@interface), তাই ভুল নাম বা ভুল element-এ compile error; element-এর টাইপ যাচাই হয়; @Target দিয়ে কোথায় বসবে তা নিয়ন্ত্রণ; আর @Retention দিয়ে লেখকই ঠিক করেন তথ্য compile-এর পরে .class ফাইলে আর চলার সময় reflection-এ থাকবে কি না।

এর প্রভাব হলো বিশাল। Java EE 5 (২০০৬) আর EJB 3.0 XML-এর বদলে annotation গ্রহণ করল; JUnit 4 (২০০৬) “নাম test দিয়ে শুরু হতে হবে” নিয়মের বদলে @Test আনল; Spring 2.5 (২০০৭) @Component আর @Autowired আনল। পরের ধাপে Java 6-এর JSR 269 compile-time processing-এর মানসম্মত API দিল, আর Java 8-এ @Repeatable ও type annotation (JSR 308) এলো।

তবে একটি মূল্যও আছে। annotation তথ্যকে আবার কোডের সাথে বেঁধে ফেলে: একটি class-এর ডেটাবেস টেবিলের নাম বদলাতে এখন Java কোড বদলে আবার compile করতে হয়, যেখানে XML-এ শুধু ফাইল বদলালেই হতো। তাই বড় সিস্টেমে আজও কিছু কনফিগারেশন (সার্ভারের ঠিকানা, পাসওয়ার্ড) annotation-এ নয়, বাইরের ফাইলে বা environment variable-এ থাকে।

মূল উপলব্ধি: annotation-এর আসল উদ্ভাবন “@ চিহ্ন” নয় — কোড-সম্পর্কিত তথ্যকে comment বা দূরের XML থেকে তুলে এনে compiler-যাচাই করা, টাইপসহ ভাষার অংশ বানানো; আর কোন তথ্য কোডের সাথে থাকবে, কোনটা বাইরে, সেই বিচার এখনো আপনার।

প্র ০২“Convention over configuration” কী? Spring আর Java EE-তে XML থেকে annotation-এ সরে আসা এই দর্শনের সাথে কীভাবে যুক্ত, আর এর অন্ধকার দিক কী?

Convention over configuration কথাটি জনপ্রিয় হয় ২০০৪-০৫ সালে Ruby on Rails-এর সাথে, যার স্রষ্টা David Heinemeier Hansson. ধারণাটা হলো: প্রতিটি সিদ্ধান্ত কনফিগারেশন ফাইলে স্পষ্ট লিখতে না বলে framework কিছু যুক্তিসংগত ডিফল্ট রীতি ধরে নেবে; আপনি শুধু রীতির বাইরে গেলে লিখবেন। Customer নামের model-এর টেবিল নিজে থেকেই customers; আলাদা কিছু চাইলে তখন বলবেন।

Rails-এর সরলতা দেখে Java জগতে চাপ তৈরি হয়, কারণ তখনকার Spring অ্যাপে শত শত লাইনের XML ছিল সাধারণ ব্যাপার — প্রতিটি bean, প্রতিটি নির্ভরতা হাতে লেখা। Java 5-এর annotation সমাধানের হাতিয়ার দিল। Spring 2.5 component scanning আনল: XML-এ প্রতিটি bean না লিখে শুধু একটি package-এর নাম, আর class-এর গায়ে @Component. Spring 3.0 Java-কোডে কনফিগারেশন (@Configuration, @Bean) আনল। আর ২০১৪ সালে Spring Boot convention over configuration-কে চরমে নিয়ে গেল: classpath-এ একটি ডেটাবেস driver থাকলে সে নিজেই ধরে নেয় আপনি ডেটাবেস চান, একটি web লাইব্রেরি থাকলে নিজেই সার্ভার চালু করে — একে বলে auto-configuration. JPA-তেও একই দর্শন: @Entity দিলেই field-গুলো ডিফল্টে কলাম।

লাভটা স্পষ্ট: নতুন প্রজেক্ট মিনিটের মধ্যে চালু, কম boilerplate, তথ্য কোডের পাশে। কিন্তু অন্ধকার দিকও আছে, আর অভিজ্ঞ ডেভেলপাররা তা ভালো করে জানেন। প্রথমত, “অদৃশ্য আচরণ”: কোনো bean কেন তৈরি হলো, কোন ডিফল্ট কোথা থেকে এলো, তা কোড পড়ে বোঝা যায় না — জানতে হয় framework-এর রীতি। নতুনদের কাছে এটা জাদুর মতো লাগে, আর জাদু ভেঙে গেলে ডিবাগ করা কঠিন। দ্বিতীয়ত, ভুল দেরিতে ধরা পড়ে: দুটি bean একই interface implement করলে বা একটি bean খুঁজে না পেলে error আসে অ্যাপ চালুর সময়, compile-এর সময় নয়। তৃতীয়ত, “annotation hell”: একটি class-এর ওপর ছয়-সাতটি annotation, যাদের পারস্পরিক প্রভাব বুঝতে ডকুমেন্টেশন ঘাঁটতে হয়।

এর প্রতিক্রিয়ায় আধুনিক Spring নিজেও কিছুটা স্পষ্টতার দিকে ফিরেছে — field injection (@Autowired private field) বাদ দিয়ে constructor injection-এর পরামর্শ, যাতে নির্ভরতা কোডে দেখা যায় আর টেস্টে হাতে বসানো যায়।

মূল উপলব্ধি: convention over configuration লেখার সময় বাঁচায়, কিন্তু বোঝার দায় সরিয়ে দেয় framework-এর রীতির ওপর — তাই এই পর্বের মতো ভেতরের কৌশল বোঝা ছাড়া Spring “ব্যবহার” করা যায়, নিয়ন্ত্রণ করা যায় না।

প্র ০৩reflection “ধীর” — এই কথা কতটা সত্য? Method.invoke-এর “inflation” কী ছিল, আর Java 18-এর JEP 416 ভেতরে কী বদলাল?

“reflection ধীর” কথাটির তিনটি আলাদা অংশ আছে, আর এদের দাম আলাদা। প্রথম, খোঁজা: getMethod("deposit", long.class) নাম আর parameter মিলিয়ে method খোঁজে, নিরাপত্তার জন্য Method object-এর কপি বানায় — এটা তুলনামূলক দামি, তাই framework-গুলো চালুর সময় একবার খুঁজে cache করে রাখে। দ্বিতীয়, ডাকা: প্রতিটি invoke-এ argument-গুলোকে Object[]-এ ভরা, primitive হলে boxing, access যাচাই, আর ফেরত মান আবার unboxing. তৃতীয়, JIT-এর অন্ধত্ব: সরাসরি a.deposit(5) ডাক JIT inline করে প্রায় শূন্য খরচে নিয়ে যেতে পারে (পর্ব ০৫); reflection-এর ডাকে কোন method চলবে তা data-র ওপর নির্ভর করে, তাই এই অপ্টিমাইজেশন কঠিন।

পুরোনো HotSpot JVM ডাকার খরচ কমাতে একটি কৌশল ব্যবহার করত, যাকে বলা হতো “inflation”. একটি Method-এর প্রথম কয়েকবার ডাক (ডিফল্টে ১৫ বার, sun.reflect.inflationThreshold দিয়ে বদলানো যেত) যেত native C++ কোডের মধ্য দিয়ে — শুরু দ্রুত, কিন্তু প্রতি ডাকে ধীর। সীমা পার হলে JVM চলার সময় সেই নির্দিষ্ট method ডাকার জন্য একটি ছোট bytecode class তৈরি করত (generated accessor), যা JIT সাধারণ কোডের মতো অপ্টিমাইজ করতে পারত। ফলে খুব বেশি ডাকা reflection-ও বেশ দ্রুত হতো। কিন্তু এর জটিলতা ছিল: প্রতিটি “গরম” method-এর জন্য আলাদা class, তাদের জন্য বিশেষ class loader, metaspace-এ চাপ, আর JDK-র ভেতরে প্রায় একই কাজের দুই-তিনটি আলাদা পথ রক্ষণাবেক্ষণ।

Java 7-এর MethodHandle আর invokedynamic আসার পরে JDK-র ভেতরে একটি আধুনিক, JIT-বান্ধব “method ডাকার” ব্যবস্থা তৈরি হয়ে গিয়েছিল। JEP 416 (Java 18, ২০২২) সিদ্ধান্ত নিল: java.lang.reflect.Method, Constructor আর Field-এর ভেতরের কাজ নতুন করে লেখা হবে method handle-এর ওপর, যাতে JDK-তে একটি পথই থাকে। JEP-এর লক্ষ্য ছিল মূলত রক্ষণাবেক্ষণের খরচ কমানো; গতির ব্যাপারে এর দাবি সতর্ক — কিছু ক্ষেত্রে, বিশেষ করে Method object যখন static final ধ্রুবক হিসেবে রাখা থাকে, ডাক দ্রুততর হয়, আবার কিছু ক্ষেত্রে (যেমন প্রথম কয়েকবার ডাক) সামান্য ধীর হতে পারে। পুরোনো ব্যবস্থা কিছু সময়ের জন্য একটি system property (-Djdk.reflect.useDirectMethodHandle=false) দিয়ে ফেরত আনা যেত; Java 22-এ পুরোনো ব্যবস্থাটি পুরোপুরি সরিয়ে ফেলা হয়েছে, তাই সেই পথও আর নেই।

বাস্তবে কী শেখার আছে? একটি সাধারণ ওয়েব অনুরোধে ডেটাবেস বা নেটওয়ার্কের খরচ মিলিসেকেন্ডে, reflection-এর খরচ ন্যানোসেকেন্ডে — তাই Jackson বা Spring-এর reflection সাধারণত সমস্যা নয়। সমস্যা হয় দুই জায়গায়: খুব গরম লুপের ভেতরে বারবার getMethod, আর অ্যাপ চালুর সময় হাজারো class স্ক্যান — serverless বা native-image-এর জগতে চালুর প্রতিটি সেকেন্ড দামি।

মূল উপলব্ধি: reflection-এর দাম মাপুন, অনুমান করবেন না — খোঁজা cache করুন, গরম পথে সরাসরি ডাক বা MethodHandle ব্যবহার করুন; আর জানুন যে JDK নিজেই reflection-কে বারবার নতুন করে গড়েছে, যাতে আপনার এসব চিন্তা কম করতে হয়।

প্র ০৪Lombok এত জনপ্রিয়, তবু অনেক দল একে এড়িয়ে চলে। Lombok javac-এর ভেতরের API ব্যবহার করে — এটা কেন বিতর্কিত, আর Java 16-এর strong encapsulation এতে কী প্রভাব ফেলেছিল?

JSR 269-এর annotation processing API-র একটি স্পষ্ট নিয়ম আছে: processor annotation পড়তে পারে, নতুন সোর্স ফাইল তৈরি করতে পারে, warning বা error দিতে পারে — কিন্তু বিদ্যমান class বদলাতে পারে না। MapStruct বা Dagger এই নিয়ম মেনে চলে: তারা আলাদা নতুন class লেখে (CustomerMapperImpl), আপনার class যেমন ছিল তেমনই থাকে।

Lombok-এর লক্ষ্য একেবারে আলাদা: আপনার @Data class Customer-এর ভেতরেই getter, setter, equals যোগ করা। মানক API দিয়ে এটা সম্ভব নয়। তাই Lombok processor হিসেবে javac-এ ঢুকে javac-এর নিজের ভেতরের, অপ্রকাশিত class-গুলো (com.sun.tools.javac-এর syntax tree) সরাসরি ব্যবহার করে compile হওয়ার মাঝপথে আপনার class-এর গঠন বদলে দেয়। Lombok-এর সমর্থকরা বলেন, এটা একটি ব্যবহারিক সমাধান যা বছরের পর বছর লক্ষ লক্ষ লাইনের boilerplate বাঁচিয়েছে। সমালোচকরা বলেন, Lombok দিয়ে লেখা কোড আসলে “Java” নয়, Java-র মতো দেখতে আরেকটি ভাষা — যা javac-এর একটি নির্দিষ্ট ভেতরের গঠনের ওপর নির্ভর করে, আর সেই গঠন বদলানোর অধিকার JDK দলের পুরোপুরি আছে।

এই ঝুঁকি বাস্তবে দেখা দেয় Java 16-এ। JEP 396 JDK-র ভেতরের package-গুলো ডিফল্টে বন্ধ করে দিল (অংশ ৫-এর InaccessibleObjectException-এর সেই দেয়াল), আর javac-এর ভেতরের package-ও এর আওতায় পড়ল। পুরোনো Lombok সংস্করণ নতুন JDK-তে compile-ই করতে পারছিল না; Lombok দলকে নতুন সংস্করণ বের করতে হয়েছিল, যা দেয়াল পার হওয়ার আরও কৌশল ব্যবহার করে। যারা Lombok-এর ওপর নির্ভর করতেন, তাদের JDK আপগ্রেড আটকে ছিল সেই নতুন সংস্করণ না আসা পর্যন্ত। এমন ঘটনা মনে করিয়ে দেয়: আপনার build একটি তৃতীয় পক্ষের লাইব্রেরির এমন কৌশলের ওপর দাঁড়িয়ে, যা JDK আনুষ্ঠানিকভাবে সমর্থন করে না।

আধুনিক Java এর একটি অংশের বিকল্প নিজেই দিয়েছে: পর্ব ২৫-এর record সরল data class-এর জন্য equals, hashCode, toString আর accessor ভাষার ভেতর থেকেই দেয় — কোনো লাইব্রেরি ছাড়া। তবে বদলযোগ্য JPA entity, builder pattern-এর মতো জায়গায় Lombok এখনো বহু প্রজেক্টে ব্যবহৃত। তাই অনেক দলের নীতি হলো: নতুন কোডে আগে record, Lombok শুধু সীমিত, স্পষ্টভাবে অনুমোদিত annotation-এ (যেমন @Getter, @Builder), আর delombok দিয়ে যেকোনো সময় সাধারণ Java-তে ফেরার পথ খোলা রাখা।

মূল উপলব্ধি: একটি টুল কতটা সুবিধা দেয়, তার পাশাপাশি দেখুন সেটা কোন প্রতিশ্রুতির ওপর দাঁড়িয়ে — মানক API-র ওপর গড়া টুল JDK-র সাথে বড় হয়, ভেতরের কৌশলের ওপর গড়া টুল প্রতিটি আপগ্রেডে ঝুঁকি আনে।

প্র ০৫Dagger আর Micronaut dependency injection compile-এর সময় করে, প্রচলিত Spring করে চলার সময় reflection দিয়ে। startup time, native image আর ভুল ধরার দিক থেকে এই দুই পথের তুলনা করুন।

dependency injection-এর মূল কাজ দুই পথেই এক: কোন class-এর কোন নির্ভরতা লাগবে তা বের করা, সঠিক ক্রমে object বানানো, জুড়ে দেওয়া। পার্থক্য হলো কখন এই হিসাব হয়। প্রচলিত Spring অ্যাপ চালুর সময় classpath স্ক্যান করে, প্রতিটি class-এর annotation আর constructor reflection দিয়ে পড়ে, তারপর graph বানায় (অংশ ৭-এর stepper). প্রতিবার চালু হলে এই কাজ নতুন করে।

Dagger (Square-এর প্রথম সংস্করণের পর Google-এর Dagger 2, Android-এ ব্যাপক) annotation processor হিসেবে compile-এর সময়ই পুরো graph হিসাব করে আর সাধারণ Java কোড লিখে দেয় — যেখানে স্পষ্ট লেখা new PaymentService(new BkashSmsSender()). চলার সময় কোনো স্ক্যান নেই, কোনো reflection নেই। Android-এ এই সিদ্ধান্ত স্বাভাবিক ছিল: পুরোনো ফোনে reflection ধীর ছিল, আর অ্যাপ খুলতে দেরি ব্যবহারকারী সহ্য করেন না। ২০১৮ সালে আসা Micronaut একই দর্শন সার্ভারের জগতে আনল, আর Quarkus build-এর সময় যতটা সম্ভব কাজ সেরে রাখার পথ নিল।

Startup time: বড় Spring অ্যাপ চালু হতে কয়েক সেকেন্ড থেকে বেশি সময় নিতে পারে; compile-time DI-র অ্যাপ সাধারণত দ্রুত চালু হয় আর কম মেমরি নেয়। দীর্ঘ সময় চলা একটি সার্ভারে কয়েক সেকেন্ড তেমন কিছু নয়, কিন্তু serverless function বা container যা চাহিদা অনুযায়ী বারবার চালু-বন্ধ হয়, সেখানে এটা সরাসরি খরচ আর ব্যবহারকারীর অপেক্ষা। Native image: অংশ ৭-এ দেখেছেন, GraalVM native-image reflection-এর জন্য আগাম কনফিগারেশন চায়; compile-time DI-তে reflection প্রায় নেই, তাই native image সহজ। Spring এই চাপের জবাবে Spring Framework 6 আর Spring Boot 3-এ (২০২২) AOT processing আনল — build-এর সময় bean-এর সংজ্ঞা আগেভাগে তৈরি করে রাখা আর native-image-এর কনফিগারেশন নিজে বানানো। ভুল ধরা: Dagger-এ একটি নির্ভরতা না পাওয়া গেলে compile error; Spring-এ সেটা অ্যাপ চালুর সময় exception. প্রথমটি নিরাপদ, দ্বিতীয়টির জন্য অন্তত একটি “অ্যাপ চালু হয় কি না” টেস্ট দরকার।

তাহলে কি runtime reflection পুরোনো? না। এর নমনীয়তা আসল: plugin যোগ করা, কনফিগারেশন অনুযায়ী আলাদা implementation বেছে নেওয়া, শর্তসাপেক্ষ auto-configuration — compile-time-এ এগুলো কঠিন বা অসম্ভব। Spring-এর বিশাল ইকোসিস্টেম এই নমনীয়তার ওপর দাঁড়িয়ে। প্রবণতা হলো মাঝামাঝি পথ: যা build-এর সময় জানা যায় তা আগেই হিসাব, বাকিটা চলার সময়।

মূল উপলব্ধি: একই কাজ কখন করা হবে — build-এর সময় না চালুর সময় — এই একটি সিদ্ধান্ত গতি, মেমরি, ভুল ধরা আর নমনীয়তার মধ্যে ভারসাম্য ঠিক করে; কোনো পথ সবসময় সেরা নয়, পরিবেশ ঠিক করে কোনটা মানানসই।

অনুশীলন

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

  1. Retention মুছে দেখুন: অংশ ৬-এর test runner-এ @Retention(RetentionPolicy.RUNTIME) লাইনটি মুছে দিন। না চালিয়ে বলুন কী ছাপা হবে, compile error আসবে কি না, তারপর চালিয়ে মিলিয়ে নিন।

    কোনো compile error বা warning আসে না। retention না দিলে ডিফল্ট CLASS, তাই লেবেল .class ফাইলে থাকে, কিন্তু চলার সময় reflection দেখতে পায় না। isAnnotationPresent(MyTest.class) প্রতিটি method-এর জন্য false, তাই কোনো টেস্ট চলে না, আর একমাত্র লাইন ছাপা হয়: 0/0 passed. এটাই সবচেয়ে বিপজ্জনক রূপ — “০টি ফেল” দেখে মনে হতে পারে সব ঠিক আছে। আসল test framework এজন্যই “কোনো টেস্ট পাওয়া যায়নি” আলাদা করে জানায়। আপনার রানারে if (total == 0) হলে সতর্কবার্তা যোগ করা একটি ভালো উন্নতি।

  2. সাজানো সরিয়ে দিলে: test runner থেকে Arrays.sort(...) লাইনটি মুছে দিলে প্রোগ্রাম কি ভুল হয়ে যাবে? আপনার মেশিনে আউটপুটের ক্রম একই এলে কি নিশ্চিন্ত হওয়া যায়?

    প্রোগ্রাম ভুল হবে না — একই তিনটি টেস্ট চলবে, একই ফল, শেষে 1/3 passed. বদলাতে পারে শুধু PASS/FAIL লাইনগুলোর ক্রম। আজ আপনার JVM-এ যে ক্রমই আসুক, getDeclaredMethods()-এর Javadoc কোনো ক্রমের নিশ্চয়তা দেয় না; অন্য JVM, অন্য সংস্করণ, এমনকি class-এ নতুন method যোগ করলেও বদলাতে পারে। ক্রম বদলালে দুটি সমস্যা: রিপোর্ট তুলনা করা কঠিন (CI-তে আগের রানের সাথে মেলানো), আর যদি টেস্টগুলো ভুল করে একে অপরের ওপর নির্ভর করে (একটি static variable বদলায়, আরেকটি পড়ে), তাহলে কখনো পাস কখনো ফেল — “flaky test”. JUnit 5 এজন্য ডিফল্টে একটি নির্দিষ্ট কিন্তু ইচ্ছা করে “অস্পষ্ট” ক্রম ব্যবহার করে, আর নির্দিষ্ট ক্রম চাইলে @TestMethodOrder দিতে হয় — যাতে কেউ ক্রমের ওপর নির্ভর না করে।

  3. validator-এ নতুন নিয়ম: অংশ ৬-এর validator-এ একটি @Max(int value) annotation যোগ করুন, আর SignUp-এ বয়সে @Min(18) @Max(120) দিন। কী কী বদলাতে হবে? validate method-এর বাইরের কিছু কি বদলাতে হলো?

    তিনটি ছোট বদল:

    @Retention(RetentionPolicy.RUNTIME) @Target(ElementType.RECORD_COMPONENT)
    @interface Max { int value(); }
    
    // record-এ:
    @Min(18) @Max(120) int age
    
    // validate-এর লুপে:
    if (c.getAnnotation(Max.class) instanceof Max max && (int) v > max.value())
        errors.add(c.getName() + ": must be at most " + max.value());

    main বা অন্য কোনো record-এর কোড বদলাতে হয়নি — এটাই annotation-চালিত নকশার সুবিধা: নিয়ম ঘোষণা ডেটার পাশে, যাচাইয়ের যন্ত্র এক জায়গায়। new SignUp("Rahim", "01712345678", 150) দিলে তালিকা হবে [age: must be at most 120]. খেয়াল করুন, এখানে (int) v ধরে নেয় component-টি int; long বা Integer হলে ClassCastException হবে — আসল Bean Validation এজন্য প্রতিটি নিয়ম কোন টাইপে বসানো যায় তা যাচাই করে।

  4. reflection, না অন্য কিছু? প্রতিটির জন্য এক লাইনে সিদ্ধান্ত দিন: (ক) একটি দোকানের অ্যাপে পেমেন্টের ধরন (“bkash”, “nagad”, “card”) অনুযায়ী আলাদা হিসাব; (খ) একটি লাইব্রেরি যা যেকোনো record-কে CSV-এর একটি লাইনে লেখে; (গ) টেস্টে একটি class-এর private field-এ নকল মান বসানো; (ঘ) কনফিগারেশন ফাইলে লেখা নাম থেকে plugin class লোড করা।

    (ক) reflection নয় — একটি PaymentMethod interface আর Map<String, PaymentMethod>, অথবা sealed interface আর switch; সব ধরন compile-এর সময় জানা। (খ) reflection যুক্তিসংগত — লাইব্রেরি আগে থেকে জানে না কোন record আসবে; getRecordComponents() ক্রমও নিশ্চিত করে। (গ) এড়িয়ে চলুন — private field-এ হাত দিতে হলে নকশার সংকেত; constructor দিয়ে নির্ভরতা দেওয়া (dependency injection) হলে টেস্টে সরাসরি নকল object পাঠানো যায়। (ঘ) reflection (বা Java-র ServiceLoader) যুক্তিসংগত — নাম চলার সময়ই জানা যায়; তবে কোন নাম অনুমোদিত তা যাচাই করুন, আর class যেন একটি নির্দিষ্ট interface implement করে তা দেখে নিন।

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

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

ক্রম@MyTest annotation দিয়ে টেস্ট খুঁজে চালানো একটি test runner-এর ধাপগুলো সঠিক ক্রমে সাজান (▲▼ চেপে সরান)

  1. 📂 Class.forName দিয়ে টেস্ট class-এর Class object পাওয়া
  2. 📋 getDeclaredMethods দিয়ে সব method নিয়ে নাম অনুযায়ী সাজানো
  3. 🏷️ isAnnotationPresent দিয়ে শুধু @MyTest-ওয়ালা method রাখা
  4. ▶️ নতুন instance বানিয়ে Method.invoke দিয়ে টেস্ট ডাকা
  5. 🧯 InvocationTargetException ধরে getCause() থেকে আসল কারণ বের করা
  6. 📊 পাস আর মোট টেস্টের সংখ্যা ছাপা

একটি নিজস্ব annotation-এ কোনো @Retention লেখা নেই। চলার সময় method.isAnnotationPresent(সেই annotation) কী দেবে?

  • compile error হবে
  • true, কারণ ডিফল্ট RUNTIME
  • false, কারণ ডিফল্ট CLASS
  • AnnotationFormatError ছুড়বে

Java reflection-এ getMethods() আর getDeclaredMethods()-এর পার্থক্য কোনটি সঠিক?

  • getMethods() private method-ও দেয়, getDeclaredMethods() দেয় না
  • getMethods() উত্তরাধিকারসহ শুধু public; getDeclaredMethods() এই class-এ লেখা সব, উত্তরাধিকার ছাড়া
  • দুটি একই ফল দেয়, শুধু নাম আলাদা
  • getDeclaredMethods() সবসময় নাম অনুযায়ী সাজানো ফল দেয়

Method.invoke দিয়ে ডাকা একটি method ভেতরে IllegalArgumentException ছুড়ল। ডাকার জায়গায় কী ধরা পড়ে?

  • সরাসরি IllegalArgumentException
  • IllegalAccessException
  • কিছুই না, invoke null ফেরত দেয়
  • InvocationTargetException, যার getCause() হলো IllegalArgumentException

Java 17-এ নিজের কোড থেকে String.class.getDeclaredField("value").setAccessible(true) ডাকলে সাধারণত কী হয়?

  • InaccessibleObjectException, কারণ java.base module java.lang open করে না
  • কাজ করে, শুধু একটি warning দেখায়
  • compile error হয়
  • NoSuchFieldException, কারণ field-টি নেই

Spring-এর মতো framework @Component লেখা class-এর object কীভাবে তৈরি করে, যখন অ্যাপের কোডে কোথাও new লেখা নেই?

  • @Component annotation নিজেই constructor ডাকে
  • javac compile-এর সময় object বানিয়ে .class ফাইলে রাখে
  • চালুর সময় classpath স্ক্যান করে, reflection দিয়ে constructor ডেকে নির্ভরতা জুড়ে দেয়
  • JVM class লোড করার সময় নিজে থেকেই প্রতিটি class-এর একটি object বানায়

java.lang.reflect.Proxy দিয়ে তৈরি dynamic proxy সম্পর্কে কোন কথাটি সঠিক?

  • যেকোনো class-এর জন্য proxy বানানো যায়, interface লাগে না
  • proxy-র প্রতিটি method ডাক একটি InvocationHandler-এর invoke method-এ যায়
  • proxy class compile-এর সময় annotation processor তৈরি করে
  • proxy ব্যবহার করতে হলে caller-এর কোড বদলাতে হয়

শব্দকোষ

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

মনে রাখুন

  1. annotation = কোডের গায়ে লেবেল, নিজে কিছু করে না। পড়ে compiler (@Override, @Deprecated), compile-এর সময় annotation processor (Lombok, MapStruct, Dagger), অথবা চলার সময় framework reflection দিয়ে।
  2. @interface দিয়ে নিজের annotation; element-এ default দেওয়া যায়। retention: SOURCE, CLASS (ডিফল্ট), RUNTIME — reflection-এর জন্য RUNTIME অবশ্যই। @Target ঠিক করে কোথায় বসবে।
  3. reflection-এর দরজা Class object (getClass(), Foo.class, Class.forName). getDeclared* = এই class-এর সব; get* = public, উত্তরাধিকারসহ। ক্রম নির্দিষ্ট নয় — সাজিয়ে নিন।
  4. Method.invoke ডাকা method-এর exception InvocationTargetException-এ মুড়ে দেয় — আসল কারণ getCause()-এ। setAccessible JDK-র ভেতরে InaccessibleObjectException দেয়।
  5. JUnit, Spring, Jackson, JPA = annotation + reflection (+ proxy). তবে reflection framework-এর হাতিয়ার — অ্যাপের কোডে আগে interface, Map, lambda বা sealed switch ভাবুন।

পরের পর্বে: মডিউল ৭ শুরু। এতক্ষণ সব কোড এক ফাইলে, শুধু JDK দিয়ে। কিন্তু আসল প্রজেক্টে লাগে JUnit, Jackson, Spring-এর মতো বাইরের লাইব্রেরি, তাদের সঠিক সংস্করণ, আর এক কমান্ডে compile-টেস্ট-প্যাকেজ — Maven আর Gradle.

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

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