一、 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用法,下面看看事务相关的处理。
事务处理
我们经常希望某些操作要么同时成功,要么同时失败,以达到一致性。
比如转账这个操作,假设从账号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);