一、概述 1.1、概述 LangChain4J 即 LangChain For Java。 随着人工智能（AI）技术的迅猛发展，越来越多的开发者开始将目光投向AI应用的开发。然而，目前市场上大多数AI框架和工具如LangChain、PyTorch等主要支持Python，而Java开发者常常面临工具缺乏和学习门槛较高的问题。 LangChain4J 官方文档 1.2、作用 LLM 大模型应用技术架构： 大模型开发分类：超级个体+智能体、行业垂直大模型、基础通用大模型。 产品定位： 二、起步 2.1、前置约定 2.1.1、langchain4j 执行的各种大语言模型(LLMS) Comparison Table of all supported Language Models 2.1.2、模型约定 2.1.3、配置及关键点 所有调用均基于 OpenAI 协议标准或者 SrpingBoot 官方推荐整合规则，实现一致的接口设计与规范，确保多模型切换的便利性，提供高度可扩展的开发支持。 2.2、入门 2.2.1、接入按理百炼平台的通义模型 https://bailian.console.aliyun.com 2.2.2、大模型调用 1、获得 API-key 2、获得模型名 3、获得 baseUrl 开发地址
一、Skywalking skywalking 是一个优秀的国产开源框架，2015 年由个人吴晟（华为开发者）开源 ， 2017 年加入 Apache 孵化器。 skywalking 支持 dubbo，SpringCloud，SpringBoot 集成，代码无侵入，通信方式采用 GRPC，性能较好，实现方式是 java 探针，支持告警，支持 JVM 监控，支持全局调用统计等等，功能较完善。 二、Skywalking & Spring Cloud Sleuth+ZipKin Skywalking 相比于 zipkin 还是有很大的优势的，如下： 1、Skywalking 采用字节码增强的技术实现代码无侵入，zipKin 代码侵入性比较高。 2、Skywalking 功能比较丰富，报表统计，UI 界面更加人性化。 三、Skywalking 架构 skywalking 和 zipkin 一样，也分为服务端和客户端，服务端负责收集日志数据并且展示，架构如下： 上述架构图中主要分为四个部分，如下： 上面的 Agent：负责收集日志数据，并且传递给中间的 OAP 服务器 中间的 OAP：负责接收 Agent 发送的 Tracing 和 Metric 的数据信息，然后进行分析 (Analysis Core) ，存储到外部存储器 ( Storage )，最终提供查询 ( Query ) 功能。 左面的 UI：负责提供 web 控制台，查看链路，查看各种指标，性能等等。 右面 Storage：负责数据的存储，支持多种存储类型。 Agent 负责收集日志传输数据，通过 GRPC 的方式传递给 OAP 进行分析并且存储到数据库中，最终通过 UI 界面将分析的统计报表、服务依赖、拓扑关系图展示出来。 三、服务端搭建 skywalking 同样是通过 jar 包方式启动，需要下载 jar 包，地址： https://skywalking.apache.org/downloads/ https://archive.apache.org/dist/skywalking 3.1、下载安装包 选择 V8.7.0 或其他版本，如下图： 重要的目录结构分析如下： agent：客户端需要指定的目录，其中有一个 jar，就是负责和客户端整合收集日志 bin：服务端启动的脚本 config：一些配置文件的目录 logs：oap 服务的日志目录 oap-libs：oap 所需的依赖目录 webapp：UI 服务的目录 3.2、配置修改 启动之前需要对配置文件做一些修改，修改如下： 3.2.1、/config/application.yml 这个是 oap 服务的配置文件，需要修改注册中心为 nacos，如下图： 配置 1：修改默认注册中心选择 nacos，这样就不用在启动参数中指定了。 配置 2：修改 nacos 的相关配置，由于陈某是本地的，则不用改动，根据自己情况修改。 3.2.2、webapp/webapp.yml 这个是 UI 服务的配置文件，其中有一个 server.port 配置，是 UI 服务的端口，默认 8080，可将其改成 8081，避免端口冲突。 3.3、启动服务 启动命令在 /bin 目录下，这里需要启动两个服务，如下： oap 服务：对应的启动脚本 oapService.bat，Linux 下对应的后缀是 sh。 UI 服务：对应的启动脚本 webappService.bat，Linux 下对应的后缀是 sh。 当然还有一个 startup.bat 启动文件，可以直接启动上述两个服务，我们可以直接使用这个脚本，直接双击，将会弹出两个窗口则表示启动成功。 此时直接访问：http://localhost:8888/，直接进入 UI 端。 四、客户端搭建 客户端也就是单个微服务，由于 Skywalking 采用字节码增强技术，因此对于微服务无代码侵入，只要是普通的微服务即可，不需要引入什么依赖。 想要传输数据必须借助 skywalking 提供的 agent，只需要在启动参数指定即可，命令如下： -javaagent:E:\SOFTWARE\skywalking\apache-skywalking-apm-bin\agent\skywalking-agent.jar -Dskywalking.agent.service_name=jeecg-cloud-gateway -Dskywalking.collector.backend_service=localhost:11800 上述命令解析如下： -javaagent：指定 skywalking 中的 agent 中的 skywalking-agent.jar 的路径。 -Dskywalking.agent.service_name：指定在 skywalking 中的服务名称，一般是微服务的 spring.application.name。 -Dskywalking.collector.backend_service：指定 oap 服务绑定的地址，由于这里是本地，并且 oap 服务默认的端口是 11800，因此只需要配置为 127.0.0.1:11800。 五、数据持久化 服务端重启之后，这些链路追踪数据将会丢失了，因为 skywalking 默认持久化的方式是存储在内存中。 当然这里也是可以通过插拔方式的替换掉存储中间件，企业中往往是使用 ES 存储，此处介绍一下 MySQL 的方式存储。 5.1、修改配置文件 修改 config/application.yml 文件中的存储方式，总共需要修改两处地方。 修改默认的存储方式为 mysql，如下： storage: selector: ${SW_STORAGE:h2} 修改 Mysql 相关的信息，比如用户名、密码等，如下： mysql: properties: jdbcUrl: ${SW_JDBC_URL:"jdbc:mysql://localhost:306/skywalking?ueUnicode-truescharacterEncoding=Unp-8 serverimezone=Asia/shanghai"} dataSource.user: ${SW_DATA_SOURCE_USER:root} dataSource.password: ${SW_DATA_SOURCE_PASSWORD:root@1234} dataSource.cachePrepStmts: ${SW_DATA_SOURCE_CACHE_PREP_STMTS:true} dataSource.prepStmtCacheSize: ${SW_DATA_SOURCE_PREP_STMT_CACHE_SQL_SIZE:250} dataSource.prepStmtCacheSqlLimit: ${SW_DATA_SOURCE_PREP_STMT_CACHE_SQL_LIMIT:2048} dataSource.useServerPrepStmts: ${SW_DATA_SOURCE_USE_SERVER_PREP_STMTS:true} metadataQueryMaxSize: ${SW_STORAGE_MYSQL_QUERY_MAX_SIZE:5000} maxSizeOfArrayColumn: ${SW_STORAGE_MAX_SIZE_OF_ARRAY_COLUMN:20} numOfSearchableValuesPerTag: ${SW_STORAGE_NUM_OF_SEARCHABLE_VALUES_PER_TAG:2} 5.2、添加 MySQL 的 jdbc 依赖 默认的 oap 中是没有 jdbc 驱动依赖，因此需要我们手动添加一下，只需要将驱动的 jar 放在 oap-libs 文件夹中：mysql-connector-java-8.0.15.jar 已经配置完成，启动服务端，在 skywalking 这个数据库中将会自动创建表。 六、日志监控 在 skywalking 的 UI 端有一个日志的模块，用于收集客户端的日志，默认是没有数据的，那么需要如何将日志数据传输到 skywalking 中呢？ 日志框架的种类很多，比较出名的有 log4j，logback，log4j2，此处以 logback 为例子介绍一下如何配置。 6.1、添加依赖 根据官方文档，需要先添加依赖，如下： <dependency> <groupId>org.apache.skywalking</groupId> <artifactId>apm-toolkit-logback-1.x</artifactId> <version>${project.release.version}</version> </dependency> 6.2、添加配置文件 新建一个 logback-spring.xml 放在 resource 目录下，配置如下图： <configuration scan="true" scanPeriod=" 5 seconds"> <appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender"> <encoder class="ch.qos.logback.core.encoder.LayoutWrappingEncoder"> <layout class="org.apache.skywalking.apm.toolkit.log.logback.v1.x.mdc.TraceIdMDCPatternLogbackLayout"> <Pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} [%X{tid}] [%thread] %-5level %logger{36} -%msg%n</Pattern> <pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} %boldMagenta([%tid]) [%thread] %highlight(%-5level) %cyan(%logger{50}:%L) - %msg%n</pattern> </layout> </encoder> </appender> <appender name="ASYNC" class="ch.qos.logback.classic.AsyncAppender"> <discardingThreshold>0</discardingThreshold> <queueSize>1024</queueSize> <neverBlock>true</neverBlock> <appender-ref ref="STDOUT"/> </appender> <root level="INFO"> <appender-ref ref="ASYNC"/> </root> </configuration> 当你使用 -javaagent 激活 skywalking tracer 后，logback 将会输出 traceId（如果存在的话）。如果 tracer 未激活，输出将是 TID: N/A。 注意： 如果 agent 和 oap 服务不在同一台服务器上，需要在 /agent/config/agent.config 配置文件末尾添加如下配置： plugin.toolkit.log.grpc.reporter.server_host=${SW_GRPC_LOG_SERVER_HOST:10.10.10.1} plugin.toolkit.log.grpc.reporter.server_port=${SW_GRPC_LOG_SERVER_PORT:11800} plugin.toolkit.log.grpc.reporter.max_message_size=${SW_GRPC_LOG_MAX_MESSAGE_SIZE:10485760} plugin.toolkit.log.grpc.reporter.upstream_timeout=${SW_GRPC_LOG_GRPC_UPSTREAM_TIMEOUT:30} 七、性能剖析 skywalking 在性能剖析方面真的是非常强大，提供到基于堆栈的分析结果，能够让运维人员一眼定位到问题。 新建一个 /order/list 接口，代码中休眠了 2 秒，看看如何在 skywalking 中定位这个问题。 在性能剖析模块 -> 新建任务 -> 选择服务、填写端点、监控时间，操作如下图： 选择这个任务将会出现监控到的数据，如下图： 上图中可以看到 {GET}/order/list 这个接口上耗费了2秒以上，因此选择这个接口点击分析，可以看到详细的堆栈信息，如下图： 如何定位到睡眠 2 秒钟的那一行代码，直接往下翻，如下图： 参考： SkyWalking 8 官方文档 Skywalking增加登录认证和日志收集
一、创建SpringApplication对象 initialize(sources);//创建对象 private void initialize(Object[] sources) { //【保存主配置类】 if (sources != null && sources.length > 0) { this.sources.addAll(Arrays.asList(sources)); } //【判断当前是否一个web应用】 this.webEnvironment = deduceWebEnvironment(); //从类路径下找到META-INF/spring.factories配置的所有ApplicationContextInitializer；然后保存起来 setInitializers((Collection) getSpringFactoriesInstances( ApplicationContextInitializer.class)); //从类路径下找到ETA-INF/spring.factories配置的所有ApplicationListener setListeners((Collection) getSpringFactoriesInstances(ApplicationListener.class)); //从多个配置类中找到有main方法的主配置类 this.mainApplicationClass = deduceMainApplicationClass(); } 拿到初始化器： 保存初始化器和监听器： 二、运行run方法 public ConfigurableApplicationContext run(String... args) { StopWatch stopWatch = new StopWatch(); stopWatch.start(); ConfigurableApplicationContext context = null; FailureAnalyzers analyzers = null; configureHeadlessProperty(); //获取SpringApplicationRunListeners；从类路径下META-INF/spring.factories SpringApplicationRunListeners listeners = getRunListeners(args); //回调所有的获取SpringApplicationRunListener.starting()方法 listeners.starting(); try { //封装命令行参数 ApplicationArguments applicationArguments = new DefaultApplicationArguments( args); //准备环境 ConfigurableEnvironment environment = prepareEnvironment(listeners, applicationArguments); //创建环境完成后回调SpringApplicationRunListener.environmentPrepared()；表示环境准备完成 Banner printedBanner = printBanner(environment); //创建ApplicationContext；决定创建web的ioc还是普通的ioc context = createApplicationContext(); analyzers = new FailureAnalyzers(context); //准备上下文环境;将environment保存到ioc中；而且applyInitializers()； //applyInitializers()：回调之前保存的所有的ApplicationContextInitializer的initialize方法 //回调所有的SpringApplicationRunListener的contextPrepared()； // prepareContext(context, environment, listeners, applicationArguments, printedBanner); //prepareContext运行完成以后回调所有的SpringApplicationRunListener的contextLoaded（）； //s刷新容器；ioc容器初始化（如果是web应用还会创建嵌入式的Tomcat）；Spring注解版 //扫描，创建，加载所有组件的地方；（配置类，组件，自动配置） refreshContext(context); //从ioc容器中获取所有的ApplicationRunner和CommandLineRunner进行回调 //ApplicationRunner先回调，CommandLineRunner再回调 afterRefresh(context, applicationArguments); //所有的SpringApplicationRunListener回调finished方法 listeners.finished(context, null); stopWatch.stop(); if (this.logStartupInfo) { new StartupInfoLogger(this.mainApplicationClass) .logStarted(getApplicationLog(), stopWatch); } //整个SpringBoot应用启动完成以后返回启动的ioc容器； return context; } catch (Throwable ex) { handleRunFailure(context, listeners, analyzers, ex); throw new IllegalStateException(ex); } } 三、事件监听机制 配置在 META-INF/spring.factories ApplicationContextInitializer public class HelloApplicationContextInitializer implements ApplicationContextInitializer<ConfigurableApplicationContext> { @Override public void initialize(ConfigurableApplicationContext applicationContext) { System.out.println("ApplicationContextInitializer...initialize..."+applicationContext); } } SpringApplicationRunListener public class HelloSpringApplicationRunListener implements SpringApplicationRunListener { //必须有的构造器 public HelloSpringApplicationRunListener(SpringApplication application, String[] args){ } @Override public void starting() { System.out.println("SpringApplicationRunListener...starting..."); } @Override public void environmentPrepared(ConfigurableEnvironment environment) { Object o = environment.getSystemProperties().get("os.name"); System.out.println("SpringApplicationRunListener...environmentPrepared.."+o); } @Override public void contextPrepared(ConfigurableApplicationContext context) { System.out.println("SpringApplicationRunListener...contextPrepared..."); } @Override public void contextLoaded(ConfigurableApplicationContext context) { System.out.println("SpringApplicationRunListener...contextLoaded..."); } @Override public void finished(ConfigurableApplicationContext context, Throwable exception) { System.out.println("SpringApplicationRunListener...finished..."); } } 配置（META-INF/spring.factories） org.springframework.context.ApplicationContextInitializer=\ com.atguigu.springboot.listener.HelloApplicationContextInitializer org.springframework.boot.SpringApplicationRunListener=\ com.atguigu.springboot.listener.HelloSpringApplicationRunListener 只需要放在IOC容器中 ApplicationRunner @Component public class HelloApplicationRunner implements ApplicationRunner { @Override public void run(ApplicationArguments args) throws Exception { System.out.println("ApplicationRunner...run...."); } } CommandLineRunner @Component public class HelloCommandLineRunner implements CommandLineRunner { @Override public void run(String... args) throws Exception { System.out.println("CommandLineRunner...run..."+ Arrays.asList(args)); } } 四、总结 4.1、SpringApplication.run(主程序类) new SpringApplication(主程序类) 判断是否web应用 加载并保存所有ApplicationContextInitializer（META-INF/spring.factories）， 加载并保存所有ApplicationListener 获取到主程序类 run() 回调所有的SpringApplicationRunListener（META-INF/spring.factories）的starting 获取ApplicationArguments 准备环境&回调所有监听器（ SpringApplicationRunListener ）的environmentPrepared 打印banner信息 创建ioc容器对象（ AnnotationConfigEmbeddedWebApplicationContext（web环境容器） AnnotationConfigApplicationContext（普通环境容器） 准备环境 执行ApplicationContextInitializer. initialize() 监听器SpringApplicationRunListener回调contextPrepared 加载主配置类定义信息 监听器SpringApplicationRunListener回调contextLoaded 刷新启动IOC容器； 扫描加载所有容器中的组件 包括从META-INF/spring.factories中获取的所有EnableAutoConfiguration组件 回调容器中所有的ApplicationRunner、CommandLineRunner的run方法 监听器SpringApplicationRunListener回调finishe 4.2、自动配置 Spring Boot启动扫描所有jar包的META-INF/spring.factories中配置的EnableAutoConfiguration组件 spring-boot-autoconfigure.jar\META-INF\spring.factories有启动时需要加载的EnableAutoConfiguration组件配置 配置文件中使用debug=true可以观看到当前启用的自动配置的信息 自动配置会为容器中添加大量组件 Spring Boot在做任何功能都需要从容器中获取这个功能的组件 Spring Boot 总是遵循一个标准；容器中有我们自己配置的组件就用我们配置的，没有就用自动配置默认注册进来的组件；
一、简介 MapStruct是一个Java注释处理器，用于生成类型安全的bean映射类。 定义一个映射器接口，该接口声明任何必需的映射方法。在编译期间，MapStruct将生成此接口的实现。此实现使用简单的Java方法调用在源对象和目标对象之间进行映射，即没有反射或类似内容。 与手动编写映射代码相比，MapStruct通过生成繁琐且易于出错的代码来节省时间。遵循配置方法上的约定，MapStruct使用合理的默认值，但在配置或实现特殊行为时不加理会。 与动态映射框架相比，MapStruct具有以下优点： 1、通过使用普通方法调用（settter/getter）而不是反射来快速执行 2、编译时类型安全性：只能映射相互映射的对象和属性，不能将order实体意外映射到customer DTO等。 3、如果有如下问题，编译时会抛出异常 映射不完整（并非所有目标属性都被映射） 映射不正确（找不到正确的映射方法或类型转换） 4、可以通过freemarker定制化开发 二、设置 MapStruct是基于JSR 269的Java注释处理器，因此可以在命令行构建（javac，Ant，Maven等）以及您的IDE中使用。 它包含以下工件： org.mapstruct：mapstruct：包含必需的注释，例如@Mapping org.mapstruct：mapstruct-processor：包含注释处理器，该注释处理器生成映射器实现 2.1、Maven <dependency> <groupId>org.mapstruct</groupId> <artifactId>mapstruct</artifactId> <version>1.5.0.Beta1</version> </dependency> <dependency> <groupId>org.mapstruct</groupId> <artifactId>mapstruct-processor</artifactId> <version>1.5.0.Beta1</version> </dependency> Lombok依赖：（版本最好在1.16.16以上，否则会出现问题）通常是和lombok一起使用。 <dependency> <groupid>org.projectlombok</groupid> <artifactid>lombok</artifactid> <version>${lombok.version}</version> // 版本号 1.18.12 </dependency> idea中下载 mapstruct support 插件，安装重启Idea 。 三、映射器 3.1、基本映射 要创建映射器，只需使用所需的映射方法定义一个Java接口，并用注释对其进行org.mapstruct.Mapper注释： 该@Mapper注释将使得MapStruct代码生成器创建的执行PersonMapper 过程中生成时的界面。 在生成的方法实现中，源类型（例如Person）的所有可读属性都将被复制到目标类型（例如PersonDTO）的相应属性中： 当一个属性与其目标实体对应的名称相同时，它将被隐式映射。 当属性在目标实体中具有不同的名称时，可以通过@Mapping注释指定其名称。 @Data public class Person { String describe; private String id; private String name; private int age; private BigDecimal source; private double height; private Date createTime; } @Data public class PersonDTO { String describe; private Long id; private String personName; private String age; private String source; private String height; } // mapper @Mapper public interface PersonMapper { PersonMapper INSTANCT = Mappers.getMapper(PersonMapper.class); @Mapping(target = "name", source = "personName") @Mapping(target = "id", ignore = true) // 忽略id，不进行映射 PersonDTO conver(Person person); } 生成的实现类： public class PersonMapperImpl implements PersonMapper { public PersonMapperImpl() { } public PersonDTO conver(Person person) { if (person == null) { return null; } else { PersonDTO personDTO = new PersonDTO(); personDTO.setDescribe(person.getDescribe()); if (person.getId() != null) { personDTO.setId(Long.parseLong(person.getId())); } personDTO.setPersonName(person.getName()); personDTO.setAge(String.valueOf(person.getAge())); if (person.getSource() != null) { personDTO.setSource(person.getSource().toString()); } personDTO.setHeight(String.valueOf(person.getHeight())); return personDTO; } } } 测试: @Test public void test(){ Person person = new Person(); person.setDescribe("测试"); person.setAge(18); person.setName("张三"); person.setHeight(170.5); person.setSource(new BigDecimal("100")); PersonDTO dto = PersonMapper.INSTANCT.conver(person); System.out.println(dto); // PersonDTO(describe=测试, id=null, personName=张三, age=18, source=100, height=170.5) } 3.2、指定默认值 在@Mapper接口类里面的转换方法上添加@Mapping注解。 target() 必须添加，source()可以不添加，则直接使用defaultValue。 @Mapping(target = "describe", source = "describe", defaultValue = "默认值") PersonDTO conver(Person person); 生成的impl： ... if (person.getDescribe() != null) { personDTO.setDescribe(person.getDescribe()); } else { personDTO.setDescribe("默认值"); } ... 测试： @Test public void test(){ Person person = new Person(); //person.setDescribe("测试"); person.setAge(18); person.setName("张三"); person.setHeight(170.5); person.setSource(new BigDecimal("100")); PersonDTO dto = PersonMapper.INSTANCT.conver(person); System.out.println(dto); // PersonDTO(describe=默认值, id=null, name=张三, age=18, source=100, height=170.5) } 3.3、使用表达式 目前java是唯一受支持的语言，达式必须以Java表达式的形式给出： 注意： 这个属性不能与source()、defaultValue()、defaultExpression()、qualifiedBy()、qualifiedByName()或constant()一起使用。 @Mapping(target = "describe", source = "describe", defaultValue = "默认值") @Mapping(target = "createTime",expression = "java(new java.util.Date())") PersonDTO conver(Person person); 3.4、dateFormat() 如果属性从字符串映射到日期，则该格式字符串可由SimpleDateFormat处理，反之亦然。当映射枚举常量时，将忽略所有其他属性类型。 .... @Mapping(target = "createTime" ,source = "createTime", dateFormat = "yyyy-MM-dd") PersonDTO conver(Person person); ... 3.5、组合映射 3.5.1、多个源对象 @Data public class BasicEntity { private Date createTime; private String createBy; private Date updateTime; private String updateBy; private int _ROW; } @Mapper(uses =DateFormtUtil.class) public interface PersonMapper { PersonMapper INSTANCT = Mappers.getMapper(PersonMapper.class); @Mapping(target = "personName",source = "name") PersonDTO conver(Person person); @Mapping(target = "createTime",source = "basicEntity.createTime") PersonDTO combinationConver(Person personC, BasicEntity basicEntity); } 3.5.2 使用其他的值 ... @Mapping(target = "id", source = "id") PersonDTO mapTo(Person person, String id); ... 虽然Person和Person有相同的id字段，但是映射器会使用mapTo方法里面的id参数。 3.6、嵌套映射 @Data public class Person { ... private Child personChild; ... } @Data public class PersonDTO { ... private Child child; ... } // mapper @Mapper(uses =DateFormtUtil.class) public interface PersonMapper { PersonMapper INSTANCT = Mappers.getMapper(PersonMapper.class); @Mapping(target = "child", source = "personChild") PersonDTO conver(Person person); } 如果field name一样则不需要指定@Mapping 。 3.7、numberFormat() 如果带注释的方法从数字映射到字符串，则使用DecimalFormat将格式字符串作为可处理的格式。反之亦然。对于所有其他元素类型，将被忽略。 从基本2.1 基本映射可以看出，number类型与字符串直接的转换是通过valueOf()，如果字符串格式不正确会抛出java.lang.NumberFormatException异常,例如：Integer.valueOf(“10.2”)。 使用numberFormat()之后DecimalFormat格式转换，还是会抛出NFE异常。 // mapper .... @Mapping(target = "age",source = "age", numberFormat = "#0.00") PersonDTO conver(Person person); ... // imppl personDTO.setAge((new DecimalFormat("#0.00")).format((long)person.getAge())); 3.8、逆映射 在双向映射的情况下，例如从实体到DTO以及从DTO到实体，前向方法和反向方法的映射规则通常是相似的，并且可以通过切换source和来简单地反转target。 使用注释@InheritInverseConfiguration表示方法应继承相应反向方法的反向配置。 .... @Mapping(target = "age",source = "age", numberFormat = "#0.00") PersonDTO conver(Person person); @InheritInverseConfiguration Person conver(PersonDTO dto); ... 3.8、逆映射 在双向映射的情况下，例如从实体到DTO以及从DTO到实体，前向方法和反向方法的映射规则通常是相似的，并且可以通过切换source和来简单地反转target。 使用注释@InheritInverseConfiguration表示方法应继承相应反向方法的反向配置。 3.9、继承与共享配置 3.9.1、继承配置 方法级配置注解，例如@Mapping，@BeanMapping，@IterableMapping，等等，都可以继承从一个映射方法的类似使用注释方法@InheritConfiguration： @Mapper public interface CarMapper { @Mapping(target = "numberOfSeats", source = "seatCount") Car carDtoToCar(CarDto car); @InheritConfiguration void carDtoIntoCar(CarDto carDto, @MappingTarget Car car); } 上面的示例声明了一种carDtoToCar()具有配置的映射方法，该配置定义了应如何映射numberOfSeats类型中的属性Car。在现有Instance实例上执行映射的update方法Car需要相同的配置才能成功映射所有属性。通过声明@InheritConfiguration该方法，MapStruct可以搜索继承候选，以应用继承自该方法的注释。 如果所有类型的A（源类型和结果类型）都可以分配给B的相应类型，则一个方法A可以从另一种方法B继承配置。 如果可以使用多个方法作为继承的源，则必须在注释中指定方法名称：@InheritConfiguration( name = “carDtoToCar” )。 一种方法，可以使用==@InheritConfiguration==和覆盖或通过另外施加修改的配置@Mapping，@BeanMapping等等。 3.9.2、共享配置 MapStruct提供了通过指向带注释的中央接口来定义共享配置的可能性@MapperConfig。为了使映射器使用共享配置，需要在@Mapper#config属性中定义配置接口。 该@MapperConfig注释具有相同的属性@Mapper注释。任何未通过via指定的属性@Mapper都将从共享配置中继承。中指定@Mapper的属性优先于通过引用的配置类指定的属性。列表属性例如uses可以简单组合： @MapperConfig(unmappedTargetPolicy = ReportingPolicy.ERROR ) public interface CentralConfig { } @Mapper(config = CentralConfig.class } ) // Effective configuration: // @Mapper(uses = { CustomMapperViaMapper.class, CustomMapperViaMapperConfig.class }, // unmappedTargetPolicy = ReportingPolicy.ERROR // ) public interface SourceTargetMapper { ... } 共享配置config，配置一些检查策略，例如： unmappedSourcePolicy()、unmappedTargetPolicy() : 源或者目标没有标注映射的属性怎么报告。 typeConversionPolicy() :应该报告如何进行有损(缩小)转换，例如:long到integer的转换。 collectionMappingStrategy(): 集合类型映射策略。 其他的，请阅读源码。 四、使用自定义方法 4.1、自定义类型转换方法 public class DateMapper { public String asString(Date date) { return date != null ? new SimpleDateFormat( "yyyy-MM-dd" ) .format( date ) : null; } public Date asDate(String date) { try { return date != null ? new SimpleDateFormat( "yyyy-MM-dd" ) .parse( date ) : null; } catch ( ParseException e ) { throw new RuntimeException( e ); } } } mapper: @Mapper(uses=DateMapper.class) public interface PersonMapper{ PersonMapper INSTANCT = Mappers.getMapper(PersonMapper.class); PersonDTO conver(Person person); } impl: public class PersonMapperImpl implements PersonMapper { private final DateMapper dateMapper = new DateMapper(); public PersonMapperImpl() { } public PersonDTO conver(Person person) { .... personDTO.setCreateTime(this.dateMapper.asDate(person.getCreateTime())); ... return personDTO; } } 在进行类型转换的时候直接调用改转换方法，@Mapper#uses可以使用多个类。 4.2、使用@Qualifier @Qualifier标记的自定义注解标记的方法，必须有输入, 否则编译时会抛出异常。 public class DateFormtUtil { @DateFormat public static String dateToString(Date date){ return date == null ? "": new SimpleDateFormat("yyyy-MM-dd").format(date); } @Qualifier @Target(ElementType.METHOD) @Retention(RetentionPolicy.CLASS) public @interface DateFormat{} } mapper: @Mapper(uses =DateFormtUtil.class) public interface PersonMapper { PersonMapper INSTANCT = Mappers.getMapper(PersonMapper.class); @Mapping(target = "createTime",source = "createTime",qualifiedBy = DateFormat.class) PersonDTO conver(Person person); } 4.3、使用@namd public class DateFormtUtil { @Named("dateToString") public static String dateToString(Date date){ return date == null ? "": new SimpleDateFormat("yyyy-MM-dd").format(date); } } mapper: @Mapper(uses =DateFormtUtil.class) public interface PersonMapper { PersonMapper INSTANCT = Mappers.getMapper(PersonMapper.class); @Mapping(target = "createTime",source = "createTime",qualifiedByName = "dateToString") PersonDTO conver(Person person); } 效果跟@Qualifier是一样的。 五、集合映射 MapStruct有CollectionMappingStrategy，与可能的值：ACCESSOR_ONLY，SETTER_PREFERRED，ADDER_PREFERRED和TARGET_IMMUTABLE。 在下表中，破折号-表示属性名称。接下来，尾部s表示复数形式。该表解释了这些选项以及它们是如何施加到存在/不存在的set-s，add-s和/或get-s在目标对象上的方法： 选项 仅目标set-s可用 仅目标add-可用 既可以set-s/add- 没有set-s/add- 现有目标（@TargetType） ACCESSOR_ONLY set-s get-s set-s get-s get-s SETTER_PREFERRED set-s add- set-s get-s get-s ADDER_PREFERRED set-s add- add- get-s get-s TARGET_IMMUTABLE set-s exception set-s exception set-s 六、集成到 spring 在@Mapper#componentModel中指定依赖注入框架。 @Mapper(componentModel = "spring") public interface ModelMapper { ModelMapper INSTANT = Mappers.getMapper(ModelMapper.class); ModelVO conver(Model model); } // 直接在类中使用Autowired注入就行了 @RestController class MapperSpringController { @Autowired ModelMapper modelMapper; @GetMapping("/get") ModelVO getModle(){ Model model = new Model(); model.setId("123456"); model.setName("张三"); model.setCreate(new Date()); return modelMapper.conver(model); } } 七、高级运用 7.1、spi的运用 官方文档 关于spi的运用描述 7.2、freemarker生成代码 github链接
一、概述 1.1、循环依赖 一个或多个对象之间存在直接或间接的依赖关系，这种依赖关系构成一个环形调用，有下面 3 种方式。 一个 demo： @Service public class Louzai1 { @Autowired private Louzai2 louzai2; public void test1() { } } @Service public class Louzai2 { @Autowired private Louzai1 louzai1; public void test2() { } } 这是一个经典的循环依赖，它能正常运行。 1.2、三级缓存 解读源码流程之前，spring 内部的三级缓存逻辑必须了解。 第一级缓存：singletonObjects，用于保存实例化、注入、初始化完成的 bean 实例； 第二级缓存：earlySingletonObjects，用于保存实例化完成的 bean 实例； 第三级缓存：singletonFactories，用于保存 bean 创建工厂，以便后面有机会创建代理对象。 执行逻辑： 先从“第一级缓存”找对象，有就返回，没有就找“二级缓存”； 找“二级缓存”，有就返回，没有就找“三级缓存”； 找“三级缓存”，找到了，就获取对象，放到“二级缓存”，从“三级缓存”移除。 1.3、原理执行流程 整个执行逻辑如下： 1、在第一层中，先去获取 A 的 Bean，发现没有就准备去创建一个，然后将 A 的代理工厂放入“三级缓存”（这个 A 其实是一个半成品，还没有对里面的属性进行注入），但是 A 依赖 B 的创建，就必须先去创建 B； 2、在第二层中，准备创建 B，发现 B 又依赖 A，需要先去创建 A； 3、在第三层中，去创建 A，因为第一层已经创建了 A 的代理工厂，直接从“三级缓存”中拿到 A 的代理工厂，获取 A 的代理对象，放入“二级缓存”，并清除“三级缓存”； 4、回到第二层，现在有了 A 的代理对象，对 A 的依赖完美解决（这里的 A 仍然是个半成品），B 初始化成功； 5、回到第一层，现在 B 初始化成功，完成 A 对象的属性注入，然后再填充 A 的其它属性，以及 A 的其它步骤（包括 AOP），完成对 A 完整的初始化功能（这里的 A 才是完整的 Bean）。 6、将 A 放入“一级缓存”。 二、源码 2.1、代码入口 2.2、第一层 进入 doGetBean()，从 getSingleton() 没有找到对象，进入创建 Bean 的逻辑。 进入 doCreateBean() 后，调用 addSingletonFactory()。 往三级缓存 singletonFactories 塞入 louzai1 的工厂对象。 进入到 populateBean()，执行 postProcessProperties()，这里是一个策略模式，找到下图的策略对象。 正式进入该策略对应的方法。 下面都是为了获取 louzai1 的成员对象，然后进行注入。 进入 doResolveDependency()，找到 louzai1 依赖的对象名 louzai2。 需要获取 louzai2 的 bean，是 AbstractBeanFactory 的方法。 正式获取 louzai2 的 bean。 到这里，第一层套娃基本结束，因为 louzai1 依赖 louzai2，下面我们进入第二层套娃。 2.3、第二层 2.4、第三层 三、解析 “一级缓存”的作用，变量命名为 singletonObjects，结构是 Map<String, Object>，它就是一个单例池，将初始化好的对象放到里面，给其它线程使用，如果没有第一级缓存，程序不能保证 Spring 的单例属性。 “三级缓存”的作用，变量命名为 singletonFactories，结构是 Map<String, ObjectFactory<?>>，Map 的 Value 是一个对象的代理工厂，所以“三级缓存”的作用，其实就是用来存放对象的代理工厂。 那这个对象的代理工厂有什么作用呢，我先给出答案，它的主要作用是存放半成品的单例 Bean，目的是为了“打破循环”。 为什么“三级缓存”不直接存半成品的 A，而是要存一个代理工厂呢 ？答案是因为 AOP。 那“二级缓存”的作用就清楚了，就是用来存放对象工厂生成的对象，这个对象可能是原对象，也可能是个代理对象。 3.2、能去掉第 2 级缓存么 @Service public class A { @Autowired private B b; @Autowired private C c; public void test1() { } } @Service public class B { @Autowired private A a; public void test2() { } } @Service public class C { @Autowired private A a; public void test3() { } } 根据上面的套娃逻辑，A 需要找 B 和 C，但是 B 需要找 A，C 也需要找 A。 假如 A 需要进行 AOP，因为代理对象每次都是生成不同的对象，如果干掉第二级缓存，只有第一、三级缓存： B 找到 A 时，直接通过三级缓存的工厂的代理对象，生成对象 A1。 C 找到 A 时，直接通过三级缓存的工厂的代理对象，生成对象 A2。 通过 A 的工厂的代理对象，生成了两个不同的对象 A1 和 A2，所以为了避免这种问题的出现，我们搞个二级缓存，把 A1 存下来，下次再获取时，直接从二级缓存获取，无需再生成新的代理对象。 所以“二级缓存”的目的是为了避免因为 AOP 创建多个对象，其中存储的是半成品的 AOP 的单例 bean。 如果没有 AOP 的话，其实只要 1、3 级缓存，就可以满足要求。 四、总结 三级缓存的作用： 一级缓存：为“Spring 的单例属性”而生，就是个单例池，用来存放已经初始化完成的单例 Bean； 二级缓存：为“解决 AOP”而生，存放的是半成品的 AOP 的单例 Bean； 三级缓存：为“打破循环”而生，存放的是生成半成品单例 Bean 的工厂方法。
一、关于Starter 1.1、是什么 Starter是Spring Boot中的一个非常重要的概念，Starter相当于模块，它能将模块所需的依赖整合起来并对模块内的Bean根据环境（ 条件）进行自动配置。 使用者只需要依赖相应功能的Starter，无需做过多的配置和依赖，Spring Boot就能自动扫描并加载相应的模块并设置默认值，做到开箱即用。 1.2、Springboot 自动配置 SpringBoot中的starter是一种非常重要的机制，能够抛弃以前繁杂的配置，将其统一集成进starter，应用者只需要在maven中引入starter依赖，Spring Boot就能自动扫描各个jar包下classpath路径的spring.factories文件，加载自动配置类信息，加载相应的bean信息并启动相应的默认配置。 Spring Boot提供了针对日常企业应用研发各种场景的spring-boot-starter依赖模块。所有这些依赖模块都遵循着约定成俗的默认配置，并允许我们调整这些配置，即遵循“约定大于配置”的理念。 1.3、spring.factories Spring Boot会默认扫描跟启动类平级的包，如果我们的Starter跟启动类不在同一个主包下，需要通过配置spring.factories文件来配置生效，SpringBoot默认加载各个jar包下classpath路径的spring.factories文件，配置的key为： org.springframework.boot.autoconfigure.EnableAutoConfiguration 1.4、Starter开发常用注解 SpringBoot经过查找spring.factories文件，加载自动配置类，而自动配置类中定义了各种运行时判断条件，如@ConditionalOnMissingBean(A.class)等，只要ioc容器中没有指定的A类型的bean信息，该配置文件才会生效。 @Conditional是Spring4新提供的注解，它的作用是按照一定的条件进行判断，满足条件给容器注册bean。 属性映射注解： @ConfigurationProperties ：配置文件属性值和实体类的映射 @EnableConfigurationProperties : 和 @ConfigurationProperties 配合使用，把 @ConfigurationProperties 修饰的类加入ioc容器。 配置bean注解： @Configuration : 标识该类为配置类，并把该类注入ioc容器 @Bean : 一般在方法上使用，声明一个bean，bean名称默认是方法名称，类型为返回值。 条件注解： @Conditional 是根据条件类创建特定的Bean，条件类需要实现Condition接口，并重写matches接口来构造判断条件。 @ConditionalOnBean 容器中存在指定bean，才会实例化一个Bean @ConditionalOnMissingBean 容器中不存在指定bean，才会实例化一个Bean @ConditionalOnClass 系统中有指定类，才会实例化一个Bean @ConditionalOnMissingClass 系统中没有指定类，才会实例化一个Bean @ConditionalOnExpression 当SpEl表达式为true的时候，才会实例化一个Bean @AutoConfigureAfter 在某个bean完成自动配置后实例化这个bean @AutoConfigureBefore 在某个bean完成自动配置前实例化这个bean @ConditionalOnJava 系统中版本是否符合要求 @ConditionalOnSingleCandidate 当指定的Bean在容器中只有一个，或者有多个但是指定了首选的Bean时触发实例化 @ConditionalOnResource 类路径下是否存在指定资源文件 @ConditionalOnWebApplication 是web应用 @ConditionalOnNotWebApplication 不是web应用 @ConditionalOnJndi JNDI指定存在项 @ConditionalOnProperty 配置Configuration的加载规则 prefix 配置属性名称的前缀 value 数组，获取对应property名称的值，与name不可同时使用 name 数组，可与prefix组合使用，组成完整的配置属性名称，与value不可同时使用 havingValue 比较获取到的属性值与havingValue给定的值是否相同，相同才加载配置 matchIfMissing 缺少该配置属性时是否可以加载。如果为true，没有该配置属性时也会正常加载；反之则不会生效 1.5、Full全模式和Lite轻量级模式 @Configuration参数proxyBeanMethods： 1、Full 全模式（默认）：@Configuration(proxyBeanMethods = true) 同一配置类下，当直接调用@Bean修饰的方法注入的对象，则调用该方法会被代理，从ioc容器中取bean实列，所以实列是一样的。即单实例对象，在该模式下SpringBoot每次启动都会判断检查容器中是否存在该组件。 2、Lite 轻量级模式：@Configuration(proxyBeanMethods = false) 同一配置类下，当直接调用@Bean修饰的方法注入的对象，则调用该方法不会被代理，相当于直接调用一个普通方法，会有构造方法，但是没有bean的生命周期，返回的是不同的实例。 注：proxyBeanMethods 是为了让使用@Bean注解的方法被代理。而不是@Bean的单例多例的设置参数。 @Configuration(proxyBeanMethods = false) public class AppConfig { //放一份myBean到ioc容器 @Bean public Mybean myBean() { return new Mybean(); } //放一份yourBean到ioc容器 @Bean public YourBean yourBean() { System.out.println("=========="); //注意：@Configuration(proxyBeanMethods = false)：myBean()方法不代理，直接调用 //注意：@Configuration(proxyBeanMethods = true)：myBean()方法代理，从ioc容器拿 return new YourBean(myBean()); } } 什么时候用Full全模式，什么时候用Lite轻量级模式？ 当在你的同一个Configuration配置类中，注入到容器中的bean实例之间有依赖关系时，建议使用Full全模式。 当在你的同一个Configuration配置类中，注入到容器中的bean实例之间没有依赖关系时，建议使用Lite轻量级模式，以提高springboot的启动速度和性能。 1.6、Starter命名规范 Spring官方Starter通常命名为spring-boot-starter-{name} 如：spring-boot-starter-web Spring官方建议非官方Starter命名应遵循{name}-spring-boot-starter的格式：如mybatis-spring-boot-starter。 二、开发Starter 1、创建Starter项目 新建项目后，要删除main启动类。 2、添加依赖 <?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"> <parent> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-parent</artifactId> <version>2.6.1</version> <relativePath/> </parent> <modelVersion>4.0.0</modelVersion> <groupId>com.payn</groupId> <artifactId>payn-spring-boot-starter</artifactId> <version>1.0</version> <properties> <java.version>1.8</java.version> <maven.compiler.source>8</maven.compiler.source> <maven.compiler.target>8</maven.compiler.target> </properties> <dependencies> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-autoconfigure</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-configuration-processor</artifactId> <optional>true</optional> </dependency> </dependencies> </project> 我们没有main入口，需要去除pom文件中maven打包插件spring-boot-maven-plugin。 spring-boot-configuration-processor作用： spring-boot-configuration-processor其实是一个注解处理器，在编译阶段干活的，一般在maven的声明都是optional 为true。 你在idea里面可以点击port，进到这个字段里面，还可以看到配置的提示信息。 这是因为在你的资源文件里面有一个spring-configuration-metadata.json文件，这是spring配置的元数据，是json形式。 3、编写属性类 @ConfigurationProperties可以定义一个配置信息类，和配置文件进行映射。 @ConfigurationProperties(prefix = "payn.config") public class HelloProperties { private String name = "hello 默认值！"; private int age = 8; 4、自定义业务类 这里可以模拟一些获取了配置文件信息的进行业务操作的业务类。 public class HelloService { private String name; private int age; public String getName() { return name; } public void setName(String name) { this.name = name; } public int getAge() { return age; } public void setAge(int age) { this.age = age; } public String hello() { return "HelloService{" + "name='" + name + ''' + ", age=" + age + '}'; } } 5、编写自动配置类 命名规范：XxxAutoConfiguration。 @Configuration(proxyBeanMethods = false) // 当存在某个类时，此自动配置类才会生效 @ConditionalOnClass(value = {HelloService.class}) // 导入我们自定义的配置类,供当前类使用 @EnableConfigurationProperties(value = HelloProperties.class) // 只有非web应用程序时此自动配置类才会生效 @ConditionalOnWebApplication //判断payn.config.flag的值是否为“true”， matchIfMissing = true：没有该配置属性时也会正常加载 @ConditionalOnProperty(prefix = "payn.config", name = "flag", havingValue = "true", matchIfMissing = true) public class HelloAutoConfiguration { /** * @param helloProperties 直接方法签名入参注入HelloProperties,也可以使用属性注入 * @return */ @Bean @ConditionalOnMissingBean(HelloService.class) //@ConditionalOnProperty(prefix = "ljw.config", name = "flag", havingValue = "true", matchIfMissing = true) public HelloService helloService(HelloProperties helloProperties) { HelloService helloService = new HelloService(); //把获取的信息注入 helloService.setName(helloProperties.getName()); helloService.setAge(helloProperties.getAge()); return helloService; } } 注：这里配置一个web应用才能注入，并且payn.config.flag的值是否为“true”或者不配置该key才能注入HelloService服务。 6、编写spring.factories 把自动配置类HelloAutoConfiguration配置到 org.springframework.boot.autoconfigure.EnableAutoConfiguration的key 下，springboot会自动加载该文件并根据条件装配。 org.springframework.boot.autoconfigure.EnableAutoConfiguration=\ com.payn.starter.config.HelloAutoConfiguration 7、编写配置提示文件（非必须） additional-spring-configuration-metadata.json 配置additional-spring-configuration-metadata.json文件后，在开发人员的IDE工具使用个人编写的配置读取很有效的在application.properties或application.yml文件下完成提示。 {"properties": [ { "name": "ljw.config.name", "type": "java.lang.String", "defaultValue": "hello 默认值！这里配置的是提示，真正默认值在Properties里面", "description": "这是字符串名称啊." }, { "name": "ljw.config.age", "defaultValue": 8, "description": "这是int类型的年龄啊.", "deprecation": { "reason": "过时原因.", "replacement": "替代key是：ljw.config.age22", "level": "warning" } } ]} 参考下面properties表格进行配置上的理解。 名称 类型 目的 name String 属性的全名。名称采用小写的周期分隔形式(例如server.address)。此属性是强制性的。 type String 属性的数据类型的完整签名（例如java.lang.String），但也是完整的泛型类型（例如java.util.Map<java.util.String,acme.MyEnum>）。您可以使用此属性来指导用户可以输入的值的类型。为了保持一致性，通过使用其包装对应项（例如，boolean变为java.lang.Boolean）来指定基元的类型。请注意，此类可能是一个复杂类型，它从Stringas绑定的值转换而来。如果类型未知或基本类型，则可以省略。 description String 可以向用户显示的组的简短描述。如果没有可用的描述，则可以省略。建议描述为简短段落，第一行提供简明摘要。描述中的最后一行应以句点（.）结尾。 sourceType String 贡献此属性的源的类名称。例如，如果属性来自带注释的类@ConfigurationProperties，则此属性将包含该类的完全限定名称。如果源类型未知，则可以省略。 defaultValue Object 默认值，如果未指定属性，则使用该值。如果属性的类型是数组，则它可以是值数组。如果默认值未知，则可以省略。 deprecation 数组 过时的描述。 deprecation每个properties元素的属性中包含的JSON对象可以包含以下属性： 名称 类型 目的 level String 弃用级别，可以是warning（默认）或error。当属性具有warning弃用级别时，它仍应绑定在环境中。但是，当它具有error弃用级别时，该属性不再受管理且不受约束。 reason String 该属性被弃用的原因的简短描述。如果没有可用的原因，可以省略。建议描述为简短段落，第一行提供简明摘要。描述中的最后一行应以句点（.）结尾。 replacement String 替换此不推荐使用的属性的属性的全名。如果此属性没有替换，则可以省略。 spring-configuration-metadata.json spring-configuration-metadata.json代码量挺大的，为了方便我们可以通过IDE来生成，这里使用的是idea。 在idea设置中搜索Annotation Processors，接下来勾住Enable annonation processing就完成了。 在编译打包后的文件中看到自动生成的spring-configuration-metadata.json。这个文件不用我们编写。 下面是自动生成的： { "groups": [ { "name": "test.config", "type": "com.test.starter.properties.HelloProperties", "sourceType": "com.test.starter.properties.HelloProperties" } ], "properties": [ { "name": "test.config.age", "type": "java.lang.Integer", "sourceType": "com.test.starter.properties.HelloProperties", "defaultValue": 8 }, { "name": "test.config.name", "type": "java.lang.String", "sourceType": "com.test.starter.properties.HelloProperties", "defaultValue": "hello 默认值！" } ], "hints": [] } 三、测试Starter 1、前置环境 install打包自定义starter项目：payn-spring-boot-starter。 新建项目：payn-test-spring-boot-starter。 2、添加依赖 引入打好包的自定义starter。 <dependencies> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <dependency> <groupId>com.test</groupId> <artifactId>payn-spring-boot-starter</artifactId> <version>1.0-SNAPSHOT</version> </dependency> </dependencies> 3、测试类 @Service public class TestController implements CommandLineRunner { /** * 注入自定义starter服务 */ @Resource private HelloService helloService; @Override public void run(String... args) throws Exception { System.out.println(helloService.hello()); } } 4、修改配置文件 输入前缀可以看出已经有提示了 test.config.name=payn hello! test.config.age=99 test.config.flag=true #不会注入 #test.config.flag=true1 # 可以看到哪些自动配置了 debug=true 5、运行程序打印 HelloService{name='payn hello!', age=99} 条件注入： 如果没有spring-boot-starter-web依赖，不能注入服务HelloService。 如果配置了payn.config.flag，值不是true，不能注入服务HelloService；如果不配置payn.config.flag，可以注入。 6、查看自动配置类生效的方法 查看自动配置类生效的方法： 通过启用 debug=true 属性，让控制台打印自动配置报告，这样就可以很方便地知道哪些自动配置类生效。 HelloAutoConfiguration matched: - @ConditionalOnClass found required class 'com.payn.starter.service.HelloService' (OnClassCondition) - @ConditionalOnWebApplication (required) found 'session' scope (OnWebApplicationCondition) - @ConditionalOnProperty (ljw.config.flag=true) matched (OnPropertyCondition) HelloAutoConfiguration#helloService matched: - @ConditionalOnMissingBean (types: com.payn.starter.service.HelloService; SearchStrategy: all) did not find a
一、直接杀死进程 ps -ef | grep ink-blog.jar kill -9 17186 二、 SpringBoot 提供的actuator的功能 可以执行shutdown, health, info等，默认情况下，actuator的shutdown是disable的，我们需要打开它。首先引入acturator的maven依赖。 <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-actuator</artifactId> </dependency> 然后将shutdown节点打开，也将/actuator/shutdown暴露web访问也设置上，除了shutdown之外还有health, info的web访问都打开的话将 management.endpoints.web.exposure.include=* 就可以。将如下配置设置到application.properties里边。设置一下服务的端口号为3333。 server.port=3333 management.endpoint.shutdown.enabled=true management.endpoints.web.exposure.include=shutdown 接下来，创建一个SpringBoot工程，然后设置一个bean对象，配置上PreDestroy方法。这样在停止的时候会打印语句。bean的整个生命周期分为创建、初始化、销毁，当最后关闭的时候会执行销毁操作。在销毁的方法中执行一条输出日志。 public class TerminateBean{ @PreDestroy publicvoid preDestroy() { System.out.println("TerminalBean is destroyed"); } } 做一个configuration，然后提供一个获取bean的方法，这样该bean对象会被初始化。 @Configuration public class ShutDownConfig{ @Bean public TerminateBean getTerminateBean() { return new TerminateBean(); } } 在启动类里边输出一个启动日志，当工程启动的时候，会看到启动的输出，接下来执行停止命令。 curl -X POST http://localhost:3333/actuator/shutdown 以下日志可以输出启动时的日志打印和停止时的日志打印，同时程序已经停止。 三、获取程序启动时候的context，然后关闭主程序启动时的context 这样程序在关闭的时候也会调用PreDestroy注解。 /* method 2: use ctx.close to shutdown all application context */ ConfigurableApplicationContext ctx = SpringApplication.run(ShutdowndemoApplication.class, args); try { TimeUnit.SECONDS.sleep(10); } catch (InterruptedException e) { e.printStackTrace(); } ctx.close(); 四、在SpringBoot启动的时候将进程号写入一个app.pid文件，生成的路径是可以指定的 可以通过命令 cat /Users/test/app.id | xargs kill 命令直接停止服务，这个时候bean对象的PreDestroy方法也会调用的。这种方法大家使用的比较普遍。写一个start.sh用于启动springboot程序，然后写一个停止程序将服务停止。 /* method 3 : generate a pid in a specified path, while use command to shutdown pid : 'cat /Users/test/app.pid | xargs kill' */ SpringApplication application = new SpringApplication(ShutdowndemoApplication.class); application.addListeners(new ApplicationPidFileWriter("/Users/test/app.pid")); application.run(); 五、通过调用一个SpringApplication.exit(）方法也可以退出程序 同时将生成一个退出码，这个退出码可以传递给所有的context。这个就是一个JVM的钩子，通过调用这个方法的话会把所有PreDestroy的方法执行并停止，并且传递给具体的退出码给所有Context。通过调用System.exit(exitCode)可以将这个错误码也传给JVM。程序执行完后最后会输出：Process finished with exit code 0，给JVM一个SIGNAL。 /* method 4: exit this application using static method */ ConfigurableApplicationContext ctx = SpringApplication.run(ShutdowndemoApplication.class, args); exitApplication(ctx); publicstaticvoid exitApplication(ConfigurableApplicationContext context) { int exitCode = SpringApplication.exit(context, (ExitCodeGenerator) () -> 0); System.exit(exitCode); } 六、自己写一个Controller，然后将自己写好的Controller获取到程序的context，然后调用自己配置的Controller方法退出程序 通过调用自己写的/shutDownContext方法关闭程序： curl -X POST http://localhost:3333/shutDownContext。 @RestController public class ShutDownControllerimplementsApplicationContextAware { private ApplicationContext context; @PostMapping("/shutDownContext") public String shutDownContext() { ConfigurableApplicationContext ctx = (ConfigurableApplicationContext) context; ctx.close(); return "context is shutdown"; } @GetMapping("/") public String getIndex() { return "OK"; } @Override public void setApplicationContext(ApplicationContext applicationContext)throwsBeansException { context = applicationContext; } } 以上这几种方法实现的话比较简单，但是真实工作中还需要考虑的点还很多，比如需要保护暴露的点不被别人利用，一般要加一些防火墙，或者只在内网使用，保证程序安全。 在真实的工作中的时候第四种比较常用，程序中一般使用内存队列或线程池的时候最好要优雅的关机，将内存队列没有处理的保存起来或线程池中没处理完的程序处理完。但是因为停机的时候比较快，所以停服务的时候最好不要处理大量的数据操作，这样会影响程序停止。
一、功能 用 Mina 框架，同时实现服务器和客户端，实现客户端与客户端之间的 IM 通信，使用 Java JDBC 接口连入 MySQL 数据库。 二、数据库登录处理逻辑 package database; import java.sql.Connection; import java.sql.DriverManager; import java.sql.PreparedStatement; import java.sql.ResultSet; import java.sql.SQLException; import java.sql.Statement; import org.apache.mina.core.session.IoSession; import util.MessageUtil; import util.SessionUtil; import common.Common; /** * 管理客户端的数据库 */ public class ClientJDBC { public ClientJDBC() { try { // 加载JDBC Driver Class.forName("com.mysql.jdbc.Driver"); } catch (ClassNotFoundException e) { e.printStackTrace(); } } /** * * 1、查询是否已存在此用户 * 2、注册，添加新数据 */ public void addNewData(String name, String password, IoSession loginSession) { try { // 建立连接 Connection connection = DriverManager.getConnection( "jdbc:mysql://localhost:3306/clientdb", "root", ""); // 创建Statement Statement statement = connection.createStatement(); // 查询是否已存在此用户 String selectSQL = "select password from client where name=" + "'" + name + "'"; ResultSet set = statement.executeQuery(selectSQL); // 如果不存在此用户 if (!set.next()) { // 添加数据 PreparedStatement preparedStatement = connection .prepareStatement("insert into client values(null,?,?)"); preparedStatement.setString(1, name); preparedStatement.setString(2, password); preparedStatement.executeUpdate(); // 提示用户注册成功 loginSession.write(Common.CANREG); } else { // 提示用户已注册 loginSession.write(Common.CANTRE); } connection.close(); } catch (SQLException e) { e.printStackTrace(); } } /** * * 1、查询帐号密码是否匹配 * 2、查询是否用户已登录 * 3、登录 */ public boolean login(String name, String password, IoSession userSession) { try { // 建立连接 Connection connection = DriverManager.getConnection( "jdbc:mysql://localhost:3306/clientdb", "root", ""); // 创建Statement Statement statement = connection.createStatement(); // 查询是否已存在此用户 String selectSQL = "select password from client where name=" + "'" + name + "'"; ResultSet set = statement.executeQuery(selectSQL); // 如果存在此用户 if (set.next()) { // 密码匹配成功 if (set.getString("password").equals(password)) { // 如果用户没登录 if (SessionUtil.getSessionFromName(name) == null) { // 存储用户名 userSession.setAttribute("name", name); // 登录成功 String s = name; String message = MessageUtil.createSendMessage(Common.LOGIN_SUCCESS, s); userSession.write(message); // 发送在线列表给所有客户端 SessionUtil.sendClientListToAll(); } else { // 提示用户已登录 userSession.write(Common.IS_LOGIN); } } else { // 提示帐号和密码不匹配 userSession.write(Common.ISNT_MATCH); } } else { // 提示帐号和密码不匹配 userSession.write(Common.ISNT_MATCH); } connection.close(); } catch (SQLException e) { e.printStackTrace(); } return true; } } 服务器端Handler处理逻辑。 注意，此处继承的是StreamIoHandler，为了后续对传输文件进行操作。 package server; import java.io.File; import java.io.FileNotFoundException; import java.io.FileOutputStream; import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; import java.util.concurrent.ArrayBlockingQueue; import java.util.concurrent.ThreadPoolExecutor; import java.util.concurrent.TimeUnit; import org.apache.mina.core.session.IdleStatus; import org.apache.mina.core.session.IoSession; import org.apache.mina.handler.stream.StreamIoHandler; import thread.IoStreamThreadWork; import util.SessionUtil; import common.Common; /** * Mina Handler */ public class ServerHandler extends StreamIoHandler { @Override public void exceptionCaught(IoSession session, Throwable cause) { System.out.println(session.getRemoteAddress() + " " + "exceptionCaught"); System.out.println(cause); } /** * mina只能接收以换行符结束的信息 */ @Override public void messageReceived(IoSession session, Object message) { System.out.println(session.getRemoteAddress() + " " + "messageReceived"); // 解析信息 String result = (String) message; // 注册信息 if (result.startsWith(Common.REGISTER) && result.length() > 10) { String suffix = result.substring(10); String[] array = suffix.split(","); MinaServer.clientJDBC.addNewData(array[0], array[1], session); } // 登录信息 else if (result.startsWith(Common.LOGIN) && result.length() > 10) { String suffix = result.substring(10); String[] array = suffix.split(","); MinaServer.clientJDBC.login(array[0], array[1], session); } // 登出信息 else if (result.equals(Common.LOGOUT) && result.length() == 6) { // 关闭当前session session.close(); // 发送在线列表给所有客户端 SessionUtil.sendClientListToAll(); } // 用户之间发送消息 else if (result.startsWith(Common.CLIENT) && result.length() > 10) { String suffix = result.substring(10); // 分隔选中用户名和要发送的消息 String[] array = suffix.split(Common.SEPARATOR); // 向指定用户发送消息 if (session.getAttribute("name") != null) SessionUtil.sendToSelectedClient( (String) session.getAttribute("name"), array[0], array[1]); } } @Override public void messageSent(IoSession session, Object message) throws Exception { System.out.println(session.getRemoteAddress() + " " + "messageSent"); } @Override public void sessionClosed(IoSession session) throws Exception { System.out .println(session.getRemoteAddress() + " " + "sessionClosed"); } /** * 用户创建的时候保留会话的ip和端口 */ @Override public void sessionCreated(IoSession session) throws Exception { System.out.println(session.getRemoteAddress() + " " + "sessionCreated"); // 提取客户端ip和端口 String s = session.getRemoteAddress() + ""; String port = s.substring(s.indexOf(":") + 1); String ip = s.substring((s.indexOf("/") + 1), s.length() - 1 - port.length()); // 存储ip和端口 if (session.getAttribute("ip") == null) session.setAttribute("ip", ip); if (session.getAttribute("port") == null) session.setAttribute("port", port); } /** * 会话空闲状态 */ @Override public void sessionIdle(IoSession session, IdleStatus status) { System.out.println(session.getRemoteAddress() + " " + "sessionIdle"); } @Override public void sessionOpened(IoSession session) { System.out .println(session.getRemoteAddress() + " " + "sessionOpened"); } @Override protected void processStreamIo(IoSession session, InputStream input, OutputStream output) { System.out.println(session.getRemoteAddress() + " " + "processStreamIo"); // 设定一个线程池 // 参数说明：最少数量3，最大数量6 空闲时间 3秒 ThreadPoolExecutor threadPool = new ThreadPoolExecutor(3, 6, 3, TimeUnit.SECONDS, // 缓冲队列为3 new ArrayBlockingQueue<Runnable>(3), // 抛弃旧的任务 new ThreadPoolExecutor.DiscardOldestPolicy()); FileOutputStream fos = null; // 此处路径如何动态设定。 File receiveFile = new File("F:\\111.jks"); try { fos = new FileOutputStream(receiveFile); } catch (FileNotFoundException e1) { e1.printStackTrace(); } finally { try { fos.close(); } catch (IOException e) { e.printStackTrace(); } } // 将线程放入线程池 当连接很多时候可以通过线程池处理 threadPool.execute(new IoStreamThreadWork(input, fos)); } } 客户端Handler处理逻辑： package client; import java.io.File; import java.io.FileInputStream; import java.io.FileNotFoundException; import java.io.InputStream; import java.io.OutputStream; import javax.swing.DefaultListModel; import javax.swing.JOptionPane; import javax.swing.JTextArea; import org.apache.mina.core.session.IdleStatus; import org.apache.mina.core.session.IoSession; import org.apache.mina.handler.stream.StreamIoHandler; import thread.IoStreamThreadWork; import common.Common; import frame.ChatFrame; import frame.LoginFrame; /** * Mina Handler */ // IoHandlerAdapter public class ClientHandler extends StreamIoHandler { @Override public void exceptionCaught(IoSession session, Throwable cause) { System.out.println(session.getLocalAddress() + " " + "exceptionCaught"); System.out.println(cause); } /** * mina只能接收以换行符结束的信息 */ @Override public void messageReceived(IoSession session, Object message) { System.out.println(session.getLocalAddress() + " " + "messageReceived"); // 解析信息 String result = (String) message; // 注册成功 if (result.equals(Common.CANREG) && result.length() == 6) { JOptionPane.showMessageDialog(null, "注册成功！"); } // 用户已存在注册失败 else if (result.equals(Common.CANTRE) && result.length() == 6) { JOptionPane.showMessageDialog(null, "用户已存在！"); } // 用户已登录，登录失败 else if (result.equals(Common.IS_LOGIN) && result.length() == 6) { JOptionPane.showMessageDialog(null, "用户已登录！"); session.close();// 关闭这个Session } // 帐号和密码不匹配，登录失败 else if (result.equals(Common.ISNT_MATCH) && result.length() == 6) { JOptionPane.showMessageDialog(null, "帐号和密码不匹配！"); session.close();// 关闭这个Session } // 登录成功 else if (result.startsWith(Common.LOGIN_SUCCESS) && result.length() > 10) { String suffix = result.substring(10); // 从Attribute中取出登录窗口 LoginFrame frame = (LoginFrame) session.getAttribute("loginFrame"); // 关闭登录窗口 frame.dispose(); // 打开聊天窗口 new ChatFrame(session).setTitle("用户" + suffix + "的聊天窗口"); } // 服务器发送在线列表消息 else if (result.startsWith(Common.SERVER) && result.length() > 10) { String suffix = result.substring(10); String[] array = suffix.split(","); // 判断空，因为有loginSession if (session.getAttribute("clientListModel") != null) { // 取出clientListModel DefaultListModel<String> clientListModel = (DefaultListModel<String>) (session .getAttribute("clientListModel")); // 清空原视图 clientListModel.removeAllElements(); // 重新加入在线用户列表 for (String clientName : array) clientListModel.addElement(clientName); } } // 客户发送过来的消息 else if (result.startsWith(Common.CLIENT) && result.length() > 10) { String suffix = result.substring(10); // if (session.getAttribute("receivedMessage") != null) JTextArea jta = (JTextArea) session.getAttribute("receivedMessage"); jta.append(suffix + "\n"); // 滚动到底端 jta.setCaretPosition(jta.getText().length()); } } @Override public void messageSent(IoSession session, Object message) throws Exception { System.out.println(session.getLocalAddress() + " " + "messageSent"); } @Override public void sessionClosed(IoSession session) throws Exception { System.out.println(session.getLocalAddress() + " " + "sessionClosed"); } @Override public void sessionCreated(IoSession session) throws Exception { System.out .println(session.getLocalAddress() + " " + "sessionCreated"); } /** * 会话空闲状态 */ @Override public void sessionIdle(IoSession session, IdleStatus status) { System.out.println(session.getLocalAddress() + " " + "sessionIdle"); } @Override public void sessionOpened(IoSession session) { System.out.println(session.getLocalAddress() + " " + "sessionOpened"); } @Override protected void processStreamIo(IoSession session, InputStream input, OutputStream output) { System.out.println(session.getLocalAddress() + " " + "processStreamIo"); // 客户端发送文件 File sendFile = new File("e:\\MyKey.jks"); FileInputStream fis = null; try { fis = new FileInputStream(sendFile); } catch (FileNotFoundException e) { e.printStackTrace(); } // 放入线程让其执行 // 客户端一般都用一个线程实现即可 不用线程池 new IoStreamThreadWork(fis, output).start(); return; } } 在 Mina 的使用过程中，应注意以下问题： 1、客户端IoSession.setAttribute后，服务器IoSession.getAttribute为null，这两个session虽然getId一样，但不是同一个session。 2、使用TextLineCodecFactory，messageReceived方法默认只能接收以换行符结束的信息。 3、使用getAttribute时应注意判断空指针：if (session.getAttribute("name") != null && session.getAttribute("name").equals(name))。否则不会报错，但下面的程序段会终止执行。可在exceptionCaught方法中catch到异常 4、每个客户端对应一个ConnectFuture，每个ConnectFuture对应一个IoSession。 5、可以巧用session.setAttribute("loginFrame", LoginFrame.this);来进行窗口的传递，注意：如果在监听中直接传this，要注意是不是LoginFrame.this。 6、mina不能动态修改acceptor的ProtocolCodecFilter和Handler。 7、传输文件可通过StreamIoHandler和MessageEncoder的方式。 开源项目： MiniWeChat-Server minaSpring
一、关于 由于传统的 Socket 网络编程基于一个线程对应一个客户端的实现方式，大量的线程创建和销毁导致性能下降，无法应对高并发量的访问，所以基于服务器端的网络通信开发，我们常用 Mina 网络通信框架，即常说的 Java NIO ( java non-blocking IO ) 开发。 Mina 的几个重要接口： •IoServiece ：这个接口在一个线程上负责套接字的建立，拥有自己的 Selector，监听是否有连接被建立。 •IoProcessor ：这个接口在另一个线程上负责检查是否有数据在通道上读写，也就是说它也拥有自己的 Selector，这是与我们使用 JAVA NIO 编码时的一个不同之处，通常在 JAVA NIO 编码中，我们都是使用一个 Selector，也就是不区分 IoService与 IoProcessor 两个功能接口。另外，IoProcessor 负责调用注册在 IoService 上的过滤器，并在过滤器链之后调用 IoHandler。 •IoAccepter ：相当于网络应用程序中的服务器端 •IoConnector ：相当于客户端 •IoSession ：当前客户端到服务器端的一个连接实例 •IoHandler ：这个接口负责编写业务逻辑，也就是接收、发送数据的地方。这也是实际开发过程中需要用户自己编写的部分代码。 •IoFilter ：过滤器用于悬接通讯层接口与业务层接口，这个接口定义一组拦截器，这些拦截器可以包括日志输出、黑名单过滤、数据的编码（write 方向）与解码（read 方向）等功能，其中数据的 encode与 decode是最为重要的、也是你在使用 Mina时最主要关注的地方。 Mina 的一个重要的类 IoHandlerAdapter，此类仅仅是实现了 IoHandler 接口，但并不做任何处理。 一个 IoHandler 接口中具有如下一些方法（摘自 MINA 的 API 文档）： •void exceptionCaught(IoSession session, Throwable cause) 当接口中其他方法抛出异常未被捕获时触发此方法 •void messageReceived(IoSession session, Object message) 当接收到客户端的请求信息后触发此方法 •void messageSent(IoSession session, Object message) 当信息已经传送给客户端后触发此方法 •void sessionClosed(IoSession session) 当连接被关闭时触发，例如客户端程序意外退出等等 •void sessionCreated(IoSession session) 当一个新客户端连接后触发此方法 •void sessionIdle(IoSession session, IdleStatus status) 当连接空闲时触发此方法 •void sessionOpened(IoSession session) 当连接后打开时触发此方法，一般此方法与 sessionCreated 会被同时触发 二、开发流程 Mina 服务器网络通信框架开发流程： 1、导入相关 jar 包: mina-core-2.0.13.jar、slf4j-api-1.7.14.jar 2、创建acceptor，绑定Handler，设置Filter，绑定端口。 NioSocketAcceptor acceptor = new NioSocketAcceptor(); acceptor.setHandler(new MyServerHandler()); // 获取拦截器，拦截器作用：把字节转成对象 // TextLineCodecFactory 把数据进行加解码 acceptor.getFilterChain().addLast("codec", new ProtocolCodecFilter(new TextLineCodecFactory())); // 5秒钟服务器和客户端没有进行读写，则进入空闲状态Idle acceptor.getSessionConfig().setIdleTime(IdleStatus.BOTH_IDLE, 5); //绑定9898端口 acceptor.bind(new InetSocketAddress(9898)); 3、创建自定义的Handler。 package com.example.server; import org.apache.mina.core.service.IoHandlerAdapter; import org.apache.mina.core.session.IdleStatus; import org.apache.mina.core.session.IoSession; public class MyServerHandler extends IoHandlerAdapter { @Override public void exceptionCaught(IoSession session, Throwable cause) throws Exception { System.out.println("exceptionCaught"); } @Override public void messageReceived(IoSession session, Object message) throws Exception { String s = (String) message; System.out.println("messageReceived: " + s); session.write("server replay: " + s); } @Override public void messageSent(IoSession session, Object message) throws Exception { System.out.println("messageSent"); } @Override public void sessionClosed(IoSession session) throws Exception { System.out.println("sessionClosed"); } @Override public void sessionCreated(IoSession session) throws Exception { System.out.println("sessionCreated"); } /** * 会话空闲状态 */ @Override public void sessionIdle(IoSession session, IdleStatus status) throws Exception { System.out.println("sessionIdle"); } @Override public void sessionOpened(IoSession session) throws Exception { System.out.println("sessionOpened"); } } 4、由于默认的 Filter: TextLineCodeFactory 只能接收以换行符为结束的消息，有时为满足特定需求，需要自定义一个 Factory。 package com.example.server; import org.apache.mina.core.session.IoSession; import org.apache.mina.filter.codec.ProtocolCodecFactory; import org.apache.mina.filter.codec.ProtocolDecoder; import org.apache.mina.filter.codec.ProtocolEncoder; public class MyTextLineFactory implements ProtocolCodecFactory { private MyTextLineDecoder mDecoder; private MyTextLineEncoder mEncoder; private MyTextLineCumulativeDecoder mCumulativeDecoder; public MyTextLineFactory() { mDecoder = new MyTextLineDecoder(); mEncoder = new MyTextLineEncoder(); mCumulativeDecoder = new MyTextLineCumulativeDecoder(); } // 加密 @Override public ProtocolDecoder getDecoder(IoSession arg0) throws Exception { // return mDecoder; // 解决没检测到\n时的数据丢失问题 return mCumulativeDecoder; } // 解密 @Override public ProtocolEncoder getEncoder(IoSession arg0) throws Exception { return mEncoder; } } MyTextLineEncoder package com.example.server; import java.nio.charset.Charset; import java.nio.charset.CharsetEncoder; import org.apache.mina.core.buffer.IoBuffer; import org.apache.mina.core.session.IoSession; import org.apache.mina.filter.codec.ProtocolEncoder; import org.apache.mina.filter.codec.ProtocolEncoderOutput; public class MyTextLineEncoder implements ProtocolEncoder { @Override public void dispose(IoSession session) throws Exception { } @Override public void encode(IoSession session, Object message, ProtocolEncoderOutput output) throws Exception { String s = null; if (message instanceof String) { s = (String) message; } if (s != null) { // 系统默认的Encoder CharsetEncoder charsetEncoder = (CharsetEncoder) session .getAttribute("encoder"); if (charsetEncoder == null) { charsetEncoder = Charset.defaultCharset().newEncoder(); session.setAttribute("encoder", charsetEncoder); } IoBuffer ioBuffer = IoBuffer.allocate(s.length()); ioBuffer.setAutoExpand(true);// 自动扩展 ioBuffer.putString(s, charsetEncoder); ioBuffer.flip(); output.write(ioBuffer); } } } MyTextLineDecoder package com.example.server; import org.apache.mina.core.buffer.IoBuffer; import org.apache.mina.core.session.IoSession; import org.apache.mina.filter.codec.ProtocolDecoder; import org.apache.mina.filter.codec.ProtocolDecoderOutput; public class MyTextLineDecoder implements ProtocolDecoder { @Override public void decode(IoSession session, IoBuffer ioBuffer, ProtocolDecoderOutput output) throws Exception { // 起始位置 int startPosition = ioBuffer.position(); // 是否还有数据 while (ioBuffer.hasRemaining()) { byte b = ioBuffer.get(); // 读取到\n if (b == '\n') { // 当前位置 int currentPosition = ioBuffer.position(); // 当前总长度,指向末尾 int limit = ioBuffer.limit(); // 截取行 ioBuffer.position(startPosition); ioBuffer.limit(currentPosition); IoBuffer buf = ioBuffer.slice(); // 把buf中的数据写入到dest byte[] dest = new byte[buf.limit()]; buf.get(dest); String str = new String(dest); output.write(str); // 还原位置 ioBuffer.position(currentPosition); ioBuffer.limit(limit); } } } @Override public void dispose(IoSession arg0) throws Exception { } @Override public void finishDecode(IoSession arg0, ProtocolDecoderOutput arg1) throws Exception { } } 5、为了解决数据丢失问题，我们常使用 CumulativeProtocolDecoder。 package com.example.server; import org.apache.mina.core.buffer.IoBuffer; import org.apache.mina.core.session.IoSession; import org.apache.mina.filter.codec.CumulativeProtocolDecoder; import org.apache.mina.filter.codec.ProtocolDecoderOutput; /** * 处理数据丢失 */ public class MyTextLineCumulativeDecoder extends CumulativeProtocolDecoder { @Override protected boolean doDecode(IoSession ioSession, IoBuffer ioBuffer, ProtocolDecoderOutput output) throws Exception { // 起始位置 int startPosition = ioBuffer.position(); // 是否还有数据 while (ioBuffer.hasRemaining()) { byte b = ioBuffer.get(); // 读取到\n if (b == '\n') { // 当前位置 int currentPosition = ioBuffer.position(); // 当前总长度,指向末尾 int limit = ioBuffer.limit(); // 截取行 ioBuffer.position(startPosition); ioBuffer.limit(currentPosition); IoBuffer buf = ioBuffer.slice(); // 把buf中的数据写入到dest byte[] dest = new byte[buf.limit()]; buf.get(dest); String str = new String(dest); output.write(str); // 还原位置 ioBuffer.position(currentPosition); ioBuffer.limit(limit); return true;// 读取完成 } } ioBuffer.position(startPosition); return false;// 读取未完成 } } 至此，整个服务器搭建框架就完了。
一、简介 1.1、使用SpringBoot 1）创建SpringBoot应用，选中我们需要的模块； 2）SpringBoot已经默认将这些场景配置好了，只需要在配置文件中指定少量配置就可以运行起来。 3）自己编写业务代码； 自动配置原理： 这个场景SpringBoot帮我们配置了什么？能不能修改？能修改哪些配置？能不能扩展？ xxxxAutoConfiguration 给容器中自动配置组件 xxxxProperties 配置类来封装配置文件的内容 1.2、SpringBoot对静态资源的映射 @ConfigurationProperties(prefix = "spring.resources", ignoreUnknownFields = false) public class ResourceProperties implements ResourceLoaderAware { //可以设置和静态资源有关的参数，缓存时间等 WebMvcAuotConfiguration： @Override public void addResourceHandlers(ResourceHandlerRegistry registry) { if (!this.resourceProperties.isAddMappings()) { logger.debug("Default resource handling disabled"); return; } Integer cachePeriod = this.resourceProperties.getCachePeriod(); if (!registry.hasMappingForPattern("/webjars/**")) { customizeResourceHandlerRegistration( registry.addResourceHandler("/webjars/**") .addResourceLocations( "classpath:/META-INF/resources/webjars/") .setCachePeriod(cachePeriod)); } String staticPathPattern = this.mvcProperties.getStaticPathPattern(); //静态资源文件夹映射 if (!registry.hasMappingForPattern(staticPathPattern)) { customizeResourceHandlerRegistration( registry.addResourceHandler(staticPathPattern) .addResourceLocations( this.resourceProperties.getStaticLocations()) .setCachePeriod(cachePeriod)); } } //配置欢迎页映射 @Bean public WelcomePageHandlerMapping welcomePageHandlerMapping( ResourceProperties resourceProperties) { return new WelcomePageHandlerMapping(resourceProperties.getWelcomePage(), this.mvcProperties.getStaticPathPattern()); } //配置喜欢的图标 @Configuration @ConditionalOnProperty(value = "spring.mvc.favicon.enabled", matchIfMissing = true) public static class FaviconConfiguration { private final ResourceProperties resourceProperties; public FaviconConfiguration(ResourceProperties resourceProperties) { this.resourceProperties = resourceProperties; } @Bean public SimpleUrlHandlerMapping faviconHandlerMapping() { SimpleUrlHandlerMapping mapping = new SimpleUrlHandlerMapping(); mapping.setOrder(Ordered.HIGHEST_PRECEDENCE + 1); //所有 **/favicon.ico mapping.setUrlMap(Collections.singletonMap("**/favicon.ico", faviconRequestHandler())); return mapping; } @Bean public ResourceHttpRequestHandler faviconRequestHandler() { ResourceHttpRequestHandler requestHandler = new ResourceHttpRequestHandler(); requestHandler .setLocations(this.resourceProperties.getFaviconLocations()); return requestHandler; } } 1）所有 /webjars/** ，都去 classpath:/META-INF/resources/webjars/ 找资源； webjars：以jar包的方式引入静态资源； 参考：http://www.webjars.org/ localhost:8080/webjars/jquery/3.3.1/jquery.js 在访问的时候只需要写webjars下面资源的名称即可 <dependency> <groupId>org.webjars</groupId> <artifactId>jquery</artifactId> <version>3.3.1</version> </dependency> 2）"/**" 访问当前项目的任何资源，都去（静态资源的文件夹）找映射 "classpath:/META-INF/resources/", "classpath:/resources/", "classpath:/static/", "classpath:/public/" "/"：当前项目的根路径 localhost:8080/abc ---> 去静态资源文件夹里面找abc 。 3）欢迎页； 静态资源文件夹下的所有index.html页面；被"/**"映射； localhost:8080/ 找index页面 4）所有的 **/favicon.ico 都是在静态资源文件下找； 二、SpringMVC自动配置 https://docs.spring.io/spring-boot/docs/1.5.10.RELEASE/reference/htmlsingle/#boot-features-developing-web-applications 2.1、SpringMVC auto-configuration Spring Boot 自动配置好了SpringMVC 以下是SpringBoot对SpringMVC的默认配置:（WebMvcAutoConfiguration） Inclusion of ContentNegotiatingViewResolver and BeanNameViewResolver beans. 自动配置了ViewResolver（视图解析器：根据方法的返回值得到视图对象（View），视图对象决定如何渲染（转发？重定向？）） ContentNegotiatingViewResolver：组合所有的视图解析器的； 如何定制：我们可以自己给容器中添加一个视图解析器；自动的将其组合进来； Support for serving static resources, including support for WebJars (see below).静态资源文件夹路径,webjars Static index.html support. 静态首页访问 Custom Favicon support (see below). favicon.ico 自动注册了 of Converter, GenericConverter, Formatter beans. Converter：转换器； public String hello(User user)：类型转换使用Converter Formatter 格式化器； 2017.12.17===Date； @Bean @ConditionalOnProperty(prefix = "spring.mvc", name = "date-format")//在文件中配置日期格式化的规则 public Formatter<Date> dateFormatter() { return new DateFormatter(this.mvcProperties.getDateFormat());//日期格式化组件 } 自己添加的格式化器转换器，我们只需要放在容器中即可。 Support for HttpMessageConverters (see below)。 HttpMessageConverter：SpringMVC用来转换Http请求和响应的；User---Json； HttpMessageConverters 是从容器中确定；获取所有的HttpMessageConverter；自己给容器中添加HttpMessageConverter，只需要将自己的组件注册容器中（@Bean,@Component） Automatic registration of MessageCodesResolver (see below).定义错误代码生成规则。 Automatic use of a ConfigurableWebBindingInitializer bean (see below)。 org.springframework.boot.autoconfigure.web：web的所有自动场景 If you want to keep Spring Boot MVC features, and you just want to add additional MVC configuration (interceptors, formatters, view controllers etc.) you can add your own @Configuration class of type WebMvcConfigurerAdapter, but without @EnableWebMvc. If you wish to provide custom instances of RequestMappingHandlerMapping, RequestMappingHandlerAdapter or ExceptionHandlerExceptionResolver you can declare a WebMvcRegistrationsAdapter instance providing such components. If you want to take complete control of Spring MVC, you can add your own @Configuration annotated with @EnableWebMvc. 2.2、扩展SpringMVC 在ssm中SrpingMVC的配置 <mvc:view-controller path="/hello" view-name="success"/> <mvc:interceptors> <mvc:interceptor> <mvc:mapping path="/hello"/> <bean></bean> </mvc:interceptor> </mvc:interceptors> 自行扩展： 编写一个配置类（@Configuration），是WebMvcConfigurerAdapter类型；不能标注@EnableWebMvc 既保留了所有的自动配置，也能用我们扩展的配置； //使用WebMvcConfigurerAdapter可以来扩展SpringMVC的功能 @Configuration public class MyMvcConfig extends WebMvcConfigurerAdapter { @Override public void addViewControllers(ViewControllerRegistry registry) { // super.addViewControllers(registry); //浏览器发送 /atguigu 请求来到 success registry.addViewController("/atguigu").setViewName("success"); } } 原理： 1）WebMvcAutoConfiguration是SpringMVC的自动配置类。 2）在做其他自动配置时会导入；@Import(EnableWebMvcConfiguration.class)。 @Configuration public static class EnableWebMvcConfiguration extends DelegatingWebMvcConfiguration { private final WebMvcConfigurerComposite configurers = new WebMvcConfigurerComposite(); //从容器中获取所有的WebMvcConfigurer @Autowired(required = false) public void setConfigurers(List<WebMvcConfigurer> configurers) { if (!CollectionUtils.isEmpty(configurers)) { this.configurers.addWebMvcConfigurers(configurers); //一个参考实现；将所有的WebMvcConfigurer相关配置都来一起调用； @Override // public void addViewControllers(ViewControllerRegistry registry) { // for (WebMvcConfigurer delegate : this.delegates) { // delegate.addViewControllers(registry); // } } } } 3）容器中所有的WebMvcConfigurer都会一起起作用； 4）我们的配置类也会被调用； 效果：SpringMVC的自动配置和我们的扩展配置都会起作用； 2.3、全面接管SpringMVC SpringBoot对SpringMVC的自动配置不需要了，所有都是我们自己配置；所有的SpringMVC的自动配置都失效了。 我们需要在配置类中添加@EnableWebMvc即可。 //使用WebMvcConfigurerAdapter可以来扩展SpringMVC的功能 @EnableWebMvc @Configuration public class MyMvcConfig extends WebMvcConfigurerAdapter { @Override public void addViewControllers(ViewControllerRegistry registry) { // super.addViewControllers(registry); //浏览器发送 /atguigu 请求来到 success registry.addViewController("/atguigu").setViewName("success"); } } 原理： 为什么@EnableWebMvc自动配置就失效了； 1）@EnableWebMvc的核心 @Import(DelegatingWebMvcConfiguration.class) public @interface EnableWebMvc { 2）DelegatingWebMvcConfiguration @Configuration public class DelegatingWebMvcConfiguration extends WebMvcConfigurationSupport { 3）WebMvcAutoConfiguration @Configuration @ConditionalOnWebApplication @ConditionalOnClass({ Servlet.class, DispatcherServlet.class, WebMvcConfigurerAdapter.class }) //容器中没有这个组件的时候，这个自动配置类才生效 @ConditionalOnMissingBean(WebMvcConfigurationSupport.class) @AutoConfigureOrder(Ordered.HIGHEST_PRECEDENCE + 10) @AutoConfigureAfter({ DispatcherServletAutoConfiguration.class, ValidationAutoConfiguration.class }) public class WebMvcAutoConfiguration { 4）@EnableWebMvc 将 WebMvcConfigurationSupport 组件导入进来； 5）导入的 WebMvcConfigurationSupport 只是 SpringMVC 最基本的功能；