[Deprecated]本文篇幅過長(zhǎng),且有不全之處,作者將其拆分并重新編寫,請(qǐng)移步新系列:Spring Boot實(shí)際應(yīng)用講解(一):Hello World
前言
? 本文將介紹Spring Boot的使用。
? Spring Boot是由Pivotal團(tuán)隊(duì)提供的全新框架,其設(shè)計(jì)目的是用來簡(jiǎn)化新Spring應(yīng)用的初始搭建以及開發(fā)過程。簡(jiǎn)而言之,使用Spring Boot,將極大的提高開發(fā)效率。
閱讀本文需要熟悉以下技術(shù):
-
Spring(必須) -
MySQL(必須) -
Maven(必須) -
Hibernate(非必須)
? 接下來,將使用IntelliJ IDEA創(chuàng)建一個(gè)Spring Boot工程,實(shí)現(xiàn)簡(jiǎn)單的增刪改查功能,該工程包含以下幾點(diǎn)內(nèi)容:
- 項(xiàng)目屬性配置
- Controller的使用
- 數(shù)據(jù)庫操作
- 事務(wù)管理
- AOP
- 統(tǒng)一異常處理
- 單元測(cè)試
創(chuàng)建工程
IntelliJ IDEA新建一個(gè)工程,選擇如下圖所示,點(diǎn)擊Next(IDEA企業(yè)版才有此選項(xiàng)):

Next后,輸入項(xiàng)目信息之后,進(jìn)入如下界面,選擇如圖選項(xiàng)后,一直Next到最后Finish:

Finish后,需要等待Maven下載相關(guān)依賴,此時(shí)可見如下目錄結(jié)構(gòu):

? 刪除其中選中的5項(xiàng)無用的文件,并將目錄中的
application.properties重命名為application.yml,此時(shí),application.yml即為整個(gè)工程的配置文件,并且只有這一個(gè)。? 因?yàn)?code>Spring Boot內(nèi)置了
Tomcat,所以無需額外進(jìn)行配置,直接按快捷鍵Shift + F10即可運(yùn)行此項(xiàng)目,在瀏覽器輸入http://localhost:8080/,跳轉(zhuǎn)后看見如下錯(cuò)誤頁面,則說明該工程已運(yùn)行成功:
依賴庫
? 本項(xiàng)目中要使用到JPA、MySQL、AOP,所以要在pom.xml中添加如下依賴:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-aop</artifactId>
</dependency>
配置
? 在實(shí)際的項(xiàng)目開發(fā)中,通常會(huì)有開發(fā)環(huán)境,測(cè)試環(huán)境,生產(chǎn)環(huán)境等,Spring Boot針對(duì)這點(diǎn),也有很好的支持:
1.在resources文件夾下新建兩個(gè)文件:application-dev.yml和application-pro.yml,前者表示開發(fā)環(huán)境,后者表示生產(chǎn)環(huán)境。
2.在application-dev.yml和application-pro.yml中就可寫與各自環(huán)境相關(guān)的配置,如端口號(hào),數(shù)據(jù)庫等等。
application-dev.yml配置如下(注意格式):
server:
port: 8081 //端口為8081
spring:
datasource:
driver-class-name: com.mysql.jdbc.Driver //使用MySQL
url: jdbc:mysql://localhost:3306/book_dev?useSSL=false //使用開發(fā)用數(shù)據(jù)庫book_dev
username: root
password: 123456
jpa:
hibernate:
ddl-auto: create-drop //自動(dòng)生成數(shù)據(jù)表的方式
show-sql: true
application-pro.yml配置如下(注意格式):
server:
port: 8082 //端口為8081
spring:
datasource:
driver-class-name: com.mysql.jdbc.Driver //使用MySQL
url: jdbc:mysql://localhost:3306/book?useSSL=false //使用開發(fā)用數(shù)據(jù)庫book_dev
username: root
password: 123456
jpa:
hibernate:
ddl-auto: update //自動(dòng)生成數(shù)據(jù)表的方式
show-sql: true
3.此時(shí)需要在原來的application.yml文件中,指定工程啟動(dòng)時(shí),運(yùn)行哪個(gè)環(huán)境的配置文件,代碼如下(注意格式):
spring:
profiles:
active: dev //表示加載application-dev.yml中的配置
//這里還可以寫所有環(huán)境通用的配置
正式開始
? 接下來將實(shí)現(xiàn)一個(gè)功能:提供一個(gè)可用的POST請(qǐng)求路徑,輸入?yún)?shù)name,book,提交成功后加入數(shù)據(jù)庫中。
1.創(chuàng)建實(shí)體類User:
import javax.persistence.*;
import javax.validation.constraints.NotNull;
import java.io.Serializable;
@Entity //表示該類是一個(gè)實(shí)體類,由于之前的配置中寫了 ddl-auto,jpa會(huì)將類名作為表名自動(dòng)生成表
@Table(uniqueConstraints = {@UniqueConstraint(columnNames = {"name", "book"})})//表示同一行name和book作為唯一約束
public class User implements Serializable {
@Id //主鍵約束
@GeneratedValue //自增
private Integer id;
@NotNull
private String name;
@NotNull
private String book;
public User() {
}
public User(Integer id) {
this.id = id;
}
public User(String name, String book) {
this.name = name;
this.book = book;
}
public Integer getId() {
return id;
}
public void setId(Integer id) {
this.id = id;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public String getBook() {
return book;
}
public void setBook(String book) {
this.book = book;
}
@Override
public String toString() {
return "User{" +
"id=" + id +
", name='" + name + '\'' +
", book='" + book + '\'' +
'}';
}
}
2.創(chuàng)建數(shù)據(jù)庫操作接口UserRepository:
import com.zyr.book.domain.User;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.stereotype.Repository;
import java.util.List;
@Repository
public interface UserRepository extends JpaRepository<User, Integer> { //繼承JpaRepository后,可直接使用其已有的方法,類似hibernate,且可在本類中自定義自己的操作。
List<User> findByName(String name);
}
3.創(chuàng)建service接口UserService:
import com.zyr.book.domain.User;
public interface UserService {
User insertUser(String name, String book);
}
4.實(shí)現(xiàn)接口UserService,完成業(yè)務(wù)邏輯:
import com.zyr.book.domain.User;
import com.zyr.book.enums.ApiErrorType;
import com.zyr.book.exception.UserException;
import com.zyr.book.repository.UserRepository;
import com.zyr.book.service.UserService;
import com.zyr.book.util.TextUtil;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
@Transactional //事物管理注解,當(dāng)數(shù)據(jù)庫操作失敗后,自動(dòng)回滾
@Service("userService")
public class UserServiceImpl implements UserService {
private UserRepository userRepository;
@Autowired //使用構(gòu)造器注入方式
public UserServiceImpl(UserRepository userRepository) {
this.userRepository = userRepository;
}
@Override
public User insertUser(String name, String book) {
if (TextUtil.isEmpty(name)) {
throw new UserException(ApiErrorType.NULL_NAME); //名字為空,拋出異常
}
if (TextUtil.isEmpty(book)) {
throw new UserException(ApiErrorType.NULL_BOOK); //書籍為空,拋出異常
}
return userRepository.save(new User(name, book));
}
}
此處的錯(cuò)誤處理均使用拋異常的方式,方便之后的統(tǒng)一異常處理。
其中UserException如下:
import com.zyr.book.enums.ApiErrorType;
public class UserException extends RuntimeException { //必須繼承RuntimeException,在統(tǒng)一異常處理時(shí)才能被捕獲
private ApiErrorType apiErrorType;
public UserException(ApiErrorType apiErrorType) {
super(apiErrorType.getMessage());
this.apiErrorType = apiErrorType;
}
public ApiErrorType getApiErrorType() {
return apiErrorType;
}
public void setApiErrorType(ApiErrorType apiErrorType) {
this.apiErrorType = apiErrorType;
}
}
其中ApiErrorType如下:
public enum ApiErrorType {
UNKNOWN_ERROR("服務(wù)器異常"),
EMPTY_BOOK("該用戶沒有書籍"),
EMPTY_USER("無此用戶"),
NULL_NAME("姓名不能為空"),
NULL_BOOK("書籍不能為空"),
DUPLICATED_BOOK("該用戶已有此書");
private String message;
ApiErrorType(String message) {
this.message = message;
}
public String getMessage() {
return message;
}
}
5.定義Controller:
import com.zyr.book.domain.User;
import com.zyr.book.service.UserService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RestController;
@RestController
public class UserController {
private UserService userService;
@Autowired //構(gòu)造器注入方式
public UserController(UserService userService) {
this.userService = userService;
}
@PostMapping("/add")
public User add(User user) throws Exception { //注意這里,方法拋出異常
return userService.insertUser(user.getName(), user.getBook());
}
}
- 類上的
@RestController,表示該類中的方法返回值,會(huì)自動(dòng)轉(zhuǎn)換成JSON格式; - 方法上的
@PostMapping("/add"),表示此方法僅支持POST方式,相應(yīng)的還有@GetMapping、@PutMapping、@DeleteMapping等等RESTful API的請(qǐng)求方式,括號(hào)里的add表示請(qǐng)求路徑,此處的請(qǐng)求路徑即為:http://localhost:8081/add; - 方法上直接拋出異常,方便之后的統(tǒng)一異常處理;
測(cè)試
? 以上5步,已經(jīng)完成了基本的業(yè)務(wù)代碼,現(xiàn)在有3中方式進(jìn)行測(cè)試剛才所寫的功能是否有效:
- 單元測(cè)試
-
IDEA自帶的測(cè)試工具 - 編寫客戶端調(diào)用該
API
此處介紹前兩種測(cè)試方式:
1.單元測(cè)試
? 鼠標(biāo)選中UserServiceImpl中的insertUser方法,右鍵—>Go To—>Test—>Create New Test...,IDEA自動(dòng)創(chuàng)建測(cè)試類,在其中加入測(cè)試用例如下:
@RunWith(SpringRunner.class)
@SpringBootTest
public class UserServiceImplTest {
@Autowired
private UserService userService;
@Test
public void insertUser() throws Exception {
User user = userService.insertUser("Bob", "書");
assertEquals("Bob", user.getName());
assertEquals("書", user.getBook());
}
}
? 同樣的方式創(chuàng)建UserController類中add方法的測(cè)試用例:
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.test.context.junit4.SpringRunner;
import org.springframework.test.web.servlet.MockMvc;
import org.springframework.test.web.servlet.request.MockMvcRequestBuilders;
import org.springframework.test.web.servlet.result.MockMvcResultMatchers;
@RunWith(SpringRunner.class)
@SpringBootTest
@AutoConfigureMockMvc
public class UserControllerTest {
@Autowired
private MockMvc mockMvc; //模擬網(wǎng)絡(luò)請(qǐng)求
@Test
public void testAdd() throws Exception {
mockMvc.perform(MockMvcRequestBuilders.post("/add")
.param("name", "用戶A")
.param("book", "一本書A"))
.andExpect(MockMvcResultMatchers.status().isOk())
.andExpect(MockMvcResultMatchers.content()
.string("{\"id\":1,\"name\":\"用戶A\",\"book\":\"一本書A\"}"))
;
}
}
創(chuàng)建完成之后,分別運(yùn)行測(cè)試用例即可。
2.IDEA自帶的測(cè)試工具
? 打開IDEA上面的 Tools—>Test RESTful Web Service,打開之后如下圖:

其中HTTP method選擇POST,Host/port輸入http://localhost:8081,Path輸入add,切換到Request在Request Parameters中輸入?yún)?shù),如下圖:

輸入完成之后,點(diǎn)擊左側(cè)第一個(gè)綠色的三角形按鈕,即可完成一次
POST模擬請(qǐng)求,請(qǐng)求成功后以JSON返回輸入的參數(shù):
{"id":1,"name":"Bob","book":"書"}
此時(shí)若是再此執(zhí)行相同一次上面的操作,則會(huì)返回如下錯(cuò)誤信息:
{
"timestamp":1510211138423,
"status":500,
"error":"Internal Server Error",
"exception":"org.springframework.dao.DataIntegrityViolationException",
"message":"could not execute statement; SQL [n/a]; constraint [UKl7798etvxnmv3iq4thmund7us]; nested exception is org.hibernate.exception.ConstraintViolationException: could not execute statement",
"path":"/add"
}
? 返回這種錯(cuò)誤信息的原因,就是前面在UserController里面的add方法直接拋出了異常。乍一看好像沒問題,狀態(tài)碼是500說明服務(wù)器有問題,真實(shí)原因確實(shí)也是因?yàn)闊o法插入重復(fù)的數(shù)據(jù)出的錯(cuò),但仔細(xì)一想,這樣的錯(cuò)誤信息,會(huì)讓客戶端產(chǎn)生誤解,以為是服務(wù)器現(xiàn)在有問題而暫時(shí)無法使用,只能傻傻的等待,但其實(shí)是客戶端的參數(shù)不對(duì)造成的誤解,所以此處的狀態(tài)碼應(yīng)該返回以4開頭的4XX,來說明是客戶端請(qǐng)求有誤,并且要讓客戶端能一目了然的知道是什么地方出了錯(cuò),并且這樣做,也符合RESTful API的風(fēng)格,由此,便引入了Spring Boot中的統(tǒng)一異常處理。
統(tǒng)一異常處理
1.新建類Error,保存錯(cuò)誤信息:
public class Error {
private String message;
public Error() {
}
public Error(String message) {
this.message = message;
}
public String getMessage() {
return message;
}
public void setMessage(String message) {
this.message = message;
}
@Override
public String toString() {
return "Error{" +
"message='" + message + '\'' +
'}';
}
}
2.新建類GlobalExceptionHandler,捕獲所有異常:
import com.zyr.book.domain.Error;
import com.zyr.book.enums.ApiErrorType;
import com.zyr.book.exception.UserException;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.dao.DataIntegrityViolationException;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.HttpRequestMethodNotSupportedException;
import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.ResponseBody;
@ControllerAdvice
public class GlobalExceptionHandler {
private final static Logger logger = LoggerFactory.getLogger(GlobalExceptionHandler.class);
@ResponseBody //此注解表示:以JSON形式返回?cái)?shù)據(jù)
@ExceptionHandler
public ResponseEntity<Error> handle(Exception e) { //方法名可隨意
if (e instanceof UserException) { //捕獲service中主動(dòng)拋出的異常,并且獲取其中的錯(cuò)誤信息以返回給客戶端
return new ResponseEntity<>(new Error(((UserException) e).getApiErrorType().getMessage()),
HttpStatus.BAD_REQUEST);
} else if (e instanceof DataIntegrityViolationException) { //捕獲數(shù)據(jù)庫操作異常,此處為違反唯一性約束
return new ResponseEntity<>(new Error(ApiErrorType.DUPLICATED_BOOK.getMessage()),
HttpStatus.FORBIDDEN);
} else if (e instanceof HttpRequestMethodNotSupportedException) { //捕獲請(qǐng)求方法異常
return new ResponseEntity<>(new Error(e.getLocalizedMessage()),
HttpStatus.METHOD_NOT_ALLOWED);
} else {
logger.error("【系統(tǒng)異?!?, e);
return new ResponseEntity<>(new Error(ApiErrorType.UNKNOWN_ERROR.getMessage()),
HttpStatus.INTERNAL_SERVER_ERROR);
}
}
}
? 可在GlobalExceptionHandler方法handle中的if...else if中加入想要捕獲的異常,自定義其返回信息與狀態(tài)碼。最后的else中,打印出了未捕獲的異常,以方便Fix bug。
? 以上完成之后,重啟項(xiàng)目,在REST Client中重新提交兩次相同的POST請(qǐng)求后,第二次即返回錯(cuò)誤信息{"message":"該用戶已有此書"}。如果用Jquery ajax進(jìn)行訪問,則需要在ajax的回調(diào)函數(shù)error中寫自己的失敗響應(yīng)邏輯。
AOP
? AOP即Aspect Oriented Programming,翻譯為面向切面編程,可以簡(jiǎn)單的理解為:在一些方法的執(zhí)行過程中,統(tǒng)一指定一個(gè)地方并做一些額外的事。比如方法A,B,C,可以在它們開始執(zhí)行前、執(zhí)行后、返回之后等等時(shí)候做一些統(tǒng)一的處理。接下來,以打印請(qǐng)求信息為例說明。
? 新建類HttpAspect:
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.annotation.AfterReturning;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import org.aspectj.lang.annotation.Pointcut;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Component;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;
import javax.servlet.http.HttpServletRequest;
@Aspect //此注解說明該類是一個(gè)切面類
@Component
public class HttpAspect {
private static final Logger logger = LoggerFactory.getLogger(HttpAspect.class);
@Pointcut("execution(public * com.zyr.book.controller..*.*(..))") //切入點(diǎn),execution內(nèi)的內(nèi)容即為想要切入的地方,類似于正則匹配,此處切入的地方是com.zyr.book.controller包內(nèi)的所有public方法
public void log() {
}
@Before("log()") //直接調(diào)用上面定義的切入點(diǎn)log,表示在com.zyr.book.controller包內(nèi)的所有public方法執(zhí)行前,先執(zhí)行此方法內(nèi)的內(nèi)容,即打印請(qǐng)求信息
public void doBefore(JoinPoint joinPoint) {
ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
HttpServletRequest request = attributes.getRequest();
logger.info("-------------------------------Request Log Begin----------------------------------------------");
logger.info("url={}", request.getRequestURL());
logger.info("method={}", request.getMethod());
logger.info("ip={}", request.getRemoteAddr());
logger.info("class_method={}", joinPoint.getSignature().getDeclaringTypeName() + "." + joinPoint.getSignature().getName());
logger.info("args={}", joinPoint.getArgs());
logger.info("-------------------------------Request Log End------------------------------------------------");
}
@AfterReturning(returning = "object", pointcut = "log()") //直接調(diào)用上面定義的切入點(diǎn)log,表示在com.zyr.book.controller包內(nèi)的所有public方法執(zhí)行完成并返回后,執(zhí)行此方法的內(nèi)容,即打印方法的返回值
public void doAfterReturning(Object object) {
if (object != null) {
logger.info("response={}", object.toString());
}
}
}
以上完成之后,重啟項(xiàng)目,在REST Client中提交一次POST請(qǐng)求,即可在控制臺(tái)看見打印的信息:

跨域問題
? 如果使用Jquery ajax進(jìn)行訪問,可能會(huì)有跨域問題,如果有,需要在新建工程時(shí),自動(dòng)生成的XXApplication,即有一個(gè)main方法的那個(gè)類(本Demo的是BookApplication)里加入跨域過濾器:
@Bean
public CorsFilter corsFilter() {
UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
source.registerCorsConfiguration("/**", buildConfig());
return new CorsFilter(source);
}
private CorsConfiguration buildConfig() {
CorsConfiguration corsConfiguration = new CorsConfiguration();
corsConfiguration.addAllowedOrigin("*");
corsConfiguration.addAllowedHeader("*");
corsConfiguration.addAllowedMethod("*");
return corsConfiguration;
}
完整Demo地址
ZYRzyr的GitHub地址 = Spring Boot 簡(jiǎn)易使用指南
歡迎Start,F(xiàn)ollow!
最后
? 本文內(nèi)容基本涵蓋了在實(shí)際的單體式架構(gòu)的項(xiàng)目中開發(fā)所面臨的情況,若文中有誤,歡迎評(píng)論指正。后續(xù)將推出:微服務(wù)架構(gòu)—Spring Cloud簡(jiǎn)易使用指南。有興趣可以繼續(xù)關(guān)注ZYRzyr的簡(jiǎn)書
原文作者/ZYRzyr
原文鏈接:http://m.itdecent.cn/p/d8fdd6efe2cb
請(qǐng)進(jìn)入這里獲取授權(quán):https://101709080007647.bqy.mobi