-
大佬们能不能简单通俗的说下spring @resource和@autowired的区别,通俗易懂最好,搜索引擎有的看不懂,
-
事务管理在系统开发中是不可缺少的一部分,Spring 提供了很好事务管理机制,主要分为编程式事务和声明式事务两种。 关于事务的基础知识,如什么是事务,数据库事务以及 Spring 事务的 ACID、隔离级别、传播机制、行为等,就不在这篇文章中详细介绍了。默认大家都有一定的了解。 本文,作者会先简单介绍下什么是声明式事务和编程式事务,再说一下为什么我不建议使用声明式事务。 编程式事务 基于底层的 API,如 PlatformTransactionManager、TransactionDefinition 和 TransactionTemplate 等核心接口,开发者完全可以通过编程的方式来进行事务管理。 编程式事务方式需要是开发者在代码中手动的管理事务的开启、提交、回滚等操作。 public void test() { TransactionDefinition def = new DefaultTransactionDefinition(); TransactionStatus status = transactionManager.getTransaction(def); try { // 事务操作 // 事务提交 transactionManager.commit(status); } catch (DataAccessException e) { // 事务提交 transactionManager.rollback(status); throw e; } } 如以上代码,开发者可以通过 API 自己控制事务。 声明式事务 声明式事务管理方法允许开发者配置的帮助下来管理事务,而不需要依赖底层 API 进行硬编码。开发者可以只使用注解或基于配置的 XML 来管理事务。 @Transactional public void test() { // 事务操作 } 如上,使用 @Transactional 即可给 test 方法增加事务控制。 当然,上面的代码只是简化后的,想要使用事务还需要一些配置内容。这里就不详细阐述了。 这两种事务,格子有各自的优缺点,那么,各自有哪些适合的场景呢?为什么有人会拒绝使用声明式事务呢? 声明式事务的优点 通过上面的例子,其实我们可以很容易的看出来,声明式事务帮助我们节省了很多代码,他会自动帮我们进行事务的开启、提交以及回滚等操作,把程序员从事务管理中解放出来。 声明式事务管理使用了 AOP 实现的,本质就是在目标方法执行前后进行拦截。在目标方法执行前加入或创建一个事务,在执行方法执行后,根据实际情况选择提交或是回滚事务。 使用这种方式,对代码没有侵入性,方法内只需要写业务逻辑就可以了。 但是,声明式事务真的有这么好么?倒也不见得。 声明式事务的粒度问题 首先,声明式事务有一个局限,那就是他的最小粒度要作用在方法上。 也就是说,如果想要给一部分代码块增加事务的话,那就需要把这个部分代码块单独独立出来作为一个方法。 但是,正是因为这个粒度问题,本人并不建议过度的使用声明式事务。 首先,因为声明式事务是通过注解的,有些时候还可以通过配置实现,这就会导致一个问题,那就是这个事务有可能被开发者忽略。 事务被忽略了有什么问题呢? 首先,如果开发者没有注意到一个方法是被事务嵌套的,那么就可能会再方法中加入一些如 RPC 远程调用、消息发送、缓存更新、文件写入等操作。 我们知道,这些操作如果被包在事务中,有两个问题: 1、这些操作自身是无法回滚的,这就会导致数据的不一致。可能 RPC 调用成功了,但是本地事务回滚了,可是 PRC 调用无法回滚了。 2、在事务中有远程调用,就会拉长整个事务。那么久会导致本事务的数据库连接一直被占用,那么如果类似操作过多,就会导致数据库连接池耗尽。 有些时候,即使没有在事务中进行远程操作,但是有些人还是可能会不经意的进行一些内存操作,如运算。或者如果遇到分库分表的情况,有可能不经意间进行跨库操作。 但是如果是编程式事务的话,业务代码中就会清清楚楚看到什么地方开启事务,什么地方提交,什么时候回滚。这样有人改这段代码的时候,就会强制他考虑要加的代码是否应该方法事务内。 有些人可能会说,已经有了声明式事务,但是写代码的人没注意,这能怪谁。 话虽然是这么说,但是我们还是希望可以通过一些机制或者规范,降低这些问题发生的概率。 比如建议大家使用编程式事务,而不是声明式事务。因为,作者工作这么多年来,发生过不止一次开发者没注意到声明式事务而导致的故障。 因为有些时候,声明式事务确实不够明显。 声明式事务用不对容易失效 除了事务的粒度问题,还有一个问题那就是声明式事务虽然看上去帮我们简化了很多代码,但是一旦没用对,也很容易导致事务失效。 如以下几种场景就可能导致声明式事务失效: 1、@Transactional 应用在非 public 修饰的方法上 2、@Transactional 注解属性 propagation 设置错误 3、@Transactional 注解属性 rollbackFor 设置错误 4、同一个类中方法调用,导致 @Transactional 失效 5、异常被 catch 捕获导致 @Transactional 失效 6、数据库引擎不支持事务 以上几个问题,如果使用编程式事务的话,很多都是可以避免的。 使用声明事务失效的问题我们发生过很多次。不知道大家有没有遇到过,我是实际遇到过的 因为 Spring 的事务是基于 AOP 实现的,但是在代码中,有时候我们会有很多切面,不同的切面可能会来处理不同的事情,多个切面之间可能会有相互影响。 在之前的一个项目中,我就发现我们的 Service 层的事务全都失效了,一个 SQL 执行失败后并没有回滚,排查下来才发现,是因为一位同事新增了一个切面,这个切面里面做个异常的统一捕获,导致事务的切面没有捕获到异常,导致事务无法回滚。 这样的问题,发生过不止一次,而且不容易被发现。 很多人还是会说,说到底还是自己能力不行,对事务理解不透彻,用错了能怪谁。 但是我还是那句话,我们确实无法保证所有人的能力都很高,也无法要求所有开发者都能不出错。我们能做的就是,尽量可以通过机制或者规范,来避免或者降低这些问题发生的概率。 其实,如果大家有认真看过阿里巴巴出的那份 Java 开发手册的话,其实就能发现,其中的很多规约并不是完完全全容易被人理解,有些也比较生硬,但是其实,这些规范都是从无数个坑里爬出来的开发者们总结出来的。 关于 @Transactional 的用法,规约中也有提到过,只不过规约中的观点没有我这么鲜明: 总结 最后,相信本文的观点很多人都并不一定认同,很多人会说:Spring 官方都推荐无侵入性的声明式事务,你有啥资格出来 BB 。 说实话,刚工作的前几年,我也热衷于使用声明式事务,觉得很干净,也很” 优雅”。觉得师兄们使用编程式事务多此一举,没有工匠精神。 但是慢慢的,线上发生过几次问题之后,我们复盘后发现,很多时候你自己写的代码很优雅,这完全没问题。 但是,优雅的同时也带来了一些副作用,师兄们又不能批评我,因为我的用法确实没错… 所以,有些事,还是要痛过之后才知道。 当然,本文并不要求大家一定要彻底不使用声明式事务,只是建议大家日后在使用事务的时候,能够考虑到本文中提到的观点,然后自行选择。 ———————————————— 原文作者:zhaozhangxiao 转自链接:https://learnku.com/articles/69225
-
Bean的自动装配 5.1、自动装配说明 自动装配是使用spring满足bean依赖的一种方法 spring会在应用上下文中为某个bean寻找其依赖的bean。 Spring中bean有三种装配机制,分别是: 在xml中显式配置; 在java中显式配置; 隐式的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、新建一个用户类 User public class User { private Cat cat; private Dog dog; private String name; } 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" xmlns:p="http://www.springframework.org/schema/p" xmlns:c="http://www.springframework.org/schema/c" xsi:schemaLocation="http://www.springframework.org/schema/beans https://www.springframework.org/schema/beans/spring-beans.xsd"> <bean id="cat" class="com.bby.pojo.Cat"/> <bean id="dog" class="com.bby.pojo.Dog"/> <bean id="user" class="com.bby.pojo.User"> <property name="name" value="啵啵鱼"/> <property name="cat" ref="cat"/> <property name="dog" ref="dog"/> </bean> </beans> 5、测试 public class UserTest { @Test public void test() { ApplicationContext context = new ClassPathXmlApplicationContext("beans.xml"); User user = context.getBean("user", User.class); System.out.println(user); user.getCat().shout(); user.getDog().shout(); } } 5.3、 自动装配(autowire) 5.3.1、byName (按名称自动装配) 由于在手动配置xml过程中,常常发生字母缺漏和大小写等错误,而无法对其进行检查,使得开发效率降低。 采用自动装配将避免这些错误,并且使配置简单化。 测试: 1、修改bean配置,增加一个属性 autowire=“byName” <bean id="user" class="com.bby.pojo.User" autowire="byName"> <property name="name" value="啵啵鱼"/> </bean> 1 2 3 2、再次测试,结果依旧成功输出! 3、我们将 cat 的bean id修改为 catXXX 4、再次测试, 执行时报空指针java.lang.NullPointerException。因为按byName规则找不对应set方法,真正的setCat就没执行,对象就没有初始化,所以调用时就会报空指针错误。 小结: 当一个bean节点带有 autowire byName的属性时。 将查找其类中所有的set方法名,例如setCat,获得将set去掉并且首字母小写的字符串,即cat。 去spring容器中寻找是否有此字符串名称id的对象。 如果有,就取出注入;如果没有,就报空指针异常。 5.3.2、byType (按类型自动装配) 使用autowire byType首先需要保证:同一类型的对象,在spring容器中唯一。如果不唯一,会报不唯一的异常。 NoUniqueBeanDefinitionException 1 测试: 1、将user的bean配置修改一下 : autowire=“byType” 2、测试,正常输出 3、在注册一个cat 的bean对象! <bean id="dog" class="com.bby.pojo.Dog"/> <bean id="cat" class="com.bby.pojo.Cat"/> <bean id="cat2" class="com.bby.pojo.Cat"/> <bean id="user" class="com.bby.pojo.User" autowire="byType"> <property name="str" value="啵啵鱼"/> </bean> 4、测试,报错:NoUniqueBeanDefinitionException 5、删掉cat2,将cat的bean名称改掉!测试!因为是按类型装配,所以并不会报异常,也不影响最后的结果。甚至将id属性去掉,也不影响结果。 5.3.3、使用注解 jdk1.5开始支持注解,spring2.5开始全面支持注解。 (1)准备工作 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 1 2 3 4 2、开启属性注解支持! <context:annotation-config/> 1 (2)@Autowired 直接在属性上使用即可!也可以再set方式上使用! 使用AutoWired我们可以不用编写Set方法了,前提是这个自动装配的属性在IOC(Spring) 容器中存在 @Autowired默认使用 byType来装配属性,如果匹配到类型的多个实例,再通过 byName来确定 bean 需要导入 spring-aop的包! 测试: 1、将User类中的set方法去掉,使用@Autowired注解 public class User { private String name; @Autowired private Cat cat; @Autowired private Dog dog; public String getName() { return name; } public Cat getCat() { return cat; } public Dog getDog() { return dog; } @Override public String toString() { return "User{" + "name='" + name + '\'' + ", cat=" + cat + ", dog=" + dog + '}'; } } 科普: @Nullable //字段标记了这个注解,说明这个字段可以为null 1 //如果允许对象为null,设置required = false,默认为true @Autowired(required = false) private Cat cat; (3)@Qualifier 加上@Qualifier可以根据byName的方式自动装配 @Qualifier不能单独使用。 测试实验步骤: 1、配置文件修改内容,保证类型存在对象。且名字不为类的默认名字(默认名字为类的小写 如:Dog类名字为dog)! <bean id="dog1" class="com.bby.pojo.Dog"/> <bean id="dog2" class="com.bby.pojo.Dog"/> <bean id="cat1" class="com.bby.pojo.Cat"/> <bean id="cat2" class="com.bby.pojo.Cat"/> 2、没有加Qualifier测试,直接报错 3、在属性上添加Qualifier注解 @Autowired @Qualifier(value = "cat2") private Cat cat; @Autowired @Qualifier(value = "dog2") private Dog dog; 测试,成功输出! (4)@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.bby.pojo.Dog"/> <bean id="cat1" class="com.bby.pojo.Cat"/> <bean id="cat2" class="com.bby.pojo.Cat"/> <bean id="user" class="com.bby.pojo.User"/> 1 2 3 4 5 测试:结果OK 配置文件2:beans.xml , 删掉cat2 <bean id="dog" class="com.bby.pojo.Dog"/> <bean id="cat1" class="com.bby.pojo.Cat"/> 1 2 实体类上只保留注解 @Resource private Cat cat; @Resource private Dog dog; 1 2 3 4 结果:OK 结论:先进行byName查找,失败;再进行byType查找,成功。 (5)@Resource 和 @Autowired的区别 都是用来自动装配的,都可以放在属性字段上 @Autowired默认通过byType 的方式,当匹配到多个同类型时,使用byName进行装配,默认情况下必须要求依赖对象必须存在,如果要允许null 值,可以设置它的required属性为false,如:@Autowired(required=false) ,如果我们想使用名称装配可以结合@Qualifier注解进行使用 @Resource(属于J2EE复返)默认通过byName的方式实现,如果找不到名字,则通过byType实现!如果name属性一旦指定,就只会按照名称进行装配。如果两个都找不到就报错 它们的作用相同都是用注解方式注入对象,但执行顺序不同,@Autowired先byType,@Resource先byName。 ———————————————— 版权声明:本文为CSDN博主「-BoBooY-」的原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接及本声明。 原文链接:https://blog.csdn.net/qq_58233406/article/details/127331116
-
一:回顾梳理 1.流程图 完成前面的操作之后,使用延迟队列定时提交文章功能就算完成了。在测试过程中我发现一个问题,虽然数据做了持久化处理,但是当每次消费任务之后数据库中该条数据也会随之被清理掉,这时候还会存在数据丢失的风险。为什么这么说呢,我们的定时发布是按照下面的流程图进行的: 2.代码 对应的代码如下: @Autowired private WmNewsAutoScanServiceImpl wmNewsAutoScanService; /** * 消费延迟队列数据 */ @Scheduled(fixedRate = 1000) @Override @SneakyThrows public void scanNewsByTask() { ResponseResult responseResult = scheduleClient.poll(TaskTypeEnum.NEWS_SCAN_TIME.getTaskType(), TaskTypeEnum.NEWS_SCAN_TIME.getPriority()); if(responseResult.getCode().equals(200) && responseResult.getData() != null){ log.info("文章审核---消费任务执行---begin---"); String json_str = JSON.toJSONString(responseResult.getData()); Task task = JSON.parseObject(json_str, Task.class); byte[] parameters = task.getParameters(); WmNews wmNews = ProtostuffUtil.deserialize(parameters, WmNews.class); System.out.println(wmNews.getId()+"-----------"); wmNewsAutoScanService.autoScanWmNews(wmNews.getId()); log.info("文章审核---消费任务执行---end---"); } } /** * 删除任务,更新日志 * @param taskId * @param status * @return */ private Task UpdateDb(long taskId, int status) { Task task = null; try { //1.删除任务 log.info("删除数据库中的任务..."); taskInfoMapper.deleteById(taskId); //2.更新日志 log.info("更新任务日志..."); TaskinfoLogs taskinfoLogs = taskInfoLogsMapper.selectById(taskId); taskinfoLogs.setStatus(status); taskInfoLogsMapper.updateById(taskinfoLogs); //3.设置返回值 task = new Task(); BeanUtils.copyProperties(taskinfoLogs,task); task.setExecuteTime(taskinfoLogs.getExecuteTime().getTime()); } catch (BeansException e) { throw new RuntimeException(e); } return task; } /** * 消费任务 * @param type 任务类型 * @param priority 任务优先级 * @return Task */ @Override public Task poll(int type, int priority) { Task task = null; try { String key = type + "_" + priority; String task_json = cacheService.lRightPop(ScheduleConstants.TOPIC + key); if(StringUtils.isNotBlank(task_json)) { task = JSON.parseObject(task_json,Task.class); //更新数据库 UpdateDb(task.getTaskId(),ScheduleConstants.EXECUTED); } } catch (Exception e) { e.printStackTrace(); log.error("poll task exception"); } return task; } 可以看到在redis中获取数据之后便将数据从数据库中删除,这时候假如后面的审核流程出现问题或者保存文章时候移动端微服务出现故障导致文章不能保存,而这时候数据库中及redis中的数据都删除了,这就造成了数据的丢失。 二:第一次优化 1.优化策略 首先我想到的优化策略是当检测到文章审核或者文章保存值移动端有异常时候就将已经出队列的数据重新放回队列并且在5分钟之后再进行消费直到消费成功,流程图见下图: 2.代码实现 WmAutoScanServiceImpl package com.my.wemedia.service.impl; @Slf4j @Service @Transactional public class WmAutoScanServiceImpl implements WmAutoScanService { @Autowired private WmNewsService wmNewsService; @Autowired private TextDetection textDetection; @Autowired private ImageDetection imageDetection; /** * 自动审核文章文本及图片 * @param id */ @Override @Async public void AutoScanTextAndImage(Integer id) throws Exception { log.info("开始进行文章审核..."); // Thread.sleep(300); //休眠300毫秒,以保证能够获取到数据库中的数据 WmNews wmNews = wmNewsService.getById(id); if(wmNews == null) { throw new RuntimeException("WmAutoScanServiceImpl-文章信息不存在"); } if(wmNews.getStatus().equals(WmNews.Status.SUBMIT.getCode())) { //中间步骤省略 //4,审核成功 //4.1保存文章 log.info("检测到文章无违规内容"); ResponseResult responseResult = saveAppArticle(wmNews); if(!responseResult.getCode().equals(200)) { throw new RuntimeException("AutoScanTextAndImage--检测失败"); } } } /** * 保存App端文章数据 * @param wmNews * @return */ @Override public ResponseResult saveAppArticle(WmNews wmNews) { //中间步骤省略 //7.保存App端文章 log.info("异步调用保存文章至App端"); ResponseResult responseResult = null; try { responseResult = iArticleClient.saveArticle(articleDto); } catch (Exception e) { responseResult = new ResponseResult(AppHttpCodeEnum.SERVER_ERROR.getCode(),"保存文章至App失败"); } return responseResult; } } WmNewsTaskServiceImpl @Autowired private WmAutoScanService wmAutoScanService; @Autowired private CacheService cacheService; /** * 消费任务 */ @Override @Scheduled(fixedRate = 2000) //每两秒执行一次 public void scanNewsByTask() { ResponseResult responseResult = scheduleClient.poll(TaskTypeEnum.NEWS_SCAN_TIME.getTaskType(), TaskTypeEnum.NEWS_SCAN_TIME.getPriority()); if(responseResult.getCode().equals(200) && responseResult.getData() != null) { log.info("文章审核---消费任务执行---begin"); String json_str = JSON.toJSONString(responseResult.getData()); Task task = JSON.parseObject(json_str,Task.class); byte[] parameters = task.getParameters(); //反序列化 WmNews wmNews = ProtostuffUtil.deserialize(parameters, WmNews.class); try { wmAutoScanService.AutoScanTextAndImage(wmNews.getId()); } catch (Exception e) { log.warn("审核失败,将于5分钟之后再次尝试"); //文章未能成功审核,将数据加入ZSet,5分钟之后再重新尝试 //1.构造key String key = task.getTaskType() + "_" + task.getPriority(); //2.获取5分钟之后的时间 Calendar calendar = Calendar.getInstance(); calendar.add(Calendar.MINUTE,5); long nextScheduleTime = calendar.getTimeInMillis(); //3.将数据重新加入 cacheService.zAdd(ScheduleConstants.FUTURE + key,JSON.toJSONString(task),nextScheduleTime); e.printStackTrace(); } log.info("文章审核---消费任务执行---end"); } 3.遇到的问题 这时候虽然做了优化,但是在实际运行时候 WmNewsTaskServiceImpl中的try-catch并没有发挥作用,我没有开启移动端微服务,在审核时候也抛出了异常,但是这时候WmNewsTaskServiceImpl中并没有catch到这个异常。这是为什么呢,通过断点发现在执行到AutoScanTextAndImage方法时候,程序便直接跳出了catch块,这时候便想到前面我们还没有引入延迟队列时候用的是异步方法来调用文本图片审核方法来进行审核,而且当时还出现了在数据库中获取不到数据的问题,现在想起这就是因为采用了异步调用才引起的。在这里也一样,由于在AutoScanTextAndImage方法前面添加了@Async注解,表明这是一个异步方法,这时候我们在 WmNewsTaskServiceImpl中调用该方法时候是异步执行的,这当然就捕获不到抛出的异常,因需要将该方法的@Async注解去掉,改成同步调用。 三:第二次优化 1.问题引入 问题一: 解决完上面的问题,看着似乎问题得到了解决,消息没有正确被消费时候会被重新投递回去直到被正确消费,但是这时候还应该注意到另外一个问题。虽然消息消费失败之后被重新投递了,但是这时候数据库中的数据已经被删除掉了,假如redis服务器出现了问题,这时候就算你采用了重回队列策略数据还是永久丢失了,因为你的持久化处理在这时候已经失效了。这时候可以考虑失败之后将数据再存回数据库中,这样再次做了持久化处理,但是这样显然会造成不必要的IO操作。 问题二: 前面的做法是将审核的方法由异步改成了同步,这时候由于调用的是第三方的审核接口,有时候难免会因为网络等原因造成审核时间很长,这时候假如采用同步策略,就会造成长时间阻塞,影响用户体验,同时也会浪费大量资源。 2.解决策略 针对问题一,我认为在一开始对队列消息进行消费时候就不应该立马删除数据库中的数据,而是等到最后确保消息被正确处理之后再删除数据库中相应的数据。 针对问题二,我认为不应该将审核方法由异步改成同步,但是这时候就会出现前面提到的问题----catch不到异常,这时候似乎又回到了起点。试想,我们使用这个异常捕获的目的是什么?我们的目的就是为了出现异常时候将消息重新入队防止数据丢失,这时候我们不妨换一种策略,前面提到了我们在确保消息被正确消费之后再删除数据库中的数据,这不就已经解决了问题了吗?我们会定时同步数据库中的数据到redis,这时候就算消息在redis中丢失了也没关系,只要数据库中的数据还在就行。 流程图见下图: 3.代码实现 ①在tbug-headlines-feign-api模块的IScheduleClient接口中添加如下内容: /** * 删除数据库中的任务,更新日志 * @param taskId */ @DeleteMapping("/api/v1/task/delete/{taskId}/{status}") void updateDb(@PathVariable("taskId") Long taskId, @PathVariable("status") Integer status); ②在package com.my.schedule.feign中更新该接口的实现类 /** * 删除数据库中任务,更新日志 * @param taskId */ @Override @DeleteMapping("/api/v1/task/delete/{taskId}/{status}") public void updateDb(@PathVariable("taskId")Long taskId, @PathVariable("status") Integer status) { taskService.UpdateDb(taskId,status); } ③在TaskService中增加UpdateDb(long taskId, int status)方法,将实现类中该方法权限设置为public TaskService /** * 删除数据库任务并更新日志 * @param taskId * @param status * @return */ Task UpdateDb(long taskId, int status); Impl /** * 删除任务,更新日志 * @param taskId * @param status * @return */ public Task UpdateDb(long taskId, int status) { Task task = null; try { //1.删除任务 log.info("删除数据库中的任务..."); taskInfoMapper.deleteById(taskId); //2.更新日志 log.info("更新任务日志..."); TaskinfoLogs taskinfoLogs = taskInfoLogsMapper.selectById(taskId); taskinfoLogs.setStatus(status); taskInfoLogsMapper.updateById(taskinfoLogs); //3.设置返回值 task = new Task(); BeanUtils.copyProperties(taskinfoLogs,task); task.setExecuteTime(taskinfoLogs.getExecuteTime().getTime()); } catch (BeansException e) { throw new RuntimeException(e); } return task; } ④修改taskServiceImpl中的poll方法 /** * 消费任务 * @param type 任务类型 * @param priority 任务优先级 * @return Task */ @Override public Task poll(int type, int priority) { Task task = null; try { String key = type + "_" + priority; String task_json = cacheService.lRightPop(ScheduleConstants.TOPIC + key); if(StringUtils.isNotBlank(task_json)) { task = JSON.parseObject(task_json,Task.class); //更新数据库 (抛弃该策略) // UpdateDb(task.getTaskId(),ScheduleConstants.EXECUTED); //接口幂等性 TaskinfoLogs taskinfoLogs = taskInfoLogsMapper.selectById(task.getTaskId()); //获取任务状态 if(taskinfoLogs != null) { Integer status = taskinfoLogs.getStatus(); if(ScheduleConstants.EXECUTED == status) { return null; } } } } catch (Exception e) { e.printStackTrace(); log.error("poll task exception"); } return task; } 这里主要修改了两点: 抛弃原来直接调用方法删除数据库中的任务的策略 增加接口幂等性,假如该任务已经被成功执行但是并没有在数据库中删除该任务,那么第二次执行该任务时候假如判断到该任务已经执行过则直接返回null不做处理。 ⑤修改自动审核方法 @Autowired private IScheduleClient scheduleClient; /** * 自动审核文章文本及图片 * @param id */ @Override @Async public void AutoScanTextAndImage(Integer id,Long taskId) throws Exception { log.info("开始进行文章审核..."); // Thread.sleep(300); //休眠300毫秒,以保证能够获取到数据库中的数据 WmNews wmNews = wmNewsService.getById(id); if(wmNews == null) { throw new RuntimeException("WmAutoScanServiceImpl-文章信息不存在"); } if(wmNews.getStatus().equals(WmNews.Status.SUBMIT.getCode())) { //1.提取文章文本及图片 Map map = getTextAndImages(wmNews); //2.检测文本 //2.1提取文本 String content = ((StringBuilder) map.get("content")).toString(); //2.2自管理敏感词过滤 boolean SHandleResult = handleSensitiveScan(content,wmNews); if(!SHandleResult) return; //2.3调用腾讯云进行文本检测 Boolean THandleResult = handleTextScan(content, wmNews); if(!THandleResult) return; //3.检测图片 //3.1提取图片 List imageUrl = (List) map.get("images"); //3.2调用腾讯云对图片进行检测 Boolean IHandleResult = handleImageScan(imageUrl, wmNews); if(!IHandleResult) return; //4,审核成功 //4.1保存文章 log.info("检测到文章无违规内容"); ResponseResult responseResult = saveAppArticle(wmNews); if(!responseResult.getCode().equals(200)) { throw new RuntimeException("AutoScanTextAndImage--检测失败"); } //4.2回填article_id wmNews.setArticleId((Long) responseResult.getData()); wmNews.setStatus(WmNews.Status.PUBLISHED.getCode()); wmNews.setReason("审核成功"); wmNewsService.updateById(wmNews); //删除数据库中的任务并更新日志 scheduleClient.updateDb(taskId, ScheduleConstants.EXECUTED); } } ———————————————— 版权声明:本文为CSDN博主「赵四司机」的原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接及本声明。 原文链接:https://blog.csdn.net/weixin_45750572/article/details/126453285
-
什么是数据?1.所有能被输入到计算机中,且能被计算机处理的符号的总称。如:实数、整数、字符(串)、图形和声音等。2.是计算机操作对象的集合。3.是计算机处理的信息的某种特定的符号表示形式。算法的五种性质1.有穷性2.确定性3.有效性4.输入5.输出算法设计的目标1.正确性2.可读性3.健壮性4.高效率(时间与空间)算法的描述方式:1.自然语言2.程序设计语言3.伪代码4.流程图多项式时间算法的时间复杂度有哪些形式?1.常量阶:O(1)2.线性阶:O(n)3.平方阶:O(n2)4.立方阶5.对数阶6.线性对数阶按照数据元素之间逻辑关系的特性可分为哪几类(作简要说明)?1.集合集合中数据元素之间除了“同属于一个集合”的特性外,数据元素之间无其他关系,它们之间的关系称为是松散性的2.线性结构线性结构中数据元素之间存在“一对一”的关系3.树形结构树形结构中数据元素之间存在“一对多”的关系4.图形结构图形结构中数据元素之间存在“多对多”的关系列举几个常见的链表(至少三个)?1.单链表1.循环链表2.双向链表3.双向循环链表顺序表与链表的比较1.链表比较灵活,插入和删除操作效率较高,但空间利用率低,适用于实现动态的线性表;2.顺序表实现比较简单,并且空间利用率也较高,可高效的进行随机存取,但顺序表不易扩充,插入和删除操作效率较低,适合于实现相对“稳定”的静态线性表。静态查找与动态查找分别是什么?静态查找表:查找表的操作不包含对表的修改操作。也就是仅对查找表进行查找或读表元操作。动态查找表:若在查找的同时插入了表中不存在的记录,或从查找表中删除了已存在的记录。动态表查找有什么特点?表结构本身是在查找过程中动态生成的,即对于给定值key,若表中存在关键字值等于key的记录,则查找成功返回;否则插入关键字值等于key的记录。什么是二叉排序树?二叉排序树或者是一棵空树,或者是一颗具有下列性质的二叉树:① .若左子树不空,则左子树上所有结点的值均小于根结点的值;② .若右子树不空,则右子树上所有结点的值均大于根结点的值;③ .它的左右子树也都是二叉排序树简述什么是结点的平衡因子。结点的平衡因子:该结点的左子树深度与右子树深度之差,又称为平衡度。① .平衡二叉树也就是树中任意结点的平衡因子的绝对值小于等于1的二叉树。② .在AVL树中的结点平衡因子可能有3种取值:-1、0、1在哈希表查找中,对数字的关键字常用的哈希函数有哪些(不少于5个)1. 除留余数法2. 平方取中法3. 直接定址法4. 折叠法5. 数字分析法6. 随机数法在哈希表查找中,常用的处理哈希冲突的方法有哪些(不少于3个)1. 开放定址法2. 链地址法3. 公共溢出区法4. 再哈希法computed 和 watch 的区别computed: 是计算属性,依赖其它属性值,并且 computed 的值有缓存,只有它依赖的属性值发生改变,下一次获取 computed 的值时才会重新计算 computed 的值;watch: 更多的是“观察”的作用,类似于某些数据的监听回调,每当监听的数据变化时都会执行回调进行后续操作;vue-router 路由模式有几种?vue-router 有 3 种路由模式:hash、history、abstract各模式的说明如下:hash: 使用 URL hash 值来作路由。支持所有浏览器,包括不支持 HTML5 History Api 的浏览器;history : 依赖 HTML5 History API 和服务器配置。具体可以查看 HTML5 History 模式;abstract : 支持所有 JavaScript 运行环境,如 Node.js 服务器端。如果发现没有浏览器的 API,路由会自动强制进入这个模式.vue中为什么data是一个函数组件的data写成一个函数,数据以函数返回值形式定义,这样每复用一次组件,就会返回一份新的data,类似于给每个组件实例创建一个私有的数据空间,让各个组件实例维护各自的数据。如果单纯的写成对象形式,就使得所有组件实例共用了一份data,这样一个实例中更新数据会导致其他实例数据改变。v-if 和 v-show 的区别v-if 在编译过程中会被转化成三元表达式,条件不满足时不渲染此节点。v-show 会被编译成指令,条件不满足时控制样式将此节点隐藏请列举几个vue内部指令,并说明其作用(至少五个)1. v-bind:绑定属性,动态更新HTML元素上的属性。例如 v-bind:class;2. v-on:用于监听DOM事件。例如 v-on:click v-on:keyup;3. v-text:更新元素的textContent;4. v-model:用来在 input、select、textarea、checkbox、radio 等表单控件元素上创建双向数据绑定,根据表单上的值,自动更新绑定的元素的值;5. v-for:循环指令编译出来的结果是 -L 代表渲染列表。优先级比v-if高最好不要一起使用,尽量使用计算属性去解决;6. v-show:使用指令来实现 -- 最终会通过display来进行显示隐藏;你建不建议v-if和v-for一起使用?为什么?v-for和v-if不要在同一标签中使用,因为解析时先解析v-for在解析v-if。如果遇到需要同时使用时可以考虑写成计算属性的方式。v-for为什么要加keyv-for遍历时,key是Vue中vnode的唯一标记,通过这个 key,我们的 diff 操作可以更准确、更快速。更准确是因为带 key时,在sameNode函数进行key值对比中可以避免就地复用的情况。所以会更加准确。更快速是利用 key 的唯一性生成 map 对象来获取对应节点,比遍历方式更快什么是微服务框架?微服务架构就是将单体的应程序分成多个应程序,这多个应程序就成为微服务,每个微服务运行在自己的进程中,并使用轻量级的通信机制。这些服务围绕业务能力来划分,并通过自动化机制来独立部署。汶些服务可以使用不同的编程语言,不同数据库,以保证最低限度的集中式管理Spring Cloud是什么Spring Cloud是一系列框架的有序集合,它利用Spring Boot的开发便利性巧妙的简化了分布式系统基础设施的开发,如服务发现、配置中心、智能路由、消息总线、负载均衡等,都可以用Spring Boot的开发风格来做到一键部署和启动。Spring Cloud将各家公司开发的比较成熟的服务框架组合起来,通过Spring Boot风格进行再封装,屏蔽掉了复杂的配置和实现原理,最终给开发者一套易懂、易部署和易维护的分布式系统开发工具包。Spring Cloud和Spring Boot的区别Spring Boot专注于快速方便的开发单个个体微服务。Spring Cloud关注全局,它将Spring Boot开发的单体微服务整合并管理起来Spring Cloud为各个微服务之间提供配置管理、服务发现、路由、熔断、降级等等集成服务。Spring Boot可以离开Spring Cloud独立使用开发项目,Spring Cloud离不开Spring Boot,属于依赖关系Spring Boot专注于微服务个体,Spring Cloud关注全局的微服务治理SpringCloud Alibaba有哪些主要功能?分布式配置:分布式系统的外部配置管理,配置中心可视化、分环境配置控制。配置动态更新能力。服务注册与发现:适配SpringCloud标准的服务注册与服务发现管理。服务限流与降级:可通过控制台进行实时的修改限流降级的规则,实时的Metrics监控。支持多种协议消息驱动:基于RocketMQ实现消息驱动的业务场景开发。分布式事务:开源Seata使用@GlobalTransactional注解,零侵入的实现分布式事务的支持。SpringCloud Alibaba核心组件有哪些?Nacos (配置中心与服务注册与发现)Sentinel (分布式流控)RocketMQ (消息队列)Seata (分布式事务)Dubbo (RPC)Nacos是什么,他有哪些关键特性?Nacos用于服务的注册发现与服务的配置管理,它提供了简单易用的Web Console。可以帮助开发者快速的实现服务发现、服务配置管理、服务元数据等需求它的关键特性主要有:服务发现和服务健康监测动态配置服务动态 DNS 服务服务及其元数据管理Sentinel是什么,它有什么作用?Sentinel是一个高可用的流量控制与防护组件,保障微服务的稳定性。Sentinel分为两个部分,sentinel-core与sentinel-dashboard。sentinel-core 部分能够支持在本地引入sentinel-core进行限流规则的整合与配置。sentinel-dashboard 则在core之上能够支持在线的流控规则与熔断规则的维护与调整等。熔断和降级的区别?服务降级有很多种降级方式!如开关降级、限流降级、熔断降级!服务熔断属于降级方式的一种!当发生下游服务不可用的情况,熔断和降级必定是一起出现。服务降级大多是属于一种业务级别的处理,熔断属于框架层级的实现什么是Feign?Feign是一个声明web服务客户端,这使得编写web服务客户端更容易Feign将我们需要调用的服务方法定义成抽象方法保存在本地就可以了,不需要自己构建Http请求,直接调用接口即可,不过要注意的是,调用的方法要和本地抽象方法的签名完全一致。Spring Cloud Gateway是什么,它有什么作用?Spring Cloud Gateway是Spring Cloud官方推出的第二代网关框架,取代Zuul网关。网关作为流量控制组件,在微服务系统中有着非常重要的作用,网关常见的功能有转发、权限校验、限流控制等作用什么是Redis,它有哪些基本数据类型?Redis是用C语言开发的一个开源的高性能键值对(key-value)数据库。它通过提供多种键值数据类型来适应不同场景下的存储需求,Redis支持的基本数据类型如下:字符串类型 string散列类型 hash列表类型 list集合类型 set有序集合类型 sortedset有哪些场景可以使用 RabbitMQ?服务间异步通信顺序消费定时任务请求削峰RabbitMQ有哪几种常见的工作模式?Work queuesPublish/Subscribe:发布订阅模式Routing:路由模式TopicsHeaderRPCMQ的优点有哪些?异步处理 - 相比于传统的串行、并行方式,提高了系统吞吐量。应用解耦 - 系统间通过消息通信,不用关心其他系统的处理。流量削锋 - 可以通过消息队列长度控制请求量;可以缓解短时间内的高并发请求。日志处理 - 解决大量日志传输。消息通讯 - 消息队列一般都内置了高效的通信机制,因此也可以用在纯的消息通讯。————————————————版权声明:本文为CSDN博主「陶然同学」的原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接及本声明。原文链接:https://blog.csdn.net/weixin_45481821/article/details/126163199
-
释介绍redis与Spring Cache的整合请参看上章@Cacheable@Cacheable 的作用 主要针对方法配置,能够根据方法的请求参数对其结果进行缓存@Cacheable 作用和配置方法参数 解释 examplevalue 缓存的名称,在 spring 配置文件中定义,必须指定至少一个 例如:@Cacheable(value=”mycache”)@Cacheable(value={”cache1”,”cache2”}key 缓存的 key,可以为空,如果指定要按照 SpEL 表达式编写,如果不指定,则缺省按照方法的所有参数进行组合 @Cacheable(value=”testcache”,key=”#userName”)condition 缓存的条件,可以为空,使用 SpEL 编写,返回 true 或者 false,只有为 true 才进行缓存 @Cacheable(value=”testcache”,condition=”#userName.length()>2”)实例@Cacheable(value=”accountCache”),这个注释的意思是,当调用这个方法的时候,会从一个名叫 accountCache 的缓存中查询,如果没有,则执行实际的方法(即查询数据库),并将执行的结果存入缓存中,否则返回缓存中的对象。这里的缓存中的 key 就是参数 userName,value 就是 Account 对象。“accountCache”缓存是在 spring*.xml 中定义的名称。@Cacheable(value="accountCache")// 使用了一个缓存名叫 accountCache public Account getAccountByName(String userName) { // 方法内部实现不考虑缓存逻辑,直接实现业务 System.out.println("real query account."+userName); return getFromDB(userName); } @CachePut@CachePut 的作用 主要针对方法配置,能够根据方法的请求参数对其结果进行缓存,和 @Cacheable 不同的是,它每次都会触发真实方法的调用@CachePut 作用和配置方法参数 解释 examplevalue 缓存的名称,在 spring 配置文件中定义,必须指定至少一个 @CachePut(value=”my cache”)key 缓存的 key,可以为空,如果指定要按照 SpEL 表达式编写,如果不指定,则缺省按照方法的所有参数进行组合 @CachePut(value=”testcache”,key=”#userName”)condition 缓存的条件,可以为空,使用 SpEL 编写,返回 true 或者 false,只有为 true 才进行缓存 @CachePut(value=”testcache”,condition=”#userName.length()>2”)实例@CachePut 注释,这个注释可以确保方法被执行,同时方法的返回值也被记录到缓存中,实现缓存与数据库的同步更新。@CachePut(value="accountCache",key="#account.getName()")// 更新accountCache 缓存public Account updateAccount(Account account) { return updateDB(account); } @CacheEvict@CachEvict 的作用 主要针对方法配置,能够根据一定的条件对缓存进行清空@CacheEvict 作用和配置方法参数 解释 examplevalue 缓存的名称,在 spring 配置文件中定义,必须指定至少一个 @CacheEvict(value=”my cache”)key 缓存的 key,可以为空,如果指定要按照 SpEL 表达式编写,如果不指定,则缺省按照方法的所有参数进行组合 @CacheEvict(value=”testcache”,key=”#userName”)condition 缓存的条件,可以为空,使用 SpEL 编写,返回 true 或者 false,只有为 true 才进行缓存 @CacheEvict(value=”testcache”,condition=”#userName.length()>2”)allEntries 是否清空所有缓存内容,缺省为 false,如果指定为 true,则方法调用后将立即清空所有缓存 @CachEvict(value=”testcache”,allEntries=true)beforeInvocation 是否在方法执行前就清空,缺省为 false,如果指定为 true,则在方法还没有执行的时候就清空缓存,缺省情况下,如果方法执行抛出异常,则不会清空缓存 @CachEvict(value=”testcache”,beforeInvocation=true)实例@CacheEvict(value="accountCache",key="#account.getName()")// 清空accountCache 缓存 public void updateAccount(Account account) { updateDB(account); } @CacheEvict(value="accountCache",allEntries=true)// 清空accountCache 缓存public void reload() { reloadAll()}@Cacheable(value="accountCache",condition="#userName.length() <=4")// 缓存名叫 accountCache public Account getAccountByName(String userName) { // 方法内部实现不考虑缓存逻辑,直接实现业务 return getFromDB(userName); }@CacheConfig所有的@Cacheable()里面都有一个value=“xxx”的属性,这显然如果方法多了,写起来也是挺累的,如果可以一次性声明完 那就省事了,所以,有了@CacheConfig这个配置,@CacheConfig is a class-level annotation that allows to share the cache names,如果你在你的方法写别的名字,那么依然以方法的名字为准。@CacheConfig("books")public class BookRepositoryImpl implements BookRepository { @Cacheable public Book findBook(ISBN isbn) {...}}条件缓存下面提供一些常用的条件缓存//@Cacheable将在执行方法之前( #result还拿不到返回值)判断condition,如果返回true,则查缓存; @Cacheable(value = "user", key = "#id", condition = "#id lt 10")public User conditionFindById(final Long id) //@CachePut将在执行完方法后(#result就能拿到返回值了)判断condition,如果返回true,则放入缓存; @CachePut(value = "user", key = "#id", condition = "#result.username ne 'zhang'") public User conditionSave(final User user) //@CachePut将在执行完方法后(#result就能拿到返回值了)判断unless,如果返回false,则放入缓存;(即跟condition相反)@CachePut(value = "user", key = "#user.id", unless = "#result.username eq 'zhang'")public User conditionSave2(final User user) //@CacheEvict, beforeInvocation=false表示在方法执行之后调用(#result能拿到返回值了);且判断condition,如果返回true,则移除缓存;@CacheEvict(value = "user", key = "#user.id", beforeInvocation = false, condition = "#result.username ne 'zhang'") public User conditionDelete(final User user) @Caching有时候我们可能组合多个Cache注解使用;比如用户新增成功后,我们要添加id–>user;username—>user;email—>user的缓存;此时就需要@Caching组合多个注解标签了。@Caching(put = {@CachePut(value = "user", key = "#user.id"),@CachePut(value = "user", key = "#user.username"),@CachePut(value = "user", key = "#user.email")})public User save(User user) {自定义缓存注解比如之前的那个@Caching组合,会让方法上的注解显得整个代码比较乱,此时可以使用自定义注解把这些注解组合到一个注解中,如:@Caching(put = {@CachePut(value = "user", key = "#user.id"),@CachePut(value = "user", key = "#user.username"),@CachePut(value = "user", key = "#user.email")})@Target({ElementType.METHOD, ElementType.TYPE})@Retention(RetentionPolicy.RUNTIME)@Inheritedpublic @interface UserSaveCache {}这样我们在方法上使用如下代码即可,整个代码显得比较干净。@UserSaveCachepublic User save(User user)12扩展比如findByUsername时,不应该只放username–>user,应该连同id—>user和email—>user一起放入;这样下次如果按照id查找直接从缓存中就命中了@Caching( cacheable = { @Cacheable(value = "user", key = "#username") }, put = { @CachePut(value = "user", key = "#result.id", condition = "#result != null"), @CachePut(value = "user", key = "#result.email", condition = "#result != null") })public User findByUsername(final String username) { System.out.println("cache miss, invoke find by username, username:" + username); for (User user : users) { if (user.getUsername().equals(username)) { return user; } } return null;}其实对于:id—>user;username—->user;email—>user;更好的方式可能是:id—>user;username—>id;email—>id;保证user只存一份;如:@CachePut(value="cacheName", key="#user.username", cacheValue="#user.username") public void save(User user) @Cacheable(value="cacheName", key="#user.username", cacheValue="#caches[0].get(#caches[0].get(#username).get())") public User findByUsername(String username) SpEL上下文数据Spring Cache提供了一些供我们使用的SpEL上下文数据,下表直接摘自Spring官方文档:名称 位置 描述 示例methodName root对象 当前被调用的方法名 root.methodNamemethod root对象 当前被调用的方法 root.method.nametarget root对象 当前被调用的目标对象 root.targettargetClass root对象 当前被调用的目标对象类 root.targetClassargs root对象 当前被调用的方法的参数列表 root.args[0]caches root对象 当前方法调用使用的缓存列表(如@Cacheable(value={“cache1”, “cache2”})),则有两个cache root.caches[0].nameargument name 执行上下文 当前被调用的方法的参数,如findById(Long id),我们可以通过#id拿到参数 user.idresult 执行上下文 方法执行后的返回值(仅当方法执行之后的判断有效,如‘unless’,’cache evict’的beforeInvocation=false) result@CacheEvict(value = "user", key = "#user.id", condition = "#root.target.canCache() and #root.caches[0].get(#user.id).get().username ne #user.username", beforeInvocation = true) public void conditionUpdate(User user) ————————————————原文链接:https://blog.csdn.net/whatlookingfor/article/details/51833378/
-
Spring Boot 提供了 RestTemplate 来辅助发起一个 REST 请求,默认通过 JDK 自带的 HttpURLConnection 来作为底层 HTTP 消息的发送方式,使用 JackSon 来序列化服务器返回的 JSON 数据。RestTemplate 是核心类, 提供了所有访问 REST 服务的接口,尽管实际上可以使用 HTTP Client 类或者 java.net.URL来完成,但 RestTemplate 提供了阻STful 风格的 API。 Spring Boot 提供了 RestTemplateBuilder 来创建一个 RestTemplate。 RestTemplate定义11个基本操作方法,大致如下:delete(): 在特定的URL上对资源执行HTTP DELETE操作exchange(): 在URL上执行特定的HTTP方法,返回包含对象的ResponseEntity,这个对象是从响应体中映射得到的 3.execute(): 在URL上执行特定的HTTP方法,返回一个从响应体映射得到的对象(所有的get、post、delete、put、options、head、exchange方法最终调用的都是excute方法),例如:@Override public <T> T getForObject(String url, Class<T> responseType, Object... urlVariables) throws RestClientException { RequestCallback requestCallback = acceptHeaderRequestCallback(responseType); HttpMessageConverterExtractor<T> responseExtractor = <span style="white-space:pre"> </span>new HttpMessageConverterExtractor<T>(responseType, getMessageConverters(), logger); return execute(url, HttpMethod.GET, requestCallback, responseExtractor, urlVariables); } 4.getForEntity(): 发送一个HTTP GET请求,返回的ResponseEntity包含了响应体所映射成的对象 5.getForObject() :发送一个HTTP GET请求,返回的请求体将映射为一个对象 6.postForEntity() :POST 数据到一个URL,返回包含一个对象的ResponseEntity,这个对象是从响应体中映射得到的 7.postForObject(): POST 数据到一个URL,返回根据响应体匹配形成的对象 8.headForHeaders(): 发送HTTP HEAD请求,返回包含特定资源URL的HTTP头 9.optionsForAllow(): 发送HTTP OPTIONS请求,返回对特定URL的Allow头信息 10.postForLocation() :POST 数据到一个URL,返回新创建资源的URL 11.put(): PUT 资源到特定的URL实际上,由于Post 操作的非幂等性,它几乎可以代替其他的CRUD操作.一、GET请求在RestTemplate中,发送一个GET请求,我们可以通过如下两种方式:第一种:getForEntitygetForEntity方法的返回值是一个ResponseEntity<T>,ResponseEntity<T>是Spring对HTTP请求响应的封装,包括了几个重要的元素,如响应码、contentType、contentLength、响应消息体等。其重载方法如下:<T> ResponseEntity<T> getForObject(URI url, Class<T> responseType) throws RestClientException;<T> ResponseEntity<T> getForObject(String url, Class<T> responseType, Object... uriVariables) throws RestClientException;<T> ResponseEntity<T> getForObject(String url, Class<T> responseType, Map<String, ?> uriVariables) throws RestClientException;比如下面一个例子:@Autowired RestTemplateBuilder restTemplateBuilder; @RequestMapping("/gethello") public String getHello() { RestTemplate client = restTemplateBuilder.build (); ResponseEntity<String> responseEntity = restTemplate.getForEntity("http://HELLO-SERVICE/hello", String.class); String body = responseEntity.getBody(); HttpStatus statusCode = responseEntity.getStatusCode(); int statusCodeValue = responseEntity.getStatusCodeValue(); HttpHeaders headers = responseEntity.getHeaders(); StringBuffer result = new StringBuffer(); result.append("responseEntity.getBody():").append(body).append("<hr>") .append("responseEntity.getStatusCode():").append(statusCode).append("<hr>") .append("responseEntity.getStatusCodeValue():").append(statusCodeValue).append("<hr>") .append("responseEntity.getHeaders():").append(headers).append("<hr>"); return result.toString(); }有时候我在调用服务提供者提供的接口时,可能需要传递参数,有两种不同的方式,如下:@RequestMapping("/sayhello") public String sayHello() { ResponseEntity<String> responseEntity = restTemplate.getForEntity("http://HELLO-SERVICE/sayhello?name={1}", String.class, "张三"); return responseEntity.getBody(); } @RequestMapping("/sayhello2") public String sayHello2() { Map<String, String> map = new HashMap<>(); map.put("name", "李四"); ResponseEntity<String> responseEntity = restTemplate.getForEntity("http://HELLO-SERVICE/sayhello?name={name}", String.class, map); return responseEntity.getBody(); }可以用一个数字做占位符,最后是一个可变长度的参数,来一一替换前面的占位符也可以前面使用name={name}这种形式,最后一个参数是一个map,map的key即为前边占位符的名字,map的value为参数值第一个调用地址也可以是一个URI而不是字符串,这个时候我们构建一个URI即可,参数神马的都包含在URI中了,如下:@RequestMapping("/sayhello3") public String sayHello3() { UriComponents uriComponents = UriComponentsBuilder.fromUriString("http://HELLO-SERVICE/sayhello?name={name}").build().expand("王五").encode(); URI uri = uriComponents.toUri(); ResponseEntity<String> responseEntity = restTemplate.getForEntity(uri, String.class); return responseEntity.getBody(); }通过Spring中提供的UriComponents来构建Uri即可。当然,服务提供者不仅可以返回String,也可以返回一个自定义类型的对象,比如我的服务提供者中有如下方法:@RequestMapping(value = "/getbook1", method = RequestMethod.GET)public Book book1() { return new Book("三国演义", 90, "罗贯中", "花城出版社"); }对于该方法我可以在服务消费者中通过如下方式来调用:@RequestMapping("/book1") public Book book1() { ResponseEntity<Book> responseEntity = restTemplate.getForEntity("http://HELLO- SERVICE/getbook1", Book.class); return responseEntity.getBody(); }第二种:getForObjectgetForObject函数实际上是对getForEntity函数的进一步封装,如果你只关注返回的消息体的内容,对其他信息都不关注,此时可以使用getForObject,重载方法如下:<T> T getForObject(URI url, Class<T> responseType) throws RestClientException;<T> T getForObject(String url, Class<T> responseType, Object... uriVariables) throws RestClientException;<T> T getForObject(String url, Class<T> responseType, Map<String, ?> uriVariables) throws RestClientException; 二、POST请求在RestTemplate中,POST请求可以通过如下三个方法来发起:第一种:postForEntity、postForObjectPOST请求有postForObject()和postForEntity()两种方法,和GET请求的getForObject()和getForEntity()方法类似。getForLocation()是POST请求所特有的。<T> T postForObject(URI url, Object request, Class<T> responseType) throws RestClientException;<T> T postForObject(String url, Object request, Class<T> responseType, Object... uriVariables) throws RestClientException;<T> T postForObject(String url, Object request, Class<T> responseType, Map<String, ?> uriVariables) throws RestClientException;上面三个方法中,第一个参数都是资源要POST到的URL,第二个参数是要发送的对象,而第三个参数是预期返回的Java类型。在URL作为String类型的两个版本中,第四个参数指定了URL变量(要么是可变参数列表,要么是一个Map)。 该方法和get请求中的getForEntity方法类似,如下例子:@RequestMapping("/book3") public Book book3() { Book book = new Book(); book.setName("红楼梦"); ResponseEntity<Book> responseEntity = restTemplate.postForEntity("http://HELLO-SERVICE/getbook2", book, Book.class); return responseEntity.getBody(); } 第二种:postForLacationpostForLacation()会在POST请求的请求体中发送一个资源到服务器端,返回的不再是资源对象,而是创建资源的位置。postForLocation(String url, Object request, Object... uriVariables) throws RestClientException;postForLocation(String url, Object request, Map<String, ?> uriVariables) throws RestClientException;postForLocation(URI url, Object request) throws RestClientException; public String postSpitter(Spitter spitter) { RestTemplate rest = new RestTemplate(); return rest.postForLocation("http://localhost:8080/Spitter/spitters", spitter).toString(); }postForLocation也是提交新资源,提交成功之后,返回新资源的URI,postForLocation的参数和前面两种的参数基本一致,只不过该方法的返回值为Uri,这个只需要服务提供者返回一个Uri即可,该Uri表示新资源的位置。 三、PUT请求在RestTemplate中,对PUT请求可以通过put方法进行调用实现,比如:RestTemplate restTemplate=new RestTemplate();Longid=100011; User user=new User("didi",40); restTemplate.put("http://USER-SERVICE/user/{l}",user,id); put函数也实现了三种不同的重载方法:put(String url,Object request,Object... urlVariables)put(String url,Object request,Map urlVariables)put(URI url,Object request)put函数为void类型,所以没有返回内容,也就没有其他函数定义的responseType参数,除此之外的其他传入参数定义与用法与postForObject基本一致。 四、DELETE请求在RestTemplate中,对DELETE请求可以通过delete方法进行调用实现,比如:RestTemplate restTemplate=new RestTemplate(); Longid=10001L; restTemplate.delete("http://USER-SERVICE/user/{1)",id); delete函数也实现了三种不同的重载方法:delete(String url,Object... urlVariables)delete(String url,Map urlVariables)delete(URI url)由于我们在进行REST请求时,通常都将DELETE请求的唯一标识拼接在url中,所以DELETE请求也不需要request的body信息,就如put()方法一样,返回值类型为void。说明:第三种重载方法,url指定DELETE请求的位置,urlVariables绑定url中的参数即可。五、通用方法exchange()exchange方法可以在发送个服务器端的请求中设置头信息,其重载方法如下:<T> ResponseEntity<T> exchange(URI url, HttpMethod method, HttpEntity<?> requestEntity, Class<T> responseType) throws RestClientException;<T> ResponseEntity<T> exchange(String url, HttpMethod method, HttpEntity<?> requestEntity, Class<T> responseType, Object... uriVariables) throws RestClientException;<T> ResponseEntity<T> exchange(String url, HttpMethod method, HttpEntity<?> requestEntity, Class<T> responseType, Map<String, ?> uriVariables) throws RestClientException;MultiValueMap<String, String> headers = new LinkedMultiValueMap<String, String>(); headers.add("Accept", "application/json"); HttpEntity<Object> requestEntity = new HttpEntity<Object>(headers); ResponseEntity<Spitter> response=rest.exchange("http://localhost:8080/Spitter/spitters/{spitter}", HttpMethod.GET, requestEntity, Spitter.class, spitterId); 补充: 如果期望返回的类型是一个列表,如 List,不能简单调用 xxxForObject,因为存在泛型的类型擦除, RestTemplate 在反序列化的时候并不知道实际反序列化的类型,因此可以使用 ParameterizedTypeReference 来包含泛型类型,代码如下:RestTemplate client= restTemplateBuilder.build(); //根据条件查询一纽订单 String uri = base+"/orders?offset={offset }"; Integer offset = 1; //元参数 HttpEntity body = null; ParameterizedTypeReference<List<Order> typeRef = new ParameterizedTypeReference<List<Order>(){}; ResponseEntity<List<Order> rs=client.exchange(uri, HttpMethod.GET, body, typeRef, offset); List<Order> order = rs.getBody() ; 注意到 typeRef 定义是用{}结束的,这里创建了一个 ParameterizedTypeReference 子类,依 据在类定义中的泛型信息保留的原则, typeRef保留了期望返回的泛型 List。exchange 是一个基础的 REST 调用接口,除了需要指明 HTTP Method,调用方法同其他方法类似。除了使用ParameterizedTypeReference子类对象,也可以先将返回结果映射成 json字符串,然后通过 ObjectMapper 来转为指定类型(笔记<SpringBoot中的JSON>中有介绍 ) 。————————————————原文链接:https://blog.csdn.net/fsy9595887/article/details/86420048
-
【摘要】 Sermant Agent是一种基于JavaAgent的无代理服务网格技术。它利用JavaAgent来检测主机应用程序,并具有增强的服务治理功能,以解决海量微服务架构中的服务治理问题。本文介绍了Sermant Agent的接入原理和如何使用Sermant Agent无修改接入CSE。本文分享自华为云社区《Spring Cloud应用零代码修改接入华为云微服务引擎CSE》,作者: 微服务小助手 。一、 Sermant Agent介绍Sermant Agent是一种基于JavaAgent的无代理服务网格技术。它利用JavaAgent来检测主机应用程序,并具有增强的服务治理功能,以解决海量微服务架构中的服务治理问题。Sermant Agent处于快速发展阶段,当前已支持多种服务治理能力,包含流量治理、注册、优雅上下线及动态配置能力。 二、 为何使用Sermant Agent接入代码零侵入,配置很简单相较于SDK方式接入,基于Sermant Agent的接入会更加快捷高效,配置简单,且应用无需做任何代码改造,仅需在服务启动时附带Sermant Agent即可动态接入到CSE。支持多种治理能力Sermant Agent默认集成流量治理能力,当前支持熔断、限流、隔离仓以及重试治理能力,该能力可基于CSE配置中心进行配置与发布。支持多种注册中心Sermant Agent目前支持业内主流的注册中心,已经支持了ServiceComb ServiceCenter、Naocs,Eureka、Zookeeper等正在开发中。支持应用不停机迁移Sermant Agent支持服务的双注册,可根据配置中心下发的服务订阅策略,动态修改当前服务的订阅策略,并基于该能力帮助线上应用在业务不中断的前提下完成服务迁移。不仅如此,Sermant Agent提供优雅上下线能力,在服务重启、上下线时提供保障,在保护服务的同时,规避服务下线时可能存在的流量丢失问题。三、 接入原理当然,在说明原理之前,我们首先需要了解什么是Java Agent。Java Agent是在JDK1.5之后引入的新特性,它支持JVM将字节码文件读入内存之后,JVM使用对应的字节流在Java堆中生成一个Class对象之前,用户可以对其字节码进行修改的能力,JVM使用修改之后的字节码进行Class对象的创建,从而实现Java应用的非代码侵入的业务逻辑修改和替换。Sermant Agent正是基于动态修改字节码的技术,在服务启动时,动态增强原服务的注册逻辑。那Sermant Agent是如何在不修改代码的前提下接入CSE呢?主要流程如下:Sermant Agent接入CSE的时序图包含以下6个步骤:首先服务携带Sermant Agent启动;服务启动时,针对服务执行字节码增强操作(基于Java Agent的字节码增强),主要针对注册与配置两块,在步骤3-5体现;通过字节码增强,动态识别原应用的注册中心;注入启动配置,动态关闭原应用的注册中心自动配置逻辑;随后通过Spring的SpringFactory机制注入基于Spring Cloud实现的注册CSE的自动配置类,由Spring接管;当应用发起注册时,会通过步骤5注入的注册逻辑向CSE发起注册,最终完成接入。四、 简单零代码修改,轻松接入CSE接入场景分为虚机接入和容器接入,大家可以根据自身需求选择合适的接入方式。虚机场景接入CSE虚机部署的应用可通过Sermant Agent接入到CSE,点击查看虚机接入CSE流程。接入流程基于ECS将应用接入CSE流程如下:容器场景接入CSE容器部署的应用可通过Sermant Injector自动挂载Sermant Agent,从而通过Sermant Agent接入到CSE,点击查看容器接入CSE流程。接入流程基于CCE将应用接入CSE流程如下:五、 更多支持版本当前Sermant已支持大部分业内主流版本,相关Spring及注册中心版本如下:开源方式接入除了上述接入方式,还可基于开源方式接入,您可在Sermant开源社区拉取最新代码,并自行打包,启动步骤可参考虚机接入流程。开源项目Sermant:https://github.com/huaweicloud/Sermant
-
1.Spring Security是怎么鉴权和实现权限认证的呢?一般我们会创建一个Spring Security的配置来进行权限的认证设置、各种拦截策略以及各种自定义的配置。1.1 创建Spring Security配置类只要继承了WebSecurityConfigurerAdapter抽象类并且加上Spring的配置注解@Configuration以及将Bean放到容器中管理的 @EnableWebSecurity注解和@EnableGlobalMethodSecurity(prePostEnabled=true)就可以完成一个Spring Security的配置类的创建,实现全局生效自定义的安全配置。在创建完成后,可以重载里面很多设置配置的方法,如下图所示:1.2 其中有很多public void configure这个的重载方法,用于配置不同的拦截策略。用于根据系统自定义一些拦截策略,provider就实现了AuthenticationProvider接口的自定义配置处理类1.2.1 下面这种AuthenticationManagerBuilder auth参数的是身份验证提供程序是需要自定义配置的。@Override public void configure(AuthenticationManagerBuilder auth) throws Exception { auth.authenticationProvider(provider); }provider是实现了AuthenticationProvider接口的自定义配置处理类,provider自定义类中实现方法public Authentication authenticate(Authentication authentication)中可以设置一些自定义的属性值,也可以从authentication参数中获取登录的用户名和密码。@Override public Authentication authenticate(Authentication authentication) throws AuthenticationException { CustomWebAuthenticationDetails details = (CustomWebAuthenticationDetails) authentication.getDetails(); String username = authentication.getName(); String password = (String) authentication.getCredentials(); String imgCode = details.getImgCode(); ... ... }如果想要配置一些别的参数比如验证码可以通过继承WebAuthenticationDetails类设置自定义的一些属性,可以从请求参数中获取到这些参数,再在上面的安全拦截的配置类中强转为这个自定义的类从中就可以获取我们需要设置的自定义的安全配置了。//比如可以配置验证码 public class CustomWebAuthenticationDetails extends WebAuthenticationDetails { private final String imgCode; public CustomWebAuthenticationDetails(HttpServletRequest request) { super(request); imgCode = request.getParameter("imgCode"); } public String getImgCode() { return imgCode; } }还有一种配置是下面这样的,可以设置内存指定的登录的账号密码,指定角色,不加.passwordEncoder(new MyPasswordEncoder())或者注入该类的Bean就不是以明文的方式进行匹配,会报错,如果加上就需要配置自己myUserService跟数据库配置自定义查询,然后配置自定义的加密解密的配置类MyPasswordEncoder,实现不明文传输 。@Override protected void configure(AuthenticationManagerBuilder auth) throws Exception { auth.inMemoryAuthentication().withUser("admin").password("admin").roles("ADMIN"); auth.inMemoryAuthentication().withUser("demo").password("demo").roles("ADMIN"); auth.inMemoryAuthentication().withUser("testRole").password("testRole").roles("AD"); auth.userDetailsService(myUserService).passwordEncoder(new MyPasswordEncoder()); }其中myUserService是配置自定义的数据库配置 @Component public class MyUserService implements UserDetailsService{ @Override public UserDetails loadUserByUsername(String s) throws UsernameNotFoundException { return null; } } 其中new MyPasswordEncoder()可以配置加密 @Component public class MyPasswordEncoder implements PasswordEncoder { @Override public String encode(CharSequence charSequence) { return charSequence.toString(); } @Override public boolean matches(CharSequence charSequence, String s) { return s.equals(charSequence.toString()); } //private final static String SALT = "这个是自定义的salt"; // @Override // public String encode(CharSequence charSequence) { // // return MD5Util.MD5Encode(charSequence.toString(),SALT); // } // // @Override // public boolean matches(CharSequence rawPassword, String encodedPassword) { // // return MD5Util.isPasswordValid(encodedPassword,rawPassword.toString(),SALT); // } }1.2.2 参数是HttpSecurity http的是主要的接口请求拦截配置这里就是主要的安全拦截配置了,可以在其中配置登录相关的处理,成功和失败的相关响应,配置登录和登出接口的定义(这里配置后可以不用在控制器中再配置相关接口,只需要在拦截器中配置好相关的安全参数的拦截策略就可以,在访问配置登录接口时,会自动进入到自定义的拦截器中进行参数判断)。还有很多其他配置,目前还在探索中。。。 @Override protected void configure(HttpSecurity http) throws Exception { http // 关闭csrf防护 .csrf().disable().headers().frameOptions().disable().and(); http // 登录处理 .formLogin().authenticationDetailsSource(authenticationDetailsSource) // 自定义登陆接受参数名 .usernameParameter("username").passwordParameter("password") // 定义登录页面 .loginPage("/login") // 登录处理接口 .loginProcessingUrl("/admin/login") // 登录成功后处理 .successHandler(new AuthenticationSuccessHandler() { @Override public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException, ServletException { response.setContentType("application/json;charset=utf-8"); PrintWriter writer = response.getWriter(); writer.append("{\"code\": 0,\"msg\": \"ok\"}"); writer.flush(); writer.close(); } }) // 登录失败后处理 .failureHandler(new AuthenticationFailureHandler() { @Override public void onAuthenticationFailure(HttpServletRequest request, HttpServletResponse response, AuthenticationException exception) throws IOException, ServletException { String writerStr = "{\"code\": 1,\"msg\": \"error\"}"; if (exception instanceof MyException) { MyException e = (MyException) exception; RespModel resp = new RespModel(e.getCode(), e.getMsg()); writerStr = JSON.toJSONString(resp); } response.setContentType("application/json;charset=utf-8"); PrintWriter writer = response.getWriter(); writer.append(writerStr); writer.flush(); writer.close(); } }).permitAll().and() // 退出登录 .logout().logoutUrl("/logout").permitAll(); http.authorizeRequests().anyRequest().authenticated().and(); // Session的并发控制,这里设为最多一个,只允许一个用户登录,如果同一个账户两次登录,那么第一个账户将被踢下线 http.sessionManagement().maximumSessions(1).expiredUrl("/login");//自己实现的userDetail要重写EqualsAndHashCode 注解:@EqualsAndHashCode(callSuper = true) // 当一个用户已经认证过了,在另外一个地方重新进行登录认证,spring security可以阻止其再次登录认证,从而保持原来的会话可用性 // 存在一个问题,当用户登陆后,没有退出直接关闭浏览器,则再次打开浏览器时,此时浏览器的session若被删除的话,用户只能等到服务器的session过期后,才能再次登录。 //http.sessionManagement().maximumSessions(1).maxSessionsPreventsLogin(true); }1.2.3 参数是WebSecurity web是配置静态资源可以设置忽略拦截静态资源的路径等一些配置。@Override public void configure(WebSecurity web) throws Exception { web.ignoring().antMatchers("/js/**", "/css/**", "/img/**", "/html/**", "/error"); }以上就是目前探索的一些Spring Secrity的一些安全认证和鉴权策略的实现配置方法,继续探索ing...
-
1、简介 Spring Security是一个能够为基于Spring的企业应用系统提供声明式的安全访问控制解决方案的安全框架。框架中主要涉及两个操作,一个是认证,从名字就可以知道,是为了获得一个标识,确认哪些用户可以访问,哪些用户会被拦截;还有一个是授权,从名字中也很容易明白,授权就是赋予一定的权利,可以去做某一些操作,而在授权之前,框架中首先会判断是否是认证的用户,这就是SpringSecurity安全框架中认证和授权相互配合的操作,用这样的操作来保证系统的安全可靠。2、Spring Security默认登录页面 在创建一个Spring Security安全框架后,会默认带一个登录页面,创建的过程也很简单,只要需要创建web项目的时候引入Spring Security的相关依赖就可以完成创建了。 <!--springsecurity 主要依赖--> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-security</artifactId> </dependency> <dependency> <groupId>org.springframework.security</groupId> <artifactId>spring-security-test</artifactId> </dependency> 在创建好后会有一个默认的登录页面,默认配置用户为user,密码会输出在控制台上,那么问题来了我们什么都没有写,这个登录页面是哪里来的呢?而且还自带账号和密码。 其实这个登录页面是一个默认filter里面直接写入response的,可以在自定义实现的WebSecurityConfigurerAdapter 中有找到这么一个方法,再在其中找到一个 默认配置DefaultLoginPageConfigurer。它会添加两个默认filter,再点进去,其中的loginPageGeneratingFilter中,就会生成登录页面是怎么写的了。private void applyDefaultConfiguration(HttpSecurity http) throws Exception { http.csrf(); http.addFilter(new WebAsyncManagerIntegrationFilter()); http.exceptionHandling(); http.headers(); http.sessionManagement(); http.securityContext(); http.requestCache(); http.anonymous(); http.servletApi(); http.apply(new DefaultLoginPageConfigurer<>()); http.logout(); } public void configure(H http) { AuthenticationEntryPoint authenticationEntryPoint = null; ExceptionHandlingConfigurer<?> exceptionConf = http.getConfigurer(ExceptionHandlingConfigurer.class); if (exceptionConf != null) { authenticationEntryPoint = exceptionConf.getAuthenticationEntryPoint(); } if (this.loginPageGeneratingFilter.isEnabled() && authenticationEntryPoint == null) { this.loginPageGeneratingFilter = postProcess(this.loginPageGeneratingFilter); http.addFilter(this.loginPageGeneratingFilter); http.addFilter(this.logoutPageGeneratingFilter); } }private String generateLoginPageHtml(HttpServletRequest request, boolean loginError, boolean logoutSuccess) { String errorMsg = "Invalid credentials"; if (loginError) { HttpSession session = request.getSession(false); if (session != null) { AuthenticationException ex = (AuthenticationException) session .getAttribute(WebAttributes.AUTHENTICATION_EXCEPTION); errorMsg = (ex != null) ? ex.getMessage() : "Invalid credentials"; } } String contextPath = request.getContextPath(); StringBuilder sb = new StringBuilder(); sb.append("<!DOCTYPE html>\n"); sb.append("<html lang=\"en\">\n"); sb.append(" <head>\n"); sb.append(" <meta charset=\"utf-8\">\n"); sb.append(" <meta name=\"viewport\" content=\"width=device-width, initial-scale=1, shrink-to-fit=no\">\n"); sb.append(" <meta name=\"description\" content=\"\">\n"); sb.append(" <meta name=\"author\" content=\"\">\n"); sb.append(" <title>Please sign in</title>\n"); sb.append(" <link href=\"https://maxcdn.bootstrapcdn.com/bootstrap/4.0.0-beta/css/bootstrap.min.css\" " + "rel=\"stylesheet\" integrity=\"sha384-/Y6pD6FV/Vv2HJnA6t+vslU6fwYXjCFtcEpHbNJ0lyAFsXTsjBbfaDjzALeQsN6M\" crossorigin=\"anonymous\">\n"); sb.append(" <link href=\"https://getbootstrap.com/docs/4.0/examples/signin/signin.css\" " + "rel=\"stylesheet\" crossorigin=\"anonymous\"/>\n"); sb.append(" </head>\n"); sb.append(" <body>\n"); sb.append(" <div class=\"container\">\n"); if (this.formLoginEnabled) { sb.append(" <form class=\"form-signin\" method=\"post\" action=\"" + contextPath + this.authenticationUrl + "\">\n"); sb.append(" <h2 class=\"form-signin-heading\">Please sign in</h2>\n"); sb.append(createError(loginError, errorMsg) + createLogoutSuccess(logoutSuccess) + " <p>\n"); sb.append(" <label for=\"username\" class=\"sr-only\">Username</label>\n"); sb.append(" <input type=\"text\" id=\"username\" name=\"" + this.usernameParameter + "\" class=\"form-control\" placeholder=\"Username\" required autofocus>\n"); sb.append(" </p>\n"); sb.append(" <p>\n"); sb.append(" <label for=\"password\" class=\"sr-only\">Password</label>\n"); sb.append(" <input type=\"password\" id=\"password\" name=\"" + this.passwordParameter + "\" class=\"form-control\" placeholder=\"Password\" required>\n"); sb.append(" </p>\n"); sb.append(createRememberMe(this.rememberMeParameter) + renderHiddenInputs(request)); sb.append(" <button class=\"btn btn-lg btn-primary btn-block\" type=\"submit\">Sign in</button>\n"); sb.append(" </form>\n"); } if (this.openIdEnabled) { sb.append(" <form name=\"oidf\" class=\"form-signin\" method=\"post\" action=\"" + contextPath + this.openIDauthenticationUrl + "\">\n"); sb.append(" <h2 class=\"form-signin-heading\">Login with OpenID Identity</h2>\n"); sb.append(createError(loginError, errorMsg) + createLogoutSuccess(logoutSuccess) + " <p>\n"); sb.append(" <label for=\"username\" class=\"sr-only\">Identity</label>\n"); sb.append(" <input type=\"text\" id=\"username\" name=\"" + this.openIDusernameParameter + "\" class=\"form-control\" placeholder=\"Username\" required autofocus>\n"); sb.append(" </p>\n"); sb.append(createRememberMe(this.openIDrememberMeParameter) + renderHiddenInputs(request)); sb.append(" <button class=\"btn btn-lg btn-primary btn-block\" type=\"submit\">Sign in</button>\n"); sb.append(" </form>\n"); } if (this.oauth2LoginEnabled) { sb.append("<h2 class=\"form-signin-heading\">Login with OAuth 2.0</h2>"); sb.append(createError(loginError, errorMsg)); sb.append(createLogoutSuccess(logoutSuccess)); sb.append("<table class=\"table table-striped\">\n"); for (Map.Entry<String, String> clientAuthenticationUrlToClientName : this.oauth2AuthenticationUrlToClientName .entrySet()) { sb.append(" <tr><td>"); String url = clientAuthenticationUrlToClientName.getKey(); sb.append("<a href=\"").append(contextPath).append(url).append("\">"); String clientName = HtmlUtils.htmlEscape(clientAuthenticationUrlToClientName.getValue()); sb.append(clientName); sb.append("</a>"); sb.append("</td></tr>\n"); } sb.append("</table>\n"); } if (this.saml2LoginEnabled) { sb.append("<h2 class=\"form-signin-heading\">Login with SAML 2.0</h2>"); sb.append(createError(loginError, errorMsg)); sb.append(createLogoutSuccess(logoutSuccess)); sb.append("<table class=\"table table-striped\">\n"); for (Map.Entry<String, String> relyingPartyUrlToName : this.saml2AuthenticationUrlToProviderName .entrySet()) { sb.append(" <tr><td>"); String url = relyingPartyUrlToName.getKey(); sb.append("<a href=\"").append(contextPath).append(url).append("\">"); String partyName = HtmlUtils.htmlEscape(relyingPartyUrlToName.getValue()); sb.append(partyName); sb.append("</a>"); sb.append("</td></tr>\n"); } sb.append("</table>\n"); } sb.append("</div>\n"); sb.append("</body></html>"); return sb.toString(); }这种直接拼接的写法很简单粗暴,虽然不是很优雅但是不会引入别的东西,只用原生的方法拼接,极大的减少了冲突等等一系列问题的产生。这样我们知道了登录页面是怎么来的了。但是这个账号和密码又是怎么来的呢?可以在自己的依赖包中找到相关的Properties的配置F:\maven\org\springframework\boot\spring-boot-autoconfigure\2.6.7\spring-boot-autoconfigure-2.6.7.jar!\org\springframework\boot\autoconfigure\security\SecurityProperties.class当然从上面的properties中我们可以看出这是,框架默认的配置,我们是可以对其进行修改的。只需要在yml配置文件中配置用户名和密码就可以完成spring security登录页面的账号和密码配置了。当然再配置类中也可以进行配置。spring: security: user: name: test password: 123456也可以通过配置类完成很多其他设置如下,其中@Configuration 注解将Bean 放到容器中管理,@EnableWebSecurity注解 用于将Security 生成 Bean,一定记得要加上才能使配置类生效。package com.test.config; import com.test.service.MyUserService; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.annotation.Configuration; 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.builders.WebSecurity; import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter; /** * @author * @date */ @Configuration // 将Bean 放到容器中管理 @EnableWebSecurity // 用于将Security 生成 Bean public class SpringSecurityConfig extends WebSecurityConfigurerAdapter { @Autowired private MyUserService myUserService; /* * 接口请求拦截 **/ @Override protected void configure(HttpSecurity http) throws Exception { http.authorizeRequests() //安全请求策略 .antMatchers("/home").permitAll() //可放行请求配置 .anyRequest().authenticated() //其他请求进行拦截 .and() .logout().permitAll() // 注销任意访问 .and() .formLogin(); http.cors().disable(); } /* * 告诉程序,系统中有个用户 用户名为 admin ,密码为 admin 角色为 ADMIN * */ @Override protected void configure(AuthenticationManagerBuilder auth) throws Exception { //可以设置内存指定的登录的账号密码,指定角色 //不加.passwordEncoder(new MyPasswordEncoder())或者注入该类的Bean //就不是以明文的方式进行匹配,会报错 auth.inMemoryAuthentication().withUser("admin").password("admin").roles("ADMIN"); auth.inMemoryAuthentication().withUser("demo").password("demo").roles("ADMIN"); auth.inMemoryAuthentication().withUser("testRole").password("testRole").roles("AD"); auth.userDetailsService(myUserService).passwordEncoder(new MyPasswordEncoder()); } /* * 前端拦截 * */ @Override public void configure(WebSecurity web){ // 忽视 js 、css 、 images 后缀访问 web.ignoring().antMatchers("/js/**","/css/**","/images/**"); } }
-
@ConfigurationProperties(prefix=“ XX”)告诉springboot将该注解修饰的类的所有属性和配置文件中相关的配置进行绑定,prefix属性表示的是从配置文件中为XX的下面的属性进行一一映射。该注解一般和@Component 注解一起使用。 因为是只有这个组件是容器内的组件才能用@ConfigurationProperties 这个注解如果不写@Component,提示在springboot的开启配置属性中未注册,所以需要标记为spring的Component组件或者,在配置扫描中配置,配置值得时候支持数据校验,可以给这个类上面加上@Valiadated注解,然后下面的属性上面就可以就加上类似@Email的校验。注解可以能获取yml配置文件中的复杂类型的值比如数组或者map,默认从全局配置文件中获取值@Lazy使bean懒加载,取消bean预初始化。@Primary自动装配时当出现多个Bean候选者时,被注解为@Primary的Bean将作为首选者,否者将抛出异常。@Value也可以获取配置文件的@Value需要一个个的在类中给属性上面加这个注解 用{xx.bb}的形式单个设置值,但是value注解支持el表达式,但是value注解不能获取yml配置文件中的复杂类型的值比如数组或者map@PropertySource(value={""})加载value数组路径下的配置文件@ImportSource(localtions{}) 导入spring的配置文件,springboot里面没有spring的配置,自己编写的spring文件也不会生效,想使用的话 就需要加入这个注解标准在配置类上,locations数组可以加载多个配置文件。springboot推荐使用配置类来使用配置文件@Cofinguration指明当前类为配置类,等于spring的配置文件,在这个配置下 可以给方法上面加入@Bean注解给容器中加入组件 意思为把方法的返回值放入容器,默认的id为方法名@Profile springboot。1.多个配置文件的。比如application-dev.properties application-pro.properties application-{profile}.properties 可以在配置文件中选择激活哪一个配置文件,用来切换开发生产环境2.spring-profiles-active ={profile}如果使用的是yml格式的配置文件的话 可以使用文档块每个文档块用 --- 隔开在最上面使用 3.命令行:将项目install,然后在jar包所在磁盘地址栏cmd,运行如下命令启动项目java -jar spring-boot-02-config-0.0.1-SNAPSHOT.jar --spring.profiles.active=dev; 酱紫可以直接在测试的时候,配置传入命令行参数,优先级最高,会覆盖上面的或者用idea运行项目,在中配置,效果和级别等同命令行4.虚拟机参数-Dspring.profiles.active=dev 优先级比配置文件高5.Active profiles 设置如果每个位置都有配置文件,每个配置文件都会加载,并且会配置互补。spring.config.location 可以在项目打完包之后指定运行一个配置文件,用在打包完后修改了配置.springBoot也可以从一下位置加载配置,优先级从高到低高优先级覆盖低优先级呃配置,所有的配置回互补1.多个命令行参数可以用空格隔开 --server.port=8081 --server.context-path=/abc6-9优先加载带profile的,由jar包外向jar包内寻找
-
## 责任链模式是什么 责任链模式是一种行为型设计模式,也就是重点是处理数据,假设我们有一份数据,需要经过很多个节点处理,那么就会是以下这个样子: 一个节点处理完之后,交给下一个节点,不知道大家有没有使用过审批流,当我们提完一个审批单后,你的`leader`审批,leader审批通过之后就是总监批,总监后面可能是高级总监,或者`cto`,或者`hr`。他们在同一个链条上,倘若你的`leader`没有审批完,后面的节点是不可能收到信息的。如果你的`leader`拒绝了你的申请,那数据也不会到达后面的审批节点。 如果你接触过前端,JS 中点击某个 `div` 的时候会产生冒泡事件,也就是点击下面的`A`, `A` 在`B`里面,`B`在`C`里面, `A`-> `B` -> `C` 会依次收到点击事件: 再举个例子,在 `SpringMVC`中,我们有时候会定义一些拦截器,对请求进行预处理,也就是请求过来的时候,会依次经历拦截器,通过拦截器之后才会进入我们的处理业务逻辑代码。 之前,在做人员管理的时候,有涉及到人员离职情况的处理流程,要交接工作,解除权限,禁用账号等等,这整个处理流程就很适合使用责任链来处理。当然,自动处理流程是会出错的,保存每一个阶段的状态,针对出错的场景,可以手动去从断开责任链的地方接着执行。这整个流程的框架就是应用了责任链,但是根据实际场景也添加了不少其他的东西。 ## 两点疑问 1. 责任链的每一个节点是不是一定包含下一个节点的引用? 答:不一定,要么把所有责任节点放在一个`list`里面,依次处理;要么每个节点包含下一个责任节点的引用, 1. 责任链到底是不允许中断还是不允许中断? 答:两种都可以,不拘泥于细节,可以根据自己的场景使用。 ## 责任链模式中的角色 责任链一般有以下的角色: - `Client`(客户端):调用责任链处理器的处理方法,或者在第一个链对象中调用`handle`方法。 - `Handler`(处理器):抽象类,提供给实际处理器继承然后实现`handle`方法,处理请求 - `ConcreteHandler`(具体处理器):实现`handler`的类,同时实现`handle`方法,负责处理业务逻辑类,不同业务模块有不同的`ConcreteHandler`。 - `HandlerChain`:负责组合责任链的所有节点以及流程(如果节点包含下一个节点的引用,那么`HandlerChain`可以不存在) ## 审批链的实现 下面我们分别来实现不同的写法,假设现在有一个场景,秦怀入职了一家公司,哼哧哼哧干了一年,但是一直没调薪,又过了一年,总得加薪了吧,不加就要提桶跑路了,于是秦怀大胆去内部系统提了一个申请单:【加薪申请】 ### 不中断模式 先演示不中断模式,得先弄个申请单的实体,里面包含了申请单的名字和申请人: ```java public class Requisition { // 名称 public String name; // 申请人 public String applicant; public Requisition(String name, String applicant) { this.name = name; this.applicant = applicant; } } ``` 责任链中的每个责任节点,也就是处理器,可以抽象成为一个接口: ```java public interface Handler { // 处理申请单 void process(Requisition requisition); } ``` 我们依次实现了三个不同的责任节点,分别代表`leader`,总监,`hr`审批: ```java public class ManagerHandler implements Handler { @Override public void process(Requisition requisition) { System.out.println(String.format("Manager 审批来自[%s]的申请单[%s]...", requisition.applicant, requisition.name)); } } public class DirectorHandler implements Handler{ @Override public void process(Requisition requisition) { System.out.println(String.format("Director 审批来自[%s]的申请单[%s]...", requisition.applicant, requisition.name)); } } public class HrHandler implements Handler{ @Override public void process(Requisition requisition) { System.out.println(String.format("Hr 审批来自[%s]的申请单[%s]...", requisition.applicant, requisition.name)); } } ``` 责任节点都有了,我们需要用一个责任链把它们组合起来: ```java public class HandlerChain { List handlers = new ArrayList(); public void addHandler(Handler handler){ handlers.add(handler); } public void handle(Requisition requisition){ for(Handler handler:handlers){ handler.process(requisition); } System.out.println(String.format("来自[%s]的申请单[%s]审批完成", requisition.applicant, requisition.name)); } } ``` 客户端测试类: ```java public class ClientTest { public static void main(String[] args) { HandlerChain handlerChain = new HandlerChain(); handlerChain.addHandler(new ManagerHandler()); handlerChain.addHandler(new DirectorHandler()); handlerChain.addHandler(new HrHandler()); handlerChain.handle(new Requisition("加薪申请","秦怀")); } } ``` 运行结果: ```java Manager 审批来自[秦怀]的申请单[加薪申请]... Director 审批来自[秦怀]的申请单[加薪申请]... Hr 审批来自[秦怀]的申请单[加薪申请]... 来自[秦怀]的申请单[加薪申请]审批完成 ``` 从结果上来看,申请单确实经历过了每一个节点,形成了一条链条,这就是责任链的核心思想。每个节点拿到的都是同一个数据,同一个申请单。 ### 中断模式 秦怀加薪的想法很美好,但是现实很骨感,上面的审批流程一路畅通,但是万一 Hr 想拒绝掉这个申请单了,上面的代码并没有赋予她这种能力,因此,代码得改!(Hr 内心:我就要这个功能,明天上线)。 既然是支持中断,也就是支持任何一个节点审批不通过就直接返回,不会再走到下一个节点,先给抽象的处理节点方法加上返回值: ```java public interface Handler { // 处理申请单 boolean process(Requisition requisition); } ``` 三个处理节点也同步修改: ```java public class ManagerHandler implements Handler { @Override public boolean process(Requisition requisition) { System.out.println(String.format("Manager 审批通过来自[%s]的申请单[%s]...", requisition.applicant, requisition.name)); return true; } } public class DirectorHandler implements Handler{ @Override public boolean process(Requisition requisition) { System.out.println(String.format("Director 审批通过来自[%s]的申请单[%s]...", requisition.applicant, requisition.name)); return true; } } public class HrHandler implements Handler{ @Override public boolean process(Requisition requisition) { System.out.println(String.format("Hr 审批不通过来自[%s]的申请单[%s]...", requisition.applicant, requisition.name)); return false; } } ``` 处理链调整: ```java public class HandlerChain { List handlers = new ArrayList(); public void addHandler(Handler handler) { handlers.add(handler); } public void handle(Requisition requisition) { for (Handler handler : handlers) { if (!handler.process(requisition)) { System.out.println(String.format("来自[%s]的申请单[%s]审批不通过", requisition.applicant, requisition.name)); return; } } System.out.println(String.format("来自[%s]的申请单[%s]审批完成", requisition.applicant, requisition.name)); } } ``` 修改完成之后的结果: ```java Manager 审批通过来自[秦怀]的申请单[加薪申请]... Director 审批通过来自[秦怀]的申请单[加薪申请]... Hr 审批不通过来自[秦怀]的申请单[加薪申请]... 来自[秦怀]的申请单[加薪申请]审批不通过 ``` 秦怀哭了,加薪的审批被 hr 拒绝了。虽然被拒绝了,但是秦怀也感受到了可以中断的责任链模式,这种写法在处理请求的时候也比较常见,因为我们不希望不合法的请求到正常的处理逻辑中。 ### 包含下一个节点的引用 前面说过,**在责任链模式里,很多对象由每一个对象对其下家的引用而连接起来形成一条链。**上面的写法都是不包含下一个节点引用的写法。下面我们实践一下,如何使用引用写法完成责任链。 改造`Handler`接口为抽象类: ```java public abstract class Handler { private Handler nextHandler; public void setNextHandler(Handler handler) { this.nextHandler = handler; } // 处理申请单 protected abstract boolean process(Requisition requisition); // 暴露方法 public boolean handle(Requisition requisition) { boolean result = process(requisition); if (result) { if (nextHandler != null) { return nextHandler.handle(requisition); } else { return true; } } return false; } } ``` 三个实现类不变: ```java public class ManagerHandler extends Handler{ @Override boolean process(Requisition requisition) { System.out.println(String.format( "Manager 审批通过来自[%s]的申请单[%s]...", requisition.applicant, requisition.name)); return true; } } public class DirectorHandler extends Handler { @Override public boolean process(Requisition requisition) { System.out.println(String.format( "Director 审批通过来自[%s]的申请单[%s]...", requisition.applicant, requisition.name)); return true; } } public class HrHandler extends Handler{ @Override public boolean process(Requisition requisition) { System.out.println(String.format("Hr 审批不通过来自[%s]的申请单[%s]...", requisition.applicant, requisition.name)); return false; } } ``` 测试方法,构造嵌套引用: ```java public class ClientTest { public static void main(String[] args) { HrHandler hrHandler = new HrHandler(); DirectorHandler directorHandler = new DirectorHandler(); directorHandler.setNextHandler(hrHandler); ManagerHandler managerHandler = new ManagerHandler(); managerHandler.setNextHandler(directorHandler); managerHandler.handle(new Requisition("加薪申请","秦怀")); } } ``` 可以看到运行结果也是一样: ```java Manager 审批通过来自[秦怀]的申请单[加薪申请]... Director 审批通过来自[秦怀]的申请单[加薪申请]... Hr 审批不通过来自[秦怀]的申请单[加薪申请]... ``` ### 拓展一下 其实责任链配合上`Spring`更加好用,主要有两点: 1、可以使用注入,自动识别该接口的所有实现类。 ```java @Autowire public List handlers; ``` 2、可以使用`@Order`注解,让接口实现类按照顺序执行。 ```java @Order(1) public class HrHandler extends Handler{ ... } ``` ### 源码中的应用 - `Mybatis` 中的 `Plugin` 机制使用了责任链模式,配置各种官方或者自定义的 `Plugin`,与 `Filter` 类似,可以在执行 `Sql` 语句的时候执行一些操作。 - `Spring`中使用责任链模式来管理`Adviser`。 比如`Mybatis`中可以添加若干的插件,比如`PageHelper`,多个插件对对象的包装采用的动态代理来实现,多层代理。 ```java //责任链插件 public class InterceptorChain { private final List interceptors = new ArrayList(); // 生成代理对象 public Object pluginAll(Object target) { for (Interceptor interceptor : interceptors) { target = interceptor.plugin(target); } return target; } //一层一层的拦截器 public void addInterceptor(Interceptor interceptor) { interceptors.add(interceptor); } public List getInterceptors() { return Collections.unmodifiableList(interceptors); } } ``` ## 总结 责任链模式的优点: - 降低对象直接的耦合度,对象会自动传递到下一个责任节点,不管是引用方式,还是非引用方式。 - 增强拓展性,如果需要添加新的责任节点,也比较方便,实现特定的接口即可。 - 责任节点的顺序可控,可以指定一个顺序属性,排序即可。 - 每个责任节点职责专一,只处理自己的任务,符合类的单一职责原则。 责任链的缺点: - 如果责任链比较长,性能会受影响。 - 责任链可能会中途断掉,请求不一定会被接收。 责任链一般是在流程化的处理中,多个节点处理同一份数据,依次传递,可能有顺序要求,也可能没有,处理器的能力抽象成接口,方便拓展。
-
一、前言前面已经讲解了快速上手SpringBoot入门程序制作的四种方式,相信各位小伙伴们已经可以熟练的使用这些方式来创建一个简单的web程序了,但是仅仅知道这些还是不够的。接下来,带大家一起了解parent、starter、引导类、以及内嵌Tomcat相关的知识!二、百度百科Spring Boot是由Pivotal团队提供的全新框架,其设计目的是用来简化新Spring应用的初始搭建以及开发过程。该框架使用了特定的方式来进行配置,从而使开发人员不再需要定义样板化的配置。通过这种方式,Spring Boot致力于在蓬勃发展的快速应用开发领域(rapid application development)成为领导者。三、简化开发从百度百科中可以看出,其目的是用来简化Spring!那么到底简化在什么地方呢?让我们想想在学习SSM时,做过原始SpringMVC程序的小伙伴应该知道,写SpringMVC程序,最基础的spring-web和spring-webmvc这两个坐标是必须的,这些还不包含我们使用的json啊等等坐标,现在呢?一个坐标搞定!以前写配置类或者配置文件,然后用什么东西就要自己写加载bean这些东西,现在呢?什么都没写,照样能用。有以下优点:简化依赖配置简化常用工程相关配置内置服务器,比如Tomcat别着急,让我们慢慢来探讨探讨其中的奥秘~四、parent介绍打开创建好的springboot程序,可以看见pom.xml文件中的<parent> </parent> <parent> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-parent</artifactId> <version>2.6.4</version> <relativePath/> </parent>这里的<version>2.6.4<version>就是自己使用的springboot版本,打开后可以发现其中又继承了一个坐标,引入了很多依赖<parent> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-dependencies</artifactId> <version>2.6.4</version> </parent>再次点击打开,就可以找到其中的奥秘了。从下图我们可以发现各式各样的依赖版本号属性,下面列出依赖版本属性的局部,可以看的出来,定义了若干个技术的依赖版本号再看看下图,各式各样的的依赖坐标信息,可以看出依赖坐标定义中没有具体的依赖版本号,而是引用了第一组信息中定义的依赖版本属性值注意:上面的依赖坐标定义是出现在<dependencyManagement>标签中的,其实是对引用坐标的依赖管理,并不是实际使用的坐标。因此当我们的项目中继承了这组parent信息后,在不使用对应坐标的情况下,前面的这组定义是不会具体导入某个依赖的最后来看看使用不同的springboot版本时,其对应的pom依赖文件有什么不同。我这里对比的是springboot2.5.6版本和springboot2.6.4从图中可以清楚的看到,当我们使用不同的springboot版本时,他们的依赖版本就会不同。这也确保了,在使用springboot时,我们可以在某种程度上避免版本冲突的复杂问题,方便了程序员们的开发!五、starter介绍SpringBoot关注到开发者在实际开发时,对于依赖坐标的使用往往都有一些固定的组合方式,比如使用spring-webmvc就一定要使用spring-web。每次都要固定搭配着写,非常繁琐,而且格式固定,没有任何技术含量。SpringBoot一看这种情况,把所有的技术使用的固定搭配格式都给开发出来,以后我们使用某个技术,就不用一次写一堆依赖了,直接用springboot做好的这个东西就好了,对于这样的固定技术搭配,SpringBoot给它起了个名字叫做starter。starter定义了使用某种技术时对于依赖的固定搭配格式,也是一种最佳解决方案,使用starter可以帮助开发者减少依赖配置 <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency>比如我想开发web应用,就需要引入上面的web对应的starter依赖,并没有写SpringMVC的坐标,点击spring-boot-starter-web我们会发现在spring-boot-starter-web中又定义了若干个具体依赖的坐标通过上图我们可以细心的发现叫做spring-boot-starter-json的名字中也有starter,打开看看里面有什么?我们可以发现,这个starter中又包含了若干个坐标,其实就是使用SpringMVC开发通常都会使用到Json,使用json又离不开这里面定义的这些坐标,看来还真是方便,SpringBoot把我们开发中使用的东西能用到的都给提前做好了。仔细看完会发现,里面有一些我们没用过的。的确会出现这种过量导入的可能性,不过没关系,可以通过maven中的排除依赖剔除掉一部分。不过你不管它也没事,大不了就是过量导入呗。到这里基本上得到了一个信息,使用starter可以帮开发者快速配置依赖关系六、starter与parent的区别朦朦胧胧中感觉starter与parent好像都是帮助我们简化配置的,但是功能又不一样:starter是一个坐标中定了若干个坐标,以前写多个的,现在写一个,是用来减少依赖配置的书写量的parent是定义了几百个依赖版本号,以前写依赖需要自己手工控制版本,现在由SpringBoot统一管理,这样就不存在版本冲突了,是用来减少依赖冲突的温馨提示 SpringBoot官方给出了好多个starter的定义,方便我们使用,而且名称都是如下格式命名规则:spring-boot-starter-技术名称七、引导类介绍配置说完了,我们发现SpringBoot确实帮助我们减少了很多配置工作,下面说一下程序是如何运行的。目前程序运行的入口就是SpringBoot工程创建时自带的那个类了,带有main方法的那个类,运行这个类就可以启动SpringBoot工程的运行,我的是这个:@SpringBootApplication public class Springboot0101Application { public static void main(String[] args) { SpringApplication.run(Springboot0101Application.class, args); }写代码测试一下,先创建一个User类,把它放在容器中@Component public class User { }然后再写一个BookController类,也把它放在容器中@RestController @RequestMapping("/books") public class BookController { @GetMapping("/getBooks") public String getBooks() { System.out.println("springboot程序正在运行呢~"); return "Hello,SpringBoot is running"; } }看看我对应类的目录结构:最后写代码测试一下:@SpringBootApplication public class Springboot0101Application { public static void main(String[] args) { ConfigurableApplicationContext applicationContext = SpringApplication.run(Springboot0101Application.class, args); BookController bookBean = applicationContext.getBean(BookController.class); System.out.println("The message of bookBean : " + bookBean); User userBean = applicationContext.getBean(User.class); System.out.println("The message of userBean : " + userBean); } }运行结果:看到结果,小伙伴们不难猜想了——SpringBoot程序启动是创建了一个Spring容器对象吧?答案就是如此!Springboot0101Application这个类在SpringBoot程序中是所有功能的入口,称这个类为引导类。作为一个引导类最典型的特征就是当前类上方声明了一个注解@SpringBootApplication点击进入@SpringBootApplication,我们可以看到:这里面有我们之前学习SSM时用到的包扫描注解,再点击进入@SpringBootConfiguration内:我们可以发现,它最终使用了@Configuration注解,所以,归根到底,我们使用的引用类,也是一个配置类。八、内嵌Tomcat1、Tomcat定义位置程序现在已经运行了,通过引导类的main方法运行了起来。但是运行java程序不应该是执行完就结束了吗?但是我们现在明显是启动了一个web服务器啊,不然网页怎么能正常访问呢?这个服务器是在哪里写的呢?认真想一想,它就在我们引入的spring-boot-starter-web场景starter中,我们打开它来看一看:这里面有一个核心的坐标,tomcat-embed-core,叫做tomcat内嵌核心。就是这个东西把tomcat功能引入到了我们的程序中。2、Tomcat运行原理再来说第二个问题,这个服务器是怎么运行的?Tomcat服务器是一款软件,而且是一款使用java语言开发的软件,既然是使用java语言开发的,运行的时候肯定符合java程序运行的原理,java程序运行靠的是什么?对象呀,一切皆对象,万物皆对象。那tomcat运行起来呢?也是对象。如果是对象,那Spring容器是用来管理对象的,这个对象能不能交给Spring容器管理呢?答案是可以的!tomcat服务器运行其实是以对象的形式在Spring容器中运行的,怪不得我们没有安装这个tomcat,而且还能用。闹了白天这东西最后是以一个对象的形式存在,保存在Spring容器中悄悄运行的。具体运行的是什么呢?其实就是上前面提到的那个tomcat内嵌核心具体内嵌核心依赖如下:<dependency> <groupId>org.apache.tomcat.embed</groupId> <artifactId>tomcat-embed-core</artifactId> <version>9.0.58</version> <scope>compile</scope> <exclusions> <exclusion> <artifactId>tomcat-annotations-api</artifactId> <groupId>org.apache.tomcat</groupId> </exclusion> </exclusions> </dependency>3、更换内嵌Tomcat那既然是个对象,如果把这个对象从Spring容器中去掉是不是就没有web服务器的功能呢?当然可以,通过依赖排除可以去掉这个web服务器功能。根据SpringBoot的工作机制,用什么技术,加入什么依赖就行了。我选择的是SpringBoot提供的内置服务器jetty更换代码如下: <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> <exclusions> <exclusion> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-tomcat</artifactId> </exclusion> </exclusions> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-jetty</artifactId> </dependency>让我们运行一下看看是什么样的结果:输出结果是没有问题的,但是服务器就不是默认的Tomcat了,而是我选择的jetty服务器链接:https://bbs.huaweicloud.com/blogs/336909
-
什么是 Spring BootSpring Boot 是由 Pivotal 团队提供的基于 Spring 的全新框架,其设计目的是为了简化 Spring 应用的搭建和开发过程。该框架遵循“约定大于配置”原则,采用特定的方式进行配置,从而使开发者无需定义大量的 XML 配置。通过这种方式,Spring Boot 致力于在蓬勃发展的快速应用开发领域成为领导者。Spring Boot 并不重复造轮子,而且在原有 Spring 的框架基础上封装了一层,并且它集成了一些类库,用于简化开发。换句话说,Spring Boot 就是一个大容器。我们可以注意到,没有写任何的配置文件,更没有显示使用任何容器,只需要启动 Main 方法即可开启 Web 服务,从而访问到 HelloController 类里定义的路由地址。这就是 Spring Boot 的强大之处,它默认集成了 Tomcat 容器,通过 Main 方法编写的 SpringApplication.run 方法即可启动内置 Tomcat。它是如何启动的,内部又是如何运行的呢?具体原理我将在《第03课:Spring Boot 启动原理》一节中具体分析。在上面的示例中,我们没有定义应用程序启动端口,可以看到控制台,它开启了 8080 端口,这是 Spring Boot 默认的启动端口。Spring Boot 提供了默认的配置,我们也可以改变这些配置,具体方法将在后面介绍。我们在启动类里加入 @SpringBootApplication 注解,则这个类就是整个应用程序的启动类。如果不加这个注解,启动程序将会报错,读者可以尝试一下。Spring Boot 还有一个特点是利用注解代码繁琐的 XML 配置,整个应用程序只有一个入口配置文件,那就是 application.yml 或 application.properties。接下来,我将介绍其配置文件的用法。properties 和 yaml在前面的示例代码中,我们并没有看到该配置文件,那是因为 Spring Boot 对每个配置项都有默认值。当然,我们也可以添加配置文件,用以覆盖其默认值,这里以 .properties 文件为例,首先在 resources 下新建一个名为 application.properties(注意:文件名必须是 application)的文件,键入内容为:server.port=8081server.servlet.context-path=/api并且启动 Main 方法,这时程序请求地址则变成了:http://localhost:8081/api/hello。Spring Boot 支持 properties 和 yaml 两种格式的文件,文件名分别对应 application.properties 和 application.yml,下面贴出 yaml 文件格式供大家参考:server: port: 8080 servlet: context-path: /api可以看出 properties 是以逗号隔开,而 yaml 则换行+ 两个空格 隔开,这里需要注意的是冒号后面必须空格,否则会报错。yaml 文件格式更清晰,更易读,这里作者建议大家都采用 yaml 文件来配置。以上示例只是小试牛刀,更多的配置将在后面的课程中讲解。本教程的所有配置均采用 yaml 文件。打包、运行Spring Boot 打包分为 war 和 jar 两个格式,下面将分别演示如何构建这两种格式的启动包。在 pom.xml 加入如下配置:<packaging>war</packaging><build> <finalName>api</finalName> <resources> <resource> <directory>src/main/resources</directory> <filtering>true</filtering> </resource> </resources> <plugins> <plugin> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-maven-plugin</artifactId> </plugin> <plugin> <artifactId>maven-resources-plugin</artifactId> <version>2.5</version> <configuration> <encoding>UTF-8</encoding> </configuration> </plugin> <plugin> <groupId>org.apache.maven.plugins</groupId> <artifactId>maven-surefire-plugin</artifactId> <version>2.18.1</version> <configuration> <skipTests>true</skipTests> </configuration> </plugin> </plugins></build>这个时候运行 mvn package 就会生成 war 包,然后放到 Tomcat 当中就能启动,但是我们单纯这样配置在 Tomcat 是不能成功运行的,会报错,需要通过编码指定 Tomcat 容器启动,修改 DemoApplication 类:import org.springframework.boot.SpringApplication;import org.springframework.boot.autoconfigure.SpringBootApplication;import org.springframework.boot.builder.SpringApplicationBuilder;import org.springframework.boot.web.servlet.support.SpringBootServletInitializer;@SpringBootApplicationpublic class DemoApplication extends SpringBootServletInitializer { public static void main(String[] args) { SpringApplication.run(DemoApplication.class, args); } @Override protected SpringApplicationBuilder configure(SpringApplicationBuilder application) { return application.sources(DemoApplication.class); }}这时再打包放到 Tomcat,启动就不会报错了。在上述代码中,DemoApplication 类继承了 SpringBootServletInitializer,并重写 configure 方法,目的是告诉外部 Tomcat,启动时执行该方法,然后在该方法体内指定应用程序入口为 DemoApplication 类,如果通过外部 Tomcat 启动 Spring Boot 应用,则其配置文件设置的端口和 contextPath 是无效的。这时,应用程序的启动端口即是 Tomcat 的启动端口,contextPath 和 war 包的文件名相同。接下来我们继续看如果达成 jar 包,在 pom.xml 加入如下配置:<!-- 需要将包类型改为 jar 包 --><packaging>jar</packaging><build> <finalName>api</finalName> <resources> <resource> <directory>src/main/resources</directory> <filtering>true</filtering> </resource> </resources> <plugins> <plugin> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-maven-plugin</artifactId> <configuration> <fork>true</fork> <!-- 指定 Main 方法所在类 --> <mainClass>com.lynn.DemoApplication</mainClass> </configuration> <executions> <execution> <goals> <goal>repackage</goal> </goals> </execution> </executions> </plugin> <plugin> <artifactId>maven-resources-plugin</artifactId> <version>2.5</version> <configuration> <encoding>UTF-8</encoding> <useDefaultDelimiters>true</useDefaultDelimiters> </configuration> </plugin> <plugin> <groupId>org.apache.maven.plugins</groupId> <artifactId>maven-surefire-plugin</artifactId> <version>2.18.1</version> <configuration> <skipTests>true</skipTests> </configuration> </plugin> <plugin> <groupId>org.apache.maven.plugins</groupId> <artifactId>maven-compiler-plugin</artifactId> <version>2.3.2</version> <configuration> <source>1.8</source> <target>1.8</target> </configuration> </plugin> </plugins></build>然后通过 mvn package 打包,最后通过 java 命令启动:java -jar api.jar如果是 Linux 服务器,上述命令是前台进程,点击 Ctrl+C 进程就会停止,可以考虑用 nohup 命令开启守护进程,这样应用程序才不会自动停止。
-
Spring Cloud 是什么?在学习本课程之前,读者有必要先了解一下 Spring Cloud。Spring Cloud 是一系列框架的有序集合,它利用 Spring Boot 的开发便利性简化了分布式系统的开发,比如服务发现、服务网关、服务路由、链路追踪等。Spring Cloud 并不重复造轮子,而是将市面上开发得比较好的模块集成进去,进行封装,从而减少了各模块的开发成本。换句话说:Spring Cloud 提供了构建分布式系统所需的“全家桶”。Spring Cloud 现状目前,国内使用 Spring Cloud 技术的公司并不多见,不是因为 Spring Cloud 不好,主要原因有以下几点:Spring Cloud 中文文档较少,出现问题网上没有太多的解决方案。国内创业型公司技术老大大多是阿里系员工,而阿里系多采用 Dubbo 来构建微服务架构。大型公司基本都有自己的分布式解决方案,而中小型公司的架构很多用不上微服务,所以没有采用 Spring Cloud 的必要性。但是,微服务架构是一个趋势,而 Spring Cloud 是微服务解决方案的佼佼者,这也是作者写本系列课程的意义所在。Spring Cloud 优缺点其主要优点有:集大成者,Spring Cloud 包含了微服务架构的方方面面。约定优于配置,基于注解,没有配置文件。轻量级组件,Spring Cloud 整合的组件大多比较轻量级,且都是各自领域的佼佼者。开发简便,Spring Cloud 对各个组件进行了大量的封装,从而简化了开发。开发灵活,Spring Cloud 的组件都是解耦的,开发人员可以灵活按需选择组件。接下来,我们看下它的缺点:项目结构复杂,每一个组件或者每一个服务都需要创建一个项目。部署门槛高,项目部署需要配合 Docker 等容器技术进行集群部署,而要想深入了解 Docker,学习成本高。Spring Cloud 的优势是显而易见的。因此对于想研究微服务架构的同学来说,学习 Spring Cloud 是一个不错的选择。Spring Cloud 和 Dubbo 对比Dubbo 只是实现了服务治理,而 Spring Cloud 实现了微服务架构的方方面面,服务治理只是其中的一个方面。下面通过一张图对其进行比较:可以看出,Spring Cloud 比较全面,而 Dubbo 由于只实现了服务治理,需要集成其他模块,需要单独引入,增加了学习成本和集成成本。Spring Cloud 学习Spring Cloud 基于 Spring Boot,因此在研究 Spring Cloud 之前,本课程会首先介绍 Spring Boot 的用法,方便后续 Spring Cloud 的学习。本课程不会讲解 Spring MVC 的用法,因此学习本课程需要读者对 Spring 及 Spring MVC 有过研究。本课程共分为四个部分:第一部分初识 Spring Boot,掌握 Spring Boot 基础知识,为后续入门 Spring Cloud 打好基础 。第二部分 Spring Cloud 入门篇,主要介绍 Spring Cloud 常用模块,包括服务发现、服务注册、配置中心、链路追踪、异常处理等。第三部分 Spring Cloud 进阶篇,介绍大型分布式系统中事务处理、线程安全等问题,并以一个实例项目手把手教大家搭建完整的微服务系统。第四部分 Spring Cloud 高级篇,解析 Spring Cloud 源码,并讲解如何部署基于 Spring Cloud 的大型分布式系统。本课程的所有示例代码均可在:https://github.com/lynnlovemin/SpringCloudLesson 下载。
推荐直播
-
DeepSeek华为云全栈解决方案
2025/02/18 周二 16:30-17:30
Young-华为云公有云解决方案专家
如何让大模型发挥更大能量助力业务?本期课程以真实案例展开,带您深入探索如何构建更完整的AI解决方案。
回顾中
热门标签