Search code examples
javaspringspring-boot

Spring injected beans are null using Lombok and Constructor Injection


I'm migrating a maven project implemented with Spring to Spring Boot 1.5.20.RELEASE.

I have an adapter class which will be extended by all classes annotated with @RestControllers in order to guarantee backward compatibility with the Front-End

public class RestControllerAdapter {

    private MessageTemplate messageTemplate;

    private MessageTemplate getMessageTemplate() {

        if (messageTemplate == null) {
            messageTemplate = ApplicationContextUtils.getBean(MessageTemplate.class);
        }

        return messageTemplate;
    }

    protected final String message(@NonNull String code) {
        return getMessageTemplate().getMessage(code);
    }

    protected final String message(@NonNull String code, Object... args) {
        return getMessageTemplate().getMessage(code, args);
    }

    protected final ModelMap success() {
        val map = new ModelMap();
        map.put("success", true);
        map.put("message", getMessageTemplate().getMessage("message.success.default"));
        return map;
    }

    protected final ModelMap error(@NonNull String message) {
        val map = new ModelMap(2);
        map.put("success", false);
        map.put("error", message);
        return map;
    }

    protected final ModelMap retry(@NonNull Exception ex) {
        val map = new ModelMap(3);
        map.put("success", false);
        map.put("error", ex.getMessage());
        map.put("confirmar", true);
        return map;
    }

    protected final ModelMap empty() {
        return new ModelMap();
    }

    @ExceptionHandler(JpaSystemException.class)
    public ModelMap handleJpaSystemException(JpaSystemException ex) {
        log.error(ex.getMessage(), ex);
        return createError(ex.getMostSpecificCause());
    }

    @ExceptionHandler(DataIntegrityViolationException.class)
    public ModelMap handleDataIntegrityViolationException(DataIntegrityViolationException ex) {
        log.error(ex.getMessage(), ex);
        return createError(ex.getMostSpecificCause());
    }

    private ModelMap createError(Throwable ex) {
        val modelMap = new ModelMap();
        modelMap.put("error", translateErrorMessage(ex));
        modelMap.put("success", false);
        return modelMap;
    }

    protected String translateErrorMessage(@NonNull Throwable ex) {
        String message = ex.getLocalizedMessage().isEmpty() ? ex.getMessage() : ex.getLocalizedMessage();

        if (message.contains("12519")) {
            message = getMessageTemplate().getMessage("db.connection.error");
        } else if (message.contains("SYS_C0015328")) {
            message = getMessageTemplate().getMessage("plan.tasks.successors.sequence.error");
        } else if (message.contains("SYS_C0012415")) {
            message = getMessageTemplate().getMessage("positions.sequence.error");
        } else if (message.contains("SYS_C006343")) {
            message = getMessageTemplate().getMessage("documents.sequence.error");
        } else if (message.contains("UNIQUE_EMAIL")) {
            message = getMessageTemplate().getMessage("user.email.unique");
        } else if (message.contains("FK_PLTASK_TASK")) {
            message = getMessageTemplate().getMessage("task.delete.error");
        }

        return message;
    }
}

I have a rest controller class for users management

 @RestController
 @RequestMapping(value = "/usuario")
 @RequiredArgsConstructor
 public class UserRestController extends RestControllerAdapter {
    
   private final UserService usersService;
   private final UsersRepository usersRepository;
   private final UserBackupRepository userBackupRepository;
   private final AreaRepository areaRepository;
   private final PositionRepository positionRepository;
   private final UserMapper userMapper; 
         
   @PreAuthorize(value = SecurityUtils.ADMIN_EDIT_AUTHORITY)
   @PutMapping("/{userId}")
   public ModelMap update(@Valid @RequestBody UserCommand userCommand, 
                         @PathVariable int userId) {
       try {
           usersService.update(userId, userCommand);
           return success();
       } catch (DuplicateHolderException ex) {
           return retry(ex);
       }
   }
 
      @PreAuthorize(value = SecurityUtils.ADMIN_EDIT_AUTHORITY)
      @PostMapping("/activarUsuario")
      public ModelMap enable(@RequestParam("usuario_id") Integer id, 
                                     @AuthenticationPrincipal Users user) {
 
           if (user.getRol().getId() == 1 || user.getRol().getId() == 3) {
             usersService.enable(id);
             return success();
           }
 
           return empty();
     }
 
   @Override
   protected String translateErrorMessage(@NonNull Throwable ex) {
     String message = ex.getMessage();
 
     if (message.contains("correo_unico")) {
         message = "Ya este correo está asigado a un usuario.";
     } else if (message.contains("SYS_C0012422") || 
         message.contains("SYS_C0015349")) {
         message = "La secuencia de usuarios no está correctamente 
                                                               configurada.";
     } else if (message.contains("UNIQUE_BACKUP")) {
         message = "Un usuario no puede ser backup mas de una ocasión.";
     } else if (message.contains("UNIQUE_EMAIL")) {
         message = "No se puede insertar el usuario porque este correo ya 
           está en uso.";
     } else if (message.contains("FK_PLAN_USER_ACTIVADO")) {
         message = "No se puede insertar el usuario porque existe un plan que 
         ha sido activado por dicho usuario.";
     } else if (message.contains("FK_TASK_EXECUTED_BY")) {
         message = "No se puede insertar el usuario porque existen tareas 
         ejecutadas por dicho usuario.";
     }
 
     return super.translateErrorMessage(ex);
  }
 
 }

When UserRestController::update is called a NullPointerException is thrown due to all dependecies injected into UserServiceImpl are null, as shown in the image below.

error

However when UserRestController::enable is called everything works fine. both depends on UserServiceImpl's methods calls.

    @Service
    @RequiredArgsConstructor
    @Transactional(rollbackFor = Exception.class)
    class UserServiceImpl implements UserService {
    
        private static final int POSITION_USERS_LIMIT = 3;
        private static final int BACKUPS_LIMIT = 2;
        private static final String POSITION_LIMIT_ERROR = "user.position.limit";
        private static final String BACKUP_LIMIT_ERROR = "user.backup.limit";
    
        private final UsersRepository usersRepository;
        private final UserBackupRepository userBackupRepository;
        private final RolRepository rolRepository;
        private final PositionRepository positionRepository;
        private final PlanRepository planRepository;
        private final PlTaskRepository plTaskRepository;
        private final UserNotificationRepository userNotificationRepository;
        private final NotificacionBackupRepository notificacionBackupRepository;
        private final UserTokenRepository userTokenRepository;
        private final UserMapper userMapper;
        private final PasswordTemplate passwordTemplate;
        private final MessageTemplate messageTemplate;
    
           @Override
        public final Users update(int id, @NonNull UserCommand userCommand) {
            val user = usersRepository.findById(id)
                    .orElseThrow(() -> new IllegalArgumentException("Usuario a editar requerido"));
            val role = loadRoleFrom(userCommand);
            val newPosition = loadPositionFrom(userCommand);
    
            if (userCommand.isCheckConstraints()) {
                assertUserUpdate(userCommand, user, newPosition);
            }
    
            if (userCommand.isHolder()) {
    
                if (!user.isTitular() && usersRepository.existsByPositionAndTitularTrue(user.getPosition())) {
                    userBackupRepository.deleteAllByUsuarioPositionAndUsuarioTitularTrue(user.getPosition());
                    usersRepository.updateTitularFalse(user.getPosition());
                } else if (!user.hasPosition(newPosition) && usersRepository.existsByPositionAndTitularTrue(newPosition)) {
                    userBackupRepository.deleteAllByUsuarioPositionAndUsuarioTitularTrue(newPosition);
                    usersRepository.updateTitularFalse(newPosition);
                } else if (user.isTitular()) {
                    userBackupRepository.deleteAllByUsuarioPositionAndUsuarioTitularTrue(user.getPosition());
                }
            } else {
                userBackupRepository.deleteAllByUsuarioPositionAndUsuarioTitularTrue(user.getPosition());
            }
    
            user.setEmail(userCommand.getEmail());
            user.setUsuario(userCommand.getEmail());
            user.setName(userCommand.getName());
            user.setLastname(userCommand.getLastname());
            user.setActive(userCommand.isEnabled());
            user.setTitular(userCommand.isHolder());
            user.setPosition(newPosition);
            user.setRol(role);
    
            if (!StringUtils.isEmpty(userCommand.getPassword())) {
                user.setKeypass(passwordTemplate.encode(user.getPassword()));
            }
    
            usersRepository.save(user);
            addBackups(userCommand, user);
            return user;
        }
    
         @Override
        public Users enable(@NonNull Integer id) {
            val user = usersRepository.findById(id).orElseThrow(NoSuchElementException::new);
            user.setActive(!user.isActive());
            usersRepository.save(user);
            return user;
        }    
    }

This bug is killing me, It's a weird behavior.


Solution

  • Finally, I solved the issue.

    It's related with the final keyword in UserServiceImpl::update method. It doesn't allow the injection process to work properly. It makes sense since classes annotated with @Transactional cannot be final

    I've removed the final keyword, and now everything works fine. The example code is here