Struts2 MVC框架换成SpringMVC框架的详细步骤

洪浩
2023-12-01

一、前言

       其实大家在工作的时候,一般很少涉及框架大改的情况,除非是一个值得重视与长期发展的项目,碰巧我遇到了一个比较重要的项目,由于项目的初次搭建的时候欠缺很多方法的考虑,使用了比较旧的架构:Spring+Struts2+Hibernate+MyBatis,这个框架也有一些优点,比如Hibernate+MyBatis可以同时使用,而且还共用了数据源,也不影响事务的切面。由于Spring的发展比Struts2的发展快以及各项优点(此处不多说),加上SpringBoot、SpringCloud的出现,大多数程序员都倾向于使用SpringMVC而不是Struts2MVC,因此,考虑到公司大项目的发展,项目计划第一步由Struts2MVC转换成SpringMVC(Spring版本不变,还是3版本),第二步升级Spring版本(由版本3升级版本4,尽量做到无配置文件),第三步转为SpringBoot项目或者SpringCloud微服务(模块分割),此处由有多年架构方面经验的我来完成项目计划的第一步(由Struts2MVC转换成SpringMVC)

 

二、步骤(以下步骤仅供参考,要结合自己项目而调整)

1、修改pom文件:

去掉struts2的相关依赖:比如:struts2-core、struts2-convention-plugin、struts2-junit-plugin、struts2-json-plugin、struts2-spring-plugin等等包含struts2的依赖包

由于我的项目有appfuse-web的依赖,所以不需要添加spring-webmvc的依赖,如果你的项目没有SpringMVC相关依赖,请添加

2、修改web.xml文件:

(1)去掉Struts2的filter和filter-mapping以及删除struts.xml文件(建议备份)

        <filter>
		<filter-name>struts</filter-name>
		<filter-class>org.apache.struts2.dispatcher.ng.filter.StrutsPrepareAndExecuteFilter</filter-class>
		<init-param>
			<param-name>config</param-name>
			<!-- 配置装载struts.xml路径 -->
			<param-value>struts-default.xml,struts-plugin.xml,conf/struts/struts.xml</param-value>
		</init-param>
	</filter>
        <filter-mapping>
		<filter-name>struts</filter-name>
		<url-pattern>/*</url-pattern>
		<dispatcher>REQUEST</dispatcher>
		<dispatcher>FORWARD</dispatcher>
	</filter-mapping>

(2)加上springMVC的servlet和servlet-mapping(需要注意url-pattern的值,文章下面有其他说明)

<!-- Spring MVC 核心控制器 DispatcherServlet 配置 -->
	<servlet>
		<servlet-name>springMVC</servlet-name>
		<servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>
		<init-param>
			<param-name>contextConfigLocation</param-name>
			<param-value>classpath:conf/spring/spring-mvc.xml</param-value>
		</init-param>
		<load-on-startup>1</load-on-startup>
	</servlet>
        <servlet-mapping>
		<servlet-name>springMVC</servlet-name>
		<url-pattern>/</url-pattern>
	</servlet-mapping>

3、在classpath:conf/spring/下面添加spring-mvc.xml(Json转换器暂时使用Jackson,而MyExceptionMappingInterceptor是本项目的全局异常类,PrivilegeInterceptor是本项目的拦截器)

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:aop="http://www.springframework.org/schema/aop"
       xmlns:context="http://www.springframework.org/schema/context"
       xmlns:mvc="http://www.springframework.org/schema/mvc"
       xmlns:tx="http://www.springframework.org/schema/tx"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xmlns:p="http://www.springframework.org/schema/p"
       xsi:schemaLocation="http://www.springframework.org/schema/aop http://www.springframework.org/schema/aop/spring-aop.xsd
        http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd 
        http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context.xsd 
        http://www.springframework.org/schema/mvc http://www.springframework.org/schema/mvc/spring-mvc.xsd 
        http://www.springframework.org/schema/tx http://www.springframework.org/schema/tx/spring-tx.xsd">

    <!-- 扫描controller(controller层注入) -->
    <context:component-scan base-package="com.test.cn.**.action"/>

    <!-- 解决使用@ResponseBody乱码,Spring版本问题 -->
    <mvc:annotation-driven>
        <mvc:message-converters register-defaults="true">
            <bean class="org.springframework.http.converter.StringHttpMessageConverter">
                <property name="supportedMediaTypes" value = "text/plain;charset=UTF-8" />
            </bean>
        </mvc:message-converters>
    </mvc:annotation-driven>

    <!--避免IE执行AJAX时,返回JSON出现下载文件 -->
    <bean id="mappingJacksonHttpMessageConverter"
          class="org.springframework.http.converter.json.MappingJacksonHttpMessageConverter">
        <property name="supportedMediaTypes">
            <list>
                <value>text/html;charset=UTF-8</value>
            </list>
        </property>
    </bean>
    <!-- 启动SpringMVC的注解功能,完成请求和注解POJO的映射 -->
    <bean class="org.springframework.web.servlet.mvc.annotation.AnnotationMethodHandlerAdapter">
        <property name="messageConverters">
            <list>
                <ref bean="mappingJacksonHttpMessageConverter" /> <!-- JSON转换器 -->
            </list>
        </property>
    </bean>

    <!--对静态资源文件的访问-->
    <mvc:resources mapping="/lib/**" location="/lib/" />

    <!-- 默认的视图解析器 在上边的解析错误时使用 (默认使用html)- -->
    <bean id="defaultViewResolver" class="org.springframework.web.servlet.view.InternalResourceViewResolver">
        <property name="viewClass" value="org.springframework.web.servlet.view.JstlView"/>
        <property name="prefix" value="/WEB-INF/pages/"/>
        <property name="suffix" value=".jsp"/>
    </bean>

    <!-- 配置springMVC处理上传文件的信息 -->
    <bean id="multipartResolver" class="org.springframework.web.multipart.commons.CommonsMultipartResolver">
        <property name="defaultEncoding" value="utf-8"/>
        <property name="maxUploadSize" value="1073741824"/><!-- 最大上传1G -->
        <property name="maxInMemorySize" value="40960"/>
    </bean>

    <bean class="com.test.cn.core.web.interceptor.MyExceptionMappingInterceptor" />

     <!-- 配置拦截器 -->
    <mvc:interceptors>
        <mvc:interceptor>
            <mvc:mapping path="/**"/>
            <bean class="com.test.cn.core.web.interceptor.PrivilegeInterceptor" />
        </mvc:interceptor>
        <mvc:interceptor>
            <mvc:mapping path="/**"/>
            <bean class="com.test.cn.core.web.interceptor.LogInterceptor" />
        </mvc:interceptor>
    </mvc:interceptors>
</beans>

4、由于Controller注解在上面的xml已扫描,所以在applicationContext-service.xml去掉Controller注解的扫描(这个xml文件原有事务管理器与aop切面等配置,根据自己项目寻找自己的xml文件)【注:至于Controller注解扫描为什么与其他注解分开,这个问题请自行去了解一下SpringMVC的启动原理】

修改前:

<!--开启包注解扫描-->
	<context:component-scan base-package="com.test">
		
	</context:component-scan>

修改后:

<!--开启包注解扫描-->
	<context:component-scan base-package="com.test">
	 	<!--结合springmvc -->
		<context:exclude-filter type="annotation" expression="org.springframework.stereotype.Controller"/>
	</context:component-scan>

5、拦截器调整

(1)调整全局异常拦截器MyExceptionMappingInterceptor:由继承Struts2的ExceptionMappingInterceptor换成继承SpringMVC的ExceptionHandlerExceptionResolver,由于继承类不同,方法也不同,返回类型不同,稍微理解一下进行了更改如下:

public class MyExceptionMappingInterceptor extends ExceptionMappingInterceptor {

	private static final long serialVersionUID = -1L;

	private static final Logger logger = LoggerFactory.getLogger(MyExceptionMappingInterceptor.class);

	@Override
	public String intercept(ActionInvocation invocation) throws Exception {
		String result = null;

		try {
			result = invocation.invoke();
		} catch (GeneralException ex) {
			result = handleException(ex, invocation);
		} catch (UnauthorizedAccessException ex) {
			result = handleException(ex, invocation);
		} catch (BusinessException ex) {
			result = handleException(ex, invocation);
		} catch (IncorrectResultSizeDataAccessException ex) {
			result = handleException(new BusinessException(ex, "数据库查询唯一记录时返回了2条及以上记录!"), invocation);
		} catch (DataAccessException ex) {
			result = handleException(new BusinessException(ex, "数据库操作失败!"), invocation);
		} catch (NullPointerException ex) {
			result = handleException(new BusinessException(ex, "调用了未经初始化的对象或者是不存在的对象!"), invocation);
		} catch (IOException ex) {
			result = handleException(new BusinessException(ex, "IO异常!"), invocation);
		} catch (ClassNotFoundException ex) {
			result = handleException(new BusinessException(ex, "指定的类不存在!"), invocation);
		} catch (ArithmeticException ex) {
			result = handleException(new BusinessException(ex, "数学运算异常!"), invocation);
		} catch (ArrayIndexOutOfBoundsException ex) {
			result = handleException(new BusinessException(ex, "数组下标越界!"), invocation);
		} catch (IllegalArgumentException ex) {
			result = handleException(new BusinessException(ex, "方法的参数错误!"), invocation);
		} catch (ClassCastException ex) {
			result = handleException(new BusinessException(ex, "类型强制转换错误!"), invocation);
		} catch (SecurityException ex) {
			result = handleException(new BusinessException(ex, "违背安全原则异常!"), invocation);
		} catch (SQLException ex) {
			result = handleException(new BusinessException(ex, "SQL语句出错!"), invocation);
//		} catch (QuerySyntaxException ex) {
//			result = handleException(new BusinessException(ex, "HQL语句出错!"), invocation);
		} catch (NoSuchMethodError ex) {
			result = handleException(new BusinessException(ex, "请求的方法末找到!"), invocation);
		} catch (NoSuchMethodException ex) {
			result = handleException(new BusinessException(ex, "请求的方法末找到!"), invocation);
		} catch (InternalError ex) {
			result = handleException(new BusinessException(ex, "Java虚拟机发生了内部错误!"), invocation);
		} catch (Exception ex) {
			result = handleException(new BusinessException(ex, "程序内部错误,操作失败!"), invocation);
		}

		return result;
	}


	/**
	 * 处理异常并路由到需要的页面
	 * 与struts.xml中的global-exception-mappings配置匹配
	 * @param e
	 * 		  抛出的异常
	 * @param invocation
	 * 		  上下文环境
     * @return
     */
	private String handleException(RuntimeException e, ActionInvocation invocation) {

		ActionContext ctx = invocation.getInvocationContext();
		HttpServletRequest request = (HttpServletRequest)ctx.get(ServletActionContext.HTTP_REQUEST);
		HttpServletResponse response = (HttpServletResponse)ctx.get(ServletActionContext.HTTP_RESPONSE);

		// 记录错误日志
		writeErrorLog(e, invocation);

		// 统一处理下载请求的异常
		String contentType = response.getContentType();
		if(StringUtils.equalsIgnoreCase(contentType, "application/x-download")){
			response.setCharacterEncoding("utf-8");
			PrintWriter out = null;
			try {
				out = response.getWriter();
			} catch (IOException e1) {
				logger.error(e1.getMessage(), e1);
			}
			out.print("<script language='javascript'>alert('" + e.getMessage() + "');</script>");
			out.close();
		}

		// ajax请求异常,则返回友好提示
		String type = request.getHeader("X-Requested-With");
		if ("XMLHttpRequest".equalsIgnoreCase(type)) {
			ActionResult actionResult = new ActionResult(true);
			actionResult.setErrorMsg(e.getMessage(), e);
			AjaxUtil.renderJSON(response, JsonUtil.bean2json(actionResult));
			return null;
		}else{
			handleLogging(e);
			List<ExceptionMappingConfig> exceptionMappings = invocation.getProxy().getConfig().getExceptionMappings();
			// 根据异常的类型查找对应的路由
			ExceptionMappingConfig mappedResult = this.findMappingFromExceptions(exceptionMappings, e);
			if (mappedResult != null) {
				String result = mappedResult.getResult();
				publishException(invocation, new ExceptionHolder(e));
				return result;
			} else {
				throw e;
			}
		}
	}

	/**
	 * 记录错误日志
	 * @param e
	 * @param invocation
     */
	private void writeErrorLog(RuntimeException e, ActionInvocation invocation) {
		try {
			ActionProxy proxy = invocation.getProxy();
			String module = StringUtils.replace(proxy.getNamespace(), "/", "");
			String subModule = proxy.getActionName();
			// 请求参数
			Map<String, Object> map = invocation.getInvocationContext().getParameters();
			String message = "提交的参数:" + AppUtils.getRequestParameterStr(map);
			SysLoginUser loginUser = AppUtils.getSysLoginUser();
			String userId = loginUser != null ? loginUser.getSurId() : "";
			ErrorLogWriter.doLogging(message, e, module, subModule, userId);
		}catch (Exception ex){
			logger.error("写错误日志异常:" + e.getMessage(), ex);
		}
	}
}

修改后:

public class MyExceptionMappingInterceptor extends ExceptionHandlerExceptionResolver {

    private static final long serialVersionUID = -1L;

    private static final Logger logger = LoggerFactory.getLogger(MyExceptionMappingInterceptor.class);

    @Override
    public ModelAndView resolveException(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse,
                                         Object o, Exception e) {
        RuntimeException runtimeException = null;

        if (e instanceof GeneralException) {
            runtimeException = (RuntimeException) e;
        } else if (e instanceof UnauthorizedAccessException) {
            runtimeException = (RuntimeException) e;
        } else if (e instanceof BusinessException) {
            runtimeException = (RuntimeException) e;
        } else if (e instanceof IncorrectResultSizeDataAccessException) {
            runtimeException = new BusinessException(e, "数据库查询唯一记录时返回了2条及以上记录!");
        } else if (e instanceof DataAccessException) {
            runtimeException = new BusinessException(e, "数据库操作失败!");
        } else if (e instanceof NullPointerException) {
            runtimeException = new BusinessException(e, "调用了未经初始化的对象或者是不存在的对象!");
        } else if (e instanceof IOException) {
            runtimeException = new BusinessException(e, "IO异常!");
        } else if (e instanceof ClassNotFoundException) {
            runtimeException = new BusinessException(e, "指定的类不存在!");
        } else if (e instanceof ArithmeticException) {
            runtimeException = new BusinessException(e, "数学运算异常!");
        } else if (e instanceof ArrayIndexOutOfBoundsException) {
            runtimeException = new BusinessException(e, "数组下标越界!");
        } else if (e instanceof IllegalArgumentException) {
            runtimeException = new BusinessException(e, "方法的参数错误!");
        } else if (e instanceof ClassCastException) {
            runtimeException = new BusinessException(e, "类型强制转换错误!");
        } else if (e instanceof SecurityException) {
            runtimeException = new BusinessException(e, "违背安全原则异常!");
        } else if (e instanceof SQLException) {
            runtimeException = new BusinessException(e, "SQL语句出错!");
//		} else if (e instanceof NoSuchMethodError) {

        } else if (e instanceof NoSuchMethodException) {
            runtimeException = new BusinessException(e, "请求的方法末找到!");
//		} else if (e instanceof InternalError) {

        } else if (e instanceof InternalException) {
            runtimeException = new BusinessException(e, "Java虚拟机发生了内部错误!");
        } else if (e instanceof Exception) {
            runtimeException = new BusinessException(e, "程序内部错误,操作失败!");
        }
        return handleException(runtimeException, httpServletRequest, httpServletResponse, o);
    }


    /**
     * 处理异常并路由到需要的页面
     *
     * @param e                   抛出的异常
     * @param httpServletRequest
     * @param httpServletResponse @return
     * @param o
     */
    private ModelAndView handleException(RuntimeException e, HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, Object o) {
        // 记录错误日志
        writeErrorLog(e, httpServletRequest, httpServletResponse, o);

        // 统一处理下载请求的异常
        String contentType = httpServletResponse.getContentType();
        if (StringUtils.equalsIgnoreCase(contentType, "application/x-download")) {
            httpServletResponse.setCharacterEncoding("utf-8");
            PrintWriter out = null;
            try {
                out = httpServletResponse.getWriter();
            } catch (IOException e1) {
                logger.error(e1.getMessage(), e1);
            }
            out.print("<script language='javascript'>alert('" + e.getMessage() + "');</script>");
            out.close();
        }

        ModelAndView modelAndView = null;
        // ajax请求异常,则返回友好提示
        String type = httpServletRequest.getHeader("X-Requested-With");
        if ("XMLHttpRequest".equalsIgnoreCase(type)) {
            ActionResult actionResult = new ActionResult(true);
            actionResult.setErrorMsg(e.getMessage(), e);
            AjaxUtil.renderJSON(httpServletResponse, JsonUtil.bean2json(actionResult));
        } else {
            if (e instanceof GeneralException) {
                modelAndView = new ModelAndView("/error.jsp");
            } else if (e instanceof UnauthorizedAccessException) {
                modelAndView = new ModelAndView("/unauth.jsp");
            } else if (e instanceof BusinessException) {
                modelAndView = new ModelAndView("/error.jsp");
            }
        }

        return modelAndView;
    }

    /**
     * 记录错误日志
     *
     * @param e
     * @param httpServletRequest
     * @param httpServletResponse
     * @param o
     */
    private void writeErrorLog(RuntimeException e, HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, Object o) {
        try {

            Object handler = null;
            if (o instanceof HandlerMethod) {
                HandlerMethod handlerMethod = (HandlerMethod) o;
                handler = handlerMethod.getBean();
            }

//			String module = StringUtils.replace(proxy.getNamespace(), "/", "");
            StringBuffer url = httpServletRequest.getRequestURL();
            String module = url.delete(url.length() - httpServletRequest.getRequestURI().length(), url.length()).append("/").toString();
            String subModule = handler.getClass().getName();
            // 请求参数
            Map<String, Object> map = httpServletRequest.getParameterMap();
            String message = "提交的参数:" + AppUtils.getRequestParameterStr(map);
            SysLoginUser loginUser = AppUtils.getSysLoginUser();
            String userId = loginUser != null ? loginUser.getSurId() : "";
            ErrorLogWriter.doLogging(message, e, module, subModule, userId);
        } catch (Exception ex) {
            logger.error("写错误日志异常:" + e.getMessage(), ex);
        }
    }
}

(2)调整日志拦截器LogInterceptor:由继承AbstractInterceptor改为实现HandlerInterceptor接口

需要注意的是: AbstractInterceptor的intercept方法里面的result = invocation.invoke();这句代码为分割点,分割点前的代码放进HandlerInterceptor的preHandle方法里面,而分割点后的代码放进postHandle方法里面,至于try catch分割点的代码只能迁移到全局异常类里操作(此处省略掉),至于为什么要这样改,我也不多说了,请了解一下Struts2的拦截器和Spring的拦截器

修改前:

public class LogInterceptor extends ExceptionMappingInterceptor {

    private static final long serialVersionUID = -5260452584263751123L;

    private static final Logger logger = LoggerFactory.getLogger(LogInterceptor.class);

    private static final String ENTER = "->>>开始调用: ";

    private static final String EXIT = "<<<-结束调用: ";

    private final static String USER_NAME = "userName";

    private final static String THREADINFO = "ThreadInfo";

    private ThreadLocal<Integer> depth = new ThreadLocal<Integer>() {
        @Override
        protected Integer initialValue() {
            return 0;
        }
    };

    public String intercept(ActionInvocation invocation) throws Exception {

        long startTime = System.currentTimeMillis();

        Map<String, Object> session = invocation.getInvocationContext().getSession();
        if (session != null) {
            SysLoginUser loginUser = (SysLoginUser) session.get(Constants.SYSLOGINUSER_IN_SESSION);
            if (loginUser != null) {
                Thread current = Thread.currentThread();
                MDC.put(USER_NAME, loginUser.getSurName());
                MDC.put(THREADINFO, "ThreadID:" + current.getId());
            }
        }

        //构建StringBuffer
        ActionProxy proxy = invocation.getProxy();
        String method = proxy.getMethod();
        StringBuilder sb = new StringBuilder();
        int d = depth.get();
        for (int i = 0; i < d; i++) {
            sb.append(' ');
        }
        sb.append(ENTER);
        sb.append(proxy.getActionName());

        //可以打印请求参数(登陆请求不打印参数)
        Map<String, Object> map = invocation.getInvocationContext().getParameters();
        //参数非空则打印参数
        if (map.size() != 0) {
            sb.append('(');
            for (String key : map.keySet()) {
                if (!"sfUser.sfuNameSpelling".equals(key)) { // 不打印协同的token
                    sb.append('[');
                    sb.append(key);
                    sb.append('=');
                    sb.append(((String[]) map.get(key))[0].toString());
                    sb.append("]");
                }
            }
            sb.append(')');
        }

        //在方法调用之前打印方法名
        logger.info(sb.toString());
        depth.set(++d);

        String result = "";

        try {
            //继续调用拦截器
            result = invocation.invoke();
        } catch (Throwable t) {
            depth.set(--d);
            sb.replace(d, d + ENTER.length(), EXIT);
            logger.info(sb.toString());
            throw t;
        }

        depth.set(--d);
        //替换-> 为 <-
        sb.replace(d, d + ENTER.length(), EXIT);
        //在方法调用之后打印方法名
        logger.info(sb.toString());
        //在方法调用之后打印该请求的花费时间
        logger.info("[方法:" + proxy.getActionName() + "() 调用时间] = " + (System.currentTimeMillis() - startTime) + " 毫秒!");

        //清除用户名
        MDC.remove(USER_NAME);

        return result;
    }
}

修改后:

public class LogInterceptor implements HandlerInterceptor {

    private static final long serialVersionUID = -5260452584263751123L;

    private static final Logger logger = LoggerFactory.getLogger(LogInterceptor.class);

    private static final String ENTER = "->>>开始调用: ";

    private static final String EXIT = "<<<-结束调用: ";

    private final static String USER_NAME = "userName";

    private final static String THREADINFO = "ThreadInfo";

    private ThreadLocal<Long> startTime = new ThreadLocal<Long>() {
        @Override
        protected Long initialValue() {
            return System.currentTimeMillis();
        }
    };

    private ThreadLocal<Integer> depth = new ThreadLocal<Integer>() {
        @Override
        protected Integer initialValue() {
            return 0;
        }
    };

    private ThreadLocal<String> actionName = new ThreadLocal<String>();

    private ThreadLocal<StringBuilder> stringBuilder = new ThreadLocal<StringBuilder>(){
        @Override
        protected StringBuilder initialValue() {
            return new StringBuilder();
        }
    };


    @Override
    public boolean preHandle(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, Object o) throws Exception {

        if (o instanceof HandlerMethod) {
            HandlerMethod h = (HandlerMethod) o;
            actionName.set(h.getBean().getClass().getName());
        }

        HttpSession session = httpServletRequest.getSession();
        if (session != null) {
            SysLoginUser loginUser = (SysLoginUser) session.getAttribute(Constants.SYSLOGINUSER_IN_SESSION);
            if (loginUser != null) {
                Thread current = Thread.currentThread();
                MDC.put(USER_NAME, loginUser.getSurName());
                MDC.put(THREADINFO, "ThreadID:" + current.getId());
            }
        }

        //构建StringBuffer
        StringBuilder sb = stringBuilder.get();
        int d = depth.get();
        for (int i = 0; i < d; i++) {
            sb.append(' ');
        }
        sb.append(ENTER);
        sb.append(actionName.get());

        //可以打印请求参数(登陆请求不打印参数)
        Map<String, Object> map = httpServletRequest.getParameterMap();
        //参数非空则打印参数
        if (map.size() != 0) {
            sb.append('(');
            for (String key : map.keySet()) {
                if (!"sfUser.sfuNameSpelling".equals(key)) { // 不打印协同的token
                    sb.append('[');
                    sb.append(key);
                    sb.append('=');
                    sb.append(((String[]) map.get(key))[0].toString());
                    sb.append("]");
                }
            }
            sb.append(')');
        }

        //在方法调用之前打印方法名
        logger.info(sb.toString());
        depth.set(++d);
        stringBuilder.set(sb);

        return true;
    }

    @Override
    public void postHandle(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, Object o, ModelAndView modelAndView) throws Exception {
        int d = depth.get();
        depth.set(--d);

        StringBuilder sb = stringBuilder.get();
        //替换-> 为 <-
        sb.replace(d, d + ENTER.length(), EXIT);
        stringBuilder.set(sb);

        //在方法调用之后打印方法名
        logger.info(sb.toString());
        //在方法调用之后打印该请求的花费时间
        logger.info("[方法:" + actionName.get() + "() 调用时间] = " + (System.currentTimeMillis() - startTime.get()) + " 毫秒!");

        //清除用户名
        MDC.remove(USER_NAME);
    }

    @Override
    public void afterCompletion(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, Object o, Exception e) throws Exception {

    }
}

(3)同理调整权限拦截器PrivilegeInterceptor(这个代码比较简单,可不看):由继承AbstractInterceptor改为实现HandlerInterceptor接口

修改前:(只显示部分代码)

public class PrivilegeInterceptor extends AbstractInterceptor {

    private static final long serialVersionUID = 8887312034961459123L;

    private static final Logger logger = LoggerFactory.getLogger(PrivilegeInterceptor.class);
  

    @Override
    public String intercept(ActionInvocation invocation) throws Exception {
        String result = "";
        try {
            ActionContext ctx = invocation.getInvocationContext();
            HttpServletRequest request = (HttpServletRequest) ctx.get(ServletActionContext.HTTP_REQUEST);
            SysLoginUser loginUser = (SysLoginUser) request.getSession().getAttribute(Constants.SYSLOGINUSER_IN_SESSION);
            if (loginUser != null) {
                // 检查权限,没有权限就抛异常
                checkPrivilege(request, loginUser);
            }
            // 继续调用拦截器
            result = invocation.invoke();
        } catch (Throwable t) {
            throw t;
        }

        return result;
    }
}

修改后:

public class PrivilegeInterceptor implements HandlerInterceptor {

    private static final long serialVersionUID = 8887312034961459123L;

    private static final Logger logger = LoggerFactory.getLogger(PrivilegeInterceptor.class);


    @Override
    public boolean preHandle(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, Object o) throws Exception {
        try {
            SysLoginUser loginUser = (SysLoginUser) httpServletRequest.getSession().getAttribute(Constants.SYSLOGINUSER_IN_SESSION);
            if (loginUser != null) {
                // 检查权限,没有权限就抛异常
                checkPrivilege(httpServletRequest, loginUser);
            }

        } catch (Throwable t) {
            throw t;
        }

        return true;
    }

    @Override
    public void postHandle(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, Object o, ModelAndView modelAndView) throws Exception {

    }

    @Override
    public void afterCompletion(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, Object o, Exception e) throws Exception {

    }
}

6、由于本项目的过滤器都是使用javax.servlet.Filter,所以不需要更改filter代码

7、全局搜索ServletActionContext(这是Struts的上下文)

本项目把ServletActionContext.getResponse()替换成

((ServletWebRequest) RequestContextHolder.getRequestAttributes()).getResponse()

把ServletActionContext.getRequest()替换成

((ServletRequestAttributes) RequestContextHolder.currentRequestAttributes()).getRequest()

8、全局搜索sessionMap.get替换成sessionMap.getAttribute

9、把Struts2接收附件的MultiPartRequestWrapper替换成Spring的MultipartHttpServletRequest,替换后使用的语法不同,此处不粘贴详细代码了(上传附件建议在controller的方法里面加上参数@RequestParam("upfile") MultipartFile[] files来接收文件,本项目用ajaxfileupload.js插件上传,框架换了后这个js也有小调整)

10、页面jsp调整:去掉<%@ taglib prefix="s" uri="/struts-tags"%>以及调整s开头的标签如下:

(1)<s:form替换成<form(</s:form>同理)

(2)<s:hidden name="aaa"/> 替换成<input type="hidden" name="aaa" value="${aaa}"/>

(3)<s:textarea替换成<textarea(</s:textarea>同理)

(4)<s:property value="aaa"/>替换成${aaa}

(5)<s:property value="#session.aaa"/>替换成${sessionScope.aaa}

(6)cssStyle替换成style

11、那么重点来了,最痛苦的就是所有的url请求地址的更换,对于一般的小项目来说直接替换url,但是本项目的代码太多了,于是,找到了一个很好的办法,便是使用了urlrewrite实现url重写,把.do为后缀的url进行重写,这样就不用更换所有url了

(1)在web.xml添加filter和filter-mapping,注意filter的顺序,在该filter之前的filter处理的url还是有.do的,之后的filter则是被转换后的url

        <filter>
		<filter-name>UrlRewriteFilter</filter-name>
		<filter-class>org.tuckey.web.filters.urlrewrite.UrlRewriteFilter</filter-class>
		<init-param>
			<param-name>logLevel</param-name>
			<param-value>WARN</param-value>
		</init-param>
	</filter>
        <filter-mapping>
		<filter-name>UrlRewriteFilter</filter-name>
		<url-pattern>*.do</url-pattern>
		<dispatcher>REQUEST</dispatcher>
		<dispatcher>FORWARD</dispatcher>
	</filter-mapping>

(2)在WEB-INF下添加urlrewrite.xml(请根据自身项目调整from和to的匹配)

<?xml version="1.0" encoding="utf-8"?>
<!DOCTYPE urlrewrite PUBLIC "-//tuckey.org//DTD UrlRewrite 3.2//EN" "\\urlrewrite3.2.dtd">

<urlrewrite>
    <rule>
        <note>forward(转发模式)传参</note>
        <note>example:/a/b_c.do->/a/b/c</note>
        <from>/([A-Za-z0-9]+)/([A-Za-z0-9]+)_([A-Za-z0-9]+).do</from>
        <to type="forward">/$1/$2/$3</to>
    </rule>
</urlrewrite>

12、url调整好了,就准备开始改另一个重点,Controller层(或Action层)的代码调整

为了方便代码的更改,我自己写了一个工具类,获取获取controller类以及方法的RequestMapping注解的值

public class BaseActionUtil {

    /**
     * 获取controller的RequestMapping注解的值
     *
     * @return
     */
    public static String getActionRequestMapping(Class clazz) {
        String result = "";
        RequestMapping requestMapping = (RequestMapping) clazz.getAnnotation(RequestMapping.class);
        if (requestMapping != null) {
            String[] requestMappingValue = requestMapping.value();
            if (requestMappingValue.length > 0) {
                result = requestMappingValue[0];
            }
        }
        return result;
    }

    /**
     * 获取controller下的方法的RequestMapping注解的值
     *
     * @return
     */
    public static Map<String, String> getMethodRequestMapping(Class clazz) {
        Map<String, String> methodRequestMappingValue = new HashMap<String, String>();
        Method[] methods = clazz.getMethods();
        for (Method method : methods) {
            RequestMapping requestMapping = method.getAnnotation(RequestMapping.class);
            if (requestMapping != null) {
                String[] requestMappingValue = requestMapping.value();
                if (requestMappingValue.length > 0) {
                    methodRequestMappingValue.put(method.getName(), requestMappingValue[0]);
                }
            }
        }
        return methodRequestMappingValue;
    }
}

本项目Controller层(或Action层)的代码调整步骤:

(1)类的调整
a、去掉@Scope注解
b、加上@RequestMapping注解(值:根据请求url来写)
c、去掉BaseAction继承(本项目有抽取过父类,所以不需要继承了)
d、加上2个私有变量:
     private String actionRequestMappingValue;
     private Map<String, String> methodRequestMappingValue;
e、加上初始化方法(注意修改:aaa.class为当前类)
    @PostConstruct
    public void init() {
        this.actionRequestMappingValue = BaseActionUtil.getActionRequestMapping(aaa.class);
        this.methodRequestMappingValue = BaseActionUtil.getMethodRequestMapping(aaa.class);
    }

(2)方法调整:
a、普通方法(返回jsp路径的,非ajax)
   (a)加上@RequestMapping注解(值:根据请求url来写, 这里一般都是跟方法名一致,除了strust.xml中的某些特殊url])
   (b)返回类型都是String
  (c)除了strust.xml特殊url,返回路径都是return actionRequestMappingValue + methodRequestMappingValue.get("aaa");(注意修改:aaa为当前方法的名),因此这里就可以直接返回controller类RequestMapping注解的值+方法的RequestMapping注解的值,刚刚好就是jsp在项目中的路径。
 
b、ajax方法
    (a)加上注解 @RequestMapping、@ResponseBody
    (b)返回类型基本为ActionResult(本项目统一结果返回的类),特殊例外

(3)注意事项:
    (a)action里面的方法如果存在有不需要加注解的方法,都应该改为私有方法
    (b)若存在action的方法调用action里另一个方法,则确认2个方法是否都有url请求相对应,若都有,则把调用方法的代码改为return转发的url
    (c)若action有局部变量,由于Struts2是多例,Spring是单例,所有局部变量都得加到方法里面获取

13、Json转换器更换:由于本项目原来的Json转换器是用Gson进行转换而不是Jackson来转换json,两者在转换json的时候有差异(比如boolean类型转换的时候一个有is前缀一个则没有),因此,本项目则写一个新的Gson转换器来替换Jackson转换器

(1)把spring-mvc.xml调整为:

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:aop="http://www.springframework.org/schema/aop"
       xmlns:context="http://www.springframework.org/schema/context"
       xmlns:mvc="http://www.springframework.org/schema/mvc"
       xmlns:tx="http://www.springframework.org/schema/tx"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xmlns:p="http://www.springframework.org/schema/p"
       xsi:schemaLocation="http://www.springframework.org/schema/aop http://www.springframework.org/schema/aop/spring-aop.xsd
        http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd 
        http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context.xsd 
        http://www.springframework.org/schema/mvc http://www.springframework.org/schema/mvc/spring-mvc.xsd 
        http://www.springframework.org/schema/tx http://www.springframework.org/schema/tx/spring-tx.xsd">

    <!-- 扫描controller(controller层注入) -->
    <context:component-scan base-package="com.test.cn.**.action"/>

   <mvc:annotation-driven>
        <mvc:message-converters>
            <bean class="com.test.cn.core.web.converter.GsonHttpMessageConverter"/>
        </mvc:message-converters>
    </mvc:annotation-driven>

    <!--对静态资源文件的访问-->
    <mvc:resources mapping="/lib/**" location="/lib/" />

    <!-- 默认的视图解析器 在上边的解析错误时使用 (默认使用html)- -->
    <bean id="defaultViewResolver" class="org.springframework.web.servlet.view.InternalResourceViewResolver">
        <property name="viewClass" value="org.springframework.web.servlet.view.JstlView"/>
        <property name="prefix" value="/WEB-INF/pages/"/>
        <property name="suffix" value=".jsp"/>
    </bean>

    <!-- 配置springMVC处理上传文件的信息 -->
    <bean id="multipartResolver" class="org.springframework.web.multipart.commons.CommonsMultipartResolver">
        <property name="defaultEncoding" value="utf-8"/>
        <property name="maxUploadSize" value="1073741824"/><!-- 最大上传1G -->
        <property name="maxInMemorySize" value="40960"/>
    </bean>

    <bean class="com.test.cn.core.web.interceptor.MyExceptionMappingInterceptor" />

     <!-- 配置拦截器 -->
    <mvc:interceptors>
        <mvc:interceptor>
            <mvc:mapping path="/**"/>
            <bean class="com.test.cn.core.web.interceptor.PrivilegeInterceptor" />
        </mvc:interceptor>
        <mvc:interceptor>
            <mvc:mapping path="/**"/>
            <bean class="com.test.cn.core.web.interceptor.LogInterceptor" />
        </mvc:interceptor>
    </mvc:interceptors>
</beans>

(2)新建json转化器:GsonHttpMessageConverter 

public class GsonHttpMessageConverter extends AbstractHttpMessageConverter<Object> {
    public static final Charset DEFAULT_CHARSET = Charset.forName("UTF-8");
    private Gson gson = new GsonBuilder().setDateFormat("yyyy-MM-dd HH:mm:ss").disableHtmlEscaping().
            registerTypeAdapter(Double.class, new JsonSerializer<Double>() {
                @Override
                public JsonElement serialize(Double src, Type typeOfSrc, JsonSerializationContext context) {
                    if (src == src.longValue())
                        return new JsonPrimitive(src.longValue());
                    return new JsonPrimitive(src);
                }
            }).create();  //serializeNulls(). 不对空值序列化;
    private String jsonPrefix;

    public GsonHttpMessageConverter() {
        super(new MediaType[]{MediaType.APPLICATION_JSON, new MediaType("application", "*+json")});
    }

    public void setGson(Gson gson) {
        Assert.notNull(gson, "‘gson‘ is required");
        this.gson = gson;
    }

    public Gson getGson() {
        return this.gson;
    }

    public void setJsonPrefix(String jsonPrefix) {
        this.jsonPrefix = jsonPrefix;
    }

    public void setPrefixJson(boolean prefixJson) {
        this.jsonPrefix = prefixJson?")]}‘, ":null;
    }

    public boolean canRead(Class<?> clazz, MediaType mediaType) {
        return this.canRead(mediaType);
    }

    public boolean canWrite(Class<?> clazz, MediaType mediaType) {
        return this.canWrite(mediaType);
    }

    protected boolean supports(Class<?> clazz) {
        throw new UnsupportedOperationException();
    }

    protected Object readInternal(Class<?> clazz, HttpInputMessage inputMessage) throws IOException, HttpMessageNotReadableException {
        TypeToken<?> token = this.getTypeToken(clazz);
        return this.readTypeToken(token, inputMessage);
    }

    /**
     * Abstract template method that writes the actual body. Invoked from {@link #write}.
     *
     * @param o             the object to write to the output message
     * @param outputMessage the message to write to
     * @throws IOException                     in case of I/O errors
     * @throws HttpMessageNotWritableException in case of conversion errors
     */
    @Override
    protected void writeInternal(Object o, HttpOutputMessage outputMessage) throws IOException, HttpMessageNotWritableException {
        Charset charset = this.getCharset(outputMessage.getHeaders());
        OutputStreamWriter writer = new OutputStreamWriter(outputMessage.getBody(), charset);

        try {
            if(this.jsonPrefix != null) {
                writer.append(this.jsonPrefix);
            }

            TypeToken<?> token = this.getTypeToken(o.getClass());
            Type type = token.getType();

            if(type != null) {
                this.gson.toJson(o, type, writer);
            } else {
                this.gson.toJson(o, writer);
            }

            writer.close();
        } catch (JsonIOException var7) {
            throw new HttpMessageNotWritableException("Could not write JSON: " + var7.getMessage(), var7);
        }
    }

    public Object read(Type type, Class<?> contextClass, HttpInputMessage inputMessage) throws IOException, HttpMessageNotReadableException {
        TypeToken<?> token = this.getTypeToken(type);
        return this.readTypeToken(token, inputMessage);
    }

    protected TypeToken<?> getTypeToken(Type type) {
        return TypeToken.get(type);
    }

    private Object readTypeToken(TypeToken<?> token, HttpInputMessage inputMessage) throws IOException {
        InputStreamReader json = new InputStreamReader(inputMessage.getBody(), this.getCharset(inputMessage.getHeaders()));

        try {
            return this.gson.fromJson(json, token.getType());
        } catch (JsonParseException var5) {
            throw new HttpMessageNotReadableException("Could not read JSON: " + var5.getMessage(), var5);
        }
    }

    private Charset getCharset(HttpHeaders headers) {
        return headers != null && headers.getContentType() != null && headers.getContentType().getCharSet() != null?headers.getContentType().getCharSet():DEFAULT_CHARSET;
    }
}

三、总结

更换框架是一个很大的工作量,如果项目的代码量越大,要改的代码则会更多,本项目花了5天把除了Controller层的代码外的都换好了,然后再花费2天分配给2个人,根据我写的规则,来更改Controller层和jsp页面的代码(注意:一定要选比较认真严格的年轻小伙子,因为一不小心就会眼花改乱了,本人比较恐怖,完美主义者之一,相信世界上还有很多比我恐怖的coder,感谢大家阅读)

 类似资料: