Day48
手写Spring-MVC之前后置处理器与异常处理
前后置处理器
概念:从服务器获取的JSON数据可能是加密后的,因此服务端获取的时候需要进行解密(前置处理器)。
而从服务器传出的JSON数据可能需要加密,因此需要在处理返回值的时候进行加密(后置处理器)。
思路:
首先搭建前后置处理器的框架:
和是否处理JSON格式的数据类似,需要根据注解判断controller层中的方法是否需要对JSON格式的数据进行解密或者返回数据进行加密,因此要添加两个注解@BeforeAdviser和@AfterAdviser
java@Target(ElementType.PARAMETER) @Retention(RetentionPolicy.RUNTIME) public @interface BeforeAdviser { } @Target(ElementType.METHOD) @Retention(RetentionPolicy.RUNTIME) public @interface AfterAdviser { }
controller层的方法需要添加相应的注解标识
java/** * 使用postman发送请求 * url:http://localhost:8080/user/test12.action * json:{"username":"zs","password":"123123"} * 传递JSON参数和返回JSON */ @RequestMapping("/test12.action") @ResponseBody @AfterAdviser public User test12(@RequestBody @BeforeAdviser User user){ System.out.println("user对象:"+ user); return user;
同时需要在参数描述类中和方法描述类中添加是否有相应注解的属性:
java/** * 参数描述类 */ @NoArgsConstructor @AllArgsConstructor @Data public class ParameterDefinition { private String name;//参数名 private Class<?> clazz;//参数类型 private int index;//参数下标 private Type[] actualTypeArguments;//参数泛型的数组 private boolean requestBodyHasOrNot;//参数上是否有@RequestBody注解 private boolean beforeAdviserHasOrNot;//参数上是否有@BeforeAdviser注解 } /** * 方法描述类 */ @NoArgsConstructor @AllArgsConstructor @Data public class MethodDefinition { private String requestMappingPath;//子级URi private String name;//方法名 private Method method;//方法对象 private Class<?> returnClazz;//返回值类型 private List<ParameterDefinition> parameterDefinitions;//参数描述类对象的集合 private boolean responseBodyHasOrNot;//方法上是否有@ResponseBody注解 private boolean afterAdviserHasOrNot;//方法上是否有@AfterAdviser注解 }
添加后监听器中封装部分也要进行相应修改:
java//获取参数上是否有@BeforeAdviser注解 boolean beforeAdviserHasOrNot = false; BeforeAdviser beforeAdviser = parameters[i].getAnnotation(BeforeAdviser.class); if(beforeAdviser!=null){ beforeAdviserHasOrNot = true; } ParameterDefinition parameterDefinition = new ParameterDefinition(parameterName, parameterType, index,actualTypeArguments,requestBodyHasOrNot,beforeAdviserHasOrNot);//封装参数描述类对象 parameterList.add(parameterDefinition);
java//获取方法上是否有@AfterAdviser注解 boolean afterAdviserHasOrNot = false; AfterAdviser afterAdviser = method.getAnnotation(AfterAdviser.class); if(afterAdviser!=null){ afterAdviserHasOrNot = true; } MethodDefinition methodDefinition = new MethodDefinition(sonUri, methodName, method, returnType, parameterList,responseBodyHasOrNot,afterAdviserHasOrNot);//封装方法描述类对象
至此,监听器就能把信息记录下来,调度的DispatcherServlet进行工作的时候就可以获取到相应的注解信息。
前置处理器的使用是在获取参数类型的时候会判断是否有@BeforeAdviser注解,如果有则代表需要进行解密操作:
java//解密 if(parameterDefinition.isBeforeAdviserHasOrNot()){ //在这里进行具体的解密操作吗? }
在处理返回值的时候会判断方法是否有@AfterAdviser注解,如果有则代表需要进行加密操作:
java//加密 if(methodDefinition.isAfterAdviserHasOrNot()){ //在这里进行加密操作吗? }
进行具体的解密和加密操作:
前后置处理器的框架搭建好之后,会发现一个问题,如果在DispatcherServlet中进行具体的解密加密的话,那么在用户使用该框架的时候,就只能使用框架所规定的解密加密,这显然不具有灵活性。不同的用户解密、加密逻辑不同,所以这里的思路是在框架中只写抽象类,在DispatcherServlet中利用多态创建抽象类的继承类对象,调用继承类对象中的解密加密方法。而在web模块中用户可以自己重写一个解密加密抽象方法,这样调用的就是用户自定义的逻辑方法了。
抽象方法:
javapublic abstract class HanderAdviserResolver { public abstract String beforeRequestBody(String reqData); public abstract String afterResponseBody(String respData); public String before(String reqData){ return beforeRequestBody(reqData); } public String after(String respData){ return afterResponseBody(respData); } }
注意:这里的抽象方法是交给用户重写的,而自己的成员方法则直接调用抽象方法,通过这种方式实现在DispatcherServlet中调用用户重写方法的逻辑。
用户自定义前后置处理器:
javapublic class BeforeAndAfterAdviser extends HanderAdviserResolver { @Override public String beforeRequestBody(String reqData) { System.out.println("解密:"+reqData); return reqData; } @Override public String afterResponseBody(String respData) { System.out.println("加密:"+respData); return respData; } }
又一个问题来了,框架中怎样拿到用户自定义的类对象呢?思路和监听器拿到controller层类对象相似,通过配置文件拿到注解类,注解类通过一个注解注明用户自定义的前后置处理器路径。这样的注解叫做使能注解,它的功能就是使得DispatcherServlet能够拿到用户自定义类。然后在DispatcherServlet中重写init()方法,在方法中获取配置文件信息,进而拿到注解类,通过注解类的注解信息拿到自定义前后置处理器类对象,调用其重写的解密加密处理方法。
使能注解:
java@Target(ElementType.TYPE) @Retention(RetentionPolicy.RUNTIME) public @interface EnableAdviser { String adviserPackage(); }
注解类:
java@EnableAdviser(adviserPackage= "com.qf.shop.web.adviser.BeforeAndAfterAdviser")
DispatcherServlet中:
javaprivate HanderAdviserResolver adviserResolver; public HanderAdviserResolver getAdviserResolver(String config){ try { Class<?> clazz = Class.forName(config); EnableAdviser enableAdviserAnnotation = clazz.getAnnotation(EnableAdviser.class); String adviserPackage = enableAdviserAnnotation.adviserPackage(); if(adviserPackage!=null){ Class<?> adviserClass = Class.forName(adviserPackage); adviserResolver = (HanderAdviserResolver) adviserClass.newInstance(); } return adviserResolver; } catch (ClassNotFoundException | InstantiationException | IllegalAccessException e) { throw new RuntimeException(e); } } @Override public void init() throws ServletException { ServletContext servletContext = this.getServletContext(); String config = servletContext.getInitParameter("config"); adviserResolver = getAdviserResolver(config); }
java//加密 if(methodDefinition.isAfterAdviserHasOrNot()){ jsonString = adviserResolver.after(jsonString); }
java//解密 if(parameterDefinition.isBeforeAdviserHasOrNot()){ jsonStr = adviserResolver.before(jsonStr); }
异常
概念:DispatcherServlet中对于全局的异常需要进行处理,而具体如何处理也是由业务决定的,换言之是用户进行定义而非框架中写死。但是框架中又需要调用处理异常的方法,如何处理?-和前后置处理器一样,通过多态,servlet调用的是用户继承框架中抽象类的类重写的方法。
抽象类:
javapublic abstract class HanderGlobalException { public abstract void handlerException(Exception err, HttpServletRequest request, HttpServletResponse response); public void hander(Exception err, HttpServletRequest request, HttpServletResponse response){ handlerException(err,request,response); } }
用户继承类:
javapublic class GlobalException extends HanderGlobalException { @Override public void handlerException(Exception err, HttpServletRequest request, HttpServletResponse response) { System.out.println("处理全局异常......"); try { request.getRequestDispatcher("/err.jsp").forward(request,response); } catch (ServletException | IOException e) { throw new RuntimeException(e); } } }
那么框架如何拿到用户自己写的处理异常类和方法呢?思路和前后置处理器一样,DispactherServlet在重写的init()方法中通过配置文件拿到配置类,配置类通过框架中写的使能注解将用户自定义的异常处理类路径告诉servlet。
使能注解:
java@Target(ElementType.TYPE) @Retention(RetentionPolicy.RUNTIME) public @interface EnableException { String exceptionPackage(); }
配置类:
java/** * 当前项目的配置类 */ @Configuration("com.qf.shop.web.controller") @EnableAdviser(adviserPackage= "com.qf.shop.web.adviser.BeforeAndAfterAdviser") @EnableException(exceptionPackage = "com.qf.shop.web.globalException.GlobalException") public class AppConfig { }
DispatcherServlet:
javaprivate HanderGlobalException globalException; public HanderGlobalException getGlobalException(String config){ try { Class<?> clazz = Class.forName(config); EnableException enableExceptionAnnotation = clazz.getAnnotation(EnableException.class); String exceptionPackage = enableExceptionAnnotation.exceptionPackage(); if(exceptionPackage!=null){ Class<?> exceptionClass = Class.forName(exceptionPackage); globalException = (HanderGlobalException) exceptionClass.newInstance(); } return globalException; } catch (ClassNotFoundException | InstantiationException | IllegalAccessException e) { throw new RuntimeException(e); } } @Override public void init() throws ServletException { ServletContext servletContext = this.getServletContext(); String config = servletContext.getInitParameter("config"); adviserResolver = getAdviserResolver(config); globalException = getGlobalException(config); }
javatry{ //调用Controller层里的某个方法 Object returnVal = method.invoke(t, args); if(returnVal!=null){ //处理返回值 handlerReturnVal(methodDefinition,returnVal,request,response,model); } }catch (Exception e){ globalException.hander(e,request,response);//处理全局异常 }
数据库模块
功能:该模块不是Spring-MVC中的一部分,是用来和数据库交互的框架。
思路:分包逐步实现JDBC。base包:根据获得的结果集实现封装功能的接口,包含结果集处理器接口和行处理器接口
java/** * 结果集处理器的接口 *实现类:BeanHandler(获取单个对象)、BeanListHandler(获取对象集合) */ public interface ResultSetHandler<T> { public T handler(ResultSet resultSet)throws SQLException,IllegalAccessException,InstantiationException, InvocationTargetException; }
java/** * 行处理器的接口 * @param <T> */ public interface RowProcessor<T> { public T toArray(ResultSet resultSet) throws SQLException; }
handler包:实现接口:
java/** * 结果集处理接口的实现类 * 操作结果集并封装实体类 * @param <T> */ public class BeanHandler<T> implements ResultSetHandler<T> { private Class beanClass; public BeanHandler(Class beanClass) { this.beanClass = beanClass; } @Override public T handler(ResultSet resultSet) throws SQLException, IllegalAccessException, InstantiationException, InvocationTargetException { ResultSetMetaData metaData = resultSet.getMetaData(); int columnCount = metaData.getColumnCount(); if(resultSet.next()){ T t = (T) beanClass.newInstance(); for (int i = 0; i < columnCount; i++) { String columnName = metaData.getColumnName(i + 1); Object columnValue = resultSet.getObject(columnName); BeanUtils.copyProperty(t,columnName,columnValue); } return t; } return null; } }
java/** * 结果集处理接口的实现类 * 处理结果集并封装为集合 * @param <T> */ public class BeanListHandler<T> implements ResultSetHandler<List<T>> { private Class beanListClass; public BeanListHandler(Class beanListClass) { this.beanListClass = beanListClass; } @Override public List<T> handler(ResultSet resultSet) throws SQLException, IllegalAccessException, InstantiationException, InvocationTargetException { List<T> list = new ArrayList<>(); ResultSetMetaData metaData = resultSet.getMetaData(); int columnCount = metaData.getColumnCount(); while(resultSet.next()){ T t = (T) beanListClass.newInstance(); for (int i = 0; i < columnCount; i++) { String columnName = metaData.getColumnName(i + 1); T columnValue = (T) resultSet.getObject(columnName); BeanUtils.copyProperty(t,columnName,columnValue); list.add(t); } } return list; } }
java/** * 结果集处理接口的实现类 * 操作结果集并封装数组对象 * * @param <T> */ public class ArrayHandler<T> implements ResultSetHandler<T> { //行处理器 private RowProcessor<T> rowProcessor; public ArrayHandler(RowProcessor<T> rowProcessor) { this.rowProcessor = rowProcessor; } @Override public T handler(ResultSet resultSet) throws SQLException, IllegalAccessException, InstantiationException, InvocationTargetException { return rowProcessor.toArray(resultSet); } }
其中,行处理的逻辑是拿到结果集后交给自定义的行处理接口实现类方法处理,这样做的目的是统一代码的格式(见后续web项目中的使用)。
processor包(处理行):
java/** * 行处理器的实现类,将结果集中的数据获取并封装成数组 * @param <T> */ public class BaskRowProcessor<T> implements RowProcessor<T[]> { private Class arrayTClass; public BaskRowProcessor(Class arrayTClass) { this.arrayTClass = arrayTClass; } @Override public T[] toArray(ResultSet resultSet) throws SQLException { //创建数据容器 List<T> list = new ArrayList<>(); //遍历结果集 while(resultSet.next()){ T t = (T) resultSet.getObject(1); list.add(t); } if(list.size()==0||resultSet==null){ throw new RuntimeException("参数异常无法获取泛型数组"); }else { //创建数组 T[] ts = (T[]) Array.newInstance(arrayTClass, list.size()); //添加数据 for (int i = 0; i < list.size(); i++) { ts[i] = list.get(i); } return ts; } } }
通过结果集返回封装好的对象、列表、数组的功能已实现,接下类实现操作数据库返回结果集的功能:
core包:
javapublic class QueryRunner { private DruidDataSource dataSource; private ThreadLocal<Connection> local = new ThreadLocal<>(); public QueryRunner(DruidDataSource dataSource) { this.dataSource = dataSource; } //获取连接 private Connection getConnection() throws SQLException { Connection connection = local.get(); if(connection==null){ connection = dataSource.getConnection(); local.set(connection); } return connection; } //配置参数 private PreparedStatement getPreparedStatement(Connection connection,String sql,Object... args) throws SQLException { PreparedStatement statement = connection.prepareStatement(sql); for (int i = 0; i < args.length; i++) { statement.setObject(i+1,args[i]); } return statement; } //更新操作 public int update(String sql,Object... args) throws SQLException { Connection connection = getConnection(); PreparedStatement statement = getPreparedStatement(connection, sql, args); int i = statement.executeUpdate(); return i; } //查询操作 public <T> T query(ResultSetHandler<T> handler,String sql,Object... args) throws SQLException, InvocationTargetException, IllegalAccessException, InstantiationException { Connection connection = getConnection(); PreparedStatement statement = getPreparedStatement(connection, sql, args); ResultSet resultSet = statement.executeQuery(); T t = handler.handler(resultSet); return t; } }
至此,框架搭建完毕,接下来以用户创建的web项目举例:
数据库工具类:
javapublic class JDBCUtils { //德鲁伊连接池引用 private static DruidDataSource dataSource; static{ //创建德鲁伊连接池 dataSource = new DruidDataSource(); //获取配置信息 Properties properties = new Properties(); try { properties.load(JDBCUtils.class.getClassLoader().getResourceAsStream("DBConfig.properties")); String username = properties.getProperty("username"); String password = properties.getProperty("password"); String url = properties.getProperty("url"); String driverName = properties.getProperty("driverName"); dataSource.setUrl(url); dataSource.setUsername(username); dataSource.setPassword(password); dataSource.setDriverClassName(driverName); } catch (IOException e) { throw new RuntimeException(e); } } public static QueryRunner getQueryRunner(){ return new QueryRunner(dataSource); } }
controller层中对数据库进行操作:
java//---操作数据库--------------------------------------------------------------------------------------- @RequestMapping("/test13.action") public void test13() throws SQLException { //操作更新语句 JDBCUtils.getQueryRunner().update("update user set password = ? where username = ?","123456","zs"); } @RequestMapping("/test14.action") public void test14() throws SQLException, InvocationTargetException, IllegalAccessException, InstantiationException { //查询对象 User zs = JDBCUtils.getQueryRunner().query(new BeanHandler<>(User.class), "select * from user where username = ?", "zs"); System.out.println(zs); } @RequestMapping("/test15.action") public void test15() throws SQLException, InvocationTargetException, IllegalAccessException, InstantiationException { //查询对象列表 List<User> users = JDBCUtils.getQueryRunner().query(new BeanListHandler<>(User.class), "select * from user"); for (User user : users) { System.out.println("列表中的对象:"+user); } } @RequestMapping("/test16.action") public void test16() throws SQLException, InvocationTargetException, IllegalAccessException, InstantiationException { //查询所有的username String[] usernames = JDBCUtils.getQueryRunner().query(new ArrayHandler<>(new BaskRowProcessor<>(String.class)),"select username from user"); System.out.println(usernames); }
遇到的问题:
1.无法找到数据库连接的配置文件DBCfig.properties
解决方案:将配置文件放到src/main/resources文件夹中去,这样 资源文件才会被复制到target/classes 目录下。