springboot 入门

news/2024/11/1 21:33:37/

springboot是什么

传统的开发模式下,无论是基于xml注解,都要做许多配置,如果项目中集成越多的其他框架,配置内容也会越多。为了让开发人员以最少的配置去开发应用,springboot诞生了。springboot的原则是约定大于配置能不配就不配)。

springboot还未spring项目提供了很多非功能特性,比如嵌入式tomcat。

创建springboot项目

点击File->New->Project,然后左侧选择Spring Initializr,输入相关项目信息,点击next,然后选择相关依赖,选择Spring Web,点击Create

在这里插入图片描述

springboot配置文件

实际开发中,经常需要用到自定义配置下面是以微信公众号开发时为例)。

/src/main/resources/application.yml

wechat:appid: wx123456789asdsadtoken: caappSecret: a123b456c789d147e258f369

读取配置项的方式(一)

/src/main/java/com/asd/config/WeChat.java

@ConfigurationProperties("wechat") // 参数输入前缀。@ConfigurationProperties和@Value注解用于获取配置文件中的属性定义并绑定到Java Bean或属性中
@Component
public class WeChat {private String appId;private String token;private String appSecret;...getter setter
}

/src/main/java/com/asd/controller/HellowController.java

@RestController // 等同于@Controller + @ResponseBody
public class HellowController {// 注入WeChat实体类@Autowiredprivate WeChat weChat;...@GetMapping("/getWeChat")public WeChat getWeChat() {return weChat;}
}

重启服务后,访问 localhost:8080/getWeChat :

{"appId":"wx123456789asdsad", "token":"ca", "appSecret":"a123b456c789d147e258f369"}

读取配置项的方式(二)

/src/main/java/com/asd/config/WeChatConfig.java

@Configuration // 标识这是一个配置类
public class WeChatConfig {@Bean // 这样的话springboot就自动帮我们注入weChat对象@ConfigurationProperties("wechat")public WeChat weChat() {return new WeChat();}
}

读取配置项的方式(三)

/src/main/java/com/asd/controller/HellowController.java

@RestController
public class HellowController {@Value("${wechat.appId}")private String appId;@Value("${wechat.token}")private String token;@Value("${wechat.appSecret}")private String appSecret;...@GetMapping("/getWeChat")public WeChat getWeChat() {WeChat weChat = new WeChat();weChat.setAppId(appId);weChat.setAppSecret(appSecret);weChat.setToken(token);return weChat;}
}
如果属性较少,可以用@Value。如果属性较多,建议用第一种或第二种。

在实际项目开发中,有时需要读取自定义的配置文件。新建自定义配置文件:

/src/main/resources/my.yml

wechat1:appid: wx123456789asdsad

/src/main/java/com/asd/config/MyWeChat.java

@PropertySource("my.yml")
@ConfigurationProperties("wechat1")
@Component
public class MyWeChat {private String appId;...getter setter
}

/src/main/java/com/asd/controller/HellowController.java

@RestController
public class HellowController {@Resourceprivate MyWeChat myWeChat;...@GetMapping("/getWeChat2")public MyWeChat getWeChat2() {return myWeChat;}
}

springboot多环境配置

开发项目时,通常需要经历几个阶段。

  • 本地开发接口(本地开发环境,local)
  • 开发完后与前端做接口联调(前后端联调环境,dev)
  • 联调完后提交测试(测试环境,test)
  • 测试完后有些公司会预发布(预发布环境,pre)
  • 部署到线上(生产环境,prod)

不同的开发环境,属性配置一般都不一样。如果不做多环境配置,就得去频繁修改配置文件,这样有一定的安全隐患。比如在本地开发时,不小心连上线上数据库,这样会对线上数据库造成一定的数据污染。

在resources目录下创建各环境的配置文件,springboot在启动的过程中,首先会加载application.yml,其次去加载这N个不同环境配置文件中的某一个。

在application.yml中的属性名可以当做变量,即${}来进行引用。

application-local.yml

server:port: 8001

application-dev.yml

server:port: 8002

application.yml

wechat:appid: wx123456789asdsadtoken: caappSecret: a123b456c789d147e258f369port: ${server.port}# 引用属性值,如果是引入local配置文件,其值就会是8001
spring:profiles:active: local #写你要启动的配置文件的后缀就行

获取参数

比如有实体类Student,他有name、age属性。
1)通过request对象获取参数

2)@RequestParam(针对请求头方式为x-www-form-urlencoded,比如form表单)

@GetMapping("/get")
public Student getById(@RequestParam Integer id,@RequestParam String name) {return id;
}
如果请求参数的name是id1,就得写成@RequestParam("id1")

3)@RequestBody(针对请求头方式为application/json)

@PostMapping("/save")
public Student save(@RequestBody Student student) {return student;
}

4)@PathVariable(接收url路径参数)

@GetMapping("/get/{id}")
public Student getById(@PathVariable Integer id) {return id;
}
若是{id1},与参数名id不一样,得写为@PathVariable("id1")

springboot集成mybatis

pom.xml

<dependency><groupId>org.mybatis.spring.boot</groupId><artifactId>mybatis-spring-boot-starter</artifactId><version>2.1.3</version>
</dependency><dependency><groupId>mysql</groupId><artifactId>mysql-connector-java</artifactId><version>8.0.13</version>
</dependency>

application-local.yml

spring:datasource:url: jdbc:mysql://127.0.0.1:3306/test?driver-class-name: com.mysql.cj.jdbc.Driverusername: rootpassword: wenhikari:connection-timeout: 30000maximum-pool-size: 30minimum-idle: 10max-lifetime: 6000
mybatis:configuration:#配置打印sql日志log-impl: org.apache.ibatis.logging.stdout.StdOutImpl#设置xml文件扫描路径。把所有配置文件放在resources/mapper下面mapper-locations: mapper/**/*.xml
在这个例子中application.yml目前引用的是application-local.yml。下同

最后,启动类要加注解@MapperScan("com.ca.mapper"),即扫描dao包里的接口。或在dao上加@Mapper注解。

springboot访问静态资源

resources/static里可存放静态资源文件,比如放一张图片后,localhost:8080/asd.jpg就能访问。

这是springboot默认存放静态资源的目录

若想访问自定义目录下的静态资源,比如新建resources/images目录,里面放图片。此时,需要对该文件夹做配置。

WebAppConfig.java

@Component
public class WebAppConfig implements WebMvcConfigurer {@Value("${upload.path}")private String uploadPath;// 下面文件上传时用@Resourceprivate TokenInterceptor tokenInterceptor;// 下面拦截器时用@Overridepublic void addResourceHandlers(ResourceHandlerRegistry registry) {registry.addResourceHandler("/static/**").addResourceLocations("classpath:/images/");// 下面文件上传时用,让匹配upload开头的URL,让他去找文件路径。registry.addResourceHandler("/upload/**").addResourceLocations("file:" + uploadPath);}// 配置拦截器,下面拦截器时用@Overridepublic void addInterceptors(InterceptorRegistry registry) {// 默认拦截所有url// registry.addInterceptor(tokenInterceptor);// 注册拦截器// 但实际开发中,要对部分url放行。所以可以针对一些url进行匹配,匹配需要拦截的,即addPathPatterns("/**"),"/**"是所有url // 也可以配置不需要拦截的url,比如对/student/...放行。registry.addInterceptor(tokenInterceptor).addPathPatterns("/**").excludePathPatterns("/student/*");}
}

WebMvcConfigurer配置类其实是Spring内部的一种配置方式,采用JavaBean的形式来代替传统的xml配置文件形式进行针对框架个性化定制,可以自定义一些Handler,Interceptor,ViewResolver,MessageConverter。基于java-based方式的spring mvc配置,需要创建一个配置类并实现WebMvcConfigurer 接口。

访问localhost:8080/static/test.jpg就行。

springboot上传文件

pom.xml

<!-- hutool -->
<dependency><groupId>cn.hutool</groupId><artifactId>hutool-all</artifactId><version>5.7.13</version>
</dependency>

application-local.yml

upload:path: d:\\uploads\\

UploadController.java

@RestController
public class UploadController {// 文件上传的根路径@Value("${upload.path}")private String uploadPath;@PostMapping("/upload")public Result upload(MultipartFile file) throws IOException{// 获取文件名称String fileName = file.getOriginalFilename();// 获取文件后缀(通过hutool的FileUtil工具类,需要注意的是,这里没有“.”,比如只有jpg)String suffix = FileUtil.getSuffix(fileName);// 对文件名进行重命名(文件的子路径)// 可以指定文件名策略,比如:时间戳-UUID.文件后缀String url = shijianchuo + uuid + "." + suffix;// 文件上传的真实路径String filePath = uploadPath + url;// 获取文件的字节流InputStream inputStream = file.getInputStream();// 把这个字节流写入到保存路径FileUtil.writeFromStream(inputStream, filePath);// 把文件的子路径返回到前端return Result.success(url);}	
}

访问刚才上传的文件:localhost:8080/upload/上传的文件路径.jpg

统一异常处理

调用接口过程中,可能会发生各种异常。此时,springboot默认给前端响应500,但即便发生异常,也应该给前端做一个正常的响应,以及告诉前端响应错误的原因。

有人会说用try catch,但如果项目越来越大,每个方法都用try catch,代码就会越来越臃肿。springboot中可以对项目的异常做一个统一的拦截处理。

/exception/BusinessException.java

public class BusinessException extends RuntimeException { // 这是自定义异常类,要继承RuntimeException// 写一个构造方法,传递Msgpublic BusinessException(String msg) {super(msg);}
}

/exception/SystemExceptionHandler.java

// 对系统异常做一个统一的处理(全局异常处理器)。
@RestControllerAdvice // 等同于 @ControllerAdvice + @ResponseBody。此注解通过对异常的拦截实现了统一异常返回处理
public class SystemExceptionHandler {@ExceptionHandler(BusinessException.class) // 指定要拦截的异常类public Result handlerException(BusinessException e) {// 也可以当做参数使用// 当我们拦截到异常后,直接给前端返回return Result.fail(e.getMessage());}// 也可以拦截Exception@ExceptionHandler(Exception.class)public Result exception() {return Result.fail("系统异常");}// 在参数校验时使用// 在这里拦截MethodArgumentNotValidException异常@ExceptionHandler(MethodArgumentNotValidException.class)public Result methodArgumentNotValidException(MethodArgumentNotValidException e) {// 拿到所有的参数校验失败的提示List<String> errorMessage = e.getBindingResult().getAllErrors() //获取所有的错误提示,返回的是集合对象.stream() // 对集合对象进行遍历.map(DefaultMessageSourceResolvable::getDefaultMessage) // 获取message.collect(Collectors.toList()) // 然后返回集合对象// 可以使用hutool的工具包,判断集合是否为空if (CollUtil.isNotEmpty(errorMessage)) {// 如果不为空,直接取他的第一个元素,得到错误提示String errorMsg = errorMessage.get(0);// 把错误提示信息返回到前端return Result.fail(errorMsg);}return Result.fail("系统异常");}}

配置后,出现异常时,就会返回Result这个统一的restful返回信息了。

如果想拦截自定义的BusinessException类,在代码中写throw new BusinessException("错误");即可。

拦截器

拦截器的拦截对象是controller里的方法。
例子:判断客户端发送的请求头中,是否包含token,并且是否值为asd。

/interceptor/TokenInterceptor.java

@Component
public class TokenInterceptor implements HandlerInterceptor{// 当返回true时执行controller里面的代码,但返回false时不执行下面的postHandler afterCompletion方法。@Overridepublic boolean preHandler(HttpServletRequest request, HttpServletResponse response, Object handler) {// 所以在这里面可以对token进行拦截String token = request.getHeader("token");// 获取请求头里面的Token if (StrUtil.isBlank(token)) {throw new BusinessException("请求头未包含token");}if (!"asd".equals(token)) {throw new BusinessException("请求头参数错误");}return true;}// controller方法执行之后,并且视图未渲染之前进行调用。(在前后端分离开发时,没有试图这个概念,所以了解即可)@Overridepublic void postHandler(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception{HandlerInterceptor.super.postHandler(request, response, handler, modelAndView);}// 在整个请求处理完毕之后进行回调,所以在这个方法里可以做一些线程资源的释放@Overridepublic void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {HandlerInterceptor.super.afterCompletion(request, response, handler, ex)}
}

执行顺序是preHandler、controller的方法、postHandler、aferCompletion。

用注解方式

/anotation/NeadToken.java

@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)// 设置此注解只能用于方法
public @interface NeedToken {}

@Retention作用是定义被它所注解的注解保留多久,一共有三种策略,定义在RetentionPolicy枚举中.
source:注解只保留在源文件,当Java文件编译成class文件的时候,注解被遗弃;被编译器忽略
class:注解被保留到class文件,但jvm加载class文件时候被遗弃,这是默认的生命周期
runtime:注解不仅被保存到class文件中,jvm加载class文件之后,仍然存在。

修改preHandler方法(当controller的方法加上这个注解时,我们才去判断他的请求头中是否有token。):

@Override
public boolean preHandler(HttpServletRequest request, HttpServletResponse response, Object handler) {// 判断handler是不是HandlerMethod	if (handler instanceof HandlerMethod) {// 转成HandlerMethod对象HandlerMethod handlerMethod = (HandlerMethod) handler;// 通过handlerMethod可以去获得方法上面的注解NeedToken needToken = handlerMethod.getMethod().getAnnotation(NeedToken.class);if (needToken != null) {// 不为空,说明方法用了这个注解,就去校验tokenString token = request.getHeader("token");// 获取请求头里面的Token if (StrUtil.isBlank(token)) {throw new BusinessException("请求头未包含token");}if (!"asd".equals(token)) {throw new BusinessException("请求头参数错误");}}}return true;
}

测试时,为了方便,应该去把url匹配部分注释。

registry.addInterceptor(tokenInterceptor)//.addPathPatterns("/**")//.excludePathPatterns("/student/*");

springboot aop

aop五大通知:

  • 前置通知 before advice:在目标方法执行之前执行
  • 后置通知 after returning advice:在目标方法执行之后执行
  • 异常通知 after throwing advice:目标方法抛出异常后执行
  • 最终通知 after finally advice:在目标方法执行之后都会执行(发生异常时,不执行后置通知,但执行最终通知,这是这两的区别)
  • 环绕通知 around advice:可以在目标方法执行之前执行,也可以在目标方法执行之后执行。

pom.xml

<dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-aop</artifactId>
</dependency>

/aop/LogAspect.java

@Aspect // 标识这是一个切面类
@Component
public class LogAspect {// @Pointcut用来标识切点// RestfulStudentController.*(),指RestfulStudentController里面的所有方法@Pointcut("execution(* com.ca.controller.RestfulStudentController.*())") public void pointcut() {}	// 前置通知@Before("pointcut()") // "pointcut":加切点,加上pointcut这个方法public void before() {logger.info("执行before方法");}// 最终通知@After("pointcut()")public void after() {logger.info("执行after方法");}// 后置通知@AfterReturning("pointcut()")public void afterReturn() {logger.info("执行afterReturn方法");}// 异常通知@AfterThrowing("pointcut()")public void ex() {logger.info("执行ex方法");}
}

执行后:
没异常的话,顺序为before、控制器的接口方法、afterReturn、after。
有异常的话,顺序为before、控制器的接口方法、ex、after。

补充

@Pointcut("execution(* com.ca.controller.RestfulStudentController.*(..))") // 所有方法的所有参数.*(..)
public void pointcut() {}@Before("pointcut()")
public void before(JoinPoint joinPoint) {// 这里可以加参数 JoinPoint joinPoint(@After、@AfterReturning亦是)// 通过joinPoint可以获取目标方法的参数Object[] args = joinPoint.getArgs();// 还可以通过joinPoint获取目标方法MethodSignature methodSignature = (MethodSignature)joinPoint.getSignature();Method method = methodSignature.getMethod();// 获取目标方法logger.info("拦截目标方法:{}", method.getName());logger.info("目标方法参数:{}"JSONUtil.toJsonStr(args));// 转换为json后输出。这时候要取pointcut里修改为拦截有参数的方法。logger.info("执行before方法");
}@AfterThrowing(value="pointcut()", throwing="exception")// 然后注解里要写上throwing="exception"
public void ex(Exception exception) {// 这里也可以传递异常对象,就能获取到是什么异常对象了// 之后可以打印异常logger.error(exception.getMessage(), exception);logger.info("执行ex方法");
}

环绕通知

顾名思义,就是对目标方法进行包围。使用环绕通知时,就不用使用其他通知了,可以统统注释掉。

@Around("pointcut()")
public void around(ProceedingJoinPoint proceedingJoinPoint) {// 里面可以传递 ProceedingJoinPoint proceedingJoinPoint 参数// 执行目标方法前执行前置通知System.out.println("执行前置通知");Object result;try {// 通过proceedingJoinPoint也可以参数和目标方法Object[] args = proceedingJoinPoint.getArgs();MethodSignature methodSignature = (MethodSignature)proceedingJoinPoint.getSignature();Method method = methodSignature.getMethod();logger.info("拦截目标方法:{}", method.getName());logger.info("目标方法参数:{}"JSONUtil.toJsonStr(args));	result = proceedingJoinPoint.proceed();// 执行目标方法}catch(Throwable e) {// 还可以捕获异常logger.error(e.getMessage(), e);}// 执行目标方法后执行后置通知System.out.println("执行后置通知");return result;
}

springboot事务

事务指的是多个操作(插入、更新、删除等)同时进行,要么同时成功或同时失败。

需要用到事务的地方,在方法上添加@Transactional

注意事项

1)比如service里有save和save1方法,本来save上有@Transactional。后来把save里面的代码和@Transactional都干到save1上,然后在save里调用save1。这时,若出现异常也不会回滚。因为@Transactional使用了aop,aop首先要拦截的方法是save,而不是save1,所以他并没有获取到事务注解,此时事务就直接交给数据库自动控制了。所以@Transactional得加到save上才行(然后干掉save1上@Transactional)。

2)假如@Transactional加到save,在save中使用try catch,此时就算有异常,也不会回滚。因为aop只看这个方法是否抛出异常,抛出了才回滚。所以在catch里throw ex;即可。

3)目前service里有save,加上了@Transactional。假如有service1,里面有save1。在service的save里调用service1.save1(),而service1的save1加了@Transactional(propagation = Propagation.REQUIRES_NEW),REQUIRES_NEW是表示每次都重新开启一个新的事务。此时,在save中做了三个操作(加管理员,加管理员角色,[刚刚追加的]加角色)能否同时成功或失败?执行后可以发现,save里有异常时前两个失败,但save1成功了,这是因为save1开启了新的事务,所以他自己就提交了。所以,要保证所有的操作都要在同一个事务里面。

4)创建自定义异常类TestException(继承了Exception)。然后在save里throw new TestException();,但发现不回滚。因为@Transactional默认拦截的异常需要继承RuntimeException。但可以通过@Transactional(rollbackFor = Exception.class)来解决。

参数校验

在controller里用传统的if方式校验参数时,若参数多,就会代码冗余。为了解决这个,有必要使用参数校验框架springboot validation。

他允许我们通过注解的方式来定义对象校验规则,把校验和业务逻辑分离。

springboot validation 常用注解如下:

  • @NotBlank(校验字符串不为null,并且不为空字符串)
  • @NotEmpty(校验字符串不为null,允许空字符串)
  • @NotNull(校验对象不为null)
  • @Length(校验字符串长度)
  • @Min(最小值校验)
  • @Max(最大值校验)
  • @Pattern(正则匹配校验),比如邮箱号、手机号时可以使用这个
  • @Valid(当一个对象嵌套另外一个对象时可使用)
    class Student {
    @Valid
    private Admin admin;
    }
    当我们使用学员类去接收参数时,我们也需要去校验Admin里面的参数的时候,就可以使用@Valid去进行校验。

pom.xml

<dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-validation</artifactId>
</dependency>

Student.java

public class Student {@NotBlank(message="姓名不能为空")@Length(max = 5, message = "姓名不能超过5个字符")private String name;@NotNull@Max(value=100, message="年龄不能大于100岁")@Min(value=1, message="年龄不能小于1岁")private Integer age;@NotNull@Min(value = 1)@Max(value = 2)private Integer sex;
}

StudentController.java

@PostMapping
public Student save(@RequestBody @Valid Student student) {}

也可以使用@Validated,这个是spring里面的注解,@Validjavax里面的注解,这两个是都可以的,但有点区别。

运行后,若参数有问题,就会校验不通过,抛出MethodArgumentNotValidException异常,但不告诉前端是什么问题。

当然,可以用全局异常拦截器来解决这个问题。

校验模式

springboot validation分两种校验模式,全校验快速校验推荐)。

全校验指的是对所有的参数都校验完毕之后进行返回,快速校验是指只要遇到接口参数校验失败就立即返回。

使用快速校验模式时,需要做一个单独的配置,只需要把下面代码复制到项目即可。

BeanConfig.java

@Configuration
public class BeanConfig {// 配置快速校验模式@Beanpublic Validator validator() {ValidatorFactory validatorFactory = Validation.byProvider(HibernateValidator.class).configure().failFast(true)// 配置校验失败就立即停止.buildValidatorFactory();Validator validator = validatorFactory.getValidator();return validator;}
}

其他

使用springboot validator时,可能会遇到一个问题,比如save和update时,若都用Student来接收参数的话,save时虽然不需要做id校验,但update时则需要做id校验。此时,可对校验做分组。

比如@NotNull(groups = Update.class)groups = 这里要设置一个类(得是接口),所以先创建两个接口。若设置为groups = Update.class就说明要在修改时才做校验。

/valid/Save.java

public interface Save {}

/valid/Update.java

public interface Update {}

Student.java

public class Student {@NotNull(groups=Update.class)private Integer id;@NotBlank(message="姓名不能为空", groups={Save.class, Update.class})// 也可以指定多个分组@Length(max = 5, message = "姓名不能超过5个字符", groups=Save.class)private String name;@NotNull@Max(value=100, message="年龄不能大于100岁", groups=Save.class)@Min(value=1, message="年龄不能小于1岁", groups=Save.class)private Integer age;@NotNull@Min(value = 1, groups=Save.class)@Max(value = 2, groups=Save.class)private Integer sex;
}

然后还要把controller里的@Valid改为springboot里的@Validated(Save.class),即也指定了分组。

StudentController.java

@PostMapping
public Student save(@RequestBody @Validated(Save.class) Student student) {}@PutMapping
public Student update(@RequestBody @Validated(Update.class) Student student) {}

以上内容源于网络。


http://www.ppmy.cn/news/20181.html

相关文章

untiy TextMeshPro(简称TMP)组件详细内容

首先unity官方API对该组件的描述是&#xff1a; TextMeshPro 是 Unity 的最终文本解决方案。它是 Unity UI Text 和旧版 Text Mesh 的完美替代方案。 功能强大且易于使用的 TextMeshPro&#xff08;也称为 TMP&#xff09;使用高级文本渲染技巧以及一组自定义着色器&#xff1b…

Android 蓝牙开发——Avrcp协议(十二)

SDK路径&#xff1a;frameworks/base/core/java/android/bluetooth/ 服务路径&#xff1a;packages/apps/Bluetooth/src/com/android/bluetooth/ 在使用协议类的时候无法找到该类&#xff0c;由于安卓源码中关于蓝牙协议的 Client 部分或相关接口都被 hide 给隐藏掉了&#xf…

初学者如何学好Java数组,不妨点进来看看,赶在新年前肝完的万字博客

新年好~~~新年开篇万字博客 —Java数组的学习,有点干货,建议收藏观看!!! 本篇介绍了数组的概念,数组创建和初始化.数组的使用(元素访问,和数组遍历方法),初识引用数据类型,简单介绍JVM内存分布,认识null,堆区空间的释放 二维数组相关知识的介绍~ 学习Java中的数组一.数组的基本…

一起自学SLAM算法:10.3 机器学习与SLAM

连载文章&#xff0c;长期更新&#xff0c;欢迎关注&#xff1a; 前面已经分析过的8种SLAM算法案例&#xff08;Gmapping、Cartographer、LOAM、ORB-SLAM2、LSD-SLAM、SVO、RTABMAP和VINS&#xff09;都可以称为传统方法&#xff0c;因为这些算法都是在人为精心设计的特定规则下…

LeetCode[685]冗余连接II

难度&#xff1a;困难题目&#xff1a;在本问题中&#xff0c;有根树指满足以下条件的 有向 图。该树只有一个根节点&#xff0c;所有其他节点都是该根节点的后继。该树除了根节点之外的每一个节点都有且只有一个父节点&#xff0c;而根节点没有父节点。输入一个有向图&#xf…

原力分入门技能树-模拟

博客(blog) 以下哪种情况不可以提升原力分&#xff1a; 发高质量博客发低质量博客博客被点赞博客被评论 质量分(qc) 在以下哪个网址中可以查询博文质量分&#xff1a; https://ask.csdn.net/https://bbs.csdn.net/https://blog.csdn.net/https://www.csdn.net/qc 问答(as…

2022回顾

2022年回顾 前言 新年和亲朋好友的相聚差不多接近尾声&#xff0c;假期也所剩无几&#xff0c;开始静下心来写作&#xff0c;回顾一下我的2022年&#xff0c;看下自己去年 做得好的和不足&#xff0c;展望下2023&#xff0c;开始新一年的生活。&#xff08;因为是公历2023年…

MFC|Toolbox内控件简单介绍

参考&#xff1a; MFC控件工具箱 &#xff08;https://blog.csdn.net/Hubz131/article/details/77684910&#xff09; 对应工具的超链接是本人搜到认为较易理解的单个控件介绍。 Pointer&#xff1a;就是普通的鼠标&#xff0c;默认状态Button&#xff1a;按钮&#xff0c;用…