Spring Boot RESTful API 开发与测试

Spring Boot RESTful API 开发与测试

Spring Boot RESTful API 开发与测试

在这里插入图片描述
20.1 学习目标与重点提示

学习目标:掌握Spring Boot RESTful API开发与测试的核心概念与使用方法,包括RESTful API的定义与特点、Spring Boot RESTful API的开发、Spring Boot RESTful API的测试、Spring Boot RESTful API的认证与授权、Spring Boot RESTful API的实际应用场景,学会在实际开发中处理RESTful API问题。
重点:RESTful API的定义与特点(资源、表现层、状态转移)Spring Boot RESTful API的开发(@RestController、@RequestMapping、@GetMapping、@PostMapping、@PutMapping、@DeleteMapping)Spring Boot RESTful API的测试(单元测试、集成测试、Mock测试)Spring Boot RESTful API的认证与授权(Spring Security、JWT)Spring Boot RESTful API的实际应用场景

20.2 RESTful API概述

RESTful API是Java开发中的主流API设计风格。

20.2.1 RESTful API的定义

定义:RESTful API是一种基于REST架构风格的API设计。
作用

  • 实现Web应用的API设计。
  • 提高开发效率。
  • 提供统一的编程模型。

REST架构风格的特点

  • 资源(Resource):使用URI表示资源。
  • 表现层(Representation):使用HTTP请求方法(GET、POST、PUT、DELETE)表示操作。
  • 状态转移(State Transfer):使用HTTP响应状态码表示操作结果。

✅ 结论:RESTful API是一种基于REST架构风格的API设计,作用是实现Web应用的API设计、提高开发效率、提供统一的编程模型。

20.2.2 RESTful API的常用HTTP方法

定义:RESTful API的常用HTTP方法是指RESTful API使用的HTTP请求方法。
方法

  • GET:获取资源。
  • POST:创建资源。
  • PUT:更新资源。
  • DELETE:删除资源。
  • PATCH:更新部分资源。

常用HTTP响应状态码

  • 200:成功。
  • 201:资源创建成功。
  • 400:请求参数错误。
  • 401:未授权。
  • 403:禁止访问。
  • 404:资源不存在。
  • 500:服务器内部错误。

✅ 结论:RESTful API的常用HTTP方法包括GET、POST、PUT、DELETE、PATCH,常用HTTP响应状态码包括200、201、400、401、403、404、500。

20.3 Spring Boot RESTful API的开发

Spring Boot RESTful API的开发是Java开发中的重要内容。

20.3.1 开发RESTful API的步骤

定义:开发RESTful API的步骤是指使用Spring Boot开发RESTful API的方法。
步骤

  1. 创建Spring Boot项目。
  2. 添加所需的依赖。
  3. 创建实体类。
  4. 创建Repository接口。
  5. 创建Service类。
  6. 创建Controller类。
  7. 测试应用。

示例
pom.xml文件中的依赖:

<dependencies><!-- Web依赖 --><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-web</artifactId></dependency><!-- Data JPA依赖 --><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-data-jpa</artifactId></dependency><!-- H2数据库依赖 --><dependency><groupId>com.h2database</groupId><artifactId>h2</artifactId><scope>runtime</scope></dependency><!-- 测试依赖 --><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-test</artifactId><scope>test</scope></dependency></dependencies>

实体类:

importjavax.persistence.*;@Entity@Table(name ="product")publicclassProduct{@Id@GeneratedValue(strategy =GenerationType.IDENTITY)privateLong id;privateString productId;privateString productName;privatedouble price;privateint sales;publicProduct(){}publicProduct(String productId,String productName,double price,int sales){this.productId = productId;this.productName = productName;this.price = price;this.sales = sales;}// Getter和Setter方法publicLonggetId(){return id;}publicvoidsetId(Long id){this.id = id;}publicStringgetProductId(){return productId;}publicvoidsetProductId(String productId){this.productId = productId;}publicStringgetProductName(){return productName;}publicvoidsetProductName(String productName){this.productName = productName;}publicdoublegetPrice(){return price;}publicvoidsetPrice(double price){this.price = price;}publicintgetSales(){return sales;}publicvoidsetSales(int sales){this.sales = sales;}@OverridepublicStringtoString(){return"Product{"+"id="+ id +",+ productId +'\''+",+ productName +'\''+", price="+ price +", sales="+ sales +'}';}}

Repository接口:

importorg.springframework.data.jpa.repository.JpaRepository;importorg.springframework.stereotype.Repository;importjava.util.List;@RepositorypublicinterfaceProductRepositoryextendsJpaRepository<Product,Long>{List<Product>findBySalesGreaterThan(int sales);}

Service类:

importorg.springframework.beans.factory.annotation.Autowired;importorg.springframework.stereotype.Service;importorg.springframework.transaction.annotation.Transactional;importjava.util.List;@ServicepublicclassProductService{@AutowiredprivateProductRepository productRepository;@TransactionalpublicvoidaddProduct(Product product){ productRepository.save(product);}@TransactionalpublicvoidupdateProduct(Product product){ productRepository.save(product);}@TransactionalpublicvoiddeleteProduct(Long id){ productRepository.deleteById(id);}@Transactional(readOnly =true)publicList<Product>getAllProducts(){return productRepository.findAll();}@Transactional(readOnly =true)publicList<Product>getTopSellingProducts(int topN){List<Product> products = productRepository.findBySalesGreaterThan(0); products.sort((p1, p2)-> p2.getSales()- p1.getSales());if(products.size()> topN){return products.subList(0, topN);}return products;}}

Controller类:

importorg.springframework.beans.factory.annotation.Autowired;importorg.springframework.http.HttpStatus;importorg.springframework.http.ResponseEntity;importorg.springframework.web.bind.annotation.*;importjava.util.List;@RestController@RequestMapping("/api/products")publicclassProductController{@AutowiredprivateProductService productService;@GetMapping("/")publicResponseEntity<List<Product>>getAllProducts(){List<Product> products = productService.getAllProducts();returnnewResponseEntity<>(products,HttpStatus.OK);}@PostMapping("/")publicResponseEntity<Void>addProduct(@RequestBodyProduct product){ productService.addProduct(product);returnnewResponseEntity<>(HttpStatus.CREATED);}@PutMapping("/{id}")publicResponseEntity<Void>updateProduct(@PathVariableLong id,@RequestBodyProduct product){ product.setId(id); productService.updateProduct(product);returnnewResponseEntity<>(HttpStatus.OK);}@DeleteMapping("/{id}")publicResponseEntity<Void>deleteProduct(@PathVariableLong id){ productService.deleteProduct(id);returnnewResponseEntity<>(HttpStatus.NO_CONTENT);}@GetMapping("/top-selling")publicResponseEntity<List<Product>>getTopSellingProducts(@RequestParamint topN){List<Product> products = productService.getTopSellingProducts(topN);returnnewResponseEntity<>(products,HttpStatus.OK);}}

测试类:

importorg.junit.jupiter.api.Test;importorg.springframework.beans.factory.annotation.Autowired;importorg.springframework.boot.test.context.SpringBootTest;importorg.springframework.boot.test.web.client.TestRestTemplate;importorg.springframework.boot.web.server.LocalServerPort;importjava.util.List;importstaticorg.assertj.core.api.Assertions.assertThat;@SpringBootTest(webEnvironment =SpringBootTest.WebEnvironment.RANDOM_PORT)classProductApplicationTests{@LocalServerPortprivateint port;@AutowiredprivateTestRestTemplate restTemplate;@TestvoidcontextLoads(){}@TestvoidtestGetAllProducts(){List<Product> products = restTemplate.getForObject("http://localhost:"+ port +"/api/products/",List.class);assertThat(products).hasSize(5);}@TestvoidtestAddProduct(){Product product =newProduct("P006","平板",2000.0,70); restTemplate.postForEntity("http://localhost:"+ port +"/api/products/", product,Void.class);List<Product> products = restTemplate.getForObject("http://localhost:"+ port +"/api/products/",List.class);assertThat(products).hasSize(6);}@TestvoidtestUpdateProduct(){Product product =newProduct("P001","手机",1500.0,120); restTemplate.put("http://localhost:"+ port +"/api/products/1", product);List<Product> products = restTemplate.getForObject("http://localhost:"+ port +"/api/products/",List.class);assertThat(products.get(0).getPrice()).isEqualTo(1500.0);}@TestvoidtestDeleteProduct(){ restTemplate.delete("http://localhost:"+ port +"/api/products/2");List<Product> products = restTemplate.getForObject("http://localhost:"+ port +"/api/products/",List.class);assertThat(products).hasSize(4);}@TestvoidtestGetTopSellingProducts(){List<Product> topSellingProducts = restTemplate.getForObject("http://localhost:"+ port +"/api/products/top-selling?topN=3",List.class);assertThat(topSellingProducts).hasSize(3);assertThat(topSellingProducts.get(0).getProductId()).isEqualTo("P004");assertThat(topSellingProducts.get(1).getProductId()).isEqualTo("P005");assertThat(topSellingProducts.get(2).getProductId()).isEqualTo("P001");}}

✅ 结论:开发RESTful API的步骤包括创建Spring Boot项目、添加所需的依赖、创建实体类、创建Repository接口、创建Service类、创建Controller类、测试应用。

20.4 Spring Boot RESTful API的测试

Spring Boot RESTful API的测试是Java开发中的重要内容。

20.4.1 单元测试

定义:单元测试是指测试单个方法或类的功能。
常用注解

  • @SpringBootTest:标记测试类为Spring Boot测试。
  • @Test:标记方法为测试方法。
  • @Autowired:注入依赖。

示例

importorg.junit.jupiter.api.Test;importorg.springframework.beans.factory.annotation.Autowired;importorg.springframework.boot.test.context.SpringBootTest;importjava.util.List;importstaticorg.assertj.core.api.Assertions.assertThat;@SpringBootTestclassProductServiceTests{@AutowiredprivateProductService productService;@TestvoidtestAddProduct(){Product product =newProduct("P006","平板",2000.0,70); productService.addProduct(product);List<Product> products = productService.getAllProducts();assertThat(products).hasSize(6);}@TestvoidtestUpdateProduct(){Product product =newProduct("P001","手机",1500.0,120); product.setId(1L); productService.updateProduct(product);List<Product> products = productService.getAllProducts();assertThat(products.get(0).getPrice()).isEqualTo(1500.0);}@TestvoidtestDeleteProduct(){ productService.deleteProduct(2L);List<Product> products = productService.getAllProducts();assertThat(products).hasSize(4);}@TestvoidtestGetTopSellingProducts(){List<Product> topSellingProducts = productService.getTopSellingProducts(3);assertThat(topSellingProducts).hasSize(3);assertThat(topSellingProducts.get(0).getProductId()).isEqualTo("P004");assertThat(topSellingProducts.get(1).getProductId()).isEqualTo("P005");assertThat(topSellingProducts.get(2).getProductId()).isEqualTo("P001");}}

✅ 结论:单元测试是指测试单个方法或类的功能,常用注解包括@SpringBootTest、@Test、@Autowired。

20.4.2 集成测试

定义:集成测试是指测试多个组件之间的交互。
常用注解

  • @SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT):标记测试类为Spring Boot集成测试。
  • @LocalServerPort:注入服务器端口。
  • @Autowired:注入依赖。

示例

importorg.junit.jupiter.api.Test;importorg.springframework.beans.factory.annotation.Autowired;importorg.springframework.boot.test.context.SpringBootTest;importorg.springframework.boot.test.web.client.TestRestTemplate;importorg.springframework.boot.web.server.LocalServerPort;importjava.util.List;importstaticorg.assertj.core.api.Assertions.assertThat;@SpringBootTest(webEnvironment =SpringBootTest.WebEnvironment.RANDOM_PORT)classProductControllerTests{@LocalServerPortprivateint port;@AutowiredprivateTestRestTemplate restTemplate;@TestvoidtestGetAllProducts(){List<Product> products = restTemplate.getForObject("http://localhost:"+ port +"/api/products/",List.class);assertThat(products).hasSize(5);}@TestvoidtestAddProduct(){Product product =newProduct("P006","平板",2000.0,70); restTemplate.postForEntity("http://localhost:"+ port +"/api/products/", product,Void.class);List<Product> products = restTemplate.getForObject("http://localhost:"+ port +"/api/products/",List.class);assertThat(products).hasSize(6);}@TestvoidtestUpdateProduct(){Product product =newProduct("P001","手机",1500.0,120); restTemplate.put("http://localhost:"+ port +"/api/products/1", product);List<Product> products = restTemplate.getForObject("http://localhost:"+ port +"/api/products/",List.class);assertThat(products.get(0).getPrice()).isEqualTo(1500.0);}@TestvoidtestDeleteProduct(){ restTemplate.delete("http://localhost:"+ port +"/api/products/2");List<Product> products = restTemplate.getForObject("http://localhost:"+ port +"/api/products/",List.class);assertThat(products).hasSize(4);}@TestvoidtestGetTopSellingProducts(){List<Product> topSellingProducts = restTemplate.getForObject("http://localhost:"+ port +"/api/products/top-selling?topN=3",List.class);assertThat(topSellingProducts).hasSize(3);assertThat(topSellingProducts.get(0).getProductId()).isEqualTo("P004");assertThat(topSellingProducts.get(1).getProductId()).isEqualTo("P005");assertThat(topSellingProducts.get(2).getProductId()).isEqualTo("P001");}}

✅ 结论:集成测试是指测试多个组件之间的交互,常用注解包括@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)、@LocalServerPort、@Autowired。

20.4.3 Mock测试

定义:Mock测试是指模拟对象的行为。
常用注解

  • @WebMvcTest:标记测试类为Spring MVC测试。
  • @MockBean:注入Mock对象。
  • @Autowired:注入依赖。

示例

importorg.junit.jupiter.api.Test;importorg.springframework.beans.factory.annotation.Autowired;importorg.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest;importorg.springframework.boot.test.mock.mockito.MockBean;importorg.springframework.http.MediaType;importorg.springframework.test.web.servlet.MockMvc;importjava.util.Arrays;importjava.util.List;importstaticorg.mockito.ArgumentMatchers.any;importstaticorg.mockito.Mockito.*;importstaticorg.springframework.test.web.servlet.request.MockMvcRequestBuilders.*;importstaticorg.springframework.test.web.servlet.result.MockMvcResultMatchers.*;@WebMvcTest(ProductController.class)classProductControllerMockTests{@AutowiredprivateMockMvc mockMvc;@MockBeanprivateProductService productService;@TestvoidtestGetAllProducts()throwsException{List<Product> products =Arrays.asList(newProduct("P001","手机",1000.0,100),newProduct("P002","电脑",5000.0,50),newProduct("P003","电视",3000.0,80),newProduct("P004","手表",500.0,200),newProduct("P005","耳机",300.0,150));when(productService.getAllProducts()).thenReturn(products); mockMvc.perform(get("/api/products/")).andExpect(status().isOk()).andExpect(content().contentType(MediaType.APPLICATION_JSON)).andExpect(jsonPath("$[0].productId").value("P001")).andExpect(jsonPath("$[1].productId").value("P002")).andExpect(jsonPath("$[2].productId").value("P003")).andExpect(jsonPath("$[3].productId").value("P004")).andExpect(jsonPath("$[4].productId").value("P005"));verify(productService,times(1)).getAllProducts();}@TestvoidtestAddProduct()throwsException{Product product =newProduct("P006","平板",2000.0,70);doNothing().when(productService).addProduct(any(Product.class)); mockMvc.perform(post("/api/products/").contentType(MediaType.APPLICATION_JSON).content("{\"productId\":\"P006\",\"productName\":\"平板\",\"price\":2000.0,\"sales\":70}")).andExpect(status().isCreated());verify(productService,times(1)).addProduct(any(Product.class));}@TestvoidtestUpdateProduct()throwsException{Product product =newProduct("P001","手机",1500.0,120);doNothing().when(productService).updateProduct(any(Product.class)); mockMvc.perform(put("/api/products/1").contentType(MediaType.APPLICATION_JSON).content("{\"id\":1,\"productId\":\"P001\",\"productName\":\"手机\",\"price\":1500.0,\"sales\":120}")).andExpect(status().isOk());verify(productService,times(1)).updateProduct(any(Product.class));}@TestvoidtestDeleteProduct()throwsException{doNothing().when(productService).deleteProduct(anyLong()); mockMvc.perform(delete("/api/products/2")).andExpect(status().isNoContent());verify(productService,times(1)).deleteProduct(anyLong());}@TestvoidtestGetTopSellingProducts()throwsException{List<Product> topSellingProducts =Arrays.asList(newProduct("P004","手表",500.0,200),newProduct("P005","耳机",300.0,150),newProduct("P001","手机",1000.0,100));when(productService.getTopSellingProducts(3)).thenReturn(topSellingProducts); mockMvc.perform(get("/api/products/top-selling?topN=3")).andExpect(status().isOk()).andExpect(content().contentType(MediaType.APPLICATION_JSON)).andExpect(jsonPath("$[0].productId").value("P004")).andExpect(jsonPath("$[1].productId").value("P005")).andExpect(jsonPath("$[2].productId").value("P001"));verify(productService,times(1)).getTopSellingProducts(3);}}

✅ 结论:Mock测试是指模拟对象的行为,常用注解包括@WebMvcTest、@MockBean、@Autowired。

20.5 Spring Boot RESTful API的认证与授权

Spring Boot RESTful API的认证与授权是Java开发中的重要内容。

20.5.1 Spring Security

定义:Spring Security是Spring Boot提供的安全框架。
作用

  • 实现用户认证。
  • 实现用户授权。
  • 提供安全的编程模型。

示例
pom.xml文件中的Spring Security依赖:

<dependencies><!-- Web依赖 --><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-web</artifactId></dependency><!-- Data JPA依赖 --><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-data-jpa</artifactId></dependency><!-- H2数据库依赖 --><dependency><groupId>com.h2database</groupId><artifactId>h2</artifactId><scope>runtime</scope></dependency><!-- Spring Security依赖 --><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-security</artifactId></dependency><!-- 测试依赖 --><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-test</artifactId><scope>test</scope></dependency></dependencies>

Spring Security配置类:

importorg.springframework.context.annotation.Configuration;importorg.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;importorg.springframework.security.config.annotation.web.builders.HttpSecurity;importorg.springframework.security.config.annotation.web.configuration.EnableWebSecurity;importorg.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;importorg.springframework.security.crypto.password.NoOpPasswordEncoder;@Configuration@EnableWebSecuritypublicclassSecurityConfigextendsWebSecurityConfigurerAdapter{@Overrideprotectedvoidconfigure(AuthenticationManagerBuilder auth)throwsException{ auth.inMemoryAuthentication().passwordEncoder(NoOpPasswordEncoder.getInstance()).withUser("admin").password("admin123").roles("ADMIN").and().withUser("user").password("user123").roles("USER");}@Overrideprotectedvoidconfigure(HttpSecurity http)throwsException{ http.authorizeRequests().antMatchers("/api/products/top-selling").hasRole("ADMIN").antMatchers("/api/products/**").hasRole("USER").and().httpBasic();}}

测试类:

importorg.junit.jupiter.api.Test;importorg.springframework.beans.factory.annotation.Autowired;importorg.springframework.boot.test.context.SpringBootTest;importorg.springframework.boot.test.web.client.TestRestTemplate;importorg.springframework.boot.web.server.LocalServerPort;importorg.springframework.http.HttpEntity;importorg.springframework.http.HttpHeaders;importorg.springframework.http.HttpMethod;importorg.springframework.http.ResponseEntity;importjava.util.Base64;importjava.util.List;importstaticorg.assertj.core.api.Assertions.assertThat;@SpringBootTest(webEnvironment =SpringBootTest.WebEnvironment.RANDOM_PORT)classProductControllerSecurityTests{@LocalServerPortprivateint port;@AutowiredprivateTestRestTemplate restTemplate;@TestvoidtestGetAllProductsWithoutAuthentication(){ResponseEntity<List> response = restTemplate.getForEntity("http://localhost:"+ port +"/api/products/",List.class);assertThat(response.getStatusCodeValue()).isEqualTo(401);}@TestvoidtestGetAllProductsWithUserAuthentication(){String credentials ="user:user123";String base64Credentials =Base64.getEncoder().encodeToString(credentials.getBytes());HttpHeaders headers =newHttpHeaders(); headers.add("Authorization","Basic "+ base64Credentials);HttpEntity<String> entity =newHttpEntity<>(headers);ResponseEntity<List> response = restTemplate.exchange("http://localhost:"+ port +"/api/products/",HttpMethod.GET, entity,List.class);assertThat(response.getStatusCodeValue()).isEqualTo(200);assertThat(response.getBody()).hasSize(5);}@TestvoidtestGetTopSellingProductsWithUserAuthentication(){String credentials ="user:user123";String base64Credentials =Base64.getEncoder().encodeToString(credentials.getBytes());HttpHeaders headers =newHttpHeaders(); headers.add("Authorization","Basic "+ base64Credentials);HttpEntity<String> entity =newHttpEntity<>(headers);ResponseEntity<List> response = restTemplate.exchange("http://localhost:"+ port +"/api/products/top-selling?topN=3",HttpMethod.GET, entity,List.class);assertThat(response.getStatusCodeValue()).isEqualTo(403);}@TestvoidtestGetTopSellingProductsWithAdminAuthentication(){String credentials ="admin:admin123";String base64Credentials =Base64.getEncoder().encodeToString(credentials.getBytes());HttpHeaders headers =newHttpHeaders(); headers.add("Authorization","Basic "+ base64Credentials);HttpEntity<String> entity =newHttpEntity<>(headers);ResponseEntity<List> response = restTemplate.exchange("http://localhost:"+ port +"/api/products/top-selling?topN=3",HttpMethod.GET, entity,List.class);assertThat(response.getStatusCodeValue()).isEqualTo(200);assertThat(response.getBody()).hasSize(3);}}

✅ 结论:Spring Security是Spring Boot提供的安全框架,作用是实现用户认证、用户授权、提供安全的编程模型。

20.5.2 JWT

定义:JWT是一种基于JSON的开放标准,用于在网络应用之间安全地传输信息。
作用

  • 实现用户认证。
  • 实现用户授权。
  • 提供安全的编程模型。

示例
pom.xml文件中的JWT依赖:

<dependencies><!-- Web依赖 --><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-web</artifactId></dependency><!-- Data JPA依赖 --><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-data-jpa</artifactId></dependency><!-- H2数据库依赖 --><dependency><groupId>com.h2database</groupId><artifactId>h2</artifactId><scope>runtime</scope></dependency><!-- Spring Security依赖 --><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-security</artifactId></dependency><!-- JWT依赖 --><dependency><groupId>io.jsonwebtoken</groupId><artifactId>jjwt</artifactId><version>0.9.1</version></dependency><!-- 测试依赖 --><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-test</artifactId><scope>test</scope></dependency></dependencies>

JWT工具类:

importio.jsonwebtoken.Claims;importio.jsonwebtoken.Jwts;importio.jsonwebtoken.SignatureAlgorithm;importorg.springframework.beans.factory.annotation.Value;importorg.springframework.stereotype.Component;importjava.util.Date;importjava.util.HashMap;importjava.util.Map;importjava.util.function.Function;@ComponentpublicclassJwtUtil{@Value("${jwt.secret}")privateString secret;@Value("${jwt.expiration}")privateLong expiration;publicStringextractUsername(String token){returnextractClaim(token,Claims::getSubject);}publicDateextractExpiration(String token){returnextractClaim(token,Claims::getExpiration);}public<T>TextractClaim(String token,Function<Claims,T> claimsResolver){finalClaims claims =extractAllClaims(token);return claimsResolver.apply(claims);}privateClaimsextractAllClaims(String token){returnJwts.parser().setSigningKey(secret).parseClaimsJws(token).getBody();}privateBooleanisTokenExpired(String token){returnextractExpiration(token).before(newDate());}publicStringgenerateToken(String username){Map<String,Object> claims =newHashMap<>();returncreateToken(claims, username);}privateStringcreateToken(Map<String,Object> claims,String subject){returnJwts.builder().setClaims(claims).setSubject(subject).setIssuedAt(newDate(System.currentTimeMillis())).setExpiration(newDate(System.currentTimeMillis()+ expiration *1000)).signWith(SignatureAlgorithm.HS256, secret).compact();}publicBooleanvalidateToken(String token,String username){finalString extractedUsername =extractUsername(token);return(extractedUsername.equals(username)&&!isTokenExpired(token));}}

JWT过滤器:

importorg.springframework.beans.factory.annotation.Autowired;importorg.springframework.security.authentication.UsernamePasswordAuthenticationToken;importorg.springframework.security.core.context.SecurityContextHolder;importorg.springframework.security.core.userdetails.UserDetails;importorg.springframework.security.core.userdetails.UserDetailsService;importorg.springframework.security.web.authentication.WebAuthenticationDetailsSource;importorg.springframework.stereotype.Component;importorg.springframework.web.filter.OncePerRequestFilter;importjavax.servlet.FilterChain;importjavax.servlet.ServletException;importjavax.servlet.http.HttpServletRequest;importjavax.servlet.http.HttpServletResponse;importjava.io.IOException;@ComponentpublicclassJwtRequestFilterextendsOncePerRequestFilter{@AutowiredprivateUserDetailsService userDetailsService;@AutowiredprivateJwtUtil jwtUtil;@OverrideprotectedvoiddoFilterInternal(HttpServletRequest request,HttpServletResponse response,FilterChain chain)throwsServletException,IOException{finalString authorizationHeader = request.getHeader("Authorization");String username =null;String jwt =null;if(authorizationHeader !=null&& authorizationHeader.startsWith("Bearer ")){ jwt = authorizationHeader.substring(7); username = jwtUtil.extractUsername(jwt);}if(username !=null&&SecurityContextHolder.getContext().getAuthentication()==null){UserDetails userDetails =this.userDetailsService.loadUserByUsername(username);if(jwtUtil.validateToken(jwt, userDetails.getUsername())){UsernamePasswordAuthenticationToken usernamePasswordAuthenticationToken =newUsernamePasswordAuthenticationToken( userDetails,null, userDetails.getAuthorities()); usernamePasswordAuthenticationToken.setDetails(newWebAuthenticationDetailsSource().buildDetails(request));SecurityContextHolder.getContext().setAuthentication(usernamePasswordAuthenticationToken);}} chain.doFilter(request, response);}}

Spring Security配置类:

importorg.springframework.beans.factory.annotation.Autowired;importorg.springframework.context.annotation.Bean;importorg.springframework.context.annotation.Configuration;importorg.springframework.security.authentication.AuthenticationManager;importorg.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;importorg.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity;importorg.springframework.security.config.annotation.web.builders.HttpSecurity;importorg.springframework.security.config.annotation.web.configuration.EnableWebSecurity;importorg.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;importorg.springframework.security.config.http.SessionCreationPolicy;importorg.springframework.security.core.userdetails.UserDetailsService;importorg.springframework.security.crypto.password.NoOpPasswordEncoder;importorg.springframework.security.crypto.password.PasswordEncoder;importorg.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;@Configuration@EnableWebSecurity@EnableGlobalMethodSecurity(prePostEnabled =true)publicclassSecurityConfigextendsWebSecurityConfigurerAdapter{@AutowiredprivateUserDetailsService userDetailsService;@AutowiredprivateJwtRequestFilter jwtRequestFilter;@Overrideprotectedvoidconfigure(AuthenticationManagerBuilder auth)throwsException{ auth.userDetailsService(userDetailsService);}@BeanpublicPasswordEncoderpasswordEncoder(){returnNoOpPasswordEncoder.getInstance();}@Bean@OverridepublicAuthenticationManagerauthenticationManagerBean()throwsException{returnsuper.authenticationManagerBean();}@Overrideprotectedvoidconfigure(HttpSecurity http)throwsException{ http.csrf().disable().authorizeRequests().antMatchers("/authenticate").permitAll().anyRequest().authenticated().and().sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS); http.addFilterBefore(jwtRequestFilter,UsernamePasswordAuthenticationFilter.class);}}

认证控制器:

importorg.springframework.beans.factory.annotation.Autowired;importorg.springframework.http.ResponseEntity;importorg.springframework.security.authentication.AuthenticationManager;importorg.springframework.security.authentication.BadCredentialsException;importorg.springframework.security.authentication.UsernamePasswordAuthenticationToken;importorg.springframework.security.core.userdetails.UserDetails;importorg.springframework.security.core.userdetails.UserDetailsService;importorg.springframework.web.bind.annotation.*;@RestControllerpublicclassJwtAuthenticationController{@AutowiredprivateAuthenticationManager authenticationManager;@AutowiredprivateJwtUtil jwtTokenUtil;@AutowiredprivateUserDetailsService userDetailsService;@PostMapping("/authenticate")publicResponseEntity<?>createAuthenticationToken(@RequestBodyJwtRequest authenticationRequest)throwsException{try{ authenticationManager.authenticate(newUsernamePasswordAuthenticationToken(authenticationRequest.getUsername(), authenticationRequest.getPassword()));}catch(BadCredentialsException e){thrownewException("Incorrect username or password", e);}finalUserDetails userDetails = userDetailsService.loadUserByUsername(authenticationRequest.getUsername());finalString jwt = jwtTokenUtil.generateToken(userDetails.getUsername());returnResponseEntity.ok(newJwtResponse(jwt));}}

JwtRequest类:

publicclassJwtRequest{privateString username;privateString password;publicJwtRequest(){}publicJwtRequest(String username,String password){this.username = username;this.password = password;}// Getter和Setter方法publicStringgetUsername(){return username;}publicvoidsetUsername(String username){this.username = username;}publicStringgetPassword(){return password;}publicvoidsetPassword(String password){this.password = password;}}

JwtResponse类:

publicclassJwtResponse{privatefinalString jwt;publicJwtResponse(String jwt){this.jwt = jwt;}publicStringgetJwt(){return jwt;}}

用户服务类:

importorg.springframework.security.core.GrantedAuthority;importorg.springframework.security.core.authority.SimpleGrantedAuthority;importorg.springframework.security.core.userdetails.User;importorg.springframework.security.core.userdetails.UserDetails;importorg.springframework.security.core.userdetails.UserDetailsService;importorg.springframework.security.core.userdetails.UsernameNotFoundException;importorg.springframework.stereotype.Service;importjava.util.ArrayList;importjava.util.List;@ServicepublicclassMyUserDetailsServiceimplementsUserDetailsService{@OverridepublicUserDetailsloadUserByUsername(String username)throwsUsernameNotFoundException{if("user".equals(username)){List<GrantedAuthority> authorities =newArrayList<>(); authorities.add(newSimpleGrantedAuthority("ROLE_USER"));returnnewUser("user","user123", authorities);}elseif("admin".equals(username)){List<GrantedAuthority> authorities =newArrayList<>(); authorities.add(newSimpleGrantedAuthority("ROLE_ADMIN"));returnnewUser("admin","admin123", authorities);}else{thrownewUsernameNotFoundException("User not found with username: "+ username);}}}

应用配置文件(application.properties):

# 服务器端口 server.port=8080 # 数据库连接信息 spring.datasource.url=jdbc:h2:mem:testdb spring.datasource.driver-class-name=org.h2.Driver spring.datasource.username=sa spring.datasource.password=password # JPA配置 spring.jpa.hibernate.ddl-auto=update spring.jpa.show-sql=true # H2数据库控制台 spring.h2.console.enabled=true spring.h2.console.path=/h2-console # JWT配置 jwt.secret=mysecret jwt.expiration=3600 

测试类:

importorg.junit.jupiter.api.Test;importorg.springframework.beans.factory.annotation.Autowired;importorg.springframework.boot.test.context.SpringBootTest;importorg.springframework.boot.test.web.client.TestRestTemplate;importorg.springframework.boot.web.server.LocalServerPort;importorg.springframework.http.HttpEntity;importorg.springframework.http.HttpHeaders;importorg.springframework.http.HttpMethod;importorg.springframework.http.ResponseEntity;importjava.util.Map;importstaticorg.assertj.core.api.Assertions.assertThat;@SpringBootTest(webEnvironment =SpringBootTest.WebEnvironment.RANDOM_PORT)classJwtAuthenticationControllerTests{@LocalServerPortprivateint port;@AutowiredprivateTestRestTemplate restTemplate;@TestvoidtestAuthenticateUser(){JwtRequest request =newJwtRequest("user","user123");ResponseEntity<JwtResponse> response = restTemplate.postForEntity("http://localhost:"+ port +"/authenticate", request,JwtResponse.class);assertThat(response.getStatusCodeValue()).isEqualTo(200);assertThat(response.getBody().getJwt()).isNotNull();}@TestvoidtestAuthenticateAdmin(){JwtRequest request =newJwtRequest("admin","admin123");ResponseEntity<JwtResponse> response = restTemplate.postForEntity("http://localhost:"+ port +"/authenticate", request,JwtResponse.class);assertThat(response.getStatusCodeValue()).isEqualTo(200);assertThat(response.getBody().getJwt()).isNotNull();}@TestvoidtestAuthenticateInvalidUser(){JwtRequest request =newJwtRequest("invalid","invalid123");ResponseEntity<JwtResponse> response = restTemplate.postForEntity("http://localhost:"+ port +"/authenticate", request,JwtResponse.class);assertThat(response.getStatusCodeValue()).isEqualTo(401);}@TestvoidtestGetAllProductsWithUserJwt(){JwtRequest request =newJwtRequest("user","user123");ResponseEntity<JwtResponse> authResponse = restTemplate.postForEntity("http://localhost:"+ port +"/authenticate", request,JwtResponse.class);String token = authResponse.getBody().getJwt();HttpHeaders headers =newHttpHeaders(); headers.add("Authorization","Bearer "+ token);HttpEntity<String> entity =newHttpEntity<>(headers);ResponseEntity<Map> response = restTemplate.exchange("http://localhost:"+ port +"/api/products/",HttpMethod.GET, entity,Map.class);assertThat(response.getStatusCodeValue()).isEqualTo(200);}@TestvoidtestGetTopSellingProductsWithAdminJwt(){JwtRequest request =newJwtRequest("admin","admin123");ResponseEntity<JwtResponse> authResponse = restTemplate.postForEntity("http://localhost:"+ port +"/authenticate", request,JwtResponse.class);String token = authResponse.getBody().getJwt();HttpHeaders headers =newHttpHeaders(); headers.add("Authorization","Bearer "+ token);HttpEntity<String> entity =newHttpEntity<>(headers);ResponseEntity<Map> response = restTemplate.exchange("http://localhost:"+ port +"/api/products/top-selling?topN=3",HttpMethod.GET, entity,Map.class);assertThat(response.getStatusCodeValue()).isEqualTo(200);}}

✅ 结论:JWT是一种基于JSON的开放标准,作用是实现用户认证、用户授权、提供安全的编程模型。

20.6 Spring Boot RESTful API的实际应用场景

在实际开发中,Spring Boot RESTful API的应用场景非常广泛,如:

  • 实现商品的展示与购买。
  • 实现订单的管理。
  • 实现用户的管理。
  • 实现博客的发布与管理。

示例

importorg.springframework.beans.factory.annotation.Autowired;importorg.springframework.boot.SpringApplication;importorg.springframework.boot.autoconfigure.SpringBootApplication;importorg.springframework.data.jpa.repository.JpaRepository;importorg.springframework.stereotype.Repository;importorg.springframework.stereotype.Service;importorg.springframework.transaction.annotation.Transactional;importorg.springframework.web.bind.annotation.*;importjavax.persistence.*;importjava.util.List;// 产品类@Entity@Table(name ="product")publicclassProduct{@Id@GeneratedValue(strategy =GenerationType.IDENTITY)privateLong id;privateString productId;privateString productName;privatedouble price;privateint sales;publicProduct(){}publicProduct(String productId,String productName,double price,int sales){this.productId = productId;this.productName = productName;this.price = price;this.sales = sales;}// Getter和Setter方法publicLonggetId(){return id;}publicvoidsetId(Long id){this.id = id;}publicStringgetProductId(){return productId;}publicvoidsetProductId(String productId){this.productId = productId;}publicStringgetProductName(){return productName;}publicvoidsetProductName(String productName){this.productName = productName;}publicdoublegetPrice(){return price;}publicvoidsetPrice(double price){this.price = price;}publicintgetSales(){return sales;}publicvoidsetSales(int sales){this.sales = sales;}@OverridepublicStringtoString(){return"Product{"+"id="+ id +",+ productId +'\''+",+ productName +'\''+", price="+ price +", sales="+ sales +'}';}}// 产品Repository@RepositorypublicinterfaceProductRepositoryextendsJpaRepository<Product,Long>{List<Product>findBySalesGreaterThan(int sales);}// 产品Service@ServicepublicclassProductService{@AutowiredprivateProductRepository productRepository;@TransactionalpublicvoidaddProduct(Product product){ productRepository.save(product);}@TransactionalpublicvoidupdateProduct(Product product){ productRepository.save(product);}@TransactionalpublicvoiddeleteProduct(Long id){ productRepository.deleteById(id);}@Transactional(readOnly =true)publicList<Product>getAllProducts(){return productRepository.findAll();}@Transactional(readOnly =true)publicList<Product>getTopSellingProducts(int topN){List<Product> products = productRepository.findBySalesGreaterThan(0); products.sort((p1, p2)-> p2.getSales()- p1.getSales());if(products.size()> topN){return products.subList(0, topN);}return products;}}// 产品控制器@RestController@RequestMapping("/api/products")publicclassProductController{@AutowiredprivateProductService productService;@GetMapping("/")publicList<Product>getAllProducts(){return productService.getAllProducts();}@PostMapping("/")publicvoidaddProduct(@RequestBodyProduct product){ productService.addProduct(product);}@PutMapping("/{id}")publicvoidupdateProduct(@PathVariableLong id,@RequestBodyProduct product){ product.setId(id); productService.updateProduct(product);}@DeleteMapping("/{id}")publicvoiddeleteProduct(@PathVariableLong id){ productService.deleteProduct(id);}@GetMapping("/top-selling")publicList<Product>getTopSellingProducts(@RequestParamint topN){return productService.getTopSellingProducts(topN);}}// 应用启动类@SpringBootApplicationpublicclassProductApplication{publicstaticvoidmain(String[] args){SpringApplication.run(ProductApplication.class, args);}@AutowiredprivateProductService productService;publicvoidrun(String... args){// 初始化数据 productService.addProduct(newProduct("P001","手机",1000.0,100)); productService.addProduct(newProduct("P002","电脑",5000.0,50)); productService.addProduct(newProduct("P003","电视",3000.0,80)); productService.addProduct(newProduct("P004","手表",500.0,200)); productService.addProduct(newProduct("P005","耳机",300.0,150));}}// 测试类@SpringBootTest(webEnvironment =SpringBootTest.WebEnvironment.RANDOM_PORT)classProductApplicationTests{@LocalServerPortprivateint port;@AutowiredprivateTestRestTemplate restTemplate;@TestvoidcontextLoads(){}@TestvoidtestGetAllProducts(){List<Product> products = restTemplate.getForObject("http://localhost:"+ port +"/api/products/",List.class);assertThat(products).hasSize(5);}@TestvoidtestAddProduct(){Product product =newProduct("P006","平板",2000.0,70); restTemplate.postForEntity("http://localhost:"+ port +"/api/products/", product,Void.class);List<Product> products = restTemplate.getForObject("http://localhost:"+ port +"/api/products/",List.class);assertThat(products).hasSize(6);}@TestvoidtestUpdateProduct(){Product product =newProduct("P001","手机",1500.0,120); restTemplate.put("http://localhost:"+ port +"/api/products/1", product);List<Product> products = restTemplate.getForObject("http://localhost:"+ port +"/api/products/",List.class);assertThat(products.get(0).getPrice()).isEqualTo(1500.0);}@TestvoidtestDeleteProduct(){ restTemplate.delete("http://localhost:"+ port +"/api/products/2");List<Product> products = restTemplate.getForObject("http://localhost:"+ port +"/api/products/",List.class);assertThat(products).hasSize(4);}@TestvoidtestGetTopSellingProducts(){List<Product> topSellingProducts = restTemplate.getForObject("http://localhost:"+ port +"/api/products/top-selling?topN=3",List.class);assertThat(topSellingProducts).hasSize(3);assertThat(topSellingProducts.get(0).getProductId()).isEqualTo("P004");assertThat(topSellingProducts.get(1).getProductId()).isEqualTo("P005");assertThat(topSellingProducts.get(2).getProductId()).isEqualTo("P001");}}

输出结果

  • 访问http://localhost:8080/api/products/:返回产品列表。
  • 访问http://localhost:8080/api/products/top-selling?topN=3:返回销量TOP3的产品列表。

✅ 结论:在实际开发中,Spring Boot RESTful API的应用场景非常广泛,需要根据实际问题选择合适的RESTful API设计。

总结

本章我们学习了Spring Boot RESTful API开发与测试,包括RESTful API的定义与特点、Spring Boot RESTful API的开发、Spring Boot RESTful API的测试、Spring Boot RESTful API的认证与授权、Spring Boot RESTful API的实际应用场景,学会了在实际开发中处理RESTful API问题。其中,RESTful API的定义与特点、Spring Boot RESTful API的开发、Spring Boot RESTful API的测试、Spring Boot RESTful API的认证与授权、Spring Boot RESTful API的实际应用场景是本章的重点内容。从下一章开始,我们将学习Spring Boot的其他组件、微服务等内容。

Read more

Flutter 组件 graphql 的适配 鸿蒙Harmony 实战 - 驾驭标准化分布式图形协议、实现鸿蒙端实时订阅与高性能交互网关方案

Flutter 组件 graphql 的适配 鸿蒙Harmony 实战 - 驾驭标准化分布式图形协议、实现鸿蒙端实时订阅与高性能交互网关方案

欢迎加入开源鸿蒙跨平台社区:https://openharmonycrossplatform.ZEEKLOG.net Flutter 组件 graphql 的适配 鸿蒙Harmony 实战 - 驾驭标准化分布式图形协议、实现鸿蒙端实时订阅与高性能交互网关方案 前言 在鸿蒙(OpenHarmony)生态的万物互联、极繁交互中台、以及对数据获取灵活性有极致要求的现代应用研发中,“高效的数据检索协议”是应用响应速度的灵魂。面对复杂的社交网络关系查询、实时的行情推送、或是海量状态信息的聚合。如果仅仅依靠传统的 RESTful 接口,那么不仅会导致因为 Over-fetching(获取多余数据)导致的带宽浪费,更会因为频繁的 API 版本演进引入严重的跨端兼容性碎片化问题。 我们需要一种“按需检索、逻辑解耦”的交互艺术。 graphql 是一套专为 Flutter 设计的标准 GraphQL 客户端套件。它通过构建规范的规范化缓存(Normalized Cache)与极其灵活的连接链路(Links)

By Ne0inhk
解决:OpenClaw启动报错:unauthorized: gateway password missing (enter the password in Control UI settings)

解决:OpenClaw启动报错:unauthorized: gateway password missing (enter the password in Control UI settings)

解决:OpenClaw启动报错:unauthorized: gateway password missing (enter the password in Control UI settings) * 一·问题描述: * 1.使用`openclaw gateway`或`openclaw gateway --auth password`两个命令,均能够在终端启动成功 * 2.访问控制UI界面:http://127.0.0.1:18789/,界面有红色字体报错 * 3.配置文件`openclaw.json`的`gateway`配置如下 * 二·问题原因:没有在UI控制界面再次配置OpenClaw密码 * 三·解决方案: * 四·验证:成功对话

By Ne0inhk
微服务链路追踪实战:SkyWalking vs Zipkin 架构深度解析与性能优化指南

微服务链路追踪实战:SkyWalking vs Zipkin 架构深度解析与性能优化指南

目录 1. 链路追踪:分布式系统的“X光机” 1.1 从单体到微服务:排查困境的演变 1.2 链路追踪的核心价值矩阵 2. 核心原理解析:Trace、Span与上下文传播 2.1 基本概念:一次请求的完整“病历” 2.2 上下文传播:Trace ID的“接力赛” 2.3 采样算法:平衡精度与开销的智慧 3. SkyWalking深度解析:无侵入监控的艺术 3.1 架构全景:从Agent到UI的完整链路 3.2 字节码增强:Java Agent的魔法 3.3 生产环境配置模板 3.4 性能特性与调优 4.

By Ne0inhk
Rust异步Web框架Axum的深入原理与高级用法

Rust异步Web框架Axum的深入原理与高级用法

Rust异步Web框架Axum的深入原理与高级用法 一、Axum框架的架构与核心组件 1.1 Axum框架的设计理念 💡Axum是基于Tokio异步运行时的Rust Web框架,由Tokio团队官方维护,具有以下核心设计理念: 1. 模块化与可扩展性:通过中间件、请求提取器和响应映射器等组件,实现高度模块化的架构,允许开发者根据需求灵活组合功能。 2. 类型安全:利用Rust的类型系统确保请求处理逻辑的正确性,减少运行时错误。 3. 异步优先:完全基于Tokio异步运行时,充分利用现代硬件的并发能力。 4. 低门槛:提供简单易用的API,同时保持足够的灵活性,适合不同经验水平的开发者。 1.2 Axum框架的核心组件 1.2.1 请求提取器 请求提取器负责从HTTP请求中提取所需的数据,如路径参数、查询参数、请求体等。Axum提供了多种内置的请求提取器,并允许开发者自定义提取器。 内置请求提取器示例: useaxum::{extract::Path,response::IntoResponse,routing::get,

By Ne0inhk