springboot工程中使用tcp协议

一、概述

在上文JAVA TCP协议初体验 中,我们使用java实现了tcp协议的一个雏形,实际中大部分项目都已采用springboot,那么,怎么在springboot中整合tcp协议呢?如何实现服务器controller通过tcp协议下发命令到tcp client执行,并且在controller中获取执行结果?

二、实现思路

为了方便演示,本文我们将TcpClient、TcpServer放在同一工程,具体做法为:

  1. 拷贝之前文章的TcpClient、TcpServer代码
  2. 提取服务器、客户端启动代码,使用@PostConstruct注解修饰
  3. 开发controller,定义客户端发送消息、服务器发送消息

三、代码结构

在这里插入图片描述

四、代码分析

1. 服务端发送命令

参考DemoController L67-68,这边通过封装命令对象,转换为字符串,发送到客户端。

封装命令
转换为字符串
发送到客户端
 Command command = new Command().setClientId(initManager.getClient().getClientName()).setText(msg);
 initManager.getServer().sendMsg(initManager.getClient().getClientName(), JsonBeanUtils.beanToJson(command));

2. 客户端处理

if (msg.contains("commandId"))
{
    Command cmd = JsonBeanUtils.jsonToBean(msg, Command.class);
    sendMsg("hello Command!" + JsonBeanUtils.beanToJson(cmd));
}

3. 结果回调

这边使用了观察者模式

定义可观察对象
添加观察者
观察者业务处理

定义可观察对象

 class NewClient extends Observable 

添加观察者

// 注册观察者
addObserver(SpringContextUtils.getBean(ResultCallBack.class));

观察者业务处理

/**
 * 结果回调处理
 */
@Slf4j
@Service
public class ResultCallBack implements Observer
{
    Map<String, String> result = new ConcurrentHashMap<>();
    
    ExecutorService executorService = Executors.newFixedThreadPool(10);
    
    /**
     * 观察者接受数据处理
     */
    @Override
    public void update(Observable observable, Object msg)
    {
        log.info("### accept #### {}", msg);
        try
        {
            if (msg instanceof String)
            {
                String json = StringUtils.substringAfter((String)msg, "hello Command!");
                Command command = JsonBeanUtils.jsonToBean(json, Command.class);
                log.info("{}", command);
                result.put(command.getCommandId(), json);
            }
        }
        catch (IOException e)
        {
            log.error(e.getMessage());
        }
    }
    。。。

4. 接口获取处理结果

DemoController 这里使用了3种不同的方式处理,大家可以比较处理的异同

   
    /**
     * 客户端业务处理结果更多是通过回调实现的
     * 
     * @param cmd
     * @return
     * @throws IOException
     */
    @ApiOperation("server发消息Callable")
    @ApiImplicitParam(name = "msg", example = "千树万树梨花开", required = true)
    @PostMapping("/server/sendMsg/callable")
    public Callable<JsonResult<?>> sendMsgFromServer01(String msg)
        throws IOException
    {
        return () -> {
            Command command = new Command().setClientId(initManager.getClient().getClientName()).setText(msg);
            initManager.getServer().sendMsg(initManager.getClient().getClientName(), JsonBeanUtils.beanToJson(command));
            String result;
            for (int i = 0; i < 50; i++)
            {
                result = resultCallBack.queryResult(command.getCommandId());
                if (StringUtils.isNotBlank(result))
                {
                    return JsonResult.success(result, "获取处理结果成功");
                }
                else
                {
                    TimeUnit.MILLISECONDS.sleep(100);
                }
            }
            return JsonResult.error("响应超时,请重试");
        };
    }
    
    /**
     * 客户端业务处理结果更多是通过回调实现的(WebAsyncTask升级版callable,增加超时异常等处理)
     * 
     * @param cmd
     * @return
     * @throws IOException
     */
    @ApiOperation("server发消息webAsyncTask")
    @ApiImplicitParam(name = "msg", example = "千树万树梨花开", required = true)
    @PostMapping("/server/sendMsg/webAsyncTask")
    public WebAsyncTask<String> sendMsgFromServer02(String msg)
        throws IOException
    {
        WebAsyncTask<String> webAsyncTask = new WebAsyncTask<String>(2000L, executor, () -> {
            Command command = new Command().setClientId(initManager.getClient().getClientName()).setText(msg);
            initManager.getServer().sendMsg(initManager.getClient().getClientName(), JsonBeanUtils.beanToJson(command));
            String result;
            for (int i = 0; i < 100; i++)
            {
                result = resultCallBack.queryResult(command.getCommandId());
                if (StringUtils.isNotBlank(result))
                {
                    return result;
                }
                else
                {
                    TimeUnit.MILLISECONDS.sleep(50);
                }
            }
            return "响应超时,请重试";
        });
        webAsyncTask.onCompletion(() -> log.info("调用完成"));
        webAsyncTask.onError(() -> {
            log.error("业务处理出错");
            return "error";
        });
        webAsyncTask.onTimeout(() -> {
            log.info("业务处理超时");
            return "Time Out";
        });
        return webAsyncTask;
    }
    
    /**
     * 客户端业务处理结果更多是通过回调实现的
     * 
     * @param cmd
     * @return
     * @throws IOException
     * @throws TimeoutException
     * @throws ExecutionException
     * @throws InterruptedException
     */
    @ApiOperation("server发消息")
    @ApiImplicitParam(name = "msg", example = "千树万树梨花开", required = true)
    @PostMapping("/server/sendMsg")
    public DeferredResult<String> sendMsgFromServer(String msg)
        throws IOException, InterruptedException, ExecutionException, TimeoutException
    {
        String clientName = initManager.getClient().getClientName();
        Command command = new Command().setClientId(clientName).setText(msg);
        initManager.getServer().sendMsg(clientName, JsonBeanUtils.beanToJson(command));
        
        // 异步返回结果
        DeferredResult<String> deferredResult = new DeferredResult<>(20000L, "失败");
        deferredResult.onCompletion(() -> log.info("调用完成"));
        deferredResult.onTimeout(() -> {
            log.info("调用超时");
            deferredResult.setResult("调用超时");
        });
        resultCallBack.processResult(deferredResult, command.getCommandId());
        return deferredResult;
    }

五、代码放送

https://gitcode.com/00fly/tcp-show/tree/main/springboot-tcp

或者使用下面的备份文件恢复成原始的项目代码

如何恢复,请移步查阅:神奇代码恢复工具

//goto docker\docker-compose.yml
version: '3.7'
services:
  springboot-tcp:
    image: registry.cn-shanghai.aliyuncs.com/00fly/springboot-tcp:1.0.0
    container_name: springboot-tcp
    deploy:
      resources:
        limits:
          cpus: '1.0'
          memory: 200M
        reservations:
          cpus: '0.05'
          memory: 200M
    ports:
    - 8080:8081
    restart: on-failure
    logging:
      driver: json-file
      options:
        max-size: '5m'
        max-file: '1'
//goto docker\restart.sh
#!/bin/bash
docker-compose down && docker-compose up -d && docker stats
//goto docker\stop.sh
#!/bin/bash
docker-compose down
//goto docker\wait-for.sh
#!/bin/sh

TIMEOUT=15
QUIET=0

echoerr() {
  if [ "$QUIET" -ne 1 ]; then printf "%s\n" "$*" 1>&2; fi
}

usage() {
  exitcode="$1"
  cat << USAGE >&2
Usage:
  $cmdname host:port [-t timeout] [-- command args]
  -q | --quiet                        Do not output any status messages
  -t TIMEOUT | --timeout=timeout      Timeout in seconds, zero for no timeout
  -- COMMAND ARGS                     Execute command with args after the test finishes
USAGE
  exit "$exitcode"
}

wait_for() {
  for i in `seq $TIMEOUT` ; do
    nc -z "$HOST" "$PORT" > /dev/null 2>&1

    result=$?
    if [ $result -eq 0 ] ; then
      if [ $# -gt 0 ] ; then
        exec "$@"
      fi
      exit 0
    fi
    sleep 1
  done
  echo "Operation timed out" >&2
  exit 1
}

while [ $# -gt 0 ]
do
  case "$1" in
    *:* )
    HOST=$(printf "%s\n" "$1"| cut -d : -f 1)
    PORT=$(printf "%s\n" "$1"| cut -d : -f 2)
    shift 1
    ;;
    -q | --quiet)
    QUIET=1
    shift 1
    ;;
    -t)
    TIMEOUT="$2"
    if [ "$TIMEOUT" = "" ]; then break; fi
    shift 2
    ;;
    --timeout=*)
    TIMEOUT="${1#*=}"
    shift 1
    ;;
    --)
    shift
    break
    ;;
    --help)
    usage 0
    ;;
    *)
    echoerr "Unknown argument: $1"
    usage 1
    ;;
  esac
done

if [ "$HOST" = "" -o "$PORT" = "" ]; then
  echoerr "Error: you need to provide a host and port to test."
  usage 2
fi

wait_for "$@"
//goto Dockerfile
#基础镜像
FROM adoptopenjdk/openjdk8-openj9:alpine-slim

COPY docker/wait-for.sh /
RUN chmod +x /wait-for.sh && \
    ln -sf /usr/share/zoneinfo/Asia/Shanghai /etc/localtime && \
    echo 'Asia/Shanghai' >/etc/timezone

#引入运行包
COPY target/*.jar /app.jar

#指定交互端口
EXPOSE 8081

CMD ["--server.port=8081"]

#项目的启动方式
ENTRYPOINT ["java", "-Djava.security.egd=file:/dev/./urandom", "-Xshareclasses", "-Xquickstart", "-jar", "/app.jar"]
//goto pom.xml
<?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>com.fly</groupId>
	<artifactId>springboot-tcp</artifactId>
	<version>1.0.0</version>
	<packaging>jar</packaging>

	<parent>
		<groupId>org.springframework.boot</groupId>
		<artifactId>spring-boot-starter-parent</artifactId>
		<version>2.2.4.RELEASE</version>
		<relativePath />
	</parent>

	<properties>
		<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
		<docker.hub>registry.cn-shanghai.aliyuncs.com</docker.hub>
		<java.version>1.8</java.version>
		<skipTests>true</skipTests>
	</properties>

	<dependencies>
		<!-- Compile -->
		<dependency>
			<groupId>org.springframework.boot</groupId>
			<artifactId>spring-boot-starter-web</artifactId>
			<exclusions>
				<exclusion>
					<groupId>org.springframework.boot</groupId>
					<artifactId>spring-boot-starter-tomcat</artifactId>
				</exclusion>
				<exclusion>
					<groupId>org.springframework.boot</groupId>
					<artifactId>spring-boot-starter-logging</artifactId>
				</exclusion>
			</exclusions>
		</dependency>
		<dependency>
			<groupId>org.springframework.boot</groupId>
			<artifactId>spring-boot-starter-log4j2</artifactId>
		</dependency>
		<dependency>
			<groupId>org.springframework.boot</groupId>
			<artifactId>spring-boot-starter-webflux</artifactId>
		</dependency>
		<dependency>
			<groupId>org.springframework.boot</groupId>
			<artifactId>spring-boot-starter-jetty</artifactId>
		</dependency>
		<dependency>
			<groupId>com.github.xiaoymin</groupId>
			<artifactId>knife4j-spring-boot-starter</artifactId>
			<version>2.0.8</version>
		</dependency>

		<!-- 异步日志,需要加入disruptor依赖 -->
		<dependency>
			<groupId>com.lmax</groupId>
			<artifactId>disruptor</artifactId>
			<version>3.4.2</version>
		</dependency>

		<dependency>
			<groupId>commons-io</groupId>
			<artifactId>commons-io</artifactId>
			<version>2.5</version>
		</dependency>
		<dependency>
			<groupId>org.apache.commons</groupId>
			<artifactId>commons-lang3</artifactId>
		</dependency>

		<!-- Test -->
		<dependency>
			<groupId>org.springframework.boot</groupId>
			<artifactId>spring-boot-starter-test</artifactId>
			<scope>test</scope>
			<exclusions>
				<exclusion>
					<groupId>org.junit.vintage</groupId>
					<artifactId>junit-vintage-engine</artifactId>
				</exclusion>
			</exclusions>
		</dependency>
		<dependency>
			<groupId>org.projectlombok</groupId>
			<artifactId>lombok</artifactId>
			<scope>provided</scope>
		</dependency>
		<dependency>
			<groupId>org.springframework.boot</groupId>
			<artifactId>spring-boot-devtools</artifactId>
			<optional>true</optional>
		</dependency>
	</dependencies>

	<!-- 阿里云maven仓库 -->
	<repositories>
		<repository>
			<id>public</id>
			<name>aliyun nexus</name>
			<url>https://maven.aliyun.com/repository/public/</url>
			<releases>
				<enabled>true</enabled>
			</releases>
		</repository>
	</repositories>
	<pluginRepositories>
		<pluginRepository>
			<id>public</id>
			<name>aliyun nexus</name>
			<url>https://maven.aliyun.com/repository/public/</url>
			<releases>
				<enabled>true</enabled>
			</releases>
			<snapshots>
				<enabled>false</enabled>
			</snapshots>
		</pluginRepository>
	</pluginRepositories>

	<build>
		<finalName>${project.artifactId}</finalName>
		<plugins>
			<plugin>
				<groupId>org.springframework.boot</groupId>
				<artifactId>spring-boot-maven-plugin</artifactId>
			</plugin>

			<!-- 添加docker-maven插件 -->
			<plugin>
				<groupId>io.fabric8</groupId>
				<artifactId>docker-maven-plugin</artifactId>
				<version>0.40.3</version>
				<executions>
					<execution>
						<phase>package</phase>
						<goals>
							<goal>build</goal>
							<!--<goal>push</goal>-->
							<!--<goal>remove</goal>-->
						</goals>
					</execution>
				</executions>
				<configuration>
					<!-- 连接到带docker环境的linux服务器编译image -->
					<!-- <dockerHost>http://192.168.182.10:2375</dockerHost> -->

					<!-- Docker 推送镜像仓库地址 -->
					<pushRegistry>${docker.hub}</pushRegistry>
					<images>
						<image>
							<name>
								${docker.hub}/00fly/${project.artifactId}:${project.version}</name>
							<build>
								<dockerFileDir>${project.basedir}</dockerFileDir>
							</build>
						</image>
					</images>
				</configuration>
			</plugin>
		</plugins>
		<resources>
			<resource>
				<directory>src/main/java</directory>
				<excludes>
					<exclude>**/*.java</exclude>
				</excludes>
			</resource>
			<resource>
				<directory>src/main/resources</directory>
				<includes>
					<include>**/**</include>
				</includes>
				<filtering>false</filtering>
			</resource>
		</resources>
	</build>
</project>
//goto src\main\java\com\fly\BootApplication.java
package com.fly;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;

@SpringBootApplication
public class BootApplication
{
    public static void main(String[] args)
    {
        SpringApplication.run(BootApplication.class, args);
    }
}
//goto src\main\java\com\fly\core\config\AsyncThreadPoolConfig.java
package com.fly.core.config;

import java.util.concurrent.Executor;
import java.util.concurrent.ThreadPoolExecutor;

import org.springframework.aop.interceptor.AsyncUncaughtExceptionHandler;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.scheduling.annotation.AsyncConfigurer;
import org.springframework.scheduling.annotation.EnableAsync;
import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor;

import lombok.extern.slf4j.Slf4j;

/**
 * 
 * 异步线程池配置
 * 
 * @author 00fly
 * @version [版本号, 2023年10月22日]
 * @see [相关类/方法]
 * @since [产品/模块版本]
 */
@Slf4j
@Configuration
@EnableAsync
public class AsyncThreadPoolConfig implements AsyncConfigurer
{
    @Bean
    ThreadPoolTaskExecutor taskExecutor()
    {
        int processors = Runtime.getRuntime().availableProcessors();
        ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
        executor.setCorePoolSize(Math.max(processors, 5));
        executor.setMaxPoolSize(Math.max(processors, 5) * 2);
        executor.setQueueCapacity(10000);
        executor.setKeepAliveSeconds(60);
        executor.setThreadNamePrefix("asyncTask-");
        
        // ThreadPoolExecutor类有几个内部实现类来处理这类情况:
        // AbortPolicy 丢弃任务,抛运行时异常
        // CallerRunsPolicy 执行任务
        // DiscardPolicy 忽视,什么都不会发生
        // DiscardOldestPolicy 从队列中踢出最先进入队列(最后一个执行)的任务
        executor.setRejectedExecutionHandler(new ThreadPoolExecutor.DiscardPolicy());
        return executor;
    }
    
    @Override
    public Executor getAsyncExecutor()
    {
        return taskExecutor();
    }
    
    @Override
    public AsyncUncaughtExceptionHandler getAsyncUncaughtExceptionHandler()
    {
        return (ex, method, params) -> {
            log.info("Exception message - {}", ex.getMessage());
            log.info("Method name - {}", method.getName());
            for (Object param : params)
            {
                log.info("Parameter value - {}", param);
            }
        };
    }
}
//goto src\main\java\com\fly\core\config\Knife4jConfig.java
package com.fly.core.config;

import java.util.Collections;
import java.util.List;

import org.springframework.boot.autoconfigure.condition.ConditionalOnWebApplication;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Import;

import com.github.xiaoymin.knife4j.spring.annotations.EnableKnife4j;

import io.swagger.annotations.ApiOperation;
import springfox.bean.validators.configuration.BeanValidatorPluginsConfiguration;
import springfox.documentation.builders.ApiInfoBuilder;
import springfox.documentation.builders.PathSelectors;
import springfox.documentation.builders.RequestHandlerSelectors;
import springfox.documentation.service.ApiInfo;
import springfox.documentation.service.ApiKey;
import springfox.documentation.spi.DocumentationType;
import springfox.documentation.spring.web.plugins.Docket;
import springfox.documentation.swagger2.annotations.EnableSwagger2WebMvc;

/**
 * Knife4jConfig
 *
 */
@EnableKnife4j
@Configuration
@EnableSwagger2WebMvc
@ConditionalOnWebApplication
@Import(BeanValidatorPluginsConfiguration.class)
public class Knife4jConfig
{
    /**
     * 开发、测试环境接口文档打开
     * 
     * @return
     * @see [类、类#方法、类#成员]
     */
    @Bean
    Docket createRestApi()
    {
        return new Docket(DocumentationType.SWAGGER_2).apiInfo(apiInfo())
            .enable(true)
            .select()
            .apis(RequestHandlerSelectors.withMethodAnnotation(ApiOperation.class))
            .paths(PathSelectors.any()) // 包下的类,生成接口文档
            .build()
            .securitySchemes(security());
    }
    
    private ApiInfo apiInfo()
    {
        return new ApiInfoBuilder().title("数据接口API").description("接口文档").termsOfServiceUrl("http://00fly.online/").version("1.0.0").build();
    }
    
    private List<ApiKey> security()
    {
        return Collections.singletonList(new ApiKey("token", "token", "header"));
    }
}
//goto src\main\java\com\fly\core\config\WebClientConfig.java
package com.fly.core.config;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.reactive.function.client.WebClient;

/**
 * 配置WebClient
 */
@Configuration
public class WebClientConfig
{
    @Bean
    WebClient webClient()
    {
        return WebClient.builder().codecs(configurer -> configurer.defaultCodecs().maxInMemorySize(-1)).build();
    }
}
//goto src\main\java\com\fly\core\config\WebMvcConfig.java
package com.fly.core.config;

import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;

import org.springframework.boot.autoconfigure.condition.ConditionalOnWebApplication;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.MediaType;
import org.springframework.http.converter.HttpMessageConverter;
import org.springframework.http.converter.StringHttpMessageConverter;
import org.springframework.http.converter.json.MappingJackson2HttpMessageConverter;
import org.springframework.web.servlet.config.annotation.ContentNegotiationConfigurer;
import org.springframework.web.servlet.config.annotation.ViewControllerRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;

/**
 * 
 * mvc配置
 * 
 * @author 00fly
 * @version [版本号, 2021年4月23日]
 * @see [相关类/方法]
 * @since [产品/模块版本]
 */
@Configuration
@ConditionalOnWebApplication
public class WebMvcConfig implements WebMvcConfigurer
{
    @Override
    public void configureMessageConverters(final List<HttpMessageConverter<?>> converters)
    {
        converters.add(stringHttpMessageConverter());
        converters.add(mappingJackson2HttpMessageConverter());
    }
    
    @Override
    public void configureContentNegotiation(final ContentNegotiationConfigurer configurer)
    {
        configurer.defaultContentType(MediaType.APPLICATION_JSON);
        configurer.ignoreUnknownPathExtensions(false);
        configurer.favorPathExtension(true);
        configurer.favorParameter(false);
        final Map<String, MediaType> mediaTypes = new ConcurrentHashMap<>(3);
        mediaTypes.put("atom", MediaType.APPLICATION_ATOM_XML);
        mediaTypes.put("html", MediaType.TEXT_HTML);
        mediaTypes.put("json", MediaType.APPLICATION_JSON);
        configurer.mediaTypes(mediaTypes);
    }
    
    @Bean
    StringHttpMessageConverter stringHttpMessageConverter()
    {
        return new StringHttpMessageConverter();
    }
    
    @Bean
    MappingJackson2HttpMessageConverter mappingJackson2HttpMessageConverter()
    {
        final MappingJackson2HttpMessageConverter messageConverter = new MappingJackson2HttpMessageConverter();
        final List<MediaType> list = new ArrayList<>();
        list.add(MediaType.APPLICATION_JSON);
        list.add(MediaType.APPLICATION_XML);
        list.add(MediaType.TEXT_PLAIN);
        list.add(MediaType.TEXT_HTML);
        list.add(MediaType.TEXT_XML);
        messageConverter.setSupportedMediaTypes(list);
        return messageConverter;
    }
    
    /**
     * 等价于mvc中<mvc:view-controller path="/" view-name="redirect:index" /><br>
     * 等价于mvc中<mvc:view-controller path="/index" view-name="index.html" />
     * 
     * @param registry
     */
    @Override
    public void addViewControllers(final ViewControllerRegistry registry)
    {
        registry.addViewController("/").setViewName("redirect:index");
        registry.addViewController("/index").setViewName("index.html");
    }
}
//goto src\main\java\com\fly\core\exception\GlobalExceptionHandler.java
package com.fly.core.exception;

import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.stream.Collectors;

import org.apache.commons.lang3.StringUtils;
import org.springframework.validation.BindException;
import org.springframework.validation.BindingResult;
import org.springframework.web.bind.MethodArgumentNotValidException;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.RestControllerAdvice;

import com.fly.core.JsonResult;

import lombok.extern.slf4j.Slf4j;

/**
 * 统一异常处理器
 * 
 * @author 00fly
 * @version [版本号, 2018-09-11]
 * @see [相关类/方法]
 * @since [产品/模块版本]
 */
@Slf4j
@RestControllerAdvice
public class GlobalExceptionHandler
{
    @ExceptionHandler(Exception.class)
    public JsonResult<?> handleBadRequest(Exception exception)
    {
        // JSR303参数校验异常
        if (exception instanceof BindException)
        {
            BindingResult bindingResult = ((BindException)exception).getBindingResult();
            if (null != bindingResult && bindingResult.hasErrors())
            {
                List<String> errMsg = new ArrayList<>();
                bindingResult.getFieldErrors().stream().forEach(fieldError -> {
                    errMsg.add(fieldError.getDefaultMessage());
                });
                Collections.sort(errMsg);
                return JsonResult.error(StringUtils.join(errMsg, ","));
            }
        }
        if (exception instanceof MethodArgumentNotValidException)
        {
            BindingResult bindingResult = ((MethodArgumentNotValidException)exception).getBindingResult();
            if (null != bindingResult && bindingResult.hasErrors())
            {
                // stream写法优化
                return JsonResult.error(bindingResult.getFieldErrors().stream().map(e -> e.getDefaultMessage()).sorted().collect(Collectors.joining(",")));
            }
        }
        
        // 其余情况
        log.error("Error: handleBadRequest StackTrace : {}", exception);
        return JsonResult.error(StringUtils.defaultString(exception.getMessage(), "系统异常,请联系管理员"));
    }
}
//goto src\main\java\com\fly\core\JsonResult.java
package com.fly.core;

import io.swagger.annotations.ApiModel;
import io.swagger.annotations.ApiModelProperty;
import lombok.Data;
import lombok.experimental.Accessors;

/**
 * 
 * 结果对象
 * 
 * @author 00fly
 * @version [版本号, 2021年5月2日]
 * @see [相关类/方法]
 * @since [产品/模块版本]
 */
@Data
@Accessors(chain = true)
@ApiModel(description = "Json格式消息体")
public class JsonResult<T>
{
    @ApiModelProperty(value = "数据对象")
    private T data;
    
    @ApiModelProperty(value = "是否成功", required = true, example = "true")
    private boolean success;
    
    @ApiModelProperty(value = "错误码")
    private String errorCode;
    
    @ApiModelProperty(value = "提示信息")
    private String message;
    
    public JsonResult()
    {
        super();
    }
    
    public static <T> JsonResult<T> success(T data)
    {
        JsonResult<T> r = new JsonResult<>();
        r.setData(data);
        r.setSuccess(true);
        return r;
    }
    
    public static <T> JsonResult<T> success(T data, String msg)
    {
        JsonResult<T> r = new JsonResult<>();
        r.setData(data);
        r.setMessage(msg);
        r.setSuccess(true);
        return r;
    }
    
    public static JsonResult<?> success()
    {
        JsonResult<Object> r = new JsonResult<>();
        r.setSuccess(true);
        return r;
    }
    
    public static JsonResult<Object> error(String code, String msg)
    {
        JsonResult<Object> r = new JsonResult<>();
        r.setSuccess(false);
        r.setErrorCode(code);
        r.setMessage(msg);
        return r;
    }
    
    public static JsonResult<Object> error(String msg)
    {
        return error("500", msg);
    }
}
//goto src\main\java\com\fly\core\runner\WebStartedRunner.java
package com.fly.core.runner;

import org.apache.commons.lang3.StringUtils;
import org.apache.commons.lang3.SystemUtils;
import org.springframework.boot.CommandLineRunner;
import org.springframework.boot.autoconfigure.condition.ConditionalOnWebApplication;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.stereotype.Component;

import com.fly.core.utils.SpringContextUtils;

@Component
@Configuration
@ConditionalOnWebApplication
public class WebStartedRunner
{
    @Bean
    @ConditionalOnWebApplication
    CommandLineRunner init()
    {
        return args -> {
            if (SystemUtils.IS_OS_WINDOWS)// 防止非windows系统报错,启动失败
            {
                String url = SpringContextUtils.getServerBaseURL();
                if (StringUtils.containsNone(url, "-")) // junit port:-1
                {
                    Runtime.getRuntime().exec("cmd /c start /min " + url);
                    Runtime.getRuntime().exec("cmd /c start /min " + url + "/doc.html");
                }
            }
        };
    }
}
//goto src\main\java\com\fly\core\utils\JsonBeanUtils.java
package com.fly.core.utils;

import java.io.IOException;

import com.fasterxml.jackson.core.type.TypeReference;
import com.fasterxml.jackson.databind.DeserializationFeature;
import com.fasterxml.jackson.databind.JavaType;
import com.fasterxml.jackson.databind.ObjectMapper;

/**
 * JsonBean转换工具
 * 
 * @author 00fly
 *
 */
public class JsonBeanUtils
{
    private static ObjectMapper objectMapper = new ObjectMapper();
    
    /**
     * bean转json字符串
     * 
     * @param bean
     * @return
     * @throws IOException
     */
    public static String beanToJson(Object bean)
        throws IOException
    {
        return beanToJson(bean, false);
    }
    
    /**
     * bean转json字符串
     * 
     * @param bean
     * @param pretty 是否格式美化
     * @return
     * @throws IOException
     */
    public static String beanToJson(Object bean, boolean pretty)
        throws IOException
    {
        String jsonText = objectMapper.writeValueAsString(bean);
        if (pretty)
        {
            return objectMapper.readTree(jsonText).toPrettyString();
        }
        return objectMapper.readTree(jsonText).toString();
    }
    
    /**
     * json字符串转bean
     * 
     * @param jsonText
     * @return
     * @throws IOException
     */
    public static <T> T jsonToBean(String jsonText, Class<T> clazz)
        throws IOException
    {
        objectMapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, true);
        return objectMapper.readValue(jsonText, clazz);
    }
    
    /**
     * json字符串转bean
     * 
     * @param jsonText
     * @param clazz
     * @param ingoreError 是否忽略无法识别字段
     * @return
     * @throws IOException
     */
    public static <T> T jsonToBean(String jsonText, Class<T> clazz, boolean ingoreError)
        throws IOException
    {
        objectMapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, !ingoreError);
        return objectMapper.readValue(jsonText, clazz);
    }
    
    /**
     * json字符串转bean
     * 
     * @param jsonText
     * @return
     * @throws IOException
     */
    public static <T> T jsonToBean(String jsonText, JavaType javaType)
        throws IOException
    {
        return objectMapper.readValue(jsonText, javaType);
    }
    
    /**
     * json字符串转bean
     * 
     * @param jsonText
     * @return
     * @throws IOException
     */
    public static <T> T jsonToBean(String jsonText, TypeReference<T> typeRef)
        throws IOException
    {
        return objectMapper.readValue(jsonText, typeRef);
    }
}
//goto src\main\java\com\fly\core\utils\SpringContextUtils.java
package com.fly.core.utils;

import java.net.InetAddress;
import java.net.UnknownHostException;

import javax.servlet.ServletContext;

import org.apache.commons.lang3.StringUtils;
import org.springframework.beans.BeansException;
import org.springframework.context.ApplicationContext;
import org.springframework.context.ApplicationContextAware;
import org.springframework.stereotype.Component;
import org.springframework.util.Assert;

import lombok.extern.slf4j.Slf4j;

/**
 * Spring Context 工具类
 */
@Slf4j
@Component
public class SpringContextUtils implements ApplicationContextAware
{
    private static ApplicationContext applicationContext;
    
    private static String SERVER_BASE_URL = null;
    
    @Override
    public void setApplicationContext(ApplicationContext applicationContext)
        throws BeansException
    {
        log.info("###### execute setApplicationContext ######");
        SpringContextUtils.applicationContext = applicationContext;
    }
    
    public static ApplicationContext getApplicationContext()
    {
        return applicationContext;
    }
    
    public static <T> T getBean(Class<T> clazz)
    {
        Assert.notNull(applicationContext, "applicationContext is null");
        return applicationContext.getBean(clazz);
    }
    
    /**
     * execute @PostConstruct May be SpringContextUtils not inited, throw NullPointerException
     * 
     * @return
     */
    public static String getActiveProfile()
    {
        Assert.notNull(applicationContext, "applicationContext is null");
        String[] profiles = applicationContext.getEnvironment().getActiveProfiles();
        return StringUtils.join(profiles, ",");
    }
    
    /**
     * can use in @PostConstruct
     * 
     * @param context
     * @return
     */
    public static String getActiveProfile(ApplicationContext context)
    {
        Assert.notNull(context, "context is null");
        String[] profiles = context.getEnvironment().getActiveProfiles();
        return StringUtils.join(profiles, ",");
    }
    
    /**
     * get web服务基准地址,一般为 http://${ip}:${port}/${contentPath}
     * 
     * @return
     * @throws UnknownHostException
     * @see [类、类#方法、类#成员]
     */
    public static String getServerBaseURL()
        throws UnknownHostException
    {
        ServletContext servletContext = getBean(ServletContext.class);
        Assert.notNull(servletContext, "servletContext is null");
        if (SERVER_BASE_URL == null)
        {
            String ip = InetAddress.getLocalHost().getHostAddress();
            SERVER_BASE_URL = "http://" + ip + ":" + getProperty("server.port") + servletContext.getContextPath();
        }
        return SERVER_BASE_URL;
    }
    
    /**
     * getProperty
     * 
     * @param key eg:server.port
     * @return
     * @see [类、类#方法、类#成员]
     */
    public static String getProperty(String key)
    {
        return applicationContext.getEnvironment().getProperty(key, "");
    }
}
//goto src\main\java\com\fly\tcp\base\InitManager.java
package com.fly.tcp.base;

import javax.annotation.PostConstruct;

import org.apache.commons.lang3.StringUtils;
import org.springframework.stereotype.Service;

/**
 * 初始化管理类
 */
@Service
public class InitManager
{
    TcpServer server = new TcpServer();
    
    TcpClient client = new TcpClient("CLIENT_1");
    
    public TcpServer getServer()
    {
        return server;
    }
    
    public TcpClient getClient()
    {
        return client;
    }
    
    /**
     * 启动TcpServer、TcpClient
     */
    @PostConstruct
    private void init()
    {
        if (server.startServer("0.0.0.0", 8000))
        {
            new Thread(server).start();
        }
        // docker环境下优先使用docker-compose中environment值
        String serverIp = StringUtils.defaultIfBlank(System.getenv().get("TCP_SERVER"), "127.0.0.1");
        if (client.connectServer(serverIp, 8000))
        {
            new Thread(client).start();
        }
    }
}
//goto src\main\java\com\fly\tcp\base\TcpClient.java
package com.fly.tcp.base;

import java.io.DataInputStream;
import java.io.DataOutputStream;
import java.io.IOException;
import java.net.InetAddress;
import java.net.Socket;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.TimeUnit;

import com.fly.core.utils.JsonBeanUtils;
import com.fly.tcp.entity.Command;

import lombok.extern.slf4j.Slf4j;

@Slf4j
public class TcpClient implements Runnable
{
    private String ip;
    
    private int port;
    
    private Socket socket;
    
    private DataOutputStream dataOutputStream;
    
    private String clientName;
    
    private boolean isClientCoreRun = false;
    
    private ScheduledExecutorService scheduler = Executors.newScheduledThreadPool(2);
    
    private ExecutorService executor = Executors.newFixedThreadPool(2);
    
    public TcpClient(String clientName)
    {
        super();
        this.clientName = clientName;
    }
    
    public String getClientName()
    {
        return clientName;
    }
    
    /**
     * 
     * @param ip 服务端IP
     * @param port 服务端PORT
     * @return
     */
    public boolean connectServer(String ip, int port)
    {
        try
        {
            this.ip = ip;
            this.port = port;
            socket = new Socket(InetAddress.getByName(ip), port);
            log.info("****** TcpClient will connect to Server {}:{}", ip, port);
            scheduler.scheduleAtFixedRate(this::checkConnection, 0, 10, TimeUnit.SECONDS);
            isClientCoreRun = true;
            dataOutputStream = new DataOutputStream(socket.getOutputStream());
            dataOutputStream.writeUTF(clientName);
            dataOutputStream.flush();
        }
        catch (IOException e)
        {
            log.error(e.getMessage());
            isClientCoreRun = false;
        }
        return isClientCoreRun;
    }
    
    /**
     * 检查TCP连接
     */
    private void checkConnection()
    {
        if (socket == null || socket.isClosed())
        {
            log.error("Connection lost, attempting to reconnect");
            reconnect();
        }
    }
    
    private void reconnect()
    {
        try
        {
            socket = new Socket(InetAddress.getByName(ip), port);
            log.info("****** TcpClient will connect to Server {}:{}", ip, port);
            isClientCoreRun = true;
            executor.execute(new ReceiveMsg());
            dataOutputStream = new DataOutputStream(socket.getOutputStream());
            dataOutputStream.writeUTF(clientName);
            dataOutputStream.flush();
        }
        catch (IOException e)
        {
            log.error(e.getMessage());
            isClientCoreRun = false;
        }
    }
    
    /**
     * 发送报文
     */
    public void sendMsg(String msg)
    {
        try
        {
            dataOutputStream.writeUTF(msg);
            dataOutputStream.flush();
        }
        catch (IOException e)
        {
            log.error(e.getMessage());
            closeClientConnect();
        }
    }
    
    /**
     * 断开客户端与服务端的连接
     */
    public void closeClientConnect()
    {
        if (dataOutputStream != null)
        {
            try
            {
                dataOutputStream.close();
                isClientCoreRun = false;
                if (socket != null)
                {
                    socket.close();
                }
            }
            catch (IOException e)
            {
                log.error(e.getMessage());
            }
        }
    }
    
    @Override
    public void run()
    {
        executor.execute(new ReceiveMsg());
    }
    
    class ReceiveMsg implements Runnable
    {
        private DataInputStream dataInputStream;
        
        public ReceiveMsg()
        {
            try
            {
                // 数据输入流
                dataInputStream = new DataInputStream(socket.getInputStream());
            }
            catch (IOException e)
            {
                log.error(e.getMessage());
            }
        }
        
        @Override
        public void run()
        {
            try
            {
                // server停止后, 会影响接受消息线程工作
                while (isClientCoreRun)
                {
                    String msg = dataInputStream.readUTF();
                    log.info("{} get msg: {}", clientName, msg);
                    if (msg.contains("commandId"))
                    {
                        Command cmd = JsonBeanUtils.jsonToBean(msg, Command.class);
                        sendMsg("hello Command!" + JsonBeanUtils.beanToJson(cmd));
                    }
                }
            }
            catch (IOException e)
            {
                log.error(e.getMessage());
                // 防止重连失败
                closeClientConnect();
            }
        }
    }
}
//goto src\main\java\com\fly\tcp\base\TcpServer.java
package com.fly.tcp.base;

import java.io.DataInputStream;
import java.io.DataOutputStream;
import java.io.IOException;
import java.net.InetSocketAddress;
import java.net.ServerSocket;
import java.net.Socket;
import java.util.HashMap;
import java.util.Map;
import java.util.Observable;

import org.apache.commons.lang3.StringUtils;
import org.apache.commons.lang3.time.DateFormatUtils;

import com.fly.core.utils.SpringContextUtils;
import com.fly.tcp.service.ResultCallBack;

import lombok.extern.slf4j.Slf4j;

@Slf4j
public class TcpServer implements Runnable
{
    private ServerSocket serverSocket;
    
    private boolean isServerCoreRun = false;
    
    private Map<String, NewClient> allClient = new HashMap<>();
    
    public boolean startServer(String ip, int port)
    {
        try
        {
            serverSocket = new ServerSocket();
            serverSocket.bind(new InetSocketAddress(ip, port));
            isServerCoreRun = true;
        }
        catch (IOException e)
        {
            log.error(e.getMessage());
            isServerCoreRun = false;
        }
        return isServerCoreRun;
    }
    
    /**
     * 关闭服务
     *
     * #1 断开与所有客户端的连接,并将客户端容器中的所有已连接的客户端清空。 #2 关闭服务器套接字
     */
    public void closeServer()
    {
        try
        {
            isServerCoreRun = false;
            for (Map.Entry<String, NewClient> all : this.allClient.entrySet())
            {
                all.getValue().isNewClientRun = false;
                all.getValue().socket.close();
            }
            allClient.clear();
            serverSocket.close();
        }
        catch (IOException e)
        {
            log.error(e.getMessage());
        }
    }
    
    /**
     * 向客户端发送报文
     */
    public void sendMsg(String clientName, String msg)
    {
        if (allClient.containsKey(clientName))
        {
            allClient.get(clientName).sendMsg(msg);
        }
    }
    
    @Override
    public void run()
    {
        try
        {
            log.info("TcpServer will start");
            while (isServerCoreRun)
            {
                // 阻塞式等待客户端连接
                Socket socket = serverSocket.accept();
                String clientName = new DataInputStream(socket.getInputStream()).readUTF();
                String clientIP = socket.getInetAddress().getHostAddress();
                int clientPort = socket.getPort();
                String clientConnectDateTime = DateFormatUtils.format(System.currentTimeMillis(), "yyyy-MM-dd HH:mm:ss");
                NewClient newClient = new NewClient(socket, clientName, clientIP, clientPort, clientConnectDateTime);
                allClient.put(clientName, newClient);
                log.info("**** add new client ===> {}", allClient.keySet());
                new Thread(newClient).start();
            }
        }
        catch (IOException e)
        {
            log.error(e.getMessage());
        }
    }
    
    /**
     * 客户端线程(被观察者)
     */
    class NewClient extends Observable implements Runnable
    {
        // 客户端套接字
        private Socket socket;
        
        // 数据输入流
        private DataInputStream dataInputStream;
        
        // 数据输出流
        private DataOutputStream dataOutputStream;
        
        // 客户端运行(收、发报文)状态
        private boolean isNewClientRun = true;
        
        // 客户端的名称
        private String clientName;
        
        // 客户端的IP地址
        private String clientIP;
        
        // 构造方法初始化成员属性
        public NewClient(Socket socket, String clientName, String clientIP, int clientPort, String clientConnectDateTime)
        {
            this.socket = socket;
            this.clientName = clientName;
            this.clientIP = clientIP;
            try
            {
                // 注册观察者
                addObserver(SpringContextUtils.getBean(ResultCallBack.class));
                
                // 创建客户端数据输入、输出流
                dataInputStream = new DataInputStream(socket.getInputStream());
                dataOutputStream = new DataOutputStream(socket.getOutputStream());
            }
            catch (IOException e)
            {
                log.error(e.getMessage());
                closeCurrentClient();
            }
        }
        
        @Override
        public void run()
        {
            try
            {
                // 客户端在运行才能收发报文
                while (this.isNewClientRun)
                {
                    // 获取到客户端发送的报文
                    String msg = dataInputStream.readUTF();
                    if (StringUtils.isNotBlank(msg))
                    {
                        log.info("clientName: {}, clientIP: {}, send msg ===> {}", clientName, clientIP, msg);
                    }
                    
                    // 通知观察者处理
                    if (StringUtils.startsWith(msg, "hello Command"))
                    {
                        setChanged();
                        notifyObservers(msg);
                        continue;
                    }
                    // 向客户端传送数据
                    int index = 0;
                    for (String key : allClient.keySet())
                    {
                        index++;
                        if (StringUtils.equals(key, clientName))
                        {
                            allClient.get(key).sendMsg("from server " + msg + StringUtils.repeat("-----", index));
                        }
                    }
                }
            }
            catch (IOException e)
            {
                log.error(e.getMessage());
                closeCurrentClient();
            }
        }
        
        /**
         * 断开当前客户端的连接释放资源
         */
        public void closeCurrentClient()
        {
            try
            {
                // 结束客户端的运行状态
                isNewClientRun = false;
                // 断开数据输出出流
                if (dataOutputStream != null)
                {
                    dataOutputStream.close();
                }
                // 断开数据输入出流
                if (dataInputStream != null)
                {
                    dataInputStream.close();
                }
                // 断开客户端套解析
                if (socket != null)
                {
                    socket.close();
                }
                // 将该客户端从客户端容器中删除
                allClient.remove(clientName);
                log.info("**** remove client ===> {}", allClient.keySet());
            }
            catch (IOException e)
            {
                log.error(e.getMessage());
            }
        }
        
        /**
         * 发送报文
         */
        public void sendMsg(String msg)
        {
            try
            {
                // 发送报文
                dataOutputStream.writeUTF(msg);
                // 清空报文缓存
                dataOutputStream.flush();
            }
            catch (IOException e)
            {
                log.error(e.getMessage());
                closeCurrentClient();
            }
        }
    }
}
//goto src\main\java\com\fly\tcp\DemoController.java
package com.fly.tcp;

import java.io.IOException;
import java.util.concurrent.Callable;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.TimeoutException;

import org.apache.commons.lang3.StringUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.context.request.async.DeferredResult;
import org.springframework.web.context.request.async.WebAsyncTask;

import com.fly.core.JsonResult;
import com.fly.core.utils.JsonBeanUtils;
import com.fly.tcp.base.InitManager;
import com.fly.tcp.entity.Command;
import com.fly.tcp.service.ResultCallBack;

import io.swagger.annotations.Api;
import io.swagger.annotations.ApiImplicitParam;
import io.swagger.annotations.ApiOperation;
import lombok.extern.slf4j.Slf4j;

@Slf4j
@Api(tags = "tcp应用接口")
@RestController
@RequestMapping("/tcp")
public class DemoController
{
    @Autowired
    InitManager initManager;
    
    @Autowired
    ResultCallBack resultCallBack;
    
    @Autowired
    ThreadPoolTaskExecutor executor;
    
    @ApiOperation("client发消息")
    @ApiImplicitParam(name = "msg", example = "忽如一夜春风来", required = true)
    @PostMapping("/client/sendMsg")
    public String sendMsgFromClient(String msg)
    {
        initManager.getClient().sendMsg(msg);
        return msg;
    }
    
    /**
     * 客户端业务处理结果更多是通过回调实现的
     * 
     * @param cmd
     * @return
     * @throws IOException
     */
    @ApiOperation("server发消息Callable")
    @ApiImplicitParam(name = "msg", example = "千树万树梨花开", required = true)
    @PostMapping("/server/sendMsg/callable")
    public Callable<JsonResult<?>> sendMsgFromServer01(String msg)
        throws IOException
    {
        return () -> {
            Command command = new Command().setClientId(initManager.getClient().getClientName()).setText(msg);
            initManager.getServer().sendMsg(initManager.getClient().getClientName(), JsonBeanUtils.beanToJson(command));
            String result;
            for (int i = 0; i < 50; i++)
            {
                result = resultCallBack.queryResult(command.getCommandId());
                if (StringUtils.isNotBlank(result))
                {
                    return JsonResult.success(result, "获取处理结果成功");
                }
                else
                {
                    TimeUnit.MILLISECONDS.sleep(100);
                }
            }
            return JsonResult.error("响应超时,请重试");
        };
    }
    
    /**
     * 客户端业务处理结果更多是通过回调实现的(WebAsyncTask升级版callable,增加超时异常等处理)
     * 
     * @param cmd
     * @return
     * @throws IOException
     */
    @ApiOperation("server发消息webAsyncTask")
    @ApiImplicitParam(name = "msg", example = "千树万树梨花开", required = true)
    @PostMapping("/server/sendMsg/webAsyncTask")
    public WebAsyncTask<String> sendMsgFromServer02(String msg)
        throws IOException
    {
        WebAsyncTask<String> webAsyncTask = new WebAsyncTask<String>(2000L, executor, () -> {
            Command command = new Command().setClientId(initManager.getClient().getClientName()).setText(msg);
            initManager.getServer().sendMsg(initManager.getClient().getClientName(), JsonBeanUtils.beanToJson(command));
            String result;
            for (int i = 0; i < 100; i++)
            {
                result = resultCallBack.queryResult(command.getCommandId());
                if (StringUtils.isNotBlank(result))
                {
                    return result;
                }
                else
                {
                    TimeUnit.MILLISECONDS.sleep(50);
                }
            }
            return "响应超时,请重试";
        });
        webAsyncTask.onCompletion(() -> log.info("调用完成"));
        webAsyncTask.onError(() -> {
            log.error("业务处理出错");
            return "error";
        });
        webAsyncTask.onTimeout(() -> {
            log.info("业务处理超时");
            return "Time Out";
        });
        return webAsyncTask;
    }
    
    /**
     * 客户端业务处理结果更多是通过回调实现的
     * 
     * @param cmd
     * @return
     * @throws IOException
     * @throws TimeoutException
     * @throws ExecutionException
     * @throws InterruptedException
     */
    @ApiOperation("server发消息")
    @ApiImplicitParam(name = "msg", example = "千树万树梨花开", required = true)
    @PostMapping("/server/sendMsg")
    public DeferredResult<String> sendMsgFromServer(String msg)
        throws IOException, InterruptedException, ExecutionException, TimeoutException
    {
        String clientName = initManager.getClient().getClientName();
        Command command = new Command().setClientId(clientName).setText(msg);
        initManager.getServer().sendMsg(clientName, JsonBeanUtils.beanToJson(command));
        
        // 异步返回结果
        DeferredResult<String> deferredResult = new DeferredResult<>(20000L, "失败");
        deferredResult.onCompletion(() -> log.info("调用完成"));
        deferredResult.onTimeout(() -> {
            log.info("调用超时");
            deferredResult.setResult("调用超时");
        });
        resultCallBack.processResult(deferredResult, command.getCommandId());
        return deferredResult;
    }
}
//goto src\main\java\com\fly\tcp\entity\Command.java
package com.fly.tcp.entity;

import java.util.UUID;

import lombok.Data;
import lombok.experimental.Accessors;

@Data
@Accessors(chain = true)
public class Command
{
    /**
     * 命令id
     */
    private String commandId = UUID.randomUUID().toString();
    
    /**
     * 客户端id
     */
    private String clientId;
    
    /**
     * 具体命令内容
     */
    private String text;
}
//goto src\main\java\com\fly\tcp\service\ResultCallBack.java
package com.fly.tcp.service;

import java.io.IOException;
import java.util.Map;
import java.util.Observable;
import java.util.Observer;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.TimeoutException;

import org.apache.commons.lang3.StringUtils;
import org.springframework.stereotype.Service;
import org.springframework.web.context.request.async.DeferredResult;

import com.fly.core.utils.JsonBeanUtils;
import com.fly.tcp.entity.Command;

import lombok.extern.slf4j.Slf4j;

/**
 * 结果回调处理
 */
@Slf4j
@Service
public class ResultCallBack implements Observer
{
    Map<String, String> result = new ConcurrentHashMap<>();
    
    ExecutorService executorService = Executors.newFixedThreadPool(10);
    
    /**
     * 观察者接受数据处理
     */
    @Override
    public void update(Observable observable, Object msg)
    {
        log.info("### accept #### {}", msg);
        try
        {
            if (msg instanceof String)
            {
                String json = StringUtils.substringAfter((String)msg, "hello Command!");
                Command command = JsonBeanUtils.jsonToBean(json, Command.class);
                log.info("{}", command);
                result.put(command.getCommandId(), json);
            }
        }
        catch (IOException e)
        {
            log.error(e.getMessage());
        }
    }
    
    /**
     * 获取命令处理结果
     * 
     * @param commandId
     * @return
     */
    public String queryResult(String commandId)
    {
        return result.get(commandId);
    }
    
    /**
     * 业务线程处理业务,DeferredResult可以通过任何线程来计算返回一个结果
     * 
     * @param deferredResult
     * @param commandId
     * @throws TimeoutException
     * @throws ExecutionException
     * @throws InterruptedException
     */
    public void processResult(DeferredResult<String> deferredResult, String commandId)
        throws InterruptedException, ExecutionException, TimeoutException
    {
        processResult02(deferredResult, commandId);
    }
    
    /**
     * 无超时设置,不推荐
     * 
     * @param deferredResult
     * @param commandId
     */
    void processResult01(DeferredResult<String> deferredResult, String commandId)
    {
        executorService.execute(() -> {
            while (!result.containsKey(commandId))
            {
                try
                {
                    log.info("waitting......");
                    TimeUnit.MILLISECONDS.sleep(20);
                }
                catch (InterruptedException e)
                {
                }
            }
            deferredResult.setResult(result.get(commandId));
        });
    }
    
    /**
     * 有超时设置(推荐)
     * 
     * 
     * @param deferredResult
     * @param commandId
     * @throws InterruptedException
     * @throws ExecutionException
     * @throws TimeoutException
     */
    private void processResult02(DeferredResult<String> deferredResult, String commandId)
        throws InterruptedException, ExecutionException, TimeoutException
    {
        Future<String> future = executorService.submit(() -> {
            while (!result.containsKey(commandId))
            {
                log.info("waitting......");
                TimeUnit.MILLISECONDS.sleep(20);
            }
            return result.get(commandId);
        });
        String result = future.get(1000L, TimeUnit.MILLISECONDS);
        deferredResult.setResult(result);
    }
}
//goto src\main\resources\application-dev.yml
#设置日志级别
logging: 
  level:
    org:
      springframework:
        web: INFO
//goto src\main\resources\application.yml
server:
  port: 8081
  servlet:
    context-path: /
    session:
      timeout: 1800
spring:
  servlet:
    multipart:
      max-file-size: 10MB
      max-request-size: 100MB
  profiles:
    active:
    - test

#必须启用下面配置,否则接口排序失效
knife4j:
  enable: true

#设置日志级别
logging: 
  level:
    root: INFO
//goto src\test\java\com\fly\controller\ApiControllerTest.java
package com.fly.controller;

import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.util.Arrays;

import org.apache.commons.io.IOUtils;
import org.junit.jupiter.api.Test;
import org.springframework.core.io.ClassPathResource;
import org.springframework.core.io.Resource;
import org.springframework.http.HttpEntity;
import org.springframework.http.HttpHeaders;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.http.client.SimpleClientHttpRequestFactory;
import org.springframework.web.client.RestTemplate;

import lombok.extern.slf4j.Slf4j;

@Slf4j
public class ApiControllerTest
{
    private static RestTemplate restTemplate;
    
    static
    {
        // 配置proxy,connectTimeout,readTimeout等参数
        SimpleClientHttpRequestFactory requestFactory = new SimpleClientHttpRequestFactory();
        requestFactory.setConnectTimeout(1000);
        requestFactory.setReadTimeout(1000);
        restTemplate = new RestTemplate(requestFactory);
    }
    
    @Test
    public void testJsonRequestBody2()
        throws IOException
    {
        String url = "http://192.168.114.250:8080/api/post";
        HttpHeaders headers = new HttpHeaders();
        headers.setContentType(MediaType.APPLICATION_JSON);
        headers.setAccept(Arrays.asList(MediaType.APPLICATION_JSON));
        
        Resource resource = new ClassPathResource("data/json");
        String text = IOUtils.toString(resource.getInputStream(), StandardCharsets.UTF_8);
        HttpEntity<String> requestEntity = new HttpEntity<>(text, headers);
        ResponseEntity<String> responseEntity = restTemplate.postForEntity(url, requestEntity, String.class);
        log.info("******  ResponseEntity body: {}", responseEntity.getBody());
    }
}
//goto src\test\java\com\fly\executor\ExecutorServiceTest.java
package com.fly.executor;

import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Future;
import java.util.concurrent.LinkedBlockingQueue;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.TimeoutException;

import org.apache.commons.lang3.concurrent.BasicThreadFactory;
import org.junit.jupiter.api.Test;
import org.springframework.http.MediaType;
import org.springframework.web.reactive.function.client.WebClient;

import lombok.extern.slf4j.Slf4j;

@Slf4j
public class ExecutorServiceTest
{
    private WebClient webClient = WebClient.builder().codecs(configurer -> configurer.defaultCodecs().maxInMemorySize(-1)).build();
    
    private ExecutorService executorService = new ThreadPoolExecutor(10, 10, 0L, TimeUnit.MILLISECONDS, new LinkedBlockingQueue<Runnable>(), new BasicThreadFactory.Builder().namingPattern("t-%03d").daemon(true).priority(Thread.MAX_PRIORITY).build());
    
    /**
     * 带超时条件线程池调用
     * 
     * @throws TimeoutException
     * @throws ExecutionException
     * @throws InterruptedException
     * 
     * @throws IOException
     */
    @Test
    public void testTimeout()
        throws InterruptedException, ExecutionException, TimeoutException
    {
        String ip = "192.168.0.1";
        Future<String> future = executorService.submit(() -> {
            return webClient.get()
                .uri(uriBuilder -> uriBuilder.scheme("http").host(ip).port("2375").path("/_ping").build())// URI
                .acceptCharset(StandardCharsets.UTF_8)
                .accept(MediaType.APPLICATION_JSON)
                .retrieve()
                .bodyToMono(String.class)
                .block();
        });
        log.info("return: {}", future.get(1000L, TimeUnit.MILLISECONDS));
    }
}
//goto src\test\resources\log4j2.xml
<?xml version="1.0" encoding="UTF-8"?>
<Configuration status="WARN">
	<Properties>
		<Property name="LOG_EXCEPTION_CONVERSION_WORD">%xwEx</Property>
		<Property name="LOG_LEVEL_PATTERN">%5p</Property>
		<Property name="LOG_DATEFORMAT_PATTERN">yyyy-MM-dd HH:mm:ss.SSS</Property>
		<Property name="CONSOLE_LOG_PATTERN">%clr{%d{${LOG_DATEFORMAT_PATTERN}}}{faint} %clr{${LOG_LEVEL_PATTERN}} %clr{%pid}{magenta} %clr{---}{faint} %clr{[%15.15t]}{faint} %clr{%-40.40c{1.}}{cyan} %clr{:}{faint} %m%n${sys:LOG_EXCEPTION_CONVERSION_WORD}</Property>
		<Property name="FILE_LOG_PATTERN">%d{${LOG_DATEFORMAT_PATTERN}} ${LOG_LEVEL_PATTERN} %pid --- [%t] %-40.40c{1.} : %m%n${sys:LOG_EXCEPTION_CONVERSION_WORD}</Property>
	</Properties>
	<Appenders>
		<Console name="Console" target="SYSTEM_OUT" follow="true">
			<PatternLayout pattern="${sys:CONSOLE_LOG_PATTERN}" />
		</Console>
	</Appenders>
	<Loggers>
		<Logger name="org.apache.catalina.startup.DigesterFactory" level="error" />
		<Logger name="org.apache.catalina.util.LifecycleBase" level="error" />
		<Logger name="org.apache.coyote.http11.Http11NioProtocol" level="warn" />
		<logger name="org.apache.sshd.common.util.SecurityUtils" level="warn"/>
		<Logger name="org.apache.tomcat.util.net.NioSelectorPool" level="warn" />
		<Logger name="org.eclipse.jetty.util.component.AbstractLifeCycle" level="error" />
		<Logger name="org.hibernate.validator.internal.util.Version" level="warn" />
		<logger name="org.springframework.boot.actuate.endpoint.jmx" level="warn"/>
		<Root level="info">
			<AppenderRef ref="Console" />
		</Root>
	</Loggers>
</Configuration>

六、运行界面

在这里插入图片描述
在这里插入图片描述
具体的部署和测试不再详细阐述。

七. 主要技术点

  1. 观察者模式
  2. 命令模式
  3. 线程池应用
  4. 异步Callable、WebAsyncTask、DeferredResult

有任何问题和建议,都可以向我提问讨论,大家一起进步,谢谢!

-over-

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值