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

Codex 真香!终端 AI 编程神器装好了,Cursor可以不续费了

Codex 真香!终端 AI 编程神器装好了,Cursor可以不续费了

🧠 Codex 是什么? Codex 是 OpenAI 推出的终端编程助手,支持自然语言指令直接生成/修改代码,并能运行任务、创建文件、调用脚本等——ChatGPT Plus 用户免费使用! • 🖥️ 支持终端编程 CLI 模式 • 🧩 支持 VS Code 插件(也支持其他 IDE) • 💬 本地运行、支持自然语言交互、任务可记忆 • ✅ 支持 GPT-4 系列(目前只有 GPT-5.x 模型,不支持 gpt-4o)   ⸻ 支持两种安装方式: 1. 1. 🚀 快速安装 Codex CLI npm install -g codex-cli codex login 安装成功后输入 codex,就可以开始使用:

By Ne0inhk
【2026必看 AI智能体】零基础Coze平台使用教程

【2026必看 AI智能体】零基础Coze平台使用教程

目录 一、Coze智能体实战初体验 1.1 写提示词 1.2 预览智能体 1.3 发布智能体 二、Coze入门 2.1 大语言模型LLM配置 生成多样性-temperature Top P 重复性语句惩罚 携带上下文轮数 最大回复长度 2.2 插件 什么是插件? 插件使用 三、智能体之知识(RAG-高考志愿填报) 3.1 智能体提示词 3.2 知识之文本 3.3 知识之表格 3.4 知识之图片 3.5 如何管理本地知识库 四、Coze记忆-对话体验 4.1

By Ne0inhk
人工智能:深度学习中的卷积神经网络(CNN)实战应用

人工智能:深度学习中的卷积神经网络(CNN)实战应用

人工智能:深度学习中的卷积神经网络(CNN)实战应用 1.1 本章学习目标与重点 💡 学习目标:掌握卷积神经网络的核心原理、经典网络架构,以及在图像分类任务中的实战开发流程。 💡 学习重点:理解卷积层、池化层的工作机制,学会使用 TensorFlow 搭建 CNN 模型并完成训练与评估。 1.2 卷积神经网络核心原理 1.2.1 卷积层:提取图像局部特征 💡 卷积层是 CNN 的核心组件,其作用是通过卷积核对输入图像进行局部特征提取。 卷积核本质是一个小型的权重矩阵。它会按照设定的步长在图像上滑动。每滑动一次,卷积核就会与对应区域的像素值做内积运算,输出一个特征值。 这个过程可以捕捉图像的边缘、纹理等基础特征。 ⚠️ 注意:卷积核的数量决定了输出特征图的通道数,数量越多,提取的特征维度越丰富。 ① 定义一个 3×3 大小的卷积核,步长设为 1,填充方式为 SAME

By Ne0inhk
【保姆级】TrendRadar本地部署:告别算法推荐,打造个人专属AI热点情报局

【保姆级】TrendRadar本地部署:告别算法推荐,打造个人专属AI热点情报局

【保姆级】TrendRadar本地部署:告别算法推荐,打造个人专属AI热点情报局 摘要 本文基于 TrendRadar 项目,详细拆解如何通过 Docker 本地部署一套个人专属的 AI 热点情报系统。涵盖从环境准备、Docker 镜像拉取、局域网访问配置、飞书推送修复到 Cherry Studio MCP 服务对接的全流程。通过 Nvidia 免费模型 API,实现零成本的深度热点分析与趋势追踪。 关键词: TrendRadar, Docker部署, AI热点分析, MCP服务, Cherry Studio 1. 背景与需求:为什么我们需要 TrendRadar? 在这个大数据算法横行的时代,我们每天被头条、抖音等平台“投喂”大量信息,不仅容易陷入信息茧房,还浪费了大量时间筛选有效资讯。 我的核心需求很简单: 1. 拒绝算法绑架:需要一个个人定制化的新闻推送服务。 2.

By Ne0inhk