<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-jdbc</artifactId>
</dependency>
spring.datasource.driver-class-name=com.mysql.jdbc.Driver
spring.datasource.url=jdbc:mysql://127.0.0.1:3306/...
spring.datasource.username=root
spring.datasource.password=root
~~~java
- 集群Session管理(后面讲)
~~~java
<dependency>
<groupId>org.springframework.session</groupId>
<artifactId>spring-session</artifactId>
</dependency>
刚开始的时候我们不一定需要使用,我们可以设置手动关闭:
在application.yml中进行设置:
spring.session.store-type:none
//将Session关闭
配置端口号:server.prot=8060
当我们引入SpringSecurity框架的时候,会默认启动身份验证,如果我们不想启动身份验证我们可以先手动关闭:
security.basic.enabled=false
//这样后访问系统内的接口就不会再自动身份验证啦~
后面我们会将这些全部打开,并进行相关的配置;
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
<version>1.3.3.RELEASE</version>
</plugin>
</plugins>
<finalName>demo</finalName> //打包出来的名字
</build>
当有了这些配置后,我们在target中就可以看到打包有两个文件:
一个以jar.original结尾的是原始jar包,不包含第三方的;
一个是可执行jar包,名字为demo.jar 它是可以直接执行的;
学习内容:
RESTful API特点:
在请求中只放资源,而不会表明做什么操作,通过HTTP状态进行对应
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
</denpendency>
//添加这个依赖后会将Spring的测试框架引入
- @RunWith(SpringRunner.class)
- @SpringBootTest
- publci class UserControllerTest{
- @Autowired
- private WebApplicationContext wac;
- private MockMvc mockMVC;
- @Befroe
- public void setup(){
- mockMvc=MockMvcBuilders.webAppContextSetup(wac).build();
- }
- @Test
- public void whenQuerySuccess() throws Exception{
- mockMvc.perform(MockMvcRequestBuilders.get("/user")
- . param("username","pojo")
- .contentType(MediaType.APPLICATION_JSON_UTF8))
- .addExpect(MockMvcResultMathchers.status().isOk())
- .andExpect(MockMvcResultMathers.jsonPath("$.length()").value(3));}} //返回的结果json长度必须为3
@RestController
public class UserController{
@RequestMapping(value="/user",method=RequestMethod.GET)
public List<User>query(@RequestParam String username){ //这里的@RequestParam 表示请求的参数必须带一个这样的参数username
List<User> users=new ArrayList<>();
users.add(new User());
users.add(new User());
users.add(new User());
return users;
}
}
@RequestParam(required=false,defaultVGFalue="tom") String nickname 这样设置的时候,即便没有给这个nickname值,也不会报错,同时还可以给到它一个默认的值
@Test
public void whenGenInfoSuccess() throws Exception{
mockMvc.perform(get("/user/1")
.contentType(MediaType.APPLICATION_JSON_UTF8))
.andExpect(status().isOk())
.andExpect(jaonPath("$.username").value("tom"))
}
这里测试用例,限制了这个请求接口必须为GET请求,然后犯规的username是tom,用户id值为1的用户详细信息,这里将MockMvcRequestBuilders以及另外一个提取出来做为了一个静态方法,所以可以省略直接调用;
接口方法代码:
@RequestMapping(value="/user/{id}",method=RequestMethod.GET)
public User getInfo(@PathVariable(name="id") String id){ //指定了name=id 那么后面的String类型的变量名可以随便取名,不一定为id
User user=new User();
user.setUsername("tom");
return user;
}
我们还可以进行限制,id值只能传入数字,使用正则表达式
@RequestMapping(value="/user/{id:\\d+}",method=RequestMethod.GET)
public User getInfo(@PathVariable(name="id") String id){ //指定了name=id 那么后面的String类型的变量名可以随便取名,不一定为id
User user=new User();
user.setUsername("tom");
return user;
}
当id值为字母的时候,肯定是错误的,那么我们可以做一个错误测试用例,检测上面的正则表达式是否有效果;
@Test
public void whenGetInfoFail() throws Exception{
mockMvc.perform(get("/user/a")
.contentType(MediaType.APPLICATION_JSON_UTF8))
.andExpect(status().is4xxClientError()); //服务器返回4..的状态比如404,400等都是在内
}
使用步骤:
概述: JsonView 是什么,为什么要使用它?
代码示例:
import org.hibernate.validator.constraints.NotBlank;
import com.fasterxml.jackson.annotation.JsonView;
public class User {
//声明一般视图接口 只允许这个视图返回用户名属性
public interface UserSimpView{};
//声明完整视图接口 允许返回用户名密码属性 由于集成了一般视图接口 含义是拥有了一般视图所具有的返回属性
public interface UserDetailView extends UserSimpView{};
private Integer Id;
private String userName;
private String passWord;
public User() {
}
public User(Integer Id, String userName, String passWord) {
this.Id = Id;
this.userName = userName;
this.passWord = passWord;
}
@JsonView(UserSimpView.class)
public Integer getId() {
return Id;
}
public void setId(Integer id) {
Id = id;
}
@JsonView(UserSimpView.class)
public String getUserName() {
return userName;
}
public void setUserName(String userName) {
this.userName = userName;
}
@JsonView(UserDetailView.class)
public String getPassWord() {
return passWord;
}
public void setPassWord(String passWord) {
this.passWord = passWord;
}
@Override
public String toString() {
return "User [Id=" + Id + ", userName=" + userName + ", passWord=" + passWord + "]";
}
}
@RestController
@RequestMapping("/user")
public class UserController {
@GetMapping(produces="application/json;charset=UTF-8")
@JsonView(UserDetailView.class)
public List getUserAll(@RequestParam("token") String token){
List<User> userList = new ArrayList<>();
userList.add(new User(1,"二十岁以后特殊视图0","123456特殊视图"));
userList.add(new User(2,"二十岁以后特殊视图1","qweqwe特殊视图"));
userList.add(new User(3,"二十岁以后特殊视图2","asdgdd特殊视图"));
return userList;
}
@RestController
@RequestMapping("/user")
public class UserController {
@GetMapping(produces="application/json;charset=UTF-8")
@JsonView(UserDetailView.class) //完整视图,返回username,password
public List getUserAll(@RequestParam("token") String token){
List<User> userList = new ArrayList<>();
userList.add(new User(1,"二十岁以后特殊视图0","123456完整视图"));
userList.add(new User(2,"二十岁以后特殊视图1","qweqwe完整视图"));
userList.add(new User(3,"二十岁以后特殊视图2","asdgdd完整视图"));
return userList;
}
@RestController
@RequestMapping("/user")
public class UserController {
@GetMapping(produces="application/json;charset=UTF-8")
@JsonView(UserSimpView.class)
public List getUserAll(@RequestParam("token") String token){
List<User> userList = new ArrayList<>();
userList.add(new User(1,"二十岁以后特殊视图0","123456完整视图"));
userList.add(new User(2,"二十岁以后特殊视图1","qweqwe完整视图"));
userList.add(new User(3,"二十岁以后特殊视图2","asdgdd完整视图"));
return userList;
}
我们可以使用变体简化:
有测试用例的时候,我们在重构代码就能按着正确的路线走;
@Test
public void whenCreateSuccess() throws Exception{
String content="{\"username\":\"tom\",\"password\":null}";
mockMvc.perform(post("/user").contentType(MediaType.APPLICATION_JSON_UTF8)
.content(content)) //这里是给接口发送请求的时候携带的参数,表示只给了username和password,而username有值,password没有值
.andExpect(status().isOk())
.andExpect(jsonPath("$.id").value("1"));
}}
@PostMapping
public User create(@RequestBody() User user){ //使用@RequestBody后才能将请求的参数映射到对象里面,它是将Post的请求体Body里面的值映射
user.setId("id");
return user;
}
在类中有生日Birthday的日期类型,我们应该如何去操作呢?
代码改造如下:
@PostMapping
public User create(@RequestBody User user){
System.out.println(user.getId());
System.out.println(user.getUsername());
System.out.println(user.getPassword());
System.out.println(user.getBirthday()); //返回标准日期格式
user.setId("1");
return user;
}
@Test
public void whenCreateSuccess() throws Exception{
Date date=new Date();
String content ="{\"username\":\"tom\",\"password\":null,\"birthday\":"+date.getTime}; //将时间戳信息给content
String result=mockMvc.perform(post("/user").contentType(MediaType.APPLICATION_JSON_UTF8)
.content(content))
.andExpect(status().isOk())
.andExpect(jsonPath("$.id").value("1"))
.andReturn().getResponse().getContentAsString();
}
@NotBlank
private String password;
@PostMapping
public User create(@Valid @RequestBody User user){
user.setId("1");
return user;
}
//返回的结果大致为:
//显示内容:may not be empty
//具体方法:error.getDefaultMessage()
@PostMapping
public User create(@Valid @RequestBody User user,BindingResult errors){
if(errors.hasErrors()){
errors.getAllErrors().stream().forEach(errorm -> System.out.println(error.getDefaultMessage()));
}
user.setId("1");
return user;
}
图示1:
[外链图片转存失败(img-MNxuvF3S-1563862092757)(https://i.imgur.com/n2v52DS.jpg)]
图示2:
[外链图片转存失败(img-XByUolvq-1563862092758)(https://i.imgur.com/fHrZxhi.jpg)]
Date date=new Date(LocalDateTime.now().plusYears(1).atZone(ZoneId.systemDefault()).toInstant().toEpochMilli())
@Past(message="生日必须是过去的时间")
private Date birthday;
@Target({ElementType.METHOD,ElementType.FIELD})
@Retention(RetentionPolicy.RUNTIME)
@Constraint(validateBy=MyConstrainvalidator.class) //锁定这个校验接口实现类
public @interface MyConstraint{
String message();
Class<?>[] groups[] default{};
Class<? extends Payload>[] payload() default{};
}
//这里不用加@Component等注解,继承了这个接口就会自动加入Bean
public class MyConstraintValidator implements ConstraintValidator<MyConstraint,Object>{
@Autowired
private HelloService helloService;
@Override
public void initialize(MyConstraint constraintAnnotation){
System.out.println("My validator init");
}
@Override
public boolean isValid(Object value,ConstraintValidatorContext context){
helloService.geeting("tom"); //这里可以忽略,执行业务逻辑,我们平时可以在这里随便写,可以注入Service
//这里的value是传入的值;
return true;
}
}
@MyConstraint(message="这是一个测试")
private String username;
校验的时候,我们如果需要根据业务来校验,就可以使用这种方式,自定义了业务逻辑,可以从传入的值进行判断,减少了很多不必要的操作;
PutMapping("/{id:\\d+}")
public User update(@Valid @RequestBody User user,BindingResult errors){
if(errors.hasErrors()){
errors.getAllErrors().stream().forEach(err0r->{System.out.println(error.getDefaultMessage());});
}
}
@Test
public void whenUpdateSuccess() throws Exception{
Date date=new Date(LocalDateTime.now().plusYears(1).atZone(ZoneId.systemDefault()).toInstant().toEpochMilli());
String content ="{\"username\":\"tom\",\"password\":null,\"birthday\":"+date.getTime}; //将时间戳信息给content
String result=mockMvc.perform(post("/user/1").contentType(MediaType.APPLICATION_JSON_UTF8)
.content(content))
.andExpect(status().isOk())
.andExpect(jsonPath("$.id").value("1"))
.andReturn().getResponse().getContentAsString();
System.out.println(result);
}
@Test
public void whenDeleteSuccess() throws Exception{
mockMvc.perform(delete("/user/1")
.contentType(MediaType.APPLICATION_JSON_UTF8))
.andExpect(status().isOk());
}
@DeleteMapping("/{id:\\d+}")
public void delete(@PathVariable String id){
System.out.println(id);
}
SpringBoot中默认的错误处理机制
自定义异常处理
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
</head>
您所访问的页面不存在
</body>
</html>
@ControllerAdvice //管辖所有的Controller类下的异常,本身不处理HTTP请求
public class ControllerExceptionHandler{
@ExceptionHandler(UserNotExistException.class) //加了这个注解后,其他Controller抛出的这个类型的异常就会转入到这个方法进行处理;0.
@ResponseBody
@ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR) //500状态码
public Map<String,Object>handleUserNotExistException(UserNotExistException ex){
Map<String,Object> result =new HashMap<>();
result.put("id",ex.getId());
result.put("message",ex.getMessage());
return result; //返回错误后的Result
}
}
@Component
public class TimeFilter implements Filter{
@Override
public void destroy(){
//销毁时执行的方法
}
@Override
public void doFilter(ServletRequest request,ServletResponse response,FilterChain chain) throws IOException,ServletException{
System.out.println("time filter start");
long start =new Date().getTime();
chain.doFilter(request,response); //这个表示用户执行的方法,因为过滤器有两部分,第一部分是用户请求前会进入一次过滤器,当用户执行完方法之后也会执行一次过滤器,而包裹我们的doFilter方法则是执行方法前,这里的chain.doFilter是执行方法时,当执行完后又会进入doFIlter里面,继续未完成的代码,通过这里我们可以实现计算一个方法的执行时间需要多久;
System.out.println("time filter 耗时:"+(new Date().getTime()-start));
System.out.println("time filter finish");
}
@Override
public void init(FilterConfig arg0) throws ServletException{
System.out.println("time filter init");
}
}
@Configuration
public class WebConfig{
@Bean
public FilterRegistrationBean timeFilter(){
FilterRegistrationBean registrationBean =new FilterRegistrationBean(); //创建一个注册过滤器
TimeFilter timeFilter=new TimeFilter(); //创建一个自定义过滤器对象
registrationBean.setFiter(timeFilter); //将自定义过滤器注册到注册过滤器中;
List<String> urls=new ArrayList<>();
ursls.add("/*");
registrationBean.setUrlPatterns(urls); //给过滤器设置拦截范围
return registrationBean
}
~~~
@Component //这里只声明注解是不行的,我们必须添加到Interceptor中去
public TimeInterceptor implements HandlerInterceptor{
@Override
public boolean preHandle(HttpServletRequest request ,HttpServletResponse response,Object handler) throws Exceptoon{
System.out.println(((HanderMethod)handler).getBean().getClass().getName()); //打印执行方法的类名
System.out.println(((HandleMethod)handler).getMethod().getName()); //打印执行方法的方法名
request.setAttribute("startTime",new Date().getTime());
return true; //这里只有返回ture才会执行后面的postHandle方法
}
@Override
public void postHandle(HttpServletRequest request ,HttpServletResponse response,Object handler,ModelAndView modelAndView ) throws Exception{
Long start=(Long) request.getAttribute("startTime"); //从上一个拦截器方法中找到这个start时间,然后通过执行方法后调用的这个PostHandle方法的时间相减,就可以得到方法的调用花费时间了;
System.out.println("time interceptor 耗时:"+ (new Date().getTime()-start))
}
@Override
public void afterCompletion(HttpServletRequest request,HttpServletResponse response,Object handler,Exceptiopn ex) throws Exception{
System.out.println("ex is "+ex); //这里打印异常,如果异常被统一异常处理所捕捉了,那么即便发生了异常,这里打印还是会为null,需要注意;
}
}
@Configuration
public class WebConfig extends WebMvcConfigurerAdpter{
@Autowired
private TimeInterceptor timeInterceptor;
@Override
public void addInterceptors(InterceptorRegistry registry){
registry.addInterceptor(timeInterceptor); //通过引入已经加入@Compnonet注解的拦截器,然后将其挂载到Interceptor上,即可实现了自定义拦截器
}
}
拦截器不仅能拦击我们的东西,也可以拦截Spring相关的Controller的方法;
拦截器虽然能处理方法,但是它不能知道方法中参数具体的类型,参数中具体的值,它的局限性我们可以通过切片解决;
Spring AOP 简介:
操作:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>Spring-boot-starter-aop</artifactId>
</dependency>
@Aspect
@Component
public class TimeAspect{
@Around("execution(* com.migu.web.controller.UserController.*(...))") //这里是围绕,我们还可以使用@After,@Before等
public Object handleControllerMethod(ProceedingJoinPoint pjp) throws Throwable{
Object[] args= pjp.getArgs(); //获取到所有的参数
for(Object arg: args){
System.out.println("arg is "+arg);
}
long start =new Date().getTime();
Object object=pjp.proceed(); //拿到返回值
return object;
}
}
过滤器只能拿到原始的Request,而拦截器能拿到方法但是不能拿到具体的值,而切片则可以拿到具体的参数和具体的返回值等。
@Test
public void whenUploadSuccess() throws Exception{
String result=mockMvc.perform(fileUpload("/file")
.file(new MockMultipartFile("file","text.txt","multipart/form-data","hello upload".getBytes("UTF-8")))
.andExpect(status().isOk)
.andReturn().getResponse().getContentAsString();
System.out.println(result);
)
}
@RestController
@RequestMapping("/file")
public class FileController{
@PostMapping
public FileInfo uplaod(MultipartFile file) throws Exception{
System.out.println(file.getName()+"-"+file.getOriginalFilename()+"-"+file.getSize());
String folder="/..." //这里是定义文件上传的地址,我们这里是存入本地,如果是上传到图片服务器之类的,则添加相应的东西即可;
File localFile=new File(folder,new Date().getTime()+".txt"); //将新文件改名
file.transferTo(localFile); //执行上传操作
return new FileInfo(localFile.getAbsolutePath());
}}
<dependency>
<groupId>commons-io</groupId>
<artifactId>commons-io</artifactId>
<version>2.5</version>
</dependency>
@GetMapping("/{id}")
public void download(@Pathvaariable String id,HttpServletRequest request,HttpResponse response){
try(
InputStream inputStream =new FileInputStream(new File(folder,id+".txt"));
OutputStream outputStream =response.getOutpustStream(); ){
response.setContentType("application/x-download");
response.addHeader("Content-Disposition","attachment;filename=test.txt");
IOUtils.copy(inputStream,outputStream);
}}
~~~
这里是提供一个id值,然后以这个id值命名将文件下载下来;先InputStream读取文件信息,然后再使用outputStream输出,通过工具类IOUtils.copy完成;在这里面我们将try()中声明了inputStream 和outStream 流,这是JDK7以上的新特性,我们就可以不必手动关闭流了;
为什么要使用异步处理?在高并发环境下有较好的吞吐量,比同步处理能同时处理更多的请求;
注意:如果想处理异步的请求有上面两种方式,但是在拦截方面我们也需要一个@Configuration注解所修饰的类实现WebMavConfigurerAdapter并重写configureAsyncSupport方法,在重写方法中configurer.registerCollable…或configurer.registerDeferredResult…要注册这两个异步的,因为同步和异步的拦截是不一样的;
相关的东西涉及的方面很多,难度也较大,这里只大概提一下,有兴趣的朋友可以单独找这方面的了解一下
- 使用swagger自动生成html文档
- 根据写的代码自动生成文档
- 使用WireMock快速伪造RESTful服务
<dependency>
<groupId>io.springfox</groupId>
<artifactId>springfox-swagger2</artifactId>
<2.7.0>
</dependency>
<dependency>
<groupId>io.springfox</groupId>
<artifactId>springfox-swagger-ui</artifactId>
<2.7.0>
</dependency>
@ApiModelProperty(value="用户年龄起始值")
private int age;
这几个是比较常用的,其他的注解需要了解的可以网上找相关文章;
我们后端如果做伪造服务,那么前端就可以不用造假数据了。因为如果前端造假数据的话,如果是APP(安卓,苹果),浏览器等众多前端组每个都自己造假数据的话,对工作效率有影响,同时质量也残差不齐,我们后端可以做这方面的事情;
WireMock:它是一个独立的服务器,可以定义当它收到了什么请求后,返回什么样的响应;
<dependency>
<groupId>com.github.tomakehurst</groupId>
<artifactId>wiremock</artifactId>
<version>2.5.1</version>
</dependency>
//如果不要版本号可以省略
public class MockServer{
public static void main(String[] args){
WireMock.configureFor(8062); //因为是在本机上测试,所以可以不用指定ip
WireMock.removeAllMappings(); //每次开服务清除以前的配置,重新加配置更新;
WireMock.stubFor(geturlPathEqualTo("/order/1")).willReturn(aResponse().withBody("{\"id\":1}").withStatus(200))); //这里的链式编程还可以继续加东西;而且相关的Json我们可以单独写,然后读取出来;
}}
//浏览器访问: localhost:8062/order/1
{
"id": 1
"type": "A"
}
public class MockServer{
public static void main(String[] args){
WireMock.configureFor(8062); //因为是在本机上测试,所以可以不用指定ip
WireMock.removeAllMappings(); //每次开服务清除以前的配置,重新加配置更新;
ClassPathResource resource=new ClassPathResource("mock/response/01.txt");
String content =StringUtils.join(FileUtils.readLines(resource.getFile(),"UTF-8").toArray(),"\n")
WireMock.stubFor(geturlPathEqualTo("/order/1")).willReturn(aResponse().withBody("{\"id\":1}").withStatus(200))); //这里的链式编程还可以继续加东西;而且相关的Json我们可以单独写,然后读取出来;
}}
//浏览器访问: localhost:8062/order/1
WireMock可以做各种请求,以及其他很多的高级特性,与前端的联合是很方便的;如果有深入了解的朋友可以详细查看官方文档;