Best way to dynamically resolve dependencies in Java Spring? - java

Let's say I have this code structure:
public class NotificationService {
public void send(Notification notification) {
// call other services and send the notification
}
}
public class OrderNotification implements Notification {
#Autowired
public TranslationService translationService;
private String orderNumber;
public OrderNotification(String orderNumber) {
this.orderNumber = orderNumber;
}
public String getMessage() {
return translationService.trans('notification.order', new Object[]{orderNumber});
}
}
So, my goal is to use the NotificationService in this way:
notificationService.send(new OrderNotification(orderNumber));
But I know that code above won't work, because of the translationService won't be resolved.
My goal is to pass custom parameters to my Notification classes and being able to use services inside that class. What is the best way to do it in the Spring?

I know that below is not the correct answer to your question. It is however a bad design pattern to combine Entities and Services. An Entity should only contain information about the object and not business logic. A Service contains all the business logic.
You need to separate your Service from your Entity.
OrderNotification looks like a regular entity. The entity should not contain business logic. You need a specific service for the business logic.
public class OrderNotification implements Notification {
private String orderNumber;
public OrderNotification(String orderNumber) {
this.orderNumber = orderNumber;
}
public String getMessage() {
return "Order number: " + orderNumber;
}
//Getter & Setters
...
}
#Service
public class NotificationService {
#Autowired
public TranslationService translationService;
public void send(Notification notification) {
//I do not know what trans accepts, so I assume it can accept Notification
translationService.trans(notification.getMessage());
}
}
If you really need to combine the entity and service - Then I recommend this approach:
#Service
public class Master{
#Autowired
NotificationService notificationService
public void testMethod(){
Notification notification = notificationService.createOrder("order1");
notificationService.send(notification);
}
}
#Service
public class NotificationService {
#Autowired
public TranslationService translationService;
public Notification createOrder(String orderNumber){
return new OrderNotification(orderNumber, translationService);
}
public void send(Notification notification) {
// call other services and send the notification
notification.getMessage();
}
}
public class OrderNotification implements Notification {
private TranslationService translationService;
private String orderNumber;
//I have changed this constructor to accept TranslationService.
public OrderNotification(String orderNumber, TranslationService translationService) {
this.orderNumber = orderNumber;
this.translationService = translationService;
}
public String getMessage() {
return translationService.trans('notification.order', new Object[]{orderNumber});
}
}

You have few options available:
Configure AOP and load time weaving to process Spring annotations on objects created with new keyword. This is explained in the docs 5.8.1. Using AspectJ to dependency inject domain objects with Spring.
Declare OrderNotification as a prototype scoped bean and obtain each instance from the context using BeanFactory.getBean(Class<T> requiredType, Object... args) method.
String orderNumber = "123";
OrderNotificaton = factory.getBean(OrderNotificaton.class, orderNumber);
Drop the #Autowired and use plain constructor injection.
public OrderNotification(TranslationService translationService, String orderNumber) {
this.translationService = Objects.requireNonNull(translationService);
this.orderNumber = Objects.requireNonNull(orderNumber);
}
If you only require simple #Autowired I'd go with option 3. It's the simplest approach and makes writing unit tests easier as you don't have to depend on Spring.

Related

How to provide converter service to POJO class without passing it as method argument

I have ShoppingList service which is responsible for generating shopping list and a IngredientConverter service which is a helping tool for converting objects. My current implementation looks like this
#Service
#AllArgsConstructor
public class ShoppingListService {
private final RecipeService recipeService;
private final IngredientConverter ingredientConverter;
public ShoppingList generateShoppingList(List<UUID> uuidsOfRecipes) {
List<Recipe> recipes = recipeService.getAllByIDIn(uuidsOfRecipes);
ShoppingList shoppingList = ShoppingList.empty();
for (Recipe recipe : recipes) {
shoppingList.addIngredients(recipe.getIngredients());
}
shoppingList.finishAddition(ingredientConverter);
return shoppingList;
}
}
#RequiredArgsConstructor
public class ShoppingList {
#Getter
private final List<IngredientQuantity> optimizedList;
private final Map<Ingredient, Integer> ingredientAmountMap;
public static ShoppingList empty() {
return new ShoppingList(new ArrayList<>(), new HashMap<>());
}
public void addIngredients(List<IngredientQuantity> ingredients) { ... }
public void addIngredient(IngredientQuantity ingredientQuantity) { ... }
public void finishAddition(IngredientConverter ingredientConverter) {
for (Ingredient ingredient : ingredientAmountMap.keySet()) {
IngredientQuantity ingredientQuantity = ingredientConverter.convertWithAmount(
ingredient.getName(),
ingredientAmountMap.get(ingredient),
ingredient.getUnit());
optimizedList.add(ingredientQuantity);
}
}
}
#Service
public class IngredientConverter {
public IngredientQuantity convertWithAmount(String name, int amount, Unit unit) { ... }
}
Is there a better strategy for providing IngredientConverter service to this class? Could I Autowire it somehow despite ShoppingList being POJO class? Should ShoppingList be marked as Component maybe? Not sure what is the best approach.
You cannot autowire service class into POJO. Autowire can be done only within spring managed classes. I can see that ShoppingList is not a spring managed class. Adding #Component will also not be ideal solution. AFAIK, The best solution to use here would be mapStruct. mapStruct can be used to map fields between entity and POJO. And in cases where any field has to be calculated separately, you can write your custom logic and autowire services. Below are steps
Add mapStruct library to pom.xml
Add below mapper class to your project. componentModel="spring" tells the system that this mapper is managed by spring.
All the fields that have same name will be automapped.
For fields which require conversions, you can write #BeforeMapping
Mapper(componentModel="spring")
public abstract class ShoppingListMapper
{
#Autowired
IngredientConverter ingredientConverter; //autowire method you use.
public abstract shoppingListToShoppingListDTO(ShoppingList shoppingList) throws Exception;
public abstract List<ShoppingList> mapShoppingListsToDTOs(List<ShoppingList> shoppingLists) throws Exception;
#BeforeMapping
public void convertLogic(ShoppingList la, #MappingTarget ShoppingListDTO slDto) throws Exception
{
//your logic to set required shoppinglist field using converter
}
}
If this example is not clear, you can refer to web for various mapstruct examples. Let me know if you need further help

Spring Boot cache not caching method call based on dynamic controller parameter

I am attempting to use Spring Boot Cache with a Caffeine cacheManager.
I have injected a service class into a controller like this:
#RestController
#RequestMapping("property")
public class PropertyController {
private final PropertyService propertyService;
#Autowired
public PropertyController(PropertyService propertyService) {
this.propertyService = propertyService;
}
#PostMapping("get")
public Property getPropertyByName(#RequestParam("name") String name) {
return propertyService.get(name);
}
}
and the PropertyService looks like this:
#CacheConfig(cacheNames = "property")
#Service
public class PropertyServiceImpl implements PropertyService {
private final PropertyRepository propertyRepository;
#Autowired
public PropertyServiceImpl(PropertyRepository propertyRepository) {
this.propertyRepository = propertyRepository;
}
#Override
public Property get(#NonNull String name, #Nullable String entity, #Nullable Long entityId) {
System.out.println("inside: " + name);
return propertyRepository.findByNameAndEntityAndEntityId(name, entity, entityId);
}
#Cacheable
#Override
public Property get(#NonNull String name) {
return get(name, null, null);
}
}
Now, when I call the RestController get endpoint and supply a value for the name, every request ends up doing inside the method that should be getting cached.
However, if I call the controller get endpoint but pass a hardcoded String into the service class method, like this:
#PostMapping("get")
public Property getPropertyByName(#RequestParam("name") String name) {
return propertyService.get("hardcoded");
}
Then the method is only invoked the first time, but not on subsequent calls.
What's going on here? Why is it not caching the method call when I supply a value dynamically?
Here is some configuration:
#Configuration
public class CacheConfiguration {
#Bean
public CacheManager cacheManager() {
val caffeineCacheManager = new CaffeineCacheManager("property", "another");
caffeineCacheManager.setCaffeine(caffeineCacheBuilder());
return caffeineCacheManager;
}
public Caffeine<Object, Object> caffeineCacheBuilder() {
return Caffeine.newBuilder()
.initialCapacity(200)
.maximumSize(500)
.weakKeys()
.recordStats();
}
}
2 solutions (they work for me):
remove .weakKeys()
propertyService.get(name.intern()) - wouldn't really do that, possibly a big cost
Sorry, but I don't have enough knowledge to explain this. Probably something to do with internal key representation by Caffeine.

Spring Framework. Inject bean at runtime based on request parameter

Say we have a FileLoader Interface:
public interface FileLoader {
default String loadFile(String fileId) {
// Default business logic
return "Default implementation for FileLoader. Loading file" + fileId;
}
}
And different implementations for different countries:
public class USAFileLoader implements FileLoader {
#Override
public String loadFile(String fileId) {
// ... Specific business logic for USA
return "USA implementation for FileLoader. Loading file" + fileId;
}
}
public class FRAFileLoader implements FileLoader {
#Override
public String loadFile(String fileId) {
// ... Specific business logic for France
return "France implementation for FileLoader. Loading file" + fileId;
}
}
And we create an endpoint to load files:
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RestController;
#RestController
public class FileUploadController {
FileLoader fileLoader;
#PostMapping("/load/{fileId}/{countryCode}")
public String loadFile(#PathVariable String fileId, #PathVariable String countryCode) {
fileLoader = ... // Inject the right loader based on countryCode
return fileLoader.loadFile(fileId);
}
}
How can I inject the right FileLoader at runtime for every request, based on countryCode? I've found something in Spring called FactoryBean that apparently may work, but I'm now sure if it's the right tool, or if this is the right way to address this problem. Also, I don't know how injection will behave with requests being proccessed at the same time.
The best thing you can do here using run time polymorphism, add one more abstract method in interface FileLoader for country code
public interface FileLoader {
default String loadFile(String fileId) {
// Default business logic
return "Default implementation for FileLoader. Loading file" + fileId;
}
public abstract String getCountryCode();
}
And then implement it in every implementation class with return the appropriate country code
public class USAFileLoader implements FileLoader {
#Override
public String loadFile(String fileId) {
// ... Specific business logic for USA
return "USA implementation for FileLoader. Loading file" + fileId;
}
public String getCountryCode(){
return "USA";
}
}
And then you can Autowire all beans of type FileLoader into List and call loadFile on appropriate bean
#RestController
public class FileUploadController {
#Autowire
List<FileLoader> fileLoaders;
#PostMapping("/load/{fileId}/{countryCode}")
public String loadFile(#PathVariable String fileId, #PathVariable String countryCode) {
return fileLoaders.stream()
.filter(f->f.getCountryCode().equlas(countryCode))
.findFirst()
.map(loader->loader.loadFile(fileId))
.orElse(()-> FileLoader.super.loadFile(fileId)); //calling interface default method
}
}
You can receive a bean with another way at runtime using ApplicationContext::getBean:
#Autowired
ApplicationContext
#PostMapping("/load/{fileId}/{countryCode}")
public String loadFile(#PathVariable String fileId, #PathVariable String countryCode) {
FileLoader fileloader = (FileLoader) applicationContext.getBean(countryCode);
return fileLoader.loadFile(fileId);
}
However, I'd recommend creating a service layer that aggregates the country-specific implementations and uses a factory pattern. There is nothing bad on such implementation.

Convert legacy code to Spring dependency injection

The legacy code snippet is shown as below. What I want to do is to convert this code to Spring. But the problem is Spring managed the dependency on it's own. My question is how to inject the serviceId provided by constructor?
public class MyService{
public Attribute getAttribute(){
int serviceId =1;
new ServiceDao(serviceId).getAttribute();
}
}
class ServiceDao{
private int serviceId;
ServiceDao(int serviceId){
this.serviceId = serviceId;
}
public Attribute getAttribute(){
//to get attribute
}
}
Basically you are trying to create new objects each time you call
new ServiceDao(serviceId).getAttribute();
This is purely against dependency injection. As your logic is based on the service ID you can create a service class as follows
#Service
class ServiceDao{
ServiceDao(){
}
public Attribute getAttribute(int serviceId){
//to get attribute
//return attribute based on service Id,
//if(serviceId==1)
//{ return new Attribute("Red");}
}
}
Your Myservice can be something like this
#Service
public class MyService{
#Autowired
ServiceDao dao;
public Attribute getAttribute(){
int serviceId =1;
return dao.getAttribute(1);
}
}

How to autowire a spring bean based on some session info

We are working on a multilingual Spring based web application (not Spring Boot).
Now we are searching for the "spring way" of doing the following.
user starts a http session with some parameters, e.g. a locale "de" and/or a country-code "DE" (the type of parameter isn't really important)
user works with application
at some point the user triggers an action that somewhere deep inside needs a "localized" functionality
Example (java pseudocode):
// service with functionality common for all users
#Service
class CommonService implements ICommonService
{
// how to autowire a service based on some info in the actual HttpSession, eg. CustomServiceUK or CustomServiceDE
#Autowired
private ICustomService customServiceImpl;
#Override
public void doSomeAction(String param)
{
... do some common stuff
customResult = customServiceImpl.calculate(param);
... do some common stuff with custom result
}
}
// custom service implementations
#Service("CustomServiceUK")
class CustomServiceUK implements ICustomService
{
#Override
public String calculate(String value)
{
... execute logic on value for an "uk" user
}
}
#Service("CustomServiceDE")
class CustomServiceDE implements ICustomService
{
#Override
public String calculate(String value)
{
... execute logic on value for an "de" user
}
}
How to inject a custom service based on some info in the actual HttpSession (e.g. CustomServiceUK or CustomServiceDE) into CommonService?
What are our options to solve this issue? Is there something like a dynamic #Qualifier or some #Autowired Spring-Factory thing?
(the service implementation to use must not necessarily depend on the locale of the user but on some other piece of session/request information)
Thanks for your answers.
Actually we end up with the following solution which works for us.
We created an additional implementation of ICustomService with name CustomServiceProxy.
This service has #Primary annotation to tell Spring that this component should be injected when no explicit qualifier is supplied.
The service gets the sessionData and a Map with all Spring managed ICustomService-Components injected (Map-Key = Qualifier of the Component).
Now when some method on CustomServiceProxy gets called, it generates the Map-Key based on the actual sessionData (e.g. language), lookup the ICustomService in the Map and delegates the call to this specific service.
// service with functionality common for all users
#Service
class CommonService implements ICommonService
{
// because of #Primary an instance of CustomServiceProxy will be injected
#Autowired
private ICustomService customServiceImpl;
#Override
public void doSomeAction(String param)
{
... do some common stuff
customResult = customServiceImpl.calculate(param);
... do some common stuff with custom result
}
}
// custom service implementations
#Service
#Primary
class CustomServiceProxy implements ICustomService
{
private CustomData sessionData;
private Map<String, ICustomService> services;
#Autowired
public CustomServiceProxy(CustomData sessionData, Map<String, ICustomService> services)
{
this.sessionData = sessionData;
this.services = services;
}
#Override
public String calculate(String value)
{
String serviceName = "CustomService" + sessionData.getLanguage().toUpperCase();
ICustomService customService = services.get(serviceName);
// handle missing service: throw exception or maybe switch to a default implementation
Objects.requireNonNull(customService, "missing CustomService with name " + serviceName);
return customService.calculate(value);
}
}
#Service("CustomServiceUK")
class CustomServiceUK implements ICustomService
{
#Override
public String calculate(String value)
{
... execute logic on value for an "uk" user
}
}
#Service("CustomServiceDE")
class CustomServiceDE implements ICustomService
{
#Override
public String calculate(String value)
{
... execute logic on value for an "de" user
}
}

Categories