Spring Boot学习

本文详细介绍了如何使用Spring Boot设计RESTful API,包括HTTP请求方法、标记使用、Controller中的API处理,以及使用MongoRepository进行资料库操作。讨论了GET、POST、PUT、DELETE请求的实现,查询字串的处理,三层式架构的应用,以及MongoDB的数据存储。同时,文章还提及了MockMvc自动化测试的基础知识。
摘要由CSDN通过智能技术生成

学习链接

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

  1. fromCurrentRequest:以当前呼叫的资源路径为基础来建立URI,此处为“ http://…/products”。
  2. path:以目前的资源路径再做延伸,定义新的路径格式,此处为“ http://…/ products / {id}”。
  3. 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)

}

三层式架构

概念

  1. 表示层 Controller
  2. 业务逻辑层:Service
  3. 资料持久层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);
    }
}

基础增删改查方法

  1. findById:根据文件的「_id」栏位来查询。字串参数会自动被转化成「ObjectId」来寻找。
  2. insert:将Product物件新增到资料库集合。若id重复,会抛出「org.springframework.dao.DuplicateKeyException」例外。此处不给Product物件设定id,MongoDB会自动产生并赋值。
  3. save:用Product物件「覆盖」文件。若透过物件的id栏位有找到文件,会正常进行覆盖,否则将新增一个文件。
  4. 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 {
    }
}
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值