前言
最近懒了很多也忙了很多,好多东西没办法分享到blog,因为知识点比较杂,没有时间整理。
写这篇文章主要原因是,因为遇到了同样的问题,但是网上没有很好的解决方案于是自己解决后,分享给大家
写这篇文章主要原因是,因为遇到了同样的问题,但是网上没有很好的解决方案于是自己解决后,分享给大家
源码在csdn download
文章尾部可以下载
IOC迭代版
SpringMVC IOC DI接口版本管理(迭代版)
http://blog.csdn.net/crazyzxljing0621/article/details/76677865
文章尾部可以下载
IOC迭代版
SpringMVC IOC DI接口版本管理(迭代版)
http://blog.csdn.net/crazyzxljing0621/article/details/76677865
(2017-8-4更新 )
概述
1.springMVC 多版本接口
2.接口有IP访问控制
3.支持jsonp
4.log发送到email
5.springMVC+mybatis
6.ANT打包通过SSH发布到linux
7.springMVC @ResponseBody乱码问题
准备工作
Java8
安装java8环境
java version "1.8.0_91"
Java(TM) SE Runtime Environment (build 1.8.0_91-b15)
Java HotSpot(TM) 64-Bit Server VM (build 25.91-b15, mixed mode)
Tomcat 7.x +
安装tomcat7.x或tomcat8 总之要支持java8
Ant
下载ant,配置ant环境变量。
设置ANT_HOME,然后path追加 %ANT_HOME%/bin;
Apache Ant(TM) version 1.10.1 compiled on February 2 2017
其他
Centos 64bit
有安装ant插件的eclipse
mysql
开始
多版本接口到底有何问题?
我们想提供多版本接口正常来看,获取一个version参数,通过判断不同参数,指向不同的函数或class
if(version == 1)
{
Controller.save();
}
else if(version == 2)
{
Controller.save(name)
}
高耦合不易修改,因为你要为你每个版本都提供一套if else...
按照之前网上一些文章所说我们可以在@RequestMapping中配置
@RequestMapping("/v1/xxxx")
public String save1()
@RequestMapping("/v2/xxxx")
public String save2()
来看看我的思路
@requestMapping ("/{domain}/{version}")
class CoreController{
@requestMapping ("/**")
public Object execute(@PathVariable domain,@PathVariable String version)
{
return null;
}
}
通过domain和version我们告诉抽象工厂去调用谁的 Controller
/**
* 获取class
*
* @param version
* @param domain
* @return
* @throws Exception
*/
private Class<?> selectClass(String version, String domain) throws Exception {
switch (domain) {
case ROOM:
return ControllerAbstractFactoryImpl.instance(version).iRoomController().Class();
case USER:
return ControllerAbstractFactoryImpl.instance(version).iUserController().Class();
default:
return null;
}
}
package com.api.controller.factory;
import com.api.controller.factory.inf.IController;
/**
* controller层抽象工厂
* @author Allen 2017年5月24日
*
*/
public interface IControllerAbstractFactory {
IController iController() throws Exception;
}
通过反射来拼classPath并得到他的实例返回给使用者
package com.api.controller.factory;
import com.api.controller.factory.inf.IController;
import com.api.modules.Conf;
import com.api.util.exception.ApiVersionException;
/**
* controller层抽象工厂
*
* @author Allen 2017年5月24日
*
*/
public class ControllerAbstractFactoryImpl implements IControllerAbstractFactory {
/** class Constant **/
final StringBuilder CONTROLLER_PATH = new StringBuilder("com.api.controller.factory.inf.Controller");
private String version;
private ControllerAbstractFactoryImpl(String version) {
this.version = version;
}
private ControllerAbstractFactoryImpl() {
// TODO Auto-generated constructor stub
}
public static IController instance(String version) throws Exception {
if (!version.matches(Conf.PATTERN_COMPLIE_VERSION))
throw new ApiVersionException();
else
return new ControllerAbstractFactoryImpl(version).iController();
}
@Override
public IController iController() throws Exception {
return (IController) Class.forName(CONTROLLER_PATH.append(version).toString()).newInstance();
}
}
在CoreController中得到了对应版本和domain的Controller后,执行函数调用
这里通过反射来遍历目标class中所有的methods以及父级methods,寻找与我们 自定义的注释@requestAlias匹配的method并调用他,当然也是要判断我们自定义的注释
@RequestStrategyUtils,确保访问IP时策略内
@RequestStrategyUtils,确保访问IP时策略内
/**
* 执行目标函数
*
* @param clazz
* @param method
* @return
* @throws NoSuchMethodException
* @throws InstantiationException
* @throws SecurityException
* @throws InvocationTargetException
* @throws IllegalArgumentException
* @throws IllegalAccessException
*/
private int targetMethod(Class<?> clazz, HttpServletRequest req)
throws NoSuchMethodException, IllegalAccessException, IllegalArgumentException, InvocationTargetException,
SecurityException, InstantiationException {
Method[] methods = clazz.getMethods();
// 找到目标method并配置参数类型class
for (Method m : methods) {
// 通过注解判断是否匹配访问domain
if (m.isAnnotationPresent(RequestAlias.class)
&& m.getAnnotation(RequestAlias.class).value().equals(methodName)) {
if (!ipControl(getRemoteHost(req), clazz, m)) {
// ip不匹配访问权限
return -1;
}
result = clazz.getMethod(m.getName(), m.getParameterTypes()).invoke(clazz.newInstance(), obParam);
return 0;
}
}
return -2;
}
/**
* IP访问控制
*
* @param request
* @param response
* @param handler
* @return
*/
private boolean ipControl(String requestIp, Class<?> clazz, Method m) {
if (clazz != null && m != null) {
boolean isClazz = clazz.isAnnotationPresent(RequestStrategyUtils.class);
boolean isMethod = m.isAnnotationPresent(RequestStrategyUtils.class);
RequestStrategyUtils rc = null;
// 如果方法和类声明中同时存在这个注解,那么方法中的会覆盖类中的设定。
rc = isMethod ? m.getAnnotation(RequestStrategyUtils.class)
: isClazz ? clazz.getAnnotation(RequestStrategyUtils.class) : null;
if (rc == null)
return false;
String[] value = rc.ip();
// 包含则true
return Arrays.asList(value).stream().anyMatch(s -> s.equals(Conf.All_IP)
|| ((s.equals(Conf.LOCAL_IP) || (s.equals(Conf.DEVELOP_IP))) && s.indexOf(requestIp) != -1));
}
return false;
}
这里是我们的UserController1_1版本,他的父级是1_0
import com.api.modules.Conf;
import com.api.service.factory.ServiceAbstractFactoryImpl;
import com.api.util.annotations.RequestAlias;
import com.api.util.annotations.RequestStrategyUtils;
/**
* 用户
*
* @author Allen 2017年5月25日
*
*/
@RequestStrategyUtils(ip = { Conf.LOCAL_IP, Conf.DEVELOP_IP })
public class UserController1_1 extends UserController1_0 {
@RequestAlias("save")
public Object save(String version, String name, String alias) throws Exception {
// TODO Auto-generated method stub
return ServiceAbstractFactoryImpl.instance(version).iUser().save(name, alias);
}
}
通过以上步骤锁定目标Controller并进行调用成功
ResultValue是我们封装的针对返回值组装为Vo并利用spring jackson,自动返回为json格式
if (flag == -1) {// IP访问权限不足
return ResultValue.execute(State.IPPERMISSIONS, callback);
} else if (flag == -2) {// 找不到的访问域
return ResultValue.execute(State.DOMAINNOTFOUND, callback);
} else if (result == null) { // 返回值为null,controller业务参数判定有无
return ResultValue.execute(State.NPE, callback);
} else { // success
return ResultValue.execute(State.SUCCESS, result, callback);
}
result JSON:
{ state: "100", stateValue: "成功", value: [ { id: 1, name: "若风" }, { id: 2, name: "Miss&7号" }, { id: 3, name: "55开" } ] }
至此Controller多版本问题解决,并解耦。
下面来看我们如果要增加一个新的版本就简单很多
1.新建UserController1_2
2.UserController1_2 extends UserController1_1
3.UserController1_2 编写新特性或重写老版本函数
4.然后在调用的时候 url version中传入1_2就可以了
5.无需修改任何IF ELSE
再看一下如何实现的多版本接口过程
1.用户发起请求 https://127.0.0.1/MyApi/room/1_1/save/李毅/国足大帝
2.CoreController是唯一添加了 @Controller的Controller类,所以他得到了请求
3.解析得到domain为room,version为1_1 ,请求method alias save ,传入参数 李毅,国足大帝
4.通过domain 选择不同的 具体Controller
switch (domain) {
case ROOM:
return ControllerAbstractFactoryImpl.instance(version).iRoomController().Class();
case USER:
return ControllerAbstractFactoryImpl.instance(version).iUserController().Class();
}
5.通过vesion在抽象工厂中通过反射得到目标版本的 Controller
6.回到CoreController。
7.拿着version版的Controller,去反射查alias匹配的method
8.查到method后再匹配ip访问权限
9.然后入参,调用
因为Controller提供的是访问控制,代码具体体现在参数和访问上,然而我们在具体的业务中,新版本接口可能差异从Dao就开始不同了。
我的做法是Service也做了抽象工厂的版本,当然Dao层我没有做,我认为没有必要,因为耦合永远都是存在的,无论是什么设计模式还是架构,只不过是把耦合从表面上放到了桌子下面,让更多的人无需了解桌子下面的细节罢了。
UML
其他
MySql
创建一个库myapi
CREATE TABLE `room` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`name` varchar(25) COLLATE utf8_bin NOT NULL,
PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=4 DEFAULT CHARSET=utf8 COLLATE=utf8_bin;
CREATE TABLE `user` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`name` varchar(25) COLLATE utf8_bin NOT NULL,
`alias` varchar(25) COLLATE utf8_bin DEFAULT NULL,
PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=9 DEFAULT CHARSET=utf8 COLLATE=utf8_bin;
insert into `room`(`id`,`name`) values (1,'若风'),(2,'Miss&7号'),(3,'55开');
乱码问题
按照我下面的web和springmvc的xml来配就不会有问题
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"
xsi:schemaLocation="http://java.sun.com/xml/ns/javaee http://java.sun.com/xml/ns/javaee/web-app_3_0.xsd"
id="WebApp_ID" version="3.0">
<display-name>liveApi</display-name>
<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>
<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>
<servlet>
<servlet-name>springDispatcherServlet</servlet-name>
<servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>
<!-- 配置Spring mvc下的配置文件的位置和名称 -->
<init-param>
<param-name>contextConfigLocation</param-name>
<param-value>/WEB-INF/springmvc.xml</param-value>
</init-param>
<load-on-startup>1</load-on-startup>
</servlet>
<!-- 防止Spring内存溢出监听器 -->
<listener>
<listener-class>org.springframework.web.util.IntrospectorCleanupListener</listener-class>
</listener>
<servlet-mapping>
<servlet-name>springDispatcherServlet</servlet-name>
<url-pattern>/</url-pattern>
</servlet-mapping>
<!-- 静态资源访问 -->
<servlet-mapping>
<servlet-name>default</servlet-name>
<url-pattern>*.css</url-pattern>
</servlet-mapping>
<servlet-mapping>
<servlet-name>default</servlet-name>
<url-pattern>*.gif</url-pattern>
</servlet-mapping>
<servlet-mapping>
<servlet-name>default</servlet-name>
<url-pattern>*.jpg</url-pattern>
</servlet-mapping>
<servlet-mapping>
<servlet-name>default</servlet-name>
<url-pattern>*.js</url-pattern>
</servlet-mapping>
<servlet-mapping>
<servlet-name>default</servlet-name>
<url-pattern>*.html</url-pattern>
</servlet-mapping>
<!-- 异常页面捕获 -->
<error-page>
<exception-type>java.lang.Throwable</exception-type>
<location>/500.html</location>
</error-page>
<error-page>
<error-code>500</error-code>
<location>/500.html</location>
</error-page>
<error-page>
<error-code>404</error-code>
<location>/404.html</location>
</error-page>
</web-app>
springMVC.xml
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:context="http://www.springframework.org/schema/context"
xmlns:mvc="http://www.springframework.org/schema/mvc" xmlns:tx="http://www.springframework.org/schema/tx"
xmlns:task="http://www.springframework.org/schema/task"
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-4.0.xsd
http://www.springframework.org/schema/mvc http://www.springframework.org/schema/mvc/spring-mvc-4.0.xsd
http://www.springframework.org/schema/tx
http://www.springframework.org/schema/tx/spring-tx-4.0.xsd
http://www.springframework.org/schema/task http://www.springframework.org/schema/task/spring-task-3.0.xsd">
<!-- ====================================================== -->
<!-- 配置@ResponseBody 保证返回值为UTF-8 -->
<!-- 因为StringHttpMessageConverter默认是ISO8859-1 -->
<!-- 用于使用@ResponseBody后返回中文避免乱码 -->
<bean id="utf8Charset" class="java.nio.charset.Charset"
factory-method="forName">
<constructor-arg value="UTF-8" />
</bean>
<mvc:annotation-driven>
<mvc:message-converters>
<bean class="org.springframework.http.converter.StringHttpMessageConverter">
<constructor-arg ref="utf8Charset" />
</bean>
</mvc:message-converters>
</mvc:annotation-driven>
<!-- 启动SpringMVC的注解功能,完成请求和注解POJO的映射 -->
<bean id="mappingJacksonHttpMessageConverter"
class="org.springframework.http.converter.json.MappingJackson2HttpMessageConverter">
<property name="supportedMediaTypes">
<list>
<value>text/html;charset=UTF-8</value>
</list>
</property>
</bean>
<bean
class="org.springframework.web.servlet.mvc.annotation.AnnotationMethodHandlerAdapter">
<property name="messageConverters">
<list>
<ref bean="mappingJacksonHttpMessageConverter" /> <!-- JSON转换器 -->
</list>
</property>
</bean>
<!-- ====================================================== -->
<!-- 配置自动扫描的包 -->
<context:component-scan base-package="com"></context:component-scan>
<!-- ====================================================== -->
</beans>
配置conf.properties
properties调用很麻烦我们在一个conf.java中与之进行匹配和初始化
#=========================
#======== Mybatis ========
#=========================
url=jdbc:mysql://你的ip:3306/myapi?useUnicode=true&characterEncoding=utf-8&autoReconnect=true
driver=com.mysql.jdbc.Driver
username=root
password=123456
#=========================
#======== 通用 ========
#=========================
#时间格式化标准(在不了解调用细节的情况下不建议修改)
date_format=yyyy-MM-dd HH:mm:ss
pattern_complie_version=^[0-9]\\d*_?[0-9]*$
#=================================
#======== 邮件 && 日志 ========
#=================================
#邮件标题(local)
local_title=我是一个标题
#邮件标题(release)
release_title=我是发布后的标题
#邮件头
mail_head_title=【异常】直播服务器
#错误日志收件人
notice_mail=123@qq.com
#发件箱账户
from_account=123@163.com;
#发件箱
from_password=123
#发件箱smtp
smtp_host=smtp.163.com
package com.api.modules;
import java.util.Properties;
import com.api.util.PropertiesUtil;
/**
* 全局公用常量
*
* @author Allen 2017年5月18日
*
*/
public class Conf {
/************************
* 内外网访问策略 Final不可配在conf.properties Controller注解中配置策略用
**********************/
/** 本地ip **/
public final static String LOCAL_IP = "127.0.0.1";
/** 白名单机 **/
public final static String DEVELOP_IP = "172.26.106.38";
/** 全域 **/
public final static String All_IP = "*";
/************************
* 通用
**********************/
public static String DATE_FORMAT = value("date_format");
public static String PATTERN_COMPLIE_VERSION = value("pattern_complie_version");
/************************
* Log && E-Mail
**********************/
public static String LOCAL_TITLE = value("local_title");
public static String RELEASE_TITLE = value("release_title");
public static String MAIL_HEAD_TITLE = value("mail_head_title");
public static String NOTICE_MAIL = value("notice_mail");
public static String FROM_ACCOUNT = value("from_account");
public static String FROM_PASSWORD = value("from_password");
public static String SMTP_HOST = value("smtp_host");
static Properties properties;
static {
properties = PropertiesUtil.getProperties("conf.properties");
}
private static String value(String key) {
if (properties == null)
properties = PropertiesUtil.getProperties("conf.properties");
return properties.getProperty(key);
}
}
Log&Mail
这里没什么好说的得到exception堆栈日志,发送到邮箱即可,注意的是邮件标题很有可能被邮箱拒收,被认作垃圾邮件,所以有条件还是用公司自己的smtp邮件服务器好
Ant
ant环境变量安装好后,执行build.xml 进行发布。记得看好配置,这个ant支持多服务器发布,所以会弹出选框。
jsch-0.1.54.jar
SSH2的jar,把这个添加到ant中,Windows->preferences->ant->runtime->classpath->ant home Entries 把这个jar添加进去
SSH2的jar,把这个添加到ant中,Windows->preferences->ant->runtime->classpath->ant home Entries 把这个jar添加进去
org.eclipse.jdt-4.6.3.zip
org.eclipse.jdt.compiler.tool_1.1.100.v20160418-1457.jar
org.eclipse.jdt.core_3.12.3.v20170228-1205.jar
org.eclipse.jdt.core_3.12.3.v20170228-1205.jar
org.eclipse.jdt.debug.ui_3.7.201.v20160811-0450.jar
把zip解压缩把以上3个jar放到 ant/lib中,然后在eclipse->选中build.xml->run as -> 第二个ant build -> JRE -> select " run in the same JRE as the workspace "
总结
最后你发现你不需要维护if else ,你只需要做一个新的controller来集成父级,来写新的业务即可,其他的都不需要你考虑的
看看我们用抽象工厂和反射来解决了这个看似复杂的问题
我们不需要在if(version),我们只需要extends,其他任何细节都不需要考虑,是不是美妙了许多
下载地址
http://download.csdn.net/detail/crazyzxljing0621/9852818