7.1.1 Truy xuất dữ liệu từ máy chủ
Một điều mà chúng ta muốn ứng dụng Taco Cloud có thể làm là cho phép những người yêu thích taco thiết kế các món taco theo ý thích và chia sẻ chúng với những người đam mê taco khác. Một cách để làm điều đó là hiển thị danh sách các món taco được tạo gần đây nhất trên trang web.
Để hỗ trợ tính năng đó, chúng ta cần tạo một endpoint xử lý các yêu cầu GET đến /api/tacos có chứa tham số “recent” và phản hồi với danh sách các món taco được thiết kế gần đây. Bạn sẽ tạo một controller mới để xử lý yêu cầu như vậy. Danh sách dưới đây cho thấy controller sẽ đảm nhiệm công việc này.
Danh sách 7.1 Một RESTful controller cho các yêu cầu API thiết kế taco
package tacos.web.api;
import org.springframework.data.domain.PageRequest;
import org.springframework.data.domain.Sort;
import org.springframework.web.bind.annotation.CrossOrigin;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import tacos.Taco;
import tacos.data.TacoRepository;
@RestController
@RequestMapping(path="/api/tacos",
produces="application/json")
@CrossOrigin(origins="*")
public class TacoController {
private TacoRepository tacoRepo;
public TacoController(TacoRepository tacoRepo) {
this.tacoRepo = tacoRepo;
}
@GetMapping(params="recent")
public Iterable<Taco> recentTacos() {
PageRequest page = PageRequest.of(
0, 12, Sort.by("createdAt").descending());
return tacoRepo.findAll(page).getContent();
}
}Bạn có thể đang nghĩ rằng tên controller này nghe có vẻ quen thuộc. Ở chương 2, bạn đã tạo một DesignTacoController có tên tương tự xử lý các loại yêu cầu tương tự. Nhưng trong khi controller đó tạo ra kết quả HTML trong ứng dụng Taco Cloud, thì TacoController mới này là một REST controller, như được biểu thị bởi annotation @RestController.
Annotation @RestController phục vụ hai mục đích. Đầu tiên, nó là một annotation kiểu định danh (stereotype) giống như @Controller và @Service, đánh dấu một lớp để được phát hiện bởi component scanning. Nhưng điều quan trọng nhất trong ngữ cảnh REST là annotation @RestController cho Spring biết rằng tất cả các phương thức xử lý trong controller sẽ có giá trị trả về được ghi trực tiếp vào thân của phản hồi, thay vì được mang trong model để hiển thị qua một view.
Ngoài ra, bạn cũng có thể đánh dấu TacoController bằng @Controller, giống như bất kỳ controller Spring MVC nào. Nhưng khi đó, bạn cần đánh dấu tất cả các phương thức xử lý bằng @ResponseBody để đạt được kết quả tương tự. Một lựa chọn khác nữa là trả về một đối tượng ResponseEntity, điều mà chúng ta sẽ thảo luận ngay sau đây.
Annotation @RequestMapping ở cấp lớp hoạt động cùng với annotation @GetMapping trên phương thức recentTacos() để xác định rằng phương thức recentTacos() chịu trách nhiệm xử lý các yêu cầu GET đến /design?recent.
Bạn sẽ nhận thấy rằng annotation @RequestMapping cũng thiết lập thuộc tính produces. Điều này chỉ định rằng bất kỳ phương thức xử lý nào trong TacoController chỉ xử lý yêu cầu nếu client gửi yêu cầu với header Accept bao gồm "application/json", nghĩa là client chỉ có thể xử lý phản hồi ở định dạng JSON. Việc sử dụng produces như vậy sẽ giới hạn API của bạn chỉ trả về kết quả dạng JSON, và điều đó cho phép một controller khác (có thể là TacoController trong chương 2) xử lý các yêu cầu có cùng đường dẫn, miễn là những yêu cầu đó không yêu cầu phản hồi JSON.
Mặc dù thiết lập produces là "application/json" sẽ giới hạn API của bạn chỉ sử dụng định dạng JSON (và điều đó là phù hợp với nhu cầu của bạn), bạn hoàn toàn có thể đặt produces là một mảng String cho nhiều loại nội dung. Ví dụ, để cho phép phản hồi dưới định dạng XML, bạn có thể thêm "text/xml" vào thuộc tính produces như sau:
@RequestMapping(path="/api/tacos",
produces={"application/json", "text/xml"})Một điều khác mà bạn có thể đã nhận thấy trong danh sách 7.1 là lớp này được đánh dấu với @CrossOrigin. Rất thường gặp khi một giao diện người dùng dựa trên JavaScript, chẳng hạn như được viết bằng framework Angular hoặc ReactJS, được phục vụ từ một host và/hoặc port khác với API (ít nhất là hiện tại), và trình duyệt web sẽ ngăn client của bạn truy cập API. Hạn chế này có thể được vượt qua bằng cách bao gồm các header CORS (chia sẻ tài nguyên giữa các nguồn khác nhau) trong phản hồi của máy chủ. Spring giúp bạn dễ dàng áp dụng CORS với annotation @CrossOrigin.
Như được áp dụng ở đây, @CrossOrigin cho phép các client từ localhost, port 8080, truy cập API. Thuộc tính origins chấp nhận một mảng, vì vậy bạn cũng có thể chỉ định nhiều giá trị, như minh họa dưới đây:
@RestController
@RequestMapping(path="/api/tacos",
produces="application/json")
@CrossOrigin(origins={"http://tacocloud:8080", "http://tacocloud.com"})
public class TacoController {
...
}Logic bên trong phương thức recentTacos() khá đơn giản. Nó tạo một đối tượng PageRequest để chỉ định rằng bạn muốn chỉ lấy trang đầu tiên (trang 0) với 12 kết quả, được sắp xếp theo thứ tự giảm dần dựa trên ngày tạo của taco. Nói ngắn gọn, bạn muốn lấy 12 thiết kế taco được tạo gần đây nhất. Đối tượng PageRequest này được truyền vào lời gọi phương thức findAll() của TacoRepository, và nội dung của trang kết quả đó sẽ được trả về cho client (như bạn đã thấy trong danh sách 7.1, sẽ được sử dụng như dữ liệu mô hình để hiển thị cho người dùng).
Giờ đây bạn đã có một API Taco Cloud khởi đầu cho client của mình. Để thử nghiệm trong quá trình phát triển, bạn cũng có thể sử dụng các tiện ích dòng lệnh như curl hoặc HTTPie https://httpie.org/ để kiểm tra API. Ví dụ, dòng lệnh sau đây cho thấy cách bạn có thể truy xuất các taco mới tạo bằng curl:
curl localhost:8080/api/tacos?recentHoặc như thế này nếu bạn thích dùng HTTPie:
http :8080/api/tacos?recentBan đầu, cơ sở dữ liệu sẽ trống, vì vậy kết quả từ các yêu cầu này cũng sẽ trống. Chúng ta sẽ thấy ngay cách xử lý các yêu cầu POST để lưu taco. Nhưng trong lúc chờ đợi, bạn có thể thêm một bean CommandLineRunner để tải sẵn cơ sở dữ liệu với một số dữ liệu thử nghiệm. Phương thức bean CommandLineRunner sau đây cho thấy cách bạn có thể tải trước một vài nguyên liệu và một vài taco:
@Bean
public CommandLineRunner dataLoader(
IngredientRepository repo,
UserRepository userRepo,
PasswordEncoder encoder,
TacoRepository tacoRepo) {
return args -> {
Ingredient flourTortilla = new Ingredient(
"FLTO", "Flour Tortilla", Type.WRAP);
Ingredient cornTortilla = new Ingredient(
"COTO", "Corn Tortilla", Type.WRAP);
Ingredient groundBeef = new Ingredient(
"GRBF", "Ground Beef", Type.PROTEIN);
Ingredient carnitas = new Ingredient(
"CARN", "Carnitas", Type.PROTEIN);
Ingredient tomatoes = new Ingredient(
"TMTO", "Diced Tomatoes", Type.VEGGIES);
Ingredient lettuce = new Ingredient(
"LETC", "Lettuce", Type.VEGGIES);
Ingredient cheddar = new Ingredient(
"CHED", "Cheddar", Type.CHEESE);
Ingredient jack = new Ingredient(
"JACK", "Monterrey Jack", Type.CHEESE);
Ingredient salsa = new Ingredient(
"SLSA", "Salsa", Type.SAUCE);
Ingredient sourCream = new Ingredient(
"SRCR", "Sour Cream", Type.SAUCE);
repo.save(flourTortilla);
repo.save(cornTortilla);
repo.save(groundBeef);
repo.save(carnitas);
repo.save(tomatoes);
repo.save(lettuce);
repo.save(cheddar);
repo.save(jack);
repo.save(salsa);
repo.save(sourCream);
Taco taco1 = new Taco();
taco1.setName("Carnivore");
taco1.setIngredients(Arrays.asList(
flourTortilla, groundBeef, carnitas,
sourCream, salsa, cheddar));
tacoRepo.save(taco1);
Taco taco2 = new Taco();
taco2.setName("Bovine Bounty");
taco2.setIngredients(Arrays.asList(
cornTortilla, groundBeef, cheddar,
jack, sourCream));
tacoRepo.save(taco2);
Taco taco3 = new Taco();
taco3.setName("Veg-Out");
taco3.setIngredients(Arrays.asList(
flourTortilla, cornTortilla, tomatoes,
lettuce, salsa));
tacoRepo.save(taco3);
};
}Bây giờ nếu bạn thử dùng curl hoặc HTTPie để gửi yêu cầu đến endpoint các taco gần đây, bạn sẽ nhận được phản hồi như sau (được định dạng lại cho dễ đọc):
$ curl localhost:8080/api/tacos?recent
[
{
"id": 4,
"name": "Veg-Out",
"createdAt": "2021-08-02T00:47:09.624+00:00",
"ingredients": [
{ "id": "FLTO", "name": "Flour Tortilla", "type": "WRAP" },
{ "id": "COTO", "name": "Corn Tortilla", "type": "WRAP" },
{ "id": "TMTO", "name": "Diced Tomatoes", "type": "VEGGIES" },
{ "id": "LETC", "name": "Lettuce", "type": "VEGGIES" },
{ "id": "SLSA", "name": "Salsa", "type": "SAUCE" }
]
},
{
"id": 3,
"name": "Bovine Bounty",
"createdAt": "2021-08-02T00:47:09.621+00:00",
"ingredients": [
{ "id": "COTO", "name": "Corn Tortilla", "type": "WRAP" },
{ "id": "GRBF", "name": "Ground Beef", "type": "PROTEIN" },
{ "id": "CHED", "name": "Cheddar", "type": "CHEESE" },
{ "id": "JACK", "name": "Monterrey Jack", "type": "CHEESE" },
{ "id": "SRCR", "name": "Sour Cream", "type": "SAUCE" }
]
},
{
"id": 2,
"name": "Carnivore",
"createdAt": "2021-08-02T00:47:09.520+00:00",
"ingredients": [
{ "id": "FLTO", "name": "Flour Tortilla", "type": "WRAP" },
{ "id": "GRBF", "name": "Ground Beef", "type": "PROTEIN" },
{ "id": "CARN", "name": "Carnitas", "type": "PROTEIN" },
{ "id": "SRCR", "name": "Sour Cream", "type": "SAUCE" },
{ "id": "SLSA", "name": "Salsa", "type": "SAUCE" },
{ "id": "CHED", "name": "Cheddar", "type": "CHEESE" }
]
}
]Giờ giả sử bạn muốn cung cấp một endpoint để truy xuất một taco duy nhất dựa trên ID của nó. Bằng cách sử dụng một biến giữ chỗ trong đường dẫn của phương thức xử lý và chấp nhận biến đường dẫn, bạn có thể lấy ID và sử dụng nó để tìm đối tượng Taco thông qua repository như sau:
@GetMapping("/{id}")
public Optional<Taco> tacoById(@PathVariable("id") Long id) {
return tacoRepo.findById(id);
}Vì đường dẫn cơ sở của controller là /api/tacos, nên phương thức này sẽ xử lý các yêu cầu GET cho /api/tacos/{id}, trong đó {id} là phần giữ chỗ. Giá trị thực tế trong yêu cầu được truyền vào tham số id, và được ánh xạ tới {id} thông qua @PathVariable.
Bên trong tacoById(), tham số id được truyền cho phương thức findById() của repository để truy xuất đối tượng Taco. Phương thức findById() trả về một Optional<Taco>, vì có thể không tồn tại taco nào với ID đã cho. Optional<Taco> này được trả về trực tiếp từ phương thức controller.
Spring sau đó sẽ gọi phương thức get() của Optional<Taco> để tạo phản hồi. Nếu ID không khớp với bất kỳ taco nào đã biết, nội dung phản hồi sẽ là “null” và mã trạng thái HTTP sẽ là 200 (OK). Client sẽ nhận được một phản hồi không sử dụng được, nhưng mã trạng thái lại cho biết mọi thứ vẫn ổn. Một cách tiếp cận tốt hơn là trả về phản hồi với mã trạng thái HTTP 404 (NOT FOUND).
Với cách viết hiện tại, không có cách dễ dàng để trả về mã trạng thái 404 từ tacoById(). Nhưng nếu bạn thực hiện một vài chỉnh sửa nhỏ, bạn có thể đặt mã trạng thái một cách thích hợp, như minh họa sau:
@GetMapping("/{id}")
public ResponseEntity<Taco> tacoById(@PathVariable("id") Long id) {
Optional<Taco> optTaco = tacoRepo.findById(id);
if (optTaco.isPresent()) {
return new ResponseEntity<>(optTaco.get(), HttpStatus.OK);
}
return new ResponseEntity<>(null, HttpStatus.NOT_FOUND);
}Bây giờ, thay vì trả về một đối tượng Taco, tacoById() sẽ trả về một ResponseEntity<Taco>. Nếu tìm thấy Taco, bạn bao bọc nó trong một ResponseEntity với mã trạng thái HTTP OK (giống như hành vi trước đây). Nhưng nếu không tìm thấy, bạn trả về một ResponseEntity chứa null và mã trạng thái HTTP NOT FOUND để biểu thị rằng client đang cố gắng truy vấn một taco không tồn tại.
Việc định nghĩa một endpoint trả về thông tin chỉ là bước khởi đầu. Nếu API của bạn cần nhận dữ liệu từ client thì sao? Hãy cùng xem cách bạn có thể viết các phương thức controller xử lý dữ liệu đầu vào từ yêu cầu.
