spring 5.x 系列第2篇 —— springmvc基础 (代码配置方式)

一、搭建 Hello Spring 工程

1.1 项目搭建

1.新建 maven web 工程,并引入相应的依赖:

<properties>
    <spring-base-version>5.1.3.RELEASEspring-base-version>
properties>

<dependencies>
    <dependency>
        <groupId>org.springframeworkgroupId>
        <artifactId>spring-contextartifactId>
        <version>${spring-base-version}version>
    dependency>
    <dependency>
        <groupId>org.springframeworkgroupId>
        <artifactId>spring-beansartifactId>
        <version>${spring-base-version}version>
    dependency>
    <dependency>
        <groupId>org.springframeworkgroupId>
        <artifactId>spring-coreartifactId>
        <version>${spring-base-version}version>
    dependency>
    <dependency>
        <groupId>org.springframeworkgroupId>
        <artifactId>spring-webartifactId>
        <version>${spring-base-version}version>
    dependency>
    <dependency>
        <groupId>org.springframeworkgroupId>
        <artifactId>spring-webmvcartifactId>
        <version>${spring-base-version}version>
    dependency>
    <dependency>
        <groupId>javax.servletgroupId>
        <artifactId>javax.servlet-apiartifactId>
        <version>4.0.1version>
        <scope>providedscope>
    dependency>
dependencies>

2.得益于 servlet3.0 和 Spring 的共同支持,我们可以在没有 web.xml 的情况下完成关于 servlet 配置。新建 DispatcherServletInitializer,这个类的作用相当于我们在 xml 方式下 web.xml 中配置的 DispatcherServlet:

public class DispatcherServletInitializer extends AbstractAnnotationConfigDispatcherServletInitializer {

    protected Class<?>[] getRootConfigClasses() {
        return new Class[0];
    }

    protected Class<?>[] getServletConfigClasses() {
        return new Class[]{ServletConfig.class};
    }

    protected String[] getServletMappings() {
        return new String[]{"/"};
    }
}

3.新建 ServletConfig ,文件内容如下 (这个类相当于我们在 xml 配置方式中的 springApplication.xml ):

@Configuration
@EnableWebMvc
@ComponentScan(basePackages = {"com.heibaiying.controller"})
public class ServletConfig implements WebMvcConfigurer {

    /**
     * 配置视图解析器
     */
    @Bean
    public ViewResolver viewResolver() {
        InternalResourceViewResolver internalResourceViewResolver = new InternalResourceViewResolver();
        internalResourceViewResolver.setPrefix("/WEB-INF/jsp/");
        internalResourceViewResolver.setSuffix(".jsp");
        internalResourceViewResolver.setExposeContextBeansAsAttributes(true);
        return internalResourceViewResolver;
    }

    /**
     * 配置静态资源处理器
     */
    public void configureDefaultServletHandling(DefaultServletHandlerConfigurer configurer) {
        configurer.enable();
    }

}

4.新建 Controller 用于测试整体配置是否成功:

@Controller
@RequestMapping("mvc")
public class HelloController {

    @RequestMapping("hello")
    private String hello() {
        return "hello";
    }
}

5.在 WEB-INF 下新建 jsp 文件夹,并创建一个简单的 hello.jsp 文件:

<%@ page contentType="text/html;charset=UTF-8" language="java" %>


    Title


    Hello Spring MVC!


6.启动 tomcat 服务,访问的服务地址为:localhost:8080/mvc/hello

1.2 相关注解说明

1.@Configuration

@Configuration 用于配置类的定义,等价于xml 配置文件,配置类内部可以包含一个或多个被 @Bean 注解的方法,这些方法将会被 AnnotationConfigApplicationContext 或 AnnotationConfigWebApplicationContext 类进行扫描,方法返回的实体会被直接注册为容器内的 Bean。

2.@EnableWebMvc

简单的说就是提供了部分 springmvc 的功能,例如格式转换和参数绑定。

二、配置自定义拦截器

1.创建自定义拦截器,实现接口 HandlerInterceptor(这里我们创建两个拦截器,用于测试拦截器的执行顺序):

public class MyFirstInterceptor implements HandlerInterceptor {

    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) {
        System.out.println("进入第一个拦截器 preHandle");
        return true;
    }

    // 需要注意的是,如果对应的程序报错,不一定会进入这个方法 但一定会进入 afterCompletion 这个方法
    public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) {
        System.out.println("进入第一个拦截器 postHandle");
    }

    public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) {
        System.out.println("进入第一个拦截器 afterCompletion");
    }
}
public class MySecondInterceptor implements HandlerInterceptor {

    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) {
        System.out.println("进入第二个拦截器 preHandle");
        return true;
    }

    public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) {
        System.out.println("进入第二个拦截器 postHandle");
    }

    public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) {
        System.out.println("进入第二个拦截器 afterCompletion");
    }
}

2.在 ServletConfig 中注册自定义拦截器:

public void addInterceptors(InterceptorRegistry registry) {
    registry.addInterceptor(new MyFirstInterceptor()).addPathPatterns("/mvc/**").excludePathPatterns("mvc/login");
    registry.addInterceptor(new MySecondInterceptor()).addPathPatterns("/mvc/**");
}

3.关于多个拦截器方法执行顺序的说明

拦截器的执行顺序是按声明的先后顺序执行的,先声明的拦截器中的 preHandle 方法会先执行,然而它的 postHandle 方法和 afterCompletion 方法却会后执行。

三、全局异常处理

1.定义自定义异常:

public class NoAuthException extends RuntimeException {

    public NoAuthException() {
        super();
    }

    public NoAuthException(String message) {
        super(message);
    }

    public NoAuthException(String message, Throwable cause) {
        super(message, cause);
    }

    public NoAuthException(Throwable cause) {
        super(cause);
    }
}

2.实现自定义异常处理器:

public class NoAuthExceptionResolver implements HandlerExceptionResolver {

    public ModelAndView resolveException(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) {
        if (ex instanceof NoAuthException && !isAjax(request)) {
            return new ModelAndView("NoAuthPage");
        }
        return new ModelAndView();
    }

    // 判断是否是 Ajax 请求
    private boolean isAjax(HttpServletRequest request) {
        return "XMLHttpRequest".equalsIgnoreCase(request.getHeader("X-Requested-With"));
    }
}

3.在 ServletConfig 中注册自定义异常处理器:

public void configureHandlerExceptionResolvers(List<HandlerExceptionResolver> resolvers) {
    resolvers.add(new NoAuthExceptionResolver());
}

4.定义测试 controller,抛出自定义异常:

@Controller
@RequestMapping("mvc")
public class HelloController {

    @RequestMapping("hello")
    private String hello() {
        return "hello";
    }

    @RequestMapping("auth")
    private void auth() {
        throw new NoAuthException("没有对应的访问权限!");
    }
}

调用这个 Controller 时,同时可以验证在拦截器部分提到的:如果对应的程序报错,拦截器不一定会进入 postHandle 这个方法 但一定会进入 afterCompletion 这个方法。

四、参数绑定

4.1 参数绑定

1.新建 Programmer.java 作为测试实体类:

@Data
public class Programmer {

    private String name;

    private int age;

    private float salary;

    private String birthday;
}

2.新建 ParamBindController.java 文件,跳转到指定视图:

@Controller
public class ParamBindController {

    @InitBinder
    protected void initBinder(WebDataBinder binder) {
        binder.addCustomFormatter(new DateFormatter("yyyy-MM-dd HH:mm:ss"));
    }


    // 参数绑定与日期格式转换
    @RequestMapping("param")
    public String param(String name, int age, double salary, @DateTimeFormat(pattern = "yyyy-MM-dd HH:mm:ss") Date birthday, Model model) {
        model.addAttribute("name", name);
        model.addAttribute("age", age);
        model.addAttribute("salary", salary);
        model.addAttribute("birthday", birthday);
        return "param";
    }

    @RequestMapping("param2")
    public String param2(String name, int age, double salary, Date birthday, Model model) {
        model.addAttribute("name", name);
        model.addAttribute("age", age);
        model.addAttribute("salary", salary);
        model.addAttribute("birthday", birthday);
        return "param";
    }


    @PostMapping("param3")
    public String param3(Programmer programmer, String extendParam, Model model) {
        System.out.println("extendParam" + extendParam);
        model.addAttribute("p", programmer);
        return "param";
    }

}

3.新建 param.jsp 文件,用于测试数据在视图中的绑定情况:

<%@ page contentType="text/html;charset=UTF-8" language="java" %>


    Restful


  • 姓名:${empty name ? p.name : name}
  • 年龄:${empty age ? p.age : age}
  • 薪酬:${empty salary ? p.salary : salary}
  • 生日:${empty birthday ? p.birthday : birthday}

4.启动 tomcat,接着可以使用 postman 等接口测试软件发送测试请求。

4.2 日期格式转换

Spring 支持使用以下三种方法来对参数中的日期格式进行转换:

方法一:如上面的实例代码所示,在对应的 Controller 中初始化绑定:

@InitBinder
protected void initBinder(WebDataBinder binder) {
    binder.addCustomFormatter(new DateFormatter("yyyy-MM-dd HH:mm:ss"));
}

方法二:利用 @DateTimeFormat 注解,如果是用实体类去接收参数,则在对应的实体类的属性上用 @DateTimeFormat 和 @JsonFormat 进行声明:

public String param(@DateTimeFormat(pattern = "yyyy-MM-dd HH:mm:ss") Date birthday)

方法三:使用全局的日期格式绑定,新建自定义日期格式转化类,之后在 springApplication.xml 中进行注册,采用这种方式会对全局范围内的日期格式转换生效:

public class CustomDateConverter implements Converter<String, Date> {

    public Date convert(String s) {
        try {
            SimpleDateFormat simpleDateFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
            return simpleDateFormat.parse(s);
        } catch (Exception e) {
            e.printStackTrace();
        }
        return null;
    }
}

在 ServletConfig 中进行注册:

public void addFormatters(FormatterRegistry registry) {
	registry.addConverter(new CustomDateConverter());
}

五、数据校验

1.Spring 支持 JSR303 标准的校验,需要引入相关的依赖:


<dependency>
    <groupId>org.hibernate.validatorgroupId>
    <artifactId>hibernate-validatorartifactId>
    <version>6.0.13.Finalversion>
dependency>
<dependency>
    <groupId>javax.validationgroupId>
    <artifactId>validation-apiartifactId>
    <version>2.0.1.Finalversion>
dependency>

2.新建测试类 ParamValidController.java,在需要校验的参数前加上 @Validated 注解,表明该参数需要被校验。同时在方法声明中加上 bindingResult 参数,可以从这个参数中获取最终校验的结果:

@RestController
public class ParamValidController {

    @PostMapping("validate")
    public void valid(@Validated Programmer programmer,BindingResult bindingResult) {
        List<ObjectError> allErrors = bindingResult.getAllErrors();
        for (ObjectError error : allErrors) {
            System.out.println(error.getDefaultMessage());
        }
    }
}

3.在 Programmer.java 的对应属性上加上注解约束,用于声明每个参数的校验规则:

@Data
public class Programmer {

    @NotNull
    private String name;

    @Min(value = 0,message = "年龄不能为负数!" )
    private int age;

    @Min(value = 0,message = "薪酬不能为负数!" )
    private float salary;

    private String birthday;
}

其他支持的注解可以到 javax.validation.constraints 包下进行查看。

六、文件上传与下载

6.1 文件上传

1.在 ServletConfig 中进行配置,开启文件上传:

@Bean
public CommonsMultipartResolver multipartResolver(){
    CommonsMultipartResolver resolver = new CommonsMultipartResolver();
    resolver.setMaxUploadSize(1024*1000*10);
    resolver.setMaxUploadSizePerFile(1024*1000);
    resolver.setDefaultEncoding("utf-8");
    return resolver;
}

2.新建上传测试类:

@Controller
public class FileController {

    @GetMapping("file")
    public String filePage() {
        return "file";
    }


    /***
     * 单文件上传
     */
    @PostMapping("upFile")
    public String upFile(MultipartFile file, HttpSession session) {
        //保存在项目根目录下 image 文件夹下,如果文件夹不存在则创建
        FileUtil.saveFile(file, session.getServletContext().getRealPath("/image"));
        // success.jsp 就是一个简单的成功页面
        return "success";
    }

    /***
     * 多文件上传 多个文件用同一个名字
     */
    @PostMapping("upFiles")
    public String upFiles(@RequestParam(name = "file") MultipartFile[] files, HttpSession session) {
        for (MultipartFile file : files) {
            FileUtil.saveFile(file, session.getServletContext().getRealPath("images"));
        }
        return "success";
    }

    /***
     * 多文件上传方式 2 分别为不同文件指定不同名字
     */
    @PostMapping("upFiles2")
    public String upFile(String extendParam,
                         @RequestParam(name = "file1") MultipartFile file1,
                         @RequestParam(name = "file2") MultipartFile file2, HttpSession session) {
        String realPath = session.getServletContext().getRealPath("images2");
        FileUtil.saveFile(file1, realPath);
        FileUtil.saveFile(file2, realPath);
        System.out.println("extendParam:" + extendParam);
        return "success";
    }
}

3.其中工具类 FileUtil.java 代码如下:

public class FileUtil {

    public static String saveFile(MultipartFile file, String path) {
        InputStream inputStream = null;
        FileOutputStream outputStream = null;
        String fullPath = path + File.separator + file.getOriginalFilename();
        try {
            File saveDir = new File(path);
            if (!saveDir.exists()) {
                saveDir.mkdirs();
            }
            outputStream = new FileOutputStream(new File(fullPath));
            inputStream = file.getInputStream();
            byte[] bytes = new byte[1024 * 1024];
            int read;
            while ((read = inputStream.read(bytes)) != -1) {
                outputStream.write(bytes, 0, read);
            }
        } catch (IOException e) {
            e.printStackTrace();
        } finally {
            if (inputStream != null) {
                try {
                    inputStream.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
            if (outputStream != null) {
                try {
                    outputStream.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
        }
        return fullPath;
    }

}

4.新建用于上传的 jsp 页面,上传文件时表单必须声明 enctype="multipart/form-data"

<%@ page contentType="text/html;charset=UTF-8" language="java" %>


    文件上传
    



    
请选择上传文件:
请选择上传文件 (多选):
请选择上传文件 1:
请选择上传文件 2:
文件内容额外备注:

6.2 文件下载

1.在 fileController.java 中增加下载方法:

/***
 * 上传用于下载的文件
 */
@PostMapping("upFileForDownload")
public String upFileForDownload(MultipartFile file, HttpSession session, Model model) throws UnsupportedEncodingException {
    String path = FileUtil.saveFile(file, session.getServletContext().getRealPath("/image"));
    model.addAttribute("filePath", URLEncoder.encode(path,"utf-8"));
    model.addAttribute("fileName", file.getOriginalFilename());
    return "fileDownload";
}

/***
 * 下载文件
 */
@GetMapping("download")
public ResponseEntity<byte[]> downloadFile(String filePath) throws IOException {
    HttpHeaders headers = new HttpHeaders();
    File file = new File(filePath);
    // 解决文件名中文乱码
    String fileName=new String(file.getName().getBytes("UTF-8"),"iso-8859-1");
    headers.setContentType(MediaType.APPLICATION_OCTET_STREAM);
    headers.setContentDispositionFormData("attachment", fileName);

    return new ResponseEntity<byte[]>(FileUtils.readFileToByteArray(file),
                                      headers, HttpStatus.CREATED);
}

2.其中 fileDownload.jsp 如下:

<%@ page contentType="text/html;charset=UTF-8" language="java" %>


    文件下载


    ${fileName}



七、RESTful 风格的请求

1.新建测试实体类:

@Data
public class Pet {

    private String ownerId;

    private String petId;
}

2.新建 RestfulController.java,用 @PathVariable 和 @ModelAttribute 注解进行参数绑定:

@RestController
public class RestfulController {

    @GetMapping("restful/owners/{ownerId}/pets/{petId}")
    public void get(@PathVariable String ownerId, @PathVariable String petId) {
        System.out.println("ownerId:" + ownerId);
        System.out.println("petId:" + petId);
    }

    @GetMapping("restful2/owners/{ownerId}/pets/{petId}")
    public void get(@ModelAttribute Pet pet) {
        System.out.println("ownerId:" + pet.getOwnerId());
        System.out.println("petId:" + pet.getPetId());
    }
}

在 RESTful 风格的请求中,资源通过 URL 进行标识和定位,而操作行为是通过 HTTP 方法进行定义,在进行不同行为时对应 HTTP 方法和 Spring 注解分别如下:

  • 创建资源时:POST(@PostMapping)
  • 读取资源时:GET( @GetMapping)
  • 更新资源时:PUT 或 PATCH(@PutMapping、@PatchMapping)
  • 删除资源时:DELETE(@DeleteMapping)

附:源码GitHub地址:https://github.com/heibaiying/spring-samples-for-all

你可能感兴趣的:(spring)