GVKun编程网logo

springmvc下的基于token的防重复提交(springmvc防止重复提交)

10

本文将分享springmvc下的基于token的防重复提交的详细内容,并且还将对springmvc防止重复提交进行详尽解释,此外,我们还将为大家带来关于asp.netcoreMVC之实现基于token

本文将分享springmvc下的基于token的防重复提交的详细内容,并且还将对springmvc防止重复提交进行详尽解释,此外,我们还将为大家带来关于asp.net core MVC之实现基于token的认证、java day61【 SpringMVC 的基本概念 、 SpringMVC 的入门 、 请求参数的绑定 、常用注解 】、java day62【 响应数据和结果视图 、 SpringMVC 实现文件上传 、 SpringMVC 中的异常处理 、 SpringMVC 中的拦截器 】、JAVA WEB快速入门之从编写一个基于SpringMVC框架的网站了解Maven、SpringMVC、SpringJDBC的相关知识,希望对你有所帮助。

本文目录一览:

springmvc下的基于token的防重复提交(springmvc防止重复提交)

springmvc下的基于token的防重复提交(springmvc防止重复提交)

问题描述:

现在的网站在注册步骤中,由于后台要处理大量信息,造成响应变慢(测试机器性能差也是造成变慢的一个因素),在前端页面提交信息之前,等待后端响应,此时如果用户
再点一次提交按钮,后台会保存多份用户信息。为解决此问题,借鉴了struts2的token思路,在springmvc下实现token。

实现思路:

在springmvc配置文件中加入拦截器的配置,拦截两类请求,一类是到页面的,一类是提交表单的。当转到页面的请求到来时,生成token的名字和token值,一份放到redis缓存中,一份放传给页面表单的隐藏域。(注:这里之所以使用redis缓存,是因为tomcat服务器是集群部署的,要保证token的存储介质是全局线程安全的,而redis是单线程的)

当表单请求提交时,拦截器得到参数中的tokenName和token,然后到缓存中去取token值,如果能匹配上,请求就通过,不能匹配上就不通过。这里的tokenName生成时也是随机的,每次请求都不一样。而从缓存中取token值时,会立即将其删除(删与读是原子的,无线程安全问题)。


实现方式:

TokenInterceptor.java

package com.xxx.www.common.interceptor;

import java.io.IOException;
import java.util.HashMap;
import java.util.Map;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.apache.log4j.Logger;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.servlet.handler.HandlerInterceptorAdapter;
import com.xxx.cache.redis.IRedisCacheClient;
import com.xxx.common.utility.JsonUtil;
import com.xxx.www.common.utils.TokenHelper;

/**
 * 
 * @see TokenHelper
 */
public class TokenInterceptor extends HandlerInterceptorAdapter
{
    
    private static Logger log = Logger.getLogger(TokenInterceptor.class);
    private static Map<String , String> viewUrls = new HashMap<String , String>();
    private static Map<String , String> actionUrls = new HashMap<String , String>();
    private Object clock = new Object();
    
    @Autowired
    private IRedisCacheClient redisCacheClient;
    static
    {
        viewUrls.put("/user/regc/brandregnamecard/", "GET");
        viewUrls.put("/user/regc/regnamecard/", "GET");
        
        actionUrls.put("/user/regc/brandregnamecard/", "POST");
        actionUrls.put("/user/regc/regnamecard/", "POST");
    }
    {
        TokenHelper.setRedisCacheClient(redisCacheClient);
    }
    
    /**
     * 拦截方法,添加or验证token
     */
    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception
    {
        String url = request.getRequestURI();
        String method = request.getMethod();
        if(viewUrls.keySet().contains(url) && ((viewUrls.get(url)) == null || viewUrls.get(url).equals(method)))
        {
            TokenHelper.setToken(request);
            return true;
        }
        else if(actionUrls.keySet().contains(url) && ((actionUrls.get(url)) == null || actionUrls.get(url).equals(method)))
        {
            log.debug("Intercepting invocation to check for valid transaction token.");
            return handleToken(request, response, handler);
        }
        return true;
    }
    
    protected boolean handleToken(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception
    {
        synchronized(clock)
        {
            if(!TokenHelper.validToken(request))
            {
                System.out.println("未通过验证...");
                return handleInvalidToken(request, response, handler);
            }
        }
        System.out.println("通过验证...");
        return handleValidToken(request, response, handler);
    }
    
    /**
     * 当出现一个非法令牌时调用
     */
    protected boolean handleInvalidToken(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception
    {
        Map<String , Object> data = new HashMap<String , Object>();
        data.put("flag", 0);
        data.put("msg", "请不要频繁操作!");
        writeMessageUtf8(response, data);
        return false;
    }
    
    /**
     * 当发现一个合法令牌时调用.
     */
    protected boolean handleValidToken(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception
    {
        return true;
    }
    
    private void writeMessageUtf8(HttpServletResponse response, Map<String , Object> json) throws IOException
    {
        try
        {
            response.setCharacterEncoding("UTF-8");
            response.getWriter().print(JsonUtil.toJson(json));
        }
        finally
        {
            response.getWriter().close();
        }
    }
    
}

TokenHelper.java

package com.xxx.www.common.utils;

import java.math.BigInteger;
import java.util.Map;
import java.util.Random;
import javax.servlet.http.HttpServletRequest;
import org.apache.log4j.Logger;
import com.xxx.cache.redis.IRedisCacheClient;

/**
 * TokenHelper
 * 
 */
public class TokenHelper
{
    
    /**
     * 保存token值的默认命名空间
     */
    public static final String TOKEN_NAMESPACE = "xxx.tokens";
    
    /**
     * 持有token名称的字段名
     */
    public static final String TOKEN_NAME_FIELD = "xxx.token.name";
    private static final Logger LOG = Logger.getLogger(TokenHelper.class);
    private static final Random RANDOM = new Random();
    
    private static IRedisCacheClient redisCacheClient;// 缓存调用,代替session,支持分布式
    
    public static void setRedisCacheClient(IRedisCacheClient redisCacheClient)
    {
        TokenHelper.redisCacheClient = redisCacheClient;
    }
    
    /**
     * 使用随机字串作为token名字保存token
     * 
     * @param request
     * @return token
     */
    public static String setToken(HttpServletRequest request)
    {
        return setToken(request, generateGUID());
    }
    
    /**
     * 使用给定的字串作为token名字保存token
     * 
     * @param request
     * @param tokenName
     * @return token
     */
    private static String setToken(HttpServletRequest request, String tokenName)
    {
        String token = generateGUID();
        setCacheToken(request, tokenName, token);
        return token;
    }
    
    /**
     * 保存一个给定名字和值的token
     * 
     * @param request
     * @param tokenName
     * @param token
     */
    private static void setCacheToken(HttpServletRequest request, String tokenName, String token)
    {
        try
        {
            String tokenName0 = buildTokenCacheAttributeName(tokenName);
            redisCacheClient.listLpush(tokenName0, token);
            request.setAttribute(TOKEN_NAME_FIELD, tokenName);
            request.setAttribute(tokenName, token);
        }
        catch(IllegalStateException e)
        {
            String msg = "Error creating HttpSession due response is commited to client. You can use the CreateSessionInterceptor or create the HttpSession from your action before the result is rendered to the client: " + e.getMessage();
            LOG.error(msg, e);
            throw new IllegalArgumentException(msg);
        }
    }
    
    /**
     * 构建一个基于token名字的带有命名空间为前缀的token名字
     * 
     * @param tokenName
     * @return the name space prefixed session token name
     */
    public static String buildTokenCacheAttributeName(String tokenName)
    {
        return TOKEN_NAMESPACE + "." + tokenName;
    }
    
    /**
     * 从请求域中获取给定token名字的token值
     * 
     * @param tokenName
     * @return the token String or null, if the token could not be found
     */
    public static String getToken(HttpServletRequest request, String tokenName)
    {
        if(tokenName == null)
        {
            return null;
        }
        Map params = request.getParameterMap();
        String[] tokens = (String[]) (String[]) params.get(tokenName);
        String token;
        if((tokens == null) || (tokens.length < 1))
        {
            LOG.warn("Could not find token mapped to token name " + tokenName);
            return null;
        }
        
        token = tokens[0];
        return token;
    }
    
    /**
     * 从请求参数中获取token名字
     * 
     * @return the token name found in the params, or null if it could not be found
     */
    public static String getTokenName(HttpServletRequest request)
    {
        Map params = request.getParameterMap();
        
        if(!params.containsKey(TOKEN_NAME_FIELD))
        {
            LOG.warn("Could not find token name in params.");
            return null;
        }
        
        String[] tokenNames = (String[]) params.get(TOKEN_NAME_FIELD);
        String tokenName;
        
        if((tokenNames == null) || (tokenNames.length < 1))
        {
            LOG.warn("Got a null or empty token name.");
            return null;
        }
        
        tokenName = tokenNames[0];
        
        return tokenName;
    }
    
    /**
     * 验证当前请求参数中的token是否合法,如果合法的token出现就会删除它,它不会再次成功合法的token
     * 
     * @return 验证结果
     */
    public static boolean validToken(HttpServletRequest request)
    {
        String tokenName = getTokenName(request);
        
        if(tokenName == null)
        {
            LOG.debug("no token name found -> Invalid token ");
            return false;
        }
        
        String token = getToken(request, tokenName);
        
        if(token == null)
        {
            if(LOG.isDebugEnabled())
            {
                LOG.debug("no token found for token name " + tokenName + " -> Invalid token ");
            }
            return false;
        }
        
        String tokenCacheName = buildTokenCacheAttributeName(tokenName);
        String cacheToken = redisCacheClient.listLpop(tokenCacheName);
        
        if(!token.equals(cacheToken))
        {
            LOG.warn("xxx.internal.invalid.token Form token " + token + " does not match the session token " + cacheToken + ".");
            return false;
        }
        
        // remove the token so it won''t be used again
        
        return true;
    }
    
    public static String generateGUID()
    {
        return new BigInteger(165, RANDOM).toString(36).toUpperCase();
    }
    
}

spring-mvc.xml

<!-- token拦截器-->
    <bean id="tokenInterceptor" ></bean>    
    <bean >    
        <property name="interceptors">    
            <list>    
                <ref bean="tokenInterceptor"/>    
            </list>
        </property>    
    </bean>

input.jsp 在form中加如下内容:

<input type="hidden" name="<%=request.getAttribute("xxx.token.name") %>" value="<%=token %>"/>

<input type="hidden" name="xxx.token.name" value="<%=request.getAttribute("xxx.token.name") %>"/>

当前这里也可以用类似于struts2的自定义标签来做。

另:公司域名做了隐藏,用xxx替换了。


asp.net core MVC之实现基于token的认证

asp.net core MVC之实现基于token的认证

安装Nuget包

项目中添加包:dotnet add package Microsoft.AspNetCore.Authentication.JwtBearer

添加认证配置

Startup类中添加如下配置:

public void ConfigureServices(IServiceCollection services)
{
    ...
    services.AddAuthentication(defaultScheme: JwtBearerDefaults.AuthenticationScheme);
}

public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
    ...
    app.UseAuthentication();
    app.UseAuthorization();
    app.UseEndpoints(endpoints =>
    {
        endpoints.MapControllers();
    }); 
}

AddAuthentication方法会向依赖注入容器添加认证服务和它所使用的其他服务,其参数defaultScheme用于指定当未指定具体的认证方案时将会使用的默认方案,上例为Bearer认证。

AddAuthentication方法的另一重载能够使用AuthenticationOptions类为认证过程中的每一个动作指明所使用的认证方案,如DefaultAuthenticateScheme、
DefaultChallengeScheme、
DefaultSignInScheme、
DefaultSignOutScheme、
DefaultForbidScheme。
如果没有为这些属性设置认证方案,则将使用DefaultScheme属性所指定的值。

当添加JwtBearer认证方式时,JwtBearerOptions对象能够配置该认证的选项,它的TokenValidationParameters属性用于指定验证Token时的规则:

var tokenSection = Configuration.GetSection("Security:Token");
services.AddAuthentication(options => {
    options.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme;
    options.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme;
}).AddJwtBearer(options => {
    options.TokenValidationParameters = new TokenValidationParameters{
        ValidateAudience = true,
        ValidateLifetime = true,
        ValidateIssuer = true,
        ValidateIssuerSigningKey = true,
        ValidIssuer = tokenSection["Issuer"],
        ValidAudience = tokenSection["Audience"],
        IssuerSigningKey = new SymmetricSecurityKey(
            Encoding.UTF8.GetBytes(tokenSection["Key"])
        ),
        ClockSkew = TimeSpan.Zero
    };
});

TokenValidationParameters类作为Token验证参数类,它包含了一些属性,这些属性如ValidateAudience、ValidateIssuer、ValidateLifetime和ValidateIssuerSigningKey,它们都是布尔类型,用于指明是否验证相应的项;而ValidIssuer和ValidAudience属性则用于指明合法的签发者(Issuer)与接受方(Audience)。在上例中,它们的值都从配置文件中获取;IssuerSigningKey属性的值用于指定进行签名验证的安全密钥,它的值为SymmetricSecurityKey对象,即对称加密密钥;ClockSkew属性的值表示验证时间的时间偏移值。

上述代码会从配置文件中读取关于Token的信息,因此还需在appsettings.json中添加如下内容。

"Security": {
  "Token": {
    "Issuer": "demo_issuer",
    "Audience": "demo_audience",
    "Key": "<your_secret_key>"
  }
}

为Controller添加认证

接下来,为了使用ASP.NET Core的认证功能来保护资源,应为Controller或Action添加[Authorize]特性,该特性能够实现在访问相应的Controller或Action时,要求请求方提供指定的认证方式,它位于Microsoft.AspNetCore.Authorization命名空间中。需要为AuthorController和BookController添加该特性。

[Authorize]
public class AuthorController : ControllerBase
{
}
[Authorize(AuthenticationSchemes = JwtBearerDefaults.AuthenticationScheme)]
public class BookController : ControllerBase
{
}

如果使用了多个认证方式,则可以使用[Authorize]特性的AuthenticationSchemes属性指明当前Controller或Action要使用哪一种认证方式(如上例中的BookController);如果不设置,则会使用所添加认证时设置的默认方案;如果没有设置默认方案,则会出现InvalidOperationException异常,并提示未指定默认方案;此外,如果为AuthenticationSchemes属性指定了不存在的方案名称,也会出现InvalidOperationException异常。

此时再访问Book和Author资源,会出现401 Unauthorized异常:

如果要允许某个Action可以被匿名访问,可以在Action方法上添加属性标记 [AllowAnonymous]:

[AllowAnonymous]
public async Task<ActionResult<IEnumerable<AuthorDto>>> GetAuthorsAsync([FromQuery] AuthorResourceParameters parameters)

添加认证信息生成接口

JwtBearer中间件提供了对JWT的验证功能,然而并未提供生成Token的功能。要生成Token,可以使用JwtSecurityTokenHandler类,它位于System.IdentityModel.Tokens.Jwt命名空间,它不仅能够生成JWT,由于它实现了ISecurityTokenValidator接口,因此对JWT的验证也是由它完成的。接下来,我们将创建一个Controller,它将会根据用户的认证信息生成JWT,并返回给客户端。

在Controllers文件夹中创建一个Controller,名为AuthenticateController,内容如下:

using System;
using System.Collections.Generic;
using System.IdentityModel.Tokens.Jwt;
using System.Security.Claims;
using System.Text;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Configuration;
using Microsoft.IdentityModel.Tokens;

namespace Library.Api.Controllers
{
    [ApiController, Route("api/auth")]
    public class AuthenticateController : ControllerBase
    {
        public IConfiguration Configuration { get; }
        public AuthenticateController(IConfiguration configuration)
        {
            Configuration = configuration;
        }

        [HttpPost("token", Name = nameof(GenerateToken))]
        public IActionResult GenerateToken(string username, string password)
        {
            if (username != "demouser" || password != "demopassword")
            {
                return Unauthorized();
            }

            var claims = new List<Claim>{
                new Claim(JwtRegisteredClaimNames.Sub, username)
            };

            var tokenConfigSection = Configuration.GetSection("Security:Token");
            var key = new SymmetricSecurityKey(
                Encoding.UTF8.GetBytes(tokenConfigSection["Key"])
            );
            var signCredential = new SigningCredentials(key, SecurityAlgorithms.HmacSha256);
            var jwtToken = new JwtSecurityToken(
                issuer: tokenConfigSection["Issuer"],
                audience: tokenConfigSection["Audience"],
                claims: claims,
                expires: DateTime.Now.AddMinutes(3),
                signingCredentials: signCredential
            );

            return Ok(new {
                token = new JwtSecurityTokenHandler().WriteToken(jwtToken),
                expiration = TimeZoneInfo.ConvertTimeFromUtc(jwtToken.ValidTo, TimeZoneInfo.Local)
            });
        }
    }
}

在AuthenticateController中的GenerateToken方法中,通过创建JwtSecurityToken对象,并使用JwtSecurityTokenHandler对象的WriteToken方法最终得到生成的JWT。当创建JwtSecurityToken对象时,我们可以指定issuer、audience以及当前用户的Claim信息,此外,还可以指定该Token的有效时间。这里需要注意,由于JWT不支持销毁以及撤回功能,因此在设置它的有效时间时,应设置一个较短的时间(如上例中的3分钟),这样可以有效避免Token在意外被窃取后所带来的风险。

现在就可以请求认证接口获取 token:

这时重新请求资源接口,在请求头中添加Authorization项,值为Bearer ,就可以得到结果了:

这次示例中,使用了固定的用户名和密码,实际情况中,用户名和密码通常是存在数据库中的,可以使用ASP.NET Core Identity来实现这一功能。

到此这篇关于asp.net core MVC之实现基于token的认证的文章就介绍到这了,更多相关asp.net core token的认证内容请搜索以前的文章或继续浏览下面的相关文章希望大家以后多多支持!

您可能感兴趣的文章:
  • ASP.NET Core MVC 过滤器的使用方法介绍
  • asp.net core MVC 过滤器之ActionFilter过滤器(2)
  • asp.net core MVC 全局过滤器之ExceptionFilter过滤器(1)
  • ASP.NET Core MVC 修改视图的默认路径及其实现原理解析
  • ASP.NET Core MVC解决控制器同名Action请求不明确的问题
  • 如何在Asp.Net Core MVC中处理null值的实现
  • ASP.NET Core MVC如何实现运行时动态定义Controller类型
  • ASP.NET Core MVC/WebApi基础系列2
  • ASP.NET Core MVC/WebApi基础系列1
  • 如何使用Rotativa在ASP.NET Core MVC中创建PDF详解
  • ASP.NET Core Mvc中空返回值的处理方法详解
  • ASP.NET Core MVC 过滤器(Filter)

java day61【 SpringMVC 的基本概念 、 SpringMVC 的入门 、 请求参数的绑定 、常用注解 】

java day61【 SpringMVC 的基本概念 、 SpringMVC 的入门 、 请求参数的绑定 、常用注解 】

第1章 SpringMVC 的基本概念

1.1关于三层架构和 MVC

1.1.1 三层架构

1.1.2 MVC 模型

1.2SpringMVC 概述

1.2.1 SpringMVC 是什么

1.2.2 SpringMVC 在三层架构的位置

1.2.3 SpringMVC 的优势

1.2.4 SpringMVC 和 Struts2 的优略分析

第2章 SpringMVC 的入门

2.1SpringMVC 的入门案例

2.1.1 前期准备

2.1.2 拷贝 jar 包

2.1.3 配置核心控制器-一个 Servlet

2.1.4 创建 spring mvc 的配置文件

2.1.5 编写控制器并使用注解配置

2.1.6 测试

2.2入门案例的执行过程及原理分析

2.2.1 案例的执行过程

2.2.2 SpringMVC 的请求响应流程

2.3入门案例中涉及的组件

2.3.1 DispatcherServlet:前端控制器

2.3.2 HandlerMapping:处理器映射器

2.3.3 Handler:处理器

2.3.4 HandlAdapter:处理器适配器

2.3.5 View Resolver:视图解析器

2.3.6 View:视图

2.4RequestMapping 注解

2.4.1 使用说明

2.4.2 使用示例

2.4.2.1 出现位置的示例:

2.4.2.2 method 属性的示例:

 2.4.2.3 params 属性的示例:

第3章 请求参数的绑定

3.1绑定说明

3.1.1 绑定的机制

3.1.2 支持的数据类型:

3.1.3 使用要求:

3.1.4 使用示例

3.1.4.1 基本类型和 String 类型作为参数

3.1.4.2 POJO 类型作为参数

3.1.4.3 POJO 类中包含集合类型参数

3.1.4.4 请求参数乱码问题

3.2特殊情况

3.2.1 自定义类型转换器

3.2.1.1 使用场景:

3.2.1.2 使用步骤

3.2.2 使用 ServletAPI 对象作为方法参数

第4章 常用注解

4.1RequestParam

4.1.1 使用说明

4.1.2 使用示例

4.2RequestBody

4.2.2 使用示例

4.3PathVaribale

4.3.1 使用说明

4.3.2 使用示例

4.3.3 REST 风格 URL

4.3.4 基于 HiddentHttpMethodFilter 的示例

4.4RequestHeader

4.4.1 使用说明

4.4.2 使用示例

4.5CookieValue

4.5.1 使用说明

4.5.2 使用示例

4.6ModelAttribut

4.6.1 使用说明

4.6.2 使用示例

4.6.2.1 基于 POJO 属性的基本使用:

4.6.2.2 基于 Map 的应用场景示例 1:ModelAttribute 修饰方法带返回值

4.6.2.3 基于 Map 的应用场景示例 1:ModelAttribute 修饰方法不带返回值

4.7SessionAttribute

4.7.1 使用说明

4.7.2 使用示例

 

java day62【 响应数据和结果视图 、 SpringMVC 实现文件上传 、 SpringMVC 中的异常处理 、 SpringMVC 中的拦截器 】

java day62【 响应数据和结果视图 、 SpringMVC 实现文件上传 、 SpringMVC 中的异常处理 、 SpringMVC 中的拦截器 】

第 1 章 响应数据和结果视图

1.1 返回值分类

1.1.1 字符串

1.1.2 void

1.1.3 ModelAndView

1.2 转发和重定向

1.2.1 forward 转发

1.2.2 Redirect 重定向

1.3ResponseBody 响应 json 数据

1.3.1 使用说明

1.3.2 使用示例

第 2 章 SpringMVC 实现文件上传

2.1 文件上传的回顾

2.1.1 文件上传的必要前提

2.1.2 文件上传的原理分析

2.1.3 借助第三方组件实现文件上传

2.2springmvc 传统方式的文件上传

2.2.1 说明

2.2.2 实现步骤

2.2.2.1 第一步:拷贝文件上传的 jar 包到工程的 lib 目录

2.2.2.2 第二步:编写 jsp 页面

2.2.2.3 第三步:编写控制器

2.2.2.4 第四步:配置文件解析器

2.3springmvc 跨服务器方式的文件上传

2.3.1 分服务器的目的

2.3.2 准备两个 tomcat 服务器,并创建一个用于存放图片的 web 工程

2.3.3 拷贝 jar 包

 

2.3.4 编写控制器实现上传图片

2.3.5 编写 jsp 页面

2.3.6 配置解析器

第 3 章 SpringMVC 中的异常处理

3.1 异常处理的思路

3.2 实现步骤

3.2.1 编写异常类和错误页面

3.2.2 自定义异常处理器

3.2.3 配置异常处理器

3.2.4 运行结果:

第 4 章 SpringMVC 中的拦截器

4.1 拦截器的作用

4.2 自定义拦截器的步骤

4.2.1 第一步:编写一个普通类实现 HandlerInterceptor 接口

4.2.2 第二步:配置拦截器

4.2.3 测试运行结果:

 4.3 拦截器的细节

4.3.1 拦截器的放行

4.3.2 拦截器中方法的说明

4.3.3 拦截器的作用路径

4.3.4 多个拦截器的执行顺序

4.4 正常流程测试

4.4.1 配置文件:

4.4.2 拦截器 1 的代码:

4.4.3 拦截器 2 的代码:

4.4.4 运行结果:

4.5 中断流程测试

4.5.1 配置文件:

4.5.2 拦截器 1 的代码:

4.5.3 拦截器 2 的代码:

4.5.4 运行结果:

4.6 拦截器的简单案例(验证用户是否登录)

4.6.1 实现思路

4.6.2 控制器代码

4.6.3 拦截器代码

 

JAVA WEB快速入门之从编写一个基于SpringMVC框架的网站了解Maven、SpringMVC、SpringJDBC

JAVA WEB快速入门之从编写一个基于SpringMVC框架的网站了解Maven、SpringMVC、SpringJDBC

接上篇《JAVA WEB快速入门之通过一个简单的Spring项目了解Spring的核心(AOP、IOC)》,了解了Spring的核心(AOP、IOC)后,我们再来学习与实践Maven、SpringMVC、SpringJDBC(即:SSM中的S(Spring)S(SpringMVC)),暂不涉及ORM部份(即:M(Mybatis)),Mybatis将在下一篇文章中继续给大家分享。我相信通过之前几篇文章的学习与实践,已基本熟悉了搭建JSP网站及把AOP IOC应用到项目中,已具备编写JSP 普通WEB网站了,故从本文开始,一些之前讲述过的步骤不再一一说明,只讲重点及结果。

(提示:本文内容有点长,涉及的知识点也比较多,若是新手建议耐心看完!)

 一、了解Maven并基于Maven构建一个空的SpringMVC网站:

 1.1Maven是什么?

  Maven 翻译为"专家"、"内行",是 Apache 下的一个纯 Java 开发的开源项目。基于项目对象模型(缩写:POM)概念,Maven利用一个中央信息片断能管理一个项目的构建、报告和文档等步骤。

  主要功能有:构建、文档生成、报告、依赖、SCMs、发布、分发、邮件列表

  Maven 提倡使用一个共同的标准目录结构,Maven 使用约定优于配置的原则,大家尽可能的遵守这样的目录结构,如下图示(来源网络):(假定 ${basedir} 表示工程目录)

  

  Maven 有以下三个标准的生命周期:(每个生命周期中都包含着一系列的阶段(phase)。这些 phase 就相当于 Maven 提供的统一的接口,然后这些 phase 的实现由 Maven 的插件来完成。)
  clean:项目清理的处理 、default(或 build):项目部署的处理、site:项目站点文档创建的处理

  详细说明请参见:http://www.runoob.com/maven/maven-build-life-cycle.html

1.2搭建Maven环境

  1.2.1 从官网下载地址:http://maven.apache.org/download.cgi 中选择对应版本点击下载(开发一般以WINDOWS为主,故下载apache-maven-x.x.x-bin.zip),下载后解压到对应的目录,然后配置如下环境变量:

    新增变量名: MAVEN_HOME,变量值:maven解压根目录

        编辑系统变量 Path,添加变量值:%MAVEN_HOME%\bin (WIN10系统直接添加一行)

    配置完后,在cmd中输入:mvn -v 如果正常输出maven版本信息则表示OK;

  1.2.2 设置Maven的本地仓库的下载位置(默认是在系统盘(一般为C):\Users\当前用户名\.m2\repository),如果不改变则会导致系统盘分区容量不足,建议及时修改

  打开%MAVEN_HOME%\conf 目录下的settings.xml,修改localRepository元素的内容为自定义的repository目录,如:

<localRepository>E:/LocalMvnRepositories</localRepository>

  然后设置IDE(eclipse或idea)MAVEN的存储位置,如下是eclipse的修改截图,idea同理

  依次打开:windows->perferences->Maven(右边列表节点)->User Settings,修改Global Settings 、User Settings的路径(将settings.xml COPY到指定的目录,然后这里设置这个目录),如下图示:【若没有maven选项可能需要手动安装maven插件,详见:https://www.cnblogs.com/dtting/p/8254103.html】

  

  点击update Settings按钮完成更新,最后Apply即可

  1.2.3 settings.xml中设置镜像中央仓库URL,如下:(阿里云仓库)

<mirror>
      <id>alimaven</id>
      <name>aliyun maven</name>
      <url>http://maven.aliyun.com/nexus/content/groups/public/</url>
      <mirrorOf>central</mirrorOf>        
    </mirror>
  </mirrors>

  1.2.4 使用maven命令创建一个空的maven webapp,在cmd中执行如下命令:(请先cd切换到指定的项目创建根目录再执行如下命令)

  mvn archetype:generate

  然后根据每步提示进行交互处理,一般依次输入:archetypeArtifactId(如果要搭建MVC,则选择maven-archetype-webapp)、gourpId、artifactId、version、package,如下图示:

  

   当然如果不想一步步的按照向导来操作,可以带上完整参数来进行操作,例如:

mvn archetype:generate
-DgroupId=cn.zuowenjun.java
-DartifactId=mvnspringmvc2 
-DarchetypeArtifactId=maven-archetype-webapp 
-DinteractiveMode=false

 执行完成后会输出build success字样,就表示构建maven webapp项目成功,本地文件目录就会生成相关的文件,如果需要使用IDE打开,可以在eclipse中通过:File->Open Projects from File systm or archive->选择import source路径(命令生成的项目根目录)->finish即可。

 1.2.5 通过IDE(eclipse) maven插件来构建项目,操作步骤为:

   File->New->Maven Project ->向导界面(Create a simple Proejct不要勾选,其余项按需设置)->向导界面(Select an Archetype:选择maven-archetype-webapp,如下图示)->设置->finish即可

   

  如果发现Archetype(即:项目原型模板,与VS中创建某个项目类似)的Version比较低,可以使用右下角的"Add Archetype"添加最新的Archetype(如上图示出现的1.4版本就是我加的),这里我们仍选择1.0,下一步就出现如下图示,设置相关信息即可

  

两个踩坑点:(不论是用mvn命令行还是maven插件创建的webapp项目存在两个问题,需要处理)

  1.构建WebApp项目资源目录显示不全,缺少java等目录,问题原因是默认的项目是使用的JRE1.5,我们只需改成当前最新的版本即可(如:1.8),参考:https://blog.csdn.net/Sunny1994_/article/details/79058685

  2.改完后可能还会报:Description Resource Path Location Type Java compiler level does not match the version of the installed Java project facet. mvnspringmvc Unknown Faceted Project Problem (Java Version Mismatch),这是由于编译的JDK版本与项目的JDK版本不一致造成的,通过:在项目上右键Properties-》Project Facets,在打开的Project Facets页面中的Java下拉列表中,选择相应版本,如下图示:

  

  3.报缺少javax.servlet.http.HttpServlet父类,需要在pom文件中添加一下JAR包依赖,配置如下:(version请按需要配置)

<dependencies>
		<dependency>
			<groupId>javax.servlet</groupId>
			<artifactId>javax.servlet-api</artifactId>
			<version>3.1.0</version>
		</dependency>
	</dependencies>

    解决后,最后再buid proejct如无报错,说明空的maven project已创建OK。

1.3添加springMVC、SpringJDBC相关依赖、配置web.xml,实现SpringMVC项目开发环境

   1.3.1在pom.xml中添加springMVC、SpringJDBC依赖,如下:(这里将版本号单独统一配置在properties中)

<properties>
		<spring.version>5.1.3.RELEASE</spring.version>
	</properties>
	
	<dependencies>
		<dependency>
			<groupId>junit</groupId>
			<artifactId>junit</artifactId>
			<version>3.8.1</version>
			<scope>test</scope>
		</dependency>
		<dependency>
			<groupId>javax.servlet</groupId>
			<artifactId>javax.servlet-api</artifactId>
			<version>3.1.0</version>
		</dependency>
		<dependency>
			<groupId>org.springframework</groupId>
			<artifactId>spring-core</artifactId>
			<version>${spring.version}</version>
		</dependency>
		<dependency>
			<groupId>org.springframework</groupId>
			<artifactId>spring-web</artifactId>
			<version>${spring.version}</version>
		</dependency>
		<dependency>
			<groupId>org.springframework</groupId>
			<artifactId>spring-webmvc</artifactId>
			<version>${spring.version}</version>
		</dependency>
		<dependency>
			<groupId>org.springframework</groupId>
			<artifactId>spring-jdbc</artifactId>
			<version>${spring.version}</version>
		</dependency>
	</dependencies>

 如下是完整示例POM文件内容:(比较完整,涉及springMVC、springJDBC、数据驱动【这里是sqlserver】、jsp视图【JSTL、EL】)

<project xmlns="http://maven.apache.org/POM/4.0.0"
	xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
	xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd">
	<modelVersion>4.0.0</modelVersion>
	<groupId>cn.zuowenjun.java</groupId>
	<artifactId>mvnspringmvc</artifactId>
	<packaging>war</packaging>
	<version>0.0.1-SNAPSHOT</version>
	<name>mvnspringmvc Maven Webapp</name>
	<url>http://maven.apache.org</url>


	<properties>
		<spring.version>5.1.3.RELEASE</spring.version>
	</properties>

	<dependencies>
		<dependency>
			<groupId>junit</groupId>
			<artifactId>junit</artifactId>
			<version>3.8.1</version>
			<scope>test</scope>
		</dependency>
		<!-- JSP视图所需依赖 -->
		<dependency>
			<groupId>javax.servlet</groupId>
			<artifactId>javax.servlet-api</artifactId>
			<version>3.1.0</version>
		</dependency>
		<dependency>
			<groupId>javax.servlet.jsp</groupId>
			<artifactId>jsp-api</artifactId>
			<version>2.2</version>
		</dependency>
		
		<!-- JSP JSTL所需依赖 -->
		<dependency>
			<groupId>javax.servlet.jsp.jstl</groupId>
			<artifactId>jstl-api</artifactId>
			<version>1.2</version>
			<exclusions>
				<exclusion>
					<groupId>javax.servlet</groupId>
					<artifactId>servlet-api</artifactId>
				</exclusion>
				<exclusion>
					<groupId>javax.servlet.jsp</groupId>
					<artifactId>jsp-api</artifactId>
				</exclusion>
			</exclusions>
		</dependency>
		<dependency>
			<groupId>org.glassfish.web</groupId>
			<artifactId>jstl-impl</artifactId>
			<version>1.2</version>
			<exclusions>
				<exclusion>
					<groupId>javax.servlet</groupId>
					<artifactId>servlet-api</artifactId>
				</exclusion>
				<exclusion>
					<groupId>javax.servlet.jsp</groupId>
					<artifactId>jsp-api</artifactId>
				</exclusion>
				<exclusion>
					<groupId>javax.servlet.jsp.jstl</groupId>
					<artifactId>jstl-api</artifactId>
				</exclusion>
			</exclusions>
		</dependency>

		<!-- springMVC所需依赖  -->
		<dependency>
			<groupId>org.springframework</groupId>
			<artifactId>spring-core</artifactId>
			<version>${spring.version}</version>
		</dependency>
		<dependency>
			<groupId>org.springframework</groupId>
			<artifactId>spring-web</artifactId>
			<version>${spring.version}</version>
		</dependency>
		<dependency>
			<groupId>org.springframework</groupId>
			<artifactId>spring-webmvc</artifactId>
			<version>${spring.version}</version>
		</dependency>
		
		<!-- Spring JDBC【数据访问】所需依赖  -->
		<dependency>
			<groupId>org.springframework</groupId>
			<artifactId>spring-jdbc</artifactId>
			<version>${spring.version}</version>
		</dependency>
		<dependency>
			<groupId>com.microsoft.sqlserver</groupId>
			<artifactId>mssql-jdbc</artifactId>
			<version>7.0.0.jre8</version>
		</dependency>
		
	</dependencies>
	<build>
		<finalName>mvnspringmvc</finalName>
	</build>
</project>

  

 1.3.2在web.xml中配置DispatcherServlet及ContextLoaderListener,如下:(如下完整的web.xml,有改造过,因为默认的web的声明头有问题,另外如果不配置contextConfigLocation,那么springContext的配置文件默认路径:[servlet-name(是DispatcherServlet配置的名称)]-servlet.xml)

<web-app version="3.0" xmlns="http://java.sun.com/xml/ns/javaee"
	xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
	xsi:schemaLocation="http://java.sun.com/xml/ns/javaee 
    http://java.sun.com/xml/ns/javaee/web-app_3_0.xsd">

	<display-name>Archetype Created Web Application</display-name>

	<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>

	<!-- Map all requests to the DispatcherServlet for handling -->
	<servlet-mapping>
		<servlet-name>springmvc</servlet-name>
		<url-pattern>/</url-pattern>
	</servlet-mapping>
	<context-param>
		<param-name>contextConfigLocation</param-name>
		<param-value>classpath:springmvc-servlet.xml</param-value>
	</context-param>

	<listener>
		<listener-class>org.springframework.web.context.ContextLoaderListener</listener-class>
	</listener>

	<!-- 解决中文请求乱码问题 -->
	<filter>
		<filter-name>CharacterEncoding</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>
		<init-param>
			<param-name>forceEncoding</param-name>
			<param-value>true</param-value>
		</init-param>
	</filter>
	<filter-mapping>
		<filter-name>CharacterEncoding</filter-name>
		<url-pattern>/*</url-pattern>
	</filter-mapping>

	<!-- 定义默认首页,欢迎页 -->
	<welcome-file-list>
		<welcome-file>index.jsp</welcome-file>
	</welcome-file-list>

	<!-- 定义错误处理页面,此处只定义404,其余的通过@ControllerAdvice+@ExceptionHandler来处理 -->
	<error-page>
		<error-code>404</error-code>
		<location>/WEB-INF/errors/notfound.jsp</location>
	</error-page>
</web-app>

 知识扩展说明:springMVC异常统一处理有多种方式,可参见:https://www.cnblogs.com/bloodhunter/p/4825279.html  、 https://www.cnblogs.com/junzi2099/p/7840294.html 、https://blog.csdn.net/butioy_org/article/details/78718405

 1.3.3在src/main/resources中创建springmvc-servlet.xml(即:contextConfigLocation配置的路径文件),这个是spring Context配置文件与上一篇介绍的Beans.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:context="http://www.springframework.org/schema/context"
	xmlns:mvc="http://www.springframework.org/schema/mvc" 
	xmlns:tx="http://www.springframework.org/schema/tx"
	xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-4.3.xsd
		http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context-4.3.xsd
		http://www.springframework.org/schema/mvc http://www.springframework.org/schema/mvc/spring-mvc-4.3.xsd
		http://www.springframework.org/schema/tx http://www.springframework.org/schema/tx/spring-tx-4.3.xsd">

	<!-- 对包中的所有类进行扫描,以完成Bean创建和自动依赖注入的功能 ,多个包名用逗号分隔 -->
	<context:component-scan
		base-package="cn.zuowenjun.java.mvc"></context:component-scan>
	
	<mvc:annotation-driven>
<!-- 		<mvc:argument-resolvers></mvc:argument-resolvers> -->
<!-- 		<mvc:async-support></mvc:async-support> -->
<!-- 		<mvc:message-converters></mvc:message-converters> -->
<!-- 		<mvc:path-matching/> -->
<!-- 		<mvc:return-value-handlers></mvc:return-value-handlers> -->
	</mvc:annotation-driven>
	
	<bean>
		<property name="prefix" value="/WEB-INF/jsp/" />
		<property name="suffix" value=".jsp" />
	</bean>
	
	<!-- 配置静态资源,如html,图片,js,css等,多个资源位置可用逗号分隔 -->
	 <mvc:resources mapping="/pages/**" location="/WEB-INF/pages/" />
	
	<!-- 可配置读取外部配置文件,如果有配置jdbc.properties,则下面的dataSource的相关property可以使用${xxx}占位符,这里不演示 -->
	<!--<context:property-placeholder location="classpath:jdbc.properties" /> -->
	
	<bean id="dataSource">
		<property name="driverClassName"  value="com.microsoft.sqlserver.jdbc.SQLServerDriver"></property>
		<property name="url" value="jdbc:sqlserver://ip:port;DatabaseName=testDB"></property>
		<property name="username" value="dbuser"></property>
		<property name="password" value="password"></property>
	</bean>
	
	
	
	<!-- 配置事务管理器  -->
    <bean id="txManager">
        <property name="dataSource" ref="dataSource" />
    </bean>
    
<!--     事务扫描开始(开启@Tranctional) 此示例暂不启用-->
<!--     <tx:annotation-driven transaction-manager="txManager" /> -->
	
</beans>

对如下配置作简要说明:

 context:component-scan:自动扫描包并将标记@Component(组件),@Service(服务),@Controller(控制器),@Repository(数据仓库)的Bean注册到Spring IOC容器中,这样就无需手动在xml进行单独配置了;详见:https://www.cnblogs.com/fightingcoding/p/component-scan.html

 mvc:annotation-driven:简化配置自动注册DefaultAnnotationHandlerMapping与AnnotationMethodHandlerAdapter两个bean,这些是spring MVC为@Controller分发请求所必须的。里面也包含一些子元素的配置,详见:https://starscream.iteye.com/blog/1098880

 mvc:resources:配置处理静态资源,配置的url路径将只会由default默认的servlet来处理,而不再由DispatcherServlet处理,详见:https://www.cnblogs.com/linnuo/p/7699401.html

 注册Bean:InternalResourceViewResolver,视图解析器,用于找到视图文件;

 注册Bean:DriverManagerDataSource,指定SpringJDBC的数据源驱动相关连接信息;

 注册Bean:DataSourceTransactionManager,配置事务管理器,用于事务处理;

 如上步骤都操作完后,就可以Buid Project,如果构建成功,则说明SpringMVC项目环境已OK;可以看到项目目录如下图示:

(其中那些包都是我为后面写代码时提前创建的)

二、编写SpringMVC示例代码(演示:发博文,写评论),了解SpringMVC及SpringJDBC:

  2.1.定义Model:(在cn.zuowenjun.java.mvc.model包中分类定义:Post、PostComment 两个数据模型)

//Post.java
package cn.zuowenjun.java.mvc.model;

import java.util.Date;

public class Post {
	private int id;
	private String title;
	private String content;
	private String author;
	private Date createTime;
	
	public int getId() {
		return id;
	}
	public void setId(int id) {
		this.id = id;
	}
	
	public String getTitle() {
		return title;
	}
	public void setTitle(String title) {
		this.title = title;
	}
	
	public String getContent() {
		return content;
	}
	public void setContent(String content) {
		this.content = content;
	}
	
	public String getAuthor() {
		return author;
	}
	public void setAuthor(String author) {
		this.author = author;
	}
	
	public Date getCreateTime() {
		return createTime;
	}
	public void setCreateTime(Date createTime) {
		this.createTime = createTime;
	}
	
	

}



//PostComment.java

package cn.zuowenjun.java.mvc.model;

import java.util.Date;

public class PostComment {
	private int id;
	private int postid;
	private String content;
	private String createby;
	private Date createTime;
	
	public int getId() {
		return id;
	}
	public void setId(int id) {
		this.id = id;
	}
	
	public int getPostid() {
		return postid;
	}
	public void setPostid(int postid) {
		this.postid = postid;
	}
	
	public String getContent() {
		return content;
	}
	public void setContent(String content) {
		this.content = content;
	}
	
	public String getCreateby() {
		return createby;
	}
	public void setCreateby(String createby) {
		this.createby = createby;
	}
	
	public Date getCreateTime() {
		return createTime;
	}
	public void setCreateTime(Date createTime) {
		this.createTime = createTime;
	}
	
	

}

  非常简单的POJO风格的两个类,只有成对的getter、setter方法。

  2.2定义Dao接口:(在cn.zuowenjun.java.mvc.dao包中分别定义:PostDao、PostCommentDao 两个DAO接口,注意JAVA与C#的接口定义规范有所不同,C#中要求以I开头,而JAVA中并没有此要求,JAVA只要要求实现类以Impl后缀结尾)

//PostDao.java
package cn.zuowenjun.java.mvc.dao;

import java.util.Date;
import java.util.List;

import cn.zuowenjun.java.mvc.model.Post;

public interface PostDao {
	Post get(int id);
	List<Post> getList(Date frmDate,Date toDate);
	int create(Post post);
	Boolean delete(int id);
	Boolean update(Post post);
	
}


//PostCommentDao.java
package cn.zuowenjun.java.mvc.dao;

import java.util.List;

import cn.zuowenjun.java.mvc.model.PostComment;

public interface PostCommentDao {
	PostComment get(int id);
	List<PostComment> getList(int postId);
	Boolean create(PostComment postCmmt);
	Boolean delete(int id);
	Boolean update(PostComment postCmmt);
}

 如上代码所示,DAO接口主要定义了增、删、改、查(查单个,查多个),这是大部份的常见场景,根据需要定义。

 2.3实现Dao接口:(在cn.zuowenjun.java.mvc.dao.impl包中分别定义:BaseDao、PostDaoImpl、PostCommentDaoImpl,其中BaseDao是抽像类,主要是约束构造函数的入参及提供通用的获取JdbcTemplate实例对象)

 BaseDao抽象类定义:

 

package cn.zuowenjun.java.mvc.dao.impl;

import javax.sql.DataSource;

import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.jdbc.core.namedparam.NamedParameterJdbcTemplate;

public abstract class BaseDao {
	private JdbcTemplate jdbcTemplateObj;
	private NamedParameterJdbcTemplate namedParamJdbcTemplate;
	
	public BaseDao(DataSource dataSource) {
		this.jdbcTemplateObj=new JdbcTemplate(dataSource);
		this.namedParamJdbcTemplate=new NamedParameterJdbcTemplate(dataSource);
	}
	
	protected JdbcTemplate getJdbcTemplate() {
		return this.jdbcTemplateObj;
	}
	
	protected NamedParameterJdbcTemplate getNamedParameterJdbcTemplate() {
		return this.namedParamJdbcTemplate;
	}
	
	
}

BaseDao抽象类中分别实例化了两个数据访问对象:JdbcTemplate、NamedParameterJdbcTemplate,之所以这样做是因为我会分别在PostDaoImpl演示使用NamedParameterJdbcTemplate来进行CRUD操作,PostCommentDaoImpl演示使用JdbcTemplate来进行CRUD操作。通过对比代码来发现两者的区别以及优势。

PostDaoImpl类定义:

package cn.zuowenjun.java.mvc.dao.impl;

import java.util.*;

import javax.sql.DataSource;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.dao.DataAccessException;
import org.springframework.jdbc.core.BeanPropertyRowMapper;
import org.springframework.jdbc.core.namedparam.*;
import org.springframework.jdbc.support.*;
import org.springframework.transaction.*;
import org.springframework.transaction.support.*;

import cn.zuowenjun.java.mvc.dao.*;
import cn.zuowenjun.java.mvc.model.Post;

public class PostDaoImpl extends BaseDao implements PostDao {

	private PlatformTransactionManager transactionManager;
	
	@Autowired
	public PostDaoImpl(DataSource dataSource,PlatformTransactionManager transactionManager) {
		super(dataSource);
		this.transactionManager=transactionManager;
	}

	@Override
	public Post get(int id) {
		String sql="select * from TA_TestPost where id=:id";
		MapSqlParameterSource mapSqlParameterSource = new MapSqlParameterSource();
				mapSqlParameterSource.addValue("id", id);
		return this.getNamedParameterJdbcTemplate().queryForObject(sql, mapSqlParameterSource,new BeanPropertyRowMapper<>(Post.class));
	}

	@Override
	public List<Post> getList(Date frmDate, Date toDate) {
		String sql="select * from TA_TestPost where createTime between :frmDate to :toDate";
		Map<String, Object> paramMap = new HashMap<>();
		paramMap.put("frmDate", frmDate);
		paramMap.put("toDate", toDate);
		
		return this.getNamedParameterJdbcTemplate().query(sql, paramMap,new BeanPropertyRowMapper<>(Post.class));
	}

	@Override
	public int create(Post post) {
		String sql="insert into TA_TestPost(title, content, author, createTime) values(:title,:content,:author,getdate())";
		BeanPropertySqlParameterSource beanPropParam=new BeanPropertySqlParameterSource(post);
        KeyHolder keyHolder = new GeneratedKeyHolder();
        
		int r= this.getNamedParameterJdbcTemplate().update(sql, beanPropParam, keyHolder);
		if(r>0) {
			System.out.println("create is ok!");
			return keyHolder.getKey().intValue();
		}
		else {
			System.out.println("create is failed!");
			return -1;
		}
	}

	@Override
	public Boolean delete(int id) {
		TransactionDefinition def = new DefaultTransactionDefinition();
	    TransactionStatus status = transactionManager.getTransaction(def);
		try {
			Map<String, Object> paramMap = new HashMap<>();
			paramMap.put("postid",id);
			NamedParameterJdbcTemplate namedJdbcTemplate=this.getNamedParameterJdbcTemplate();
			namedJdbcTemplate.update("delete from TA_TestPost where id=:postid",paramMap);
			namedJdbcTemplate.update("delete from TA_TestPostComment where postid=:postid", paramMap);
			transactionManager.commit(status);
			
			System.out.println("delete is ok!");
			
			return true;
			 
		}catch(DataAccessException daEx) {
			System.out.printf("delete is failed,Exception:%s",daEx.getMessage());
			transactionManager.rollback(status);
			return false;
		}
	}

	@Override
	public Boolean update(Post post) {
		String sql="update TA_TestPost set title=:title,content=:content,author=:author,createTime=getdate() where id=:id";
		BeanPropertySqlParameterSource beanPropParam=new BeanPropertySqlParameterSource(post);
		int r= this.getNamedParameterJdbcTemplate().update(sql, beanPropParam);
		if(r>0) {
			System.out.println("update is ok!");
			return true;
		}
		else {
			System.out.println("update is failed!");
			return false;
		}
	}

}

 涉及知识要点说明:

1.NamedParameterJdbcTemplate支持的常用SQL参数类型有:MapSqlParameterSource(键值对),Map<String, ?>,BeanPropertySqlParameterSource(把一个Bean所有属性映射成参数,推荐这种)

2.NamedParameterJdbcTemplate 使用的SQL语句中参数的命名规则为:冒号+参数名,如: :content,有点类似C#中的ADO.NET的参数化类型(@content),只是前缀指示符不同而矣。

3.若执行新增一条记录,且需要返回自增的ID,这时可以通过传入GeneratedKeyHolder的实例,最后使用该实例的变量获取自增ID,如上面的create方法中的一样,最终使用:keyHolder.getKey().intValue()获取到自增ID;

4.查询返回的结果若要映射为实体对象(POJO、JavaBean),则需要自定义实现RowMapper<T>,然后传入自定义的RowMapper的变量,当然可以使用Spring JDBC 的默认实现类:BeanPropertyRowMapper(内部使用反射, 可能在某些场景下性能不佳),如果只需要返回某列的值,则可以直接指定映射的类型,如:String.class

5.若想查询返回一个实体对象或实体对象列表,应该使用queryForObject、query的重载方法含RowMapper<T>的参数,注意:queryForList 只能返回某一列的值,不能直接返回实体对象列表,因为最后的一个参数Class<T> requiredType最终内部转化为了:SingleColumnRowMapper

6.使用事务需要PlatformTransactionManager、TransactionDefinition、TransactionStatus,如上面的delete方法;

PostCommentDaoImpl类定义:

package cn.zuowenjun.java.mvc.dao.impl;

import java.sql.ResultSet;
import java.sql.SQLException;
import java.util.List;

import javax.sql.DataSource;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.jdbc.core.RowMapper;

import cn.zuowenjun.java.mvc.dao.PostCommentDao;
import cn.zuowenjun.java.mvc.model.PostComment;
import cn.zuowenjun.java.mvc.model.mapper.PostCommentMapper;

public class PostCommentDaoImpl extends BaseDao implements PostCommentDao {

	@Autowired
	public PostCommentDaoImpl(DataSource dataSource) {
		super(dataSource);
		// TODO Auto-generated constructor stub
	}

	@Override
	public PostComment get(int id) {
		String sql = "select * from TA_TestPostComment where id=?";

		// 第一种:基于定义好的实现了RowMapper的PostCommentMapper实例
		// return this.getJdbcTemplate().queryForObject(sql,new Object[] {id},new
		// PostCommentMapper());

		// 第二种:直接使用匿名内部类(可以理解为与C#的委托类型)
//		return this.getJdbcTemplate().queryForObject(sql,new Object[]{id},new RowMapper<PostComment>() {
//
//			@Override
//			public PostComment mapRow(ResultSet rs, int rowNum) throws SQLException {
//				PostComment model=new PostComment();
//				model.setId(rs.getInt("id"));
//				model.setPostid(rs.getInt("postid"));
//				model.setContent(rs.getString("content"));
//				model.setCreateby(rs.getString("createby"));
//				model.setCreateTime(rs.getDate("createtime"));
//				return model;
//			}
//		});

		// 第三种:直接使用Lambda表达式,这个与C#lambda就比较像了,因为JAVA抄自C#
		return this.getJdbcTemplate().queryForObject(sql, new Object[] { id }, (rs, i) -> {
			PostComment model = new PostComment();
			model.setId(rs.getInt("id"));
			model.setPostid(rs.getInt("postid"));
			model.setContent(rs.getString("content"));
			model.setCreateby(rs.getString("createby"));
			model.setCreateTime(rs.getDate("createtime"));
			return model;
		});

	}

	@Override
	public List<PostComment> getList(int postId) {
		String sql = "select * from TA_TestPostComment where postid=?";
		return this.getJdbcTemplate().query(sql, new Object[] { postId }, new PostCommentMapper());
	}

	@Override
	public Boolean create(PostComment postCmmt) {
		String sql = "insert into TA_TestPostComment(id, postid, content, createby, createTime) values(?,?,?,?,?)";
		int r = this.getJdbcTemplate().update(sql, new Object[] { postCmmt.getId(), postCmmt.getPostid(),
				postCmmt.getContent(), postCmmt.getCreateby(), postCmmt.getCreateTime() });

		if (r > 0) {
			System.out.println("create is ok!");
			return true;
		} else {
			System.out.println("create is failed!");
			return false;
		}
	}

	@Override
	public Boolean delete(int id) {
		String sql = "delete from TA_TestPostComment where id=?";
		int r = this.getJdbcTemplate().update(sql, new Object[] { id });
		if (r > 0) {
			System.out.println("delete is ok!");
			return true;
		} else {
			System.out.println("delete is failed!");
			return false;
		}
	}

	@Override
	public Boolean update(PostComment postCmmt) {
		String sql = "update TA_TestPostComment set postid=?,content=?,createby=?,createTime=getdate() where id=?";
		int r = this.getJdbcTemplate().update(sql, (pss) -> {
			pss.setInt(1, postCmmt.getPostid());
			pss.setString(2, postCmmt.getContent());
			pss.setString(3, postCmmt.getCreateby());
			pss.setInt(4, postCmmt.getId());
		});

		if (r > 0) {
			System.out.println("update is ok!");
			return true;
		} else {
			System.out.println("update is failed!");
			return false;
		}

	}

}

PostCommentMapper类定义:

package cn.zuowenjun.java.mvc.model.mapper;

import java.sql.ResultSet;
import java.sql.SQLException;

import org.springframework.jdbc.core.RowMapper;

import cn.zuowenjun.java.mvc.model.PostComment;

public class PostCommentMapper implements RowMapper<PostComment> {

	@Override
	public PostComment mapRow(ResultSet rs, int rowNum) throws SQLException {
		PostComment model=new PostComment();
		model.setId(rs.getInt("id"));
		model.setPostid(rs.getInt("postid"));
		model.setContent(rs.getString("content"));
		model.setCreateby(rs.getString("createby"));
		model.setCreateTime(rs.getDate("createtime"));
		
		return model;
	}

}

  

涉及知识要点说明:

 1.JdbcTemplate支持的常见SQL参数类型有:Object[](NamedParameterJdbcTemplate不直接支持,可通过getJdbcTemplate获得JdbcTemplate,然后继续使用JdbcTemplate的CRUD用法),PreparedStatementCreator、PreparedStatementSetter

 2.JdbcTemplate返回结果常见处理转换类型有:RowMapper<T>、RowCallbackHandler、ResultSetExtractor<T> ,注意涉及数据索引都是从1开始

 3.无论是NamedParameterJdbcTemplate 还是JdbcTemplate的入参SQL参数类型、返回结果参数处理类型大部份都是函数式接口(标注了@FunctionalInterface),意味着我们可以直接使用匿名内部类或Lambda表达式来传参,如上面代码中的get方法,分别演示了使用PostCommentMapper、new RowMapper<PostComment>(){...}匿名内部类、(rs, i) -> {...}Lambda表达式,其它同理;

 4.JdbcTemplate的SQL语句中的参数命名规则为:?,与使用原生的JDBC 进行参数化查询用法相同

 NamedParameterJdbcTemplate 与JdbcTemplate 都实现JdbcOperations接口;

2.4定义Service接口:(在cn.zuowenjun.java.mvc.service包中分别定义了:PostService、PostCommentService、UserService 三个接口)

//PostService.java
package cn.zuowenjun.java.mvc.service;

import java.util.Date;
import java.util.List;

import cn.zuowenjun.java.mvc.model.Post;

public interface PostService {
	
	Post get(int id);
	List<Post> getList(Date frmDate,Date toDate);
	List<Post> getAll();
	int create(Post post);
	Boolean delete(int id);
	Boolean update(Post post);
}

//PostCommentService.java
package cn.zuowenjun.java.mvc.service;

import java.util.List;

import cn.zuowenjun.java.mvc.model.PostComment;

public interface PostCommentService {
	PostComment get(int id);
	List<PostComment> getList(int postId);
	Boolean create(PostComment postCmmt);
	Boolean delete(int id);
	Boolean update(PostComment postCmmt);
}

//UserService .java
package cn.zuowenjun.java.mvc.service;

public interface UserService {
	String login(String uid,String pwd);
	String logout();
	String getLoginUserName();
}

 如上代码所示,接口中只是定义了业务所需要的方法,可能大家看到觉得与dao接口很类似(示例代码中确实是相同的),但其实它们服务的对象不同,dao可能相比service更原子化,更单一 一些,dao只需要为单个表提供数据读写服务,而service则是为表现层(UI)提供真实的服务场景,而这些场景有可能是复杂的,包含多个dao的数据源或操作多个dao,同时还承担了业务数据的验证逻辑处理转换等功能,service层我理解是业务服务层(BLL),如果按照DDD来划分,我认为service层则是应用服务层或领域服务层

2.5实现Service接口:(在cn.zuowenjun.java.mvc.service.impl包中分别定义PostServiceImpl、PostCommentServiceImpl、UserServiceImpl类,它们实现了对应的service接口)

//PostServiceImpl.java
package cn.zuowenjun.java.mvc.service.impl;

import java.security.*;
import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.List;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;

import cn.zuowenjun.java.mvc.model.Post;
import cn.zuowenjun.java.mvc.service.PostService;
import cn.zuowenjun.java.mvc.dao.*;

@Service
public class PostServiceImpl implements PostService {

	@Autowired
	private PostDao postDao;
	
	@Override
	public Post get(int id) {
		return postDao.get(id);
	}

	@Override
	public List<Post> getList(Date frmDate, Date toDate) {
		long frmDateVal=frmDate.getTime();
		long toDateVal=toDate.getTime();
		
		if(frmDateVal>toDateVal) {
			throw new InvalidParameterException("开始时间需<=结束时间");
		}
		return postDao.getList(frmDate, toDate);
	}
	
	@Override
	public List<Post> getAll() {
        SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd");
		try {
			return getList(sdf.parse("1900-1-1"), sdf.parse("2099-12-1"));
		} catch (ParseException e) {
			return null;
		}
	}
	

	@Override
	public int create(Post post) {
		String result=verifyModel(post,true);
		if(!result.isEmpty()) {
			throw new InvalidParameterException(result);
		}
		return postDao.create(post);
	}

	@Override
	public Boolean delete(int id) {
		return postDao.delete(id);
	}

	@Override
	public Boolean update(Post post) {
		String result=verifyModel(post,true);
		if(!result.isEmpty()) {
			throw new InvalidParameterException(result);
		}
		return postDao.update(post);
	}
	
	private String verifyModel(Post post,Boolean isNew) {
		StringBuilder errMsgBuilder=new StringBuilder();
		
		if(!isNew && post.getId()<=0) {
			errMsgBuilder.append("ID不能为空!");
		}
		
		if(post.getTitle().trim().isEmpty()) {
			errMsgBuilder.append("标题不能为空!");
		}
		
		if(post.getContent().trim().isEmpty()) {
			errMsgBuilder.append("内容不能为空!");
		}
		
		if(post.getAuthor().trim().isEmpty()) {
			errMsgBuilder.append("作者不能为空!");
		}
		
		return errMsgBuilder.toString();
	}
}

//PostCommentServiceImpl.java
package cn.zuowenjun.java.mvc.service.impl;

import java.security.InvalidParameterException;
import java.util.Date;
import java.util.List;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;

import cn.zuowenjun.java.mvc.dao.PostCommentDao;
import cn.zuowenjun.java.mvc.model.PostComment;
import cn.zuowenjun.java.mvc.service.PostCommentService;

@Service
public class PostCommentServiceImpl implements PostCommentService {

	@Autowired
	private PostCommentDao postCommentDao;
	
	@Override
	public PostComment get(int id) {
		return postCommentDao.get(id);
	}

	@Override
	public List<PostComment> getList(int postId) {
		return postCommentDao.getList(postId);
	}

	@Override
	public Boolean create(PostComment postCmmt) {
		String result=verifyModel(postCmmt,true);
		if(!result.isEmpty()) {
			throw new InvalidParameterException(result);
		}
		postCmmt.setCreateTime(new Date());
		return postCommentDao.create(postCmmt);
	}

	@Override
	public Boolean delete(int id) {
		return postCommentDao.delete(id);
	}

	@Override
	public Boolean update(PostComment postCmmt) {
		String result=verifyModel(postCmmt,false);
		if(!result.isEmpty()) {
			throw new InvalidParameterException(result);
		}
		
		return postCommentDao.update(postCmmt);
	}
	
	private String verifyModel(PostComment postCmmt,Boolean isNew) {
		StringBuilder errMsgBuilder=new StringBuilder();
		
		if(!isNew && postCmmt.getId()<=0) {
			errMsgBuilder.append("ID不能为空!");
		}
		
		if(postCmmt.getPostid()<=0) {
			errMsgBuilder.append("文章ID不能为空!");
		}
		
		if(postCmmt.getContent().trim().isEmpty()) {
			errMsgBuilder.append("内容不能为空!");
		}
		
		if(postCmmt.getCreateby().trim().isEmpty()) {
			errMsgBuilder.append("回复者不能为空!");
		}
		
		return errMsgBuilder.toString();
	}

}


//UserServiceImpl.java
package cn.zuowenjun.java.mvc.service.impl;

import cn.zuowenjun.java.mvc.service.UserService;
import java.util.*;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpSession;

import org.springframework.stereotype.Service;
import org.springframework.web.context.request.*;

@Service
public class UserServiceImpl implements UserService {

	@Override
	public String login(String uid, String pwd) {
		if(uid.isEmpty() || pwd.isEmpty()) {
			return "用户名与密码都不能为空!";
		}
		
		ResourceBundle userRes= ResourceBundle.getBundle("user");
		String configUid= userRes.getString("user.userid");
		String configPwd=userRes.getString("user.password");
		
		if(configUid.equals(uid) && configPwd.equals(pwd)) {
			
			String configUName=userRes.getString("user.username");
			
			HttpSession session= getRequest().getSession();
			session.setAttribute("loginUid", uid);
			session.setAttribute("loginUname",configUName);
			return null;
		}else{
			return "用户名或密码不正确!";
		}
	}

	@Override
	public String logout() {
		try {
			getRequest().getSession().removeAttribute("loginUid");
			return null;
		}catch(Exception ex) {
			return ex.getMessage();
		}
		
	}

	@Override
	public String getLoginUserName() {
		Object loginUnameObj= getRequest().getSession().getAttribute("loginUname");
		if(loginUnameObj==null) {
			return null;
		}else{
			return  (String)loginUnameObj;
		}
	}

	private HttpServletRequest getRequest() {
		HttpServletRequest  request= ((ServletRequestAttributes)RequestContextHolder.getRequestAttributes()).getRequest();
		return request;
	}
}

 注意:登录服务中我使用了user.properties属性配置文件,username中的中文自动转码了。

user.userid=admin
user.password=www.zuowenjun.cn.java
user.username=\u68A6\u5728\u65C5\u9014  

如上代码所示,所有的service实现类都标注了@Service注解,之前的dao实现类也都标注了@Repository,目的是为了实现spring容器的自动扫描并注册到IOC容器中,正如我在上面讲到的springmvc-servlet.xml配置文件中说的一样;另外service实现类除了调用dao完成数据的操作,另外还有业务数据的校验,比如代码中的:verifyModel方法等,当然实际的大型项目中可能业务逻辑复杂得多,模型验证也可以通过注解来实现,与C#中在System.ComponentModel.DataAnnotations下的相关特性类似,大家有兴趣可以查阅相关资料;

2.6设计Controller及View:

 2.6.1创建一个BlogController类,用于处理管理博文(查看、发表、修改、删除)、评论(发表)的相关ACTION,如下:

package cn.zuowenjun.java.mvc.controller;

import java.io.IOException;
import java.security.InvalidParameterException;
import java.text.*;
import java.util.*;

import javax.servlet.http.HttpServletResponse;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.propertyeditors.CustomDateEditor;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.ui.ModelMap;
import org.springframework.web.bind.WebDataBinder;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.context.request.WebRequest;
import org.springframework.web.servlet.ModelAndView;
import org.springframework.web.servlet.mvc.support.RedirectAttributes;

import cn.zuowenjun.java.mvc.model.Post;
import cn.zuowenjun.java.mvc.model.PostComment;
import cn.zuowenjun.java.mvc.service.*;
/**
 * 
 * @author zuowenjun.cn
 *refer-mavendepconfig:https://blog.csdn.net/qq_29227939/article/details/52063869
 *refer-EL:http://www.cnblogs.com/dongfangshenhua/p/6731421.html
 */
@Controller
@RequestMapping("/blog")
public class BlogController {
	
	@Autowired
	private PostService postService;
	
	@Autowired
	private PostCommentService postCommentService;
	
	@RequestMapping()
	public ModelAndView list() {
		List<Post> postList= postService.getAll();
		ModelAndView mv=new ModelAndView();
		mv.addObject("posts",postList);
		mv.setViewName("bloglist");

		return mv;
	}
	
	@RequestMapping(path="/querylist",method=RequestMethod.POST)
	public ModelAndView list(@RequestParam(required=true) Date frmDate,@RequestParam(required=true) Date toDate,ModelAndView mv) {
		List<Post> postList=postService.getList(frmDate, toDate);
		mv.setViewName("bloglist");
		mv.addObject("posts",postList);
		return mv;
	}
	
	@RequestMapping("/post/{postid}")
	public String detail(@PathVariable String postid,ModelMap model) {
		int pid=Integer.parseInt(postid);
		model.put("post", postService.get(pid));
		model.put("comments", postCommentService.getList(pid));
		
		return "blogdetail";
	}

	
	@RequestMapping(path="/savecomment",method=RequestMethod.POST)
	public String saveComment(@ModelAttribute() PostComment postComment,RedirectAttributes redirectAttr) {
		String resultMsg="评论保存成功";
		if(!postCommentService.create(postComment)) {
			resultMsg="评论保存失败,请稍后重试";
		}
		redirectAttr.addFlashAttribute("msg", resultMsg);
		
		return "redirect:/blog/post/" + postComment.getPostid();
	}
	
	@RequestMapping(path="/editpost/{postid}",method=RequestMethod.GET)
	public ModelAndView editPost(@PathVariable(required=true) int postid) {
		ModelAndView mv=new ModelAndView();
		Post post=null;
		post=postService.get(postid);
		if(post==null) {
			throw new InvalidParameterException("无效的postid");
		}
		mv.addObject("post", post);
		mv.setViewName("blogedit");
		
		return mv;
	}
	
	@RequestMapping("/editpost")
	public String createPost(Map<String,Object> viewDataMap) {
		Post post=new Post();
		viewDataMap.put("post", post);
		return "blogedit";
	}
	
	@RequestMapping(path="/editpost",method=RequestMethod.POST)
	public String updatePost(@ModelAttribute("post") Post post,@RequestParam("doAction") String action,Model model,
			HttpServletResponse reponse) throws IOException {
		
		String result="保存成功!";
		if(action.equals("delete")) { //删除操作
			if(!postService.delete(post.getId())){
				result="删除失败,请重试!";
			}else {
				String jsResult="<script>alert(''删除成功!'');self.close();</script>";
				reponse.setContentType("text/html;charset=utf-8");
				reponse.getWriter().append(jsResult);
				return null;
			}
		}
		else { //编辑操作
			
			if(post.getId()<=0) { //新增博文逻辑
				int postId= postService.create(post);
				if(postId>0) {
					post.setId(postId);
				}else {
					result="保存失败,请重试!";
				}
			}else if(!postService.update(post)) { //更新博文逻辑
				result="保存失败,请重试!";
			}
		}
		model.addAttribute("result", result);
		return "blogedit";
	}

	
	
	@InitBinder
	public void initBinder(WebDataBinder binder, WebRequest request) {
		
		//转换日期
		DateFormat dateFormat=new SimpleDateFormat("yyyy-MM-dd");
		binder.registerCustomEditor(Date.class, new CustomDateEditor(dateFormat, true));// CustomDateEditor为自定义日期编辑器
	}
	
}

 创建AccountController类,用于提供登录、登出的途径,代码如下:

package cn.zuowenjun.java.mvc.controller;

import java.io.IOException;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.servlet.ModelAndView;

import cn.zuowenjun.java.mvc.service.UserService;

/**
 * seeparambind:http://www.cnblogs.com/xiaoxi/p/5695783.html
 * 
 */
@Controller
@RequestMapping("/account")
public class AccountController {
	
	@Autowired
	private UserService userService;
	
	@RequestMapping("/signin")
	public String signIn() {
		return "signin";
	}
	
	@RequestMapping(path="/signin",method=RequestMethod.POST)
	public ModelAndView signIn(@RequestParam(required=true) String uid,@RequestParam(required=true) String pwd) {
		String loginResult=userService.login(uid, pwd);
		ModelAndView mv= new ModelAndView();
		if(loginResult==null || loginResult.isEmpty()) {//登录成功跳转
			mv.setViewName("redirect:/blog");
		}
		else {
			mv.setViewName("signin");
			mv.addObject("message",loginResult==null || loginResult.isEmpty()?"登录成功":"登录失败:" + loginResult);
		}
		
		return mv;
	}
	
	@RequestMapping("/signout")
	public void signOut(HttpServletRequest request, HttpServletResponse response) throws IOException {
		userService.logout();
		response.sendRedirect(request.getContextPath() + "/account/signin");
	}
}

温馨提示:由于是DEMO演示,故我在controller中尽可能的使用不同的方式来完成各种逻辑,以便大家看到应用的效果,实际项目中不会是这样的。

关于Controller类涉及如下知识点说明:

  a.命名规范,统一使用资源名(一般是名词)+controller结尾,如示例:BlogController,AccountController,尽量符合REST的风格,同时类上标注:@Controller,以便告诉spring容器这是一个Controller的bean;

  b.@RequestMapping:指定映射的请求路径,与ASP.NET MVC的Route特性有异由同工之效,详细用法可参考:https://www.iteye.com/news/32657/ 、https://www.cnblogs.com/jpfss/p/8047628.html

  c.Action获取请求参数(按照ASP.NET MVC的说法就是:Model Binding),详细用法可参考:https://www.cnblogs.com/xiaoxi/p/5695783.html

  d.Action指定视图以及为View指定Model数据(传值给view):可以使用ModelAndView、Model、ModelMap、Map<String,Object>、@ModelAttribute(作用于ACTION上)、HttpServletRequest(其实前面的几种方法底层最终都是通过HttpServletRequest.setAttribute来实现的),注意除了可以直接返回ModelAndView,因为它包含了setView的方法,其余的都应该返回String的viewName,可以对照上面的示例代码看一下,也可参见:https://blog.csdn.net/u012190514/article/details/80237885

  e:Controller间或Action间的跳转,使用:redirect:ACTION路径,ACTION传参多种方法详见:https://blog.csdn.net/jaryle/article/details/52263717,,我主要想特别说明的是RedirectAttributes,它不用URL传参,而是采用一次性session的模式,类似ASP.NET MVC中的TempData

  2.6.2 创建相关UI视图页面(根据InternalResourceViewResolver配置的视图解析位置及后缀名(/WEB-INF/jsp/、.jsp)在/WEB-INF/jsp/创建相关的jsp文件),分别有:signin.jsp(登录)、bloglist.jsp(博客列表主页)、blogdetail.jsp(博文详情,评论)、blogedit.jsp(博文编辑,新增、修改、删除),具体代码如下:

 登录:

<%@ page language="java" contentType="text/html; charset=UTF-8"
    pageEncoding="UTF-8" isELIgnored="false" %>
<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core"%>
<%@ taglib prefix="fn" uri="http://java.sun.com/jsp/jstl/functions"%>

<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>梦在旅途演示博客-登录</title>
<style type="text/css">
	#loginbox{
		width:300px;
		margin:100px auto 0 auto;
		padding:50px;
		border:5px groove gray;
	}
	
	#loginbox div{
		margin:20px auto;
	}
	
	.txtcenter{
		text-align:center;
	}
</style>
</head>
<body>
	<form method="post">
	<div id="loginbox">
		<div><h3>欢迎,请登录!</h3></div>
		<div>用户ID:<input type="text" id="txtuid" name="uid" /></div>
		<div>密     码:<input type="password" id="txtpwd" name="pwd" /></div>
		<div>
			<input type="submit" id="btnSubmit" value="登 录" />
			<input type="reset" id="btnReset" value="重置" />
		</div>
	</div>
	</form>
	<div>
		<c:if test="${message!=null}">
			<p>${message}</p>
		</c:if>
	</div>
	<p>Copyright 2018  zuowj.cnblogs.com and zuowenjun.cn demo.</p>
</body>
</html>

博客列表主页、博文详情,评论:

<!-- bloglist.jsp -->
<%@ page language="java" contentType="text/html; charset=UTF-8"
	pageEncoding="UTF-8" isELIgnored="false"%>
<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core"%>
<%@ taglib prefix="fn" uri="http://java.sun.com/jsp/jstl/functions"%>
<%@ taglib prefix="fmt" uri="http://java.sun.com/jsp/jstl/fmt"%>

<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>博客列表</title>
</head>
<body>
	<div>
		<span>[ ${sessionScope.loginUid }(${sessionScope.loginUname }),
		<a href="${pageContext.request.contextPath}/account/signout">[退出]</a> ]</span>   
		<a href="${pageContext.request.contextPath}/blog/editpost" target="_blank">[ +发表博文 ]</a>
	</div>
	<div>
		<form method="post" action="${pageContext.request.contextPath }/blog/querylist">
			<fieldset>
				<legend>范围查询:</legend>
				<span>开始时间:</span> <input type="text" name="frmDate" 
					value="${param.frmDate }"
					placeholder="yyyy-MM-dd" />  
				<span>结束时间:</span> <input type="text"
					name="toDate" placeholder="yyyy-MM-dd"  
					value="${param.toDate}" />
				<button id="btnquery">查询</button>
			</fieldset>
		</form>
	</div>
	<c:choose>
		<c:when test="${posts!=null && posts.size()>0}">
			<c:forEach items="${posts}" var="item">
				<div>
					<h2>
						<a href="${pageContext.request.contextPath}/blog/post/${item.id }"
							target="_blank">${item.title}</a>  
							<a href="${pageContext.request.contextPath}/blog/editpost/${item.id }" 
							target="_blank">[修改]</a>
					</h2>
					<h4>
						作者:${item.author },时间:
						<fmt:formatDate value="${item.createTime}" pattern="yyyy-MM-dd" />
					</h4>
					<p>${item.content }</p>
				</div>
				<hr />
			</c:forEach>
		</c:when>
		<c:otherwise>
			<p>没有任何博客文章记录!</p>
		</c:otherwise>
	</c:choose>

</body>
</html>



<!-- blogedit.jsp -->
<%@ page language="java" contentType="text/html; charset=UTF-8"
	pageEncoding="UTF-8" isELIgnored="false"%>
<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core"%>
<%@ taglib prefix="fn" uri="http://java.sun.com/jsp/jstl/functions"%>
<%@ taglib prefix="fmt" uri="http://java.sun.com/jsp/jstl/fmt"%>

<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>博文详情:${post.title }</title>
</head>
<body>
	<div>
		<h2>
			${post.title}
		</h2>
		<h4>
			作者:${post.author },
			时间:<fmt:formatDate value="${post.createTime}" pattern="yyyy-MM-dd" />
		</h4>
		<p>${post.content }</p>
	</div>
	<hr/>
	<div>
		<c:choose>
			<c:when test="${comments!=null && fn:length(comments)>0 }">
				<c:forEach items="${ comments}" var="item">
					<div>
						<div>
						${item.createby } 回复于:<fmt:formatDate value="${item.createTime}" pattern="yyyy-MM-dd HH:mm" />
						</div>
						<div>
							${item.content }
						</div>
					</div>
				</c:forEach>
			</c:when>
			<c:otherwise>
				<p>暂无相关评论!</p>
			</c:otherwise>
		</c:choose>
		<div>
			<form method="post" action="../savecomment">
			<h3>发表新评论:</h3>
			<p>评论人:<input type="text" id="createby" name="createby" /></p>
			<p>评论内容:</p>
			<p><textarea rows="5"id="content" name="content"></textarea>
			</p>
			<p>
				<button id="btnreply">提交评论</button>
				<input type="hidden" name="postid" value="${post.id }" />
			</p>
			</form>
			<div>
				<c:if test="${msg!=null}">
					<p>提交评论结果:${msg}</p>
				</c:if>
			</div>
		</div>
	</div>
</body>
</html>

博文编辑:

<%@ page language="java" contentType="text/html; charset=UTF-8"
	pageEncoding="UTF-8" isELIgnored="false" import="cn.zuowenjun.java.mvc.model.*" %>
<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core"%>
<%@ taglib prefix="fn" uri="http://java.sun.com/jsp/jstl/functions"%>
<%@ taglib prefix="fmt" uri="http://java.sun.com/jsp/jstl/fmt"%>
<%@ taglib prefix="form" uri="http://www.springframework.org/tags/form"%>

<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>
<%-- ${post.id>0?"编辑"+ post.title:"新增博文" } --%>
<%
	Post post=(Post)request.getAttribute("post");
	if(post==null){
		out.print("post is null!");
		return;
	}
	
	if(post.getId()>0){
		out.print("编辑"+ post.getTitle());
	}else{
		out.print("新增博文");
	}
%>
</title>
</head>
<body>
	<form:form modelAttribute="post" method="POST" id="mainForm"
		action="${pageContext.request.contextPath }/blog/editpost">
		<div>文章标题:</div>
		<div>
			<form:input path="title" />
		</div>
		<div>作者:</div>
		<div>
			<form:input path="author" />
		</div>
		<div>文章内容:</div>
		<div>
			<form:textarea path="content" rows="10"/>
		</div>
		<div>
			<button type="button" id="btnSave" data-action="update" onclick="javascript:doSubmit(this);">保存</button>
			<c:if test="${post.id>0 }">
			<button type="button" id="btnDelete" data-action="delete"onclick="javascript:doSubmit(this);">删除</button>
			</c:if>
			<form:hidden path="id"/>
			<input type="hidden" name="doAction" id="_doAction" />
		</div>
	</form:form>
	<c:if test="${result!=null && fn:length(result)>0 }">
		<p>操作结果:${result}</p>
	</c:if>
	<script type="text/javascript">
		function doSubmit(btn){
			if(!confirm("你确定要" + btn.innerText +"吗?")){
				return false;
			}
			var actionVal=btn.getAttribute("data-action");
			//alert(actionVal);
			document.getElementById("_doAction").setAttribute("Value",actionVal);
			document.getElementById("mainForm").submit();
		}
	</script>
</body>
</html>

jsp视图涉及知识点说明:

 a.JSP EL表达式:简化JAVA代码在前台的使用,具体用法详见:http://www.runoob.com/jsp/jsp-expression-language.html

 b.JSP JSTL(标准标签库):是一个JSP标签集合,它封装了JSP应用的通用核心功能,支持通用的、结构化的任务,比如迭代,条件判断,XML文档操作,国际化标签,SQL标签等,具体用法详见:http://www.runoob.com/jsp/jsp-jstl.html

 个人认为如果需要用好JSP视图,就需要熟悉EL及JSTL这两个利器,如果是采用前后端分离那就另当别论; 

2.7.为springMVC网站增加统一拦截器,实现统一的身份登录状态验证

2.7.1定义一个实现了HandlerInterceptor接口的拦截器类:LoginValidationInterceptor,然后重写preHandle方法,在里面根据session来判断登录状态,若没有登录则跳转至登录页面,代码如下:

package cn.zuowenjun.java.mvc.service.impl;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

import org.springframework.web.servlet.HandlerInterceptor;

public class LoginValidationInterceptor implements HandlerInterceptor {
	
	@Override
	public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler)
			throws Exception {
			
			String url = request.getRequestURI();
			if(url.indexOf("/signin")>0) {//登录页面无需验证登录,放行
				return true;
			}
			
			if(request.getSession().getAttribute("loginUid")==null) {//检测到未登录,转到登录页面
				response.sendRedirect(request.getContextPath() + "/account/signin");
				return false;
			}else {
				return true;
			}
	}
	
	//其余postHandle、afterCompletion方法未重写,直接使用默认实现,这与C#有区别(C#8.0中也会有默认实现)

}

2.7.2在springmvc-servlet.xml中注册拦截器,并指定拦截URL的匹配路径,如下:

<!-- 配置拦截器 -->
	<mvc:interceptors>
		<mvc:interceptor>
			<mvc:mapping path="/**"/><!-- 拦截所有请求, /表示只拦截非JSP的请求,/*只拦截一级目录,/**拦截所有目录 -->
			<bean></bean>
		</mvc:interceptor>
	</mvc:interceptors>

 如上两步即完成了拦截器的定义及配置,这样当有请求过来后就会进入拦截器,当然也可以使用JSP WEB中的filter过滤器来实现,两者的区别,详见:https://blog.csdn.net/xiaoyaotan_111/article/details/53817918

2.8.为springMVC网站增加自定义错误页面

2.8.1统一处理错误有多种方法,如:@ExceptionHandler、@ResponseStatus、@ControllerAdvice+@ExceptionHandler,当然这些背后都是使用了默认的HandlerExceptionResolver的实现,详见:http://www.cnblogs.com/xinzhao/p/4902295.html,本文采用@ControllerAdvice+@ExceptionHandler的方式来进行统一处理异常,代码如下:

package cn.zuowenjun.java.mvc.service.impl;

import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.ResponseBody;
import org.springframework.web.bind.annotation.ResponseStatus;
import org.springframework.web.servlet.ModelAndView;

import javax.servlet.http.HttpServletRequest;

import org.springframework.http.*;
import org.springframework.http.converter.HttpMessageNotReadableException;
import org.springframework.web.*;

/**
 * 
 * @author Zuowenjun
 *refer http://www.cnblogs.com/xinzhao/p/4902295.html
 */
@ControllerAdvice
public class WebExceptionHandler {
	

    @ResponseStatus(HttpStatus.BAD_REQUEST)
    @ExceptionHandler(HttpMessageNotReadableException.class)
    public void handleHttpMessageNotReadableException(HttpMessageNotReadableException e) {
    }
    
    /**
     * 405 - Method Not Allowed
     */
    @ResponseStatus(HttpStatus.METHOD_NOT_ALLOWED)
    @ExceptionHandler(HttpRequestMethodNotSupportedException.class)
    public void handleHttpRequestMethodNotSupportedException(HttpRequestMethodNotSupportedException e) {
        
    }
    

    /**
     * 415 - Unsupported Media Type
     */
    @ResponseStatus(HttpStatus.UNSUPPORTED_MEDIA_TYPE)
    @ExceptionHandler(HttpMediaTypeNotSupportedException.class)
    public void handleHttpMediaTypeNotSupportedException(Exception e) {

    }

//    /**
//     * 500 - Internal Server Error
//     */
//    @ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR)
//    @ExceptionHandler(Exception.class)
//    public void handleException(Exception e) {
//    	
//    }
    
    // 创建ModleAndView,将异常和请求的信息放入到Model中,指定视图名字,并返回该ModleAndView
    @ExceptionHandler(Exception.class)
    public ModelAndView handleError(HttpServletRequest req, Exception exception) {
      ModelAndView mv = new ModelAndView();
      mv.addObject("exception", exception);
      mv.addObject("url", req.getRequestURL());
      mv.setViewName("error");
      
      return mv;
    }
}

对应的error.jsp 、notfound.jsp视图页面代码如下:(其中notfound.jsp中重新设置了staus,目的是为了兼容IE,如果是404,那么IE将会显示IE的默认404页面)

<!--error.jsp-->
<%@ page language="java" contentType="text/html; charset=UTF-8"
	pageEncoding="UTF-8" isELIgnored="false" import="java.io.*,java.lang.*" %>
<%!

	//获取完整堆栈信息
	String getStackTrace(Throwable throwable){
	StringWriter stringWriter=new StringWriter();
	PrintWriter printWriter=new PrintWriter(stringWriter);

	try {
		throwable.printStackTrace(printWriter);
		return stringWriter.toString();
	}finally {
		printWriter.close();
	}
}

%>
<%
	Exception ex= (Exception)request.getAttribute("exception");
%>
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>发生错误!</title>
</head>
<body>
	<h2>对不起,处理请求时发生错误!</h2>
	<p>错误信息:${exception.getMessage() }</p>
	<p>错误详情:<%=getStackTrace(ex) %></p>
	<p>请求URL:${url}</p>
	<p><a href="javascript:history.back();">[返回]</a></p>
</body>
</html>


<!--notfound.jsp-->
<%@ page language="java" contentType="text/html; charset=UTF-8"
	pageEncoding="UTF-8" isELIgnored="false"%>
<%
	response.setStatus(200);
%>
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>404-页面不存在!</title>
</head>
<body>
	<div>
		<p>你访问的资源不存在,可能被外星人吃掉了!</p>
		<p>请求URL:<span id="rawUrl"></span></p>
	</div>
	
	<script type="text/javascript">
		window.onload=function(){
			document.getElementById("rawUrl").innerHTML=window.location.href;
		};
	</script>
</body>
</html>  

效果展示:

登录:

  

博客主页:

博文详情及评论:

编辑博文:

当页面出现错误时:

          404页面:

 

最后小结

1.本文内容涵盖了:Maven、SpringMVC、SpringJDBC,相关的常见知识点都有在示例代码中呈现出来了,有利于理解;

2.本文的springMVC Demo网站虽然简单,但基本把相关的功能都完成了,大家可以参照示例代码进行学习与深入,源代码Git地址:https://github.com/zuowj/mvnspringmvc

3.springMVC打包WAR包的方式请参见:https://www.cnblogs.com/qlqwjy/p/8231032.html,多项目部署到同一个tomcat的方法请参见:https://blog.csdn.net/dreamstar613/article/details/75282962/,如果出现打包或部署相关错误请根据错误描述自行百度,可能坑有点多;

比如:打包时可能报编译JDK的问题或类型无法解析问题,这时可能需要配置maven的编译插件,详见:https://www.cnblogs.com/softidea/p/6256543.html

如果报:Cannot change version of project facet Dynamic Web Module to 3.0.详见:https://blog.csdn.net/xiongyouqiang/article/details/79130656

4.本文中对于一些关键的知识点都有说明,同时有与ASP.NET MVC 进行对照说明,以便JAVA,.NET开发者可以相互快速了解与上手;

5.由于目前都是使用前后端分离的模式,同时由于目前都是使用ORM,且虽然目前的手动配置依赖相比之前的JSP WEB手动引入依赖包要方便,但还是有点效率低下,故下一篇计划讲解: 基于springMVC实现Reset API,熟悉:Spring Boot+Mybatis+SpringMVC,敬请期待,谢谢!

注:文章若有不足之处欢迎评论交流,谢谢!(本文从开始写到发表断断续续的差不多耗时了3周时间,本来计划元旦前发出但由于工作原因未能及时总结,故拖到现在才发表)

 

关于springmvc下的基于token的防重复提交springmvc防止重复提交的问题我们已经讲解完毕,感谢您的阅读,如果还想了解更多关于asp.net core MVC之实现基于token的认证、java day61【 SpringMVC 的基本概念 、 SpringMVC 的入门 、 请求参数的绑定 、常用注解 】、java day62【 响应数据和结果视图 、 SpringMVC 实现文件上传 、 SpringMVC 中的异常处理 、 SpringMVC 中的拦截器 】、JAVA WEB快速入门之从编写一个基于SpringMVC框架的网站了解Maven、SpringMVC、SpringJDBC等相关内容,可以在本站寻找。

本文标签: