Introducing Objectos PetClinic v001! (Part 2)

Marcio EndoMarcio EndoNov 24, 2024

In the first post of this series, we introduced Objectos PetClinic v001, a web application that showcases:

  • Objectos Way, our library for writing web applications in pure Java; and

  • Objectos MK, a collection of Makefiles that allows you to build Java projects using Make.

In the second and final post of this series we will show:

  • that the project is built using Make;

  • the Objectos Way support for "no-restarts" web application development;

  • the "no-magic" SQL transaction management;

  • the support for testing web pages; and

  • tha the project is fully modular.

Let's continue.

Build it with Make

Is it possible to build a Java project with Make?

Yes, it is possible! The Objectos PetClinic application is built with Make.

Here's a video of a full build run:

By full build I mean:

  • bootstrap Maven Resolver from Maven central;

  • resolve and download the compile dependencies;

  • compile the main sources of the project;

  • resolve and download the test dependencies;

  • compile the test sources of the project; and

  • run the tests of the project.

It is quite fast.

You should try to run the Objectos PetClinic application locally on your machine. The project README has instructions on how to do it.

Support for development mode

In Part 1 we saw that we write the application UI using only Java. You may ask: do I need to wait for a full application restart in order to see a smalll change in my UI?

The answer is no. Objectos Way provides a facility for reloading class definitions at runtime. There's no magic though, in an Objectos Way application we prefer to be explicit.

Here's how we do it.

First, we create a NIO WatchService instance:

// WatchServiceFileSystem fileSystem;fileSystem = FileSystems.getDefault();WatchService watchService;try {  watchService = fileSystem.newWatchService();} catch (IOException e) {  throw App.serviceFailed("WatchService", e);}shutdownHook.register(watchService);

It will be responsible for watching for file system changes.

Next, we create an instance of the Objectos Way App.Reloader interface:

// App.ReloaderApp.Reloader reloader;try {  reloader = App.Reloader.create(config -> {    config.binaryName("objectos.petclinic.site.SiteModule");    config.watchService(watchService);    config.noteSink(injector.noteSink());    config.directory(classOutputOption.get());  });  shutdownHook.register(reloader);} catch (IOException e) {  throw App.serviceFailed("ClassReloader", e);}

Let's break it down:

  • using the watch service created earlier we watch our application class output directory;

  • when the IDE recompiles a class file our reloader is notified;

  • the reloader will reload the objectos.petclinic.site.SiteModule class; and

  • in the next HTTP request from the browser, the server will handle the request with the reloaded class.

Finally, Objectos MK provides a Makefile for starting Objectos Way applications in dev mode. We use it like so:

#
# petclinic@dev
#

## dev main class
DEV_MAIN := objectos.petclinic.StartDev

## dev jvm opts
DEV_JVM_OPTS := -Xmx64m
DEV_JVM_OPTS += -XX:+UseSerialGC
ifeq ($(ENABLE_DEBUG),1)
DEV_JVM_OPTS += -agentlib:jdwp=transport=dt_socket,server=y,suspend=y,address=localhost:7000
endif

## dev --add-exports
DEV_ADD_EXPORTS := objectos.petclinic/objectos.petclinic.site=ALL-UNNAMED

## dev app args
DEV_APP_ARGS := --class-output $(CLASS_OUTPUT)

include make/java-dev.mk

In practice it means the following:

  • start the application in dev mode with make dev;

  • edit a Java class, e.g., change the home page background color;

  • save the file and have your IDE recompile it; and

  • see the changes in your browser after you refresh the page.

In other words, no application restarts during development.

SQL transaction management

Web applications require proper SQL transaction management. Once again, in an Objectos Way application we prefer to be explicit. On the other hand, writing transaction management code at every page would get old very quickly. Here's how we do it in the PetClinic application.

We use a HTTP interceptor at every page requiring database access. Here's how we declare it for the /owners page:

route("/owners",    interceptor(injector::transactional),    handlerFactory(Owners::new));

We are declaring the /owners route in the code above. Additionally, we are declaring that the HTTP requests to this route should be:

  • intercepted by the injector::transaction interceptor; and

  • handled by the handler provided by the Onwers::new supplier.

Let's focus on the interceptor. Here's its implementation:

public final Http.Handler transactional(Http.Handler handler) {  return http -> {    Sql.Transaction trx;    trx = db.beginTransaction(Sql.SERIALIZABLE);    try {      http.set(Sql.Transaction.class, trx);      handler.handle(http);      trx.commit();    } catch (Throwable t) {      throw trx.rollbackAndWrap(t);    } finally {      trx.close();    }  };}

So, before we delegate the request to the Owners handler, we:

  • begin a database transaction with the serializable isolation level; and

  • store the object representing the active database session in the HTTP request.

If the Owners handler completes normally then we commit the transaction. On the other hand, if an error occurs, we rollback the transaction.

All of the code is visible and there's no magic. In other words, you can easily:

  • adapt the code to your needs; and

  • set a breakpoint if you need to debug it.

Testing the application

You should write tests. In order to write tests, code should be testable. Web pages included. Objectos Way allows you to do just that.

Here's how we do it in the Objectos PetClinic application.

First, in an Objectos Way application you're not restricted by an IOC container. It is straightforward to setup a database connection to be used exclusively when running tests:

JdbcConnectionPool dataSource;try {  dataSource = PetClinicH2.createSchema();} catch (SQLException e) {  throw new RuntimeException(e);}shutdownHook.register(dataSource::dispose);Sql.Database database;database = Sql.Database.create(config -> config.dataSource(dataSource));

We have setup an H2 instance with our schema only.

Second, as we have seen in the previous section, Objectos Way allows you to do transaction management. So we can have our test begin a transaction and perform a rollback after it is done:

protected Sql.Transaction trx;@BeforeClasspublic void setUp() {  trx = Testing.beginTransaction(Sql.SERIALIZABLE);  trx.sql(testData());  trx.scriptUpdate();}@AfterClass(alwaysRun = true)public void cleanUp() {  if (trx != null) {    Sql.rollbackAndClose(trx);  }}protected abstract String testData();

So a test may safely insert, update or delete data.

Third, given the two previous items, it is possible to use data specific to each test class:

@Overrideprotected final String testData() {  return """  INSERT INTO owners (id, first_name, last_name, address, city, telephone)  VALUES (11, 'OW11', 'DDD', 'Add 11', 'City 11', '11')  ,      (12, 'OW12', 'AAA', 'Add 12', 'City 12', '12')  ,      (13, 'OW13', 'DDD', 'Add 13', 'City 13', '13')  ,      (14, 'OW14', 'BBB', 'Add 14', 'City 14', '14')  INSERT INTO types (id, name)  VALUES (91, 'Type 1')  ,      (92, 'Type 2')  ,      (93, 'Type 3')  INSERT INTO pets (id, owner_id, type_id, name, birth_date)  VALUES (11091, 11, 91, 'LLL', '2010-09-07')  ,      (12092, 12, 92, 'BBB', '2012-08-06')  ,      (12192, 12, 92, 'CCC', '2012-08-06')  ,      (12292, 12, 92, 'BBB', '2012-08-06')  ,      (13093, 13, 93, 'TTT', '2011-04-17')  """;}

Which is a great use for Java text blocks.

Fourth, and finally, the Html.Template class provides the testable facility:

tr(    className("td:text-start"),        td(        testable("owner.name", owner.name)    ),        td(        testable("owner.address", owner.address)    ),    td(        testable("owner.city", owner.city)    ),    td(        testable("owner.telephone", owner.telephone)    ),    td(        testable("owner.pets", owner.pets)    ));

When used to generate an HTML output it behaves like a regular HTML text node.

When used to produce a testable text output, on the other hand, it produces a string representation suitable for tests:

assertEquals(    writeResponseBody(http, "testCase01"),    """    owner.name: OW12 AAA    owner.address: Add 12    owner.city: City 12    owner.telephone: 12    owner.pets: BBB, BBB, CCC    owner.name: OW14 BBB    owner.address: Add 14    owner.city: City 14    owner.telephone: 14    owner.pets:    owner.name: OW11 DDD    owner.address: Add 11    owner.city: City 11    owner.telephone: 11    owner.pets: LLL    owner.name: OW13 DDD    owner.address: Add 13    owner.city: City 13    owner.telephone: 13    owner.pets: TTT    """);

So we can verify if:

  • the SQL query produces the correct listing; and

  • the data table is rendered with the correct data.

And how do we check the HTML layout? The writeResponseBody method writes a file named /tmp/OwnersTest.testCase01.html. You can open it in your browser and check the rendering as if you were running the HTTP server.

A fully modular application

The Objectos PetClinic application is fully modular. By fully modular we mean it is contains a module-info.java file:

module objectos.petclinic {  exports objectos.petclinic;  requires objectos.way;  requires com.h2database;}

As a result, it will be possible to deploy it:

  • as a Java runtime image create by the jlink tool; or

  • as a native binary created by the GraalVM native-image tool.

Not all Java libraries are fully modular, i.e., not all provide a compiled module-info.class. The H2 database engine used by this demo application is one example.

The Objectos MK project provides the java-module-info.mk Makefile. It is used to make an existing library modular. Here's how the Objectos PetClinic build use it:

#
# petclinic@h2
#
# creates alternate h2.jar containing module-info.class
#

include make/java-module-info.mk

com.h2database_MULTI_RELEASE := 21
com.h2database_IGNORE_MISSING_DEPS := 1
$(eval $(call module-info,com.h2database,$(H2_LOCAL),$(H2_SRC),$(SLF4J_API)))

The included Makefile provides targets that:

  • generate a module-info.java for the H2 library using the jdeps tool;

  • compile the generated module-info.java; and

  • package an alternate JAR file for the H2 library with the compiled module-info.class file.

So the Objectos PetClinic application and all of its dependencies have module-info.class files.

Conclusion

Objectos PetClinic v001 is the first release in a planned series of releases. The objective is to give Java developers an idea of what is like to develop web applications using:

  • Objectos Way; and

  • Objectos MK.

In part 2 of this series we have discussed:

  • it is possible to build a Java project with Make;

  • building a Java project with Make is fast;

  • Objectos Way allows for a "no-restarts" development cycle;

  • a "no-magic" SQL transaction management;

  • Objectos Way allows for testable code, including web pages; and

  • Objectos Way allows for fully modular applications.

Be sure to check out the project on GitHub.