Spring Boot 簡(jiǎn)易使用指南

文/ZYRzyr
原文鏈接:http://m.itdecent.cn/p/d8fdd6efe2cb

[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)):

new project.png

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

Web.png

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

目錄.png

? 刪除其中選中的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)行成功:
error page.png


依賴庫

? 本項(xiàng)目中要使用到JPAMySQL、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.ymlapplication-pro.yml,前者表示開發(fā)環(huán)境,后者表示生產(chǎn)環(huán)境。

2.application-dev.ymlapplication-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,打開之后如下圖:

REST Client.png

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

POST.png

輸入完成之后,點(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

? AOPAspect 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)看見打印的信息:

log.png


跨域問題

? 如果使用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

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容