一、概述
在上文JAVA TCP协议初体验 中,我们使用java实现了tcp协议的一个雏形,实际中大部分项目都已采用springboot,那么,怎么在springboot中整合tcp协议呢?如何实现服务器controller通过tcp协议下发命令到tcp client执行,并且在controller中获取执行结果?
二、实现思路
为了方便演示,本文我们将TcpClient、TcpServer放在同一工程,具体做法为:
- 拷贝之前文章的TcpClient、TcpServer代码
- 提取服务器、客户端启动代码,使用@PostConstruct注解修饰
- 开发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>
六、运行界面
具体的部署和测试不再详细阐述。
七. 主要技术点
- 观察者模式
- 命令模式
- 线程池应用
- 异步Callable、WebAsyncTask、DeferredResult
有任何问题和建议,都可以向我提问讨论,大家一起进步,谢谢!
-over-