手写Spring-MVC之前后置处理器与异常处理、数据库框架

news2024/7/7 16:25:20

Day48

手写Spring-MVC之前后置处理器与异常处理

前后置处理器

概念:从服务器获取的JSON数据可能是加密后的,因此服务端获取的时候需要进行解密(前置处理器)。

而从服务器传出的JSON数据可能需要加密,因此需要在处理返回值的时候进行加密(后置处理器)。

思路:

首先搭建前后置处理器的框架

和是否处理JSON格式的数据类似,需要根据注解判断controller层中的方法是否需要对JSON格式的数据进行解密或者返回数据进行加密,因此要添加两个注解@BeforeAdviser和@AfterAdviser

@Target(ElementType.PARAMETER)
@Retention(RetentionPolicy.RUNTIME)
public @interface BeforeAdviser {
}

@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface AfterAdviser {
}

controller层的方法需要添加相应的注解标识

/**
     * 使用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;

同时需要在参数描述类中和方法描述类中添加是否有相应注解的属性:

/**
 * 参数描述类
*/
@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注解
}

添加后监听器中封装部分也要进行相应修改:

//获取参数上是否有@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);
//获取方法上是否有@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注解,如果有则代表需要进行解密操作:

//解密
if(parameterDefinition.isBeforeAdviserHasOrNot()){
 //在这里进行具体的解密操作吗?
}

在处理返回值的时候会判断方法是否有@AfterAdviser注解,如果有则代表需要进行加密操作:

//加密
if(methodDefinition.isAfterAdviserHasOrNot()){
 //在这里进行加密操作吗?
}

进行具体的解密和加密操作:

前后置处理器的框架搭建好之后,会发现一个问题,如果在DispatcherServlet中进行具体的解密加密的话,那么在用户使用该框架的时候,就只能使用框架所规定的解密加密,这显然不具有灵活性。不同的用户解密、加密逻辑不同,所以这里的思路是在框架中只写抽象类,在DispatcherServlet中利用多态创建抽象类的继承类对象,调用继承类对象中的解密加密方法。而在web模块中用户可以自己重写一个解密加密抽象方法,这样调用的就是用户自定义的逻辑方法了。

抽象方法:

public 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中调用用户重写方法的逻辑。

用户自定义前后置处理器:

public 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()方法,在方法中获取配置文件信息,进而拿到注解类,通过注解类的注解信息拿到自定义前后置处理器类对象,调用其重写的解密加密处理方法。

使能注解:

@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
public @interface EnableAdviser {
 String adviserPackage();
}

注解类:

@EnableAdviser(adviserPackage= "com.qf.shop.web.adviser.BeforeAndAfterAdviser")

DispatcherServlet中:

private 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);
     

 }
//加密
if(methodDefinition.isAfterAdviserHasOrNot()){
 jsonString = adviserResolver.after(jsonString);
}
//解密
if(parameterDefinition.isBeforeAdviserHasOrNot()){
 jsonStr = adviserResolver.before(jsonStr);
}

异常

概念:DispatcherServlet中对于全局的异常需要进行处理,而具体如何处理也是由业务决定的,换言之是用户进行定义而非框架中写死。但是框架中又需要调用处理异常的方法,如何处理?-和前后置处理器一样,通过多态,servlet调用的是用户继承框架中抽象类的类重写的方法。

抽象类:

public 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);
 }
}

用户继承类:

public 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。

使能注解:

@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
public @interface EnableException {

 String exceptionPackage();
}

配置类:

/**
 * 当前项目的配置类
*/
@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:

private 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);

 }


try{
  //调用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包:根据获得的结果集实现封装功能的接口,包含结果集处理器接口和行处理器接口

/**
* 结果集处理器的接口
*实现类:BeanHandler(获取单个对象)、BeanListHandler(获取对象集合)
*/
public interface ResultSetHandler<T> {
 public T handler(ResultSet resultSet)throws SQLException,IllegalAccessException,InstantiationException, InvocationTargetException;
}
/**
* 行处理器的接口
* @param <T>
*/
public interface RowProcessor<T> {
 public T toArray(ResultSet resultSet) throws SQLException;
}

handler包:实现接口:

/**
* 结果集处理接口的实现类
* 操作结果集并封装实体类
* @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;
 }
}

/**
* 结果集处理接口的实现类
* 处理结果集并封装为集合
* @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;

 }
}

/**
* 结果集处理接口的实现类
* 操作结果集并封装数组对象
*
* @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包(处理行):

/**
* 行处理器的实现类,将结果集中的数据获取并封装成数组
* @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包:

public 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项目举例:

数据库工具类:

public 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层中对数据库进行操作:

//---操作数据库---------------------------------------------------------------------------------------
@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 目录下。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/1894287.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

玩机进阶教程----MTK芯片杂牌机 小品牌机型解除bl锁以及root的操作步骤解析

在玩机过程中会遇到很多小品牌机型或者杂牌机类的。大多都使用mtk芯片。而且基本很少有官方线刷包。在这些机型中玩机首先我们要想办法导出系统来制作线刷包。以免后续解锁bl或者root出现未知故障可以恢复原系统。 那么对于这些机型该如何进行备份固件和root呢。通过博文可以初…

图书借阅小程序论文(设计)开题报告

一、课题的背景和意义 近些年来&#xff0c;随着移动互联网巅峰时期的来临&#xff0c;互联网产业逐渐趋于“小、轻、微”的方向发展&#xff0c;符合轻应用时代特点的各类技术受到了不同领域的广泛关注。在诸多产品中&#xff0c;被誉为“运行着程序的网站”之名的微信小程序…

开始尝试从0写一个项目--前端(一)

基础项目构建 创建VUE初始工程 确保自己下载了node.js和npm node -v //查看node.js的版本 npm -v //查看npm的版本 npm i vue/cli -g //安装VUE CLI 创建 以管理员身份运行 输入&#xff1a;vue ui 就会进入 点击创建 自定义项目名字&#xff0c;选择npm管理 结…

什么是多态(Polymorphism)

什么是多态&#xff08;Polymorphism&#xff09; 1、多态的基本概念2、多态的实现方式2.1 方法重载&#xff08;Overloading&#xff09;2.2 方法重写&#xff08;Overriding&#xff09;2.3 接口和抽象类 3、为什么要使用多态&#xff1f;4、结论 &#x1f496;The Begin&…

启明智显Model3A芯片方案7寸高清触摸屏ZX7D00CM21S:开箱、设置与实操全攻略指南

一、背景 本指南将详细介绍启明智显的Model3A芯片方案下的7寸高清触摸屏ZX7D00CM21S的开箱步骤、基础设置以及实操应用。无论您是电子爱好者、开发者还是工程师&#xff0c;这份指南都能助您快速上手并充分利用这款触摸屏的各项功能。 二、硬件介绍 ZX7D00CM21S 7寸高清触摸屏是…

500mA、低压差、低噪声、超快、无需旁路电容的CMOS LDO稳压器RT9013

一般描述 RT9013 SOT23-5封装的外观和丝印 RT9013 是一款高性能的 500mA LDO 稳压器&#xff0c;具有极高的 PSRR 和超低压差。非常适合具有苛刻性能和空间要求的便携式射频和无线应用。 RT9013的静态电流低至25μA&#xff0c;进一步延长了电池的使用寿命。RT9013 也适用于低…

kafka的工作原理与常见问题

定义 kafka是一个分布式的基于发布/订阅模式的消息队列&#xff08;message queue&#xff09;&#xff0c;主要应用于大数据的实时处理领域 消息队列工作原理 kafka的组成结构 kafka的基础架构主要有broker、生产者、消费者组构成&#xff0c;还包括zookeeper. 生产者负责发送…

【Android源码】Gerrit安装

前言 如果你打开 https://android.googlesource.com/platform/manifest&#xff0c;就会发现&#xff0c;google官方管理Android源码&#xff0c;使用的是Gerrit。Android系统源码是非常大的&#xff0c;用Git肯定是不适合。对于大型项目&#xff0c;得用Gerrit&#xff0c;今…

小龙虾优化24种机器学习多输入单输出回归|时序预测模型

小龙虾优化24种机器学习多输入单输出回归|时序预测模型 文章目录 小龙虾优化24种机器学习多输入单输出回归|时序预测模型前言一、小龙虾优化基本原理二、优化机器学习模型1.COA-CNN-BiGRU-Attention回归模型2.基于小龙虾优化支持向量机的数据回归预测Matlab程序COA-SVM 多特征输…

Web应用防火墙用在哪些场景?

WAF是Web Application Firewall的缩写&#xff0c;翻译为“Web应用防火墙”是一种网络安全设备或服务&#xff0c;用于保护Web应用程序免受各种网络攻击和漏洞的影响。 WAF特别设计用于识别和阻止特定于Web应用程序的攻击&#xff0c;例如SQL注入、跨站脚本(XSS)、跨站请求伪造…

014-GeoGebra基础篇-快速解决滑动条的角度无法输入问题

有客户反馈&#xff0c;他的Geogebra一直有个bug&#xff0c;那就是输入角度最大值时总不按照他设定的展示&#xff0c;快被气炸了~ 目录 一、问题复现&#xff08;1&#xff09;插入一个滑动条&#xff08;2&#xff09;选择Angle&#xff08;3&#xff09;输入90&#xff0c;…

MySQL学习(8):约束

1.什么是约束 约束是作用于表中字段上的规则&#xff0c;以限制表中数据&#xff0c;保证数据的正确性、有效性、完整性 约束分为以下几种&#xff1a; not null非空约束限制该字段的数据不能为nullunique唯一约束保证该字段的所有数据都是唯一、不重复的primary key主键约束…

linux中与网络有关的命令

本文的命令总览 ifconfig命令 在 Linux 系统中&#xff0c;ifconfig 命令用于配置和显示网络接口的信息&#xff0c;包括 IP 地址、MAC 地址、网络状态等。同时我们也可以利用ifconfig 命令设置网络接口对应的ip地址&#xff0c;子网掩码等 当你使用 ifconfig 命令时&#xf…

Oracle数据库中RETURNING子句

RETURNING子句允许您检索插入、删除或更新所修改的列&#xff08;以及基于列的表达式&#xff09;的值。如果不使用RETURNING&#xff0c;则必须在DML语句完成后运行SELECT语句&#xff0c;才能获得更改列的值。因此&#xff0c;RETURNING有助于避免再次往返数据库&#xff0c;…

SpringBoot 启动流程一

SpringBoot启动流程一 我们首先创建一个新的springboot工程 我们不添加任何依赖 查看一下pom文件 我们创建一个文本文档 记录我们的工作流程 我们需要的是通过打断点实现 我们首先看一下启动响应类 package com.bigdata1421.start_up;import org.springframework.boot.Spr…

Element中的日期时间选择器DateTimePicker和级联选择器Cascader

简述&#xff1a;在Element UI框架中&#xff0c;Cascader&#xff08;级联选择器&#xff09;和DateTimePicker&#xff08;日期时间选择器&#xff09;是两个非常实用且常用的组件&#xff0c;它们分别用于日期选择和多层级选择&#xff0c;提供了丰富的交互体验和便捷的数据…

Chart.js四个示例

示例代码在图片后面&#xff0c;点赞加关注&#xff0c;谢谢 条形图 雷达图 折线图 圆环图 完整例子代码 具体代码在干什么看粗体加重的注释 <!DOCTYPE html> <html lang"en"> <head> <meta charset"UTF-8"> <me…

TensorFlow与PyTorch的对比与选择(Python深度学习)

目录 一、TensorFlow与PyTorch概述 1.1 TensorFlow 1.2 PyTorch 二、性能对比 2.1 静态图与动态图 2.2 分布式计算 三、易用性与灵活性 3.1 易用性 3.2 灵活性 四、社区支持 4.1 TensorFlow 4.2 PyTorch 五、实际案例与代码示例 5.1 TensorFlow案例&#xff1a;手…

关于 lvds 屏幕的一些知识

网上的截图&#xff1a; lvds的 通道。 lvds 的协议 关于 sync 模式与 de 模式&#xff1a; ------------------------------------------------------------------------------------------------------------------ 芯片的数据手册的看法。 这个手册 &#xff0c;就指明了…