I am currently using Spring Cloud Function, and I want to deploy my GET function on AWS Lambda, using the adapter for AWS.
Till now all the Spring Cloud Function implemented was a single POST function with the following structure.
This was done using the
SpringBootRequestHandler.
The DemoRequest class was a POJO class defined to accept the request body and DemoResponse is for the response.
I understand that this part of the code is Depricated.
Interface to define the Endpoint:
#SuppressWarnings("hiding")
#FunctionalInterface
public interface Demoapi<DemoRequest, DemoResponse>{
#PostMapping(value = "/v1/demo")
public ResponseEntity<DemoResponse> demo(#RequestBody DemoRequest demoInfo);
}
Followed by the controller where the #RestController was defined
Part of the controller:
#RestController
public class Democontroller implements Demoapi\<DemoRequest, DemoResponse\>{
static final Logger LOGGER = LogManager.getLogger(Democontroller.class);
#Autowired
private DemoService demoService;
#Override
public ResponseEntity<DemoResponse> demo(#RequestBody DemoRequest demoInfo) {
DemoResponse demoResponse=new DemoResponse();
try {
demoResponse = demoService.demofun(demoInfo);
//logic
}
Now I want to modify this for the GET call and this is the modification I am using.
#SuppressWarnings("hiding")
#FunctionalInterfacepublic interface Demoapi<Void, DemoResponse>{ // as there is no request #GetMapping(value = "/v1/demo")public ResponseEntity<DemoResponse> demo(#RequestParam(name="q", required = false defaultValue="...") String name);// the get call has an optional query param}
Followed by the controller where the #RestController was defined
Part of the controller:
#RestControllerpublic class Democontroller implements Demoapi<Void, DemoResponse>{static final Logger LOGGER = LogManager.getLogger(Democontroller.class);
#Autowiredprivate DemoService demoService;
#Overridepublic ResponseEntity<DemoResponse> demo(#RequestParam(name="q", required = false , defaultValue="...") String name) {DemoResponse demoResponse=new DemoResponse();try {demoResponse = demoService.demofun(name);//logic}
This set up works fine when tested in local but when deployed as Lambda ( I provide Handler as com.demo.DemoHandler in the AWS console, and in the Environment Variable under FUNCTION_NAME I give the DemoController class with the Starting letter in small caps i.e demoController ) and the lambda throws the following error when tested in the console as well as directed from an API Gateway:
\*2023-02-17 11:24:04.080 INFO 10 --- \[ main\] lambdainternal.AWSLambda : Started AWSLambda in 2.649 seconds (JVM running for 3.349)
2023-02-17 11:24:04.329 ERROR 10 --- \[ main\] c.f.c.c.BeanFactoryAwareFunctionRegistry : Failed to invoke function 'domainController'
java.lang.IllegalArgumentException: argument type mismatch
\*
As per error seems like Handler going to find #RestController 'domainController'
In your code there is all Democontroller
The bean for the demoController not defined properly.
Related
I am having some difficulties to do something I thought would be easy.
I have a service:
#Service("tr_v1_mapper")
#RequiredArgsConstructor
#Slf4j
public class VoneMapper implements GenericMapper{
// Mapping code ...
// Service name
String serviceName = "tr_v1_mapper"
}
Basically, I want to stock my service name in a String variable inside my class VoneMapper without harcoding it.
is it something possible ?
Thanks by advance for your help.
PM
What about using a constant in the annotation? More info here: https://www.baeldung.com/java-annotation-attribute-value-restrictions
#Service(VoneMapper.SERVICE_NAME)
#RequiredArgsConstructor
#Slf4j
public class VoneMapper implements GenericMapper {
// Mapping code ...
// Service name
static final String SERVICE_NAME = "tr_v1_mapper";
}
I have a simple problem - SpringBootApplication doesn't see my controller - what's more weird - only one of three.
I have UserController, WalletController and DashboardController - this one is not visible for my application.
What I have already done is:
Every package with controller is under the main package, where my SpringBootApplication.class is,
I tried annotate main SpringBootApplication.class with #ComponentScan both with basePackages and basePackageClasses,
There is no other beans - which should be annotated #Component, I removed them and moved methods to my DashboardService.class
This is my controller, which is not visible:
DashboardController
And this is my package structure(seems to be right): Package Structure
Thank You for help!
EDIT:
It might be important, that I use the third-party api to get the data I need
In that methods I use url:
private String getNbpJson(String url) {
return new RestTemplate().getForObject(url, String.class);
}
private CurrentRateDTO getCurrentExchangeRate(String json) throws JsonProcessingException {
ObjectMapper mapper = new ObjectMapper();
JsonNode node = mapper.readTree(json);
String code = node.get("code").toString();
String date = node.get("rates").get(0).get("effectiveDate").toString();
double bid = node.get("rates").get(0).get("bid").asDouble();
double ask = node.get("rates").get(0).get("ask").asDouble();
return new CurrentRateDTO(code, date, bid, ask);
}
And then in ExchangeService this is my url
#Service
public class ExchangeRateService implements IExchangeRateService {
private static final Logger logger = LoggerFactory.getLogger(ExchangeRateService.class);
private String NBP_CURRENT_RATE_URL = "http://api.nbp.pl/api/exchangerates/rates/c/eur/2020-12-11/?format=json";
What's more... when I move methods from DashboardController to WalletController (which works)
Another thing that I have found out is that only methods, which make use of the third-party api don't work.
Basically, I retrieve data from the url above - I get the specific fields, create an objects with filled fields.
May it be a problem with retrieving data from the third-party and then implementing it in my app?
I have no more ideas for now...
Maybe your controller is registered but you type a slightly different url. Try this property logging.level.org.springframework.web.servlet.mvc.method.annotation: TRACE
and check on application startup if the controller is registered under some other url.
Try to add this to your controller
#RestController()
#RequestMapping("exchangerates")
Okay, I probably found out the problem... Before I have 3 controllers:
#RestController("/api")
public class UserController {}
#RestController("/wallets")
public class WalletController {}
#RestController("/exchangerates")
public class DashboardController {}
I changed the above to
#RestController("/api")
public class UserController {}
#RestController
public class WalletController {}
#RestController
public class DashboardController {}
So, basically I removed base ?endpoint? for each controller and now it works... It seems like basic endpoints in three RestControllers is too much and the third one is not available, but I don't know is it truth (I bet that it's not).
Why that happened then, can anybody explain that behaviour of controllers?
Thank You for explanantion.
I'm trying to achieve something like this:
#Controller
public SomeController {
#CustomConfig("var.a")
private String varA;
#CustomConfig("var.b")
private String varB;
#RequestMapping(value = "/", method = RequestMethod.GET)
public String get() {
return varA;
}
}
CustomConfig would be an #Interface class that accepts one value parameter. The reason why we are not using #Value is because this will not come from config file but from API (such as https://getconfig.com/get?key=var.a). So we are going to make HTTP request to inject it.
So far I've only manage to make something work if the varA and varB is inside get() method as parameter, by using below in a class that extends WebMvcConfigurerAdapter:
#Override
public void addArgumentResolvers(List<HandlerMethodArgumentResolver> argumentResolvers) {
CustomConfigResolver resolver = new CustomConfigResolver();
argumentResolvers.add(resolver);
}
And inside CustomComfigResolver.resolveArgument() we would do the HTTP query, but that's not really what we wanted, we need it to be injected as class variable.
Does anyone have experience in resolving it at class variable level?
Thank you
This could work if you use #Value instead of your own custom annotation. This uses the built in environment:
#Order(Ordered.HIGHEST_PRECEDENCE)
#Configuration
public class TcpIpPropertySourceConfig implements InitializingBean {
#Autowired
private ConfigurableEnvironment env;
#Autowired
private RestTemplate rest;
public void afterPropertiesSet() {
// Call your api using Resttemplate
RemoteProperties props = //Rest Call here;
// Add your source to the environment.
MutablePropertySources sources = env.getPropertySources();
sources.addFirst(new PropertiesPropertySource("customSourceName", props)
}
}
What you are trying to achieve is difficult when you start to consider "unhappy" scenarios. Server down / not reachable. You need to account for all of that in the method above.
I would highly recommend to instead use Spring Cloud Config. Great guide on that is here: https://www.baeldung.com/spring-cloud-configuration
This provides:
- Reloading of your #Value() properties, so no custom annotation needed.
- A more stable server and great Spring integration out of the box.
Best of all, it is easy to apply Retries and Backoffs if the configuration server goes down (see https://stackoverflow.com/a/44203216/2082699). This will make sure your app doesn't just crash when the server is not available.
I am trying to pass an argument to my #RESTController Spring Boot class.
In the #POSTMapping method I want to use a method of a self defined Java class for processing the received body and returning a response.
The Spring application is launched in Application.java. The Controller-Object seems to get created implicitly.
I already tried adding a constructor to my RESTController class. But I couldn't find a way to call that constructor with an argument.
// Application.java
public static void main (String[] args) {
SpringApplication.run(Application.class, args);
}
//ConnectorService.java
#RestController
public class ConnectorService {
private Solveable solver;
public ConnectorService() {}
public ConnectorService (Solveable solveable) {
this.solver = solveable;
}
#CrossOrigin(origins = "http://localhost:3000")
#PostMapping(path = "/maze")
public Solution Test(#RequestBody Test test) {
return solver.solve(test);
}
}
Even though i could define a second constructor, i didn't find any way to call it with my Object.
Use #RequestParam annotation to pass an argument
You can pass parameter with #RequestParam annotation like this:
#CrossOrigin(origins = "http://localhost:3000")
#PostMapping(path = "/maze")
public Solution Test(#RequestParam("paramName") String param, #RequestBody Test test) {
return solver.solve(test);
}
And you can put it with http request:
http://localhost:3000/maze?paramName=someValue
Assuming that you have POST request, there may be different ways to build this request, depending on the API testing tools you use.
#RestController follows the same rules for dependency injection as any other #Component in Spring framework.
If you have a single constructor, Spring will try to „inject” the parameters while instantiating the controller.
You need to register your dependency as a Spring bean.
It seems that you are new to Spring and you are starting with advanced topics like Spring Boot and rest controllers. Please find some time to read about the basics.
Yo can create a Bean configuration file to initialize your objects like:
#Configuration
#ComponentScan("com.xxx.xxx") // the base package you want to scan
public class Config {
#Bean
//where Solveable is a class and is annotated with an Spring's annotation
public Solveable solveable() {
return new Solveable();
}
}
And use the #Autowired annotation to inject the object in:
#Autowired
public ConnectorService (Solveable solveable) {
this.solver = solveable;
}
This last block will initialize or pass(what you want) the object to the ConnectorService class.
I am writing spring controller, which injects a bean.
The bean is added in config(we use java config for everything):
#Bean
public NotificationService notificationService() {
return new NotificationService();
}
The service itself has few injected dependencies and few functions:
public class NotificationService {
#Inject
NotificationRepository notificationRepository;
#Inject
ProjectRepository projectRepository;
#Inject
ModelMapper modelMapper;
public NotificationDto create(NotificationDto notificationDto) {
//convert to domain object, save, return dto with updated ID
return notificationDto;
}
public void markAsRead(Long id, String recipientNip) {
//find notification, update status
}
}
Model mapper has almost no configuration, is only set to strict. Meanwhile repositoriers are interfaces extending JpaRepository with no custom functions. They are found by #EnableJpaRepositories.
Finally I have controller that tries to use the code above:
#RestController
#RequestMapping("/notifications")
public class NotificationController extends ExceptionHandlerController {
#Autowired
private NotificationService notificationService;
#PreAuthorize("isFullyAuthenticated() and hasRole('create_notification')")
#RequestMapping(method = RequestMethod.POST, consumes = MediaTypeExtension.APPLICATION_JSON_UTF8_VALUE)
public ResponseEntity<?> createNotification(#Valid #RequestBody(required = true) final NotificationDto notification) {
this.notificationService.create(notification);
final HttpHeaders headers = new HttpHeaders();
return new ResponseEntity<>(headers, HttpStatus.CREATED);
}
#PreAuthorize("isFullyAuthenticated() and hasRole('update_notification')")
#RequestMapping(value = "/{id}/read", method = RequestMethod.PUT)
private ResponseEntity<?> markNotificationAsRead(#PathVariable("id") Long id, #AuthenticatedContractor ContractorDto contractor) {
this.notificationService.markAsRead(id, contractor.getNip());
final HttpHeaders headers = new HttpHeaders();
return new ResponseEntity<>(headers, HttpStatus.OK);
}
}
All controllers are added trough #ComponentScan, based on their package.
As you can see both functions use notificationService. When I send POST for create on /notifications the notificationService is properly injected. In the same controller, when I do PUT request on /{id}/read, the notificationService is null.
I think it has something to do with spring putting things into its container, and for some reason not being able to do it for that one function. I have few more functions in the controller and in all of them notificationService is properly injected. I don't see any real difference between createNotification and markNotificationAsRead functions and I couldn't find anything even remotely related on google/stack. In all cases the service wouldn't inject at all because of configuration mistake.
Edit
I have tried changing things around in the function until it has started working. My final code looks like this:
#PreAuthorize("isFullyAuthenticated() and hasRole('update_notification')")
#RequestMapping(value = "{id}/read", method = RequestMethod.PUT)
public ResponseEntity<?> read(#PathVariable("id") Long id, #AuthenticatedContractor ContractorDto contractor) {
this.notificationService.markAsRead(id, contractor.getNip());
final HttpHeaders headers = new HttpHeaders();
return new ResponseEntity<>(headers, HttpStatus.OK);
}
and it works. Honestly I can't see any difference from my original code, and I have been staring at it for last hour or so. The imports are the same too.
I have also noticed(on unworking code) that while all functions from the controller on debug stack were marked as
NotificationController.functionName(arguments) line: x
The non working function was:
NotificationController$$EnhancerBySpringCGLIB$$64d88bfe(NotificationController).markNotificationAsRead(ContractorDto) line: 86
Why this single function was enhanced by spring CGLIB I have no idea. I have tried looking it up, but for now I came empty handed. Even though the code started to work I am leaving the question open in order to find the underlying cause.
Your method markNotificationAsRead is private and that probably causes the issue. I've just had same issue with final method - this message appeared in log:
2016-11-28 17:19:14.186 INFO 97079 --- [ main] o.s.aop.framework.CglibAopProxy : Unable to proxy method [public final java.util.Map com.package.controller.MyController.someMethod(javax.servlet.http.HttpServletResponse)] because it is final: All calls to this method via a proxy will NOT be routed to the target instance.
Looks like in one case we see a CGLib proxy, and in another - the actual class. Only one of those has all the fields injected, looks like the proxy has all fields nulls. But it doesn't matter - the point is your method should be public and not final in order to be proxied properly by #PreAuthorize methods.
I was also facing the same issue. It was all due to the private access modifier used and #PreAuthorize. Making the controller method private does not make an issue if you do not make it secure. But, to make secure, make it public.