spring全集---第四章springmvc

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 技术让改动快速生效。

注解简化

注意到每个方法都需要加 @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 响应

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

@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/**");
    }
}

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值