关于Spring的最常见的十道面试题

面试题一:JDK动态代理和CGLib有什么区别?

JDK动态代理(JDK Proxy)和CGLib都是Spring中用于实现AOP代理的技术,但是它们之间存在以下区别:

  1. 来源不同:JDK Proxy 是Java语言自带的功能,无需通过第三方类实现。Java对JDK Proxy提供了稳定的支持,并且会持续地升级和更新JDK Proxy;而CGLib是第三方提供的工具,基于ASM(一种字节码操作框架)实现的
  2. 使用场景不同:JDK Proxy只能代理实现了接口的类,而CGLib无需实现接口,它是通过实现目标类的子类来完成调用的,所以要求代理类不能被final修饰
  3. 性能不同:JDK Proxy在JDK 7之前性能是远不如GCLib的,但是JDK 7之后性能是略高于CGLib

总结:JDK Proxy是Java自带的,在JDK高版本性能比较高的动态代理工具,但是它要求被代理类必须实现接口,它的性能在JDK 7之后是略高于CGLib;而CGLib是基于字节码技术实现的第三方动态代理,它是通过生成代理对象的子类来实现代理的,所以要求被代理类不能被final修饰

面试题二:SpringAOP默认使用的是JDK动态代理还是CGLib?

Spring默认使用的是JDK动态代理,这在官方问档中有所说明:官方文档

如下图所示:

 关于Spring的最常见的十道面试题_第1张图片

面试题三:SpringBoot中AOP默认使用的是JDK动态代理还是CGLib?如何证明这个问题?

 Spring默认使用的是JDK动态代理,这在官方问档中有所说明:官方文档

如下图所示:

 关于Spring的最常见的十道面试题_第2张图片

然而Spring Boot2.0之后默认使用的是GCLib,如图:

 关于Spring的最常见的十道面试题_第3张图片

证明Spring Boot中AOP默认使用了CGLib

//创建一个普通的目标类,该类没有实现任何接口
public class MyTargetClass {
    public void doSomething() {
        System.out.println("Doing something in the target class");
    }
}
//创建一个AOP切面类
@Aspect
@Component
public class MyAspect {
    @Before("execution(* com.example.MyTargetClass.*(..))")
    public void beforeMethod() {
        System.out.println("Before method execution");
    }
}
//创建一个Spring Boot应用类
@SpringBootApplication
public class MyApplication {
    public static void main(String[] args) {
        SpringApplication.run(MyApplication.class, args);

        MyTargetClass target = new MyTargetClass();
        target.doSomething();
    }
}

当我们运行的时候发现,是正常执行的所以 Spring Boot中AOP默认使用了CGLib

面试题四:Bean有几种注入方式?它们有什么区别?

Bean对象中有以下几种注入方式:

  1. 属性注入
  2. Setter注入
  3. 构造方法注入

属性注入

属性注入是我们最熟悉的,也是日常开发最常使用的一种注入方式,它的实现代码如下:

@RestController
public class UserController {

    @Autowired
    private UserService userService;

    @RequestMapping("/add")
    public UserInfo add(@RequestParam String username, @RequestParam String password) {
        return userService.add(username, password);
    }
}

优点:属性注入最大的优点就是实现简单、使用简单。只需要给变量添加一个@AutoWried注释,就可以在不new对象的情况下,直接获得注入对象

缺点:属性注入的缺点主要是有以下两种:

功能性问题:无法注入一个不可变的对象(final修饰)

在Java中final修饰的对象要么直接赋值,要么在构造方法中赋值,所以当使用属性注入final对象时,不符合Java中的final使用规范,所以注入不成功

通用性问题:只能适应与IoC容器,Idea也会提醒不建议使用:关于Spring的最常见的十道面试题_第4张图片

Setter注入

Setter注入的实现代码如下:

@RestController
public class UserController {

    private UserService userService;

    @Autowired
    public void setUserService(UserService userService) {
        this.userService = userService;
    }

    @RequestMapping("/add")
    public UserInfo add(@RequestParam String username, @RequestParam String password) {
        return userService.add(username, password);
    }
}

优点:它符合单一职责的设计原则(一个类应该只负责一项职责或一个功能),因为每一个Setter只针对一个对象

缺点:不能注入不可变的对象(final修饰);注入的对象可以调用多次,也就是注入对象会被修改

构造方法注入

构造方法注入是Spring官方从4.x之后推荐的注入方式,它的实现代码如下:

@RestController
public class UserController {

    private final UserService userService;

    @Autowired
    public UserController(UserService userService) {
        this.userService = userService;
    }

    @RequestMapping("/add")
    public UserInfo add(@RequestParam String username, @RequestParam String password) {
        return userService.add(username, password);
    }
}

如果当前的类只有一个构造方法,那么@Autowired有也可以省略,如:

@RestController
public class UserController {

    private final UserService userService;

    public UserController(UserService userService) {
        this.userService = userService;
    }

    @RequestMapping("/add")
    public UserInfo add(@RequestParam String username, @RequestParam String password) {
        return userService.add(username, password);
    }
}

优点:

注入不可变对象:使用构造方法可以注入不可变对象,如下代码:

注入对象不会被修改:构造方法不会像Setter注入那样,构造方法在对象创建只会执行依次,因此它不存在注入对象被随时(调用)修改的情况 

完全初始化:因为依赖对象是在构造方法中执行的,而构造方法是在对象创建之初执行的,因此被注入的对象在使用之前,会被完全初始化

通用性更好:构造注入和属性注入不同,构造方法注入可适用于任何环境,无论是IoC框架还是非IoC框架,构造方法注入的代码都是通用的

缺点:不如属性注入简单

面试题五:说一下Bean的生命周期?

Bean的生命周期是指在Spring(IoC)中从创建到销毁的过程。Bean的生命周期主要是包含以下五个流程:

  1. 实例化:Bean的实例化是指创建Bean对象的过程。这通常涉及到构造函数。在Spring中,可以通过配置文件或注解来指定如何实例化Bean
  2. 属性赋值:在Bean实例化之后,Spring 将会通过依赖注入(DI)的方式将 Bean 的属性赋值。
  3. BeanPostProcessor的前置处理:如果有注册的BeanPostProcessor实现,它们的前置处理方法将在Bean初始化之前被调用
  4. 初始化:在这个阶段,如果Bean实现了InitializingBean接口,或者配置了init-mothod,Spring容器会调用Bean的初始化方法。这是在Bean完全初始化后的阶段
  5. BeanPostProcessor的后置处理:果有注册的 BeanPostProcessor 实现,它们的后置处理方法将在 Bean 初始化之后被调用。这个阶段同样可以执行一些额外的操作
  6. Bean可用/使用:在这个阶段,Bean已经完全初始化,可以被应用程序使用
  7. 销毁:当Bean不再需要时,Spring容器会调用Bean的销毁方法

面试题六:Bean是线程安全的吗?实际工作中怎么保证其线程安全?

默认情况下,Bean是非线程安全的。因为默认情况下Bean的作用域是单例模式,那么此时,所有的请求都会共享一个Bean实例,这意味着如果这个Bean实例在多线程下,会被同时修改(成员变量),就可能出现线程安全问题

单例模式就是所有线程可见共享的,而原型模式则是每次请求都创建一个新的原型对象

单例Bean一定是非线程安全的吗

并不是,单例Bean主要是分为以下两种类型:

  1. 无状态Bean(线程安全):

    • 定义: 无状态的Bean通常是指不包含成员变量或者所有成员变量都是常量的Bean。这意味着每个方法调用都是独立的,不依赖于之前的调用结果。
    • 线程安全性: 由于没有可变的状态,无状态Bean在多线程环境中是线程安全的。多个线程可以同时调用这个Bean的方法而不会相互干扰。无状态Bean的方法执行不依赖于共享的状态。
    public class StatelessBean {
        public int add(int a, int b) {
            return a + b;
        }
    }
    
  2. 有状态Bean(非线程安全):

    • 定义: 有状态的Bean包含了可变的成员变量。多个方法调用可能会依赖于之前的调用结果,因为它们共享相同的状态。
    • 线程安全性: 有状态Bean在多线程环境中通常是非线程安全的,因为多个线程可能同时修改Bean的状态,导致数据不一致或竞态条件。
    • 需谨慎处理: 如果使用有状态Bean,需要确保在多线程环境下进行正确的同步或加锁操作,以防止竞态条件。对于有状态Bean的设计和使用需要更谨慎,确保在多线程环境中能够正确地处理状态。
    public class StatefulBean {
        private int count = 0;
    
        public int increment() {
            return count++;
        }
    }
    

如何保证线程安全

想要保证有状态Bean线程安全,可以通过以下几种方法:

  1. 使用ThreadLocal:

    • 描述: 通过 ThreadLocal,每个线程都拥有自己的变量副本,从而避免了线程安全问题。
    • 代码示例:
    public class MyThreadLocalBean {
        private static final ThreadLocal counter = new ThreadLocal<>();
    
        public int increment() {
            counter.set(counter.get() == null ? 1 : counter.get() + 1);
            return counter.get();
        }
    }
    
  2. 使用锁机制:

    • 描述: 使用 synchronized 或 ReentrantLock 等锁机制,确保对有状态Bean的修改操作是原子的,从而保证线程安全。
    • 代码示例:
    public class MySynchronizedBean {
        private int counter = 0;
        private final Object lock = new Object();
    
        public synchronized int increment() {
            return ++counter;
        }
    }
    
  3. 设置Bean为原型作用域(Prototype):

    • 描述: 将Bean的作用域设置为原型,确保每次请求该Bean都会创建一个新的实例,从而避免不同线程之间的数据冲突。
    • 代码示例:
    @Scope("prototype")
    public class MyPrototypeBean {
        private int counter = 0;
    
        public int increment() {
            return ++counter;
        }
    }
    
  4. 使用线程安全容器(Atomic):

    • 描述: 使用 Atomic 类,如 AtomicInteger,来保证线程安全。这些类提供了一些原子操作,避免了使用锁的复杂性。
    • 代码示例:
    import java.util.concurrent.atomic.AtomicInteger;
    
    public class MyAtomicBean {
        private AtomicInteger counter = new AtomicInteger(0);
    
        public int increment() {
            return counter.incrementAndGet();
        }
    }

实际工作中会使用那种方案来保证Bean的线程安全

实际工作中,通常会根据具体业务来选择合适的线程安全方案,但是以上解决线程安全的方案中:

ThreadLocal和原型作用域会使用更多的资源,占用更多的空间来保证线程安全,所以在使用时通常不会作为最佳的考虑方案

而锁机制和线程安全容器通常会优先考虑,但是需要注意的是AtomicInteger底层是乐观锁CAS实现的,因此存在乐观锁的典型问题ABA问题(如果有状态的Bean中既有++操作,又有--操作的时候,可能出现ABA问题),此时就要使用锁机制,或者AtomicStampedReference来解决ABA问题

面试题七:SpringBoot中的自动装配是啥意思?举例说明一下?

Spring Boot的自动装配是指在应用程序启动时,根据类路径下的依赖、配置文件以及预定义规则,自动配置和初始化Spring应用程序中的各种组件、模块和功能的过程

这种自动配置机制大大减少了开发人员手动配置的工作,使得开发者可以更专注注重业务逻辑的实现,同时提供了更高效、快速的应用程序启动和开发体验

例如,在Spring我们需要手动设置数据库的连接URL、用户名、密码等参数,并将其实例化为一个Bean。如下列代码:

@Configuration
public class DataSourceConfig {

    @Value("${spring.datasource.url}")
    private String url;

    @Value("${spring.datasource.username}")
    private String username;

    @Value("${spring.datasource.password}")
    private String password;

    @Bean
    public DataSource dataSource() {
        DriverManagerDataSource dataSource = new DriverManagerDataSource();
        dataSource.setDriverClassName("com.mysql.cj.jdbc.Driver");
        dataSource.setUrl(url);
        dataSource.setUsername(username);
        dataSource.setPassword(password);
        return dataSource;
    }
}

@Value 注解用于注入配置文件中的属性值。在 application.propertiesapplication.yml 中,你可以设置数据库连接的 URL、用户名和密码,例如:

spring.datasource.url=jdbc:mysql://localhost:3306/mydatabase
spring.datasource.username=username
spring.datasource.password=password

面试题八:如何实现自定义注解?实际工作中哪些地方使用到了自定义注解?

自定义注解可以标记在方法上或类上,用于编译器或运行期进行待定的业务功能处理。在Java中,自定义注解使用@interface关键字来定义,它可以实现如:日志记录、性能监控、权限校验等功能

在Spring Boot中实现一个自定义注解可以通过以下两种方式:

  1. 通过AOP实现
  2. 通过拦截器实现

实际工作中我们通常会使用自定义注解来实现如权限校验或幂等性判断等功能

幂等性判断是指在分布式系统或并发环境中,对于同一操作的多次重复请求,系统的响应结果应该是一致的。简无论接收到多少次相同的请求,系统的行为和结果都应该是相同的

面试题九:什么是拦截器?如何实现拦截器?

拦截器(Interceptor)是一种在应用程序中用于拦截、处理和转换请求和响应的组件。在Web开发中,拦截器是一种常见的技术,用于在请求到达控制器之前或响应返回浏览器之前进行干预和处理

Speing Boot中拦截器实现主要是分为以下两步:

  1. 创建一个普通的拦截器,实现HandlerInterceptor接口,并重写接口中的相关方法
  2. 将上一步创建的拦截器加入到Spring Boot的配置文件中,并配置拦截规则

创建拦截器类

创建一个实现HandlerInterceptor接口,并实现其中的方法。例如:

import org.springframework.web.servlet.HandlerInterceptor;
import org.springframework.web.servlet.ModelAndView;

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

public class MyCustomInterceptor implements HandlerInterceptor {

    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
        // 在请求处理之前执行,返回 true 则继续执行后续的拦截器和处理器方法,返回 false 则中断执行
        System.out.println("Pre-handle method is called");
        return true;
    }

    @Override
    public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception {
        // 在请求处理之后,视图渲染之前执行
        System.out.println("Post-handle method is called");
    }

    @Override
    public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
        // 在整个请求处理完毕,视图渲染完毕后执行
        System.out.println("After-completion method is called");
    }
}

配置拦截器

创建一个配置类,继承WebMvcConfigurerAdapter或实现WebMvcConfigurer接口,并重写addInterceptors。例如:

import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;

@Configuration
public class MyInterceptorConfig implements WebMvcConfigurer {

    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        registry.addInterceptor(new MyCustomInterceptor())
                .addPathPatterns("/my/**")  // 设置拦截路径
                .excludePathPatterns("/my/exclude");  // 设置排除拦截路径
    }
}

面试题十:什么是过滤器?如何实现过滤器?

过滤器(Filter)是一种常见的 Web 组件,用于在 Servlet 容器中对请求和响应进行预处理和后处理。过滤器提供了一种在请求和响应的处理链上干预和修改数据的机制,可以用于实现一些与应用程序业务逻辑无关的通用功能

过滤器可以使用 Servlet 3.0 提供的 @WebFilter 注解,配置过滤的 URL 规则,然后再实现 Filter 接口,重写接口中的 doFilter 方法。具体实现代码如下:

@WebFilter(
    filterName = "timingFilter",
    urlPatterns = "/*",
    initParams = {
        // 可以配置一些初始化参数
    }
)
public class TimingFilter implements Filter {

    @Override
    public void init(FilterConfig filterConfig) throws ServletException {
        // 初始化操作,这里暂时不需要进行额外的初始化
    }

    @Override
    public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
        // 在请求处理前记录请求的时间戳
        long startTime = System.currentTimeMillis();

        // 调用 chain.doFilter() 将请求传递给下一个过滤器或 Servlet
        chain.doFilter(request, response);

        // 在请求处理后记录请求的处理时间
        long endTime = System.currentTimeMillis();
        long processingTime = endTime - startTime;

        System.out.println("Request processed in " + processingTime + " milliseconds.");
    }

    @Override
    public void destroy() {
        // 资源释放操作,这里暂时不需要进行额外的销毁
    }
}

你可能感兴趣的:(面试题,java,spring,spring,boot,面试)