Spring Boot with AngularJS html5Mode
Asked Answered
S

9

17

I start my web application with spring boot. It use a simple main class to start an embedded tomcat server:

@Configuration
@EnableAutoConfiguration
@ComponentScan
public class Application {

    public static void main(String[] args) {
        SpringApplication.run(Application.class, args);
    }

}

I want to configure the server in the way that he can handle angularjs html5mode that will be activated with

$locationProvider.html5Mode(true);

Relevant postings from other users shows that you need to redirect to the root. the html5 mode remove the hashbag from the url. If you refresh the page the server doesnt find the page cause he do not handle the hash. see: AngularJS - Why when changing url address $routeProvider doesn't seem to work and I get a 404 error

Scrivner answered 19/7, 2014 at 7:5 Comment(1)
I don't think there's enough information there to know what you did, and why it didn't work.Parks
S
11

I found a solution I can live with it.

@Controller
public class ViewController {

    @RequestMapping("/")
    public String index() {
        return "index";
    }

    @RequestMapping("/app/**")
    public String app() {
        return "index";
    }
}

The angularjs app has to be under the subdomain app. If you do not want that you could create a subdomain like app.subdomain.com that mapps to your subdomain app. With this construct you have no conflicts with webjars, statis content and so on.

Scrivner answered 29/9, 2014 at 12:43 Comment(2)
Could you elaborate how this works? For me it returns only the string "index"! my index.html is under resources/static/appAbattoir
The purpose of the html5mode is to avoid using a # prefix. You're replacing it with a app prefix, so what is the value. Better just use the hash based routing strategy.Inextirpable
T
27

Use this controller to forward the URI to index.html in order to preserve AngularJS routes. Source https://spring.io/blog/2015/05/13/modularizing-the-client-angular-js-and-spring-security-part-vii

@Controller
public class ForwardController {

    @RequestMapping(value = "/**/{[path:[^\\.]*}")
    public String redirect() {
        // Forward to home page so that route is preserved.
        return "forward:/";
    }
} 

In this solution ForwardController forwards only paths, which are not defined in any other Controller nor RestController. It means if you already have:

@RestController
public class OffersController {

    @RequestMapping(value = "api/offers")
    public Page<OfferDTO> getOffers(@RequestParam("page") int page) {
        return offerService.findPaginated(page, 10);
    }
} 

both controllers are going to work properly - @RequestMapping(value = "api/offers") is checked before @RequestMapping(value = "/**/{[path:[^\\.]*}")

Tuning answered 30/6, 2017 at 16:15 Comment(6)
What does that Regex in the value "say"?Winshell
Match everything without a suffix (so not a static resource)Tuning
The great thing about this vs some other options is this option preserves routes like /login from the security controller.Ey
This solution helped in my case.Sarmatia
Why does the regex have 2 opening braces but only 1 closing brace?Pronoun
It seems this won't work if you have multiple apps. I have one located at / and two more on /app-1/index.html and /app-2/index.html. Can this be adapted to something like this?Abbate
A
22

I had same problem. As far as I know, in html5 mode, angularjs don't resolve hash but entered url or url added through pushState.

The problem was that PathResourceResolver map directories but not files. Because it intended to serve requested files from directory but not to rewrite urls. For app it's mean, if you refresh your browser window or type url like http://example.com/mystate, it's query "/mystate" from the server. If spring don't know url, they return 404. One of the solutions is map every possible state to index.html like here (source, btw look at webjars - it's great!). But in my case I can safely map "/**" to index.html and therefore my solution is to override PathResourceResolver#getResource:

@Configuration
@EnableConfigurationProperties({ ResourceProperties.class })
public class WebMvcConfig extends WebMvcConfigurerAdapter {

    @Autowired
    private ResourceProperties resourceProperties = new ResourceProperties();

    @Override
    public void addResourceHandlers(ResourceHandlerRegistry registry) {
        Integer cachePeriod = resourceProperties.getCachePeriod();

        registry.addResourceHandler("/static/**")
                .addResourceLocations("classpath:/static/")
                .setCachePeriod(cachePeriod);

        registry.addResourceHandler("/**")
                .addResourceLocations("classpath:/static/index.html")
                .setCachePeriod(cachePeriod).resourceChain(true)
                .addResolver(new PathResourceResolver() {
                    @Override
                    protected Resource getResource(String resourcePath,
                            Resource location) throws IOException {
                        return location.exists() && location.isReadable() ? location
                                : null;
                    }
                });
    }
}
Agoraphobia answered 27/9, 2014 at 10:3 Comment(3)
I can't get this to work with other static resources included within index.html i.e. JS and CSS resources. For example I have another css file in my static folder and I link to it within the head section of my index.html file. When I look at that file in Chrome Dev Tools, it's contents are index.html.Mell
I ended up manually mapping the angular routes to a specific controller that forwards to index.html as per your linked source. It works fine and is clear and easy to understand :)Scuba
@SébastienTromp How to manually map routes to specific controller? I'm new to spring boot. My requirement is that when I hit the URL in browser, it should load the specific angular component in browser.Morph
S
11

I found a solution I can live with it.

@Controller
public class ViewController {

    @RequestMapping("/")
    public String index() {
        return "index";
    }

    @RequestMapping("/app/**")
    public String app() {
        return "index";
    }
}

The angularjs app has to be under the subdomain app. If you do not want that you could create a subdomain like app.subdomain.com that mapps to your subdomain app. With this construct you have no conflicts with webjars, statis content and so on.

Scrivner answered 29/9, 2014 at 12:43 Comment(2)
Could you elaborate how this works? For me it returns only the string "index"! my index.html is under resources/static/appAbattoir
The purpose of the html5mode is to avoid using a # prefix. You're replacing it with a app prefix, so what is the value. Better just use the hash based routing strategy.Inextirpable
R
4

A small adjustment to a previous code which works to me.

// Running with Spring Boot v1.3.0.RELEASE, Spring v4.2.3.RELEASE
@Configuration
@EnableConfigurationProperties({ ResourceProperties.class })
public class WebMvcConfig extends WebMvcConfigurerAdapter {

@Autowired
private ResourceProperties resourceProperties = new ResourceProperties();

@Override
public void addResourceHandlers(ResourceHandlerRegistry registry) {
    Integer cachePeriod = resourceProperties.getCachePeriod();

    final String[] staticLocations = resourceProperties.getStaticLocations();
    final String[] indexLocations  = new String[staticLocations.length];
    for (int i = 0; i < staticLocations.length; i++) {
        indexLocations[i] = staticLocations[i] + "index.html";
    }
    registry.addResourceHandler(
            "/**/*.css",
            "/**/*.html",
            "/**/*.js",
            "/**/*.json",
            "/**/*.bmp",
            "/**/*.jpeg",
            "/**/*.jpg",
            "/**/*.png",
            "/**/*.ttf",
            "/**/*.eot",
            "/**/*.svg",
            "/**/*.woff",
            "/**/*.woff2"
            )
            .addResourceLocations(staticLocations)
            .setCachePeriod(cachePeriod);

    registry.addResourceHandler("/**")
            .addResourceLocations(indexLocations)
            .setCachePeriod(cachePeriod)
            .resourceChain(true)
            .addResolver(new PathResourceResolver() {
                @Override
                protected Resource getResource(String resourcePath,
                        Resource location) throws IOException {
                    return location.exists() && location.isReadable() ? location
                            : null;
                }
            });
}

}

Rajab answered 6/4, 2016 at 9:32 Comment(0)
C
4

You can forward all not found resources to your main page by providing custom ErrorViewResolver. All you need to do is to add this to your @Configuration class:

@Bean
ErrorViewResolver supportPathBasedLocationStrategyWithoutHashes() {
    return new ErrorViewResolver() {
        @Override
        public ModelAndView resolveErrorView(HttpServletRequest request, HttpStatus status, Map<String, Object> model) {
            return status == HttpStatus.NOT_FOUND
                    ? new ModelAndView("index.html", Collections.<String, Object>emptyMap(), HttpStatus.OK)
                    : null;
        }
    };
}
Coworker answered 27/10, 2016 at 9:45 Comment(0)
L
3

I finally get my Angular 5 application working with spring boot with or without spring-boot-starter-tomcat as provided (embedded) or not!

/**
 * Needed for html5mode (PathLocationStrategy in Angular). Every path except api/* and resources (css, html, js, woff, etc..)
 * should be redirect to index.html and then should angular managed routes (which could be correct or non existing).
 */
@RestController
@RequestMapping
public class ForwardController {

    @GetMapping(value = "/**/{[path:[^\\.]*}")
    public ModelAndView forward() {
        return new ModelAndView("/index.html");
    }
}
Leroylerwick answered 19/1, 2018 at 10:15 Comment(1)
Are you sure this works, how @RestController can return a view?Gaither
J
0

I just encountered the similar issue where I wanted to configure Resources and at the same time I wanted to use AngularJS Html5 mode enabled.

In my case my static files were served from /public route so I used the following request mapping on my index action and it all works fine.

@RequestMapping(value = {"", "/", "/{[path:(?!public).*}/**"}, method = GET)
public String indexAction() {
    return "index";
}
Justino answered 7/8, 2016 at 9:3 Comment(0)
R
0

I had the same problem while using angular Html5Mode. The solution that worked for me was to configure error page for 404 in web.xml assigning the path to my Index view in my case "/".

<error-page>
    <error-code>404</error-code>
    <location>/</location>
</error-page>

Similarly, you can try configuring error page in spring boot. for reference, you can check this link.

Spring boot and custom 404 error page

Roofer answered 18/2, 2017 at 18:55 Comment(1)
It works but the response status code will be 404, that's not nice. I will use this as a bypass. ThanksTaurine
S
0

1- first you create new Controller then copy and paste simple below code

@Controller
public class viewController {

 @RequestMapping(value = "/**/{[path:[^\\.]*}")
 public String redirect() {
    // Forward to home page so that route is preserved.
    return "forward:/";
 }

}

3- remove 2 below item from angular app

$locationProvider.hashPrefix('!');
$urlRouterProvider.otherwise("/");

2- in angular application you must add $locationProvider.html5Mode(true); to app route

3- Don't forget to place the base tag before any http request in your index.html file

<head>
<base href="/"> /* Or whatever your base path is */

//call every http request for style and other 
...
</head>

it's work fine for me

Schlessinger answered 23/4, 2018 at 10:26 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.