Java微服务应用开发(简版)实战之SpringBoot

本文涉及的产品
服务治理 MSE Sentinel/OpenSergo,Agent数量 不受限
简介: Java微服务应用开发(简版)是以前写的新人培训文档简化而来,力求简单、粗暴、明了,上手快。内容涵盖了SpringBoot、SpringBoot与数据层、SpringCloud、JVM内存机制与问题排查等。

一、 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用法,下面看看事务相关的处理。

事务处理

我们经常希望某些操作要么同时成功,要么同时失败,以达到一致性。

tran.png

比如转账这个操作,假设从账号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);
相关实践学习
基于Redis实现在线游戏积分排行榜
本场景将介绍如何基于Redis数据库实现在线游戏中的游戏玩家积分排行榜功能。
云数据库 Redis 版使用教程
云数据库Redis版是兼容Redis协议标准的、提供持久化的内存数据库服务,基于高可靠双机热备架构及可无缝扩展的集群架构,满足高读写性能场景及容量需弹性变配的业务需求。 产品详情:https://www.aliyun.com/product/kvstore &nbsp; &nbsp; ------------------------------------------------------------------------- 阿里云数据库体验:数据库上云实战 开发者云会免费提供一台带自建MySQL的源数据库&nbsp;ECS 实例和一台目标数据库&nbsp;RDS实例。跟着指引,您可以一步步实现将ECS自建数据库迁移到目标数据库RDS。 点击下方链接,领取免费ECS&amp;RDS资源,30分钟完成数据库上云实战!https://developer.aliyun.com/adc/scenario/51eefbd1894e42f6bb9acacadd3f9121?spm=a2c6h.13788135.J_3257954370.9.4ba85f24utseFl
目录
相关文章
|
16天前
|
移动开发 Java Android开发
构建高效Android应用:探究Kotlin与Java的性能差异
【4月更文挑战第3天】在移动开发领域,性能优化一直是开发者关注的焦点。随着Kotlin的兴起,其在Android开发中的地位逐渐上升,但关于其与Java在性能方面的对比,尚无明确共识。本文通过深入分析并结合实际测试数据,探讨了Kotlin与Java在Android平台上的性能表现,揭示了在不同场景下两者的差异及其对应用性能的潜在影响,为开发者在选择编程语言时提供参考依据。
|
17天前
|
缓存 算法 Java
Java内存管理与调优:释放应用潜能的关键
【4月更文挑战第2天】Java内存管理关乎性能与稳定性。理解JVM内存结构,如堆和栈,是优化基础。内存泄漏是常见问题,需谨慎管理对象生命周期,并使用工具如VisualVM检测。有效字符串处理、选择合适数据结构和算法能提升效率。垃圾回收自动回收内存,但策略调整影响性能,如选择不同类型的垃圾回收器。其他优化包括调整堆大小、使用对象池和缓存。掌握这些技巧,开发者能优化应用,提升系统性能。
|
16天前
|
Java
深入理解Java并发编程:线程池的应用与优化
【4月更文挑战第3天】 在Java并发编程中,线程池是一种重要的资源管理工具,它能有效地控制和管理线程的数量,提高系统性能。本文将深入探讨Java线程池的工作原理、应用场景以及优化策略,帮助读者更好地理解和应用线程池。
|
23天前
|
Java 编译器 Android开发
构建高效Android应用:探究Kotlin与Java的性能差异
在开发高性能的Android应用时,选择合适的编程语言至关重要。近年来,Kotlin因其简洁性和功能性受到开发者的青睐,但其性能是否与传统的Java相比有所不足?本文通过对比分析Kotlin与Java在Android平台上的运行效率,揭示二者在编译速度、运行时性能及资源消耗方面的具体差异,并探讨在实际项目中如何做出最佳选择。
17 4
|
24天前
|
数据采集 分布式计算 大数据
Java语言在大数据处理中的应用
传统的大数据处理往往依赖于庞大的数据中心和高性能的服务器,然而随着大数据时代的到来,Java作为一种强大的编程语言正在被广泛应用于大数据处理领域。本文将探讨Java语言在大数据处理中的优势和应用,以及其在分布式计算、数据处理和系统集成等方面的重要作用。
|
5天前
|
Kubernetes 监控 Cloud Native
构建高效云原生应用:基于Kubernetes的微服务治理实践
【4月更文挑战第13天】 在当今数字化转型的浪潮中,企业纷纷将目光投向了云原生技术以支持其业务敏捷性和可扩展性。本文深入探讨了利用Kubernetes作为容器编排平台,实现微服务架构的有效治理,旨在为开发者和运维团队提供一套优化策略,以确保云原生应用的高性能和稳定性。通过分析微服务设计原则、Kubernetes的核心组件以及实际案例,本文揭示了在多变的业务需求下,如何确保系统的高可用性、弹性和安全性。
11 4
|
6天前
|
Java
探秘jstack:解决Java应用线程问题的利器
探秘jstack:解决Java应用线程问题的利器
14 1
探秘jstack:解决Java应用线程问题的利器
|
10天前
|
XML JSON JavaScript
Java中XML和JSON的比较与应用指南
本文对比了Java中XML和JSON的使用,XML以自我描述性和可扩展性著称,适合结构复杂、需验证的场景,但语法冗长。JSON结构简洁,适用于轻量级数据交换,但不支持命名空间。在Java中,处理XML可使用DOM、SAX解析器或XPath,而JSON可借助GSON、Jackson库。根据需求选择合适格式,注意安全、性能和可读性。
23 0
|
16天前
|
XML JSON JavaScript
使用JSON和XML:数据交换格式在Java Web开发中的应用
【4月更文挑战第3天】本文比较了JSON和XML在Java Web开发中的应用。JSON是一种轻量级、易读的数据交换格式,适合快速解析和节省空间,常用于API和Web服务。XML则提供更强的灵活性和数据描述能力,适合复杂数据结构。Java有Jackson和Gson等库处理JSON,JAXB和DOM/SAX处理XML。选择格式需根据应用场景和需求。
|
17天前
|
安全 Java 容器
Java并发编程:实现高效、线程安全的多线程应用
综上所述,Java并发编程需要注意线程安全、可见性、性能等方面的问题。合理使用线程池、同步机制、并发容器等工具,可以实现高效且线程安全的多线程应用。
14 1