首页
关于这个博客
Search
1
Java 实现Google 账号单点登录(OAuth 2.0)全流程解析
231 阅读
2
Spring AI 无法获取大模型深度思考内容?解决方案来了
202 阅读
3
微信小程序实现页面返回前确认弹窗:兼容左上角返回与右滑返回
91 阅读
4
服务器遭遇 XMRig 挖矿程序入侵排查与清理全记录
66 阅读
5
解决 Mac 版 PicGo 无法打开问题:“已损坏,无法打开” 报错处理指南
37 阅读
Java 核心
框架与中间件
数据库技术
开发工具与效率
问题排查与踩坑记录
程序员成长与思考
前端
登录
Search
标签搜索
java虚拟机
JVM
保姆级教程
Java
Spring AI
SpringBoot
Nginx
WebFlux
Spring
cdn
https
dcdn
网站加速
Tool
图片导出
服务部署
源码解析
单点登录
google
sso
Luca Ju
累计撰写
35
篇文章
累计收到
1
条评论
首页
栏目
Java 核心
框架与中间件
数据库技术
开发工具与效率
问题排查与踩坑记录
程序员成长与思考
前端
页面
关于这个博客
搜索到
9
篇与
的结果
2025-03-14
自定义 Spring-Boot-Starter 结合 TrueLicense 实现证书授权拦截
引言在软件产品交付场景中,授权管理是保障软件权益的重要手段。传统的硬编码时间限制方式存在修改麻烦、需重新部署等问题,而基于证书(License)的授权方式可通过替换证书文件实现灵活授权,无需改动源码。本文将详解如何基于 Spring Boot 自定义 Starter,并整合开源证书管理引擎 TrueLicense,实现对接口的证书授权拦截功能,帮助开发者快速搭建可控的授权体系。一、技术背景与场景说明1.1 什么是 TrueLicense?TrueLicense 是一个基于 Java 的开源证书管理引擎,提供了证书的生成、颁发、验证等核心功能,支持通过密钥对加密证书内容,确保授权信息的安全性。其官网地址为:https://truelicense.java.net。1.2 为什么需要自定义 Spring Boot Starter?Spring Boot Starter 的核心作用是简化依赖管理和自动配置。通过自定义 Starter,我们可以将证书校验逻辑封装为独立组件,只需在目标项目中引入依赖并配置参数,即可快速集成证书授权功能,实现 "即插即用"。1.3 核心场景软件试用期授权:通过证书指定有效期,到期后自动限制使用。硬件绑定授权:限制软件仅能在指定 MAC 地址的设备上运行。接口级授权控制:对敏感接口添加证书校验,未授权请求直接拦截。二、密钥对生成(基于 keytool)证书的安全性依赖于非对称加密的密钥对(私钥用于生成证书,公钥用于验证证书)。我们使用 JDK 自带的keytool工具生成密钥对,步骤如下:2.1 生成私钥库私钥库用于存储生成证书的私钥,执行以下命令:keytool -genkey -alias privatekey -keystore privateKeys.store -storepass "123456q" -keypass "123456q" -keysize 1024 -validity 3650参数说明:-alias privatekey:私钥别名(后续生成证书需引用)。-keystore privateKeys.store:生成的私钥库文件名。-storepass "123456q":私钥库访问密码。-keypass "123456q":私钥本身的密码(建议与 storepass 一致,简化管理)。-keysize 1024:密钥长度(1024 位及以上确保安全性)。-validity 3650:私钥有效期(单位:天,此处为 10 年)。执行后需输入所有者信息(如姓名、组织等),可根据实际情况填写。2.2 导出公钥证书从私钥库中导出公钥证书(用于校验证书的合法性):keytool -export -alias privatekey -file certfile.cer -keystore privateKeys.store -storepass "123456q"参数说明:-export:指定操作类型为导出证书。-file certfile.cer:导出的公钥证书文件名。执行成功后,当前目录会生成certfile.cer公钥文件。2.3 导入公钥到公钥库将公钥证书导入公钥库(供应用程序验证证书时使用):keytool -import -alias publiccert -file certfile.cer -keystore publicCerts.store -storepass "123456q"参数说明:-alias publiccert:公钥在公钥库中的别名(后续校验需引用)。-keystore publicCerts.store:生成的公钥库文件名。执行时需确认导入(输入yes),完成后公钥库publicCerts.store生成。注意:私钥库(privateKeys.store)需妥善保管,公钥库(publicCerts.store)和公钥证书(certfile.cer)可随应用程序部署。三、证书生成工具实现基于 TrueLicense 的 API,我们可以通过代码生成证书文件。以下是核心实现步骤:3.1 核心参数类定义首先定义证书生成所需的参数封装类(LicenseCreatorParam):/** * License证书生成类需要的参数 * @author : jucunqi * @since : 2025/3/12 */ @Data public class LicenseCreatorParam implements Serializable { private static final long serialVersionUID = 2832129012982731724L; /** * 证书subject * */ private String subject; /** * 密钥级别 * */ private String privateAlias; /** * 密钥密码(需要妥善保存,密钥不能让使用者知道) */ private String keyPass; /** * 访问密钥库的密码 * */ private String storePass; /** * 证书生成路径 * */ private String licensePath; /** * 密钥库存储路径 * */ private String privateKeysStorePath; /** * 证书生效时间 * */ @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8") private Date issuedTime = new Date(); /** * 证书的失效时间 * */ @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8") private Date expiryTime; /** * 用户的使用类型 * */ private String consumerType ="user"; /** * 用户使用数量 * */ private Integer consumerAmount = 1; /** * 描述信息 * */ private String description = ""; /** * 额外的服务器硬件校验信息(机器码) * */ private LicenseCheckModel licenseCheckModel; }/** * 自定义需要校验的参数 * @author : jucunqi * @since : 2025/3/12 */ @Data public class LicenseCheckModel implements Serializable { private static final long serialVersionUID = -2314678441082223148L; /** * 可被允许IP地址白名单 * */ private List<String> ipAddress; /** * 可被允许的MAC地址白名单(网络设备接口的物理地址,通常固化在网卡(Network Interface Card,NIC)的EEPROM(电可擦可编程只读存储器)中,具有全球唯一性。) * */ private List<String> macAddress; /** * 可允许的CPU序列号 * */ private String cpuSerial; /** * 可允许的主板序列号(硬件序列化?) * */ private String mainBoardSerial; }3.2 证书生成器实现实现LicenseCreator类,封装证书生成逻辑:public class LicenseCreator { private final static X500Principal DEFAULT_HOLDER_AND_ISSUER = new X500Principal("CN=localhost, OU=localhost, O=localhost, L=SH, ST=SH, C=CN"); private final LicenseCreatorParam param; public LicenseCreator(LicenseCreatorParam param) { this.param = param; } /** * 生成License证书 * @return boolean */ public boolean generateLicense(){ try { LicenseManager licenseManager = new CustomLicenseManager(initLicenseParam()); LicenseContent licenseContent = initLicenseContent(); licenseManager.store(licenseContent,new File(param.getLicensePath())); return true; }catch (Exception e){ throw new LicenseCreateException(MessageFormat.format("证书生成失败:{0}", param), e); } } /** * 初始化证书生成参数 * @return de.schlichtherle.license.LicenseParam */ private LicenseParam initLicenseParam(){ Preferences preferences = Preferences.userNodeForPackage(LicenseCreator.class); //设置对证书内容加密的秘钥 CipherParam cipherParam = new DefaultCipherParam(param.getStorePass()); KeyStoreParam privateStoreParam = new CustomKeyStoreParam(LicenseCreator.class ,param.getPrivateKeysStorePath() ,param.getPrivateAlias() ,param.getStorePass() ,param.getKeyPass()); return new DefaultLicenseParam(param.getSubject() ,preferences ,privateStoreParam ,cipherParam); } /** * 设置证书生成正文信息 * @return de.schlichtherle.license.LicenseContent */ private LicenseContent initLicenseContent(){ LicenseContent licenseContent = new LicenseContent(); licenseContent.setHolder(DEFAULT_HOLDER_AND_ISSUER); licenseContent.setIssuer(DEFAULT_HOLDER_AND_ISSUER); licenseContent.setSubject(param.getSubject()); licenseContent.setIssued(param.getIssuedTime()); licenseContent.setNotBefore(param.getIssuedTime()); licenseContent.setNotAfter(param.getExpiryTime()); licenseContent.setConsumerType(param.getConsumerType()); licenseContent.setConsumerAmount(param.getConsumerAmount()); licenseContent.setInfo(param.getDescription()); //扩展校验服务器硬件信息 licenseContent.setExtra(param.getLicenseCheckModel()); return licenseContent; } }3.3 生成证书示例通过单元测试或主方法生成证书:public class LicenseCreateTest { public static void main(String[] args) { LicenseCreatorParam param = new LicenseCreatorParam(); param.setSubject("your subject"); param.setPrivateAlias("privatekey"); // 与私钥库中别名一致 param.setKeyPass("123456q"); // 私钥密码 param.setStorePass("123456q"); // 私钥库密码 param.setLicensePath("your path"); // 证书输出路径 param.setPrivateKeysStorePath("your path"); // 私钥库路径 param.setIssuedTime(DateUtil.parseDate("2025-05-25")); // 生效时间 param.setExpiryTime(DateUtil.parseDate("2025-09-01")); // 过期时间 param.setConsumerType("your type"); param.setConsumerAmount(1); param.setDescription("your desc"); // 绑定MAC地址(仅允许指定设备使用) LicenseCheckModel checkModel = new LicenseCheckModel(); checkModel.setMacAddressList(Collections.singletonList("8c:84:74:e7:62:a6")); param.setLicenseCheckModel(checkModel); // 生成证书 LicenseCreator creator = new LicenseCreator(param); boolean result = creator.generateLicense(); System.out.println("证书生成结果:" + (result ? "成功" : "失败")); } }执行后,指定路径会生成your path.lic证书文件。四、证书校验核心逻辑应用程序需通过公钥库验证证书的合法性(有效期、设备绑定等),核心实现如下:4.1 校验参数类定义/** * license证书校验参数类 * @author : jucunqi * @since : 2025/3/12 */ @Data public class LicenseVerifyParam { /** * 证书subject */ private String subject; /** * 公钥别称 */ private String publicAlias; /** * 访问公钥库的密码 */ private String storePass; /** * 证书生成路径 */ private String licensePath; /** * 密钥库存储路径 */ private String publicKeysStorePath; }4.2 校验器实现/** * license证书校验类 * @author : jucunqi * @since : 2025/3/12 */ @Slf4j public class LicenseVerify { /** * 认证需要提供的参数 */ private final LicenseVerifyParam param; /** * 是否启用license */ private final Boolean enableLicense; public LicenseVerify(LicenseVerifyParam param,Boolean enableLicense) { this.param = param; this.enableLicense = enableLicense; } /** * 安装License证书 */ public synchronized LicenseContent install(){ log.info("服务启动,检查是否启用license验证,结果:" + enableLicense); if (!enableLicense) { return null; } LicenseContent result = null; DateFormat format = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"); //1. 安装证书 try{ LicenseManager licenseManager = LicenseManagerHolder.getInstance(initLicenseParam(param)); licenseManager.uninstall(); result = licenseManager.install(new File(param.getLicensePath())); log.info(MessageFormat.format("证书安装成功,证书有效期:{0} - {1}",format.format(result.getNotBefore()),format.format(result.getNotAfter()))); }catch (Exception e){ log.error("证书安装失败!",e); } return result; } /** * 校验License证书 * @return boolean */ public boolean verify(){ LicenseManager licenseManager = LicenseManagerHolder.getInstance(null); DateFormat format = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"); //2. 校验证书 try { LicenseContent licenseContent = licenseManager.verify(); log.info(MessageFormat.format("证书校验通过,证书有效期:{0} - {1}",format.format(licenseContent.getNotBefore()),format.format(licenseContent.getNotAfter()))); return true; }catch (Exception e){ log.error("证书校验失败!",e); return false; } } /** * 初始化证书生成参数 * @param param License校验类需要的参数 * @return de.schlichtherle.license.LicenseParam */ private LicenseParam initLicenseParam(LicenseVerifyParam param){ Preferences preferences = Preferences.userNodeForPackage(LicenseVerify.class); CipherParam cipherParam = new DefaultCipherParam(param.getStorePass()); KeyStoreParam publicStoreParam = new CustomKeyStoreParam(LicenseVerify.class ,param.getPublicKeysStorePath() ,param.getPublicAlias() ,param.getStorePass() ,null); return new DefaultLicenseParam(param.getSubject() ,preferences ,publicStoreParam ,cipherParam); } } 五、Spring Boot Starter 自动配置5.1 配置属性类定义配置文件参数映射类,支持通过application.yml配置证书相关参数:/** * 证书认证属性类 * * @author : jucunqi * @since : 2025/3/12 */ @Data @ConfigurationProperties(prefix = "license") public class LicenseConfigProperties { /** * 证书subject */ private String subject; /** * 公钥别称 */ private String publicAlias; /** * 访问公钥库的密码 */ private String storePass; /** * 证书生成路径 */ private String licensePath; /** * 密钥库存储路径 */ private String publicKeysStorePath; /** * 是否启用license认证 */ private Boolean enableLicense; }5.2 自动配置类通过@Configuration实现自动配置,注入校验器 Bean:@Configuration @AllArgsConstructor @EnableConfigurationProperties(LicenseConfigProperties.class) public class LicenseAutoConfiguration { private final LicenseConfigProperties licenseConfigProperties; // 注入LicenseVerify Bean,启动时执行install方法 @Bean(initMethod = "install") public LicenseVerify licenseVerify() { LicenseVerifyParam param = new LicenseVerifyParam(); param.setSubject(licenseConfigProperties.getSubject()); param.setPublicAlias(licenseConfigProperties.getPublicAlias()); param.setStorePass(licenseConfigProperties.getStorePass()); param.setLicensePath(licenseConfigProperties.getLicensePath()); param.setPublicKeysStorePath(licenseConfigProperties.getPublicKeysStorePath()); return new LicenseVerify(param, licenseConfigProperties.getEnableLicense()); } }5.3 AOP 拦截实现通过自定义注解@RequireLicense和 AOP 拦截,实现接口级别的证书校验:5.3.1 自定义注解/** * 标记需要证书校验的接口方法 */ @Target({ElementType.METHOD}) @Retention(RetentionPolicy.RUNTIME) public @interface RequireLicense { boolean value() default true; // 是否启用校验(默认启用) }5.3.2 AOP 拦截逻辑@Slf4j @Aspect @Component public class RequireLicenseAspect { private final LicenseVerify licenseVerify; private final LicenseConfigProperties properties; public RequireLicenseAspect(LicenseVerify licenseVerify, LicenseConfigProperties properties) { this.licenseVerify = licenseVerify; this.properties = properties; } // 拦截所有添加@RequireLicense注解的方法 @Around("@annotation(requireLicense)") public Object around(ProceedingJoinPoint point, RequireLicense requireLicense) throws Throwable { // 注解禁用校验或全局禁用校验,直接执行方法 if (!requireLicense.value() || !properties.getEnableLicense()) { log.info("接口[{}]跳过证书校验", point.getSignature().getName()); return point.proceed(); } // 执行证书校验 boolean verifyResult = licenseVerify.verify(); if (verifyResult) { return point.proceed(); // 校验通过,执行原方法 } else { throw new LicenseInterceptException("接口调用失败:证书未授权或已过期"); } } }5.4 注册自动配置类在src/main/resources/META-INF目录下创建spring.factories文件,指定自动配置类:org.springframework.boot.autoconfigure.EnableAutoConfiguration=\ com.jcq.license.autoconfigure.LicenseAutoConfiguration,\ com.jcq.license.verify.aop.RequireLicenseAspect六、Starter 打包配置为确保其他项目引用 Starter 时能正常加载类,需修改pom.xml的构建配置:<build> <plugins> <plugin> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-maven-plugin</artifactId> <!-- 跳过Spring Boot默认的可执行JAR打包(避免类路径嵌套在BOOT-INF下) --> <configuration> <skip>true</skip> </configuration> </plugin> </plugins> </build>原因:默认情况下,Spring Boot 插件会将类打包到BOOT-INF/classes目录下,导致其他项目引用时无法通过常规类路径加载类。设置skip=true后,会生成标准的 JAR 包,类路径更友好。七、使用示例7.1 引入依赖在目标项目的pom.xml中引入自定义 Starter:<dependency> <groupId>com.jcq</groupId> <artifactId>license-spring-boot-starter</artifactId> <version>1.0.0</version> </dependency>7.2 配置参数在application.yml中配置证书相关参数,配置时idea会弹出提示:license: subject: 企业版软件授权证书 public-alias: publiccert store-pass: 123456q license-path: classpath:license.lic # 证书文件存放路径 public-keys-store-path: classpath:publicCerts.store # 公钥库路径 enable-license: true # 启用证书校验7.3 接口使用注解在需要授权的接口方法上添加@RequireLicense注解:@RestController @RequestMapping("/api") public class DemoController { @GetMapping("/sensitive") @RequireLicense // 需要证书校验 public String sensitiveOperation() { return "敏感操作执行成功(已授权)"; } @GetMapping("/public") @RequireLicense(false) // 禁用校验(即使全局启用也会跳过) public String publicOperation() { return "公开操作执行成功(无需授权)"; } }八、总结本文通过自定义 Spring Boot Starter 整合 TrueLicense,实现了一套灵活的证书授权方案,核心优势包括:可插拔性:通过 Starter 封装,引入依赖即可使用,无需重复开发。灵活性:支持全局开关和接口级开关,方便测试环境跳过校验。安全性:基于非对称加密和硬件绑定,防止证书伪造和非法传播。易维护:授权到期后只需替换证书文件,无需修改代码或重启服务。实际项目中可根据需求扩展校验维度(如 CPU 序列号、内存大小等),进一步增强授权的安全性。附录:核心类说明类名作用LicenseCreator证书生成工具类LicenseVerify证书校验核心类(启动校验 + 实时校验)LicenseConfigProperties配置参数映射类LicenseAutoConfigurationStarter 自动配置类RequireLicense接口校验注解RequireLicenseAspectAOP 拦截器(实现接口级校验)完整源码可参考 GitHub 仓库:https://github.com/Jucunqi/license-spring-boot-starter.git(示例地址)。
2025年03月14日
28 阅读
0 评论
0 点赞
2025-01-15
响应式编程学习笔记
响应式编程1、Reactor核心前置知识1、Lambda2、Function根据出参,入参分类1、有入参,有出参 --> FunctionFunction<String, Integer> function = a -> Integer.parseInt(a);2、有入参,无出参Consumer<String> consumer = a -> System.out.println(a);3、无入参,有出参Supplier<String> supplier = () -> UUID.randomUUID().toString();4、无入参,无出参Runnable runnable = () -> System.out.println("xixi"); 3、StreamAPI流式操作,三大步骤1、创建流Stream<Integer> integerStream = Stream.of(1, 2, 3); Stream<Integer> stream = list.stream();2、中间操作(intermediate operation),可以有多个filter,map,mapToInt,mapToLong,mapToDouble,flatMap,flatMapToInt,flatMapToLong,flatMapToDouble,mapMulti,mapMultiToInt,mapMultiToLong,mapMultiToDouble,peek...3、终止操作(terminal operation),只能有一个forEach,forEachOrdered,toArray,toArray,reduce,collect,toList,min,max,count,anyMatch,findFirst,findAny...流式操作是否并发? // 流的三大部份 // 1.创建流 2.N个中间操作 3.一个终止操作 Stream<Integer> integerStream = Stream.of(1, 2, 3); Stream<Object> buildStream = Stream.builder().add(1).add(2).add(3).build(); Stream<Object> concatStream = Stream.concat(integerStream, buildStream); Stream<Integer> stream = list.stream(); List<Integer> resultList = new ArrayList<>(); System.out.println("main线程: "+Thread.currentThread().getName()); // 流是不是并发操作? 答:默认单线程,可以通过parallel开启多线程,但是如果开启多线程,则需要自身注意线程安全问题 long count = list.stream() .parallel() // 开启多线程 并发流 .filter(i -> { // resultList.add(i); // 开启多线程,不能这样写,要保证流里面的数据是无状态的,即流里面的数据只在流内部使用 // 可以计算完成以后返回出去,但是不能在内部又引用外部的数据,可能会出现问题 System.out.println("filter线程: " + Thread.currentThread().getName()); return i > 2; }) .count(); System.out.println(resultList);注意: 要保证流里面的数据是无状态的中间操作:filter:过滤,挑出我们要的元素takeWhile示例List<Integer> collect = Stream.of(1, 2, 3, 4, 5, 6) .filter(a -> a > 2) // 无条件遍历 .toList(); System.out.println(collect); List<Integer> collect1 = Stream.of(1, 2, 3, 4, 5, 6) .takeWhile(a -> a < 2) // 当条件不满足时,直接返回 .toList(); System.out.println(collect1);map:映射,一对一映射mapToInt,MapToDouble..flatMap: 打散、散列、展开,一对多映射...终止操作:forEach、forEachOrdered、toArray、reduce、collect、toList、min、 max、count、anyMatch、allMatch、noneMatch、findFirst、findAny、iterator4、Reactive Stream目的:通过全异步的方式,加缓冲区构建一个实时的数据流系统。kafka,mq能构建大型的分布式响应系统,缺少本地化分布式响应系统方案jvm推出Reactive Stream,让所有异步线程能够互相监听消息,处理消息,构建实时消息处理流Api Component:1、Publisher:发布者2、Subscriber:订阅者3、Processor:处理器响应式编程总结:1、底层:基于数据缓冲队列+消息驱动模型+异步回调机制2、编码:流式编程+链式调用+生命式API3、效果:优雅全异步+消息实时处理+高吞吐量+占用少量资源与传统写法对比:传统写法痛点:以前要做一个高并发系统:缓存、异步、队列,手动控制整个逻辑现在:全自动控制整个逻辑Reactor1、快速上手介绍Reactor 是一个用于JVM的完全非阻塞的响应式编程框架,具备高效的需求管理(即对 “背压(backpressure)”的控制)能力。它与 Java 8 函数式 API 直接集成,比如 CompletableFuture, Stream, 以及 Duration。它提供了异步序列 API Flux(用于[N]个元素)和 Mono(用于 [0|1]个元素),并完全遵循和实现了“响应式扩展规范”(Reactive Extensions Specification)。Reactor 的 reactor-ipc 组件还支持非阻塞的进程间通信(inter-process communication, IPC)。 Reactor IPC 为 HTTP(包括 Websockets)、TCP 和 UDP 提供了支持背压的网络引擎,从而适合 应用于微服务架构。并且完整支持响应式编解码(reactive encoding and decoding)。依赖<dependencyManagement> <dependencies> <dependency> <groupId>io.projectreactor</groupId> <artifactId>reactor-bom</artifactId> <version>2023.0.0</version> <type>pom</type> <scope>import</scope> </dependency> </dependencies> </dependencyManagement><dependencies> <dependency> <groupId>io.projectreactor</groupId> <artifactId>reactor-core</artifactId> </dependency> <dependency> <groupId>io.projectreactor</groupId> <artifactId>reactor-test</artifactId> <scope>test</scope> </dependency> </dependencies>2、响应式编程响应式编程是一种关注于数据流(data streams)和变化传递(propagation of change)的异步编程方式。 这意味着它可以用既有的编程语言表达静态(如数组)或动态(如事件源)的数据流。3、核心特性1、Mono和FluxMono: 0|1 数据流Flux: N数据流响应式流:元素(内容) + 信号(完成/异常);2、subscribe()自定义流的信号感知回调.subscribe( System.out::println // 消费方法 , throwable -> System.out.println(throwable.getMessage()) // 感知异常 , () -> System.out.println("complete") // 感知正常结束 ); // 流只有被订阅了才会执行,否则没有任何操作自定义消费者.subscribe(new BaseSubscriber<String>() { // 自定义消费者 @Override protected void hookOnSubscribe(Subscription subscription) { System.out.println("被订阅"); requestUnbounded(); } @Override protected void hookOnNext(String value) { System.out.println("下个元素"); } @Override protected void hookOnComplete() { System.out.println("完成信号"); } @Override protected void hookOnError(Throwable throwable) { System.out.println("异常信号"); } @Override protected void hookOnCancel() { System.out.println("结束信号"); } @Override protected void hookFinally(SignalType type) { System.out.println("终止信号"); } });3、流的取消消费者调用 cancle() 取消流的订阅;4、自定义消费者推荐直接编写jdk自带的BaseSubscriber的实现类5、背压(back pressure)和请求重塑(reshape requests)buffer/** * 缓冲区 */ private static void bufferTest() { Flux.range(1, 10).buffer(3).subscribe(v -> System.out.println("v的类型:" + v.getClass() + "的值:" + v)); }limitRate/** * 测试limitRate */ private static void limitTest() { Flux.range(1,1000) .log() .limitRate(100) // 一次预取100个元素 75%预取策略,第一次取100个如果75%已经处理,继续请求新的75%数据 .subscribe(System.out::println); }6、以编程方式创建序列-SinkSink.nextSink.complete1、同步环境-generate/** * 通过generate创建序列 */ private static void generateTest() { List<Integer> list = List.of(1, 2, 3, 4, 5, 6, 7, 8, 9); Flux.generate(() -> 0, // 初始值 (i, a) -> { a.next(list.get(i)); // 把元素放入通道 if (i == list.size() - 1) { a.complete(); // 完成 } return ++i; // 下次回调的元素 } ) .subscribe(System.out::println); }2、多线程-create/** * 通过create创建序列,create适用与多线程环境,generate适用于单线程环境 */ private static void createTest() { Flux.create(sink -> { for (int i = 0; i < 10; i++) { sink.next("2"); } }).subscribe(System.out::println); }7、handle自定义流中的处理规则/** * handle自定义处理 */ private static void handleTest() { Flux.range(1, 10) .handle((value,sink) -> { System.out.println("接收到value:" + value); sink.next("haha_" + value); }) .subscribe(); }8、自定义线程调度响应式:响应式编程: 全异步、消息、事件回调默认还是用当前线程,生成整个流、发布流、流操作/** * 自定义线程测试 */ private static void threadTest() { // 响应式编程:全异步,消息,回调机制 Schedulers.boundedElastic(); // 有界的,弹性线程池 Schedulers.single(); // 单线程 Schedulers.immediate(); // 都在同一个当前线程(默认) Scheduler scheduler = Schedulers.newParallel("my-parallel"); Flux<Integer> flux = Flux.range(1, 10) .publishOn(scheduler) .log(); flux.subscribe(); }9、异常处理命令式编程:常见的错误处理方式Catch and return a static default value. 捕获异常返回一个静态默认值try { return doSomethingDangerous(10); } catch (Throwable error) { return "RECOVERED"; }onErrorReturn: 实现上面效果,错误的时候返回一个值●1、吃掉异常,消费者无异常感知●2、返回一个兜底默认值●3、流正常完成;Catch and execute an alternative path with a fallback method.吃掉异常,执行一个兜底方法;try { return doSomethingDangerous(10); } catch (Throwable error) { return doOtherthing(10); }onErrorResume●1、吃掉异常,消费者无异常感知●2、调用一个兜底方法●3、流正常完成Flux.just(1, 2, 0, 4) .map(i -> "100 / " + i + " = " + (100 / i)).onErrorResume(err -> Mono.just("哈哈-777")) .subscribe(v -> System.out.println("v = " + v), err -> System.out.println("err = " + err), () -> System.out.println("流结束"));Catch and dynamically compute a fallback value. 捕获并动态计算一个返回值根据错误返回一个新值try { Value v = erroringMethod(); return MyWrapper.fromValue(v); } catch (Throwable error) { return MyWrapper.fromError(error); }.onErrorResume(err -> Flux.error(new BusinessException(err.getMessage()+":炸了")))●1、吃掉异常,消费者有感知●2、调用一个自定义方法●3、流异常完成Catch, wrap to a BusinessException, and re-throw.捕获并包装成一个业务异常,并重新抛出try { return callExternalService(k); } catch (Throwable error) { throw new BusinessException("oops, SLA exceeded", error); }包装重新抛出异常: 推荐用 .onErrorMap●1、吃掉异常,消费者有感知●2、抛新异常●3、流异常完成.onErrorResume(err -> Flux.error(new BusinessException(err.getMessage()+":炸了"))) Flux.just(1, 2, 0, 4) .map(i -> "100 / " + i + " = " + (100 / i)) .onErrorMap(err-> new BusinessException(err.getMessage()+": 又炸了...")) .subscribe(v -> System.out.println("v = " + v), err -> System.out.println("err = " + err), () -> System.out.println("流结束"));Catch, log an error-specific message, and re-throw.捕获异常,记录特殊的错误日志,重新抛出try { return callExternalService(k); } catch (RuntimeException error) { //make a record of the error log("uh oh, falling back, service failed for key " + k); throw error; }Flux.just(1, 2, 0, 4) .map(i -> "100 / " + i + " = " + (100 / i)) .doOnError(err -> { System.out.println("err已被记录 = " + err); }).subscribe(v -> System.out.println("v = " + v), err -> System.out.println("err = " + err), () -> System.out.println("流结束"));●异常被捕获、做自己的事情●不影响异常继续顺着流水线传播●1、不吃掉异常,只在异常发生的时候做一件事,消费者有感知Use the finally block to clean up resources or a Java 7 “try-with-resource” construct. Flux.just(1, 2, 3, 4) .map(i -> "100 / " + i + " = " + (100 / i)) .doOnError(err -> { System.out.println("err已被记录 = " + err); }) .doFinally(signalType -> { System.out.println("流信号:"+signalType); })忽略当前异常,仅通知记录,继续推进Flux.just(1,2,3,0,5) .map(i->10/i) .onErrorContinue((err,val)->{ System.out.println("err = " + err); System.out.println("val = " + val); System.out.println("发现"+val+"有问题了,继续执行其他的,我会记录这个问题"); }) //发生 .subscribe(v-> System.out.println("v = " + v), err-> System.out.println("err = " + err));10、常用操作filter、flatMap、concatMap、flatMapMany、transform、defaultIfEmpty、switchIfEmpty、concat、concatWith、merge、mergeWith、mergeSequential、zip、zipWith...2、Spring Webflux0、组件对比API功能Servlet-阻塞式WebWebFlux-响应式Web前端控制器DispatcherServletDispatcherHandler处理器ControllerWebHandler/Controller请求、响应ServletRequest、ServletResponseServerWebExchange:ServerHttpRequest、ServerHttpResponse过滤器Filter(HttpFilter)WebFilter异常处理器HandlerExceptionResolverDispatchExceptionHandlerWeb配置@EnableWebMvc@EnableWebFlux自定义配置WebMvcConfigurerWebFluxConfigurer返回结果任意Mono、Flux、任意发送REST请求RestTemplateWebClientMono: 返回0|1 数据流Flux:返回N数据流1、WebFlux底层基于Netty实现的Web容器与请求/响应处理机制参照:https://docs.spring.io/spring-framework/reference/6.0/web/webflux.html2、引入<parent> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-parent</artifactId> <version>3.1.6</version> </parent> <dependencies> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-webflux</artifactId> </dependency> </dependencies>Context 响应式上下文数据传递; 由下游传播给上游;以前: 浏览器 --> Controller --> Service --> Dao: 阻塞式编程现在: Dao(数据源查询对象【数据发布者】) --> Service --> Controller --> 浏览器: 响应式大数据流程: 从一个数据源拿到大量数据进行分析计算;ProductVistorDao.loadData() .distinct() .map() .filter() .handle().subscribe();;//加载最新的商品浏览数据3、Reactor Core1、HttpHandler、HttpServer** * 测试webflux * @author : jucunqi * @since : 2025/1/16 */ public class FluxMainApplication { public static void main(String[] args) throws IOException { HttpHandler handler = (ServerHttpRequest request, ServerHttpResponse response) -> { URI uri = request.getURI(); System.out.println(Thread.currentThread() + "请求进来: " + uri); //编写请求处理的业务,给浏览器写一个内容 URL + "Hello~!" // response.getHeaders(); //获取响应头 // response.getCookies(); //获取Cookie // response.getStatusCode(); //获取响应状态码; // response.bufferFactory(); //buffer工厂 // response.writeWith() //把xxx写出去 // response.setComplete(); //响应结束 //创建 响应数据的 DataBuffer DataBufferFactory factory = response.bufferFactory(); String result = "Hello world"; //数据Buffer DataBuffer buffer = factory.wrap(result.getBytes(StandardCharsets.UTF_8)); // 需要一个 DataBuffer 的发布者 return response.writeWith(Flux.just(buffer)); }; //2、启动一个服务器,监听8080端口,接受数据,拿到数据交给 HttpHandler 进行请求处理 ReactorHttpHandlerAdapter adapter = new ReactorHttpHandlerAdapter(handler); //3、启动Netty服务器 HttpServer.create() .host("localhost") .port(8080) .handle(adapter) //用指定的处理器处理请求 .bindNow(); //现在就绑定 System.out.println("服务器启动完成....监听8080,接受请求"); System.in.read(); System.out.println("服务器停止...."); } }4、DispatcherHandlerSpringMVC: DispatcherServlet;SpringWebFlux: DispatcherHandler1、请求处理流程HandlerMapping:请求映射处理器; 保存每个请求由哪个方法进行处理HandlerAdapter:处理器适配器;反射执行目标方法HandlerResultHandler:处理器结果处理器;SpringMVC: DispatcherServlet 有一个 doDispatch() 方法,来处理所有请求;WebFlux: DispatcherHandler 有一个 handle(ServerWebExchange exchange) 方法,来处理所有请求;public Mono<Void> handle(ServerWebExchange exchange) { if (this.handlerMappings == null) { return createNotFoundError(); } if (CorsUtils.isPreFlightRequest(exchange.getRequest())) { return handlePreFlight(exchange); } return Flux.fromIterable(this.handlerMappings) //拿到所有的 handlerMappings .concatMap(mapping -> mapping.getHandler(exchange)) //找每一个mapping看谁能处理请求 .next() //直接触发获取元素; 拿到流的第一个元素; 找到第一个能处理这个请求的handlerAdapter .switchIfEmpty(createNotFoundError()) //如果没拿到这个元素,则响应404错误; .onErrorResume(ex -> handleDispatchError(exchange, ex)) //异常处理,一旦前面发生异常,调用处理异常 .flatMap(handler -> handleRequestWith(exchange, handler)); //调用方法处理请求,得到响应结果 }1、请求和响应都封装在 ServerWebExchange 对象中,由handle方法进行处理2、如果没有任何的请求映射器; 直接返回一个: 创建一个未找到的错误; 404; 返回Mono.error;终结流3、跨域工具,是否跨域请求,跨域请求检查是否复杂跨域,需要预检请求;4、Flux流式操作,先找到HandlerMapping,再获取handlerAdapter,再用Adapter处理请求,期间的错误由onErrorResume触发回调进行处理;源码中的核心两个:handleRequestWith: 编写了handlerAdapter怎么处理请求handleResult: String、User、ServerSendEvent、Mono、Flux ...concatMap: 先挨个元素变,然后把变的结果按照之前元素的顺序拼接成一个完整流private <R> Mono<R> createNotFoundError() { Exception ex = new ResponseStatusException(HttpStatus.NOT_FOUND); return Mono.error(ex); } Mono.defer(() -> { Exception ex = new ResponseStatusException(HttpStatus.NOT_FOUND); return Mono.error(ex); }); //有订阅者,且流被激活后就动态调用这个方法; 延迟加载; 5、注解开发1、目标方法传参https://docs.spring.io/spring-framework/reference/6.0/web/webflux/controller/ann-methods/arguments.htmlController method argumentDescriptionServerWebExchange封装了请求和响应对象的对象; 自定义获取数据、自定义响应ServerHttpRequest, ServerHttpResponse请求、响应WebSession访问Session对象java.security.Principal org.springframework.http.HttpMethod请求方式java.util.Locale国际化java.util.TimeZone + java.time.ZoneId时区@PathVariable路径变量@MatrixVariable矩阵变量@RequestParam请求参数@RequestHeader请求头;@CookieValue获取Cookie@RequestBody获取请求体,Post、文件上传HttpEntity封装后的请求对象@RequestPart获取文件上传的数据 multipart/form-data.java.util.Map, org.springframework.ui.Model, and org.springframework.ui.ModelMap.Map、Model、ModelMap@ModelAttribute Errors, BindingResult数据校验,封装错误SessionStatus + class-level @SessionAttributes UriComponentsBuilderFor preparing a URL relative to the current request’s host, port, scheme, and context path. See URI Links.@SessionAttribute @RequestAttribute转发请求的请求域数据Any other argument所有对象都能作为参数:1、基本类型 ,等于标注@RequestParam 2、对象类型,等于标注 @ModelAttribute2、返回值写法sse和websocket区别:SSE:单工;请求过去以后,等待服务端源源不断的数据websocket:双工: 连接建立后,可以任何交互;Controller method return valueDescription@ResponseBody把响应数据写出去,如果是对象,可以自动转为jsonHttpEntity, ResponseEntityResponseEntity:支持快捷自定义响应内容HttpHeaders没有响应内容,只有响应头ErrorResponse快速构建错误响应ProblemDetailSpringBoot3;String就是和以前的使用规则一样;forward: 转发到一个地址redirect: 重定向到一个地址配合模板引擎View直接返回视图对象java.util.Map, org.springframework.ui.Model以前一样@ModelAttribute以前一样Rendering新版的页面跳转API; 不能标注 @ResponseBody 注解void仅代表响应完成信号Flux, Observable, or other reactive type使用 text/event-stream 完成SSE效果Other return values未在上述列表的其他返回值,都会当成给页面的数据;6、文件上传https://docs.spring.io/spring-framework/reference/6.0/web/webflux/controller/ann-methods/multipart-forms.htmlclass MyForm { private String name; private MultipartFile file; // ... } @Controller public class FileUploadController { @PostMapping("/form") public String handleFormUpload(MyForm form, BindingResult errors) { // ... } }现在@PostMapping("/") public String handle(@RequestPart("meta-data") Part metadata, @RequestPart("file-data") FilePart file) { // ... }7、错误处理 @ExceptionHandler(ArithmeticException.class) public String error(ArithmeticException exception){ System.out.println("发生了数学运算异常"+exception); //返回这些进行错误处理; // ProblemDetail: 建造者:声明式编程、链式调用 // ErrorResponse : return "炸了,哈哈..."; }8、自定义Flux配置 WebFluxConfigurer容器中注入这个类型的组件,重写底层逻辑@Configuration public class MyWebConfiguration { //配置底层 @Bean public WebFluxConfigurer webFluxConfigurer(){ return new WebFluxConfigurer() { @Override public void addCorsMappings(CorsRegistry registry) { registry.addMapping("/**") .allowedHeaders("*") .allowedMethods("*") .allowedOrigins("localhost"); } }; } }9、Filter@Component public class MyWebFilter implements WebFilter { @Override public Mono<Void> filter(ServerWebExchange exchange, WebFilterChain chain) { ServerHttpRequest request = exchange.getRequest(); ServerHttpResponse response = exchange.getResponse(); System.out.println("请求处理放行到目标方法之前..."); Mono<Void> filter = chain.filter(exchange); //放行 //流一旦经过某个操作就会变成新流 Mono<Void> voidMono = filter.doOnError(err -> { System.out.println("目标方法异常以后..."); }) // 目标方法发生异常后做事 .doFinally(signalType -> { System.out.println("目标方法执行以后..."); });// 目标方法执行之后 //上面执行不花时间。 return voidMono; //看清楚返回的是谁!!! } }3、R2DBC1、手写R2DBC用法:1、导入驱动: 导入连接池(r2dbc-pool)、导入驱动(r2dbc-mysql )2、使用驱动提供的API操作引入依赖<dependency> <groupId>io.asyncer</groupId> <artifactId>r2dbc-mysql</artifactId> <version>1.0.5</version> </dependency>手写代码public static void main(String[] args) throws IOException { // 创建mysql配置 MySqlConnectionConfiguration configuration = MySqlConnectionConfiguration.builder() .host("localhost") .port(3306) .username("root") .password("12345678") .database("test") .build(); // 获取mysql连接工厂 MySqlConnectionFactory factory = MySqlConnectionFactory.from(configuration); Mono.from( factory.create() .flatMapMany(conn -> conn .createStatement("select * from customers where customer_id = ?") .bind(0, 1L) .execute() ).flatMap(result -> result.map(readable -> { return new Customers(((Integer) readable.get("customer_id")), Objects.requireNonNull(readable.get("customer_name")).toString()); })) ).subscribe(System.out::println); System.in.read(); }2、Spring Data R2DBC提升生产力方式的 响应式数据库操作0、整合1、导入依赖 <!-- https://mvnrepository.com/artifact/io.asyncer/r2dbc-mysql --> <dependency> <groupId>io.asyncer</groupId> <artifactId>r2dbc-mysql</artifactId> <version>1.0.5</version> </dependency> <!-- 响应式 Spring Data R2dbc--> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-data-r2dbc</artifactId> </dependency>2、编写配置spring: r2dbc: password: 123456 username: root url: r2dbc:mysql://localhost:3306/test name: test3、@Autowired private R2dbcEntityTemplate template; /** * 测试template // 适合单表操作,复杂sql不好编写 * @throws IOException io异常 */ @Test public void springDataR2dbcTest() throws IOException { // 1. 构建查询条件 Criteria criteria = Criteria .empty() .and("project_leader") .is("1"); // 构建Query对象 Query query = Query .query(criteria); // 查询数据 template.select(query, com.jcq.r2dbc.eneity.Test.class) .subscribe(test -> System.out.println("test = " + test)); System.out.println(System.in.read()); } @Autowired private DatabaseClient databaseClient; /** * 测试databaseClient // 更底层,适合复杂sql 比如join */ @Test public void databaseClientTest() throws IOException { databaseClient.sql("select * from test where id in (?,?)") .bind(0, 1) .bind(1, 2) .fetch() // 抓取数据 .all() // 抓取所有数据 .map(a -> new com.jcq.r2dbc.eneity.Test(((Integer) a.get("id")),a.get("project_leader").toString())) .subscribe(a -> System.out.println("a = " + a)); System.out.println(System.in.read()); }1、声明式接口:R2dbcRepositoryRepository接口@Repository public interface TAutherRepository extends R2dbcRepository<TAuther,Long> { // 根据命名实现sql Flux<TAuther> findAllByIdAndNameLike(Long id,String name); @Query("select * from t_author") Flux<TAuther> queryList(); } 自定义Converter@ReadingConverter // 读取数据库的时候,吧row转成 TBook public class TBookConverter implements Converter<Row, TBook> { @Override public TBook convert(Row source) { TBook tBook = new TBook(); tBook.setId((Long) source.get("id")); tBook.setTitle((String) source.get("title")); tBook.setAuthorId((Long) source.get("author_id")); Object instance = source.get("publish_time"); System.out.println(instance); ZonedDateTime instance1 = (ZonedDateTime) instance; tBook.setPublishTime(instance1.toInstant()); TAuther tAuther = new TAuther(); tAuther.setName(source.get("name", String.class)); tBook.setTAuther(tAuther); return tBook; } }配置生效@Configuration public class R2DbcConfiguration { @Bean @ConditionalOnMissingBean public R2dbcCustomConversions r2dbcCustomConversions() { return R2dbcCustomConversions.of(MySqlDialect.INSTANCE, new TBookConverter()); } } 3、编程式组件R2dbcEntityTemplateDatabaseClient4、最佳实践最佳实践: 提升生产效率的做法1、Spring Data R2DBC,基础的CRUD用 R2dbcRepository 提供好了2、自定义复杂的SQL(单表): @Query;3、多表查询复杂结果集: DatabaseClient 自定义SQL及结果封装;@Query + 自定义 Converter 实现结果封装经验:1-1:1-N 关联关系的封装都需要自定义结果集的方式Spring Data R2DBC:自定义Converter指定结果封装DatabaseClient:贴近底层的操作进行封装; 见下面代码MyBatis: 自定义 ResultMap 标签去来封装databaseClient.sql("select b.*,t.name as name from t_book b " + "LEFT JOIN t_author t on b.author_id = t.id " + "WHERE b.id = ?") .bind(0, 1L) .fetch() .all() .map(row-> { String id = row.get("id").toString(); String title = row.get("title").toString(); String author_id = row.get("author_id").toString(); String name = row.get("name").toString(); TBook tBook = new TBook(); tBook.setId(Long.parseLong(id)); tBook.setTitle(title); TAuthor tAuthor = new TAuthor(); tAuthor.setName(name); tAuthor.setId(Long.parseLong(author_id)); tBook.setAuthor(tAuthor); return tBook; }) .subscribe(tBook -> System.out.println("tBook = " + tBook));
2025年01月15日
17 阅读
0 评论
0 点赞
2020-04-11
写给新同学的SpringBoot教程 — 高级篇
写给新同学的SpringBoot教程 — 高级篇一、添加缓存1、使用SpringBoot自带缓存功能1、基本使用在主类上添加注解@EnableCaching在业务方法上添加@Cacheable(cacheNames = "xxx")注解这样即可实现基础的缓存功能基本流程:@Cacheable:方法执行之前,先去查询Cache(缓存组件),按照CacheNames指定的名字获取,(CacheManager先获取相应的缓存),第一次获取缓存如果没有Cache组件会自行创建去Cache中查找缓存的内容,使用一个key,默认就是方法的参数key是按照某种策略生成的,默认hi使用keyGenerator生成的。默认使用SimplekeyGenerator生成key: 如果有一个参数,key=参数的值 如果没有参数,key=new SimpleKey(); 如果有多个参数,key=new SimpleKey(params);没有查到缓存就去调用目标方法;方法查询结束后,将目标方法返回的结果放入到缓存中。总结: @Cacheable标注的方法执行之前先来检查缓存中有没有这个数据,默认按照参数的值作为key去查询缓存,如果缓存中没有,就执行方法并将结果放入到缓存中;以后再来调用就可以直接将缓存中的数据进行返回。核心:使用CaCheManager【ConCurrentMapCacheManager】按照名字得到Cache【ConcurrentMapCache】组件key是使用keyGenerator生成的,默认是SimpleKeyGenerator主类:@SpringBootApplication @MapperScan(value = "com.jcq.cache.mapper") @EnableCaching //这个注解不要忘了 public class Springboot09CacheApplication { public static void main(String[] args) { SpringApplication.run(Springboot09CacheApplication.class, args); } }业务方法@Service public class EmployeeService { @Resource private EmployeeMapper employeeMapper; @Cacheable(cacheNames = "emp",key = "#root.methodName+'['+#id+']'") public Employee getEmp(Integer id){ return employeeMapper.selEmpByid(id); } }2、其他属性keyGenerator:key的生成器,可以自己指定key的生成器组件的id ,key/keyGenerator二选一使用。cacheManager:指定缓存管理器,或者cacheResolver指定获取解析器。condition:满足条件是放入缓存。unless:满足条件是不放入缓存,与condition正好相反。sync:默认为false,true为开启异步缓存。若开启异步,unless注解无效3、其他注解@CachePut注解:方法运行完后,将数据放入缓存中@CachePut与@Cacheable的区别: 后者在方法运行前先到缓存总查询,前者在方法执行后,将数据放入缓存中。注意: 只有保持数据的key相同,才能实现数据的同步更新。 @CachePut(cacheNames = "emp",key = "#employee.id") public Employee update(Employee employee){ employeeMapper.updEmp(employee); return employee; }@CacheEvict:清除缓存数据allEntries:清空所有缓存,默认为falsebeforeInvocation:在方法执行前清空缓存,默认为false注意:保持数据key同一,才可以保持数据同步。 //测试清除缓存中的数据 @CacheEvict(cacheNames = "emp",key = "#id") public void delEmpCache(Integer id){ System.out.println("清除缓存"); }@Caching:可以将多个注解组合//测试Caching的使用 @Caching(cacheable = { @Cacheable(value = "emp", key = "#lastName") }, put = {@CachePut(value = "emp", key = "#result.id"), @CachePut(value = "emp",key = "#result.email") } ) public Employee selBylastName(String lastName){ return employeeMapper.selBylastName(lastName); } 2、整合redis1、redis命令常用命令网址2、步骤在pom.xml中引入下列依赖<!--引入redis启动器--> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-data-redis</artifactId> </dependency>配置文件如下#配置redis spring.redis.host=192.168.91.128SpringBoot把对redis的操作封装在RedisTemplate 和 StringRedisTemplate中@SpringBootTest class Springboot09CacheApplicationTests { @Autowired private RedisTemplate redisTemplate; @Autowired private StringRedisTemplate stringRedisTemplate; @Autowired private EmployeeService employeeService; @Test public void testRedis(){ //添加字符串 // stringRedisTemplate.opsForValue().append("msg","helloworld"); //添加list // stringRedisTemplate.opsForList().leftPushAll("mylist","1","2","3"); //取出list // String str = stringRedisTemplate.opsForList().leftPop("mylist"); // System.out.println(str); Employee emp = employeeService.getEmp(1); //添加对象 redisTemplate.opsForValue().set("emp",emp); }3、添加自定义组件,将对象转化为json字符串存储@Configuration public class MyRedisTemplate { @Bean public RedisTemplate<Object, Employee> redisTemplate(RedisConnectionFactory redisConnectionFactory) throws UnknownHostException { RedisTemplate<Object, Employee> template = new RedisTemplate(); template.setConnectionFactory(redisConnectionFactory); template.setDefaultSerializer(new Jackson2JsonRedisSerializer(Employee.class)); return template; } }而在服务启动过程中,使用的CacheManager依旧默认按照序列化的方式进行对象存储,所以还需要添加组件来达到将对象用json存储的目的 /** * 缓存管理器 */ @Bean public CacheManager cacheManager(RedisConnectionFactory redisConnectionFactory) { //初始化一个RedisCacheWriter RedisCacheWriter redisCacheWriter = RedisCacheWriter.nonLockingRedisCacheWriter(redisConnectionFactory); //设置CacheManager的值序列化方式为json序列化 RedisSerializer<Object> jsonSerializer = new GenericJackson2JsonRedisSerializer(); RedisSerializationContext.SerializationPair<Object> pair = RedisSerializationContext.SerializationPair .fromSerializer(jsonSerializer); RedisCacheConfiguration defaultCacheConfig=RedisCacheConfiguration.defaultCacheConfig() .serializeValuesWith(pair); //设置默认超过期时间是30秒 defaultCacheConfig.entryTtl(Duration.ofSeconds(30)); //初始化RedisCacheManager return new RedisCacheManager(redisCacheWriter, defaultCacheConfig); }二、SpringBoot与消息1、使用docker安装rabbitmq//下载rabbitmq docker pull rabbitmq:3-management //运行 docker run -d -p 5672:5672 -p 15672:15672 --name myrabbitmq id默认用户名密码均为:guest转换器类型:direct:只有key相同的队列才可以收到消息fanout:任意队列都能收到消息atguigu.# : #表示0或多个词.news : 表示0或一个单词2、SpringBoot整合Rabbitmq1、基础使用配置文件:**spring.rabbitmq.username=guest spring.rabbitmq.password=guest spring.rabbitmq.host=192.168.91.128 #默认为5672,不需要配置 #spring.rabbitmq.port=5672测试代码: /** * 单播 */ @Test void contextLoads() { Map<String, Object> map = new HashMap<>(); map.put("name", "张三"); map.put("age", 18); rabbitTemplate.convertAndSend("exchange.direct","atguigu.news",map); }但是这样发送的消息,会以默认的序列化方式进行存储。效果接收消息: /** * 接收消息 */ @Test void recieve(){ Object o = rabbitTemplate.receiveAndConvert("atguigu.news"); System.out.println(o.getClass()); System.out.println(o); }自定义组件,使用json格式进行对象转化@Configuration public class MyAMQPConfig { @Bean public MessageConverter messageConverter(){ return new Jackson2JsonMessageConverter(); } }广播测试代码 /** * 广播 */ @Test void sendMsg(){ rabbitTemplate.convertAndSend("exchange.fanout","",new Book("SpringMVC","张佳明")); }2、消息监听主类上添加@EnableRabbit注解@EnableRabbit @SpringBootApplication public class SpringBoot10AmqpApplication { public static void main(String[] args) { SpringApplication.run(SpringBoot10AmqpApplication.class, args); } }业务代码如下:@Service public class BookService { @RabbitListener(queues = "atguigu.news") public void bookLinstener(Book book){ System.out.println("接收到消息"+book); } }3、使用AmqpAdmin管理工具 @Autowired private AmqpAdmin amqpAdmin; @Test void createRabbit(){ //创建转换器 // amqpAdmin.declareExchange(new DirectExchange("AmqpAdmin.exchange")); // System.out.println("转换器创建成功"); //创建消息队列 // amqpAdmin.declareQueue(new Queue("AmqpAdmin.news")); // System.out.println("消息队列创建成功"); //创建绑定规则 amqpAdmin.declareBinding(new Binding("AmqpAdmin.news", Binding.DestinationType.QUEUE, "AmqpAdmin.exchange", "AmqpAdmin.haha", null)); System.out.println("创建绑定成功"); //删除队列 amqpAdmin.deleteQueue("AmqpAdmin.news"); System.out.println("删除队列"); amqpAdmin.deleteExchange("AmqpAdmin.exchange"); System.out.println("删除转换器"); }三、SpringBoot与检索ElasticSearch1、搭建环境1、下载es docker pull elasticsearch 2、启动命令 [root@localhost ~]# docker run -e ES_JAVA_OPTS="-Xms256m -Xmx256m" -d -p 9200:9200 -p 9300:9300 --name ES01 5acf0e8da90b 7d33661ebabc32621173c4c9648047016e6ede9fc625c51340692736b7fdf170 其中: ES_JAVA_OPTS="-Xms256m -Xmx256m" :-Xms 初始堆内存大小 -Xmx 最大使用内存 9200:es默认web通信使用9200端口 9300:当分布式情况下es多个节点之间通信使用9300端口 文档地址:https://www.elastic.co/guide/cn/elasticsearch/guide/current/index-doc.htmlSpringBoot默认支持两种技术和ElasticSearch交互一、Jest(默认不生效)需要导入jest的工具包(io.serachbox.chlient.JestClient)需要将默认的SpringBoot-data-Elasticsearch注释掉导入jest依赖但是jest已经过时,不演示了二、SpringDataClient节点信息clusterNodes,clusterNameElasticsearchTemplate来操作es编写一个ElasticsearchRepository子接口来操作es因版本问题,暂时没有解决SpringBoot2.X版本整合es的方法,使用会报错四、SpringBoot与任务1、异步任务1、在主类添加@EnableAsync注解2、在业务层添加@Async注解即可实现异步任务@Async @Service public class AsyncService { public void testAsync(){ try { Thread.sleep(3000); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println("方法执行完毕"); } }2、定时任务cron表达式:Cron表达式的格式:秒 分 时 日 月 周 年(可选)。字段名 允许的值 允许的特殊字符 秒 0-59 , - * / 分 0-59 , - * / 小时 0-23 , - * / 日 1-31 , - * ? / L W C月 1-12 or JAN-DEC , - * / 周几 1-7 or SUN-SAT , - * ? / L C # 年(可选字段) empty 1970-2099 , - * /具体的用法:https://www.cnblogs.com/lazyInsects/p/8075487.html @Scheduled(cron = "0 * * * * MON-FRI") 周一到周五,没整分运行一次 // @Scheduled(cron = "0/4 * * * * MON-FRI") 没整分,间隔4秒执行 @Scheduled(cron = ) public void testAsync(){ System.out.println("方法执行完毕"); } }在方法上加上@Scheduled注解在主类添加@EnableSchelding注解主类代码如下//@EnableAsync @EnableScheduling @SpringBootApplication public class SpringBoot12TaskApplication { public static void main(String[] args) { SpringApplication.run(SpringBoot12TaskApplication.class, args); } } 3、邮件任务配置文件spring.mail.username=1036658425@qq.com spring.mail.password=kersbnhzzuhfbdih spring.mail.host=smtp.qq.com@Autowired JavaMailSenderImpl mailSender; @Test void contextLoads() { //创建简单邮件对象 SimpleMailMessage massage = new SimpleMailMessage(); //设置主题 massage.setSubject("简单测试"); //设置内容 massage.setText("测试简单邮件发送"); //设置收件方 massage.setTo("594082079@qq.com"); //设置发送方 massage.setFrom("1036658425@qq.com"); mailSender.send(massage); } @Test public void test02() throws MessagingException { //测试复杂邮件发送 MimeMessage mimeMessage = mailSender.createMimeMessage(); MimeMessageHelper helper = new MimeMessageHelper(mimeMessage,true); helper.setSubject("复杂发送"); helper.setText("<b style='color:red'>测试复杂邮件发送</b>",true); helper.addAttachment("a.jpg",new File("E:\\笔记\\SpringBoot\\springboot高级\\SpringBoot高级.assets\\image-20200310195943079.png")); helper.addAttachment("b.jpg",new File("E:\\笔记\\SpringBoot\\springboot高级\\SpringBoot高级.assets\\image-20200310201323308.png")); helper.setTo("594082079@qq.com"); helper.setFrom("1036658425@qq.com"); mailSender.send(mimeMessage); } 五、SpringBoot与安全1、业务部分代码编写配置类@EnableWebSecurity public class MySecurityConfig extends WebSecurityConfigurerAdapter { //添加认证功能 @Override protected void configure(HttpSecurity http) throws Exception { // super.configure(http); http.authorizeRequests().mvcMatchers("/").permitAll() .mvcMatchers("/level1/**").hasRole("VIP1") .mvcMatchers("/level2/**").hasRole("VIP2") .mvcMatchers("/level3/**").hasRole("VIP3"); //开启登录认证页面 http.formLogin(); //开启自动配置注销功能 http.logout().logoutSuccessUrl("/"); //访问/logout 清除session //开启自动配置记住我功能 http.rememberMe(); } //添加登录授权功能 @Override protected void configure(AuthenticationManagerBuilder auth) throws Exception { // super.configure(auth); auth.inMemoryAuthentication().passwordEncoder(new BCryptPasswordEncoder()) .withUser("zhangsan").password(new BCryptPasswordEncoder().encode("123456")).roles("VIP1","VIP2") .and().withUser("lisi").password(new BCryptPasswordEncoder().encode("123456")).roles("VIP2","VIP3") .and().withUser("wangwu").password(new BCryptPasswordEncoder().encode("123456")).roles("VIP3","VIP1"); }整合页面部分,因版本问题,暂时没有效果,先不放图片了六、分布式1、Dubbo下载镜像[root@localhost ~]# docker pull zookeeper [root@localhost ~]# docker run --name zk01 -p 2181:2181 --restart always -d bbebb888169c 1、提供者将服务提供者注册到注册中心导入相关依赖Dubbo和Zkclient<!--引入Dubbo服务启动器--> <dependency> <groupId>com.alibaba.boot</groupId> <artifactId>dubbo-spring-boot-starter</artifactId> <version>0.1.0</version> </dependency> <!--引入zookeeper的客户端工具--> <!-- https://mvnrepository.com/artifact/com.github.sgroschupf/zkclient --> <dependency> <groupId>com.github.sgroschupf</groupId> <artifactId>zkclient</artifactId> <version>0.1</version> </dependency>2. 在主类添加启用Dubbo的注解 @EnableDubbo @SpringBootApplication public class ProviderTicketApplication { public static void main(String[] args) { SpringApplication.run(ProviderTicketApplication.class, args); } }如果不加此注解,消费者会报空指针异常 2. 配置application配置文件 dubbo.application.name=provider dubbo.registry.address=zookeeper://192.168.91.128:2181 dubbo.scan.base-packages=com.jcq.ticket.service使用@service发布服务@Component @Service public class TickerServiceImpl implements TickerService { @Override public String getTicker() { return "《厉害了,我的国》"; } }2、消费者导入相关依赖<!--引入Dubbo服务启动器--> <dependency> <groupId>com.alibaba.boot</groupId> <artifactId>dubbo-spring-boot-starter</artifactId> <version>0.1.0</version> </dependency> <!--引入zookeeper的客户端工具--> <!-- https://mvnrepository.com/artifact/com.github.sgroschupf/zkclient --> <dependency> <groupId>com.github.sgroschupf</groupId> <artifactId>zkclient</artifactId> <version>0.1</version> </dependency>配置文件dubbo.application.name=consumer dubbo.registry.address=zookeeper://192.168.91.128:2181 server.port=8081引用服务需要把提供者的包个接口复制到项目中,且保持路径同一加下来就是编写业务代码了import com.alibaba.dubbo.config.annotation.Reference; import org.springframework.stereotype.Service; @Service public class UserServiceImpl { @Reference private TickerService tickerService; public String getTicker(){ return tickerService.getTicker(); } }注意不要到错包了,会导致空指针异常。2、SpringCloud1、注册中心1、首先在主类添加@EnableEurekaServer注解@EnableEurekaServer @SpringBootApplication public class EurekaServerApplication { public static void main(String[] args) { SpringApplication.run(EurekaServerApplication.class, args); } }2、添加配置文件server: port: 8761 #修改服务端口 eureka: client: fetch-registry: false #不从注册中心获取服务 register-with-eureka: false #不在服务中心注册服务 service-url: defaultZone: http://localhost:8761/eureka/ #访问路径 instance: hostname: eureka-server #主机名称3、登录页面查看是否启动成功网址:http://localhost:8761/2、提供者1、编写配置文件server: port: 8001 spring: application: name: provider-ticket eureka: instance: prefer-ip-address: true #注册服务时,使用服务的ip地址 service-url: defaultZone: http://localhost:8761/eureka/2、正常编写业务代码及控制器代码一个项目注册多个服务直接将不同端口好的项目进行打包,使用cmd命令分别执行即可项目打包后的图片管理页面显示内容入下3、消费者1、编写配置文件server: port: 8200 eureka: instance: prefer-ip-address: true service-url: defaultZone: http://localhost:8761/eureka/ spring: application: name: consumer-user2、主类代码:@EnableDiscoveryClient //开启到注册中心查找服务 @SpringBootApplication public class ConsumerUserApplication { public static void main(String[] args) { SpringApplication.run(ConsumerUserApplication.class, args); } /** * 给容器添加组件 * 可以发送远程http请求 * @return */ @LoadBalanced //开启负载均衡功能 @Bean public RestTemplate restTemplate(){ return new RestTemplate(); } }3、业务代码@RestController public class UserController { @Autowired private RestTemplate restTemplate; @GetMapping("/buy") public String buyTicket(String name){ //发送远程请求,请求注册中心的业务方法。地址直接写注册中心中的服务名,不需写端口号 String ticket = restTemplate.getForObject("http://PROVIDER-TICKET/get", String.class); return name+"购买了"+ticket; } }七、热部署只需要到如SpringBoot提供的依赖即可<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-devtools</artifactId> <scope>runtime</scope> <optional>true</optional> </dependency>更改代码后,手动CTRL+F9 重新编译,即可实现热部署
2020年04月11日
4 阅读
0 评论
0 点赞
2020-04-06
写给新同学的SpringBoot教程 — 入门篇
一、写给新同学的SpringBoot教程 — 入门篇1、springboot简介在 Java 开发的世界里,Spring Boot 绝对是一个绕不开的重要框架。它是由 Pivotal 团队在 2014 年推出的,基于 Spring 框架的开源项目,旨在简化 Spring 应用的初始搭建和开发过程。2、微服务2014,martin fowler微服务:架构风格(服务微化)一个应用应该是一组小型服务;可以通过HTTP的方式进行互通;单体应用:ALL IN ONE微服务:每一个功能元素最终都是一个可独立替换和独立升级的软件单元;3、环境准备1、maven设置推荐使用3.X我使用的是3.3.32、IDEA配置配置maven、jdk等相关设置4、springboot编写helloworld通过idea自动提供的脚手架功能快速创建springboot项目1、创建springboot时可能出现的错误1、连接springboot网站超时问题 如图给与解决方式 1、1 点击设置 1、2 一次点击图示位置进行网站设置 1、3 在弹出的界面中数据一下网址,并测试连接情况 1、4 出现图示连接成功后,重新创建springboot项目即可5、HelloWorld探究1、pom文件1、父项目<parent> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-parent</artifactId> <version>2.2.5.RELEASE</version> <relativePath/> <!-- lookup parent from repository --> </parent> springboot会自动依赖一个父项目2、启动器<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> 不同的项目会导入不同的启动器,springboot会根据启动器而去选择导入相应的包,从而省去了我们之前配置的xml文件3、主程序类 主程序的main方法要写在最外层的包中,即包含所有的子包,这样就可以加载到所有的配置文件,这是必须的 主程序类需要配置相应的注解:@SpringBootApplication @EnableAutoConfiguration:此注解的作用为,开启自动配置 我们需要自己配置的东西,自动配置类都帮我们解决了。 以上配置都在springboot-autoconfigure这个jar包中6、使用idea快速创建项目1、java目录 1、使用快速创建的项目,java目录中会自动生成主方法,我们只需编写业务逻辑代码即可。 2、@SpringBootApplication:该注解等同于@RequestMapping()和ResponseBody相结合2、resource目录 1、static文件夹:用于存放静态资源,如js、html、image、等 2、templates文件夹:用户放页面显示文件,springboot默认不支持jsp,但是也可以引用模板来使用jsp或他模板3、application.properties 1、该文件中可以修改一些springboot的默认配置,如Tomcat默认端口号等。项目目录结构见下图二、配置文件1、spring认同的配置文件1、application.properties 与我们之前用的配置文件使用方法相同。2、application.yml2、yml文件 yaml 是一种文件类型 也可以也做ymlyaml yaml ain't markup language 这句话可以理解为:yaml a markup language yaml是一个标记语言yaml ism't a markup language yaml不是一个标记语言其实yaml还是标记语言yaml与xml的区别yaml以数据为中心,比xml、json等更适合作为配置文件server: port: 8081 xml<servetr> <port>8081</port> </servetr>3、yaml语法1、基本语法 yaml文件中默认的格式为 k: v k与v中间的空格是不可缺少的,所有左对齐的数据为同一级别。 yaml文件只可以使用空格,不可以使用tab键2、值得表示字面量:普通的值 包括:数字,字符串,布尔值等 语法: k: v 直接在后面写值即可 如: name: zhangsan yaml中“” 与‘’ 的区别: yaml 中双引号不会对内容进行转义 name: "zhangsan \n" 会输出张三+换行 yaml 中单引号会对内容进行转义 name: 'zhangsan \n' 会输出张三 \n对象、map语法如下:people: name: zhangsan age: 19行内式写法people: {name: zhangsan,age: 19}数组(list、集合)以- 的方式进行书写语法如下:pets: - dog - cat - pig行内式的写法如下:pets: [dog,cat,pig]如此写法,你懂了吗?3、yaml文件及代码示例yaml配置文件person: name: zhangsan age: 18 date: 2020/3/4 map: {k1: v1,k2: v2} list: - l1 - l2 pet: name: zhuzhu age: 3代码部分如下:public class Person { private String name; private int age; private Date date; private Map<String,Object> map; private List<Object> list; private Pet pet; //getter and setter 省略...4、使用properties进行相同的文件配置person.name=张三 person.age=18 person.date=2020/3/5 person.map.k1=v1 person.map.k2=v2 person.list=l1,l2 person.pet.name=zhuzhu person.pet.age=34、配置文件值得注入1、properties默认采用utf-8编码,记得修改idea编码格式 修改方法见下图 将图示位置进行修改,点击ok返回。2、@ConfigurationProperties与@Value的比较 @ConfigurationProperties@value功能批量注入配置文件中的属性单值进行配置文件注入松散绑定(松散语法)支持不支持SpL不支持支持JSR303数据校验支持不支持复杂类型封装支持不支持注: 1、松散绑定:如 person-name 等同于personName 2、SpEL:spring计算语言 如 #{30*2} 可以在文件注入时进行数据计算 3、JSR303数据校验:如@Email 校验数据格式是否为邮件格式 ,使用数据校验需要在类上添加@Validate注解 4、复杂类型封装:如map等总结: 1、都可以进行配置文件注入。 2、如果对整个javabean进行值的注入,使用@ConfigurationProperties,方便快捷。 3、如果只是在逻辑中,单独对某个属性进行注入,使用@Value。3、数据校验代码如下@Component @ConfigurationProperties(prefix = "person") @Validated public class Person { @Email private String name; private int age; private Date date; private Map<String,Object> map; private List<Object> list; private Pet pet;@Validated 与 @Email 搭配使用4、@PropertiesSource和@ImportResource的使用1、@PropertiesSource 它的作用是加载制定位置的配置文件。@Component @ConfigurationProperties(prefix = "person") @PropertySource(value = {"classpath:person.properties"}) public class Person { private String name; private int age; private Date date; private Map<String,Object> map; private List<Object> list; private Pet pet; 注意:不可以把@ConfigurationProperties注释掉2、@ImportResource给spring添加组件以前使用xml的形式给spring容器添加组件如:<?xml version="1.0" encoding="UTF-8"?> <beans xmlns="http://www.springframework.org/schema/beans" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd"> <bean id="helloService" class="com.jcq.springboot.service.HelloService"></bean> </beans>然后在主类上添加注解@ImportResource@ImportResource(locations = {"classpath:bean.xml"})不过springboot更推荐使用配置类的方式添加组件使用全注解的方式配置类的代码如下:/** * @Configuration:指明当前类是一个配置类,用来替代之前的配置文件 */ @Configuration public class MyAppConfig { @Bean public HelloService helloService(){ System.out.println("给容器中添加组件"); //方法的返回值,将会添加到容器中,组件的默认id为方法名 return new HelloService(); } } 5、配置文件占位符1、随机数2、占位符代码如下person.name=张三${random.uuid} person.age=${random.int} person.date=2020/3/5 person.map.k1=v1 person.map.k2=${persion.service:hello}_v2 person.list=l1,l2 person.pet.name=${person.name}zhuzhu person.pet.age=35、profile1、properties文件 1、通过 application-{fileName}.properties 的方式进行区分 2、在application.properties 文件中激活server.port=8081 spring.profiles.active=dev #--------- server.port=8082 spring.profiles=dev2、yaml文件 通过 --- 进行区分server: port: 8081 spring: profiles: active: dev #表示激活 --- server: port: 8082 spring: profiles: prod --- server: port: 8083 spring: profiles: dev3、在命令行进行激活 指令: --spring.profiles.active=dev可以将文件打成far包后用 java -jar命令后 继续使用该命令也可以通过给虚拟机使用命令 -Dspring.profiles.active=dev 达到激活的效果6、配置文件加载位置1、优先级springboot会扫描一下位置下的properties或yaml文件作为默认的配置文件file:/configfile:/classpath:/configclasspath:/优先级从上到下依次递减2、互补配置==优先级高的配置文件会覆盖掉优先级低的配置文件,进行配置互补==也可以在项目发布后使用命令添加配置文件加载路径,改变默认配置--spring.config.location=${filepath}7、外部配置加载位置==最高级为命令行==...==jar包外的优先于jar包内的====带{profile}的优先于不带{profile}的==详情参考官方文档分类十分详细,没有做具体笔记==优先级从高到低;高优先级的配置覆盖低优先级的配置,所有的配置会 形成互补配置== 1.命令行参数 所有的配置都可以在命令行上进行指定 java -jar spring-boot-02-config-02-0.0.1-SNAPSHOT.jar --server.port=8087 --server.context-path=/abc 多个配置用空格分开; --配置项=值 2.来自java:comp/env的JNDI属性 3.Java系统属性(System.getProperties()) 4.操作系统环境变量 5.RandomValuePropertySource配置的random.*属性值 ==由jar包外向jar包内进行寻找;== ==优先加载带profile== 6.jar包外部的application-{profile}.properties或application.yml(带spring.profile)配置文件 7.jar包内部的application-{profile}.properties或application.yml(带spring.profile)配置文件 ==再来加载不带profile== 8.jar包外部的application.properties或application.yml(不带spring.profile)配置文件 9.jar包内部的application.properties或application.yml(不带spring.profile)配置文件 10.@Configuration注解类上的@PropertySource 11.通过SpringApplication.setDefaultProperties指定的默认属性 所有支持的配置加载来源; 参考官方文档8、自动配置原理springboot会加载相关的自动配置类自动配置类中会个容器中添加相应的组件每个组件会加载相应的properties类,在属性类中,会到我们的配置文件中加载相应属性1、自动配置类如:@Configuration( proxyBeanMethods = false ) @EnableConfigurationProperties({HttpProperties.class})//相应的properties类 @ConditionalOnWebApplication( type = Type.SERVLET ) @ConditionalOnClass({CharacterEncodingFilter.class}) @ConditionalOnProperty( prefix = "spring.http.encoding", //可以从这里查看我们在配置文件中应该配置的内容前缀 value = {"enabled"}, matchIfMissing = true ) public class HttpEncodingAutoConfiguration { private final Encoding properties;//引用properties类 @Bean //给容器中添加组件 @ConditionalOnMissingBean public CharacterEncodingFilter characterEncodingFilter() { CharacterEncodingFilter filter = new OrderedCharacterEncodingFilter(); //有些内容需要用到配置文件中的相关配置 filter.setEncoding(this.properties.getCharset().name()); filter.setForceRequestEncoding(this.properties.shouldForce(org.springframework.boot.autoconfigure.http.HttpProperties.Encoding.Type.REQUEST)); filter.setForceResponseEncoding(this.properties.shouldForce(org.springframework.boot.autoconfigure.http.HttpProperties.Encoding.Type.RESPONSE)); return filter; }2、properties类public class HttpProperties { private boolean logRequestDetails; private final HttpProperties.Encoding encoding = new HttpProperties.Encoding(); //通过静态内部类的方式加载属性文件 //这些属性的值,就是我们在配置问价中可以进行的相关配置 public static class Encoding { public static final Charset DEFAULT_CHARSET; private Charset charset; private Boolean force; private Boolean forceRequest; private Boolean forceResponse; private Map<Locale, Charset> mapping;3、精髓:SpringBoot启动时会加载大量配置类我们看我们需要的功能是否具备相应的配置类我们再来看这个配置类中配置了哪些组件。(如果没有我们需要的组件,就需要我们自行进行配置,如果存在相应配置类,我们就不用再进行配置了)给容器中自动配置了添加属性时,会从Properties类中获取某些属性,我们就可以在配置文件中,对这些属性进行赋值。xxxAutoConfiguration:自动配置类给容器中添加组件;xxxProperties:相应属性类封装配置文件中相关属性;4、细节@Condition注解的的扩展@Conditional的扩展注解 1.class条件注解 @ConditionalOnClass:某个class位于类路径上,才会实例化一个Bean。 @ConditionalOnMissingClass:某个class类路径上不存在的时候,才会实例化一个Bean。 2.Bean条件注解 @ConditionalOnBean:当容器中有指定Bean的条件下进行实例化。 @ConditionalOnMissingBean:当容器里没有指定Bean的条件下进行实例化。 3.属性条件注解 @ConditionalOnProperty:当指定的属性有指定的值时进行实例化。 4.Resource条件注解 @ConditionalOnResource:当类路径下有指定的资源时触发实例化。 5.web条件注解 @ConditionalOnNotWebApplication:不是web应用,才会实例化一个Bean。 @ConditionalOnWebApplication:当项目是一个Web项目时进行实例化。 6.表达式条件注解 @ConditionalOnExpression:基于SpEL表达式的条件判断,当表达式为true的时候,才会实例化一个Bean。 @ConditionalOnJava:当JVM版本为指定的版本范围时触发实例化。 @ConditionalOnJndi:在JNDI存在的条件下触发实例化。 @ConditionalOnSingleCandidate:当指定的Bean在容器中只有一个,或者有多个但是指定了首选的Bean时触发实例化。 @Condition自动配置报告在配置文件中添加debug=true属性即可打印自动配置报告配置内容#server.port=8081 #spring.profiles.active=dev debug=true自动配置报告============================ CONDITIONS EVALUATION REPORT ============================ Positive matches: ----------------- AopAutoConfiguration matched: - @ConditionalOnProperty (spring.aop.auto=true) matched (OnPropertyCondition) AopAutoConfiguration.ClassProxyingConfiguration matched: - @ConditionalOnMissingClass did not find unwanted class 'org.aspectj.weaver.Advice' (OnClassCondition) - @ConditionalOnProperty (spring.aop.proxy-target-class=true) matched (OnPropertyCondition) Negative matches: ----------------- ActiveMQAutoConfiguration: Did not match: - @ConditionalOnClass did not find required class 'javax.jms.ConnectionFactory' (OnClassCondition) 自动配置报告中,包括符合条件启动的配置类,以及未符合条件的配置类三、日志1、日志框架日志框架的来源,不在此进行记录了spring框架默认是采用JCL门面,commons-logging==SpringBoot选用SLF4J和logback==2、SLF4F框架使用1、helloworld 入门import org.slf4j.Logger; import org.slf4j.LoggerFactory; public class HelloWorld { public static void main(String[] args) { Logger logger = LoggerFactory.getLogger(HelloWorld.class); logger.info("Hello World"); } }2、sff4j结合其他框架使用需要的jar3、遗留问题应用层框架不能直接转换成slf4j,还需要中间包进行转换3、SpringBoot日志关系==SpringBoot能自动适配所有日志,而且底层使用SLF4J和logback方式记录日志,引入其他框架的时候,只需要把这个框架依赖的日志包排除掉==4、日志使用1、默认配置SpringBoot默认对打印日志的格式和级别进行了设置,我们可以在配置文件中,进行修改SpringBoot的默认打印日志级别为Info,并且不会将日志写到文件。代码如下: //记录器 Logger logger = LoggerFactory.getLogger(getClass()); //日志级别 logger.trace("这是trance日志。。。"); logger.debug("这是debug日志。。。"); logger.info("这是info日志。。。"); logger.warn("这是warn日志。。。"); logger.error("这是error日志。。。");配置文件如下:# 修改指定包下的日志级别 logging.level.com.jcq.springboot=trace #修改日志输出格式 logging.pattern.console=%C %d{YYYY-MM-dd hh:mm:ss} %m %n2、指定配置在类路径下放上不同日志框架指定格式的配置文件,就可以不使用SpringBoot的默认配置了。Logging SystemCustomizationLogbacklogback-spring.xml, logback-spring.groovy, logback.xml, or logback.groovyLog4j2log4j2-spring.xml or log4j2.xmlJDK (Java Util Logging)logging.propertieslogback.xml:直接被识别logback-spring.xml:日志框架不会直接加载配置项,有SpringBoot解析配置项,可以使用SpringBoot的高级功能,推荐使用logback-spring.xml3、日志框架转换按照之前的转换图进行转换,注意移除部分依赖四、web开发1、简介创建SpringBoot的web项目创建项目是勾选需要使用到的模块。SpringBoot已经提供了相应功能的支持。编写自己的业务代码熟悉每个模块需要自动配置类,以及需要的配置文件2、静态资源路径规则1)、引入的静态资源默认路径为/webjar/** public void addResourceHandlers(ResourceHandlerRegistry registry) { if (!this.resourceProperties.isAddMappings()) { logger.debug("Default resource handling disabled"); } else { Duration cachePeriod = this.resourceProperties.getCache().getPeriod(); CacheControl cacheControl = this.resourceProperties.getCache().getCachecontrol().toHttpCacheControl(); if (!registry.hasMappingForPattern("/webjars/**")) { this.customizeResourceHandlerRegistration(registry.addResourceHandler(new String[]{"/webjars/**"}).addResourceLocations(new String[]{"classpath:/META-INF/resources/webjars/"}).setCachePeriod(this.getSeconds(cachePeriod)).setCacheControl(cacheControl)); String staticPathPattern = this.mvcProperties.getStaticPathPattern(); if (!registry.hasMappingForPattern(staticPathPattern)) { this.customizeResourceHandlerRegistration(registry.addResourceHandler(new String[]{staticPathPattern}).addResourceLocations(WebMvcAutoConfiguration.getResourceLocations(this.resourceProperties.getStaticLocations())).setCachePeriod(this.getSeconds(cachePeriod)).setCacheControl(cacheControl)); } } }可以到webjar官方获得相应依赖网站访问项目的资源路径为: http://localhost:8080/webjar/${filename}webjar:以jar包的方式引用静态资源2)、/** 表示类路径下任意资源,要是没有处理则到下列路径下查找文件"classpath:/META-INF/resources/", "classpath:/resources/", "classpath:/static/", "classpath:/public/" "/" //当前项目根路径3)、欢迎页的配置WelcomePageHandlerMapping(TemplateAvailabilityProviders templateAvailabilityProviders, ApplicationContext applicationContext, Optional<Resource> welcomePage, String staticPathPattern) { if (welcomePage.isPresent() && "/**".equals(staticPathPattern)) { logger.info("Adding welcome page: " + welcomePage.get()); this.setRootViewName("forward:index.html");在静态资源路径下的index.html网页访问路径为/**如:http://localhost:8080/基本目录结构如下:3、模板引擎SpringBoot官方推荐Thymeleaf模板引擎1、引入Thymeleaf<!--引入模板引擎--> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-thymeleaf</artifactId> </dependency>使用SpringBoot2.2.5,引入的模板引擎版本为3.0.11RELEASE2、thymeleaf的使用public class ThymeleafProperties { private static final Charset DEFAULT_ENCODING; public static final String DEFAULT_PREFIX = "classpath:/templates/"; public static final String DEFAULT_SUFFIX = ".html"; private boolean checkTemplate = true; private boolean checkTemplateLocation = true; private String prefix = "classpath:/templates/"; private String suffix = ".html"; private String mode = "HTML";文件放在根路径下的templates文件夹下的html文件4、语法规则头部引入若使用Thymeleaf的语法规则,必须在html文件中引入头文件,否则没有提示功能<html xmlns="http://www.w3.org/1999/xhtml" xmlns:th="http://www.thymeleaf.org">基本语法th:text="${}" 可以将作用域中的值取出,并且会对字符进行转义th:utext="${} 也可以取出作用域中的值,但是不会进行转义th:each="user:${users}" 循环遍历,遍历的总对象为大括号中的内容,每一个对象为冒号前不带大括号的内容代码如下:@RequestMapping("/success") public String success(Map<String,Object> map){ map.put("hello","<h1>你好</h1"); map.put("users", Arrays.asList("zhangsan","lisi","wangwu")); return "success"; }<body> <div th:utext="${hello}"></div> <hr /> <!--会对文本中的内容进行转义--> <div th:text="${hello}"></div> <hr> <!--每次遍历都会生成一个所在的标签 --> <div th:text="${user}" th:each="user:${users}" ></div> <hr> <h4 th:each="user:${users}" th:text="${user}"></h4> </body>页面显示:# 你好 ------ <h1>你好</h1> ------ zhangsan lisi wangwu ------ #### zhangsan #### lisi #### wangwu使用Thymeleaf给js函数传参注意:单引号的使用以及转义符号的使用和理解 <!--thymeleaf给js函数传参--> <a th:href="'javascript:a(\''+${msg}+'\')'">点我</a> <script language="JavaScript"> function a(msg) { alert("传过来的消息为:"+msg); } </script>5、SpringMVC自动配置1、MVCAutoConfigurationSpringBoot以为我们提供了大量的自动配置功能,不过我们都可以都过自己的方式向容器中添加组件,来满足自己想要的功能。2、SpringMVC扩展想要扩展配置功能,需要我们自己定义一个配置类,并放入容器中,但是不要添加@EnableWebMvc注解@Configuration public class MyMvcConfig implements WebMvcConfigurer { @Override public void addViewControllers(ViewControllerRegistry registry) { //给指定url访问添加视图解析器,访问同名的静态资源 //registry.addViewController("/success"); //给指定url访问添加视图解析器,访问指定的静态资源 registry.addViewController("/jcq").setViewName("success"); } }3、全面接管SpringMVCSpringBoot给SpringMVC提供的所有配置都不起作用,只有用户自己配置的起作用,相当于,原始的SpringMVC都需要我们手动进行配置。达到这种效果,只需要我们在配置类上添加@EnableWebMvc注解即可。一般不推荐使用。@EnableWebMvc @Configuration public class MyMvcConfig implements WebMvcConfigurer {6、如何修改SpringBoot的默认配置SpringBoot提供自动配置的通常模式:SpringBoot在自动配置很多组件的时候,都会先检查容器中是否已经存在了这个组件,如果没有则按照SpringBoot提供的默认配置进行,如果有则按照用户自己设定的功能执行。如何该组件有多个共同使用(ViewResolver),SpringBoot会通过自己的功能,达到用户与默认的功能共同生效,配合使用。在SpringBoot中会有很多的xxxConfigurer帮助我们进行扩展配置7、RestfulCRUD1、访问默认首页将页面及样式脚本等导入项目中注意:前段页面要导入到templates文件夹中,这样可以使用模板脚本的高级功能普通脚本等静态资源导入到static文件夹中将欢迎页设置为login.html@Configuration public class MyMvcConfig implements WebMvcConfigurer { @Override public void addViewControllers(ViewControllerRegistry registry) { registry.addViewController("/").setViewName("login"); } }在模板中将资源引用使用Thymeleaf语言进行修改,这样可以让路径跟随项目根路径动态改变。th:href="@{/asserts/css/bootstrap.min.css}"' th:src="@{/asserts/img/bootstrap-solid.svg}"2、国际化创建一个基本的属性文件以及分别分别创建不同语言的属性文件可以使用idea使用的快捷界面工具SpringBoot默认的国际化属性文件是根目录下的message.properties文件所以,需要在配置文件中修改默认配置spring.messages.basename=i18n.login 注意: ==只需要写基本配置文件的名字即可,不需要写文件后缀==修改html文件中的文本域内容<img class="mb-4" src="../asserts/img/bootstrap-solid.svg" th:src="@{/asserts/img/bootstrap-solid.svg}" alt="" width="72" height="72"> <h1 class="h3 mb-3 font-weight-normal" th:text="#{login.please}">Please sign in</h1> <label class="sr-only" th:text="#{login.userName}">Username</label> <input type="text" class="form-control" placeholder="Username" th:placeholder="#{login.userName}" required="" autofocus=""> <label class="sr-only" th:text="#{login.passWord}">Password</label> <input type="password" class="form-control" placeholder="Password" th:placeholder="#{login.passWord}" required=""> <div class="checkbox mb-3"> <label> <input type="checkbox" value="remember-me"> [[#{login.remember}]]Thymeleaf的语法为 #{}创建自定义的国际化配置类public class MyLocaleResolver implements LocaleResolver { @Override public Locale resolveLocale(HttpServletRequest request) { //得到url中的语言参数 String len = request.getParameter("l"); System.out.println(len); Locale locale = Locale.getDefault(); if(!StringUtils.isEmpty(len)){ //将得到的参数进行分割 String[] s = len.split("_"); System.out.println(s); locale = new Locale(s[0],s[1]); } return locale; } //逻辑为按照请求url中的参数进行判断将配置类加入SpringBoot容器中 @Bean public LocaleResolver localeResolver(){ System.out.println("执行国际化"); return new MyLocaleResolver(); }注意: ==像SpringBoot容器中注入组件时,方法名严格为返回类名的首字母小写格式,否则需要咋在bean标签中添加说明== //注意这里 @Bean("localeResolver") public LocaleResolver mylocaleResolver(){ System.out.println("执行国际化"); return new MyLocaleResolver(); }3、登录功能实现 首先设置配置文件,取消模板缓存,使用ztrl+F9快捷键,重新编译前台页面表单域中添加name属性,否则无法进行数据提交<input name="username" type="text" class="form-control" placeholder="Username" th:placeholder="#{login.userName}" required="" autofocus=""> <label class="sr-only" th:text="#{login.passWord}">Password</label> <input name="password" type="password" class="form-control" placeholder="Password" th:placeholder="#{login.passWord}" required="">编写控制层代码,进行用户认证@Controller public class LoginController { @PostMapping(value = "/user/login") public String login(@RequestParam("username") String username, @RequestParam("password") String password, Map<String,Object> map,HttpSession session){ //用户登录验证 if(!StringUtils.isEmpty(username) && "123456".equals(password)){ //登录成功 //将数据放入session中 session.setAttribute("loginUser","username"); return "redirect:/main.html"; }else{ //登录失败 map.put("msg","用户名或密码错误"); return "login"; } } }编写拦截器,将未登录用户拦截/** * 拦截器配置 */ public class LoginHandlerInterceptor implements HandlerInterceptor { //进入之前检查,是否已经登录 @Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { Object user = request.getSession().getAttribute("loginUser"); if(user!=null&&!user.equals("")){ //已登录 return true; }else{ //未登录 request.setAttribute("msg","请先进行登录"); request.getRequestDispatcher("/index.html").forward(request,response); return false; } }将拦截器添加到容器 中 //添加拦截器 @Override public void addInterceptors(InterceptorRegistry registry) { //定义拦截url registry.addInterceptor(new LoginHandlerInterceptor()).addPathPatterns("/**") //定义放行url .excludePathPatterns("/","/index.html","/user/login"); }注意: 在controller中可以使用视图解析器,在其他类中,不可以使用视图解析器。 注意视图解析器的使用,作为重点4、CRUD-员工实验普通crud和restful的crud 普通CRUDrestfulCRUD添加addEmpemp{id}---POST修改updEmpemp{id}---PUT查询selEmpemp---GET删除delEmpemp{id}---DELETE实验的请求架构实验功能请求URI请求方式查询所有员工empsGET查询某个员工emp/{id}GET来到添加页面empGET添加员工emp/{id}POST来到修改页面(显示员工)empGET修改员工emp{id}PUT删除员工emp/{id}DELETE5、CRUD-员工列表Thymeleaf提取公共元素定义一个公共片段 <div th:fragment="copy"> © 2011 The Good Thymes Virtual Grocery </div> 在文件中引入该片段 <div th:insert="~{footer :: copy}"></div> 效果 <div id="copy-section"> © 2011 The Good Thymes Virtual Grocery </div> 有三种引入片段的方法:th:insert:直接将元素插入本标签th:replace: 依旧使用原来标签,本标签不做显示th:include:替代掉原有的标签效果<div th:insert="footer :: copy"></div> <div th:replace="footer :: copy"></div> <div th:include="footer :: copy"></div> <div> <footer> © 2011 The Good Thymes Virtual Grocery </footer> </div> <footer> © 2011 The Good Thymes Virtual Grocery </footer> <div> © 2011 The Good Thymes Virtual Grocery </div> 错误:在向模板传值的时候使用的Model也没有导 错包,为什么前台页面接收不到?最后我将Model换成了Map结果前台就接收到了?我竟然把list引起来了? 这个小错我竟然找了一个小时?唉 ,笑笑吧可能还是对idea的不熟练代码放上来打脸@Controller public class EmpController { @Autowired public EmployeeDao employeeDao; @GetMapping("/emps") public String list(Map<String,Object> map,Model model){ Collection<Employee> list = employeeDao.getAll(); // model.addAttribute("emps","list"); map.put("emps",list); return "emp/list"; } }6、CRUD-员工添加引入添加页面<form> <div class="form-group"> <label>LastName</label> <input type="text" class="form-control" placeholder="zhangsan"> </div> <div class="form-group"> <label>Email</label> <input type="email" class="form-control" placeholder="zhangsan@atguigu.com"> </div> <div class="form-group"> <label>Gender</label><br/> <div class="form-check form-check-inline"> <input class="form-check-input" type="radio" name="gender" value="1"> <label class="form-check-label">男</label> </div> <div class="form-check form-check-inline"> <input class="form-check-input" type="radio" name="gender" value="0"> <label class="form-check-label">女</label> </div> </div> <div class="form-group"> <label>department</label> <select class="form-control"> <option>1</option> <option>2</option> <option>3</option> <option>4</option> <option>5</option> </select> </div> <div class="form-group"> <label>Birth</label> <input type="text" class="form-control" placeholder="zhangsan"> </div> <button type="submit" class="btn btn-primary">添加</button> </form>错误:日期格式化错误,可在全局配置文件中做修改#设置日期格式化 spring.mvc.date-format=yyyy-MM-dd7、员工修改put方式请求的发送:在SpringBoot默认没有开启put请求接收,需要修改全局配置文件#表单提交方式 spring.mvc.hiddenmethod.filter.enabled=true在表单域中使用hidden属性,name为“_method” value="put " put不区分大小写<!--如果emp不为空,发送put请求--> <input type="hidden" name="_method" value="put" th:if="${emp!=null}"/> <input type="hidden" name="id" th:value="${emp.id}" th:if="${emp!=null}" />控制器使用@PutMapping注解接收put请求@PutMapping("/emp") public String updEmp(Employee employee){ System.out.println("接收到的修改请求:"+employee); employeeDao.save(employee); return "redirect:/emps"; }8、员工删除注意delete请求的发送以及,js代码的编写语法//表单 <form id="delete_form" method="post"> <input type="hidden" name="_method" value="delete" /> </form> //标签按钮 <button th:attr="delUri=@{/emp/}+${emp.id}" class="btn btn-sm btn-danger deleteBtn">删除</button>js代码<!--删除事件--> <script> $(".deleteBtn").click(function () { $("#delete_form").attr("action",$(this).attr("delUri")).submit(); return false; }) </script>8、错误处理机制1、SpringBoot默认的错误处理1、如何定制错误页面有模板引擎的情况:有模板引擎的话,会找模板引擎下的error目录下的4XX或5XX文件。也可以获取到错误消息:timestemp:时间戳。status:状态码error:错误提示exception:错误对象message:异常消息errors:JSR303数据校验错误信息都在这里没有模板的情况:没有模板引擎的话,会到静态资源也就是static文件夹下寻找,但是无法获取到页面相应信息。即没有模板引擎,也没有配静态资源。使用SpringBoot默认的错误信息页面。2、如何定制json数据9、使用外部servlet容器使用版本为,SpringBoot2.X Tomcat9之前使用Tomcat7会有错误问题五、Docker1、安装虚拟机2、安装Docker1、内核必须为3.10以上 uname -r 2、安装docker yum install docker 3、输入y正确安装 4、启动systemctl docker start 出现版本号表示正常安装完成 [root@localhost ~]# systemctl start docker [root@localhost ~]# docker -v Docker version 1.13.1, build cccb291/1.13.1 5、开机自启 [root@localhost ~]# systemctl enable docker Created symlink from /etc/systemd/system/multi-user.target.wants/docker.service to /usr/lib/systemd/system/docker.service. 6、停止docker [root@localhost ~]# systemctl stop docker 7、查询镜像 [root@localhost ~]# docker search mysql 3、Docker常用操作1、使用镜像加速器# 创建并编辑文件 vi /etc/docker/daemon.json # 编辑文件内容 { "registry-mirrors":["https://6kx4zyno.mirror.aliyuncs.com"] } # 重启服务 systemctl daemon-reload systemctl restart docker.service 重启docker service docker restart 验证 # docker info Registry Mirrors: https://xxxxxx.mirror.aliyuncs.com 出现错误:Error response from daemon: Get https://index.docker.io/v1/search?q=restart&n=25: net/http: TLS handshake timeout 解决方案:vim /etc/resolv.conf把里面的内容注释,并改为:nameserver 8.8.8.8 nameserver 8.8.8.4重启网络服务systemctl restart network2、容器操作步骤1、搜索镜像 docker search tomcat 2、拉取镜像 docker pull tomcat 3、启动一个做了端口映射的tomcat docker run -d -p 8888:8080 tomcat -d 后台运行 -p 将主机端口映射到容器内部端口 4为了使用简单,关闭了linux的防火墙 查看防火墙状态 service firewalld status 5关闭防火墙 service firewalld stop 6查看日志内容 docker logs container-name/container-id (容器id) 启动容器中的任务:docker start id3、安装MySql1、搜索MySql docker pull mysql 2、正确启动 docker run --name mysql01 -e MYSQL_ROOT_PASSWORD=123456 -d mysql:tag做了端口映射的启动docker run -p 3306:3306 --name mysql02 -e MYSQL_ROOT_PASSWORD=123456 -d mysql:5.7 其他高级的操作docker run -p 3306:3306 --name mysql02 -e MYSQL_ROOT_PASSWORD=123456 -d mysql:5.7 --character-set-server=utf8mb4 --collation-server=utf8mb4_unicode_ci 指定mysql的一些参数六、SpringBoot与数据访问1、JDBC创建项目 <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-jdbc</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <dependency> <groupId>mysql</groupId> <artifactId>mysql-connector-java</artifactId> <scope>runtime</scope> </dependency> <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>配置文件spring: datasource: //注意:用户名密码前面没有前缀 username: root password: 123456 url: jdbc:mysql://192.168.91.128:3307/jdbc driver-class-name: com.mysql.cj.jdbc.Driver还可以在配置文件中配置要执行的sql脚本文件schema: - classpath:department.sql //必须要配置这段话,否则会报错 initialization-mode: alwaysSpring封装sql的类,以前没有过用过JdbcTemplate@Controller public class HelloController { @Autowired private JdbcTemplate jdbcTemplate; @RequestMapping("/query") @ResponseBody public Map<String,Object> Query(){ //查询出一个集合 List<Map<String, Object>> list = jdbcTemplate.queryForList("select * from department"); return list.get(0); } }2、整合Druid数据源1、配置文件spring: datasource: # 数据源基本配置 username: root password: 123456 driver-class-name: com.mysql.cj.jdbc.Driver url: jdbc:mysql://192.168.91.128:3307/mybatis type: com.alibaba.druid.pool.DruidDataSource # 数据源其他配置 initialSize: 5 minIdle: 5 maxActive: 20 maxWait: 60000 timeBetweenEvictionRunsMillis: 60000 minEvictableIdleTimeMillis: 300000 validationQuery: SELECT 1 FROM DUAL testWhileIdle: true testOnBorrow: false testOnReturn: false poolPreparedStatements: true # 配置监控统计拦截的filters,去掉后监控界面sql无法统计,'wall'用于防火墙 filters: stat,wall,slf4j maxPoolPreparedStatementPerConnectionSize: 20 useGlobalDataSourceStat: true connectionProperties: druid.stat.mergeSql=true;druid.stat.slowSqlMillis=500 2、引入数据源@Configuration public class DruidConfig { @ConfigurationProperties(prefix = "spring.datasource") @Bean public DataSource druid(){ return new DruidDataSource(); } //配置druid监控 //配置一个管理后台的Servlet @Bean public ServletRegistrationBean statViewServlet(){ ServletRegistrationBean bean = new ServletRegistrationBean(new StatViewServlet(),"/druid/*"); Map<String,String> initParams = new HashMap<>(); initParams.put("loginUsername","admin"); initParams.put("loginPassword","123456"); initParams.put("allow","");//允许所有访问 //拒绝谁访问 // initParams.put("deny",""); bean.setInitParameters(initParams); return bean; } //配置一个web监控的filter @Bean public FilterRegistrationBean webStatFilter(){ FilterRegistrationBean bean = new FilterRegistrationBean(); bean.setFilter(new WebStatFilter()); Map<String,String> initParams = new HashMap<>(); initParams.put("exclusions","*.js,*.css,/druid/*"); bean.setInitParameters(initParams); bean.setUrlPatterns(Arrays.asList("/*")); return bean; } }3、整合MyBatis1、pom.xml <dependency> <groupId>org.mybatis.spring.boot</groupId> <artifactId>mybatis-spring-boot-starter</artifactId> <version>2.1.1</version> </dependency>步骤:引入Druid数据源给数据库建表创建实体类2、注解版Mapper文件@Mapper public interface DepartmentMapper { //使用主键自增,keyProperty属性为,实体类的属性 @Options(useGeneratedKeys = true,keyProperty = "id") @Insert("insert into department (departmentName) values (#{departmentName})") int insDept(Department department); @Delete("delete from department where id = #{id}") int delDept(Integer id); @Update("update department set departmentName=#{departmentName} where id = #{id}") int updDept(Department department); @Select("select * from department where id = #{id}") Department selDept(Integer id); } Controller层代码@RestController public class DeptController { @Resource DepartmentMapper departmentMapper; @RequestMapping("/insert") public Department insert(Department department){ departmentMapper.insDept(department); return department; } @RequestMapping("/delete/{id}") public Department delete(@PathVariable Integer id){ departmentMapper.delDept(id); return null; } @RequestMapping("/update") public Department update(Department department){ departmentMapper.updDept(department); return null; } @RequestMapping("/select/{id}") public Department select(@PathVariable Integer id){ return departmentMapper.selDept(id); } }开启驼峰匹配原则@org.springframework.context.annotation.Configuration public class MyBatisConfig { //配置驼峰解析原则 @Bean public ConfigurationCustomizer configurationCustomizer(){ return new ConfigurationCustomizer() { @Override public void customize(Configuration configuration) { System.out.println("来了吗"); configuration.setMapUnderscoreToCamelCase(true); } }; } } 3、配置文件版MyBatis配置文件:mybatis.configuration.map-underscore-to-camel-case=true mybatis.mapper-locations=classpath:mapper/*Mapper.xml<?xml version="1.0" encoding="UTF-8" ?> <!DOCTYPE configuration PUBLIC "-//mybatis.org//DTD Config 3.0//EN" "http://mybatis.org/dtd/mybatis-3-config.dtd"> <configuration> <!--设置驼峰匹配规则--> <settings> <setting name="mapUnderscoreToCamelCase" value="true"/> </settings> </configuration>Mapper映射文件xml:<?xml version="1.0" encoding="UTF-8" ?> <!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd"> <mapper namespace="com.jcq.springboot.mapper.EmployeeMapper"> <!-- 命名空间 --> <!-- 对象映射,可以不写 --> <!-- 查询功能,resultType 设置返回值类型 --> <select id="selById" resultType="com.jcq.springboot.bean.Employee"> <!-- 书写 SQL 语句 --> SELECT * FROM employee where id=#{id} </select> </mapper>其他的用法跟正常使用MyBatis相同4、整合JPA1、配置文件#配置数据源 spring: datasource: url: jdbc:mysql://192.168.91.128:3307/jpa username: root password: 123456 driver-class-name: com.mysql.cj.jdbc.Driver jpa: hibernate: #自动建表 ddl-auto: update #显示Sql语句 show-sql: true2、实体类@Entity @Table(name = "user") //如果通过id直接获取对象需要开放这个注解,否则会报懒加载错误 //@JsonIgnoreProperties(value = {"hibernateLazyInitializer","handler"}) public class User implements Serializable { @Id //这是一个主键 @GeneratedValue(strategy = GenerationType.IDENTITY) //主键自增策略 private Integer id; @Column private String username; @Column private String email; //getter and setter .....3、Repository类只需定义一个借口并继承JpaRepository即可public interface UserRepository extends JpaRepository<User,Integer> { }4、Controller层@RestController public class UserController { @Autowired private UserRepository userRepository; @GetMapping("/user/{id}") public User getUser(@PathVariable Integer id){ //User user = userRepository.findById(id).get(); User user = userRepository.getOne(id); return user } }七、自定义启动器步骤:创建空项目1 启动器模块1.2 自动配置模块启动器模块只需依赖自动配置模块即可 <dependencies> <!--引入自动配置模块--> <dependency> <groupId>com.jcq.starter</groupId> <artifactId>jcq-spring-boot-starter-autoconfigurer</artifactId> <version>0.0.1-SNAPSHOT</version> </dependency> </dependencies>自动配置模块项目目录如下在自动配置模块中添加业务方法public class HelloService { HelloProperties helloProperties; public HelloProperties getHelloProperties() { return helloProperties; } public void setHelloProperties(HelloProperties helloProperties) { this.helloProperties = helloProperties; } public String sayHello(String name){ return helloProperties.getPrefix()+name+helloProperties.getSuffix(); } } 编写xxxProperties类//注解指明,当有项目使用启动器时,配置文件的前缀 @ConfigurationProperties(prefix = "jcq.hello") public class HelloProperties { private String prefix; private String suffix; public String getPrefix() { return prefix; } public void setPrefix(String prefix) { this.prefix = prefix; } public String getSuffix() { return suffix; } public void setSuffix(String suffix) { this.suffix = suffix; } }编写自动配置类//表明是一个自动配置类 @Configuration //引用xxxProperties类中的数据 @EnableConfigurationProperties(HelloProperties.class) //只有引用的项目是Web项目是生效 @ConditionalOnWebApplication public class HelloAutoConfiguration { @Autowired HelloProperties helloProperties; //将业务类注入到容器中 @Bean public HelloService helloService(){ HelloService helloService = new HelloService(); helloService.setHelloProperties(helloProperties); return helloService; } }在类路径下创建WEB/INF文件夹,在该路径下创建spring.factories文件# Auto Configure org.springframework.boot.autoconfigure.EnableAutoConfiguration=\ com.jcq.starter.HelloAutoConfiguration将项目安装到仓库中其他项目使用此启动器,只需在pom.xml中添加依赖即可 <!--引入自定义启动器--> <dependency> <groupId>com.jcq</groupId> <artifactId>jcq-spring-boot-starter</artifactId> <version>1.0-SNAPSHOT</version> </dependency>这样便可使用启动器中的自动配置类的功能
2020年04月06日
9 阅读
0 评论
0 点赞
1
2