I have a Client-Server application using SpringBoot and Angular2.
I would like to load a image from the server by filename. This works fine.
I store the attribute image:string at the client and I place it in the template again.
You might pay attention to return res.url;; I do not use the actual ressource, which might be wrong.
My objective is that image is cached. To my understanding the web-browser can automatically cache the images. Correct?
But the caching does not work yet and maybe somebody could give me a hint what needs to be adjusted?
Is a different header required?
Server (SpringBoot)
public class ImageRestController {
#RequestMapping(value = "/getImage/{filename:.+}", method = RequestMethod.GET)
public ResponseEntity<Resource> getImage(#PathVariable String filename) {
try {
String path = Paths.get(ROOT, filename).toString();
Resource loader = resourceLoader.getResource("file:" + path);
return new ResponseEntity<Resource>(loader, HttpStatus.OK);
} catch (Exception e) {
return new ResponseEntity<Resource>(HttpStatus.NOT_FOUND);
}
}
}
Client (Angular2)
#Component({
selector: 'my-image',
template: `
<img src="{{image}}"/>
`
})
export class MyComponent {
image:string;
constructor(private service:MyService) {}
showImage(filename:string) {
this.service.getImage(filename)
.subscribe((file) => {
this.image = file;
});
}
}
export class MyService() {
getImage(filename:String):Observable<any> {
return this.http.get(imagesUrl + "getImage/" + filename)
.map(this.extractUrl)
.catch(this.handleError);
}
extractUrl(res:Response):string {
return res.url;
}
}
You could do something like this on the server side (and perhaps add an ETag or Last-Modified header if you can get that information):
return ResponseEntity
.ok()
.cacheControl(CacheControl.maxAge(30, TimeUnit.DAYS))
.body(loader);
See the HTTP caching part of the reference documentation in Spring.
If you're just serving resources and not applying any additional logic, then you'd better do the following:
#Configuration
public class WebConfig extends WebMvcConfigurerAdapter {
#Override
public void addResourceHandlers(ResourceHandlerRegistry registry) {
registry.addResourceHandler("/getImage/**")
.addResourceLocations("classpath:/path/to/root/")
.setCacheControl(CacheControl.maxAge(1, TimeUnit.DAYS).cachePublic());
}
}
See the other relevant part of the reference documentation. You can also apply transformations and leverage cache busting (see this section as well).
Related
I am currently writing an application in Spring Boot 2.4.0 that is required to listen on multiple ports (3, to be specific - but might be 4 in the future). The idea is that each port makes a different API available for other services/apps to connect to it.
So, for a minimal working example, I'd say we have a SpringBootApp like this:
#SpringBootApplication
public class MultiportSpringBoot {
public static void main(String[] args)
{
SpringApplication.run(MultiportSpringBoot.class, args);
}
}
Now, I'd want to have this listening on 3 different ports, say 8080, 8081, and 8082. For all (!) requests to one of these ports, a specific controller should be "in charge". One of the reasons for this requirement is that one controller needs to handle a (regular) web frontend and another an API. In case an invalid request is received, the API-controller needs to give a different error message than the frontend should. Hence, the requirement given is a clear separation.
So I imagine multiple controllers for the different ports, such as:
#Controller
public class Controller8080
{
#RequestMapping(value = "/", method = RequestMethod.GET)
public ModelAndView test8080()
{
ModelAndView modelAndView = new ModelAndView();
modelAndView.setViewName("test8080");
return modelAndView;
}
}
with similar controllers for the other ports:
#Controller
public class Controller8081
{
#RequestMapping(value = "/", method = RequestMethod.GET)
public ResponseEntity test8081()
{
JSONObject stuff = doSomeStuffForPort8081();
return new ResponseEntity<String>(stuff, HttpStatus.OK);
}
}
I hoped for an annotation similar to #RequestMapping to be able to match and fix the port numbers for the controllers, but this seems to be no option as no such annotation seems to exist.
Now, this topic seems to be a bit specific, which is probably why you don't find all too much info on the web. I found Starting Spring boot REST controller in two ports, but I can also only have ONE instance running. I looked at https://tech.asimio.net/2016/12/15/Configuring-Tomcat-to-Listen-on-Multiple-ports-using-Spring-Boot.html, but this is outdated for Spring Boot 2.4.0 and a bit bloated with JavaMelody examples.
Anyone can provide a minimum working example for a solution for this?
--
EDIT:
To clarify a bit more: I need multiple, separate RESTControllers that each handle requests on different ports. I.e. a request to domain.com:8080/ should be handled by a different controller than a request to domain.com:8081/.
As an example, consider the two following controllers that should handle requests on ports 8080 and 8081 respectively:
//controller for port 8080
#RestController
public class ControllerA
{
#GetMapping("/")
String helloA(HttpServletRequest request)
{
return "ControllerA at port " + request.getLocalPort();
}
}
and
//controller for port 8081
#RestController
public class ControllerB
{
#GetMapping("/")
String helloB(HttpServletRequest request)
{
return "ControllerB at port " + request.getLocalPort();
}
}
The tomcat class names changed a little bit so the link you provide has the old code but it is enough for the new code. Code below shows how you can open multiple ports in spring boot 2.4
#Bean
public ServletWebServerFactory servletContainer() {
TomcatServletWebServerFactory tomcat = new TomcatServletWebServerFactory();
tomcat.addAdditionalTomcatConnectors(additionalConnector());
return tomcat;
}
private Connector[] additionalConnector() {
if (!StringUtils.hasLength(this.additionalPorts)) {
return null;
}
String[] ports = this.additionalPorts.split(",");
List<Connector> result = new ArrayList<>();
for (String port : ports) {
Connector connector = new Connector("org.apache.coyote.http11.Http11NioProtocol");
connector.setScheme("http");
connector.setPort(Integer.valueOf(port));
result.add(connector);
}
return result.toArray(new Connector[]{});
}
And for responding to different ports with different controller you can implement the logic like check getLocalPort and respond it accordingly.
#GetMapping("/hello")
String hello(HttpServletRequest request) {
return "hello from " + request.getLocalPort();
}
Or you can write a logical controller in filter. example code below
#Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain fc) throws IOException, ServletException {
HttpServletRequest req = (HttpServletRequest) request;
HttpServletResponse res = (HttpServletResponse) response;
if (req.getLocalPort() == 8882 && req.getRequestURI().startsWith("/somefunction")) {
res.sendError(HttpServletResponse.SC_FORBIDDEN);
} else {
fc.doFilter(request, response);
}
}
You can find all running example here https://github.com/ozkanpakdil/spring-examples/tree/master/multiport
This is how it looks in my local
In order to have same path with different controllers you can use #RequestMapping("/controllerNO") on top of the classes(check), NO should be number 1 , 2, otherwise spring will complain "you have same path" and will give you this exception
Caused by: java.lang.IllegalStateException: Ambiguous mapping. Cannot map 'testController2' method
com.mascix.multiport.TestController2#hello(HttpServletRequest)
to {GET [/hello]}: There is already 'testController1' bean method
Because from design spring will allow only one path to correspond to one controller, after requestmapping you can change the filter as this. Good thing about reflection you will learn very different exceptions. java.lang.NoSuchMethodException or java.lang.IllegalArgumentException
Latest code how it works in my local
I must say this approach is not right and against the design of spring, in order to have different ports with different controllers, have multiple JVMs. If you mix the logic it will be harder for you to solve future problems and implement new features.
If you have to do it in one jvm, write a service layer and call the functions separately from one controller and write a logic like below
#GetMapping("/hello")
String hello(HttpServletRequest request) {
if (request.getLocalPort() == 8888) {
return service.hellofrom8888();
}
if (request.getLocalPort() == 8889) {
return service.hellofrom8889();
}
return "no repsonse ";
}
At least this will be easy to maintain and debug. Still looks "ugly" though :)
Özkan has already provided detailed information on how to get Tomcat to listen to multiple ports by supplying your own ServletWebServerFactory #Bean based on TomcatServletWebServerFactory.
As for the mapping, how about this approach:
Add a #RequestMapping("/8080") to your controller (methods keep their specific #RequestMapping)
#Controller
#RequestMapping("/8080")
public class Controller8080
{
#RequestMapping(value = "/", method = RequestMethod.GET)
public ModelAndView test8080()
{
ModelAndView modelAndView = new ModelAndView();
modelAndView.setViewName("test8080");
return modelAndView;
}
}
Define your own RequestMappingHandlerMapping as
public class PortBasedRequestMappingHandlerMapping extends RequestMappingHandlerMapping {
#Override
protected HandlerMethod lookupHandlerMethod(final String lookupPath, final HttpServletRequest request) throws Exception {
return super.lookupHandlerMethod(request.getLocalPort() + lookupPath, request);
}
}
and use it by
#Bean
public WebMvcRegistrations webMvcRegistrationsHandlerMapping() {
return new WebMvcRegistrations() {
#Override
public RequestMappingHandlerMapping getRequestMappingHandlerMapping() {
return new PortBasedRequestMappingHandlerMapping();
}
};
}
This will attempt to map a request to /foobar on port 8080 to /8080/foobar.
Another approach is by using org.springframework.web.servlet.mvc.condition.RequestCondition which I think is cleaner https://stackoverflow.com/a/69397870/6166627
I want to serve a .yaml file via a REST endpoint with Spring, I know that it cannot be directly displayed in a browser (just talking about Chrome here), since it doesn't support display of yaml files.
I have included what I think is the necessary library for this purpose
compile group: 'com.fasterxml.jackson.dataformat', name: 'jackson-dataformat-yaml', version: '2.9.9'.
If I open the endpoint /v2/api-doc in the browser, it will prompt me, to download a file named exactly as the endpoint /v2/api-doc. It contains the correct content.
Question: Is there a way to correctly transfer the .yaml file, so that the user will be prompted to safe myfile.yaml?
#RequestMapping(value = "/v2/api-doc", produces = "application/x-yaml")
public ResponseEntity<String> produceApiDoc() throws IOException {
byte[] fileBytes;
try (InputStream in = getClass().getResourceAsStream("/restAPI/myfile.yaml")) {
fileBytes = IOUtils.toByteArray(in);
}
if (fileBytes != null) {
String data = new String(fileBytes, StandardCharsets.UTF_8);
return new ResponseEntity<>(data, HttpStatus.OK);
} else {
return new ResponseEntity<>(HttpStatus.NOT_FOUND);
}
}
You should set a Content-Disposition header (and I recommend using ResourceLoader to load resources in Spring Framework).
Example:
#RestController
public class ApiDocResource {
private final ResourceLoader resourceLoader;
public ApiDocResource(ResourceLoader resourceLoader) {
this.resourceLoader = resourceLoader;
}
#GetMapping(value = "/v2/api-doc", produces = "application/x-yaml")
public ResponseEntity produceApiDoc() throws IOException {
Resource resource = resourceLoader.getResource("classpath:/restAPI/myfile.yaml");
if (resource.exists()) {
return ResponseEntity
.ok()
.contentType(MediaType.parseMediaType("application/x-yaml"))
.header("Content-Disposition", "attachment; filename=myfile.yaml")
.body(new InputStreamResource(resource.getInputStream()));
} else {
return new ResponseEntity<>(HttpStatus.NOT_FOUND);
}
}
}
I have some webservice endpoints that should offer json data by default. Therefore configuring as follows:
#Configuration
public class ContentNegotiationConfiguration implements WebMvcConfigurer {
#Override
public void configureContentNegotiation(ContentNegotiationConfigurer configurer) {
configurer.defaultContentType(MediaType.APPLICATION_JSON);
}
}
Problem: now I want to create an endpoint that offers a file download (thus is not json).
#RestController
public class FileServlet {
#GetMapping(value = "/docs/{filename}", consumes = MediaType.ALL_VALUE, produces = APPLICATION_OCTET_STREAM_VALUE)
public Object download(#Pathvariable filename) {
File file = fileservice.resolve(filename);
return new FileSystemResource(file);
}
}
Accessing this endpoint from the browser works fine. I can download the files.
But: when using native clients that are not setting any http headers like content-type, accept-header etc, the access fails with:
WARN o.s.w.s.m.m.a.ExceptionHandlerExceptionResolver: Resolved
[org.springframework.web.HttpMediaTypeNotAcceptableException:
Could not find acceptable representation]
All of them result in the exception:
curl localhost:8080/docs/testfile.txt
curl -O localhost:8080/docs/testfile.txt
wget localhost:8080/docs/testfile.txt
This is probably because I set the default content type to json above in ContentNegotiationConfiguration. I cannot change that due to all the other endpoints that should be json by default.
Question: how can I explicit ignore that default json setting on that single endpoint, and always just offer the download stream?
Try custom ContentNegotiationStrategy with AntPathMatcher something like:
#Override
public void configureContentNegotiation(ContentNegotiationConfigurer configurer) {
// configurer.defaultContentType(MediaType.APPLICATION_JSON,MediaType.APPLICATION_OCTET_STREAM);
configurer.defaultContentTypeStrategy(
new ContentNegotiationStrategy() {
private UrlPathHelper urlPathHelper = new UrlPathHelper();
AntPathMatcher antPathMatcher = new AntPathMatcher();
#Override
public List<MediaType> resolveMediaTypes(NativeWebRequest webRequest) throws HttpMediaTypeNotAcceptableException {
HttpServletRequest request = webRequest.getNativeRequest(HttpServletRequest.class);
if (request == null) {
return null;
}
String path = this.urlPathHelper.getLookupPathForRequest(request);
if (antPathMatcher.match("/docs/*", path)) {
return Collections.singletonList(MediaType.APPLICATION_OCTET_STREAM);
} else {
return Collections.singletonList(MediaType.APPLICATION_JSON);
}
}
});
}
With the hint from #M. Deinum, I got it working as follows:
#GetMapping(value = "/docs/{filename}")
public void download(#Pathvariable filename) {
FileSystemResource file = new FileSystemResource(fileservice.resolve(filename));
rsp.setHeader("Content-Disposition", "attachment; filename=" + file.getFilename());
ResourceHttpMessageConverter handler = new ResourceHttpMessageConverter();
handler.write(file, MediaType.APPLICATION_OCTET_STREAM, new ServletServerHttpResponse(rsp));
}
That way writing directly to the stream bypassing the content negotiation, while still relying on the Spring class ResourceHttpMessageConverter for not having to implement the response writer myself.
I have a Spring Boot web application where I catch my custom exceptions in ControllerAdvice class. The problem is that Spring Boot doesn't throw exception by default if no handler is found (it sends json back to a client).
What I want is to catch NoHandlerFoundException in my ControllerAdvice class. To make this possible I explicitly configured
spring.mvc.throw-exception-if-no-handler-found=true
spring.resources.add-mappings=false
This trick does the job and I can catch NoHandlerFoundException now but it disables Spring to auto-configure path to static resources. So all my static resources are not available for a client now. I tried to resolve this using one more configuration which doesn't help
spring.resources.static-locations=classpath:/resources/static/
Could anybody please advise how to map static resources in Spring Boot when auto-configuration was disabled with spring.resources.add-mappings=false?
Thanks!
If your static resources are limited to specific URL paths, you can configure only those paths to be handled by the Spring static resources handler. In this example, the /doc URL path is served by static resources in the /resources/static/doc/ folder in the classpath:
spring.mvc.static-path-pattern=/doc/**
spring.resources.static-locations=classpath:/resources/static/doc/
You'll need to remove this configuration:
spring.resources.add-mappings=false
I experienced the same issue and after some research, I found out that it is obviously not possible to have both options enabled (i.e. throwing NoHandlerFoundException by setting spring.mvc.throw-exception-if-no-handler-found=true AND serving static resources automatically).
Enabling the option to throw NoHandlerFoundException requires one to set spring.resources.add-mappings to false, otherwise it would not work. Furthermore, in my test setup it was not possible to disable spring.resources.add-mappings and specify the URLs for static resources manually (e.g. via application properties spring.mvc.static-path-pattern and spring.resources.static-locations or programmatically by overriding public void addResourceHandlers(ResourceHandlerRegistry registry)), because then the spring.resources.add-mappings=false setting seems to be overruled.
Finally, I implemented the following workaround for serving static resources manually via my own controller implementation:
#Controller
public class StaticWebContentController {
private Map<String, byte[]> cache = new HashMap<String,byte[]>();
#RequestMapping(value = "/css/{file}", method = RequestMethod.GET)
public ResponseEntity<byte[]> getCssFile(#PathVariable("file") String name){
ResponseEntity<byte[]> responseEntity = loadResource(".\\static\\css\\"+name,"text/css");
return responseEntity;
}
#RequestMapping(value = "/img/bootstrap-icons-1.1.0/{file}", method = RequestMethod.GET)
public ResponseEntity<byte[]> getimgFile(#PathVariable("file") String name){
ResponseEntity<byte[]> responseEntity = loadResource(".\\static\\img\\bootstrap-icons-1.1.0\\"+name,"image/svg+xml");
return responseEntity;
}
#RequestMapping(value = "/js/{file}", method = RequestMethod.GET)
public ResponseEntity<byte[]> getJsFile(#PathVariable("file") String name){
ResponseEntity<byte[]> responseEntity = loadResource(".\\static\\js\\"+name,"text/javascript");
return responseEntity;
}
private ResponseEntity<byte[]> loadResource(String path, String contentType){
HttpHeaders responseHeaders = new HttpHeaders();
responseHeaders.add("Content-Type", contentType);
if(hasCachedContent(path)){
return new ResponseEntity<byte[]>(getCachedContent(path),responseHeaders,HttpStatus.OK);
}else{
Resource resource = new ClassPathResource(path);
if(resource.exists()){
try{
InputStream inputStream = resource.getInputStream();
byte[] content = inputStream.readAllBytes();
putCache(path, content);
return new ResponseEntity<byte[]>(content,responseHeaders,HttpStatus.OK);
}catch(IOException e){
throw new ResponseStatusException(HttpStatus.INTERNAL_SERVER_ERROR,e.getMessage());
}
}else{
throw new ResponseStatusException(HttpStatus.NOT_FOUND,"The requested resource '"+path+"' does not exist'");
}
}
}
private byte[] getCachedContent(String path){
return cache.get(path);
}
private boolean hasCachedContent(String path){
return cache.containsKey(path);
}
private void putCache(String path, byte[] content){
cache.put(path, content);
}
}
In my application, I have three types of static resources located in three different sub folders. Each type is handled by a separate endpoint in order to set the Content-Type header properly. Moreover, the controller caches each resource in order to avoid to reload the requested resource from hard disk again.
Probably, this is not the best solution, however, a feasible workaround in case of my application. Any recommendations for improvement are highly appreciated!
Instead of adding below lines to config properties
spring.mvc.throw-exception-if-no-handler-found=true
spring.resources.add-mappings=false
write your custom Error Attributes as below:
#Configuration
public class CustomErrorAttributes extends DefaultErrorAttributes {
#Bean
public ErrorAttributes errorAttributes() {
return new DefaultErrorAttributes() {
#Override
public Map<String, Object> getErrorAttributes(RequestAttributes requestAttributes, boolean includeStackTrace) {
Map<String, Object> errorAttributes = super.getErrorAttributes(requestAttributes, includeStackTrace);
Map<String, Object> newErrorAttributes = new LinkedHashMap<String, Object>();
Object errorMessage = requestAttributes.getAttribute(RequestDispatcher.ERROR_MESSAGE, RequestAttributes.SCOPE_REQUEST);
if (errorMessage != null) {
newErrorAttributes.put("response-type", "error");
newErrorAttributes.put("error-code", errorAttributes.get("status"));
newErrorAttributes.put("message", errorAttributes.get("message"));
newErrorAttributes.put("error-message", errorAttributes.get("error"));
}
return newErrorAttributes;
}
};
}
}
I have, what I think to be, a very simple Spring WebSocket application. However, I'm trying to use path variables for the subscription as well as the message mapping.
I've posted a paraphrased example below. I would expect the #SendTo annotation to return back to the subscribers based on their fleetId. ie, a POST to /fleet/MyFleet/driver/MyDriver should notify subscribers of /fleet/MyFleet, but I'm not seeing this behavior.
It's worth noting that subscribing to literal /fleet/{fleetId} works. Is this intended? Am I missing some piece of configuration? Or is this just not how it works?
I'm not very familiar with WebSockets or this Spring project yet, so thanks in advance.
Controller.java
...
#MessageMapping("/fleet/{fleetId}/driver/{driverId}")
#SendTo("/topic/fleet/{fleetId}")
public Simple simple(#DestinationVariable String fleetId, #DestinationVariable String driverId) {
return new Simple(fleetId, driverId);
}
...
WebSocketConfig.java
#Configuration
#EnableWebSocketMessageBroker
public class WebSocketConfig extends AbstractWebSocketMessageBrokerConfigurer {
#Override
public void configureMessageBroker(MessageBrokerRegistry config) {
config.enableSimpleBroker("/topic");
config.setApplicationDestinationPrefixes("/live");
}
#Override
public void registerStompEndpoints(StompEndpointRegistry registry) {
registry.addEndpoint("/fleet").withSockJS();
}
}
index.html
var socket = new SockJS('/fleet');
var stompClient = Stomp.over(socket);
stompClient.connect({}, function(frame) {
// Doesn't Work
stompClient.subscribe('/topic/fleet/MyFleet', function(greeting) {
// Works
stompClient.subscribe('/topic/fleet/{fleetId}', function(greeting) {
// Do some stuff
});
});
Send Sample
stompClient.send("/live/fleet/MyFleet/driver/MyDriver", {}, JSON.stringify({
// Some simple content
}));
Even though #MessageMapping supports placeholders, they are not exposed / resolved in #SendTo destinations. Currently, there's no way to define dynamic destinations with the #SendTo annotation (see issue SPR-12170). You could use the SimpMessagingTemplate for the time being (that's how it works internally anyway). Here's how you would do it:
#MessageMapping("/fleet/{fleetId}/driver/{driverId}")
public void simple(#DestinationVariable String fleetId, #DestinationVariable String driverId) {
simpMessagingTemplate.convertAndSend("/topic/fleet/" + fleetId, new Simple(fleetId, driverId));
}
In your code, the destination '/topic/fleet/{fleetId}' is treated as a literal, that's the reason why subscribing to it works, just because you are sending to the exact same destination.
If you just want to send some initial user specific data, you could return it directly in the subscription:
#SubscribeMapping("/fleet/{fleetId}/driver/{driverId}")
public Simple simple(#DestinationVariable String fleetId, #DestinationVariable String driverId) {
return new Simple(fleetId, driverId);
}
Update:
In Spring 4.2, destination variable placeholders are supported it's now possible to do something like:
#MessageMapping("/fleet/{fleetId}/driver/{driverId}")
#SendTo("/topic/fleet/{fleetId}")
public Simple simple(#DestinationVariable String fleetId, #DestinationVariable String driverId) {
return new Simple(fleetId, driverId);
}
you can send a variable inside the path. for example i send "este/es/el/chat/java/" and obtaned in the server as "este:es:el:chat:java:"
client:
stompSession.send("/app/chat/este/es/el/chat/java/*", ...);
server:
#MessageMapping("/chat/**")
#SendToUser("/queue/reply")
public WebsocketData greeting(Message m,HelloMessage message,#Header("simpSessionId") String sessionId) throws Exception {
Map<String, LinkedList<String>> nativeHeaders = (Map<String, LinkedList<String>>) m.getHeaders().get("nativeHeaders");
String value= nativeHeaders.get("destination").getFirst().replaceAll("/app/chat/","").replaceAll("/",":");
Actually I think this is what you might be looking for:
#Autorwired
lateinit var template: SimpMessageTemplate;
#MessageMapping("/class/{id}")
#Throws(Exception::class)
fun onOffer(#DestinationVariable("id") id: String?, #Payload msg: Message) {
println("RECEIVED " + id)
template.convertAndSend("/topic/class/$id", Message("The response"))
}
Hope this helps someone! :)