Loading

Spring实战 五 Web中的Spring

概述

Spring作为一个Java领域杰出的框架,自然是拥有web应用方面的支持的。

Spring提供了spring-webmvc包来支持构建Web应用,从名字就能看出这个Web框架是基于MVC架构的。

请求走向

Web框架主要的工作就是和用户请求打交道,Spring在Java Servlet技术之上,通过使用一个DispatcherServlet来接收全局所有的请求到框架中。

用户主要的工作是编写控制器,而Spring在接收到请求之后会根据请求的url匹配对应的控制器进行处理,也就是把请求的控制权返还给我们。

请求到达了控制器,控制器中的代码一般会按照请求的具体参数,再去服务层接口或者模型层接口去获取返回数据,这些需要返回给前端的数据称之为模型(Model)。

你不能直接把原始的数据返回给用户,你要使用更有表现力和交互性的视图来呈现给用户,所以,控制器要做的最后一件事就是将模型和指定的视图打包,返回给DispatcherServlet,然后控制器的工作就完成了,控制权回到了DispatcherServlet手中。

上一步我们传递的视图并不是一个实体的视图对象,而是一个逻辑视图名,DispatcherServlet拿到之后使用视图解析器来通过逻辑视图名解析对应的视图实现(JSP或其他模板)。

最后DispatcherServlet将模型数据交付给视图,视图将模型渲染输出到模板中,最后返回到页面上。

搭建Spring MVC

引入依赖

首先在idea中创建一个Web项目,然后引入一些依赖。

<properties>
    <spring.version>5.3.9</spring.version>
</properties>

<dependencies>
    <dependency>
        <groupId>org.springframework</groupId>
        <artifactId>spring-webmvc</artifactId>
        <version>${spring.version}</version>
    </dependency>
    <dependency>
        <groupId>jstl</groupId>
        <artifactId>jstl</artifactId>
        <version>1.2</version>
    </dependency>

配置DispatcherServlet

听起来好像只需要在web.xml中配置DispathcerServlet就行了,但那总归是早先的做法。Servlet3和Spring3.1开始就已经支持使用Java类进行配置了。

public class SpittrWebApplicationInitializer
    extends AbstractAnnotationConfigDispatcherServletInitializer {
    @Override
    protected Class<?>[] getRootConfigClasses() {
        return new Class[]{ RootConfig.class };
    }

    @Override
    protected Class<?>[] getServletConfigClasses() {
        return new Class[]{ WebConfig.class };
    }

    @Override
    protected String[] getServletMappings() {
        return new String[]{ "/" };
    }
}

这个刚看到一定很迷,一点一点来解释。

枯燥的源码分析部分

如果不想看可以直接跳过

首先Servlet3中为对第三方框架友好,而提供了一个ServletContainerInitializer接口。它的作用是在web容器启动时给第三方框架一些初始化的机会,比如注册前端servlet(如DispatcherServlet)和filter。这样就省去了用户的一些手动配置过程。

框架如果想要使用这个特性进行自动初始化,必须在对应的jar包的META-INF/services目录创建一个名为javax.servlet.ServletContainerInitializer的文件,用来指定Web框架自己实现的ServletContainerInitializer实现类。Servlet容器在启动时会扫描每个jar包中的这个文件,然后实例化具体的Initializer实现类,执行其中的onStartup方法。

我们去Spring的包下看,果然有这个文件

其中的内容就是Spring框架中的Initializer实现类。

进去瞅瞅,首先就是一个简单的实现类,实现了Serlvet规定的接口

但是上面有个注解@HandlesTypes(WebApplicationInitializer.class),这是啥意思呢。

这个注解也是Servlet规范提供的,web容器在启动扫描到框架的Initializer实现类时,会检查这个类有没有HandlesTypes注解,如果有,会扫描所有HandlesTypes中指定类的实现类,并将这些实现类作为onStartup方法的参数传入。

咱也不知道它是用了什么黑科技扫描到所有实现类的。。。反正就扫到了。

也就是说,所有你项目中的WebApplicationInitializer的实现类都会被扫描到,并传入到SpringServletContainerInitializeronStartup方法中。

然后SpringServletContainerInitializeronStartup方法中对这些HandlesTypes中指定的实现类进行初始化。实际上,Spring在这个方法中就是做了一些检查操作,创建那些你项目中那些实现类的实例,并且调用它们自己的方法进行初始化。挺绕的。

更绕的是,他喵的,WebApplicationInitializer接口的初始化方法也叫onStartup......但它和Servlet规范中的Initalizer没有任何关系。

不管那么多了,反正,我们现在已经知道了如果你想使用Spring集成Servlet容器,那么首先在你的Web项目当中创建一个WebApplicationInitializer的实现类。

运行

额,虽然成功了,但我不禁又陷入了沉思,我在这个onStartup里能干啥呢??我拥有的是一个ServletContext对象,但是我能拿他干啥呢??书读少了,东西都给你了你却不知道咋干活,呜呜呜。

Spring给我们提供了一个更高级的类,为了避免我们陷入刚才那种沉思。。。哈哈哈哈!!这个更高级的类就是AbstractAnnotationConfigDispatcherServletInitializer,有点长啊啊啊。

我们直接继承它就好了。那么这个类又是怎么写onStartup方法的呢?我进去瞄了一眼,但没敢细瞄,我现在的知识储备可能还不足以理解其中的各种关系。

不过,我们可以看到在往上捣了N层父类之后,有注册DispatcherServlet相关的代码

有注册Spring的WebApplicationContextContextLoaderListener的代码。

所以,继承了这个实现类之后,这些琐碎的工作就都不用我们干了,我们就可以享受开箱即用的快感。

在以前使用web.xml配置的时候,都是自己来手敲注册DispatcherServlet的。

回到不枯燥的部分

再看我们的初始化器代码

public class SpittrWebApplicationInitializer
    extends AbstractAnnotationConfigDispatcherServletInitializer {
    @Override
    protected Class<?>[] getRootConfigClasses() {
        return new Class[]{ RootConfig.class };
    }

    @Override
    protected Class<?>[] getServletConfigClasses() {
        return new Class[]{ WebConfig.class };
    }

    @Override
    protected String[] getServletMappings() {
        return new String[]{ "/" };
    }
}

现在我们只需要通过三个方法给Spring提供一些必要的信息,Spring就会自动帮我们完成所有配置,就能直接用了。

getRootConfigClasses中返回的类是Spring标准的Java配置类,这个配置中的Bean会被存到ContextLoaderListener创建的应用上下文Bean中。

getServletConfigClasses中返回的类同样是Spring的Java配置类,这个配置中的Bean会被存到Spring的DispatcherServlet的应用上下文中。

我知道这里所介绍的,由于对框架源码理解的不透彻,肯定会有一些错误,不过暂时不影响理解。

两个配置用来存储不同的Bean,getRootConfigClasses中返回的配置主要用于声明和后端或者其他组件来协同工作的Bean,而getServletConfigClasses中返回的配置主要用于声明和Web程序相关的Bean,如Web控制器,视图解析器等等。

getServletMappings就用于指定DispatcherServlet对哪些Url进行接管,现在我们指定的是对所有请求都进行接管。

把这些学完,记完,我已元气大伤。我要先休息一会了八嘎。

启用Spring MVC

现在我们有了一个空壳,它能跑起来但它什么都不能干,没有控制器,没有视图解析器,甚至没开启SpringWebMVC。

我们在和Web应用相关的配置类WebConfig中编写一些代码

@Configuration
@EnableWebMvc
@ComponentScan("io.lilpig.springlearn.springlearn02.web")
public class WebConfig implements WebMvcConfigurer {

    @Bean
    public ViewResolver viewResolver() {
        InternalResourceViewResolver resolver = new InternalResourceViewResolver();
        resolver.setPrefix("/WEB-INF/views/");
        resolver.setSuffix(".jsp");
        resolver.setExposeContextBeansAsAttributes(true);
        return resolver;
    }

    @Override
    public void configureDefaultServletHandling(DefaultServletHandlerConfigurer configurer) {
        configurer.enable();
    }
}

@EnableWebMvc注解告诉Spring我要启用WebMvc框架,@ComponentScan注解只扫描web包下的内容。我们以后所有web相关的代码都放到这个包。

然后我们让配置类实现了WebMvcConfigurer接口,用于做一些WebMvc相关的配置。这里原书(Spring in Action第四版)使用的是继承WebMvcConfigurerAdapter,然而在Spring5中这个类已经被废弃,由于JDK8接口提供了default方法,所以Spring直接使用接口了。

然后定义了一个Bean做视图解析器,这个视图解析器将Controller返回的逻辑视图名加上前缀和后缀,如果逻辑视图名是home,那么经过视图解析器之后就是/WEB-INF/views/home.jspsetExposeContextBeansAsAttributes的意思是是否让所有应用程序上下文中的SpringBean都作为request的Attribuite。

重写的configureDefaultServletHandling是告诉Spring需要将静态资源的请求转发到容器中默认的Servlet上。

RootConfig目前还没啥内容

@Configuration
@ComponentScan(basePackages = "io.lilpig.springlearn.springlearn02",
    excludeFilters = {@ComponentScan.Filter(type= FilterType.ANNOTATION, value = EnableWebMvc.class)}
)
public class RootConfig {
}

编写Web应用

声明基础控制器

在web包下创建一个Controller类

@Controller
public class HomeController {
    @RequestMapping(value = "/", method = GET)
    public String home(){
        return "home";
    }
}

@Controller说明它是一个控制器类,@Controller其实就是一个@Component所以会被自动扫描到,按理说直接写@Component也行,但就是可读性会更差。

@RequestMapping用来匹配请求,给定一个匹配规则和请求方法。这里我们直接返回了一个字符串,会被Spring认为是逻辑视图名,然后DispatcherServlet就会调用视图控制器进行一系列操作了。

编写视图

<%@ taglib uri="http://java.sun.com/jsp/jstl/core" prefix="c"%>
<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<html>
<head>
    <title>Title</title>
</head>
<body>
    <h1>Welcome to Spittr</h1>
    <a href="<c:url value="/spittles"/>">Spittles</a> |
    <a href="<c:url value="/spitter/regisiter"/>">Register</a>
</body>
</html>

现在运行应该能看到效果了

使用MockMVC进行单元测试

如何针对Web应用中的控制器进行单元测试?恐怕我们的测试类需要会主动发起HTTP请求,对比返回内容的异同,如果返回的不是JSON而是视图渲染后的HTML的话更加麻烦。

Spring提供了简单的测试工具MockMVC,它会自动帮我们发起HTTP请求,匹配视图名等等工作。

MockMVC需要引用两个依赖

<dependency>
    <groupId>org.springframework</groupId>
    <artifactId>spring-test</artifactId>
    <version>${spring.version}</version>
</dependency>
<dependency>
    <groupId>org.hamcrest</groupId>
    <artifactId>hamcrest-library</artifactId>
    <version>1.3</version>
</dependency>

第一个是Spring的测试工具,第二个是因为这个测试工具里使用到了JUnit的一些高级功能,而这些高级功能依赖hamcrest,但JUnit高版本又没有在依赖中添加这个库。。。莫名其妙。

现在可以编写测试类了。

import static org.springframework.test.web.servlet.setup.MockMvcBuilders.*;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.*;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*;

public class HomeControllerTest {
    @Test
    public void testHome() throws Exception {
        HomeController controller = new HomeController();
        MockMvc mockMvc =
                standaloneSetup(controller).build();
        mockMvc.perform(get("/"))
                .andExpect(view().name("home"));
    }
}

首先构建一个HomeController,然后初始化MockMVC,并且把我们的Controller装载进去,然后执行get对根路径进行请求,如果Controller工作正常,那么将返回一个名字为home的视图,而在这里又对视图名进行了断言。

执行测试

类级别的RequestMapping

@Controller
@RequestMapping("/")
public class HomeController {
    @RequestMapping(method = GET)
    public String homeGet(){
        return "home";
    }
    @RequestMapping(method = POST)
    public String homePost() {
        return homeGet();
    }
}

现在我们把RequestMapping注解放到了类级别上,那么这个Controller中的所有方法都会拥有这个/的映射。

这里我们构建了两个方法来实现Post和Get。

测试一下

类级别映射更常见的用法是这样的

@RequestMapping("/user")
public class UserController {
    @RequestMapping(value = "/getUsers",method = GET)
    public void getUsers(){}
}

这样配置/user/getUsers请求会被映射到getUsers方法上。

Spittr应用介绍

这是《Spring实战》(Spring In Action)的作者为教学演示而创建的示例,后面会逐渐完善这个示例。不过要先介绍其中的一些概念。

  • Spittr:应用名,省略了一个e,为了和下面一个概念做区分。Spittr是一个类似Twitter的博文社交应用。
  • Spitter:Spittr的用户,内容生产者,例如Youtuber是Youtube中的内容生产者。
  • Spittle:例如Tweet是Twitter中的帖子的概念一样,Spittle就是Spittr中用户生产的内容,也就是一篇博文。

所以,视图中的内容应该能明白了。

将Model数据传入视图

下面完善Spittle的部分,Spittle是Spittr中的博文。首先先创建Bean。

public class Spittle {
    private final Long id;
    private final String message;
    private final Date time;
    // 发布时所在的经纬度
    private Double latitude;
    private Double longtitude;

    // 省略Constructer,Getter And Setter...
}

定义一个持久层接口,findSpittle的max参数指定返回的Spittle中ID的最大值,而count参数指定返回的数量。

public interface SpittleRepository {
    List<Spittle> findSpittles(long max, int count);
}

需要将Model数据传入视图,首先要在控制器方法中声明一个Model参数,Spring作为一个基于依赖注入理念的框架,给你注入一个Model属性看起来很顺理成章。

@Controller
@RequestMapping("/spittles")
public class SpittleController {
    private SpittleRepository repository;

    @Autowired
    public SpittleController(SpittleRepository repository) {
        this.repository = repository;
    }

    @RequestMapping(method = GET)
    public String spittles(Model model) {
        model.addAttribute("spittleList",
                repository.findSpittles(Long.MAX_VALUE,20)
        );
        return "spittles";
    }
}

同时我们向Spring框架要了一个SpittleRepository持久层对象,通过构造方法给我们,显然现在还没有这个对象。不过先把关注点放在处理器方法spittles上,这也是分离设计的好处。

Spring会给spittles方法注入一个model类型的参数,这个model中可以存一些数据,上面我们调用了model.addAttribute存入了一个id为spittleList的Spittle列表,然后返回视图名。DispatcherServlet会将这个模型中的内容绑定到视图中。

现在运行肯定失败,持久层还是一个接口,没有具体的实现类,而且视图也没写。

我们编写一个简单的SpittleRepository实现类,其中忽略参数,只返回固定的四个Spittle用作测试。并且我们使用了@Component注解,这样Spring才能扫描到它并注入到Controller中。

@Component
public class SpittleRepositoryImpl implements SpittleRepository{

    @Override
    public List<Spittle> findSpittles(long max, int count) {
        return Arrays.asList(
                new Spittle(1l,"First Spittle!!!!", new Date(), null, null),
                new Spittle(2l,"Another Spittle!!!", new Date(), null, null),
                new Spittle(3l,"Spittle!! Spittle!! Spittle!!", new Date(), null, null),
                new Spittle(4l,"Spittles go forth!!", new Date(), null, null)
        );
    }
}

编写视图,这就没啥好说的了。

<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<%@ taglib uri="http://java.sun.com/jsp/jstl/core" prefix="c"%>
<html>
<head>
    <title>Title</title>
</head>
<body>
    <c:forEach items="${spittleList}" var="spittle">
        <li id="spittle-<c:out value="${spittle.id}"/>">
            <div class="spittle-message">
                <c:out value="${spittle.message}"/>
            </div>

            <div>
                <span class="spittle-time"><c:out value="${spittle.time}"/></span>
                <span class="spittle-location">
                    (<c:out value="${spittle.latitude}"/>, <c:out value="${spittle.longtitude}"/>)
                </span>
            </div>
        </li>
    </c:forEach>
</body>
</html>

你可以将上面的方法简写成

@RequestMapping(method = GET)
public String spittles(Model model) {
    model.addAttribute(
            repository.findSpittles(Long.MAX_VALUE,20)
    );
    return "spittles";
}

省略了addAttribute的第一个参数,也就是说没有给传递到Model中的参数提供id,默认的ID根据传入的类型推断,如现在是List<Spittle>,所以推断出的ID就是spittleList

还可以写成

@RequestMapping(method = GET)
public List<Spittle> spittles() {
    return repository.findSpittles(Long.MAX_VALUE,20);
}

Controller返回的不是字符串,而是List<Spittle>,Spring就会把它自动按照上面的id推断来存入到Model中,并且这次处理针对的是/spittles请求,那么对应的视图名就是spittles。这里就有点面向约定编程了。如果能写全我还是喜欢写全,但毕竟现在直接用视图模板开发的很少了,后端只会返回JSON。

请求参数

Web应用会有很多输入,包括Get请求的QueryString,Post请求体中携带的内容等等,这些输入需要我们处理。Spring使用依赖注入以一种灵活强大的方式来处理这些输入。

持久层的`findSpittles有两个参数,但我们的程序还没法接到这两个参数,这两个参数应该由用户进行上一页,下一页操作时传入。

最简单的办法就是通过@RequestParam

@RequestMapping(method = GET)
public List<Spittle> spittles(
        @RequestParam Long max,
        @RequestParam Integer count
) {
    System.out.println("MAX IS: "+max+", COUNT IS: "+count);
    return repository.findSpittles(max,count);
}

现在我们就能接收到QueryString中的参数了,试试访问

http://localhost:8080/[Application Context Path]/spittles?max=20&count=20

控制台会打印出这两个内容

@RequestParam完全不指定任何参数的时候,就用方法中的参数名去QueryString中查找。所以现在的maxcount是方法中完全对应的,事实上也推荐这样来命名,但凡事总有例外,如果前端给的参数就是不符合Java的变量名命名规范,而且还不能改,就必须使用name属性来显式匹配QueryString中的参数了。

@RequestMapping(method = GET)
public List<Spittle> spittles(
        @RequestParam("max") Long max,
        @RequestParam("count") Integer count
) { ... }

name属性是value属性互为彼此的别名,所以你可以直接省略name="max"的前缀。

现在,运行项目,如果直接通过home页面的连接点击到/spittles中,会发生异常。

异常消息中说,没有提供必要的参数max,我们希望在用户没有提供其中一些字段时使用默认值,而不是完全无法访问。

首先,设置@RequestParam(name="xxx", required=false)时,对应的参数没在QueryString中提供会被忽略,但并不会有具体的默认值。

defaultValue属性则为对应的参数提供了默认值。

static final String MAX_LONG_AS_STRING = "9223372036854775807";

@RequestMapping(method = GET)
public List<Spittle> spittles(
        @RequestParam(name = "max", defaultValue = MAX_LONG_AS_STRING) Long max,
        @RequestParam(name = "count", defaultValue = "20") Integer count
) {
    System.out.println("MAX IS: "+max+", COUNT IS: "+count);
    return repository.findSpittles(max,count);
}

由于Java注解限制,这些默认值必须是字符类型常量,但稍后会被转换成正确的类型传入到方法参数中。

这时,不携带参数再去访问/spittles

同样也可以使用Model,同时接收参数。

路径参数

前几年Restful API风格火起来了,但最近好像越来越少,但不耽误它是一个应该被学习的简洁的好API风格。

Restful API不再使用QueryString,而是把所有参数放在路径中,比如获取全部Spittle的路径可以是/spittles,那么根据单一id获取Spittle的路径就是/spittles/1,这里的1就代表Spittle的ID。

我们先修改持久层接口,让它支持获取单个Spittle。

public interface SpittleRepository {
    List<Spittle> findSpittles(long max, int count);
    Spittle findOne(long id);
}
@Component
public class SpittleRepositoryImpl implements SpittleRepository{
    private List<Spittle> spittles = Arrays.asList(
            new Spittle(1l,"First Spittle!!!!", new Date(), null, null),
            new Spittle(2l,"Another Spittle!!!", new Date(), null, null),
            new Spittle(3l,"Spittle!! Spittle!! Spittle!!", new Date(), null, null),
            new Spittle(4l,"Spittles go forth!!", new Date(), null, null)
    );

    @Override
    public List<Spittle> findSpittles(long max, int count) {
        return spittles;
    }

    @Override
    public Spittle findOne(long id) {
        return spittles.stream().filter(s->s.getId().equals(id)).findFirst().get();
    }

}

然后编写控制器。

@RequestMapping(path = "/{spittleId}",method = GET)
public String spittle(Model model,
        @PathVariable("spittleId") Long spittleId
) {
    model.addAttribute(
            repository.findOne(spittleId)
    );
    return "spittle";
}

@RequestMapping注解中的path使用占位符指定了在url中的一个参数,与类上的路径合并之后就是/spittles/{spittleId}。然后@PathVariable中也指定了spittleId目的是和方法的参数名匹配,未指定情况下,Spring会假设在@RequestMapping中的占位符就是参数的名字,所以这里我们也可以删掉@PathVariable中的参数。

其余的内容没啥新鲜的了。视图代码我也不贴了。

处理表单数据

现在加入注册功能,注册往往需要用到表单数据。

添加Spitter对应的持久层接口和实现类

public interface SpitterRepository {
    int save(Spitter spitter);
    Spitter getOne(Long id);
}
@Component
public class SpitterRepositoryImpl implements SpitterRepository {
    private Map<Long, Spitter> spitterStorage = new HashMap<>();

    public SpitterRepositoryImpl(){
        spitterStorage.put(0l,new Spitter(0l,"Yu","Laoba","yulaoba","125123"));
    }

    @Override
    public int save(Spitter spitter) {
        // 如果Spitter的Id不为null并且这个ID存在 视为修改Spitter
        if (spitter.getId() != null && spitterStorage.containsKey(spitter.getId())) {
            spitterStorage.put(spitter.getId(),spitter);
        } else {
            // 视为新建Spitter 忽略之前的
            Long maxId = spitterStorage.keySet().stream().max((a,b)->a.compareTo(b)).get();
            spitter.setId(maxId+1);
            spitterStorage.put(maxId+1,spitter);
        }
        return 1;
    }

    @Override
    public Spitter getOne(Long id) {
        return spitterStorage.get(id);
    }
}

这个实现类就是为了学习而简单创建的一个实现类,因为不想引入数据库逻辑而让示例变得复杂。但这个实现类在Web的并发场景下其实是线程不安全的,请不要在生产中写这种代码。

然后我们要编写SpitterController,来给用户提供一个注册的界面

这里用到了一个新的注解,GetMapping,这下就不用使用参数指明接管的请求方法了。

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

然后要编写register.jsp,很简单。

<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<html>
<head>
    <title>Title</title>
</head>
<body>
    <h1>开启Spittr历险</h1>
    <form method="post">
        <input type="text" name="firstName" placeholder="firstname"/>
        <input type="text" name="lastName" placeholder="lastname"/>
        <input type="text" name="username" placeholder="username"/>
        <input type="password" name="password" placeholder="password"/>
        <button type="submit">注册成为Spitter</button>
    </form>
</body>
</html>

form标签没指定action,那么就会向当前页面相同的url发送一个post请求。所以我们要在控制器中添加PostMapping来处理注册的逻辑。

@PostMapping("/register")
public String register(Spitter spitter) {
    repository.save(spitter);
    return "redirect:/spitter/"+spitter.getId();
}

这里十分简单,Spring会自动将表单数据转换成对应的JavaBean对象,要求是它们的字段名相同。

public class Spitter {
    private Long id;
    private String firstName;
    private String lastName;
    private String userName;
    private String password;
    // 省略构造器,getter,setter,equals和hashCode
}

这样,我们直接调用repository.save即可新建一个用户,实现注册逻辑。在创建用户之后,我们发送了一个转发请求,实际上转发到了/spitter/[用户id]这个链接上。我们用之前学过的路径参数来编写展示用户信息的控制器方法。

该方法通过调用持久层对象获取到对应的Spitter实例,然后存到model中,绑定到视图。

@GetMapping("/{spitterId}")
public String getSpitter(Model model,
        @PathVariable Long spitterId
) {
    model.addAttribute("spitter",repository.getOne(spitterId));
    return "spitter";
}

还需要编写最后一步,spitter.jsp

代码我就不贴了,我只是简单的把用户的firstname输出出来了。

校验表单

由于书中代码在新版本中出现异常,无法运行,先略过。

2021年9月9日14点43。喝喝,我回来了,我现在的心情有些激动,请允许我说一些废话。

本来打算开始学第六章的,但是发现第五章的问题不解决没法进行,现在已经快要上课了,看来今天很难学完第六章了。

关于我要解决的异常,有很多原因导致,大多是依赖冲突问题,可是我的项目中没有任何冲突依赖,其实只是hibernate-validator6.1.13中本身的依赖问题,升级版本就好了。非常感谢rschmidt13在Stack Overflow上的回答帮我解决了这个问题,可惜我没有足够的声誉,无法评论表示感谢,贴上原回答链接

原书解决可能会有版本冲突的办法倒是简单,原书中从来不向用户展示对应的maven依赖或jar包的细节,只是说有这么个东西,让用户自己去注入对应的版本,真是把解耦的思想运用到极致。。。。。。吐槽下哈哈哈哈。

好的那我开始了。

首先,现在如果我们啥都不填就注册,也能成。如果你想将用户的输入限制到一定范围,你就得在控制器中进行大量的if判断,如果有一个参数不满足,就不能继续注册,返回给用户对应的提示信息。

这样做也行,关键是又把控制器不该关注的逻辑拿到控制器里了,控制器该干的就是协调各方工作,关于咋工作,那是各个模块的事,该我控制器jier事啊?

Spring使用JSR303验证标准对数据进行校验,支持直接在实体类中使用注解进行校验。如下是JSR303提供的注解。

注意注意,JSR303只提供了规范,并没提供实现,实现需要其它库来,而且其他库可能会带有其它的扩展注解。

我们使用hibernate-validator这一实现,它和那个ORM框架不是一回事儿,它只是一个校验器模块而已。在pom中导入。

<dependency>
    <groupId>org.hibernate.validator</groupId>
    <artifactId>hibernate-validator</artifactId>
    <version>6.0.21.Final</version>
</dependency>

现在我们便可以给实体类打上注解,我们目前不对id做任何限制,后面可能会做。

public class Spitter {
    private Long id;
    @NotBlank(message = "firstName不能为空")
    private String firstName;
    @NotBlank(message = "lastName不能为空")
    private String lastName;
    @Size(min=6, max=20, message = "userName长度需要在4~20之间")
    private String userName;
    @Size(min=6, max=20, message = "password长度需要在4~20之间")
    private String password;
    // 省略
}

我们分别使用这些注解对各个字段进行了限制,再在控制器中使用@Valid注解,对某一个参数启用校验即可开启自动校验功能。

@PostMapping("/register")
public String register(
    @Valid Spitter spitter,
    Errors errors
) {
    if (errors.hasErrors()) {
        return "register";
    }
    repository.save(spitter);
    return "redirect:/spitter/"+spitter.getId();
}

这儿,我们给Spitter添加上了@Valid注解,这会让Spring在读取表单,转换为实体对象的阶段时验证对应的表单字段,如果有不满足要求的,就放到Errors对象中,然后我们只使用了一个if语句来判断是否有error,如果有就还是跳转到注册页面,没有才保存这个实体。

八嘎,赶紧去学第六章了,拜拜~~~

参考

posted @ 2021-09-05 20:21  yudoge  阅读(231)  评论(0编辑  收藏  举报