每日学习:DAY24
日常开发与学习记录
前言
怎么感觉自己越来越懒了。
日程
忘记写了,大概是早上做了 SQL 表单,晚上写了 DispatcherController
。
学习记录
操作系统
- 页面分配置换策略
学习内容
省流
- SQL 表单构建
- 关于嵌套注解的生效机制
DispatcherController
统一路由接收/转发器
1. SQL 表单构建
表结构设计
每张表的通用表单项:创建时间,更新时间。
-
用户表
CREATE TABLE 用户 (id INT PRIMARY KEY,姓名 VARCHAR(50),用户名 VARCHAR(50) UNIQUE,密码 VARCHAR(100),身份标识符 INT CHECK (身份标识符 IN (0, 1, 2)) -- 0: 学生,1:老师,2:管理员 );
-
学生班级关联表
CREATE TABLE 学生班级关联 (id INT PRIMARY KEY,学生id INT,班级id INT,FOREIGN KEY (学生id) REFERENCES 用户(id),FOREIGN KEY (班级id) REFERENCES 班级(id) );
-
教师课程关联表
CREATE TABLE 教师课程关联 (id INT PRIMARY KEY,教师id INT,课程id INT,FOREIGN KEY (教师id) REFERENCES 用户(id),FOREIGN KEY (课程id) REFERENCES 课程(id) );
-
班级表
CREATE TABLE 班级 (id INT PRIMARY KEY,名称 VARCHAR(100) );
-
题目表
CREATE TABLE 题目 (id INT PRIMARY KEY,题目类型标识符 INT CHECK (题目类型标识符 IN (0, 1)), -- 0:选择题,1:简答题描述 TEXT,题干 TEXT,答案 TEXT,难度 INT,分值 INT,创建者id INT,FOREIGN KEY (创建者id) REFERENCES 用户(id) );
-
练习列表表
CREATE TABLE 练习列表 (id INT PRIMARY KEY,名称 VARCHAR(100),课程id INT,开始时间 DATETIME,截止时间 DATETIME,状态 INT CHECK (状态 IN (0, 1, 2)), -- 0: 未开始, 1: 进行中, 2: 已结束创建者id INT,FOREIGN KEY (课程id) REFERENCES 课程(id),FOREIGN KEY (创建者id) REFERENCES 用户(id) );
-
练习题目表
CREATE TABLE 练习题目 (id INT PRIMARY KEY,练习id INT,题目id INT,排序号 INT,FOREIGN KEY (练习id) REFERENCES 练习列表(id),FOREIGN KEY (题目id) REFERENCES 题目(id) );
-
学生答题记录表
CREATE TABLE 学生答题记录表 (id INT PRIMARY KEY,学生id INT,练习id INT,题目id INT,提交答案 TEXT,得分 INT,批改状态 INT CHECK (批改状态 IN (0, 1)), -- 0: 未批改, 1: 已批改批改备注 TEXT,批改时间 DATETIME,提交时间 DATETIME,FOREIGN KEY (学生id) REFERENCES 用户(id),FOREIGN KEY (练习id) REFERENCES 练习列表(id),FOREIGN KEY (题目id) REFERENCES 题目(id) );
-
练习班级表
CREATE TABLE 练习班级 (id INT PRIMARY KEY,练习id INT,班级id INT,FOREIGN KEY (练习id) REFERENCES 练习列表(id),FOREIGN KEY (班级id) REFERENCES 班级(id) );
-
学期表
CREATE TABLE 学期 (id INT PRIMARY KEY,名称 VARCHAR(100),开始时间 DATETIME,截至时间 DATETIME );
-
课程表
CREATE TABLE 课程 (id INT PRIMARY KEY,名称 VARCHAR(100),学期id INT,FOREIGN KEY (学期id) REFERENCES 学期(id) );
-
提醒表
CREATE TABLE 提醒表 (id INT PRIMARY KEY,目标用户id INT,发起者id INT,生效时间 DATETIME,内容 TEXT,FOREIGN KEY (目标用户id) REFERENCES 用户(id),FOREIGN KEY (发起者id) REFERENCES 用户(id) );
核心层级关系
课程 → 练习 → 题目
(1对多) (1对多)
具体关联方式
层级 | 关联表 | 关键字段 | 说明 |
---|---|---|---|
课程层 | 课程 | 学期id | 课程属于特定学期 |
教师课程关联 | 教师id +课程id | 定义教师授课权限 | |
练习层 | 练习列表 | 课程id | 练习必须归属于一个课程 |
练习班级 | 练习id +班级id | 练习可分配给多个班级 | |
题目层 | 练习题目 | 练习id +题目id | 题目可被多个练习复用 |
数据流动示例
2. 关于嵌套注解的生效机制
想简化注解,我理解的注解机制和实际的有不少偏差。
元注解
元注解(Meta-annotation)是注解在其他注解上的注解。例如:
@Inherited
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
@KatComponent
public @interface KatController {String value() default "";
}
默认情况下注解不具备继承性。@Inherited
只对类注解有效,对方法/字段注解无效。
@Inherited // 只有加上这个注解才能被继承
public @interface MyAnnotation {}
直接获取
// 只能获取直接注解,不会获取元注解
if (clazz.isAnnotationPresent(KatComponent.class)) {...}
递归获取元注解
// 获取所有注解(包括元注解)
Annotation[] annotations = clazz.getAnnotations();// 递归检查元注解
public static boolean isAnnotationPresent(Class<?> targetClass, Class<? extends Annotation> annotationClass) {// 检查直接注解if (targetClass.isAnnotationPresent(annotationClass)) {return true;}// 检查元注解for (Annotation annotation : targetClass.getAnnotations()) {if (annotation.annotationType().isAnnotationPresent(annotationClass)) {return true;}}return false;
}
在 Spring Boot 中,实现了 AnnotatedElementUtils
来进行多级注解查找,而且还支持注解属性合并:子注解可以覆盖元注解的属性值。
3. DispatcherController
统一路由接收/转发器
包装类设置
// 路由表: 路径 -> 控制器方法映射
private final Map<RouteKey, HandlerMethod> routeMappings = new ConcurrentHashMap<>();// 路由键定义
private record RouteKey(String path, String httpMethod) {private RouteKey(String path, String httpMethod) {this.path = PathUtils.normalize(path);this.httpMethod = httpMethod.toUpperCase();}@Overridepublic boolean equals(Object o) {if (this == o) return true;if (o == null || getClass() != o.getClass()) return false;RouteKey routeKey = (RouteKey) o;return path.equals(routeKey.path) &&httpMethod.equals(routeKey.httpMethod);}@Overridepublic String toString() {return httpMethod + " " + path;}
}// 处理器方法封装
private record HandlerMethod(Object controllerInstance, Method method) {}
初始化路由表
@Override
public void init(){// 获取ContainerFactory进而获取BeanRegistrythis.containerFactory = (ContainerFactory) getServletContext().getAttribute("ContainerFactory");// 构建路由表buildRouteMappings();
}private void buildRouteMappings() {// 获取所有控制器类(标记了@KatComponent和@Controller)containerFactory.getRegistry().getClassRegistry().forEach((beanName, clazz) -> {if (clazz.isAnnotationPresent(KatController.class)) {registerControllerRoutes(clazz, containerFactory.getBean(clazz));}});
}
路由表的构建
private void registerControllerRoutes(Class<?> controllerClass, Object controllerInstance) {// 类级别的路径String basePath = "";if (controllerClass.isAnnotationPresent(KatRequestMapping.class)) {basePath = controllerClass.getAnnotation(KatRequestMapping.class).path();}// 方法级别的映射for (Method method : controllerClass.getDeclaredMethods()) {if (method.isAnnotationPresent(KatRequestMapping.class)) {KatRequestMapping mapping = method.getAnnotation(KatRequestMapping.class);String fullPath = PathUtils.normalize(basePath + mapping.path());String httpMethod = mapping.method().toUpperCase();RouteKey key = new RouteKey(fullPath, httpMethod);routeMappings.put(key, new HandlerMethod(controllerInstance, method));}}
}
请求处理
@Override
protected void service(HttpServletRequest req, HttpServletResponse resp)throws IOException {// 标准化请求路径String path = PathUtils.normalize(req.getRequestURI().replace(req.getContextPath(), ""));String method = req.getMethod().toUpperCase();RouteKey key = new RouteKey(path, method);// 查找处理器HandlerMethod handler = routeMappings.get(key);if (handler == null) {resp.sendError(HttpServletResponse.SC_NOT_FOUND);return;}// 调用处理器方法try {Object result = handler.method.invoke(handler.controllerInstance, req, resp);// 处理返回结果ServletUtils.sendResponse(resp, Result.success(result));} catch (Exception e) {handleError(resp, e);}
}
结语
被蚊子轮着咬。
天生万物以养人,人滋血性以养蚊。