共计 30895 个字符,预计需要花费 78 分钟才能阅读完成。
前言
不得不说 SpringMVC 的设计奇妙,如果应用 Servlet 原生自带的 API,光是办法转发就有够头疼麻烦的,间接看代码如下:
public class BaseServlet extends HttpServlet {
private static final long serialVersionUID = -68576590380714085L;
@Override
protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {doPost(request, response);
}
@Override
protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
try {
// 获取申请参数 action 的值
String action = request.getParameter("action");
System.out.println("action:" + action);
// 通过 action 值,反射以后对象调用以该值命名的办法
if (action == null || action.length() == 0) {Result result = new Result(false, "短少 action 参数");
printResult(response, result);
return;
}
Class<?> clazz = this.getClass();
Method method = clazz.getMethod(action, HttpServletRequest.class, HttpServletResponse.class);
method.invoke(this, request, response);
} catch (Exception ex) {ex.printStackTrace();
Result result = new Result(false, "action 参数不正确,未匹配到 web 办法");
printResult(response, result);
}
}
/**
* 公共办法
* 输入 JSON 到客户端
* @param resp
* @param result
* @throws ServletException
* @throws IOException
*/
protected void printResult(HttpServletResponse resp, Result result) throws ServletException, IOException {resp.setContentType("application/json;charset=utf-8");
JSON.writeJSONString(resp.getWriter(), result);
}
}
1.0 版本:定义父类 Servlet,所有子 Servlet 集成父类,依据办法参数进行具体子 Servlet 的办法调用,弊病的话,1:每个 servlet 都要继承,2:办法名反复问题等等,在此角度上咱们进行本人的一个简化版本的 MVC 框架的实现,基于注解和反射进行;
Web.xml 配置
这里配置次要是配置一个总控制器,须要留神 url-pattern 匹配规定;
<?xml version="1.0" encoding="UTF-8"?>
<web-app xmlns="http://xmlns.jcp.org/xml/ns/javaee"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://xmlns.jcp.org/xml/ns/javaee http://xmlns.jcp.org/xml/ns/javaee/web-app_3_1.xsd"
version="3.1">
<servlet>
<servlet-name>default</servlet-name>
<servlet-class>com.itheima.study.mvc.controller.CenterServlet</servlet-class>
</servlet>
<servlet-mapping>
<servlet-name>default</servlet-name>
<url-pattern>/</url-pattern>
</servlet-mapping>
</web-app>
注解自定义
注解次要是两个注解:
Controller:表明须要组件类
RequestMapping:办法映射门路
Controller:
package com.itheima.study.mvc.anno;
import java.lang.annotation.*;
/**
* 模拟 springmvc RequestMapping
* @author lijie
* @date 2020-08-01
* @version v1.0.0
*/
@Documented
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
public @interface Controller {
/** DESC:映射门路 */
String value() default "";}
RequestMapping:
package com.itheima.study.mvc.anno;
import java.lang.annotation.*;
/**
* 模拟 springmvc RequestMapping
* @author lijie
* @date 2020-08-01
* @version v1.0.0
*/
@Inherited
@Documented
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface RequestMapping {
/** DESC:映射门路 */
String value();}
扫描包工具
public final class ClassScanner {private ClassScanner() { }
/**
* 取得包上面的所有的 class
* @param
* @return List 蕴含所有 class 的实例
*/
public static List<Class<?>> getClasssFromPackage(String packageName) {List<Class<?>> clazzs = new ArrayList<>();
// 是否循环搜寻子包
boolean recursive = true;
// 包名对应的门路名称
String packageDirName = packageName.replace('.', '/');
Enumeration<URL> dirs;
try {dirs = Thread.currentThread().getContextClassLoader().getResources(packageDirName);
while (dirs.hasMoreElements()) {URL url = dirs.nextElement();
String protocol = url.getProtocol();
if ("file".equals(protocol)) {String filePath = URLDecoder.decode(url.getFile(), "UTF-8");
findClassInPackageByFile(packageName, filePath, recursive, clazzs);
}
}
} catch (Exception e) {e.printStackTrace();
}
return clazzs;
}
/**
* 在 package 对应的门路下找到所有的 class
*/
private static void findClassInPackageByFile(String packageName, String filePath, final boolean recursive, List<Class<?>> clazzs) {File dir = new File(filePath);
if (!dir.exists() || !dir.isDirectory()) {return;}
// 在给定的目录下找到所有的文件,并且进行条件过滤
File[] dirFiles = dir.listFiles(new FileFilter() {
@Override
public boolean accept(File file) {boolean acceptDir = recursive && file.isDirectory();// 承受 dir 目录
boolean acceptClass = file.getName().endsWith("class");// 承受 class 文件
return acceptDir || acceptClass;
}
});
for (File file : dirFiles) {if (file.isDirectory()) {findClassInPackageByFile(packageName + "." + file.getName(), file.getAbsolutePath(), recursive, clazzs);
} else {String className = file.getName().substring(0, file.getName().length() - 6);
try {clazzs.add(Thread.currentThread().getContextClassLoader().loadClass(packageName + "." + className));
} catch (Exception e) {e.printStackTrace();
}
}
}
}
}
自定义实现
思路:
- 扫描具体包下的所有类
- 判断是否为 Controller 标识的类
- 判断办法是否蕴含 RequestMapping 注解类
- 匹配对应的 映射门路,执行对应办法
拓展:
- 对匹配门路的欠缺
- Tomcat 启动的时候将类扫描加载进一个全局容器,缩小每次申请时匹配的性能耗费
拓展临时就想到这些,前期有工夫能够进行深层次的批改;
/**
* TODO
* Created with IntelliJ IDEA.
* @author lijie
* @date 2020-08-01
* @version v1.0.0
*/
public class CenterServlet extends HttpServlet {
private static final long serialVersionUID = -7316297883149893301L;
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {this.doPost(req, resp);
}
@Override
protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {String contextPath = req.getContextPath();
String requestPath = req.getServletPath();
System.out.println("全文门路:contextPath -->" + contextPath);
System.out.println("申请门路:requestPath -->" + requestPath);
List<Class<?>> classList = ClassScanner.getClasssFromPackage("com.study.mvc.controller");
for (Class<?> item : classList) {
// 不蕴含 Controller 注解则不持续进行
boolean isController = item.isAnnotationPresent(Controller.class);
if (!isController) {continue;}
// 类办法为空不继续执行
Method[] methods = item.getMethods();
if (methods == null || methods.length <= 0) {continue;}
// 遍历类办法,获取映射注解,判断办法是否存在注解;Object instance = null;
for (Method method : methods) {boolean annotationPresent = method.isAnnotationPresent(RequestMapping.class);
if (annotationPresent) {String value = method.getAnnotation(RequestMapping.class).value();
if (Objects.equals(value, requestPath)) {
// 提早加载避免结构无意义对象
if (instance == null) {
try {instance = item.newInstance();
} catch (InstantiationException | IllegalAccessException e) {System.out.println("实例化对象产生异样:");
e.printStackTrace();}
}
// 执行办法
try {method.invoke(instance, req, resp);
} catch (IllegalAccessException | InvocationTargetException e) {System.out.println("办法调用产生了异样:");
e.printStackTrace();}
}
}
}
}
ResponseUtils.printResult(resp, new Result(false, "申请门路有误!!"));
}
}
增强实现 – 容器治理
为了更近一步模拟 SpringMVC,这次增强了容器治理,在 Tomcat 启动的时候将门路、办法、以及 Controller 加载进去容器中;以便于后续的复用
1】增加配置文件
mvc.base.package=com.study.mvc.controller
2】定义全局的容器
/**
* Created with IntelliJ IDEA.
* @author lijie
* @date 2020-08-01
* @version v1.0.0
*/
public class ContextUtils {
/**
* Servlet 启动时存储对应的 @Contoller 的 ClassBean
*/
public static final Map<String, Object> BEAN_MAP = new HashMap<>();
/**
* Servlet 启动时存储对应的 @RequestMapping 的门路和对应的办法
*/
public static final Map<String, Method> URL_MAP = new HashMap<>();}
3】增加监听器
/**
* Servlet 启动监听类
* Created with IntelliJ IDEA.
* @author lijie
* @date 2020-08-01
* @version v1.0.0
*/
@WebListener("全局监听器")
public class ContextListener implements ServletContextListener {
/** DESC:读取配置文件,加载配置文件 */
private static final Properties PROPERTIES = new Properties();
static {InputStream is = ContextListener.class.getClassLoader().getResourceAsStream("app.properties");
try {PROPERTIES.load(is);
} catch (IOException e) {System.out.println("类初始化资源谬误!");
}
}
@Override
public void contextInitialized(ServletContextEvent sce) {System.out.println("@@@@@@@@@@@@@@@@@@@@@@@@@@@ 容器初始化开始 @@@@@@@@@@@@@@@@@@@@@@@@@@@");
String basePakcage = PROPERTIES.getProperty("mvc.base.package");
List<Class<?>> classList = HmClassScanner.getClasssFromPackage(basePakcage);
for (Class<?> classItem : classList) {
// 不蕴含 Controller 注解则不持续进行
boolean isController = classItem.isAnnotationPresent(Controller.class);
if (!isController) {continue;}
// 类办法为空不继续执行
Method[] methods = classItem.getMethods();
if (methods == null || methods.length <= 0) {continue;}
// 存储 Controller Bean
String finalControllerValue = null;
String controllerValue = classItem.getAnnotation(Controller.class).value();
if (controllerValue.isEmpty() || Objects.equals(controllerValue, "")) {finalControllerValue = classItem.getSimpleName();
} else {finalControllerValue = controllerValue;}
// 判断是否存在反复的 Bean;否则存入 BEAN 容器中
Object o = ContextUtils.BEAN_MAP.get(finalControllerValue);
if (o != null) {throw new RuntimeException(finalControllerValue + "---> 存在反复的 Bean");
}
try {ContextUtils.BEAN_MAP.put(finalControllerValue, classItem.newInstance());
} catch (InstantiationException | IllegalAccessException e) {System.out.println("实例化对象谬误 ---- >" + e.getMessage());
}
// 将所有门路注册在门路容器中
for (Method method : methods) {boolean annotationPresent = method.isAnnotationPresent(RequestMapping.class);
if (!annotationPresent) {continue;}
String value = method.getAnnotation(RequestMapping.class).value();
if (value == null || value.isEmpty() || Objects.equals(value, "")) {continue;}
ContextUtils.URL_MAP.put(value, method);
}
}
System.out.println("==================== Bean ====================");
for (Map.Entry<String, Object> bean : ContextUtils.BEAN_MAP.entrySet()) {String key = bean.getKey();
Object value = bean.getValue();
System.out.println(key + "---->" + value);
}
System.out.println("==================== Bean ====================\n");
System.out.println("==================== URL ====================");
for (Map.Entry<String, Method> url : ContextUtils.URL_MAP.entrySet()) {String key = url.getKey();
Object value = url.getValue();
System.out.println(key + "---->" + value);
}
System.out.println("==================== URL ====================\n");
System.out.println("@@@@@@@@@@@@@@@@@@@@@@@@@@@ 容器初始化胜利 @@@@@@@@@@@@@@@@@@@@@@@@@@@");
}
@Override
public void contextDestroyed(ServletContextEvent sce) {System.out.println("@@@@@@@@@@@@@@@@@@@@@@@@@@@ 容器销毁中 @@@@@@@@@@@@@@@@@@@@@@@@@@@");
ContextUtils.URL_MAP.clear();
ContextUtils.BEAN_MAP.clear();
System.out.println("@@@@@@@@@@@@@@@@@@@@@@@@@@@ 容器销毁胜利 @@@@@@@@@@@@@@@@@@@@@@@@@@@");
}
}
4】批改总控制器
/**
* TODO
* Created with IntelliJ IDEA.
* @author lijie
* @date 2020-08-01
* @version v1.0.0
*/
public class CenterServlet extends HttpServlet {
private static final long serialVersionUID = -7316297883149893301L;
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {this.doPost(req, resp);
}
@Override
protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {String contextPath = req.getContextPath();
String requestPath = req.getServletPath();
System.out.println("全文门路:contextPath -->" + contextPath);
System.out.println("申请门路:requestPath -->" + requestPath);
// 获取办法名称
Method method = ContextUtils.URL_MAP.get(requestPath);
if (method == null) {ResponseUtils.printResult(resp, new Result(false, "申请门路有误!!"));
return;
}
// 获取改办法所在类的注解名称,获取类实例
String beanFinalKey = null;
Class<?> declaringClass = method.getDeclaringClass();
String controllerValue = declaringClass.getAnnotation(Controller.class).value();
if (controllerValue.isEmpty() || Objects.equals(controllerValue, "")) {beanFinalKey = declaringClass.getSimpleName();
} else {beanFinalKey = controllerValue;}
Object o = ContextUtils.BEAN_MAP.get(beanFinalKey);
// 通过实例调用办法
try {method.invoke(o, req, resp);
} catch (IllegalAccessException | InvocationTargetException e) {e.printStackTrace();
}
}
}
5】增加测试方法:
@Controller
public class TestCourseController {@RequestMapping("/test/add")
public void add(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {System.out.println("获取客户端申请...");
System.out.println("调用 Service... 实现增加");
System.out.println("响应客户端...");
Result result = new Result(true, "增加学科胜利");
ResponseUtils.printResult(resp, result);
}
// 删除学科
public void delete(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
// 获取前端申请数据
System.out.println("获取客户端申请...");
System.out.println("调用 Service... 实现删除");
System.out.println("响应客户端...");
Result result = new Result(true, "删除学科胜利");
// super.printResult(resp, result);
}
// 更新学科
public void update(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
// 获取前端申请数据
System.out.println("获取客户端申请...");
System.out.println("调用 Service... 实现更新");
System.out.println("响应客户端...");
Result result = new Result(true, "更新学科胜利");
// super.printResult(resp, result);
}
// 查问学科
public void query(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
// 获取前端申请数据
System.out.println("获取客户端申请...");
System.out.println("调用 Service... 实现查问");
System.out.println("响应客户端...");
CourseService courseService = new CourseServiceImpl();
Result result = new Result(true, "查问学科胜利", courseService.findAll());
// super.printResult(resp, result);
}
}
完整版自定义 MVC 思路
思路:
筹备工作:
- 提供一个工具类,扫描某个包下所有的字节码文件类;
- 编写注解、@RequestMapping、@Compenent,用来形容类和办法门路;
- 编写注解、@AutoSetter,用来进行主动注入;
正式工作:
- 编写配置文件,动静配置包门路;
-
监听器:Servlet 启动的时候创立,依据配置文件扫描包下的字节码文件
- 调用工具类办法扫描字节码文件,获取容器实例,注册到
ServetContext
域中,实例过程:1】将所有
@Compenent
的类信息,类实例,注册到 Map 的 Bean 容器中,同时单例对立治理2】将所有
@Compenent
类下,带有@RequestMapping
的办法进行解析 value 的地址值,注册办法到 Method 容器中3】将所有
@Compenent
类下,带有@AutoSetter
下的字段值获取 Bean 容器的实例化对象反射注入赋值
- 调用工具类办法扫描字节码文件,获取容器实例,注册到
- 过滤器:在申请达到具体的办法前对申请体、响应体进行对立的 UTF- 8 编码操作
-
顶级 Servlet(dispartherServlet):
- 1】Servlet 初始化操作,获取容器对象;
- 2】客户端申请发送解决流程:
获取申请门路,req.getServletPath();{残缺门路:req.getRequestURL(), 带 contextPath 门路:req.getRequestURI()}
通过申请门路获取容器中的办法,并应用反射技术执行改办法
实现:
[这边应用原生 Sevlet3.0 应用全注解的形式代替,Web.xml]()
①:注解定义
这里三个注解定义模拟 SpringMVC
@AutoSetter
:简化版本的注入注解@Component
:申明组件@RequestMapping
:映射办法注册
@Target(ElementType.FIELD)
@Retention(RetentionPolicy.RUNTIME)
public @interface AutoSetter {String value();
}
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
public @interface Component {String value() default "";
}
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface RequestMapping {String value();
}
②:定义 Bean
见到名识意,别离保留形容组件对象以及映射门路所在的办法;
@Data
@AllArgsConstructor
public class ComponentBean {
/** Desced -- Bean 在容器中的 id */
private String id;
/** Desced -- Bean 的全限定名称 */
private String className;//
/** Desced -- Bean 的类的字节码对象信息 */
private Class<?> clazz;
/** Desced -- Bean 的实例对象 */
private Object instance;
}
@Data
@AllArgsConstructor
public class ComponentMethod {
/** DESC:以后对象某一办法实例 */
private Method method;
/** DESC:以后对象类的字节码实例 */
private Class<?> clazz;
/** DESC:对象 */
private Object instance;
}
③:常量类定义
public class AppConst {
/**
* 存储 ApplicationContext 的 Key
**/
public static final String APPLICATION_CONTEXT = "ApplicationContext";
/**
* 资源文件名称
*/
public static final String APPLICATION_CONFIG_NAME = "application.properties";
/**
* 属性名
**/
public static final String APPLICATION_CONFIG_PACKAGE_PROP = "mvc.base.package";
}
④:ApplicationContext 容器外围类
- 传入配置文件名称,在结构的时候初始化 methodMaps、beanMaps 保留 Bean 和映射办法;
- initBean 办法次要是将所有的
@component
注解标注进行实例化,并且保留到 BeanMap 容器中,底层应用 ConcurrentHashMap;- initBeanField 次要对所有 须要注入的成员变量进行注入,并且将映射门路所在的办法注入到 MethodMap 中
/**
* @author:seanyang
* @date:Created in 2019/8/9
* @description:利用上下文
* 存储注解类的实例
* 存储注解办法的实例
* @version: 1.0
*/
@Slf4j
@Getter
public class ApplicationContext {
private static Properties properties;
/** DESC:定义容器,解析办法注解,存储映射地址与办法实例 */
private final Map<String, ComponentMethod> methodMaps;
/** DESC:定义容器,解析类注解,存储 bean 实例 */
private final Map<String, ComponentBean> beanMaps;
/**
* Desced: 初始化容器 <br>
* @param path 配置文件门路
* @author lijie
* @date 2020/8/6 22:27
*/
public ApplicationContext(String path) throws AppException {log.debug("path:{}", path);
// 创立容器对象、加载配置文件(约定配置文件寄存在类加载门路下)
beanMaps = new ConcurrentHashMap<>();
methodMaps = new ConcurrentHashMap<>();
InputStream is = ApplicationContext.class.getClassLoader().getResourceAsStream(path);
// 初始化 Bean、进行主动注入
try {this.initBean(is);
this.initBeanField();} catch (Exception e) {e.printStackTrace();
throw new AppException("初始化上下文失败," + e.getMessage());
}
}
/**
* Desced: 依据资源加载字节码文件并进行注册 <br>
* @param resource 资源文件
* @author lijie
* @date 2020/8/6 22:29
*/
private void initBean(InputStream resource) throws Exception {
// 取得 component-scan 标签的根本包名称
if (resource == null) {throw new AppException("本地配置文件资源为空!");
}
// 资源加载
Properties properties = new Properties();
properties.load(resource);
String pakcageName = properties.getProperty(AppConst.APPLICATION_CONFIG_PACKAGE_PROP);
if (pakcageName == null) {return;}
// 获取字节码文件,注册应用了 @Component 的 Bean
List<Class<?>> classsFromPackage = ClassScannerUtils.getClasssFromPackage(pakcageName);
if (null != classsFromPackage && classsFromPackage.size() > 0) {for (Class<?> aClass : classsFromPackage) {
// 判断是否应用的 @HmComponent 注解
if (aClass.isAnnotationPresent(Component.class)) {
// 取得该类上的注解对象
Component component = aClass.getAnnotation(Component.class);
// 判断属性是否赋值 如果 Component 没有值 就赋值为以后类名
String beanId = "".equals(component.value()) ? aClass.getSimpleName() : component.value();
// 创立 BeanProperty 存储到 beanMaps 中
ComponentBean myBean = new ComponentBean(beanId, aClass.getName(), aClass, aClass.newInstance());
this.beanMaps.put(beanId, myBean);
}
}
}
}
/**
* 读取类成员属性注解,并初始化注解
* @throws Exception
*/
private void initBeanField() throws Exception {if (this.beanMaps == null || this.beanMaps.size() == 0) {return;}
for (Map.Entry<String, ComponentBean> entry : this.beanMaps.entrySet()) {ComponentBean bean = entry.getValue();
Object instance = bean.getInstance();
Class<?> clazz = bean.getClazz();
Field[] declaredFields = clazz.getDeclaredFields();
// 成员实例进行主动注入
if (declaredFields != null && declaredFields.length > 0) {for (Field declaredField : declaredFields) {if (declaredField.isAnnotationPresent(AutoSetter.class)) {String injectionBeanId = declaredField.getAnnotation(AutoSetter.class).value();
Object injectionBean = this.beanMaps.get(injectionBeanId).getInstance();
declaredField.setAccessible(true);
declaredField.set(instance, injectionBean);
}
}
}
// 注册映射门路
Method[] methods = clazz.getMethods();
if (methods != null && methods.length > 0) {for (Method method : methods) {if (method.isAnnotationPresent(RequestMapping.class)) {String requestPath = method.getAnnotation(RequestMapping.class).value();
ComponentMethod cMethod = new ComponentMethod(method, clazz, instance);
methodMaps.put(requestPath, cMethod);
}
}
}
}
}
}
⑤:上下文监听器:容器什么时候结构注册?Tomcat 启动的时候,就进行结构注入;
/**
* @author:seanyang
* @date:Created in 2019/8/9
* @description:容器上下文监听器
* 负责加载配置文件
* 依据配置文件加载资源
* @version: 1.0
*/
@Slf4j
@WebListener("ContextLoaderListener")
public class ContextListener implements ServletContextListener {
@Override
public void contextInitialized(ServletContextEvent servletContextEvent) {log.debug("ContextLoaderListener contextInitialized......");
// 取得以后 ServletContext,获取配置初始化配置文件门路参数
ServletContext servletContext = servletContextEvent.getServletContext();
try {
// 创立容器对象,将容器对象存储在 servletContext 域中
ApplicationContext applicationContext = new ApplicationContext(AppConst.APPLICATION_CONFIG_NAME);
servletContext.setAttribute(AppConst.APPLICATION_CONTEXT, applicationContext);
} catch (Exception e) {e.printStackTrace();
}
}
@Override
public void contextDestroyed(ServletContextEvent servletContextEvent) {}}
⑥:外围转发器 DispatherServlet:
次要做了两件事:
- 拦挡所有
.do
结尾的申请- 依据申请的门路,从 MethodMap 容器中获取办法以及对象实例进行执行转发,并将后果用
application/json;charset=utf-8
返回;
/**
* @author:lijie
* @description:申请转发控制器,负责把所有客户端的申请门路,转发调用对应的控制器类实例的具体方法
* @version: 1.0
*/
@WebServlet(
name = "contextServlet",
urlPatterns = "*.do",
loadOnStartup = 1,
description = "外围控制器",
displayName = "My MVC ContextServlet"
)
public class DispatherServlet extends HttpServlet {
private static final long serialVersionUID = 6091161103788682549L;
// 读取上下文信息
private ApplicationContext applicationContext;
@Override
public void init(ServletConfig config) throws ServletException {super.init(config);
ServletContext servletContext = config.getServletContext();
applicationContext = (ApplicationContext) servletContext.getAttribute(AppConst.APPLICATION_CONTEXT);
}
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {super.doGet(req, resp);
}
@Override
protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
// 解析申请地址 例如:/mm/xxx.do
String servletPath = req.getServletPath();
if (servletPath.endsWith("/")) {servletPath = servletPath.substring(0, servletPath.lastIndexOf("/"));
}
int lastIndex = servletPath.indexOf(".do");
if (lastIndex != -1) {servletPath = servletPath.substring(0, lastIndex);
}
String mappingPath = servletPath;
// 依据门路,找到 HmMethod 对象,并调用控制器的办法
JSONObject object = new JSONObject();
object.put("flag", false);
ComponentMethod cMethod = applicationContext.getMethodMaps().get(mappingPath);
if (Objects.nonNull(cMethod)) {
// 取出办法资源进行执行
Method method = cMethod.getMethod();
try {Object invoke = method.invoke(cMethod.getInstance(), req, resp);
this.printResult(resp, invoke);
} catch (Exception e) {object.put("message", e.getMessage());
this.printResult(resp, object);
}
} else {object.put("message", "申请门路有误,mappingPath =" + mappingPath);
this.printResult(resp, object);
}
}
private void printResult(HttpServletResponse response, Object obj) throws IOException {response.reset();
response.setContentType("application/json;charset=utf-8");
JSON.writeJSONString(response.getWriter(), obj);
}
}
⑦:附加 EncodingFilter,字符串编码过滤器:
/**
* 字符集过滤器
* 对立解决申请与响应字符集
* 默认 utf-8
*/
@WebFilter(
description = "",
filterName = "characterEncodingFilter",
urlPatterns = "/*",
initParams = {@WebInitParam(name = "encoding", value = "UTF-8")}
)
public class EncodingFilter implements Filter {
/** DESC:定义变量存储编码 */
private String encoding;
@Override
public void init(FilterConfig filterConfig) throws ServletException {encoding = filterConfig.getInitParameter("encoding") == null ? encoding : filterConfig.getInitParameter("encoding");
}
@Override
public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {servletRequest.setCharacterEncoding(encoding);
servletResponse.setContentType("text/html;charset=" + encoding);
filterChain.doFilter(servletRequest, servletResponse);
}
@Override
public void destroy() {}
}
问题:
问题 1:控制器门路问题
解决方案①:拦挡*.do、*.htm
,例如:/user/add.do,这是最传统的形式,最简略也最实用。不会导致动态文件(jpg,js,css)被拦挡。
解决方案②:拦挡 /,例如:/user/add,能够实现当初很风行的 REST 格调。很多互联网类型的利用很喜爱这种格调的 URL;弊病:会导致动态文件(jpg,js,css)被拦挡后不能失常显示。想实现 REST 格调,事件就是麻烦一些。
解决方案③:拦挡 /*,这是一个谬误的形式,匹配任意前缀,申请能够走到 Action 中,但转到 jsp 时再次被拦挡,不能拜访到 jsp。
问题 2:Transient 关键字和 @Transient 注解
1、Java 语言的关键字,变量修饰符,如果用 transient 申明一个实例变量,当对象存储时,它的值不须要维持。换句话来说就是,用 transient 关键字标记的成员变量不参加序列化过程。
2、Hibernate 注解,实体类中应用了 @Table 注解后,想要增加表中不存在的字段,就要应用 @Transient 这个注解了。应用 @Transient 示意该属性并非是一个要映射到数据库表中的字段, 只是起辅助作用.ORM 框架将会疏忽该属性
问题 3:Lombok 注解 @Builder 踩坑
// 如果对象为空, 并且有进行成员变量的初始化赋值,会产生 NPE;// 如果没有进行成员变量的初始化赋值则不会
Dict build = null;
if (build == null) {build = Dict.builder().build();}
System.out.println(build == null);
System.out.println(build);
增加自定义权限校验器
如果通过了下面的自定义 MVC 思路的实现的话,其实对这种权限校验的自定义实现也并不是特地的难。首先咱们须要一个根底的 RBAC 权限模型,如下:
用户表 <-> 角色表 === 用户角色表
角色表 <-> 权限表 === 角色权限表
①:用户的革新,在用户登录后查问出用户所领有的所有权限
@Override
public Result<User> login(User param) {String username = param.getUsername();
Assert.notBlank(username, "用户名为空");
String password = param.getPassword();
Assert.notBlank(password, "明码为空");
User user = this.findByUserName(param.getUsername());
if (user == null) {return Result.faild("登录失败,用户不存在!");
}
if (!Objects.equals(param.getPassword(), user.getPassword())) {return Result.faild("登录失败,账号或者明码不正确!");
}
// 查问用户的权限
SqlSession session = super.getSession();
UserMapper dao = super.getDao(session, UserMapper.class);
List<String> permission = dao.selectUserPermission(user.getId());
if (CollUtil.isNotEmpty(permission)) {List<String> collect = permission.stream().distinct().collect(Collectors.toList());
user.setAuthorityList(collect);
}
super.closeSession(session);
return Result.success(user);
}
这里为了简略起见,就一条 SQL 关联查问出了用户权限,因为用户和角色是多对多的关系,一个用户多个角色,角色和权限也是多对多的关系,一个角色多个权限,那么间接的一个用户也是领有多个权限的,x = y 那么 y = x;反着站在用户独自立场想,一个用户有多个角色(一对多),一个角色有多个权限;
-- 语句 1(程序保障用户的存在)SELECT tp.keyword
FROM t_permission tp
LEFT JOIN tr_role_permission trp ON trp.permission_id = tp.id
LEFT JOIN t_role tr ON tr.id = trp.role_id
LEFT JOIN tr_user_role tur ON tur.role_id = tr.id
WHERE tur.user_id = #{userId}
-- 语句 2
SELECT tp.* FROM t_role tr
LEFT JOIN tr_user_role tur ON tur.role_id = tr.id
LEFT JOIN tr_role_permission trp ON trp.role_id = tr.id
LEFT JOIN t_permission tp ON tp.id = trp.permission_id
WHERE tur.user_id = 1
-- 语句 3(通过语句保障用户的确关联存在)SELECT t.*,tr.*,tp.* FROM t_user t
LEFT JOIN tr_user_role tur ON tur.user_id = t.id -- 关联以后用户的所有角色
LEFT JOIN t_role tr ON tr.id = tur.role_id -- 关联以后角色的所有一一对应的信息
LEFT JOIN tr_role_permission trp ON trp.role_id = tr.id -- 关联角色对应的所有权限信息
LEFT JOIN t_permission tp ON tp.id = trp.permission_id -- 关联每个角色一一对应的权限信息
WHERE t.id = 1
②:定义本人的权限注解
/**
* Desc:受权注解
* @author lijie
* @date 2020-08-07
* @version v1.0.0
*/
@Documented
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface HasPerm {String value() default "";
}
③:封装权限 Bean
@Data
@AllArgsConstructor
public class ComponentPerm {
/**
* Desc: 权限
*/
private String permission;
/**
* Desc: 权限领有的办法
*/
private ComponentMethod componentMethod;
}
④:定义 SecurityContext 容器组件
@Slf4j
@Getter
public class SecurityContext {
/**
* Desc: 权限 Map
*/
private final Map<String, ComponentPerm> permMaps;
/**
* Desc: 初始化平安容器 <br>
* @param context
* @return null
* @author lijie
* @date 2020/8/7 13:42
*/
public SecurityContext(ApplicationContext context) throws AppException {log.info("context --> {}", context);
permMaps = new ConcurrentHashMap<>();
if (context == null) {throw new AppException("初始化权限容器失败,ApplicationContext is null!");
}
try {this.initSecurityBean(context);
} catch (Exception e) {e.printStackTrace();
throw new AppException("初始化权限容器失败" + e.getMessage());
}
}
/**
* Desc: 初始化 SecurityBean <br>
* @param context
* @return void
* @author lijie
* @date 2020/8/7 13:47
*/
private void initSecurityBean(ApplicationContext context) {
// 判断 MethodMaps 中是否蕴含数据,权限注解肯定是应用再映射注解之上的
Map<String, ComponentMethod> methodMaps = context.getMethodMaps();
if (methodMaps == null && methodMaps.size() < 0) {return;}
for (Map.Entry<String, ComponentMethod> entry : methodMaps.entrySet()) {ComponentMethod componentMethod = entry.getValue();
Method originMethod = componentMethod.getMethod();
// 注册 Bean
if (originMethod.isAnnotationPresent(HasPerm.class) && originMethod.isAnnotationPresent(RequestMapping.class)) {String reqUrl = originMethod.getAnnotation(RequestMapping.class).value();
if (reqUrl == null || reqUrl.trim().isEmpty()) {return;}
String value = originMethod.getAnnotation(HasPerm.class).value();
if (value == null || value.trim().isEmpty()) {return;}
ComponentPerm componentPerm = new ComponentPerm(value, componentMethod);
permMaps.put(reqUrl, componentPerm);
}
}
}
}
⑤:外围过滤器的定义
- Filter 初始化的时候先把 SecurityContext 容器注册到 ServletContext 中
- 每次申请通过 Security 容器获取门路,如果不存在门路的办法阐明没注解,间接放行。
- 存在的话须要去判断会话、用户、权限
/**
* Desc:权限控制器
* @author lijie
* @date 2020-08-07
* @version v1.0.0
*/
@Slf4j
@WebFilter(filterName = "PermissionFilter", urlPatterns = "/*")
public class PermissionFilter extends BaseSecurityFilter{
@Getter
private SecurityContext securityContext;
@Getter
private ApplicationContext appContext;
@Override
public void init(FilterConfig filterConfig) throws ServletException {ServletContext servletContext = filterConfig.getServletContext();
String contextPath = servletContext.getContextPath();
log.info("contextPath --> {}", contextPath);
// 初始化后的办法存入全局域中
appContext = (ApplicationContext) servletContext.getAttribute(AppConst.APPLICATION_CONTEXT);
try {securityContext = new SecurityContext(appContext);
servletContext.setAttribute(AppSecurityConst.APPLICATION_CONTEXT_SECURITYCONST, securityContext);
} catch (AppException e) {throw new ServletException(e.getMessage());
}
}
@Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {HttpServletRequest req = (HttpServletRequest) request;
HttpServletResponse res = (HttpServletResponse) response;
// 获取申请地址
String servletPath = req.getServletPath();
if (servletPath.endsWith("/")) {servletPath = servletPath.substring(0, servletPath.lastIndexOf("/"));
}
if (servletPath.endsWith(".do")) {servletPath = servletPath.substring(0, servletPath.indexOf(".do"));
}
// 依据拜访门路判断是否在拜访权限中,不在则继续执行;ComponentPerm componentPerm = securityContext.getPermMaps().get(servletPath);
if (componentPerm == null) {chain.doFilter(request, response);
return;
}
// 会话校验
log.info("进入权限校验,以后申请门路 --> {}", req.getServletPath());
HttpSession session = req.getSession(false);
if (session == null) {log.error("会话曾经过期,请从新登录!");
this.printeResult(res, null, false);
return;
}
log.info("用户 session --> {}", session);
// 用户校验
User user = (User) session.getAttribute(GlobalConst.SESSION_KEY_USER);
if (user == null) {log.error("十分申请,用户不存在!");
this.printeResult(res, null, false);
return;
}
log.info("存在 user --> {}", user);
// 权限校验
List<String> authorityList = user.getAuthorityList();
if (authorityList == null || authorityList.isEmpty()) {this.printeResult(res, "权限有余,申请失败!", true);
return;
}
log.info("权限 authorityList --> {}", authorityList);
// 是否蕴含权限
String permission = componentPerm.getPermission();
if (!authorityList.contains(permission)) {this.printeResult(res, "权限有余,申请失败!", true);
return;
}
log.info("蕴含权限 --> {}", permission);
chain.doFilter(request, response);
}
@Override
public void destroy() {}
/**
* Desc: 返回后果 <br>
* @param response
* @author lijie
* @date 2020/8/7 14:17
*/
private void printeResult(HttpServletResponse response, String message, boolean isJson) throws IOException {if (isJson) {response.setContentType("application/json;charset=utf-8");
JSON.writeJSONString(response.getWriter(), Result.faild(message));
}
else {response.sendRedirect("http://localhost:8081/mm/login.html");
}
}
}
温习 Servlet 三大组件
①:过滤器 Filter
Filter 过滤器与 Servlet 十分相似,但它具备拦挡客户端(浏览器)申请的性能,通常申请会在达到 Servlet 之前先通过 Filter 过滤,Filter 过滤器能够扭转申请中的内容,来满足理论开发中的须要。每个过滤器都要间接或者间接实现 Filter;
理论开发场景:字符编码过滤,避免 XSS 攻打,避免 SQL 注入,权限过滤 等等作用非常弱小;
实现办法如下:
== 注解配置时:多个过滤器时 Servlet 会依照名称以此执行。Web.xml 配置:依照配置程序先后执行 ==
/**
* 字符集过滤器
* 对立解决申请与响应字符集
* 默认 utf-8
*/
@WebFilter(filterName = "characterEncodingFilter",
urlPatterns = "/*",
initParams = {@WebInitParam(name = "encoding", value = "UTF-8")})
public class EncodingFilter implements Filter {
@Override
public void init(FilterConfig filterConfig) throws ServletException {// 我的项目启动时过滤器的初始化操作}
@Override
public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain) throws IOException, ServletException {
// 对每次申请进行过滤
req = (HttpServletRequest) req;
res = (HttpServletResponse) res;
xxxx
// 如果通过校验或者其余则调用
chain.doFilter(res,req);
}
@Override
public void destroy() {// 我的项目进行过滤器的一些操作,通常用来开释资源}
}
②、监听器 Listener
监听器次要是监听某个对象的的状态变动,比如说申请,会话,全局;在 Servlet 中,监听器次要分为如下三大类:
-
ServletContext
:服务器启动创立、服务器敞开销毁个别次要用于我的项目启动的时候加载一些配置,或者启动一些中间件如 MQ 客户端监听等等
@Slf4j @WebListener("ContextLoaderListener") public class ContextListener implements ServletContextListener { @Override public void contextInitialized(ServletContextEvent servletContextEvent) {ServletContext servletContext = servletContextEvent.getServletContext(); try { // 创立容器对象,将容器对象存储在 servletContext 域中 ApplicationContext applicationContext = new ApplicationContext(AppConst.APPLICATION_CONFIG_NAME); servletContext.setAttribute(AppConst.APPLICATION_CONTEXT, applicationContext); } catch (Exception e) {e.printStackTrace(); } } @Override public void contextDestroyed(ServletContextEvent servletContextEvent) {}}
-
HttpSession
:第一次调用 request.getSession 时创立,服务器敞开销毁,session 过期,手动销毁;该监听器能够用来统计网站的在线人数
/** * @author:seanyang * @date:Created in 2019/8/22 * @description:会话监听
*/
@Slf4j
@WebListener
public class MmSessionListener implements HttpSessionListener {
@Override
public void sessionCreated(HttpSessionEvent httpSessionEvent) {// Session 创立}
@Override
public void sessionDestroyed(HttpSessionEvent httpSessionEvent) {// Session 销毁}
}
- `ServletRequestListener`:每一次申请都会创立 request,申请完结销毁;统计页面的拜访人数等等,对申请进行一些非凡的操作
@Slf4j
@WebListener
public class MmRequestListener implements ServletRequestListener {
@Override
public void requestInitialized(ServletRequestEvent sre) {ServletRequest servletRequest = sre.getServletRequest();
// xxxxxxxxxxxxxxxxx 创立
}
@Override
public void requestDestroyed(ServletRequestEvent sre) {ServletRequest servletRequest = sre.getServletRequest();
// xxxxxxxxxxxxxxxxx 销毁
}
}
## 温习树节点数据组装(不递归)
// 判断查问的后果是否为空
Result<PageResult> listMethod = this.findListMethod(req, res);
List<Dict> rows = (List<Dict>) listMethod.getResult().getRows();
if (listMethod == null || rows.isEmpty()) {return Result.faild();
}
// 封装整体后果
Map<Integer, Dict> collect = rows.stream().collect(Collectors.toMap(Dict::getId, dict -> dict));
for (Dict row : rows) {
// 父 ID 为空必定是顶级元素,持续走上面的
Integer pid = row.getPid();
if (pid == null) {continue;}
// 父 ID 不为空子元素,那么通过 Map 获取父元素,设置到子元素中
List<Dict> subList = collect.get(pid).getSubList();
if (subList == null) {subList = new ArrayList<>();
}
subList.add(row);
collect.get(pid).setSubList(subList);
}
// 最初拿到所有的顶级元素即可
List<Dict> results = collect.values()
.stream()
.filter(dict -> dict.getPid() == null)
.collect(Collectors.toList());
results.forEach(System.out::println);
## Servlet 踩坑汇合
> Servlet 服务器始终无奈返回 Session 给客户端,排查了许久发现 response.reset()是罪魁祸首
>
> - reset()用于重置,然而在重置的时候也会清空相干数据,例如 session 存的信息
private void printResult(HttpServletResponse response, Object obj) throws IOException {
// response.reset()
response.setContentType("application/json;charset=utf-8");
response.setCharacterEncoding("UTF-8");
JSON.writeJSONString(response.getWriter(), obj);
}
## 结言
> 不得不说思考、看代码是一个枯燥乏味的过程,工作这么久,天天应用这个框架那个框架,忽然一回首发现最重要的基础知识曾经遗记的差不多。>
> 遇到问题只会谷歌、百度、必应,然而实际上遇到问题了,根底如果不够扎实,理解的不够透彻,基本难以解决问题,技术上也并不能进精。最开始温习的时候是最难熬的把,很多货色这里用着不不便,那里不不便,比方 Spring 帮咱们做好的注入、映射、事务、动静代理等等,不得不说轮子好用。>
> 然而同样的用久了轮子,也会遗记怎么走路了的。当初的反复造轮子更多的是对本人技术的负责,技术的最终目标还是为了能计划落地才行。当然最次要当初公司个别都谋求麻利,大多数的工夫也献给了公司,剩下的工夫玩玩泥巴可能都不够了,毕竟还有生存。