SpringSecurity学习总结-2 第三章 使用SpringMVC开发Restful API_springmvc restful接口开发

总结

机会是留给有准备的人,大家在求职之前应该要明确自己的态度,熟悉求职流程,做好充分的准备,把一些可预见的事情做好。

对于应届毕业生来说,校招更适合你们,因为绝大部分都不会有工作经验,企业也不会有工作经验的需求。同时,你也不需要伪造高大上的实战经验,以此让自己的简历能够脱颖而出,反倒会让面试官有所怀疑。

你在大学时期应该明确自己的发展方向,如果你在大一就确定你以后想成为Java工程师,那就不要花太多的时间去学习其他的技术语言,高数之类的,不如好好想着如何夯实Java基础。下图涵盖了应届生乃至转行过来的小白要学习的Java内容:

请转发本文支持一下

本文已被CODING开源项目:【一线大厂Java面试题解析+核心总结学习笔记+最新讲解视频+实战项目源码】收录

需要这份系统化的资料的朋友,可以点击这里获取

 */
public interface UserDetailView extends UserSimpleView{};

public User() {

}

public User(String username, String password) {
    this.username = username;
    this.password = password;
}

public User(String id, String username, String password) {
    this.id = id;
    this.username = username;
    this.password = password;
}

public User(String id, String username, String password, Date birthDay) {
    this.id = id;
    this.username = username;
    this.password = password;
    this.birthDay = birthDay;
}

/**
 * 设定id只在简单视图显示
 * @return
 */
@JsonView(UserSimpleView.class)
public String getId() {
    return id;
}

public void setId(String id) {
    this.id = id;
}

/**
 * 设定用户名只在简单视图显示
 * @return
 */
@JsonView(UserSimpleView.class)
public String getUsername() {
    return username;
}

public void setUsername(String username) {
    this.username = username;
}

/**
 * 设定密码只在详情视图显示
 * @return
 */
@JsonView(UserDetailView.class)
public String getPassword() {
    return password;
}

public void setPassword(String password) {
    this.password = password;
}

/**
 * 设定生日只在简单视图显示
 * @return
 */
@JsonView(UserSimpleView.class)
public Date getBirthDay() {
    return birthDay;
}

public void setBirthDay(Date birthDay) {
    this.birthDay = birthDay;
}

@Override
public String toString() {
    return "User{" +
            "id='" + id + '\'' +
            ", username='" + username + '\'' +
            ", password='" + password + '\'' +
            ", birthDay=" + birthDay +
            '}';
}

}


#### (3)自定义校验注解


①、新建自定义的校验注解



package security.demo.validator;

import javax.validation.Constraint;
import javax.validation.Payload;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

@Target({ElementType.METHOD,ElementType.FIELD})
@Retention(RetentionPolicy.RUNTIME)
@Constraint(validatedBy = MyConstraintValidator.class)//表明这个注解具有校验作用,使用的校验逻辑由validatedBy指定的校验类来执行
public @interface MyConstraint {
/**
* 校验需要的提示信息
* @return
*/
String message();

Class<?>[] groups() default {};

Class<? extends Payload>[] payload() default {};

}


②、新建自定义的校验逻辑类



package security.demo.validator;

import org.springframework.beans.factory.annotation.Autowired;
import security.demo.service.HelloService;

import javax.validation.ConstraintValidator;
import javax.validation.ConstraintValidatorContext;

/**

  • 自定义的校验类需要实现ConstraintValidator接口

  • MyConstraint:是需要验证的注解

  • String:需要验证的字段类型,这样该注解就只能用在String类型的参数上,如果设置成Object,则任何类型都能使用该注解
    */
    public class MyConstraintValidator implements ConstraintValidator<MyConstraint,Object> {

    @Autowired
    private HelloService helloService;

    /**

    • 校验类初始化时执行
    • @param constraintAnnotation
      */
      @Override
      public void initialize(MyConstraint constraintAnnotation) {
      System.out.println(“my custom validator init”);
      }

    /**

    • @param o 需要校验的值
    • @param constraintValidatorContext
    • @return
      */
      @Override
      public boolean isValid(Object o, ConstraintValidatorContext constraintValidatorContext) {
      helloService.greeting(“tom”);
      System.out.println(“需要校验的值:”+o);
      // return true;
      return false;
      }
      }

③、将自定义的校验注解加到需要校验的字段上。



/**
 * 用户名不能为空
 */
@MyConstraint(message = "用户名不能为空")
private String username;

### 


### 7、Restful API错误处理机制(3-6节内容)


#### (1)SpringBoot默认的错误处理机制


①: 浏览器端发出的请求,springboot如果处理时有错误,会由**BasicErrorController的errorHtml()方法**来处理,然后返回页面;


②: 非浏览器发出的请求(如PostMan等),springboot如果处理时有错误,会由**BasicErrorController的error()方法**来处理,然后返回JSON格式的错误信息;


#### (2)自定义异常处理


①: 浏览器端发出的请求,出现异常时返回的页面是在**src/main/resources/templates/error/**404.html配置,状态码是多少的就配置以该状态码为名称的页面,并在页面中设置返回的信息。


②: 非浏览器发出的请求(如PostMan等),springboot如果处理时有异常的情况处理方法:


步骤一:创建一个异常处理类



package security.demo.exception;

import java.io.Serializable;

/**

  • 自定义的处理用户不存在的异常处理
    */
    public class UserNotExistException extends RuntimeException implements Serializable {

    private static final long serialVersionUID = -4253671095865255861L;

    private String id;

    public UserNotExistException(String id) {
    super(“User Not Exist!”);
    this.id = id;
    }

    public String getId() {
    return id;
    }

    public void setId(String id) {
    this.id = id;
    }
    }


步骤二、创建一个异常的拦截处理器



package security.demo.web.controller;

import org.springframework.http.HttpStatus;
import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.ResponseBody;
import org.springframework.web.bind.annotation.ResponseStatus;
import security.demo.exception.UserNotExistException;

import java.util.HashMap;
import java.util.Map;

/**

  • ControllerAdvice:处理Controller抛出的异常
    */
    @ControllerAdvice
    public class ControllerExceptionHandler {

    /**

    • 处理抛出来的UserNotExistException类型的异常

    • @param exception

    • @return
      */
      @ExceptionHandler(UserNotExistException.class)
      @ResponseBody
      @ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR)
      public Map<String,Object> handlerUserNotExistException(UserNotExistException exception){
      Map<String,Object> result = new HashMap<>();
      result.put(“id”,exception.getId());
      result.put(“message”,exception.getMessage());

      return result;
      }

}


只要springboot方法里抛出**UserNotExistException**类型的异常就会由**ControllerExceptionHandler**配置的**handlerUserNotExistException()**方法来处理。


### 


### 8、Restful API的拦截(以拦截记录时间为例)(3-7、8节内容)


#### (1)过滤器(Filter)


**优点: filter里面是能够获取到(HttpServletRequest request)和响应(HttpServletResponse response),从request中也能获取到传入的参数信息;**


**缺点:无法知道是哪一个Controller类中的哪个方法被执行。**



Filter随web应用的启动而启动,只初始化一次,随web应用的停止而销毁。

1.启动服务器时加载过滤器的实例,并调用init()方法来初始化实例;

2.每一次请求时都只调用方法doFilter()进行处理;

3.停止服务器时调用destroy()方法,销毁实例。


方式一:实现Filter类(**有@Component注解**)



package security.demo.web.filter;

import org.springframework.stereotype.Component;

import javax.servlet.*;
import java.io.IOException;
import java.util.Date;

/**

  • @Component 这个注解的目的是将TimeFilter交给容器来管理。

  • 时间过滤器
    */
    @Component
    public class TimeFilter implements Filter {

    @Override
    public void init(FilterConfig filterConfig) throws ServletException {
    System.out.println(“时间过滤器初始化!”);
    }

    @Override
    public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
    System.out.println(“时间过滤器开始执行过滤!”);
    // long startTime = new Date().getTime();
    long startTime = System.currentTimeMillis();

     //执行下一个过滤器
     filterChain.doFilter(servletRequest,servletResponse);
     System.out.println("TimeFilter的耗时: "+(System.currentTimeMillis() - startTime));
     System.out.println("时间过滤器结束执行过滤!");
    

    }

    @Override
    public void destroy() {
    System.out.println(“销毁时间过滤器!”);
    }
    }


方式二:实现Filter((无**@Component注解**)


①先定义一个没有@Component注解的过滤器



package security.demo.web.filter;

import org.springframework.stereotype.Component;

import javax.servlet.*;
import java.io.IOException;

/**

  • 没有@Component注解的过滤器
    */
    public class OtherTimeFilter implements Filter {

    @Override
    public void init(FilterConfig filterConfig) throws ServletException {
    System.out.println(“OtherTimeFilter初始化!”);
    }

    @Override
    public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
    System.out.println(“OtherTimeFilter开始执行过滤!”);
    long startTime = System.currentTimeMillis();

     //执行下一个过滤器
     filterChain.doFilter(servletRequest,servletResponse);
     System.out.println("OtherTimeFilter的耗时: "+(System.currentTimeMillis() - startTime));
     System.out.println("OtherTimeFilter结束执行过滤!");
    

    }

    @Override
    public void destroy() {
    System.out.println(“销毁OtherTimeFilter!”);
    }
    }


②使用Filter配置类来管理没有@Component注解的过滤器



package security.demo.web.config;

import org.springframework.boot.web.servlet.FilterRegistrationBean;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import security.demo.web.filter.OtherTimeFilter;

/**

  • Filter:无法获取处理请求的方法信息

  • springboot中filter的配置和顺序执行:https://www.cnblogs.com/douJiangYouTiao888/p/9183095.html

  • 配置类
    */
    @Configuration
    public class WebFilterConfig {

    /**

    • FilterRegistrationBean是用来注册Filter的类

    • @return
      */
      @Bean
      public FilterRegistrationBean otherTimeFilter(){

      FilterRegistrationBean registrationBean = new FilterRegistrationBean();
      //将自定义的没有@Component注解的过滤器注册进来
      registrationBean.setFilter(new OtherTimeFilter());
      //设置过滤顺序
      registrationBean.setOrder(2);
      //设置过滤器的名字
      registrationBean.setName(“OtherTimeFilter”);
      //设置需要过滤的地址
      registrationBean.addUrlPatterns(“/*”);
      return registrationBean;

    }

}


#### 方式一和方式二的区别:


**方式二没有使用@Component注解,而是使用配置类来注册过滤器,这样的好处正如在过滤器注册时的设置一样清晰:**


**①:可以设置过滤器在过滤器链的位置(顺序);**


**②:可以设置过滤器在过滤器链中的名称;**


**③:可以设置过滤器拦截的规则;**


**而使用@Component注解的过滤器则无法更好的实现上述细节。**


#### (2)拦截器(Interceptor)


**优点:  可获请求(HttpServletRequest request)和响应(HttpServletResponse response)对象,也可获取方法所在类的类名及方法名信息****。**



    MethodParameter[] methodParameters = ((HandlerMethod)handler).getMethodParameters();
    for (MethodParameter methodParameter : methodParameters) {
        String parameterName = methodParameter.getParameterName();
        // 只能获取参数的名称,不能获取到参数的值
        //System.out.println("parameterName: " + parameterName);
    }

**缺点:从handler对象中只能获取参数的名称,不能获取到参数的值,所以无法获取处理请求的方法里的形参信息,(从request中可以*间接*能获取到传入的参数信息)**。


**它比filter的执行优先级低。**


步骤一:需要先实现**HandlerInterceptor**



package security.demo.web.interceptor;

import org.springframework.stereotype.Component;
import org.springframework.web.method.HandlerMethod;
import org.springframework.web.servlet.HandlerInterceptor;
import org.springframework.web.servlet.ModelAndView;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

/**

  • Interceptor:可获取处理请求的方法信息
    */
    @Component
    public class TimeInterceptor implements HandlerInterceptor {

    /**

    • 处理请求的方法执行前运行此方法

    • @param request

    • @param response

    • @param handler 处理请求的方法

    • @return

    • @throws Exception
      */
      @Override
      public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
      System.out.println(“preHandle方法执行”);
      System.out.println("处理请求的方法的类名: "+((HandlerMethod)handler).getBean().getClass().getName());
      System.out.println("处理请求的方法名: "+((HandlerMethod)handler).getMethod().getName());
      request.setAttribute(“startTime”,System.currentTimeMillis());

      return true;
      }

    /**

    • 处理请求的方法执行后运行此方法
    • @param request
    • @param response
    • @param handler
    • @param modelAndView
    • @throws Exception
      */
      @Override
      public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception {
      System.out.println(“postHandle方法执行”);
      Long startTime = (Long)request.getAttribute(“startTime”);
      System.out.println("Interceptor执行耗时: "+ (System.currentTimeMillis() - startTime));

    }

    /**

    • 处理请求的方法执行完毕后运行此方法

    • @param request

    • @param response

    • @param handler

    • @param exception

    • @throws Exception
      */
      @Override
      public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception exception) throws Exception {
      System.out.println(“afterCompletion方法执行”);
      Long startTime = (Long)request.getAttribute(“startTime”);
      System.out.println("Interceptor执行耗时: "+ (System.currentTimeMillis() - startTime));

      System.out.println(“exception is :”+exception);
      }
      }


步骤二:新建一个Web的配置类继承WebMvcConfigurationSupport



package security.demo.web.config;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.web.servlet.FilterRegistrationBean;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurationSupport;
import security.demo.web.filter.OtherTimeFilter;
import security.demo.web.interceptor.TimeInterceptor;

/**

  • Filter:无法获取处理请求的方法信息

  • springboot中filter的配置和顺序执行:https://www.cnblogs.com/douJiangYouTiao888/p/9183095.html

  • 配置类:配置过滤器和拦截器,如果同时配置了拦截器和过滤器,二者都会起作用
    */
    @Configuration
    public class WebConfig extends WebMvcConfigurationSupport {

    @Autowired
    private TimeInterceptor timeInterceptor;

    /**

    • FilterRegistrationBean是用来注册Filter的类

    • @return
      */
      @Bean
      public FilterRegistrationBean otherTimeFilter(){

      FilterRegistrationBean registrationBean = new FilterRegistrationBean();
      //将自定义的没有@Component注解的过滤器注册进来
      registrationBean.setFilter(new OtherTimeFilter());
      //设置过滤顺序
      registrationBean.setOrder(2);
      //设置过滤器的名字
      registrationBean.setName(“OtherTimeFilter”);
      //设置需要过滤的地址
      registrationBean.addUrlPatterns(“/*”);
      return registrationBean;

    }

    /**

    • 注册拦截器
    • @param registry
      */
      @Override
      protected void addInterceptors(InterceptorRegistry registry) {
      registry.addInterceptor(timeInterceptor);
      }
      }

#### (3)切片(Aspect)(3-8节内容) **(最常使用)**


**优点: 可以获取方法信息及方法里的参数信息。**


**缺点: 无法直接获取方法的请求及响应对象。**


**(通过下面代码可以间接获取方法的请求及响应对象)**



ServletRequestAttributes attribute = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
HttpServletRequest request = attribute.getRequest();
HttpServletResponse response = attribute.getResponse();


**常使用在日志,事务,请求参数安全验证等**



package security.demo.web.aspect;

import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.springframework.stereotype.Component;

@Aspect
@Component
public class TimeAspect {

/**
 * (* security.demo.web.controller.UserController.*(..)):
 * 第一个星号表示任何类型的返回值,第二个星号是UserController类里任何的方法
 * (..):表示方法里的任何参数
 * 整个的意思是作用在UserController的任何方法上,且不论方法的参数是什么,并且不管返回的是什么类型的方法上(即UserController的所有方法上)
 * ProceedingJoinPoint:ProceedingJoinPoint的对象包含了被拦截的方法的所有信息
 */
@Around("execution(* security.demo.web.controller.UserController.*(..))")
public Object handleControllerMethod(ProceedingJoinPoint pjp) throws Throwable {
    System.out.println("time aspect start!");
    //获取被拦截的方法
     Object proceed = pjp.proceed();
     //获取方法的参数的数组
     Object[] args = pjp.getArgs();
     for(Object arg : args){
         System.out.println("参数是: "+arg);
     }
    System.out.println("time aspect end!");

    return proceed;
}

}



**三者在程序中的执行顺序:**


①Filter  ②Interceptor  (③ControllerAdvice)  ④Aspect


**执行后的返回顺序:**


controller -> aspect -> controllerAdvice -> Interceptor -> Filter



![无](https://img-blog.csdnimg.cn/20200214234449453.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L01yX1hpTXU=,size_16,color_FFFFFF,t_70)


### 9、文件的上传下载


下载功能需要加入IO的jar包



	<!-- 处理文件上传下载的IO依赖  -->
	<dependency>
		<groupId>commons-io</groupId>
		<artifactId>commons-io</artifactId>
		<version>2.5</version>
	</dependency>

代码:



package security.demo.web.controller;

import org.apache.commons.io.IOUtils;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.multipart.MultipartFile;
import security.demo.dto.FileInfo;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.*;
import java.util.UUID;

@RestController
@RequestMapping(“/file”)
public class FileController {

/**
 * 文件上传
 * @param file
 * @return
 */
@PostMapping
public FileInfo update(MultipartFile file){
    //上传时参数的名字
    System.out.println(file.getName());
    //原始的文件名
    String fileOriginalName = file.getOriginalFilename();
    //文件的后缀名
    String fileSuffixName = fileOriginalName.substring(fileOriginalName.lastIndexOf("."));
    //文件的大小
    System.out.println(file.getSize());
    String folder = "自己指定一个路径";

    File uploadPath = new File(folder);
    if(!uploadPath.exists()){
        uploadPath.mkdirs();
    }

    File saveFilePath = new File(folder, UUID.randomUUID().toString()+fileSuffixName);
    try {
        file.transferTo(saveFilePath);
    } catch (IOException e) {
        e.printStackTrace();
    }

    FileInfo fileInfo = new FileInfo(saveFilePath.getAbsolutePath());
    return fileInfo;
}

/**
 * 文件下载,因为是写在响应response中的,所以是无返回值的
 * @param request
 * @param response
 * @param id
 */
@GetMapping("/{id}")
public void download(HttpServletRequest request, HttpServletResponse response, @PathVariable("id") String id) throws IOException {

        //输入流
        InputStream inputStream = new FileInputStream(new File("文件存储路径","文件的名字(包含后缀)"));
        //输出流
        OutputStream outputStream = response.getOutputStream();
        //设置响应中的内容类型为下载类型
        response.setContentType("application/x-download");
        //设置下载时文件的名字
        response.addHeader("Content-Disposition","attachment;filename=(设定的文件名(包含后缀名))");
        IOUtils.copy(inputStream,outputStream);

        outputStream.flush();
        //关闭流
        inputStream.close();
        outputStream.close();
}

}



### 10、异步处理REST服务


#### (1)使用Runnable异步处理Rest服务


在主线程中使用Callable来发起一个副线程来执行耗时的逻辑。(**需要注意的是副线程必须要由主线程发起调用,即主副线程之间必须要由关联**)



package security.demo.web.async;

import lombok.extern.slf4j.Slf4j;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;

import java.util.concurrent.Callable;

/**

  • @author chenlc
    */
    @RestController
    @Slf4j
    public class AsyncController {

    /**

    • 模拟同步请求处理
    • @return
    • @throws InterruptedException
      */
      @GetMapping(“/synchroOrder”)
      public String synchroOrder() throws InterruptedException {
      log.info(“主线程开始运行!”);
      Thread.sleep(1000);
      log.info(“主线程结束返回!”);
      return “success”;
      }

    /**

    • 模拟异步请求处理

    • 使用Runnable的Callable来异步处理Rest服务

    • 副线程必须要由主线程发起调用,即主副线程之间必须要由关联

    • @return

    • @throws InterruptedException
      */
      @GetMapping(“/asyncOrder”)
      public Callable asyncOrder() throws InterruptedException {
      log.info(“主线程开始运行!”);

      Callable result = new Callable(){
      @Override
      public String call() throws Exception {
      log.info(“副线程开始运行!”);
      Thread.sleep(1000);
      log.info(“副线程结束返回!”);
      return “success”;
      }
      };
      log.info(“主线程结束返回!”);
      return result;
      }

}


#### (2)使用DeferredResult异步处理Rest服务


![无](https://img-blog.csdnimg.cn/20200215154844283.PNG?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L01yX1hpTXU=,size_16,color_FFFFFF,t_70)


①:模拟一个消息队列和应用2



package security.demo.web.async;

import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;

/**

  • 模拟一个消息队列
    */
    @Component
    @Slf4j
    public class MockQueue {

    /**

    • 下单
      */
      private String placeOrder;

    /**

    • 完成订单
      */
      private String completeOrder;

    public String getPlaceOrder() {
    return placeOrder;
    }

    /**

    • 模拟一个处理下单请求的应用
    • @param placeOrder
    • @throws InterruptedException
      */
      public void setPlaceOrder(String placeOrder){
      new Thread(() ->{
      log.info(“接到下单请求!”);
      //模拟处理下单请求
      // try {
      // Thread.sleep(1000);
      // } catch (InterruptedException e) {
      // e.printStackTrace();
      // }
      //模拟完成订单请求

最后

2020年在匆匆忙忙慌慌乱乱中就这么度过了,我们迎来了新一年,互联网的发展如此之快,技术日新月异,更新迭代成为了这个时代的代名词,坚持下来的技术体系会越来越健壮,JVM作为如今是跳槽大厂必备的技能,如果你还没掌握,更别提之后更新的新技术了。

更多JVM面试整理:

本文已被CODING开源项目:【一线大厂Java面试题解析+核心总结学习笔记+最新讲解视频+实战项目源码】收录

需要这份系统化的资料的朋友,可以点击这里获取

blic String getPlaceOrder() {
return placeOrder;
}

/**
 * 模拟一个处理下单请求的应用
 * @param placeOrder
 * @throws InterruptedException
 */
public void setPlaceOrder(String placeOrder){
    new Thread(() ->{
        log.info("接到下单请求!");
        //模拟处理下单请求

// try {
// Thread.sleep(1000);
// } catch (InterruptedException e) {
// e.printStackTrace();
// }
//模拟完成订单请求

最后

2020年在匆匆忙忙慌慌乱乱中就这么度过了,我们迎来了新一年,互联网的发展如此之快,技术日新月异,更新迭代成为了这个时代的代名词,坚持下来的技术体系会越来越健壮,JVM作为如今是跳槽大厂必备的技能,如果你还没掌握,更别提之后更新的新技术了。

[外链图片转存中…(img-Zu3i28vE-1715470455290)]

更多JVM面试整理:

[外链图片转存中…(img-WnL3w9Om-1715470455290)]

本文已被CODING开源项目:【一线大厂Java面试题解析+核心总结学习笔记+最新讲解视频+实战项目源码】收录

需要这份系统化的资料的朋友,可以点击这里获取

  • 25
    点赞
  • 30
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值