I am trying to understand how Hystrix works with non-fault errors and the HystrixBadRequestException, particularly in the area of validation. I use JSR-303 bean validation (Hibernate validator) for all my beans:
public class User {
#Min(1L)
private Long id;
#NotNull
#Email
private String email;
}
public class UserValidator {
private Validator validator;
// Throw exception if the user is invalid; return void otherwise.
public void validateUser(User user) {
Set<ConstraintViolation<User>> violations = validator.validate(user);
if(!violations.isEmpty()) {
return new BadEntityException(violations);
}
}
}
// Hystrix command.
public class SaveUserCommand extends HystrixCommand<User> {
public User user;
public void doSaveUser(User user) {
this.user = user;
execute();
}
#Override
protected User run() {
// Save 'user' somehow
}
#Override
protected User getFallback() {
return null;
}
}
// My service client that uses my Hystrix command.
public class UserClient {
private SaveUserCommandFactory factory = new SaveUserCommandFactory();
private UserValidator validator = new UserValidator();
public User saveUser(User user) {
SaveUserCommand saveUserCommand = factory.newSaveUserCommand();
validator.validate(user);
user = saveUserCommand.doSaveUser(user);
return user;
}
}
While this should work, I feel like the HystrixBadRequestException was created for this purpose, and I could somehow be putting the validator inside the command (not outside of it). According to the docs, this exception was intended for non-fault exceptions, including illegal arguments. I'm just not seeing how I could put my validation inside the command and leverage it (such that failed validations don't count against my metrics/stats).
It turns out you need to throw the HystrixBadRequestException inside the HystrixCommand impl. In my case, the solution was to move the validator into the SaveUserCommand#run() method:
#Override
protected void run() {
try {
validator.validate(user);
// Save user somehow
} catch(BadEntityException bexc) {
log.error(bexc);
throw new HystrixBadRequestException("Hystrix caught a bad request.", bexc);
}
}
Now, if validation fails, the outer exception is a HystrixBadRequestException and it will not count against circuit breaker stats or published metrics.
Related
I implemented a validation using the chain of responsibility pattern. The request payload to validate can have different parameters. The logic is: if the payload has some parameters, validate it and continue to validate other, else throw an exception. In a level of the validation chain I need to call other services, and here comes into play the Dependency Injection.
The validation structure is like a tree, starting from top to bottom.
So, the class where I need to start the Validation
#Service
public class ServiceImpl implements Service {
private final .....;
private final Validator validator;
public ServiceImpl(
#Qualifier("lastLevelValidator") Validator validator, .....) {
this.validator = validator;
this...........=............;
}
/...../
private void validateContext(RequestContex rc) {
Validator validation = new FirstLevelValidator(validator);
validation.validate(rc);
}
}
So the Validator Interface
public interface Validator<T> {
void validate(T object);
}
The validation classes that implements Validator
#Component
public class FirstLevelValidator implements Validator<RequestContext>{
private final Validator<RequestContext> validator;
#Autowired
public FirstLevelValidator(#Qualifier("lastLevelValidator") Validator<RequestContext> validator) {
this.validator = validator;
}
#Override
public void validate(RequestContext requestContext) {
if ( requestContext.getData() == null ) {
LOGGER.error(REQUEST_ERROR_MSG);
throw new BadRequestException(REQUEST_ERROR_MSG, INVALID_CODE);
}
if (requestContex.getData() == "Some Data") {
Validator validator = new SecondLevelValidator(this.validator);
validator.validate(requestContext);
} else {/* other */ }
}
#Component
public class SecondLevelValidator implements Validator<RequestContext>{
private final Validator<RequestContext> validator;
#Autowired
public SecondLevelValidator(#Qualifier("lastLevelValidator") Validator<RequestContext> validator) {
this.validator = validator;
}
#Override
public void validate(RequestContext requestContext) {
if ( requestContext.getOption() == null ) {
LOGGER.error(REQUEST_ERROR_MSG);
throw new BadRequestException(REQUEST_ERROR_MSG, INVALID_CODE);
}
if ( requestContext.getOption() == " SOME " ) {
validator.validate(requestContext); //HERE WHERE I CALL THE Qualifier
}
}
#Component
public class LastLevelValidator implements Validator<RequestContext>{
private final ClientService1 client1;
private final ClientService2 client2;
public LastLevelValidator(ClientService1 client1, ClientService2 client2) {
this.client1 = client1;
this.client2 = client2;
}
#Override
public void validate(RequestContext requestContext) {
Integer userId = client2.getId()
List<ClientService1Response> list = client1.call(requestContext.id(), userId);
boolean isIdListValid = list
.stream()
.map(clientService1Response -> clientService1Response.getId())
.collect(Collectors.toSet()).containsAll(requestContext.getListId());
if (!isIdListValid) {
LOGGER.error(NOT_FOUND);
throw new BadRequestException(NOT_FOUND, INVALID_CODE);
} else { LOGGER.info("Context List validated"); }
}
}
In the LastLevelValidator I need to call other services to make the validation, for that I inject into each validator class (First.., Second..) the #Qualifier("lastLevelValidator") object, so when I need to instantiate the LastLevelValidation class I can call it like validator.validate(requestContext); instance of validator.validate(ClientService1, ClientService2 ) that it would force me to propagate the ClientServices objects through all the chain from the ServiceImpl class.
Is it this a good solution ?
Is there any concern I didn't evaluate?
I tried also declaring the services I need to call for the validation as static in the LastLevelValidation, in the way that I can call it like LastLevelValidation.methodvalidar(), but look like not a good practice declares static objects.
I tried to pass the objects I need propagating it for each Validation class, but seems to me that if I need another object for the validation I have to pass it through all the validation chain.
I'm trying to make artificial CONSTRAINT violation by Spring instead of throwing exception from DB (an expert sad DB-produced errors have high performance cost):
import javax.validation.ConstraintViolation;
import javax.validation.Validator;
#Component
public class AccountValidator implements org.springframework.validation.Validator {
#Autowired
private Validator validator;
private final AccountService accountService;
public AccountValidator(#Qualifier("accountServiceAlias")AccountService accountService) {
this.accountService = accountService;
}
#Override
public boolean supports(Class<?> clazz) {
return AccountRequestDTO.class.equals(clazz);
}
#Override
public void validate(Object target, Errors errors) {
Set<ConstraintViolation<Object>> validates = validator.validate(target);
for (ConstraintViolation<Object> constraintViolation : validates) {
String propertyPath = constraintViolation.getPropertyPath().toString();
String message = constraintViolation.getMessage();
errors.rejectValue(propertyPath, "", message);
}
AccountRequestDTO account = (AccountRequestDTO) target;
if(accountService.getPhone(account.getPhone()) != null){
errors.rejectValue("phone", "", "Validator in action! This number is already in use.");
}
}
}
However, second part of validate() method never works for reasons I cant understand and always pass a call from controller to be handled in try-catch block throwing exception from DB:
public void saveAccount(AccountRequestDTO accountRequestDTO) throws Exception {
LocalDate birthday = LocalDate.parse(accountRequestDTO.getBirthday());
if (LocalDate.from(birthday).until(LocalDate.now(), ChronoUnit.YEARS) < 18) {
throw new RegistrationException("You must be 18+ to register");
}
Account account = new Account(accountRequestDTO.getName(), accountRequestDTO.getSurname(),
accountRequestDTO.getPhone(), birthday, BCrypt.hashpw
(accountRequestDTO.getPassword(), BCrypt.gensalt(4)));
account.addRole(Role.CLIENT);
try {
accountRepository.save(account);
}
catch (RuntimeException exc) {
throw new PersistenceException("Database exception: this number is already in use.");
}
}
Here's a controller method:
#PostMapping("/confirm")
public String signIn(#ModelAttribute("account") #Valid AccountRequestDTO accountRequestDTO,
BindingResult result, Model model) {
accountValidator.validate(accountRequestDTO, result);
if(result.hasErrors()) {
return "/auth/register";
}
try {
accountService.saveAccount(accountRequestDTO);
}
catch (Exception exc) {
model.addAttribute("message", exc.getMessage());
return "/auth/register";
}
return "/auth/login";
}
At service:
#Transactional(readOnly = true)
public String getPhone(String phone){
return accountRepository.getPhone(phone);
}
JpaRepository query:
#Query("SELECT phone FROM Account accounts WHERE phone=:check")
String getPhone(String check);
Tests are green:
#BeforeAll
static void prepare() {
search = new String("0000000000");
}
#BeforeEach
void set_up() {
account = new Account
("Admin", "Adminov", "0000000000", LocalDate.of(2001, 01, 01), "superadmin");
accountRepository.save(account);
}
#Test
void check_if_phone_presents() {
assertThat(accountRepository.getPhone(search).equals(account.getPhone())).isTrue();
}
#Test
void check_if_phone_not_presents() {
String newPhone = "9999999999";
assertThat(accountRepository.getPhone(newPhone)).isNull();
}
#AfterEach
void tear_down() {
accountRepository.deleteAll();
account = null;
}
#AfterAll
static void clear() {
search = null;
}
You need to register your validator.
After we've defined the validator, we need to map it to a specific
event which is generated after the request is accepted.
This can be done in three ways:
Add Component annotation with name “beforeCreateAccountValidator“.
Spring Boot will recognize prefix beforeCreate which determines the
event we want to catch, and it will also recognize WebsiteUser class
from Component name.
#Component("beforeCreateAccountValidator")
public class AccountValidator implements Validator {
...
}
Create Bean in Application Context with #Bean annotation:
#Bean
public AccountValidator beforeCreateAccountValidator () {
return new AccountValidator ();
}
Manual registration:
#SpringBootApplication
public class SpringDataRestApplication implements RepositoryRestConfigurer {
public static void main(String[] args) {
SpringApplication.run(SpringDataRestApplication.class, args);
}
#Override
public void configureValidatingRepositoryEventListener(
ValidatingRepositoryEventListener v) {
v.addValidator("beforeCreate", new AccountValidator ());
}
}
I have a quite simple quarkus extension which defines a ContainerRequestFilter to filter authentication and add data to a custom AuthenticationContext.
Here is my code:
runtime/AuthenticationContext.java
public interface AuthenticationContext {
User getCurrentUser();
}
runtime/AuthenticationContextImpl.java
#RequestScoped
public class AuthenticationContextImpl implements AuthenticationContext {
private User user;
#Override
public User getCurrentUser() {
return user;
}
public void setCurrentUser(User user) {
this.user = user;
}
}
runtime/MyFilter.java
#ApplicationScoped
public class MyFilter implements ContainerRequestFilter {
#Inject
AuthenticationContextImpl authCtx;
#Override
public void filter(ContainerRequestContext requestContext){
// doing some stuff like retrieving the user from the request Context
// ...
authCtx.setCurrentUser(retrievedUser)
}
}
deployment/MyProcessor.java:
class MyProcessor {
//... Some stuff
#BuildStep
AdditionalBeanBuildItem createContext() {
return new AdditionalBeanBuildItem(AuthenticationContextImpl.class);
}
}
I have a Null Pointer Exception in authCtx.setCurrentUser(retrievedUser) call (authCtx is never injected)
What am I missing here ?
Thanks
Indexing the runtime module of the extension fixes the problem.
There are multiple ways to do that as mentioned in https://stackoverflow.com/a/55513723/2504224
I have a Quarkus application in which I implemented the ContainerRequestFilter interface to save a header from incoming requests:
#PreMatching
public class SecurityFilter implements ContainerRequestFilter {
private static final String HEADER_EMAIL = "HD-Email";
#Override
public void filter(ContainerRequestContext requestContext) throws IOException {
String email = requestContext.getHeaders().getFirst(HEADER_EMAIL);
if (email == null) {
throw new AuthenticationFailedException("Email header is required");
}
requestContext.setSecurityContext(new SecurityContext() {
#Override
public Principal getUserPrincipal() {
return () -> email;
}
#Override
public boolean isUserInRole(String role) {
return false;
}
#Override
public boolean isSecure() {
return false;
}
#Override
public String getAuthenticationScheme() {
return null;
}
});
}
}
In a class annotated with ApplicationScoped I injected the context as follows:
#ApplicationScoped
public class ProjectService {
#Context
SecurityContext context;
...
}
The problem is that the context attribute is actually never injected, as it is always null.
What am I doing wrong? What should I do to be able to retrieve the SecurityContext throughout the application's code?
I like to abstract this problem, so that the business logic does not depend on JAX-RS-specific constructs. So, I create a class to describe my user, say User, and another interface, the AuthenticationContext, that holds the current user and any other authentication-related information I need, e.g.:
public interface AuthenticationContext {
User getCurrentUser();
}
I create a RequestScoped implementation of this class, that also has the relevant setter(s):
#RequestScoped
public class AuthenticationContextImpl implements AuthenticationContext {
private User user;
#Override
public User getCurrentUser() {
return user;
}
public void setCurrentUser(User user) {
this.user = user;
}
}
Now, I inject this bean and the JAX-RS SecurityContext in a filter, that knows how to create the User and set it into my application-specific AuthenticationContext:
#PreMatching
public class SecurityFilter implements ContainerRequestFilter {
#Inject AuthenticationContextImpl authCtx; // Injecting the implementation,
// not the interface!!!
#Context SecurityContext securityCtx;
#Override
public void filter(ContainerRequestContext requestContext) throws IOException {
User user = ...// translate the securityCtx into a User
authCtx.setCurrentUser(user);
}
}
And then, any business bean that needs the user data, injects the environment-neutral, application-specific AuthenticationContext.
#Context can only be used in JAX-RS classes - i.e. classes annotated with #Path.
In your case, ProjectService is a CDI bean, not a JAX-RS class.
The canonical way to do what you want is to inject the SecurityContext into a JAX-RS resource and then pass that as a method parameter to your ProjectService
I am trying to substitute the correct use of Mockito, during a test session by JUnit, in place of to stub a class.
Unfortunately on web there are a lot of tutorials regarding Mockito but, less for the stub approach, and I would like to learn this technique.
This test is made by Mockito:
#Test
public void addWrongNewUserSpaceInUsername() throws Exception {
when(userValidator.isValidUsername(user.getUsername())).thenReturn(false);
try {
mockMvc.perform(
post("/register")
.contentType(MediaType.APPLICATION_JSON)
.content(asJsonString(user)
));
} catch (Exception e) {
Assert.assertTrue(e.getCause() instanceof UsernameNotValidException);
}
}
To clarify these are the classes involved:
1) Controller
#RestController
public class UserController {
#Autowired
RepositoryUserDB repositoryUserDB;
#Autowired
UserValidator userValidator;
#RequestMapping(value = "/register", method = RequestMethod.POST)
public User createUser(#RequestBody User user) {
if (userValidator.isValidUsername(user.getUsername())) {
if(!userValidator.isValidPassword(user.getPassword())){
throw new PasswordNotValidException();
}
if(userValidator.isValidDateOfBirth(user.getDateOfBirth()) == false){
throw new DOBNotValidException();
}
// se lo user e' gia' presente
if (repositoryUserDB.getUserByUsername(user.getUsername()) == null) {
return repositoryUserDB.createUser(user);
}
throw new UsernameAlreadyExistException();
} else throw new UsernameNotValidException();
}
}
2)Interface of repo:
public interface RepositoryUserDB {
User getUserByUsername(String username);
User createUser(User user);
}
3)Repo:
#Component
public class MemoryUserDB implements RepositoryUserDB{
Map<String, User> repo;
public MemoryUserDB() {
this.repo = new HashMap<>();
}
#Override
public User getUserByUsername(String username) {
return repo.get(username);
}
#Override
public User createUser(User user) {
repo.put(user.getUsername(),user);
return repo.get(user.getUsername());
}
}
4) Validator:
#Component
public class UserValidator {
public boolean isValidUsername(String username) {
return username.matches("^[a-zA-Z0-9]+$");
}
public boolean isValidPassword(String pwd) {
if (pwd == null)
return false;
return pwd.matches("^(?=.*[a-z])(?=.*[A-Z])(?=.*\\d){4,}.+$");
}
public boolean isValidDateOfBirth(String DOB) {
return DOB.matches("^(?:(?:(?:0?[13578]|1[02])(\\/|-|\\.)31)\\1|(?:(?:0?[1,3-9]|1[0-2])(\\/|-|\\.)(?:29|30)\\2))(?:(?:1[6-9]|[2-9]\\d)?\\d{2})$|^(?:0?2(\\/|-|\\.)29\\3(?:(?:(?:1[6-9]|[2-9]\\d)?(?:0[48]|[2468][048]|[13579][26])|(?:(?:16|[2468][048]|[3579][26])00))))$|^(?:(?:0?[1-9])|(?:1[0-2]))(\\/|-|\\.)(?:0?[1-9]|1\\d|2[0-8])\\4(?:(?:1[6-9]|[2-9]\\d)?\\d{2})$");
}
}
5)ResEntityExceptionHandler
#ControllerAdvice
public class RestEntityExceptionHandler {
#ExceptionHandler(UsernameNotValidException.class)
#ResponseStatus(value = HttpStatus.FORBIDDEN, reason = "username wrong")
public void handleUsernameException() {
}
#ExceptionHandler(UsernameAlreadyExistException.class)
#ResponseStatus(value = HttpStatus.BAD_REQUEST, reason = "the username is already presents")
public void handleUsername() {
}
#ExceptionHandler(PasswordNotValidException.class)
#ResponseStatus(value = HttpStatus.FORBIDDEN, reason = "password wrong")
public void handlePasswordException() {
}
#ExceptionHandler(DOBNotValidException.class)
#ResponseStatus(value = HttpStatus.FORBIDDEN, reason = "Date Of Birth wrong")
public void handleDOBException(){
}
}
Theoretically, for you use case, the stub approach should be rather simple to do.
But as you rely on a Spring Boot Test that uses the Spring beans container, things are really harder to setup as you should find a way to inject the mocked bean in the container to replace the actual bean : UserValidator.
Mocks in Spring Boot test rely usually on Spring Boot MockBean.
It is not a mockito mock but not very far.
To understand differences with Mockito mocks, you may refer to this question.
Using a framework gives many features out of the box, but also limits yourself as you want to bypass the framework features.
Supposing that you didn't perform an integration test with Spring Boot, but a real unit test (so without the Spring boot container), things could be performed in this way.
Instead of mocking UserValidator.isValidUsername(), you define a custom implementation of UserValidator that stubs the method return as expected in your test.
Finally, what Mockito or any mock framework does for you.
So here is the stub class :
public class UserValidatorStub extends UserValidator {
private String expectedUsername;
private boolean isValidUsername;
public UserValidatorStub(String expectedUsername, boolean isValidUsername){
this.expectedUsername = expectedUsername;
this.isValidUsername = isValidUsername;
}
public boolean isValidUsername(String username) {
if (username.equals(expectedUsername)){
return isValidUsername;
}
// as fallback, it uses the default implementation but you may return false or null as alternative
return super.isValidUsername(username);
}
}
It accepts a constructor to store the expected arguments passed to the stubbed method and the stubbed result to return.
Now, here how your test could be written :
#Test
public void addWrongNewUserSpaceInUsername() throws Exception {
// inject the mock in the class under test
UserController userController = new UserController(new UserValidatorStub(user.getUsername(), false));
try {
userController.createUser(user);
} catch (Exception e) {
Assert.assertTrue(e.getCause() instanceof UsernameNotValidException);
}
}
Note that the example relies on constructor injection in UserController to set the dependencies.