先来构建一个极为简单的web应用,从controller到dao。不考虑具体实现,只是先对整体架构有一个清晰的了解。日后在分层细述每一层的细节。
我们将用到如下jar包:
aopalliance-1.0.jar
commons-logging-1.1.1.jar
log4j-1.2.15.jar
spring-beans-2.5.6.jar
spring-context-2.5.6.jar
spring-context-support-2.5.6.jar
spring-core-2.5.6.jar
spring-tx-2.5.6.jar
spring-web-2.5.6.jar
spring-webmvc-2.5.6.jar
先看web.xml
- <?xml version="1.0" encoding="UTF-8"?>
- <web-app
- xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
- xmlns="http://java.sun.com/xml/ns/javaee"
- xmlns:web="http://java.sun.com/xml/ns/javaee/web-app_2_5.xsd"
- xsi:schemaLocation="http://java.sun.com/xml/ns/javaee http://java.sun.com/xml/ns/javaee/web-app_2_5.xsd"
- id="WebApp_ID"
- version="2.5">
- <display-name>spring</display-name>
- <!-- 应用路径 -->
- <context-param>
- <param-name>webAppRootKey</param-name>
- <param-value>spring.webapp.root</param-value>
- </context-param>
- <!-- Log4J 配置 -->
- <context-param>
- <param-name>log4jConfigLocation</param-name>
- <param-value>classpath:log4j.xml</param-value>
- </context-param>
- <context-param>
- <param-name>log4jRefreshInterval</param-name>
- <param-value>60000</param-value>
- </context-param>
- <!--Spring上下文 配置 -->
- <context-param>
- <param-name>contextConfigLocation</param-name>
- <param-value>/WEB-INF/applicationContext.xml</param-value>
- </context-param>
- <!-- 字符集 过滤器 -->
- <filter>
- <filter-name>CharacterEncodingFilter</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>CharacterEncodingFilter</filter-name>
- <url-pattern>/*</url-pattern>
- </filter-mapping>
- <!-- Spring 监听器 -->
- <listener>
- <listener-class>org.springframework.web.context.ContextLoaderListener</listener-class>
- </listener>
- <listener>
- <listener-class>org.springframework.web.util.Log4jConfigListener</listener-class>
- </listener>
- <!-- Spring 分发器 -->
- <servlet>
- <servlet-name>spring</servlet-name>
- <servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>
- <init-param>
- <param-name>contextConfigLocation</param-name>
- <param-value>/WEB-INF/servlet.xml</param-value>
- </init-param>
- </servlet>
- <servlet-mapping>
- <servlet-name>spring</servlet-name>
- <url-pattern>*.do</url-pattern>
- </servlet-mapping>
- <welcome-file-list>
- <welcome-file>index.html</welcome-file>
- <welcome-file>index.htm</welcome-file>
- <welcome-file>index.jsp</welcome-file>
- <welcome-file>default.html</welcome-file>
- <welcome-file>default.htm</welcome-file>
- <welcome-file>default.jsp</welcome-file>
- </welcome-file-list>
- </web-app>
有不少人问我,这段代码是什么:
- <!-- 应用路径 -->
- <context-param>
- <param-name>webAppRootKey</param-name>
- <param-value>spring.webapp.root</param-value>
- </context-param>
这是当前应用的路径变量,也就是说你可以在其他代码中使用${spring.webapp.root}指代当前应用路径。我经常用它来设置log的输出目录。
为什么要设置参数log4jConfigLocation?
- <!-- Log4J 配置 -->
- <context-param>
- <param-name>log4jConfigLocation</param-name>
- <param-value>classpath:log4j.xml</param-value>
- </context-param>
- <context-param>
- <param-name>log4jRefreshInterval</param-name>
- <param-value>60000</param-value>
- </context-param>
这是一种基本配置,spring中很多代码使用了不同的日志接口,既有log4j也有commons-logging,这里只是强制转换为log4j!并且,log4j的配置文件只能放在classpath根路径。同时,需要通过commons-logging配置将日志控制权转交给log4j。同时commons-logging.properties必须放置在classpath根路径。
commons-logging内容:
- org.apache.commons.logging.Log=org.apache.commons.logging.impl.Log4JLogger
最后,记得配置log4j的监听器:
- <listener>
- <listener-class>org.springframework.web.util.Log4jConfigListener</listener-class>
- </listener>
接下来,我们需要配置两套配置文件,applicationContext.xml和servlet.xml。
applicationContext.xml用于对应用层面做整体控制。按照分层思想,统领service层和dao层。servlet.xml则单纯控制controller层。
- <?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"
- xsi:schemaLocation="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.xsd">
- <import
- resource="service.xml" />
- <import
- resource="dao.xml" />
- </beans>
applicationContext.xml什么都不干,它只管涉及到整体需要的配置,并且集中管理。
这里引入了两个配置文件service.xml和dao.xml分别用于业务、数据处理。
service.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"
- xsi:schemaLocation="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.xsd">
- <context:component-scan
- base-package="org.zlex.spring.service" />
- </beans>
注意,这里通过<context:component-scan />标签指定了业务层的基础包路径——“org.zlex.spring.service”。也就是说,业务层相关实现均在这一层。这是有必要的分层之一。
dao.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:aop="http://www.springframework.org/schema/aop"
- xmlns:context="http://www.springframework.org/schema/context"
- xmlns:tx="http://www.springframework.org/schema/tx"
- xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd
- http://www.springframework.org/schema/aop http://www.springframework.org/schema/aop/spring-aop.xsd
- http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context.xsd
- http://www.springframework.org/schema/tx http://www.springframework.org/schema/tx/spring-tx.xsd">
- <context:component-scan
- base-package="org.zlex.spring.dao" />
- </beans>
dao层如法炮制,包路径是"org.zlex.spring.dao"。从这个角度看,注解还是很方便的!
最后,我们看看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:context="http://www.springframework.org/schema/context"
- xsi:schemaLocation="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.xsd">
- <context:component-scan
- base-package="org.zlex.spring.controller" />
- <bean
- id="urlMapping"
- class="org.springframework.web.servlet.mvc.annotation.DefaultAnnotationHandlerMapping" />
- <bean
- class="org.springframework.web.servlet.mvc.annotation.AnnotationMethodHandlerAdapter" />
- </beans>
包路径配置就不细说了,都是一个概念。最重要的时候后面两个配置,这将使得注解生效!
“org.springframework.web.servlet.mvc.annotation.DefaultAnnotationHandlerMapping”是默认实现,可以不写,Spring容器默认会默认使用该类。
“org.springframework.web.servlet.mvc.annotation.AnnotationMethodHandlerAdapter”直接关系到多动作控制器配置是否可用!
简单看一下代码结构,如图:
Account类是来存储账户信息,属于域对象,极为简单,代码如下所示:
Account.java
- /**
- * 2010-1-23
- */
- package org.zlex.spring.domain;
- import java.io.Serializable;
- /**
- *
- * @author <a href="mailto:zlex.dongliang@gmail.com">梁栋</a>
- * @version 1.0
- * @since 1.0
- */
- public class Account implements Serializable {
- /**
- *
- */
- private static final long serialVersionUID = -533698031946372178L;
- private String username;
- private String password;
- /**
- * @param username
- * @param password
- */
- public Account(String username, String password) {
- this.username = username;
- this.password = password;
- }
- /**
- * @return the username
- */
- public String getUsername() {
- return username;
- }
- /**
- * @param username the username to set
- */
- public void setUsername(String username) {
- this.username = username;
- }
- /**
- * @return the password
- */
- public String getPassword() {
- return password;
- }
- /**
- * @param password the password to set
- */
- public void setPassword(String password) {
- this.password = password;
- }
- }
通常,在构建域对象时,需要考虑该对象可能需要进行网络传输,本地缓存,因此建议实现序列化接口Serializable
我们再来看看控制器,这就稍微复杂了一点代码如下所示:
AccountController .java
- /**
- * 2010-1-23
- */
- package org.zlex.spring.controller;
- 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.web.bind.ServletRequestUtils;
- import org.springframework.web.bind.annotation.RequestMapping;
- import org.springframework.web.bind.annotation.RequestMethod;
- import org.zlex.spring.service.AccountService;
- /**
- *
- * @author <a href="mailto:zlex.dongliang@gmail.com">梁栋</a>
- * @version 1.0
- * @since 1.0
- */
- @Controller
- @RequestMapping("/account.do")
- public class AccountController {
- @Autowired
- private AccountService accountService;
- @RequestMapping(method = RequestMethod.GET)
- public void hello(HttpServletRequest request, HttpServletResponse response)
- throws Exception {
- String username = ServletRequestUtils.getRequiredStringParameter(
- request, "username");
- String password = ServletRequestUtils.getRequiredStringParameter(
- request, "password");
- System.out.println(accountService.verify(username, password));
- }
- }
分段详述:
- @Controller
- @RequestMapping("/account.do")
这两行注解, @Controller 是告诉Spring容器,这是一个控制器类, @RequestMapping("/account.do") 是来定义该控制器对应的请求路径(/account.do)
- @Autowired
- private AccountService accountService;
这是用来自动织入业务层实现AccountService,有了这个注解,我们就可以不用写setAccountService()方法了!
同时,JSR-250标准注解,推荐使用 @Resource 来代替Spring专有的@Autowired注解。
引用 Spring 不但支持自己定义的@Autowired注解,还支持几个由JSR-250规范定义的注解,它们分别是@Resource、@PostConstruct以及@PreDestroy。
@Resource的作用相当于@Autowired,只不过@Autowired按byType自动注入,而@Resource默认按 byName自动注入罢了。@Resource有两个属性是比较重要的,分别是name和type,Spring将@Resource注解的name属性解析为bean的名字,而type属性则解析为bean的类型。所以如果使用name属性,则使用byName的自动注入策略,而使用type属性时则使用byType自动注入策略。如果既不指定name也不指定type属性,这时将通过反射机制使用byName自动注入策略。
@Resource装配顺序
1. 如果同时指定了name和type,则从Spring上下文中找到唯一匹配的bean进行装配,找不到则抛出异常
2. 如果指定了name,则从上下文中查找名称(id)匹配的bean进行装配,找不到则抛出异常
3. 如果指定了type,则从上下文中找到类型匹配的唯一bean进行装配,找不到或者找到多个,都会抛出异常
4. 如果既没有指定name,又没有指定type,则自动按照byName方式进行装配(见2);如果没有匹配,则回退为一个原始类型(UserDao)进行匹配,如果匹配则自动装配;
1.6. @PostConstruct(JSR-250)
在方法上加上注解@PostConstruct,这个方法就会在Bean初始化之后被Spring容器执行(注:Bean初始化包括,实例化Bean,并装配Bean的属性(依赖注入))。
这有点像ORM最终被JPA一统天下的意思! 大家知道就可以了,具体使用何种标准由项目说了算!
最后,来看看核心方法:
- @RequestMapping(method = RequestMethod.GET)
- public void hello(HttpServletRequest request, HttpServletResponse response)
- throws Exception {
- String username = ServletRequestUtils.getRequiredStringParameter(
- request, "username");
- String password = ServletRequestUtils.getRequiredStringParameter(
- request, "password");
- System.out.println(accountService.verify(username, password));
- }
注解@RequestMapping(method = RequestMethod.GET)指定了访问方法类型。
注意,如果没有用这个注解标识方法,Spring容器将不知道那个方法可以用于处理get请求!
对于方法名,我们可以随意定!方法中的参数,类似于“HttpServletRequest request, HttpServletResponse response”,只要你需要方法可以是有参也可以是无参!
解析来看Service层,分为接口和实现:
AccountService.java
- /**
- * 2010-1-23
- */
- package org.zlex.spring.service;
- /**
- *
- * @author <a href="mailto:zlex.dongliang@gmail.com">梁栋</a>
- * @version 1.0
- * @since 1.0
- */
- public interface AccountService {
- /**
- * 验证用户身份
- *
- * @param username
- * @param password
- * @return
- */
- boolean verify(String username, String password);
- }
接口不需要任何Spring注解相关的东西,它就是一个简单的接口!
重要的部分在于实现层,如下所示:
AccountServiceImpl.java
- /**
- * 2010-1-23
- */
- package org.zlex.spring.service.impl;
- import org.springframework.beans.factory.annotation.Autowired;
- import org.springframework.stereotype.Service;
- import org.springframework.transaction.annotation.Transactional;
- import org.zlex.spring.dao.AccountDao;
- import org.zlex.spring.domain.Account;
- import org.zlex.spring.service.AccountService;
- /**
- *
- * @author <a href="mailto:zlex.dongliang@gmail.com">梁栋</a>
- * @version 1.0
- * @since 1.0
- */
- @Service
- @Transactional
- public class AccountServiceImpl implements AccountService {
- @Autowired
- private AccountDao accountDao;
- /*
- * (non-Javadoc)
- *
- * @see org.zlex.spring.service.AccountService#verify(java.lang.String,
- * java.lang.String)
- */
- @Override
- public boolean verify(String username, String password) {
- Account account = accountDao.read(username);
- if (password.equals(account.getPassword())) {
- return true;
- } else {
- return false;
- }
- }
- }
注意以下内容:
- @Service
- @Transactional
注解@Service用于标识这是一个Service层实现,@Transactional用于控制事务,将事务定位在业务层,这是非常务实的做法!
接下来,我们来看持久层:AccountDao和AccountDaoImpl类
AccountDao.java
- /**
- * 2010-1-23
- */
- package org.zlex.spring.dao;
- import org.zlex.spring.domain.Account;
- /**
- *
- * @author <a href="mailto:zlex.dongliang@gmail.com">梁栋</a>
- * @version 1.0
- * @since 1.0
- */
- public interface AccountDao {
- /**
- * 读取用户信息
- *
- * @param username
- * @return
- */
- Account read(String username);
- }
这个接口就是简单的数据提取,无需任何Spring注解有关的东西!
再看其实现类:
AccountDaoImpl.java
- /**
- * 2010-1-23
- */
- package org.zlex.spring.dao.impl;
- import org.springframework.stereotype.Repository;
- import org.zlex.spring.dao.AccountDao;
- import org.zlex.spring.domain.Account;
- /**
- *
- * @author <a href="mailto:zlex.dongliang@gmail.com">梁栋</a>
- * @version 1.0
- * @since 1.0
- */
- @Repository
- public class AccountDaoImpl implements AccountDao {
- /* (non-Javadoc)
- * @see org.zlex.spring.dao.AccountDao#read(java.lang.String)
- */
- @Override
- public Account read(String username) {
- return new Account(username,"wolf");
- }
- }
这里只需要注意注解:
- @Repository
意为持久层,Dao实现这层我没有过于细致的介绍通过注解调用ORM或是JDBC来完成实现,这些内容后续细述!
这里我们没有提到注解 @Component ,共有4种“组件”式注解:
引用
@Component:可装载组件
@Repository:持久层组件
@Service:服务层组件
@Controller:控制层组件
这样spring容器就完成了控制层、业务层和持久层的构建。
启动应用,访问 http://localhost:8080/spring/account.do?username=snow&password=wolf
观察控制台,如果得到包含“true”的输出,本次构建就成功了!
代码见附件!
顺便说一句:在Spring之前的XML配置中,如果你想在一个类中获得文件可以通过在xml配置这个类的某个属性。在注解的方式(Spring3.0)中,你可以使用 @Value 来指定这个文件。
例如,我们想要在一个类中获得一个文件,可以这样写:
- @Value("/WEB-INF/database.properties")
- private File databaseConfig;
如果这个properties文件已经正常在容器中加载,可以直接这样写:
- @Value("${jdbc.url}")
- private String url;
获得这个url参数!
容器中加载这个Properties文件:
- <util:properties id="jdbc" location="/WEB-INF/database.properties"/>
这样,我们就能通过注解 @Value 获得 /WEB-INF/database.properties 这个文件!
如果我们想要获得注入在xml中的某个类,例如dataSource(<bean id ="dataSource">)可以在注解的类中这么写:
- @Resource(name = "dataSource")
- private BasicDataSource dataSource;
如果只有这么一个类使用该配置文件:
- @ImportResource("/WEB-INF/database.properties")
- public class AccountDaoImpl extends AccountDao {
就这么简单!
对Spring注解有了一个整体认识,至少完成了一个简单的web应用搭建。当然,还不完善,这仅仅只是个开始!
今天看了Spring 3.0的注解,我感觉自己被颠覆了。多年前,为了减少代码依赖我们用配置文件进行模块间耦合,降低模块之间的黏度。现如今,所有可配置的内容都塞进了代码中,我只能说:这多少有点顾此失彼,有点倒退的意思!使用注解的好处是:代码通读性增强。这既是优势也是劣势!如果我要改一段配置,就要打开代码逐行扫描;如果恰巧这是别人封装的jar包,那我只好反编译;如果碰巧遇上这个jar包经过了混淆,那我只好求助于AOP了。 为了这么一个配置,我的代码观几乎将要被颠覆!
言归正传,研究一下注解下的控制层。
我习惯于使用JSTL展示页面,因此需要在原lib基础上增加jstl.jar和standard.jar,详细lib依赖如下:
aopalliance-1.0.jar
commons-logging-1.1.1.jar
log4j-1.2.15.jar
spring-beans-2.5.6.jar
spring-context-2.5.6.jar
spring-context-support-2.5.6.jar
spring-core-2.5.6.jar
spring-tx-2.5.6.jar
spring-web-2.5.6.jar
spring-webmvc-2.5.6.jar
standard.jar
jstl.jar
上一篇文中,我们定义了控制器AccountController:
AccountController.java
- /**
- * 2010-1-23
- */
- package org.zlex.spring.controller;
- 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.web.bind.ServletRequestUtils;
- import org.springframework.web.bind.annotation.RequestMapping;
- import org.springframework.web.bind.annotation.RequestMethod;
- import org.zlex.spring.service.AccountService;
- /**
- *
- * @author <a href="mailto:zlex.dongliang@gmail.com">梁栋</a>
- * @version 1.0
- * @since 1.0
- */
- @Controller
- @RequestMapping("/account.do")
- public class AccountController {
- @Autowired
- private AccountService accountService;
- @RequestMapping(method = RequestMethod.GET)
- public void hello(HttpServletRequest request, HttpServletResponse response)
- throws Exception {
- String username = ServletRequestUtils.getRequiredStringParameter(
- request, "username");
- String password = ServletRequestUtils.getRequiredStringParameter(
- request, "password");
- System.out.println(accountService.verify(username, password));
- }
- }
先说注解 @RequestMapping
这里使用注解 @RequestMapping(method = RequestMethod.GET) 指定这个方法为get请求时调用。同样,我们可以使用注解 @RequestMapping(method = RequestMethod.POST) 指定该方法接受post请求。
- @Controller
- @RequestMapping("/account.do")
- public class AccountController {
- @RequestMapping(method = RequestMethod.GET)
- public void get() {
- }
- @RequestMapping(method = RequestMethod.POST)
- public void post() {
- }
- }
这与我们久别的Servlet很相像,类似于doGet()和doPost()方法!
我们也可以将其改造为多动作控制器,如下代码所示:
- @Controller
- @RequestMapping("/account.do")
- public class AccountController {
- @RequestMapping(params = "method=login")
- public void login() {
- }
- @RequestMapping(params = "method=logout")
- public void logout() {
- }
这样,我们可以通过参数“method”指定不同的参数值从而通过请求("/account.do?method=login"和"/account.do?method=logout")调用不同的方法!
注意:使用多动作控制器必须在配置文件中加入注解支持!
- <bean
- class="org.springframework.web.servlet.mvc.annotation.AnnotationMethodHandlerAdapter" />
当然,我们也可以将注解 @RequestMapping 指定到某一个方法上,如:
- @Controller
- public class AccountController {
- @RequestMapping("/a.do")
- public void a() {}
- @RequestMapping("/b.do")
- public void b() {}
- }
这样,请求“a.do”和“b.do”将对应不同的方法a() 和b()。这使得一个控制器可以同时承载多个请求!
@RequestMapping("/account.do") 是 @RequestMapping(value="/account.do") 的简写!
再说输入参数!
这里的方法名可以随意定义,但是参数和返回值却又要求!
为什么?直接看源代码,我们就能找到答案!
AnnotationMethodHandlerAdapter.java部分源代码——有关参数部分:
- @Override
- protected Object resolveStandardArgument(Class parameterType, NativeWebRequest webRequest)
- throws Exception {
- HttpServletRequest request = (HttpServletRequest) webRequest.getNativeRequest();
- HttpServletResponse response = (HttpServletResponse) webRequest.getNativeResponse();
- if (ServletRequest.class.isAssignableFrom(parameterType)) {
- return request;
- }
- else if (ServletResponse.class.isAssignableFrom(parameterType)) {
- this.responseArgumentUsed = true;
- return response;
- }
- else if (HttpSession.class.isAssignableFrom(parameterType)) {
- return request.getSession();
- }
- else if (Principal.class.isAssignableFrom(parameterType)) {
- return request.getUserPrincipal();
- }
- else if (Locale.class.equals(parameterType)) {
- return RequestContextUtils.getLocale(request);
- }
- else if (InputStream.class.isAssignableFrom(parameterType)) {
- return request.getInputStream();
- }
- else if (Reader.class.isAssignableFrom(parameterType)) {
- return request.getReader();
- }
- else if (OutputStream.class.isAssignableFrom(parameterType)) {
- this.responseArgumentUsed = true;
- return response.getOutputStream();
- }
- else if (Writer.class.isAssignableFrom(parameterType)) {
- this.responseArgumentUsed = true;
- return response.getWriter();
- }
- return super.resolveStandardArgument(parameterType, webRequest);
- }
也就是说,如果我们想要在自定义的方法中获得一些个“标准”输入参数,参数类型必须包含在以下类型中:
引用
ServletRequest
ServletResponse
HttpSession
Principal
Locale
InputStream
OutputStream
Reader
Writer
当然,上述接口其实都是对于HttpServletRequest和HttpServletResponse的扩展。
此外,我们还可以定义自己的参数。
注意:自定义参数必须是实现类,绝非接口!Spring容器将帮你完成对象初始化工作!
比如说上文中,我们需要参数username和password。我们可以这么写:
- @RequestMapping(method = RequestMethod.GET)
- public void hello(String username,String password) {
- System.out.println(accountService.verify(username, password));
- }
如果参数名不能与这里的变量名保持一致,那么我们可以使用注解 @RequestParam 进行强制绑定,代码如下所示:
- @RequestMapping(method = RequestMethod.GET)
- public void hello(@RequestParam("username") String u,
- @RequestParam("password") String p) {
- System.out.println(accountService.verify(u, p));
- }
这比起我们之前写的代码有所简洁:
- @RequestMapping(method = RequestMethod.GET)
- public void hello(HttpServletRequest request, HttpServletResponse response)
- throws Exception {
- String username = ServletRequestUtils.getRequiredStringParameter(
- request, "username");
- String password = ServletRequestUtils.getRequiredStringParameter(
- request, "password");
- System.out.println(accountService.verify(username, password));
- }
ServletRequestUtils类的工作已经由Spring底层实现了,我们只需要把参数名定义一致即可,其内部取参无需关心!
除了传入参数,我们还可以定义即将传出的参数,如加入ModelMap参数:
- @SuppressWarnings("unchecked")
- @RequestMapping(method = RequestMethod.GET)
- public Map hello(String username, String password, ModelMap model) {
- System.out.println(accountService.verify(username, password));
- model.put("msg", username);
- return model;
- }
这时,我们没有定义页面名称,Spring容器将根据请求名指定同名view,即如果是jap页面,则account.do->account.jsp!
不得不承认,这样写起来的确减少了代码量!
接着说输出参数!
通过ModelMap,我们可以绑定输出到的页面的参数,但最终我们将要返回到何种页面呢?再次查看AnnotationMethodHandlerAdapter源代码!
AnnotationMethodHandlerAdapter.java部分源代码——有关返回值部分:
- @SuppressWarnings("unchecked")
- public ModelAndView getModelAndView(Method handlerMethod, Class handlerType, Object returnValue,
- ExtendedModelMap implicitModel, ServletWebRequest webRequest) {
- if (returnValue instanceof ModelAndView) {
- ModelAndView mav = (ModelAndView) returnValue;
- mav.getModelMap().mergeAttributes(implicitModel);
- return mav;
- }
- else if (returnValue instanceof Model) {
- return new ModelAndView().addAllObjects(implicitModel).addAllObjects(((Model) returnValue).asMap());
- }
- else if (returnValue instanceof Map) {
- return new ModelAndView().addAllObjects(implicitModel).addAllObjects((Map) returnValue);
- }
- else if (returnValue instanceof View) {
- return new ModelAndView((View) returnValue).addAllObjects(implicitModel);
- }
- else if (returnValue instanceof String) {
- return new ModelAndView((String) returnValue).addAllObjects(implicitModel);
- }
- else if (returnValue == null) {
- // Either returned null or was 'void' return.
- if (this.responseArgumentUsed || webRequest.isNotModified()) {
- return null;
- }
- else {
- // Assuming view name translation...
- return new ModelAndView().addAllObjects(implicitModel);
- }
- }
- else if (!BeanUtils.isSimpleProperty(returnValue.getClass())) {
- // Assume a single model attribute...
- ModelAttribute attr = AnnotationUtils.findAnnotation(handlerMethod, ModelAttribute.class);
- String attrName = (attr != null ? attr.value() : "");
- ModelAndView mav = new ModelAndView().addAllObjects(implicitModel);
- if ("".equals(attrName)) {
- Class resolvedType = GenericTypeResolver.resolveReturnType(handlerMethod, handlerType);
- attrName = Conventions.getVariableNameForReturnType(handlerMethod, resolvedType, returnValue);
- }
- return mav.addObject(attrName, returnValue);
- }
- else {
- throw new IllegalArgumentException("Invalid handler method return value: " + returnValue);
- }
- }
返回值的定义十分庞大,或者说可怕的if-else多少有点让我觉得厌恶!
我们可以定义以下类型的返回值:
引用
ModelAndView
Model
View
Map
String
null
ModelAndView、Model和View都是Spring之前版本所特有的元素,Map对应于传入参数ModelMap,String定义页面名称,null即对应void类型方法!
最常用的实现方式如下:
- @SuppressWarnings("unchecked")
- @RequestMapping(method = RequestMethod.GET)
- public String hello(String username, String password, ModelMap model) {
- System.out.println(accountService.verify(username, password));
- model.put("msg", username);
- return "account";
- }
当然,对于我来说在返回值中写入这么一个字符串多少有点不能接受,于是我还是乐于使用输入参数ModelMap+输出参数Map的方式。
给出一个完整的AccountController实现:
AccountController.java
- /**
- * 2010-1-23
- */
- package org.zlex.spring.controller;
- import java.util.Map;
- import org.springframework.beans.factory.annotation.Autowired;
- import org.springframework.stereotype.Controller;
- import org.springframework.ui.ModelMap;
- import org.springframework.web.bind.annotation.RequestMapping;
- import org.springframework.web.bind.annotation.RequestMethod;
- import org.zlex.spring.service.AccountService;
- /**
- *
- * @author <a href="mailto:zlex.dongliang@gmail.com">梁栋</a>
- * @version 1.0
- * @since 1.0
- */
- @Controller
- @RequestMapping("/account.do")
- public class AccountController {
- @Autowired
- private AccountService accountService;
- @SuppressWarnings("unchecked")
- @RequestMapping(method = RequestMethod.GET)
- public Map hello(String username, String password, ModelMap model) {
- System.out.println(accountService.verify(username, password));
- model.put("msg", username);
- return model;
- }
- }
最后说注解 @Session
如果想将某个ModelMap中的参数指定到Session中,可以使用 @Session 注解,将其绑定为Session熟悉,代码如下所示:
- @Controller
- @RequestMapping("/account.do")
- @SessionAttributes("msg")
- public class AccountController {
- @Autowired
- private AccountService accountService;
- @SuppressWarnings("unchecked")
- @RequestMapping(method = RequestMethod.GET)
- public Map hello(String username, String password, ModelMap model) {
- System.out.println(accountService.verify(username, password));
- model.put("msg", username);
- return model;
- }
- }
当然,我们还需要配置一下对应的视图解析器,给出完整配置:
servelt.xml
- <?xml version="1.0" encoding="UTF-8"?>
- <beans
- xmlns="http://www.springframework.org/schema/beans"
- xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
- xmlns:p="http://www.springframework.org/schema/p"
- xmlns:context="http://www.springframework.org/schema/context"
- xsi:schemaLocation="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.xsd">
- <context:component-scan
- base-package="org.zlex.spring.controller" />
- <bean
- id="urlMapping"
- class="org.springframework.web.servlet.mvc.annotation.DefaultAnnotationHandlerMapping" />
- <bean
- class="org.springframework.web.servlet.mvc.annotation.AnnotationMethodHandlerAdapter" />
- <bean
- id="jstlViewResolver"
- class="org.springframework.web.servlet.view.InternalResourceViewResolver"
- p:viewClass="org.springframework.web.servlet.view.JstlView"
- p:prefix="/WEB-INF/page/"
- p:suffix=".jsp" />
- </beans>
这里使用了JstlView作为视图解析器。同时,指定前缀路径为"/WEB-INF/page/",后缀路径为".jsp"。也就是说,Spring容器将会在这个路径中寻找匹配的jsp文件!
注意加入 xmlns:p="http://www.springframework.org/schema/p" 命名空间!
再给出页面内容:
taglib.jsp
- <%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core"%>
- <%@ taglib prefix="fmt" uri="http://java.sun.com/jsp/jstl/fmt"%>
- <%@ taglib prefix="sql" uri="http://java.sun.com/jsp/jstl/sql"%>
- <%@ taglib prefix="x" uri="http://java.sun.com/jsp/jstl/xml"%>
- <%@ taglib prefix="fn" uri="http://java.sun.com/jsp/jstl/functions"%>
- <%@ taglib prefix="spring" uri="http://www.springframework.org/tags"%>
- <%@ taglib prefix="form" uri="http://www.springframework.org/tags/form"%>
account.jap
- <%@ page language="java" contentType="text/html; charset=UTF-8"
- pageEncoding="UTF-8"%>
- <%@ include file="/WEB-INF/page/taglib.jsp"%>
- <!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>Account</title>
- </head>
- <body>
- <c:out value="${msg}"></c:out>
- </body>
- </html>
目录结构如图所示:
启动应用,最后将得到一个带有内容的页面,如图:
代码见附件!
如果要说表单,最简单的就是用户登录页面了!估计大多数做B/S出身的兄弟可能写的第一个表单就是登录表单了! 今天,我也不例外,做一个登录验证实现!
首先,改造一下账户类Account,增加一个id字段:
Account.java
- /**
- * 2010-1-23
- */
- package org.zlex.spring.domain;
- import java.io.Serializable;
- /**
- * 账户
- *
- * @author <a href="mailto:zlex.dongliang@gmail.com">梁栋</a>
- * @version 1.0
- * @since 1.0
- */
- public class Account implements Serializable {
- /**
- *
- */
- private static final long serialVersionUID = -533698031946372178L;
- /**
- * 主键
- */
- private int id;
- /**
- * 用户名
- */
- private String username;
- /**
- * 密码
- */
- private String password;
- public Account() {
- }
- /**
- * @param id
- */
- public Account(int id) {
- this.id = id;
- }
- // get、set方法省略
- }
接下来,为了协调逻辑处理,我们改造接口AccountService及其实现类AccountServiceImpl:
AccountService.java
- /**
- * 2010-1-23
- */
- package org.zlex.spring.service;
- import org.springframework.transaction.annotation.Transactional;
- import org.zlex.spring.domain.Account;
- /**
- * 账户业务接口
- *
- * @author <a href="mailto:zlex.dongliang@gmail.com">梁栋</a>
- * @version 1.0
- * @since 1.0
- */
- @Transactional
- public interface AccountService {
- /**
- * 获得账户
- *
- * @param username
- * @param password
- * @return
- */
- Account read(String username, String password);
- /**
- * 获得账户
- *
- * @param id
- * @return
- */
- Account read(int id);
- }
我们暂时抛开AccountDao该做的事情,在AccountServiceImpl中完成数据提取:
AccountServiceImpl.java
- /**
- * 2010-1-23
- */
- package org.zlex.spring.service.impl;
- import org.springframework.beans.factory.annotation.Autowired;
- import org.springframework.stereotype.Service;
- import org.zlex.spring.dao.AccountDao;
- import org.zlex.spring.domain.Account;
- import org.zlex.spring.service.AccountService;
- /**
- * 账户业务
- *
- * @author <a href="mailto:zlex.dongliang@gmail.com">梁栋</a>
- * @version 1.0
- * @since 1.0
- */
- @Service
- public class AccountServiceImpl implements AccountService {
- @Autowired
- private AccountDao accountDao;
- @Override
- public Account read(String username, String password) {
- Account account = null;
- if (username.equals("snowolf") && password.equals("zlex")) {
- account = new Account();
- account.setId(1);
- account.setUsername(username);
- account.setPassword(password);
- }
- return account;
- }
- @Override
- public Account read(int id) {
- Account account = new Account();
- account.setId(1);
- account.setUsername("snowolf");
- account.setPassword("zlex");
- return account;
- }
- }
先来一个账户信息的展示,构建一个控制器ProfileController:
ProfileController.java
- /**
- * 2010-1-26
- */
- package org.zlex.spring.controller;
- import org.springframework.beans.factory.annotation.Autowired;
- import org.springframework.stereotype.Controller;
- import org.springframework.ui.ModelMap;
- import org.springframework.web.bind.annotation.RequestMapping;
- import org.springframework.web.bind.annotation.RequestMethod;
- import org.springframework.web.bind.annotation.RequestParam;
- import org.zlex.spring.domain.Account;
- import org.zlex.spring.service.AccountService;
- /**
- * 账户信息控制器
- *
- * @author <a href="mailto:zlex.dongliang@gmail.com">梁栋</a>
- * @version 1.0
- * @since 1.0
- */
- @Controller
- @RequestMapping(value = "/profile.do")
- public class ProfileController {
- @Autowired
- private AccountService accountService;
- /**
- * 账户信息展示
- *
- * @param id
- * @param model
- * @return
- */
- @RequestMapping(method = RequestMethod.GET)
- public String profile(@RequestParam("id") int id, ModelMap model) {
- Account account = accountService.read(id);
- model.addAttribute("account", account);
- // 跳转到用户信息页面
- return "account/profile";
- }
- }
@RequestMapping(value = "/profile.do") 为该控制器绑定url(/profile.do)
@RequestMapping(method = RequestMethod.GET) 指定为GET请求
model.addAttribute("account", account); 绑定账户
return "account/profile"; 跳转到“/WEB-INF/page/account/porfile.jsp”页面
对应构建这个页面:
porfile.jsp
- <fieldset><legend>用户信息</legend>
- <ul>
- <li><label>用户名:</label><c:out value="${account.username}" /></li>
- </ul>
- </fieldset>
账户信息已经绑定在response的属性上。自然,使用<c:out />标签就可以获得账户信息内容。
访问地址 http://localhost:8080/spring/profile.do?id=1 ,结果如图所示:
接着构建一个登录控制器LoginController
LoginController.java
- /**
- * 2010-1-25
- */
- package org.zlex.spring.controller;
- import org.springframework.beans.factory.annotation.Autowired;
- import org.springframework.stereotype.Controller;
- import org.springframework.ui.ModelMap;
- import org.springframework.web.bind.annotation.ModelAttribute;
- import org.springframework.web.bind.annotation.RequestMapping;
- import org.springframework.web.bind.annotation.RequestMethod;
- import org.zlex.spring.domain.Account;
- import org.zlex.spring.service.AccountService;
- /**
- * 登录控制器
- *
- * @author <a href="mailto:zlex.dongliang@gmail.com">梁栋</a>
- * @version 1.0
- * @since 1.0
- */
- @Controller
- @RequestMapping(value = "/login.do")
- public class LoginController {
- @Autowired
- private AccountService accountService;
- /**
- * 初始化表单
- *
- * @param model
- * @return
- */
- @RequestMapping(method = RequestMethod.GET)
- public String initForm(ModelMap model) {
- Account account = new Account();
- model.addAttribute("account", account);
- // 直接跳转到登录页面
- return "account/login";
- }
- /**
- * 登录
- *
- * @param account
- * @return
- */
- @RequestMapping(method = RequestMethod.POST)
- public String login(@ModelAttribute("account") Account account) {
- Account acc = accountService.read(account.getUsername(), account
- .getPassword());
- if (acc != null) {
- return "redirect:profile.do?id=" + acc.getId();
- } else {
- return "redirect:login.do";
- }
- }
- }
分段详述,先说初始化表单:
- /**
- * 初始化表单
- *
- * @param model
- * @return
- */
- @RequestMapping(method = RequestMethod.GET)
- public String initForm(ModelMap model) {
- Account account = new Account();
- model.addAttribute("account", account);
- // 直接跳转到登录页面
- return "account/login";
- }
@RequestMapping(method = RequestMethod.GET) 指定了GET请求方式,这与POST表单提交相对应!
model.addAttribute("account", account); 绑定账户对象,也就是这个登录表单对象
return "account/login"; 指向登录页面
再看登录方法:
- /**
- * 登录
- *
- * @param account
- * @return
- */
- @RequestMapping(method = RequestMethod.POST)
- public String login(@ModelAttribute("account") Account account) {
- Account acc = accountService.read(account.getUsername(), account
- .getPassword());
- if (acc != null) {
- return "redirect:profile.do?id=" + acc.getId();
- } else {
- return "redirect:login.do";
- }
- }
@RequestMapping(method = RequestMethod.POST) 绑定POST表单提交请求
@ModelAttribute("account") Account account 绑定表单对象。
最后,再来看看页面:
login.jsp
- <fieldset><legend>登录</legend><form:form commandName="account">
- <form:hidden path="id" />
- <ul>
- <li><form:label path="username">用户名:</form:label><form:input
- path="username" /></li>
- <li><form:label path="password">密码:</form:label><form:password
- path="password" /></li>
- <li>
- <button type="submit">登录</button>
- <button type="reset">重置</button>
- </li>
- </ul>
- </form:form></fieldset>
注意, <form:form commandName="account"> 必须指明 commandName ,且与表单初始化、提交方法中的表单对象名称保持一致!
页面目录结构如下图所示:
在页面中,我加入了一部分css效果,这部分代码我就不在这里唠叨了,大家可以看源码!
登录试试,如图:
用户名:snwolf 密码:zlex
如果登录成功,我们就会跳转到之前的账户信息页面!
注解的确减少了代码的开发量,当然,这对于我们理解程序是一种挑战!如果你不知道原有的SpringMVC的流程,很难一开始就能摆弄清楚这些内容!
完整代码见附件!
搞搞持久层。不搞太复杂的东西,Spring注解对于持久层的改造并不难懂! 我们用最直接的JdbcTemplate诠释Spring注解持久层部分,关于业务层和事务控制,稍后详述! 某位兄弟不要着急,咱要一步一步来!
这里将用到以下几个包:
aopalliance-1.0.jar
commons-collections.jar
commons-dbcp.jar
commons-logging-1.1.1.jar
commons-pool.jar
jstl.jar
log4j-1.2.15.jar
mysql-connector-java-5.1.6-bin.jar
spring-beans-2.5.6.jar
spring-context-2.5.6.jar
spring-context-support-2.5.6.jar
spring-core-2.5.6.jar
spring-jdbc-2.5.6.jar
spring-tx-2.5.6.jar
spring-web-2.5.6.jar
spring-webmvc-2.5.6.jar
standard.jar
主要增加了commons-collections.jar、commons-dbcp.jar、commons-pool.jar、mysql-connector-java-5.1.6-bin.jar和spring-jdbc-2.5.6.jar
先弄个数据库,这里使用MySQL,我的最爱! 可惜前途未卜!
建库:
- CREATE DATABASE `spring` /*!40100 DEFAULT CHARACTER SET utf8 */;
建表:
- DROP TABLE IF EXISTS `spring`.`account`;
- CREATE TABLE `spring`.`account` (
- `id` int(10) unsigned NOT NULL AUTO_INCREMENT,
- `username` varchar(45) NOT NULL,
- `password` varchar(45) NOT NULL,
- `birthday` datetime NOT NULL,
- `email` varchar(45) NOT NULL,
- PRIMARY KEY (`id`)
- ) ENGINE=InnoDB AUTO_INCREMENT=2 DEFAULT CHARSET=utf8;
插入默认数据:
- INSERT INTO `spring`.`account`(
- `username`,
- `password`,
- `birthday`,
- `email`)
- VALUES(
- 'snowolf',
- 'zlex',
- '2010-01-01',
- 'snowolf@zlex.org');
给出一个数据库查询的结果:
很不巧,为了能让数据查询更有意义,我又要改动Account类:
Account.java
- public class Account implements Serializable {
- /**
- * 主键
- */
- private int id;
- /**
- * 用户名
- */
- private String username;
- /**
- * 密码
- */
- private String password;
- /**
- * 生日
- */
- private Date birthday;
- /**
- */
- private String email;
- // get方法set方法省略
- }
这样,域对象与数据库表将完成一一对应绑定关系。
再建立一个用于构建数据源配置的文件database.properties
database.properties:
- dataSource.driverClassName=com.mysql.jdbc.Driver
- dataSource.url=jdbc:mysql://localhost:3306/spring
- dataSource.username=root
- dataSource.password=admin
- dataSource.maxActive=200
- dataSource.maxIdle=50
- dataSource.maxWait=10000
该文件位于/WEB-INF/目录下。
接下来,我们需要把它引入spring容器,修改applicationContext.xml:
applicationContext.xml
- <context:property-placeholder
- location="/WEB-INF/database.properties" />
如果需要引入多个properties文件,可以用逗号分隔。
这时,我们已经引入了数据源配置,我们可以通过修改dao.xml构建基于DBCP的数据源:
dao.xml中dataSource配置
- <bean
- id="dataSource"
- class="org.apache.commons.dbcp.BasicDataSource"
- destroy-method="close"
- lazy-init="false"
- p:driverClassName="${dataSource.driverClassName}"
- p:url="${dataSource.url}"
- p:username="${dataSource.username}"
- p:password="${dataSource.password}"
- p:maxActive="${dataSource.maxActive}"
- p:maxIdle="${dataSource.maxIdle}"
- p:maxWait="${dataSource.maxWait}" />
上述配置稀松平常,没有什么好阐述的内容,这与一般spring配置无异样。
需要注意的是这个jdbcTemplate配置!
dao.xml中jdbcTemplate配置
- <bean
- class="org.springframework.jdbc.core.JdbcTemplate"
- p:dataSource-ref="dataSource" />
这个配置很关键,如果你要使用其他的ORM框架,同样需要配置这样的模板类,在Dao实现中无需继承JdbcDaoSupport类。
不需要明确JdbcTemplate的id(id="jdbcTemplate")吗?不再需要了!
AccountDao.java
- public interface AccountDao {
- /**
- * 读取账户信息
- *
- * @param username
- * @return
- */
- Account read(String username);
- /**
- * 读取账户信息
- *
- * @param id
- * @return
- */
- Account read(int id);
- }
AccountDaoImpl.java
- /**
- * 2010-1-23
- */
- package org.zlex.spring.dao.impl;
- 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.RowMapper;
- import org.springframework.stereotype.Repository;
- import org.zlex.spring.dao.AccountDao;
- import org.zlex.spring.domain.Account;
- /**
- * 账户数据库实现
- *
- * @author <a href="mailto:zlex.dongliang@gmail.com">梁栋</a>
- * @version 1.0
- * @since 1.0
- */
- @Repository
- public class AccountDaoImpl implements AccountDao {
- @Autowired
- private JdbcTemplate jdbcTemplate;
- @Override
- public Account read(String username) {
- String sql = "SELECT * From account WHERE username = ?";
- return (Account) jdbcTemplate.queryForObject(sql,
- new Object[] { username }, accountRowMap);
- }
- @Override
- public Account read(int id) {
- String sql = "SELECT * From account WHERE id = ?";
- return (Account) jdbcTemplate.queryForObject(sql, new Object[] { id },
- accountRowMap);
- }
- protected RowMapper accountRowMap = new RowMapper() {
- @Override
- public Object mapRow(ResultSet rs, int rowNum) throws SQLException {
- Account account = new Account();
- account.setId(rs.getInt("id"));
- account.setUsername(rs.getString("username"));
- account.setPassword(rs.getString("password"));
- account.setBirthday(rs.getDate("birthday"));
- account.setEmail(rs.getString("email"));
- return account;
- }
- };
- }
分段详述:
注解 @Repository 明确这个类是用于持久层的实现类,注意这样的注解不能用于接口,仅适用于实现类!
同时,不再需要继承JdbcDaoSupport类,其而代之的是直接注入JdbcTemplate类!
再看声明JdbcTemplate类:
- @Autowired
- private JdbcTemplate jdbcTemplate;
需要说明一下,这里的JdbcTemplate对象jdbcTemplate名称可以自定,没有任何限制!
这里使用 RowMapper 定义了一个用于绑定Account域对象的内部映射类:
RowMapper
- protected RowMapper accountRowMap = new RowMapper() {
- @Override
- public Object mapRow(ResultSet rs, int rowNum) throws SQLException {
- Account account = new Account();
- account.setId(rs.getInt("id"));
- account.setUsername(rs.getString("username"));
- account.setPassword(rs.getString("password"));
- account.setBirthday(rs.getDate("birthday"));
- account.setEmail(rs.getString("email"));
- return account;
- }
- };
语句级的内容,十分简单,如下所示:
- @Override
- public Account read(String username) {
- String sql = "SELECT * From account WHERE username = ?";
- return (Account) jdbcTemplate.queryForObject(sql,
- new Object[] { username }, accountRowMap);
- }
- @Override
- public Account read(int id) {
- String sql = "SELECT * From account WHERE id = ?";
- return (Account) jdbcTemplate.queryForObject(sql, new Object[] { id },
- accountRowMap);
- }
写完这两段代码不由感慨,我曾经就这么噼里啪啦的敲了一年多这样的代码。不断的做绑定、映射、写SQL,直到可以有机会将持久层JDBC实现替换Hibernate、iBatis,我才得以解放!
接着,再调整一下Service实现类
AccountServiceImpl.java
- public class AccountServiceImpl implements AccountService {
- @Autowired
- private AccountDao accountDao;
- @Override
- public Account read(String username, String password) {
- Account account = accountDao.read(username);
- if (!password.equals(account.getPassword())) {
- account = null;
- }
- return account;
- }
- @Override
- public Account read(int id) {
- return accountDao.read(id);
- }
- }
使用AccountDao接口来完成响应的操作,逻辑部分不做详述,根据业务逻辑而定!
稍稍修改一下profile.jsp,将用户的生日、邮件地址都输出出来!
- <fieldset><legend>用户信息</legend>
- <ul>
- <li><label>用户名:</label><c:out value="${account.username}" /></li>
- <li><label>生日:</label><fmt:formatDate value="${account.birthday}"
- pattern="yyyy年MM月dd日" /></li>
- <li><label>Email:</label><c:out value="${account.email}" /></li>
- </ul>
- </fieldset>
- 标签<fmt:formatDate />用于格式化输出,大家可以了解一下,很简单很好用的标签! :D
启动应用,登录,查看用户信息:
相关代码见附件!
控制器层、持久层都有了一些介绍,剩下的就是业务层了!
业务层中的关键问题在于事务控制!Spring的注解式事务处理其实很简单!
这里将用到以下几个包:
aopalliance-1.0.jar
commons-collections.jar
commons-dbcp.jar
commons-logging-1.1.1.jar
commons-pool.jar
jstl.jar
log4j-1.2.15.jar
mysql-connector-java-5.1.6-bin.jar
spring-aop-2.5.6.jar
spring-beans-2.5.6.jar
spring-context-2.5.6.jar
spring-context-support-2.5.6.jar
spring-core-2.5.6.jar
spring-jdbc-2.5.6.jar
spring-tx-2.5.6.jar
spring-web-2.5.6.jar
spring-webmvc-2.5.6.jar
standard.jar
主要增加了spring-aop-2.5.6.jar的AOP支持包!
之前我们在AccountService中加入了注解 @Transactional 标签,但是要想要真正发挥事务作用,还需要一些配置。
主要需要调整dao.xml文件
dao.xml-事务管理
- <bean
- id="transactionManager"
- class="org.springframework.jdbc.datasource.DataSourceTransactionManager"
- p:dataSource-ref="dataSource" />
- <tx:annotation-driven
- transaction-manager="transactionManager" />
细化一下AccountService接口方法
AccountService.java
- /**
- * 2010-1-23
- */
- package org.zlex.spring.service;
- import org.springframework.dao.DataAccessException;
- import org.springframework.transaction.annotation.Transactional;
- import org.zlex.spring.domain.Account;
- /**
- * 账户业务接口
- *
- * @author <a href="mailto:zlex.dongliang@gmail.com">梁栋</a>
- * @version 1.0
- * @since 1.0
- */
- public interface AccountService {
- /**
- * 获得账户
- *
- * @param username
- * @param password
- * @return
- */
- @Transactional(readOnly = true)
- Account read(String username, String password);
- /**
- * 获得账户
- *
- * @param id
- * @return
- */
- @Transactional(readOnly = true)
- Account read(int id);
- /**
- * 注册用户
- *
- * @param account
- * @return
- */
- @Transactional(readOnly = false, rollbackFor = DataAccessException.class)
- Account register(Account account);
- }
这里我把注解 @Transactional 调整到了具体的方法上,也就是说这样写的话,凡是加入注解的标注的方法都属于事务配置!
Account register(Account account); 用做用户注册作用!
@Transactional(readOnly = true) 只读属性
@Transactional(readOnly = false, rollbackFor = DataAccessException.class) 只读关闭,遇到DataAccessException异常回滚!如果不对异常进行处理,该异常将一直向上层抛出,直至抛出到页面!
如果你的Eclipse集成了SpringIDE,你可以观察一下这时的xml配置文件和AccoutServiceImpl.java的变化!
这次,来个用户注册功能演示,故意在某个位置制造一个异常,看看是否正常回滚!
先看注册控制器
RegisterController.java
- /**
- * 2010-2-4
- */
- package org.zlex.spring.controller;
- import java.text.DateFormat;
- import java.text.SimpleDateFormat;
- import java.util.Date;
- import org.springframework.beans.factory.annotation.Autowired;
- import org.springframework.beans.propertyeditors.CustomDateEditor;
- import org.springframework.stereotype.Controller;
- import org.springframework.ui.ModelMap;
- import org.springframework.web.bind.WebDataBinder;
- import org.springframework.web.bind.annotation.InitBinder;
- import org.springframework.web.bind.annotation.ModelAttribute;
- import org.springframework.web.bind.annotation.RequestMapping;
- import org.springframework.web.bind.annotation.RequestMethod;
- import org.zlex.spring.domain.Account;
- import org.zlex.spring.service.AccountService;
- /**
- * 用户注册控制器
- *
- * @author <a href="mailto:zlex.dongliang@gmail.com">梁栋</a>
- * @version 1.0
- * @since 1.0
- */
- @Controller
- @RequestMapping(value = "/register.do")
- public class RegisterController {
- @Autowired
- private AccountService accountService;
- @InitBinder
- public void initBinder(WebDataBinder binder) {
- // 忽略字段绑定异常
- // binder.setIgnoreInvalidFields(true);
- DateFormat format = new SimpleDateFormat("yyyy-MM-dd");
- binder.registerCustomEditor(Date.class, "birthday",
- new CustomDateEditor(format, true));
- }
- @RequestMapping(method = RequestMethod.GET)
- public String initForm(ModelMap model) {
- Account account = new Account();
- model.addAttribute("account", account);
- // 直接跳转到登录页面
- return "account/register";
- }
- @RequestMapping(method = RequestMethod.POST)
- protected String submit(@ModelAttribute("account") Account account) {
- int id = accountService.register(account).getId();
- // 跳转到用户信息页面
- return "redirect:profile.do?id=" + id;
- }
- }
@InitBinder 用于表单自定义属性绑定。这里我们要求输入一个日期格式的生日。
@RequestMapping(method = RequestMethod.GET) 用于初始化页面。
@RequestMapping(method = RequestMethod.POST) 用于提交页面。
再看注册页面
register.jsp
- <html>
- <head>
- <meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
- <title>注册</title>
- <link rel="stylesheet" type="text/css" href="css/style.css" />
- <script type="text/javascript" src="js/calendar.js"></script>
- </head>
- <body>
- <fieldset><legend>用户注册</legend><form:form
- commandName="account">
- <ul>
- <li><form:label path="username">用户名:</form:label><form:input
- path="username" /></li>
- <li><form:label path="password">密码:</form:label><form:password
- path="password" /></li>
- <li><form:label path="birthday">生日:</form:label><form:input
- path="birthday" onfocus="showDate(this);" /></li>
- <li><form:label path="email">Email:</form:label><form:input
- path="email" /></li>
- <li>
- <button type="submit">注册</button>
- <button type="reset">重置</button>
- </li>
- </ul>
- </form:form></fieldset>
- </body>
- </html>
这里我用了一个JavaScript日期控制标签:
- <script type="text/javascript" src="js/calendar.js"></script>
使用起来就像是这样:
非常好用!!! 当然,你完全可以使用JE上的那个JS控件!
接下来稍微调整一下AccountService接口及其实现AccountServiceImpl
AccountService.java
- public interface AccountService {
- // 省略
- /**
- * 注册用户
- *
- * @param account
- * @return
- */
- @Transactional(readOnly = false, rollbackFor = DataAccessException.class)
- Account register(Account account);
- // 省略
- }
- @Service
- public class AccountServiceImpl implements AccountService {
- @Autowired
- private AccountDao accountDao;
- // 省略
- @Override
- public Account register(Account account) {
- accountDao.create(account);
- return accountDao.read(account.getUsername());
- }
- }
为了在插入一条记录后获得当前用户的主键,我们还得这么玩! 的确有点雷人~
从架构考虑,这是符合业务要求的实现!如果用iBatis或者Hibernate,这个问题就有数据库一次IO处理完成了!
再看看AccountDao接口及其实现AccountDaoImpl
AccountDao.java
- public interface AccountDao {
- // 省略
- /**
- * 构建用户记录
- *
- * @param account
- * @return
- */
- void create(Account account);
- }
AccountDaoImpl.java
- @Repository
- public class AccountDaoImpl implements AccountDao {
- // 省略
- @Override
- public void create(Account account) {
- String sql = "INSERT INTO account(username, password, birthday, email) VALUES(?,?,?,?)";
- jdbcTemplate.update(sql, new Object[] { account.getUsername(),
- account.getPassword(), account.getBirthday(),
- account.getEmail() });
- }
- }
来个注册演示!
注册:
信息展示:
来制造一起事故!
先看看数据库目前的状况!
在AccountDaoImpl中来个破坏!
- @Override
- public void create(Account account) {
- String sql = "INSERT INTO account(username, password, birthday, email) VALUES(?,?,?,?)";
- jdbcTemplate.update(sql, new Object[] { account.getUsername(),
- account.getPassword(), account.getBirthday(),
- account.getEmail() });
- throw new RecoverableDataAccessException("TEST");
- }
我们强行在执行完Insert语句后抛出DataAccessException异常(RecoverableDataAccessException)!
来个注册试试!
点击提交看看返回的异常!
异常回滚生效!
数据库中当然是什么都没有,我就不废话了!
相关实现见附件
既然系统基于注解自成一体,那么基于Spring的测试是否可以依赖注解轻松完成呢?坚决地没问题!
这里将用到以下几个包:
aopalliance-1.0.jar
commons-collections.jar
commons-dbcp.jar
commons-logging-1.1.1.jar
commons-pool.jar
junit-4.4.jar
jstl.jar
log4j-1.2.15.jar
mysql-connector-java-5.1.6-bin.jar
spring-aop-2.5.6.jar
spring-beans-2.5.6.jar
spring-context-2.5.6.jar
spring-context-support-2.5.6.jar
spring-core-2.5.6.jar
spring-jdbc-2.5.6.jar
spring-tx-2.5.6.jar
spring-test-2.5.6.jar
spring-web-2.5.6.jar
spring-webmvc-2.5.6.jar
standard.jar
主要增加了spring-test-2.5.6.jar和junit-4.4.jar两个用于测试的包!
这里尤其要说明一下,由于我们使用注解方式自然要用到JUnit-4.X系列,而Sring-Test对于JUnit有个累人的要求,JUnit的版本必须是4.4,不支持高版本(如4.5、4.7等)。否则,会产生java.lang.ClassNotFoundException: org.junit.Assume$AssumptionViolatedException异常。
先来一个能够自动回滚的用于测试的父类——AbstractTestCase
AbstractTestCase.java
- /**
- * 2009-12-16
- */
- package org.zlex.spring;
- import org.junit.runner.RunWith;
- import org.springframework.test.AbstractTransactionalDataSourceSpringContextTests;
- import org.springframework.test.context.ContextConfiguration;
- import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
- import org.springframework.test.context.transaction.TransactionConfiguration;
- import org.springframework.transaction.annotation.Transactional;
- /**
- * @author <a href="mailto:zlex.dongliang@gmail.com">梁栋</a>
- * @version 1.0
- * @since 1.0
- */
- @ContextConfiguration(locations = "classpath:applicationContext.xml")
- @RunWith(SpringJUnit4ClassRunner.class)
- @Transactional
- @TransactionConfiguration(transactionManager = "transactionManager", defaultRollback = true)
- public abstract class AbstractTestCase extends
- AbstractTransactionalDataSourceSpringContextTests {
- }
让每一个测试类都写一堆配置忒麻烦! 索性来个老爹替子子孙孙都完成基础工作!
逐行分析:
@ContextConfiguration(locations = "classpath:applicationContext.xml") 导入配置文件。这时候,我们可以看出之前使用applicationContext.xml文件作为系统总控文件的好处! 当然,Spring-Test的这个配置只认classpath,很无奈,我必须拷贝这些文件到根目录!
@RunWith(SpringJUnit4ClassRunner.class) SpringJUnit支持,由此引入Spring-Test框架支持!
@Transactional 这个非常关键,如果不加入这个注解配置,事务控制就会完全失效!
@TransactionConfiguration(transactionManager = "transactionManager", defaultRollback = true) 这里的事务关联到配置文件中的事务控制器(transactionManager = "transactionManager"),同时指定自动回滚(defaultRollback = true)。这样做操作的数据才不会污染数据库!
AbstractTransactionalDataSourceSpringContextTests 要想构建这一系列的无污染纯绿色事务测试框架就必须找到这个基类!
给出一个整体结构图:
test子目录下的文件将编译到classpath下,这其实还同时是个maven测试项目!拷一堆配置文件的确有些不方便!
AbstractTestCase.java用于抽象测试类控制。
AccountDaoTest.java用于AccountDao测试。
DaoAllTests.java用于Dao层的整体测试。
来看看AccountDaoTest
AccountDaoTest.java
- /**
- * 2009-12-16
- */
- package org.zlex.spring;
- import java.util.Date;
- import org.junit.Test;
- import org.springframework.beans.factory.annotation.Autowired;
- import org.zlex.spring.dao.AccountDao;
- import org.zlex.spring.domain.Account;
- /**
- * @author <a href="mailto:zlex.dongliang@gmail.com">梁栋</a>
- * @version 1.0
- * @since 1.0
- */
- public class AccountDaoTest extends AbstractTestCase {
- @Autowired
- private AccountDao accountDao;
- @Test
- public void test() {
- Account ac = new Account();
- ac.setBirthday(new Date());
- ac.setUsername("SPRING");
- ac.setPassword("SNOWOLF");
- ac.setEmail("spring@zlex.org");
- // 创建用户
- accountDao.create(ac);
- // 检索
- Account account = accountDao.read("SPRING");
- // 校验
- assertNotNull(account);
- }
- }
只要记得使用注解 @Test 标识方法即可!
这里插入了一条数据,之后进行检索。如果数据存在则认为测试成功! 当然,这时候你要看看数据库是不是真的插入了一条数据!
执行这个方法,同时监控数据库,观察日志!最有效的办法是在执行检索方法时加入断点,同时监控数据库记录,你会发现此时数据库无此数据录入! 实际上这是一个未提交的事务!
完成操作,看看这时的日志:
数据库其实已经进行了回滚!
再看看DaoAllTests
DaoAllTests.java
- /**
- * 2009-12-17
- */
- package org.zlex.spring;
- import org.junit.runner.RunWith;
- import org.junit.runners.Suite;
- import org.junit.runners.Suite.SuiteClasses;
- /**
- * @author <a href="mailto:zlex.dongliang@gmail.com">梁栋</a>
- * @version 1.0
- * @since 1.0
- */
- @RunWith(Suite.class)
- @SuiteClasses( { AccountDaoTest.class, AccountDaoTest.class })
- public class DaoAllTests {
- }
逐行说明:
@RunWith(Suite.class) 集合测试
@SuiteClasses( { AccountDaoTest.class }) 集合,包括AccountDaoTest类,多个测试类可使用逗号分隔!
这个测试类可用于Dao层集合测试,与Spring无关!
完整代码见附件!
Spring-Test义不容辞的完成了这个任务!并且,通过Spring-Test的事务会滚控制,我们可以在不污染数据库数据的前提下进行业务测试!
完成这项内容,本次Spring 注解学习手札整理就正式落幕了! 感谢大家的关注
最近需要做些接口服务,服务协议定为JSON,为了整合在Spring中,一开始确实费了很大的劲,经朋友提醒才发现,SpringMVC已经强悍到如此地步,佩服!
SpringMVC层跟JSon结合,几乎不需要做什么配置,代码实现也相当简洁。再也不用为了组装协议而劳烦辛苦了!
一、Spring注解@ResponseBody,@RequestBody和HttpMessageConverter
Spring 3.X系列增加了新注解@ResponseBody,@RequestBody
- @RequestBody 将HTTP请求正文转换为适合的HttpMessageConverter对象。
- @ResponseBody 将内容或对象作为 HTTP 响应正文返回,并调用适合HttpMessageConverter的Adapter转换对象,写入输出流。
HttpMessageConverter 接口,需要开启 <mvc:annotation-driven /> 。
AnnotationMethodHandlerAdapter 将会初始化7个转换器,可以通过调用 AnnotationMethodHandlerAdapter 的 getMessageConverts() 方法来获取转换器的一个集合 List<HttpMessageConverter>
引用 ByteArrayHttpMessageConverter
StringHttpMessageConverter
ResourceHttpMessageConverter
SourceHttpMessageConverter
XmlAwareFormHttpMessageConverter
Jaxb2RootElementHttpMessageConverter
MappingJacksonHttpMessageConverter
可以理解为,只要有对应协议的解析器,你就可以通过几行配置,几个注解完成协议——对象的转换工作!
PS:Spring默认的json协议解析由Jackson完成。
二、servlet.xml配置
Spring的配置文件,简洁到了极致,对于当前这个需求只需要三行核心配置:
- <context:component-scan base-package="org.zlex.json.controller" />
- <context:annotation-config />
- <mvc:annotation-driven />
三、pom.xml配置
闲言少叙,先说依赖配置,这里以Json+Spring为参考:
pom.xml
- <dependency>
- <groupId>org.springframework</groupId>
- <artifactId>spring-webmvc</artifactId>
- <version>3.1.2.RELEASE</version>
- <type>jar</type>
- <scope>compile</scope>
- </dependency>
- <dependency>
- <groupId>org.codehaus.jackson</groupId>
- <artifactId>jackson-mapper-asl</artifactId>
- <version>1.9.8</version>
- <type>jar</type>
- <scope>compile</scope>
- </dependency>
- <dependency>
- <groupId>log4j</groupId>
- <artifactId>log4j</artifactId>
- <version>1.2.17</version>
- <scope>compile</scope>
- </dependency>
主要需要 spring-webmvc 、 jackson-mapper-asl 两个包,其余依赖包Maven会帮你完成。至于 log4j ,我还是需要看日志嘛。
包依赖图:
至于版本,看项目需要吧!
四、代码实现
域对象:
- public class Person implements Serializable {
- private int id;
- private String name;
- private boolean status;
- public Person() {
- // do nothing
- }
- }
这里需要一个空构造,由Spring转换对象时,进行初始化。
@ResponseBody,@RequestBody,@PathVariable
控制器:
- @Controller
- public class PersonController {
- /**
- * 查询个人信息
- *
- * @param id
- * @return
- */
- @RequestMapping(value = "/person/profile/{id}/{name}/{status}", method = RequestMethod.GET)
- public @ResponseBody
- Person porfile(@PathVariable int id, @PathVariable String name,
- @PathVariable boolean status) {
- return new Person(id, name, status);
- }
- /**
- * 登录
- *
- * @param person
- * @return
- */
- @RequestMapping(value = "/person/login", method = RequestMethod.POST)
- public @ResponseBody
- Person login(@RequestBody Person person) {
- return person;
- }
- }
备注: @RequestMapping(value = "/person/profile/{id}/{name}/{status}", method = RequestMethod.GET) 中的 {id}/{name}/{status} 与 @PathVariable int id, @PathVariable String name,@PathVariable boolean status 一一对应,按名匹配。 这是restful式风格。
如果映射名称有所不一,可以参考如下方式:
- @RequestMapping(value = "/person/profile/{id}", method = RequestMethod.GET)
- public @ResponseBody
- Person porfile(@PathVariable("id") int uid) {
- return new Person(uid, name, status);
- }
- GET模式下,这里使用了@PathVariable绑定输入参数,非常适合Restful风格。因为隐藏了参数与路径的关系,可以提升网站的安全性,静态化页面,降低恶意攻击风险。
- POST模式下,使用@RequestBody绑定请求对象,Spring会帮你进行协议转换,将Json、Xml协议转换成你需要的对象。
- @ResponseBody可以标注任何对象,由Srping完成对象——协议的转换。
做个页面测试下:
JS
- $(document).ready(function() {
- $("#profile").click(function() {
- profile();
- });
- $("#login").click(function() {
- login();
- });
- });
- function profile() {
- var url = 'http://localhost:8080/spring-json/json/person/profile/';
- var query = $('#id').val() + '/' + $('#name').val() + '/'
- + $('#status').val();
- url += query;
- alert(url);
- $.get(url, function(data) {
- alert("id: " + data.id + "\nname: " + data.name + "\nstatus: "
- + data.status);
- });
- }
- function login() {
- var mydata = '{"name":"' + $('#name').val() + '","id":"'
- + $('#id').val() + '","status":"' + $('#status').val() + '"}';
- alert(mydata);
- $.ajax({
- type : 'POST',
- contentType : 'application/json',
- url : 'http://localhost:8080/spring-json/json/person/login',
- processData : false,
- dataType : 'json',
- data : mydata,
- success : function(data) {
- alert("id: " + data.id + "\nname: " + data.name + "\nstatus: "
- + data.status);
- },
- error : function() {
- alert('Err...');
- }
- });
Table
- <table>
- <tr>
- <td>id</td>
- <td><input id="id" value="100" /></td>
- </tr>
- <tr>
- <td>name</td>
- <td><input id="name" value="snowolf" /></td>
- </tr>
- <tr>
- <td>status</td>
- <td><input id="status" value="true" /></td>
- </tr>
- <tr>
- <td><input type="button" id="profile" value="Profile——GET" /></td>
- <td><input type="button" id="login" value="Login——POST" /></td>
- </tr>
- </table>
四、简单测试
Get方式测试:
Post方式测试:
五、常见错误
POST操作时,我用$.post()方式,屡次失败,一直报各种异常:
引用 org.springframework.web.HttpMediaTypeNotSupportedException: Content type 'application/x-www-form-urlencoded;charset=UTF-8' not supported
org.springframework.web.HttpMediaTypeNotSupportedException: Content type 'application/x-www-form-urlencoded;charset=UTF-8' not supported
org.springframework.web.HttpMediaTypeNotSupportedException: Content type 'application/x-www-form-urlencoded;charset=UTF-8' not supported
直接用$.post()直接请求会有点小问题,尽管我标识为 json 协议,但实际上提交的 ContentType 还是 application/x-www-form-urlencoded 。需要使用$.ajaxSetup()标示下 ContentType 。
- function login() {
- var mydata = '{"name":"' + $('#name').val() + '","id":"'
- + $('#id').val() + '","status":"' + $('#status').val() + '"}';
- alert(mydata);
- $.ajaxSetup({
- contentType : 'application/json'
- });
- $.post('http://localhost:8080/spring-json/json/person/login', mydata,
- function(data) {
- alert("id: " + data.id + "\nname: " + data.name
- + "\nstatus: " + data.status);
- }, 'json');
- };
效果是一样!
Spring注解,改变了我的开发思路。前段时间,用 @RequestBody , @ResponseBody ,不费吹灰之力就解决了JSon自动绑定。接着就发现,如果遇到 RuntimeException ,需要给出一个默认返回JSON。
以前都是用SimpleMappingExceptionResolver拦截实现,今天偶尔看下资料,@ExceptionHandler,就把这个异常给拦截了,太方便了!
直接上代码:
- @Controller
- public class AccessController {
- /**
- * 异常页面控制
- *
- * @param runtimeException
- * @return
- */
- @ExceptionHandler(RuntimeException.class)
- public @ResponseBody
- Map<String,Object> runtimeExceptionHandler(RuntimeException runtimeException) {
- logger.error(runtimeException.getLocalizedMessage());
- Map model = new TreeMap();
- model.put("status", false);
- return model;
- }
- }
当这个Controller中任何一个方法发生异常,一定会被这个方法拦截到。然后,输出日志。封装Map并返回,页面上得到status为false。就这么简单。
或者这个有些有些复杂,来个简单易懂的,上代码:
- @Controller
- public class AccessController {
- /**
- * 异常页面控制
- *
- * @param runtimeException
- * @return
- */
- @ExceptionHandler(RuntimeException.class)
- public String runtimeExceptionHandler(RuntimeException runtimeException,
- ModelMap modelMap) {
- logger.error(runtimeException.getLocalizedMessage());
- modelMap.put("status", IntegralConstant.FAIL_STATUS);
- return "exception";
- }
- }