首页
壁纸
留言板
友链
更多
统计归档
Search
1
TensorBoard:训练日志及网络结构可视化工具
12,437 阅读
2
主板开机跳线接线图【F_PANEL接线图】
4,996 阅读
3
海康威视监控摄像头使用指南
2,245 阅读
4
Pytorch 实战:卷积神经网络(CNN)实现MINIST手写数字识别
2,209 阅读
5
Linux使用V2Ray 原生客户端
2,178 阅读
好物分享
实用教程
linux使用
wincmd
学习笔记
mysql
java学习
nginx
综合面试题
大数据
网络知识
放码过来
python
javascript
java
opencv
蓝桥杯
leetcode
深度学习
开源模型
相关知识
数据集和工具
模型轻量化
语音识别
计算机视觉
杂七杂八
硬件科普
主机安全
嵌入式设备
其它
bug处理
登录
/
注册
Search
标签搜索
好物分享
学习笔记
linux
MySQL
nvidia
typero
内网穿透
webdav
vps
java
cudann
gcc
cuda
树莓派
v2ray
CNN
图像去雾
ssh安全
nps
暗通道先验
jupiter
累计撰写
324
篇文章
累计收到
55
条评论
首页
栏目
好物分享
实用教程
linux使用
wincmd
学习笔记
mysql
java学习
nginx
综合面试题
大数据
网络知识
放码过来
python
javascript
java
opencv
蓝桥杯
leetcode
深度学习
开源模型
相关知识
数据集和工具
模型轻量化
语音识别
计算机视觉
杂七杂八
硬件科普
主机安全
嵌入式设备
其它
bug处理
页面
壁纸
留言板
友链
统计归档
搜索到
25
篇与
的结果
2023-11-03
SpringBoot集成Redisson延迟队列
0. 使用场景下单成功,30分钟未支付。支付超时,自动取消订单订单签收,签收后7天未进行评价。订单超时未评价,系统默认好评下单成功,商家5分钟未接单,订单取消配送超时,推送短信提醒1.Redisson延迟队列原理redisson 使用了 两个list + 一个 sorted-set + pub/sub 来实现延时队列,而不是单一的sort-set。sorted-set:存放未到期的消息&到期时间,提供消息延时排序功能list1:存放未到期消息,作为消息的原始顺序视图,提供如查询、删除指定第几条消息的功能(分析源码得出的,查看哪些地方有使用这个list)list2:消费队列,存放到期后的消息,提供消费整体流程(对应画图PPT链接): 结合源码分析:org.redisson.RedissonDelayedQueue#RedissonDelayedQueue 首先创建延时队列的时候,会创建一个QueueTransferTask, 在里面会订阅一个topic,订阅成功后,执行其pushTask方法,里面会查询sorted-set中100个已到期的消息,将其push到lis2中,并从sorted-set和list1中移除。(这里是为了投递历史未处理的消息)protected RedissonDelayedQueue(QueueTransferService queueTransferService, Codec codec, final CommandAsyncExecutor commandExecutor, String name) { super(codec, commandExecutor, name); channelName = prefixName("redisson_delay_queue_channel", getRawName()); queueName = prefixName("redisson_delay_queue", getRawName()); timeoutSetName = prefixName("redisson_delay_queue_timeout", getRawName()); QueueTransferTask task = new QueueTransferTask(commandExecutor.getConnectionManager()) { @Override protected RFuture<Long> pushTaskAsync() { return commandExecutor.evalWriteAsync(getRawName(), LongCodec.INSTANCE, RedisCommands.EVAL_LONG, "local expiredValues = redis.call('zrangebyscore', KEYS[2], 0, ARGV[1], 'limit', 0, ARGV[2]); " + "if #expiredValues > 0 then " + "for i, v in ipairs(expiredValues) do " + "local randomId, value = struct.unpack('dLc0', v);" + "redis.call('rpush', KEYS[1], value);" + "redis.call('lrem', KEYS[3], 1, v);" + "end; " + "redis.call('zrem', KEYS[2], unpack(expiredValues));" + "end; " // get startTime from scheduler queue head task + "local v = redis.call('zrange', KEYS[2], 0, 0, 'WITHSCORES'); " + "if v[1] ~= nil then " + "return v[2]; " + "end " + "return nil;", Arrays.<Object>asList(getRawName(), timeoutSetName, queueName), System.currentTimeMillis(), 100); } @Override protected RTopic getTopic() { return RedissonTopic.createRaw(LongCodec.INSTANCE, commandExecutor, channelName); } }; queueTransferService.schedule(queueName, task); this.queueTransferService = queueTransferService; } org.redisson.RedissonDelayedQueue#offerAsync(V, long, java.util.concurrent.TimeUnit) 发送延时消息时,会将消息写入 list1和 sorted-set 中,msg会添加一个randomId,支持发送相同的消息。并且判断sorted-set首条消息如果是刚插入的,则publish timeout(到期时间) 到 topicpublic RFuture<Void> offerAsync(V e, long delay, TimeUnit timeUnit) { if (delay < 0) { throw new IllegalArgumentException("Delay can't be negative"); } long delayInMs = timeUnit.toMillis(delay); long timeout = System.currentTimeMillis() + delayInMs; long randomId = ThreadLocalRandom.current().nextLong(); return commandExecutor.evalWriteAsync(getRawName(), codec, RedisCommands.EVAL_VOID, "local value = struct.pack('dLc0', tonumber(ARGV[2]), string.len(ARGV[3]), ARGV[3]);" + "redis.call('zadd', KEYS[2], ARGV[1], value);" + "redis.call('rpush', KEYS[3], value);" // if new object added to queue head when publish its startTime // to all scheduler workers + "local v = redis.call('zrange', KEYS[2], 0, 0); " + "if v[1] == value then " + "redis.call('publish', KEYS[4], ARGV[1]); " + "end;", Arrays.<Object>asList(getRawName(), timeoutSetName, queueName, channelName), timeout, randomId, encode(e)); }org.redisson.QueueTransferTask#scheduleTask 订阅到topic消息后,会先判断其是否临期(delay<10ms),如果是则调用pushTask方法,不是则启动一个定时任务(使用的netty时间轮),延时delay后执行pushTask方法。// 订阅topic onMessage 时调用 private void scheduleTask(final Long startTime) { TimeoutTask oldTimeout = lastTimeout.get(); if (startTime == null) { return; } if (oldTimeout != null) { oldTimeout.getTask().cancel(); } long delay = startTime - System.currentTimeMillis(); if (delay > 10) { // 使用 netty 时间轮 启动一个定时任务 Timeout timeout = connectionManager.newTimeout(new TimerTask() { @Override public void run(Timeout timeout) throws Exception { pushTask(); TimeoutTask currentTimeout = lastTimeout.get(); if (currentTimeout.getTask() == timeout) { lastTimeout.compareAndSet(currentTimeout, null); } } }, delay, TimeUnit.MILLISECONDS); if (!lastTimeout.compareAndSet(oldTimeout, new TimeoutTask(startTime, timeout))) { timeout.cancel(); } } else { pushTask(); } } private void pushTask() { RFuture<Long> startTimeFuture = pushTaskAsync(); startTimeFuture.onComplete((res, e) -> { if (e != null) { if (e instanceof RedissonShutdownException) { return; } log.error(e.getMessage(), e); scheduleTask(System.currentTimeMillis() + 5 * 1000L); return; } if (res != null) { scheduleTask(res); } }); }2.SpringBoot集成实验环境:SpringBoot版本3.0.12<parent> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-parent</artifactId> <version>3.0.12</version> <relativePath/> <!-- lookup parent from repository --> </parent>2.1 引入 Redisson 依赖 <!--redission--> <dependency> <groupId>org.redisson</groupId> <artifactId>redisson-spring-boot-starter</artifactId> <version>3.19.0</version> </dependency>2.2 配置文件spring: data: redis: host: 172.19.236.66 port: 6379 #password: 123456 database: 0 timeout: 30002.3 创建 RedissonConfig 配置package com.example.redissionstudy.config; import org.redisson.Redisson; import org.redisson.api.RedissonClient; import org.redisson.config.Config; import org.springframework.beans.factory.annotation.Value; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; /** * @author LuoJia * @version 1.0 * @description: Redis链接配置文件 * @date 2023/11/3 8:59 */ @Configuration public class RedissonConfig { @Value("${spring.data.redis.host}") private String host; @Value("${spring.data.redis.port}") private int port; @Value("${spring.data.redis.database}") private int database; //@Value("${spring.data.redis.password}") //private String password; @Bean public RedissonClient redissonClient() { Config config = new Config(); config.useSingleServer() .setAddress("redis://" + host + ":" + port) .setDatabase(database); //.setPassword(password) return Redisson.create(config); } }测试使用@SpringBootTest @Slf4j class RedissionStudyApplicationTests { @Resource RedissonClient redissonClient; @Test void testRedission() { //字符串操作 RBucket<String> rBucket = redissonClient.getBucket("strKey"); // 设置value和key的有效期 rBucket.set("张三", 30, TimeUnit.MINUTES); // 通过key获取value System.out.println(redissonClient.getBucket("strKey").get()); } }张三redis查看结果127.0.0.1:6379> keys str* 1) "strKey" 127.0.0.1:6379> get strKey "\x03\x83\xe5\xbc\xa0\xe4\xb8\x89"2.4 封装 Redis 延迟队列工具类package com.example.redissionstudy.utils; import lombok.extern.slf4j.Slf4j; import org.redisson.api.RBlockingDeque; import org.redisson.api.RDelayedQueue; import org.redisson.api.RedissonClient; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Component; import java.util.Map; import java.util.concurrent.TimeUnit; /** * @author LuoJia * @version 1.0 * @description: Redission 延迟队列工具类 * @date 2023/11/3 9:51 */ @Slf4j @Component public class RedisDelayQueueUtil { @Autowired private RedissonClient redissonClient; /** * 添加延迟队列 * * @param value 队列值 * @param delay 延迟时间 * @param timeUnit 时间单位 * @param queueCode 队列键 * @param <T> */ public <T> void addDelayQueue(T value, long delay, TimeUnit timeUnit, String queueCode) { try { RBlockingDeque<Object> blockingDeque = redissonClient.getBlockingDeque(queueCode); RDelayedQueue<Object> delayedQueue = redissonClient.getDelayedQueue(blockingDeque); delayedQueue.offer(value, delay, timeUnit); log.info("(添加延时队列成功) 队列键:{},队列值:{},延迟时间:{}", queueCode, value, timeUnit.toSeconds(delay) + "秒"); } catch (Exception e) { log.error("(添加延时队列失败) {}", e.getMessage()); throw new RuntimeException("(添加延时队列失败)"); } } /** * 获取延迟队列 * * @param queueCode * @param <T> * @return * @throws InterruptedException */ public <T> T getDelayQueue(String queueCode) throws InterruptedException { RBlockingDeque<Map> blockingDeque = redissonClient.getBlockingDeque(queueCode); T value = (T) blockingDeque.take(); return value; } }2.5 创建延迟队列业务枚举package com.example.redissionstudy.enums; import lombok.AllArgsConstructor; import lombok.Getter; import lombok.NoArgsConstructor; /** * @author LuoJia * @version 1.0 * @description: 延迟队列业务枚举 * @date 2023/11/3 9:53 */ @Getter @AllArgsConstructor @NoArgsConstructor public enum RedisDelayQueueEnum { ORDER_PAYMENT_TIMEOUT("ORDER_PAYMENT_TIMEOUT", "订单支付超时,自动取消订单", "orderPaymentTimeout"), ORDER_TIMEOUT_NOT_EVALUATED("ORDER_TIMEOUT_NOT_EVALUATED", "订单超时未评价,系统默认好评", "orderTimeoutNotEvaluated"); /** * 延迟队列 RedisKey */ private String code; /** * 中文描述 */ private String name; /** * 延迟队列具体业务实现的 Bean * 可通过 Spring 的上下文获取 */ private String beanId; }2.6 定义延迟队列执行器package com.example.redissionstudy.handler; /** * @author LuoJia * @version 1.0 * @description: 延迟队列执行器接口 * @date 2023/11/3 9:58 */ public interface RedisDelayQueueHandle<T>{ void execute(T t); }2.7 创建枚举中定义的Bean,并实现延迟队列执行器OrderPaymentTimeout:订单支付超时延迟队列处理类package com.example.redissionstudy.handler.impl; import com.example.redissionstudy.enums.RedisDelayQueueEnum; import com.example.redissionstudy.handler.RedisDelayQueueHandle; import lombok.extern.slf4j.Slf4j; import org.springframework.stereotype.Component; import java.util.Map; /** * @author LuoJia * @version 1.0 * @description: 订单支付超时处理类 * @date 2023/11/3 10:00 */ @Component @Slf4j public class OrderPaymentTimeout implements RedisDelayQueueHandle<Map> { @Override public void execute(Map map) { log.info("{} {}", RedisDelayQueueEnum.ORDER_PAYMENT_TIMEOUT.getName(), map); // TODO 订单支付超时,自动取消订单处理业务... } } OrderTimeoutNotEvaluated:订单超时未评价延迟队列处理类package com.example.redissionstudy.handler.impl; import com.example.redissionstudy.enums.RedisDelayQueueEnum; import com.example.redissionstudy.handler.RedisDelayQueueHandle; import lombok.extern.slf4j.Slf4j; import org.springframework.stereotype.Component; import java.util.Map; /** * @author LuoJia * @version 1.0 * @description: 订单超时未评价处理类 * @date 2023/11/3 10:01 */ @Component @Slf4j public class OrderTimeoutNotEvaluated implements RedisDelayQueueHandle<Map> { @Override public void execute(Map map) { log.info("{} {}", RedisDelayQueueEnum.ORDER_TIMEOUT_NOT_EVALUATED.getName(), map); // TODO 订单超时未评价,系统默认好评处理业务... } }2.8 创建延迟队列消费线程,项目启动完成后开启package listener; import com.example.redissionstudy.enums.RedisDelayQueueEnum; import com.example.redissionstudy.handler.RedisDelayQueueHandle; import com.example.redissionstudy.utils.RedisDelayQueueUtil; import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.CommandLineRunner; import org.springframework.context.ApplicationContext; import org.springframework.stereotype.Component; /** * @author LuoJia * @version 1.0 * @description: 启动延迟队列 * @date 2023/11/3 10:02 */ @Slf4j @Component public class RedisDelayQueueRunner implements CommandLineRunner { @Autowired private RedisDelayQueueUtil redisDelayQueueUtil; @Autowired private ApplicationContext applicationContext; @Override public void run(String... args) { new Thread(() -> { while (true) { try { RedisDelayQueueEnum[] queueEnums = RedisDelayQueueEnum.values(); for (RedisDelayQueueEnum queueEnum : queueEnums) { Object value = redisDelayQueueUtil.getDelayQueue(queueEnum.getCode()); if (value != null) { RedisDelayQueueHandle redisDelayQueueHandle = (RedisDelayQueueHandle) applicationContext.getBean(queueEnum.getBeanId()); redisDelayQueueHandle.execute(value); } } } catch (InterruptedException e) { log.error("(Redis延迟队列异常中断) {}", e.getMessage()); } } }).start(); log.info("(Redis延迟队列启动成功)"); } }以上步骤,Redis 延迟队列核心代码已经完成,下面我们写一个测试接口,用 PostMan 模拟测试一下2.9 创建一个测试接口,模拟添加延迟队列package com.example.redissionstudy.controller; /** * @author LuoJia * @version 1.0 * @description: 延迟队列测试 * @date 2023/11/3 10:05 */ import com.example.redissionstudy.enums.RedisDelayQueueEnum; import com.example.redissionstudy.utils.RedisDelayQueueUtil; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.RestController; import java.util.HashMap; import java.util.Map; import java.util.concurrent.TimeUnit; @RestController public class RedisDelayQueueController { @Autowired private RedisDelayQueueUtil redisDelayQueueUtil; @GetMapping("/addQueue") public void addQueue() { Map<String, String> map1 = new HashMap<>(); map1.put("orderId", "100"); map1.put("remark", "其他信息"); Map<String, String> map2 = new HashMap<>(); map2.put("orderId", "200"); map2.put("remark", "其他信息"); // 添加订单支付超时,自动取消订单延迟队列。为了测试效果,延迟10秒钟 redisDelayQueueUtil.addDelayQueue(map1, 10, TimeUnit.SECONDS, RedisDelayQueueEnum.ORDER_PAYMENT_TIMEOUT.getCode()); // 订单超时未评价,系统默认好评。为了测试效果,延迟20秒钟 redisDelayQueueUtil.addDelayQueue(map2, 20, TimeUnit.SECONDS, RedisDelayQueueEnum.ORDER_TIMEOUT_NOT_EVALUATED.getCode()); } }运行结果2023-11-03T10:09:46.800+08:00 INFO 21480 --- [ main] o.s.b.w.embedded.tomcat.TomcatWebServer : Tomcat started on port(s): 8080 (http) with context path '' 2023-11-03T10:09:46.816+08:00 INFO 21480 --- [ main] c.e.r.RedissionStudyApplication : Started RedissionStudyApplication in 4.888 seconds (process running for 5.743) 2023-11-03T10:09:46.825+08:00 INFO 21480 --- [ main] c.e.r.listener.RedisDelayQueueRunner : (Redis延迟队列启动成功) 2023-11-03T10:09:47.039+08:00 INFO 21480 --- [-10.108.155.252] o.a.c.c.C.[Tomcat].[localhost].[/] : Initializing Spring DispatcherServlet 'dispatcherServlet' 2023-11-03T10:09:47.040+08:00 INFO 21480 --- [-10.108.155.252] o.s.web.servlet.DispatcherServlet : Initializing Servlet 'dispatcherServlet' 2023-11-03T10:09:47.042+08:00 INFO 21480 --- [-10.108.155.252] o.s.web.servlet.DispatcherServlet : Completed initialization in 2 ms 2023-11-03T10:10:25.798+08:00 INFO 21480 --- [nio-8080-exec-4] c.e.r.utils.RedisDelayQueueUtil : (添加延时队列成功) 队列键:ORDER_PAYMENT_TIMEOUT,队列值:{orderId=100, remark=其他信息},延迟时间:10秒 2023-11-03T10:10:25.802+08:00 INFO 21480 --- [nio-8080-exec-4] c.e.r.utils.RedisDelayQueueUtil : (添加延时队列成功) 队列键:ORDER_TIMEOUT_NOT_EVALUATED,队列值:{orderId=200, remark=其他信息},延迟时间:20秒 2023-11-03T10:10:35.779+08:00 INFO 21480 --- [ Thread-2] c.e.r.handler.impl.OrderPaymentTimeout : 订单支付超时,自动取消订单 {orderId=100, remark=其他信息} 2023-11-03T10:10:45.860+08:00 INFO 21480 --- [ Thread-2] c.e.r.h.impl.OrderTimeoutNotEvaluated : 订单超时未评价,系统默认好评 {orderId=200, remark=其他信息}参考资料SpringBoot集成Redisson实现延迟队列 - 掘金 (juejin.cn)SpringBoot集成Redisson实现延迟队列_redssion延时队列订阅_刘鹏博.的博客-CSDN博客Maven Repository: org.redisson » redisson-spring-boot-starter (mvnrepository.com)【进阶篇】Redis实战之Redisson使用技巧详解 - 知乎 (zhihu.com)Table of Content · redisson/redisson Wiki · GitHub浅析 Redisson 的分布式延时队列 RedissonDelayedQueue 运行流程 - 掘金 (juejin.cn)Redisson分布式延时队列 RedissonDelayedQueue - 掘金 (juejin.cn)
2023年11月03日
7 阅读
0 评论
0 点赞
2023-10-12
Java Stream学习笔记
1.Stream介绍1.1 概述什么是Stream?java 8 新增的Stream配合同版本出现的 Lambda ,给我们操作集合(Collection)提供了极大的便利。Stream将要处理的元素集合看作一种流,在流的过程中,借助Stream API对流中的元素进行操作,比如:筛选、排序、聚合等。Stream对流的操作分类中间操作,每次返回一个新的流,可以有多个。(筛选filter、映射map、排序sorted、去重组合skip—limit)终端操作,每个流只能进行一次终端操作,终端操作结束后流无法再次使用。终端操作会产生一个新的集合或值。(遍历foreach、匹配find–match、规约reduce、聚合max–min–count、收集collect)Stream特性stream不存储数据,而是按照特定的规则对数据进行计算,一般会输出结果。stream不会改变数据源,通常情况下会产生一个新的集合或一个值。stream具有延迟执行特性,只有调用终端操作时,中间操作才会执行。1.2 Stream与传统遍历对比几乎所有的集合(如 Collection 接口或 Map 接口等)都支持直接或间接的遍历操作。而当我们需要对集合中的元素进行操作的时候,除了必需的添加、删除、获取外,最典型的就是集合遍历。例如现有一个需求:将list集合中姓张的元素过滤到一个新的集合中,然后将过滤出来的姓张的元素中,再过滤出来长度为3的元素,存储到一个新的集合中传统遍历import java.util.ArrayList; import java.util.List; public class Demo1List { public static void main(String[] args) { List<String> list = new ArrayList<>(); list.add("张无忌"); list.add("周芷若"); list.add("赵敏"); list.add("小昭"); list.add("殷离"); list.add("张三"); list.add("张三丰"); List<String> listA = new ArrayList<>(); for ( String s : list) { if (s.startsWith("张")) listA.add(s); } List<String> listB = new ArrayList<>(); for (String s: listA) { if (s.length() == 3) listB.add(s); } for (String s: listB) { System.out.println(s); } } }使用Stream写法import java.util.ArrayList; import java.util.List; public class Demo2Steam { public static void main(String[] args) { List<String> list = new ArrayList<>(); list.add("张无忌"); list.add("周芷若"); list.add("赵敏"); list.add("小昭"); list.add("殷离"); list.add("张三"); list.add("张三丰"); list.stream() .filter(name -> name.startsWith("张")) .filter(name -> name.length() == 3) .forEach(name -> System.out.println(name)); } }2.Stream的创建2.0 顺序流和并行流stream和parallelStream的简单区分: stream是顺序流,由主线程按顺序对流执行操作,而parallelStream是并行流,内部以多线程并行执行的方式对流进行操作,但前提是流中的数据处理没有顺序要求。例如筛选集合中的奇数,两者的处理不同之处:如果流中的数据量足够大,并行流可以加快处速度。除了直接创建并行流,还可以通过parallel()把顺序流转换成并行流2.1 通过 java.util.Collection.stream() 方法用集合创建流@Test public void testCreateStream(){ List<String> list = Arrays.asList("a", "b", "c"); // 创建一个顺序流 Stream<String> stream = list.stream(); System.out.println("创建顺序流:stream"); stream.forEach(x-> System.out.println(x)); // 创建一个并行流 Stream<String> parallelStream = list.parallelStream(); System.out.println("创建并行流:parallelStream"); parallelStream.forEach(x-> System.out.println(x)); }创建顺序流:stream a b c 创建并行流:parallelStream b c a2.2 使用java.util.Arrays.stream(T[] array)方法用数组创建流@Test public void testCreateStream(){ int[] array1={1,3,5,6,8}; IntStream stream1 = Arrays.stream(array1); stream1.forEach(x-> System.out.println(x)); String[] array2={"1","3","5","6","8"}; Stream<String> stream2 = Arrays.stream(array2); stream2.forEach(x-> System.out.println(x)); }2.3 使用Stream的静态方法:of()、iterate()、generate()@Test public void testCreateStream(){ Stream<Integer> stream1 = Stream.of(1, 2, 3); stream1.forEach(System.out::println); Stream<Integer> stream2 = Stream.iterate(0, (x) -> x + 3).limit(3); stream2.forEach(System.out::println); Stream<Double> stream3 = Stream.generate(Math::random).limit(3); stream3.forEach(System.out::println); }3.Stream使用/Stream流的常用方法在使用stream之前,先理解一个概念:Optional 。Optional类是一个可以为null的容器对象。如果值存在则isPresent()方法会返回true,调用get()方法会返回该对象。Stream流的常用方法分类:终结方法:返回值类型不再是Stream接口本身类型的方法,例如:forEach方法和count方法非终结方法/延迟方法:返回值类型仍然是Stream接口自身类型的方法,除了终结方法都是延迟方法。例如:filter,limit,skip,map,conat方法名称方法作用方法种类是否支持链式调用count统计个数终结方法否forEach逐一处理终结方法否filter过滤函数拼接是limit取用前几个函数拼接是skip跳过前几个函数拼接是map映射函数拼接是concat组合函数拼接是3.1 遍历/匹配(foreach、find、match)Stream也是支持类似集合的遍历和匹配元素的,只是Stream中的元素是以Optional类型存在的@Test public void testStream(){ List<Integer> list = Arrays.asList(7, 6, 9, 3, 8, 2, 1); // 遍历输出符合条件的元素 list.stream().filter(x -> x > 6).forEach(System.out::println); // 匹配第一个 Optional<Integer> findFirst = list.stream().filter(x -> x > 6).findFirst(); // 匹配任意(适用于并行流) Optional<Integer> findAny = list.parallelStream().filter(x -> x > 6).findAny(); // 是否包含符合特定条件的元素 boolean anyMatch = list.stream().anyMatch(x -> x > 6); System.out.println("匹配第一个值:" + findFirst.get()); System.out.println("匹配任意一个值:" + findAny.get()); System.out.println("是否存在大于6的值:" + anyMatch); }7 9 8 匹配第一个值:7 匹配任意一个值:8 是否存在大于6的值:true3.2 筛选(filter)@Test public void testStream(){ List<Integer> list = Arrays.asList(6, 7, 3, 8, 1, 2, 9); Stream<Integer> stream = list.stream(); stream.filter(x -> x > 7).forEach(System.out::println); }8 93.3 聚合(max、min、count)获取Integer集合中的最大/小值@Test public void testStream(){ List<Integer> list = Arrays.asList(7, 6, 9, 4, 11, 6); // 自然排序 Optional<Integer> max = list.stream().max(Integer::compareTo); Optional<Integer> min = list.stream().min(Integer::compareTo); // 自定义排序 Optional<Integer> max2 = list.stream().max(new Comparator<Integer>() { @Override public int compare(Integer o1, Integer o2) { return o1.compareTo(o2); } }); Optional<Integer> min2 = list.stream().min((o1, o2) -> o1.compareTo(o2)); System.out.println("自然排序的最大值:" + max.get()+",最小值:"+min.get()); System.out.println("自定义排序的最大值:" + max2.get()+",最小值:"+min2.get()); }自然排序的最大值:11,最小值:4 自定义排序的最大值:11,最小值:4获取String集合中最长/最低的元素@Test public void testStream(){ List<String> list = Arrays.asList("adnm", "admmt", "pot", "xbangd", "weoujgsd"); Optional<String> max = list.stream().max(Comparator.comparing(String::length)); Optional<String> min = list.stream().min(Comparator.comparing(String::length)); System.out.println("最长的字符串:" + max.get()); System.out.println("最短的字符串:" + min.get()); }最长的字符串:weoujgsd 最短的字符串:pot获取员工工资最高的人@Data class Person { private String name; // 姓名 private int salary; // 薪资 private int age; // 年龄 private String sex; //性别 private String area; // 地区 } @Test public void testStream(){ List<Person> personList = new ArrayList<Person>(); personList.add(new Person("Tom", 8900, 23, "male", "New York")); personList.add(new Person("Jack", 7000, 25, "male", "Washington")); personList.add(new Person("Lily", 7800, 21, "female", "Washington")); personList.add(new Person("Anni", 8200, 24, "female", "New York")); personList.add(new Person("Owen", 9500, 25, "male", "New York")); personList.add(new Person("Alisa", 7900, 26, "female", "New York")); Optional<Person> max = personList.stream().max(Comparator.comparingInt(Person::getSalary)); Optional<Person> min = personList.stream().min(new Comparator<Person>() { @Override public int compare(Person o1, Person o2) { return Integer.compare(o1.getSalary(), o2.getSalary()); } }); System.out.println("员工工资最大值:" + max.get().getSalary()); System.out.println("员工工资最小值:" + min.get().getSalary()); }员工工资最大值:9500 员工工资最小值:7000计算Integer集合中大于6的元素的个数@Test public void testStream(){ List<Integer> list = Arrays.asList(7, 6, 4, 8, 2, 11, 9); long count = list.stream().filter(x -> x > 6).count(); System.out.println("list中大于6的元素个数:" + count); }list中大于6的元素个数:43.4 映射(map、flatMap)映射,可以将一个流的元素按照一定的映射规则映射到另一个流中。分为map和flatMap:map:接收一个函数作为参数,该函数会被应用到每个元素上,并将其映射成一个新的元素。flatMap:接收一个函数作为参数,将流中的每个值都换成另一个流,然后把所有流连接成一个流。英文字符串数组的元素全部改为大写。整数数组每个元素+3@Test public void testStream(){ String[] strArr = { "abcd", "bcdd", "defde", "fTr" }; List<String> strList = Arrays.stream(strArr).map(String::toUpperCase).collect(Collectors.toList()); List<Integer> intList = Arrays.asList(1, 3, 5, 7, 9, 11); List<Integer> intListNew = intList.stream().map(x -> x + 3).collect(Collectors.toList()); System.out.println("每个元素大写:" + strList); System.out.println("每个元素+3:" + intListNew); }每个元素大写:[ABCD, BCDD, DEFDE, FTR] 每个元素+3:[4, 6, 8, 10, 12, 14]将两个字符数组合并成一个新的字符数组@Test public void testStream(){ List<String> list = Arrays.asList("m,k,l,a", "1,3,5,7"); List<String> listNew = list.stream().flatMap(s -> { // 将每个元素转换成一个stream String[] split = s.split(","); Stream<String> s2 = Arrays.stream(split); return s2; }).collect(Collectors.toList()); System.out.println("处理前的集合:" + list); System.out.println("处理后的集合:" + listNew); }处理前的集合:[m,k,l,a, 1,3,5,7] 处理后的集合:[m, k, l, a, 1, 3, 5, 7]3.5 规约(reduce)归约,也称缩减,顾名思义,是把一个流缩减成一个值,能实现对集合求和、求乘积和求最值操作。@Test public void testStream(){ List<Integer> list = Arrays.asList(1, 3, 2, 8, 11, 4); // 求和方式1 Optional<Integer> sum = list.stream().reduce((x, y) -> x + y); // 求和方式2 Optional<Integer> sum2 = list.stream().reduce(Integer::sum); // 求和方式3 Integer sum3 = list.stream().reduce(0, Integer::sum); // 求乘积 Optional<Integer> product = list.stream().reduce((x, y) -> x * y); // 求最大值方式1 Optional<Integer> max = list.stream().reduce((x, y) -> x > y ? x : y); // 求最大值写法2 Integer max2 = list.stream().reduce(1, Integer::max); System.out.println("list求和:" + sum.get() + "," + sum2.get() + "," + sum3); System.out.println("list求积:" + product.get()); System.out.println("list求和:" + max.get() + "," + max2); }list求和:29,29,29 list求积:2112 list求和:11,113.6 收集(collect)collect,收集,可以说是内容最繁多、功能最丰富的部分了。从字面上去理解,就是把一个流收集起来,最终可以是收集成一个值也可以收集成一个新的集合。collect主要依赖java.util.stream.Collectors类内置的静态方法。3.6.1 归集(toList、toSet、toMap)因为流不存储数据,那么在流中的数据完成处理后,需要将流中的数据重新归集到新的集合里。toList、toSet和toMap比较常用,另外还有toCollection、toConcurrentMap等复杂一些的用法。@Data class Person { private String name; // 姓名 private int salary; // 薪资 private int age; // 年龄 private String sex; //性别 private String area; // 地区 } @Test public void testStream(){ List<Integer> list = Arrays.asList(1, 6, 3, 4, 6, 7, 9, 6, 20); List<Integer> listNew = list.stream().filter(x -> x % 2 == 0).collect(Collectors.toList()); Set<Integer> set = list.stream().filter(x -> x % 2 == 0).collect(Collectors.toSet()); List<Person> personList = new ArrayList<Person>(); personList.add(new Person("Tom", 8900, 23, "male", "New York")); personList.add(new Person("Jack", 7000, 25, "male", "Washington")); personList.add(new Person("Lily", 7800, 21, "female", "Washington")); personList.add(new Person("Anni", 8200, 24, "female", "New York")); Map<?, Person> map = personList.stream().filter(p -> p.getSalary() > 8000) .collect(Collectors.toMap(Person::getName, p -> p)); System.out.println("toList:" + listNew); System.out.println("toSet:" + set); System.out.println("toMap:" + map); }toList:[6, 4, 6, 6, 20] toSet:[4, 20, 6] toMap:{Tom=Person{name='Tom', salary=8900, age=23, sex='male', area='New York'}, Anni=Person{name='Anni', salary=8200, age=24, sex='female', area='New York'}}3.7 排序(sorted)sorted,中间操作。有两种排序:sorted():自然排序,流中元素需实现Comparable接口sorted(Comparator com):Comparator排序器自定义排序员工按工资、年龄排序@Data class Person { private String name; // 姓名 private int salary; // 薪资 private int age; // 年龄 private String sex; //性别 private String area; // 地区 } @Test public void testStream(){ List<Person> personList = new ArrayList<Person>(); personList.add(new Person("Sherry", 9000, 24, "female", "New York")); personList.add(new Person("Tom", 8900, 22, "male", "Washington")); personList.add(new Person("Jack", 9000, 25, "male", "Washington")); personList.add(new Person("Lily", 8800, 26, "male", "New York")); personList.add(new Person("Alisa", 9000, 26, "female", "New York")); // 按工资升序排序(自然排序) List<String> newList = personList.stream().sorted(Comparator.comparing(Person::getSalary)) .map(Person::getName) .collect(Collectors.toList()); // 按工资倒序排序 List<String> newList2 = personList.stream().sorted(Comparator.comparing(Person::getSalary).reversed()) .map(Person::getName) .collect(Collectors.toList()); // 先按工资再按年龄升序排序 List<String> newList3 = personList.stream() .sorted(Comparator.comparing(Person::getSalary).thenComparing(Person::getAge)) .map(Person::getName) .collect(Collectors.toList()); // 先按工资再按年龄自定义排序(降序) List<String> newList4 = personList.stream().sorted((p1, p2) -> { if (p1.getSalary() == p2.getSalary()) { return p2.getAge() - p1.getAge(); } else { return p2.getSalary() - p1.getSalary(); } }).map(Person::getName).collect(Collectors.toList()); System.out.println("按工资升序排序:" + newList); System.out.println("按工资降序排序:" + newList2); System.out.println("先按工资再按年龄升序排序:" + newList3); System.out.println("先按工资再按年龄自定义降序排序:" + newList4); }按工资升序排序:[Lily, Tom, Sherry, Jack, Alisa] 按工资降序排序:[Sherry, Jack, Alisa, Tom, Lily] 先按工资再按年龄升序排序:[Lily, Tom, Sherry, Jack, Alisa] 先按工资再按年龄自定义降序排序:[Alisa, Jack, Sherry, Tom, Lily]参考资料Java Stream流(详解)_java stream()-CSDN博客【java基础】吐血总结Stream流操作_java stream流操作-CSDN博客Java 8 Stream | 菜鸟教程 (runoob.com)
2023年10月12日
14 阅读
0 评论
0 点赞
2023-10-12
MapStruct学习笔记
1.MapStruct介绍在现在多模块多层级的项目中,应用于应用之间,模块于模块之间数据模型一般都不通用,每层都有自己的数据模型。这种对象与对象之间的互相转换,目前都是使用get,set方法,或者使用自定义的Beans.copyProperties进行转换。使用get,set方式会使得编码非常的麻烦,BeanUtils.copyProperties的方式是使用反射的方式,对性能的消耗比较大。Mapstruct的性能远远高于BeanUtils,对象转换次数属性个数BeanUtils耗时Mapstruct耗时5千万次614秒1秒5千万次1536秒1秒5千万次2555秒1秒MapStruct是一个开源的基于Java的代码生成器,用于创建实现Java Bean之间转换的扩展映射器。使用MapStruct,我们只需要创建接口,而该库会通过注解在编译过程中自动创建具体的映射实现,大大减少了通常需要手工编写的样板代码的数量。2.maven项目使用MapStruct2.1 pom依赖<dependency> <groupId>org.mapstruct</groupId> <artifactId>mapstruct</artifactId> <version>1.5.0.Final</version> </dependency> <dependency> <groupId>org.mapstruct</groupId> <artifactId>mapstruct-processor</artifactId> <version>1.5.0.Final</version> </dependency> <dependency> <groupId>org.projectlombok</groupId> <artifactId>lombok</artifactId> <version>1.18.12</version> </dependency>2.2 配置打包插件<build> <plugins> <plugin> <groupId>org.apache.maven.plugins</groupId> <artifactId>maven-compiler-plugin</artifactId> <version>3.8.1</version> <configuration> <source>17</source> <target>17</target> <annotationProcessorPaths> <path> <groupId>org.mapstruct</groupId> <artifactId>mapstruct-processor</artifactId> <version>1.5.0.Final</version> </path> </annotationProcessorPaths> </configuration> </plugin> </plugins> </build>2.3 创建实体类Source .java@Data public class Source { private Long id; private Date gmtCreate; private Date createTime; private Long buyerId; private Long age; private String userNick; private String userVerified; }Target.java@Data public class `Target { private Long id; private Date gmtCreate; private Date createTime; private Long buyerId; private Long age; private String userNick; private String userVerified; }2.4 创建映射接口定义一个接口,其中包含源类和目标类之间的映射方法。MapStruct将在编译时自动为这个接口生成实现import org.mapstruct.Mapper; import org.mapstruct.factory.Mappers; @Mapper public interface SourceTargetMapper { SourceTargetMapper INSTANCE = Mappers.getMapper(SourceTargetMapper.class); Target sourceToTarget(Source source); }2.5 测试映射接口@Test public void testMapStruct(){ Source source = new Source(); source.setId(1L); source.setGmtCreate(new Date()); source.setCreateTime(new Date()); source.setBuyerId(43252534643L); source.setAge(99L); source.setUserNick("mapstruct测试"); source.setUserVerified("ok"); System.out.println(source); Target target = SourceTargetMapper.INSTANCE.sourceToTarget(source); System.out.println(target); }运行结果Source{id=1, gmtCreate=Wed Oct 11 15:46:15 CST 2023, createTime=Wed Oct 11 15:46:15 CST 2023, buyerId=43252534643, age=99, userNick='mapstruct测试', userVerified='ok'} Target{id=1, gmtCreate=Wed Oct 11 15:46:15 CST 2023, createTime=Wed Oct 11 15:46:15 CST 2023, buyerId=43252534643, age=99, userNick='mapstruct测试', userVerified='ok'}3. @Mapping解决字段名不一致的问题@Mapping(target = "targetName", source = "sourceName"),此处的意思就是在转化的过程中,将Source的Target属性值赋值给sourceName的targetName属性。@Data public class Source { private String sourceName; } @Data public class Target { private String targetName; }import org.mapstruct.Mapper; import org.mapstruct.Mapping; @Mapper public interface SourceTargetMapper { @Mapping(target = "targetName", source = "sourceName") Target sourceToTarget(Source source); }4.String转日期&String转数字&忽略某个字端&给默认值等@Mapping(target = "createTime", source = "createTime", dateFormat = "yyyy-MM-dd") @Mapping(target = "age", source = "age", numberFormat = "#0.00") @Mapping(target = "id", ignore = true) @Mapping(target = "userVerified", defaultValue = "defaultValue-2")参考资料MapStruct使用指南 - 知乎 (zhihu.com)告别BeanUtils,Mapstruct从入门到精通 - 掘金 (juejin.cn)Maven中配置maven-compiler-plugin 插件和jdk 17版本 - 楼兰胡杨 - 博客园 (cnblogs.com)
2023年10月12日
15 阅读
0 评论
0 点赞
2023-09-17
EasyExcel学习笔记
1.简介1.1 EasyExcel简介Java解析、生成Excel比较有名的框架有Apache poi、jxl。但他们都存在一个严重的问题就是非常的耗内存,poi有一套SAX模式的API可以一定程度的解决一些内存溢出的问题,但POI还是有一些缺陷,比如07版Excel解压缩以及解压后存储都是在内存中完成的,内存消耗依然很大。easyexcel重写了poi对07版Excel的解析,一个3M的excel用POI sax解析依然需要100M左右内存,改用easyexcel可以降低到几M,并且再大的excel也不会出现内存溢出;03版依赖POI的sax模式,在上层做了模型转换的封装,让使用者更加简单方便2.SpringBoot集成easyexcel2.1 pom依赖<dependency> <groupId>com.alibaba</groupId> <artifactId>easyexcel</artifactId> <version>3.3.2</version> </dependency>2.2 简单使用实体类@Data public class ArticleScoreData { @ExcelProperty("姓名") private String name; @ExcelProperty("文章") private String title; @ExcelProperty("得分") private Double score; }读操作待读取article.xlsx姓名文章得分张三张三的文章87李四李四的文章34王五王五的文章99读操作代码@Test public void testRead() { String pathName = "C:\\Users\\jupiter\\Desktop\\article.xlsx"; // PageReadListener:excel一行一行的回调监听器 EasyExcel.read(pathName, ArticleScoreData.class, new PageReadListener<ArticleScoreData>(dataList -> { for (ArticleScoreData demoData : dataList) { log.info("读取到一条数据{}", "姓名:" + demoData.getName() + " 文章:" + demoData.getTitle() + " 得分:" + demoData.getScore()); } })).sheet().doRead(); }运行结果2023-09-14T21:41:36.957+08:00 INFO 81220 --- [ main] c.e.e.EasyExcelStudyApplicationTests : 读取到一条数据姓名:张三 文章:张三的文章 得分:87.0 2023-09-14T21:41:36.962+08:00 INFO 81220 --- [ main] c.e.e.EasyExcelStudyApplicationTests : 读取到一条数据姓名:李四 文章:李四的文章 得分:34.0 2023-09-14T21:41:36.962+08:00 INFO 81220 --- [ main] c.e.e.EasyExcelStudyApplicationTests : 读取到一条数据姓名:王五 文章:王五的文章 得分:99.0写操作代码@Test public void testWrite() { String xlsxPath = "C:\\Users\\jupiter\\Desktop\\output.xls"; List<ArticleScoreData> dataList = new ArrayList<>(); for (int i = 0; i < 5; i++) { ArticleScoreData data = new ArticleScoreData(); data.setName("姓名" + i) data.setTitle("文章" + i); data.setScore(80.0+i); dataList.add(data); } EasyExcel.write(xlsxPath, ArticleScoreData.class) .sheet("文章得分表") .doWrite(() -> dataList); }运行效果output.xls姓名文章得分姓名0文章080姓名1文章181姓名2文章282姓名3文章383姓名4文章4842.3 单独实现最简单的读的监听器进行文件读取待读取article.xlsx姓名文章得分张三张三的文章87李四李四的文章34王五王五的文章99实体类@Data public class ArticleScoreData { @ExcelProperty("姓名") private String name; @ExcelProperty("文章") private String title; @ExcelProperty("得分") private Double score; }SimpleDataListenerimport cn.hutool.json.JSONUtil; import com.alibaba.excel.context.AnalysisContext; import com.alibaba.excel.metadata.data.ReadCellData; import com.alibaba.excel.read.listener.ReadListener; import com.example.easyexcelstudy.domain.entity.ArticleScoreData; import lombok.extern.slf4j.Slf4j; import java.util.Map; @Slf4j public class SimpleDataListener implements ReadListener<ArticleScoreData> { /** * 解析excel的表头-第一行 */ @Override public void invokeHead(Map<Integer, ReadCellData<?>> headMap, AnalysisContext context) { ReadListener.super.invokeHead(headMap, context); log.info("读取到表头:{}",JSONUtil.toJsonStr(headMap)); } /** * 读取excel的每一行都会调用该方法 */ @Override public void invoke(ArticleScoreData articleScoreData, AnalysisContext analysisContext) { log.info("解析到一条数据:{}", JSONUtil.toJsonStr(articleScoreData)); } /** * 所有数据解析完成了,都会来调用 */ @Override public void doAfterAllAnalysed(AnalysisContext context) { log.info("所有数据解析完成!"); } } test @Test public void testReadBySimpleDataListener() { String xlsxPath = "C:\\Users\\jupiter\\Desktop\\article.xlsx"; EasyExcel.read(xlsxPath,new SimpleDataListener()).sheet().doRead(); } 运行结果2023-09-14T22:39:47.596+08:00 INFO 186196 --- [ main] c.e.e.util.excel.SimpleDataListener : 读取到表头: { "0": { "dataFormatData": { "index": 0, "format": "General" }, "type": "STRING", "stringValue": "姓名", "rowIndex": 0, "columnIndex": 0 }, "1": { "dataFormatData": { "index": 0, "format": "General" }, "type": "STRING", "stringValue": "文章", "rowIndex": 0, "columnIndex": 1 }, "2": { "dataFormatData": { "index": 0, "format": "General" }, "type": "STRING", "stringValue": "得分", "rowIndex": 0, "columnIndex": 2 } } 2023-09-14T22:39:47.689+08:00 INFO 186196 --- [ main] c.e.e.util.excel.SimpleDataListener : 解析到一条数据:{"title":"张三的文章","score":87,"name":"张三"} 2023-09-14T22:39:47.690+08:00 INFO 186196 --- [ main] c.e.e.util.excel.SimpleDataListener : 解析到一条数据:{"title":"李四的文章","score":34,"name":"李四"} 2023-09-14T22:39:47.690+08:00 INFO 186196 --- [ main] c.e.e.util.excel.SimpleDataListener : 解析到一条数据:{"title":"王五的文章","score":99,"name":"王五"} 2023-09-14T22:39:47.691+08:00 INFO 186196 --- [ main] c.e.e.util.excel.SimpleDataListener : 所有数据解析完成!2.4 (★★★)读取超级版本:无需实体类,实现任意excel文件的读取待读取excel文件sheet1sheet2正常情况2 表头1表头2表头3表头4表头5表头1表头2表头3表头4表头5列头1 列头2 列头3 ExcelSheetDataReadListenerpackage com.example.excelstudy.utils.excel; import cn.hutool.json.JSONUtil; import com.alibaba.excel.context.AnalysisContext; import com.alibaba.excel.enums.CellExtraTypeEnum; import com.alibaba.excel.event.AnalysisEventListener; import com.alibaba.excel.metadata.CellExtra; import lombok.extern.slf4j.Slf4j; import java.util.ArrayList; import java.util.List; import java.util.Map; /** * @author LuoJia * @version 1.0 * @description: 万能excel的单个sheet读取Listener * @date 2023/9/15 11:24 */ @Slf4j public class ExcelSheetDataReadListener extends AnalysisEventListener<Map<Integer,String>> { // 表格sheet编号 int sheetNo; // 表格行数 int rowCount=0; // 表格列数 int colCount=0; // 用于存储原生读取到的数据 List<Map<Integer, String>> lineDataList = new ArrayList<>(); // 用于存储表格的合并单元格的区域列表 List<CellExtra> mergeAreaList = new ArrayList<>(); /** * 解析excel的表头-即读取第一行 */ @Override public void invokeHeadMap(Map<Integer, String> headMap, AnalysisContext context) { // 设置不忽略空行 context.readWorkbookHolder().setIgnoreEmptyRow(false); // 获取sheetNo sheetNo = context.readSheetHolder().getSheetNo(); // 更新表格行列数 rowCount += 1; colCount = Math.max(colCount, headMap.size()); // 保存原始的单行数据 lineDataList.add(headMap); //log.info("读取到表头:{}", JSONUtil.toJsonStr(headMap)); } /** * 读取excel的每一行都会调用该方法 */ @Override public void invoke(Map<Integer,String> lineData, AnalysisContext context) { // 更新表格行列数 rowCount += 1; colCount = Math.max(colCount, lineData.size()); // 保存原始的单行数据 lineDataList.add(lineData); //log.info("解析到一条数据:{}", JSONUtil.toJsonStr(lineData)); } /** * 获取合并单元格的范围 */ @Override public void extra(CellExtra extra, AnalysisContext context) { if (extra.getType() != CellExtraTypeEnum.MERGE) { return ; } mergeAreaList.add(extra); } /** * 所有数据解析完成了,都会来调用 */ @Override public void doAfterAllAnalysed(AnalysisContext context) { log.info("============================================================="); log.info("sheet{}-所有数据解析完成!sheet总行数:{},总列数:{}",sheetNo+1,rowCount,colCount); // 处理mergerList--即处理所有的合并单元格 for(CellExtra mergeArea:mergeAreaList){ // 获取填充部分的单元格的有效值 String value = lineDataList.get(mergeArea.getFirstRowIndex()).get(mergeArea.getFirstColumnIndex()); // 对合并单元格的为null值部分的数据进行有效填充 for (int i = mergeArea.getFirstRowIndex(); i <= mergeArea.getLastRowIndex(); i++) { for (int j = mergeArea.getFirstColumnIndex(); j <= mergeArea.getLastColumnIndex(); j++) { // 合并单元格的最最左上角已经被有效填充了,跳过 if(i==mergeArea.getFirstRowIndex()&&j== mergeArea.getFirstColumnIndex()){ continue; } // 对合并单元格的其他单元格进行数据填充 lineDataList.get(i).put(j,value); } } } // 打印表格数据 for (int i = 0; i < rowCount; i++) { log.info("sheet第{}行数据:{}",(i+1),JSONUtil.toJsonStr(lineDataList.get(i))); } log.info("============================================================="); } }test @Test public void testRead() throws FileNotFoundException { String pathName = "C:\\Users\\LuoJia\\Desktop\\test.xlsx"; InputStream inputStream = new FileInputStream(new File(pathName)); // 创建excel读取reader ExcelReader excelReader = EasyExcel.read(inputStream).extraRead(CellExtraTypeEnum.MERGE).ignoreEmptyRow(false).build(); // 创建每个sheet的读取listener并执行读取 List<ReadSheet> readSheets = excelReader.excelExecutor().sheetList(); List<ExcelSheetDataReadListener> listenerList = new ArrayList<>(readSheets.size()); // 用于进行数据和合并单元格区域保存 //读取多个sheet List<ReadSheet> sheetList = readSheets.stream().map(sheet -> { ExcelSheetDataReadListener listener = new ExcelSheetDataReadListener(); ReadSheet readSheet = EasyExcel.readSheet(sheet.getSheetName()).registerReadListener(listener).build(); listenerList.add(listener); return readSheet; }).collect(Collectors.toList()); excelReader.read(sheetList); // 释放资源 excelReader.finish(); }运行结果xxxx: ============================================================= xxxx: sheet1-所有数据解析完成!sheet总行数:10,总列数:6 xxxx: sheet第1行数据:{} xxxx: sheet第2行数据:{"0":"异常情况","1":"异常情况","2":"异常情况","3":"异常情况","4":"异常情况"} xxxx: sheet第3行数据:{"0":"表头1","1":"表头2","2":"表头3","3":"表头4","4":"表头5"} xxxx: sheet第4行数据:{"0":"表头1","1":"表头2","2":"表头3","3":"表头4","4":"表头5"} xxxx: sheet第5行数据:{} xxxx: sheet第6行数据:{} xxxx: sheet第7行数据:{"3":"dasdada","4":"dasdada"} xxxx: sheet第8行数据:{"3":"dasdada","4":"dasdada"} xxxx: sheet第9行数据:{} xxxx: sheet第10行数据:{"5":"saSASA"} xxxx: ============================================================= xxxx: ============================================================= xxxx: sheet2-所有数据解析完成!sheet总行数:2,总列数:5 xxxx: sheet第1行数据:{"0":"正常情况1","1":"正常情况1","2":"正常情况1","3":"正常情况1","4":"正常情况1"} xxxx: sheet第2行数据:{"0":"表头1","1":"表头2","2":"表头3","3":"表头4","4":"表头5"} xxxx: ============================================================= xxxx: ============================================================= xxxx: sheet3-所有数据解析完成!sheet总行数:5,总列数:5 xxxx: sheet第1行数据:{"0":"正常情况1","1":"正常情况1","2":"正常情况1","3":"正常情况1","4":"正常情况1"} xxxx: sheet第2行数据:{"0":"表头1","1":"表头2","2":"表头3","3":"表头4","4":"表头5"} xxxx: sheet第3行数据:{"0":"列头1"} xxxx: sheet第4行数据:{"0":"列头2"} xxxx: sheet第5行数据:{"0":"列头3"} xxxx: =============================================================2.5(★★★)带合并单元格的写入ExcelCustomMergeHandler(处理单元格合并的handler) /** * @author jupiter * @version 1.0 * @description: TODO * @date 2023/9/16 23:01 */ @Data @NoArgsConstructor @AllArgsConstructor @Slf4j public class ExcelCustomMergeHandler implements CellWriteHandler { // 表格行数 int rowCount; // 表格列数 int colCount; // 用于存储表格的合并单元格的区域列表 List<CellExtra> mergeAreaList = new ArrayList<>(); /** * @description: 在单元格被创建之前的处理 * @author jupiter * @date: 2023/9/16 23:12 */ @Override public void beforeCellCreate(WriteSheetHolder writeSheetHolder, WriteTableHolder writeTableHolder, Row row, Head head, Integer columnIndex, Integer relativeRowIndex, Boolean isHead) { CellWriteHandler.super.beforeCellCreate(writeSheetHolder, writeTableHolder, row, head, columnIndex, relativeRowIndex, isHead); } /** * @description: 在单元格被创建之后的处理 * @author jupiter * @date: 2023/9/16 23:12 */ @Override public void afterCellCreate(WriteSheetHolder writeSheetHolder, WriteTableHolder writeTableHolder, Cell cell, Head head, Integer relativeRowIndex, Boolean isHead) { CellWriteHandler.super.afterCellCreate(writeSheetHolder, writeTableHolder, cell, head, relativeRowIndex, isHead); } @Override public void afterCellDispose(WriteSheetHolder writeSheetHolder, WriteTableHolder writeTableHolder, List<WriteCellData<?>> cellDataList, Cell cell, Head head, Integer relativeRowIndex, Boolean isHead) { // 获取当前的单元格 Sheet sheet = writeSheetHolder.getSheet(); // 设置单元格居中 CellStyle cellStyle = cell.getCellStyle(); cellStyle.setAlignment(HorizontalAlignment.CENTER); //log.info("当前处理的单元格序号:{},{}",cell.getRowIndex(),cell.getColumnIndex()); // 在最后一个单元格处理单元格合并 if(cell.getRowIndex()==rowCount-1&&cell.getColumnIndex()==colCount-1){ for(CellExtra mergeArea:mergeAreaList){ CellRangeAddress cellAddresses = new CellRangeAddress(mergeArea.getFirstRowIndex(),mergeArea.getLastRowIndex(),mergeArea.getFirstColumnIndex(),mergeArea.getLastColumnIndex()); log.info("写入添加合并区域:{}", JSONUtil.toJsonStr(mergeArea)); sheet.addMergedRegion(cellAddresses); } } } }test(这里为了避免构建数据直接衔接了2.4用的读取后的数据)@Test public void testCustomWrite() throws FileNotFoundException { String pathName = "C:\\Users\\jupiter\\Desktop\\test.xlsx"; // 创建excel读取reader ExcelReader excelReader = EasyExcel.read(pathName).extraRead(CellExtraTypeEnum.MERGE).ignoreEmptyRow(false).build(); // 创建每个sheet的读取listener并执行读取 List<ReadSheet> readSheets = excelReader.excelExecutor().sheetList(); List<ExcelSheetDataReadListener> listenerList = new ArrayList<>(readSheets.size()); // 用于进行数据和合并单元格区域保存 //读取多个sheet List<ReadSheet> sheetList = readSheets.stream().map(sheet -> { ExcelSheetDataReadListener listener = new ExcelSheetDataReadListener(); ReadSheet readSheet = EasyExcel.readSheet(sheet.getSheetName()).registerReadListener(listener).build(); listenerList.add(listener); return readSheet; }).collect(Collectors.toList()); excelReader.read(sheetList); // 释放资源 excelReader.finish(); // 开始执行excel写入 pathName = "C:\\Users\\jupiter\\Desktop\\testWrite.xlsx"; // 创建excel写入writer ExcelWriter excelWriter = EasyExcel.write(pathName).build(); // 写入多个sheetList for (int i = 0; i < sheetList.size(); i++) { // 单元格总行数 int rowCount = listenerList.get(i).getRowCount(); // 单元格总列数 int colCount = listenerList.get(i).getColCount(); // sheet的逐行数据 List<Map<Integer, String>> lineDataList = listenerList.get(i).getLineDataList(); // 需要合并的单元格区域 List<CellExtra> mergeAreaList = listenerList.get(i).getMergeAreaList(); // 处理单元格合并的handle ExcelCustomMergeHandler writeHandle = new ExcelCustomMergeHandler(rowCount,colCount,mergeAreaList); // 构建sheet写入对象 WriteSheet writeSheet = EasyExcel.writerSheet(i,sheetList.get(i).getSheetName()).registerWriteHandler(writeHandle).build(); // 执行sheet数据写入 excelWriter.write(lineDataList,writeSheet); } // 释放资源 excelWriter.finish(); }执行效果test.xlsxsheet1sheet2testWrite.xlsxsheet1sheet2参考资料EasyExcel官方文档 - 基于Java的Excel处理工具 | Easy Excel (alibaba.com)EasyExcel全面教程快速上手_easeexcel_知春秋的博客-CSDN博客解决EasyExcel工具读取Excel空数据行的问题_easyexcel空行导入问题_流沙QS的博客-CSDN博客EasyExcel导入(含表头验证+空白行读取)_easyexcel导入表头校验_MMO_的博客-CSDN博客阿里的easyExcel_easyexcel aftercelldispose_一直想成为大神的菜鸟的博客-CSDN博客easyexcel导出中自定义合并单元格,通过重写AbstractRowWriteHandler_easyexcel合并单元格策略_阿莫西林的博客-CSDN博客EasyExcel导出自定义合并单元格的策略_我可能在扯淡的博客-CSDN博客
2023年09月17日
30 阅读
0 评论
0 点赞
2023-09-16
idea配置优化-默认maven/自动导包/方法类注释模板
1.配置默认mavenSTEP1:配置本项目File-->Setting配置所有新项目File-->New Projects Setup-->Setting for New Projects...STEP2:2.配置自动导包STEP1:配置本项目File-->Setting配置所有新项目File-->New Projects Setup-->Setting for New Projects...STEP2:3.配置注释3.1 新建类的时候自动添加类注释STEP1:File-->SettingSTEP2:如上图所示添加注释:/** * @description: TODO * @author ${USER} * @date ${DATE} ${TIME} * @version 1.0 */给接口和枚举加上的方式同理。3.2 自定义模版配置(类,方法)按照上图的提示,找到位置1的Live Templates找到位置2,选择下拉框中的Enter选项到位置3点击“+”号,首先选择Template Group,新建一个自己的分组鼠标选中新建的分组,如位置4的ybyGroup,然后在点击位置3的“+”号,选择Live Template给模版添加快捷提示的字符,描述,和模版,比如我这里新增了两个,方法的注释,类注释*在位置5处的Template text里面贴上模版内容在位置6选择应用的范围,一般选择EveryWhere里面的Java就可以了在位置7配置Template Text里面用$修饰的属性,具体配置截图如下:params的default value:groovyScript("def result=''; def params=\"${_1}\".replaceAll('[\\\\[|\\\\]|\\\\s]', '').split(',').toList(); for(i = 0; i < params.size(); i++) {result+='' + params[i] + ((i < params.size() - 1) ? '\\n':'')}; return result", methodParameters())方法注释模版:** * @description: TODO * @param: $params$ * @return: $returns$ * @author $USER$ * @date: $date$ $time$ */ 类注释模版:** * @description: TODO * @author $user$ * @date $date$ $time$ * @version 1.0 */参考资料idea注释模版配置(吐血推荐!!!)_ida注释模板_骑着乌龟漫步的博客-CSDN博客IDEA设置方法注释模板_idea设置注释模板_布丁吖的博客-CSDN博客
2023年09月16日
38 阅读
1 评论
0 点赞
2023-09-05
JWT(JSON Web Token)学习笔记
1.简介1.1 什么是JWT?官网地址: https://jwt.io/introduction/定义: Json Web Token(JWT)是一个开放标准(rfc7519),它定义了一种紧凑的、自包含的方式,用于在各方之间以JSON对象安全地传输信息。此信息可以验证和信任,因为它是数字签名的。 jwt可以使用密钥(使用HMAC算法)或使用RSA或ECDSA的公钥/私钥对进行签名通俗解释:JSON Web Token,简称JWT,也就是通过JSON形式作为Web应用中的令牌,用于在各方之间安全地将信息作为JSON对象传输。在数据传输过程中还可以完成数据加密、签名等相关处理。1.2 JWT具体的作用/什么时候使用 JWT ?授权:这是使用 JWT 的最常见方案。用户登录后,每个后续请求都将包含 JWT,允许用户访问该令牌允许的路由、服务和资源。单点登录是当今广泛使用 JWT 的一项功能,因为它的开销很小,并且能够跨不同域轻松使用。信息交换:JSON Web令牌是在各方之间安全传输信息的好方法。由于 JWT 可以签名(例如,使用公钥/私钥对),因此您可以确定发送方就是他们所说的人。此外,由于签名是使用标头和有效负载计算的,因此您还可以验证内容是否未被篡改。1.3 为什么选择了使用JWT1.3.1 基于传统的 Session 认证认证过程http协议本身是一种无状态的协议,这就意味着如果用户向我们的应用提供了用户名和密码来进行用户认证,即使验证通过后,那么下一次请求时,用户还要再一次进行用户认证才行,因为根据http协议,我们并不能知道是哪个用户发出的请求,所以为了让我们的应用能识别是哪个用户发出的请求,我们只能在服务器存储一份用户登录的信息,这份登录信息会在响应时传递给浏览器,告诉其保存为cookie,以便下次请求时发送给我们的应用,这样我们的应用就能识别请求来自哪个用户了,这就是传统的基于session认证。存在的问题每个用户经过应用认证之后,应用都要在服务端做一次记录,以方便用户下次请求的鉴别,通常而言session都是保存在内存中,而随着认证用户的增多,服务端的开销会明显增大用户认证之后,服务端做认证记录,如果认证的记录被保存在内存中的话,这意味着用户下次请求还必须要请求在这台服务器上,这样才能拿到授权的资源,这样在分布式的应用上,相应的限制了负载均衡器的能力。这也意味着限制了应用的扩展能力。因为是基于cookie来进行用户识别的, cookie如果被截获,用户就会很容易受到跨站请求伪造的攻击。在前后端分离系统中就更加痛苦,前后端分离在应用解耦后增加了部署的复杂性。通常用户一次请求就要转发多次。如果用session 每次携带sessionid 到服务器,服务器还要查询用户信息。同时如果用户很多,这些信息存储在服务器内存中,给服务器增加负担。还有就是CSRF攻击(跨站伪造请求攻击),session是基于cookie进行用户识别的, cookie如果被截获,用户就会很容易受到跨站请求伪造的攻击。还有就是sessionid就是一个特征值,表达的信息不够丰富,不容易扩展。如果你后端应用是多节点部署。那么就需要实现session共享机制。集群应用的搭建将变得十分困难。1.3.2 基于JWT认证基于JWT认证首先,前端通过Web表单将自己的用户名和密码发送到后端的接口。这一过程一般是一个HTTP POST请求。建议的方式是通过SSL加密的传输(https协议),从而避免敏感信息被嗅探。后端核对用户名和密码成功后,将用户的id等其他信息作为JWT Payload(负载),将其与头部分别进行Base64编码拼接后签名,形成一个JWT(Token)。形成的JWT就是一个形同xxxxx.yyyyy.zzzzz的字符串, token = head.payload.singurater后端将JWT字符串作为登录成功的返回结果返回给前端。前端可以将返回的结果保存在localStorage或sessionStorage上,退出登录时前端删除保存的JWT即可。前端在每次请求时将JWT放入HTTP Header中的Authorization位。(解决XSS和XSRF问题)后端检查是否存在,如存在验证JWT的有效性。例如,检查签名是否正确;检查Token是否过期;检查Token的接收方是否是自己(可选)。验证通过后后端使用JWT中包含的用户信息进行其他逻辑操作,返回相应结果。jwt的优点简洁(Compact): 可以通过URL,POST参数或者在HTTP header发送,由于其数据量小,并不会占用过多带宽自包含(Self-contained):负载中包含了所有用户所需要的信息,避免了多次查询数据库,减轻了数据库的压力因为Token是以JSON加密的形式保存在客户端的,所以JWT是跨语言的,原则上任何web形式都支持。不需要在服务端保存会话信息,特别适用于分布式微服务。2.JWT的结构JWT 由三部分组成2.1 Header(头部)是一个JSON 对象, 描述JWT的元数据,eg:# alg是签名算法,默认是HS256, # typ是token类型,一般JWT默认为JWT { "alg": "HS256", "typ": "JWT" }2.2 载荷-payload是一个JSON 对象, 用来存放实际需要传递的数据,eg:{ "iss": "songshu", "sub": "1234567890", "name": "haha", "admin": true }其中payload官方规定了7个字段:iss (issuer):签发人、 exp (expiration time):过期时间 sub (subject):主题 aud (audience):受众 nbf (Not Before):生效时间 iat (Issued At):签发时间 jti (JWT ID):编号$\color{red}{ 注意:对于已签名的令牌,此信息虽然受到保护以防止篡改,但任何人都可以读取。不要将机密信息放在 JWT 的有效负载或标头元素中,除非它已加密。}$2.3 签名(Signature)signature是对前两部分的签名,防止数据篡改。需要指定一个密钥(secret)这个密钥只有服务器才知道,不能泄露给客户端使用 Header 里面指定的签名算法(例如,如果要使用 HMAC SHA256 算法),按照下面的公式产生签名:HMACSHA256( base64UrlEncode(header) + "." +base64UrlEncode(payload), secret)把 Header、Payload、Signature 三个部分拼成一个字符串:Header.Payload.Signature = xxxx.yyy.zzz3.使用JWT3.1 引入jwt依赖<!--引入jwt--> <dependency> <groupId>com.auth0</groupId> <artifactId>java-jwt</artifactId> <version>3.4.0</version> </dependency>3.2 使用测试生成JWT令牌@Test public void testGetJWTToken() { Calendar instance = Calendar.getInstance(); instance.add(Calendar.SECOND, 900);//Calendar.SECOND代表单位为秒 //生成令牌 String token = JWT.create() .withClaim("username", "李四")//设置payload,保存自定义用户名,可设置多个 .withExpiresAt(instance.getTime())//设置过期时间 .sign(Algorithm.HMAC256("jupiter"));//设置加密算法及签名密钥,这里使用了默认的HMAC256 //输出令牌 System.out.println(token); }eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJleHAiOjE2OTM4OTcyOTUsInVzZXJuYW1lIjoi5p2O5ZubIn0.vO3kvqrFx4AlEs1qnFP7ghFeCIUAXFod4lH3SLhc5RE验证JWT令牌@Test public void testDecodeJWTToken(){ String token = "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJleHAiOjE2OTM4OTcyOTUsInVzZXJuYW1lIjoi5p2O5ZubIn0.vO3kvqrFx4AlEs1qnFP7ghFeCIUAXFod4lH3SLhc5RE"; JWTVerifier jwtVerifier = JWT.require(Algorithm.HMAC256("jupiter")).build(); DecodedJWT decodedJWT = jwtVerifier.verify(token);//验证token System.out.println("用户名: " + decodedJWT.getClaim("username").asString()); // 存的是时候是什么类型,取得时候就是什么类型,否则取出来的是null。 System.out.println("过期时间: "+decodedJWT.getExpiresAt()); }用户名: 李四 过期时间: Tue Sep 05 15:01:35 CST 20233.3 验证过程中可能会出现的异常SignatureVerificationException: 签名不一致异常:系统中并未签发过该token,一般是恶意用户伪造出来的 TokenExpiredException: 令牌过期异常 AlgorithmMismatchException: 算法不匹配异常:签名算法和验签算法不一致 InvalidClaimException: 失效的payload异常:token无效3.4 封装工具类public class JWTUtils { private static final String SIGNATURE="jupiter";//加密密钥 /** * 生成token header.payload.signatureResult * @param map : 需要保存的payload,也就是用户信息 * @return */ public static String getToken(Map<String,String> map){ Calendar instance = Calendar.getInstance(); instance.add(Calendar.MINUTE,4320);//3天过期 JWTCreator.Builder builder = JWT.create(); //设置payload map.forEach((k,v)->{ builder.withClaim(k,v); }); //设置过期时间 builder.withExpiresAt(instance.getTime()); //设置加密算法和加密密钥 String token = builder.sign(Algorithm.HMAC256(SIGNATURE)); return token; } /** * 验证token的合法性,若验证不通过,直接抛出异常 * @param token */ public static DecodedJWT verify(String token){ return JWT.require(Algorithm.HMAC256(SIGNATURE)).build().verify(token); } }4.SpringBoot项目整合使用在实际场景当中,我们需要对用户每一次的请求做验证,如果将每次验证的代码都要写一遍,这将造成大量代码的冗余,因此采用springmvc当中的拦截器InterceptorJWTInterceptorpublic class JWTInterceptor implements HandlerInterceptor { /** * 这里我们只需要实现请求之前的验证即可,因此只要实现preHandle()方法即可 * @param request * @param response * @param handler * @return * @throws Exception */ @Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { //一般我们要求将token放在请求的header中,所以这里我们从request中拿token String token = request.getHeader("token"); HashMap<Object, Object> map = new HashMap<>(); try { JWTUtils.verify(token); return true;//验证通过,放行 }catch (SignatureVerificationException e){ e.printStackTrace(); map.put("msg","签名错误"); }catch (AlgorithmMismatchException e){ e.printStackTrace(); map.put("msg","加密算法不一致"); }catch (TokenExpiredException e){ e.printStackTrace(); map.put("msg","token已过期"); }catch (Exception e) { e.printStackTrace(); map.put("msg","token无效"); } map.put("state",false);//设置状态 //这里返回json格式的map,只能手动将map处理成json 使用jackson String json = new ObjectMapper().writeValueAsString(map); response.setContentType("application/json;charset=UTF-8"); response.getWriter().println(json); return false; } }然后对拦截器进行配置JWTInterceptorConfig@Configuration public class JWTInterceptorConfig implements WebMvcConfigurer { @Override public void addInterceptors(InterceptorRegistry registry) { registry.addInterceptor(new JWTInterceptor()) .addPathPatterns("/**")//拦截所有请求 .excludePathPatterns("/user/login");//放行登录请求 } }参考资料JWT的学习笔记_algorithmmismatchexception 算法不匹配是什么原因_'小竹子'的博客-CSDN博客JWT(JSON Web Token) 学习笔记(整合Spring Boot)_jwt.getclaims_songshu。的博客-CSDN博客JWT 的结构详解_jwt结构_荆茗Scaler的博客-CSDN博客
2023年09月05日
20 阅读
0 评论
0 点赞
2023-09-05
JVM GC日志输出配置
1.命令格式java <GC日志参数> -jar <your_application.jar>2. JDK8 具体的GC日志参数基本(必备)JVM配置描述-Xloggc:/path/to/gc.log写入 GC 日志的路径-XX:+UseGCLogFileRotation启用 GC 日志文件轮换-XX:NumberOfGCLogFiles=5要保留的轮换 GC 日志文件数-XX:GCLogFileSize=104857600用于启动轮换的每个 GC 日志文件的大小-XX:+PrintGCDetails详细的GC日志-XX:+PrintGCDateStamps实际日期和时间戳-XX:+PrintGCApplicationStoppedTime应用程序在 GC 期间停止的时间量-XX:+PrintGCApplicationConcurrentTime应用程序在 GC 之间运行的时间量-XX:-PrintCommandLineFlags打印 GC 日志中的所有命令行标志增强JVM配置描述-XX:+PrintAdaptiveSizePolicy有关GC工程的详细信息-XX:+PrintTenuringDistribution幸存者空间的使用和分配-XX:+PrintReferenceGC处理引用所花费的时间3.JDK17具体的GC日志参数基本(必备)-Xlog参数\JVM配置描述:file=/opt/gc-%t.log写入 GC 日志的路径,%t表示当前时间:filesize=104857600,filecount=5启用日志分割,保留分割 GC 日志文件数+单个GC日志文件的大小<br/>超过了限制将会执行循环写入,先进先出式写入gc*详细的GC日志level,tags,time,uptime,pid实际日期和时间戳 与关键信息safepoint应用程序在 GC 期间停止的时间量-XX:-PrintCommandLineFlags打印 GC 日志中的所有命令行标志java -Xlog:gc*,safepoint:file=gc-%t.log:level,tags,time,uptime,pid:filesize=104857600,filecount=5 -jar <your_application.jar> 增强-Xlog参数\JVM配置描述gc+ergo*=trace有关GC工程的详细信息gc+age=trace幸存者空间的使用和分配gc+phases*=trace处理引用所花费的时间java -Xlog:gc*,safepoint,gc+ergo*=trace,gc+age=trace,gc+phases*=trace:file=gc-%t.log:level,tags,time,uptime,pid:filesize=104857600,filecount=5 -jar <your_application.jar>参考资料Java中的GC(垃圾回收)log ,以及 JVM 介绍_gc java命令_sun0322的博客-CSDN博客Java GC算法——日志解读与分析(GC参数基础配置分析)-腾讯云开发者社区-腾讯云 (tencent.com)JVM 配置GC日志_jvm打印gc日志_Coco_淳的博客-CSDN博客一篇带你搞定⭐《生产环境JVM日志配置》⭐_不学会Ⅳ的博客-CSDN博客
2023年09月05日
4 阅读
0 评论
0 点赞
2023-08-24
Java日志体系和springboot默认日志配置
1.日志框架的分类1.1 门面型日志框架日志门面定义了一组日志的接口规范,它并不提供底层具体的实现逻辑。JCL:Apache基金会所属的项目,是一套Java日志接口,之前叫Jakarta Commons Logging,后更名为Commons LoggingSLF4J:是一套简易Java日志门面,本身并无日志的实现。(Simple Logging Facade for Java,缩写Slf4j)1.2 记录型日志框架日志实现则是日志具体的实现,包括日志级别控制、日志打印格式、日志输出形式(输出到数据库、输出到文件、输出到控制台等)。Jul (Java Util Logging):JDK中的日志记录工具,也常称为JDKLog、jdk-logging,自Java1.4以来的官方日志实现。Log4j:Apache Log4j是一个基于Java的日志记录工具。它是由Ceki Gülcü首创的,现在则是Apache软件基金会的一个项目。 Log4j是几种Java日志框架之一。Log4j2:一个具体的日志实现框架,是Log4j 1的下一个版本,与Log4j 1发生了很大的变化,Log4j 2不兼容Log4j 1。Logback:一个具体的日志实现框架,和Slf4j是同一个作者,但其性能更好(推荐使用)。将日志门面和日志实现分离其实是一种典型的门面模式,这种方式可以让具体业务在不同的日志实现框架之间自由切换,而不需要改动任何代码,开发者只需要掌握日志门面的 API 即可。日志门面是不能单独使用的,它必须和一种具体的日志实现框架相结合使用。2.Java日志的恩怨情仇1996年早期,欧洲安全电子市场项目组决定编写它自己的程序跟踪API(Tracing API)。经过不断的完善,这个API终于成为一个十分受欢迎的Java日志软件包,即Log4j(由Ceki创建)。后来Log4j成为Apache基金会项目中的一员,Ceki也加入Apache组织。后来Log4j近乎成了Java社区的日志标准。据说Apache基金会还曾经建议Sun引入Log4j到Java的标准库中,但Sun拒绝了。2002年Java1.4发布,Sun推出了自己的日志库JUL(Java Util Logging),其实现基本模仿了Log4j的实现。在JUL出来以前,Log4j就已经成为一项成熟的技术,使得Log4j在选择上占据了一定的优势。接着,Apache推出了Jakarta Commons Logging,JCL只是定义了一套日志接口(其内部也提供一个Simple Log的简单实现),支持运行时动态加载日志组件的实现,也就是说,在你应用代码里,只需调用Commons Logging的接口,底层实现可以是Log4j,也可以是Java Util Logging。后来(2006年),Ceki不适应Apache的工作方式,离开了Apache。然后先后创建了Slf4j(日志门面接口,类似于Commons Logging)和Logback(Slf4j的实现)两个项目,并回瑞典创建了QOS公司,QOS官网上是这样描述Logback的:The Generic,Reliable Fast&Flexible Logging Framework(一个通用,可靠,快速且灵活的日志框架)。Java日志领域被划分为两大阵营:Commons Logging阵营和Slf4j阵营。Commons Logging在Apache大树的笼罩下,有很大的用户基数。但有证据表明,形式正在发生变化。2013年底有人分析了GitHub上30000个项目,统计出了最流行的100个Libraries,可以看出Slf4j的发展趋势更好。Apache眼看有被Logback反超的势头,于2012-07重写了Log4j 1.x,成立了新的项目Log4j 2, Log4j 2具有Logback的所有特性。3.项目中选择日志框架选择如果是在一个新的项目中建议使用Slf4j与Logback组合,这样有如下的几个优点。Slf4j实现机制决定Slf4j限制较少,使用范围更广。由于Slf4j在编译期间,静态绑定本地的LOG库使得通用性要比Commons Logging要好。Logback拥有更好的性能。Logback声称:某些关键操作,比如判定是否记录一条日志语句的操作,其性能得到了显著的提高。这个操作在Logback中需要3纳秒,而在Log4J中则需要30纳秒。LogBack创建记录器(logger)的速度也更快:13毫秒,而在Log4J中需要23毫秒。更重要的是,它获取已存在的记录器只需94纳秒,而Log4J需要2234纳秒,时间减少到了1/23。跟JUL相比的性能提高也是显著的。Commons Logging开销更高Logback文档免费。Logback的所有文档是全面免费提供的,不象Log4J那样只提供部分免费文档而需要用户去购买付费文档。4.Spring Boot 默认日志4.1 默认日志pom依赖Spring Boot 的日志支持依赖是 spring-boot-starter-logging,默认使用slf4j+logback的方式来记录日志。<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-logging</artifactId> <version>2.2.5.RELEASE</version> </dependency>4.2 默认日志级别Logback 支持 TRACE, DEBUG, INFO, WARN, ERROR 日志级别,优先级关系为 TRACE < DEBUG < INFO < WARN < ERROR , 可以在 application 配置文件中更改打印日志级别# Log level config logging.level.root=DEBUG4.3 默认日志打印测试手动创建logger@SpringBootTest class SpringBootLogStudyApplicationTests { private static final Logger logger = LoggerFactory.getLogger(SpringBootLogStudyApplicationTests.class); @Test void contextLoads() { logger.debug("debug日志"); logger.info("info日志"); logger.warn("warn日志"); logger.error("error日志"); } }使用@Slf4j注解需要实现引入lombok<dependency> <groupId>org.projectlombok</groupId> <artifactId>lombok</artifactId> <version>1.18.28</version> </dependency>打印测试@Slf4j @SpringBootTest class SpringBootLogStudyApplicationTests { @Test void testPrintLog() { log.debug("debug日志"); log.info("info日志"); log.warn("warn日志"); log.error("error日志"); } }4.4 日志格式设置使用属性 logging.pattern.console 和 logging.pattern.file 可以分别自定义控制台日志和文件日志的格式#控制台显示日志的格式 logging.pattern.console=%d{yyyy-MM-dd HH:mm:ss} [%thread] %-5level %logger{5}- %msg%n #文件显示日志的格式 logging.pattern.file=%d{yyyy-MM-dd HH:mm} [%thread] %-5level %logger- %msg%n上面用的一些标签含义如下:%d: 日期实践%thread: 线程名%-5level:级别从左显示5个字符宽度%logger{5}:表示logger名字最长5个字符,否则按照句点分割。%msg:日志消息%n:换行4.Logback 的两种配置方式使用application.yml 简单配置logging: level: root: info # 根日志级别设置为info com.example: debug # 特定包的日志级别设置为debug pattern: console: "%d{yyyy-MM-dd HH:mm:ss.SSS} %-5level [%thread] %logger{36} - %msg%n" file: name: application.log # 日志文件名称使用自定义规则的xml,可以实现每天生成一个日志文件,日志文件按等级分文件保存,保存日期等 复杂规则日志<?xml version="1.0" encoding="UTF-8"?> <configuration debug="false"> <contextName>study</contextName> <!--定义日志文件的存储地址 勿在 LogBack 的配置中使用相对路径--> <property name="LOG_HOME" value="./logs" /> <property name="appName" value="onlineStudy" /> <!--layout 为日志展示的形式--> <appender name="consoleLog" class="ch.qos.logback.core.ConsoleAppender"> <layout class="ch.qos.logback.classic.PatternLayout"> <pattern> %d-%msg%n </pattern> </layout> </appender> <!--控制台日志, 控制台输出 --> <appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender"> <encoder class="ch.qos.logback.classic.encoder.PatternLayoutEncoder"> <!--格式化输出:%d表示日期,%thread表示线程名,%-5level:级别从左显示5个字符宽度,%msg:日志消息,%n是换行符--> <pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{50} - %msg%n</pattern> </encoder> </appender> <!--RollingFileAppender:滚动记录文件,先将日志记录到指定文件,当符合某个条件时,将日志记录到其他文件--> <!--文件日志, 按照每天生成日志文件 --> <appender name="FILE" class="ch.qos.logback.core.rolling.RollingFileAppender"> <!--如果是 true,日志被追加到文件结尾,如果是 false,清空现存文件,默认是true --> <append>true</append> <!--当发生滚动时,决定RollingFileAppender的行为,涉及文件移动和重命名--> <rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy"> <!--日志文件输出的文件名--> <!--logType:日志类型,如 stats/monitor/access--> <FileNamePattern>${LOG_HOME}/${appName}_monitor_debug_%d{yyyy-MM-dd}.log</FileNamePattern> <!--日志文件保留天数--> <MaxHistory>30</MaxHistory> </rollingPolicy> <encoder class="ch.qos.logback.classic.encoder.PatternLayoutEncoder"> <!--格式化输出:%d表示日期,%thread表示线程名,%-5level:级别从左显示5个字符宽度%msg:日志消息,%n是换行符--> <pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{50} - %msg%n</pattern> </encoder> <!--日志文件最大的大小--> <triggeringPolicy class="ch.qos.logback.core.rolling.SizeBasedTriggeringPolicy"> <MaxFileSize>10MB</MaxFileSize> </triggeringPolicy> </appender> <!-- 按日志级别打印 INFO --> <appender name="infoAppender" class="ch.qos.logback.core.rolling.RollingFileAppender"> <!--被写入的文件名,可以是相对目录,也可以是绝对目录,如果上级目录不存在会自动创建,没有默认值。--> <file>${LOG_HOME}/info/info.log</file> <rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy"> <!-- 文件名称 --> <fileNamePattern>${LOG_HOME}/info/info.%d{yyyy-MM-dd}.log</fileNamePattern> <!-- 文件最大保存历史数量 --> <MaxHistory>30</MaxHistory> </rollingPolicy> <encoder> <pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{50} - %msg%n</pattern> </encoder> <!--filter 过滤输出--> <filter class="ch.qos.logback.classic.filter.LevelFilter"> <level>INFO</level> <onMatch>ACCEPT</onMatch> <onMismatch>DENY</onMismatch> </filter> </appender> <!-- 日志输出级别设置,ref 属性为 appender 的name--> <root level="DEBUG"> <appender-ref ref="consoleLog" /> <appender-ref ref="STDOUT" /> <appender-ref ref="FILE"/> <appender-ref ref="infoAppender"/> </root> </configuration>参考资料面试官:SpringBoot中关于日志工具的使用,我想问你几个常见问题-腾讯云开发者社区-腾讯云 (tencent.com)深入掌握Java日志体系,再也不迷路了 - 掘金 (juejin.cn)Java日志体系详解_Jeremy_Lee123的博客-CSDN博客Spring Boot中集成Slf4j 与Logback-腾讯云开发者社区-腾讯云 (tencent.com)Spring Boot 默认日志使用_logging.level.springfox__星辰夜风的博客-CSDN博客
2023年08月24日
49 阅读
0 评论
0 点赞
2023-08-21
java数据随机初始化&bean随机初始化
0.背景最近在开发一个web小demo,其中进行数据测试需要对一些字段特别多的java bean进行初始化,一番百度之后查找到了几个解决方案,记录如下。1.Jpopulator1.1 pom依赖<dependency> <groupId>io.github.benas</groupId> <artifactId>jpopulator</artifactId> <version>1.0.1</version> </dependency>1.2 使用方式及测试实体类import lombok.Data; @Data public class Person { private int id; private String name; private String gender; }测试代码@Test public void testJpopulator(){ Populator populator = new PopulatorBuilder().build(); Person person = (Person) populator.populateBean(Person.class); System.out.println(person); List<Person> persons = populator.populateBeans(Person.class, 2); System.out.println(persons); }Person(id=-1042426959, name=nOBdJpkFbB, gender=bCwLDjOxXb) [Person(id=813095571, name=zxDkvjecWM, gender=SztfWBTZEj), Person(id=1664076867, name=ckMadNdfcX, gender=qmxsXZOiOI)]2.PODAM2.1 pom依赖<dependency> <groupId>uk.co.jemos.podam</groupId> <artifactId>podam</artifactId> <version>7.1.1.RELEASE</version> </dependency>2.2 使用方式及测试实体类import lombok.Data; @Data public class Person { private int id; private String name; private String gender; }基本使用@Test public void testPodam(){ PodamFactory factory = new PodamFactoryImpl(); Person person = factory.manufacturePojo(Person.class); System.out.println(person); }Person(id=1889825458, name=s6tv8LgsPD, gender=zY5put8aZT)2.3 进阶自定义数据生成策略比较复杂,不推荐。参考文档:Java Unit Tests make easy - Random Values with PODAM (onloadcode.com)3.common-random参考文档:https://github.com/yindz/common-random支持的数据类型:数字(int/long/double)汉字(简体)邮箱地址中文人名(简体)英文人名虚拟身份证号码(中国大陆)虚拟信用卡号码(Visa/Mastercard/JCB/银联/AmericanExpress)手机号码(中国大陆)省份和城市(中国大陆)邮编(中国大陆)联系地址(中国大陆)车牌号(中国大陆,包括新能源车型)域名静态URL日期(特定日期之前/特定日期之后)时间(过去/未来)时间戳强密码网络昵称(登录名)拼音网络昵称(登录名)IPv4地址端口号QQ号码非主流QQ网名学历小学名称、年级、班级中学名称、年级、班级高校名称(数据取自教育部网站)公司名称经纬度(中国)中文短句User-Agent(PC/Android/iOS)网卡MAC地址RGB颜色值HEX颜色值股票名称+股票代码开放式基金名称+基金代码缺点:只能逐一生成单个的随机字段,数据、生成对象需要逐一对对象的属性进行填充吗。对于字段较多的对象生成比较麻烦4.Jmockdata(★★★)(推荐)支持类型:Java基本类型字符串枚举日期数组多维数组集合[List|Set|Map]Java对象4.1 pom依赖<dependency> <groupId>com.github.jsonzou</groupId> <artifactId>jmockdata</artifactId> <version>4.2.0</version> </dependency>4.2 Java常用类型的生成@Test public void testJmockdata(){ int randomInt = JMockData.mock(int.class); long randomLong = JMockData.mock(long.class); double randomDouble = JMockData.mock(double.class); float randomFloat = JMockData.mock(float.class); String randomString = JMockData.mock(String.class); BigDecimal randomBigDecimal = JMockData.mock(BigDecimal.class); System.out.println("randomInt:"+randomInt); System.out.println("randomLong:"+randomLong); System.out.println("randomDouble:"+randomDouble); System.out.println("randomFloat:"+randomFloat); System.out.println("randomString:"+randomString); System.out.println("randomBigDecimal:"+randomBigDecimal); }randomInt:1116 randomLong:6722 randomDouble:1303.83 randomFloat:9700.79 randomString:cggMvc randomBigDecimal:3029.84.3 对象的生成实体类@Data public class Student { private int id; private String name; private int age; private LocalDateTime createTime; }生成测试数据@Test public void testJmockdata(){ Student student1 = JMockData.mock(Student.class); System.out.println("默认生成策略生成结果:"+student1); //按照规则定义Student对象里面单个字段生成,没配置的就按默认的生成邪恶了 MockConfig mockConfig = new MockConfig() .subConfig("id").intRange(1,10) .subConfig("name").subConfig("[a-zA-Z_]{1}[a-z0-9_]{5,15}") .subConfig("age").intRange(18,21) .globalConfig(); Student student2 = JMockData.mock(Student.class,mockConfig); System.out.println("自定义生成策略生成结果:"+student2); }执行结果默认生成策略生成结果:Student(id=7105, name=s, age=9755, createTime=2039-09-15T13:53:47.294) 自定义生成策略生成结果:Student(id=1, name=akXrp, age=18, createTime=2050-12-04T07:05:53.299)4.4 根据正则模拟数据配合4.3共同完成对象属性的初始化。/** * 根据正则模拟数据 * 正则优先于其他规则 */ @Test public void testRegexMock() { MockConfig mockConfig = new MockConfig() // 随机段落字符串 .stringRegex("I'am a nice man\\.And I'll just scribble the characters, like:[a-z]{2}-[0-9]{2}-[abc123]{2}-\\w{2}-\\d{2}@\\s{1}-\\S{1}\\.?-.") // 邮箱 .subConfig(RegexTestDataBean.class,"userEmail") .stringRegex("[a-z0-9]{5,15}\\@\\w{3,5}\\.[a-z]{2,3}") // 用户名规则 .subConfig(RegexTestDataBean.class,"userName") .stringRegex("[a-zA-Z_]{1}[a-z0-9_]{5,15}") // 年龄 .subConfig(RegexTestDataBean.class,"userAge") .numberRegex("[1-9]{1}\\d?") // 用户现金 .subConfig(RegexTestDataBean.class,"userMoney") .numberRegex("[1-9]{2}\\.\\d?") // 用户的得分 .subConfig(RegexTestDataBean.class,"userScore") .numberRegex("[1-9]{1}\\d{1}") // 用户身价 .subConfig(RegexTestDataBean.class,"userValue") .numberRegex("[1-9]{1}\\d{3,8}") .globalConfig(); }参考资料Jpopulator测试数据生成工具-腾讯云开发者社区-腾讯云 (tencent.com)Podam 一个Pojo填充随机值利器_这个程序员像只猴的博客-CSDN博客jmockdata: Jmockdta是一款实现模拟JAVA类型或对象的实例化并随机初始化对象的数据的工具框架。单元测试的利器 (gitee.com)GitHub - yindz/common-random: 简单易用的随机数据生成器。生成各种比较真实的假数据。一般用于开发和测试阶段的数据填充模拟。支持各类中国特色本地化的数据格式。An easy-to use random data generator. Generally used for data filling, simulation, demonstration and other scenarios in the development and test phase.JmockData---jfairy 学习记录(生成测试数据)_"小王"的博客-CSDN博客Jmockdta是一款实现模拟JAVA类型或对象的实例化并随机初始化对象的数据的工具框架-面圈网 (mianshigee.com)
2023年08月21日
30 阅读
0 评论
0 点赞
2023-05-10
SpringBoot调用阿里云内容审核API实现文本和图片审核
1.服务开通地址:https://vision.aliyun.com/imageaudit?spm=5176.11065253.1411203.3.7e8153f6mehjzV2.引入公共POM依赖<!--json转换依赖--> <dependency> <groupId>com.alibaba</groupId> <artifactId>fastjson</artifactId> <version>2.0.25</version> </dependency> <!--文字内容审核依赖及图片审核依赖共用--> <dependency> <groupId>com.aliyun</groupId> <artifactId>imageaudit20191230</artifactId> <version>2.0.6</version> </dependency>3.文本审核3.1 核心代码private static final String accessKeyId = "<your-access-key-id>"; private static final String accessKeySecret = "<your-access-key-secret>"; @PostMapping("/scanText") public String scanText(@RequestBody HashMap<String,String> reqMap) throws Exception { // 获取待检测的文字 String text = reqMap.get("text"); System.out.println("text="+text); // 返回结果的变量 Map<String,String> resMap = new HashMap<>(); //实例化客户端 Config config = new Config() // 必填,您的 AccessKey ID .setAccessKeyId(accessKeyId) // 必填,您的 AccessKey Secret .setAccessKeySecret(accessKeySecret); config.endpoint = "imageaudit.cn-shanghai.aliyuncs.com"; Client client = new Client(config); /** * spam:文字垃圾内容识别 * politics:文字敏感内容识别 * abuse:文字辱骂内容识别 * terrorism:文字暴恐内容识别 * porn:文字鉴黄内容识别 * flood:文字灌水内容识别 * contraband:文字违禁内容识别 * ad:文字广告内容识别 */ // 设置待检测类型 ScanTextRequest.ScanTextRequestLabels labels0 = new ScanTextRequest.ScanTextRequestLabels() .setLabel("politics"); ScanTextRequest.ScanTextRequestLabels labels1 = new ScanTextRequest.ScanTextRequestLabels() .setLabel("contraband"); ScanTextRequest.ScanTextRequestLabels labels2 = new ScanTextRequest.ScanTextRequestLabels() .setLabel("terrorism"); ScanTextRequest.ScanTextRequestLabels labels3 = new ScanTextRequest.ScanTextRequestLabels() .setLabel("abuse"); ScanTextRequest.ScanTextRequestLabels labels4 = new ScanTextRequest.ScanTextRequestLabels() .setLabel("spam"); ScanTextRequest.ScanTextRequestLabels labels5 = new ScanTextRequest.ScanTextRequestLabels() .setLabel("ad"); // 设置待检测内容 ScanTextRequest.ScanTextRequestTasks tasks0 = new ScanTextRequest.ScanTextRequestTasks() .setContent(text); ScanTextRequest scanTextRequest = new ScanTextRequest() .setTasks(java.util.Arrays.asList( tasks0 )) .setLabels(java.util.Arrays.asList( labels0, labels1, labels2, labels3, labels4, labels5 )); RuntimeOptions runtime = new RuntimeOptions(); ScanTextResponse response = null; try { // 复制代码运行请自行打印 API 的返回值 response = client.scanTextWithOptions(scanTextRequest, runtime); resMap.put("data",JSON.toJSONString(response.getBody().getData().getElements().get(0).getResults())); //调用后获取到他的返回对象, 然后判断我们的文字 是什么内容 List<ScanTextResponseBody.ScanTextResponseBodyDataElementsResultsDetails> responseDetails = response.getBody().getData().getElements().get(0).getResults().get(0).getDetails(); if (responseDetails.size()>0){ resMap.put("state","block"); StringBuilder error = new StringBuilder("检测到:"); for (ScanTextResponseBody.ScanTextResponseBodyDataElementsResultsDetails detail : responseDetails) { if ("abuse".equals(detail.getLabel())) error.append("辱骂内容、"); if ("spam".equals(detail.getLabel())) error.append("垃圾内容、"); if ("politics".equals(detail.getLabel())) error.append("敏感内容、"); if ("terrorism".equals(detail.getLabel())) error.append("暴恐内容、"); if ("porn".equals(detail.getLabel())) error.append("黄色内容、"); if ("flood".equals(detail.getLabel())) error.append("灌水内容、"); if ("contraband".equals(detail.getLabel())) error.append("违禁内容、"); if ("ad".equals(detail.getLabel())) error.append("广告内容、"); } resMap.put("msg",error.toString()); return JSON.toJSONString(resMap); }else { resMap.put("state","pass"); resMap.put("msg","未检测出违规!"); return JSON.toJSONString(resMap); } } catch (Exception _error) { resMap.put("state","review"); resMap.put("msg","阿里云无法进行判断,需要人工进行审核,错误详情:"+_error); return JSON.toJSONString(resMap); } }3.2 调用结果req{ "text":"hello word! 卧槽6666" }res{ "state": "block", "msg": "检测到:辱骂内容、", "data": { "details": [{ "contexts": [{ "context": "卧槽" }], "label": "abuse" }], "label": "abuse", "rate": 99.91, "suggestion": "block" } }4.图片审核4.1 核心代码private static final String accessKeyId = "<your-access-key-id>"; private static final String accessKeySecret = "<your-access-key-secret>"; @PostMapping("/scanImage") public String scanImage(@RequestBody HashMap<String,String> reqMap) throws Exception { // 获取待检测的文字 String image = reqMap.get("image"); System.out.println("image="+image); // 返回结果的变量 Map<String,String> resMap = new HashMap<>(); //实例化客户端 Config config = new Config() // 必填,您的 AccessKey ID .setAccessKeyId(accessKeyId) // 必填,您的 AccessKey Secret .setAccessKeySecret(accessKeySecret); config.endpoint = "imageaudit.cn-shanghai.aliyuncs.com"; Client client = new Client(config); // 设置待检测内容 ScanImageRequest.ScanImageRequestTask task0 = new ScanImageRequest.ScanImageRequestTask().setImageURL(image); // 封装检测请求 /** * porn:图片智能鉴黄 * terrorism:图片敏感内容识别、图片风险人物识别 * ad:图片垃圾广告识别 * live:图片不良场景识别 * logo:图片Logo识别 */ ScanImageRequest scanImageRequest = new ScanImageRequest() .setTask(java.util.Arrays.asList( task0 )) .setScene(java.util.Arrays.asList( "porn","terrorism","live" )); RuntimeOptions runtime = new RuntimeOptions(); // 调用API获取检测结果 ScanImageResponse response = client.scanImageWithOptions(scanImageRequest, runtime); resMap.put("data",JSON.toJSONString(response.getBody().getData().getResults().get(0))); // 检测结果解析 try { List<ScanImageResponseBody.ScanImageResponseBodyDataResultsSubResults> responseSubResults = response.getBody().getData().getResults().get(0).getSubResults(); for(ScanImageResponseBody.ScanImageResponseBodyDataResultsSubResults responseSubResult : responseSubResults){ if(responseSubResult.getSuggestion()!="pass"){ resMap.put("state",responseSubResult.getSuggestion()); String msg = ""; switch (responseSubResult.getLabel()){ case "porn": msg = "图片智能鉴黄未通过"; break; case "terrorism": msg = "图片敏感内容识别、图片风险人物识别未通过"; break; case "ad": msg = "图片垃圾广告识别未通过"; break; case "live": msg = "图片不良场景识别未通过"; break; case "logo": msg = "图片Logo识别未通过"; break; } return JSON.toJSONString(resMap); } } } catch (Exception error) { resMap.put("state","review"); resMap.put("msg","发生错误,详情:"+error); return JSON.toJSONString(resMap); } resMap.put("state","pass"); return JSON.toJSONString(resMap); }4.2 调用结果req{ "image":"https://jupite-aliyun.oss-cn-hangzhou.aliyuncs.com/second_hand_shop/client/img/goodImgs/1683068284289.jpg" }res{ "data": { "imageURL": "http://jupite-aliyun.oss-cn-hangzhou.aliyuncs.com/second_hand_shop/client/img/goodImgs/1683068284289.jpg", "subResults": [ { "label": "normal", "rate": 99.9, "scene": "porn", "suggestion": "pass" }, { "label": "normal", "rate": 99.88, "scene": "terrorism", "suggestion": "pass" }, { "label": "normal", "rate": 99.91, "scene": "live", "suggestion": "pass" } ] }, "state": "pass" }参考资料https://next.api.aliyun.com/api/imageaudit/2019-12-30/ScanImage阿里云文本检测 使用教程(Java)https://vision.aliyun.com/imageaudit?spm=5176.11065253.1411203.3.7e8153f6mehjzV
2023年05月10日
318 阅读
0 评论
0 点赞
2023-05-02
SpringBoot整合阿里云短信发送
0.业务场景短信发送验证码实现注册,登录...1.开通阿里云短信服务去到阿里云官方网址:https://www.aliyun.com/ 选择短信服务,在这里能获取到我们需要的4个参数,分别是accessKeyId、accessKeySecret、短信签名、模板code。2.整合进SpringBoot-方法一(推荐方法二)2.1导入依赖<dependency> <groupId>com.aliyun</groupId> <artifactId>aliyun-java-sdk-core</artifactId> <version>4.3.3</version> </dependency>2.2 封装成工具类或者服务类package top.inat.shop.utils; import com.aliyuncs.CommonRequest; import com.aliyuncs.CommonResponse; import com.aliyuncs.DefaultAcsClient; import com.aliyuncs.IAcsClient; import com.aliyuncs.http.MethodType; import com.aliyuncs.profile.DefaultProfile; import org.springframework.stereotype.Component; import java.util.Random; /** * @program: server * @ClassName AliMessageUtil * @description: 验证码工具类 * @author: jupiter * @create: 2023-04-23 10:17 * @Version 1.0 **/ @Component public class AliMessageUtil { /** * 需要配置的参数 */ // 阿里云的id和秘钥 从个人中心进行创建 private static final String accessKeyId="XXXXXXXX"; private static final String secret="XXXXXXXX"; //申请的阿里云的签名名称 private static final String SignName="smile佳"; //申请的阿里云的短信模板code private static final String TemplateCode = "SMS_147439706"; /** * 生成6位数字验证码函数 */ public static String generateVerifiCode() { int n = 6; StringBuilder code = new StringBuilder(); Random ran = new Random(); for (int i = 0; i < n; i++) { code.append(Integer.valueOf(ran.nextInt(10)).toString()); } return code.toString(); } /** * 通过阿里云短信发送验证码 * @param code 验证码 * @param phone 手机号 * @return */ public static boolean sendMsmVerifyCode(String phone,String code) { //default 地域节点,默认就好 后面是 阿里云的 id和秘钥 DefaultProfile profile = DefaultProfile.getProfile("default", accessKeyId, secret); IAcsClient client = new DefaultAcsClient(profile); // 组装请求对象 SendSmsRequest request = new SendSmsRequest(); request.putQueryParameter("PhoneNumbers", phone); request.putQueryParameter("SignName",SignName); request.putQueryParameter("TemplateCode", TemplateCode); request.putQueryParameter("TemplateParam", "{\"code\":\"" + code + "\"}"); try { CommonResponse response = client.getCommonResponse(request); System.out.println(response.getData()); return response.getHttpResponse().isSuccess(); } catch (Exception e) { e.printStackTrace(); } return false; } }进行测试 @Test void aliMessageTest(){ String code = AliMessageUtil.generateVerifiCode(); System.out.println("生成的验证码为:"+code); String phone = "18673918533"; boolean sendRes = AliMessageUtil.sendMsmVerifyCode(phone,code); System.out.println("短信发送结果:"+sendRes); }2.3 运行结果生成的验证码为:196573 {"Message":"OK","RequestId":"97D16831-6EB8-5300-AF5F-25EC86638C26","Code":"OK","BizId":"405312082956966726^0"} 短信发送结果:true2.整合进SpringBoot-方法二2.1导入依赖<dependency> <groupId>com.aliyun</groupId> <artifactId>dysmsapi20170525</artifactId> <version>2.0.9</version> </dependency> <!-- fastjson 打印详细的发送返回的结果用的,只看发送成功失败的话可以去掉 --> <dependency> <groupId>com.alibaba</groupId> <artifactId>fastjson</artifactId> <version>1.2.35</version> </dependency>2.2 封装成工具类或者服务类package top.inat.shop.utils; import com.alibaba.fastjson.JSON; import com.aliyun.dysmsapi20170525.Client; import com.aliyun.dysmsapi20170525.models.SendSmsRequest; import com.aliyun.dysmsapi20170525.models.SendSmsResponse; import com.aliyun.dysmsapi20170525.models.SendSmsResponseBody; import com.aliyun.teaopenapi.models.Config; import org.springframework.stereotype.Component; import java.util.Random; /** * @program: server * @ClassName AliMessageUtil * @description: 验证码工具类 * @author: jupiter * @create: 2023-04-23 10:17 * @Version 1.0 **/ @Component public class AliMessageUtil { /** * 需要配置的参数 */ private static final String accessKeyId="LTAI5t7Lg3SECa8JSvyYrhoj";//这里修改为个人中心生成的AccessKey ID private static final String accessKeySecret="AXeyeLFKUU8MkgUSnTj2qTLqnZv2rL";//这里修改为个人中心生成的AccessKey Secret private static final String SignName="smile佳"; //申请的阿里云的签名名称 private static final String TemplateCode = "SMS_147439706"; ////申请的阿里云的短信模板code /** * 生成6位数字验证码 */ public static String generateVerifiCode() { int n = 6; StringBuilder code = new StringBuilder(); Random ran = new Random(); for (int i = 0; i < n; i++) { code.append(Integer.valueOf(ran.nextInt(10)).toString()); } return code.toString(); } /** * 通过阿里云短信发送验证码 * @param code 验证码 * @param phone 手机号 * @return */ public static boolean sendMsmVerifyCode(String phone,String code) throws Exception { Config config = new Config().setAccessKeyId(accessKeyId).setAccessKeySecret(accessKeySecret).setEndpoint( "dysmsapi.aliyuncs.com"); Client client = new Client(config); SendSmsRequest request = new SendSmsRequest(); request.setPhoneNumbers(phone); request.setSignName(SignName); request.setTemplateCode(TemplateCode); request.setTemplateParam("{\"code\":\"" + code + "\"}"); SendSmsResponse response = client.sendSms(request); SendSmsResponseBody body = response.getBody(); System.out.println(JSON.toJSONString(body));//不用fastjson打印结果就注释掉这一行 if("OK".equals(body.getCode())){ return true; } return false; } } 进行测试 @Test void aliMessageTest(){ String code = AliMessageUtil.generateVerifiCode(); System.out.println("生成的验证码为:"+code); String phone = "18673918533"; boolean sendRes = AliMessageUtil.sendMsmVerifyCode(phone,code); System.out.println("短信发送结果:"+sendRes); }2.3 运行结果生成的验证码为:196573 {"Message":"OK","RequestId":"97D16831-6EB8-5300-AF5F-25EC86638C26","Code":"OK","BizId":"405312082956966726^0"} 短信发送结果:true参考资料SpringBoot整合阿里云短信服务详细过程(保证初学者也能实现)SpringBoot集成阿里云短信服务发送短信阿里云——Java实现手机短信验证码功能
2023年05月02日
323 阅读
0 评论
0 点赞
2022-09-01
排序算法重梳理
0.复杂度和稳定性汇总版本一名词解释:n:数据规模k:"桶"的个数In-place:占用常数内存,不占用额外内存Out-place:占用额外内存稳定性:排序后 2 个相等键值的顺序和排序之前它们的顺序相同版本二 其中:k表示计数排序中最大值和最小值之间的差值;l表示桶排序中桶的个数;d表示基数排序中最大值的位数,r表示是多少进制;希尔排序的时间复杂度很大程度上取决于增量gap sequence的选择,不同的增量会有不同的时间复杂度。文中使用的“gap=length/2”和“gap=gap/2”是一种常用的方式,也被称为希尔增量,但其并不是最优的。其实希尔排序增量的选择与证明一直都是个数学难题,而下图列出的是迄今为止大部分的gap sequence选择的方案:1.逐一代码实现1.1 冒泡排序每次循环都比较前后两个元素的大小,如果前者大于后者,则将两者进行交换。这样做会将每次循环中最大的元素替换到末尾,逐渐形成有序集合。将每次循环中的最大(小)元素逐渐由队首转移到队尾的过程形似“冒泡”过程,故因此得名。一个优化冒泡排序的方法就是 如果在一次循环的过程中没有发生交换,则可以立即退出当前循环,因为此时已经排好序了(也就是时间复杂度最好情况下是$O(n)$的由来)。import java.util.Arrays; class Solution { // 交换数组元素位置 public static void swap(int[] array,int index1,int index2){ int temp = array[index1]; array[index1] = array[index2]; array[index2] = temp; } public static void bubbleSort(int[] array){ for (int i = 0; i < array.length-1; i++) { boolean flag = false;//记录本轮是否发生冒泡 for (int j = 0; j < array.length-1-i; j++) { if(array[j]>array[j+1]){ swap(array,j,j+1); flag = true; } } if (!flag) { //本轮没有发生冒泡则表示数组已经有序,可以直接break break; } } } public static void main(String[] args) { int[] array = new int[]{1,9,4,8,2,3,0,7,5,6}; bubbleSort(array); System.out.println(Arrays.toString(array)); } }1.2 选择排序每次循环都会找出当前循环中最小(大)的元素,然后和此次循环中的队首元素进行交换。import java.util.Arrays; class Solution { // 交换数组元素位置 public static void swap(int[] array,int index1,int index2){ int temp = array[index1]; array[index1] = array[index2]; array[index2] = temp; } public static void selectSort(int[] array){ for (int i = 0; i < array.length; i++) { int minIndex = i; for (int j = i+1; j < array.length; j++) { minIndex = array[j]<array[minIndex]?j:minIndex; } swap(array,i,minIndex); } } public static void main(String[] args) { int[] array = new int[]{1,9,4,8,2,3,0,7,5,6}; selectSort(array); System.out.println(Arrays.toString(array)); } }1.3 快速排序import java.util.Arrays; class Solution { // 交换数组元素位置 public static void swap(int[] array,int index1,int index2){ int temp = array[index1]; array[index1] = array[index2]; array[index2] = temp; } /** * @brief 快速排序-partition * 大体思想:把比pivot小的换到前面 */ public static int partition(int[] array,int left,int right){ // 取最后一个元素作为中心元素 int pivot = array[right]; // 遍历数组中的所有元素,将比中心元素大的放在右边,比中心元素小的放在左边---该步骤有点类似于选择排序 int i = left; for (int j = left; j < right; j++) { if (array[j] <= pivot) { swap(array,i,j);//比pivot小的,全部换到前面去 i++; } } //此时,i指向的元素一定大于等于pivot,把privot换回到中间 swap(array,i,right); return i; } /** * @brief 快速排序-递归划分 */ public static void quickSort(int[] array){ int mid = partition(array,0, array.length-1); quickSort(array,0,mid-1); quickSort(array,mid+1,right); } public static void main(String[] args) { int[] array = new int[]{1,9,4,8,2,3,0,7,5,6}; quickSort(array); System.out.println(Arrays.toString(array)); } }1.4 插入排序插入排序的精髓在于每次都会在先前排好序的子集合中插入下一个待排序的元素,每次都会判断待排序元素的上一个元素是否大于待排序元素,如果大于,则将元素右移,然后判断再上一个元素与待排序元素...以此类推。直到小于等于比较元素时就是找到了该元素的插入位置。这里的等于条件放在哪里很重要,因为它是决定插入排序稳定与否的关键。import java.util.Arrays; class Solution { // 交换数组元素位置 public static void swap(int[] array,int index1,int index2){ int temp = array[index1]; array[index1] = array[index2]; array[index2] = temp; } public static void insertSort(int[] array){ for (int i = 1; i < array.length; i++) { int curItem = array[i];//缓存下一个待排序的元素 // 把有序集合中的所有比curItem大的元素都往后移一位 int j = i-1; while (j>=0&&array[j]>curItem){ array[j+1] = array[j--]; } // 把待排序元素插入到有序序列中 array[j+1] = curItem; } } public static void main(String[] args) { int[] array = new int[]{1,9,4,8,2,3,0,7,5,6}; insertSort(array); System.out.println(Arrays.toString(array)); } }1.5 希尔排序希尔排序可以认为是插入排序的改进版本。首先按照初始增量来将数组分成多个组,每个组内部使用插入排序。然后缩小增量来重新分组,组内再次使用插入排序...重复以上步骤,直到增量变为1的时候,这个时候整个数组就是一个分组,进行最后一次完整的插入排序即可结束。在排序开始时的增量较大,分组也会较多,但是每个分组中的数据较少,所以插入排序会很快。随着每一轮排序的进行,增量和分组数会逐渐变小,每个分组中的数据会逐渐变多。但因为之前已经经过了多轮的分组排序,而此时的数组会趋近于一个有序的状态,所以这个时候的排序也是很快的。而对于数据较多且趋向于无序的数据来说,如果只是使用插入排序的话效率就并不高。所以总体来说,希尔排序的执行效率是要比插入排序高的。import java.util.Arrays; class Solution { // 交换数组元素位置 public static void swap(int[] array,int index1,int index2){ int temp = array[index1]; array[index1] = array[index2]; array[index2] = temp; } public static void shellSort(int[] array){ int gap = array.length >>> 1; // 希尔排序的初始增量 while (gap>0){ for (int i = 0; i < gap; i++) { //对根据增量划分的组执行插入排序 // 一次排序一个增量组--插入排序 for (int j = i+gap; j < array.length; j+=gap) { int curItem = array[j]; // 待排序元素 int k = j - gap; while (k>=i&&array[k]>curItem){ array[k+gap] = array[k]; k-=gap; } array[k+gap] = curItem; } } gap >>>= 1; } } public static void main(String[] args) { int[] array = new int[]{1,9,4,8,2,3,0,7,5,6}; shellSort(array); System.out.println(Arrays.toString(array)); } }1.6 堆排序堆排序的过程是首先构建一个大(小)顶堆,大顶堆首先是一棵完全二叉树,其次它保证堆中任意节点的值总是不大(小)于其父节点的值。因为大顶堆中的最大元素肯定是根节点,所以每次取出根节点即为当前大顶堆中的最大元素,取出后剩下的节点再重新构建大顶堆,再取出根节点,再重新构建…重复这个过程,直到数据都被取出,最后取出的结果即为排好序的结果。import java.util.Arrays; class Solution { // 交换数组元素位置 public static void swap(int[] array,int index1,int index2){ int temp = array[index1]; array[index1] = array[index2]; array[index2] = temp; } /** * 维护堆的性质 * @param array 存储堆的数组 * @param size 堆的大小 * @param i 待维护节点的下标 */ public static void heapify(int[] array,int size,int i ){ int largest = i; int lson = i*2+1; int rson = i*2+2; if(lson<size&&array[lson]>array[largest]) largest = lson; if(rson<size&&array[rson]>array[largest]) largest = rson; if(largest!=i){ swap(array,largest,i); heapify(array,size,largest); } } public static void heapSort(int[] array,int size){ // 建堆 for (int i = size/2-1; i>=0 ; i--) { // 从最后一个元素的父节点开始维护 heapify(array,size,i); } // 排序 for (int i = size-1; i >=0 ; i--) { swap(array,i,0); // 将堆的最后一个元素和堆顶元素进行交换 heapify(array,i,0); // 将堆顶元素移出堆,并维护堆顶元素的性质 } } public static void main(String[] args) { int[] array = new int[]{1,9,4,8,2,3,0,7,5,6}; heapSort(array, array.length); System.out.println(Arrays.toString(array)); } }1.7 归并排序归并排序使用的是分治的思想,首先将数组不断拆分,直到最后拆分成两个元素的子数组,将这两个元素进行排序合并,再向上递归。不断重复这个拆分和合并的递归过程,最后得到的就是排好序的结果。import java.util.Arrays; class Solution { // 交换数组元素位置 public static void swap(int[] array,int index1,int index2){ int temp = array[index1]; array[index1] = array[index2]; array[index2] = temp; } // 合并 public static void merge(int[] array, int left, int mid, int right) { int[] temp = new int[right - left + 1]; // 临时数组 int p1 = left; // 标记左半区第一个未排序的元素 int p2 = mid + 1; // 标记右半区第一个未排序的元素 int k = 0; //临时数组元素的下标 // 合并两个有序数组 while (p1 <= mid && p2 <= right) { if (array[p1] <= array[p2]) { temp[k++] = array[p1++]; } else { temp[k++] = array[p2++]; } } // 把剩余的数组直接放到temp数组中 while (p1 <= mid) { temp[k++] = array[p1++]; } while (p2 <= right) { temp[k++] = array[p2++]; } // 复制回原数组 for (int i = 0; i < temp.length; i++) { array[i + left] = temp[i]; } } public static void mergeSort(int[] array,int left,int right){ //如果只有一个元素,那么就不需要继续划分 //只有一个元素的区域,本生就是有序的,只需要被归并即可 if(left<right){ //找中间点,这里没有选择“(left + right) / 2”的方式,是为了防止数据溢出 int mid = left + ((right - left) >>> 1); // 递归划分左半区 mergeSort(array, left, mid); // 递归划分右半区 mergeSort(array, mid + 1, right); // 对子数组进行合并 merge(array, left, mid, right); } } public static void main(String[] args) { int[] array = new int[]{1,9,4,8,2,3,0,7,5,6}; mergeSort(array,0, array.length); System.out.println(Arrays.toString(array)); } }1.8 计数排序计数排序会创建一个临时的数组,里面存放每个数出现的次数。比如一个待排序的数组是[2,4,1,2,5,3,4,8,7],那么这个临时数组中记录的数据就是[0,1,2,1,2,1,0,1,1]。那么最后只需要遍历这个临时数组中的计数值就可以了。import java.util.Arrays; class Solution { // 交换数组元素位置 public static void swap(int[] array,int index1,int index2){ int temp = array[index1]; array[index1] = array[index2]; array[index2] = temp; } public static int[] countingSort(int[] array,int left,int right){ //记录待排序数组中的最大值 int max = array[0]; //记录待排序数组中的最小值 int min = array[0]; for (int item : array) { if (item > max) max = item; if (item < min) min = item; } //记录每个数出现的次数 int[] temp = new int[max - min + 1]; for (int item : array) { temp[item - min]++; } // 将结果复制回原数组 int index = 0; for (int i = 0; i < temp.length; i++) { //当输出一个数之后,当前位置的计数就减一,直到减到0为止 while (temp[i]-- > 0) { array[index++] = i + min; } } return array; } public static void main(String[] args) { int[] array = new int[]{1,9,4,8,2,3,0,7,5,6}; countingSort(array,0, array.length); System.out.println(Arrays.toString(array)); } }1.9 桶排序上面的计数排序在数组最大值和最小值之间的差值是多少,就会生成一个多大的临时数组,也就是生成了一个这么多的桶,而每个桶中就只插入一个数据。如果差值比较大的话,会比较浪费空间。那么我能不能在一个桶中插入多个数据呢?当然可以,而这就是桶排序的思路。桶排序类似于哈希表,通过一定的映射规则将数组中的元素映射到不同的桶中,每个桶内进行内部排序,最后将每个桶按顺序输出就行了。桶排序执行的高效与否和是否是稳定的取决于哈希散列的算法以及内部排序的结果。需要注意的是,这个映射算法并不是常规的映射算法,要求是每个桶中的所有数都要比前一个桶中的所有数都要大,这样最后输出的才是一个排好序的结果。比如说第一个桶中存1-30的数字,第二个桶中存31-60的数字,第三个桶中存61-90的数字...以此类推import java.util.ArrayList; import java.util.Arrays; import java.util.Comparator; import java.util.List; class Solution { // 交换数组元素位置 public static void swap(int[] array,int index1,int index2){ int temp = array[index1]; array[index1] = array[index2]; array[index2] = temp; } public static int[] bucketSort(int[] array,int left,int right){ if (array == null || array.length < 2) { return array; } //记录待排序数组中的最大值 int max = array[0]; //记录待排序数组中的最小值 int min = array[0]; for (int i : array) { if (i > max) { max = i; } if (i < min) { min = i; } } //计算桶的数量(可以自定义实现) int bucketNumber = (max - min) / array.length + 1; List<Integer>[] buckets = new ArrayList[bucketNumber]; //计算每个桶存数的范围(可以自定义实现或者不用实现) int bucketRange = (max - min + 1) / bucketNumber; for (int value : array) { //计算应该放到哪个桶中(可以自定义实现) int bucketIndex = (value - min) / (bucketRange + 1); //延迟初始化 if (buckets[bucketIndex] == null) { buckets[bucketIndex] = new ArrayList<>(); } //放入指定的桶 buckets[bucketIndex].add(value); } int index = 0; for (List<Integer> bucket : buckets) { if (bucket == null) { continue; } //对每个桶进行内部排序,我这里使用的是快速排序,也可以使用别的排序算法,当然也可以继续递归去做桶排序 bucket.sort(new Comparator<Integer>() { @Override public int compare(Integer o1, Integer o2) { return o1-o2; } }); //将不为null的桶中的数据按顺序写回到array数组中 for (Integer integer : bucket) { array[index++] = integer; } } return array; } public static void main(String[] args) { int[] array = new int[]{1,9,4,8,2,3,0,7,5,6}; bucketSort(array,0, array.length); System.out.println(Arrays.toString(array)); } }1.10 基数排序基数排序不是根据一个数的整体来进行排序的,而是将数的每一位上的数字进行排序。比如说第一轮排序,我拿到待排序数组中所有数个位上的数字来进行排序;第二轮排序我拿到待排序数组中所有数十位上的数字来进行排序;第三轮排序我拿到待排序数组中所有数百位上的数字来进行排序...以此类推。每一轮的排序都会累加上一轮所有前几位上排序的结果,最终的结果就会是一个有序的数列。基数排序一般是对所有非负整数进行排序的,但是也可以有别的手段来去掉这种限制(比如都加一个固定的数或者都乘一个固定的数,排完序后再恢复等等)。基数排序和桶排序很像,桶排序是按数值的区间进行划分,而基数排序是按数的每一位的值进行划分。同时这两个排序都是需要依靠其他排序算法来实现的(如果不算递归调用桶排序本身的话)。基数排序每一轮的内部排序会使用到计数排序来实现,因为每一位上的数字无非就是0-9,是一个小范围的数,所以使用计数排序很合适。import java.util.ArrayList; import java.util.Arrays; import java.util.Comparator; import java.util.List; class Solution { // 交换数组元素位置 public static void swap(int[] array,int index1,int index2){ int temp = array[index1]; array[index1] = array[index2]; array[index2] = temp; } public static void radixSort(int[] array) { //定义一个二维数组,表示10个桶,每个桶就是一个一维数组 int[][] bucket = new int[10][array.length];//很明显,基数排序使用了空间换时间 //为了记录每个桶中实际存放了多少个数据,定义一个一维数组来记录每次放入数据的个数 //比如bucketElementCounts[0]=3,意思是bucket[0]存放了3个数据 int[] bucketElementCounts = new int[10]; int digitOfElement = 0;//每次取出的元素的位数 //找到数组中最大数的位数 int max = 0; for (int i = 0; i < array.length; i++) { if (max < String.valueOf(array[i]).length()) { max = String.valueOf(array[i]).length(); } } int index = 0; for (int i = 0, n = 1; i < max; i++, n *= 10) { //第i+1轮排序(针对每个元素的位进行排序处理) for (int j = 0; j < array.length; j++) { digitOfElement = array[j] / n % 10;//取出每个元素的位 bucket[digitOfElement][bucketElementCounts[digitOfElement]] = array[j];//放入对应的桶 bucketElementCounts[digitOfElement]++; } //按照桶的顺序(一维数组的下标取出数据),放入原来的数组 index = 0; //遍历每一个桶,并将桶中数据放入原数组 for (int k = 0; k < bucketElementCounts.length; k++) { //如果桶中有数据,我们才放到原数组 if (bucketElementCounts[k] != 0) { //循环第k个桶,放入 for (int l = 0; l < bucketElementCounts[k]; l++) { array[index] = bucket[k][l]; index++; } } bucketElementCounts[k] = 0;//置零!!!!! } } } public static void main(String[] args) { int[] array = new int[]{1,9,4,8,2,3,0,7,5,6}; radixSort(array); System.out.println(Arrays.toString(array)); } }参考资料排序算法:快速排序【图解+代码】排序算法:堆排序【图解+代码】[排序算法:希尔排序【图解+代码】]()排序算法:归并排序【图解+代码】十种经典排序算法总结基数排序(Java)
2022年09月01日
797 阅读
0 评论
0 点赞
2022-06-05
SpringBoot学习笔记
SpringBoot学习笔记1.SpringBoot简介1.1 回顾什么是SpringSpring是一个开源框架,2003 年兴起的一个轻量级的Java 开发框架,作者:Rod Johnson 。Spring是为了解决企业级应用开发的复杂性而创建的,简化开发。1.2 Spring是如何简化Java开发的为了降低Java开发的复杂性,Spring采用了以下4种关键策略:1、基于POJO的轻量级和最小侵入性编程,所有东西都是bean;2、通过IOC,依赖注入(DI)和面向接口实现松耦合;3、基于切面(AOP)和惯例进行声明式编程;4、通过切面和模版减少样式代码,RedisTemplate,xxxTemplate;1.3什么是SpringBoot学过javaweb的同学就知道,开发一个web应用,从最初开始接触Servlet结合Tomcat, 跑出一个Hello Wolrld程序,是要经历特别多的步骤;后来就用了框架Struts,再后来是SpringMVC,到了现在的SpringBoot,过一两年又会有其他web框架出现;你们有经历过框架不断的演进,然后自己开发项目所有的技术也在不断的变化、改造吗?建议都可以去经历一遍;言归正传,什么是SpringBoot呢,就是一个javaweb的开发框架,和SpringMVC类似,对比其他javaweb框架的好处,官方说是简化开发,约定大于配置, you can "just run",能迅速的开发web应用,几行代码开发一个http接口。所有的技术框架的发展似乎都遵循了一条主线规律:从一个复杂应用场景 衍生 一种规范框架,人们只需要进行各种配置而不需要自己去实现它,这时候强大的配置功能成了优点;发展到一定程度之后,人们根据实际生产应用情况,选取其中实用功能和设计精华,重构出一些轻量级的框架;之后为了提高开发效率,嫌弃原先的各类配置过于麻烦,于是开始提倡“约定大于配置”,进而衍生出一些一站式的解决方案。是的这就是Java企业级应用->J2EE->spring->springboot的过程。随着 Spring 不断的发展,涉及的领域越来越多,项目整合开发需要配合各种各样的文件,慢慢变得不那么易用简单,违背了最初的理念,甚至人称配置地狱。Spring Boot 正是在这样的一个背景下被抽象出来的开发框架,目的为了让大家更容易的使用 Spring 、更容易的集成各种常用的中间件、开源软件;Spring Boot 基于 Spring 开发,Spirng Boot 本身并不提供 Spring 框架的核心特性以及扩展功能,只是用于快速、敏捷地开发新一代基于 Spring 框架的应用程序。也就是说,它并不是用来替代 Spring 的解决方案,而是和 Spring 框架紧密结合用于提升 Spring 开发者体验的工具。Spring Boot 以约定大于配置的核心思想,默认帮我们进行了很多设置,多数 Spring Boot 应用只需要很少的 Spring 配置。同时它集成了大量常用的第三方库配置(例如 Redis、MongoDB、Jpa、RabbitMQ、Quartz 等等),Spring Boot 应用中这些第三方库几乎可以零配置的开箱即用。简单来说就是SpringBoot其实不是什么新的框架,它默认配置了很多框架的使用方式,就像maven整合了所有的jar包,spring boot整合了所有的框架 。Spring Boot 出生名门,从一开始就站在一个比较高的起点,又经过这几年的发展,生态足够完善,Spring Boot 已经当之无愧成为 Java 领域最热门的技术。Spring Boot的主要优点:为所有Spring开发者更快的入门开箱即用,提供各种默认配置来简化项目配置内嵌式容器简化Web项目没有冗余代码生成和XML配置的要求真的很爽,我们快速去体验开发个接口的感觉吧!2.HelloSpringBoot2.1 准备工作我们将学习如何快速的创建一个Spring Boot应用,并且实现一个简单的Http请求处理。通过这个例子对Spring Boot有一个初步的了解,并体验其结构简单、开发快速的特性。环境准备:java version "1.8.0_181"Maven-3.6.1SpringBoot 2.x 最新版开发工具:IDEA2.2 创建基础项目说明Spring官方提供了非常方便的工具让我们快速构建应用Spring Initializr:https://start.spring.io/项目创建方式一:使用Spring Initializr 的 Web页面创建项目1、打开 https://start.spring.io/2、填写项目信息3、点击”Generate Project“按钮生成项目;下载此项目4、解压项目包,并用IDEA以Maven项目导入,一路下一步即可,直到项目导入完毕。5、如果是第一次使用,可能速度会比较慢,包比较多、需要耐心等待一切就绪。项目创建方式二:使用 IDEA 直接创建项目1、创建一个新项目2、选择spring initalizr , 可以看到默认就是去官网的快速构建工具那里实现3、填写项目信息4、选择初始化的组件(初学勾选 Web 即可)5、填写项目路径6、等待项目构建成功项目结构分析:通过上面步骤完成了基础项目的创建。就会自动生成以下文件。1、程序的主启动类2、一个 application.properties 配置文件3、一个 测试类4、一个 pom.xml2.3 pom.xml 分析打开pom.xml,看看Spring Boot项目的依赖:<!-- 父依赖 --> <parent> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-parent</artifactId> <version>2.2.5.RELEASE</version> <relativePath/> </parent> <dependencies> <!-- web场景启动器 --> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <!-- springboot单元测试 --> <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> </dependencies> <build> <plugins> <!-- 打包插件 --> <plugin> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-maven-plugin</artifactId> </plugin> </plugins> </build>2.4 编写一个http接口1、在主程序的同级目录下,新建一个controller包,一定要在同级目录下,否则识别不到2、在包中新建一个HelloController类@RestController public class HelloController { @RequestMapping("/hello") public String hello() { return "Hello World"; } }3、编写完毕后,从主程序启动项目,浏览器发起请求,看页面返回;控制台输出了 Tomcat 访问的端口号!简单几步,就完成了一个web接口的开发,SpringBoot就是这么简单。所以我们常用它来建立我们的微服务项目!2.5 将项目打成jar包,点击 maven的 package如果遇到以上错误,可以配置打包时 跳过项目运行测试用例<!-- 在工作中,很多情况下我们打包是不想执行测试用例的 可能是测试用例不完事,或是测试用例会影响数据库数据 跳过测试用例执 --> <plugin> <groupId>org.apache.maven.plugins</groupId> <artifactId>maven-surefire-plugin</artifactId> <configuration> <!--跳过项目运行测试用例--> <skipTests>true</skipTests> </configuration> </plugin>如果打包成功,则会在target目录下生成一个 jar 包打成了jar包后,就可以在任何地方运行了!OK3.运行原理探究我们之前写的HelloSpringBoot,到底是怎么运行的呢,Maven项目,我们一般从pom.xml文件探究起;3.1 pom.xml父依赖其中它主要是依赖一个父项目,主要是管理项目的资源过滤及插件!<parent> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-parent</artifactId> <version>2.2.5.RELEASE</version> <relativePath/> <!-- lookup parent from repository --> </parent>点进去,发现还有一个父依赖<parent> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-dependencies</artifactId> <version>2.2.5.RELEASE</version> <relativePath>../../spring-boot-dependencies</relativePath> </parent>这里才是真正管理SpringBoot应用里面所有依赖版本的地方,SpringBoot的版本控制中心;以后我们导入依赖默认是不需要写版本;但是如果导入的包没有在依赖中管理着就需要手动配置版本了;启动器 spring-boot-starter<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency>springboot-boot-starter-xxx:就是spring-boot的场景启动器spring-boot-starter-web:帮我们导入了web模块正常运行所依赖的组件;SpringBoot将所有的功能场景都抽取出来,做成一个个的starter (启动器),只需要在项目中引入这些starter即可,所有相关的依赖都会导入进来 , 我们要用什么功能就导入什么样的场景启动器即可 ;我们未来也可以自己自定义 starter;3.2 主启动类即相关注解默认的主启动类//@SpringBootApplication 来标注一个主程序类 //说明这是一个Spring Boot应用 @SpringBootApplication public class SpringbootApplication { public static void main(String[] args) { //以为是启动了一个方法,没想到启动了一个服务 SpringApplication.run(SpringbootApplication.class, args); } }但是一个简单的启动类并不简单!我们来分析一下这些注解都干了什么@SpringBootApplication作用:标注在某个类上说明这个类是SpringBoot的主配置类 , SpringBoot就应该运行这个类的main方法来启动SpringBoot应用;进入这个注解:可以看到上面还有很多其他注解!//@SpringBootApplication 来标注一个主程序类 //说明这是一个Spring Boot应用 @SpringBootApplication public class SpringbootApplication { public static void main(String[] args) { //以为是启动了一个方法,没想到启动了一个服务 SpringApplication.run(SpringbootApplication.class, args); } }@ComponentScan这个注解在Spring中很重要 ,它对应XML配置中的元素。作用:自动扫描并加载符合条件的组件或者bean , 将这个bean定义加载到IOC容器中@SpringBootConfiguration作用:SpringBoot的配置类 ,标注在某个类上 , 表示这是一个SpringBoot的配置类;我们继续进去这个注解查看// 点进去得到下面的 @Component @Configuration public @interface SpringBootConfiguration {} @Component public @interface Configuration {}这里的 @Configuration,说明这是一个配置类 ,配置类就是对应Spring的xml 配置文件;里面的 @Component 这就说明,启动类本身也是Spring中的一个组件而已,负责启动应用!我们回到 SpringBootApplication 注解中继续看。@EnableAutoConfiguration@EnableAutoConfiguration :开启自动配置功能以前我们需要自己配置的东西,而现在SpringBoot可以自动帮我们配置 ;@EnableAutoConfiguration告诉SpringBoot开启自动配置功能,这样自动配置才能生效;点进注解接续查看:@AutoConfigurationPackage :自动配置包@Import({Registrar.class})public @interface AutoConfigurationPackage {}@import :Spring底层注解@import , 给容器中导入一个组件Registrar.class 作用:将主启动类的所在包及包下面所有子包里面的所有组件扫描到Spring容器 ;这个分析完了,退到上一步,继续看@Import({AutoConfigurationImportSelector.class}) :给容器导入组件 ;AutoConfigurationImportSelector :自动配置导入选择器,那么它会导入哪些组件的选择器呢?我们点击去这个类看源码:1、这个类中有一个这样的方法// 获得候选的配置 protected List<String> getCandidateConfigurations(AnnotationMetadata metadata, AnnotationAttributes attributes) { //这里的getSpringFactoriesLoaderFactoryClass()方法 //返回的就是我们最开始看的启动自动导入配置文件的注解类;EnableAutoConfiguration List<String> configurations = SpringFactoriesLoader.loadFactoryNames(this.getSpringFactoriesLoaderFactoryClass(), this.getBeanClassLoader()); Assert.notEmpty(configurations, "No auto configuration classes found in META-INF/spring.factories. If you are using a custom packaging, make sure that file is correct."); return configurations; }2、这个方法又调用了 SpringFactoriesLoader 类的静态方法!我们进入SpringFactoriesLoader类loadFactoryNames() 方法public static List<String> loadFactoryNames(Class<?> factoryClass, @Nullable ClassLoader classLoader) { String factoryClassName = factoryClass.getName(); //这里它又调用了 loadSpringFactories 方法 return (List)loadSpringFactories(classLoader).getOrDefault(factoryClassName, Collections.emptyList()); }3、我们继续点击查看 loadSpringFactories 方法private static Map<String, List<String>> loadSpringFactories(@Nullable ClassLoader classLoader) { //获得classLoader , 我们返回可以看到这里得到的就是EnableAutoConfiguration标注的类本身 MultiValueMap<String, String> result = (MultiValueMap)cache.get(classLoader); if (result != null) { return result; } else { try { //去获取一个资源 "META-INF/spring.factories" Enumeration<URL> urls = classLoader != null ? classLoader.getResources("META-INF/spring.factories") : ClassLoader.getSystemResources("META-INF/spring.factories"); LinkedMultiValueMap result = new LinkedMultiValueMap(); //将读取到的资源遍历,封装成为一个Properties while(urls.hasMoreElements()) { URL url = (URL)urls.nextElement(); UrlResource resource = new UrlResource(url); Properties properties = PropertiesLoaderUtils.loadProperties(resource); Iterator var6 = properties.entrySet().iterator(); while(var6.hasNext()) { Entry<?, ?> entry = (Entry)var6.next(); String factoryClassName = ((String)entry.getKey()).trim(); String[] var9 = StringUtils.commaDelimitedListToStringArray((String)entry.getValue()); int var10 = var9.length; for(int var11 = 0; var11 < var10; ++var11) { String factoryName = var9[var11]; result.add(factoryClassName, factoryName.trim()); } } } cache.put(classLoader, result); return result; } catch (IOException var13) { throw new IllegalArgumentException("Unable to load factories from location [META-INF/spring.factories]", var13); } } }4、发现一个多次出现的文件:spring.factories,全局搜索它spring.factories我们根据源头打开spring.factories , 看到了很多自动配置的文件;这就是自动配置根源所在!WebMvcAutoConfiguration我们在上面的自动配置类随便找一个打开看看,比如 :WebMvcAutoConfiguration可以看到这些一个个的都是JavaConfig配置类,而且都注入了一些Bean,可以找一些自己认识的类,看着熟悉一下!所以,自动配置真正实现是从classpath中搜寻所有的META-INF/spring.factories配置文件 ,并将其中对应的 org.springframework.boot.autoconfigure. 包下的配置项,通过反射实例化为对应标注了 @Configuration的JavaConfig形式的IOC容器配置类 , 然后将这些都汇总成为一个实例并加载到IOC容器中。结论:SpringBoot在启动的时候从类路径下的META-INF/spring.factories中获取EnableAutoConfiguration指定的值将这些值作为自动配置类导入容器 , 自动配置类就生效 , 帮我们进行自动配置工作;整个J2EE的整体解决方案和自动配置都在springboot-autoconfigure的jar包中;它会给容器中导入非常多的自动配置类 (xxxAutoConfiguration), 就是给容器中导入这个场景需要的所有组件 , 并配置好这些组件 ;有了自动配置类 , 免去了我们手动编写配置注入功能组件等的工作;现在大家应该大概的了解了下,SpringBoot的运行原理,后面我们还会深化一次!3.3 SpringApplication不简单的方法我最初以为就是运行了一个main方法,没想到却开启了一个服务;@SpringBootApplication public class SpringbootApplication { public static void main(String[] args) { SpringApplication.run(SpringbootApplication.class, args); } }SpringApplication.run分析分析该方法主要分两部分,一部分是SpringApplication的实例化,二是run方法的执行;SpringApplication这个类主要做了以下四件事情:1、推断应用的类型是普通的项目还是Web项目2、查找并加载所有可用初始化器 , 设置到initializers属性中3、找出所有的应用程序监听器,设置到listeners属性中4、推断并设置main方法的定义类,找到运行的主类查看构造器:public SpringApplication(ResourceLoader resourceLoader, Class... primarySources) { // ...... this.webApplicationType = WebApplicationType.deduceFromClasspath(); this.setInitializers(this.getSpringFactoriesInstances(); this.setListeners(this.getSpringFactoriesInstances(ApplicationListener.class)); this.mainApplicationClass = this.deduceMainApplicationClass(); }3.4 run方法流程分析4. yaml语法学习4.1 配置文件SpringBoot使用一个全局的配置文件 , 配置文件名称是固定的application.properties语法结构 :key=valueapplication.yml语法结构 :key:空格 value配置文件的作用 :修改SpringBoot自动配置的默认值,因为SpringBoot在底层都给我们自动配置好了;比如我们可以在配置文件中修改Tomcat 默认启动的端口号!测试一下!server.port=80814.2 yaml概述YAML是 "YAML Ain't a Markup Language" (YAML不是一种标记语言)的递归缩写。在开发的这种语言时,YAML 的意思其实是:"Yet Another Markup Language"(仍是一种标记语言)这种语言以数据作为中心,而不是以标记语言为重点!以前的配置文件,大多数都是使用xml来配置;比如一个简单的端口配置,我们来对比下yaml和xml传统xml配置:<server> <port>8081<port> </server>yaml配置:server: prot: 80804.3 yaml基础语法说明:语法要求严格!1、空格不能省略2、以缩进来控制层级关系,只要是左边对齐的一列数据都是同一个层级的。3、属性和值的大小写都是十分敏感的。字面量:普通的值 [ 数字,布尔值,字符串 ]字面量直接写在后面就可以 , 字符串默认不用加上双引号或者单引号;k: v注意:“ ” 双引号,不会转义字符串里面的特殊字符 , 特殊字符会作为本身想表示的意思;比如 :name: "kuang \n shen" 输出 :kuang 换行 shen'' 单引号,会转义特殊字符 , 特殊字符最终会变成和普通字符一样输出比如 :name: ‘kuang \n shen’ 输出 :kuang \n shen对象、Map(键值对)#对象、Map格式 k: v1: v2:在下一行来写对象的属性和值得关系,注意缩进;比如:student: name: qinjiang age: 3行内写法student: {name: qinjiang,age: 3}数组( List、set )用 - 值表示数组中的一个元素,比如:pets: - cat - dog - pig行内写法pets: [cat,dog,pig]修改SpringBoot的默认端口号配置文件中添加,端口号的参数,就可以切换端口;server: port: 80825.注入配置文件yaml文件更强大的地方在于,他可以给我们的实体类直接注入匹配值!5.1 yaml注入配置文件1、在springboot项目中的resources目录下新建一个文件 application.yml2、编写一个实体类 Dog;package com.kuang.springboot.pojo; @Component //注册bean到容器中 public class Dog { private String name; private Integer age; //有参无参构造、get、set方法、toString()方法 }3、思考,我们原来是如何给bean注入属性值的!@Value,给狗狗类测试一下:@Component //注册bean public class Dog { @Value("阿黄") private String name; @Value("18") private Integer age; }4、在SpringBoot的测试类下注入狗狗输出一下;@SpringBootTest class DemoApplicationTests { @Autowired //将狗狗自动注入进来 Dog dog; @Test public void contextLoads() { System.out.println(dog); //打印看下狗狗对象 } }结果成功输出,@Value注入成功,这是我们原来的办法对吧。5、我们在编写一个复杂一点的实体类:Person 类@Component //注册bean到容器中 public class Person { private String name; private Integer age; private Boolean happy; private Date birth; private Map<String,Object> maps; private List<Object> lists; private Dog dog; //有参无参构造、get、set方法、toString()方法 }6、我们来使用yaml配置的方式进行注入,大家写的时候注意区别和优势,我们编写一个yaml配置!person: name: qinjiang age: 3 happy: false birth: 2000/01/01 maps: {k1: v1,k2: v2} lists: - code - girl - music dog: name: 旺财 age: 17、我们刚才已经把person这个对象的所有值都写好了,我们现在来注入到我们的类中!/* @ConfigurationProperties作用: 将配置文件中配置的每一个属性的值,映射到这个组件中; 告诉SpringBoot将本类中的所有属性和配置文件中相关的配置进行绑定 参数 prefix = “person” : 将配置文件中的person下面的所有属性一一对应 */ @Component //注册bean @ConfigurationProperties(prefix = "person") public class Person { private String name; private Integer age; private Boolean happy; private Date birth; private Map<String,Object> maps; private List<Object> lists; private Dog dog; }8、IDEA 提示,springboot配置注解处理器没有找到,让我们看文档,我们可以查看文档,找到一个依赖!<!-- 导入配置文件处理器,配置文件进行绑定就会有提示,需要重启 --> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-configuration-processor</artifactId> <optional>true</optional> </dependency>9、确认以上配置都OK之后,我们去测试类中测试一下:@SpringBootTest class DemoApplicationTests { @Autowired Person person; //将person自动注入进来 @Test public void contextLoads() { System.out.println(person); //打印person信息 } }结果:所有值全部注入成功!yaml配置注入到实体类完全OK!课堂测试:1、将配置文件的key 值 和 属性的值设置为不一样,则结果输出为null,注入失败2、在配置一个person2,然后将 @ConfigurationProperties(prefix = "person2") 指向我们的person2;5.2 加载指定的配置文件@PropertySource :加载指定的配置文件;@configurationProperties:默认从全局配置文件中获取值;1、我们去在resources目录下新建一个person.properties文件name=kuangshen2、然后在我们的代码中指定加载person.properties文件@PropertySource(value = "classpath:person.properties") @Component //注册bean public class Person { @Value("${name}") private String name; ...... }3、再次输出测试一下:指定配置文件绑定成功!5.3 配置文件占位符配置文件还可以编写占位符生成随机数person: name: qinjiang${random.uuid} # 随机uuid age: ${random.int} # 随机int happy: false birth: 2000/01/01 maps: {k1: v1,k2: v2} lists: - code - girl - music dog: name: ${person.hello:other}_旺财 age: 15.4 回顾properties配置我们上面采用的yaml方法都是最简单的方式,开发中最常用的;也是springboot所推荐的!那我们来唠唠其他的实现方式,道理都是相同的;写还是那样写;配置文件除了yml还有我们之前常用的properties , 我们没有讲,我们来唠唠!【注意】properties配置文件在写中文的时候,会有乱码 , 我们需要去IDEA中设置编码格式为UTF-8;settings-->FileEncodings 中配置;测试步骤:1、新建一个实体类User@Component //注册bean public class User { private String name; private int age; private String sex; }2、编辑配置文件 user.propertiesuser1.name=kuangshen user1.age=18 user1.sex=男3、我们在User类上使用@Value来进行注入!@Component //注册bean @PropertySource(value = "classpath:user.properties") public class User { //直接使用@value @Value("${user.name}") //从配置文件中取值 private String name; @Value("#{9*2}") // #{SPEL} Spring表达式 private int age; @Value("男") // 字面量 private String sex; }4、Springboot测试@SpringBootTest class DemoApplicationTests { @Autowired User user; @Test public void contextLoads() { System.out.println(user); } }结果正常输出:5.5 对比小结@Value这个使用起来并不友好!我们需要为每个属性单独注解赋值,比较麻烦;我们来看个功能对比图1、@ConfigurationProperties只需要写一次即可 , @Value则需要每个字段都添加2、松散绑定:这个什么意思呢? 比如我的yml中写的last-name,这个和lastName是一样的, - 后面跟着的字母默认是大写的。这就是松散绑定。可以测试一下3、JSR303数据校验 , 这个就是我们可以在字段是增加一层过滤器验证 , 可以保证数据的合法性4、复杂类型封装,yml中可以封装对象 , 使用value就不支持结论:配置yml和配置properties都可以获取到值 , 强烈推荐 yml;如果我们在某个业务中,只需要获取配置文件中的某个值,可以使用一下 @value;如果说,我们专门编写了一个JavaBean来和配置文件进行一一映射,就直接@configurationProperties,不要犹豫!6.JSR303数据校验6.1 先看看如何使用Springboot中可以用@validated来校验数据,如果数据异常则会统一抛出异常,方便异常中心统一处理。我们这里来写个注解让我们的name只能支持Email格式;@Component //注册bean @ConfigurationProperties(prefix = "person") @Validated //数据校验 public class Person { @Email(message="邮箱格式错误") //name必须是邮箱格式 private String name; }运行结果 :default message [不是一个合法的电子邮件地址];使用数据校验,可以保证数据的正确性;6.2 常见参数@NotNull(message="名字不能为空") private String userName; @Max(value=120,message="年龄最大不能查过120") private int age; @Email(message="邮箱格式错误") private String email; 空检查 @Null 验证对象是否为null @NotNull 验证对象是否不为null, 无法查检长度为0的字符串 @NotBlank 检查约束字符串是不是Null还有被Trim的长度是否大于0,只对字符串,且会去掉前后空格. @NotEmpty 检查约束元素是否为NULL或者是EMPTY. Booelan检查 @AssertTrue 验证 Boolean 对象是否为 true @AssertFalse 验证 Boolean 对象是否为 false 长度检查 @Size(min=, max=) 验证对象(Array,Collection,Map,String)长度是否在给定的范围之内 @Length(min=, max=) string is between min and max included. 日期检查 @Past 验证 Date 和 Calendar 对象是否在当前时间之前 @Future 验证 Date 和 Calendar 对象是否在当前时间之后 @Pattern 验证 String 对象是否符合正则表达式的规则 .......等等 除此以外,我们还可以自定义一些数据校验规则7.多环境切换profile是Spring对不同环境提供不同配置功能的支持,可以通过激活不同的环境版本,实现快速切换环境;7.1 多配置文件我们在主配置文件编写的时候,文件名可以是 application-{profile}.properties/yml , 用来指定多个环境版本;例如:application-test.properties 代表测试环境配置application-dev.properties 代表开发环境配置但是Springboot并不会直接启动这些配置文件,它默认使用application.properties主配置文件;我们需要通过一个配置来选择需要激活的环境:#比如在配置文件中指定使用dev环境,我们可以通过设置不同的端口号进行测试; #我们启动SpringBoot,就可以看到已经切换到dev下的配置了; spring.profiles.active=dev7.2 yaml的多文档块和properties配置文件中一样,但是使用yml去实现不需要创建多个配置文件,更加方便了 !server: port: 8081 #选择要激活那个环境块 spring: profiles: active: prod --- server: port: 8083 spring: profiles: dev #配置环境的名称 --- server: port: 8084 spring: profiles: prod #配置环境的名称注意:如果yml和properties同时都配置了端口,并且没有激活其他环境 , 默认会使用properties配置文件的!7.3 配置文件加载位置外部加载配置文件的方式十分多,我们选择最常用的即可,在开发的资源文件中进行配置!官方外部配置文件说明参考文档springboot 启动会扫描以下位置的application.properties或者application.yml文件作为Spring boot的默认配置文件:优先级1:项目路径下的config文件夹配置文件 优先级2:项目路径下配置文件 优先级3:资源路径下的config文件夹配置文件 优先级4:资源路径下配置文件优先级由高到底,高优先级的配置会覆盖低优先级的配置;SpringBoot会从这四个位置全部加载主配置文件;互补配置;我们在最低级的配置文件中设置一个项目访问路径的配置来测试互补问题;#配置项目的访问路径 server.servlet.context-path=/kuang7.4 拓展,运维小技巧指定位置加载配置文件我们还可以通过spring.config.location来改变默认的配置文件位置项目打包好以后,我们可以使用命令行参数的形式,启动项目的时候来指定配置文件的新位置;这种情况,一般是后期运维做的多,相同配置,外部指定的配置文件优先级最高java -jar spring-boot-config.jar --spring.config.location=F:/application.properties8.自动配置原理配置文件到底能写什么?怎么写?SpringBoot官方文档中有大量的配置,我们无法全部记住8.1 分析自动配置原理我们以HttpEncodingAutoConfiguration(Http编码自动配置)为例解释自动配置原理;//表示这是一个配置类,和以前编写的配置文件一样,也可以给容器中添加组件; @Configuration //启动指定类的ConfigurationProperties功能; //进入这个HttpProperties查看,将配置文件中对应的值和HttpProperties绑定起来; //并把HttpProperties加入到ioc容器中 @EnableConfigurationProperties({HttpProperties.class}) //Spring底层@Conditional注解 //根据不同的条件判断,如果满足指定的条件,整个配置类里面的配置就会生效; //这里的意思就是判断当前应用是否是web应用,如果是,当前配置类生效 @ConditionalOnWebApplication( type = Type.SERVLET ) //判断当前项目有没有这个类CharacterEncodingFilter;SpringMVC中进行乱码解决的过滤器; @ConditionalOnClass({CharacterEncodingFilter.class}) //判断配置文件中是否存在某个配置:spring.http.encoding.enabled; //如果不存在,判断也是成立的 //即使我们配置文件中不配置pring.http.encoding.enabled=true,也是默认生效的; @ConditionalOnProperty( prefix = "spring.http.encoding", value = {"enabled"}, matchIfMissing = true ) public class HttpEncodingAutoConfiguration { //他已经和SpringBoot的配置文件映射了 private final Encoding properties; //只有一个有参构造器的情况下,参数的值就会从容器中拿 public HttpEncodingAutoConfiguration(HttpProperties properties) { this.properties = properties.getEncoding(); } //给容器中添加一个组件,这个组件的某些值需要从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; } //。。。。。。。 }一句话总结 :根据当前不同的条件判断,决定这个配置类是否生效!一但这个配置类生效;这个配置类就会给容器中添加各种组件;这些组件的属性是从对应的properties类中获取的,这些类里面的每一个属性又是和配置文件绑定的;所有在配置文件中能配置的属性都是在xxxxProperties类中封装着;配置文件能配置什么就可以参照某个功能对应的这个属性类//从配置文件中获取指定的值和bean的属性进行绑定 @ConfigurationProperties(prefix = "spring.http") public class HttpProperties { // ..... }我们去配置文件里面试试前缀,看提示!这就是自动装配的原理!8.2 精髓1、SpringBoot启动会加载大量的自动配置类2、我们看我们需要的功能有没有在SpringBoot默认写好的自动配置类当中;3、我们再来看这个自动配置类中到底配置了哪些组件;(只要我们要用的组件存在在其中,我们就不需要再手动配置了)4、给容器中自动配置类添加组件的时候,会从properties类中获取某些属性。我们只需要在配置文件中指定这些属性的值即可;xxxxAutoConfigurartion:自动配置类;给容器中添加组件xxxxProperties:封装配置文件中相关属性;8.3 了解:@Conditional了解完自动装配的原理后,我们来关注一个细节问题,自动配置类必须在一定的条件下才能生效;@Conditional派生注解(Spring注解版原生的@Conditional作用)作用:必须是@Conditional指定的条件成立,才给容器中添加组件,配置配里面的所有内容才生效;那么多的自动配置类,必须在一定的条件下才能生效;也就是说,我们加载了这么多的配置类,但不是所有的都生效了。我们怎么知道哪些自动配置类生效?我们可以通过启用 debug=true属性;来让控制台打印自动配置报告,这样我们就可以很方便的知道哪些自动配置类生效;#开启springboot的调试类 debug=truePositive matches:(自动配置类启用的:正匹配)Negative matches:(没有启动,没有匹配成功的自动配置类:负匹配)Unconditional classes: (没有条件的类)【演示:查看输出的日志】9.整合JDBC9.1 SpringData简介对于数据访问层,无论是 SQL(关系型数据库) 还是 NOSQL(非关系型数据库),Spring Boot 底层都是采用 Spring Data 的方式进行统一处理。Spring Boot 底层都是采用 Spring Data 的方式进行统一处理各种数据库,Spring Data 也是 Spring 中与 Spring Boot、Spring Cloud 等齐名的知名项目。Sping Data 官网:https://spring.io/projects/spring-data数据库相关的启动器 :可以参考官方文档:https://docs.spring.io/spring-boot/docs/2.2.5.RELEASE/reference/htmlsingle/#using-boot-starter9.2 创建测试项目测试数据源1、我去新建一个项目测试:springboot-data-jdbc ; 引入相应的模块!基础模块2、项目建好之后,发现自动帮我们导入了如下的启动器:<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-jdbc</artifactId> </dependency> <dependency> <groupId>mysql</groupId> <artifactId>mysql-connector-java</artifactId> <scope>runtime</scope> </dependency>3、编写yaml配置文件连接数据库;spring: datasource: username: root password: 123456 #?serverTimezone=UTC解决时区的报错 url: jdbc:mysql://localhost:3306/springboot?serverTimezone=UTC&useUnicode=true&characterEncoding=utf-8 driver-class-name: com.mysql.cj.jdbc.Driver4、配置完这一些东西后,我们就可以直接去使用了,因为SpringBoot已经默认帮我们进行了自动配置;去测试类测试一下@SpringBootTest class SpringbootDataJdbcApplicationTests { //DI注入数据源 @Autowired DataSource dataSource; @Test public void contextLoads() throws SQLException { //看一下默认数据源 System.out.println(dataSource.getClass()); //获得连接 Connection connection = dataSource.getConnection(); System.out.println(connection); //关闭连接 connection.close(); } }结果:我们可以看到他默认给我们配置的数据源为 : class com.zaxxer.hikari.HikariDataSource , 我们并没有手动配置我们来全局搜索一下,找到数据源的所有自动配置都在 :DataSourceAutoConfiguration文件:@Import( {Hikari.class, Tomcat.class, Dbcp2.class, Generic.class, DataSourceJmxConfiguration.class} ) protected static class PooledDataSourceConfiguration { protected PooledDataSourceConfiguration() { } }这里导入的类都在 DataSourceConfiguration 配置类下,可以看出 Spring Boot 2.2.5 默认使用HikariDataSource 数据源,而以前版本,如 Spring Boot 1.5 默认使用 org.apache.tomcat.jdbc.pool.DataSource 作为数据源;HikariDataSource 号称 Java WEB 当前速度最快的数据源,相比于传统的 C3P0 、DBCP、Tomcat jdbc 等连接池更加优秀;可以使用 spring.datasource.type 指定自定义的数据源类型,值为 要使用的连接池实现的完全限定名。9.3 JDBCTemplate1、有了数据源(com.zaxxer.hikari.HikariDataSource),然后可以拿到数据库连接(java.sql.Connection),有了连接,就可以使用原生的 JDBC 语句来操作数据库;2、即使不使用第三方第数据库操作框架,如 MyBatis等,Spring 本身也对原生的JDBC 做了轻量级的封装,即JdbcTemplate。3、数据库操作的所有 CRUD 方法都在 JdbcTemplate 中。4、Spring Boot 不仅提供了默认的数据源,同时默认已经配置好了 JdbcTemplate 放在了容器中,程序员只需自己注入即可使用5、JdbcTemplate 的自动配置是依赖 org.springframework.boot.autoconfigure.jdbc 包下的 JdbcTemplateConfiguration 类JdbcTemplate主要提供以下几类方法:execute方法:可以用于执行任何SQL语句,一般用于执行DDL语句;update方法及batchUpdate方法:update方法用于执行新增、修改、删除等语句;batchUpdate方法用于执行批处理相关语句;query方法及queryForXXX方法:用于执行查询相关语句;call方法:用于执行存储过程、函数相关语句。9.4 测试编写一个Controller,注入 jdbcTemplate,编写测试方法进行访问测试;package com.kuang.controller; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.jdbc.core.JdbcTemplate; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.PathVariable; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; import java.util.Date; import java.util.List; import java.util.Map; @RestController @RequestMapping("/jdbc") public class JdbcController { /** * Spring Boot 默认提供了数据源,默认提供了 org.springframework.jdbc.core.JdbcTemplate * JdbcTemplate 中会自己注入数据源,用于简化 JDBC操作 * 还能避免一些常见的错误,使用起来也不用再自己来关闭数据库连接 */ @Autowired JdbcTemplate jdbcTemplate; //查询employee表中所有数据 //List 中的1个 Map 对应数据库的 1行数据 //Map 中的 key 对应数据库的字段名,value 对应数据库的字段值 @GetMapping("/list") public List<Map<String, Object>> userList(){ String sql = "select * from employee"; List<Map<String, Object>> maps = jdbcTemplate.queryForList(sql); return maps; } //新增一个用户 @GetMapping("/add") public String addUser(){ //插入语句,注意时间问题 String sql = "insert into employee(last_name, email,gender,department,birth)" + " values ('狂神说','24736743@qq.com',1,101,'"+ new Date().toLocaleString() +"')"; jdbcTemplate.update(sql); //查询 return "addOk"; } //修改用户信息 @GetMapping("/update/{id}") public String updateUser(@PathVariable("id") int id){ //插入语句 String sql = "update employee set last_name=?,email=? where id="+id; //数据 Object[] objects = new Object[2]; objects[0] = "秦疆"; objects[1] = "24736743@sina.com"; jdbcTemplate.update(sql,objects); //查询 return "updateOk"; } //删除用户 @GetMapping("/delete/{id}") public String delUser(@PathVariable("id") int id){ //插入语句 String sql = "delete from employee where id=?"; jdbcTemplate.update(sql,id); //查询 return "deleteOk"; } }测试请求,结果正常;10.整合Druid10.1 Druid简介Java程序很大一部分要操作数据库,为了提高性能操作数据库的时候,又不得不使用数据库连接池。Druid 是阿里巴巴开源平台上一个数据库连接池实现,结合了 C3P0、DBCP 等 DB 池的优点,同时加入了日志监控。Druid 可以很好的监控 DB 池连接和 SQL 的执行情况,天生就是针对监控而生的 DB 连接池。Druid已经在阿里巴巴部署了超过600个应用,经过一年多生产环境大规模部署的严苛考验。Spring Boot 2.0 以上默认使用 Hikari 数据源,可以说 Hikari 与 Driud 都是当前 Java Web 上最优秀的数据源,我们来重点介绍 Spring Boot 如何集成 Druid 数据源,如何实现数据库监控。Github地址:https://github.com/alibaba/druid/com.alibaba.druid.pool.DruidDataSource 基本配置参数如下:10.2 配置数据源1、添加上 Druid 数据源依赖。<!-- https://mvnrepository.com/artifact/com.alibaba/druid --> <dependency> <groupId>com.alibaba</groupId> <artifactId>druid</artifactId> <version>1.1.21</version> </dependency>2、切换数据源;之前已经说过 Spring Boot 2.0 以上默认使用 com.zaxxer.hikari.HikariDataSource 数据源,但可以 通过 spring.datasource.type 指定数据源。spring: datasource: username: root password: 123456 url: jdbc:mysql://localhost:3306/springboot?serverTimezone=UTC&useUnicode=true&characterEncoding=utf-8 driver-class-name: com.mysql.cj.jdbc.Driver type: com.alibaba.druid.pool.DruidDataSource # 自定义数据源3、数据源切换之后,在测试类中注入 DataSource,然后获取到它,输出一看便知是否成功切换;4、切换成功!既然切换成功,就可以设置数据源连接初始化大小、最大连接数、等待时间、最小连接数 等设置项;可以查看源码 spring: datasource: username: root password: 123456 #?serverTimezone=UTC解决时区的报错 url: jdbc:mysql://localhost:3306/springboot?serverTimezone=UTC&useUnicode=true&characterEncoding=utf-8 driver-class-name: com.mysql.cj.jdbc.Driver type: com.alibaba.druid.pool.DruidDataSource #Spring Boot 默认是不注入这些属性值的,需要自己绑定 #druid 数据源专有配置 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,stat:监控统计、log4j:日志记录、wall:防御sql注入 #如果允许时报错 java.lang.ClassNotFoundException: org.apache.log4j.Priority #则导入 log4j 依赖即可,Maven 地址:https://mvnrepository.com/artifact/log4j/log4j filters: stat,wall,log4j maxPoolPreparedStatementPerConnectionSize: 20 useGlobalDataSourceStat: true connectionProperties: druid.stat.mergeSql=true;druid.stat.slowSqlMillis=5005、导入Log4j 的依赖<!-- https://mvnrepository.com/artifact/log4j/log4j --> <dependency> <groupId>log4j</groupId> <artifactId>log4j</artifactId> <version>1.2.17</version> </dependency>6、现在需要程序员自己为 DruidDataSource 绑定全局配置文件中的参数,再添加到容器中,而不再使用 Spring Boot 的自动生成了;我们需要 自己添加 DruidDataSource 组件到容器中,并绑定属性;package com.kuang.config; import com.alibaba.druid.pool.DruidDataSource; import org.springframework.boot.context.properties.ConfigurationProperties; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import javax.sql.DataSource; @Configuration public class DruidConfig { /* 将自定义的 Druid数据源添加到容器中,不再让 Spring Boot 自动创建 绑定全局配置文件中的 druid 数据源属性到 com.alibaba.druid.pool.DruidDataSource从而让它们生效 @ConfigurationProperties(prefix = "spring.datasource"):作用就是将 全局配置文件中 前缀为 spring.datasource的属性值注入到 com.alibaba.druid.pool.DruidDataSource 的同名参数中 */ @ConfigurationProperties(prefix = "spring.datasource") @Bean public DataSource druidDataSource() { return new DruidDataSource(); } }7、去测试类中测试一下;看是否成功!@SpringBootTest class SpringbootDataJdbcApplicationTests { //DI注入数据源 @Autowired DataSource dataSource; @Test public void contextLoads() throws SQLException { //看一下默认数据源 System.out.println(dataSource.getClass()); //获得连接 Connection connection = dataSource.getConnection(); System.out.println(connection); DruidDataSource druidDataSource = (DruidDataSource) dataSource; System.out.println("druidDataSource 数据源最大连接数:" + druidDataSource.getMaxActive()); System.out.println("druidDataSource 数据源初始化连接数:" + druidDataSource.getInitialSize()); //关闭连接 connection.close(); } }输出结果 :可见配置参数已经生效!10.3 配置Druid数据源监控Druid 数据源具有监控的功能,并提供了一个 web 界面方便用户查看,类似安装 路由器 时,人家也提供了一个默认的 web 页面。所以第一步需要设置 Druid 的后台管理页面,比如 登录账号、密码 等;配置后台管理; //配置 Druid 监控管理后台的Servlet; //内置 Servlet 容器时没有web.xml文件,所以使用 Spring Boot 的注册 Servlet 方式 @Bean public ServletRegistrationBean statViewServlet() { ServletRegistrationBean bean = new ServletRegistrationBean(new StatViewServlet(), "/druid/*"); // 这些参数可以在 com.alibaba.druid.support.http.StatViewServlet // 的父类 com.alibaba.druid.support.http.ResourceServlet 中找到 Map<String, String> initParams = new HashMap<>(); initParams.put("loginUsername", "admin"); //后台管理界面的登录账号 initParams.put("loginPassword", "123456"); //后台管理界面的登录密码 //后台允许谁可以访问 //initParams.put("allow", "localhost"):表示只有本机可以访问 //initParams.put("allow", ""):为空或者为null时,表示允许所有访问 initParams.put("allow", ""); //deny:Druid 后台拒绝谁访问 //initParams.put("kuangshen", "192.168.1.20");表示禁止此ip访问 //设置初始化参数 bean.setInitParameters(initParams); return bean; }配置完毕后,我们可以选择访问 :http://localhost:8080/druid/login.html进入之后配置 Druid web 监控 filter 过滤器//配置 Druid 监控 之 web 监控的 filter //WebStatFilter:用于配置Web和Druid数据源之间的管理关联监控统计 @Bean public FilterRegistrationBean webStatFilter() { FilterRegistrationBean bean = new FilterRegistrationBean(); bean.setFilter(new WebStatFilter()); //exclusions:设置哪些请求进行过滤排除掉,从而不进行统计 Map<String, String> initParams = new HashMap<>(); initParams.put("exclusions", "*.js,*.css,/druid/*,/jdbc/*"); bean.setInitParameters(initParams); //"/*" 表示过滤所有请求 bean.setUrlPatterns(Arrays.asList("/*")); return bean; }平时在工作中,按需求进行配置即可,主要用作监控!11.整合MyBatis11.1 整合测试1、导入 MyBatis 所需要的依赖<dependency> <groupId>org.mybatis.spring.boot</groupId> <artifactId>mybatis-spring-boot-starter</artifactId> <version>2.1.1</version> </dependency>2、配置数据库连接信息(不变)spring: datasource: username: root password: 123456 #?serverTimezone=UTC解决时区的报错 url: jdbc:mysql://localhost:3306/springboot?serverTimezone=UTC&useUnicode=true&characterEncoding=utf-8 driver-class-name: com.mysql.cj.jdbc.Driver type: com.alibaba.druid.pool.DruidDataSource #Spring Boot 默认是不注入这些属性值的,需要自己绑定 #druid 数据源专有配置 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,stat:监控统计、log4j:日志记录、wall:防御sql注入 #如果允许时报错 java.lang.ClassNotFoundException: org.apache.log4j.Priority #则导入 log4j 依赖即可,Maven 地址:https://mvnrepository.com/artifact/log4j/log4j filters: stat,wall,log4j maxPoolPreparedStatementPerConnectionSize: 20 useGlobalDataSourceStat: true connectionProperties: druid.stat.mergeSql=true;druid.stat.slowSqlMillis=5003、测试数据库是否连接成功!4、创建实体类,导入 Lombok!Department.javapackage com.kuang.pojo; import lombok.AllArgsConstructor; import lombok.Data; import lombok.NoArgsConstructor; @Data @NoArgsConstructor @AllArgsConstructor public class Department { private Integer id; private String departmentName; }5、创建mapper目录以及对应的 Mapper 接口DepartmentMapper.java //@Mapper : 表示本类是一个 MyBatis 的 Mapper @Mapper @Repository public interface DepartmentMapper { // 获取所有部门信息 List<Department> getDepartments(); // 通过id获得部门 Department getDepartment(Integer id); }6、对应的Mapper映射文件DepartmentMapper.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.kuang.mapper.DepartmentMapper"> <select id="getDepartments" resultType="Department"> select * from department; </select> <select id="getDepartment" resultType="Department" parameterType="int"> select * from department where id = #{id}; </select> </mapper>7、maven配置资源过滤问题<resources> <resource> <directory>src/main/java</directory> <includes> <include>**/*.xml</include> </includes> <filtering>true</filtering> </resource> </resources>8、编写部门的 DepartmentController 进行测试!@RestController public class DepartmentController { @Autowired DepartmentMapper departmentMapper; // 查询全部部门 @GetMapping("/getDepartments") public List<Department> getDepartments(){ return departmentMapper.getDepartments(); } // 查询全部部门 @GetMapping("/getDepartment/{id}") public Department getDepartment(@PathVariable("id") Integer id){ return departmentMapper.getDepartment(id); } }启动项目访问进行测试!11.2 增加一个员工类再测试下,为之后做准备1、新建一个pojo类 Employee ;@Data @AllArgsConstructor @NoArgsConstructor public class Employee { private Integer id; private String lastName; private String email; //1 male, 0 female private Integer gender; private Integer department; private Date birth; private Department eDepartment; // 冗余设计 }2、新建一个 EmployeeMapper 接口//@Mapper : 表示本类是一个 MyBatis 的 Mapper @Mapper @Repository public interface EmployeeMapper { // 获取所有员工信息 List<Employee> getEmployees(); // 新增一个员工 int save(Employee employee); // 通过id获得员工信息 Employee get(Integer id); // 通过id删除员工 int delete(Integer id); }3、编写 EmployeeMapper.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.kuang.mapper.EmployeeMapper"> <resultMap id="EmployeeMap" type="Employee"> <id property="id" column="eid"/> <result property="lastName" column="last_name"/> <result property="email" column="email"/> <result property="gender" column="gender"/> <result property="birth" column="birth"/> <association property="eDepartment" javaType="Department"> <id property="id" column="did"/> <result property="departmentName" column="dname"/> </association> </resultMap> <select id="getEmployees" resultMap="EmployeeMap"> select e.id as eid,last_name,email,gender,birth,d.id as did,d.department_name as dname from department d,employee e where d.id = e.department </select> <insert id="save" parameterType="Employee"> insert into employee (last_name,email,gender,department,birth) values (#{lastName},#{email},#{gender},#{department},#{birth}); </insert> <select id="get" resultType="Employee"> select * from employee where id = #{id} </select> <delete id="delete" parameterType="int"> delete from employee where id = #{id} </delete> </mapper>4、编写EmployeeController类进行测试@RestController public class EmployeeController { @Autowired EmployeeMapper employeeMapper; // 获取所有员工信息 @GetMapping("/getEmployees") public List<Employee> getEmployees(){ return employeeMapper.getEmployees(); } @GetMapping("/save") public int save(){ Employee employee = new Employee(); employee.setLastName("kuangshen"); employee.setEmail("qinjiang@qq.com"); employee.setGender(1); employee.setDepartment(101); employee.setBirth(new Date()); return employeeMapper.save(employee); } // 通过id获得员工信息 @GetMapping("/get/{id}") public Employee get(@PathVariable("id") Integer id){ return employeeMapper.get(id); } // 通过id删除员工 @GetMapping("/delete/{id}") public int delete(@PathVariable("id") Integer id){ return employeeMapper.delete(id); } }12.Web开发静态资源处理12.1 简介1、创建一个SpringBoot应用,选择我们需要的模块,SpringBoot就会默认将我们的需要的模块自动配置好2、手动在配置文件中配置部分配置项目就可以运行起来了3、专注编写业务代码,不需要考虑以前那样一大堆的配置了。要熟悉掌握开发,之前学习的自动配置的原理一定要搞明白!比如SpringBoot到底帮我们配置了什么?我们能不能修改?我们能修改哪些配置?我们能不能扩展?向容器中自动配置组件 : Autoconfiguration*自动配置类,封装配置文件的内容:Properties*没事就找找类,看看自动装配原理!我们之后来进行一个单体项目的小项目测试,让大家能够快速上手开发!12.2 静态资源映射规则首先,我们搭建一个普通的SpringBoot项目,回顾一下HelloWorld程序!写请求非常简单,那我们要引入我们前端资源,我们项目中有许多的静态资源,比如css,js等文件,这个SpringBoot怎么处理呢?如果我们是一个web应用,我们的main下会有一个webapp,我们以前都是将所有的页面导在这里面的,对吧!但是我们现在的pom呢,打包方式是为jar的方式,那么这种方式SpringBoot能不能来给我们写页面呢?当然是可以的,但是SpringBoot对于静态资源放置的位置,是有规定的!我们先来聊聊这个静态资源映射规则:SpringBoot中,SpringMVC的web配置都在 WebMvcAutoConfiguration 这个配置类里面;我们可以去看看 WebMvcAutoConfigurationAdapter 中有很多配置方法;有一个方法:addResourceHandlers 添加资源处理@Override public void addResourceHandlers(ResourceHandlerRegistry registry) { if (!this.resourceProperties.isAddMappings()) { // 已禁用默认资源处理 logger.debug("Default resource handling disabled"); return; } // 缓存控制 Duration cachePeriod = this.resourceProperties.getCache().getPeriod(); CacheControl cacheControl = this.resourceProperties.getCache().getCachecontrol().toHttpCacheControl(); // webjars 配置 if (!registry.hasMappingForPattern("/webjars/**")) { customizeResourceHandlerRegistration(registry.addResourceHandler("/webjars/**") .addResourceLocations("classpath:/META-INF/resources/webjars/") .setCachePeriod(getSeconds(cachePeriod)).setCacheControl(cacheControl)); } // 静态资源配置 String staticPathPattern = this.mvcProperties.getStaticPathPattern(); if (!registry.hasMappingForPattern(staticPathPattern)) { customizeResourceHandlerRegistration(registry.addResourceHandler(staticPathPattern) .addResourceLocations(getResourceLocations(this.resourceProperties.getStaticLocations())) .setCachePeriod(getSeconds(cachePeriod)).setCacheControl(cacheControl)); } }读一下源代码:比如所有的 /webjars/** , 都需要去 classpath:/META-INF/resources/webjars/ 找对应的资源;12.3 什么是webjars 呢?Webjars本质就是以jar包的方式引入我们的静态资源 , 我们以前要导入一个静态资源文件,直接导入即可。使用SpringBoot需要使用Webjars,我们可以去搜索一下:网站:https://www.webjars.org 要使用jQuery,我们只要要引入jQuery对应版本的pom依赖即可!<dependency> <groupId>org.webjars</groupId> <artifactId>jquery</artifactId> <version>3.4.1</version> </dependency>导入完毕,查看webjars目录结构,并访问Jquery.js文件!访问:只要是静态资源,SpringBoot就会去对应的路径寻找资源,我们这里访问:http://localhost:8080/webjars/jquery/3.4.1/jquery.js12.4 第二种静态资源映射规则那我们项目中要是使用自己的静态资源该怎么导入呢?我们看下一行代码;我们去找staticPathPattern发现第二种映射规则 :/** , 访问当前的项目任意资源,它会去找 resourceProperties 这个类,我们可以点进去看一下分析:// 进入方法 public String[] getStaticLocations() { return this.staticLocations; } // 找到对应的值 private String[] staticLocations = CLASSPATH_RESOURCE_LOCATIONS; // 找到路径 private static final String[] CLASSPATH_RESOURCE_LOCATIONS = { "classpath:/META-INF/resources/", "classpath:/resources/", "classpath:/static/", "classpath:/public/" };ResourceProperties 可以设置和我们静态资源有关的参数;这里面指向了它会去寻找资源的文件夹,即上面数组的内容。所以得出结论,以下四个目录存放的静态资源可以被我们识别:"classpath:/META-INF/resources/" "classpath:/resources/" "classpath:/static/" "classpath:/public/"我们可以在resources根目录下新建对应的文件夹,都可以存放我们的静态文件;比如我们访问 http://localhost:8080/1.js , 他就会去这些文件夹中寻找对应的静态资源文件;12.5 自定义静态资源路径我们也可以自己通过配置文件来指定一下,哪些文件夹是需要我们放静态资源文件的,在application.properties中配置;spring.resources.static-locations=classpath:/coding/,classpath:/kuang/一旦自己定义了静态文件夹的路径,原来的自动配置就都会失效了!12.6 首页处理静态资源文件夹说完后,我们继续向下看源码!可以看到一个欢迎页的映射,就是我们的首页!@Bean public WelcomePageHandlerMapping welcomePageHandlerMapping(ApplicationContext applicationContext, FormattingConversionService mvcConversionService, ResourceUrlProvider mvcResourceUrlProvider) { WelcomePageHandlerMapping welcomePageHandlerMapping = new WelcomePageHandlerMapping( new TemplateAvailabilityProviders(applicationContext), applicationContext, getWelcomePage(), // getWelcomePage 获得欢迎页 this.mvcProperties.getStaticPathPattern()); welcomePageHandlerMapping.setInterceptors(getInterceptors(mvcConversionService, mvcResourceUrlProvider)); return welcomePageHandlerMapping; }点进去继续看private Optional<Resource> getWelcomePage() { String[] locations = getResourceLocations(this.resourceProperties.getStaticLocations()); // ::是java8 中新引入的运算符 // Class::function的时候function是属于Class的,应该是静态方法。 // this::function的funtion是属于这个对象的。 // 简而言之,就是一种语法糖而已,是一种简写 return Arrays.stream(locations).map(this::getIndexHtml).filter(this::isReadable).findFirst(); } // 欢迎页就是一个location下的的 index.html 而已 private Resource getIndexHtml(String location) { return this.resourceLoader.getResource(location + "index.html"); }欢迎页,静态资源文件夹下的所有 index.html 页面;被 /** 映射。比如我访问 http://localhost:8080/ ,就会找静态资源文件夹下的 index.html新建一个 index.html ,在我们上面的3个目录中任意一个;然后访问测试 http://localhost:8080/ 看结果!12.7 关于网站图标说明:与其他静态资源一样,Spring Boot在配置的静态内容位置中查找 favicon.ico。如果存在这样的文件,它将自动用作应用程序的favicon。1、关闭SpringBoot默认图标#关闭默认图标 spring.mvc.favicon.enabled=false2、自己放一个图标在静态资源目录下,我放在 public 目录下3、清除浏览器缓存!刷新网页,发现图标已经变成自己的了!13.Thymeleaf模板引擎13.1 模板引擎前端交给我们的页面,是html页面。如果是我们以前开发,我们需要把他们转成jsp页面,jsp好处就是当我们查出一些数据转发到JSP页面以后,我们可以用jsp轻松实现数据的显示,及交互等。jsp支持非常强大的功能,包括能写Java代码,但是呢,我们现在的这种情况,SpringBoot这个项目首先是以jar的方式,不是war,像第二,我们用的还是嵌入式的Tomcat,所以呢,他现在默认是不支持jsp的。那不支持jsp,如果我们直接用纯静态页面的方式,那给我们开发会带来非常大的麻烦,那怎么办呢?SpringBoot推荐你可以来使用模板引擎:模板引擎,我们其实大家听到很多,其实jsp就是一个模板引擎,还有用的比较多的freemarker,包括SpringBoot给我们推荐的Thymeleaf,模板引擎有非常多,但再多的模板引擎,他们的思想都是一样的,什么样一个思想呢我们来看一下这张图:模板引擎的作用就是我们来写一个页面模板,比如有些值呢,是动态的,我们写一些表达式。而这些值,从哪来呢,就是我们在后台封装一些数据。然后把这个模板和这个数据交给我们模板引擎,模板引擎按照我们这个数据帮你把这表达式解析、填充到我们指定的位置,然后把这个数据最终生成一个我们想要的内容给我们写出去,这就是我们这个模板引擎,不管是jsp还是其他模板引擎,都是这个思想。只不过呢,就是说不同模板引擎之间,他们可能这个语法有点不一样。其他的我就不介绍了,我主要来介绍一下SpringBoot给我们推荐的Thymeleaf模板引擎,这模板引擎呢,是一个高级语言的模板引擎,他的这个语法更简单。而且呢,功能更强大。我们呢,就来看一下这个模板引擎,那既然要看这个模板引擎。首先,我们来看SpringBoot里边怎么用。13.2 引入Thymeleaf怎么引入呢,对于springboot来说,什么事情不都是一个start的事情嘛,我们去在项目中引入一下。给大家三个网址:Thymeleaf 官网:https://www.thymeleaf.org/Thymeleaf 在Github 的主页:https://github.com/thymeleaf/thymeleafSpring官方文档:找到我们对应的版本https://docs.spring.io/spring-boot/docs/2.2.5.RELEASE/reference/htmlsingle/#using-boot-starter 找到对应的pom依赖:可以适当点进源码看下本来的包!<!--thymeleaf--> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-thymeleaf</artifactId> </dependency>Maven会自动下载jar包,我们可以去看下下载的东西;13.3 Thymeleaf分析前面呢,我们已经引入了Thymeleaf,那这个要怎么使用呢?我们首先得按照SpringBoot的自动配置原理看一下我们这个Thymeleaf的自动配置规则,在按照那个规则,我们进行使用。我们去找一下Thymeleaf的自动配置类:ThymeleafProperties@ConfigurationProperties( prefix = "spring.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"; private Charset encoding; }我们可以在其中看到默认的前缀和后缀!我们只需要把我们的html页面放在类路径下的templates下,thymeleaf就可以帮我们自动渲染了。使用thymeleaf什么都不需要配置,只需要将他放在指定的文件夹下即可!13.4 测试1、编写一个TestController@Controller public class TestController { @RequestMapping("/t1") public String test1(){ //classpath:/templates/test.html return "test"; } }2、编写一个测试页面 test.html 放在 templates 目录下<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <title>Title</title> </head> <body> <h1>测试页面</h1> </body> </html>3、启动项目请求测试13.5 Thymeleaf 语法学习要学习语法,还是参考官网文档最为准确,我们找到对应的版本看一下;Thymeleaf 官网:https://www.thymeleaf.org/ , 简单看一下官网!我们去下载Thymeleaf的官方文档!我们做个最简单的练习 :我们需要查出一些数据,在页面中展示1、修改测试请求,增加数据传输;@RequestMapping("/t1") public String test1(Model model){ //存入数据 model.addAttribute("msg","Hello,Thymeleaf"); //classpath:/templates/test.html return "test"; }2、我们要使用thymeleaf,需要在html文件中导入命名空间的约束,方便提示。我们可以去官方文档的#3中看一下命名空间拿来过来:xmlns:th="http://www.thymeleaf.org"3、我们去编写下前端页面<!DOCTYPE html> <html lang="en" xmlns:th="http://www.thymeleaf.org"> <head> <meta charset="UTF-8"> <title>狂神说</title> </head> <body> <h1>测试页面</h1> <!--th:text就是将div中的内容设置为它指定的值,和之前学习的Vue一样--> <div th:text="${msg}"></div> </body> </html>4、启动测试!OK,入门搞定,我们来认真研习一下Thymeleaf的使用语法!1、我们可以使用任意的 th:attr 来替换Html中原生属性的值!2、我们能写哪些表达式呢?Simple expressions:(表达式语法) Variable Expressions: ${...}:获取变量值;OGNL; 1)、获取对象的属性、调用方法 2)、使用内置的基本对象:#18 #ctx : the context object. #vars: the context variables. #locale : the context locale. #request : (only in Web Contexts) the HttpServletRequest object. #response : (only in Web Contexts) the HttpServletResponse object. #session : (only in Web Contexts) the HttpSession object. #servletContext : (only in Web Contexts) the ServletContext object. 3)、内置的一些工具对象: #execInfo : information about the template being processed. #uris : methods for escaping parts of URLs/URIs #conversions : methods for executing the configured conversion service (if any). #dates : methods for java.util.Date objects: formatting, component extraction, etc. #calendars : analogous to #dates , but for java.util.Calendar objects. #numbers : methods for formatting numeric objects. #strings : methods for String objects: contains, startsWith, prepending/appending, etc. #objects : methods for objects in general. #bools : methods for boolean evaluation. #arrays : methods for arrays. #lists : methods for lists. #sets : methods for sets. #maps : methods for maps. #aggregates : methods for creating aggregates on arrays or collections. ================================================================================== Selection Variable Expressions: *{...}:选择表达式:和${}在功能上是一样; Message Expressions: #{...}:获取国际化内容 Link URL Expressions: @{...}:定义URL; Fragment Expressions: ~{...}:片段引用表达式 Literals(字面量) Text literals: 'one text' , 'Another one!' ,… Number literals: 0 , 34 , 3.0 , 12.3 ,… Boolean literals: true , false Null literal: null Literal tokens: one , sometext , main ,… Text operations:(文本操作) String concatenation: + Literal substitutions: |The name is ${name}| Arithmetic operations:(数学运算) Binary operators: + , - , * , / , % Minus sign (unary operator): - Boolean operations:(布尔运算) Binary operators: and , or Boolean negation (unary operator): ! , not Comparisons and equality:(比较运算) Comparators: > , < , >= , <= ( gt , lt , ge , le ) Equality operators: == , != ( eq , ne ) Conditional operators:条件运算(三元运算符) If-then: (if) ? (then) If-then-else: (if) ? (then) : (else) Default: (value) ?: (defaultvalue) Special tokens: No-Operation: _13.5 练习测试1、 我们编写一个Controller,放一些数据@RequestMapping("/t2") public String test2(Map<String,Object> map){ //存入数据 map.put("msg","<h1>Hello</h1>"); map.put("users", Arrays.asList("qinjiang","kuangshen")); //classpath:/templates/test.html return "test"; }2、测试页面取出数据<!DOCTYPE html> <html lang="en" xmlns:th="http://www.thymeleaf.org"> <head> <meta charset="UTF-8"> <title>狂神说</title> </head> <body> <h1>测试页面</h1> <div th:text="${msg}"></div> <!--不转义--> <div th:utext="${msg}"></div> <!--遍历数据--> <!--th:each每次遍历都会生成当前这个标签:官网#9--> <h4 th:each="user :${users}" th:text="${user}"></h4> <h4> <!--行内写法:官网#12--> <span th:each="user:${users}">[[${user}]]</span> </h4> </body> </html>3、启动项目测试!我们看完语法,很多样式,我们即使现在学习了,也会忘记,所以我们在学习过程中,需要使用什么,根据官方文档来查询,才是最重要的,要熟练使用官方文档!14.项目集成Swagger14.1 Swagger简介前后端分离前端 -> 前端控制层、视图层后端 -> 后端控制层、服务层、数据访问层前后端通过API进行交互前后端相对独立且松耦合产生的问题前后端集成,前端或者后端无法做到“及时协商,尽早解决”,最终导致问题集中爆发解决方案首先定义schema [ 计划的提纲 ],并实时跟踪最新的API,降低集成风险Swagger号称世界上最流行的API框架Restful Api 文档在线自动生成器 => API 文档 与API 定义同步更新直接运行,在线测试API支持多种语言 (如:Java,PHP等)官网:https://swagger.io/14.2 SpringBoot集成SwaggerSpringBoot集成Swagger => springfox,两个jar包Springfox-swagger2swagger-springmvc使用Swagger要求:jdk 1.8 + 否则swagger2无法运行步骤:1、新建一个SpringBoot-web项目2、添加Maven依赖<!-- https://mvnrepository.com/artifact/io.springfox/springfox-swagger2 --> <dependency> <groupId>io.springfox</groupId> <artifactId>springfox-swagger2</artifactId> <version>2.9.2</version> </dependency> <!-- https://mvnrepository.com/artifact/io.springfox/springfox-swagger-ui --> <dependency> <groupId>io.springfox</groupId> <artifactId>springfox-swagger-ui</artifactId> <version>2.9.2</version> </dependency>3、编写HelloController,测试确保运行成功!4、要使用Swagger,我们需要编写一个配置类-SwaggerConfig来配置 Swagger@Configuration //配置类 @EnableSwagger2// 开启Swagger2的自动配置 public class SwaggerConfig { }5、访问测试 :http://localhost:8080/swagger-ui.html ,可以看到swagger的界面;14.3 配置Swagger1、Swagger实例Bean是Docket,所以通过配置Docket实例来配置Swaggger。@Bean //配置docket以配置Swagger具体参数 public Docket docket() { return new Docket(DocumentationType.SWAGGER_2); }2、可以通过apiInfo()属性配置文档信息//配置文档信息 private ApiInfo apiInfo() { Contact contact = new Contact("联系人名字", "http://xxx.xxx.com/联系人访问链接", "联系人邮箱"); return new ApiInfo( "Swagger学习", // 标题 "学习演示如何配置Swagger", // 描述 "v1.0", // 版本 "http://terms.service.url/组织链接", // 组织链接 contact, // 联系人信息 "Apach 2.0 许可", // 许可 "许可链接", // 许可连接 new ArrayList<>()// 扩展 ); }3、Docket 实例关联上 apiInfo()@Bean public Docket docket() { return new Docket(DocumentationType.SWAGGER_2).apiInfo(apiInfo()); }4、重启项目,访问测试 http://localhost:8080/swagger-ui.html 看下效果;14.4 配置扫描接口1、构建Docket时通过select()方法配置怎么扫描接口。@Bean public Docket docket() { return new Docket(DocumentationType.SWAGGER_2) .apiInfo(apiInfo()) .select()// 通过.select()方法,去配置扫描接口,RequestHandlerSelectors配置如何扫描接口 .apis(RequestHandlerSelectors.basePackage("com.kuang.swagger.controller")) .build(); }2、重启项目测试,由于我们配置根据包的路径扫描接口,所以我们只能看到一个类3、除了通过包路径配置扫描接口外,还可以通过配置其他方式扫描接口,这里注释一下所有的配置方式:any() // 扫描所有,项目中的所有接口都会被扫描到 none() // 不扫描接口 // 通过方法上的注解扫描,如withMethodAnnotation(GetMapping.class)只扫描get请求 withMethodAnnotation(final Class<? extends Annotation> annotation) // 通过类上的注解扫描,如.withClassAnnotation(Controller.class)只扫描有controller注解的类中的接口 withClassAnnotation(final Class<? extends Annotation> annotation) basePackage(final String basePackage) // 根据包路径扫描接口4、除此之外,我们还可以配置接口扫描过滤:@Bean public Docket docket() { return new Docket(DocumentationType.SWAGGER_2) .apiInfo(apiInfo()) .select()// 通过.select()方法,去配置扫描接口,RequestHandlerSelectors配置如何扫描接口 .apis(RequestHandlerSelectors.basePackage("com.kuang.swagger.controller")) // 配置如何通过path过滤,即这里只扫描请求以/kuang开头的接口 .paths(PathSelectors.ant("/kuang/**")) .build(); }5、这里的可选值还有any() // 任何请求都扫描 none() // 任何请求都不扫描 regex(final String pathRegex) // 通过正则表达式控制 ant(final String antPattern) // 通过ant()控制14.5 配置Swagger开关1、通过enable()方法配置是否启用swagger,如果是false,swagger将不能在浏览器中访问了@Bean public Docket docket() { return new Docket(DocumentationType.SWAGGER_2) .apiInfo(apiInfo()) .enable(false) //配置是否启用Swagger,如果是false,在浏览器将无法访问 .select()// 通过.select()方法,去配置扫描接口,RequestHandlerSelectors配置如何扫描接口 .apis(RequestHandlerSelectors.basePackage("com.kuang.swagger.controller")) // 配置如何通过path过滤,即这里只扫描请求以/kuang开头的接口 .paths(PathSelectors.ant("/kuang/**")) .build(); }2、如何动态配置当项目处于test、dev环境时显示swagger,处于prod时不显示?@Bean public Docket docket(Environment environment) { // 设置要显示swagger的环境 Profiles of = Profiles.of("dev", "test"); // 判断当前是否处于该环境 // 通过 enable() 接收此参数判断是否要显示 boolean b = environment.acceptsProfiles(of); return new Docket(DocumentationType.SWAGGER_2) .apiInfo(apiInfo()) .enable(b) //配置是否启用Swagger,如果是false,在浏览器将无法访问 .select()// 通过.select()方法,去配置扫描接口,RequestHandlerSelectors配置如何扫描接口 .apis(RequestHandlerSelectors.basePackage("com.kuang.swagger.controller")) // 配置如何通过path过滤,即这里只扫描请求以/kuang开头的接口 .paths(PathSelectors.ant("/kuang/**")) .build(); }3、可以在项目中增加一个dev的配置文件查看效果!14.6 配置API分组1、如果没有配置分组,默认是default。通过groupName()方法即可配置分组:@Bean public Docket docket(Environment environment) { return new Docket(DocumentationType.SWAGGER_2).apiInfo(apiInfo()) .groupName("hello") // 配置分组 // 省略配置.... }2、重启项目查看分组3、如何配置多个分组?配置多个分组只需要配置多个docket即可:@Bean public Docket docket1(){ return new Docket(DocumentationType.SWAGGER_2).groupName("group1"); } @Bean public Docket docket2(){ return new Docket(DocumentationType.SWAGGER_2).groupName("group2"); } @Bean public Docket docket3(){ return new Docket(DocumentationType.SWAGGER_2).groupName("group3"); }4、重启项目查看即可14.7 实体配置1、新建一个实体类@ApiModel("用户实体") public class User { @ApiModelProperty("用户名") public String username; @ApiModelProperty("密码") public String password; }2、只要这个实体在请求接口的返回值上(即使是泛型),都能映射到实体项中:@RequestMapping("/getUser") public User getUser(){ return new User(); }3、重启查看测试注:并不是因为@ApiModel这个注解让实体显示在这里了,而是只要出现在接口方法的返回值上的实体都会显示在这里,而@ApiModel和@ApiModelProperty这两个注解只是为实体添加注释的。@ApiModel为类添加注释@ApiModelProperty为类属性添加注释14.8 常用注解Swagger的所有注解定义在io.swagger.annotations包下下面列一些经常用到的,未列举出来的可以另行查阅说明:Swagger注解简单说明@Api(tags = "xxx模块说明")作用在模块类上@ApiOperation("xxx接口说明")作用在接口方法上@ApiModel("xxxPOJO说明")作用在模型类上:如VO、BO@ApiModelProperty(value = "xxx属性说明",hidden = true)作用在类方法和属性上,hidden设置为true可以隐藏该属性@ApiParam("xxx参数说明")作用在参数、方法和字段上,类似@ApiModelProperty我们也可以给请求的接口配置一些注释@ApiOperation("狂神的接口") @PostMapping("/kuang") @ResponseBody public String kuang(@ApiParam("这个名字会被返回")String username){ return username; }这样的话,可以给一些比较难理解的属性或者接口,增加一些配置信息,让人更容易阅读!相较于传统的Postman或Curl方式测试接口,使用swagger简直就是傻瓜式操作,不需要额外说明文档(写得好本身就是文档)而且更不容易出错,只需要录入数据然后点击Execute,如果再配合自动化框架,可以说基本就不需要人为操作了。Swagger是个优秀的工具,现在国内已经有很多的中小型互联网公司都在使用它,相较于传统的要先出Word接口文档再测试的方式,显然这样也更符合现在的快速迭代开发行情。当然了,提醒下大家在正式环境要记得关闭Swagger,一来出于安全考虑二来也可以节省运行时内存。14.9 拓展:其他皮肤我们可以导入不同的包实现不同的皮肤定义:1、默认的 访问 http://localhost:8080/swagger-ui.html<dependency> <groupId>io.springfox</groupId> <artifactId>springfox-swagger-ui</artifactId> <version>2.9.2</version> </dependency>2、bootstrap-ui 访问 http://localhost:8080/doc.html<!-- 引入swagger-bootstrap-ui包 /doc.html--> <dependency> <groupId>com.github.xiaoymin</groupId> <artifactId>swagger-bootstrap-ui</artifactId> <version>1.9.1</version> </dependency>3、Layui-ui 访问 http://localhost:8080/docs.html<!-- 引入swagger-ui-layer包 /docs.html--> <dependency> <groupId>com.github.caspar-chen</groupId> <artifactId>swagger-ui-layer</artifactId> <version>1.1.3</version> </dependency>4、mg-ui 访问 http://localhost:8080/document.html<!-- 引入swagger-ui-layer包 /document.html--> <dependency> <groupId>com.zyplayer</groupId> <artifactId>swagger-mg-ui</artifactId> <version>1.0.6</version> </dependency>15.异步、定时、邮件任务15.1 异步任务1、创建一个service包2、创建一个类AsyncService异步处理还是非常常用的,比如我们在网站上发送邮件,后台会去发送邮件,此时前台会造成响应不动,直到邮件发送完毕,响应才会成功,所以我们一般会采用多线程的方式去处理这些任务。编写方法,假装正在处理数据,使用线程设置一些延时,模拟同步等待的情况;@Service public class AsyncService { public void hello(){ try { Thread.sleep(3000); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println("业务进行中...."); } }3、编写controller包4、编写AsyncController类我们去写一个Controller测试一下@RestController public class AsyncController { @Autowired AsyncService asyncService; @GetMapping("/hello") public String hello(){ asyncService.hello(); return "success"; } }5、访问http://localhost:8080/hello进行测试,3秒后出现success,这是同步等待的情况。问题:我们如果想让用户直接得到消息,就在后台使用多线程的方式进行处理即可,但是每次都需要自己手动去编写多线程的实现的话,太麻烦了,我们只需要用一个简单的办法,在我们的方法上加一个简单的注解即可,如下:6、给hello方法添加@Async注解;//告诉Spring这是一个异步方法 @Async public void hello(){ try { Thread.sleep(3000); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println("业务进行中...."); }SpringBoot就会自己开一个线程池,进行调用!但是要让这个注解生效,我们还需要在主程序上添加一个注解@EnableAsync ,开启异步注解功能;@EnableAsync //开启异步注解功能 @SpringBootApplication public class SpringbootTaskApplication { public static void main(String[] args) { SpringApplication.run(SpringbootTaskApplication.class, args); } }7、重启测试,网页瞬间响应,后台代码依旧执行!15.2 定时任务项目开发中经常需要执行一些定时任务,比如需要在每天凌晨的时候,分析一次前一天的日志信息,Spring为我们提供了异步执行任务调度的方式,提供了两个接口。TaskExecutor接口TaskScheduler接口两个注解:@EnableScheduling@Scheduledcron表达式:测试步骤:1、创建一个ScheduledService我们里面存在一个hello方法,他需要定时执行,怎么处理呢?@Service public class ScheduledService { //秒 分 时 日 月 周几 //0 * * * * MON-FRI //注意cron表达式的用法; @Scheduled(cron = "0 * * * * 0-7") public void hello(){ System.out.println("hello....."); } }2、这里写完定时任务之后,我们需要在主程序上增加@EnableScheduling 开启定时任务功能@EnableAsync //开启异步注解功能 @EnableScheduling //开启基于注解的定时任务 @SpringBootApplication public class SpringbootTaskApplication { public static void main(String[] args) { SpringApplication.run(SpringbootTaskApplication.class, args); } }3、我们来详细了解下cron表达式;http://www.bejson.com/othertools/cron/4、常用的表达式(1)0/2 * * * * ? 表示每2秒 执行任务 (1)0 0/2 * * * ? 表示每2分钟 执行任务 (1)0 0 2 1 * ? 表示在每月的1日的凌晨2点调整任务 (2)0 15 10 ? * MON-FRI 表示周一到周五每天上午10:15执行作业 (3)0 15 10 ? 6L 2002-2006 表示2002-2006年的每个月的最后一个星期五上午10:15执行作 (4)0 0 10,14,16 * * ? 每天上午10点,下午2点,4点 (5)0 0/30 9-17 * * ? 朝九晚五工作时间内每半小时 (6)0 0 12 ? * WED 表示每个星期三中午12点 (7)0 0 12 * * ? 每天中午12点触发 (8)0 15 10 ? * * 每天上午10:15触发 (9)0 15 10 * * ? 每天上午10:15触发 (10)0 15 10 * * ? 每天上午10:15触发 (11)0 15 10 * * ? 2005 2005年的每天上午10:15触发 (12)0 * 14 * * ? 在每天下午2点到下午2:59期间的每1分钟触发 (13)0 0/5 14 * * ? 在每天下午2点到下午2:55期间的每5分钟触发 (14)0 0/5 14,18 * * ? 在每天下午2点到2:55期间和下午6点到6:55期间的每5分钟触发 (15)0 0-5 14 * * ? 在每天下午2点到下午2:05期间的每1分钟触发 (16)0 10,44 14 ? 3 WED 每年三月的星期三的下午2:10和2:44触发 (17)0 15 10 ? * MON-FRI 周一至周五的上午10:15触发 (18)0 15 10 15 * ? 每月15日上午10:15触发 (19)0 15 10 L * ? 每月最后一日的上午10:15触发 (20)0 15 10 ? * 6L 每月的最后一个星期五上午10:15触发 (21)0 15 10 ? * 6L 2002-2005 2002年至2005年的每月的最后一个星期五上午10:15触发 (22)0 15 10 ? * 6#3 每月的第三个星期五上午10:15触发15.3 邮件任务邮件发送,在我们的日常开发中,也非常的多,Springboot也帮我们做了支持邮件发送需要引入spring-boot-start-mailSpringBoot 自动配置MailSenderAutoConfiguration定义MailProperties内容,配置在application.yml中自动装配JavaMailSender测试邮件发送测试:1、引入pom依赖<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-mail</artifactId> </dependency>看它引入的依赖,可以看到 jakarta.mail<dependency> <groupId>com.sun.mail</groupId> <artifactId>jakarta.mail</artifactId> <version>1.6.4</version> <scope>compile</scope> </dependency>2、查看自动配置类:MailSenderAutoConfiguration这个类中存在bean,JavaMailSenderImpl然后我们去看下配置文件@ConfigurationProperties( prefix = "spring.mail" ) public class MailProperties { private static final Charset DEFAULT_CHARSET; private String host; private Integer port; private String username; private String password; private String protocol = "smtp"; private Charset defaultEncoding; private Map<String, String> properties; private String jndiName; }3、配置文件:spring.mail.username=24736743@qq.com spring.mail.password=你的qq授权码 spring.mail.host=smtp.qq.com # qq需要配置ssl spring.mail.properties.mail.smtp.ssl.enable=true获取授权码:在QQ邮箱中的设置->账户->开启pop3和smtp服务4、Spring单元测试@Autowired JavaMailSenderImpl mailSender; @Test public void contextLoads() { //邮件设置1:一个简单的邮件 SimpleMailMessage message = new SimpleMailMessage(); message.setSubject("通知-明天来狂神这听课"); message.setText("今晚7:30开会"); message.setTo("24736743@qq.com"); message.setFrom("24736743@qq.com"); mailSender.send(message); } @Test public void contextLoads2() throws MessagingException { //邮件设置2:一个复杂的邮件 MimeMessage mimeMessage = mailSender.createMimeMessage(); MimeMessageHelper helper = new MimeMessageHelper(mimeMessage, true); helper.setSubject("通知-明天来狂神这听课"); helper.setText("<b style='color:red'>今天 7:30来开会</b>",true); //发送附件 helper.addAttachment("1.jpg",new File("")); helper.addAttachment("2.jpg",new File("")); helper.setTo("24736743@qq.com"); helper.setFrom("24736743@qq.com"); mailSender.send(mimeMessage); }查看邮箱,邮件接收成功!我们只需要使用Thymeleaf进行前后端结合即可开发自己网站邮件收发功能了!16.富文本编辑器16.1 简介思考:我们平时在博客园,或者CSDN等平台进行写作的时候,有同学思考过他们的编辑器是怎么实现的吗?在博客园后台的选项设置中,可以看到一个文本编辑器的选项:其实这个就是富文本编辑器,市面上有许多非常成熟的富文本编辑器,比如:Editor.md——功能非常丰富的编辑器,左端编辑,右端预览,非常方便,完全免费官网:https://pandao.github.io/editor.md/wangEditor——基于javascript和css开发的 Web富文本编辑器, 轻量、简洁、界面美观、易用、开源免费。官网:http://www.wangeditor.com/TinyMCE——TinyMCE是一个轻量级的基于浏览器的所见即所得编辑器,由JavaScript写成。它对IE6+和Firefox1.5+都有着非常良好的支持。功能齐全,界面美观,就是文档是英文的,对开发人员英文水平有一定要求。官网:https://www.tiny.cloud/docs/demo/full-featured/博客园百度ueditor——UEditor是由百度web前端研发部开发所见即所得富文本web编辑器,具有轻量,功能齐全,可定制,注重用户体验等特点,开源基于MIT协议,允许自由使用和修改代码,缺点是已经没有更新了官网:https://ueditor.baidu.com/website/onlinedemo.htmlkindeditor——界面经典。官网:http://kindeditor.net/demo.phpTextbox——Textbox是一款极简但功能强大的在线文本编辑器,支持桌面设备和移动设备。主要功能包含内置的图像处理和存储、文件拖放、拼写检查和自动更正。此外,该工具还实现了屏幕阅读器等辅助技术,并符合WAI-ARIA可访问性标准。官网:https://textbox.io/CKEditor——国外的,界面美观。官网:https://ckeditor.com/ckeditor-5/demo/quill——功能强大,还可以编辑公式等官网:https://quilljs.com/simditor——界面美观,功能较全。官网:https://simditor.tower.im/summernote——UI好看,精美官网:https://summernote.org/jodit——功能齐全官网:https://xdsoft.net/jodit/froala Editor——界面非常好看,功能非常强大,非常好用(非免费)官网:https://www.froala.com/wysiwyg-editor总之,目前可用的富文本编辑器有很多......这只是其中的一部分16.2 Editor.md我这里使用的就是Editor.md,作为一个资深码农,Mardown必然是我们程序猿最喜欢的格式,看下面,就爱上了!我们可以在官网下载它:https://pandao.github.io/editor.md/ , 得到它的压缩包!解压以后,在examples目录下面,可以看到他的很多案例使用!学习,其实就是看人家怎么写的,然后进行模仿就好了!我们可以将整个解压的文件倒入我们的项目,将一些无用的测试和案例删掉即可!16.3 基础工程搭建数据库设计article:文章表字段 备注idint文章的唯一IDauthorvarchar作者titlevarchar标题contentlongtext文章的内容建表SQL:CREATE TABLE `article` ( `id` int(10) NOT NULL AUTO_INCREMENT COMMENT 'int文章的唯一ID', `author` varchar(50) NOT NULL COMMENT '作者', `title` varchar(100) NOT NULL COMMENT '标题', `content` longtext NOT NULL COMMENT '文章的内容', PRIMARY KEY (`id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8基础项目搭建1、建一个SpringBoot项目配置spring: datasource: username: root password: 123456 #?serverTimezone=UTC解决时区的报错 url: jdbc:mysql://localhost:3306/springboot?serverTimezone=UTC&useUnicode=true&characterEncoding=utf-8 driver-class-name: com.mysql.cj.jdbc.Driver<resources> <resource> <directory>src/main/java</directory> <includes> <include>**/*.xml</include> </includes> <filtering>true</filtering> </resource> </resources>2、实体类://文章类 @Data @NoArgsConstructor @AllArgsConstructor public class Article implements Serializable { private int id; //文章的唯一ID private String author; //作者名 private String title; //标题 private String content; //文章的内容 }3、mapper接口:@Mapper @Repository public interface ArticleMapper { //查询所有的文章 List<Article> queryArticles(); //新增一个文章 int addArticle(Article article); //根据文章id查询文章 Article getArticleById(int id); //根据文章id删除文章 int deleteArticleById(int id); }<?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.kuang.mapper.ArticleMapper"> <select id="queryArticles" resultType="Article"> select * from article </select> <select id="getArticleById" resultType="Article"> select * from article where id = #{id} </select> <insert id="addArticle" parameterType="Article"> insert into article (author,title,content) values (#{author},#{title},#{content}); </insert> <delete id="deleteArticleById" parameterType="int"> delete from article where id = #{id} </delete> </mapper>既然已经提供了 myBatis 的映射配置文件,自然要告诉 spring boot 这些文件的位置mybatis: mapper-locations: classpath:com/kuang/mapper/*.xml type-aliases-package: com.kuang.pojo编写一个Controller测试下,是否ok;16.4 文章编辑整合(重点)1、导入 editor.md 资源 ,删除多余文件2、编辑文章页面 editor.html、需要引入 jQuery;<!DOCTYPE html> <html class="x-admin-sm" lang="zh" xmlns:th="http://www.thymeleaf.org"> <head> <meta charset="UTF-8"> <title>秦疆'Blog</title> <meta name="renderer" content="webkit"> <meta http-equiv="X-UA-Compatible" content="IE=edge,chrome=1"> <meta name="viewport" content="width=device-width,user-scalable=yes, minimum-scale=0.4, initial-scale=0.8,target-densitydpi=low-dpi" /> <!--Editor.md--> <link rel="stylesheet" th:href="@{/editormd/css/editormd.css}"/> <link rel="shortcut icon" href="https://pandao.github.io/editor.md/favicon.ico" type="image/x-icon" /> </head> <body> <div class="layui-fluid"> <div class="layui-row layui-col-space15"> <div class="layui-col-md12"> <!--博客表单--> <form name="mdEditorForm"> <div> 标题:<input type="text" name="title"> </div> <div> 作者:<input type="text" name="author"> </div> <div id="article-content"> <textarea name="content" id="content" style="display:none;"> </textarea> </div> </form> </div> </div> </div> </body> <!--editormd--> <script th:src="@{/editormd/lib/jquery.min.js}"></script> <script th:src="@{/editormd/editormd.js}"></script> <script type="text/javascript"> var testEditor; //window.onload = function(){ } $(function() { testEditor = editormd("article-content", { width : "95%", height : 400, syncScrolling : "single", path : "../editormd/lib/", saveHTMLToTextarea : true, // 保存 HTML 到 Textarea emoji: true, theme: "dark",//工具栏主题 previewTheme: "dark",//预览主题 editorTheme: "pastel-on-dark",//编辑主题 tex : true, // 开启科学公式TeX语言支持,默认关闭 flowChart : true, // 开启流程图支持,默认关闭 sequenceDiagram : true, // 开启时序/序列图支持,默认关闭, //图片上传 imageUpload : true, imageFormats : ["jpg", "jpeg", "gif", "png", "bmp", "webp"], imageUploadURL : "/article/file/upload", onload : function() { console.log('onload', this); }, /*指定需要显示的功能按钮*/ toolbarIcons : function() { return ["undo","redo","|", "bold","del","italic","quote","ucwords","uppercase","lowercase","|", "h1","h2","h3","h4","h5","h6","|", "list-ul","list-ol","hr","|", "link","reference-link","image","code","preformatted-text", "code-block","table","datetime","emoji","html-entities","pagebreak","|", "goto-line","watch","preview","fullscreen","clear","search","|", "help","info","releaseIcon", "index"] }, /*自定义功能按钮,下面我自定义了2个,一个是发布,一个是返回首页*/ toolbarIconTexts : { releaseIcon : "<span bgcolor=\"gray\">发布</span>", index : "<span bgcolor=\"red\">返回首页</span>", }, /*给自定义按钮指定回调函数*/ toolbarHandlers:{ releaseIcon : function(cm, icon, cursor, selection) { //表单提交 mdEditorForm.method = "post"; mdEditorForm.action = "/article/addArticle";//提交至服务器的路径 mdEditorForm.submit(); }, index : function(){ window.location.href = '/'; }, } }); }); </script> </html>3、编写Controller,进行跳转,以及保存文章@Controller @RequestMapping("/article") public class ArticleController { @GetMapping("/toEditor") public String toEditor(){ return "editor"; } @PostMapping("/addArticle") public String addArticle(Article article){ articleMapper.addArticle(article); return "editor"; } }图片上传问题1、前端js中添加配置//图片上传 imageUpload : true, imageFormats : ["jpg", "jpeg", "gif", "png", "bmp", "webp"], imageUploadURL : "/article/file/upload", // //这个是上传图片时的访问地址2、后端请求,接收保存这个图片, 需要导入 FastJson 的依赖!//博客图片上传问题 @RequestMapping("/file/upload") @ResponseBody public JSONObject fileUpload(@RequestParam(value = "editormd-image-file", required = true) MultipartFile file, HttpServletRequest request) throws IOException { //上传路径保存设置 //获得SpringBoot当前项目的路径:System.getProperty("user.dir") String path = System.getProperty("user.dir")+"/upload/"; //按照月份进行分类: Calendar instance = Calendar.getInstance(); String month = (instance.get(Calendar.MONTH) + 1)+"月"; path = path+month; File realPath = new File(path); if (!realPath.exists()){ realPath.mkdir(); } //上传文件地址 System.out.println("上传文件保存地址:"+realPath); //解决文件名字问题:我们使用uuid; String filename = "ks-"+UUID.randomUUID().toString().replaceAll("-", ""); //通过CommonsMultipartFile的方法直接写文件(注意这个时候) file.transferTo(new File(realPath +"/"+ filename)); //给editormd进行回调 JSONObject res = new JSONObject(); res.put("url","/upload/"+month+"/"+ filename); res.put("success", 1); res.put("message", "upload success!"); return res; }3、解决文件回显显示的问题,设置虚拟目录映射!在我们自己拓展的MvcConfig中进行配置即可!@Configuration public class MyMvcConfig implements WebMvcConfigurer { // 文件保存在真实目录/upload/下, // 访问的时候使用虚路径/upload,比如文件名为1.png,就直接/upload/1.png就ok了。 @Override public void addResourceHandlers(ResourceHandlerRegistry registry) { registry.addResourceHandler("/upload/**") .addResourceLocations("file:"+System.getProperty("user.dir")+"/upload/"); } }表情包问题自己手动下载,emoji 表情包,放到图片路径下:修改editormd.js文件// Emoji graphics files url path editormd.emoji = { path : "../editormd/plugins/emoji-dialog/emoji/", ext : ".png" };16.5文章展示1、Controller 中增加方法@GetMapping("/{id}") public String show(@PathVariable("id") int id,Model model){ Article article = articleMapper.getArticleById(id); model.addAttribute("article",article); return "article"; }2、编写页面 article.html<!DOCTYPE html> <html lang="en" xmlns:th="http://www.thymeleaf.org"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1"> <title th:text="${article.title}"></title> </head> <body> <div> <!--文章头部信息:标题,作者,最后更新日期,导航--> <h2 style="margin: auto 0" th:text="${article.title}"></h2> 作者:<span style="float: left" th:text="${article.author}"></span> <!--文章主体内容--> <div id="doc-content"> <textarea style="display:none;" placeholder="markdown" th:text="${article.content}"></textarea> </div> </div> <link rel="stylesheet" th:href="@{/editormd/css/editormd.preview.css}" /> <script th:src="@{/editormd/lib/jquery.min.js}"></script> <script th:src="@{/editormd/lib/marked.min.js}"></script> <script th:src="@{/editormd/lib/prettify.min.js}"></script> <script th:src="@{/editormd/lib/raphael.min.js}"></script> <script th:src="@{/editormd/lib/underscore.min.js}"></script> <script th:src="@{/editormd/lib/sequence-diagram.min.js}"></script> <script th:src="@{/editormd/lib/flowchart.min.js}"></script> <script th:src="@{/editormd/lib/jquery.flowchart.min.js}"></script> <script th:src="@{/editormd/editormd.js}"></script> <script type="text/javascript"> var testEditor; $(function () { testEditor = editormd.markdownToHTML("doc-content", {//注意:这里是上面DIV的id htmlDecode: "style,script,iframe", emoji: true, taskList: true, tocm: true, tex: true, // 默认不解析 flowChart: true, // 默认不解析 sequenceDiagram: true, // 默认不解析 codeFold: true });}); </script> </body> </html>重启项目,访问进行测试!大功告成!17. Dubbo和Zookeeper集成17.1 分布式理论什么是分布式系统?在《分布式系统原理与范型》一书中有如下定义:“分布式系统是若干独立计算机的集合,这些计算机对于用户来说就像单个相关系统”;分布式系统是由一组通过网络进行通信、为了完成共同的任务而协调工作的计算机节点组成的系统。分布式系统的出现是为了用廉价的、普通的机器完成单个计算机无法完成的计算、存储任务。其目的是利用更多的机器,处理更多的数据。分布式系统(distributed system)是建立在网络之上的软件系统。首先需要明确的是,只有当单个节点的处理能力无法满足日益增长的计算、存储任务的时候,且硬件的提升(加内存、加磁盘、使用更好的CPU)高昂到得不偿失的时候,应用程序也不能进一步优化的时候,我们才需要考虑分布式系统。因为,分布式系统要解决的问题本身就是和单机系统一样的,而由于分布式系统多节点、通过网络通信的拓扑结构,会引入很多单机系统没有的问题,为了解决这些问题又会引入更多的机制、协议,带来更多的问题。。。Dubbo文档随着互联网的发展,网站应用的规模不断扩大,常规的垂直应用架构已无法应对,分布式服务架构以及流动计算架构势在必行,急需一个治理系统确保架构有条不紊的演进。在Dubbo的官网文档有这样一张图单一应用架构当网站流量很小时,只需一个应用,将所有功能都部署在一起,以减少部署节点和成本。此时,用于简化增删改查工作量的数据访问框架(ORM)是关键。适用于小型网站,小型管理系统,将所有功能都部署到一个功能里,简单易用。缺点:1、性能扩展比较难2、协同开发问题3、不利于升级维护垂直应用架构当访问量逐渐增大,单一应用增加机器带来的加速度越来越小,将应用拆成互不相干的几个应用,以提升效率。此时,用于加速前端页面开发的Web框架(MVC)是关键。通过切分业务来实现各个模块独立部署,降低了维护和部署的难度,团队各司其职更易管理,性能扩展也更方便,更有针对性。缺点:公用模块无法重复利用,开发性的浪费分布式服务架构当垂直应用越来越多,应用之间交互不可避免,将核心业务抽取出来,作为独立的服务,逐渐形成稳定的服务中心,使前端应用能更快速的响应多变的市场需求。此时,用于提高业务复用及整合的分布式服务框架(RPC)是关键。流动计算架构当服务越来越多,容量的评估,小服务资源的浪费等问题逐渐显现,此时需增加一个调度中心基于访问压力实时管理集群容量,提高集群利用率。此时,用于提高机器利用率的资源调度和治理中心(SOA)[ Service Oriented Architecture]是关键。17.2 什么是RPCRPC【Remote Procedure Call】是指远程过程调用,是一种进程间通信方式,他是一种技术的思想,而不是规范。它允许程序调用另一个地址空间(通常是共享网络的另一台机器上)的过程或函数,而不用程序员显式编码这个远程调用的细节。即程序员无论是调用本地的还是远程的函数,本质上编写的调用代码基本相同。也就是说两台服务器A,B,一个应用部署在A服务器上,想要调用B服务器上应用提供的函数/方法,由于不在一个内存空间,不能直接调用,需要通过网络来表达调用的语义和传达调用的数据。为什么要用RPC呢?就是无法在一个进程内,甚至一个计算机内通过本地调用的方式完成的需求,比如不同的系统间的通讯,甚至不同的组织间的通讯,由于计算能力需要横向扩展,需要在多台机器组成的集群上部署应用。RPC就是要像调用本地的函数一样去调远程函数;推荐阅读文章:https://www.jianshu.com/p/2accc2840a1bRPC基本原理步骤解析:RPC两个核心模块:通讯,序列化。17.3 测试环境搭建DubboApache Dubbo |ˈdʌbəʊ| 是一款高性能、轻量级的开源Java RPC框架,它提供了三大核心能力:面向接口的远程方法调用,智能容错和负载均衡,以及服务自动注册和发现。dubbo官网 http://dubbo.apache.org/zh-cn/index.html1.了解Dubbo的特性2.查看官方文档dubbo基本概念服务提供者(Provider):暴露服务的服务提供方,服务提供者在启动时,向注册中心注册自己提供的服务。服务消费者(Consumer):调用远程服务的服务消费方,服务消费者在启动时,向注册中心订阅自己所需的服务,服务消费者,从提供者地址列表中,基于软负载均衡算法,选一台提供者进行调用,如果调用失败,再选另一台调用。注册中心(Registry):注册中心返回服务提供者地址列表给消费者,如果有变更,注册中心将基于长连接推送变更数据给消费者监控中心(Monitor):服务消费者和提供者,在内存中累计调用次数和调用时间,定时每分钟发送一次统计数据到监控中心调用关系说明l 服务容器负责启动,加载,运行服务提供者。l 服务提供者在启动时,向注册中心注册自己提供的服务。l 服务消费者在启动时,向注册中心订阅自己所需的服务。l 注册中心返回服务提供者地址列表给消费者,如果有变更,注册中心将基于长连接推送变更数据给消费者。l 服务消费者,从提供者地址列表中,基于软负载均衡算法,选一台提供者进行调用,如果调用失败,再选另一台调用。l 服务消费者和提供者,在内存中累计调用次数和调用时间,定时每分钟发送一次统计数据到监控中心。Dubbo环境搭建点进dubbo官方文档,推荐我们使用Zookeeper 注册中心什么是zookeeper呢?可以查看官方文档Window下安装zookeeper1、下载zookeeper :地址, 我们下载3.4.14 , 最新版!解压zookeeper2、运行/bin/zkServer.cmd ,初次运行会报错,没有zoo.cfg配置文件;可能遇到问题:闪退 !解决方案:编辑zkServer.cmd文件末尾添加pause 。这样运行出错就不会退出,会提示错误信息,方便找到原因。3、修改zoo.cfg配置文件将conf文件夹下面的zoo_sample.cfg复制一份改名为zoo.cfg即可。注意几个重要位置:dataDir=./ 临时数据存储的目录(可写相对路径)clientPort=2181 zookeeper的端口号修改完成后再次启动zookeeper4、使用zkCli.cmd测试ls /:列出zookeeper根下保存的所有节点[zk: 127.0.0.1:2181(CONNECTED) 4] ls / [zookeeper]create –e /kuangshen 123:创建一个kuangshen节点,值为123get /kuangshen:获取/kuangshen节点的值我们再来查看一下节点window下安装dubbo-admindubbo本身并不是一个服务软件。它其实就是一个jar包,能够帮你的java程序连接到zookeeper,并利用zookeeper消费、提供服务。但是为了让用户更好的管理监控众多的dubbo服务,官方提供了一个可视化的监控程序dubbo-admin,不过这个监控即使不装也不影响使用。我们这里来安装一下:1、下载dubbo-admin地址 :https://github.com/apache/dubbo-admin/tree/master2、解压进入目录修改 dubbo-admin\src\main\resources \application.properties 指定zookeeper地址erver.port=7001 spring.velocity.cache=false spring.velocity.charset=UTF-8 spring.velocity.layout-url=/templates/default.vm spring.messages.fallback-to-system-locale=false spring.messages.basename=i18n/message spring.root.password=root spring.guest.password=guest dubbo.registry.address=zookeeper://127.0.0.1:21813、在项目目录下打包dubbo-adminmvn clean package -Dmaven.test.skip=true第一次打包的过程有点慢,需要耐心等待!直到成功!4、执行 dubbo-admin\target 下的dubbo-admin-0.0.1-SNAPSHOT.jarjava -jar dubbo-admin-0.0.1-SNAPSHOT.jar【注意:zookeeper的服务一定要打开!】执行完毕,我们去访问一下 http://localhost:7001/ , 这时候我们需要输入登录账户和密码,我们都是默认的root-root;登录成功后,查看界面安装完成!17.4 SpringBoot + Dubbo + zookeeper框架搭建1. 启动zookeeper !2. IDEA创建一个空项目;3.创建一个模块,实现服务提供者:provider-server , 选择web依赖即可4.项目创建完毕,我们写一个服务,比如卖票的服务;编写接口package com.kuang.provider.service; public interface TicketService { public String getTicket(); }编写实现类package com.kuang.provider.service; public class TicketServiceImpl implements TicketService { @Override public String getTicket() { return "《狂神说Java》"; } }5.创建一个模块,实现服务消费者:consumer-server , 选择web依赖即可6.项目创建完毕,我们写一个服务,比如用户的服务;编写servicepackage com.kuang.consumer.service; public class UserService { //我们需要去拿去注册中心的服务 }需求:现在我们的用户想使用买票的服务,这要怎么弄呢 ?服务提供者1、将服务提供者注册到注册中心,我们需要整合Dubbo和zookeeper,所以需要导包我们从dubbo官网进入github,看下方的帮助文档,找到dubbo-springboot,找到依赖包<!-- Dubbo Spring Boot Starter --> <dependency> <groupId>org.apache.dubbo</groupId> <artifactId>dubbo-spring-boot-starter</artifactId> <version>2.7.3</version> </dependency> zookeeper的包我们去maven仓库下载,zkclient;<!-- https://mvnrepository.com/artifact/com.github.sgroschupf/zkclient --> <dependency> <groupId>com.github.sgroschupf</groupId> <artifactId>zkclient</artifactId> <version>0.1</version> </dependency>【新版的坑】zookeeper及其依赖包,解决日志冲突,还需要剔除日志依赖;<!-- 引入zookeeper --> <dependency> <groupId>org.apache.curator</groupId> <artifactId>curator-framework</artifactId> <version>2.12.0</version> </dependency> <dependency> <groupId>org.apache.curator</groupId> <artifactId>curator-recipes</artifactId> <version>2.12.0</version> </dependency> <dependency> <groupId>org.apache.zookeeper</groupId> <artifactId>zookeeper</artifactId> <version>3.4.14</version> <!--排除这个slf4j-log4j12--> <exclusions> <exclusion> <groupId>org.slf4j</groupId> <artifactId>slf4j-log4j12</artifactId> </exclusion> </exclusions> </dependency>2、在springboot配置文件中配置dubbo相关属性!#当前应用名字 dubbo.application.name=provider-server #注册中心地址 dubbo.registry.address=zookeeper://127.0.0.1:2181 #扫描指定包下服务 dubbo.scan.base-packages=com.kuang.provider.service3、在service的实现类中配置服务注解,发布服务!注意导包问题import org.apache.dubbo.config.annotation.Service; import org.springframework.stereotype.Component; @Service //将服务发布出去 @Component //放在容器中 public class TicketServiceImpl implements TicketService { @Override public String getTicket() { return "《狂神说Java》"; } }逻辑理解 :应用启动起来,dubbo就会扫描指定的包下带有@component注解的服务,将它发布在指定的注册中心中!服务消费者1、导入依赖,和之前的依赖一样;<!--dubbo--> <!-- Dubbo Spring Boot Starter --> <dependency> <groupId>org.apache.dubbo</groupId> <artifactId>dubbo-spring-boot-starter</artifactId> <version>2.7.3</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> <!-- 引入zookeeper --> <dependency> <groupId>org.apache.curator</groupId> <artifactId>curator-framework</artifactId> <version>2.12.0</version> </dependency> <dependency> <groupId>org.apache.curator</groupId> <artifactId>curator-recipes</artifactId> <version>2.12.0</version> </dependency> <dependency> <groupId>org.apache.zookeeper</groupId> <artifactId>zookeeper</artifactId> <version>3.4.14</version> <!--排除这个slf4j-log4j12--> <exclusions> <exclusion> <groupId>org.slf4j</groupId> <artifactId>slf4j-log4j12</artifactId> </exclusion> </exclusions> </dependency>2、配置参数#当前应用名字 dubbo.application.name=consumer-server #注册中心地址 dubbo.registry.address=zookeeper://127.0.0.1:21813. 本来正常步骤是需要将服务提供者的接口打包,然后用pom文件导入,我们这里使用简单的方式,直接将服务的接口拿过来,路径必须保证正确,即和服务提供者相同;4. 完善消费者的服务类package com.kuang.consumer.service; import com.kuang.provider.service.TicketService; import org.apache.dubbo.config.annotation.Reference; import org.springframework.stereotype.Service; @Service //注入到容器中 public class UserService { @Reference //远程引用指定的服务,他会按照全类名进行匹配,看谁给注册中心注册了这个全类名 TicketService ticketService; public void bugTicket(){ String ticket = ticketService.getTicket(); System.out.println("在注册中心买到"+ticket); } }5. 测试类编写;@RunWith(SpringRunner.class) @SpringBootTest public class ConsumerServerApplicationTests { @Autowired UserService userService; @Test public void contextLoads() { userService.bugTicket(); } }启动测试1. 开启zookeeper2. 打开dubbo-admin实现监控【可以不用做】3. 开启服务者4. 消费者消费测试,结果:监控中心 :ok , 这就是SpingBoot + dubbo + zookeeper实现分布式开发的应用,其实就是一个服务拆分的思想;18.集成SpringSecurity18.1 安全简介在 Web 开发中,安全一直是非常重要的一个方面。安全虽然属于应用的非功能性需求,但是应该在应用开发的初期就考虑进来。如果在应用开发的后期才考虑安全的问题,就可能陷入一个两难的境地:一方面,应用存在严重的安全漏洞,无法满足用户的要求,并可能造成用户的隐私数据被攻击者窃取;另一方面,应用的基本架构已经确定,要修复安全漏洞,可能需要对系统的架构做出比较重大的调整,因而需要更多的开发时间,影响应用的发布进程。因此,从应用开发的第一天就应该把安全相关的因素考虑进来,并在整个应用的开发过程中。市面上存在比较有名的:Shiro,Spring Security !这里需要阐述一下的是,每一个框架的出现都是为了解决某一问题而产生了,那么Spring Security框架的出现是为了解决什么问题呢?首先我们看下它的官网介绍:Spring Security官网地址Spring Security is a powerful and highly customizable authentication and access-control framework. It is the de-facto standard for securing Spring-based applications.Spring Security is a framework that focuses on providing both authentication and authorization to Java applications. Like all Spring projects, the real power of Spring Security is found in how easily it can be extended to meet custom requirementsSpring Security是一个功能强大且高度可定制的身份验证和访问控制框架。它实际上是保护基于spring的应用程序的标准。Spring Security是一个框架,侧重于为Java应用程序提供身份验证和授权。与所有Spring项目一样,Spring安全性的真正强大之处在于它可以轻松地扩展以满足定制需求从官网的介绍中可以知道这是一个权限框架。想我们之前做项目是没有使用框架是怎么控制权限的?对于权限 一般会细分为功能权限,访问权限,和菜单权限。代码会写的非常的繁琐,冗余。怎么解决之前写权限代码繁琐,冗余的问题,一些主流框架就应运而生而Spring Scecurity就是其中的一种。Spring 是一个非常流行和成功的 Java 应用开发框架。Spring Security 基于 Spring 框架,提供了一套 Web 应用安全性的完整解决方案。一般来说,Web 应用的安全性包括用户认证(Authentication)和用户授权(Authorization)两个部分。用户认证指的是验证某个用户是否为系统中的合法主体,也就是说用户能否访问该系统。用户认证一般要求用户提供用户名和密码。系统通过校验用户名和密码来完成认证过程。用户授权指的是验证某个用户是否有权限执行某个操作。在一个系统中,不同用户所具有的权限是不同的。比如对一个文件来说,有的用户只能进行读取,而有的用户可以进行修改。一般来说,系统会为不同的用户分配不同的角色,而每个角色则对应一系列的权限。对于上面提到的两种应用情景,Spring Security 框架都有很好的支持。在用户认证方面,Spring Security 框架支持主流的认证方式,包括 HTTP 基本认证、HTTP 表单验证、HTTP 摘要认证、OpenID 和 LDAP 等。在用户授权方面,Spring Security 提供了基于角色的访问控制和访问控制列表(Access Control List,ACL),可以对应用中的领域对象进行细粒度的控制。18.2 实战测试实验环境搭建1、新建一个初始的springboot项目web模块,thymeleaf模块2、导入静态资源welcome.html |views |level1 1.html 2.html 3.html |level2 1.html 2.html 3.html |level3 1.html 2.html 3.html Login.html3、controller跳转!package com.kuang.controller; import org.springframework.stereotype.Controller; import org.springframework.web.bind.annotation.PathVariable; import org.springframework.web.bind.annotation.RequestMapping; @Controller public class RouterController { @RequestMapping({"/","/index"}) public String index(){ return "index"; } @RequestMapping("/toLogin") public String toLogin(){ return "views/login"; } @RequestMapping("/level1/{id}") public String level1(@PathVariable("id") int id){ return "views/level1/"+id; } @RequestMapping("/level2/{id}") public String level2(@PathVariable("id") int id){ return "views/level2/"+id; } @RequestMapping("/level3/{id}") public String level3(@PathVariable("id") int id){ return "views/level3/"+id; } }4、测试实验环境是否OK!认识SpringSecuritySpring Security 是针对Spring项目的安全框架,也是Spring Boot底层安全模块默认的技术选型,他可以实现强大的Web安全控制,对于安全控制,我们仅需要引入 spring-boot-starter-security 模块,进行少量的配置,即可实现强大的安全管理!记住几个类:WebSecurityConfigurerAdapter:自定义Security策略AuthenticationManagerBuilder:自定义认证策略@EnableWebSecurity:开启WebSecurity模式Spring Security的两个主要目标是 “认证” 和 “授权”(访问控制)。“认证”(Authentication)身份验证是关于验证您的凭据,如用户名/用户ID和密码,以验证您的身份。身份验证通常通过用户名和密码完成,有时与身份验证因素结合使用。“授权” (Authorization)授权发生在系统成功验证您的身份后,最终会授予您访问资源(如信息,文件,数据库,资金,位置,几乎任何内容)的完全权限。这个概念是通用的,而不是只在Spring Security 中存在。认证和授权目前,我们的测试环境,是谁都可以访问的,我们使用 Spring Security 增加上认证和授权的功能1、引入 Spring Security 模块<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-security</artifactId> </dependency>2、编写 Spring Security 配置类参考官网:https://spring.io/projects/spring-security 查看我们自己项目中的版本,找到对应的帮助文档:https://docs.spring.io/spring-security/site/docs/5.3.0.RELEASE/reference/html5 #servlet-applications 8.16.43、编写基础配置类package com.kuang.config; import org.springframework.security.config.annotation.web.builders.HttpSecurity; import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter; @EnableWebSecurity // 开启WebSecurity模式 public class SecurityConfig extends WebSecurityConfigurerAdapter { @Override protected void configure(HttpSecurity http) throws Exception { } }4、定制请求的授权规则@Override protected void configure(HttpSecurity http) throws Exception { // 定制请求的授权规则 // 首页所有人可以访问 http.authorizeRequests().antMatchers("/").permitAll() .antMatchers("/level1/**").hasRole("vip1") .antMatchers("/level2/**").hasRole("vip2") .antMatchers("/level3/**").hasRole("vip3"); }5、测试一下:发现除了首页都进不去了!因为我们目前没有登录的角色,因为请求需要登录的角色拥有对应的权限才可以!6、在configure()方法中加入以下配置,开启自动配置的登录功能!// 开启自动配置的登录功能 // /login 请求来到登录页 // /login?error 重定向到这里表示登录失败 http.formLogin();7、测试一下:发现,没有权限的时候,会跳转到登录的页面!8、查看刚才登录页的注释信息;我们可以定义认证规则,重写configure(AuthenticationManagerBuilder auth)方法//定义认证规则 @Override protected void configure(AuthenticationManagerBuilder auth) throws Exception { //在内存中定义,也可以在jdbc中去拿.... auth.inMemoryAuthentication() .withUser("kuangshen").password("123456").roles("vip2","vip3") .and() .withUser("root").password("123456").roles("vip1","vip2","vip3") .and() .withUser("guest").password("123456").roles("vip1","vip2");9、测试,我们可以使用这些账号登录进行测试!发现会报错!There is no PasswordEncoder mapped for the id “null”10、原因,我们要将前端传过来的密码进行某种方式加密,否则就无法登录,修改代码//定义认证规则 @Override protected void configure(AuthenticationManagerBuilder auth) throws Exception { //在内存中定义,也可以在jdbc中去拿.... //Spring security 5.0中新增了多种加密方式,也改变了密码的格式。 //要想我们的项目还能够正常登陆,需要修改一下configure中的代码。我们要将前端传过来的密码进行某种方式加密 //spring security 官方推荐的是使用bcrypt加密方式。 auth.inMemoryAuthentication().passwordEncoder(new BCryptPasswordEncoder()) .withUser("kuangshen").password(new BCryptPasswordEncoder().encode("123456")).roles("vip2","vip3") .and() .withUser("root").password(new BCryptPasswordEncoder().encode("123456")).roles("vip1","vip2","vip3") .and() .withUser("guest").password(new BCryptPasswordEncoder().encode("123456")).roles("vip1","vip2"); }11、测试,发现,登录成功,并且每个角色只能访问自己认证下的规则!搞定权限控制和注销1、开启自动配置的注销的功能//定制请求的授权规则 @Override protected void configure(HttpSecurity http) throws Exception { //.... //开启自动配置的注销的功能 // /logout 注销请求 http.logout(); }2、我们在前端,增加一个注销的按钮,index.html 导航栏中<a class="item" th:href="@{/logout}"> <i class="address card icon"></i> 注销 </a>3、我们可以去测试一下,登录成功后点击注销,发现注销完毕会跳转到登录页面!4、但是,我们想让他注销成功后,依旧可以跳转到首页,该怎么处理呢?// .logoutSuccessUrl("/"); 注销成功来到首页 http.logout().logoutSuccessUrl("/");5、测试,注销完毕后,发现跳转到首页OK6、我们现在又来一个需求:用户没有登录的时候,导航栏上只显示登录按钮,用户登录之后,导航栏可以显示登录的用户信息及注销按钮!还有就是,比如kuangshen这个用户,它只有 vip2,vip3功能,那么登录则只显示这两个功能,而vip1的功能菜单不显示!这个就是真实的网站情况了!该如何做呢?我们需要结合thymeleaf中的一些功能sec:authorize="isAuthenticated()":是否认证登录!来显示不同的页面Maven依赖:<!-- https://mvnrepository.com/artifact/org.thymeleaf.extras/thymeleaf-extras-springsecurity4 --> <dependency> <groupId>org.thymeleaf.extras</groupId> <artifactId>thymeleaf-extras-springsecurity5</artifactId> <version>3.0.4.RELEASE</version> </dependency>7、修改我们的 前端页面导入命名空间xmlns:sec="http://www.thymeleaf.org/thymeleaf-extras-springsecurity5"修改导航栏,增加认证判断<!--登录注销--> <div class="right menu"> <!--如果未登录--> <div sec:authorize="!isAuthenticated()"> <a class="item" th:href="@{/login}"> <i class="address card icon"></i> 登录 </a> </div> <!--如果已登录--> <div sec:authorize="isAuthenticated()"> <a class="item"> <i class="address card icon"></i> 用户名:<span sec:authentication="principal.username"></span> 角色:<span sec:authentication="principal.authorities"></span> </a> </div> <div sec:authorize="isAuthenticated()"> <a class="item" th:href="@{/logout}"> <i class="address card icon"></i> 注销 </a> </div> </div>8、重启测试,我们可以登录试试看,登录成功后确实,显示了我们想要的页面;9、如果注销404了,就是因为它默认防止csrf跨站请求伪造,因为会产生安全问题,我们可以将请求改为post表单提交,或者在spring security中关闭csrf功能;我们试试:在 配置中增加 http.csrf().disable();http.csrf().disable();//关闭csrf功能:跨站请求伪造,默认只能通过post方式提交logout请求 http.logout().logoutSuccessUrl("/");10、我们继续将下面的角色功能块认证完成!<!-- sec:authorize="hasRole('vip1')" --> <div class="column" sec:authorize="hasRole('vip1')"> <div class="ui raised segment"> <div class="ui"> <div class="content"> <h5 class="content">Level 1</h5> <hr> <div><a th:href="@{/level1/1}"><i class="bullhorn icon"></i> Level-1-1</a></div> <div><a th:href="@{/level1/2}"><i class="bullhorn icon"></i> Level-1-2</a></div> <div><a th:href="@{/level1/3}"><i class="bullhorn icon"></i> Level-1-3</a></div> </div> </div> </div> </div> <div class="column" sec:authorize="hasRole('vip2')"> <div class="ui raised segment"> <div class="ui"> <div class="content"> <h5 class="content">Level 2</h5> <hr> <div><a th:href="@{/level2/1}"><i class="bullhorn icon"></i> Level-2-1</a></div> <div><a th:href="@{/level2/2}"><i class="bullhorn icon"></i> Level-2-2</a></div> <div><a th:href="@{/level2/3}"><i class="bullhorn icon"></i> Level-2-3</a></div> </div> </div> </div> </div> <div class="column" sec:authorize="hasRole('vip3')"> <div class="ui raised segment"> <div class="ui"> <div class="content"> <h5 class="content">Level 3</h5> <hr> <div><a th:href="@{/level3/1}"><i class="bullhorn icon"></i> Level-3-1</a></div> <div><a th:href="@{/level3/2}"><i class="bullhorn icon"></i> Level-3-2</a></div> <div><a th:href="@{/level3/3}"><i class="bullhorn icon"></i> Level-3-3</a></div> </div> </div> </div> </div>11、测试一下!12、权限控制和注销搞定!记住我现在的情况,我们只要登录之后,关闭浏览器,再登录,就会让我们重新登录,但是很多网站的情况,就是有一个记住密码的功能,这个该如何实现呢?很简单1、开启记住我功能//定制请求的授权规则 @Override protected void configure(HttpSecurity http) throws Exception { //。。。。。。。。。。。 //记住我 http.rememberMe(); }2、我们再次启动项目测试一下,发现登录页多了一个记住我功能,我们登录之后关闭 浏览器,然后重新打开浏览器访问,发现用户依旧存在!思考:如何实现的呢?其实非常简单我们可以查看浏览器的cookie3、我们点击注销的时候,可以发现,spring security 帮我们自动删除了这个 cookie4、结论:登录成功后,将cookie发送给浏览器保存,以后登录带上这个cookie,只要通过检查就可以免登录了。如果点击注销,则会删除这个cookie,具体的原理我们在JavaWeb阶段都讲过了,这里就不在多说了!定制登录页现在这个登录页面都是spring security 默认的,怎么样可以使用我们自己写的Login界面呢?1、在刚才的登录页配置后面指定 loginpagehttp.formLogin().loginPage("/toLogin");2、然后前端也需要指向我们自己定义的 login请求<a class="item" th:href="@{/toLogin}"> <i class="address card icon"></i> 登录 </a>3、我们登录,需要将这些信息发送到哪里,我们也需要配置,login.html 配置提交请求及方式,方式必须为post:在 loginPage()源码中的注释上有写明:<form th:action="@{/login}" method="post"> <div class="field"> <label>Username</label> <div class="ui left icon input"> <input type="text" placeholder="Username" name="username"> <i class="user icon"></i> </div> </div> <div class="field"> <label>Password</label> <div class="ui left icon input"> <input type="password" name="password"> <i class="lock icon"></i> </div> </div> <input type="submit" class="ui blue submit button"/> </form>4、这个请求提交上来,我们还需要验证处理,怎么做呢?我们可以查看formLogin()方法的源码!我们配置接收登录的用户名和密码的参数!http.formLogin() .usernameParameter("username") .passwordParameter("password") .loginPage("/toLogin") .loginProcessingUrl("/login"); // 登陆表单提交请求5、在登录页增加记住我的多选框<input type="checkbox" name="remember"> 记住我6、后端验证处理!//定制记住我的参数! http.rememberMe().rememberMeParameter("remember");7、测试,OK完整配置代码package com.kuang.config; import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder; import org.springframework.security.config.annotation.web.builders.HttpSecurity; import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter; import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; @EnableWebSecurity public class SecurityConfig extends WebSecurityConfigurerAdapter { //定制请求的授权规则 @Override protected void configure(HttpSecurity http) throws Exception { http.authorizeRequests().antMatchers("/").permitAll() .antMatchers("/level1/**").hasRole("vip1") .antMatchers("/level2/**").hasRole("vip2") .antMatchers("/level3/**").hasRole("vip3"); //开启自动配置的登录功能:如果没有权限,就会跳转到登录页面! // /login 请求来到登录页 // /login?error 重定向到这里表示登录失败 http.formLogin() .usernameParameter("username") .passwordParameter("password") .loginPage("/toLogin") .loginProcessingUrl("/login"); // 登陆表单提交请求 //开启自动配置的注销的功能 // /logout 注销请求 // .logoutSuccessUrl("/"); 注销成功来到首页 http.csrf().disable();//关闭csrf功能:跨站请求伪造,默认只能通过post方式提交logout请求 http.logout().logoutSuccessUrl("/"); //记住我 http.rememberMe().rememberMeParameter("remember"); } //定义认证规则 @Override protected void configure(AuthenticationManagerBuilder auth) throws Exception { //在内存中定义,也可以在jdbc中去拿.... //Spring security 5.0中新增了多种加密方式,也改变了密码的格式。 //要想我们的项目还能够正常登陆,需要修改一下configure中的代码。我们要将前端传过来的密码进行某种方式加密 //spring security 官方推荐的是使用bcrypt加密方式。 auth.inMemoryAuthentication().passwordEncoder(new BCryptPasswordEncoder()) .withUser("kuangshen").password(new BCryptPasswordEncoder().encode("123456")).roles("vip2","vip3") .and() .withUser("root").password(new BCryptPasswordEncoder().encode("123456")).roles("vip1","vip2","vip3") .and() .withUser("guest").password(new BCryptPasswordEncoder().encode("123456")).roles("vip1","vip2"); } }参考资料http://mp.weixin.qq.com/mp/homepage?__biz=Mzg2NTAzMTExNg==&hid=1&sn=3247dca1433a891523d9e4176c90c499&scene=18#wechat_redirecthttps://www.bilibili.com/video/BV19E411v7Ty
2022年06月05日
922 阅读
0 评论
0 点赞
2022-05-05
SpringMVC学习笔记
1、回顾MVC1.1、什么是MVCMVC是模型(Model)、视图(View)、控制器(Controller)的简写,是一种软件设计规范。是将业务逻辑、数据、显示分离的方法来组织代码。MVC主要作用是降低了视图与业务逻辑间的双向偶合。MVC不是一种设计模式,MVC是一种架构模式。当然不同的MVC存在差异。Model(模型):数据模型,提供要展示的数据,因此包含数据和行为,可以认为是领域模型或JavaBean组件(包含数据和行为),不过现在一般都分离开来:Value Object(数据Dao) 和 服务层(行为Service)。也就是模型提供了模型数据查询和模型数据的状态更新等功能,包括数据和业务。View(视图):负责进行模型的展示,一般就是我们见到的用户界面,客户想看到的东西。Controller(控制器):接收用户请求,委托给模型进行处理(状态改变),处理完毕后把返回的模型数据返回给视图,由视图负责展示。也就是说控制器做了个调度员的工作。最典型的MVC就是JSP + servlet + javabean的模式。1.2、Model1时代在web早期的开发中,通常采用的都是Model1。Model1中,主要分为两层,视图层和模型层。Model1优点:架构简单,比较适合小型项目开发;Model1缺点:JSP职责不单一,职责过重,不便于维护;1.3、Model2时代Model2把一个项目分成三部分,包括视图、控制、模型。用户发请求Servlet接收请求数据,并调用对应的业务逻辑方法业务处理完毕,返回更新后的数据给servletservlet转向到JSP,由JSP来渲染页面响应给前端更新后的页面职责分析:Controller:控制器取得表单数据调用业务逻辑转向指定的页面Model:模型业务逻辑保存数据的状态View:视图显示页面Model2这样不仅提高的代码的复用率与项目的扩展性,且大大降低了项目的维护成本。Model 1模式的实现比较简单,适用于快速开发小规模项目,Model1中JSP页面身兼View和Controller两种角色,将控制逻辑和表现逻辑混杂在一起,从而导致代码的重用性非常低,增加了应用的扩展性和维护的难度。Model2消除了Model1的缺点。1.4、回顾Servlet新建一个Maven工程当做父工程!pom依赖!<dependencies> <dependency> <groupId>junit</groupId> <artifactId>junit</artifactId> <version>4.12</version> </dependency> <dependency> <groupId>org.springframework</groupId> <artifactId>spring-webmvc</artifactId> <version>5.1.9.RELEASE</version> </dependency> <dependency> <groupId>javax.servlet</groupId> <artifactId>servlet-api</artifactId> <version>2.5</version> </dependency> <dependency> <groupId>javax.servlet.jsp</groupId> <artifactId>jsp-api</artifactId> <version>2.2</version> </dependency> <dependency> <groupId>javax.servlet</groupId> <artifactId>jstl</artifactId> <version>1.2</version> </dependency> </dependencies>建立一个Moudle:springmvc-01-servlet , 添加Web app的支持!导入servlet 和 jsp 的 jar 依赖<dependency> <groupId>javax.servlet</groupId> <artifactId>servlet-api</artifactId> <version>2.5</version> </dependency> <dependency> <groupId>javax.servlet.jsp</groupId> <artifactId>jsp-api</artifactId> <version>2.2</version> </dependency>编写一个Servlet类,用来处理用户的请求package com.kuang.servlet; //实现Servlet接口 public class HelloServlet extends HttpServlet { @Override protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { //取得参数 String method = req.getParameter("method"); if (method.equals("add")){ req.getSession().setAttribute("msg","执行了add方法"); } if (method.equals("delete")){ req.getSession().setAttribute("msg","执行了delete方法"); } //业务逻辑 //视图跳转 req.getRequestDispatcher("/WEB-INF/jsp/hello.jsp").forward(req,resp); } @Override protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { doGet(req,resp); } }编写Hello.jsp,在WEB-INF目录下新建一个jsp的文件夹,新建hello.jsp<%@ page contentType="text/html;charset=UTF-8" language="java" %> <html> <head> <title>Kuangshen</title> </head> <body> ${msg} </body> </html>在web.xml中注册Servlet<?xml version="1.0" encoding="UTF-8"?> <web-app xmlns="http://xmlns.jcp.org/xml/ns/javaee" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://xmlns.jcp.org/xml/ns/javaee http://xmlns.jcp.org/xml/ns/javaee/web-app_4_0.xsd" version="4.0"> <servlet> <servlet-name>HelloServlet</servlet-name> <servlet-class>com.kuang.servlet.HelloServlet</servlet-class> </servlet> <servlet-mapping> <servlet-name>HelloServlet</servlet-name> <url-pattern>/user</url-pattern> </servlet-mapping> </web-app>配置Tomcat,并启动测试localhost:8080/user?method=addlocalhost:8080/user?method=deleteMVC框架要做哪些事情将url映射到java类或java类的方法 .封装用户提交的数据 .处理请求--调用相关的业务处理--封装响应数据 .将响应的数据进行渲染 . jsp / html 等表示层数据 .说明: 常见的服务器端MVC框架有:Struts、Spring MVC、ASP.NET MVC、Zend Framework、JSF;常见前端MVC框架:vue、angularjs、react、backbone;由MVC演化出了另外一些模式如:MVP、MVVM 等等....2、什么是SpringMVC2.1、概述Spring MVC是Spring Framework的一部分,是基于Java实现MVC的轻量级Web框架。查看官方文档:https://docs.spring.io/spring/docs/5.2.0.RELEASE/spring-framework-reference/web.html#spring-web我们为什么要学习SpringMVC呢?Spring MVC的特点:轻量级,简单易学高效 , 基于请求响应的MVC框架与Spring兼容性好,无缝结合约定优于配置功能强大:RESTful、数据验证、格式化、本地化、主题等简洁灵活Spring的web框架围绕DispatcherServlet [ 调度Servlet ] 设计。DispatcherServlet的作用是将请求分发到不同的处理器。从Spring 2.5开始,使用Java 5或者以上版本的用户可以采用基于注解形式进行开发,十分简洁;正因为SpringMVC好 , 简单 , 便捷 , 易学 , 天生和Spring无缝集成(使用SpringIoC和Aop) , 使用约定优于配置 . 能够进行简单的junit测试 . 支持Restful风格 .异常处理 , 本地化 , 国际化 , 数据验证 , 类型转换 , 拦截器 等等......所以我们要学习 .最重要的一点还是用的人多 , 使用的公司多 .2.2、中心控制器 Spring的web框架围绕DispatcherServlet设计。DispatcherServlet的作用是将请求分发到不同的处理器。从Spring 2.5开始,使用Java 5或者以上版本的用户可以采用基于注解的controller声明方式。 Spring MVC框架像许多其他MVC框架一样, 以请求为驱动 , 围绕一个中心Servlet分派请求及提供其他功能,DispatcherServlet是一个实际的Servlet (它继承自HttpServlet 基类)。SpringMVC的原理如下图所示: 当发起请求时被前置的控制器拦截到请求,根据请求参数生成代理请求,找到请求对应的实际控制器,控制器处理请求,创建数据模型,访问数据库,将模型响应给中心控制器,控制器使用模型与视图渲染视图结果,将结果返回给中心控制器,再将结果返回给请求者。2.3、SpringMVC执行原理图为SpringMVC的一个较完整的流程图,实线表示SpringMVC框架提供的技术,不需要开发者实现,虚线表示需要开发者实现。简要分析执行流程DispatcherServlet表示前置控制器,是整个SpringMVC的控制中心。用户发出请求,DispatcherServlet接收请求并拦截请求。我们假设请求的url为 : http://localhost:8080/SpringMVC/hello如上url拆分成三部分:http://localhost:8080服务器域名SpringMVC部署在服务器上的web站点hello表示控制器通过分析,如上url表示为:请求位于服务器localhost:8080上的SpringMVC站点的hello控制器。HandlerMapping为处理器映射。DispatcherServlet调用HandlerMapping,HandlerMapping根据请求url查找Handler。HandlerExecution表示具体的Handler,其主要作用是根据url查找控制器,如上url被查找控制器为:hello。HandlerExecution将解析后的信息传递给DispatcherServlet,如解析控制器映射等。HandlerAdapter表示处理器适配器,其按照特定的规则去执行Handler。Handler让具体的Controller执行。Controller将具体的执行信息返回给HandlerAdapter,如ModelAndView。HandlerAdapter将视图逻辑名或模型传递给DispatcherServlet。DispatcherServlet调用视图解析器(ViewResolver)来解析HandlerAdapter传递的逻辑视图名。视图解析器将解析的逻辑视图名传给DispatcherServlet。DispatcherServlet根据视图解析器解析的视图结果,调用具体的视图。最终视图呈现给用户。3.Hello,SpringMVC3.1 配置版1、新建一个Moudle , springmvc-02-hello , 添加web的支持!2、确定导入了SpringMVC 的依赖!3、配置web.xml , 注册DispatcherServlet<?xml version="1.0" encoding="UTF-8"?> <web-app xmlns="http://xmlns.jcp.org/xml/ns/javaee" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://xmlns.jcp.org/xml/ns/javaee http://xmlns.jcp.org/xml/ns/javaee/web-app_4_0.xsd" version="4.0"> <!--1.注册DispatcherServlet--> <servlet> <servlet-name>springmvc</servlet-name> <servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class> <!--关联一个springmvc的配置文件:【servlet-name】-servlet.xml--> <init-param> <param-name>contextConfigLocation</param-name> <param-value>classpath:springmvc-servlet.xml</param-value> </init-param> <!--启动级别-1--> <load-on-startup>1</load-on-startup> </servlet> <!--/ 匹配所有的请求;(不包括.jsp)--> <!--/* 匹配所有的请求;(包括.jsp)--> <servlet-mapping> <servlet-name>springmvc</servlet-name> <url-pattern>/</url-pattern> </servlet-mapping> </web-app>4、编写SpringMVC 的 配置文件!名称:springmvc-servlet.xml : [servletname]-servlet.xml说明,这里的名称要求是按照官方来的<?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"> </beans>5、添加 处理映射器<bean class="org.springframework.web.servlet.handler.BeanNameUrlHandlerMapping"/>6、添加 处理器适配器<bean class="org.springframework.web.servlet.mvc.SimpleControllerHandlerAdapter"/>7、添加 视图解析器<!--视图解析器:DispatcherServlet给他的ModelAndView--> <bean class="org.springframework.web.servlet.view.InternalResourceViewResolver" id="InternalResourceViewResolver"> <!--前缀--> <property name="prefix" value="/WEB-INF/jsp/"/> <!--后缀--> <property name="suffix" value=".jsp"/> </bean>8、编写我们要操作业务Controller ,要么实现Controller接口,要么增加注解;需要返回一个ModelAndView,装数据,封视图;package com.kuang.controller; import org.springframework.web.servlet.ModelAndView; import org.springframework.web.servlet.mvc.Controller; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; //注意:这里我们先导入Controller接口 public class HelloController implements Controller { public ModelAndView handleRequest(HttpServletRequest request, HttpServletResponse response) throws Exception { //ModelAndView 模型和视图 ModelAndView mv = new ModelAndView(); //封装对象,放在ModelAndView中。Model mv.addObject("msg","HelloSpringMVC!"); //封装要跳转的视图,放在ModelAndView中 mv.setViewName("hello"); //: /WEB-INF/jsp/hello.jsp return mv; } }9、将自己的类交给SpringIOC容器,注册bean<!--Handler--> <bean id="/hello" class="com.kuang.controller.HelloController"/>10、写要跳转的jsp页面,显示ModelandView存放的数据,以及我们的正常页面;<%@ page contentType="text/html;charset=UTF-8" language="java" %> <html> <head> <title>Kuangshen</title> </head> <body> ${msg} </body> </html>11、配置Tomcat 启动测试!可能遇到的问题:访问出现404,排查步骤:查看控制台输出,看一下是不是缺少了什么jar包。如果jar包存在,显示无法输出,就在IDEA的项目发布中,添加lib依赖!重启Tomcat 即可解决!3.2 注解版(推荐)1、新建一个Moudle,springmvc-03-hello-annotation 。添加web支持!2、由于Maven可能存在资源过滤的问题,我们将配置完善<build> <resources> <resource> <directory>src/main/java</directory> <includes> <include>**/*.properties</include> <include>**/*.xml</include> </includes> <filtering>false</filtering> </resource> <resource> <directory>src/main/resources</directory> <includes> <include>**/*.properties</include> <include>**/*.xml</include> </includes> <filtering>false</filtering> </resource> </resources> </build>3、在pom.xml文件引入相关的依赖:主要有Spring框架核心库、Spring MVC、servlet , JSTL等。我们在父依赖中已经引入了!4、配置web.xml注意点: <?xml version="1.0" encoding="UTF-8"?> <web-app xmlns="http://xmlns.jcp.org/xml/ns/javaee" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://xmlns.jcp.org/xml/ns/javaee http://xmlns.jcp.org/xml/ns/javaee/web-app_4_0.xsd" version="4.0"> <!--1.注册servlet--> <servlet> <servlet-name>SpringMVC</servlet-name> <servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class> <!--通过初始化参数指定SpringMVC配置文件的位置,进行关联--> <init-param> <param-name>contextConfigLocation</param-name> <param-value>classpath:springmvc-servlet.xml</param-value> </init-param> <!-- 启动顺序,数字越小,启动越早 --> <load-on-startup>1</load-on-startup> </servlet> <!--所有请求都会被springmvc拦截 --> <servlet-mapping> <servlet-name>SpringMVC</servlet-name> <url-pattern>/</url-pattern> </servlet-mapping> </web-app>/ 和 /* 的区别:< url-pattern > / </ url-pattern > 不会匹配到.jsp, 只针对我们编写的请求;即:.jsp 不会进入spring的 DispatcherServlet类 。< url-pattern > /* </ url-pattern > 会匹配 *.jsp,会出现返回 jsp视图 时再次进入spring的DispatcherServlet 类,导致找不到对应的controller所以报404错。注意web.xml版本问题,要最新版!注册DispatcherServlet关联SpringMVC的配置文件启动级别为1映射路径为 / 【不要用/*,会404】5、添加Spring MVC配置文件在resource目录下添加springmvc-servlet.xml配置文件,配置的形式与Spring容器配置基本类似,为了支持基于注解的IOC,设置了自动扫描包的功能,具体配置信息如下:<?xml version="1.0" encoding="UTF-8"?> <beans xmlns="http://www.springframework.org/schema/beans" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:context="http://www.springframework.org/schema/context" xmlns:mvc="http://www.springframework.org/schema/mvc" xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd http://www.springframework.org/schema/context https://www.springframework.org/schema/context/spring-context.xsd http://www.springframework.org/schema/mvc https://www.springframework.org/schema/mvc/spring-mvc.xsd"> <!-- 自动扫描包,让指定包下的注解生效,由IOC容器统一管理 --> <context:component-scan base-package="com.kuang.controller"/> <!-- 让Spring MVC不处理静态资源 --> <mvc:default-servlet-handler /> <!-- 支持mvc注解驱动 在spring中一般采用@RequestMapping注解来完成映射关系 要想使@RequestMapping注解生效 必须向上下文中注册DefaultAnnotationHandlerMapping 和一个AnnotationMethodHandlerAdapter实例 这两个实例分别在类级别和方法级别处理。 而annotation-driven配置帮助我们自动完成上述两个实例的注入。 --> <mvc:annotation-driven /> <!-- 视图解析器 --> <bean class="org.springframework.web.servlet.view.InternalResourceViewResolver" id="internalResourceViewResolver"> <!-- 前缀 --> <property name="prefix" value="/WEB-INF/jsp/" /> <!-- 后缀 --> <property name="suffix" value=".jsp" /> </bean> </beans>在视图解析器中我们把所有的视图都存放在/WEB-INF/目录下,这样可以保证视图安全,因为这个目录下的文件,客户端不能直接访问。让IOC的注解生效静态资源过滤 :HTML . JS . CSS . 图片 , 视频 .....MVC的注解驱动配置视图解析器6、创建Controller编写一个Java控制类:com.kuang.controller.HelloController , 注意编码规范package com.kuang.controller; import org.springframework.stereotype.Controller; import org.springframework.ui.Model; import org.springframework.web.bind.annotation.RequestMapping; @Controller @RequestMapping("/HelloController") public class HelloController { //真实访问地址 : 项目名/HelloController/hello @RequestMapping("/hello") public String sayHello(Model model){ //向模型中添加属性msg与值,可以在JSP页面中取出并渲染 model.addAttribute("msg","hello,SpringMVC"); //web-inf/jsp/hello.jsp return "hello"; } }@Controller是为了让Spring IOC容器初始化时自动扫描到;@RequestMapping是为了映射请求路径,这里因为类与方法上都有映射所以访问时应该是/HelloController/hello;方法中声明Model类型的参数是为了把Action中的数据带到视图中;方法返回的结果是视图的名称hello,加上配置文件中的前后缀变成WEB-INF/jsp/hello.jsp。7、创建视图层在WEB-INF/ jsp目录中创建hello.jsp , 视图可以直接取出并展示从Controller带回的信息;可以通过EL表示取出Model中存放的值,或者对象;<%@ page contentType="text/html;charset=UTF-8" language="java" %> <html> <head> <title>SpringMVC</title> </head> <body> ${msg} </body> </html>8、配置Tomcat运行配置Tomcat , 开启服务器 , 访问 对应的请求路径!OK,运行成功!3.3 小结实现步骤其实非常的简单:新建一个web项目导入相关jar包编写web.xml , 注册DispatcherServlet编写springmvc配置文件接下来就是去创建对应的控制类 , controller最后完善前端视图和controller之间的对应测试运行调试.使用springMVC必须配置的三大件:处理器映射器、处理器适配器、视图解析器通常,我们只需要手动配置视图解析器,而处理器映射器和处理器适配器只需要开启注解驱动即可,而省去了大段的xml配置再来回顾下原理吧~4. 控制器Controller4.1 控制器Controller控制器复杂提供访问应用程序的行为,通常通过接口定义或注解定义两种方法实现。控制器负责解析用户的请求并将其转换为一个模型。在Spring MVC中一个控制器类可以包含多个方法在Spring MVC中,对于Controller的配置方式有很多种4.2 实现Controller接口Controller是一个接口,在org.springframework.web.servlet.mvc包下,接口中只有一个方法;//实现该接口的类获得控制器功能 public interface Controller { //处理请求且返回一个模型与视图对象 ModelAndView handleRequest(HttpServletRequest var1, HttpServletResponse var2) throws Exception; }测试新建一个Moudle,springmvc-04-controller 。将刚才的03 拷贝一份, 我们进行操作!删掉HelloControllermvc的配置文件只留下 视图解析器!编写一个Controller类,ControllerTest1//定义控制器 //注意点:不要导错包,实现Controller接口,重写方法; public class ControllerTest1 implements Controller { public ModelAndView handleRequest(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse) throws Exception { //返回一个模型视图对象 ModelAndView mv = new ModelAndView(); mv.addObject("msg","Test1Controller"); mv.setViewName("test"); return mv; } }编写完毕后,去Spring配置文件中注册请求的bean;name对应请求路径,class对应处理请求的类<bean name="/t1" class="com.kuang.controller.ControllerTest1"/>编写前端test.jsp,注意在WEB-INF/jsp目录下编写,对应我们的视图解析器<%@ page contentType="text/html;charset=UTF-8" language="java" %> <html> <head> <title>Kuangshen</title> </head> <body> ${msg} </body> </html>配置Tomcat运行测试,我这里没有项目发布名配置的就是一个 / ,所以请求不用加项目名,OK!说明:实现接口Controller定义控制器是较老的办法缺点是:一个控制器中只有一个方法,如果要多个方法则需要定义多个Controller;定义的方式比较麻烦;4.3 使用注解@Controller(推荐)@Controller注解类型用于声明Spring类的实例是一个控制器(在讲IOC时还提到了另外3个注解);Spring可以使用扫描机制来找到应用程序中所有基于注解的控制器类,为了保证Spring能找到你的控制器,需要在配置文件中声明组件扫描。<!-- 自动扫描指定的包,下面所有注解类交给IOC容器管理 --> <context:component-scan base-package="com.kuang.controller"/>增加一个ControllerTest2类,使用注解实现;//@Controller注解的类会自动添加到Spring上下文中 @Controller public class ControllerTest2{ //映射访问路径 @RequestMapping("/t2") public String index(Model model){ //Spring MVC会自动实例化一个Model对象用于向视图中传值 model.addAttribute("msg", "ControllerTest2"); //返回视图位置 return "test"; } }运行tomcat测试可以发现,我们的两个请求都可以指向一个视图,但是页面结果的结果是不一样的,从这里可以看出视图是被复用的,而控制器与视图之间是弱偶合关系。注解方式是平时使用的最多的方式!4.4 RequestMapping@RequestMapping@RequestMapping注解用于映射url到控制器类或一个特定的处理程序方法。可用于类或方法上。用于类上,表示类中的所有响应请求的方法都是以该地址作为父路径。为了测试结论更加准确,我们可以加上一个项目名测试 myweb只注解在方法上面@Controller public class TestController { @RequestMapping("/h1") public String test(){ return "test"; } }访问路径:http://localhost:8080 / 项目名 / h1同时注解类与方法@Controller @RequestMapping("/admin") public class TestController { @RequestMapping("/h1") public String test(){ return "test"; } }访问路径:http://localhost:8080 / 项目名/ admin /h1 , 需要先指定类的路径再指定方法的路径;4.5 RestFul 风格概念Restful就是一个资源定位及资源操作的风格。不是标准也不是协议,只是一种风格。基于这个风格设计的软件可以更简洁,更有层次,更易于实现缓存等机制。功能资源:互联网所有的事物都可以被抽象为资源资源操作:使用POST、DELETE、PUT、GET,使用不同方法对资源进行操作。分别对应 添加、 删除、修改、查询。传统方式操作资源 :通过不同的参数来实现不同的效果!方法单一,post 和 get http://127.0.0.1/item/queryItem.action?id=1 查询,GET http://127.0.0.1/item/saveItem.action 新增,POST http://127.0.0.1/item/updateItem.action 更新,POST http://127.0.0.1/item/deleteItem.action?id=1 删除,GET或POST使用RESTful操作资源 :可以通过不同的请求方式来实现不同的效果!如下:请求地址一样,但是功能可以不同! http://127.0.0.1/item/1 查询,GET http://127.0.0.1/item 新增,POST http://127.0.0.1/item 更新,PUT http://127.0.0.1/item/1 删除,DELETE学习测试在新建一个类 RestFulController@Controller public class RestFulController { }在Spring MVC中可以使用 @PathVariable 注解,让方法参数的值对应绑定到一个URI模板变量上。@Controller public class RestFulController { //映射访问路径 @RequestMapping("/commit/{p1}/{p2}") public String index(@PathVariable int p1, @PathVariable int p2, Model model){ int result = p1+p2; //Spring MVC会自动实例化一个Model对象用于向视图中传值 model.addAttribute("msg", "结果:"+result); //返回视图位置 return "test"; } }我们来测试请求查看下思考:使用路径变量的好处?使路径变得更加简洁;获得参数更加方便,框架会自动进行类型转换。通过路径变量的类型可以约束访问参数,如果类型不一样,则访问不到对应的请求方法,如这里访问是的路径是/commit/1/a,则路径与方法不匹配,而不会是参数转换失败。  我们来修改下对应的参数类型,再次测试//映射访问路径 @RequestMapping("/commit/{p1}/{p2}") public String index(@PathVariable int p1, @PathVariable String p2, Model model){ String result = p1+p2; //Spring MVC会自动实例化一个Model对象用于向视图中传值 model.addAttribute("msg", "结果:"+result); //返回视图位置 return "test"; }使用method属性指定请求类型用于约束请求的类型,可以收窄请求范围。指定请求谓词的类型如GET, POST, HEAD, OPTIONS, PUT, PATCH, DELETE, TRACE等我们来测试一下:增加一个方法//映射访问路径,必须是POST请求 @RequestMapping(value = "/hello",method = {RequestMethod.POST}) public String index2(Model model){ model.addAttribute("msg", "hello!"); return "test"; }我们使用浏览器地址栏进行访问默认是Get请求,会报错405:如果将POST修改为GET则正常了;//映射访问路径,必须是Get请求 @RequestMapping(value = "/hello",method = {RequestMethod.GET}) public String index2(Model model){ model.addAttribute("msg", "hello!"); return "test"; }小结:Spring MVC 的 @RequestMapping 注解能够处理 HTTP 请求的方法, 比如 GET, PUT, POST, DELETE 以及 PATCH。所有的地址栏请求默认都会是 HTTP GET 类型的。方法级别的注解变体有如下几个:组合注解@GetMapping @PostMapping @PutMapping @DeleteMapping @PatchMapping@GetMapping 是一个组合注解,平时使用的会比较多!它所扮演的是 @RequestMapping(method =RequestMethod.GET) 的一个快捷方式。5.结果跳转方式5.1 ModelAndView设置ModelAndView对象 , 根据view的名称 , 和视图解析器跳到指定的页面 .页面 : {视图解析器前缀} + viewName +{视图解析器后缀}<!-- 视图解析器 --> <bean class="org.springframework.web.servlet.view.InternalResourceViewResolver" id="internalResourceViewResolver"> <!-- 前缀 --> <property name="prefix" value="/WEB-INF/jsp/" /> <!-- 后缀 --> <property name="suffix" value=".jsp" /> </bean>对应的controller类public class ControllerTest1 implements Controller { public ModelAndView handleRequest(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse) throws Exception { //返回一个模型视图对象 ModelAndView mv = new ModelAndView(); mv.addObject("msg","ControllerTest1"); mv.setViewName("test"); return mv; } }5.2 ServletAPI通过设置ServletAPI , 不需要视图解析器 .1、通过HttpServletResponse进行输出2、通过HttpServletResponse实现重定向3、通过HttpServletResponse实现转发@Controller public class ResultGo { @RequestMapping("/result/t1") public void test1(HttpServletRequest req, HttpServletResponse rsp) throws IOException { rsp.getWriter().println("Hello,Spring BY servlet API"); } @RequestMapping("/result/t2") public void test2(HttpServletRequest req, HttpServletResponse rsp) throws IOException { rsp.sendRedirect("/index.jsp"); } @RequestMapping("/result/t3") public void test3(HttpServletRequest req, HttpServletResponse rsp) throws Exception { //转发 req.setAttribute("msg","/result/t3"); req.getRequestDispatcher("/WEB-INF/jsp/test.jsp").forward(req,rsp); } }5.3 SpringMVC通过SpringMVC来实现转发和重定向 - 无需视图解析器;测试前,需要将视图解析器注释掉@Controller public class ResultSpringMVC { @RequestMapping("/rsm/t1") public String test1(){ //转发 return "/index.jsp"; } @RequestMapping("/rsm/t2") public String test2(){ //转发二 return "forward:/index.jsp"; } @RequestMapping("/rsm/t3") public String test3(){ //重定向 return "redirect:/index.jsp"; } }通过SpringMVC来实现转发和重定向 - 有视图解析器;重定向 , 不需要视图解析器 , 本质就是重新请求一个新地方嘛 , 所以注意路径问题.可以重定向到另外一个请求实现 .@Controller public class ResultSpringMVC2 { @RequestMapping("/rsm2/t1") public String test1(){ //转发 return "test"; } @RequestMapping("/rsm2/t2") public String test2(){ //重定向 return "redirect:/index.jsp"; //return "redirect:hello.do"; //hello.do为另一个请求/ } }6.数据处理6.1 处理提交数据1、提交的域名称和处理方法的参数名一致提交数据 : http://localhost:8080/hello?name=kuangshen处理方法 :@RequestMapping("/hello") public String hello(String name){ System.out.println(name); return "hello"; }后台输出 : kuangshen2、提交的域名称和处理方法的参数名不一致提交数据 : http://localhost:8080/hello?username=kuangshen处理方法 ://@RequestParam("username") : username提交的域的名称 . @RequestMapping("/hello") public String hello(@RequestParam("username") String name){ System.out.println(name); return "hello"; }后台输出 : kuangshen3、提交的是一个对象要求提交的表单域和对象的属性名一致 , 参数使用对象即可1、实体类public class User { private int id; private String name; private int age; //构造 //get/set //tostring() }2、提交数据 : http://localhost:8080/mvc04/user?name=kuangshen&id=1&age=153、处理方法 :@RequestMapping("/user") public String user(User user){ System.out.println(user); return "hello"; }后台输出 : User { id=1, name='kuangshen', age=15 }说明:如果使用对象的话,前端传递的参数名和对象名必须一致,否则就是null。6.2 数据显示到前端第一种 : 通过ModelAndView我们前面一直都是如此 . 就不过多解释public class ControllerTest1 implements Controller { public ModelAndView handleRequest(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse) throws Exception { //返回一个模型视图对象 ModelAndView mv = new ModelAndView(); mv.addObject("msg","ControllerTest1"); mv.setViewName("test"); return mv; } }第二种 : 通过ModelMapModelMap@RequestMapping("/hello") public String hello(@RequestParam("username") String name, ModelMap model){ //封装要显示到视图中的数据 //相当于req.setAttribute("name",name); model.addAttribute("name",name); System.out.println(name); return "hello"; }第三种 : 通过ModelModel@RequestMapping("/ct2/hello") public String hello(@RequestParam("username") String name, Model model){ //封装要显示到视图中的数据 //相当于req.setAttribute("name",name); model.addAttribute("msg",name); System.out.println(name); return "test"; }对比就对于新手而言简单来说使用区别就是:Model 只有寥寥几个方法只适合用于储存数据,简化了新手对于Model对象的操作和理解; ModelMap 继承了 LinkedMap ,除了实现了自身的一些方法,同样的继承 LinkedMap 的方法和特性; ModelAndView 可以在储存数据的同时,可以进行设置返回的逻辑视图,进行控制展示层的跳转。当然更多的以后开发考虑的更多的是性能和优化,就不能单单仅限于此的了解。请使用80%的时间打好扎实的基础,剩下18%的时间研究框架,2%的时间去学点英文,框架的官方文档永远是最好的教程。6.3 乱码问题测试步骤:1、我们可以在首页编写一个提交的表单<form action="/e/t" method="post"> <input type="text" name="name"> <input type="submit"> </form>2、后台编写对应的处理类@Controller public class Encoding { @RequestMapping("/e/t") public String test(Model model,String name){ model.addAttribute("msg",name); //获取表单提交的值 return "test"; //跳转到test页面显示输入的值 } }3、输入中文测试,发现乱码不得不说,乱码问题是在我们开发中十分常见的问题,也是让我们程序猿比较头大的问题!以前乱码问题通过过滤器解决 , 而SpringMVC给我们提供了一个过滤器 , 可以在web.xml中配置 .修改了xml文件需要重启服务器!<filter> <filter-name>encoding</filter-name> <filter-class>org.springframework.web.filter.CharacterEncodingFilter</filter-class> <init-param> <param-name>encoding</param-name> <param-value>utf-8</param-value> </init-param> </filter> <filter-mapping> <filter-name>encoding</filter-name> <url-pattern>/*</url-pattern> </filter-mapping>但是我们发现 , 有些极端情况下.这个过滤器对get的支持不好 .处理方法 :1、修改tomcat配置文件 :设置编码!<Connector URIEncoding="utf-8" port="8080" protocol="HTTP/1.1" connectionTimeout="20000" redirectPort="8443" />2、自定义过滤器package com.kuang.filter; import javax.servlet.*; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletRequestWrapper; import javax.servlet.http.HttpServletResponse; import java.io.IOException; import java.io.UnsupportedEncodingException; import java.util.Map; /** * 解决get和post请求 全部乱码的过滤器 */ public class GenericEncodingFilter implements Filter { @Override public void destroy() { } @Override public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException { //处理response的字符编码 HttpServletResponse myResponse=(HttpServletResponse) response; myResponse.setContentType("text/html;charset=UTF-8"); // 转型为与协议相关对象 HttpServletRequest httpServletRequest = (HttpServletRequest) request; // 对request包装增强 HttpServletRequest myrequest = new MyRequest(httpServletRequest); chain.doFilter(myrequest, response); } @Override public void init(FilterConfig filterConfig) throws ServletException { } } //自定义request对象,HttpServletRequest的包装类 class MyRequest extends HttpServletRequestWrapper { private HttpServletRequest request; //是否编码的标记 private boolean hasEncode; //定义一个可以传入HttpServletRequest对象的构造函数,以便对其进行装饰 public MyRequest(HttpServletRequest request) { super(request);// super必须写 this.request = request; } // 对需要增强方法 进行覆盖 @Override public Map getParameterMap() { // 先获得请求方式 String method = request.getMethod(); if (method.equalsIgnoreCase("post")) { // post请求 try { // 处理post乱码 request.setCharacterEncoding("utf-8"); return request.getParameterMap(); } catch (UnsupportedEncodingException e) { e.printStackTrace(); } } else if (method.equalsIgnoreCase("get")) { // get请求 Map<String, String[]> parameterMap = request.getParameterMap(); if (!hasEncode) { // 确保get手动编码逻辑只运行一次 for (String parameterName : parameterMap.keySet()) { String[] values = parameterMap.get(parameterName); if (values != null) { for (int i = 0; i < values.length; i++) { try { // 处理get乱码 values[i] = new String(values[i] .getBytes("ISO-8859-1"), "utf-8"); } catch (UnsupportedEncodingException e) { e.printStackTrace(); } } } } hasEncode = true; } return parameterMap; } return super.getParameterMap(); } //取一个值 @Override public String getParameter(String name) { Map<String, String[]> parameterMap = getParameterMap(); String[] values = parameterMap.get(name); if (values == null) { return null; } return values[0]; // 取回参数的第一个值 } //取所有值 @Override public String[] getParameterValues(String name) { Map<String, String[]> parameterMap = getParameterMap(); String[] values = parameterMap.get(name); return values; } }这个也是我在网上找的一些大神写的,一般情况下,SpringMVC默认的乱码处理就已经能够很好的解决了!然后在web.xml中配置这个过滤器即可!乱码问题,需要平时多注意,在尽可能能设置编码的地方,都设置为统一编码 UTF-8!7.整合SSM7.1 环境要求环境:IDEAMySQL 5.7.19Tomcat 9Maven 3.6要求:需要熟练掌握MySQL数据库,Spring,JavaWeb及MyBatis知识,简单的前端知识;7.2 数据库环境创建一个存放书籍数据的数据库表CREATE DATABASE `ssmbuild`; USE `ssmbuild`; DROP TABLE IF EXISTS `books`; CREATE TABLE `books` ( `bookID` INT(10) NOT NULL AUTO_INCREMENT COMMENT '书id', `bookName` VARCHAR(100) NOT NULL COMMENT '书名', `bookCounts` INT(11) NOT NULL COMMENT '数量', `detail` VARCHAR(200) NOT NULL COMMENT '描述', KEY `bookID` (`bookID`) ) ENGINE=INNODB DEFAULT CHARSET=utf8 INSERT INTO `books`(`bookID`,`bookName`,`bookCounts`,`detail`)VALUES (1,'Java',1,'从入门到放弃'), (2,'MySQL',10,'从删库到跑路'), (3,'Linux',5,'从进门到进牢');7.3 基本环境搭建1、新建一Maven项目!ssmbuild , 添加web的支持2、导入相关的pom依赖!<dependencies> <!--Junit--> <dependency> <groupId>junit</groupId> <artifactId>junit</artifactId> <version>4.12</version> </dependency> <!--数据库驱动--> <dependency> <groupId>mysql</groupId> <artifactId>mysql-connector-java</artifactId> <version>5.1.47</version> </dependency> <!-- 数据库连接池 --> <dependency> <groupId>com.mchange</groupId> <artifactId>c3p0</artifactId> <version>0.9.5.2</version> </dependency> <!--Servlet - JSP --> <dependency> <groupId>javax.servlet</groupId> <artifactId>servlet-api</artifactId> <version>2.5</version> </dependency> <dependency> <groupId>javax.servlet.jsp</groupId> <artifactId>jsp-api</artifactId> <version>2.2</version> </dependency> <dependency> <groupId>javax.servlet</groupId> <artifactId>jstl</artifactId> <version>1.2</version> </dependency> <!--Mybatis--> <dependency> <groupId>org.mybatis</groupId> <artifactId>mybatis</artifactId> <version>3.5.2</version> </dependency> <dependency> <groupId>org.mybatis</groupId> <artifactId>mybatis-spring</artifactId> <version>2.0.2</version> </dependency> <!--Spring--> <dependency> <groupId>org.springframework</groupId> <artifactId>spring-webmvc</artifactId> <version>5.1.9.RELEASE</version> </dependency> <dependency> <groupId>org.springframework</groupId> <artifactId>spring-jdbc</artifactId> <version>5.1.9.RELEASE</version> </dependency> </dependencies>3、Maven资源过滤设置<build> <resources> <resource> <directory>src/main/java</directory> <includes> <include>**/*.properties</include> <include>**/*.xml</include> </includes> <filtering>false</filtering> </resource> <resource> <directory>src/main/resources</directory> <includes> <include>**/*.properties</include> <include>**/*.xml</include> </includes> <filtering>false</filtering> </resource> </resources> </build>4、建立基本结构和配置框架!com.kuang.pojocom.kuang.daocom.kuang.servicecom.kuang.controllermybatis-config.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> </configuration>applicationContext.xml<?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"> </beans>Mybatis层编写1、数据库配置文件 database.propertiesjdbc.driver=com.mysql.jdbc.Driver jdbc.url=jdbc:mysql://localhost:3306/ssmbuild?useSSL=true&useUnicode=true&characterEncoding=utf8 jdbc.username=root jdbc.password=1234562、IDEA关联数据库3、编写MyBatis的核心配置文件<?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> <typeAliases> <package name="com.kuang.pojo"/> </typeAliases> <mappers> <mapper resource="com/kuang/dao/BookMapper.xml"/> </mappers> </configuration>4、编写数据库对应的实体类 com.kuang.pojo.Books使用lombok插件!package com.kuang.pojo; import lombok.AllArgsConstructor; import lombok.Data; import lombok.NoArgsConstructor; @Data @AllArgsConstructor @NoArgsConstructor public class Books { private int bookID; private String bookName; private int bookCounts; private String detail; }5、编写Dao层的 Mapper接口!package com.kuang.dao; import com.kuang.pojo.Books; import java.util.List; public interface BookMapper { //增加一个Book int addBook(Books book); //根据id删除一个Book int deleteBookById(int id); //更新Book int updateBook(Books books); //根据id查询,返回一个Book Books queryBookById(int id); //查询全部Book,返回list集合 List<Books> queryAllBook(); }6、编写接口对应的 Mapper.xml 文件。需要导入MyBatis的包;<?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.kuang.dao.BookMapper"> <!--增加一个Book--> <insert id="addBook" parameterType="Books"> insert into ssmbuild.books(bookName,bookCounts,detail) values (#{bookName}, #{bookCounts}, #{detail}) </insert> <!--根据id删除一个Book--> <delete id="deleteBookById" parameterType="int"> delete from ssmbuild.books where bookID=#{bookID} </delete> <!--更新Book--> <update id="updateBook" parameterType="Books"> update ssmbuild.books set bookName = #{bookName},bookCounts = #{bookCounts},detail = #{detail} where bookID = #{bookID} </update> <!--根据id查询,返回一个Book--> <select id="queryBookById" resultType="Books"> select * from ssmbuild.books where bookID = #{bookID} </select> <!--查询全部Book--> <select id="queryAllBook" resultType="Books"> SELECT * from ssmbuild.books </select> </mapper>7、编写Service层的接口和实现类接口:package com.kuang.service; import com.kuang.pojo.Books; import java.util.List; //BookService:底下需要去实现,调用dao层 public interface BookService { //增加一个Book int addBook(Books book); //根据id删除一个Book int deleteBookById(int id); //更新Book int updateBook(Books books); //根据id查询,返回一个Book Books queryBookById(int id); //查询全部Book,返回list集合 List<Books> queryAllBook(); }实现类:package com.kuang.service; import com.kuang.dao.BookMapper; import com.kuang.pojo.Books; import java.util.List; public class BookServiceImpl implements BookService { //调用dao层的操作,设置一个set接口,方便Spring管理 private BookMapper bookMapper; public void setBookMapper(BookMapper bookMapper) { this.bookMapper = bookMapper; } public int addBook(Books book) { return bookMapper.addBook(book); } public int deleteBookById(int id) { return bookMapper.deleteBookById(id); } public int updateBook(Books books) { return bookMapper.updateBook(books); } public Books queryBookById(int id) { return bookMapper.queryBookById(id); } public List<Books> queryAllBook() { return bookMapper.queryAllBook(); } }OK,到此,底层需求操作编写完毕!7.4 Spring层1、配置Spring整合MyBatis,我们这里数据源使用c3p0连接池;2、我们去编写Spring整合Mybatis的相关的配置文件;spring-dao.xml<?xml version="1.0" encoding="UTF-8"?> <beans xmlns="http://www.springframework.org/schema/beans" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:context="http://www.springframework.org/schema/context" xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd http://www.springframework.org/schema/context https://www.springframework.org/schema/context/spring-context.xsd"> <!-- 配置整合mybatis --> <!-- 1.关联数据库文件 --> <context:property-placeholder location="classpath:database.properties"/> <!-- 2.数据库连接池 --> <!--数据库连接池 dbcp 半自动化操作 不能自动连接 c3p0 自动化操作(自动的加载配置文件 并且设置到对象里面) --> <bean id="dataSource" class="com.mchange.v2.c3p0.ComboPooledDataSource"> <!-- 配置连接池属性 --> <property name="driverClass" value="${jdbc.driver}"/> <property name="jdbcUrl" value="${jdbc.url}"/> <property name="user" value="${jdbc.username}"/> <property name="password" value="${jdbc.password}"/> <!-- c3p0连接池的私有属性 --> <property name="maxPoolSize" value="30"/> <property name="minPoolSize" value="10"/> <!-- 关闭连接后不自动commit --> <property name="autoCommitOnClose" value="false"/> <!-- 获取连接超时时间 --> <property name="checkoutTimeout" value="10000"/> <!-- 当获取连接失败重试次数 --> <property name="acquireRetryAttempts" value="2"/> </bean> <!-- 3.配置SqlSessionFactory对象 --> <bean id="sqlSessionFactory" class="org.mybatis.spring.SqlSessionFactoryBean"> <!-- 注入数据库连接池 --> <property name="dataSource" ref="dataSource"/> <!-- 配置MyBaties全局配置文件:mybatis-config.xml --> <property name="configLocation" value="classpath:mybatis-config.xml"/> </bean> <!-- 4.配置扫描Dao接口包,动态实现Dao接口注入到spring容器中 --> <!--解释 :https://www.cnblogs.com/jpfss/p/7799806.html--> <bean class="org.mybatis.spring.mapper.MapperScannerConfigurer"> <!-- 注入sqlSessionFactory --> <property name="sqlSessionFactoryBeanName" value="sqlSessionFactory"/> <!-- 给出需要扫描Dao接口包 --> <property name="basePackage" value="com.kuang.dao"/> </bean> </beans>3、Spring整合service层<?xml version="1.0" encoding="UTF-8"?> <beans xmlns="http://www.springframework.org/schema/beans" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:context="http://www.springframework.org/schema/context" xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context.xsd"> <!-- 扫描service相关的bean --> <context:component-scan base-package="com.kuang.service" /> <!--BookServiceImpl注入到IOC容器中--> <bean id="BookServiceImpl" class="com.kuang.service.BookServiceImpl"> <property name="bookMapper" ref="bookMapper"/> </bean> <!-- 配置事务管理器 --> <bean id="transactionManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager"> <!-- 注入数据库连接池 --> <property name="dataSource" ref="dataSource" /> </bean> </beans>Spring层搞定!再次理解一下,Spring就是一个大杂烩,一个容器!对吧!7.5 SpringMVC层1、web.xml<?xml version="1.0" encoding="UTF-8"?> <web-app xmlns="http://xmlns.jcp.org/xml/ns/javaee" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://xmlns.jcp.org/xml/ns/javaee http://xmlns.jcp.org/xml/ns/javaee/web-app_4_0.xsd" version="4.0"> <!--DispatcherServlet--> <servlet> <servlet-name>DispatcherServlet</servlet-name> <servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class> <init-param> <param-name>contextConfigLocation</param-name> <!--一定要注意:我们这里加载的是总的配置文件,之前被这里坑了!--> <param-value>classpath:applicationContext.xml</param-value> </init-param> <load-on-startup>1</load-on-startup> </servlet> <servlet-mapping> <servlet-name>DispatcherServlet</servlet-name> <url-pattern>/</url-pattern> </servlet-mapping> <!--encodingFilter--> <filter> <filter-name>encodingFilter</filter-name> <filter-class> org.springframework.web.filter.CharacterEncodingFilter </filter-class> <init-param> <param-name>encoding</param-name> <param-value>utf-8</param-value> </init-param> </filter> <filter-mapping> <filter-name>encodingFilter</filter-name> <url-pattern>/*</url-pattern> </filter-mapping> <!--Session过期时间--> <session-config> <session-timeout>15</session-timeout> </session-config> </web-app>2、spring-mvc.xml<?xml version="1.0" encoding="UTF-8"?> <beans xmlns="http://www.springframework.org/schema/beans" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:context="http://www.springframework.org/schema/context" xmlns:mvc="http://www.springframework.org/schema/mvc" xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context.xsd http://www.springframework.org/schema/mvc https://www.springframework.org/schema/mvc/spring-mvc.xsd"> <!-- 配置SpringMVC --> <!-- 1.开启SpringMVC注解驱动 --> <mvc:annotation-driven /> <!-- 2.静态资源默认servlet配置--> <mvc:default-servlet-handler/> <!-- 3.配置jsp 显示ViewResolver视图解析器 --> <bean class="org.springframework.web.servlet.view.InternalResourceViewResolver"> <property name="viewClass" value="org.springframework.web.servlet.view.JstlView" /> <property name="prefix" value="/WEB-INF/jsp/" /> <property name="suffix" value=".jsp" /> </bean> <!-- 4.扫描web相关的bean --> <context:component-scan base-package="com.kuang.controller" /> </beans>3、Spring配置整合文件,applicationContext.xml<?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"> <import resource="spring-dao.xml"/> <import resource="spring-service.xml"/> <import resource="spring-mvc.xml"/> </beans>配置文件,暂时结束!Controller 和 视图层编写1、BookController 类编写 , 方法一:查询全部书籍@Controller @RequestMapping("/book") public class BookController { @Autowired @Qualifier("BookServiceImpl") private BookService bookService; @RequestMapping("/allBook") public String list(Model model) { List<Books> list = bookService.queryAllBook(); model.addAttribute("list", list); return "allBook"; } }2、编写首页 index.jsp<%@ page language="java" contentType="text/html; charset=UTF-8" pageEncoding="UTF-8" %> <!DOCTYPE HTML> <html> <head> <title>首页</title> <style type="text/css"> a { text-decoration: none; color: black; font-size: 18px; } h3 { width: 180px; height: 38px; margin: 100px auto; text-align: center; line-height: 38px; background: deepskyblue; border-radius: 4px; } </style> </head> <body> <h3> <a href="${pageContext.request.contextPath}/book/allBook">点击进入列表页</a> </h3> </body> </html>3、书籍列表页面 allbook.jsp<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %> <%@ page contentType="text/html;charset=UTF-8" language="java" %> <html> <head> <title>书籍列表</title> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <!-- 引入 Bootstrap --> <link href="https://cdn.bootcss.com/bootstrap/3.3.7/css/bootstrap.min.css" rel="stylesheet"> </head> <body> <div class="container"> <div class="row clearfix"> <div class="col-md-12 column"> <div class="page-header"> <h1> <small>书籍列表 —— 显示所有书籍</small> </h1> </div> </div> </div> <div class="row"> <div class="col-md-4 column"> <a class="btn btn-primary" href="${pageContext.request.contextPath}/book/toAddBook">新增</a> </div> </div> <div class="row clearfix"> <div class="col-md-12 column"> <table class="table table-hover table-striped"> <thead> <tr> <th>书籍编号</th> <th>书籍名字</th> <th>书籍数量</th> <th>书籍详情</th> <th>操作</th> </tr> </thead> <tbody> <c:forEach var="book" items="${requestScope.get('list')}"> <tr> <td>${book.getBookID()}</td> <td>${book.getBookName()}</td> <td>${book.getBookCounts()}</td> <td>${book.getDetail()}</td> <td> <a href="${pageContext.request.contextPath}/book/toUpdateBook?id=${book.getBookID()}">更改</a> | <a href="${pageContext.request.contextPath}/book/del/${book.getBookID()}">删除</a> </td> </tr> </c:forEach> </tbody> </table> </div> </div> </div>4、BookController 类编写 , 方法二:添加书籍@RequestMapping("/toAddBook") public String toAddPaper() { return "addBook"; } @RequestMapping("/addBook") public String addPaper(Books books) { System.out.println(books); bookService.addBook(books); return "redirect:/book/allBook"; }5、添加书籍页面:addBook.jsp<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %> <%@ page contentType="text/html;charset=UTF-8" language="java" %> <html> <head> <title>新增书籍</title> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <!-- 引入 Bootstrap --> <link href="https://cdn.bootcss.com/bootstrap/3.3.7/css/bootstrap.min.css" rel="stylesheet"> </head> <body> <div class="container"> <div class="row clearfix"> <div class="col-md-12 column"> <div class="page-header"> <h1> <small>新增书籍</small> </h1> </div> </div> </div> <form action="${pageContext.request.contextPath}/book/addBook" method="post"> 书籍名称:<input type="text" name="bookName"><br><br><br> 书籍数量:<input type="text" name="bookCounts"><br><br><br> 书籍详情:<input type="text" name="detail"><br><br><br> <input type="submit" value="添加"> </form> </div>6、BookController 类编写 , 方法三:修改书籍@RequestMapping("/toUpdateBook") public String toUpdateBook(Model model, int id) { Books books = bookService.queryBookById(id); System.out.println(books); model.addAttribute("book",books ); return "updateBook"; } @RequestMapping("/updateBook") public String updateBook(Model model, Books book) { System.out.println(book); bookService.updateBook(book); Books books = bookService.queryBookById(book.getBookID()); model.addAttribute("books", books); return "redirect:/book/allBook"; }7、修改书籍页面 updateBook.jsp<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %> <%@ page contentType="text/html;charset=UTF-8" language="java" %> <html> <head> <title>修改信息</title> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <!-- 引入 Bootstrap --> <link href="https://cdn.bootcss.com/bootstrap/3.3.7/css/bootstrap.min.css" rel="stylesheet"> </head> <body> <div class="container"> <div class="row clearfix"> <div class="col-md-12 column"> <div class="page-header"> <h1> <small>修改信息</small> </h1> </div> </div> </div> <form action="${pageContext.request.contextPath}/book/updateBook" method="post"> <input type="hidden" name="bookID" value="${book.getBookID()}"/> 书籍名称:<input type="text" name="bookName" value="${book.getBookName()}"/> 书籍数量:<input type="text" name="bookCounts" value="${book.getBookCounts()}"/> 书籍详情:<input type="text" name="detail" value="${book.getDetail() }"/> <input type="submit" value="提交"/> </form> </div>8、BookController 类编写 , 方法四:删除书籍@RequestMapping("/del/{bookId}") public String deleteBook(@PathVariable("bookId") int id) { bookService.deleteBookById(id); return "redirect:/book/allBook"; }配置Tomcat,进行运行!到目前为止,这个SSM项目整合已经完全的OK了,可以直接运行进行测试!这个练习十分的重要,大家需要保证,不看任何东西,自己也可以完整的实现出来!7.6 小结及展望这个是同学们的第一个SSM整合案例,一定要烂熟于心!SSM框架的重要程度是不言而喻的,学到这里,大家已经可以进行基本网站的单独开发。但是这只是增删改查的基本操作。可以说学到这里,大家才算是真正的步入了后台开发的门。也就是能找一个后台相关工作的底线。或许很多人,工作就做这些事情,但是对于个人的提高来说,还远远不够!我们后面还要学习一些 SpringMVC 的知识!Ajax 和 Json文件上传和下载拦截器8.Json8.1 什么是JSON?JSON(JavaScript Object Notation, JS 对象标记) 是一种轻量级的数据交换格式,目前使用特别广泛。采用完全独立于编程语言的文本格式来存储和表示数据。简洁和清晰的层次结构使得 JSON 成为理想的数据交换语言。易于人阅读和编写,同时也易于机器解析和生成,并有效地提升网络传输效率。在 JavaScript 语言中,一切都是对象。因此,任何JavaScript 支持的类型都可以通过 JSON 来表示,例如字符串、数字、对象、数组等。看看他的要求和语法格式:对象表示为键值对,数据由逗号分隔花括号保存对象方括号保存数组JSON 键值对是用来保存 JavaScript 对象的一种方式,和 JavaScript 对象的写法也大同小异,键/值对组合中的键名写在前面并用双引号 "" 包裹,使用冒号 : 分隔,然后紧接着值:{"name": "QinJiang"} {"age": "3"} {"sex": "男"}很多人搞不清楚 JSON 和 JavaScript 对象的关系,甚至连谁是谁都不清楚。其实,可以这么理解:JSON 是 JavaScript 对象的字符串表示法,它使用文本表示一个 JS 对象的信息,本质是一个字符串。var obj = {a: 'Hello', b: 'World'}; //这是一个对象,注意键名也是可以使用引号包裹的 var json = '{"a": "Hello", "b": "World"}'; //这是一个 JSON 字符串,本质是一个字符串JSON 和 JavaScript 对象互转要实现从JSON字符串转换为JavaScript 对象,使用 JSON.parse() 方法:var obj = JSON.parse('{"a": "Hello", "b": "World"}'); //结果是 {a: 'Hello', b: 'World'}要实现从JavaScript 对象转换为JSON字符串,使用 JSON.stringify() 方法:var json = JSON.stringify({a: 'Hello', b: 'World'}); //结果是 '{"a": "Hello", "b": "World"}'代码测试1、新建一个module ,springmvc-05-json , 添加web的支持2、在web目录下新建一个 json-1.html , 编写测试内容<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <title>JSON_秦疆</title> </head> <body> <script type="text/javascript"> //编写一个js的对象 var user = { name:"秦疆", age:3, sex:"男" }; //将js对象转换成json字符串 var str = JSON.stringify(user); console.log(str); //将json字符串转换为js对象 var user2 = JSON.parse(str); console.log(user2.age,user2.name,user2.sex); </script> </body> </html>3、在IDEA中使用浏览器打开,查看控制台输出!8.2 Controller返回JSON数据Jackson应该是目前比较好的json解析工具了当然工具不止这一个,比如还有阿里巴巴的 fastjson 等等。我们这里使用Jackson,使用它需要导入它的jar包;<!-- https://mvnrepository.com/artifact/com.fasterxml.jackson.core/jackson-core --> <dependency> <groupId>com.fasterxml.jackson.core</groupId> <artifactId>jackson-databind</artifactId> <version>2.9.8</version> </dependency>配置SpringMVC需要的配置web.xml<?xml version="1.0" encoding="UTF-8"?> <web-app xmlns="http://xmlns.jcp.org/xml/ns/javaee" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://xmlns.jcp.org/xml/ns/javaee http://xmlns.jcp.org/xml/ns/javaee/web-app_4_0.xsd" version="4.0"> <!--1.注册servlet--> <servlet> <servlet-name>SpringMVC</servlet-name> <servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class> <!--通过初始化参数指定SpringMVC配置文件的位置,进行关联--> <init-param> <param-name>contextConfigLocation</param-name> <param-value>classpath:springmvc-servlet.xml</param-value> </init-param> <!-- 启动顺序,数字越小,启动越早 --> <load-on-startup>1</load-on-startup> </servlet> <!--所有请求都会被springmvc拦截 --> <servlet-mapping> <servlet-name>SpringMVC</servlet-name> <url-pattern>/</url-pattern> </servlet-mapping> <filter> <filter-name>encoding</filter-name> <filter-class>org.springframework.web.filter.CharacterEncodingFilter</filter-class> <init-param> <param-name>encoding</param-name> <param-value>utf-8</param-value> </init-param> </filter> <filter-mapping> <filter-name>encoding</filter-name> <url-pattern>/</url-pattern> </filter-mapping> </web-app>springmvc-servlet.xml<?xml version="1.0" encoding="UTF-8"?> <beans xmlns="http://www.springframework.org/schema/beans" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:context="http://www.springframework.org/schema/context" xmlns:mvc="http://www.springframework.org/schema/mvc" xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd http://www.springframework.org/schema/context https://www.springframework.org/schema/context/spring-context.xsd http://www.springframework.org/schema/mvc https://www.springframework.org/schema/mvc/spring-mvc.xsd"> <!-- 自动扫描指定的包,下面所有注解类交给IOC容器管理 --> <context:component-scan base-package="com.kuang.controller"/> <!-- 视图解析器 --> <bean class="org.springframework.web.servlet.view.InternalResourceViewResolver" id="internalResourceViewResolver"> <!-- 前缀 --> <property name="prefix" value="/WEB-INF/jsp/" /> <!-- 后缀 --> <property name="suffix" value=".jsp" /> </bean> </beans>我们随便编写一个User的实体类,然后我们去编写我们的测试Controller;package com.kuang.pojo; import lombok.AllArgsConstructor; import lombok.Data; import lombok.NoArgsConstructor; //需要导入lombok @Data @AllArgsConstructor @NoArgsConstructor public class User { private String name; private int age; private String sex; }这里我们需要两个新东西,一个是@ResponseBody,一个是ObjectMapper对象,我们看下具体的用法编写一个Controller;@Controller public class UserController { @RequestMapping("/json1") @ResponseBody public String json1() throws JsonProcessingException { //创建一个jackson的对象映射器,用来解析数据 ObjectMapper mapper = new ObjectMapper(); //创建一个对象 User user = new User("秦疆1号", 3, "男"); //将我们的对象解析成为json格式 String str = mapper.writeValueAsString(user); //由于@ResponseBody注解,这里会将str转成json格式返回;十分方便 return str; } }配置Tomcat , 启动测试一下!http://localhost:8080/json1发现出现了乱码问题,我们需要设置一下他的编码格式为utf-8,以及它返回的类型;通过@RequestMaping的produces属性来实现,修改下代码//produces:指定响应体返回类型和编码 @RequestMapping(value = "/json1",produces = "application/json;charset=utf-8")再次测试, http://localhost:8080/json1 , 乱码问题OK!【注意:使用json记得处理乱码问题】8.3 代码优化乱码统一解决上一种方法比较麻烦,如果项目中有许多请求则每一个都要添加,可以通过Spring配置统一指定,这样就不用每次都去处理了!我们可以在springmvc的配置文件上添加一段消息StringHttpMessageConverter转换配置!<mvc:annotation-driven> <mvc:message-converters register-defaults="true"> <bean class="org.springframework.http.converter.StringHttpMessageConverter"> <constructor-arg value="UTF-8"/> </bean> <bean class="org.springframework.http.converter.json.MappingJackson2HttpMessageConverter"> <property name="objectMapper"> <bean class="org.springframework.http.converter.json.Jackson2ObjectMapperFactoryBean"> <property name="failOnEmptyBeans" value="false"/> </bean> </property> </bean> </mvc:message-converters> </mvc:annotation-driven>返回json字符串统一解决在类上直接使用 @RestController ,这样子,里面所有的方法都只会返回 json 字符串了,不用再每一个都添加@ResponseBody !我们在前后端分离开发中,一般都使用 @RestController ,十分便捷!@RestController public class UserController { //produces:指定响应体返回类型和编码 @RequestMapping(value = "/json1") public String json1() throws JsonProcessingException { //创建一个jackson的对象映射器,用来解析数据 ObjectMapper mapper = new ObjectMapper(); //创建一个对象 User user = new User("秦疆1号", 3, "男"); //将我们的对象解析成为json格式 String str = mapper.writeValueAsString(user); //由于@ResponseBody注解,这里会将str转成json格式返回;十分方便 return str; } }启动tomcat测试,结果都正常输出!8.4 测试集合输出增加一个新的方法@RequestMapping("/json2") public String json2() throws JsonProcessingException { //创建一个jackson的对象映射器,用来解析数据 ObjectMapper mapper = new ObjectMapper(); //创建一个对象 User user1 = new User("秦疆1号", 3, "男"); User user2 = new User("秦疆2号", 3, "男"); User user3 = new User("秦疆3号", 3, "男"); User user4 = new User("秦疆4号", 3, "男"); List<User> list = new ArrayList<User>(); list.add(user1); list.add(user2); list.add(user3); list.add(user4); //将我们的对象解析成为json格式 String str = mapper.writeValueAsString(list); return str; }运行结果 : 十分完美,没有任何问题!8.5 输出时间对象增加一个新的方法@RequestMapping("/json3") public String json3() throws JsonProcessingException { ObjectMapper mapper = new ObjectMapper(); //创建时间一个对象,java.util.Date Date date = new Date(); //将我们的对象解析成为json格式 String str = mapper.writeValueAsString(date); return str; }运行结果 :默认日期格式会变成一个数字,是1970年1月1日到当前日期的毫秒数!Jackson 默认是会把时间转成timestamps形式解决方案:取消timestamps形式 , 自定义时间格式@RequestMapping("/json4") public String json4() throws JsonProcessingException { ObjectMapper mapper = new ObjectMapper(); //不使用时间戳的方式 mapper.configure(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS, false); //自定义日期格式对象 SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"); //指定日期格式 mapper.setDateFormat(sdf); Date date = new Date(); String str = mapper.writeValueAsString(date); return str; }运行结果 : 成功的输出了时间!8.6 抽取为工具类如果要经常使用的话,这样是比较麻烦的,我们可以将这些代码封装到一个工具类中;我们去编写下package com.kuang.utils; import com.fasterxml.jackson.core.JsonProcessingException; import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.databind.SerializationFeature; import java.text.SimpleDateFormat; public class JsonUtils { public static String getJson(Object object) { return getJson(object,"yyyy-MM-dd HH:mm:ss"); } public static String getJson(Object object,String dateFormat) { ObjectMapper mapper = new ObjectMapper(); //不使用时间差的方式 mapper.configure(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS, false); //自定义日期格式对象 SimpleDateFormat sdf = new SimpleDateFormat(dateFormat); //指定日期格式 mapper.setDateFormat(sdf); try { return mapper.writeValueAsString(object); } catch (JsonProcessingException e) { e.printStackTrace(); } return null; } }我们使用工具类,代码就更加简洁了!@RequestMapping("/json5") public String json5() throws JsonProcessingException { Date date = new Date(); String json = JsonUtils.getJson(date); return json; }大功告成!完美!8.7 FastJsonfastjson.jar是阿里开发的一款专门用于Java开发的包,可以方便的实现json对象与JavaBean对象的转换,实现JavaBean对象与json字符串的转换,实现json对象与json字符串的转换。实现json的转换方法很多,最后的实现结果都是一样的。fastjson 的 pom依赖!<dependency> <groupId>com.alibaba</groupId> <artifactId>fastjson</artifactId> <version>1.2.60</version> </dependency>fastjson 三个主要的类:JSONObject 代表 json 对象JSONObject实现了Map接口, 猜想 JSONObject底层操作是由Map实现的。JSONObject对应json对象,通过各种形式的get()方法可以获取json对象中的数据,也可利用诸如size(),isEmpty()等方法获取"键:值"对的个数和判断是否为空。其本质是通过实现Map接口并调用接口中的方法完成的。JSONArray 代表 json 对象数组内部是有List接口中的方法来完成操作的。JSON代表 JSONObject和JSONArray的转化JSON类源码分析与使用仔细观察这些方法,主要是实现json对象,json对象数组,javabean对象,json字符串之间的相互转化。代码测试,我们新建一个FastJsonDemo 类package com.kuang.controller; import com.alibaba.fastjson.JSON; import com.alibaba.fastjson.JSONObject; import com.kuang.pojo.User; import java.util.ArrayList; import java.util.List; public class FastJsonDemo { public static void main(String[] args) { //创建一个对象 User user1 = new User("秦疆1号", 3, "男"); User user2 = new User("秦疆2号", 3, "男"); User user3 = new User("秦疆3号", 3, "男"); User user4 = new User("秦疆4号", 3, "男"); List<User> list = new ArrayList<User>(); list.add(user1); list.add(user2); list.add(user3); list.add(user4); System.out.println("*******Java对象 转 JSON字符串*******"); String str1 = JSON.toJSONString(list); System.out.println("JSON.toJSONString(list)==>"+str1); String str2 = JSON.toJSONString(user1); System.out.println("JSON.toJSONString(user1)==>"+str2); System.out.println("\n****** JSON字符串 转 Java对象*******"); User jp_user1=JSON.parseObject(str2,User.class); System.out.println("JSON.parseObject(str2,User.class)==>"+jp_user1); System.out.println("\n****** Java对象 转 JSON对象 ******"); JSONObject jsonObject1 = (JSONObject) JSON.toJSON(user2); System.out.println("(JSONObject) JSON.toJSON(user2)==>"+jsonObject1.getString("name")); System.out.println("\n****** JSON对象 转 Java对象 ******"); User to_java_user = JSON.toJavaObject(jsonObject1, User.class); System.out.println("JSON.toJavaObject(jsonObject1, User.class)==>"+to_java_user); } }这种工具类,我们只需要掌握使用就好了,在使用的时候在根据具体的业务去找对应的实现。和以前的commons-io那种工具包一样,拿来用就好了!9.Ajax研究9.1 简介AJAX = Asynchronous JavaScript and XML(异步的 JavaScript 和 XML)。AJAX 是一种在无需重新加载整个网页的情况下,能够更新部分网页的技术。Ajax 不是一种新的编程语言,而是一种用于创建更好更快以及交互性更强的Web应用程序的技术。在 2005 年,Google 通过其 Google Suggest 使 AJAX 变得流行起来。Google Suggest能够自动帮你完成搜索单词。Google Suggest 使用 AJAX 创造出动态性极强的 web 界面:当您在谷歌的搜索框输入关键字时,JavaScript 会把这些字符发送到服务器,然后服务器会返回一个搜索建议的列表。就和国内百度的搜索框一样!传统的网页(即不用ajax技术的网页),想要更新内容或者提交一个表单,都需要重新加载整个网页。使用ajax技术的网页,通过在后台服务器进行少量的数据交换,就可以实现异步局部更新。使用Ajax,用户可以创建接近本地桌面应用的直接、高可用、更丰富、更动态的Web用户界面。9.2 伪造Ajax我们可以使用前端的一个标签来伪造一个ajax的样子。iframe标签1、新建一个module :sspringmvc-06-ajax , 导入web支持!2、编写一个 ajax-frame.html 使用 iframe 测试,感受下效果<!DOCTYPE html> <html> <head lang="en"> <meta charset="UTF-8"> <title>kuangshen</title> </head> <body> <script type="text/javascript"> window.onload = function(){ var myDate = new Date(); document.getElementById('currentTime').innerText = myDate.getTime(); }; function LoadPage(){ var targetUrl = document.getElementById('url').value; console.log(targetUrl); document.getElementById("iframePosition").src = targetUrl; } </script> <div> <p>请输入要加载的地址:<span id="currentTime"></span></p> <p> <input id="url" type="text" value="https://www.baidu.com/"/> <input type="button" value="提交" onclick="LoadPage()"> </p> </div> <div> <h3>加载页面位置:</h3> <iframe id="iframePosition" style="width: 100%;height: 500px;"></iframe> </div> </body> </html>3、使用IDEA开浏览器测试一下!利用AJAX可以做:注册时,输入用户名自动检测用户是否已经存在。登陆时,提示用户名密码错误删除数据行时,将行ID发送到后台,后台在数据库中删除,数据库删除成功后,在页面DOM中将数据行也删除。....等等9.3 jQuery.ajax纯JS原生实现Ajax我们不去讲解这里,直接使用jquery提供的,方便学习和使用,避免重复造轮子,有兴趣的同学可以去了解下JS原生XMLHttpRequest !Ajax的核心是XMLHttpRequest对象(XHR)。XHR为向服务器发送请求和解析服务器响应提供了接口。能够以异步方式从服务器获取新数据。jQuery 提供多个与 AJAX 有关的方法。通过 jQuery AJAX 方法,您能够使用 HTTP Get 和 HTTP Post 从远程服务器上请求文本、HTML、XML 或 JSON – 同时您能够把这些外部数据直接载入网页的被选元素中。jQuery 不是生产者,而是大自然搬运工。jQuery Ajax本质就是 XMLHttpRequest,对他进行了封装,方便调用!jQuery.ajax(...) 部分参数: url:请求地址 type:请求方式,GET、POST(1.9.0之后用method) headers:请求头 data:要发送的数据 contentType:即将发送信息至服务器的内容编码类型(默认: "application/x-www-form-urlencoded; charset=UTF-8") async:是否异步 timeout:设置请求超时时间(毫秒) beforeSend:发送请求前执行的函数(全局) complete:完成之后执行的回调函数(全局) success:成功之后执行的回调函数(全局) error:失败之后执行的回调函数(全局) accepts:通过请求头发送给服务器,告诉服务器当前客户端可接受的数据类型 dataType:将服务器端返回的数据转换成指定类型 "xml": 将服务器端返回的内容转换成xml格式 "text": 将服务器端返回的内容转换成普通文本格式 "html": 将服务器端返回的内容转换成普通文本格式,在插入DOM中时,如果包含JavaScript标签,则会尝试去执行。 "script": 尝试将返回值当作JavaScript去执行,然后再将服务器端返回的内容转换成普通文本格式 "json": 将服务器端返回的内容转换成相应的JavaScript对象 "jsonp": JSONP 格式使用 JSONP 形式调用函数时,如 "myurl?callback=?" jQuery 将自动替换 ? 为正确的函数名,以执行回调函数我们来个简单的测试,使用最原始的HttpServletResponse处理 , .最简单 , 最通用1、配置web.xml 和 springmvc的配置文件,复制上面案例的即可 【记得静态资源过滤和注解驱动配置上】<?xml version="1.0" encoding="UTF-8"?> <beans xmlns="http://www.springframework.org/schema/beans" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:context="http://www.springframework.org/schema/context" xmlns:mvc="http://www.springframework.org/schema/mvc" xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd http://www.springframework.org/schema/context https://www.springframework.org/schema/context/spring-context.xsd http://www.springframework.org/schema/mvc https://www.springframework.org/schema/mvc/spring-mvc.xsd"> <!-- 自动扫描指定的包,下面所有注解类交给IOC容器管理 --> <context:component-scan base-package="com.kuang.controller"/> <mvc:default-servlet-handler /> <mvc:annotation-driven /> <!-- 视图解析器 --> <bean class="org.springframework.web.servlet.view.InternalResourceViewResolver" id="internalResourceViewResolver"> <!-- 前缀 --> <property name="prefix" value="/WEB-INF/jsp/" /> <!-- 后缀 --> <property name="suffix" value=".jsp" /> </bean> </beans>2、编写一个AjaxController@Controller public class AjaxController { @RequestMapping("/a1") public void ajax1(String name , HttpServletResponse response) throws IOException { if ("admin".equals(name)){ response.getWriter().print("true"); }else{ response.getWriter().print("false"); } } }3、导入jquery , 可以使用在线的CDN , 也可以下载导入<script src="https://code.jquery.com/jquery-3.1.1.min.js"></script> <script src="${pageContext.request.contextPath}/statics/js/jquery-3.1.1.min.js"></script>4、编写index.jsp测试<%@ page contentType="text/html;charset=UTF-8" language="java" %> <html> <head> <title>$Title$</title> <%--<script src="https://code.jquery.com/jquery-3.1.1.min.js"></script>--%> <script src="${pageContext.request.contextPath}/statics/js/jquery-3.1.1.min.js"></script> <script> function a1(){ $.post({ url:"${pageContext.request.contextPath}/a1", data:{'name':$("#txtName").val()}, success:function (data,status) { alert(data); alert(status); } }); } </script> </head> <body> <%--onblur:失去焦点触发事件--%> 用户名:<input type="text" id="txtName" onblur="a1()"/> </body> </html>5、启动tomcat测试!打开浏览器的控制台,当我们鼠标离开输入框的时候,可以看到发出了一个ajax的请求!是后台返回给我们的结果!测试成功!9.4 Springmvc实现实体类user@Data @AllArgsConstructor @NoArgsConstructor public class User { private String name; private int age; private String sex; }我们来获取一个集合对象,展示到前端页面@RequestMapping("/a2") public List<User> ajax2(){ List<User> list = new ArrayList<User>(); list.add(new User("秦疆1号",3,"男")); list.add(new User("秦疆2号",3,"男")); list.add(new User("秦疆3号",3,"男")); return list; //由于@RestController注解,将list转成json格式返回 }前端页面<%@ page contentType="text/html;charset=UTF-8" language="java" %> <html> <head> <title>Title</title> </head> <body> <input type="button" id="btn" value="获取数据"/> <table width="80%" align="center"> <tr> <td>姓名</td> <td>年龄</td> <td>性别</td> </tr> <tbody id="content"> </tbody> </table> <script src="${pageContext.request.contextPath}/statics/js/jquery-3.1.1.min.js"></script> <script> $(function () { $("#btn").click(function () { $.post("${pageContext.request.contextPath}/a2",function (data) { console.log(data) var html=""; for (var i = 0; i <data.length ; i++) { html+= "<tr>" + "<td>" + data[i].name + "</td>" + "<td>" + data[i].age + "</td>" + "<td>" + data[i].sex + "</td>" + "</tr>" } $("#content").html(html); }); }) }) </script> </body> </html>成功实现了数据回显!可以体会一下Ajax的好处!9.5 注册提示效果我们再测试一个小Demo,思考一下我们平时注册时候,输入框后面的实时提示怎么做到的;如何优化我们写一个Controller@RequestMapping("/a3") public String ajax3(String name,String pwd){ String msg = ""; //模拟数据库中存在数据 if (name!=null){ if ("admin".equals(name)){ msg = "OK"; }else { msg = "用户名输入错误"; } } if (pwd!=null){ if ("123456".equals(pwd)){ msg = "OK"; }else { msg = "密码输入有误"; } } return msg; //由于@RestController注解,将msg转成json格式返回 }前端页面 login.jsp<%@ page contentType="text/html;charset=UTF-8" language="java" %> <html> <head> <title>ajax</title> <script src="${pageContext.request.contextPath}/statics/js/jquery-3.1.1.min.js"></script> <script> function a1(){ $.post({ url:"${pageContext.request.contextPath}/a3", data:{'name':$("#name").val()}, success:function (data) { if (data.toString()=='OK'){ $("#userInfo").css("color","green"); }else { $("#userInfo").css("color","red"); } $("#userInfo").html(data); } }); } function a2(){ $.post({ url:"${pageContext.request.contextPath}/a3", data:{'pwd':$("#pwd").val()}, success:function (data) { if (data.toString()=='OK'){ $("#pwdInfo").css("color","green"); }else { $("#pwdInfo").css("color","red"); } $("#pwdInfo").html(data); } }); } </script> </head> <body> <p> 用户名:<input type="text" id="name" onblur="a1()"/> <span id="userInfo"></span> </p> <p> 密码:<input type="text" id="pwd" onblur="a2()"/> <span id="pwdInfo"></span> </p> </body> </html>【记得处理json乱码问题】测试一下效果,动态请求响应,局部刷新,就是如此!9.6 获取baidu接口Demo<!DOCTYPE HTML> <html> <head> <meta http-equiv="Content-Type" content="text/html; charset=utf-8"> <title>JSONP百度搜索</title> <style> #q{ width: 500px; height: 30px; border:1px solid #ddd; line-height: 30px; display: block; margin: 0 auto; padding: 0 10px; font-size: 14px; } #ul{ width: 520px; list-style: none; margin: 0 auto; padding: 0; border:1px solid #ddd; margin-top: -1px; display: none; } #ul li{ line-height: 30px; padding: 0 10px; } #ul li:hover{ background-color: #f60; color: #fff; } </style> <script> // 2.步骤二 // 定义demo函数 (分析接口、数据) function demo(data){ var Ul = document.getElementById('ul'); var html = ''; // 如果搜索数据存在 把内容添加进去 if (data.s.length) { // 隐藏掉的ul显示出来 Ul.style.display = 'block'; // 搜索到的数据循环追加到li里 for(var i = 0;i<data.s.length;i++){ html += '<li>'+data.s[i]+'</li>'; } // 循环的li写入ul Ul.innerHTML = html; } } // 1.步骤一 window.onload = function(){ // 获取输入框和ul var Q = document.getElementById('q'); var Ul = document.getElementById('ul'); // 事件鼠标抬起时候 Q.onkeyup = function(){ // 如果输入框不等于空 if (this.value != '') { // ☆☆☆☆☆☆☆☆☆☆☆☆☆☆☆☆☆☆JSONPz重点☆☆☆☆☆☆☆☆☆☆☆☆☆☆☆☆☆☆☆☆ // 创建标签 var script = document.createElement('script'); //给定要跨域的地址 赋值给src //这里是要请求的跨域的地址 我写的是百度搜索的跨域地址 script.src = 'https://sp0.baidu.com/5a1Fazu8AA54nxGko9WTAnF6hhy/su?wd='+this.value+'&cb=demo'; // 将组合好的带src的script标签追加到body里 document.body.appendChild(script); } } } </script> </head> <body> <input type="text" id="q" /> <ul id="ul"> </ul> </body> </html>10.拦截器10.1 概述SpringMVC的处理器拦截器类似于Servlet开发中的过滤器Filter,用于对处理器进行预处理和后处理。开发者可以自己定义一些拦截器来实现特定的功能。过滤器与拦截器的区别:拦截器是AOP思想的具体应用。过滤器servlet规范中的一部分,任何java web工程都可以使用在url-pattern中配置了/*之后,可以对所有要访问的资源进行拦截拦截器拦截器是SpringMVC框架自己的,只有使用了SpringMVC框架的工程才能使用拦截器只会拦截访问的控制器方法, 如果访问的是jsp/html/css/image/js是不会进行拦截的10.2 自定义拦截器那如何实现拦截器呢?想要自定义拦截器,必须实现 HandlerInterceptor 接口。1、新建一个Moudule , springmvc-07-Interceptor , 添加web支持2、配置web.xml 和 springmvc-servlet.xml 文件3、编写一个拦截器package com.kuang.interceptor; import org.springframework.web.servlet.HandlerInterceptor; import org.springframework.web.servlet.ModelAndView; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; public class MyInterceptor implements HandlerInterceptor { //在请求处理的方法之前执行 //如果返回true执行下一个拦截器 //如果返回false就不执行下一个拦截器 public boolean preHandle(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, Object o) throws Exception { System.out.println("------------处理前------------"); return true; } //在请求处理方法执行之后执行 public void postHandle(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, Object o, ModelAndView modelAndView) throws Exception { System.out.println("------------处理后------------"); } //在dispatcherServlet处理后执行,做清理工作. public void afterCompletion(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, Object o, Exception e) throws Exception { System.out.println("------------清理------------"); } }4、在springmvc的配置文件中配置拦截器<!--关于拦截器的配置--> <mvc:interceptors> <mvc:interceptor> <!--/** 包括路径及其子路径--> <!--/admin/* 拦截的是/admin/add等等这种 , /admin/add/user不会被拦截--> <!--/admin/** 拦截的是/admin/下的所有--> <mvc:mapping path="/**"/> <!--bean配置的就是拦截器--> <bean class="com.kuang.interceptor.MyInterceptor"/> </mvc:interceptor> </mvc:interceptors>5、编写一个Controller,接收请求package com.kuang.controller; import org.springframework.stereotype.Controller; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.ResponseBody; //测试拦截器的控制器 @Controller public class InterceptorController { @RequestMapping("/interceptor") @ResponseBody public String testFunction() { System.out.println("控制器中的方法执行了"); return "hello"; } }6、前端 index.jsp<a href="${pageContext.request.contextPath}/interceptor">拦截器测试</a>7、启动tomcat 测试一下!10.3 验证用户是否登录 (认证用户)实现思路1、有一个登陆页面,需要写一个controller访问页面。2、登陆页面有一提交表单的动作。需要在controller中处理。判断用户名密码是否正确。如果正确,向session中写入用户信息。返回登陆成功。3、拦截用户请求,判断用户是否登陆。如果用户已经登陆。放行, 如果用户未登陆,跳转到登陆页面测试:1、编写一个登陆页面 login.jsp<%@ page contentType="text/html;charset=UTF-8" language="java" %> <html> <head> <title>Title</title> </head> <h1>登录页面</h1> <hr> <body> <form action="${pageContext.request.contextPath}/user/login"> 用户名:<input type="text" name="username"> <br> 密码:<input type="password" name="pwd"> <br> <input type="submit" value="提交"> </form> </body> </html>2、编写一个Controller处理请求package com.kuang.controller; import org.springframework.stereotype.Controller; import org.springframework.web.bind.annotation.RequestMapping; import javax.servlet.http.HttpSession; @Controller @RequestMapping("/user") public class UserController { //跳转到登陆页面 @RequestMapping("/jumplogin") public String jumpLogin() throws Exception { return "login";d } //跳转到成功页面 @RequestMapping("/jumpSuccess") public String jumpSuccess() throws Exception { return "success"; } //登陆提交 @RequestMapping("/login") public String login(HttpSession session, String username, String pwd) throws Exception { // 向session记录用户身份信息 System.out.println("接收前端==="+username); session.setAttribute("user", username); return "success"; } //退出登陆 @RequestMapping("logout") public String logout(HttpSession session) throws Exception { // session 过期 session.invalidate(); return "login"; } }3、编写一个登陆成功的页面 success.jsp<%@ page contentType="text/html;charset=UTF-8" language="java" %> <html> <head> <title>Title</title> </head> <body> <h1>登录成功页面</h1> <hr> ${user} <a href="${pageContext.request.contextPath}/user/logout">注销</a> </body> </html>4、在 index 页面上测试跳转!启动Tomcat 测试,未登录也可以进入主页!<%@ page contentType="text/html;charset=UTF-8" language="java" %> <html> <head> <title>$Title$</title> </head> <body> <h1>首页</h1> <hr> <%--登录--%> <a href="${pageContext.request.contextPath}/user/jumplogin">登录</a> <a href="${pageContext.request.contextPath}/user/jumpSuccess">成功页面</a> </body> </html>5、编写用户登录拦截器package com.kuang.interceptor; import org.springframework.web.servlet.HandlerInterceptor; import org.springframework.web.servlet.ModelAndView; import javax.servlet.ServletException; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import javax.servlet.http.HttpSession; import java.io.IOException; public class LoginInterceptor implements HandlerInterceptor { public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws ServletException, IOException { // 如果是登陆页面则放行 System.out.println("uri: " + request.getRequestURI()); if (request.getRequestURI().contains("login")) { return true; } HttpSession session = request.getSession(); // 如果用户已登陆也放行 if(session.getAttribute("user") != null) { return true; } // 用户没有登陆跳转到登陆页面 request.getRequestDispatcher("/WEB-INF/jsp/login.jsp").forward(request, response); return false; } public void postHandle(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, Object o, ModelAndView modelAndView) throws Exception { } public void afterCompletion(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, Object o, Exception e) throws Exception { } }6、在Springmvc的配置文件中注册拦截器<!--关于拦截器的配置--> <mvc:interceptors> <mvc:interceptor> <mvc:mapping path="/**"/> <bean id="loginInterceptor" class="com.kuang.interceptor.LoginInterceptor"/> </mvc:interceptor> </mvc:interceptors>7、再次重启Tomcat测试!OK,测试登录拦截功能无误.11.文件上传和下载11.1 准备工作文件上传是项目开发中最常见的功能之一 ,springMVC 可以很好的支持文件上传,但是SpringMVC上下文中默认没有装配MultipartResolver,因此默认情况下其不能处理文件上传工作。如果想使用Spring的文件上传功能,则需要在上下文中配置MultipartResolver。前端表单要求:为了能上传文件,必须将表单的method设置为POST,并将enctype设置为multipart/form-data。只有在这样的情况下,浏览器才会把用户选择的文件以二进制数据发送给服务器;对表单中的 enctype 属性做个详细的说明:application/x-www=form-urlencoded:默认方式,只处理表单域中的 value 属性值,采用这种编码方式的表单会将表单域中的值处理成 URL 编码方式。multipart/form-data:这种编码方式会以二进制流的方式来处理表单数据,这种编码方式会把文件域指定文件的内容也封装到请求参数中,不会对字符编码。text/plain:除了把空格转换为 "+" 号外,其他字符都不做编码处理,这种方式适用直接通过表单发送邮件。<form action="" enctype="multipart/form-data" method="post"> <input type="file" name="file"/> <input type="submit"> </form>一旦设置了enctype为multipart/form-data,浏览器即会采用二进制流的方式来处理表单数据,而对于文件上传的处理则涉及在服务器端解析原始的HTTP响应。在2003年,Apache Software Foundation发布了开源的Commons FileUpload组件,其很快成为Servlet/JSP程序员上传文件的最佳选择。Servlet3.0规范已经提供方法来处理文件上传,但这种上传需要在Servlet中完成。而Spring MVC则提供了更简单的封装。Spring MVC为文件上传提供了直接的支持,这种支持是用即插即用的MultipartResolver实现的。Spring MVC使用Apache Commons FileUpload技术实现了一个MultipartResolver实现类:CommonsMultipartResolver。因此,SpringMVC的文件上传还需要依赖Apache Commons FileUpload的组件。11.2 文件上传1、导入文件上传的jar包,commons-fileupload , Maven会自动帮我们导入他的依赖包 commons-io包;<!--文件上传--> <dependency> <groupId>commons-fileupload</groupId> <artifactId>commons-fileupload</artifactId> <version>1.3.3</version> </dependency> <!--servlet-api导入高版本的--> <dependency> <groupId>javax.servlet</groupId> <artifactId>javax.servlet-api</artifactId> <version>4.0.1</version> </dependency>2、配置bean:multipartResolver【注意!!!这个bena的id必须为:multipartResolver , 否则上传文件会报400的错误!在这里栽过坑,教训!】<!--文件上传配置--> <bean id="multipartResolver" class="org.springframework.web.multipart.commons.CommonsMultipartResolver"> <!-- 请求的编码格式,必须和jSP的pageEncoding属性一致,以便正确读取表单的内容,默认为ISO-8859-1 --> <property name="defaultEncoding" value="utf-8"/> <!-- 上传文件大小上限,单位为字节(10485760=10M) --> <property name="maxUploadSize" value="10485760"/> <property name="maxInMemorySize" value="40960"/> </bean>CommonsMultipartFile 的 常用方法:String getOriginalFilename():获取上传文件的原名InputStream getInputStream():获取文件流void transferTo(File dest):将上传文件保存到一个目录文件中我们去实际测试一下3、编写前端页面<form action="/upload" enctype="multipart/form-data" method="post"> <input type="file" name="file"/> <input type="submit" value="upload"> </form>4、Controllerpackage com.kuang.controller; import org.springframework.stereotype.Controller; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.multipart.commons.CommonsMultipartFile; import javax.servlet.http.HttpServletRequest; import java.io.*; @Controller public class FileController { //@RequestParam("file") 将name=file控件得到的文件封装成CommonsMultipartFile 对象 //批量上传CommonsMultipartFile则为数组即可 @RequestMapping("/upload") public String fileUpload(@RequestParam("file") CommonsMultipartFile file , HttpServletRequest request) throws IOException { //获取文件名 : file.getOriginalFilename(); String uploadFileName = file.getOriginalFilename(); //如果文件名为空,直接回到首页! if ("".equals(uploadFileName)){ return "redirect:/index.jsp"; } System.out.println("上传文件名 : "+uploadFileName); //上传路径保存设置 String path = request.getServletContext().getRealPath("/upload"); //如果路径不存在,创建一个 File realPath = new File(path); if (!realPath.exists()){ realPath.mkdir(); } System.out.println("上传文件保存地址:"+realPath); InputStream is = file.getInputStream(); //文件输入流 OutputStream os = new FileOutputStream(new File(realPath,uploadFileName)); //文件输出流 //读取写出 int len=0; byte[] buffer = new byte[1024]; while ((len=is.read(buffer))!=-1){ os.write(buffer,0,len); os.flush(); } os.close(); is.close(); return "redirect:/index.jsp"; } }5、测试上传文件,OK!采用file.Transto 来保存上传的文件1、编写Controller/* * 采用file.Transto 来保存上传的文件 */ @RequestMapping("/upload2") public String fileUpload2(@RequestParam("file") CommonsMultipartFile file, HttpServletRequest request) throws IOException { //上传路径保存设置 String path = request.getServletContext().getRealPath("/upload"); File realPath = new File(path); if (!realPath.exists()){ realPath.mkdir(); } //上传文件地址 System.out.println("上传文件保存地址:"+realPath); //通过CommonsMultipartFile的方法直接写文件(注意这个时候) file.transferTo(new File(realPath +"/"+ file.getOriginalFilename())); return "redirect:/index.jsp"; }2、前端表单提交地址修改3、访问提交测试,OK!11.3 文件下载文件下载步骤:1、设置 response 响应头2、读取文件 -- InputStream3、写出文件 -- OutputStream4、执行操作5、关闭流 (先开后关)代码实现:@RequestMapping(value="/download") public String downloads(HttpServletResponse response ,HttpServletRequest request) throws Exception{ //要下载的图片地址 String path = request.getServletContext().getRealPath("/upload"); String fileName = "基础语法.jpg"; //1、设置response 响应头 response.reset(); //设置页面不缓存,清空buffer response.setCharacterEncoding("UTF-8"); //字符编码 response.setContentType("multipart/form-data"); //二进制传输数据 //设置响应头 response.setHeader("Content-Disposition", "attachment;fileName="+URLEncoder.encode(fileName, "UTF-8")); File file = new File(path,fileName); //2、 读取文件--输入流 InputStream input=new FileInputStream(file); //3、 写出文件--输出流 OutputStream out = response.getOutputStream(); byte[] buff =new byte[1024]; int index=0; //4、执行 写出操作 while((index= input.read(buff))!= -1){ out.write(buff, 0, index); out.flush(); } out.close(); input.close(); return null; }前端<a href="/download">点击下载</a>测试,文件下载OK,大家可以和我们之前学习的JavaWeb原生的方式对比一下,就可以知道这个便捷多了!参考资料【狂神说Java】SpringBoot最新教程IDEA版通俗易懂SpringMVC运行原理
2022年05月05日
891 阅读
0 评论
0 点赞
2022-04-28
spring5学习笔记
1.spring简介1.1 介绍spring:春天--给软件行业带来了春天2002年,首次推出了spring框架的雏形:interface212004年3月24日诞生Rod Johnson,Spring Framework创始人spring理念:使现有的技术更加容易复用,本身是一个大杂烩,整合了现有的技术框架官网:https://spring.io/Spring Framework 中文文档:https://www.docs4dev.com/docs/zh/spring-framework/官方下载地址 : https://repo.spring.io/libs-release-local/org/springframework/spring/GitHub : https://github.com/spring-projectsjava两套框架SSH:Struct2+Spring+HibernateSSH:SpringMVC+Spring+Mybatis1.2 优点spring是一个开源免费的框架(容器)spring是一个轻量级的、非入侵式的框架支持事务处理支持对框架整合的指出1.3 核心特点控制反转(IOC)面向切面编程(AOP)1.4 组成Spring 框架是一个分层架构,由 7 个定义良好的模块组成。Spring 模块构建在核心容器之上,核心容器定义了创建、配置和管理 bean 的方式 .组成 Spring 框架的每个模块(或组件)都可以单独存在,或者与其他一个或多个模块联合实现。每个模块的功能如下:核心容器:核心容器提供 Spring 框架的基本功能。核心容器的主要组件是 BeanFactory,它是工厂模式的实现。BeanFactory 使用控制反转(IOC) 模式将应用程序的配置和依赖性规范与实际的应用程序代码分开。Spring 上下文:Spring 上下文是一个配置文件,向 Spring 框架提供上下文信息。Spring 上下文包括企业服务,例如 JNDI、EJB、电子邮件、国际化、校验和调度功能。Spring AOP:通过配置管理特性,Spring AOP 模块直接将面向切面的编程功能 , 集成到了 Spring 框架中。所以,可以很容易地使 Spring 框架管理任何支持 AOP的对象。Spring AOP 模块为基于 Spring 的应用程序中的对象提供了事务管理服务。通过使用 Spring AOP,不用依赖组件,就可以将声明性事务管理集成到应用程序中。Spring DAO:JDBC DAO 抽象层提供了有意义的异常层次结构,可用该结构来管理异常处理和不同数据库供应商抛出的错误消息。异常层次结构简化了错误处理,并且极大地降低了需要编写的异常代码数量(例如打开和关闭连接)。Spring DAO 的面向 JDBC 的异常遵从通用的 DAO 异常层次结构。Spring ORM:Spring 框架插入了若干个 ORM 框架,从而提供了 ORM 的对象关系工具,其中包括 JDO、Hibernate 和 iBatis SQL Map。所有这些都遵从 Spring 的通用事务和 DAO 异常层次结构。Spring Web 模块:Web 上下文模块建立在应用程序上下文模块之上,为基于 Web 的应用程序提供了上下文。所以,Spring 框架支持与 Jakarta Struts 的集成。Web 模块还简化了处理多部分请求以及将请求参数绑定到域对象的工作。Spring MVC 框架:MVC 框架是一个全功能的构建 Web 应用程序的 MVC 实现。通过策略接口,MVC 框架变成为高度可配置的,MVC 容纳了大量视图技术,其中包括 JSP、Velocity、Tiles、iText 和 POI。1.5 拓展在Spring的官网有这个介绍:现代化的Java开发!说白就是基于Spring的开发!Spring Boot与Spring CloudSpring Boot 是 Spring 的一套快速配置脚手架,可以基于Spring Boot 快速开发单个微服务;Spring Cloud是基于Spring Boot实现的;Spring Boot专注于快速、方便集成的单个微服务个体,Spring Cloud关注全局的服务治理框架;Spring Boot使用了约束优于配置的理念,很多集成方案已经帮你选择好了,能不配置就不配置 , Spring Cloud很大的一部分是基于Spring Boot来实现,Spring Boot可以离开Spring Cloud独立使用开发项目,但是Spring Cloud离不开Spring Boot,属于依赖的关系。SpringBoot在SpringClound中起到了承上启下的作用,如果你要学习SpringCloud必须要学习SpringBoot。。因为现在大多数公司都在使用SpringBoot进行快速开发,学习SpringBoot的前提,需要完全掌握Spring及SpringMVC!1.6 弊端弊端:发展了太久之后,违背了原来的理念!配置十分繁琐,人称:“配置地狱!”1.7 小结总结一句话:Spring是一个轻量级的控制反转(IOC)和面向切面编程(AOP)的框架。2. IOC理论推导2.1 分析实现0、新建一个空白的maven项目我们先用我们原来的方式写一段代码 .1、先写一个UserDao接口public interface UserDao { public void getUser(); }2、再去写Dao的实现类public class UserDaoImpl implements UserDao { @Override public void getUser() { System.out.println("获取用户数据"); } }3、然后去写UserService的接口public interface UserService { public void getUser(); }4、最后写Service的实现类public class UserServiceImpl implements UserService { private UserDao userDao = new UserDaoImpl(); @Override public void getUser() { userDao.getUser(); } }5、测试一下@Test public void test(){ UserService service = new UserServiceImpl(); service.getUser(); }这是我们原来的方式 , 开始大家也都是这么去写的对吧 . 那我们现在修改一下 .把Userdao的实现类增加一个 .public class UserDaoMySqlImpl implements UserDao { @Override public void getUser() { System.out.println("MySql获取用户数据"); } }紧接着我们要去使用MySql的话 , 我们就需要去service实现类里面修改对应的实现public class UserServiceImpl implements UserService { private UserDao userDao = new UserDaoMySqlImpl(); @Override public void getUser() { userDao.getUser(); } }在假设, 我们再增加一个Userdao的实现类 .public class UserDaoOracleImpl implements UserDao { @Override public void getUser() { System.out.println("Oracle获取用户数据"); } }那么我们要使用Oracle , 又需要去service实现类里面修改对应的实现 . 假设我们的这种需求非常大 , 这种方式就根本不适用了, 甚至反人类对吧 , 每次变动 , 都需要修改大量代码 . 这种设计的耦合性太高了, 牵一发而动全身 .那我们如何去解决呢 ? 我们可以在需要用到他的地方 , 不去实现它 , 而是留出一个接口 , 利用set , 我们去代码里修改下 .public class UserServiceImpl implements UserService { private UserDao userDao; // 利用set实现 public void setUserDao(UserDao userDao) { this.userDao = userDao; } @Override public void getUser() { userDao.getUser(); } }现在去我们的测试类里 , 进行测试 ;@Test public void test(){ UserServiceImpl service = new UserServiceImpl(); service.setUserDao( new UserDaoMySqlImpl() ); service.getUser(); //那我们现在又想用Oracle去实现呢 service.setUserDao( new UserDaoOracleImpl() ); service.getUser(); }大家发现了区别没有 ? 可能很多人说没啥区别 . 但是同学们 , 他们已经发生了根本性的变化 , 很多地方都不一样了 . 仔细去思考一下 , 以前所有东西都是由程序去进行控制创建 , 而现在是由我们自行控制创建对象 , 把主动权交给了调用者 . 程序不用去管怎么创建,怎么实现了 . 它只负责提供一个接口 .这种思想 , 从本质上解决了问题 , 我们程序员不再去管理对象的创建了 , 更多的去关注业务的实现 . 耦合性大大降低 . 这也就是IOC的原型 !2.2 IOC本质控制反转IoC(Inversion of Control),是一种设计思想,DI(依赖注入)是实现IoC的一种方法,也有人认为DI只是IoC的另一种说法。没有IoC的程序中 , 我们使用面向对象编程 , 对象的创建与对象间的依赖关系完全硬编码在程序中,对象的创建由程序自己控制,控制反转后将对象的创建转移给第三方,个人认为所谓控制反转就是:获得依赖对象的方式反转了。IoC是Spring框架的核心内容,使用多种方式完美的实现了IoC,可以使用XML配置,也可以使用注解,新版本的Spring也可以零配置实现IoC。Spring容器在初始化时先读取配置文件,根据配置文件或元数据创建与组织对象存入容器中,程序使用时再从Ioc容器中取出需要的对象。采用XML方式配置Bean的时候,Bean的定义信息是和实现分离的,而采用注解的方式可以把两者合为一体,Bean的定义信息直接以注解的形式定义在实现类中,从而达到了零配置的目的。控制反转是一种通过描述(XML或注解)并通过第三方去生产或获取特定对象的方式。在Spring中实现控制反转的是IoC容器,其实现方法是依赖注入(Dependency Injection,DI)。3.HelloSpring3.1 实现HelloSpring0、导入Jar包<dependency> <groupId>org.springframework</groupId> <artifactId>spring-webmvc</artifactId> <version>5.1.10.RELEASE</version> </dependency>1、编写一个Hello实体类package com.kuang.pojo; public class Hello { private String name; public String getName(){ return name; } public void setName(String name){ this.name = name; } public void show(){ System.out.println("Hello"+name); } }2、编写我们的spring文件 , 这里我们命名为beans.xml<?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就是java对象 , 由Spring创建和管理--> <bean id="hello" class="com.kuang.pojo.Hello"> <property name="name" value="Spring"/> </bean> </beans>3、我们可以去进行测试了 .@Test public void test(){ //解析beans.xml文件 , 生成管理相应的Bean对象 ApplicationContext context = new ClassPathXmlApplicationContext("beans.xml"); //getBean : 参数即为spring配置文件中bean的id . Hello hello = (Hello) context.getBean("hello"); hello.show(); }3.2 思考Hello 对象是谁创建的 ? hello 对象是由Spring创建的Hello 对象的属性是怎么设置的 ? hello 对象的属性是由Spring容器设置的这个过程就叫控制反转 :控制 : 谁来控制对象的创建 , 传统应用程序的对象是由程序本身控制创建的 , 使用Spring后 , 对象是由Spring来创建的反转 : 程序本身不创建对象 , 而变成被动的接收对象 .依赖注入 : 就是利用set方法来进行注入的.IOC是一种编程思想,由主动的编程变成被动的接收可以通过newClassPathXmlApplicationContext去浏览一下底层源码 .3.3 修改案例一我们在案例一中, 新增一个Spring配置文件beans.xml<?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="MysqlImpl" class="com.kuang.dao.impl.UserDaoMySqlImpl"/> <bean id="OracleImpl" class="com.kuang.dao.impl.UserDaoOracleImpl"/> <bean id="ServiceImpl" class="com.kuang.service.impl.UserServiceImpl"> <!--注意: 这里的name并不是属性 , 而是set方法后面的那部分 , 首字母小写--> <!--引用另外一个bean , 不是用value 而是用 ref--> <property name="userDao" ref="OracleImpl"/> </bean> </beans>测试!@Test public void test2(){ ApplicationContext context = new ClassPathXmlApplicationContext("beans.xml"); UserServiceImpl serviceImpl = (UserServiceImpl) context.getBean("ServiceImpl"); serviceImpl.getUser(); }OK , 到了现在 , 我们彻底不用再程序中去改动了 , 要实现不同的操作 , 只需要在xml配置文件中进行修改 , 所谓的IoC,一句话搞定 : 对象由Spring 来创建 , 管理 , 装配 !3.4 IOC创建对象方式3.4.1 通过无参构造方法来创建1、User.javapublic class User { private String name; public User() { System.out.println("user无参构造方法"); } public void setName(String name) { this.name = name; } public void show(){ System.out.println("name="+ name ); } }2、beans.xml<?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="user" class="com.kuang.pojo.User"> <property name="name" value="kuangshen"/> </bean> </beans>3、测试类@Test public void test(){ ApplicationContext context = new ClassPathXmlApplicationContext("beans.xml"); //在执行getBean的时候, user已经创建好了 , 通过无参构造 User user = (User) context.getBean("user"); //调用对象的方法 . user.show(); }结果可以发现,在调用show方法之前,User对象已经通过无参构造初始化了!3.4.2 通过有参构造方法来创建1、UserT . javapublic class UserT { private String name; public UserT(String name) { this.name = name; } public void setName(String name) { this.name = name; } public void show(){ System.out.println("name="+ name ); } }2、beans.xml 有三种方式编写<!-- 第一种根据index参数下标设置 --> <bean id="userT" class="com.kuang.pojo.UserT"> <!-- index指构造方法 , 下标从0开始 --> <constructor-arg index="0" value="kuangshen2"/> </bean> <!-- 第二种根据参数名字设置 --> <bean id="userT" class="com.kuang.pojo.UserT"> <!-- name指参数名 --> <constructor-arg name="name" value="kuangshen2"/> </bean> <!-- 第三种根据参数类型设置 --> <bean id="userT" class="com.kuang.pojo.UserT"> <constructor-arg type="java.lang.String" value="kuangshen2"/> </bean>3、测试@Test public void testT(){ ApplicationContext context = new ClassPathXmlApplicationContext("beans.xml"); UserT user = (UserT) context.getBean("userT"); user.show(); }结论:在配置文件加载的时候。其中管理的对象都已经初始化了!3.5 其他Spring配置3.5.1 别名alias 设置别名 , 为bean设置别名 , 可以设置多个别名<!--设置别名:在获取Bean的时候可以使用别名获取--> <alias name="userT" alias="userNew"/>3.5.2 Bean的配置<!--bean就是java对象,由Spring创建和管理--> <!-- id 是bean的标识符,要唯一,如果没有配置id,name就是默认标识符 如果配置id,又配置了name,那么name是别名 name可以设置多个别名,可以用逗号,分号,空格隔开 如果不配置id和name,可以根据applicationContext.getBean(.class)获取对象; class是bean的全限定名=包名+类名 --> <bean id="hello" name="hello2 h2,h3;h4" class="com.kuang.pojo.Hello"> <property name="name" value="Spring"/> </bean>3.5.3 import团队的合作通过import来实现 .<import resource="{path}/beans.xml"/>4.依赖注入(DI)4.1 概念依赖注入(Dependency Injection,DI)。依赖 : 指Bean对象的创建依赖于容器 . Bean对象的依赖资源 .注入 : 指Bean对象所依赖的资源 , 由容器来设置和装配 .4.2 构造器注入我们在之前的案例已经讲过了4.3 Set 注入 (重点)要求被注入的属性 , 必须有set方法 , set方法的方法名由set + 属性首字母大写 , 如果属性是boolean类型 , 没有set方法 , 是 is .测试pojo类 :Address.java public class Address { private String address; public String getAddress() { return address; } public void setAddress(String address) { this.address = address; } }Student.java package com.kuang.pojo; import java.util.List; import java.util.Map; import java.util.Properties; import java.util.Set; public class Student { private String name; private Address address; private String[] books; private List<String> hobbys; private Map<String,String> card; private Set<String> games; private String wife; private Properties info; public void setName(String name) { this.name = name; } public void setAddress(Address address) { this.address = address; } public void setBooks(String[] books) { this.books = books; } public void setHobbys(List<String> hobbys) { this.hobbys = hobbys; } public void setCard(Map<String, String> card) { this.card = card; } public void setGames(Set<String> games) { this.games = games; } public void setWife(String wife) { this.wife = wife; } public void setInfo(Properties info) { this.info = info; } public void show(){ System.out.println("name="+ name + ",address="+ address.getAddress() + ",books=" ); for (String book:books){ System.out.print("<<"+book+">>\t"); } System.out.println("\n爱好:"+hobbys); System.out.println("card:"+card); System.out.println("games:"+games); System.out.println("wife:"+wife); System.out.println("info:"+info); } }1、常量注入 <bean id="student" class="com.kuang.pojo.Student"> <property name="name" value="小明"/> </bean>测试: @Test public void test01(){ ApplicationContext context = new ClassPathXmlApplicationContext("applicationContext.xml"); Student student = (Student) context.getBean("student"); System.out.println(student.getName()); }2、Bean注入 注意点:这里的值是一个引用,ref <bean id="addr" class="com.kuang.pojo.Address"> <property name="address" value="重庆"/> </bean> <bean id="student" class="com.kuang.pojo.Student"> <property name="name" value="小明"/> <property name="address" ref="addr"/> </bean>3、数组注入 <bean id="student" class="com.kuang.pojo.Student"> <property name="name" value="小明"/> <property name="address" ref="addr"/> <property name="books"> <array> <value>西游记</value> <value>红楼梦</value> <value>水浒传</value> </array> </property> </bean>4、List注入 <property name="hobbys"> <list> <value>听歌</value> <value>看电影</value> <value>爬山</value> </list> </property>5、Map注入 <property name="card"> <map> <entry key="中国邮政" value="456456456465456"/> <entry key="建设" value="1456682255511"/> </map> </property>6、set注入 <property name="games"> <set> <value>LOL</value> <value>BOB</value> <value>COC</value> </set> </property>7、Null注入 <property name="wife"><null/></property>8、Properties注入 <property name="info"> <props> <prop key="学号">20190604</prop> <prop key="性别">男</prop> <prop key="姓名">小明</prop> </props> </property>测试结果:4.4 p命名和c命名注入User.java :【注意:这里没有有参构造器!】 public class User { private String name; private int age; public void setName(String name) { this.name = name; } public void setAge(int age) { this.age = age; } @Override public String toString() { return "User{" + "name='" + name + '\'' + ", age=" + age + '}'; } }1、P命名空间注入 : 需要在头文件中加入约束文件 导入约束 : xmlns:p="http://www.springframework.org/schema/p" <!--P(属性: properties)命名空间 , 属性依然要设置set方法--> <bean id="user" class="com.kuang.pojo.User" p:name="狂神" p:age="18"/>2、c 命名空间注入 : 需要在头文件中加入约束文件 导入约束 : xmlns:c="http://www.springframework.org/schema/c" <!--C(构造: Constructor)命名空间 , 属性依然要设置set方法--> <bean id="user" class="com.kuang.pojo.User" c:name="狂神" c:age="18"/>发现问题:爆红了,刚才我们没有写有参构造!解决:把有参构造器加上,这里也能知道,c 就是所谓的构造器注入!测试代码: @Test public void test02(){ ApplicationContext context = new ClassPathXmlApplicationContext("applicationContext.xml"); User user = (User) context.getBean("user"); System.out.println(user); }4.5 Bean的作用域在Spring中,那些组成应用程序的主体及由Spring IoC容器所管理的对象,被称之为bean。简单地讲,bean就是由IoC容器初始化、装配及管理的对象 .几种作用域中,request、session作用域仅在基于web的应用中使用(不必关心你所采用的是什么web应用框架),只能用在基于web的Spring ApplicationContext环境。Singleton当一个bean的作用域为Singleton,那么Spring IoC容器中只会存在一个共享的bean实例,并且所有对bean的请求,只要id与该bean定义相匹配,则只会返回bean的同一实例。Singleton是单例类型,就是在创建起容器时就同时自动创建了一个bean的对象,不管你是否使用,他都存在了,每次获取到的对象都是同一个对象。注意,Singleton作用域是Spring中的缺省作用域。要在XML中将bean定义成singleton,可以这样配置: <bean id="ServiceImpl" class="cn.csdn.service.ServiceImpl" scope="singleton">测试: @Test public void test03(){ ApplicationContext context = new ClassPathXmlApplicationContext("applicationContext.xml"); User user = (User) context.getBean("user"); User user2 = (User) context.getBean("user"); System.out.println(user==user2); }Prototype当一个bean的作用域为Prototype,表示一个bean定义对应多个对象实例。Prototype作用域的bean会导致在每次对该bean请求(将其注入到另一个bean中,或者以程序的方式调用容器的getBean()方法)时都会创建一个新的bean实例。Prototype是原型类型,它在我们创建容器的时候并没有实例化,而是当我们获取bean的时候才会去创建一个对象,而且我们每次获取到的对象都不是同一个对象。根据经验,对有状态的bean应该使用prototype作用域,而对无状态的bean则应该使用singleton作用域。在XML中将bean定义成prototype,可以这样配置: <bean id="account" class="com.foo.DefaultAccount" scope="prototype"/> 或者 <bean id="account" class="com.foo.DefaultAccount" singleton="false"/>Request当一个bean的作用域为Request,表示在一次HTTP请求中,一个bean定义对应一个实例;即每个HTTP请求都会有各自的bean实例,它们依据某个bean定义创建而成。该作用域仅在基于web的Spring ApplicationContext情形下有效。考虑下面bean定义: <bean id="loginAction" class=cn.csdn.LoginAction" scope="request"/>针对每次HTTP请求,Spring容器会根据loginAction bean的定义创建一个全新的LoginAction bean实例,且该loginAction bean实例仅在当前HTTP request内有效,因此可以根据需要放心的更改所建实例的内部状态,而其他请求中根据loginAction bean定义创建的实例,将不会看到这些特定于某个请求的状态变化。当处理请求结束,request作用域的bean实例将被销毁。Session当一个bean的作用域为Session,表示在一个HTTP Session中,一个bean定义对应一个实例。该作用域仅在基于web的Spring ApplicationContext情形下有效。考虑下面bean定义: <bean id="userPreferences" class="com.foo.UserPreferences" scope="session"/>针对某个HTTP Session,Spring容器会根据userPreferences bean定义创建一个全新的userPreferences bean实例,且该userPreferences bean仅在当前HTTP Session内有效。与request作用域一样,可以根据需要放心的更改所创建实例的内部状态,而别的HTTP Session中根据userPreferences创建的实例,将不会看到这些特定于某个HTTP Session的状态变化。当HTTP Session最终被废弃的时候,在该HTTP Session作用域内的bean也会被废弃掉。5.Bean的自动装配5.1 自动装配说明自动装配是使用spring满足bean依赖的一种方法spring会在应用上下文中为某个bean寻找其依赖的bean。Spring中bean有三种装配机制,分别是:在xml中显式配置;在java中显式配置;隐式的bean发现机制和自动装配。这里我们主要讲第三种:自动化的装配bean。Spring的自动装配需要从两个角度来实现,或者说是两个操作:组件扫描(component scanning):spring会自动发现应用上下文中所创建的bean;自动装配(autowiring):spring自动满足bean之间的依赖,也就是我们说的IoC/DI;组件扫描和自动装配组合发挥巨大威力,使得显示的配置降低到最少。推荐不使用自动装配xml配置 , 而使用注解 .5.2 测试环境搭建1、新建一个项目2、新建两个实体类,Cat Dog 都有一个叫的方法public class Cat { public void shout() { System.out.println("miao~"); } } public class Dog { public void shout() { System.out.println("wang~"); } }3、新建一个用户类 Userpublic class User { private Cat cat; private Dog dog; private String str; }4、编写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="dog" class="com.kuang.pojo.Dog"/> <bean id="cat" class="com.kuang.pojo.Cat"/> <bean id="user" class="com.kuang.pojo.User"> <property name="cat" ref="cat"/> <property name="dog" ref="dog"/> <property name="str" value="qinjiang"/> </bean> </beans>5、测试public class MyTest { @Test public void testMethodAutowire() { ApplicationContext context = new ClassPathXmlApplicationContext("beans.xml"); User user = (User) context.getBean("user"); user.getCat().shout(); user.getDog().shout(); } }结果正常输出,环境OK5.3 autowire byName (按名称自动装配)由于在手动配置xml过程中,常常发生字母缺漏和大小写等错误,而无法对其进行检查,使得开发效率降低。采用自动装配将避免这些错误,并且使配置简单化。测试:1、修改bean配置,增加一个属性 autowire="byName"<bean id="user" class="com.kuang.pojo.User" autowire="byName"> <property name="str" value="qinjiang"/> </bean>2、再次测试,结果依旧成功输出!3、我们将 cat 的bean id修改为 catXXX4、再次测试, 执行时报空指针java.lang.NullPointerException。因为按byName规则找不对应set方法,真正的setCat就没执行,对象就没有初始化,所以调用时就会报空指针错误。小结:当一个bean节点带有 autowire byName的属性时。将查找其类中所有的set方法名,例如setCat,获得将set去掉并且首字母小写的字符串,即cat。去spring容器中寻找是否有此字符串名称id的对象。如果有,就取出注入;如果没有,就报空指针异常。5.4 autowire byType (按类型自动装配)使用autowire byType首先需要保证:同一类型的对象,在spring容器中唯一。如果不唯一,会报不唯一的异常。测试:1、将user的bean配置修改一下 : autowire="byType"2、测试,正常输出3、在注册一个cat 的bean对象!<bean id="dog" class="com.kuang.pojo.Dog"/> <bean id="cat" class="com.kuang.pojo.Cat"/> <bean id="cat2" class="com.kuang.pojo.Cat"/> <bean id="user" class="com.kuang.pojo.User" autowire="byType"> <property name="str" value="qinjiang"/> </bean>4、测试,报错:NoUniqueBeanDefinitionException5、删掉cat2,将cat的bean名称改掉!测试!因为是按类型装配,所以并不会报异常,也不影响最后的结果。甚至将id属性去掉,也不影响结果。这就是按照类型自动装配!5.5 使用注解jdk1.5开始支持注解,spring2.5开始全面支持注解。准备工作:利用注解的方式注入属性。1、在spring配置文件中引入context文件头xmlns:context="http://www.springframework.org/schema/context" http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context.xsd <beans xmlns="http://www.springframework.org/schema/beans" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:context="http://www.springframework.org/schema/context" xmlns:aop="http://www.springframework.org/schema/aop" xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context.xsd http://www.springframework.org/schema/aop http://www.springframework.org/schema/context/spring-aop.xsd">2、开启属性注解支持!<context:annotation-config/>@Autowired@Autowired是按类型自动转配的,不支持id匹配。需要导入 spring-aop的包!测试:1、将User类中的set方法去掉,使用@Autowired注解public class User { @Autowired private Cat cat; @Autowired private Dog dog; private String str; public Cat getCat() { return cat; } public Dog getDog() { return dog; } public String getStr() { return str; } }2、此时配置文件内容<context:annotation-config/> <bean id="dog" class="com.kuang.pojo.Dog"/> <bean id="cat" class="com.kuang.pojo.Cat"/> <bean id="user" class="com.kuang.pojo.User"/>3、测试,成功输出结果!【小狂神科普时间】@Autowired(required=false) 说明:false,对象可以为null;true,对象必须存对象,不能为null。//如果允许对象为null,设置required = false,默认为true @Autowired(required = false) private Cat cat;@Qualifier@Autowired是根据类型+id自动装配的,加上@Qualifier则可以根据byName的方式自动装配@Qualifier不能单独使用。测试实验步骤:1、配置文件修改内容,保证类型存在对象。且名字不为类的默认名字!<bean id="dog1" class="com.kuang.pojo.Dog"/> <bean id="dog2" class="com.kuang.pojo.Dog"/> <bean id="cat1" class="com.kuang.pojo.Cat"/> <bean id="cat2" class="com.kuang.pojo.Cat"/>2、没有加Qualifier测试,直接报错3、在属性上添加Qualifier注解@Autowired @Qualifier(value = "cat2") private Cat cat; @Autowired @Qualifier(value = "dog2") private Dog dog;测试,成功输出!@Resource@Resource如有指定的name属性,先按该属性进行byName方式查找装配;其次再进行默认的byName方式进行装配;如果以上都不成功,则按byType的方式自动装配。都不成功,则报异常。实体类:public class User { //如果允许对象为null,设置required = false,默认为true @Resource(name = "cat2") private Cat cat; @Resource private Dog dog; private String str; }beans.xml<bean id="dog" class="com.kuang.pojo.Dog"/> <bean id="cat1" class="com.kuang.pojo.Cat"/> <bean id="cat2" class="com.kuang.pojo.Cat"/> <bean id="user" class="com.kuang.pojo.User"/>测试:结果OK配置文件2:beans.xml , 删掉cat2<bean id="dog" class="com.kuang.pojo.Dog"/> <bean id="cat1" class="com.kuang.pojo.Cat"/>实体类上只保留注解@Resource private Cat cat; @Resource private Dog dog;结果:OK结论:先进行byName查找,失败;再进行byType查找,成功。@Autowired与@Resource异同:1、@Autowired与@Resource都可以用来装配bean。都可以写在字段上,或写在setter方法上。2、@Autowired默认按类型装配(属于spring规范),默认情况下必须要求依赖对象必须存在,如果要允许null 值,可以设置它的required属性为false,如:@Autowired(required=false) ,如果我们想使用名称装配可以结合@Qualifier注解进行使用3、@Resource(属于J2EE复返),默认按照名称进行装配,名称可以通过name属性进行指定。如果没有指定name属性,当注解写在字段上时,默认取字段名进行按照名称查找,如果注解写在setter方法上默认取属性名进行装配。当找不到与名称匹配的bean时才按照类型进行装配。但是需要注意的是,如果name属性一旦指定,就只会按照名称进行装配。它们的作用相同都是用注解方式注入对象,但执行顺序不同。@Autowired先byType,@Resource先byName。6. 静态/动态代理模式为什么要学习代理模式,因为AOP的底层机制就是动态代理!代理模式:静态代理动态代理学习aop之前 , 我们要先了解一下代理模式!6.1 静态代理静态代理角色分析抽象角色 : 一般使用接口或者抽象类来实现真实角色 : 被代理的角色代理角色 : 代理真实角色 ; 代理真实角色后 , 一般会做一些附属的操作 .客户 : 使用代理角色来进行一些操作 .代码实现Rent . java 即抽象角色//抽象角色:租房 public interface Rent { public void rent(); }Host . java 即真实角色//真实角色: 房东,房东要出租房子 public class Host implements Rent{ public void rent() { System.out.println("房屋出租"); } }Proxy . java 即代理角色//代理角色:中介 public class Proxy implements Rent { private Host host; public Proxy() { } public Proxy(Host host) { this.host = host; } //租房 public void rent(){ seeHouse(); host.rent(); fare(); } //看房 public void seeHouse(){ System.out.println("带房客看房"); } //收中介费 public void fare(){ System.out.println("收中介费"); } }Client . java 即客户//客户类,一般客户都会去找代理! public class Client { public static void main(String[] args) { //房东要租房 Host host = new Host(); //中介帮助房东 Proxy proxy = new Proxy(host); //你去找中介! proxy.rent(); } }分析:在这个过程中,你直接接触的就是中介,就如同现实生活中的样子,你看不到房东,但是你依旧租到了房东的房子通过代理,这就是所谓的代理模式,程序源自于生活,所以学编程的人,一般能够更加抽象的看待生活中发生的事情。静态代理的好处:可以使得我们的真实角色更加纯粹 . 不再去关注一些公共的事情 .公共的业务由代理来完成 . 实现了业务的分工 ,公共业务发生扩展时变得更加集中和方便 .缺点 :类多了 , 多了代理类 , 工作量变大了 . 开发效率降低 .我们想要静态代理的好处,又不想要静态代理的缺点,所以 , 就有了动态代理 !6.2 静态代理再理解同学们练习完毕后,我们再来举一个例子,巩固大家的学习!练习步骤:1、创建一个抽象角色,比如咋们平时做的用户业务,抽象起来就是增删改查!//抽象角色:增删改查业务 public interface UserService { void add(); void delete(); void update(); void query(); }2、我们需要一个真实对象来完成这些增删改查操作//真实对象,完成增删改查操作的人 public class UserServiceImpl implements UserService { public void add() { System.out.println("增加了一个用户"); } public void delete() { System.out.println("删除了一个用户"); } public void update() { System.out.println("更新了一个用户"); } public void query() { System.out.println("查询了一个用户"); } }3、需求来了,现在我们需要增加一个日志功能,怎么实现!思路1 :在实现类上增加代码 【麻烦!】思路2:使用代理来做,能够不改变原来的业务情况下,实现此功能就是最好的了!4、设置一个代理类来处理日志!代理角色//代理角色,在这里面增加日志的实现 public class UserServiceProxy implements UserService { private UserServiceImpl userService; public void setUserService(UserServiceImpl userService) { this.userService = userService; } public void add() { log("add"); userService.add(); } public void delete() { log("delete"); userService.delete(); } public void update() { log("update"); userService.update(); } public void query() { log("query"); userService.query(); } public void log(String msg){ System.out.println("执行了"+msg+"方法"); } }5、测试访问类:public class Client { public static void main(String[] args) { //真实业务 UserServiceImpl userService = new UserServiceImpl(); //代理类 UserServiceProxy proxy = new UserServiceProxy(); //使用代理类实现日志功能! proxy.setUserService(userService); proxy.add(); } }OK,到了现在代理模式大家应该都没有什么问题了,重点大家需要理解其中的思想;我们在不改变原来的代码的情况下,实现了对原有功能的增强,这是AOP中最核心的思想聊聊AOP:纵向开发,横向开发6.3 动态代理动态代理的角色和静态代理的一样 .动态代理的代理类是动态生成的 . 静态代理的代理类是我们提前写好的动态代理分为两类 : 一类是基于接口动态代理 , 一类是基于类的动态代理基于接口的动态代理----JDK动态代理基于类的动态代理--cglib现在用的比较多的是 javasist 来生成动态代理 . 百度一下javasist我们这里使用JDK的原生代码来实现,其余的道理都是一样的!、JDK的动态代理需要了解两个类核心 : InvocationHandler 和 Proxy , 打开JDK帮助文档看看【InvocationHandler:调用处理程序】Object invoke(Object proxy, 方法 method, Object[] args); //参数 //proxy - 调用该方法的代理实例 //method -所述方法对应于调用代理实例上的接口方法的实例。方法对象的声明类将是该方法声明的接口,它可以是代理类继承该方法的代理接口的超级接口。 //args -包含的方法调用传递代理实例的参数值的对象的阵列,或null如果接口方法没有参数。原始类型的参数包含在适当的原始包装器类的实例中,例如java.lang.Integer或java.lang.Boolean 。【Proxy : 代理】//生成代理类 public Object getProxy(){ return Proxy.newProxyInstance(this.getClass().getClassLoader(), rent.getClass().getInterfaces(),this); }代码实现 抽象角色和真实角色和之前的一样!Rent . java 即抽象角色//抽象角色:租房 public interface Rent { public void rent(); }Host . java 即真实角色//真实角色: 房东,房东要出租房子 public class Host implements Rent{ public void rent() { System.out.println("房屋出租"); } }ProxyInvocationHandler. java 即代理角色public class ProxyInvocationHandler implements InvocationHandler { private Rent rent; public void setRent(Rent rent) { this.rent = rent; } //生成代理类,重点是第二个参数,获取要代理的抽象角色!之前都是一个角色,现在可以代理一类角色 public Object getProxy(){ return Proxy.newProxyInstance(this.getClass().getClassLoader(), rent.getClass().getInterfaces(),this); } // proxy : 代理类 method : 代理类的调用处理程序的方法对象. // 处理代理实例上的方法调用并返回结果 @Override public Object invoke(Object proxy, Method method, Object[] args) throws Throwable { seeHouse(); //核心:本质利用反射实现! Object result = method.invoke(rent, args); fare(); return result; } //看房 public void seeHouse(){ System.out.println("带房客看房"); } //收中介费 public void fare(){ System.out.println("收中介费"); } }Client . java//租客 public class Client { public static void main(String[] args) { //真实角色 Host host = new Host(); //代理实例的调用处理程序 ProxyInvocationHandler pih = new ProxyInvocationHandler(); pih.setRent(host); //将真实角色放置进去! Rent proxy = (Rent)pih.getProxy(); //动态生成对应的代理类! proxy.rent(); } }核心:一个动态代理 , 一般代理某一类业务 , 一个动态代理可以代理多个类,代理的是接口!、6.4 深化理解我们来使用动态代理实现代理我们后面写的UserService!我们也可以编写一个通用的动态代理实现的类!所有的代理对象设置为Object即可!public class ProxyInvocationHandler implements InvocationHandler { private Object target; public void setTarget(Object target) { this.target = target; } //生成代理类 public Object getProxy(){ return Proxy.newProxyInstance(this.getClass().getClassLoader(), target.getClass().getInterfaces(),this); } // proxy : 代理类 // method : 代理类的调用处理程序的方法对象. public Object invoke(Object proxy, Method method, Object[] args) throws Throwable { log(method.getName()); Object result = method.invoke(target, args); return result; } public void log(String methodName){ System.out.println("执行了"+methodName+"方法"); } }测试!public class Test { public static void main(String[] args) { //真实对象 UserServiceImpl userService = new UserServiceImpl(); //代理对象的调用处理程序 ProxyInvocationHandler pih = new ProxyInvocationHandler(); pih.setTarget(userService); //设置要代理的对象 UserService proxy = (UserService)pih.getProxy(); //动态生成代理类! proxy.delete(); } }测试,增删改查,查看结果!动态代理的好处静态代理有的它都有,静态代理没有的,它也有!可以使得我们的真实角色更加纯粹 . 不再去关注一些公共的事情 .公共的业务由代理来完成 . 实现了业务的分工 ,公共业务发生扩展时变得更加集中和方便 .一个动态代理 , 一般代理某一类业务一个动态代理可以代理多个类,代理的是接口!7.AOP7.1 什么是AOPAOP(Aspect Oriented Programming)意为:面向切面编程,通过预编译方式和运行期动态代理实现程序功能的统一维护的一种技术。AOP是OOP的延续,是软件开发中的一个热点,也是Spring框架中的一个重要内容,是函数式编程的一种衍生范型。利用AOP可以对业务逻辑的各个部分进行隔离,从而使得业务逻辑各部分之间的耦合度降低,提高程序的可重用性,同时提高了开发的效率。7.2 Aop在Spring中的作用提供声明式事务;允许用户自定义切面以下名词需要了解下:横切关注点:跨越应用程序多个模块的方法或功能。即是,与我们业务逻辑无关的,但是我们需要关注的部分,就是横切关注点。如日志 , 安全 , 缓存 , 事务等等 ....切面(ASPECT):横切关注点 被模块化 的特殊对象。即,它是一个类。通知(Advice):切面必须要完成的工作。即,它是类中的一个方法。目标(Target):被通知对象。代理(Proxy):向目标对象应用通知之后创建的对象。切入点(PointCut):切面通知 执行的 “地点”的定义。连接点(JointPoint):与切入点匹配的执行点。SpringAOP中,通过Advice定义横切逻辑,Spring中支持5种类型的Advice:即 Aop 在 不改变原有代码的情况下 , 去增加新的功能 .7.3 使用Spring实现Aop【重点】使用AOP织入,需要导入一个依赖包!<!-- https://mvnrepository.com/artifact/org.aspectj/aspectjweaver --> <dependency> <groupId>org.aspectj</groupId> <artifactId>aspectjweaver</artifactId> <version>1.9.4</version> </dependency>第一种方式:通过 Spring API 实现首先编写我们的业务接口和实现类public interface UserService { public void add(); public void delete(); public void update(); public void search(); } public class UserServiceImpl implements UserService{ @Override public void add() { System.out.println("增加用户"); } @Override public void delete() { System.out.println("删除用户"); } @Override public void update() { System.out.println("更新用户"); } @Override public void search() { System.out.println("查询用户"); } }然后去写我们的增强类 , 我们编写两个 , 一个前置增强 一个后置增强public class BeforeLog implements MethodBeforeAdvice { //method : 要执行的目标对象的方法 //objects : 被调用的方法的参数 //Object : 目标对象 @Override public void before(Method method, Object[] objects, Object o) throws Throwable { System.out.println( o.getClass().getName() + "的" + method.getName() + "方法被执行了"); } } public class AfterLog implements AfterReturningAdvice { //returnValue 返回值 //method被调用的方法 //args 被调用的方法的对象的参数 //target 被调用的目标对象 @Override public void afterReturning(Object returnValue, Method method, Object[] args, Object target) throws Throwable { System.out.println("执行了" + target.getClass().getName() +"的"+method.getName()+"方法," +"返回值:"+returnValue); } }最后去spring的文件中注册 , 并实现aop切入实现 , 注意导入约束 .<?xml version="1.0" encoding="UTF-8"?> <beans xmlns="http://www.springframework.org/schema/beans" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:aop="http://www.springframework.org/schema/aop" xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd http://www.springframework.org/schema/aop http://www.springframework.org/schema/aop/spring-aop.xsd"> <!--注册bean--> <bean id="userService" class="com.kuang.service.UserServiceImpl"/> <bean id="beforLog" class="com.kuang.log.BeforeLog"/> <bean id="afterLog" class="com.kuang.log.AfterLog"/> <!--aop的配置--> <aop:config> <!--切入点 expression:表达式匹配要执行的方法--> <aop:pointcut id="pointcut" expression="execution(* com.kuang.service.UserServiceImpl.*(..))"/> <!--执行环绕; advice-ref执行方法 . pointcut-ref切入点--> <aop:advisor advice-ref="beforLog" pointcut-ref="pointcut"/> <aop:advisor advice-ref="afterLog" pointcut-ref="pointcut"/> </aop:config> </beans>测试public class MyTest { @Test public void test(){ ApplicationContext context = new ClassPathXmlApplicationContext("beans.xml"); UserService userService = (UserService) context.getBean("userService"); userService.search(); } }Aop的重要性 : 很重要 . 一定要理解其中的思路 , 主要是思想的理解这一块 .Spring的Aop就是将公共的业务 (日志 , 安全等) 和领域业务结合起来 , 当执行领域业务时 , 将会把公共业务加进来 . 实现公共业务的重复利用 . 领域业务更纯粹 , 程序猿专注领域业务 , 其本质还是动态代理 .第二种方式:自定义类来实现Aop目标业务类不变依旧是userServiceImpl第一步 : 写我们自己的一个切入类public class DiyPointcut { public void before(){ System.out.println("---------方法执行前---------"); } public void after(){ System.out.println("---------方法执行后---------"); } }去spring中配置<!--第二种方式自定义实现--> <!--注册bean--> <bean id="diy" class="com.kuang.config.DiyPointcut"/> <!--aop的配置--> <aop:config> <!--第二种方式:使用AOP的标签实现--> <aop:aspect ref="diy"> <aop:pointcut id="diyPonitcut" expression="execution(* com.kuang.service.UserServiceImpl.*(..))"/> <aop:before pointcut-ref="diyPonitcut" method="before"/> <aop:after pointcut-ref="diyPonitcut" method="after"/> </aop:aspect> </aop:config>测试:public class MyTest { @Test public void test(){ ApplicationContext context = new ClassPathXmlApplicationContext("beans.xml"); UserService userService = (UserService) context.getBean("userService"); userService.add(); } }第三种方式:使用注解实现第一步:编写一个注解实现的增强类package com.kuang.config; import org.aspectj.lang.ProceedingJoinPoint; import org.aspectj.lang.annotation.After; import org.aspectj.lang.annotation.Around; import org.aspectj.lang.annotation.Aspect; import org.aspectj.lang.annotation.Before; @Aspect public class AnnotationPointcut { @Before("execution(* com.kuang.service.UserServiceImpl.*(..))") public void before(){ System.out.println("---------方法执行前---------"); } @After("execution(* com.kuang.service.UserServiceImpl.*(..))") public void after(){ System.out.println("---------方法执行后---------"); } @Around("execution(* com.kuang.service.UserServiceImpl.*(..))") public void around(ProceedingJoinPoint jp) throws Throwable { System.out.println("环绕前"); System.out.println("签名:"+jp.getSignature()); //执行目标方法proceed Object proceed = jp.proceed(); System.out.println("环绕后"); System.out.println(proceed); } }第二步:在Spring配置文件中,注册bean,并增加支持注解的配置<!--第三种方式:注解实现--> <bean id="annotationPointcut" class="com.kuang.config.AnnotationPointcut"/> <aop:aspectj-autoproxy/>aop:aspectj-autoproxy:说明通过aop命名空间的<aop:aspectj-autoproxy />声明自动为spring容器中那些配置@aspectJ切面的bean创建代理,织入切面。当然,spring 在内部依旧采用AnnotationAwareAspectJAutoProxyCreator进行自动代理的创建工作,但具体实现的细节已经被<aop:aspectj-autoproxy />隐藏起来了 <aop:aspectj-autoproxy />有一个proxy-target-class属性,默认为false,表示使用jdk动态代理织入增强,当配为<aop:aspectj-autoproxy poxy-target-class="true"/>时,表示使用CGLib动态代理技术织入增强。不过即使proxy-target-class设置为false,如果目标类没有声明接口,则spring将自动使用CGLib动态代理。8.整合MyBatis8.1 步骤1、导入相关jar包<!--junit--> <dependency> <groupId>junit</groupId> <artifactId>junit</artifactId> <version>4.12</version> </dependency> <!--mybatis--> <dependency> <groupId>org.mybatis</groupId> <artifactId>mybatis</artifactId> <version>3.5.2</version> </dependency> <!--mysql-connector-java--> <dependency> <groupId>mysql</groupId> <artifactId>mysql-connector-java</artifactId> <version>5.1.47</version> </dependency> <!--spring相关--> <dependency> <groupId>org.springframework</groupId> <artifactId>spring-webmvc</artifactId> <version>5.1.10.RELEASE</version> </dependency> <dependency> <groupId>org.springframework</groupId> <artifactId>spring-jdbc</artifactId> <version>5.1.10.RELEASE</version> </dependency> <!--aspectJ AOP 织入器--> <!-- https://mvnrepository.com/artifact/org.aspectj/aspectjweaver --> <dependency> <groupId>org.aspectj</groupId> <artifactId>aspectjweaver</artifactId> <version>1.9.4</version> </dependency> <!--mybatis-spring整合包 【重点】--> <dependency> <groupId>org.mybatis</groupId> <artifactId>mybatis-spring</artifactId> <version>2.0.2</version> </dependency>配置Maven静态资源过滤问题!<build> <resources> <resource> <directory>src/main/java</directory> <includes> <include>**/*.properties</include> <include>**/*.xml</include> </includes> <filtering>true</filtering> </resource> </resources> </build>2、编写配置文件3、代码实现8.2 回忆MyBatis编写pojo实体类package com.kuang.pojo; public class User { private int id; //id private String name; //姓名 private String pwd; //密码 }实现mybatis的配置文件<?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> <typeAliases> <package name="com.kuang.pojo"/> </typeAliases> <environments default="development"> <environment id="development"> <transactionManager type="JDBC"/> <dataSource type="POOLED"> <property name="driver" value="com.mysql.jdbc.Driver"/> <property name="url" value="jdbc:mysql://localhost:3306/mybatis?useSSL=true&useUnicode=true&characterEncoding=utf8"/> <property name="username" value="root"/> <property name="password" value="123456"/> </dataSource> </environment> </environments> <mappers> <package name="com.kuang.dao"/> </mappers> </configuration>UserDao接口编写public interface UserMapper { public List<User> selectUser(); }接口对应的Mapper映射文件<?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.kuang.dao.UserMapper"> <select id="selectUser" resultType="User"> select * from user </select> </mapper>测试类@Test public void selectUser() throws IOException { String resource = "mybatis-config.xml"; InputStream inputStream = Resources.getResourceAsStream(resource); SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(inputStream); SqlSession sqlSession = sqlSessionFactory.openSession(); UserMapper mapper = sqlSession.getMapper(UserMapper.class); List<User> userList = mapper.selectUser(); for (User user: userList){ System.out.println(user); } sqlSession.close(); }8.3 MyBatis-Spring学习引入Spring之前需要了解mybatis-spring包中的一些重要类;http://www.mybatis.org/spring/zh/index.html什么是 MyBatis-Spring?MyBatis-Spring 会帮助你将 MyBatis 代码无缝地整合到 Spring 中。知识基础在开始使用 MyBatis-Spring 之前,你需要先熟悉 Spring 和 MyBatis 这两个框架和有关它们的术语。这很重要MyBatis-Spring 需要以下版本:MyBatis-SpringMyBatisSpring 框架Spring BatchJava2.03.5+5.0+4.0+Java 8+1.33.4+3.2.2+2.1+Java 6+如果使用 Maven 作为构建工具,仅需要在 pom.xml 中加入以下代码即可:<dependency> <groupId>org.mybatis</groupId> <artifactId>mybatis-spring</artifactId> <version>2.0.2</version> </dependency>要和 Spring 一起使用 MyBatis,需要在 Spring 应用上下文中定义至少两样东西:一个 SqlSessionFactory 和至少一个数据映射器类。在 MyBatis-Spring 中,可使用SqlSessionFactoryBean来创建 SqlSessionFactory。要配置这个工厂 bean,只需要把下面代码放在 Spring 的 XML 配置文件中:<bean id="sqlSessionFactory" class="org.mybatis.spring.SqlSessionFactoryBean"> <property name="dataSource" ref="dataSource" /> </bean>注意:SqlSessionFactory需要一个 DataSource(数据源)。这可以是任意的 DataSource,只需要和配置其它 Spring 数据库连接一样配置它就可以了。在基础的 MyBatis 用法中,是通过 SqlSessionFactoryBuilder 来创建 SqlSessionFactory 的。而在 MyBatis-Spring 中,则使用 SqlSessionFactoryBean 来创建。在 MyBatis 中,你可以使用 SqlSessionFactory 来创建 SqlSession。一旦你获得一个 session 之后,你可以使用它来执行映射了的语句,提交或回滚连接,最后,当不再需要它的时候,你可以关闭 session。SqlSessionFactory有一个唯一的必要属性:用于 JDBC 的 DataSource。这可以是任意的 DataSource 对象,它的配置方法和其它 Spring 数据库连接是一样的。一个常用的属性是 configLocation,它用来指定 MyBatis 的 XML 配置文件路径。它在需要修改 MyBatis 的基础配置非常有用。通常,基础配置指的是 < settings> 或 < typeAliases>元素。需要注意的是,这个配置文件并不需要是一个完整的 MyBatis 配置。确切地说,任何环境配置(),数据源()和 MyBatis 的事务管理器()都会被忽略。SqlSessionFactoryBean 会创建它自有的 MyBatis 环境配置(Environment),并按要求设置自定义环境的值。SqlSessionTemplate 是 MyBatis-Spring 的核心。作为 SqlSession 的一个实现,这意味着可以使用它无缝代替你代码中已经在使用的 SqlSession。模板可以参与到 Spring 的事务管理中,并且由于其是线程安全的,可以供多个映射器类使用,你应该总是用 SqlSessionTemplate 来替换 MyBatis 默认的 DefaultSqlSession 实现。在同一应用程序中的不同类之间混杂使用可能会引起数据一致性的问题。可以使用 SqlSessionFactory 作为构造方法的参数来创建 SqlSessionTemplate 对象。<bean id="sqlSession" class="org.mybatis.spring.SqlSessionTemplate"> <constructor-arg index="0" ref="sqlSessionFactory" /> </bean>现在,这个 bean 就可以直接注入到你的 DAO bean 中了。你需要在你的 bean 中添加一个 SqlSession 属性,就像下面这样:public class UserDaoImpl implements UserDao { private SqlSession sqlSession; public void setSqlSession(SqlSession sqlSession) { this.sqlSession = sqlSession; } public User getUser(String userId) { return sqlSession.getMapper...; } }按下面这样,注入 SqlSessionTemplate:<bean id="userDao" class="org.mybatis.spring.sample.dao.UserDaoImpl"> <property name="sqlSession" ref="sqlSession" /> </bean>8.4 整合实现一1、引入Spring配置文件beans.xml<?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">2、配置数据源替换mybaits的数据源<!--配置数据源:数据源有非常多,可以使用第三方的,也可使使用Spring的--> <bean id="dataSource" class="org.springframework.jdbc.datasource.DriverManagerDataSource"> <property name="driverClassName" value="com.mysql.jdbc.Driver"/> <property name="url" value="jdbc:mysql://localhost:3306/mybatis?useSSL=true&useUnicode=true&characterEncoding=utf8"/> <property name="username" value="root"/> <property name="password" value="123456"/> </bean>3、配置SqlSessionFactory,关联MyBatis<!--配置SqlSessionFactory--> <bean id="sqlSessionFactory" class="org.mybatis.spring.SqlSessionFactoryBean"> <property name="dataSource" ref="dataSource"/> <!--关联Mybatis--> <property name="configLocation" value="classpath:mybatis-config.xml"/> <property name="mapperLocations" value="classpath:com/kuang/dao/*.xml"/> </bean>4、注册sqlSessionTemplate,关联sqlSessionFactory;<!--注册sqlSessionTemplate , 关联sqlSessionFactory--> <bean id="sqlSession" class="org.mybatis.spring.SqlSessionTemplate"> <!--利用构造器注入--> <constructor-arg index="0" ref="sqlSessionFactory"/> </bean>5、增加Dao接口的实现类;私有化sqlSessionTemplatepublic class UserMapperImpl implements UserMapper { //sqlSession不用我们自己创建了,Spring来管理 private SqlSessionTemplate sqlSession; public void setSqlSession(SqlSessionTemplate sqlSession) { this.sqlSession = sqlSession; } public List<User> selectUser() { UserMapper mapper = sqlSession.getMapper(UserMapper.class); return mapper.selectUser(); } }6、注册bean实现<bean id="UserMapper" class="com.kuang.mapper.UserMapperImpl"> <property name="sqlSession" ref="sqlSession"/> </bean>7、测试 @Test public void test2(){ ApplicationContext context = new ClassPathXmlApplicationContext("beans.xml"); UserMapper mapper = (UserMapper) context.getBean("userDao"); List<User> user = mapper.selectUser(); System.out.println(user); }结果成功输出!现在我们的Mybatis配置文件的状态!发现都可以被Spring整合!<?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> <typeAliases> <package name="com.kuang.pojo"/> </typeAliases> </configuration>8.5 整合实现二mybatis-spring1.2.3版以上的才有这个 .官方文档截图 :dao继承Support类 , 直接利用 getSqlSession() 获得 , 然后直接注入SqlSessionFactory . 比起方式1 , 不需要管理SqlSessionTemplate , 而且对事务的支持更加友好 . 可跟踪源码查看测试:1、将我们上面写的UserDaoImpl修改一下public class UserDaoImpl extends SqlSessionDaoSupport implements UserMapper { public List<User> selectUser() { UserMapper mapper = getSqlSession().getMapper(UserMapper.class); return mapper.selectUser(); } }2、修改bean的配置<bean id="userDao" class="com.kuang.dao.UserDaoImpl"> <property name="sqlSessionFactory" ref="sqlSessionFactory" /> </bean>3、测试@Test public void test2(){ ApplicationContext context = new ClassPathXmlApplicationContext("beans.xml"); UserMapper mapper = (UserMapper) context.getBean("userDao"); List<User> user = mapper.selectUser(); System.out.println(user); }9.声明式事务9.1 回顾事务事务在项目开发过程非常重要,涉及到数据的一致性的问题,不容马虎!事务管理是企业级应用程序开发中必备技术,用来确保数据的完整性和一致性。事务就是把一系列的动作当成一个独立的工作单元,这些动作要么全部完成,要么全部不起作用。事务四个属性ACID原子性(atomicity)事务是原子性操作,由一系列动作组成,事务的原子性确保动作要么全部完成,要么完全不起作用一致性(consistency)一旦所有事务动作完成,事务就要被提交。数据和资源处于一种满足业务规则的一致性状态中隔离性(isolation)可能多个事务会同时处理相同的数据,因此每个事务都应该与其他事务隔离开来,防止数据损坏持久性(durability)事务一旦完成,无论系统发生什么错误,结果都不会受到影响。通常情况下,事务的结果被写到持久化存储器中9.2 测试将上面的代码拷贝到一个新项目中在之前的案例中,我们给userDao接口新增两个方法,删除和增加用户;//添加一个用户 public int addUser(User user); //根据id删除用户 public int deleteUser(int id);mapper文件,我们故意把 deletes 写错,测试!<insert id="addUser" parameterType="com.kuang.pojo.User"> insert into user (id,name,pwd) values (#{id},#{name},#{pwd}) </insert> <delete id="deleteUser" parameterType="int"> deletes from user where id = #{id} </delete>编写接口的实现类,在实现类中,我们去操作一波public class UserDaoImpl extends SqlSessionDaoSupport implements UserMapper { //增加一些操作 public List<User> selectUser() { User user = new User(4,"小明","123456"); UserMapper mapper = getSqlSession().getMapper(UserMapper.class); mapper.addUser(user); mapper.deleteUser(4); return mapper.selectUser(); } //新增 public int addUser(User user) { UserMapper mapper = getSqlSession().getMapper(UserMapper.class); return mapper.addUser(user); } //删除 public int deleteUser(int id) { UserMapper mapper = getSqlSession().getMapper(UserMapper.class); return mapper.deleteUser(id); } }测试@Test public void test2(){ ApplicationContext context = new ClassPathXmlApplicationContext("beans.xml"); UserMapper mapper = (UserMapper) context.getBean("userDao"); List<User> user = mapper.selectUser(); System.out.println(user); }报错:sql异常,delete写错了结果 :插入成功!没有进行事务的管理;我们想让他们都成功才成功,有一个失败,就都失败,我们就应该需要事务!以前我们都需要自己手动管理事务,十分麻烦!但是Spring给我们提供了事务管理,我们只需要配置即可;9.3 Spring中的事务管理Spring在不同的事务管理API之上定义了一个抽象层,使得开发人员不必了解底层的事务管理API就可以使用Spring的事务管理机制。Spring支持编程式事务管理和声明式的事务管理。编程式事务管理将事务管理代码嵌到业务方法中来控制事务的提交和回滚缺点:必须在每个事务操作业务逻辑中包含额外的事务管理代码声明式事务管理一般情况下比编程式事务好用。将事务管理代码从业务方法中分离出来,以声明的方式来实现事务管理。将事务管理作为横切关注点,通过aop方法模块化。Spring中通过Spring AOP框架支持声明式事务管理。使用Spring管理事务,注意头文件的约束导入 : txxmlns:tx="http://www.springframework.org/schema/tx" http://www.springframework.org/schema/tx http://www.springframework.org/schema/tx/spring-tx.xsd"> <beans xmlns="http://www.springframework.org/schema/beans" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:tx="http://www.springframework.org/schema/tx" xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd http://www.springframework.org/schema/tx http://www.springframework.org/schema/tx/spring-tx.xsd">事务管理器无论使用Spring的哪种事务管理策略(编程式或者声明式)事务管理器都是必须的。就是 Spring的核心事务管理抽象,管理封装了一组独立于技术的方法。JDBC事务<bean id="transactionManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager"> <property name="dataSource" ref="dataSource" /> </bean>配置好事务管理器后我们需要去配置事务的通知<!--配置事务通知--> <tx:advice id="txAdvice" transaction-manager="transactionManager"> <tx:attributes> <!--配置哪些方法使用什么样的事务,配置事务的传播特性--> <tx:method name="add" propagation="REQUIRED"/> <tx:method name="delete" propagation="REQUIRED"/> <tx:method name="update" propagation="REQUIRED"/> <tx:method name="search*" propagation="REQUIRED"/> <tx:method name="get" read-only="true"/> <tx:method name="*" propagation="REQUIRED"/> </tx:attributes> </tx:advice>spring事务传播特性:事务传播行为就是多个事务方法相互调用时,事务如何在这些方法间传播。spring支持7种事务传播行为:propagation_requierd:如果当前没有事务,就新建一个事务,如果已存在一个事务中,加入到这个事务中,这是最常见的选择。propagation_supports:支持当前事务,如果没有当前事务,就以非事务方法执行。propagation_mandatory:使用当前事务,如果没有当前事务,就抛出异常。propagation_required_new:新建事务,如果当前存在事务,把当前事务挂起。propagation_not_supported:以非事务方式执行操作,如果当前存在事务,就把当前事务挂起。propagation_never:以非事务方式执行操作,如果当前事务存在则抛出异常。propagation_nested:如果当前存在事务,则在嵌套事务内执行。如果当前没有事务,则执行与propagation_required类似的操作Spring 默认的事务传播行为是 PROPAGATION_REQUIRED,它适合于绝大多数的情况。假设 ServiveX#methodX() 都工作在事务环境下(即都被 Spring 事务增强了),假设程序中存在如下的调用链:Service1#method1()->Service2#method2()->Service3#method3(),那么这 3 个服务类的 3 个方法通过 Spring 的事务传播机制都工作在同一个事务中。就好比,我们刚才的几个方法存在调用,所以会被放在一组事务当中!配置AOP导入aop的头文件!<!--配置aop织入事务--> <aop:config> <aop:pointcut id="txPointcut" expression="execution(* com.kuang.dao.*.*(..))"/> <aop:advisor advice-ref="txAdvice" pointcut-ref="txPointcut"/> </aop:config>进行测试删掉刚才插入的数据,再次测试!@Test public void test2(){ ApplicationContext context = new ClassPathXmlApplicationContext("beans.xml"); UserMapper mapper = (UserMapper) context.getBean("userDao"); List<User> user = mapper.selectUser(); System.out.println(user); }思考问题?为什么需要配置事务?如果不配置,就需要我们手动提交控制事务;事务在项目开发过程非常重要,涉及到数据的一致性的问题,不容马虎!参考资料【狂神说Java】Spring5最新完整教程IDEA版通俗易懂
2022年04月28日
854 阅读
0 评论
0 点赞
1
2