支持传统web开发

pull/1/head
tanghc 5 years ago
parent d85a09ef11
commit ae40a868d2
  1. 48
      doc/docs/files/10100_传统web开发.md
  2. 16
      sop-common/sop-gateway-common/src/main/java/com/gitee/sop/gatewaycommon/loadbalancer/SopPropertiesFactory.java
  3. 6
      sop-common/sop-gateway-common/src/main/java/com/gitee/sop/gatewaycommon/manager/AbstractConfiguration.java
  4. 7
      sop-common/sop-gateway-common/src/main/java/com/gitee/sop/gatewaycommon/manager/EnvironmentContext.java
  5. 45
      sop-common/sop-gateway-common/src/main/java/com/gitee/sop/gatewaycommon/manager/EnvironmentKeys.java
  6. 4
      sop-common/sop-gateway-common/src/main/java/com/gitee/sop/gatewaycommon/route/EurekaRegistryListener.java
  7. 15
      sop-common/sop-gateway-common/src/main/java/com/gitee/sop/gatewaycommon/route/ServiceRouteListener.java
  8. 4
      sop-common/sop-gateway-common/src/main/java/com/gitee/sop/gatewaycommon/zuul/result/ZuulResultExecutor.java
  9. 2
      sop-common/sop-service-common/src/main/java/com/gitee/sop/servercommon/bean/ServiceApiInfo.java
  10. 3
      sop-common/sop-service-common/src/main/java/com/gitee/sop/servercommon/bean/ServiceContext.java
  11. 28
      sop-common/sop-service-common/src/main/java/com/gitee/sop/servercommon/manager/ApiMetaBuilder.java
  12. 5
      sop-common/sop-service-common/src/main/java/com/gitee/sop/servercommon/manager/ServiceRouteController.java
  13. 4
      sop-common/sop-service-common/src/main/java/com/gitee/sop/servercommon/manager/ServiceRouteInfoBuilder.java
  14. 50
      sop-common/sop-service-common/src/main/java/com/gitee/sop/servercommon/param/ApiArgumentResolver.java
  15. 22
      sop-example/sop-story/sop-story-web/src/main/java/com/gitee/sop/storyweb/controller/TraditionalWebappController.java
  16. 2
      sop-gateway/src/main/java/com/gitee/sop/gateway/controller/RestServlet.java

@ -4,39 +4,13 @@ SOP既可以作为网关服务开发,又可以作为传统的webapp服务开
本篇介绍如何使用SOP进行传统web服务开发,即对接前端应用(H5、小程序、App)。
- 网关ZuulConfig继承WebappZuulConfiguration类
- 打开sop-gateway配置文件,新增一行配置:
```java
@Configuration
public class ZuulConfig extends WebappZuulConfiguration {
static {
new ManagerInitializer();
}
}
```
设置完毕,网关不在进行签名验证,网关统一的返回结果如下:
```json
{
"result": {
...
}
}
```
- 微服务OpenServiceConfig继承WebappServiceConfiguration类
```java
public class OpenServiceConfig extends WebappServiceConfiguration {
...
}
```properties
# 开启传统的webapp服务开发
sop.web-model.enable=true
```
其它内容不变
- 前端app请求网关
请求格式为:`http://ip:port/rest/your_path`,其中`http://ip:port/rest/`为固定部分,后面跟微服务请求路径。
@ -49,7 +23,7 @@ public class OpenServiceConfig extends WebappServiceConfiguration {
@RestController
@RequestMapping("food")
public class TraditionalWebappController {
@ApiMapping(value = "getFoodById", method = RequestMethod.GET)
@RequestMapping(value = "getFoodById", method = RequestMethod.GET)
public Food getFoodById(Integer id) {
Food food = new Food();
food.setId(id);
@ -57,22 +31,12 @@ public class TraditionalWebappController {
food.setPrice(new BigDecimal(20.00));
return food;
}
// 加版本号
@ApiMapping(value = "getFoodById", method = RequestMethod.GET, version = "1.1")
public Food getFoodById2(Integer id) {
Food food = new Food();
food.setId(id);
food.setName("香蕉2");
food.setPrice(new BigDecimal(22.00));
return food;
}
}
```
这是一个`食品服务`例子,假设网关ip为10.0.1.11,端口8081;食品服务ip为10.0.1.22,端口2222
1. 网关访问:`http://10.0.1.11:8081/rest/food/getFoodById?id=2`。加版本号:`http://localhost:8081/rest/food/getFoodById?id=2&version=1.1`
1. 网关访问:`http://10.0.1.11:8081/rest/food/getFoodById?id=2`
2. 本地访问:`http://10.0.1.22:2222/food/getFoodById/?id=2`

@ -1,9 +1,8 @@
package com.gitee.sop.gatewaycommon.loadbalancer;
import com.gitee.sop.gatewaycommon.manager.EnvironmentKeys;
import com.netflix.loadbalancer.IRule;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.cloud.netflix.ribbon.PropertiesFactory;
import org.springframework.core.env.Environment;
/**
* 自定义PropertiesFactory用来动态添加LoadBalance规则
@ -11,17 +10,6 @@ import org.springframework.core.env.Environment;
*/
public class SopPropertiesFactory extends PropertiesFactory {
/**
* 可在配置文件中设置<code>zuul.custom-rule-classname=com.xx.ClassName</code>指定负载均衡规则类
* 默认使用com.gitee.sop.gateway.loadbalancer.PreEnvironmentServerChooser
*/
public static final String PROPERTIES_KEY = "zuul.custom-rule-classname";
private static final String CUSTOM_RULE_CLASSNAME = EnvironmentServerChooser.class.getName();
@Autowired
private Environment environment;
/**
* 配置文件配置<serviceId>.ribbon.NFLoadBalancerRuleClassName=com.gitee.sop.gateway.loadbalancer.EnvironmentServerChooser
* @param clazz
@ -31,7 +19,7 @@ public class SopPropertiesFactory extends PropertiesFactory {
@Override
public String getClassName(Class clazz, String name) {
if (clazz == IRule.class) {
return this.environment.getProperty(PROPERTIES_KEY, CUSTOM_RULE_CLASSNAME);
return EnvironmentKeys.ZUUL_CUSTOM_RULE_CLASSNAME.getValue();
} else {
return super.getClassName(clazz, name);
}

@ -178,19 +178,19 @@ public class AbstractConfiguration implements ApplicationContextAware {
@PostConstruct
public final void after() {
EnvironmentContext.setEnvironment(environment);
SpringContext.setApplicationContext(applicationContext);
if (RouteRepositoryContext.getRouteRepository() == null) {
throw new IllegalArgumentException("RouteRepositoryContext.setRouteRepository()方法未使用");
}
String serverName = environment.getProperty("spring.application.name");
String serverName = EnvironmentKeys.SPRING_APPLICATION_NAME.getValue();
if (!"api-gateway".equals(serverName)) {
throw new IllegalArgumentException("spring.application.name必须为api-gateway");
}
String urlencode = environment.getProperty("sign.urlencode");
String urlencode = EnvironmentKeys.SIGN_URLENCODE.getValue();
if ("true".equals(urlencode)) {
SignConfig.enableUrlencodeMode();
}
EnvironmentContext.setEnvironment(environment);
initMessage();
initBeanInitializer();

@ -16,11 +16,8 @@ public class EnvironmentContext {
EnvironmentContext.environment = environment;
}
public static String getProfile(Environment env) {
return env.getProperty("spring.profiles.active", "default");
public static String getValue(String key, String defaultValue) {
return environment.getProperty(key, defaultValue);
}
public static String getProfile() {
return getProfile(environment);
}
}

@ -0,0 +1,45 @@
package com.gitee.sop.gatewaycommon.manager;
import com.gitee.sop.gatewaycommon.loadbalancer.EnvironmentServerChooser;
public enum EnvironmentKeys {
SPRING_PROFILES_ACTIVE("spring.profiles.active", "default"),
/**
* spring.application.name
*/
SPRING_APPLICATION_NAME("spring.application.name"),
/**
* 指定负载均衡规则类默认使用com.gitee.sop.gateway.loadbalancer.PreEnvironmentServerChooser
*/
ZUUL_CUSTOM_RULE_CLASSNAME("zuul.custom-rule-classname", EnvironmentServerChooser.class.getName()),
/**
* sign.urlencode=true签名验证拼接字符串的value部分进行urlencode
*/
SIGN_URLENCODE("sign.urlencode"),
/**
* sop.web-model.enable=true开启传统web开发模式
*/
SOP_WEB_MODEL_ENABLE("sop.web-model.enable");
private String key;
private String defaultValue;
public String getKey() {
return key;
}
EnvironmentKeys(String key) {
this.key = key;
}
EnvironmentKeys(String key, String defaultValue) {
this.key = key;
this.defaultValue = defaultValue;
}
public String getValue() {
return EnvironmentContext.getValue(key, defaultValue);
}
}

@ -2,7 +2,7 @@ package com.gitee.sop.gatewaycommon.route;
import com.gitee.sop.gatewaycommon.bean.InstanceDefinition;
import com.gitee.sop.gatewaycommon.loadbalancer.EurekaEnvironmentServerChooser;
import com.gitee.sop.gatewaycommon.loadbalancer.SopPropertiesFactory;
import com.gitee.sop.gatewaycommon.manager.EnvironmentKeys;
import com.netflix.appinfo.InstanceInfo;
import com.netflix.discovery.shared.Application;
import com.netflix.discovery.shared.Applications;
@ -24,7 +24,7 @@ import java.util.stream.Collectors;
public class EurekaRegistryListener extends BaseRegistryListener {
static {
System.setProperty(SopPropertiesFactory.PROPERTIES_KEY, EurekaEnvironmentServerChooser.class.getName());
System.setProperty(EnvironmentKeys.ZUUL_CUSTOM_RULE_CLASSNAME.getKey(), EurekaEnvironmentServerChooser.class.getName());
}
private Set<String> cacheServices = new HashSet<>();

@ -4,9 +4,12 @@ import com.alibaba.fastjson.JSON;
import com.gitee.sop.gatewaycommon.bean.InstanceDefinition;
import com.gitee.sop.gatewaycommon.bean.ServiceRouteInfo;
import com.gitee.sop.gatewaycommon.manager.BaseRouteCache;
import com.gitee.sop.gatewaycommon.manager.EnvironmentKeys;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang.StringUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.HttpEntity;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
@ -24,6 +27,8 @@ public class ServiceRouteListener extends BaseServiceListener {
private static final String METADATA_SOP_ROUTES_PATH = "sop.routes.path";
private static final String HEADER_WEB_MODEL = "web-model";
@Autowired
private BaseRouteCache<?> baseRouteCache;
@ -42,7 +47,7 @@ public class ServiceRouteListener extends BaseServiceListener {
String serviceName = instance.getServiceId();
String url = getRouteRequestUrl(instance);
log.info("拉取路由配置,serviceId: {}, url: {}", serviceName, url);
ResponseEntity<String> responseEntity = getRestTemplate().getForEntity(url, String.class);
ResponseEntity<String> responseEntity = getRestTemplate().postForEntity(url, getHttpEntity(), String.class);
if (responseEntity.getStatusCode() == HttpStatus.OK) {
String body = responseEntity.getBody();
ServiceRouteInfo serviceRouteInfo = JSON.parseObject(body, ServiceRouteInfo.class);
@ -52,6 +57,14 @@ public class ServiceRouteListener extends BaseServiceListener {
}
}
protected HttpEntity<String> getHttpEntity() {
HttpHeaders headers = new HttpHeaders();
String webModel = EnvironmentKeys.SOP_WEB_MODEL_ENABLE.getValue();
boolean isWebModel = "true".equals(webModel);
headers.add(HEADER_WEB_MODEL, String.valueOf(isWebModel));
return new HttpEntity<>(headers);
}
/**
* 拉取路由请求url
*

@ -60,8 +60,8 @@ public class ZuulResultExecutor extends BaseExecutorAdapter<RequestContext, Stri
@Override
public String buildErrorResult(RequestContext request, Throwable throwable) {
Error error = getError(throwable);
JSONObject jsonObject = (JSONObject) JSON.toJSON(error);
return this.merge(request, jsonObject);
return isMergeResult(request) ? this.merge(request, (JSONObject) JSON.toJSON(error))
: JSON.toJSONString(error);
}
public static Error getError(Throwable throwable) {

@ -28,6 +28,8 @@ public class ServiceApiInfo {
private int mergeResult;
/** 是否需要授权才能访问 */
private int permission;
/** 是否是原始Mapping */
private boolean originalMapping;
public ApiMeta() {
}

@ -13,6 +13,9 @@ public class ServiceContext extends ConcurrentHashMap<String, Object> {
public static final String REQUEST_KEY = "request";
public static final String RESPONSE_KEY = "response";
public static final String OPEN_CONTEXT_KEY = "openContext";
public static final String WEB_MODEL_KEY = "sop-web-model";
protected static Class<? extends ServiceContext> contextClass = ServiceContext.class;
protected static final ThreadLocal<? extends ServiceContext> THREAD_LOCAL = new ThreadLocal<ServiceContext>() {

@ -2,6 +2,7 @@ package com.gitee.sop.servercommon.manager;
import com.gitee.sop.servercommon.bean.ServiceApiInfo;
import com.gitee.sop.servercommon.bean.ServiceConfig;
import com.gitee.sop.servercommon.bean.ServiceContext;
import com.gitee.sop.servercommon.mapping.ApiMappingInfo;
import com.gitee.sop.servercommon.mapping.ApiMappingRequestCondition;
import com.gitee.sop.servercommon.mapping.RouteUtil;
@ -12,6 +13,7 @@ import org.springframework.web.servlet.mvc.method.RequestMappingInfo;
import org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerMapping;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Comparator;
import java.util.List;
import java.util.Map;
@ -25,6 +27,8 @@ public class ApiMetaBuilder {
/** 接口名规则:允许字母、数字、点、下划线 */
private static final String REGEX_API_NAME = "^[a-zA-Z0-9\\.\\_\\-]+$";
private static final List<String> IGNORE_PATTERN_KEYWORDS = Arrays.asList("swagger","sop/routes");
public ServiceApiInfo getServiceApiInfo(String serviceId, RequestMappingHandlerMapping requestMappingHandlerMapping) {
if (serviceId == null) {
throw new IllegalArgumentException("请在application.properties中指定spring.application.name属性");
@ -83,8 +87,30 @@ public class ApiMetaBuilder {
apiMeta.setMergeResult(BooleanUtils.toInteger(apiMappingInfo.isMergeResult()));
apiMeta.setPermission(BooleanUtils.toInteger(apiMappingInfo.isPermission()));
return apiMeta;
} else {
if (!ServiceContext.getCurrentContext().getBoolean(ServiceContext.WEB_MODEL_KEY, false)) {
return null;
}
String path = patterns.iterator().next();
if (path.contains("$") || isIgnorePattern(path)) {
return null;
}
ServiceApiInfo.ApiMeta apiMeta = new ServiceApiInfo.ApiMeta(path, path, "1.0");
apiMeta.setIgnoreValidate(BooleanUtils.toInteger(true));
apiMeta.setMergeResult(BooleanUtils.toInteger(false));
apiMeta.setPermission(BooleanUtils.toInteger(false));
apiMeta.setOriginalMapping(true);
return apiMeta;
}
}
private boolean isIgnorePattern(String pattern) {
for (String keyword : IGNORE_PATTERN_KEYWORDS) {
if (pattern.contains(keyword)) {
return true;
}
}
return null;
return false;
}
protected void checkApiName(String name) {

@ -1,6 +1,7 @@
package com.gitee.sop.servercommon.manager;
import com.gitee.sop.servercommon.bean.ServiceApiInfo;
import com.gitee.sop.servercommon.bean.ServiceContext;
import com.gitee.sop.servercommon.route.ServiceRouteInfo;
import com.gitee.sop.servercommon.util.OpenUtil;
import lombok.Getter;
@ -23,6 +24,7 @@ import javax.servlet.http.HttpServletResponse;
public class ServiceRouteController {
private static final String SECRET = "a3d9sf!1@odl90zd>fkASwq";
private static final String HEADER_WEB_MODEL = "web-model";
@Autowired
private RequestMappingHandlerMapping requestMappingHandlerMapping;
@ -50,6 +52,9 @@ public class ServiceRouteController {
throw new IllegalArgumentException("未指定spring.application.name参数");
}
ApiMetaBuilder apiMetaBuilder = getApiMetaBuilder();
String webModel = request.getHeader(HEADER_WEB_MODEL);
boolean isWebModel = "true".equals(webModel);
ServiceContext.getCurrentContext().set(ServiceContext.WEB_MODEL_KEY, isWebModel);
ServiceApiInfo serviceApiInfo = apiMetaBuilder.getServiceApiInfo(serviceId, requestMappingHandlerMapping);
ServiceRouteInfoBuilder serviceRouteInfoBuilder = new ServiceRouteInfoBuilder(environment);
return serviceRouteInfoBuilder.build(serviceApiInfo);

@ -64,7 +64,9 @@ public class ServiceRouteInfoBuilder {
RouteDefinition routeDefinition = new RouteDefinition();
// 唯一id规则:接口名 + 版本号
String routeId = apiMeta.fetchNameVersion();
this.checkPath(routeId, "接口定义(" + routeId + ")不能有斜杠字符'/'");
if (!apiMeta.isOriginalMapping()) {
this.checkPath(routeId, "接口定义(" + routeId + ")不能有斜杠字符'/'");
}
BeanUtils.copyProperties(apiMeta, routeDefinition);
routeDefinition.setId(routeId);
routeDefinition.setFilters(Collections.emptyList());

@ -60,6 +60,8 @@ public class ApiArgumentResolver implements SopHandlerMethodArgumentResolver {
private RequestMappingHandlerAdapter requestMappingHandlerAdapter;
private static List<MethodParameter> openApiParams = new ArrayList<>(64);
static {
try {
pushBuilder = ClassUtils.forName("javax.servlet.http.PushBuilder",
@ -75,18 +77,7 @@ public class ApiArgumentResolver implements SopHandlerMethodArgumentResolver {
List<HandlerMethodArgumentResolver> argumentResolversNew = new ArrayList<>(64);
// 先加自己,确保在第一个位置
argumentResolversNew.add(this);
HandlerMethodArgumentResolver lastOne = null;
for (HandlerMethodArgumentResolver argumentResolver : Objects.requireNonNull(requestMappingHandlerAdapter.getArgumentResolvers())) {
// RequestResponseBodyMethodProcessor暂存起来,放在最后面
if (argumentResolver instanceof RequestResponseBodyMethodProcessor) {
lastOne = argumentResolver;
} else {
argumentResolversNew.add(argumentResolver);
}
}
if (lastOne != null) {
argumentResolversNew.add(lastOne);
}
argumentResolversNew.addAll(requestMappingHandlerAdapter.getArgumentResolvers());
requestMappingHandlerAdapter.setArgumentResolvers(argumentResolversNew);
this.requestMappingHandlerAdapter = requestMappingHandlerAdapter;
}
@ -96,8 +87,8 @@ public class ApiArgumentResolver implements SopHandlerMethodArgumentResolver {
// 是否有注解
boolean hasAnnotation = methodParameter.hasMethodAnnotation(ApiMapping.class)
|| methodParameter.hasMethodAnnotation(ApiAbility.class);
if (!hasAnnotation) {
return false;
if (hasAnnotation) {
openApiParams.add(methodParameter);
}
Class<?> paramType = methodParameter.getParameterType();
if (paramType == OpenContext.class) {
@ -132,23 +123,24 @@ public class ApiArgumentResolver implements SopHandlerMethodArgumentResolver {
, NativeWebRequest nativeWebRequest
, WebDataBinderFactory webDataBinderFactory
) throws Exception {
Object paramObj = this.getParamObject(methodParameter, nativeWebRequest);
if (paramObj != null) {
// JSR-303验证
paramValidator.validateBizParam(paramObj);
return paramObj;
} else {
HandlerMethodArgumentResolver resolver = getOtherArgumentResolver(methodParameter);
if (resolver != null) {
return resolver.resolveArgument(
methodParameter
, modelAndViewContainer
, nativeWebRequest
, webDataBinderFactory
);
if (openApiParams.contains(methodParameter)) {
Object paramObj = this.getParamObject(methodParameter, nativeWebRequest);
if (paramObj != null) {
// JSR-303验证
paramValidator.validateBizParam(paramObj);
return paramObj;
}
return null;
}
HandlerMethodArgumentResolver resolver = getOtherArgumentResolver(methodParameter);
if (resolver != null) {
return resolver.resolveArgument(
methodParameter
, modelAndViewContainer
, nativeWebRequest
, webDataBinderFactory
);
}
return null;
}

@ -1,6 +1,5 @@
package com.gitee.sop.storyweb.controller;
import com.gitee.sop.servercommon.annotation.ApiMapping;
import com.gitee.sop.servercommon.util.UploadUtil;
import lombok.Data;
import org.springframework.web.bind.annotation.RequestBody;
@ -24,7 +23,7 @@ public class TraditionalWebappController {
// http://localhost:8081/rest/food/getFoodById?id=1 网关入口
// http://localhost:2222/food/getFoodById/?id=12 本地入口
@ApiMapping(value = "getFoodById", method = RequestMethod.GET)
@RequestMapping(value = "getFoodById", method = RequestMethod.GET)
public Food getFoodById(Integer id) {
Food food = new Food();
food.setId(id);
@ -33,31 +32,20 @@ public class TraditionalWebappController {
return food;
}
// http://localhost:8081/rest/food/getFoodById?id=2&version=1.1 网关入口
// http://localhost:2222/food/getFoodById/?id=12&version=1.1
@ApiMapping(value = "getFoodById", method = RequestMethod.GET, version = "1.1")
public Food getFoodById2(Integer id) {
Food food = new Food();
food.setId(id);
food.setName("香蕉2");
food.setPrice(new BigDecimal(22.00));
return food;
}
// http://localhost:8081/rest/food/getFoodByObj?id=2
@ApiMapping(value = "getFoodByObj", method = RequestMethod.GET)
@RequestMapping(value = "getFoodByObj", method = RequestMethod.GET)
public Food getFoodByObj(Food food) {
return food;
}
// http://localhost:8081/rest/food/saveFood
@ApiMapping(value = "saveFood", method = RequestMethod.POST)
@RequestMapping(value = "saveFood", method = RequestMethod.POST)
public Food saveFood(@RequestBody Food food) {
food.setId(3);
return food;
}
@ApiMapping(value = "foodUpload", method = RequestMethod.POST)
@RequestMapping(value = "foodUpload", method = RequestMethod.POST)
public Food upload(Food food, HttpServletRequest request) {
// 获取上传的文件
Collection<MultipartFile> uploadFiles = UploadUtil.getUploadFiles(request);
@ -70,7 +58,7 @@ public class TraditionalWebappController {
return food;
}
@ApiMapping(value = "foodUpload3", method = RequestMethod.POST)
@RequestMapping(value = "foodUpload3", method = RequestMethod.POST)
public Food upload3(Food food, MultipartFile file) {
food.setId(5);
food.setName("文件名称+" + file.getOriginalFilename());

@ -38,7 +38,7 @@ public class RestServlet extends HttpServlet {
int index = url.indexOf(REST_PATH);
// 取/rest的后面部分
String path = url.substring(index + REST_PATH.length());
String method = RouteUtil.buildApiName(path);
String method = path;
String version = request.getParameter(ParamNames.VERSION_NAME);
if (version == null) {
version = defaultVersion;

Loading…
Cancel
Save