Spring Framework & Spring Boot - Câu hỏi phỏng vấn Senior Engineer
1. Dependency Injection và IoC Container
Câu hỏi:
"Hãy giải thích Dependency Injection trong Spring. So sánh constructor injection, setter injection và field injection. Bạn recommend cách nào và tại sao?"
Câu trả lời:
Dependency Injection là design pattern cho phép inject dependencies từ bên ngoài thay vì tự tạo trong class.
IoC (Inversion of Control) Container quản lý lifecycle và dependencies của beans.
Ví dụ so sánh các loại injection:
// 1. Constructor Injection (RECOMMENDED)
@Service
public class UserService {
private final UserRepository userRepository;
private final EmailService emailService;
private final ValidationService validationService;
// Single constructor - no @Autowired needed since Spring 4.3
public UserService(UserRepository userRepository,
EmailService emailService,
ValidationService validationService) {
this.userRepository = userRepository;
this.emailService = emailService;
this.validationService = validationService;
}
// All dependencies are immutable and guaranteed to be non-null
public User createUser(CreateUserRequest request) {
validationService.validate(request);
User user = userRepository.save(new User(request));
emailService.sendWelcomeEmail(user);
return user;
}
}
// 2. Setter Injection (Less preferred)
@Service
public class OrderService {
private PaymentService paymentService;
private InventoryService inventoryService;
@Autowired
public void setPaymentService(PaymentService paymentService) {
this.paymentService = paymentService;
}
@Autowired
public void setInventoryService(InventoryService inventoryService) {
this.inventoryService = inventoryService;
}
// Dependencies can be null, need null checks
public Order processOrder(OrderRequest request) {
if (paymentService == null) {
throw new IllegalStateException("PaymentService not initialized");
}
// Process order...
return new Order();
}
}
// 3. Field Injection (NOT RECOMMENDED)
@Service
public class NotificationService {
@Autowired
private EmailService emailService; // Hard to test, hidden dependencies
@Autowired
private SmsService smsService;
// Cannot ensure dependencies are set during construction
// Difficult to test without Spring context
}
// Configuration example
@Configuration
@EnableJpaRepositories
@ComponentScan(basePackages = "com.company.service")
public class ApplicationConfig {
@Bean
@Primary
public EmailService primaryEmailService() {
return new SmtpEmailService();
}
@Bean
@Qualifier("backup")
public EmailService backupEmailService() {
return new SendGridEmailService();
}
// Conditional bean creation
@Bean
@ConditionalOnProperty(name = "feature.advanced-validation", havingValue = "true")
public ValidationService advancedValidationService() {
return new AdvancedValidationService();
}
@Bean
@ConditionalOnMissingBean(ValidationService.class)
public ValidationService basicValidationService() {
return new BasicValidationService();
}
}
Tại sao Constructor Injection tốt nhất: - Immutable dependencies - Fail-fast initialization - Easy testing without Spring - Circular dependency detection - Clear dependencies trong constructor
2. Spring Boot Auto-Configuration
Câu hỏi:
"Hãy giải thích cơ chế Auto-Configuration trong Spring Boot. Làm thế nào để tạo custom auto-configuration cho một library?"
Câu trả lời:
Ví dụ tạo Custom Auto-Configuration:
// 1. Configuration Properties
@ConfigurationProperties(prefix = "notification")
@Data
public class NotificationProperties {
private boolean enabled = true;
private String provider = "email";
private Email email = new Email();
private Sms sms = new Sms();
@Data
public static class Email {
private String host;
private int port = 587;
private String username;
private String password;
private boolean starttls = true;
}
@Data
public static class Sms {
private String apiKey;
private String apiSecret;
private String defaultSender;
}
}
// 2. Service Interfaces
public interface NotificationService {
void send(String to, String subject, String message);
}
@ConditionalOnProperty(name = "notification.provider", havingValue = "email")
public class EmailNotificationService implements NotificationService {
private final NotificationProperties.Email emailConfig;
public EmailNotificationService(NotificationProperties properties) {
this.emailConfig = properties.getEmail();
}
@Override
public void send(String to, String subject, String message) {
// Email sending implementation
System.out.println("Sending email to: " + to);
}
}
@ConditionalOnProperty(name = "notification.provider", havingValue = "sms")
public class SmsNotificationService implements NotificationService {
private final NotificationProperties.Sms smsConfig;
public SmsNotificationService(NotificationProperties properties) {
this.smsConfig = properties.getSms();
}
@Override
public void send(String to, String subject, String message) {
// SMS sending implementation
System.out.println("Sending SMS to: " + to);
}
}
// 3. Auto Configuration Class
@Configuration
@EnableConfigurationProperties(NotificationProperties.class)
@ConditionalOnProperty(name = "notification.enabled", havingValue = "true", matchIfMissing = true)
public class NotificationAutoConfiguration {
private final NotificationProperties properties;
public NotificationAutoConfiguration(NotificationProperties properties) {
this.properties = properties;
}
@Bean
@ConditionalOnProperty(name = "notification.provider", havingValue = "email")
@ConditionalOnMissingBean(NotificationService.class)
public NotificationService emailNotificationService() {
return new EmailNotificationService(properties);
}
@Bean
@ConditionalOnProperty(name = "notification.provider", havingValue = "sms")
@ConditionalOnMissingBean(NotificationService.class)
public NotificationService smsNotificationService() {
return new SmsNotificationService(properties);
}
@Bean
@ConditionalOnBean(NotificationService.class)
public NotificationHealthIndicator notificationHealthIndicator(NotificationService service) {
return new NotificationHealthIndicator(service);
}
}
// 4. Health Indicator
public class NotificationHealthIndicator implements HealthIndicator {
private final NotificationService notificationService;
public NotificationHealthIndicator(NotificationService notificationService) {
this.notificationService = notificationService;
}
@Override
public Health health() {
try {
// Test notification service
return Health.up()
.withDetail("provider", "available")
.build();
} catch (Exception ex) {
return Health.down()
.withDetail("error", ex.getMessage())
.build();
}
}
}
File spring.factories:
# src/main/resources/META-INF/spring.factories
org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
com.company.notification.config.NotificationAutoConfiguration
Cách sử dụng:
# application.yml
notification:
enabled: true
provider: email
email:
host: smtp.gmail.com
port: 587
username: ${SMTP_USERNAME}
password: ${SMTP_PASSWORD}
3. Spring AOP (Aspect-Oriented Programming)
Câu hỏi:
"Implement một custom annotation để log execution time và cache results. Giải thích các loại advice khác nhau trong Spring AOP."
Câu trả lời:
Ví dụ Custom AOP Implementation:
// 1. Custom Annotations
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface LogExecutionTime {
String value() default "";
boolean includeArgs() default false;
}
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface Cacheable {
String value() default "";
long ttl() default 300; // seconds
}
// 2. Logging Aspect
@Aspect
@Component
@Slf4j
public class LoggingAspect {
// Pointcut for all methods with @LogExecutionTime
@Pointcut("@annotation(logExecutionTime)")
public void logExecutionTimePointcut(LogExecutionTime logExecutionTime) {}
// Around advice for execution time logging
@Around("logExecutionTimePointcut(logExecutionTime)")
public Object logExecutionTime(ProceedingJoinPoint joinPoint,
LogExecutionTime logExecutionTime) throws Throwable {
long startTime = System.currentTimeMillis();
String methodName = joinPoint.getSignature().getName();
String className = joinPoint.getTarget().getClass().getSimpleName();
try {
Object result = joinPoint.proceed();
long endTime = System.currentTimeMillis();
long executionTime = endTime - startTime;
String logMessage = String.format("Method %s.%s executed in %d ms",
className, methodName, executionTime);
if (logExecutionTime.includeArgs()) {
Object[] args = joinPoint.getArgs();
logMessage += " with args: " + Arrays.toString(args);
}
log.info(logMessage);
return result;
} catch (Exception ex) {
long endTime = System.currentTimeMillis();
log.error("Method {}.{} failed after {} ms: {}",
className, methodName, (endTime - startTime), ex.getMessage());
throw ex;
}
}
// Before advice for method entry logging
@Before("execution(* com.company.service.*.*(..))")
public void logMethodEntry(JoinPoint joinPoint) {
log.debug("Entering method: {}.{}",
joinPoint.getTarget().getClass().getSimpleName(),
joinPoint.getSignature().getName());
}
// After returning advice
@AfterReturning(pointcut = "execution(* com.company.service.*.*(..))",
returning = "result")
public void logMethodExit(JoinPoint joinPoint, Object result) {
log.debug("Method {}.{} returned: {}",
joinPoint.getTarget().getClass().getSimpleName(),
joinPoint.getSignature().getName(),
result);
}
// After throwing advice for exception handling
@AfterThrowing(pointcut = "execution(* com.company.service.*.*(..))",
throwing = "ex")
public void logException(JoinPoint joinPoint, Exception ex) {
log.error("Exception in method {}.{}: {}",
joinPoint.getTarget().getClass().getSimpleName(),
joinPoint.getSignature().getName(),
ex.getMessage(), ex);
}
}
// 3. Caching Aspect
@Aspect
@Component
@Slf4j
public class CachingAspect {
private final CacheManager cacheManager;
public CachingAspect(CacheManager cacheManager) {
this.cacheManager = cacheManager;
}
@Around("@annotation(cacheable)")
public Object cache(ProceedingJoinPoint joinPoint, Cacheable cacheable) throws Throwable {
String cacheKey = generateCacheKey(joinPoint, cacheable);
String cacheName = cacheable.value().isEmpty() ? "default" : cacheable.value();
Cache cache = cacheManager.getCache(cacheName);
if (cache != null) {
Cache.ValueWrapper cached = cache.get(cacheKey);
if (cached != null) {
log.debug("Cache hit for key: {}", cacheKey);
return cached.get();
}
}
log.debug("Cache miss for key: {}", cacheKey);
Object result = joinPoint.proceed();
if (cache != null && result != null) {
cache.put(cacheKey, result);
log.debug("Cached result for key: {}", cacheKey);
}
return result;
}
private String generateCacheKey(ProceedingJoinPoint joinPoint, Cacheable cacheable) {
StringBuilder keyBuilder = new StringBuilder();
keyBuilder.append(joinPoint.getTarget().getClass().getSimpleName())
.append(".")
.append(joinPoint.getSignature().getName());
Object[] args = joinPoint.getArgs();
if (args.length > 0) {
keyBuilder.append("(")
.append(Arrays.stream(args)
.map(arg -> arg != null ? arg.toString() : "null")
.collect(Collectors.joining(",")))
.append(")");
}
return keyBuilder.toString();
}
}
// 4. Service using the annotations
@Service
public class UserService {
private final UserRepository userRepository;
public UserService(UserRepository userRepository) {
this.userRepository = userRepository;
}
@LogExecutionTime(value = "Finding user by ID", includeArgs = true)
@Cacheable(value = "users", ttl = 600)
public User findById(Long id) {
// Simulate expensive operation
try {
Thread.sleep(100);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
return userRepository.findById(id)
.orElseThrow(() -> new UserNotFoundException("User not found: " + id));
}
@LogExecutionTime("Creating new user")
public User createUser(CreateUserRequest request) {
User user = new User();
user.setEmail(request.getEmail());
user.setName(request.getName());
return userRepository.save(user);
}
@LogExecutionTime(value = "Complex user search", includeArgs = true)
@Cacheable(value = "userSearch", ttl = 300)
public List<User> searchUsers(String query, int page, int size) {
// Complex search logic
return userRepository.findByNameContainingIgnoreCase(query,
PageRequest.of(page, size)).getContent();
}
}
// 5. Configuration
@Configuration
@EnableAspectJAutoProxy
@EnableCaching
public class AopConfig {
@Bean
public CacheManager cacheManager() {
CaffeineCacheManager cacheManager = new CaffeineCacheManager();
cacheManager.setCaffeine(Caffeine.newBuilder()
.maximumSize(1000)
.expireAfterWrite(10, TimeUnit.MINUTES));
return cacheManager;
}
}
Các loại Advice: - @Before: Execute trước method - @After: Execute sau method (regardless of outcome) - @AfterReturning: Execute khi method return successfully - @AfterThrowing: Execute khi method throws exception - @Around: Wrap around method execution (most powerful)
4. Spring Security Implementation
Câu hỏi:
"Implement JWT authentication với role-based authorization trong Spring Security. Làm thế nào để handle security cho microservices?"
Câu trả lời:
Ví dụ JWT Security Implementation:
// 1. JWT Utility
@Component
public class JwtUtil {
@Value("${app.jwt.secret}")
private String jwtSecret;
@Value("${app.jwt.expiration}")
private int jwtExpirationMs;
public String generateToken(UserDetails userDetails) {
return generateTokenFromUsername(userDetails.getUsername());
}
public String generateTokenFromUsername(String username) {
return Jwts.builder()
.setSubject(username)
.setIssuedAt(new Date())
.setExpiration(new Date(System.currentTimeMillis() + jwtExpirationMs))
.signWith(SignatureAlgorithm.HS512, jwtSecret)
.compact();
}
public String getUsernameFromToken(String token) {
return Jwts.parser()
.setSigningKey(jwtSecret)
.parseClaimsJws(token)
.getBody()
.getSubject();
}
public boolean validateToken(String token) {
try {
Jwts.parser().setSigningKey(jwtSecret).parseClaimsJws(token);
return true;
} catch (MalformedJwtException ex) {
log.error("Invalid JWT token: {}", ex.getMessage());
} catch (ExpiredJwtException ex) {
log.error("JWT token is expired: {}", ex.getMessage());
} catch (UnsupportedJwtException ex) {
log.error("JWT token is unsupported: {}", ex.getMessage());
} catch (IllegalArgumentException ex) {
log.error("JWT claims string is empty: {}", ex.getMessage());
}
return false;
}
}
// 2. JWT Authentication Filter
public class JwtAuthenticationFilter extends OncePerRequestFilter {
private final JwtUtil jwtUtil;
private final CustomUserDetailsService userDetailsService;
public JwtAuthenticationFilter(JwtUtil jwtUtil, CustomUserDetailsService userDetailsService) {
this.jwtUtil = jwtUtil;
this.userDetailsService = userDetailsService;
}
@Override
protected void doFilterInternal(HttpServletRequest request,
HttpServletResponse response,
FilterChain filterChain) throws ServletException, IOException {
String jwt = parseJwt(request);
if (jwt != null && jwtUtil.validateToken(jwt)) {
String username = jwtUtil.getUsernameFromToken(jwt);
UserDetails userDetails = userDetailsService.loadUserByUsername(username);
UsernamePasswordAuthenticationToken authentication =
new UsernamePasswordAuthenticationToken(userDetails, null,
userDetails.getAuthorities());
authentication.setDetails(new WebAuthenticationDetailsSource().buildDetails(request));
SecurityContextHolder.getContext().setAuthentication(authentication);
}
filterChain.doFilter(request, response);
}
private String parseJwt(HttpServletRequest request) {
String headerAuth = request.getHeader("Authorization");
if (StringUtils.hasText(headerAuth) && headerAuth.startsWith("Bearer ")) {
return headerAuth.substring(7);
}
return null;
}
}
// 3. Custom User Details Service
@Service
public class CustomUserDetailsService implements UserDetailsService {
private final UserRepository userRepository;
public CustomUserDetailsService(UserRepository userRepository) {
this.userRepository = userRepository;
}
@Override
@Transactional
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
User user = userRepository.findByUsername(username)
.orElseThrow(() -> new UsernameNotFoundException("User not found: " + username));
return UserPrincipal.create(user);
}
}
// 4. User Principal
public class UserPrincipal implements UserDetails {
private Long id;
private String username;
private String email;
private String password;
private Collection<? extends GrantedAuthority> authorities;
public UserPrincipal(Long id, String username, String email, String password,
Collection<? extends GrantedAuthority> authorities) {
this.id = id;
this.username = username;
this.email = email;
this.password = password;
this.authorities = authorities;
}
public static UserPrincipal create(User user) {
List<GrantedAuthority> authorities = user.getRoles().stream()
.map(role -> new SimpleGrantedAuthority("ROLE_" + role.getName().name()))
.collect(Collectors.toList());
return new UserPrincipal(
user.getId(),
user.getUsername(),
user.getEmail(),
user.getPassword(),
authorities
);
}
@Override
public Collection<? extends GrantedAuthority> getAuthorities() {
return authorities;
}
// Other UserDetails methods...
@Override
public boolean isAccountNonExpired() { return true; }
@Override
public boolean isAccountNonLocked() { return true; }
@Override
public boolean isCredentialsNonExpired() { return true; }
@Override
public boolean isEnabled() { return true; }
}
// 5. Security Configuration
@Configuration
@EnableWebSecurity
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class SecurityConfig {
private final CustomUserDetailsService userDetailsService;
private final JwtAuthenticationEntryPoint unauthorizedHandler;
private final JwtUtil jwtUtil;
public SecurityConfig(CustomUserDetailsService userDetailsService,
JwtAuthenticationEntryPoint unauthorizedHandler,
JwtUtil jwtUtil) {
this.userDetailsService = userDetailsService;
this.unauthorizedHandler = unauthorizedHandler;
this.jwtUtil = jwtUtil;
}
@Bean
public JwtAuthenticationFilter jwtAuthenticationFilter() {
return new JwtAuthenticationFilter(jwtUtil, userDetailsService);
}
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
@Bean
public AuthenticationManager authenticationManager(
AuthenticationConfiguration authConfig) throws Exception {
return authConfig.getAuthenticationManager();
}
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
http.cors().and().csrf().disable()
.exceptionHandling().authenticationEntryPoint(unauthorizedHandler).and()
.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS).and()
.authorizeHttpRequests(authz -> authz
.requestMatchers("/api/auth/**").permitAll()
.requestMatchers("/api/public/**").permitAll()
.requestMatchers(HttpMethod.GET, "/api/users/**").hasRole("USER")
.requestMatchers(HttpMethod.POST, "/api/admin/**").hasRole("ADMIN")
.anyRequest().authenticated()
);
http.addFilterBefore(jwtAuthenticationFilter(), UsernamePasswordAuthenticationFilter.class);
return http.build();
}
}
// 6. Controller with Security
@RestController
@RequestMapping("/api/users")
public class UserController {
private final UserService userService;
public UserController(UserService userService) {
this.userService = userService;
}
@GetMapping("/profile")
public ResponseEntity<UserProfile> getCurrentUserProfile(Authentication authentication) {
UserPrincipal userPrincipal = (UserPrincipal) authentication.getPrincipal();
UserProfile profile = userService.getUserProfile(userPrincipal.getId());
return ResponseEntity.ok(profile);
}
@PreAuthorize("hasRole('ADMIN')")
@GetMapping("/all")
public ResponseEntity<List<User>> getAllUsers() {
List<User> users = userService.getAllUsers();
return ResponseEntity.ok(users);
}
@PreAuthorize("hasRole('ADMIN') or #userId == authentication.principal.id")
@GetMapping("/{userId}")
public ResponseEntity<User> getUser(@PathVariable Long userId) {
User user = userService.findById(userId);
return ResponseEntity.ok(user);
}
@PostAuthorize("returnObject.username == authentication.name")
@GetMapping("/{userId}/details")
public User getUserDetails(@PathVariable Long userId) {
return userService.findById(userId);
}
}
Microservices Security: - JWT cho stateless authentication - API Gateway để centralize security - Service-to-service authentication với mTLS - Distributed session management với Redis - OAuth2/OpenID Connect cho SSO
5. Spring Data JPA Advanced
Câu hỏi:
"Implement custom repository với Specifications và Criteria API. Làm thế nào để optimize N+1 query problem?"