天天看點

Java微服務應用開發(簡版)實戰之SpringBoot

一、 SpringBoot 基本介紹

SpringBoot依賴于Spring架構而建構,它簡化了以往複雜配置,并且可以無縫內建大量常用三方元件,極大的提高了工程師的開發效率

1. SpringBoot基礎環境

SpringBoot2.3.1需要Java8及以上版本,對應的Spring Framework 5.2.7.RELEASE,建構工具主要是Maven(3.3+)、Gradle(6.3+)

容器方面:

名稱 Servlet版本
Tomcat9.0 4.0
Jetty9.4 3.1
Undertow 2.0

2. 與SpringCloud的關系

SpringCloud依賴于SpringBoot,建構起強大的微服務生态,要應用好SpringCloud,必須先非常了解SpringBoot。下面我們首先看看SpringBoot相關生态。

二、 SpringBoot實戰

1. 編寫第一個 Web 服務

為了盡快能跑通一個SpringBoot Web服務,最快的方式是通過Spring Initializr生成一個,位址如下:

https://start.spring.io/

在這裡,大家可以選擇版本,maven坐标(Group、Artifact)等,目前SpringBoot的穩定版本是2.1.7.RELEASE,但經過測試,該版本和部分其他開源元件有些沖突,是以暫時采用2.0.4.RELEASE。為了讓大家更快上手,可以直接在pom中加入以下依賴

<parent>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-parent</artifactId>
    <version>2.0.4.RELEASE</version>
    <relativePath/> <!-- lookup parent from repository -->
  </parent>
  
  <dependencies>
    <dependency>
      <groupId>org.springframework.boot</groupId>
      <artifactId>spring-boot-starter-web</artifactId>
    </dependency>
    <dependency>
      <groupId>org.springframework.boot</groupId>
      <artifactId>spring-boot-starter-test</artifactId>
      <scope>test</scope>
    </dependency>
  </dependencies>
           

然後,我們需要建立Controller類,提供Result Api服務:

@RestController
@RequestMapping("/demo")
public class DemoController {

    @GetMapping("/test")
    public String test(){
        return "hello springboot!";
    }
}           

@RestController注解可以将所有的方法的傳回值都直接轉成json格式。@RequestMapping注解加在類上面,用以定義該Controller的公共路徑。@GetMapping表示get請求方式,與此相對的是@PostMapping,表示post請求。

最後,建立服務啟動類,啟動服務:

@SpringBootApplication
public class AppMain {
    public static void main( String[] args ){
        SpringApplication.run(AppMain.class,args);
    }
}           

運作啟動類,此時會在控制台看到關于Tomcat運作在8080端口上的輸出,并能通路:

http://localhost

:8080/demo/test

Tomcat是内嵌的預設伺服器,可以替換成其他伺服器。端口也可以在配置檔案中進行配置。關于個性化定制或者配置,會在後面進行講解。

為了後面測試和檢視友善,我們這裡先加上swagger,依賴:

<dependency>
      <groupId>io.springfox</groupId>
      <artifactId>springfox-swagger2</artifactId>
      <version>2.7.0</version>
    </dependency>

    <dependency>
      <groupId>io.springfox</groupId>
      <artifactId>springfox-swagger-ui</artifactId>
      <version>2.7.0</version>
    </dependency>           

配置:

@Configuration
@EnableSwagger2
//@Profile({"dev","test"})
public class Swagger2 {

    @Bean
    public Docket createRestApi(){

        return new Docket(DocumentationType.SWAGGER_2)
                .apiInfo(apiInfo())
                .select()
                //為目前包路徑
                .apis(RequestHandlerSelectors.basePackage("com.learn.sc.controller"))
                .paths(PathSelectors.any())
                .build();//.globalOperationParameters(pars);

    }

    private ApiInfo apiInfo() {
        return new ApiInfoBuilder()
                //頁面标題
                .title("平台API")//
                //建立人
                .contact(new Contact("dyf", "http://www.baidu.com", "[email protected]"))
                //版本号
                .version("1.0")
                //描述
                .description("")
                .build();
    }
}           

swagger通路位址:

:8080/swagger-ui.html

2. RestFul 語義的遵守與取舍

REST:Representational State Transfer,表示層狀态轉移

GET用來擷取資源

POST用來建立資源(也可以用于更新資源)

PUT用來更新資源

DELETE用來删除資源

示例代碼如下:

@GetMapping("/orders")
    public String listOrders(){
        return "orders";
    }

    @GetMapping("/order/{orderId}")
    public String findOrder(@PathVariable String orderId){
        return "order:"+orderId;
    }

    @PostMapping("/saveOrder")
    public OrderVo saveOrder(@RequestBody OrderVo orderVo){
          return orderVo;
    }

    @PutMapping("/updateOrder")
    public OrderVo updateOrder(OrderVo orderVo){
         return orderVo;
    }

    @DeleteMapping("/deleteOrder")
    public String deleteOrder(String id){
        return "delete "+id;
    }           

在實際開發中,put和delete很少用到,不會刻意遵循restful規範。另外,關于規範的還一個說法是,URI中最好不要包含動詞,隻包含名稱,畢竟它本身表示一種資源,比如上面的saveOrder,應該是/order,但是方法用post。那麼這樣的話,在代碼級别看着就會比較難受,是以大家自行取舍。

3. SpringBoot 與持久層(MyBatis)

不得不說,雖然NoSql的概念炒了很多年,但是以MySql為代表的關系型資料庫仍然是大部分項目的首選,在國内,MyBatis是應用最為廣泛的關系型持久層架構。

SpringBoot+MyBatis整合應用

首先引入持久層相關的依賴,分别是JDBC驅動、連接配接池、MyBatis整合包:

<!-- 持久層相關 begin-->
   
    <dependency>
      <groupId>mysql</groupId>
      <artifactId>mysql-connector-java</artifactId>
      <version>8.0.11</version>
    </dependency>

   <dependency>
      <groupId>com.alibaba</groupId>
      <artifactId>druid</artifactId>
      <version>1.1.14</version>
    </dependency>
    
    <dependency>
      <groupId>org.mybatis.spring.boot</groupId>
      <artifactId>mybatis-spring-boot-starter</artifactId>
      <version>2.0.0</version>
    </dependency>

    <!-- 持久層相關 end-->           

然後在application.properties配置資料庫連接配接資訊:

spring.datasource.distributedtran.type=com.alibaba.druid.pool.DruidDataSource
spring.datasource.distributedtran.driver-class-name=com.mysql.jdbc.Driver
spring.datasource.distributedtran.url=
spring.datasource.distributedtran.username=
spring.datasource.distributedtran.password=           

這裡我們打算用代碼來配置連接配接池,原因在于這樣會更加清晰一點,而且後面會做一些修改,全部放在配置檔案,會比較麻煩和臃腫:

@Configuration
@MapperScan(basePackages = {"com.learn.sc.data.mapper"}, sqlSessionFactoryRef = "disSqlSessionFactory")
public class DataSourceConfig {

    @Value("${spring.datasource.distributedtran.url}")
    private String dbUrl;

    @Value("${spring.datasource.distributedtran.username}")
    private String userName;

    @Value("${spring.datasource.distributedtran.password}")
    private String password;

    @Value("${spring.datasource.distributedtran.driver-class-name}")
    private String driverClassName;

    @Bean(name = "disDataSource")
    public DataSource disDataSource() {
        DruidDataSource druidDataSource = new DruidXADataSource();
        druidDataSource.setUrl(dbUrl);
        druidDataSource.setUsername(userName);
        druidDataSource.setPassword(password);
        druidDataSource.setDriverClassName(driverClassName);
        return druidDataSource;
    }

    @Bean(name = "disSqlSessionFactory")
    public SqlSessionFactory disSqlSessionFactory(@Qualifier("disDataSource") DataSource dataSource) throws Exception {
        SqlSessionFactoryBean sqlSessionFactoryBean = new SqlSessionFactoryBean();
        sqlSessionFactoryBean.setDataSource(dataSource);
        return sqlSessionFactoryBean.getObject();
    }

}           

分别建立Mapper和po(持久層對象):

@Setter
@Getter
public class Account {

    private Integer id;

    private String name;

    private Double balance;

}           
@Mapper
public interface AccountMapper {
    @Insert("insert into account(name,balance) values(#{name},#{balance})")
    void insert(Account account);

    @Select("select * from account where id= #{id}")
    Account query(@Param("id") Integer id);
}           

為了更快示範效果,我們直接使用MyBatis的注解,實際效果和XML配置一模一樣,但更簡潔了。

最後我們使用JUnit測試一下:

@RunWith(SpringRunner.class)
@SpringBootTest(classes = AppMain.class, webEnvironment = SpringBootTest.WebEnvironment.DEFINED_PORT)
public class AccountServiceTest {

    @Autowired
    private AccountService accountService;

    @Test
    public void testSave(){
        Account account=new Account();
        account.setName("SpringBoot");
        account.setBalance(100.0);
        accountService.saveAccount(account);
    }
}           

以上是一個最基本的MyBatis用法,下面看看事務相關的處理。

事務處理

我們經常希望某些操作要麼同時成功,要麼同時失敗,以達到一緻性。

Java微服務應用開發(簡版)實戰之SpringBoot

比如轉賬這個操作,假設從賬号A中轉金額x到賬号B中,過程一般是:先扣除A中的金額x,然後再增加賬号B中金額x,下面代碼展示了這個過程:

/**
     * 轉賬
     * @param fromId 轉出賬号
     * @param toId 轉入賬号
     * @param balance 轉入金額
     */
    public void transferAccount(Integer fromId, Integer toId,Double balance){
        Account fromAccount=accountMapper.query(fromId);
        double fromAccountBalance=fromAccount.getBalance()-balance;
        accountMapper.updateBalance(fromId,fromAccountBalance);

        Account toAccount=accountMapper.query(toId);
        double toAccountBalance=toAccount.getBalance()+balance;
        accountMapper.updateBalance(toId,toAccountBalance);

        //int i=10/0;
    }           

在正常情況下,這個操作沒有任何問題,但是當其中有一個出現異常時,另外一個并不會復原到最初狀态,由于資料庫異常比較難以模拟,這裡直接用個最基礎的除數為0的異常來模拟。

很顯然,我們是希望在這個方法裡,任何地方(包括兩個資料庫操作)出現異常後,都能復原,那麼怎麼做呢?很簡單,可以直接在方法簽名上加上:

@Transactional(rollbackFor = {Exception.class})           

這種方式對于單庫操作是沒問題的,但是假如需要操作多個庫,并能保持事務性,這種方式就失效了。

首先遇到的問題就是,一旦配置了多資料源,即使是單庫操作,事務也是失效狀态。

下面我們測試下。首先建立order庫,并建立orderdetail表,如下:

CREATE TABLE `orderdetail` (
  `id` int(11) NOT NULL AUTO_INCREMENT,
  `price` double DEFAULT NULL,
  `name` varchar(5) DEFAULT NULL,
  PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;           

在項目中,新增mapper、po等基礎代碼(過程略),下面直接上業務代碼:

/**
     * 更新賬戶餘額并新增訂單詳情
     * @param account
     * @param orderDetail
     */

    @Transactional(rollbackFor = {Exception.class})
    public void updateCoreData(Account account,OrderDetail orderDetail){
        Integer accountId=account.getId();
        Account originAccount=accountMapper.query(accountId);
        Double balance=originAccount.getBalance()-orderDetail.getPrice();
        accountMapper.updateBalance(accountId,balance);
        orderMapper.insert(orderDetail);
    }           

這個業務方法用于模拟使用者,在購買商品後,更新賬戶餘額并新增訂單記錄(這裡隻是簡單模拟,實際業務代碼會比這裡嚴謹)。測試代碼如下:

@Test
    public void testDistributedTran(){
        Account account=new Account();
        account.setId(12);

        OrderDetail orderDetail=new OrderDetail();
        orderDetail.setName("Java8 實戰");
        orderDetail.setPrice(120.0);

        accountService.updateCoreData(account,orderDetail);

    }           

正常情況下,這個操作可以完全成功,但是假如order的插入報錯,也别期望事務能起作用,原因就在于:這個業務方法裡面,實際上已經是在處理兩個不同資料庫的表,已經是分布式事務的範疇,而@Transactional壓根不支援分布式事務。有關于分布式事務的内容,我們以後再去探讨。

4. SpringBoot 與緩存

最常見的緩存中間件即Redis,在Java client

中,主要有Jedis和Lettuce兩個庫可選。前者是較早的老庫了,線程非安全,性能一般,後者基于Netty建構,性能較好,且線程安全。spring-boot-starter-data-redis提供了統一的API,用于Redis的不同client。SpringBoot2的data-redis中,內建了Lettuce,是以隻需要做如下依賴:

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>           

下面簡單看下它的基礎用法:

Redis支援的資料類型有string、hash、list、set、zset、geo。

string:簡單字元串類型

hash:hash值類型,對象結構

list:雙向連結清單結構,有序且可重複,類似于LinkedList

set:無序集合結構,且不可重複

zset:帶權重分數的有序集合

geo:地理位置結構

RedisTemplate直接支援上述所有資料結構的操作,下面主要示範常用的string、list、hash類型。

string操作:
//設定一個字元串值
redisTemplate.opsForValue().set("age","18");
Assert.assertEquals("18",redisTemplate.opsForValue().get("age"));

//設定帶有過期時間的字元串值
redisTemplate.opsForValue().set("name","microfocus",10,TimeUnit.SECONDS);
Assert.assertEquals("microfocus",redisTemplate.opsForValue().get("name"));
try {
    Thread.sleep(11000);
} catch (InterruptedException e) {
    e.printStackTrace();
}
Assert.assertNull(redisTemplate.opsForValue().get("name"));           
list操作:
redisTemplate.delete("orderList");

//向左邊插入三個元素
redisTemplate.opsForList().leftPush("orderList","order1");
redisTemplate.opsForList().leftPush("orderList","order2");
long count=redisTemplate.opsForList().leftPush("orderList","order3");
Assert.assertEquals(3,count);

//列出所有值[order3, order2, order1]
List<Object> listValues=redisTemplate.opsForList().range("orderList",0,-1);

//取得某個索引下的值
Object value2=redisTemplate.opsForList().index("orderList",2);
Assert.assertEquals("order1",value2);

//彈出最左邊的值,并删除之
Object leftValue=redisTemplate.opsForList().leftPop("orderList");
Assert.assertEquals("order3",leftValue);
Assert.assertEquals(2,redisTemplate.opsForList().size("orderList").longValue());           
hash操作:
redisTemplate.opsForHash().put("person","name","A");
redisTemplate.opsForHash().put("person","age","18");

//{name=A, age=18}
Map personMap=redisTemplate.opsForHash().entries("person");
personMap.put("sex","男");
redisTemplate.opsForHash().putAll("person",personMap);