admin管理员组

文章数量:1574550

1. Web 开发

1.1 jar 入门案例

基于 war 的调试、部署都较为麻烦,因此 Spring 官方也建议,采用 jar 方式来开发 web 项目,好处有:

  1. 仍然通过 main 方法测试运行,开发方便
  2. 打包打成 jar 包,内嵌 tomcat,无需再安装 tomcat 服务器
  3. 代价是不再支持 jsp

步骤1:创建模块,打包方式选择 jar

勾选 Spring Web 支持,这与之前一样

步骤2:编写控制器

@Controller
public class MyController {

    @RequestMapping("/hello")
    @ResponseBody
    public String abc() {
        System.out.println("进入了控制器");
        return "Hello, Spring Boot";
    }
}

与前面例子不同的是:

  • 新加 @ResponseBody 注解,它的含义是不再去寻找 jsp 视图,而是把控制器方法的返回结果直接作为响应体

步骤3:运行引导类的 main 方法启动程序

步骤4:打开浏览器,输入如下地址访问控制器方法

http://localhost:8080/hello

可以看到,少了 jsp 的拖累,开发的简捷性大大提升

注意

  • 作为了解,底层请求都会进入一个统一入口 DispatcherServlet

1.2 进阶

⭐️1) 路径映射

@RequestMapping 用来映射请求路径,除了可以加在方法上以外,还可以同时加在类上,如果类和方法都加了 @RequestMapping,那么最终的请求路径由二者共同决定

例如

@Controller
@RequestMapping("/person")
public class PersonController {

    private static final Logger log = LoggerFactory.getLogger(PersonController.class);

    @RequestMapping("/save")
    @ResponseBody
    public String save() {
        log.debug("save");
        return "Person Save";
    }

    @RequestMapping("/update")
    @ResponseBody
    public String update() {
        log.debug("update");
        return "Person Update";
    }
}
  • 访问新增用户时,请求路径为 /person/save
  • 访问修改用户时,请求路径为 /person/update

当然写成下面也是效果是一样的

@Controller
public class PersonController {

    private static final Logger log = LoggerFactory.getLogger(PersonController.class);

    @RequestMapping("/person/save")
    @ResponseBody
    public String save() {
        log.debug("save");
        return "Person Save";
    }

    @RequestMapping("/person/update")
    @ResponseBody
    public String update() {
        log.debug("update");
        return "Person Update";
    }
}

注意

  • spring mvc 中的路径映射不如 servlet 那么严格,路径前不加 / 也不会报错
让改动快速生效

加入依赖后

<dependency>
	<groupId>org.springframework.boot</groupId>
	<artifactId>spring-boot-devtools</artifactId>
	<optional>true</optional>
</dependency>

可以在类或配置改变后,无需重新启动程序,devtools 会利用 restart 技术让改动快速生效。idea 中触发 restart 的方法如图所示

注解简化

注意到每个方法都需要加 @ResponseBody,这里可以简化为

@RestController
@RequestMapping("/person")
public class PersonController {

    private static final Logger log = LoggerFactory.getLogger(PersonController.class);

    @RequestMapping("/save")
    public String save() {
        log.debug("save");
        return "Person Save";
    }

    @RequestMapping("/update")
    public String update() {
        log.debug("update");
        return "Person Update";
    }
}

原理是 @RestController 是一个组合注解,同时含有 @Controller 与 @ResponseBody

@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Controller
@ResponseBody
public @interface RestController {

	// ...

}
调整端口号

可以调整 Spring Boot 内嵌服务器的端口号

server.port=端口号

⭐️2) 静态资源映射

如果希望使用 html、css、js、图片这样的静态资源,可以把它们放在下面目录结构中的 static 目录下

src
	|- main
		|- java
		|- resources
			|- static (此处)

除此以外,静态资源放在下面一些目录,也能够被 Spring Boot 所找到

src
	|- main
		|- java
		|- resources
			|- static (此处)
			|- public (此处)
			|- resources (此处)
			|- META-INF
				|- resources (此处)

比如浏览器中输入 http://localhost:8080/aaa.html 这个静态资源路径,Spring 会在下面的位置进行搜索

  • src/main/resources/static/ 下找有没有 aaa.html
  • src/main/resources/public/ 下找有没有 aaa.html
  • src/main/resources/resources/ 下找有没有 aaa.html
  • src/main/resources/META-INF/resources/ 下找有没有 aaa.html

再比如浏览器中输入 http://localhost:8080/bbb/aaa.html 这个静态资源路径,Spring 会在下面的位置进行搜索

  • src/main/resources/static/ 下先找 bbb 这个目录,再找内部有没有 aaa.html
  • src/main/resources/public/ 下先找 bbb 这个目录,再找内部有没有 aaa.html
  • src/main/resources/resources/ 下先找 bbb 这个目录,再找内部有没有 aaa.html
  • src/main/resources/META-INF/resources/ 下先找 bbb 这个目录,再找内部有没有 aaa.html
自定义静态资源映射

如果以上位置都不能满足你、或是 url 路径与资源路径不一致,需要编写如下代码

@SpringBootApplication
public class Demo2Application implements WebMvcConfigurer {

    public static void main(String[] args) {
        SpringApplication.run(Demo2Application.class, args);
    }

    @Override
    public void addResourceHandlers(ResourceHandlerRegistry registry) {
        registry.addResourceHandler("/image/**").addResourceLocations("classpath:/pic/");
    }
}

这样当浏览器中输入 http://localhost:8080/image/ 开头的静态资源时

  • 就会到 src/main/resources/pic 下去查找同名资源
  • classpath: 表示类路径,对 maven 项目而言,对应 src/main/resources/
  • 也可以映射到磁盘路径,例如:file:d:/pic/

⭐️3) 处理请求参数

例1

只要请求参数名与方法参数名一一对应,将来请求参数就会被填充至方法参数,并且支持常见的数据类型转换,非常方便

@RestController
public class ParameterController {
    private static final Logger log = LoggerFactory.getLogger(ParameterController.class);
    @RequestMapping("/param/r1")
    public String r1(Integer id, String username) {
        String format = String.format("编号:%s 用户名:%s", id, username);
        log.debug(format);
        return format;
    }
}

浏览器通过地址栏发送 GET 请求

http://localhost:8081/param/r1?id=1&username=zhang

输出

[DEBUG] 09:12:22.544 [http-nio-8081-exec-19] c.i.c.c.ParameterController - 编号:1 用户名:zhang

但要注意,如果类型转换出现错误,在没有配置异常处理器时,会出现 400 响应

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-BTGL3y63-1648028816996)(img/image-20211021174021318.png)]

例2

当请求参数较多时,例如:

如果定义同样数量的方法参数显得非常繁琐,这时可以改用对象接收,请求参数名对象属性名对应

@RestController
public class ParameterController {
    // ...
    @RequestMapping("/param/r2")
    public String r2(User user) {
        String format = String.format("%s", user);
        log.debug(format);
        return format;
    }
}

其中 User 对象

public class User {
    private Integer id;
    private String username;
    private Integer[] luckyNumber;
    
    // 省略 get set toString
}

多值参数

<input type="checkbox" name="luckyNumber" value="1"/>1
<input type="checkbox" name="luckyNumber" value="2"/>2
<input type="checkbox" name="luckyNumber" value="3"/>3

java 这边可以用数组属性或 List 集合属性来接收

例3

还有一个比较有用的注解 @RequestParam 可以用来接收请求参数的默认值,代码

@RestController
public class ParameterController {
    // ...
    @RequestMapping("/param/r3")
    public String r3(
            @RequestParam(defaultValue = "1") Integer page,
            @RequestParam(defaultValue = "10") Integer size) {
        String format = String.format("页号:%s 每页记录数:%s", page, size);
        log.debug(format);
        return format;
    }
}

4) 文件上传

请求体格式区别

对于 post 请求,其请求体的数据格式可以有多种,常见的有

  • application/x-www-form-urlencoded
  • multipart/form-data
  • application/json(暂时放一下,后面有例子)

那么它们的区别是什么呢?
表单1

<form enctype="application/x-www-form-urlencoded" method="post" action="/upload">
    <input type="text" name="username"/>
    <input type="file" name="file"/>
    <input type="submit" value="提交">
</form>

表单2

<form enctype="multipart/form-data" method="post" action="/upload">
    <input type="text" name="username"/>
    <input type="file" name="file"/>
    <input type="submit" value="提交">
</form>

填写数据一样:

  • text 框都填写 zhangsan
  • file 框都选择 1.txt 文件,其内容为
hello

用 firefox 浏览器(推荐,因为能看到更详细的请求信息),打开 Web 开发者->网络 面板,分别提交这两个表单(先不管错误,只看请求参数)

表单1结果 - 可以看到格式与 get 请求参数格式一样,都是 名1=值1&名2=值2

表单2结果 - 可以看到表单由一个分隔线划分成了多个部分(这也是 multipart 的由来),可以包含更多的信息

接收 multipart/form-data 数据

对于 application/x-www-form-urlencoded,我们前面的例子中编写的例子都是。那么该如何接收 multipart/form-data 格式的数据呢?

@RestController
public class UploadController {

    private static final Logger log = LoggerFactory.getLogger(UploadController.class);

    @RequestMapping("/upload")
    public String upload(String username, MultipartFile file) throws IOException {
        if (file.isEmpty()) {
            return "未选中文件";
        }

        String filename = file.getOriginalFilename();
        log.debug("获取原始文件名:{}", filename);

        log.debug("文件的后缀名:{}", FilenameUtils.getExtension(filename));
        log.debug("文件大小:{}", file.getSize());
        log.debug("文件类型:{}", file.getContentType());
        log.debug("其他参数:{}", username);

        file.transferTo(Paths.get("d:\\", filename));

        return "上传成功";
    }

}

其中 FilenameUtils 是工具类,用来获取文件扩展名(非必须,可以自己写),需要导入下面依赖

<dependency>
    <groupId>commons-io</groupId>
    <artifactId>commons-io</artifactId>
    <version>2.11.0</version>
</dependency>

5) header 与 cookie 处理

@RestController
public class HeaderCookieController {

    private static final Logger log = LoggerFactory.getLogger(HeaderCookieController.class);

    @RequestMapping("/headerCookie")
    public String getHeaderAndCookie(
            @RequestHeader("Accept-Language") String header,
            @CookieValue(value = "aaa",required = false) String cookie) {
        String format = String.format("header:%s, cookie:%s", header, cookie);
        log.debug(format);
        return format;
    }

}

@RequestHeader 与 @CookieValue 分别可以用来获得请求头与 Cookie 的信息

在浏览器中可以手动设置 cookie:

document.cookie="aaa=abc;domain=localhost"
location.reload();

注意

  • 如果对 js 不熟悉,可以改用 postman 来发送自定义 cookie

6) request,response,session 处理

如果要用到 Servlet API 中的 request,response,session 对象,可以把它们当做控制器方法的参数传入

@RestController
public class ServletObjectController {

    private static final Logger log = LoggerFactory.getLogger(ServletObjectController.class);

    @RequestMapping("/servletObject")
    public String getServletObject(
            HttpServletRequest request,
            HttpServletResponse response,
            HttpSession session) {
        String format = String.format("请求URI: %s,响应状态码: %s,session id: %s",
                request.getRequestURI(), response.getStatus(), session.getId());
        log.debug(format);
        return format;
    }

}

⭐️7) 异常处理

回忆一下以前学习的异常处理原则

  • dao,service 层向上抛即可
  • 但 controller 层不同,它如果再向上抛,此异常必然暴露给最终用户,这是不允许的

处理方式有两种

  • 简单的方式就是自己 try ... catch 在 catch 块中返回合适信息
  • Spring MVC 还提供了另一种处理异常的方式,如下所示:
@RestController
public class StudentController {

    private static final Logger log = LoggerFactory.getLogger(StudentController.class);

    @RequestMapping("/student/save")
    public String save() throws FileNotFoundException {
        log.debug("student save...");
        new FileInputStream("aaa");
        return "Student save";
    }

    @RequestMapping("/student/update")
    public String update() {
        log.debug("student update...");
        int i = 1 / 0;
        return "Student update";
    }

    @ExceptionHandler
    public String ioException(IOException e) {
        log.debug("LocalExceptionHandler ...");
        return "错误为:" + e.getMessage();
    }
}

其中 @ExceptionHanlder 标注的方法,可以在方法参数处声明需要捕获的异常,本例中

  • 访问 /student/save 会走 ioException 方法
  • 访问 /student/update 则不会走 ioException 方法,因为异常的类型匹配不上

控制器内未捕获的异常,还可以通过一个全局通知类来处理

@RestControllerAdvice
public class GlobalExceptionHandler {
    private static final Logger log = LoggerFactory.getLogger(GlobalExceptionHandler.class);

    @ExceptionHandler
    public String globalException(Exception e) {
        log.debug("GlobalExceptionHandler...");
        return "全局错误:" + e.getMessage();
    }

}

注意

  • @RestControllerAdvice 也是组合注解,相当于 @ControllerAdvice + @ResponseBody
  • @ControllerAdvice 标注的类会对所有 Controller 增强,但要注意其底层原理并非 AOP
  • 方法参数处的异常类型为 XxxException 可以匹配
    • 所有 XxxException 以及 XxxException 的子类异常
    • 实际异常对象中,cause 为 XxxException 类型的异常

⭐️8) 拦截器

拦截器会在控制器方法前、后、完成时进行拦截增强,有一点像之前学习过的 Filter 过滤器

使用步骤
  1. 编写目标 - 代码片段1
  2. 编写拦截器代码 - 代码片段2
  3. 配置拦截器 - 代码片段3.1 与 代码片段3.2 二选一

代码片段1

@RestController
public class TeacherController {

    private static final Logger log = LoggerFactory.getLogger(TeacherController.class);

    @RequestMapping("/teacher/save")
    public String save() {
        log.debug("teacher save...");
        return "Teacher save";
    }

    @RequestMapping("/teacher/update")
    public String update() {
        log.debug("teacher update...");
        return "Teacher update";
    }

}

代码片段2

public class TeacherInterceptor implements HandlerInterceptor {

    private static final Logger log = LoggerFactory.getLogger(TeacherInterceptor.class);

    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) {
        log.debug("preHandle...");        
        return true;
    }

    @Override
    public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) {
        log.debug("postHandle...");
    }

    @Override
    public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) {
        log.debug("afterCompletion...");
    }
}

编写要点

  • 拦截器需要实现接口 HandlerInterceptor,但不必实现其全部方法,因为它们默认都是 default 方法
  • preHandle 在控制器方法之前被执行,方法返回 true 表示放行,返回 false 表示拦截
  • postHandle 在控制器方法之后被执行,如果控制器方法执行出错,则不会调用 postHandle
  • afterCompletion 在更后被执行,无论控制器方法执行是否出错,都会调用 afterCompletion

配置要点

  • 在引导类添加较为方便,引导类要实现 WebMvcConfigurer 接口并覆盖其 addInterceptors 方法,其中 registry.addInterceptor(拦截器对象) 用来注册拦截器,并为之设置拦截路径
  • 由于 addInterceptors 方法是接口中定义的,已经规定死了,不能把拦截器通过方法参数传递进来
  • 解决这个问题有两种方法

代码片段3.1:用 @Bean 把拦截器交给 Spring 管理,然后通过方法调用来获得拦截器

@SpringBootApplication
public class Demo2Application implements WebMvcConfigurer {

    // ...

    @Bean
    public TeacherInterceptor teacherInterceptor() {
        return new TeacherInterceptor();
    }

    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        registry.addInterceptor(teacherInterceptor()).addPathPatterns("/teacher/**");
        // 注意与下面代码含义不同
        // * 下面的拦截器是自己 new 的,不受 spring 管理
        // * 而经过 teacherInterceptor() 调用得到的拦截器受到 spring 管理,可以享受 spring 各种特性
        // registry.addInterceptor(new TeacherInterceptor()).addPathPatterns("/teacher/**");
    }
}

代码片段3.2:用 @Component 或 @Bean 把拦截器交给 Spring 管理,然后在引导类中把拦截器当做成员变量注入进来

@SpringBootApplication
public class Demo2Application implements WebMvcConfigurer {

    // ...

    @Bean
    public TeacherInterceptor teacherInterceptor() {
        return new TeacherInterceptor();
    }

    @Autowired
    private TeacherInterceptor teacherInterceptor;

    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        registry.addInterceptor(teacherInterceptor).addPathPatterns("/teacher/**");
    }
}
handler

handler 对象可以获取被拦截对象及方法的信息

public class TeacherInterceptor implements HandlerInterceptor {

    private static final Logger log = LoggerFactory.getLogger(TeacherInterceptor.class);

    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) {
        log.debug("preHandle...");
		log.debug("handler的类型是:" + handler.getClass());
        if(handler instanceof HandlerMethod) {
            HandlerMethod handlerMethod = (HandlerMethod) handler;
            log.debug("拦截的方法所在的类:" + handlerMethod.getBean().getClass());
            log.debug("控制器处理请求的方法名:" + handlerMethod.getMethod().getName());
        }
        return true;
    }

    // ...
}
🈵如何拦截

例如要实现如下功能,username 为 admin 允许访问控制器方法,否则拦截

@RestController
public class UserController {

    private static final Logger log = LoggerFactory.getLogger(UserController.class);

    @RequestMapping("/user/save")
    public String save(String username) {
        log.debug("user save...{}", username);
        return "User save";
    }

    @ExceptionHandler
    public String handler(RuntimeException e) {
        return "错误:" + e.getMessage();
    }
}

方案1 - 在拦截器中抛异常,在控制器里根据异常跳转

public class UserInterceptor1 implements HandlerInterceptor {

    private static final Logger log = LoggerFactory.getLogger(UserInterceptor1.class);

    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) {
        log.debug("preHandle...");
        String username = request.getParameter("username");
        if (!"admin".equals(username)) {
            log.debug("拦截...");
            throw new RuntimeException("没有权限执行此操作,请重新登录");
        }
        log.debug("放行...");
        return true;
    }
}

方案2 - 在拦截器里请求转发,返回 false 拦截

public class UserInterceptor2 implements HandlerInterceptor {

    private static final Logger log = LoggerFactory.getLogger(UserInterceptor2.class);

    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler)
		throws ServletException, IOException {
        log.debug("preHandle...");
        String username = request.getParameter("username");
        if (!"admin".equals(username)) {
            log.debug("拦截...");
            response.setContentType("text/html;charset=utf-8");
            response.getWriter().print("错误:没有权限执行此操作,请重新登录");
            return false;
        }
        log.debug("放行...");
        return true;
    }
}

配置时,拦截器实现类从 UserInterceptor1 和 UserInterceptor2 择一即可

@SpringBootApplication
public class Demo2Application implements WebMvcConfigurer {

    // ...

    @Bean
    public UserInterceptor1 userInterceptor1() {
        return new UserInterceptor1();
    }

    @Bean
    public UserInterceptor2 userInterceptor2() {
        return new UserInterceptor2();
    }

    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        registry.addInterceptor(teacherInterceptor()).addPathPatterns("/teacher/**");
        registry.addInterceptor(userInterceptor2()).addPathPatterns("/user/**");
    }
}

✒️练习 - web

题目见 exercise1 中的 1~6 题

2. RESTful 开发

⭐️2.1 RESTful 风格

RESTful 是一种软件架构风格,它采用了以上提到的要素来构建网络应用程序

  • 设计要访问的资源【名词】,用统一的 URI 表示
  • 选择资源的展现的方式:一般是 json 格式,也可以是其它格式
  • 用 HTTP Method 【动词】来转换资源状态

例如有一本《神雕侠侣》这个资源,它有唯一 id=10 ,这样设计

获取资源,Accept 一般用来指示我想要的格式

GET /api/books/10 HTTP/1.1
Accept: application/json;charset=utf-8

服务器返回,Content-Type 一般用来指示我提供的格式

HTTP/1.1 200 OK 

Content-Type: application/json;charset=utf-8
{
	"id":10,
	"title":"神雕侠侣",
	"author":"金庸"
}

如果想换一种格式,注意资源 URI 不用变!

GET /api/books/10 HTTP/1.1
Accept: application/xml;charset=utf-8

服务器返回

HTTP/1.1 200 OK 
Content-Type: application/xml;charset=utf-8

<book>
	<id>10</id>
	<title>10</title>
	<author>金庸</author>
</book>

更新资源,注意资源 URI 不用变!

PUT /api/books/10 HTTP/1.1
Content-Type: application/json;charset=utf-8

{
	"title":"神雕侠侣",
	"author":"查良镛"
}

删除资源,注意资源 URI 不用变!

DELETE /api/books/10 HTTP/1.1

新增资源,可以这么干:如果希望服务器生成 id,则用 post,否则用 put

POST /api/books HTTP/1.1
Content-Type: application/json;charset=utf-8

{
	"title":"侠客行",
	"author":"金庸"
}

注意

  • RESTful 是一种风格,并没有什么规范强制你【必须】怎么做,所以常常可以看到一些与 RESTful 理念不符的实例,因开发者而不同,此为正常现象

2.2 开发 RESTful 应用

⭐️1) 设计统一 URI

要实现【设计要访问的资源【名词】,用统一的 URI 表示】这一特性,我们发现 RESTful 风格中,唯一标识 id,并不是像之前一样从请求参数(即 ? 后)传递过来,而是此 id 就是路径的组成部分,因此需要方便的办法获取路径中的参数。

Spring 提供了 @PathVariable 来解析资源路径中的参数信息

例1:当路径参数名与请求参数名一致时

@RestController
public class Controller01 {

    private static final Logger log = LoggerFactory.getLogger(Controller01.class);

    @RequestMapping("/test1/{id}")
    public String test1(@PathVariable int id) {
        log.debug("编号:{}", id);
        return "test";
    }
}

其中 {参数名} 代表路径中变化的部分,比如这里的 {id} 就代表设置了一个 id 参数,可以用来接收

  • /test1/100
  • /test1/101
    等路径中的 100、101 这些编号值,SpringMVC 会检查标注了 @PathVariable 的方法参数,将这些值传递给同名参数

测试

http://localhost:8080/test1/100

服务器控制台输出

[DEBUG] 08:36:43.731 [http-bio-8080-exec-8] c.i.c.c.Controller01 - 编号:100 

注意
RESTful 的路径参数作用与普通 ?参数名=参数值 没有两样,都是用来获取请求中的信息。区别仅在于风格格式不同,一种是将信息包含在路径里,一种是将信息跟在 ? 号后

例2:当路径参数名与请求参数名不一致时

@RequestMapping("/test2/{sid}")
public String test2(@PathVariable("sid") int id) {
	log.debug("编号:{}", id);
	return "test";
}

测试

http://localhost:8080/test2/200

服务器输出

[DEBUG] 08:37:47.302 [http-bio-8080-exec-1] c.i.c.c.Controller01 - 编号:200 

例3:路径参数可以有多个

@RequestMapping("/test3/{id}/{name}")
public String test3(@PathVariable int id, @PathVariable String name) {
	log.debug("编号:{} 用户名:{}", id, name);
	return "test";
}

测试

http://localhost:8080/test3/300/zhangsan

服务器输出

[DEBUG] 08:38:33.937 [http-bio-8080-exec-1] c.i.c.c.Controller01 - 编号:300 用户名:zhangsan  

注意
当参数比较多时,仍然建议把信息放在请求体中而不是路径里,路径里一般只放关键信息

⭐️2) 区分请求方法

要实现【用 HTTP Method 【动词】来转换资源状态】就需要更方便的办法按 **请求方法(GET POST 等)**来区分请求,访问资源时,增删改查,路径都是同一个(增可能会有不同),这时就需要对路径加以区分,否则就会有歧义:

@RestController
public class Controller02 {
    private static final Logger log = LoggerFactory.getLogger(Controller02.class);

    @RequestMapping("/users/{id}")
    public void get(@PathVariable Integer id) {
        log.debug("get...");
    }

    @RequestMapping("/users/{id}") // 当然新增一般不需指定 id
    public void post(@PathVariable Integer id) {
        log.debug("post...");
    }
}

启动时会报错

Caused by: java.lang.IllegalStateException: Ambiguous mapping. Cannot map 'controller02' method 
com.itheima.controller.case05.Controller02#post(Integer)
to { /user/{id}}: There is already 'controller02' bean method com.itheima.controller.case05.Controller02#get(Integer) mapped.

显然必须加以区分,怎么区分呢?可以通过 Request 的不同 Method 来区分。其实想想也是正常,RESTful 不就是建议用 Request Method 来区分对资源的增删改查嘛

代码改成下面这样就可以了

@RestController
public class Controller02 {
    private static final Logger log = LoggerFactory.getLogger(Controller02.class);

    @RequestMapping(value = "/users/{id}", method = RequestMethod.GET)
    public void get(@PathVariable Integer id) {
        log.debug("get...");
    }

    @RequestMapping(value = "/users/{id}", method = RequestMethod.POST)
    public void post(@PathVariable Integer id) {
        log.debug("post...");
    }
}

但显然有些繁琐,这里 SpringMVC 为我们提供了几个简化(衍生)注解,相信一看就明白

@RestController
public class Controller02 {
    private static final Logger log = LoggerFactory.getLogger(Controller02.class);

    @GetMapping("/users/{id}")
    public void get(@PathVariable Integer id) {
        log.debug("get...");
    }

    @PostMapping("/users/{id}") 
    public void post(@PathVariable Integer id) {
        log.debug("post...");
    }

    @DeleteMapping("/users/{id}")
    public void delete(@PathVariable Integer id) {
        log.debug("delete...");
    }

    @PutMapping("/users/{id}")
    public void put(@PathVariable Integer id) {
        log.debug("put...");
    }
}

顺便把 @DeleteMapping 和 @PutMapping 也加上了

不过又有了新的问题:浏览器表单可以发送 POST、GET 请求,但它不支持 PUT、DELETE 请求,虽然有方法可以让表单通过 POST 模拟后两种,但属于偏门知识,这里就不讲了,正规的、常见方法有三种:

  • ajax 可以支持各种请求的发送,但属于前台知识,不是重点
  • Spring 提供了 RestTemplate 发送各种请求,底层就是用 java 代码发送 http 请求
  • 第三方工具发送请求,底层也都是 http
    • 图形界面的有 postman
    • 命令行界面的有 curl
postman 基本使用

postman 采用了一个比较旧的版本 6.6.1,够用(其它版本类似),默认安装位置为:

C:\Users\{用户名}\AppData\Local\Postman\app-6.6.1

新建一个 collection

collection 的作用就好比一个文件夹,里面可以存放多个相关的请求,方便管理

接下来创建 request

可以对请求进行各方面设置
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-ZesuoE61-1648028816997)(img/15.png)]

⭐️3) @RequestBody

客户端的参数比较多,比较复杂时,常会在请求体中传递过来 json 字符串,而 controller 这边可以用 @RequestBody 将此 json 字符串转换为 java 对象

客户端数据

{"name":"张三", "age":18}

根据参数设计一个用来接收的 java 类

static class Student {
	String name;
	Integer age;

	// 省略 get set 方法
}

方法参数用 @RequestBody 标注

@RestController
public class Controller03 {

    @PostMapping("/students")
    public void post(@RequestBody Student student) {
        log.debug("{}", student);
    }
	
	// ...
}

如果客户端数据更复杂一些,一样没问题

[
	{"name":"张三", "age":18},
	{"name":"李四", "age":20}
]

控制器方法

@RestController
public class Controller03 {

    @PutMapping("/students")
    public void put(@RequestBody List<Student> students) {
        for (Student student : students) {
            log.debug("{}", student);
        }
    }
	
	// ...
}

4) 请求参数校验

请求参数接收完,还有一步重要操作,那就是校验。数据不校验,就可能导致非法、残缺数据入库,严重还可能引起系统漏洞

步骤

  1. 添加依赖 - 代码片段1
  2. 对象属性上添加校验规则 - 代码片段2
  3. 控制器方法改动 - 代码片段3

代码片段1(pom.xml)

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-validation</artifactId>
</dependency>

代码片段2

static class Student {
    @NotEmpty
    String name;
    @Min(18)
    Integer age;

    // ...
}

校验规则有不同注解进行控制,代码中的

  • @NotEmpty 表示该参数不能为 null,也不能为 ''
  • @Min 用来验证参数是整数,并且最小值不能小于 18

代码片段3

@RestController
public class ValidateController {

    private static final Logger log = LoggerFactory.getLogger(ValidateController.class);

    @PostMapping("/validate")
    public void validate(@Valid @RequestBody Student student, BindingResult result) {
        log.debug(student.toString());
        if (result.hasErrors()) {
            for (FieldError error : result.getFieldErrors()) {
                log.debug("{} 验证错误: {}", error.getField(), error.getDefaultMessage());
            }
        }
    }
}

说明

  • @Valid 用来标注是哪个对象需要验证
  • BindingResult result 用来保存验证结果,获取结果方法见以上代码
  • BindingResult result 与被 @Valid 对象必须紧邻

结果1 - 当姓名与年龄都不正确时

2021-11-02 09:03:12.147 DEBUG 30652 --- [nio-8080-exec-2] com.itheima.demo3.Controller06           : Student{name='', age=11}
2021-11-02 09:03:12.147 DEBUG 30652 --- [nio-8080-exec-2] com.itheima.demo3.Controller06           : age 验证错误: 最小不能小于18
2021-11-02 09:03:12.147 DEBUG 30652 --- [nio-8080-exec-2] com.itheima.demo3.Controller06           : name 验证错误: 不能为空

结果2 - 当年龄正确,姓名为空时

2021-11-02 09:03:43.525 DEBUG 30652 --- [nio-8080-exec-3] com.itheima.demo3.Controller06           : Student{name='', age=18}
2021-11-02 09:03:43.525 DEBUG 30652 --- [nio-8080-exec-3] com.itheima.demo3.Controller06           : name 验证错误: 不能为空

⭐️5) @ResponseBody

虽然前面讲到了 RESTful 软件架构风格中,资源可以有多种表现形式,但实际用的最多的还是 json,之前见过的 @ResponseBody 的作用将控制器方法的返回值做相应的转换,再写入响应:

  • 如果方法返回的是字符串,这时结果仍被视为普通字符串,即 text/plain
  • 如果方法返回的是 Java Bean、List、Map 等,这时结果会转换为 json 字符串,即 application/json
  • 方法可以声明为 void,表示响应体没有内容

下面的例子都用到同一个 User 类

public class User {
    private Integer id;
    private String username;
    private String password;
    private Date birthday;
	
	// ...
}
例1 - 转换 java bean
@RequestMapping("/json/c1")
@ResponseBody
public User c1() {
	return new User(1001, "张三", "123", new Date());
}

访问后返回响应

{
  "id": 1001,
  "username": "张三",
  "password": "123",
  "birthday": 1611019823007
}
例2 - Jackson 注解

注意到

  1. 日期被转换为毫秒值
    • 当然这个值用起来也挺方便的,只是可读性差,可以用 @JsonFormat 注解来格式化
  2. 密码属性被转换成了 json
    • 如果转换时,希望某一属性被忽略,可以使用 @JsonIgnore 注解
public class User {
    private Integer id;
    private String username;
	
    @JsonIgnore
    private String password;
	
    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8")
    private Date birthday;
	
	// ...
}

访问后响应

HTTP/1.1 200 OK

{
  "id": 1001,
  "username": "张三",
  "birthday": "2021-01-19 09:41:33"
}

注意

  • 这几个注解 jackson 类库提供的,如果你将来用其他 json 实现,需要根据实际情况修改
  • @JsonFormat 注解也可以用以下全局配置,更为方便
    • spring.jackson.date-format=yyyy-MM-dd HH:mm:ss
    • spring.jackson.time-zone=GMT+8:00
例3 - 忽略 null 值
@RequestMapping("/json/c2")
@ResponseBody
public User c2() {
    return new User(null, "张三", null, null);
}

访问后响应

HTTP/1.1 200 OK

{
  "id":null,
  "username":"张三",
  "birthday":null
}

有时候希望 null 值的属性不要参与转换 json,可以这么做

@JsonInclude(JsonInclude.Include.NON_NULL)
public class User {
    
	// ...
}

或者通过配置

spring.jackson.default-property-inclusion=non_null

结果变成

HTTP/1.1 200 OK

{
  "username":"张三"
}
例4 - 转换 List
@RequestMapping("/json/c4")
@ResponseBody
public List<String> c4() {
	return Arrays.asList("张三", "李四");
}

访问后响应

HTTP/1.1 200 OK

[
  "张三",
  "李四"
]
例5 - 转换 Map
@RequestMapping("/json/c5")
@ResponseBody
public Map<String, String> c5() {
	Map<String, String> map = new HashMap<>();
	map.put("beijing", "北京");
	map.put("shanghai", "上海");
	map.put("shenzhen", "深圳");
	return map;
}

访问后响应

HTTP/1.1 200 OK

{
  "shanghai": "上海",
  "shenzhen": "深圳",
  "beijing": "北京"
}

可以看到它的转换结果与 java bean 是一样的,但 Map 有缺点:

  • Map 类型是弱类型,将来接参数时,key还好说都是 String,但 value 转换时不能控制该转换为何种类型
  • Map 上无法方便配合上述注解,对转换 json 时进行控制

6) 响应码与响应头

问题
@RestController
public class Controller06 {
    private static final Logger log = LoggerFactory.getLogger(Controller06.class);

    @RequestMapping("/rs/c1")
    public User c1() {
        int i = 1 / 0;
        return new User(1001, "张三", "123", new Date());
    }

    @ExceptionHandler
    public Map<String, String> ex(ArithmeticException e) {
        Map<String, String> map = new HashMap<>();
        map.put("error", e.getMessage());
        return map;
    }
}

访问后响应

HTTP/1.1 200 OK

{
  "error": "/ by zero"
}

虽然也可以,但缺点是,必须解析响应体后才知道出现了错误

@ResponseStatus

可以用 @ResponseStatus 来指定控制器方法或异常处理方法的响应头

修改代码为

@RestController
public class Controller06 {

    // ...

    @ExceptionHandler
    @ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR)
    public Map<String, String> ex(ArithmeticException e) {
        Map<String, String> map = new HashMap<>();
        map.put("error", e.getMessage());
        return map;
    }
}

访问后响应

HTTP/1.1 500 Internal Server Error

{
  "error": "/ by zero"
}

其实在 RESTful 架构风格中,建议响应状态码应当更精确,例如

响应状态码用法
200 OK查询、更新、删除成功时
201 Created新增成功时,一般响应中应含有新资源的 URI
202 Accepted请求成功接收,会在未来处理(异步处理)
204 No Content成功但没有响应体时
400 Bad Request请求有误(例如参数不正确)
401 Unauthorized请求身份验证失败
403 Forbidden请求身份验证成功,但没有权限
404 Not Found请求资源不存在
405 Method Not Allowed请求 Method 不被支持
415 Unsupported Media Type请求资源的表现形式不支持
429 Too Many Requests请求次数超过限额
500 Internal Server Error服务器内部错误,服务仍可用
503 Service Unavailable服务器已不可用

这时候 @ResponseStatus 就可以派上用场了

响应码中还有一个比较重要的 304 这个讲到缓存时再说

ResponseEntity

如果希望控制响应的各个部分:响应码、响应头、响应体,还可以使用 ResponseEntity 类型作为控制器方法的返回值

例如:

@RequestMapping("/rs/c2")
public ResponseEntity<User> c2() {
	return ResponseEntity.ok()
			.header("My-Header", "yeah")
			.body(new User(1001, "张三", "123", new Date()));
}

访问后响应

HTTP/1.1 200 OK
My-Header: yeah

{
  "id": 1001,
  "username": "张三",
  "birthday": "2021-01-19 11:05:38"
}

7) 统一响应格式

  • 响应状态码的不足:上一节说到,可以用状态码较为精确地描述此操作是成功失败、是客户端的问题、还是服务端的问题,但状态码毕竟有限,是最为通用的描述,如果牵扯业务,就有些不够用了
  • 返回的响应体中,可能会表示正常的数据,也有可能包含错误提示,若不统一,就增加了前端解析成本

因此一般应用开发时,会定义一个 AppResult 统一响应格式

AppResult
@JsonInclude(JsonInclude.Include.NON_NULL)
public class AppResult {
	// 应用状态码,对响应状态码算是补充和扩展
    private Integer code;	
	// 应用出错描述
    private String msg;	
	// 应用响应数据
    private Object data;
    public AppResult(Integer code, String msg, Object data) {
        this.code = code;
        this.msg = msg;
        this.data = data;
    }	
	// ...
}

对应的 json 数据如下

HTTP/1.1 响应状态码

{
  "code": "应用状态码",
  "msg": "应用出错描述",
  "data": "应用响应数据"
}

应用状态码都是应用内自定义的,以豆瓣 api 的为例,根据豆瓣 api 中通用错误码来看,它的这些错误码有两层分类:大类关联响应状态码

400 - 客户端校验错误
401 - 客户端认证失败
403 - 客户端权限错误
404 - 资源不存在

而每大类下又分了多个小类,小类关联应用状态码,例如:

400
	999 unknown_v2_error
	1002 missing_args     缺失参数
	1003 image_too_large  图片太大
	1004 has_ban_word     有违禁词
	1005 input_too_short
	1006 target_not_fount
	1008 image_unknow
	1009 image_wrong_format
	1012 title_missing
	1013 desc_missing
403
	1001 need_permission   需要权限
	1007 need_captcha      需要验证码
	1010 image_wrong_ck
	1011 image_ck_expired   图片过期

✒️练习 - RESTful

题目见 exercise2 中的 1~7 题

✒️大作业

完成 homework

附录

本章注解

Web 注解

注解名称位置注解作用备注
@RequestMapping方法映射路径
@RequestMapping为映射路径加统一前缀
@ResponseBody方法该控制器方法的返回值即为响应体内容
@ResponseBody影响该类的所有控制器方法
@RestController组合注解 @Controller + @ResponseBody
@RequestParam参数主要设置请求参数默认值
@RequestHeader参数获取请求头,头名称不区分大小写
@CookieValue参数获取 cookie 值
@ExceptionHandler方法处理控制器异常
@RestControllerAdvice该类用来全局处理控制器异常,@ControllerAdvice + @ResponseBody
@ResponseStatus方法控制返回响应的状态码

JSON 注解

注解名称位置注解作用备注
@JsonIgnore成员变量转 json 时忽略此成员变量
@JsonFormat成员变量转 json 时控制日期格式和时区有等价配置
@JsonInclude控制该类取值 null 的成员变量不参与转换有等价配置

校验注解

注解名称位置注解作用备注
@Valid方法参数该参数需要校验
@NotEmpty成员变量字符串不能为 null、空、集合不能为空
@Min成员变量数字不能小于某个值
@Max成员变量数字不能大于某个值
@NotNull成员变量不能为 null
@NotBlank成员变量字符串不能为 null、空、不能全为空白字符
@DecimalMin成员变量与 @Min 类似,但可以校验字符串
@DecimalMax成员变量与 @Max 类似,但可以校验字符串
@Digits成员变量校验数字的整数、小数位数
@Size成员变量字符串长度、集合大小
@Past成员变量必须是过去的时间
@Future成员变量必须是未来的时间
@Pattern成员变量必须符合正则表达式
@Email成员变量必须符合 Email 格式

备注为【扩】的表示课堂没讲,将来用到时再去了解

本章扩展

war 入门案例

步骤1:创建模块,区别在于打包方式选择 war

接下来勾选 Spring Web 支持

生成的模块,多了一个 ServletInitializer,它的作用是在 Tomcat 启动时,根据它找到 Spring Boot 引导类,初始化 Spring 容器

步骤2:添加如下依赖,因为本项目需要用到 jsp,后期 jsp 用的少,现阶段用它是案例需要

<dependency>
    <groupId>org.apache.tomcat.embed</groupId>
    <artifactId>tomcat-embed-jasper</artifactId>
</dependency>

步骤3:编写控制器

@Controller
public class MyController {

    @RequestMapping("/hello")
    public String abc() {
        System.out.println("进入了控制器");
        return "hello";
    }
}

其中

  • @Controller 注解表示此类为控制器类
  • @RequestMapping("/hello") 用来指定通过 /hello 这个 uri 路径能够找到它标注的方法处理请求
  • 返回类型为 String,返回结果代表视图逻辑名称

步骤4:编写 jsp 视图,新建 webapp 目录和一个 hello.jsp 文件,注意文件名与控制器方法返回的视图逻辑名一致

src
	|- main
		|- java
		|- resources
		|- webapp
			|- hello.jsp

步骤5:配置视图路径,打开 application.properties 文件

spring.mvc.view.prefix=/
spring.mvc.view.suffix=.jsp

将来 prefix + 控制器方法返回值 + suffix 即为视图完整路径

步骤6:运行服务器,这回不能用 main 方法来运行了,打开 terminal 窗口,在模块根目录下运行

mvn spring-boot:run

即可启动 tomcat 服务器,并初始化 Spring 容器

步骤7:打开浏览器,输入如下地址访问控制器方法

http://localhost:8080/hello

资源映射

Spring Boot 中与路径映射相关的 bean 叫做 HandlerMapping,默认已配置了如下 5 种:

  1. requestMappingHandlerMapping = RequestMappingHandlerMapping

  2. welcomePageHandlerMapping = WelcomePageHandlerMapping

  3. beanNameHandlerMapping = BeanNameUrlHandlerMapping

  4. routerFunctionMapping = RouterFunctionMapping

  5. resourceHandlerMapping = SimpleUrlHandlerMapping

其中等号前是该 bean 的名称,等号后是该 bean 的类型,跟目前有关系的有1、2、5

  • 其中 1 对应 @RequestMapping 的路径映射
  • 2 对应的是欢迎页,会找静态资源中名为 index.html 的资源作为欢迎页
  • 5 对应的就是静态资源映射,可以通过调用它的 getUrlMap() 方法来查看它映射了哪些路径

RequestMappingHandlerMapping 会在初始化时扫描所有加了 @RequestMapping 及其衍生注解的类及方法,用作后续请求映射

字符编码过滤器

  • POST 请求参数如果有中文,无需特殊设置,这是因为 Spring Boot 已经配置了 org.springframework.boot.web.servlet.filter.OrderedCharacterEncodingFilter
  • 对应配置 server.servlet.encoding.charset=UTF-8,默认就是 UTF-8
  • 当然,它只影响非 json 格式的数据

文件上传

Spring Boot 能自动处理文件上传,是因为它通过

  • org.springframework.boot.autoconfigure.web.servlet.MultipartAutoConfiguration 配置了 org.springframework.web.multipart.support.StandardServletMultipartResolver
  • 后者用来解析 multipart/form-data 格式的数据

请求处理流程

当浏览器发送一个请求 http://localhost:8080/hello 后,请求到达服务器,服务器使用了 Spring Boot 或 SSM 技术,其处理流程是类似的:

  1. 服务器提供了 DispatcherServlet,它使用的是标准 Servlet 技术,默认映射路径为 /,即会匹配到所有请求 URL

    • 在 Boot 中,由 DispatcherServletAutoConfiguration 这个自动配置类提供 DispatcherServlet 的 bean
    • DispatcherServlet 作为请求的统一入口,也被称之为前控制器
  2. DispatcherServlet 会利用 HandlerMapping 进行路径匹配,找到 @RequestMapping("/hello") 对应的控制器方法

    • RequestMappingHandlerMapping 会识别 @RequestMapping,前面提过
    • 控制器方法会被封装为 HandlerMethod 对象,并结合匹配到的拦截器一起返回给 DispatcherServlet
    • HandlerMethod 和拦截器合在一起称为 HandlerExecutionChain(调用链)对象
  3. DispatcherServlet 接下来会:

    1. 调用拦截器的 preHandle 方法

    2. 使用 HandlerAdapter 准备 HandlerMethod 需要的参数

    3. 使用 HandlerAdapter 调用 HandlerMethod

    4. 使用 HandlerAdapter 处理 HandlerMethod 执行返回的结果(统一封装为 ModelAndView)

    5. 调用拦截器的 postHandle 方法

    6. 调用拦截器的 afterCompletion 方法

  4. 返回响应

    • 标注了 @ResponseBody 的控制器方法,会在 3.4 这一步调用 HttpMessageConverter 来将结果转换为 JSON
    • 否则,走 ModelAndView 及视图解析的逻辑

什么是 RESTful

它是 Representational State Transfer(表现层状态转换)的缩写,但要注意的是:它省略了主语 Resources(资源),合在一起,应当是【资源的表现形式及状态转换】,下面逐一解释

1) Resources

资源就是网络上一个实体【名词】,可以是

  • 一支专辑
  • 一只股票
  • 一部电影、一本书
  • 一个 github 仓库
  • 一场比赛
  • 一日天气
    每个资源可以被表示为一个 URI
2) Representational

即资源的表现形式,可以是 html,也可以是 json,还可以是一张图片等等,以周董《范特西》这个资源为例

可以是 html 格式
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-jJ615JUz-1648028816999)(img/9.png)]

也可以是 json 格式

可以演示 spring_case_mvc_show 这个小案例,案例的 postman 测试数据在资料文件夹中:springmvc restful show.json

用 json 或 xml 作为资源的表现形式,最大的好处是仅返回数据,而数据如何表现交给客户端来完成,这是前后端分离的基础

3) State

所谓【状态】就是一些信息,例如专辑名称《范特西》,专辑作者周董,都可以视为状态

状态分为两类

  • 资源自身状态,这些状态存储于服务器端,可以被所有客户端共享
    • 例如《范特西》这张专辑信息存储于服务器端,任多少客户端来访问,都是一样的
  • 用户状态,这些状态存储于客户端,需要通过请求每次传递给服务器
    • 例如张三客户端是VIP,想看一部付费电影,他必须告诉服务器【我是张三】这个信息
    • 而李四客户端不是VIP,想看那部付费电影看就看不了
    • 服务器根据【用户】这个状态判断该操作能否被执行
4) State Transfer

【转换】也可以从两个角度来理解

资源自身状态的变化,如:

  • 资源从无到有,这是一种状态转换【新增】
  • 资源信息变更,这是一种状态转换【修改】
  • 资源从有到无,这还是一种状态转换【删除】

客户端与服务器端的交互也会发生状态的转换(传输)

  • 服务器URI?username=张三,这相当于将客户端的状态信息传输至服务器
  • 随后服务器将资源的状态信息返回给客户端

这些增删改查、可以用 HTTP Method 来表示,分别为

HTTP Method说明是否安全是否幂等可否缓存
GET获取资源状态
PUT修改资源状态
DELETE删除资源状态
POST新增或修改资源状态

解释

  • 安全 - 意味着不会改变服务器资源状态
  • 幂等 - 意味着对某一资源【访问多次】,与对此资源【访问一次】结果相同
  • 缓存 - 意味着允许客户端存储响应结果,以便将来能重用

Ajax 跨域

参考 资料文件夹中 ajax.html

页面中通过该 axios 里的 get、post、put、delete 方法发送请求,然而,会发现这些请求统统访问不了,比如点击了 get 后,浏览器控制台上会出现如下错误信息

已拦截跨源请求:同源策略禁止读取位于 http://localhost:8080/users/2 的远程资源。(原因:CORS 头缺少 'Access-Control-Allow-Origin')

这里的请求报错原因是,它的底层是通过 ajax 技术发送的,而浏览器为了保障 ajax 请求的安全性,默认只允许同源请求

同源请求的含义是,必须 ajax 所请求的资源必须与当前页面是:

  • 相同协议
  • 相同域名
  • 相同端口

有一项不一样,就认为是跨源请求(或称之为跨域请求),默认是不允许访问的。说白了,就是默认情况下,只能是ajax 访问自己的网站的其它资源是 ok 的,访问其它网站的资源就得征求该网站的同意了!

现在这种情况下,【客户端】需要发送一个 Origin 头告诉【目标网站】原始域是什么,而【目标网站】返回一个 Access-Control-Allow-Origin 响应头,这个头用来描述【目标网站】究竟允许哪些别的网站来访问,例如

  • Access-Control-Allow-Origin: * 表示允许所有网站的 ajax 请求访问
  • Access-Control-Allow-Origin: http://www.163 只允许 http://www.163 网站发来的 ajax 请求访问
  • Access-Control-Allow-Origin: null 只允许通过双击 html 文件打开的网页,发来的 ajax 请求访问

SpringMVC 可以使用 @CrossOrigin 来标注控制器类或控制器方法来生成必要的 Access-Control-Allow-Origin 响应头:

例1:如果想允许 http://www.163的 ajax 请求来访问本站资源,做如下设置即可

@CrossOrigin("http://www.163")
@RestController
public class Controller07 {
    // ...
}
  • 加在类上会影响类中所有方法
  • 加在方法上只会影响此方法

例2:如果想允许所有域的 ajax 请求来访问本站资源,可以进行全局配置

@SpringBootApplication
public class HomeworkApplication implements WebMvcConfigurer {

    public static void main(String[] args) {
        SpringApplication.run(HomeworkApplication.class, args);
    }

    @Override
    public void addCorsMappings(CorsRegistry registry) {
        registry.addMapping("/**").allowedOrigins("*");
    }
}

RestTemplate

@SpringBootTest
public class TestRestTemplate {

    private String uri = "https://www.tianqiapi/free/day?appid=89955246&appsecret=9JJ8QmZs&unescape=1";

    @Autowired
    private RestTemplateBuilder builder;

    @Test
    @DisplayName("发送get请求, 以字符串接收")
    public void test1() {
        RestTemplate template = builder.build();
        // 构造请求:uri 以及请求方法
        RequestEntity<Void> entity = RequestEntity.method(HttpMethod.GET, uri).build();
        // 发送请求,接收响应,响应类型为 String.class
        ResponseEntity<String> response = template.exchange(entity, String.class);
        System.out.println(response.getBody());
    }

    @Test
    @DisplayName("发送get请求, 以对象接收")
    public void test2() {
        RestTemplate template = builder.build();
        // 构造请求:uri 以及请求方法
        RequestEntity<Void> entity = RequestEntity.method(HttpMethod.GET, uri).build();
        // 发送请求,接收响应,响应类型为自定义Java对象
        ResponseEntity<Weather> response = template.exchange(entity, Weather.class);
        System.out.println(response.getBody());
    }

    // {"cityid":"101200101","city":"武汉","update_time":"16:57","wea":"多云","wea_img":"yun","tem":"15","tem_day":"18","tem_night":"1","win":"西北风","win_speed":"1级","win_meter":"2km\/h","air":"65"}
    static class Weather {
        private String cityid;
        private String city;
        private String updateTime;
        private String wea;
        private String weaImg;
        private String tem;
        private String temDay;
        private String temNight;
        private String win;
        private String winSpeed;
        private String winMeter;
        private String air;
        // 省略 get set toString
    }
}

做了下面的配置,来实现驼峰下划线转换

spring.jackson.property-naming-strategy=SNAKE_CASE

本章参考

  • devtools 使用
  • 控制器方法支持的参数类型
  • 阮一峰 - 理解 RESTful 架构
  • RESTful 释义 - 维基百科中文
  • RESTful 释义 - 维基百科英文
  • POST 可否缓存响应
  • RESTful 的提出者
  • 原生校验注解
    • 也可以参考 com.itheima.demo3.TestValidator,以单元测试方式演示了常见原生校验注解的使用
  • 响应状态码的作用

RESTful API 实例

  • 网易云音乐 http://cloud-music.pl-fe

    • http://cloud-music.pl-fe/album?id=18915
  • 新浪财经

    • http://hq.sinajs/list=sh601003,sh601001
    • http://image.sinajs/newchart/daily/n/sh601006.gif
  • 豆瓣 API https://douban-api-docs.zce.me/

  • github https://docs.github/en/rest/overview

  • 极电竞 https://www.jdj007/

  • 天气API http://www.tianqiapi/

    • 免费 api https://www.tianqiapi/free/day?appid=89955246&appsecret=9JJ8QmZs&unescape=1
  • ElasticSearch REST api

  • MongoDB REST api

注意

  • 一些 api 网址会随时间产生变化甚至不可用,请以当时实际情况为准
    h",“air”:“65”}
    static class Weather {
    private String cityid;
    private String city;
    private String updateTime;
    private String wea;
    private String weaImg;
    private String tem;
    private String temDay;
    private String temNight;
    private String win;
    private String winSpeed;
    private String winMeter;
    private String air;
    // 省略 get set toString
    }
    }

做了下面的配置,来实现驼峰下划线转换

```properties
spring.jackson.property-naming-strategy=SNAKE_CASE

本章参考

  • devtools 使用
  • 控制器方法支持的参数类型
  • 阮一峰 - 理解 RESTful 架构
  • RESTful 释义 - 维基百科中文
  • RESTful 释义 - 维基百科英文
  • POST 可否缓存响应
  • RESTful 的提出者
  • 原生校验注解
    • 也可以参考 com.itheima.demo3.TestValidator,以单元测试方式演示了常见原生校验注解的使用
  • 响应状态码的作用

RESTful API 实例

  • 网易云音乐 http://cloud-music.pl-fe

    • http://cloud-music.pl-fe/album?id=18915
  • 新浪财经

    • http://hq.sinajs/list=sh601003,sh601001
    • http://image.sinajs/newchart/daily/n/sh601006.gif
  • 豆瓣 API https://douban-api-docs.zce.me/

  • github https://docs.github/en/rest/overview

  • 极电竞 https://www.jdj007/

  • 天气API http://www.tianqiapi/

    • 免费 api https://www.tianqiapi/free/day?appid=89955246&appsecret=9JJ8QmZs&unescape=1
  • ElasticSearch REST api

  • MongoDB REST api

注意

  • 一些 api 网址会随时间产生变化甚至不可用,请以当时实际情况为准

本文标签: SpringMvc