Skip to main content

Combining generation and custom code

Tip submitted by @tcharl

Goal: Jhipster is very good for managing your model entities, thanks to its powerful Domain Specific language. But getting the best both custom code and generative world is always a hard task. Here are the different pattern you can adopt in order to make it real.

Pattern 1 - Generate once

This approach is the simplest one, and used in most of the use case. It consists in modeling your entities once, generate the first model, then override what you want after this first shot. If one day you want to resync, you can always regen in another branch, the compare both codes through your IDE. However, that later process is always painful and you can spend days for a major upgrade.

Pro

  • Do what you want

Con

  • You'll tend do not benefit from JHipster new features

Pattern 2 - Split generated code and custom code

With this one, you'll try to avoid modifying generated class and to host your custom code in dedicated ones. Here, you can use the --with-generated-flag jhipster cli option in order to easily differentiate the generated classes from your custom ones. Finally, you'll only modify the main router on frontend part in order to route to your custom home page instead of the generated one.

In order to avoid your router file being overridden at each generation, you can create a .yo-resolve file at the root of your project and tell to yeoman the expected behavior.

Example:

src/main/resources/swagger/api.yml skip
src/main/webapp/app/modules/home/home.tsx skip

Pro

  • Can combine generation and custom code without so much hassle

Con

  • Dead code
  • Custom classes that have different names or package than your model (can be considered as a DDD best practice but still).

Pattern 3 - Side by side

Here, the goal is to use classes extensions and beans precedence in order to inject your custom code instead of the generated one.

Let's take an example with a Customer jhipster entity.

Repository

At the repository level, you'll annotate jhipster generated repository using NoRepositoryBean annotation in order to disable discovery. You can then create your custom repository class

@Repository
@Primary
MemberRepositoryPrimary extends MemberRepository

Service

Here, you'll use the serviceImpl option in order to be able to inject your custom Bean in your Controller. Then, you can simply extends the generated service and annotate your bean with @Primary in order to get precedence.

Controller

You'll use another API prefix for your custom endpoints (for example /api/v2).

Angular

Same extension applies on the frontend side, you can then configure your beans precedence in the app.module.ts file:

providers: [
// keep other entries
{ provide: MemberDomainService, useExisting: MemberDomainServicePrimary },
]

Pro

  • Can override generated code behavior
  • Easy to find custom code
  • Keep the Jhipster best layout even for custom code

Con

  • File duplication

Alternative Side-by-Side Approach

This approach aims to add functionality to an existing JHipster application with minimal changes to the generated code.

Repository Tier

Create your repository interface with all custom methods in a separate custom package. This approach ensures that the generated repository code remains untouched, while your custom logic is organized and maintainable.

Service Tier

There are three main approaches for adding custom code at this tier:

Aspect-Oriented Programming (AOP):

AOP allows you to introduce cross-cutting concerns such as logging, security, or transaction management without altering the core business logic. This separation of concerns helps maintain cleaner and more modular code.

Event Listeners:

If the new functionality is triggered by specific events in the application (e.g., after saving a Foo entity), you can use Spring's event listener mechanism. This keeps the core logic clean, though it may make the execution flow harder to trace.

Decorator Pattern:

To add new methods or modify existing behavior in a controlled manner, the decorator pattern is highly recommended. This pattern involves wrapping the original class with a decorator that provides additional functionality. It is clean, testable, and requires minimal changes to the original client classes, ensuring better maintainability.

Step 1: Create or extend the existing Interface depending on how your service classes are generated

interface IFooService {
void newFeature();
//Existing method declarations
...
}

Step 2: Extension through Composition

@Service
@Qualifier("entended")
public class ExtendedFooService implements FooService{
private final FooService existingFooService;
private final ExtendedFooRepository extendedFooRepository; // For new repository functionality
...
}

Step 3: Add the @Primary to the existing service class and mark the existing service class as an interface implementation in step 1.

Step 4: Change the attribute type of the original service client to the interface created in step 1.

You can omit step 1 and adjust related changes in the following steps for a simplified implementation of this approach if you are unlikely to have multiple implementations of a service method or don't expect frequent changes in a service method.

Web Tier

Create New Endpoint Controllers.

@RestController
@RequestMapping("/api/extended/foo")
public class ExtendedFooController {
private final ExtendedFooService extendedFooService;

public ExtendedFooController(ExtendedFooService extendedFooService) {
this.extendedFooService = extendedFooService;
}

// New endpoints
@GetMapping("/new-feature")
public ResponseEntity<?> newFeature() {
// Implementation
}
}