前言
本文将介绍Spring Boot的使用。
Spring Boot
是由Pivotal团队提供的全新框架,其设计目的是用来简化新Spring
应用的初始搭建以及开发过程。简而言之,使用Spring Boot
,将极大的提高开发效率。
阅读本文需要熟悉以下技术:
Spring
(必须)MySQL
(必须)Maven
(必须)Hibernate
(非必须)
接下来,将使用IntelliJ IDEA创建一个Spring Boot
工程,实现简单的增删改查功能,该工程包含以下几点内容:
项目属性配置
Controller的使用
数据库操作
事务管理
AOP
统一异常处理
单元测试
创建工程
IntelliJ IDEA
新建一个工程,选择如下图所示,点击Next
(IDEA
企业版才有此选项):
new project.png
Next
后,输入项目信息之后,进入如下界面,选择如图选项后,一直Next
到最后Finish
:
Web.png
Finish
后,需要等待Maven
下载相关依赖,此时可见如下目录结构:
目录.png
删除其中选中的5项无用的文件,并将目录中的application.properties
重命名为application.yml
,此时,application.yml
即为整个工程的配置文件,并且只有这一个。
因为Spring Boot
内置了Tomcat
,所以无需额外进行配置,直接按快捷键Shift + F10
即可运行此项目,在浏览器输入http://localhost:8080/
,跳转后看见如下错误页面,则说明该工程已运行成功:
error page.png
依赖库
本项目中要使用到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>
配置
在实际的项目开发中,通常会有开发环境,测试环境,生产环境等,Spring Boot
针对这点,也有很好的支持:
1.在resources
文件夹下新建两个文件:application-dev.yml
和application-pro.yml
,前者表示开发环境,后者表示生产环境。
2.在application-dev.yml
和application-pro.yml
中就可写与各自环境相关的配置,如端口号,数据库等等。application-dev.yml
配置如下(注意格式):
server: port: 8081 //端口为8081spring: datasource: driver-class-name: com.mysql.jdbc.Driver //使用MySQL url: jdbc:mysql://localhost:3306/book_dev?useSSL=false //使用开发用数据库book_dev username: root password: 123456 jpa: hibernate: ddl-auto: create-drop //自动生成数据表的方式 show-sql: true
application-pro.yml
配置如下(注意格式):
server: port: 8082 //端口为8081spring: datasource: driver-class-name: com.mysql.jdbc.Driver //使用MySQL url: jdbc:mysql://localhost:3306/book?useSSL=false //使用开发用数据库book_dev username: root password: 123456 jpa: hibernate: ddl-auto: update //自动生成数据表的方式 show-sql: true
3.此时需要在原来的application.yml
文件中,指定工程启动时,运行哪个环境的配置文件,代码如下(注意格式):
spring: profiles: active: dev //表示加载application-dev.yml中的配置//这里还可以写所有环境通用的配置
正式开始
接下来将实现一个功能:提供一个可用的POST
请求路径,输入参数name
,book
,提交成功后加入数据库中。
1.创建实体类User
:
import javax.persistence.*;import javax.validation.constraints.NotNull;import java.io.Serializable;@Entity //表示该类是一个实体类,由于之前的配置中写了 ddl-auto,jpa会将类名作为表名自动生成表@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.创建数据库操作接口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.创建service
接口UserService
:
import com.zyr.book.domain.User;public interface UserService { User insertUser(String name, String book); }
4.实现接口UserService
,完成业务逻辑:
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 //事物管理注解,当数据库操作失败后,自动回滚@Service("userService")public class UserServiceImpl implements UserService { private UserRepository userRepository; @Autowired //使用构造器注入方式 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)); } }
此处的错误处理均使用抛异常的方式,方便之后的统一异常处理。
其中UserException
如下:
import com.zyr.book.enums.ApiErrorType;public class UserException extends RuntimeException { //必须继承RuntimeException,在统一异常处理时才能被捕获 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("服务器异常"), 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;@RestControllerpublic class UserController { private UserService userService; @Autowired //构造器注入方式 public UserController(UserService userService) { this.userService = userService; } @PostMapping("/add") public User add(User user) throws Exception { //注意这里,方法抛出异常 return userService.insertUser(user.getName(), user.getBook()); } }
类上的
@RestController
,表示该类中的方法返回值,会自动转换成JSON
格式;方法上的
@PostMapping("/add")
,表示此方法仅支持POST
方式,相应的还有@GetMapping
、@PutMapping
、@DeleteMapping
等等RESTful API
的请求方式,括号里的add
表示请求路径,此处的请求路径即为:http://localhost:8081/add
;方法上直接抛出异常,方便之后的统一异常处理;
测试
以上5步,已经完成了基本的业务代码,现在有3中方式进行测试刚才所写的功能是否有效:
单元测试
IDEA
自带的测试工具编写客户端调用该
API
此处介绍前两种测试方式:
1.单元测试
鼠标选中UserServiceImpl
中的insertUser
方法,右键—>Go To—>Test—>Create New Test...,IDEA
自动创建测试类,在其中加入测试用例如下:
@RunWith(SpringRunner.class)@SpringBootTestpublic class UserServiceImplTest { @Autowired private UserService userService; @Test public void insertUser() throws Exception { User user = userService.insertUser("Bob", "书"); assertEquals("Bob", user.getName()); assertEquals("书", user.getBook()); } }
同样的方式创建UserController
类中add
方法的测试用例:
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; //模拟网络请求 @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\"}")) ; } }
创建完成之后,分别运行测试用例即可。
2.IDEA
自带的测试工具
打开IDEA
上面的 Tools—>Test RESTful Web Service,打开之后如下图:
REST Client.png
其中HTTP method
选择POST
,Host/port
输入http://localhost:8081
,Path
输入add
,切换到Request
在Request Parameters
中输入参数,如下图:
POST.png
输入完成之后,点击左侧第一个绿色的三角形按钮,即可完成一次POST
模拟请求,请求成功后以JSON
返回输入的参数:
{"id":1,"name":"Bob","book":"书"}
此时若是再此执行相同一次上面的操作,则会返回如下错误信息:
{ "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"}
返回这种错误信息的原因,就是前面在UserController
里面的add
方法直接抛出了异常。乍一看好像没问题,状态码是500
说明服务器有问题,真实原因确实也是因为无法插入重复的数据出的错,但仔细一想,这样的错误信息,会让客户端产生误解,以为是服务器现在有问题而暂时无法使用,只能傻傻的等待,但其实是客户端的参数不对造成的误解,所以此处的状态码应该返回以4
开头的4XX
,来说明是客户端请求有误,并且要让客户端能一目了然的知道是什么地方出了错,并且这样做,也符合RESTful API
的风格,由此,便引入了Spring Boot
中的统一异常处理。
统一异常处理
1.新建类Error
,保存错误信息:
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;@ControllerAdvicepublic class GlobalExceptionHandler { private final static Logger logger = LoggerFactory.getLogger(GlobalExceptionHandler.class); @ResponseBody //此注解表示:以JSON形式返回数据 @ExceptionHandler public ResponseEntity<Error> handle(Exception e) { //方法名可随意 if (e instanceof UserException) { //捕获service中主动抛出的异常,并且获取其中的错误信息以返回给客户端 return new ResponseEntity<>(new Error(((UserException) e).getApiErrorType().getMessage()), HttpStatus.BAD_REQUEST); } else if (e instanceof DataIntegrityViolationException) { //捕获数据库操作异常,此处为违反唯一性约束 return new ResponseEntity<>(new Error(ApiErrorType.DUPLICATED_BOOK.getMessage()), HttpStatus.FORBIDDEN); } else if (e instanceof HttpRequestMethodNotSupportedException) { //捕获请求方法异常 return new ResponseEntity<>(new Error(e.getLocalizedMessage()), HttpStatus.METHOD_NOT_ALLOWED); } else { logger.error("【系统异常】", e); return new ResponseEntity<>(new Error(ApiErrorType.UNKNOWN_ERROR.getMessage()), HttpStatus.INTERNAL_SERVER_ERROR); } } }
可在GlobalExceptionHandler
方法handle
中的if...else if
中加入想要捕获的异常,自定义其返回信息与状态码。最后的else
中,打印出了未捕获的异常,以方便Fix bug。
以上完成之后,重启项目,在REST Client
中重新提交两次相同的POST
请求后,第二次即返回错误信息{"message":"该用户已有此书"}
。如果用Jquery ajax
进行访问,则需要在ajax
的回调函数error
中写自己的失败响应逻辑。
AOP
AOP
即Aspect Oriented Programming
,翻译为面向切面编程
,可以简单的理解为:在一些方法的执行过程中,统一指定一个地方并做一些额外的事。比如方法A
,B
,C
,可以在它们开始执行前、执行后、返回之后等等时候做一些统一的处理。接下来,以打印请求信息为例说明。
新建类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 //此注解说明该类是一个切面类@Componentpublic class HttpAspect { private static final Logger logger = LoggerFactory.getLogger(HttpAspect.class); @Pointcut("execution(public * com.zyr.book.controller..*.*(..))") //切入点,execution内的内容即为想要切入的地方,类似于正则匹配,此处切入的地方是com.zyr.book.controller包内的所有public方法 public void log() { } @Before("log()") //直接调用上面定义的切入点log,表示在com.zyr.book.controller包内的所有public方法执行前,先执行此方法内的内容,即打印请求信息 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()") //直接调用上面定义的切入点log,表示在com.zyr.book.controller包内的所有public方法执行完成并返回后,执行此方法的内容,即打印方法的返回值 public void doAfterReturning(Object object) { if (object != null) { logger.info("response={}", object.toString()); } } }
以上完成之后,重启项目,在REST Client
中提交一次POST
请求,即可在控制台看见打印的信息:
log.png
跨域问题
如果使用Jquery ajax
进行访问,可能会有跨域问题,如果有,需要在新建工程时,自动生成的XXApplication
,即有一个main
方法的那个类(本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 简易使用指南
欢迎Start,Follow!