Thursday, September 3, 2026

AI Series - Learn How to Build Java Agentic Apps with LangGraph4j and Jupyter Notebooks

A Java Multi-Turn, Multi-Agent Conversational System 

Welcome to the tutorial for building java agentic apps powered by LangChain4j and LangGraph4j. 

We will build a conversational application with two specialized agents: a career advisor and an education advisor. The graph keeps the conversation history in an in-memory checkpoint and hands the conversation between advisors when the user's intent changes.

You will learn how to:

  • Define LangChain4j tools in Java
  • Wrap specialized agents in LangGraph4j nodes
  • Pause between turns while retaining conversation state
  • Route a follow-up turn to another agent using the same thread ID

Prerequisites

Install the following before running the notebook:

  • Java 11 or newer. Check with java -version.
  • Python and JupyterLab or Jupyter Notebook. On macOS, the Jupyter documentation's Homebrew recipe is brew install jupyter. On Windows, install Python from python.org and then run pip install jupyterlab (or pip install notebook).
  • The JJava kernel. Download jjava-${version}-kernelspec.zip from the JJava GitHub releases, unzip it, and install the kernel from the directory containing the unzipped folder:
jupyter kernelspec install jjava-${version}-kernelspec --user --name=java
  • Maven 3.9 or newer, available as mvn on PATH. The dependency cell uses it to download LangGraph4j, LangChain4j, and their transitive dependencies.
  • Network access to Maven Central for the first dependency download.
  • An OPENAI_API_KEY environment variable. The key is read by Java and is never stored in this notebook.

Verify the installation with jupyter kernelspec list, then start Jupyter with jupyter lab or jupyter notebook and select the Java (jjava) kernel. If another Java kernel is already installed under the same name, remove it first with jupyter kernelspec remove java.

These requirements follow the JJava prerequisites.

Step 1: Prepare the Java dependencies

This notebook uses the Java (jjava) kernel and the LangGraph4j/LangChain4j libraries. The tutorial implementation itself is embedded below; only the third-party JARs need to be placed on the kernel classpath. Run the next Java cell once from this notebook. It writes a standalone Maven POM and downloads the dependencies without relying on the repository's Java source tree.

mkdir -p java-notebook-dependencies
cat > java-notebook-dependencies/pom.xml <<'EOF'
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0">
  <modelVersion>4.0.0</modelVersion>
  <groupId>local.notebook</groupId>
  <artifactId>langgraph4j-notebook-dependencies</artifactId>
  <version>1.0.0</version>
  <dependencyManagement>
    <dependencies>
      <dependency>
        <groupId>dev.langchain4j</groupId>
        <artifactId>langchain4j-bom</artifactId>
        <version>1.19.0</version>
        <type>pom</type>
        <scope>import</scope>
      </dependency>
    </dependencies>
  </dependencyManagement>
  <dependencies>
    <dependency>
      <groupId>org.bsc.langgraph4j</groupId>
      <artifactId>langgraph4j-agent-executor</artifactId>
      <version>1.8.26</version>
    </dependency>
    <dependency>
      <groupId>org.bsc.langgraph4j</groupId>
      <artifactId>langgraph4j-langchain4j</artifactId>
      <version>1.8.26</version>
    </dependency>
    <dependency>
      <groupId>dev.langchain4j</groupId>
      <artifactId>langchain4j-open-ai</artifactId>
    </dependency>
  </dependencies>
</project>
EOF
mvn -f java-notebook-dependencies/pom.xml dependency:copy-dependencies -DincludeScope=runtime -DoutputDirectory=lib

The standalone POM downloads the direct and transitive dependencies from Maven Central into java-notebook-dependencies/lib. The optional shell commands above are equivalent to the runnable Java cell. The following classpath cell adds those JARs to jjava. No application source files are imported by this notebook.

import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;

Path dependencyDirectory = Path.of("java-notebook-dependencies");
Path pomFile = dependencyDirectory.resolve("pom.xml");
try {
    Files.createDirectories(dependencyDirectory);

String pom = """
        <?xml version="1.0" encoding="UTF-8"?>
        <project xmlns="http://maven.apache.org/POM/4.0.0">
          <modelVersion>4.0.0</modelVersion>
          <groupId>local.notebook</groupId>
          <artifactId>langgraph4j-notebook-dependencies</artifactId>
          <version>1.0.0</version>
          <dependencyManagement>
            <dependencies>
              <dependency>
                <groupId>dev.langchain4j</groupId>
                <artifactId>langchain4j-bom</artifactId>
                <version>1.19.0</version>
                <type>pom</type>
                <scope>import</scope>
              </dependency>
            </dependencies>
          </dependencyManagement>
          <dependencies>
            <dependency>
              <groupId>org.bsc.langgraph4j</groupId>
              <artifactId>langgraph4j-agent-executor</artifactId>
              <version>1.8.26</version>
            </dependency>
            <dependency>
              <groupId>org.bsc.langgraph4j</groupId>
              <artifactId>langgraph4j-langchain4j</artifactId>
              <version>1.8.26</version>
            </dependency>
            <dependency>
              <groupId>dev.langchain4j</groupId>
              <artifactId>langchain4j-open-ai</artifactId>
            </dependency>
          </dependencies>
        </project>
        """;
    Files.writeString(pomFile, pom);

    Process maven = new ProcessBuilder(
            "mvn", "-f", pomFile.toString(), "dependency:copy-dependencies",
            "-DincludeScope=runtime", "-DoutputDirectory=lib")
            .inheritIO()
            .start();
    int exitCode = maven.waitFor();
    if (exitCode != 0) {
        throw new IllegalStateException("Maven dependency download failed with exit code " + exitCode);
    }
    System.out.println("Dependencies are ready in " + dependencyDirectory.resolve("lib").toAbsolutePath());
} catch (IOException | InterruptedException exception) {
    if (exception instanceof InterruptedException) {
        Thread.currentThread().interrupt();
    }
    throw new IllegalStateException("Could not download Maven dependencies", exception);
}
Dependencies are ready in java-notebook-dependencies/lib
%classpath java-notebook-dependencies/lib/*

Step 2: Configure the model

Set OPENAI_API_KEY in the environment before starting Jupyter. Do not paste an API key into a notebook cell. The model and endpoint can also be overridden for an OpenAI-compatible provider such as DeepInfra.

import dev.langchain4j.agent.tool.P;
import dev.langchain4j.agent.tool.ReturnBehavior;
import dev.langchain4j.agent.tool.Tool;
import dev.langchain4j.data.message.AiMessage;
import dev.langchain4j.data.message.ChatMessage;
import dev.langchain4j.data.message.SystemMessage;
import dev.langchain4j.data.message.ToolExecutionResultMessage;
import dev.langchain4j.data.message.UserMessage;
import dev.langchain4j.model.chat.ChatModel;
import dev.langchain4j.model.openai.OpenAiChatModel;
import org.bsc.langgraph4j.CompiledGraph;
import org.bsc.langgraph4j.GraphDefinition;
import org.bsc.langgraph4j.GraphInput;
import org.bsc.langgraph4j.GraphStateException;
import org.bsc.langgraph4j.RunnableConfig;
import org.bsc.langgraph4j.StateGraph;
import org.bsc.langgraph4j.agentexecutor.AgentExecutor;
import org.bsc.langgraph4j.action.AsyncNodeAction;
import org.bsc.langgraph4j.checkpoint.MemorySaver;
import org.bsc.langgraph4j.langchain4j.serializer.std.LC4jStateSerializer;
import org.bsc.langgraph4j.prebuilt.MessagesState;
import org.bsc.langgraph4j.prebuilt.MessagesStateGraph;
import java.util.Objects;
import java.util.concurrent.CompletableFuture;
import java.util.List;
import java.util.Map;

String apiKey = System.getenv("OPENAI_API_KEY");
if (apiKey == null || apiKey.isBlank()) {
    throw new IllegalStateException("Set OPENAI_API_KEY before running the model cells.");
}

String modelName = System.getenv().getOrDefault("OPENAI_MODEL", "gpt-4o-mini");
String baseUrl = System.getenv().getOrDefault("OPENAI_BASE_URL", "https://api.openai.com/v1"); System.out.println("Using model: " + modelName); System.out.println("Using endpoint: " + baseUrl);
Using model: gpt-4o-mini
Using endpoint: https://api.openai.com/v1

Step 3: Define the agent tools

The Java implementation expresses tools with LangChain4j's @Tool and @P annotations. All tool classes are defined in the next cell, so the notebook does not depend on application source files. The advisor-specific wrappers expose only the tools that belong to each agent: getCareerPaths, getLearningResources, transfer_to_education_advisor, and transfer_to_career_advisor.

interface Advisor {
    List<ChatMessage> respond(List<ChatMessage> messages);
}

class CareerEducationTools {
    private static final List<String> CAREER_PATHS =
            List.of("data science", "product management", "cybersecurity");
    private static final Map<String, List<String>> LEARNING_RESOURCES = Map.of(
            "data science", List.of("Coursera: IBM Data Science", "edX: Harvard's Data Science Series"),
            "product management", List.of("Udemy: Become a Product Manager", "Reforge Programs"),
            "cybersecurity", List.of("Cybrary", "CompTIA Security+ Certification"));

    @Tool("Suggest career options based on general user interest.")
    public String getCareerPaths() {
        return CAREER_PATHS.get(java.util.concurrent.ThreadLocalRandom.current()
                .nextInt(CAREER_PATHS.size()));
    }

    @Tool("Provide online resources or certifications for a given career path.")
    public List<String> getLearningResources(
            @P("One of: data science, product management, cybersecurity") String career) {
        List<String> resources = LEARNING_RESOURCES.get(career.toLowerCase());
        if (resources == null) {
            throw new IllegalArgumentException("Unsupported career path: " + career);
        }
        return resources;
    }

    @Tool(value = "Ask the education advisor agent for help.",
            returnBehavior = ReturnBehavior.IMMEDIATE)
    public String transferToEducationAdvisor() {
        return "Successfully transferred to education advisor.";
    }

    @Tool(value = "Ask the career advisor agent for help.",
            returnBehavior = ReturnBehavior.IMMEDIATE)
    public String transferToCareerAdvisor() {
        return "Successfully transferred to career advisor.";
    }
}

class CareerAdvisorTools {
    private final CareerEducationTools tools;

    CareerAdvisorTools(CareerEducationTools tools) {
        this.tools = tools;
    }

    @Tool("Suggest career options based on general user interest.")
    public String getCareerPaths() {
        return tools.getCareerPaths();
    }

    @Tool(name = "transfer_to_education_advisor",
            value = "Ask the education advisor agent for help.",
            returnBehavior = ReturnBehavior.IMMEDIATE)
    public String transferToEducationAdvisor() {
        return tools.transferToEducationAdvisor();
    }
}

class EducationAdvisorTools {
    private final CareerEducationTools tools;

    EducationAdvisorTools(CareerEducationTools tools) {
        this.tools = tools;
    }

    @Tool("Provide online resources or certifications for a given career path.")
    public List<String> getLearningResources(
            @P("One of: data science, product management, cybersecurity") String career) {
        return tools.getLearningResources(career);
    }

    @Tool(name = "transfer_to_career_advisor",
            value = "Ask the career advisor agent for help.",
            returnBehavior = ReturnBehavior.IMMEDIATE)
    public String transferToCareerAdvisor() {
        return tools.transferToCareerAdvisor();
    }
}

CareerEducationTools toolSet = new CareerEducationTools();
CareerAdvisorTools careerTools = new CareerAdvisorTools(toolSet);
EducationAdvisorTools educationTools = new EducationAdvisorTools(toolSet);

System.out.println("Example career: " + toolSet.getCareerPaths());
System.out.println("Data science resources: " + toolSet.getLearningResources("data science"));
Example career: data science
Data science resources: [Coursera: IBM Data Science, edX: Harvard's Data Science Series]

Step 4: Create the specialized agents

Each agent uses the same chat model but has a different system prompt and tool set. AgentExecutor provides the ReAct-style tool-calling loop, while the embedded graph controller will decide which advisor receives the next turn.

ChatModel model = OpenAiChatModel.builder()
        .apiKey(apiKey)
        .modelName(modelName)
        .baseUrl(baseUrl)
        .temperature(0.0)
        .maxRetries(2)
        .build();

final CompiledGraph<AgentExecutor.State> careerAgent;
final CompiledGraph<AgentExecutor.State> educationAgent;
try {
    careerAgent = AgentExecutor.builder()
            .chatModel(model)
            .systemMessage(SystemMessage.from(
                    "You are a career expert. Help users explore career options. "
                            + "If they ask about courses or education, transfer to the education advisor. "
                            + "Always explain your reasoning before transferring."))
            .toolsFromObject(careerTools)
            .build()
            .compile();

    educationAgent = AgentExecutor.builder()
            .chatModel(model)
            .systemMessage(SystemMessage.from(
                    "You are an education expert. Recommend learning paths for specific careers. "
                            + "If the user changes their career preference, transfer back to the career advisor. "
                            + "Always explain your reasoning before transferring."))
            .toolsFromObject(educationTools)
            .build()
            .compile();
} catch (GraphStateException exception) {
    throw new IllegalStateException("Could not compile the advisor agents", exception);
}

Step 5: Wrap the agents in LangGraph4j advisors

The embedded Advisor functional interface accepts the complete ChatMessage history and returns the messages generated by an agent. This is the Java equivalent of the Python @task functions.

Advisor careerAdvisor = messages -> careerAgent.invoke(Map.of("messages", messages))
        .orElseThrow(() -> new IllegalStateException("Career advisor produced no state"))
        .messages();

Advisor educationAdvisor = messages -> educationAgent.invoke(Map.of("messages", messages))
        .orElseThrow(() -> new IllegalStateException("Education advisor produced no state"))
        .messages();

Step 6: Create the multi-turn controller

The embedded CareerEducationGraph is the LangGraph4j controller. It starts at the career advisor, stores messages in a MemorySaver, interrupts after each answer, and routes the next turn to the education advisor when the user asks about courses, learning, or resources. A request using the same thread ID resumes the checkpointed conversation.

class CareerEducationGraph {
    static final String CAREER_ADVISOR = "career_advisor";
    static final String EDUCATION_ADVISOR = "education_advisor";
    private static final String CAREER_WAIT = "career_wait";
    private static final String EDUCATION_WAIT = "education_wait";

    private final CompiledGraph<MessagesState<ChatMessage>> graph;

    CareerEducationGraph(Advisor careerAdvisor, Advisor educationAdvisor) {
        Objects.requireNonNull(careerAdvisor, "careerAdvisor");
        Objects.requireNonNull(educationAdvisor, "educationAdvisor");
        try {
            var serializer = new LC4jStateSerializer<MessagesState<ChatMessage>>(MessagesState::new);
            StateGraph<MessagesState<ChatMessage>> workflow = new MessagesStateGraph<>(serializer);
            workflow.addNode(CAREER_ADVISOR,
                    AsyncNodeAction.node_async(state -> invoke(careerAdvisor, state)));
            workflow.addNode(EDUCATION_ADVISOR,
                    AsyncNodeAction.node_async(state -> invoke(educationAdvisor, state)));
            workflow.addNode(CAREER_WAIT, AsyncNodeAction.node_async(state -> Map.of()));
            workflow.addNode(EDUCATION_WAIT, AsyncNodeAction.node_async(state -> Map.of()));
            workflow.addEdge(GraphDefinition.START, CAREER_ADVISOR);
            workflow.addEdge(CAREER_ADVISOR, CAREER_WAIT);
            workflow.addEdge(EDUCATION_ADVISOR, EDUCATION_WAIT);
            workflow.addConditionalEdges(CAREER_WAIT,
                    state -> CompletableFuture.completedFuture(nextAdvisor(state, CAREER_ADVISOR)),
                    Map.of(CAREER_ADVISOR, CAREER_ADVISOR, EDUCATION_ADVISOR, EDUCATION_ADVISOR));
            workflow.addConditionalEdges(EDUCATION_WAIT,
                    state -> CompletableFuture.completedFuture(nextAdvisor(state, EDUCATION_ADVISOR)),
                    Map.of(CAREER_ADVISOR, CAREER_ADVISOR, EDUCATION_ADVISOR, EDUCATION_ADVISOR));
            graph = workflow.compile(org.bsc.langgraph4j.CompileConfig.builder()
                    .checkpointSaver(new MemorySaver())
                    .interruptAfter(CAREER_WAIT, EDUCATION_WAIT)
                    .interruptBeforeEdge(true)
                    .releaseThread(false)
                    .build());
        } catch (GraphStateException exception) {
            throw new IllegalStateException("Could not compile the career conversation graph", exception);
        }
    }

    ConversationTurn turn(String threadId, String userInput) {
        if (threadId == null || threadId.isBlank()) {
            throw new IllegalArgumentException("threadId must not be blank");
        }
        if (userInput == null || userInput.isBlank()) {
            throw new IllegalArgumentException("userInput must not be blank");
        }
        var config = RunnableConfig.builder().threadId(threadId).build();
        Map<String, Object> input = Map.of(MessagesState.MESSAGES_STATE,
                List.of(UserMessage.from(userInput)));
        GraphInput graphInput = graph.stateOf(config).isPresent()
                ? GraphInput.resume(input) : GraphInput.args(input);
        var outputs = graph.stream(graphInput, config).stream().toList();
        if (outputs.isEmpty()) {
            throw new IllegalStateException("The conversation graph produced no output");
        }
        var output = outputs.get(outputs.size() - 1);
        String response = output.state().messages().stream()
                .filter(AiMessage.class::isInstance).map(AiMessage.class::cast)
                .reduce((first, second) -> second).map(AiMessage::text).orElse("");
        String advisor = CAREER_WAIT.equals(output.node()) ? CAREER_ADVISOR
                : EDUCATION_WAIT.equals(output.node()) ? EDUCATION_ADVISOR : output.node();
        return new ConversationTurn(threadId, advisor, response, true);
    }

    private static Map<String, Object> invoke(Advisor advisor, MessagesState<ChatMessage> state) {
        int previousSize = state.messages().size();
        List<ChatMessage> generated = advisor.respond(List.copyOf(state.messages()));
        if (generated == null || generated.isEmpty()) {
            throw new IllegalStateException("Advisor produced no messages");
        }
        List<ChatMessage> newMessages = generated.size() >= previousSize
                && generated.subList(0, previousSize).equals(state.messages())
                ? generated.subList(previousSize, generated.size()) : generated;
        return Map.of(MessagesState.MESSAGES_STATE, List.copyOf(newMessages));
    }

    private static String nextAdvisor(MessagesState<ChatMessage> state, String currentAdvisor) {
        for (int index = state.messages().size() - 1; index >= 0; index--) {
            var message = state.messages().get(index);
            if (message instanceof ToolExecutionResultMessage toolResult) {
                if ("transfer_to_education_advisor".equals(toolResult.toolName())) {
                    return EDUCATION_ADVISOR;
                }
                if ("transfer_to_career_advisor".equals(toolResult.toolName())) {
                    return CAREER_ADVISOR;
                }
            }
        }
        String latestUserText = state.messages().stream()
                .filter(UserMessage.class::isInstance).map(UserMessage.class::cast)
                .reduce((first, second) -> second).map(UserMessage::singleText)
                .orElse("").toLowerCase();
        if (CAREER_ADVISOR.equals(currentAdvisor)
                && containsAny(latestUserText, "course", "education", "learn", "resource")) {
            return EDUCATION_ADVISOR;
        }
        if (EDUCATION_ADVISOR.equals(currentAdvisor)
                && containsAny(latestUserText, "career", "change", "different path")) {
            return CAREER_ADVISOR;
        }
        return currentAdvisor;
    }

    private static boolean containsAny(String text, String... terms) {
        for (String term : terms) {
            if (text.contains(term)) return true;
        }
        return false;
    }

    record ConversationTurn(String threadId, String advisor, String response,
                           boolean waitingForUser) {}
}

CareerEducationGraph conversation = new CareerEducationGraph(careerAdvisor, educationAdvisor);
String threadId = java.util.UUID.randomUUID().toString();
System.out.println("Conversation thread: " + threadId);
Conversation thread: c67b69e6-2788-48ec-897d-70ae2398ae46

Step 7: Test the multi-turn conversation

The three prompts below use one stable thread ID. The first turn starts with the career advisor; the second asks about courses and is routed to the education advisor; the third remains in the education conversation. This is the Java equivalent of resuming the Python graph with Command(resume=...).

List<String> prompts = List.of(
        "I'm interested in technology but not sure what career fits me.",
        "That sounds good. What courses should I take to get started?",
        "Awesome! Are these resources beginner friendly?");

for (int index = 0; index < prompts.size(); index++) {
    String prompt = prompts.get(index);
    var turn = conversation.turn(threadId, prompt);
    System.out.println("\n--- Conversation Turn " + (index + 1) + " ---");
    System.out.println("User: " + prompt);
    System.out.println("Advisor: " + turn.advisor());
    System.out.println("Assistant: " + turn.response());
}
--- Conversation Turn 1 ---
User: I'm interested in technology but not sure what career fits me.
Advisor: career_advisor
Assistant: Based on your interest in technology, one potential career path to consider is **Data Science**.

Data Science involves using statistical and computational methods to extract insights from structured and unstructured data. It's a broad field that can lead to roles like Data Analyst, Data Scientist, or Machine Learning Engineer. It often appeals to people who enjoy problem-solving, working with numbers, and finding patterns.

Would you like to explore other tech careers, or do you have questions about the education or courses needed to become a Data Scientist? If you're interested in the educational path, I can transfer you to an education advisor.

--- Conversation Turn 2 ---
User: That sounds good. What courses should I take to get started?
Advisor: education_advisor
Assistant: To get started in Data Science, here are two highly recommended courses:

1.  **Coursera: IBM Data Science** - This course provides a comprehensive introduction to data science, covering Python, SQL, and data visualization. It's a great starting point for beginners.
2.  **edX: Harvard's Data Science Series** - This series offers a more academic approach, starting with foundational statistics and programming in R, which is excellent for building a strong theoretical background.

Would you like more details on either of these courses, or are you interested in exploring a different career path?

--- Conversation Turn 3 ---
User: Awesome! Are these resources beginner friendly?
Advisor: education_advisor
Assistant: Yes, both resources are designed to be beginner-friendly.

*   **Coursera: IBM Data Science** is structured to take you from the basics to more advanced topics, making it accessible for those new to the field.
*   **edX: Harvard's Data Science Series** also starts with foundational concepts, ensuring that even if you have no prior experience, you can build up your skills step-by-step.

Would you like to dive deeper into either of these courses, or is there anything else I can help you with?

Notes

MemorySaver is process-local and is intended for this tutorial. For a deployed application, replace it with a persistent checkpoint saver and keep the thread ID stable across requests. The same graph is also available through the Java service endpoint documented in java/README.md.


You can find the downloadable jupyter notebook here. It should be fully executable provided you have fulfilled the prequisites.


Enjoy!

Dikran

Thursday, August 13, 2026

How to Get an Extra Hour of Battery Life When a Long-Running, Can’t-Stop Workload Eats It in Minutes

The situation

  • You are working on your laptop.

  • Your long-running (8-hour), terminal-launched, compute-intensive, iterative task, which produces data for the next iteration, is halfway through its work.

  • You need to take your laptop to a place where you will have no power source for a couple of hours.

  • At some point, your battery gets low and you receive a warning that the computer will soon go into sleep mode.

  • You should not put your computer to sleep because, apart from needing the computer for the activity you have there, sleep mode may interrupt connections and cause other undesired side effects that would disrupt the running task.

The solution?

1. Freeze the task/process

Get the process ID and run:

kill -STOP <your process id>

The task freezes, CPU usage drops, and your battery lasts much longer.


You can now keep working on other things, while your battery lasts much longer. I my case I it took more than an hour before getting back to my power adapter.

Once you return to a power source, plug in your laptop.

2. Unfreeze the process

Run:

kill -CONT <your process id>

Your task resumes unaffected. Your work is safe.


Note: If your task was running in a terminal, you likely won't be able to run the unfreeze command from that terminal, because the terminal process itself is already frozen.

You will need to find an alternative way to run the command. For instance:

On Mac: use the Shortcuts app → New Shortcut → Shortcut type "Run shell" → set the unfreeze command → Run.

On Linux: there are also various methods, such as the "Run Command" or "Quick Run" prompt (Alt+F2), available in most Linux GUI variants.


Disclaimer: This is based on my personal experience. I have not tested multiple cases, such as having multiple parent-child processes used by or connected to the frozen task. Test it with your particular case and use with caution.

Good luck!

Dikran

Monday, February 8, 2021

SLF4j Logging performance: lazy argument evaluation

Sometimes we need to log a dynamically generated expressions that are very expensive to compute. For instance I had to log an object to yaml format only when debug was enabled. Serializing an object to yaml is an expensive operation especially when you need to scale up to thousands of calls per second.

(As reference, I am using java 8 with slf4j-1.7.25)

If I had directly used

 log.debug("The message is:{}", toYamlString(myObject));

then the message generating method would be called every time even if debug as disabled on the logger. This is because of the java argument evaluation mechanism.

The obvious choice here is:

if(log.isDebugEnabled()){
  log.debug("The message is:{}", toYamlString(myObject));
} 

but, apart of adding unpleasing code on top of your method, this is also doubling the call on if(log.isDebugEnabled()) that is also performed in the logging framework itself.So I took some time to see if it could be done in a better way.

At some point I found this post that was nicely solving this. I liked it and wrote my code accordingly. Then I realised it could be even simpler!

So I simplified it to only this:

  private static Object lazyString(final Supplier<?> stringSupplier) {
    return new Object() {
      @Override
      public String toString() {
        return String.valueOf(stringSupplier.get());
    }
  }

Then in my logging call:

  log.debug("The message is:{}",lazyString(() -> toYamlString(myObject)));

or, if your method take no arguments, you can use method reference:

  log.debug("The message is:{}",lazyString(this::toYamlString));

That's it! Simple and elegant.

The good news is that more and more logging frameworks added or are adding native support for deferred evaluation of arguments.

Until then we can use simple nice workarounds like this.

Have a nice day,

Dikran.

Tuesday, April 3, 2018

Compile Maven project and tests with different compilers and with different unit and integration test directories

My project is java, and I wanted to give my team the possibility to use java/junit and groovy/spock for our tests.

Moreover I wanted to keep unit tests separated from integration tests and if possible with different compilation life cycles so that the flow is:

1. compile the project code from src/main/java using the default compiler
2. compile and run the unit tests using the mixed java-groovy eclipse compiler
3. compile and run the integration tests using the mixed java-groovy eclipse compiler

This way the production code is compiled natively while we can play with java-groovy mixed classes in unit and integration tests.

After digging a lot and trying many unsuccessful approaches I got it working exactly as I wished.
Here is the pom:

As you may notice, I have left the unit tests in the standard maven path ie. src/test/java, but, if I want to further move them to src/test/unit/java then I need to configure both the compiler section and the surefire-plugin section in the same way I did for the integration tests.

Basically the secret in in instructing the compiler on:
- when to run (we configure this aspect within an execution section)
- where to compile sources from - within the element compilesourceroots
- where to output classes
- what classes (by name or pattern) to include - in the element outputDirectory
and at the same time to instruct the test runner (surefire or failsafe) on:
- where the test sources are located - within testSourceDirectory
- when the test classes are located - within testClassesDirectory

One essential thing to notice is the id of each execution element (in our case default-testCompile and integration-testCompile, because maven identifies each instance by it's id s it must be uniquely named.

Another thing that many don't know is that the ids can be overridden and indeed I have used the default maven compiler id for the unit test compilation so that only the eclipse compiler shall be run instead of runing also the default compiler. you can change the id and test yourself.

 Hope that shall help you too!

Cheers, Dikran

Friday, October 14, 2016

How to install docker on centos 6 - quick and dirty

Installing docker 1.12 on centos 6.8

While doing consultancy work at one big telecom company, I proposed introducing Docker in the process of automating and autoscaling parts of the software infrastructure, especially on the the java development chain and runtime deployment sides. They were enthusiastic about this so I got the task of making it happen. Just that at the time I did not know their infrastructure (all Centos 6.8) did not support docker...

Who got here must be quite desperate, as I got for a while after taking this task.
Lots of research and trial got me to put together the following instructions that made it work.

This is an unpolished, quick hand log of what I had to do in order to make docker successfully run on this OS version.

WARNING: Be sure of your deep linux understanding and knowledge before trying this into production systems!

Hopefully it shall help. I'll come back to this post time allowing, to better arrange, document and cleanup.

Cheers,
Dikran

Wednesday, March 30, 2016

Updating all branches from all local git projects in one shot


There are many times when I need to update at once more than just one git project.
I usually structure my projects under a common directory like /Users/Dikran/workspace/projects.
When I update I cd to the respective project and git pull.

I justs happens that recently I needed two things:
1) check updated code of more than one project.
2) check changes made on other branches than the current one.

As you know, git pull is updating only the current branch in a project. Moreover it has the following limitations (quoting from git-up project):

"It merges upstream changes by default, when it's really more polite to rebase over them, unless your collaborators enjoy a commit graph that looks like bedhead.
It only updates the branch you're currently on, which means git push will shout at you for being behind on branches you don't particularly care about right now."


So in order to solve those needs at once, there is a simple solution enabled by a simple script and a great git extension called git-up. This is a very convenient tool that does many nice things in completion to what git already offers. Check the site for docs and info.

The steps:

1. Install git-up extension
For Ruby (the original):
gem install git-up
Or for the Python port:
pip install git-up

2. Create a script (i.e. updateAll.sh) in the root directory of your git subprojects, containing the following:
#!/bin/bash

set -x

for project in */
  do git -C $project up &
done

wait
The script cycles through all subdirectories in the current directory, and issues background calls to git-up on every discovered directory.

The wait is added at the end so that the script shall exit only after all directory updating commands finished.

For a variation you can filter directory names if for instance you want updated only specific directories within a certain project. So for instance if you have your main project called myshop and the composing modules are myshop-frontend/ myshop-backend/ myshop-tests/ then just change the
for project in */
with
for project in myshop*/
Thats't all. Simple, isn't it?

A warning note though. Although git-up suits most cases, please check the documentation first, to be sure it won't mess things in your specific project's commit conventions.

Have a nice day,
Dikran

Tuesday, December 1, 2015

Automated Testing Aid: Manually Running a Quartz Job

I work in a project where testing is a first class citizen. We do unit tests, security tests, integration tests, end-to-ends api tests (SBE), and end-to-end functional (interface based) tests also by using SBE.
All right, everything's fine, until I need to throw in some asserts at the end of my integration/sbe test where I should check if the whole process performed well. Ok, only that this part of the process is accomplished by quartz jobs that run asynchronously in their specific setup, beyond our control.

Note: The assumption for this post is that your Quartz scheduler can be run within the same application with your tested classes. For distributed quartz jobs there is another story.

Some could say, ok, by you have the possibility to get a job by it's name and call triggerJob(JobKey) on a quartz scheduler instance, that should trigger the job immediately. But, be careful is about triggering a job, and not running the job. That means that:

  • the command is asynchronous and return immediately;
  • the job could actually start later, depending on the schedule's config and
  • you don't actually know when the job shall finish so that you can test your assumptions about the outcome of it.

Two quick solutions:
  1. after test's execution finished, before asserting on data, sleep the test thread for a while to give quartz time to do it's work. But sleep for how long? Some manual tries could give us some empirical idea of how long should we wait before the jos is usually executed, but we are never going to be 100% sure it actually was. And then, this approach could bring the execution of our test suite to last forever, imagine running hundreds of tests of this type that each are sleeping for few seconds... It doesn't sound very appealing.
  2. add a JobListener listener to the scheduler, then trigger the job and then put your main thread in wait until the listener is triggered on execution finished, and notifies your main thread so it can resume it's testing task. But, again, there might be many jobs already triggered and running until ours get's it's change to run. And after all, would you really want to get into unexpected threading issues? I think not.

So, after trying the aforementioned approaches and not really being happy with them I thought, why not directly run the jobs I am directly interested in?
Well this is not that trivial, because I'd like to run the jobs as they are, without having to know what other stuff is injected in each of my classes extending QuartzJob in order to make it work. So, after some research and study of how quartz works in collaboration with spring, that is what came out:


import org.quartz.Job;
import org.quartz.JobExecutionContext;
import org.quartz.Scheduler;
import org.springframework.beans.BeanWrapper;
import org.springframework.beans.BeansException;
import org.springframework.beans.MutablePropertyValues;
import org.springframework.beans.PropertyAccessorFactory;
import org.springframework.context.ApplicationContext;
import org.springframework.context.ApplicationContextAware;

import java.lang.reflect.Method;
import java.util.Map;

public class ManualJobExecutor implements ApplicationContextAware {

    private ApplicationContext applicationContext;

    public void executeJob(final Class<Job> jobClass) {

        try {
            //create job instance
            final Job quartzJob = jobClass.newInstance();
            // For the created job instance, search all services that are injected by quartz.
            // Those service instances are kept inside each scheduler context as a map
            final BeanWrapper beanWrapper = PropertyAccessorFactory.forBeanPropertyAccess(quartzJob);
            final MutablePropertyValues propertyValues = new MutablePropertyValues();
            //get all schedulers defined across all spring configurations for this application
            final Map<String, Scheduler> schedulers = applicationContext.getBeansOfType(Scheduler.class);
            for (final Scheduler scheduler : schedulers.values()) {
                // Populate the possible properties with service instances found
                propertyValues.addPropertyValues(scheduler.getContext());
            }
            //set the properties of the job (injected dependencies) with the matching services
            //the other services in the list that have no matching properties shall be ignored 
            beanWrapper.setPropertyValues(propertyValues, true);

            //get method executeInternal(JobExecutionContext) from job class extending QuartzJobBean 
            final Method executeJobMethod = quartzJob.getClass().getDeclaredMethod("executeInternal", (JobExecutionContext.class));
            executeJobMethod.setAccessible(true);
            //call the processItems method on the Job class instance
            executeJobMethod.invoke(quartzJob);
        } catch (final Exception e) {
            throw new RuntimeException(String.format("Exception while retrieving and executing job for name=%s", jobClass.getName()), e);
        }
    }

    @Override
    public void setApplicationContext(final ApplicationContext applicationContext) throws BeansException {
        this.applicationContext = applicationContext;
    }
}

That's it!
Of course there are also other aspects, i.e checking if other job of the same class is already executing so that it won't overlap with your execution. Usually in Quarz, @DisableConcurrentExecution takes care of this but here you need to check it yourself.
You could also make your method accept a job by its name instead of class so you can get the names from your database instead of looking into project classes.

I hope this is going to ease your testing.
Please share your thoughts.


Have a nice day,
Dikran