• SpringMVC


    SpringMVC是什么?

    SpringMVC和Struts2都属于表现层的框架,它是Spring框架的一部分,我们可以从Spring的整体结构中看得出来:

    SpringMVC的处理流程

     入门

    1、springmvc配置文件

    <?xml version="1.0" encoding="UTF-8"?>
    <beans xmlns="http://www.springframework.org/schema/beans"
        xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:p="http://www.springframework.org/schema/p"
        xmlns:context="http://www.springframework.org/schema/context"
        xmlns:mvc="http://www.springframework.org/schema/mvc"
        xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-4.0.xsd
            http://www.springframework.org/schema/mvc http://www.springframework.org/schema/mvc/spring-mvc-4.0.xsd
            http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context-4.0.xsd">
    
        <!-- 配置controller扫描包 -->
        <context:component-scan base-package="com.meimeixia.springmvc.controller" />
    
    </beans>

    注解映射器和适配器

    配置组件扫描器

    使用组件扫描器可省去在Spring容器中配置每个Controller类的繁琐。我们只要在SpringMVC的核心配置文件中使用了<context:component-scan>标签,那么就能自动扫描标记有@Controller注解的控制器类了。

    <context:component-scan base-package="com.meimeixia.springmvc.controller" /

    注意,如果要扫描多个包,多个包中间使用半角逗号分隔。

    配置RequestMappingHandlerMapping

    注解式处理器映射器,对类中标记@ResquestMapping注解的方法进行映射,根据@ResquestMapping注解中绑定的url请求来匹配@ResquestMapping注解标记的方法,匹配成功返回HandlerMethod对象给前端控制器,HandlerMethod对象中封装了url请求对应的Method方法。
    温馨提示:从Spring3.1版本开始,废除了DefaultAnnotationHandlerMapping的使用,推荐使用RequestMappingHandlerMapping完成注解式处理器映射(即推荐使用最新版本的注解式处理器映射器)。

    <!-- 配置(最新的)处理器映射器,处理器适配器是干嘛的呢?它用于绑定用户的请求(配置完它之后,就不使用默认的了) -->
    <bean class="org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerMapping" />

    配置RequestMappingHandlerAdapter

    注解式处理器适配器,对标记@ResquestMapping注解的方法进行适配。从Spring3.1版本开始,废除了AnnotationMethodHandlerAdapter的使用,推荐使用RequestMappingHandlerAdapter完成注解式处理器适配(即推荐使用最新版本的注解式处理器适配器)。

    <!-- 配置(最新的)处理器适配器,处理器适配器是干嘛的呢?它帮我们实例化Controller,同时调用其相应的方法(配置完它之后,也不使用默认的了) -->
    <bean class="org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerAdapter" />

    如果你想对以上注解式处理器适配器进行扩展,那么可以在上面的bean里面配置其他的属性。当我们配置完注解式处理器映射器和注解式处理器适配器之后,SpringMVC核心配置文件(即springmvc.xml)的内容就变成下面这个样子了。

    <?xml version="1.0" encoding="UTF-8"?>
    <beans xmlns="http://www.springframework.org/schema/beans"
        xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:p="http://www.springframework.org/schema/p"
        xmlns:context="http://www.springframework.org/schema/context"
        xmlns:mvc="http://www.springframework.org/schema/mvc"
        xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-4.0.xsd
            http://www.springframework.org/schema/mvc http://www.springframework.org/schema/mvc/spring-mvc-4.0.xsd
            http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context-4.0.xsd">
    
        <!-- 配置controller扫描包 -->
        <context:component-scan base-package="com.meimeixia.springmvc.controller" />
        
        <!-- 配置(最新的)处理器映射器,处理器适配器是干嘛的呢?它用于绑定用户的请求(配置完它之后,就不使用默认的了) -->
        <bean class="org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerMapping" />
        
        <!-- 配置(最新的)处理器适配器,处理器适配器是干嘛的呢?它帮我们实例化Controller,同时调用其相应的方法(配置完它之后,也不使用默认的了) -->
        <bean class="org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerAdapter" />
    
    </beans>

    继续优化注解,配置<mvc:annotation-driven>

    使用注解要注意一个问题,就是注解适配器和映射器必须配对使用,也就是说,不能一个用注解,一个用非注解,要用就一起用,要么就都不用。其实在SpringMVC中还有更加简便的配置,即使用<mvc:annotation-driven>标签自动加载RequestMappingHandlerMapping和RequestMappingHandlerAdapter,这样说来,就可以在SpringMVC核心配置文件中使用<mvc:annotation-driven>标签替代注解处理器和适配器的配置了,如下图所示。

    配置视图解析器

    我们也可以在SpringMVC核心配置文件中自己手动配置视图解析器,如下:

    <!-- 配置视图解析器(对jsp默认解析的视图解析器) --> <bean class="org.springframework.web.servlet.view.InternalResourceViewResolver"> <property name="prefix" value="/WEB-INF/jsp/"></property> <property name="suffix" value=".jsp"></property> </bean>

    对于以上视图解析器的配置,我觉得有几点需要说明一下。

    • InternalResourceViewResolver:支持jsp视图解析;
    • viewClass:JstlView表示jsp模板页面需要使用JSTL标签库,所以classpath中必须包含jstl的相关jar包(例如jstl-1.2.jar)。此属性可以不设置,默认就为JstlView;
    • prefix和suffix:表示查找视图页面的前缀和后缀,最终视图的址为:前缀+逻辑视图名+后缀,逻辑视图名需要在Controller类返回的ModelAndView中指定,比如逻辑视图名为hello,则最终返回的jsp物理视图地址就为WEB-INF/jsp/hello.jsp

    Controller类的参数绑定

    要根据商品id查询商品信息,需要从url请求中把请求参数(id)取出来。请求参数(id)应该包含在HttpServletRequest对象中,所以可以从该对象中取出id。因此我们应在ItemController类中添加一个如下方法。

    @RequestMapping("itemEdit") //如何获取HttpServletRequest对象呢?直接像下面这样写就行。这就是SpringMVC默认参数的传递
    public ModelAndView itemEdit(HttpServletRequest request) {
        ModelAndView mav = new ModelAndView();
        String idStr = request.getParameter("id");
        //查询商品信息
        Item item = itemService.getItemById(new Integer(idStr));
        mav.addObject("item", item);
        mav.setViewName("itemEdit");
        return mav;
    }

    SpringMVC框架默认支持的参数类型

    处理器形参中添加如下类型的参数,处理适配器会默认识别并进行赋值。

     温馨提示:使用Model和ModelMap的效果是一样的,如果直接使用Model接口,SpringMVC会实例化ModelMap。

    如果使用了Model接口,那么可以不必再使用ModelAndView对象了,Model对象可以向页面传递数据(model是框架给我们传递过来的对象,所以这个对象不需要我们返回),View对象则可以使用String返回值替代。不管是Model还是ModelAndView,其本质都是使用HttpServletRequest对象向jsp页面传递数据。

    简单数据类型绑定

    当请求的参数名称和处理器形参名称一致时会将请求参数与形参进行绑定。如此一来,从HttpServletRequest对象中取参数的方法可以进一步简化,于是,itemEdit方法就要改造成下面这个样子了。

    @RequestMapping("itemEdit")
    public String itemEdit(Model model, Integer id) {
        //查询商品信息
        Item item = itemService.getItemById(id);
        //model返回数据模型(数据模型通过Model返回)
        model.addAttribute("item", item);
        return "itemEdit";
    }

    支持的数据类型

    对于布尔类型的参数,请求的参数值为true或者false。处理器方法可以是下面这样的:

    public String editItem(Model model, Integer id, Boolean status) throws Exception {
    	...
    }

    至于请求的url,可以是http://localhost:8080/xxx.action?id=2&status=false这样的。温馨提示:参数类型推荐使用包装数据类型,因为基础数据类型不可以为null。

    @RequestParam

    使用@RequestParam注解常用于处理简单数据类型的绑定,该注解有如下三个非常重要的属性。

    • value:表示参数名字,即入参的请求参数名字,例如value="item_id"表示请求的参数区中的名字为item_id的参数的值将传入;
    • required:表示请求参数是否必须,默认是true,表示请求中一定要有相应的参数,否则将会报错误:
    • defaultValue:请求参数的默认值,表示如果请求中没有同名参数时的默认值。
    @RequestMapping("itemEdit")            //@RequestParam注解中的required=true表示咱们的请求参数必须要进行提交,defaultValue="1"表示如果请求参数没有进行提交,则为请求参数设置一个默认值
    public String itemEdit(Model model, @RequestParam(value="id", required=true, defaultValue="1") Integer ids) {
        //查询商品信息
        Item item = itemService.getItemById(ids);
        //model返回数据模型(数据模型通过Model返回)
        model.addAttribute("item", item);
        return "itemEdit";
    }

     形参名称为ids,但是这里使用value="id"限定请求的参数名为id,所以页面传递参数的名称必须为id。这里通过required=true限定id参数为必须传递,如果不传递则报400错误,如下图所示。

     为了解决以上异常,可以使用defaultvalue设置请求参数的默认值,这时,即使不传递id参数也是可以的。

    绑定pojo类型

    现有这样一个需求:将页面修改后的商品信息保存到数据库表中。如何解决这个需求呢?这儿是我对这个需求的分析:假设请求的url为updateItem.action,由于我想要将页面修改后的商品信息保存到数据库表中,所以需要传递的参数是表单中的数据。最终的一个响应结果就是重新跳转到商品编辑页面,在该页面中显示修改商品信息成功的信息。

    使用pojo接收表单数据

    如果提交的参数很多,或者提交的表单中的内容很多,这时便可以使用pojo来接收数据,但要求pojo对象中的属性名和表单中input元素的name属性一致,就像下图所示的这样。

     在Controller类中使用pojo数据类型进行参数绑定,即应在ItemController类中添加一个如下方法。

    @RequestMapping("updateItem")
    public String updateItem(Item item, Model model) {
        itemService.updateItem(item);
        model.addAttribute("item", item);
        model.addAttribute("msg", "修改商品信息成功");
        return "itemEdit";
    }

    请求的参数名称和pojo类中的属性名称一致,会自动将请求参数赋值给pojo类中的属性。注意:提交的表单中最好不要有日期类型的数据,否则会报400错误。如果想提交日期类型的数据需要用到后面的自定义参数绑定的内容。
    这样,当我们修改完商品信息之后,去数据库表中瞧一瞧,那么就会看到中文乱码问题出现了

    我们可以在web.xml文件中加入一个Spring提供的CharacterEncodingFilter过滤器,使用该过滤器来解决post请求的中文乱码问题。

    <!-- 使用Spring提供的CharacterEncodingFilter过滤器解决post请求的中文乱码问题 -->
    <filter>
        <filter-name>encoding</filter-name>
        <filter-class>org.springframework.web.filter.CharacterEncodingFilter</filter-class>
        <!-- 设置字符编码为UTF8 -->
        <init-param>
            <param-name>encoding</param-name>
            <param-value>UTF-8</param-value>
        </init-param>
    </filter>
    <filter-mapping>
        <filter-name>encoding</filter-name>
        <url-pattern>/*</url-pattern>
    </filter-mapping>

    注意:以上配置只能解决post请求的中文乱码问题。对于get请求方式中文参数出现乱码的解决方法有两个:

    1. 修改Tomcat服务器的配置文件(server.xml),即添加编码与工程编码一致。

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

    1. 另外一种方法是对参数进行重新编码。
    2. String username = new String(request.getParamter("userName").getBytes("ISO8859-1"),"utf-8");
      1. ISO8859-1是Tomcat服务器默认编码,需要将Tomcat服务器编码后的内容按utf-8编码。

      解决完post请求的中文乱码问题之后,再次修改商品信息,去数据库表中瞧一瞧,你会发现一切都正常了。

      https://blog.csdn.net/a83370892/article/details/82726700

    绑定包装pojo

    现有这样一个需求:使用包装的pojo接收商品信息的查询条件。为了解决这个需求,首先要在com.meimeixia.springmvc.pojo包下编写一个包装类。

    public class QueryVo {
        
        private Item item;
    
        public Item getItem() {
            return item;
        }
    
        public void setItem(Item item) {
            this.item = item;
        }
        
    }

    然后,在itemList.jsp页面中添加如下input输入项。

    <input type="text" name="item.name" />
    <input type="text" name="item.price" />
    @RequestMapping("queryItem")
    public String queryItem(QueryVo vo, Model model) {
        if (vo.getItem() != null) {
            System.out.println(vo.getItem());
        }
        
        //这里不做具体的搜索,只是模拟搜索商品
        List<Item> list = itemService.getItemList();
        model.addAttribute("itemList", list);
        return "itemList";
    }

    自定义参数绑定

    有这样一个需求:在商品修改页面中需要修改商品的生产日期,并且根据业务需求自定义日期格式。为了解决这个需求,首先要在itemEdit.jsp页面中添加商品生产日期的输入项,因此该页面应改造成下面这个样子。

                <tr>
                    <td>商品生产日期</td>
                    <td><input type="text" name="createtime"
                        value="<fmt:formatDate value="${item.createtime}" pattern="yyyy-MM-dd HH:mm:ss"/>" /></td>
                </tr>

    提交的表单中最好不要有日期类型的数据,否则就会报400错误。如果想提交日期类型的数据则需要用到后面的自定义参数绑定的内容。所以要真正解决这个问题,就必然要用到自定义参数绑定的内容了。由于日期数据有很多种格式,SpringMVC根本没办法把字符串转换成日期类型,所以这才需要自定义参数来绑定。前端控制器接收到请求后,找到注解形式的处理器适配器,对@RequestMapping注解标记的方法进行适配,并对方法中的形参进行参数绑定。在SpringMVC中,可以在处理器适配器上自定义Converter以此来进行参数绑定。如果使用了<mvc:annotation-driven/>,则可以在此标签上进行扩展。

    自定义Converter

    在com.meimeixia.springmvc.converter包下编写一个自定义Converter,即DateConverter.java。

    public class DateConverter implements Converter<String, Date> {
    
        @Override
        public Date convert(String source) {
            try {
                SimpleDateFormat simpleDateFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
                Date date = simpleDateFormat.parse(source);
                return date;
            } catch (ParseException e) {
                e.printStackTrace();
            }
            return null;
        }
    
    }

    配置Converter

    如果使用了<mvc:annotation-driven/>,那么就可以在此标签上进行扩展了,即在springmvc.xml配置文件中添加如下配置。

    <!-- 
        配置注解驱动,相当于同时使用了最新的处理器映射器和处理器适配器,须知处理器映射器和处理器适配器必须配套使用!
        注解驱动还能对json数据的响应提供支持。
    -->
    <mvc:annotation-driven conversion-service="conversionService" />
    <!-- 转换器的配置 -->
    <bean id="conversionService" class="org.springframework.format.support.FormattingConversionServiceFactoryBean">
        <property name="converters">
            <set>
                <bean class="com.meimeixia.springmvc.converter.DateConverter"/>
            </set>
        </property>
    </bean>

    高级参数绑定

    现在进入到SpringMVC中高级参数绑定的学习中。

    绑定数组

    现有这样一个需求:在商品列表页面选中多个商品,然后删除之(也即批量删除)。要解决该需求,就要求商品列表页面中的每个商品前都应有一个checkbook(复选框),选中多个商品后点击删除按钮把商品id传递给Controller,再根据商品id批量删除商品信息。于是,首先要将itemList.jsp页面改造成下面这个样子。

    在Controller类的方法中,可以使用Integer[]接收,或者使用pojo类中的Integer[]属性接收,两种方式任选其一即可。

    @RequestMapping("queryItem")
    public String queryItem(QueryVo vo, Integer[] ids, Model model) {
        if (vo.getItem() != null) {
            System.out.println(vo.getItem());
        }
        if (ids != null && ids.length > 0) {
            for (Integer id : ids) {
                System.out.println(id);
            }
        }
        
        //这里不做具体的搜索,只是模拟搜索商品
        List<Item> list = itemService.getItemList();
        model.addAttribute("itemList", list);
        return "itemList";
    }

    方式二:使用pojo类中的Integer[]属性接收。为了演示这种方式,我们首先要在QueryVo类中添加一个如下属性

     此时,ItemController类中的queryItem方法不变,假设依然为:

    @RequestMapping("queryItem")
    public String queryItem(QueryVo vo, Integer[] ids, Model model) {
        if (vo.getItem() != null) {
            System.out.println(vo.getItem());
        }
        if (ids != null && ids.length > 0) {
            for (Integer id : ids) {
                System.out.println(id);
            }
        }
        
        //这里不做具体的搜索,只是模拟搜索商品
        List<Item> list = itemService.getItemList();
        model.addAttribute("itemList", list);
        return "itemList";
    }

    如果在Controller类的方法中即使用了Integer[]来接收又使用了pojo类中的Integer[]属性来接收,那么都能接收到参数。

    将表单的数据绑定到List

    现有这样一个需求:实现商品信息的批量修改。要想实现商品信息的批量修改,在商品列表页面中就要能对商品信息进行修改,并且可以批量提交修改后的商品信息。为了解决这个需求,首先,我们需要将QueryVo包装类改造一下,在该包装类定义一个List集合,集合中存放的是Item对象。

        private Item item;
        
        private Integer[] ids;
        
        private List<Item> items;

    页面内容如下:

    <tr>
        <td><input type="checkbox" name="ids" value="${item.id}" /></td>
        <td>
        <input type="hidden" name="items[${status.index}].id" value="${item.id}" />
        <input type="text" name="items[${status.index}].name" value="${item.name }" /></td>
        <td><input type="text" name="items[${status.index}].price" value="${item.price }" /></td>
        <td>
            <%-- <fmt:formatDate value="${item.createtime}" pattern="yyyy-MM-dd HH:mm:ss" /> --%>
            <input type="text" name="items[${status.index}].createtime" value='<fmt:formatDate value="${item.createtime}" pattern="yyyy-MM-dd HH:mm:ss"/>' />
        </td>
        <td><input type="text" name="items[${status.index}].detail" value="${item.detail }" /></td>
        
        <td><a href="${pageContext.request.contextPath }/itemEdit.action?id=${item.id}">修改</a></td>
    </tr>

    注意:以上<input>输入项中的name属性的值必须是QueryVo包装类的items属性+下标+元素属性。大家可能忘了<c:forEach>标签中的varStatus属性了,在此我对它做一个总结,希望能唤醒读者的记忆。

     温馨提示:接收List类型的数据必须是pojo类中的属性,方法的形参为List类型无法正确接收到数据。
    最后,在if (vo.getItem() != null) {这句代码处打一个断点,继而以Debug模式运行queryItem方法,观察vo这个变量,就能看到我们所想要的效果了。

    @RequestMapping注解的使用

    之前,我们就已经使用过@RequestMapping注解了,但还有一些细节我还没讲到,所以在此将详细讲讲@RequestMapping注解的使用。

    @RequestMapping

    通过@RequestMapping注解可以定义不同的处理器映射规则。

    value的值是数组,所以可以将多个url映射到同一个方法上,就像下面这样。

    窄化请求映射

    在class上添加@RequestMapping(url)可以指定通用请求前缀,限制此类下的所有方法请求url必须以请求前缀开头。@RequestMapping注解加在类头部,可以用于目录分级管理,就像下面这样,@RequestMapping注解放在类名上边,设置请求前缀。

    @Controller
    @RequestMapping("item")
    public class ItemController {
    
        //...
        
    }

    请求方式限定

    • 可以使用@RequestMapping(method = RequestMethod.GET)来限定GET请求方式。例如:

      

     可以使用@RequestMapping(method={RequestMethod.GET,RequestMethod.POST})来限定GET和POST请求方式。例如:

     Controller返回值

    返回ModelAndView

    在Controller类方法中定义ModelAndView对象并返回,该对象可添加Model数据以及指定View。之前我就已讲过,所以在此并不过多赘述。

    返回void

    如果现在Controller类方法的返回值是void,那么在Controller类方法中就应使用request来跳转页面。所以,我们可以在ItemController类中编写一个返回值是void的如下方法。

    @RequestMapping("queryVoid")
    public void queryVoid(HttpServletRequest request, HttpServletResponse response) throws Exception {
        //request响应用户请求
        request.setAttribute("msg", "这个是requst响应的消息");
        request.getRequestDispatcher("/WEB-INF/jsp/msg.jsp").forward(request, response);
    }

    通过response实现页面重定向,例如:
    在这里插入图片描述
    此时,在浏览器地址栏中输入http://localhost:8080/04-springmvc-mybatis/queryVoid.action这样的url访问地址进行访问,你就会看到如下图所示的效果了。

    当然了,我们也可以通过response来指定响应结果,例如响应一串字符串,就像下面这样。

    返回字符串

    逻辑视图名

    Controller类中的方法返回字符串可以指定逻辑视图名,然后通过视图解析器解析为物理视图地址。

    重定向(redirect)

    Contrller类方法返回结果可以重定向到一个url地址,例如商品信息修改提交后需要重定向到商品列表查询方法,但是请注意参数是无法直接带到商品列表查询方法中的。

    @RequestMapping(value="updateItem", method={RequestMethod.POST, RequestMethod.GET})
    public String updateItem(Item item) {
        itemService.updateItem(item);
        return "redirect:itemList.action";//请求重定向
    }

    其实,以上redirect方式相当于response.sendRedirect(…),请求重定向后浏览器的地址栏变为重定向后的地址,因为请求重定向即执行了一个新的request和response。由于新发起了一个request,所以原来的参数在请求重定向时就不能传递到下一个url了。如果要继续传递参数,那么可以在itemList.action后边加参数,就像下面这样。

    return "redirect:itemList.action?id=xxx&name=xxx";

    转发(forward)

    转发意味着Controller类方法执行后可以继续执行另一个Controller类方法,例如商品信息修改提交后转发到商品列表页面,这时,修改商品的id参数等可以直接带到商品列表查询方法中。

    @RequestMapping(value="updateItem", method={RequestMethod.POST, RequestMethod.GET})
    public String updateItem(Item item) throws IOException {
        itemService.updateItem(item);
        //请求转发到itemList.action
        return "forward:itemList.action";//请求转发,可以带上request参数
    }

    其实,forward方式相当于request.getRequestDispatcher(…).forward(request,response),请求转发后浏览器地址栏还是原来的地址。请求转发并没有执行新的request和response,而是和转发前的请求共用一个request和response。所以转发前请求的参数在转发后仍然可以读取到。

    SpringMVC在处理请求过程中出现的异常可以交由异常处理器进行处理,而自定义异常处理器可以实现一个系统的异常处理逻辑。

    异常处理思路

    我们知道,系统中出现的异常包括两类:预期异常和运行时异常(RuntimeException),前者通过捕获异常从而获取异常信息,后者主要通过规范代码开发、测试,通过手段来减少运行时异常的发生。系统中的dao、service、controller这些层出现异常都可以通过throws Exception向上抛出异常,最后由SpringMVC前端控制器交由异常处理器进行异常处理,SpringMVC提供全局异常处理器(一个系统应只有一个全局异常处理器)进行统一异常处理,如下图所示。

    全局异常处理器案例

    下面我会编写一个全局异常处理器,来处理所有没有处理过的运行时异常,这样可以更加友好地给用户提示信息。

    自定义异常类

    为了区别不同的异常,通常要根据异常类型自定义异常类,这里我们创建一个自定义系统异常,如果controller、service、dao这些层抛出此类异常,那么说明这就是系统预期处理的异常。我们可在工程的com.meimeixia.springmvc.exception包下创建一个自定义异常类,例如MyException.java。

    自定义异常处理器

    全局异常处理器处理异常的思路如下:

    1. 解析出异常类型;
    2. 如果该异常类型是系统自定义的异常,直接取出异常信息,在错误页面展示;
    3. 如果该异常类型不是系统自定义的异常,那么应取出错误的堆栈信息,并记录下来,在错误页面展示。

    SpringMVC提供了一个HandlerExceptionResolver接口,自定义全局异常处理器必须要实现这个接口,所以我们可在com.meimeixia.springmvc.exception包下创建一个自定义全局异常处理器。

    public class CustomerExceptionResolver implements HandlerExceptionResolver {
    
        @Override
        public ModelAndView resolveException(HttpServletRequest request, HttpServletResponse response, Object handler,
                Exception ex) {
            //默认响应给用户的消息
            String result = "系统发生异常了,请联系管理员!";
            //自定义异常处理
            //判断异常的种类
            if (ex instanceof MyException) {
                //如果是自定义异常,就从异常里面取出错误消息
                result = ((MyException) ex).getMsg();
            } else {
                //如果是运行时异常,则取出错误的堆栈信息
                ex.printStackTrace();//向控制台上打印堆栈信息
                
                StringWriter s = new StringWriter();
                PrintWriter printWriter = new PrintWriter(s);
                ex.printStackTrace(printWriter);
                result = s.toString();
            }
            
            //写日志、发短信或者发邮件
            //在此省略这一步......
            
            //返回一个友好的错误页面,并显示错误消息
            ModelAndView mav = new ModelAndView();
            mav.addObject("msg", result);
            mav.setViewName("msg");
            return mav;
        }
    
    }

    记得还要在/WEB-INF/jsp目录下提供一个提示信息显示页面,即msg.jsp。

    <%@ page language="java" contentType="text/html; charset=UTF-8"
        pageEncoding="UTF-8"%>
    <!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN" "http://www.w3.org/TR/html4/loose.dtd">
    <html>
    <head>
    <meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
    <title>温馨提示</title>
    </head>
    <body>
        ${msg }
    </body>
    </html>

    配置异常处理器

    全局异常处理器中的逻辑很清楚,我就不再多说了,然后我们就要在springmvc.xml配置文件中配置这个自定义的异常处理器了。

    <!-- 配置全局异常处理器,且全局异常处理器只能有一个 -->
    <bean class="com.meimeixia.springmvc.exception.CustomerExceptionResolver" />

    这一讲主要来讲一下SpringMVC是如何实现文件上传的,但请注意这里我只讲单个文件的上传。

    环境准备

    SpringMVC要想实现上传文件,就得需要两个jar包的支持,如下:

    工程中肯定要导入以上两个jar包,因为CommonsMultipartResolver解析器要依赖commons-fileupload和commons-io这两个jar包。

    单个文件的上传

    前台页面

    我们要改造一下itemEdit.jsp页面,主要是在form表单中添加商品图片一栏,效果我截图如下:

     注意一点的是form表单中别忘了写enctype="multipart/form-data"属性。

    对多部件类型multipart解析

    意思就是说针对上面的enctype="multipart/form-data"类型,SpringMVC需要对multipart类型的数据进行解析,在springmvc.xml配置文件中配置multipart类型解析器即可。

    <!-- 配置多媒体文件解析器 -->
    <!-- 注意,这里id必须填写为multipartResolver -->
    <bean id="multipartResolver" class="org.springframework.web.multipart.commons.CommonsMultipartResolver">
        <!-- 最大上传文件大小(即上传文件不能超过8M) -->
        <property name="maxUploadSize" value="8388608" />
    </bean>

    创建文件保存的虚拟目录

    在上传文件之前,首先要创建一个虚拟目录来保存文件,这个虚拟目录会对应磁盘上的一个实际的目录,在实际开发中肯定会有一个服务器专门存储资源的,但在这里我们就用本地磁盘来保存文件,然后映射一个虚拟目录,用来在程序中指定获取文件的路径(其实上面前台页面itemEdit.jsp中,那个src="/pic/${item.pic}"中的/pic就是虚拟目录)。
    创建一个虚拟目录来保存文件一共有两种方法:一种方法是在Eclipse中双击Tomcat服务器,就会弹出下面的框框。

     选择Modules,然后点击【Add External Web Module…】,在弹出的窗口中进行如下操作,都在图里面了

     设置好之后保存即可,这样上传的文件都会保存到Document base指定的目录中,相当于虚拟映射到Path指定的目录中,程序中获取这个文件,要从Path指定的虚拟目录中获取,即我上面写的/pic。这里,我举一个例子,比方说将一个图片(例如1.jpg)拷贝到Document base指定的目录中,即F: empimages目录中,然后启动Tomcat服务器,在浏览器地址栏中输入http://localhost:8080/pic/1.jpg这样的url访问地址即可访问到该图片。
    第二种方法就是在Tomcat服务器的配置文件(即server.xml)中配置一下,其实刚刚在Eclipse中的操作已经自动写到这个配置文件中了,看一下该文件会发现里面会多了一行。

     这就是刚刚我配置的,它自动写到server.xml这个文件中了,所以我们也可以直接在该配置文件中进行配置,这样就不需要在Eclipse中配置了。

    编写后台Controller方法

    接下来就是重点了,前台传过来的文件,我们需要在ItemController类中进行处理,然后保存到磁盘中,同时也就映射到了我们配置的虚拟路径中了,那么如何接收前台传过来的文件呢?看下面的代码:

    @RequestMapping(value="updateItem", method={RequestMethod.POST, RequestMethod.GET})
    public String updateItem(Item item, MultipartFile pictureFile, Model model) throws Exception {
        //保存文件(图片)时,这个文件(图片)有的时候文件名可能会重复,你保存多了会把原来的文件(图片)给覆盖掉,这就不太合适了,所以可以为每个文件(图片)生成一个新的文件名
        //新的文件(图片)名字
        String newName = UUID.randomUUID().toString();
        //上传文件(图片)的原来的名字
        String oldName = pictureFile.getOriginalFilename();
        //截取文件(图片)的后缀,从.开始截取
        String sux = oldName.substring(oldName.lastIndexOf("."));
        //新建本地文件流
        File file = new File("F:\temp\images\" + newName + sux);
        //写入本地磁盘
        pictureFile.transferTo(file);//transferTo方法可以将上传的文件(图片)写入本地磁盘
        
        //保存文件(图片)到数据库
        item.setPic(newName + sux);
        
        itemService.updateItem(item);
        model.addAttribute("item", item);
        model.addAttribute("msg", "修改商品信息成功");
        return "itemEdit";
    }

    注意,考虑到实际情况,保存文件(图片)时一般不只你一个人保存,那么这个文件(图片)有的时候文件名可能会重复,你保存多了会把原来的文件(图片)给覆盖掉,这就不太合适了,所以需要使用UUID算法为每个文件(图片)生成一个新的文件名。
    然后,来看一下updateItem方法中的形参,主要有Item、Model以及MultipartFile类型的pictureFile,我这里上传的图片是Item类中的一个属性,所以有了这个形参,那么就可以写到该类中。SpringMVC文件上传的类是MultipartFile,参数名称pictureFile必须和前台页面itemEdit.jsp中的name属性一致才行。上传图片成功之后的效果类似于下面这样。

    最后总结一下,我上传文件的逻辑可能判断不严谨,如果说要更加严谨一点的话,上传文件的逻辑就应是这样的:首先判断有没有上传文件,如果上传了,那么对文件重新命名然后写到磁盘中。如果没有上传文件,那么我应该还是用原来的文件(图片)。读者如果有兴趣可以参考下面的代码。

    @RequestMapping(value="/updateItem",method={RequestMethod.POST,RequestMethod.GET})
    public String updateItem(Item item, MultipartFile pictureFile) throws Exception {
    
        // 处理上传的单个图片    
        String originalFileName = pictureFile.getOriginalFilename();// 原始名称
        // 上传图片
        if (pictureFile != null && originalFileName != null && originalFileName.length() > 0) {
            // 存储图片的物理路径,实际中是要写到配置文件中的,不能在这写死
            String pic_path = "F:\temp\images\";
            // 新的图片名称
            String newFileName = UUID.randomUUID()
                    + originalFileName.substring(originalFileName
                            .lastIndexOf("."));     
            File newFile = new File(pic_path + newFileName);//新图片   
            pictureFile.transferTo(newFile);// 将内存中的数据写入磁盘
            item.setPic(newFileName);// 将新图片名称写到itemsCustom中
        } else {
            //如果用户没有选择图片就上传了,还用原来的图片
            Item temp = itemsService.findItemsById(item.getId());
            item.setPic(temp.getPic());
        }
    
        // 调用service更新商品信息,页面需要将商品信息传到此方法
        itemService.updateItem(item);
        return "forward:itemEdit.action";
    }

    SpringMVC快速入门第十讲——SpringMVC与前台的json数据交互

    json数据格式在接口调用和html页面中比较常用,json格式比较简单,解析也比较方便,所以使用很普遍。在SpringMVC中,也支持对json格式数据的解析和转换,这篇文章主要介绍在SpringMVC中如何和前台交互json数据。

    两种交互形式

    SpringMVC和前台交互主要有两种形式,如下图所示。

     从上图可以看出,前台传过来的方式有两种,一种是传json格式的数据过来,另一种就是在url请求的末尾传普通的key/value串过来,针对这两种方式,在Controller类中会有不同的解析,但是在Controller类中返回的json格式的数据都是一样的。下面来具体介绍一下SpringMVC是如何与前台进行json数据的交互的,在讲之前先认识两个注解。

    @RequestBody注解

    @RequestBody注解用于读取http请求的内容(字符串),通过SpringMVC提供的HttpMessageConverter接口将读到的内容转换为json、xml等格式的数据并绑定到Controller类方法的参数上。
    例如,@RequestBody注解实现接收http请求的json格式的数据,将json格式的数据转换为Java对象。

    @ResponseBody注解

    @ResponseBody注解用于将Controller类方法返回的对象通过HttpMessageConverter接口转换为指定格式(如json、xml等)的数据,并通过response响应给客户端。
    例如,@ResponseBody注解将Controller类方法返回的对象转换为json格式的数据,并响应给客户端。

    SpringMVC与前台的json数据交互

    导入与json交互的jar包

    SpringMVC默认使用MappingJacksonHttpMessageConverter对json格式的数据进行转换,所以需要加入jackson的包。这里我使用的是SpringMVC4(注意,SpringMVC3和SpringMVC4针对json交互的jar包有所区别),所以需要在工程中导入如下三个jar包。

    json交互的测试

    这里,我使用jQuery的ajax来提交json串,对输出的json串结果进行解析。前台的程序如下:

    <button onclick="sendJson()">json数据交互测试</button>
    <script type="text/javascript">
        function sendJson() {
            $.ajax({
                type:"post",
                url:"${pageContext.request.contextPath }/item/json_test.action",
                data:'{"id":"1","name":"电冰箱","price":"1999"}',
                contentType:"application/json;charset=utf-8",
                success:function(data) {
                    alert(data.id + ":" + data.name);
                }
            });
        }
    </script>

    然后,编辑ItemController类,并在该类中编写一个如下方法。

    //json数据交互
    //@RequestBody注解接收json格式的数据,并可以将其转换成pojo对象
    //@ResponseBody注解用于响应json格式的数据,即把pojo对象转换成json格式的数据并响应给客户端
    @RequestMapping("json_test")
    @ResponseBody
    public Item jsonTest(@RequestBody Item itemIn) {
        System.out.println(itemIn);
        itemIn.setName("Apple手机");
        return itemIn;
    }

    由于前台传的是id、name以及price三个属性,所以在后台就用Item类来接收了,这个类中也有这三个属性。重点是@RequestBody注解,它是将前台传过来的json串转换成Item对象,然后再将该对象return回去,通过@ResponseBody注解将Item对象转成json格式的数据返回给前台。这样前台接收到了后就可以解析了。我们看一下测试的结果:

    SpringMVC快速入门第十一讲——SpringMVC实现Resultful服务

    Restful就是一个资源定位及资源操作的风格,不是标准也不是协议,只是一种风格,是对http协议的诠释。

    • 资源定位:互联网所有的事物都可以抽象为资源,Restful风格的请求url中没有动词,只有名词,也没有参数,就像下面这样:

    https://liayun.blog.csdn.net/article/details/72514034
    • 资源操作:使用put、delete、post、get等不同方法对资源进行操作,分别对应添加、删除、修改以及查询。一般使用时还是以post和get为主,put和delete几乎使用不到。

    例如,现在有这样一个需求:使用Restful方式实现商品信息查询。为了解决该需求,我们可以在ItemController类中添加一个如下方法。

    @RequestMapping("item/{id}") //{}就是一个占位符
    public String itemQuery(@PathVariable("id") Integer ids, Model model) {
        //查询商品信息
        Item item = itemService.getItemById(ids);
        //model返回数据模型(数据模型通过Model返回)
        model.addAttribute("item", item);
        return "itemEdit";
    }

    @RequestMapping("item/{id}")注解中的{}表示占位符,所以请求的url可以是/item/1或者/item/2,继而可以在方法中使用@PathVariable注解来获取{}中的变量。@PathVariable注解用于将请求url中的模板变量映射到功能处理方法的参数上,也就是说当占位符参数名与方法参数名不一致时,可以通过@PathVariable注解来绑定。如果@RequestMapping注解中value属性的值为/item/{id},并且id和形参名称一致,那么@PathVariable注解中的内容就可以不用写了,即直接写为@PathVariable即可。
    除此之外,还要在前端控制器中针对Restful风格的请求进行配置,即将web.xml文件中的前端控制器配置改造成下面这个样子。

    除此之外,还要在前端控制器中针对Restful风格的请求进行配置,即将web.xml文件中的前端控制器配置改造成下面这个样子。

    <!-- 配置前端控制器 -->
    <servlet>
        <servlet-name>springmvc-web</servlet-name>
        <servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>
        <init-param>
            <!-- 指定SpringMVC配置文件的路径。如果不指定,默认为/WEB-INF/${servlet-name}-servlet.xml -->
            <param-name>contextConfigLocation</param-name>
            <param-value>classpath:spring/springmvc.xml</param-value>
        </init-param>
    </servlet>
    <servlet-mapping>
        <servlet-name>springmvc-web</servlet-name>
        <url-pattern>*.action</url-pattern>
    </servlet-mapping>
    <servlet-mapping>
        <servlet-name>springmvc-web</servlet-name>
        <!-- /:拦截所有请求,但是不包括jsp。/*:拦截所有请求,包括jsp,这里应该配置为"/" -->
        <url-pattern>/</url-pattern>
    </servlet-mapping>

    在SpingMVC框架中,/即表示拦截所有请求,但不拦截jsp,/*即表示所有的都拦截,包括jsp,这里显然应该配置/,因为你想啊!假如说Controller类方法里面已经把数据处理完了,要跳转到jsp页面,如果你配置的是/*,这个jsp页面的跳转也会被拦截,那么就会报404错误了。
    最后,为了测试,可以发布咱们的项目到Tomcat服务器上,并启动Tomcat服务器,在Google Chrome浏览器地址栏中输入http://localhost:8080/04-springmvc-mybatis/item/1这样的url地址进行访问,这时你就能看到如下图所示的效果了。

    但是这里有个问题,使用上面的配置后会拦截所有的请求(虽说不包括jsp),那么当然也会对静态资源进行拦截了,如此一来,DispatcherServlet也会解析静态资源,所以就会出错了。因此,我们要设置一下不让其解析静态资源,而SpringMVC中的<mvc:resources mapping="" location="" />标签可以实现对静态资源进行映射访问。如下是对js文件的访问配置:

    <mvc:resources location="/js/" mapping="/js/**"/>

    如果说我们把js文件夹放到工程的WEB-INF目录下,照理来说外界肯定是无法访问到的,但是我们非得访问呢?这时就可通过<mvc:resources mapping="" location="" />标签来对它进行映射访问了,如下图所示。

    SpringMVC快速入门第十二讲——SpringMVC中的拦截器

    SpringMVC中的处理器拦截器类似于Servlet开发中的Filter过滤器,用于对处理器进行预处理和后处理。本文主要介绍SpringMVC中的拦截器是如何定义的,以及测试拦截器的执行情况和使用方法。

    SpringMVC中拦截器的定义和配置

    SpringMVC中拦截器的定义

    在SpringMVC中,自定义拦截器需要实现HandlerInterceptor接口,并实现该接口中提供的三个方法。

    public class MyInterceptor implements HandlerInterceptor {
    
        /**
         * 该方法执行的时机是在Handler执行之后,在返回ModelAndView之后。注意,只要拦截器放行了,那么该方法就会被执行
         * 
         * 适用场景:Handler执行过程中出现异常,可以在此处理异常,或者是清理资源,或者是记录日志等等
         */
        @Override
        public void afterCompletion(HttpServletRequest arg0, HttpServletResponse arg1, Object arg2, Exception arg3)
                throws Exception {
            System.out.println("MyInterceptor.afterCompletion......");
        }
    
        /**
         * 该方法执行的时机是在Handler执行之后,在返回ModelAndView之前
         * 
         * 适用场景:可以对ModelAndView做些手脚,例如设置页面的共用参数等等
         */
        @Override
        public void postHandle(HttpServletRequest arg0, HttpServletResponse arg1, Object arg2, ModelAndView arg3)
                throws Exception {
            System.out.println("MyInterceptor.postHandle......");
        }
    
        /**
         * 该方法执行的时机是在Controller类方法执行之前,也就是说是在Handler执行之前
         * 
         * 返回值:
         *         如果返回true,那么就放行,不拦截,拦截器放行之后,就会处理Controller类里面的方法,正常执行Handler
         *         如果返回false,那么就拦截,Handler就不能被正常执行了
         * 
         * 适用场景:进行登录拦截或者权限校验等等
         */
        @Override
        public boolean preHandle(HttpServletRequest arg0, HttpServletResponse arg1, Object arg2) throws Exception {
            System.out.println("MyInterceptor.preHandle......");
            //true:放行,false:拦截
            return true;//拦截器放行之后,就会处理Controller类里面的方法,正常执行Handler
        }
    
    }

    针对这三个方法,我做一下简单的分析:

    1. preHandle方法:该方法执行的时机是在Controller类方法执行之前,也就是说是在Handler执行之前。其应用场景:可以用于身份认证、身份授权等,比如如果认证没有通过表示用户没有登录,那么需要此方法拦截到不再往下执行(return false),否则就放行(return true);
    2. postHandle方法:该方法执行的时机是在Handler执行之后,在返回ModelAndView之前,可以看到该方法中有个ModelAndView类型的形参。其应用场景:从ModelAndView出发,将公用的模型数据(比如菜单导航之类的)在这里传到视图,也可以在这里统一指定视图;
    3. afterCompletion方法:该方法执行的时机是在Handler执行之后,在返回ModelAndView之后。其应用场景:统一异常处理(即Handler执行过程中出现异常,可以在此处理异常)、统一日志处理等。

    SpringMVC中拦截器的配置

    针对某种HandlerMapping配置拦截器

    在SpringMVC中,拦截器是针对具体的HandlerMapping进行配置的,也就是说如果在某个HandlerMapping中配置拦截,经过该HandlerMapping映射成功的Handler最终会使用该拦截器。假设如果我们在springmvc.xml配置文件中配置的映射器是org.springframework.web.servlet.handler.BeanNameUrlHandlerMapping,那么我们可以像下面这样来配置拦截器。

    <bean class="org.springframework.web.servlet.handler.BeanNameUrlHandlerMapping">
        <property name="interceptors">
            <list>
                <ref bean="handlerInterceptor1"/>
                <ref bean="handlerInterceptor2"/>
            </list>
        </property>
    </bean>
    <bean id="handlerInterceptor1" class="com.meimeixia.springmvc.interceptor.MyInterceptor"/>
    <bean id="handlerInterceptor2" class="com.meimeixia.springmvc.interceptor.MyInterceptor2"/>

    针对所有HandlerMapping配置全局拦截器

    在SpringMVC中,如何配置类似于全局的拦截器呢?上面也说了,SpringMVC中的拦截器是针对具体的映射器而言的,为了解决这个问题,SpringMVC框架将配置的类似全局的拦截器注入到每个HandlerMapping中,这样就可以成为全局的拦截器了。配置如下所示:

    <!-- 配置拦截器 -->
    <mvc:interceptors>
        <!-- 按照配置的顺序执行拦截器 -->
        <mvc:interceptor>
            <!-- /**:拦截所有请求,包括二级以上目录;/*:拦截所有请求,但不包括二级以上目录 -->
            <mvc:mapping path="/**"/>
            <bean class="com.meimeixia.springmvc.interceptor.MyInterceptor" />
        </mvc:interceptor>
        <mvc:interceptor>
            <!-- /**:拦截所有请求,包括二级以上目录;/*:拦截所有请求,但不包括二级以上目录 -->
            <mvc:mapping path="/**"/>
            <bean class="com.meimeixia.springmvc.interceptor.MyInterceptor2" />
        </mvc:interceptor>
    </mvc:interceptors>

    温馨提示:path="/**"表示拦截所有的请求url(包括子url路径)。在实际开发中,一般我们都用这种配置,在<mvc:mapping>标签中指定要拦截的url。

    SpringMVC拦截器的执行测试

    仿照上面的MyInterceptor再写一个拦截器,即MyInterceptor2,配置是按照上面的配置来的,然后我们就来测试一下这两个拦截器的执行情况,并做相关总结。

    两个拦截器都放行

    也就是说,我们将两个拦截器的preHandle方法中的返回值都改成true,来测试一下拦截器的执行情况,访问任何一个url,测试结果都将是下面这样子的。

    有一个拦截器不放行

    我们将第二个拦截器的preHandle方法中的返回值改成false,前一个还是true,来测试一下拦截器的执行情况,访问任何一个url,测试结果都将是下面这样子的。

    根据打印的结果,我们可以做一个总结:

    1. 由于拦截器1放行,所以拦截器2的preHandle方法才能执行。也就是说前面的拦截器放行了,后面的拦截器才能执行preHandle方法;
    2. 拦截器2不放行,所以其另外两个方法没有被执行。即如果某个拦截器不放行,那么它的另外两个方法就不会被执行;
    3. 只要有一个拦截器不放行,所有拦截器的postHandle方法都不会执行,但是只要执行过preHandle方法并且放行的,就会执行afterCompletion方法。

    两个拦截器都不放行

    这种情况其实可以参考上面的情况了,是一个特例,也看一下拦截器的执行情况。

    总结

    1. preHandle方法是按拦截器定义顺序调用的;
    2. postHandler方法是按拦截器定义逆序调用的;
    3. afterCompletion方法也是按拦截器定义逆序调用;
    4. postHandler方法在拦截器链内所有拦截器返回true才调用;
    5. afterCompletion方法只有在preHandle方法返回true时才调用。

    拦截器的应用

    从第二种情况来看,比如现在要写一个统一异常处理的逻辑,那么要将该拦截器放在拦截器链的第一个位置,且一定要放行,因为只有放行了,才会去执行afterCompletion方法,而且放在拦截器链的第一个位置的话,afterCompletion方法会最后执行,这样才能在里面执行统一异常处理的逻辑。再比如,登录认证拦截器要放在拦截器链接中的第一个位置(如果有统一异常处理,那么应该放在统一异常处理的后面);权限校验拦截器要放在登录认证拦截器之后(因为只有登录通过后才进行权限校验)。但在这里我就只以登录验证的拦截器为例来说明如何使用SpringMVC的拦截器。

    处理流程

    1. 先得有一个登录页面,然后需要编写一个Controller类来访问该页面;
    2. 登录页面中有一个提交表单的动作,也即需要在Controller类中进行处理:
      • a) 判断用户名密码是否正确;
      • b) 如果正确,向Session对象中写入用户信息;
      • c) 返回登录成功页面,或者跳转到商品列表页面。
    3. 拦截器的处理:
      • a) 拦截用户请求,判断用户是否登录;
      • b) 如果用户已经登录,则放行;
      • c) 如果用户未登录,则跳转到登录页面。
  • 相关阅读:
    MySQL的事务用法:
    MySQL基本命令(增删改查,check约束)总结:
    (转)nodejs npm国内镜像
    NodeJS学习笔记(三)
    NodeJS学习笔记(二).js
    minimist的用法
    npm link没有效果的问题
    DOM Based XSS介绍
    NodeJS学习笔记(一)
    用Ruby写自动生成Sql语句脚本遇到的问题
  • 原文地址:https://www.cnblogs.com/baldprogrammer/p/13599350.html
Copyright © 2020-2023  润新知