本系列文章是我的通過購買慕課網中Spring Security技术栈开发企业级认证与授权课程编写的學習筆記
开发环境
Jdk1.8、Idea、Mysql、maven module
使用框架组件
spring boot 、spring security、spring social、spring oauth
搭建项目目标
深入理解spring security原理、功能及代码
基于spring security及相关框架独立开发认证授权相关功能
掌握抽象和封装的常见技巧,可以编写可重用的模块拱他人使用
Spring Security是一个能够为基于Spring的企业应用系统提供声明式的安全访问控制解决方案的安全框架。它提供了一组可以在Spring应用上下文中配置的Bean,充分利用了Spring IoC,DI(控制反转Inversion of Control ,DI:Dependency Injection 依赖注入)和AOP(面向切面编程)功能,为应用系统提供声明式的安全访问控制功能,减少了为企业系统安全控制编写大量重复代码的工作。
Spring IO是构建现代应用程序的一个有凝聚力的版本化平台。这是一个模块化的企业级分布,提供了一组策略依赖关系,同时让开发人员完全控制只部署他们所需的部分。
主要解决问题:
在使用Spring的时候,经常会使用到第三方库,一般大家都是根据经验挑选一个版本号或挑选最新的,随意性较大,其实这是有问题的,除非做过完整的测试,保证集成该版本的依赖不会出现问题,且后续集成其它第三方库的时候也不会出现问题,否则风险较大,且后续扩展会越来越困难,因为随着业务复杂度的增加,集成的第三方组件会越来会多,依赖之间的关联也会也来越复杂。
优点:
Spring IO平台提供了各种Spring项目及其依赖项的版本。通过指定的配置添加到您的构建脚本中,您就可以声明您的依赖关系,而无需担心版本号,保证最大限度的扩展,而且该版本的依赖是经过测试的,可以完美的与其它组件结合使用。
airports
| - - airport-core < - - |
| - - pom.xml |
| - - airport-browser - - |(依赖) <-|
| - - pom.xml | |
| - - airport-app - - | <-|
| - - pom.xml |(选择性依赖)
| - - traffic-forecast - - - - - - - |
| - - pom.xml
| - - pom.xml
airports: 项目父模块,只负责管理其下的子项目,只有一个pom文件,并且pom文件中打包方式为
pom
<modules>
<module>airportsCoremodule>
<module>airportsBrowsermodule>
<module>airportsAppmodule>
<module>trafficForecastmodule>
modules>
<packaging>pompackaging>
<properties>
<com.chuIllusion.version>1.0-SNAPSHOTcom.chuIllusion.version>
<project.build.sourceEncoding>UTF-8project.build.sourceEncoding>
<project.reporting.outputEncoding>UTF-8project.reporting.outputEncoding>
<java.version>1.8java.version>
properties>
<dependencyManagement>
<dependencies>
<dependency>
<groupId>io.spring.platformgroupId>
<artifactId>platform-bomartifactId>
<version>Brussels-SR4version>
<type>pomtype>
<scope>importscope>
dependency>
<dependency>
<groupId>org.springframework.cloudgroupId>
<artifactId>spring-cloud-dependenciesartifactId>
<version>Dalston.SR2version>
<type>pomtype>
<scope>importscope>
dependency>
dependencies>
dependencyManagement>
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.pluginsgroupId>
<artifactId>maven-compiler-pluginartifactId>
<version>2.3.2version>
<configuration>
<source>${java.version}source>
<target>${java.version}target>
<encoding>${project.build.sourceEncoding}encoding>
configuration>
plugin>
plugins>
build>
airport-core: 核心业务逻辑,提供核心通用功能
pom.xml
引入核心包,包括jdbc、aop及一些基本工具类,并且引入核心组件:spring security、spring oauth、spring social
<dependencies>
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-jdbcartifactId>
dependency>
<dependency>
<groupId>org.mybatis.spring.bootgroupId>
<artifactId>mybatis-spring-boot-starterartifactId>
<version>1.3.0version>
dependency>
<dependency>
<groupId>mysqlgroupId>
<artifactId>mysql-connector-javaartifactId>
dependency>
<dependency>
<groupId>commons-langgroupId>
<artifactId>commons-langartifactId>
dependency>
<dependency>
<groupId>commons-collectionsgroupId>
<artifactId>commons-collectionsartifactId>
dependency>
<dependency>
<groupId>commons-iogroupId>
<artifactId>commons-ioartifactId>
dependency>
<dependency>
<groupId>commons-beanutilsgroupId>
<artifactId>commons-beanutilsartifactId>
dependency>
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-configuration-processorartifactId>
dependency>
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-aopartifactId>
dependency>
<dependency>
<groupId>org.springframework.cloudgroupId>
<artifactId>spring-cloud-starter-oauth2artifactId>
dependency>
<dependency>
<groupId>org.springframework.socialgroupId>
<artifactId>spring-social-configartifactId>
dependency>
<dependency>
<groupId>org.springframework.socialgroupId>
<artifactId>spring-social-coreartifactId>
dependency>
<dependency>
<groupId>org.springframework.socialgroupId>
<artifactId>spring-social-securityartifactId>
dependency>
<dependency>
<groupId>org.springframework.socialgroupId>
<artifactId>spring-social-webartifactId>
dependency>
dependencies>
airport-browser: 浏览器相关业务,依赖核心项目,拓展浏览器项目特有的功能
pom.xml
引入核心模块的支持,并拓展浏览器特有的支持,如session
<dependencies>
<dependency>
<groupId>com.chuIllusiongroupId>
<artifactId>airports.coreartifactId>
<version>${com.chuIllusion.version}version>
dependency>
<dependency>
<groupId>org.springframework.sessiongroupId>
<artifactId>spring-sessionartifactId>
dependency>
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-thymeleafartifactId>
dependency>
dependencies>
airport-app: App或前后端分离项目中相关业务,依赖核心项目,拓展了前后端分离项目的功能
pom.xml
引入核心模块的支持,并拓展App或前后端分离项目特有的支持,如使用redis代替session存储
<dependencies>
<dependency>
<groupId>com.chuIllusionsgroupId>
<artifactId>airports.coreartifactId>
<version>1.0-SNAPSHOTversion>
dependency>
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-data-redisartifactId>
dependency>
dependencies>
traffic-forecast: 项目程序,根据项目情况,选择性依赖浏览器模块或App模块
pom.xml
根据需求,引用浏览器核心支持或App核心支持,并且引用编译插件的支持
<dependencies>
<dependency>
<groupId>com.chuIllusiongroupId>
<artifactId>airports.appartifactId>
<version>${com.chuIllusion.version}version>
dependency>
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-testartifactId>
dependency>
dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-maven-pluginartifactId>
<version>1.3.3.RELEASEversion>
<executions>
<execution>
<goals>
<goal>repackagegoal>
goals>
execution>
executions>
plugin>
plugins>
<finalName>trafficForecastfinalName>
build>
注意
spring boot 包扫描机制
在项目程序启动前,一定要保证spring boot 启动类一定是在所有依赖项目中父级包,启动类扫描同级包和下级包的所有类才能生效,否则如果其他项目中有使用配置的类(相对于启动类是在启动类的上一级)则无法生效,建议将使用同一命名,以下是我这几个项目包的结构,每个模块中的代码都对应在其标准包下
在核心包中:com.chuillusion.core,在浏览器包中:com.chuillusion.browser,在App包中:com.chuillusion.app,在项目程序中:com.chuIllusion.trafiicforecast,启动类在com.chuIllusion包中创建
依赖配置
在项目中引入jdbc的依赖需要配置数据源、session的依赖需要配置session类型,否则系统启动会报错,根据系统报错信息,查找原因,排查原因,则很快可以解决问题
启动类
maven 默认 编译成jar包,使用java -jar运行命令即可将项目运行
但是在项目中我们需要war包,则要继承SpringBootServletInitializer
并且实现其config
方法,否则会报错误(无法找到入口)
@SpringBootApplication
public class TrafficForecastEntryApplication extends SpringBootServletInitializer {
public TrafficForecastEntryApplication() {
}
protected SpringApplicationBuilder configure(SpringApplicationBuilder application) {
return application.sources(new Class[]{TrafficForecastEntryApplication.class});
}
public static void main(String[] args) {
SpringApplication.run(TrafficForecastEntryApplication.class, args);
}
}
Representational State Transfer,简称REST,一种软件架构风格、设计风格,而不是标准,只是提供了一组设计原则和约束条件。它主要用于客户端和服务器交互类的软件。基于这个风格设计的软件可以更简洁,更有层次,更易于实现缓存等机制。
特点:
用URL描述资源
使用HTTP方法描述行为,使用HTTP状态码表示不同的结果
HTTP METHOD:GET(从服务器取出资源(一项或多项))、POST(在服务器新建一个资源)、PUT(在服务器更新资源)、DELETE(从服务器删除资源)
HTTP STATUS CODE:200(请求成功)、401(未授权)、404(未找到资源)、500(服务器内部错误)
使用JSON交互数据
RESTful只是一种风格,并不是强制的标准
例子:
URL | METHOD | DESRCIBE |
---|---|---|
/product | GET | 列出所有商品 |
/product?limit=10 | GET | 返回指定数量的商品 |
/product?limit=10&type=1 | GET | 返回指定类型和数量的商品 |
/product/id | GET | 获取指定的商品 |
/product/id/image | GET | 获取指定商品的所有图片 |
/product/id/image/id | GET | 获取指定商品的指定图片 |
/product | POST | 新建商品 |
/product/id | PUT | 更新指定的商品信息 |
/prodect/id | DELETE | 删除指定的商品 |
成熟度模型
spring boot 对 RESTful API 提供了多个开发注解,在以下将会慢慢介绍
@RestController : 标明此controller提供RESTful API
@RestController注解相当于@ResponseBody + @Controller合在一起的作用。
@RequestMapping及其变体。映射HTTP请求URL到JAVA方法上
@RequestParam 映射请求参数到JAVA方法的参数
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-testartifactId>
dependency>
建立测试用例的目的,是为了确保我们的服务能按照我们指定的方式去运行,并且获取预期的效果
//如何运行测试用例
@RunWith(SpringRunner.class)
//标记此类为测试类
@SpringBootTest
public class UserControllerTest {
//web环境 启动的时候spring 已经创建了,直接注入就好了
@Autowired
private WebApplicationContext webApplicationContext;
//伪造一个mvc的环境
private MockMvc mockMvc;
@Before
public void setup() {
mockMvc = MockMvcBuilders.webAppContextSetup(webApplicationContext).build();
}
//查询用例
@Test
public void whenQuerySuccess() throws Exception{
//模拟请求
mockMvc.perform(
MockMvcRequestBuilders.get("/user")//模拟发出get请求
.contentType(MediaType.APPLICATION_JSON_UTF8))//设置请求的contentType
.andExpect(MockMvcResultMatchers.status().isOk())//执行之后,期望的返回结果是返回状态码是200
//返回一个集合,包含三个元素,jsonPath解析返回的内容,并对json进行判断 $.length()集合的长度
.andExpect(MockMvcResultMatchers.jsonPath("$.length()").value(3));
}
}
启动测试用例
Error:
Error:java.lang.IllegalStateException: Unable to find a @SpringBootConfiguration, you need to use @ContextConfiguration or @SpringBootTest(classes=...) with your test
Cause:测试用例位置必须在Spring boot 启动类同名子包下,才能扫描打
Solve:
更为测试类位置为Spring boot 启动类同名子包下
添加指定启动类位置
@SpringBootTest(classes = StartApplication.class)
Error:
java.lang.AssertionError: Status
Expected :200
Actual :404
Cause and Solve:未建立对应的服务,应建立相应的服务
服务的创建
@RestController
public class UserController {
@RequestMapping(value = "/user",method = RequestMethod.GET)
public List query(){
return null;
}
}
Error:
Description
java.lang.AssertionError: No value at JSON path "$.length()", exception: json can not be null or empty
Cause:服务中的返回值为null,而测试对返回值的期待是返回数据的长度为3
Solve:使服务中返回指定的数据长度
@RequestMapping(value = "/user",method = RequestMethod.GET)
public List query(){
List users = new ArrayList<>();
users.add(new User());
users.add(new User());
users.add(new User());
return users;
}
@RequestParam
注解的使用在测试用例上添加参数传递
mockMvc.perform(
MockMvcRequestBuilders.get("/user")//模拟发出get请求
.contentType(MediaType.APPLICATION_JSON_UTF8)//设置请求的contentType
.param("username","victorys"))//设置参数
在服务中接受参数
public List query(
@RequestParam(name="username",//请求中属性名
required = true,//属性必须,否则报错
defaultValue = "jojo") //没有则设置默认值
String username){}
掌握
@PathVariable 映射URL片段到java方法的参数
在url声明中使用正则表达式
@JsonView控制json输出内容
jsonview使用步骤:1)使用接口声明视图;2)在值对象的get方法上指定视图;3)在controller方法上指定视图
/**
* 使用RESTful API 查询用户详情
* /user/1 代表资源
* get请求代表操作
* @throws Exception
*/
@Test
public void whenGetInfoSuccess() throws Exception {
String result = mockMvc.perform(MockMvcRequestBuilders.get("/user/1")//获取用户为1的信息
.contentType(MediaType.APPLICATION_JSON_UTF8))
.andExpect(MockMvcResultMatchers.status().isOk())//用状态码判断是成功还是失败,成功就是200
.andExpect(MockMvcResultMatchers.jsonPath("$.username").value("tom"))//期望值
.andReturn().getResponse().getContentAsString();//获得返回结果,并以字符串形式输出
System.out.println(result);
}
**
* 测试使用正则表达式接受资源的服务
* @throws Exception
*/
@Test
public void whenGetInfoFail() throws Exception {
mockMvc.perform(MockMvcRequestBuilders.get("/user/a")
.contentType(MediaType.APPLICATION_JSON_UTF8))
.andExpect(MockMvcResultMatchers.status().is4xxClientError());
}
@RestController
public class UserController {
@RequestMapping(value = "/user",method = RequestMethod.GET)
@JsonView(User.UserSimpleView.class)
public List query(
@RequestParam(name="username",required = true,defaultValue = "jojo")
String username,
//spring提供的一个分页信息存储对象
@PageableDefault(page = 2, size = 17, sort = "username,asc") Pageable pageable){
List users = new ArrayList<>();
users.add(new User());
users.add(new User());
users.add(new User());
return users;
}
/**
* @PathVariable:将请求资源中的片段,映射到请求服务方法中的参数中
* @JsonView:使用场景:List query()方法中返回的用户信息没有包含密码信息
* User getInfo()方法返回的用户信息有密码信息
* 1.在 User 类中 使用接口定义视图
* public interface UserSimpleView {}; //简单视图
* public interface UserDetailView extends UserSimpleView {};//包含简单视图内容和扩展视图内容
* 2.在 User 类中值对象get方法中指定视图,UserDetailView可以显示所有UserSimpleView视图的显示内容
* @JsonView(UserSimpleView.class)
* public String getUsername() { return username;}
* @JsonView(UserDetailView.class)
public String getPassword() { return password;}
3.在controller中指定视图
* @param id
* @return
*/
//@GetMapping("/{id:\\d+}")
@JsonView(User.UserDetailView.class)
@RequestMapping(value = "/user/{id:\\d+}",method = RequestMethod.GET)
public User getInfo(@PathVariable(name = "id") String id) {
System.out.println("进入getInfo服务");
User user = new User();
user.setUsername("tom");
return user;
}
}
public class User {
public interface UserSimpleView {};
public interface UserDetailView extends UserSimpleView {};
private String id;
//@MyConstraint(message = "这是一个测试")
//@ApiModelProperty(value = "用户名")
private String username;
@NotBlank(message = "密码不能为空")
private String password;
@Past(message = "生日必须是过去的时间")
private Date birthday;
@JsonView(UserSimpleView.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;
}
@JsonView(UserSimpleView.class)
public String getId() {
return id;
}
public void setId(String id) {
this.id = id;
}
@JsonView(UserSimpleView.class)
public Date getBirthday() {
return birthday;
}
public void setBirthday(Date birthday) {
this.birthday = birthday;
}
}
代码重构
使用RequestMapping变体简化代码
@RequestMapping("/user")
public class UserController {
@GetMapping
public List query(){}
@GetMapping("/{id:\\d+}")
public User getInfo(){}
}
/**
* 使用RESTful API 创建一个用户
* @throws Exception
*/
@Test
public void whenCreateSuccess() throws Exception {
Date date = new Date();
System.out.println(date.getTime());
String content = "{\"username\":\"tom\",\"password\":null,\"birthday\":"+date.getTime()+"}";
String reuslt = mockMvc.perform(MockMvcRequestBuilders.post("/user").contentType(MediaType.APPLICATION_JSON_UTF8)
.content(content))
.andExpect(MockMvcResultMatchers.status().isOk())
.andExpect(MockMvcResultMatchers.jsonPath("$.id").value("1"))
.andReturn().getResponse().getContentAsString();
System.out.println(reuslt);
}
/**
* 通过post请求创建用户
* @ RequestBody:若请求数据是以json字符串格式数据传输,参数无法封装到方法中的参数
* 使用requestBody可以解析json格式为方法体中的参数赋值
* @ Valid 验证、校验参数
* @param errors 校验错误信息存储器,与@Valid注解成对出现
* 若校验不接收错误信息,则不会进入方法体,并且返回错误到请求处
* 若已接受错误信息,则进入方法体,完成方法体内容
*/
@PostMapping
//@ApiOperation(value = "创建用户")
public User create(@Valid @RequestBody User user,BindingResult errors) {
if(errors.hasErrors()){//是否有错误校验信息
//流化并且打印错误信息
errors.getAllErrors().stream().forEach(error -> System.out.println(error.getDefaultMessage()));
}
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;
}
@NotBlank(message = "密码不能为空")
private String password;
/**
* 测试更新请求
* 请求方式put请求
* @throws Exception
*/
@Test
public void whenUpdateSuccess() throws Exception {
Date date = new Date(LocalDateTime.now().plusYears(1).atZone(ZoneId.systemDefault()).toInstant().toEpochMilli());
System.out.println(date.getTime());
String content = "{\"id\":\"1\", \"username\":\"tom\",\"password\":null,\"birthday\":"+date.getTime()+"}";
String reuslt = mockMvc.perform(MockMvcRequestBuilders.put("/user/1").contentType(MediaType.APPLICATION_JSON_UTF8)
.content(content))
.andExpect(MockMvcResultMatchers.status().isOk())
.andExpect(MockMvcResultMatchers.jsonPath("$.id").value("1"))
.andReturn().getResponse().getContentAsString();
System.out.println(reuslt);
}
/**
* 请求方式Delete
*/
@Test
public void whenDeleteSuccess() throws Exception {
mockMvc.perform(MockMvcRequestBuilders.delete("/user/1")
.contentType(MediaType.APPLICATION_JSON_UTF8))
.andExpect(MockMvcResultMatchers.status().isOk());
}
@PutMapping("/{id:\\d+}")
public User update(@Valid @RequestBody User user, BindingResult errors) {
if (errors.hasErrors()){
errors.getAllErrors().stream().forEach(error -> {
FieldError fieldError = (FieldError)error;
String message = fieldError.getField() + " " + fieldError.getDefaultMessage();
System.out.println(message);
});
}
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;
}
/**
* Delete请求
* @param id
*/
@DeleteMapping("/{id:\\d+}")
public void delete(@PathVariable String id) {
System.out.println(id);
}
@NotBlank(message = "密码不能为空")
private String password;
@Past(message = "生日必须是过去的时间")
private Date birthday;
MyConstrain.java
/**
* 自定义校验注解
*
* 自定义校验器要声明以下必须的三个方法
*/
@Target({ElementType.METHOD, ElementType.FIELD})//使用在方法和字段上
@Retention(RetentionPolicy.RUNTIME)//运行时注解
@Constraint(validatedBy = MyConstraintValidator.class)//表名此注解用于校验 validatedBy:当前注解,用哪一个类去校验
public @interface MyConstraint {
/**
* 错误信息
*/
String message();
Class>[] groups() default { };
Class extends Payload>[] payload() default { };
}
MyConstraintValidator.java
/**
* ConstraintValidator
* A:使用哪个注解
* B:验证类型。如String,那么注解就只有String类型上起作用
* 在类结构上,不需要声明为spring管理的类
* 原因:spring扫描到 ConstraintValidator 接口, 自动将其实现类为自己管理的bean
*/
public class MyConstraintValidator implements ConstraintValidator<MyConstraint, Object> {
/**
* 在校验器中,可以注入spring容器中的任何Bean
*/
@Autowired
private HelloService helloService;
/**
* 校验器初始化工作
* @param constraintAnnotation
*/
@Override
public void initialize(MyConstraint constraintAnnotation) {
System.out.println("my validator init");
}
/**
* 真正的校验逻辑
* @param value 需要校验的值
* @param context 验证器上下文
* @return
*/
@Override
public boolean isValid(Object value, ConstraintValidatorContext context) {
helloService.greeting("tom");//模拟校验逻辑
System.out.println(value);
//return false; //失败
return true; //校验成功
}
}
Whitelabel Error Page
This application has no explicit mapping for /error, so you are seeing this as a fallback.
Tue Oct 03 18:46:23 CST 2017
There was an unexpected error (type=Not Found, status=404).
No message available
{
"timestamp": 1507028069711,
"status": 404,
"error": "Not Found",
"message": "No message available",
"path": "/xxx"
}
浏览器发出的请求
Request Headers:
Accept:text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8
非浏览器请求
Request Headers:
Accept:*/*
源码分析
@Controller //本身就是一个控制器
@RequestMapping({"${server.error.path:${error.path:/error}}"})//处理error请求
public class BasicErrorController extends AbstractErrorController {
/*
* 以下两个方法:
* 根据请求信息不同,返回结果不同
*/
@RequestMapping(produces = {"text/html"})
public ModelAndView errorHtml(HttpServletRequest request, HttpServletResponse response) {
HttpStatus status = this.getStatus(request);
Map model = Collections.unmodifiableMap(this.getErrorAttributes(request, this.isIncludeStackTrace(request, MediaType.TEXT_HTML)));//返回一个HTML
response.setStatus(status.value());
ModelAndView modelAndView = this.resolveErrorView(request, response, status, model);
return modelAndView == null?new ModelAndView("error", model):modelAndView;
}
//返回map转为json数据
@RequestMapping
@ResponseBody //将map集合输出为json数据
public ResponseEntity
默认情况下,spring boot默认的处理机制已经是可以满足我们一般的开发目标,若有特定需求,可以自定义处理方案
定义浏览器返回的错误处理资源
在资源目录下,创建resources/error
目录,根据状态码命名错误页面,自定义错误页面的内容
定义非浏览器(APP)返回的错误处理
UserNotExistException.java
/**
* 自定义异常
*/
public class UserNotExistException extends RuntimeException {
private static final long serialVersionUID = -6112780192479692859L;
private String id;
public UserNotExistException(String id) {
super("user not exist");
this.id = id;
}
public String getId() {return id; }
public void setId(String id) {this.id = id;}
}
@GetMapping("/{id:\\d+}")
public User getInfo(@PathVariable(name = "id") String id) {
throw new UserNotExistException("1");//测试异常处理
}
{
"timestamp": 1507034846064,
"status": 500,
"error": "Internal Server Error",
"exception": "vic.tjs.Exception.UserNotExistException",
"message": "user not exist",
"path": "/user/1"
}
/**
* @ControllerAdvice:处理其他控制器抛出的异常,不处理http请求,只负责处理控制器抛出的异常
* @ExceptionHandler:当任何一个控制器,抛出指定的异常,都会执行该注解下的方法进行处理
*/
@ControllerAdvice
public class ControllerExceptionHandler {
@ExceptionHandler(UserNotExistException.class)
@ResponseBody //返回的map集合转成json
@ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR)//返回的htto状态码
public Map handleUserNotExistException(UserNotExistException ex) {
Map result = new HashMap<>();
result.put("id", ex.getId());
result.put("message", ex.getMessage());
return result;
}
}
{
"id": "1",
"message": "user not exist"
}
/**
* 自定义过滤器,拦截所有服务,并且计算服务时长
*/
@Component //声明为spring管理的组件才能识别此类为过滤器
public class TimeFilter implements Filter {
@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); //整个服务方法就在过滤器中执行
System.out.println("time filter 耗时:"+ (new Date().getTime() - start));
System.out.println("time filter finish");
}
}
访问服务,console输出
time filter start
进入getInfo服务
time filter 耗时:240
time filter finish
time filter start
time filter 耗时:10
time filter finish
取消组件
//@Component //声明为spring管理的组件才能识别此类为过滤器
public class TimeFilter implements Filter {}
配置类配置
/**
* 配置类
*/
@Configuration
public class WebConfig{
@Bean
public FilterRegistrationBean timeFilter() {
FilterRegistrationBean registrationBean = new FilterRegistrationBean();
TimeFilter timeFilter = new TimeFilter();
registrationBean.setFilter(timeFilter);
List urls = new ArrayList<>();
urls.add("/*");
registrationBean.setUrlPatterns(urls);
return registrationBean;
}
}
由于Filter
是在J2EE
规范定义的,未知sping
相关的任何东西,只能获取request
和response
请求,无法获取请求的controller
信息和哪个方法处理请求的
/**
* 服务时间记录拦截器
*/
@Component
public class TimeInterceptor implements HandlerInterceptor {
/**
* 服务方法执行前 调用
* @param handler 服务控制器
* @return
* @throws Exception
*/
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler)
throws Exception {
System.out.println("preHandle");
//获得服务的控制器的信息
System.out.println(((HandlerMethod)handler).getBean().getClass().getName());
System.out.println(((HandlerMethod)handler).getMethod().getName());
request.setAttribute("startTime", new Date().getTime());
return true; //是否放行
}
/**
* 服务方法执行完后 调用
* 若服务方法抛出异常,则此方法不会被调用
* @param handler
* @param modelAndView
* @throws Exception
*/
@Override
public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler,
ModelAndView modelAndView) throws Exception {
System.out.println("postHandle");
Long start = (Long) request.getAttribute("startTime");
System.out.println("time interceptor 耗时:"+ (new Date().getTime() - start));
}
/**
* 不管服务方法是正常完成还是抛出异常,此方法都会调用
* @param handler
* @param exception
* @throws Exception
*/
@Override
public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception exception)
throws Exception {
System.out.println("afterCompletion");
Long start = (Long) request.getAttribute("startTime");
System.out.println("time interceptor 耗时:"+ (new Date().getTime() - start));
System.out.println("exception is "+ exception);
}
}
/**
* 配置类
*/
@Configuration
public class WebConfig extends WebMvcConfigurerAdapter {
@Autowired
private TimeInterceptor timeInterceptor; //注入拦截器
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(timeInterceptor);
}
……
}
time filter start -- doFilter
preHandle -- preHandle
vic.tjs.web.controller.UserController -- preHandle
query -- preHandle
postHandle -- postHandle
time interceptor 耗时:167 -- postHandle
afterCompletion -- afterCompletion
time interceptor 耗时:167
exception is null
time filter 耗时:170 -- doFilter
time filter finish
设置异常点
@GetMapping("/{id:\\d+}")
public User getInfo(@PathVariable(name = "id") String id) {
throw new UserNotExistException("1");//测试异常处理
}
异常输出
time filter start
preHandle
vic.tjs.web.controller.UserController
getInfo
-- 没有执行postHandle
afterCompletion
time interceptor 耗时:30
exception is null
time filter 耗时:40
time filter finish
疑问?exception为null?
解释:exception为空是因为之前配置中把异常处理了,自定义的异常控制处理器 比 拦截器中的afterCompletion 先执行,把异常处理完后才调用拦截器,因此不会获得异常信息
@ControllerAdvice
public class ControllerExceptionHandler {
@ExceptionHandler(UserNotExistException.class)
public Map handleUserNotExistException(UserNotExistException ex) {}
解决:
public User getInfo(@PathVariable(name = "id") String id) {
throw new RuntimeException("user not exist"); }
time filter start
preHandle
vic.tjs.web.controller.UserController
getInfo
afterCompletion
time interceptor 耗时:0
exception is java.lang.RuntimeException: user not exist
………
//拦截器会拦截所有的控制器
//以下的spring 异常处理控制器也被拦截了
preHandle
org.springframework.boot.autoconfigure.web.BasicErrorController
error
postHandle
time interceptor 耗时:0
afterCompletion
time interceptor 耗时:0
exception is null
拦截器Object handle
中的能获取请求的控制器的信息,但无法获取请求的参数的值
DispatcherServlet.java分发请求源码分析
public class DispatcherServlet extends FrameworkServlet {
protected void doService(HttpServletRequest request, HttpServletResponse response){
this.doDispatch(request, response);
}
protected void doDispatch(HttpServletRequest request, HttpServletResponse response){
//此方法是调用拦截器中的preHandle
if(!mappedHandler.applyPreHandle(processedRequest, response)) {
return;
}
//此处真正的调用控制器,方法参数的拼装在此处进行,所以拦截器无法获取参数信息
mv = ha.handle(processedRequest, response, mappedHandler.getHandler());
if(asyncManager.isConcurrentHandlingStarted()) {
return;
}
}
}
在pom.xml
中添加aop支持
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-aopartifactId>
dependency>
@Aspect //定义一个切片类
@Component
public class TimeAspect {
/**
* Around:什么时候执行
* execution:在哪些符合条件的方法上执行
* @param pjp 当前被拦截的方法信息对象
* @return
*/
@Around("execution(* vic.tjs.web.controller.UserController.*(..))")
public Object handleControllerMethod(ProceedingJoinPoint pjp) throws Throwable {
System.out.println("time aspect start");
//获取被拦截的控制器的参数
Object[] args = pjp.getArgs();
for (Object arg : args) {
System.out.println("arg is "+arg);
}
long start = new Date().getTime();
Object object = pjp.proceed(); //很像Filter中的doFilter,得到的结果就是控制器的返回值
System.out.println("time aspect 耗时:"+ (new Date().getTime() - start));
System.out.println("time aspect end");
return object;
}
}
控制台输出
time filter start #过滤器
preHandle # 拦截器
vic.tjs.web.controller.UserController$$EnhancerBySpringCGLIB$$678cdc94 #拦截器
getInfo #拦截器
time aspect start # 切片
arg is 1 #切片
进入getInfo服务 #服务
time aspect 耗时:3 #切片
time aspect end # 切片
postHandle # 拦截器
time interceptor 耗时:83 # 拦截器
afterCompletion #拦截器
time interceptor 耗时:83
exception is null
time filter 耗时:93 #过滤器
time filter finish #过滤器
切片不能像Filter和Interceptor能获取http请求与响应
总结:这三种拦截方式各有各好处,应根据需求选取其中一种
执行顺序:
<dependency>
<groupId>commons-iogroupId>
<artifactId>commons-ioartifactId>
dependency>
/**
* 模拟客户端实现文件上传
* @throws Exception
*/
@Test
public void whenUploadSuccess() throws Exception {
String result = mockMvc.perform(MockMvcRequestBuilders.fileUpload("/file")
.file(new MockMultipartFile("file", "test.txt", "multipart/form-data", "hello upload".getBytes("UTF-8"))))
.andExpect(MockMvcResultMatchers.status().isOk())
.andReturn().getResponse().getContentAsString();
System.out.println(result);
}
/**
* 文件上传处理控制器
*/
@RestController
@RequestMapping("/file")
public class FileController {
//上传到本地
private String folder = "D:\\idea_location\\vicsecuritydemo\\src\\main\\java\\vic\\tjs\\web\\controller
@PostMapping
public FileInfo upload(MultipartFile file) throws Exception {
//上传文件的信息
System.out.println(file.getName());
System.out.println(file.getOriginalFilename());
System.out.println(file.getSize());
File localFile = new File(folder, new Date().getTime() + ".txt");
file.transferTo(localFile);
return new FileInfo(localFile.getAbsolutePath());
}
}
注:这里的FileInfo
只是一个自定义的简单实体类,用来记录文件位置
@GetMapping("/{id}")
public void download(@PathVariable String id, HttpServletRequest request, HttpServletResponse response) throws Exception {
/**
* jdk 7 语法,将流声明在try()中,调用完后可以自动关闭对应的流,简化开发
*/
try (InputStream inputStream = new FileInputStream(new File(folder, id + ".txt"));
OutputStream outputStream = response.getOutputStream();) {
response.setContentType("application/x-download");
response.addHeader("Content-Disposition", "attachment;filename=test.txt");
IOUtils.copy(inputStream, outputStream);
outputStream.flush();
}
}
副线程由主线程唤起
@RestController
public class AsyncController {
private Logger logger = LoggerFactory.getLogger(getClass());
@RequestMapping("/order")
public Callable order(){
logger.info("主线程开始");
Callable result = new Callable() {
@Override
public String call() throws Exception {
logger.info("副线程开始");
Thread.sleep(5000);
logger.info("副线程返回");
return "success";
}
};
logger.info("执行其他事情 模拟 1 ");
//Thread.sleep(1000);
logger.info("执行其他事情 模拟 2 ");
//Thread.sleep(1000);
logger.info("主线程结束");
//Thread.sleep(1000);
return result;
}
}
补充:Callable与Runnable区别
测试:开两个浏览器客户端同时发起请求,得到的结果是:
2018-03-16 20:45:06.860 INFO 8276 --- [p-nio-80-exec-1] vic.tjs.web.async.AsyncController : 主线程开始
2018-03-16 20:45:06.860 INFO 8276 --- [p-nio-80-exec-1] vic.tjs.web.async.AsyncController : 执行其他事情 模拟 1
2018-03-16 20:45:06.860 INFO 8276 --- [p-nio-80-exec-1] vic.tjs.web.async.AsyncController : 执行其他事情 模拟 2
2018-03-16 20:45:06.860 INFO 8276 --- [p-nio-80-exec-1] vic.tjs.web.async.AsyncController : 主线程结束
2018-03-16 20:45:06.860 INFO 8276 --- [ MvcAsync4] vic.tjs.web.async.AsyncController : 副线程开始
2018-03-16 20:45:07.848 INFO 8276 --- [p-nio-80-exec-2] vic.tjs.web.async.AsyncController : 主线程开始
2018-03-16 20:45:07.848 INFO 8276 --- [p-nio-80-exec-2] vic.tjs.web.async.AsyncController : 执行其他事情 模拟 1
2018-03-16 20:45:07.848 INFO 8276 --- [p-nio-80-exec-2] vic.tjs.web.async.AsyncController : 执行其他事情 模拟 2
2018-03-16 20:45:07.848 INFO 8276 --- [p-nio-80-exec-2] vic.tjs.web.async.AsyncController : 主线程结束
2018-03-16 20:45:07.850 INFO 8276 --- [ MvcAsync5] vic.tjs.web.async.AsyncController : 副线程开始
2018-03-16 20:45:11.860 INFO 8276 --- [ MvcAsync4] vic.tjs.web.async.AsyncController : 副线程返回
2018-03-16 20:45:12.852 INFO 8276 --- [ MvcAsync5] vic.tjs.web.async.AsyncController : 副线程返回
疑问:副线程的执行都是在return时返回结果?测试一下将服务中的的返回结果改为return null;
得到如下结果
2018-03-16 20:51:01.144 INFO 7668 --- [p-nio-80-exec-2] vic.tjs.web.async.AsyncController : 主线程开始
2018-03-16 20:51:01.144 INFO 7668 --- [p-nio-80-exec-2] vic.tjs.web.async.AsyncController : 执行其他事情 模拟 1
2018-03-16 20:51:01.144 INFO 7668 --- [p-nio-80-exec-2] vic.tjs.web.async.AsyncController : 执行其他事情 模拟 2
2018-03-16 20:51:01.144 INFO 7668 --- [p-nio-80-exec-2] vic.tjs.web.async.AsyncController : 主线程结束
从上面的结果,可以清楚的看到,Callable线程就是在return时启动执行的。也就是说,主线程并未结束,而是唤起了副线程,并且副线程执行返回的结果还是交给主线程,并由主线程将结果返回,从这一点上,让我对异步这个概念混淆不清了。那跟单线程就没什么区别了。
备注:有时间深入了解一下这个东西,毕竟知识有限
/**
* 模拟下单队列
*/
@Component
public class MockQueue {
private String placeOrder;
private String completeOrder;
private Logger logger = LoggerFactory.getLogger(getClass());
public String getPlaceOrder() { return placeOrder; }
public void setPlaceOrder(String placeOrder) throws Exception {
new Thread(() -> {
logger.info("接到下单请求, " + placeOrder);
try {
Thread.sleep(1000);
} catch (Exception e) {
e.printStackTrace();
}
this.completeOrder = placeOrder;
logger.info("下单请求处理完毕," + placeOrder);
}).start();
}
public String getCompleteOrder() {return completeOrder; }
public void setCompleteOrder(String completeOrder) {this.completeOrder = completeOrder; }
}
@Component
public class DeferredResultHolder {
//key:订单号,value:订单处理结果
private Map> map = new HashMap>();
public Map> getMap() {
return map;
}
public void setMap(Map> map) {
this.map = map;
}
}
@RestController
public class AsyncController {
@Autowired
private MockQueue mockQueue;
@Autowired
private DeferredResultHolder deferredResultHolder;
private Logger logger = LoggerFactory.getLogger(getClass());
@RequestMapping("/order")
public DeferredResult order() throws Exception {
logger.info("主线程开始");
//随机生成订单号
String orderNumber = RandomStringUtils.randomNumeric(8);
mockQueue.setPlaceOrder(orderNumber);//设置订单号
DeferredResult result = new DeferredResult<>();
deferredResultHolder.getMap().put(orderNumber, result);
return result;
}
}
/**
* 监听下单,返回Result
*/
@Component
public class QueueListener implements ApplicationListener<ContextRefreshedEvent> {
@Autowired
private MockQueue mockQueue;
@Autowired
private DeferredResultHolder deferredResultHolder;
private Logger logger = LoggerFactory.getLogger(getClass());
@Override
public void onApplicationEvent(ContextRefreshedEvent event) {
new Thread(() -> {
while (true) {
if (StringUtils.isNotBlank(mockQueue.getCompleteOrder())) {
String orderNumber = mockQueue.getCompleteOrder();
logger.info("返回订单处理结果:"+orderNumber);
deferredResultHolder.getMap().get(orderNumber).setResult("place order success");//当DeferredResult.setResult被执行,就意味着异步执行完毕需要向浏览器返回向信息
mockQueue.setCompleteOrder(null);
}else{
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}).start();
}
}
2017-10-04 14:00:44.512 INFO 6976 --- [nio-8080-exec-1] vic.tjs.web.async.AsyncController : 主线程开始
2017-10-04 14:00:44.516 INFO 6976 --- [ Thread-10] vic.tjs.web.async.MockQueue : 接到下单请求, 75192809
2017-10-04 14:00:45.517 INFO 6976 --- [ Thread-10] vic.tjs.web.async.MockQueue : 下单请求处理完毕,75192809
2017-10-04 14:00:45.597 INFO 6976 --- [ Thread-7] vic.tjs.web.async.QueueListener : 返回订单处理结果:75192809
place order success
@Configuration
public class WebConfig extends WebMvcConfigurerAdapter {
/**
* 配置异步支持
* 需要支持异步的拦截器都需要配在这里,支持异步的拦截器和addInterceptors不一样
* @param configurer
*/
@Override
public void configureAsyncSupport(AsyncSupportConfigurer configurer) {
//super.configureAsyncSupport(configurer);
configurer.registerCallableInterceptors(...); //支持Callable的拦截器
configurer.registerDeferredResultInterceptors(..);//支持DeferredResult的拦截器
configurer.setDefaultTimeout(..);//异步请求的默认超时时间
configurer.setTaskExecutor(..);//设置可重用的线程池
}
}
目前很多项目都是前后端分离,并且前后端开发并行工作,后端人员手动编写文档,效率低而且难度大,并且在修改代码的同时,也需要修改文档。使用文档自动生成可以解决以上这些缺点,并且很好的使前端开发人员容易的调用所有的RESTful服务
pom.xml
中添加依赖
<dependency>
<groupId>io.springfoxgroupId>
<artifactId>springfox-swagger2artifactId>
<version>2.7.0version>
dependency>
<dependency>
<groupId>io.springfoxgroupId>
<artifactId>springfox-swagger-uiartifactId>
<version>2.7.0version>
dependency
import springfox.documentation.swagger2.annotations.EnableSwagger2;
@EnableSwagger2
public class StartApplication{}
/swagger-ui.html
@ApiOperation
注解声明在方法中
@ApiOperation(value = "创建用户")
public User create(@Valid @RequestBody User user,BindingResult errors){}
@ApiParam
注解声明在方法参数中
public void delete(@ApiParam(value = "用户id") @PathVariable String id){}
@ApiModelProperty
注解声明在对象属性中
public class User {
@ApiModelProperty(value = "用户名")
private String username;
}
一般开发项目都会分模块进行,比如都会把前端和后端分开,在前端和后端里面也通常是分模块开发的。当开发进度不一致时,可以对依赖接口构建Mock Service,模拟不同输入/数据/场景,这样不至于影响本模块的开发进度。
http://wiremock.org/docs/running-standalone/
将下载后的wiremock-standalone-2.14.0.jar
运行,可以参考官方文档进行参数设置
该文件目录下执行命令: java -jar wiremock-standalone-2.8.0.jar - -port 8020
# 其他参数 - - verbose 开启日志
项目中使用
<dependency>
<groupId>com.github.tomakehurstgroupId>
<artifactId>wiremockartifactId>
dependency>
<!— wiremock需要依赖http工具类 -->
<dependency>
<groupId>org.apache.httpcomponentsgroupId>
<artifactId>httpmimeartifactId>
dependency>
/**
* WireMock 服务器搭建
*/
public class MockServer {
/**
* 使用静态引入WireMock,直接调用方法
*/
public static void main(String[] args) throws IOException {
configureFor(8020);
removeAllMappings();
mock("/order/1", "01");
mock("/order/2", "02");
}
private static void mock(String url, String file) throws IOException {
ClassPathResource resource = new ClassPathResource("vic/response/" + file + ".txt");
String content = StringUtils.join(FileUtils.readLines(resource.getFile(), "UTF-8").toArray(), "\n");
stubFor(get(urlPathEqualTo(url)).willReturn(aResponse().withBody(content).withStatus(200)));
}
}
在资源目录下创建文件:vic/response/01.txt
{
"id":1,
"type":"C"
"data" : {
"name" : "victorys",
"age" : 22,
"sex" : 0
"interest" : {
"0" : "basketball",
"1" : "swingming"
}
}
}
通过访问/order/1或得返回结果