I'm developing a system using Spring injection. During a certain point, there's a warning on the screen that is shown using an attribute on a WarningHelper class, which is a Spring Controller. Following is summarized code:
#Controller
#Scope(WebApplicationContext.SCOPE_SESSION)
public class WarningHelper implements Serializable {
//Bunch of attributes
private String warningText;
//Bunch of other methods
private String configureWarning(Integer caller, Outcome outcome, WarningReturn warningReturn, GlobalWebUser user) {
//business logic
if (hasWarning()) {
setWarningText(warningReturn.getWarningText());
}
return redirect;
}
}
That part works perfectly. Later on, a xhtml page shows this warning using another controller in which this first one is injected. Following is the edited code for the second controller:
#Controller
#Scope(WebApplicationContext.SCOPE_APPLICATION)
public class CustomUrlController {
//Bunch of other attributes
#Inject
private WarningHelper warningHelper;
//Bunch of methods
public String getAllMessages() {
String completeMessage = "";
//business logic creating the message
completeMessage += warningHelper.getWarningText();
return complete Message
}
}
This all works just fine the first time around. The problem is that if I try then to enter another profile which has a different message, the first one is still shown. Note that this change process does not involve another login, so the session is still the same. I have tried toying around with the scope, but to no avail.
Change the #Scope(WebApplicationContext.SCOPE_SESSION) as #Scope(WebApplicationContext.SCOPE_REQUEST) in both WarningHelper and CustomUrlController class. This will instantiate the CustomUrlController and warningHelper for every request.
Related
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 would like to ask you for some best practice to reduce the amount of repeatable code in my controller's methods - one of them is presented below.
I have quite a big and complex view with a lot of information and two forms. In every method of my controller (and there are quite a few) I have to pass the same attributes to my view (in post controllers even twice). I added a note SAME CODE in the code snippet below indicating identical pieces of code.
I wonder if there is any possibility to make a one global method in the controller gathering all attributes to be passed to the model and just reference it in any of particular methods?
I looked into ModelAndView or ModelMap, but cannot see those being suitable here.
Just want to avoid repeating this part:
Repeatable piece of code
model.addAttribute("hotels", hotelService.getAllHotels());
List<GetRoomDto> roomsDto = roomService.getAllRoomsByHotelId(hotelId);
model.addAttribute("rooms", roomsDto);
model.addAttribute("roomTypes", roomTypeService.findAllRoomTypeNames());
Full method with that piece of code appearing twice
#PostMapping("/hotels/{hotelId}/rooms")
public String createRoomForHotelById(#ModelAttribute("room") #Valid NewRoomDto roomDto,
BindingResult result,
#PathVariable("hotelId") Long hotelId,
Model model) {
if(result.hasErrors()) {
// SAME CODE
model.addAttribute("hotels", hotelService.getAllHotels());
List<GetRoomDto> roomsDto = roomService.getAllRoomsByHotelId(hotelId);
model.addAttribute("rooms", roomsDto);
model.addAttribute("roomTypes", roomTypeService.findAllRoomTypeNames());
//
model.addAttribute("hotel", new NewHotelDto());
LOG.info("Binding error: {}", result.toString());
return "admin/dashboard";
}
// SAME CODE
model.addAttribute("hotels", hotelService.getAllHotels());
List<GetRoomDto> roomsDto = roomService.getAllRoomsByHotelId(hotelId);
model.addAttribute("rooms", roomsDto);
model.addAttribute("roomTypes", roomTypeService.findAllRoomTypeNames());
//
LOG.info("AdminController: CreateRoomForHotelById: Created room: {}", roomDto.toString());
roomDto.setHotelId(hotelId);
roomService.createNewRoom(roomDto);
return "redirect:/auth/admin/hotels/{hotelId}/rooms";
}
You can also move the code from J Asgarov's answer into the same controller instead of another class annotated with #ControllerAdvice. That way that code will only be executed for #RequestMapping methods within that controller.
For multiple values you could also do something like this:
#ModelAttribute
public void foo(Model model, #PathVariable(required = false) Long hotelId) {
model.addAttribute("hotels", hotelService.getAllHotels());
if (hotelId != null) {
List<GetRoomDto> roomsDto = roomService.getAllRoomsByHotelId(hotelId);
model.addAttribute("rooms", roomsDto);
}
model.addAttribute("roomTypes", roomTypeService.findAllRoomTypeNames());
}
But seeing your code I would rather suggest you move the repeated code into a private method and call it whenever you need those inside your model.
Your method createRoomForHotelById for example causes a redirect, which basically discards everything you put in your model.
For global model attributes you can use #ControllerAdvice:
Create a class and annotate it with #ControllerAdvice.
Inside of that class pass the model attribute (which will now be available globally) like so:
#ModelAttribute("foo")
public Foo foo() {
return new Foo();
}
I'm looking for a way to take advantage of Spring's Scopes(Prototype or Request) while being able to get the state of these scoped components.
Given the following example
#RestController
#Scope("prototype")
public class FooController {
private FooService fooService;
#Autowired
public FooController(FooService fooService) {
this.fooService = fooService;
}
#RequestMapping("/foo")
public String foo() {
fooService.foo();
return "OK";
}
#RequestMapping("/foo/status")
public int fooStatus() {
return fooService.getState();
}
}
#Service
#Scope("prototype")
public class FooService
{
public int getState() {
return state;
}
public void setState(int state) {
this.state = state;
}
private int state;
public FooService() { }
public void foo() {
//do long work
state++;
//do long work
state++;
}
}
There is a Controller and a Service, prototyped scoped. How can I get the state of FooService.
The code above isn't working. Maybe something with scopes? status is always zero.
The scenario is to hit /foo/status endpoint and get the status value.
Yes, the answer 0 is expected. Because the spring prototype scope says that you'll get a new instance of the bean, everytime it's requested.
https://www.journaldev.com/21039/spring-bean-scopes
If you use request or prototype scope, it means that with every request new service bean is created with default State variable initialised to 0.
When you first call /foo a new bean of service is created and the State is increased to 2. Next when you call /foo/status new bean with default State 0 is created and returned. Which essentially means that state can't be shared between two requests with these bean scopes.
Remove the #scope on the service and controller it should start working fine.
You are creating a business logic component which is recreated on each HTTP request and keeps no track of status.
I guess you have 2 options:
remove the scope annotation that recreates your component on each request and change your actual logic / architecture (which is as you say not what you want),
define the status variable as static (not recommended). When 2 services run simultaneously your foo method you have to make your incrementation of status thread-save.
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!
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.