前后端分离实践系列文章总目录
目录
一、统一的JSON数据返回格式
1、JSON响应结构预览
2、JSON响应结构与Java类的映射
3、添加Springboot-web模块的Maven依赖
4、新建一个Controller类编写JSON响应结构的测试方法
5、测试JSON响应结构
二、集成Swagger2进行在线接口文档维护
1、添加Swagger2的Maven依赖
2、在Springboot中启用Swagger2
3、在config包中添加Swagger2的配置类SwaggerConfig
4、测试Swagger的使用
三、使用AOP进行全局的异常处理
1、在config包中添加一个异常切面配置类ExceptionAspectConfig
2、在ApiController中添加测试异常处理的方法
3、进入swagger-ui页面进行/api/exception接口的测试
四、使用hibernate-validator进行请求参数验证
1、config包中添加一个验证配置类ValidatorConfig
2、dto包中添加一个用户请求参数类UserRequestDTO
3、在ApiController中添加一个测试方法
4、进入swagger-ui页面进行/api/addUser接口的测试
五、使用Token提供安全验证机制
1、新建一个Token异常处理类继承至RuntimeException
2、在全局异常配置类中添加TokenException的配置
3、在annotation包中新建一个Token验证注解UserTokenCheck
4、添加Springboot-AOP的Maven依赖
5、在config包中添加安全检查切面配置类验证用户Token信息
6、在ApiController中添加一个测试方法
7、进入swagger-ui页面进行/api/token接口的测试
六、使用CORS技术解决跨域问题
七、最终的项目结构预览
八、源码地址
使用REST风格实现前后端分离架构,首先需要确定返回的JSON响应结构是统一的,也就是说每个REST请求返回的是相同结构的JSON数据。该JSON响应结构如下:
{
"code":"000",
"msg":"OK",
"data":{}
}
为了在架构中映射以上JSON响应结构,我们可以编写一个ApiResponse类与之对应,如下:
/**
* 统一的JSON格式数据响应类
*/
public class ApiResponse {
private Integer code;
private String msg;
private T data;
public ApiResponse(Integer code, String msg, T data) {
this.code = code;
this.msg = msg;
this.data = data;
}
//getter setter ...
}
org.springframework.boot
spring-boot-starter-web
在controller包中新建一个ApiController类,内容如下:
import com.mengfei.fbsepjava.pojo.ApiResponse;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.RestController;
@RestController
@RequestMapping("/api")
public class ApiController {
@RequestMapping(value = "/hello",method = RequestMethod.GET)
public ApiResponse helloWorld(){
return new ApiResponse(9999,"OK","Hello World");
}
}
@RestController注解代表整个类中所有方法都会返回JSON格式的数据,相当于在每个方法上加上@ResponseBody注解
启动Springboot项目,直接在浏览器中访问http://localhost:8080/api/hello,访问结果如下图所示:
io.springfox
springfox-swagger2
2.6.1
io.springfox
springfox-swagger-ui
2.6.1
在Springboot启动器的main方法中添加开启Swagger2的注解@EnableSwagger2
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import springfox.documentation.swagger2.annotations.EnableSwagger2;
@SpringBootApplication
@EnableSwagger2
public class FbsepJavaApplication {
public static void main(String[] args) {
SpringApplication.run(FbsepJavaApplication.class, args);
}
}
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import springfox.documentation.builders.ApiInfoBuilder;
import springfox.documentation.builders.ParameterBuilder;
import springfox.documentation.builders.PathSelectors;
import springfox.documentation.builders.RequestHandlerSelectors;
import springfox.documentation.schema.ModelRef;
import springfox.documentation.service.ApiInfo;
import springfox.documentation.service.Parameter;
import springfox.documentation.spi.DocumentationType;
import springfox.documentation.spring.web.plugins.Docket;
import java.util.ArrayList;
import java.util.List;
@Configuration
public class SwaggerConfig {
@Bean
public Docket createRestApi() {
return new Docket(DocumentationType.SWAGGER_2)
.apiInfo(apiInfo())
.select()
//扫描的swagger接口包路径
.apis(RequestHandlerSelectors.basePackage("com.mengfei.fbsepjava.controller"))
.paths(PathSelectors.any())
.build()
.globalOperationParameters(this.setParameter());//不需要添加全局参数时这一行可以删掉
}
//通过参数构造器为swagger添加对header参数的支持,如果不需要的话可以删掉
private List setParameter(){
ParameterBuilder ticketPar = new ParameterBuilder();
List pars = new ArrayList();
ticketPar.name("userToken").description("用户的Token信息")
.modelRef(new ModelRef("string")).parameterType("header")
.required(false) //header中的userToken参数现在设置的是非必填,传空也可以
.build();
pars.add(ticketPar.build());
return pars;
}
private ApiInfo apiInfo() {
return new ApiInfoBuilder()
.title("Springboot利用swagger构建api文档")
.description("简单优雅的restful风格 http://blog.csdn.net/")
.termsOfServiceUrl("http://blog.csdn.net/")
.version("1.0")
.build();
}
}
启动项目访问http://localhost:8080/swagger-ui.html,看到如下所示的页面:
更多关于Swagger的使用可以参考:Swagger的使用相关系列文档
import com.mengfei.fbsepjava.pojo.ApiResponse;
import org.jboss.logging.Logger;
import org.springframework.http.HttpStatus;
import org.springframework.http.converter.HttpMessageNotReadableException;
import org.springframework.validation.ObjectError;
import org.springframework.web.HttpMediaTypeNotSupportedException;
import org.springframework.web.HttpRequestMethodNotSupportedException;
import org.springframework.web.bind.MethodArgumentNotValidException;
import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.ResponseBody;
import org.springframework.web.bind.annotation.ResponseStatus;
import java.util.List;
import java.util.stream.Collectors;
/**
* Title: 全局异常处理切面
* Description: 利用 @ControllerAdvice + @ExceptionHandler 组合处理Controller层RuntimeException异常
* 在运行时从上往下依次调用每个异常处理方法,匹配当前异常类型是否与@ExceptionHandler注解所定义的异常相匹配,
* 若匹配,则执行该方法,同时忽略后续所有的异常处理方法,最终会返回经JSON序列化后的Response对象
*/
@ControllerAdvice
@ResponseBody
public class ExceptionAspectConfig {
/** Log4j日志处理 */
private static final Logger log = Logger.getLogger(ExceptionAspectConfig.class);
/**
* 400 - Bad Request
*/
@ResponseStatus(HttpStatus.BAD_REQUEST)
@ExceptionHandler(HttpMessageNotReadableException.class)
public ApiResponse handleHttpMessageNotReadableException(
HttpMessageNotReadableException e) {
log.error("could_not_read_json...", e);
return new ApiResponse<>(HttpStatus.BAD_REQUEST.value(),"could_not_read_json...",null);
}
/**
* 400 - Bad Request
*/
@ResponseStatus(HttpStatus.BAD_REQUEST)
@ExceptionHandler(MethodArgumentNotValidException.class)
public ApiResponse handleValidationException(MethodArgumentNotValidException e) {
// 获取BindingResult对象,然后获取其中的错误信息
// 如果开启了fail_fast,这里只会有一个信息
// 如果没有,则可能会有多个验证提示信息
List errorInformation = e.getBindingResult().getAllErrors()
.stream()
.map(ObjectError::getDefaultMessage)
.collect(Collectors.toList());
log.error(errorInformation.toString(), e);
return new ApiResponse<>(HttpStatus.BAD_REQUEST.value(),errorInformation.toString(),null);
}
/**
* 405 - Method Not Allowed。HttpRequestMethodNotSupportedException
* 是ServletException的子类,需要Servlet API支持
*/
@ResponseStatus(HttpStatus.METHOD_NOT_ALLOWED)
@ExceptionHandler(HttpRequestMethodNotSupportedException.class)
public ApiResponse handleHttpRequestMethodNotSupportedException(
HttpRequestMethodNotSupportedException e) {
log.error("request_method_not_supported...", e);
return new ApiResponse<>(HttpStatus.METHOD_NOT_ALLOWED.value(),"request_method_not_supported",null);
}
/**
* 415 - Unsupported Media Type。HttpMediaTypeNotSupportedException
* 是ServletException的子类,需要Servlet API支持
*/
@ResponseStatus(HttpStatus.UNSUPPORTED_MEDIA_TYPE)
@ExceptionHandler(HttpMediaTypeNotSupportedException.class)
public ApiResponse handleHttpMediaTypeNotSupportedException(Exception e) {
log.error("content_type_not_supported...", e);
return new ApiResponse<>(HttpStatus.UNSUPPORTED_MEDIA_TYPE.value(),"content_type_not_supported...",null);
}
/**
* 500 - Internal Server Error
*/
@ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR)
@ExceptionHandler(Exception.class)
public ApiResponse handleException(Exception e) {
log.error("Internal Server Error...", e);
return new ApiResponse<>(HttpStatus.INTERNAL_SERVER_ERROR.value(),"Internal Server Error...",null);
}
}
//测试全局的异常处理
@RequestMapping(value = "/exception",method = RequestMethod.GET)
public ApiResponse testExceptionHandle(){
int i = 10/0;
return new ApiResponse(9999,"OK",null);
}
import org.hibernate.validator.HibernateValidator;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import javax.validation.Validation;
import javax.validation.Validator;
import javax.validation.ValidatorFactory;
@Configuration
public class ValidatorConfig {
@Bean
public Validator validator() {
ValidatorFactory factory = Validation.byProvider(HibernateValidator.class)
.configure()
// 将fail_fast设置为true即可,如果想验证全部字段,则设置为false或者取消配置即可
.addProperty("hibernate.validator.fail_fast", "true")
.buildValidatorFactory();
return factory.getValidator();
}
}
import org.hibernate.validator.constraints.Range;
import javax.validation.constraints.*;
public class UserRequestDTO {
@NotBlank(message = "用户名不能为空")
private String username;
@NotNull
@Size(min = 5,max = 10,message = "密码必须在5-10位之间")
private String pwd;
@Range(min = 0,max = 120,message = "年龄不能为负数并且不能超过120岁")
private Integer age;
@Pattern(regexp = "^([0-9A-Za-z\\-_\\.]+)@([0-9a-z]+\\.[a-z]{2,3}(\\.[a-z]{2})?)$",message = "需要输入正确格式的邮箱")
private String email;
//getter setter toString() ...
}
//测试hibernate-validator验证
@RequestMapping(value = "/addUser",method = RequestMethod.POST)
public ApiResponse addUser(@RequestBody @Valid UserRequestDTO requestDTO){
System.out.println(requestDTO);
return new ApiResponse<>(9999,"添加用户成功",requestDTO);
}
验证不通过时抛出的异常是MethodArgumentNotValidException,可在异常处理切面配置类中查看。
更多关于hibernate-validator验证的内容请参考:在SpringBoot中使用Hibernate Validate
/**
* 自定义TokenException类
*/
public class TokenException extends RuntimeException {
public TokenException(){super();}
public TokenException(String message){super(message);}
}
注意:不要添加到最后面去了,异常是从上到下依次匹配的,最后一个是Exception
/**
* 1000 Token验证未通过
*/
@ExceptionHandler(TokenException.class)
public ApiResponse handleTokenException(TokenException e) {
log.error("Token验证未通过", e);
return new ApiResponse<>(1000,"Token验证未通过",null);
}
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface UserTokenCheck {
}
org.springframework.boot
spring-boot-starter-aop
import com.mengfei.fbsepjava.annotation.UserTokenCheck;
import com.mengfei.fbsepjava.exception.TokenException;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.reflect.MethodSignature;
import org.springframework.stereotype.Component;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;
import javax.servlet.http.HttpServletRequest;
import java.lang.reflect.Method;
@Component
@Aspect
public class SecurityAspectConfig {
//伪Token,暂存于内存,通常的解决方案是在用户登录时将Token信息存储在Redis中
private static String USER_TOKEN = "123456789";
@Around("@annotation(org.springframework.web.bind.annotation.RequestMapping)")
public Object execute(ProceedingJoinPoint pjp) throws Throwable {
//获取请求信息
ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
HttpServletRequest request = attributes.getRequest();
//从切入点上获取目标方法
MethodSignature methodSignature = (MethodSignature) pjp.getSignature();
Method method = methodSignature.getMethod();
//若目标方法需要安全性检查,则进行Token验证,如果不进行此判断可以对所有HTTP请求进行安全性检查
if (method.isAnnotationPresent(UserTokenCheck.class)) {
// 从 request header 中获取当前 token
String token = request.getHeader("userToken");
// 检查 token 有效性
if (!USER_TOKEN.equals(token)) {
String message = String.format("用户Token验证不通过", token);
throw new TokenException(message);
}
}
// 调用目标方法
return pjp.proceed();
}
}
//测试Token验证功能
@UserTokenCheck //在需要验证token的方法上添加此注解
@RequestMapping(value = "/token",method = RequestMethod.GET)
public ApiResponse checkToken(){
return new ApiResponse(9999,"OK",null);
}
Spring框架已经实现了CORS技术,我们只需要在Controller中添加对应的实现注解表明接口类允许跨域请求即可,也可添加在方法上,如下:
@CrossOrigin
@RestController
@RequestMapping("/api")
public class ApiController {
//……
}
更多关于@CrossOrigin注解的内容请参考:注解@CrossOrigin解决跨域问题
https://github.com/Alexshi5/demo-fbsep/tree/master/fbsep-java
参考链接:
1、REST风格框架实战:从MVC到前后端分离(附完整Demo)