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

Spring Boot-এর শুরু — object-দের জোড়া লাগানোর দায়িত্ব যখন framework-এর

Getting Started with Spring Boot — Inversion of Control, Dependency Injection, Beans, Auto-configuration, Properties and Profiles

Taught in Bangla, this lesson starts Spring Boot from the problem it solves: why hard-wired new hurts testing, Inversion of Control and dependency injection with a hand-written mini container, ApplicationContext, beans, stereotypes, scopes and @Qualifier, Spring Initializr, starters and auto-configuration, properties, profiles and secrets, logging, a first endpoint, Actuator and virtual threads.

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

  • class নিজেই নির্ভরতা বানালে কেন টেস্ট আর পরিবর্তন কঠিন হয়, আর Inversion of Control কী উল্টে দেয়
  • reflection দিয়ে নিজের হাতে একটি ছোট IoC container লেখা — Spring ভেতরে যা করে, তার মূল অংশ
  • Spring-এর মূল ধারণা: ApplicationContext, bean, stereotype annotation, @Configuration + @Bean, constructor injection, scope, @PostConstruct, @Primary/@Qualifier
  • Spring Boot: Initializr, starter, auto-configuration, @SpringBootApplication, embedded server, ./mvnw spring-boot:run আর java -jar
  • কনফিগারেশন (@Value, record দিয়ে @ConfigurationProperties), profile, গোপন তথ্য, logging, প্রথম endpoint, Actuator, DevTools, virtual thread আর Spring কখন বেশি ভারী

১ · সমস্যাটা আগে — প্রতিটি class নিজেই new লিখলে কী হয়

ছোট প্রোগ্রামে সব কাজ একটি-দুটি class-এ হয়ে যায়। কিন্তু একটি অনলাইন দোকানের অর্ডার নেওয়ার কথা ভাবুন। OrderService-এর কাজ অর্ডার নেওয়া, কিন্তু টাকা কাটতে তার লাগে একটি PaymentGateway (bKash, Nagad বা কার্ড), গ্রাহককে জানাতে একটি SmsSender, অর্ডার সংরক্ষণে একটি OrderRepository (পর্ব ৪৯-এর JDBC), আর সময় লিখতে একটি Clock (পর্ব ৩৮). অ্যাপ যত বড় হয়, object-রা তত বেশি অন্য object-এর ওপর নির্ভর করে। এই অন্য object-গুলোকে বলে নির্ভরতা বা dependency.

প্রশ্ন হলো, এই নির্ভরতাগুলো কে বানাবে? সবচেয়ে সহজ উত্তর — class নিজেই:

class OrderService {
    private final PaymentGateway gateway = new BkashGateway("merchant-123", "secret-key");
    private final SmsSender sms = new RealSmsSender("https://sms.example.com/api");
    private final OrderRepository repo = new JdbcOrderRepository("jdbc:mysql://prod-db/shop");

    void placeOrder(Order order) { ... }
}

কোডটি চলে। কিন্তু এর ভেতরে তিনটি লুকানো সমস্যা আছে, যা অ্যাপ বড় হলে যন্ত্রণা হয়ে দাঁড়ায়:

টেস্ট করা যায় না

পর্ব ৪৭-এ JUnit দিয়ে placeOrder টেস্ট করতে চাইলে প্রতিবার সত্যিকারের bKash-এ টাকা কাটার চেষ্টা হবে, সত্যিকারের SMS যাবে, আসল ডেটাবেসে লেখা হবে। নকল (mock) বসানোর কোনো দরজা নেই।

বদলাতে class খুলতে হয়

bKash-এর বদলে Nagad চালু করতে, বা SMS কোম্পানি বদলাতে OrderService-এর কোড বদলাতে হয় — অথচ অর্ডার নেওয়ার নিয়ম একটুও বদলায়নি।

কনফিগারেশন কোডে বন্দি

ডেটাবেসের ঠিকানা, পাসওয়ার্ড, API key সোর্স কোডে লেখা। dev মেশিনে এক ঠিকানা, সার্ভারে আরেক — প্রতিবার কোড বদলে নতুন build. আর গোপন key Git-এ চলে যায়।

এই অবস্থাকে বলে শক্ত বাঁধন বা tight coupling: OrderService শুধু “কোনো একটি পেমেন্ট ব্যবস্থা” নয়, একটি নির্দিষ্ট class, নির্দিষ্ট key আর নির্দিষ্ট ঠিকানার সাথে বাঁধা। পর্ব ২১-এ এর সমাধানের প্রথম অর্ধেক দেখেছেন — interface-এর দিকে প্রোগ্রাম করা, আর নির্ভরতা constructor দিয়ে বাইরে থেকে দেওয়া:

class OrderService {
    private final PaymentGateway gateway;     // interface — কোন implementation, জানে না
    private final SmsSender sms;

    OrderService(PaymentGateway gateway, SmsSender sms) {   // যা লাগবে, বাইরে থেকে আসবে
        this.gateway = gateway;
        this.sms = sms;
    }
}

এখন OrderService পরিষ্কার: তার কী লাগবে, constructor-এর দিকে তাকালেই বোঝা যায়। কিন্তু সমস্যাটা পুরোপুরি যায়নি, শুধু সরে গেছে। কাউকে তো new BkashGateway(...) আর new OrderService(gateway, sms) লিখতেই হবে, ঠিক ক্রমে। দশটি class হলে এটা সহজ; দুইশো class হলে — যেখানে A-র লাগে B আর C, B-র লাগে D, আর D-র লাগে কনফিগারেশন থেকে পড়া একটি URL — এই “জোড়া লাগানোর” কোড নিজেই একটি বড় প্রজেক্ট হয়ে দাঁড়ায়। দ্বিতীয় অর্ধেক সমাধান হলো এই কাজটাও একটি যন্ত্রের হাতে দেওয়া।

একটি বিয়ের অনুষ্ঠানের কথা ভাবুন। প্রতিটি অতিথি যদি নিজেই রান্নার লোক খোঁজেন, নিজের চেয়ার আনেন, নিজের আলো জ্বালান — বিশৃঙ্খলা। তার বদলে একজন ডেকোরেটর আর একজন বাবুর্চি দায়িত্ব নেন: কে কোন টেবিলে বসবেন, কখন খাবার আসবে, কোন জেনারেটর লাগবে — সব তাঁরা ঠিক করেন আর জায়গামতো পৌঁছে দেন। অতিথি শুধু বলেন “আমার একটি চেয়ার আর এক প্লেট বিরিয়ানি লাগবে”। কী লাগবে বলা অতিথির কাজ; কে বানাবে আর কীভাবে পৌঁছাবে, সেটা আয়োজকের। Spring হলো সেই আয়োজক।

২ · Inversion of Control আর Dependency Injection — নিজের হাতে একটি mini container

সাধারণ প্রোগ্রামে নিয়ন্ত্রণ আপনার কোডের হাতে: আপনার main ঠিক করে কখন কোন object বানানো হবে, কখন কোন method ডাকা হবে। লাইব্রেরি (যেমন Math.sqrt) আপনি ডাকেন। Inversion of Control (IoC) মানে এই নিয়ন্ত্রণ উল্টে দেওয়া: object বানানো, জোড়া দেওয়া, কখন কোনটা চালু হবে — এই সিদ্ধান্তগুলো নেয় একটি framework, আর আপনার কোড শুধু ছোট ছোট অংশ দেয়, যাদের framework সময়মতো ডাকে। একে মজা করে বলা হয় “Hollywood নীতি”: don't call us, we'll call you — আমাদের ডাকবেন না, দরকার হলে আমরাই ডাকব।

IoC একটি বড় ধারণা; তার সবচেয়ে প্রচলিত রূপ হলো dependency injection (DI): একটি object নিজের নির্ভরতা নিজে বানায় না বা খুঁজে আনে না, বাইরে থেকে কেউ সেগুলো ঢুকিয়ে (inject করে) দেয়। আর যে যন্ত্র এই কাজটা করে, তাকে বলে IoC container. পর্ব ৪৫-এ দেখেছেন Spring কীভাবে @Component class খুঁজে reflection দিয়ে constructor ডাকে। এবার সেই ধারণা দিয়ে একটি পুরো ছোট container লেখা যাক — কোনো লাইব্রেরি ছাড়া, শুধু JDK দিয়ে। কোড দেখার আগে, container কীভাবে OrderService-এর নির্ভরতা পুনরাবৃত্তি (recursion, পর্ব ১২) দিয়ে খুঁজে বের করে, ধাপে ধাপে দেখুন:

1Object create(Class<?> type) {2  if (singletons.containsKey(type)) return singletons.get(type);3  Class<?> impl = bindings.getOrDefault(type, type);4  Constructor<?> ctor = impl.getDeclaredConstructors()[0];5  for (...) args[i] = create(paramTypes[i]);   // পুনরাবৃত্তি6  Object bean = ctor.newInstance(args);7  singletons.put(type, bean); return bean;8}
অবস্থা
type
impl
ডাকের স্তূপ
singletons
  1. OrderService চাওয়া হলো। cache-এ নেই। OrderService-এর জন্য আলাদা binding নেই, তাই class নিজেই implementation. তার একমাত্র constructor-এর parameter টাইপ reflection দিয়ে পড়া হলো: PaymentGateway আর SmsSender. এগুলো না পেলে OrderService বানানো যাবে না — তাই প্রতিটির জন্য আবার create.
  2. প্রথম নির্ভরতা: PaymentGateway. এটা একটি interface; binding বলছে এর implementation BkashGateway. তার constructor-এ কোনো parameter নেই, তাই পুনরাবৃত্তি এখানেই থামে — সরাসরি বানানো আর cache-এ রাখা হলো।
  3. দ্বিতীয় নির্ভরতা: SmsSender. একইভাবে binding থেকে ConsoleSms, বানানো, cache-এ রাখা। লক্ষ করুন: যে নির্ভরতা নিজে কারও ওপর নির্ভর করে না, সেটা আগে তৈরি হয় — গাছের পাতা আগে, গোড়া পরে।
  4. ফিরে এসে OrderService. দুটি argument এখন হাতে। ctor.newInstance(gateway, sms) — আপনার কোডে কোথাও new OrderService(...) নেই, অথচ ঠিক নির্ভরতাসহ object তৈরি। এটাও cache-এ গেল।
  5. আবার চাইলে একই object. পরে কেউ SmsSender চাইলে নতুন বানানো হয় না, cache থেকে আগেরটাই দেওয়া হয়। একে বলে singleton scope — Spring-এর ডিফল্টও এটাই (অংশ ৪).

এবার পুরো প্রোগ্রাম। interface থেকে implementation-এর “binding” হাতে লেখা, বাকি সব container নিজে বের করে:

import java.lang.reflect.*;
import java.util.*;
interface PaymentGateway { String pay(long amount); }
interface SmsSender { void send(String to, String text); }
class BkashGateway implements PaymentGateway { public String pay(long a) { return "bKash paid " + a; } }
class ConsoleSms implements SmsSender { public void send(String to, String t) { System.out.println("SMS " + to + ": " + t); } }
class OrderService {
    private final PaymentGateway gateway; private final SmsSender sms;
    OrderService(PaymentGateway g, SmsSender s) { gateway = g; sms = s; }
    void placeOrder(String phone, long amount) { sms.send(phone, gateway.pay(amount)); }
}
class MiniContainer {
    private final Map<Class<?>, Class<?>> bindings = new HashMap<>();
    private final Map<Class<?>, Object> singletons = new HashMap<>();
    void bind(Class<?> type, Class<?> impl) { bindings.put(type, impl); }
    <T> T get(Class<T> type) throws ReflectiveOperationException { return type.cast(create(type, "")); }
    private Object create(Class<?> type, String pad) throws ReflectiveOperationException {
        if (singletons.containsKey(type)) { System.out.println(pad + "reuse " + type.getSimpleName()); return singletons.get(type); }
        Class<?> impl = bindings.getOrDefault(type, type);
        System.out.println(pad + "need " + type.getSimpleName() + " -> " + impl.getSimpleName());
        Constructor<?> ctor = impl.getDeclaredConstructors()[0];     // প্রতিটি class-এ constructor একটিই
        Object[] args = new Object[ctor.getParameterCount()];
        for (int i = 0; i < args.length; i++) args[i] = create(ctor.getParameterTypes()[i], pad + "  ");
        Object bean = ctor.newInstance(args);
        singletons.put(type, bean);
        System.out.println(pad + "created " + impl.getSimpleName());
        return bean;
    }
}
class Main {
    public static void main(String[] args) throws Exception {
        var box = new MiniContainer();
        box.bind(PaymentGateway.class, BkashGateway.class);
        box.bind(SmsSender.class, ConsoleSms.class);
        box.get(OrderService.class).placeOrder("01712345678", 1500);
        System.out.println("same sender? " + (box.get(SmsSender.class) == box.get(SmsSender.class)));
    }
}

আউটপুটের প্রথম ছয় লাইন জোড়া লাগানোর ক্রম দেখায়: need OrderService -> OrderService, তারপর দুই ঘর সরে need PaymentGateway -> BkashGateway, created BkashGateway, need SmsSender -> ConsoleSms, created ConsoleSms, আর শেষে created OrderService. ফাঁকা ঘরগুলো পুনরাবৃত্তির গভীরতা বোঝায় — ভেতরের নির্ভরতা আগে তৈরি হয়েছে। তারপর আসল কাজ: SMS 01712345678: bKash paid 1500. শেষ তিন লাইন reuse SmsSender (দুবার, কারণ দুবার চাওয়া হয়েছে) আর same sender? true — singleton cache কাজ করছে। প্রতিটি class-এ ঠিক একটি constructor রাখা হয়েছে, কারণ getDeclaredConstructors() কোন ক্রমে ফল দেবে তার নিশ্চয়তা নেই (পর্ব ৪৫). OrderService-এ বা main-এ কোথাও new BkashGateway() নেই — Nagad চালু করতে শুধু bind-এর একটি লাইন বদলাবে। সব class public ছাড়া, কারণ অনলাইন runner ফাইলটিকে অন্য নামে compile করে; নিজের প্রজেক্টে সাধারণত public class Main থাকে Main.java-তে।

মূল ধারণা

প্রায় ২০ লাইনের এই MiniContainer-এর ভেতরেই Spring-এর হৃদয়: (১) কোন interface-এর জন্য কোন class, তা জানা; (২) constructor-এর parameter পড়ে নির্ভরতার গাছ বোঝা; (৩) ঠিক ক্রমে বানানো; (৪) একবার বানিয়ে রেখে দেওয়া। Spring এর সাথে যোগ করে classpath স্ক্যান (binding হাতে লিখতে হয় না), কনফিগারেশন পড়া, lifecycle, proxy, আর হাজারো তৈরি করা integration — কিন্তু ভেতরের যুক্তি এটাই।

Spring-এর জন্ম ভারী EJB-র প্রতিক্রিয়ায়। ২০০২ সালে Rod Johnson তাঁর বই Expert One-on-One J2EE Design and Development-এ দেখালেন, সাধারণ Java object আর একটি হালকা container দিয়েই enterprise অ্যাপ লেখা যায়; বইয়ের সাথের কোড থেকেই পরে Spring Framework তৈরি হয়, আর Spring 1.0 আসে ২০০৪ সালে। একই বছরের শুরুতে Martin Fowler তাঁর প্রবন্ধে এই কৌশলের নাম দেন “Dependency Injection” — বাংলায় নির্ভরতা ঢুকিয়ে দেওয়া। Spring Boot আসে ২০১৪ সালে। ভাবনার প্রশ্ন ১ আর ২-এ এই ইতিহাস বিস্তারিত।

৩ · Spring-এর মূল ধারণা — ApplicationContext, bean আর stereotype

Spring-এ আপনার mini container-এর নাম ApplicationContext. এর ভেতরে যে object-গুলো থাকে আর যাদের জীবন Spring নিয়ন্ত্রণ করে, তাদের বলে bean. সব object bean নয় — একটি Order record, যা প্রতিটি অনুরোধে নতুন তৈরি হয়, সাধারণ object. bean হলো সেই “সেবাদানকারী” object-গুলো, যারা অ্যাপজুড়ে কাজ করে: service, repository, controller, কনফিগারেশন।

Spring-কে কীভাবে বলবেন কোন class bean হবে? পর্ব ৪৫-এর component scanning: class-এর গায়ে একটি লেবেল। মূল লেবেল @Component, আর তার চারটি “আত্মীয়”, যাদের বলে stereotype annotation:

annotationকোন স্তরের জন্যবাড়তি কী করে
@Componentসাধারণ যেকোনো beanশুধু bean হিসেবে নিবন্ধন
@Serviceব্যবসায়িক নিয়ম (OrderService)মূলত পড়ার সুবিধা — ভূমিকা স্পষ্ট করে
@Repositoryডেটাবেস-স্তরডেটাবেসের নিজস্ব exception-কে Spring-এর একটি সাধারণ exception শ্রেণিতে রূপান্তর করতে পারে
@Controller / @RestControllerweb অনুরোধ গ্রহণURL থেকে method-এ পৌঁছানো (পর্ব ৫১)
package com.shop.order;

import org.springframework.stereotype.Service;

@Service
public class OrderService {
    private final PaymentGateway gateway;
    private final SmsSender sms;

    public OrderService(PaymentGateway gateway, SmsSender sms) {   // @Autowired লাগে না
        this.gateway = gateway;
        this.sms = sms;
    }

    public String placeOrder(String phone, long amount) {
        String tx = gateway.pay(amount);
        sms.send(phone, "Order confirmed: " + tx);
        return tx;
    }
}

লক্ষ করুন, @Autowired লেখা নেই। Spring 4.3 থেকে একটি class-এ constructor একটিই থাকলে Spring সেটাকেই নির্ভরতা দেওয়ার পথ ধরে নেয়। এটাই constructor injection, আর আধুনিক Spring-এ এটাই প্রস্তাবিত পথ। OrderService-এর কোড প্রায় সাধারণ Java — শুধু একটি লেবেল আর import. চাইলে Spring ছাড়াই new OrderService(fakeGateway, fakeSms) লিখে টেস্ট করা যায়।

@Configuration আর @Bean — যে class আপনার লেখা নয়

নিজের class-এ @Service লাগানো সহজ। কিন্তু java.time.Clock বা একটি HTTP client লাইব্রেরির class-এর সোর্স আপনার হাতে নেই, সেখানে লেবেল লাগাবেন কীভাবে? তখন একটি কনফিগারেশন class-এ method লিখে বলে দিন, “এই object-টাও bean হবে”:

@Configuration
public class AppConfig {

    @Bean
    public Clock clock() {
        return Clock.system(ZoneId.of("Asia/Dhaka"));       // JDK-র class — আমাদের লেখা নয়
    }

    @Bean
    public HttpClient httpClient() {
        return HttpClient.newBuilder().connectTimeout(Duration.ofSeconds(5)).build();
    }
}

@Bean-ওয়ালা প্রতিটি method-এর ফেরত object Spring container-এ রাখে, method-এর নাম হয় bean-এর নাম (clock, httpClient). এখন যেকোনো service constructor-এ Clock clock চাইলে এটাই পাবে — আর টেস্টে আপনি Clock.fixed(...) দিতে পারবেন, যাতে “আজকের তারিখ”-নির্ভর কোডও নিশ্চিত ফল দেয় (পর্ব ৩৮).

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

C-তে নির্ভরতা বাইরে থেকে দেওয়ার পুরোনো রূপ হলো function pointer বা struct-এ “operations table” পাঠানো; C++-এ constructor-এ interface (abstract class)-এর reference বা std::unique_ptr দেওয়া — ধারণা একই, শুধু কেউ স্বয়ংক্রিয়ভাবে জোড়া দেয় না। Python-এর FastAPI-তে Depends(...) একটি হালকা DI ব্যবস্থা — Python-এর ওয়েব ব্যাকএন্ড পর্বে দেখুন। Spring-এর পার্থক্য হলো মাত্রায়: পুরো অ্যাপের সব সেবাদানকারী object একটি container-এ, টাইপ দেখে স্বয়ংক্রিয়ভাবে জোড়া।

৪ · injection-এর ধরন, scope, lifecycle আর একাধিক implementation

field injection — কেন এড়িয়ে চলবেন

পুরোনো টিউটোরিয়াল আর অনেক AI-লেখা কোডে এমন দেখবেন:

@Service
public class OrderService {
    @Autowired private PaymentGateway gateway;    // field injection — এড়িয়ে চলুন
    @Autowired private SmsSender sms;
}

এটাকে বলে field injection: Spring reflection দিয়ে (পর্ব ৪৫-এর setAccessible) সরাসরি private field-এ মান বসায়। লেখা ছোট, কিন্তু দাম অনেক। field final হতে পারে না, তাই object তৈরির পরে বদলে যাওয়ার সুযোগ থাকে। নির্ভরতা লুকানো — class-এর বাইরে থেকে দেখে বোঝা যায় না কী লাগবে। আর সবচেয়ে বড় কথা: Spring ছাড়া new OrderService() লিখলে দুটি field-ই null, প্রথম method ডাকেই NullPointerException. টেস্টে নকল বসাতে তখন আবার reflection বা পুরো Spring চালু করতে হয়। constructor injection-এ এই তিনটি সমস্যার কোনোটিই নেই। (setter injection আরেকটি পথ — ঐচ্ছিক নির্ভরতার জন্য মাঝে মাঝে কাজে লাগে।)

constructor-এ পাঁচ-সাতটি parameter জমে গেলে সেটা field injection-এ ফেরার কারণ নয় — সেটা সংকেত যে class-টি অনেক বেশি দায়িত্ব নিয়েছে (পর্ব ০২-এর cohesion). field injection এই সংকেত লুকিয়ে ফেলে, constructor injection চোখের সামনে রাখে।

bean scope — একটি, নাকি প্রতিবার নতুন?

mini container-এ দেখেছেন, একবার বানানো object বারবার দেওয়া হয়। Spring-এ এটাকে বলে bean scope, আর ডিফল্ট হলো singleton — প্রতিটি container-এ একটি bean-এর একটিই object. এজন্য singleton bean-এ অনুরোধ-ভিত্তিক বদলযোগ্য অবস্থা (যেমন “বর্তমান ব্যবহারকারী” একটি field-এ) রাখা বিপজ্জনক: একসাথে হাজার অনুরোধ একই object ব্যবহার করে (পর্ব ৪০-এর race condition). অন্য scope-গুলো ধাপে ধাপে দেখুন:

  1. singletonডিফল্টcontainer-এ একটিই
  2. prototype@Scopeপ্রতিবার নতুন
  3. ফাঁদsingleton ← prototypeএকবারই inject
  4. requestwebপ্রতি HTTP অনুরোধে একটি
singletonদুবার চাইলে
OrderService a = ctx.getBean(OrderService.class);
OrderService b = ctx.getBean(OrderService.class);
a == b   → true
অ্যাপ চালুর সময় একবার তৈরি, বন্ধ হওয়া পর্যন্ত একই object
prototype@Scope("prototype")
@Component @Scope("prototype")
class ReceiptBuilder { ... }

ctx.getBean(ReceiptBuilder.class) == ctx.getBean(ReceiptBuilder.class)
→ false   প্রতিবার নতুন object
ফাঁদsingleton-এর ভেতরে prototype
@Service class InvoiceService {
    InvoiceService(ReceiptBuilder rb) { ... }   // inject হয় একবারই
}
InvoiceService singleton → ReceiptBuilder-ও কার্যত একটিই
প্রতিবার নতুন চাইলে: ObjectProvider<ReceiptBuilder>.getObject()
request scopeweb অ্যাপে
অনুরোধ ১ (রহিম)  → CartContext #1
অনুরোধ ২ (করিম)  → CartContext #2
অনুরোধ ১ শেষ     → CartContext #1 ফেলে দেওয়া
session scope হলে: একই ব্যবহারকারীর পুরো session-এ একটি
  1. singleton — ডিফল্ট। কিছু না লিখলে প্রতিটি bean singleton. service আর repository সাধারণত অবস্থাহীন (stateless) — শুধু নির্ভরতা রাখে, অনুরোধের ডেটা method-এর parameter-এ আসে — তাই একটি object-ই সবার জন্য যথেষ্ট আর নিরাপদ। (এটা Spring-এর singleton, পর্ব ১৮-এর static singleton pattern নয় — দুটি আলাদা container-এ দুটি object হতে পারে।)
  2. prototype — প্রতিবার নতুন। যে object-এ কাজের মাঝের অবস্থা জমে (একটি রসিদ তৈরির builder), তার জন্য। Spring বানিয়ে দেয়, কিন্তু তারপর আর খোঁজ রাখে না — বন্ধ করার দায়িত্ব আপনার।
  3. সবচেয়ে প্রচলিত ভুল। singleton bean-এর constructor-এ prototype চাইলে inject হয় মাত্র একবার — singleton তৈরির সময়। তারপর সেই একটি object-ই চিরকাল থাকে, যেন prototype কোনো কাজই করেনি। প্রতিবার নতুন চাইলে ObjectProvider নিন আর দরকারের সময় getObject() ডাকুন।
  4. web scope. web অ্যাপে আরও দুটি: request (প্রতিটি HTTP অনুরোধে একটি) আর session (প্রতিটি ব্যবহারকারীর session-এ একটি)। singleton-এ এদের inject করতে Spring একটি proxy বসায় (পর্ব ৪৫-এর dynamic proxy), যা প্রতিটি ডাকে বর্তমান অনুরোধের object খুঁজে নেয়।

lifecycle — তৈরি হওয়ার পরে আর বন্ধ হওয়ার আগে

কিছু bean-এর তৈরি হওয়ার পরে একটি প্রস্তুতি লাগে (cache গরম করা, কনফিগারেশন যাচাই), আর অ্যাপ বন্ধের আগে পরিষ্কার করা লাগে (connection বন্ধ)। এর জন্য দুটি annotation, Spring Boot 3-এ jakarta.annotation package থেকে:

import jakarta.annotation.PostConstruct;
import jakarta.annotation.PreDestroy;

@Component
public class TariffCache {
    @PostConstruct
    void load() { System.out.println("loading delivery tariffs"); }   // সব নির্ভরতা inject হওয়ার পরে

    @PreDestroy
    void close() { System.out.println("tariff cache closed"); }      // context বন্ধ হওয়ার আগে
}

ক্রমটা মনে রাখুন: constructor চলে → নির্ভরতা inject হয় → @PostConstruct চলে → bean ব্যবহারের জন্য তৈরি → … → অ্যাপ বন্ধের সময় @PreDestroy. constructor injection-এ নির্ভরতা constructor-এই এসে যায়, তাই সাধারণ প্রস্তুতি constructor-এই করা যায়; @PostConstruct কাজে লাগে যখন কাজটা constructor-এ রাখতে চান না (যেমন ধীর I/O-এর কাজ)।

একটি interface, তিনটি implementation — @Primary আর @Qualifier

দোকানে এখন bKash, Nagad আর কার্ড — তিনটিই PaymentGateway:

@Component("bkash") class BkashGateway implements PaymentGateway { ... }
@Component("nagad") class NagadGateway implements PaymentGateway { ... }
@Component("card")  class CardGateway  implements PaymentGateway { ... }

OrderService(PaymentGateway gateway, ...) চাইলে Spring কোনটা দেবে? সে অনুমান করে না — অ্যাপ চালু হওয়াই বন্ধ হয়ে যায়। Spring Boot এমন একটি বার্তা দেখায় (প্রত্যাশিত আউটপুট, সংক্ষেপে — সংস্করণভেদে শব্দ আলাদা হতে পারে):

***************************
APPLICATION FAILED TO START
***************************

Description:

Parameter 0 of constructor in com.shop.order.OrderService required a single bean, but 3 were found:
	- bkash: defined in file [.../BkashGateway.class]
	- nagad: defined in file [.../NagadGateway.class]
	- card: defined in file [.../CardGateway.class]

Action:

Consider marking one of the beans as @Primary, updating the consumer to accept multiple beans, or using @Qualifier to identify the bean that should be consumed

বার্তাটি নিজেই তিনটি সমাধান বলে দিচ্ছে:

  • @Primary: একটির গায়ে লাগালে সেটা ডিফল্ট পছন্দ — যে কেউ শুধু PaymentGateway চাইলে সেটাই পাবে।
  • @Qualifier: যে চাইছে, সে নাম দিয়ে নির্দিষ্ট করে: OrderService(@Qualifier("nagad") PaymentGateway gateway, ...). @Qualifier @Primary-এর চেয়ে অগ্রাধিকার পায়।
  • সবগুলো একসাথে: List<PaymentGateway> বা Map<String, PaymentGateway> চাইলে Spring তিনটিই দেয়; Map-এ key হলো bean-এর নাম। গ্রাহক চেকআউটে যেটা বাছবেন, gateways.get("bkash") দিয়ে সেটা — পর্ব ৪৫-এর অনুশীলনে বলা “reflection নয়, Map” সমাধানের Spring রূপ।

৫ · Spring Boot — Initializr, starter, auto-configuration আর এক লাইনে চালু

Spring Framework নিজে শক্তিশালী, কিন্তু আগে একটি web অ্যাপ দাঁড় করাতেই অনেক কাজ লাগত: ঠিক সংস্করণের দশটি লাইব্রেরি বাছাই, XML বা Java-তে কনফিগারেশন, আলাদা Tomcat সার্ভার ইনস্টল করে তাতে WAR ফাইল deploy. Spring Boot (২০১৪) এই প্রস্তুতির কাজটা সরিয়ে দিল। তার দর্শন পর্ব ৪৫-এ দেখা convention over configuration: যুক্তিসংগত ডিফল্ট আগে থেকেই ঠিক করা, আপনি শুধু ব্যতিক্রমটুকু লিখবেন।

প্রজেক্ট তৈরি — start.spring.io

Spring Initializr (start.spring.io) একটি ওয়েবসাইট, যেখানে কয়েকটি ঘর পূরণ করলে একটি তৈরি প্রজেক্টের zip পাওয়া যায়: build টুল (Maven বা Gradle — পর্ব ৪৬), ভাষা (Java), Spring Boot সংস্করণ, group (com.shop), artifact (shop), Java সংস্করণ (21), আর dependency — শুরুতে “Spring Web” যথেষ্ট। IntelliJ IDEA আর VS Code-এর Spring extension-এর ভেতরেও একই Initializr আছে। zip খুললে পাবেন:

shop/
├── mvnw, mvnw.cmd              ← Maven Wrapper: Maven ইনস্টল ছাড়াই build
├── pom.xml                     ← dependency আর Spring Boot সংস্করণ
└── src/
    ├── main/java/com/shop/ShopApplication.java
    ├── main/resources/application.properties
    └── test/java/com/shop/ShopApplicationTests.java

pom.xml-এ সবচেয়ে গুরুত্বপূর্ণ অংশ একটি starter:

<parent>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-parent</artifactId>
    <version>3.x.y</version>   <!-- Initializr যে সংস্করণ বসায় -->
</parent>

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-web</artifactId>   <!-- সংস্করণ লিখতে হয় না -->
</dependency>

starter নিজে প্রায় কোনো কোড নয় — এটা একটি “বান্ডিল”, যা একসাথে-কাজ-করে-এমন লাইব্রেরিগুলোর একটি পরীক্ষিত সেট টেনে আনে। spring-boot-starter-web আনে Spring MVC, JSON-এর জন্য Jackson (পর্ব ৩৯), embedded Tomcat আর logging. parent POM প্রতিটি লাইব্রেরির সংস্করণ ঠিক করে রাখে, তাই আপনাকে আলাদা সংস্করণ মেলাতে হয় না। অন্য প্রচলিত starter: spring-boot-starter-data-jpa (ডেটাবেস), spring-boot-starter-validation, spring-boot-starter-test (JUnit 5 আর Mockito — পর্ব ৪৭), spring-boot-starter-actuator.

main class আর @SpringBootApplication

package com.shop;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;

@SpringBootApplication
public class ShopApplication {
    public static void main(String[] args) {
        SpringApplication.run(ShopApplication.class, args);
    }
}

পুরো অ্যাপের শুরু এই কয়েকটি লাইন। @SpringBootApplication আসলে তিনটি annotation-এর সংক্ষেপ:

  • @SpringBootConfiguration — যা নিজে একটি @Configuration; তাই এই class-এও @Bean method লেখা যায়।
  • @EnableAutoConfiguration — auto-configuration চালু করে (নিচে)।
  • @ComponentScan — এই class-এর package (com.shop) আর তার নিচের সব package-এ @Component খোঁজে। তাই main class সবসময় সবচেয়ে ওপরের package-এ রাখুন; com.other-এ রাখা একটি @Service Spring খুঁজেই পাবে না।

auto-configuration — “আপনার classpath দেখে বুঝে নিচ্ছি”

auto-configuration Spring Boot-এর আসল জাদু, আর এটাও শুধু শর্তসাপেক্ষ @Configuration class. Spring Boot-এর ভেতরে শত শত এমন class আছে, প্রতিটির গায়ে শর্ত:

@AutoConfiguration
@ConditionalOnClass(DataSource.class)                    // classpath-এ JDBC আছে?
public class DataSourceAutoConfiguration {

    @Bean
    @ConditionalOnMissingBean                            // আপনি নিজে DataSource দেননি?
    DataSource dataSource(DataSourceProperties props) {  // তাহলে properties থেকে আমরা বানাই
        ...
    }
}

@ConditionalOnClass বলে “এই class classpath-এ থাকলে তবেই”। @ConditionalOnMissingBean বলে “আপনি নিজে এমন bean না দিয়ে থাকলে তবেই” — তাই আপনার নিজের @Bean DataSource থাকলে Spring Boot সরে দাঁড়ায়। @ConditionalOnProperty একটি property-র মান দেখে। ফলাফল: pom.xml-এ web starter যোগ করলেই Tomcat চালু হয়, JPA starter আর একটি ডেটাবেস driver যোগ করলেই connection pool তৈরি হয় — কোনো কনফিগারেশন কোড না লিখে। কোন auto-configuration কেন চলল বা চলল না, দেখতে চাইলে --debug দিয়ে চালান; Spring Boot একটি “conditions evaluation report” ছাপে।

এবার সব জোড়া দিয়ে দেখুন SpringApplication.run চালালে ভেতরে কী কী ঘটে:

  1. contextrun()খালি container
  2. স্ক্যানcom.shop.*নিজের bean
  3. কনফিগpropertiesenv, profile
  4. auto-config@Conditionalশর্ত যাচাই
  5. bean তৈরিgraphনির্ভরতার ক্রমে
  6. সার্ভার:8080অনুরোধের অপেক্ষা
ধাপ ১context তৈরি
SpringApplication.run(ShopApplication.class, args)
→ classpath-এ Spring MVC আছে → web ধরনের ApplicationContext
→ banner আর প্রথম log লাইন
Starting ShopApplication using Java 21 ...
ধাপ ২component scanning
com.shop.order.OrderService       @Service     ✔
com.shop.pay.BkashGateway         @Component   ✔
com.shop.sms.FakeSmsSender        @Component @Profile("dev")  (শর্ত পরে)
com.shop.AppConfig                @Configuration ✔
এখনো কোনো object নেই — শুধু bean-এর “সংজ্ঞা” (recipe)
ধাপ ৩কনফিগারেশন পড়া
application.properties          shop.vat-rate=0.15
application-dev.properties      (profile dev সক্রিয় হলে)
environment variable            SHOP_SMS_TOKEN=****
command line                    --server.port=9090
একই key কয়েক জায়গায় থাকলে বাইরেরটা জেতে (অংশ ৬)
ধাপ ৪auto-configuration
TomcatServletWebServer...   @ConditionalOnClass(Tomcat)    ✔ চালু
JacksonAutoConfiguration    @ConditionalOnClass(ObjectMapper) ✔ চালু
DataSourceAutoConfiguration @ConditionalOnClass(DataSource) ✘ JDBC নেই
আপনার নিজের bean থাকলে @ConditionalOnMissingBean সরে দাঁড়ায়
ধাপ ৫নির্ভরতার গাছ ধরে তৈরি
clock            ← নির্ভরতা নেই        → আগে
bkash, smsSender ← নির্ভরতা নেই        → আগে
orderService     ← (bkash, smsSender)  → পরে
ঠিক mini container-এর মতো — পাতা আগে, গোড়া পরে
ধাপ ৬inject আর lifecycle
new OrderService(bkash, smsSender)   ← constructor injection
@PostConstruct TariffCache.load()
proxy বসানো (যেমন @Transactional থাকলে)
কোনো নির্ভরতা না পেলে এখানেই APPLICATION FAILED TO START
ধাপ ৭embedded Tomcat চালু
Tomcat started on port 8080 (http) with context path '/'
Started ShopApplication in 2.1 seconds (process running for 2.6)
প্রত্যাশিত আউটপুট (সংক্ষেপে) — সংস্করণভেদে আলাদা
এখন http://localhost:8080 অনুরোধ নিতে তৈরি
  1. context তৈরি। run প্রথমে দেখে classpath-এ কী আছে — Spring MVC থাকলে web-এর উপযোগী একটি ApplicationContext বানায়। এখনো এটি খালি একটি container.
  2. স্ক্যান। main class-এর package-এর নিচের সব @Component-জাতীয় class খুঁজে bean-এর সংজ্ঞা তৈরি হয়। object এখনো বানানো হয়নি — আগে পুরো তালিকা দরকার, যাতে ক্রম ঠিক করা যায়।
  3. কনফিগারেশন। properties ফাইল, সক্রিয় profile-এর ফাইল, environment variable আর command-line argument পড়ে একটি একক “Environment” তৈরি হয়। এতে ঠিক হয় কোন profile সক্রিয়, তাই @Profile-ওয়ালা bean-গুলোর ভাগ্যও এখানে ঠিক হয়।
  4. auto-configuration. Spring Boot-এর শর্তসাপেক্ষ কনফিগারেশন class-গুলো একে একে যাচাই হয়। classpath-এ Tomcat আছে, তাই web server; Jackson আছে, তাই JSON; JDBC নেই, তাই DataSource বাদ।
  5. bean তৈরি, নির্ভরতার ক্রমে। সব সংজ্ঞা থেকে একটি নির্ভরতার graph; যার নির্ভরতা নেই সে আগে। দুটি bean একে অপরকে constructor-এ চাইলে (চক্র) Spring চালু হতে অস্বীকার করে — সেটা নকশার ভুলের সংকেত।
  6. inject আর প্রস্তুতি। constructor-এ নির্ভরতা ঢোকে, তারপর @PostConstruct, তারপর দরকার হলে proxy (পর্ব ৪৫-এর AOP). একটি নির্ভরতাও না মিললে অ্যাপ এখানেই থেমে যায় — compile-এর সময় নয়, চালুর সময়। তাই অন্তত একটি “context চালু হয় কি না” টেস্ট রাখা ভালো (পর্ব ৪৭).
  7. সার্ভার চালু। embedded Tomcat একটি port-এ (ডিফল্ট 8080) শোনা শুরু করে। আলাদা সার্ভার ইনস্টল নেই, WAR deploy নেই — সার্ভার আপনার অ্যাপের ভেতরেই একটি লাইব্রেরি।

চালানো আর প্যাকেজ করা

# development-এর সময় চালানো (Windows-এ: mvnw.cmd spring-boot:run)
./mvnw spring-boot:run

# একটি চালানো-যায়-এমন jar বানানো, টেস্টসহ
./mvnw package

# সার্ভারে শুধু JDK লাগে — Tomcat আলাদা লাগে না
java -jar target/shop-0.0.1-SNAPSHOT.jar --server.port=9090

./mvnw package একটি executable jar বানায় (অনেকে বলেন “fat jar”) — আপনার কোড, সব লাইব্রেরি আর embedded Tomcat একটি ফাইলে। এই ফাইল যেকোনো মেশিনে, Docker container-এ বা ক্লাউডে একই কমান্ডে চলে। embedded server-এর এই ধারণাই Spring Boot-কে microservice যুগের স্বাভাবিক পছন্দ বানিয়েছে।

৬ · কনফিগারেশন — properties, profile আর গোপন তথ্য কোডের বাইরে

অংশ ১-এর তৃতীয় সমস্যা ছিল কনফিগারেশন কোডে বন্দি। Spring Boot-এ মানগুলো থাকে application.properties-এ (অথবা একই কাজের YAML সংস্করণ application.yml-এ):

spring.application.name=shop
server.port=8080

# আমাদের নিজের key — "shop." দিয়ে শুরু, যাতে Spring-এর key-এর সাথে না মেশে
shop.vat-rate=0.15
shop.sms.gateway-url=https://sms.example.com/api
shop.sms.sender-id=MyShop
shop.sms.token=${SHOP_SMS_TOKEN}

logging.level.com.shop=DEBUG
shop:
  vat-rate: 0.15
  sms:
    gateway-url: https://sms.example.com/api
    sender-id: MyShop

মান পড়ার দুটি পথ। ছোট, একক মানের জন্য @Value:

@Service
public class PriceService {
    private final BigDecimal vatRate;

    public PriceService(@Value("${shop.vat-rate}") BigDecimal vatRate) {
        this.vatRate = vatRate;
    }
}

অনেকগুলো সম্পর্কিত মানের জন্য ভালো পথ @ConfigurationProperties — একটি record-এ সব মান টাইপসহ (পর্ব ২৫):

@ConfigurationProperties(prefix = "shop.sms")
public record SmsProperties(URI gatewayUrl, String senderId, String token) { }

// main class-এ একবার:
@SpringBootApplication
@ConfigurationPropertiesScan
public class ShopApplication { ... }

Spring Boot shop.sms.gateway-url-কে gatewayUrl-এ মিলিয়ে নেয় (kebab-case থেকে camelCase), String-কে URI-তে রূপান্তর করে, আর record-এর constructor দিয়ে object বানায়। ভুল ফরম্যাটের মান দিলে অ্যাপ চালুর সময়েই ধরা পড়ে, প্রথম SMS পাঠানোর সময় নয়। এখন যেকোনো bean constructor-এ SmsProperties props চাইতে পারে। @Value-এর তুলনায় সুবিধা: টাইপ-যাচাই, এক জায়গায় সব মান, IDE-তে auto-complete, আর টেস্টে new SmsProperties(...) লিখেই নকল কনফিগারেশন।

গোপন তথ্য কখনো কোডে নয়

ওপরের shop.sms.token=${SHOP_SMS_TOKEN} লাইনটি খেয়াল করুন। আসল token properties ফাইলেও নেই; ${...} বলে “এই নামের environment variable থেকে নাও”। Spring Boot আরও এক ধাপ সহজ করে: environment variable SHOP_SMS_TOKEN থাকলে সেটা সরাসরি shop.sms.token-এর মান হিসেবেও ধরা হয় — বড় হাতের অক্ষর, ডট-এর জায়গায় underscore (একে বলে relaxed binding). অগ্রাধিকারের মোটামুটি নিয়ম: command-line argument > environment variable > profile-এর ফাইল > application.properties. তাই একই jar dev মেশিনে, টেস্ট সার্ভারে আর আসল সার্ভারে আলাদা মানে চলে — build একবারই।

ডেটাবেস পাসওয়ার্ড, পেমেন্ট গেটওয়ের secret key, SMS token কখনো application.properties-এ লিখে Git-এ commit করবেন না। public repository-তে ফাঁস হওয়া key স্বয়ংক্রিয় bot খুব দ্রুত খুঁজে ফেলে। আসল সার্ভারে environment variable বা ক্লাউডের secret manager ব্যবহার করুন; লোকাল মেশিনে Git-এ উপেক্ষা করা আলাদা ফাইল।

profile — একই কোড, আলাদা পরিবেশ

development-এ প্রতিবার সত্যিকারের SMS পাঠানো অপচয়, আর টাকাও লাগে। profile দিয়ে বলা যায়, কোন পরিবেশে কোন bean:

@Component
@Profile("dev")
class FakeSmsSender implements SmsSender {
    private static final Logger log = LoggerFactory.getLogger(FakeSmsSender.class);
    public void send(String to, String text) { log.info("[FAKE SMS] {} : {}", to, text); }
}

@Component
@Profile("prod")
class RealSmsSender implements SmsSender {
    private final SmsProperties props;
    private final HttpClient http;
    RealSmsSender(SmsProperties props, HttpClient http) { this.props = props; this.http = http; }
    public void send(String to, String text) { /* props.gatewayUrl()-এ HTTP POST */ }
}
  1. একই কোডshop.jarদুটি SmsSender
  2. devFakeSmsSenderlog-এ লেখে
  3. prodRealSmsSenderআসল SMS
  4. profile নেইdefaultকোনোটিই নয়
একই কোডNotificationService জানে শুধু interface
@Service class NotificationService {
    NotificationService(SmsSender sms) { ... }
}
FakeSmsSender  @Profile("dev")
RealSmsSender  @Profile("prod")
dev./mvnw spring-boot:run -Dspring-boot.run.profiles=dev
The following 1 profile is active: "dev"
application.properties + application-dev.properties
SmsSender → FakeSmsSender
INFO  [FAKE SMS] 01711111111 : Order confirmed
prodসার্ভারের environment variable
SPRING_PROFILES_ACTIVE=prod
SHOP_SMS_TOKEN=****            ← secret, কোডে নেই
java -jar shop.jar
SmsSender → RealSmsSender(SmsProperties, HttpClient)
কোনো profile সক্রিয় নয়প্রত্যাশিত আউটপুট (সংক্ষেপে)
No active profile set, falling back to 1 default profile: "default"
APPLICATION FAILED TO START
Parameter 0 of constructor in com.shop.NotificationService
required a bean of type 'com.shop.SmsSender' that could not be found.
  1. একটি কোড, দুটি implementation. NotificationService শুধু SmsSender interface চেনে। কোন implementation আসবে, সেটা তার মাথাব্যথা নয় — এটাই dependency injection-এর মূল লাভ।
  2. dev profile. dev সক্রিয় হলে Spring application-dev.properties-ও পড়ে (যদি থাকে), আর @Profile("dev")-ওয়ালা FakeSmsSender bean হয়। RealSmsSender-এর শর্ত মেলে না, তাই সেটা তৈরিই হয় না। SMS শুধু log-এ।
  3. prod profile. সার্ভারে environment variable দিয়ে profile আর token দেওয়া। একই jar এবার RealSmsSender বানায়, যে SmsProperties থেকে URL আর token পায়। কোনো কোড বদলায়নি, কোনো নতুন build হয়নি।
  4. profile ভুলে গেলে। কোনো profile না দিলে সক্রিয় থাকে “default” profile — তখন দুটি bean-এর কোনোটিই শর্ত মেলায় না, আর অ্যাপ চালুই হয় না। সমাধান: নকলটিকে @Profile("!prod") দিন (prod ছাড়া সব জায়গায়), অথবা application.properties-এ spring.profiles.active=dev ডিফল্ট রাখুন আর সার্ভারে environment variable দিয়ে বদলান।

Spring Boot যা স্বয়ংক্রিয়ভাবে করে, তার মূল যুক্তি সাধারণ Java-তে চালিয়ে দেখুন। প্রথমে একটি class যে নিজেই new করে, তারপর constructor injection-এর class — যাকে “profile” অনুযায়ী আলাদা implementation দেওয়া হয়, আর টেস্টে নিজের হাতে নকল:

import java.util.*;
interface SmsSender { String send(String to, String text); }
record RealSmsSender(String url) implements SmsSender {
    public String send(String to, String text) { return "POST " + url + " to=" + to; }
}
class FakeSmsSender implements SmsSender {
    final List<String> sent = new ArrayList<>();
    public String send(String to, String text) { sent.add(to + " " + text); return "fake, saved " + to; }
}
class HardWiredNotifier {                       // নিজেই new করে — বদলানো যায় না
    private final SmsSender sms = new RealSmsSender("https://sms.example.com/api");
    String welcome(String phone) { return sms.send(phone, "Welcome"); }
}
class NotificationService {                     // বাইরে থেকে পায় — constructor injection
    private final SmsSender sms;
    NotificationService(SmsSender sms) { this.sms = sms; }
    String welcome(String phone) { return sms.send(phone, "Welcome"); }
}
class Main {
    static SmsSender smsFor(String profile, Map<String, String> config) {
        return switch (profile) {
            case "prod" -> new RealSmsSender(config.get("sms.gateway.url"));
            default -> new FakeSmsSender();
        };
    }
    public static void main(String[] args) {
        var config = Map.of("sms.gateway.url", "https://sms.example.com/api");
        System.out.println("hard-wired: " + new HardWiredNotifier().welcome("01711111111"));
        for (String profile : List.of("dev", "prod"))
            System.out.println(profile + ": " + new NotificationService(smsFor(profile, config)).welcome("01711111111"));
        var fake = new FakeSmsSender();             // টেস্টে নিজের হাতে নকল দেওয়া
        new NotificationService(fake).welcome("01822222222");
        System.out.println("test saw: " + fake.sent);
    }
}

আউটপুট চার লাইন: hard-wired: POST https://sms.example.com/api to=01711111111, dev: fake, saved 01711111111, prod: POST https://sms.example.com/api to=01711111111, আর test saw: [01822222222 Welcome]. HardWiredNotifier-কে কোনোভাবেই নকল দিয়ে চালানো যায় না — সে সবসময় আসল URL-এ পাঠাতে চাইবে; টেস্টে সত্যিকারের SMS যেত। NotificationService-এর কোড একটুও না বদলে দুটি “profile”-এ দুটি আলাদা আচরণ, আর শেষ লাইনে টেস্টের মতো করে একটি নকল দিয়ে যাচাই — নকলটি কী পাঠানো হলো তা মনে রেখেছে। smsFor method হলো Spring-এর @Profile আর properties-এর ছোট্ট হাতে-লেখা রূপ; Spring-এ এটা আপনাকে লিখতে হয় না। RealSmsSender একটি record, তাই তার একমাত্র মান (URL) constructor-এ আসে।

৭ · logging, প্রথম endpoint, Actuator, DevTools আর virtual thread

logging — println নয়

সার্ভারে চলা অ্যাপে System.out.println যথেষ্ট নয়: কখন লেখা হলো, কোন class থেকে, কতটা গুরুত্বপূর্ণ — কিছুই থাকে না, আর বন্ধও করা যায় না। Spring Boot-এ ডিফল্টে SLF4J (লেখার API) আর Logback (আসল লেখক) সাজানো থাকে:

private static final Logger log = LoggerFactory.getLogger(OrderService.class);

log.debug("checking stock for {}", productId);          // খুঁটিনাটি — dev-এ
log.info("order {} placed, amount {}", orderId, amount); // স্বাভাবিক ঘটনা
log.warn("SMS gateway slow: {} ms", elapsed);            // সন্দেহজনক
log.error("payment failed for order {}", orderId, ex);   // ভুল — exception সহ

{} জায়গাধারী: লেখাটা আসলেই ছাপা হলে তবেই String জোড়া হয়, তাই বন্ধ থাকা debug log প্রায় বিনা খরচে। log level-এর ক্রম TRACE < DEBUG < INFO < WARN < ERROR; Spring Boot-এর ডিফল্ট INFO, মানে debug আর trace ছাপা হয় না। logging.level.com.shop=DEBUG দিয়ে শুধু নিজের package-এর জন্য বাড়ানো যায়। log-এ কখনো পাসওয়ার্ড, পুরো কার্ড নম্বর বা OTP লিখবেন না।

প্রথম endpoint — একটি ঝলক

@RestController
public class HelloController {
    @GetMapping("/hello")
    public String hello() {
        return "Assalamu Alaikum from Spring Boot!";
    }
}
curl http://localhost:8080/hello
# প্রত্যাশিত আউটপুট: Assalamu Alaikum from Spring Boot!

@RestController একটি stereotype (তাই স্ক্যানে ধরা পড়ে), আর @GetMapping("/hello") বলে এই URL-এ GET অনুরোধ এলে method-টি চলবে, ফেরত মান সরাসরি উত্তর হিসেবে যাবে। record ফেরত দিলে Jackson সেটাকে JSON বানায়। URL-এর ভেতরের মান পড়া, POST-এ JSON নেওয়া, যাচাই, ভুলের উত্তর, JPA দিয়ে ডেটাবেস — এসব পুরো পর্ব ৫১-এর বিষয়।

Actuator আর DevTools

Actuator starter যোগ করলে অ্যাপ নিজের স্বাস্থ্যের খবর দিতে পারে: /actuator/health-এ গেলে উত্তর {"status":"UP"}. ডেটাবেস বা disk-এর মতো নির্ভরতা খারাপ হলে সেটা DOWN হয়। Kubernetes, load balancer বা monitoring টুল এই URL দেখে বোঝে অ্যাপটি অনুরোধ নিতে প্রস্তুত কি না। নিরাপত্তার কারণে HTTP দিয়ে ডিফল্টে মূলত শুধু health খোলা থাকে; বাকি endpoint (metrics, env) খুলতে আলাদা property লাগে — আর সেগুলো সাধারণ ব্যবহারকারীর কাছে খোলা রাখা উচিত নয়।

DevTools (spring-boot-devtools) development-এর আরাম: কোড বদলে compile করলে অ্যাপ নিজে থেকেই দ্রুত restart হয়, template-এর cache বন্ধ থাকে। java -jar দিয়ে পুরো প্যাকেজ করা অ্যাপ চালালে DevTools নিজেই বন্ধ হয়ে যায়, তবু এটা শুধু development-এর dependency হিসেবে রাখাই নিয়ম।

virtual thread — একটি লাইনের সুইচ

embedded Tomcat ডিফল্টে প্রতিটি অনুরোধের জন্য একটি platform thread ব্যবহার করে (thread-per-request), আর thread pool-এর আকার সীমিত। পর্ব ৪২-এ দেখেছেন, virtual thread খুব হালকা, তাই ডেটাবেস বা SMS gateway-র উত্তরের অপেক্ষায় বসে থাকা হাজারো অনুরোধ একসাথে সামলানো যায়। Spring Boot 3.2 থেকে, Java 21-এ, শুধু একটি property:

spring.threads.virtual.enabled=true

এতে Tomcat অনুরোধ সামলাতে virtual thread ব্যবহার করে, আর Spring-এর কিছু অন্য executor-ও। কোড একই থাকে — সাধারণ blocking স্টাইল। তবে পর্ব ৪২-এর সতর্কতা এখানেও খাটে: CPU-ভারী কাজ দ্রুত হয় না, আর ডেটাবেস connection pool-এর আকার এখনো আসল সীমা।

৮ · Spring Boot 3, jakarta.* আর কখন Spring বেশি ভারী

Spring Boot 3.0 (২০২২-এর শেষে) দুটি বড় বদল আনে, যা পুরোনো কোড আর AI-এর উত্তর পড়ার সময় জানা জরুরি। এই পর্বের কোড Spring Boot 3.x ধরে লেখা; পরের সংস্করণে কিছু starter-এর নাম বা ডিফল্ট বদলাতে পারে — সন্দেহ হলে start.spring.io যা তৈরি করে আর অফিসিয়াল ডকুমেন্টেশন, সেটাই মানুন।

  • Java 17 ন্যূনতম। Spring Boot 3 Java 17 বা নতুন ছাড়া চলে না। Java 21 ব্যবহার করলে virtual thread-এর সুবিধাও পাবেন।
  • javax.* থেকে jakarta.*. Java EE-র API-গুলো (persistence, validation, servlet, annotation) Eclipse Foundation-এ গিয়ে Jakarta EE নামে চলে, আর Jakarta EE 9 থেকে তাদের package-এর নাম javax. থেকে jakarta. হয়েছে। Spring Boot 3 নতুন নাম ব্যবহার করে: jakarta.persistence.Entity, jakarta.validation.constraints.NotBlank, jakarta.servlet.http.HttpServletRequest, jakarta.annotation.PostConstruct.

Boot 3 প্রজেক্টে পুরোনো import javax.persistence.Entity; লিখলে (আর পুরোনো লাইব্রেরি classpath-এ না থাকলে) javac সাধারণ একটি বার্তা দেয় (OpenJDK 21-এ যাচাই করা, ফাইলের নাম বদলানো):

Customer.java:1: error: package javax.persistence does not exist
import javax.persistence.Entity;
                        ^
Customer.java:3: error: cannot find symbol
@Entity
 ^
  symbol: class Entity
2 errors

সাবধানে বুঝুন: সব javax বদলায়নি। শুধু আগের Java EE (enterprise) API-গুলো jakarta-তে গেছে। JDK-র নিজের অংশ — javax.sql.DataSource (পর্ব ৪৯), javax.crypto, javax.net.ssl — আগের নামেই আছে, Spring Boot 3-এও। তাই “সব javax-কে jakarta বানিয়ে দাও” — এমন অন্ধ খোঁজ-বদল কোড ভাঙে।

Spring কখন বেশি ভারী

Spring Boot-এর চালুর সময় স্ক্যান, reflection আর proxy তৈরিতে কিছু সময় আর মেমরি লাগে — একটি সাধারণ অ্যাপে কয়েক সেকেন্ড আর কয়েকশো MB পর্যন্ত। দীর্ঘ সময় চলা সার্ভারে এটা নগণ্য। কিন্তু কিছু জায়গায় এটা ভাবার বিষয়:

  • ছোট CLI টুল বা একটি ফাইল প্রসেস করার script: পর্ব ২২-এর মতো সাধারণ Java, প্রয়োজনে picocli — পুরো container লাগে না।
  • serverless আর বারবার চালু-বন্ধ হওয়া container: চালুর প্রতিটি সেকেন্ড খরচ। এখানে Quarkus, Micronaut বা Helidon-এর মতো framework নজর কাড়ে — এরা অনেক কাজ build-এর সময় সেরে রাখে আর GraalVM native-image-এর সাথে খুব দ্রুত চালু হয়। Spring Boot নিজেও AOT আর native-image সমর্থন এনেছে।
  • শেখার শুরুতে: Spring অনেক কিছু লুকিয়ে রাখে। আগে সাধারণ Java-তে DI, JDBC, HTTP বুঝে তারপর Spring — যেভাবে এই কোর্স এগিয়েছে — ডিবাগ করার শক্তি দেয়।

তবু বড় ব্যবসায়িক অ্যাপ, ব্যাংক, টেলিকম, ই-কমার্স আর enterprise-এর ব্যাকএন্ডে Spring Boot আজও সবচেয়ে প্রচলিত Java পছন্দগুলোর একটি — তার বিশাল ইকোসিস্টেম (security, data, messaging, cloud) আর দীর্ঘ সময়ের স্থিতিশীলতার কারণে।

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

IoC আর DI-এর পার্থক্য কী? IoC হলো নিয়ন্ত্রণ framework-এর হাতে দেওয়ার সাধারণ নীতি; DI তার একটি রূপ, যেখানে নির্ভরতা বাইরে থেকে ঢুকিয়ে দেওয়া হয়।
constructor injection কেন field injection-এর চেয়ে ভালো? field final রাখা যায়, নির্ভরতা স্পষ্ট, আর Spring ছাড়াই টেস্টে নকল দেওয়া যায়।
Spring bean-এর ডিফল্ট scope কী? singleton — প্রতিটি container-এ একটি object.
@SpringBootApplication কী কী মিলিয়ে তৈরি? @SpringBootConfiguration (একটি @Configuration), @EnableAutoConfiguration আর @ComponentScan.
একই interface-এর দুটি bean থাকলে কী হয়? চালুর সময় ভুল; @Primary, @Qualifier বা profile দিয়ে একটি বেছে দিতে হয়।

৯ · AI-এর যুগে — পুরোনো আর বানানো Spring কোড চিনে ফেলা

Spring নিয়ে ইন্টারনেটে প্রায় বিশ বছরের লেখা আছে — XML কনফিগারেশন, Spring Boot 1 আর 2, field injection-এর যুগ। AI এই সব লেখা থেকে শিখেছে, তাই তার উত্তরে প্রায়ই নতুন আর পুরোনো মিশে যায়। কোড compile হলেও ভুল সংস্করণের ধারণা থাকতে পারে, আর কখনো AI এমন property বা annotation লেখে, যার অস্তিত্বই নেই — Spring Boot অজানা property সাধারণত নীরবে উপেক্ষা করে, তাই কিছুই ঘটে না, ভুলও ধরা পড়ে না।

AI যা করেকেন সমস্যাআপনি যা করবেন
সব জায়গায় @Autowired private fieldটেস্টে null, final নয়, নির্ভরতা লুকানোconstructor injection চান; একটি constructor হলে @Autowired লাগে না
javax.persistence আর jakarta.validation একই ফাইলেBoot 3-এ javax EE package নেই; Boot 2-তে jakarta নেইBoot সংস্করণ বলে দিন; import-গুলো মিলিয়ে দেখুন
অস্তিত্বহীন property (দেখতে বিশ্বাসযোগ্য নাম)Spring নীরবে উপেক্ষা করে — কনফিগারেশন কাজই করে নাঅফিসিয়াল “Common Application Properties” তালিকায় খুঁজুন; IDE-র auto-complete দেখুন
Boot 2-এর পুরোনো কৌশল (যেমন WebSecurityConfigurerAdapter)নতুন সংস্করণে সরানো হয়েছে; compile-ই হয় নাপ্রতিটি অচেনা class-এর নাম বর্তমান ডকুমেন্টেশনে যাচাই করুন
secret সরাসরি application.properties-এGit-এ ফাঁস হয়${ENV_VAR} বা secret manager চান
singleton service-এ অনুরোধের ডেটা field-এ রাখেএকসাথে আসা অনুরোধে ডেটা মিশে যায়অবস্থাহীন service; ডেটা parameter-এ

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

আমি Spring Boot 3.x আর Java 21 ব্যবহার করছি, build টুল Maven. [কাজের বর্ণনা — যেমন: একটি OrderService যা PaymentGateway (bKash/Nagad) আর SmsSender ব্যবহার করে] শর্ত: ১. শুধু constructor injection; কোনো @Autowired field নয়। একটি constructor হলে @Autowired লিখবে না। ২. শুধু jakarta.* import (jakarta.persistence, jakarta.validation, jakarta.annotation) ব্যবহার করবে। javax.* EE package নয়; তবে javax.sql-এর মতো JDK package ঠিক আছে। ৩. প্রতিটি property-র নাম অফিসিয়াল ডকুমেন্টেশনে আছে কি না নিশ্চিত হয়ে লেখো; নিজের property হলে "shop." prefix আর একটি @ConfigurationProperties record দাও। ৪. কোনো password, token বা API key কোডে বা properties-এ নয় — ${ENV_VAR} ব্যবহার করো। ৫. একই interface-এর একাধিক implementation হলে @Qualifier বা @Profile দিয়ে স্পষ্ট করো, আর কেন তা বলো। ৬. Spring Boot 2-এর কোনো সরানো class (যেমন WebSecurityConfigurerAdapter) ব্যবহার করবে না। ৭. শেষে বলো কোন starter লাগবে, আর Spring ছাড়া সাধারণ JUnit টেস্টে OrderService-কে নকল নির্ভরতা দিয়ে কীভাবে চালানো যায়।

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

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

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

প্র ০১Spring কেন জন্মাল? ২০০২ সালের Rod Johnson-এর বই আর ২০০৪ সালের Spring 1.0 কোন সমস্যার প্রতিক্রিয়া ছিল, আর “POJO” কথাটা এত গুরুত্বপূর্ণ হলো কেন?

২০০০-এর দশকের শুরুতে Java-তে বড় ব্যবসায়িক অ্যাপ লেখার “অফিসিয়াল” পথ ছিল J2EE আর তার কেন্দ্রে EJB (Enterprise JavaBeans) 2.x. একটি সাধারণ ব্যবসায়িক object লিখতে লাগত কয়েকটি interface (remote, home), একটি implementation class যাকে নির্দিষ্ট EJB interface implement করতে হতো, আর একটি বড় XML deployment descriptor. কোডটি শুধু একটি ভারী application server-এর ভেতরেই চলত — সাধারণ main বা একটি সাধারণ unit test থেকে চালানো প্রায় অসম্ভব। প্রতিটি ছোট বদলের পরে package করে সার্ভারে deploy করে তবেই দেখা যেত কাজ করছে কি না।

Rod Johnson ২০০২ সালে Expert One-on-One J2EE Design and Development বইয়ে একটি সাহসী কথা বললেন: অধিকাংশ অ্যাপের EJB-র জটিলতা দরকারই নেই। transaction, নিরাপত্তা, কনফিগারেশনের মতো সেবা সাধারণ Java object-এর চারপাশেও দেওয়া যায়, যদি একটি হালকা container object-গুলো বানিয়ে জোড়া দেয়। বইয়ের সাথে তিনি প্রায় তিরিশ হাজার লাইনের একটি কাঠামোর কোড দেন; পাঠকদের আগ্রহে Juergen Hoeller-সহ কয়েকজন মিলে সেটাকে open source প্রজেক্টে রূপ দেন, নাম হয় Spring — শীতের (J2EE-র কঠিন দিন) পরে বসন্ত। Spring 1.0 আসে ২০০৪ সালের মার্চে।

এর কেন্দ্রের ধারণা ছিল POJO — “Plain Old Java Object”, কথাটি Martin Fowler, Rebecca Parsons আর Josh MacKenzie-র তৈরি। আপনার ব্যবসায়িক class কোনো framework-এর interface implement করবে না, কোনো বিশেষ base class থেকে আসবে না; সে একটি সাধারণ class, যার নির্ভরতা বাইরে থেকে আসে। এর সরাসরি ফল: সার্ভার ছাড়াই new OrderService(fakeGateway) লিখে টেস্ট। JUnit-এর জনপ্রিয়তা আর test-driven development-এর উত্থানের সাথে এটা নিখুঁতভাবে মিলে গেল।

প্রভাব এতটাই হলো যে Java EE নিজেই বদলাল: EJB 3.0 (Java EE 5, ২০০৬) annotation আর POJO-ভিত্তিক হলো, আর পরে CDI নামে মানসম্মত dependency injection এলো। মজার ব্যাপার, পরের দশকে Spring নিজেই এত বড় হয়ে গেল যে “Spring-ও ভারী” অভিযোগ উঠল — যার উত্তর Spring Boot.

মূল উপলব্ধি: Spring-এর আসল উদ্ভাবন কোনো একটি API নয় — ব্যবসায়িক কোডকে framework থেকে আলাদা রাখার নীতি, যাতে সে সাধারণ Java থাকে আর সহজে টেস্ট করা যায়; constructor injection আজও সেই নীতিরই প্রয়োগ।

প্র ০২Martin Fowler ২০০৪ সালে “Inversion of Control” নামটিকে অস্পষ্ট বলে নতুন নাম দিলেন “Dependency Injection” — কেন? DI আর Service Locator-এর পার্থক্য কী, আর কোনটা কখন ভালো?

২০০৩-০৪ সালে Java জগতে কয়েকটি “হালকা container” পাশাপাশি জনপ্রিয় হচ্ছিল — Spring, PicoContainer, Apache Avalon/HiveMind. সবাই নিজেদের পরিচয় দিত “Inversion of Control container” বলে। Martin Fowler ২০০৪ সালের জানুয়ারিতে তাঁর প্রবন্ধ Inversion of Control Containers and the Dependency Injection pattern-এ একটি গুরুত্বপূর্ণ আপত্তি তুললেন: IoC কথাটা খুব সাধারণ। যেকোনো framework-ই কোনো না কোনোভাবে নিয়ন্ত্রণ উল্টে দেয় — একটি GUI framework-এ আপনি event listener দেন আর framework বোতাম চাপলে আপনাকে ডাকে, সেটাও IoC. তাহলে এই container-গুলো কোন নিয়ন্ত্রণ উল্টাচ্ছে? উত্তর: নির্ভরতা খুঁজে আনার নিয়ন্ত্রণ। আরও স্পষ্ট নাম হিসেবে, বিভিন্ন IoC সমর্থকের সাথে দীর্ঘ আলোচনার পরে, তিনি বেছে নিলেন “Dependency Injection” নামটি।

প্রবন্ধে তিনি তিন ধরনের injection বর্ণনা করেন — constructor, setter আর interface injection — আর DI-কে তুলনা করেন একটি বিকল্প নকশার সাথে: Service Locator. Service Locator-এ object নিজেই একটি কেন্দ্রীয় রেজিস্ট্রিকে জিজ্ঞেস করে: Registry.get(PaymentGateway.class). এটাও implementation থেকে আলাদা করে, কিন্তু একটি পার্থক্য আছে: প্রতিটি class এখন locator-এর ওপর নির্ভর করে, আর তার কী কী লাগবে তা constructor দেখে বোঝা যায় না — কোডের ভেতরে খুঁজতে হয়। টেস্টে locator-এ আগে থেকে নকল বসিয়ে রাখতে হয়, যা ভুলে গেলে অন্য টেস্টের নকল চলে আসতে পারে।

Fowler কোনো একটিকে সবসময় সেরা বলেননি; তাঁর মত ছিল, ছোট পরিসরে Service Locator সরল হতে পারে, কিন্তু অন্যের ব্যবহারের জন্য লেখা component-এ DI ভালো, কারণ ব্যবহারকারী নির্ভরতা নিজের মতো দিতে পারে। সময়ের সাথে শিল্প প্রায় পুরোপুরি DI-র দিকে ঝুঁকেছে, আর constructor injection বিশেষভাবে, কারণ এতে object তৈরির মুহূর্তেই সম্পূর্ণ আর অপরিবর্তনীয়। তবে Spring-এর ApplicationContext.getBean(...) নিজেই একটি service locator — তাই ব্যবসায়িক কোডে getBean ডাকা ভালো অভ্যাস নয়।

মূল উপলব্ধি: সঠিক নাম চিন্তাকে স্পষ্ট করে — “IoC” বলে framework কী করে, “DI” বলে আপনার class কেমন হবে: যা লাগবে তা ঘোষণা করবে, খুঁজতে যাবে না।

প্র ০৩Spring Boot (২০১৪) Spring-এর কোনো নতুন ক্ষমতা আনেনি, তবু Java জগৎ বদলে দিল। convention over configuration আর auto-configuration এর পেছনে কী ছিল, আর “জাদু”র দাম কী?

২০১০-এর দশকের শুরুতে Spring শক্তিশালী কিন্তু শুরু করা কঠিন ছিল। একটি নতুন web প্রজেক্টে লাগত সঠিক সংস্করণের অনেকগুলো লাইব্রেরি মেলানো (Spring-এর বিভিন্ন module, Jackson, Hibernate, logging — একটির সংস্করণ আরেকটির সাথে না মিললে অদ্ভুত ভুল), XML বা Java কনফিগারেশনে DispatcherServlet, view resolver, transaction manager সাজানো, তারপর WAR বানিয়ে আলাদা Tomcat-এ deploy. একই সময়ে Ruby on Rails আর Node.js দেখাচ্ছিল, একটি কমান্ডে প্রজেক্ট, আরেকটিতে চালু। Spring-এর issue tracker-এ ২০১২ সালে একটি অনুরোধ উঠেছিল — container ছাড়া, সহজে Spring web অ্যাপ চালানোর ব্যবস্থা — যাকে প্রায়ই Spring Boot-এর সূচনা হিসেবে উল্লেখ করা হয়। Spring Boot 1.0 আসে ২০১৪ সালের এপ্রিলে।

Boot-এর চারটি স্তম্ভ: starter (পরীক্ষিত dependency-র বান্ডিল, সংস্করণ মেলানোর ঝামেলা শেষ), auto-configuration (classpath আর properties দেখে শর্তসাপেক্ষ bean), embedded server (সার্ভার অ্যাপের ভেতরে, java -jar-এ চালু), আর externalized configuration (একই jar, পরিবেশ অনুযায়ী মান)। এর কোনোটিই Spring-এর নতুন মূল ক্ষমতা নয় — সবই আগের Spring দিয়ে হাতে করা যেত। Boot যা করল তা হলো সিদ্ধান্তগুলো আগে থেকে নিয়ে রাখা: “আপনি web starter দিয়েছেন, তাহলে নিশ্চয়ই Tomcat আর JSON চান”। এই সময়েই microservice আর Docker জনপ্রিয় হচ্ছিল, যেখানে একটি স্বয়ংসম্পূর্ণ jar ঠিক যা দরকার।

দাম কী? প্রথমত, অদৃশ্য আচরণ: একটি bean কোথা থেকে এলো, কেন একটি ডিফল্ট কাজ করছে, তা কোড পড়ে বোঝা যায় না — জানতে হয় Boot-এর রীতি, বা --debug-এর condition report পড়তে হয়। দ্বিতীয়ত, classpath-এ একটি লাইব্রেরি যোগ করলেই আচরণ বদলে যেতে পারে — একটি transitive dependency অজান্তে একটি auto-configuration চালু করে দিল। তৃতীয়ত, নতুনরা “annotation লাগালে কাজ হয়” শেখেন, কিন্তু ভুল হলে কোথায় খুঁজবেন জানেন না। Boot-এর নকশাকারীরা এজন্য একটি নীতি রাখেন: আপনার নিজের bean সবসময় auto-configuration-এর চেয়ে অগ্রাধিকার পায় (@ConditionalOnMissingBean), যাতে জাদু থেকে বের হওয়ার দরজা খোলা থাকে।

মূল উপলব্ধি: ভালো ডিফল্ট শুরুর গতি দেয়, কিন্তু নিয়ন্ত্রণ পেতে হলে ডিফল্টের পেছনের যন্ত্র বুঝতে হয় — এই পর্বের mini container আর auto-configuration-এর শর্ত বোঝা সেই নিয়ন্ত্রণের প্রথম ধাপ।

প্র ০৪“Don't call us, we'll call you” — Hollywood নীতি দিয়ে framework আর library-র পার্থক্য কীভাবে বোঝা যায়? Spring-এ এই নীতির সুবিধা আর ঝুঁকি কী?

Hollywood-এর অভিনেতা বাছাইয়ের গল্প থেকে কথাটি এসেছে: অডিশন দেওয়ার পরে প্রার্থীকে বলা হতো, “আমাদের ফোন করবেন না, দরকার হলে আমরাই করব”। সফটওয়্যারে কথাটি ১৯৮০-এর দশক থেকে ব্যবহৃত, আর inversion of control ধারণার সাথে যুক্ত; ১৯৮৮ সালে Ralph Johnson আর Brian Foote-এর একটি প্রবন্ধ framework-এর এই বৈশিষ্ট্যকে স্পষ্টভাবে বর্ণনা করে।

এই নীতি দিয়ে library আর framework-এর পার্থক্য সবচেয়ে পরিষ্কারভাবে বোঝা যায়। library আপনি ডাকেন: Math.max, Jackson-এর objectMapper.readValue, JDBC-র executeQuery. নিয়ন্ত্রণ আপনার হাতে — কখন ডাকবেন, ফল দিয়ে কী করবেন, সব আপনার সিদ্ধান্ত। framework আপনাকে ডাকে: Spring ঠিক করে কখন OrderService তৈরি হবে, কখন @PostConstruct চলবে, কোন HTTP অনুরোধে কোন controller method চলবে। আপনি কাঠামোর ফাঁকা জায়গাগুলো পূরণ করেন। JUnit-ও framework: আপনি @Test method লেখেন, JUnit ডাকে। পর্ব ২১-এ দেখা template method pattern এরই ছোট রূপ — base class প্রবাহ ঠিক করে, subclass শুধু ধাপগুলো দেয়।

সুবিধা বিশাল: thread, HTTP parsing, transaction, connection pool, নিরাপত্তা — এসব কঠিন আর ভুলপ্রবণ কাজ framework একবার ভালোভাবে করে, আপনি শুধু ব্যবসার যুক্তি লেখেন। একটি দলের সবার কোড একই কাঠামোয় থাকে, তাই নতুন সদস্য দ্রুত বুঝতে পারেন। কিন্তু ঝুঁকিও আছে। প্রবাহ আপনার কোডে দেখা যায় না — stack trace-এ আপনার একটি method-এর ওপরে framework-এর বহু স্তর। framework-এর রীতির সাথে বাঁধা পড়েন; অন্য framework-এ যাওয়া মানে বড় পুনর্লিখন। আর framework-এর আচরণ না বুঝলে ডিবাগ করা অনুমানের খেলা হয়ে দাঁড়ায়।

এই ঝুঁকি কমানোর একটি পরীক্ষিত কৌশল: ব্যবসার মূল নিয়ম (VAT হিসাব, ডিসকাউন্ট, অর্ডারের অবস্থা) সাধারণ Java class-এ রাখুন, যারা Spring-এর কিছুই জানে না; Spring শুধু বাইরের স্তরে — web, ডেটাবেস, জোড়া লাগানো। তাহলে মূল নিয়মগুলো দ্রুত সাধারণ JUnit-এ টেস্ট হয়, আর framework বদলালেও সেগুলো থেকে যায়।

মূল উপলব্ধি: library ব্যবহার করলে আপনি নিয়ন্ত্রক, framework ব্যবহার করলে আপনি অতিথি — অতিথি হওয়ার সুবিধা নিন, কিন্তু নিজের মূল্যবান যুক্তি এমনভাবে রাখুন যাতে ঘর বদলালেও সাথে নিয়ে যেতে পারেন।

প্র ০৫চাকরির বাজারে Spring এত গুরুত্বপূর্ণ কেন, আর Jakarta EE 9-এর javax থেকে jakarta-য় নাম বদল (২০২০) কেন ঘটল? একটি package-এর নাম বদল কীভাবে পুরো ইকোসিস্টেমকে নাড়িয়ে দিল?

বাংলাদেশসহ বিশ্বের বহু জায়গায় Java ব্যাকএন্ডের চাকরির বিজ্ঞপ্তিতে Spring Boot প্রায় নিয়মিত একটি চাওয়া — ব্যাংক, মোবাইল আর্থিক সেবা, টেলিকম, ই-কমার্স আর বিদেশি ক্লায়েন্টের জন্য কাজ করা সফটওয়্যার প্রতিষ্ঠানে। কারণগুলো বোঝা কঠিন নয়: বড় প্রতিষ্ঠান দীর্ঘ সময় টিকে থাকা, ভালোভাবে সমর্থিত প্রযুক্তি চায়; Spring প্রায় দুই দশক ধরে আছে, নিরাপত্তা (Spring Security), ডেটা, messaging, batch কাজ — প্রায় সব দরকারের জন্য একটি পরিণত অংশ আছে, আর অভিজ্ঞ ডেভেলপার পাওয়া তুলনামূলক সহজ। তবে বাজারের নির্দিষ্ট সংখ্যা সময় আর জায়গাভেদে বদলায়; Spring শেখার সবচেয়ে টেকসই মূল্য হলো এর নিচের ধারণাগুলো — DI, নির্ভরতা আলাদা রাখা, কনফিগারেশন বাইরে রাখা — যা অন্য framework-এও কাজে লাগে।

এবার নাম বদলের গল্প। Java EE ছিল Oracle-এর (আগে Sun-এর) নিয়ন্ত্রণে। ২০১৭ সালে Oracle Java EE-কে Eclipse Foundation-এ হস্তান্তরের সিদ্ধান্ত নেয়, যাতে এটি আরও খোলা প্রক্রিয়ায় এগোয়। কিন্তু “Java” trademark আর javax namespace-এ নতুন কিছু যোগ বা বদলের অধিকার Oracle হস্তান্তর করেনি। তাই নতুন প্ল্যাটফর্মের নাম হলো Jakarta EE. Jakarta EE 8 (২০১৯) পুরোনো javax নামেই ছিল, শুধু নতুন বাড়িতে। কিন্তু API-তে নতুন কিছু যোগ করতে হলে নতুন namespace লাগবে — তাই Jakarta EE 9 (২০২০-এর শেষে) একটি “বড় ধাক্কার” সিদ্ধান্ত নিল: নতুন কোনো feature না এনে শুধু সব package-এর নাম javax.* থেকে jakarta.*.

একটি নাম বদল কেন এত বড় ঘটনা? কারণ Java-তে package নাম class-এর পরিচয়ের অংশ। javax.persistence.Entity আর jakarta.persistence.Entity দুটি সম্পূর্ণ আলাদা টাইপ। ফলে প্রতিটি লাইব্রেরি — Hibernate, Tomcat, validation, Spring নিজে — নতুন সংস্করণ বের করতে হলো, আর একটি অ্যাপের সব dependency একসাথে নতুন প্রজন্মে যেতে হলো; একটি পুরোনো লাইব্রেরিও পুরো upgrade আটকে দিতে পারত। Spring Boot 3 (২০২২) এই লাফ দিল, একই সাথে Java 17-কে ন্যূনতম করে। OpenRewrite-এর মতো স্বয়ংক্রিয় রূপান্তর টুল আর IDE-র migration সহায়তা এই কাজ অনেক সহজ করেছে।

মূল উপলব্ধি: চাকরির বাজার নির্দিষ্ট টুল চায়, কিন্তু টিকে থাকে ধারণা; আর javax থেকে jakarta দেখায়, একটি ইকোসিস্টেমে আইনি আর প্রাতিষ্ঠানিক সিদ্ধান্তও কোডের প্রতিটি import পর্যন্ত পৌঁছাতে পারে — তাই সংস্করণ আর namespace সবসময় খেয়াল রাখুন।

অনুশীলন

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

  1. binding ভুলে গেলে: অংশ ২-এর mini container-এ box.bind(SmsSender.class, ConsoleSms.class); লাইনটি মুছে দিন। না চালিয়ে ভাবুন: container কী করার চেষ্টা করবে, আর কোথায় ব্যর্থ হবে? Spring-এ একই ভুল হলে কেমন বার্তা আসে?

    binding না থাকলে bindings.getOrDefault(type, type) interface SmsSender-কেই implementation ধরে নেয়। interface-এর কোনো constructor নেই, তাই getDeclaredConstructors() খালি array দেয়, আর [0]-তে ArrayIndexOutOfBoundsException (পর্ব ১৩) হয় — need SmsSender -> SmsSender লাইন ছাপার ঠিক পরেই। বার্তাটি আসল কারণ বলে না; একটি ভালো container এখানে “SmsSender-এর কোনো implementation নিবন্ধিত নেই” বলত। Spring ঠিক এটাই করে: required a bean of type 'com.shop.SmsSender' that could not be found — অ্যাপ চালুর সময়, প্রথম অনুরোধের আগেই। অনুশীলন হিসেবে container-এ if (impl.isInterface()) throw new IllegalStateException("no binding for " + type.getSimpleName()); যোগ করে দেখুন বার্তা কত পরিষ্কার হয়।

  2. scope বেছে নিন: প্রতিটির জন্য scope বলুন আর এক লাইনে কারণ: (ক) VatCalculator — শুধু properties থেকে পাওয়া হার দিয়ে হিসাব করে; (খ) ShoppingCart — একজন ব্যবহারকারীর কার্টের পণ্য রাখে; (গ) CsvReportBuilder — একটি রিপোর্ট তৈরির সময় সারি জমায়; (ঘ) OrderRepository.

    (ক) singleton — অবস্থাহীন, হার একবার পড়ে বদলায় না; সবার জন্য একটি object নিরাপদ। (খ) session scope (web অ্যাপে) — প্রতিটি ব্যবহারকারীর নিজস্ব; singleton হলে সবার কার্ট মিশে যেত, যা ভয়ংকর ভুল। অনেক আধুনিক অ্যাপ এটা ডেটাবেস বা cache-এ রাখে, যাতে সার্ভার একাধিক হলেও কাজ করে। (গ) prototype — প্রতিটি রিপোর্টের জন্য নতুন জমানোর জায়গা; singleton-এ রাখলে দুটি রিপোর্ট একসাথে তৈরি হলে সারি মিশে যেত। আর singleton service থেকে ব্যবহার করলে ObjectProvider লাগবে। অথবা আরও সহজ: এটাকে bean-ই বানাবেন না, প্রয়োজনের সময় new করুন — সব object-এর bean হওয়ার দরকার নেই। (ঘ) singleton — অবস্থাহীন; connection আসে pool থেকে, প্রতিটি method-এর ভেতরে।

  3. Nagad যোগ করুন: অংশ ৬-এর runnable প্রোগ্রামের ধাঁচে ভাবুন — দোকানে PaymentGateway-এর তিনটি implementation (bKash, Nagad, Card). গ্রাহক চেকআউটে যেটা বাছবেন সেটাই চলবে। Spring-এ OrderService-এর constructor কেমন হবে? if-else বা switch ছাড়া বাছাই কীভাবে করবেন?

    তিনটি implementation-কে নাম দিয়ে bean বানান (@Component("bkash") ইত্যাদি), আর OrderService-এ সবগুলো একসাথে নিন:

    @Service
    public class OrderService {
        private final Map<String, PaymentGateway> gateways;
        public OrderService(Map<String, PaymentGateway> gateways) { this.gateways = gateways; }
    
        public String pay(String method, long amount) {
            PaymentGateway g = gateways.get(method);          // "bkash", "nagad", "card"
            if (g == null) throw new IllegalArgumentException("unknown payment method: " + method);
            return g.pay(amount);
        }
    }

    Spring Map-এর key হিসেবে bean-এর নাম বসায়। নতুন “Rocket” যোগ করতে শুধু একটি নতুন @Component("rocket") class — OrderService একটুও বদলায় না (পর্ব ২০-এর open-closed নীতি)। অজানা নামের জন্য স্পষ্ট exception রাখা জরুরি, কারণ ইনপুট আসছে বাইরে থেকে। এখানে @Qualifier চলত না, কারণ বাছাই চালুর সময় নয়, প্রতিটি অনুরোধে।

  4. কনফিগারেশন কোথায় রাখবেন: নিচের প্রতিটি মান কোথায় রাখবেন — কোডে ধ্রুবক, application.properties, profile-এর ফাইল, নাকি environment variable/secret manager? (ক) VAT হার ১৫%; (খ) SMS gateway-র token; (গ) dev-এ log level DEBUG; (ঘ) একটি অর্ডারে সর্বোচ্চ ১০টি পণ্যের ব্যবসায়িক নিয়ম; (ঙ) আসল ডেটাবেসের পাসওয়ার্ড।

    (ক) application.properties — নীতি বদলালে কোড না বদলে মান বদলানো যায়, আর @ConfigurationProperties record-এ টাইপসহ পড়া যায়; সব পরিবেশে একই। (খ) environment variable বা secret manager — গোপন, কখনো Git-এ নয়; properties-এ শুধু ${SHOP_SMS_TOKEN}. (গ) application-dev.properties — শুধু dev-এর জন্য, prod-এ INFO থাকবে। (ঘ) এটা বিতর্কের বিষয়: সত্যিকারের ব্যবসায়িক নিয়ম, যা খুব কম বদলায় আর টেস্ট দিয়ে রক্ষা করা উচিত, কোডে একটি নামযুক্ত ধ্রুবক হিসেবে রাখা যুক্তিসংগত; ব্যবসা দল ঘন ঘন বদলাতে চাইলে properties-এ। মূল কথা: “বদলালে কি নতুন build লাগা উচিত?” (ঙ) environment variable বা secret manager — properties ফাইলে কখনো নয়, লোকাল dev-এর জন্য আলাদা, Git-উপেক্ষিত ফাইল।

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

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

ক্রমSpringApplication.run ডাকার পরে একটি Spring Boot web অ্যাপ চালুর ধাপগুলো সঠিক ক্রমে সাজান (▲▼ চেপে সরান)

  1. 🏁 ApplicationContext তৈরি
  2. 🔍 main class-এর package থেকে component scanning
  3. 📄 properties, profile আর environment variable পড়া
  4. 🧠 শর্তসাপেক্ষ auto-configuration যাচাই
  5. 🏭 নির্ভরতার ক্রমে bean তৈরি আর constructor-এ inject
  6. 🌐 embedded Tomcat চালু হয়ে অনুরোধের অপেক্ষা

Spring-এ একটি @Service class-এ ঠিক একটি constructor আছে, যার parameter দুটি bean. নির্ভরতা inject হওয়ার জন্য কী লাগবে?

  • constructor-এর ওপর @Autowired লেখা বাধ্যতামূলক
  • কিছুই না — একটিমাত্র constructor হলে Spring সেটাই ব্যবহার করে
  • XML ফাইলে bean দুটির নাম লিখতে হবে
  • field-গুলোতেও @Autowired দিতে হবে

Spring bean-এর ডিফল্ট scope কী, আর তার অর্থ কী?

  • prototype — প্রতিবার চাইলে নতুন object
  • request — প্রতিটি HTTP অনুরোধে নতুন object
  • singleton — প্রতিটি container-এ একটিই object
  • static — পুরো JVM-এ একটি static field

একটি Spring অ্যাপে PaymentGateway interface-এর তিনটি @Component implementation আছে, আর OrderService-এর constructor শুধু একটি PaymentGateway চায়। আর কিছু না করলে কী হবে?

  • অ্যাপ চালুর সময় ব্যর্থ হবে, কারণ একটি bean দরকার কিন্তু তিনটি পাওয়া গেছে
  • Spring বর্ণানুক্রমে প্রথমটি বেছে নেবে
  • compile error হবে
  • OrderService-এ null inject হবে

@SpringBootApplication annotation কোন তিনটি annotation-এর কাজ একসাথে করে?

  • @Service, @Repository আর @Controller
  • @Autowired, @Bean আর @Value
  • @RestController, @GetMapping আর @Profile
  • @SpringBootConfiguration (একটি @Configuration), @EnableAutoConfiguration আর @ComponentScan

Spring Boot 3 প্রজেক্টে JPA entity লিখতে কোন import সঠিক?

  • import javax.persistence.Entity;
  • import jakarta.persistence.Entity;
  • import org.springframework.stereotype.Entity;
  • import java.persistence.Entity;

একটি Spring Boot অ্যাপে SMS gateway-র গোপন token রাখার সবচেয়ে নিরাপদ পথ কোনটি?

  • কোডে একটি private static final String ধ্রুবক হিসেবে
  • application.properties-এ সরাসরি লিখে commit করা
  • environment variable বা secret manager-এ, আর properties-এ শুধু ${SHOP_SMS_TOKEN}
  • অ্যাপ চালুর সময় log-এ ছাপিয়ে রাখা, যাতে হারিয়ে না যায়

একটি singleton bean-এর constructor-এ একটি prototype-scope bean inject করা হলো। প্রতিবার singleton-এর method ডাকলে prototype-এর কয়টি object ব্যবহার হবে?

  • একটিই — singleton তৈরির সময় একবার inject হয়েছে
  • প্রতিবার নতুন একটি
  • Spring অ্যাপ চালু হতেই দেবে না
  • প্রতিটি thread-এর জন্য একটি

শব্দকোষ

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

মনে রাখুন

  1. hard-wired new টেস্ট আর পরিবর্তন কঠিন করে। নির্ভরতা interface হিসেবে constructor-এ নিন — dependency injection; জোড়া লাগানোর দায়িত্ব container-এর — Inversion of Control.
  2. container-এর মূল কাজ চারটি: কোন interface-এর কোন implementation, constructor পড়ে নির্ভরতার গাছ, ঠিক ক্রমে তৈরি, singleton হিসেবে রেখে দেওয়া। Spring-এ এটা ApplicationContext.
  3. bean বানানোর দুই পথ: নিজের class-এ @Service/@Repository/@Component; অন্যের class-এ @Configuration + @Bean. constructor injection প্রস্তাবিত; একাধিক implementation হলে @Primary, @Qualifier, profile বা Map.
  4. Spring Boot = starter + auto-configuration + embedded server + বাইরের কনফিগারেশন। @SpringBootApplication = configuration + auto-config + component scan; চালানো ./mvnw spring-boot:run বা java -jar.
  5. কনফিগারেশন কোডের বাইরে, secret Git-এর বাইরে। record দিয়ে @ConfigurationProperties, পরিবেশভেদে profile, token environment variable-এ। Boot 3 মানে Java 17+ আর jakarta.*.

পরের পর্বে: এখন অ্যাপ চালু হয় আর /hello-তে একটি লেখা ফেরত দেয়। এবার আসল কাজ — Spring Boot দিয়ে পুরো REST API: URL আর HTTP method, JSON নেওয়া-দেওয়া, ইনপুট যাচাই, ভুলের সঠিক উত্তর আর JPA দিয়ে ডেটাবেস।

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

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