Spring Boot 系列之 Web 开发

My email address: zrg1390556486@gmail.com

1. SpringBoot 对静态资源映射规则

  1. 所有 webjars/*,都去 classpath:/META-INF/resources/webjars/ 下找资源。 webjars,以jar包的方式引入静态资源,官网:https://www.webjars.org/

    eg:localhost:8080/webjars/jquery/3.3.1/jquery.js
    
  2. 访问当前项目内的资源

    "classpath:/META-INF/resources/"
    "classpath:/resources/"
    "classpath:/static/"
    "classpath:/public/"
    
  3. 欢迎页:静态资源目录下的所有 index.html 页面
  4. 所有的 /favicon.ico 都是在静态资源文件下找

    可配置:spring.web.resources.static-locations=classpath:/hello,classpath:/assets
    

2. Thymeleaf 模版引擎

JSP, Velocity, Thymeleaf, Mustache, freeMarker, Groovy

Thymeleaf用法:

  1. 引入thymeleaf

    <properties>
      <thymeleaf.version>3.0.9.RELEASE</thymeleaf.version>
      <!-- 布局功能的支持程序  thymeleaf3主程序  layout2以上版本 -->
      <!-- thymeleaf2   layout1-->
      <thymeleaf-layout-dialect.version>2.2.2</thymeleaf-layout-dialect.version>
    </properties>
    <dependency>
      <groupId>org.springframework.boot</groupId>
      <artifactId>spring-boot-starter-thymeleaf</artifactId>
      <version>${thymeleaf.version}</version>
    </dependency>
    
    
  2. 直接将 html 文件放在 classpath:/templates 目录下,就能自动渲染。
  3. 然后官网查看详细用法:https://www.thymeleaf.org/

    <html lang="en" xmlns:th="http://www.thymeleaf.org">
    
  4. 语法规则

    <p th:text="${hello}"></p>
    

3. Spring Boot 开发笔记

3.1. Spring MVC 自动配置

3.1.1. auto-configuration 原理

Spring Boot 自动配置好了 SpringMVC,以下是 SpringBoot 对 SpringMVC 的默认配置: WebMvcAutoConfiguration

  1. Inclusion of `ContentNegotiatingViewResolver` and `BeanNameViewResolver` beans.
    • 自动配置了ViewResolver(视图解析器:根据方法的返回值得到视图对象(View),视图对象决定如何渲染(转发?重定向?))
    • ContentNegotiatingViewResolver:组合所有的视图解析器的;
    • 如何定制:我们可以自己给容器中添加一个视图解析器;自动的将其组合进来;
  2. Support for serving static resources, including support for WebJars (see below).静态资源文件夹路径,webjars
  3. Static `index.html` support. 静态首页访问
  4. Custom `Favicon` support (see below). favicon.ico
  5. 自动注册了 of `Converter`, `GenericConverter`, `Formatter` beans.
    • Converter:转换器; public String hello(User user):类型转换使用Converter
    • `Formatter` 格式化器; 2017.12.17===Date;自己添加的格式化器转换器,我们只需要放在容器中即可
  6. Support for `HttpMessageConverters` (see below).
    • HttpMessageConverter:SpringMVC用来转换Http请求和响应的;User—Json;
    • `HttpMessageConverters` 是从容器中确定;获取所有的HttpMessageConverter;自己给容器中添加HttpMessageConverter,只需要将自己的组件注册容器中(@Bean,@Component)
  7. Automatic registration of `MessageCodesResolver` (see below).定义错误代码生成规则
  8. Automatic use of a `ConfigurableWebBindingInitializer` bean (see below). 我们可以配置一个ConfigurableWebBindingInitializer来替换默认的;(添加到容器)
    • org.springframework.boot.autoconfigure.web: web的所有自动场景
    • If you want to keep Spring Boot MVC features, and you just want to add additional MVC configuration (interceptors, formatters, view controllers etc.) you can add your own `@Configuration` class of type `WebMvcConfigurerAdapter`, but without `@EnableWebMvc`. If you wish to provide custom instances of `RequestMappingHandlerMapping`, `RequestMappingHandlerAdapter` or `ExceptionHandlerExceptionResolver` you can declare a `WebMvcRegistrationsAdapter` instance providing such components.
    • If you want to take complete control of Spring MVC, you can add your own `@Configuration` annotated with `@EnableWebMvc`.

3.1.2. 扩展与全面接管SpringMVC

  1. 扩展SpringMVC 原先在spring-mvc.xml中这样的:

    <mvc:view-controller path="/hello" view-name="success"/>
    <mvc:interceptors>
      <mvc:interceptor>
        <mvc:mapping path="/hello"/>
        <bean />
      </mvc:interceptor>
    </mvc:interceptors>
    

    现在,SpringBoot 可以编写一个配置类(@Configuration),是 WebMvcConfigurerAdapter 类型;不能标注 @EnableWebMvc。这样既保留了所有的自动配置,也能用我们扩展的配置。

    // 在 Spring Boot 2.0 之后 WebMvcConfigurerAdapter 就已经过时了,并且 WebMvcConfigurer 接口也发生了变化,里面所有的方法都定义成了默认方法(default)。
    // 因此我们可以直接实现 WebMvcConfigurer 接口,重写对应的方法即可。
    @Configuration
    public class MvcConfig implements WebMvcConfigurer {
        @Override
        public void addViewControllers(ViewControllerRegistry registry) {
            // super.addViewControllers(registry);
            //浏览器发送 /atguigu 请求来到 success
            registry.addViewController("/success").setViewName("index");
        }
    }
    

    原理:

    • WebMvcAutoConfiguration是SpringMVC的自动配置类,在做其他自动配置时会导入:@Import(EnableWebMvcConfiguration.class)
    • 容器中所有的WebMvcConfigurer都会一起起作用
    • 自定义的配置类也会被调用
  2. 全面接管 SpringMVC SpringBoot 对 SpringMVC 的自动配置不需要了,所有都是我们自己配置,所有的 SpringMVC 的自动配置都失效了。(但是实际开发中,不推荐全面接管,除非写很小的应用,因为大部分功能都会用到)

    **方法**:在配置类中添加 @EnableWebMvc 即可。在springboot中,有非常多的xxxx Configuration 帮助我们进行扩展配置,只要看见了这个东西,我们就要注意了!因为它可能改变了 Spring 原有的东西。 **注意**:当我们使用 @EnableWebMvc,则静态资源无法访问。

为什么添加@EnableWebMvc后,SpringBoot 自动配置就失效了?

  • @EnableWebMvc的核心

    @Import({DelegatingWebMvcConfiguration.class})
    public @interface EnableWebMvc {
    
  • DelegatingWebMvcConfiguration

    @Configuration(
                   proxyBeanMethods = false
                   )
    public class DelegatingWebMvcConfiguration extends WebMvcConfigurationSupport {
    
  • 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 {
    
  • @EnableWebMvc将WebMvcConfigurationSupport组件导入进来
  • 导入的WebMvcConfigurationSupport只是SpringMVC最基本的功能.

3.1.3. 如何修改 SpringBoot 的默认配置?

  • SpringBoot在自动配置很多组件的时候,先看容器中有没有用户自己配置的(@Bean、@Component)如果有就用用户配置的,如果没有,才自动配置;如果有些组件可以有多个(ViewResolver)将用户配置的和自己默认的组合起来;
  • 在SpringBoot中会有非常多的xxxConfigurer帮助我们进行扩展配置。
  • 在SpringBoot中会有很多的xxxCustomizer帮助我们进行定制配置。

3.2. SpringBoot 错误处理机制

3.2.1. 默认的错误处理机制

原理:可以参照ErrorMvcAutoConfiguration;错误处理的自动配置。

  1. DefaultErrorAttributes

    @Override
    public Map<String, Object> getErrorAttributes(RequestAttributes requestAttributes,
                                                  boolean includeStackTrace) {
        Map<String, Object> errorAttributes = new LinkedHashMap<String, Object>();
        errorAttributes.put("timestamp", new Date());
        addStatus(errorAttributes, requestAttributes);
        addErrorDetails(errorAttributes, requestAttributes, includeStackTrace);
        addPath(errorAttributes, requestAttributes);
        return errorAttributes;
    }
    
  2. BasicErrorController:处理默认/error请求

    @Controller
    @RequestMapping("${server.error.path:${error.path:/error}}")
    public class BasicErrorController extends AbstractErrorController {
    
        @RequestMapping(produces = "text/html")//产生html类型的数据;浏览器发送的请求来到这个方法处理
        public ModelAndView errorHtml(HttpServletRequest request,
                                      HttpServletResponse response) {
            HttpStatus status = getStatus(request);
            Map<String, Object> model = Collections.unmodifiableMap(getErrorAttributes(
                                                                                       request, isIncludeStackTrace(request, MediaType.TEXT_HTML)));
            response.setStatus(status.value());
    
            //去哪个页面作为错误页面;包含页面地址和页面内容
            ModelAndView modelAndView = resolveErrorView(request, response, status, model);
            return (modelAndView == null ? new ModelAndView("error", model) : modelAndView);
        }
    
        @RequestMapping
        @ResponseBody    //产生json数据,其他客户端来到这个方法处理;
        public ResponseEntity<Map<String, Object>> error(HttpServletRequest request) {
            Map<String, Object> body = getErrorAttributes(request,
                                                          isIncludeStackTrace(request, MediaType.ALL));
            HttpStatus status = getStatus(request);
            return new ResponseEntity<Map<String, Object>>(body, status);
        }
    
  3. ErrorPageCustomizer

    @Value("${error.path:/error}")
    private String path = "/error";  系统出现错误以后来到error请求进行处理;(web.xml注册的错误页面规则)
    
  4. DefaultErrorViewResolver

    @Override
    public ModelAndView resolveErrorView(HttpServletRequest request, HttpStatus status,
                                         Map<String, Object> model) {
        ModelAndView modelAndView = resolve(String.valueOf(status), model);
        if (modelAndView == null && SERIES_VIEWS.containsKey(status.series())) {
            modelAndView = resolve(SERIES_VIEWS.get(status.series()), model);
        }
        return modelAndView;
    }
    
    private ModelAndView resolve(String viewName, Map<String, Object> model) {
        //默认SpringBoot可以去找到一个页面?  error/404
        String errorViewName = "error/" + viewName;
    
        //模板引擎可以解析这个页面地址就用模板引擎解析
        TemplateAvailabilityProvider provider = this.templateAvailabilityProviders
            .getProvider(errorViewName, this.applicationContext);
        if (provider != null) {
            //模板引擎可用的情况下返回到errorViewName指定的视图地址
            return new ModelAndView(errorViewName, model);
        }
        //模板引擎不可用,就在静态资源文件夹下找errorViewName对应的页面   error/404.html
        return resolveResource(errorViewName, model);
    }
    


  • 一但系统出现4xx或者5xx之类的错误,ErrorPageCustomizer就会生效(定制错误的响应规则),就会来到/error请求;就会被BasicErrorController处理;
  • 去哪个页面是由DefaultErrorViewResolver解析得到的;

3.2.2. 定制错误响应

3.2.2.1. 定制错误页面
  1. 有模板引擎的情况下;error/状态码; 【将错误页面命名为 错误状态码.html 放在模板引擎文件夹里面的 error文件夹下】,发生此状态码的错误就会来到 对应的页面;
    页面能获取的信息:
    • timestamp:时间戳
    • status:状态码
    • error:错误提示
    • exception:异常对象
    • message:异常消息
    • errors:JSR303数据校验的错误都在这里
  2. 没有模板引擎(模板引擎找不到这个错误页面),静态资源文件夹下找;
  3. 以上都没有错误页面,就是默认来到SpringBoot默认的错误提示页面;
3.2.2.2. 定制错误的json数据
  1. 自定义异常处理&返回定制json数据

    @ControllerAdvice
    public class MyExceptionHandler {
    
        @ResponseBody
        @ExceptionHandler(UserNotExistException.class)
        public Map<String,Object> handleException(Exception e){
            Map<String,Object> map = new HashMap<>();
            map.put("code","user.notexist");
            map.put("message",e.getMessage());
            return map;
        }
    }
    
  2. 转发到/error进行自适应响应效果处理

    @ExceptionHandler(UserNotExistException.class)
    public String handleException(Exception e, HttpServletRequest request){
        Map<String,Object> map = new HashMap<>();
        //传入我们自己的错误状态码  4xx 5xx,否则就不会进入定制错误页面的解析流程
        /**
         * Integer statusCode = (Integer) request
         .getAttribute("javax.servlet.error.status_code");
        */
        request.setAttribute("javax.servlet.error.status_code",500);
        map.put("code","user.notexist");
        map.put("message",e.getMessage());
        //转发到/error
        return "forward:/error";
    }
    
  3. 将我们的定制数据携带出去 出现错误以后,会来到/error请求,会被BasicErrorController处理,响应出去可以获取的数据是由getErrorAttributes得到的(是AbstractErrorController(ErrorController)规定的方法)。
    • 完全来编写一个ErrorController的实现类【或者是编写AbstractErrorController的子类】,放在容器中;
    • 页面上能用的数据,或者是json返回能用的数据都是通过errorAttributes.getErrorAttributes得到;

容器中DefaultErrorAttributes.getErrorAttributes();默认进行数据处理的;

  1. 自定义ErrorAttribute

    //给容器中加入我们自己定义的ErrorAttributes
    @Component
    public class MyErrorAttributes extends DefaultErrorAttributes {
    
        @Override
        public Map<String, Object> getErrorAttributes(RequestAttributes requestAttributes, boolean includeStackTrace) {
            Map<String, Object> map = super.getErrorAttributes(requestAttributes, includeStackTrace);
            map.put("company","atguigu");
            return map;
        }
    }
    

3.3. 配置嵌入式 Servlet 容器

3.3.1. 如何定制和修改 Servlet 容器的相关配置?

  • 修改和server有关的配置(ServerProperties【也是EmbeddedServletContainerCustomizer】)

    server.port=8081
    server.context-path=/crud
    
    server.tomcat.uri-encoding=UTF-8
    
    //通用的Servlet容器设置
    server.xxx
    //Tomcat的设置
    server.tomcat.xxx
    
  • 编写一个EmbeddedServletContainerCustomizer:嵌入式的Servlet容器的定制器;来修改Servlet容器的配置

    @Bean  //一定要将这个定制器加入到容器中
    public EmbeddedServletContainerCustomizer embeddedServletContainerCustomizer(){
        return new EmbeddedServletContainerCustomizer() {
    
            //定制嵌入式的Servlet容器相关的规则
            @Override
            public void customize(ConfigurableEmbeddedServletContainer container) {
                container.setPort(8083);
            }
        };
    }
    

3.3.2. 注册 Servlet 三大组件:Servlet、Filter、Listener

由于SpringBoot默认是以jar包的方式启动嵌入式的Servlet容器来启动SpringBoot的web应用,没有web.xml文件。注册三大组件用以下方式:

  1. ServletRegistrationBean

    //注册三大组件
    @Bean
    public ServletRegistrationBean myServlet(){
        ServletRegistrationBean registrationBean = new ServletRegistrationBean(new MyServlet(),"/myServlet");
        return registrationBean;
    }
    
  2. FilterRegistrationBean

    @Bean
    public FilterRegistrationBean myFilter(){
        FilterRegistrationBean registrationBean = new FilterRegistrationBean();
        registrationBean.setFilter(new MyFilter());
        registrationBean.setUrlPatterns(Arrays.asList("/hello","/myServlet"));
        return registrationBean;
    }
    
  3. ServletListenerRegistrationBean

    @Bean
    public ServletListenerRegistrationBean myListener(){
        ServletListenerRegistrationBean<MyListener> registrationBean = new ServletListenerRegistrationBean<>(new MyListener());
        return registrationBean;
    }
    
  4. SpringBoot帮我们自动SpringMVC的时候,自动的注册SpringMVC的前端控制器;DIspatcherServlet;DispatcherServletAutoConfiguration中:

    @Bean(name = DEFAULT_DISPATCHER_SERVLET_REGISTRATION_BEAN_NAME)
    @ConditionalOnBean(value = DispatcherServlet.class, name = DEFAULT_DISPATCHER_SERVLET_BEAN_NAME)
    public ServletRegistrationBean dispatcherServletRegistration(
          DispatcherServlet dispatcherServlet) {
       ServletRegistrationBean registration = new ServletRegistrationBean(
             dispatcherServlet, this.serverProperties.getServletMapping());
        //默认拦截: /  所有请求;包静态资源,但是不拦截jsp请求;   /*会拦截jsp
        //可以通过server.servletPath来修改SpringMVC前端控制器默认拦截的请求路径
    
       registration.setName(DEFAULT_DISPATCHER_SERVLET_BEAN_NAME);
       registration.setLoadOnStartup(
             this.webMvcProperties.getServlet().getLoadOnStartup());
       if (this.multipartConfig != null) {
          registration.setMultipartConfig(this.multipartConfig);
       }
       return registration;
    }
    

3.3.3. 使用其他嵌入式容器(Undertow)

<dependencies>
    <!-- 引入Spring Web(默认依赖Tomcat,需排除) -->
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-web</artifactId>
        <!-- 排除默认的Tomcat依赖 -->
        <exclusions>
            <exclusion>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-starter-tomcat</artifactId>
            </exclusion>
        </exclusions>
    </dependency>

    <!-- 引入Undertow容器 -->
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-undertow</artifactId>
    </dependency>
</dependencies>

3.3.4. 嵌入式 Servlet 容器自动配置原理

  1. ServletWebServerFactoryAutoConfiguration

    package org.springframework.boot.autoconfigure.web.servlet;
    
    import javax.servlet.ServletRequest;
    import org.springframework.beans.BeansException;
    import org.springframework.beans.factory.BeanFactory;
    import org.springframework.beans.factory.BeanFactoryAware;
    import org.springframework.beans.factory.config.ConfigurableListableBeanFactory;
    import org.springframework.beans.factory.support.BeanDefinitionRegistry;
    import org.springframework.beans.factory.support.RootBeanDefinition;
    import org.springframework.boot.autoconfigure.AutoConfigureOrder;
    import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
    import org.springframework.boot.autoconfigure.condition.ConditionalOnWebApplication;
    import org.springframework.boot.autoconfigure.condition.ConditionalOnWebApplication.Type;
    import org.springframework.boot.autoconfigure.web.ServerProperties;
    import org.springframework.boot.context.properties.EnableConfigurationProperties;
    import org.springframework.boot.web.server.ErrorPageRegistrarBeanPostProcessor;
    import org.springframework.boot.web.server.WebServerFactoryCustomizerBeanPostProcessor;
    import org.springframework.context.annotation.Bean;
    import org.springframework.context.annotation.Configuration;
    import org.springframework.context.annotation.Import;
    import org.springframework.context.annotation.ImportBeanDefinitionRegistrar;
    import org.springframework.core.type.AnnotationMetadata;
    import org.springframework.util.ObjectUtils;
    
    @Configuration
    @AutoConfigureOrder(Integer.MIN_VALUE)
    @ConditionalOnClass({ServletRequest.class})
    @ConditionalOnWebApplication(
        type = Type.SERVLET
    )
    @EnableConfigurationProperties({ServerProperties.class})
    @Import({BeanPostProcessorsRegistrar.class, ServletWebServerFactoryConfiguration.EmbeddedTomcat.class, ServletWebServerFactoryConfiguration.EmbeddedJetty.class, ServletWebServerFactoryConfiguration.EmbeddedUndertow.class})
    public class ServletWebServerFactoryAutoConfiguration {
        public ServletWebServerFactoryAutoConfiguration() {
        }
    
        @Bean
        public ServletWebServerFactoryCustomizer servletWebServerFactoryCustomizer(ServerProperties serverProperties) {
            return new ServletWebServerFactoryCustomizer(serverProperties);
        }
    
        @Bean
        @ConditionalOnClass(
            name = {"org.apache.catalina.startup.Tomcat"}
        )
        public TomcatServletWebServerFactoryCustomizer tomcatServletWebServerFactoryCustomizer(ServerProperties serverProperties) {
            return new TomcatServletWebServerFactoryCustomizer(serverProperties);
        }
    
        public static class BeanPostProcessorsRegistrar implements ImportBeanDefinitionRegistrar, BeanFactoryAware {
            private ConfigurableListableBeanFactory beanFactory;
    
            public BeanPostProcessorsRegistrar() {
            }
    
            public void setBeanFactory(BeanFactory beanFactory) throws BeansException {
                if (beanFactory instanceof ConfigurableListableBeanFactory) {
                    this.beanFactory = (ConfigurableListableBeanFactory)beanFactory;
                }
    
            }
    
            public void registerBeanDefinitions(AnnotationMetadata importingClassMetadata, BeanDefinitionRegistry registry) {
                if (this.beanFactory != null) {
                    this.registerSyntheticBeanIfMissing(registry, "webServerFactoryCustomizerBeanPostProcessor", WebServerFactoryCustomizerBeanPostProcessor.class);
                    this.registerSyntheticBeanIfMissing(registry, "errorPageRegistrarBeanPostProcessor", ErrorPageRegistrarBeanPostProcessor.class);
                }
            }
    
            private void registerSyntheticBeanIfMissing(BeanDefinitionRegistry registry, String name, Class<?> beanClass) {
                if (ObjectUtils.isEmpty(this.beanFactory.getBeanNamesForType(beanClass, true, false))) {
                    RootBeanDefinition beanDefinition = new RootBeanDefinition(beanClass);
                    beanDefinition.setSynthetic(true);
                    registry.registerBeanDefinition(name, beanDefinition);
                }
    
            }
        }
    }
    
    

3.3.5. 嵌入式 Servlet 容器启动原理

  1. SpringBoot应用启动运行run方法
  2. refreshContext(context);SpringBoot刷新IOC容器【创建IOC容器对象,并初始化容器,创建容器中的每一个组件】;如果是web应用创建AnnotationConfigEmbeddedWebApplicationContext,否则:AnnotationConfigApplicationContext
  3. refresh(context);**刷新刚才创建好的ioc容器;
  4. onRefresh(); web的ioc容器重写了onRefresh方法;
  5. webioc容器会创建嵌入式的Servlet容器;createEmbeddedServletContainer();
  6. 获取嵌入式的Servlet容器工厂
  7. 使用容器工厂获取嵌入式的Servlet容器
  8. 嵌入式的Servlet容器创建对象并启动Servlet容器

3.4. 使用Spring Data JPA、Hikari连接池操作MySQL数据库

  1. pom中引入spring-boot-starter-data-jpa依赖,以及MySQL连接类mysql-connector-java依赖。
  2. springboot 2.0 后默认连接池就是Hikari了,所以引用parents后不用专门加依赖。
  3. 为了减少实体类或虚拟实体类的代码,引入**lombok**依赖。Lombok能以简单的注解形式来简化java代码,提高开发人员的开发效率。Lombok参考:https://www.jianshu.com/p/2ea9ff98f7d6

3.4.1. @Query自定义查询语句

在声明的方法上面标注@Query注解,即可通过写SQL实现自定义查询语句。正式生产编程中,除非迫不得已,否则不建议使用此方式进行数据查询或持久化操作。建议多用面向对象的思路进行编程,涉及多表关联等太过复杂的查询可以在业务层拼装数据。使用SQL,首先SQL维护起来不方便,其次而且如果大量使用了某个数据库的原生SQL将会造成系统与某一数据库绑定,无法更换数据库,各家数据库部分语法还是略有差异的。

原生查询

@Query(value = "SELECT * FROM STUDENT WHERE GENDER = :gender",nativeQuery = true)
public List<Student> findAllByGender(@Param("gender") String gender);

-- 其中使用@Param("gender")注入参数,nativeQuery = true代表使用当前数据库原生SQL语句。各家数据库部分语法还是略有差异,在非特殊情况下,不建议大量使用,如果大量使用,换数据库时会很痛苦,甚至整套系统只能使用某一品牌数据库。
@Query(value = "SELECT * FROM STUDENT WHERE GENDER = ?1 AND NAME like %?2%",nativeQuery = true)
public List<Student> findAllByGender( String gender,String namelk);

HQL查询

HQL学习可参考[Hibernate 之强大的HQL查询](https://www.cnblogs.com/quchengfeng/p/4111749.html)

3.5. RESTful API:CRUD

我们项目封了 Data REST,又封了 Data JPA,其实最后执行持久化到数据库里,是基于Hibernate的。当我们的json或者其他格式的数据转换成这个需要持久化的对象时,没有的属性转换时自然就为空值,保存到数据库里的也就为空值。 所以做更新时,后台给前台对象的哪些属性,调用RESTful更新接口时,前台也要给后台返回全部字段,这样不管如何增减字段,都由后台控制,前端只需返回原样的数据模型即可。后端人员在编写接口说明时,一定要特别注意这个细节,否则处理不当可能会发生生产事故。

还有另外一种方法就是后台接收到更新请求后,通过主键反查出此对象(findById),通过反射直接赋值。此种方式需重写更新方法不说,还牺牲了后台的效率,并不推荐。

小结:Spring Data REST都可快速帮我们实现了HAL数据风格的RESTful API接口。HAL概念请参考:分布式架构设计之Rest API HAL。换句话来说,Spring Data REST帮我们写了service层和controller层的代码。

3.5.1. 默认访问首页

//使用WebMvcConfigurerAdapter可以来扩展SpringMVC的功能
//@EnableWebMvc   不要接管SpringMVC
@Configuration
public class MyMvcConfig extends WebMvcConfigurerAdapter {

    @Override
    public void addViewControllers(ViewControllerRegistry registry) {
        // super.addViewControllers(registry);
        //浏览器发送 /atguigu 请求来到 success
        registry.addViewController("/atguigu").setViewName("success");
    }

    //所有的WebMvcConfigurerAdapter组件都会一起起作用
    @Bean //将组件注册在容器
    public WebMvcConfigurerAdapter webMvcConfigurerAdapter(){
        WebMvcConfigurerAdapter adapter = new WebMvcConfigurerAdapter() {
                @Override
                public void addViewControllers(ViewControllerRegistry registry) {
                    registry.addViewController("/").setViewName("login");
                    registry.addViewController("/index.html").setViewName("login");
                }
            };
        return adapter;
    }
}

3.5.2. 引入静态资源

@Override
public void addResourceHandlers(ResourceHandlerRegistry registry){
    registry.addResourceHandlers("/static/**").addResourceLocations("classpath:/static/");
}

3.5.3. 登录

  1. 登陆错误消息的显示

    @Controller
    public class LoginController {
        @RequestMapping(value = "/user/login", method = RequestMethod.POST)
        public String login(@RequestParam("username") String username,
                            @RequestParam("password") String password,
                            Map<String,Object> map) {
            if (!StringUtils.isEmpty(username) && "123456".equals(password)) {
                // 登录成功,防止表单重复提交,重定向到首页
                httpSession.setAttribute("user", username);
                return "redirect:/main.html";
            } else {
                map.put("msg","用户名或密码错误");
                return "signin";
            }
        }
    }
    
    <p style="color: red" th:text="${msg}" th:if="${not #strings.isEmpty(msg)}"></p>
    
  2. 注册拦截器

    @Bean
    public WebMvcConfigurer webMvcConfigurer() {
        return new WebMvcConfigurer() {
            @Override
            public void addViewControllers(ViewControllerRegistry registry) {
                registry.addViewController("/").setViewName("signin");
                registry.addViewController("/index.html").setViewName("signin"); // 登录页
                registry.addViewController("/main.html").setViewName("dashboard"); // 首页
            }
    
            // 注册拦截器
            @Override
            public void addInterceptors(InterceptorRegistry registry) {
                // springboot已经做好了静态资源映射
                registry.addInterceptor(new LoginHandlerInterceptor()).addPathPatterns("/**").excludePathPatterns("/index.html", "/", "/user/login");
            }
        };
    }
    

3.5.4. 国际化

  1. 编写国际化配置文件
  2. 使用ResourceBundleMessageSource管理国际化资源文件
  3. 在页面使用fmt:message取出国际化内容

步骤:

  1. 编写国际化配置文件,抽取页面需要显示的国际化消息

    signin.properties
    signin_en_US.properties
    signin_zh_CN.properties
    
  2. SpringBoot自动配置好了管理国际化资源文件的组件

    @Configuration(proxyBeanMethods = false)
    @ConditionalOnMissingBean(
                              name = {"messageSource"},
                              search = SearchStrategy.CURRENT
                              )
    @AutoConfigureOrder(-2147483648)
    @Conditional({MessageSourceAutoConfiguration.ResourceBundleCondition.class})
    @EnableConfigurationProperties
    public class MessageSourceAutoConfiguration {
        private static final Resource[] NO_RESOURCES = new Resource[0];
    
        public MessageSourceAutoConfiguration() {
        }
    
        @Bean
        @ConfigurationProperties(
                                 prefix = "spring.messages"
                                 )
        public MessageSourceProperties messageSourceProperties() {
            return new MessageSourceProperties();
        }
    
        @Bean
        public MessageSource messageSource(MessageSourceProperties properties) {
            ResourceBundleMessageSource messageSource = new ResourceBundleMessageSource();
            if (StringUtils.hasText(properties.getBasename())) {
                //设置国际化资源文件的基础名(去掉语言国家代码的)
                messageSource.setBasenames(StringUtils.commaDelimitedListToStringArray(StringUtils.trimAllWhitespace(properties.getBasename())));
            }
    
            if (properties.getEncoding() != null) {
                messageSource.setDefaultEncoding(properties.getEncoding().name());
            }
    
            messageSource.setFallbackToSystemLocale(properties.isFallbackToSystemLocale());
            Duration cacheDuration = properties.getCacheDuration();
            if (cacheDuration != null) {
                messageSource.setCacheMillis(cacheDuration.toMillis());
            }
    
            messageSource.setAlwaysUseMessageFormat(properties.isAlwaysUseMessageFormat());
            messageSource.setUseCodeAsDefaultMessage(properties.isUseCodeAsDefaultMessage());
            return messageSource;
        }
    
  3. 去页面获取国际化的值

    <!DOCTYPE html>
    <html lang="en" xmlns:th="http://www.thymeleaf.org">
      <head>
        <title>Signin Template · Bootstrap v5.0</title>
      </head>
      <body class="text-center">
    
        <main class="form-signin">
          <form>
            <img class="mb-4" src="/assets/img/svg/bootstrap-logo.svg" alt="" width="72" height="57">
            <h1 class="h3 mb-3 fw-normal" th:text="#{signin.tip}">Please sign in</h1>
            <label for="inputEmail" class="visually-hidden" th:text="#{signin.email}">Email address</label>
            <input type="email" id="inputEmail" class="form-control" placeholder="Email address"
                   th:placeholder="#{signin.email}" required="" autofocus="">
            <label for="inputPassword" class="visually-hidden" th:text="#{signin.password}">Password</label>
            <input type="password" id="inputPassword" class="form-control" placeholder="Password"
                   th:placeholder="#{signin.password}" required="">
            <div class="checkbox mb-3">
              <label>
                <input type="checkbox" value="remember-me"> [[#{signin.remeber}]]
              </label>
            </div>
            <button class="w-100 btn btn-lg btn-primary" type="submit" th:text="#{signin.btn}">Sign in</button>
            <p class="mt-5 mb-3 text-muted">© 2017-2020</p>
          </form>
        </main>
    
      </body>
    </html>
    

效果:根据浏览器语言设置的信息切换了国际化。

**原理**:国际化Locale(区域信息对象);LocaleResolver(获取区域信息对象);

// WebMvcAutoConfiguration.class
@Bean
@ConditionalOnMissingBean(
                          name = {"localeResolver"}
                          )
public LocaleResolver localeResolver() {
    if (this.webProperties.getLocaleResolver() == org.springframework.boot.autoconfigure.web.WebProperties.LocaleResolver.FIXED) {
        return new FixedLocaleResolver(this.webProperties.getLocale());
    } else if (this.mvcProperties.getLocaleResolver() == org.springframework.boot.autoconfigure.web.servlet.WebMvcProperties.LocaleResolver.FIXED) {
        return new FixedLocaleResolver(this.mvcProperties.getLocale());
    } else {
        AcceptHeaderLocaleResolver localeResolver = new AcceptHeaderLocaleResolver();
        Locale locale = this.webProperties.getLocale() != null ? this.webProperties.getLocale() : this.mvcProperties.getLocale();
        localeResolver.setDefaultLocale(locale);
        return localeResolver;
    }
}

3.6. Spring Validation 参数校验

3.6.1. Valid 和 Validated 的区别

  Valid Validated
提供者 标准 Java Bean Validation(JSR 303/JSR 380) Spring Framework 扩展
是否支持分组 不支持 支持
标注位置 METHOD, FIELD, CONSTRUCTOR, PARAMETER, TYPE_USE TYPE, METHOD, PARAMETER
嵌套校验 支持嵌套验证(即验证嵌套对象的字段) 也支持嵌套验证,但常用于 Spring 中的复杂验证场景
兼容性 兼容 Java 标准 Bean Validation 兼容 Spring 框架,但依赖 Spring

3.6.2. 引入依赖

如果spring-boot版本小于2.3.x,spring-boot-starter-web会自动传入hibernate-validator依赖。
如果spring-boot版本大于2.3.x,则需要手动引入依赖:
<dependency>
  <groupId>org.hibernate</groupId>
  <artifactId>hibernate-validator</artifactId>
  <version>6.0.1.Final</version>
</dependency>

3.6.3. 预定义对象的说明

接口统一返回 ReturnResult 定义
import lombok.Data;
import lombok.experimental.Accessors;

/**
 * Return Result
 *
 */
@Data
@Accessors(chain = true)
public class ReturnResult<T> {
    private int code;
    private String message;
    private T data;

    public boolean ok() {
        return this.code == 0;
    }

    public static <T> Result<T> success() {
        return new Result<T>().setCode(0).setMessage("SUCCESS");
    }

    public static <T> Result<T> success(T data) {
        return new Result<T>().setCode(0).setMessage("SUCCESS").setData(data);
    }

    public static <T> Result<T> failure() {
        return new Result<T>().setCode(-1).setMessage("FAILURE");
    }

    public static <T> Result<T> failure(int code, String msg) {
        return new Result<T>().setCode(code).setMessage(msg);
    }

    public static <T> Result<T> failure(int code, String msg, T data) {
        return new Result<T>().setCode(-1).setMessage("FAILURE").setData(data);
    }
}
ErrorCode
/**
 * Error Code
 *
 */
public final class ErrorCode {
    /**
     * Normal
     */
    public static final int NORMAL = 200;
    /**
     * Request error
     */
    public static final int REQUEST_ERROR = 400;
    /**
     * Server refuse request
     */
    public static final int SERVER_REFUSE_REQUEST = 403;
    /**
     * Server internal error
     */
    public static final int SERVER_INTERNAL_ERROR = 500;
    /**
     * Argument valid failure
     */
    public static final int ARGUMENT_VALID_FAILURE = 1001;

}

3.6.4. 常用参数校验

限制 说明
@Null 限制只能为null
@NotNull  
@AssertFalse  
@AssertTrue  
@DecimalMax(value)  
@DecimalMin(value)  
@Digits(integer,fraction) 限制必须为一个小数,且整数部分的位数不能超过integer,小数部分的位数不能超过fraction
@Future  
@Max(value)  
@Min(value)  
@Pattern(value) 必须符合指定的正则表达式
@Size(max,min)  
@NotEmpty  
@NotBlank  
@Email  

3.6.5. RequestBody校验

/**
 * RequestBody 参数校验
 * 校验失败会抛出 MethodArgumentNotValidException 异常
 *
 */
@RequestMapping("/api/user")
@RestController
public class UserController {

    /**
     * RequestBody 参数校验
     * 使用 @Valid 和 @Validated 都可以
     */
    @PostMapping("/save/1")
    public ReturnResult saveUser(@RequestBody @Validated UserDTO userDTO) {
        return ReturnResult.success();
    }

    @PostMapping("/save/2")
    public ReturnResult save2User(@RequestBody @Valid UserDTO userDTO) {
        return ReturnResult.success();
    }
}

3.6.6. RequestParam / PathVariable 校验

/**
 * RequestMapping / PathVariable 参数校验
 * 校验失败会抛出 ConstraintViolationException 异常
 * 
 * 此时必须在Controller上标注 @Validated 注解,并在入参上声明约束注解
 */
@RequestMapping("/api/user")
@RestController
@Validated
public class UserController {
    /**
     * 路径变量
     * 添加约束注解 @Min
     */
    @GetMapping("{userId}")
    public ReturnResult detail(@PathVariable("userId") @Min(10000000000000000L) Long userId) {
        // 校验通过,才会执行业务逻辑处理
    }

    /**
     * 查询参数
     * 添加约束注解 @Length @NotNull
     */
    @GetMapping("getByAccount")
    public ReturnResult getByAccount(@Length(min = 6, max = 20) @NotNull String  account) {
        // 校验通过,才会执行业务逻辑处理
    }
}

3.6.7. 全局异常处理


在实际项目开发中,通常会用统一异常处理来返回一个更友好的提示。

/**
 * 统一异常处理
 *
 */
@RestControllerAdvice
public class GlobalExceptionHandler {
    /**
     * 参数校验错误的异常处理
     */
    @ExceptionHandler({MethodArgumentNotValidException.class})
    @ResponseStatus(HttpStatus.OK)
    @ResponseBody
    public Result handleMethodArgumentNotValidException(MethodArgumentNotValidException ex) {
        BindingResult bindingResult = ex.getBindingResult();
        StringBuilder sb = new StringBuilder("校验失败:");
        for (FieldError fieldError : bindingResult.getFieldErrors()) {
            sb.append(fieldError.getField()).append(":").append(fieldError.getDefaultMessage()).append(", ");
        }
        String msg = sb.toString();
        return ReturnResult.failure(ErrorCode.ARGUMENT_VALID_FAILURE, msg);
    }

    @ExceptionHandler({ConstraintViolationException.class})
    @ResponseStatus(HttpStatus.OK)
    @ResponseBody
    public Result handleConstraintViolationException(ConstraintViolationException ex) {
        return ReturnResult.failure(ErrorCode.ARGUMENT_VALID_FAILURE, ex.getMessage());
    }

    /**
     * 未知异常处理
     * @param e Exception
     * @return
     */
    @ExceptionHandler(Exception.class)
    @ResponseBody
    public ResponseEntity handlerException(Exception e){
        log.error(e.getMessage(),e);
        StringBuffer errorMsg = new StringBuffer();
        errorMsg.append(e.getMessage());
        HttpHeaders httpHeaders = new HttpHeaders();
        httpHeaders.setContentType(MediaType.APPLICATION_JSON);
        ResponseEntity<ReturnData> returnDataResponseEntity = new ResponseEntity<>(new ReturnData(ReturnData.FAIL_CODE, errorMsg.toString(), null, null), httpHeaders, HttpStatus.OK);
        return returnDataResponseEntity;
    }
}

3.6.8. 分组校验


为了区分业务场景,对于不同场景下的数据验证规则可能不一样(例如新增时可以不用传递 ID,而修改时必须传递ID),可以使用分组校验。

/**
 * 分组校验
 *
 */
@Data
public class UserGroupValidDTO {

    @NotNull(groups = Update.class)
    @Min(value = 10000000000000000L, groups = Update.class)
    private Long userId;

    @NotNull(groups = {Save.class, Update.class})
    @Length(min = 2, max = 10, groups = {Save.class, Update.class})
    private String userName;

    @NotNull(groups = {Save.class, Update.class})
    @Length(min = 6, max = 20, groups = {Save.class, Update.class})
    private String account;

    @NotNull(groups = {Save.class, Update.class})
    @Length(min = 6, max = 20, groups = {Save.class, Update.class})
    private String password;

    /**
     * 保存的时候校验分组
     */
    public interface Save {
    }

    /**
     * 更新的时候校验分组
     */
    public interface Update {
    }
}


Controller 实现:

/**
 * 分组校验
 *
 */
@RestController
@RequestMapping("/api/user_group_valid")
public class UserGroupValidController {

    @PostMapping("/save")
    public Result saveUser(@RequestBody @Validated(UserGroupValidDTO.Save.class) UserGroupValidDTO userDTO) {
        // 校验通过,才会执行业务逻辑处理
        return Result.success();
    }

    @PostMapping("/update")
    public Result updateUser(@RequestBody @Validated(UserGroupValidDTO.Update.class) UserGroupValidDTO userDTO) {
        // 校验通过,才会执行业务逻辑处理
        return Result.success();
    }
}

3.6.9. 嵌套校验


上面的校验主要是针对基本类型进行了校验,如果DTO中包含了自定义的实体类,就需要用到嵌套校验。

/**
 * 嵌套校验
 * DTO中的某个字段也是一个对象,这种情况下,可以使用嵌套校验
 *
 */
@Data
public class UserNestedValidDTO {
    @Min(value = 10000000000000000L, groups = Update.class)
    private Long userId;

    @NotNull(groups = {Save.class, Update.class})
    @Length(min = 2, max = 10, groups = {Save.class, Update.class})
    private String userName;

    @NotNull(groups = {Save.class, Update.class})
    @Length(min = 6, max = 20, groups = {Save.class, Update.class})
    private String account;

    @NotNull(groups = {Save.class, Update.class})
    @Length(min = 6, max = 20, groups = {Save.class, Update.class})
    private String password;

    /**
     * 此时DTO类的对应字段必须标记@Valid注解
     */
    @Valid
    @NotNull(groups = {Save.class, Update.class})
    private Job job;

    @Data
    public static class Job {

        @NotNull(groups = {Update.class})
        @Min(value = 1, groups = Update.class)
        private Long jobId;

        @NotNull(groups = {Save.class, Update.class})
        @Length(min = 2, max = 10, groups = {Save.class, Update.class})
        private String jobName;

        @NotNull(groups = {Save.class, Update.class})
        @Length(min = 2, max = 10, groups = {Save.class, Update.class})
        private String position;
    }

    /**
     * 保存的时候校验分组
     */
    public interface Save {
    }

    /**
     * 更新的时候校验分组
     */
    public interface Update {
    }
}


Controller 实现:

/**
 * 嵌套校验
 *
 */
@RestController
@RequestMapping("/api/user_nested_valid")
public class UserNestedValidController {

    @PostMapping("/save")
    public Result saveUser(@RequestBody @Validated(UserNestedValidDTO.Save.class) UserNestedValidDTO userDTO) {
        // 校验通过,才会执行业务逻辑处理
        return Result.success();
    }

    @PostMapping("/update")
    public Result updateUser(@RequestBody @Validated(UserNestedValidDTO.Update.class) UserNestedValidDTO userDTO) {
        // 校验通过,才会执行业务逻辑处理
        return Result.success();
    }
}

3.6.10. 集合校验


如果请求体直接传递了json数组给后台,并希望对数组中的每一项都进行参数校验。此时,如果我们直接使用java.util.Collection下的list或者set来接收数据,参数校验并不会生效(单个数组可以使用)!我们可以使用自定义list集合来接收参数:

/**
 * 包装 List类型,并声明 @Valid 注解
 * @param <E>
 */
@Data
public class ValidationList<E> implements List<E> {

    @Delegate // @Delegate是lombok注解
    @Valid // 一定要加@Valid注解
    public List<E> list = new ArrayList<>();

    // 一定要记得重写toString方法
    @Override
    public String toString() {
        return list.toString();
    }
}

Controller

/**
 * 集合校验
 *
 */
@RestController
@RequestMapping("/api/valid_list")
public class ValidListController {

    @PostMapping("/saveList")
    public Result saveList(@RequestBody @Validated(UserGroupValidDTO.Save.class) ValidationList<UserGroupValidDTO> userList) {
        // 校验通过,才会执行业务逻辑处理
        return Result.success();
    }
}

3.6.11. 编程式校验


上面都是通过注解来进行校验,也可以使用编程的方式进行校验:

/**
 * 编程式校验参数
 *
 */
@RequestMapping("/api/valid_with_code")
@RestController
public class ValidWithCodeController {
    @Autowired
    private javax.validation.Validator globalValidator;

    /**
     * 编程式校验
     */
    @PostMapping("/saveWithCodingValidate")
    public Result saveWithCodingValidate(@RequestBody UserGroupValidDTO userGroupValidDTO) {
        Set<ConstraintViolation<UserGroupValidDTO>> validate = globalValidator.validate(userGroupValidDTO, UserGroupValidDTO.Save.class);
        // 如果校验通过,validate为空;否则,validate包含未校验通过项
        if (validate.isEmpty()) {
            // 校验通过,才会执行业务逻辑处理

        } else {
            for (ConstraintViolation<UserGroupValidDTO> userGroupValidDTOConstraintViolation : validate) {
                // 校验失败,做其它逻辑
                System.out.println(userGroupValidDTOConstraintViolation);
                // throw new RuntimeException();
            }
        }
        return Result.success();
    }
}

配置快速失败

/**
 * Web 配置
 *
 * @author zrg
 * @date 2021/5/17 16:11
 */
@Configuration
public class WebConfig {
    /**
     * 校验参数时只要出现校验失败的情况,就立即抛出对应的异常,结束校验,不再进行后续的校验
     *
     * @return
     */
    @Bean
    public Validator validator() {
        ValidatorFactory validatorFactory = Validation.byProvider(HibernateValidator.class)
            .configure()
            .failFast(true)
            .buildValidatorFactory();
        return validatorFactory.getValidator();
    }

    @Bean
    public MethodValidationPostProcessor methodValidationPostProcessor() {
        MethodValidationPostProcessor methodValidationPostProcessor = new MethodValidationPostProcessor();
        methodValidationPostProcessor.setValidator(validator());
        return methodValidationPostProcessor;
    }
}

3.7. @Transactional配置参数详解

3.7.1. 1、rollbackFor:配置何种异常回滚

在@Transactional注解中如果不配置rollbackFor属性,那么只会在遇到RuntimeException的时候才会回滚,加上rollbackFor=Exception.class,可以让事务在遇到非运行时异常时也回滚。一般在日常生产开发中,我们配置成rollbackFor=Exception.class

3.7.2. 2、readOnly:读写事务控制

readOnly=true表明所注解的方法或类只是读取数据,我们的某个方法只提供查询时,可以进行此种配置。readOnly=false表明所注解的方法或类是增加,删除,修改数据。默认是false,一般使用默认即可,无需配置。

3.7.3. 3、Propagation事务传播行为

*开发人员不得进行此项配置,只能与项目负责人申请评估后方可进行配置

Propagation属性用来枚举事务的传播行为。所谓事务传播行为就是多个事务方法相互调用时,事务如何在这些方法间传播。Spring支持7种事务传播行为,默认为REQUIRED。

1、REQUIRED

REQUIRED是常用的事务传播行为,如果当前没有事务,就新建一个事务,如果已经存在一个事务中,加入到这个事务中。

我们使用sping data jpa时,它的实现类的方法就是使用了此项默认配置,所以我们操作各表时,事务能绑定到同一个,异常时全部回滚。

2、SUPPORTS

SUPPORTS表示当前方法不需要事务上下文,但是如果存在当前事务的话,那么这个方法会在这个事务中运行。

3、MANDATORY

MANDATORY表示该方法必须在事务中运行,如果当前事务不存在,则会抛出一个异常。不会主动开启一个事务。

4、REQUIRES_NEW

REQUIRES_NEW表示当前方法必须运行在它自己的事务中。一个新的事务将被启动,如果存在当前事务,在该方法执行期间,当前事务会被挂起(如果一个事务已经存在,则先将这个存在的事务挂起)。如果使用JTATransactionManager的话,则需要访问TransactionManager。

5、NOT_SUPPORTED

NOT_SUPPORTED表示该方法不应该运行在事务中,如果存在当前事务,在该方法运行期间,当前事务将被挂起。如果使用JTATransactionManager的话,则需要访问TransactionManager。

6、NEVER

NEVER表示当前方法不应该运行在事务上下文中,如果当前正有一个事务在运行,则会抛出异常。

7、NESTED

NESTED表示如果当前已经存在一个事务,那么该方法将会在嵌套事务中运行。嵌套的事务可以独立于当前事务进行单独地提交或回滚。如果当前事务不存在,那么其行为与REQUIRED一样。嵌套事务一个非常重要的概念就是内层事务依赖于外层事务。外层事务失败时,会回滚内层事务所做的动作。而内层事务操作失败并不会引起外层事务的回滚。

3.7.4. 4、isolation:事务隔离级别 [详见数据库篇]

*开发人员不得进行此项配置,只能与项目负责人申请评估后方可进行配置