# springmvcprotogenesis-study **Repository Path**: south-it-school-project/springmvcprotogenesis-study ## Basic Information - **Project Name**: springmvcprotogenesis-study - **Description**: 原生编写mvc,原生编写mvc原生编写mvc原生编写mvc原生编写mvc原生编写mvc原生编写mvc - **Primary Language**: Java - **License**: Not specified - **Default Branch**: master - **Homepage**: None - **GVP Project**: No ## Statistics - **Stars**: 0 - **Forks**: 0 - **Created**: 2025-05-09 - **Last Updated**: 2025-10-10 ## Categories & Tags **Categories**: Uncategorized **Tags**: None ## README # springmvc-study #### 介绍 ## MVC ## Springmvc原理解析 ![springmvc](https://wangl-pic.oss-cn-hangzhou.aliyuncs.com/uPic/springmvc.png) ### 1. DispatcherServlet springmvc的核心控制器,负责截获所有的请求,当截获请求后委托给HandlerMapping进行请求映射的解析工作,目的是找到哪一个Controller的方法可以处理该请求,找到后再交由给HandlerAdaptor去负责调用并返回ModelAndView对象,然后将ModelAndView对象交给相应的视图解析器(ViewResolver)解析成对应的视图(View)对象,最后由这个视图对象响应客户端。 ### 2. HandlerMapping和HandlerAdapter 当xml配置了 或者配置类中配置了@EnableWebMvc注解时,spring会自动装配RequestMappingHandlerMapping(请求映射处理器)RequestMappingHandlerAdapter(请求处理适配器)这两个类。 **RequestMappingHandlerMapping:** 负责解析带有@ReqeustMapping注解的方法以及类信息,并在请求到达时找到相应的HandlerMethod(一个JavaBean,封装了请求处理方法、参数信息、类信息以及IOC容器等重要的内容)。当找到相应的HandlerMethod后,如果程序中有定义拦截器,那么就会将这个HandlerMethod封装到HandlerExecutionChain的类中,这个类包含了一个拦截器的集合和一个HandlerMethod的对象。最后将这个chain返回给DispatcherServlet。DispatcherServlet从这个HandlerExecutionChain中取出HandlerMethod来匹配相应的HandlerAdapter,找到合适的可以调用HandlerMathod的请求处理适配器。接着DispatcherServlet负责调用HandlerExecutionChain中的所有拦截器中的预处理方法,如果预处理方法没有任何问题,那么就将HandlerMethod交给HandlerAdapter去调用。 **RequestMappingHandlerAdapter:** DispatcherServlet将HandlerMethod传递给HandlerAdapter,由它负责调用HandlerMethod(也就是目标控制器的方法)。调用时还会使用具体的MethodArgumentResolver( 方法参数解析器,RequestMappingHandlerAdapter内部会初始化一系列默认的HandlerMethodArgumentResolver)将请求中的参数解析为请求处理方法所需要的具体类型参数。最后将Controller方法返回的ModelAndView一并返回到DispatcherServlet中。接着DispatcherServlet会继续执行所有拦截器中的后置处理方法。 ### 3. ViewResolver springmvc内部提供了许多视图解析器用于解析不同的视图对象,最长见的有InternalResourceViewResolver(内部资源视图解析器)、FreeMarkerViewResolver(模板引擎视图解析器)等。 **InternalResourceViewResolver:** 在DispatcherServlet接收到HandlerAdapter返回的ModelAndView之后,DispatcherServlet将这个ModelAndView交给指定InternalResourceViewResolver来进行视图解析,InternalResourceViewResolver会根据ModelAndView的视图名称来创建一个InternalResourceView的视图对象返回到DispatcherServlet。由DispatcherServlet去调用视图对象的渲染方法来响应视图。在渲染完视图之后,DispatcherServlet会执行所有拦截器中的after方法。 ### 4. View 视图对象是由相应的视图解析器解析出来的,Spring也提供了不同的视图对象来完成不同的视图响应工作,常见的有的InternalResourceView(内部资源转发视图)等。 **InternalResourceView:** 这个视图对象会将ModeAndView中而外带的数据放入请求作用域,以及获取到拼接好的转发地址。并提供一个renderMergedOutputModel渲染方法由DispatcherServlet调用,这个方法就是负责具体的url转发工作。 **MVC(Model-View-Controller)** 是一种**软件设计模式**,核心思想是将程序逻辑分为三个独立的组件:**模型(Model)**、**视图(View) ** 和**控制器(Controller)**。它的目标是实现代码的**高内聚、低耦合**,提升可维护性和扩展性。 最主要的是Model、Service 简化了Service开发 MVC模式的核心思想是将应用程序分为三个主要部分: - **模型(Model)**:负责处理应用程序的数据和业务逻辑。它包含了应用程序的状态和操作这些状态的方法。模型通常与数据库交互,以存储和检索数据。 - **视图(View)**:负责处理用户界面的显示和用户交互。它通常使用 HTML、CSS 和 JavaScript 等技术来生成用户界面。 - **控制器(Controller)**:负责处理用户请求,并调用模型和视图 ### MVC需要用到注解: **@EnableWebMvc**注释开启MVC配置 **@Configuration** 注释表明这是一个配置类,Spring会自动扫描并加载这个类中的配置。 **@ComponentScan** 注释用于指定Spring应该扫描哪些包以查找组件。在这个例子中,它告诉Spring扫描`org.dulong`包及其子包中的组件。 **@Import** 注释用于导入其他配置类。在ch02这个例子中,它导入了`MvcConfig`类,这个类包含了Spring MVC的配置。 ```java // MvcConfig 类负责配置MVC视图解析器 public class MvcConfig implements WebMvcConfigurer { @Override public void configureViewResolvers(ViewResolverRegistry registry) { // 实例化内部资源视图解析器 InternalResourceViewResolver jsp = new InternalResourceViewResolver(); // 设置前缀 jsp.setPrefix("/WEB-INF/jsp/"); // 设置后缀 jsp.setSuffix(".jsp"); // 注册视图解析器 registry.viewResolver(jsp); } } ``` 编写MVC配置类 ```java // WebConfig 类负责配置Spring MVC的DispatcherServlet public class WebConfig extends AbstractAnnotationConfigDispatcherServletInitializer { /** * @param * @Description: 用于设置Spring的配置类 * @return: Class[] * @Author: dulong * @Date: 2025/5/9 */ @Override protected Class[] getRootConfigClasses() { // 由于我们在RootConfig.class中已经导入了MvcConfig.class // 所以我们默认选择第一个创建的IOC容器,在RootConfig.class中 // 已经配置了MvcConfig.class,所以我们getServletConfigClasses()方法 // 返回一个 new Class[]{RootConfig.class}让IOC容器为DispatcherServlet创建IOC容器 // 所以我们这个方法根据下面创建的IOC容器里面的第一个配置类 // 来获取第一个IOC容器 return new Class[0]; } /** * @param * @Description: 用于设置SpringMVC的配置类 * 在Web环境下创建一个唯一的IOC容器 * @return: Class[] * @Author: dulong * @Date: 2025/5/9 */ @Override protected Class[] getServletConfigClasses() { return new Class[]{RootConfig.class}; } /** * @param * @Description: 用于设置DispatcherServlet的映射 * @return: String[] * @Author: dulong * @Date: 2025/5/9 */ @Override protected String[] getServletMappings() { return new String[]{"/"}; } } ``` ### 静态资源处理 #### 在原本MVC中 传统的 Servlet 容器(如 Tomcat)有一个 DefaultServlet,默认处理静态资源(直接从 webapp/ 目录读取文件)。 但若 DispatcherServlet 映射到 "/",会覆盖 DefaultServlet 的默认行为,导致容器无法处理静态资源。 #### 解决方案 1. **配置 DefaultServlet**:在 web.xml 中配置 DefaultServlet,并设置其映射为 "/"。这样,静态资源请求会由 DefaultServlet 处理,而其他请求由 DispatcherServlet 处理。(**不推荐也不建议这样做**) 2. **配置静态资源处理**:在 Spring MVC 配置类中,通过实现 WebMvcConfigurer 接口,重写 `configureDefaultServletHandling` 方法,启用 DefaultServlet。 3. **配置静态资源路径**:在 Spring MVC 配置类中,通过实现 WebMvcConfigurer 接口,重写 `addResourceHandlers` 方法,配置静态资源路径。 ### SpringMVC注解(基于SpringFramework) 在Controller中,SpringMVC提供了@PostMapping来获取POST请求参数 但是在方法参数写入很麻烦根据以下案例进行查看若需要详细让他详细了解需要写入@RequestParam注解 ```java @PostMapping("/add") public String add(@RequestParam("username") String userName, @RequestParam("age") int userAge, @RequestParam("phones") String[] tels) { System.out.println(userName); System.out.println(userAge); System.out.println(Arrays.toString(tels)); return "index"; } ``` #### 解决方案: 我们可以使用实体类进行编写,这样就方便与简写了我们代码的使用,如果需要添加字段直接从实体类里面添加字段即可,也方便了阅读代码的查看 ```java @PostMapping("/add2") public String add2(User user) { System.out.println(user.getUsername()); System.out.println(user.getAge()); System.out.println(user.getBirthday()); user.getPhones().forEach(System.out::println); System.out.println(user.getCard().getCardNumer()); user.getAddresses().forEach(address -> System.out.println(address.getCity() + " : " + address.getStreet())); return "index"; } ``` ### SpringMVC自定义异常类(基于SpringFramework) 在日常报错中,分为系统报错还有我们自己自定义的报错系统的报错有(Exception)异常类,我们自定义的报错有(自定义异常类)异常类 #### 解决方案: 自定义异常类,在exception中编写一个自定义的异常(用于错误码报出但是这个异常需要继承RuntimeException这个异常类) 列图如下 ```java public class GlobalException extends RuntimeException { // 自定义错误码 private Integer errorcode; public GlobalException(Integer errorcode, String message) { super(message); this.errorcode = errorcode; } public Integer getErrorcode() { return errorcode; } } ``` 根据我们以上自定义的异常类,我们进行编写异常处理类,列图如下 因为我们在SpringMVC中,我们使用@RestControllerAdvice注解进行编写,这个注解是SpringMVC中提供的一个 返回json格式的错误信息的一个注解使用这个标注注解,那么这个注解会自动的返回json格式的错误信息 ,第一种方法就是我们**自定义的异常处理类**,第二种方法就是我们如果**只想对一个controller进行异常处理** ,那么我们就可以使用@ExceptionHandler注解进行编写 ,第三种是系统的异常处理,我们有自定义异常处理类了,但是我们还需要一个Exception来进行兜底,因为我们需要判断如果是系统报出来的系统错误,我们可以使用Exception进行兜底 ,**并不是系统报错然后触发了我们的自定义异常处理类**,所以我们需要使用Exception进行兜底 ```java @RestControllerAdvice(basePackages = "org.dulong.web") public class GlobalExceptionAdivice { /** * @Description: 处理最顶层的自定义异常 * @Author: dulong * @Date: 2025/5/13 */ @ExceptionHandler(GlobalException.class) public ResultVO handleGlobalException(GlobalException e) { e.printStackTrace(); ResultVO resultVO = new ResultVO<>(); resultVO.setCode(e.getErrorcode()); resultVO.setMessage(e.getMessage()); return resultVO; } /** * @param * @Description: 使用@ExceptionHandler注解的方法是异常处理方法 * 如果在controller中定义异常处理方法,那么这些方法 * 仅对当前的controller有效 * ,如果想要全局处理异常,那么就需要使用@ControllerAdvice注解 * @return: * @Author: dulong * @Date: 2025/5/13 */ /* @ExceptionHandler(LoginException.class) public ResultVO handleLoginException(LoginException e) { e.printStackTrace(); ResultVO resultVO = new ResultVO<>(); resultVO.setCode(e.getErrorcode()); resultVO.setMessage(e.getMessage()); return resultVO; } */ /** * @Description: 捕获一个最大的异常兜底 * @Author: dulong * @Date: 2025/5/13 */ @ExceptionHandler(Exception.class) public ResultVO handleException(Exception e) { // 一般公司常用的就是这个当日志使用 system.out.println("系统异常类" + e.getClass().getSimpleName() +"系统错误"+ e.getCause()); e.printStackTrace(); ResultVO resultVO = new ResultVO<>(); resultVO.setCode(HttpStatus.INTERNAL_SERVER_ERROR.value()); resultVO.setMessage("服务器繁忙,请稍后尝试"); return resultVO; } } ``` ### SpringMVC自定义过滤器(基于SpringFramework) 在SpringMVC中,我们使用过滤器,可以过滤掉一些请求,比如登录请求,或者静态资源请求,或者不需要认证的请求等等 或者最常见的是过滤掉一些静态资源请求,比如css,js,图片等等让静态资源请求不进行处理(就是不存在与静态资源文件夹下的文件) #### 解决方案: 以下是过滤器的实现代码(是验证token验证的但是个人还是推荐使用拦截器进行编写) ```java /** * 放行的地址都保存在这个集合中 */ private static List list; @Override public void init(FilterConfig filterConfig) throws ServletException { list = Arrays.asList("/auth", "/login.html", "/index.html"); } @Override public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException { HttpServletRequest request = (HttpServletRequest) servletRequest; HttpServletResponse response = (HttpServletResponse) servletResponse; //判断请求地址,如果是认证和登陆页面则直接放行 String url = request.getRequestURI(); if (list.contains(url)) { filterChain.doFilter(servletRequest, servletResponse); } else { //获取token认证 String token = request.getHeader("auth"); if (token != null && !token.isEmpty() && !token.equals("null")) { filterChain.doFilter(servletRequest, servletResponse); } else { //先判断是否是ajax请求,如果是则响应ResultVO //否则重定向到登陆页面 String requestType = request.getHeader("X-Requested-With"); if ("XMLHttpRequest".equals(requestType)) { //响应vo ResultVO vo = new ResultVO<>(); vo.setCode(HttpStatus.UNAUTHORIZED.value()); vo.setMessage("尚未登录"); response.setContentType("application/json"); //将vo转换成json字符串 String json = new ObjectMapper().writeValueAsString(vo); response.getWriter().write(json); } else { //重定向登陆页面 response.sendRedirect(request.getContextPath() + "/login.html"); } } } } ``` ### SpringMVC自定义拦截器(基于SpringFramework) 在SpringMVC中,我们使用拦截器,可以拦截请求,进行一些处理,比如:登录验证,权限验证等等,我们使用拦截器,需要实现HandlerInterceptor接口,然后重写preHandle方法,在preHandle方法中,我们可以获取到请求和响应,然后进行一些处理,比如登录验证,权限验证等等。 #### 解决方案: 以下是拦截器的实现代码(是验证token验证的) ```java public class LoginInterceptor implements HandlerInterceptor { /** * 放行的地址都保存在这个集合中 */ private static final List WHITELIST = Arrays.asList( "/auth", "/login.html", "/index.html" ); private final ObjectMapper objectMapper = new ObjectMapper(); /** * 在控制器(Controller)方法调用之前 * * @param request * @param response * @param handler * @return 返回值是一个boolean类型,true表示放行请求, * false表示拒绝,请求将不会到达控制器方法 * @throws Exception */ @Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { String uri = request.getRequestURI(); // 白名单直接放行 if (WHITELIST.contains(uri)) { return true; } // 从 header 获取 token String token = request.getHeader("auth"); if (token != null && !token.isEmpty() && !"null".equals(token)) { // token 存在,则放行 return true; } // token 不存在,根据是否 Ajax 请求返回不同内容 String xhr = request.getHeader("X-Requested-With"); if ("XMLHttpRequest".equalsIgnoreCase(xhr)) { // AJAX: 返回 JSON 401 ResultVO vo = new ResultVO<>(); vo.setCode(HttpStatus.UNAUTHORIZED.value()); vo.setMessage("尚未登录"); response.setStatus(HttpStatus.UNAUTHORIZED.value()); response.setContentType("application/json;charset=UTF-8"); String json = objectMapper.writeValueAsString(vo); response.getWriter().write(json); } else { // 普通请求:重定向到登录页 response.sendRedirect(request.getContextPath() + "/login.html"); } // 阻止后续处理 return false; } /** * 在控制器方法调用之后,视图渲染之前执行 * 前提条件:preHandle返回true才会执行 * * @param request * @param response * @param handler * @param modelAndView * @throws Exception */ @Override public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception { System.out.println("postHandle..."); } /** * 整个请求处理完成后(视图渲染完毕) * 前提条件:preHandle返回true才会执行 * * @param request * @param response * @param handler * @param ex * @throws Exception */ @Override public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception { System.out.println("afterCompletion..."); } } ``` ### 拦截器中的跨域(常见问题) 当然,SpringMVC中, also allow cross-origin requests,即允许跨域请求。 拦截器中的跨域配置代码如下: 但是我们需要在MvcConfig这个类下实现一下 WebMvcConfigurer进行配置 ```java public class MvcConfig implements WebMvcConfigurer { /** * 静态资源处理 * @param configurer */ @Override public void configureDefaultServletHandling(DefaultServletHandlerConfigurer configurer) { configurer.enable(); } /** * 装配拦截器 * @param registry */ @Override public class MvcConfig implements WebMvcConfigurer { //可以配置多个拦截器 //registry.addInterceptor(new XxxInterceptor()) } @Override public void addCorsMappings(CorsRegistry registry) { registry.addMapping("/**") .allowedOriginPatterns("http://localhost:5000") .allowedMethods("GET", "POST", "PUT", "DELETE", "OPTIONS") .allowedHeaders("*"); } } ``` 配置完之后我们再通过一个拦截器进行拦截"GET", "POST" 是普通请求所以,我们不需要进行一个放行 如果不放行"PUT", "DELETE", "OPTIONS" 这几个进阶接口其他接口跨域是无法使用的 注意:在"PUT", "DELETE", "OPTIONS" 这几个进阶请求是需要在拦截器中放行的(因为他默认有一个默认的OPTIONS的拦截器进行校验所以我们需要进行放行) ```java public class LoginInterceptor implements HandlerInterceptor { /** * 在控制器(Controller)方法调用之前 * @param request * @param response * @param handler * @return 返回值是一个boolean类型,true表示放行请求, * false表示拒绝,请求将不会到达控制器方法 * @throws Exception */ @Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { System.out.println("preHandle..."); //如果是预检请求就直接放行 if("OPTIONS".equals(request.getMethod())) { return true; } return true; } } ``` ### SpringMVC自定义IO流(基于SpringFramework) 在日常生活中我们会有很多需要上传文件的需求,比如 1. 上传头像 2. 上传文件 3. 上传视频 4. 上传图片 5. 上传音频 等等,这些都需要上传文件,那么我们如何实现文件上传呢?我们可以使用SpringMVC提供的IO流来实现文件上传,下面我们来看一下如何实现文件上传。 #### 解决方案: 以下是文件上传的实现代码 ```java @RestController public class UploadController extends BaseController { /** * @param * @Description: MultipartFile 是Spring MVC 封装上传文件的对象 * 这个对象包含文件名、类型、大小、以及文件的输入流等信息 * @return: * @Author: dulong * @Date: 2025/5/15 */ @PostMapping("/upload") public ResultVO upload(@RequestParam("files") MultipartFile[] files) throws IOException { //获取上传的目录 // 获取上传的目录 File uploadDir = new File("D:" + File.separator + "upload"); // mac的 //File uploadDir = new File("/Users/xxx/work/upload"); if(!uploadDir.exists()) { uploadDir.mkdirs(); } //循环文件数组 for(MultipartFile file : files) { //获取文件名 String fileName = file.getOriginalFilename(); System.out.println(fileName); //获取文件大小 long size = file.getSize(); System.out.println(size); //获取文件的类型 String contentType = file.getContentType(); System.out.println(contentType); //构建上传路径(上传目录的绝对路径 + 文件名) Path path = FileSystems.getDefault().getPath(uploadDir.getAbsolutePath(), fileName); //执行上传 file.transferTo(path); //响应vo } return success(); } } ```