Java - SpringMVC 框架详解(三)

1. 关于@RequestMapping注解

在处理请求的方法之前添加@RequestMapping注解,可以配置请求路径与处理请求的方法的映射关系!

在该注释中声明了:

@AliasFor("path")
String[] value() default {
    
    };

和:

@AliasFor("value")
String[] path() default {
    
    };

则可以在使用注解时,配置一个名为value的属性,该属性的值是字符串类型的数组:

@RequestMapping(value={
    
    "值1", "值2", "值3"})

该属性的作用就是配置映射的路径,是默认的属性,所以,在配置时,可以不必显式的声明属性名称,即:以上配置可以简化为:

@RequestMapping({
    
    "值1", "值2", "值3"})

在配置注解的属性时,如果属性的值是某种数组类型的,其值可以是数组格式的,也可以是它的某个元素,并不一定需要写成数组,例如:

@RequestMapping("值1")

所以,以下几种写法是等效的:

@RequestMapping(value={
    
    "reg.do"})
@RequestMapping({
    
    "reg.do"})
@RequestMapping(value="reg.do")
@RequestMapping("reg.do")

即:当值是数组格式,且当前只设置1个值时,写不写成数组格式是无所谓的,当设置的是value值,不需要写出value=,而是直接写值即可!

该注解中的pathvalue属性是完全等效的,path是从SpringMVC 4.2版本加入的!

而且,该属性并不仅仅只是用于配置方法之前的路径,还可以将该注解声明在控制器类之前并进行配置,例如:

@RequestMapping("user")
@Controller
public class UserController {
    
    

	@RequestMapping("login.do")
	public String showLogin() {
    
    
		return "login";
	}

}

在类之前添加注解后,当访问控制器类中配置在某个方法之前映射的路径时,在URL中需要先添加在类之前的路径,再添加方法之前的路径!以以上代码为例,访问路径应该是http://localhost:8080/项目名/user/login.do

并且,在类之前添加注解后,在当前控制器类的每一个方法的映射的路径之前都必须添加这一层路径!

强烈建议在每一个控制器类之前都添加该注解!

在配置路径时,左右两侧多余的/可以被忽略,并且,在类和方法之前都配置时,也不需要考虑类的注解路径拼接方法的注解路径之间的/的问题!即几下在类和方法之前添加配置的效果是等效的:

/user	/login.do
/user	login.do
/user/	/login.do
/user/	login.do
user	/login.do
user	login.do
user/	/login.do
user/	login.do

以上做法都是等效的,在实际应用时,选取其中1种风格,并在项目中保持使用固定的风格即可!

另外,在注解的源代码中,还声明了:

RequestMethod[] method() default {
    
    };

以上属性是用于约束请求方式的!假设控制器中配置为:

@RequestMapping(path="handle_reg.do", method=RequestMethod.POST)

客户端却使用GET方式提交请求,就会出现405错误:

HTTP Status 405 – Method Not Allowed

Message : Request method 'GET' not supported

当注解中配置多个属性时,每个属性的配置中,都必须显式的指定属性名!仅当只配置1个属性,且属性是默认的,才可以不必显式的指定属性名!

2. 关于@RequestParam注解

可以在处理请求的方法的参数之前添加@RequestParam注解!

在该注解中声明了:

@AliasFor("name")
String value() default "";

@AliasFor("value")
String name() default "";

也就是说,在注解中可以配置valuename属性,这2个属性是等效的!其作用是配置将客户端提交的哪个请求参数,绑定到当前方法的参数中!默认情况下,假设客户端提交了名为username的参数,在处理请求的方法的参数列表中,声明String username即可获取到客户端提交的参数的值,即只要同名即可!但是,如果名称无法匹配,就可以使用该注解进行配置!

假设客户端提交了名为uname的请求参数,则在控制器的处理请求的方法中,为参数添加注解:

@RequestParam("uname") String username

也可以正常的获取到请求参数!所以,以上配置的作用就是:将客户端提交的名为uname的参数,绑定到处理请求的方法中名为username的参数中!

当添加以上注解后,如果客户端发出请求时,并没有提交匹配名称的参数,默认情况下,会出现400错误:

HTTP Status 400 – Bad Request

Message : Required String parameter 'uname' is not present

之所以会出现以上问题,是因为在@RequestParam的源代码中存在:

boolean required() default true;

即:该源代码中声明了名为required的属性,值是boolean类型的,默认值为true

所以,如果一定要使用该注解,并且允许不提交该请求参数,可以配置为:

@RequestParam(name="uname", required=false) String username

最后,该注解的源代码中还有:

String defaultValue() default ValueConstants.DEFAULT_NONE;

该属性是用于配置默认值的,即客户端没有提交指定的请求参数时,等同于提交了某个值!例如配置为:

@RequestParam(name="uname", required=false, defaultValue="admin") String username

当然,在使用defaultValue属性时,务必required属性设置为false

3. 关于Session

因为HTTP协议是无状态协议,所以,同一个客户端的多次请求,服务器是无法区分这是来自同一个客户端的,如果需要保存用户的某些数据或状态,就需要使用Session。

Session是保存在服务器端的内存中的数据,每个客户端提交请求后,都会在服务器端的内存中有一份专属于这个客户端的数据!

在SpringMVC中,如果需要使用Session,只需要在处理请求的方法的参数列表中添加HttpSession类型的参数即可,然后,在处理请求的过程中,调用该参数对象的API封装或取出数据。

直接使用HttpSession作为处理请求的方法的参数,主要问题在于不便于执行单元测试!

在SpringMVC中,也提供了框架中管理Session的做法,就是将需要封装在Session中的数据放在ModelMap中,也就是说,不管是要转发的数据,还是Session的数据,都放在ModelMap中,然后,还要在控制器类之前添加@SessionAttributes注解,用于配置“ModelMap中的哪个属性的数据是Session中的数据”,并且,这种做法并不是真的将数据放到了Session中,而是由框架进行管理的,即使调用HttpSessioninvalidate()方法也不能清除这些数据!

总的来说,使用SpringMVC的做法来管理Session相对比较麻烦,使用HttpSession的用法虽然不便于执行单元测试,但是,用法却非常简单,代码也很直观,所以,一般使用HttpSession的较多。

@RequestMapping(path = "handleLogin.do", method = {
    
     RequestMethod.GET, RequestMethod.POST})
public String handleLogin (
          String username,
          String password,
          @RequestParam(name = "sex", required = false, defaultValue = "未知") String sex,
          String[] skill,
          ModelMap modelMap,
          HttpSession session
  ) {
    
    
      System.out.println("username = " + username);
      System.out.println("password = " + password);
      System.out.println("sex = " + sex);
      System.out.println("skill = " + skill);
//        System.out.println("skill.length = " + skill.length);
//        if (skill.length > 0) {
    
    
//            for (int i = 0; i < skill.length; i++) {
    
    
//                System.out.println("skill[" + i + "] = " + skill[i]);
//            }
//        }

      if ("admin".equals(username)) {
    
    
          if ("123456".equals(password)) {
    
    
              System.out.println("登录成功");
              session.setAttribute("id", username + password);
              modelMap.addAttribute("username", username);
              return "redirect:../main/index.do";
          } else {
    
    
              System.out.println("密码错误");
              return "loginFailed";
          }
      } else {
    
    
          System.out.println("用户名不存在");
          return "loginFailed";
      }
  }

4. 拦截器(Interceptor)

拦截器:是一种可以使得若干种请求都会自动的执行其中的代码组件!该组件对所处理的请求可以选择放行,或选择阻止继续执行!

在SpringMVC项目中,如果需要使用拦截器,首先,需要自定义类,实现HandlerInterceptor拦截器接口:

public class LoginInterceptor implements HandlerInterceptor {
    
    
	
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler)
			throws Exception {
    
    
		System.out.println("LoginInterceptor.preHandle()");
		return false;
	}

	public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler,
			ModelAndView modelAndView) throws Exception {
    
    
		System.out.println("LoginInterceptor.postHandle()");
	}

	public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex)
			throws Exception {
    
    
		System.out.println("LoginInterceptor.afterCompletion()");
	}

}

然后,在spring.xml中添加配置:

<!-- 配置拦截器链 -->
<mvc:interceptors>
	<!-- 配置第1个拦截器 -->
	<mvc:interceptor>
		<!-- 拦截路径 -->
		<mvc:mapping path="/main/index.do" />
		<!-- 拦截器类 -->
		<bean class="cn.tedu.spring.LoginInterceptor"></bean>
	</mvc:interceptor>		
	<!-- 配置第2个拦截器 -->
</mvc:interceptors>

可以看到,当拦截器类的preHandle()方法返回false时,表示阻止继续运行,浏览器的页面中将显示一片空白,当返回true时,表示放行,会依次执行preHandle() -> 控制器类中的方法 -> postHandle() -> afterCompletion()

所以,真正具有“拦截”功能的,其实只有preHandle()方法

如果需要实现“登录拦截”的效果,即:如果已经登录,允许正常访问,如果没有登录,则重定向到登录页。可以在拦截器的preHandle()方法中,对Session中的数据进行判断,选择放行或阻止运行,并且,在阻止运行时,重定向到登录页面即可!例如:

public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
    
    
	System.out.println("LoginInterceptor.preHandle()");
	HttpSession session = request.getSession();
	if (session.getAttribute("id") == null) {
    
    
		// 重定向:必须使用绝对路径!
		response.sendRedirect(request.getContextPath() + "/user/login.do");
		// 返回 false 代表被拦截,不再继续执行
		return false; // // 这里必须要return false,否则程序依然会往下执行!
	}
	return true;
}

如果还有其它路径也需要被拦截器处理,可以在配置中添加更多的<mvc:mapping />节点,例如:

<!-- 配置拦截器链 -->
<mvc:interceptors>
	<!-- 配置第1个拦截器 -->
	<mvc:interceptor>
		<!-- 拦截路径 -->
		<mvc:mapping path="/main/index.do" />
		<mvc:mapping path="/user/password.do" />
		<mvc:mapping path="/blog/delete.do" />
		<mvc:mapping path="/blog/publish.do" />
		<mvc:mapping path="/blog/edit.do" />
		<!-- 拦截器类 -->
		<bean class="cn.tedu.spring.LoginInterceptor"></bean>
	</mvc:interceptor>		
	<!-- 配置第2个拦截器 -->
</mvc:interceptors>

在配置路径时,还可以使用星号(*)通配符,例如:

<mvc:mapping path="/blog/*" />

则以上配置可以匹配到/blog/delete.do/blog/edit.do等……

需要注意的是:1个星号只能表示资源,不能通配多层级路径,例如/blog/*就不可以匹配到/blog/2019/spring.do这样的路径!如果需要通配多层级路径及资源,必须使用2个星号(**),例如配置为/blog/**

除此以外,还可以添加<mvc:exclude-mapping />节点来配置例外路径/排除路径,例如已经配置了<mvc:mapping path="/user/**" />,为了保证注册、登录功能的正常使用,应该把相关路径设置为“例外”:

<!--  配置拦截器链  -->
<mvc:interceptors>
     <!-- 配置第一个拦截器 -->
     <mvc:interceptor>
         <!-- 1. 拦截路径(顺序必须在前) -->
         <mvc:mapping path="/main/index.do"></mvc:mapping>
         <mvc:mapping path="/user/**"/>
         <!-- 2. 例外(白名单) -->
         <mvc:exclude-mapping path="/user/login.do"></mvc:exclude-mapping>
         <mvc:exclude-mapping path="/user/handleLogin.do"></mvc:exclude-mapping>
         <mvc:exclude-mapping path="/user/reg.do"></mvc:exclude-mapping>
         <!-- 拦截器类 -->
         <bean class="cn.tedu.spring.LoginInterceptor"></bean>
     </mvc:interceptor>
     <!-- 配置第2个拦截器 -->
 </mvc:interceptors>

凡是添加在“例外”的路径,就相当于拦截器并不处理这些路径对应的请求,拦截器中的所有方法都是不执行的!

<mvc:interceptor>的配置中,子级的节点顺序必须是先配置<mvc:mapping />,最后配置<bean>

5. 使用过滤器解决POST请求的乱码问题

在SpringMVC框架中,默认使用的编码全部是ISO-8859-1,是不支持中文的

在SpringMVC框架中,定义了CharacterEncodingFilter过滤器类,用于处理字符编码,所以,应该在web.xml中配置这个过滤器类,并指定所使用的编码即可:

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

<filter-mapping>
	<filter-name>CharacterEncodingFilter</filter-name>
	<url-pattern>/*</url-pattern>
</filter-mapping>

6. SpringMVC小结

  1. 理解SpringMVC框架的作用;

  2. 认识在web.xml中关于DispatcherServletCharacterEncodingFilter的配置;

  3. 认识在spring.xml中关于组件扫描和Thymeleaft相关的配置,主要是使用哪种模版解析器,及前缀与后缀的配置;

  4. 掌握创建类的正确创建方式(必须在组件扫描范围内,必须添加@Controller注解);

  5. 掌握添加方法处理请求;

  6. 掌握@RequestMapping@RequestParam注解的使用;

  7. 掌握接收请求参数的方式;

  8. 掌握转发数据的方式;

  9. 掌握重定向的做法;

  10. 掌握Session的管理;

  11. 掌握拦截器的使用;

  12. 理解SpringMVC核心执行流程图。

附1:关于拦截器(Interceptor)和过滤器(Filter)

拦截器和过滤器都是可以应用于若干种不同的请求的组件,并且,都可以实现阻止运行或放行的效果,甚至,这2种组件都可以形成“链”。

过滤器(Filter)是Java EE中的组件,而拦截器(Interceptor)是SpringMVC中的组件!即:任何一个Java Web项目都可以使用过滤器,但是,只有使用了SpringMVC框架的项目才可以使用拦截器,并且,如果SpringMVC框架中关于DispatcherServlet的路径配置为*.do的话,只有被SpringMVC框架处理的请求(以.do作为后缀的请求),才可能被拦截器处理!

过滤器(Filter)是执行在所有的Servlet之前的组件,而SpringMVC中的拦截器的第1次执行是在DispatcherServlet之后,且在Controller之前执行的!(对应SpringMVC核心执行流程图,过滤器是1号位置执行,而拦截器第1次执行是在4号位置)

过滤器(Filter)需要在web.xml中进行配置,且映射的路径配置相对比较受限(没有白名单),产生的配置代码量也非常大(每个路径对应4行配置代码),而拦截器(Interceptor)的配置简单、灵活!

所以,如果某个请求是被SpringMVC框架所处理的,并且,不在乎处理的时间,应该优先使用拦截器(Interceptor),反之,如果必须在所有Servlet之前就必须处理的,就必须使用过滤器(Filter)。

完整的spring.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:jdbc="http://www.springframework.org/schema/jdbc"
       xmlns:jee="http://www.springframework.org/schema/jee"
       xmlns:tx="http://www.springframework.org/schema/tx"
       xmlns:aop="http://www.springframework.org/schema/aop"
       xmlns:mvc="http://www.springframework.org/schema/mvc"
       xmlns:util="http://www.springframework.org/schema/util"
       xmlns:jpa="http://www.springframework.org/schema/data/jpa"
       xsi:schemaLocation="
		http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-3.2.xsd
		http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context-3.2.xsd
		http://www.springframework.org/schema/jdbc http://www.springframework.org/schema/jdbc/spring-jdbc-3.2.xsd
		http://www.springframework.org/schema/jee http://www.springframework.org/schema/jee/spring-jee-3.2.xsd
		http://www.springframework.org/schema/tx http://www.springframework.org/schema/tx/spring-tx-3.2.xsd
		http://www.springframework.org/schema/data/jpa http://www.springframework.org/schema/data/jpa/spring-jpa-1.3.xsd
		http://www.springframework.org/schema/aop http://www.springframework.org/schema/aop/spring-aop-3.2.xsd
		http://www.springframework.org/schema/mvc http://www.springframework.org/schema/mvc/spring-mvc-3.2.xsd
		http://www.springframework.org/schema/util http://www.springframework.org/schema/util/spring-util-3.2.xsd">

    <context:component-scan base-package="cn.tedu"></context:component-scan>

    <!-- 配置模版解析器 -->
    <bean id="templateResolver"
          class="org.thymeleaf.templateresolver.ClassLoaderTemplateResolver">
        <!-- 前缀 -->
        <property name="prefix" value="/web/"/>
        <!-- 后缀 -->
        <property name="suffix" value=".html"/>
        <!-- 字符编码 -->
        <property name="characterEncoding" value="UTF-8"/>
        <!-- 模版模式 -->
        <property name="templateMode" value="HTML"/>
        <!-- 是否缓存 -->
        <property name="cacheable" value="false"/>
    </bean>

    <!-- 配置模版引擎 -->
    <bean id="templateEngine" class="org.thymeleaf.spring4.SpringTemplateEngine">
        <!-- 模版解析器 -->
        <property name="templateResolver" ref="templateResolver"/>
    </bean>

    <!-- 配置视图解析器:ThymeleafViewResolver -->
    <bean class="org.thymeleaf.spring4.view.ThymeleafViewResolver">
        <!-- 模版引擎 -->
        <property name="templateEngine" ref="templateEngine"/>
        <!-- 字符编码 -->
        <property name="characterEncoding" value="UTF-8"/>
    </bean>

    <!--  配置拦截器链  -->
    <mvc:interceptors>
        <!-- 配置第一个拦截器 -->
        <mvc:interceptor>
            <!-- 1. 拦截路径(顺序必须在前) -->
            <mvc:mapping path="/main/index.do"></mvc:mapping>
            <mvc:mapping path="/user/**"/>
            <!-- 2. 例外(白名单) -->
            <mvc:exclude-mapping path="/user/login.do"></mvc:exclude-mapping>
            <mvc:exclude-mapping path="/user/handleLogin.do"></mvc:exclude-mapping>
            <mvc:exclude-mapping path="/user/reg.do"></mvc:exclude-mapping>
            <!-- 拦截器类 -->
            <bean class="cn.tedu.spring.LoginInterceptor"></bean>
        </mvc:interceptor>
    </mvc:interceptors>
</beans>

Spring AOP

AOP:面向切面(Aspect)编程;

AOP并不是Spring的特有的功能,只不过是Spring很好的支持了AOP。

在一个项目中,关于功能的处理流程是:

注册:	前端页面 --> 控制器层 --> 业务层 --> 持久层

登录:	前端页面 --> 控制器层 --> 业务层 --> 持久层

改密:	前端页面 --> 控制器层 --> 业务层 --> 持久层

头像:	前端页面 --> 控制器层 --> 业务层 --> 持久层

如果,需要在每个功能的处理过程中,增加一些新的操作,例如计算业务层的执行耗时,或添加某些日志等!如果按照传统模式来实现,则应该将相关的代码封装在某个方法中,然后,在业务层的各方法中均调用这个封装的方法即可!

AOP在思想,是假想在整个处理流程中可以存在切面,切面中可以有方法,只要将切面加在处理流程中,就可以使得整个处理流程过程中执行切面中的方法!

在开发AOP之前,需要添加 aspectj-toolsaspectj-weaver 这2个依赖:

<dependency>
 <groupId>org.aspectj</groupId>
 <artifactId>aspectjtools</artifactId>
 <version>1.9.5</version>
</dependency>

<dependency>
 <groupId>org.aspectj</groupId>
 <artifactId>aspectjweaver</artifactId>
 <version>1.9.5</version>
</dependency>

使用AOP时,并不会对原有的处理业务的代码产生影响!首先,需要创建一个切面类,如cn.tedu.store.aop.TimerAspect。并在类之前添加@Aspect和@Component注解:

@Aspect
@Component
public class TimerAspect {
    
    

}

然后,需要确定切面的作用方式,如果是添加在某个组件之前,后续需要使用@Before注解,如果是添加在某个组件之后,后续需要使用@After注解,如果是在某个组件之前和之后都需要执行,后续需要使用@Around注解!所以,如果需求是“统计业务层的执行耗时”,则应该在业务层执行之前记录时间,并在执行之后也记录时间,对比2个时间,就能得到执行耗时,所以,此功能需要使用@Around注解!

然后,还需要确定切面的连接点(作用于哪里),后续将通过表达式来设置!如果需要作用于业务层,可以将表达式设置为:

execution( cn.tedu.store.service.impl..*(..))
// 无视返回值 impl包.所在类.所有方法(无视参数数量)

将以上注解中配置以上表达式,将注解添加在切面方法之前即可,关于切面方法的声明:

应该使用public权限;

如果使用@Around注解,需要使用Object作为返回值类型,表示业务层方法的返回值;

方法名可以自定义;

如果使用@Around注解,需要添加ProceedingJoinPoint作为参数,它是用于调用业务方法的!

所以,在切面类中添加切面方法:

@Aspect
@Component
public class TimerAspect {
    
    

  @Around("execution( cn.tedu.store.service.impl..*(..))")
  public Object aaaaa(ProceedingJoinPoint pjp) {
    
    
  	return null;
  }

}

最后,完成切面方法:

@Aspect
@Component
public class TimerAspect {
    
    

  @Around("execution( cn.tedu.store.service.impl..*(..))")
  public Object aaaaa(ProceedingJoinPoint pjp) throws Throwable {
    
    
  	// 记录起始时间
  	long start = System.currentTimeMillis();
  	
  	// 执行业务层的方法,例如UserServiceImpl中的reg()方法或login()方法
  	// 调用以下方法时,会出现异常,必须抛出,不可以try...catch
  	Object obj = pjp.proceed();
  	
  	// 记录结束时间
  	long end = System.currentTimeMillis();
  	System.err.println("耗时:" + (end - start) + "ms.");
  	
  	// 必须返回以上proeceed()方法的返回值
  	// 否则,相当于login()、reg()等业务方法都不会返回值
  	return obj;
  }

}

如果这篇文章有帮助到您,请简单给个赞吧,谢谢~

猜你喜欢

转载自blog.csdn.net/Kevinblant/article/details/120858098