学习Spring这些技术性框架,光掌握理论知识是远远不够了,我们要懂得学以致用,用键盘将学到的敲出来,在正确与错误中寻找Spring的用法。

      为了给读者一个直观的概念,这里我用Spring搭建一个简单的登录,可以让你很快的了解Spring在持久层、业务层、表现层是怎么运作的,这样后面我们分模块讲解的时候,读者也能很快的知道。

     本文所用工具为Eclipse IDE,数据库为Oracle 11g。

     首先我们来了解登录这个功能,用户访问登录页面,输入账号和密码,点击登录,后台验证是否有账号和密码匹配,如果匹配成功,则跳到欢迎页面,并显示用户积分,如果登录失败则返回登录页面,并提示登录失败信息。

    简化图:

    wKioL1e2Xnni1dJXAAAaEcfSJQA181.png

 有经验的程序员知道在开发之前都会先写开发设计或者画类图,这里面笔者也从开发设计开始,针对登录的全过程先设计好再开发,毕竟磨刀不误砍材功。

    上面是登录功能设计的全部过程大致描述一下:

wKioL1e2bn_yL2l5AABR7JviNvI265.png

1.首先访问login.jsp,返回可输入用户账号以及密码的表单登录页面

2.用户在登录页面输入账号、密码,提交表单到服务器,Spring根据配置调用LoginController控制器响应请求。

3.LoginController调用LoginService/isExistUser方法,根据传递的用户、密码参数查询是否存在用户,LoginService内部通过持久层LoginDao与数据进行交互。

4.如果匹配不到用户,则返回Login.jsp,并返回提示信息,如果匹配成功,则进入下一步。

5.LoginController调用LoginService/findUserByUsername获取当前登录人的实体,设置当前登录时间以及积分更新后的值。

6.LoginController调用LoginService/loginsuccess()方法,进行登录成功后的业务处理,首先更新用户信息,以及创建LoginInfo日志对象并插入数据库。

7.重定向欢迎页面welcome.jsp页面,并返回响应。

环境准备:

1.表创建

create table TB_USER   --用户表
(
  userid          VARCHAR2(20),
  username        VARCHAR2(20),
  credits         NUMBER,
  password        VARCHAR2(100),
  last_login_time DATE,
  last_login_ip   VARCHAR2(30)
)
create table TB_LOGIN_LOG --用户登录日志
(
  login_id      VARCHAR2(20),
  login_user_id VARCHAR2(20),
  ip            VARCHAR2(20),
  login_time    DATE
)
--插入一条用户
insert into tb_user (USERID, USERNAME, CREDITS, PASSWORD, LAST_LOGIN_TIME, LAST_LOGIN_IP)
values ('zhangsan', '张三', 0, '123456', to_date('06-07-2016 09:10:39', 'dd-mm-yyyy hh24:mi:ss'), '0:0:0:0:0:0:0:1');

2.建立工程

wKioL1e2cxyigYQ_AAAt88MbgS8270.png

    良好的包分类以及分层会给维护以及其他开发带来的很大的好处,一般企业项目中包的分类还应该按项目模块分,本文的项目较简单,因此分类就简单化,同时对于Spring 的配置文件一般都会有文件夹存放,这里我们就不详细说明,相信读者在工作中看到的项目大多数都是结构良好的。

   关于Spring3.0相关jar,这里面读者可以自己去下载。

3.创建实体类

   我们可以认为实体类是贯穿业务层、持久层、表现层的一条线,一个功能模块往往是围绕一个实体类,业务层修改实体类状态,持久层将实体类存储到数据库,表现层展现实体类的状态,可见实体类的重要性。

public class User {
	 private String userid;//用户ID
	 private String username;//用户姓名
	 private Integer credits;//积分
	 private String password;//密码
	 private Date lastlogintime;//最后登录时间
	 private String loginip;//最后登录ip
	 }
public class LoginInfo {
    private String loginid;//登录主键
    private String loginuserid;//登录人ID
    private String loginip;//登录IP
    private Date logintime;//登录时间
    }
......省略get、set方法

4.持久层LoginDao与LoginLogDao

 我们先看看关于操作User的DAO,主要包括3个方法

1.getUserCount(),返回结果为×××,如果返回1,则表示用户、密码匹配正确,返回0则表示用户、密码匹配错误,实际中这里存在密码加密解密功能,这里我们简单化。

2.findUserByUsername(),根据用户账号获取User实体。

3.updateUserInfo(),更新用户最后登录时间、 积分以及用户IP.

import java.sql.ResultSet;
import java.sql.SQLException;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.jdbc.core.RowCallbackHandler;
import org.springframework.stereotype.Repository;
import domain.User;
@Repository //通过Spring注解定义一个Dao
public class LoginDao {
    
     @Autowired//自动注入id为JdbcTemplate的bean
     private JdbcTemplate jdbcTemplate; 
     /**
      * 
       * @author zangyn
       * @date 2016-8-19 下午1:54:16
       * @Title: getUserCount
       * @Description: 根据账号、密码匹配是否存在
       * @return int    返回类型
       * @throws
      */
     public int getUserCount(String username,String password){
    	 String sql="SELECT COUNT(*) FROM TB_USER U WHERE U.USERID=? AND U.PASSWORD=?";
		 int count=jdbcTemplate.queryForInt(sql,new Object[]{username,password});
    	 return count;
	}
	/**
	 * 
	  * @author zangyn
	  * @date 2016-8-19 下午1:54:43
	  * @Title: findUserByUsername
	  * @Description: 根据Userid查询是否存在该User
	  * @return User    返回类型
	  * @throws
	 */
	public User findUserByUsername(String userid){
		String sql="SELECT * FROM TB_USER U WHERE U.USERID=?";
		final User u=new User();
		jdbcTemplate.query(sql, new Object[]{userid}, new RowCallbackHandler() {
			
			@Override
			public void proce***ow(ResultSet rs) throws SQLException {
				u.setUserid(rs.getString("userid"));
				u.setUsername(rs.getString("username"));
			   	u.setCredits(rs.getInt("credits"));
			}
		});
		return u;
	}
	/**
	 * 
	  * @author zangyn
	  * @date 2016-8-19 下午1:55:28
	  * @Title: updateUserInfo
	  * @Description: 更新用户相关信息
	  * @return void    返回类型
	  * @throws
	 */
	
	public void updateUserInfo(User user){
		String sql="UPDATE TB_USER U SET U.LAST_LOGIN_TIME=?,U.LAST_LOGIN_IP=?,U.CREDITS=? WHERE U.USERID=?";
		jdbcTemplate.update(sql, new Object[]{user.getLastlogintime(),user.getLoginip(),user.getCredits(),user.getUserid()});
	}
}

   Spring2.5以后,可以使用注解的方式定义Bean,相比较XML配置,注解配置简单太多,所以我们尽量要以注解为主进行Bean配置。

这里我们用@Repository定义了Dao bean的配置,使用 @Autowired将容器的Bean注入进来。

  从上面可以看到关于数据库操作,我们引入了Spring JdbcTemplate,用过原始JDBC的都知道,在执行数据库操作之前都需要经过:获取连接>创建Statement>执行数据库操作>获取结果>关闭Statement>关闭结果集>关闭连接,同时我们还要关注异常的处理,每次执行sql,我们的1/3代码都重复的写以上的代码,对于以上样板式的代码,Spring JDBC进行了薄层的封装,将样板式的代码进行了封装,用户只需要编写那些必不可少的代码。

关于拼写SQL注意:在拼写较长的sql时,我们经常需要换行,由于上下行最终会转换为一个sql,这时候如果在每行的末尾没有空格,就会存在连在一起的情况,因此,良好的拼写SQL方法是在每行开始和结尾都打一个空格。

 关于JdbcTemplate各种方法运用,我会在后面一篇详细介绍。

 LoginLogDao只有一个存放日志记录的方法,代码如下:

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.stereotype.Repository;
import domain.LoginInfo;

@Repository //通过Spring注解定义一个Dao
public class LoginLogDao {
	
	@Autowired//自动注入id为JdbcTemplate的bean
     private JdbcTemplate jdbcTemplate;
	 public void insertLoginfo(LoginInfo info){
		 String sql="INSERT INTO TB_LOGIN_INFO(LOGIN_ID,LOGIN_USER_ID,IP,LOGIN_TIME) VALUES(login_info_seq.nextval,?,?,?)";
		 Object[] obj=new Object[]{info.getLoginuserid(),info.getLoginip(),info.getLogintime()};
		 jdbcTemplate.update(sql, obj);
	 }
}

5.数据源配置以及Spring 加载Dao

在src下创建一个名为applicationContext.xml的文件,配置文件内容如下:

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
	xmlns:context="http://www.springframework.org/schema/context"
	xsi:schemaLocation="http://www.springframework.org/schema/beans
	http://www.springframework.org/schema/beans/spring-beans-3.0.xsd
	http://www.springframework.org/schema/context
	http://www.springframework.org/schema/context/spring-context-3.0.xsd">
	
<!-- ①自动扫描com.gloryscience.dao包下的所有类,将带有注解的类 纳入spring容器管理-->
<context:component-scan base-package="dao"></context:component-scan>
<!-- ②定义一个使用DBCP实现的数据源-->
 <bean id="dataSourceDBCP"  class="org.apache.commons.dbcp.BasicDataSource" destroy-method="close">
      <property name="driverClassName" value="oracle.jdbc.driver.OracleDriver"/>  
        <property name="url" value="jdbc:oracle:thin:@127.0.0.1:1521:orcl"/>  
        <property name="username" value="gloryseience"/>  
        <property name="password" value="gloryseience"/>  
     </bean>
     <!-- ③定义Jdbc模板bean-->
    <bean id="jdbcTemplate" class="org.springframework.jdbc.core.JdbcTemplate">
		<property name="dataSource" ref="dataSourceDBCP"></property>
	</bean>
</beans>

Spring的<context:component-scan>扫描指定包下的类,这样@Repository、@Autowired注解才能起作用。

关于数据源配置这里只存在不同的配置源问题,关于为什么要这么配置,只能说是JDBC规范要求。

jdbcTemplate这个bean将dataSource注入到JdbcTemplate,然后JdbcTemplate Bean通过@Autowired注入到LoginLogDao和LoginDao中。

6. 服务层 LoginService

    loginservice接口有三个方法:isExistUser用于验证账号、密码正确性、findUserByUsername用于根据用户账号加载User实体、loginSuccess用于登录成功进行积分变更、日志插入操作,这里存在事务操作,更新tb_user并插入tb_login_info,但是我们却没看到关于事务控制的代码,Spring的一大优点就是将事务从代码中解放出来,下面我会讲解关于Spring处理事务能力。

   

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import dao.LoginDao;
import dao.LoginLogDao;
import domain.LoginInfo;
import domain.User;

@Service //将LoginServcie标注为一个服务层Bean
public class LoginService {
	@Autowired
	private LoginDao logindao;
	@Autowired
	private LoginLogDao loginlogdao;
	
  public boolean isExistUser(String userid,String password){
	  int count=logindao.getUserCount(userid, password);
	  return count>0;
  }
  public User findUserByUsername(String userid){
	  return logindao.findUserByUsername(userid);
  } 
  public void loginSuccess(User u){
	  u.setCredits(u.getCredits()+5);
	  LoginInfo info=new LoginInfo();
	  info.setLoginuserid(u.getUserid());
	  info.setLogintime(u.getLastlogintime());
	  info.setLoginip(u.getLoginip());
	  loginlogdao.insertLoginfo(info);
	  logindao.updateUserInfo(u);
  }
}

7.Spring加载Service

   Spring的事务虽然不用出现在代码中,但是我们需要告诉Spring哪些业务类需要工作于事务环境以及事务的规则等内容,让Spring根据这些信息自动为目标业务类添加事务管理功能。

 

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
	xmlns:mvc="http://www.springframework.org/schema/mvc" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
	xmlns:p="http://www.springframework.org/schema/p" xmlns:context="http://www.springframework.org/schema/context"
	xmlns:aop="http://www.springframework.org/schema/aop"  
	xmlns:tx="http://www.springframework.org/schema/tx" 
	xsi:schemaLocation="http://www.springframework.org/schema/beans
	http://www.springframework.org/schema/beans/spring-beans-3.0.xsd
	http://www.springframework.org/schema/context
	http://www.springframework.org/schema/context/spring-context-3.0.xsd
	http://www.springframework.org/schema/mvc
	http://www.springframework.org/schema/mvc/spring-mvc-3.0.xsd
	http://www.springframework.org/schema/aop  
    http://www.springframework.org/schema/aop/spring-aop-3.0.xsd
    http://www.springframework.org/schema/tx 
    http://www.springframework.org/schema/tx/spring-tx-3.0.xsd">
	
	<!-- 自动扫描com.gloryscience.dao包下的所有类,将带有注解的类 纳入spring容器管理-->
	<context:component-scan base-package="dao"></context:component-scan>
    <context:component-scan base-package="service"></context:component-scan>
    
     <bean id="dataSourceDBCP"  class="org.apache.commons.dbcp.BasicDataSource" destroy-method="close">
      <property name="driverClassName" value="oracle.jdbc.driver.OracleDriver"/>  
        <property name="url" value="jdbc:oracle:thin:@127.0.0.1:1521:orcl"/>  
        <property name="username" value="gloryseience"/>  
        <property name="password" value="gloryseience"/>  
     </bean>
     
    <bean id="jdbcTemplate" class="org.springframework.jdbc.core.JdbcTemplate">
		<property name="dataSource" ref="dataSourceDBCP"></property>
	</bean>
	<!-- 配置事务管理器声-->
	<bean id="transactionManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
    <property name="dataSource" ref="dataSourceDBCP"></property>
    </bean>
    <!-- 通过AOP配置提供事务增强,让Service包下所有方法拥有事务 -->
    <aop:config>
        <aop:pointcut id="serviceMethod"
            expression="execution(* service..*(..))" />
        <aop:advisor advice-ref="txAdvice" pointcut-ref="serviceMethod" />        
    </aop:config>   
    
    <tx:advice id="txAdvice" transaction-manager="transactionManager">
        <tx:attributes>
            <tx:method name="*"/>
        </tx:attributes>
    </tx:advice>
</beans>

关于业务层和配置已经完成,我们先单元测试一下。

8.单元测试

   Spring 3.0的测试框架可以和Junit4整合,通过@RunWith可以指定SpringJUnit4Cla***unner测试运行器,该运行器是Spring提供的,可以将Spring容器和Junit4测试框架结合。@ContextConfiguration也是Spring提供的注解,用于指定Spring配置文件。@Test用于将方法标注为测试方法,

import static org.junit.Assert.*;
import java.util.Date;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.junit4.SpringJUnit4Cla***unner;
import domain.User;
import service.LoginService;

@RunWith(SpringJUnit4Cla***unner.class)//基于JUnit4的Spring测试框架
@ContextConfiguration(locations={"classpath:applicationContext.xml"})//启动spring容器
public class TestUserService {
	@Autowired//注入spring容器bean
	private LoginService loginservice;
	@Test
	public void isExistUser() {
		boolean a1=loginservice.isExistUser("zhangsan", "123456");
		boolean a2=loginservice.isExistUser("zhangsan", "1");
		assertTrue(a1);
		assertTrue(!a2);
	}
	@Test
	public void findUserbyUsername() {
		User u=loginservice.findUserByUsername("zhangsan");
		assertEquals(u.getUserid(), "zhangsan");
	}
	
	@Test
	public void loginSuccess(){
		User u=new User();
		u.setUserid("zhangsan");
		u.setCredits(0);
		u.setLoginip("127.0.0.1");
		u.setLastlogintime(new Date());
		loginservice.loginSuccess(u);
	}
}

9.配置SpringMVC

  业务层以及持久层已经完成,现在我们需要用SpringMVC将展现层加入个整个功能里。首先对web.xml进行配置

<?xml version="1.0" encoding="UTF-8"?>
<web-app id="acms" version="2.4"
	xmlns="http://java.sun.com/xml/ns/j2ee" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
	xsi:schemaLocation="http://java.sun.com/xml/ns/j2ee http://java.sun.com/xml/ns/j2ee/web-app_2_4.xsd">
<!-- 从类文件下加载spring配置文件 -->
<context-param>
<param-name>contextConfigLocation</param-name>
<param-value>classpath:applicationContext.xml</param-value>
</context-param>
<!-- 启动spring 的监听器,引用contextConfigLocation的配置文件 -->
<listener>
	<listener-class>org.springframework.web.context.ContextLoaderListener</listener-class>
</listener>

<servlet>
<servlet-name>Spring</servlet-name>
<servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>
    <init-param>
        <param-name>contextConfigLocation</param-name>
        <param-value>classpath:Spring-servlet.xml</param-value>
    </init-param>
 <load-on-startup>1</load-on-startup>  
</servlet>

<servlet-mapping>
<servlet-name>Spring</servlet-name>
<url-pattern>*.html</url-pattern>
</servlet-mapping>
</web-app>

   web容器通过上下文参数指定Spring配置文件,多个配置文件可用逗号分开,然后web容器在启动的时候会自动启动监听器ContextLoaderListener,该监听器的作用是获取Spring配置文件并启动Spring容器,注意将log4j.properties放在类路径下,以便日志引擎生效。

   SpringMVC也是通过截获URL请求,然后进行相关处理。

   这里面注意SpringMVC也有个配置文件,该配置文件的文件与这里定义的Servlet名有一个契约,即

采用<servlet名>-servlet.xml,在这里Servlet的名称为Spring,意思是/WEB-INF下必须有个Spring-servlet.xml的配置文件,这个配置文件无需通过web.xml的上下文进行配置,SpringMVC的Servlet会自动将Spring-servlet.xml与其他配置文件拼装。

 <init-param>

        <param-name>contextConfigLocation</param-name>

        <param-value>classpath:Spring-servlet.xml</param-value>

    </init-param>

这里如果配置这个参数则配置文件就不会去执行上面的契约去查找Spring-servlet,会根据我们自己配置的文件查找。

   Servlet的URL路径进行定义我们让所有后缀以.html的URL都能被Spring Servlet截获,然后转给SpringMVC进行处理,请求被SpringMVC接受后,首先根据URL找到目标的处理控制器,并将请求参数封装成一个“命令”对象一起传给控制器处理,控制器调用Spring 的业务Bean进行处理并返回视图。

10.控制器类LoginController

  LoginController负责处理登录请求,完成登录业务,根据登录结果返回不同页面。

import java.util.Date;
import javax.servlet.http.HttpServletRequest;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.servlet.ModelAndView;
import domain.LoginCommand;
import domain.User;
import service.LoginService;

@Controller
public class LoginController {

	@Autowired
	private LoginService loginservice;
	
	@RequestMapping(value="/index.html")
	public String loginPage(){
		return "login";
	}
	@RequestMapping(value="/loginCheck.html")
	public ModelAndView loginCheck(HttpServletRequest request,LoginCommand loginCommand){
		boolean isValid=loginservice.isExistUser(loginCommand.getUserid(), loginCommand.getPassword());
		if(!isValid){
			return  new ModelAndView("login","error","用户或密码错误");
		}else{
			User user=loginservice.findUserByUsername(loginCommand.getUserid());
			user.setLoginip(request.getRemoteAddr());
			user.setLastlogintime(new Date());
			request.getSession().setAttribute("user", user);
			loginservice.loginSuccess(user);
			return new ModelAndView("welcome");
		}
	}
}

@Controller将LoginController标注为SpringMVC控制器,处理HTTP请求,一个控制器包含多个HTTP请求路径的处理方法,通过@RequestMapping指定方法映射请求路径。

请求参数会根据参数名称默认契约自动绑定到响应方法的入参中,在loginCheck(HttpServletRequest request,LoginCommand loginCommand),请求参数会根据名称匹配绑定到loginCommand中。

public class LoginCommand {

	private String userid;//用户id
	private String password;//用户密码
	省略get/set
	}

请求方法可以返回ModelAndView或者字符串,SpringMVC会解析并自动转向目标响应页面。关于ModelAndView读者只需知道它目前代表一种视图就行,后面会解释。

现在需要知道ModelAndView返回的两种形式,ModelAndView("login","error","用户或密码错误")和ModelAndView("main"),第一个参数代表视图的逻辑名,第二个以及第三个代表数据模型名称和数据模型对象,数据模型对象将以数据模型名称为参数名放置到request属性中。

SpringMVC的配置文件Spring-servlet.xml

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
	xmlns:mvc="http://www.springframework.org/schema/mvc" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
	xmlns:p="http://www.springframework.org/schema/p" xmlns:context="http://www.springframework.org/schema/context"
	xsi:schemaLocation="http://www.springframework.org/schema/beans
	http://www.springframework.org/schema/beans/spring-beans-3.0.xsd
	http://www.springframework.org/schema/context
	http://www.springframework.org/schema/context/spring-context-3.0.xsd
	http://www.springframework.org/schema/mvc
	http://www.springframework.org/schema/mvc/spring-mvc-3.0.xsd">
	<!-- 扫描controllerd,应用spring注释 -->
	<context:component-scan base-package="controller" />
	<!-- 对模型视图解析 -->
	<bean class="org.springframework.web.servlet.view.InternalResourceViewResolver" p:prefix="/WEB-INF/login/" p:suffix=".jsp" />
</beans>

上面SpringMVC通过配置来解析ModelAndView,Spring有许多解析ModelAndView的方式,这里我们用InternalResourceViewResolver,它通过添加前后缀方式来解析。如ModelAndView("welcome"),将解析为/WEB-INF/login/welcome.jsp

11.JSP页面

<%@ page language="java" contentType="text/html; charset=UTF-8" pageEncoding="UTF-8"%>
<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %>
<!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN" "http://www.w3.org/TR/html4/loose.dtd">
<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8">
<title>论坛登录</title>
</head>
<c:if test="${not empty error}">
<font color="red"><c:out value="${error}"></c:out></font>
</c:if>
<form action="<c:url value='/loginCheck.html'/>" method="post">

					  	<input type="text"  id="userid" name="userid" placeholder="用户名" maxlength="40"/>
						<input type="password"id="password" name="password" placeholder="密码" maxlength="40"/>
						<input type="submit" value="登 录" style="font-size: 20px;"/>
						<input type="reset" value="重置" style="font-size: 20px;"/>
					
</form>
</body>
</html>
<%@ page language="java" contentType="text/html; charset=UTF-8" pageEncoding="UTF-8"%>
<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %>
<!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN" "http://www.w3.org/TR/html4/loose.dtd">
<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8">
<title>论坛主页</title>
</head>

${user.username},欢迎你进入本论坛,你的积分为${user.credits}.
</body>
</html>

这里我引入c标签,需要引入两个jar包。

剩下的可以直接部署了,然后访问http://localhost:8088/项目部署名称/,输入zhangsan、123456即可登录成功。