1 创建logback-boot.xml文件
使用System.out.println()方法打印测试结果的方式太不优雅,所以想使用日志在控制台打印结果,需要在QIQIHAL-SECURITY-DEMO项目中加入logback日志框架的配置文件logback-boot.xml,logback-boot.xml用来配置logback,配置方式一般比较固定,可以参考本配置文件的配置,也可自行搜索增加或减少配置
在resources文件夹下创建logback-boot.xml文件
复制以下logback-boot.xml内容,这样就可以在控制台以log方式打印测试结果了
%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{50} - %msg%n
${LOG_HOME}/${appName}.log
${LOG_HOME}/${appName}-%d{yyyy-MM-dd}-%i.log
365
100MB
%d{yyyy-MM-dd HH:mm:ss.SSS} [ %thread ] - [ %-5level ] [ %logger{50} : %line ] - %msg%n
2 创建Controller类和Controller测试类
在QIQIHAL-SECURITY-DEMO项目中的QIQIHAL-SECURITY-DEMO\src\main\java\com\qiqihal路径下创建controller文件夹,在该文件夹下创建UserController.java和FileController.java用于编写接口
在QIQIHAL-SECURITY-DEMO项目中的QIQIHAL-SECURITY\QIQIHAL-SECURITY-DEMO\src\test\java\com\qiqihal路径下创建controller文件夹,在该文件夹下创建UserControllerTest.java用于编写测试方法
QIQIHAL-SECURITY\QIQIHAL-SECURITY-DEMO\src\main\java\com\qiqihal\controller\UserController.java文件内容如下:
package com.qiqihal.controller;
import com.fasterxml.jackson.annotation.JsonView;
import com.qiqihal.entities.User;
import com.qiqihal.entities.UserConditions;
import io.swagger.annotations.ApiOperation;
import io.swagger.annotations.ApiParam;
import org.apache.commons.lang.builder.ReflectionToStringBuilder;
import org.apache.commons.lang.builder.ToStringStyle;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.data.domain.Pageable;
import org.springframework.data.web.PageableDefault;
import org.springframework.validation.BindingResult;
import org.springframework.validation.FieldError;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.*;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Date;
import java.util.List;
@RequestMapping("/user")
@RestController
public class UserController {
private final static Logger LOGGER = LoggerFactory.getLogger(UserController.class);
private SimpleDateFormat format = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
@GetMapping("/")
/*展示User类中UserSimpleView接口声明的视图*/
@JsonView(User.UserSimpleView.class)
//@ApiOperation用于swagger提供开发者文档,文档中生成的接口的说明。
@ApiOperation(value = "根据条件查询全部用户信息")
public List all(
//使用get方式提交表单请求,多个参数也可以封装到对应的实体中
UserConditions conditions,
/*给Pageable设置默认值*/
@PageableDefault(page = 2,size = 10,sort = "username,asc")//以username升序排序
/*spring-data的自动分页工具,如果不使用spring-data可以忽略此参数*/
Pageable pageable)throws Exception{
/*commons-lang包下的ReflectionToStringBuilder使用反射方式打印实体,ToStringStyle.MULTI_LINE_STYLE以多行展示*/
LOGGER.info("\n" +
"param:"+ReflectionToStringBuilder.toString(conditions,ToStringStyle.MULTI_LINE_STYLE)+"\n"+
"param:"+ReflectionToStringBuilder.toString(pageable,ToStringStyle.MULTI_LINE_STYLE));
List users = new ArrayList<>();
users.add(new User().setId(1L)
.setUsername(conditions.getUsername())
.setPassword(conditions.getPassword())
.setMobile(conditions.getMobile())
.setBirthday(conditions.getBirthday()));
users.add(new User().setId(2L)
.setUsername(conditions.getUsername())
.setPassword(conditions.getPassword())
.setMobile(conditions.getMobile())
.setBirthday(conditions.getBirthday()));
users.add(new User().setId(3L)
.setUsername(conditions.getUsername())
.setPassword(conditions.getPassword())
.setMobile(conditions.getMobile())
.setBirthday(conditions.getBirthday()));
return users;
}
/*请求路径添加正则表达式,\\d+表示路径参数只能接收数字*/
@GetMapping("/{id:\\d+}")
/*展示User类中UserDetailView接口声明的视图*/
@JsonView(User.UserDetailView.class)
@ApiOperation(value = "根据id查询用户信息")
public User get(
//@ApiParam用于swagger提供开发者文档,文档中生成的接口参数的说明。
@ApiParam(value = "用户id")
@PathVariable(name = "id")Long id)throws Exception{
LOGGER.info("param:"+String.valueOf(id));
SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
return new User().setId(1L)
.setUsername("QIQIHAL")
.setPassword("123456")
.setMobile("17343105273")
.setBirthday(new Date().getTime());
}
@PostMapping("/")
@ApiOperation(value = "添加用户信息")
public User add(
@RequestBody
/*UserConditions作为接口的请求参数,增加@Valid注解,
* UserConditions的username属性,使用@NotBlank注解的非空校验就会生效*/
@Validated UserConditions conditions,
/*BindingResult用来保存校验产生的校验报错信息,@Validated和BindingResult bindingResult是配对出现,并且形参顺序是固定的(一前一后)*/
BindingResult errors){
/*如果产生了报错*/
if (errors.hasErrors()){
/*循环打印报错信息*/
errors.getAllErrors().stream().forEach(error->LOGGER.info(error.getDefaultMessage()));
}
LOGGER.info("param:"+ReflectionToStringBuilder.toString(conditions,ToStringStyle.MULTI_LINE_STYLE));
return new User().setId(1L);
}
@PutMapping("/{id:\\d+}")
@ApiOperation(value = "根据id更新用户信息")
public User update(
@ApiParam(value = "用户id")
@PathVariable(value = "id") Long id,
@RequestBody
/*UserConditions作为接口的请求参数,增加@Valid注解,
UserConditions的username属性,使用@Past注解的校验该字段的日期是在过去,就会生效*/
@Validated UserConditions conditions,
/*BindingResult用来保存校验产生的报错结果*/
BindingResult errors)throws Exception{
if (errors.hasErrors()){
errors.getAllErrors().stream().forEach(error->{
FieldError fieldError = (FieldError) error;
/*fieldError.getField()获取校验的报错的属性,error.getDefaultMessage()获取校验报错的信息*/
String message = fieldError.getField() +" "+error.getDefaultMessage();
LOGGER.info(message);
});
}
LOGGER.info("param:"+ReflectionToStringBuilder.toString(conditions,ToStringStyle.MULTI_LINE_STYLE));
return new User().setId(id)
.setUsername(conditions.getUsername())
.setPassword(conditions.getPassword())
.setMobile(conditions.getMobile())
.setBirthday(conditions.getBirthday());
}
}
QIQIHAL-SECURITY\QIQIHAL-SECURITY-DEMO\src\main\java\com\qiqihal\controller\FileController.java文件内容如下:
package com.qiqihal.controller;
import com.qiqihal.entities.FileInfo;
import io.swagger.annotations.ApiOperation;
import org.apache.commons.io.IOUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.multipart.MultipartFile;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.File;
import java.io.FileInputStream;
import java.io.InputStream;
import java.io.OutputStream;
import java.util.Date;
@RestController
@RequestMapping("/file")
public class FileController {
private final static Logger LOGGER = LoggerFactory.getLogger(FileController.class);
@PostMapping
@ApiOperation(value = "文件上传")
public FileInfo upload(MultipartFile file)throws Exception{
LOGGER.info("\n" +
"接收上传文件参数名:" + file.getName() + "\n" +
"上传文件全名:" + file.getOriginalFilename() + "\n" +
"上传文件大小:" + file.getSize());
//获取文件输入流,将文件写入其他对象中
//file.getInputStream();
//文件的地址
String folder = "D://";
//根据文件地址和时间戳文件名为条件新创建的文件
File localFile = new File(folder,new Date().getTime() + ".txt");
//将上传的文件的内容写入新创建的文件中
file.transferTo(localFile);
//将新创建的文件的绝对路径封装到FileInfo实体中返回
return new FileInfo(localFile.getAbsolutePath());
}
@GetMapping("/{id}")
@ApiOperation(value = "文件下载")
public void downLoad(@PathVariable String id, HttpServletRequest request, HttpServletResponse response)throws Exception{
//JDK1.7新语法,对流的操作直接写在try的括号中,这样当流执行完毕后JDK会自动关闭流,无需再finally中关闭流
try (
//读取文件内容到输入流,测试时需要在window下D盘中创建1.txt的文件
InputStream inputStream = new FileInputStream(new File("D://",id + ".txt" ));
//创建输出流
OutputStream outputStream = response.getOutputStream();
){
//Content-Type设置为下载
response.setContentType("application/x-download");
//filename=QIQIHAL.txt指定下载的文件名
response.addHeader("Content-Disposition","attachment;filename=QIQIHAL.txt");
//使用commons-io中的IOUtils.copy方法将输入流中的文件内容复制到输出流中
IOUtils.copy(inputStream,outputStream);
outputStream.flush();
}
}
}
QIQIHAL-SECURITY\QIQIHAL-SECURITY-DEMO\src\test\java\com\qiqihal\controller\SecurityTestController.java文件内容如下:
package com.qiqihal.controller;
import com.qiqihal.QiqihalSecurityDemoApplication;
import com.qiqihal.entities.UserConditions;
import org.codehaus.jackson.map.ObjectMapper;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.http.MediaType;
import org.springframework.mock.web.MockMultipartFile;
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;
import org.springframework.test.web.servlet.setup.MockMvcBuilders;
import org.springframework.web.context.WebApplicationContext;
import java.time.LocalDateTime;
import java.time.ZoneId;
import java.util.Date;
@RunWith(SpringRunner.class)
//使用@SpringBootTest注解一定要指定测试类所在主项目的主启动类
@SpringBootTest(classes = QiqihalSecurityDemoApplication.class)
public class UserControllerTest {
private final static Logger LOGGER = LoggerFactory.getLogger(UserControllerTest.class);
@Autowired
//注入一个web应用环境(容器)
WebApplicationContext wac;
//初始化MockMvc
private MockMvc mockMvc;
//使用@Before注解,在测试方法执行之前,创建一个MockMvc
@Before
public void setup(){
mockMvc = MockMvcBuilders.webAppContextSetup(wac).build();
}
//测试表单请求
@Test
public void testAll() throws Exception{
//mockMvc.perform执行一个请求,MockMvcRequestBuilders.get()构造一个get方式请求
String result = mockMvc.perform(
MockMvcRequestBuilders.get("/user/")
//param("键","值")表单请求,Mock将param添加的参数添加到request的parameter中,即将参数和值追加到请求路径后,baidu.com?username=QIQIHAL,
.param("username","QIQIHAL")
.param("password","123456")
.param("mobile","17343105273")
//虽然参数都添加到request的parameter中,但是SpringBoot依旧能够将参数自动将参数映射到UserConditions实体中
.param("birthday",String.valueOf(new Date().getTime()))
//将参数映射到Pageable,spring-data的自动分页工具,如果不使用spring-data可以忽略此参数
//每页15条数据
.param("size","15")
//第三页
.param("page","3")
//以username字段降序排序
.param("sort","username,desc")
//设置Content-Type
.contentType(MediaType.APPLICATION_FORM_URLENCODED))
//andExpect添加ResultMatcher验证规则,MockMvcResultMatchers.status().isOk()验证返回状态码是200*/
.andExpect(MockMvcResultMatchers.status().isOk())
/*MockMvcResultMatchers.jsonPath("$.length()")验证获取json中数组的长度,$代表json数据
,value(3)验证集合长度为3,具体可以搜索jsonPath语法,使用jsonPath可以像解析xml解析json,
无需遍历json就可以获取任意key和value值*/
.andExpect(MockMvcResultMatchers.jsonPath("$.length()").value(3))
//以String形式返回返回值数据
.andReturn().getResponse().getContentAsString();
LOGGER.info("result:"+result);
}
//测试路径请求
@Test
public void testGet()throws Exception {
String result = mockMvc.perform(
//("url/{path}",参数值)路径请求,Mock将url的参数添加到request中的parameter中,即将参数作为求路径的一部分,baidu.com/QIQIHAL/
//@GetMapping("/user/{id:\\d+}")使用增则表达式,指定了:\\d+,表示只能接受数字
MockMvcRequestBuilders.get("/user/{id}",1)
.contentType(MediaType.APPLICATION_JSON_UTF8))
.andExpect(MockMvcResultMatchers.status().isOk())
/*$.username验证json中的名称为"username"的key,
value("QIQIHAL"))验证名称为username的key的value值为"QIQIHAL"*/
.andExpect(MockMvcResultMatchers.jsonPath("$.username").value("QIQIHAL"))
.andReturn().getResponse().getContentAsString();
LOGGER.info("result:"+result);
}
//测试JSON请求
@Test
public void testAdd()throws Exception{
//约定返回前端的时间统一为时间戳,前端用时间戳进行自定义转换,减少项目不必要的更新部署
UserConditions conditions = new UserConditions();
//将username设置为空字符串,验证@NotBlank注解校验是否执行
conditions.setUsername("")
.setPassword("123456")
.setMobile("17343105273")
.setBirthday(new Date().getTime());
String content = new ObjectMapper().writeValueAsString(conditions);
String result = mockMvc.perform(
//MockMvcRequestBuilders.get()构造一个post方式请求
MockMvcRequestBuilders.post("/user/")
.contentType(MediaType.APPLICATION_JSON_UTF8)
//content(Json字符串)JSON请求,Mock将内容、类型并没有进行解析,直接添加到request的content中,即直接作为二进制数据保存在content中
.content(content))
.andExpect(MockMvcResultMatchers.status().isOk())
.andExpect(MockMvcResultMatchers.jsonPath("$.id").value(1))
.andReturn().getResponse().getContentAsString();
LOGGER.info("result:"+result);
}
//测试JSON请求
@Test
public void testUpdate()throws Exception{
/*将birthday时间增加一年,验证@Past自定义注解校验是否执行
LocalDateTime.now().plusYears(1)将当前时间增加一年,
atZone(ZoneId.systemDefault()使用系统默认时区,
.toInstant().toEpochMilli()将时间转换成毫秒值*/
Date date = new Date(LocalDateTime.now().plusYears(1)
.atZone(ZoneId.systemDefault())
.toInstant()
.toEpochMilli());
UserConditions conditions = new UserConditions();
conditions.setUsername("QIQIHAL")
.setPassword("123456")
//将mobile设置为非手机号码格式,验证@Mobile自定义注解校验是否执行
.setMobile("1734310527")
.setBirthday(date.getTime());
String content = new ObjectMapper().writeValueAsString(conditions);
String result = mockMvc.perform(
//MockMvcRequestBuilders.put()构造一个put方式请求
MockMvcRequestBuilders.put("/user/1")
.contentType(MediaType.APPLICATION_JSON_UTF8)
.content(content))
.andExpect(MockMvcResultMatchers.status().isOk())
.andExpect(MockMvcResultMatchers.jsonPath("$.id").value(1))
.andReturn().getResponse().getContentAsString();
LOGGER.info("result:"+result);
}
//测试文件上传
@Test
public void testUpload()throws Exception{
String result = mockMvc.perform(
//fileUpload方法过时了,所以使用multipart方法上传文件
MockMvcRequestBuilders.multipart("/file")
/*MockMultipartFile构造方法的四个参数第一个参数指定上传文件接口中接收上传文件参数的名字即FileController类中upload方法的参数MutipartFile file
第二个参数指定上传文件的全名(包括文件类型)
第三个参数指定Content-Type媒体格式类型为mutlipart/form-data用于在表单中进行文件上传,具体搜索Content-Type
第四个参数是文件流(此处使用字符串的byte数组模拟)
*/
.file(new MockMultipartFile("file","QIQIHAL.txt","mutlipart/form-data","QIQIHAL".getBytes("UTF-8"))))
.andExpect(MockMvcResultMatchers.status().isOk())
.andReturn().getResponse().getContentAsString();
LOGGER.info("result:"+result);
}
}
QIQIHAL-SECURITY-DEMO\src\main\java\com\qiqihal\entities\UserConditions.java文件内容如下:
package com.qiqihal.entities;
import com.fasterxml.jackson.annotation.JsonIgnore;
import com.qiqihal.validator.Mobile;
import io.swagger.annotations.ApiModelProperty;
import lombok.Data;
import lombok.NoArgsConstructor;
import lombok.experimental.Accessors;
import javax.validation.constraints.NotBlank;
import javax.validation.constraints.Past;
import java.util.Date;
@SuppressWarnings("serial")
@NoArgsConstructor
@Data
@Accessors(chain = true)
public class UserConditions {
//@NotBlank不能为空,检查时会将空格忽略
@NotBlank(message = "用户名不能为空")//添加自定义校验报错信息,保存在接口的BindingResult参数中
//用于swagger提供开发者文档,文档中生成对model属性的说明
@ApiModelProperty(value = "用户名")
private String username;
@ApiModelProperty(value = "密码")
private String password;
//自定义Java Bean Validation注解@Mobile,用于正则校验手机号
@Mobile
@ApiModelProperty(value = "手机号")
private String mobile;
@ApiModelProperty(value = "生日(时间戳)",dataType = "String")
private Long birthday;
//@Past检查该字段的日期是在过去,因为生日只能是过去,不能是未来的
@Past(message = "生日必须是过去的时间")
//@JsonIgnore,Json序列化时将java bean中的一些属性忽略掉,序列化和反序列化都受影响
@JsonIgnore
private Date birthdayDate;
/*因为前端请求birthday请求参数是Long型时间戳,所以不能用Date类型接收,
但是如果将birthday请求参数改为Long,@Past校验无法校验Long型数据报错,
所以用折中的办法,创建Date类型的birthdayDate属性,在birthdayDate增加@Past校验,
并在birthdayDate的get方法上将Long类型birthday转换成Date类型,
这样前端既能传递Long类型时间戳,后端又能使用@Past校验功能
*/
private Date getBirthdayDate() {
return birthdayDate = new Date(birthday);
}
/*Java Bean Validation注解校验
@AssertTrue //用于boolean字段,该字段只能为true
@AssertFalse//该字段的值只能为false
@CreditCardNumber//对信用卡号进行一个大致的验证
@DecimalMax//只能小于或等于该值
@DecimalMin//只能大于或等于该值
@Digits(integer=2,fraction=20)//检查是否是一种数字的整数、分数,小数位数的数字。
@Email//检查是否是一个有效的email地址
@Future//检查该字段的日期是否是属于将来的日期
@Length(min=,max=)//检查所属的字段的长度是否在min和max之间,只能用于字符串
@Max//该字段的值只能小于或等于该值
@Min//该字段的值只能大于或等于该值
@NotNull//不能为null
@NotBlank//不能为空,检查时会将空格忽略
@NotEmpty//不能为空,这里的空是指空字符串
@Null//检查该字段为空
@Past//检查该字段的日期是在过去
@Size(min=, max=)//检查该字段的size是否在min和max之间,可以是字符串、数组、集合、Map等
@URL(protocol=,host,port)//检查是否是一个有效的URL,如果提供了protocol,host等,则该URL还需满足提供的条件
@Valid//该注解只要用于字段为一个包含其他对象的集合或map或数组的字段,或该字段直接为一个其他对象的引用
*/
}
QIQIHAL-SECURITY-DEMO\src\main\java\com\qiqihal\entities\User.java文件内容如下:
package com.qiqihal.entities;
import com.fasterxml.jackson.annotation.JsonView;
import lombok.Data;
import lombok.NoArgsConstructor;
import lombok.experimental.Accessors;
/**
* lombok注解:
* @AllArgsConstructor:全参的构造方法
* @NoArgsConstructor:无参的构造方法
* @Data:get和set方法
* @Accessors(chain = true)使用链式编程
*/
@SuppressWarnings("serial")
@NoArgsConstructor
@Data
@Accessors(chain = true)
public class User {
private Long id;
private String username;
private String password;
private String mobile;
private Long birthday;
/*使用接口声明多个视图*/
public interface UserSimpleView{};
/*视图继承,可以获得被继承的视图*/
public interface UserDetailView extends UserSimpleView{};
/*@JsonView注解用来控制输入输出的实体转换成json后展示实体中的不同字段,
使用@JsonView注解在属性的get方法上指定不同接口声明的视图,
属性的get方法上没有指定的接口声明的视图,不会在实体换换成的json中出现该字段*/
@JsonView(UserSimpleView.class)
public String getUsername() {
return username;
}
/*使用lombok的@Data注解为实体自动添加get方法*/
@JsonView(UserSimpleView.class)
public String getPassword() {
return password;
}
/*UserDetailView接口继承UserSimpleView接口,所以UserDetailView可以展示UserSimpleView接口声明的视图*/
@JsonView(UserDetailView.class)
public String getMobile() {
return mobile;
}
@JsonView(UserDetailView.class)
public Long getBirthday() {
return birthday;
}
}
QIQIHAL-SECURITY-DEMO\src\main\java\com\qiqihal\entities\FileInfo.java文件内容如下:
package com.qiqihal.entities;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.experimental.Accessors;
@SuppressWarnings("serial")
//使用lombok自动创建全参构造方法
@AllArgsConstructor
@Data
@Accessors(chain = true)
public class FileInfo {
private String filePath;
}
4 创建自定义校验注解和校验逻辑类
在QIQIHAL-SECURITY-DEMO项目中的QIQIHAL-SECURITY-DEMO\src\main\java\com\qiqihal路径下创建validator文件夹,在该文件夹下创建Mobile.java和MobileValidator.java文件,其中Mobile是Annotation注解,用于创建自定义逻辑,创建方法是idea中右键选择new,在二级弹出菜单中选择Java Class,在Create New Class弹出框中点击kind下拉列表,选择Annotation,输入Name后点击OK后成功创建Annotation注解,MobileValidator是校验逻辑实现类,要继承ConstraintValidator接口
QIQIHAL-SECURITY-DEMO\src\main\java\com\qiqihal\validator\Mobile.java文件内容如下:
package com.qiqihal.validator;
import javax.validation.Constraint;
import javax.validation.Payload;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
/*
@Target说明了Annotation所修饰的对象范围
ElementType.TYPE:说明该注解只能被声明在一个类前。
ElementType.FIELD:说明该注解只能被声明在一个类的字段前。
ElementType.METHOD:说明该注解只能被声明在一个类的方法前。
ElementType.PARAMETER:说明该注解只能被声明在一个方法参数前。
ElementType.CONSTRUCTOR:说明该注解只能声明在一个类的构造方法前。
ElementType.LOCAL_VARIABLE:说明该注解只能声明在一个局部变量前。
ElementType.ANNOTATION_TYPE:说明该注解只能声明在一个注解类型前。
ElementType.PACKAGE:说明该注解只能声明在一个包名前。
*/
@Target({ElementType.METHOD,ElementType.FIELD,ElementType.CONSTRUCTOR,ElementType.PARAMETER})
/*@Retention定义注解的生命周期,RetentionPolicy.RUNTIME注解不仅被保存到class文件中,
jvm加载class文件之后,仍然存在,此时可以通过反射获得定义在某个类上的所有注解*/
@Retention(RetentionPolicy.RUNTIME)
/*@Constraint开启注解校验功能,validateBy具体校验规则的实现类*/
@Constraint(validatedBy = MobileValidator.class)
public @interface Mobile {
/*required()校验规则类MobileValidator的初始化方法,default true默认返回true*/
boolean required() default true;
/*要让自定义注解具有校验功能,必须有以下三个参数,@NotBlank、@Past等注解中有的*/
String message() default "手机号码格式错误";
Class>[] groups() default { };
Class extends Payload>[] payload() default { };
}
QIQIHAL-SECURITY-DEMO\src\main\java\com\qiqihal\validator\MobileValidator.java文件内容如下:
package com.qiqihal.validator;
import org.apache.commons.lang.StringUtils;
import javax.validation.ConstraintValidator;
import javax.validation.ConstraintValidatorContext;
import java.util.regex.Pattern;
/*ConstraintValidator第一个泛型是自定义注解类Constraint,第二个泛型是要验证的数据类型,
* 实现了ConstraintValidator接口的类自动注入到spring容器中,无需加入@Configuration或@Compoment注解*/
public class MobileValidator implements ConstraintValidator {
private boolean required = false;
@Override
/*校验初始化*/
public void initialize(Mobile constraintAnnotation) {
required = constraintAnnotation.required();
}
@Override
/*校验逻辑*/
public boolean isValid(Object value, ConstraintValidatorContext context) {
if (required) {
return mobileRegular(value);
} else {
if (StringUtils.isEmpty(value.toString())) {
return true;
} else {
return mobileRegular(value);
}
}
}
/*手机号正则校验*/
private boolean mobileRegular(Object mobile){
return Pattern.compile("^[1](([3][0-9])|([4][5,7,9])|([5][^4,6,9])|([6][6])|([7][3,5,6,7,8])|([8][0-9])|([9][8,9]))[0-9]{8}$").matcher(mobile.toString()).matches();
}
}
4 测试
4.1 选中testAll测试方法,点击右键,在弹出的下拉菜单中选择Debug或Run,执行测试用例
测试结果:
说明:可以看到打印Json返回值中只有"username"和"password"这两个字段,因为User类作为封装返回值实体,我们在User类中声明了两个不同的接口,作为@JsonView注解的参数,最后把具有不同参数的@JsonView注解标注在User类中不同的属性的get方法上,这样User类的属性就被不同的接口区分开来,这些接口被称为视图,我们看到User类中"username"和"password"属性的get方法被参数为UserSimpleView.class的@JsonView注解修饰,则UserSimpleView视图具有username和password这两个属性,同时,在UserController类中,使用User作为封装返回值实体的接口上也被参数为UserSimpleView.class的@JsonView注解修饰,与修饰"username"和"password"属性的get方法的注解的参数相同,这说明当接口将User作为封装返回值实体被转成Json数据时,只展示User类中get方法被参数为UserSimpleView.class的@JsonView注解修饰的属性,即展示UserSimpleView视图
4.2 选中testGet测试方法,点击右键,在弹出的下拉菜单中选择Debug或Run,执行测试用例
测试结果:
说明:可以看到打印Json返回值中的字段包含了User类中的全部属性,因为User类作为封装返回值实体,我们看到User类中"mobile"和"birthday"属性的get方法被参数为UserDetailView.class的@JsonView注解修饰,则UserDetailView视图具有mobile和birthday这两个属性,而UserDetailView接口又集成自UserSimpleView接口,说明UserDetailView视图同时具有UserSimpleView视图所具有的属性,所以UserDetailView视图拥有所有User类中的属性,当我们要在接口返回的Json结果中使用UserDetailView视图时,我们就在UserController类中,找到使用User作为封装返回值实体的接口,并使用参数为UserDetailView.class的@JsonView注解修饰,则说明当接口将User作为封装返回值实体被转成Json数据时,展示User类中的全部属性,即展示UserSimpleView视图
4.3 选中testAdd测试方法,点击右键,在弹出的下拉菜单中选择Debug或Run,执行测试用例
测试结果:
说明:可以看到返回打印返回结果中包含错误提示"用户名不能为空",这是因为我们在封装请求参数的UserConditions类中的username属性上增加了@NotBlank(message = "用户名不能为空")的注解,当我们要使用Java Bean Validation注解校验时,我们就在UserController类中,找到使用UserConditions作为接口方法入参的接口add方法,并在UserConditions上增加@Validated注解,这样我们在UserConditions类的username属性上增加@NotBlank注解才能生效,当用请求中username字段为空或空字符串时,校验报错信息就是@NotBlank注解的参数"用户名不能为空",校验报错信息就会保存在第二个参数BindingResult中,需要注意的是,@Validated和BindingResult是配对出现,并且形参顺序是固定的(一前一后)
4.3.1 也许有同学会认为,我们在MockMvc测试用例中,就使用了UserConditions类封装请求参数,会不会是在Mock测试用例中就发生了校验?这里总结一下Java Bean Validation是如何生效的,首先我们在要做校验的实体的属性上加入Java Bean Validation注解,然后将实体作为方法参数,并在参数上加入@Validated注解才能让校验生效,如果我们只在实体的属性上增加校验Java Bean Validation注解,而不在作为方法参数的实体上增加@Validated注解,校验是无法生效的,最重要的是,我们在MockMvc测试用例上,把请求参数封装进带有Java Bean Validation注解的实体中,最后使用jackson转换成Json格式,以Post请求方式将请求参数发送到接口,因为我们在MockMvc测试用例中,只传递了请求参数的Json字符串,并没有将封装了请求参数的UserConditions类传递过去,所以作为请求参数的Json字符串会被SpringBoot重新封装进UserController的add方法的使用@Validated修饰的第一个参数UserConditions conditions中,并且经过Java Bean Validation的校验逻辑后,最终将校验报错信息保存在add方法的第二个参数BindingResult
4.4 选中testUpdate测试方法,点击右键,在弹出的下拉菜单中选择Debug或Run,执行测试用例
测试结果:
说明:可以看到打印返回结果中包含错误提示"birthday 生日必须是过去的时间"和"mobile 手机号码格式错误"
4.4.1 提示"birthday 生日必须是过去的时间",这是因为我们在在封装请求参数的UserConditions类中使用birthday属性接收请求参数中的Long类型时间戳,然后在的birthdayDate属性的get方法上将Long类型时间戳转换成Date类型赋值给birthdayDate属性,并在birthdayDate属性上增加了@Past(message = "生日必须是过去的时间")的注解,当我们在MockMvc测试用例中,将birthday时间增加一年,验证@Past自定义注解校验是否执行,所以最后在BindingResult类中打印"生日必须是过去的时间"的校验报错信息
4.4.2 提示"mobile 手机号码格式错误",这是因为我们在QIQIHAL-SECURITY-DEMO\src\main\java\com\qiqihal\validator路径下创建了Mobile自定义注解,并将@Mobile注解加在了UserConditions类的mobile属性上
4.4.2.1 @Mobile是如何被创建、如何具有校验功能的?
首先我们在QIQIHAL-SECURITY-DEMO\src\main\java\com\qiqihal\validator路径下右键选择new,在二级弹出菜单中选择Java Class,在Create New Class弹出框中点击kind下拉列表,选择Annotation,输入Name后点击OK后成功创建@Moblie注解,并在@Mobile注解上增加三个注解(三个注解的含义具体看QIQIHAL-SECURITY-DEMO\src\main\java\com\qiqihal\validator\Mobile.java文件中的注释)@Target、@Retention、@Constraint,其中@Constraint注解的参数validateBy的值就是具体校验规则的实现类——MobileValidator(MobileValidator类具体看QIQIHAL-SECURITY-DEMO\src\main\java\com\qiqihal\validator\MobileValidator.java文件中的注释),MobileValidator是校验逻辑实现类,要继承ConstraintValidator接口,并重写initialize初始化方法、重写isValid校验逻辑方法,其中@Moblie注解的核心逻辑——手机号正则校验就写在isValid方法中,最终我们在UserConditions作为封装请求参数值实体作为接口方法入参的接口上,使用@Validated注解修饰UserConditions参数,当用mobile属性格式不是手机号码格式时,我们的自定义@Mobile注解才会生效
4.5 选中testUpload测试方法,点击右键,在弹出的下拉菜单中选择Debug或Run,执行测试用例
说明:可以看到打印参数以及文件上传的绝对路径,同时打开D盘下1542097139887.txt文件中的内容为"QIQIHAL"
4.6 测试下载功能,QIQIHAL-SECURITY-DEMO\src\main\java\com\qiqihal\QiqihalSecurityDemoApplication.java点击右键,在弹出的下拉菜单中选择Debug或Run,运行QIQIHAL-SECURITY-DEMO项目
在D盘创建名称为1.txt的文件,内容为"QIQIHAL",在浏览器地址栏输入localhost:8080/file/1
由于本项目引入Oauth2,所以输入application.yml中设置的登录账号:root和密码:123456
测试结果:我们就看到浏览器下载了一个名为"QIQIHAL.txt"的文件
文件内容为"QIQIHAL"
5 swagger测试,因为我们在QIQIHAL-SECURITY父项目的pom文件中已经引入了springfox-swagger2和springfox-swagger-ui的jar包,所以我们可以直接使用swagger进行测试
重复4.6 步骤的操作,QIQIHAL-SECURITY-DEMO\src\main\java\com\qiqihal\QiqihalSecurityDemoApplication.java点击右键,在弹出的下拉菜单中选择Debug或Run,运行QIQIHAL-SECURITY-DEMO项目
在浏览器地址栏输入http://localhost:8080/swagger-ui.html
由于本项目引入Oauth2,所以输入application.yml中设置的登录账号:root和密码:123456
进入swagger界面
点击file-controller\security-test-controller、user-controller,这三个我们自己写的接口,其他接口是SpringBoot项目的接口,这些SpringBoot项目的接口在我们方位swaggerui的时候会报错,我们不需要理会,这些接口报错并不影响我们使用swagger测试
我们可以看到,每个接口都有访问方式GET、POST、PUT等访问方式,同时还有访问路径以及接口说明
其中"根据条件查询全部用户信息"就是QIQIHAL-SECURITY-DEMO\src\main\java\com\qiqihal\controller\UserController类中all方法上@ApiOperation注解的参数value的值
点击"根据条件查询全部用户信息"接口
在弹出的列表中点击Try it out
其中"生日(时间戳)"就是QIQIHAL-SECURITY-DEMO\src\main\java\com\qiqihal\entities\UserConditions类中birthday属性上@ApiModelProperty注解的参数value的值,dataType属性用来注释参数为何种类型
填写好我们注释的请求参数(offset、pageNumber、pageSize、paged等属性是参数用Pageable封装,Pageable是spring-data的自动分页工具,如果不使用spring-data可以忽略此参数,同时因为Pageable是.calss,无法对class文件进行修改,所以无法给offset、pageNumber、pageSize、paged等属性增加@ApiModelProperty注解)
点击Execute执行请求
可以看到访问成功Code 200 以及Response body中的返回结果