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

C++ 继承入门(上):从基础概念定义到默认成员函数,吃透类复用的核心逻辑

C++ 继承入门(上):从基础概念定义到默认成员函数,吃透类复用的核心逻辑

🔥小叶-duck:个人主页 ❄️个人专栏:《Data-Structure-Learning》 《C++入门到进阶&自我学习过程记录》《算法题讲解指南》--从优选到贪心 ✨未择之路,不须回头 已择之路,纵是荆棘遍野,亦作花海遨游 目录 前言 一. 继承的概念与定义   1、继承的核心概念   2、继承的定义格式   3、继承方式与成员访问权限 二. 基类与派生类的转换:子类对象能当父类用吗? 三. 继承中的作用域:同名成员会冲突吗?   1、变量隐藏   2、函数隐藏 四、派生类的默认成员函数:构造、拷贝、析构怎么写?   1、构造函数:先调用父类构造,再初始化子类成员   2、拷贝构造:先拷贝父类,再拷贝子类   3、 赋值重载:

By Ne0inhk
RPC魔法揭秘:从原理到BRPC实战,用C++玩转分布式通信

RPC魔法揭秘:从原理到BRPC实战,用C++玩转分布式通信

文章目录 * 本篇摘要 * 一.什么是rpc * 简单理解 * 核心特点 * RPC 工作原理 * 常见 RPC 框架 * 典型使用场景 * 二.BRPC介绍 * 是什么? * 比gRPC强在哪? * 三.基于brpc实现简单的服务调用 * brpc安装教程 * 简单实现客户端向brpc服务端口请求服务完成应答过程(以echo回显为例) * 测试效果 * 代码汇总 * 四.封装每个服务的channels及所有服务管理者 * 五.基于etcd实现服务上下线监控来完成brpc服务调用 * 测试效果 * 代码汇总 * 六.本篇小结 本篇摘要 本文从RPC核心概念出发,阐释其“透明远程调用”的本质与工作原理,对比主流框架后聚焦百度开源的C++高性能RPC框架BRPC,详解其安装、Echo服务示例代码(含客户端/服务端实现),并延伸介绍基于ETCD的服务注册发现与信道管理封装,完整呈现分布式通信方案落地过程。 一.什么是rpc 简单理解 RPC(远程过程调用)就是让程序调用

By Ne0inhk

Visual C++运行库一键修复终极指南:告别DLL缺失烦恼

Visual C++运行库一键修复终极指南:告别DLL缺失烦恼 【免费下载链接】vcredistAIO Repack for latest Microsoft Visual C++ Redistributable Runtimes 项目地址: https://gitcode.com/gh_mirrors/vc/vcredist 你是否曾经遇到过这样的情况?✅ 刚下载的游戏无法启动,提示"VCRUNTIME140.dll缺失";⚠️ 专业软件突然崩溃,显示错误代码0xc000007b;🚀 重装系统后原本正常的程序无法运行。这些问题往往都源于Visual C++运行库组件的问题。 Visual C++运行库是Windows系统中至关重要的组件,它为使用Visual Studio开发的应用程序提供运行时支持。当这些运行库缺失、损坏或版本不匹配时,各种软件就会出现运行异常。今天,我将为你介绍一款强大的修复工具——VisualCppRedist AIO,让你轻松解决这些烦人的系统依赖问题。 常见问题场景:你中招了吗?

By Ne0inhk
【C++笔记】STL详解:string的实现

【C++笔记】STL详解:string的实现

前言:                 在前面的学习中,我们已经初步掌握了string类接口函数的使用方法,本文将带领大家从零开始,逐步实现一个完整的string类。          一、string类总览                 温馨提示: 为了避免与标准库中的string产生命名冲突,我们使用mystd命名空间进行封装。 namespace mystd { class string { public: //迭代器 typedef char* iterator; typedef const char* const_iterator; //默认成员函数 string(); string(const char* str); //构造函数 string(const string& s); //拷贝构造函数 string& operator=(const string& s); //赋值运算符重载函数 ~string(); //析构函数 //迭代器相关函数 iterator begin(

By Ne0inhk