spring -MVC-02
SpringMVC-11 - 响应
在 SpringMVC 中,响应是服务器对客户端请求的反馈,它可以以多种形式呈现,包括视图名称、ModelAndView 对象、JSON 数据以及重定向等。以下是对 SpringMVC 中不同响应类型的详细介绍:
1. 视图名称
通过返回视图名称,SpringMVC 会将请求转发到对应的视图资源进行渲染。视图资源可以是 JSP、Thymeleaf 模板、FreeMarker 模板等。
@RequestMapping("/home")
public String home(Model model) {
model.addAttribute("message", "Welcome to Spring MVC");
return "home"; // 返回视图名称
}
在上述示例中,home方法返回一个字符串home,SpringMVC 会根据配置的视图解析器,将该字符串解析为实际的视图资源(例如/WEB-INF/views/home.jsp),并将模型数据(message)传递给视图进行渲染。最终,渲染后的视图将作为响应返回给客户端。
视图解析器配置
视图解析器负责将逻辑视图名称解析为实际的视图资源。常见的视图解析器有InternalResourceViewResolver、ThymeleafViewResolver等。
InternalResourceViewResolver
@Bean
public ViewResolver viewResolver() {
InternalResourceViewResolver resolver = new InternalResourceViewResolver();
resolver.setPrefix("/WEB-INF/views/");
resolver.setSuffix(".jsp");
return resolver;
}
ThymeleafViewResolver
@Bean
public ViewResolver thymeleafViewResolver(SpringTemplateEngine templateEngine) {
ThymeleafViewResolver resolver = new ThymeleafViewResolver();
resolver.setTemplateEngine(templateEngine);
resolver.setCharacterEncoding("UTF-8");
return resolver;
}
@Bean
public SpringTemplateEngine templateEngine(TemplateResolver templateResolver) {
SpringTemplateEngine engine = new SpringTemplateEngine();
engine.setTemplateResolver(templateResolver);
return engine;
}
@Bean
public TemplateResolver templateResolver() {
ServletContextTemplateResolver resolver = new ServletContextTemplateResolver();
resolver.setPrefix("/WEB-INF/templates/");
resolver.setSuffix(".html");
resolver.setTemplateMode("HTML5");
resolver.setCharacterEncoding("UTF-8");
return resolver;
}
2. ModelAndView
ModelAndView对象包含了模型数据和视图信息,通过返回ModelAndView,可以在一个对象中同时指定模型数据和要渲染的视图。
@RequestMapping("/products")
public ModelAndView listProducts() {
ModelAndView mav = new ModelAndView("products/list");
mav.addObject("products", productService.getAllProducts());
return mav;
}
在上述示例中,listProducts方法创建了一个ModelAndView对象,指定了视图名称为products/list,并添加了一个名为products的模型数据,该数据包含了所有产品的列表。SpringMVC 会根据视图名称解析视图资源,并将模型数据传递给视图进行渲染。
使用 ModelAndView 的优势
灵活性高:可以在一个对象中同时处理模型数据和视图逻辑。
方便传递复杂数据结构:适合传递多个模型数据或复杂的对象。
3. JSON 响应
在现代 Web 应用中,JSON 是一种常用的数据交换格式。
SpringMVC 提供了对 JSON 响应的支持,通过使用@RestController注解或@ResponseBody注解,可以将方法的返回值直接转换为 JSON 格式并返回给客户端。
@RestController
@RequestMapping("/api/products")
public class ProductRestController {
@GetMapping
public List<Product> getAllProducts() {
return productService.getAllProducts();
}
}
在上述示例中,ProductRestController类使用了@RestController注解,该注解等价于@Controller和@ResponseBody的组合。因此,getAllProducts方法的返回值会被自动转换为 JSON 格式并返回给客户端。
JSON 序列化与反序列化
SpringMVC 使用 Jackson 库来进行 JSON 的序列化和反序列化。Jackson 会自动将 对象转换为 JSON 字符串,并在需要时将 JSON 字符串转换回 对象。
配置 JSON 序列化选项
可以通过配置 Jackson 的ObjectMapper来定制 JSON 的序列化行为,例如:
@Configuration
public class WebConfig implements WebMvcConfigurer {
@Override
public void configureMessageConverters(List<HttpMessageConverter<?>> converters) {
MappingJackson2HttpMessageConverter converter = new MappingJackson2HttpMessageConverter();
ObjectMapper mapper = new ObjectMapper();
// 配置序列化选项
mapper.setSerializationInclusion(JsonInclude.Include.NON_NULL); // 不序列化null值
mapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false); // 忽略未知属性
converter.setObjectMapper(mapper);
converters.add(converter);
}
}
4. 重定向
重定向是指服务器返回一个 HTTP 状态码 302(或其他重定向状态码),并在响应头中指定一个新的 URL,客户端会根据这个新的 URL 再次发送请求。
@RequestMapping("/save")
public String saveProduct(@ModelAttribute Product product) {
productService.save(product);
return "redirect:/products";
}
在上述示例中,saveProduct方法在保存产品后,返回一个redirect:/products的字符串,SpringMVC 会将其解析为重定向指令,客户端会被重定向到/products路径。
重定向的作用
防止表单重复提交:用户刷新页面时不会再次提交表单。
引导用户到新的页面:例如注册成功后重定向到登录页面。
重定向传递参数
可以使用RedirectAttributes来在重定向过程中传递参数:
@RequestMapping("/save")
public String saveProduct(@ModelAttribute Product product, RedirectAttributes attrs) {
productService.save(product);
attrs.addFlashAttribute("message", "产品保存成功");
return "redirect:/products";
}
在上述示例中,addFlashAttribute方法添加了一个临时属性message,这个属性只会在重定向后的下一次请求中有效。
总结
SpringMVC 提供了多种响应类型,包括视图名称、ModelAndView、JSON 响应和重定向。开发者可以根据具体需求选择合适的响应方式,以实现灵活、高效的 Web 应用开发。
视图名称:适用于传统的页面渲染场景,通过视图解析器将逻辑视图名映射到实际视图资源。
ModelAndView:提供了更灵活的方式来处理模型数据和视图逻辑,适合传递多个模型数据或复杂对象。
JSON 响应:用于返回 JSON 格式的数据,适用于前后端分离的架构,通过 Jackson 库进行 JSON 的序列化和反序列化。
重定向:用于引导用户到新的页面,防止表单重复提交,可通过RedirectAttributes传递临时参数。
SpringMVC-12-REST 风格简介
REST (Representational State Transfer) 是一种软件架构风格,它使用 HTTP 协议的标准方法 (GET、POST、PUT、DELETE) 来操作资源。
REST 的主要特点:
资源导向:每个 URL 代表一种资源
使用标准 HTTP 方法:GET (获取)、POST (创建)、PUT (更新)、DELETE (删除)
无状态:每个请求都是独立的,不依赖于之前的请求
统一接口:所有资源都通过统一的接口进行操作
SpringMVC-13-RESTful 入门案例
下面是一个简单的 RESTful API 示例:
@RestController
@RequestMapping("/api/products")
public class ProductRestController {
@Autowired
private ProductService productService;
// 获取所有产品
@GetMapping
public List<Product> getAll() {
return productService.getAll();
}
// 获取单个产品
@GetMapping("/{id}")
public Product getById(@PathVariable Long id) {
return productService.getById(id);
}
// 创建产品
@PostMapping
public Product create(@RequestBody Product product) {
return productService.save(product);
}
// 更新产品
@PutMapping("/{id}")
public Product update(@PathVariable Long id, @RequestBody Product product) {
return productService.update(id, product);
}
// 删除产品
@DeleteMapping("/{id}")
public void delete(@PathVariable Long id) {
productService.delete(id);
}
}
SpringMVC-14-RESTful 快速开发
可以使用 Spring Data JPA 和 Spring HATEOAS 来加速 RESTful API 的开发。
添加依赖:
xml
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jpa</artifactId></dependency><dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-hateoas</artifactId>
</dependency>
创建实体和 Repository:
@Entity
public class Product {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String name;
private double price;
// getters and setters....
}
public interface ProductRepository extends JpaRepository<Product, Long> {}
创建资源控制器:
@RestController
@RequestMapping("/api/products")
public class ProductController {
private final ProductRepository repository;
private final EntityLinks entityLinks;
@Autowired
public ProductController(ProductRepository repository, EntityLinks entityLinks) {
this.repository = repository;
this.entityLinks = entityLinks;
}
@GetMapping
public Resources<Resource<Product>> getAll() {
List<Resource<Product>> products = repository.findAll().stream()
.map(product -> new Resource<>(product,
linkTo(methodOn(ProductController.class).getOne(product.getId())).withSelfRel(),
linkTo(methodOn(ProductController.class).getAll()).withRel("products")))
.collect(Collectors.toList());
return new Resources<>(products,
linkTo(methodOn(ProductController.class).getAll()).withSelfRel());
}
@GetMapping("/{id}")
public Resource<Product> getOne(@PathVariable Long id) {
Product product = repository.findById(id)
.orElseThrow(() -> new ResourceNotFoundException("Product not found with id: " + id));
return new Resource<>(product,
linkTo(methodOn(ProductController.class).getOne(id)).withSelfRel(),
linkTo(methodOn(ProductController.class).getAll()).withRel("products"));
}
}
SpringMVC-15 - 案例:基于 RESTful 页面数据交互 (后台接口开发)
假设我们要开发一个简单的产品管理系统,下面是后台接口的实现:
@RestController
@RequestMapping("/api/products")
public class ProductApiController {
@Autowired
private ProductService productService;
@GetMapping
public Page<ProductDTO> listProducts(
@RequestParam(name = "page", defaultValue = "0") int page,
@RequestParam(name = "size", defaultValue = "10") int size) {
return productService.getProducts(page, size);
}
@PostMapping
public ResponseEntity<ProductDTO> createProduct
(@RequestBody ProductDTO productDTO) {
ProductDTO createdProduct = productService.createProduct(productDTO);
URI location = ServletUriComponentsBuilder
.fromCurrentRequest()
.path("/{id}")
.buildAndExpand(createdProduct.getId())
.toUri();
return ResponseEntity.created(location).body(createdProduct);
}
@GetMapping("/{id}")
public ResponseEntity<ProductDTO> getProduct(@PathVariable Long id) {
ProductDTO productDTO = productService.getProduct(id);
return ResponseEntity.ok(productDTO);
}
@PutMapping("/{id}")
public ResponseEntity<ProductDTO> updateProduct(@PathVariable Long id, @RequestBody ProductDTO productDTO) {
ProductDTO updatedProduct = productService.updateProduct(id, productDTO);
return ResponseEntity.ok(updatedProduct);
}
@DeleteMapping("/{id}")
public ResponseEntity<?> deleteProduct(@PathVariable Long id) {
productService.deleteProduct(id);
return ResponseEntity.noContent().build();
}
}
SpringMVC-16 - 案例:基于 RESTful 页面数据交互 (页面访问处理)
下面是一个使用 Thymeleaf 模板引擎的前端页面示例:
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head>
<title>Product Management</title>
<script src="https://cdn.tailwindcss.com">
</script>
<link href="https://cdn.jsdelivr.net/npm/font-awesome@4.7.0/css/font-awesome.min.css" rel="stylesheet">
</head>
<body class="bg-gray-100">
<div class="container mx-auto px-4 py-8">
<h1 class="text-3xl font-bold mb-6">Product Management</h1>
<div class="mb-6">
<button id="addProductBtn" class="bg-blue-500 hover:bg-blue-700 text-white font-bold py-2 px-4 rounded">
<i class="fa fa-plus"></i> Add Product
</button>
</div>
<div class="bg-white rounded-lg shadow-md p-6 mb-6">
<table class="min-w-full">
<thead>
<tr class="bg-gray-200 text-gray-600 uppercase text-sm leading-normal">
<th class="py-3 px-6 text-left">ID</th>
<th class="py-3 px-6 text-left">Name</th>
<th class="py-3 px-6 text-left">Price</th>
<th class="py-3 px-6 text-left">Actions</th>
</tr>
</thead>
<tbody id="productsTableBody" class="text-gray-600 text-sm font-light">
<!-- Products will be loaded here via Script -->
</tbody>
</table>
</div>
<!-- Pagination -->
<div id="pagination" class="flex items-center justify-between bg-white rounded-lg shadow-md p-6">
<!-- Pagination controls will be loaded here via Script -->
</div>
<!-- Add/Edit Modal -->
<div id="productModal" class="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50 hidden">
<div class="bg-white rounded-lg shadow-xl w-full max-w-md p-6">
<h2 id="modalTitle" class="text-2xl font-bold mb-4">Add Product</h2>
<form id="productForm">
<input type="hidden" id="productId">
<div class="mb-4">
<label for="productName" class="block text-gray-700 font-bold mb-2">Name:</label>
<input type="text" id="productName" class="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent" required>
</div>
<div class="mb-4">
<label for="productPrice" class="block text-gray-700 font-bold mb-2">Price:</label>
<input type="number" id="productPrice" class="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent" step="0.01" required>
</div>
<div class="flex justify-end">
<button type="button" id="cancelBtn" class="mr-2 px-4 py-2 bg-gray-200 hover:bg-gray-300 rounded">Cancel</button>
<button type="submit" class="px-4 py-2 bg-blue-500 hover:bg-blue-700 text-white rounded">Save</button>
</div>
</form>
</div>
</div>
</div>
<script>
// Script code for handling API calls and UI interactions
document.addEventListener('DOMContentLoaded', function() {
// Load products on page load
loadProducts(0);
// Add product button click
document.getElementById('addProductBtn').addEventListener('click', function() {
document.getElementById('modalTitle').textContent = 'Add Product';
document.getElementById('productId').value = '';
document.getElementById('productName').value = '';
document.getElementById('productPrice').value = '';
document.getElementById('productModal').classList.remove('hidden');
});
// Cancel button click
document.getElementById('cancelBtn').addEventListener('click', function() {
document.getElementById('productModal').classList.add('hidden');
});
// Product form submission
document.getElementById('productForm').addEventListener('submit', function(e) {
e.preventDefault();
const productId = document.getElementById('productId').value;
const productName = document.getElementById('productName').value;
const productPrice = document.getElementById('productPrice').value;
const productData = {
name: productName,
price: parseFloat(productPrice)
};
if (productId) {
// Update existing product
fetch(`/api/products/${productId}`, {
method: 'PUT',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify(productData)
})
.then(response => response.json())
.then(() => {
document.getElementById('productModal').classList.add('hidden');
loadProducts(currentPage);
})
.catch(error => console.error('Error:', error));
} else {
// Create new product
fetch('/api/products', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify(productData)
})
.then(response => response.json())
.then(() => {
document.getElementById('productModal').classList.add('hidden');
loadProducts(0);
})
.catch(error => console.error('Error:', error));
}
});
});
let currentPage = 0;
function loadProducts(page) {
currentPage = page;
fetch(`/api/products?page=${page}`)
.then(response => response.json())
.then(data => {
const productsTableBody = document.getElementById('productsTableBody');
productsTableBody.innerHTML = '';
data.content.forEach(product => {
const row = document.createElement('tr');
row.className = 'border-b border-gray-200 hover:bg-gray-100';
row.innerHTML = `
<td class="py-3 px-6">${product.id}</td>
<td class="py-3 px-6">${product.name}</td>
<td class="py-3 px-6">$${product.price.toFixed(2)}</td>
<td class="py-3 px-6">
<button οnclick="editProduct(${product.id})" class="text-blue-500 hover:text-blue-700 mr-2">
<i class="fa fa-pencil"></i>
</button>
<button οnclick="deleteProduct(${product.id})" class="text-red-500 hover:text-red-700">
<i class="fa fa-trash"></i>
</button>
</td>
`;
productsTableBody.appendChild(row);
});
// Update pagination
updatePagination(data);
})
.catch(error => console.error('Error:', error));
}
function editProduct(id) {
fetch(`/api/products/${id}`)
.then(response => response.json())
.then(product => {
document.getElementById('modalTitle').textContent = 'Edit Product';
document.getElementById('productId').value = product.id;
document.getElementById('productName').value = product.name;
document.getElementById('productPrice').value = product.price;
document.getElementById('productModal').classList.remove('hidden');
})
.catch(error => console.error('Error:', error));
}
function deleteProduct(id) {
if (confirm('Are you sure you want to delete this product?')) {
fetch(`/api/products/${id}`, {
method: 'DELETE'
})
.then(() => {
loadProducts(currentPage);
})
.catch(error => console.error('Error:', error));
}
}
function updatePagination(data) {
const pagination = document.getElementById('pagination');
pagination.innerHTML = '';
const prevButton = document.createElement('button');
prevButton.className = 'px-4 py-2 bg-gray-200 hover:bg-gray-300 rounded';
prevButton.disabled = !data.first;
prevButton.innerHTML = '<i class="fa fa-chevron-left"></i> Previous';
prevButton.onclick = () => loadProducts(currentPage - 1);
pagination.appendChild(prevButton);
const pageInfo = document.createElement('span');
pageInfo.className = 'px-4';
pageInfo.textContent = `Page ${data.number + 1} of ${data.totalPages}`;
pagination.appendChild(pageInfo);
const nextButton = document.createElement('button');
nextButton.className = 'px-4 py-2 bg-gray-200 hover:bg-gray-300 rounded';
nextButton.disabled = !data.last;
nextButton.innerHTML = 'Next <i class="fa fa-chevron-right"></i>';
nextButton.onclick = () => loadProducts(currentPage + 1);
pagination.appendChild(nextButton);
}
</script>
</body>
</html>