Spring Boot 2.6 and Angular in static Resources

Viewed 743

My question is very similar to this question that has already been asked and answered but is not 100% up-to-date.

We used the solution from Chris Gaskill for quite some time and it suited us perfectly because we wanted to redirect requests that contain more than one path segment (i.e. /foo/bar)

From Spring Boot 2.4 on, Boot uses the PathPatternParser instead of the AntPathMatcher, wherein the former does not support ** at the start of a pattern anymore (see docs).

Is there some other solution to get the same behavior? What do you use to redirect all requests, that did not match anything else, to the index.html of the Angular app?

This is the code of the controller that forwards the requests.

@Controller
class SpaRoutingController {

  @GetMapping("/**/{path:[^\\.]*}", headers = "X-Requested-With!=XMLHttpRequest")
  fun forward(): String? {
      return "forward:/"
  }
}
5 Answers

I would rather go solution where I configure a WebMvcConfigurer instead. Please see below for code.

Maven Configuration

Maven configuration directs frontend-maven-plugin to build angular and copy built content into target/static directory, so that it is available in final WAR and JAR built. Then I additionally configure a WebMvcConfigurer to find a resource in static directory of JAR / WAR, if found - send it to client, otherwise redirect to index.html.

<plugin>
        <artifactId>maven-clean-plugin</artifactId>
        <version>${maven-clean-plugin.version}</version>
        <configuration>
          <filesets>
            <fileset>
              <directory>src/main/webapp</directory>
              <includes>
                <include>*.js</include>
                <include>*.txt</include>
                <include>*.html</include>
                <include>*.css</include>
                <include>*.map</include>
                <include>*.json</include>
                <include>*.ico</include>
                <followSymlinks>false</followSymlinks>
              </includes>
            </fileset>
            <fileset>
              <directory>src/main/webapp/assets</directory>
              <includes>
                <include>**</include>
                <followSymlinks>false</followSymlinks>
              </includes>
            </fileset>
          </filesets>
        </configuration>
        <executions>
          <execution>
            <id>auto-clean</id>
            <phase>initialize</phase>
            <goals>
              <goal>clean</goal>
            </goals>
          </execution>
        </executions>
      </plugin>

      <plugin>
        <artifactId>maven-resources-plugin</artifactId>
        <executions>
          <execution>
            <id>copy-resources</id>
            <phase>process-resources</phase>
            <goals>
              <goal>copy-resources</goal>
            </goals>
            <configuration>
              <outputDirectory>${project.build.directory}/classes/static</outputDirectory>
              <includeEmptyDirs>true</includeEmptyDirs>
              <resources>
                <resource>
                  <directory>${basedir}/src/main/${angular.project.name}/dist/${angular.project.name}</directory>
                  <filtering>true</filtering>
                </resource>
              </resources>
            </configuration>
          </execution>
        </executions>
      </plugin>

      <plugin>
        <groupId>com.github.eirslett</groupId>
        <artifactId>frontend-maven-plugin</artifactId>
        <version>1.8.0</version>
        <configuration>
          <workingDirectory>src/main/${angular.project.name}</workingDirectory>
        </configuration>
        <executions>
          <execution>
            <id>install node and npm</id>
            <goals>
              <goal>install-node-and-npm</goal>
            </goals>
            <phase>generate-resources</phase>
            <configuration>
              <nodeVersion>v14.16.1</nodeVersion>
            </configuration>

          </execution>
          <execution>
            <id>npm build angular</id>
            <goals>
              <goal>npm</goal>
            </goals>
            <phase>generate-resources</phase>
            <configuration>
              <arguments>run build:prod</arguments>
            </configuration>
          </execution>
        </executions>
      </plugin>

MVC Configuration

Here you try to find if a resource exists in your static directory, if not redirect it to index.html.

You may have to change source location as per your requirement.

@Configuration
public class ApplicationWebMvcConfiguration implements WebMvcConfigurer {
  @Override
  public void addResourceHandlers(ResourceHandlerRegistry registry) {

    registry.addResourceHandler("/**")
            .addResourceLocations("classpath:/static/")
            .resourceChain(true)
            .addResolver(new PathResourceResolver() {
              @Override
              protected Resource getResource(String resourcePath, Resource location)
                  throws IOException {

                Resource requestedResource = location.createRelative(resourcePath);
                if (requestedResource.exists() && requestedResource.isReadable()) {
                  return requestedResource;
                }

                return new ClassPathResource("/static/index.html");

              }
            });

  }

}

PathPatternParser equivalent to previous AntPathMatcher /** would be:

@GetMapping("/{*path}")

according to spring documentation:

/resources/{*path} — matches all files underneath the /resources/, as well as /resources, and captures their relative path in a variable named "path"; /resources/image.png will match with "path" → "/image.png", and /resources/css/spring.css will match with "path" → "/css/spring.css"

Edit - relating to loop:

in your case in order to avoid the loop due to the use of forward:/ if you only care about loading your angular page just replace forward with the actual content of your index.html

if for some reason you need to forward in order to flatten the uri before returning the response you can solve it in various manners you can either forward:/index.html @GetMapping("/index.html") which should have precedence over wildcard or use @PathVariable in order to differentiate behavior and avoid eternal forward loop

@Controller
class SpaRoutingController {

  @GetMapping("/{*path}")
  fun forward(@PathVariable(value="path", required=false) path: String): String? {
      if ("/".equals(path)) { // may need to handle additional cases
         return ""; // or your index.html
      }
      return "forward:/";
  }
}

Have you tried to implement redirection on Angular side? In my application I've solved this like:

const routes: Routes = [
  {path: '', redirectTo: '/myProjects', pathMatch: 'full'},
  //...
  {path: '**', redirectTo: ''}
}

@NgModule({
  imports: [
    RouterModule.forRoot(routes, {useHash: true})
  ],
  exports: [
    RouterModule
  ],
  declarations: []
})
export class MyRoutingModule {
}

While using Angular you can not talk about pages such as index.html. What you have it to a route !

So when you redirect, you should do is to a route not a page.

ModelAndView forward() {
        return new ModelAndView("redirect:/");
    }

PS : If / is your default route.

Have you tried using a Servlet Filter to handle endpoints not handled by Spring Boot? This article from Baeldung explains a solution which you could check for your use case.

Related