SSM Chapter 10 使用Spring MVC 框架改造超市订单系统-1 笔记

本章目标

  • 掌握并理解单例模式
  • 搭建Spring MVC+Spring+JDBC框架,并掌握在此框架上进行项目开发
  • 掌握Spring JDBC的配置与使用
  • 掌握SpringMVC访问静态文件
  • 掌握ServletAPI作为入参
  • 掌握Spring MVC异常(局部、全局)处理

1. 单例模式

1.1 单例模式简介

假如我们在一个项目中,需要读取一下配置文件,暂定这个类叫ConfigManager.java,那么如果不是单例模式的,则需要每次都去new一个此类的实例。

而这些都是固定的配置参数,对于每一个线程都是一样的,大家完全可以共享一个实例,并且读取配置文件属于I/O操作,要知道I/O操作本来就是很昂贵的,如果每次调用都去读取一次,会严重影响系统的性能。

那么可不可以将这个类只实例化一次,而且读取配置文件的操作只执行一次,然后就拿到内存中供所有人使用呢?

单例模式就是用来解决这种问题的。它是23中设计模式之一,也是最常用的一种设计模式。顾名思义,单例模式就是在系统运行期间,有且仅有一个实例。它有三个必须满足的条件:

  • 一个类只有一个实例;
  • 自身创建自身的实例;
  • 自行向整个系统提供自身实例。

这三个条件,到底怎么满足呢?接下来我们以一个例子,来学习单例模式。

1.2 单例模式的简单实现

假设现在我们要读取数据库配置文件——data.properties。

首先在resources目录下新建data.properties文件:

  1. driver=com.mysql.jdbc.Driver
  2. #在和mysql传递数据的过程中,使用unicode编码格式,并且字符集设置为utf-8
  3. url=jdbc:mysql://127.0.0.1:3306/foo?useUnicode=true&characterEncoding=utf-8
  4. username=root
  5. password=root

然后创建ConfigManager.java文件:

/**
 *  单例模式
 *  负责读取数据库连接信息
 * */
public class ConfigManager {
    private static ConfigManager configManager;  
    private static Properties properties;

    /**
     *  私有构造函数,执行在整个程序运行期间只需要进行一次的操作
     * */
    private ConfigManager(){
        properties=new Properties();
        InputStream inputStream=ConfigManager.class.getClassLoader().getResourceAsStream("database.properties");
        try {
            properties.load(inputStream);
            inputStream.close();
        } catch (IOException e) {
            e.printStackTrace();
        }
    }


    /**
     *  对外公开的方法
     * */
    public static ConfigManager getInstance(){
        if (configManager==null)
            configManager=new ConfigManager();  //自身创建自身的实例
        return configManager;
    }


    /**
     *  返回读取到的值
     * */
    public String getValue(String key){
        return properties.getProperty(key);
    }
}

若是IDEA 的maven项目,且配置文件在java目录下, 在pom.xml文件下 加入以下代码:

<resources>
    <resource>
        <directory>src/main/java</directory>
        <includes>
            <include>**/*.properties</include>
            <include>**/*.xml</include>
        </includes>
        <filtering>false</filtering>
    </resource>
</resources>

最后创建测试类:

public class TestConfigManager {
    @Test
    public void doTest(){
        ConfigManager configManager = ConfigManager.getInstance();
        System.out.println(configManager.getValue("username"));
    }
}

运行结果会输出字符串“root”。接下来说说我们是怎样满足那三个条件的。

  • 1、一个类只有一个实例。这是满足单例模式最基本的要求,若要满足这一点,我们只能提供私有的构造函数,保证不能随意创建该类的实例。并且我们在构造函数中执行了读取配置文件的操作,这样就保证了I/O操作只执行一次。
  • 2、自身创建自身的实例。对于这一点,正是体现了“有且仅有一个实例”的特性。我们要保证唯一性,也就意味着必须提供一个实例并由它自身创建,所以我们定义了一个ConfigManager类型的静态变量,以便向外界提供实例时使用。
  • 3、自行向整个系统提供自身实例。这一点是至关重要的,因为我们设置了ConfigManager类的构造器是私有的,所以外界是无法通过new操作符去获得它的实例的。那么就必须提供一个静态公有方法,该方法创建自身的实例并返回。所以我们定义了一个全局访问点getInstance()方法,该方法返回该类的实例,并做了逻辑判断,如果不为null则直接返回,不需要再创建实例。

到这一步,我们已经可以获取ConfigManager类的实例了,并调用它的getValue方法,获取从配置文件读到的值。

以上就是最简单的单例模式实现,但是在并发环境下,它有很严重的弊端。比如线程不安全,可能会出现多个ConfigManager的实例,所以在实际开发中不会采用这种单例模式的实现。那么如何解决这个问题呢?

这就需要再学习单例模式的两种实现——懒汉模式和饿汉模式。

1.3 懒汉模式

所谓懒汉模式,正如其名,比较懒,在类加载的时候并不创建自身实例,采用延迟加载的方式,只有在运行时调用了相关的方法才会被动创建自身的实例。

上述的示例我们就采用了懒汉模式,只有我们调用全局访问点getInstance()方法的时候,这个类的实例才被创建。当然,在这种情况下虽然保证了其延迟加载的特性,但是存在线程安全问题.

使用main方法测试并发情况下,懒汉模式的线程安全问题,代码如下:

public static void main(String[] args) {
    //测试并发情况下懒汉单例模式的线程安全问题
    for (int i = 0; i < 10; i++) {
        new Thread(() -> {
            //得到该对象的hashCode值 比较是否是同一个实例      
          System.out.println(ConfigManager.getInstance().hashCode());
        }).start();
    }
}

控制台输出如下:

1535148795 2043165005 2043165005 1064850353 1540356119 1540356119 782973738 782973738 782973738 782973738

从输出结果分析,很明显 , 在多线程下无法正常工作,这也是致命的缺陷。

现在对上面的示例进行修改,只修改getInstance()方法即可,因为最简单的方法就是考虑同步,这里采用synchronized实现。

/**
*  对外公开的方法,延迟加载
* */
public static synchronized ConfigManager getInstance(){
    if (configManager==null)
        configManager=new ConfigManager();
    return configManager;
}

以上写法能够在多线程并发环境中很好地工作,并且看起来它也具备了延迟加载的特性。但是很遗憾,这种处理方式效率不高,可以说95%以上的情况都不需要同步。

那么对于线程安全问题,还有另一种解决方式,即饿汉模式。

1.4 饿汉模式

饿汉模式在类加载的时候就已经完成了初始化操作,所以类加载较慢,但是获取对象的速度很快。由于饿汉模式在类初始化时就完成了实例化,所以它是不存在线程安全问题的。修改以上的代码:

/**
 *  单例模式
 *  负责读取数据库连接信息
 * */
public class ConfigManager {
    private static ConfigManager configManager=new ConfigManager();  //加载时即实例化
    private static Properties properties;

    /**
     *  私有构造函数,执行在整个程序运行期间只需要进行一次的操作
     * */
    private ConfigManager(){
        properties=new Properties();
        InputStream inputStream=ConfigManager.class.getClassLoader().getResourceAsStream("data.properties");
        try {
            properties.load(inputStream);
            inputStream.close();
        } catch (IOException e) {
            e.printStackTrace();
        }
    }


    /**
     *  对外公开的方法,延迟加载
     * */
    public static ConfigManager getInstance(){
        return configManager;
    }


    /**
     *  返回读取到的值
     * */
    public static String getValue(String key){
        return properties.getProperty(key);
    }
}

上述代码中修改了getInstance()方法,直接返回configManager,而此实例在类加载时就已经自行实例化了。这种方式基于classloader机制,有效避免了多线程的同步问题。

但是,由于导致类加载的原因比较多,而此时单例类ConfigManager在类加载时就实例化,显然没有达到延迟加载的效果。

现在可以对比一下两种方式:

懒汉模式,在类加载时不创建实例,因此类加载速度快,但是运行时获取对象的速度较慢,具备延迟加载的特性,但是又存在线程不安全的问题。

饿汉模式在类加载时就完成初始化,所以类加载较慢,但是获取对象的速度很快。
懒汉模式是“时间换空间”,饿汉模式是“空间换时间”,因为一开始就创建了实例,所以每次使用时直接返回该实例就好了。

在实际开发场景中,实例化单例类很消耗资源,我们希望它可以延迟加载,显然饿汉模式并不能实现。那么我们应该怎么处理呢?

要想让饿汉模式同时具备延迟加载的特性,可以搭配静态内部类进行改造实现。

1.5 单例模式的实现:饿汉模式+静态内部类

直接上代码:

/**
 *  单例模式
 *  负责读取数据库连接信息
 * */
public class ConfigManager {
    private static ConfigManager configManager;
    private static Properties properties;

    /**
     *  私有构造函数,执行在整个程序运行期间只需要进行一次的操作
     * */
    private ConfigManager(){
        properties=new Properties();
        InputStream inputStream=ConfigManager.class.getClassLoader().getResourceAsStream("data.properties");
        try {
            properties.load(inputStream);
            inputStream.close();
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

    /**
     *  静态内部类
     * */
    public static class ConfigManagerHelp{
        private static final ConfigManager INSTANCE=new ConfigManager();  //静态常量
    }

    /**
     *  对外公开的方法,装载静态内部类,延迟加载
     * */
    public static ConfigManager getInstance(){
        configManager = ConfigManagerHelp.INSTANCE;
        return configManager;
    }

    /**
     *  返回读取到的值
     * */
    public static String getValue(String key){
        return properties.getProperty(key);
    }
}

以上代码同样利用了classloader机制来保证初始化INSTANCE时只有一个线程,但是与之前的方式有点不同。

按照之前的方式,只要ConfigManger类被装载了,那么configManger属性就会被实例化,并没有达到延迟加载的效果。

而现在采用静态内部类的方式进行改造,在ConfigManger类被装载时不一定进行实例化,因为ConfigManagerHelp类没有被主动调用。只有通过调用了getInstance()方法,才会装载ConfigManagerHelp类,从而进行实例化。而且INSTANCE是静态常量,一旦创建了实例,内存地址是不会变的,在getInstance()方法只需要进行引用即可。

这种方式比懒汉模式、饿汉模式都要好,既实现了线程安全,又实现了延迟加载。

将此方式引用foo超市订单管理系统中,修改BaseDao的代码,将读取数据库连接部分的代码修改如下:

/**
 * 操作数据库的基类--静态类
 *
 * @author Administrator
 */
public class BaseDao {
    /**
     * 获取数据库连接
     *
     * @return
     */
    public static Connection getConnection() {
        Connection connection = null;
        String  driver = ConfigManager.getInstance().getProperty("driver");
        String  url = ConfigManager.getInstance().getProperty("url");
        String  user = ConfigManager.getInstance().getProperty("user");
        String  password = ConfigManager.getInstance().getProperty("password");
        try {
            Class.forName(driver);
            connection = DriverManager.getConnection(url, user, password);
        } catch (Exception e) {
            e.printStackTrace();
        }
        return connection;
    }
    //省略其他代码.....

}

总结:

对于单例模式的使用,不管是懒汉模式,饿汉模式,或者静态内部类的方式,根据具体的业务需求而定。反正要遵守一个原则:

在整个程序运行期间,有且仅有一个实例。

若违背这一点,那么即使设计的天花乱坠,也不是单例模式的实现。

1.6 Spring MVC - Controller 的 单例管理

SpringMVC 的Controller类默认是单例的,这样设计的原因是基于性能考虑的,因为Controller 设计为单例模式,不需要每次都创建实例,速度和性能自然很优越.

基于SpringMVC中的Controller的这一特点,我们在使用的时候也千万要注意,在默认单例情况下,一般不要在Controller中定义除了接口类型,常量以外的成员变量,若出现这样的语法,将会导致严重的线程安全以及资源使用问题,下面通过一个示例演示 说明该问题.

首先改造 POJO(User.java),增加有参构造方法,关键示例代码如下:

public class User {
    private Integer id; //id
    private String userCode; //用户编码
    private String userName; //用户名称
    private String userPassword; //用户密码
    private Integer gender;  //性别
    private Date birthday;  //出生日期
    private String phone;   //电话
    private String address; //地址
    private Integer userRole;    //用户角色
    private Integer createdBy;   //创建者
    private Date creationDate; //创建时间
    private Integer modifyBy;     //更新者
    private Date modifyDate;   //更新时间

    private Integer age;//年龄

    private String userRoleName;    //用户角色名称

    public User(){}

    public User(Integer id,
                String userCode,
                String userName,
                String userPassword,
                Integer gender,
                Date birthday,
                String phone,
                String address,
                Integer userRole,
                Integer createdBy,
                Date creationDate, Integer modifyBy, Date modifyDate) {
        this.id = id;
        this.userCode = userCode;
        this.userName = userName;
        this.userPassword = userPassword;
        this.gender = gender;
        this.birthday = birthday;
        this.phone = phone;
        this.address = address;
        this.userRole = userRole;
        this.createdBy = createdBy;
        this.creationDate = creationDate;
        this.modifyBy = modifyBy;
        this.modifyDate = modifyDate;
    }
    //省略getter setter 以及 toString
    //注意age只需提供getter方法: 方法如下:
    public Integer getAge() {
        //Date date = new Date();
        //方法已过时
        //Integer age = date.getYear()-birthday.getYear();
        //使用默认时区获取当前的日历
        Calendar now = Calendar.getInstance();
        Calendar birthday = Calendar.getInstance();
        //设置日历为生日的日期
        birthday.setTime(this.birthday);
        //获取现在的年份-生日的年份
        return now.get(Calendar.YEAR) - birthday.get(Calendar.YEAR);
    }
}

然后在UserController里面增加一个 userList(ArrayList<User>)的成员变量,并添加查询方法list(),执行查询全部用户的操作,示例代码如下:

@Controller
@RequestMapping("/user")
public class UserController {
    private Logger logger = Logger.getLogger(this.getClass());
    private List<User> userList = new ArrayList<>();

    public UserController(){
        try {//初始化用户数据
            userList.add(new User(1, "test001","测试用户001", "1111111", 1,
                    new SimpleDateFormat("yyyy-MM-dd").parse("1986-12-10"), 
                    "13566669998", "北京市朝阳区北苑", 1, 1, new Date(), 1,
                    new Date()));
            userList.add(new User(2, "zhaoyan","赵燕", "2222222", 1,
                    new SimpleDateFormat("yyyy-MM-dd").parse("1984-11-10"), 
                    "18678786545", "北京市海淀区成府路", 1, 1,new Date(), 1,
                    new Date()));
            userList.add(new User(3, "test003","测试用户003", "3333333", 1,
                    new SimpleDateFormat("yyyy-MM-dd").parse("1980-11-11"), 
                    "13121334531", "北京市通州北苑", 1,1, new Date(), 1, new Date()));
            userList.add(new User(4, "wanglin","王林", "4444444", 1,
                    new SimpleDateFormat("yyyy-MM-dd").parse("1989-09-10"), 
                    "18965652364", "北京市学院路",1, 1, new Date(),1, new Date()));
            userList.add(new User(5, "zhaojing","赵静", "5555555", 1, new 
                    SimpleDateFormat("yyyy-MM-dd").parse("1981-08-01"),
                    "13054784445","北京市广安门",1, 1,new Date(), 1, new Date()));
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
    //没有查询条件下,获取userList(公共查询)
    @RequestMapping(value="list",method = RequestMethod.GET)
    public String list(Model model){
        logger.info("无查询条件下,获取userList(公共查询)===="+userList);
        model.addAttribute("userList",userList);
        return "user/userlist";
    }
}

在上述代码中,由于没有查询条件,即公共查询,输入url请求: http://localhost:8080/项目名/user/list,可以获取全部用户数据,每个用户看到的数据结果都是一样的.接下来编写 WEB-INF/jsp/user/userlist.jsp 页面, 页面代码如下:

<%@ page contentType="text/html;charset=UTF-8" language="java" %>
  <%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %>
    <html>
      <head>
        <title>用户查询页面</title>
      </head>
      <body>
        <h1>查询用户列表</h1>
        <c:forEach items="${userList}" var="user">
          <div>
            id:${user.id} --
            用户编码:${user.userCode} --
            用户名称:${user.userName} --
            用户密码:${user.userPassword} --
            用户生日:${user.birthday} --
            用户地址:${user.address} ---
          </div>
        </c:forEach>
      </body>
</html>

在上述代码中,使用 JSTL 标签 和 EL 表达式 来 完成页面的展示,结果如图:

01.png

由于这是一个没有查询条件的公共查询,运行结果正常,看似成员变量userList没有什么问题,若增加了查询条件,就会出现问题了.

现在在UserController里增加一个按用户名(userName) 进行模糊查询的方法.示例关键代码如下:

@Controller
@RequestMapping("/user")
public class UserController {
    //省略部分代码
    private List<User> queryUserList = new ArrayList<>();
    //省略相同代码
    //增加查询条件:userName
    @RequestMapping(value="/list",method = RequestMethod.POST)
    public String list(@RequestParam(value="userName",required = false) String userName, Model model{
        logger.info("查询条件:userName:"+userName+"获取userList");
        if (null != userName && !"".equals(userName)) {
            userList.forEach(x->{
                if(x.getUserName().indexOf(userName) != -1){
                    queryUserList.add(x);
                }
            });
            model.addAttribute("userList",queryUserList);
        }else{
            //添加userList 而不要添加根据条件查询的集合
            model.addAttribute("userList",userList);
        }
        return "user/userlist";
    }
}

在上述代码中,list()方法 对应的请求:http://localhost:8080/项目名/user/list, 但是请注意是POST请求,在该方法中进行模糊匹配,最后将匹配成功的用户列表结果放入Model中去. 改造 userlist.jsp页面,代码如下:

<%@ page contentType="text/html;charset=UTF-8" language="java" %>
  <%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %>
    <html>
      <head>
        <title>用户查询页面</title>
      </head>
      <body>
        <h1>查询用户列表</h1>
        <form action="${pageContext.request.contextPath}/user/list" method="post">
          用户名:<input type="text" name="userName" value="">
          <input type="submit" value="查询">
        </form>
        <c:forEach items="${userList}" var="user">
          <div>
            id:${user.id} --
            用户编码:${user.userCode} --
            用户名称:${user.userName} --
            用户密码:${user.userPassword} --
            用户生日:${user.birthday} --
            用户地址:${user.address} ---
          </div>
        </c:forEach>
      </body>
</html>

在上述代码中增加一个form表单,提交请求到 "/user/list",但是method 是 POST请求,以便于之前的GET请求区分,表单内有一个用户名称的输入框 和 submit 提交按钮.

当输入用户名称之后,单击"提交" 按钮,就会以POST 的方式 进入相应的UserController的处理方法(带查询条件的list方法)中,连续多次输入查询条件(userName) 为 “001” 后,运行结果如图:

02.png

很显然,查询出来的数据不正确.出现这种问题的原因是因为Spring MVC 的Controller是单例的,其内部的成员变量是公用的.

当在高并发的环境下,用户A 和 用户B 同时输入不同的查询条件之后,结果就会显示有误,即会出现用户B的CPU抢占了资源,那么用户A 在页面看到的就是用户B查询的结果. 所以绝对不能这样做.

根据业务场景,需要更改Controller的scope 为 prototype, 在@Controller 之前增加注解 @Scope(“prototype”)或者使用@Scope(ConfigurableBeanFactory.SCOPE_PROTOTYPE),就不再是单例模式了,但是这种用法就不上很少用.

SpringMVC框架的这种设计也恰恰体现了其性能速度的优越。

> 小结

1、在默认单例情况下,一般不要在Controller中定义成员变量,若出现这样的用法,那么会导致严重的线程安全以及资源使用问题(这是由于Cotroller默认是单例的,成员变量是公用的,当在高并发的环境下,会出现资源抢占问题)

2、在Controller内,可以声明service成员变量,通过@Resource或者@Autowired方式装载(之所以可以这样做,是由于service是大家公用的,并且它是接口,接口内没有成员变量,都是方法,而方法里的变量也都是线程级的,故不会出现数据资源抢占的问题或者内存溢出。所以一般情况下,Controller内的成员变量就只有service对象。)

注意

输入查询条件时,若输入中文时,会出现乱码,解决方案如下:

在web.xml中增加Spring字符编码过滤器,配置字符编码为UTF-8,关键代码如下:

<filter>
    <filter-name>encodingFilter</filter-name>
    <filter-class>
        org.springframework.web.filter.CharacterEncodingFilter
    </filter-class>
    <!-- encoding用来设置编码格式-->
    <init-param>
        <param-name>encoding</param-name>
        <param-value>UTF-8</param-value>
    </init-param>
    <!-- forceEncoding用来设置是否理会 request.getCharacterEncoding()方法,
设置为true则强制覆盖之前的编码格式-->
    <init-param>
        <param-name>forceEncoding</param-name>
        <param-value>true</param-value>
    </init-param>
</filter>
<filter-mapping>
    <filter-name>encodingFilter</filter-name>
    <url-pattern>/*</url-pattern>
</filter-mapping>

其中:encoding用来设置编码格式,forceEncoding用来设置是否理会 request.getCharacterEncoding()方法,设置为true则强制覆盖之前的编码格式

2. 搭建Spring MVC + Spring + JDBC 框架

基于性能方面的考虑,Spring MVC + Spring + JDBC 框架在一些互联网中使用比率也比较高,故在本章中,在原有素材的基础上,使用Spring MVC框架改造超市订单管理系统的Controller层,DAO层暂时使用JDBC实现.

在上一实例的基础上,实现步骤如下:

1. 加入Spring Spring MVC 数据库驱动等相关jar文件

pom.xml文件添加下列依赖:


<properties>
    <spring.version>5.1.5.RELEASE</spring.version>
    <javax.servlet-api.version>3.1.0</javax.servlet-api.version>
    <jsp-api.version>2.1</jsp-api.version>
    <log4j-web.version>2.11.1</log4j.version>
    <junit.version>4.12</junit.version>
    <jstl.version>1.2</jstl.version>
    <mysql-connector-java.version>5.1.38</mysql-connector-java.version>
</properties>
<!--Spring webmvc-->
<dependency>
    <groupId>org.springframework</groupId>
    <artifactId>spring-webmvc</artifactId>
    <version>${spring.version}</version>
</dependency>
<!--servlet - api-->
<dependency>
    <groupId>javax.servlet</groupId>
    <artifactId>javax.servlet-api</artifactId>
    <version>${javax.servlet-api.version}</version>
    <scope>provided</scope>
</dependency>
<!--jsp - api-->
<dependency>
    <groupId>javax.servlet.jsp</groupId>
    <artifactId>jsp-api</artifactId>
    <version>${jsp-api.version}</version>
    <scope>provided</scope>
</dependency>
<!--log4j-->
<dependency>
    <groupId>org.apache.logging.log4j</groupId>
    <artifactId>log4j-web</artifactId>
    <version>${log4j-web.version}</version>
</dependency>
<!--jstl 标签-->
<dependency>
    <groupId>javax.servlet</groupId>
    <artifactId>jstl</artifactId>
    <version>${jstl.version}</version>
</dependency>
<!--mysql 驱动-->
<dependency>
    <groupId>mysql</groupId>
    <artifactId>mysql-connector-java</artifactId>
    <version>${mysql-connector-java.version}</version>
</dependency>
<build>
    <!--将项目打成war包之后的最终名字-->
    <!--<finalName>ssm_ch09</finalName>-->
</build>

2. Spring 配置文件

在resources目录下 增加 Spring 配置文件(applicationContext-jdbc.xml) , 实例代码如下:

<!--省略命名空间的引入-->
<!-- 扫描指定包下的所有类,让Spring注解的类生效 -->   
<context:component-scan base-package="cn.foo.dao,cn.foo.service"/>

3. log4j2.xml:

log4j2.xml内容如下:

<?xml version="1.0" encoding="UTF-8"?>
<Configuration status="off">
    <!-- 文件路径 -->
    <properties>
        <!--设置日志在硬盘上输出的目录${log4j:configParentLocation}使用此查找将日志文件放在相对于log4j配置文件的目录中-->
        <property name="Log_Home">${web:rootDir}/log</property>
    </properties>
    <Appenders>
        <Console name="Console" target="SYSTEM_OUT">
            <!--输出日志的格式
            %L::输出代码中的行号。
            %M:输出产生日志信息的方法名。-->
            <!--"%highlight{%d{HH:mm:ss.SSS} %-5level %logger{36}.%M() 
            @%L - %msg%n}{FATAL=Bright Red, ERROR=Bright Magenta, 
            WARN=Bright Yellow, INFO=Bright Green, DEBUG=Bright Cyan, 
            TRACE=Bright White}"-->

            <PatternLayout pattern="%d{HH:mm:ss.SSS} %-5level %class{36}.%M @%L :-> %msg%xEx%n"/>
        </Console>
        <!--这个会打印出所有的信息,每次大小超过size,则这size大小的日志会自动存入按年份-月份建立的文件夹下面并进行压缩,作为存档-->
        <RollingFile name="RollingFileInfo" fileName="${Log_Home}/info.${date:yyyy-MM-dd}.log" immediateFlush="true"
                     filePattern="${Log_Home}/$${date:yyyy-MM}/info-%d{MM-dd-yyyy}-%i.log.gz">
            <PatternLayout pattern="%d{yyyy-MM-dd 'at' HH:mm:ss z} %-5level %class{36}.%M @%L :-> %msg%xEx%n"/>
            <!--控制台只输出level及以上级别的信息(onMatch),其他的直接拒绝(onMismatch)-->
            <filters>
                <ThresholdFilter level="error" onMatch="DENY" onMismatch="NEUTRAL"/>
                <ThresholdFilter level="info" onMatch="ACCEPT" onMismatch="DENY"/>
            </filters>
            <Policies>
                <TimeBasedTriggeringPolicy modulate="true" interval="1"/>
                <SizeBasedTriggeringPolicy size="10MB"/>
            </Policies>
        </RollingFile>
    </Appenders>

    <Loggers>

        <!-- 
            限制Spring框架日志的输出级别,其它框架类似配置
            或者使用 AppenderRef 标签,将其输出到指定文件中,记得加上 additivity="false"
        -->
        <logger name="org.springframework.core" level="info"/>
        <logger name="org.springframework.beans" level="info"/>
        <logger name="org.springframework.context" level="info"/>
        <logger name="org.springframework.web" level="info"/>
        <logger name="org.springframework.convert" level="debug" />
        <!--建立一个默认的root的logger-->
        <root level="info">
            <appender-ref ref="Console"/>
            <appender-ref ref="RollingFileInfo" />
        </root>
    </Loggers>
</Configuration>

4. 配置 web.xml

在web.xml指定Spring 配置文件 所在的位置 并配置 ContextLoaderListener

  • (1) 需要在 web.xml中通过 contextConfigLocation 参数,指定 步骤2 创建的 Spring 配置文件(applicationContext-jdbc.xml)的路径
  • (2) 由于Spring 需要启动容器才能为其他框架提供服务,而Web应用程序的入口是被Web服务器控制的,因此无法在main()方法中通过创建 ClassPathXmlApplicationContext对象来启动Spring容器. Spring 提供了一个监听器类 **org.springframework.web.context.ContextLoaderListener** 来解决这个问题.

该监听器实现了ServletContextListener接口,可以在Web 容器启动的时候初始化 Spring 容器.当然,前提是需要字web.xml中配置好这个监听器. 配置代码如下:

 <!--省略web.xml头文件的引入-->
<welcome-file-list>
    <welcome-file>/WEB-INF/jsp/login.jsp</welcome-file>
</welcome-file-list>
<!--配置SpringMVC的核心控制器-->
<servlet>
    <servlet-name>springmvc</servlet-name>
    <servlet-class>
        org.springframework.web.servlet.DispatcherServlet
    </servlet-class>
    <init-param>
        <param-name>contextConfigLocation</param-name>
        <param-value>classpath:springmvc-servlet.xml</param-value>
    </init-param>
    <load-on-startup>1</load-on-startup>
</servlet>
<servlet-mapping>
    <servlet-name>springmvc</servlet-name>
    <url-pattern>/</url-pattern>
</servlet-mapping>
<!--配置字符编码-->
<filter>
    <filter-name>encodingFilter</filter-name>
    <filter-class>
        org.springframework.web.filter.CharacterEncodingFilter
    </filter-class>
    <init-param>
        <param-name>encoding</param-name>
        <param-value>UTF-8</param-value>
    </init-param>
</filter>
<filter-mapping>
    <filter-name>encodingFilter</filter-name>
    <url-pattern>/*</url-pattern>
</filter-mapping>
<!--指定Spring的配置文件所在的位置 -->
<context-param>
    <param-name>contextConfigLocation</param-name>
    <param-value>classpath*:applicationContext-*.xml</param-value>
</context-param>
<!--配置Spring的监听器类,初始化Spring 容器,使得Spring的配置文件生效 -->
<listener>
    <listener-class>
        org.springframework.web.context.ContextLoaderListener
    </listener-class>
</listener>

通过以上配置,就可以在Web应用启动的同时初始化Spring容器了

> 注意

如果没有指定 contextConfigLocation 参数, ContextLoaderListener 默认会去查找 /WEB-INF/applicationContext.xml .

换句话说,如果我们将Spring的配置文件命名为 applicationContext.xml 并放在 WEB-INF 目录下,即使不指定contextConfigLocation参数,也能实现配置文件的加载(官网上可以查看).

而此处我们将Spring的配置文件命名为 applicationContext-jdbc.xml,故我们使用以"*"为通配符的方式(applicationContext-*.xml)来装载该文件

Spring MVC 的配置文件(springmvc-servlet.xml) 以及在web.xml中相关配置,此处还是正常使用.

> 使用配置类时完整的web.xml文件配置如下

 <!--省略web.xml头文件的引入-->
<welcome-file-list>
    <welcome-file>/WEB-INF/jsp/login.jsp</welcome-file>
</welcome-file-list>

<!-- Configure ContextLoaderListener to use AnnotationConfigWebApplicationContext
        instead of the default XmlWebApplicationContext -->
<context-param>
    <param-name>contextClass</param-name>
    <param-value>
        org.springframework.web.context.support.AnnotationConfigWebApplicationContext
    </param-value>
</context-param>

<!-- Configuration locations must consist of one or more comma- or space-delimited
        fully-qualified @Configuration classes. Fully-qualified packages may also be
        specified for component-scanning -->
<context-param>
    <param-name>contextConfigLocation</param-name>
    <param-value>cn.foo.config.AppConfig</param-value>
</context-param>

<!-- Bootstrap the root application context as usual using ContextLoaderListener -->
<listener>
    <listener-class>
        org.springframework.web.context.ContextLoaderListener
    </listener-class>
</listener>

<!-- 声明一个Spring MVC DispatcherServlet -->
<servlet>
    <servlet-name>dispatcher</servlet-name>
    <servlet-class>
        org.springframework.web.servlet.DispatcherServlet
    </servlet-class>
    <!-- 配置DispatcherServlet以使用注释configwebapplicationcontext
而不是默认的XmlWebApplicationContextt -->
    <init-param>
        <param-name>contextClass</param-name>
        <param-value>
            org.springframework.web.context.support.AnnotationConfigWebApplicationContext
        </param-value>
    </init-param>
    <!-- 配置位置必须由一个或多个逗号或空格分隔的
和完全限定的@Configuration类 -->
    <init-param>
        <param-name>contextConfigLocation</param-name>
        <param-value>cn.foo.web.MvcConfig</param-value>
    </init-param>
</servlet>

<!-- 将/的所有请求映射到dispatcher servlet -->
<servlet-mapping>
    <servlet-name>dispatcher</servlet-name>
    <url-pattern>/</url-pattern>
</servlet-mapping>

> 配置类代替web.xml文件代码如下

public class MyWebAppInitialize extends AbstractAnnotationConfigDispatcherServletInitializer {
    //指定Spring的配置类所在的位置
    @Override
    protected Class<?>[] getRootConfigClasses() {
        return new Class<?>[]{AppConfig.class};
    }
    //指定SpringMVC配置类所在的位置
    @Override
    protected Class<?>[] getServletConfigClasses() {
        return new Class<?>[]{WebConfig.class};
    }

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

    //配置字符编码过滤器
    @Override
    protected Filter[] getServletFilters() {
        CharacterEncodingFilter encodingFilter = new CharacterEncodingFilter();
        encodingFilter.setEncoding("utf-8");
        encodingFilter.setForceEncoding(true);
        return new Filter[]{encodingFilter};
    }
}

3. 使用Spring MVC 实现登录 、注销

3.1 登录功能的初步改造

用户登录和注销两个功能都是系统的最基本功能,也代表着系统的入口和出口,其他的业务功能都是在此基础上实现的。因此,我们就从改造登录和注销功能入手。具体实现步骤如下:

1. 改造 DAO层

DAO层使用素材(UserDao.java,UserDaoImpl.java) 实现即可,并在 UserDaoImpl.java 类名上增加@Repository注解。

2. 改造 Service层

Service层使用素材(UserService.java,UserServiceImpl.java)实现,并在UserServiceImpl.java的类名上增加@Service注解,成员变量userDao 通过 @Resource 注解 或者 @Autowired 注解进行注入,关键代码如下:

@Autowired
private UserDao userDao;

3. 改造 Controller 层

创建 Controller层(UserController.java),要实现系统登录功能了,需要增加三个方法:

@Controller
@RequestMapping("/user")
public class UserController {
    private Logger log = LogManager.getLogger();
    @Autowired
    private UserService userService;
    //跳转到系统登录页
    @RequestMapping("/login.html")
    public String login() {
        logger.info("UserCotroller welcome foo=====================");
        return "login";
    }
    //实现登录
    @RequestMapping("/dologin.html")
    public String doLogin(@RequestParam String userCode,@RequestParam String userPassword) {
        User user = userService.login(userCode, userPassword);
        if(null == user) {
            return "login";
        }else {
            return "redirect:/user/main.html";
        }
    }
    //主页
    @RequestMapping("/main.html")
    public String main() {
        return "frame";
    }
}

说明:

在doLogin()方法中,主要进行用户信息匹配验证(登录系统所输入的登录名和密码 与 后台数据库 中存储的是否一致)。

首先,通过方法入参(@RequestParam String userCode,@RequestParam String userPassword) 来获取用户前台输入。

然后调用业务方法,将参数(userCode,userPassword)传入后台,进行用户匹配,根据返回结果(User 对象),来判断是否登录成功.若登录成功,跳转到系统首页(frame.jsp),若登录失败,跳转到系统登录页面(login.jsp)。

注意 :

登录成功重定向到系统首页: response.sendRedirect(“jsp/frame.jsp”);

一般情况下,控制器方法返回的字符串都会被当成逻辑视图名处理,这仅仅进行服务端的页面的转发而已,并非重定向.若想进行重定向操作,就需要加入"redirect:"前缀, Spring MVC 将对它进行特殊处理:将"redirect"当做指示符,其后的字符串作为URL处理,比如此处的"redirect:/user/main.html"

redirect会让浏览器重新发送一个新的请求/user/main.html,从而进入控制器的main()处理中,当然main()方法中也可以加入其他的一些业务逻辑处理后再进行页面的跳转。

4. 改造 View层

将对应的素材复制到 WEB-INF/jsp目录下,并修改login.jsp页面中 form表的action路径,代码如下:

<form class="loginForm" action="${pageContext.request.contextPath }/user/dologin.html"  name="actionForm" id="actionForm"  method="post" >
  <!--a 中间 省略 -->
</form>

5. 部署运行

改造完后,部署并进行测试,运行结果正常输出.

技巧

系统登录页一般都会作为欢迎也配置在web.xml中:

<welcome-file-list>
    <welcome-file>/WEB-INF/jsp/login.jsp</welcome-file>
</welcome-file-list>

这样访问系统时,在浏览器中输入URL地址: http://localhost:8080/项目名 即可,方便用户输入

6. 扩展Spring JDBC的配置与使用

Spring jdbc与Hibernate,Mybatis相比较,功能不是特别强大,但是在小型项目中,用到还是比较灵活简单。

6.1 JdbcTemplate概述:

它是 spring 框架中提供的一个对象,是对原始 Jdbc API 对象的简单封装。spring 框架为我们提供了很多的操作模板类。如下所示:

  • 操作关系型数据的:
    JdbcTemplate
    HibernateTemplate
  • 操作 nosql 数据库的:
    RedisTemplate
  • 操作消息队列的:
    JmsTemplate
  • ……

6.2 JdbcTemplate对象的创建:

参考源码,一探究竟:

public JdbcTemplate() {
}

public JdbcTemplate(DataSource dataSource) {
    setDataSource(dataSource);
    afterPropertiesSet();
}

public JdbcTemplate(DataSource dataSource, boolean lazyInit) {
    setDataSource(dataSource);
    setLazyInit(lazyInit);
    afterPropertiesSet();
}

除了默认构造函数之外,都需要提供一个数据源。既然有set方法,依据我们之前学过的依赖注入,我们可以在配置文件中配置数据源对象。

6.3 环境搭建:
  1. 导入对应的jar包,若是maven项目导入以下依赖: ```xml org.springframework spring-context 5.2.6.RELEASE org.springframework spring-jdbc 5.2.6.RELEASE
mysql mysql-connector-java 5.1.46

junit junit 4.12 test



2.  编写Spring的配置文件 applicationContext.xml,内容如下: 
```xml
<!--使用Spring内置的数据源-->
<bean id="dataSource" class="org.springframework.jdbc.datasource.DriverManagerDataSource">
  <property name="driverClassName" value="${jdbc.driverClassName}"/>
  <property name="url" value="${jdbc.url}"/>
  <property name="username" value="${jdbc.username}"/>
  <property name="password" value="${jdbc.password}"/>
</bean>
<context:property-placeholder location="jdbc.properties"/>
<!--配置JdbcTemplate-->
<bean class="org.springframework.jdbc.core.JdbcTemplate">
  <constructor-arg ref="dataSource"/>
</bean>
<context:component-scan base-package="cn.foo.dao"/>


jdbc.properties文件内容如下:

  1. 编码:
    编写User.java,代码如下: ```java public class User implements Serializable { private Integer id; //id private String userCode; //用户编码 private String userName; //用户名称 private String userPassword; //用户密码 private Integer gender; //性别 private Date birthday; //出生日期 private String phone; //电话 private String address; //地址 private Integer userRole; //用户角色 private Integer createdBy; //创建者 private Date creationDate; //创建时间 private Integer modifyBy; //更新者 private Date modifyDate; //更新时间
    private String userRoleName; //用户角色名称 //省略其他属性的getter setter 以及toString方法

}

<br />编写UserDao.java,代码如下:  
```java
public interface UserDao {
    List<User> getUserList();
}

编写UserDaoImpl.java,代码如下:

@Repository
public class UserDaoImpl implements UserDao {
    @Autowired
    private JdbcTemplate jdbcTemplate;
    @Override
    public List<User> getUserList() {
        String sql = "select * from foo_user";
        return jdbcTemplate.query(sql,new BeanPropertyRowMapper<>(User.class));
    }
}

若实体类的属性与数据库表中的列名不一致,或者不符合数实体类属性的驼峰命名法与数据库中带下划线的列名一致的情况,可以使用如下lambda表达式解决:

@Repository
public class UserDaoImpl implements UserDao {
     @Autowired
    private JdbcTemplate jdbcTemplate;
     @Override
    public List<User> getUserList() {
        String sql = "select * from foo_user";
        User user = jdbcTemplate.queryForObject(sql, params, (rs, i) -> {
            User u = new User();
            u.setId(rs.getInt("id"));
            u.setUserCode(rs.getString("userCode"));
            u.setUserName(rs.getString("userName"));
            u.setUserPassword(rs.getString("userPassword"));
            u.setGender(rs.getInt("gender"));
            u.setBirthday(rs.getDate("birthday"));
            u.setPhone(rs.getString("phone"));
            u.setAddress(rs.getString("address"));
            u.setUserRole(rs.getInt("userRole"));
            u.setCreatedBy(rs.getInt("createdBy"));
            u.setCreationDate(rs.getTimestamp("creationDate"));
            u.setModifyBy(rs.getInt("modifyBy"));
            u.setModifyDate(rs.getTimestamp("modifyDate"));
            return u;
        });
        return user;
    }
}
  1. 编写测试代码,如下:
    @Test
    public void testGetUserList(){
    ApplicationContext applicationContext =
        new ClassPathXmlApplicationContext("applicationContext.xml");
    UserDao userDao = applicationContext.getBean(UserDao.class);
    userDao.getUserList().forEach(System.out::println);
    }
    

6.4 JdbcTemplate 的增删改查操作:

保存操作:

UserDao.java编写新增方法,代码如下:

int insertUser(User user);

UserDaoImpl.java编写方法实现,代码如下:

@Override
public int insertUser(User user) {
    String sql = "insert into foo_user (userCode,userName,userPassword," +
        "userRole,gender,birthday,phone,address,creationDate,createdBy) " +
        "values(?,?,?,?,?,?,?,?,?,?)";
    Object[] params = {user.getUserCode(),user.getUserName(),user.getUserPassword(),
                       user.getUserRole(),user.getGender(),user.getBirthday(),
                       user.getPhone(),user.getAddress(),user.getCreationDate(),user.getCreatedBy()};
    return jdbcTemplate.update(sql,params);
}

测试代码如下:

ApplicationContext applicationContext =
    new ClassPathXmlApplicationContext("applicationContext.xml");
UserDao userDao = applicationContext.getBean(UserDao.class);
User user = new User();
user.setUserCode("tom");
user.setUserName("Tom");
user.setUserPassword("123");
user.setAddress("New York");
user.setGender(1);
user.setBirthday(new SimpleDateFormat("yyyy-MM-dd").parse("1990-09-09"));
user.setPhone("13800000000");
user.setCreatedBy(1);
user.setCreationDate(new Date());
int i = userDao.insertUser(user);
System.out.println(i);

更新操作

UserDao.java编写更新方法,代码如下:

int updateUserById(User user);

UserDaoImpl.java编写方法实现,代码如下:

@Override
public int updateUserById(User user) {
    String sql = "update foo_user set userCode=?, userName=?,"+
            "gender=?,birthday=?,phone=?,address=?,userRole=?,modifyBy=?,modifyDate=? where id = ? ";
    Object[] params = {user.getUserCode(),user.getUserName(),user.getGender(),user.getBirthday(),
            user.getPhone(),user.getAddress(),user.getUserRole(),user.getModifyBy(),
            user.getModifyDate(),user.getId()};
    return jdbcTemplate.update(sql,params);
}

测试代码如下:

ApplicationContext applicationContext =
    new ClassPathXmlApplicationContext("applicationContext.xml");
UserDao userDao = applicationContext.getBean(UserDao.class);
User user = new User();
user.setId(17);
user.setUserCode("jack");
user.setUserName("Jack");
user.setUserPassword("123456");
user.setAddress("New York");
user.setGender(1);
user.setBirthday(new SimpleDateFormat("yyyy-MM-dd").parse("1992-09-09"));
user.setPhone("13800000000");
user.setModifyBy(1);
user.setModifyDate(new Date());
int i = userDao.updateUserById(user);
System.out.println(i);

删除操作

UserDao.java编写删除方法,代码如下:

int deleteUserById(int id);

UserDaoImpl.java编写方法实现,代码如下:

@Override
public int deleteUserById(int id) {
    String sql = "delete from foo_user where id=?";
    return jdbcTemplate.update(sql,id);
}

测试删除方法,代码如下:

ApplicationContext applicationContext =
    new ClassPathXmlApplicationContext("applicationContext.xml");
UserDao userDao = applicationContext.getBean(UserDao.class);
int i = userDao.deleteUserById(16);
System.out.println(i);

查询操作

UserDao.java编写查询单个对象的方法,代码如下:

//根据ID查看用户信息
User getUserById(int id);

UserDaoImpl.java编写方法实现,代码如下:

@Override
public User getUserById(int id) {
    String sql = "select * from foo_user where id=?";
    User user;
    try {
        user = jdbcTemplate.queryForObject(sql,new BeanPropertyRowMapper<>(User.class),id);
    }catch (EmptyResultDataAccessException e){
        user =  null;
    }
    return user;
}

测试查询方法,代码如下:

ApplicationContext applicationContext =
                new ClassPathXmlApplicationContext("applicationContext.xml");
UserDao userDao = applicationContext.getBean(UserDao.class);
User user = userDao.getUserById(17);
System.out.println(user);

6.5 使用JdbcDaoSupport类简化DAO的实现

Spring提供了一个JdbcDaoSupport类,用于简化DAO的实现。这个JdbcDaoSupport没什么复杂的,核心代码就是持有一个JdbcTemplate

public abstract class JdbcDaoSupport extends DaoSupport {
    @Nullable
    private JdbcTemplate jdbcTemplate;

    public JdbcDaoSupport() {
    }

    public final void setDataSource(DataSource dataSource) {
        if (this.jdbcTemplate == null || dataSource != this.jdbcTemplate.getDataSource()) {
            this.jdbcTemplate = this.createJdbcTemplate(dataSource);
            this.initTemplateConfig();
        }

    }

    @Nullable
    public final JdbcTemplate getJdbcTemplate() {
        return this.jdbcTemplate;
    }
    //.....省略其他代码
}

它的意图是子类直接从JdbcDaoSupport继承后,可以随时调用getJdbcTemplate()获得JdbcTemplate的实例。那么问题来了:因为JdbcDaoSupportjdbcTemplate字段没有标记@Autowired,所以,子类想要注入JdbcTemplate,还得自己想个办法。

通过源码我们可以分析出来,该类有两个重要的方法:

  • public final void setDataSource(DataSource dataSource)
  • public final JdbcTemplate getJdbcTemplate()

其中setDataSource()方法使DAO通过依赖注入方式获得DataSource的实例 , 并创建JdbcTemplate的实例 . 从而通过调用getJdbcTemplate()方法返回**JdbcTemplate**的实例 , 帮助DAO类完成持久化操作

综上我们可以将DAO的实现类简化如下:

//@Transactional如果需要事务处理 则配置事务管理器 并加上对应的注解
@Repository
public class UserDaoImpl extends JdbcDaoSupport implements UserDao {
    public UserDaoImpl() {}
    @Autowired
    public UserDaoImpl(DataSource dataSource) {
        this.setDataSource(dataSource);
    }
     @Override
    public List<User> getUserList() {
        String sql = "select * from foo_user";
        return this.getJdbcTemplate().query(sql,new BeanPropertyRowMapper<>(User.class));
    }
        @Override
    public User getLoginUser(Connection connection, String userCode)
            throws Exception {
        User user = null;
        String sql = "select * from foo_user where userCode=?";
        Object[] params = {userCode};
        System.out.println("使用jdbcTemplate查询数据");
        //数据库的列名不符合以下规则:
        // 列名和属性名必须一致
        // 列名可以使用下划线 属性名可以使用驼峰命名法
        //可以使用手动映射
        /*user = jdbcTemplate.queryForObject(sql,params,(rs,i)->{
            User u = new User();
            u.setId(rs.getInt("id"));
            u.setUserCode(rs.getString("userCode"));
            u.setUserName(rs.getString("userName"));
            u.setUserPassword(rs.getString("userPassword"));
            u.setGender(rs.getInt("gender"));
            u.setBirthday(rs.getDate("birthday"));
            u.setPhone(rs.getString("phone"));
            u.setAddress(rs.getString("address"));
            u.setUserRole(rs.getInt("userRole"));
            u.setCreatedBy(rs.getInt("createdBy"));
            u.setCreationDate(rs.getTimestamp("creationDate"));
            u.setModifyBy(rs.getInt("modifyBy"));
            u.setModifyDate(rs.getTimestamp("modifyDate"));
            return u;
        });*/

        try {
            user = this.getJdbcTemplate().queryForObject(sql,params,new BeanPropertyRowMapper<>(User.class));
           }catch (EmptyResultDataAccessException e){
            user =  null;
        }
        return user;
    }
}

这样,子类的代码就非常干净,可以直接调用getJdbcTemplate()

倘若肯再多写一点样板代码,就可以封装AbstractDao并将声明为泛型,实现getById()getAll()deleteById()这些的通用方法:

public abstract class AbstractDao<T> extends JdbcDaoSupport {
    private String table;
    private Class<T> entityClass;
    private RowMapper<T> rowMapper;

    public AbstractDao() {
        // 获取当前类型的泛型类型:
        this.entityClass = getParameterizedType();
        //拼接表名
        this.table ="foo_"+this.entityClass.getSimpleName().toLowerCase();
        this.rowMapper = new BeanPropertyRowMapper<>(entityClass);
    }
    public Class<T> getParameterizedType(){
        //getClass()获得一个实例的类型类;相当于 某类.class eg:AbstractDao.class
        //getGenericSuperclass()获得带有泛型的父类
        //Type是 Java 编程语言中所有类型的公共高级接口。
        // 它们包括原始类型、参数化类型、数组类型、类型变量和基本类型
        Type type = this.getClass().getGenericSuperclass();
        //ParameterizedType参数化类型,即泛型
        ParameterizedType pt = (ParameterizedType)type;
        // 获取参数化类型中,实际类型的定义
        Type argType = pt.getActualTypeArguments()[0];
        //转换
        return (Class<T>)argType;
    }

    public T getById(int id) {
        return getJdbcTemplate().queryForObject("SELECT * FROM " + table + " WHERE id = ?", this.rowMapper, id);
    }

    public List<T> getAll(int pageIndex) {
        int limit = 5;
        int offset = limit * (pageIndex - 1);
        return getJdbcTemplate().query("SELECT * FROM " + table + " LIMIT ? OFFSET ?",
                new Object[] { limit, offset },
                this.rowMapper);
    }

    public void deleteById(int id) {
        getJdbcTemplate().update("DELETE FROM " + table + " WHERE id = ?", id);
    }
    //.......省略其他通用方法 。。。。。
}

这样,每个子类就自动获得了这些通用方法:

//@Transactional如果需要事务处理 则配置事务管理器 并加上对应的注解
@Repository
public class UserDaoImpl extends AbstractDao<User> implements UserDao {
    // 已经有了:
    // User getById(int)
    // List<User> getAll(int)
    // void deleteById(int)
}

//@Transactional如果需要事务处理 则配置事务管理器 并加上对应的注解
@Repository
public class RoleDaoImpl extends AbstractDao<Role> implements RoleDao {
    // 已经有了:
    // Role getById(int)
    // List<Role> getAll(int)
    // void deleteById(int)
}

DAO模式就是一个简单的数据访问模式,是否使用DAO,根据实际情况决定,因为很多时候,直接在Service层操作数据库也是完全没有问题的。

> 备注

其他方法请查询官方API:https://docs.spring.io/spring/docs/5.2.7.RELEASE/javadoc-api/

3.2 使用Servlet API 对象 作为入参:

在Spring MVC 中,控制器可以不依赖任何Servlet API 对象, 也可以将 Servlet API 对象作为处理方法的入参使用,使用非常方便.比如:此处我们需要使用HttpSession对象,就可以直接将该对象作为入参使用,改造UserController.java,代码如下:

@RequestMapping("/dologin.html")
public String doLogin(@RequestParam String userCode,
        @RequestParam String userPassword,
        HttpServletRequest request,HttpSession session) {
    User user = userService.login(userCode, userPassword);
    if(null == user) {
        request.setAttribute("error", "用户名或者密码有误!!");
        return "login";
    }else {
        session.setAttribute(Constants.USER_SESSION,user);
        return "redirect:/user/main.html";
    }
}
@RequestMapping("/main.html")
public String main(HttpSession session) {
    if(session.getAttribute(Constants.USER_SESSION) == null) {
        return "redirect:/user/login.html";
    }
    return "frame";
}

部署并运行测试,登录成功后,从session中取出当前用户信息,在页面显示用户名称,如图:

image-20220711115338268.png

> 注意

Spring MVC 中使用 Servlet API 作为入参时,Spring MVC 会自动将Web层对应的Servlet对象传递给处理方法入参.处理方法入参,可以同时使用Servlet API 作为参数和其他符合要求的入参,它们之间的位置,顺序并没有特殊要求.

3.3 静态资源文件的引用

通过 在web.xml中配置DispatcherServlet的请求映射为"/" , Spring MVC 将会捕获Web容器的所有请求,当然也包括系统中引用的静态资源的请求. Spring MVC 会将他们当成一个普通的请求处理,但是由于找不到对应的处理器,所有引用的静态文件都无法访问.

对此,Spring MVC 提供了 <mvc:resources /> 标签即可解决静态资源的访问问题.

首先 为了方便配置管理 , 我们将项目中所有的静态文件统一放置在一个目录下,如图:
04.png

然后在 Spring MVC 的 配置文件(springmvc-servlet.xml)中增加配置如下:

<!-- mapping:将静态资源映射到指定的路径(/statics)下
     location:本地静态资源所在的文件目录 -->
<!--<mvc:resources mapping="/statics/**" location="/statics/"/>-->
<!--处理静态资源的第二种方式:
将静态资源的处理经由Spring MVC框架交回Web应用服务器处理
推荐使用第二种-->
<mvc:default-servlet-handler />

说明:

> 方法1.采用

**springmvc-servlet.xml**中配置**<mvc:default-servlet-handler />**后,会在Spring MVC上下文中定义一个**org.springframework.web.servlet.resource.DefaultServletHttpRequestHandler**,它会像一个检查员,对进入DispatcherServlet的URL进行筛查,如果发现是静态资源的请求,就将该请求转由Web应用服务器默认的Servlet处理,如果不是静态资源的请求,才由**DispatcherServlet**继续处理。

一般Web应用服务器默认的Servlet名称是”default”,因此**DefaultServletHttpRequestHandler**可以找到它。如果你所有的Web应用服务器的默认Servlet名称不是”default”,则需要通过default-servlet-name属性显示指定:

**<mvc:default-servlet-handler default-servlet-name="所使用的Web服务器默认使用的Servlet名称" />**

备注:

各种WEB服务器自带的默认Servlet名称:

  • Tomcat, Jetty, JBoss, and GlassFish 自带的默认Servlet的名字 — “default”
  • Google App Engine 自带的 默认Servlet的名字 — “_ah_default”
  • Resin 自带的 默认Servlet的名字 — “resin-file”
  • WebLogic 自带的 默认Servlet的名字 — “FileServlet”
  • WebSphere 自带的 默认Servlet的名字 — “SimpleFileServlet”

> 方法2.采用<mvc:resources />

<mvc:default-servlet-handler />将静态资源的处理经由Spring MVC框架交回Web应用服务器处理。而<mvc:resources />更进一步,由Spring MVC框架自己处理静态资源,并添加一些有用的附加值功能。

首先,<mvc:resources />允许静态资源放在任何地方,如WEB-INF目录下、类路径下等,你甚至可以将JavaScript等静态文件打到JAR包中。通过location属性指定静态资源的位置,由于location属性是Resources类型,因此可以使用诸如”classpath:”等的资源前缀指定资源位置。传统Web容器的静态资源只能放在Web容器的根路径下,<mvc:resources />完全打破了这个限制。

其次,<mvc:resources />依据当前著名的Page Speed、YSlow等浏览器优化原则对静态资源提供优化。你可以通过cacheSeconds属性指定静态资源在浏览器端的缓存时间,一般可将该时间设置为一年,以充分利用浏览器端的缓存。在输出静态资源时,会根据配置设置好响应报文头的Expires 和 Cache-Control值。

在接收到静态资源的获取请求时,会检查请求头的Last-Modified值,如果静态资源没有发生变化,则直接返回303相应状态码,提示客户端使用浏览器缓存的数据,而非将静态资源的内容输出到客户端,以充分节省带宽,提高程序性能。

springmvc-servlet中添加如下配置:

<mvc:resources location="/,classpath:/META-INF/publicResources/" mapping="/resources/**"/>

以上配置将Web根路径”/“及类路径下 /META-INF/publicResources/ 的目录映射为/resources路径。假设Web根路径下拥有images、js这两个资源目录,在images下面有bg.gif图片,在js下面有test.js文件,则可以通过 /resources/images/bg.gif/resources/js/test.js访问两个个静态资源。

假设WebRoot还拥有images/bg1.gifjs/test1.js,则也可以在网页中通过 /resources/images/bg1.gif/resources/js/test1.js 进行引用。

最后需要修改页面引用静态文件的目录路径,加上 /statics.重新运行测试,发现样式,图片以及js均起效,结果如图:

05.png

注意

在实际工作开发中,推荐使用方法一配置静态资源的请求

> 使用配置类代替配置文件如下

@Configuration
@EnableWebMvc
@ComponentScan(basePackages = "cn.foo.controller",useDefaultFilters = false,
includeFilters = {@ComponentScan.Filter(type= FilterType.ANNOTATION,classes = Controller.class)})
public class WebConfig implements WebMvcConfigurer {
    //视图解析器
    @Bean
    public ViewResolver viewResolver(){
        InternalResourceViewResolver viewResolver = new InternalResourceViewResolver("/WEB-INF/jsp/",".jsp");
        return viewResolver;
    }

    /**
     * 此方法用来代替配置文件中的
     * <mvc:resources mapping="/statics/**" location="/statics/"/>
     * @param registry
     */
    /*@Override
    public void addResourceHandlers(ResourceHandlerRegistry registry) {
        registry.addResourceHandler("/statics/**")
                .addResourceLocations("/statics/");
    }*/

    /**
     * 此方法代替配置文件中的:
     * <mvc:default-servlet-handler />
     * @param configurer
     */
    @Override
    public void configureDefaultServletHandling(DefaultServletHandlerConfigurer configurer) {
        configurer.enable();
    }

}

3.4 异常处理

Spring MVC 通过 HandlerExceptionResolver处理程序异常,包括处理器异常,数据绑定异常 以及 处理器执行时发生的异常。HandlerExceptionResolver接口中仅有一个方法:

06.png

当发生异常时,Spring MVC 会调用 resolveException() 方法,并转到 ModelAndView 对应的视图中,作为一个异常报告页面反馈给用户。对于异常处理, 我们一般分为 局部异常处理 和 全局异常处理。

1. 局部异常处理

局部异常处理,仅能处理指定 Controller 中的异常,使用@ExceptionHandler注解实现,在上一个示例UserController.java中增加 exLogin() 和 exceptionHandler() 方法来处理用户登录请求以及异常处理(注:该组方法主要演示如何进行异常处理),代码如下:

//演示用户编码或者密码输入错误的时候 程序抛出异常
@RequestMapping("/exlogin.html")
public String doLogin(@RequestParam String userCode,
        @RequestParam String userPassword) {
    User user = userService.login(userCode, userPassword);
    if(null == user) {
        throw new RuntimeException("用户名或者密码有误");
    }
    return "redirect:/user/main.html";
}

@ExceptionHandler(value=RuntimeException.class)
public String exceptionHandler(RuntimeException e,HttpServletRequest req) {
    req.setAttribute("e", e);
    return "error";
}

增加异常展示页面:WEB-INF\jsp\error.jsp,代码如下:

<%@ page language="java" contentType="text/html; charset=UTF-8"
    pageEncoding="UTF-8"%>
  <!DOCTYPE html>
  <html>
    <head>
      <meta charset="UTF-8">
      <title>异常页面</title>
    </head>
    <body>
      <h1>${e.message }</h1>
    </body>
</html>

部署并运行测试,地址栏中输入:http://localhost:8080/项目名/user/exlogin.html?userCode=admin&userPassword=123,对应的结果跳转到了error.jsp页面.

使用局部处理异常 仅能处理某个Controller中的异常,若需要对所有的异常进行统一处理,就需要进行全局异常处理了.

2. 全局异常处理

> 全局异常之配置

全局异常处理可使用SimpleMappingExceptionResolver来实现.它将异常类名映射为视图名,即发生异常时使用对应的视图报告异常.

改造上一个示例,首先注释掉UserController.java里的局部异常处理方法exceptionHandler(),然后在springmvc-servlet.xml中配置全局异常

关键代码如下:

<!-- 全局异常处理 -->
<bean class="org.springframework.web.servlet.handler.SimpleMappingExceptionResolver">
  <property name="exceptionMappings">
    <props>
      <prop key="java.lang.RuntimeException">error</prop>
    </props>
    <!-- 或者使用value标签来配置 -->
    <!--<value>java.lang.RuntimeException=error
        </value>-->
    <!-- 或者配置以下两个属性来实现 -->
    <!-- <property name="excludedExceptions" value="java.lang.Exception"/>
         <property name="defaultErrorView" value="error"/>-->
  </property> 
</bean>

上述配置中,我们指定当控制器发送RuntimeException异常时,使用error视图进行异常信息显示.当然,我们也可以在<props>标签内定义多个异常,error.jsp页面中的message显示 也需修改为${exception.message}来进行异常信息的展示.error.jsp代码如下:

<%@ page language="java" contentType="text/html; charset=UTF-8"
    pageEncoding="UTF-8"%>
  <!DOCTYPE html>
  <html>
    <head>
      <meta charset="UTF-8">
      <title>异常页面</title>
    </head>
    <body>
      <h1>${exception.message }</h1>
    </body>
</html>

部署项目,运行结果如上图所示.

也可以使用配置类的方式实现

核心代码如下:

@Configuration
@EnableWebMvc
@ComponentScan(basePackages = "cn.foo.controller")
public class MyWebConfig implements WebMvcConfigurer {

    @Bean
    public ViewResolver viewResolver(){
        return new InternalResourceViewResolver("/WEB-INF/jsp/",".jsp");
    }

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

    @Override
    public void configureHandlerExceptionResolvers(List<HandlerExceptionResolver> resolvers) {
        SimpleMappingExceptionResolver exceptionResolver = new SimpleMappingExceptionResolver();
        exceptionResolver.setDefaultErrorView("error");
        exceptionResolver.setExcludedExceptions(Exception.class);
        resolvers.add(exceptionResolver);
    }
}

> 全局异常之注解

@ControllerAdvice注解:增强型控制器,对于控制器的全局配置放在同一个位置

使用 @ControllerAdvice,结合@ExceptionHandler可用于全局异常的处理. 演示代码如下:

增加全局配置的普通java类,代码如下:

@ControllerAdvice
public class ControllerException {
    @ExceptionHandler(value=RuntimeException.class)
    public String  exceptionHandler(RuntimeException e, HttpServletRequest req) {
        req.setAttribute("e", e);
        return "error";
    }

}

修改error.jsp页面,代码如下:

<%@ page language="java" contentType="text/html; charset=UTF-8"
    pageEncoding="UTF-8"%>
  <!DOCTYPE html>
  <html>
    <head>
      <meta charset="UTF-8">
      <title>异常页面</title>
    </head>
    <body>
      <h1>${e.message }</h1> 
    </body>
</html>

将Spring MVC 配置文件中关于全局配置的bean注视掉,部署项目,运行结果如上图.

4. 使用 Spring MVC 实现用户列表查询

在前面的章节中, 我们完成了用户的登录和注销,在本小节中,我们将继续改造超市订单管理系统的用户管理功能模块中的用户列表查询. 实现如图所示

07.png

需求分析:

  • (1) 查询条件: 用户名称(模糊匹配), 用户角色(精确匹配)
  • (2) 列表页显示字段
  • (3) 分页显示数据列表

具体步骤如下:

4.1 改造后台实现

改造主要集中在控制层 和 视图层, 故 DAO . Service. tools, POJO 可以直接使用foo素材提供的对应的包和类.但是需要注意在对应的实现加上注解.

4.2 改造Controller层

改造UserController.java,增加查询用户列表getUserList()方法(该方法的具体实现可以从foo素材中的UserServlet的query()方法中获取,进行简单改造即可),关键代码如下:

@Controller
@RequestMapping("/user")
public class UserController {
    private Logger logger = Logger.getLogger(this.getClass());
    @Autowired
    private UserService userService;
    @Autowired
    private RoleService roleService;
    //省略其他代码 .....
    @RequestMapping("/userlist.html")
    public String getUserList(Model model,
            @RequestParam(value="queryUserName",required=false) String queryUserName,
            @RequestParam(value="queryUserRole",required=false) String queryUserRole,
            @RequestParam(value="pageIndex",required=false) String pageIndex) {
        logger.info("getUserList--->queryUsername==="+queryUserName);
        logger.info("getUserList--->queryUserRole==="+queryUserRole);
        logger.info("getUserList--->pageIndex==="+pageIndex);
        //封装分页
        int currentPageNo = 1;//当前页
        int pageSize = Constants.pageSize;//页面容量
        if(null == queryUserName) {
            queryUserName = "";
        }
        int userRole = 0;
        if(queryUserRole != null && !"".equals(queryUserRole)) {
            try {
                userRole = Integer.parseInt(queryUserRole);                
            }catch(NumberFormatException e) {
                //若发现用户在进行非法操作 则重定至错误页面
                return "redirect:/user/syserror.html";
            }
        }
        if(pageIndex != null && !"".equals(pageIndex)) {
            try {
                currentPageNo = Integer.parseInt(pageIndex);                
            }catch(NumberFormatException e) {
                //若发现用户在进行非法操作 则重定至错误页面
                return "redirect:/user/syserror.html";
            }
        }
        //总记录数
        int totalCount = userService.getUserCount(queryUserName, userRole);
        PageSupport pages = new PageSupport();
        pages.setCurrentPageNo(currentPageNo);
        pages.setPageSize(pageSize);
        pages.setTotalCount(totalCount);
        //获取总页数
        int totalPageCount = pages.getTotalPageCount();
        //控制首尾页
        if(currentPageNo < 1) {
            currentPageNo = 1;
        }else if(currentPageNo > totalPageCount) {
            currentPageNo = totalPageCount;
        }
        //获取查询的用户信息
        List<User> userList = userService.getUserList(queryUserName, userRole, currentPageNo, pageSize);
        //获取角色信息
        List<Role> roleList = roleService.getRoleList();
        model.addAttribute("userList",userList);
        model.addAttribute("roleList",roleList);
        model.addAttribute("queryUserName",queryUserName);
        model.addAttribute("queryUserRole",queryUserRole);
        model.addAttribute("totalPageCount",totalPageCount);
        model.addAttribute("totalCount",totalCount);
        model.addAttribute("currentPageNo",currentPageNo);
        //封装查询信息
        return "userlist";
    }

}

在上述代码中,@RequestMapping 只指定了 value,无须指定method.这是因为查询用户列表的入口有两处:

菜单栏的用户管理链接(GET请求),用户列表界面的查询from(POST请求).对于Controller来说,只需提供一个请求的处理方法即可,无论是GET请求还是POST请求,均可进入此请求的处理方法.

根据查询条件,有4个入参:Model,用户名称,用户角色,当前页码.最后把需要回显的查询条件,以及查询处理的用户列表,用户角色列表放入Model中,返回逻辑视图名.

在处理页码,或者处理用户角色的过程中,若catch到异常,则重定向至系统的错误页面:

//若发现用户在进行非法操作 则重定至错误页面
return "redirect:/user/syserror.html";

在UserController.java里增加处理该请求的sayError()方法,示例代码如下:

//若发现用户在进行非法操作 则重定至错误页面
@RequestMapping("/syserror.html")
public String sysError() {
    return "syserror";
}

4.3 改造View层

增加用户列表的显示页(使用foo素材中的userlist.jsp rolepage.jsp即可),改造过程中需要注意修改所有的js,css,image的引用路径(如:/statics/….).userlist.jsp页面的关键示例代码如下:

<div class="search">
  <form method="post" action="${pageContext.request.contextPath }/user/userlist.html">
    <input name="method" value="query" class="input-text" type="hidden">
    <span>用户名:</span>
    <!-- 注意form表单中控件的name跟后台指定的参数一致 -->
    <input name="queryUserName" class="input-text" type="text" value="${queryUserName }">

    <span>用户角色:</span>
    <select name="queryUserRole">
      <c:if test="${roleList != null }">
        <option value="0">--请选择--</option>
        <c:forEach var="role" items="${roleList}">
          <option <c:if test="${role.id == queryUserRole }">selected="selected"</c:if>
        value="${role.id}">${role.roleName}</option>
      </c:forEach>
    </c:if>
  </select>

<input type="hidden" name="pageIndex" value="1"/>
<input    value="查 询" type="submit" id="searchbutton">
<a href="${pageContext.request.contextPath}/jsp/useradd.jsp" >添加用户</a>
</form>
</div>

修改head.jsp中菜单里的”用户管理”的链接:

<li><a href="${pageContext.request.contextPath }/user/userlist.html">用户管理</a></li>

最后增加syserror.jsp,关键代码如下:

<h1>操作有误,请重新登录后再访问该页面</h1>
<a href="${pageContext.request.contextPath }/user/login.html">返回登录页</a>

4.4 部署后运行测试

直接在地址栏中输入URL:http://localhost:8080/项目名 ,成功登录系统后,进入用户管理,进入用户管理,进行用户列表的查询.

5. 将项目打成war包部署到Tomcat服务器上

5.1 修改pom.xml文件

在pom.xml文件中加入以下元素 , 为打成的项目war包起别名:

<build>
    <finalName>ssm_ch09</finalName>
</build>

5.2 使用maven的方式将项目打成war包

IDEA工具中的详细操作如图所示:

1578393603622.png

或者在项目目录上打开cmd窗口,输入如下命令:

mvn clean package -DskipTests

如图所示:

1578393960671.png

备注:

解决使用maven命令打包项目报错问题,报错如下:

[ERROR] Unknown lifecycle phase "mvn". You must specify a valid lifecycle phase or a goal in the format <plugin-prefix>:<goal> or <plugin-group-id>:<plugin-artifact-id>[:<plugin-version>]:<goal>. Available lifecycle phases are: validate, initialize, generate-sources, process-sources, generate-resources, process-resources, compile, process-classes, generate-test-sources, process-test-sources, generate-test-resources, process-test-resources, test-compile, process-test-classes, test, prepare-package, package, pre-integration-test, integration-test, post-integration-test, verify, install, deploy, pre-clean, clean, post-clean, pre-site, site, post-site, site-deploy. -> [Help 1]

运行以下命令,可以解决:

第一步:

mvn install

第二步:

mvn compiler:compile

第三步:

mvn org.apache.maven.plugins:maven-compiler-plugin:compile

第四步:

mvn org.apache.maven.plugins:maven-compiler-plugin:2.0.2:compile

5.3 将打好的war包,放入到tomcat的webapps目录下:

如图所示:

1578394540275.png

5.4 启动tomcat服务器

浏览器输入路径:http://localhost:8080/ssm_ch09/,页面显示如图所示:

1578394700402.png

6. 项目部署到Tomcat上的中文乱码问题

6.1 开发工具:

  • IntelliJ IDEA
  • Tomcat 9.0
  • jdk1.8

js插件:bootstrap、bootstrap-table、jQuery …

6.2 详细问题:

web app项目在原先的Tomcat 7运行时,各种js以及post、get请求返回到前台以及控制台的数据是正常的,把项目部署到Tomcat9后,浏览器上显示的js注释、js访问后台返回来数据在控制台上输出是正常的但是显示到页面上就是中文乱码,jsp页面上的中文是正常的。

6.3 解决方案:

  • step 1:修改 D:\Java\apache-tomcat\conf\server.xml,添加 如下代码:

    <Connector port="8080" protocol="HTTP/1.1"
                connectionTimeout="20000"
                redirectPort="8443" 
                URIEncoding="UTF-8" />
    


    如图所示:
    1578395138380.png

  • step 2:修改 D:\Java\apache-tomcat\bin\catalina.bat 添加:

    set "JAVA_OPTS=%JAVA_OPTS% %JSSE_OPTS%  -Dfile.encoding=UTF-8"
    


    如图所示:
    1578395266957.png

  • step 3 : 重启Tomcat,当你刷新页面时发现还有乱码问题

  • step 4 : 清除浏览器的缓存,重新打开项目,乱码不见了!!!