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

简介: 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);
目录
相关文章
|
6月前
|
人工智能 算法 Java
Java与AI驱动区块链:构建智能合约与去中心化AI应用
区块链技术和人工智能的融合正在开创去中心化智能应用的新纪元。本文深入探讨如何使用Java构建AI驱动的区块链应用,涵盖智能合约开发、去中心化AI模型训练与推理、数据隐私保护以及通证经济激励等核心主题。我们将完整展示从区块链基础集成、智能合约编写、AI模型上链到去中心化应用(DApp)开发的全流程,为构建下一代可信、透明的智能去中心化系统提供完整技术方案。
422 3
|
7月前
|
监控 Java 数据库
从零学 Dropwizard:手把手搭轻量 Java 微服务,告别 Spring 臃肿
Dropwizard 整合 Jetty、Jersey 等成熟组件,开箱即用,无需复杂配置。轻量高效,启动快,资源占用少,内置监控、健康检查与安全防护,搭配 Docker 部署便捷,是构建生产级 Java 微服务的极简利器。
770 117
|
7月前
|
监控 Kubernetes Java
使用 New Relic APM 和 Kubernetes Metrics 监控 EKS 上的 Java 微服务
在阿里云AKS上运行Java微服务常遇性能瓶颈与OOMKilled等问题。本文教你通过New Relic实现集群与JVM双层监控,集成Helm部署、JVM代理注入、GC调优及告警仪表盘,打通从节点资源到应用内存的全链路观测,提升排障效率,保障服务稳定。
508 115
|
6月前
|
Cloud Native Serverless API
微服务架构实战指南:从单体应用到云原生的蜕变之路
🌟蒋星熠Jaxonic,代码为舟的星际旅人。深耕微服务架构,擅以DDD拆分服务、构建高可用通信与治理体系。分享从单体到云原生的实战经验,探索技术演进的无限可能。
微服务架构实战指南:从单体应用到云原生的蜕变之路
|
6月前
|
监控 Cloud Native Java
Spring Boot 3.x 微服务架构实战指南
🌟蒋星熠Jaxonic,技术宇宙中的星际旅人。深耕Spring Boot 3.x与微服务架构,探索云原生、性能优化与高可用系统设计。以代码为笔,在二进制星河中谱写极客诗篇。关注我,共赴技术星辰大海!(238字)
1095 2
Spring Boot 3.x 微服务架构实战指南
|
7月前
|
消息中间件 Ubuntu Java
SpringBoot整合MQTT实战:基于EMQX实现双向设备通信
本教程指导在Ubuntu上部署EMQX 5.9.0并集成Spring Boot实现MQTT双向通信,涵盖服务器搭建、客户端配置及生产实践,助您快速构建企业级物联网消息系统。
2512 1
|
6月前
|
消息中间件 缓存 Java
Spring框架优化:提高Java应用的性能与适应性
以上方法均旨在综合考虑Java Spring 应该程序设计原则, 数据库交互, 编码实践和系统架构布局等多角度因素, 旨在达到高效稳定运转目标同时也易于未来扩展.
427 8
|
7月前
|
人工智能 Java API
Java与大模型集成实战:构建智能Java应用的新范式
随着大型语言模型(LLM)的API化,将其强大的自然语言处理能力集成到现有Java应用中已成为提升应用智能水平的关键路径。本文旨在为Java开发者提供一份实用的集成指南。我们将深入探讨如何使用Spring Boot 3框架,通过HTTP客户端与OpenAI GPT(或兼容API)进行高效、安全的交互。内容涵盖项目依赖配置、异步非阻塞的API调用、请求与响应的结构化处理、异常管理以及一些面向生产环境的最佳实践,并附带完整的代码示例,助您快速将AI能力融入Java生态。
1130 12
|
8月前
|
负载均衡 监控 Java
微服务稳定性三板斧:熔断、限流与负载均衡全面解析(附 Hystrix-Go 实战代码)
在微服务架构中,高可用与稳定性至关重要。本文详解熔断、限流与负载均衡三大关键技术,结合API网关与Hystrix-Go实战,帮助构建健壮、弹性的微服务系统。
791 1
微服务稳定性三板斧:熔断、限流与负载均衡全面解析(附 Hystrix-Go 实战代码)
|
7月前
|
安全 Java API
Java SE 与 Java EE 区别解析及应用场景对比
在Java编程世界中,Java SE(Java Standard Edition)和Java EE(Java Enterprise Edition)是两个重要的平台版本,它们各自有着独特的定位和应用场景。理解它们之间的差异,对于开发者选择合适的技术栈进行项目开发至关重要。
1168 1