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

免费部署openClaw龙虾机器人(经典)

免费部署openClaw龙虾机器人(经典)

前几天出了个免费玩龙虾的详细教程,很多小伙伴觉得不错,但是还有一些新手留言反馈内容不够详细,这次我将重新梳理一遍,做一期更细致的攻略,同时扩展补充配置好之后的推荐(我认为是必要)操作,争取一篇文章让大家可以收藏起来,随时全套参照复用。 先看效果测试 部署完成基础运行效果测试,你可以直接问clawdbot当前的模型: 1.Token平台准备 首先,还是准备好我们可以免费撸的API平台 这里我找到了两个可以免费使用的API,测试之后执行效率还可以,下面将分别进行细致流程拆解。 1.1 硅基流动获取ApiKey (相对免费方案 推荐) 硅基流动地址:https://cloud.siliconflow.cn/i/6T57VxS2 如果有账号的直接登录,没有的注册一个账号,这个认证就送16元,可以直接玩收费模型,真香。认证完成后在API秘钥地方新建秘钥。 硅基流动里面很多模型原来是免费的,有了16元注册礼,很多收费的模型也相当于免费用了,我体验一下了原来配置免费模型还能用,也是值得推荐的。建议使用截图的第一个模型体验一下,我一直用它。 1.2 推理时代

小米 “养龙虾”:手机 Agent 落地,智能家居十年困局被撬开

小米 “养龙虾”:手机 Agent 落地,智能家居十年困局被撬开

3月6日,小米正式推出国内首个手机端类 OpenClaw Agent 应用 ——Xiaomi miclaw,开启小范围邀请封测。这款被行业与网友戏称为小米 “开养龙虾” 的新品,绝非大模型浪潮下又一款语音助手的常规升级,而是基于自研 MiMo 大模型、具备系统级权限、全场景上下文理解能力的端侧智能体。 作为深耕智能家居领域的行业媒体,《智哪儿》始终认为:智能家居行业过去十年的迭代,始终没能跳出 “被动执行” 的底层困局。而 miclaw 的落地,不止是小米在端侧 AI 赛道的关键落子,更是为整个智能家居行业的底层逻辑重构,提供了可落地的参考范本。需要清醒认知的是,目前该产品仍处于小范围封测阶段,复杂场景执行成功率、端侧功耗表现、第三方生态适配进度等核心体验,仍有待大规模用户实测验证。本文将结合具象场景、量化数据与多维度视角,客观拆解 miclaw 的突破价值、现实挑战,以及它对智能家居行业的长期影响。 01 复盘行业困局:智能家居十年 始终困在 “被动执行”

《星辰 RPA 全自动:做一个小红书自动发文机器人》

《星辰 RPA 全自动:做一个小红书自动发文机器人》

前引:在企业数智化转型的浪潮中,如何突破 “有 AI 无落地、有流程无智能” 的困局?星辰 Agent 与星辰 RPA 的出现,正是为了解决这一痛点。作为科大讯飞旗下的双核心产品,星辰 Agent 以企业级 Agentic Workflow 开发平台为底座,提供 AI 工作流编排、模型管理与跨系统连接能力;而星辰 RPA 则以超过 300 个自动化原子能力,让业务流程真正 “动” 起来! 目录 一、企业机器人自动化平台:RPA (1)RPA介绍 (2)服务端安装 (1)clone项目 (2)配置为本地访问 (3)检查镜像源 (4)配置default.conf

GTC2026前瞻(二)Agentic AI 与开源模型篇+(三)Physical AI 与机器人篇

GTC2026前瞻(二)Agentic AI 与开源模型篇+(三)Physical AI 与机器人篇

(二)Agentic AI 与开源模型篇 Agentic AI与开源模型:英伟达想定义的,不只是“更聪明的模型”,而是“能持续工作的数字劳动力” 如果说过去两年的大模型竞赛,核心问题还是“谁能生成更像人的答案”,那么到了 GTC 2026,问题已经明显变了。英伟达把 Agentic AI 直接列为大会四大核心主题之一,官方对这一主题的定义也很明确:重点不再是单轮问答,而是让 AI agent 能够推理、规划、检索并执行动作,最终把企业数据转化为可投入生产的“数字劳动力”。这说明,Agentic AI 在英伟达的语境里,已经不是一个前沿概念,而是下一阶段 AI 商业化的主战场。(NVIDIA) 一、GTC 2026真正的变化,是 AI 开始从“会回答”走向“会做事”