Spring MVC: One View - Multiply Controllers - java

For note, I'm using Thymeleaf & Spring 2.5.4. For example, I've three different "banner" entities that I need to show on one page. There are "mainBanner", "backgroundBanner" and "newsBanner". First of all, is it the right way to combine controllers in one (in the frame of banner entities)? Or is there exist any standard that says we must write controllers separately for each entity? But the main question is how to write #RequestingMapping correctly for the banner page? I have the banner page ("/admin/banners/") where should be three tables of those entities. As I understand I need to create BannerPageController with #RequestingMapping("/admin/banners/"), isn't it? Hoping for any help in solving
I've written controllers this way:
#MainBannerController.class
#Controller
#RequestMapping("admin/banners/main/")
#AllArgsConstructor(onConstructor = #__(#Autowired))
public class MainBannerController {
...
#BackgroundBannerController.class
#Controller
#RequestMapping("admin/banners/background/")
#AllArgsConstructor(onConstructor = #__(#Autowired))
public class MainBannerController {
...
#NewsBannerController.class
#Controller
#RequestMapping("admin/banners/news/")
#AllArgsConstructor(onConstructor = #__(#Autowired))
public class MainBannerController {
...
Moreover, how to get 3 different models for one view?
#BannerController.class ???
#Controller
#RequestMapping("admin/banners/main/")
#AllArgsConstructor(onConstructor = #__(#Autowired))
public class MainBannerController {
private final MainBannerService mainBannerService;
private final MainBannerService mainBannerService;
private final MainBannerService mainBannerService;
// How to get 3 different models for one view?
#GetMapping({"/", ""})
public ModelAndView allBanners() {
// new ModelAndView("/admin/banners/index", "mainBanners", mainBannerService.getAllMainBanners());
// new ModelAndView("/admin/banners/index", "backgroundBanners", backgroundBannerService.getAllBackgroundBanners());
// new ModelAndView("/admin/banners/index", "newsBanners", newsBannerService.getAllNewsBanners());
return null;
}

create your modelAndView
ModelAndView modelAndView = new ModelAndView("/admin/banners/index");
then add as many object as you want, under a different name each
modelAndView.addObject("mainBanners", mainBannerService.getAllMainBanners());
modelAndView.addObject("backgroundBanners",mainBannerService.getAllBackgroundBanners());
return modelAndView;

There is no any rule that you have to create different controllers for different URI but if you create different one that will be easy to understand where you mapped each of them.
If you are creating only one controller
Use #RestController and #RequestMapping("/your uri") for every different method.

Related

Spring Boot can't access REST Controller

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.

Spring mvc - which layer should convert entities to dtos (and vice versa)

In which layer should DTO/Entity conversion take place.
Having following structure in a Spring Mvc application:
Controller
Service
Repository
The approach I'm using now, where service layer is #Transactional.
#RestController
public class ExampleController {
#Autowired
private ExampleService exampleService;
#Autowired
private ExampleMapper exampleMapper;
#GetMapping("/examples")
public ResponseEntity<List<ExamleDto>> getAll() {
var examples = exampleService.getAll();
return ResponseEntity.ok(exampleMapper.examplesToExampleDtos(examples));
}
#PostMapping("/examples")
public ResponseEntity<Void> create(#RequestBody #Valid ExampleCreateDto createDto) {
var example = exampleService.create(createDto)
return ResponseEntity.created(URI.create("examples/" + example.getId()).build();
}
// PUT, DELETE, ...
}
#Service
#Transactional
public class ExampleService {
#Autowired
private ExampleRepository exampleRepository;
#Autowired
private ExampleMapper exampleMapper;
public List<Examle> getAll() {
var examples = exampleRepository.findAll();
return examples;
}
public void create(ExampleDto exampleDto) {
var example = exampleMapper.asExample(exampleDto);
return exampleRepository.save(example);
}
}
public interface ExampleRepository extends JpaRepository<Example, Long> {
Why I choose this aproach:
The service layer is transactional, so whenever we get back to the controller, all changes will be flushed (version field for example) will all be set.
It makes you think about your entitygraph, lets say you have a Person entity which has a list of Deparments. Lets say the PersonDto contains also the list of DeparmentDtos, it forces you to fetch all deparments before hand or you will run into a LazyInitializationException in the controller layer.
Which in my opinion is a good thing, because if you would perform the mapping in the service you would be doing N + 1 queries (N being the number of deparments) without realizing it.
Services who need each other to perform there business tasks, work on the entity model instead of the DTO model, which might have some validation (#NotNull, #Size, ...) which only supposed to be valided when it comes from the outside, but internally not all validations should be applied.
Business rules will still be checked in the service layer as part of the service method.
The only thing here is that for update/creates service still communicate by passing dtos iso of entities.
I googled this topic a lot, but couldn't find a definitive answer.

Service injected into spring controller is not available in one of the functions

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.

Spring MVC - #Scope("session") doesn't work like intended

i'm trying to develop simple flashcards app using Spring MVC.
Everything went fine until i tried to test it with multiple users.
I wanted every user to log in and assumed that #Session("scope") will create every bean anew for every user. It seems to me that this doesn't work - i logged in on 3 different devices and it is all connected, when i log in on one computer the same is logged on the second and third. When i log out - all computers are logged out.
I'm confused - please help:
#Controller
#Scope("session")
#SessionAttributes({"logged", "LoggedUser"})
#RequestMapping("/")
public class IndexController {
public static NewUser loggedUser;
#Autowired
private NewUserRepository newUserRepository;
#Autowired
private FlashcardRepository flashcardRepository;
(.....)
maybe it has something to do with this static variable - i want it to be separate for every browser session.
#Scope annotation is for making whole bean session aware with all it's properties. #SessionAttributes makes your model object session aware, which you need to populate and manage manually (init for example).
As #Sotirios Delimanolis wrote, you should learn the difference between static and non-static properties.
If I'm corrent, you're looking for something like this:
#Controller
#SessionAttributes({IndexController.LOGGED_USER_MODEL })
#RequestMapping("/")
public class IndexController {
private static final String LOGGED_USER_MODEL = "loggedUser";
public NewUser loggedUser;
#Autowired
private NewUserRepository newUserRepository;
#Autowired
private FlashcardRepository flashcardRepository;
#ModelAttribute(LOGGED_USER_MODEL)
public NewUser initLoggedUser() {
//this is to make sure that our session attribute model is always populated, otherwise Spring throws exception
return new NewUser();
}
#RequestMapping
public Model someMethod(#ModelAttribute(LOGGED_USER_MODEL) NewUser loggedUser) {
//do something with logged user, populete with repository for example
}
(...)
if (loggedUser!=null){
model.addAttribute("LoggedUser", loggedUser.getName());}
if (loggedUser==null){
model.addAttribute("LoggedUser", "not logged");}
return "index";
}
(...)
#RequestMapping(value = "/logUser", method = RequestMethod.POST)
public String logUser(#ModelAttribute NewUser newUser, BindingResult result, ModelMap model) {
if (!newUserRepository.findByName(newUser.getName()).isEmpty()) {
loggedUser=newUserRepository.findByName(newUser.getName()).get(0);
DrillController.queueKeys=null;
DrillController.cards.clear();
DrillController.wrongAnswers=null;
if (loggedUser.getNumberOfRepsSetting()!=null) {
DrillController.NumberOfReps = Integer.parseInt(loggedUser.getNumberOfRepsSetting());
}
}
return "redirect:/";
}
Maybe there is something wrong with this? i'm not sure if this static field is created anew with every session?
Sorry for all that confusion - I just want that this static field is null with every new browser session. I understand how static works in plain java, but i'm confused how it works in Spring context with beans and multiple users. Am i thinking correctly here?
Ok - i know what is wrong... every time the new bean is created (with session scope) the static field remains the same. i should store login-data in non-static variable... sorry for confusion and thank you!

Invoke controller method from java class

I just want to know whether controller class method is accessible from another java class.
following is my controller and its method.
#Controller
public class TestResultUploadController {
#RequestMapping(method = RequestMethod.POST,value="/uploadTestResult")
public #ResponseBody
String uploadTestResult(String testResultBarcode,int deviceLoc) {
//some code goes here
return something;
}
I just want to call this controller method from another java class.
How can I make it work?
please suggest..
Short answer: yes, it is possible. In your other class/thread, you can do
// this will create a new instance of that controller where no fields are wired/injected
TestResultUploadController controller = new TestResultUploadController();
controller.uploadTestResult("someString", 1234);
However, keep in mind that your setup is highly unusual and all your autowired fields wouldn't be wired correctly. If you obtain your controller from the context, you'd be able to have your fields wired/injected properly:
// obtain controller bean from context, should have fields wired properly
TestResultUploadController controller = ctx.getBean(TestResultUploadController.class);
controller.uploadTestResult("someString", 1234);
or you can, in your other class, have:
#Autowired private TestResultUploadController controller;
....
public void doStuff(){
controller.uploadTestResult("someString", 1234);
}
Again, this is highly unusual, but highly possible. However, just cause something is possible to be done, doesn't mean you should do it. I would recommend the more common Spring/MVC approach in which you outsource the business logic to Services. Basically, to have something like this:
#Controller
public class TestResultUploadController {
#Autowired private UploadTestResultService uploadTestResultService;
#RequestMapping(method = RequestMethod.POST,value="/uploadTestResult")
public #ResponseBody String uploadTestResult(String testResultBarcode,int deviceLoc) {
return uploadTestResultService.uploadTestResult(testResultBarcode, deviceLoc);
}
}
And in your thread:
//somehow get the service
UploadTestResultService uploadTestResultService = //somehowGetTheService (whether from context or in some other way)
uploadTestResultService.uploadTestResult(testResultBarcode, deviceLoc);
That way, you'd be able to mock the UploadTestResultService in your tests of the controller, and you'd also be able to test the uploadTestResult method of that service on its own without it being in a controller.
EDIT:
How to obtain the Spring context is outside of the scope of this question. I assume you know basic Spring and also basic java.

Categories

Resources