交大智邦后端Java笔试题
交大智邦后端Java笔试题
简答题
-
只要一个类加上了
@Component
注解,就一定能成为一个Spring Bean吗?如果不是,请举出反例。不一定
- 扫描范围不包括 com.code.lab.web.component 或者被 @ComponentScan 显式排除 通过 excludeFilters 手动排除特定类
- 被条件注解排除 @ConditionalOnProperty(name = “feature.enabled”, havingValue = “false”)
- 被@Profile(“test”)限制环境
package com.code.lab.web.component; import org.springframework.stereotype.Component; /** * @author Cai */ @Component @ConditionalOnProperty(name = "feature.enabled", havingValue = "false") @Profile("test") public class MyComponent { }
package com.code.lab.web; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; import org.springframework.context.annotation.ComponentScan; import org.springframework.scheduling.annotation.EnableAsync; /** * @author Cai */ @EnableAsync @SpringBootApplication @ComponentScan("com.code.lab.web.config",excludeFilters = @ComponentScan.Filter(type = FilterType.ASSIGNABLE_TYPE, classes = MyComponent.class)) public class SpringbootCommonsPool2DemoApplication { public static void main(String[] args) { SpringApplication.run(SpringbootCommonsPool2DemoApplication.class, args); } }
-
如果有两个相同类型,但不重名的String Bean,在引入的时候应如何处理?针对
@Autowired
和@Resource
分别作答。
package com.code.lab.web.config.thrad;
import java.util.Map;
import java.util.concurrent.*;
import java.util.concurrent.atomic.AtomicInteger;
import lombok.extern.slf4j.Slf4j;
import org.slf4j.MDC;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.task.TaskDecorator;
import org.springframework.lang.NonNull;
import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor;
/**
* @author Cai
*/
@Configuration
@Slf4j
public class EnhancedThreadPoolConfig {
/**
* 订单处理线程池(高优先级)
*
* @return 线程池
*/
@Bean("orderThreadPool")
@Primary
public ThreadPoolTaskExecutor orderThreadPool() {
// 新增关键配置
ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
executor.setCorePoolSize(4);
executor.setMaxPoolSize(8);
executor.setKeepAliveSeconds(60);
executor.setQueueCapacity(2000);
executor.setThreadFactory(new CustomThreadFactory("Order-Thread-"));
executor.setRejectedExecutionHandler(new CustomRejectPolicy());
executor.setWaitForTasksToCompleteOnShutdown(true);
executor.setAwaitTerminationSeconds(30);
// 优雅停机
executor.setWaitForTasksToCompleteOnShutdown(true);
executor.setTaskDecorator(new ContextCopyDecorator());
executor.initialize();
return executor;
}
/**
* 日志记录线程(低优先级)
*
* @return 线程池
*/
@Bean("logThreadPool")
public ThreadPoolTaskExecutor logThreadPool() {
ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
executor.setCorePoolSize(2);
executor.setMaxPoolSize(2);
executor.setQueueCapacity(1000);
executor.setThreadNamePrefix("Log-Thread-");
executor.setRejectedExecutionHandler(new ThreadPoolExecutor.DiscardPolicy());
executor.initialize();
return executor;
}
}
1. 使用 @Autowired 注入 结合 @Qualifier 指定 Bean 名称
显式指定要注入的 Bean 名称:
@Qualifier("orderThreadPool")
@Autowired
ThreadPoolTaskExecutor orderExecutor
2.使用 @Primary 标记默认 Bean
3.使用 @Resource 注入
@Resource(name="orderThreadPool")
ThreadPoolTaskExecutor orderExecutor
3.如果需要修改默认的时间输出格式,需要如何处理?(单个字段、全局)
局部
1.第一种使用 @JsonFormat(Jackson 序列化)
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8")
private LocalDateTime createTime;
2:使用 @DateTimeFormat(Spring MVC 参数绑定)
@DateTimeFormat(pattern = "yyyy/MM/dd")
private Date startDate;
全局时间格式修改
修改 application.yml
spring:
jackson:
date-format: yyyy-MM-dd HH:mm:ss
time-zone: GMT+8
通过代码自定义全局序列化规则:
@Configuration
public class JacksonConfig {
@Bean
public ObjectMapper objectMapper() {
ObjectMapper mapper = new ObjectMapper();
// 注册 JavaTime 模块(处理 LocalDateTime 等类型)
mapper.registerModule(new JavaTimeModule());
// 全局日期格式
mapper.setDateFormat(new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"));
// 禁用时间戳格式
mapper.disable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS);
return mapper;
}
}
4.如何保证接口消息的幂等性。
同一操作多次执行的结果与一次执行的结果一致:
1.唯一标识符(ID 幂等)
2.Token 机制
3.数据库唯一约束
4.锁
5.在请求接口中开启一个线程执行高CPU操作,是否会导致接口线程阻塞?
当请求到达接口时,主线程会执行以下操作
1.创建并启动一个新线程用于执行高 CPU 操作
2.继续执行后续代码
在启动新线程后不会被阻塞,会立即释放并处理其他请求
能耗统计
在项目现场需要统计用电量,因此拟使用一块电表进行电量的采集。与家用的电表一样,此表的采集值永远是累计值。采集到的JSON数据如下:
{
"timestamp": "采集时的时间戳,数字格式",
"meterId": "电表唯一号",
"value": "采集到的结果,数字格式"
}
由于现场的不稳定,可能存在以下的特殊情况:
- 可能由于网络问题,导致采集数据失败,得到的value为0。
- 可能由于设备更换(不考虑更换期间的耗电),此累积值归零,但下一次采集的结果中,meterId会发生变化。(存在可能设备修复后继续使用)
请编写一段伪代码,以完成对此电表的采集,并按天给出使用电量。
package com.code.lab.pool;
import java.time.Instant;
import java.time.LocalDate;
import java.time.ZoneId;
import java.time.format.DateTimeFormatter;
import java.util.HashMap;
import java.util.Map;
/**
* @author Cai
*/
public class ElectricityMeterProcessor {
// 记录每个电表的上次有效读数:meterId -> (lastTimestamp, lastValue)
private final Map<String, MeterRecord> meterDb = new HashMap<>();
// 按天统计总用电量:dateStr -> totalUsage
private final Map<String, Long> dailyUsage = new HashMap<>();
private static final DateTimeFormatter DATE_FORMATTER = DateTimeFormatter.ofPattern("yyyy-MM-dd");
// 处理电表数据
public void processMeterData(MeterData data) {
// 过滤无效数据(value=0)
if (data.value == 0) {
return;
}
String meterId = data.meterId;
long currentTimestamp = data.timestamp;
long currentValue = data.value;
// 转换时间戳为日期字符串(UTC时区)
LocalDate date = Instant.ofEpochSecond(currentTimestamp).atZone(ZoneId.of("UTC")).toLocalDate();
String dateStr = date.format(DATE_FORMATTER);
// 新电表或设备更换后首次读数
if (!meterDb.containsKey(meterId)) {
meterDb.put(meterId, new MeterRecord(currentTimestamp, currentValue));
return;
}
// 获取上次记录
MeterRecord lastRecord = meterDb.get(meterId);
long lastValue = lastRecord.value;
// 计算增量
long increment;
if (currentValue < lastValue) {
// 设备重置,视为新设备开始累计
increment = currentValue;
} else {
// 正常增量
increment = currentValue - lastValue;
}
// 更新电表记录
meterDb.put(meterId, new MeterRecord(currentTimestamp, currentValue));
// 累加当日用电量
dailyUsage.merge(dateStr, increment, Long::sum);
}
// 获取按天统计结果
public Map<String, Long> getDailyUsage() {
return new HashMap<>(dailyUsage);
}
// 电表数据类
public static class MeterData {
long timestamp; // 时间戳(秒级)
String meterId; // 电表ID
long value; // 电表读数
public MeterData(long timestamp, String meterId, long value) {
this.timestamp = timestamp;
this.meterId = meterId;
this.value = value;
}
}
// 电表记录类(保存上次读数和时间)
private static class MeterRecord {
long timestamp;
long value;
public MeterRecord(long timestamp, long value) {
this.timestamp = timestamp;
this.value = value;
}
}
// 测试示例
public static void main(String[] args) {
ElectricityMeterProcessor processor = new ElectricityMeterProcessor();
// 示例数据流(时间戳为秒级)
// 2023-10-01,初始值
processor.processMeterData(new MeterData(1696118400L, "A", 100));
// 2023-10-02,+200
processor.processMeterData(new MeterData(1696204800L, "A", 300));
// 无效数据(过滤)
processor.processMeterData(new MeterData(1696291200L, "A", 0));
// 2023-10-03,设备重置 +50
processor.processMeterData(new MeterData(1696291200L, "A", 50));
// 无效数据(过滤)
processor.processMeterData(new MeterData(1696377600L, "B", 0));
// 2023-10-05,新电表初始值
processor.processMeterData(new MeterData(1696464000L, "B", 200));
// 输出结果
System.out.println(processor.getDailyUsage());
}
}
权限管理
有一套系统,可以对用户、权限、菜单进行管理,即通过配置用户的权限、权限可访问的菜单,完成对用户操作区域的管理。
- 请设计上述内容的表结构,并完成service部分的伪代码,以及所使用的SQL语句(或使用JPA表达)。
- 请完成接口鉴权的伪代码。(假设同一菜单的URL前缀一致)
#用户表 (user)
CREATE TABLE user (
id BIGINT PRIMARY KEY AUTO_INCREMENT,
username VARCHAR(50) UNIQUE NOT NULL,
password VARCHAR(100) NOT NULL
);
#权限表 (permission)
CREATE TABLE permission (
id BIGINT PRIMARY KEY AUTO_INCREMENT,
name VARCHAR(50) NOT NULL, -- 如 "用户管理-查询"
code VARCHAR(50) UNIQUE NOT NULL -- 如 "user:read"
);
#菜单表 (menu)
CREATE TABLE menu (
id BIGINT PRIMARY KEY AUTO_INCREMENT,
name VARCHAR(50) NOT NULL, -- 如 "用户管理"
url_prefix VARCHAR(100) NOT NULL, -- 如 "/api/users"
parent_id BIGINT, -- 支持多级菜单
FOREIGN KEY (parent_id) REFERENCES menu(id)
);
#用户权限关联表 (user_permission)
CREATE TABLE user_permission (
user_id BIGINT NOT NULL,
permission_id BIGINT NOT NULL,
PRIMARY KEY (user_id, permission_id),
FOREIGN KEY (user_id) REFERENCES user(id),
FOREIGN KEY (permission_id) REFERENCES permission(id)
);
#权限菜单关联表 (permission_menu)
CREATE TABLE permission_menu (
permission_id BIGINT NOT NULL,
menu_id BIGINT NOT NULL,
PRIMARY KEY (permission_id, menu_id),
FOREIGN KEY (permission_id) REFERENCES permission(id),
FOREIGN KEY (menu_id) REFERENCES menu(id)
);
Service 伪代码与 SQL/JPA 实现
@Service
public class UserService {
@Autowired
private UserRepository userRepository;
// 为用户分配权限
public void assignPermission(Long userId, Long permissionId) {
userRepository.addUserPermission(userId, permissionId);
}
// 查询用户拥有的所有菜单
public List<Menu> getUserMenus(Long userId) {
return userRepository.findMenusByUserId(userId);
}
}
// PermissionService.java
@Service
public class PermissionService {
@Autowired
private PermissionRepository permissionRepository;
// 为权限关联菜单
public void linkMenuToPermission(Long permissionId, Long menuId) {
permissionRepository.addPermissionMenu(permissionId, menuId);
}
}
// MenuService.java
@Service
public class MenuService {
@Autowired
private MenuRepository menuRepository;
// 根据 URL 前缀查找菜单
public Menu findMenuByUrlPrefix(String urlPrefix) {
return menuRepository.findByUrlPrefix(urlPrefix);
}
}
// User.java(实体类)
@Entity
public class User {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String username;
private String password;
@ManyToMany
@JoinTable(
name = "user_permission",
joinColumns = @JoinColumn(name = "user_id"),
inverseJoinColumns = @JoinColumn(name = "permission_id")
)
private Set<Permission> permissions;
}
// Permission.java(实体类)
@Entity
public class Permission {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String name;
private String code;
@ManyToMany
@JoinTable(
name = "permission_menu",
joinColumns = @JoinColumn(name = "permission_id"),
inverseJoinColumns = @JoinColumn(name = "menu_id")
)
private Set<Menu> menus;
}
// Menu.java(实体类)
@Entity
public class Menu {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String name;
private String urlPrefix;
@ManyToOne
@JoinColumn(name = "parent_id")
private Menu parent;
}
SQL 查询示例
-- 查询用户的所有菜单(JPA 会自动生成类似 SQL)
SELECT m.* FROM menu m
JOIN permission_menu pm ON m.id = pm.menu_id
JOIN permission p ON pm.permission_id = p.id
JOIN user_permission up ON p.id = up.permission_id
WHERE up.user_id = 1;
接口鉴权伪代码
- 用户请求到达接口时,解析请求 URL。
- 根据 URL 前缀匹配对应的菜单。
- 检查用户是否有权限访问该菜单关联的权限。
// AuthInterceptor.java(拦截器伪代码)
public class AuthInterceptor implements HandlerInterceptor {
@Autowired
private MenuService menuService;
@Autowired
private UserService userService;
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) {
String requestUrl = request.getRequestURI();
// 1. 根据 URL 前缀匹配菜单
Menu menu = menuService.findMenuByUrlPrefix(requestUrl);
if (menu == null) {
// 无菜单配置,默认放行或拒绝
return true;
}
// 2. 获取当前用户 ID(假设已通过登录验证)
Long userId = (Long) request.getAttribute("currentUserId");
// 3. 检查用户是否有权限访问该菜单
List<Menu> userMenus = userService.getUserMenus(userId);
if (!userMenus.contains(menu)) {
response.sendError(403, "无权限访问");
return false;
}
return true;
}
}
分布式锁
基于spring data redis中提供的redisTempalte
,实现分布式锁的功能,要求满足:
-
能在持有锁的线程终止后,自动释放锁。
@Component public class RedisDistributedLock { @Autowired private RedisTemplate<String, String> redisTemplate; private static final String LOCK_PREFIX = "LOCK:"; private static final String LOCK_VALUE = UUID.randomUUID().toString(); // 确保唯一性 private static final long DEFAULT_EXPIRE = 30000; // 默认锁过期时间 30 秒 // 获取锁 public boolean tryLock(String lockKey, long expireMillis) { String key = LOCK_PREFIX + lockKey; return Boolean.TRUE.equals( redisTemplate.opsForValue().setIfAbsent( key, LOCK_VALUE, expireMillis > 0 ? expireMillis : DEFAULT_EXPIRE, TimeUnit.MILLISECONDS ) ); } // 释放锁(Lua 脚本保证原子性) public void unlock(String lockKey) { String key = LOCK_PREFIX + lockKey; String script = "if redis.call('get', KEYS[1]) == ARGV[1] then " + " return redis.call('del', KEYS[1]) " + "else " + " return 0 " + "end"; redisTemplate.execute( new DefaultRedisScript<>(script, Long.class), Collections.singletonList(key), LOCK_VALUE ); } }
-
能强行抢占锁,被强的线程能暂停执行,并等待锁的恢复。
public class LockInterruptHandler { @Autowired private RedisTemplate<String, String> redisTemplate; private static final String CHANNEL_PREFIX = "LOCK_INTERRUPT:"; // 发送抢占信号 public void sendInterruptSignal(String lockKey) { String channel = CHANNEL_PREFIX + lockKey; redisTemplate.convertAndSend(channel, "INTERRUPT"); } // 订阅抢占信号 public void subscribe(String lockKey, Runnable onInterrupt) { String channel = CHANNEL_PREFIX + lockKey; redisTemplate.getConnectionFactory().getConnection().subscribe( (message, pattern) -> onInterrupt.run(), channel.getBytes(StandardCharsets.UTF_8) ); } }
-
实现公平锁,即锁的持有是有序的。(扩展题,可不作答)
使用 Redis List 维护等待队列,确保按请求顺序获取锁。
public class FairRedisLock extends RedisDistributedLock {
private static final String QUEUE_PREFIX = "LOCK_QUEUE:";
private static final String QUEUE_VALUE_PREFIX = "REQUEST:";
// 获取公平锁
public boolean tryFairLock(String lockKey, long timeout) {
String queueKey = QUEUE_PREFIX + lockKey;
String requestId = QUEUE_VALUE_PREFIX + UUID.randomUUID();
// 加入等待队列
redisTemplate.opsForList().rightPush(queueKey, requestId);
try {
long start = System.currentTimeMillis();
while (System.currentTimeMillis() - start < timeout) {
// 检查是否轮到当前请求
String head = redisTemplate.opsForList().index(queueKey, 0);
if (requestId.equals(head) && tryLock(lockKey, DEFAULT_EXPIRE)) {
redisTemplate.opsForList().remove(queueKey, 0, requestId);
return true;
}
TimeUnit.MILLISECONDS.sleep(100);
}
return false;
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
return false;
} finally {
redisTemplate.opsForList().remove(queueKey, 0, requestId);
}
}
}