一、 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 |
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", "[email protected]"))
//版本号
.version("1.0")
//描述
.description("")
.build();
}
}
swagger通路位址:
: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);