1. Web 开发
1.1 jar 入门案例
基于 war 的调试、部署都较为麻烦,因此 Spring 官方也建议,采用 jar 方式来开发 web 项目,好处有:
-
仍然通过 main 方法测试运行,开发方便
-
打包打成 jar 包,内嵌 tomcat,无需再安装 tomcat 服务器
-
代价是不再支持 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
-
编写拦截器代码 - 代码片段2
-
配置拦截器 - 代码片段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/**");
}
}