• [SSM框架]—SpringMVC


    前言

    SSM最后一节—SpringMVC

    之前就已经了解过PHP的MVC模式,现在是Spring,原理上都是想通的,所以就不记MVC介绍了

    PHP MVC框架初探_Sentiment.的博客-CSDN博客_phpmvc框架

    入门案例

    开发环境

    创建maven工程

    打包方式改成war,会自动生成web模块

    <packaging>warpackaging>
    
    • 1

    依赖

    <dependency>
        <groupId>org.springframeworkgroupId>
        <artifactId>spring-webmvcartifactId>
        <version>5.3.22version>
    dependency>
    
    <dependency>
        <groupId>ch.qos.logbackgroupId>
        <artifactId>logback-classicartifactId>
        <version>1.2.3version>
    dependency>
    
    <dependency>
        <groupId>javax.servletgroupId>
        <artifactId>javax.servlet-apiartifactId>
        <version>3.1.0version>
        <scope>providedscope>
    dependency>
    
    <dependency>
        <groupId>org.thymeleafgroupId>
        <artifactId>thymeleaf-spring5artifactId>
        <version>3.0.12.RELEASEversion>
    dependency>
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24

    web.xml

    url-patten中/和/*的区别:

    • /:匹配浏览器向服务器发送的所有请求(不包括.jsp)
    • /*:匹配浏览器向服务器发送的所有请求(包括.jsp)
    
    <servlet>
        <servlet-name>SpringMVCservlet-name>
        <servlet-class>org.springframework.web.servlet.DispatcherServletservlet-class>
    servlet>
    <servlet-mapping>
        <servlet-name>SpringMVCservlet-name>
        <url-pattern>/url-pattern>
    servlet-mapping>
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9

    创建配置文件

    配置文件的命名规则:±servlet.xml

    SpringMVC-servlet.xml

    
    <beans xmlns="http://www.springframework.org/schema/beans"
           xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
           xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd">
        
        <context:component-scan base-package="com.sentiment.controller">context:component-scan>
        <bean id="viewResolver" class="org.thymeleaf.spring5.view.ThymeleafViewResolver">
            <property name="order" value="1"/>
            <property name="characterEncoding" value="UTF-8"/>
            <property name="templateEngine">
                <bean class="org.thymeleaf.spring5.SpringTemplateEngine">
                    <property name="templateResolver">
                        <bean class="org.thymeleaf.spring5.templateresolver.SpringResourceTemplateResolver">
                            <property name="prefix" value="/WEB-INF/templates/"/>
                            <property name="suffix" value=".html"/>
                            <property name="templateMode" value="HTML5"/>
                            <property name="characterEncoding" value="UTF-8"/>
                        bean>
                    property>
                bean>
            property>
        bean>
        
    beans>
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24

    测试HelloWorld

    启动一个tomcat环境,默认路径设为/SpringMVC

    @RequestMapping(“/”),环境启动后,会默认访问/WEB-INF/templates/下的index.html

    @Controller
    public class HelloController {
        @RequestMapping("/")
        public String protal(){
            return "index";
        }
    
        @RequestMapping("/hello")
        public String hello(){
            return "success";
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12

    在index设置一个链接,th表示thymeleaf标签需要导入命名空间xmlns:th="http://www.thymeleaf.org"

    <a th:href="@{/hello}">测试a>
    
    • 1

    此时当点链接"测试"后,thymeleaf会根据当前路径访问/SpringMVC/hello,返回success,跳转到/WEB-INF/templates/下的success.html中

    这里如果用的是如下标签,则会默认访问绝对路径即:localhost:808/hello,故请求错误(404)

    <a href="/hello">测试a>
    
    • 1

    扩展

    前边说到Thymeleaf-spring5的配置文件命名规则必须是:+servlet.xml,而且它默认放在WEB-INF下,而一般资源都放在resources中,所以可以通过web.xml中的配置进行修改,这样就也无需按照之前的命名规则了

    <init-param>
        <param-name>contextConfigLocationparam-name>
        <param-value>classpath:SpringMVC.xmlparam-value>
    init-param>
    
    • 1
    • 2
    • 3
    • 4

    除此外servlet-class设置的为:DispatcherServlet,而DispatcherServlet进行了很多配置,所以在初次访问时需要一段时间响应,这里就可以通过Serlvet中的load-on-startup配置设置启动后的优先级

    <load-on-startup>1load-on-startup>
    
    • 1

    @RequstMapping注解

    @RequestMapping注解的作用就是将请求和处理请求的控制器方法关联起来,建立映射关系

    注解位置

    @RequestMapping标识一个类,设置映射请求的请求路径的初始信息

    @RequestMapping标识一个方法:设置映射请求请求路径的具体信息

    @Controller
    @RequestMapping("/test")
    public class RequestMappingTest {
        //此时请求hello方法路径为: /test/hello
        @RequestMapping("/hello")
        public String hello(){
            return "success";
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9

    value属性

    value属性是数组类型,即当前浏览器请求value属性中的任何一个值都会处理注解所标识的方法

    @Controller
    public class RequestMappingTest {
        @RequestMapping({"/hello","/aaa"})
        public String hello(){
            return "success";
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7

    访问/aaa时,同样会执行hello()

    method属性

    method属性是数组类型,即当前服务器请求method属性中的任何一个方式,都会被处理,报错信息为:405

    测试

    将/hello,/aaa路径设为post方式,此时就无法通过get方式访问了

    @Controller
    public class RequestMappingTest {
        @RequestMapping(
                value = {"/hello","/aaa"},
                method = {RequestMethod.POST}
        )
        public String hello(){
            return "success";
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10

    写个post方式的form表单,通过post请求成功访问

    <form th:action="@{/hello}" method="post">
        <input type="submit" value="method属性">
    form>
    
    • 1
    • 2
    • 3

    由于是数组类型,所以也可以设置多个值,此时get、post就都可以访问了

    @RequestMapping(
            value = {"/hello","/aaa"},
            method = {RequestMethod.POST,RequestMethod.GET}
    )
    
    • 1
    • 2
    • 3
    • 4

    派生注解

    除method属性外,还可以用@GetMapping、@PostMapping等注解实现同样功能

    params

    params属性是数组类型,通过请求的请求参数进行匹配,即浏览器发送的请求参数必须满足params属性的设置,报错信息为:400

    params的四种表达式

    “param”:表示当前所匹配的请求必须携带param参数

    “!param”:表示当前所匹配请求的请求参数一定不能携带param参数

    “param=value”:表示当前所匹配请求的请求参数必须鞋带param参数且值必须为value

    “param!=value”:表示当前匹配请求的请求参数可以不携带param参数,若携带一定不能是value

    如下为四种表达式分别表示:请求必须携带username,不能携带password,必须携带age且值为20,可以不鞋带gender若携带之不能为女

    params = {"username","!password","age=20","gender!=女"}
    
    • 1

    在这里插入图片描述

    headers

    headers属性通过请求的请求头信息匹配请求映射,报错信息为:404

    也有四种表达式且跟params中的一样

    如下表示必须携带referer,否则404

    headers = {"referer"}
    
    • 1

    ant风格

    @RequestMapping注解的value属性中可以设置一些特殊的字符:

    ?:任意的单个字符(不包括 ? 和 /)

    *:任意个数的任意字符(不包括 ? 和 /)

    **:任意层数的任意目录,但使用该字符时前后不能有任何其他字符

    ?和*就不看了 ,看下**

    @RequestMapping({"/**/test"})
    public String ant(){
        return "success";
    }
    
    • 1
    • 2
    • 3
    • 4

    使用该种方式,只要以任意形式访问/test目录都可跳转到success.html

    路径占位符(rest)

    传统:/deleteuser?id=1

    rest: /user/delete/1

    需要在@RequestNapping注解的value属性中所设置的路径中,使用{xxx}的方式表示路径中的数据

    在通过@Pathvariable注解,将占位符所标识的值和控制器方法的形参进行绑定

    @RequestMapping("/rest/{id}")
    public String rest(@PathVariable("id") int id){
        System.out.println(id);
        return "success";
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5

    SpringMVC获取请求参数

    通过ServletAPI获取参数

    只需要在控制器方法的形参位置,设置HttpservletRequest类型的形参,就可以在控制器方法中使用request对象获取请求参数

    @Controller
    public class TestParamController {
        @RequestMapping("/param/servletAPI")
        public String getParamByServletAPI(HttpServletRequest request){
            String uname = request.getParameter("uname");
            String password = request.getParameter("password");
            System.out.println("uname:"+uname+",password:"+password);
            return "success";
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10

    @RequestParam注解

    默认情况下其实只需要设置形参就可以匹配请求的参数

    @RequestMapping("/test")
    public String getParamByRequestParam(String uname , int password){
        System.out.println("uname:"+uname+",password:"+password);
        return "success";
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5

    但若形参设置的和请求参数不一致时,就会匹配不到,此时就可以用@RequestParam注解,即:

    public String getParamByRequestParam(
            @RequestParam("uname") String uname, 
            @RequestParam("password") int password)
    
    • 1
    • 2
    • 3

    注解有三个属性:value、required、defaultValue

    • value:参数名
    • required:设置是否必须传输value对应的请求参数,默认值为true(必须传),若没传则报错:400
    • defaultValue:设置value的默认值,设置该属性后required属性无论为true或false,都不会报错

    @RequestHeader和@CookieValue注解

    通过@RequestHeader获取referer头,@CookieValue获取XDEBUG_SESSION(Cookie中发现有之前配置的xdebug直接用了)

    @RequestMapping("/test")
    public String getParamByRequestParam(
            @RequestParam(value = "uname",required = true,defaultValue = "hello") String uname,
            @RequestParam("password") int password,
            @RequestHeader("referer") String referer,
            @CookieValue("XDEBUG_SESSION") String session
    ){
        System.out.println("referer:"+referer);
        System.out.println("XDEBUG_SESSION:"+session);
        System.out.println("uname:"+uname+",password:"+password);
        return "success";
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12

    在这里插入图片描述

    通过pojo获取请求参数

    前边提到可以通过注解获取请求参数,但是如果请求参数过多那么注解就会显得比较繁琐,这时候就可以通过pojo方式来进行管理,只需要请求参数名与pojo的属性名一致即可。

    @RequestMapping("/test/pojo")
    public String getParamByPojo(User user){
        System.out.println(user);
        return "success";
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5

    POJO

    @Data
    @AllArgsConstructor
    @NoArgsConstructor
    public class User {
        private int id;
        private String uname;
        private String password;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8

    请求参数乱码问题

    在/conf/server.xml中设置URLEncoding="UTF-8"可解决乱码问题,但该种方式只针对于GET传参,若用POST传参仍会出现乱码,这是就可以通过配置filter解决**(注:filter应放在其他配置之前,因为配置文件是按顺序执行的)**

    <filter>
        <filter-name>CharacterEncodingFilterfilter-name>
        <filter-class>org.springframework.web.filter.CharacterEncodingFilterfilter-class>
        <init-param>
            <param-name>encodingparam-name>
            <param-value>UTF-8param-value>
        init-param>
        <init-param>
            <param-name>ForceEncodingparam-name>
            <param-value>trueparam-value>
        init-param>
    filter>
    <filter-mapping>
        <filter-name>CharacterEncodingFilterfilter-name>
        <url-pattern>/*url-pattern>
    filter-mapping>
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16

    encoding代表请求编码,ForceEncoding代表响应编码

    看下CharacterEncodingFilter的继承关系
    在这里插入图片描述

    Filter接口的doFilter()方法由OncePerRequestFilter类实现,而在该类的doFilter方法中,最后的过滤是通过doFilterInternal实现的

    try {
       doFilterInternal(httpRequest, httpResponse, filterChain);
    }
    
    • 1
    • 2
    • 3

    doFilterInternal()写在CharacterEncodingFilter类中,
    在这里插入图片描述

    ①:获取encoding参数即:UTF-8

    ②:if判断,后或运算后边的,request.getCharacterEncoding()默认值为空,所以无论涉不设置ForceEncoding,都会执行if中的语句

    ③:判断isForceResponseEncoding的值,如果为真则为response设置编码,而我们传入的就是true,所以会执行

    通过②、③可以看出,ForceEncoding可以控制request和response的编码方式,而request中由于是或运算默认就会执行if中的语

    句,所以可以浅显的理解为ForceEncoding是控制response编码问题的参数

    域对象共享数据

    使用ServletAPI向request域对象共享数据

    跟Servlet的request作用域一样,不例举了

    使用ModelAndView向域对象共享数据

    ModelAndView中有Model和View功能,Model用于想请求域共享数据,View用于设置视图实现页面跳转

    public class TestScopeController {
        @RequestMapping("/test/mav")
        public ModelAndView testMAV(){
            ModelAndView mav = new ModelAndView();
           //向请求域中共享数据
            mav.addObject("testRequestScope", "Hello,Sentiment!");
            //设置逻辑视图
            mav.setViewName("success");
            return mav;
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11

    success.html

    获取请求域的testRequestScope数据

    DOCTYPE html>
    <html lang="en">
    <head>
        <meta charset="UTF-8">
        <title>Titletitle>
    head>
    <body>
        <h1>Success!h1>
        <p th:text="${testRequestScope}">p>
    body>
    html>
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11

    访问后的效果
    在这里插入图片描述

    Model、ModelMap、Map

    三个用法差不多放到了一起

    @RequestMapping("/test/model")
    public String testModel(Model model){
        model.addAttribute("testRequestScope","Hello,Model!");
        return "success";
    }
    
    @RequestMapping("/test/modelMap")
    public String testModelMap(ModelMap modelMap){
        modelMap.addAttribute("testRequestScope","Hello,ModelMap!");
        return "success";
    }
    
    @RequestMapping("/test/map")
    public String testMap(Map<String,Object> map){
        map.put("testRequestScope","Hello,Map!");
        return "success";
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17

    三种方法其实都是基于BindingAwareModelMap类的

    public class BindingAwareModelMap extends ExtendedModelMap {}
    public class ExtendedModelMap extends ModelMap implements Model{}
    public class ModelMap extends LinkedHashMap<String, Object> {}
    public class LinkedHashMap<K,V> extends HashMap<K,V> implements Map<K,V>{}
    
    • 1
    • 2
    • 3
    • 4

    BindingAwareModelMap继承ExtendedModelMapExtendedModelMap中继承了ModelMap并实现了Model,而ModelMap继承于LinkedHashMap,LinkedHashMap又实现了Map

    所以通过BindingAwareModelMap间接的实现了上述三个方法

    Seesion、Application

    用的是Servlet-api的形式,因为SpringMVC的方式相对而言更麻烦些

    @RequestMapping("/test/session")
    public String testSession(HttpSession httpSession){
        httpSession.setAttribute("testSeesionScope","Hello,Seesion!");
        return "success";
    }
    
    @RequestMapping("/test/application")
    public String testApplication(HttpSession httpSession){
        httpSession.getServletContext().setAttribute("testApplicationScope","Hello,Application!");
        return "success";
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11

    调用thymeleaf时需要加上seesion或application

    <p th:text="${seesion.testSeesionScope}">p>
    <p th:text="${application.testApplicationScope}">p>
    
    • 1
    • 2

    注:seesion是当前会话有效,所以当访问/test/session后,以后再访问其他页面后都会回显Hello,Seesion!,Application同理

    视图

    InternalResourceView

    请求后会进行内部转发,但是不会经过thymeleaf渲染所以不常用

    @Controller
    public class TestViewController {
        @RequestMapping("/test/view/forward")
        public String testInterResourceView(){
            return "forward:/test/model";
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7

    RedirectView

    请求后会进行重定向

    @RequestMapping("/test/view/redirect")
    public String testRedirectView(){
        return "redirect:/test/model";
    }
    
    • 1
    • 2
    • 3
    • 4

    视图控制器

    之前定义了一个控制器,在访问首页时触发:

    import org.springframework.stereotype.Controller;
    import org.springframework.web.bind.annotation.RequestMapping;
    
    @Controller
    public class ProtalMapping {
        @RequestMapping("/")
        public String protal(){
            return "index";
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10

    但只为了实现这一个控制器而写了一个类未免有点小题大做,因此可以通过视图控制器方式来定义

    <mvc:view-controller path="/" view-name="index">mvc:view-controller>
    
    • 1

    这样在访问根路径时就会自动跳转到index.html,方便许多,但是又引入了一个新的问题:

    ​ 当时用该控制器后,只有视图控制器所设置的请求会被处理,其他的请求都是404

    此时就必须配置一个新的标签就可以直接解决该问题

    RESTful

    REST: Representational State Transfer,表现层资源状态转移。

    可以理解为:restful只关心我们需要的资源,而不在意资源的操作方式,例:假如对user库进行资源的增删改查,那么这些操作都是对user库的操作,restful就可以将他设置为/user路径,之后的操作通过不同的请求方式来判断如何操作数据库即:

    GET:获取资源

    POST:新建资源

    PUT:更新资源

    DELETE:删除资源

    操作传统方式REST风格
    查询操作getUserById?id=1user/1 —> get请求方式
    保存操作saveUseruser —> post请求方式
    删除操作deleteUser?id=1user/1 —> delete请求方式
    更新操作updateUseruser —> put请求方式

    数据库操作

    查询操作

    GET方式

    @Controller
    public class TestRestController {
        @RequestMapping(value = "/user/{id}",method = RequestMethod.GET)
        public String getUserById(@PathVariable("id") int id){
            System.out.println("根据id查询信息-->/user/"+id+"-->get");
            return "success";
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8

    添加操作

    POST方式

    @RequestMapping(value ="/user",method = RequestMethod.POST)
    public String inserUser(){
        System.out.println("成功添加用户信息");
        return "success";
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5

    更新操作

    HiddenHttpMethodFilter

    表单中没有PUT和DELETE请求,所以可以通过HiddenHttpMethodFilter过滤器来获取这两种请求方式

    web.xml加上

    <filter>
        <filter-name>HiddenHttpMethodFilterfilter-name>
        <filter-class>org.springframework.web.filter.HiddenHttpMethodFilterfilter-class>
    filter>
    <filter-mapping>
        <filter-name>HiddenHttpMethodFilterfilter-name>
        <url-pattern>/*url-pattern>
    filter-mapping>
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8

    之后表单中加上name="_method" value="put"即可**(需注意method必须为post才行)**

    <form th:action="@{/user}" method="post">
        <input type="hidden" name="_method" value="put">
        <input type="submit" value="更新用户数据">
    form>
    
    • 1
    • 2
    • 3
    • 4

    这样就可以成功获取put参数了

    @RequestMapping(value = "/user",method = RequestMethod.PUT)
    public String updateUser(){
        System.out.println("成功更新用户数据");
        return "success";
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5

    删除操作

    删除操作同理,DELET请求方式

    @RequestMapping(value = "/user/{id}",method = RequestMethod.DELETE)
    public String deleteUser(@PathVariable("id") int id){
        System.out.println("根据id删除信息-->/user/"+id+"-->get");
        return "success";
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5

    HTML

    <form th:action="@{/user/2}" method="post">
        <input type="hidden" name="_method" value="delete">
        <input type="submit" value="删除用户数据">
    form>
    
    • 1
    • 2
    • 3
    • 4

    RESTful案例

    准备工作

    实体类

    package com.sentiment.pojo;
    
    public class Employee {
        private Integer id;
        private String lastName;
        private String email;
        //1 male, 0 female
        private Integer gender;
    
        public Employee() {
        }
    
        public Employee(Integer id, String lastName, String email, Integer gender) {
            this.id = id;
            this.lastName = lastName;
            this.email = email;
            this.gender = gender;
        }
    
        public Integer getId() {
            return id;
        }
    
        public void setId(Integer id) {
            this.id = id;
        }
    
        public String getLastName() {
            return lastName;
        }
    
        public void setLastName(String lastName) {
            this.lastName = lastName;
        }
    
        public String getEmail() {
            return email;
        }
    
        public void setEmail(String email) {
            this.email = email;
        }
    
        public Integer getGender() {
            return gender;
        }
    
        public void setGender(Integer gender) {
            this.gender = gender;
        }
    
        @Override
        public String toString() {
            return "Employee{" +
                    "id=" + id +
                    ", lastName='" + lastName + '\'' +
                    ", email='" + email + '\'' +
                    ", gender=" + gender +
                    '}';
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58
    • 59
    • 60
    • 61

    dao模拟数据

    @Repository
    public class EmployeeDao {
        private static Map<Integer, Employee> employees = null;
        static{
            employees = new HashMap<Integer, Employee>();
            employees.put(1001, new Employee(1001, "E-AA", "aa@163.com", 1));
            employees.put(1002, new Employee(1002, "E-BB", "bb@163.com", 1));
            employees.put(1003, new Employee(1003, "E-CC", "cc@163.com", 0));
            employees.put(1004, new Employee(1004, "E-DD", "dd@163.com", 0));
            employees.put(1005, new Employee(1005, "E-EE", "ee@163.com", 1));
        }
        private static Integer initId = 1006;
        public void save(Employee employee){
            if(employee.getId() == null){
                employee.setId(initId++);
            }
            employees.put(employee.getId(), employee);
        }
        public Collection<Employee> getAll(){
            return employees.values();
        }
        public Employee get(Integer id){
            return employees.get(id);
        }
        public void delete(Integer id){
            employees.remove(id);
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28

    功能清单

    功能URL地址请求方式
    访问首页/GET
    查询全部数据/employeeGET
    删除/employee/2DELETE
    跳转到添加数据页面/toAddGET
    执行保存/employeePOST
    跳转到更新数据页面/employee/2GET
    执行更新/employeePUT

    处理静态资源

    控制层

    package com.sentiment.controller;
    
    import com.sentiment.dao.EmployeeDao;
    import com.sentiment.pojo.Employee;
    import org.springframework.beans.factory.annotation.Autowired;
    import org.springframework.stereotype.Controller;
    import org.springframework.ui.Model;
    import org.springframework.web.bind.annotation.RequestMapping;
    import org.springframework.web.bind.annotation.RequestMethod;
    
    import java.util.Collection;
    
    @Controller
    public class EmployeeController {
        @Autowired
        private EmployeeDao employeeDao;
    
        @RequestMapping(value = "/employee",method = RequestMethod.GET)
        public String getAllEmployee(Model model){
            //获取所有的员工信息
            Collection<Employee> allEmployee = employeeDao.getAll();
            //将所有的员工信息在请求域中共享
            model.addAttribute("allEmployee",allEmployee);
            //跳转到列表页面
            return "employee_list";
        }
    
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28

    employee_list.html

    DOCTYPE html>
    <html lang="en" xmlns:th="http://www.thymeleaf.org">
    <head>
        <meta charset="UTF-8">
        <title>employee listtitle>
        <link rel="stylesheet" th:href="@{/static/css/index_work.css}">
    head>
    <body>
    <table>
        <tr>
            <th colspan="5">employee listth>
        tr>
        <tr>
            <th>idth>
            <th>lastNameth>
            <th>emailth>
            <th>genderth>
            <th>optionsth>
        tr>
        <tr th:each="employee : ${allEmployee}">
            <td th:text="${employee.id}">td>
            <td th:text="${employee.lastName}">td>
            <td th:text="${employee.email}">td>
            <td th:text="${employee.gender}">td>
            <td>
                <a href="">deletea>
                <a href="">updatea>
            td>
        tr>
    table>
    body>
    html>
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32

    css

    @charset "UTF-8";
    
    form {
       margin: 0px;
    }
    
    img {
       border: medium none;
       margin: 0;
       padding: 0;
    } /* img elements 图片元素 */
    /** 设置默认字体 **/
    body,button,input,select,textarea {
       font-size: 12px;
       font: 12px/1.5 ’宋体’, Arial, tahoma, Srial, helvetica, sans-serif;
    }
    
    h1,h2,h3,h4,h5,h6 {
       font-size: 100%;
    }
    
    em {
       font-style: normal;
    }
    /** 重置列表元素 **/
    ul,ol {
       list-style: none;
    }
    /** 重置超链接元素 **/
    a {
       text-decoration: none;
       color: #4f4f4f;
    }
    
    a:hover {
       text-decoration: underline;
       color: #F40;
    }
    /** 重置图片元素 **/
    img {
       border: 0px;
       margin-bottom: -7px;
    }
    
    body {
       width: 80%;
       margin: 40px auto;
       font-family: 'trebuchet MS', 'Lucida sans', Arial;
       font-size: 14px;
       color: #444;
       background: url(../css/img/body1.jpg);
       background-repeat: no-repeat;
       background-size: 100% auto;
       /* background: #F5F5F5; */
    }
    
    table {
       border: solid #ccc 1px;
       -webkit-border-radius: 6px;
       border-radius: 6px;
       /* -webkit-box-shadow: 0 1px 1px #ccc;
       box-shadow: 0 1px 1px #ccc; */
       -webkit-box-shadow:  0px 2px 1px 5px rgba(242, 242, 242, 0.1);
        box-shadow:  5px 20px 30px 30px rgba(242, 242, 242, 0.1);
       width: 100%;
    }
    
    table thead th {
       background:url(../css/img/zebratable.png);
       background-repeat:no-repeat;
       background-position: 0px center;
    }
    
    table tr {
       background: #D5EAF0;
       -webkit-box-shadow: 0 1px 0 rgba(255, 255, 255, .8) inset;
       box-shadow: 0 1px 0 rgba(255, 255, 255, .8) inset;
    }
    
    table tr:nth-child(even) {
       background: #D7E1C5;
       -webkit-box-shadow: 0 1px 0 rgba(255, 255, 255, .8) inset;
       box-shadow: 0 1px 0 rgba(255, 255, 255, .8) inset;
    }
    
    table tr:hover {
       background: #91C5D4;
       -o-transition: all 0.1s ease-in-out;
       -webkit-transition: all 0.1s ease-in-out;
       -ms-transition: all 0.1s ease-in-out;
       transition: all 3s ease-in-out;
       
       background-image: -webkit-gradient(linear, left top, left bottom, from(#151515), to(#404040)) !important;
        background-image: -webkit-linear-gradient(top, #151515, #404040) !important;
        background-image:    -moz-linear-gradient(top, #151515, #404040) !important;
        background-image:     -ms-linear-gradient(top, #151515, #404040) !important;
        background-image:      -o-linear-gradient(top, #151515, #404040) !important;
        background-image:         linear-gradient(top, #151515, #404040) !important;
       color:#fff !important;
    }
    
    table td,table th {
       border-left: 1px solid #ccc;
       border-top: 1px solid #ccc;
       padding: 10px;
       text-align: center;
    }
    
    table th {
       background-color: #66a9bd;
       background-image: -moz-linear-gradient(top, #dce9f9, #66a9bd);
       -webkit-box-shadow: 0 1px 0 rgba(255, 255, 255, .8) inset;
       box-shadow: 0 1px 0 rgba(255, 255, 255, .8) inset;
       border-top: none;
       text-shadow: 0 1px 0 rgba(255, 255, 255, .5);
    }
    
    table td:first-child,table th:first-child {
       border-left: none;
    }
    
    table th:first-child {
       -webkit-border-radius: 6px 0 0 0;
       border-radius: 6px 0 0 0;
    }
    
    table th:last-child {
       -webkit-border-radius: 0 6px 0 0;
       border-radius: 0 6px 0 0;
    }
    
    table th:only-child {
       -webkit-border-radius: 6px 6px 0 0;
       border-radius: 6px 6px 0 0;
    }
    
    table tr:last-child td:first-child {
       -webkit-border-radius: 0 0 0 6px;
       border-radius: 0 0 0 6px;
    }
    
    table tr:last-child td:last-child {
       -webkit-border-radius: 0 0 6px 0;
       border-radius: 0 0 6px 0;
    }
    
    input[type="button"],input[type="submit"],input[type="reset"] {
       border: solid #ccc 1px;
       -webkit-border-radius: 6px;
       border-radius: 6px;
       -webkit-box-shadow: 0 1px 1px #ccc;
       box-shadow: 0 1px 1px #ccc;
       background: #B0CC7F;
       margin: 0 2px 0;
    }
    
    input[type="text"],input[type="password"] {
       border: solid #ccc 2px;
       -webkit-border-radius: 6px;
       border-radius: 6px;
       -webkit-box-shadow: 0 1px 1px #ccc;
       box-shadow: 0 1px 1px #ccc;
       background: #efefef;
       margin: 0 2px 0;
       text-indent: 5px;
    }
    select {
       width:200px;
       border: solid #ccc 2px;
       -webkit-border-radius: 6px;
       border-radius: 6px;
       -webkit-box-shadow: 0 1px 1px #ccc;
       background: #efefef;
       margin: 0 2px 0;
       text-indent: 5px;
    }
    option {
       width:180px;
       border: solid #ccc 2px;
       -webkit-border-radius: 6px;
       border-radius: 6px;
       -webkit-box-shadow: 0 1px 1px #ccc;
       background: #efefef;
       margin: 0 2px 0;
       text-indent: 5px;
    }
    
    input[name="page.now"] {
       border: solid #ccc 1px;
       -webkit-border-radius: 6px;
       border-radius: 6px;
       -webkit-box-shadow: 0 1px 1px #ccc;
       box-shadow: 0px 0px 0px #CEB754;
       background: #D5EAF0;
       margin: 0px 10px 0px 0px;
       padding-bottom: 0px;
       padding-top: 5px; 
       width: 24px; 
       line-height:10px; 
       height: 12xp; 
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58
    • 59
    • 60
    • 61
    • 62
    • 63
    • 64
    • 65
    • 66
    • 67
    • 68
    • 69
    • 70
    • 71
    • 72
    • 73
    • 74
    • 75
    • 76
    • 77
    • 78
    • 79
    • 80
    • 81
    • 82
    • 83
    • 84
    • 85
    • 86
    • 87
    • 88
    • 89
    • 90
    • 91
    • 92
    • 93
    • 94
    • 95
    • 96
    • 97
    • 98
    • 99
    • 100
    • 101
    • 102
    • 103
    • 104
    • 105
    • 106
    • 107
    • 108
    • 109
    • 110
    • 111
    • 112
    • 113
    • 114
    • 115
    • 116
    • 117
    • 118
    • 119
    • 120
    • 121
    • 122
    • 123
    • 124
    • 125
    • 126
    • 127
    • 128
    • 129
    • 130
    • 131
    • 132
    • 133
    • 134
    • 135
    • 136
    • 137
    • 138
    • 139
    • 140
    • 141
    • 142
    • 143
    • 144
    • 145
    • 146
    • 147
    • 148
    • 149
    • 150
    • 151
    • 152
    • 153
    • 154
    • 155
    • 156
    • 157
    • 158
    • 159
    • 160
    • 161
    • 162
    • 163
    • 164
    • 165
    • 166
    • 167
    • 168
    • 169
    • 170
    • 171
    • 172
    • 173
    • 174
    • 175
    • 176
    • 177
    • 178
    • 179
    • 180
    • 181
    • 182
    • 183
    • 184
    • 185
    • 186
    • 187
    • 188
    • 189
    • 190
    • 191
    • 192
    • 193
    • 194
    • 195
    • 196
    • 197
    • 198
    • 199
    • 200
    • 201

    此时当访问employee时,会发现我们调用的css样式并没有被渲染,这是因为:

    当前工程的web.xml配置的前端控制器DispatcherServlet的url-pattern是 /,而 tomcat的web.xml配置的DefaultServlet的url-pattern也是 /,此时浏览器发送的请求会优先被DispatcherServlet进行处理,但是DispatcherServlet无法处理静态资源

    所以为了解决这个问题引入了一个配置标签:

    ,此时浏览器发送的所有请求都会被DefaultServlet处理,但是我们处理用的并不是DefaultServlet,而是DispatcherServlet所以还需要加上

    此时浏览器发送的请求就会先被DispatcherServlet处理
    在这里插入图片描述

    添加功能

    在options后边添加一个add按钮

    <th>options(<a th:href="@{/to/add}">adda>)th>
    
    • 1

    跳转到,/to/add,所以写一个add界面,employee_add.html

    DOCTYPE html>
    <html lang="en" xmlns:th="http://www.thymeleaf.org">
    <head>
        <meta charset="UTF-8">
        <title>add employeetitle>
        <link rel="stylesheet" th:href="@{/static/css/index_work.css}">
    head>
    <body>
        <form th:action="@{/employee}" method="post">
            <table>
                <tr>
                    <td colspan="2">add employeetd>
                tr>
                <tr>
                    <td>lastNametd>
                    <td><input type="text" name="lastName">td>
                tr>
                <tr>
                    <td>emailtd>
                    <td><input type="text" name="email">td>
                tr>
                <tr>
                    <td>gendertd>
                    <td>
                        <input type="radio" name="gender" value="1">male
                        <input type="radio" name="gender" value="0">female
                    td>
                tr>
                <tr>
                    <td colspan="2"><input type="submit" value="add">td>
                tr>
            table>
        form>
    body>
    html>
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35

    当点击添加之后,通过post方式跳转到/employee,并通过DAO将数据保存,之后重定向到GET请求的/employee回到首页,发现添加成功

    @RequestMapping(value = "/employee",method = RequestMethod.POST)
    public String addEmployee(Employee employee){
        employeeDao.save(employee);
        return "redirect:/employee";
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5

    在这里插入图片描述

    修改功能

    记录一下流程:

    1、为update标签添加链接

    <a th:href="@{'/employee/'+${employee.id}}">updatea>
    
    • 1

    2、访问指定路径,并转到update界面

    @RequestMapping(value = "/employee/{id}",method = RequestMethod.GET)
    public String toUpdate(@PathVariable("id") int id,Model model){
        Employee employee = employeeDao.get(id);
        model.addAttribute("employee",employee);
        return "employee_update";
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6

    3、注册界面employee_update.html

    DOCTYPE html>
    <html lang="en" xmlns:th="http://www.thymeleaf.org">
    <head>
        <meta charset="UTF-8">
        <title>update employeetitle>
        <link rel="stylesheet" th:href="@{/static/css/index_work.css}">
    head>
    <body>
        <form th:action="@{/employee}" method="post">
            <input type="hidden" name="_method" value="put">
            <input type="hidden" name="id" th:value="${employee.id}">
            <table>
                <tr>
                    <td colspan="2">update employeetd>
                tr>
                <tr>
                    <td>lastNametd>
                    <td><input type="text" name="lastName" th:value="${employee.lastName}">td>
                tr>
                <tr>
                    <td>emailtd>
                    <td><input type="text" name="email" th:value="${employee.email}">td>
                tr>
                <tr>
                    <td>gendertd>
                    <td>
                        <input type="radio" name="gender" value="1" th:field="${employee.gender}">male
                        <input type="radio" name="gender" value="0" th:field="${employee.gender}">female
                    td>
                tr>
                <tr>
                    <td colspan="2"><input type="submit" value="update">td>
                tr>
            table>
        form>
    body>
    html>
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37

    4、提交表单用的是put方式,所以再写个接受put请求的方法

    @RequestMapping(value = "/employee",method = RequestMethod.PUT)
    public String updateEmployee(Employee employee){
        employeeDao.save(employee);
        return "redirect:/employee";
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5

    5、修改后结果:

    在这里插入图片描述

    @RestController注解

    标识在控制器上,就相当于添加了@Controller,并且给每个方法添加了@ResponseBody注解

    文件上传下载

    文件下载

    @RequestMapping("/test/down")
    public ResponseEntity<byte[]> testResponseEntity(HttpSession session) throws IOException {
        //获取ServletContext对象
        ServletContext servletContext = session.getServletContext();
        //获取服务器中文件的真实路径
        String realPath = servletContext.getRealPath("img");
        realPath = realPath + File.separator+"1.jpg";
        //创建输入流
        InputStream is = new FileInputStream(realPath);
        //创建字节数组
        //is.available() is字节流对应的所有字节数
        byte[] bytes = new byte[is.available()];
        //将流读到字节数组中
        is.read(bytes);
        //创建HttpHeaders对象设置响应头信息
        MultiValueMap<String, String> headers = new HttpHeaders();
        //设置要下载方式以及下载文件的名字
        headers.add("Content-Disposition", "attachment;filename=Sentiment.jpg");
        //设置响应状态码
        HttpStatus statusCode = HttpStatus.OK;
        //创建ResponseEntity对象
        ResponseEntity<byte[]> responseEntity = new ResponseEntity<byte[]>(bytes, headers, statusCode);
        //关闭输入流
        is.close();
        return responseEntity;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26

    文件上传

    需要用到commons-fileupload

    <dependency>
        <groupId>commons-fileuploadgroupId>
        <artifactId>commons-fileuploadartifactId>
        <version>1.3.1version>
    dependency>
    
    • 1
    • 2
    • 3
    • 4
    • 5

    上传页面 (enctype 属性:表示将数据回发到服务器时浏览器使用的编码类型)

    <form th:action="@{/test/upload}" enctype="multipart/form-data" method="post">
        图片:<input type="file" name="photo" ><br>
        <input type="submit" value="上传">
    form>
    
    • 1
    • 2
    • 3
    • 4

    文件上传

    @RequestMapping("/test/upload")
    public String testUpload(MultipartFile photo, HttpSession session) throws IOException {
        //获取上传的文件的文件名
        String fileName = photo.getOriginalFilename();
        //获取ServletContext对象
        ServletContext servletContext = session.getServletContext();
        //获取当前工程下photo目录的真实路径
        String photoPath = servletContext.getRealPath("photo");
        //创建photoPath所对应的File对象
        File file = new File(photoPath);
        //判断file所对应目录是否存在
        if(!file.exists()){
            file.mkdir();
        }
        String finalPath = photoPath + File.separator + fileName;
        //上传文件
        photo.transferTo(new File(finalPath));
        return "success";
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19

    上传后发现空指针,因为形参MultipartFile photo获取不到,这是就需要在SpringMVC.xml设置文件上传解析器 (由于这种bean管理方式不是基于类型的,所以需要加上id)

    
    <bean id="multipartResolver" class="org.springframework.web.multipart.commons.CommonsMultipartResolver">
    
    bean>
    
    • 1
    • 2
    • 3
    • 4

    这是上传文件后,就会上传到photo目录中
    在这里插入图片描述

    文件重命名问题

    如果此时再上传一个1.jpg,那么新上传的图片的二进制数据就会替换到原来的图片数据,呈现出将图片替换了的现象

    此时就可以通过命名规范的方式解决此问题,一般可以用uuid时间戳命名解决:

    //获取文件后缀
    String hzName = fileName.substring(fileName.lastIndexOf("."));
    //通过uuid生成文件名
    String uuid = UUID.randomUUID().toString();
    //通过uuid和后缀拼接一个新文件
    fileName=uuid+hzName;
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6

    拦截器

    拦截器用于拦截控制器方法的执行

    拦截器需要实现HandlerInterceptor

    拦截器必须在SpingMVC的配置文件中进行配置

    拦截器的配置

    创建拦截器,注:preHandle是boolean类型的,他的返回值代表是否拦截,false代表丽拦截

    @Component("firstInterceptor")
    public class FirstInterceptor implements HandlerInterceptor {
        @Override
        public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
            System.out.println("FirstInterceptor -> preHandle");
            return true;
        }
    
        @Override
        public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception {
            System.out.println("FirstInterceptor -> postHandle");
        }
    
        @Override
        public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
            System.out.println("FirstInterceptor -> afterCompletion");
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18

    配置文件

    有三种方式配置拦截器:

    
    <mvc:interceptors>
        
        
        
        
        
        
        
        <mvc:interceptor>
            
            <mvc:mapping path="/**"/>
            
            <mvc:exclude-mapping path="/abc"/>
            
            <ref bean="firstInterceptor"/>
        mvc:interceptor>
    mvc:interceptors>
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18

    执行顺序

    preHandle()

    在控制器方法执行之前执行,起返回值表示对控制器方法的拦截(false)和放行(true)

    postHandle()

    在控制器方法执行之后执行

    afterCompletion()

    在控制器方法执行之后,且渲染视图完毕之后执行

    多个拦截器

    当存在多个拦截器时,拦截器的执行顺序和SpringMVC配置文件中的配置顺序有关

    再写个SecondInterceptor,看下执行顺序:

    perHandle()按配置顺序执行,postHandle()和afterCompletion()按配置反序执行
    在这里插入图片描述

    为什么postHandle和afterCompletion是逆序执行的?

    有三个拦截器:

    • conversion(系统自带)
    • first
    • second

    preHandle

    这里经过三轮遍历,interceptorIndex的值为2,最后retrun true
    在这里插入图片描述

    postHandle

    刚刚返回true后,向下继续执行postHandle,而这里for循环,是通过自减的方式执行的,所以下边的interceptor.postHandle(request, response, this.handler, mv);首先调用的就是索引为2的interceptor,所以先调用了SecondInterceptor的postHandle方法,接着经过自减后i=1,调用FirstInterceptor的postHandle方法

    所以这也就是逆序执行的原因
    在这里插入图片描述

    afterCompletion

    与postHandle同理

  • 相关阅读:
    HCIE云计算
    C++ Reference: Standard C++ Library reference: Containers: list: list: cbegin
    使用 MySQL 日志 | 二进制日志 - Part 2
    Docker 官方镜像Tomcat 无法访问 解决方案
    ESP8266-Arduino编程实例-GA1A12S202对数刻度模拟光传感器
    Java-钉钉订阅事件
    wget 命令的使用:HTTP文件下载、FTP文件下载--九五小庞
    驱动点云格式修改带来的效率提升
    Node.js项目(一)
    【Swift 60秒】04 - Doubles and booleans
  • 原文地址:https://blog.csdn.net/weixin_54902210/article/details/127408331