@WebServlet annotation doesn't work with Tomcat 8

I want to use the @WebServlet annotation in a Java EE webapp which runs on Tomcat 8.

I have read that I need to declare Servlet Version 3.1 in my web.xml and that my Servlet needs to extend HttpServlet. I did all that but still the @WebServlet doesn't work. I am getting a HTTP 404.

I also tried my configuration with metadata-complete="false" in my web.xml, but still no success.

Here is my web.xml and Servlet.

The complete sample code can be found on GitHub.

web.xml

<?xml version="1.0" encoding="UTF-8"?>
<web-app 
  version="3.1" 
  xmlns="http://xmlns.jcp.org/xml/ns/javaee" 
  xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" 
  xsi:schemaLocation="http://xmlns.jcp.org/xml/ns/javaee http://xmlns.jcp.org/xml/ns/javaee/web-app_3_1.xsd">

  <context-param>
    <param-name>facelets.DEVELOPMENT</param-name>
    <param-value>true</param-value>
  </context-param>

  <!-- http://stackoverflow.com/a/7924117/451634 -->
  <!-- Put "-1" to disable this feature -->
  <context-param>
    <param-name>facelets.REFRESH_PERIOD</param-name>
    <param-value>1</param-value>
  </context-param>

  <servlet>
    <servlet-name>Faces Servlet</servlet-name>
    <servlet-class>javax.faces.webapp.FacesServlet</servlet-class>
    <load-on-startup>1</load-on-startup>
  </servlet>

  <servlet-mapping>
    <servlet-name>Faces Servlet</servlet-name>
    <url-pattern>*.xhtml</url-pattern>
  </servlet-mapping>

  <welcome-file-list>
    <welcome-file>index.xhtml</welcome-file>
  </welcome-file-list>

  <!-- JSF -->
  <listener>
    <listener-class>org.apache.myfaces.webapp.StartupServletContextListener</listener-class>
  </listener>   

  <!-- CDI -->
  <listener>
    <listener-class>org.apache.webbeans.servlet.WebBeansConfigurationListener</listener-class>
  </listener>

</web-app>

TestServlet.java

import java.io.IOException;
import javax.servlet.ServletException;
import javax.servlet.ServletOutputStream;
import javax.servlet.annotation.WebServlet;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

@WebServlet(name = "TestServlet", urlPatterns = {"*.serve"})
public class TestServlet extends HttpServlet {

  @Override
  protected void doGet(HttpServletRequest req, HttpServletResponse resp)
          throws ServletException, IOException {
    try (ServletOutputStream out = resp.getOutputStream()) {
      out.write("Hello World".getBytes());
      out.flush();
    }
  }

}

I got it working. I had to extend the way I started my Tomcat 8.0.12 server.

Nevertheless, there are three main things that must be done:

  1. web-app version in web.xml has to be at least 3.0 (I used 3.1)
  2. metadata-complete in web.xml may not be true (default is "false")
  3. classes directories have to be added to the embedded Tomcat before start

Here is an example for the web.xml file:

<?xml version="1.0" encoding="UTF-8"?>
<web-app 
  version="3.1" 
  metadata-complete="false"  
  xmlns="http://xmlns.jcp.org/xml/ns/javaee" 
  xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" 
  xsi:schemaLocation="http://xmlns.jcp.org/xml/ns/javaee http://xmlns.jcp.org/xml/ns/javaee/web-app_3_1.xsd">

  <welcome-file-list>
    <welcome-file>index.html</welcome-file>
  </welcome-file-list>

</web-app>

This is how my Tomcat main class looks like (which supports now @WebServlet annotations):

public static void main(String[] args) throws Exception {
  String contextPath = "/";
  String webappDirLocation = "src/main/webapp/";
  String baseDirectory = new File(webappDirLocation).getAbsolutePath();

  Tomcat tomcat = new Tomcat();
  tomcat.setPort(8080);
  StandardContext context = (StandardContext) tomcat.addWebapp(contextPath, baseDirectory);

  // Additions to make @WebServlet work
  String buildPath = "target/classes";
  String webAppMount = "/WEB-INF/classes";

  File additionalWebInfClasses = new File(buildPath);
  WebResourceRoot resources = new StandardRoot(context);
  resources.addPreResources(new DirResourceSet(resources, webAppMount, additionalWebInfClasses.getAbsolutePath(), contextPath));
  context.setResources(resources);
  // End of additions

  tomcat.start();
  tomcat.getServer().await();
}