Spring boot
学习链接
设计RESTful API
REST(Representational State Transfer),是一种设计风格,将网络上的东西都视为资源,并且有不同的操作方式。一个完整的RESTful API,包含请求方法method与资源路径url。
HTTP请求方法
方法 | 功能 |
---|---|
GET | 取得资源 |
POST | 新增资源 |
PUT | 覆盖资源 |
PATCH | 部分更新资源 |
DELETE | 删除资源 |
spring boot 的 API 表示法
标记
@RestController:标记在用来接收请求与回传资料的表示层。
@Service:标记在负责资料处理的业务逻辑层。
@Repository:标记在能与资料库互动的资料持久层。
@Configuration:标记在专门读取应用程式设定值的类别。
@Component:如果一个类别不太好归类到以上类型,可以使用这个名称比较通俗的标记,它的中文意思就是「元件」而已。
@Bean:标记在方法上,其回传值将被建立成元件。该方法通常被宣告在Configuration类别中。好处是能自行进行元件的建构。
// 取得一个产品
@GetMapping("/products/{id}")
// 取得全部产品
@GetMapping("/products")
// 新增产品
@PostMapping("/products")
// 编辑一个产品
@PutMapping("/products/{id}")
// 刪除一个产品
@DeleteMapping("/products/{id}")
// 对一个购物车做结账
@PostMapping("/carts/{id}/checkout")
标记名称代表HTTP请求方法,参数值代表路径的格式,路径格式中的{id}是占位符,前端呼叫API时,后端可通过id取得传送过来的值
Controller中的API
@RestController
@RequestMapping(produces = MediaType.APPLICATION_JSON_VALUE)
public class ProductController {
@GetMapping("/products/{id}")
public Product getProduct(@PathVariable("id") String id) {
Product product = new Product();
product.setId(id);
product.setName("Romantic Story");
product.setPrice(200);
return product;
}
}
@RestController 代表这是一个Controller
@RequestMapping 借由参数定义回传资料格式为json
@GetMapping 传入资源路径,当前端呼叫时后端会自动执行这个getProduct方法,并通过@PathVariable标记,获取路径中的id值
发送Get请求&自定回应
Get请求将内容放在url中。
Spring Boot提供了回应实体ResponseEntity,回传常见状态200(OK)、201(Created)、204(No Content)、404(Not Found)或用status自定
@GetMapping("/products/{id}")
public ResponseEntity<Product> getProduct(@PathVariable("id") String id) {
Product product = new Product();
product.setId(request.getId());
product.setName(request.getName());
product.setPrice(request.getPrice());
return ResponseEntity.ok().body(product);
}
发送POST请求
POST请求的内容放在RequestBody中,需要先@PostMapping配置API,再通过@RequestBody来接收前端送来的请求对象,SpringBoot响应请求的JSON字串转换为该参数型态的对象。
新增资源的API,一般会响应状态码201(已创建),并附上指向这个新资源的URI。此处通过“ ServletUriComponentsBuilder”来建立URI
- fromCurrentRequest:以当前呼叫的资源路径为基础来建立URI,此处为“ http://…/products”。
- path:以目前的资源路径再做延伸,定义新的路径格式,此处为“ http://…/ products / {id}”。
- buildAndExpand:将参数填入路径,产生真实的资源路径,此处为“ http://…/ products /实际产品编号”。
@PostMapping("/products")
public ResponseEntity<Product> createProduct(@RequestBody Product request) {
boolean isIdDuplicated = productDB.stream()
.anyMatch(p -> p.getId().equals(request.getId()));
if (isIdDuplicated) {
return ResponseEntity.status(HttpStatus.CONFLICT).build();
}
Product product = new Product();
product.setId(request.getId());
product.setName(request.getName());
product.setPrice(request.getPrice());
productDB.add(product);
URI location = ServletUriComponentsBuilder
.fromCurrentRequest()
.path("/{id}")
.buildAndExpand(product.getId())
.toUri();
return ResponseEntity.created(location).body(product);
}
发送PUT请求
@PutMapping标记可配置PUT请求的API,「@PathVariable」与「@RequestBody」标记,分别获取资源路径上的id,以及请求主体经转换后的资料。
@PutMapping("/products/{id}")
public ResponseEntity<Product> replaceProduct(
@PathVariable("id") String id, @RequestBody Product request) {
Optional<Product> productOp = productDB.stream()
.filter(p -> p.getId().equals(id))
.findFirst();
if (!productOp.isPresent()) {
return ResponseEntity.notFound().build();
}
Product product = productOp.get();
product.setName(request.getName());
product.setPrice(request.getPrice());
return ResponseEntity.ok().body(product);
}
发送DELETE请求
DELETE API可以删除指定的资源。实际上有时是真的把资料删除(hard delete),有时是用一个栏位代表隐藏(soft delete),本节采用前者。
使用「@DeleteMapping」标记可配置DELETE请求的API,并使用「@PathVariable」标记获取资源路径上的id。
@DeleteMapping("/products/{id}")
public ResponseEntity<Void> deleteProduct(@PathVariable("id") String id) {
boolean isRemoved = productDB.removeIf(p -> p.getId().equals(id));
if (isRemoved) {
return ResponseEntity.noContent().build();
} else {
return ResponseEntity.notFound().build();
}
}
查询字串
GET API取得产品
「@RequestParam」标记可接收查询字串,并赋值给「keyword」字串,作为搜寻关键字。
「value」参数可指定要接收资源路径上的哪个参数,预设为方法的参数名称keyword。
「defaultValue」参数则可在请求未带上查询字串时,给予keyword预设值。
「required」参数能规定是否必须带上这个查询字串,预设值为true。若需要带上却没带,会得到状态码400(Bad Request)。
@GetMapping ( " /products " )
public ResponseEntity< List< Product > > getProducts(
@RequestParam ( value = " keyword " , defaultValue = " " ) String keyword) {
List< Product > products = productDB . stream()
.filter(p - > p . getName() . toUpperCase() . contains(keyword . toUpperCase()))
.collect( Collectors . toList());
return ResponseEntity . ok() . body(products);
}
该标记还有另一种用法,是代替「@GetMapping」之类的API配置标记。Spring Boot并未提供所有种类的标记,因此其他请求方法就得透过@RequestMapping标记配置。用法如下:
@RestController
@RequestMapping(value = "/products", produces = MediaType.APPLICATION_JSON_VALUE)
public class ProductController {
// @GetMapping("/{id}")
@RequestMapping(value = "/{id}", method = RequestMethod.GET)
// @PostMapping
@RequestMapping(method = RequestMethod.POST)
// POST /products
@PostMapping
// @PatchMapping("/{id}")
@RequestMapping(value = "/{id}", method = RequestMethod.PATCH)
// Spring Boot 並未提供HEAD請求方法的配置標記
@RequestMapping(method = RequestMethod.HEAD)
}
三层式架构
概念
- 表示层 Controller
- 业务逻辑层:Service
- 资料持久层DAO:Repository
抛出异常
先定义异常类,继承「RuntimeException」,并使用「@ResponseStatus」标记,定义抛出异常时回应给呼叫方的HTTP状态码。
@ResponseStatus ( HttpStatus . CONFLICT )
public class ConflictException extends RuntimeException {
public ConflictException () {
super ();
}
public ConflictException ( String message ) {
super (message);
}
}
资料持久层
「@Repository」标记
@Repository
public class MockProductDAO {
private List< Product > productDB = new ArrayList<> ();
public Product insert ( Product product ) {
return null ;
}
public Product replace ( String id , Product product ) {
return null ;
}
public void delete ( String id ) {
}
public Optional< Product > find ( String id ) {
return null ;
}
public List< Product > find ( ProductQueryParameter param ) {
return null ;
}
}
业务逻辑层
「@Service」标记
调用DAO层时用「@Autowired」,让Spring Boot启动时,自动给该变量传入对象。
使用@Autowired标记的全域变数,其资料型态必须是有添加特定标记的类别,如「@Service」与「@Repository」等
@Service
public class ProductService {
@Autowired
private MockProductDAO productDAO;
public Product createProduct( Product request) {
boolean isIdDuplicated = productDAO . find(request . getId()) . isPresent();
if (isIdDuplicated) {
throw new ConflictException ( " The id of the product is duplicated. " );
}
Product product = new Product ();
product . setId(request . getId());
product . setName(request . getName());
product . setPrice(request . getPrice());
return productDAO . insert(product);
}
public Product getProduct( String id) {
return productDAO . find(id)
.orElseThrow(() - > new NotFoundException ( " Can't find product. " ));
}
public Product replaceProduct( String id, Product request) {
Product product = getProduct(id);
return productDAO . replace(product . getId(), request);
}
public void deleteProduct( String id) {
Product product = getProduct(id);
productDAO . delete(product . getId());
}
public List< Product > getProducts( ProductQueryParameter param) {
return productDAO . find(param);
}
Controller改写
改写Controller层,让每个API都调用Service层
@RestController
@RequestMapping ( " /products " )
public class ProductController {
@Autowired
private ProductService productService;
@GetMapping ( " /{id} " )
public ResponseEntity< Product > getProduct ( @PathVariable ( " id " ) String id ) {
Product product = productService . getProduct(id);
return ResponseEntity . ok(product);
}
@PostMapping
public ResponseEntity< Product > createProduct ( @RequestBody Product request ) {
Product product = productService . createProduct(request);
URI location = ServletUriComponentsBuilder
.fromCurrentRequest()
.path( " /{id} " )
.buildAndExpand(product . getId())
.toUri();
return ResponseEntity . created(location) . body(product);
}
@PutMapping ( " /{id} " )
public ResponseEntity< Product > replaceProduct (
@PathVariable ( " id " ) String id , @RequestBody Product request ) {
Product product = productService . replaceProduct(id, request);
return ResponseEntity . ok(product);
}
@DeleteMapping ( " /{id} " )
public ResponseEntity deleteProduct ( @PathVariable ( " id " ) String id ) {
productService . deleteProduct(id);
return ResponseEntity . noContent() . build();
}
@GetMapping
public ResponseEntity< List< Product > > getProducts ( @ModelAttribute ProductQueryParameter param ) {
List< Product > products = productService . getProducts(param);
return ResponseEntity . ok(products);
}
}
使用MongoRepository存取资料库
对象存储为Document格式
在该类别加上「@Document」标记,并传入「collection」参数,可以指定这个Product对象在MongoDB资料库要存到哪个集合。
图中的「_id」栏位,是MongoDB固定使用的文件主键,不会重复。并且Java类别中若有名称为「id」的栏位,都将被转换成「_id」。
图中的「_class」栏位,是告诉函式库要将文件转换成哪一种Java对象。
@Document ( collection = " products " )
public class Product {
private String id;
private String name;
private int price;
//略过get与set方法
}
资料持久层
「@Repository」元件标记
「ProductRepository」继承MongoRepository,得到基本的增删改查方法。
Spring Boot启动时会根据方法的定义操作,比方说「findById」方法,它的名称会被解读,变成真的能查询资料的方法。
@Repository
public interface ProductRepository extends MongoRepository< Product , String > {
}
Service 层调用Repository
@Service
public class ProductService {
@Autowired
private ProductRepository repository;
}
改写先前的方法:
@Service
public class ProductService {
public Product getProduct ( String id ) {
return repository . findById(id)
.orElseThrow(() - > new NotFoundException ( " Can't find product. " ));
}
public Product createProduct ( Product request ) {
Product product = new Product ();
product . setName(request . getName());
product . setPrice(request . getPrice());
return repository . insert(product);
}
public Product replaceProduct ( String id , Product request ) {
Product oldProduct = getProduct(id);
Product product = new Product ();
product . setId(oldProduct . getId());
product . setName(request . getName());
product . setPrice(request . getPrice());
return repository . save(product);
}
public void deleteProduct ( String id ) {
repository . deleteById(id);
}
}
基础增删改查方法
- findById:根据文件的「_id」栏位来查询。字串参数会自动被转化成「ObjectId」来寻找。
- insert:将Product物件新增到资料库集合。若id重复,会抛出「org.springframework.dao.DuplicateKeyException」例外。此处不给Product物件设定id,MongoDB会自动产生并赋值。
- save:用Product物件「覆盖」文件。若透过物件的id栏位有找到文件,会正常进行覆盖,否则将新增一个文件。
- deleteById:根据文件的「_id」栏位来删除资料。字串参数id会被转化成「ObjectId」来寻找。即便文件不存在,也不会抛出异常。
其中insert和save方法会回传资料库处理后的结果,包含自动产生的id。
自定义查询
//找出name栏位值有包含参数的所有文件,且不分大小写
List< Product > findByNameLikeIgnoreCase( String name);
//找出id栏位值有包含在参数之中的所有文件
List< Product > findByIdIn( List< String > ids);
//是否有文件的email栏位值等于参数
boolean existsByEmail( String email);
//找出username与password栏位值皆符合参数的一笔文件
Optional< User > findByUsernameAndPassword( String email, String pwd);
排序
加上「Sort」型态的参数
Sort sort = new Sort(Sort.Direction.ASC, "price");
List<Product> findByNameLikeIgnoreCase(String name, Sort sort);
多重排序:
//先按price递增,再按name递减
Sort sort = Sort.by(
Sort.Order.asc("price"),
Sort.Order.desc("name")
);
使用Mongo语法查询
「@Query」标记让开发人员能传入MongoDB的语法,直接针对资料库文件的既有栏位进行查询。
//查询price栏位在特定范围的文件(参数亦可使用Date)
// gte:大于等于;lte:小于等于;Between:大于及小于,两者略有差异。
@Query ( " {'price': {'$gte': ?0, '$lte': ?1}} " )
List< Product > findByPriceBetween( int from, int to);
//查询name字串栏位有包含参数的文件,不分大小写
@Query ( " {'name': {'$regex': ?0, '$options': 'i'}} " )
List< Product > findByNameLikeIgnoreCase( String name);
//查询同时符合上述两个条件的文件
@Query ( " {'$and': [{'price': {'$gte': ?0, '$lte': ?1}}, {'name': {'$regex': ?2, ' $options': 'i'}}]} " )
List< Product > findByPriceBetweenAndNameLikeIgnoreCase( int priceFrom, int priceTo, String name);
语法中的「?0」、「?1」等符号,程序会分别填入方法的第一、二个参数,来产生资料库能使用的语法。
@Query也可以传入「count」、「exists」、「delete」与「sort」等参数
//回传id栏位值有包含在参数之中的文件数量
@Query ( value = " {'_id': {'$in': ?0}} " , count = true )
int countByIdIn( List< String > ids);
//回传是否有文件的id栏位值包含在参数之中
@Query ( value = " {'_id': {'$in': ?0}} " , exists = true )
boolean existsByIdIn( List< String > ids);
//删除id栏位值包含在参数之中的文件
@Query ( delete = true )
void deleteByIdIn( List< String > ids);
//找出id栏位值有包含在参数之中的文件,并先后做name栏位递增与price栏位递减的排序
@Query ( sort = " {'name': 1, 'price': -1} " )
List< Product > findByIdInOrderByNameAscPriceDesc( List< String > ids);
第三、四个方法,未在@Query标记传入MongoDB语法,此时查询条件将套用方法名称。
MockMvc自动化测试(学习中······)
Spring Boot提供的「MockMvc」套件能模拟出发送HTTP请求的动作,并取得状态码、回应标头与主体等结果。此外也可以检查回应内容是否如开发者所预期,例如有几笔资料、某个JSON栏位值为多少
首先在pom.xml写入
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
标记
「@RunWith」和「@SpringBootTest」定义测试程式要在Spring Boot的环境下执行。
@AutoConfigureMockMvc」代表测试开始时会在元件容器中建立MockMvc对象
@RunWith ( SpringRunner . class)
@SpringBootTest
@AutoConfigureMockMvc
public class ProductTest {
@Autowired
private MockMvc mockMvc;
}
测试程序要有新增「resources」资料夹,建立application.properties,避免与开发程序相互影响
「@Test」标记,代表这是要被执行的测试程式
public class ProductTest {
@Autowired
private MockMvc mockMvc;
@Test
public void testCreateProduct () throws Exception {
}
}