SpringMVC

Starter

依赖包

在加入Spring依赖包的基础上,加入:

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

额外包:

<!-- FileUpload -->
<dependency>
    <groupId>commons-fileupload</groupId>
    <artifactId>commons-fileupload</artifactId>
</dependency>
<dependency>
    <groupId>commons-io</groupId>
    <artifactId>commons-io</artifactId>
</dependency>

<!-- JSTL,Taglib -->
<dependency>
    <groupId>jstl</groupId>
    <artifactId>jstl</artifactId>
</dependency>
<dependency>
    <groupId>taglibs</groupId>
    <artifactId>standard</artifactId>
</dependency> 

<!-- Test-->
<dependency>
    <groupId>junit</groupId>
    <artifactId>junit</artifactId>
</dependency>
<dependency>
    <groupId>xmlunit</groupId>
    <artifactId>xmlunit</artifactId>
</dependency>

<dependency>
    <groupId>org.springframework</groupId>
    <artifactId>spring-test</artifactId>
</dependency>

<dependency>
    <groupId>org.mockito</groupId>
    <artifactId>mockito-all</artifactId>
</dependency>
<dependency>
    <groupId>org.mockito</groupId>
    <artifactId>mockito-core</artifactId>
</dependency>

使用

  1. 配置web.xml (在配置了Spring的基础上)
     <!-- 配置加载DispatchServlet 拦截请求-->
     <servlet>
         <servlet-name>xxx</servlet-name>
         <servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>
         <load-on-startup>1</load-on-startup>
     </servlet>
     <servlet-mapping>
         <servlet-name>xxx</servlet-name>
         <url-pattern>/</url-pattern>
     </servlet-mapping>
    
  2. 配置xxx-servlet.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:mvc="http://www.springframework.org/schema/mvc"
         xmlns:context="http://www.springframework.org/schema/context"
         xsi:schemaLocation="http://www.springframework.org/schema/mvc http://www.springframework.org/schema/mvc/spring-mvc-3.1.xsd
             http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd
             http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context-3.1.xsd">
    
         <mvc:annotation-driven/>
         <!-- find controller and registered as beans-->
         <context:component-scan base-package="com.mytest.controller"/>
    
         <!-- PS: 若使用@ControllerAdvice,需如下配置
         <context:component-scan base-package="com.mytest.controller" use-default-filters="false">
             <context:include-filter type="annotation" expression="org.springframework.stereotype.Controller"  />
             <context:include-filter type="annotation" expression="org.springframework.web.bind.annotation.ControllerAdvice"/>
         </context:component-scan>
         -->
    
         <!-- Handle requests for static resources -->
         <mvc:resources location="/resources/" mapping="/resources/**"/>
    
         <!-- use InternalResourceViewResolver to resolve internal view:
             prefix + logical view name + suffix
         -->
         <bean class="org.springframework.web.servlet.view.InternalResourceViewResolver">
             <property name="prefix" value="/WEB-INF/user/"/>
             <property name="suffix" value=".jsp"/>
         </bean>
    
         <!-- for file uploads -->
         <bean id="multipartResolver" class="org.springframework.web.multipart.commons.CommonsMultipartResolver">
             <property name="maxUploadSize" value="1000000"/>
         </bean>
    
     </beans>
    
  3. 在Spring配置文件(beans.xml)中去除加载Controller(避免重复)

     <context:component-scan base-package="com.mytest" >
         <context:exclude-filter type="regex" expression="com.mytest.controller.*"/>
     </context:component-scan>
    
  4. Controller

     @Controller
     @RequestMapping(value={"/users"})
     @SessionAttributes("loginUser")
     public class UserController
     {
         private UserService userService;
         public UserService getUserService(){return this.userService;}
         @Inject
         public void setUserService(UserService userService){this.userService=userService;}
    
         // List Users
         @RequestMapping(value={"/"})
         public String list(Model model)
         {
             model.addAttribute("users",userService.list());
             return "list";
         }
    
         // get user details by id
         @RequestMapping(value={"/{id}"},method=RequestMethod.GET)
         public String details(@PathVariable int id,Model model)
         {
             User user=userService.get(id);
             if(user!=null)
                 model.addAttribute(user);
             return "create";
         }
    
         //create user & file upload
         @RequestMapping(value={"/"},method=RequestMethod.POST)
         public String create(@Valid User user,BindingResult binding, @RequestParam(value="photos",required=false) MultipartFile[] photos,HttpServletRequest req)
                 throws IOException
         {
             if(binding.hasErrors())
                 return "create";
    
             //file upload
             String realpath = req.getSession().getServletContext().getRealPath("/resources/upload/");
             for(MultipartFile photo : photos)
             {
                 if(photo.isEmpty())
                     continue;
                 FileUtils.copyInputStreamToFile(photo.getInputStream(), new File(realpath+"/"+photo.getOriginalFilename()));
             }
    
             //save user
             userService.save(user)
             return "redirect:list";
         }
    
         // login
         @RequestMapping(value={"/login"},method=RequestMethod.POST)
         public String login(String username,String password,Model model)
         {
             User user=userService.check(username,password);
             if(user!=null){
                 model.addAttribute("loginUser",user);
                 //return InternalResourceViewResolver.REDIRECT_URL_PREFIX+"/users"
                 return "redirect:list";
             }
             throw new UserException("Login Fail");
             //return "login";
         }
    
         @ExceptionHandler(value={UserException.class})
         public String handlerUserException(Exception ex,HttpServletRequest req)
         {
             req.setAttribute("ex",ex.getMessage());
             return "error";
         }
     }
    
  5. JSP

    • WEB_INF/user/list.jsp 部分代码:

      <!-- 在页面中引入:-->
      <%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %>
      
      <!-- 显示登录用户-->
      <h2>Welcome : ${loginUser.username}<h2>
      
      <!-- Menu Link -->
      <a href="create">Create User</a> | <a href="login">Login User</a>
      
      <!-- 循环遍历 model:users-->
      <h2>User List</h2>
      <table>
      <tr>
      <td>Id</td>
      <td>userName</td>
      <td>nickName</td>
      <td>password</td>
      <td>email</td>
      <td>Opera</td>
      </tr>
      <c:forEach items="${users}"  var="u">
      <tr>
        <td>${u.value.id}</td>
        <td>${u.value.username }</td>
        <td>${u.value.nickname }</td>
        <td>${u.value.password }</td>
        <td>${u.value.email }</td>
        <td><a href="${u.key}">Details</a></td>
      </tr>
      </c:forEach>
      </table>
      
    • WEB-INF/user/create.jsp 部分代码:

      <!-- 页面中引入spring form:-->
      <%@ taglib prefix="sf" uri="http://www.springframework.org/tags/form" %>
      
      <h2>Create User</h2>
      <sf:form method="post" modelAttribute="user" enctype="multipart/form-data">
        UserName:<sf:input path="username"/><sf:errors path="username"/>
        NickName:<sf:input path="nickname"/><sf:errors path="nickname"/>
        Password:<sf:password path="password"/><sf:errors path="password"/>
        Email:</td><td><sf:input path="email"/><sf:errors path="email"/>
        photo1:<input type="file" name="photos"/>
        photo2:<input type="file" name="photos"/>
        <sf:button type="reset">Reset</sf:button>
        <sf:button type="submit" >Submit</sf:button>
      </sf:form>
      
    • WEB-INF/user/login.jsp部分代码:

      <h2>User Login</h2>
      <form method="post" >
        UserName:<input name="username" type="text"/>
        Password:<input type="password" name="password"/>
        <button type="reset">Reset</button>
        <button type="submit">Submit</button>
      </form>
      
    • WEB-INF/user/error.jsp部分代码:

      <body>${ex}</body>
      
  6. 测试

     @RunWith(SpringJUnit4ClassRunner.class)
     @WebAppConfiguration
     @ContextConfiguration({"classpath:beans.xml","file:src/main/webapp/WEB-INF/mytest-servlet.xml"})
     public class MyControllerTest
     {
         private MockMvc mockMvc;
         protected MockHttpSession mockSession;
         @Inject
         private WebApplicationContext wac;
    
         @Before
         public void setup()
         {
             MockitoAnnotations.initMocks(this);
             mockMvc =MockMvcBuilders.webAppContextSetup(wac).build();
             mockSession=new MockHttpSession();
         }
    
         @Test
         public void testList() throws Exception{
             MvcResult result = mockMvc.perform(MockMvcRequestBuilders.get("/users"))
                 .andExpect(MockMvcResultMatchers.view().name("list"))
                 .andExpect(MockMvcResultMatchers.model().attributeExists("users"))
                 .andDo(MockMvcResultHandlers.print())
                 .andReturn();
             printMvcResult(result);
             Assert.assertNotNull(result.getModelAndView().getModel().get("users"));
         }
    
         private void printMvcResult(MvcResult result) throws IOException
         {
             MockHttpServletRequest request= result.getRequest();
             MockHttpServletResponse response= result.getResponse();
             //ModelAndView  mv=getModelAndView() //得到执行后的ModelAndView
    
             System.out.println("Request:");
             System.out.println("URI:"+request.getRequestURI());
             System.out.println("URL:"+request.getRequestURL());
             System.out.println("Method:"+request.getMethod()+",ContentType:"+request.getContentType());
     //System.out.println("InputStream:"+IOUtils.toString(request.getInputStream()));
             System.out.println("Response:");
             System.out.println("Status:"+response.getStatus()+" ErrorMessage:"+response.getErrorMessage());
             System.out.println("content:"+response.getContentAsString());
             System.out.println("contentType:"+response.getContentType());
         }
     }
    

处理流程

Spring MVC

Request会由DispatcherServlet分配给具体Controller(由HandlerMapping根据RequestURL来确定)。Controller完成处理后,Request会被发送给一个View(由ViewResolver确定)来呈现输出结果

具体过程:

  1. Request发送,由DispatcherServlet拦截
  2. 将Request交给HandlerMapping,HandlerMapping根据URL决策使用哪个Controller来处理
  3. 将Request发送给选定的Controller完成逻辑处理
  4. 将需要Client显示的信息打包成Model,并标志出处用于渲染输出的视图名称(即逻辑逻辑视图名)发送回DispatcherServlet
  5. ViewResolver根据逻辑视图名确定真正的视图实现(View)
  6. 视图将Model渲染输出,通过Response传递回Client

当要为用户展现信息时,SpringMVC可以使用视图解析器选择合适的视图:

View resolver Description
BeanNameViewResolver Finds an implementation of View that’s registered as a whose ID is the same as the logical view name.
ContentNegotiatingViewResolver Delegates to one or more other view resolvers, the choice of which is based on the content type being requested.
FreeMarkerViewResolver Finds a FreeMarker-based template whose path is determined by prefixing and suffixing the logical view name.
InternalResourceViewResolver Finds a view template contained within the web application’s WAR file. The path to the view template is derived by prefixing and suffixing the logical view name.
JasperReportsViewResolver Finds a view defined as a Jasper Reports report file whose path is derived by prefixing and suffixing the logical view name.
ResourceBundleViewResolver Looks up View implementations from a properties file.
TilesViewResolver Looks up a view that is defined as a Tiles template. The name of the template is the same as the logical view name.
UrlBasedViewResolver This is the base class for some of the other view resolvers, such as InternalResourceViewResolver. It can be used on its own, but it’s not as powerful as its sub-classes. For example, UrlBasedViewResolver is unable to resolve views based on the current locale.
VelocityLayoutViewResolver This is a subclass of VelocityViewResolver that supports page composition via Spring’s VelocityLayoutView (a view implementation that emulates Velocity’s VelocityLayoutServlet).
VelocityViewResolver Resolves a Velocity-based view where the path of a Velocity template is derived by prefixing and suffixing the logical view name.
XmlViewResolver Finds an implementation of View that’s declared as a in an XML file (/WEB-INF/views.xml). This view resolver is a lot like BeanNameViewResolver except that the view s are declared separately from those for the application’s Spring context.
XsltViewResolver Resolves an XSLT-based view where the path of the XSLT stylesheet is derived by prefixing and suffixing the logical view name.

Restful

URI: 统一资源定位符,每种"资源"对应一个特定的URI(无状态); 所以要获取这个资源,访问它的URI就可以,URI为每一个资源的地址或独一无二的识别符

REST=Representational State Transfer(表现层状态转化)

  • Representional:客户端和服务器之间,通过HTTP请求的头信息(Accept,Content-Type)指定"资源"的表现形式,例如:xml,json,html
  • State Transfer:客户端通过HTTP动词(GET,POST,PUT,PUT,DELETE),对服务器端"资源"进行操作,实现"表现层状态转化"
  • transferring the state of resources

REST 中最重要的概念是资源(resources),使用全球 ID(通常使用 URI)标识。 客户端应用程序使用 HTTP 方法(GET/ POST/ PUT/ DELETE)操作资源或资源集。 通常,RESTful Web 服务应该定义以下方面:

  • Web 服务的基/根 URI,比如 http://host/<appcontext>/resources
  • 支持 MIME 类型的响应数据,包括 JSON/XML/ATOM 等等
  • 服务支持的操作集合(例如 POST、GET、PUT 或 DELETE)

Restful

  • 面向资源的设计
  • 是一种风格,而不是标准
  • 其目标是“使延迟和网络交互最小化,同时使组件实现的独立性和扩展性最大化”
  • 最主要驱动是分布性和扩展性

Restless & Restful URL示例:

Restless (Restless)

Restfull (Restful)

HTTP动词:

  • GET(SELECT):从服务器取出资源(一项或多项)。
  • POST(CREATE):在服务器新建一个资源。
  • PUT(UPDATE):在服务器更新资源(客户端提供改变后的完整资源)。
  • PATCH(UPDATE):在服务器更新资源(客户端提供改变的属性)。
  • DELETE(DELETE):从服务器删除资源。
  • HEAD:获取资源的元数据。
  • OPTIONS:获取信息,关于资源的哪些属性是客户端可以改变的

HTTP状态码

Status Codes Description
200 OK - [GET] 服务器成功返回用户请求的数据,该操作是幂等的(Idempotent)
201 CREATED - [POST/PUT/PATCH] 用户新建或修改数据成功
202 Accepted - [*] 表示一个请求已经进入后台排队(异步任务)
204 NO CONTENT - [DELETE] 用户删除数据成功
400 INVALID REQUEST - [POST/PUT/PATCH] 用户发出的请求有错误,服务器没有进行新建或修改数据的操作,该操作是幂等的
401 Unauthorized - [*] 表示用户没有权限(令牌、用户名、密码错误)
403 Forbidden - [*] 表示用户得到授权(与401错误相对),但是访问是被禁止的
404 NOT FOUND - [*] 用户发出的请求针对的是不存在的记录,服务器没有进行操作,该操作是幂等的
406 Not Acceptable - [GET] 用户请求的格式不可得(比如用户请求JSON格式,但是只有XML格式)
410 Gone -[GET] 用户请求的资源被永久删除,且不会再得到的
422 Unprocesable entity - [POST/PUT/PATCH] 当创建一个对象时,发生一个验证错误
500 INTERNAL SERVER ERROR - [*] 服务器发生错误,用户将无法判断发出的请求是否成功

Restful 设计示例:

  • GET /users:列出所有用户
  • POST /users:新建一个用户
  • GET /users/{id}:获取某个指定用户的信息
  • PUT /users/{id}:更新某个指定用户的信息(提供该用户的全部信息)
  • PATCH /users/{id}:更新某个指定用户的信息(提供该用户的部分信息)
  • DELETE /users/{id}:删除某个用户
  • GET /users/{id}/roles:列出某个指定用户的所有角色
  • DELETE /users/{id}/roles/{roleId}:删除某个指定用户的指定角色

使用示例

  1. SpringMVC配置加入Restful (xxx-servlet.xml)

     <bean class="org.springframework.web.servlet.view.ContentNegotiatingViewResolver">
         <property name="mediaTypes">
             <map>
                 <!-- <entry key="html" value="text/html"/> -->
                 <entry key="json" value="application/json"/>
             </map>
         </property>
         <!-- <property name="defaultContentType" value="text/html"/> -->
         <property name="viewResolvers">
             <list>
                 <bean class="org.springframework.web.servlet.view.InternalResourceViewResolver">
                     <property name="viewClass" value="org.springframework.web.servlet.view.JstlView"/>
                     <property name="prefix" value="/WEB-INF/"/>
                     <property name="suffix" value=".jsp"/>
                 </bean> 
             </list>
         </property>
         <property name="defaultViews">
             <list>
                 <bean class="org.springframework.web.servlet.view.json.MappingJacksonJsonView" >
                     <property name="objectMapper">
                         <bean class="org.codehaus.jackson.map.ObjectMapper">
                             <property name="serializationConfig.serializationInclusion">
                                 <value type="org.codehaus.jackson.map.annotate.JsonSerialize.Inclusion">NON_NULL</value>
                             </property> 
                         </bean>
                     </property> 
                 </bean>
             </list>
         </property>
     </bean>
    
  2. Controller

     @Controller
     @RequestMapping("/users")
     public class UserController{
         private SysUserService userService;
         public SysUserService getUserService() {return userService;}
         @Inject
         public void setUserService(SysUserService userService) {this.userService = userService;}
    
         @RequestMapping(method=RequestMethod.GET)
         @ResponseBody
         public Object list(){
             List<User> users=this.userService.list();
             return users;
         }
    
         @RequestMapping(value={"/{id}"},method=RequestMethod.GET)
         @ResponseBody
         public Object getUser(@PathVariable("id") String id){
                 return this.userService.get(id);
         }
    
         @RequestMapping(method=RequestMethod.POST)
         @ResponseBody
         public Object create(@RequestBody User user,HttpServletRequest request){
             return this.userService.save(user);
         }
    
         @RequestMapping(method=RequestMethod.POST)
         @ResponseBody
         public Object upload(@RequestParam(value="file",required=true) MultipartFile file,
                 @RequestParam(value="remark",required=false) String remark) 
                         throws IOException, InvalidFormatException
         {
             System.out.println("ContentType:"+file.getContentType());
             System.out.println("Name:"+file.getName());
             System.out.println("OriginalFilename:"+file.getOriginalFilename());
             System.out.println("Size:"+file.getSize());
             System.out.println("remark:"+remark);
    
             String filename=file.getOriginalFilename();
             int pos=filename.lastIndexOf(".");
             String fileExtesion=(pos!=-1?filename.substring(pos+1):"UnKnow");
    
             String realpath = req.getSession().getServletContext().getRealPath("/resources/uploads/");
             FileUtils.copyInputStreamToFile(file.getInputStream(), new File(realpath+File.separatorChar+fileName));
             return ResponseUtils.sendSuccess(fileName);
         }
     }
    
  3. 测试

     //GET
     MvcResult result=this.mockMvc.perform(MockMvcRequestBuilders.get(url)
                 .session(this.mockSession)
                 .contentType(MediaType.APPLICATION_JSON))
                 .andDo(MockMvcResultHandlers.print())
                 .andExpect(status().isOk())
                 .andExpect(jsonPath("$.success",Matchers.equalTo(true)))
                 .andReturn();
    
     //POST
     User user=new User();
     user.setName("xxx");
     user.setAge(xxx);
    
     ObjectMapper mapper = new ObjectMapper();
     mapper.setSerializationInclusion(Include.NON_NULL);
     byte[] reqBytes=mapper.writeValueAsString(user);
    
     MvcResult result=this.mockMvc.perform(post(url)
                 .contentType(MediaType.APPLICATION_JSON)
                 .content(reqBytes))
                 .andDo(MockMvcResultHandlers.print())
                 .andExpect(status().isOk())
                 //.andExpect(jsonPath("$.success",Matchers.equalTo(true)))
                 .andExpect(jsonPath("$.name",Matchers.equalTo("xxx")))
                 .andReturn();
    

    或使用RestTemplate

     RestTemplate restTemplate = new RestTemplate();
     User[] users = restTemplate.getForObject(SERVER_URI+"/users", User[].class);
     User user = restTemplate.getForObject(SERVER_URI+"/users/{id}", User.class,id);
     User response = restTemplate.postForObject(SERVER_URI+"/users", user, User.class);