DZone
Thanks for visiting DZone today,
Edit Profile
  • Manage Email Subscriptions
  • How to Post to DZone
  • Article Submission Guidelines
Sign Out View Profile
  • Post an Article
  • Manage My Drafts
Over 2 million developers have joined DZone.
Log In / Join
Refcards Trend Reports Events Over 2 million developers have joined DZone. Join Today! Thanks for visiting DZone today,
Edit Profile Manage Email Subscriptions Moderation Admin Console How to Post to DZone Article Submission Guidelines
View Profile
Sign Out
Refcards
Trend Reports
Events
Zones
Culture and Methodologies Agile Career Development Methodologies Team Management
Data Engineering AI/ML Big Data Data Databases IoT
Software Design and Architecture Cloud Architecture Containers Integration Microservices Performance Security
Coding Frameworks Java JavaScript Languages Tools
Testing, Deployment, and Maintenance Deployment DevOps and CI/CD Maintenance Monitoring and Observability Testing, Tools, and Frameworks
Partner Zones AWS Cloud
by AWS Developer Relations
Culture and Methodologies
Agile Career Development Methodologies Team Management
Data Engineering
AI/ML Big Data Data Databases IoT
Software Design and Architecture
Cloud Architecture Containers Integration Microservices Performance Security
Coding
Frameworks Java JavaScript Languages Tools
Testing, Deployment, and Maintenance
Deployment DevOps and CI/CD Maintenance Monitoring and Observability Testing, Tools, and Frameworks
Partner Zones
AWS Cloud
by AWS Developer Relations

Trending

  • Automating the Migration From JS to TS for the ZK Framework
  • 5 Key Concepts for MQTT Broker in Sparkplug Specification
  • File Upload Security and Malware Protection
  • From On-Prem to SaaS

Trending

  • Automating the Migration From JS to TS for the ZK Framework
  • 5 Key Concepts for MQTT Broker in Sparkplug Specification
  • File Upload Security and Malware Protection
  • From On-Prem to SaaS
  1. DZone
  2. Coding
  3. Frameworks
  4. Lightweight Embedded Java REST Server Without a Framework

Lightweight Embedded Java REST Server Without a Framework

Check out this example of a simple lightweight REST server with the appropriate middleware and less custom code than Spring Boot.

Bill O'Neil user avatar by
Bill O'Neil
·
May. 12, 17 · Tutorial
Like (16)
Save
Tweet
Share
54.81K Views

Join the DZone community and get the full member experience.

Join For Free

A quick Google search of Java REST frameworks will lead you to tons of existing frameworks. For example, you can easily find tons of Spring Boot REST examples which have very few lines of code. However, what they do have is lots of annotations and lots of magic. Also, most of them don't do very much; they tend to leave out exception handling, logging, and metrics. Here is an example of a lightweight REST server with a lot of the appropriate middleware, no magic and not that many lines of custom code. This example does not include validation or databases, we will save that for a later date.

Model/POJO

Our service will be very simple and only handle CRUD operations for a User class.

public class User {
    private final String email;
    private final Set<Role> roles;
    private final LocalDate dateCreated;

    public User(
            @JsonProperty("email") String email,
            @JsonProperty("roles") Set<Role> roles,
            @JsonProperty("dateCreated") LocalDate dateCreated) {
        super();
        this.email = email;
        this.roles = roles;
        this.dateCreated = dateCreated;
    }

    public String getEmail() {
        return email;
    }

    public Set<Role> getRoles() {
        return roles;
    }

    public LocalDate getDateCreated() {
        return dateCreated;
    }

    public static enum Role {
        USER, ADMIN
    }

    private static final TypeReference<User> typeRef = new TypeReference<User>() {};
    public static TypeReference<User> typeRef() {
        return typeRef;
    }
    private static final TypeReference<List<User>> listTypeRef = new TypeReference<List<User>>() {};
    public static TypeReference<List<User>> listTypeRef() {
        return listTypeRef;
    }
}

 View on GitHub.

In Memory Dao

Simple in memory dao just for an example.

/*
 * In memory Dao. Less than ideal but just for an example.
 */
public class UserDao {
    private final ConcurrentMap<String, User> userMap;

    public UserDao() {
        this.userMap = new ConcurrentHashMap<>();
    }

    public User create(String email, Set<User.Role> roles) {
        User user = new User(email, roles, LocalDate.now());

        // If we get a non null value that means the user already exists in the Map.
        if (null != userMap.putIfAbsent(user.getEmail(), user)) {
            return null;
        }
        return user;
    }

    public User get(String email) {
        return userMap.get(email);
    }

    // Alternate implementation to throw exceptions instead of return nulls for not found.
    public User getThrowNotFound(String email) {
        User user = userMap.get(email);
        if (null == user) {
            throw Exceptions.notFound(String.format("User %s not found", email));
        }
        return user;
    }

    public User update(User user) {
        // This means no user existed so update failed. return null
        if (null == userMap.replace(user.getEmail(), user)) {
            return null;
        }
        // Update succeeded return the user
        return user;
    }

    public boolean delete(String email) {
        return null != userMap.remove(email);
    }

    public List<User> listUsers() {
        return userMap.values()
                      .stream()
                      .sorted(Comparator.comparing((User u) -> u.getEmail()))
                      .collect(Collectors.toList());
    }
}

 View on GitHub

Request Utilities

Simple helpers to reduce boilerplate and reuse defaults/string literals across routes. In Spring, it's not uncommon to see something like @PathParam("userId") littered all over the code, or worse some say userId some say id and it can be inconsistent. Of course, it's possible to write your own custom annotations in Spring for reuse, but who wants to do that for every single param? (There are other and better approaches out there but they don't seem to be followed very often).

public class UserRequests {

    public String email(HttpServerExchange exchange) {
        return Exchange.pathParams().pathParam(exchange, "email").orElse(null);
    }

    public User user(HttpServerExchange exchange) {
        return Exchange.body().parseJson(exchange, User.typeRef());
    }

    public void exception(HttpServerExchange exchange) {
        boolean exception = Exchange.queryParams()
                                    .queryParamAsBoolean(exchange, "exception")
                                    .orElse(false);
        if (exception) {
            throw new RuntimeException("Some random exception. Could be anything!");
        }
    }
}

 View on GitHub.

Handlers

public class UserRoutes {
    private static final UserRequests userRequests = new UserRequests();
    private static final UserDao userDao = new UserDao();

    public static void createUser(HttpServerExchange exchange) {
        User userInput = userRequests.user(exchange);
        User user = userDao.create(userInput.getEmail(), userInput.getRoles());
        if (null == user) {
            ApiHandlers.badRequest(exchange, String.format("User %s already exists.", userInput.getEmail()));
            return;
        }
        exchange.setStatusCode(StatusCodes.CREATED);
        Exchange.body().sendJson(exchange, user);
    }

    public static void getUser(HttpServerExchange exchange) {
        String email = userRequests.email(exchange);
        User user = userDao.get(email);
        if (null == user) {
            ApiHandlers.notFound(exchange, String.format("User %s not found.", email));
            return;
        }
        Exchange.body().sendJson(exchange, user);
    }

    // Alternative Not Found by throwing / handling Exceptions.
    public static void getUserThrowNotFound(HttpServerExchange exchange) {
        String email = userRequests.email(exchange);
        User user = userDao.getThrowNotFound(email);
        Exchange.body().sendJson(exchange, user);
    }

    public static void updateUser(HttpServerExchange exchange) {
        User userInput = userRequests.user(exchange);
        User user = userDao.update(userInput);
        if (null == user) {
            ApiHandlers.notFound(exchange, String.format("User {} not found.", userInput.getEmail()));
            return;
        }
        Exchange.body().sendJson(exchange, user);
    }

    public static void deleteUser(HttpServerExchange exchange) {
        String email = userRequests.email(exchange);

        // If you care about it you can handle it.
        if (false == userDao.delete(email)) {
            ApiHandlers.notFound(exchange, String.format("User {} not found.", email));
            return;
        }
        exchange.setStatusCode(StatusCodes.NO_CONTENT);
        exchange.endExchange();
    }

    public static void listUsers(HttpServerExchange exchange) {
        List<User> users = userDao.listUsers();
        Exchange.body().sendJson(exchange, users);
    }
}

 View on GitHub.

Notice how there are two options for handling NotFound in the server. You can throw/handle exceptions or handle it directly in the route since we already know it's not found. Spring and most frameworks tend to push for the throw/handle exception model. It is easy to do that, just remember throwing and catching exceptions can be expensive, in these cases, it is easily avoidable so pick what makes sense for your business domain.

Routing/Server

Notice we have all the rest routes as well as exception handling/logging/metrics middleware.

private static final HttpHandler ROUTES = new RoutingHandler()
    .get("/users", timed("listUsers", UserRoutes::listUsers))
    .get("/users/{email}", timed("getUser", UserRoutes::getUser))
    .get("/users/{email}/exception", timed("getUser", UserRoutes::getUserThrowNotFound))
    .post("/users", timed("createUser", UserRoutes::createUser))
    .put("/users", timed("updateUser", UserRoutes::updateUser))
    .delete("/users/{email}", timed("deleteUser", UserRoutes::deleteUser))
    .get("/metrics", timed("metrics", CustomHandlers::metrics))
    .get("/health", timed("health", CustomHandlers::health))
    .setFallbackHandler(timed("notFound", RoutingHandlers::notFoundHandler))
;

/*
 *  Small wrapper to mimic throwing exceptions. Just add &exception=true
 *  to any route and this will throw an exception. Notice it throws a RuntimeException
 *  not an API exception. This will be handled by the global ExceptionHandler.
 */
private static final HttpHandler EXCEPTION_THROWER = (HttpServerExchange exchange) -> {
    new UserRequests().exception(exchange);
    ROUTES.handleRequest(exchange);
};

private static final HttpHandler ROOT = CustomHandlers.exception(EXCEPTION_THROWER)
    .addExceptionHandler(ApiException.class, ApiHandlers::handleApiException)
    .addExceptionHandler(Throwable.class, ApiHandlers::serverError)
;

 View on GitHub.

public static void main(String[] args) {
    // Once again pull in a bunch of common middleware.
    SimpleServer server = SimpleServer.simpleServer(Middleware.common(ROOT));
    server.start();
}

 View on GitHub.

Examples

Create User

curl -X POST "localhost:8080/users" -d '
{
  "email": "user1@test.com",
  "roles": ["USER"]
}
';
{"email":"user1@test.com","roles":["USER"],"dateCreated":"2017-01-16"}

curl -X POST "localhost:8080/users" -d '
{
  "email": "user2@test.com",
  "roles": ["ADMIN"]
}
';
{"email":"user2@test.com","roles":["ADMIN"],"dateCreated":"2017-01-16"}

Update User

curl -X PUT "localhost:8080/users" -d '
{
  "email": "user2@test.com",
  "roles": ["USER", "ADMIN"]
}
';
{"email":"user2@test.com","roles":["ADMIN","USER"]}

List Users

curl -X GET "localhost:8080/users"
[{"email":"user1@test.com","roles":["USER"],"dateCreated":"2017-01-16"},{"email":"user2@test.com","roles":["ADMIN","USER"]}]

Get User

We are using both styles of get user here.

curl -X GET "localhost:8080/users/user1@test.com"
{"email":"user1@test.com","roles":["USER"],"dateCreated":"2017-01-16"}

curl -X GET "localhost:8080/users/user1@test.com/exception"
{"email":"user1@test.com","roles":["USER"],"dateCreated":"2017-01-16"}

Delete User

curl -v -X DELETE "localhost:8080/users/user1@test.com"
* Connected to localhost (127.0.0.1) port 8080 (#0)
> DELETE /users/user1@test.com HTTP/1.1
> Host: localhost:8080
> User-Agent: curl/7.49.1
> Accept: */*
>
< HTTP/1.1 204 No Content
< Date: Mon, 16 Jan 2017 18:45:52 GMT

Get User 404

Both approaches respond in the same way however the exception handling version will be more expensive. It has to unroll the stack trace and whatnot. If that is acceptable by all means go for it. If you need maximum performance don't throw exceptions when you don't need to.

curl -X GET "localhost:8080/users/user1@test.com"
{"statusCode":404,"message":"User user1@test.com not found."}

curl -X GET "localhost:8080/users/user1@test.com/exception"
{"statusCode":404,"message":"User user1@test.com not found"}

List Users Again

curl -X GET "localhost:8080/users"
[{"email":"user2@test.com","roles":["ADMIN","USER"]}]

Handle Unknown Exception

Adding exception=true to any route will throw a RuntimeException. Notice this exception is handled with the fallback handler that handles any Throwable.class.

curl -X GET "localhost:8080/users?exception=true"
{"statusCode":500,"message":"Internal Server Error"}

Metrics

Once again we have all of our metrics from our middleware logic.

{
  "version": "3.0.0",
  "gauges": {},
  "counters": {},
  "histograms": {},
  "meters": {
    "status.code.200": {
      "count": 6,
      "m15_rate": 17.219082651255942,
      "m1_rate": 0.5436625964502019,
      "m5_rate": 8.9579872747367,
      "mean_rate": 1.1273159803060255,
      "units": "events/minute"
    },
    "status.code.201": {
      "count": 1,
      "m15_rate": 9.397673938878663,
      "m1_rate": 0.3067383984780894,
      "m5_rate": 5.7636636130775925,
      "mean_rate": 0.2625854307076081,
      "units": "events/minute"
    },
    "status.code.204": {
      "count": 1,
      "m15_rate": 10.101505049683713,
      "m1_rate": 0.9062621341052866,
      "m5_rate": 7.158067076339619,
      "mean_rate": 0.3709252334705832,
      "units": "events/minute"
    },
    "status.code.400": {
      "count": 1,
      "m15_rate": 10.858049016431512,
      "m1_rate": 2.6775619217811597,
      "m5_rate": 8.889818648180613,
      "mean_rate": 0.6165310747609489,
      "units": "events/minute"
    },
    "status.code.404": {
      "count": 2,
      "m15_rate": 10.68590211257018,
      "m1_rate": 2.878023977404449,
      "m5_rate": 8.514829995177887,
      "mean_rate": 1.033923244541407,
      "units": "events/minute"
    },
    "status.code.500": {
      "count": 1,
      "m15_rate": 11.542290915278693,
      "m1_rate": 6.696421749240567,
      "m5_rate": 10.678581251856285,
      "mean_rate": 1.3928552880390135,
      "units": "events/minute"
    }
  },
  "timers": {
    "createUser": {
      "count": 2,
      "max": 468.563385,
      "mean": 230.19748354543808,
      "min": 2.3202819999999997,
      "p50": 2.3202819999999997,
      "p75": 468.563385,
      "p95": 468.563385,
      "p98": 468.563385,
      "p99": 468.563385,
      "p999": 468.563385,
      "stddev": 233.06255505190282,
      "m15_rate": 0.0939588952884057,
      "m1_rate": 0.010507188050111582,
      "m5_rate": 0.13998158006315464,
      "mean_rate": 0.36653143968815943,
      "duration_units": "milliseconds",
      "rate_units": "calls/minute"
    },
    "deleteUser": {
      "count": 1,
      "max": 4.8252109999999995,
      "mean": 4.8252109999999995,
      "min": 4.8252109999999995,
      "p50": 4.8252109999999995,
      "p75": 4.8252109999999995,
      "p95": 4.8252109999999995,
      "p98": 4.8252109999999995,
      "p99": 4.8252109999999995,
      "p999": 4.8252109999999995,
      "stddev": 0,
      "m15_rate": 0.055963873354550935,
      "m1_rate": 0.0724607194316669,
      "m5_rate": 0.11831244221923884,
      "mean_rate": 0.18326783459947024,
      "duration_units": "milliseconds",
      "rate_units": "calls/minute"
    },
    "getUser": {
      "count": 4,
      "max": 10.177731,
      "mean": 4.490080459374723,
      "min": 1.042088,
      "p50": 1.575589,
      "p75": 10.177731,
      "p95": 10.177731,
      "p98": 10.177731,
      "p99": 10.177731,
      "p999": 10.177731,
      "stddev": 4.276904527854368,
      "m15_rate": 0.22399902400953256,
      "m1_rate": 0.42427789694811446,
      "m5_rate": 0.47991807604468867,
      "mean_rate": 0.7330560454806502,
      "duration_units": "milliseconds",
      "rate_units": "calls/minute"
    },
    "listUsers": {
      "count": 2,
      "max": 18.343650999999998,
      "mean": 1.9680702759999467,
      "min": 1.244971,
      "p50": 1.244971,
      "p75": 1.244971,
      "p95": 1.244971,
      "p98": 18.343650999999998,
      "p99": 18.343650999999998,
      "p999": 18.343650999999998,
      "stddev": 3.441100196972346,
      "m15_rate": 0.1111005918141424,
      "m1_rate": 0.3354051301588863,
      "m5_rate": 0.2403451569244876,
      "mean_rate": 0.3665140477638753,
      "duration_units": "milliseconds",
      "rate_units": "calls/minute"
    },
    "metrics": {
      "count": 0,
      "max": 0,
      "mean": 0,
      "min": 0,
      "p50": 0,
      "p75": 0,
      "p95": 0,
      "p98": 0,
      "p99": 0,
      "p999": 0,
      "stddev": 0,
      "m15_rate": 0,
      "m1_rate": 0,
      "m5_rate": 0,
      "mean_rate": 0,
      "duration_units": "milliseconds",
      "rate_units": "calls/minute"
    },
    "notFound": {
      "count": 1,
      "max": 6.1470899999999995,
      "mean": 6.1470899999999995,
      "min": 6.1470899999999995,
      "p50": 6.1470899999999995,
      "p75": 6.1470899999999995,
      "p95": 6.1470899999999995,
      "p98": 6.1470899999999995,
      "p99": 6.1470899999999995,
      "p999": 6.1470899999999995,
      "stddev": 0,
      "m15_rate": 0.06648182394123926,
      "m1_rate": 0.9594670244481206,
      "m5_rate": 0.1983425541405901,
      "mean_rate": 0.18326906627298534,
      "duration_units": "milliseconds",
      "rate_units": "calls/minute"
    },
    "updateUser": {
      "count": 1,
      "max": 2.389231,
      "mean": 2.389231,
      "min": 2.389231,
      "p50": 2.389231,
      "p75": 2.389231,
      "p95": 2.389231,
      "p98": 2.389231,
      "p99": 2.389231,
      "p999": 2.389231,
      "stddev": 0,
      "m15_rate": 0.04710994577423798,
      "m1_rate": 0.005472367185912239,
      "m5_rate": 0.07057403311423895,
      "mean_rate": 0.18326677583386106,
      "duration_units": "milliseconds",
      "rate_units": "calls/minute"
    }
  }
}

Building an Executable Fat JAR

Check out our post on Multi-project builds with Gradle and Fat Jars with Shadow.

Java Client With OkHttp

 cURL  is great for debugging and testing, however, you will probably want programmatic access to your API with a Java HTTP Client (OkHttp).

REST Web Protocols Java (programming language) Framework Embedded Java Spring Framework

Published at DZone with permission of Bill O'Neil. See the original article here.

Opinions expressed by DZone contributors are their own.

Trending

  • Automating the Migration From JS to TS for the ZK Framework
  • 5 Key Concepts for MQTT Broker in Sparkplug Specification
  • File Upload Security and Malware Protection
  • From On-Prem to SaaS

Comments

Partner Resources

X

ABOUT US

  • About DZone
  • Send feedback
  • Careers
  • Sitemap

ADVERTISE

  • Advertise with DZone

CONTRIBUTE ON DZONE

  • Article Submission Guidelines
  • Become a Contributor
  • Visit the Writers' Zone

LEGAL

  • Terms of Service
  • Privacy Policy

CONTACT US

  • 600 Park Offices Drive
  • Suite 300
  • Durham, NC 27709
  • support@dzone.com

Let's be friends: