I have a requirement where I need to call aspect before processing the rest endpoint method, I have created an annotation and annotating the rest endpoint,
I am able to process the request but when reading InpuStream it is already closed inseide aspect. The code snippet is attached I am using spring boot 1.5
GitHub https://github.com/primeap/user-auth-poc.git
Once I get input stream in the Aspect I will cache it before using it, but my problem is at the current code it is throwing an
io exception stream is closed
at line
Map jsonMap =
mapper.readValue(request.getInputStream(), Map.class);
Application
#SpringBootApplication
#ComponentScan(basePackages = { "org.ap" })
public class UserAuthPocApplication {
public static void main(String[] args) {
SpringApplication.run(UserAuthPocApplication.class, args);
}
}
Annotation
#Target({ ElementType.METHOD })
#Retention(RetentionPolicy.RUNTIME)
public #interface HasPrivilegeXX {
}
Aspect
#Aspect
#Component
public class MyPrivilegeAspect {
#Before("#annotation(HasPrivilegeXX)")
public boolean logBeforeAllMethods(JoinPoint joinPoint) {
HttpServletRequest request = ((ServletRequestAttributes) RequestContextHolder.currentRequestAttributes())
.getRequest();
try {
ObjectMapper mapper = new ObjectMapper();
Map<String, Object> jsonMap = mapper.readValue(request.getInputStream(), Map.class);
System.out.println("In aspect " + jsonMap.toString());
} catch (Exception e) {
e.printStackTrace();
}
return true;
}
}
Rest Endpoint
#RestController
#RequestMapping("/api")
public class Api {
#PostMapping("/hi")
#HasPrivilegeXX()
public String hiPost(#RequestBody User user) {
System.out.println(user.toString());
return "Hi...";
}
}
User
public class User {
private String name;
private String company;
private Integer id;
}
Related
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 am working on Kotlin with spring boot.
I have an entity Hours
data class Hours(
#get: Max(value=3) val value : Long)
And in my constructor, I have the following
fun postHours(#RequestBody #Valid hours: #Valid LinkedHashMap<String, Array<Hours>>): String {
return service.addHours(hours)
}
But the validation is not working at all.
I am able to send requests with value > 3 even though I have set the max value as 3. and there is no error.
Could someone tell me whats wrong here?
I have a similar case that is working in Java (you can rewrite to Kotlin). Please try :
define a new bean in your configuration class annotated with #Configuration :
#Bean
public MethodValidationPostProcessor methodValidationPostProcessor() {
return new MethodValidationPostProcessor();
}
annotate your controller with :
#RestController
#Validated
Then you can validate you class with :
postHours(#RequestBody LinkedHashMap<String, #Valid Hours> hours)
----- MODIFICATION TO PROVIDE FULL EXAMPLE -----
You can use this full example. I am using : spring-boot-starter-web 2.2.6 with lombok.
Here is the class to validate :
#Data
public class Foo {
#Max(value = 3)
private Integer count;
}
Controller :
#RestController
#Validated
public class FooController {
#PostMapping("/validate")
public String validate(#RequestBody LinkedHashMap<String, List<#Valid Foo>> foos) {
return "foo";
}
}
Configuration class :
#Configuration
public class AppConfig {
#Bean
public MethodValidationPostProcessor methodValidationPostProcessor() {
return new MethodValidationPostProcessor();
}
}
Controller advice (to send exception as json) :
#ControllerAdvice
public class CustomResponseEntityExceptionHandler extends ResponseEntityExceptionHandler {
#Data
#AllArgsConstructor
class ApiError {
private HttpStatus status;
private String message;
private List<String> errors;
}
#ExceptionHandler({ ConstraintViolationException.class })
public ResponseEntity<Object> handleConstraintViolation(ConstraintViolationException ex, WebRequest request) {
List<String> errors = new ArrayList<>();
for (ConstraintViolation<?> violation : ex.getConstraintViolations()) {
errors.add(violation.getRootBeanClass().getName() + " " +
violation.getPropertyPath() + ": " + violation.getMessage());
}
ApiError apiError = new ApiError(HttpStatus.BAD_REQUEST, ex.getLocalizedMessage(), errors);
return new ResponseEntity<>(apiError, new HttpHeaders(), apiError.getStatus());
}
}
Test class :
#WebMvcTest(FooController.class)
public class FooControllerTest {
#Autowired
private MockMvc mockMvc;
#Autowired
private ObjectMapper objectMapper;
#Test
public void test() throws Exception {
LinkedHashMap<String, Foo> foos = new LinkedHashMap<>();
Foo foo = new Foo();
foo.setCount(4);
foos.put("foo", foo);
this.mockMvc.perform(post("/validate")
.contentType(MediaType.APPLICATION_JSON)
.content(objectMapper.writeValueAsString(bars)))
.andExpect(status().isBadRequest())
.andExpect(content().string(containsString("validate.foos[foo].<map value>[0].count: must be less than or equal to 3")));
}
}
The unit test shows that validation inside linkedhashmap is working.
Finally found a fix for this. Wrote an Array wrapper class
class ArrayWrapper<E>( elements: Array<E>?) {
#Valid var _array: Array<E>? = elements
override fun equals(other: Any?): Boolean {
return if (other !is ArrayWrapper<*>) {
false
} else Arrays.equals(_array, other._array)
}
override fun hashCode(): Int {
return Arrays.hashCode(_array)
}
}
and then modified the controller method like this.
fun postHours(#RequestBody #Valid hours: LinkedHashMap<String, ArrayWrapper<Hours>>): String {
return service.addHours(hours)
}
Now validation works perfectly!
I'm developing a spring web application. I have no XML configuration at all.
The main class of the spring boot app is annotatd with a component scan which includes all the beans here listed.
Having this controller class:
#CrossOrigin
#RestController
#RequestMapping(value = "/documento/detail/tabs")
public class InfoController {
#Autowired
private DocDetailService detailService;
/**
* SEC 26-28: Pricing e posizioni
*/
#LogMethod
#GetMapping(value = "/pricing/{numOperazione}", produces = MediaType.APPLICATION_JSON_VALUE)
private ResponseEntity<DetailPricingDTO> getDettagliPricingEPosizioni(
#PathVariable(value = "numOperazione") final String numOperazione) throws DocumentNotFoundException {
return ResponseEntity.ok(detailService.getDettagliPricing(numOperazione));
}
And #LogMethod defined like this:
#Documented
#Retention(RUNTIME)
#Target({ METHOD })
public #interface LogMethod {
}
With an aspect defined as follows, to log all method annotated with that request
#Aspect
#Component
#Scope("singleton")
public class LogEventAspect {
private final Logger log = LoggerFactory.getLogger(this.getClass());
#PostConstruct
public void postConstruct() {
log.info("# LogMethod annotation ASPECT is enabled #");
}
#Pointcut("#annotation(LogMethod)")
public void logEventAnnotationPointCut() {
// Method is empty as this is just a point-cut, the implementations are in the
// advises.
}
#AfterThrowing(pointcut = "logEventAnnotationPointCut()", throwing = "e")
public void logAfterThrowing(JoinPoint joinPoint, Throwable e) {
log.error("Exception in {}.{}() with cause = \'{}\' and exception = \'{}\'",
joinPoint.getSignature().getDeclaringTypeName(), joinPoint.getSignature().getName(),
e.getCause() != null ? e.getCause() : "NULL", e.getMessage(), e);
}
#Around("logEventAnnotationPointCut()")
public Object logAround(ProceedingJoinPoint joinPoint) throws Throwable {
// Log before execution
if (log.isDebugEnabled()) {
log.debug("Enter>>>: {}.{}() with argument[s] = {}", joinPoint.getSignature().getDeclaringTypeName(),
joinPoint.getSignature().getName(), Arrays.toString(joinPoint.getArgs()));
}
Object result = joinPoint.proceed();
// log after execution
if (log.isDebugEnabled()) {
log.debug("Exit<<<: {}.{}() with result = {}", joinPoint.getSignature().getDeclaringTypeName(),
joinPoint.getSignature().getName(), result);
}
return result;
}
}
The detailService in the controller is NULL. If I remove the #LogMethod the service is correctly initialized.
Also, if I use the #LogMethod in a #Service class instead of the #RestController, the autowiring of other beans does work.
Why does this happen?
I have myannotation and whenever my method(which has myannotation) is executed then AOP should be called but which is not working in my spring boot controller.But which is working for methods which has other annotations.Please help me to understand about what happens.
Update: MyAnnotation
#Retention(RUNTIME)
#Target({ METHOD, CONSTRUCTOR })
public #interface MyAnnotation {
}
#Aspect
#Component
public class AnnotationAspect {
private static final String POINTCUT_METHOD1 = "#annotation(com.somepackage.MyAnnotation)";
#Around(POINTCUT_METHOD1)
public Object weaveJoinPoint(ProceedingJoinPoint joinPoint) throws Throwable {
try {
System.out.println("Beforeee " + joinPoint);
joinPoint.proceed();
} finally {
System.out.println("Afterrr " + joinPoint);
}
return null;
}
}
Scenario 1:(Working)
#Controller
#RequestMapping("user")
public class ArticleController {
#GetMapping("article/{id}")
#MyAnnotation // here it is
public ResponseEntity<String> getArticleById(#PathVariable("id") Integer id)
{
return new ResponseEntity<String>(dummyMethod(), HttpStatus.OK);
}
public String dummyMethod() {
System.out.println("Dummy method with MyAnnotation");
return "HelloWorld!";
}
}
Log:(Working)
Beforeee execution(ResponseEntity com.mypackage.getArticleById(Integer))
Dummy method with MyAnnotation
Afterrr execution(ResponseEntity com.mypackage.getArticleById(Integer))
Scenario 2:(Not Working)
#Controller
#RequestMapping("user")
public class ArticleController {
#GetMapping("article/{id}")
public ResponseEntity<String> getArticleById(#PathVariable("id") Integer id)
{
return new ResponseEntity<String>(dummyMethod(), HttpStatus.OK);
}
#MyAnnotation //here it is
public String dummyMethod() {
System.out.println("Dummy method with MyAnnotation");
return "HelloWorld!";
}
}
Log:(Not Working)
Dummy method with MyAnnotation
Scenario 3: (Not Working)
#Service
public class ArticleService {
#MyAnnotation //here it is
public String dummyMethod() {
System.out.println("Dummy method with MyAnnotation");
return "HelloWorld!";
}
}
It might not work because you call dummyMethod() from the same class. Try moving dummyMethod() to another service class. The reason is that calls within the same class does not go though the Spring proxy. The call to getArticleById() is proxied and will be handled by AOP but dummyMethod() might as well be a private method.
I'm trying to use the Spring Validator and #Validated annotation to validate a Get Request parameter but cannot get the validator to run. I'm using a ModelAttribute to try and get the validator to run on the Path Variable instead of the Request Body. Is it possible to run a validator on a Get Request Path Variable?
Here is my controller class and method
#RestController
public class ProfileController {
#RequestMapping(value = "/profile/{param}", method = RequestMethod.GET)
public IVRProfile getProfile(#Validated(ParamValidator.class) #ModelAttribute("param") String param) {
return sampleProfile();
}
#ModelAttribute("param")
public String paramAsModelAttribute(#PathVariable String param) {
return param;
}
}
And the Validator class
#Component
public class ParamValidator implements Validator
{
#Override
public boolean supports(Class<?> clazz)
{
System.out.println("Validator supports test");
return String.class.isAssignableFrom(clazz);
}
#Override
public void validate(Object target, Errors errors)
{
System.out.println("Validator Test");
// Validation code
}
}
Neither prints statements are executed when hitting the endpoint.
Any help on what I could be missing or do differently would be greatly appreciated, thanks!
You can implement desired validation functionality as following.
public class ParamValidator implements ConstraintValidator<ParamConstraint, String> {
#Override
public void initialize(ParamConstraint paramConstraint) {
}
#Override
public boolean isValid(String paramField, ConstraintValidatorContext cxt) {
//Perform paramField validation
return true;
}
}
-
#Documented
#Constraint(validatedBy = ParamValidator.class)
#Target( { ElementType.PARAMETER })
#Retention(RetentionPolicy.RUNTIME)
public #interface ParamConstraint {
String message() default "Default validation message";
Class<?>[] groups() default {};
Class<? extends Payload>[] payload() default {};
}
-
#RequestMapping(value = "/profile/{param}", method = RequestMethod.GET)
public IVRProfile getProfile(#Valid #ParamConstraint #ModelAttribute("param") String param) {
return sampleProfile();
}
And finally don't forget to annotate Controller with #Validated.
#RestController
#Validated
public class ProfileController {
//...
}
More details you can find in the example as mentioned here.
You can create the answer you want by using the fields in the ConstraintViolationException with the following method;
#ExceptionHandler(ConstraintViolationException.class)
protected ResponseEntity<Object> handlePathVariableError(final ConstraintViolationException exception) {
log.error(exception.getMessage(), exception);
final List<SisSubError> subErrors = new ArrayList<>();
exception.getConstraintViolations().forEach(constraintViolation -> subErrors.add(generateSubError(constraintViolation)));
final SisError error = generateErrorWithSubErrors(VALIDATION_ERROR, HttpStatus.BAD_REQUEST, subErrors);
return new ResponseEntity<>(error, HttpStatus.BAD_REQUEST);
}
You need to added an #Validated annotation to Controller class and any validation annotation before path variable field
If you want to get single RequestParams like status, you can force it by following the code below.
#RestController
public class ProfileController {
#RequestMapping(value = "/profile/{param}", method = RequestMethod.GET)
public IVRProfile getProfile(#RequestParam(name = "status", required = true) String status, #ModelAttribute("param") String param) {}
}
if you want to force PathVariable, then do this.
#RestController
public class ProfileController {
#RequestMapping(value = "/profile/{param}", method = RequestMethod.GET)
public IVRProfile getProfile(#PathVariable(name = "param", required = true) String param, #ModelAttribute("param") String param) {}
}
Hope this work!!!