spring boot 中的web开发

spring boot 对spring mvc 几乎全部都做了自动配置,所以基本不用使用者进行配置,当然也可以自己配置。例如:静态资源、内容协商、试图解析器、网站图标、数据绑定器、欢迎页等等都会自动注册。

一、静态资源和访问

目录

类路径下 :/static (or /public or /resources or /META-INF/resources),只要静态资源放入这些路径下,那么直接在浏览器就可以访问了(根路径)。

静态资源映射的路径是/**。那么当存在动态资源和静态资源请求路径相同的时候,容器会先处理动态资源,如果动态资源不存在,则交给静态资源处理。

静态资源访问前缀(默认无)

spring.mvc.static-path-pattern=/resources/**

写了这个配置,那么就会静态资源加前缀。

修改静态资源的文件夹

spring.web.resources.static-locations

写了这个配置,就可以修改默认的静态资源路径,可以接受数组。

同时支持webjars,也就是把一些css、js打包成一个jar包,然后直接在pom中引入,直接可以使用。访问路径是 /webjars/**

欢迎页和图标

静态页有2中处理方式,第一种是直接在静态文件夹下放入index.html,另一种是增加一个controller处理欢迎页。

index.html 配置欢迎页的时候,不能配置访问前缀,否则会欢迎页会失效。

图标也是直接命名放入静态资源文件夹,同时配置前缀也会导致无法使用网站图标。

在测试的时候,会发现经常出现缓存问题。

二、静态资源访问的底层原理

  • spring boot 启动的时候,会加载相关加载类
  • spring mvc 启动自动配置类大部分都在 WebMvcAutoConfiguration
    @Configuration(proxyBeanMethods = false)
    @ConditionalOnWebApplication(type = Type.SERVLET)
    @ConditionalOnClass({ Servlet.class, DispatcherServlet.class, WebMvcConfigurer.class })
    @ConditionalOnMissingBean(WebMvcConfigurationSupport.class)
    @AutoConfigureOrder(Ordered.HIGHEST_PRECEDENCE + 10)
    @AutoConfigureAfter({ DispatcherServletAutoConfiguration.class, TaskExecutionAutoConfiguration.class,
    		ValidationAutoConfiguration.class })
  • 容器中配置一个webmvc的配置类,其中属性依赖于WebMvcProperties.class,ResourceProperties.class
    	@Configuration(proxyBeanMethods = false)
    	@Import(EnableWebMvcConfiguration.class)
    	@EnableConfigurationProperties({ WebMvcProperties.class, ResourceProperties.class })
    	@Order(0)
    	public static class WebMvcAutoConfigurationAdapter implements WebMvcConfigurer {
         
         
    • WebMvcProperties == spring mvc
    • ResourceProperties == spring resource
  • 配置类只有一个有参构造器,那么所有参数都在容器中
  • 资源处理的默认规则
    @Override
    		public void addResourceHandlers(ResourceHandlerRegistry registry) {
                //判断静态资源是否开启  add-mappings
    			if (!this.resourceProperties.isAddMappings()) {
    				logger.debug("Default resource handling disabled");
    				return;
    			}
                //静态资源,缓存的时间
    			Duration cachePeriod = this.resourceProperties.getCache().getPeriod();
    			CacheControl cacheControl = this.resourceProperties.getCache().getCachecontrol().toHttpCacheControl();
    			if (!registry.hasMappingForPattern("/webjars/**")) {
    			//webjars 路径处理,同时会缓存一段时间	customizeResourceHandlerRegistration(registry.addResourceHandler("/webjars/**")
    						.addResourceLocations("classpath:/META-INF/resources/webjars/")
    						.setCachePeriod(getSeconds(cachePeriod)).setCacheControl(cacheControl));
    			}
                //静态资源访问规则处理
    			String staticPathPattern = this.mvcProperties.getStaticPathPattern();
    			if (!registry.hasMappingForPattern(staticPathPattern)) {
    			//在配置的位置,寻找静态资源	customizeResourceHandlerRegistration(registry.addResourceHandler(staticPathPattern)
    						.addResourceLocations(getResourceLocations(this.resourceProperties.getStaticLocations()))
    						.setCachePeriod(getSeconds(cachePeriod)).setCacheControl(cacheControl));
    			}
    		}
  • 欢迎页源码
    	WelcomePageHandlerMapping(TemplateAvailabilityProviders templateAvailabilityProviders,
    			ApplicationContext applicationContext, Optional<Resource> welcomePage, String staticPathPattern) {
    		if (welcomePage.isPresent() && "/**".equals(staticPathPattern)) {
                //欢迎页存在 && /** 直接进入到欢迎页
    			logger.info("Adding welcome page: " + welcomePage.get());
    			setRootViewName("forward:index.html");
    		}
    		else if (welcomeTemplateExists(templateAvailabilityProviders, applicationContext)) {
                //否则进入一个controller
    			logger.info("Adding welcome page template: index");
    			setRootViewName("index");
    		}
    	}
    	    //HandlerMapping:处理器映射,保存了每个handler能处理那些请求
            //下面就是放入一个欢迎页处理的handlerMapping
            @Bean
    		public WelcomePageHandlerMapping welcomePageHandlerMapping(ApplicationContext applicationContext,
    				FormattingConversionService mvcConversionService, ResourceUrlProvider mvcResourceUrlProvider) {
    			WelcomePageHandlerMapping welcomePageHandlerMapping = new WelcomePageHandlerMapping(
    					new TemplateAvailabilityProviders(applicationContext), applicationContext, getWelcomePage(),
    					this.mvcProperties.getStaticPathPattern());
    			welcomePageHandlerMapping.setInterceptors(getInterceptors(mvcConversionService, mvcResourceUrlProvider));
    			welcomePageHandlerMapping.setCorsConfigurations(getCorsConfigurations());
    			return welcomePageHandlerMapping;
    		}

三、请求处理 

请求映射就不去说了,相抵比较简单。

简单说一下 rest风格的请求,现在开发中期望使用http的方法动词进行操作的区分,同样针对user的数据处理,都是/user,GET-获取用户、DELETE-删除用户、PUT-修改用户、POST-保存用户

核心就是配置HiddenHttpMethodFilter的核心filter,只要带一个“_method”的隐藏域就可以实现rest请求了。需要在配置文件开启  spring.mvc.hiddenmethod.filter = true

表单提交的rest风格原理,因为表单只有post和get,所以需要处理,其他可以提交Rest风格提交的时候,不需要处理。

  • 需要_method参数
  • 被HiddenHttpMethodFilter拦截
  • 必须是post方式,才能使用rest风格的提交
  • 获取到_method的值
  • 根据判断,使用wapper包装模式,重写一下请求
  • 放行的时候,使用wapper进行处理请求
//
// Source code recreated from a .class file by IntelliJ IDEA
// (powered by FernFlower decompiler)
//

package org.springframework.web.filter;

import java.io.IOException;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import java.util.Locale;
import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.ServletRequest;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletRequestWrapper;
import javax.servlet.http.HttpServletResponse;
import org.springframework.http.HttpMethod;
import org.springframework.util.Assert;
import org.springframework.util.StringUtils;

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

    public HiddenHttpMethodFilter() {
    }

    public void setMethodParam(String methodParam) {
        Assert.hasText(methodParam, "'methodParam' must not be empty");
        this.methodParam = methodParam;
    }

    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);
    }

    static {
        ALLOWED_METHODS = Collections.unmodifiableList(Arrays.asList(HttpMethod.PUT.name(), HttpMethod.DELETE.name(), HttpMethod.PATCH.name()));
    }

    private static class HttpMethodRequestWrapper extends HttpServletRequestWrapper {
        private final String method;

        public HttpMethodRequestWrapper(HttpServletRequest request, String method) {
            super(request);
            this.method = method;
        }

        public String getMethod() {
            return this.method;
        }
    }
}

请求映射原理

所有对spring mvc的分析,都是从DispatcherServlet开始的,查看它的继承结构,可以看到本身就是一个 servlet,查找它的doGet\doPost方法,调用顺序是:

doGet\doPost(FrameworkServlet)->processRequest(FrameworkServlet)->doService(DispatcherServlet)->doDispatch(DispatcherServlet) 。

protected void doDispatch(HttpServletRequest request, HttpServletResponse response) throws Exception {
        HttpServletRequest processedRequest = request;
        HandlerExecutionChain mappedHandler = null;
        boolean multipartRequestParsed = false;
        WebAsyncManager asyncManager = WebAsyncUtils.getAsyncManager(request);

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

                try {
                    processedRequest = this.checkMultipart(request);
                    multipartRequestParsed = processedRequest != request;
                    //查看那个handler处理该请求
                    mappedHandler = this.getHandler(processedRequest);
                    if (mappedHandler == null) {
                        this.noHandlerFound(processedRequest, response);
                        return;
                    }

所有的handler 都保存在 handlerMappings中。

其中 RequestMappingHandlerMapping 保存,每一个类处理哪个请求:

 所有的请求映射都保存在handlerMapping中,

  • spring boot 自动配置了欢迎页的 handlerMapping 自动配置进去
  • 请求进来后会尝试所有的HandlerMapping是否处理请求,直到找到能处理请求的
  • spring boot 自动配置的handlerMapping,一共5个
  • 使用者也可以自己配置handlerMapping,例如api/v1 和 api/v2 调用不同的包的业务实现

四、参数处理

spring boot 对于参数的处理沿用了 spring mvc 方式,主要有一下几种:

  • 在路径中使用通配符/car/{id}/owner/{username},那么在参数中可以使用@PathVariable(“参数名”)获取,可以直接用map接受全部参数
  • 获取请求头的方式,使用@RequestHeader,也可以使用map接收全部请求头
  • 最常用的方式就是使用@RequestParam 接收 ?x1=a1&x2=a2_1&x2=a2_2,也可以使用map接收全部参数
  • 使用@CookieValue 注解获取Cookie中的值,也可以使用map接收全部Cookie的值
  • 使用@RequestBody 获取Post请求体的全部内容,一般使用一个bean接收全部值
  • 使用@RequestAttribute 获取请求域中的属性值
  • 使用@MatrixVariable获取矩阵变量,矩阵变量就是请求中使用;进行分割的参数,例如:/car;jsession=abc,可以使它处理cookie被禁用而无法使用session的问题,但是在spring boot中默认禁用矩阵变量的,对于路径的处理都是使用UrlPathHepler,其中有一个属性removeSemicolonContent默认移除;号后的内容,设置为true即可

各种参数解析原理

  • HandlerMapping中找到能够处理请求的Handler(其实就是Controller的某个方法)
  • 为当前的Handler找到一个适配器HandlerAdapter->RequestMappingHandlerAdapter

四种HandlerAdapter:

  • 0-支持RequestMapping 注解的
  • 1-支持函数式编程的 

之后就会执行目标方法:

26个参数解析器,其实也就是接收参数有多少种写法:

 15中返回值处理器,也就是能够写多少种返回值方式:

 真正的执行目标方法:

 确定目标方法每一个参数值:

protected Object[] getMethodArgumentValues(NativeWebRequest request, @Nullable ModelAndViewContainer mavContainer, Object... providedArgs) throws Exception {
        //获取参数详细信息
        MethodParameter[] parameters = this.getMethodParameters();
        if (ObjectUtils.isEmpty(parameters)) {
            return EMPTY_ARGS;
        } else {
            //返回值
            Object[] args = new Object[parameters.length];
            //遍历各个参数
            for(int i = 0; i < parameters.length; ++i) {
                MethodParameter parameter = parameters[i];
                parameter.initParameterNameDiscovery(this.parameterNameDiscoverer);
                args[i] = findProvidedArgument(parameter, providedArgs);
                if (args[i] == null) {
                    //解析器是否支持该参数,遍历26个解析器哪个一个能支持传递过来的参数
                    //就是查看参数上标注的哪个注解,例如标注了@ReqeustParam
                    if (!this.resolvers.supportsParameter(parameter)) {
                        throw new IllegalStateException(formatArgumentError(parameter, "No suitable resolver"));
                    }

                    try {
                        //给参数赋值,拿到当前参数的参数解析器
                        args[i] = this.resolvers.resolveArgument(parameter, mavContainer, request, this.dataBinderFactory);
                    } catch (Exception var10) {
                        if (this.logger.isDebugEnabled()) {
                            String exMsg = var10.getMessage();
                            if (exMsg != null && !exMsg.contains(parameter.getExecutable().toGenericString())) {
                                this.logger.debug(formatArgumentError(parameter, exMsg));
                            }
                        }

                        throw var10;
                    }
                }
            }

            return args;
        }
    }

五、Servlet Api 参数

spring mvc 也是可以传入 servlet api的参数,例如 request、session、header等等。

跟参数解析一样,也是在26个解析中查询到符合哪个解析器,例如request就是由解析器ServletRequestMethodArgumentResolver进行解析。

六、复杂参数

map和model

相当于在request的请求与中放入数据。map、model类型的参数,最终会返 BingingAwareModelMap,是map也会model,继承了linkHashMap。

最终map和model对象都解析成一个对象:

 

当目标方法执行完毕后, 将所有的数据放在ModelAndViewContainer,包含需要去的页面地址和相关的数据

 

model中的所有数据,遍历放入请求域中。

RedirectAttribute

重定向携带的数据

ServletResponse

原生的响应体所带数据

七、自定义对象参数

spring mvc 可以将页面提交的数据,直接封装成对象,下面看看数据绑定的原理。

查找支持的解析器,发现是 ServletModelAttributeMethodProcessor

是否标注注解了或者不是简单类型:

 发现支持之后,就准备进行封装:

  public final Object resolveArgument(MethodParameter parameter, @Nullable ModelAndViewContainer mavContainer, NativeWebRequest webRequest, @Nullable WebDataBinderFactory binderFactory) throws Exception {
        Assert.state(mavContainer != null, "ModelAttributeMethodProcessor requires ModelAndViewContainer");
        Assert.state(binderFactory != null, "ModelAttributeMethodProcessor requires WebDataBinderFactory");
        String name = ModelFactory.getNameForParameter(parameter);
        ModelAttribute ann = (ModelAttribute)parameter.getParameterAnnotation(ModelAttribute.class);
        if (ann != null) {
            mavContainer.setBinding(name, ann.binding());
        }

        Object attribute = null;
        BindingResult bindingResult = null;
        if (mavContainer.containsAttribute(name)) {
            attribute = mavContainer.getModel().get(name);
        } else {
            try {
                //创建一个空的pojo对象
                attribute = this.createAttribute(name, parameter, binderFactory, webRequest);
            } catch (BindException var10) {
                if (this.isBindExceptionRequired(parameter)) {
                    throw var10;
                }

                if (parameter.getParameterType() == Optional.class) {
                    attribute = Optional.empty();
                }

                bindingResult = var10.getBindingResult();
            }
        }

        if (bindingResult == null) {
            //web数据绑定器,将请求参数的值绑定到制定的bean中(attribute)
            //利用类型转换器,把数据转换成java中的类型
            WebDataBinder binder = binderFactory.createBinder(webRequest, attribute, name);
            if (binder.getTarget() != null) {
                if (!mavContainer.isBindingDisabled(name)) {
                    this.bindRequestParameters(binder, webRequest);
                }

                this.validateIfApplicable(binder, parameter);
                if (binder.getBindingResult().hasErrors() && this.isBindExceptionRequired(binder, parameter)) {
                    throw new BindException(binder.getBindingResult());
                }
            }

            if (!parameter.getParameterType().isInstance(attribute)) {
                attribute = binder.convertIfNecessary(binder.getTarget(), parameter.getParameterType(), parameter);
            }

            bindingResult = binder.getBindingResult();
        }

        Map<String, Object> bindingResultModel = bindingResult.getModel();
        mavContainer.removeAttributes(bindingResultModel);
        mavContainer.addAllAttributes(bindingResultModel);
        return attribute;
    }

 绑定器中的转换服务:

利用反射机制,进行数据封装。GenericConversionService(每个属性的值,也是在124个转换器进行找到可以转换的转换器,进行转换)

使用者也可以在webDataBind放入自己的转换器。

猜你喜欢

转载自blog.csdn.net/liming0025/article/details/120604463