Path variables are similar to query parameters in that both are stored and transported within a URL. However, some distinct differences exist in what they are and how they are used. This lesson will cover the relevant differences and how you can use the @PathVariable annotation to pull data from the path of a URL.
Example Location:com.codingnomads.springweb.gettingdatafromclient.pathvariable
Path Variables vs. Query Parameters
Here are some of the most important differences between path variables and query parameters:
-
Order Matters: The order of the elements in a URL path matter, while those of query parameters don't. This is because a query parameter is always paired with its key. This makes it easy for a URL parser to correctly identify the data regardless of its order. With path variables, this is not the case. A path variable does not use a key-value pair structure, so its placement relative to all other path elements is essential for identification.
-
Encoded or Unencoded: Query parameters are generally always encoded, and Spring automatically decodes them. Path variables can be encoded too, and if you enter encoded characters like
%20for a space, Spring will decode it to a regular space character. So, forthis%20thatas a path variable, Spring would automatically decode it intothis that. It's also worth noting that certain characters like+can be left as is by Spring if they're part of a path segment. -
Readability: There is also a stylistic difference between the two. Some people find path variables to have more flow than query parameters. Here's an example: One could set up the path
/friend/add/548856743. This URL structure uses only path variables to indicate the action anduserID, but this individual could also decide to use only query parameters like so/friend?action=add&userId=548856743. Both of these are functionally the same, but some may find the structure of the path to be more intuitive and concise than that of the query parameters. The choice is yours, but remember you can mix and match as you please. They are not mutually exclusive.
Spring @PathVariable
Now that you have the relevant information on path variables, it's time to learn how to use them in your APIs. In Spring projects, you'll be using the @PathVariable annotation. There are two components to using @PathVariable:
- Changing the value of a
@RequestMappingto facilitate path variables. - The actual annotation and its options.
Augmenting @RequestMapping
Before you can use @PathVariable to pull information from the URL, you need to learn how to properly change your URL path to allow for path variable identification. Spring makes this process relatively painless. Let's use the same example in the above readability explanation: /friend/add/548856743.
This URL has two path variables: an action followed by a user ID. To add these to your @RequestMapping URL path, you have to name the variables. You might choose action and userId for this example. To then indicate to Spring that these are path variables, you surround the name with curly brackets like this: {action} and {userId}. With that information, you can probably figure out that the URL path structure would look like this:
/friend/{action}/{userId}
Where friend is constant, and action & userId can vary freely. As a refresher, the @RequestMapping annotation for this URL would look like this:
@RequestMapping(path = "/friend/{action}/{userId}", method = RequestMethod.GET)
Or better yet:
@GetMapping("/friend/{action}/{userId}")
Using @PathVariable
This annotation is used in the same location as @RequestParam. The code below shows you how. It expands on the previously mentioned friend endpoint:
@GetMapping("/friend/{action}/{userId}")
public ResponseEntity<?> friending(
@PathVariable String action, @PathVariable Long userId) {
if (action.equalsIgnoreCase("add")) {
// add user to friend list
return ResponseEntity.ok(
"Your friend request to " + userId + " has been sent!");
} else if (action.equalsIgnoreCase("remove")) {
// remove user from friend list
return ResponseEntity.ok(
"You removed your friend with ID " + userId);
} else if (action.equalsIgnoreCase("block")) {
// remove user from friend list and add to blocked list
return ResponseEntity.ok(
"You blocked the user with ID " + userId);
} else {
return ResponseEntity
.badRequest()
.body("You added an invalid action! " +
"The possible actions are add, remove, block. " +
"Check your request and try again.");
}
}
The method above looks big and complicated but is easy to understand. Starting with the usage of @PathVariable, let's go over what this method does:
- The method uses
@PathVariableto extract theactionanduserIdfrom the URL passed in. - Depending on the action specified in the path, the method either removes, adds, or blocks the user for the individual making the request. It is also possible the user entered an invalid action, and an error is thrown in that case.
- Lastly, ResponseEntity informs the user of the action taken or the error thrown.
Let's dive into Step 1. After all, this is a lesson on @PathVariable!
Path Variable Names
This example may have you wondering how Spring correctly maps the path variable data onto the intended Java variable. Is it by order, by name, or random? The random answer should make you cringe; the order answer would be possible but buggy. The name idea is best/least ambiguous. The creators of Spring therefore opted to match the path and Java variables by name. Just as you learned with @RequestParam, there are two different ways this is done:
- If your
@RequestMappingannotation matches your Java variable name,@PathVariablewill automatically match and map them. - To use a different name in your path than in your method, you specify the name of the path variable like so:
@PathVariable(name = "name_of_path_variable)or simply like this:@PathVariable("name_of_path_variable").
You just saw an example of the first option above, now here's an example of the second:
@PostMapping("/file/{name}/{access}")
public ResponseEntity<String> createNewFile(
@PathVariable(name = "name") String fileName,
@PathVariable(name = "access") String fileAccessType) {
// try with resources opens output stream
try (FileWriter fileWriter =
new FileWriter("src/main/resources/" + fileName + ".txt")) {
// create access type message to be written at top of file
String accessString = "FILE ACCESS: " + fileAccessType;
while (accessString.length() != 0) {
// write character to file
fileWriter.write(accessString.substring(0, 1));
// remove character from accessString to prevent infinite loop
accessString = accessString.substring(1);
}
// return success message
return ResponseEntity.ok("The file was successfully created");
} catch (IOException e) {
// return internal server error message
return ResponseEntity
.status(500)
.body("Something went wrong.");
}
}
Making Path Variable Optional
It is also possible to make a path variable optional using the required attribute. To do this, you add an extra path structure in case the optional path variable is missing. For example, the code below shows the same file creator example but this time the access path variable is optional.
@PostMapping({"/file/{name}/{access}", "/file/{name}"})
public ResponseEntity<String> createNewFile(
@PathVariable(name = "name") String fileName,
@PathVariable(required = false) String access) {
//assign access a default if it is not passed in
if (access == null) {
access = "public";
}
//try with resources opens output stream
try (FileWriter fileWriter =
new FileWriter("src/main/resources/" + fileName + ".txt")) {
// create access type message to be written at top of file
String accessString = "FILE ACCESS: " + access;
while (accessString.length() != 0) {
// write character to file
fileWriter.write(accessString.substring(0, 1));
// remove character from accessString to prevent infinite loop
accessString = accessString.substring(1);
}
// return success message
return ResponseEntity.ok("The file was successfully created");
} catch (IOException e) {
// return internal server error message
return ResponseEntity
.status(500)
.body("Something went wrong.");
}
}
The three big things to notice in this example are:
- The
@PostMappingannotation has two possible endpoints. One without the{access}path variable and one with. - The
requiredparameter in theaccess@PathVariableannotation. - The null check to make sure that
accessalways has an assigned value.
Info: Unlike @RequestParam, @PathVariable does not have a default attribute. This means an optional path variable will always require a null check, or you can expect an NullPointerException when it's not present.
To take this one step further, you can also make the name path variable optional. Doing this would require:
- Adding
/fileas a third URL structure in the@PostMappingannotation. - Toggling the
requiredboolean in the@PathVariableannotation and setting up a null check.
Making name optional and adding a default would not make much sense in this context, but you may encounter a situation where path two variables should be optional. Keep in mind that if you decided to use optional path variables, that you cannot mix and match optionality. What does this mean? Using the example above, it would not be possible to specify only an access parameter while leaving the name path variable empty. If you tried to do this, Spring would interpret the access type in the URL as the file's name. This is where path variables are limited.
Storing Path Variables in a Map
If you have a lot of path variables, or you find storing data in a Map more intuitive, you can have all your path variables returned to you in a Map<String, String> instead of individual variables. Doing this requires only a small change to your method parameters. Instead of creating a new Java variable for each path variable, you add a single Map<String, String> parameter annotated with @PathVariable. This way, all path variables will be placed into the map, where each path variable name is the key and what's passed in by the client is the value. Take a look at this in code. The first example here shows you how this works without optional path variables and the second shows you with:
// without optional path variables
@GetMapping("/with-map/{id}/{name}/{completed}")
public Task getTask(
@PathVariable Map<String, String> pathVariableMap) {
return Task.builder()
.id(Long.valueOf(pathVariableMap.get("id")))
.name(pathVariableMap.get("name"))
.completed(
Boolean.parseBoolean(
pathVariableMap.get("completed")))
.build();
}
// with optional path variables
@GetMapping({
"/with-map/{id}/{name}/{completed}",
"/with-map/{id}/{name}",
"/with-map/{id}",
"/with-map"})
public Task getTask(@PathVariable Map<String, String> pathVariableMap) {
return Task.builder()
.id(Long.valueOf(pathVariableMap.get("id")))
.name(pathVariableMap.get("name"))
.completed(
Boolean.parseBoolean(
pathVariableMap.get("completed")))
.build();
}
There are both ups and downs to using the map storage solution. First, the ups:
- You have a much smaller number of method parameters, making the method signature more readable.
- Specifying an optional path variable is only a matter of changing the path structure in
@RequestMapping(or@GetMappingin this case).
And the downs:
- For some actions, you may have to manually assign values from the map.
- You must manually translate/parse all path variable values from Strings stored in a map into other data types.
In short, if you have upwards of five path variables, it may be worth using a map, otherwise it is most likely cleaner to declare multiple method parameters. But like most things, the decision is up to you - the developer!
Wrapping Up
Wow, that was a lot of information! The core focus here is that @PathVariable lets you map data from path variables to your method parameters. Use for the rest of the options discussed here will come in time.
Learn by Doing
Package: springweb.gettingdatafromclient.pathvariable
Path variable time!
- Run PathVariableDemo and hit the existing endpoints using Postman.
- Carefully study the results (hanging in the debugger never hurts!).
- Inside TaskController, create at least three new methods/endpoints that utilize the
@PathVariableannotation. - Be sure to test your work, great job!
Please push your work to GitHub when you're done.
Summary: @PathVariable
Much like a query parameter, a path variable is used to pass information from the client to the server via the URL. But there are a few differences:
- Order matters for path variables.
- Query parameters are encoded, path variables usually are not (but they can be).
- Some find that path variables are more easily readable or logical than query parameters
- Path variables require changes to the value of your
@RequestMappingannotation.