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 4.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", "123456@qq.com"))
                //版本号
                .version("1.0")
                //描述
                .description("")
                .build();
    }
}

swagger访问地址:http://localhost: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);
上一篇:《软件开发实践:项目驱动式的Java开发指南》正式出版(译著)


下一篇:Android开发教程 - 使用Data Binding(三)在Activity中的使用