SpringBoot 之 Web开发

本文涉及的产品
公共DNS(含HTTPDNS解析),每月1000万次HTTP解析
云解析 DNS,旗舰版 1个月
全局流量管理 GTM,标准版 1个月
简介: SpringBoot 之 Web开发

2、Web开发

+13.png
2.1、SpringMVC自动化配置概述

Spring Boot provides auto-configuration for Spring MVC that works well with most applications.(大多场景我们都无需自定义配置)


The auto-configuration adds the following features on top of Spring’s defaults:


Inclusion of ContentNegotiatingViewResolver and BeanNameViewResolver beans.


内容协商视图解析器和BeanName视图解析器

Support for serving static resources, including support for WebJars (covered later in this document)).


静态资源(包括webjars)

Automatic registration of Converter, GenericConverter, and Formatter beans.


自动注册 Converter,GenericConverter,Formatter

Support for HttpMessageConverters (covered later in this document).


支持 HttpMessageConverters (后来我们配合内容协商理解原理)

Automatic registration of MessageCodesResolver (covered later in this document).


自动注册 MessageCodesResolver (国际化用)

Static index.html support.


静态index.html 页支持

Custom Favicon support (covered later in this document).


自定义 Favicon

Automatic use of a ConfigurableWebBindingInitializer bean (covered later in this document).


自动使用 ConfigurableWebBindingInitializer ,(DataBinder负责将请求数据绑定到JavaBean上)

If you want to keep those Spring Boot MVC customizations and make more MVC customizations (interceptors, formatters, view controllers, and other features), you can add your own @Configuration class of type WebMvcConfigurer but without @EnableWebMvc.


不用@EnableWebMvc注解。使用 @Configuration + WebMvcConfigurer 自定义规则


If you want to provide custom instances of RequestMappingHandlerMapping, RequestMappingHandlerAdapter, or ExceptionHandlerExceptionResolver, and still keep the Spring Boot MVC customizations, you can declare a bean of type WebMvcRegistrations and use it to provide custom instances of those components.


声明 WebMvcRegistrations 改变默认底层组件


If you want to take complete control of Spring MVC, you can add your own @Configuration annotated with @EnableWebMvc, or alternatively add your own @Configuration-annotated DelegatingWebMvcConfiguration as described in the Javadoc of @EnableWebMvc.


使用 @EnableWebMvc + @Configuration + DelegatingWebMvcConfiguration 全面接管SpringMVC


2.2、简单功能分析

2.2.1、静态资源访问

① 静态资源目录

静态资源目录:/static (or /public or /resources or /META-INF/resources)


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


原理:静态映射 /**


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


修改默认的静态资源路径

spring:
  mvc:
    static-path-pattern: /resources/**
  web:
    resources:
      static-locations: [classpath:/haha/]

② 静态资源访问前缀

spring:
  mvc:
    static-path-pattern: "/resources/**"

访问:当前项目 + static-path-pattern + 静态资源名

③ webjar

自动映射 /webjars/**

https://www.webjars.org/

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

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


2.2.2、欢迎页支持

静态资源路径下 index.html


可以配置静态资源路径

但是不能配置静态资源的访问前缀。否则导致 index.html 页面不能被默认访问

spring:
#  mvc:
#    static-path-pattern: /resources/**
  web:
    resources:
      static-locations: [classpath:/haha/]

controller 处理 /index


2.2.3、自定义Favicon

favicon.ico放在静态资源目录下


2.2.4、静态资源配置原理

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


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

@AutoConfiguration(
    after = {DispatcherServletAutoConfiguration.class, TaskExecutionAutoConfiguration.class, ValidationAutoConfiguration.class}
)
@ConditionalOnWebApplication(
    type = Type.SERVLET
)
@ConditionalOnClass({Servlet.class, DispatcherServlet.class, WebMvcConfigurer.class})
@ConditionalOnMissingBean({WebMvcConfigurationSupport.class})
@AutoConfigureOrder(-2147483638)
public class WebMvcAutoConfiguration {
}

给容器中配置了什么

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

配置文件的相关属性和xxx进行绑定

WebMvcProperties==spring.mvc

WebProperties==spring.web

① 资源处理的默认规则

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});
            }
        });
    }
}
spring:
  web:
    resources:
      add-mappings: false # 禁用所有静态资源
@ConfigurationProperties("spring.web")
public class WebProperties {
  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 Chain chain;
        private final Cache cache;
        public Resources() {
            this.staticLocations = CLASSPATH_RESOURCE_LOCATIONS;
            this.addMappings = true;
            this.customized = false;
            this.chain = new Chain();
            this.cache = new Cache();
        }
    }
}

② 欢迎页的处理规则

@Bean
public WelcomePageHandlerMapping welcomePageHandlerMapping(ApplicationContext applicationContext, FormattingConversionService mvcConversionService, ResourceUrlProvider mvcResourceUrlProvider) {
    WelcomePageHandlerMapping welcomePageHandlerMapping = new WelcomePageHandlerMapping(new TemplateAvailabilityProviders(applicationContext), applicationContext, this.getWelcomePage(), this.mvcProperties.getStaticPathPattern());
    welcomePageHandlerMapping.setInterceptors(this.getInterceptors(mvcConversionService, mvcResourceUrlProvider));
    welcomePageHandlerMapping.setCorsConfigurations(this.getCorsConfigurations());
    return welcomePageHandlerMapping;
}
WelcomePageHandlerMapping(TemplateAvailabilityProviders templateAvailabilityProviders, ApplicationContext applicationContext, Resource welcomePage, String staticPathPattern) {
    if (welcomePage != null && "/**".equals(staticPathPattern)) {
        logger.info("Adding welcome page: " + welcomePage);
        this.setRootViewName("forward:index.html");
    } else if (this.welcomeTemplateExists(templateAvailabilityProviders, applicationContext)) {
        logger.info("Adding welcome page template: index");
        this.setRootViewName("index");
    }
}

③ favicon

2.3、请求参数处理

2.3.1、请求映射

① rest使用与原理

@xxxMapping;


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


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

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

核心Filter;HiddenHttpMethodFilter

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

SpringBoot中手动开启

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

@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-张三";
}
@Bean
@ConditionalOnMissingBean({HiddenHttpMethodFilter.class})
@ConditionalOnProperty(
    prefix = "spring.mvc.hiddenmethod.filter",
    name = {"enabled"}
)
public OrderedHiddenHttpMethodFilter hiddenHttpMethodFilter() {
    return new OrderedHiddenHttpMethodFilter();
}

自定义filter

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

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


表单提交会带上**_method=PUT**


请求过来被HiddenHttpMethodFilter拦截


请求是否正常,并且是POST


获取到**_method**的值。


兼容以下请求;PUT、DELETE、PATCH


原生request(post),包装模式requesWrapper重写了getMethod方法,返回的是传入的值。


过滤器链放行的时候用wrapper。以后的方法调用getMethod是调用requesWrapper的。


Rest使用客户端工具


例如 Postman 直接发送 put、delete请求方式时,无需Filter

spring:

 mvc:

   hiddenmethod:

     filter:

       enabled: true   #开启页面表单的Rest功能

spring:
  mvc:
    hiddenmethod:
      filter:
        enabled: true   #开启页面表单的Rest功能


② 请求映射原理

+14.png
SpringMVC功能分析都从 org.springframework.web.servlet.DispatcherServlet —> doDispatch()

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;
            Exception dispatchException = null;
            try {
                processedRequest = this.checkMultipart(request);
                multipartRequestParsed = processedRequest != request;
                mappedHandler = this.getHandler(processedRequest);
                if (mappedHandler == null) {
                    this.noHandlerFound(processedRequest, response);
                    return;
                }
                HandlerAdapter ha = this.getHandlerAdapter(mappedHandler.getHandler());
                String method = request.getMethod();
                boolean isGet = HttpMethod.GET.matches(method);
                if (isGet || HttpMethod.HEAD.matches(method)) {
                    long lastModified = ha.getLastModified(request, mappedHandler.getHandler());
                    if ((new ServletWebRequest(request, response)).checkNotModified(lastModified) && isGet) {
                        return;
                    }
                }
                if (!mappedHandler.applyPreHandle(processedRequest, response)) {
                    return;
                }
                mv = ha.handle(processedRequest, response, mappedHandler.getHandler());
                if (asyncManager.isConcurrentHandlingStarted()) {
                    return;
                }
                this.applyDefaultViewName(processedRequest, mv);
                mappedHandler.applyPostHandle(processedRequest, response, mv);
            } catch (Exception var20) {
                dispatchException = var20;
            } catch (Throwable var21) {
                dispatchException = new NestedServletException("Handler dispatch failed", var21);
            }
            this.processDispatchResult(processedRequest, response, mappedHandler, mv, (Exception)dispatchException);
        } catch (Exception var22) {
            this.triggerAfterCompletion(processedRequest, response, mappedHandler, var22);
        } catch (Throwable var23) {
            this.triggerAfterCompletion(processedRequest, response, mappedHandler, new NestedServletException("Handler processing failed", var23));
        }
    } finally {
        if (asyncManager.isConcurrentHandlingStarted()) {
            if (mappedHandler != null) {
                mappedHandler.applyAfterConcurrentHandlingStarted(processedRequest, response);
            }
        } else if (multipartRequestParsed) {
            this.cleanupMultipart(processedRequest);
        }
    }
}

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


所有的请求映射都在HandlerMapping中


SpringBoot自动装配欢迎页的 WelcomePageHandlerMapping 。访问 / 能访问到index.html


SpringBoot自动配置了默认 的 RequestMappingHandlerMapping


pringBoot自动配置了默认 的 RequestMappingHandlerMapping


如果有就找到这个请求对应的handler

如果没有就是下一个 HandlerMapping

我们需要一些自定义的映射处理,我们也可以自己给容器中放 HandlerMapping



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

2.3.2、普通参数与基本注解

① 注解

@PathVariable、@RequestHeader、@ModelAttribute、@RequestParam、@MatrixVariable、@CookieValue、@RequestBody

@RestController
public class ParameterTestController {
    //  car/2/owner/zhangsan
    @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("_ga") String _ga,
                                     @CookieValue("_ga") Cookie cookie){
        Map<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);
        map.put("age",age);
        map.put("inters",inters);
        map.put("params",params);
        map.put("_ga",_ga);
        System.out.println(cookie.getName()+"===>"+cookie.getValue());
        return map;
    }
    @PostMapping("/save")
    public Map postMethod(@RequestBody String content){
        Map<String,Object> map = new HashMap<>();
        map.put("content",content);
        return map;
    }
    //1、语法: 请求路径:/cars/sell;low=34;brand=byd,audi,yd
    //2、SpringBoot默认是禁用了矩阵变量的功能
    //      手动开启:原理。对于路径的处理。UrlPathHelper进行解析。
    //              removeSemicolonContent(移除分号内容)支持矩阵变量的
    //3、矩阵变量必须有url路径变量才能被解析
    @GetMapping("/cars/{path}")
    public Map carsSell(@MatrixVariable("low") Integer low,
                        @MatrixVariable("brand") List<String> brand,
                        @PathVariable("path") String path){
        Map<String,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){
        Map<String,Object> map = new HashMap<>();
        map.put("bossAge",bossAge);
        map.put("empAge",empAge);
        return map;
    }
}

② Servlet API

WebRequest、ServletRequest、MultipartRequest、 HttpSession、javax.servlet.http.PushBuilder、Principal、InputStream、Reader、HttpMethod、Locale、TimeZone、ZoneId


ServletRequestMethodArgumentResolver 以上的部分参数

@Override
public boolean supportsParameter(MethodParameter parameter) {
    Class<?> paramType = parameter.getParameterType();
    return (WebRequest.class.isAssignableFrom(paramType) ||
            ServletRequest.class.isAssignableFrom(paramType) ||
            MultipartRequest.class.isAssignableFrom(paramType) ||
            HttpSession.class.isAssignableFrom(paramType) ||
            (pushBuilder != null && pushBuilder.isAssignableFrom(paramType)) ||
            Principal.class.isAssignableFrom(paramType) ||
            InputStream.class.isAssignableFrom(paramType) ||
            Reader.class.isAssignableFrom(paramType) ||
            HttpMethod.class == paramType ||
            Locale.class == paramType ||
            TimeZone.class == paramType ||
            ZoneId.class == paramType);
}

③ 复杂参数

Map、**Model(map、model里面的数据会被放在request的请求域 request.setAttribute)、**Errors/BindingResult、RedirectAttributes( 重定向携带数据)、ServletResponse(response)、SessionStatus、UriComponentsBuilder、ServletUriComponentsBuilder


Map<String,Object> map, Model model, HttpServletRequest request 都是可以给request域中放数据

request.getAttribute() 进行获取


Map、Model类型的参数,会返回 mavContainer.getModel()—> BindingAwareModelMap 是Model 也是Map


mavContainer.getModel(); 获取到值的


④ 自定义对象参数

@Data
public class Person {
    private String  userName;
    private Integer age;
    private Date birth;
    private Pet pet;
}
@Data
public class Pet {
    private String name;
    private Integer age;
}

2.3.3、POJO封装过程

ServletModelAttributeMethodProcessor

2.3.4、参数处理原理

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

为当前的 Handler 找到一个适配器 HandlerAdapter; RequestMappingHandlerAdapter

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

① HandlerAdapter

0 — 支持方法上面标注 @RequestMapping

1 — 支持函数式编程

……

② 执行目标方法

//DispatcherServlet -- doDispatch
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目标方法能写多少种参数类型。取决于参数解析器


当前解析器是否支持解析这种参数

支持就才调用 resolveArgument

④ 返回值处理器

⑤ 如何确定目标方法每一个参数的值

// InvocableHandlerMethod
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) {
                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 (logger.isDebugEnabled()) {
                        String exMsg = var10.getMessage();
                        if (exMsg != null && !exMsg.contains(parameter.getExecutable().toGenericString())) {
                            logger.debug(formatArgumentError(parameter, exMsg));
                        }
                    }
                    throw var10;
                }
            }
        }
        return args;
    }
}

挨个判断所有参数解析器那个支持解析这个参数

@Nullable
private HandlerMethodArgumentResolver getArgumentResolver(MethodParameter parameter) {
    HandlerMethodArgumentResolver result = (HandlerMethodArgumentResolver)this.argumentResolverCache.get(parameter);
    if (result == null) {
        Iterator var3 = this.argumentResolvers.iterator();
        while(var3.hasNext()) {
            HandlerMethodArgumentResolver resolver = (HandlerMethodArgumentResolver)var3.next();
            if (resolver.supportsParameter(parameter)) {
                result = resolver;
                this.argumentResolverCache.put(parameter, resolver);
                break;
            }
        }
    }
    return result;
}

解析这个参数的值


调用各自 HandlerMethodArgumentResolver 的 resolveArgument 方法即可


自定义类型参数 封装POJO


ServletModelAttributeMethodProcessor 这个参数处理器支持


是否为简单类型

// ModelAttributeMethodProcessor
public boolean supportsParameter(MethodParameter parameter) {
    return parameter.hasParameterAnnotation(ModelAttribute.class) || this.annotationNotRequired && !BeanUtils.isSimpleProperty(parameter.getParameterType());
}
// BeanUtils
public static boolean isSimpleProperty(Class<?> type) {
    Assert.notNull(type, "'type' must not be null");
    return isSimpleValueType(type) || type.isArray() && isSimpleValueType(type.getComponentType());
}
public static boolean isSimpleValueType(Class<?> type) {
    return Void.class != type && Void.TYPE != type && (ClassUtils.isPrimitiveOrWrapper(type) || Enum.class.isAssignableFrom(type) || CharSequence.class.isAssignableFrom(type) || Number.class.isAssignableFrom(type) || Date.class.isAssignableFrom(type) || Temporal.class.isAssignableFrom(type) || URI.class == type || URL.class == type || Locale.class == type || Class.class == type);
}
@Nullable
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 {
            attribute = this.createAttribute(name, parameter, binderFactory, webRequest);
        } catch (BindException var10) {
            if (this.isBindExceptionRequired(parameter)) {
                throw var10;
            }
            if (parameter.getParameterType() == Optional.class) {
                attribute = Optional.empty();
            } else {
                attribute = var10.getTarget();
            }
            bindingResult = var10.getBindingResult();
        }
    }
    if (bindingResult == null) {
        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;
}

WebDataBinder binder = binderFactory.createBinder(webRequest, attribute, name);


WebDataBinder :web数据绑定器,将请求参数的值绑定到指定的JavaBean里面


WebDataBinder 利用它里面的 Converters 将请求数据转成指定的数据类型。再次封装到JavaBean中


GenericConversionService:在设置每一个值的时候,找它里面的所有converter那个可以将这个数据类型(request带来参数的字符串)转换到指定的类型(JavaBean – Integer)


byte – > file


未来我们可以给WebDataBinder里面放自己的Converter;


private static final class StringToNumber<T extends Number> implements Converter<String, T>


自定义Converter

@Bean
public WebMvcConfigurer webMvcConfigurer(){
    return new WebMvcConfigurer() {
        @Override
        public void configurePathMatch(PathMatchConfigurer configurer) {
            UrlPathHelper urlPathHelper = new UrlPathHelper();
            // 不移除分号后面的内容,矩阵变量才能生效
            urlPathHelper.setRemoveSemicolonContent(false);
            configurer.setUrlPathHelper(urlPathHelper);
        }
        @Override
        public void addFormatters(FormatterRegistry registry) {
            registry.addConverter(new Converter<String, Pet>() {
                @Override
                public Pet convert(String source) {
                    if (!StringUtils.isEmpty(source)){
                        String[] split = source.split(",");
                        Pet pet = new Pet();
                        pet.setName(split[0]);
                        pet.setAge(Integer.parseInt(split[1]));
                        return pet;
                    }
                    return null;
                }
            });
        }
    };
}

⑥ 目标方法执行完成

将所有的数据都放在 ModelAndViewContainer;包含要去的页面地址View。还包含Model数据


⑦ 处理派发结果

processDispatchResult(processedRequest, response, mappedHandler, mv, dispatchException);


renderMergedOutputModel(mergedModel, getRequestToExpose(request), response);

// InternalResourceView
protected void renderMergedOutputModel(Map<String, Object> model, HttpServletRequest request, HttpServletResponse response) throws Exception {
    this.exposeModelAsRequestAttributes(model, request);
    this.exposeHelpers(request);
    String dispatcherPath = this.prepareForRendering(request, response);
    RequestDispatcher rd = this.getRequestDispatcher(request, dispatcherPath);
    if (rd == null) {
        throw new ServletException("Could not get RequestDispatcher for [" + this.getUrl() + "]: Check that the corresponding file exists within your web application archive!");
    } else {
        if (this.useInclude(request, response)) {
            response.setContentType(this.getContentType());
            if (this.logger.isDebugEnabled()) {
                this.logger.debug("Including [" + this.getUrl() + "]");
            }
            rd.include(request, response);
        } else {
            if (this.logger.isDebugEnabled()) {
                this.logger.debug("Forwarding to [" + this.getUrl() + "]");
            }
            rd.forward(request, response);
        }
    }
}

暴露模型作为请求域属性

exposeModelAsRequestAttributes(model, request);

// AbstractView
protected void exposeModelAsRequestAttributes(Map<String, Object> model, HttpServletRequest request) throws Exception {
    model.forEach((name, value) -> {
        if (value != null) {
            request.setAttribute(name, value);
        } else {
            request.removeAttribute(name);
        }
    });
}

2.4、数据响应与内容协商


+15.png

2.4.1、响应JSON

① jackson.jar + @ResponseBody

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!-- web场景自动引入了json场景 -->

给前端自动返回 json


a、返回值解析器


源码太难了……


b、返回值解析器原理


返回值处理器是否支持这种类型返回值 supportReturnType

返回值处理器调用 handleReturnValue 进行处理

RequestResponseBodyMethodProcessor 可以处理返回值标了 @Response 注解的

利用 MessageConverters 进行处理将数据写为json

服务器最终根据自己自身的能力,决定服务器能生产出什么样的内容类型的数据

SpringMVC 会挨个遍历所有容器底层的 HttpMessageConverter ,看谁能处理?

得到 MappingJackson2HttpMessageConverter 可以将对象写为 json

利用 MappingJackson2HttpMessageConverter 将对象转为 json 再写出去

② SpringMVC到底支持那些返回值

ModelAndView

Model

View

ResponseEntity

ResponseBodyEmitter

StreamingResponseBody

HttpEntity

HttpHeaders

Callable

DeferredResult

ListenableFuture

CompletionStage

WebAsyncTask

有 @ModelAttribute 且为对象类型的

@ResponseBody 注解 —> RequestResponseBodyMethodProcessor;


③ HttpMessageConverter原理

a、MessageConverter规范


HttpMessageConverter: 看是否支持将 此 Class类型的对象,转为MediaType类型的数据。


例子:Person对象转为JSON。或者 JSON转为Person


b、默认的MessageConvverter


……


2.4.2、内容协商

根据客户端接收能力不同,返回不同媒体类型的数据


① 引入xml依赖

<dependency>
  <groupId>com.fasterxml.jackson.dataformat</groupId>
  <artifactId>jackson-dataformat-xml</artifactId>
</dependency>

② postman分别测试返回json和xml

只需要改变请求头中方中的 Accept 字段,Http协议中规定的,告诉服务器本客户端可以接收的数据类型

16.png
③ 开启浏览器参数方式内容协商功能

为了方便内容协商,开启基于请求参数的内容协商协议

spring:
    contentnegotiation:
      favor-parameter: true  #开启请求参数内容协商模式

发请求:


http://localhost:8080/test/person?format=json


http://localhost:8080/test/person?format=xml


确定客户端接收什么类型的内容类型


Parameter 策略优先确定是要返回 json 数据(获取请求头中的 format 的值)

最终进行内容协商返回给客户端 json 即可

④ 内容协商原理

判断当前响应头中是否有确定的媒体类型 MediaType

获取客户端(Postman、浏览器)支持接收的内容类型。(获取客户端Accept请求头字段)【application/xml】

contentNegotationMannager 内容协商管理器 默认使用基于请求头的策略

HeaderConventNegotationStrategy 确定客户端可以接受的内容类型

遍历循环所有当前系统的 MessageConverter 看谁支持操作这个对象(Person)

找到支持操作 Person 的converter 把 converter 支持的媒体类型统计出来

客户端需要【application/xml】服务器能力【10种,json、xml】

进行内容协商的最佳匹配媒体类型

用支持将对象转换为最佳媒体类型的 converter,调用它进行转换

⑤ 自定义MessageConverter

实现多协议数据兼容 json、xml、x-jingchao


@ResponseBody 响应数据出去 调用 RequestResponseBodyMethodProcessor

Processor 处理方法返回值 通过 MessageConverter 处理

所有 MessageConverter 合起来可以支持各种媒体类型数据的操作(读、写)

内容协商找到最终的 messageConverter

SpringMVC 的什么功能 一个入口给容器中添加一个 WebMvcConfigurer

@Bean
public WebMvcConfigurer webMvcConfigurer(){
    return new WebMvcConfigurer() {
        @Override
        public void extendMessageConverters(List<HttpMessageConverter<?>> converters) {
      converters.add(new JingChaoMessageConverter());
        }
    }
}

有可能我们添加的自定义的功能会覆盖默认很多功能,导致一些默认的功能失效。


大家考虑,上述功能除了我们完全自定义外?SpringBoot有没有为我们提供基于配置文件的快速修改媒体类型功能?怎么配置呢?【提示:参照SpringBoot官方文档web开发内容协商章节】


2.5、视图解析与模板引擎

视图解析:SpringBoot默认不支持 JSP,需要引入第三方模板引擎技术实现页面渲染


2.5.1、视图解析

+17.png
① 视图解析原理流程

目标方法处理的过程中,所有的数据都会被放在 ModelAndViewContainer 里面,包括数据和视图地址

方法的参数是一个自定义类型对象(从请求参数中确定的)。把他重新放在 ModelAndViewContainer

任何目标方法执行完成以后都会返回 ModelAndVIew(数据和视图地址)

processDispatchResult 处理派发结果(页面该如何响应)

1.

2.5.2、模板引擎-Thymeleaf

① thymeleaf简介

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


② 基本语法

表达式

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

字面量


文本值:‘one text’,‘Another one!’,……


数字:0,44,3.14,……


布尔值:false、true


空值: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)

特殊运算


无操作:_

③ 设置属性值

设置单个值

<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}" />

④ 迭代

<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>

⑥ 属性优先级

2.5.3、thymeleaf 使用

① 引入 starter

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

② 自动配置好了 thymeleaf

@Configuration(proxyBeanMethods = false)
@EnableConfigurationProperties(ThymeleafProperties.class)
@ConditionalOnClass({ TemplateMode.class, SpringTemplateEngine.class })
@AutoConfigureAfter({ WebMvcAutoConfiguration.class, WebFluxAutoConfiguration.class })
public class ThymeleafAutoConfiguration { }

自动配置的策略

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

2.配置好了 SpringTemplateEngine

3.配好了 ThymeleafViewResolver

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

public static final String DEFAULT_PREFIX = "classpath:/templates/";
public static final String DEFAULT_SUFFIX = ".html";  //xxx.html

③ 页面开发

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

2.5.4、构建后台管理系统

① 创建项目

thymeleaf、web-starter、devtools、lombok


② 静态资源管理

自动配置好,将静态资源放到 static 文件夹xia


③ 路径构建

th:action=“@{/login}”


④ 模板抽取

th:insert/replace/include


⑤ 页面跳转

@PostMapping("/login")
public String main(User user, HttpSession session, Model model){
    if(StringUtils.hasLength(user.getUserName()) && "123456".equals(user.getPassword())){
        //把登陆成功的用户保存起来
        session.setAttribute("loginUser",user);
        //登录成功重定向到main.html;  重定向防止表单重复提交
        return "redirect:/main.html";
    }else {
        model.addAttribute("msg","账号密码错误");
        //回到登录页面
        return "login";
    }
}

⑥ 数据渲染

@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 ", "aaddd"));
    model.addAttribute("users",users);
    return "table/dynamic_table";
}
<table class="display table table-bordered" id="hidden-table-info">
    <thead>
        <tr>
            <th>#</th>
            <th>用户名</th>
            <th>密码</th>
        </tr>
    </thead>
    <tbody>
        <tr class="gradeX" th:each="user,stats:${users}">
            <td th:text="${stats.count}">Trident</td>
            <td th:text="${user.userName}">Internet</td>
            <td >[[${user.password}]]</td>
        </tr>
    </tbody>
</table>

2.6、拦截器

2.6.1、HandlerInterceptor 接口

/**
 * 登录检查
 * 1、配置好拦截器要拦截那些请求
 * 2、把这些配置放在容器中
 */
@Slf4j
public class LoginInterceptor implements HandlerInterceptor {
    /**
     * 目标方法执行之前
     * @param request
     * @param response
     * @param handler
     * @return
     * @throws Exception
     */
    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
        String requestURI = request.getRequestURI();
        log.info("preHandle拦截的路径为:"+ requestURI);
        // 登录检查逻辑
        HttpSession session = request.getSession();
        Object loginUser = session.getAttribute("loginUser");
        if (loginUser != null){
            return true;
        }
        /* session.setAttribute("msg", "请先登录!");
        response.sendRedirect("/"); */
        request.setAttribute("msg","请先登录!");
        request.getRequestDispatcher("/").forward(request, response);
        return false;
    }
    /**
     * 目标方法执行完成以后
     * @param request
     * @param response
     * @param handler
     * @param modelAndView
     * @throws Exception
     */
    @Override
    public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception {
        log.info("postHandle执行"+ modelAndView);
    }
    /**
     * 页面渲染以后
     * @param request
     * @param response
     * @param handler
     * @param ex
     * @throws Exception
     */
    @Override
    public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
        log.info("afterCompletion执行异常"+ ex);
    }
}

2.6.2、配置拦截器

@Configuration
public class AdminWebConfig implements WebMvcConfigurer {
    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        registry.addInterceptor(new LoginInterceptor())
                //所有资源都会被拦截,包括静态资源
                .addPathPatterns("/**")
                // 放行资源
                .excludePathPatterns("/","/login","/css/**","/fonts/**","/images/**","/js/**");
    }
}

2.6.3、拦截器原理

根据当前请求,找到 HandlerExecutionChain 【可以处理请求的 handler 以及 handler 的所有拦截器】

先来顺序执行 所有拦截器的 preHandle 方法

如果当前拦截器 prehandler返回为 ture,则执行下一个拦截器的 preHandle

如果当前拦截器返回为 false,直接倒序执行所有以及执行了的拦截器的 afterCompletion

如果任何一个拦截器返回 false。直接跳出不执行目标方法

所有拦截器都返回 ture,执行目标方法

倒序执行所有拦截器的 postHandle 方法

前面的步骤有任何异常都会直接倒序触发 afterCompletion

页面成功渲染完成以后,也会倒序触发 afterCompletion


2.7、文件上传

2.7.1、页面表单

<form method="post" action="/upload" enctype="multipart/form-data">
    <input type="file" name="file"><br>
    <input type="submit" value="提交">
</form>

2.7.2、文件上传代码

/**
 * 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) throws IOException {
    log.info("上传消息: email={}, username={}, headerImg={}, photos={}",
             email, username, headerImg.getSize(),photos.length);
    if(!headerImg.isEmpty()){
        // 保存到文件服务器
        String filename = headerImg.getOriginalFilename();
        headerImg.transferTo(new File("E:\\jingchao\\"+filename));
    }
    if (photos.length > 0){
        for (MultipartFile photo : photos) {
            if (!photo.isEmpty()){
                String photoOriginalFilename = photo.getOriginalFilename();
                photo.transferTo(new File("E:\\jingchao\\"+photoOriginalFilename));
            }
        }
    }
    return "index";
}

2.7.3、自动配置原理

文件上传自动配置类 MultipartAutoConfiguration MultipartProperties


自动配置好了 StandardServletMultipartResolver 【文件上传解析器】

原理步骤

请求进来使用文件上传解析器判断(isMultipart)并封装(resolveMultipart,返回MultipartHttpServletRequest)文件上传请求

参数解析器来解析请求中的文件内容封装成MultipartFile

将request中文件信息封装为一个 Map MultiValueMap<String, MultipartFile>

FileCopyUtils 实现文件流的拷贝

2.8、异常处理

2.8.1、错误处理

① 默认规则

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


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

+18.png

+19.png
要对其进行自定义,添加 View 解析为 error


要完全替换默认行为,可以实现 ErrorController 并注册该类型的 Bean 定义,或添加 ErrorAttributes 类型的组件 以实现现有机制但替换其内容


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

② 定制错误处理逻辑

自定义错误页

error/404.html error/5xx.html 有精确的错误状态码页面就匹配精确,没有就找4xx.html,如果都没有就触发白页

@ControllerAdvice + @ExceptionHandler 处理全局异常,底层是 ExceptionHandlerExeptionResolver 支持的

@ResponseStatus + 自定义异常,底层是 ResponseStatusExceptionResolver,把responsestatus注解的信息底层调用 response.sendError(statusCode, resolverReason), tomcat发送的 /error

Spring 底层的异常,如参数类型转换异常,DefaultHandlerExceptionResolver 处理框架底层的异常

response.sendError(HttpServletResponse.SC_BAD_REQUEST, ex.getMessage());

自定义实现 HandlerExceptionResolver 处理异常,可以作为默认的全局异常处理规则

ErrorViewResolver 实现自定义处理异常

response.sendError error 请求就会转给 controller

你的异常没有任何人能处理,tomcat底层 response.sendError error请求就会转给controller

basicErrorController 要去的页面地址是 ErrorViewController

③ 异常处理自动配置原理

ErrorMvcAutoConfiguration 自动配置异常处理规则

容器中的组件:类型:DeafultErrorAttributes —> id:errorAttributes

public class DefaultErrorAttributes implements ErrorAttribubtes, HandlerExceptionResolver

DefaultErrorAttributes: 定义错误页面中包含那些数据

容器中的组件:类型:BasicErrorController —> id: basicErrorController(json + 白页 适配响应)

容器中有组件 View —> id 是 error(响应默认错误页)

容器中放组件 BeanNameViewResolver (视图解析器);按照返回的视图名作为组件的 id 去容器中找到 View 对象

容器中的组件:类型:DefaultErrorViewResolver —> id: conventionErrorViewResolver

如果发生错误,会以 HTTP 的状态码 作为视图页地址(viewName)找到真正的页面

error/404、5xx.html

如果想要返回页面,就会找 error 视图【StaticView】,默认是一个白页


④ 异常处理步骤流程

执行目标方法,目标方法运行期间有任何异常都会被 catch、而且标志当前请求结束;并且用 dispatchException

进入视图解析流程 processDispatchResult(processedRequest, response, mappedHandler, mv, dispatchException);

mv = processHandlerException;处理 handler 发生的异常, 处理完成后返回 ModelAndVIew

遍历所有的 handlerExceptionResolvers,看谁能处理当前异常【HandlerExceptionResolver处理器异常解析器】

系统默认的 异常解析器

DefaultErrorAttributes 先来处理异常,把异常信息保存到 request 域,并返回 null

默认没有任何人能处理异常,所以异常将会被抛出

如果没有任何人能处理最终底层就会 发送 /error 请求。会被底层的 BasicErrorController 处理

解析错误视图;遍历所有的 ErrorViewResolver 看谁能解析

默认的 DefaultErrorViewResolver,作用是把响应状态码作为错误页的地址,error/500.html

模板引擎最终响应这个页面 error/500.html


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

2.9.1、使用Servlet API

指定原生 Servlet 组件放置的位置

@ServletComponentScan(basePackages = "com.jingchao.admin")

使用

@WebServlet(urlPatterns = "/my")
public class MyServlet extends HttpServlet {
    @Override
    protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
        resp.getWriter().write("666666666666");
    }
}

直接响应,没有经过 Spring 的拦截器

解决办法

@WebFilter(urlPatterns = {"/css/*","/images/*"})
public class MyFilter implements Filter {}
@WebListener
public class MyServletContextListener implements ServletContextListener {}

2.9.2、使用RegistrationBean

ServletRegistrationBean, FilterRegistrationBean, ServletListenerRegistrationBean

@Configuration
public class MyRegistryConfig {
    @Bean
    public ServletRegistrationBean myServlet(){
        MyServlet myServlet = new MyServlet();
        return new ServletRegistrationBean(myServlet, "/my","/my1");
    }
    @Bean
    public FilterRegistrationBean myFilter(){
        MyFilter myFilter = new MyFilter();
        // return new FilterRegistrationBean(myFilter, myServlet());
        FilterRegistrationBean filterRegistrationBean = new FilterRegistrationBean(myFilter);
        filterRegistrationBean.setUrlPatterns(Arrays.asList("/my","/images/*"));
        return filterRegistrationBean;
    }
    @Bean
    public ServletListenerRegistrationBean myListener(){
        MyServletContextListener myServletContextListener = new MyServletContextListener();
        return new ServletListenerRegistrationBean(myServletContextListener);
    }
}

2.10、嵌入式Servlet容器

2.10.1、切换嵌入式Servlet容器

默认支持的 webServer


Tomcat、Jetty、Undertow

ServletWebServerApplicationContext 容器自动寻找 ServletWebServerFactory 并引导创建服务器

切换服务器

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-web</artifactId>
    <exclusions>
        <exclusion>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-tomcat</artifactId>
        </exclusion>
    </exclusions>
</dependency>

原理


SpringBoot应用启动发现当前是Web应用。web场景包-导入tomcat

web应用会创建一个web版的ioc容器 ServletWebServerApplicationContext

ServletWebServerApplicationContext 启动的时候寻找 ServletWebServerFactory(Servlet 的web服务器工厂—> Servlet 的web服务器)

SpringBoot底层默认有很多的WebServer工厂;TomcatServletWebServerFactory, JettyServletWebServerFactory, or UndertowServletWebServerFactory

底层直接会有一个自动配置类。ServletWebServerFactoryAutoConfiguration

ServletWebServerFactoryAutoConfiguration导入了ServletWebServerFactoryConfiguration(配置类)

ServletWebServerFactoryConfiguration 配置类 根据动态判断系统中到底导入了那个Web服务器的包。(默认是web-starter导入tomcat包),容器中就有 TomcatServletWebServerFactory

TomcatServletWebServerFactory 创建出Tomcat服务器并启动;TomcatWebServer 的构造器拥有初始化方法initialize—this.tomcat.start()

内嵌服务器,就是手动把启动服务器的代码调用(tomcat核心jar包存在)

2.10.2、定制Servlet容器

实现


WebServerFactoryCustomizer


把配置文件的值和 ServletWebServerFactory 进行绑定

修改配置文件 server.xxx


直接自定义 ConfigurableServletWebServerFactory


xxxCustomizer:定制化器,可以改变 xxx 的默认规则

import org.springframework.boot.web.server.WebServerFactoryCustomizer;
import org.springframework.boot.web.servlet.server.ConfigurableServletWebServerFactory;
import org.springframework.stereotype.Component;
@Component
public class CustomizationBean implements WebServerFactoryCustomizer<ConfigurableServletWebServerFactory> {
    @Override
    public void customize(ConfigurableServletWebServerFactory server) {
        server.setPort(9000);
    }
}

2.11、定制化原理

2.11.1、定制化的常见方式

修改配置文件


xxxxCustomizer


编写自定义组件的配置类 xxxConfiguration;+ @Bean替换、增加容器中默认组件;视图解析器


Web应用 编写一个配置类实现 WebMvcConfigurer 即可定制化web功能 + @Bean给容器中再扩展一些组件

@Configuration
public class AdminWebConfig implements WebMvcConfigurer

@EnableWebMvc + WebMvcConfigurer —— @Bean 可以全面接管SpringMVC,所有规则全部自己重新配置; 实现定制和扩展功能


原理

WebMvcAutoConfiguration 默认的SpringMVC的自动配置功能类。静态资源、欢迎页…

一旦使用 @EnableWebMvc 会 @Import(DelegatingWebMvcConfiguration.class)

DelegatingWebMvcConfiguration 的 作用,只保证SpringMVC最基本的使用

把所有系统中的 WebMvcConfigurer 拿过来。所有功能的定制都是这些 WebMvcConfigurer 合起来一起生效

自动配置了一些非常底层的组件。RequestMappingHandlerMapping、这些组件依赖的组件都是从容器中获取

public class DelegatingWebMvcConfiguration extends WebMvcConfigurationSupport

WebMvcAutoConfiguration 里面的配置要能生效 必须 @ConditionalOnMissingBean(WebMvcConfigurationSupport.class)

@EnableWebMvc 导致了 WebMvcAutoConfiguration 没有生效。

2.11.2、原理分析的套路

场景starter - xxxxAutoConfiguration - 导入xxx组件 - 绑定xxxProperties – 绑定配置文件项

相关文章
|
2月前
|
Java API 数据库
构建RESTful API已经成为现代Web开发的标准做法之一。Spring Boot框架因其简洁的配置、快速的启动特性及丰富的功能集而备受开发者青睐。
【10月更文挑战第11天】本文介绍如何使用Spring Boot构建在线图书管理系统的RESTful API。通过创建Spring Boot项目,定义`Book`实体类、`BookRepository`接口和`BookService`服务类,最后实现`BookController`控制器来处理HTTP请求,展示了从基础环境搭建到API测试的完整过程。
52 4
|
2月前
|
XML JSON API
ServiceStack:不仅仅是一个高性能Web API和微服务框架,更是一站式解决方案——深入解析其多协议支持及简便开发流程,带您体验前所未有的.NET开发效率革命
【10月更文挑战第9天】ServiceStack 是一个高性能的 Web API 和微服务框架,支持 JSON、XML、CSV 等多种数据格式。它简化了 .NET 应用的开发流程,提供了直观的 RESTful 服务构建方式。ServiceStack 支持高并发请求和复杂业务逻辑,安装简单,通过 NuGet 包管理器即可快速集成。示例代码展示了如何创建一个返回当前日期的简单服务,包括定义请求和响应 DTO、实现服务逻辑、配置路由和宿主。ServiceStack 还支持 WebSocket、SignalR 等实时通信协议,具备自动验证、自动过滤器等丰富功能,适合快速搭建高性能、可扩展的服务端应用。
143 3
|
11天前
|
Java 开发者 微服务
Spring Boot 入门:简化 Java Web 开发的强大工具
Spring Boot 是一个开源的 Java 基础框架,用于创建独立、生产级别的基于Spring框架的应用程序。它旨在简化Spring应用的初始搭建以及开发过程。
27 6
Spring Boot 入门:简化 Java Web 开发的强大工具
|
1月前
|
设计模式 前端开发 数据库
Python Web开发:Django框架下的全栈开发实战
【10月更文挑战第27天】本文介绍了Django框架在Python Web开发中的应用,涵盖了Django与Flask等框架的比较、项目结构、模型、视图、模板和URL配置等内容,并展示了实际代码示例,帮助读者快速掌握Django全栈开发的核心技术。
153 45
|
7天前
|
前端开发 安全 JavaScript
2025年,Web3开发学习路线全指南
本文提供了一条针对Dapp应用开发的学习路线,涵盖了Web3领域的重要技术栈,如区块链基础、以太坊技术、Solidity编程、智能合约开发及安全、web3.js和ethers.js库的使用、Truffle框架等。文章首先分析了国内区块链企业的技术需求,随后详细介绍了每个技术点的学习资源和方法,旨在帮助初学者系统地掌握Dapp开发所需的知识和技能。
2025年,Web3开发学习路线全指南
|
14天前
|
存储 前端开发 JavaScript
如何在项目中高效地进行 Web 组件化开发
高效地进行 Web 组件化开发需要从多个方面入手,通过明确目标、合理规划、规范开发、加强测试等一系列措施,实现组件的高效管理和利用,从而提高项目的整体开发效率和质量,为用户提供更好的体验。
24 7
|
18天前
|
开发框架 搜索推荐 数据可视化
Django框架适合开发哪种类型的Web应用程序?
Django 框架凭借其强大的功能、稳定性和可扩展性,几乎可以适应各种类型的 Web 应用程序开发需求。无论是简单的网站还是复杂的企业级系统,Django 都能提供可靠的支持,帮助开发者快速构建高质量的应用。同时,其活跃的社区和丰富的资源也为开发者在项目实施过程中提供了有力的保障。
|
17天前
|
开发框架 JavaScript 前端开发
TypeScript 是一种静态类型的编程语言,它扩展了 JavaScript,为 Web 开发带来了强大的类型系统、组件化开发支持、与主流框架的无缝集成、大型项目管理能力和提升开发体验等多方面优势
TypeScript 是一种静态类型的编程语言,它扩展了 JavaScript,为 Web 开发带来了强大的类型系统、组件化开发支持、与主流框架的无缝集成、大型项目管理能力和提升开发体验等多方面优势。通过明确的类型定义,TypeScript 能够在编码阶段发现潜在错误,提高代码质量;支持组件的清晰定义与复用,增强代码的可维护性;与 React、Vue 等框架结合,提供更佳的开发体验;适用于大型项目,优化代码结构和性能。随着 Web 技术的发展,TypeScript 的应用前景广阔,将继续引领 Web 开发的新趋势。
31 2
|
1月前
|
前端开发 API 开发者
Python Web开发者必看!AJAX、Fetch API实战技巧,让前后端交互如丝般顺滑!
在Web开发中,前后端的高效交互是提升用户体验的关键。本文通过一个基于Flask框架的博客系统实战案例,详细介绍了如何使用AJAX和Fetch API实现不刷新页面查看评论的功能。从后端路由设置到前端请求处理,全面展示了这两种技术的应用技巧,帮助Python Web开发者提升项目质量和开发效率。
44 1
|
1月前
|
XML 安全 PHP
PHP与SOAP Web服务开发:基础与进阶教程
本文介绍了PHP与SOAP Web服务的基础和进阶知识,涵盖SOAP的基本概念、PHP中的SoapServer和SoapClient类的使用方法,以及服务端和客户端的开发示例。此外,还探讨了安全性、性能优化等高级主题,帮助开发者掌握更高效的Web服务开发技巧。