在某些業務場景中,我們只需要業務程式碼中定義相應的介面或者相應的註解,並不需要實現對應的邏輯。
比如 mybatis和feign: 在 mybatis 中,我們只需要定義對應的mapper介面;在 feign 中,我們只需要定義對應業務系統中的介面即可。
那麼在這種場景下,具體的業務邏輯時怎麼執行的呢,其實原理都是動態代理。
我們這裡不具體介紹動態代理,主要看一下它在springboot專案中的實際應用,下面我們模仿feign來實現一個呼叫三方介面的 httpclient。
一、定義註解
定義好註解,方便後面掃描使用了該註解的類和方法
package com.mysgk.blogdemo.annotation;
import java.lang.annotation.Documented;
import java.lang.annotation.Retention;
import java.lang.annotation.Target;
import static java.lang.annotation.ElementType.METHOD;
import static java.lang.annotation.ElementType.TYPE;
import static java.lang.annotation.RetentionPolicy.RUNTIME;
/**
* @author mysgk
* @link https://www.cnblogs.com/mysgk/p/15619895.html
* <p>
* 這裡應該還有一個註解來標識方法,demo為了方便都使用該註解
*/
@Retention(RUNTIME)
@Target({TYPE, METHOD})
@Documented
public @interface MyHttpClient {
}
二、建立動態代理類
用於產生代理類並執行對應方法
package com.mysgk.blogdemo.proxy;
import org.springframework.beans.factory.FactoryBean;
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;
/**
* @author mysgk
* @link https://www.cnblogs.com/mysgk/p/15619895.html
*/
public class RibbonAopProxyFactory<T> implements FactoryBean<T>, InvocationHandler {
private Class<T> interfaceClass;
public Class<T> getInterfaceClass() {
return interfaceClass;
}
public void setInterfaceClass(Class<T> interfaceClass) {
this.interfaceClass = interfaceClass;
}
@Override
public T getObject() throws Exception {
final Class[] interfaces = {interfaceClass};
return (T) Proxy.newProxyInstance(this.getClass().getClassLoader(), interfaces, this);
}
@Override
public Class<?> getObjectType() {
return interfaceClass;
}
@Override
public boolean isSingleton() {
return true;
}
/**
* 真正執行的方法,會被aop攔截
*/
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
return "invoke " + interfaceClass.getName() + "." + method.getName() + " , do anything ..";
}
}
三、注入spring容器
在專案啟動時,掃描第一步定義的註解,生成該類的實現類,並將其注入到spring容器
package com.mysgk.blogdemo.start;
import cn.hutool.core.util.ClassUtil;
import cn.hutool.core.util.StrUtil;
import com.mysgk.blogdemo.annotation.MyHttpClient;
import com.mysgk.blogdemo.proxy.RibbonAopProxyFactory;
import org.springframework.beans.BeansException;
import org.springframework.beans.factory.config.ConfigurableListableBeanFactory;
import org.springframework.beans.factory.support.BeanDefinitionBuilder;
import org.springframework.beans.factory.support.BeanDefinitionRegistry;
import org.springframework.beans.factory.support.BeanDefinitionRegistryPostProcessor;
import org.springframework.beans.factory.support.GenericBeanDefinition;
import org.springframework.stereotype.Component;
import java.util.Set;
/**
* @author mysgk
* @link https://www.cnblogs.com/mysgk/p/15619895.html
*/
@Component
public class ScanHttpClients implements BeanDefinitionRegistryPostProcessor {
public void run(BeanDefinitionRegistry registry) {
Set<Class<?>> scanPackage = ClassUtil.scanPackageByAnnotation("com.mysgk", MyHttpClient.class);
for (Class<?> cls : scanPackage) {
BeanDefinitionBuilder builder = BeanDefinitionBuilder.genericBeanDefinition(cls);
GenericBeanDefinition definition = (GenericBeanDefinition) builder.getRawBeanDefinition();
definition.getPropertyValues().add("interfaceClass", definition.getBeanClassName());
definition.setBeanClass(RibbonAopProxyFactory.class);
definition.setAutowireMode(GenericBeanDefinition.AUTOWIRE_BY_TYPE);
String beanName = StrUtil.removePreAndLowerFirst(cls.getSimpleName(), 0) + "RibbonClient";
registry.registerBeanDefinition(beanName, definition);
}
}
@Override
public void postProcessBeanDefinitionRegistry(BeanDefinitionRegistry registry) throws BeansException {
run(registry);
}
@Override
public void postProcessBeanFactory(ConfigurableListableBeanFactory beanFactory) throws BeansException {
}
}
四、編寫攔截器
攔截動態代理生成的方法,實現我們真正的業務邏輯
package com.mysgk.blogdemo.aop;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Pointcut;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.HttpEntity;
import org.springframework.http.HttpMethod;
import org.springframework.http.ResponseEntity;
import org.springframework.stereotype.Component;
import org.springframework.web.client.RestTemplate;
/**
* @author mysgk
* @link https://www.cnblogs.com/mysgk/p/15619895.html
*/
@Component
@Aspect
public class InterceptAnnotation {
@Autowired
RestTemplate ribbonLoadBalanced;
@Pointcut("@annotation(com.mysgk.blogdemo.annotation.MyHttpClient)")
public void execute() {
}
@Around("execute()")
public Object interceptAnnotation(ProceedingJoinPoint joinPoint) throws Throwable {
/**
* 此處省略 獲取 url, httpMethod, requestEntity, responseType 等引數的處理過程
*/
ResponseEntity<?> exchange = ribbonLoadBalanced.exchange("http://www.baidu.com", HttpMethod.GET, HttpEntity.EMPTY, Object.class);
return exchange.getBody();
}
}
五、建立客戶端呼叫類
此類實現將遠端介面當做本地方法呼叫
package com.mysgk.blogdemo.client;
import com.mysgk.blogdemo.annotation.MyHttpClient;
import org.springframework.web.bind.annotation.PostMapping;
/**
* @author mysgk
* @link https://www.cnblogs.com/mysgk/p/15619895.html
*/
@MyHttpClient
public interface MyHttpClientTest {
@MyHttpClient
@PostMapping(value = "test/t1")
Object test(String param);
}
六、main方法測試
注入客戶端進行呼叫
package com.mysgk.blogdemo;
import com.mysgk.blogdemo.client.MyHttpClientTest;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.annotation.Bean;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.client.RestTemplate;
/**
* @author mysgk
* @link https://www.cnblogs.com/mysgk/p/15619895.html
*/
@SpringBootApplication
@RestController
public class Main {
@Bean
RestTemplate restTemplate() {
return new RestTemplate();
}
@Autowired
MyHttpClientTest myHttpClientTest;
public static void main(String[] args) {
SpringApplication.run(Main.class, args);
}
@GetMapping("/t1")
public Object t1() {
return myHttpClientTest.test("1");
}
}
七、啟動專案
訪問 127.0.0.1:8080/t1,得到如下結果
此時我們呼叫的是
myHttpClientTest.test
但是真正執行的是
RibbonAopProxyFactory.invoke
此時返回的應該是
invoke com.mysgk.blogdemo.client.MyHttpClientTest.test , do anything ..
由於我們使用aop攔截了該方法,所以最終的結果是訪問到了baidu
呼叫流程圖
最後附上pom
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>org.example</groupId>
<artifactId>bolgdemo</artifactId>
<version>1.0-SNAPSHOT</version>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
<version>2.1.6.RELEASE</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-aop</artifactId>
<version>2.1.6.RELEASE</version>
</dependency>
<dependency>
<groupId>cn.hutool</groupId>
<artifactId>hutool-all</artifactId>
<version>5.3.10</version>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-ribbon</artifactId>
<version>2.1.5.RELEASE</version>
<exclusions>
<exclusion>
<artifactId>httpclient</artifactId>
<groupId>org.apache.httpcomponents</groupId>
</exclusion>
</exclusions>
</dependency>
<dependency>
<artifactId>httpclient</artifactId>
<groupId>org.apache.httpcomponents</groupId>
<version>4.5.13</version>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<version>3.5.1</version>
<configuration>
<source>1.8</source>
<target>1.8</target>
</configuration>
</plugin>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
<executions>
<execution>
<goals>
<goal>repackage</goal>
</goals>
</execution>
</executions>
</plugin>
</plugins>
</build>
</project>