DZone
Thanks for visiting DZone today,
Edit Profile
  • Manage Email Subscriptions
  • How to Post to DZone
  • Article Submission Guidelines
Sign Out View Profile
  • Post an Article
  • Manage My Drafts
Over 2 million developers have joined DZone.
Log In / Join
Please enter at least three characters to search
Refcards Trend Reports
Events Video Library
Refcards
Trend Reports

Events

View Events Video Library

Zones

Culture and Methodologies Agile Career Development Methodologies Team Management
Data Engineering AI/ML Big Data Data Databases IoT
Software Design and Architecture Cloud Architecture Containers Integration Microservices Performance Security
Coding Frameworks Java JavaScript Languages Tools
Testing, Deployment, and Maintenance Deployment DevOps and CI/CD Maintenance Monitoring and Observability Testing, Tools, and Frameworks
Culture and Methodologies
Agile Career Development Methodologies Team Management
Data Engineering
AI/ML Big Data Data Databases IoT
Software Design and Architecture
Cloud Architecture Containers Integration Microservices Performance Security
Coding
Frameworks Java JavaScript Languages Tools
Testing, Deployment, and Maintenance
Deployment DevOps and CI/CD Maintenance Monitoring and Observability Testing, Tools, and Frameworks

The software you build is only as secure as the code that powers it. Learn how malicious code creeps into your software supply chain.

Apache Cassandra combines the benefits of major NoSQL databases to support data management needs not covered by traditional RDBMS vendors.

Generative AI has transformed nearly every industry. How can you leverage GenAI to improve your productivity and efficiency?

Modernize your data layer. Learn how to design cloud-native database architectures to meet the evolving demands of AI and GenAI workloads.

Related

  • Understanding Jakarta EE 8 - CDI Part 1
  • Why I Started Using Dependency Injection in Python
  • Introducing SmallRye LLM: Injecting Langchain4J AI Services
  • Injecting Implementations With Jakarta CDI Using Polymorphism

Trending

  • Integration Isn’t a Task — It’s an Architectural Discipline
  • Go 1.24+ Native FIPS Support for Easier Compliance
  • Role of Cloud Architecture in Conversational AI
  • Customer 360: Fraud Detection in Fintech With PySpark and ML
  1. DZone
  2. Testing, Deployment, and Maintenance
  3. Testing, Tools, and Frameworks
  4. Making Readable Code With Dependency Injection and Jakarta CDI

Making Readable Code With Dependency Injection and Jakarta CDI

Learn more about dependency injection with Jakarta CDI and enhance the effectiveness and readability of your code.

By 
Otavio Santana user avatar
Otavio Santana
DZone Core CORE ·
Jan. 17, 22 · Tutorial
Likes (6)
Comment
Save
Tweet
Share
9.5K Views

Join the DZone community and get the full member experience.

Join For Free

Within programming, object orientation (also known as OOP) is one of the wealthiest paradigms in documentation and standards, as with dependency injection.

However, the most common is that these standards are misinterpreted and implemented and what would allow a clear vision and a good design for a cooperative application, in practice, harms the quality and efficiency of your code.

In this article, I will bring the advantages of exploring object orientation, more specifically dependency injection, together with the specification of the Java world that covers this API: the Jakarta CDI.

What Is Dependency Injection?

The first step is to contextualize what dependency injection is and why it differs from inversion of controls, although the two concepts are very confused.

In the context of OOP (Object Orientation), dependency is related to any direct subordination of a class, and that can be done in several ways. For example, through constructors, accessor methods, such as setters, or creation methods, such as the Factory Method.

Java
 
public Person(String name) {
 this.name = name;
}


This example injection also can measure how much one class is linked to another, which we call coupling. It is vital because it is part of the job of good code design to be concerned with high cohesion and low coupling.

The Difference Between Dependency Injection and Inversion

Once the basic concepts of OOP are explained, the next step is to understand what differs between injection and dependency inversion.

This distinction happens because of what we call the DIP, or Dependency Inversion Principle, which works as a guide to good practices focused on decoupling a class from concrete dependencies through two recommendations:

  1. High-level modules must not depend on low-level modules, but both must depend on abstractions (Example: interfaces).

  1. Abstractions, in turn, should not depend on details. However, parties or concrete implementations must depend on abstractions.

While inversion assumes all these requirements, dependency injection is a DIP technique to supply a class's dependencies, usually through a constructor, attribute or a method such as a setter.

In short: dependency injection is part of the best practices advocated by inversion of control (IoC).

As tricky as these concepts are, at first, they are fundamental because they can be correlated to the Barbara Liskov principle. After all, both the Liskov principle and dependency injection are part of SOLID.

Where Does CDI Come in, and How Do We Get to Jakarta CDI?

As with injection and inversion, it is crucial to take a step back and contextualize what CDI is.

The CDI (Dependency and Contexts Injection) arose from the need to create a specification within the JCP, JSR 365, mainly because, in the Java world and projects like Spring and Google Guice, there are several solutions and implementations of these frameworks.

Briefly, the purpose of Jakarta CDI is to allow the management of stateful or stateless component life control via context and component injection.

It is a project in constant evolution. If you want to keep up with the latest updates, it's worth knowing Eclipse Open-DI. It is currently being optimized to improve booting through CDI Lite.

CDI in Practice

To exemplify the features of CDI, we will create a simple Java SE application with CDI to show six essential elements within CDI, which are:

  1. Perform a single injection.

  2. Differentiate implementations through qualifications.

  3. Teach CDI to create and distribute objects.

  4. Apply Observer.

  5. Apply Decorator.

  6. Use the interceptor.

In this article, even to avoid making the text too big, we will highlight the codes. If you want, you can later access the code in full.

It is nice to mention that this content was created with Karina Varela and is part of the classes we took throughout 2021, mainly in American and European countries, in partnership with Microstream and Payara.

Perform a Single Injection

In our first implementation, we will create a "Vehicle" interface and an implementation using the Java SE container.

An important point: we have the context in addition to the injection of dependencies. We can define the life cycle of a class or bean, and, in this case, the implementation will have the application scope.

Java
 
try (SeContainer container = SeContainerInitializer.newInstance().initialize()) {
    Vehicle vehicle = container.select(Vehicle.class).get();
    vehicle.move();

    Car car = container.select(Car.class).get();
    car.move();

    System.out.println("Is the same vehicle? " + car.equals(vehicle));
}

public interface Vehicle {

    void move();
}

@ApplicationScoped
public class Car implements Vehicle {
//... implementation here
}


In this piece of code, we have the interface Vehicle and its respective implementation, Car, in which the CDI will be able to inject an instance both through the interface and through the implementation.

Since we are talking about best practices, please understand that we are using the case of a single interface for a single implementation for teaching purposes. In practice, ideally, you don't do this so as not to break the KISS principle.

A good indicator for this is interfaces that start with “I” or implementations that end with “Impl”. In addition to being an indicator of unnecessary complexity, it is a code smell. After all, it is not a meaningful name, breaking the principle of the Clean Code.

Differentiate Implementations Through Qualifications

In the previous example, we had the case of a one-to-one relationship, that is, an interface for a single implementation. But what happens when we have multiple implementations for the same interface?

If we don't have this set, CDI won't know the default implementation and will throw the AmbiguousResolutionException. You can solve this problem using the Named annotation or a Qualifier. In our case, we will use the Qualifiers.

Imagine the following scenario in which we have an orchestra with several musical instruments. This orchestra will need to play all the instruments together and be able to discriminate between them. In our example, this scenario would look something like the following code:

Java
 
<pre=”java”>
public interface Instrument {
    String sound();
}

public enum InstrumentType {
    STRING, PERCUSSION, KEYBOARD;
}

@Qualifier
@Retention(RUNTIME)
@Target({TYPE, METHOD, FIELD, PARAMETER})
public @interface MusicalInstrument {
    InstrumentType value();
}


We have the instrument interface, an enumerator to define its type, and the CDI Qualifier through a new annotation in this code example.

After the implementations and qualifiers, it will be pretty simple for the orchestra to play all the instruments together and special tools considering their different types, for example, string, percussion, or wood.

Java
 
@MusicalInstrument(InstrumentType.KEYBOARD)
@Default
class Piano implements Instrument {
    @Override
    public String sound() {
        return "piano";
    }
}

@MusicalInstrument(InstrumentType.STRING)
class Violin implements Instrument {
    @Override
    public String sound() {
        return "violin";
    }
}

@MusicalInstrument(InstrumentType.PERCUSSION)
class Xylophone implements Instrument {
    @Override
    public String sound() {
        return "xylophone";
    }
}

@ApplicationScoped
public class Orchestra {

    @Inject
    @MusicalInstrument(InstrumentType.PERCUSSION)
    private Instrument percussion;
    @Inject
    @MusicalInstrument(InstrumentType.KEYBOARD)
    private Instrument keyboard;

    @Inject
    @MusicalInstrument(InstrumentType.STRING)
    private Instrument string;

    @Inject
    private Instrument solo;

    @Inject
    @Any
    private Instance<Instrument> instruments;

}


The CDI container injects an Orchestra instance and demonstrates the use of each instrument.

Teach CDI to Create and Distribute Objects

In addition to defining its scope, it is often impossible to perform activities such as determining or creating a class within the CDI container. This type of resource is essential for cases such as, for example, when we want to inject a connection to some service.

It is possible to teach the container to create instances and destroy them within CDI through the Produces and Disposes annotations, respectively. For this example, we will create a connection that will be created and closed.

Java
 
public interface Connection extends AutoCloseable {

    void commit(String execute);
}

@ApplicationScoped
class ConnectionProducer {


    @Produces
    Connection getConnection() {
        return new SimpleConnection();
    }

    public void dispose(@Disposes Connection connection) throws Exception {
        connection.close();
    }

}


Right now, we have a connection from which we teach how the CDI should create and close the connection.

Java
 
Connection connection = container.select(Connection.class).get();
connection.commit("Database instruction");


Apply Observer

We cannot forget the Observer among the patterns very present in corporate architectures and present in the GoF.

One of the assessments I make of this pattern is that, given its importance, we can see it similarly in architectural practices, such as Event-Driven, and even in a paradigm with reactive programming.

In CDI, we can handle events synchronously and asynchronously. Imagine, for example, that we have a journalist, and he will need to notify all the necessary media. If we do the coupling of these media directly in the "Journalist" class, every time a media is added or removed, it will be necessary to modify it. It breaks the open-closed principle to solve; we will use Observer.

Java
 
@ApplicationScoped
public class Journalist {

    @Inject
    private Event<News> event;

    @Inject
    @Specific
    private Event<News> specificEvent;

    public void receiveNews(News news) {
        this.event.fire(news);
    }
}


public class Magazine implements Consumer<News> {

    private static final Logger LOGGER = Logger.getLogger(Magazine.class.getName());

    @Override
    public void accept(@Observes News news) {
        LOGGER.info("We got the news, we'll publish it on a magazine: " + news.get());
    }
}

public class NewsPaper implements Consumer<News> {

    private static final Logger LOGGER = Logger.getLogger(NewsPaper.class.getName());

    @Override
    public void accept(@Observes News news) {
        LOGGER.info("We got the news, we'll publish it on a newspaper: " + news.get());
    }
}

public class SocialMedia implements Consumer<News> {

    private static final Logger LOGGER = Logger.getLogger(SocialMedia.class.getName());

    @Override
    public void accept(@Observes News news) {
        LOGGER.info("We got the news, we'll publish it on Social Media: " + news.get());
    }
}


So we created a Journalist class that notifies the media, a novelty thanks to the Observer pattern with the CDI. The event is triggered by the Event instance, and to listen for it; it is necessary to use the @Observers annotation with the specific parameter to be listened to.

Apply Decorator

The Decorator pattern allows us to add behavior inside the object, obeying the principle of composition over inheritance. We see this within the Java world with wrappers of primitive types like Integer, Double, Long, etc.

Java
 
public interface Worker {

    String work(String job);
}

@ApplicationScoped
public class Programmer implements Worker {

    private static final Logger LOGGER = Logger.getLogger(Programmer.class.getName());

    @Override
    public String work(String job) {
        return "A programmer has received a job, it will convert coffee in code: " + job;
    }
}

@Decorator
@Priority(Interceptor.Priority.APPLICATION)
public class Manager implements Worker {

    @Inject
    @Delegate
    @Any
    private Worker worker;

    @Override
    public String work(String job) {
        return "A manager has received a job and it will delegate to a programmer -> " + worker.work(job);
    }
}


We created a worker abstraction, the Programmer, and the Manager responsible for delegating the worker. In this way, we could add a behavior, such as sending an email, without modifying the programmer.

Java
 
Worker worker = container.select(Worker.class).get();
String work = worker.work("Just a single button");
System.out.println("The work result: " + work);


The Interceptor

The CDI can also perform and control some operations in the code in a transversal way, similar to what we do with aspect-oriented programming and cutpoints with Spring.

The CDI interceptor tends to be quite helpful when we want, for example, a logging mechanism, transaction control, or a timer for a method that will be executed, among others. In this case, the sample used will be a timer with an interceptor.

Java
 
@InterceptorBinding
@Target({METHOD, TYPE})
@Retention(RUNTIME)
public @interface Timed {
}

@Timed
@Interceptor
@Priority(Interceptor.Priority.APPLICATION)
public class TimedInterceptor {

    private static final Logger LOGGER = Logger.getLogger(TimedInterceptor.class.getName());

    @AroundInvoke
    public Object auditMethod(InvocationContext ctx) throws Exception {
        long start = System.currentTimeMillis();
        Object result = ctx.proceed();
        long end = System.currentTimeMillis() - start;
        String message = String.format("Time to execute the class %s, the method %s is of %d milliseconds",
                ctx.getTarget().getClass(), ctx.getMethod(), end);
        LOGGER.info(message);
        return result;
    }
}


The interceptor implementation will allow the creation of the annotation that will be used to indicate the intercept, and the class, in turn, will define how this intercept will be implemented, which, in our case, will be a counter.

The following and last example is to create two methods that we will count, with two implementations, one of which will have a delay of two seconds.

Java
 
public class FastSupplier implements Supplier<String> {

    @Timed
    @Override
    public String get() {
        return "The Fast supplier result";
    }
}

public class SlowSupplier implements Supplier<String> {

    @Timed
    @Override
    public String get() {
        try {
            TimeUnit.MILLISECONDS.sleep(200L);
        } catch (InterruptedException e) {
            //TODO it is only a sample, don't do it on production :)
            throw  new RuntimeException(e);
        }
        return "The slow result";
    }
}


Supplier<String> fastSupplier = container.select(FastSupplier.class).get();
Supplier<String> slowSupplier = container.select(SlowSupplier.class).get();
System.out.println("The result: " + fastSupplier.get());
System.out.println("The result: " + slowSupplier.get());


Dependency Injection With Jakarta CDI: More Options for Your Development

Given these practical examples, we can see the various possibilities and challenges possible when working with Object Orientation and better understand some of its principles around dependency injection and CDI.

It's important to remember that, as Uncle Ben once said, "with great power comes great responsibility." As such, common sense remains the best compass for exploring these and other CDI features, resulting in a high-quality design and clarity.

References

  • Dependency Injection or Inversion?

  • Difference between dependency injection and dependency inversion

  • Dependency Injection Is NOT The Same As The Dependency Inversion Principle

CDI Dependency injection

Opinions expressed by DZone contributors are their own.

Related

  • Understanding Jakarta EE 8 - CDI Part 1
  • Why I Started Using Dependency Injection in Python
  • Introducing SmallRye LLM: Injecting Langchain4J AI Services
  • Injecting Implementations With Jakarta CDI Using Polymorphism

Partner Resources

×

Comments
Oops! Something Went Wrong

The likes didn't load as expected. Please refresh the page and try again.

ABOUT US

  • About DZone
  • Support and feedback
  • Community research
  • Sitemap

ADVERTISE

  • Advertise with DZone

CONTRIBUTE ON DZONE

  • Article Submission Guidelines
  • Become a Contributor
  • Core Program
  • Visit the Writers' Zone

LEGAL

  • Terms of Service
  • Privacy Policy

CONTACT US

  • 3343 Perimeter Hill Drive
  • Suite 100
  • Nashville, TN 37211
  • support@dzone.com

Let's be friends:

Likes
There are no likes...yet! 👀
Be the first to like this post!
It looks like you're not logged in.
Sign in to see who liked this post!