Configuration

In this lesson, we will dive into configuration parameters associated with WebSocket endpoints. Simply put, Configuration is nothing but a bunch of (meta) data associated with an endpoint (server or client). You will learn about

  • Server endpoint configuration (for both annotated and programmatic endpoints)
  • Client endpoint configuration (for both annotated and programmatic endpoints)
  • (Server & client) Endpoint Configurators

From an API perspective, a WebSocket endpoint Configuration is represented by the EndpointConfig interface which is extended by ServerEndpointConfig and ClientEndpointConfig for server and client respectively

Server configuration

Before we dive into the details, here is a quick snapshot of the related interfaces

Class/Interface Description
ServerEndpointConfig A derivative of EndpointConfig interface which is specific for configuration related to server side WebSocket endpoints
ServerEndpointConfig.Builder Used only for programmatic server endpoints to build a ServerEndpointConfig instance
ServerEndpointConfig.Configurator An interface whose custom implementation allows sharing of global (for all endpoints) available logic/state as well opportunity to intercept the WebSocket handshake process (via method override)

Configuring annotated server endpoints

Annotated server endpoints are configured implicitly via the elements of the @ServerEndpoint annotation. The WebSocket container picks up the value from the annotation elements and creates an instance of EndpointConfig behind the scenes

//annotated server endpoint with all its configuration elements

@ServerEnpdoint(
    value = "/chat/",
    configurator = ChatEndpointConfigurator.class, //discussed later
    decoders = JSONToChatObjectDecoder.class,
    encoders = ChatObjectToJSONEncoder.class,
    subprotocols = {"chat"}
)
public class ChatServer {
    //business logic...
}

The EndpointConfig instance is automatically injected (at run time by the WebSocket container) as a parameter of the @OnOpen method

//server endpoint configuration in action

@OnOpen
public void onOpenCallback(Session session, EndpointConfig epConfig){
    ServerEndpointConfig serverConfig = (ServerEndpointConfig) epConfig;
    Map<String, Object> globalPropertiesMap = serverConfig.getUserProperties();
    ......
}

Configuring programmatic server endpoints

Programmatic endpoints need (explicit) coding as far as configuration is concerned. This is because of the fact that programmatic endpoints are deployed differently and need an instance of ServerEndpointConfig

Don't worry about the deployment aspect since it's covered in detail in the next lesson

Here is where the fluent builder ServerEndpointConfig.Builder comes into picture. Let's look at an example which demonstrates it's usage

ServerEndpointConfig serverConfig = ServerEndpointConfig.Builder
    .create(StockTrackerEndpoint.class , "/pop-stocks/").
    .configurator(StockTrackerConfigurator.getInstance()) //discussed later
    .decoders(JSONToStockTickerObject.class)
    .encoders(StockTickerObjectToJSON.class)
    .build();

An instance of ServerEndpointConfig is made available in the onOpen method of the javax.websocket.Endpoint (as a parameter)

public class ProgrammaticChatClient extends Endpoint {
    @Override
    public void onOpen(Session session, EndpointConfig config){
      ServerEndpointConfig serverConfig = (ServerEndpointConfig) epConfig;
      .....
    }
}

Client configuration

You must have built a fair understanding about WebSocket clients from the WebSocket Client API lesson. They too have configuration parameters associated with them which are used while connecting to WebSocket server endpoints. Before we dive into the details, here is a quick snapshot of the related interfaces

Class/Interface Description
ClientEndPointConfig A derivative of EndpointConfig interface which is specific for configuration related to client side WebSocket endpoints
ClientEndpointConfig.Builder Used only for programmatic client endpoints to build a ClientEndpointConfig instance
ClientEndpointConfig.Configurator The client side equivalent of ServerEndpointConfig.Configurator

Configuring annotated client endpoints

Annotated client endpoints are configured implicitly via the elements of the @ClientEndpoint annotation

@ClientEndpoint(
    configurator = ChatClientEndpointConfigurator.class, //discussed later
    decoders = JSONToChatObjectDecoder.class,
    encoders = ChatObjectToJSONEncoder.class,
    subprotocols = {"chat"}
)
public class ChatClient {
 //business logic...
}

This instance is automatically injected (at runtime) as a parameter of the @OnOpen method

//server endpoint configuration in action

@OnOpen
public void onOpenCallback(Session session, EndpointConfig epConfig){
  ClientEndpointConfig clientConfig = (ClientEndpointConfig) epConfig;
  ......
}

Configuring programmatic client endpoints

Just like their server side counterparts, configuration for programmatic clients can be coded using a fluent builder API - ClientEndpointConfig

ClientEndpointConfig cec = ClientEndpointConfig.Builder
    .configurator(ChatClientConfigurator.getInstance()) //discussed later
    .decoders(JSONToStockTickerObject.class)
    .encoders(StockTickerObjectToJSON.class)
    .build();

This configuration object is used while initiating connection to a WebSocket endpoint. Please refer to the WebSocket Client API chapter for code samples

Additional notes

  • As you might have already noticed, there is not much of a difference b/w annotated client and server side configurations, except for the fact that a client endpoint does not have the concept of a path or a URL where its listening for connections - that's something that a server endpoint does
  • An EndpointConfig instance provides the capability to store (global) properties which are common to all instances of an endpoint. It does so by providing a getUserProperties() method which exposes a mutable Map

The big picture

Annotated and programmatic endpoint configuration are handled differently, but the end result is the same. Below is a table which illustrates this point for both server and client endpoints

  • For server endpoints, the table shows the mapping b/w corresponding element of the @ServerEndpoint annotation, the corresponding method in ServerEndpointConfig as well as appropriate the method in the ServerEndpointConfig.Builder, and
@ServerEndpoint annotation element ServerEndpointConfig method ServerEndpointConfig.Builder method
value getPath() create(Class<?> endpointClass, String path)
configurator getConfigurator() configurator(ServerEndpointConfig.Configurator sec)
decoders getDecoders() decoders(List<Class<? extends Decoder>> decoders)
encoders getEncoders() encoders(List<Class<? extends Encoder>> encoders)
subprotocols getSubprotocols() subprotocols(List<String> subprotocols)
  • In case of client endpoints, the table shows the mapping b/w corresponding element of the @ClientEndpoint annotation, the corresponding method in ClientEndpointConfig as well as appropriate the method in the ClientEndpointConfig.Builder
@ClientEndpoint annotation element ClientEndpointConfig method ClientEndpointConfig.Builder method
configurator getConfigurator() configurator(ClientEndpointConfig.Configurator clientEndpointConfigurator)
decoders getDecoders() decoders(List<Class<? extends Decoder>> decoders)
encoders getEncoders() encoders(List<Class<? extends Encoder>> encoders)
subprotocols getPreferredSubprotocols() preferredSubprotocols(List<String> preferredSubprotocols)

Configurators

Basics

Configurators (which in my opinion could have been named differently) are applicable to both server and client side WebSocket endpoints. These are components which can intercept handshake phase of the WebSocket connection lifecycle. They can be used to implement a bunch of things such as

  • customizing the WebSocket handshake process
  • plugging in a custom implementation for producing endpoint instances
  • implementing common logic which can be used by all endpoint instances which are configured using the Configuration with which the Configurator is associated

If the developer does not override (provide a custom implementation) of a Configurator a default one is internally used by the container

Server side

The table below provides an overview. It lists out the methods of a ServerEndpointConfig.Configurator which needs to be overridden to provide custom behavior

Capbility Method in ServerEndpointConfig.Configurator Details
Handshake modification void modifyHandshake(ServerEndpointConfig sec, HandshakeRequest request, HandshakeResponse response) a custom implementation can override this method in order to modify the HandshakeResponse created by the runtime (in response to the handshake request)
Customizing endpoint creation <T> T getEndpointInstance(Class<T> endpointClass) overriding this method allows you to hook into the WebSocket endpoint substantiation process
Origin check boolean checkOrigin(String originHeaderValue) it provides the value of the HTTP Origin header sent by the client during the handshake process in order to enforce security checks if required
Subprotocol negotiation List<Extension> getNegotiatedExtensions(List<Extension> installed, List<Extension> requested) selects the appropriate subprotocol depending upon the best match b/w what the server supports and what is being requested by the client (empty if there is no match)
Extension negotiation String getNegotiatedSubprotocol(List<String> supported, List<String> requested) similar to the subprotocol, the extension support can be negotiated as well

The catch If you choose to customize the endpoint creation process, (Java EE) container services like dependency injection might not available since the container default convention is being overridden

Let's look at en example

//custom configurator

public class CustomServerEndpointConfigurator extends ServerEndpointConfig.Configurator {

    @Override
    public <T> T getEndpointInstance(Class<T> endpointClass){
        //override the default behavior by providing a 'Singleton'
        return (T) StockTickerEndpoint.getInstance();
    }

    @Override
    public boolean checkOrigin(String originHeaderValue){
        //just audit this
        audit(originHeaderValue);
        return true;
    }

    private String user;

    @Override
    public void modifyHandshake(ServerEndpointConfig sec, HandshakeRequest request, HandshakeResponse response){
        //introspect the request headers
        System.out.println(request);

        //the authenticated user
        this.user = request.getUserPrincipal().getName();

    }

    @Override
    public List<Extension> getNegotiatedExtensions(List<Extension> installed, List<Extension> requested){
        //invoke default implementation
        return super.getNegotiatedExtensions(installed, requested);
    }

    @Override
    public String getNegotiatedSubprotocol(List<String> supported, List<String> requested){
        //invoke default implementation
        return super.getNegotiatedSubprotocol(supported, requested);
    }
}
//declaring the custom configuration

@ServerEndpoint(value = "/letschat" , configurator = CustomServerEndpointConfigurator.class)
public class AnnotatedServerEndpointExample {
  //call back life cycle method(s) implementation...
}

Client side

Client configurators are similar in sprirt to their server counterparts. They slightly less complicated and just define hooks for inercepting the phases before and after the handshake

Capbility Method in ClientEndpointConfig.Configurator
Intercept Handshake void afterResponse(HandshakeResponse hr), beforeRequest(Map<String,List<String>> headers)
//custom configurator

public class CustomClientEndpointConfigurator extends ClientEndpointConfig.Configurator {

    @Override
    public void beforeRequest(Map<String,List<String>> headers){
        //mutate the header
        String token = ...;
        headers.put("X-token" , Arrays.asList(token));
    }


    @Override
    public void afterResponse(HandshakeResponse hr){
        //introspect the handshake response
        System.out.println(hr.getHeaders());
    }
}
//declaring the client configuration

@ClientEndpoint(configurator = CustomClientEndpointConfigurator.class)
public class AnnotatedClientEndpointExample {
  //call back life cycle method(s) implementation...
}

Let's move on...

.. and take a closer look at the Deployment related aspects

results matching ""

    No results matching ""