Automated tests for Spring Boot WebSocket server

20 05 2017

Developing WebSocket server for your Spring Boot app is fairly simple and well described and documented. However when it comes to making sure that it ‘actually works’ is done manually in most cases.

Below I will show how I do the automated integration tests for Websocket server using Spring’s StompClient. I assume that you are familiar with the idea of WebSockets in Spring. If not, here is a very good article: https://docs.spring.io/spring/docs/current/spring-framework-reference/html/websocket.html

Source Code

Code of this tutorial is for you to see here: https://github.com/yacekmm/looksok/tree/WebSocketDemo/Spring/WebSocket

System under test: configuration

The demo will be presented on the simpliest WS configuration which consists of one entry point endpoint (`/ws`) and in-memory message broker (under `/queue`):

@Configuration
@EnableWebSocketMessageBroker
public class WsConfig extends AbstractWebSocketMessageBrokerConfigurer {

    @Override
    public void registerStompEndpoints(StompEndpointRegistry registry) { 
        registry.addEndpoint("/ws");
    }

    @Override
    public void configureMessageBroker(MessageBrokerRegistry registry) {
        registry.enableSimpleBroker("/queue");
    }
}

The idea behind the integration test

In the test I’m going to:
– use SpringRunner to start up the whole application with the full context
– Autowire Component that in production will be responsible for sending messages to WebSocket clients
– Build and configure Spring’s StompClient and connect a StompSession to my WebSocket server
send a message over WebSocket and verify if my test client received it

Starting the application for tests

With SpringRunner.class used within jUnit test I start the app context and autowire the WSProxy component (the one that sends messages to WS clients):

@RunWith(SpringRunner.class)
@SpringBootTest(webEnvironment = RANDOM_PORT)
public class WsConfigIntegrationTest {

    @Value("${local.server.port}")
    private int port;
    @Autowired
    private WsProxy wsProxy;

WsProxy in this demo is a simple component sending message with a SimpMessagingTemplate:

@Component
public class WsProxy {

    private SimpMessagingTemplate messagingTemplate;

    @Autowired
    public WsProxy(SimpMessagingTemplate messagingTemplate) {
        this.messagingTemplate = messagingTemplate;
    }

    public void sendMessage(@RequestParam String clientId,
                            @RequestParam String payload){
        messagingTemplate.convertAndSend("/queue/" + clientId, payload);
    }
}

In this configuration, the url of WS endpoint is:

String wsUrl = "ws://127.0.0.1:" + port + "/ws";

Configuring StompClient and connecting StompSession

Using the StompClient with a minimum configuration:

WebSocketStompClient stompClient = new WebSocketStompClient(new StandardWebSocketClient());
stompClient.setMessageConverter(new StringMessageConverter());

I create StompSession to my WS url:

StompSession stompSession = stompClient.connect(wsUrl, new MyStompSessionHandler()).get();

The connect() method returns a future, but here, in tests, I wait synchronously until this session is ready by calling get() on it to get the session instantly.

Oh, and don’t worry about the MyStompSessionHandler – in this configuration it does nothing, except debug logging on the ‘Connect to WS’ event (just overrides the StompSessionHandlerAdapter)

Now it’s time to subscribe the /queue/my-id Channel within the session:

stompSession.subscribe(
    "/queue/my-id", 
    new MyStompFrameHandler((payload) -> resultKeeper.complete(payload.toString())));

The MyStompFrameHandler class is responsible for handling the incoming message in within the session and completing the CompletableFuture promise that it received as an argument. CompletableFuture is a helper variable needed to test asynchronous code:

CompletableFuture<String> resultKeeper = new CompletableFuture<>();

And the handler uses it as follows:

public class MyStompFrameHandler implements StompFrameHandler {

    private final Consumer<String> frameHandler;

    public MyStompFrameHandler(Consumer<String> frameHandler) {
        this.frameHandler = frameHandler;
    }

    ...

    @Override
    public void handleFrame(StompHeaders headers, Object payload) {
        log.info("received message: {} with headers: {}", payload, headers);
        frameHandler.accept(payload.toString());
    }
}

Sending the message

Message is sent by a WsProxy with SimpMessagingTemplate:

@Component
public class WsProxy {

    private SimpMessagingTemplate messagingTemplate;

    @Autowired
    public WsProxy(SimpMessagingTemplate messagingTemplate) {
        this.messagingTemplate = messagingTemplate;
    }

    public void sendMessage(String clientId, String payload){
        messagingTemplate.convertAndSend("/queue/" + clientId, payload);
    }
}

On some machines it’s also good to wait until the connection is fully established so don’t hesitate to add good old:

Thread.currentThread().sleep(1000);

Testing the result asynchronously

The code in test is async so I pass the Future and wait until it completes with the expected result, or to fail test after timeout on waiting for the response, verifying its body:

assertThat(resultKeeper.get(2, SECONDS)).isEqualTo("test-payload");

That’s it

Now you can run the test, it will start your app, send a message, receive it and verify the contents. Which is everything you need to implement the WebSockets.

Source Code

I’m sure that seeing the source code will make you understand the article better. Grab it from my GitHub: https://github.com/yacekmm/looksok/tree/WebSocketDemo/Spring/WebSocket

Advertisements




Spring: Securing REST API with BasicAuth

23 07 2016

The simpliest, yet effective way to secure Spring REST API is to use Basic Auth. I’m going to show how to do it with Spring Security.

Note

Basic Auth is stateless (no need to manage sessions) and simple to implement. However it should be used with https only if outside of a trusted network. Also be aware that there are issues with Basic Auth (http://security.stackexchange.com/questions/988/is-basic-auth-secure-if-done-over-https)

Source Code

As usual, get it from my GitHub: https://github.com/yacekmm/looksok/tree/HttpBasicAuthDemo/Spring/SecurityBasicAuth

1. Create a project

Use your favorite tool. I choose Spring Boot Initializr in IntelliJ. Add web and security starter poms:

Przechwytywanie

Note that right after you add the security starter pom to your classpath, the auto configuration kicks in and your app is secured by default (the default password is logged on the application startup). I will override that default configuration and customize it later on.

2. Create Controller with urls to be secured

The most straight forward one as possible like mine below:

@RestController
@RequestMapping("/api")
public class SecuredController {

    @RequestMapping(value = "/secured", method = GET, produces = TEXT_PLAIN_VALUE)
    public String securedGet(){
        return "You have access to secured API!";
    }

    @RequestMapping(value = "/admin", method = GET, produces = TEXT_PLAIN_VALUE)
    public String adminGet(){
        return "Welcome, Admin";
    }

    @RequestMapping(value = "/open", method = GET, produces = TEXT_PLAIN_VALUE)
    public String openedGet(){ return "this API does not require authentication"; }
}

It contains 3 endpoints. Each of them I will secure to be accessible on various levels.

3. Create Basic Auth security configuration for selected endpoints

As in one of my previous tutorials (Spring Security Tutorial: Authorization and user roles), you need to extend the WebSecurityConfigurerAdapter:

@EnableWebSecurity
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
    ...
}

and configure the HttpSecurity in there:

@Override
protected void configure(HttpSecurity http) throws Exception {
    http
            .httpBasic()
        .and()
            .authorizeRequests()
            .antMatchers("/api/secured").authenticated()
            .antMatchers("/api/admin").hasRole("ADMIN")
            .antMatchers("/api/open").permitAll()
            .anyRequest().authenticated();
}

What just happened here?

  • first of all with httpBasic() you say that BasicAuth will be in use
  • then you say that want to authorizeRequests() so that:
    • url matching “/api/secured” is accessible for all authenticated users
    • url matching “/api/admin” is accessible for ADMIN users only
    • url matching “/api/open” is accessible with no authentication required
    • any other request is accessible for all authenticated() users

4. Create security configuration: usernames and passwords

Here I create two user accounts with USER and ADMIN role. That method is also in my WebsecurityConfig class. Using those accounts you can log in to the application:

@Autowired
protected void configureGlobal(AuthenticationManagerBuilder auth) throws Exception {
    auth.inMemoryAuthentication()
            .withUser("admin").password("superhard").roles("ADMIN")
        .and()
            .withUser("user").password("easyone").roles("USER");
}

Normaly you would use the AuthenticationProvider, probably connected with your database or LDAP. This is separate big subject not to be cevered in this article.

5. Go ahead and try it!

Run the app and:

  1. head over to url 127.0.0.1:8080/api/open – you should access it without logging in
  2. head over http://127.0.0.1:8080/api/secured – the browser will ask you for username and password: type ‘user’ and ‘easyone’. Then you should access the site
  3. head over to url http://127.0.0.1:8080/api/admin – you will get 403 Forbidden, since browser cached your auth data and reused it. Only admin is permitted to access that url.
  4. To log in as another user you need to restart your browser and open the url once again. You can also use private browsing.




Spring JMX: Manage beans in runtime

19 06 2016

JMX (Java Management Extensions) allows to change the bean field values or invoke bean methods on the fly at runtime. It is in opposite to DI where you configure application when it starts, using profiles or classpath. With JMX you can tune, monitor or configure your application anytime when it is running.

At the heart of JMX are MBeans. MBean is a JavaBean that exposes certain methods that define the management interface. To make of those exposed methods you can use tool of your choice. My choice is Java Mission Control. Others are VisualVM or JConsole.

With Spring you can expose those methods with annotations. I will guide you how to do it.

Source Code

Source Code for this tutorial you can find on my GitHub at: https://github.com/yacekmm/looksok/tree/JmxDemo/Spring/SpringJMX

Demo app introduction

The simplest app to demonstrate JMX basic capabilities is app with one REST controller exposing one GET resource method:

@RequestMapping(value = "/api/my-jmx-value", method = GET)
public int getManagedAttributeValue(){
    return myJmxValue;
}

The return value of that method will be managed by JMX. You can run the app from my GitHub sources and access that resource at http://127.0.0.1:8080/api/my-jmx-value. The default hardcoded value of 25 will be returned and displayed in your browser.

Exposing Bean as a MBean
First of all you need to annotate your Bean with @ManagedResource so it can expose its methods. My annotated Controller is:

@RestController
@ManagedResource(objectName = "jmxDemo:name=MyController")
public class MyController { ... }

The name value indicates the name under which the MBean will be exported. jmxDemo is a domain and MyController is the name of the MBean.

Exposing Bean’s Attribute via JMX

You can expose a field via accessors methods annotated with @ManagedAttribute. Thanks to that you will be able to change the field value via JMX. Annotated attributes in my controller are:

private int myJmxValue = 25;

@ManagedAttribute
public void setMyJmxValue(int myJmxValue) {
    this.myJmxValue = myJmxValue;
}
    
@ManagedAttribute
public int getMyJmxValue(){
    return myJmxValue;
}

Exposing Bean’s Method via JMX

You can also expose your Bean method to be able to invoke it by JMX. This time the @ManagedOperation annotation should be used:

@ManagedOperation
public void logMyJmxValue(String requester){
    log.info("{} requested to log JmxManagedValue which is: {}",
            requester, myJmxValue);
}

Managing JMX Attributes in Java Mission Control

Java Mission Control (JMC) is a tool you can find in your jdk/bin folder under the jmc.exe name. With JMC you can monitor java process health and manage MBeans. See image below to get familiar with the tool and see how to find your MBean there:

JMC-MBeans

1. run the JMC and under the list of local java processes find your app and connect to its MBean Server
2. go to MBean Browser tab and find jmxDemo domain
3. under MyController you can set the MyJmxValue attribute to any nymber you want
4. go to http://127.0.0.1:8080/api/my-jmx-value and test that the returned value is exactly how you set it

Managing JMX Operations in Java Mission Control

Similarly you can invoke exposed operation via JMC. Go to Operations tab, provide the operation param and execute it. In your app logs you should find message indicating that it actually ran:

[4)-192.168.56.1] p.l.spring.controller.MyController       : Jacek requested to log JmxManagedValue which is: 99




Java equals() vs. compareTo() contract

1 05 2016

The definitions

Object’s equals(Object obj) method:

Indicates whether some other object is "equal to" this one

compareTo(T t) is an abstract method from Comparable interface and it:

Compares this object with the specified object for order. 
Returns a negative integer, zero, or a positive integer 
as this object is less than, equal to, or greater than the specified object.

The contract

That being said here seems to appear the contract between those two methods: compareTo should return 0 if equals returns true. This is yet another contract that the equals() is involved (mind the hashCode()). In fact the Comparable JavaDoc says it clearly:

It is strongly recommended, but not strictly required that 
(x.compareTo(y)==0) == (x.equals(y)).

Since it is not strictly required, the JavaDoc clears out things and says:

Generally speaking, any class that implements the Comparable interface 
and violates this condition should clearly indicate this fact. 
The recommended language is: 
"Note: this class has a natural ordering that is inconsistent with equals."

Natural ordering

A class should implement Comparable interface if it is eligible to natural ordering. Natural ordering is perceived as an order where an object can be compared to another of the same type (alphabetical comparison for String, numerical for Number or any other specific to your domain).

If your class does not have a natural ordering that is consistent with equals() but you still need to sort it, you should consider using the Comparator interface instead of Comparable.

In fact the String class follows the equals() and compareTo() contract and it sorts Strings alphabetically according to ASCII characters ordering. Note that this ordering sorts lowercase letters in front of upper case.

The BigDecimal breaks the contract

The BigDecimal class violates the recommendation and its equals() is not consistent with compareTo() in  a matter of a scale. The documentation clearly states it:

according to equals(): 2.0 is not equal to 2.00
according to compareTo(): 2.0 is equal to 2.00

Why? Who knows. There are discussions that in physics for example the 2.0 definetely should not be considered as equal to 2.00 due to the precision. However in most of the domains the values can be considered as equal.

Problems when violating the contract

The problem that arises is for example putting the BigDecimal in a HashSet:

Set<BigDecimal> set = new HashSet<>();
set.add(new BigDecimal("0.20"));
...
if (set.contains(new BigDecimal("0.2")) { // Returns false, but should return true
    ...
}

Because of that the HashSet is usable with BigDecimal if you define the scale that its elements have. So if you put the number with two digits precision, you should always look for two digits precision as well.

The other solution is to use TreeSet, since it uses the compareTo() to search for elements (ignoring the scale). However the TreeSet is sorted as opposed to the HashSet. If you do not need sorting then you ahve to consider whether it hurts your performance (putting an element to the sorted set is more expensive – due to the sorting).





Java 8 StringJoiner demo

24 01 2016

Finally Java has convenient and intuitive API for joining strings with delimiters! Since Java 8 there is StringJoiner class. It is an API that you may know from Guava Joiner classes (see my post: https://looksok.wordpress.com/2015/10/17/guava-joiner-join-all-strings-in-an-array-or-map/). Here is a short StringJoiner demo.

Basic String joins

The most basic usage is to create StringJoiner instance with delimiter as a constructor param and add() strings:

StringJoiner joiner = new StringJoiner(",");
joiner.add("apple");
joiner.add("banana");
joiner.add("orange");

System.out.println("Joiner result is: " + joiner.toString());

The result is:

Joiner result is: apple,banana,orange

If you prefer, you can chain add() calls:

StringJoiner joiner = new StringJoiner(",")
    .add("apple")
    .add("banana")
    .add("orange");

Join Collection of Strings

If you have Collection of Strings, the new static String.join() method can join them:

List<String> list = Arrays.asList("apple", "banana", "orange");
String joined = String.join(", ", list);

System.out.println("Join Array result is: " + joined);

With the result of:

Join Array result is: apple, banana, orange

Join inline

You can prepare joined String in one line with String.join() overloaded with varargs, like that:

String.join(", ", "apple", "banana", "orange");

Joining Collector in Stream API

When using streams you have joining Collector at your disposal:

List<String> list = Arrays.asList("apple", "banana", "orange");
String joined = list.stream()
        .collect(Collectors.joining(", "));

System.out.println("Joined with collector: " + joined);

This will result with:

Joined with collector: apple, banana, orange

Source Code

As always, I share with you the source code for this demo on my github: https://github.com/yacekmm/looksok/tree/StringJoinerDemo/Java/JavaDemo





Asynchronous Producer Consumer with BlockingQueue in Java

19 12 2015

What is it

Producer Consumer pattern is used when one module (producer) produces events, messages or any other kind of data with various pace and the other module (consumer) processes it when the data occur. The asynchronous version means that the producer does not wait until consumer processes each item – just sends it to consumer and forgets about it. The consumer consumes it when its turn comes.

How it works? BlockingQueue

Technically Producer puts events into FIFO BlockingQueue and Consumer takes the event one by one. The queue instance is shared among both actors, so that they can access is. BlockingQueue API has two main operations:

  • put() used by Producer to add new item (if the queue is bounded and is full, the put() method will block the Producer thread until there is a space to add new item)
  • take() used by Consumer to take next event to process (if the queue is empty, the consumer will be blocked until the event occur)

One more API method that is a good practice to use over the take() is:

  • offer() used by the producer to put() with maximum timeout to block producer thread when the queue is full. The offer() method returns boolean result saying if put was successful or not. This value should be interpreted in your code.

There are two BlockingQueue implementations:

  • ArrayBlockingQueue that has to be bounded and is more performant (use it if your case uses constant queue size)
  • LinkedBlockingQueue that can be bounded or not (in fact it is then bounded to Integer.MAX_VALUE) but can be less performant because of that

BlockingQueue is thread-safe and does not accept null values.

Work Unit

It is also a good practice to wrap your messages put in the queue in a wrapper that holds the message and returns it when consumer needs it. The reason to introduce this level of indirection is to be able to add additional data to the Producer Consumer mechanism that your message does not need. That can be:

  • performance metrics indicating when and how fast your messages are processed
  • testing to track the changes in your object

Work unit implementation according to this practice is:

public class MyEventWorkUnit<T> {

    private T myEventWorkUnit;

    public MyEventWorkUnit(T myEventWorkUnit) {
        this.myEventWorkUnit = myEventWorkUnit;
    }

    public T getWorkUnit() {
        return myEventWorkUnit;
    }
}

Then your BlockingQueue holds this type:

BlockingQueue<MyEventWorkUnit<MyEvent>> queue

The MyEvent class is a simple object that can handle itself when asked and has externally generated id. Notice that handle() method sleeps thread to simulate that handling events takes certain amount of time:

public class MyEvent {

    private static final Logger log = Logger.getLogger(MethodHandles.lookup().lookupClass().getSimpleName());

    private final int eventId;

    public MyEvent(int eventId) {
        this.eventId = eventId;
    }

    public void handle() {
        log.info("Serving event with id: " + eventId);
        try {
            Thread.sleep(500);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }

 

Producer Consumer implementation

My Producer generates 10 events and puts it into queue with offer() method with a timeout. This is the implementation:

public class EventProducer extends Thread {
    private final BlockingQueue<MyEventWorkUnit<MyEvent>> queue;
    private AtomicInteger idGenerator;

    public static final Logger log = Logger.getLogger(MethodHandles.lookup().lookupClass().getName());

    public EventProducer(BlockingQueue<MyEventWorkUnit<MyEvent>> queue) {
        this.queue = queue;
        idGenerator = new AtomicInteger();
    }

    @Override
    public void run() {
        for (int i = 0; i < 10; i++) {
            boolean wasAdded = false;
            MyEvent eventToAdd = new MyEvent(idGenerator.getAndIncrement());
            try {
                wasAdded = queue.offer(new MyEventWorkUnit<>(eventToAdd), 100, TimeUnit.MILLISECONDS);
            } catch (InterruptedException e) {
                log.info("Adding Thread was interrupted");
            } finally {
                handleAddResult(wasAdded, eventToAdd);
            }
        }
    }
    
    ...
}

My consumer simply takes events from the queue and handles them:

public class EventConsumer extends Thread{
    private final BlockingQueue<MyEventWorkUnit<MyEvent>> queue;

    public EventConsumer(BlockingQueue<MyEventWorkUnit<MyEvent>> queue) {
        this.queue = queue;
    }

    @Override
    public void run() {
        while(true){
            try {
                MyEventWorkUnit<MyEvent> workUnit = queue.take();
                workUnit.getWorkUnit().handle();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
}

Producer Consumer Construction

I create the ArrayBlockingQueue instance with size bounded to 3 items. This is not enough to handle fast-producing and slow consuming Threads. I limit the size to show how offer() timeouts helps to handle such situations:

BlockingQueue<MyEventWorkUnit<MyEvent>> queue = new ArrayBlockingQueue<>(3);

EventProducer producer = new EventProducer(queue);
EventConsumer consumer = new EventConsumer(queue);

producer.start();
consumer.start();

The result

Notice that:
1. the event (id=0) is immediately added and served. Service takes some time
2. the events with ids 1, 2, 3 are added to the queue, event with id=0 is still being served
3. events 4, 5, 6, and 7 are not added to the queue since it’s full so the timeout occur
4. event id=1 is served by the consumer. There is room for one more event in the queue
5. event id = 8 is added successfully to the queue, event id=9 is rejected due to timeout
6. all events from the queue are handled

INFO: Event was added to the queue: MyEvent{eventId=0}
INFO: Serving event with id: 0
INFO: Event was added to the queue: MyEvent{eventId=1}
INFO: Event was added to the queue: MyEvent{eventId=2}
INFO: Event was added to the queue: MyEvent{eventId=3}
WARNING: Unable to add event to queue due to timeout: MyEvent{eventId=4}
WARNING: Unable to add event to queue due to timeout: MyEvent{eventId=5}
WARNING: Unable to add event to queue due to timeout: MyEvent{eventId=6}
WARNING: Unable to add event to queue due to timeout: MyEvent{eventId=7}
INFO: Serving event with id: 1
INFO: Event was added to the queue: MyEvent{eventId=8}
WARNING: Unable to add event to queue due to timeout: MyEvent{eventId=9}
INFO: Serving event with id: 2
INFO: Serving event with id: 3
INFO: Serving event with id: 8

Source Code
You can run this sample on your own. Get the conde from my GitHub: https://github.com/yacekmm/looksok/tree/ProducerConsumer/Java/BlockingQueue

Inspirations

This post was inspired by the book ‘Well Grounded Java Developer’ and sites: http://ashkrit.blogspot.com/2014/01/java-queues-bad-practices.html, http://javarevisited.blogspot.com/2012/12/blocking-queue-in-java-example-ArrayBlockingQueue-LinkedBlockingQueue.html





Java 7 NIO.2 Paths to make path handling easy for you

12 12 2015

Java 7 introduced a big change in file and directory structure manipulation API. Now you have:

  • Path that represents locacation in a filesystem (filesystem can be Windows, *nix or a zip archive)
  • Files to deal with directory trees (walk directory tree, iterate over files in directory, filter files, perform recursive move, copy, delete)
  • WatchService to listen for the changes in file
  • File attributes access like size, permissions, modification time
  • Symbolic links detection and support
  • Asynchronous I/O operations (with Future or callback style) for processing big files without impact on main application Thread

Now its time for short demo of new Path features. See the demo source code on my GitHub: https://github.com/yacekmm/looksok/tree/PathsDemo/Java/Java7NIO.2

Creating a Path

See how to get the absolute path to the current working directory:

Path path = Paths.get("").toAbsolutePath();
System.out.println("Current working dir absolute path is: " + path);

Current absolute path is:

C:\Code\looksok\looksok\Java\Java7NIO.2

Path properties

On linux system you can analyze the path properties with some convenient methods:

path = Paths.get("/var/lib/jenkins/jobs/sampleJob");
System.out.println("\nSample linux path that will be analyzed: " + path.toAbsolutePath());
System.out.println("Number of segments in path: " + path.getNameCount());
System.out.println("Parent in path: " + path.getParent());
System.out.println("Root on this filesystem: " + path.getRoot());
System.out.println("Subpath (2 elements up): " + path.subpath(path.getNameCount() - 2, path.getNameCount()));

Notice that I’ve created linux Path on Windows machine. This is possible because path is an abstract concept that may point to non existing resource. Because it can be a path to resource that you are just about to create in that Path. Meaning that the path existence will be verified already when you want to do something in this location.

The properties check result is:

Sample linux path that will be analyzed: C:\var\lib\jenkins\jobs\sampleJob
Number of segments in path: 5
Parent in path: \var\lib\jenkins\jobs
Root on this filesystem: \
Subpath (2 elements up): jobs\sampleJob

Symbolic link path finding

When you work with paths containing Symbolic links (kind of shortcuts to the target file on linux OS) you need to know whether it is a file or symlink and get the real location of the file behind the link. Do it with:

path = Paths.get("/var/log/logFile");
System.out.println("Real symlink path: " + path.toRealPath());

getRealPath() throws IOException if Path does not exist. You can also getRealPath(LinkOptions.NOFOLLOW_LINKS) if you want a symlink file path, not the element behind it.

Converting paths

In NIO.2 you can easily join Paths, create a Path between two other Paths, and compare Paths against each other. This is how you join paths

Path currentDir = Paths.get("");
Path relativeConfigPath = Paths.get("conf/app.properties");
Path propertiesFilesPath = currentDir.resolve(relativeConfigPath);

The resolved path is:

C:\Code\looksok\looksok\Java\Java7NIO.2\conf\app.properties

Path between two other Paths

This is how to get the relative path between two other Paths:

Path logsDir = Paths.get("/var/log/myapp");
Path appDir = Paths.get("/var/lib/myapp");
Path relativePathToLogs = appDir.relativize(logsDir);

And the result is a relative path:

..\..\log\myapp

NIO.2 and existing Java’s File class

You can now refactor pre Java 7 code to use new constructs, however if you need to interact with old APIs using File class, be noticed that File has now toPath() method, as well as the Path having its toFile() equivalent:

File file = new File("./PathDemo.java");
Path pathFromFile = file.toPath();
file = pathFromFile.toFile();







%d bloggers like this: