Spring MVC controller methods with @RequestMapping, @PathVariable, @RequestParam, and @RequestBody
A comprehensive demonstration of Spring MVC controller methods featuring @RequestMapping attributes, HTTP method shortcuts (@GetMapping, @PostMapping), parameter handling (@PathVariable, @RequestParam, @RequestBody), and response configuration.
@Controllervs@RestController@RequestMappingwith multiple attributes (path, method, params, headers, consumes, produces)- HTTP method shortcuts (
@GetMapping,@PostMapping,@PutMapping,@DeleteMapping) @PathVariablefor URL path variables@RequestParamfor query parameters@RequestBodyfor JSON request body@ResponseBodyfor JSON response@ResponseStatusfor HTTP status codesparamscondition matching (params vs no params)consumesandproducesfor media type control- RESTful API design demonstration
- H2 database integration
- Spring Boot 3.4.5
- Spring MVC 6.2.5
- Spring Data JPA
- Java 21
- H2 Database 2.3.232
- Joda Money 2.0.2
- Lombok
- Maven 3.8+
- JDK 21 or higher
- Maven 3.8+ (or use included Maven Wrapper)
Run the application:
./mvnw spring-boot:runTest the API:
# Get all coffees
curl http://localhost:8080/coffee/
# Get coffee by ID
curl http://localhost:8080/coffee/1
# Get coffee by name
curl "http://localhost:8080/coffee/?name=mocha"# JPA/Hibernate configuration
spring.jpa.hibernate.ddl-auto=none
spring.jpa.properties.hibernate.show_sql=true
spring.jpa.properties.hibernate.format_sql=true
# Error response configuration (for development only)
server.error.include-message=always
server.error.include-binding-errors=alwaysImportant:
show_sql=true: Show SQL statements (development only)include-message=always: Include error messages in response (development only)
curl -X GET http://localhost:8080/coffee/Endpoint: GET /coffee/
Condition: No name parameter (params = "!name")
Response: List of all coffees
Sample Response:
[
{
"id": 1,
"name": "espresso",
"price": "TWD 100.00",
"createTime": "2025-10-16T16:52:02",
"updateTime": "2025-10-16T16:52:02"
},
{
"id": 2,
"name": "latte",
"price": "TWD 125.00",
"createTime": "2025-10-16T16:52:02",
"updateTime": "2025-10-16T16:52:02"
}
]curl -X GET http://localhost:8080/coffee/1Endpoint: GET /coffee/{id}
Path Variable: id (Long)
Produces: application/json
Sample Response:
{
"id": 1,
"name": "espresso",
"price": "TWD 100.00",
"createTime": "2025-10-16T16:52:02",
"updateTime": "2025-10-16T16:52:02"
}curl -X GET "http://localhost:8080/coffee/?name=mocha"Endpoint: GET /coffee/
Condition: Has name parameter (params = "name")
Query Parameter: name (String)
Sample Response:
{
"id": 4,
"name": "mocha",
"price": "TWD 150.00",
"createTime": "2025-10-16T16:52:02",
"updateTime": "2025-10-16T16:52:02"
}curl -X GET http://localhost:8080/order/1Endpoint: GET /order/{id}
Path Variable: id (Long)
Sample Response:
{
"id": 1,
"customer": "Ray Chu",
"items": [
{
"id": 4,
"name": "mocha",
"price": "TWD 150.00",
"createTime": "2025-10-16T16:52:02",
"updateTime": "2025-10-16T16:52:02"
}
],
"state": "INIT",
"createTime": "2025-10-16T16:52:02",
"updateTime": "2025-10-16T16:52:02"
}curl -X POST http://localhost:8080/order/ \
-H "Content-Type: application/json" \
-H "Accept: application/json" \
-d '{
"customer": "Ray Chu",
"items": ["mocha", "latte"]
}'Endpoint: POST /order/
Consumes: application/json (required)
Produces: application/json (required)
Request Body: NewOrderRequest object
Response Status: 201 CREATED
Sample Response:
{
"id": 2,
"customer": "Ray Chu",
"items": [
{
"id": 4,
"name": "mocha",
"price": "TWD 150.00",
"createTime": "2025-10-16T16:52:02",
"updateTime": "2025-10-16T16:52:02"
},
{
"id": 2,
"name": "latte",
"price": "TWD 125.00",
"createTime": "2025-10-16T16:52:02",
"updateTime": "2025-10-16T16:52:02"
}
],
"state": "INIT",
"createTime": "2025-10-16T16:52:05",
"updateTime": "2025-10-16T16:52:05"
}@Controller
@RequestMapping("/coffee")
public class CoffeeController {
@Autowired
private CoffeeService coffeeService;
/**
* Get all coffees
* Condition: When NO name parameter exists
* params = "!name" means "name parameter must not exist"
*/
@GetMapping(path = "/", params = "!name")
@ResponseBody
public List<Coffee> getAll() {
return coffeeService.getAllCoffee();
}
/**
* Get coffee by ID
* @PathVariable: Extract id from URL path
* produces: Only produce application/json response
*/
@RequestMapping(path = "/{id}", method = RequestMethod.GET,
produces = MediaType.APPLICATION_JSON_VALUE)
@ResponseBody
public Coffee getById(@PathVariable Long id) {
Coffee coffee = coffeeService.getCoffee(id);
return coffee;
}
/**
* Get coffee by name
* Condition: When name parameter exists
* params = "name" means "name parameter must exist"
* @RequestParam: Extract name from query parameter
*/
@GetMapping(path = "/", params = "name")
@ResponseBody
public Coffee getByName(@RequestParam String name) {
return coffeeService.getCoffee(name);
}
}Annotation Explanation:
- @Controller: Mark class as MVC controller
- @RequestMapping("/coffee"): Base path for all methods
- @ResponseBody: Convert return value to HTTP response body
- params = "!name": Match when
nameparameter does NOT exist - params = "name": Match when
nameparameter EXISTS - @PathVariable: Extract variable from URL path
- @RequestParam: Extract value from query parameter
URL Routing:
GET /coffee/→getAll()(no name param)GET /coffee/?name=mocha→getByName()(has name param)GET /coffee/1→getById()(path variable)
@RestController
@RequestMapping("/order")
@Slf4j
public class CoffeeOrderController {
@Autowired
private CoffeeOrderService orderService;
@Autowired
private CoffeeService coffeeService;
/**
* Get order by ID
* @RestController automatically includes @ResponseBody
*/
@GetMapping("/{id}")
public CoffeeOrder getOrder(@PathVariable("id") Long id) {
return orderService.get(id);
}
/**
* Create new order
* consumes: Only accept application/json
* produces: Only produce application/json
* @RequestBody: Extract JSON from request body
* @ResponseStatus: Set response status to 201 CREATED
*/
@PostMapping(path = "/",
consumes = MediaType.APPLICATION_JSON_VALUE,
produces = MediaType.APPLICATION_JSON_VALUE)
@ResponseStatus(HttpStatus.CREATED)
public CoffeeOrder create(@RequestBody NewOrderRequest newOrder) {
log.info("Receive new Order {}", newOrder);
Coffee[] coffeeList = coffeeService.getCoffeeByName(newOrder.getItems())
.toArray(new Coffee[] {});
return orderService.createOrder(newOrder.getCustomer(), coffeeList);
}
}Annotation Explanation:
- @RestController:
@Controller+@ResponseBodycombined - consumes = APPLICATION_JSON_VALUE: Only accept JSON requests
- produces = APPLICATION_JSON_VALUE: Only produce JSON responses
- @RequestBody: Auto-convert JSON to Java object
- @ResponseStatus(CREATED): Set HTTP status to 201
@Getter
@Setter
@ToString
public class NewOrderRequest {
private String customer; // Customer name
private List<String> items; // List of coffee names
}Sample JSON:
{
"customer": "Ray Chu",
"items": ["mocha", "latte"]
}@RequestMapping(
path = "/coffee", // URL path
method = RequestMethod.GET, // HTTP method
params = "name", // Parameter condition
headers = "X-Custom-Header", // Header condition
consumes = "application/json", // Accept Content-Type
produces = "application/json" // Response Content-Type
)// Equivalent shortcuts
@GetMapping("/coffee") // GET
@PostMapping("/order") // POST
@PutMapping("/order/{id}") // PUT
@DeleteMapping("/order/{id}") // DELETE
@PatchMapping("/order/{id}") // PATCH
// Instead of verbose @RequestMapping
@RequestMapping(path = "/coffee", method = RequestMethod.GET)// @PathVariable: From URL path
@GetMapping("/coffee/{id}")
public Coffee get(@PathVariable Long id) { }
// @RequestParam: From query string
@GetMapping("/coffee")
public Coffee get(@RequestParam String name) { }
// @RequestBody: From request body
@PostMapping("/order")
public Order create(@RequestBody NewOrderRequest request) { }
// @RequestHeader: From HTTP headers
@GetMapping("/coffee")
public Coffee get(@RequestHeader("User-Agent") String userAgent) { }// @ResponseBody: Convert to HTTP response
@ResponseBody
public Coffee get() { }
// @ResponseStatus: Set HTTP status code
@ResponseStatus(HttpStatus.CREATED)
public Order create() { }
// @RestController: Auto @ResponseBody for all methods
@RestController
public class OrderController { }// Method 1: Match when NO name parameter
@GetMapping(path = "/", params = "!name")
public List<Coffee> getAll() { }
// Method 2: Match when name parameter EXISTS
@GetMapping(path = "/", params = "name")
public Coffee getByName(@RequestParam String name) { }URL Routing:
| URL | Matched Method | Explanation |
|---|---|---|
GET /coffee/ |
getAll() |
No name param → matches params = "!name" |
GET /coffee/?name=mocha |
getByName() |
Has name param → matches params = "name" |
GET /coffee/1 |
getById() |
Path variable → different mapping |
Advanced Params:
// Multiple conditions
@GetMapping(params = {"name", "size"}) // Both must exist
// Value matching
@GetMapping(params = "name=mocha") // name must equal "mocha"
// Negation
@GetMapping(params = "!debug") // debug param must not exist@PostMapping(path = "/order/",
consumes = MediaType.APPLICATION_JSON_VALUE)
public Order create(@RequestBody NewOrderRequest request) { }Test:
# ✅ Correct: Content-Type is application/json
curl -X POST http://localhost:8080/order/ \
-H "Content-Type: application/json" \
-d '{"customer": "Ray", "items": ["mocha"]}'
# ❌ Wrong: Content-Type is application/pdf
curl -X POST http://localhost:8080/order/ \
-H "Content-Type: application/pdf" \
-d '{"customer": "Ray", "items": ["mocha"]}'
# Error: 415 Unsupported Media Type@GetMapping(path = "/{id}",
produces = MediaType.APPLICATION_JSON_VALUE)
public Coffee getById(@PathVariable Long id) { }Test:
# ✅ Correct: Accept is application/json (or not specified)
curl -H "Accept: application/json" http://localhost:8080/coffee/1
# ❌ Wrong: Accept is application/xml (not supported)
curl -H "Accept: application/xml" http://localhost:8080/coffee/1
# Error: 406 Not Acceptable| Code | Status | Usage | This Project |
|---|---|---|---|
| 200 | OK | Successful GET | getAll(), getById(), getByName(), getOrder() |
| 201 | Created | Successful POST | create() with @ResponseStatus(CREATED) |
| 204 | No Content | Successful DELETE | - |
| 400 | Bad Request | Invalid request body | Missing required fields |
| 404 | Not Found | Resource not found | Coffee or Order not exists |
| 406 | Not Acceptable | Accept header mismatch | Accept: application/xml |
| 415 | Unsupported Media Type | Content-Type mismatch | Content-Type: application/pdf |
// Automatic 200 OK
@GetMapping("/order/{id}")
public Order getOrder(@PathVariable Long id) { }
// Explicit 201 CREATED
@PostMapping("/order/")
@ResponseStatus(HttpStatus.CREATED)
public Order create(@RequestBody NewOrderRequest request) { }
// 204 NO_CONTENT (for delete)
@DeleteMapping("/order/{id}")
@ResponseStatus(HttpStatus.NO_CONTENT)
public void delete(@PathVariable Long id) { }1. Get All Coffees:
curl -v http://localhost:8080/coffee/2. Get Coffee by ID:
curl -v http://localhost:8080/coffee/13. Get Coffee by Name:
curl -v "http://localhost:8080/coffee/?name=mocha"4. Get Order:
curl -v http://localhost:8080/order/15. Create Order (Success):
curl -v -X POST http://localhost:8080/order/ \
-H "Content-Type: application/json" \
-H "Accept: application/json" \
-d '{
"customer": "Ray Chu",
"items": ["mocha", "latte"]
}'
# Response: 201 CREATED6. Create Order (Error - Wrong Content-Type):
curl -v -X POST http://localhost:8080/order/ \
-H "Content-Type: application/pdf" \
-H "Accept: application/json" \
-d '{
"customer": "Ray Chu",
"items": ["mocha"]
}'
# Response: 415 Unsupported Media Type7. Create Order (Error - Wrong Accept):
curl -v -X POST http://localhost:8080/order/ \
-H "Content-Type: application/json" \
-H "Accept: application/xml" \
-d '{
"customer": "Ray Chu",
"items": ["mocha"]
}'
# Response: 406 Not AcceptableCreate Order:
- Method: POST
- URL:
http://localhost:8080/order/ - Headers:
Content-Type:application/jsonAccept:application/json
- Body (raw JSON):
{ "customer": "Ray Chu", "items": ["mocha", "latte"] }
@Controller
@RequestMapping("/coffee")
public class CoffeeController {
@GetMapping("/")
@ResponseBody // Required for JSON response
public List<Coffee> getAll() {
return coffeeService.getAllCoffee();
}
}Characteristics:
- Traditional MVC controller
- Needs
@ResponseBodyfor each JSON method - Can return view names (for template engines)
@RestController
@RequestMapping("/order")
public class OrderController {
@GetMapping("/{id}")
// No @ResponseBody needed!
public Order getOrder(@PathVariable Long id) {
return orderService.get(id);
}
}Characteristics:
@Controller+@ResponseBodycombined- Automatic JSON response for all methods
- Best for RESTful APIs
Selection Guide:
- @Controller: For traditional MVC (returning views)
- @RestController: For RESTful APIs (returning JSON/XML)
// ✅ Recommended: Use shortcuts
@GetMapping("/orders")
@PostMapping("/orders")
@PutMapping("/orders/{id}")
@DeleteMapping("/orders/{id}")
// ❌ Not recommended: Verbose @RequestMapping
@RequestMapping(path = "/orders", method = RequestMethod.GET)
@RequestMapping(path = "/orders", method = RequestMethod.POST)// ✅ Recommended: Explicit name
@GetMapping("/orders/{orderId}")
public Order get(@PathVariable("orderId") Long orderId) { }
// ⚠️ Acceptable: Same name
@GetMapping("/orders/{id}")
public Order get(@PathVariable Long id) { } // Variable name = path name// ✅ Recommended: Set defaults and required
@GetMapping("/orders")
public List<Order> getOrders(
@RequestParam(defaultValue = "0") int page,
@RequestParam(defaultValue = "10") int size,
@RequestParam(required = false) String status) {
// Implementation
}// ✅ Recommended: Explicit media types
@PostMapping(path = "/orders",
consumes = MediaType.APPLICATION_JSON_VALUE,
produces = MediaType.APPLICATION_JSON_VALUE)
public Order create(@RequestBody OrderRequest request) { }
// ⚠️ Acceptable: Default (any media type)
@PostMapping("/orders")
public Order create(@RequestBody OrderRequest request) { }// ✅ Recommended: Explicit status codes
@PostMapping("/orders")
@ResponseStatus(HttpStatus.CREATED) // 201
public Order create(@RequestBody OrderRequest request) { }
@DeleteMapping("/orders/{id}")
@ResponseStatus(HttpStatus.NO_CONTENT) // 204
public void delete(@PathVariable Long id) { }
@PutMapping("/orders/{id}")
@ResponseStatus(HttpStatus.OK) // 200 (default)
public Order update(@PathVariable Long id, @RequestBody OrderRequest request) { }// Add Jakarta Bean Validation
import jakarta.validation.Valid;
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.NotEmpty;
@PostMapping("/orders")
@ResponseStatus(HttpStatus.CREATED)
public Order create(@Valid @RequestBody NewOrderRequest request) { }
// NewOrderRequest with validation
public class NewOrderRequest {
@NotBlank(message = "Customer name is required")
private String customer;
@NotEmpty(message = "Items list cannot be empty")
private List<String> items;
}Error:
{
"timestamp": "2025-10-16T16:52:02.000+00:00",
"status": 415,
"error": "Unsupported Media Type",
"message": "Content-Type 'application/pdf' is not supported"
}Cause: Request Content-Type doesn't match consumes attribute
Solution:
# Ensure Content-Type is application/json
curl -X POST http://localhost:8080/order/ \
-H "Content-Type: application/json" \
-d '{"customer": "Ray", "items": ["mocha"]}'Error:
{
"timestamp": "2025-10-16T16:52:02.000+00:00",
"status": 406,
"error": "Not Acceptable",
"message": "Could not find acceptable representation"
}Cause: Request Accept header doesn't match produces attribute
Solution:
# Ensure Accept is application/json or omit it
curl -X POST http://localhost:8080/order/ \
-H "Content-Type: application/json" \
-H "Accept: application/json" \
-d '{"customer": "Ray", "items": ["mocha"]}'Error:
{
"timestamp": "2025-10-16T16:52:02.000+00:00",
"status": 400,
"error": "Bad Request",
"message": "Required request body is missing"
}Cause: Missing request body for @RequestBody
Solution:
# Include request body
curl -X POST http://localhost:8080/order/ \
-H "Content-Type: application/json" \
-d '{
"customer": "Ray Chu",
"items": ["mocha"]
}'Problem: Wrong method is called
Example:
# Want: getAll() - all coffees
curl http://localhost:8080/coffee/
# Get: Error if name param exists
curl "http://localhost:8080/coffee/?name=mocha"Solution: Check params attribute matches request
// No name param → getAll()
@GetMapping(path = "/", params = "!name")
// Has name param → getByName()
@GetMapping(path = "/", params = "name")schema.sql:
drop table t_coffee if exists;
drop table t_order if exists;
drop table t_order_coffee if exists;
create table t_coffee (
id bigint auto_increment,
create_time timestamp,
update_time timestamp,
name varchar(255),
price bigint,
primary key (id)
);
create table t_order (
id bigint auto_increment,
create_time timestamp,
update_time timestamp,
customer varchar(255),
state integer not null,
primary key (id)
);
create table t_order_coffee (
coffee_order_id bigint not null,
items_id bigint not null
);
insert into t_coffee (name, price, create_time, update_time)
values ('espresso', 10000, now(), now());
insert into t_coffee (name, price, create_time, update_time)
values ('latte', 12500, now(), now());
insert into t_coffee (name, price, create_time, update_time)
values ('capuccino', 12500, now(), now());
insert into t_coffee (name, price, create_time, update_time)
values ('mocha', 15000, now(), now());
insert into t_coffee (name, price, create_time, update_time)
values ('macchiato', 15000, now(), now());1. Use Proper HTTP Methods:
// ✅ Recommended
GET /orders // List all orders
GET /orders/{id} // Get specific order
POST /orders // Create new order
PUT /orders/{id} // Update entire order
PATCH /orders/{id} // Update partial order
DELETE /orders/{id} // Delete order2. Use Plural Nouns:
// ✅ Recommended
/orders
/coffees
// ❌ Not recommended
/order
/coffee3. Use Nested Resources:
// ✅ Recommended
GET /orders/{orderId}/items // Get order items
POST /orders/{orderId}/items // Add item to order
// ❌ Not recommended
GET /order-items?orderId=14. Use Query Parameters for Filtering:
// ✅ Recommended
GET /orders?status=PAID&customer=Ray
// Implementation
@GetMapping("/orders")
public List<Order> getOrders(
@RequestParam(required = false) String status,
@RequestParam(required = false) String customer) {
// Filter logic
}5. Return Appropriate Status Codes:
// ✅ Recommended
@PostMapping("/orders")
@ResponseStatus(HttpStatus.CREATED) // 201
public Order create() { }
@DeleteMapping("/orders/{id}")
@ResponseStatus(HttpStatus.NO_CONTENT) // 204
public void delete() { }- @RequestMapping Attributes: path, method, params, consumes, produces
- HTTP Method Shortcuts: @GetMapping, @PostMapping
- Parameter Handling: @PathVariable, @RequestParam, @RequestBody
- Response Configuration: @ResponseBody, @ResponseStatus
- Params Condition: Dynamic routing based on query parameters
- Media Type Control: Restrict request/response formats
- RESTful API Design: Standard HTTP methods and status codes
- Spring MVC Documentation
- Spring Boot Web Documentation
- RESTful API Design Best Practices
- HTTP Status Codes
MIT License - see LICENSE file for details.
我們主要專注在敏捷專案管理、物聯網(IoT)應用開發和領域驅動設計(DDD)。喜歡把先進技術和實務經驗結合,打造好用又靈活的軟體解決方案。近來也積極結合 AI 技術,推動自動化工作流,讓開發與運維更有效率、更智慧。持續學習與分享,希望能一起推動軟體開發的創新和進步。
風清雲談 - 專注於敏捷專案管理、物聯網(IoT)應用開發和領域驅動設計(DDD)。
- 🌐 官方網站:風清雲談部落格
- 📘 Facebook:風清雲談粉絲頁
- 💼 LinkedIn:Chu Kuo-Lung
- 📺 YouTube:雲談風清頻道
- 📧 Email:fengqing.tw@gmail.com
⭐ 如果這個專案對您有幫助,歡迎給個 Star!