前言
不得不说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 :会话监听 * @version: 1.0 */ @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帮我们做好的注入、映射、事务、动态代理等等,不得不说轮子好用。
但是同样的用久了轮子,也会忘记怎么走路了的。现在的重复造轮子更多的是对自己技术的负责,技术的最终目的还是为了能方案落地才行。当然最主要现在公司一般都追求敏捷,大多数的时间也献给了公司,剩下的时间玩玩泥巴可能都不够了,毕竟还有生活。