本章利用反射机制实现了服务的自动注册,本章的commit为e33497a。
自定义注解
我们利用反射机制实现服务端服务的自动注册,首先需要的是通过注解来定义扫描的规则,所以我们需要定义两个注解,一个是启动类注解,即标注在服务端main方法上的,表示会以他为基点进行扫描,这里我们是去扫描位于当前启动类同一包或者是当前启动类所在包的子包中的所有服务类;第二个则是服务类的注解,标注服务类注解,在启动类启动时被扫描,会被自动注册到服务端的服务缓存以及注册中心中。
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
public @interface ServiceScan {
public String value() default "";
}
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
public @interface Service {
public String name() default "";
}
利用反射机制进行扫描
我们需要扫描的是与启动类位于同一包或者子包的所有类文件,因此我们需要获取到启动类所处的包名。而在java程序运行中,所有的方法运行都会创建一个属于自己的栈帧,之后按先后顺序在虚拟机栈中入栈以及出栈,由于main方法是所有方法的入口,因此main方法是第一个入栈最后一个出栈,且一直位于栈底,所以我们获取包名可以这样:
public static String getClassName(){
StackTraceElement[] stackTrace = new Throwable().getStackTrace();
String className = stackTrace[stackTrace.length - 1].getClassName();
return className;
}
getStackTrace底层调用的是一个native方法,返回的就是当前虚拟机栈中方法执行数组。而之后根据包名获取包下所有的class文件则利用一个Reflect工具类,具体注释可以参考怎么用Java反射获取包下所有类?,详细代码如下:
public class ReflectUtils {
public static String getClassName(){
StackTraceElement[] stackTrace = new Throwable().getStackTrace();
String className = stackTrace[stackTrace.length - 1].getClassName();
return className;
}
public static Set<Class<?>> getClasses(String packageName){
Set<Class<?>> setClasses = new LinkedHashSet<>();
boolean recursive = true;
String packageDirName = packageName.replace(".","/");
Enumeration<URL> dirs;
try{
dirs = Thread.currentThread().getContextClassLoader().getResources(packageDirName);
while(dirs.hasMoreElements()){
URL url = dirs.nextElement();
String protocol = url.getProtocol();
if("file".equals(protocol)){
String filePath = URLDecoder.decode(url.getFile(),"UTF-8");
findClassesInPackageByFile(packageName,filePath,recursive,setClasses);
}else if("jar".equals(protocol)){
JarFile jarFile;
try{
jarFile = ((JarURLConnection)url.openConnection()).getJarFile();
Enumeration<JarEntry> entries = jarFile.entries();
finaClassesInPackageByJar(packageName,entries,packageDirName,recursive,setClasses);
}catch (IOException e){
e.printStackTrace();
}
}
}
}catch (IOException e){
e.printStackTrace();
}
return setClasses;
}
private static void findClassesInPackageByFile(String packageName,String packagePath,final boolean recursive,Set<Class<?>> set){
File dir = new File(packagePath);
if(!dir.exists() || !dir.isDirectory()){
return;
}
File[] dirFiles = dir.listFiles(pathname -> (recursive || pathname.isDirectory()) || (pathname.getName().endsWith(".class")));
for(File file:dirFiles){
if(file.isDirectory()){
findClassesInPackageByFile(packageName + "." + file.getName(), file.getAbsolutePath(), recursive,set);
}else{
String className = file.getName().substring(0,file.getName().length() - 6);
try {
set.add(Thread.currentThread().getContextClassLoader().loadClass(packageName + "." +className));
} catch (ClassNotFoundException e) {
e.printStackTrace();
}
}
}
}
private static void finaClassesInPackageByJar(String packageName, Enumeration<JarEntry> entries,String packageDirName , final boolean recursive,Set<Class<?>> set){
while(entries.hasMoreElements()){
JarEntry jarEntry = entries.nextElement();
String name = jarEntry.getName();
if(name.charAt(0) == '/'){
name = name.substring(1);
}
if(name.startsWith(packageDirName)){
int idx = name.lastIndexOf("/");
if(idx != -1){
packageName = name.substring(0,idx).replace("/",".");
}
if(idx != -1 || recursive){
if(name.endsWith(".class") && !jarEntry.isDirectory()){
String className = name.substring(packageName.length()+1,name.length() - 6);
try {
set.add(Thread.currentThread().getContextClassLoader().loadClass(packageName + "." + className));
} catch (ClassNotFoundException e) {
e.printStackTrace();
}
}
}
}
}
}
}
由于所有的服务端(不管是socket实现还是netty实现)都会进行服务都注册,所以最后都可以改为服务端自动注册,因此我们可以使用模板方法,利用一个抽象类去实现顶层Server接口中的注册以及扫描功能,而具体各个服务端的start方法则交给各自自主实现。AbstractServer如下:
public abstract class AbstractServer implements CommonServer{
private static final Logger logger = LoggerFactory.getLogger(AbstractServer.class);
protected ServerPublisher serverPublisher;
protected RegisterService registerService;
protected String host;
protected int port;
@Override
public void scanServices(){
String mainClassName = ReflectUtils.getClassName();
Class<?> clazz;
try{
clazz = Class.forName(mainClassName);
if(!clazz.isAnnotationPresent(ServiceScan.class)){
logger.error("启动类缺少 @ServiceScan 注解");
throw new RpcException(RpcError.SERVICE_SCAN_ANNOTATION_NOT_FOUND);
}
} catch (ClassNotFoundException e) {
logger.error("出现未知错误");
throw new RpcException(RpcError.RPC_UNKNOWN_ERROR);
}
String basePackage = clazz.getAnnotation(ServiceScan.class).value();
if("".equals(basePackage)){
basePackage = mainClassName.substring(0,mainClassName.lastIndexOf("."));
}
Set<Class<?>> classes = ReflectUtils.getClasses(basePackage);
for(Class<?> oneClass : classes){
if(oneClass.isAnnotationPresent(Service.class)) {
Object service;
try {
service = oneClass.newInstance();
} catch (InstantiationException | IllegalAccessException e) {
logger.error("创建" + oneClass + "时发生错误");
continue;
}
serverPublisher.addService(service);
Class<?>[] interfaces = service.getClass().getInterfaces();
for (Class<?> anInterface : interfaces) {
registerService.registry(anInterface.getCanonicalName(),new InetSocketAddress(host,port));
}
}
}
}
@Override
public void publishService(List<Object> services) {
for(Object service : services){
serverPublisher.addService(service);
Class<?>[] interfaces = service.getClass().getInterfaces();
for (Class<?> anInterface : interfaces) {
registerService.registry(anInterface.getCanonicalName(),new InetSocketAddress(host,port));
}
}
start();
}
}
之后在Netty构造器中引入扫描方法,则会在类加载初始化阶段执行扫描,并将服务进行注册。Socket服务端与Netty端类似,下边只展示Netty服务端修改后的结果:
public class NettyServer extends AbstractServer {
private static final Logger logger = LoggerFactory.getLogger(NettyServer.class);
private final CommonSerializer serializer;
public NettyServer(CommonSerializer serializer,String host,int port){
serverPublisher = new DefaultServerPublisher();
this.serializer = serializer;
registerService = new NacosRegisterService();
this.host = host;
this.port = port;
scanServices();
}
@Override
public void start() {
ShutdownHook.getShutdownHook().addClearHook();
EventLoopGroup boss = new NioEventLoopGroup();
EventLoopGroup worker = new NioEventLoopGroup();
try{
ServerBootstrap serverBootstrap = new ServerBootstrap();
serverBootstrap.group(boss,worker)
.channel(NioServerSocketChannel.class)
.handler(new LoggingHandler(LogLevel.INFO))
.option(ChannelOption.SO_BACKLOG,256)
.option(ChannelOption.SO_KEEPALIVE,true)
.childOption(ChannelOption.TCP_NODELAY,true)
.childHandler(new ChannelInitializer<SocketChannel>() {
@Override
protected void initChannel(SocketChannel ch) throws Exception {
ChannelPipeline pipeline = ch.pipeline();
pipeline.addLast(new IdleStateHandler(30,0,0, TimeUnit.SECONDS))
.addLast(new MyEncoder(serializer))
.addLast(new MyDecoder())
.addLast(new NettyServerHandler(serverPublisher));
}
});
ChannelFuture future = serverBootstrap.bind(host,port).sync();
future.channel().closeFuture().sync();
}catch (InterruptedException e){
logger.error("启动服务时发生错误");
}
finally {
boss.shutdownGracefully();
worker.shutdownGracefully();
}
}
}
测试
服务端
在服务端我们只需要将我们想注册的服务标注@Service注解,并在启动类标注@ServiceScan注解即可:
@Service
public class ByeServiceImpl implements ByeService {
@Override
public String bye(RpcObject object) {
return "(" + object.getMessage() + "),bye!";
}
}
@Service
public class HelloServiceImpl implements HelloService {
@Override
public String sayHello(RpcObject object) {
return "这是id为:" + object.getId() + "发送的:" + object.getMessage();
}
}
@ServiceScan
public class NettyServerTest {
public static void main(String[] args) {
NettyServer server = new NettyServer(new ProtobufSerializer(),"127.0.0.1",9999);
server.start();
}
}
客户端
客户端未进行任何修改,无改动。
测试结果
[main] INFO cn.fzzfrjf.core.DefaultServerPublisher - 向接口:interface cn.fzzfrjf.entity.ByeService注册服务:cn.fzzfrjf.test.ByeServiceImpl
[main] WARN com.alibaba.nacos.client.utils.LogUtils - Load Log4j Configuration of Nacos fail, message: org/apache/logging/log4j/LogManager
[main] INFO com.alibaba.nacos.client.naming - initializer namespace from System Property :null
[main] INFO com.alibaba.nacos.client.naming - initializer namespace from System Environment :null
[main] INFO com.alibaba.nacos.client.naming - initializer namespace from System Property :null
[main] INFO com.alibaba.nacos.client.utils.ParamUtil - [settings] [req-serv] nacos-server port:8848
[main] INFO com.alibaba.nacos.client.utils.ParamUtil - [settings] [http-client] connect timeout:1000
[main] INFO com.alibaba.nacos.client.utils.ParamUtil - NACOS_CLIENT_VERSION: ${project.version}
[main] INFO com.alibaba.nacos.client.utils.ParamUtil - PER_TASK_CONFIG_SIZE: 3000.0
[main] INFO com.alibaba.nacos.client.naming - [BEAT] adding beat: BeatInfo{port=9999, ip='activate.navicat.com', weight=1.0, serviceName='DEFAULT_GROUP@@cn.fzzfrjf.entity.ByeService', cluster='DEFAULT', metadata={}, scheduled=false, period=5000, stopped=false} to beat map.
[main] INFO com.alibaba.nacos.client.naming - [REGISTER-SERVICE] public registering service DEFAULT_GROUP@@cn.fzzfrjf.entity.ByeService with instance: Instance{instanceId='null', ip='activate.navicat.com', port=9999, weight=1.0, healthy=true, enabled=true, ephemeral=true, clusterName='DEFAULT', serviceName='null', metadata={}}
[main] INFO com.alibaba.nacos.client.identify.CredentialWatcher - null No credential found
[main] INFO cn.fzzfrjf.core.DefaultServerPublisher - 向接口:interface cn.fzzfrjf.entity.HelloService注册服务:cn.fzzfrjf.test.HelloServiceImpl
[main] INFO com.alibaba.nacos.client.naming - [BEAT] adding beat: BeatInfo{port=9999, ip='activate.navicat.com', weight=1.0, serviceName='DEFAULT_GROUP@@cn.fzzfrjf.entity.HelloService', cluster='DEFAULT', metadata={}, scheduled=false, period=5000, stopped=false} to beat map.
[main] INFO com.alibaba.nacos.client.naming - [REGISTER-SERVICE] public registering service DEFAULT_GROUP@@cn.fzzfrjf.entity.HelloService with instance: Instance{instanceId='null', ip='activate.navicat.com', port=9999, weight=1.0, healthy=true, enabled=true, ephemeral=true, clusterName='DEFAULT', serviceName='null', metadata={}}
[main] INFO cn.fzzfrjf.utils.ShutdownHook - 关闭后将自动注销所有服务。。。
[main] WARN io.netty.bootstrap.ServerBootstrap - Unknown channel option 'SO_KEEPALIVE' for channel '[id: 0xdfe47c66]'
[nioEventLoopGroup-2-1] INFO io.netty.handler.logging.LoggingHandler - [id: 0xdfe47c66] REGISTERED
[nioEventLoopGroup-2-1] INFO io.netty.handler.logging.LoggingHandler - [id: 0xdfe47c66] BIND: /127.0.0.1:9999
[nioEventLoopGroup-2-1] INFO io.netty.handler.logging.LoggingHandler - [id: 0xdfe47c66, L:/127.0.0.1:9999] ACTIVE
[nioEventLoopGroup-2-1] INFO io.netty.handler.logging.LoggingHandler - [id: 0xdfe47c66, L:/127.0.0.1:9999] READ: [id: 0x46b09133, L:/127.0.0.1:9999 - R:/127.0.0.1:63633]
[nioEventLoopGroup-2-1] INFO io.netty.handler.logging.LoggingHandler - [id: 0xdfe47c66, L:/127.0.0.1:9999] READ COMPLETE
[netty-server-handler-0] INFO cn.fzzfrjf.core.NettyServerHandler - 服务器接收到请求:cn.fzzfrjf.entity.RpcRequest@4b6f621d
[netty-server-handler-1] INFO cn.fzzfrjf.core.NettyServerHandler - 服务器接收到请求:cn.fzzfrjf.entity.RpcRequest@4fc73f8c
[netty-server-handler-2] INFO cn.fzzfrjf.core.NettyServerHandler - 收到客服端的心跳包。。。
[netty-server-handler-3] INFO cn.fzzfrjf.core.NettyServerHandler - 收到客服端的心跳包。。。
[Thread-7] INFO com.alibaba.nacos.client.naming - [BEAT] removing beat: DEFAULT_GROUP@@cn.fzzfrjf.entity.ByeService:activate.navicat.com:9999 from beat map.
[Thread-7] INFO com.alibaba.nacos.client.naming - [DEREGISTER-SERVICE] public deregistering service DEFAULT_GROUP@@cn.fzzfrjf.entity.ByeService with instance: Instance{instanceId='null', ip='activate.navicat.com', port=9999, weight=1.0, healthy=true, enabled=true, ephemeral=true, clusterName='DEFAULT', serviceName='null', metadata={}}
[Thread-7] INFO cn.fzzfrjf.utils.NacosUtils - 成功注销cn.fzzfrjf.entity.ByeService服务
[Thread-7] INFO com.alibaba.nacos.client.naming - [BEAT] removing beat: DEFAULT_GROUP@@cn.fzzfrjf.entity.HelloService:activate.navicat.com:9999 from beat map.
[Thread-7] INFO com.alibaba.nacos.client.naming - [DEREGISTER-SERVICE] public deregistering service DEFAULT_GROUP@@cn.fzzfrjf.entity.HelloService with instance: Instance{instanceId='null', ip='activate.navicat.com', port=9999, weight=1.0, healthy=true, enabled=true, ephemeral=true, clusterName='DEFAULT', serviceName='null', metadata={}}
[Thread-7] INFO cn.fzzfrjf.utils.NacosUtils - 成功注销cn.fzzfrjf.entity.HelloService服务
[Thread-7] INFO cn.fzzfrjf.utils.ThreadPoolFactory - 关闭所有线程。。。。
[Thread-7] INFO cn.fzzfrjf.utils.ThreadPoolFactory - 关闭线程池[shutdown-hook-pool][true]
[Thread-7] INFO cn.fzzfrjf.utils.ThreadPoolFactory - 关闭线程池[netty-server-handler][true]
[main] WARN com.alibaba.nacos.client.utils.LogUtils - Load Log4j Configuration of Nacos fail, message: org/apache/logging/log4j/LogManager
[main] INFO com.alibaba.nacos.client.naming - initializer namespace from System Property :null
[main] INFO com.alibaba.nacos.client.naming - initializer namespace from System Environment :null
[main] INFO com.alibaba.nacos.client.naming - initializer namespace from System Property :null
[main] INFO com.alibaba.nacos.client.utils.ParamUtil - [settings] [req-serv] nacos-server port:8848
[main] INFO com.alibaba.nacos.client.utils.ParamUtil - [settings] [http-client] connect timeout:1000
[main] INFO com.alibaba.nacos.client.utils.ParamUtil - NACOS_CLIENT_VERSION: ${project.version}
[main] INFO com.alibaba.nacos.client.utils.ParamUtil - PER_TASK_CONFIG_SIZE: 3000.0
[main] INFO com.alibaba.nacos.client.identify.CredentialWatcher - null No credential found
[main] INFO com.alibaba.nacos.client.naming - new ips(1) service: DEFAULT_GROUP@@cn.fzzfrjf.entity.HelloService -> [{"instanceId":"activate.navicat.com#9999#DEFAULT#DEFAULT_GROUP@@cn.fzzfrjf.entity.HelloService","ip":"activate.navicat.com","port":9999,"weight":1.0,"healthy":true,"enabled":true,"ephemeral":true,"clusterName":"DEFAULT","serviceName":"DEFAULT_GROUP@@cn.fzzfrjf.entity.HelloService","metadata":{},"instanceHeartBeatInterval":5000,"instanceIdGenerator":"simple","ipDeleteTimeout":30000,"instanceHeartBeatTimeOut":15000}]
[main] INFO com.alibaba.nacos.client.naming - current ips:(1) service: DEFAULT_GROUP@@cn.fzzfrjf.entity.HelloService -> [{"instanceId":"activate.navicat.com#9999#DEFAULT#DEFAULT_GROUP@@cn.fzzfrjf.entity.HelloService","ip":"activate.navicat.com","port":9999,"weight":1.0,"healthy":true,"enabled":true,"ephemeral":true,"clusterName":"DEFAULT","serviceName":"DEFAULT_GROUP@@cn.fzzfrjf.entity.HelloService","metadata":{},"instanceHeartBeatInterval":5000,"instanceIdGenerator":"simple","ipDeleteTimeout":30000,"instanceHeartBeatTimeOut":15000}]
[nioEventLoopGroup-2-1] INFO cn.fzzfrjf.core.ChannelProvider - 获取channel连接成功,连接到服务器activate.navicat.com:9999
[nioEventLoopGroup-2-1] INFO cn.fzzfrjf.core.NettyClient - 成功向服务器发送请求:cn.fzzfrjf.entity.RpcRequest@179f45b1
[nioEventLoopGroup-2-1] INFO cn.fzzfrjf.core.NettyClientHandler - 客户端获取到服务端返回的信息:RpcResponse(code=200, requestId=13fcd293-edf4-474c-904c-f8b5bbc1bf27, data=这是id为:2发送的:This is NettyClient!)
[main] INFO com.alibaba.nacos.client.naming - new ips(1) service: DEFAULT_GROUP@@cn.fzzfrjf.entity.ByeService -> [{"instanceId":"activate.navicat.com#9999#DEFAULT#DEFAULT_GROUP@@cn.fzzfrjf.entity.ByeService","ip":"activate.navicat.com","port":9999,"weight":1.0,"healthy":true,"enabled":true,"ephemeral":true,"clusterName":"DEFAULT","serviceName":"DEFAULT_GROUP@@cn.fzzfrjf.entity.ByeService","metadata":{},"instanceHeartBeatInterval":5000,"instanceIdGenerator":"simple","ipDeleteTimeout":30000,"instanceHeartBeatTimeOut":15000}]
[main] INFO com.alibaba.nacos.client.naming - current ips:(1) service: DEFAULT_GROUP@@cn.fzzfrjf.entity.ByeService -> [{"instanceId":"activate.navicat.com#9999#DEFAULT#DEFAULT_GROUP@@cn.fzzfrjf.entity.ByeService","ip":"activate.navicat.com","port":9999,"weight":1.0,"healthy":true,"enabled":true,"ephemeral":true,"clusterName":"DEFAULT","serviceName":"DEFAULT_GROUP@@cn.fzzfrjf.entity.ByeService","metadata":{},"instanceHeartBeatInterval":5000,"instanceIdGenerator":"simple","ipDeleteTimeout":30000,"instanceHeartBeatTimeOut":15000}]
[nioEventLoopGroup-2-1] INFO cn.fzzfrjf.core.NettyClient - 成功向服务器发送请求:cn.fzzfrjf.entity.RpcRequest@25f5f97e
[nioEventLoopGroup-2-1] INFO cn.fzzfrjf.core.NettyClientHandler - 客户端获取到服务端返回的信息:RpcResponse(code=200, requestId=7e17ec7a-814e-4188-a7de-1656261904a9, data=(This is NettyClient!),bye!)
这是id为:2发送的:This is NettyClient!
(This is NettyClient!),bye!
[nioEventLoopGroup-2-1] INFO cn.fzzfrjf.core.NettyClientHandler - 向服务端:activate.navicat.com/127.0.0.1:9999发送心跳包:cn.fzzfrjf.entity.RpcRequest@3ab5c0bc
[nioEventLoopGroup-2-1] INFO cn.fzzfrjf.core.NettyClientHandler - 向服务端:activate.navicat.com/127.0.0.1:9999发送心跳包:cn.fzzfrjf.entity.RpcRequest@65f51052
功能完成,由于博主目前对于Netty的掌握还不够,因此对于此简易框架的后续完善可能短期内不会拓展,更多的拓展留到更远的将来进行!