SpringBoot——Web开发

1、SpringMVC自动配置概览

https://docs.spring.io/spring-boot/docs/current/reference/html/web.html#web.servlet.spring-mvc.auto-configuration


2、简单功能分析

2.1、静态资源访问

2.1.1、静态资源目录

静态资源放在类路径下: /static or /public or /resources or /META-INF/resources

访问:当前项目根路径/ + 静态资源名

原理: 静态映射/**。

请求进来,先去找Controller看能不能处理。不能处理的所有请求又都交给静态资源处理器。静态资源也找不到则响应404页面。

2.1.2、静态资源访问前缀

默认无前缀

spring:
  mvc:
    static-path-pattern: /res/**

访问:当前项目根路径/ + res + /静态资源名

2.1.3、静态资源目录

默认是在2.1.1节中的四个目录中,如果我们想改变默认目录:

spring:
  web:
    resources:
      static-locations: [classpath:/haha/]

2.1.4、webjar

https://www.webjars.org/

自动映射 /webjars/**

<dependency>
    <groupId>org.webjars</groupId>
    <artifactId>jquery</artifactId>
    <version>3.6.0</version>
</dependency>

访问地址:http://localhost:8080/webjars/jquery/3.6.0/jquery.js 后面地址要按照依赖里面的包路径

在这里插入图片描述

2.2、欢迎页支持

  • 静态资源路径下创建 index.html
    • 可以配置静态资源路径
    • 但是不可以配置静态资源的访问前缀。否则导致 index.html 不能被默认访问
  • controller能处理/index
spring:
#  mvc:
#    static-path-pattern: /res/**  这个会导致welcome page功能失效

  web:
    resources:
      static-locations: [classpath:/haha/]

2.3、自定义 Favicon

favicon.ico 放在静态资源目录下即可。名字必须是 favicon.ico

spring:
#  mvc:
#    static-path-pattern: /res/**  这个会导致Favicon功能失效

  web:
    resources:
      static-locations: [classpath:/haha/]

2.4、静态资源配置原理

  • SpringBoot 启动默认加载 xxxAutoConfiguration 类(自动配置类)

    在这里插入图片描述

  • SpringMVC 功能的自动配置类 WebMvcAutoConfiguration,生效

    @Configuration(
        proxyBeanMethods = false
    )
    @ConditionalOnWebApplication(
        type = Type.SERVLET
    )
    @ConditionalOnClass({Servlet.class, DispatcherServlet.class, WebMvcConfigurer.class})
    @ConditionalOnMissingBean({WebMvcConfigurationSupport.class})
    @AutoConfigureOrder(-2147483638)
    @AutoConfigureAfter({DispatcherServletAutoConfiguration.class, TaskExecutionAutoConfiguration.class, ValidationAutoConfiguration.class})
    public class WebMvcAutoConfiguration {
        ......
    
  • 给容器中配了什么:

    @Configuration(
        proxyBeanMethods = false
    )
    @Import({WebMvcAutoConfiguration.EnableWebMvcConfiguration.class})
    @EnableConfigurationProperties({WebMvcProperties.class, WebProperties.class})
    @Order(0)
    public static class WebMvcAutoConfigurationAdapter implements WebMvcConfigurer, ServletContextAware {
        ......
    

    @EnableConfigurationProperties,说明配置文件的相关属性和xxx进行了绑定。

    主要有两个:WebMvcProperties 和 WebProperties。

    先来看WebMvcProperties:

    @ConfigurationProperties(
        prefix = "spring.mvc"
    )
    public class WebMvcProperties {......
    

    可以看到 WebMvcProperties 和 spring.mvc 进行了绑定

    再来看WebProperties:

    @ConfigurationProperties("spring.web")
    public class WebProperties {
        ......
    

    可以看到 WebProperties 和 spring.web 进行了绑定

    配置类只有一个有参构造器:

    有参构造器中的所有参数都会从容器中确定。

    public WebMvcAutoConfigurationAdapter(WebProperties webProperties, WebMvcProperties mvcProperties, ListableBeanFactory beanFactory, ObjectProvider<HttpMessageConverters> messageConvertersProvider, ObjectProvider<WebMvcAutoConfiguration.ResourceHandlerRegistrationCustomizer> resourceHandlerRegistrationCustomizerProvider, ObjectProvider<DispatcherServletPath> dispatcherServletPath, ObjectProvider<ServletRegistrationBean<?>> servletRegistrations) {
        this.resourceProperties = webProperties.getResources();
        this.mvcProperties = mvcProperties;
        this.beanFactory = beanFactory;
        this.messageConvertersProvider = messageConvertersProvider;
        this.resourceHandlerRegistrationCustomizer = (WebMvcAutoConfiguration.ResourceHandlerRegistrationCustomizer)resourceHandlerRegistrationCustomizerProvider.getIfAvailable();
        this.dispatcherServletPath = dispatcherServletPath;
        this.servletRegistrations = servletRegistrations;
        this.mvcProperties.checkConfiguration();
    }
    

    分析其参数:

    • WebProperties webProperties

      获取和 spring.web 绑定的所有值的对象

    • WebMvcProperties mvcProperties

      获取和 spring.mvc 绑定的所有值的对象

    • ListableBeanFactory beanFactory

      Spring 的 beanFactory

    • ObjectProvider<HttpMessageConverters> messageConvertersProvider

      找到所有的messageConvertersProvider

    • ObjectProvider<WebMvcAutoConfiguration.ResourceHandlerRegistrationCustomizer> resourceHandlerRegistrationCustomizerProvider

      找到资源处理器的自定义器

    • ObjectProvider<DispatcherServletPath> dispatcherServletPath

    • ObjectProvider<ServletRegistrationBean<?>> servletRegistrations

      给应用注册Servlet、Filter…

    资源处理的默认规则:

    public void addResourceHandlers(ResourceHandlerRegistry registry) {
        if (!this.resourceProperties.isAddMappings()) {
            logger.debug("Default resource handling disabled");
        } else {
            this.addResourceHandler(registry, "/webjars/**", "classpath:/META-INF/resources/webjars/");
            this.addResourceHandler(registry, this.mvcProperties.getStaticPathPattern(), (registration) -> {
                registration.addResourceLocations(this.resourceProperties.getStaticLocations());
                if (this.servletContext != null) {
                    ServletContextResource resource = new ServletContextResource(this.servletContext, "/");
                    registration.addResourceLocations(new Resource[]{resource});
                }
    
            });
        }
    }
    

    先看源代码,如果this.resourceProperties.isAddMappings()是true,才会访问else下边的代码,可以看到是处理静态资源的路径的。

    public static class Resources {
        private static final String[] CLASSPATH_RESOURCE_LOCATIONS = new String[]{"classpath:/META-INF/resources/", "classpath:/resources/", "classpath:/static/", "classpath:/public/"};
        private String[] staticLocations;
        private boolean addMappings;
        private boolean customized;
        private final WebProperties.Resources.Chain chain;
        private final WebProperties.Resources.Cache cache;
    
        public Resources() {
            this.staticLocations = CLASSPATH_RESOURCE_LOCATIONS;
            this.addMappings = true;
            this.customized = false;
            this.chain = new WebProperties.Resources.Chain();
            this.cache = new WebProperties.Resources.Cache();
        }
        ......
    

    默认为true。

    spring:
    #  mvc:
    #    static-path-pattern: /res/**
    
      web:
        resources:
          static-locations: [classpath:/haha/]
          add-mappings: false #禁用所有静态资源规则
    

3、请求参数处理

3.1、请求映射

3.1.1、Rest使用与原理

  • @xxxMapping;

  • Rest风格支持(使用HTTP请求方式动词来表示对资源的操作)

    • 以前:/getUser 获取用户 /deleteUser 删除用户 /editUser 修改用户 /saveUser 保存用户

    • 现在: /user GET-获取用户 DELETE-删除用户 PUT-修改用户 POST-保存用户

    • 核心Filter;HiddenHttpMethodFilter

      用法: 表单method=post,隐藏域 _method=put

      SpringBoot中手动开启

在控制器中添加四个请求映射:

@RequestMapping(value = "/user",method = RequestMethod.GET)
public String getUser(){
    return "GET-张三";
}

@RequestMapping(value = "/user",method = RequestMethod.POST)
public String saveUser(){
    return "POST-张三";
}


@RequestMapping(value = "/user",method = RequestMethod.PUT)
public String putUser(){
    return "PUT-张三";
}

@RequestMapping(value = "/user",method = RequestMethod.DELETE)
public String deleteUser(){
    return "DELETE-张三";
}

index.html中创建对应请求的表单:

<h1>测试REST风格</h1>
<form action="/user" method="get">
    <input value="REST-GET 提交" type="submit">
</form>

<form action="/user" method="post">
    <input value="REST-POST 提交" type="submit">
</form>

<form action="/user" method="post">
    <input name="_method" type="hidden" value="DELETE">
    <input value="REST-DELETE 提交" type="submit">
</form>

<form action="/user" method="post">
    <input name="_method" type="hidden" value="put">
    <input value="REST-PUT 提交" type="submit">
</form>

在这里需要配置<input name="_method" type="hidden" value="put">的原因,查看源码:

public class HiddenHttpMethodFilter extends OncePerRequestFilter {
    private static final List<String> ALLOWED_METHODS;
    public static final String DEFAULT_METHOD_PARAM = "_method";
    private String methodParam = "_method";
    ......

这里必须有一个"_method"前缀。而且:

protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
    HttpServletRequest requestToUse = request;
    if ("POST".equals(request.getMethod()) && request.getAttribute("javax.servlet.error.exception") == null) {
        String paramValue = request.getParameter(this.methodParam);
        if (StringUtils.hasLength(paramValue)) {
            String method = paramValue.toUpperCase(Locale.ENGLISH);
            if (ALLOWED_METHODS.contains(method)) {
                requestToUse = new HiddenHttpMethodFilter.HttpMethodRequestWrapper(request, method);
            }
        }
    }

    filterChain.doFilter((ServletRequest)requestToUse, response);
}

必须是POST才可以。

但是经过测试还是无法得到要求。再看查看SpringBoot为我们自动注入的HiddenHttpMethodFilter过滤器源码:

@Bean
@ConditionalOnMissingBean({HiddenHttpMethodFilter.class})
@ConditionalOnProperty(
    prefix = "spring.mvc.hiddenmethod.filter",
    name = {"enabled"}
)
public OrderedHiddenHttpMethodFilter hiddenHttpMethodFilter() {
    return new OrderedHiddenHttpMethodFilter();
}

我们没有配置spring.mvc.hiddenmethod.filter。那么就在application.yaml中添加配置:

spring:
#  mvc:
#    static-path-pattern: /res/**

  web:
    resources:
      static-locations: [classpath:/haha/]
      add-mappings: true
  mvc:
    hiddenmethod:
      filter:
        enabled: true

成功运行!

Rest原理(表单提交要使用REST的时候)

  • 表单提交会带上_method=PUT

  • 请求过来被HiddenHttpMethodFilter拦截

    请求是否正常,并且是POST

    • 获取到_method的值
    • 兼容以下请求:PUT、DELETE、PATCH
    • 原生request(post),包装模式requesWrapper重写了getMethod方法,返回的是传入的值;
    • 过滤器链放行的时候用wrapper。以后的方法调用getMethod的调用requesWrapper的。

Rest使用客户端工具:如PostMan直接发送Put、delete等方式请求,无需Filter。

扩展:如何把_method 这个名字换成我们自己喜欢的:

查看源码我们知道:

public class HiddenHttpMethodFilter extends OncePerRequestFilter {
    private static final List<String> ALLOWED_METHODS;
    public static final String DEFAULT_METHOD_PARAM = "_method";
    private String methodParam = "_method";
    ......

虽然DEFAULT_METHOD_PARAM是final,但是methodParam是可以更改的。而且没有HiddenHttpMethodFilter时,SpringBoot才会给我们自动注入,那么我们就自己配置一个,并且设定methodParam的值。

@Configuration(proxyBeanMethods = false)
public class WebConfig {
    public HiddenHttpMethodFilter hiddenHttpMethodFilter(){
        HiddenHttpMethodFilter hiddenHttpMethodFilter = new HiddenHttpMethodFilter();
        hiddenHttpMethodFilter.setMethodParam("_m");
        return hiddenHttpMethodFilter;
    }
}

3.1.2、请求映射原理

我们知道无论什么请求,都要经过DispatchServlet,而且一定要重写doGet()doPost()方法:

在这里插入图片描述

查看继承树,发现DispatcherServlet继承了FrameworkServlet,FrameworkServlet继承了HttpServletBean。在HttpServletBean中并没有发现doGet()doPost()方法,在FrameworkServlet中找到了doGet()doPost()方法。

protected final void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
    this.processRequest(request, response);
}

protected final void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
    this.processRequest(request, response);
}

无论是doGet()doPost()方法,都最终调用了本类的processRequest()方法。

protected final void processRequest(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
    long startTime = System.currentTimeMillis();
    Throwable failureCause = null;
    LocaleContext previousLocaleContext = LocaleContextHolder.getLocaleContext();
    LocaleContext localeContext = this.buildLocaleContext(request);
    RequestAttributes previousAttributes = RequestContextHolder.getRequestAttributes();
    ServletRequestAttributes requestAttributes = this.buildRequestAttributes(request, response, previousAttributes);
    WebAsyncManager asyncManager = WebAsyncUtils.getAsyncManager(request);
    asyncManager.registerCallableInterceptor(FrameworkServlet.class.getName(), new FrameworkServlet.RequestBindingInterceptor());
    this.initContextHolders(request, localeContext, requestAttributes);

    try {
        this.doService(request, response);
    } catch (IOException | ServletException var16) {
        failureCause = var16;
        throw var16;
    } catch (Throwable var17) {
        failureCause = var17;
        throw new NestedServletException("Request processing failed", var17);
    } finally {
        this.resetContextHolders(request, previousLocaleContext, previousAttributes);
        if (requestAttributes != null) {
            requestAttributes.requestCompleted();
        }

        this.logResult(request, response, (Throwable)failureCause, asyncManager);
        this.publishRequestHandledEvent(request, response, startTime, (Throwable)failureCause);
    }

}

前边的都是获取参数配置项,最主要的就是try之后的doService()方法。

protected abstract void doService(HttpServletRequest request, HttpServletResponse response) throws Exception;

FrameworkServlet类中的doService()方法是abstract(抽象)类,在FrameworkServlet类中并没有实现。因此我们需要在其继承类中寻找其实现。

在DispatcherServlet中有doService()方法的实现:

protected void doService(HttpServletRequest request, HttpServletResponse response) throws Exception {
    this.logRequest(request);
    Map<String, Object> attributesSnapshot = null;
    if (WebUtils.isIncludeRequest(request)) {
        attributesSnapshot = new HashMap();
        Enumeration attrNames = request.getAttributeNames();

        label116:
        while(true) {
            String attrName;
            do {
                if (!attrNames.hasMoreElements()) {
                    break label116;
                }

                attrName = (String)attrNames.nextElement();
            } while(!this.cleanupAfterInclude && !attrName.startsWith("org.springframework.web.servlet"));

            attributesSnapshot.put(attrName, request.getAttribute(attrName));
        }
    }

    request.setAttribute(WEB_APPLICATION_CONTEXT_ATTRIBUTE, this.getWebApplicationContext());
    request.setAttribute(LOCALE_RESOLVER_ATTRIBUTE, this.localeResolver);
    request.setAttribute(THEME_RESOLVER_ATTRIBUTE, this.themeResolver);
    request.setAttribute(THEME_SOURCE_ATTRIBUTE, this.getThemeSource());
    if (this.flashMapManager != null) {
        FlashMap inputFlashMap = this.flashMapManager.retrieveAndUpdate(request, response);
        if (inputFlashMap != null) {
            request.setAttribute(INPUT_FLASH_MAP_ATTRIBUTE, Collections.unmodifiableMap(inputFlashMap));
        }

        request.setAttribute(OUTPUT_FLASH_MAP_ATTRIBUTE, new FlashMap());
        request.setAttribute(FLASH_MAP_MANAGER_ATTRIBUTE, this.flashMapManager);
    }

    RequestPath previousRequestPath = null;
    if (this.parseRequestPath) {
        previousRequestPath = (RequestPath)request.getAttribute(ServletRequestPathUtils.PATH_ATTRIBUTE);
        ServletRequestPathUtils.parseAndCache(request);
    }

    try {
        this.doDispatch(request, response);
    } finally {
        if (!WebAsyncUtils.getAsyncManager(request).isConcurrentHandlingStarted() && attributesSnapshot != null) {
            this.restoreAttributesAfterInclude(request, attributesSnapshot);
        }

        if (this.parseRequestPath) {
            ServletRequestPathUtils.setParsedRequestPath(previousRequestPath, request);
        }

    }

}

前边的也是一堆初始化,最重要的也是try之后的一个doDispatch()方法。也就是说:

SpringMVC功能分析都要从org.springframework.web.servlet.DispatcherServletdoDispatch()开始。

......
try {
    ModelAndView mv = null;
    Object dispatchException = null;

    try {
        processedRequest = this.checkMultipart(request);
        multipartRequestParsed = processedRequest != request;
        //找到当前请求使用哪个Handler(Controller)处理
        mappedHandler = this.getHandler(processedRequest);
        if (mappedHandler == null) {
            this.noHandlerFound(processedRequest, response);
            return;
        }
        ......

查看getHandler()方法:

@Nullable
protected HandlerExecutionChain getHandler(HttpServletRequest request) throws Exception {
    if (this.handlerMappings != null) {
        Iterator var2 = this.handlerMappings.iterator();

        while(var2.hasNext()) {
            HandlerMapping mapping = (HandlerMapping)var2.next();
            HandlerExecutionChain handler = mapping.getHandler(request);
            if (handler != null) {
                return handler;
            }
        }
    }

    return null;
}

其中有一个handlerMappings,查看,有5个:

在这里插入图片描述

RequestMappingHandlerMapping 保存了所有 @RequestMapping 和 handler 的映射规则。

那这个是怎么找到映射的呢?

可以看到handlerMappings后边使用了一个迭代,分别拿出这5个映射规则(mapping),一一对应:

在这里插入图片描述

总结:

所有的请求映射都在handlerMappings中。

  • SpringBoot 自动配置欢迎页的 WelcomePageHandlerMapping 。访问 /能访问到index.html;
  • SpringBoot 自动配置了默认的 RequestMappingHandlerMapping
  • 请求进来,挨个尝试所有的 handlerMappings 看是否有请求信息
    • 如果有就找到这个请求对应的 handler
    • 如果没有就是下一个 handlerMapping
  • 我们需要一些自定义的映射处理,我们也可以自己给容器中放handlerMappings。自定义HandlerMapping

3.2、普通参数与基本注解

3.2.1、注解

  • @PathVariable:获取请求路径中的变量

    获取路径中的值,赋值给方法中的变量。

    <a href="/car/3/owner/lisi">/car/{id}/owner/{username}</a>
    
    // car/id/owner/username
    @GetMapping("/car/{id}/owner/{username}")
    public Map<String, Object> getCar(@PathVariable("id") Integer id,
                                      @PathVariable("username") String name,
                                      @PathVariable Map<String, String> pv){
    
        HashMap<String, Object> map = new HashMap<>();
    
        map.put("id", id);
        map.put("name",name);
        map.put("pv", pv);
        return map;
    }
    

    在这里插入图片描述

  • @RequestHeader:获取请求头

    @RestController
    public class ParameterTestController {
    
        // car/id/owner/username
        @GetMapping("/car/{id}/owner/{username}")
        public Map<String, Object> getCar(@PathVariable("id") Integer id,
                                          @PathVariable("username") String name,
                                          @PathVariable Map<String, String> pv,
                                          @RequestHeader("User-Agent") String userAgent,
                                          @RequestHeader Map<String, String> header){
    
            HashMap<String, Object> map = new HashMap<>();
    
            map.put("id", id);
            map.put("name",name);
            map.put("pv", pv);
    
            map.put("userAgent", userAgent);
            map.put("headers", header);
            return map;
        }
    }
    

    加入@RequestHeader("User-Agent") String userAgent@RequestHeader Map<String, String> header

    在这里插入图片描述

  • @RequestParam:获取请求参数中的值

    <a href="/car/3/owner/lisi?age=18&inters=basketball&inters=game">/car/{id}/owner/{username}</a>
    

    我想要获取age、inters中的值。

    @GetMapping("/car/{id}/owner/{username}")
    public Map<String, Object> getCar(@PathVariable("id") Integer id,
                                      @PathVariable("username") String name,
                                      @PathVariable Map<String, String> pv,
                                      @RequestHeader("User-Agent") String userAgent,
                                      @RequestHeader Map<String, String> header,
                                      @RequestParam("age") Integer age,
                                      @RequestParam("inters") List<String> inters,
                                      @RequestParam Map<String, String> params){
    

    加入@RequestParam("age") Integer age@RequestParam("inters") List<String> inters@RequestParam Map<String, String> params

    在这里插入图片描述

  • @CookieValue:获取Cookie的值

    在这里插入图片描述

    @GetMapping("/car/{id}/owner/{username}")
    public Map<String, Object> getCar(@PathVariable("id") Integer id,
                                      @PathVariable("username") String name,
                                      @PathVariable Map<String, String> pv,
                                      @RequestHeader("User-Agent") String userAgent,
                                      @RequestHeader Map<String, String> header,
                                      @RequestParam("age") Integer age,
                                      @RequestParam("inters") List<String> inters,
                                      @RequestParam Map<String, String> params,
                                      @CookieValue("ol_offset") String ol_offset,
                                      @CookieValue("ol_offset") Cookie cookie){
    

    加入@CookieValue("ol_offset") String ol_offset@CookieValue("ol_offset") Cookie cookie

    在这里插入图片描述

    System.out.println(cookie); //javax.servlet.http.Cookie@701df2b8
    
  • @RequestBody:获取请求体

    我们知道请求体只存在于POST的请求方式。

    index.html写一个表单:

    <form action="/save" method="post">
        测试@RequestBody获取数据<br/>
        用户名:<input name="userName" /> <br>
        邮箱:<input name="email">
        <input type="submit" value="提交">
    </form>
    

    重新定义一个控制器方法:

    @PostMapping("/save")
    public Map postMethod(@RequestBody String content){
        HashMap<Object, Object> map = new HashMap<>();
        map.put("content", content);
        return map;
    }
    

    在这里插入图片描述

  • @RequestAttribute:获取Request域

    一般用来获取Request域中的值,进行域属性共享和传递。

    创建一个新的控制器,使用@Controller,一般用来实现页面跳转。在这里模拟页面跳转,使用转发。

    @Controller //普通的控制器,一般用来实现页面跳转
    public class RequestController {
    
        @GetMapping("/goto")
        public String goToPage(HttpServletRequest request){
            request.setAttribute("msg", "成功了...");
            request.setAttribute("code", 200);
            return "forward:/success"; //转发到 /success请求
        }
    
        @ResponseBody
        @GetMapping("/success")
        public Map success(@RequestAttribute("msg") String msg,
                           @RequestAttribute("code") Integer code,
                           HttpServletRequest request){
    
            Object msg1 = request.getAttribute("msg");
            HashMap<Object, Object> map = new HashMap<>();
    
            map.put("requestMethod", msg1);
            map.put("annotation_msg", msg);
            return map;
        }
    }
    

    在success方法中,使用注解@RequestAttribute("msg") String msg和原生的HttpServletRequest request分别获取request域中的值。

    在这里插入图片描述

  • @MatrixVariable:获取矩阵变量

    路径的了两种写法:

    1. /cars/{path}?xxx=xxx&aaa=ccc

      称之为queryString,查询字符串

      可以使用@RequestParam获取。

    2. /cars/sell;low=34;brand=byd,audi,yd

      称之为矩阵变量

    一个面试题:页面开发,如果cookie禁用了,session里边的内容怎么使用?

    使用session里边的内容,要使用session.set(a,b),我们要知道jsessionid,而jsessionid是被保存在cookie中,每次发请求时携带。

    把cookie禁用了,那么就获取不到session了。

    可以使用url重写:/abc;jsessionid=xxx。把cookie的值使用矩阵变量的方式进行传递。

    矩阵变量的语法:

    • 矩阵变量需要在SpringBoot中手动开启
    • 根据RFC3986的规范,矩阵变量应当绑定在路径变量中
    • 若是有多个变量,应当使用英文符号;进行分隔
    • 若是一个矩阵变量有多个值,应当使用英文,进行分隔,或者命名多个重复的key即可

    index.html中写上测试链接:

    <a href="/cars/sell;low=34;brand=byd,audi,yd">@MatrixVariable 矩阵变量1 /cars/{path}</a>
    <a href="/cars/sell;low=34;brand=byd,audi;brand=yd">@MatrixVariable 矩阵变量2</a>
    <a href="/boss/1;age=20/2;age=10">@MatrixVariable 矩阵变量3 /boss/{bossId}/{empId}</a>
    

    SpringBoot中需要手动开启矩阵变量,在web的配置类中:

    @Override
    public void configurePathMatch(PathMatchConfigurer configurer) {
        UrlPathHelper urlPathHelper = new UrlPathHelper();
        //不移除;后边的内容,矩阵变量就可以生效
        urlPathHelper.setRemoveSemicolonContent(false);
        configurer.setUrlPathHelper(urlPathHelper);
    }
    

    测试第一种情况:

    // /cars/sell;low=34;brand=byd,audi,yd
    @GetMapping("/cars/{path}")
    public Map carsSell(@MatrixVariable("low") Integer low,
                        @MatrixVariable("brand") List<String> brand,
                        @PathVariable("path") String path){
        HashMap<Object, Object> map = new HashMap<>();
    
        map.put("low", low);
        map.put("brand", brand);
        map.put("path", path);
        return map;
    }
    

    我们来看看请求路径到底是什么:

    在这里插入图片描述

    测试第三个,每一个路径下边都有值:

    // /boss/1;age=20/2;age=10
    @GetMapping("/boss/{bossId}/{empId}")
    public Map boss(@MatrixVariable(value = "age", pathVar = "bossId") Integer bossAge,
                    @MatrixVariable(value = "age", pathVar = "empId") Integer empAge){
        HashMap<Object, Object> map = new HashMap<>();
    
        map.put("bossAge", bossAge);
        map.put("empAge", empAge);
        return map;
    }
    

    在这里插入图片描述

@PathVariable:获取请求路径中的变量

@RequestHeader:获取请求头

@RequestParam:获取请求参数

@MatrixVariable:获取矩阵变量

@CookieValue:获取cookie

@RequestBody:获取请求体

各种类型参数解析原理:

在DispathcerServlet的doDispatcher()方法打上断点,调试运行:

发送localhost:8080/car/3/owner/lisi?age=18&inters=basketball&inters=game请求。

在这里插入图片描述

  • HandlerMapping中找到能处理请求的Handler(Controller.method())

  • 为当前 Handler 找一个适配器 HandlerAdapter,默认 HandlerAdapter 有四种;

    在这里插入图片描述

  • 适配器执行目标方法并确定方法参数的每一个值

    mv = ha.handle(processedRequest, response, mappedHandler.getHandler());
    
    mav = invokeHandlerMethod(request, response, handlerMethod); //真正执行目标方法
    
    
    //ServletInvocableHandlerMethod
    Object returnValue = invokeForRequest(webRequest, mavContainer, providedArgs);
    //获取方法的参数值
    Object[] args = getMethodArgumentValues(request, mavContainer, providedArgs);
    

    参数解析器-HandlerMethodArgumentResolver

    确定将要执行的目标方法的每一个参数的值是什么

    SpringMVC目标方法能写多少种参数类型。取决于参数解析器。

    在这里插入图片描述

返回值处理器

在这里插入图片描述


4、视图解析与模板引擎

4.1、试图解析

4.2、模板引擎-Thymeleaf

4.2.1、thymeleaf简介

Thymeleaf is a modern server-side Java template engine for both web and standalone environments, capable of processing HTML, XML, JavaScript, CSS and even plain text.

现代化、服务端Java模板引擎

4.2.2、基本语法

表达式:

表达式名字语法用途
变量取值${…}获取请求域、session域、对象等值
选择变量*{…}获取上下文对象值
消息#{…}获取国际化等值
链接@{…}生成链接
片段表达式~{…}jsp:include 作用,引入公共页面片段

字面值:

  • 文本值: ‘one text’ , ‘Another one!’ **,…**数字: 0 , 34 , 3.0 , 12.3 **,…**布尔值: true , false
  • 空值: null
  • 变量: one,two,… 变量不能有空格

文本操作:

  • 字符串拼接: +
  • 变量替换: |The name is ${name}|

数学运算:

  • 运算符: + , - , * , / , %

布尔运算:

  • 运算符: and , or
  • 一元运算: ! , not

比较运算:

  • 比较: > , < , >= , <= ( gt , lt , ge , le **)**等式: == , != ( eq , ne )

条件运算:

  • If-then: (if) ? (then)
  • If-then-else: (if) ? (then) : (else)
  • Default: (value) ?: (defaultvalue)

特殊操作:

  • 无操作: _

设置属性值-th:attr:

  • 设置单个值

    <form action="subscribe.html" th:attr="action=@{/subscribe}">
      <fieldset>
        <input type="text" name="email" />
        <input type="submit" value="Subscribe!" th:attr="value=#{subscribe.submit}"/>
      </fieldset>
    </form>
    
  • 设置多个值

    <img src="../../images/gtvglogo.png"  th:attr="src=@{/images/gtvglogo.png},title=#{logo},alt=#{logo}" />
    
  • 以上两个的代替写法 th:xxxx

    <input type="submit" value="Subscribe!" th:value="#{subscribe.submit}"/>
    <form action="subscribe.html" th:action="@{/subscribe}">
    

迭代:

<tr th:each="prod : ${prods}">
    <td th:text="${prod.name}">Onions</td>
    <td th:text="${prod.price}">2.41</td>
    <td th:text="${prod.inStock}? #{true} : #{false}">yes</td>
</tr>
<tr th:each="prod,iterStat : ${prods}" th:class="${iterStat.odd}? 'odd'">
  <td th:text="${prod.name}">Onions</td>
  <td th:text="${prod.price}">2.41</td>
  <td th:text="${prod.inStock}? #{true} : #{false}">yes</td>
</tr>

条件运算:

<a href="comments.html"
th:href="@{/product/comments(prodId=${prod.id})}"
th:if="${not #lists.isEmpty(prod.comments)}">view</a>
<div th:switch="${user.role}">
  <p th:case="'admin'">User is an administrator</p>
  <p th:case="#{roles.manager}">User is a manager</p>
  <p th:case="*">User is some other thing</p>
</div>

4.3、thymeleaf使用

4.3.1、引入Starter

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

4.3.2、自动配置好了thymeleaf

@EnableConfigurationProperties({ThymeleafProperties.class})
@ConditionalOnClass({TemplateMode.class, SpringTemplateEngine.class})
@AutoConfigureAfter({WebMvcAutoConfiguration.class, WebFluxAutoConfiguration.class})
@Import({ReactiveTemplateEngineConfiguration.class, DefaultTemplateEngineConfiguration.class})
public class ThymeleafAutoConfiguration {
    ......

自动配好的策略:

  1. 所有thymeleaf的配置值都在 ThymeleafProperties

  2. 配置好了 SpringTemplateEngine

  3. 配好了 thymeleafViewResolver

  4. 我们只需要直接开发页面

    @ConfigurationProperties(
        prefix = "spring.thymeleaf"
    )
    public class ThymeleafProperties {
        private static final Charset DEFAULT_ENCODING;
        public static final String DEFAULT_PREFIX = "classpath:/templates/";
        public static final String DEFAULT_SUFFIX = ".html";
        ......
    

    页面放在"classpath:/templates/"类路径下的templates下,后缀都是.html

    在templates目录下新建一个success.html

    <!DOCTYPE html>
    <html lang="en" xmlns:th="http://www.thymeleaf.org">
    <head>
        <meta charset="UTF-8">
        <title>Title</title>
    </head>
    <body>
    
        <h1 th:text="${msg}">哈哈</h1>
        <h2>
            <a href="www.baidu.com" th:href="${link}">去百度</a>
            <a href="www.baidu.com" th:href="@{/link}">去百度</a>
        </h2>
    </body>
    </html>
    

    新建一个控制器:

    @GetMapping(&quot;/baidu&quot;)
    public String baidu(Model model){
    
        //model中的数据会被放在请求域中,request.setAttribute(&quot;a&quot;, aa)
        model.addAttribute(&quot;msg&quot;, &quot;你好,百度&quot;);
        model.addAttribute(&quot;link&quot;,&quot;http://www.baidu.com&quot;);
        return &quot;success&quot;;
    }
    

4.4、构建后台管理系统

4.4.1、项目创建

thymeleaf、web-starter、devtools、lombok

4.4.2、静态资源处理

自动配置好,我们只需要把所有静态资源放到 static 文件夹下

4.4.3、路径构建

@Controller
public class IndexController {

    @GetMapping({"/","/login"})
    public String loginPage(){
        return "login";
    }

    @PostMapping("/login")
    public String main(String name, String password){

        //登录成功,重定向到main页面
        return "redirect:/main.html";
    }

    @GetMapping("/main.html")
    public String mainPage(){
        //去main页面
        return "main";
    }
}

重定向main.html,解决了刷新表单重复提交的问题。

<form class="form-signin" action="index.html" method="post" th:action="@{/login}">

这样写有一个缺陷,只要输入请求main.html就可以访问,我们需要登录之后才可以访问:

@Controller
public class IndexController {

    @GetMapping({"/","/login"})
    public String loginPage(){
        return "login";
    }

    @PostMapping("/login")
    public String main(User user, HttpSession session, Model model){

        if (!StringUtils.isEmpty(user.getUserName()) && !StringUtils.isEmpty(user.getPassword())){
            session.setAttribute("loginUser", user);
            //登录成功,重定向到main页面
            return "redirect:/main.html";
        }else {

            model.addAttribute("msg", "账号密码错误");
            return "login";
        }

    }

    @GetMapping("/main.html")
    public String mainPage(HttpSession session, Model model){

        //是否登录成功     拦截器,过滤器
        Object loginUser = session.getAttribute("loginUser");
        if (loginUser != null){
            return "main";
        }else {
            //回到登录页面
            model.addAttribute("msg", "请重新登录");
            return "login";
        }
    }
}
@Data
public class User {
    private String userName;
    private String password;
}
<input type="text" name="userName" class="form-control" placeholder="用户名" autofocus>
<input type="password" name="password" class="form-control" placeholder="密码">
<a href="#" class="btn btn-default dropdown-toggle" data-toggle="dropdown">
    <img src="images/photos/user-avatar.png" alt="" />
    [[${session.loginUser.userName}]]
    <span class="caret"></span>
</a>

模拟实现了登录检查功能。

4.4.4、模板抽取

把页面中公共的部门抽取出来,放在一个commo.html中。

在其他页面引入即可,例如:

<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org">

<head>
    <meta charset="utf-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0">
    <meta name="description" content="">
    <meta name="author" content="ThemeBucket">
    <link rel="shortcut icon" href="#" type="image/png">

    <title>Basic Table</title>

    <div th:include="common :: commonheader"></div>
</head>
<body class="sticky-header">
    <section>

        <div th:replace="common :: #leftmenu"></div>

        <!-- main content start-->
        <div class="main-content">

            <div th:replace="common :: headermenu"></div>

            <!-- 页面标题开始-->
            ......
            <!-- 页面标题结束-->
            ......
            
            <!--内容区域开始-->
            ......
            <!--内容区域结束-->
                        <!--footer  start-->
            <footer>2014 &copy; AdminEx by ThemeBucket</footer>
            <!--footer  end--> </div>
        <!-- main content end--> </section>

    <!-- Placed js at the end of the document so the pages load faster -->
    <div th:replace="common :: #commonscript"></div>

</body>

</html>

使用th:include或者th:replace

4.4.5、动态表格

@GetMapping("/dynamic_table")
public String dynamic_table(Model model){

    //表格内容的遍历
    List<User> users = Arrays.asList(new User("zhangsan", "123456"),
            new User("lisi", "123444"),
            new User("haha", "aaaaa"),
            new User("hehe", "bbbbb"));
    model.addAttribute("users", users);
    return "table/dynamic_table";
}

dynamic_table.html中动态获取表格内容:

<table  class="display table table-bordered table-striped" id="dynamic-table">
<thead>
<tr>
    <th>#</th>
    <th>用户名</th>
    <th>密码</th>
</tr>
</thead>
<tbody>
    <tr th:each="user,stats:${users}">
        <td th:text="${stats.count}">Trident</td>
        <td th:text="${user.userName}">Internet</td>
        <td th:text="${user.password}">Internet</td>
    </tr>
</tbody>
</table>

4.4.6、拦截器

配置拦截器,具体拦截方式:

@Configuration
public class AdminWebConfig implements WebMvcConfigurer {

    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        registry.addInterceptor(new LoginIntercepter())
                .addPathPatterns("/**") //静态资源也会拦截
                .excludePathPatterns("/","/login","/css/**","/fonts/**","/images/**","/js/**"); //放行的请求
    }
}

4.4.7、单文件与多文件的上传

页面:

<form role="form" th:action="@{/upload}" method="post" enctype="multipart/form-data">
    <div class="form-group">
        <label for="exampleInputEmail1">邮箱</label>
        <input type="email" name="email" class="form-control" id="exampleInputEmail1" placeholder="Enter email">
    </div>
    <div class="form-group">
        <label for="exampleInputPassword1">名字</label>
        <input type="text" name="username" class="form-control" id="exampleInputPassword1" placeholder="Password">
    </div>
    <div class="form-group">
        <label for="exampleInputFile">头像</label>
        <input type="file" name="headerImg" id="exampleInputFile">
        <p class="help-block">Example block-level help text here.</p>
    </div>

    <div class="form-group">
        <label for="exampleInputFile">生活照</label>
        <input type="file" name="photos" multiple>
        <p class="help-block">Example block-level help text here.</p>
    </div>

    <div class="checkbox">
        <label>
            <input type="checkbox"> Check me out
        </label>
    </div>
    <button type="submit" class="btn btn-primary">提交</button>
</form>

设置上传大小限制:

spring.servlet.multipart.max-file-size=100MB
spring.servlet.multipart.max-request-size=100MB

控制器:

/**
 * 文件上传测试
 */
@Slf4j
@Controller
public class FormTestController {

    @GetMapping("/form_layouts")
    public String form_layouts(){
        return "form/form_layouts";
    }

    /**
     * MultipartFile 自动封装上传过来的文件
     * @param email
     * @param username
     * @param headerImg
     * @param photos
     * @return
     */
    @PostMapping("/upload")
    public String upload(@RequestParam("email") String email,
                         @RequestParam("username") String username,
                         @RequestPart("headerImg") MultipartFile headerImg,
                         @RequestPart("photos") MultipartFile[] photos){

        log.info("上传的信息:email={}, username={}, headerImg={}, photos={}",
                email, username, headerImg.getSize(), photos.length);

        return "main";
    }
}
上传的信息:email=1448402493@qq.com, username=67465, headerImg=4286, photos=2

把上传的文件进行保存:

@PostMapping("/upload")
public String upload(@RequestParam("email") String email,
                     @RequestParam("username") String username,
                     @RequestPart("headerImg") MultipartFile headerImg,
                     @RequestPart("photos") MultipartFile[] photos) throws IOException {

    log.info("上传的信息:email={}, username={}, headerImg={}, photos={}",
            email, username, headerImg.getSize(), photos.length);

    if (!headerImg.isEmpty()){
        //保存到文件服务器,oss服务器
        String originalFilename = headerImg.getOriginalFilename();
        headerImg.transferTo(new File("G:\\work\\coding\\java\\SpringBoot2\\documents\\"+originalFilename));
    }

    if (photos.length > 0){
        for (MultipartFile photo : photos) {
            if (!photo.isEmpty()) {
                //保存到文件服务器,oss服务器
                String originalFilename = photo.getOriginalFilename();
                photo.transferTo(new File("G:\\work\\coding\\java\\SpringBoot2\\documents\\"+originalFilename));
            }
        }
    }

    return "main";
}

5、异常处理

5.1、默认规则

  • 默认情况下,Spring Boot 提供 /error 处理所有错误的映射

  • 对于机器客户端,它将生成 JSON 响应,其中包含错误,HTTP 状态和异常消息的详细信息。对于浏览器客户端,响应一个 “ whitelabel” 错误视图,以 HTML 格式呈现相同的数据

    在这里插入图片描述

  • error/下的4xx,5xx页面会被自动解析;


6、Web原生组件注入(Servlet、Filter、Listener)

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值