Action Servlet Framework lets you create simple REST Controllers with minimum of magic.
Example API:
public class MyApiServlet extends ApiServlet {
public MyApiServlet() {
super(new MyApiController());
}
}
public class MyApiController {
@GET("/v1/api/objects")
@JsonBody
public List<SomePojo> listObjects(
@RequestParam("query") Optional<String> query,
@RequestParam("maxHits") Optional<Integer> maxHits
) {
// ... this is up to you
}
@GET("/v1/api/objects/:id")
@JsonBody
public SomePojo getObject(@PathParam("id") UUID id) {
// ... this is up to you
}
@POST("/v1/api/objects/")
@SendRedirect
public String postData(
@JsonBody SomePojo myPojo,
@SessionParameter("user") Optional<User> user
) {
// ... do your thing
return "/home/";
}
@GET("/oauth2callback")
@SendRedirect
public String establishUserSession(
@RequestParam("code") String authenticationCode,
@SessionParameter(value = "userProfile", invalidate=true) Consumer<UserProfile> setUserProfile
) {
// ...
setUserProfile.apply(newlyLoggedInUser);
return "/profile";
}
}
The magic that makes Action Controller work is the annotations like
@PathParam
and
@JsonBody
.
The set of annotations is actually extensible. Here's how
@RequestParam
is defined:
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.PARAMETER)
@HttpParameterMapping(RequestParam.RequestParameterMappingFactory.class)
public @interface RequestParam {
String value();
}
@Retention
tells javac to keep the information about the annotation available for reflection
(by default, annotations are only used by the compiler). @Target
tells javac to only allow
this annotation on method parameters (as opposed to, for example class declarations).
@HttpParameterMapping
tells
Action Controller to use this annotation to resolve the value of a action method parameter. The
RequestParam.RequestParameterMappingFactory
describes what Action Controller should do with the annotation. Here's how it's defined:
public class RequestParameterMappingFactory extends HttpRequestParameterMappingFactory<RequestParam> {
@Override
public HttpRequestParameterMapping create(RequestParam annotation, Parameter parameter) {
String name = annotation.value();
return (exchange) -> exchange.getParameter(name, parameter);
}
}
Action Servlet instantiates the mapping factory with a default constructor and invokes create, which lets the factory set up the mapping with properties from the annotation. The mapper itself takes an ApiHttpExchange (which encapsulates the HTTP request and the response) and returns the value to use for the method parameter on the action controller.
That's really all there it to it! :-)
import javax.servlet.ServletContextListener;
import javax.servlet.ServletContextEvent;
import javax.servlet.ServletContext;
import org.eclipse.jetty.server.Server;
import org.eclipse.jetty.servlet.ServletContextHandler;
public class MyServer {
public class MyListener implements ServletContextListener {
@Override
public void contextInitialized(ServletContextEvent sce) {
ServletContext context = sce.getServletContext();
context.addServlet("api", new ApiServlet(new MyApiController())).addMapping("/api/*");
}
@Override
public void contextDestroyed(ServletContextEvent sce) {
}
}
public static void main(String[] args) {
Server server = new Server(8080);
ServletContextHandler handler = new ServletContextHandler();
handler.setContextPath("/demo");
handler.addEventListener(new MyListener());
server.setHandler(handler);
server.start();
}
}
- Implemement a
ServletContextListener
which createsApiServlet
- Add the
ServletContextListener
to yourweb.xml
file
import javax.servlet.ServletContextListener;
import javax.servlet.ServletContextEvent;
import javax.servlet.ServletContext;
public class MyListener implements ServletContextListener {
@Override
public void contextInitialized(ServletContextEvent sce) {
ServletContext context = sce.getServletContext();
context.addServlet("api", new ApiServlet(new MyApiController())).addMapping("/api/*");
}
@Override
public void contextDestroyed(ServletContextEvent sce) {
}
}
<web-app>
<listener>
<listener-class>
MyListener
</listener-class>
</listener>
</web-app>
import com.sun.net.httpserver.HttpServer;
import org.actioncontroller.httpserver.ApiHandler;
public class MyServer {
public static void main(String[] args){
HttpServer httpServer = HttpServer.create(new InetSocketAddress("localhost", 8080), 0);
httpServer.createContext("/demo/api", new ApiHandler(new MyApiController()));
httpServer.start();
}
}
interface MyApi {
@GET("/v1/api/objects")
@JsonBody
List<SomePojo> listObjects(
@RequestParam("query") Optional<String> query,
@RequestParam("maxHits") Optional<Integer> maxHits
);
@GET("/v1/api/objects/:id")
@JsonBody
SomePojo getObject(@PathParam("id") long id);
}
public class ApiClientDemo {
public static void main(String[] args){
HttpURLConnectionApiClient client = new HttpURLConnectionApiClient("http://localhost:8080/api");
MyApi myApi = ApiClientProxy.create(MyApi.class, httpClient);
// Will perform HTTP GET of http://localhost:8080/api/v1/api/objects/5001
SomePojo object = myApi.getObject(50001);
}
}
ConfigObserver is a revolutionary way to think of application configuration. ConfigObserver monitors the configuration values and calls back to you application when a configuration value you care about is changed. This way, your application can easily hot-reload configuration values.
Example:
public class MyApplication {
private String myConfigValue;
private DataSource dataSource;
private ServerSocket serverSocket;
public MyApplication(ConfigObserver config) {
config.onStringValue("myConfigValue", null, v -> this.myConfigValue = v);
config.onPrefixedValue("dataSource", DataSourceConfig::create, dataSource -> this.dataSouce = dataSource);
config.onInetSocketAddress("serverAddress", 10080,
address -> {
if (serverSocket != null) serverSocket.close();
serverSocket = new ServerSocket(address);
startServerSocket(serverSocket);
});
}
public static void main(String[] args){
ConfigObserver config = new ConfigObserver(new File("."), "myApp");
new MyApplication(config);
}
}
- Log payloads
- Split HttpClientParameterMapping from HttpParameterMapping?