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

本文涉及的产品
服务治理 MSE Sentinel/OpenSergo,Agent数量 不受限
云原生网关 MSE Higress,422元/月
注册配置 MSE Nacos/ZooKeeper,118元/月
简介: 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
目录
相关文章
|
8天前
|
JSON Java Apache
非常实用的Http应用框架,杜绝Java Http 接口对接繁琐编程
UniHttp 是一个声明式的 HTTP 接口对接框架,帮助开发者快速对接第三方 HTTP 接口。通过 @HttpApi 注解定义接口,使用 @GetHttpInterface 和 @PostHttpInterface 等注解配置请求方法和参数。支持自定义代理逻辑、全局请求参数、错误处理和连接池配置,提高代码的内聚性和可读性。
|
17天前
|
人工智能 前端开发 Java
基于开源框架Spring AI Alibaba快速构建Java应用
本文旨在帮助开发者快速掌握并应用 Spring AI Alibaba,提升基于 Java 的大模型应用开发效率和安全性。
基于开源框架Spring AI Alibaba快速构建Java应用
|
10天前
|
SQL Java 数据库连接
从理论到实践:Hibernate与JPA在Java项目中的实际应用
本文介绍了Java持久层框架Hibernate和JPA的基本概念及其在具体项目中的应用。通过一个在线书店系统的实例,展示了如何使用@Entity注解定义实体类、通过Spring Data JPA定义仓库接口、在服务层调用方法进行数据库操作,以及使用JPQL编写自定义查询和管理事务。这些技术不仅简化了数据库操作,还显著提升了开发效率。
24 3
|
16天前
|
运维 NoSQL Java
后端架构演进:微服务架构的优缺点与实战案例分析
【10月更文挑战第28天】本文探讨了微服务架构与单体架构的优缺点,并通过实战案例分析了微服务架构在实际应用中的表现。微服务架构具有高内聚、低耦合、独立部署等优势,但也面临分布式系统的复杂性和较高的运维成本。通过某电商平台的实际案例,展示了微服务架构在提升系统性能和团队协作效率方面的显著效果,同时也指出了其带来的挑战。
55 4
|
20天前
|
SQL 监控 Java
技术前沿:Java连接池技术的最新发展与应用
本文探讨了Java连接池技术的最新发展与应用,包括高性能与低延迟、智能化管理和监控、扩展性与兼容性等方面。同时,结合最佳实践,介绍了如何选择合适的连接池库、合理配置参数、使用监控工具及优化数据库操作,为开发者提供了一份详尽的技术指南。
29 7
|
18天前
|
SQL Java 数据库连接
在Java应用中,数据库访问常成为性能瓶颈。连接池技术通过预建立并复用数据库连接,有效减少连接开销,提升访问效率
在Java应用中,数据库访问常成为性能瓶颈。连接池技术通过预建立并复用数据库连接,有效减少连接开销,提升访问效率。本文介绍了连接池的工作原理、优势及实现方法,并提供了HikariCP的示例代码。
32 3
|
18天前
|
存储 Java 关系型数据库
在Java开发中,数据库连接是应用与数据交互的关键环节。本文通过案例分析,深入探讨Java连接池的原理与最佳实践
在Java开发中,数据库连接是应用与数据交互的关键环节。本文通过案例分析,深入探讨Java连接池的原理与最佳实践,包括连接创建、分配、复用和释放等操作,并通过电商应用实例展示了如何选择合适的连接池库(如HikariCP)和配置参数,实现高效、稳定的数据库连接管理。
34 2
|
19天前
|
缓存 Java 数据库连接
Hibernate:Java持久层框架的高效应用
通过上述步骤,可以在Java项目中高效应用Hibernate框架,实现对关系数据库的透明持久化管理。Hibernate提供的强大功能和灵活配置,使得开发者能够专注于业务逻辑的实现,而不必过多关注底层数据库操作。
12 1
|
23天前
|
移动开发 前端开发 JavaScript
java家政系统成品源码的关键特点和技术应用
家政系统成品源码是已开发完成的家政服务管理软件,支持用户注册、登录、管理个人资料,家政人员信息管理,服务项目分类,订单与预约管理,支付集成,评价与反馈,地图定位等功能。适用于各种规模的家政服务公司,采用uniapp、SpringBoot、MySQL等技术栈,确保高效管理和优质用户体验。
|
23天前
|
SQL 监控 Java
Java性能优化:提升应用效率与响应速度的全面指南
【10月更文挑战第21】Java性能优化:提升应用效率与响应速度的全面指南