源码地址:https://github.com/huangyichun/remotetransfer
本文涉及知识点:
- 自定义注解
- 动态代理
- Spring bean加载
- Java 8 优化的策略模式
项目背景:由于原直播系统采用SpringCloud部署在虚拟机(仅使用SpringCloud的注册中心),现打算使用k8s部署,由于k8s和SpringCloud功能很多重合了,如果在k8s系统使用SpringCloud将无法用到k8s的注册中心、负载均衡等功能,而且项目部署也过臃肿。 因此有2种方案:
-
方案一
采用spring-cloud-starter-kubernetes 组件替换SpringCloud,该方案存在一个问题,研发需要熟悉k8s功能,而且未来项目必须使用k8s部署。
-
方案二
采用Http请求替换SpringCloud的注册中心Feign调用。由于接口较多,如果每个接口都改动工作量较大,也不利于代码维护。因此使用自定义注解来替换@FeignClient注册。
具体实现:
-
创建自定义注解
/** * 远程调用,替换SpringCloud Feign * @Author: huangyichun * @Date: 2021/2/22 */ @Target(ElementType.TYPE) @Inherited @Retention(RetentionPolicy.RUNTIME) @Documented public @interface RemoteTransfer { String hostName(); }
-
定义handler接口用于处理实际业务逻辑
/** * @Author: huangyichun * @Date: 2021/2/24 */ public interface RemoteTransferHandler { Object handler(String host, Method method, Object[] args); }
- 采用反射动态创建类,并注册到Spring容器中
创建 RemoteTransferRegister类,并实现BeanFactoryPostProcessor接口,该接口的方法postProcessBeanFactory是在bean被实例化之前被调用的。这样保证了自定义注解声明的Bean能被其他模块Bean依赖。
/**
* @Author: huangyichun
* @Date: 2021/2/23
*/
@Slf4j
@Component
public class RemoteTransferRegister implements BeanFactoryPostProcessor {
/**
* 设置扫描的包
*/
private static final String SCAN_PATH = "com.huang.web";
@Override
public void postProcessBeanFactory(ConfigurableListableBeanFactory beanFactory) throws BeansException {
RemoteTransferInvoke transferInvoke = new RemoteTransferInvoke(invokeRestTemplate());
//扫描注解声明类
Set> classSet = new Reflections(SCAN_PATH).getTypesAnnotatedWith(RemoteTransfer.class);
for (Class> cls : classSet) {
log.info("create proxy class name:{}", cls.getName());
//动态代理的handler
InvocationHandler handler = (proxy, method, args) -> transferInvoke.invoke(cls, method, args);
//生成代理类
Object proxy = Proxy.newProxyInstance(this.getClass().getClassLoader(), new Class>[]{cls}, handler);
//注册到Spring容器
beanFactory.registerSingleton(cls.getName(), proxy);
}
}
private RestTemplate invokeRestTemplate() {
//生成restTemplate 省略代码
}
}
-
创建RemoteTransferInvoke类,处理动态代理类的请求
支持解析@GetMapping、@PostMapping、@PutMapping、以及@DeleteMapping。使用策略模式+Map优化 if else 语句。具体代码如下
@Slf4j @Data public class RemoteTransferInvoke { private final RestTemplate restTemplate; /** * 用于实际远程调用处理 */ private Map
, RemoteTransferHandler> requestMethodMap = new HashMap<>(); public RemoteTransferInvoke(RestTemplate restTemplate) { this.restTemplate = restTemplate; init(); } /** * 使用策略模式+Map 去除 */ private void init() { requestMethodMap.put(GetMapping.class, (host, method, args) -> { GetMapping methodAnnotation = method.getAnnotation(GetMapping.class); String path = methodAnnotation.value()[0]; return getOrDeleteMapping(method, args, host, path, HttpMethod.GET); }); requestMethodMap.put(PutMapping.class, (host, method, args) -> { PutMapping methodAnnotation = method.getAnnotation(PutMapping.class); String path = methodAnnotation.value()[0]; return putOrPostMapping(method, args, host, path, HttpMethod.PUT); }); requestMethodMap.put(PostMapping.class, (host, method, args) -> { PostMapping methodAnnotation = method.getAnnotation(PostMapping.class); String path = methodAnnotation.value()[0]; return putOrPostMapping(method, args, host, path, HttpMethod.POST); }); requestMethodMap.put(DeleteMapping.class, (host, method, args) -> { DeleteMapping methodAnnotation = method.getAnnotation(DeleteMapping.class); String path = methodAnnotation.value()[0]; return getOrDeleteMapping(method, args, host, path, HttpMethod.DELETE); }); } /** * 动态代理调用方法 * @param tClass 类 * @param method 方法 * @param args 请求参数 * @return 返回值 */ public Object invoke(Class> tClass, Method method, Object[] args) { RemoteTransfer remoteAnnotation = tClass.getAnnotation(RemoteTransfer.class); String host = RemoteTransferConfig.map.get(remoteAnnotation.hostName()); Annotation[] annotations = method.getAnnotations(); Optional first = Arrays.stream(annotations).filter(annotation1 -> requestMethodMap.containsKey(annotation1.annotationType())).findFirst(); Preconditions.checkArgument(first.isPresent(), "注解使用错误"); Annotation methodAnnotation = first.get(); return requestMethodMap.get(methodAnnotation.annotationType()).handler(host, method, args); } private Object putOrPostMapping(Method method, Object[] args, String host, String url, HttpMethod httpMethod) { url = RemoteTransferUtil.dealPathVariable(method, args, url); UriComponentsBuilder builder = UriComponentsBuilder.fromHttpUrl(host + url); HttpHeaders httpHeaders = new HttpHeaders(); HttpEntity entity = new HttpEntity<>(RemoteTransferUtil.extractBody(method, args), httpHeaders); ResponseEntity> exchange = restTemplate.exchange(builder.toUriString(), httpMethod, entity, method.getReturnType()); return exchange.getBody(); } private Object getOrDeleteMapping(Method method, Object[] args, String host, String url, HttpMethod httpMethod) { UriComponentsBuilder builder = buildGetUrl(method, args, host, url); HttpHeaders httpHeaders = new HttpHeaders(); HttpEntity entity = new HttpEntity<>(null, httpHeaders); return this.restTemplate.exchange(builder.toUriString(), httpMethod, entity, method.getReturnType()).getBody(); } private UriComponentsBuilder buildGetUrl(Method method, Object[] args, String host, String url) { url = RemoteTransferUtil.dealPathVariable(method, args, url); UriComponentsBuilder builder = UriComponentsBuilder.fromHttpUrl(host + url); for (Map.Entry entry : RemoteTransferUtil.extractParams(method, args).entrySet()) { builder.queryParam(entry.getKey(), entry.getValue()); } return builder; } -
配置类
用于获取注解的host地址
@Component public class RemoteTransferConfig { public static Map
map = new HashMap<>(); @Value("${remote.transfer.host}") private String port; @PostConstruct public void init() { map.put("TRADE-PLAY", port); } } -
工具类
/** * @Author: huangyichun * @Date: 2021/2/23 */ @Slf4j public class RemoteTransferUtil { /** * 获取Path参数 * @param method 方法 * @param args 值 * @return */ public static Map
extractPath(Method method, Object[] args) { Map params = new HashMap<>(); Parameter[] parameters = method.getParameters(); if (parameters.length == 0) { return params; } for (int i = 0; i < parameters.length; i++) { PathVariable param = parameters[i].getAnnotation(PathVariable.class); if (param != null) { params.put(param.value(), String.valueOf(args[i])); } } return params; } /** * 获取请求body * @param method * @param args * @return */ public static JSONObject extractBody(Method method, Object[] args) { JSONObject object = new JSONObject(); Parameter[] parameters = method.getParameters(); if (parameters.length == 0) { return null; } for (int i = 0; i < parameters.length; i++) { RequestBody param = parameters[i].getAnnotation(RequestBody.class); if (param != null) { String returnStr = JSON.toJSONString(args[i], SerializerFeature.WriteMapNullValue, SerializerFeature.DisableCircularReferenceDetect, SerializerFeature.WriteDateUseDateFormat); object = JSONObject.parseObject(returnStr); } } return object; } /** * 处理url的Path * @param method 方法 * @param args 值 * @param url url * @return */ public static String dealPathVariable(Method method, Object[] args, String url) { for (Map.Entry entry : RemoteTransferUtil.extractPath(method, args).entrySet()) { if (url.contains("{" + entry.getKey() + "}")) { url = url.replace("{" + entry.getKey() + "}", entry.getValue()); } } return url; } /** * 处理请求参数 * @param method 方法 * @param args 值 * @return */ public static LinkedHashMap extractParams(Method method, Object[] args) { LinkedHashMap params = new LinkedHashMap<>(); Parameter[] parameters = method.getParameters(); if (parameters.length == 0) { return params; } for (int i = 0; i < parameters.length; i++) { RequestParam param = parameters[i].getAnnotation(RequestParam.class); if (param != null) { params.put(param.value(), String.valueOf(args[i])); } } return params; } }
注解的具体使用:
@RemoteTransfer(hostName = "TRADE-PLAY")
public interface TradePlayServiceApi {
@GetMapping("/open/get")
String test(@RequestParam("type") String type);
@PostMapping("/open/post")
ResponseResult post(@RequestBody PostRequest request);
}
测试类:
@SpringBootTest
class WebApplicationTests {
@Autowired
private TradePlayServiceApi tradePlayServiceApi;
@Test
public void get() {
String type = tradePlayServiceApi.test("type");
System.out.println(type);
}
@Test
public void post() {
PostRequest request = new PostRequest();
request.setId("id");
PostRequest.Person person = new PostRequest.Person("name", "age");
request.setPerson(person);
ResponseResult post = tradePlayServiceApi.post(request);
System.out.println(post);
}
}
最终测试通过,请求正常返回。